feat: oneOf inputs (#3893)

* feat: oneOf input for typescript

* feat: oneOf order and light form

* feat: oneOf UI builder
This commit is contained in:
HugoCasa
2024-06-17 21:35:46 +02:00
committed by GitHub
parent 0da97877fd
commit 13c8d3f492
21 changed files with 674 additions and 84 deletions
+74 -1
View File
@@ -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) {
@@ -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,
}
+4 -4
View File
@@ -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",
+2 -2
View File
@@ -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",
+4 -1
View File
@@ -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 = {
+107 -1
View File
@@ -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} />
+37 -3
View File
@@ -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
+3
View File
@@ -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'
}