Editor forms described by the server, with field checks on focus lost #164

Closed
opened 2026-10-07 15:21:42 +02:00 by dominik.polakovics · 0 comments

This was generated by AI during triage.

Summary

The browser stops knowing field types. For every form it asks the server which fields the form has, each with its label, widget, widget settings, required flag and current value, already read through the field-type definitions from #163, and renders widgets from that. Errors come only from the server: from a field-check endpoint when a field loses focus, and from every save. Element params are read on the server like page fields, so a hand-written date in an element no longer blocks its save (#158).

Current behavior

  • Page properties already work this way. GET /api/page/settings returns {fields, values}, with the values read on the server.
  • Every other form is assembled in the browser from the sidecar schemas (GET /api/elements, GET /api/pagetypes) plus raw values:
    • the element form and its item rows (FormPane.tsx) take values from the page tree, which GET /api/page and every page-changing response return as a plain parse of the file;
    • the inline-element dialog (ChipDialog in web/src/rte/RichTextField.tsx) takes params the browser parses out of the body markdown (web/src/rte/chip.ts);
    • the create dialog (CreateDialog.tsx) starts from schema defaults, and from the current time for a datetime field.
  • The browser re-implements type logic: the type list (web/src/api/types.ts), seeding (initialFieldValue), checks (validateFieldValues, isRFC3339), building saved values (buildFieldValues, which for inline elements also joins categories with commas), the link's <name>Target companion (five files), read-only display (readonlyValueText), the module list's cells (ModuleScreen.tsx) and its sort rule (SORTABLE_TYPES in web/src/modulequery.ts).
  • #158, reproduced in triage: a hand-written start="2026-07-10" in an element's datetime param reaches the form unread. The browser's RFC3339 check rejects it ("Must be a valid date and time") even though the picker shows the date, and that blocks saving any field of the element (in an item row, its parent element) until the date is entered again. The same happens with 2026-07-10 09:00:00, 2026-07-10T09:00:00 and 10 Jul 2026. No shipped element has a datetime param, but custom elements can.
  • Three error formats: element saves return {errors: [{param, msg}]}, properties saves return {fields: {name: msg}}, and page create returns only its first error, as text.

Desired behavior

  • The server describes every form. For each form the editor shows, the browser asks the server and gets, per field: name, label, widget, widget settings (such as a choice's options or a page picker's page type), required, item display, and the current value. This covers:
    • the element form with its item rows;
    • the inline-element dialog (the browser sends the element's current params, because they live in unsaved body text);
    • page properties, in the Page module and on an editor module's Properties tab;
    • the create dialog;
    • the module list: its columns, and which fields can be searched, sorted and filtered.
  • Values are read on the server through the type definitions. A value in any spelling Hugo reads arrives as the standard value; an absent or unreadable one arrives as the field's default, else empty. A datetime field in the create dialog still starts at the current time.
  • The browser holds widgets, not types: text, multi-line text, number, checkbox, choice, multi-choice, date-time, image picker, link picker, page picker and category picker. A widget draws what the server names and hands back what the user entered. Apart from its widgets' own conversions (such as the date-time picker's local time to and from RFC3339), the browser has no field-type list, no checks and no value conversions.
  • Errors come only from the server, in one format:
    • Field check on focus lost: when a field loses focus, the browser sends its value, with the form it belongs to, to a field-check endpoint, and shows exactly the error a save would report for that field, or clears it.
    • Saves (element, properties, page create, body) report every failing field in that format, with item rows as items[<i>].<name>.
    • The inline-element dialog checks its whole form with the server on submit and gets back either the errors or the params to write into the body. The server writes them, for example joining categories with commas.
  • A link field has one value that includes "open in new tab". It is still stored as <name> plus <name>Target.
  • #158 is fixed: a hand-written date in any spelling Hugo reads, in a block element, an item row or an inline element, shows in the form and saves without being entered again. The file then holds the same instant, as RFC3339.
  • AGENTS.md says a datetime shortcode param, like front matter, may be written in any spelling Hugo reads, and that the editor writes RFC3339 (its datetime paragraph and "Editing content by hand").

Decisions

  • The server describes every form, generalizing the page-properties read (maintainer: "the frontend could just ask, which fields are in this form, load the field definition and build everything based on that").
  • The browser holds widgets, not types: the split TYPO3 makes between TCA type and renderType. Each type definition names its widget and the widget's settings. Changing a rule touches only Go, changing a widget touches only the browser, and only a type that needs new UI touches both.
  • Errors come only from the server, with a field check on focus lost (maintainer, for UX). The check runs the same code a save runs, so the two can't disagree.
  • No validation rules, such as regexes, served to the browser. Not every rule is a pattern (real calendar dates, page and category existence, unique selections), and Go and JavaScript regexes are different dialects, so a shared pattern could pass in one and fail in the other.
  • No WebAssembly build of the Go rules. It means a multi-megabyte download, and the site checks still need the server.
  • Element params are read when the form loads, not by rewriting the page tree. The tree stays a plain parse of the file. This settles #158's open question (convert on the server, not in the browser) without touching every response that returns the tree.
  • Request and error shapes change freely: there are no existing sites, and deckle-hosted calls none of these endpoints.

Key interfaces

  • The type definitions from #163: each names its widget and widget settings.
  • New: a form-description endpoint (one, or one per form kind) and a field-check endpoint, with one error format for checks and saves. The shapes are the implementer's choice.
  • Existing endpoints: GET /api/page/settings (handlePageSettings in internal/server/pageops.go), the model to generalize; GET /api/elements, GET /api/pagetypes, GET /api/modules, GET /api/module/entries, GET /api/element/body; the saves POST /api/page/save, POST /api/page/settings/save and POST /api/page/create.
  • Browser starting points: web/src/components/fields.tsx, FormPane.tsx, PageSettings.tsx, CreateDialog.tsx, EntryEditor.tsx, ModuleScreen.tsx, web/src/rte/RichTextField.tsx, web/src/rte/chip.ts, web/src/modulequery.ts, web/src/api/types.ts.

Acceptance criteria

  • The element form with its item rows, the inline-element dialog, page properties (Page module and a module's Properties tab) and the create dialog render from server descriptions.
  • No browser code outside tests compares a field-type name, and the browser's field-type list is gone.
  • A test fails when the server can name a widget the browser doesn't have.
  • start="2026-07-10", "2026-07-10 09:00:00", "2026-07-10T09:00:00" and "10 Jul 2026" in a block element's, an item row's and an inline element's datetime param show in the form and save without being touched. The file then holds the same instant, as RFC3339. (#158)
  • An unreadable stored value shows as the field's default, else empty.
  • Leaving a field that holds an invalid value shows the message a save would report for it; leaving it with a valid value clears the message.
  • Every save, page create included, reports all failing fields in the one error format.
  • While its values are invalid, the inline-element dialog shows the server's errors and inserts nothing; once they're valid, it inserts the params the server returned.
  • A link's "open in new tab" setting works as before in element forms, item rows, inline elements and properties, and its stored keys are unchanged.
  • The module list's columns, search, sort and filters behave as before, with what is searchable, sortable and filterable coming from the server.
  • AGENTS.md is updated for datetime shortcode params (CLAUDE.md rule).
  • make test passes.

How to verify

  • make test, with Go tests for each form kind's description, the field check for each type, and the error format, and browser tests for the widgets and for forms rendered from mocked descriptions.
  • In a real browser against harness serve (the repo's playwright-core + nix Chromium recipe), on a site with an element sidecar that declares a datetime param and an inline element that has one:
    • hand-write dates in each spelling above, then open and save each form;
    • leave fields holding invalid values and check the messages;
    • create a news item;
    • use the News module list's search, sort and filters.

Assumptions to check

  • That each form kind gives the server the context it needs for its description and checks (for the create dialog: parent, page type and language).
  • That one request per focus lost feels instant through deckle-hosted's proxy as well as against a local harness.

Out of scope

  • Letting a sidecar choose a widget (like TYPO3's renderType), and new field types.
  • How the rich-text editor finds inline elements in markdown: that stays in the browser; only their values move to the server.
  • Honoring the site's timeZone.
  • deckle-hosted.

Blocked by

> *This was generated by AI during triage.* ## Summary The browser stops knowing field types. For every form it asks the server which fields the form has, each with its label, widget, widget settings, required flag and current value, already read through the field-type definitions from #163, and renders widgets from that. Errors come only from the server: from a field-check endpoint when a field loses focus, and from every save. Element params are read on the server like page fields, so a hand-written date in an element no longer blocks its save (#158). ## Current behavior - **Page properties already work this way.** `GET /api/page/settings` returns `{fields, values}`, with the values read on the server. - **Every other form is assembled in the browser** from the sidecar schemas (`GET /api/elements`, `GET /api/pagetypes`) plus raw values: - the element form and its item rows (`FormPane.tsx`) take values from the page tree, which `GET /api/page` and every page-changing response return as a plain parse of the file; - the inline-element dialog (`ChipDialog` in `web/src/rte/RichTextField.tsx`) takes params the browser parses out of the body markdown (`web/src/rte/chip.ts`); - the create dialog (`CreateDialog.tsx`) starts from schema defaults, and from the current time for a `datetime` field. - **The browser re-implements type logic:** the type list (`web/src/api/types.ts`), seeding (`initialFieldValue`), checks (`validateFieldValues`, `isRFC3339`), building saved values (`buildFieldValues`, which for inline elements also joins categories with commas), the link's `<name>Target` companion (five files), read-only display (`readonlyValueText`), the module list's cells (`ModuleScreen.tsx`) and its sort rule (`SORTABLE_TYPES` in `web/src/modulequery.ts`). - **#158, reproduced in triage:** a hand-written `start="2026-07-10"` in an element's `datetime` param reaches the form unread. The browser's RFC3339 check rejects it ("Must be a valid date and time") even though the picker shows the date, and that blocks saving any field of the element (in an item row, its parent element) until the date is entered again. The same happens with `2026-07-10 09:00:00`, `2026-07-10T09:00:00` and `10 Jul 2026`. No shipped element has a `datetime` param, but custom elements can. - **Three error formats:** element saves return `{errors: [{param, msg}]}`, properties saves return `{fields: {name: msg}}`, and page create returns only its first error, as text. ## Desired behavior - **The server describes every form.** For each form the editor shows, the browser asks the server and gets, per field: name, label, widget, widget settings (such as a choice's options or a page picker's page type), required, item display, and the current value. This covers: - the element form with its item rows; - the inline-element dialog (the browser sends the element's current params, because they live in unsaved body text); - page properties, in the Page module and on an editor module's Properties tab; - the create dialog; - the module list: its columns, and which fields can be searched, sorted and filtered. - **Values are read on the server** through the type definitions. A value in any spelling Hugo reads arrives as the standard value; an absent or unreadable one arrives as the field's default, else empty. A `datetime` field in the create dialog still starts at the current time. - **The browser holds widgets, not types:** text, multi-line text, number, checkbox, choice, multi-choice, date-time, image picker, link picker, page picker and category picker. A widget draws what the server names and hands back what the user entered. Apart from its widgets' own conversions (such as the date-time picker's local time to and from RFC3339), the browser has no field-type list, no checks and no value conversions. - **Errors come only from the server, in one format:** - **Field check on focus lost:** when a field loses focus, the browser sends its value, with the form it belongs to, to a field-check endpoint, and shows exactly the error a save would report for that field, or clears it. - **Saves** (element, properties, page create, body) report every failing field in that format, with item rows as `items[<i>].<name>`. - **The inline-element dialog** checks its whole form with the server on submit and gets back either the errors or the params to write into the body. The server writes them, for example joining categories with commas. - **A link field has one value** that includes "open in new tab". It is still stored as `<name>` plus `<name>Target`. - **#158 is fixed:** a hand-written date in any spelling Hugo reads, in a block element, an item row or an inline element, shows in the form and saves without being entered again. The file then holds the same instant, as RFC3339. - **AGENTS.md** says a `datetime` shortcode param, like front matter, may be written in any spelling Hugo reads, and that the editor writes RFC3339 (its `datetime` paragraph and "Editing content by hand"). ## Decisions - **The server describes every form**, generalizing the page-properties read (maintainer: "the frontend could just ask, which fields are in this form, load the field definition and build everything based on that"). - **The browser holds widgets, not types**: the split TYPO3 makes between TCA `type` and `renderType`. Each type definition names its widget and the widget's settings. Changing a rule touches only Go, changing a widget touches only the browser, and only a type that needs new UI touches both. - **Errors come only from the server, with a field check on focus lost** (maintainer, for UX). The check runs the same code a save runs, so the two can't disagree. - **No validation rules, such as regexes, served to the browser.** Not every rule is a pattern (real calendar dates, page and category existence, unique selections), and Go and JavaScript regexes are different dialects, so a shared pattern could pass in one and fail in the other. - **No WebAssembly build of the Go rules.** It means a multi-megabyte download, and the site checks still need the server. - **Element params are read when the form loads, not by rewriting the page tree.** The tree stays a plain parse of the file. This settles #158's open question (convert on the server, not in the browser) without touching every response that returns the tree. - **Request and error shapes change freely:** there are no existing sites, and deckle-hosted calls none of these endpoints. ## Key interfaces - The type definitions from #163: each names its widget and widget settings. - New: a form-description endpoint (one, or one per form kind) and a field-check endpoint, with one error format for checks and saves. The shapes are the implementer's choice. - Existing endpoints: `GET /api/page/settings` (`handlePageSettings` in `internal/server/pageops.go`), the model to generalize; `GET /api/elements`, `GET /api/pagetypes`, `GET /api/modules`, `GET /api/module/entries`, `GET /api/element/body`; the saves `POST /api/page/save`, `POST /api/page/settings/save` and `POST /api/page/create`. - Browser starting points: `web/src/components/fields.tsx`, `FormPane.tsx`, `PageSettings.tsx`, `CreateDialog.tsx`, `EntryEditor.tsx`, `ModuleScreen.tsx`, `web/src/rte/RichTextField.tsx`, `web/src/rte/chip.ts`, `web/src/modulequery.ts`, `web/src/api/types.ts`. ## Acceptance criteria - [ ] The element form with its item rows, the inline-element dialog, page properties (Page module and a module's Properties tab) and the create dialog render from server descriptions. - [ ] No browser code outside tests compares a field-type name, and the browser's field-type list is gone. - [ ] A test fails when the server can name a widget the browser doesn't have. - [ ] `start="2026-07-10"`, `"2026-07-10 09:00:00"`, `"2026-07-10T09:00:00"` and `"10 Jul 2026"` in a block element's, an item row's and an inline element's `datetime` param show in the form and save without being touched. The file then holds the same instant, as RFC3339. (#158) - [ ] An unreadable stored value shows as the field's default, else empty. - [ ] Leaving a field that holds an invalid value shows the message a save would report for it; leaving it with a valid value clears the message. - [ ] Every save, page create included, reports all failing fields in the one error format. - [ ] While its values are invalid, the inline-element dialog shows the server's errors and inserts nothing; once they're valid, it inserts the params the server returned. - [ ] A link's "open in new tab" setting works as before in element forms, item rows, inline elements and properties, and its stored keys are unchanged. - [ ] The module list's columns, search, sort and filters behave as before, with what is searchable, sortable and filterable coming from the server. - [ ] AGENTS.md is updated for `datetime` shortcode params (CLAUDE.md rule). - [ ] `make test` passes. ## How to verify - `make test`, with Go tests for each form kind's description, the field check for each type, and the error format, and browser tests for the widgets and for forms rendered from mocked descriptions. - In a real browser against `harness serve` (the repo's playwright-core + nix Chromium recipe), on a site with an element sidecar that declares a `datetime` param and an inline element that has one: - hand-write dates in each spelling above, then open and save each form; - leave fields holding invalid values and check the messages; - create a news item; - use the News module list's search, sort and filters. ## Assumptions to check - That each form kind gives the server the context it needs for its description and checks (for the create dialog: parent, page type and language). - That one request per focus lost feels instant through deckle-hosted's proxy as well as against a local harness. ## Out of scope - Letting a sidecar choose a widget (like TYPO3's `renderType`), and new field types. - How the rich-text editor finds inline elements in markdown: that stays in the browser; only their values move to the server. - Honoring the site's `timeZone`. - deckle-hosted. ## Blocked by - #163
Sign in to join this conversation.
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#164
No description provided.