diff options
| author | Adam Malczewski <[email protected]> | 2026-06-25 11:13:07 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-25 11:13:07 +0900 |
| commit | c5ea2232f117adda740c7e3b8366e9f10f14d3cb (patch) | |
| tree | 39df64c85d8b2ae13506b1db9bf1264dda84992e /src | |
| parent | 4ac993df443db0ecbcdf7c13e39c987b8d1ef28b (diff) | |
| download | dispatch-web-c5ea2232f117adda740c7e3b8366e9f10f14d3cb.tar.gz dispatch-web-c5ea2232f117adda740c7e3b8366e9f10f14d3cb.zip | |
feat(workspaces): open workspace in a new browser tab
The 'Open' button on a workspace card did client-side navigation
(onNavigate -> pushState), opening the workspace in the SAME tab.
Replace the JS click handler with a native anchor using target="_blank"
(+ rel=noopener noreferrer) so clicking Open opens the workspace in a
NEW browser tab. The new tab does a full load and routes to /<id> via
the existing parsePath -> App wiring.
The card no longer needs the onNavigate prop (its only navigation was
the Open button), so drop it from WorkspaceCard + WorkspacesHome and
update the card tests to assert the new-tab link attributes instead of
the onNavigate call.
Diffstat (limited to 'src')
| -rw-r--r-- | src/features/workspaces/ui/WorkspaceCard.svelte | 12 | ||||
| -rw-r--r-- | src/features/workspaces/ui/WorkspaceCard.test.ts | 27 | ||||
| -rw-r--r-- | src/features/workspaces/ui/WorkspacesHome.svelte | 2 |
3 files changed, 17 insertions, 24 deletions
diff --git a/src/features/workspaces/ui/WorkspaceCard.svelte b/src/features/workspaces/ui/WorkspaceCard.svelte index 6348bb9..81d6fe3 100644 --- a/src/features/workspaces/ui/WorkspaceCard.svelte +++ b/src/features/workspaces/ui/WorkspaceCard.svelte @@ -8,11 +8,9 @@ let { ws, store, - onNavigate, }: { ws: WorkspaceEntry; store: WorkspaceStore; - onNavigate: (path: string) => void; } = $props(); // ── Title: double-click to rename inline ────────────────────────────────── @@ -79,10 +77,6 @@ await store.remove(ws.id); deleting = false; } - - function open(): void { - onNavigate(workspacePath(ws.id)); - } </script> <li class="flex flex-col gap-2 rounded-box border border-primary bg-primary/10 p-3"> @@ -163,10 +157,8 @@ <a class="btn" href={workspacePath(ws.id)} - onclick={(e) => { - e.preventDefault(); - open(); - }} + target="_blank" + rel="noopener noreferrer" > Open </a> diff --git a/src/features/workspaces/ui/WorkspaceCard.test.ts b/src/features/workspaces/ui/WorkspaceCard.test.ts index 385856d..0264d5f 100644 --- a/src/features/workspaces/ui/WorkspaceCard.test.ts +++ b/src/features/workspaces/ui/WorkspaceCard.test.ts @@ -45,9 +45,8 @@ function fakeStore() { describe("WorkspaceCard", () => { it("renders the title, slug, and an Open link", () => { const store = fakeStore() as unknown as WorkspaceStore; - const onNavigate = vi.fn(); render(WorkspaceCard, { - props: { ws: fakeEntry(), store, onNavigate }, + props: { ws: fakeEntry(), store }, }); expect(screen.getByText("My Workspace")).toBeInTheDocument(); expect(screen.getByText("/my-ws")).toBeInTheDocument(); @@ -57,7 +56,7 @@ describe("WorkspaceCard", () => { it("double-clicking the title reveals an edit input", async () => { const user = userEvent.setup(); const store = fakeStore() as unknown as WorkspaceStore; - render(WorkspaceCard, { props: { ws: fakeEntry(), store, onNavigate: vi.fn() } }); + render(WorkspaceCard, { props: { ws: fakeEntry(), store } }); await user.dblClick(screen.getByText("My Workspace")); expect(screen.getByLabelText("Workspace title")).toHaveValue("My Workspace"); @@ -66,7 +65,7 @@ describe("WorkspaceCard", () => { it("renames via the store on Enter", async () => { const user = userEvent.setup(); const store = fakeStore() as unknown as WorkspaceStore; - render(WorkspaceCard, { props: { ws: fakeEntry(), store, onNavigate: vi.fn() } }); + render(WorkspaceCard, { props: { ws: fakeEntry(), store } }); await user.dblClick(screen.getByText("My Workspace")); const input = screen.getByLabelText("Workspace title"); @@ -80,7 +79,7 @@ describe("WorkspaceCard", () => { const user = userEvent.setup(); const store = fakeStore() as unknown as WorkspaceStore; render(WorkspaceCard, { - props: { ws: fakeEntry({ defaultCwd: "/old" }), store, onNavigate: vi.fn() }, + props: { ws: fakeEntry({ defaultCwd: "/old" }), store }, }); const input = screen.getByLabelText("Default working directory"); @@ -99,7 +98,7 @@ describe("WorkspaceCard", () => { const user = userEvent.setup(); const store = fakeStore() as unknown as WorkspaceStore; render(WorkspaceCard, { - props: { ws: fakeEntry({ defaultCwd: "/old" }), store, onNavigate: vi.fn() }, + props: { ws: fakeEntry({ defaultCwd: "/old" }), store }, }); const input = screen.getByLabelText("Default working directory"); @@ -109,13 +108,15 @@ describe("WorkspaceCard", () => { expect(store.setDefaultCwd).toHaveBeenCalledWith("my-ws", null); }); - it("the Open link calls onNavigate with the workspace path (no full-card nav)", async () => { - const user = userEvent.setup(); + it("the Open link opens the workspace in a new browser tab (no same-tab navigation)", () => { const store = fakeStore() as unknown as WorkspaceStore; - const onNavigate = vi.fn(); - render(WorkspaceCard, { props: { ws: fakeEntry(), store, onNavigate } }); - - await user.click(screen.getByRole("link", { name: "Open" })); - expect(onNavigate).toHaveBeenCalledWith("/my-ws"); + render(WorkspaceCard, { props: { ws: fakeEntry(), store } }); + + const open = screen.getByRole("link", { name: "Open" }); + // Native new-tab link: href points at the workspace, and target="_blank" + // opens it in a new browser tab rather than client-side navigating. + expect(open).toHaveAttribute("href", "/my-ws"); + expect(open).toHaveAttribute("target", "_blank"); + expect(open.getAttribute("rel") ?? "").toMatch(/noopener/); }); }); diff --git a/src/features/workspaces/ui/WorkspacesHome.svelte b/src/features/workspaces/ui/WorkspacesHome.svelte index 5894f0a..f71c3ba 100644 --- a/src/features/workspaces/ui/WorkspacesHome.svelte +++ b/src/features/workspaces/ui/WorkspacesHome.svelte @@ -78,7 +78,7 @@ {:else} <ul class="flex flex-col gap-2"> {#each store.list as ws (ws.id)} - <WorkspaceCard {ws} {store} {onNavigate} /> + <WorkspaceCard {ws} {store} /> {/each} </ul> {/if} |
