feat(editor): manage a container's children in the owner's form, saved with it #261
No reviewers
Labels
No labels
bug
enhancement
needs-triage
ready-for-agent
ready-for-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Cloonar/deckle!261
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "afk/259"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #259
What
A
container: trueelement's form (e.g.accordion→accordion-item) gets a children section where an items collection sits, using the same row shape. Each row has: grip, icon, served summary,#uid, Disabled chip, then up/down, eye, pencil and trash.<a href>to the child's edit page, withreturnset to this form. A plain click goes through the router, so the guard runs first.ElementPickerfiltered for this parent the way the overview's nested insert row filters it. It then callseditor.insertat the end of the container and navigates to the new child, returning here.FormRows.tsx.Server
POST /api/page/savetakes an optionalchildren: {order: [{uid, enabled}], removed: [uid]}. If absent, the children are untouched.children.notContainer. On a connected translation it is a 409trans.connectedStructure.children(children.stale) in the same body as any param errors. Nothing is written, params included.internal/elemops/children.go, applied afterApplyEditon the same document. Removal merges text withcontent.MergeText, and reordering uses the newcontent.PermuteShortcodes. Enabled state goes throughSetShortcodeEnabled.syncTranslations(its repair propagates reorders and removals to connected translations; enabled state stays per language), and one snapshot, so a save is one undo step.POST /api/formdescribes a container withchildren: {label}. The label belongs to the one element placement allows inside, or is empty, in which case the client shows a generic heading.One owner per rule
content.PermuteShortcodesis now the single in-place sibling reorder;l10n'sorderListandorderSlotscall it.elemops.RoleAtreplaces the hand walk initemAt.elemops.InsertRefusalcombines the hidden and insertable checks (insert and the children label both use it).routing.elementEditRouteandrouting.isPlainClickreplace hand-built copies. ElementCard's copy was also missing thealtKey/defaultPreventedcheck.role/summary, the same fields the overview sub-list reads.Docs and skills
scaffold/site/AGENTS.md: the Containers paragraph describes the section, the Translations paragraph covers the connected read-only rule, and the "no content" note now excludes containers.add-element(Children bullet and verify step),edit-content,verify-site.preview-site,redo-theme,add-page-type,create-featureandremove-elementwere checked and need no change.Verification
go vet ./...andgo test ./...pass, including new tests ininternal/server/children_test.go,internal/elemops/children_test.goandTestPermuteShortcodes.cd web && npx tsc --noEmit -p . && npm test: 3148/3148 pass, 23 of them new (ElementFormChildren.test.tsx,FormScreenChildren.test.tsx).npm run buildpasses.harness initsite,harness checkis clean. Smoke test withharness serveon a hand-written accordion:/api/formreturnschildren.label = "Accordion item".children, and the file is byte-identical.Notes
/api/element/deletealready leaves. That is existing engine behaviour, unchanged here.FormScreennow fetches/api/pagetypeson mount, for the picker's page-type rule. It could be made lazy if the extra request matters.🤖 Generated with Claude Code
A container element's form (accordion → accordion-item) gains a children section with the items collection's row shape: grip, icon, served summary, #uid, Disabled chip, up/down, eye, pencil (a real link to the child's form with a return here) and trash can. Reorder, enable/disable and remove are pending until Save; Add is immediate and opens the placement-filtered insert picker, then the new child's form. In a connected translation the rows are read-only apart from the pencil. POST /api/page/save takes an optional `children: {order, removed}` applied as the element operations it stands for (delete, move within the container, enable/disable) on the same document as the params: validated before anything is applied (a stale set is a 422 field error on "children", nothing written), one write, translation repair, one snapshot. POST /api/form describes a container with `children: {label}`. One owner per rule: content.PermuteShortcodes is the single in-place sibling reorder (l10n's two copies call it), elemops.RoleAt replaces itemAt's walk, elemops.InsertRefusal combines the hidden/insertable checks, routing.elementEditRoute and isPlainClick replace hand-built copies, and the items rows share their row pieces with the new section (FormRows). AGENTS.md and the add-element, edit-content and verify-site skills describe the children section. Closes #259 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>[autoland] verdict: pass
Verdict: CONCERNS (non-blocking), approved, not auto-merged
What I relied on: CI
ci / build-and-testcame back green on both the original headee96b1c(run 262) and the new head56a8578(run 263). On the merged tree I also rango vet ./...andgo test ./...(all packages pass),npx tsc --noEmit -p .,npm test(3166/3166 in 123 files),npm run build, andharness checkon a freshharness initsite, which was clean.Conventions: the title is a Conventional Commit,
feat(editor): …. The body hasCloses #259. The diff stays within the issue's scope. It touchesscaffold/site/AGENTS.md(Containers, Translations, the "no content" note) and theadd-element,edit-contentandverify-siteskills, as CLAUDE.md requires. The one-owner refactors also follow CLAUDE.md "Every rule has exactly one owner":content.PermuteShortcodes,elemops.RoleAt,elemops.InsertRefusal,routing.elementEditRouteandrouting.isPlainClick.Merge conflict: main had moved on (#258, sidecar label translations). Three files conflicted, and I merged origin/main into afk/259 as
56a8578(merge, no force-push). All three were unions of separate additions:README.md: the/api/formshape now has bothchildren?and the field'si18n?.internal/server/forms.go: imports bothpagetypeandsidecardir; both are used.scaffold/site/.claude/skills/add-element/SKILL.md: the verify step keeps both the container children-section check and the per-language label check.Concern: the children-section heading skips #258's label translation
This comes from #258 and this PR landing at about the same time. Since #258, every sidecar label the client shows goes through
web/src/api/labels.ts(thei18nmap from<name>.<lang>.yaml). The items description goti18nanditemI18nfor this. The new children description did not:internal/server/forms.go:149(childrenDescription.Label) and:690(return only.Label) send the raw sidecar label with noi18n.web/src/api/forms.ts:158(the decoder) resolvesitemslabels but neverchildren.web/src/components/ChildrenSection.tsx:272(heading) and:470(Add button) therefore show the untranslated label.What a user sees: on the shipped scaffold with the editor in German, the accordion form's children heading reads "Accordion item" and the Add button "Accordion item hinzufügen", where both should say "Akkordeon-Eintrag" (
scaffold/editor/elements/accordion-item.de.yaml). The rows and the insert picker are translated, because they go throughelementDisplay. The suggested fix is small:I18n sidecardir.I18n \json:"i18n,omitempty"`tochildrenDescription, set fromonly.I18n.Labels()`;resolveLabel(desc.children)in theforms.tsdecoder;The feature itself works and is well covered. I'm leaving the merge to a human because of this concern.
Fixed the label-translation concern in
4c8ebae.childrenDescriptionnow carriesI18n sidecardir.I18n \json:"i18n,omitempty"`, set from the child element'sI18n.Labels().childrenLabelbecamechildElement` and returns the schema, so label and translations come from one place.forms.tsdecoder callsresolveLabel(desc.children), andChildrenDescriptiondeclaresi18n?.TestFormDescribeChildrenadds anaccordion-item.de.yamland expects{"label":"Accordion item","i18n":{"de":{"label":"Akkordeon-Eintrag"}}};labels.test.tschecks the heading resolves to the German label, keeps the written one without a translation, and keeps""for the generic heading./api/formshape now readschildren: {label, i18n?}.Verified:
go vet ./...andgo test ./...pass;npx tsc --noEmit -p .,npm test(3167/3167) andnpm run buildpass.[autoland] verdict: pass
PASS. Signal relied on: CI
ci / build-and-testgreen on run 264, the run for the current head4c8ebae(runs 262 and 263 covered the earlier heads, as the autoland comment records). Nothing re-run.Closes #259; AGENTS.md and the add-element, edit-content and verify-site skills are updated as CLAUDE.md requires.4c8ebae:childrenDescriptioncarriesi18nfrom the child schema'sI18n.Labels(), the forms.ts decoder resolves it withresolveLabel, README names the key, tests on both sides. Reviewed the fix diff in full.internal/elemops/children.go:CheckChildrenruns entirely beforeApplyChildren, so a stale uid set writes nothing, params included; the enabled comparison and the slot move/toggle refusal are correct.