Skip to content

fix(admin): start new entries with each field's default value - #3758

Merged
danielmlr merged 2 commits into
emdash-cms:mainfrom
danielmlr:fix/new-entry-field-defaults
Oct 4, 2026
Merged

danielmlr merged 2 commits into
emdash-cms:mainfrom
danielmlr:fix/new-entry-field-defaults

Conversation

@danielmlr

@danielmlr danielmlr commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Starts the admin's new-entry form with each field's defaultValue. On main, a boolean field with defaultValue: true renders switched off, and saving the entry untouched leaves an optional field's column NULL.

The default is stored on the field, but the manifest's field descriptor leaves it out and the form starts from empty data. The manifest now carries each field's default, except on a relation-bound reference field, whose value create refuses in data. The form seeds a new entry from a deep copy of those defaults, so the first save sends them and mutations to entry data cannot change defaults shared with later new entries. Existing entries still load their stored values, and API creates are unchanged. The admin's manifest takes its collections from the database alone, so collections passed to generateManifest as Zod schemas are unchanged.

The bot's candidate, 0bef4c6, seeded the form but left the manifest alone; with only that half applied, the switch still starts off.

Closes #2230

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). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main. (n/a: no new strings)
  • I have added and reviewed the user-facing changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/... (n/a: bug fix)
  • I have included screenshots below if this PR changes the UI

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 5.5 and GPT-6 (Codex)

Screenshots / test output

The New Post form in demos/simple, with an optional featured boolean field (defaultValue: true) added to Posts. Before the change, the switch starts off.

New Post form before the change: the Featured switch, whose field has defaultValue true, starts switched off

After the change, it starts on, and saving the untouched entry stores 1 where main stored NULL.

New Post form after the change: the Featured switch starts switched on

  • ContentEditor.test.tsx, "field defaults": a new entry shows and sends its boolean, select, number and string defaults (fails on main, the switch stays unchecked); an existing entry that stores false, 0, an empty string or nothing keeps those values and stays saved; an edit form whose entry failed to load gets no defaults.
  • ContentEditor.test.tsx, "keeps nested defaults unchanged across new entries": changes to the first entry's saved nested object and array leave the manifest defaults and the next entry unchanged (fails without cloning).
  • manifest-build.test.ts: the manifest carries true, false, 0 and "blue" defaults (fails on main: expected undefined to be true).
  • manifest-reference.test.ts: no default on a bound reference field, on SQLite and Postgres.

Initial validation: pnpm format, pnpm build, pnpm typecheck, pnpm lint, pnpm lint:json with 0 diagnostics, the admin suite (2984 tests) and core vitest --changed (1287 tests).

Follow-up validation: pnpm format, root pnpm build, pnpm typecheck, pnpm lint:quick, pnpm lint, pnpm lint:json with 0 diagnostics, all 142 ContentEditor tests, 25 manifest tests, and an Arabic editor check with the translated catalog and RTL layout.

Overlaps with #3791 in the shared field descriptors; both defaultValue and integer must be preserved when integrating the second PR.

A field's defaultValue was stored on its field row but never reached the
editor: the admin manifest's field descriptor left it out, and the
new-entry form started from empty data. A boolean field with
defaultValue: true rendered switched off, and if the field was optional,
saving the entry without touching it left its column NULL.

The manifest now carries each field's default, except on a reference
field bound to a relation, whose value create refuses in data. The
new-entry form starts from those defaults and sends them with the first
save. Existing entries still load their stored values.
@changeset-bot

changeset-bot Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d17a277

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

This PR includes changesets to release 12 packages
Name Type
emdash Patch
@emdash-cms/admin Patch
@emdash-cms/cloudflare Patch
@emdash-cms/plugin-test Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
create-emdash Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
@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 added the review/needs-review No maintainer or bot review yet label Oct 2, 2026

@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.

This PR takes the right approach for the stated scope: it forwards the registry’s stored defaultValue through the admin manifest, seeds new-entry formData from those defaults, and correctly omits defaults for relation-bound (storageless) reference fields. The added tests cover new entries, existing entries, an unloaded edit form, and core manifest forwarding including falsy defaults like false and 0.

I traced the admin route props and the ContentEditor state/dirty logic and the change behaves correctly for registry/manual collections. The one gap I found is that config-defined collections using Zod schemas (e.g. z.boolean().default(true)) are not covered: extractFieldType unwraps ZodDefault and discards its default value, so those collections’ admin new-entry forms will still ignore defaults. Since the PR title/description emphasizes stored defaults, this is likely out of the immediate scope, but it leaves the fix inconsistent across collection definition paths. I’d suggest addressing it (or documenting it) before merging if config schemas are a supported way to declare defaults.


Findings

  • [suggestion] packages/core/src/api/handlers/manifest.ts:274

    The ZodDefault branch recurses into def.innerType but never reads the default value. That means config-defined collections whose schemas use z.boolean().default(true) (or similar) will not carry defaultValue in the generated manifest, so the admin new-entry form will still start those fields empty. The DB-field path added below covers registry/manual defaults; this path leaves config-defined collections uncovered.

    Consider extending extractFieldType to capture the value from the Zod default descriptor and set it on the returned field descriptor:

    case "ZodDefault":
    case "ZodOptional": {
    	const inner = def?.innerType
    		? extractFieldType(name, def.innerType)
    		: { kind: "string", label: formatLabel(name) };
    	if (typeName === "ZodDefault") {
    		// Zod stores the default as a function in `_def.defaultValue`.
    		// The result should already be JSON-serializable for valid schemas.
    		const defaultValue = typeof def?.defaultValue === "function"
    			? def.defaultValue()
    			: def?.defaultValue;
    		if (defaultValue !== undefined) {
    			(inner as ManifestFieldDescriptor).defaultValue = defaultValue;
    		}
    	}
    	return inner;
    }

    If config-defined defaults are intentionally out of scope, a short note in the changeset would help set expectations.

@emdashbot emdashbot Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Oct 2, 2026
@pkg-pr-new

pkg-pr-new Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@3758

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@3758

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@3758

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@3758

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@3758

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@3758

emdash

npm i https://pkg.pr.new/emdash@3758

create-emdash

npm i https://pkg.pr.new/create-emdash@3758

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@3758

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@3758

@emdash-cms/plugin-test

npm i https://pkg.pr.new/@emdash-cms/plugin-test@3758

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@3758

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@3758

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@3758

@emdash-cms/registry-loader

npm i https://pkg.pr.new/@emdash-cms/registry-loader@3758

@emdash-cms/registry-moderation

npm i https://pkg.pr.new/@emdash-cms/registry-moderation@3758

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@3758

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@3758

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@3758

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@3758

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@3758

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@3758

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@3758

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@3758

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@3758

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@3758

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@3758

commit: d17a277

@danielmlr danielmlr added bot:review Trigger an emdashbot code review on this PR ci:run Approve and re-run CI on the PR's latest commit labels Oct 2, 2026
@github-actions github-actions Bot removed the ci:run Approve and re-run CI on the PR's latest commit label Oct 2, 2026

@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.

This re-review confirms the PR correctly implements its stated scope: stored field defaultValues now flow from the registry through the admin manifest into the new-entry ContentEditor form, while existing entries and unloaded edit forms continue to ignore them.

What I checked:

  • packages/core/src/api/handlers/manifest.ts forwards field.defaultValue to the manifest descriptor, guarded by !isStoragelessField(field) so relation-bound references stay default-free.
  • ContentEditor seeds formData from defaultFieldValues(fields) only for new entries (isNew with no item.data), using ?? so existing/hydrated entries never get overridden.
  • lastSavedData initializes from item?.data || {}, so a new entry with defaults starts dirty (Save enabled) as intended.
  • Tests cover new-entry defaults, existing-entry preservation of falsy stored values (false, 0, ""), unloaded edit forms, core manifest forwarding including falsy defaults, and bound-reference omission on both SQLite and Postgres.
  • Changeset is clear, user-focused, and names the affected packages.

The only item from my prior review remains open: config-defined collections whose schemas use ZodDefault (e.g. z.boolean().default(true)) still do not carry defaultValue in the generated manifest because extractFieldType unwraps ZodDefault without reading its value. The PR description explicitly scopes this out — “collections passed to generateManifest as Zod schemas are unchanged” — which is a reasonable boundary for this fix, though it does leave default handling inconsistent across collection-definition paths.

No new blocking issues; the implementation is clean for the chosen scope.

@emdashbot emdashbot Bot added review/approved Approved; no new commits since and removed bot:review Trigger an emdashbot code review on this PR review/awaiting-author Reviewed; waiting on the author to respond labels Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026

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.

@swissky swissky left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, Daniel, nice fix. Forwarding defaultValue through the manifest and seeding only new entries is the right layer, and the tests cover the edit and failed-load cases too. Approving.

Two small, non-blocking suggestions:

  • defaultFieldValues puts default values into form state by reference, and those objects live in the react-query cache. For array/object defaults (repeater, multiSelect, json, blocks), an in-place change would leak into the next new entry. structuredClone(field.defaultValue) would match what lib/block-field-state.ts already does.
  • The changeset only describes the admin form, but the manifest field descriptors now also include defaultValue. One sentence about that would help anyone reading the API.

Heads-up: #3791 adds a property at the same spot in three files, so whichever lands second needs a quick rebase (keep both lines).

Clone each default when creating form state so nested entry data cannot mutate the manifest defaults used by later entries. Cover the save callback boundary with a nested object and array regression, and document the manifest defaultValue field in the changeset.
@github-actions github-actions Bot added size/L review/needs-rereview New commits since the last review and removed size/M review/approved Approved; no new commits since labels Oct 4, 2026
@danielmlr danielmlr added the ci:run Approve and re-run CI on the PR's latest commit label Oct 4, 2026
@github-actions github-actions Bot removed the ci:run Approve and re-run CI on the PR's latest commit label Oct 4, 2026
@danielmlr danielmlr added the bot:review Trigger an emdashbot code review on this PR label Oct 4, 2026

@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.

This re-review confirms the implementation matches its stated bug-fix scope and the prior approved state.

Approach and scope — The PR is the right fix for #2230: new admin entries now start from stored field defaultValues, while existing entries and unloaded edit forms keep their current behavior. Seeding defaults only when isNew && !item?.data, and cloning them with structuredClone, prevents shared-mutation bugs across new-entry sessions. Excluding relation-bound references from the manifest is also correct, since API create rejects data values for those fields.

What I checked:

  • packages/admin/src/components/ContentEditor.tsx seeds formData from defaultFieldValues(fields) only for new entries, uses ?? so existing/hydrated data wins, and clones defaults before storing in React state.
  • lastSavedData still initializes from item?.data || {}, so new entries with defaults are dirty and the Save button is enabled as intended.
  • packages/core/src/api/handlers/manifest.ts forwards field.defaultValue only when it is not undefined and the field is not storage-less (i.e., not a relation-bound reference).
  • The type definitions in packages/core/src/api/types.ts, packages/core/src/astro/types.ts, and packages/admin/src/lib/api/client.ts were all updated consistently.
  • New tests in ContentEditor.test.tsx cover new-entry defaults, falsy stored-value preservation, unloaded edit forms, and cross-entry default isolation. Core tests cover manifest forwarding and bound-reference omission.
  • The changeset names both affected packages and describes the user-facing behavior.
  • No new user-facing strings, RTL/Tailwind changes, SQL changes, authorization changes, or logged-out-route query changes.

Known scope boundary (unchanged): The PR deliberately does not propagate Zod schema ZodDefault values into the generated manifest for config-defined collections. That inconsistency remains, but it is explicitly scoped out in the PR description and was the only open item from the previous review.

No new blocking issues. The implementation is clean for the chosen scope.

@emdashbot emdashbot Bot added review/approved Approved; no new commits since and removed bot:review Trigger an emdashbot code review on this PR review/needs-rereview New commits since the last review labels Oct 4, 2026
@danielmlr
danielmlr merged commit d0c7384 into emdash-cms:main Oct 4, 2026
70 of 72 checks passed
@danielmlr
danielmlr deleted the fix/new-entry-field-defaults branch October 4, 2026 13:54
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.

Boolean field defaultValue is stored but never applied by the new-entry form

2 participants