mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-18 19:30:27 +08:00
Keep the browsed file in the source view route
Browsing the file tree only changed component state, so no file view was ever a history entry and back left the repository. The path now travels as a query parameter, since trailing URL segments are already consumed by the revision.
This commit is contained in:
@@ -3,12 +3,12 @@
|
||||
|
||||
const {
|
||||
name,
|
||||
fetchBlob,
|
||||
onSelect,
|
||||
active,
|
||||
indent = 0.5,
|
||||
}: {
|
||||
name: string;
|
||||
fetchBlob: () => Promise<void>;
|
||||
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}>
|
||||
<div class="global-flex" style:padding="0.25rem 0">
|
||||
<div class="icon txt-missing">
|
||||
<Icon name="document" />
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
interface Props {
|
||||
fetchTree: (path: string) => Promise<Tree>;
|
||||
fetchBlob: (path: string) => Promise<void>;
|
||||
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"}
|
||||
<FileTreeFolder
|
||||
{fetchTree}
|
||||
{fetchBlob}
|
||||
{onSelect}
|
||||
name={entry.name}
|
||||
{currentPath}
|
||||
{isExpanded}
|
||||
@@ -81,7 +81,7 @@
|
||||
{:else if entry.kind === "blob"}
|
||||
<FileTreeFile
|
||||
name={entry.name}
|
||||
fetchBlob={() => fetchBlob(entry.path)}
|
||||
onSelect={() => onSelect(entry.path)}
|
||||
active={entry.path === currentPath}
|
||||
indent={indent + 1.5} />
|
||||
{/if}
|
||||
|
||||
@@ -11,10 +11,10 @@
|
||||
tree: Tree;
|
||||
currentPath: string;
|
||||
fetchTree: (path: string) => Promise<Tree>;
|
||||
fetchBlob: (path: string) => Promise<void>;
|
||||
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}
|
||||
<FileTreeFile
|
||||
name={entry.name}
|
||||
fetchBlob={() => fetchBlob(entry.path)}
|
||||
onSelect={() => onSelect(entry.path)}
|
||||
active={entry.path === currentPath} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
@@ -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}
|
||||
<ScrollArea
|
||||
style="border-right: 1px solid var(--color-border-subtle); flex: 1; min-height: 0; width: 100%; padding: 0.5rem;">
|
||||
<TreeComponent {tree} {currentPath} {fetchTree} {fetchBlob} />
|
||||
<TreeComponent
|
||||
{tree}
|
||||
{currentPath}
|
||||
{fetchTree}
|
||||
onSelect={showPath} />
|
||||
</ScrollArea>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -298,7 +320,7 @@
|
||||
rid={repo.rid}
|
||||
sha={oid}
|
||||
path={currentPath}
|
||||
onNavigate={fetchBlob} />
|
||||
onNavigate={showPath} />
|
||||
{:else}
|
||||
<Markdown content={blob.content} />
|
||||
{/if}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"),
|
||||
[
|
||||
|
||||
Reference in New Issue
Block a user