Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions apps/app/src/App.legacy-automation-routes.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// @vitest-environment jsdom

import type { ReactNode } from "react";
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { MemoryRouter, Route, Routes, useLocation } from "react-router-dom";
import {
LegacyAutomationCollectionRedirect,
LegacyAutomationDetailRedirect,
} from "./App";
import {
LEGACY_AUTOMATION_DETAIL_ROUTE_PATH,
LEGACY_AUTOMATIONS_ROUTE_PATH,
LEGACY_TOOLS_AUTOMATION_BROWSE_ROUTE_PATH,
LEGACY_TOOLS_AUTOMATION_DETAIL_ROUTE_PATH,
LEGACY_TOOLS_AUTOMATION_EDIT_ROUTE_PATH,
LEGACY_TOOLS_AUTOMATIONS_ROUTE_PATH,
} from "./lib/route-paths";

function LocationPath() {
const location = useLocation();
return <span>{`${location.pathname}${location.search}`}</span>;
}

function renderRedirect(path: string, pattern: string, element: ReactNode) {
render(
<MemoryRouter initialEntries={[path]}>
<Routes>
<Route path={pattern} element={element} />
<Route path="*" element={<LocationPath />} />
</Routes>
</MemoryRouter>,
);
}

describe("legacy automation route redirects", () => {
afterEach(cleanup);

it.each([
[
"/tools/automations",
LEGACY_TOOLS_AUTOMATIONS_ROUTE_PATH,
"/plugins/automations/automations",
],
[
"/tools/automations?view=browse",
LEGACY_TOOLS_AUTOMATIONS_ROUTE_PATH,
"/plugins/automations/automations/browse",
],
[
"/tools/automations/browse",
LEGACY_TOOLS_AUTOMATION_BROWSE_ROUTE_PATH,
"/plugins/automations/automations/browse",
],
[
"/automations",
LEGACY_AUTOMATIONS_ROUTE_PATH,
"/plugins/automations/automations",
],
])("redirects %s to %s", (from, pattern, to) => {
renderRedirect(from, pattern, <LegacyAutomationCollectionRedirect />);
expect(screen.getByText(to)).toBeTruthy();
});

it.each([
[
"/tools/automations/proj_1/auto_1",
LEGACY_TOOLS_AUTOMATION_DETAIL_ROUTE_PATH,
"/plugins/automations/automations/proj_1/auto_1",
],
[
"/tools/automations/proj_1/auto_1/edit",
LEGACY_TOOLS_AUTOMATION_EDIT_ROUTE_PATH,
"/plugins/automations/automations/proj_1/auto_1/edit",
],
[
"/automations/proj_1/auto_1",
LEGACY_AUTOMATION_DETAIL_ROUTE_PATH,
"/plugins/automations/automations/proj_1/auto_1",
],
])(
"preserves automation identity when redirecting %s",
(from, pattern, to) => {
renderRedirect(from, pattern, <LegacyAutomationDetailRedirect />);
expect(screen.getByText(to)).toBeTruthy();
},
);
});
96 changes: 62 additions & 34 deletions apps/app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { lazy, Suspense } from "react";
import { Navigate, Route, Routes, useParams } from "react-router-dom";
import {
Navigate,
Route,
Routes,
useLocation,
useParams,
} from "react-router-dom";
import { AppLayout } from "./components/layout/AppLayout";
import { AuthCallbackView } from "./views/AuthCallbackView";
import { QuickCreateProjectProvider } from "./hooks/useQuickCreateProject";
Expand All @@ -10,12 +16,14 @@ import { useDesktopThemeSync } from "./hooks/useDesktopThemeSync";
import { usePluginFrontendBoot } from "./hooks/usePluginFrontendBoot";
import { useWebSocket } from "./hooks/useWebSocket";
import {
AUTOMATION_DETAIL_ROUTE_PATH,
AUTOMATIONS_ROUTE_PATH,
AUTH_CALLBACK_ROUTE_PATH,
LEGACY_AUTOMATION_DETAIL_ROUTE_PATH,
LEGACY_AUTOMATIONS_ROUTE_PATH,
LEGACY_SKILLS_ROUTE_PATH,
LEGACY_TOOLS_AUTOMATION_BROWSE_ROUTE_PATH,
LEGACY_TOOLS_AUTOMATION_DETAIL_ROUTE_PATH,
LEGACY_TOOLS_AUTOMATION_EDIT_ROUTE_PATH,
LEGACY_TOOLS_AUTOMATIONS_ROUTE_PATH,
LEGACY_TOOLS_SKILL_DETAIL_ROUTE_PATH,
PROJECT_ARCHIVED_ROUTE_PATH,
PROJECTLESS_ARCHIVED_ROUTE_PATH,
Expand All @@ -26,8 +34,6 @@ import {
SETTINGS_ROUTE_PATH,
SETTINGS_SECTION_ROUTE_PATH,
SKILLS_ROUTE_PATH,
TOOLS_AUTOMATION_BROWSE_ROUTE_PATH,
TOOLS_AUTOMATION_EDIT_ROUTE_PATH,
TOOLS_PLUGIN_BROWSE_ROUTE_PATH,
TOOLS_PLUGIN_DETAIL_ROUTE_PATH,
TOOLS_PLUGINS_ROUTE_PATH,
Expand All @@ -36,6 +42,8 @@ import {
TOOLS_ROUTE_PATH,
TOOLS_SKILL_DETAIL_ROUTE_PATH,
getAutomationDetailRoutePath,
getAutomationEditRoutePath,
getAutomationsRoutePath,
getSettingsRoutePath,
getSkillDetailRoutePath,
} from "./lib/route-paths";
Expand All @@ -60,18 +68,40 @@ const ProjectSettingsView = lazy(() =>
);
const SplitWorkspaceRoute = lazy(() => import("./views/SplitWorkspaceRoute"));

function LegacyAutomationDetailRedirect() {
export function LegacyAutomationDetailRedirect() {
const location = useLocation();
const { projectId, automationId } = useParams<{
projectId?: string;
automationId?: string;
}>();

if (!projectId || !automationId) {
return <Navigate to={AUTOMATIONS_ROUTE_PATH} replace />;
return <Navigate to={getAutomationsRoutePath()} replace />;
}
return (
<Navigate
to={getAutomationDetailRoutePath({ projectId, automationId })}
to={
location.pathname.endsWith("/edit")
? getAutomationEditRoutePath({ projectId, automationId })
: getAutomationDetailRoutePath({ projectId, automationId })
}
replace
/>
);
}

export function LegacyAutomationCollectionRedirect() {
const location = useLocation();
const browse =
location.pathname.endsWith("/browse") ||
new URLSearchParams(location.search).get("view") === "browse";
return (
<Navigate
to={
browse
? `${getAutomationsRoutePath()}/browse`
: getAutomationsRoutePath()
}
replace
/>
);
Expand Down Expand Up @@ -129,6 +159,30 @@ function AppRoutes() {
path={PROJECTLESS_ARCHIVED_ROUTE_PATH}
element={<Navigate to={getSettingsRoutePath("archived")} replace />}
/>
<Route
path={LEGACY_TOOLS_AUTOMATIONS_ROUTE_PATH}
element={<LegacyAutomationCollectionRedirect />}
/>
<Route
path={LEGACY_TOOLS_AUTOMATION_BROWSE_ROUTE_PATH}
element={<LegacyAutomationCollectionRedirect />}
/>
<Route
path={LEGACY_TOOLS_AUTOMATION_DETAIL_ROUTE_PATH}
element={<LegacyAutomationDetailRedirect />}
/>
<Route
path={LEGACY_TOOLS_AUTOMATION_EDIT_ROUTE_PATH}
element={<LegacyAutomationDetailRedirect />}
/>
<Route
path={LEGACY_AUTOMATIONS_ROUTE_PATH}
element={<LegacyAutomationCollectionRedirect />}
/>
<Route
path={LEGACY_AUTOMATION_DETAIL_ROUTE_PATH}
element={<LegacyAutomationDetailRedirect />}
/>
<Route element={<ToolsExperimentGate />}>
<Route
path={TOOLS_ROUTE_PATH}
Expand Down Expand Up @@ -165,36 +219,10 @@ function AppRoutes() {
path={TOOLS_PLUGIN_DETAIL_ROUTE_PATH}
element={<ToolsView />}
/>
<Route path={AUTOMATIONS_ROUTE_PATH} element={<ToolsView />} />
<Route
path={TOOLS_AUTOMATION_BROWSE_ROUTE_PATH}
element={
<Navigate
to={`${AUTOMATIONS_ROUTE_PATH}?view=browse`}
replace
/>
}
/>
<Route
path={AUTOMATION_DETAIL_ROUTE_PATH}
element={<ToolsView />}
/>
<Route
path={TOOLS_AUTOMATION_EDIT_ROUTE_PATH}
element={<ToolsView />}
/>
<Route
path={LEGACY_SKILLS_ROUTE_PATH}
element={<Navigate to={SKILLS_ROUTE_PATH} replace />}
/>
<Route
path={LEGACY_AUTOMATIONS_ROUTE_PATH}
element={<Navigate to={AUTOMATIONS_ROUTE_PATH} replace />}
/>
<Route
path={LEGACY_AUTOMATION_DETAIL_ROUTE_PATH}
element={<LegacyAutomationDetailRedirect />}
/>
</Route>
<Route path="*" element={<SplitWorkspaceRoute />} />
</Routes>
Expand Down
32 changes: 5 additions & 27 deletions apps/app/src/components/layout/AppLayout.tools-breadcrumbs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,13 @@ describe("resolveToolsBreadcrumbs", () => {
to,
})),
).toEqual([
{ id: "skills", label: "Skills", icon: "Zap", to: "/tools/skills" },
{
id: "plugins",
label: "Plugins",
icon: "ElectricPlugs",
to: "/tools/plugins",
},
{
id: "automations",
label: "Automations",
icon: "TimeSchedule",
to: "/tools/automations",
},
{ id: "skills", label: "Skills", icon: "Zap", to: "/tools/skills" },
]);
});

Expand All @@ -43,12 +37,6 @@ describe("resolveToolsBreadcrumbs", () => {
{ label: "Plugins", to: "/tools/plugins" },
{ label: "Installed" },
]);
expect(
resolveToolsBreadcrumbs("/tools/automations", "?view=browse"),
).toEqual([
{ label: "Automations", to: "/tools/automations" },
{ label: "Browse" },
]);
});

it("resolves literal browse paths as Browse, not as a resource named browse", () => {
Expand All @@ -63,10 +51,6 @@ describe("resolveToolsBreadcrumbs", () => {
{ label: "Skills", to: "/tools/skills" },
{ label: "Browse" },
]);
expect(resolveToolsBreadcrumbs("/tools/automations/browse")).toEqual([
{ label: "Automations", to: "/tools/automations" },
{ label: "Browse" },
]);
});

it("makes every detail ancestor clickable and keeps the resource passive", () => {
Expand Down Expand Up @@ -103,15 +87,9 @@ describe("resolveToolsBreadcrumbs", () => {
{ label: "UI Patterns" },
]);
expect(
resolveToolsBreadcrumbs("/tools/automations/personal/weekly-review/edit"),
).toEqual([
{ label: "Automations", to: "/tools/automations" },
{ label: "Installed", to: "/tools/automations" },
{
label: "weekly-review",
to: "/tools/automations/personal/weekly-review",
},
{ label: "Edit" },
]);
resolveToolsBreadcrumbs(
"/plugins/automations/automations/personal/weekly-review",
),
).toBeNull();
});
});
6 changes: 3 additions & 3 deletions apps/app/src/components/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -641,9 +641,9 @@ export function AppLayout({ children }: AppLayoutProps) {
? `Thread ${threadId.slice(0, 8)}`
: "Thread";
// Gated with the rest of the Tools surface: ROOT_ROUTE_ALIASES maps /skills
// and /automations into Tools crumbs, so a gate-off user following an old
// link would otherwise see Tools chrome for the whole config fetch before
// ToolsExperimentGate redirects them away.
// into Tools crumbs, so a gate-off user following an old link would otherwise
// see Tools chrome for the whole config fetch before ToolsExperimentGate
// redirects them away.
const toolsBreadcrumbs = toolsHubEnabled
? resolveToolsBreadcrumbs(
location.pathname,
Expand Down
Loading
Loading