fix(forks): replace browser confirm() with ConfirmationModal for fork conflict

The fork-conflict warning previously used the browser's native confirm()
which doesn't match Windmill's design system. Switches to a singleton
ConfirmationModal mounted at the (logged) layout root, driven by a new
forkConflictModal store. The withForkConflictRetry helper now sets the
store and awaits the user's choice via a Promise, instead of blocking
on window.confirm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-04-30 15:50:58 +02:00
co-authored by Claude Opus 4.7
parent 7247c8741f
commit 41ec0ddc92
4 changed files with 67 additions and 8 deletions
@@ -0,0 +1,30 @@
<script lang="ts">
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
import { forkConflictModal } from '$lib/stores'
const state = $derived(forkConflictModal.val)
function close(confirmed: boolean) {
const resolve = state?.resolve
forkConflictModal.val = undefined
resolve?.(confirmed)
}
</script>
<ConfirmationModal
open={!!state}
title="Enable in fork conflicts with parent"
confirmationText="Enable anyway"
onConfirmed={() => close(true)}
onCanceled={() => close(false)}
>
{#if state}
<p>
This {state.kindLabel} is also enabled in the parent workspace (<span class="font-mono"
>{state.parentWorkspaceId}</span
>). Enabling it here means both will run at the same time and may compete for the same
upstream events or duplicate side effects.
</p>
<p class="mt-2">Enable in this fork anyway?</p>
{/if}
</ConfirmationModal>
+14 -1
View File
@@ -132,12 +132,25 @@ export const codeCompletionSessionEnabled = writable<boolean>(
export const usedTriggerKinds = writable<string[]>([])
export let globalDbManagerDrawer: StateStore<DbManagerUriState | undefined> = { val: undefined }
export let globalForkModal: StateStore<GlobalForkModalState | undefined> = createState({ val: undefined })
export let globalForkModal: StateStore<GlobalForkModalState | undefined> = createState({
val: undefined
})
export type GlobalForkModalState = {
opened: true
}
export type ForkConflictModalState = {
kind: string
kindLabel: string
parentWorkspaceId: string
resolve: (proceed: boolean) => void
}
export let forkConflictModal: StateStore<ForkConflictModalState | undefined> = createState({
val: undefined
})
type SQLBaseSchema = {
[schemaKey: string]: {
[tableKey: string]: {
+20 -7
View File
@@ -1,3 +1,5 @@
import { forkConflictModal } from '$lib/stores'
/**
* The backend rejects "enable" requests on triggers/schedules in a fork when
* the parent workspace has the same path enabled. The error body is shaped as
@@ -22,8 +24,20 @@ export function detectForkConflict(e: unknown): ForkConflict | null {
}
/**
* Catches a fork-conflict error from `fn(false)`, asks the user to confirm,
* and retries with `fn(true)` when accepted. Re-throws every other error.
* Opens the global ForkConflictModal and awaits the user's choice. Resolves
* to true when the user clicks "Enable anyway", false when they cancel or
* dismiss.
*/
function askForkConflictConfirm(kind: string, kindLabel: string, parentWorkspaceId: string) {
return new Promise<boolean>((resolve) => {
forkConflictModal.val = { kind, kindLabel, parentWorkspaceId, resolve }
})
}
/**
* Catches a fork-conflict error from `fn(false)`, shows the confirmation
* dialog, and retries with `fn(true)` when the user accepts. Re-throws every
* other error.
*
* `kindLabel` is shown to the user — pass a friendly name like "kafka trigger"
* or "schedule" so the dialog reads naturally.
@@ -37,11 +51,10 @@ export async function withForkConflictRetry<T>(
} catch (e) {
const conflict = detectForkConflict(e)
if (!conflict) throw e
const proceed = window.confirm(
`This ${kindLabel} is also enabled in the parent workspace ` +
`(${conflict.parentWorkspaceId}). Both will run at the same time and may ` +
`compete for the same upstream events or duplicate side effects.\n\n` +
`Enable in this fork anyway?`
const proceed = await askForkConflictConfirm(
conflict.kind,
kindLabel,
conflict.parentWorkspaceId
)
if (!proceed) {
throw new Error(
@@ -15,6 +15,7 @@
import WorkspaceMenu from '$lib/components/sidebar/WorkspaceMenu.svelte'
import SidebarContent from '$lib/components/sidebar/SidebarContent.svelte'
import CriticalAlertModal from '$lib/components/sidebar/CriticalAlertModal.svelte'
import ForkConflictModal from '$lib/components/ForkConflictModal.svelte'
import {
enterpriseLicense,
isPremiumStore,
@@ -837,6 +838,8 @@
<DBManagerDrawer uriState={globalDbManagerDrawer.val} />
{/if}
<ForkConflictModal />
<Modal2
title="Forking {$workspaceStore}"
target="#content"