mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
fix(webapp): a bogus project deep link says so instead of swapping projects (BEA-83) (#131)
/no-such-project-xyz/some/file.md used to redirect to whichever project the fallback chain picked, dropping the path — nothing on screen distinguished "that link is wrong" from "you opened your project." An unknown route.project now renders a "Project not found" panel at the URL as typed, mirroring the org-not-found page two blocks up: shell, sidebar and account bar stay mounted, and "Back to <name>" points at the same project the fallback picks today. The fallback chain itself is unchanged, and "/" still redirects to the remembered project (BEA-75). All four redirects below the flag rewrite the address bar off current.id, so they are gated together — /bad-id/insights and /bad-id/notes/ would otherwise undo the fix on their own. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e19fec0534
commit
32a099997e
@@ -808,6 +808,24 @@ test("an old version of an extensionless file previews the same way", async ({ p
|
|||||||
await expect(page.locator("#content .empty")).toContainText("That version isn't available.");
|
await expect(page.locator("#content .empty")).toContainText("That version isn't available.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// BEA-83. A deep link to a project id you can't see used to swap in another
|
||||||
|
// project and throw the path away, with nothing on screen to say so.
|
||||||
|
test("a bogus project deep link says so and keeps the URL", async ({ page }) => {
|
||||||
|
await login(page);
|
||||||
|
await page.goto("/no-such-project-xyz/some/file.md");
|
||||||
|
await expect(page.locator("#content .empty")).toContainText("Project not found");
|
||||||
|
expect(page.url()).toContain("/no-such-project-xyz/some/file.md");
|
||||||
|
await expect(page.locator("#sidebar")).toBeVisible();
|
||||||
|
await page.reload(); // no bounce, no loop
|
||||||
|
await expect(page.locator("#content .empty")).toContainText("Project not found");
|
||||||
|
expect(page.url()).toContain("/no-such-project-xyz/some/file.md");
|
||||||
|
// The two other URL rewrites off current.id must not undo the fix either.
|
||||||
|
await page.goto("/no-such-project-xyz/insights");
|
||||||
|
expect(page.url()).toContain("/no-such-project-xyz/insights");
|
||||||
|
await page.goto("/no-such-project-xyz/notes/");
|
||||||
|
expect(page.url()).toContain("/no-such-project-xyz/notes/");
|
||||||
|
});
|
||||||
|
|
||||||
// BEA-81: an old URL for a file that has since been renamed or dragged into
|
// BEA-81: an old URL for a file that has since been renamed or dragged into
|
||||||
// a folder still lands on the file, rewrites itself, and says what happened.
|
// a folder still lands on the file, rewrites itself, and says what happened.
|
||||||
test("a moved file's old URL redirects and says so", async ({ page }) => {
|
test("a moved file's old URL redirects and says so", async ({ page }) => {
|
||||||
|
|||||||
@@ -22,10 +22,14 @@ test("deep link to a project resolves after reload", async ({ page }) => {
|
|||||||
await expect(page).toHaveURL("/" + pid);
|
await expect(page).toHaveURL("/" + pid);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("unknown project id falls back to a real project", async ({ page }) => {
|
// BEA-83 replaced the silent fallback with a not-found page: the sidebar
|
||||||
|
// still shows a real project (the fallback chain is unchanged), but the URL
|
||||||
|
// you typed stays put and the content pane says the id resolved to nothing.
|
||||||
|
test("unknown project id says so instead of swapping projects", async ({ page }) => {
|
||||||
await login(page);
|
await login(page);
|
||||||
await page.goto("/p-00000000");
|
await page.goto("/p-00000000");
|
||||||
await page.waitForURL(/\/[0-9a-f-]{36}$/);
|
await expect(page.locator("#content .empty")).toContainText("Project not found");
|
||||||
|
await expect(page).toHaveURL("/p-00000000");
|
||||||
await expect(page.locator("#project-select")).toContainText(/.+/);
|
await expect(page.locator("#project-select")).toContainText(/.+/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -202,6 +202,25 @@ export default function HubApp({ config }: { config: ServerConfig }) {
|
|||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
// Same rule as orgMissing, for the project id: a deep link to an id that is
|
||||||
|
// not yours is not a landing. `current` still resolves (the fallback chain
|
||||||
|
// above is untouched) — it is what the sidebar shows and where "back" points.
|
||||||
|
const projectMissing = !!route.project && !projects.some((p) => p.id === route.project);
|
||||||
|
const projectPage = projectMissing
|
||||||
|
? {
|
||||||
|
crumb: "Project",
|
||||||
|
body: (
|
||||||
|
<div className="empty">
|
||||||
|
<h3>Project not found</h3>
|
||||||
|
<p>This project doesn't exist, or you're no longer a member.</p>
|
||||||
|
<p>
|
||||||
|
<a {...linkProps("/" + current.id)}>Back to {current.name}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
// Billing is hub-level (the managed deployment's surface), not
|
// Billing is hub-level (the managed deployment's surface), not
|
||||||
// project-scoped — like the org route it borrows whichever project the
|
// project-scoped — like the org route it borrows whichever project the
|
||||||
// sidebar is showing. An OSS hub has no billing block; a hand-typed
|
// sidebar is showing. An OSS hub has no billing block; a hand-typed
|
||||||
@@ -249,35 +268,38 @@ export default function HubApp({ config }: { config: ServerConfig }) {
|
|||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
// Landing ("/") and unknown project ids both resolve to a real project
|
// Every redirect below rewrites the address bar off `current.id`, so all of
|
||||||
// URL; replace so back/forward never bounces through the redirect. The
|
// them are wrong for a bogus deep link: /bad-id, /bad-id/insights and
|
||||||
// org route is not project-scoped, so it is exempt — it borrows whichever
|
// /bad-id/notes/ would each swap in another project and drop the path.
|
||||||
// project the sidebar is showing.
|
// projectMissing renders instead (projectPage above) at the URL as typed.
|
||||||
if (!route.org && !route.billing && route.project !== current.id) {
|
if (!projectMissing) {
|
||||||
return <Redirect to={"/" + current.id} />;
|
// Landing ("/") resolves to a real project URL; replace so back/forward
|
||||||
}
|
// never bounces through the redirect. The org route is not project-scoped,
|
||||||
|
// so it is exempt — it borrows whichever project the sidebar is showing.
|
||||||
|
if (!route.org && !route.billing && route.project !== current.id) {
|
||||||
|
return <Redirect to={"/" + current.id} />;
|
||||||
|
}
|
||||||
|
|
||||||
// A renamed view URL (/insights) still resolves; swap it for the current
|
// A renamed view URL (/insights) still resolves; swap it for the current
|
||||||
// one so there is one live URL per page. Filters ride along: the hop is a
|
// one so there is one live URL per page. Filters ride along: the hop is a
|
||||||
// rename, not a reset, and dropping them would silently widen the feed.
|
// rename, not a reset, and dropping them would silently widen the feed.
|
||||||
if (route.legacyView && route.view) {
|
if (route.legacyView && route.view) {
|
||||||
return <Redirect to={urlForView(route.view, current.id, route.viewTarget, route.filters)} />;
|
return <Redirect to={urlForView(route.view, current.id, route.viewTarget, route.filters)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// /history?path=guide.md resolved to guide.md's feed (the query form is
|
// /history?path=guide.md resolved to guide.md's feed (the query form is
|
||||||
// what the History API teaches); put the canonical path URL in the address
|
// what the History API teaches); put the canonical path URL in the address
|
||||||
// bar. Below the unknown-project redirect for the same reason the trailing
|
// bar.
|
||||||
// slash one is: normalizing on a bad project id would pin the wrong project.
|
if (route.queryTarget && route.view) {
|
||||||
if (route.queryTarget && route.view) {
|
return <Redirect to={urlForView(route.view, current.id, route.viewTarget, route.filters)} />;
|
||||||
return <Redirect to={urlForView(route.view, current.id, route.viewTarget, route.filters)} />;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// /notes/ is the same page as /notes — resolve it, then take the slash off
|
// /notes/ is the same page as /notes — resolve it, then take the slash off
|
||||||
// the address bar. After the rewrite the flag is false, so there is no
|
// the address bar. After the rewrite the flag is false, so there is no
|
||||||
// second hop. Must stay below the unknown-project redirect above, or a bad
|
// second hop.
|
||||||
// project id would be normalized on the path and keep the wrong project.
|
if (route.trailingSlash && route.path) {
|
||||||
if (route.trailingSlash && route.path) {
|
return <Redirect to={urlForPath(route.path, current.id, route.version)} />;
|
||||||
return <Redirect to={urlForPath(route.path, current.id, route.version)} />;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -339,7 +361,7 @@ export default function HubApp({ config }: { config: ServerConfig }) {
|
|||||||
),
|
),
|
||||||
orgBar: accountBar,
|
orgBar: accountBar,
|
||||||
}}
|
}}
|
||||||
panel={activePanel || orgPage || billingPage || routePage}
|
panel={activePanel || orgPage || projectPage || billingPage || routePage}
|
||||||
onClosePanel={() => setPanel(null)}
|
onClosePanel={() => setPanel(null)}
|
||||||
/>
|
/>
|
||||||
{newProjectDialog}
|
{newProjectDialog}
|
||||||
|
|||||||
+14
-14
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>BearDrive</title>
|
<title>BearDrive</title>
|
||||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='%23f5a623'><rect x='4' y='4' width='5.6' height='24'/><rect x='11.2' y='4' width='14.4' height='11.2'/><rect x='11.2' y='16.8' width='16.8' height='11.2'/></svg>">
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32' fill='%23f5a623'><rect x='4' y='4' width='5.6' height='24'/><rect x='11.2' y='4' width='14.4' height='11.2'/><rect x='11.2' y='16.8' width='16.8' height='11.2'/></svg>">
|
||||||
<script type="module" crossorigin src="/assets/index-u2Ih5Asg.js"></script>
|
<script type="module" crossorigin src="/assets/index-C64R_Rr_.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BdCy9HmN.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BdCy9HmN.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
Reference in New Issue
Block a user