From abe660d778ee9ff72d95183fc8fa85f428f07d19 Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Mon, 8 Jun 2026 01:36:14 +0200 Subject: [PATCH] =?UTF-8?q?fix(drafts):=20OtherUsersDraftsModal=20?= =?UTF-8?q?=E2=80=94=20close=20on=20Fork,=20don't=20leak=20clicks=20throug?= =?UTF-8?q?h=20nested=20JSON?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs in the per-editor "another user has a draft" banner: - Fork landed the immediate save but didn't close the banner before navigating. Svelte hadn't torn down the previous route's components by the time goto returned, so the banner lingered on top of the destination editor. Comment the explicit isOpen=false on the happy path so it's clear it MUST run before goto. - Clicking anywhere on the screen while the View JSON drilldown was open closed the underlying banner too. Modal2's clickOutside action fired on every Modal2 instance — both the JSON modal and the underlying banner — because both attach their own listener at the document level. Add `closeOnOutsideClick` opt-out on Modal2 and pass `closeOnOutsideClick={!jsonOpen}` to the outer modal so clicks outside the JSON drilldown only close the drilldown. Drive-by: Modal2's keydown handler now ignores Escape when its own isOpen is false (was a no-op closer that would still preventDefault on every key press, swallowing key events for any siblings). --- .../confirmationModal/DraftSyncConflictModal.svelte | 2 +- .../confirmationModal/OtherUsersDraftsModal.svelte | 9 ++++++++- .../src/lib/components/common/modal/Modal2.svelte | 11 ++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/components/common/confirmationModal/DraftSyncConflictModal.svelte b/frontend/src/lib/components/common/confirmationModal/DraftSyncConflictModal.svelte index 772d69d4a9..7ee8c10bcd 100644 --- a/frontend/src/lib/components/common/confirmationModal/DraftSyncConflictModal.svelte +++ b/frontend/src/lib/components/common/confirmationModal/DraftSyncConflictModal.svelte @@ -61,7 +61,7 @@

- Someone else (another tab, browser, or teammate) saved a newer version of this draft. Your + Someone else (another tab, browser or AI Agent) saved a newer version of this draft. Your autosave was rejected to avoid overwriting their work.

{#if conflictHandle.conflict} diff --git a/frontend/src/lib/components/common/confirmationModal/OtherUsersDraftsModal.svelte b/frontend/src/lib/components/common/confirmationModal/OtherUsersDraftsModal.svelte index 0609f16bef..34de1723f8 100644 --- a/frontend/src/lib/components/common/confirmationModal/OtherUsersDraftsModal.svelte +++ b/frontend/src/lib/components/common/confirmationModal/OtherUsersDraftsModal.svelte @@ -97,7 +97,7 @@ // Bypass the autosave debouncer so the fork lands on the // server BEFORE we navigate. The destination route loads // via `getDraft=true` and 404s if no draft yet exists at - // the fork path — the prior `UserDraft.save` call + // the fork path — `UserDraft.save` alone would have // scheduled a debounced POST 1.5s out, so a fresh nav was // always too early. await UserDraftDbSyncer.save({ @@ -107,6 +107,12 @@ value, immediate: true }) + // Close the banner BEFORE the navigation so the user sees the + // modal disappear on click. Without this the modal stays + // visible during the navigation tear-down — Svelte hasn't + // torn down the previous route's components by the time + // `goto` returns, so the banner lingers on top of the + // destination editor for a beat. isOpen = false goto(editPathFor(target)) } catch (e) { @@ -122,6 +128,7 @@ title="Other users are currently working on {path}" fixedWidth="sm" fixedHeight="sm" + closeOnOutsideClick={!jsonOpen} >
diff --git a/frontend/src/lib/components/common/modal/Modal2.svelte b/frontend/src/lib/components/common/modal/Modal2.svelte index 13e36af170..1b29b93b9a 100644 --- a/frontend/src/lib/components/common/modal/Modal2.svelte +++ b/frontend/src/lib/components/common/modal/Modal2.svelte @@ -17,6 +17,11 @@ fixedWidth?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' fixedHeight?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl' contentClasses?: string + /** Close when the user clicks outside the modal body. Default + * true. Set false when the caller stacks a child modal on top + * and clicks "outside" the child would otherwise propagate + * here and close the underlying modal. */ + closeOnOutsideClick?: boolean headerLeft?: import('svelte').Snippet headerRight?: import('svelte').Snippet children?: import('svelte').Snippet @@ -33,6 +38,7 @@ fixedWidth = 'md', fixedHeight = 'md', contentClasses = '', + closeOnOutsideClick = true, headerLeft, headerRight, children @@ -64,6 +70,7 @@ } function handleKeyDown(event: KeyboardEvent) { + if (!isOpen) return if (event.key === 'Escape') { event.preventDefault() event.stopPropagation() @@ -94,7 +101,9 @@ css?.popup?.class, 'wm-modal-form-popup' )} - use:clickOutside={{ onClickOutside: () => close() }} + use:clickOutside={{ + onClickOutside: () => closeOnOutsideClick && close() + }} >