feat: unified, editable frontmatter editor in publish modal
All checks were successful
CI / test (push) Successful in 16s

- Preset fields (from settings) are now seeded as editable/deletable rows in
  the modal alongside custom ones, using one consistent row style.
- The note's own merged frontmatter is shown as read-only (disabled) rows at
  the top of the list (only when 'Merge document frontmatter' is on, and only
  for keys not overridden by an editable row).
- Drops the separate read-only 'resolved' text preview; the rows are the
  WYSIWYG result.

Adds pure, unit-tested composeFrontmatter()/docFrontmatterToPairs() and removes
the superseded resolveFrontmatter(). Editable rows are authoritative: on a key
collision they override the merged document field. Presets are deep-copied so
editing rows never mutates saved settings.

Co-Authored-By: Claude
This commit is contained in:
2026-06-19 03:20:36 +00:00
parent f2a4040ae8
commit fb5d11dd6a
4 changed files with 166 additions and 59 deletions

View File

@@ -1,5 +1,5 @@
import { describe, expect, test } from "vitest";
import { parseNote, resolveFrontmatter, serializeFrontmatter } from "./frontmatter";
import { composeFrontmatter, docFrontmatterToPairs, parseNote, serializeFrontmatter } from "./frontmatter";
describe("parseNote", () => {
test("splits frontmatter and body, strips BOM", () => {
@@ -12,27 +12,70 @@ describe("parseNote", () => {
});
});
describe("resolveFrontmatter", () => {
const presets = [{ key: "layout", value: "post" }, { key: "kind", value: "essay" }];
test("presets only when mergeDoc is false", () => {
expect(resolveFrontmatter(presets, { title: "X" }, [], false)).toEqual(presets);
});
test("merge appends doc keys and doc overrides preset value", () => {
const r = resolveFrontmatter(presets, { kind: "note", title: "X" }, [], true);
expect(r).toEqual([
{ key: "layout", value: "post" },
{ key: "kind", value: "note" },
{ key: "title", value: "X" },
describe("docFrontmatterToPairs", () => {
test("converts a record to ordered string pairs", () => {
const pairs = docFrontmatterToPairs({
title: "Hello",
date: new Date("2026-02-06T00:00:00Z"),
draft: true,
});
expect(pairs).toEqual([
{ key: "title", value: "Hello" },
{ key: "date", value: "2026-02-06" },
{ key: "draft", value: "true" },
]);
});
test("custom rows come last and override everything", () => {
const r = resolveFrontmatter(presets, {}, [{ key: "layout", value: "page" }], false);
expect(r[0]).toEqual({ key: "layout", value: "page" });
expect(r).toHaveLength(2);
test("empty record yields an empty array", () => {
expect(docFrontmatterToPairs({})).toEqual([]);
});
test("date object is rendered as YYYY-MM-DD", () => {
const r = resolveFrontmatter([], { date: new Date("2026-06-11T00:00:00Z") }, [], true);
expect(r).toEqual([{ key: "date", value: "2026-06-11" }]);
});
describe("composeFrontmatter", () => {
const doc = [
{ key: "title", value: "Hello" },
{ key: "date", value: "2026-02-06" },
];
const editable = [
{ key: "layout", value: "post" },
{ key: "kind", value: "essay" },
];
test("merge on: document fields first, then editable rows", () => {
expect(composeFrontmatter(doc, editable, true)).toEqual([
{ key: "title", value: "Hello" },
{ key: "date", value: "2026-02-06" },
{ key: "layout", value: "post" },
{ key: "kind", value: "essay" },
]);
});
test("merge off: only the editable rows", () => {
expect(composeFrontmatter(doc, editable, false)).toEqual(editable);
});
test("an editable row overrides a document field with the same key (doc field dropped)", () => {
const ed = [
{ key: "title", value: "Override" },
{ key: "layout", value: "post" },
];
expect(composeFrontmatter(doc, ed, true)).toEqual([
{ key: "date", value: "2026-02-06" },
{ key: "title", value: "Override" },
{ key: "layout", value: "post" },
]);
});
test("blank-key editable rows are skipped", () => {
const ed = [
{ key: "", value: "" },
{ key: "layout", value: "post" },
{ key: " ", value: "x" },
];
expect(composeFrontmatter([], ed, true)).toEqual([{ key: "layout", value: "post" }]);
});
test("duplicate editable keys: last value wins at first position", () => {
const ed = [
{ key: "tag", value: "a" },
{ key: "tag", value: "b" },
];
expect(composeFrontmatter([], ed, false)).toEqual([{ key: "tag", value: "b" }]);
});
});