443 lines
12 KiB
Svelte
443 lines
12 KiB
Svelte
<script lang="ts">
|
|
import Alert from '$lib/components/common/alert/Alert.svelte'
|
|
import Label from '../Label.svelte'
|
|
import Toggle from '../Toggle.svelte'
|
|
import SimpleEditor from '../SimpleEditor.svelte'
|
|
import type ItemPicker from '../ItemPicker.svelte'
|
|
import type VariableEditor from '../VariableEditor.svelte'
|
|
import { createEventDispatcher } from 'svelte'
|
|
import ArgInput from '../ArgInput.svelte'
|
|
import ObjectTypeNarrowing from '../ObjectTypeNarrowing.svelte'
|
|
import Tabs from '../common/tabs/Tabs.svelte'
|
|
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 | undefined = undefined
|
|
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
|
|
export let variableEditor: VariableEditor | undefined = undefined
|
|
export let itemPicker: ItemPicker | undefined = undefined
|
|
export let nullable: boolean = false
|
|
export let disabled: boolean = false
|
|
export let defaultValue: any = undefined
|
|
export let propsNames: any = []
|
|
export let showExpr: string | undefined = undefined
|
|
export let extra: Record<string, any> = {}
|
|
export let customErrorMessage: string | undefined = undefined
|
|
export let itemsType:
|
|
| {
|
|
type?: 'string' | 'number' | 'bytes' | 'object'
|
|
contentEncoding?: 'base64'
|
|
enum?: string[]
|
|
multiselect?: string[]
|
|
}
|
|
| undefined = undefined
|
|
export let properties: Record<string, any> = {}
|
|
export let order: string[] = []
|
|
export let requiredProperty: string[] = []
|
|
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)
|
|
schema = schema
|
|
}
|
|
} else if (!oneOf) {
|
|
schema.oneOf = undefined
|
|
schema = schema
|
|
}
|
|
}
|
|
$: oneOfUpdate(oneOf)
|
|
|
|
function orderUpdate(order) {
|
|
if (order && !deepEqual(order, schema.order)) {
|
|
// update from external reordering
|
|
schema.order = order
|
|
schema = schema
|
|
}
|
|
}
|
|
$: orderUpdate(order)
|
|
|
|
const dispatch = createEventDispatcher()
|
|
|
|
function getResourceTypesFromFormat(format: string | undefined): string[] {
|
|
if (format?.startsWith('resource-')) {
|
|
return [format.split('-')[1]]
|
|
}
|
|
|
|
return []
|
|
}
|
|
|
|
let schema = {
|
|
properties,
|
|
order,
|
|
required: requiredProperty,
|
|
oneOf: oneOf ? getOneOfWithoutLabel(oneOf) : undefined
|
|
}
|
|
|
|
function schemaUpdate(changedSchema: typeof schema) {
|
|
if (
|
|
!deepEqual(changedSchema, {
|
|
properties,
|
|
order,
|
|
required: requiredProperty,
|
|
oneOf: oneOf ? getOneOfWithoutLabel(oneOf) : undefined
|
|
})
|
|
) {
|
|
properties = structuredClone(changedSchema.properties)
|
|
order = structuredClone(changedSchema.order)
|
|
requiredProperty = structuredClone(changedSchema.required)
|
|
oneOf = changedSchema.oneOf?.map((v) => {
|
|
return {
|
|
...v,
|
|
properties: {
|
|
...v.properties,
|
|
label: {
|
|
type: 'string',
|
|
enum: [v.title ?? '']
|
|
}
|
|
}
|
|
}
|
|
})
|
|
dispatch('schemaChange', { properties, order, requiredProperty, oneOf })
|
|
}
|
|
}
|
|
|
|
$: 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 = ''
|
|
}
|
|
}
|
|
|
|
let initialObjectSelected =
|
|
Object.keys(schema?.properties ?? {}).length == 0 ? 'resource' : 'custom-object'
|
|
</script>
|
|
|
|
<div class="flex flex-col gap-2">
|
|
{#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={initialObjectSelected}
|
|
on:selected={(e) => {
|
|
if (e.detail === 'custom-object') {
|
|
format = ''
|
|
}
|
|
}}
|
|
>
|
|
<Tab value="resource">Resource</Tab>
|
|
<Tab value="custom-object">Custom Object</Tab>
|
|
<svelte:fragment slot="content">
|
|
<div class="pt-2">
|
|
<TabContent value="custom-object">
|
|
<EditableSchemaDrawer bind:schema />
|
|
</TabContent>
|
|
|
|
<TabContent value="resource">
|
|
<ObjectTypeNarrowing bind:format />
|
|
</TabContent>
|
|
</div>
|
|
</svelte:fragment>
|
|
</Tabs>
|
|
{/if}
|
|
|
|
{#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}
|
|
{disabled}
|
|
/>
|
|
{:else}
|
|
<ArgInput
|
|
{itemPicker}
|
|
resourceTypes={getResourceTypesFromFormat(format)}
|
|
bind:value={defaultValue}
|
|
type={password ? 'string' : type}
|
|
displayHeader={false}
|
|
{pattern}
|
|
{customErrorMessage}
|
|
{itemsType}
|
|
{contentEncoding}
|
|
{format}
|
|
{extra}
|
|
{disabled}
|
|
{nullable}
|
|
{variableEditor}
|
|
compact
|
|
noMargin
|
|
/>
|
|
{/if}
|
|
</Label>
|
|
{/if}
|
|
|
|
<div class="flex flex-row gap-2">
|
|
<Toggle
|
|
options={{ right: 'Required' }}
|
|
size="xs"
|
|
disabled={type === 'boolean'}
|
|
on:change={(event) => {
|
|
dispatch('requiredChange', { required: event?.detail })
|
|
}}
|
|
checked={required}
|
|
on:change={(event) => {
|
|
if (event?.detail) {
|
|
nullable = false
|
|
}
|
|
}}
|
|
/>
|
|
{#if type === 'string'}
|
|
<Toggle
|
|
options={{
|
|
right: 'Nullable',
|
|
rightTooltip: 'If enabled, the default value will be null and not an empty string.'
|
|
}}
|
|
lightMode
|
|
size="xs"
|
|
bind:checked={nullable}
|
|
disabled={required}
|
|
/>
|
|
{/if}
|
|
<Toggle
|
|
options={{
|
|
right: 'Disabled',
|
|
rightTooltip: 'Do not let user modify this field'
|
|
}}
|
|
lightMode
|
|
size="xs"
|
|
bind:checked={disabled}
|
|
/>
|
|
</div>
|
|
|
|
{#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
|
|
recommend using default values in the signature of the steps where this value is used (using
|
|
`x=default`) to have the desired behavior.
|
|
</Alert>
|
|
{/if}
|
|
</div>
|
|
<div>
|
|
<Toggle
|
|
size="xs"
|
|
options={{ right: 'Show this field only when conditions are met' }}
|
|
checked={Boolean(showExpr)}
|
|
on:change={() => {
|
|
showExpr = showExpr ? undefined : 'true //fields.foo == 42'
|
|
}}
|
|
/>
|
|
{#if showExpr != undefined}
|
|
<div class="border">
|
|
<SimpleEditor
|
|
extraLib={`declare const fields: Record<${propsNames
|
|
?.filter((x) => x != name)
|
|
.map((x) => `"${x}"`)
|
|
.join(' | ')}, any>;\n`}
|
|
lang="javascript"
|
|
bind:code={showExpr}
|
|
shouldBindKey={false}
|
|
fixedOverflowWidgets={false}
|
|
autoHeight
|
|
/>
|
|
</div>
|
|
<div class="flex flex-row-reverse text-2xs text-tertiary">
|
|
<div>
|
|
Other fields are available under <code>fields</code> (e.g:
|
|
<code>fields.foo == 42</code>)
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|