mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-07 16:03:21 +00:00
* fix(frontend): scope raw-app/flow/script editors to the session workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): scope flow and script editor operations to the session workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): scope flow preview, inline-script creation and datatable schema to the session workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review — thread session workspace through flow resource pickers, script fetch, preview cancel/recording and path collision check Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Claude review — pass session workspace to preview FlowStatusViewer and align FlowChatManager guards Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Pi review — show acting workspace in script-not-found message and fetch picked script from it in EditorBar Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review round 2 — thread session workspace into flow step test, raw-app inline runnable, inline editor toolbars and MCP OAuth path Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review round 3 — thread session workspace into dynamic-input helpers and the flow-preview argument side panel (history/saved-inputs/captures) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review round 4 — thread session workspace into nested flow/script drawers, flow chat inputs and the flow input side tabs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review round 5 — thread session workspace into script-module fork/reload and key the raw-app schema cache by workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review round 6 — key the DB manager schema cache by acting workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): address Codex review round 7 — thread session workspace into resource-valued arg pickers and the editor variable/resource helper drawers Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(frontend): scope the flow asset explorer's ResourceEditorDrawer to the acting workspace Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: thread acting workspace through flow asset explore controls Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: thread acting workspace through SQL REPL, secret args, helper forms, S3 inputs, saved inputs Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
229 lines
6.6 KiB
Svelte
229 lines
6.6 KiB
Svelte
<script lang="ts" module>
|
|
function validSelectObject(x): string | undefined {
|
|
if (typeof x != 'object') {
|
|
return JSON.stringify(x) + ' is not an object'
|
|
}
|
|
let keys = Object.keys(x)
|
|
if (!keys.includes('value') || !keys.includes('label')) {
|
|
return JSON.stringify(x) + ' does not contain value or label field'
|
|
}
|
|
if (typeof x['label'] != 'string') {
|
|
return JSON.stringify(x) + ' label is not a string'
|
|
}
|
|
return
|
|
}
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
import { usePromise } from '$lib/svelte5Utils.svelte'
|
|
import JobLoader, { type Callbacks } from './JobLoader.svelte'
|
|
import Select from './select/Select.svelte'
|
|
import MultiSelect from './select/MultiSelect.svelte'
|
|
import { safeSelectItems } from './select/utils.svelte'
|
|
import Tooltip from './Tooltip.svelte'
|
|
import { Loader2 } from 'lucide-svelte'
|
|
import { type DynamicInput } from '$lib/utils'
|
|
import { deepEqual } from 'fast-equals'
|
|
import { untrack } from 'svelte'
|
|
import { getHelperEntrypointArgs } from '$lib/infer'
|
|
|
|
interface Props {
|
|
value?: any
|
|
helperScript?: DynamicInput.HelperScript
|
|
format: string
|
|
otherArgs?: Record<string, any>
|
|
name: string
|
|
/** Workspace the helper script runs in; defaults to the nav workspace. */
|
|
workspace?: string
|
|
}
|
|
|
|
let {
|
|
value = $bindable(),
|
|
helperScript,
|
|
format,
|
|
otherArgs: otherArgs,
|
|
workspace = undefined
|
|
}: Props = $props()
|
|
|
|
let [inputType, entrypoint] = $derived(format.includes('-') ? format.split('-', 2) : [format, ''])
|
|
|
|
let isMultiple = $derived(inputType === 'dynmultiselect')
|
|
let isSelect = $derived(inputType === 'dynselect' || inputType === 'dynmultiselect')
|
|
|
|
$effect.pre(() => {
|
|
if (isMultiple && value === undefined) {
|
|
value = []
|
|
}
|
|
})
|
|
|
|
let resultJobLoader: JobLoader | undefined = $state()
|
|
// loadInit:false — the $effect below owns the first refresh once
|
|
// resultJobLoader is bound; without this the promise is kicked off twice.
|
|
let _items = usePromise(getItemsFromOptions, { clearValueOnRefresh: false, loadInit: false })
|
|
let items = $derived(_items.value)
|
|
|
|
let filterText: string = $state('')
|
|
let open: boolean = $state(false)
|
|
|
|
async function getItemsFromOptions() {
|
|
return new Promise<{ label: string; value: any }[]>((resolve, reject) => {
|
|
let cb: Callbacks = {
|
|
doneResult({ result }) {
|
|
if (!result || !Array.isArray(result)) {
|
|
if (result?.error?.message && result?.error?.name) {
|
|
reject(
|
|
`Error in ${inputType} function execution: ` +
|
|
result?.error?.name +
|
|
' - ' +
|
|
result?.error?.message
|
|
)
|
|
} else {
|
|
reject('Result was not an array but ' + JSON.stringify(result, null, 2))
|
|
}
|
|
return
|
|
}
|
|
if (result.length == 0) resolve([])
|
|
|
|
if (result.every((x) => typeof x == 'string')) {
|
|
result = result.map((x) => ({ label: x, value: x }))
|
|
} else if (result.find((x) => validSelectObject(x) != undefined)) {
|
|
reject(validSelectObject(result.find((x) => validSelectObject(x) != undefined)))
|
|
return
|
|
}
|
|
resolve(result)
|
|
},
|
|
cancel: () => reject(),
|
|
doneError({ id, error }) {
|
|
reject(error)
|
|
}
|
|
}
|
|
resultJobLoader?.runDynamicInputScript(
|
|
entrypoint,
|
|
helperScript!,
|
|
{ ...otherArgs, filterText, _ENTRYPOINT_OVERRIDE: entrypoint },
|
|
cb
|
|
)
|
|
})
|
|
}
|
|
|
|
let neverLoaded = $state(true)
|
|
|
|
$effect(() => {
|
|
if (_items.value && value !== undefined && isSelect) {
|
|
if (isMultiple && Array.isArray(value) && Array.isArray(_items.value)) {
|
|
const availableValues = new Set(_items.value.map((x) => x.value))
|
|
const filteredValue = value.filter((v) => availableValues.has(v))
|
|
if (filteredValue.length !== value.length) {
|
|
value = filteredValue
|
|
}
|
|
} else if (!isMultiple && value !== undefined) {
|
|
if (!_items.value.find((x) => x.value == value)) {
|
|
value = undefined
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
let lastArgs = $state.snapshot(untrack(() => otherArgs))
|
|
|
|
let timeout: number | undefined = $state()
|
|
let nargs = $state($state.snapshot(untrack(() => otherArgs)))
|
|
$effect(() => {
|
|
otherArgs
|
|
untrack(() => clearTimeout(timeout))
|
|
timeout = setTimeout(() => {
|
|
nargs = $state.snapshot(otherArgs)
|
|
}, 1000)
|
|
})
|
|
|
|
// Parameter names declared by the helper function. When known, we restrict
|
|
// the change-detection to only those keys so typing in unrelated form fields
|
|
// no longer retriggers the dynselect job. `undefined` means we couldn't
|
|
// determine the signature → fall back to a full-args comparison.
|
|
let helperParams = $state<Set<string> | undefined>(undefined)
|
|
|
|
$effect(() => {
|
|
const script = helperScript
|
|
const ep = entrypoint
|
|
if (!script) {
|
|
helperParams = undefined
|
|
return
|
|
}
|
|
let cancelled = false
|
|
void getHelperEntrypointArgs(script, ep || undefined).then((params) => {
|
|
if (!cancelled) helperParams = params
|
|
})
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
})
|
|
|
|
function filterArgs(args: Record<string, any> | undefined) {
|
|
if (!args || !helperParams) return args
|
|
const filtered: Record<string, any> = {}
|
|
for (const k of helperParams) {
|
|
if (k in args) filtered[k] = args[k]
|
|
}
|
|
return filtered
|
|
}
|
|
|
|
$effect(() => {
|
|
;[filterText, entrypoint, helperScript]
|
|
if (
|
|
resultJobLoader &&
|
|
(open || neverLoaded || !deepEqual(filterArgs(lastArgs), filterArgs(nargs)))
|
|
) {
|
|
neverLoaded = false
|
|
lastArgs = $state.snapshot(otherArgs)
|
|
_items.refresh()
|
|
}
|
|
})
|
|
</script>
|
|
|
|
{#if helperScript}
|
|
<JobLoader onlyResult workspaceOverride={workspace} bind:this={resultJobLoader} />
|
|
|
|
<div class="w-full flex-col flex">
|
|
{#if inputType === 'dynmultiselect'}
|
|
<MultiSelect
|
|
bind:value
|
|
items={safeSelectItems(items || [])}
|
|
placeholder="Select items"
|
|
noItemsMsg={_items.status === 'loading' ? 'Loading...' : 'No items found'}
|
|
disabled={_items.status === 'loading'}
|
|
/>
|
|
{:else if inputType === 'dynselect'}
|
|
<Select
|
|
bind:value
|
|
bind:open
|
|
{items}
|
|
bind:filterText
|
|
loading={!open && _items.status === 'loading'}
|
|
clearable
|
|
noItemsMsg={_items.status === 'loading' ? 'Loading...' : 'No items found'}
|
|
/>
|
|
{:else}
|
|
<!-- Future dynamic input types can be added here -->
|
|
<div class="text-red-400 text-sm">
|
|
Unsupported dynamic input type: {inputType}
|
|
</div>
|
|
{/if}
|
|
{#if _items.error}
|
|
<div class="text-red-400 text-2xs">
|
|
error: <Tooltip>{_items.error}</Tooltip>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{:else}
|
|
<div class="flex flex-col gap-1 w-full">
|
|
<div class="text-xs text-primary"
|
|
>Dynamic input ({inputType}) is not available in this mode, write value directly</div
|
|
>
|
|
{#await import('$lib/components/JsonEditor.svelte')}
|
|
<Loader2 class="animate-spin" />
|
|
{:then Module}
|
|
<Module.default code={JSON.stringify(value, null, 2)} bind:value />
|
|
{/await}
|
|
</div>
|
|
{/if}
|