Fix suspend step duplicate forms (#7581)

* nit totalEventsWaiting

* remove temp console log

* loading indicator in Resume button

* refactor

* Revert "refactor"

This reverts commit eae6213cdd.

* useThrottle

* fix promises

* Revert "fix promises"

This reverts commit 90a5613245.

* svelte 5 migration

* Fix duplicate resume forms

* Check all flow_jobs
This commit is contained in:
Diego Imbert
2026-01-15 06:09:14 +00:00
committed by GitHub
parent b360780916
commit b59ef3efc6
3 changed files with 58 additions and 64 deletions
@@ -32,16 +32,6 @@
extra,
result_streams
}: Props = $props()
// Sometimes the approval form is duplicated but I can't reproduce the issue
// This is a temporary debug log to try to catch it when it happens
// (See the #each below)
$effect(() =>
console.log(
'suspendStatusVal',
Object.entries(suspendStatus.val || {}).map(([k, v]) => [k, v.job.id])
)
)
</script>
<FlowPreviewStatus {job} {hideJobId} {extra} />
@@ -49,6 +49,7 @@
import JobAssetsViewer from './assets/JobAssetsViewer.svelte'
import McpToolCallDetails from './McpToolCallDetails.svelte'
import { SelectionManager } from './graph/selectionUtils.svelte'
import { useThrottle } from 'runed'
let {
flowState: flowStateStore,
@@ -503,8 +504,21 @@
}
let jobMissingStartedAt: Record<string, number | 'P'> = {}
let lastSelectedLoopSwitch: number | undefined
let selectedLoopSwitchTimeout: number | undefined = undefined
let setSelectedLoopSwitch = useThrottle(async (lastStarted: string, mod: FlowStatusModule) => {
let position = mod.flow_jobs?.indexOf(lastStarted)
if (!position) return
for (const flow_job of mod.flow_jobs ?? []) {
if (flow_job === lastStarted) {
break
} else if (flow_job !== lastStarted && suspendStatus.val[flow_job]) {
console.log('setSelectedLoopSwitch deleting suspend for', flow_job)
delete suspendStatus.val[flow_job]
}
}
console.log('setSelectedLoopSwitch', position, lastStarted, mod.id, suspendStatus)
setIteration(position, lastStarted, false, mod.id ?? '', true)
}, 2000)
function updateInnerModules() {
if (localModuleStates) {
@@ -641,29 +655,7 @@
})
if (anySet) {
updateDurationStatuses(key, nDurationStatuses)
selectedLoopSwitchTimeout && clearTimeout(selectedLoopSwitchTimeout)
function setSelectedLoopSwitch() {
if (lastStarted) {
let position = mod.flow_jobs?.indexOf(lastStarted)
if (position != undefined) {
lastSelectedLoopSwitch = new Date().getTime()
console.log('setSelectedLoopSwitch', position, lastStarted)
setIteration(position, lastStarted, false, mod.id ?? '', true)
}
}
}
if (
lastSelectedLoopSwitch &&
new Date().getTime() - lastSelectedLoopSwitch < 3000
) {
selectedLoopSwitchTimeout = setTimeout(() => {
setSelectedLoopSwitch()
}, 2000)
} else {
console.log('setSelectedLoopSwitch')
setSelectedLoopSwitch()
}
if (lastStarted) setSelectedLoopSwitch(lastStarted, mod)
}
})
.catch((e) => {
@@ -1293,6 +1285,10 @@
tabsHeight.graphHeight
)
)
let totalEventsWaiting = $derived(
Object.values(suspendStatus?.val ?? {}).reduce((a, b) => a + (b?.nb ?? 0), 0)
)
</script>
<JobLoader workspaceOverride={workspaceId} {noLogs} noCode bind:this={jobLoader} />
@@ -1744,13 +1740,11 @@
</span>
{/if}
{/each}
{#each Object.values(suspendStatus?.val ?? {}) as count}
{#if count.nb}
<span class="text-sm">
Flow suspended, waiting for {count.nb} events
</span>
{/if}
{/each}
{#if totalEventsWaiting}
<span class="text-sm">
Flow suspended, waiting for {totalEventsWaiting} events
</span>
{/if}
</div>
<FlowGraphV2
{selectionManager}
@@ -9,24 +9,26 @@
import { Button } from './common'
import SchemaForm from './SchemaForm.svelte'
import { twMerge } from 'tailwind-merge'
import { untrack } from 'svelte'
export let isOwner: boolean
export let workspaceId: string | undefined
export let job: Job
export let light: boolean = false
interface Props {
isOwner: boolean
workspaceId: string | undefined
job: Job
light?: boolean
}
let default_payload: object = {}
let resumeUrl: string | undefined = undefined
let cancelUrl: string | undefined = undefined
let description: any = undefined
let hide_cancel = false
let { isOwner, workspaceId, job, light = false }: Props = $props()
$: approvalStep = (job?.flow_status?.step ?? 1) - 1
let default_payload: object = $state({})
let resumeUrl: string | undefined = $state(undefined)
let cancelUrl: string | undefined = $state(undefined)
let description: any = $state(undefined)
let hide_cancel = $state(false)
let defaultValues = {}
$: job && getDefaultArgs()
let defaultValues = $state({})
let schema = {}
let schema = $state({})
let lastJobId: string | undefined = undefined
async function getDefaultArgs() {
let jobId = job?.flow_status?.modules?.[approvalStep]?.job
@@ -56,7 +58,9 @@
)
}
let loading = $state(false)
async function continu(approve: boolean) {
loading = true
if ((resumeUrl && approve) || (cancelUrl && !approve)) {
let split = (approve ? resumeUrl : cancelUrl)!.split('/')
let signatureUrl = split.pop() ?? ''
@@ -114,6 +118,10 @@
}
}
}
let approvalStep = $derived((job?.flow_status?.step ?? 1) - 1)
$effect(() => {
job && untrack(() => getDefaultArgs())
})
</script>
<div class="w-full h-full mt-2 text-xs text-primary">
@@ -130,6 +138,7 @@
<div>
<Button
title="Cancel the step"
{loading}
iconOnly
startIcon={{ icon: X }}
variant="default"
@@ -139,12 +148,13 @@
</div>
{/if}
<div>
<Button variant="accent" on:click={() => continu(true)}
>Resume <Tooltip class="text-white"
>Since you are an owner of this flow, you can send resume events without necessarily
knowing the resume id sent by the approval step</Tooltip
></Button
>
<Button variant="accent" on:click={() => continu(true)} {loading}>
Resume
<Tooltip class="text-white">
Since you are an owner of this flow, you can send resume events without necessarily
knowing the resume id sent by the approval step
</Tooltip>
</Button>
</div>
{#if job?.raw_flow?.modules?.[approvalStep]?.suspend?.resume_form?.schema}
@@ -157,9 +167,9 @@
<SchemaForm onlyMaskPassword bind:args={default_payload} {defaultValues} {schema} />
</div>
{/if}
<Tooltip
>The payload is optional, it is passed to the following step through the `resume` variable</Tooltip
>
<Tooltip>
The payload is optional, it is passed to the following step through the `resume` variable
</Tooltip>
</div>
{:else}
You cannot resume the flow yourself without receiving the resume secret since you are not an