Skip to content

feat(studio,ui,api): RFC-0045 step 6a, reference and repeater fields in the entry editor - #219

Merged
Madraka merged 4 commits into
mainfrom
claude/rfc45-step6a
Nov 13, 2026
Merged

Madraka merged 4 commits into
mainfrom
claude/rfc45-step6a

Conversation

@Madraka

@Madraka Madraka commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

RFC-0045 rollout step 6, first of two PRs: the blocks and the entry editor. Step 6b (the builder, the delete dialog and the publish warnings) follows once this merges.

Both types stay refused on every production write path until step 7, so no production collection can define them and Studio looks unchanged there.

Blocks (packages/ui/src/blocks, all words as props)

Block What it does
FieldControl One typed control, extracted from EntryFieldsForm (444 → ~240 lines); EntryFieldsForm and repeater items share it. A reference is a fieldset around the host's renderReference.
RepeaterField (+ repeater-state.ts) Items as fieldsets with an ICU legend ("Item 2: Ada Lovelace"), folded in Collapsibles; "Add item" + "3 of 20"; up/down icon buttons from sm and a ⋯ menu (move, duplicate, remove). Focus goes to the new item's first control after add, to the next item's toggle (else "Add item") after remove; onRemoved hands the host an undo. Moves and removals are announced in a polite live region. A new error map opens the items it names; collapsed items with problems are listed above the items.
ReferenceList InsetPanel rows: the title linked, collection · language, "Draft" / "In review" / "Archived" with "Will not show until published", "Not found" with the id in mono; up/down from sm and ⋯ (open, move, remove).
EntryPicker A LayerDialog opened by its trigger, with focus returning to it. A labelled search over a combobox listbox (aria-activedescendant, arrows, Home, End, Enter) and a polite result count. Collection and status badges. One pick closes the dialog; several are ticked up to the remaining count. "Show more" or "Keep searching" follows the cursor after a truncated read. It takes a search(q, cursor) prop and makes no API calls itself.
UsedByList A two-column Table (the entry, where the value sits, who else shows it; the status), so it fits 390 px.

Studio

  • FieldReference composes ReferenceList and EntryPicker around GET …/entries/pick. The entry's own group and targets already chosen can't be picked.
  • Repeaters use FieldImage and FieldReference for their sub-fields. Removing an item shows an info toast with "Undo" for 10 s.
  • Save issues map by path: fields/speakers/2/person shows on that control.
  • On a translation:
    • shared references and repeaters are read-only, with "From the original" and a link;
    • empty localized ones say "The original: …";
    • a shared list says "Not translated: this list is shared by every language." on multilingual sites.
  • "Used by" (UsedByCard) is a flush SectionCard from GET …/entries/:id/references, published referrers first, with "Show more". It shows only when some field of the site can reference the collection, so no production site shows it yet.
  • EntryFieldCards lazy-loads both cards; ArticleEditorPage lost its inline lazy (446 → 442 lines in the baseline).
  • Bundle: every new module is in LAZY_ONLY. The initial load is unchanged at 391.9 kB gzip.

API (one deviation)

New read-only GET /api/sites/:siteId/entries/groups?ids=a,b (content:read; scopes table and playground allow-list). It answers each group's members in the site, any status, as the picker's cards, with [] for a group with none left. It takes 1 to 100 well-formed ids.

The picker finds new targets only, and references lists referrers, so the editor otherwise had no way to name a stored id or say "Not found". A shared-suite case runs on SQLite and D1.

Deviations (also in the RFC, "Amended in 6a")

  • Legends read "Item 2: Ada Lovelace", not "Speaker 2: …". The contract has no singular label for a repeater. The name comes from itemLabel, else the first text, select or reference sub-field.
  • The member the editor shows for a target is the one in the entry's language, else the default language's, else the first, whatever its status. This is not pickMember; the locale-fallback cases are left to the publish warnings in 6b.
  • EntryPicker's several-choice marks are check icons inside each option, not Kumo Checkboxes, so the listbox nests no controls. A group found in several languages is listed once.
  • No capability check in 6a. The editor can't meet the types in production before step 7. 6b gates the builder on the server's capability.

i18n

New words go in the lazy fields namespace (fields.repeater.*, fields.reference.*, fields.usedBy.*, three editor keys and two issue codes). en is hand-written. The other 18 languages are AI drafts awaiting review, imported with pnpm i18n:import. No language is marked reviewed.

Checks

  • pnpm design:lint (rtl-logical and rtl-icon included) and pnpm size:check pass.
  • pnpm design:shots at 1440 and 390 px passes: no sideways scroll, no console errors. It covered the event editor, a referenced place ("Used by") and the open picker, on a local scratch site seeded through a store with fieldTypes.
  • New tests:
    • packages/ui/test/repeater-field.test.tsx: add focus, move focus and announcement, remove focus and undo, last-item removal to "Add item", duplicate, errors opening items and the collapsed list;
    • entry-picker.test.tsx: search through the prop, polite count, arrows and Enter, focus back to the trigger, several up to the remaining count, keep searching from the cursor, plus ReferenceList and UsedByList;
    • entry-fields-form.test.tsx: slots and paths, errors by path, translation fallbacks, undo hand-off;
    • apps/studio/test/fields-references.test.tsx: picker over the pick route, Not found, the undo toast, item issues, Used by.
  • pnpm check locally. Lint, size, generated-file checks and typecheck pass. The test stage has three caveats:
    • Run with turbo run test --concurrency=1 --continue, 18 of 19 packages pass, including Studio (84 files) and UI (20). The default --concurrency=3 overloads this machine: unrelated Studio tests time out at 20 s.
    • I raised the timeout of the one test that kept timing out. i18n-messages.test.ts "every catalog file is valid ICU" now has 60 s instead of the 20 s default; it compiles every catalog synchronously and takes about 5 s alone.
    • core-ts: every SQLite variant passes. Only the Miniflare D1 and Durable Object variants of store.test.ts and list-values-query.test.ts fail, all with EADDRNOTAVAIL. That is ephemeral-port exhaustion, with another project's workerd processes running. core-ts's change here is type-only, and CI covers those variants.

🤖 Generated with Claude Code

Madraka and others added 4 commits November 13, 2026 11:51
…in the entry editor

New @cloudhal/ui blocks, built from Kumo: FieldControl (one typed control,
extracted from EntryFieldsForm), RepeaterField (items as fieldsets with ICU
legends, add/move/duplicate/remove through buttons, focus after add and
remove, undo, a polite live region, errors opening their items),
ReferenceList, EntryPicker (labelled search, listbox with arrow keys and
Enter, polite result count, cursor and truncated reads, focus back to the
trigger; no API calls of its own) and UsedByList.

Studio's entry editor renders references through FieldReference (around
GET .../entries/pick and the new GET .../entries/groups), repeaters with
FieldImage and FieldReference sub-fields, item errors by path, translation
fallbacks, an undo toast, and a "Used by" card. Everything loads with the
lazy fields chunks; the initial bundle is unchanged. Words live in the lazy
fields namespace; every other language is an AI draft through i18n:import.

Both types stay refused on every production write path until step 7.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
- Repeater undo writes through the latest onChange and items; EntryFieldsForm
  writes over its latest value and the editor updates its draft functionally,
  so an undo keeps later edits and the saved rev. The toast is scoped to its
  entry (closed on leave/unmount); undo returns false when the list is gone
  or full, and Studio says so.
- Item errors are re-keyed to stable item keys when they arrive, so they
  follow their items through moves, removals and undo.
- Live regions empty before each message (repeated messages re-announce).
- ReferenceList keeps focus on a moved row, moves it to the next row or the
  picker trigger after a remove, and announces both.
- EntryPicker: a full field keeps its trigger focusable (aria-disabled); a
  failed search is the polite status; the spinner is hidden from AT.
- Groups read sends only well-formed group ids; others show "Not found"; a
  failed read shows an error banner with retry.
- During running field operations: renamed keys (top level and sub-field)
  are read and written through the marker; unconverted values show
  read-only as stored with "Being changed".
- Saves leave out empty repeater items.
- i18n: six new fields keys, AI drafts for 18 languages.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
- Item issues after a save that left empty items out are mapped back onto
  the draft's items (sentIndexes, draftPath) before the error map is built,
  so a problem never lands on an empty item.
- A read-only repeater shows every item open (its toggles sit in the
  editor's disabled fieldset).
- "Used by" moved to the reach cards (EntryUsedByCard, beside "In menus"),
  outside the read-only fieldset, so "Show more" works when the person
  can't edit the article.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
- Duplicate leaves empty repeater items out, like a save (duplicateOf runs
  withoutEmptyItems; the page function shrinks, baseline tightened).
- Item issues map back through the entry the failed save sent (the
  mutation's variables, passed as `save`), not the draft at error time.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
@Madraka
Madraka merged commit f823aa1 into main Nov 13, 2026
8 checks passed
@Madraka
Madraka deleted the claude/rfc45-step6a branch November 13, 2026 11:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant