v4: a link from one dump's page to another's opens that dump — it reloaded the one on screen: the page stays mounted when its path changes, and for one render the old dump was still loaded under the new path, so useCanonicalPath took that path for a former one and put the old dump's back, which was then fetched again. A related dump's link did it, and so did any dump link in a note or a comment. The page now drops the dump on screen in the render that sees a path naming another, as usePlaylist and useProfile do. The dump handed over by DumpEdit or a title edit goes with it, so that coming back to its path fetches it instead of matching that snapshot and loading forever.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -79,7 +79,7 @@ export function Dump(): React.JSX.Element {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
// Taken once, since it stays in the history entry, where a reload or Back
|
// Taken once, since it stays in the history entry, where a reload or Back
|
||||||
// would show that snapshot forever. It is cleared from there below.
|
// would show that snapshot forever. It is cleared from there below.
|
||||||
const [handedOver] = useState(
|
const [handedOver, setHandedOver] = useState(
|
||||||
() => (location.state as { dump?: Dump } | null)?.dump ?? null,
|
() => (location.state as { dump?: Dump } | null)?.dump ?? null,
|
||||||
);
|
);
|
||||||
const preloaded = isAt(handedOver, username, slug) ? handedOver : null;
|
const preloaded = isAt(handedOver, username, slug) ? handedOver : null;
|
||||||
@@ -95,6 +95,19 @@ export function Dump(): React.JSX.Element {
|
|||||||
preloaded ? { status: "loaded", dump: preloaded } : { status: "loading" },
|
preloaded ? { status: "loaded", dump: preloaded } : { status: "loading" },
|
||||||
);
|
);
|
||||||
const loadedDump = dumpState.status === "loaded" ? dumpState.dump : null;
|
const loadedDump = dumpState.status === "loaded" ? dumpState.dump : null;
|
||||||
|
// The path last rendered. One that names another dump, as a related dump's
|
||||||
|
// link does, drops the dump on screen in the same render: left for the
|
||||||
|
// fetch below to drop, useCanonicalPath would first put its path back in
|
||||||
|
// the URL. The dump handed over goes with it, so that coming back to its
|
||||||
|
// path fetches it.
|
||||||
|
const [target, setTarget] = useState({ username, slug });
|
||||||
|
if (target.username !== username || target.slug !== slug) {
|
||||||
|
setTarget({ username, slug });
|
||||||
|
if (!isAt(loadedDump, username, slug)) {
|
||||||
|
setHandedOver(null);
|
||||||
|
setDumpState({ status: "loading" });
|
||||||
|
}
|
||||||
|
}
|
||||||
useCanonicalPath(loadedDump && dumpUrl(loadedDump));
|
useCanonicalPath(loadedDump && dumpUrl(loadedDump));
|
||||||
// The dump on screen, which a path it was just given, by useCanonicalPath or
|
// The dump on screen, which a path it was just given, by useCanonicalPath or
|
||||||
// a slug edited elsewhere, needn't fetch again.
|
// a slug edited elsewhere, needn't fetch again.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
import { act, screen, waitFor, within } from "@testing-library/react";
|
import { act, screen, waitFor, within } from "@testing-library/react";
|
||||||
import { userEvent } from "@testing-library/user-event";
|
import { userEvent } from "@testing-library/user-event";
|
||||||
import { Route } from "react-router";
|
import { Link, Route } from "react-router";
|
||||||
import { Dump } from "../../pages/Dump.tsx";
|
import { Dump } from "../../pages/Dump.tsx";
|
||||||
import { DumpEdit } from "../../pages/DumpEdit.tsx";
|
import { DumpEdit } from "../../pages/DumpEdit.tsx";
|
||||||
import { RestrictedLoggedIn } from "../../pages/RestrictedLoggedIn.tsx";
|
import { RestrictedLoggedIn } from "../../pages/RestrictedLoggedIn.tsx";
|
||||||
@@ -225,6 +225,73 @@ describe("Dump page", () => {
|
|||||||
expect(await screen.findByText("Best of")).toBeInTheDocument();
|
expect(await screen.findByText("Best of")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("a related dump's link leads to that dump", async () => {
|
||||||
|
const first = dumpRoutes();
|
||||||
|
const second = makeDump({
|
||||||
|
id: "d2",
|
||||||
|
slug: "next-find",
|
||||||
|
title: "Next find",
|
||||||
|
});
|
||||||
|
route(
|
||||||
|
"GET /api/users/:u/dumps/:slug",
|
||||||
|
({ params }) => wire(params.slug === second.slug ? second : first),
|
||||||
|
);
|
||||||
|
route(
|
||||||
|
"GET /api/dumps/:id/related",
|
||||||
|
({ params }) => wire(params.id === first.id ? [second] : []),
|
||||||
|
);
|
||||||
|
const { user } = openDump();
|
||||||
|
await user.click(await screen.findByRole("link", { name: "Next find" }));
|
||||||
|
expect(await screen.findByRole("heading", { name: "Next find", level: 1 }))
|
||||||
|
.toBeInTheDocument();
|
||||||
|
expect(currentPath()).toBe("/@owner/dumps/next-find");
|
||||||
|
// Its parts are the new dump's.
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.queryByRole("heading", { name: "Related" })).not
|
||||||
|
.toBeInTheDocument()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a dump handed over is fetched when come back to", async () => {
|
||||||
|
const first = dumpRoutes();
|
||||||
|
const second = makeDump({
|
||||||
|
id: "d2",
|
||||||
|
slug: "next-find",
|
||||||
|
title: "Next find",
|
||||||
|
});
|
||||||
|
route(
|
||||||
|
"GET /api/users/:u/dumps/:slug",
|
||||||
|
({ params }) => wire(params.slug === second.slug ? second : first),
|
||||||
|
);
|
||||||
|
route(
|
||||||
|
"GET /api/dumps/:id/related",
|
||||||
|
({ params }) => wire([params.id === first.id ? second : first]),
|
||||||
|
);
|
||||||
|
const user = userEvent.setup();
|
||||||
|
renderApp(<Dump />, {
|
||||||
|
route: "/start",
|
||||||
|
path: "/:handle/dumps/:slug",
|
||||||
|
extraRoutes: (
|
||||||
|
<Route
|
||||||
|
path="/start"
|
||||||
|
element={
|
||||||
|
<Link to="/@owner/dumps/a-find" state={{ dump: first }}>open</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
FakeWebSocket.last.welcome();
|
||||||
|
await user.click(screen.getByRole("link", { name: "open" }));
|
||||||
|
await user.click(await screen.findByRole("link", { name: "Next find" }));
|
||||||
|
await user.click(await screen.findByRole("link", { name: "A find" }));
|
||||||
|
expect(await screen.findByRole("heading", { name: "A find", level: 1 }))
|
||||||
|
.toBeInTheDocument();
|
||||||
|
expect(calls("GET /api/users/:u/dumps/:slug").map((c) => c.path)).toEqual([
|
||||||
|
"/api/users/@owner/dumps/next-find",
|
||||||
|
"/api/users/@owner/dumps/a-find",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
test("a live update of this dump is applied", async () => {
|
test("a live update of this dump is applied", async () => {
|
||||||
const d = dumpRoutes();
|
const d = dumpRoutes();
|
||||||
const { socket } = openDump();
|
const { socket } = openDump();
|
||||||
|
|||||||
Reference in New Issue
Block a user