Skip to content

feat(admin): support hidden repeater sub-fields#1585

Open
dennisklappe wants to merge 1 commit into
emdash-cms:mainfrom
dennisklappe:feat/hidden-repeater-subfields
Open

feat(admin): support hidden repeater sub-fields#1585
dennisklappe wants to merge 1 commit into
emdash-cms:mainfrom
dennisklappe:feat/hidden-repeater-subfields

Conversation

@dennisklappe

@dennisklappe dennisklappe commented Jun 22, 2026

Copy link
Copy Markdown

What does this PR do?

Adds an optional hidden flag 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). hidden lets the key live in the data without cluttering the editor or risking accidental edits.

f("teksten", "repeater", "Texts", {
  validation: { subFields: [
    { slug: "label", type: "string", label: "Label" },
    { slug: "value", type: "text", label: "Text" },
    { slug: "key", type: "string", label: "Key", hidden: true }, // kept in data, not shown
  ]},
})

Implementation:

  • core: add hidden?: boolean to RepeaterSubField and accept it in the collection schema validator.
  • admin: RepeaterField skips 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

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Not applicable: this change adds no new user-visible strings, it only stops rendering hidden sub-fields.
  • I have added a changeset (if this PR changes a published package)
  • New features link to a Discussion: Support hidden repeater sub-fields #1586 (opened for maintainer approval)

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.8 (Claude Code)

Screenshots / test output

  • pnpm typecheck (all packages) passes.
  • Changed files pass prettier --check and oxlint --type-aware.
  • Admin test suite: 81 files, 1016 tests passing, including the two new RepeaterField hidden sub-field tests (hidden sub-field not rendered, hidden value preserved when a visible field changes).

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-bot

changeset-bot Bot commented Jun 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 25aaba5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
@emdash-cms/admin Minor
emdash Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

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

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.)

@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Jun 23, 2026
@dennisklappe

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant