From 7de5507f11eb0d22066a2736ac66cff061c259a6 Mon Sep 17 00:00:00 2001 From: Morgan Wowk Date: Mon, 29 Jun 2026 15:37:44 -0700 Subject: [PATCH] Show editor/legacy switcher on run view pages The EditorVersionToggle is already mounted in both run views' headers (legacy via DefaultAppMenu, v2 via AppMenuActions in RunViewMenuBar), but it only recognized /editor and /editor-v2 paths, so it never rendered on /runs and /runs-v2. Generalize the toggle to swap between any v1/v2 route-base pair, preserving the rest of the path (run id and subgraph execution id), so users can move between the new and legacy run views the same way they do for the editor. --- .../shared/EditorVersionToggle.test.tsx | 81 +++++++++++++++++++ src/components/shared/EditorVersionToggle.tsx | 61 +++++++++----- 2 files changed, 120 insertions(+), 22 deletions(-) create mode 100644 src/components/shared/EditorVersionToggle.test.tsx diff --git a/src/components/shared/EditorVersionToggle.test.tsx b/src/components/shared/EditorVersionToggle.test.tsx new file mode 100644 index 000000000..a385af74b --- /dev/null +++ b/src/components/shared/EditorVersionToggle.test.tsx @@ -0,0 +1,81 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { EditorVersionToggle } from "./EditorVersionToggle"; + +const mockNavigate = vi.fn(); +let mockPathname = "/"; +let mockFlagEnabled = true; + +vi.mock("@tanstack/react-router", async (importOriginal) => ({ + ...(await importOriginal()), + useNavigate: () => mockNavigate, + useLocation: () => ({ pathname: mockPathname }), +})); + +vi.mock("./Settings/useFlags", () => ({ + useFlagValue: () => mockFlagEnabled, +})); + +describe("EditorVersionToggle", () => { + beforeEach(() => { + mockNavigate.mockClear(); + mockPathname = "/"; + mockFlagEnabled = true; + }); + + it("renders nothing when the v2_editor flag is disabled", () => { + mockFlagEnabled = false; + mockPathname = "/editor/my-pipeline"; + + render(); + + expect(screen.queryByRole("button")).toBeNull(); + }); + + it("renders nothing on routes without a v1/v2 counterpart", () => { + mockPathname = "/pipelines"; + + render(); + + expect(screen.queryByRole("button")).toBeNull(); + }); + + it.each([ + ["/editor/my-pipeline", "/editor-v2/my-pipeline", "Switch to new editor"], + ["/runs/run-123", "/runs-v2/run-123", "Switch to new view"], + ["/runs/run-123/sub-456", "/runs-v2/run-123/sub-456", "Switch to new view"], + ])("switches %s to the new version (%s)", (from, to, label) => { + mockPathname = from; + + render(); + + const button = screen.getByRole("button", { name: label }); + fireEvent.click(button); + + expect(mockNavigate).toHaveBeenCalledWith({ to }); + }); + + it.each([ + [ + "/editor-v2/my-pipeline", + "/editor/my-pipeline", + "Switch to legacy editor", + ], + ["/runs-v2/run-123", "/runs/run-123", "Switch to legacy view"], + [ + "/runs-v2/run-123/sub-456", + "/runs/run-123/sub-456", + "Switch to legacy view", + ], + ])("switches %s to the legacy version (%s)", (from, to, label) => { + mockPathname = from; + + render(); + + const button = screen.getByRole("button", { name: label }); + fireEvent.click(button); + + expect(mockNavigate).toHaveBeenCalledWith({ to }); + }); +}); diff --git a/src/components/shared/EditorVersionToggle.tsx b/src/components/shared/EditorVersionToggle.tsx index 35ced979e..47c61cc80 100644 --- a/src/components/shared/EditorVersionToggle.tsx +++ b/src/components/shared/EditorVersionToggle.tsx @@ -2,15 +2,44 @@ import { useLocation, useNavigate } from "@tanstack/react-router"; import TooltipButton from "@/components/shared/Buttons/TooltipButton"; import { Icon } from "@/components/ui/icon"; -import { APP_ROUTES, EDITOR_PATH } from "@/routes/router"; +import { APP_ROUTES, EDITOR_PATH } from "@/routes/appRoutes"; import { useFlagValue } from "./Settings/useFlags"; type EditorVersion = "v1" | "v2"; -const detectEditorVersion = (pathname: string): EditorVersion | null => { - if (pathname.startsWith(`${APP_ROUTES.EDITOR_V2}/`)) return "v2"; - if (pathname.startsWith(`${EDITOR_PATH}/`)) return "v1"; +const ROUTE_BASE_PAIRS: { v1: string; v2: string; noun: string }[] = [ + { v1: EDITOR_PATH, v2: APP_ROUTES.EDITOR_V2, noun: "editor" }, + { v1: APP_ROUTES.RUNS, v2: APP_ROUTES.RUNS_V2, noun: "view" }, +]; + +type ToggleTarget = { + to: string; + tooltip: string; + targetVersion: EditorVersion; +}; + +const getToggleTarget = (pathname: string): ToggleTarget | null => { + for (const { v1, v2, noun } of ROUTE_BASE_PAIRS) { + if (pathname.startsWith(`${v2}/`)) { + const rest = pathname.slice(v2.length + 1); + if (!rest) return null; + return { + to: `${v1}/${rest}`, + tooltip: `Switch to legacy ${noun}`, + targetVersion: "v1", + }; + } + if (pathname.startsWith(`${v1}/`)) { + const rest = pathname.slice(v1.length + 1); + if (!rest) return null; + return { + to: `${v2}/${rest}`, + tooltip: `Switch to new ${noun}`, + targetVersion: "v2", + }; + } + } return null; }; @@ -21,28 +50,16 @@ export const EditorVersionToggle = () => { if (!isEnabled) return null; - const version = detectEditorVersion(location.pathname); - if (!version) return null; - - const lastSegment = location.pathname.split("/").pop() ?? ""; - const pipelineName = decodeURIComponent(lastSegment); - if (!pipelineName) return null; - - const targetVersion = version === "v1" ? "v2" : "v1"; - const targetPath = - targetVersion === "v2" - ? `${APP_ROUTES.EDITOR_V2}/${encodeURIComponent(pipelineName)}` - : `${EDITOR_PATH}/${encodeURIComponent(pipelineName)}`; - const tooltip = - targetVersion === "v2" ? "Switch to new editor" : "Switch to legacy editor"; + const target = getToggleTarget(location.pathname); + if (!target) return null; return ( navigate({ to: targetPath })} - aria-label={tooltip} + tooltip={target.tooltip} + onClick={() => navigate({ to: target.to })} + aria-label={target.tooltip} > - + ); };