mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
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:
@@ -17,6 +17,8 @@ import { layout, type Flash } from "./layout.ts";
|
||||
import * as pages from "./pages.ts";
|
||||
import * as inbox from "./inbox.ts";
|
||||
import * as posts from "./posts.ts";
|
||||
import * as editor from "./editor.ts";
|
||||
import { sitePagesView } from "./sitepages.ts";
|
||||
import { suggestionChange, suggestionsPanel } from "./suggestions.ts";
|
||||
|
||||
const css = readFileSync(new URL("../public/admin.css", import.meta.url), "utf8");
|
||||
@@ -127,6 +129,24 @@ export function createAdminServer(api: ContentApi, opts: { now?: () => Date } =
|
||||
if (note.type !== "post") return new Redirect(pages.noteHref(note.path));
|
||||
return editorPage(posts.draftFromNote(note));
|
||||
}
|
||||
case "/site": {
|
||||
const all = await Promise.all(["page", "site-settings", "store-settings", "status-kinds", "skills", "education"].map((t) => api.fullNotes(t)));
|
||||
const singles = new Map(all.slice(1).flatMap((l) => (l[0] ? [[l[0].type, l[0]] as const] : [])));
|
||||
return { title: "Site pages", body: sitePagesView(all[0]!, singles) };
|
||||
}
|
||||
case "/edit": {
|
||||
const note = await api.note(required(q, "path"));
|
||||
if (note.type === "post") return new Redirect(posts.editHref(note.path));
|
||||
return noteEditorPage(editor.draftFromNote(note));
|
||||
}
|
||||
case "/edit/new": {
|
||||
const type = required(q, "type");
|
||||
if (type === "post") return new Redirect("/posts/new");
|
||||
if (!noteTypeNames.includes(type as never)) throw new ApiError(404, `There is no note type ${type}.`, null);
|
||||
const data: Record<string, unknown> = {};
|
||||
if (type === "page" && q.get("page")) data.page = q.get("page");
|
||||
return noteEditorPage({ type, title: q.get("title") ?? "", publish: false, data, body: "" });
|
||||
}
|
||||
case "/images": {
|
||||
const folder = q.get("folder") ?? "";
|
||||
return { title: "Images", body: posts.imagesPage(await api.attachments(), folder === "-" ? "" : folder) };
|
||||
@@ -156,6 +176,45 @@ export function createAdminServer(api: ContentApi, opts: { now?: () => Date } =
|
||||
return { title: d.title || "Write a post", body: posts.postEditor(d, { images, notes: names, showPreview }) };
|
||||
}
|
||||
|
||||
async function noteEditorPage(d: editor.EditorDraft, reasons: string[] = []): Promise<Page> {
|
||||
const [images, notes] = await Promise.all([api.attachments(), api.notes({ limit: 5000 })]);
|
||||
const names = [...new Set(notes.filter((n) => n.type !== "status").map((n) => n.title))].sort((a, b) => a.localeCompare(b));
|
||||
const body = editor.noteEditor(d, { images, notes: names });
|
||||
return {
|
||||
title: d.title || "New note",
|
||||
body: reasons.length ? html`<div class="flash err" role="alert">Not saved.<ul>${reasons.map((r) => html`<li>${r}</li>`)}</ul></div>${body}` : body,
|
||||
};
|
||||
}
|
||||
|
||||
/** Save the note editor: upload chosen images, then reorder a row or save. */
|
||||
async function saveNote(f: Form, files: Upload[]): Promise<Page | Redirect> {
|
||||
const type = required(f, "type");
|
||||
if (!noteTypeNames.includes(type as never)) throw new ApiError(400, `There is no note type ${type}.`, null);
|
||||
const path = f.get("path") || undefined;
|
||||
const uploaded = new Map<string, string[]>();
|
||||
for (const u of files.filter((x) => x.field.startsWith("upload."))) {
|
||||
const key = u.field.slice("upload.".length);
|
||||
uploaded.set(key, [...(uploaded.get(key) ?? []), await api.upload(editor.uploadFolder(type), u.name, u.bytes)]);
|
||||
}
|
||||
let values = editor.formData(type, f, uploaded);
|
||||
const title = (f.get("title") ?? "").trim();
|
||||
const body = (f.get("body") ?? "").replace(/\r\n/g, "\n");
|
||||
const draft = (): editor.EditorDraft => ({ type, path, title, publish: values.publish === true, data: values, body });
|
||||
const move = f.get("move");
|
||||
if (move) {
|
||||
values = editor.moveRow(values, move);
|
||||
return noteEditorPage(draft());
|
||||
}
|
||||
if (!title) return noteEditorPage(draft(), ["A note needs a title."]);
|
||||
const fields = editor.changeFields(values, !!path);
|
||||
const text = editor.hasBody(type) ? body : undefined;
|
||||
const o = path
|
||||
? await api.apply({ op: "update", path, fields, body: text })
|
||||
: await api.apply({ op: "create", data: { type, title, ...fields }, body: text });
|
||||
if (o.status === "rejected") return noteEditorPage(draft(), o.reasons.map((r) => r.message));
|
||||
return after(editor.editHref(o.path), o, "Saved. The site shows it within a minute.");
|
||||
}
|
||||
|
||||
/** Save the post form: upload its images, then preview, save a draft or publish. */
|
||||
async function savePost(f: Form, files: Upload[]): Promise<Page | Redirect> {
|
||||
const d = posts.draftFromForm(f);
|
||||
@@ -185,6 +244,8 @@ export function createAdminServer(api: ContentApi, opts: { now?: () => Date } =
|
||||
switch (path) {
|
||||
case "/posts/save":
|
||||
return savePost(f, files);
|
||||
case "/edit/save":
|
||||
return saveNote(f, files);
|
||||
case "/images/upload": {
|
||||
const folder = f.get("folder") || "Posts";
|
||||
const added: string[] = [];
|
||||
|
||||
Reference in New Issue
Block a user