fix: schema editor reactivity improvements (#6496)

* all

* all

* all

* all

* all

* nit

* all

* all

* all

* all
This commit is contained in:
Ruben Fiszel
2025-08-30 02:32:35 +00:00
committed by GitHub
parent de15ca4671
commit d15510effa
31 changed files with 433 additions and 391 deletions
+2 -1
View File
@@ -25,7 +25,7 @@
"@scalar/openapi-parser": "^0.15.0",
"@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1",
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.2",
"@windmill-labs/svelte-dnd-action": "^0.9.48",
"@windmill-labs/svelte-dnd-action": "^0.9.44",
"@xterm/addon-fit": "^0.10.0",
"@xyflow/svelte": "^1.0.0",
"ag-charts-community": "^9.0.1",
@@ -3898,6 +3898,7 @@
"version": "0.9.48",
"resolved": "https://registry.npmjs.org/@windmill-labs/svelte-dnd-action/-/svelte-dnd-action-0.9.48.tgz",
"integrity": "sha512-A6pWayH3nOi79DZohTscGj5t2PuNtHlKM5WIAj9WlVDx5pS1A+MsyfuNZi8oCpj2590wFmQl+58G4y4xZqDa1Q==",
"license": "MIT",
"peerDependencies": {
"svelte": ">=3.23.0 || ^5.0.0-next.0"
}
+1 -1
View File
@@ -92,7 +92,7 @@
"@scalar/openapi-parser": "^0.15.0",
"@tanstack/svelte-table": "npm:tanstack-table-8-svelte-5@^0.1",
"@tutorlatin/svelte-tiny-virtual-list": "^3.0.2",
"@windmill-labs/svelte-dnd-action": "^0.9.48",
"@windmill-labs/svelte-dnd-action": "^0.9.44",
"@xterm/addon-fit": "^0.10.0",
"@xyflow/svelte": "^1.0.0",
"ag-charts-community": "^9.0.1",
+2 -2
View File
@@ -1,7 +1,7 @@
/// <reference types="@sveltejs/kit" />
declare type Item = import('svelte-dnd-action').Item
declare type DndEvent<ItemType = Item> = import('svelte-dnd-action').DndEvent<ItemType>
declare type Item = import('@windmill-labs/svelte-dnd-action').Item
declare type DndEvent<ItemType = Item> = import('@windmill-labs/svelte-dnd-action').DndEvent<ItemType>
declare namespace svelte.JSX {
interface HTMLAttributes<T> {
onconsider?: (event: CustomEvent<DndEvent<ItemType>> & { target: EventTarget & T }) => void
+12 -15
View File
@@ -992,8 +992,11 @@
}
}
bind:args={value}
dndType={`nested-${title}`}
hiddenArgs={['label', 'kind']}
hiddenArgs={[
oneOf?.find((o) => Object.keys(o.properties ?? {}).includes('kind'))
? 'kind'
: 'label'
]}
on:reorder={(e) => {
if (oneOf && oneOf[objIdx]) {
const keys = e.detail
@@ -1086,20 +1089,14 @@
{disablePortal}
{disabled}
{prettifyHeader}
bind:schema={
() => ({
properties,
$schema: '',
required: nestedRequired ?? [],
type: 'object',
order
}),
(newSchema) => {
dispatch('nestedChange')
}
}
schema={{
properties,
$schema: '',
required: nestedRequired ?? [],
type: 'object',
order
}}
bind:args={value}
dndType={`nested-${title}`}
on:reorder={(e) => {
const keys = e.detail
order = keys
@@ -14,13 +14,18 @@
import FlowPropertyEditor from './schema/FlowPropertyEditor.svelte'
import PropertyEditor from './schema/PropertyEditor.svelte'
import SimpleEditor from './SimpleEditor.svelte'
import { createEventDispatcher, tick, untrack } from '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 { DynamicSelect, emptyString } from '$lib/utils'
import {
DynamicSelect,
emptyString,
generateRandomString,
readFieldsRecursively
} from '$lib/utils'
import Popover from './meltComponents/Popover.svelte'
import SchemaFormDnd from './schema/SchemaFormDND.svelte'
import { deepEqual } from 'fast-equals'
@@ -48,7 +53,6 @@
isAppInput?: boolean
displayWebhookWarning?: boolean
onlyMaskPassword?: boolean
dndType?: string | undefined
editTab:
| 'inputEditor'
| 'history'
@@ -75,6 +79,8 @@
addProperty?: import('svelte').Snippet
runButton?: import('svelte').Snippet
extraTab?: import('svelte').Snippet
schemaFormClassName?: string
onChange?: (args: Record<string, any>) => void
}
let {
@@ -91,7 +97,6 @@
isAppInput = false,
displayWebhookWarning = false,
onlyMaskPassword = false,
dndType = undefined,
editTab,
previewSchema = undefined,
editPanelInitialSize = undefined,
@@ -110,7 +115,9 @@
openEditTab,
addProperty,
runButton,
extraTab
extraTab,
schemaFormClassName = undefined,
onChange = undefined
}: Props = $props()
$effect.pre(() => {
@@ -125,6 +132,13 @@
}
})
$effect(() => {
if (onChange) {
readFieldsRecursively(args)
onChange(args ?? {})
}
})
$effect(() => {
if (schema && dynSelectCode !== undefined && dynSelectLang !== undefined) {
if (dynSelectCode && dynSelectCode.trim()) {
@@ -154,9 +168,10 @@
let variableEditor: VariableEditor | undefined = $state(undefined)
let keys: string[] = $state(
Array.isArray(schema?.order)
(Array.isArray(schema?.order)
? [...schema.order]
: (Object.keys(schema?.properties ?? {}) ?? Object.keys(schema?.properties ?? {}))
).filter((x) => !hiddenArgs?.includes(x))
)
function alignOrderWithProperties(schema: {
@@ -187,22 +202,18 @@
return hasChanged
}
function onSchemaChange() {
let editSchema = false
if (alignOrderWithProperties(schema)) {
console.log('alignOrderWithProperties', JSON.stringify(schema, null, 2))
editSchema = true
// console.log('alignOrderWithProperties', JSON.stringify(schema, null, 2))
}
let lkeys = schema?.order ?? Object.keys(schema?.properties ?? {})
let lkeys = (schema?.order ?? Object.keys(schema?.properties ?? {})).filter(
(x) => !hiddenArgs?.includes(x)
)
if (schema?.properties && !deepEqual(lkeys, keys)) {
keys = [...lkeys]
editSchema = true
if (opened == undefined) {
opened = keys[0]
}
}
if (editSchema) {
schema = schema
}
}
let opened: string | undefined = $state(untrack(() => keys[0]))
@@ -248,30 +259,31 @@
// clear the input
el.value = oldName
} else {
let newSchema = $state.snapshot(schema)
if (args) {
args[newName] = args[oldName]
delete args[oldName]
}
schema.properties[newName] = schema.properties[oldName]
delete schema.properties[oldName]
newSchema.properties[newName] = newSchema.properties[oldName]
delete newSchema.properties[oldName]
if (schema.required?.includes(oldName)) {
schema.required = schema.required?.map((x) => (x === oldName ? newName : x))
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 (schema.order) {
const index = schema.order.indexOf(oldName)
if (newSchema.order) {
const index = newSchema.order.indexOf(oldName)
if (index !== -1) {
schema.order[index] = newName
newSchema.order[index] = newName
}
}
opened = newName
schema = $state.snapshot(schema)
dispatch('change', schema)
schema = newSchema
sendUserToast('Argument renamed')
}
}
@@ -368,6 +380,8 @@
const code = generateFn(functionName)
dynSelectCode = dynSelectCode ? dynSelectCode.concat(code) : code
}
let dndType = $state(generateRandomString())
</script>
<div class="w-full h-full">
@@ -404,15 +418,15 @@
class="min-h-0 overflow-y-auto grow rounded-md {runButton ? 'flex flex-col gap-2' : ''}"
>
<SchemaFormDnd
nestedClasses={'flex flex-col gap-1'}
{dndType}
nestedClasses={'flex flex-col gap-1 ' + (schemaFormClassName ?? '')}
bind:schema={
() => (previewSchema ? previewSchema : schema),
(newSchema) => {
schema = newSchema
tick().then(() => dispatch('change', schema))
}
}
{dndType}
{hiddenArgs}
{disableDnd}
{onlyMaskPassword}
bind:args
@@ -420,11 +434,16 @@
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
}
tick().then(() => dispatch('change', schema))
}}
helperScript={{
type: 'inline',
@@ -436,9 +455,6 @@
{diff}
on:acceptChange
on:rejectChange
on:nestedChange={() => {
dispatch('change', schema)
}}
{shouldDispatchChanges}
bind:isValid
noVariablePicker={noVariablePicker || customUi?.disableVariablePicker === true}
@@ -446,8 +462,8 @@
{@render runButton?.()}
<div class="h-full">
{#if dynSelectFunctions.length > 0}
{#if dynSelectFunctions.length > 0}
<div class="grow">
<Section
label="Dynamic select functions"
collapsable={true}
@@ -494,8 +510,8 @@
{/key}
</div>
</Section>
{/if}
</div>
</div>
{/if}
</div>
</div>
</Pane>
@@ -514,7 +530,7 @@
{#if jsonEnabled && customUi?.jsonOnly != true}
<div class="w-full p-3 flex gap-4 justify-end items-center">
{#if addPropertyInEditorTab}
<AddPropertyV2 bind:schema on:change>
<AddPropertyV2 bind:schema>
{#snippet trigger()}
<Button color="light" size="xs" iconOnly startIcon={{ icon: Plus }} />
{/snippet}
@@ -629,7 +645,7 @@
</div>
{#if opened === argName}
<div class="p-4 border-t">
{#if !hiddenArgs.includes(argName) && Object.keys(schema?.properties ?? {}).includes(argName)}
{#if Object.keys(schema?.properties ?? {}).includes(argName)}
{#if typeof args == 'object' && schema?.properties[argName]}
<PropertyEditor
bind:description={schema.properties[argName].description}
@@ -650,10 +666,6 @@
bind:order={schema.properties[argName].order}
{isFlowInput}
{isAppInput}
on:change={() => {
schema = $state.snapshot(schema)
dispatch('change', schema)
}}
>
{#snippet typeeditor()}
{#if isFlowInput || isAppInput}
@@ -742,13 +754,9 @@
type: v
}
}
schema.properties = schema.properties
}
}
on:selected={(e) => {
schema = schema
dispatch('change', schema)
dispatch('schemaChange')
}}
>
{#snippet children({ item })}
{#each typeOptions as x}
@@ -762,6 +770,9 @@
{#if isFlowInput || isAppInput}
<FlowPropertyEditor
onDrawerClose={() => {
dndType = generateRandomString()
}}
bind:defaultValue={schema.properties[argName].default}
{variableEditor}
{itemPicker}
@@ -792,12 +803,6 @@
(x) => x !== argName
)
}
dispatch('change', schema)
}}
on:schemaChange={(e) => {
schema = $state.snapshot(schema)
dispatch('change', schema)
dispatch('schemaChange')
}}
/>
{/if}
@@ -822,7 +827,6 @@
on:change={() => {
try {
schema = JSON.parse(schemaString)
dispatch('change', schema)
error = ''
} catch (err) {
error = err.message
+11 -7
View File
@@ -1,4 +1,4 @@
<script context="module">
<script module>
import { tick } from 'svelte'
export function portal(el, options) {
@@ -50,13 +50,17 @@
<script>
/**
* DOM Element or CSS Selector
* @type { HTMLElement|string}
* @typedef {Object} Props
* @property { HTMLElement|string} [target] - DOM Element or CSS Selector
* @property {any} [name]
* @property {string|undefined} [class]
* @property {import('svelte').Snippet} [children]
*/
export let target = 'body'
export let name = undefined
/** @type {Props} */
let { target = 'body', name = undefined, class: clazz = undefined, children } = $props()
</script>
<div use:portal={{ target, name }} hidden class={$$props.class}>
<slot />
<div use:portal={{ target, name }} hidden class={clazz}>
{@render children?.()}
</div>
@@ -108,7 +108,7 @@
className = '',
computeS3ForceViewerPolicies = undefined,
workspace = undefined,
actions
actions: actions_render = undefined
}: Props = $props()
const dispatch = createEventDispatcher()
@@ -249,8 +249,6 @@
$effect.pre(() => {
isValid = allTrue(inputCheck ?? {})
})
const actions_render = $derived(actions)
</script>
{#if showReset}
@@ -173,11 +173,11 @@
<Disposable
{id}
bind:this={disposable}
on:open={() => {
onOpen={() => {
outputs?.open.set(true)
onOpenRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
}}
on:close={() => {
onClose={() => {
outputs?.open.set(false)
onCloseRecomputeIds?.forEach((id) => $runnableComponents?.[id]?.cb?.map((cb) => cb?.()))
}}
@@ -10,6 +10,7 @@
import { AppService, DraftService, SettingService, type Policy } from '$lib/gen'
import { redo, undo } from '$lib/history.svelte'
import { enterpriseLicense, userStore, workspaceStore } from '$lib/stores'
import type { Item } from '$lib/utils'
import {
AlignHorizontalSpaceAround,
BellOff,
@@ -12,9 +12,8 @@
} from './componentCallbacks.svelte'
import type { AppEditorContext, AppViewerContext } from '../../types'
const { history, movingcomponents, jobsDrawerOpen } = getContext<AppEditorContext>(
'AppEditorContext'
) as AppEditorContext
const { history, movingcomponents, jobsDrawerOpen, runnableJobEditorPanel } =
getContext<AppEditorContext>('AppEditorContext') as AppEditorContext
const { app, selectedComponent, focusedGrid, componentControl } = getContext<AppViewerContext>(
'AppViewerContext'
) as AppViewerContext
@@ -33,7 +32,8 @@
let classes = event.target?.['className']
if (
(typeof classes === 'string' && classes.includes('inputarea')) ||
['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName!)
['INPUT', 'TEXTAREA'].includes(document.activeElement?.tagName!) ||
$runnableJobEditorPanel.focused
) {
return
}
@@ -308,7 +308,7 @@ export async function handlePaste(
} else {
copiedGridItems = undefined
}
} catch {}
} catch { }
}
if (tempGridItems != undefined) {
@@ -237,6 +237,8 @@
$effect(() => {
items != undefined && handleItemsChange()
})
const rnd = generateRandomString()
</script>
<div class="flex gap-2 flex-col mt-2 w-full">
@@ -286,7 +288,8 @@
use:dragHandleZone={{
items,
flipDurationMs,
dropTargetStyle: {}
dropTargetStyle: {},
type: rnd
}}
onconsider={handleConsider}
onfinalize={handleFinalize}
@@ -126,6 +126,8 @@
})
component.numberOfSubgrids = items.length + 1
}
const rnd = generateRandomString()
</script>
<PanelSection title={'Conditions'}>
@@ -141,7 +143,8 @@
use:dragHandleZone={{
items: items,
flipDurationMs: 200,
dropTargetStyle: {}
dropTargetStyle: {},
type: rnd
}}
onconsider={handleConsider}
onfinalize={handleFinalize}
@@ -121,6 +121,8 @@
let resolvedPaths: string[] = $state([])
let resolvedLabels: string[] = $state([])
const rnd = generateRandomString()
</script>
<PanelSection
@@ -134,7 +136,8 @@
use:dragHandleZone={{
items,
flipDurationMs: 200,
dropTargetStyle: {}
dropTargetStyle: {},
type: rnd
}}
onconsider={handleConsider}
onfinalize={handleFinalize}
@@ -158,6 +158,8 @@
})
}
}
const rnd = generateRandomString()
</script>
<PanelSection title={`${word}s ${tabs && tabs.length > 0 ? `(${tabs.length})` : ''}`}>
@@ -169,7 +171,8 @@
use:dragHandleZone={{
items,
flipDurationMs: 200,
dropTargetStyle: {}
dropTargetStyle: {},
type: rnd
}}
onconsider={handleConsider}
onfinalize={handleFinalize}
@@ -103,6 +103,8 @@
items = newItems
}
const rnd = generateRandomString()
</script>
{#if components}
@@ -132,7 +134,8 @@
use:dragHandleZone={{
items,
flipDurationMs: 200,
dropTargetStyle: {}
dropTargetStyle: {},
type: rnd
}}
onconsider={handleConsider}
onfinalize={handleFinalize}
@@ -1,25 +1,33 @@
<script lang="ts" context="module">
export let openedDrawers: string[] = []
<script lang="ts" module>
export let openedDrawers: { val: string[] } = $state({ val: [] })
</script>
<script lang="ts">
import { createDispatcherIfMounted } from '$lib/createDispatcherIfMounted'
import { zIndexes } from '$lib/zIndexes'
import { untrack } from 'svelte'
import { createEventDispatcher } from 'svelte'
export let open = false
export let id = (Math.random() + 1).toString(36).substring(10)
export let preventEscape = false
if (open) {
openedDrawers.push(id)
interface Props {
open?: boolean
id?: any
preventEscape?: boolean
initialOffset?: number
children?: import('svelte').Snippet<[any]>
onOpen?: () => void
onClose?: () => void
}
export let initialOffset = 0
let {
open = $bindable(false),
id = (Math.random() + 1).toString(36).substring(10),
preventEscape = false,
initialOffset = 0,
children,
onOpen,
onClose
}: Props = $props()
let offset = initialOffset
let offset = $state(initialOffset)
let zIndex = $derived(zIndexes.disposables + offset)
export function toggleDrawer() {
if (!open) {
@@ -30,32 +38,31 @@
}
export function openDrawer() {
open = true
offset = openedDrawers.length - 1
if (openedDrawers.includes(id)) {
return
if (!open) {
open = true
if (openedDrawers.val.includes(id)) {
return
}
offset = openedDrawers.val.length - 1
openedDrawers.val.push(id)
}
openedDrawers.push(id)
}
export function closeDrawer() {
open = false
offset = initialOffset
// remove the last opened drawer
openedDrawers = openedDrawers.filter((drawer) => drawer !== id)
if (open) {
open = false
offset = initialOffset
// remove the last opened drawer
openedDrawers.val = openedDrawers.val.filter((drawer) => drawer !== id)
}
}
export function isOpen() {
return open
}
const dispatch = createEventDispatcher()
const dispatchIfMounted = createDispatcherIfMounted(dispatch)
function handleClickAway(e) {
const last = openedDrawers[openedDrawers.length - 1]
const last = openedDrawers.val[openedDrawers.val.length - 1]
if (last === id) {
e.stopPropagation()
@@ -68,10 +75,11 @@
switch (event.key) {
case 'Escape':
if (
(id == openedDrawers[openedDrawers.length - 1] || openedDrawers.length == 0) &&
(id == openedDrawers.val[openedDrawers.val.length - 1] ||
openedDrawers.val.length == 0) &&
!preventEscape
) {
openedDrawers.pop()
openedDrawers.val.pop()
event.preventDefault()
event.stopPropagation()
event.stopImmediatePropagation()
@@ -82,11 +90,34 @@
}
}
$: zIndex = zIndexes.disposables + offset
if (open) {
openedDrawers.val.push(id)
}
$: dispatchIfMounted(open ? 'open' : 'close')
let wasEverOpen = false
let lastOpen = open
$effect.pre(() => {
if (open === untrack(() => lastOpen)) {
return
}
lastOpen = open
if (open) {
// console.log('open', id, wasEverOpen)
wasEverOpen = true
onOpen?.()
} else if (untrack(() => wasEverOpen)) {
// console.log('close', id)
onClose?.()
}
})
</script>
<svelte:window on:keydown={onKeyDown} />
<svelte:window onkeydown={onKeyDown} />
<slot {handleClickAway} {zIndex} {closeDrawer} {open} />
{@render children?.({
handleClickAway,
zIndex,
closeDrawer,
open,
isTop: openedDrawers.val[openedDrawers.val.length - 1] == id
})}
@@ -1,5 +1,5 @@
<script lang="ts">
import { onMount, createEventDispatcher } from 'svelte'
import { onMount, createEventDispatcher, untrack } from 'svelte'
import { BROWSER } from 'esm-env'
import Disposable from './Disposable.svelte'
import ConditionalPortal from './ConditionalPortal.svelte'
@@ -32,7 +32,7 @@
disableChatOffset = false,
class: clazz = '',
positionClass = undefined,
children
children: children_render
}: Props = $props()
if (open === undefined) {
@@ -52,11 +52,12 @@
}
export function closeDrawer() {
if (open) {
setTimeout(() => {
dispatch('afterClose')
}, durationMs)
}
disposable?.closeDrawer()
setTimeout(() => {
dispatch('afterClose')
}, durationMs)
}
export function isOpen() {
@@ -83,7 +84,10 @@
})
$effect(() => {
open ? openDrawer() : closeDrawer()
open
untrack(() => {
open ? openDrawer() : closeDrawer()
})
})
let timeout = $state(true)
@@ -94,7 +98,6 @@
mounted = true
})
const children_render = $derived(children)
const aiChatOpen = $derived(chatState.size > 0)
</script>
@@ -103,11 +106,11 @@
initialOffset={offset}
bind:open
bind:this={disposable}
on:open
on:close
onOpen={() => dispatch('open')}
onClose={() => dispatch('close')}
{preventEscape}
>
{#snippet children({ handleClickAway, zIndex })}
{#snippet children({ handleClickAway, zIndex, isTop })}
<aside
class="drawer windmill-app windmill-drawer {clazz ?? ''} {positionClass ?? ''} {aiChatOpen
? 'respect-global-chat'
@@ -122,7 +125,7 @@
<div class="overlay {positionClass ?? ''}" onclick={handleClickAway}></div>
<div class="panel {placement} {positionClass}" class:size>
{#if open || !timeout || alwaysOpen}
{@render children_render?.({ open })}
{@render children_render?.({ open, isTop })}
{/if}
</div>
</aside>
@@ -40,9 +40,10 @@
} from '$lib/components/schema/schemaUtils.svelte'
import SideBarTab from '$lib/components/meltComponents/SideBarTab.svelte'
import CaptureTable from '$lib/components/triggers/CaptureTable.svelte'
import { isObjectTooBig } from '$lib/utils'
import { isObjectTooBig, readFieldsRecursively } from '$lib/utils'
import { refreshStateStore } from '$lib/svelte5Utils.svelte'
import type { ScriptLang } from '$lib/gen'
import { deepEqual } from 'fast-equals'
interface Props {
noEditor: boolean
@@ -333,11 +334,19 @@
function resetArgs() {
if (!previewSchema) {
// previewArguments = undefined
savedPreviewArgs = undefined
}
}
$effect(() => {
if (!previewArgs && savedPreviewArgs != undefined) {
readFieldsRecursively(flowStore.val.schema)
untrack(() => {
resetArgs()
})
}
})
let historicInputs: HistoricInputs | undefined = $state(undefined)
let captureTable: CaptureTable | undefined = $state(undefined)
let savedInputsPicker: SavedInputsPicker | undefined = $state(undefined)
@@ -363,9 +372,6 @@
bind:this={editableSchemaForm}
bind:schema={flowStore.val.schema}
isFlowInput
on:edit={(e) => {
addPropertyV2?.openDrawer(e.detail)
}}
on:delete={(e) => {
addPropertyV2?.handleDeleteArgument([e.detail])
}}
@@ -399,14 +405,13 @@
})
}}
shouldDispatchChanges={true}
on:change={() => {
onChange={() => {
if (!previewSchema) {
savedPreviewArgs = structuredClone($state.snapshot(previewArgs.val))
let args = $state.snapshot(previewArgs.val)
if (!deepEqual(args, savedPreviewArgs)) {
savedPreviewArgs = args
}
}
refreshStateStore(flowStore)
}}
on:schemaChange={() => {
resetArgs()
}}
bind:isValid
bind:dynSelectCode
@@ -475,15 +480,9 @@
<AddPropertyV2
bind:schema={flowStore.val.schema}
bind:this={addPropertyV2}
on:change={() => {
refreshStateStore(flowStore)
if (editableSchemaForm) {
editableSchemaForm.updateJson()
}
}}
on:addNew={(e) => {
onAddNew={(argName) => {
handleEditSchema('inputEditor')
editableSchemaForm?.openField(e.detail)
editableSchemaForm?.openField(argName)
refreshStateStore(flowStore)
}}
>
@@ -1,6 +1,4 @@
<script lang="ts">
import { run } from 'svelte/legacy'
import Toggle from '$lib/components/Toggle.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
import InputTransformForm from '$lib/components/InputTransformForm.svelte'
@@ -49,7 +47,7 @@
}
}
run(() => {
$effect(() => {
if ($workspaceStore && allUserGroups.length === 0) {
untrack(() => {
loadGroups()
@@ -57,9 +55,22 @@
}
})
$effect(() => {
// If the schema is empty, remove the form
if (Object.keys(flowModule?.suspend?.resume_form?.schema?.properties ?? {}).length === 0) {
untrack(() => {
tick().then(() => {
if (!flowModule.suspend) return
flowModule.suspend.resume_form = undefined
})
})
}
})
let jsonView: boolean = $state(false)
</script>
A{JSON.stringify(flowModule.suspend?.resume_form)}B
<Section label="Suspend/Approval/Prompt" class="w-full">
{#snippet action()}
<SuspendDrawer text="Approval/Prompt helpers" />
@@ -234,21 +245,7 @@
<div class="grid grid-cols-4 mt-4 gap-8">
<div class="col-span-2">
{#if flowModule?.suspend?.resume_form}
<EditableSchemaDrawer
bind:schema={flowModule.suspend.resume_form.schema}
on:change={(e) => {
const schema = e.detail
// If the schema is empty, remove the form
if (Object.keys(schema?.properties ?? {}).length === 0) {
tick().then(() => {
if (!flowModule.suspend) return
flowModule.suspend.resume_form = undefined
})
}
}}
{jsonView}
/>
<EditableSchemaDrawer bind:schema={flowModule.suspend.resume_form.schema} {jsonView} />
{:else if emptyString($enterpriseLicense)}
<Alert type="warning" title="Adding a form to the approval page is an EE feature" />
{:else}
@@ -23,6 +23,7 @@
export async function openDrawer(hash: string, cb: () => void): Promise<void> {
script = undefined
closeAnyway = false
scriptEditorDrawer?.openDrawer?.()
script = await ScriptService.getScriptByHash({
workspace: $workspaceStore!,
@@ -114,7 +115,6 @@
async function checkForUnsavedChanges() {
if (closeAnyway) {
scriptEditorDrawer?.closeDrawer()
closeAnyway = false
return
}
if (savedScript && script) {
@@ -122,12 +122,15 @@
const current = cleanValueProperties(script)
if (orderedJsonStringify(saved) !== orderedJsonStringify(current)) {
unsavedModalOpen = true
scriptEditorDrawer?.openDrawer()
} else {
scriptEditorDrawer?.closeDrawer()
}
}
}
let args = $state({})
let displayEditor = $state(true)
</script>
<ConfirmationModal
@@ -138,8 +141,9 @@
unsavedModalOpen = false
}}
on:confirmed={() => {
unsavedModalOpen = false
console.log('confirmed')
closeAnyway = true
unsavedModalOpen = false
scriptEditorDrawer?.closeDrawer()
}}
>
@@ -156,7 +160,7 @@
}
unsavedModalOpen = false
closeAnyway = true
scriptEditorDrawer?.closeDrawer()
displayEditor = false
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
title: 'Saved <> Current',
@@ -182,7 +186,6 @@
bind:this={scriptEditorDrawer}
size="1200px"
on:close={() => {
scriptEditorDrawer?.openDrawer()
checkForUnsavedChanges()
}}
>
@@ -195,7 +198,7 @@
scriptEditorDrawer?.closeDrawer()
}}
>
{#if script}
{#if script && displayEditor}
{#key script.hash}
<ScriptEditor
showCaptures={false}
@@ -236,7 +239,7 @@
return
}
closeAnyway = true
scriptEditorDrawer?.closeDrawer()
displayEditor = false
diffDrawer?.openDrawer()
diffDrawer?.setDiff({
mode: 'simple',
@@ -276,10 +279,6 @@
<DiffDrawer
bind:this={diffDrawer}
on:close={() => {
if (!closeAnyway) {
scriptEditorDrawer?.openDrawer()
} else {
closeAnyway = false
}
displayEditor = true
}}
/>
@@ -83,6 +83,13 @@
if (Object.keys(arg).length === 1 && Object.keys(arg).includes('s3')) {
return 's3object'
}
if (
Object.keys(arg).length === 2 &&
Object.keys(arg).includes('s3') &&
Object.keys(arg).includes('filename')
) {
return 's3object'
}
return typeof arg
}
@@ -172,9 +179,12 @@
</span>
{/snippet}
<Portal name="object-viewer">
<S3FilePicker bind:this={s3FileViewer} readOnlyMode={true} />
</Portal>
{#if getTypeAsString(jsonFiltered) === 's3object'}
<Portal name="object-viewer">
<S3FilePicker bind:this={s3FileViewer} readOnlyMode={true} />
</Portal>
{/if}
{#if level == 0}
<div class="float-right">
{#if searchOpen}
@@ -1,15 +1,15 @@
<script lang="ts">
import { type Schema, modalToSchema, type ModalSchemaProperty } from '$lib/common'
import { emptySchema, sendUserToast } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import AddPropertyFormV2 from './AddPropertyFormV2.svelte'
interface Props {
schema?: Schema | any
trigger?: import('svelte').Snippet
noPopover?: boolean
onAddNew?: (argName: string) => void
}
let { schema = $bindable(emptySchema()), trigger, noPopover }: Props = $props()
let { schema = $bindable(emptySchema()), trigger, noPopover, onAddNew }: Props = $props()
export const DEFAULT_PROPERTY: ModalSchemaProperty = {
selectedType: 'string',
@@ -18,8 +18,6 @@
required: false
}
const dispatch = createEventDispatcher()
if (!schema) {
schema = emptySchema()
}
@@ -107,7 +105,6 @@
editing = false
oldArgName = undefined
schema = $state.snapshot(newSchema)
dispatch('change', schema)
}
if (argError !== '') {
@@ -139,14 +136,11 @@
if (modifiedObject.order) {
modifiedObject.order = modifiedObject.order.filter((arg) => arg !== argName)
}
dispatch('change', schema)
} else {
throw Error('Argument not found!')
}
syncOrders(modifiedObject)
schema = $state.snapshot(modifiedObject)
dispatch('change', schema)
} catch (err) {
sendUserToast(`Could not delete argument: ${err}`, true)
}
@@ -164,7 +158,7 @@
selectedType: 'string',
name: e.detail.name
})
dispatch('addNew', e.detail.name)
onAddNew?.(e.detail.name)
} catch (err) {
sendUserToast(`Could not add argument: ${err}`, true)
}
@@ -7,7 +7,7 @@
import AddProperty from './AddProperty.svelte'
import { dragHandle, dragHandleZone } from '@windmill-labs/svelte-dnd-action'
import { flip } from 'svelte/animate'
import { emptyString } from '$lib/utils'
import { emptyString, generateRandomString } from '$lib/utils'
import Button from '$lib/components/common/button/Button.svelte'
import { createEventDispatcher, tick } from 'svelte'
import Label from '../Label.svelte'
@@ -50,11 +50,16 @@
interface Props {
schema: Schema | undefined | any
parentId?: string | undefined
jsonView?: boolean
hiddenArgs?: string[]
onClose?: () => void
}
let { schema = $bindable(), parentId = undefined, jsonView = $bindable(false) }: Props = $props()
let {
schema = $bindable(),
jsonView = $bindable(false),
hiddenArgs = undefined
}: Props = $props()
// let schema = $state(structuredClone($state.snapshot(schema)))
@@ -63,14 +68,18 @@
let error: string | undefined = $state(undefined)
let items = $derived([
...new Set(
(schema?.order ?? Object.keys(schema?.properties ?? {}))?.map((item, index) => {
return { value: item, id: item }
}) ?? []
(schema?.order ?? Object.keys(schema?.properties ?? {}))
?.map((item, index) => {
return { value: item, id: item }
})
.filter((item) => !hiddenArgs?.includes(item.value)) ?? []
)
]) as Array<{
value: string
id: string
}>
const rnd = generateRandomString()
</script>
<div class="flex flex-col items-end mb-2 w-full">
@@ -103,124 +112,115 @@
/>
{#if !jsonView}
<div
use:dragHandleZone={{
items,
flipDurationMs,
dropTargetStyle: {},
type: parentId ? `app-editor-fields-${parentId}` : 'app-editor-fields'
}}
onconsider={handleConsider}
onfinalize={handleFinalize}
class="gap-1 flex flex-col mt-2"
>
{#if items?.length > 0}
{#each items as item (item.id)}
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<div
animate:flip={{ duration: 200 }}
class="w-full flex flex-col justify-between border items-center py-1 px-2 rounded-md bg-surface text-sm"
>
{#if schema.properties?.[item.value]}
<div class="flex flex-row justify-between items-center w-full">
{`${item.value}${
schema.properties?.[item.value]?.title
? ` (title: ${schema.properties?.[item.value]?.title})`
: ''
} `}
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="flex flex-row gap-1 item-center h-full justify-center">
<Button
iconOnly
size="xs2"
color="light"
startIcon={{ icon: Trash }}
on:click={() => {
addPropertyComponent?.handleDeleteArgument([item.value])
}}
/>
<Button
iconOnly
size="xs2"
color="light"
startIcon={{ icon: Pen }}
on:click={() => {
schemaFormDrawer?.openDrawer()
{#key rnd}
<div
use:dragHandleZone={{
items,
flipDurationMs,
dropTargetStyle: {},
type: rnd
}}
onconsider={handleConsider}
onfinalize={handleFinalize}
class="gap-1 flex flex-col mt-2"
>
{#if items?.length > 0}
{#each items as item (item.id)}
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<div
animate:flip={{ duration: 200 }}
class="w-full flex flex-col justify-between border items-center py-1 px-2 rounded-md bg-surface text-sm"
>
{#if schema.properties?.[item.value]}
<div class="flex flex-row justify-between items-center w-full">
{`${item.value}${
schema.properties?.[item.value]?.title
? ` (title: ${schema.properties?.[item.value]?.title})`
: ''
} `}
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="flex flex-row gap-1 item-center h-full justify-center">
<Button
iconOnly
size="xs2"
color="light"
startIcon={{ icon: Trash }}
on:click={() => {
addPropertyComponent?.handleDeleteArgument([item.value])
}}
/>
<Button
iconOnly
size="xs2"
color="light"
startIcon={{ icon: Pen }}
on:click={() => {
schemaFormDrawer?.openDrawer()
tick().then(() => {
editableSchemaForm?.openField(item.value)
})
}}
/>
tick().then(() => {
editableSchemaForm?.openField(item.value)
})
}}
/>
<div class="cursor-move flex items-center handle" use:dragHandle>
<GripVertical size={16} />
<div class="flex items-center handle" use:dragHandle>
<GripVertical size={16} />
</div>
</div>
</div>
</div>
{#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">
<EditableSchemaDrawer
on:change={() => {
schema = $state.snapshot(schema)
dispatch('change', schema)
}}
bind:schema={schema.properties[item.value]}
parentId={item.value}
/>
</Label>
</div>
{#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">
<EditableSchemaDrawer bind:schema={schema.properties[item.value]} />
</Label>
</div>
{/if}
{:else}
<div class="text-tertiary"> Value is undefined </div>
{/if}
{:else}
<div class="text-tertiary"> Value is undefined </div>
{/if}
</div>
{/each}
{/if}
</div>
</div>
{/each}
{/if}
</div>
{/key}
<Drawer bind:this={schemaFormDrawer} size="1200px">
<DrawerContent title="UI Customisation" on:close={schemaFormDrawer.closeDrawer}>
<EditableSchemaForm
on:change={(e) => {
schema = $state.snapshot(schema)
dispatch('change', schema)
}}
bind:this={editableSchemaForm}
bind:schema
isAppInput
on:edit={(e) => {
addPropertyComponent?.openDrawer(e.detail)
}}
on:delete={(e) => {
addPropertyComponent?.handleDeleteArgument([e.detail])
}}
dndType="drawer"
editTab="inputEditor"
>
{#snippet addProperty()}
<AddPropertyV2
bind:schema
on:change
on:addNew={(e) => {
schema = $state.snapshot(schema)
editableSchemaForm?.openField(e.detail)
}}
>
{#snippet trigger()}
<div
class="w-full py-2 flex justify-center items-center border border-dashed rounded-md hover:bg-surface-hover"
>
<Plus size={14} />
</div>
{/snippet}
</AddPropertyV2>
{/snippet}
</EditableSchemaForm>
</DrawerContent>
{#snippet children()}
<DrawerContent title="UI Customisation" on:close={() => schemaFormDrawer?.closeDrawer()}>
<EditableSchemaForm
schemaFormClassName="h-full"
bind:this={editableSchemaForm}
bind:schema
isAppInput
on:edit={(e) => {
addPropertyComponent?.openDrawer(e.detail)
}}
on:delete={(e) => {
addPropertyComponent?.handleDeleteArgument([e.detail])
}}
{hiddenArgs}
editTab="inputEditor"
>
{#snippet addProperty()}
<AddPropertyV2
bind:schema
onAddNew={(argName) => {
editableSchemaForm?.openField(argName)
}}
>
{#snippet trigger()}
<div
class="w-full py-2 flex justify-center items-center border border-dashed rounded-md hover:bg-surface-hover"
>
<Plus size={14} />
</div>
{/snippet}
</AddPropertyV2>
{/snippet}
</EditableSchemaForm>
</DrawerContent>
{/snippet}
</Drawer>
{:else}
<div class="mt-2"></div>
@@ -1,10 +1,28 @@
<script lang="ts">
import { readFieldsRecursively } from '$lib/utils'
import { deepEqual } from 'fast-equals'
import type { EditableSchemaWrapperProps } from './editable_schema_wrapper'
import EditableSchemaWrapper from './EditableSchemaWrapper.svelte'
let { schema: oldSchema, ...props }: EditableSchemaWrapperProps = $props()
let {
schema: oldSchema,
onSchemaChange,
...props
}: EditableSchemaWrapperProps & { onSchemaChange?: (schema: any) => void } = $props()
let schema = $state(oldSchema)
let lastSchema = $state.snapshot(schema)
$effect(() => {
if (onSchemaChange) {
readFieldsRecursively(schema)
let newSchema = $state.snapshot(schema)
if (!deepEqual(lastSchema, newSchema)) {
lastSchema = newSchema
onSchemaChange(newSchema)
}
}
})
</script>
<EditableSchemaWrapper {schema} {...props} />
<EditableSchemaWrapper bind:schema {...props} />
@@ -16,7 +16,6 @@
noPreview = false,
fullHeight = true,
formatExtension = $bindable(undefined),
onSchemaChange,
customUi
}: EditableSchemaWrapperProps = $props()
@@ -51,7 +50,6 @@
}
}
}
onSchemaChange?.({ schema: $state.snapshot(schema) })
}
let suggestedFileExtensions = $state([
@@ -81,9 +79,8 @@
noPopover={customUi?.noAddPopover}
bind:schema
bind:this={addPropertyComponent}
on:change={() => onSchemaChange?.({ schema: $state.snapshot(schema) })}
on:addNew={(e) => {
editableSchemaForm?.openField(e.detail)
onAddNew={(argName) => {
editableSchemaForm?.openField(argName)
}}
>
{#snippet trigger()}
@@ -99,11 +96,7 @@
onlyMaskPassword
bind:this={editableSchemaForm}
bind:schema
on:change={() => onSchemaChange?.({ schema: $state.snapshot(schema) })}
isFlowInput
on:edit={(e) => {
addPropertyComponent?.openDrawer(e.detail)
}}
on:delete={(e) => {
addPropertyComponent?.handleDeleteArgument([e.detail])
}}
@@ -117,7 +110,6 @@
noPopover={customUi?.noAddPopover}
bind:schema
bind:this={addPropertyComponent}
on:change={() => onSchemaChange?.({ schema })}
>
{#snippet trigger()}
<div
@@ -49,6 +49,7 @@
order?: string[] | undefined
requiredProperty?: string[] | undefined
displayWebhookWarning?: boolean
onDrawerClose?: () => void
}
let {
@@ -72,10 +73,11 @@
properties = $bindable(undefined),
order = $bindable(undefined),
requiredProperty = $bindable(undefined),
displayWebhookWarning = true
displayWebhookWarning = true,
onDrawerClose = undefined
}: Props = $props()
let oneOfSelected: string | undefined = $state(undefined)
let oneOfSelected: string | undefined = $state(oneOf?.[0]?.title)
const dispatch = createEventDispatcher()
@@ -259,44 +261,13 @@
{#if oneOfSelected && oneOf}
{@const idx = oneOf.findIndex((obj) => obj.title === oneOfSelected)}
<EditableSchemaDrawer
bind:schema={
() => {
if (oneOf?.[idx]) {
let properties = Object.fromEntries(
Object.entries(oneOf[idx].properties ?? {}).filter(
([k]) => k !== 'label' && k !== 'kind'
)
)
return {
...oneOf[idx],
properties: properties,
order: Object.keys(properties),
required: oneOf[idx].required ?? []
}
}
},
(v) => {
if (oneOf?.[idx]) {
const tagKey = oneOf?.find((o) => Object.keys(o.properties ?? {}).includes('kind'))
? 'kind'
: 'label'
oneOf[idx] = {
...(v ?? {}),
type: 'object',
properties: {
...(v?.properties ?? {}),
[tagKey]: {
type: 'string',
enum: [v?.title ?? '']
}
}
}
}
}
}
on:change={() => {
dispatch('schemaChange')
onClose={() => {
onDrawerClose?.()
}}
bind:schema={oneOf[idx]}
hiddenArgs={[
oneOf?.find((o) => Object.keys(o.properties ?? {}).includes('kind')) ? 'kind' : 'label'
]}
/>
{/if}
{:else if type === 'object' && format !== 'resource-s3_object' && !isDynSelect}
@@ -359,7 +330,6 @@
properties = v.properties
order = v.order
requiredProperty = v.required
dispatch('schemaChange')
}
}
/>
@@ -382,7 +352,7 @@
</TabContent>
<TabContent value="resource">
<ObjectTypeNarrowing on:change={() => dispatch('schemaChange')} bind:format />
<ObjectTypeNarrowing bind:format />
</TabContent>
</div>
{/snippet}
@@ -6,9 +6,8 @@
import type { Schema } from '$lib/common'
import { deepEqual } from 'fast-equals'
import type { SchemaDiff } from '$lib/components/schema/schemaUtils.svelte'
import type { DynamicSelect } from '$lib/utils'
import { generateRandomString, type DynamicSelect } from '$lib/utils'
interface Props {
dndType?: string | undefined
schema: Schema
args?: Record<string, any>
prettifyHeader?: boolean
@@ -24,10 +23,11 @@
isValid?: boolean
noVariablePicker?: boolean
helperScript?: DynamicSelect.HelperScript
className?: string
dndType?: string
}
let {
dndType = undefined,
schema = $bindable(),
args = $bindable(undefined),
prettifyHeader = false,
@@ -42,7 +42,9 @@
diff = {},
nestedClasses = '',
isValid = $bindable(true),
noVariablePicker = false
noVariablePicker = false,
className = '',
dndType = generateRandomString()
}: Props = $props()
$effect.pre(() => {
@@ -55,14 +57,16 @@
let items = $state(computeItems())
let dragDisabled = $state(true)
let dragDisabledState = $state(true)
function computeItems() {
return (
(schema?.order ?? Object.keys(schema?.properties ?? {}) ?? []).map((key) => ({
id: key,
value: key
})) ?? []
($state.snapshot(schema?.order) ?? Object.keys(schema?.properties ?? {}) ?? []).map(
(key) => ({
id: key,
value: key
})
) ?? []
)
}
@@ -74,23 +78,20 @@
}
function handleConsider(e) {
dragDisabled = false
dragDisabledState = false
const { items: newItems } = e.detail
items = newItems
items = $state.snapshot(newItems)
}
function handleFinalize(e) {
const { items: newItems } = e.detail
dragDisabled = true
items = newItems
dragDisabledState = true
items = $state.snapshot(newItems)
const newOrder = items.map((item) => item.value)
// console.log('handleFinalize', newOrder, e.detail)
dispatch('reorder', newOrder)
}
$effect(() => {
schema && dragDisabled && untrack(() => updateItems())
schema && dragDisabledState && untrack(() => updateItems())
})
</script>
@@ -118,6 +119,7 @@
{disablePortal}
{disabled}
{helperScript}
{className}
bind:schema
dndConfig={disableDnd
? undefined
@@ -125,7 +127,7 @@
items,
flipDurationMs,
dropTargetStyle: {},
type: dndType ?? 'top-level'
type: dndType
}}
{items}
{diff}
@@ -9,5 +9,4 @@ export type EditableSchemaWrapperProps = {
customUi?: {
noAddPopover?: boolean
}
onSchemaChange?: ({ schema }: { schema: Schema }) => void
}
@@ -5,6 +5,7 @@
import type { Placement } from '@floating-ui/core'
import { isCloudHosted } from '$lib/cloud'
import { CloudOff } from 'lucide-svelte'
import type { Item } from '$lib/utils'
interface Props {
setDropdownWidthToButtonWidth?: boolean
@@ -25,5 +25,12 @@
</script>
<!-- <ScriptWrapper {script} neverShowMeta={true} {customUi} /> -->
<EditableSchemaSdkWrapper {customUi} {schema} />
{JSON.stringify(schema)}
<EditableSchemaSdkWrapper
{customUi}
{schema}
onSchemaChange={(newSchema) => {
console.log('onSchemaChange', newSchema)
schema = newSchema
}}
/>