mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 08:01:35 +00:00
feat(pipeline): compact preview layout, two-way Test/Run sync
This commit is contained in:
@@ -161,6 +161,13 @@
|
||||
* orientation the flow editor uses for module step editing.
|
||||
*/
|
||||
previewLayout?: 'right' | 'bottom'
|
||||
/**
|
||||
* Fires whenever the test/preview run state changes — used by the
|
||||
* pipeline editor to mirror the running script onto the canvas
|
||||
* (animate its incoming/outgoing edges) so Test feels equivalent
|
||||
* to clicking a run button on the graph.
|
||||
*/
|
||||
onTestStateChange?: (running: boolean) => void
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -195,9 +202,14 @@
|
||||
modules = $bindable(undefined),
|
||||
editorBarRight,
|
||||
enablePreprocessorSnippet = false,
|
||||
previewLayout = 'right'
|
||||
previewLayout = 'right',
|
||||
onTestStateChange
|
||||
}: Props = $props()
|
||||
|
||||
$effect(() => {
|
||||
onTestStateChange?.(testIsLoading)
|
||||
})
|
||||
|
||||
let initialArgs = structuredClone($state.snapshot(args))
|
||||
let jsonView = $state(false)
|
||||
let schemaHeight = $state(0)
|
||||
@@ -1485,8 +1497,16 @@
|
||||
{@render editorContent()}
|
||||
{/if}
|
||||
</Pane>
|
||||
<Pane bind:size={testPanelSize} minSize={0}>
|
||||
<div class="flex flex-col h-full">
|
||||
<Pane
|
||||
bind:size={testPanelSize}
|
||||
minSize={0}
|
||||
class={customUi?.previewPanel?.hideArgs ? '!overflow-visible' : ''}
|
||||
>
|
||||
<div
|
||||
class={customUi?.previewPanel?.hideArgs
|
||||
? 'flex flex-col h-full !overflow-visible'
|
||||
: 'flex flex-col h-full'}
|
||||
>
|
||||
{#if showTabs}
|
||||
<div transition:slide={{ duration: 200 }}>
|
||||
<Tabs bind:selected={selectedTab}>
|
||||
@@ -1604,171 +1624,244 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#key previewLayout}
|
||||
<Splitpanes
|
||||
horizontal={previewLayout !== 'bottom'}
|
||||
class="!max-h-[calc(100%-{debugMode && isDebuggableScript
|
||||
? '83'
|
||||
: previewLayout === 'bottom'
|
||||
? '0'
|
||||
: '43'}px)]"
|
||||
>
|
||||
<Pane size={previewLayout === 'bottom' ? 40 : 33}>
|
||||
{#if previewLayout === 'bottom' && !(debugMode && isDebuggableScript)}
|
||||
<div class="px-3 pt-2 pb-1 flex items-center gap-2">
|
||||
{#if testIsLoading}
|
||||
<Button on:click={jobLoader?.cancelJob} unifiedSize="sm" btnClasses="w-full">
|
||||
<WindmillIcon
|
||||
white={true}
|
||||
class="mr-2 text-white"
|
||||
height="14px"
|
||||
width="16px"
|
||||
spin="fast"
|
||||
/>
|
||||
Cancel
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
on:click={() => runTest()}
|
||||
unifiedSize="sm"
|
||||
btnClasses="w-full"
|
||||
variant="accent-secondary"
|
||||
startIcon={{ icon: Play, classes: 'animate-none' }}
|
||||
shortCut={{ Icon: CornerDownLeft }}
|
||||
>
|
||||
Test
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if jsonView}
|
||||
<div
|
||||
class="py-2"
|
||||
style="height: {!schemaHeight || schemaHeight < 600 ? 600 : schemaHeight}px"
|
||||
data-schema-picker
|
||||
>
|
||||
<JsonInputs
|
||||
on:select={(e) => {
|
||||
if (e.detail) {
|
||||
if (activeModuleTab !== null) {
|
||||
testPanelArgs = e.detail
|
||||
} else {
|
||||
args = e.detail
|
||||
}
|
||||
}
|
||||
}}
|
||||
updateOnBlur={false}
|
||||
placeholder={`Write args as JSON.<br/><br/>Example:<br/><br/>{<br/> "foo": "12"<br/>}`}
|
||||
{#if customUi?.previewPanel?.hideArgs}
|
||||
<!-- Compact preview layout used by the pipeline editor:
|
||||
no args column (the script is known to take no
|
||||
inputs), LogPanel takes the full width, and the
|
||||
Test/Cancel button floats at the top-left with a
|
||||
-translate-y so it visually attaches to the editor
|
||||
panel above instead of stealing vertical space
|
||||
inside the preview band. -->
|
||||
<div class="relative h-full">
|
||||
<div class="absolute top-0 left-2 -translate-y-1/2 z-10">
|
||||
{#if testIsLoading}
|
||||
<Button on:click={jobLoader?.cancelJob} unifiedSize="sm" btnClasses="shadow-md">
|
||||
<WindmillIcon
|
||||
white={true}
|
||||
class="mr-2 text-white"
|
||||
height="14px"
|
||||
width="16px"
|
||||
spin="fast"
|
||||
/>
|
||||
</div>
|
||||
Cancel
|
||||
</Button>
|
||||
{:else}
|
||||
<div class="px-4">
|
||||
<div class="break-words relative font-sans" bind:clientHeight={schemaHeight}>
|
||||
{#key argsRender}
|
||||
{#if activeModuleTab !== null}
|
||||
<SchemaForm
|
||||
helperScript={{
|
||||
source: 'inline',
|
||||
code: editorCode,
|
||||
//@ts-ignore
|
||||
lang: effectiveLang
|
||||
}}
|
||||
compact
|
||||
schema={testPanelSchema}
|
||||
bind:args={testPanelArgs}
|
||||
bind:isValid
|
||||
noVariablePicker={customUi?.previewPanel?.disableVariablePicker ===
|
||||
true}
|
||||
showSchemaExplorer
|
||||
<Button
|
||||
on:click={() => runTest()}
|
||||
unifiedSize="sm"
|
||||
btnClasses="shadow-md"
|
||||
variant="accent-secondary"
|
||||
startIcon={{ icon: Play, classes: 'animate-none' }}
|
||||
shortCut={{ Icon: CornerDownLeft }}
|
||||
>
|
||||
Test
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<LogPanel
|
||||
bind:this={logPanel}
|
||||
{lang}
|
||||
previewJob={debugMode
|
||||
? ({
|
||||
id: 'debug',
|
||||
logs: $debugState.logs,
|
||||
result: $debugState.result,
|
||||
success: !$debugState.error,
|
||||
type: hasDebugResult ? 'CompletedJob' : 'QueuedJob'
|
||||
} as any)
|
||||
: testJob}
|
||||
{pastPreviews}
|
||||
previewIsLoading={debugMode
|
||||
? $debugState.running && !$debugState.stopped
|
||||
: testIsLoading}
|
||||
{editor}
|
||||
{diffEditor}
|
||||
args={activeModuleTab !== null ? testPanelArgs : args}
|
||||
{showCaptures}
|
||||
customUi={customUi?.previewPanel}
|
||||
showCustomResultPanel={showDebugPanel}
|
||||
>
|
||||
{#if scriptProgress && !debugMode}
|
||||
<JobProgressBar
|
||||
job={testJob}
|
||||
{scriptProgress}
|
||||
bind:this={jobProgressBar}
|
||||
compact={true}
|
||||
/>
|
||||
{/if}
|
||||
</LogPanel>
|
||||
</div>
|
||||
{:else}
|
||||
{#key previewLayout}
|
||||
<Splitpanes
|
||||
horizontal={previewLayout !== 'bottom'}
|
||||
class="!max-h-[calc(100%-{debugMode && isDebuggableScript
|
||||
? '83'
|
||||
: previewLayout === 'bottom'
|
||||
? '0'
|
||||
: '43'}px)]"
|
||||
>
|
||||
<Pane size={previewLayout === 'bottom' ? 40 : 33}>
|
||||
{#if previewLayout === 'bottom' && !(debugMode && isDebuggableScript)}
|
||||
<div class="px-3 pt-2 pb-1 flex items-center gap-2">
|
||||
{#if testIsLoading}
|
||||
<Button
|
||||
on:click={jobLoader?.cancelJob}
|
||||
unifiedSize="sm"
|
||||
btnClasses="w-full"
|
||||
>
|
||||
<WindmillIcon
|
||||
white={true}
|
||||
class="mr-2 text-white"
|
||||
height="14px"
|
||||
width="16px"
|
||||
spin="fast"
|
||||
/>
|
||||
{:else}
|
||||
<SchemaForm
|
||||
helperScript={{
|
||||
source: 'inline',
|
||||
code,
|
||||
//@ts-ignore
|
||||
lang
|
||||
}}
|
||||
compact
|
||||
{schema}
|
||||
bind:args
|
||||
bind:isValid
|
||||
noVariablePicker={customUi?.previewPanel?.disableVariablePicker ===
|
||||
true}
|
||||
showSchemaExplorer
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
{#if showPsCommonParams}
|
||||
<div class="mt-2">
|
||||
<PowerShellCommonParams bind:args={psCommonParams} />
|
||||
</div>
|
||||
Cancel
|
||||
</Button>
|
||||
{:else}
|
||||
<Button
|
||||
on:click={() => runTest()}
|
||||
unifiedSize="sm"
|
||||
btnClasses="w-full"
|
||||
variant="accent-secondary"
|
||||
startIcon={{ icon: Play, classes: 'animate-none' }}
|
||||
shortCut={{ Icon: CornerDownLeft }}
|
||||
>
|
||||
Test
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Pane>
|
||||
<Pane size={previewLayout === 'bottom' ? 60 : 67} class="relative">
|
||||
<LogPanel
|
||||
bind:this={logPanel}
|
||||
{lang}
|
||||
previewJob={debugMode
|
||||
? ({
|
||||
id: 'debug',
|
||||
logs: $debugState.logs,
|
||||
result: $debugState.result,
|
||||
success: !$debugState.error,
|
||||
type: hasDebugResult ? 'CompletedJob' : 'QueuedJob'
|
||||
} as any)
|
||||
: testJob}
|
||||
{pastPreviews}
|
||||
previewIsLoading={debugMode
|
||||
? $debugState.running && !$debugState.stopped
|
||||
: testIsLoading}
|
||||
{editor}
|
||||
{diffEditor}
|
||||
args={activeModuleTab !== null ? testPanelArgs : args}
|
||||
{showCaptures}
|
||||
customUi={customUi?.previewPanel}
|
||||
showCustomResultPanel={showDebugPanel}
|
||||
>
|
||||
{#if scriptProgress && !debugMode}
|
||||
<!-- Put to the slot in logpanel -->
|
||||
<JobProgressBar
|
||||
job={testJob}
|
||||
{scriptProgress}
|
||||
bind:this={jobProgressBar}
|
||||
compact={true}
|
||||
/>
|
||||
{/if}
|
||||
{#snippet capturesTab()}
|
||||
<div class="h-full p-2">
|
||||
<CaptureTable
|
||||
bind:this={captureTable}
|
||||
{hasPreprocessor}
|
||||
canHavePreprocessor={canHavePreprocessor(lang)}
|
||||
isFlow={false}
|
||||
path={stablePathForCaptures}
|
||||
canEdit={true}
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
{#if jsonView}
|
||||
<div
|
||||
class="py-2"
|
||||
style="height: {!schemaHeight || schemaHeight < 600 ? 600 : schemaHeight}px"
|
||||
data-schema-picker
|
||||
>
|
||||
<JsonInputs
|
||||
on:select={(e) => {
|
||||
if (e.detail) {
|
||||
if (activeModuleTab !== null) {
|
||||
testPanelArgs = e.detail
|
||||
} else {
|
||||
args = e.detail
|
||||
}
|
||||
}
|
||||
}}
|
||||
updateOnBlur={false}
|
||||
placeholder={`Write args as JSON.<br/><br/>Example:<br/><br/>{<br/> "foo": "12"<br/>}`}
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet customResultPanel()}
|
||||
<DebugPanel
|
||||
stackFrames={$debugState.stackFrames}
|
||||
scopes={$debugState.scopes}
|
||||
variables={$debugState.variables}
|
||||
client={dapClient}
|
||||
bind:selectedFrameId={selectedDebugFrameId}
|
||||
/>
|
||||
{/snippet}
|
||||
</LogPanel>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
{/key}
|
||||
{:else}
|
||||
<div class="px-4">
|
||||
<div class="break-words relative font-sans" bind:clientHeight={schemaHeight}>
|
||||
{#key argsRender}
|
||||
{#if activeModuleTab !== null}
|
||||
<SchemaForm
|
||||
helperScript={{
|
||||
source: 'inline',
|
||||
code: editorCode,
|
||||
//@ts-ignore
|
||||
lang: effectiveLang
|
||||
}}
|
||||
compact
|
||||
schema={testPanelSchema}
|
||||
bind:args={testPanelArgs}
|
||||
bind:isValid
|
||||
noVariablePicker={customUi?.previewPanel?.disableVariablePicker ===
|
||||
true}
|
||||
showSchemaExplorer
|
||||
/>
|
||||
{:else}
|
||||
<SchemaForm
|
||||
helperScript={{
|
||||
source: 'inline',
|
||||
code,
|
||||
//@ts-ignore
|
||||
lang
|
||||
}}
|
||||
compact
|
||||
{schema}
|
||||
bind:args
|
||||
bind:isValid
|
||||
noVariablePicker={customUi?.previewPanel?.disableVariablePicker ===
|
||||
true}
|
||||
showSchemaExplorer
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
{#if showPsCommonParams}
|
||||
<div class="mt-2">
|
||||
<PowerShellCommonParams bind:args={psCommonParams} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Pane>
|
||||
<Pane size={previewLayout === 'bottom' ? 60 : 67} class="relative">
|
||||
<LogPanel
|
||||
bind:this={logPanel}
|
||||
{lang}
|
||||
previewJob={debugMode
|
||||
? ({
|
||||
id: 'debug',
|
||||
logs: $debugState.logs,
|
||||
result: $debugState.result,
|
||||
success: !$debugState.error,
|
||||
type: hasDebugResult ? 'CompletedJob' : 'QueuedJob'
|
||||
} as any)
|
||||
: testJob}
|
||||
{pastPreviews}
|
||||
previewIsLoading={debugMode
|
||||
? $debugState.running && !$debugState.stopped
|
||||
: testIsLoading}
|
||||
{editor}
|
||||
{diffEditor}
|
||||
args={activeModuleTab !== null ? testPanelArgs : args}
|
||||
{showCaptures}
|
||||
customUi={customUi?.previewPanel}
|
||||
showCustomResultPanel={showDebugPanel}
|
||||
>
|
||||
{#if scriptProgress && !debugMode}
|
||||
<!-- Put to the slot in logpanel -->
|
||||
<JobProgressBar
|
||||
job={testJob}
|
||||
{scriptProgress}
|
||||
bind:this={jobProgressBar}
|
||||
compact={true}
|
||||
/>
|
||||
{/if}
|
||||
{#snippet capturesTab()}
|
||||
<div class="h-full p-2">
|
||||
<CaptureTable
|
||||
bind:this={captureTable}
|
||||
{hasPreprocessor}
|
||||
canHavePreprocessor={canHavePreprocessor(lang)}
|
||||
isFlow={false}
|
||||
path={stablePathForCaptures}
|
||||
canEdit={true}
|
||||
on:applyArgs
|
||||
on:updateSchema
|
||||
on:addPreprocessor
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
{#snippet customResultPanel()}
|
||||
<DebugPanel
|
||||
stackFrames={$debugState.stackFrames}
|
||||
scopes={$debugState.scopes}
|
||||
variables={$debugState.variables}
|
||||
client={dapClient}
|
||||
bind:selectedFrameId={selectedDebugFrameId}
|
||||
/>
|
||||
{/snippet}
|
||||
</LogPanel>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
{/key}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</Pane>
|
||||
|
||||
@@ -110,6 +110,19 @@
|
||||
// gets a "Recomputing…" banner so the user knows the rendered
|
||||
// snapshot is about to be replaced.
|
||||
activeRunnable?: { kind: 'script' | 'flow'; path: string } | undefined
|
||||
// Forwarded from ScriptEditor when the user hits the Test button —
|
||||
// gives the page a chance to mark the script as the active
|
||||
// runnable on the canvas (animates its edges) so Test and the
|
||||
// canvas Run button feel equivalent.
|
||||
onTestStateChange?: (running: boolean) => void
|
||||
// Counter pattern (cf. `requestRemoveSignal`) — bumped by the
|
||||
// page when the user clicks the canvas Run button on the
|
||||
// currently-open script, so we can route the dispatch through
|
||||
// ScriptEditor.runTest() and surface the running state in the
|
||||
// preview panel (logs/result/cancel) instead of just animating
|
||||
// the edges. Counter rather than boolean so back-to-back runs
|
||||
// re-fire the effect even if no other prop changed.
|
||||
requestRunSignal?: number
|
||||
// Folder-scoped non-editable prefix shown next to the suffix
|
||||
// editor when the user renames a draft (e.g. `f/<folder>/`). The
|
||||
// new path = pathPrefix + suffix.
|
||||
@@ -144,11 +157,24 @@
|
||||
runsPendingJobId,
|
||||
onRunCompleted,
|
||||
activeRunnable,
|
||||
onTestStateChange,
|
||||
requestRunSignal,
|
||||
pathPrefix = '',
|
||||
onDraftPathChange,
|
||||
requestRemoveSignal
|
||||
}: Props = $props()
|
||||
|
||||
// Held ref to ScriptEditor so we can route a canvas-side Run dispatch
|
||||
// through .runTest() — gives the test panel logs/result/cancel for
|
||||
// runs initiated from the graph, not just the in-pane Test button.
|
||||
let scriptEditorRef: { runTest: () => Promise<unknown> } | undefined = $state(undefined)
|
||||
$effect(() => {
|
||||
// Track the counter; ignore the initial 0/undefined.
|
||||
const sig = requestRunSignal
|
||||
if (sig === undefined || sig === 0) return
|
||||
void scriptEditorRef?.runTest()
|
||||
})
|
||||
|
||||
// True when the script that writes to the currently-selected asset is
|
||||
// running right now. Drives the "Recomputing…" banner above the
|
||||
// preview and pairs with `onRunCompleted` (which bumps
|
||||
@@ -671,6 +697,7 @@
|
||||
{:else if script}
|
||||
{#key (script.hash ?? `draft:${script.path}`) + script.language}
|
||||
<ScriptEditor
|
||||
bind:this={scriptEditorRef}
|
||||
showCaptures={false}
|
||||
noSyncFromGithub
|
||||
lang={script.language}
|
||||
@@ -683,12 +710,21 @@
|
||||
disableHistory: true,
|
||||
disableTracing: true,
|
||||
disableTriggerCaptures: true,
|
||||
disableJsonView: true
|
||||
disableJsonView: true,
|
||||
// Pipeline scripts in this view are dispatched without
|
||||
// args (the page's runScriptByPath / runScriptPreview
|
||||
// passes `{}`), so the SchemaForm column is dead
|
||||
// space. Drop it, render the LogPanel full-width with
|
||||
// logs|result side by side, and float the Test/Cancel
|
||||
// button onto the editor band above.
|
||||
hideArgs: true,
|
||||
logsResultSideBySide: true
|
||||
}
|
||||
}}
|
||||
bind:code={script.content}
|
||||
bind:schema={script.schema}
|
||||
bind:assets={liveBodyAssets}
|
||||
{onTestStateChange}
|
||||
{args}
|
||||
>
|
||||
{#snippet editorBarRight()}
|
||||
|
||||
@@ -61,6 +61,16 @@ export type PreviewPanelUi = {
|
||||
disableVariablePicker?: boolean
|
||||
disableDownload?: boolean
|
||||
tagLabel?: string
|
||||
// Hide the args/SchemaForm pane entirely (use cases where the script
|
||||
// is known to take no inputs — e.g. the pipeline editor's per-script
|
||||
// preview). When set, the Test/Cancel button is rendered as a small
|
||||
// floating affordance at the top-left of the preview area instead of
|
||||
// inside the (now-absent) args column.
|
||||
hideArgs?: boolean
|
||||
// Render the LogPanel's logs/result as a left/right split instead of
|
||||
// the default top/bottom. Pairs naturally with `hideArgs` when the
|
||||
// preview area is the full bottom band.
|
||||
logsResultSideBySide?: boolean
|
||||
}
|
||||
|
||||
export type EditorBarUi = {
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<SplitPanesWrapper>
|
||||
<Splitpanes horizontal>
|
||||
<Splitpanes horizontal={customUi?.logsResultSideBySide !== true}>
|
||||
<Pane class="relative">
|
||||
<LogViewer
|
||||
jobId={previewJob?.id}
|
||||
|
||||
@@ -782,6 +782,12 @@
|
||||
// AssetRunsPanel reports the run is done. The same hook will later
|
||||
// be reused for live pipeline status.
|
||||
let activeRunnable = $state<{ kind: 'script' | 'flow'; path: string } | undefined>(undefined)
|
||||
// Counter bumped when the canvas Run button targets the currently-open
|
||||
// script — the pane intercepts and routes through ScriptEditor.runTest
|
||||
// so logs/result/cancel land in the test panel instead of going off
|
||||
// into nowhere with only edge animation as feedback. Counter rather
|
||||
// than boolean so back-to-back runs re-fire.
|
||||
let requestRunSignal = $state(0)
|
||||
// Counter bumped from the runnable-node action menu to ask the pane to
|
||||
// open its archive/delete confirmation modal for the loaded script.
|
||||
// Counter (vs boolean) so successive triggers re-fire even if the user
|
||||
@@ -1092,6 +1098,20 @@
|
||||
// immediately — its background poll only kicks in
|
||||
// for already-listed in-flight jobs.
|
||||
if (!$workspaceStore || producer.kind !== 'script') return undefined
|
||||
// If the producer being run is the script currently
|
||||
// edited in the pane, route through ScriptEditor's
|
||||
// Test path — the test panel then shows logs/result
|
||||
// and the user can cancel from there. Same UX as
|
||||
// hitting the Test button directly.
|
||||
const openPath =
|
||||
activeDraftPath ??
|
||||
(selection?.kind === 'runnable' && selection.runnable_kind === 'script'
|
||||
? selection.path
|
||||
: undefined)
|
||||
if (openPath === producer.path) {
|
||||
requestRunSignal++
|
||||
return undefined
|
||||
}
|
||||
let jobId: string | undefined
|
||||
if (producer.unsaved) {
|
||||
const draft = drafts.get(producer.path)
|
||||
@@ -1150,7 +1170,25 @@
|
||||
{runsPendingJobId}
|
||||
{activeRunnable}
|
||||
onRunCompleted={() => (activeRunnable = undefined)}
|
||||
onTestStateChange={(running) => {
|
||||
// Bridge: ScriptEditor's Test button triggers the
|
||||
// same canvas-level "is running" hint as the
|
||||
// per-node Run button. The currently-edited script
|
||||
// is whichever path is open in the pane (active
|
||||
// draft, or the persisted-script selection).
|
||||
const openPath =
|
||||
activeDraftPath ??
|
||||
(selection?.kind === 'runnable' && selection.runnable_kind === 'script'
|
||||
? selection.path
|
||||
: undefined)
|
||||
if (running && openPath) {
|
||||
activeRunnable = { kind: 'script', path: openPath }
|
||||
} else if (!running && activeRunnable?.path === openPath) {
|
||||
activeRunnable = undefined
|
||||
}
|
||||
}}
|
||||
{requestRemoveSignal}
|
||||
{requestRunSignal}
|
||||
draftScript={activeDraft?.script}
|
||||
{pathPrefix}
|
||||
onDraftPathChange={renameDraft}
|
||||
|
||||
Reference in New Issue
Block a user