Edit any note and its images from the admin page

Every note gets an Edit form built from its note type's schema: image
fields show a thumbnail with a pick from the vault or an upload, link
fields offer note names, and lists of rows can be added to, removed and
reordered. A new Site pages view lists each page of the site with its
text and images and the notes that fill it. Image and link fields now
carry a JSON Schema format so a form knows to offer a picker.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
This commit is contained in:
Claude
2026-10-18 14:19:38 +00:00
parent 5077aeacfd
commit 0f78505e99
10 changed files with 781 additions and 10 deletions
+95
View File
@@ -0,0 +1,95 @@
import { describe, expect, it } from "vitest";
import { changeFields, formData, kindOf, moveRow, noteEditor, shapeOf } from "../src/editor.ts";
const form = (pairs: [string, string][]) => new URLSearchParams(pairs);
describe("note editor fields", () => {
it("knows every field of every kind of page it edits", () => {
const p = shapeOf("page").properties;
expect(kindOf(p.portrait!)).toBe("image");
expect(kindOf(p.cover!)).toBe("image");
expect(kindOf(p.photos!)).toBe("rows");
expect(kindOf(p.links!)).toBe("links");
expect(kindOf(p.tags!)).toBe("lines");
expect(kindOf(shapeOf("make").properties.photos!)).toBe("images");
expect(kindOf(shapeOf("make").properties.date!)).toBe("date");
expect(kindOf(shapeOf("experience").properties.start!)).toBe("month");
expect(kindOf(shapeOf("product-line").properties.slots!)).toBe("group");
expect(kindOf(shapeOf("craft").properties.status!)).toBe("enum");
});
it("reads a Page text form back into note values", () => {
const v = formData(
"page",
form([
["page", "/about"],
["lede", " Maker of things "],
["portrait", "greg.jpg"],
["photos.0.file", "kashi.jpg"],
["photos.0.alt", "Kashi asleep"],
["photos.0.caption", "Kashi"],
["photos.1.file", "tea.jpg"],
["photos.1.alt", "Tea"],
["photos.1.remove", "1"],
["photos.2.file", ""],
["photos.2.alt", ""],
["has.publish", "1"],
["publish", "1"],
["has.favorite", "1"],
["links", "Resin\n[[Galaxy dice]]\n"],
]),
new Map([["cover", ["new-cover.jpg"]]]),
);
expect(v).toMatchObject({
page: "/about",
lede: "Maker of things",
portrait: "[[greg.jpg]]",
cover: "[[new-cover.jpg]]",
photos: [{ file: "[[kashi.jpg]]", alt: "Kashi asleep", caption: "Kashi" }],
publish: true,
links: ["[[Resin]]", "[[Galaxy dice]]"],
});
expect(v.quickFacts).toBeUndefined();
expect(v.favorite).toBeUndefined();
});
it("adds uploaded photos to an image list and keeps https images", () => {
const v = formData(
"make",
form([
["photos.0", "https://example.com/a.jpg"],
["photos.1", "b.jpg"],
["photos.1.remove", "1"],
]),
new Map([["photos", ["c.jpg", "d.jpg"]]]),
);
expect(v.photos).toEqual(["https://example.com/a.jpg", "[[c.jpg]]", "[[d.jpg]]"]);
});
it("clears emptied fields on an update and leaves them out on a create", () => {
const v = { lede: "Hi", portrait: undefined, photos: undefined };
expect(changeFields(v, true)).toEqual({ lede: "Hi", portrait: null, photos: null });
expect(changeFields(v, false)).toEqual({ lede: "Hi" });
});
it("moves a row up or down", () => {
const v = { photos: ["a", "b", "c"] };
expect(moveRow(v, "photos:2:up").photos).toEqual(["a", "c", "b"]);
expect(moveRow(v, "photos:0:down").photos).toEqual(["b", "a", "c"]);
expect(moveRow(v, "photos:0:up").photos).toEqual(["a", "b", "c"]);
});
it("renders a picker with a thumbnail for each image", () => {
const page = String(
noteEditor(
{ type: "page", path: "Pages/About.md", title: "About", publish: true, data: { page: "/about", portrait: "[[greg.jpg]]", photos: [{ file: "[[kashi.jpg]]", alt: "Kashi" }] }, body: "Hello" },
{ images: [{ name: "greg.jpg", folder: "Pages", size: 1, modified: "", usedBy: [] }], notes: [] },
),
);
expect(page).toContain('<img src="/attachments/greg.jpg"');
expect(page).toContain('<img src="/attachments/kashi.jpg"');
expect(page).toContain('name="upload.portrait"');
expect(page).toContain('value="photos:0:down"');
expect(page).toContain(">Hello</textarea>");
});
});