932 lines
29 KiB
Svelte
932 lines
29 KiB
Svelte
<script lang="ts">
|
|
import { createBubbler, preventDefault, stopPropagation } from 'svelte/legacy'
|
|
|
|
const bubble = createBubbler()
|
|
import type { Schema } from '$lib/common'
|
|
import { VariableService, type ScriptLang } from '$lib/gen'
|
|
import { workspaceStore } from '$lib/stores'
|
|
import { Button } from './common'
|
|
import ItemPicker from './ItemPicker.svelte'
|
|
import VariableEditor from './VariableEditor.svelte'
|
|
import { Pen, Plus, X } from 'lucide-svelte'
|
|
import { Pane, Splitpanes } from 'svelte-splitpanes'
|
|
import { twMerge } from 'tailwind-merge'
|
|
import FlowPropertyEditor from './schema/FlowPropertyEditor.svelte'
|
|
import PropertyEditor from './schema/PropertyEditor.svelte'
|
|
import SimpleEditor from './SimpleEditor.svelte'
|
|
import { createEventDispatcher, untrack } from 'svelte'
|
|
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
|
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
|
import Label from './Label.svelte'
|
|
import { sendUserToast } from '$lib/toast'
|
|
import Toggle from './Toggle.svelte'
|
|
import {
|
|
DynamicInput,
|
|
emptyString,
|
|
generateRandomString,
|
|
readFieldsRecursively
|
|
} from '$lib/utils'
|
|
import Popover from './meltComponents/Popover.svelte'
|
|
import SchemaFormDnd from './schema/SchemaFormDND.svelte'
|
|
import { deepEqual } from 'fast-equals'
|
|
import { tweened } from 'svelte/motion'
|
|
import type { SchemaDiff } from '$lib/components/schema/schemaUtils.svelte'
|
|
import type { EditableSchemaFormUi } from '$lib/components/custom_ui'
|
|
import Section from '$lib/components/Section.svelte'
|
|
import Editor from './Editor.svelte'
|
|
import AddPropertyV2 from './schema/AddPropertyV2.svelte'
|
|
|
|
// export let openEditTab: () => void = () => {}
|
|
const dispatch = createEventDispatcher()
|
|
|
|
interface Props {
|
|
schema: Schema | any
|
|
hiddenArgs?: string[]
|
|
args?: Record<string, any>
|
|
shouldHideNoInputs?: boolean
|
|
noVariablePicker?: boolean
|
|
flexWrap?: boolean
|
|
uiOnly?: boolean
|
|
isFlowInput?: boolean
|
|
noPreview?: boolean
|
|
jsonEnabled?: boolean
|
|
isAppInput?: boolean
|
|
displayWebhookWarning?: boolean
|
|
onlyMaskPassword?: boolean
|
|
editTab:
|
|
| 'inputEditor'
|
|
| 'history'
|
|
| 'savedInputs'
|
|
| 'json'
|
|
| 'captures'
|
|
| 'firstStepInputs'
|
|
| undefined
|
|
previewSchema?: Record<string, any> | undefined
|
|
editPanelInitialSize?: number | undefined
|
|
editPanelSize?: number
|
|
diff?: Record<string, SchemaDiff>
|
|
disableDnd?: boolean
|
|
shouldDispatchChanges?: boolean
|
|
isValid?: boolean
|
|
customUi?: EditableSchemaFormUi | undefined
|
|
pannelExtraButtonWidth?: number
|
|
class?: string
|
|
dynCode?: string | undefined
|
|
dynLang?: ScriptLang | undefined
|
|
showDynOpt?: boolean
|
|
addPropertyInEditorTab?: boolean
|
|
openEditTab?: import('svelte').Snippet
|
|
addProperty?: import('svelte').Snippet
|
|
runButton?: import('svelte').Snippet
|
|
extraTab?: import('svelte').Snippet
|
|
schemaFormClassName?: string
|
|
onChange?: (args: Record<string, any>) => void
|
|
}
|
|
|
|
let {
|
|
schema = $bindable(),
|
|
hiddenArgs = [],
|
|
args = $bindable(undefined),
|
|
shouldHideNoInputs = false,
|
|
noVariablePicker = false,
|
|
flexWrap = false,
|
|
uiOnly = false,
|
|
isFlowInput = false,
|
|
noPreview = false,
|
|
jsonEnabled = true,
|
|
isAppInput = false,
|
|
displayWebhookWarning = false,
|
|
onlyMaskPassword = false,
|
|
editTab,
|
|
previewSchema = undefined,
|
|
editPanelInitialSize = undefined,
|
|
editPanelSize = $bindable(0),
|
|
diff = {},
|
|
disableDnd = false,
|
|
shouldDispatchChanges = false,
|
|
isValid = $bindable(true),
|
|
customUi = undefined,
|
|
pannelExtraButtonWidth = 0,
|
|
class: clazz = '',
|
|
dynCode = $bindable(),
|
|
dynLang = $bindable(),
|
|
showDynOpt = false,
|
|
addPropertyInEditorTab = false,
|
|
openEditTab,
|
|
addProperty,
|
|
runButton,
|
|
extraTab,
|
|
schemaFormClassName = undefined,
|
|
onChange = undefined
|
|
}: Props = $props()
|
|
|
|
$effect.pre(() => {
|
|
if (args == undefined) {
|
|
args = {}
|
|
}
|
|
if (dynLang === undefined) {
|
|
dynLang = schema?.['x-windmill-dyn-select-lang'] || 'bun'
|
|
}
|
|
if (dynCode === undefined) {
|
|
dynCode = schema?.['x-windmill-dyn-select-code'] || ''
|
|
}
|
|
})
|
|
|
|
$effect(() => {
|
|
if (onChange) {
|
|
readFieldsRecursively(args)
|
|
onChange(args ?? {})
|
|
}
|
|
})
|
|
|
|
$effect(() => {
|
|
if (schema && dynCode !== undefined && dynLang !== undefined) {
|
|
if (dynCode && dynCode.trim()) {
|
|
schema['x-windmill-dyn-select-code'] = dynCode.trim()
|
|
schema['x-windmill-dyn-select-lang'] = dynLang
|
|
} else {
|
|
delete schema['x-windmill-dyn-select-code']
|
|
delete schema['x-windmill-dyn-select-lang']
|
|
}
|
|
}
|
|
})
|
|
|
|
export function setDefaults() {
|
|
const nargs = {}
|
|
|
|
Object.keys(schema?.properties ?? {}).forEach((key) => {
|
|
if (schema?.properties[key].default != undefined && args?.[key] == undefined) {
|
|
let value = schema?.properties[key].default
|
|
nargs[key] = value === 'object' ? structuredClone($state.snapshot(value)) : value
|
|
}
|
|
})
|
|
args = nargs
|
|
}
|
|
|
|
let pickForField: string | undefined
|
|
let itemPicker: ItemPicker | undefined = $state(undefined)
|
|
let variableEditor: VariableEditor | undefined = $state(undefined)
|
|
|
|
let keys: string[] = $state(
|
|
(Array.isArray(schema?.order)
|
|
? [...schema.order]
|
|
: (Object.keys(schema?.properties ?? {}) ?? Object.keys(schema?.properties ?? {}))
|
|
).filter((x) => !hiddenArgs?.includes(x))
|
|
)
|
|
|
|
function alignOrderWithProperties(schema: {
|
|
properties: Record<string, any>
|
|
order: string[] | undefined
|
|
}) {
|
|
if (schema.order == undefined && !Array.isArray(schema.order)) {
|
|
schema.order = []
|
|
}
|
|
let norder = [...schema.order]
|
|
let properties = Object.keys(schema?.properties ?? {})
|
|
let index = 0
|
|
let hasChanged = false
|
|
for (let k of properties) {
|
|
if (schema.properties[k]?.type === 'object' && schema.properties[k].properties) {
|
|
hasChanged = hasChanged || alignOrderWithProperties(schema.properties[k])
|
|
}
|
|
if (!norder.includes(k)) {
|
|
norder = [...norder.slice(0, index), k, ...norder.slice(index)]
|
|
}
|
|
index += 1
|
|
}
|
|
norder = norder.filter((x) => properties.includes(x))
|
|
if (!deepEqual(schema.order, norder)) {
|
|
schema.order = norder
|
|
return true
|
|
}
|
|
return hasChanged
|
|
}
|
|
function onSchemaChange() {
|
|
if (alignOrderWithProperties(schema)) {
|
|
// console.log('alignOrderWithProperties', JSON.stringify(schema, null, 2))
|
|
}
|
|
let lkeys = (schema?.order ?? Object.keys(schema?.properties ?? {})).filter(
|
|
(x) => !hiddenArgs?.includes(x)
|
|
)
|
|
if (schema?.properties && !deepEqual(lkeys, keys)) {
|
|
keys = [...lkeys]
|
|
if (opened == undefined) {
|
|
opened = keys[0]
|
|
}
|
|
}
|
|
}
|
|
|
|
let opened: string | undefined = $state(untrack(() => keys[0]))
|
|
|
|
function computeSelected(property: any) {
|
|
if (!opened) return ''
|
|
if (property?.type !== 'object') return property?.type
|
|
if (property.format === 'resource-s3_object') return 'S3'
|
|
if (property.format?.startsWith('dynselect-')) return 'dynselect'
|
|
if (property.format?.startsWith('dynmultiselect-')) return 'dynmultiselect'
|
|
if (property.oneOf && property.oneOf.length >= 2) return 'oneOf'
|
|
if (property.format?.startsWith('resource-')) return 'resource'
|
|
return 'object'
|
|
}
|
|
|
|
export function openField(key: string) {
|
|
opened = key
|
|
}
|
|
|
|
export function deleteField(key: string) {
|
|
delete args?.[key]
|
|
delete schema.properties[key]
|
|
if (schema.required?.includes(key)) {
|
|
schema.required = schema.required?.filter((x) => x !== key)
|
|
}
|
|
if (schema.order) {
|
|
schema.order = schema.order.filter((x) => x !== key)
|
|
}
|
|
}
|
|
|
|
function renameProperty(oldName: string, key: string) {
|
|
const el = document.getElementById(key) as HTMLInputElement
|
|
const newName = el.value
|
|
if (oldName === newName) {
|
|
return
|
|
}
|
|
|
|
if (Object.keys(schema.properties ?? {}).includes(newName)) {
|
|
sendUserToast('There is already an argument with this name', true)
|
|
|
|
// clear the input
|
|
el.value = oldName
|
|
} else {
|
|
let newSchema = $state.snapshot(schema)
|
|
if (args) {
|
|
args[newName] = args[oldName]
|
|
delete args[oldName]
|
|
}
|
|
|
|
newSchema.properties[newName] = newSchema.properties[oldName]
|
|
delete newSchema.properties[oldName]
|
|
|
|
if (newSchema.required?.includes(oldName)) {
|
|
newSchema.required = newSchema.required?.map((x) => (x === oldName ? newName : x))
|
|
}
|
|
|
|
// Replace the old name with the new name in the order array
|
|
if (newSchema.order) {
|
|
const index = newSchema.order.indexOf(oldName)
|
|
if (index !== -1) {
|
|
newSchema.order[index] = newName
|
|
}
|
|
}
|
|
|
|
opened = newName
|
|
|
|
schema = newSchema
|
|
|
|
sendUserToast('Argument renamed')
|
|
}
|
|
}
|
|
|
|
let jsonView: boolean = $state(customUi?.jsonOnly == true)
|
|
let schemaString: string = $state(JSON.stringify(schema, null, '\t'))
|
|
let error: string | undefined = $state(undefined)
|
|
let editor: SimpleEditor | undefined = $state(undefined)
|
|
let dynamicSelectEditor: Editor | undefined = $state(undefined)
|
|
export function updateJson() {
|
|
schemaString = JSON.stringify(schema, null, '\t')
|
|
editor?.setCode(schemaString)
|
|
}
|
|
|
|
const editTabDefaultSize = noPreview ? 100 : 50
|
|
editPanelSize = editTab ? (editPanelInitialSize ?? editTabDefaultSize) : 0
|
|
let inputPanelSize = $state(100 - editPanelSize)
|
|
let editPanelSizeSmooth = tweened(editPanelSize, {
|
|
duration: 150
|
|
})
|
|
let inputPanelSizeSmooth = tweened(
|
|
untrack(() => inputPanelSize),
|
|
{ duration: 150 }
|
|
)
|
|
|
|
function openEditTabFn() {
|
|
if (editPanelSize > 0) return
|
|
editPanelSizeSmooth.set(editTabDefaultSize)
|
|
inputPanelSizeSmooth.set(100 - editTabDefaultSize)
|
|
}
|
|
|
|
function closeEditTab() {
|
|
editPanelSizeSmooth.set(0)
|
|
inputPanelSizeSmooth.set(100)
|
|
}
|
|
|
|
function updatePanelSizes(editSize: number, inputSize: number) {
|
|
editPanelSize = editSize
|
|
inputPanelSize = inputSize
|
|
dispatch('editPanelSizeChanged', editSize)
|
|
}
|
|
|
|
let panelButtonWidth: number = $state(0)
|
|
$effect(() => {
|
|
if (args == undefined || typeof args !== 'object') {
|
|
args = {}
|
|
}
|
|
})
|
|
$effect(() => {
|
|
schema && untrack(() => onSchemaChange())
|
|
})
|
|
$effect(() => {
|
|
updatePanelSizes($editPanelSizeSmooth, $inputPanelSizeSmooth)
|
|
})
|
|
$effect(() => {
|
|
!!editTab ? openEditTabFn() : closeEditTab()
|
|
})
|
|
|
|
let dynamicFunctions = $derived(
|
|
Object.entries(schema?.properties ?? {})
|
|
.filter(([_, property]) => {
|
|
const props = property as any
|
|
return props?.type === 'object' && DynamicInput.isDynInputFormat(props.format)
|
|
})
|
|
.map(([fieldName, _]) => fieldName.replace(/\s+/g, '_'))
|
|
)
|
|
|
|
const DYNAMIC_OPTIONS = [
|
|
['DynSelect', 'dynselect'],
|
|
['DynMultiselect', 'dynmultiselect']
|
|
]
|
|
|
|
let typeOptions = $state([
|
|
['String', 'string'],
|
|
['Number', 'number'],
|
|
['Integer', 'integer'],
|
|
['Boolean', 'boolean'],
|
|
['Resource', 'resource'],
|
|
['Object', 'object'],
|
|
['OneOf', 'oneOf'],
|
|
['Array', 'array'],
|
|
['S3', 'S3']
|
|
])
|
|
|
|
function initDynFn(lang: ScriptLang) {
|
|
const generateFn = DynamicInput.getGenerateTemplateFn(lang)
|
|
return dynamicFunctions.map((functionName) => generateFn(functionName)).join('')
|
|
}
|
|
|
|
function updateDynCode(functionName: string, lang: ScriptLang = 'bun') {
|
|
if (
|
|
(lang == 'bun' && dynCode?.includes(`function ${functionName}`)) ||
|
|
(lang == 'python3' && dynCode?.includes(`def ${functionName}`))
|
|
) {
|
|
// Don't add the function if it already exists
|
|
return
|
|
}
|
|
|
|
const generateFn = DynamicInput.getGenerateTemplateFn(lang)
|
|
const code = generateFn(functionName)
|
|
dynCode = dynCode ? dynCode.concat(code) : code
|
|
}
|
|
|
|
let dndType = $state(generateRandomString())
|
|
</script>
|
|
|
|
<div class="w-full h-full">
|
|
<div class="relative z-[100000]">
|
|
<div
|
|
class="absolute"
|
|
style="right: calc({editPanelSize}% - 1px - {pannelExtraButtonWidth}px); top: 0px;"
|
|
bind:clientWidth={panelButtonWidth}
|
|
>
|
|
{@render openEditTab?.()}
|
|
</div>
|
|
</div>
|
|
<Splitpanes class="splitter-hidden w-full">
|
|
{#if !noPreview}
|
|
<Pane bind:size={inputPanelSize} minSize={20}>
|
|
<div
|
|
class="h-full flex flex-col gap-2 {openEditTab && editPanelSize > 0
|
|
? 'pr-[38px]'
|
|
: 'pr-2'}"
|
|
>
|
|
{#if addProperty}
|
|
<div class="w-full justify-left pr-2 grow-0">
|
|
<div
|
|
style={editPanelSize > 0
|
|
? `width: 100%;`
|
|
: `width: calc(100% - ${panelButtonWidth - pannelExtraButtonWidth}px);`}
|
|
>
|
|
{@render addProperty?.()}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<div
|
|
class="min-h-0 overflow-y-auto grow rounded-md {runButton ? 'flex flex-col gap-2' : ''}"
|
|
>
|
|
<SchemaFormDnd
|
|
{dndType}
|
|
nestedClasses={'flex flex-col gap-1 ' + (schemaFormClassName ?? '')}
|
|
bind:schema={
|
|
() => (previewSchema ? previewSchema : schema),
|
|
(newSchema) => {
|
|
schema = newSchema
|
|
}
|
|
}
|
|
{hiddenArgs}
|
|
{disableDnd}
|
|
{onlyMaskPassword}
|
|
bind:args
|
|
on:click={(e) => {
|
|
opened = e.detail
|
|
}}
|
|
on:reorder={(e) => {
|
|
let order = e.detail
|
|
let newProperties = {}
|
|
for (let key of order) {
|
|
newProperties[key] = schema.properties[key]
|
|
}
|
|
schema = {
|
|
...schema,
|
|
properties: newProperties,
|
|
order: e.detail
|
|
}
|
|
}}
|
|
helperScript={{
|
|
source: 'inline',
|
|
code: dynCode!,
|
|
lang: dynLang!
|
|
}}
|
|
prettifyHeader={isAppInput}
|
|
disabled={!!previewSchema}
|
|
{diff}
|
|
on:acceptChange
|
|
on:rejectChange
|
|
{shouldDispatchChanges}
|
|
bind:isValid
|
|
noVariablePicker={noVariablePicker || customUi?.disableVariablePicker === true}
|
|
/>
|
|
|
|
{@render runButton?.()}
|
|
|
|
{#if dynamicFunctions.length > 0}
|
|
<Section
|
|
label="Dynamic input functions"
|
|
collapsable={true}
|
|
collapsed={false}
|
|
class="text-sm"
|
|
>
|
|
<div class="flex flex-col gap-2 h-full">
|
|
{#if dynamicFunctions.length > 0}
|
|
<div class="bg-blue-50 dark:bg-blue-900/20 p-3 rounded-md">
|
|
<div class="text-sm font-semibold text-blue-800 dark:text-blue-200 mb-2">
|
|
Expected Functions for Dynamic Input Fields:
|
|
</div>
|
|
<ul class="text-xs text-blue-700 dark:text-blue-300 space-y-1">
|
|
{#each dynamicFunctions as functionName}
|
|
<li class="font-mono bg-blue-100 dark:bg-blue-800/30 px-2 py-1 rounded">
|
|
{functionName}()
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</div>
|
|
{/if}
|
|
<ToggleButtonGroup
|
|
bind:selected={dynLang}
|
|
on:selected={({ detail }) => {
|
|
dynCode = initDynFn(detail)
|
|
}}
|
|
>
|
|
{#snippet children({ item })}
|
|
<ToggleButton value="bun" label="Typescript (Bun)" {item} />
|
|
<ToggleButton value="python3" label="Python" {item} />
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
{#key dynLang}
|
|
<div class="border w-full h-full">
|
|
<Editor
|
|
bind:this={dynamicSelectEditor}
|
|
class="flex flex-1 grow h-80 w-full"
|
|
scriptLang={dynLang}
|
|
useWebsockets={false}
|
|
automaticLayout
|
|
bind:code={dynCode}
|
|
/>
|
|
</div>
|
|
{/key}
|
|
</div>
|
|
</Section>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</Pane>
|
|
{/if}
|
|
|
|
{#if editPanelSize > 0}
|
|
<Pane
|
|
bind:size={editPanelSize}
|
|
minSize={noPreview ? 100 : 50}
|
|
class={twMerge('border rounded-md', panelButtonWidth > 0 ? 'rounded-tl-none' : '')}
|
|
>
|
|
{#if editTab !== 'inputEditor'}
|
|
{@render extraTab?.()}
|
|
{:else}
|
|
<!-- WIP -->
|
|
{#if jsonEnabled && customUi?.jsonOnly != true}
|
|
<div class="w-full px-4 py-2 flex gap-4 justify-end items-center">
|
|
{#if addPropertyInEditorTab}
|
|
<AddPropertyV2
|
|
bind:schema
|
|
onAddNew={(propertyName) => {
|
|
openField(propertyName)
|
|
}}
|
|
>
|
|
{#snippet trigger()}
|
|
<Button variant="subtle" size="xs" iconOnly startIcon={{ icon: Plus }} />
|
|
{/snippet}
|
|
</AddPropertyV2>
|
|
{/if}
|
|
<div class="shrink-0">
|
|
<Toggle
|
|
bind:checked={jsonView}
|
|
size="xs"
|
|
options={{
|
|
left: 'JSON editor',
|
|
leftTooltip:
|
|
'Arguments can be edited either using the wizard, or by editing their JSON Schema.'
|
|
}}
|
|
lightMode
|
|
on:change={(e) => {
|
|
if (e.detail) {
|
|
schemaString = JSON.stringify(schema, null, '\t')
|
|
editor?.setCode(schemaString)
|
|
} else {
|
|
schema = JSON.parse(schemaString)
|
|
}
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if !jsonView}
|
|
<div
|
|
class="w-full {clazz} {flexWrap ? 'flex flex-row flex-wrap gap-x-6 ' : ''} divide-y"
|
|
>
|
|
{#if keys.length > 0}
|
|
{#each keys as argName, i (argName)}
|
|
<div>
|
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
<div
|
|
class={twMerge(
|
|
'w-full flex bg-gray-50 dark:bg-gray-800 px-4 py-1 justify-between items-center hover:bg-gray-100 cursor-pointer',
|
|
opened === argName ? 'bg-gray-100 hover:bg-gray-200' : ''
|
|
)}
|
|
onclick={() => {
|
|
if (opened === argName) {
|
|
opened = undefined
|
|
} else {
|
|
opened = argName
|
|
}
|
|
}}
|
|
>
|
|
<div class="flex flex-row gap-2 text-sm">
|
|
{argName}
|
|
{#if !uiOnly}
|
|
<div onclick={stopPropagation(preventDefault(bubble('click')))}>
|
|
<Popover placement="bottom-end" containerClasses="p-4" closeButton>
|
|
{#snippet trigger()}
|
|
<Button
|
|
variant="subtle"
|
|
size="xs3"
|
|
nonCaptureEvent
|
|
startIcon={{ icon: Pen }}
|
|
iconOnly
|
|
/>
|
|
{/snippet}
|
|
{#snippet content({ close })}
|
|
<Label label="Name" class="p-4">
|
|
<div class="flex flex-col gap-2">
|
|
<input
|
|
type="text"
|
|
class="w-full !bg-surface"
|
|
value={argName}
|
|
id={argName + i}
|
|
onkeydown={(event) => {
|
|
if (event.key === 'Enter') {
|
|
renameProperty(argName, argName + i)
|
|
close()
|
|
}
|
|
}}
|
|
/>
|
|
<Button
|
|
variant="default"
|
|
size="xs"
|
|
on:click={() => {
|
|
renameProperty(argName, argName + i)
|
|
close()
|
|
}}
|
|
>
|
|
Rename
|
|
</Button>
|
|
</div>
|
|
</Label>
|
|
{/snippet}
|
|
</Popover>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if schema.required?.includes(argName)}
|
|
<span class="text-red-500 text-xs"> Required </span>
|
|
{/if}
|
|
|
|
{#if !uiOnly}
|
|
<button
|
|
class="delete-schema-field-button
|
|
rounded-full p-1 text-gray-500 bg-white
|
|
duration-200 hover:bg-gray-600 focus:bg-gray-600 hover:text-white dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800"
|
|
aria-label="Clear"
|
|
onclick={() => {
|
|
dispatch('delete', argName)
|
|
}}
|
|
>
|
|
<X size={16} />
|
|
</button>
|
|
{/if}
|
|
</div>
|
|
{#if opened === argName}
|
|
<div class="p-4 border-t">
|
|
{#if Object.keys(schema?.properties ?? {}).includes(argName)}
|
|
{#if typeof args == 'object' && schema?.properties[argName]}
|
|
<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}
|
|
bind:contentEncoding={schema.properties[argName].contentEncoding}
|
|
bind:customErrorMessage={
|
|
schema.properties[argName].customErrorMessage
|
|
}
|
|
bind:nonEmpty={schema.properties[argName].nonEmpty}
|
|
bind:itemsType={schema.properties[argName].items}
|
|
bind:extra={schema.properties[argName]}
|
|
bind:title={schema.properties[argName].title}
|
|
bind:placeholder={schema.properties[argName].placeholder}
|
|
bind:properties={schema.properties[argName].properties}
|
|
bind:order={schema.properties[argName].order}
|
|
{isFlowInput}
|
|
{isAppInput}
|
|
>
|
|
{#snippet typeeditor()}
|
|
{#if isFlowInput || isAppInput}
|
|
<Label label="Type">
|
|
<ToggleButtonGroup
|
|
tabListClass="flex-wrap"
|
|
class="h-auto"
|
|
bind:selected={
|
|
() => computeSelected(schema.properties[opened ?? '']),
|
|
(v) => {
|
|
const isResource = v == 'resource'
|
|
const isS3 = v == 'S3'
|
|
const isOneOf = v == 'oneOf'
|
|
const isDynSelect = v == 'dynselect'
|
|
const isDynMultiselect = v == 'dynmultiselect'
|
|
const emptyProperty = {
|
|
contentEncoding: undefined,
|
|
enum_: undefined,
|
|
pattern: undefined,
|
|
default: undefined,
|
|
min: undefined,
|
|
max: undefined,
|
|
currency: undefined,
|
|
currencyLocale: undefined,
|
|
multiselect: undefined,
|
|
password: undefined,
|
|
dateFormat: undefined,
|
|
...(v == 'array' ? { items: { type: 'string' } } : {}),
|
|
showExpr: undefined,
|
|
nullable: undefined,
|
|
required: undefined
|
|
}
|
|
|
|
if (
|
|
isDynMultiselect &&
|
|
args &&
|
|
!Array.isArray(args?.[argName])
|
|
) {
|
|
args[argName] = []
|
|
}
|
|
|
|
if (isS3) {
|
|
schema.properties[argName] = {
|
|
...emptyProperty,
|
|
type: 'object',
|
|
format: 'resource-s3_object'
|
|
}
|
|
} else if (isResource) {
|
|
schema.properties[argName] = {
|
|
...emptyProperty,
|
|
type: 'object',
|
|
format: 'resource-'
|
|
}
|
|
} else if (isDynSelect || isDynMultiselect) {
|
|
const functionName = argName.replace(/\s+/g, '_')
|
|
schema.properties[argName] = {
|
|
...emptyProperty,
|
|
type: 'object',
|
|
format:
|
|
(isDynSelect ? 'dynselect-' : 'dynmultiselect-') +
|
|
functionName
|
|
}
|
|
updateDynCode(argName, dynLang)
|
|
dynamicSelectEditor?.setCode(dynCode || '')
|
|
} 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'
|
|
}
|
|
},
|
|
order: ['property_1']
|
|
},
|
|
{
|
|
title: 'Option 2',
|
|
type: 'object',
|
|
properties: {
|
|
label: {
|
|
type: 'string',
|
|
enum: ['Option 2']
|
|
},
|
|
property_2: {
|
|
type: 'string'
|
|
}
|
|
},
|
|
order: ['property_2']
|
|
}
|
|
]
|
|
}
|
|
} else {
|
|
schema.properties[argName] = {
|
|
...emptyProperty,
|
|
format: undefined,
|
|
type: v
|
|
}
|
|
}
|
|
schema.properties = schema.properties
|
|
}
|
|
}
|
|
>
|
|
{#snippet children({ item })}
|
|
{#each typeOptions as x}
|
|
<ToggleButton value={x[1]} label={x[0]} {item} />
|
|
{/each}
|
|
{#if showDynOpt}
|
|
{#each DYNAMIC_OPTIONS as x}
|
|
<ToggleButton value={x[1]} label={x[0]} {item} />
|
|
{/each}
|
|
{/if}
|
|
{/snippet}
|
|
</ToggleButtonGroup>
|
|
</Label>
|
|
{/if}
|
|
{/snippet}
|
|
|
|
{#if isFlowInput || isAppInput}
|
|
<FlowPropertyEditor
|
|
onDrawerClose={() => {
|
|
dndType = generateRandomString()
|
|
}}
|
|
bind:defaultValue={schema.properties[argName].default}
|
|
{variableEditor}
|
|
{itemPicker}
|
|
bind:nullable={schema.properties[argName].nullable}
|
|
bind:disabled={schema.properties[argName].disabled}
|
|
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}
|
|
pattern={schema.properties[argName].pattern}
|
|
password={schema.properties[argName].password}
|
|
propsNames={schema.properties[argName].propsNames}
|
|
bind:showExpr={schema.properties[argName].showExpr}
|
|
extra={schema.properties[argName]}
|
|
customErrorMessage={schema.properties[argName].customErrorMessage}
|
|
itemsType={schema.properties[argName].items}
|
|
bind:properties={schema.properties[argName].properties}
|
|
bind:order={schema.properties[argName].order}
|
|
bind:requiredProperty={schema.properties[argName].required}
|
|
{displayWebhookWarning}
|
|
on:requiredChange={(event) => {
|
|
if (event.detail.required) {
|
|
schema.required = schema.required ?? []
|
|
schema.required.push(argName)
|
|
} else {
|
|
schema.required = schema.required?.filter(
|
|
(x) => x !== argName
|
|
)
|
|
}
|
|
}}
|
|
/>
|
|
{/if}
|
|
</PropertyEditor>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
{:else if !shouldHideNoInputs}
|
|
<div class="text-secondary text-xs p-2">No inputs</div>
|
|
{/if}
|
|
</div>
|
|
{:else}
|
|
<div class="p-2">
|
|
<div class="border rounded h-full">
|
|
<SimpleEditor
|
|
bind:this={editor}
|
|
small
|
|
fixedOverflowWidgets={false}
|
|
on:change={() => {
|
|
try {
|
|
schema = JSON.parse(schemaString)
|
|
error = ''
|
|
} catch (err) {
|
|
error = err.message
|
|
}
|
|
}}
|
|
bind:code={schemaString}
|
|
lang="json"
|
|
autoHeight
|
|
automaticLayout
|
|
/>
|
|
</div>
|
|
{#if !emptyString(error)}
|
|
<div class="text-red-400 text-xs">{error}</div>
|
|
{:else}
|
|
<div><br /> </div>
|
|
{/if}
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</Pane>
|
|
{/if}
|
|
</Splitpanes>
|
|
</div>
|
|
|
|
{#if !noVariablePicker}
|
|
<ItemPicker
|
|
bind:this={itemPicker}
|
|
pickCallback={(path, _) => {
|
|
if (pickForField) {
|
|
if (args) {
|
|
args[pickForField] = '$var:' + path
|
|
}
|
|
}
|
|
}}
|
|
itemName="Variable"
|
|
tooltip="Variables are dynamic values that have a key associated to them and can be retrieved during the execution of a Script or Flow."
|
|
documentationLink="https://www.windmill.dev/docs/core_concepts/variables_and_secrets"
|
|
extraField="path"
|
|
loadItems={async () =>
|
|
(await VariableService.listVariable({ workspace: $workspaceStore ?? '' })).map((x) => ({
|
|
name: x.path,
|
|
...x
|
|
}))}
|
|
>
|
|
{#snippet submission()}
|
|
<div>
|
|
<Button
|
|
variant="default"
|
|
size="sm"
|
|
startIcon={{ icon: Plus }}
|
|
on:click={() => variableEditor?.initNew?.()}
|
|
>
|
|
New variable
|
|
</Button>
|
|
</div>
|
|
{/snippet}
|
|
</ItemPicker>
|
|
|
|
<VariableEditor bind:this={variableEditor} />
|
|
{/if}
|
|
|
|
<style>
|
|
:global(.splitter-hidden .splitpanes__splitter) {
|
|
background-color: transparent !important;
|
|
border: none !important;
|
|
opacity: 0 !important;
|
|
}
|
|
</style>
|