fix(frontend): scope raw-app, flow and script editors to the session workspace (#10015)

* 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>
This commit is contained in:
Guilhem
2026-07-09 01:53:43 +02:00
committed by GitHub
parent 9ad6927231
commit c000bbca28
74 changed files with 791 additions and 388 deletions
+10 -1
View File
@@ -1070,7 +1070,14 @@
{/if}
</div>
{:else if inputCat == 'dynamic'}
<DynamicInput name={label} {otherArgs} {helperScript} bind:value format={format ?? ''} />
<DynamicInput
name={label}
{otherArgs}
{helperScript}
{workspace}
bind:value
format={format ?? ''}
/>
{:else if inputCat == 'resource-object' && resourceTypes == undefined}
<span class="text-2xs text-primary">Loading resource types...</span>
{:else if inputCat == 'resource-object' && (resourceTypes == undefined || (format && format?.split('-').length > 1 && resourceTypes.includes(format?.substring('resource-'.length))))}
@@ -1078,6 +1085,7 @@
<ObjectResourceInput
datatableAsPgResource={label === 'database'}
{disabled}
{workspace}
{defaultValue}
selectFirst={!noDefaultOnSelectFirst && required}
{disablePortal}
@@ -1423,6 +1431,7 @@
<ResourcePicker
selectFirst={noDefaultOnSelectFirst}
{disablePortal}
{workspace}
bind:value
initialValue={defaultValue}
resourceType={format && format.split('-').length > 1
@@ -39,6 +39,10 @@
/** Tables that are already added and should show as disabled */
disabledTables?: SelectedTable[]
onImport?: (mode: 'schema_and_data' | 'schema_only') => void
/** Workspace the datatable/schema lookups run against. Defaults to the
* navigation `$workspaceStore`; pass the acting workspace when embedded in
* a session preview whose workspace differs from the top nav. */
workspace?: string
}
let {
@@ -51,10 +55,13 @@
multiSelectMode = false,
selectedTables = $bindable([]),
disabledTables = [],
onImport
onImport,
workspace = undefined
}: Props = $props()
let dbSchema: DBSchema | undefined = $derived(input && $dbSchemas[getDbSchemasPath(input)])
let ws = $derived(workspace ?? $workspaceStore)
let dbSchema: DBSchema | undefined = $derived(input && $dbSchemas[schemaCacheKey(input)])
const outOfOrderModal = createAsyncConfirmationModal()
@@ -67,28 +74,36 @@
}
}
// Scope the shared `dbSchemas` cache by the acting workspace: a datatable of
// the same name can exist in both the nav and the acting workspace, so the
// bare resource path alone would let one workspace's schema be reused for the
// other while DB operations target the acting one.
function schemaCacheKey(input: DbInput): string {
return `${ws}:${getDbSchemasPath(input)}`
}
let colDefs = resource(
() => [input],
() => [input, ws],
async () => {
if (!input) return
return await loadAllTablesMetaData($workspaceStore, input)
return await loadAllTablesMetaData(ws, input)
}
)
let dbSchemasPromise = resource(
() => [input],
() => [input, ws],
async () => {
if (!input) return
const dbSchemasPath = getDbSchemasPath(input)
const dbSchemasPath = schemaCacheKey(input)
if (input.type == 'database') {
$dbSchemas[dbSchemasPath] = await getDbSchemas(
input.resourceType,
input.resourcePath,
$workspaceStore,
ws,
(message: string) => sendUserToast(message, true)
)
} else if (input.type == 'ducklake') {
$dbSchemas[dbSchemasPath] = await getDucklakeSchema({
workspace: $workspaceStore!,
workspace: ws!,
ducklake: input.ducklake
})
}
@@ -130,7 +145,7 @@
}}
/>
{#if dbSchema && $workspaceStore && input}
{#if dbSchema && ws && input}
{@const _input = input}
{@const dbType = getDbType(_input)}
<Splitpanes horizontal>
@@ -165,11 +180,11 @@
colDefs,
tableKey,
input: _input,
workspace: $workspaceStore
workspace: ws
})}
dbSchemaOps={dbSchemaOpsWithPreviewScripts({
input: _input,
workspace: $workspaceStore,
workspace: ws,
confirmRunOutOfOrder: (pending) =>
outOfOrderModal.ask({
title: 'Run migration out of order',
@@ -201,6 +216,7 @@
<Pane bind:size={replPanelSize} minSize={REPL_MIN_SIZE} class="relative">
<SqlRepl
{input}
{workspace}
onData={(data) => {
replResultData = data
}}
@@ -35,13 +35,15 @@
let open = $derived(uriState.open)
// The workspace the drawer's DB operations run against — the acting workspace of
// the editor that opened it (set via openDrawer), else the nav workspace.
let ws = $derived(uriState.workspace ?? $workspaceStore)
// Load available datatables when drawer opens with datatable input
const datatables = resource<string[]>([], async () => {
if (!$workspaceStore) return []
if (!ws) return []
try {
return (await WorkspaceService.listDataTables({ workspace: $workspaceStore })).map(
(d) => d.name
)
return (await WorkspaceService.listDataTables({ workspace: ws })).map((d) => d.name)
} catch (e) {
console.error('Failed to load datatables:', e)
return []
@@ -113,10 +115,10 @@
async function handleExportSchema() {
const source = currentSourceIdentifier()
if (!source || !$workspaceStore) return
if (!source || !ws) return
try {
exportResult = await WorkspaceService.exportPgSchema({
workspace: $workspaceStore,
workspace: ws,
requestBody: { source }
})
exportDrawerOpen = true
@@ -126,13 +128,13 @@
}
async function handleImportDatabase() {
if (!importSource || !$workspaceStore) return
if (!importSource || !ws) return
const target = currentSourceIdentifier()
if (!target) return
importLoading = true
try {
await WorkspaceService.importPgDatabase({
workspace: $workspaceStore,
workspace: ws,
requestBody: {
source: toSourceIdentifier(importSource),
target,
@@ -173,11 +175,12 @@
noPadding
id="db-manager-drawer"
>
{#if uriState.effectiveInput && $workspaceStore}
{#if uriState.effectiveInput && ws}
{#key uriState.selectedDatatable}
<DBManagerContent
bind:this={dbManagerContent}
input={uriState.effectiveInput}
workspace={uriState.workspace}
bind:hasReplResult
bind:selectedSchemaKey={uriState.selectedSchema}
bind:selectedTableKey={uriState.selectedTable}
@@ -207,9 +210,9 @@
{/key}
{/if}
{#snippet actions()}
{#if uriState.isDatatableInput && uriState.selectedDatatable && $workspaceStore}
{#if uriState.isDatatableInput && uriState.selectedDatatable && ws}
<DataTableMigrationsButton
workspace={$workspaceStore}
workspace={ws}
datatable={uriState.selectedDatatable}
onSchemaChanged={refreshManager}
/>
@@ -274,7 +277,12 @@
</Alert>
<div class="flex flex-col gap-2">
<span class="text-sm font-medium">Source database</span>
<ResourcePicker datatableAsPgResource bind:value={importSource} resourceType="postgresql" />
<ResourcePicker
datatableAsPgResource
bind:value={importSource}
resourceType="postgresql"
workspace={ws}
/>
</div>
<div class="flex flex-col gap-2">
<span class="text-sm font-medium">Import mode</span>
@@ -33,9 +33,17 @@
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 }: Props = $props()
let {
value = $bindable(),
helperScript,
format,
otherArgs: otherArgs,
workspace = undefined
}: Props = $props()
let [inputType, entrypoint] = $derived(format.includes('-') ? format.split('-', 2) : [format, ''])
@@ -173,7 +181,7 @@
</script>
{#if helperScript}
<JobLoader onlyResult bind:this={resultJobLoader} />
<JobLoader onlyResult workspaceOverride={workspace} bind:this={resultJobLoader} />
<div class="w-full flex-col flex">
{#if inputType === 'dynmultiselect'}
+25 -18
View File
@@ -117,6 +117,10 @@
right?: import('svelte').Snippet
openAiChat?: boolean
moduleId?: string
// Workspace to scope variable/resource/data-table lookups to. Defaults to
// the nav `$workspaceStore`; an AI-session live editor passes the session's
// acting workspace (a fork) so the helper pickers hit the right workspace.
workspace?: string
}
let {
@@ -141,9 +145,12 @@
showHistoryDrawer = $bindable(false),
right,
openAiChat = false,
moduleId = undefined
moduleId = undefined,
workspace = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let contextualVariablePicker: ItemPicker | undefined = $state()
let variablePicker: ItemPicker | undefined = $state()
let resourcePicker: ItemPicker | undefined = $state()
@@ -350,12 +357,12 @@
})
async function loadVariables() {
return await VariableService.listVariable({ workspace: $workspaceStore ?? '' })
return await VariableService.listVariable({ workspace: ws ?? '' })
}
async function loadContextualVariables() {
return await VariableService.listContextualVariables({
workspace: $workspaceStore ?? 'NO_W'
workspace: ws ?? 'NO_W'
})
}
@@ -366,7 +373,7 @@
async function onScriptPick(e: { detail: { path: string } }) {
codeObj = undefined
codeViewer?.openDrawer?.()
codeObj = await getScriptByPath(e.detail.path ?? '')
codeObj = await getScriptByPath(e.detail.path ?? '', ws)
}
const dispatch = createEventDispatcher()
@@ -423,7 +430,7 @@
async function resourceTypePickCallback(name: string) {
if (!editor) return
const resourceType = await ResourceService.getResourceType({
workspace: $workspaceStore ?? 'NO_W',
workspace: ws ?? 'NO_W',
path: name
})
@@ -785,8 +792,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
buttons={{ 'Edit/View': (x) => resourceEditor?.initEdit(x) }}
extraField="description"
extraField2="resource_type"
loadItems={async () =>
await ResourceService.listResource({ workspace: $workspaceStore ?? 'NO_W' })}
loadItems={async () => await ResourceService.listResource({ workspace: ws ?? 'NO_W' })}
>
{#snippet submission()}
<div class="flex flex-row gap-x-1 mr-2">
@@ -812,12 +818,15 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
documentationLink="https://www.windmill.dev/docs/core_concepts/resources_and_types"
itemName="Resource Type"
extraField="name"
loadItems={async () =>
await ResourceService.listResourceType({ workspace: $workspaceStore ?? 'NO_W' })}
loadItems={async () => await ResourceService.listResourceType({ workspace: ws ?? 'NO_W' })}
/>
{/if}
<ResourceEditorDrawer bind:this={resourceEditor} on:refresh={resourcePicker.openDrawer} />
<VariableEditor bind:this={variableEditor} on:create={variablePicker.openDrawer} />
<ResourceEditorDrawer
bind:this={resourceEditor}
workspace={ws}
on:refresh={resourcePicker.openDrawer}
/>
<VariableEditor bind:this={variableEditor} workspace={ws} on:create={variablePicker.openDrawer} />
{#if showDucklakePicker}
<ItemPicker
@@ -842,9 +851,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
documentationLink="https://www.windmill.dev/docs/core_concepts/persistent_storage/ducklake"
itemName="ducklake"
loadItems={async () =>
(await WorkspaceService.listDucklakes({ workspace: $workspaceStore ?? 'NO_W' })).map(
(path) => ({ path })
)}
(await WorkspaceService.listDucklakes({ workspace: ws ?? 'NO_W' })).map((path) => ({ path }))}
>
{#snippet submission()}
<div class="flex flex-row gap-x-1 mr-2">
@@ -885,9 +892,9 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
documentationLink="https://www.windmill.dev/docs/core_concepts/persistent_storage/data_tables"
itemName="data table"
loadItems={async () =>
(await WorkspaceService.listDataTables({ workspace: $workspaceStore ?? 'NO_W' })).map(
(d) => ({ path: d.name })
)}
(await WorkspaceService.listDataTables({ workspace: ws ?? 'NO_W' })).map((d) => ({
path: d.name
}))}
>
{#snippet submission()}
<div class="flex flex-row gap-x-1 mr-2">
@@ -923,7 +930,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
extraField2="resource_type"
loadItems={async () =>
await ResourceService.listResource({
workspace: $workspaceStore ?? 'NO_W',
workspace: ws ?? 'NO_W',
resourceType: 'postgresql,mysql,bigquery'
})}
></ItemPicker>
@@ -220,6 +220,7 @@
{kind}
size="sm"
drawerOffset={4000}
workspaceOverride={workspaceId}
/>
{#if savedPath && path && path !== savedPath}
<Alert
@@ -33,7 +33,8 @@
noText = false,
buttonVariant = 'default',
btnClasses = '',
disabled = false
disabled = false,
workspace = undefined
}: {
asset: Asset
_resourceMetadata?: { resource_type?: string }
@@ -44,9 +45,12 @@
buttonVariant?: ButtonType.Variant
btnClasses?: string
disabled?: boolean
/** Workspace the explored asset lives in; defaults to the nav workspace. */
workspace?: string
} = $props()
let dbManagerDrawer = $derived(globalDbManagerDrawer.val)
let ws = $derived(workspace ?? $workspaceStore)
const assetUri = $derived(formatAsset(asset))
</script>
@@ -60,18 +64,20 @@
on:click={async () => {
if (asset.kind === 'resource' && isDbType(_resourceMetadata?.resource_type)) {
let [resourcePath, specificTable] = asset.path.split('?table=')
dbManagerDrawer?.openDrawer({
type: 'database',
resourceType: _resourceMetadata.resource_type,
resourcePath,
specificTable
})
dbManagerDrawer?.openDrawer(
{
type: 'database',
resourceType: _resourceMetadata.resource_type,
resourcePath,
specificTable
},
ws
)
} else if (asset.kind === 's3object' && isS3Uri(assetUri)) {
s3FilePicker?.open(assetUri)
} else if (asset.kind === 'volume') {
const storage =
(await VolumeService.getVolumeStorage({ workspace: $workspaceStore! })) ?? undefined
s3FilePicker?.open({ s3: `volumes/${$workspaceStore}/${asset.path}/`, storage })
const storage = (await VolumeService.getVolumeStorage({ workspace: ws! })) ?? undefined
s3FilePicker?.open({ s3: `volumes/${ws}/${asset.path}/`, storage })
} else if (asset.kind === 'ducklake') {
let ducklake = asset.path.split('/')[0]
let specificTableSplit = asset.path.split('/')[1]?.split('.') as string[] | undefined
@@ -79,7 +85,7 @@
specificTableSplit?.length === 2
? [specificTableSplit[0], specificTableSplit[1]]
: [undefined, specificTableSplit?.[0]]
dbManagerDrawer?.openDrawer({ type: 'ducklake', ducklake, specificSchema, specificTable })
dbManagerDrawer?.openDrawer({ type: 'ducklake', ducklake, specificSchema, specificTable }, ws)
} else if (asset.kind === 'datatable') {
let datatable = asset.path.split('/')[0]
let specificTableSplit = asset.path.split('/')[1]?.split('.') as string[] | undefined
@@ -87,13 +93,16 @@
specificTableSplit?.length === 2
? [specificTableSplit[0], specificTableSplit[1]]
: [undefined, specificTableSplit?.[0]]
dbManagerDrawer?.openDrawer({
type: 'database',
resourceType: 'postgresql',
resourcePath: `datatable://${datatable}`,
specificTable,
specificSchema
})
dbManagerDrawer?.openDrawer(
{
type: 'database',
resourceType: 'postgresql',
resourcePath: `datatable://${datatable}`,
specificTable,
specificSchema
},
ws
)
}
onClick?.()
}}
@@ -73,7 +73,8 @@
runPreview(previewArgs, undefined)
}
const { flowStateStore, pathStore } = getContext<FlowEditorContext>('FlowEditorContext')
const { flowStateStore, pathStore, opWorkspace } =
getContext<FlowEditorContext>('FlowEditorContext')
const dispatch = createEventDispatcher()
export async function runPreview(
@@ -82,7 +83,15 @@
) {
progressBar?.reset()
const newFlow = { value: { modules }, summary: '' }
jobId = await runFlowPreview(args, newFlow, $pathStore, restartedFrom)
jobId = await runFlowPreview(
args,
newFlow,
$pathStore,
restartedFrom,
undefined,
undefined,
opWorkspace?.()
)
isRunning = true
}
@@ -130,7 +139,7 @@
try {
jobId &&
(await JobService.cancelQueuedJob({
workspace: $workspaceStore ?? '',
workspace: opWorkspace?.() ?? $workspaceStore ?? '',
id: jobId,
requestBody: {}
}))
@@ -177,6 +186,7 @@
{#if jobId}
<FlowStatusViewer
bind:flowState={flowStateStore.val}
workspaceId={opWorkspace?.()}
{jobId}
onJobsLoaded={({ job: newJob }) => {
job = newJob
@@ -133,8 +133,11 @@
fakeInitialPath,
customUi,
executionCount,
devTempScriptRefs
devTempScriptRefs,
opWorkspace
} = $state(getContext<FlowEditorContext>('FlowEditorContext'))
// Acting workspace when previewing inside an AI session; else the nav workspace.
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
const dispatch = createEventDispatcher()
let renderCount: number = $state(0)
@@ -193,14 +196,15 @@
lastPreviewFlow = JSON.stringify(flowStore.val)
flowProgressBar?.reset()
const newFlow = extractFlow(previewMode)
args = await processSecretArgs(args, flowStore.val.schema as any)
args = await processSecretArgs(args, flowStore.val.schema as any, opWs)
newJobId = await runFlowPreview(
args,
newFlow,
$pathStore,
restartedFrom,
conversationId,
devTempScriptRefs?.()
devTempScriptRefs?.(),
opWorkspace?.()
)
jobId = newJobId
isRunning = true
@@ -286,7 +290,7 @@
subJobIds.map(async (subId) => {
try {
const subJob = await JobService.getJob({
workspace: $workspaceStore!,
workspace: opWs!,
id: subId
})
flowRecording.addCompletedJob(subId, subJob)
@@ -332,11 +336,11 @@
untrack(() => {
for (const mod of modules) {
if (mod.job) {
flowRecording.watchSubJob(mod.job, $workspaceStore!)
flowRecording.watchSubJob(mod.job, opWs!)
}
}
if (job?.flow_status?.failure_module?.job) {
flowRecording.watchSubJob(job.flow_status.failure_module.job, $workspaceStore!)
flowRecording.watchSubJob(job.flow_status.failure_module.job, opWs!)
}
})
}
@@ -347,7 +351,7 @@
try {
jobId &&
(await JobService.cancelQueuedJob({
workspace: $workspaceStore ?? '',
workspace: opWs ?? '',
id: jobId,
requestBody: {}
}))
@@ -514,6 +518,7 @@
runnableId={$initialPathStore}
stablePathForCaptures={$initialPathStore || fakeInitialPath}
runnableType={'FlowPath'}
workspace={opWs}
previewArgs={previewArgs.val}
on:openTriggers
on:select={(e) => {
@@ -562,6 +567,7 @@
<SchemaForm
noVariablePicker
compact
workspace={opWs}
schema={flowStore.val.schema}
bind:args={previewArgs.val}
on:change={() => {
@@ -632,7 +638,7 @@
<div class="w-full my-6">
<FlowExecutionStatus
{job}
workspaceId={$workspaceStore}
workspaceId={opWs}
{isOwner}
innerModules={job?.flow_status?.modules}
{suspendStatus}
@@ -665,6 +671,7 @@
hideDownloadInGraph={customUi?.downloadLogs === false}
wideResults
bind:flowState={flowStateStore.val}
workspaceId={opWs}
{jobId}
onDone={async ({ job: completedJob }) => {
isRunning = false
@@ -17,6 +17,8 @@
placement?: 'bottom-start' | 'top-start' | 'bottom-end' | 'top-end'
limitPayloadSize?: boolean
searchArgs?: Record<string, any> | undefined
/** Workspace to read run history from; defaults to the nav workspace. */
workspace?: string
}
let {
@@ -26,9 +28,12 @@
showAuthor = false,
placement = 'top-end',
limitPayloadSize = false,
searchArgs = undefined
searchArgs = undefined,
workspace = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let historicList: HistoricList | undefined = $state(undefined)
const dispatch = createEventDispatcher()
@@ -111,7 +116,7 @@
jobKinds: getJobKinds(runnableType),
syncQueuedRunsCount: false,
refreshRate: 10000,
currentWorkspace: $workspaceStore ?? '',
currentWorkspace: ws ?? '',
skip: !runnableId,
excludesEntrypointOverride: true
}) satisfies UseJobLoaderArgs
+8 -8
View File
@@ -119,7 +119,7 @@
if (lastJobId && (job || lastCallbacks?.loadExtraLogs)) {
plimit(() =>
JobService.getCompletedJobLogsTail({
workspace: $workspaceStore!,
workspace: workspace!,
id: lastJobId
})
).then((res) => {
@@ -222,7 +222,7 @@
return abstractRun(
() =>
JobService.runScriptByPath({
workspace: $workspaceStore!,
workspace: workspace!,
path: path ?? '',
requestBody: args,
skipPreprocessor: true
@@ -239,7 +239,7 @@
return abstractRun(
() =>
JobService.runScriptByHash({
workspace: $workspaceStore!,
workspace: workspace!,
hash: hash ?? '',
requestBody: args,
skipPreprocessor: true
@@ -256,7 +256,7 @@
return abstractRun(
() =>
JobService.runFlowByPath({
workspace: $workspaceStore!,
workspace: workspace!,
path: path ?? '',
requestBody: args,
skipPreprocessor: true
@@ -274,7 +274,7 @@
return abstractRun(
() =>
JobService.runFlowPreview({
workspace: $workspaceStore!,
workspace: workspace!,
requestBody: {
args,
value: flow.value,
@@ -318,7 +318,7 @@
return abstractRun(
() =>
JobService.runDynamicSelect({
workspace: $workspaceStore!,
workspace: workspace!,
requestBody: { entrypoint_function, args, runnable_ref }
}),
callbacks
@@ -342,7 +342,7 @@
return abstractRun(
() =>
JobService.runScriptPreview({
workspace: $workspaceStore!,
workspace: workspace!,
timeout,
requestBody: {
path,
@@ -371,7 +371,7 @@
currentEventSource = undefined
try {
await JobService.cancelQueuedJob({
workspace: $workspaceStore ?? '',
workspace: workspace ?? '',
id,
requestBody: {}
})
@@ -39,9 +39,13 @@
stepsInputArgs,
previewArgs,
modulesTestStates,
devTempScriptRefs
devTempScriptRefs,
opWorkspace
} = getContext<FlowEditorContext>('FlowEditorContext')
// Acting workspace when the flow editor runs in an AI session; else the nav workspace.
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
let jobLoader: JobLoader | undefined = $state(undefined)
let jobProgressReset: () => void = () => {}
let stepHistoryLoader = getStepHistoryLoaderContext()
@@ -102,8 +106,8 @@
)
} else if (val.type == 'script') {
const script = val.hash
? await ScriptService.getScriptByHash({ workspace: $workspaceStore!, hash: val.hash })
: await getScriptByPath(val.path)
? await ScriptService.getScriptByHash({ workspace: opWs!, hash: val.hash })
: await getScriptByPath(val.path, opWs)
await jobLoader?.runPreview(
val.path,
script.content,
@@ -202,6 +206,7 @@
<JobLoader
noCode={true}
toastError={noEditor}
workspaceOverride={opWs}
bind:scriptProgress
bind:this={jobLoader}
bind:isLoading={
@@ -20,6 +20,8 @@
disabled?: boolean
datatableAsPgResource?: boolean
onClear?: () => void
/** Workspace the resource picker lists from; defaults to the nav workspace. */
workspace?: string
}
let {
@@ -32,7 +34,8 @@
editor = $bindable(undefined),
disabled = false,
datatableAsPgResource = false,
onClear = undefined
onClear = undefined,
workspace = undefined
}: Props = $props()
function isResource() {
@@ -55,7 +58,7 @@
<!-- {JSON.stringify({ value })} -->
<div class="flex flex-row w-full flex-wrap gap-x-2 gap-y-0.5">
{#if format === 'resource-s3_object'}
<S3ObjectPicker bind:value />
<S3ObjectPicker bind:value {workspace} />
{:else if value == undefined || typeof value === 'string'}
<ResourcePicker
{datatableAsPgResource}
@@ -63,6 +66,7 @@
{selectFirst}
{disablePortal}
{onClear}
{workspace}
bind:value={
() => valueToPath(),
(v) => {
+34 -25
View File
@@ -78,6 +78,11 @@
disableEditing?: boolean
size?: 'sm' | 'md'
drawerOffset?: number
/** Workspace the folder list and path-existence checks run against.
* Defaults to the navigation `$workspaceStore`; pass the session's acting
* workspace when the editor operates on a workspace other than the one the
* top nav points at (see the sessions preview / dev-workspace flows). */
workspaceOverride?: string
}
let {
@@ -94,9 +99,12 @@
hideUser = false,
disableEditing = false,
size = 'md',
drawerOffset = 0
drawerOffset = 0,
workspaceOverride = undefined
}: Props = $props()
let ws = $derived(workspaceOverride ?? $workspaceStore)
$effect.pre(() => {
if (path == undefined) {
path = ''
@@ -203,7 +211,7 @@
folders = initialFolders.concat(
(
await FolderService.listFolderNames({
workspace: $workspaceStore!
workspace: ws!
})
)
.filter((x) => !excludedFolders.includes(x))
@@ -244,74 +252,74 @@
async function pathExists(path: string, kind: PathKind): Promise<boolean> {
if (!path.length) return false
if (kind == 'flow') {
return await FlowService.existsFlowByPath({ workspace: $workspaceStore!, path: path })
return await FlowService.existsFlowByPath({ workspace: ws!, path: path })
} else if (kind == 'script') {
return await ScriptService.existsScriptByPath({
workspace: $workspaceStore!,
workspace: ws!,
path: path
})
} else if (kind == 'resource') {
return await ResourceService.existsResource({
workspace: $workspaceStore!,
workspace: ws!,
path: path
})
} else if (kind == 'variable') {
return await VariableService.existsVariable({
workspace: $workspaceStore!,
workspace: ws!,
path: path
})
} else if (kind == 'schedule') {
return await ScheduleService.existsSchedule({ workspace: $workspaceStore!, path: path })
return await ScheduleService.existsSchedule({ workspace: ws!, path: path })
} else if (kind == 'app') {
return await AppService.existsApp({ workspace: $workspaceStore!, path: path })
return await AppService.existsApp({ workspace: ws!, path: path })
} else if (kind == 'http_trigger') {
return await HttpTriggerService.existsHttpTrigger({
workspace: $workspaceStore!,
workspace: ws!,
path: path
})
} else if (kind == 'websocket_trigger') {
return await WebsocketTriggerService.existsWebsocketTrigger({
workspace: $workspaceStore!,
workspace: ws!,
path: path
})
} else if (kind == 'kafka_trigger') {
return await KafkaTriggerService.existsKafkaTrigger({
workspace: $workspaceStore!,
workspace: ws!,
path: path
})
} else if (kind == 'postgres_trigger') {
return await PostgresTriggerService.existsPostgresTrigger({
workspace: $workspaceStore!,
workspace: ws!,
path: path
})
} else if (kind == 'nats_trigger') {
return await NatsTriggerService.existsNatsTrigger({
workspace: $workspaceStore!,
workspace: ws!,
path: path
})
} else if (kind === 'mqtt_trigger') {
return await MqttTriggerService.existsMqttTrigger({
workspace: $workspaceStore!,
workspace: ws!,
path: path
})
} else if (kind == 'sqs_trigger') {
return await SqsTriggerService.existsSqsTrigger({
workspace: $workspaceStore!,
workspace: ws!,
path: path
})
} else if (kind === 'gcp_trigger') {
return await GcpTriggerService.existsGcpTrigger({
workspace: $workspaceStore!,
workspace: ws!,
path: path
})
} else if (kind === 'azure_trigger') {
return await AzureTriggerService.existsAzureTrigger({
workspace: $workspaceStore!,
workspace: ws!,
path: path
})
} else if (kind === 'email_trigger') {
return await EmailTriggerService.existsEmailTrigger({
workspace: $workspaceStore!,
workspace: ws!,
path: path
})
} else {
@@ -398,7 +406,7 @@
})
})
$effect.pre(() => {
if ($workspaceStore && $userStore) {
if (ws && $userStore) {
untrack(() => {
loadFolders()
initPath()
@@ -412,30 +420,30 @@
)
let pathUsageInFlowsPromise = $derived(
(kind == 'script' || kind == 'flow') &&
$workspaceStore &&
ws &&
initialPath &&
FlowService.listFlowPathsFromWorkspaceRunnable({
workspace: $workspaceStore,
workspace: ws,
path: initialPath,
runnableKind: kind
})
)
let pathUsageInAppsPromise = $derived(
(kind == 'script' || kind == 'flow') &&
$workspaceStore &&
ws &&
initialPath &&
AppService.listAppPathsFromWorkspaceRunnable({
workspace: $workspaceStore,
workspace: ws,
path: initialPath,
runnableKind: kind
})
)
let pathUsageInScriptsPromise = $derived(
kind == 'script' &&
$workspaceStore &&
ws &&
initialPath &&
ScriptService.listScriptPathsFromWorkspaceRunnable({
workspace: $workspaceStore,
workspace: ws,
path: initialPath
})
)
@@ -525,6 +533,7 @@
bind:this={inputP}
bind:value={meta.name}
prefix={`${meta.ownerKind?.charAt(0) ?? ''}/${meta.owner ?? ''}/`}
workspace={ws}
{size}
{error}
{autofocus}
@@ -101,6 +101,10 @@
error?: string | boolean
textInputClass?: string
onkeyup?: (e: KeyboardEvent) => void
/** Workspace whose paths feed the autocomplete. Defaults to the navigation
* `$workspaceStore`; pass the acting workspace when the editor operates on
* a workspace other than the one the top nav points at. */
workspace?: string
}
let {
@@ -113,9 +117,12 @@
size = 'md',
error,
textInputClass,
onkeyup
onkeyup,
workspace = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let inputEl: TextInput | undefined = $state(undefined)
export function focus() {
inputEl?.focus()
@@ -258,12 +265,12 @@
async function loadPaths(workspace: string) {
const paths = await fetchWorkspacePaths(workspace)
// Guard against workspace changing during the in-flight fetch.
if ($workspaceStore === workspace) allPaths = paths
if (ws === workspace) allPaths = paths
}
$effect(() => {
const ws = $workspaceStore
if (ws) void loadPaths(ws)
const w = ws
if (w) void loadPaths(w)
})
$effect(() => {
@@ -300,7 +307,7 @@
function onInputFocus() {
hasFocus = true
// Opportunistic refresh if the cache is stale.
if ($workspaceStore) void loadPaths($workspaceStore)
if (ws) void loadPaths(ws)
}
function onInputBlur() {
setTimeout(() => {
@@ -402,6 +402,7 @@
{loadingSchema}
{resourceToEdit}
onLoadResourceType={() => resourceTypeResource.refetch()}
workspace={selected}
/>
{/key}
{/if}
@@ -42,6 +42,9 @@
loadingSchema: boolean
resourceToEdit: Resource | undefined
onLoadResourceType?: () => void
/** Workspace the path is validated against and the connection is tested in;
* defaults to the nav workspace. */
workspace?: string | undefined
}
let {
@@ -62,9 +65,12 @@
resourceSchema,
loadingSchema,
resourceToEdit,
onLoadResourceType
onLoadResourceType,
workspace = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let editDescription = $state(false)
let rawCode: string | undefined = $state(undefined)
let textFileContent: string = $state('')
@@ -131,11 +137,12 @@
{/if}
<Label label="Path">
<Path
disabled={initialPath != '' && !isOwner(initialPath, $userStore, $workspaceStore)}
disabled={initialPath != '' && !isOwner(initialPath, $userStore, ws)}
bind:path
{initialPath}
namePlaceholder="resource"
kind="resource"
workspaceOverride={workspace}
/>
</Label>
</div>
@@ -218,7 +225,11 @@
{#if resourceToEdit?.resource_type === 'nats' || resourceToEdit?.resource_type === 'kafka'}
<TestTriggerConnection kind={resourceToEdit?.resource_type} args={{ connection: args }} />
{:else}
<TestConnection resourceType={resourceToEdit?.resource_type} {args} />
<TestConnection
resourceType={resourceToEdit?.resource_type}
{args}
workspaceOverride={workspace}
/>
{/if}
{#if resource_type === 'git_repository' && $workspaceStore && ($userStore?.is_admin || $userStore?.is_super_admin)}
<GitHubAppIntegration
@@ -274,6 +285,7 @@
schema={resourceSchema}
bind:args
bind:isValid
{workspace}
/>
{/if}
{:else if !can_write}
@@ -18,6 +18,9 @@
selectedFileKey?: { s3: string; storage?: string } | undefined
folderOnly?: boolean
regexFilter?: RegExp | undefined
/** Workspace to browse S3 storage in — the acting workspace of the editor that
* opened the picker, else the nav workspace. */
workspace?: string | undefined
onClose?: () => void
onSelectAndClose?: (selected: { s3: string; storage: string | undefined }) => void
}
@@ -30,10 +33,13 @@
selectedFileKey = $bindable(undefined),
folderOnly = false,
regexFilter = undefined,
workspace = undefined,
onClose,
onSelectAndClose
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let drawer: Drawer | undefined = $state()
let s3FilePickerInner: S3FilePickerInner | undefined = $state()
@@ -55,8 +61,8 @@
> = $state({})
let secondaryStorageNames = resource(
() => $workspaceStore,
() => SettingService.getSecondaryStorageNames({ workspace: $workspaceStore! }),
() => ws,
() => SettingService.getSecondaryStorageNames({ workspace: ws! }),
{ lazy: true }
)
@@ -105,6 +111,7 @@
bind:uploadModalOpen
{folderOnly}
{regexFilter}
{workspace}
/>
{#snippet actions()}
<div class="flex gap-1">
@@ -65,6 +65,8 @@
regexFilter?: RegExp | undefined
hideS3SpecificDetails?: boolean
rootPath?: string
/** Workspace to browse S3 storage in — defaults to the nav workspace. */
workspace?: string | undefined
workspaceSettingsInitialized?: boolean
storage?: string | undefined
uploadModalOpen?: boolean
@@ -103,6 +105,7 @@
regexFilter = undefined,
hideS3SpecificDetails = false,
rootPath: initialRootPath = '',
workspace = undefined,
workspaceSettingsInitialized = $bindable(true),
storage = $bindable(undefined),
uploadModalOpen = $bindable(false),
@@ -117,6 +120,8 @@
testConnectionRequest = HelpersService.datasetStorageTestConnection
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let rootPath = $state(initialRootPath)
let rootPathNestingLevel = $derived(1 * (rootPath.split('/').length - 1))
@@ -183,7 +188,7 @@
async function loadFiles() {
fileListLoading = true
let availableFiles = await listStoredFilesRequest({
workspace: $workspaceStore!,
workspace: ws!,
maxKeys: maxKeys, // fixed pages of 1000 files for now
marker: page == 0 ? undefined : listMarkers[page - 1],
prefix: rootPath ?? (filter.trim() != '' ? filter : undefined),
@@ -280,7 +285,7 @@
}
fileInfoLoading = true
let fileMetadataRaw = await loadFileMetadataRequest({
workspace: $workspaceStore!,
workspace: ws!,
fileKey: fileKey,
storage: storage
})
@@ -300,7 +305,7 @@
async function loadFilePreview(fileKey: string, fileSizeInBytes?: number, fileMimeType?: string) {
let filePreviewRaw = await loadFilePreviewRequest({
workspace: $workspaceStore!,
workspace: ws!,
fileKey: fileKey,
fileSizeInBytes: fileSizeInBytes,
fileMimeType: fileMimeType,
@@ -349,7 +354,7 @@
}
try {
await deleteS3FileRequest({
workspace: $workspaceStore!,
workspace: ws!,
fileKey: fileKey,
storage: storage
})
@@ -409,7 +414,7 @@
}
try {
await moveS3FileRequest({
workspace: $workspaceStore!,
workspace: ws!,
srcFileKey: srcFileKey,
destFileKey: destFileKey!,
storage: storage
@@ -457,7 +462,7 @@
fileListLoading = true
try {
await testConnectionRequest({
workspace: $workspaceStore!,
workspace: ws!,
storage: storage
})
workspaceSettingsInitialized = true
@@ -716,7 +721,7 @@
{#if filePreview !== undefined && (!hideS3SpecificDetails || !readOnlyMode || allowDelete)}
<div class="flex gap-2 shrink-0">
{#if !hideS3SpecificDetails}
{@const downloadApiPath = `/w/${$workspaceStore}/job_helpers/download_s3_file?file_key=${encodeURIComponent(fileMetadata?.fileKey ?? '')}${storage ? `&storage=${storage}` : ''}`}
{@const downloadApiPath = `/w/${ws}/job_helpers/download_s3_file?file_key=${encodeURIComponent(fileMetadata?.fileKey ?? '')}${storage ? `&storage=${storage}` : ''}`}
{@const downloadName =
fileMetadata?.fileKey.split('/').pop() ?? 'unnamed_download.file'}
{#if shouldDownloadViaClient()}
@@ -14,9 +14,15 @@
interface Props {
value: any
editor?: SimpleEditor | undefined
/** Workspace to browse/upload S3 objects in; defaults to the nav workspace. */
workspace?: string | undefined
}
let { value = $bindable(), editor = $bindable(undefined) }: Props = $props()
let {
value = $bindable(),
editor = $bindable(undefined),
workspace = undefined
}: Props = $props()
const dispatch = createEventDispatcher()
@@ -48,6 +54,7 @@
editor?.setCode(rawValue)
}}
readOnlyMode={false}
{workspace}
/>
<div class="flex flex-col w-full gap-1">
@@ -85,6 +92,7 @@
}
}}
defaultValue={value?.s3}
{workspace}
/>
{/if}
<Button
@@ -11,12 +11,13 @@
const dispatch = createEventDispatcher()
interface Props {
runnableId: string | undefined;
runnableType: RunnableType | undefined;
args: object;
disabled?: boolean;
small?: boolean | undefined;
showTooltip?: boolean | undefined;
runnableId: string | undefined
runnableType: RunnableType | undefined
args: object
disabled?: boolean
small?: boolean | undefined
showTooltip?: boolean | undefined
workspace?: string | undefined
}
let {
@@ -25,8 +26,11 @@
args,
disabled = false,
small = undefined,
showTooltip = undefined
}: Props = $props();
showTooltip = undefined,
workspace = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let savingInputs = $state(false)
@@ -40,7 +44,7 @@
try {
await InputService.createInput({
workspace: $workspaceStore!,
workspace: ws!,
runnableId,
runnableType,
requestBody
@@ -21,6 +21,8 @@
noButton?: boolean
jsonView?: boolean
limitPayloadSize?: boolean
/** Workspace to read/write saved inputs from; defaults to the nav workspace. */
workspace?: string
}
let {
@@ -30,9 +32,12 @@
isValid = false,
noButton = false,
jsonView = false,
limitPayloadSize = false
limitPayloadSize = false,
workspace = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
interface EditableInput extends Input {
isEditing?: boolean
isSaving?: boolean
@@ -58,7 +63,7 @@
try {
await InputService.updateInput({
workspace: $workspaceStore!,
workspace: ws!,
requestBody: {
id: input.id,
name: input.name,
@@ -76,7 +81,7 @@
function initLoadInputs() {
const loadInputsPageFn = async (page: number, perPage: number) => {
const inputs = await InputService.listInputs({
workspace: $workspaceStore!,
workspace: ws!,
runnableId,
runnableType,
page,
@@ -104,7 +109,7 @@
const deleteInputFn = async (id: string) => {
await InputService.deleteInput({
workspace: $workspaceStore!,
workspace: ws!,
input: id
})
}
@@ -119,7 +124,7 @@
if (!id) return
return await InputService.getArgsFromHistoryOrSavedInput({
jobOrInputId: id,
workspace: $workspaceStore!,
workspace: ws!,
input,
allowLarge
})
@@ -198,7 +203,7 @@
}
$effect(() => {
$workspaceStore &&
ws &&
runnableId &&
runnableType &&
(infiniteList && untrack(() => initLoadInputs()), (draft = false))
@@ -222,6 +227,7 @@
{runnableId}
{runnableType}
args={previewArgs ?? {}}
workspace={ws}
disabled={!previewArgs || !runnableId || !isValid || jsonView}
on:update={() => {
refresh()
@@ -121,6 +121,8 @@
actions: actions_render = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
const dispatch = createEventDispatcher()
let inputCheck: { [id: string]: boolean } = $state({})
@@ -502,7 +504,7 @@
documentationLink="https://www.windmill.dev/docs/core_concepts/variables_and_secrets"
extraField="path"
loadItems={async () =>
(await VariableService.listVariable({ workspace: $workspaceStore ?? '' })).map((x) => ({
(await VariableService.listVariable({ workspace: ws ?? '' })).map((x) => ({
name: x.path,
...x
}))}
@@ -521,5 +523,5 @@
{/snippet}
</ItemPicker>
<VariableEditor bind:this={variableEditor} />
<VariableEditor bind:this={variableEditor} workspace={ws} />
{/if}
@@ -19,6 +19,9 @@
isValid?: boolean
jsonView?: boolean
children?: import('svelte').Snippet
/** Workspace the History / Saved inputs / Captures tabs read and mutate;
* defaults to the nav workspace. Session editors pass their acting workspace. */
workspace?: string
}
let {
@@ -28,7 +31,8 @@
previewArgs,
isValid = true,
jsonView = false,
children
children,
workspace = undefined
}: Props = $props()
const dispatch = createEventDispatcher()
@@ -140,6 +144,7 @@
bind:this={historicInputs}
{runnableId}
{runnableType}
{workspace}
on:select={(e) => {
dispatch('select', { payload: e.detail?.args, type: 'history' })
}}
@@ -152,6 +157,7 @@
{runnableId}
{runnableType}
{previewArgs}
{workspace}
bind:this={savedInputsPicker}
on:select={(e) => {
dispatch('select', { payload: e.detail, type: 'saved' })
@@ -171,6 +177,7 @@
<div class="h-full">
<CaptureTable
path={stablePathForCaptures}
{workspace}
on:select={(e) => {
dispatch('select', { payload: e.detail, type: 'captures' })
}}
@@ -1151,6 +1151,7 @@
autofocus={false}
namePlaceholder="script"
kind="script"
workspaceOverride={opWorkspace}
/>
{#if initialPath && script.path && script.path !== initialPath}
<Alert
@@ -2082,6 +2083,7 @@
<ScriptEditor
{disableAi}
workspaceOverride={opWorkspace}
sessionOpen={script.path
? {
target: { kind: 'script', path: script.path },
+30 -16
View File
@@ -223,6 +223,11 @@
// built by the pipeline page from the resolved graph. Absent outside the
// pipeline editor — the check still runs, just without suppression.
schemaContractContext?: SchemaContractGraphContext
// Workspace to scope this editor's calls to. Defaults to the nav
// `$workspaceStore`; an AI-session live editor passes the session's
// acting workspace (a fork) so tests, captures and toolbar lookups hit
// the right workspace instead of the nav one.
workspaceOverride?: string
}
let {
@@ -266,9 +271,12 @@
onTestJob,
initialTestPanelCollapsed = false,
sessionOpen = undefined,
schemaContractContext = undefined
schemaContractContext = undefined,
workspaceOverride = undefined
}: Props = $props()
let opWs = $derived(workspaceOverride ?? $workspaceStore)
$effect(() => {
onTestStateChange?.(testIsLoading)
})
@@ -596,7 +604,7 @@
let inferAssetsRes = resource([() => lang, () => code, () => code], () => inferAssets(lang, code))
let preparedSqlQueries = usePreparedAssetSqlQueries(
() => inferAssetsRes.current?.sql_queries,
() => $workspaceStore
() => opWs
)
// Asset-parse validity for the editor badge. `undefined` while loading (so
// the badge doesn't flicker red); only an explicit parser error counts as
@@ -648,7 +656,7 @@
let contractCheckSeq = 0
watch([() => inferAssetsRes.current, () => schemaContractContext], () => {
const res = inferAssetsRes.current
const workspace = $workspaceStore
const workspace = opWs
const seq = ++contractCheckSeq
if (!workspace || !res || res.status === 'error') {
contractMarkers = []
@@ -837,7 +845,7 @@
? { _ENTRYPOINT_OVERRIDE: 'preprocessor', ...(args ?? {}) }
: (args ?? {})
const testSchema = activeModuleTab !== null ? testPanelSchema : schema
const testArgs = await processSecretArgs(rawTestArgs, testSchema)
const testArgs = await processSecretArgs(rawTestArgs, testSchema, opWs)
if (showPsCommonParams) {
for (const [k, v] of Object.entries(psCommonParams)) {
if (v !== undefined && v !== false && v !== '') {
@@ -908,7 +916,7 @@
async function loadPastTests(): Promise<void> {
pastPreviewsRequest?.cancel()
const req = JobService.listCompletedJobs({
workspace: $workspaceStore!,
workspace: opWs!,
jobKinds: 'preview',
createdBy: $userStore?.username,
scriptPathExact: path,
@@ -1210,12 +1218,12 @@
dapClient = getDAPClient(dapServerUrl)
// Fetch contextual variables (WM_WORKSPACE, WM_TOKEN, etc.) from backend
const env = await fetchContextualVariables($workspaceStore ?? '')
const env = await fetchContextualVariables(opWs ?? '')
// Sign the debug request (creates audit log entry)
let signedPayload
try {
signedPayload = await signDebugRequest($workspaceStore ?? '', code ?? '', lang ?? 'python3')
signedPayload = await signDebugRequest(opWs ?? '', code ?? '', lang ?? 'python3')
debugSessionJobId = signedPayload.job_id
} catch (signError) {
sendUserToast(getDebugErrorMessage(signError), true)
@@ -1435,11 +1443,11 @@
// what's there" affordance, not a user action. Skipped when a test is
// already running so a live job's stream is never clobbered.
async function loadLastRunIntoTestPanel(): Promise<void> {
if (!path || !$workspaceStore) return
if (!path || !opWs) return
if (testIsLoading || testJob !== undefined) return
try {
const jobs = await JobService.listCompletedJobs({
workspace: $workspaceStore,
workspace: opWs,
scriptPathExact: path,
hasNullParent: true,
perPage: 1,
@@ -1471,7 +1479,7 @@
let token: string | undefined
try {
token = await signMultiplayerRequest($workspaceStore ?? '')
token = await signMultiplayerRequest(opWs ?? '')
} catch (e) {
console.error('Failed to sign multiplayer request:', e)
sendUserToast('Failed to authorize multiplayer session', true)
@@ -1486,7 +1494,7 @@
wsProvider = new WebsocketProvider(
buildWsUrl('/ws_mp/'),
$workspaceStore + '/' + (path ?? 'no-room-name'),
opWs + '/' + (path ?? 'no-room-name'),
ydoc,
{ connect: false, params: { token } }
)
@@ -1554,7 +1562,7 @@
let url = new URL(window.location.toString().split('#')[0])
url.search = ''
return (
`${url}?collab=1&workspace=${encodeURIComponent($workspaceStore ?? '')}&lang=${encodeURIComponent(lang ?? '')}` +
`${url}?collab=1&workspace=${encodeURIComponent(opWs ?? '')}&lang=${encodeURIComponent(lang ?? '')}` +
(edit ? '' : `&path=${path}`)
)
}
@@ -1725,6 +1733,7 @@
<JobLoader
noCode={true}
workspaceOverride={opWs}
bind:scriptProgress
bind:this={jobLoader}
bind:isLoading={testIsLoading}
@@ -1760,6 +1769,7 @@
<div class="flex justify-between space-x-2">
{#if args}
<EditorBar
workspace={opWs}
scriptPath={edit ? path : undefined}
on:toggleCollabMode={() => {
if (wsProvider?.shouldConnect) {
@@ -1978,7 +1988,7 @@
it visually pinned to the top edge without
relying on cross-browser overflow behaviour. -->
<div class="relative h-full pt-9 flex flex-col">
{#if testJob?.id && testJob.type === 'CompletedJob' && $workspaceStore}
{#if testJob?.id && testJob.type === 'CompletedJob' && opWs}
<!-- Right-side affordances when we're displaying a *completed*
job (either the user just ran a test, or the on-mount
last-run loader populated the panel). The job-id link
@@ -1989,7 +1999,7 @@
<div class="absolute top-1 right-2 z-10 flex items-center gap-2">
<a
class="text-3xs text-blue-600 hover:underline font-mono"
href={`${base}/run/${testJob.id}?workspace=${$workspaceStore}`}
href={`${base}/run/${testJob.id}?workspace=${opWs}`}
target="_blank"
rel="noopener noreferrer"
title="Open this run"
@@ -1997,7 +2007,7 @@
{testJob.id.slice(0, 8)}… ↗
</a>
<DispatchEventsButton
workspace={testJob.workspace_id ?? $workspaceStore}
workspace={testJob.workspace_id ?? opWs}
jobId={testJob.id}
/>
</div>
@@ -2142,6 +2152,7 @@
>
{#key argsRender}
<SchemaForm
workspace={opWs}
helperScript={{
source: 'inline',
code,
@@ -2211,6 +2222,7 @@
{#key argsRender}
{#if activeModuleTab !== null}
<SchemaForm
workspace={opWs}
helperScript={{
source: 'inline',
code: editorCode,
@@ -2227,6 +2239,7 @@
/>
{:else}
<SchemaForm
workspace={opWs}
helperScript={{
source: 'inline',
code,
@@ -2331,6 +2344,7 @@
<div class="h-full p-2">
<CaptureTable
bind:this={captureTable}
workspace={opWs}
{hasPreprocessor}
canHavePreprocessor={canHavePreprocessor(lang)}
isFlow={false}
@@ -2649,7 +2663,7 @@
client={dapClient}
currentFrameId={currentDebugFrameId}
onClose={() => (showDebugConsole = false)}
workspace={$workspaceStore}
workspace={opWs}
jobId={debugSessionJobId ?? undefined}
/>
</Pane>
+15 -5
View File
@@ -21,8 +21,18 @@
/** Called after a migration is run via the DDL guard, so the schema view
* can be refreshed to reflect the applied change. */
onSchemaChange?: () => void
/** Workspace the REPL queries and DDL migrations run against; defaults to
* the nav workspace. */
workspace?: string | undefined
}
let { input, onData, placeholderTableName, onSchemaChange }: Props = $props()
let {
input,
onData,
placeholderTableName,
onSchemaChange,
workspace = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let dbType = $derived(getDbType(input))
// A datatable REPL targets `datatable://<name>`; surface DDL statements as
@@ -47,7 +57,7 @@
let runHistory: (StepHistoryData & { code: string; result: Record<string, any>[] })[] = $state([])
async function run({ doPostgresRowToJsonFix }: { doPostgresRowToJsonFix?: boolean } = {}) {
if (isRunning || !$workspaceStore) return
if (isRunning || !ws) return
const READ_OPS = ['SELECT', 'WITH', 'SHOW', 'EXPLAIN', 'DESCRIBE']
// On a datatable, intercept DDL statements and offer to make them
@@ -91,7 +101,7 @@
let { job, result } = (await runScriptAndPollResult(
{
workspace: $workspaceStore,
workspace: ws,
requestBody: {
language: getLanguageByResourceType(dbType),
content: transformedCode,
@@ -181,6 +191,6 @@
</Pane>
</Splitpanes>
{#if datatableName && $workspaceStore}
<DdlMigrationGuard bind:this={ddlGuard} workspace={$workspaceStore} datatable={datatableName} />
{#if datatableName && ws}
<DdlMigrationGuard bind:this={ddlGuard} workspace={ws} datatable={datatableName} />
{/if}
@@ -29,6 +29,12 @@
wsSpecific: boolean
}
// The "current" workspace this editor defaults New/Edit actions to. Session
// editors pass their acting workspace so secrets are created/updated there
// rather than in the navigation workspace. Defaults to $workspaceStore.
let { workspace = undefined }: { workspace?: string } = $props()
let curWs = $derived(workspace ?? $workspaceStore)
let editPath: string | undefined = $state(undefined)
// Per-workspace handles are driven by `useMany`. We track the workspace
@@ -119,9 +125,7 @@
// that case.
const selectedDirty = $derived(!!selected && dirtyWorkspaces.includes(selected))
const otherDirty = $derived(
dirtyWorkspaces.length == 1
? dirtyWorkspaces.filter((ws) => ws !== $workspaceStore)
: dirtyWorkspaces
dirtyWorkspaces.length == 1 ? dirtyWorkspaces.filter((ws) => ws !== curWs) : dirtyWorkspaces
)
const dirtyValid = $derived(
dirtyWorkspaces.every((ws) => {
@@ -198,7 +202,7 @@
export function initNew(): void {
reset()
editPath = undefined
const ws = $workspaceStore!
const ws = curWs!
const s: VariableState = {
path: '',
variable: { value: '', is_secret: true, description: '' },
@@ -215,7 +219,7 @@
export function editVariable(edit_path: string): void {
reset()
editPath = edit_path
selected = $workspaceStore!
selected = curWs!
drawer?.openDrawer()
}
@@ -337,18 +341,14 @@
{can_write}
{edit}
onLoadSecret={loadSecret}
{workspace}
/>
{/key}
{/if}
</div>
{#snippet actions()}
{#if edit && $workspaceStore}
<WsSpecificVersions
kind="variable"
workspaceId={$workspaceStore}
{initialPath}
bind:selected
/>
{#if edit && curWs}
<WsSpecificVersions kind="variable" workspaceId={curWs} {initialPath} bind:selected />
{/if}
<Button
on:click={save}
@@ -32,6 +32,8 @@
can_write: boolean
edit: boolean
onLoadSecret?: () => void
/** Workspace the path is validated against; defaults to the nav workspace. */
workspace?: string | undefined
}
let {
@@ -44,9 +46,12 @@
deployTo,
can_write,
edit,
onLoadSecret
onLoadSecret,
workspace = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
const MAX_VARIABLE_LENGTH = 10000
let editorKind: 'plain' | 'json' | 'yaml' = $state('plain')
@@ -60,12 +65,13 @@
<div class="flex flex-col gap-1">
<label for="path" class="text-xs font-semibold text-emphasis">Path</label>
<Path
disabled={initialPath != '' && !isOwner(initialPath, $userStore, $workspaceStore)}
disabled={initialPath != '' && !isOwner(initialPath, $userStore, ws)}
bind:error={pathError}
bind:path
{initialPath}
namePlaceholder="variable"
kind="variable"
workspaceOverride={workspace}
/>
<LabelsInput bind:labels />
</div>
@@ -41,7 +41,8 @@
preserveOnBehalfOf = $bindable(false),
labels = $bindable(),
rawApp = false,
newApp = false
newApp = false,
operatingWorkspace = undefined
}: {
policy: any
setPublishState: (message?: string) => void
@@ -68,8 +69,15 @@
* (`/secret_of/...` 404s with no `app` row) and renders a placeholder
* instead of the eternally-spinning link. */
newApp?: boolean
/** Workspace the app is deployed to — the session's acting workspace when
* embedded in a session preview, else the navigation `$workspaceStore`.
* The secret-URL / custom-path / folder / on-behalf-of lookups must target
* it, not `$workspaceStore` (which stays on the nav workspace in a session). */
operatingWorkspace?: string
} = $props()
const opWs = $derived(operatingWorkspace ?? $workspaceStore)
let isDeployer = $derived($userStore?.groups?.includes(WM_DEPLOYERS_GROUP) ?? false)
// Admins always pass the backend check. For everyone else, fail closed
// while the workspace protection rules are still loading so the toggle
@@ -93,7 +101,7 @@
async function appExists(customPath: string) {
return await AppService.customPathExists({
workspace: $workspaceStore!,
workspace: opWs!,
customPath
})
}
@@ -115,7 +123,7 @@
let secretUrlHref = $derived(secretUrl ? computeSecretUrl(secretUrl) : undefined)
let fullCustomUrl = $derived(
`${window.location.origin}${base}/a/${
isCloudHosted() || globalWorkspacedRoute ? $workspaceStore + '/' : ''
isCloudHosted() || globalWorkspacedRoute ? opWs + '/' : ''
}${customPath}`
)
@@ -131,7 +139,7 @@
}
async function getSecretUrl() {
secretUrl = await AppService.getPublicSecretOfApp({
workspace: $workspaceStore!,
workspace: opWs!,
path: appPath
})
}
@@ -229,6 +237,7 @@
namePlaceholder="app"
kind="app"
autofocus={false}
workspaceOverride={operatingWorkspace}
/>
<div class="py-2"></div>
@@ -247,7 +256,7 @@
Because you are either an admin or part of the {WM_DEPLOYERS_GROUP} group, you can select another
user to run this app on behalf of. Once deployed the app will be run on behalf of
<OnBehalfOfSelector
targetWorkspace={$workspaceStore ?? ''}
targetWorkspace={opWs ?? ''}
targetValue={savedOnBehalfOfEmail}
selected={onBehalfOfChoice}
onSelect={(choice, details) => {
@@ -108,7 +108,10 @@ export interface DbManagerUriState {
selectedSchema: string | undefined
selectedTable: string | undefined
readonly open: boolean
openDrawer: (nInput: DbInput) => void
/** Workspace the drawer's DB operations run against the acting workspace of
* the editor that opened it, else the navigation workspace. */
workspace: string | undefined
openDrawer: (nInput: DbInput, workspace?: string) => void
closeDrawer: () => void
}
@@ -148,7 +151,12 @@ export function useDbManagerUriState(): DbManagerUriState {
params.dbm = buildDbm(p)
}
function openDrawer(nInput: DbInput) {
// Not URL-persisted: the drawer defaults back to the nav workspace on reload,
// which is the correct fallback outside the session that opened it.
let workspace = $state<string | undefined>(undefined)
function openDrawer(nInput: DbInput, ws?: string) {
workspace = ws
if (nInput.type === 'database') {
const isDatatable = nInput.resourcePath.startsWith('datatable://')
params.dbm = buildDbm({
@@ -203,6 +211,12 @@ export function useDbManagerUriState(): DbManagerUriState {
get open() {
return !!input
},
get workspace() {
return workspace
},
set workspace(v: string | undefined) {
workspace = v
},
openDrawer,
closeDrawer
}
@@ -7,6 +7,7 @@
let s = $state({
val: {
selectedAsset: undefined,
workspace: undefined,
s3FilePicker: undefined,
resourceEditorDrawer: undefined,
resourceMetadataCache: {},
@@ -61,7 +62,13 @@
} = $props()
const flowGraphAssetsCtx = getContext<FlowGraphAssetContext | undefined>('FlowGraphAssetContext')
const { selectionManager } = getContext<FlowEditorContext>('FlowEditorContext') || {}
const { selectionManager, opWorkspace } = getContext<FlowEditorContext>('FlowEditorContext') || {}
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
// Expose the acting workspace to the asset explore controls (ExploreAssetButton
// reads it from this context; the DB manager / S3 picker act on it).
$effect(() => {
if (flowGraphAssetsCtx) flowGraphAssetsCtx.val.workspace = opWs
})
let selectedId = $derived(selectionManager?.getSelectedId())
let allModules = $derived(getAllModules(modules))
@@ -79,7 +86,7 @@
let truncatedPath = asset.path.split('?table=')[0]
if (truncatedPath in resMetadataCache) continue
resMetadataCache[truncatedPath] = undefined // avoid fetching multiple times because of async
ResourceService.getResource({ path: truncatedPath, workspace: $workspaceStore! })
ResourceService.getResource({ path: truncatedPath, workspace: opWs! })
.then((r) => (resMetadataCache[truncatedPath] = { resource_type: r.resource_type }))
.catch((err) => console.error("Couldn't fetch resource", truncatedPath, err))
}
@@ -88,7 +95,7 @@
// Fetch transitive assets (path scripts and flows)
$effect(() => {
if (!$workspaceStore || !flowGraphAssetsCtx || !enablePathScriptAndFlowAssets) return
if (!opWs || !flowGraphAssetsCtx || !enablePathScriptAndFlowAssets) return
let usages: { path: string; kind: AssetUsageKind }[] = []
let modIds: string[] = []
for (const mod of allModules) {
@@ -101,7 +108,7 @@
}
if (usages.length) {
AssetService.listAssetsByUsage({
workspace: $workspaceStore,
workspace: opWs,
requestBody: { usages }
}).then((result) => {
result.forEach((assets, idx) => {
@@ -182,6 +189,6 @@
</script>
{#if flowGraphAssetsCtx}
<S3FilePicker bind:this={flowGraphAssetsCtx.val.s3FilePicker} readOnlyMode />
<ResourceEditorDrawer bind:this={flowGraphAssetsCtx.val.resourceEditorDrawer} />
<S3FilePicker bind:this={flowGraphAssetsCtx.val.s3FilePicker} workspace={opWs} readOnlyMode />
<ResourceEditorDrawer bind:this={flowGraphAssetsCtx.val.resourceEditorDrawer} workspace={opWs} />
{/if}
@@ -9,6 +9,8 @@
import { Skeleton } from '$lib/components/common'
import Button from '../common/button/Button.svelte'
import { ArrowRight, Loader2, Pencil, X } from 'lucide-svelte'
import { getContext } from 'svelte'
import type { FlowEditorContext } from './types'
interface Props {
path: string
@@ -17,6 +19,10 @@
}
let { path, allowFork = false, onHistoryRestore }: Props = $props()
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
let loading: boolean = $state(false)
let versions: FlowVersion[] = $state([])
@@ -29,7 +35,7 @@
async function loadFlow(version: number) {
selected = await FlowService.getFlowVersion({
workspace: $workspaceStore!,
workspace: opWs!,
version
})
}
@@ -37,7 +43,7 @@
async function loadVersions() {
loading = true
versions = await FlowService.getFlowHistory({
workspace: $workspaceStore!,
workspace: opWs!,
path: path
})
loading = false
@@ -52,7 +58,7 @@
return
}
await FlowService.updateFlowHistory({
workspace: $workspaceStore!,
workspace: opWs!,
version,
requestBody: {
deployment_msg: deploymentMsgUpdate!
@@ -66,7 +72,7 @@
async function restoreVersion(flow: Flow | undefined) {
if (!flow) return
await FlowService.updateFlow({
workspace: $workspaceStore!,
workspace: opWs!,
requestBody: {
...flow,
path
@@ -16,7 +16,8 @@
import { ScriptService, type FlowModuleValue, type PathScript } from '$lib/gen'
import { hubBaseUrlStore, workspaceStore } from '$lib/stores'
import { Flag, Lock, RefreshCw, Unlock } from 'lucide-svelte'
import { createEventDispatcher, untrack } from 'svelte'
import { createEventDispatcher, getContext, untrack } from 'svelte'
import type { FlowEditorContext } from '../types'
import { twMerge } from 'tailwind-merge'
import { getToolNameError } from '$lib/components/graph/renderers/nodes/AIToolNode.svelte'
import { DEFAULT_HUB_BASE_URL, PRIVATE_HUB_MIN_VERSION } from '$lib/hub'
@@ -47,6 +48,9 @@
let latestHash: string | undefined = $state(undefined)
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
// Extract version_id from hub path (format: hub/{version_id}/{app}/{summary})
let hubVersionId = $derived(
flowModuleValue?.type === 'script' && flowModuleValue.path?.startsWith('hub/')
@@ -55,7 +59,7 @@
)
function getCachedKey(path: string) {
return `${$workspaceStore}-${path}`
return `${opWs}-${path}`
}
function getCachedValues(path: string) {
const key = getCachedKey(path)
@@ -68,7 +72,7 @@
async function loadLatestHash(value: PathScript) {
let script = await ScriptService.getScriptByPath({
workspace: $workspaceStore!,
workspace: opWs!,
path: value.path
})
const key = getCachedKey(value.path)
@@ -7,15 +7,19 @@
import { ExternalLink, Loader2 } from 'lucide-svelte'
import Button from '$lib/components/common/button/Button.svelte'
import { emptySchema, type StateStore } from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, getContext } from 'svelte'
import { fade } from 'svelte/transition'
import { initFlow } from '$lib/components/flows/flowStore.svelte'
import type { FlowState } from '$lib/components/flows/flowState'
import type { FlowEditorContext } from '../types'
let flowEditorDrawer: Drawer | undefined = $state()
const dispatch = createEventDispatcher()
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
export async function openDrawer(path: string, cb: () => void): Promise<void> {
flowPath = path
flow = undefined
@@ -25,7 +29,7 @@
try {
const backendFlow = await FlowService.getFlowByPath({
workspace: $workspaceStore!,
workspace: opWs!,
path
})
@@ -86,6 +90,7 @@
{flowStore}
{flowStateStore}
initialPath={flowPath}
autosaveWorkspace={opWs}
newFlow={false}
selectedId="settings-metadata"
loading={false}
@@ -32,7 +32,8 @@
let { noEditor }: Props = $props()
const { flowStore } = getContext<FlowEditorContext>('FlowEditorContext')
const { flowStore, opWorkspace } = getContext<FlowEditorContext>('FlowEditorContext')
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
if (!flowStore.val.value.flow_env) {
flowStore.val.value.flow_env = {}
@@ -244,8 +245,10 @@
Flow envs can be referenced in any flow step input using the syntax{' '}
<code>flow_env.VARIABLE_NAME</code> or <code>flow_env["VARIABLE_NAME"]</code>. These
variables are available in the property picker and can be used in JavaScript expressions and
input bindings. String values can link to workspace variables using the <DollarSign size={12}
class="inline" /> button. Resource type references workspace resources resolved at runtime.
input bindings. String values can link to workspace variables using the <DollarSign
size={12}
class="inline"
/> button. Resource type references workspace resources resolved at runtime.
</Alert>
{#if flowEnvEntries.length === 0}
@@ -303,6 +306,7 @@
<ResourcePicker
bind:value={resourcePaths[entry.key]}
disabled={noEditor}
workspace={opWs}
/>
{:else if entry.type === 'json'}
<div class="w-full">
@@ -320,8 +324,7 @@
<input
type="text"
value={entry.displayValue}
oninput={(e) =>
updateEnvValue(entry.key, e.currentTarget.value, 'string')}
oninput={(e) => updateEnvValue(entry.key, e.currentTarget.value, 'string')}
disabled={noEditor}
class="input w-full"
placeholder="Variable value"
@@ -346,8 +349,7 @@
Linked to variable <a
href="/variables#{entry.value.slice(5)}"
target="_blank"
class="text-accent underline font-normal"
>{entry.value.slice(5)}</a
class="text-accent underline font-normal">{entry.value.slice(5)}</a
>
</div>
{/if}
@@ -379,7 +381,7 @@
itemName="Variable"
extraField="path"
loadItems={async () =>
(await VariableService.listVariable({ workspace: $workspaceStore ?? '' })).map((x) => ({
(await VariableService.listVariable({ workspace: opWs ?? '' })).map((x) => ({
name: x.path,
...x
}))}
@@ -7,6 +7,7 @@
import JsonInputs from '$lib/components/JsonInputs.svelte'
import { convert } from '@redocly/json-to-json-schema'
import { sendUserToast } from '$lib/toast'
import { workspaceStore } from '$lib/stores'
import EditableSchemaForm from '$lib/components/EditableSchemaForm.svelte'
import AddPropertyV2 from '$lib/components/schema/AddPropertyV2.svelte'
import FlowInputViewer from '$lib/components/FlowInputViewer.svelte'
@@ -74,8 +75,11 @@
pathStore,
initialPathStore,
fakeInitialPath,
flowInputEditorState
flowInputEditorState,
opWorkspace
} = getContext<FlowEditorContext>('FlowEditorContext')
// Acting workspace when the flow editor runs in an AI session; else the nav workspace.
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
// Get diffManager from the graph
const diffManager = $derived(flowModuleSchemaMap?.getDiffManager())
@@ -887,6 +891,7 @@
>
<HistoricInputs
bind:this={historicInputs}
workspace={opWs}
runnableId={$initialPathStore ?? undefined}
runnableType={$pathStore ? 'FlowPath' : undefined}
on:select={(e) => {
@@ -910,6 +915,7 @@
<div class="h-full">
<CaptureTable
path={$initialPathStore || fakeInitialPath}
workspace={opWs}
on:select={(e) => {
updatePreviewSchemaAndArgs(e.detail ?? undefined)
}}
@@ -929,6 +935,7 @@
title="Saved inputs"
>
<SavedInputsPicker
workspace={opWs}
runnableId={$initialPathStore ?? undefined}
runnableType={$pathStore ? 'FlowPath' : undefined}
on:select={(e) => {
@@ -6,7 +6,8 @@
import { workspaceStore } from '$lib/stores'
import { emptyString } from '$lib/utils'
import { createEventDispatcher, untrack } from 'svelte'
import { createEventDispatcher, getContext, untrack } from 'svelte'
import type { FlowEditorContext } from '../types'
import { flip } from 'svelte/animate'
import { fade } from 'svelte/transition'
interface Props {
@@ -15,6 +16,9 @@
let { children }: Props = $props()
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
// export let failureModule: boolean
const dispatch = createEventDispatcher()
@@ -25,10 +29,10 @@
let ownerFilter: string | undefined = $state(undefined)
async function loadFlows() {
items = await FlowService.listFlows({ workspace: $workspaceStore!, withoutDescription: true })
items = await FlowService.listFlows({ workspace: opWs!, withoutDescription: true })
}
$effect(() => {
$workspaceStore && untrack(() => loadFlows())
opWs && untrack(() => loadFlows())
})
let prefilteredItems = $derived(
ownerFilter ? items?.filter((x) => x.path.startsWith(ownerFilter!)) : items
@@ -94,10 +94,12 @@
pathStore,
saveDraft,
customUi,
executionCount
executionCount,
opWorkspace
} = getContext<FlowEditorContext>('FlowEditorContext')
const selectedId = $derived(selectionManager.getSelectedId())
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
interface Props {
flowModule: FlowModule
@@ -398,7 +400,7 @@
let preparedSqlQueries = usePreparedAssetSqlQueries(
() => flowGraphAssetsCtx?.val.sqlQueries[selectedId],
() => $workspaceStore
() => opWs
)
// Debug mode state
@@ -532,16 +534,12 @@
resetDAPClient()
dapClient = getDAPClient(dapServerUrl)
const env = await fetchContextualVariables($workspaceStore ?? '')
const env = await fetchContextualVariables(opWs ?? '')
const code = flowModule.value.content
let signedPayload
try {
signedPayload = await signDebugRequest(
$workspaceStore ?? '',
code ?? '',
rawScriptLang ?? 'python3'
)
signedPayload = await signDebugRequest(opWs ?? '', code ?? '', rawScriptLang ?? 'python3')
debugSessionJobId = signedPayload.job_id
} catch (signError) {
sendUserToast(getDebugErrorMessage(signError), true)
@@ -759,14 +757,14 @@
on:toggleCache={() => selectAdvanced('cache')}
on:toggleStopAfterIf={() => selectAdvanced('early-stop')}
on:fork={async () => {
const [module, state] = await fork(flowModule)
const [module, state] = await fork(flowModule, opWs)
flowModule = module
flowStateStore.val[module.id] = state
}}
on:reload={async () => {
if (flowModule.value.type == 'script') {
if (flowModule.value.hash != undefined) {
flowModule.value.hash = await getLatestHashForScript(flowModule.value.path)
flowModule.value.hash = await getLatestHashForScript(flowModule.value.path, opWs)
}
forceReload++
await reload(flowModule)
@@ -781,7 +779,8 @@
flowModule,
selectedId,
flowStateStore.val[flowModule.id]?.schema,
$pathStore
$pathStore,
opWs
)
if (flowModule.value.type == 'rawscript') {
module.value.input_transforms = flowModule.value.input_transforms
@@ -797,6 +796,7 @@
<div class="shadow-sm px-1 border-b-1 border-gray-200 dark:border-gray-700">
<EditorBar
customUi={customUi?.editorBar}
workspace={opWs}
{validCode}
{editor}
lang={flowModule.value['language'] ?? 'deno'}
@@ -903,7 +903,7 @@
},
{}
)}
key={`flow-inline-${$workspaceStore}-${$pathStore}-${flowModule.id}`}
key={`flow-inline-${opWs}-${$pathStore}-${flowModule.id}`}
moduleId={flowModule.id}
preparedAssetsSqlQueries={preparedSqlQueries.current}
customTag={flowModule.value.tag}
@@ -915,7 +915,7 @@
client={dapClient}
currentFrameId={currentDebugFrameId}
onClose={() => (showDebugConsole = false)}
workspace={$workspaceStore}
workspace={opWs}
jobId={debugSessionJobId ?? undefined}
/>
</Pane>
@@ -966,7 +966,7 @@
},
{}
)}
key={`flow-inline-${$workspaceStore}-${$pathStore}-${flowModule.id}`}
key={`flow-inline-${opWs}-${$pathStore}-${flowModule.id}`}
moduleId={flowModule.id}
preparedAssetsSqlQueries={preparedSqlQueries.current}
customTag={flowModule.value.tag}
@@ -20,7 +20,8 @@
import { getScriptByPath, scriptLangToEditorLang } from '$lib/scripts'
import { workspaceStore } from '$lib/stores'
import { Loader2 } from 'lucide-svelte'
import { untrack } from 'svelte'
import { getContext, untrack } from 'svelte'
import type { FlowEditorContext } from '../types'
interface Props {
path: string
@@ -44,6 +45,9 @@
language = $bindable(undefined)
}: Props = $props()
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
let code: string | undefined = $state()
let previousCode: string | undefined = $state()
let lock: string | undefined = $state(undefined)
@@ -51,7 +55,7 @@
let notFound = $state(false)
function getCachedKey(path: string, hash: string | undefined) {
return `${$workspaceStore}-${path}-${hash ?? ''}`
return `${opWs}-${path}-${hash ?? ''}`
}
function getCachedValues(path: string, hash: string | undefined) {
const key = getCachedKey(path, hash)
@@ -64,12 +68,15 @@
notFound = cachedValues[key]?.notFound ?? false
}
getCachedValues(untrack(() => path), untrack(() => hash))
getCachedValues(
untrack(() => path),
untrack(() => hash)
)
async function loadPreviousCode(previousHash: string) {
try {
const previousScript = await ScriptService.getScriptByHash({
workspace: $workspaceStore!,
workspace: opWs!,
hash: previousHash
})
previousCode = previousScript.content
@@ -93,8 +100,8 @@
const script = path.startsWith('hub/')
? await getScriptByPath(path!)
: hash
? await ScriptService.getScriptByHash({ workspace: $workspaceStore!, hash })
: await getScriptByPath(path!)
? await ScriptService.getScriptByHash({ workspace: opWs!, hash })
: await getScriptByPath(path!, opWs)
code = script.content
language = script.language
@@ -133,7 +140,7 @@
<div class="text-xs text-primary mb-4">tag: {tag}</div>
{/if}
{#if notFound}
<div class="text-red-400">script not found at {path} in workspace {$workspaceStore}</div>
<div class="text-red-400">script not found at {path} in workspace {opWs}</div>
{:else if showAllCode}
{#if showDiff}
{#key (previousCode ?? '') + (code ?? '')}
@@ -18,7 +18,9 @@
import EditableSchemaDrawer from '$lib/components/schema/EditableSchemaDrawer.svelte'
import AddProperty from '$lib/components/schema/AddProperty.svelte'
const { selectionManager, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext')
const { selectionManager, flowStateStore, opWorkspace } =
getContext<FlowEditorContext>('FlowEditorContext')
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
const result = flowStateStore.val[selectionManager.getSelectedId()]?.previewResult ?? {}
let editor: SimpleEditor | undefined = $state(undefined)
@@ -37,7 +39,7 @@
let isSuspendEnabled = $derived(Boolean(flowModule.suspend))
async function loadGroups(): Promise<void> {
allUserGroups = await GroupService.listGroupNames({ workspace: $workspaceStore! })
allUserGroups = await GroupService.listGroupNames({ workspace: opWs! })
schema.properties['groups'] = {
type: 'array',
items: {
@@ -1,5 +1,5 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { run } from 'svelte/legacy'
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
import FlowGraphViewer from '$lib/components/FlowGraphViewer.svelte'
@@ -7,16 +7,20 @@
import { Triggers } from '$lib/components/triggers/triggers.svelte'
import { FlowService, type Flow, type TriggersCount } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { setContext } from 'svelte'
import { getContext, setContext } from 'svelte'
import type { FlowEditorContext } from '../types'
import { writable } from 'svelte/store'
interface Props {
path: string;
noSide?: boolean;
fillAvailableHeight?: boolean;
path: string
noSide?: boolean
fillAvailableHeight?: boolean
}
let { path, noSide = false, fillAvailableHeight = false }: Props = $props();
let { path, noSide = false, fillAvailableHeight = false }: Props = $props()
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
let flow: Flow | undefined = $state(undefined)
@@ -29,15 +33,13 @@
})
async function loadFlow(path: string) {
flow = await FlowService.getFlowByPath({ workspace: $workspaceStore!, path })
triggersCount.set(
await FlowService.getTriggersCountOfFlow({ workspace: $workspaceStore!, path })
)
flow = await FlowService.getFlowByPath({ workspace: opWs!, path })
triggersCount.set(await FlowService.getTriggersCountOfFlow({ workspace: opWs!, path }))
}
run(() => {
path && loadFlow(path)
});
})
</script>
<div class="flex flex-col flex-1 h-full overflow-auto">
@@ -2,6 +2,8 @@
import FlowExecutionStatus from '$lib/components/runs/FlowExecutionStatus.svelte'
import type { Job } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { getContext } from 'svelte'
import type { FlowEditorContext } from '../types'
import FlowCard from '../common/FlowCard.svelte'
import Button from '$lib/components/common/button/Button.svelte'
import type { StateStore } from '$lib/utils'
@@ -18,6 +20,9 @@
}
let { job, isOwner, suspendStatus, noEditor, onOpenDetails }: Props = $props()
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
</script>
<FlowCard {noEditor} title="Flow result">
@@ -39,7 +44,7 @@
{#if isOwner !== undefined && suspendStatus}
<FlowExecutionStatus
{job}
workspaceId={$workspaceStore}
workspaceId={opWs}
{isOwner}
innerModules={job?.flow_status?.modules}
{suspendStatus}
@@ -147,6 +147,7 @@
initialPath={$initialPathStore}
namePlaceholder="flow"
kind="flow"
workspaceOverride={opWorkspace?.()}
/>
{#if $initialPathStore && $pathStore && $pathStore !== $initialPathStore}
<Alert
@@ -453,7 +454,7 @@
/>
{#if flowStore.val.on_behalf_of_email && canPreserve}
&rarr; <OnBehalfOfSelector
targetWorkspace={$workspaceStore ?? ''}
targetWorkspace={opWorkspace?.() ?? $workspaceStore ?? ''}
targetValue={$savedOnBehalfOfEmail}
selected={onBehalfOfChoice}
onSelect={(choice, details) => {
@@ -12,7 +12,8 @@
import Path from '$lib/components/Path.svelte'
import { sendUserToast } from '$lib/toast'
import { sameTopDomainOrigin } from '$lib/cookies'
import { onDestroy } from 'svelte'
import { getContext, onDestroy } from 'svelte'
import type { FlowEditorContext } from '../types'
interface Props {
onConnected: (resourcePath: string, resourceName: string) => void
@@ -21,6 +22,9 @@
let { onConnected, onCancel }: Props = $props()
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
let serverUrl = $state('')
let discoveryResult = $state<DiscoverMcpOauthResponse | null>(null)
let selectedScopes = $state<string[]>([])
@@ -112,7 +116,7 @@
let accountId: number | undefined
if (data.expires_in && data.refresh_token) {
const accountIdStr = await OauthService.createAccount({
workspace: $workspaceStore!,
workspace: opWs!,
requestBody: {
refresh_token: data.refresh_token,
expires_in: data.expires_in,
@@ -124,7 +128,7 @@
}
await VariableService.createVariable({
workspace: $workspaceStore!,
workspace: opWs!,
requestBody: {
path: resourcePath,
value: data.access_token,
@@ -136,7 +140,7 @@
})
await ResourceService.createResource({
workspace: $workspaceStore!,
workspace: opWs!,
requestBody: {
resource_type: 'mcp',
path: resourcePath,
@@ -228,6 +232,7 @@
initialPath=""
namePlaceholder={resourceName}
kind="resource"
workspaceOverride={opWs}
/>
<Button size="sm" onClick={startOAuth} disabled={!resourcePath || pathError !== ''}>
@@ -26,9 +26,10 @@
import { safeSelectItems } from '$lib/components/select/utils.svelte'
import ResourcePicker from '$lib/components/ResourcePicker.svelte'
import { usePromise } from '$lib/svelte5Utils.svelte'
import { untrack } from 'svelte'
import { getContext, untrack } from 'svelte'
import Alert from '$lib/components/common/alert/Alert.svelte'
import McpOAuthConnect from './McpOAuthConnect.svelte'
import type { FlowEditorContext } from '../types'
interface Props {
tool: McpTool
@@ -36,6 +37,9 @@
let { tool = $bindable() }: Props = $props()
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
let showOAuthForm = $state(false)
let refreshCount = $state(0)
let resourcePicker: ResourcePicker | undefined = $state()
@@ -43,7 +47,7 @@
let tools = usePromise(
async () =>
await loadToolsCached({
workspace: $workspaceStore!,
workspace: opWs!,
path: tool.value.resource_path,
refreshCount
}),
@@ -56,7 +60,7 @@
$effect(() => {
resourcePath
$workspaceStore
opWs
refreshCount
untrack(() => {
if (resourcePath?.length > 0) {
@@ -105,7 +109,12 @@
<div class="w-full">
<Label label="MCP Resource">
<ResourcePicker bind:this={resourcePicker} resourceType="mcp" bind:value={tool.value.resource_path} />
<ResourcePicker
bind:this={resourcePicker}
resourceType="mcp"
bind:value={tool.value.resource_path}
workspace={opWs}
/>
</Label>
</div>
@@ -13,20 +13,24 @@
orderedJsonStringify,
sendUserToast
} from '$lib/utils'
import { createEventDispatcher } from 'svelte'
import { createEventDispatcher, getContext } from 'svelte'
import { fade } from 'svelte/transition'
import WorkerTagSelect from '$lib/components/WorkerTagSelect.svelte'
import type { FlowEditorContext } from '../types'
let scriptEditorDrawer: Drawer | undefined = $state()
const dispatch = createEventDispatcher()
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
export async function openDrawer(hash: string, cb: () => void): Promise<void> {
script = undefined
closeAnyway = false
scriptEditorDrawer?.openDrawer?.()
script = await ScriptService.getScriptByHash({
workspace: $workspaceStore!,
workspace: opWs!,
hash
})
savedScript = structuredClone($state.snapshot(script))
@@ -89,7 +93,7 @@
}
await ScriptService.createScript({
workspace: $workspaceStore!,
workspace: opWs!,
requestBody: {
...script,
language: script.language!,
@@ -200,6 +204,7 @@
{#if script && displayEditor}
{#key script.hash}
<ScriptEditor
workspaceOverride={opWs}
showCaptures={false}
on:saveDraft={() => {
saveScript()
@@ -216,7 +221,10 @@
>
{#snippet editorBarRight()}
<div>
<WorkerTagSelect bind:tag={() => script?.tag, (v) => script && (script.tag = v)} />
<WorkerTagSelect
bind:tag={() => script?.tag, (v) => script && (script.tag = v)}
workspaceId={opWs}
/>
</div>
{/snippet}
</ScriptEditor>
@@ -3,7 +3,8 @@
import { createFlowChatManager } from './FlowChatManager.svelte'
import FlowConversationsSidebar from './FlowConversationsSidebar.svelte'
import FlowChatInterface from './FlowChatInterface.svelte'
import { untrack } from 'svelte'
import { getContext, untrack } from 'svelte'
import type { FlowEditorContext } from '../types'
interface Props {
onRunFlow: (
@@ -24,10 +25,13 @@
useStreaming = false,
path,
hideSidebar = false,
inputSchema = undefined,
inputSchema = undefined
}: Props = $props()
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
const manager = createFlowChatManager()
manager.operatingWorkspace = () => flowEditorContext?.opWorkspace?.()
// Initialize manager when component mounts
$effect(() => {
@@ -52,9 +56,7 @@
// Derive additional inputs schema (excluding user_message) for chat mode
const additionalInputsSchema = $derived.by(() => {
const props = inputSchema?.properties ?? {}
const filtered = Object.fromEntries(
Object.entries(props).filter(([k]) => k !== 'user_message')
)
const filtered = Object.fromEntries(Object.entries(props).filter(([k]) => k !== 'user_message'))
if (Object.keys(filtered).length === 0) return undefined
const required = inputSchema?.required
const requiredArray: string[] = Array.isArray(required) ? required : []
@@ -93,6 +93,7 @@
schema={additionalInputsSchema}
bind:args={additionalInputsValues}
helperScript={dynamicInputHelperScript}
workspace={manager.operatingWorkspace?.()}
/>
{#snippet actions()}
<Button onClick={handleModalConfirm} variant="accent">Save</Button>
@@ -53,6 +53,15 @@ export class FlowChatManager {
#useStreaming = $state(false)
#path = $state<string | undefined>(undefined)
// When the flow editor runs as an AI-session live editor, it acts on a workspace
// that can differ from the nav store. FlowChat.svelte wires this to
// FlowEditorContext.opWorkspace so workspace-scoped calls hit the acting workspace.
operatingWorkspace?: () => string | undefined
#workspace(): string | undefined {
return this.operatingWorkspace?.() ?? get(workspaceStore)
}
initialize(
onRunFlow: (
userMessage: string,
@@ -112,7 +121,7 @@ export class FlowChatManager {
// Create a new conversation object and add it to the top of the list
const newConversation: ConversationWithDraft = {
id: newConversationId,
workspace_id: get(workspaceStore)!,
workspace_id: this.#workspace()!,
flow_path: this.#path!,
title: 'New chat',
created_at: new Date().toISOString(),
@@ -160,7 +169,7 @@ export class FlowChatManager {
try {
this.deletingConversationId = conversationId
await FlowConversationsService.deleteFlowConversation({
workspace: get(workspaceStore)!,
workspace: this.#workspace()!,
conversationId
})
if (this.selectedConversationId === conversationId) {
@@ -178,14 +187,14 @@ export class FlowChatManager {
}
async cancelCurrentJob() {
if (!get(workspaceStore)) {
if (!this.#workspace()) {
return
}
try {
if (this.currentJobId) {
await JobService.cancelQueuedJob({
workspace: get(workspaceStore)!,
workspace: this.#workspace()!,
id: this.currentJobId,
requestBody: {}
})
@@ -206,11 +215,11 @@ export class FlowChatManager {
// Only used by InfiniteList
private async loadConversations(page: number, perPage: number) {
if (!get(workspaceStore) || !this.#path) return []
if (!this.#workspace() || !this.#path) return []
try {
const response = await FlowConversationsService.listFlowConversations({
workspace: get(workspaceStore)!,
workspace: this.#workspace()!,
flowPath: this.#path,
page: page,
perPage: perPage
@@ -227,7 +236,7 @@ export class FlowChatManager {
// Message loading
private async loadMessages(reset: boolean, conversationId?: string) {
let conversationIdToUse = conversationId ?? this.selectedConversationId
if (!get(workspaceStore) || !conversationIdToUse) return
if (!this.#workspace() || !conversationIdToUse) return
if (reset) {
if (this.#conversationsCache[conversationIdToUse]) {
@@ -245,7 +254,7 @@ export class FlowChatManager {
const previousScrollHeight = this.messagesContainer?.scrollHeight || 0
const response = await FlowConversationsService.listConversationMessages({
workspace: get(workspaceStore)!,
workspace: this.#workspace()!,
conversationId: conversationIdToUse,
page: pageToFetch,
perPage: this.#perPage
@@ -318,7 +327,7 @@ export class FlowChatManager {
// Polling
private async pollJobResult(jobId: string) {
try {
await waitJob(jobId)
await waitJob(jobId, this.#workspace())
} catch (error) {
console.error('Error polling job result:', error)
} finally {
@@ -338,12 +347,12 @@ export class FlowChatManager {
conversationId: string,
options?: { isNewConversation?: boolean; removeTempMessages?: boolean }
) {
if (!get(workspaceStore)) return
if (!this.#workspace()) return
try {
const lastSeq = this.getLastPersistedMessageSeq()
const response = await FlowConversationsService.listConversationMessages({
workspace: get(workspaceStore)!,
workspace: this.#workspace()!,
conversationId: conversationId,
page: 1,
perPage: 50,
@@ -488,7 +497,7 @@ export class FlowChatManager {
}
// Build the EventSource URL
const streamUrl = `/api/w/${get(workspaceStore)}/jobs_u/getupdate_sse/${jobId}`
const streamUrl = `/api/w/${this.#workspace()}/jobs_u/getupdate_sse/${jobId}`
const url = new URL(streamUrl, window.location.origin)
url.searchParams.set('poll_delay_ms', '50')
url.searchParams.set('fast', 'true')
@@ -213,14 +213,17 @@ export async function createFlow(id: string): Promise<[FlowModule, FlowModuleSta
}
export async function fork(
flowModule: FlowModule
flowModule: FlowModule,
// The acting workspace when the flow editor runs in an AI session; else the nav workspace.
workspace?: string
): Promise<[FlowModule & { value: RawScript }, FlowModuleState]> {
if (flowModule.value.type !== 'script') {
throw new Error('Can only fork a script module')
}
const forkedFlowModule = await createInlineScriptModuleFromPath(
flowModule.value.path ?? '',
flowModule.id
flowModule.id,
workspace
)
const flowModuleState = await loadFlowModuleState(forkedFlowModule)
return [forkedFlowModule, flowModuleState]
@@ -228,9 +231,10 @@ export async function fork(
async function createInlineScriptModuleFromPath(
path: string,
id: string
id: string,
workspace?: string
): Promise<FlowModule & { value: RawScript }> {
const { content, language } = await getScriptByPath(path)
const { content, language } = await getScriptByPath(path, workspace)
return {
id,
@@ -255,7 +259,10 @@ export async function createScriptFromInlineScript(
flowModule: FlowModule,
suffix: string,
schema: Schema | undefined,
flowPath: string
flowPath: string,
// The session's acting workspace when the flow editor runs in an AI session;
// falls back to the navigation workspace outside a session.
workspace?: string
): Promise<[FlowModule & { value: PathScript }, FlowModuleState]> {
const user = get(userStore)
@@ -275,10 +282,10 @@ export async function createScriptFromInlineScript(
const forkedDescription = wasForked ? `as a fork of ${originalScriptPath}` : ''
const description = `This script was edited in place of flow ${flowPath} ${forkedDescription} by ${user?.username}.`
const availablePath = await findNextAvailablePath(path)
const availablePath = await findNextAvailablePath(path, workspace)
const hash = await ScriptService.createScript({
workspace: get(workspaceStore)!,
workspace: workspace ?? get(workspaceStore)!,
requestBody: {
path: availablePath,
summary: flowModule.summary ?? '',
@@ -60,10 +60,7 @@
GroupedModulesProxy,
type ExtendedOpenFlow
} from '$lib/components/graph/groupedModulesProxy.svelte'
import {
GroupDisplayState,
type FlowGroup
} from '$lib/components/graph/groupEditor.svelte'
import { GroupDisplayState, type FlowGroup } from '$lib/components/graph/groupEditor.svelte'
import {
type FlowStructureNode,
matchStructureNode,
@@ -135,9 +132,11 @@
flowHasChanged
}: Props = $props()
const { customUi, selectionManager, history, flowStateStore, flowStore, pathStore } =
const { customUi, selectionManager, history, flowStateStore, flowStore, pathStore, opWorkspace } =
getContext<FlowEditorContext>('FlowEditorContext')
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
const moveManager = new MoveManager()
const { triggersCount, triggersState } = getContext<TriggerContext>('TriggerContext')
@@ -460,7 +459,7 @@
}
}
const previousJobId = await JobService.listCompletedJobs({
workspace: $workspaceStore!,
workspace: opWs!,
scriptPathExact: path,
jobKinds: ['preview', 'script', 'flowpreview', 'flow'].join(','),
page: 1,
@@ -468,7 +467,7 @@
})
if (previousJobId.length > 0) {
const getJobResult = await JobService.getCompletedJobResultMaybe({
workspace: $workspaceStore!,
workspace: opWs!,
id: previousJobId[0].id
})
if ('result' in getJobResult) {
@@ -1,6 +1,7 @@
<script lang="ts">
import { workspaceStore } from '$lib/stores'
import { createEventDispatcher, untrack } from 'svelte'
import { createEventDispatcher, getContext, untrack } from 'svelte'
import type { FlowEditorContext } from '../types'
import { ScriptService } from '$lib/gen'
import SearchItems from '$lib/components/SearchItems.svelte'
import { Badge, Skeleton } from '$lib/components/common'
@@ -21,6 +22,9 @@
let items = $state(undefined) as Item[] | undefined
let filteredItems = $state(undefined) as (Item & { marked?: string })[] | undefined
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
interface Props {
kind?: 'script' | 'trigger' | 'approval' | 'failure'
isTemplate?: boolean | undefined
@@ -39,7 +43,7 @@
async function loadItems(): Promise<void> {
items = await ScriptService.listScripts({
workspace: $workspaceStore!,
workspace: opWs!,
kinds: kind,
isTemplate,
withoutDescription: true
@@ -37,7 +37,8 @@
<script lang="ts">
import { workspaceStore } from '$lib/stores'
import { createEventDispatcher, untrack } from 'svelte'
import { createEventDispatcher, getContext, untrack } from 'svelte'
import type { FlowEditorContext } from '../types'
import { FlowService, ScriptService } from '$lib/gen'
import SearchItems from '$lib/components/SearchItems.svelte'
import { Skeleton } from '$lib/components/common'
@@ -57,9 +58,11 @@
hash?: string
}
const flowEditorContext = getContext<FlowEditorContext>('FlowEditorContext')
let opWs = $derived(flowEditorContext?.opWorkspace?.() ?? $workspaceStore)
let items = usePromise(
async () =>
await loadItemsCached({ workspace: $workspaceStore!, kind, isTemplate, refreshCount }),
async () => await loadItemsCached({ workspace: opWs!, kind, isTemplate, refreshCount }),
{ loadInit: false, clearValueOnRefresh: false }
)
@@ -39,7 +39,9 @@
noHistory = undefined
}: Props = $props()
const { pathStore, flowStateStore } = getContext<FlowEditorContext>('FlowEditorContext') ?? {}
const { pathStore, flowStateStore, opWorkspace } =
getContext<FlowEditorContext>('FlowEditorContext') ?? {}
let opWs = $derived(opWorkspace?.() ?? $workspaceStore)
const dispatch = createEventDispatcher()
let infiniteList: InfiniteList | undefined = $state(undefined)
@@ -51,7 +53,7 @@
loadInputsPageFn = async (page: number, perPage: number) => {
if (staticInputs) return staticInputs
const previousJobs = await JobService.listCompletedJobs({
workspace: $workspaceStore!,
workspace: opWs!,
scriptPathExact: path === '' ? $pathStore + '/' + moduleId : path,
jobKinds: ['preview', 'script', 'flowpreview', 'flow', 'flowscript'].join(','),
page,
@@ -77,7 +79,7 @@
async function getJobResultAndLogs(jobId: string, noLogs: boolean) {
try {
const job = await JobService.getJob({
workspace: $workspaceStore ?? '',
workspace: opWs ?? '',
id: jobId ?? '',
noLogs
})
@@ -106,6 +106,10 @@ export type FlowEditorContext = {
export type FlowGraphAssetContext = StateStore<{
selectedAsset: Asset | undefined
// The workspace asset explore controls (DB manager, S3/volume picker, resource
// editor) act on — the flow's acting workspace in an AI session, else the nav
// workspace. Set by FlowAssetsHandler.
workspace: string | undefined
s3FilePicker: S3FilePicker | undefined
resourceEditorDrawer: ResourceEditorDrawer | undefined
// Maps resource paths to their metadata. undefined is for error
@@ -185,11 +185,14 @@ export async function runFlowPreview(
path: string,
restartedFrom: RestartedFrom | undefined,
conversationId?: string | undefined,
tempScriptRefs?: Record<string, string>
tempScriptRefs?: Record<string, string>,
// The session's acting workspace when previewing inside an AI-session flow
// editor; falls back to the navigation workspace for full-page previews.
workspace?: string
) {
const newFlow = flow
return await JobService.runFlowPreview({
workspace: get(workspaceStore) ?? '',
workspace: workspace ?? get(workspaceStore) ?? '',
requestBody: {
args,
value: newFlow.value,
@@ -287,6 +287,7 @@
noText
buttonVariant="accent"
s3FilePicker={flowGraphAssetsCtx?.val.s3FilePicker}
workspace={flowGraphAssetsCtx?.val.workspace}
_resourceMetadata={cachedResourceMetadata}
/>
</div>
@@ -10,6 +10,10 @@
toSchemaItems
} from './datatableUtils.svelte'
import { Button } from '../common'
import { getRawAppOperatingWorkspace } from './rawAppWorkspace'
const getOpWs = getRawAppOperatingWorkspace()
let opWs = $derived(getOpWs?.() ?? $workspaceStore)
interface Props {
/** Currently selected datatable */
@@ -30,8 +34,11 @@
}: Props = $props()
// Load available datatables and schemas using shared utilities
const datatables = createDatatablesResource(() => $workspaceStore)
const schemas = createSchemasResource(() => datatable)
const datatables = createDatatablesResource(() => opWs)
const schemas = createSchemasResource(
() => datatable,
() => opWs
)
const datatableItems = $derived(toDatatableItems(datatables.current))
const schemaItems = $derived(toSchemaItems(schemas.current))
@@ -49,47 +56,43 @@
<Popover>
{#snippet trigger()}
<Button
title="Configure default datatable & schema"
unifiedSize="xs"
variant="subtle"
nonCaptureEvent
btnClasses="px-1"
>
<Settings size={12} />
</Button>
<Button
title="Configure default datatable & schema"
unifiedSize="xs"
variant="subtle"
nonCaptureEvent
btnClasses="px-1"
>
<Settings size={12} />
</Button>
{/snippet}
{#snippet content()}
<div class="flex flex-col gap-3 p-4 min-w-64 max-w-80">
<div class="text-xs font-medium text-primary">Default Datatable & Schema</div>
<div class="flex flex-col gap-3 p-4 min-w-64 max-w-80">
<div class="text-xs font-medium text-primary">Default Datatable & Schema</div>
<p class="text-2xs text-tertiary leading-relaxed">
{description}
</p>
<p class="text-2xs text-tertiary leading-relaxed">
{description}
</p>
<div class="flex flex-col gap-1">
<span class="text-2xs text-tertiary">Database</span>
<Select
items={datatableItems}
bind:value={() => datatable, (v) => onChange?.(v, schema)}
placeholder="Select database"
size="sm"
/>
</div>
<div class="flex flex-col gap-1">
<span class="text-2xs text-tertiary">Schema</span>
<Select
items={schemaItems}
bind:value={() => schema ?? '', (v) => onChange?.(datatable, v || undefined)}
placeholder="public"
size="sm"
/>
</div>
<div class="flex flex-col gap-1">
<span class="text-2xs text-tertiary">Database</span>
<Select
items={datatableItems}
bind:value={() => datatable, (v) => onChange?.(v, schema)}
placeholder="Select database"
size="sm"
/>
</div>
<div class="flex flex-col gap-1">
<span class="text-2xs text-tertiary">Schema</span>
<Select
items={schemaItems}
bind:value={() => schema ?? '', (v) => onChange?.(datatable, v || undefined)}
placeholder="public"
size="sm"
/>
</div>
</div>
{/snippet}
</Popover>
@@ -12,6 +12,10 @@
import DBManagerContent from '../DBManagerContent.svelte'
import type { DbInput } from '../dbTypes'
import type { SelectedTable } from '../DBManager.svelte'
import { getRawAppOperatingWorkspace } from './rawAppWorkspace'
const getOpWs = getRawAppOperatingWorkspace()
let opWs = $derived(getOpWs?.() ?? $workspaceStore)
interface Props {
onAdd?: (ref: DataTableRef) => void
@@ -40,11 +44,9 @@
// Load available datatables from workspace
const datatables = resource<string[]>([], async () => {
if (!$workspaceStore) return []
if (!opWs) return []
try {
return (await WorkspaceService.listDataTables({ workspace: $workspaceStore })).map(
(d) => d.name
)
return (await WorkspaceService.listDataTables({ workspace: opWs })).map((d) => d.name)
} catch (e) {
console.error('Failed to load datatables:', e)
return []
@@ -163,11 +165,12 @@
CloseIcon={hasReplResult ? ArrowLeft : undefined}
noPadding
>
{#if dbInput && $workspaceStore}
{#if dbInput && opWs}
{#key selectedDatatable}
<DBManagerContent
bind:this={dbManagerContent}
input={dbInput}
workspace={opWs}
bind:hasReplResult
bind:selectedSchemaKey
bind:selectedTableKey
@@ -14,6 +14,7 @@
// import { addWmillClient } from './utils'
import RawAppBackgroundRunner from './RawAppBackgroundRunner.svelte'
import { workspaceStore } from '$lib/stores'
import { setRawAppOperatingWorkspace } from './rawAppWorkspace'
import { useLocalStorageValue } from '$lib/svelte5Utils.svelte'
import {
genWmillTs,
@@ -175,6 +176,9 @@
// embedded in a session preview (autosaveWorkspace), else the navigation
// workspace. Deploy/save/background-runner must target it, not $workspaceStore.
const opWorkspace = $derived(autosaveWorkspace ?? $workspaceStore)
// Expose it to the sidebar sub-components (inline scripts, datatable/shared-UI
// drawers, DB selector) so their lookups target the app's workspace too.
setRawAppOperatingWorkspace(() => opWorkspace)
// Convert to object format for child components
let dataTableRefsObjects = $derived(data.tables.map(parseDataTableRef))
@@ -988,6 +992,7 @@
// Clear the cached schema so it gets refreshed with the new table
const resourcePath = `datatable://${datatableName}`
delete $dbSchemas[resourcePath]
delete $dbSchemas[`${opWorkspace}:${resourcePath}`]
}
}
@@ -709,6 +709,7 @@
{onLatest}
{savedApp}
rawApp
operatingWorkspace={opWorkspace}
bind:summary
bind:customPath
bind:deploymentMsg
@@ -24,6 +24,7 @@
import { usePreparedAssetSqlQueries } from '$lib/infer.svelte'
import AssetsDropdownButton from '../assets/AssetsDropdownButton.svelte'
import { workspaceStore } from '$lib/stores'
import { getRawAppOperatingWorkspace } from './rawAppWorkspace'
import { SvelteSet } from 'svelte/reactivity'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { editor as meditor } from 'monaco-editor'
@@ -76,6 +77,10 @@
onSelectionChange,
delete_after_secs = $bindable()
}: Props = $props()
const getOpWs = getRawAppOperatingWorkspace()
let opWs = $derived(getOpWs?.() ?? $workspaceStore)
let diffEditor = $state() as DiffEditor | undefined
let validCode = $state(true)
@@ -145,7 +150,7 @@
)
let preparedSqlQueries = usePreparedAssetSqlQueries(
() => inferAssetsRes.current?.sql_queries,
() => $workspaceStore
() => opWs
)
$effect(() => {
if (!inlineScript || !inferAssetsRes.current || inferAssetsRes.current.status === 'error')
@@ -303,13 +308,13 @@
resetDAPClient()
dapClient = getDAPClient(dapServerUrl)
const env = await fetchContextualVariables($workspaceStore ?? '')
const env = await fetchContextualVariables(opWs ?? '')
const code = inlineScript.content
let signedPayload
try {
signedPayload = await signDebugRequest(
$workspaceStore ?? '',
opWs ?? '',
code ?? '',
inlineScript.language ?? 'python3'
)
@@ -636,6 +641,7 @@
<div class="shadow-sm px-1 border-b-1 border-gray-200 dark:border-gray-700">
<EditorBar
workspace={opWs}
{validCode}
{editor}
lang={inlineScript.language}
@@ -733,7 +739,7 @@
client={dapClient}
currentFrameId={currentDebugFrameId}
onClose={() => (showDebugConsole = false)}
workspace={$workspaceStore}
workspace={opWs}
jobId={debugSessionJobId ?? undefined}
/>
</Pane>
@@ -29,6 +29,10 @@
import { userStore, workspaceStore } from '$lib/stores'
import { isHubFlowPath } from '$lib/utils'
import { sendUserToast } from '$lib/toast'
import { getRawAppOperatingWorkspace } from './rawAppWorkspace'
const getOpWs = getRawAppOperatingWorkspace()
let opWs = $derived(getOpWs?.() ?? $workspaceStore)
type RunnableWithInlineScript = RunnableWithFields & {
inlineScript?: InlineScript & { language: ScriptLang }
@@ -131,7 +135,7 @@
case 'groups':
return $userStore?.groups ?? []
case 'workspace':
return $workspaceStore ?? ''
return opWs ?? ''
case 'author':
return $userStore?.email ?? '' // In editor, author is the current user
default:
@@ -188,6 +192,7 @@
<JobLoader
noCode={true}
workspaceOverride={opWs}
bind:scriptProgress
bind:this={jobLoader}
bind:isLoading={testIsLoading}
@@ -299,6 +304,7 @@
</div>
<SchemaForm
on:keydownCmdEnter={testPreview}
workspace={opWs}
disabledArgs={Object.entries(runnable?.fields ?? {})
.filter(([_, v]) => v.type == 'static')
.map(([k]) => k)}
@@ -2,6 +2,10 @@
import { workspaceStore } from '$lib/stores'
import RawAppInlineScripRunnable, { type Runnable } from './RawAppInlineScriptRunnable.svelte'
import { createScriptFromInlineScript } from '../apps/editor/inlineScriptsPanel/utils'
import { getRawAppOperatingWorkspace } from './rawAppWorkspace'
const getOpWs = getRawAppOperatingWorkspace()
let opWs = $derived(getOpWs?.() ?? $workspaceStore)
interface Props {
runnables: Record<string, Runnable>
@@ -36,12 +40,7 @@
<RawAppInlineScripRunnable
{appPath}
on:createScriptFromInlineScript={(e) => {
createScriptFromInlineScript(
selectedRunnable ?? '',
e.detail,
$workspaceStore ?? '',
appPath
)
createScriptFromInlineScript(selectedRunnable ?? '', e.detail, opWs ?? '', appPath)
}}
on:delete={() => {
if (selectedRunnable) {
@@ -14,6 +14,10 @@
import type { InputType } from '../apps/inputType'
import Select from '$lib/components/select/Select.svelte'
import { userStore, workspaceStore } from '$lib/stores'
import { getRawAppOperatingWorkspace } from './rawAppWorkspace'
const getOpWs = getRawAppOperatingWorkspace()
let opWs = $derived(getOpWs?.() ?? $workspaceStore)
// Build ctx properties with current user's actual values
let ctxProperties = $derived([
@@ -31,7 +35,7 @@
{
value: 'workspace',
label: 'Workspace',
subtitle: `string — "${$workspaceStore ?? 'unknown'}"`
subtitle: `string — "${opWs ?? 'unknown'}"`
},
{ value: 'author', label: 'Author', subtitle: `string — "${$userStore?.email ?? 'unknown'}"` }
])
@@ -6,6 +6,10 @@
import DrawerContent from '../common/drawer/DrawerContent.svelte'
import Editor from '$lib/components/Editor.svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { getRawAppOperatingWorkspace } from './rawAppWorkspace'
const getOpWs = getRawAppOperatingWorkspace()
let opWs = $derived(getOpWs?.() ?? $workspaceStore)
let open = $state(false)
let files: Record<string, string> = $state({})
@@ -20,10 +24,10 @@
}
async function load() {
if (!$workspaceStore) return
if (!opWs) return
loading = true
try {
const res = (await WorkspaceService.getSharedUi({ workspace: $workspaceStore })) as any
const res = (await WorkspaceService.getSharedUi({ workspace: opWs })) as any
files = res.files ?? {}
version = res.version ?? 0
editedBy = res.edited_by ?? ''
@@ -3,6 +3,7 @@
import type { Policy } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { sendUserToast } from '$lib/toast'
import { getRawAppOperatingWorkspace } from './rawAppWorkspace'
import Modal from '$lib/components/common/modal/Modal.svelte'
import Button from '$lib/components/common/button/Button.svelte'
import TextInput from '$lib/components/text_input/TextInput.svelte'
@@ -60,8 +61,14 @@
let preWhitelistedTables = $state<DataTableRef[]>([])
let dataTableDrawer: RawAppDataTableDrawer | undefined = $state()
const datatables = createDatatablesResource(() => $workspaceStore)
const schemas = createSchemasResource(() => selectedDatatable)
const getOpWs = getRawAppOperatingWorkspace()
let opWs = $derived(getOpWs?.() ?? $workspaceStore)
const datatables = createDatatablesResource(() => opWs)
const schemas = createSchemasResource(
() => selectedDatatable,
() => opWs
)
const availableDatatables = $derived(datatables.current)
const availableSchemas = $derived(schemas.current)
@@ -115,11 +122,11 @@
async function start(withPrompt: boolean) {
const template = templates[selectedTemplateIndex]
if (schemaMode === 'new' && newSchemaName && selectedDatatable && $workspaceStore) {
if (schemaMode === 'new' && newSchemaName && selectedDatatable && opWs) {
try {
const { dbSchemaOpsWithPreviewScripts } = await import('$lib/components/dbOps')
const dbOps = dbSchemaOpsWithPreviewScripts({
workspace: $workspaceStore,
workspace: opWs,
input: {
type: 'database',
resourceType: 'postgresql',
@@ -25,22 +25,29 @@ export function createDatatablesResource(getWorkspace: () => string | undefined)
* Creates a resource that loads schemas for a given datatable.
* The getDatatable getter is used as a reactive dependency - when it changes, schemas are refetched.
*/
export function createSchemasResource(getDatatable: () => string | undefined) {
return resource<string[]>([() => getDatatable() ?? ''], async () => {
export function createSchemasResource(
getDatatable: () => string | undefined,
getWorkspace: () => string | undefined = () => get(workspaceStore)
) {
return resource<string[]>([() => getDatatable() ?? '', () => getWorkspace() ?? ''], async () => {
const datatable = getDatatable()
const workspace = get(workspaceStore)
const workspace = getWorkspace()
if (!datatable || !workspace) return []
const resourcePath = `datatable://${datatable}`
// Key the schema cache by workspace too: a datatable of the same name can
// exist in both the nav and the acting workspace, so `datatable://<name>`
// alone would let one workspace's schema be reused for the other.
const cacheKey = `${workspace}:${resourcePath}`
const schemas = get(dbSchemas)
let dbSchema = schemas[resourcePath]
let dbSchema = schemas[cacheKey]
if (!dbSchema) {
try {
schemas[resourcePath] = await getDbSchemas('postgresql', resourcePath, workspace, (msg) =>
schemas[cacheKey] = await getDbSchemas('postgresql', resourcePath, workspace, (msg) =>
console.error('Schema error:', msg)
)
dbSchema = get(dbSchemas)[resourcePath]
dbSchema = get(dbSchemas)[cacheKey]
} catch (e) {
console.error(`Failed to load schema for ${datatable}:`, e)
return []
@@ -0,0 +1,22 @@
import { getContext, setContext } from 'svelte'
// The workspace a raw-app editor operates on. In a session preview this is the
// session's acting workspace, which differs from the navigation `$workspaceStore`
// (a session deliberately leaves the nav store on the workspace the top nav
// points at). RawAppEditor provides it once; the sidebar sub-components (inline
// scripts, datatable/shared-UI drawers, DB selector, …) read it so their lookups
// target the workspace the app actually lives in rather than the nav workspace.
//
// A getter (not a value) so the live `$derived` opWorkspace is read reactively at
// each call site. Consumers fall back to `$workspaceStore` when unset — e.g. the
// full-page app editor, where the nav workspace IS the operating workspace.
const KEY = 'RawAppOperatingWorkspace'
export function setRawAppOperatingWorkspace(get: () => string | undefined): void {
setContext(KEY, get)
}
export function getRawAppOperatingWorkspace(): (() => string | undefined) | undefined {
return getContext(KEY)
}
@@ -11,11 +11,14 @@ import { generateRandomString } from '$lib/utils'
*/
export async function processSecretArgs(
args: Record<string, any>,
schema: Schema | undefined
schema: Schema | undefined,
// Workspace the ephemeral secret variable is created in — must match the
// workspace the preview job runs in, else $jsonvar: resolves to a missing var.
forceWorkspace?: string
): Promise<Record<string, any>> {
if (!schema?.properties) return args
const workspace = get(workspaceStore)
const workspace = forceWorkspace ?? get(workspaceStore)
const user = get(userStore)
if (!workspace || !user) return args
@@ -33,6 +33,10 @@
limitPayloadSize?: boolean
noBorder?: boolean
captureActiveIndicator?: boolean | undefined
// Workspace to scope capture list/get/delete calls to. Defaults to the nav
// `$workspaceStore`; an AI-session live editor passes the session's acting
// workspace (a fork) so captures hit the right workspace.
workspace?: string
}
let {
@@ -47,9 +51,12 @@
fullHeight = true,
limitPayloadSize = false,
noBorder = false,
captureActiveIndicator = undefined
captureActiveIndicator = undefined,
workspace = undefined
}: Props = $props()
let ws = $derived(workspace ?? $workspaceStore)
let selected: number | undefined = $state(undefined)
let testKind: 'preprocessor' | 'main' = $state('main')
let isEmpty: boolean = $state(true)
@@ -94,7 +101,7 @@
function initLoadCaptures(kind: 'preprocessor' | 'main' = testKind) {
const loadInputsPageFn = async (page: number, perPage: number) => {
const captures = await CaptureService.listCaptures({
workspace: $workspaceStore!,
workspace: ws!,
runnableKind: isFlow ? 'flow' : 'script',
path: path ?? '',
triggerKind: captureType,
@@ -114,7 +121,7 @@
payloadData: 'Too big to display here, select to view',
getFullCapture: () =>
CaptureService.getCapture({
workspace: $workspaceStore!,
workspace: ws!,
id: capture.id
})
}
@@ -148,7 +155,7 @@
const deleteInputFn = async (id: any) => {
await CaptureService.deleteCapture({
workspace: $workspaceStore!,
workspace: ws!,
id
})
}
+3 -3
View File
@@ -2,10 +2,10 @@ import { get } from 'svelte/store'
import { ScriptService } from './gen'
import { workspaceStore } from './stores'
export async function findNextAvailablePath(path: string): Promise<string> {
export async function findNextAvailablePath(path: string, workspace?: string): Promise<string> {
try {
await ScriptService.getScriptByPath({
workspace: get(workspaceStore)!,
workspace: workspace ?? get(workspaceStore)!,
path
})
@@ -17,7 +17,7 @@ export async function findNextAvailablePath(path: string): Promise<string> {
path = `${path}_${Number(version) + 1}`
return findNextAvailablePath(path)
return findNextAvailablePath(path, workspace)
} catch (e) {
// Catching an error means the path is available
return path
+9 -4
View File
@@ -189,7 +189,12 @@ export function processLangs(selected: string | undefined, langs: string[]): str
export const defaultScriptLanguages = Object.fromEntries(scriptLanguagesArray)
export async function getScriptByPath(path: string): Promise<{
export async function getScriptByPath(
path: string,
// The acting workspace when called from a session live editor; defaults to
// the navigation workspace for full-page callers.
workspace?: string
): Promise<{
content: string
language: SupportedLanguage
schema: any
@@ -216,7 +221,7 @@ export async function getScriptByPath(path: string): Promise<{
}
} else {
const script = await ScriptService.getScriptByPath({
workspace: get(workspaceStore)!,
workspace: workspace ?? get(workspaceStore)!,
path: path ?? ''
})
return {
@@ -234,9 +239,9 @@ export async function getScriptByPath(path: string): Promise<{
}
}
export async function getLatestHashForScript(path: string): Promise<string> {
export async function getLatestHashForScript(path: string, workspace?: string): Promise<string> {
const script = await ScriptService.getScriptByPath({
workspace: get(workspaceStore)!,
workspace: workspace ?? get(workspaceStore)!,
path: path ?? ''
})
return script.hash