diff --git a/src/routes/v2/pages/Editor/EditorV2.tsx b/src/routes/v2/pages/Editor/EditorV2.tsx index 4d1f9ce63..c9a3f2a59 100644 --- a/src/routes/v2/pages/Editor/EditorV2.tsx +++ b/src/routes/v2/pages/Editor/EditorV2.tsx @@ -50,6 +50,8 @@ import { useSelectionWindowSync } from "./hooks/useSelectionWindowSync"; import { useSpecLifecycle } from "./hooks/useSpecLifecycle"; import { useTipOfTheDayWindow } from "./hooks/useTipOfTheDayWindow"; import { useUndoRedoKeyboard } from "./hooks/useUndoRedoKeyboard"; +import { reconcileModeStore } from "./lineage/reconcileModeStore"; +import { ReconcileNavigationGuard } from "./lineage/ReconcileNavigationGuard"; import { ReconcileOverviewHost } from "./lineage/ReconcileOverviewHost"; import { useReconcileFromUrl } from "./lineage/useReconcileFromUrl"; import { editorRegistry } from "./nodes"; @@ -131,6 +133,12 @@ const PipelineEditor = withSuspenseWrapper( PipelineEditorSkeleton, ); +/** Hides the editor menu bar (and its navigation escape hatches) while in + * reconcile mode, leaving the canvas and reconcile banner. */ +const EditorTopBar = observer(function EditorTopBar() { + return reconcileModeStore.active ? null : ; +}); + function EditorV2Content({ pipelineRef }: { pipelineRef: PipelineRef | null }) { const { navigation } = useSharedStores(); @@ -143,7 +151,8 @@ function EditorV2Content({ pipelineRef }: { pipelineRef: PipelineRef | null }) { return ( - + + {pipelineRef ? ( diff --git a/src/routes/v2/pages/Editor/lineage/ReconcileNavigationGuard.tsx b/src/routes/v2/pages/Editor/lineage/ReconcileNavigationGuard.tsx new file mode 100644 index 000000000..bc3c4630c --- /dev/null +++ b/src/routes/v2/pages/Editor/lineage/ReconcileNavigationGuard.tsx @@ -0,0 +1,65 @@ +import { useBlocker } from "@tanstack/react-router"; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; + +import { reconcileModeStore } from "./reconcileModeStore"; + +/** + * Guards against accidentally leaving reconcile mode. While reconciling, any + * navigation that doesn't stay within the active session (the return-to overview + * or another reconcile target) is intercepted with a Tangle-styled prompt; + * `enableBeforeUnload` additionally covers refresh / tab close (browser-native). + */ +export function ReconcileNavigationGuard() { + const blocker = useBlocker({ + shouldBlockFn: ({ next }) => { + const session = reconcileModeStore.session; + if (!session) return false; + const search = (next.search ?? {}) as Record; + const staysInFlow = + search.reconcile === session.sessionId || + search.reconcileOverview === session.sessionId; + return !staysInFlow; + }, + enableBeforeUnload: () => reconcileModeStore.active, + withResolver: true, + }); + + if (blocker.status !== "blocked") return null; + + return ( + + + + Leave reconcile? + + You’re about to leave to {blocker.next.pathname}. Staged, unsaved + changes in this pipeline will be discarded. + + + + blocker.reset()}> + Return to reconcile + + { + reconcileModeStore.exit(); + blocker.proceed(); + }} + > + Continue anyway + + + + + ); +} diff --git a/src/routes/v2/pages/Editor/lineage/useReconcileFromUrl.ts b/src/routes/v2/pages/Editor/lineage/useReconcileFromUrl.ts index ce91fb834..9d05d8f1c 100644 --- a/src/routes/v2/pages/Editor/lineage/useReconcileFromUrl.ts +++ b/src/routes/v2/pages/Editor/lineage/useReconcileFromUrl.ts @@ -1,6 +1,8 @@ import { useSearch } from "@tanstack/react-router"; import { useEffect } from "react"; +import { focusModeStore } from "@/routes/v2/shared/hooks/useFocusMode"; + import { reconcileModeStore } from "./reconcileModeStore"; import { getReconcileSession, @@ -25,16 +27,18 @@ export function useReconcileFromUrl(): void { }, []); useEffect(() => { - if (!sessionId) { - reconcileModeStore.exit(); - return; - } - const session = getReconcileSession(sessionId); + const session = sessionId ? getReconcileSession(sessionId) : undefined; if (session) { reconcileModeStore.enter(session); + // Reuse focus mode to hide dock panels while reconciling (shared store — + // keeps the architecture's pages → shared dependency direction). + focusModeStore.setActive(true); } else { reconcileModeStore.exit(); } - return () => reconcileModeStore.exit(); + return () => { + reconcileModeStore.exit(); + focusModeStore.setActive(false); + }; }, [sessionId]); }