diff --git a/src/components/FileTreeFile.svelte b/src/components/FileTreeFile.svelte index 509322ff0..9f04b796f 100644 --- a/src/components/FileTreeFile.svelte +++ b/src/components/FileTreeFile.svelte @@ -3,12 +3,12 @@ const { name, - fetchBlob, + onSelect, active, indent = 0.5, }: { name: string; - fetchBlob: () => Promise; + onSelect: () => void; active: boolean; indent?: number; } = $props(); @@ -41,7 +41,7 @@ class:active style:padding-left="{indent}rem" style:padding-right="0.5rem" - onclick={fetchBlob}> + onclick={onSelect}>
diff --git a/src/components/FileTreeFolder.svelte b/src/components/FileTreeFolder.svelte index 27682c45f..229b055a7 100644 --- a/src/components/FileTreeFolder.svelte +++ b/src/components/FileTreeFolder.svelte @@ -7,7 +7,7 @@ interface Props { fetchTree: (path: string) => Promise; - fetchBlob: (path: string) => Promise; + onSelect: (path: string) => void; currentPath: string; name: string; prefix: string; @@ -20,7 +20,7 @@ const { name, - fetchBlob, + onSelect, currentPath, prefix, isExpanded, @@ -71,7 +71,7 @@ {#if entry.kind === "tree"} fetchBlob(entry.path)} + onSelect={() => onSelect(entry.path)} active={entry.path === currentPath} indent={indent + 1.5} /> {/if} diff --git a/src/components/Tree.svelte b/src/components/Tree.svelte index 3e5d3a331..c4c17684c 100644 --- a/src/components/Tree.svelte +++ b/src/components/Tree.svelte @@ -11,10 +11,10 @@ tree: Tree; currentPath: string; fetchTree: (path: string) => Promise; - fetchBlob: (path: string) => Promise; + onSelect: (path: string) => void; } - const { currentPath, tree, fetchTree, fetchBlob }: Props = $props(); + const { currentPath, tree, fetchTree, onSelect }: Props = $props(); // Manual expand/collapse overrides, keyed by folder prefix. Held here // rather than in FileTreeFolder so the state survives the virtualizer @@ -50,11 +50,11 @@ {isExpanded} {toggleExpanded} {fetchTree} - {fetchBlob} /> + {onSelect} /> {:else} fetchBlob(entry.path)} + onSelect={() => onSelect(entry.path)} active={entry.path === currentPath} /> {/if} {/snippet} diff --git a/src/views/repo/RepoHome.svelte b/src/views/repo/RepoHome.svelte index 03b913c75..d5f1dde20 100644 --- a/src/views/repo/RepoHome.svelte +++ b/src/views/repo/RepoHome.svelte @@ -10,6 +10,7 @@ import { useOverlayScrollbars } from "overlayscrollbars-svelte"; import { invoke, InvokeError } from "@app/lib/invoke"; + import * as router from "@app/lib/router"; import type { SidebarData } from "@app/lib/router/definitions"; import { highlight } from "@app/lib/syntax"; @@ -63,15 +64,32 @@ let codeElement: HTMLElement | undefined = $state(); let error: InvokeError | undefined = $state(); + // The file that was last opened, so that reloading the same route doesn't + // fetch it again, while switching revisions on the same path does. + let openedFile: string | undefined; + + // The shown file lives in the route, so browsing the tree and following + // links inside a document both go through history. $effect(() => { - if (path) { - void openPath(path); - } else { + if (!path) { + openedFile = undefined; blob = readme; currentPath = readme?.path || ""; + error = undefined; + return; + } + + const file = `${repo.rid}:${oid}:${path}`; + if (file !== openedFile) { + openedFile = file; + void openPath(path); } }); + function showPath(filePath: string) { + void router.push({ ...baseRoute, path: filePath }); + } + function isMarkdownPath(path: string): boolean { return /\.(md|mkd|markdown)$/i.test(path); } @@ -203,7 +221,11 @@ {#if tree.entries.length > 0} - + {/if}
@@ -298,7 +320,7 @@ rid={repo.rid} sha={oid} path={currentPath} - onNavigate={fetchBlob} /> + onNavigate={showPath} /> {:else} {/if} diff --git a/src/views/repo/router.ts b/src/views/repo/router.ts index d76053174..860891f96 100644 --- a/src/views/repo/router.ts +++ b/src/views/repo/router.ts @@ -34,6 +34,8 @@ export interface RepoHomeRoute { rid: string; peer?: string; revision?: string; + // The file shown in the source view. Trailing URL segments are already + // consumed by `revision`, so this travels as a query parameter. path?: string; } diff --git a/tests/e2e/repo/asciidoc.spec.ts b/tests/e2e/repo/asciidoc.spec.ts index f77992082..21d1d298d 100644 --- a/tests/e2e/repo/asciidoc.spec.ts +++ b/tests/e2e/repo/asciidoc.spec.ts @@ -25,8 +25,13 @@ test("open a relative link in the source view", async ({ page }) => { const url = page.url(); await readme.getByRole("link", { name: "the notes" }).click(); - // The link opens the file instead of navigating the app away. + // The link opens the file in the source view rather than navigating the + // window away, and the file it opened is part of the history. await expect(page.getByText("Notes from a sibling file.")).toBeVisible(); + expect(page.url()).not.toBe(url); + + await page.goBack(); + await expect(readme.getByText("Alpha section text.")).toBeVisible(); expect(page.url()).toBe(url); }); @@ -94,6 +99,54 @@ test("bound the reads of recursive includes", async ({ page }) => { expect(blobReads).toBeLessThanOrEqual(260); }); +test("go back from a file opened in the tree", async ({ page }) => { + await page.goto("/inbox"); + await page.getByRole("link", { name: "asciidoc" }).click(); + + const readme = page.locator(".asciidoc"); + await expect(readme.getByText("Alpha section text.")).toBeVisible(); + const url = page.url(); + + await page.getByText("notes.txt", { exact: true }).click(); + await expect(page.getByText("Notes from a sibling file.")).toBeVisible(); + await expect(page).toHaveURL(/\?path=notes\.txt/); + + await page.goBack(); + await expect(readme.getByText("Alpha section text.")).toBeVisible(); + expect(page.url()).toBe(url); +}); + +test("show the same path at another revision", async ({ page }) => { + await page.goto("/inbox"); + const commit = page.waitForResponse( + response => new URL(response.url()).pathname === "/repo_commit", + ); + await page.getByRole("link", { name: "asciidoc" }).click(); + const { parents } = (await (await commit).json()) as { parents: string[] }; + const rid = page.url().match(/\/repos\/(rad:[^/?]+)/)?.[1]; + + await page.getByText("notes.txt", { exact: true }).click(); + await expect(page.getByText("Notes from a sibling file.")).toBeVisible(); + + // Follow an in-app link to the same file at the previous commit, so the + // view is reused with only the revision changed. + await page.evaluate(href => { + const anchor = document.createElement("a"); + anchor.href = href; + document.body.appendChild(anchor); + anchor.click(); + anchor.remove(); + }, `/repos/${rid}/home/${parents[0]}?path=notes.txt`); + + await expect( + page.getByText("An earlier version of the notes."), + ).toBeVisible(); + await expect(page.getByText("Notes from a sibling file.")).toHaveCount(0); + + await page.goBack(); + await expect(page.getByText("Notes from a sibling file.")).toBeVisible(); +}); + test("sanitize untrusted markup in an AsciiDoc readme", async ({ page }) => { await page.goto("/inbox"); await page.getByRole("link", { name: "asciidoc" }).click(); diff --git a/tests/support/fixtures.ts b/tests/support/fixtures.ts index ddd02b08c..86bd7f22c 100644 --- a/tests/support/fixtures.ts +++ b/tests/support/fixtures.ts @@ -597,6 +597,15 @@ export async function createMarkdownFixture(peer: RadiclePeer) { export async function createAsciidocFixture(peer: RadiclePeer) { const { repoFolder } = await createRepo(peer, { name: "asciidoc" }); + // An older version of a file, so a test can show the same path at two + // revisions. + await Fs.writeFile( + Path.join(repoFolder, "notes.txt"), + "An earlier version of the notes.\n", + ); + await peer.git(["add", "."], { cwd: repoFolder }); + await peer.git(["commit", "-m", "Add notes"], { cwd: repoFolder }); + await Fs.writeFile( Path.join(repoFolder, "README.adoc"), [