mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 08:02:26 +00:00
feat (frontend): improve capture UI (#5051)
* feat (frontend): improve capture UI * show schema diff * add accept reject form * add diff for first node input * move update schema button * change modified properties display * propagate change through nested components * allow modification of nested object in schema * reject nested changes * clean logic * clean code * clean * nit * fix first strep input * fix argument preview * clean * restore indentation for nested objects * ignore undefined field in diff computation * fix run button disabled update * nit * fix update JSON * fix deletion of nested components * dark mode * clean * replace dropdown with sidebar * only check type for compatibility * add shadow * disbale dnd on edit * auto-scroll within schema * fix nested not dnd object diff viewer * fix captures drawer * open edit tab on add new arg * change button label when schema is the same * ajust padding * fix arg update * fix oneof display * propagate change event through nested schema * handle oneOf * fix preview arg sync * handle s3 object * fix arg sync * update schema input compatibility * clean compatible * accept empty array items * clean * increase of schema args gap * fix nested schema update * allow number and int compatibility * reset args when modifying schema * open fields on add with addPropertyV2 --------- Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,6 @@
|
||||
import PropertyEditor from './schema/PropertyEditor.svelte'
|
||||
import SimpleEditor from './SimpleEditor.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import Label from './Label.svelte'
|
||||
@@ -23,6 +22,7 @@
|
||||
import SchemaFormDnd from './schema/SchemaFormDND.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { tweened } from 'svelte/motion'
|
||||
import type { SchemaDiff } from '$lib/components/schema/schemaUtils'
|
||||
|
||||
export let schema: Schema | any
|
||||
export let schemaSkippedValues: string[] = []
|
||||
@@ -49,6 +49,9 @@
|
||||
export let previewSchema: Record<string, any> | undefined = undefined
|
||||
export let editPanelInitialSize: number | undefined = undefined
|
||||
export let editPanelSize = 0
|
||||
export let diff: Record<string, SchemaDiff> = {}
|
||||
export let disableDnd: boolean = false
|
||||
export let shouldDispatchChanges: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -232,6 +235,11 @@
|
||||
|
||||
let pannelButtonWidth: number = 0
|
||||
export let pannelExtraButtonWidth: number = 0
|
||||
|
||||
export function updateJson() {
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
editor?.setCode(schemaString)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full h-full">
|
||||
@@ -247,37 +255,60 @@
|
||||
<Splitpanes class="splitter-hidden w-full">
|
||||
{#if !noPreview}
|
||||
<Pane bind:size={inputPanelSize} minSize={20}>
|
||||
<div class="flex flex-col pr-2 gap-2">
|
||||
<div
|
||||
class="h-full flex flex-col gap-2 {$$slots.openEditTab && editPanelSize > 0
|
||||
? 'pr-[38px]'
|
||||
: 'pr-2'}"
|
||||
>
|
||||
{#if $$slots.addProperty}
|
||||
<div class="w-full justify-left pr-2">
|
||||
<div style={`width: calc(100% - ${pannelButtonWidth - pannelExtraButtonWidth}px);`}>
|
||||
<div class="w-full justify-left pr-2 grow-0">
|
||||
<div
|
||||
style={editPanelSize > 0
|
||||
? `width: 100%;`
|
||||
: `width: calc(100% - ${pannelButtonWidth - pannelExtraButtonWidth}px);`}
|
||||
>
|
||||
<slot name="addProperty" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<SchemaFormDnd
|
||||
schema={previewSchema ? previewSchema : schema}
|
||||
{dndType}
|
||||
bind:args
|
||||
on:click={(e) => {
|
||||
opened = e.detail
|
||||
}}
|
||||
on:reorder={(e) => {
|
||||
schema.order = e.detail
|
||||
schema = schema
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
on:change={() => {
|
||||
schema = schema
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
{lightweightMode}
|
||||
prettifyHeader={isAppInput}
|
||||
disabled={!!previewSchema}
|
||||
/>
|
||||
<div
|
||||
class="min-h-0 overflow-y-auto grow rounded-md {$$slots.runButton
|
||||
? 'flex flex-col gap-2'
|
||||
: ''}"
|
||||
>
|
||||
<SchemaFormDnd
|
||||
nestedClasses={'flex flex-col gap-1'}
|
||||
schema={previewSchema ? previewSchema : schema}
|
||||
{dndType}
|
||||
{disableDnd}
|
||||
bind:args
|
||||
on:click={(e) => {
|
||||
opened = e.detail
|
||||
}}
|
||||
on:reorder={(e) => {
|
||||
schema.order = e.detail
|
||||
schema = schema
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
on:change={() => {
|
||||
schema = schema
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
{lightweightMode}
|
||||
prettifyHeader={isAppInput}
|
||||
disabled={!!previewSchema}
|
||||
{diff}
|
||||
on:acceptChange
|
||||
on:rejectChange
|
||||
on:nestedChange={() => {
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
{shouldDispatchChanges}
|
||||
/>
|
||||
|
||||
<slot name="runButton" />
|
||||
<slot name="runButton" />
|
||||
</div>
|
||||
</div>
|
||||
</Pane>
|
||||
{/if}
|
||||
@@ -425,7 +456,6 @@
|
||||
{isAppInput}
|
||||
on:change={() => {
|
||||
schema = schema
|
||||
// console.log('schema', schema)
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
>
|
||||
@@ -440,8 +470,6 @@
|
||||
const isS3 = e.detail == 'S3'
|
||||
const isOneOf = e.detail == 'oneOf'
|
||||
|
||||
selected = e.detail
|
||||
|
||||
const emptyProperty = {
|
||||
contentEncoding: undefined,
|
||||
enum_: undefined,
|
||||
@@ -508,8 +536,12 @@
|
||||
type: e.detail
|
||||
}
|
||||
}
|
||||
schema = schema
|
||||
dispatch('change', schema)
|
||||
// No better solution than this, needs future rework
|
||||
setTimeout(() => {
|
||||
schema = schema
|
||||
dispatch('change', schema)
|
||||
}, 100)
|
||||
dispatch('schemaChange')
|
||||
}}
|
||||
>
|
||||
{#each [['String', 'string'], ['Number', 'number'], ['Integer', 'integer'], ['Object', 'object'], ['OneOf', 'oneOf'], ['Array', 'array'], ['Boolean', 'boolean'], ['S3 Object', 'S3']] as x}
|
||||
@@ -558,6 +590,7 @@
|
||||
on:schemaChange={(e) => {
|
||||
schema = schema
|
||||
dispatch('change', schema)
|
||||
dispatch('schemaChange')
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
error = e
|
||||
}
|
||||
}
|
||||
loadSchema()
|
||||
$: $flowStore && $flowStateStore && loadSchema()
|
||||
|
||||
function handleClick() {
|
||||
selected = !selected
|
||||
@@ -51,6 +51,8 @@
|
||||
if (event.key === 'Escape' && selected) {
|
||||
selected = false
|
||||
dispatch('select', undefined)
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
let jobProgressReset: () => void
|
||||
|
||||
export function test() {
|
||||
renderCount++
|
||||
runPreview($previewArgs, undefined)
|
||||
}
|
||||
|
||||
@@ -159,53 +160,53 @@
|
||||
on:close={captureLibraryDrawer?.toggleDrawer}
|
||||
>
|
||||
<div class="h-full flex flex-col gap-2">
|
||||
<CapturesInputs
|
||||
flowPath={$pathStore}
|
||||
headless={false}
|
||||
addButton={true}
|
||||
on:select={(e) => {
|
||||
selectedCapture = e.detail
|
||||
}}
|
||||
on:openTriggers={(e) => {
|
||||
dispatch('openTriggers', e.detail)
|
||||
captureLibraryDrawer?.closeDrawer()
|
||||
}}
|
||||
/>
|
||||
<div class="min-h-0 grow h-full">
|
||||
<CapturesInputs
|
||||
flowPath={$pathStore}
|
||||
headless={false}
|
||||
addButton={true}
|
||||
on:select={(e) => {
|
||||
selectedCapture = e.detail
|
||||
}}
|
||||
on:openTriggers={(e) => {
|
||||
dispatch('openTriggers', e.detail)
|
||||
captureLibraryDrawer?.closeDrawer()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="h-full overflow-hidden min-h-0 flex flex-col justify-between">
|
||||
<div class="w-full flex flex-col min-h-0 gap-2 px-2 py-2 grow">
|
||||
<div class="w-full flex flex-col">
|
||||
<Button
|
||||
color="blue"
|
||||
btnClasses="w-full"
|
||||
size="sm"
|
||||
spacingSize="xl"
|
||||
on:click={async () => {
|
||||
$previewArgs = JSON.parse(JSON.stringify(selectedCapture))
|
||||
captureLibraryDrawer?.closeDrawer()
|
||||
renderCount++
|
||||
}}
|
||||
disabled={!selectedCapture}
|
||||
<div class="w-full flex flex-col gap-2 px-2 py-2 h-[50%]">
|
||||
<div class="w-full flex flex-col">
|
||||
<Button
|
||||
color="blue"
|
||||
btnClasses="w-full"
|
||||
size="sm"
|
||||
spacingSize="xl"
|
||||
on:click={async () => {
|
||||
$previewArgs = JSON.parse(JSON.stringify(selectedCapture))
|
||||
captureLibraryDrawer?.closeDrawer()
|
||||
renderCount++
|
||||
}}
|
||||
disabled={!selectedCapture}
|
||||
>
|
||||
<ArrowLeftIcon class="w-4 h-4 mr-2" />
|
||||
Use input
|
||||
</Button>
|
||||
</div>
|
||||
<div class="w-full min-h-0 grow overflow-auto">
|
||||
{#if typeof selectedCapture == 'string' && selectedCapture == 'WINDMILL_TOO_BIG'}
|
||||
<div class="text-secondary mt-2">
|
||||
Payload too big to preview but can still be loaded</div
|
||||
>
|
||||
<ArrowLeftIcon class="w-4 h-4 mr-2" />
|
||||
Use input
|
||||
</Button>
|
||||
</div>
|
||||
<div class="w-full min-h-0 grow overflow-auto">
|
||||
{#if typeof selectedCapture == 'string' && selectedCapture == 'WINDMILL_TOO_BIG'}
|
||||
<div class="text-secondary mt-2">
|
||||
Payload too big to preview but can still be loaded</div
|
||||
>
|
||||
{:else if Object.keys(selectedCapture || {}).length > 0}
|
||||
<div class=" overflow-auto h-full p-2">
|
||||
<ObjectViewer json={selectedCapture} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center text-tertiary">
|
||||
Select an Input to preview scripts arguments
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if Object.keys(selectedCapture || {}).length > 0}
|
||||
<div class=" overflow-auto h-full p-2">
|
||||
<ObjectViewer json={selectedCapture} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-center text-tertiary">
|
||||
Select an Input to preview scripts arguments
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -102,6 +102,8 @@
|
||||
if (event.key === 'Escape' && selected) {
|
||||
selected = undefined
|
||||
dispatch('select', undefined)
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -119,16 +119,22 @@
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && isEditing) {
|
||||
setEditing(null)
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
} else if (event.key === 'Escape' && selectedInput) {
|
||||
selectedInput = null
|
||||
selectedArgs = undefined
|
||||
dispatch('select', undefined)
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' && isEditing) {
|
||||
updateInput(isEditing).then(() => {
|
||||
setEditing(null)
|
||||
})
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import LightweightArgInput from './LightweightArgInput.svelte'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { dragHandleZone, type Options as DndOptions } from '@windmill-labs/svelte-dnd-action'
|
||||
import type { SchemaDiff } from '$lib/components/schema/schemaUtils'
|
||||
|
||||
export let schema: Schema | any
|
||||
export let schemaSkippedValues: string[] = []
|
||||
@@ -46,6 +47,10 @@
|
||||
| { type: 'hash'; hash: string }
|
||||
| undefined = undefined
|
||||
export let lightHeader = false
|
||||
export let diff: Record<string, SchemaDiff> = {}
|
||||
export let nestedParent: { label: string; nestedParent: any | undefined } | undefined = undefined
|
||||
export let shouldDispatchChanges = false
|
||||
export let nestedClasses = ''
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -166,7 +171,9 @@
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="w-full {$$props.class} {flexWrap ? 'flex flex-row flex-wrap gap-x-6 ' : ''}"
|
||||
class="w-full {$$props.class} {flexWrap
|
||||
? 'flex flex-row flex-wrap gap-x-6 '
|
||||
: ''} {nestedClasses}"
|
||||
use:dragHandleZone={dndConfig ?? { items: [], dragDisabled: true }}
|
||||
on:finalize
|
||||
on:consider
|
||||
@@ -174,12 +181,61 @@
|
||||
{#if keys.length > 0}
|
||||
{#each fields as item, i (item.id)}
|
||||
{@const argName = item.value}
|
||||
<div>
|
||||
<div
|
||||
class={typeof diff[argName] === 'object' && diff[argName].diff !== 'same'
|
||||
? 'bg-red-300 dark:bg-red-800 rounded-md'
|
||||
: ''}
|
||||
>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
{#if !schemaSkippedValues.includes(argName) && keys.includes(argName)}
|
||||
{#if typeof diff[argName] === 'object' && diff[argName].oldSchema}
|
||||
{@const formerProperty = diff[argName].oldSchema}
|
||||
<ArgInput
|
||||
{disablePortal}
|
||||
{resourceTypes}
|
||||
{prettifyHeader}
|
||||
autofocus={i == 0 && autofocus ? true : null}
|
||||
label={argName}
|
||||
description={formerProperty?.description}
|
||||
value={args[argName]}
|
||||
type={formerProperty?.type}
|
||||
oneOf={formerProperty?.oneOf}
|
||||
required={formerProperty?.required}
|
||||
pattern={formerProperty?.pattern}
|
||||
valid={inputCheck[argName]}
|
||||
defaultValue={structuredClone(formerProperty?.default)}
|
||||
enum_={formerProperty?.enum}
|
||||
format={formerProperty?.format}
|
||||
contentEncoding={formerProperty?.contentEncoding}
|
||||
customErrorMessage={formerProperty?.customErrorMessage}
|
||||
properties={formerProperty?.properties}
|
||||
order={formerProperty?.order}
|
||||
nestedRequired={formerProperty?.required}
|
||||
itemsType={formerProperty?.items}
|
||||
disabled={disabledArgs.includes(argName) || disabled || formerProperty?.disabled}
|
||||
{compact}
|
||||
{variableEditor}
|
||||
{itemPicker}
|
||||
{pickForField}
|
||||
password={linkedSecret == argName}
|
||||
extra={formerProperty}
|
||||
{showSchemaExplorer}
|
||||
simpleTooltip={schemaFieldTooltip[argName]}
|
||||
{onlyMaskPassword}
|
||||
nullable={formerProperty?.nullable}
|
||||
title={formerProperty?.title}
|
||||
placeholder={formerProperty?.placeholder}
|
||||
orderEditable={dndConfig != undefined}
|
||||
otherArgs={args}
|
||||
{helperScript}
|
||||
{lightHeader}
|
||||
hideNested={typeof diff[argName].diff === 'object'}
|
||||
diffStatus={undefined}
|
||||
/>
|
||||
{/if}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="flex flex-row items-center bg-surface"
|
||||
class="flex flex-row items-center"
|
||||
on:click={() => {
|
||||
dispatch('click', argName)
|
||||
}}
|
||||
@@ -244,7 +300,14 @@
|
||||
</LightweightArgInput>
|
||||
{:else}
|
||||
<ArgInput
|
||||
on:change={() => dispatch('change')}
|
||||
on:change={() => {
|
||||
dispatch('change')
|
||||
}}
|
||||
on:nestedChange={() => {
|
||||
dispatch('nestedChange')
|
||||
}}
|
||||
on:acceptChange={(e) => dispatch('acceptChange', e.detail)}
|
||||
on:rejectChange={(e) => dispatch('rejectChange', e.detail)}
|
||||
{disablePortal}
|
||||
{resourceTypes}
|
||||
{prettifyHeader}
|
||||
@@ -285,6 +348,10 @@
|
||||
otherArgs={args}
|
||||
{helperScript}
|
||||
{lightHeader}
|
||||
diffStatus={diff[argName] ?? undefined}
|
||||
{nestedParent}
|
||||
{shouldDispatchChanges}
|
||||
{nestedClasses}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
<slot name="actions" />
|
||||
|
||||
@@ -116,7 +116,10 @@
|
||||
|
||||
let placeholderVisible = false
|
||||
function updatePlaceholderVisibility(value: string) {
|
||||
if (!value) return
|
||||
if (!value) {
|
||||
placeholderVisible = true
|
||||
return
|
||||
}
|
||||
placeholderVisible = value.trim() === ''
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/common'
|
||||
import { ButtonType } from '$lib/components/common/button/model'
|
||||
import { getContext } from 'svelte'
|
||||
import { getContext, tick } from 'svelte'
|
||||
import FlowCard from '../common/FlowCard.svelte'
|
||||
import type { FlowEditorContext } from '../types'
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
@@ -18,21 +18,30 @@
|
||||
CornerDownLeft,
|
||||
Pen,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
Plus,
|
||||
History,
|
||||
Braces,
|
||||
Code,
|
||||
Save,
|
||||
X
|
||||
X,
|
||||
Check
|
||||
} from 'lucide-svelte'
|
||||
import CaptureIcon from '$lib/components/triggers/CaptureIcon.svelte'
|
||||
import FlowPreviewContent from '$lib/components/FlowPreviewContent.svelte'
|
||||
import FlowInputEditor from './FlowInputEditor.svelte'
|
||||
import CapturesInputs from '$lib/components/CapturesInputs.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import ButtonDropDown from '$lib/components/meltComponents/ButtonDropDown.svelte'
|
||||
import CaptureButton from '$lib/components/triggers/CaptureButton.svelte'
|
||||
import {
|
||||
type SchemaDiff,
|
||||
getFullPath,
|
||||
setNestedProperty,
|
||||
getNestedProperty,
|
||||
schemaFromDiff,
|
||||
computeDiff,
|
||||
applyDiff
|
||||
} from '$lib/components/schema/schemaUtils'
|
||||
import SideBarTab from '$lib/components/meltComponents/SideBarTab.svelte'
|
||||
|
||||
export let noEditor: boolean
|
||||
export let disabled: boolean
|
||||
@@ -45,14 +54,18 @@
|
||||
let previewSchema: Record<string, any> | undefined = undefined
|
||||
let payloadData: Record<string, any> | undefined = undefined
|
||||
let previewArguments: Record<string, any> | undefined = $previewArgs
|
||||
let editOptionsOpen = false
|
||||
let dropdownItems: Array<{
|
||||
label: string
|
||||
onClick: () => void
|
||||
disabled?: boolean
|
||||
}> = []
|
||||
|
||||
let diff: Record<string, SchemaDiff> = {}
|
||||
let editPanelSize = $flowInputEditorState?.editPanelSize ?? 0
|
||||
let selectedSchema: Record<string, any> | undefined = undefined
|
||||
let runDisabled: boolean = false
|
||||
let editableSchemaForm: EditableSchemaForm | undefined = undefined
|
||||
let savedPreviewArgs: Record<string, any> | undefined = undefined
|
||||
|
||||
function updateEditPanelSize(size: number | undefined) {
|
||||
if (!$flowInputEditorState) return
|
||||
if (!size || size === 0) {
|
||||
@@ -73,7 +86,8 @@
|
||||
disabled:
|
||||
!$flowInputEditorState?.selectedTab ||
|
||||
$flowInputEditorState?.selectedTab === 'inputEditor',
|
||||
icon: Pen
|
||||
icon: Pen,
|
||||
selected: $flowInputEditorState?.selectedTab === 'inputEditor'
|
||||
},
|
||||
{
|
||||
label: 'Trigger captures',
|
||||
@@ -81,7 +95,8 @@
|
||||
handleEditSchema('captures')
|
||||
},
|
||||
disabled: $flowInputEditorState?.selectedTab === 'captures',
|
||||
icon: CaptureIcon
|
||||
icon: CaptureIcon,
|
||||
selected: $flowInputEditorState?.selectedTab === 'captures'
|
||||
},
|
||||
{
|
||||
label: 'History',
|
||||
@@ -89,7 +104,8 @@
|
||||
handleEditSchema('history')
|
||||
},
|
||||
disabled: $flowInputEditorState?.selectedTab === 'history',
|
||||
icon: History
|
||||
icon: History,
|
||||
selected: $flowInputEditorState?.selectedTab === 'history'
|
||||
},
|
||||
{
|
||||
label: 'Json payload',
|
||||
@@ -97,14 +113,16 @@
|
||||
handleEditSchema('json')
|
||||
},
|
||||
disabled: $flowInputEditorState?.selectedTab === 'json',
|
||||
icon: Braces
|
||||
icon: Braces,
|
||||
selected: $flowInputEditorState?.selectedTab === 'json'
|
||||
},
|
||||
{
|
||||
label: "First step's inputs",
|
||||
onClick: () => {
|
||||
handleEditSchema('firstStepInputs')
|
||||
},
|
||||
icon: Code
|
||||
icon: Code,
|
||||
selected: $flowInputEditorState?.selectedTab === 'firstStepInputs'
|
||||
},
|
||||
{
|
||||
label: 'Saved inputs',
|
||||
@@ -112,9 +130,10 @@
|
||||
handleEditSchema('savedInputs')
|
||||
},
|
||||
disabled: $flowInputEditorState?.selectedTab === 'savedInputs',
|
||||
icon: Save
|
||||
icon: Save,
|
||||
selected: $flowInputEditorState?.selectedTab === 'savedInputs'
|
||||
}
|
||||
].filter((item) => !item.disabled)
|
||||
]
|
||||
}
|
||||
|
||||
function handleEditSchema(editTab?: any) {
|
||||
@@ -130,7 +149,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
function schemaFromPayload(payload: any) {
|
||||
function schemaFromPayload(payloadData: any) {
|
||||
const payload = structuredClone(payloadData)
|
||||
const parsed = JSON.parse(JSON.stringify(payload))
|
||||
|
||||
if (!parsed) {
|
||||
@@ -157,43 +177,59 @@
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
|
||||
runPreview()
|
||||
} else if (event.key === 'Enter' && previewSchema && !preventEnter) {
|
||||
applySchemaAndArgs()
|
||||
connectFirstNode()
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
function runPreview() {
|
||||
if (previewArguments) {
|
||||
$previewArgs = previewArguments
|
||||
$previewArgs = structuredClone(previewArguments)
|
||||
}
|
||||
previewOpen = true
|
||||
flowPreviewContent?.test()
|
||||
}
|
||||
|
||||
function updatePreviewSchemaAndArgs(payload: any) {
|
||||
payloadData = payload
|
||||
if (!payload) {
|
||||
payloadData = undefined
|
||||
selectedSchema = undefined
|
||||
updatePreviewArguments(undefined)
|
||||
updatePreviewSchema(undefined)
|
||||
return
|
||||
}
|
||||
payloadData = structuredClone(payload)
|
||||
selectedSchema = schemaFromPayload(payloadData)
|
||||
updatePreviewSchema(selectedSchema)
|
||||
updatePreviewArguments(payloadData)
|
||||
}
|
||||
|
||||
async function updatePreviewSchema(newSchema: Record<string, any> | undefined) {
|
||||
if (!newSchema) {
|
||||
previewSchema = undefined
|
||||
if (runDisabled) {
|
||||
await tick()
|
||||
runDisabled = false
|
||||
}
|
||||
diff = {}
|
||||
return
|
||||
}
|
||||
previewSchema = schemaFromPayload(payload)
|
||||
updatePreviewArguments(payload)
|
||||
diff = {}
|
||||
const diffSchema = computeDiff(newSchema, $flowStore.schema)
|
||||
diff = diffSchema
|
||||
previewSchema = schemaFromDiff(diffSchema, $flowStore.schema)
|
||||
runDisabled = true
|
||||
}
|
||||
|
||||
function applySchemaAndArgs() {
|
||||
if (!previewSchema) {
|
||||
return
|
||||
}
|
||||
$flowStore.schema = previewSchema
|
||||
async function applySchemaAndArgs() {
|
||||
$flowStore.schema = applyDiff($flowStore.schema, diff)
|
||||
if (previewArguments) {
|
||||
$previewArgs = previewArguments
|
||||
savedPreviewArgs = structuredClone(previewArguments)
|
||||
}
|
||||
if ($flowInputEditorState) {
|
||||
$flowInputEditorState.selectedTab = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function applySchema() {
|
||||
$flowStore.schema = previewSchema
|
||||
updatePreviewSchemaAndArgs(undefined)
|
||||
if ($flowInputEditorState) {
|
||||
$flowInputEditorState.selectedTab = undefined
|
||||
}
|
||||
@@ -201,41 +237,28 @@
|
||||
|
||||
function updatePreviewArguments(payloadData: Record<string, any> | undefined) {
|
||||
if (!payloadData) {
|
||||
previewArguments = $previewArgs
|
||||
previewArguments = savedPreviewArgs
|
||||
return
|
||||
}
|
||||
previewArguments = payloadData
|
||||
savedPreviewArgs = structuredClone(previewArguments)
|
||||
previewArguments = structuredClone(payloadData)
|
||||
}
|
||||
|
||||
let jsonValid = false
|
||||
function updatePayloadFromJson(jsonInput: string) {
|
||||
if (jsonInput === undefined || jsonInput === null || jsonInput.trim() === '') {
|
||||
updatePreviewSchemaAndArgs(undefined)
|
||||
jsonValid = false
|
||||
return
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(jsonInput)
|
||||
updatePreviewSchemaAndArgs(parsed)
|
||||
jsonValid = true
|
||||
} catch (error) {
|
||||
updatePreviewSchemaAndArgs(undefined)
|
||||
jsonValid = false
|
||||
}
|
||||
}
|
||||
|
||||
let tabButtonWidth = 0
|
||||
|
||||
const TAB_TITLES: Record<string, string> = {
|
||||
inputEditor: 'Input editor',
|
||||
captures: 'Captures',
|
||||
history: 'History',
|
||||
savedInputs: 'Saved inputs',
|
||||
json: 'JSON',
|
||||
firstStepInputs: 'First step',
|
||||
undefined: ''
|
||||
}
|
||||
|
||||
let connectFirstNode: () => void = () => {}
|
||||
|
||||
let init = false
|
||||
@@ -249,6 +272,54 @@
|
||||
$: $flowInputEditorState && ((dropdownItems = getDropdownItems()), initPayloadData())
|
||||
|
||||
let preventEnter = false
|
||||
|
||||
async function acceptChange(arg: { label: string; nestedParent: any | undefined }) {
|
||||
handleChange(arg, $flowStore.schema, diff, (newSchema) => {
|
||||
$flowStore.schema = newSchema
|
||||
})
|
||||
}
|
||||
|
||||
async function rejectChange(arg: { label: string; nestedParent: any | undefined }) {
|
||||
const revertDiff = computeDiff($flowStore.schema, selectedSchema)
|
||||
handleChange(arg, selectedSchema, revertDiff, (newSchema) => {
|
||||
selectedSchema = newSchema
|
||||
})
|
||||
}
|
||||
|
||||
function handleChange(
|
||||
arg: { label: string; nestedParent: any | undefined },
|
||||
currentSchema: Record<string, any> | undefined,
|
||||
diffSchema: Record<string, SchemaDiff> | undefined,
|
||||
updateCurrentSchema: (newSchema: Record<string, any> | undefined) => void
|
||||
) {
|
||||
if (!diff || !currentSchema) {
|
||||
return
|
||||
}
|
||||
|
||||
const path = getFullPath(arg)
|
||||
const parentPath = path.slice(0, -1)
|
||||
const diffStatus = getNestedProperty({ diff: diffSchema }, path, 'diff')
|
||||
const schema = getNestedProperty(currentSchema, parentPath, 'properties')
|
||||
const localDiff = {
|
||||
[arg.label]: diffStatus
|
||||
}
|
||||
const schemaUpdated = applyDiff(schema, localDiff)
|
||||
if (parentPath.length > 0) {
|
||||
setNestedProperty(currentSchema, parentPath, schemaUpdated)
|
||||
} else {
|
||||
updateCurrentSchema(schemaUpdated)
|
||||
}
|
||||
|
||||
diff = computeDiff(selectedSchema, $flowStore.schema)
|
||||
previewSchema = schemaFromDiff(diff, $flowStore.schema)
|
||||
}
|
||||
|
||||
function resetArgs() {
|
||||
if (!previewSchema) {
|
||||
previewArguments = undefined
|
||||
savedPreviewArgs = undefined
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add svelte:window to listen for keyboard events -->
|
||||
@@ -269,6 +340,7 @@
|
||||
{#if !disabled}
|
||||
<div class="py-2 px-4 h-full">
|
||||
<EditableSchemaForm
|
||||
bind:this={editableSchemaForm}
|
||||
bind:schema={$flowStore.schema}
|
||||
isFlowInput
|
||||
on:edit={(e) => {
|
||||
@@ -284,78 +356,84 @@
|
||||
bind:editPanelSize
|
||||
editPanelInitialSize={$flowInputEditorState?.editPanelSize}
|
||||
pannelExtraButtonWidth={$flowInputEditorState?.editPanelSize ? tabButtonWidth : 0}
|
||||
{diff}
|
||||
disableDnd={!!previewSchema}
|
||||
on:rejectChange={(e) => {
|
||||
rejectChange(e.detail).then(() => {
|
||||
updatePreviewSchema(selectedSchema)
|
||||
})
|
||||
}}
|
||||
on:acceptChange={(e) => {
|
||||
acceptChange(e.detail).then(() => {
|
||||
updatePreviewSchema(selectedSchema)
|
||||
})
|
||||
}}
|
||||
shouldDispatchChanges={true}
|
||||
on:change={() => {
|
||||
previewArguments = previewArguments
|
||||
if (!previewSchema) {
|
||||
savedPreviewArgs = structuredClone(previewArguments)
|
||||
}
|
||||
}}
|
||||
on:schemaChange={() => {
|
||||
resetArgs()
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="openEditTab">
|
||||
<div
|
||||
class={twMerge(
|
||||
'flex flex-row divide-x rounded-md bg-surface overflow-hidden',
|
||||
!!$flowInputEditorState?.selectedTab ? 'rounded-r-none' : '',
|
||||
ButtonType.ColorVariants.blue.divider
|
||||
)}
|
||||
>
|
||||
<button
|
||||
on:click={() => {
|
||||
handleEditSchema()
|
||||
}}
|
||||
title={!!$flowInputEditorState?.selectedTab
|
||||
? 'Close input editor'
|
||||
: 'Open input editor'}
|
||||
class={ButtonType.ColorVariants.blue.contained}
|
||||
>
|
||||
<div class="p-2 center-center">
|
||||
<svelte:component
|
||||
this={!!$flowInputEditorState?.selectedTab ? ChevronRight : Pen}
|
||||
size={14}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<ButtonDropDown
|
||||
{dropdownItems}
|
||||
closeOnClick={true}
|
||||
bind:open={editOptionsOpen}
|
||||
placement="bottom-end"
|
||||
>
|
||||
<div
|
||||
class={twMerge(
|
||||
'p-2 center-center hover:bg-surface-hover',
|
||||
ButtonType.ColorVariants.blue.contained,
|
||||
'flex flex-row items-center rounded-br-md',
|
||||
'transition-all duration-150 ease-in-out overflow-hidden whitespace-nowrap',
|
||||
!!$flowInputEditorState?.selectedTab ? 'w-[122px] px-3' : 'w-[30px]'
|
||||
)}
|
||||
bind:clientWidth={tabButtonWidth}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-1 justify-between w-full">
|
||||
{#if !!$flowInputEditorState?.selectedTab}
|
||||
<h2 class="text-xs">{TAB_TITLES[$flowInputEditorState?.selectedTab]}</h2>
|
||||
{/if}
|
||||
<ChevronDown size={14} />
|
||||
</div>
|
||||
</div>
|
||||
</ButtonDropDown>
|
||||
<div class={twMerge('flex flex-row divide-x', ButtonType.ColorVariants.blue.divider)}>
|
||||
<SideBarTab {dropdownItems} fullMenu={!!$flowInputEditorState?.selectedTab}>
|
||||
<svelte:fragment slot="close button">
|
||||
<button
|
||||
on:click={() => {
|
||||
handleEditSchema()
|
||||
}}
|
||||
title={!!$flowInputEditorState?.selectedTab
|
||||
? 'Close input editor'
|
||||
: 'Open input editor'}
|
||||
class={twMerge(
|
||||
ButtonType.ColorVariants.blue.contained,
|
||||
!!$flowInputEditorState?.selectedTab
|
||||
? 'rounded-tl-md border-l border-t'
|
||||
: 'rounded-md border'
|
||||
)}
|
||||
>
|
||||
<div class="p-2 center-center">
|
||||
<svelte:component
|
||||
this={!!$flowInputEditorState?.selectedTab ? ChevronRight : Pen}
|
||||
size={14}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
</SideBarTab>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="addProperty">
|
||||
{#if !!previewSchema}
|
||||
<div
|
||||
class={twMerge(
|
||||
'bg-blue-50 border-blue-200 border dark:bg-blue-900/40 dark:border-blue-700/40 text-xs p-2 w-full flex flex-row gap-2 items-center justify-left rounded-md',
|
||||
'text-blue-700 dark:text-blue-100',
|
||||
'relative'
|
||||
)}
|
||||
>
|
||||
<span> Preview only, update schema to save.</span>
|
||||
<div class="flex flex-row items-center gap-2 absolute right-2">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="light"
|
||||
size="xs2"
|
||||
startIcon={{ icon: X }}
|
||||
shortCut={{ key: 'esc', withoutModifier: true }}
|
||||
nonCaptureEvent
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-row items-center gap-2 right-2 justify-end">
|
||||
<Button
|
||||
size="xs"
|
||||
color="green"
|
||||
disabled={!previewSchema}
|
||||
shortCut={{ Icon: CornerDownLeft, hide: false, withoutModifier: true }}
|
||||
startIcon={{ icon: Check }}
|
||||
on:click={() => {
|
||||
applySchemaAndArgs()
|
||||
connectFirstNode()
|
||||
}}
|
||||
>
|
||||
{Object.values(diff).every((el) => el.diff === 'same')
|
||||
? 'Apply args'
|
||||
: 'Update schema'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
startIcon={{ icon: X }}
|
||||
shortCut={{ key: 'esc', withoutModifier: true }}
|
||||
nonCaptureEvent
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<AddPropertyV2
|
||||
@@ -363,6 +441,14 @@
|
||||
bind:this={addProperty}
|
||||
on:change={() => {
|
||||
$flowStore = $flowStore
|
||||
if (editableSchemaForm) {
|
||||
editableSchemaForm.updateJson()
|
||||
}
|
||||
}}
|
||||
on:addNew={(e) => {
|
||||
handleEditSchema('inputEditor')
|
||||
editableSchemaForm?.openField(e.detail)
|
||||
$flowStore = $flowStore
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="trigger">
|
||||
@@ -378,9 +464,7 @@
|
||||
<svelte:fragment slot="extraTab">
|
||||
{#if $flowInputEditorState?.selectedTab === 'history'}
|
||||
<FlowInputEditor
|
||||
disabled={!payloadData}
|
||||
on:applySchemaAndArgs={applySchemaAndArgs}
|
||||
on:applySchema={applySchema}
|
||||
title="History"
|
||||
on:destroy={() => {
|
||||
updatePreviewSchemaAndArgs(undefined)
|
||||
}}
|
||||
@@ -396,12 +480,10 @@
|
||||
</FlowInputEditor>
|
||||
{:else if $flowInputEditorState?.selectedTab === 'captures'}
|
||||
<FlowInputEditor
|
||||
disabled={!payloadData}
|
||||
on:applySchemaAndArgs={applySchemaAndArgs}
|
||||
on:applySchema={applySchema}
|
||||
on:destroy={() => {
|
||||
updatePreviewSchemaAndArgs(undefined)
|
||||
}}
|
||||
title="Trigger captures"
|
||||
>
|
||||
<svelete:fragment slot="action">
|
||||
<div class="center-center">
|
||||
@@ -417,13 +499,10 @@
|
||||
</FlowInputEditor>
|
||||
{:else if $flowInputEditorState?.selectedTab === 'savedInputs'}
|
||||
<FlowInputEditor
|
||||
{preventEnter}
|
||||
disabled={!payloadData}
|
||||
on:applySchemaAndArgs={applySchemaAndArgs}
|
||||
on:applySchema={applySchema}
|
||||
on:destroy={() => {
|
||||
updatePreviewSchemaAndArgs(undefined)
|
||||
}}
|
||||
title="Saved inputs"
|
||||
>
|
||||
<SavedInputsPicker
|
||||
flowPath={initialPath}
|
||||
@@ -438,13 +517,10 @@
|
||||
</FlowInputEditor>
|
||||
{:else if $flowInputEditorState?.selectedTab === 'json'}
|
||||
<FlowInputEditor
|
||||
{preventEnter}
|
||||
disabled={!jsonValid}
|
||||
on:applySchemaAndArgs={applySchemaAndArgs}
|
||||
on:applySchema={applySchema}
|
||||
on:destroy={() => {
|
||||
updatePreviewSchemaAndArgs(undefined)
|
||||
}}
|
||||
title="Json payload"
|
||||
>
|
||||
<SimpleEditor
|
||||
on:focus={() => {
|
||||
@@ -470,22 +546,25 @@
|
||||
</FlowInputEditor>
|
||||
{:else if $flowInputEditorState?.selectedTab === 'firstStepInputs'}
|
||||
<FlowInputEditor
|
||||
disabled={!previewSchema}
|
||||
on:applySchemaAndArgs={() => {
|
||||
applySchemaAndArgs()
|
||||
connectFirstNode()
|
||||
}}
|
||||
on:applySchema={applySchema}
|
||||
on:destroy={() => {
|
||||
updatePreviewSchemaAndArgs(undefined)
|
||||
connectFirstNode = () => {}
|
||||
}}
|
||||
title="First step's inputs"
|
||||
>
|
||||
<FirstStepInputs
|
||||
on:connectFirstNode={({ detail }) => {
|
||||
connectFirstNode = detail.connectFirstNode
|
||||
}}
|
||||
on:select={(e) => {
|
||||
previewSchema = e.detail ?? undefined
|
||||
if (e.detail) {
|
||||
const diffSchema = computeDiff(e.detail, $flowStore.schema)
|
||||
diff = diffSchema
|
||||
previewSchema = schemaFromDiff(diffSchema, $flowStore.schema)
|
||||
runDisabled = true
|
||||
} else {
|
||||
updatePreviewSchemaAndArgs(undefined)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FlowInputEditor>
|
||||
@@ -496,7 +575,7 @@
|
||||
<Button
|
||||
color="dark"
|
||||
btnClasses="w-fit"
|
||||
disabled={!!previewSchema}
|
||||
disabled={runDisabled}
|
||||
size="xs"
|
||||
shortCut={{ Icon: CornerDownLeft, hide: false }}
|
||||
on:click={() => {
|
||||
|
||||
@@ -1,51 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onDestroy } from 'svelte'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { CornerDownLeft, Save } from 'lucide-svelte'
|
||||
|
||||
export let name: string = ''
|
||||
export let disabled = false
|
||||
export let preventEnter = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
function applySchemaAndArgs() {
|
||||
dispatch('applySchemaAndArgs')
|
||||
}
|
||||
export let title: string = ''
|
||||
|
||||
onDestroy(() => {
|
||||
dispatch('destroy')
|
||||
})
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter' && !preventEnter) {
|
||||
applySchemaAndArgs()
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div class="flex flex-col h-full px-3 pb-3">
|
||||
<div
|
||||
class="items-center grow-0 flex flex-row justify-between gap-2 data-schema-picker min-h-[40px]"
|
||||
>
|
||||
<h2 class="font-semibold text-secondary text-sm flex flex-row items-center gap-1 leading-6">
|
||||
{title}
|
||||
</h2>
|
||||
<slot name="action" />
|
||||
</div>
|
||||
|
||||
<div class="h-full p-2">
|
||||
<Section label={name} class="h-full" small={true}>
|
||||
<svelte:fragment slot="header">
|
||||
<slot name="header" />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="action">
|
||||
<div class="flex flex-row gap-2 data-schema-picker">
|
||||
<slot name="action" />
|
||||
<Button
|
||||
size="xs2"
|
||||
color="dark"
|
||||
{disabled}
|
||||
shortCut={{ Icon: CornerDownLeft, hide: false, withoutModifier: true }}
|
||||
startIcon={{ icon: Save }}
|
||||
on:click={applySchemaAndArgs}>Update schema</Button
|
||||
>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<div class="w-full min-h-0 grow">
|
||||
<slot />
|
||||
</Section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
export let dropdownItems: Array<{
|
||||
label: string
|
||||
onClick: () => void
|
||||
icon?: any
|
||||
selected?: boolean
|
||||
}> = []
|
||||
|
||||
export let fullMenu: boolean = false
|
||||
|
||||
let open = false
|
||||
let timeout: NodeJS.Timeout | null = null
|
||||
|
||||
function handleMouseEnter() {
|
||||
if (!fullMenu) return
|
||||
timeout = setTimeout(() => {
|
||||
open = true
|
||||
}, 500)
|
||||
}
|
||||
|
||||
function handleMouseLeave() {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout)
|
||||
timeout = null
|
||||
}
|
||||
open = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
class="flex flex-col relative !overflow-visible"
|
||||
on:mouseenter={handleMouseEnter}
|
||||
on:mouseleave={handleMouseLeave}
|
||||
>
|
||||
<slot name="close button" />
|
||||
|
||||
{#if fullMenu}
|
||||
<div
|
||||
class="absolute flex-col top-[30px] left-0 z-50 bg-surface border-l border-b {open
|
||||
? 'rounded-md rounded-tl-none overflow-hidden shadow-md'
|
||||
: 'rounded-bl-md'}"
|
||||
>
|
||||
{#each dropdownItems as item}
|
||||
<button
|
||||
class="hover:bg-surface-hover p-2 transition-colors duration-150 w-full {item.selected
|
||||
? 'bg-surface-selected'
|
||||
: 'text-secondary'}"
|
||||
on:click={() => {
|
||||
item.onClick()
|
||||
open = false
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2 min-h-[20px]">
|
||||
{#if item.icon}
|
||||
<svelte:component this={item.icon} size={14} />
|
||||
{/if}
|
||||
|
||||
{#if open}
|
||||
<p class="text-xs text-secondary whitespace-nowrap text-left">
|
||||
{item.label}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -138,7 +138,9 @@
|
||||
if (Object.keys(modifiedProperties).includes(argName)) {
|
||||
delete modifiedProperties[argName]
|
||||
|
||||
modifiedObject.required = schema.required.filter((arg) => arg !== argName)
|
||||
if (modifiedObject.required) {
|
||||
modifiedObject.required = schema.required.filter((arg) => arg !== argName)
|
||||
}
|
||||
if (modifiedObject.order) {
|
||||
modifiedObject.order = modifiedObject.order.filter((arg) => arg !== argName)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
} from '$lib/common'
|
||||
import { emptySchema, sendUserToast } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import SimpleEditor from '../SimpleEditor.svelte'
|
||||
import AddPropertyFormV2 from './AddPropertyFormV2.svelte'
|
||||
export let schema: Schema | any = emptySchema()
|
||||
|
||||
@@ -24,13 +23,10 @@
|
||||
schema = emptySchema()
|
||||
}
|
||||
|
||||
let schemaString: string = ''
|
||||
|
||||
// Internal state: bound to args builder modal
|
||||
let argError = ''
|
||||
let editing = false
|
||||
let oldArgName: string | undefined // when editing argument and changing name
|
||||
let jsonEditor: SimpleEditor
|
||||
|
||||
reorder()
|
||||
|
||||
@@ -59,7 +55,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddOrEditArgument(modalProperty: ModalSchemaProperty): void {
|
||||
export function handleAddOrEditArgument(modalProperty: ModalSchemaProperty): void {
|
||||
// If editing the arg's name, oldName containing the old argument name must be provided
|
||||
argError = ''
|
||||
modalProperty.name = modalProperty.name.trim()
|
||||
@@ -109,8 +105,6 @@
|
||||
}
|
||||
|
||||
schema = schema
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
jsonEditor?.setCode(schemaString)
|
||||
|
||||
if (argError !== '') {
|
||||
sendUserToast(argError, true)
|
||||
@@ -137,12 +131,13 @@
|
||||
if (Object.keys(modifiedProperties).includes(argName)) {
|
||||
delete modifiedProperties[argName]
|
||||
|
||||
modifiedObject.required = schema.required.filter((arg) => arg !== argName)
|
||||
if (modifiedObject.required) {
|
||||
modifiedObject.required = schema.required.filter((arg) => arg !== argName)
|
||||
}
|
||||
if (modifiedObject.order) {
|
||||
modifiedObject.order = modifiedObject.order.filter((arg) => arg !== argName)
|
||||
}
|
||||
schema = schema
|
||||
schemaString = JSON.stringify(schema, null, '\t')
|
||||
dispatch('change', schema)
|
||||
} else {
|
||||
throw Error('Argument not found!')
|
||||
@@ -163,6 +158,7 @@
|
||||
selectedType: 'string',
|
||||
name: e.detail.name
|
||||
})
|
||||
dispatch('addNew', e.detail.name)
|
||||
} catch (err) {
|
||||
sendUserToast(`Could not add argument: ${err}`, true)
|
||||
}
|
||||
|
||||
@@ -154,8 +154,11 @@
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<Label label="Nested properties">
|
||||
<svelte:self
|
||||
on:change={() => (schema = schema)}
|
||||
schema={schema.properties[item.value]}
|
||||
on:change={() => {
|
||||
schema = schema
|
||||
dispatch('change', schema)
|
||||
}}
|
||||
bind:schema={schema.properties[item.value]}
|
||||
parentId={item.value}
|
||||
/>
|
||||
</Label>
|
||||
@@ -187,7 +190,13 @@
|
||||
editTab="inputEditor"
|
||||
>
|
||||
<svelte:fragment slot="addProperty">
|
||||
<AddPropertyV2 bind:schema on:change>
|
||||
<AddPropertyV2
|
||||
bind:schema
|
||||
on:change
|
||||
on:addNew={(e) => {
|
||||
editableSchemaForm?.openField(e.detail)
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="trigger">
|
||||
<div
|
||||
class="w-full py-2 flex justify-center items-center border border-dashed rounded-md hover:bg-surface-hover"
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
let resourceIsTextFile: boolean = false
|
||||
let addProperty: AddPropertyV2 | undefined = undefined
|
||||
let editableSchemaForm: EditableSchemaForm | undefined = undefined
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -90,6 +91,9 @@
|
||||
bind:schema
|
||||
bind:this={addProperty}
|
||||
on:change={() => dispatch('change', schema)}
|
||||
on:addNew={(e) => {
|
||||
editableSchemaForm?.openField(e.detail)
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="trigger">
|
||||
<div
|
||||
@@ -101,6 +105,7 @@
|
||||
</AddPropertyV2>
|
||||
{/if}
|
||||
<EditableSchemaForm
|
||||
bind:this={editableSchemaForm}
|
||||
bind:schema
|
||||
on:change={() => dispatch('change', schema)}
|
||||
isFlowInput
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { GripVertical } from 'lucide-svelte'
|
||||
import type { Schema } from '$lib/common'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
|
||||
import type { SchemaDiff } from '$lib/components/schema/schemaUtils'
|
||||
export let dndType: string | undefined = undefined
|
||||
export let schema: Schema
|
||||
export let args: Record<string, any> = {}
|
||||
@@ -15,6 +15,11 @@
|
||||
export let disablePortal: boolean = false
|
||||
export let disabled: boolean = false
|
||||
export let schemaSkippedValues: string[] = []
|
||||
export let nestedParent: { label: string; nestedParent: any | undefined } | undefined = undefined
|
||||
export let disableDnd: boolean = false
|
||||
export let shouldDispatchChanges: boolean = false
|
||||
export let diff: Record<string, SchemaDiff> = {}
|
||||
export let nestedClasses = ''
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const flipDurationMs = 200
|
||||
@@ -66,12 +71,16 @@
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<SchemaForm
|
||||
{nestedClasses}
|
||||
{schemaSkippedValues}
|
||||
on:click
|
||||
on:change
|
||||
on:reorder
|
||||
on:consider={handleConsider}
|
||||
on:finalize={handleFinalize}
|
||||
on:acceptChange
|
||||
on:rejectChange
|
||||
on:nestedChange
|
||||
{lightweightMode}
|
||||
bind:args
|
||||
{prettifyHeader}
|
||||
@@ -79,17 +88,24 @@
|
||||
{disablePortal}
|
||||
{disabled}
|
||||
bind:schema
|
||||
dndConfig={{
|
||||
items,
|
||||
flipDurationMs,
|
||||
dropTargetStyle: {},
|
||||
type: dndType ?? 'top-level'
|
||||
}}
|
||||
dndConfig={disableDnd
|
||||
? undefined
|
||||
: {
|
||||
items,
|
||||
flipDurationMs,
|
||||
dropTargetStyle: {},
|
||||
type: dndType ?? 'top-level'
|
||||
}}
|
||||
{items}
|
||||
{diff}
|
||||
{nestedParent}
|
||||
{shouldDispatchChanges}
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
<div class="w-4 h-8 cursor-move ml-2 handle" aria-label="drag-handle" use:dragHandle>
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
{#if !disableDnd}
|
||||
<div class="w-4 h-8 cursor-move ml-2 handle" aria-label="drag-handle" use:dragHandle>
|
||||
<GripVertical size={16} />
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</SchemaForm>
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
export type SchemaDiff = {
|
||||
diff: 'same' | 'added' | 'removed' | 'modified' | Record<string, SchemaDiff>
|
||||
fullSchema: { [key: string]: any } | undefined
|
||||
oldSchema?: { [key: string]: any } | undefined
|
||||
}
|
||||
|
||||
function isCompatible(diff: Record<string, SchemaDiff>) {
|
||||
let compatible = true
|
||||
Object.values(diff).forEach((diff) => {
|
||||
if (diff.diff === 'added' || diff.diff === 'modified' || diff.diff === 'removed') {
|
||||
compatible = false
|
||||
} else if (isRecordSchemaDiff(diff.diff)) {
|
||||
compatible = isCompatible(diff.diff)
|
||||
}
|
||||
})
|
||||
return compatible
|
||||
}
|
||||
|
||||
function isCompatibleObject(a: any, b: any): boolean {
|
||||
if (!a || !b) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (a.type === 'null' || b.type === 'null') {
|
||||
return true
|
||||
}
|
||||
|
||||
if (a.type !== b.type) {
|
||||
if (
|
||||
(a.type === 'number' || a.type === 'integer') &&
|
||||
(b.type === 'number' || b.type === 'integer')
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
switch (a.type) {
|
||||
case 'object':
|
||||
if (a.oneOf || b.oneOf) {
|
||||
//TODO: handle oneOf compatibility. here we assume that only b is oneOf
|
||||
return true
|
||||
}
|
||||
return a.format === b.format
|
||||
|
||||
case 'array':
|
||||
return !a.items || !b.items || a.items?.type === b.items?.type
|
||||
|
||||
case 'string':
|
||||
return a.format === b.format
|
||||
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
case 'integer':
|
||||
return true
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function computeDiff(
|
||||
previewSchema: { [key: string]: any } | undefined,
|
||||
currentSchema: { [key: string]: any } | undefined
|
||||
) {
|
||||
if (!previewSchema || !currentSchema) {
|
||||
return {}
|
||||
}
|
||||
const diff: Record<string, SchemaDiff> = {}
|
||||
|
||||
if (previewSchema?.properties) {
|
||||
Object.keys(previewSchema.properties).forEach((key) => {
|
||||
if (!currentSchema?.properties?.[key]) {
|
||||
diff[key] = {
|
||||
diff: 'added',
|
||||
fullSchema: previewSchema.properties[key]
|
||||
}
|
||||
} else {
|
||||
const previewProp = previewSchema.properties[key]
|
||||
const currentProp = currentSchema.properties[key]
|
||||
if (previewProp.type === 'object' && currentProp.type === 'object') {
|
||||
if (JSON.stringify(previewProp) === JSON.stringify(currentProp)) {
|
||||
diff[key] = { diff: 'same', fullSchema: undefined }
|
||||
} else if (previewProp.oneOf || currentProp.oneOf) {
|
||||
if (isCompatibleObject(previewProp, currentProp)) {
|
||||
diff[key] = { diff: 'same', fullSchema: undefined }
|
||||
} else {
|
||||
diff[key] = { diff: 'modified', fullSchema: previewProp, oldSchema: currentProp }
|
||||
}
|
||||
} else if (previewProp.format || currentProp.format) {
|
||||
//TODO: handle s3 object compatibility
|
||||
diff[key] = { diff: 'modified', fullSchema: previewProp, oldSchema: currentProp }
|
||||
} else {
|
||||
const diffProp = computeDiff(previewProp, currentProp)
|
||||
const checkIfSame = isCompatible(diffProp)
|
||||
if (checkIfSame) {
|
||||
diff[key] = { diff: 'same', fullSchema: undefined }
|
||||
} else {
|
||||
diff[key] = {
|
||||
diff: diffProp,
|
||||
fullSchema: previewProp,
|
||||
oldSchema: currentProp
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (isCompatibleObject(previewProp, currentProp)) {
|
||||
diff[key] = { diff: 'same', fullSchema: undefined }
|
||||
} else {
|
||||
diff[key] = { diff: 'modified', fullSchema: previewProp, oldSchema: currentProp }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (currentSchema?.properties) {
|
||||
Object.keys(currentSchema.properties).forEach((key) => {
|
||||
if (!previewSchema?.properties?.[key]) {
|
||||
diff[key] = { diff: 'removed', fullSchema: undefined }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return diff
|
||||
}
|
||||
|
||||
export function schemaFromDiff(
|
||||
diff: Record<string, SchemaDiff>,
|
||||
schema: { [key: string]: any } | undefined
|
||||
) {
|
||||
if (!schema) {
|
||||
return undefined
|
||||
}
|
||||
const newSchema = structuredClone(schema)
|
||||
Object.keys(diff).forEach((key) => {
|
||||
const diffValue = diff[key].diff
|
||||
if (diffValue === 'added' || diffValue === 'modified') {
|
||||
newSchema.properties[key] = diff[key].fullSchema
|
||||
if (newSchema.order && !newSchema.order.includes(key)) {
|
||||
newSchema.order.push(key)
|
||||
}
|
||||
} else if (isRecordSchemaDiff(diffValue)) {
|
||||
// Handle nested diffs
|
||||
newSchema.properties[key] = schemaFromDiff(diffValue, schema.properties[key])
|
||||
}
|
||||
})
|
||||
return newSchema
|
||||
}
|
||||
|
||||
export function getFullPath(arg: { label: string; nestedParent: any | undefined }): string[] {
|
||||
const getPath = (current: { label: string; nestedParent: any | undefined }): string[] => {
|
||||
if (!current.nestedParent) {
|
||||
return [current.label]
|
||||
}
|
||||
return [...getPath(current.nestedParent), current.label]
|
||||
}
|
||||
|
||||
return getPath(arg)
|
||||
}
|
||||
|
||||
export function getNestedProperty(obj: any, path: string[], field: string = 'properties') {
|
||||
return path.reduce((curr, key) => curr?.[field]?.[key], obj)
|
||||
}
|
||||
|
||||
export function setNestedProperty(
|
||||
obj: any,
|
||||
path: string[],
|
||||
value: any,
|
||||
field: string = 'properties'
|
||||
) {
|
||||
const pathCopy = [...path]
|
||||
const lastKey = pathCopy.pop()
|
||||
const target = pathCopy.reduce((curr, key) => {
|
||||
if (!(key in curr[field])) {
|
||||
curr[field][key] = { [field]: {} }
|
||||
}
|
||||
return curr[field][key]
|
||||
}, obj)
|
||||
if (lastKey && value) {
|
||||
const newValue = structuredClone(value)
|
||||
target[field][lastKey] = newValue
|
||||
return
|
||||
} else if (lastKey && !value) {
|
||||
delete target[field][lastKey]
|
||||
}
|
||||
}
|
||||
|
||||
function isRecordSchemaDiff(value: SchemaDiff['diff']): value is Record<string, SchemaDiff> {
|
||||
return typeof value === 'object' && value !== null
|
||||
}
|
||||
|
||||
export function applyDiff(
|
||||
schema: Record<string, any> | undefined,
|
||||
diff: Record<string, SchemaDiff> | undefined
|
||||
) {
|
||||
if (!diff || !schema) {
|
||||
return
|
||||
}
|
||||
|
||||
let newSchema = structuredClone(schema)
|
||||
|
||||
Object.keys(diff).forEach((key) => {
|
||||
const diffValue = diff[key].diff
|
||||
if (diffValue === 'removed') {
|
||||
delete newSchema.properties[key]
|
||||
if (newSchema.order) {
|
||||
newSchema.order = newSchema.order.filter((x) => x !== key)
|
||||
}
|
||||
} else if (diffValue === 'added' || diffValue === 'modified' || isRecordSchemaDiff(diffValue)) {
|
||||
newSchema.properties[key] = diff[key].fullSchema
|
||||
if (newSchema.order && !newSchema.order.includes(key)) {
|
||||
newSchema.order.push(key)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return newSchema
|
||||
}
|
||||
@@ -133,6 +133,8 @@
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape' && selected) {
|
||||
deselect()
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,7 +151,7 @@
|
||||
|
||||
<svelte:window on:keydown={handleKeydown} />
|
||||
|
||||
<Label label="Trigger captures" {headless} class="h-full flex flex-col gap-1">
|
||||
<Label label="Trigger captures" {headless} class="h-full {headless ? '' : 'flex flex-col gap-1'}">
|
||||
<svelte:fragment slot="header">
|
||||
{#if addButton}
|
||||
<div class="inline-block">
|
||||
@@ -180,7 +182,13 @@
|
||||
</svelte:fragment>
|
||||
|
||||
<div
|
||||
class={fullHeight ? 'h-full' : capturesLength > 7 ? 'h-[300px]' : 'h-fit'}
|
||||
class={fullHeight
|
||||
? headless
|
||||
? 'h-full'
|
||||
: 'min-h-0 grow'
|
||||
: capturesLength > 7
|
||||
? 'h-[300px]'
|
||||
: 'h-fit'}
|
||||
use:clickOutside={{ capture: false, exclude: getPropPickerElements }}
|
||||
on:click_outside={() => {
|
||||
if (firstClick) {
|
||||
|
||||
Reference in New Issue
Block a user