diff --git a/frontend/src/lib/components/ForkConflictModal.svelte b/frontend/src/lib/components/ForkConflictModal.svelte new file mode 100644 index 0000000000..6457de2016 --- /dev/null +++ b/frontend/src/lib/components/ForkConflictModal.svelte @@ -0,0 +1,30 @@ + + + close(true)} + onCanceled={() => close(false)} +> + {#if state} +

+ This {state.kindLabel} is also enabled in the parent workspace ({state.parentWorkspaceId}). Enabling it here means both will run at the same time and may compete for the same + upstream events or duplicate side effects. +

+

Enable in this fork anyway?

+ {/if} +
diff --git a/frontend/src/lib/stores.ts b/frontend/src/lib/stores.ts index 2da17cc300..e19a1cc34d 100644 --- a/frontend/src/lib/stores.ts +++ b/frontend/src/lib/stores.ts @@ -132,12 +132,25 @@ export const codeCompletionSessionEnabled = writable( export const usedTriggerKinds = writable([]) export let globalDbManagerDrawer: StateStore = { val: undefined } -export let globalForkModal: StateStore = createState({ val: undefined }) +export let globalForkModal: StateStore = 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 = createState({ + val: undefined +}) + type SQLBaseSchema = { [schemaKey: string]: { [tableKey: string]: { diff --git a/frontend/src/lib/utils/forkConflict.ts b/frontend/src/lib/utils/forkConflict.ts index 77201115e4..b2866abf83 100644 --- a/frontend/src/lib/utils/forkConflict.ts +++ b/frontend/src/lib/utils/forkConflict.ts @@ -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((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( } 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( diff --git a/frontend/src/routes/(root)/(logged)/+layout.svelte b/frontend/src/routes/(root)/(logged)/+layout.svelte index 38c226e4b8..617dbe3408 100644 --- a/frontend/src/routes/(root)/(logged)/+layout.svelte +++ b/frontend/src/routes/(root)/(logged)/+layout.svelte @@ -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 @@ {/if} + +