mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-22 08:02:40 +00:00
* fix: walk the whole fork ancestry for app installations and fork conflicts Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * docs: describe the fork-conflict gate as ancestor-wide Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * chore: update ee-repo-ref to d252afcc80e77fcc4f9a2a346b80908c8605a6c0 This commit updates the EE repository reference after PR #803 was merged in windmill-ee-private. Previous ee-repo-ref: 5f68c8c351ffc92feccffe69a857b60be376464e New ee-repo-ref: d252afcc80e77fcc4f9a2a346b80908c8605a6c0 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
77 lines
2.6 KiB
TypeScript
77 lines
2.6 KiB
TypeScript
import { forkConflictModal } from '$lib/stores'
|
|
|
|
/**
|
|
* The backend rejects "enable" requests on triggers/schedules in a fork when
|
|
* an upstream workspace (the parent, or an ancestor further up) has the same
|
|
* path. The error body is shaped as
|
|
* `fork-conflict:<kind>:<upstream_workspace_id>`
|
|
* so the UI can show a tailored confirm-to-proceed dialog and re-issue the
|
|
* call with `force: true` if the user agrees.
|
|
*/
|
|
export interface ForkConflict {
|
|
kind: string
|
|
upstreamWorkspaceId: string
|
|
}
|
|
|
|
export function detectForkConflict(e: unknown): ForkConflict | null {
|
|
const body = (e as any)?.body
|
|
const raw =
|
|
typeof body === 'string'
|
|
? body
|
|
: ((body as any)?.error?.message ?? (body as any)?.message ?? (e as any)?.message ?? '')
|
|
const m = String(raw).match(/fork-conflict:([^:]+):(.+)/)
|
|
if (!m) return null
|
|
return { kind: m[1], upstreamWorkspaceId: m[2].trim() }
|
|
}
|
|
|
|
/**
|
|
* 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. If a previous modal is still pending (e.g. user clicked toggles
|
|
* on two rows in quick succession), resolve the older promise to false so
|
|
* the prior caller doesn't hang.
|
|
*/
|
|
function askForkConflictConfirm(kind: string, kindLabel: string, upstreamWorkspaceId: string) {
|
|
return new Promise<boolean>((resolve) => {
|
|
const previous = forkConflictModal.val
|
|
previous?.resolve(false)
|
|
forkConflictModal.val = { kind, kindLabel, upstreamWorkspaceId, 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.
|
|
*
|
|
* Returns `true` when the call committed (no conflict, or user confirmed and
|
|
* retry succeeded) and `false` when the user dismissed the modal. Callers
|
|
* should bail on `false` to skip success toasts and revert any optimistic UI
|
|
* state.
|
|
*
|
|
* `kindLabel` is shown to the user — pass a friendly name like "kafka trigger"
|
|
* or "schedule" so the dialog reads naturally.
|
|
*/
|
|
export async function withForkConflictRetry(
|
|
fn: (force: boolean) => Promise<unknown>,
|
|
kindLabel: string
|
|
): Promise<boolean> {
|
|
try {
|
|
await fn(false)
|
|
return true
|
|
} catch (e) {
|
|
const conflict = detectForkConflict(e)
|
|
if (!conflict) throw e
|
|
const proceed = await askForkConflictConfirm(
|
|
conflict.kind,
|
|
kindLabel,
|
|
conflict.upstreamWorkspaceId
|
|
)
|
|
// User explicitly dismissed the modal — treat as a silent no-op so the
|
|
// caller's catch block doesn't pop a redundant error toast.
|
|
if (!proceed) return false
|
|
await fn(true)
|
|
return true
|
|
}
|
|
}
|