diff --git a/src/routes/v2/pages/Editor/components/EditorMenuBar/EditorMenuBar.tsx b/src/routes/v2/pages/Editor/components/EditorMenuBar/EditorMenuBar.tsx
index a82f3b6b6..cac8dd206 100644
--- a/src/routes/v2/pages/Editor/components/EditorMenuBar/EditorMenuBar.tsx
+++ b/src/routes/v2/pages/Editor/components/EditorMenuBar/EditorMenuBar.tsx
@@ -1,13 +1,18 @@
+import { useTour } from "@reactour/tour";
+import { useNavigate } from "@tanstack/react-router";
import { observer } from "mobx-react-lite";
import { useState } from "react";
import logo from "/Tangle_Icon_White.png";
import { PipelineNameDialog } from "@/components/shared/Dialogs";
+import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Icon } from "@/components/ui/icon";
import { BlockStack, InlineStack } from "@/components/ui/layout";
import { Link } from "@/components/ui/link";
import { Text } from "@/components/ui/typography";
+import { useTourMode } from "@/providers/TourProvider/TourModeContext";
+import { APP_ROUTES } from "@/routes/router";
import { usePipelineRename } from "@/routes/v2/pages/Editor/hooks/usePipelineRename";
import { useEditorSession } from "@/routes/v2/pages/Editor/store/EditorSessionContext";
import { AppMenuActions } from "@/routes/v2/shared/components/AppMenuActions";
@@ -28,15 +33,26 @@ export const EditorMenuBar = observer(function EditorMenuBar() {
const { navigation } = useSharedStores();
const { pipelineFile } = useEditorSession();
const handlePipelineRename = usePipelineRename();
+ const tourMode = useTourMode();
+ const { setIsOpen: setTourPopupOpen } = useTour();
+ const navigate = useNavigate();
+
const spec = navigation.activeSpec;
- const pipelineName = spec?.name ?? "Untitled pipeline";
+ const pipelineNameFromSpec = spec?.name ?? "Untitled pipeline";
+ const displayName =
+ tourMode?.tour.displayName ?? tourMode?.tour.id ?? pipelineNameFromSpec;
const displayMenu = Boolean(pipelineFile.activePipelineFile);
const [renameOpen, setRenameOpen] = useState(false);
+ const handleExitTour = () => {
+ setTourPopupOpen(false);
+ void navigate({ to: APP_ROUTES.LEARN_TOURS });
+ };
+
return (
- {pipelineName}
+ {displayName}
-
+ {tourMode && (
+
+ Tour
+
+ )}
+ {!tourMode && (
+
+ )}
-
name === pipelineName}
- />
+ {!tourMode && (
+ name === pipelineNameFromSpec}
+ />
+ )}
@@ -114,6 +139,20 @@ export const EditorMenuBar = observer(function EditorMenuBar() {
)}
+ {tourMode && (
+
+ )}
+
@@ -96,15 +98,17 @@ export function FileMenu() {
Save as
- {
- track("v2.pipeline_editor.file_menu.rename.click");
- setRenameDialogOpen(true);
- }}
- >
-
- Rename
-
+ {!tourMode && (
+ {
+ track("v2.pipeline_editor.file_menu.rename.click");
+ setRenameDialogOpen(true);
+ }}
+ >
+
+ Rename
+
+ )}
{
@@ -147,17 +151,21 @@ export function FileMenu() {
>
)}
-
- {
- track("v2.pipeline_editor.file_menu.delete_pipeline.click");
- setDeleteDialogOpen(true);
- }}
- className="text-destructive focus:text-destructive"
- >
-
- Delete pipeline
-
+ {!tourMode && (
+ <>
+
+ {
+ track("v2.pipeline_editor.file_menu.delete_pipeline.click");
+ setDeleteDialogOpen(true);
+ }}
+ className="text-destructive focus:text-destructive"
+ >
+
+ Delete pipeline
+
+ >
+ )}
diff --git a/src/routes/v2/pages/Editor/components/EditorTourBridge.tsx b/src/routes/v2/pages/Editor/components/EditorTourBridge.tsx
new file mode 100644
index 000000000..ecc49762e
--- /dev/null
+++ b/src/routes/v2/pages/Editor/components/EditorTourBridge.tsx
@@ -0,0 +1,3 @@
+export function EditorTourBridge() {
+ return null;
+}
diff --git a/src/routes/v2/shared/windows/windowPersistence.ts b/src/routes/v2/shared/windows/windowPersistence.ts
index 6e40d20d6..82be0a5bd 100644
--- a/src/routes/v2/shared/windows/windowPersistence.ts
+++ b/src/routes/v2/shared/windows/windowPersistence.ts
@@ -22,9 +22,60 @@ import type { WindowStoreImpl } from "./windowStore";
*/
let activeLayoutId: string | null = null;
+function getLayoutStorageKey(layoutId: string | null): string {
+ if (!layoutId) return "editorV2-window-layout";
+ return `window-layout-${layoutId}`;
+}
+
function getStorageKey(): string {
- if (!activeLayoutId) return "editorV2-window-layout";
- return `window-layout-${activeLayoutId}`;
+ return getLayoutStorageKey(activeLayoutId);
+}
+
+function snapshotStorageKey(layoutId: string): string {
+ return `${getLayoutStorageKey(layoutId)}-snapshot`;
+}
+
+function snapshotActiveKey(layoutId: string): string {
+ return `${snapshotStorageKey(layoutId)}-active`;
+}
+
+// Stashes the layout aside so the next mount starts from defaults. Pair with
+// restoreLayout to roll back.
+export function snapshotLayout(layoutId: string): void {
+ try {
+ const key = getLayoutStorageKey(layoutId);
+ const current = localStorage.getItem(key);
+ if (current !== null) {
+ localStorage.setItem(snapshotStorageKey(layoutId), current);
+ } else {
+ localStorage.removeItem(snapshotStorageKey(layoutId));
+ }
+ localStorage.setItem(snapshotActiveKey(layoutId), "1");
+ localStorage.removeItem(key);
+ } catch (error) {
+ console.warn(`Failed to snapshot layout "${layoutId}":`, error);
+ }
+}
+
+export function restoreLayout(layoutId: string): boolean {
+ try {
+ if (localStorage.getItem(snapshotActiveKey(layoutId)) === null) {
+ return false;
+ }
+ const key = getLayoutStorageKey(layoutId);
+ const saved = localStorage.getItem(snapshotStorageKey(layoutId));
+ if (saved !== null) {
+ localStorage.setItem(key, saved);
+ } else {
+ localStorage.removeItem(key);
+ }
+ localStorage.removeItem(snapshotStorageKey(layoutId));
+ localStorage.removeItem(snapshotActiveKey(layoutId));
+ return true;
+ } catch (error) {
+ console.warn(`Failed to restore layout "${layoutId}":`, error);
+ return false;
+ }
}
interface PersistedWindowState {