editor: manage a container's children in the owner's form, saved with it #259

Closed
opened 2026-10-13 11:33:55 +02:00 by dominik.polakovics · 0 comments

Summary

A container's children (accordion → accordion-item) are managed from the container's own form, TYPO3-style: a rows list in the owner's form with reorder, enable/disable, remove, an edit link to the child's own form and an Add button. Reorder, enabled state and removals are pending until the form is saved and go in the same save as the owner's params. This is the first half of "one way to show and edit an element's children"; the second half (#260, the overview digest that replaces nested cards) depends on it, because once the page overview stops nesting cards the form is the only place a container child can be reached.

Mockup (private artifact, decided with the maintainer; the "Accordion form (container)", "Cards form (items)" and "Image form (items)" tabs are this issue): https://claude.ai/artifact/7TnAN5rACaDYNv6iZPsak5

Current behavior

  • An items owner (image, text-media, cards) edits its hidden rows inside its own form: the repeatable collection in web/src/components/ElementForm.tsx (grip, up/down, eye toggle, trash can, collapsible rows with a thumbnail for the first image field, Add via the media picker or an empty row), all pending until Save, sent as the items array of POST /api/page/save (savePageRequest in internal/server/server.go) and replaced wholesale.
  • A container (container: true) has no such section. Its children are reached only on the page overview, where hasSubList (web/src/elementcard.ts) nests them as cards under the parent card with their own insert rows, drag and drop, ⋮ menu and eye toggle, each operation applied immediately through /api/element/{move,delete,insert} and /api/element/enabled.
  • Insert anywhere on the overview (insert in web/src/stores/pageEditor.ts) creates the element immediately and navigates to its edit page.

Desired behavior

The form of a container element gains a children section, in the same place and with the same row shape as an items collection:

  • A heading with the collection's label and a count, and an Add button.
  • One row per child element (every role: "element" child in the served tree), in document order: grip (drag to reorder), the child's element icon, its served summary (label when empty), its #uid, a Disabled chip when disabled, then up, down, eye toggle, a pencil and a trash can.
  • The rows have no expander and no inline fields: a container child has its own form. The pencil is a real link to the child's edit page with a return to this form, like the overview's pencil.
  • Reorder, enable/disable and remove are pending: they mark the form dirty, show on the row (a moved row, a removed row struck through with an undo, as the mockup shows) and are applied only by Save, in the same request as the owner's params and body. Closing the form or following the pencil with pending changes goes through the existing unsaved-changes guard (web/src/leave.ts). Discard restores the served order and states.
  • Add is the one immediate action: it opens the categorised insert picker filtered to what may go inside this container (its placement, the same filter the overview's nested insert row uses) and, exactly like today's overview insert, creates the child right away at the end of the container and navigates to the new child's form. With pending changes in the owner's form the unsaved-changes guard runs first. Reason: a container child has required params of its own (accordion-item's summary) and no inline fields here, so a pending blank row would land an invalid element on Save.
  • One save, one transaction. The save request carries the children's final state — order of existing uids, enabled per uid, removed uids — and the server applies it as the existing element operations (move, enabled, delete) on top of the param save, refusing the whole save with the usual error body if any one step is refused; nothing is applied on failure. This keeps uids, bodies, nested content and translations of the children intact (never a wholesale replace, unlike items), and makes the save one undo step.
  • In a connected translation, the rows follow the source: order, enabled state and membership are source-language facts, so no reorder, remove, toggle or Add; the pencil still works. Same rule the items collection already applies to its rows in a connected translation. A free translation edits like a source.
  • The overview keeps its nested cards and nested insert rows for now (removed in #260). While both exist they must agree: a container's form rows and its overview sub-list show the same children in the same order with the same enabled states, read from the same served tree.

Decisions

  • Children are managed in the owner's form, not on the overview — one place for every kind of child (items and container children alike) so the overview can become a digest (#260). Maintainer decision, mockup linked above.
  • Reorder/enable/remove are pending until Save, like TYPO3 and like the items collection — an editor reorders, then saves; nothing in a form applies behind their back.
  • Add is immediate and opens the new child's form — see Desired behavior; it mirrors the overview's insert flow so there is one insert behavior in the editor.
  • Container rows are never sent as form values and never replaced wholesale — they are elements with bodies, uids and translations; the server applies the row changes as the ordinary element operations so every rule those operations enforce (placement, translation repair, uid stability, pending-change counting) keeps its one owner.
  • Atomic save — a half-applied reorder must never land; one undo step.
  • Connected translations: rows read-only except the pencil — the connected invariant (AGENTS.md "Translations") makes structure a source-language fact.
  • No per-element sidecar syntax for any of this — dropped by the maintainer; the sidecar's existing container, items, summary and placement keys decide everything.

Key interfaces

  • POST /api/page/save (savePageRequest, internal/server/server.go): a new optional children field beside items, absent = leave children untouched; present = the final order of existing child uids with per-uid enabled state and the removed uids. Validation: every uid must be a current role: "element" child of this element, no duplicates, no unknown uids; a refusal reports a field error the client can show on the section.
  • POST /api/form description (formDescription, internal/server/forms.go): the form needs to know it describes a container and what label to show for the children section; the children themselves come from the page tree the client already holds (roles and summaries are the server's verdicts, internal/elemops), so the description need not repeat them.
  • Element operations the save composes: internal/server/element.go (move, delete) and element_enabled.go; the pending-changes and translation code paths those already run through.
  • Client: the items collection in web/src/components/ElementForm.tsx is the row vocabulary to reuse (ItemRow, RowsBaseline, dirty tracking, the row header); insert and the navigation-with-return in web/src/stores/pageEditor.ts; the picker filter for a parent in web/src/elementcrud.ts (insertRefusal, pickerVisible); the unsaved-changes guard in web/src/leave.ts.
  • Child role and summary: elemops.ChildRole / elemops.ElementSummary via the served role and summary on every node — the client reads them, never re-derives (CLAUDE.md "Every rule has exactly one owner").
  • Docs and skills (CLAUDE.md rules): scaffold/site/AGENTS.md — the "Containers" paragraph of the element sidecar section and the editor description of how an element's children are edited; scaffold/site/.claude/skills/add-element/SKILL.md (the Children bullet and the verification step for a container element now says to check the children section of the parent's form); edit-content/SKILL.md where it describes visible children; verify-site/preview-site if they walk the overview's nested cards. scaffold/skills_test.go and internal/element/agentsdoc_test.go must stay green.

Acceptance criteria

  • Opening an accordion element's form shows a children section listing its accordion-item children in document order with served summary, uid, enabled state, grip, up/down, eye, pencil and trash can; a container with no children shows the empty section with Add.
  • Reordering (drag or arrows), toggling and removing rows marks the form dirty and changes nothing on disk until Save; Discard restores the served state; Close and the pencil run the unsaved-changes guard when dirty.
  • Save writes the owner's params/body and the children's order, enabled states and removals in one request; the content file afterwards has the children in the new order, with disabled set per the toggles and removed children gone, every surviving child keeping its uid, params and body.
  • A save whose children part is refused (for example a uid that is no longer a child) applies nothing — params included — and the form shows the error.
  • Add opens the insert picker limited to elements allowed inside this container, creates the chosen element at the end of the container immediately and navigates to its form with a return to the owner's form; Back returns to the owner's form showing the new row.
  • In a connected translation the rows are read-only (no grip, arrows, eye, trash or Add) and the pencil opens the translated child's form.
  • An items owner's collection behaves exactly as before (collapsible rows, thumbnail, trash can, pending until Save, wholesale items replace).
  • While nested cards remain on the overview, a container's form rows and its overview sub-list agree after every save.
  • Pending-change counts, undo, and the translation repair treat the children changes as the element operations they are (one save = one undo step).
  • scaffold/site/AGENTS.md and the affected skills describe the children section; drift tests pass.

How to verify

  • go test ./... (needs web/dist), cd web && npm test, harness check on the scaffolded site.
  • Client tests for the new section: reorder/toggle/remove mark dirty and are sent only on Save; Add navigates; connected-translation read-only.
  • Server tests: the children save on a kitchen-sink page with a nested accordion — reorder, disable, remove, invalid uid refusal leaves the file byte-identical, round-trip canonical.
  • Manual: harness serve the scaffold site, open the showcase page's accordion, reorder and disable items, Save, reload, check the rendered page.

Assumptions to check

  • The items collection's rows already have a trash can, collapse/expand and an image thumbnail (ElementForm.tsx); confirm nothing there needs changing beyond sharing the row header with the new section.
  • That /api/element/move can be composed inside the save handler without re-reading the page between steps; if the operations each load and write the file, the save must still be atomic (apply on one in-memory document, write once).
  • How pending changes / undo count a save that also moves children — check internal/server pending-change code and web/src/undoinfo.ts before deciding how one save is reported.

Out of scope

  • Replacing the overview's nested cards, insert rows and image-only thumbnails with a digest — #260.
  • Any sidecar key for how children are shown.
  • Moving a child into another container from the form (cut and paste on the overview still does that).
  • Inline fields for container children in the parent's form.
  • Changing the accordion-item sidecar's icon (it is a chevron that reads like an expander next to collapsible rows; a separate call for the theme).

Blocked by

None - can start immediately

## Summary A container's children (`accordion` → `accordion-item`) are managed from the container's own form, TYPO3-style: a rows list in the owner's form with reorder, enable/disable, remove, an edit link to the child's own form and an Add button. Reorder, enabled state and removals are pending until the form is saved and go in the same save as the owner's params. This is the first half of "one way to show and edit an element's children"; the second half (#260, the overview digest that replaces nested cards) depends on it, because once the page overview stops nesting cards the form is the only place a container child can be reached. Mockup (private artifact, decided with the maintainer; the "Accordion form (container)", "Cards form (items)" and "Image form (items)" tabs are this issue): https://claude.ai/artifact/7TnAN5rACaDYNv6iZPsak5 ## Current behavior - An items owner (`image`, `text-media`, `cards`) edits its hidden rows inside its own form: the repeatable collection in `web/src/components/ElementForm.tsx` (grip, up/down, eye toggle, trash can, collapsible rows with a thumbnail for the first image field, Add via the media picker or an empty row), all pending until Save, sent as the `items` array of `POST /api/page/save` (`savePageRequest` in `internal/server/server.go`) and replaced wholesale. - A container (`container: true`) has no such section. Its children are reached only on the page overview, where `hasSubList` (`web/src/elementcard.ts`) nests them as cards under the parent card with their own insert rows, drag and drop, ⋮ menu and eye toggle, each operation applied immediately through `/api/element/{move,delete,insert}` and `/api/element/enabled`. - Insert anywhere on the overview (`insert` in `web/src/stores/pageEditor.ts`) creates the element immediately and navigates to its edit page. ## Desired behavior The form of a container element gains a children section, in the same place and with the same row shape as an items collection: - A heading with the collection's label and a count, and an Add button. - One row per child element (every `role: "element"` child in the served tree), in document order: grip (drag to reorder), the child's element icon, its served summary (label when empty), its `#uid`, a Disabled chip when disabled, then up, down, eye toggle, a pencil and a trash can. - The rows have no expander and no inline fields: a container child has its own form. The pencil is a real link to the child's edit page with a `return` to this form, like the overview's pencil. - Reorder, enable/disable and remove are **pending**: they mark the form dirty, show on the row (a moved row, a removed row struck through with an undo, as the mockup shows) and are applied only by Save, in the same request as the owner's params and body. Closing the form or following the pencil with pending changes goes through the existing unsaved-changes guard (`web/src/leave.ts`). Discard restores the served order and states. - **Add is the one immediate action**: it opens the categorised insert picker filtered to what may go inside this container (its placement, the same filter the overview's nested insert row uses) and, exactly like today's overview insert, creates the child right away at the end of the container and navigates to the new child's form. With pending changes in the owner's form the unsaved-changes guard runs first. Reason: a container child has required params of its own (accordion-item's summary) and no inline fields here, so a pending blank row would land an invalid element on Save. - **One save, one transaction.** The save request carries the children's final state — order of existing uids, enabled per uid, removed uids — and the server applies it as the existing element operations (move, enabled, delete) on top of the param save, refusing the whole save with the usual error body if any one step is refused; nothing is applied on failure. This keeps uids, bodies, nested content and translations of the children intact (never a wholesale replace, unlike `items`), and makes the save one undo step. - In a **connected translation**, the rows follow the source: order, enabled state and membership are source-language facts, so no reorder, remove, toggle or Add; the pencil still works. Same rule the items collection already applies to its rows in a connected translation. A free translation edits like a source. - The overview keeps its nested cards and nested insert rows for now (removed in #260). While both exist they must agree: a container's form rows and its overview sub-list show the same children in the same order with the same enabled states, read from the same served tree. ## Decisions - Children are managed in the owner's form, not on the overview — one place for every kind of child (items and container children alike) so the overview can become a digest (#260). Maintainer decision, mockup linked above. - Reorder/enable/remove are pending until Save, like TYPO3 and like the items collection — an editor reorders, then saves; nothing in a form applies behind their back. - Add is immediate and opens the new child's form — see Desired behavior; it mirrors the overview's insert flow so there is one insert behavior in the editor. - Container rows are never sent as form values and never replaced wholesale — they are elements with bodies, uids and translations; the server applies the row changes as the ordinary element operations so every rule those operations enforce (placement, translation repair, uid stability, pending-change counting) keeps its one owner. - Atomic save — a half-applied reorder must never land; one undo step. - Connected translations: rows read-only except the pencil — the connected invariant (AGENTS.md "Translations") makes structure a source-language fact. - No per-element sidecar syntax for any of this — dropped by the maintainer; the sidecar's existing `container`, `items`, `summary` and placement keys decide everything. ## Key interfaces - `POST /api/page/save` (`savePageRequest`, `internal/server/server.go`): a new optional children field beside `items`, absent = leave children untouched; present = the final order of existing child uids with per-uid enabled state and the removed uids. Validation: every uid must be a current `role: "element"` child of this element, no duplicates, no unknown uids; a refusal reports a field error the client can show on the section. - `POST /api/form` description (`formDescription`, `internal/server/forms.go`): the form needs to know it describes a container and what label to show for the children section; the children themselves come from the page tree the client already holds (roles and summaries are the server's verdicts, `internal/elemops`), so the description need not repeat them. - Element operations the save composes: `internal/server/element.go` (move, delete) and `element_enabled.go`; the pending-changes and translation code paths those already run through. - Client: the items collection in `web/src/components/ElementForm.tsx` is the row vocabulary to reuse (`ItemRow`, `RowsBaseline`, dirty tracking, the row header); `insert` and the navigation-with-return in `web/src/stores/pageEditor.ts`; the picker filter for a parent in `web/src/elementcrud.ts` (`insertRefusal`, `pickerVisible`); the unsaved-changes guard in `web/src/leave.ts`. - Child role and summary: `elemops.ChildRole` / `elemops.ElementSummary` via the served `role` and `summary` on every node — the client reads them, never re-derives (CLAUDE.md "Every rule has exactly one owner"). - Docs and skills (CLAUDE.md rules): `scaffold/site/AGENTS.md` — the "Containers" paragraph of the element sidecar section and the editor description of how an element's children are edited; `scaffold/site/.claude/skills/add-element/SKILL.md` (the Children bullet and the verification step for a container element now says to check the children section of the parent's form); `edit-content/SKILL.md` where it describes visible children; `verify-site`/`preview-site` if they walk the overview's nested cards. `scaffold/skills_test.go` and `internal/element/agentsdoc_test.go` must stay green. ## Acceptance criteria - [ ] Opening an `accordion` element's form shows a children section listing its `accordion-item` children in document order with served summary, uid, enabled state, grip, up/down, eye, pencil and trash can; a container with no children shows the empty section with Add. - [ ] Reordering (drag or arrows), toggling and removing rows marks the form dirty and changes nothing on disk until Save; Discard restores the served state; Close and the pencil run the unsaved-changes guard when dirty. - [ ] Save writes the owner's params/body and the children's order, enabled states and removals in one request; the content file afterwards has the children in the new order, with `disabled` set per the toggles and removed children gone, every surviving child keeping its uid, params and body. - [ ] A save whose children part is refused (for example a uid that is no longer a child) applies nothing — params included — and the form shows the error. - [ ] Add opens the insert picker limited to elements allowed inside this container, creates the chosen element at the end of the container immediately and navigates to its form with a return to the owner's form; Back returns to the owner's form showing the new row. - [ ] In a connected translation the rows are read-only (no grip, arrows, eye, trash or Add) and the pencil opens the translated child's form. - [ ] An items owner's collection behaves exactly as before (collapsible rows, thumbnail, trash can, pending until Save, wholesale `items` replace). - [ ] While nested cards remain on the overview, a container's form rows and its overview sub-list agree after every save. - [ ] Pending-change counts, undo, and the translation repair treat the children changes as the element operations they are (one save = one undo step). - [ ] `scaffold/site/AGENTS.md` and the affected skills describe the children section; drift tests pass. ## How to verify - `go test ./...` (needs `web/dist`), `cd web && npm test`, `harness check` on the scaffolded site. - Client tests for the new section: reorder/toggle/remove mark dirty and are sent only on Save; Add navigates; connected-translation read-only. - Server tests: the children save on a kitchen-sink page with a nested accordion — reorder, disable, remove, invalid uid refusal leaves the file byte-identical, round-trip canonical. - Manual: `harness serve` the scaffold site, open the showcase page's accordion, reorder and disable items, Save, reload, check the rendered page. ## Assumptions to check - The items collection's rows already have a trash can, collapse/expand and an image thumbnail (`ElementForm.tsx`); confirm nothing there needs changing beyond sharing the row header with the new section. - That `/api/element/move` can be composed inside the save handler without re-reading the page between steps; if the operations each load and write the file, the save must still be atomic (apply on one in-memory document, write once). - How pending changes / undo count a save that also moves children — check `internal/server` pending-change code and `web/src/undoinfo.ts` before deciding how one save is reported. ## Out of scope - Replacing the overview's nested cards, insert rows and image-only thumbnails with a digest — #260. - Any sidecar key for how children are shown. - Moving a child into another container from the form (cut and paste on the overview still does that). - Inline fields for container children in the parent's form. - Changing the accordion-item sidecar's icon (it is a chevron that reads like an expander next to collapsible rows; a separate call for the theme). ## Blocked by None - can start immediately
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#259
No description provided.