Skip to content

Commit 46ef77b

Browse files
authored
Merge pull request #162 from code0-tech/feat/custom-inputs
New custom inputs
2 parents 907a9de + 493d7f4 commit 46ef77b

2 files changed

Lines changed: 482 additions & 9 deletions

File tree

src/util/schema.util.ts

Lines changed: 142 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,63 @@ export interface ListFileInput extends Input {
132132
mimetype?: string;
133133
}
134134

135+
/**
136+
* Represents a list of select inputs.
137+
* Emitted for any array/list whose element is a select type (a primitive
138+
* literal union or a single string/number literal — e.g. `LIST<HTTP_METHOD>`,
139+
* `('GET' | 'POST')[]`) so the UI can render a dedicated multi-select instead
140+
* of the generic list input its underlying type would otherwise produce.
141+
*/
142+
export interface ListSelectInput extends Omit<ListInput, 'input'> {
143+
input?: "list-select";
144+
}
145+
146+
/**
147+
* Represents a list of boolean inputs.
148+
* Emitted for any array/list of plain booleans (e.g. `LIST<BOOLEAN>`,
149+
* `boolean[]`) so the UI can render a dedicated multi-boolean input instead of
150+
* the generic list of individual boolean inputs its underlying type would
151+
* otherwise produce. Carries the per-item schemas in `items`, like a generic
152+
* {@link ListInput}.
153+
*/
154+
export interface ListBooleanInput extends Omit<ListInput, 'input'> {
155+
input?: "list-boolean";
156+
}
157+
158+
/**
159+
* Represents a list of number inputs.
160+
* Emitted for any array/list of plain numbers (e.g. `LIST<NUMBER>`, `number[]`)
161+
* so the UI can render a dedicated multi-number input instead of the generic
162+
* list of individual number inputs its underlying type would otherwise produce.
163+
* Carries the per-item schemas in `items`, like a generic {@link ListInput}.
164+
*/
165+
export interface ListNumberInput extends Omit<ListInput, 'input'> {
166+
input?: "list-number";
167+
}
168+
169+
/**
170+
* Represents a list of text inputs.
171+
* Emitted for any array/list of plain strings (e.g. `LIST<TEXT>`, `string[]`)
172+
* so the UI can render a dedicated multi-text input instead of the generic list
173+
* of individual text inputs its underlying type would otherwise produce. Carries
174+
* the per-item schemas in `items`, like a generic {@link ListInput}.
175+
*/
176+
export interface ListTextInput extends Omit<ListInput, 'input'> {
177+
input?: "list-text";
178+
}
179+
180+
/**
181+
* Represents a list of sub-flow inputs.
182+
* Emitted for any array/list whose element is a callable/sub-flow type (e.g.
183+
* `LIST<FLOW>`, `(() => void)[]`) so the UI can render a dedicated multi-sub-flow
184+
* input instead of the generic list of individual sub-flow inputs its underlying
185+
* type would otherwise produce. Carries the per-item schemas in `items`, like a
186+
* generic {@link ListInput}.
187+
*/
188+
export interface ListSubFlowInput extends Omit<ListInput, 'input'> {
189+
input?: "list-sub-flow";
190+
}
191+
135192
/**
136193
* Represents a data object input type with structured properties.
137194
* Includes property definitions and required field tracking.
@@ -176,6 +233,11 @@ export type Schema =
176233
| ColorInput
177234
| FileInput
178235
| ListFileInput
236+
| ListSelectInput
237+
| ListBooleanInput
238+
| ListNumberInput
239+
| ListTextInput
240+
| ListSubFlowInput
179241
| DataInput
180242
| ListInput
181243
| TypeInput
@@ -335,7 +397,8 @@ export const getSchema = (
335397
// Check primitive literal union first (e.g., "a" | "b" | "c") or a single
336398
// string/number literal (e.g., "GET"). A bare literal has only one allowed
337399
// value, so it should still surface as a select rather than a free-form text/number input.
338-
if (isPrimitiveLiteralUnion(parameterType) || isStringOrNumberLiteral(parameterType)) {
400+
// (Boolean has already been handled above; see isSelectType.)
401+
if (isSelectType(parameterType)) {
339402
return {input: "select", type, ...combinedSuggestions};
340403
}
341404
if (isNumber(parameterType)) {
@@ -364,13 +427,45 @@ export const getSchema = (
364427
return {input: "list-file", type, mimetype, ...combinedSuggestions};
365428
}
366429

430+
// Per-item schemas, computed the same way for a generic list and a
431+
// list-select (whose `items` mirror a normal list's). A union element is
432+
// split into one schema per member; a single element yields one schema.
367433
const itemSchemas = itemTypes.flatMap(itemType => {
368-
const itemTypes = itemType.isUnion() ? itemType.types : [itemType];
369-
return itemTypes.map((itemType) =>
370-
getSchema(checker, node, itemType, functionDeclarations, functions, suggestions, undefined, visited, recursionCache)
434+
const memberTypes = itemType.isUnion() ? itemType.types : [itemType];
435+
return memberTypes.map((memberType) =>
436+
getSchema(checker, node, memberType, functionDeclarations, functions, suggestions, undefined, visited, recursionCache)
371437
)
372438
})
373439

440+
// A list of a select type (LIST<HTTP_METHOD>, ('GET' | 'POST')[], ...)
441+
// surfaces a dedicated multi-select. Its `items` are the element schemas,
442+
// exactly like a generic list — only the input kind differs so the UI can
443+
// render a combined multi-select. Checked before the plain-primitive
444+
// cases below because a single literal (e.g. LIST<1>) is a select, not a
445+
// plain number.
446+
if (itemTypes.length === 1 && isSelectType(itemTypes[0])) {
447+
return {input: "list-select", type, items: itemSchemas, ...combinedSuggestions};
448+
}
449+
450+
// A homogeneous list of a plain primitive surfaces a dedicated
451+
// multi-<primitive> input. Its `items` are the element schemas, exactly
452+
// like a generic list — only the input kind differs so the UI can render
453+
// a combined multi-<primitive> input. Ordering mirrors the top-level
454+
// primitive checks: boolean first, then number, then string — select
455+
// literals have already been handled above. Custom-input elements
456+
// (DATE → date, COLOR → color, FILE → file) are structurally
457+
// intersection/object types that fail these checks, so they keep the
458+
// per-item schema of the generic list below.
459+
if (itemTypes.length === 1) {
460+
const element = itemTypes[0];
461+
if (isBoolean(element)) return {input: "list-boolean", type, items: itemSchemas, ...combinedSuggestions};
462+
if (isNumber(element)) return {input: "list-number", type, items: itemSchemas, ...combinedSuggestions};
463+
if (isString(element)) return {input: "list-text", type, items: itemSchemas, ...combinedSuggestions};
464+
// A homogeneous list of a callable/sub-flow element surfaces a
465+
// dedicated multi-sub-flow input. Checked after the primitives (none
466+
// of which are callable) and before the generic list fallback.
467+
if (isSubFlow(element)) return {input: "list-sub-flow", type, items: itemSchemas, ...combinedSuggestions};
468+
}
374469

375470
return {
376471
input: "list",
@@ -484,6 +579,19 @@ export const getSchema = (
484579
* @param nodeSchema - The schema derived from the node's concrete (narrowed) parameter type
485580
* @returns A single merged schema
486581
*/
582+
// Every list input kind whose schema carries per-element `items` — the generic
583+
// list and all specialized list-* variants that mirror it. mergeSchemas recurses
584+
// into these so element-level suggestions are preserved. list-file is excluded:
585+
// it carries a `mimetype`, not `items`.
586+
const LIST_INPUTS = new Set<string>([
587+
"list",
588+
"list-select",
589+
"list-boolean",
590+
"list-number",
591+
"list-text",
592+
"list-sub-flow",
593+
]);
594+
487595
export const mergeSchemas = (
488596
functionSchema: Schema | undefined,
489597
nodeSchema: Schema,
@@ -520,10 +628,20 @@ export const mergeSchemas = (
520628
};
521629
}
522630

523-
if (functionSchema.input === "list") {
524-
const fItems = functionSchema.items ?? [];
631+
// The generic list and every specialized list-* variant (list-select,
632+
// list-boolean/number/text, list-sub-flow) carry their element schemas in
633+
// `items`. Merge those pairwise so element-level suggestions — e.g. the
634+
// per-literal values on a list-select or the sub-flow function suggestions on
635+
// a LIST<CONSUMER<T>> element — survive the merge instead of being dropped in
636+
// favour of the suggestion-less function schema. Suggestions must never be
637+
// lost, whatever the list kind. Only merges when both sides are the same list
638+
// kind. (list-file carries a `mimetype`, not `items`, so it is not listed.)
639+
if (LIST_INPUTS.has(functionSchema.input as string)) {
640+
const fItems = (functionSchema as ListInput).items ?? [];
525641
const nItems =
526-
nodeSchema.input === "list" ? (nodeSchema.items ?? []) : [];
642+
nodeSchema.input === functionSchema.input
643+
? ((nodeSchema as ListInput).items ?? [])
644+
: [];
527645
const items =
528646
fItems.length === nItems.length && fItems.length > 0
529647
? fItems.map((f, i) => mergeSchemas(f, nItems[i]))
@@ -821,6 +939,23 @@ function isStringOrNumberLiteral(type: ts.Type): boolean {
821939
);
822940
}
823941

942+
/**
943+
* Checks whether a type surfaces as a select input.
944+
*
945+
* A select is a primitive literal union (e.g. `"a" | "b" | "c"`) or a single
946+
* string/number literal (e.g. `"GET"`). Boolean is excluded so that `true` /
947+
* `boolean` continue to render as a boolean input — mirroring the ordering in
948+
* {@link getSchema}, where the boolean check runs first. Used both for the
949+
* plain select input and to detect a {@link ListSelectInput} element.
950+
*
951+
* @param type - The type to check
952+
* @returns True if the type surfaces as a select
953+
*/
954+
function isSelectType(type: ts.Type): boolean {
955+
if (isBoolean(type)) return false;
956+
return isPrimitiveLiteralUnion(type) || isStringOrNumberLiteral(type);
957+
}
958+
824959
/**
825960
* Checks if a type is a union of primitive types only.
826961
*

0 commit comments

Comments
 (0)