mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 08:02:18 +00:00
feat: oneOf inputs (#3893)
* feat: oneOf input for typescript * feat: oneOf order and light form * feat: oneOf UI builder
This commit is contained in:
@@ -11,7 +11,7 @@ use regex::Regex;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashSet;
|
||||
use swc_ecma_visit::{noop_visit_type, Visit, VisitWith};
|
||||
use windmill_parser::{json_to_typ, Arg, MainArgSignature, ObjectProperty, Typ};
|
||||
use windmill_parser::{json_to_typ, Arg, MainArgSignature, ObjectProperty, OneOfVariant, Typ};
|
||||
|
||||
use swc_common::{sync::Lrc, FileName, SourceMap, SourceMapper, Span, Spanned};
|
||||
use swc_ecma_ast::{
|
||||
@@ -401,6 +401,79 @@ fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
|
||||
};
|
||||
(tstype_to_typ(&types[other_p]).0, true)
|
||||
} else {
|
||||
if types.len() > 1 {
|
||||
let one_of_values: Vec<OneOfVariant> = types
|
||||
.into_iter()
|
||||
.map_while(|x| match &**x {
|
||||
TsType::TsTypeLit(TsTypeLit { members, .. }) => {
|
||||
let label = members
|
||||
.iter()
|
||||
.find_map(|y| match y {
|
||||
TsTypeElement::TsPropertySignature(
|
||||
TsPropertySignature { key, type_ann, .. },
|
||||
) => match (&**key, type_ann) {
|
||||
(Expr::Ident(Ident { sym, .. }), type_ann) => {
|
||||
if sym.to_string() == "label" {
|
||||
type_ann.as_ref().map(|z| match &*z.type_ann {
|
||||
TsType::TsLitType(TsLitType {
|
||||
lit: TsLit::Str(Str { value, .. }),
|
||||
..
|
||||
}) => Some(value.to_string()),
|
||||
_ => None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.flatten();
|
||||
|
||||
match label {
|
||||
Some(label) => {
|
||||
let properties: Vec<ObjectProperty> = members
|
||||
.into_iter()
|
||||
.filter_map(|x| match x {
|
||||
TsTypeElement::TsPropertySignature(
|
||||
TsPropertySignature { key, type_ann, .. },
|
||||
) => match (*key.to_owned(), type_ann) {
|
||||
(Expr::Ident(Ident { sym, .. }), type_ann) => {
|
||||
Some(ObjectProperty {
|
||||
key: sym.to_string(),
|
||||
typ: type_ann
|
||||
.as_ref()
|
||||
.map(|typ| {
|
||||
Box::new(
|
||||
tstype_to_typ(
|
||||
&*typ.type_ann,
|
||||
)
|
||||
.0,
|
||||
)
|
||||
})
|
||||
.unwrap_or(Box::new(Typ::Unknown)),
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
Some(OneOfVariant { label, properties })
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
if one_of_values.len() == types.len() {
|
||||
return (Typ::OneOf(one_of_values), false);
|
||||
}
|
||||
}
|
||||
|
||||
let literals = types
|
||||
.into_iter()
|
||||
.filter(|x| match ***x {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"collaborators": [
|
||||
"Ruben Fiszel <ruben@windmill.dev>"
|
||||
],
|
||||
"version": "1.335.0",
|
||||
"version": "1.345.2",
|
||||
"files": [
|
||||
"windmill_parser_wasm_bg.wasm",
|
||||
"windmill_parser_wasm.js",
|
||||
|
||||
@@ -97,6 +97,15 @@ function getInt32Memory0() {
|
||||
return cachedInt32Memory0;
|
||||
}
|
||||
|
||||
let cachedFloat64Memory0 = null;
|
||||
|
||||
function getFloat64Memory0() {
|
||||
if (cachedFloat64Memory0 === null || cachedFloat64Memory0.byteLength === 0) {
|
||||
cachedFloat64Memory0 = new Float64Array(wasm.memory.buffer);
|
||||
}
|
||||
return cachedFloat64Memory0;
|
||||
}
|
||||
|
||||
function addHeapObject(obj) {
|
||||
if (heap_next === heap.length) heap.push(heap.length + 1);
|
||||
const idx = heap_next;
|
||||
@@ -115,15 +124,6 @@ function getStringFromWasm0(ptr, len) {
|
||||
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
|
||||
}
|
||||
|
||||
let cachedFloat64Memory0 = null;
|
||||
|
||||
function getFloat64Memory0() {
|
||||
if (cachedFloat64Memory0 === null || cachedFloat64Memory0.byteLength === 0) {
|
||||
cachedFloat64Memory0 = new Float64Array(wasm.memory.buffer);
|
||||
}
|
||||
return cachedFloat64Memory0;
|
||||
}
|
||||
|
||||
let cachedBigInt64Memory0 = null;
|
||||
|
||||
function getBigInt64Memory0() {
|
||||
@@ -584,6 +584,10 @@ async function __wbg_load(module, imports) {
|
||||
function __wbg_get_imports() {
|
||||
const imports = {};
|
||||
imports.wbg = {};
|
||||
imports.wbg.__wbg_eval_75d97e58e74fc19b = function(arg0, arg1) {
|
||||
const ret = eval(getStringFromWasm0(arg0, arg1));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
|
||||
takeObject(arg0);
|
||||
};
|
||||
@@ -604,22 +608,6 @@ function __wbg_get_imports() {
|
||||
const ret = typeof(getObject(arg0)) === 'bigint';
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_bigint_from_i64 = function(arg0) {
|
||||
const ret = arg0;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_jsval_eq = function(arg0, arg1) {
|
||||
const ret = getObject(arg0) === getObject(arg1);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_bigint_from_u64 = function(arg0) {
|
||||
const ret = BigInt.asUintN(64, arg0);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_error_new = function(arg0, arg1) {
|
||||
const ret = new Error(getStringFromWasm0(arg0, arg1));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_number_get = function(arg0, arg1) {
|
||||
const obj = getObject(arg1);
|
||||
const ret = typeof(obj) === 'number' ? obj : undefined;
|
||||
@@ -635,8 +623,20 @@ function __wbg_get_imports() {
|
||||
const ret = getObject(arg0) in getObject(arg1);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbg_eval_4ecb84501b3abc94 = function(arg0, arg1) {
|
||||
const ret = eval(getStringFromWasm0(arg0, arg1));
|
||||
imports.wbg.__wbindgen_bigint_from_i64 = function(arg0) {
|
||||
const ret = arg0;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_jsval_eq = function(arg0, arg1) {
|
||||
const ret = getObject(arg0) === getObject(arg1);
|
||||
return ret;
|
||||
};
|
||||
imports.wbg.__wbindgen_bigint_from_u64 = function(arg0) {
|
||||
const ret = BigInt.asUintN(64, arg0);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_error_new = function(arg0, arg1) {
|
||||
const ret = new Error(getStringFromWasm0(arg0, arg1));
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_jsval_loose_eq = function(arg0, arg1) {
|
||||
|
||||
Binary file not shown.
@@ -24,6 +24,13 @@ pub struct ObjectProperty {
|
||||
pub typ: Box<Typ>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug, PartialEq)]
|
||||
#[serde(rename_all(serialize = "lowercase"))]
|
||||
pub struct OneOfVariant {
|
||||
pub label: String,
|
||||
pub properties: Vec<ObjectProperty>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug, PartialEq)]
|
||||
#[serde(rename_all(serialize = "lowercase"))]
|
||||
pub enum Typ {
|
||||
@@ -38,6 +45,7 @@ pub enum Typ {
|
||||
Email,
|
||||
Sql,
|
||||
Object(Vec<ObjectProperty>),
|
||||
OneOf(Vec<OneOfVariant>),
|
||||
Unknown,
|
||||
}
|
||||
|
||||
|
||||
Generated
+4
-4
@@ -52,7 +52,7 @@
|
||||
"vscode-languageclient": "~9.0.1",
|
||||
"vscode-uri": "~3.0.8",
|
||||
"vscode-ws-jsonrpc": "~3.1.0",
|
||||
"windmill-parser-wasm": "^1.335.0",
|
||||
"windmill-parser-wasm": "^1.345.2",
|
||||
"windmill-sql-datatype-parser-wasm": "^1.318.0",
|
||||
"y-monaco": "^0.1.4",
|
||||
"y-websocket": "^1.5.0",
|
||||
@@ -10261,9 +10261,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/windmill-parser-wasm": {
|
||||
"version": "1.335.0",
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm/-/windmill-parser-wasm-1.335.0.tgz",
|
||||
"integrity": "sha512-a6UzDKRt1InP0FgZHZ/ZhkoEI+AgCXirZ4yq9hTgL4o9PGRId9pl8Nk18R3CEJ9XaCd1LX6+GbRppnBtThyrvA=="
|
||||
"version": "1.345.2",
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm/-/windmill-parser-wasm-1.345.2.tgz",
|
||||
"integrity": "sha512-JTAx5zOZgJG/anZ+YZAD7MtWKcr29don0SOrsAHRUenmAtysUqxFYx1dHXJIilU0SxaSeeGblaTTiPHypAIJVw=="
|
||||
},
|
||||
"node_modules/windmill-sql-datatype-parser-wasm": {
|
||||
"version": "1.318.0",
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
"@popperjs/core": "^2.11.6",
|
||||
"@redocly/json-to-json-schema": "^0.0.1",
|
||||
"@tanstack/svelte-table": "^8.9.9",
|
||||
"@windmill-labs/svelte-dnd-action": "latest",
|
||||
"ag-charts-community": "^9.0.1",
|
||||
"ag-charts-enterprise": "^9.0.1",
|
||||
"ag-grid-community": "^31.0.0",
|
||||
@@ -122,7 +123,6 @@
|
||||
"quill": "^1.3.7",
|
||||
"svelte-carousel": "^1.0.25",
|
||||
"svelte-chartjs": "^3.1.5",
|
||||
"@windmill-labs/svelte-dnd-action": "latest",
|
||||
"svelte-exmarkdown": "^3.0.3",
|
||||
"svelte-infinite-loading": "^1.3.8",
|
||||
"svelte-portal": "^2.2.1",
|
||||
@@ -132,7 +132,7 @@
|
||||
"vscode-languageclient": "~9.0.1",
|
||||
"vscode-uri": "~3.0.8",
|
||||
"vscode-ws-jsonrpc": "~3.1.0",
|
||||
"windmill-parser-wasm": "^1.335.0",
|
||||
"windmill-parser-wasm": "^1.345.2",
|
||||
"windmill-sql-datatype-parser-wasm": "^1.318.0",
|
||||
"y-monaco": "^0.1.4",
|
||||
"y-websocket": "^1.5.0",
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface SchemaProperty {
|
||||
dateFormat?: string
|
||||
title?: string
|
||||
placeholder?: string
|
||||
oneOf?: SchemaProperty[]
|
||||
}
|
||||
|
||||
export interface ModalSchemaProperty {
|
||||
@@ -69,6 +70,7 @@ export interface ModalSchemaProperty {
|
||||
dateFormat?: string
|
||||
title?: string
|
||||
placeholder?: string
|
||||
oneOf?: SchemaProperty[]
|
||||
}
|
||||
|
||||
export function modalToSchema(schema: ModalSchemaProperty): SchemaProperty {
|
||||
@@ -94,7 +96,8 @@ export function modalToSchema(schema: ModalSchemaProperty): SchemaProperty {
|
||||
nullable: schema.nullable,
|
||||
dateFormat: schema.dateFormat,
|
||||
title: schema.title,
|
||||
placeholder: schema.placeholder
|
||||
placeholder: schema.placeholder,
|
||||
oneOf: schema.oneOf
|
||||
}
|
||||
}
|
||||
export type Schema = {
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
import autosize from '$lib/autosize'
|
||||
import PasswordArgInput from './PasswordArgInput.svelte'
|
||||
import Password from './Password.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import SchemaFormDnd from './schema/SchemaFormDND.svelte'
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
|
||||
@@ -36,6 +38,7 @@
|
||||
export let format: string = ''
|
||||
export let contentEncoding: 'base64' | 'binary' | undefined = undefined
|
||||
export let type: string | undefined = undefined
|
||||
export let oneOf: SchemaProperty[] | undefined = undefined
|
||||
export let required = false
|
||||
export let pattern: undefined | string = undefined
|
||||
export let valid = true
|
||||
@@ -75,6 +78,20 @@
|
||||
export let editor: SimpleEditor | undefined = undefined
|
||||
export let orderEditable = false
|
||||
|
||||
let oneOfSelected: string | undefined = undefined
|
||||
function updateOneOfSelected(oneOf: SchemaProperty[] | undefined) {
|
||||
if (oneOf && oneOf.length >= 2 && !oneOfSelected) {
|
||||
oneOfSelected = oneOf[0]['title']
|
||||
}
|
||||
}
|
||||
$: updateOneOfSelected(oneOf)
|
||||
function updateOneOfSelectedValue(oneOfSelected: string | undefined) {
|
||||
if (oneOfSelected) {
|
||||
value = { label: oneOfSelected }
|
||||
}
|
||||
}
|
||||
$: updateOneOfSelectedValue(oneOfSelected)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let ignoreValueUndefined = false
|
||||
@@ -480,7 +497,96 @@
|
||||
{/if}
|
||||
</div>
|
||||
{:else if inputCat == 'object' || inputCat == 'resource-object' || isListJson}
|
||||
{#if properties && Object.keys(properties).length > 0}
|
||||
{#if oneOf && oneOf.length >= 2}
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
{#if oneOf && oneOf.length >= 2}
|
||||
<ToggleButtonGroup bind:selected={oneOfSelected}>
|
||||
{#each oneOf as obj}
|
||||
<ToggleButton value={obj.title} label={obj.title} />
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
|
||||
{#if oneOfSelected}
|
||||
{@const objIdx = oneOf.findIndex((o) => o.title === oneOfSelected)}
|
||||
{@const obj = oneOf[objIdx]}
|
||||
{#if obj && obj.properties && Object.keys(obj.properties).length > 0}
|
||||
<div class="p-4 pl-8 border rounded w-full">
|
||||
{#if orderEditable}
|
||||
<SchemaFormDnd
|
||||
{onlyMaskPassword}
|
||||
{disablePortal}
|
||||
{disabled}
|
||||
schema={{
|
||||
properties: Object.fromEntries(
|
||||
Object.entries(obj.properties).filter(([k, v]) => k !== 'label')
|
||||
),
|
||||
order: obj.order?.filter((k) => k !== 'label') ?? undefined,
|
||||
$schema: '',
|
||||
required: obj.required ?? [],
|
||||
type: 'object'
|
||||
}}
|
||||
args={value}
|
||||
dndType={`nested-${title}`}
|
||||
on:reorder={(e) => {
|
||||
if (oneOf && oneOf[objIdx]) {
|
||||
const keys = e.detail
|
||||
oneOf[objIdx].order = keys
|
||||
}
|
||||
}}
|
||||
on:change
|
||||
/>
|
||||
{:else}
|
||||
<SchemaForm
|
||||
{onlyMaskPassword}
|
||||
{disablePortal}
|
||||
{disabled}
|
||||
noDelete
|
||||
schema={{
|
||||
properties: Object.fromEntries(
|
||||
Object.entries(obj.properties).filter(([k, v]) => k !== 'label')
|
||||
),
|
||||
order: obj.order?.filter((k) => k !== 'label') ?? undefined,
|
||||
$schema: '',
|
||||
required: obj.required ?? [],
|
||||
type: 'object'
|
||||
}}
|
||||
bind:args={value}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if disabled}
|
||||
<textarea disabled />
|
||||
{:else}
|
||||
<JsonEditor
|
||||
bind:editor
|
||||
on:focus={(e) => {
|
||||
dispatch('focus')
|
||||
}}
|
||||
on:blur={(e) => {
|
||||
dispatch('blur')
|
||||
}}
|
||||
code={rawValue}
|
||||
bind:value
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else if disabled}
|
||||
<textarea disabled />
|
||||
{:else}
|
||||
<JsonEditor
|
||||
bind:editor
|
||||
on:focus={(e) => {
|
||||
dispatch('focus')
|
||||
}}
|
||||
on:blur={(e) => {
|
||||
dispatch('blur')
|
||||
}}
|
||||
code={rawValue}
|
||||
bind:value
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if properties && Object.keys(properties).length > 0}
|
||||
<div class="p-4 pl-8 border rounded-md w-full">
|
||||
{#if orderEditable}
|
||||
<SchemaFormDnd
|
||||
|
||||
@@ -140,6 +140,8 @@
|
||||
? schema.properties[opened].type
|
||||
: schema.properties[opened].format === 'resource-s3_object'
|
||||
? 'S3'
|
||||
: schema.properties[opened].oneOf && schema.properties[opened].oneOf.length >= 2
|
||||
? 'oneOf'
|
||||
: 'object'
|
||||
: ''
|
||||
}
|
||||
@@ -328,6 +330,7 @@
|
||||
<PropertyEditor
|
||||
bind:description={schema.properties[argName].description}
|
||||
type={schema.properties[argName].type}
|
||||
bind:oneOf={schema.properties[argName].oneOf}
|
||||
bind:pattern={schema.properties[argName].pattern}
|
||||
bind:enum_={schema.properties[argName].enum}
|
||||
bind:format={schema.properties[argName].format}
|
||||
@@ -355,6 +358,7 @@
|
||||
bind:selected
|
||||
on:selected={(e) => {
|
||||
const isS3 = e.detail == 'S3'
|
||||
const isOneOf = e.detail == 'oneOf'
|
||||
|
||||
selected = e.detail
|
||||
|
||||
@@ -382,6 +386,39 @@
|
||||
type: 'object',
|
||||
format: 'resource-s3_object'
|
||||
}
|
||||
} else if (isOneOf) {
|
||||
schema.properties[argName] = {
|
||||
...emptyProperty,
|
||||
type: 'object',
|
||||
oneOf: [
|
||||
{
|
||||
title: 'Option 1',
|
||||
type: 'object',
|
||||
properties: {
|
||||
label: {
|
||||
type: 'string',
|
||||
enum: ['Option 1']
|
||||
},
|
||||
property_1: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Option 2',
|
||||
type: 'object',
|
||||
properties: {
|
||||
label: {
|
||||
type: 'string',
|
||||
enum: ['Option 2']
|
||||
},
|
||||
property_2: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
} else {
|
||||
schema.properties[argName] = {
|
||||
...emptyProperty,
|
||||
@@ -391,7 +428,7 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#each [['String', 'string'], ['Number', 'number'], ['Integer', 'integer'], ['Object', 'object'], ['Array', 'array'], ['Boolean', 'boolean'], ['S3 Object', 'S3']] as x}
|
||||
{#each [['String', 'string'], ['Number', 'number'], ['Integer', 'integer'], ['Object', 'object'], ['OneOf', 'oneOf'], ['Array', 'array'], ['Boolean', 'boolean'], ['S3 Object', 'S3']] as x}
|
||||
<ToggleButton value={x[1]} label={x[0]} />
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
@@ -407,6 +444,7 @@
|
||||
{lightweightMode}
|
||||
bind:nullable={schema.properties[argName].nullable}
|
||||
type={schema.properties[argName].type}
|
||||
bind:oneOf={schema.properties[argName].oneOf}
|
||||
bind:format={schema.properties[argName].format}
|
||||
contentEncoding={schema.properties[argName].contentEncoding}
|
||||
required={schema.required?.includes(argName) ?? false}
|
||||
|
||||
@@ -390,6 +390,7 @@
|
||||
bind:description={schema.properties[argName].description}
|
||||
bind:value={arg.value}
|
||||
type={schema.properties[argName].type}
|
||||
oneOf={schema.properties[argName].oneOf}
|
||||
required={schema.required.includes(argName)}
|
||||
bind:pattern={schema.properties[argName].pattern}
|
||||
bind:valid={inputCheck}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
import CurrencyInput from './apps/components/inputs/currency/CurrencyInput.svelte'
|
||||
import Multiselect from 'svelte-multiselect'
|
||||
import Password from './Password.svelte'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
|
||||
export let css: ComponentCustomCSS<'schemaformcomponent'> | undefined = undefined
|
||||
export let label: string = ''
|
||||
@@ -30,6 +32,7 @@
|
||||
export let format: string = ''
|
||||
export let contentEncoding: 'base64' | 'binary' | undefined = undefined
|
||||
export let type: string | undefined = undefined
|
||||
export let oneOf: SchemaProperty[] | undefined = undefined
|
||||
export let required = false
|
||||
export let pattern: undefined | string = undefined
|
||||
export let valid = required ? false : true
|
||||
@@ -52,6 +55,20 @@
|
||||
export let title: string | undefined = undefined
|
||||
export let placeholder: string | undefined = undefined
|
||||
|
||||
let oneOfSelected: string | undefined = undefined
|
||||
function updateOneOfSelected(oneOf: SchemaProperty[] | undefined) {
|
||||
if (oneOf && oneOf.length >= 2 && !oneOfSelected) {
|
||||
oneOfSelected = oneOf[0]['title']
|
||||
}
|
||||
}
|
||||
$: updateOneOfSelected(oneOf)
|
||||
function updateOneOfSelectedValue(oneOfSelected) {
|
||||
if (oneOfSelected) {
|
||||
value = { label: oneOfSelected }
|
||||
}
|
||||
}
|
||||
$: updateOneOfSelectedValue(oneOfSelected)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export let error: string = ''
|
||||
@@ -350,6 +367,36 @@
|
||||
</div>
|
||||
{:else if inputCat == 'resource-object'}
|
||||
<LightweightObjectResourceInput {format} bind:value />
|
||||
{:else if inputCat == 'object' && oneOf && oneOf.length >= 2}
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
{#if oneOf && oneOf.length >= 2}
|
||||
<ToggleButtonGroup bind:selected={oneOfSelected}>
|
||||
{#each oneOf as obj}
|
||||
<ToggleButton value={obj.title} label={obj.title} />
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
|
||||
{#if oneOfSelected}
|
||||
{@const objIdx = oneOf.findIndex((o) => o.title === oneOfSelected)}
|
||||
{@const obj = oneOf[objIdx]}
|
||||
{#if obj && obj.properties && Object.keys(obj.properties).length > 0}
|
||||
<div class="p-4 pl-8 border rounded w-full">
|
||||
<LightweightSchemaForm
|
||||
schema={{
|
||||
properties: Object.fromEntries(
|
||||
Object.entries(obj.properties).filter(([k, v]) => k !== 'label')
|
||||
),
|
||||
$schema: '',
|
||||
required: obj.required ?? [],
|
||||
type: 'object'
|
||||
}}
|
||||
bind:args={value}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{:else if inputCat == 'object'}
|
||||
{#if properties && Object.keys(properties).length > 0}
|
||||
<div class="p-4 pl-8 border rounded w-full">
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
bind:valid={inputCheck[argName]}
|
||||
bind:error={errors[argName]}
|
||||
type={schema.properties[argName].type}
|
||||
oneOf={schema.properties[argName].oneOf}
|
||||
required={schema.required?.includes(argName) ?? false}
|
||||
pattern={schema.properties[argName].pattern}
|
||||
defaultValue={defaultValues?.[argName] ?? schema.properties[argName].default}
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
description={schema.properties[argName].description}
|
||||
bind:value={args[argName]}
|
||||
type={schema.properties[argName].type}
|
||||
oneOf={schema.properties[argName].oneOf}
|
||||
required={schema.required.includes(argName)}
|
||||
pattern={schema.properties[argName].pattern}
|
||||
bind:editor={editor[argName]}
|
||||
|
||||
@@ -163,6 +163,7 @@
|
||||
description={schema.properties[argName].description}
|
||||
bind:value={args[argName]}
|
||||
type={schema.properties[argName].type}
|
||||
oneOf={schema.properties[argName].oneOf}
|
||||
required={schema?.required?.includes(argName)}
|
||||
pattern={schema.properties[argName].pattern}
|
||||
bind:valid={inputCheck[argName]}
|
||||
@@ -220,6 +221,7 @@
|
||||
description={schema.properties[argName].description}
|
||||
bind:value={args[argName]}
|
||||
type={schema.properties[argName].type}
|
||||
oneOf={schema.properties[argName].oneOf}
|
||||
required={schema?.required?.includes(argName)}
|
||||
pattern={schema.properties[argName].pattern}
|
||||
bind:valid={inputCheck[argName]}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
export let blockOpen = false
|
||||
</script>
|
||||
|
||||
<Popover on:close>
|
||||
<Popover on:close class="leading-none">
|
||||
<PopoverButton>
|
||||
<div use:floatingRef>
|
||||
<slot name="button" />
|
||||
|
||||
@@ -106,7 +106,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if schema.properties[item.value]?.type === 'object'}
|
||||
{#if schema.properties[item.value]?.type === 'object' && !(schema.properties[item.value].oneOf && schema.properties[item.value].oneOf.length >= 2)}
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<Label label="Nested Properties">
|
||||
<svelte:self
|
||||
|
||||
@@ -12,10 +12,18 @@
|
||||
import { Tab, TabContent } from '../common'
|
||||
import LightweightArgInput from '../LightweightArgInput.svelte'
|
||||
import EditableSchemaDrawer from './EditableSchemaDrawer.svelte'
|
||||
import type { SchemaProperty } from '$lib/common'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { Pen, Plus, Trash2 } from 'lucide-svelte'
|
||||
import Popup from '../common/popup/Popup.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
export let format: string = ''
|
||||
export let contentEncoding: 'base64' | 'binary' | undefined = undefined
|
||||
export let type: string | undefined = undefined
|
||||
export let oneOf: SchemaProperty[] | undefined = undefined
|
||||
export let required = false
|
||||
export let pattern: undefined | string = undefined
|
||||
export let password = false
|
||||
@@ -41,6 +49,46 @@
|
||||
export let displayWebhookWarning: boolean = true
|
||||
export let lightweightMode: boolean = false
|
||||
|
||||
function getOneOfWithoutLabel(oneOf: SchemaProperty[]) {
|
||||
return oneOf.map((v) => ({
|
||||
...v,
|
||||
properties: Object.fromEntries(
|
||||
Object.entries(v.properties ?? {}).filter(([k, v]) => k !== 'label')
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
let oneOfSelected: string | undefined = undefined
|
||||
function oneOfUpdate(oneOf: SchemaProperty[] | undefined) {
|
||||
if (oneOf && oneOf.length >= 2) {
|
||||
if (!oneOfSelected) {
|
||||
oneOfSelected = oneOf[0].title
|
||||
}
|
||||
|
||||
if (
|
||||
!schema.oneOf ||
|
||||
!deepEqual(
|
||||
oneOf.map((v) => [v.title, v.order]),
|
||||
schema.oneOf.map((v) => [v.title, v.order])
|
||||
)
|
||||
) {
|
||||
// update schema if not exists or order changed
|
||||
schema.oneOf = getOneOfWithoutLabel(oneOf)
|
||||
}
|
||||
} else if (!oneOf) {
|
||||
schema.oneOf = undefined
|
||||
}
|
||||
}
|
||||
$: oneOfUpdate(oneOf)
|
||||
|
||||
function orderUpdate(order) {
|
||||
if (order && !deepEqual(order, schema.order)) {
|
||||
// update from external reordering
|
||||
schema.order = order
|
||||
}
|
||||
}
|
||||
$: orderUpdate(order)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function getResourceTypesFromFormat(format: string | undefined): string[] {
|
||||
@@ -51,19 +99,203 @@
|
||||
return []
|
||||
}
|
||||
|
||||
let schema = { properties, order, required: requiredProperty }
|
||||
let schema = {
|
||||
properties,
|
||||
order,
|
||||
required: requiredProperty,
|
||||
oneOf: oneOf ? getOneOfWithoutLabel(oneOf) : undefined
|
||||
}
|
||||
|
||||
$: if (schema) {
|
||||
properties = schema.properties
|
||||
order = schema.order
|
||||
requiredProperty = schema.required
|
||||
console.log('initial schema', schema)
|
||||
|
||||
dispatch('schemaChange')
|
||||
function schemaUpdate(changedSchema: typeof schema) {
|
||||
if (
|
||||
!deepEqual(changedSchema, {
|
||||
properties,
|
||||
order,
|
||||
required: requiredProperty,
|
||||
oneOf: oneOf ? getOneOfWithoutLabel(oneOf) : undefined
|
||||
})
|
||||
) {
|
||||
properties = changedSchema.properties
|
||||
order = changedSchema.order
|
||||
requiredProperty = changedSchema.required
|
||||
oneOf = changedSchema.oneOf?.map((v) => {
|
||||
return {
|
||||
...v,
|
||||
properties: {
|
||||
...v.properties,
|
||||
label: {
|
||||
type: 'string',
|
||||
enum: [v.title ?? '']
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
dispatch('schemaChange')
|
||||
}
|
||||
}
|
||||
|
||||
$: schemaUpdate(schema)
|
||||
|
||||
let variantName = ''
|
||||
function createVariant(name: string) {
|
||||
if (schema.oneOf) {
|
||||
if (schema.oneOf.some((obj) => obj.title === name)) {
|
||||
throw new Error('Variant name already exists')
|
||||
}
|
||||
const idx = schema.oneOf.findIndex((obj) => obj.title === name)
|
||||
if (idx === -1) {
|
||||
schema.oneOf = [
|
||||
...schema.oneOf,
|
||||
{
|
||||
title: name,
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
]
|
||||
oneOfSelected = name
|
||||
}
|
||||
variantName = ''
|
||||
}
|
||||
}
|
||||
|
||||
function renameVariant(name: string, selected: string) {
|
||||
if (schema.oneOf) {
|
||||
if (schema.oneOf.some((obj) => obj.title === name)) {
|
||||
throw new Error('Variant name already exists')
|
||||
}
|
||||
const idx = schema.oneOf.findIndex((obj) => obj.title === selected)
|
||||
if (idx !== -1) {
|
||||
schema.oneOf[idx].title = name
|
||||
oneOfSelected = name
|
||||
}
|
||||
variantName = ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
{#if type === 'object' && format !== 'resource-s3_object'}
|
||||
{#if type === 'object' && schema.oneOf && schema.oneOf.length >= 2}
|
||||
<div class="flex flex-row gap-2 items-center justify-start">
|
||||
<ToggleButtonGroup bind:selected={oneOfSelected} class="w-auto">
|
||||
{#each schema.oneOf as obj}
|
||||
<ToggleButton value={obj.title} label={obj.title} />
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
|
||||
<Popup
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
let:close
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<Button size="xs2" color="light" nonCaptureEvent startIcon={{ icon: Plus }} />
|
||||
</svelte:fragment>
|
||||
<Label label="Label">
|
||||
<div class="flex flex-col gap-2">
|
||||
<input
|
||||
type="text"
|
||||
class="w-full !bg-surface"
|
||||
on:keydown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
createVariant(variantName)
|
||||
close(null)
|
||||
}
|
||||
}}
|
||||
bind:value={variantName}
|
||||
/>
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
createVariant(variantName)
|
||||
close(null)
|
||||
}}
|
||||
disabled={!variantName}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
</Label>
|
||||
</Popup>
|
||||
</div>
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<span class="font-semibold text-sm">{oneOfSelected}</span>
|
||||
|
||||
<Popup
|
||||
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
|
||||
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
|
||||
let:close
|
||||
>
|
||||
<svelte:fragment slot="button">
|
||||
<Button
|
||||
size="xs2"
|
||||
color="light"
|
||||
startIcon={{ icon: Pen }}
|
||||
propagateEvent
|
||||
iconOnly={false}
|
||||
on:click={() => {
|
||||
if (oneOfSelected) {
|
||||
variantName = oneOfSelected
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<Label label="Label">
|
||||
<div class="flex flex-col gap-2">
|
||||
<input
|
||||
type="text"
|
||||
class="w-full !bg-surface"
|
||||
on:keydown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
if (oneOfSelected) {
|
||||
renameVariant(variantName, oneOfSelected)
|
||||
close(null)
|
||||
}
|
||||
}
|
||||
}}
|
||||
bind:value={variantName}
|
||||
/>
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
if (oneOfSelected) {
|
||||
renameVariant(variantName, oneOfSelected)
|
||||
close(null)
|
||||
}
|
||||
}}
|
||||
disabled={!variantName}
|
||||
>
|
||||
Rename
|
||||
</Button>
|
||||
</div>
|
||||
</Label>
|
||||
</Popup>
|
||||
<Button
|
||||
size="xs2"
|
||||
color="red"
|
||||
startIcon={{ icon: Trash2 }}
|
||||
iconOnly
|
||||
disabled={schema.oneOf.length <= 2}
|
||||
on:click={() => {
|
||||
if (schema.oneOf && oneOfSelected) {
|
||||
const idx = schema.oneOf.findIndex((obj) => obj.title === oneOfSelected)
|
||||
schema.oneOf = schema.oneOf.filter((_, i) => i !== idx)
|
||||
oneOfSelected = schema.oneOf[0].title
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if oneOfSelected && schema.oneOf}
|
||||
{@const idx = schema.oneOf.findIndex((obj) => obj.title === oneOfSelected)}
|
||||
<EditableSchemaDrawer bind:schema={schema.oneOf[idx]} />
|
||||
{/if}
|
||||
{:else if type === 'object' && format !== 'resource-s3_object'}
|
||||
<Tabs
|
||||
selected="resource"
|
||||
on:selected={(e) => {
|
||||
@@ -88,39 +320,41 @@
|
||||
</Tabs>
|
||||
{/if}
|
||||
|
||||
<Label label="Default">
|
||||
{#if lightweightMode}
|
||||
<LightweightArgInput
|
||||
bind:value={defaultValue}
|
||||
type={password ? 'string' : type}
|
||||
displayHeader={false}
|
||||
{pattern}
|
||||
{customErrorMessage}
|
||||
{itemsType}
|
||||
{contentEncoding}
|
||||
{format}
|
||||
{extra}
|
||||
/>
|
||||
{:else}
|
||||
<ArgInput
|
||||
{itemPicker}
|
||||
resourceTypes={getResourceTypesFromFormat(format)}
|
||||
bind:value={defaultValue}
|
||||
type={password ? 'string' : type}
|
||||
displayHeader={false}
|
||||
{pattern}
|
||||
{customErrorMessage}
|
||||
{itemsType}
|
||||
{contentEncoding}
|
||||
{format}
|
||||
{extra}
|
||||
{nullable}
|
||||
{variableEditor}
|
||||
compact
|
||||
noMargin
|
||||
/>
|
||||
{/if}
|
||||
</Label>
|
||||
{#if !(type === 'object' && oneOf && oneOf.length >= 2)}
|
||||
<Label label="Default">
|
||||
{#if lightweightMode}
|
||||
<LightweightArgInput
|
||||
bind:value={defaultValue}
|
||||
type={password ? 'string' : type}
|
||||
displayHeader={false}
|
||||
{pattern}
|
||||
{customErrorMessage}
|
||||
{itemsType}
|
||||
{contentEncoding}
|
||||
{format}
|
||||
{extra}
|
||||
/>
|
||||
{:else}
|
||||
<ArgInput
|
||||
{itemPicker}
|
||||
resourceTypes={getResourceTypesFromFormat(format)}
|
||||
bind:value={defaultValue}
|
||||
type={password ? 'string' : type}
|
||||
displayHeader={false}
|
||||
{pattern}
|
||||
{customErrorMessage}
|
||||
{itemsType}
|
||||
{contentEncoding}
|
||||
{format}
|
||||
{extra}
|
||||
{nullable}
|
||||
{variableEditor}
|
||||
compact
|
||||
noMargin
|
||||
/>
|
||||
{/if}
|
||||
</Label>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-row gap-2">
|
||||
<Toggle
|
||||
@@ -151,7 +385,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if displayWebhookWarning}
|
||||
{#if displayWebhookWarning && !(type === 'object' && oneOf && oneOf.length >= 2)}
|
||||
<Alert type="info" title="Default not used by webhooks" size="xs" collapsible>
|
||||
If this flow is triggered by a webhook, the default value will not replace a missing value
|
||||
from the payload. It will only be used as the default value in the autogenerated UI. We
|
||||
|
||||
@@ -10,11 +10,15 @@
|
||||
|
||||
import EditableSchemaForm from '../EditableSchemaForm.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import type { SchemaProperty } from '$lib/common'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
||||
|
||||
export let description: string = ''
|
||||
export let format: string = ''
|
||||
export let contentEncoding: 'base64' | 'binary' | undefined = undefined
|
||||
export let type: string | undefined = undefined
|
||||
export let oneOf: SchemaProperty[] | undefined = undefined
|
||||
export let pattern: undefined | string = undefined
|
||||
export let enum_: string[] | undefined = undefined
|
||||
export let extra: Record<string, any> = {}
|
||||
@@ -37,6 +41,23 @@
|
||||
|
||||
let el: HTMLTextAreaElement | undefined = undefined
|
||||
|
||||
let oneOfSelected: string | undefined =
|
||||
(oneOf && oneOf.length >= 2 && oneOf[0]['title']) || undefined
|
||||
let oneOfSchemas: any[] | undefined = undefined
|
||||
function updateOneOfSchemas(oneOf: SchemaProperty[] | undefined) {
|
||||
if (oneOf && oneOf.length >= 2) {
|
||||
oneOfSchemas = oneOf.map((obj) => {
|
||||
return {
|
||||
properties: obj.properties
|
||||
? Object.fromEntries(Object.entries(obj.properties).filter(([k, v]) => k !== 'label'))
|
||||
: {},
|
||||
order: obj.order
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
$: updateOneOfSchemas(oneOf)
|
||||
|
||||
export function focus() {
|
||||
el?.focus()
|
||||
if (el) {
|
||||
@@ -133,6 +154,24 @@
|
||||
bind:currency={extra['currency']}
|
||||
bind:currencyLocale={extra['currencyLocale']}
|
||||
/>
|
||||
{:else if type == 'object' && oneOf && oneOf.length >= 2 && !isFlowInput && !isAppInput}
|
||||
<ToggleButtonGroup bind:selected={oneOfSelected} class="mb-2">
|
||||
{#each oneOf as obj}
|
||||
<ToggleButton value={obj.title} label={obj.title} />
|
||||
{/each}
|
||||
</ToggleButtonGroup>
|
||||
{#if oneOfSelected && oneOfSchemas}
|
||||
{@const idx = oneOf.findIndex((obj) => obj.title === oneOfSelected)}
|
||||
<div class="border">
|
||||
<EditableSchemaForm
|
||||
on:change
|
||||
noPreview
|
||||
bind:schema={oneOfSchemas[idx]}
|
||||
uiOnly
|
||||
jsonEnabled={false}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if type == 'object' && !format?.startsWith('resource-') && !isFlowInput && !isAppInput}
|
||||
<div class="border">
|
||||
<EditableSchemaForm on:change noPreview bind:schema uiOnly jsonEnabled={false} />
|
||||
|
||||
@@ -6,10 +6,18 @@ export function argSigToJsonSchemaType(
|
||||
| { resource: string | null }
|
||||
| { list: string | { str: any } | { object: { key: string; typ: any }[] } | null }
|
||||
| { str: string[] | null }
|
||||
| { object: { key: string; typ: any }[] },
|
||||
| { object: { key: string; typ: any }[] }
|
||||
| {
|
||||
oneof: [
|
||||
{
|
||||
label: string
|
||||
properties: { key: string; typ: any }[]
|
||||
}
|
||||
]
|
||||
},
|
||||
oldS: SchemaProperty
|
||||
): void {
|
||||
const newS: SchemaProperty = { type: '' }
|
||||
let newS: SchemaProperty = { type: '' }
|
||||
if (t === 'int') {
|
||||
newS.type = 'integer'
|
||||
} else if (t === 'float') {
|
||||
@@ -31,12 +39,38 @@ export function argSigToJsonSchemaType(
|
||||
} else if (t === 'datetime') {
|
||||
newS.type = 'string'
|
||||
newS.format = 'date-time'
|
||||
} else if (typeof t !== 'string' && 'oneof' in t) {
|
||||
newS.type = 'object'
|
||||
if (t.oneof) {
|
||||
newS.oneOf = t.oneof.map((obj) => {
|
||||
const oldObjS = oldS.oneOf?.find((o) => o?.title === obj.label) ?? undefined
|
||||
const properties = {}
|
||||
for (const prop of obj.properties) {
|
||||
if (oldObjS?.properties && prop.key in oldObjS?.properties) {
|
||||
properties[prop.key] = oldObjS?.properties[prop.key]
|
||||
} else {
|
||||
properties[prop.key] = { description: '', type: '' }
|
||||
}
|
||||
argSigToJsonSchemaType(prop.typ, properties[prop.key])
|
||||
}
|
||||
return {
|
||||
type: 'object',
|
||||
title: obj.label,
|
||||
properties,
|
||||
order: oldObjS?.order ?? undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
} else if (typeof t !== 'string' && `object` in t) {
|
||||
newS.type = 'object'
|
||||
if (t.object) {
|
||||
const properties = {}
|
||||
for (const prop of t.object) {
|
||||
properties[prop.key] = {}
|
||||
if (oldS.properties && prop.key in oldS.properties) {
|
||||
properties[prop.key] = oldS.properties[prop.key]
|
||||
} else {
|
||||
properties[prop.key] = { description: '', type: '' }
|
||||
}
|
||||
argSigToJsonSchemaType(prop.typ, properties[prop.key])
|
||||
}
|
||||
newS.properties = properties
|
||||
|
||||
@@ -377,6 +377,7 @@ export type InputCat =
|
||||
| 'sql'
|
||||
| 'yaml'
|
||||
| 'currency'
|
||||
| 'oneOf'
|
||||
|
||||
export function setInputCat(
|
||||
type: string | undefined,
|
||||
@@ -411,6 +412,8 @@ export function setInputCat(
|
||||
return 'email'
|
||||
} else if (type == 'string' && format == 'currency') {
|
||||
return 'currency'
|
||||
} else if (type == 'oneOf') {
|
||||
return 'oneOf'
|
||||
} else {
|
||||
return 'string'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user