feat(admin): support hidden repeater sub-fields#1585
Conversation
Repeater rows sometimes carry a stable identifier a template relies on (such as a copy-lookup key) that editors should never change. A hidden sub-field keeps that value in the entry data without rendering it in the editor, so it cannot be accidentally changed and the lookup stays intact.
🦋 Changeset detectedLatest commit: 25aaba5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 16 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
All contributors have signed the CLA ✍️ ✅ |
There was a problem hiding this comment.
Approach
The change adds an optional hidden flag to repeater sub-fields: the value is kept in entry data but no input is rendered. The problem is real (repeater rows sometimes carry a stable identifier — a copy-lookup key, an external ID — that templates rely on and editors must not touch), and the approach is sound and minimal: additive to the sub-field shape, no migration, no new UI strings, no RTL impact. It fits EmDash's conventions. One process note I couldn't verify from here: AGENTS.md asks for a prior approved Discussion for features — worth confirming one exists.
What I checked
I traced the full lifecycle of hidden and the PR's core claim that hidden values round-trip on save — and it holds for content edits: handleItemChange spreads …item, stripKeys/ensureKeys and the value sync effect all spread the whole object, and handleAdd initializes every sub-field (including hidden ones) so new items carry the key slug. The summary-field lookup (line 240) and the render filter (line 290) correctly skip hidden sub-fields, and for existing configs with no hidden the !sf.hidden check is !undefined === true, so there's no behavior change. The zod validator and RepeaterSubField type both carry hidden, and emdash-runtime.ts spreads the full field.validation into the admin manifest, so programmatically-defined schemas work end-to-end. The new tests (not rendered; value preserved on edit) are appropriate, and the .elements()).toHaveLength(0) absence check is a valid @vitest/browser locator pattern. The changeset is well-formed and correctly bumps both emdash and @emdash-cms/admin as minor.
This was a static review — I did not run the suite or lint.
Headline issue
The admin schema editor (FieldEditor.tsx) save path rebuilds each sub-field as { slug, type, label, required } and drops hidden. So if a repeater field defined with hidden sub-fields is later edited via the admin UI — even just renaming the field — every hidden flag is silently stripped, un-hiding the identifier the feature exists to protect. The PR updated 3 of the 4 places that touch the sub-field shape but missed this save path. See the line-anchored finding for the one-line fix.
Minor note
ContentEditor.tsx:1370-1377 feeds subFields into RepeaterField via an inline as cast whose type omits hidden. It works today only because the cast is a pure as (the parsed JSON retains hidden at runtime, so the !sf.hidden filter still sees it), but the inline type is now inconsistent with the canonical RepeaterSubField shape, and a future refactor that turns the cast into a property pick would silently drop hidden. Adding hidden?: boolean there would keep it consistent.
| /> | ||
| ))} | ||
| {subFields | ||
| .filter((sf) => !sf.hidden) |
There was a problem hiding this comment.
[needs fixing] This render filter correctly skips hidden sub-fields, but the flag is not preserved when a repeater field is saved through the admin schema editor. FieldEditor.tsx:294-299 rebuilds each sub-field as { slug, type, label, required }, dropping hidden (and options), and RepeaterSubFieldState (FieldEditor.tsx:55) doesn't even declare hidden.
Consequence: a repeater field defined with hidden: true sub-fields (via f(...) or the API) loses every hidden flag the moment someone opens that field in the admin schema editor and saves — even if they only rename the field or add another sub-field. The previously-hidden identifier then becomes visible and editable, which is exactly the footgun this feature exists to prevent. The PR updated the type, the zod validator, and the renderer, but missed this sibling save path.
Fix in FieldEditor.tsx — add hidden?: boolean to RepeaterSubFieldState and carry it through the save map:
(validation as Record<string, unknown>).subFields = formState.subFields.map((sf) => ({
slug: sf.slug,
type: sf.type,
label: sf.label,
required: sf.required || undefined,
hidden: sf.hidden || undefined,
}));(The same map also drops options for select sub-fields — a pre-existing gap worth a follow-up, but out of scope for this PR.)
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
|
I have read the CLA Document and I hereby sign the CLA |
What does this PR do?
Adds an optional
hiddenflag to repeater sub-fields. A hidden sub-field is not rendered in the content editor, but its value is preserved on save.Repeater rows sometimes carry a stable identifier a template relies on (such as a copy-lookup key or an external ID) that editors should never change. Today every sub-field is rendered and editable, so the only options are exposing the raw key (confusing, and a footgun, since editing it silently breaks the lookup) or dropping it (then the value cannot be resolved).
hiddenlets the key live in the data without cluttering the editor or risking accidental edits.Implementation:
hidden?: booleantoRepeaterSubFieldand accept it in the collection schema validator.RepeaterFieldskips hidden sub-fields when rendering inputs and when picking the row summary label. Values are untouched, because item updates already spread existing fields, so hidden values round-trip on save.Discussion: #1586
Userland plugin that solves the same problem today (and motivates this change): https://github.com/dennisklappe/emdash-plugin-copydeck
Closes #
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runAI-generated code disclosure
Screenshots / test output
pnpm typecheck(all packages) passes.prettier --checkandoxlint --type-aware.RepeaterFieldhidden sub-field tests (hidden sub-field not rendered, hidden value preserved when a visible field changes).