diff --git a/sideboard/src/index.css b/sideboard/src/index.css index d90909f7..921509d1 100644 --- a/sideboard/src/index.css +++ b/sideboard/src/index.css @@ -378,6 +378,19 @@ button { flex-direction: column; } +.document-list__error { + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 0.75rem; + color: var(--document-list-danger); +} + +.document-list__error span { + color: var(--document-list-muted); + overflow-wrap: anywhere; +} + .document-list-item--loading { pointer-events: none; padding-left: calc(1.8rem * var(--depth) + var(--doclist-padding-x)); diff --git a/sideboard/src/sideboard/document-list-panel.test.tsx b/sideboard/src/sideboard/document-list-panel.test.tsx new file mode 100644 index 00000000..2cdd7b01 --- /dev/null +++ b/sideboard/src/sideboard/document-list-panel.test.tsx @@ -0,0 +1,40 @@ +import { render } from "solid-js/web"; +import { afterEach, describe, expect, it } from "vitest"; + +import { DocumentListError, isFolderDoc } from "./document-list-panel.tsx"; + +let dispose: (() => void) | undefined; + +afterEach(() => { + dispose?.(); + dispose = undefined; + document.body.replaceChildren(); +}); + +describe("document-list panel errors", () => { + it("recognizes folder-shaped documents", () => { + expect(isFolderDoc({ title: "Root", docs: [] })).toBe(true); + expect(isFolderDoc({ title: "Not a folder" } as any)).toBe(false); + expect(isFolderDoc(undefined)).toBe(false); + }); + + it("renders visible error details", () => { + const root = document.createElement("div"); + document.body.append(root); + + dispose = render( + () => ( + + ), + root + ); + + expect(root.querySelector('[role="alert"]')?.textContent).toContain( + "Could not load root folder." + ); + expect(root.textContent).toContain("automerge:missing"); + }); +}); diff --git a/sideboard/src/sideboard/document-list-panel.tsx b/sideboard/src/sideboard/document-list-panel.tsx index 0f18f73b..ad0b60bb 100644 --- a/sideboard/src/sideboard/document-list-panel.tsx +++ b/sideboard/src/sideboard/document-list-panel.tsx @@ -23,6 +23,24 @@ import { executeDrop } from "./dnd/operations.ts"; import { getDndPayload, hasDocumentDrag } from "./dnd/payload.ts"; import { createMarquee } from "./document-list/marquee.ts"; +export function isFolderDoc(doc: FolderDoc | undefined): doc is FolderDoc { + return !!doc && Array.isArray(doc.docs); +} + +export function DocumentListError(props: { + message: string; + detail?: string; +}) { + return ( + + ); +} + /** * The document-list panel: a sticky toolbar (new-doc button + filter) over a * scrolling, recursive list of the folder's documents and subfolders. Renders a @@ -46,6 +64,26 @@ export function DocumentListPanel(props: { // handle happens to resolve synchronously. const folderReady = () => folderHandle.state === "ready" || folderHandle.state === "refreshing"; + const folderLoadError = () => { + if (folderHandle.state === "errored") { + const error = folderHandle.error; + return { + message: "Could not load root folder.", + detail: + error instanceof Error + ? error.message + : error + ? String(error) + : props.folderUrl, + }; + } + if (folderReady() && !isFolderDoc(folder())) { + return { + message: "Root folder is not a folder.", + detail: props.folderUrl, + }; + } + }; const selectedDocUrls = subscribe( props.element, @@ -189,43 +227,57 @@ export function DocumentListPanel(props: { ); }} > -
- setFilter("")} - changeFolder={(fn) => folderHandle()?.change(fn)} - repo={props.repo} - hive={props.element.hive} - open={open} - /> -
- - setFilter(event.target.value.toLowerCase())} - /> -
-
- }> - }> - setFilter("")} - /> - + +
+ setFilter("")} + changeFolder={(fn) => folderHandle()?.change(fn)} + repo={props.repo} + hive={props.element.hive} + open={open} + /> +
+ + + setFilter(event.target.value.toLowerCase()) + } + /> +
+
+ }> + }> + setFilter("")} + /> + + + + } + > + {(error) => ( + + )}
diff --git a/sideboard/src/sideboard/sideboard.tsx b/sideboard/src/sideboard/sideboard.tsx index b6d6487d..a70a3901 100644 --- a/sideboard/src/sideboard/sideboard.tsx +++ b/sideboard/src/sideboard/sideboard.tsx @@ -5,8 +5,7 @@ import type { DocHandle, Repo } from "@automerge/automerge-repo/slim"; import type { PatchworkViewElement } from "@inkandswitch/patchwork-elements"; import type { PatchworkToolProps, SideboardAccountDoc } from "../types.ts"; -import { DocumentListPanel } from "./document-list-panel.tsx"; -import { LoadingRows } from "./document-list/loading-row.tsx"; +import { DocumentListError, DocumentListPanel } from "./document-list-panel.tsx"; import { AccountBar } from "./account-bar.tsx"; /** @@ -23,7 +22,7 @@ export function Sideboard(props: PatchworkToolProps) { fallback={ }