refactor(drafts): extract DraftEditorModals trailer block

The four editor routes (scripts/flows/apps/apps_raw) mounted an
identical pair of trailer modals — DraftSyncConflictModal +
OtherUsersDraftsModal — wrapped in the same guard chain and {#key path}
remount. Lift the markup into one component; routes thread their
itemKind, path, editPathFor, and loader callback.

Pure markup extraction, no state ownership change. Drops the unused
userStore import where the trailer was the only consumer.
This commit is contained in:
Diego Imbert
2026-06-08 15:22:08 +02:00
parent e71644500c
commit 6d263bdf1e
5 changed files with 115 additions and 95 deletions
@@ -0,0 +1,67 @@
<script lang="ts">
/**
* The two modals every editor route mounts at its trailer:
* - DraftSyncConflictModal: surfaces a 409 from the autosave pipeline.
* The route supplies `onLoadFromServer` (re-fetches the deployed-overlay
* response and re-seeds editor state) and a `getLocalDraft` getter that
* snapshots the cell value for the overwrite branch.
* - OtherUsersDraftsModal: surfaces the list of other workspace users (or
* the legacy NULL-email row) with a draft at the same path, offered for
* forking. Open-gated on `otherDraftsUsers.length > 0`.
*
* Gates the whole block on `enabled` (defaults to true) — the scripts
* route uses this to suppress when viewing a specific hash.
*
* Wrap the OtherUsersDraftsModal in `{#key path}` so navigating between
* editor paths fully remounts the modal (carrying its `jsonOpen` /
* fork-target state with it) instead of trying to recompute its
* `forkPath` derivation for a new path mid-flight.
*/
import { userStore } from '$lib/stores'
import type { UserDraftItemKind } from '$lib/gen'
import DraftSyncConflictModal from './DraftSyncConflictModal.svelte'
import OtherUsersDraftsModal, { type OtherDraftUser } from './OtherUsersDraftsModal.svelte'
type Props = {
workspace: string
itemKind: UserDraftItemKind
path: string
otherDraftsUsers: OtherDraftUser[]
editPathFor: (forkedPath: string) => string
onLoadFromServer: () => void | Promise<void>
getLocalDraft: () => unknown
/** Defaults to true; set to false to suppress both modals. */
enabled?: boolean
}
let {
workspace,
itemKind,
path,
otherDraftsUsers,
editPathFor,
onLoadFromServer,
getLocalDraft,
enabled = true
}: Props = $props()
</script>
{#if enabled && workspace && path}
<DraftSyncConflictModal
query={{ workspace, itemKind, path }}
{onLoadFromServer}
{getLocalDraft}
/>
{#if otherDraftsUsers.length > 0}
{#key path}
<OtherUsersDraftsModal
{workspace}
{itemKind}
{path}
currentUserUsername={$userStore?.username}
{otherDraftsUsers}
{editPathFor}
/>
{/key}
{/if}
{/if}
@@ -1,17 +1,15 @@
<script lang="ts">
import AppEditor from '$lib/components/apps/editor/AppEditor.svelte'
import { AppService, type AppWithLastVersion } from '$lib/gen'
import { userStore, workspaceStore } from '$lib/stores'
import { workspaceStore } from '$lib/stores'
import { replaceState } from '$app/navigation'
import { goto } from '$lib/navigation'
import { sendUserToast } from '$lib/toast'
import DiffDrawer from '$lib/components/DiffDrawer.svelte'
import type { App } from '$lib/components/apps/types'
import DraftSyncConflictModal from '$lib/components/common/confirmationModal/DraftSyncConflictModal.svelte'
import DraftEditorModals from '$lib/components/common/confirmationModal/DraftEditorModals.svelte'
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
import OtherUsersDraftsModal, {
type OtherDraftUser
} from '$lib/components/common/confirmationModal/OtherUsersDraftsModal.svelte'
import { type OtherDraftUser } from '$lib/components/common/confirmationModal/OtherUsersDraftsModal.svelte'
import { stateSnapshot } from '$lib/svelte5Utils.svelte'
import { emptyApp } from '$lib/components/apps/editor/appUtils'
import { untrack } from 'svelte'
@@ -221,25 +219,15 @@
</script>
<DiffDrawer bind:this={diffDrawer} {restoreDeployed} />
{#if $workspaceStore && path}
<DraftSyncConflictModal
query={{ workspace: $workspaceStore, itemKind: 'app', path }}
onLoadFromServer={() => loadApp()}
getLocalDraft={() => app?.value}
/>
{/if}
{#if $workspaceStore && path && otherDraftsUsers.length > 0}
{#key path}
<OtherUsersDraftsModal
workspace={$workspaceStore}
itemKind="app"
{path}
currentUserUsername={$userStore?.username}
{otherDraftsUsers}
editPathFor={(forkedPath) => `/apps/edit/${forkedPath}`}
/>
{/key}
{/if}
<DraftEditorModals
workspace={$workspaceStore ?? ''}
itemKind="app"
{path}
{otherDraftsUsers}
editPathFor={(forkedPath) => `/apps/edit/${forkedPath}`}
onLoadFromServer={() => loadApp()}
getLocalDraft={() => app?.value}
/>
{#key redraw}
{#if app}
@@ -15,11 +15,9 @@
import { type RawAppData, DEFAULT_DATA } from '$lib/components/raw_apps/dataTableRefUtils'
import { UserDraft } from '$lib/userDraft.svelte'
import { notifyDraftLoaded } from '$lib/userDraftToast'
import DraftSyncConflictModal from '$lib/components/common/confirmationModal/DraftSyncConflictModal.svelte'
import DraftEditorModals from '$lib/components/common/confirmationModal/DraftEditorModals.svelte'
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
import OtherUsersDraftsModal, {
type OtherDraftUser
} from '$lib/components/common/confirmationModal/OtherUsersDraftsModal.svelte'
import { type OtherDraftUser } from '$lib/components/common/confirmationModal/OtherUsersDraftsModal.svelte'
import RawAppTemplatePicker, {
type RawAppTemplatePickerResult
} from '$lib/components/raw_apps/RawAppTemplatePicker.svelte'
@@ -382,25 +380,15 @@
</script>
<DiffDrawer bind:this={diffDrawer} {restoreDeployed} />
{#if $workspaceStore && path}
<DraftSyncConflictModal
query={{ workspace: $workspaceStore, itemKind: 'raw_app', path }}
onLoadFromServer={() => loadApp()}
getLocalDraft={() => draftHandle.draft}
/>
{/if}
{#if $workspaceStore && path && otherDraftsUsers.length > 0}
{#key path}
<OtherUsersDraftsModal
workspace={$workspaceStore}
itemKind="raw_app"
{path}
currentUserUsername={$userStore?.username}
{otherDraftsUsers}
editPathFor={(forkedPath) => `/apps_raw/edit/${forkedPath}`}
/>
{/key}
{/if}
<DraftEditorModals
workspace={$workspaceStore ?? ''}
itemKind="raw_app"
{path}
{otherDraftsUsers}
editPathFor={(forkedPath) => `/apps_raw/edit/${forkedPath}`}
onLoadFromServer={() => loadApp()}
getLocalDraft={() => draftHandle.draft}
/>
<RawAppTemplatePicker bind:open={templatePicker} onStart={onTemplatePickerStart} />
@@ -3,18 +3,16 @@
import FlowBuilder from '$lib/components/FlowBuilder.svelte'
import { editPathFor, invalidate } from '$lib/components/workspacePicker'
import { initialArgsStore, userStore, workspaceStore } from '$lib/stores'
import { initialArgsStore, workspaceStore } from '$lib/stores'
import { decodeState, emptySchema, type StateStore } from '$lib/utils'
import { initFlow } from '$lib/components/flows/flowStore.svelte'
import { goto } from '$lib/navigation'
import { sendUserToast } from '$lib/toast'
import DiffDrawer from '$lib/components/DiffDrawer.svelte'
import DraftSyncConflictModal from '$lib/components/common/confirmationModal/DraftSyncConflictModal.svelte'
import DraftEditorModals from '$lib/components/common/confirmationModal/DraftEditorModals.svelte'
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
import OtherUsersDraftsModal, {
type OtherDraftUser
} from '$lib/components/common/confirmationModal/OtherUsersDraftsModal.svelte'
import { type OtherDraftUser } from '$lib/components/common/confirmationModal/OtherUsersDraftsModal.svelte'
import type { ScheduleTrigger } from '$lib/components/triggers'
import type { Trigger } from '$lib/components/triggers/utils'
import { tick, untrack } from 'svelte'
@@ -317,25 +315,15 @@
<!-- <div id="monaco-widgets-root" class="monaco-editor" style="z-index: 1200;" /> -->
<DiffDrawer bind:this={diffDrawer} {restoreDeployed} isFlow />
{#if $workspaceStore && flowDraftPath}
<DraftSyncConflictModal
query={{ workspace: $workspaceStore, itemKind: 'flow', path: flowDraftPath }}
onLoadFromServer={() => loadFlow()}
getLocalDraft={() => flowHandle.draft}
/>
{/if}
{#if $workspaceStore && flowDraftPath && otherDraftsUsers.length > 0}
{#key flowDraftPath}
<OtherUsersDraftsModal
workspace={$workspaceStore}
itemKind="flow"
path={flowDraftPath}
currentUserUsername={$userStore?.username}
{otherDraftsUsers}
editPathFor={(forkedPath) => `/flows/edit/${forkedPath}`}
/>
{/key}
{/if}
<DraftEditorModals
workspace={$workspaceStore ?? ''}
itemKind="flow"
path={flowDraftPath}
{otherDraftsUsers}
editPathFor={(forkedPath) => `/flows/edit/${forkedPath}`}
onLoadFromServer={() => loadFlow()}
getLocalDraft={() => flowHandle.draft}
/>
{#if notFound}
<div class="flex flex-col items-center justify-center h-full">
<h1 class="text-2xl font-bold">Flow not found at path {page.params.path}</h1>
@@ -1,16 +1,15 @@
<script lang="ts">
import { ScriptService, type NewScript, type Script } from '$lib/gen'
import { initialArgsStore, userStore, workspaceStore } from '$lib/stores'
import { initialArgsStore, workspaceStore } from '$lib/stores'
import ScriptBuilder from '$lib/components/ScriptBuilder.svelte'
import { editPathFor, invalidate } from '$lib/components/workspacePicker'
import { emptySchema } from '$lib/utils'
import { goto } from '$lib/navigation'
import { sendUserToast } from '$lib/toast'
import DiffDrawer from '$lib/components/DiffDrawer.svelte'
import OtherUsersDraftsModal, {
type OtherDraftUser
} from '$lib/components/common/confirmationModal/OtherUsersDraftsModal.svelte'
import { type OtherDraftUser } from '$lib/components/common/confirmationModal/OtherUsersDraftsModal.svelte'
import DraftEditorModals from '$lib/components/common/confirmationModal/DraftEditorModals.svelte'
import type { ScheduleTrigger } from '$lib/components/triggers'
import type { Trigger } from '$lib/components/triggers/utils'
import { get } from 'svelte/store'
@@ -19,7 +18,6 @@
import { UserDraft, type UserDraftHandle } from '$lib/userDraft.svelte'
import { notifyDraftLoaded } from '$lib/userDraftToast'
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
import DraftSyncConflictModal from '$lib/components/common/confirmationModal/DraftSyncConflictModal.svelte'
type EditableScript = NewScript & { draft_triggers?: Trigger[] }
@@ -250,25 +248,16 @@
</script>
<DiffDrawer bind:this={diffDrawer} {restoreDeployed} />
{#if !hash && $workspaceStore && page.params.path}
<DraftSyncConflictModal
query={{ workspace: $workspaceStore, itemKind: 'script', path: page.params.path }}
onLoadFromServer={() => loadScript()}
getLocalDraft={() => scriptHandle.draft}
/>
{/if}
{#if !hash && $workspaceStore && page.params.path && otherDraftsUsers.length > 0}
{#key page.params.path}
<OtherUsersDraftsModal
workspace={$workspaceStore}
itemKind="script"
path={page.params.path}
currentUserUsername={$userStore?.username}
{otherDraftsUsers}
editPathFor={(forkedPath) => `/scripts/edit/${forkedPath}`}
/>
{/key}
{/if}
<DraftEditorModals
enabled={!hash}
workspace={$workspaceStore ?? ''}
itemKind="script"
path={page.params.path ?? ''}
{otherDraftsUsers}
editPathFor={(forkedPath) => `/scripts/edit/${forkedPath}`}
onLoadFromServer={() => loadScript()}
getLocalDraft={() => scriptHandle.draft}
/>
{#if scriptHandle.draft && renderEditor}
<ScriptBuilder
bind:this={scriptBuilder}