mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-12 08:05:44 +00:00
fix: approval page freeze, stale state, and missing approval link (#8653)
* fix: prevent browser freeze when approval form number field has no default value Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: disable approval buttons and keep polling after approve/deny action Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: restore approval page link and prevent double resume in flow viewer Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: guard against NaN fallback in Range and reset actionTaken on new approval step Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix approval page url --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
df7a8eebcf
commit
7069202190
@@ -3,7 +3,7 @@
|
||||
import { type Job, JobService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { X } from 'lucide-svelte'
|
||||
import { ExternalLink, X } from 'lucide-svelte'
|
||||
import DisplayResult from './DisplayResult.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import { Button } from './common'
|
||||
@@ -23,6 +23,7 @@
|
||||
let default_payload: object = $state({})
|
||||
let description: any = $state(undefined)
|
||||
let hide_cancel = $state(false)
|
||||
let approvalPageUrl: string | undefined = $state(undefined)
|
||||
|
||||
let defaultValues = $state({})
|
||||
|
||||
@@ -47,6 +48,8 @@
|
||||
defaultValues = JSON.parse(JSON.stringify(args))
|
||||
default_payload = args
|
||||
|
||||
approvalPageUrl = job_result?.['approvalPage']
|
||||
actionTaken = false
|
||||
hide_cancel = job?.raw_flow?.modules?.[approvalStep]?.suspend?.hide_cancel ?? false
|
||||
schema = mergeSchema(
|
||||
job?.raw_flow?.modules?.[approvalStep]?.suspend?.resume_form?.schema ?? {},
|
||||
@@ -55,6 +58,7 @@
|
||||
}
|
||||
|
||||
let loading = $state(false)
|
||||
let actionTaken = $state(false)
|
||||
async function continu(approve: boolean) {
|
||||
loading = true
|
||||
try {
|
||||
@@ -66,6 +70,7 @@
|
||||
approved: approve
|
||||
}
|
||||
})
|
||||
actionTaken = true
|
||||
} catch (e: any) {
|
||||
sendUserToast(e?.body ?? e?.message ?? 'Failed', true)
|
||||
} finally {
|
||||
@@ -84,7 +89,7 @@
|
||||
<div class="mt-2"></div>
|
||||
{/if}
|
||||
<div>
|
||||
<div class={twMerge('flex gap-2', light ? 'flex-col' : 'flex-row ')}>
|
||||
<div class={twMerge('flex gap-2 items-center', light ? 'flex-col' : 'flex-row ')}>
|
||||
{#if !hide_cancel}
|
||||
<div>
|
||||
<Button
|
||||
@@ -92,7 +97,7 @@
|
||||
iconOnly
|
||||
startIcon={{ icon: X }}
|
||||
variant="default"
|
||||
disabled={loading}
|
||||
disabled={loading || actionTaken}
|
||||
destructive
|
||||
unifiedSize="md"
|
||||
on:click={() => continu(false)}
|
||||
@@ -100,12 +105,28 @@
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<Button variant="accent" onClick={() => continu(true)} disabled={loading} unifiedSize="md">
|
||||
<Button
|
||||
variant="accent"
|
||||
onClick={() => continu(true)}
|
||||
disabled={loading || actionTaken}
|
||||
unifiedSize="md"
|
||||
>
|
||||
Resume
|
||||
<Tooltip class="text-white">Resume or approve this suspended step</Tooltip>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if approvalPageUrl}
|
||||
<a
|
||||
href={approvalPageUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="text-accent flex items-center gap-1 whitespace-nowrap"
|
||||
>
|
||||
Approval page <ExternalLink size={12} />
|
||||
</a>
|
||||
{/if}
|
||||
|
||||
{#if job?.raw_flow?.modules?.[approvalStep]?.suspend?.resume_form?.schema}
|
||||
<div
|
||||
class={twMerge(
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
min = 0,
|
||||
max = 100,
|
||||
initialValue = 0,
|
||||
value = $bindable(typeof initialValue === 'string' ? parseInt(initialValue) : initialValue),
|
||||
value = $bindable(),
|
||||
disabled = false,
|
||||
defaultValue = undefined,
|
||||
format = (v) => `${v}`,
|
||||
@@ -36,8 +36,14 @@
|
||||
}
|
||||
|
||||
run(() => {
|
||||
if (value === null) {
|
||||
value = 0
|
||||
if (value === null || value === undefined || Number.isNaN(value)) {
|
||||
const fallback =
|
||||
initialValue !== undefined
|
||||
? typeof initialValue === 'string'
|
||||
? parseInt(initialValue)
|
||||
: initialValue
|
||||
: (min ?? 0)
|
||||
value = Number.isNaN(fallback) ? (min ?? 0) : fallback
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
let default_payload: any = $state({})
|
||||
let loading = $state(false)
|
||||
let valid = $state(true)
|
||||
let actionTaken: 'approved' | 'denied' | undefined = $state(undefined)
|
||||
|
||||
let pollInterval: number | undefined = undefined
|
||||
let scheduleEditor: ScheduleEditor | undefined = $state(undefined)
|
||||
@@ -81,6 +82,9 @@
|
||||
id: page.params.job ?? ''
|
||||
})) as Job
|
||||
completed = job?.type === 'CompletedJob'
|
||||
if (completed) {
|
||||
pollInterval && clearInterval(pollInterval)
|
||||
}
|
||||
} catch {
|
||||
// Job details are optional — page works with just approvalInfo
|
||||
}
|
||||
@@ -103,7 +107,7 @@
|
||||
}
|
||||
})
|
||||
sendUserToast('Flow approved')
|
||||
pollInterval && clearInterval(pollInterval)
|
||||
actionTaken = 'approved'
|
||||
loadData()
|
||||
} catch (e: any) {
|
||||
sendUserToast(e?.body ?? e?.message ?? 'Failed to approve', true)
|
||||
@@ -125,7 +129,7 @@
|
||||
}
|
||||
})
|
||||
sendUserToast('Flow denied!')
|
||||
pollInterval && clearInterval(pollInterval)
|
||||
actionTaken = 'denied'
|
||||
loadData()
|
||||
} catch (e: any) {
|
||||
sendUserToast(e?.body ?? e?.message ?? 'Failed to cancel', true)
|
||||
@@ -259,6 +263,12 @@
|
||||
<Alert type="info" title="Flow completed">
|
||||
The flow is not running anymore. You cannot cancel or resume it.
|
||||
</Alert>
|
||||
{:else if actionTaken}
|
||||
<Alert type="info" title={actionTaken === 'approved' ? 'Flow approved' : 'Flow denied'}>
|
||||
{actionTaken === 'approved'
|
||||
? 'You have approved this flow. Waiting for it to complete...'
|
||||
: 'You have denied this flow. Waiting for it to complete...'}
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
{#if approvalInfo.description != undefined}
|
||||
@@ -279,31 +289,20 @@
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if !completed && approvalInfo.can_approve}
|
||||
{#if !completed && !actionTaken && approvalInfo.can_approve}
|
||||
<div class="w-max-md flex flex-row gap-x-4 gap-y-4 justify-between w-full flex-wrap">
|
||||
{#if approvalInfo.hide_cancel !== true}
|
||||
<Button
|
||||
variant="accent"
|
||||
destructive
|
||||
onclick={cancel}
|
||||
size="lg"
|
||||
disabled={completed || loading}
|
||||
>
|
||||
<Button variant="accent" destructive onclick={cancel} size="lg" disabled={loading}>
|
||||
Deny
|
||||
</Button>
|
||||
{:else}
|
||||
<div></div>
|
||||
{/if}
|
||||
<Button
|
||||
variant="accent"
|
||||
onclick={resume}
|
||||
size="lg"
|
||||
disabled={completed || !valid || loading}
|
||||
>
|
||||
<Button variant="accent" onclick={resume} size="lg" disabled={!valid || loading}>
|
||||
Approve
|
||||
</Button>
|
||||
</div>
|
||||
{:else if !completed && !approvalInfo.can_approve}
|
||||
{:else if !completed && !actionTaken && !approvalInfo.can_approve}
|
||||
{#if approvalInfo.user_auth_required && !$userStore}
|
||||
<Login {rd} />
|
||||
{:else}
|
||||
|
||||
Reference in New Issue
Block a user