feat(editor): forms described by the server, field checks on focus lost #166

Merged
dominik.polakovics merged 3 commits from afk/164 into main 2026-10-07 21:12:43 +02:00

Closes #164

What

The browser no longer knows field types. Every editor form is described by the server and checked by the server.

  • Server-described forms — POST /api/form describes the element form (with item rows), the inline-element dialog (the browser sends the chip's current params), page properties (Page module and a module's Properties tab) and the create dialog. Each field carries name, label, widget, widget settings, required, itemDisplay, summary role and its current value, read through the field-type definitions from #163. Unreadable stored values describe as the default, else empty. A dateTime field in the create dialog starts at now.
  • Widgets, not types — each internal/element/ftype_*.go names its widget and settings. The browser has a widget registry (web/src/widgets/) and components/fields.tsx is gone. A Go drift test and a vitest test pin both sides to web/src/widgets/names.json, so the server can't name a widget the browser lacks.
  • Errors only from the server, one format — POST /api/form/check runs on focus lost, using the same code a save runs. Every save (element incl. items and body, properties, page create) returns 422 {error, errors:[{field,msg}]} with all failing fields; item rows are items[<i>].<name>.
  • Inline dialog — POST /api/form/inline checks the whole form and returns either the errors or the chip's complete new params, written by the server (categories comma-joined, datetime RFC3339, link + Target).
  • Link — one value {href, newTab}; still stored as <name> + <name>Target.
  • #158 fixed — element params are read when the form loads (the page tree stays a plain parse). 2026-07-10, 2026-07-10 09:00:00, 2026-07-10T09:00:00 and 10 Jul 2026 in a block element, an item row and an inline element show in the form and save untouched as RFC3339.
  • Module list — /api/modules serves {name,label,widget,sortable} per field and labelled filter options {value,label}. The browser no longer has a sort-type list or filter-kind switches.
  • GET /api/page/settings is removed (replaced by POST /api/form kind properties). README documents the new API. scaffold/site/AGENTS.md now says a datetime shortcode param may be written in any spelling Hugo reads and the editor writes RFC3339.

Verification

  • make test passes: vitest 69 files / 1905 tests, and go test ./... passes for every package. tsc --noEmit is clean and go vet ./... passes.
  • Go tests cover the description of each form kind, the field check for every type, the error format on every save, the #158 spellings (block, item row, inline) and link newTab round trips. Browser tests cover each widget, the registry drift, and forms rendered from mocked descriptions, including blur checks and 422 display.
  • Real browser: Playwright with Chromium against harness serve on a fresh harness init site, set to the Europe/Vienna timezone, with custom datetime element, items and inline sidecars. Covered:
    • hand-written dates in all four spellings showed in the form and saved untouched as RFC3339;
    • focus-lost messages appeared and cleared in all five forms;
    • multi-field 422s showed every message;
    • the inline dialog inserted nothing while values were invalid and inserted the server's params once they were valid;
    • "open in new tab" worked in all four places;
    • a news item was created;
    • the News list's search, sort and filters still work.

Notes

  • Category filter options now come from /api/modules (polled) rather than the client's category store, so a just-created category shows in the filter after the next poll.
  • Create: when the request carries fields, only required or create: true fields may be seeded, and requiredness is enforced. The page tree's quick create (no fields) is unchanged.
  • Things noticed during verification that this PR does not change: a stored select value outside options shows blank and must be re-picked; item rows are written back on one line.

🤖 Generated with Claude Code

Closes #164 ## What The browser no longer knows field types. Every editor form is described by the server and checked by the server. - **Server-described forms** — `POST /api/form` describes the element form (with item rows), the inline-element dialog (the browser sends the chip's current params), page properties (Page module and a module's Properties tab) and the create dialog. Each field carries name, label, widget, widget settings, required, itemDisplay, summary role and its current value, read through the field-type definitions from #163. Unreadable stored values describe as the default, else empty. A `dateTime` field in the create dialog starts at now. - **Widgets, not types** — each `internal/element/ftype_*.go` names its widget and settings. The browser has a widget registry (`web/src/widgets/`) and `components/fields.tsx` is gone. A Go drift test and a vitest test pin both sides to `web/src/widgets/names.json`, so the server can't name a widget the browser lacks. - **Errors only from the server, one format** — `POST /api/form/check` runs on focus lost, using the same code a save runs. Every save (element incl. items and body, properties, page create) returns `422 {error, errors:[{field,msg}]}` with *all* failing fields; item rows are `items[<i>].<name>`. - **Inline dialog** — `POST /api/form/inline` checks the whole form and returns either the errors or the chip's complete new params, written by the server (categories comma-joined, datetime RFC3339, link + `Target`). - **Link** — one value `{href, newTab}`; still stored as `<name>` + `<name>Target`. - **#158 fixed** — element params are read when the form loads (the page tree stays a plain parse). `2026-07-10`, `2026-07-10 09:00:00`, `2026-07-10T09:00:00` and `10 Jul 2026` in a block element, an item row and an inline element show in the form and save untouched as RFC3339. - **Module list** — `/api/modules` serves `{name,label,widget,sortable}` per field and labelled filter options `{value,label}`. The browser no longer has a sort-type list or filter-kind switches. - `GET /api/page/settings` is removed (replaced by `POST /api/form` kind `properties`). README documents the new API. `scaffold/site/AGENTS.md` now says a `datetime` shortcode param may be written in any spelling Hugo reads and the editor writes RFC3339. ## Verification - `make test` passes: vitest 69 files / 1905 tests, and `go test ./...` passes for every package. `tsc --noEmit` is clean and `go vet ./...` passes. - Go tests cover the description of each form kind, the field check for every type, the error format on every save, the #158 spellings (block, item row, inline) and link newTab round trips. Browser tests cover each widget, the registry drift, and forms rendered from mocked descriptions, including blur checks and 422 display. - **Real browser**: Playwright with Chromium against `harness serve` on a fresh `harness init` site, set to the Europe/Vienna timezone, with custom `datetime` element, items and inline sidecars. Covered: - hand-written dates in all four spellings showed in the form and saved untouched as RFC3339; - focus-lost messages appeared and cleared in all five forms; - multi-field 422s showed every message; - the inline dialog inserted nothing while values were invalid and inserted the server's params once they were valid; - "open in new tab" worked in all four places; - a news item was created; - the News list's search, sort and filters still work. ## Notes - Category filter options now come from `/api/modules` (polled) rather than the client's category store, so a just-created category shows in the filter after the next poll. - Create: when the request carries `fields`, only required or `create: true` fields may be seeded, and requiredness is enforced. The page tree's quick create (no `fields`) is unchanged. - Things noticed during verification that this PR does not change: a stored select value outside `options` shows blank and must be re-picked; item rows are written back on one line. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(editor): forms described by the server, field checks on focus lost
All checks were successful
ci / build-and-test (pull_request) Successful in 10m16s
a1774721b0
The browser stops knowing field types. Every editor form — the element
form with its item rows, the inline-element dialog, page properties and
the create dialog — is described by the server: per field its label,
widget, widget settings, required flag, item display and current value,
read on the server through the field-type definitions. The browser holds
widgets (text, multiline, number, checkbox, choice, multichoice,
dateTime, imagePicker, linkPicker, pagePicker, categoryPicker) and hands
back what the user entered; it has no field-type list, no checks and no
value conversions.

- element: each field type names its widget and settings; Schema.Describe,
  DescribeNew (a datetime starts at now in the create dialog),
  CheckForm and CheckField. A link field has one value {href, newTab},
  still stored as <name> + <name>Target. FieldError marshals as
  {field, msg}.
- server: POST /api/form (describe), POST /api/form/check (field check
  on focus lost, the same code a save runs), POST /api/form/inline
  (inline dialog submit; the server writes the params). Every save and
  check reports all failing fields in one 422 format
  {error, errors:[{field,msg}]}, item rows as items[<i>].<name>.
  GET /api/page/settings is removed. /api/modules serves each field's
  widget and sortable flag and labels every filter option.
- Element params are read when the form loads, so a hand-written date in
  any spelling Hugo reads shows in the form and saves as RFC3339 (#158).
- web: widget registry pinned to web/src/widgets/names.json by a Go and a
  vitest drift test; components/fields.tsx is gone.
- AGENTS.md: a datetime shortcode param may be written in any spelling
  Hugo reads; the editor writes RFC3339.

Closes #164

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
fix(editor): keep edits made during a save; row messages move with their row
All checks were successful
ci / build-and-test (pull_request) Successful in 9m41s
39b0256611
Review fixes for #166.

- Element form: a successful save no longer resets the form from the
  description that follows it. The save records what it sent; the new
  description becomes the saved state, and every field edited while the
  save was in flight keeps its edit and reads dirty (FormValues.rebase).
  Saved rows keep their objects and take their server-stamped uids; rows
  added, removed or moved meanwhile stay. A body edited meanwhile is no
  longer replaced by the refetched body. The widgets are remounted only
  when a description changes a field's shape (sameFieldShapes), so focus
  and caret survive the re-describe. "Saved" stays off while such an edit
  is unsaved. If the describe fails, what was sent (not what is held) is
  the saved state.
- Item rows keep their own messages (one createFormErrors per row), so a
  message moves with its row on a reorder or remove. A late focus-lost
  answer for a removed row lands on no other row, and a save's 422 goes
  to the rows as the save sent them. FormFields reads messages by bare
  field name; checks still name the field items[<i>].<name>.
- Page properties: what a save sent is the saved state, so an edit made
  meanwhile stays dirty.
- Create dialog: the title is the first described field, whatever its case,
  so a type declaring `Title` can be created (it no longer lands in
  `fields` and gets a 400).
- pageops.Create stores the title without surrounding whitespace, for the
  dialog and the quick create alike.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

This was generated by AI while landing a PR.

Validated: PASS, with one non-blocking concern. Head 39b0256.

Signal relied on: Forgejo Actions ci / build-and-test passed on the head (run 154, 9m41s). Not re-run. The head already contains main, so there is no conflict.

Checked

  • The title is a Conventional Commit and the body has Closes #164.
  • The diff matches #164's brief.
    • Server-described forms: element with item rows, inline, properties, create.
    • Field check on focus lost, through the same CheckForm the saves use.
    • One 422 format for every save.
    • #158 date spellings covered for block, item row and inline.
    • Link {href, newTab} round trip.
    • Widget drift tests on both sides.
    • scaffold/site/AGENTS.md updated, as CLAUDE.md requires.
    • deckle-hosted calls none of the changed endpoints.
  • The fix commit 39b0256 resolves the four review findings from the first pass, each with tests:
    1. Edits made while a save is in flight are kept (FormValues.rebase) and the form reads dirty.
    2. Item-row messages live on their row, so they move with it on reorder or remove. A late check for a removed row lands nowhere.
    3. The create dialog takes the title from the first described field, so a type that declares Title can be created.
    4. pageops.Create trims the title.

Concern (non-blocking)

  • web/src/widgets/controls.tsx:96-100: NumberControl converts the typed text to a Number and writes String(n) back into the input.
    • Typing -0.5 loses the minus, because -0 displays as 0.
    • Backspacing 5.1 to 5. drops the dot.
    • The old widget passed the raw text, and the server reads a string for this widget. Suggested follow-up: props.onChange(text).
[autoland] verdict: pass > *This was generated by AI while landing a PR.* **Validated: PASS, with one non-blocking concern.** Head 39b0256. **Signal relied on:** Forgejo Actions `ci / build-and-test` passed on the head (run 154, 9m41s). Not re-run. The head already contains `main`, so there is no conflict. **Checked** - The title is a Conventional Commit and the body has `Closes #164`. - The diff matches #164's brief. - Server-described forms: element with item rows, inline, properties, create. - Field check on focus lost, through the same `CheckForm` the saves use. - One 422 format for every save. - #158 date spellings covered for block, item row and inline. - Link `{href, newTab}` round trip. - Widget drift tests on both sides. - `scaffold/site/AGENTS.md` updated, as CLAUDE.md requires. - deckle-hosted calls none of the changed endpoints. - The fix commit 39b0256 resolves the four review findings from the first pass, each with tests: 1. Edits made while a save is in flight are kept (`FormValues.rebase`) and the form reads dirty. 2. Item-row messages live on their row, so they move with it on reorder or remove. A late check for a removed row lands nowhere. 3. The create dialog takes the title from the first described field, so a type that declares `Title` can be created. 4. `pageops.Create` trims the title. **Concern (non-blocking)** - `web/src/widgets/controls.tsx:96-100`: `NumberControl` converts the typed text to a `Number` and writes `String(n)` back into the input. - Typing `-0.5` loses the minus, because `-0` displays as `0`. - Backspacing `5.1` to `5.` drops the dot. - The old widget passed the raw text, and the server reads a string for this widget. Suggested follow-up: `props.onChange(text)`.
fix(editor): number field keeps what the user types
All checks were successful
ci / build-and-test (pull_request) Successful in 19m22s
30e70b81da
Review fix for #166.

The number widget read the typed text as a Number and its value binding
wrote that back into the input, so typing "-0.5" lost the minus (-0
shows as "0") and backspacing "5.1" to "5." dropped the dot.

- The widget hands back the input's text unconverted, as the old widget
  did; the server already reads a number spelled as text.
- That alone leaves the dot bug: Chromium reads a typed "5." as "5" and
  a half-typed "1e" as "", so writing the handed-back value back still
  rewrites the field (and clears it at the "e" of "1e3"). The value is
  now written into the input only when it differs from what the input
  reads; a value from elsewhere (a save's stored value, a new
  description) is still written.

Checked in Chromium 154 against harness serve: Backspace in 5.1 keeps
"5." and typing 2 gives 5.2; -0.5 and 1e3 type through; a save writes
limit=-0.5.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

This was generated by AI while landing a PR.

Re-validated: PASS. Head 30e70b8.

Signal relied on: Forgejo Actions ci / build-and-test passed on the head (run 155). Not re-run. The head already contains main, so there is no conflict.

The non-blocking concern from the previous approval is resolved by 30e70b8:

  • NumberControl now hands back the input's text unconverted, as the pre-PR widget did. The server reads a number written as text.
  • It writes a value into the input only when the input reads differently. A half-typed 5., - or 1e is no longer rewritten, and a value from a save or a new description is still written.
  • Covered by widgets.test.tsx.

Everything from the previous approval stands (#164 scope, AGENTS.md, and the four review fixes in 39b0256). Nothing open.

[autoland] verdict: pass > *This was generated by AI while landing a PR.* **Re-validated: PASS.** Head 30e70b8. **Signal relied on:** Forgejo Actions `ci / build-and-test` passed on the head (run 155). Not re-run. The head already contains `main`, so there is no conflict. The non-blocking concern from the previous approval is resolved by 30e70b8: - `NumberControl` now hands back the input's text unconverted, as the pre-PR widget did. The server reads a number written as text. - It writes a value into the input only when the input reads differently. A half-typed `5.`, `-` or `1e` is no longer rewritten, and a value from a save or a new description is still written. - Covered by `widgets.test.tsx`. Everything from the previous approval stands (#164 scope, AGENTS.md, and the four review fixes in 39b0256). Nothing open.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle!166
No description provided.