Skip to content

feat(studio,api): RFC-0045 step 6b, builder, delete dialog and publish warnings - #220

Merged
Madraka merged 3 commits into
mainfrom
claude/rfc45-step6b
Nov 13, 2026
Merged

Madraka merged 3 commits into
mainfrom
claude/rfc45-step6b

Conversation

@Madraka

@Madraka Madraka commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

RFC-0045 rollout step 6, second PR: the fields builder, the delete dialog and the publish warnings. Step 6a (#219) covered the blocks and the entry editor.

Production is unchanged. Both types stay refused on every production write path until step 7, and the builder offers them only where the server reports the capability.

The capability gate

/api/instance gains features.referenceFields. It is true when the store has SiteGraphStoreOptions.fieldTypes, the same rule MCP's tool lists use (step 5).

Studio offers Reference and List in the type Select, and as "Change type…" targets, only when it is on (useReferenceTypes). A saved field of either type still shows its type.

Tests mock the flag. design:shots ran against a local Studio whose site was seeded through a store with fieldTypes (saved definitions show without the flag).

Builder (FieldDialog)

  • Reference (ReferenceFieldsForm.tsx):
    • "Target collections" is a Checkbox.Group of the site's collections, one to five.
    • "Several" is for new fields only; a saved field changes it through "Change type…". With it, "At most" (1 to 50).
    • "Only entries where" reuses the page editor's ConditionRow, now exported with noNow since dates can't be now here. It works over the single target collection's listing fields; a muted line explains when there are several targets or no listing fields.
  • List (SubFieldList.tsx):
    • Item fields are rows (label, key, type, "Not saved") with up and down and a ⋯ menu: Edit…, and Remove for an unsaved one. A saved one is removed through the field's operations.
    • Rows carry stable ids. Focus stays on the moved row's arrow, or the other arrow at an end, and goes to the next row or "Add item field" after a removal.
    • "Add item field" opens a nested LayerDialog with the same form in sub-field mode: sub-field types, no languages switch, search or "Several"; a reference sub-field takes collections only.
    • Also "At most" and "Item name".
  • Builder state: the field being edited is keyed by its key (it was an index), and edits use functional updates.

Operations (FieldOperationDialog, OperationInputs.tsx)

  • Sub-fields: for a list, "Applies to" picks the whole field or one item field for rename, change type and remove, and sends sub.
  • New targets: "Change type…" offers reference and list targets behind the gate, with the step-4 inputs (collections, multiple, maxItems, subFields, itemLabel) drawn with the builder's own parts.
  • Stuck runs: a run or revert can get stuck when a pass rewrites nothing while unconvertible is non-empty. Before, the dialog looped up to 200 times; now it stops. The dialog shows a "Clear and finish" step: an alert, a confirmation checkbox, and a destructive primary button that sends onInvalid: "clear" on the continue or the revert.

Delete dialog (EntryDeleteDialog, used by the editor and the articles list)

  • API: DELETE …/entries/:id?rev=&dryRun=1 runs the store's own delete plan (deleteContentEntry(entry, { dryRun })) and writes nothing. It answers { dryRun: true } or the plan's refusal.
  • The store decides. While it refuses with entry_referenced, Delete stays off: DeleteResource gains a deleteDisabled prop. An error banner says "Unpublish or edit these n published entries first" and that archiving doesn't help, and UsedByList lists the referrers from the refusal's details.
  • Allowed with no other entry of the group left: draft referrers are listed, each "Will show nothing for this field".
  • Allowed with other entries left: the dialog names the language whose pages lose the card. With default-with-notice, it says they'll show the default language's version instead.
  • Delete stays off while the check runs. A failed check leaves it on, since the delete itself is guarded.
  • Unlinking a translation: Studio has no unlink action today. The store's entry_referenced and translation_group_in_use refusals of a group change now have words in errors.api, along with the other RFC-0045 codes.

Publish warnings

  • API: GET …/entries/reference-issues (content:read, on the playground allow-list) runs referenceIssues over the working copy. It returns the first 50, each with the holder's title, and the total. A site without reference fields answers without reading its entries.
  • The site's publish dialog shows a lazy PublishWarnings: an alert ("n references will show nothing", and that publishing goes ahead) and up to five holders, each linked, with the language, the field and the reason.

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

  1. The capability is instance-wide (/api/instance), not per site, as in MCP. The store still refuses per site (fields_unsupported, now worded).
  2. Two new API surfaces: the delete dry run and reference-issues.
  3. Publish warnings appear only in the site's publish dialog. The entry editor's "Publish" and snapshot publishes don't show them.
  4. Studio has no unlink action to apply the delete rules to.
  5. docs/guides/fields.md is left to step 7, as the brief allowed.

Bundle

The initial load goes from 391.9 to 392.9 kB gzip (budget 420): about +0.3 kB for the ten new errors.api sentences, and about +0.6 kB for the lazy publish-warnings wrapper and one chunk the split adds. Everything else loads with the lazy fields chunks.

i18n

New words in the lazy fields namespace (56 keys) and errors.api (10). en is hand-written; the other 18 languages are AI drafts awaiting review, imported with pnpm i18n:import. No language is marked reviewed. Two keys were named …limit because the i18n test reads …Many as a hand-made plural.

Checks

  • Biome, design:lint and size:check pass. Three functions were split to stay under 150 lines, and createApi keeps its line count.
  • Typecheck passes for studio, ui, api, cli and cloudflare.
  • design:shots at 1440 and 390 px passes, with no sideways scroll and no console errors, on three screens: the builder with a list's item fields, the delete dialog blocked by published referrers, and the publish dialog with a warning.
  • Tests:
    • apps/studio/test/fields-builder-references.test.tsx (10): the gate on and off; editing a reference's collections and conditions and saving; refusing a reference without collections; adding an item field in the nested dialog, the moved row keeping focus, and saving the order; the operation requests with sub and the new inputs; a stuck run finishing with onInvalid: "clear" only after the confirmation; the delete dialog blocked, drafts, and the language note; publish warnings.
    • The API shared suite (SQLite and D1): the delete dry run (409 with referrers, 200 writes nothing), reference-issues, and the instance flag.
    • design-rules.test.ts now checks that articles delete through EntryDeleteDialog and that it composes DeleteResource with MenuDeleteNotice.
  • Suites run alone: UI (246), Studio (85 files) and API. API 48 files, 649 tests. A full pnpm check with its default concurrency overloads this machine (see feat(studio,ui,api): RFC-0045 step 6a, reference and repeater fields in the entry editor #219), so the suites ran one package at a time.

🤖 Generated with Claude Code

Madraka and others added 3 commits November 13, 2026 14:43
…h warnings

- /api/instance reports features.referenceFields (the store's capability,
  MCP's rule); the fields builder offers reference and list only then, so
  production Studio is unchanged until step 7.
- Builder: a reference's target collections, "Several", maximum and
  conditions (the page editor's condition rows, no `now`); a list's item
  fields as rows with up/down and a menu (stable ids, focus kept), a nested
  dialog per item field, the maximum and the item name.
- Operations: "Applies to" a list's item field for rename, change type and
  remove; reference and list targets with their settings; a run or revert
  stuck on values that stopped converting finishes with onInvalid "clear"
  behind a ticked confirmation.
- Delete: DELETE ...?dryRun=1 runs the store's plan; the dialog keeps
  Delete off while it refuses (entry_referenced), says archiving doesn't
  help, lists drafts that will show nothing, and names the language that
  loses the card. DeleteResource gains deleteDisabled.
- Publish: GET .../entries/reference-issues (referenceIssues over the
  working copy) and lazy warnings in the site's publish dialog.
- Worded API errors for the RFC-0045 refusals; AI drafts for 18 languages.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
- Delete dialog: group members under their own cache key (the editor's
  `entry-groups` has another shape); the blocked list shows each holder's
  real status and the published versions that show its value; "other
  entries left" counts only members in an allowed collection; a refusal
  of the real delete shows in the dialog (referrers, or its words).
- Change type: checks the contract's and RFC-0045's rules only (a select
  with labels in another language can be retyped) and shows problems
  inline.
- A reference's conditions follow its target; leftovers are flagged.
- Unsaved item fields keep key and type editable; the item name follows a
  renamed field (options keyed by stable row ids).
- "Add condition" and "Add item field" stay focusable at their limits.
- A stuck operation locks its inputs and is announced (role="alert").
- "And # more references." where references are counted.
- i18n: three new keys, two reworded; AI drafts for 18 languages.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
- A delete refusal belongs to one opening of the dialog for one revision:
  reopening or a new revision asks the store again.
- A reference's conditions are dropped only for a new single target.
- A type change's target settings lock (disabled fieldset) with its inputs.
- "Other entries left" is decided per referring field; both notices can
  show.
- The blocked count is the store's total; the "more references" note shows
  whenever rows remain; a late refusal reads the referrers again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Madraka <[email protected]>
@Madraka
Madraka merged commit be897cf into main Nov 13, 2026
8 checks passed
@Madraka
Madraka deleted the claude/rfc45-step6b branch November 13, 2026 12:57
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