Skip to content

feat(core): configurable displayField and dateField per collection#1973

Open
CacheMeOwside wants to merge 8 commits into
emdash-cms:mainfrom
CacheMeOwside:feat/1133-configurable-diplay-and-date-field-in-collections
Open

feat(core): configurable displayField and dateField per collection#1973
CacheMeOwside wants to merge 8 commits into
emdash-cms:mainfrom
CacheMeOwside:feat/1133-configurable-diplay-and-date-field-in-collections

Conversation

@CacheMeOwside

@CacheMeOwside CacheMeOwside commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Closes #1133

Adds two optional collection options, displayField and dateField, so the admin shows a meaningful title and date for collections whose data doesn't follow the title/updated-date convention (e.g. an employees collection where name is the title and a custom column is the real date to be displayed).

  • displayField drives the entry title in the content list, editor header, search results, and reference picker (via a shared getEntryTitle helper).
  • dateField (a datetime field) drives the content-list Date column and its default sort.
  • Stored as nullable columns on _emdash_collections (migration 051), validated on update (displayField = a real field, dateField = a datetime field). Sorting by a custom field is validated against the collection's own configured fields on the server, so clients still can't sort by arbitrary columns.
  • Both optional; unset = today's behavior. Set them in the collection definition / seed file.

Follow-ups: filtering the date-range picker by the custom dateField. The date-range filter still uses system dates (Created/Updated/Published), not a custom dateField. Only display and sort use it.

Note

The newly added displayField/dateField not exposed via REST/OpenAPI or the Content Type editor. This is intentional, config-as-code scope, matching #1133's proposed API, which defines these as collection-definition properties, not admin-UI or REST-managed settings. For this PR they're seed-file / config-as-code only.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion) - This is a roadmap/1.0 issue placed on the 1.0 milestone by a maintainer
  • 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.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/...

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Code with Opus 4.8 (design, adverserial review, fixes)

Screenshots / test output

Add this to the seed file (e.g. seed/seed.json), then apply the seed and start the dev server:

// collections[]
{
  "slug": "employees",
  "label": "Employees",
  "labelSingular": "Employee",
  "supports": ["drafts", "revisions", "search"],
  "displayField": "name",      // Title column → the person's name
  "dateField": "pub_date",     // Date column + default sort → start date
  "fields": [
    { "slug": "name", "label": "Name", "type": "string", "required": true, "searchable": true },
    { "slug": "title", "label": "Job Title", "type": "string" },
    { "slug": "pub_date", "label": "Start Date", "type": "datetime" }
  ]
}

// content.employees[]
{ "id": "emp-1", "slug": "jane-doe", "status": "published",
  "data": { "name": "Jane Doe", "title": "Account Manager", "pub_date": "2019-03-14T09:00:00.000Z" } },
{ "id": "emp-2", "slug": "john-smith", "status": "published",
  "data": { "name": "John Smith", "title": "Operations Lead", "pub_date": "2021-08-02T09:00:00.000Z" } }

Then open Content → Employees in the Admin: the Title column shows names (not job titles), the Date column shows the start date, and sorting/search use those fields.

Before After
Collections-before Collections-after

@changeset-bot

changeset-bot Bot commented Jul 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0212cb1

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

This PR includes changesets to release 16 packages
Name Type
emdash Minor
@emdash-cms/admin 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

Copy link
Copy Markdown
Contributor

PR template validation failed

Please fix the following issues by editing your PR description:

See CONTRIBUTING.md for the full contribution policy.

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 645 lines across 23 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@pkg-pr-new

pkg-pr-new Bot commented Jul 12, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 0212cb1

@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 implements configurable displayField and dateField collection options in a sound, additive way: new nullable columns on _emdash_collections, server-side validation of the configured slugs, a shared getEntryTitle helper used across the admin list/editor/picker, and a closed set of allowed orderBy values resolved server-side. The approach fits EmDash's architecture and the migration/seed paths look correct.

I checked the diff against AGENTS.md conventions and runtime semantics. The implementation is generally clean and well tested, but I found a few issues worth fixing before merge:

  1. Referential integrity on field deletionSchemaRegistry.deleteField drops the ec_* column but never clears the collection's display_field/date_field when that slug is referenced. A deleted field used as dateField will later crash the content list when the admin sorts by it ("no such column"). This is the most important blocker.
  2. React useMemo dependency arrays — both the content list and the content picker filter by getEntryTitle(item, displayField) but omit displayField from their memo dependencies, which can leave client-side search results stale.
  3. API schema surface is incompleteupdateCollectionBody and collectionSchema don't include displayField/dateField, so the REST collection API cannot read or write them even though the registry supports it. Combined with ContentTypeEditor not exposing the fields, the feature is currently seed-file-only. This matches the PR description, but wiring them through the schemas would make the backend/frontend work consistent.
  4. displayField type is not validated — any existing field slug is accepted, including JSON/portableText/image/repeater fields, whose raw stored values would render poorly as entry titles.

The tests cover the happy paths and the server-side sort validation well. Fixing #1 and #2 would unblock merge from my perspective; #3 and #4 are follow-ups that can be addressed here or in a later pass.


Findings

  • [needs fixing] packages/core/src/schema/registry.ts:777

    deleteField drops the column from the ec_* table but never checks whether the slug is still referenced by display_field or date_field on _emdash_collections. After deletion, sorting the content list by the configured column fails with "no such column". Clear the collection metadata when the referenced field is removed.

    	async deleteField(collectionSlug: string, fieldSlug: string): Promise<void> {
    		const field = await this.getField(collectionSlug, fieldSlug);
    		if (!field) {
    			throw new SchemaError(
    				`Field "${fieldSlug}" not found in collection "${collectionSlug}"`,
    				"FIELD_NOT_FOUND",
    			);
    		}
    
    		const collection = await this.getCollection(collectionSlug);
    		if (collection && (collection.displayField === fieldSlug || collection.dateField === fieldSlug)) {
    			await this.updateCollection(collectionSlug, {
    				displayField: collection.displayField === fieldSlug ? null : undefined,
    				dateField: collection.dateField === fieldSlug ? null : undefined,
    			});
    		}
    
    		let schemaMutated = false;
    		try {
    			await withTransaction(this.db, async (trx) => {
    

@github-actions github-actions Bot added query-count changed PR diff modifies query-count snapshot files review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Jul 12, 2026
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review needs-rebase cla: signed and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jul 12, 2026
…e-diplay-and-date-field-in-collections

# Conflicts:
#	packages/core/src/database/migrations/runner.ts
#	packages/core/tests/integration/database/migrations.test.ts
#	scripts/query-counts.snapshot.d1.json
#	scripts/query-counts.snapshot.sqlite.json
@github-actions github-actions Bot removed needs-rebase query-count changed PR diff modifies query-count snapshot files labels Jul 12, 2026
@CacheMeOwside

Copy link
Copy Markdown
Contributor Author

Feature PRs require a link to an approved Discussion (https://github.com/emdash-cms/emdash/discussions/categories/ideas).

Note for maintainer: This feature has already been placed in the 1.0 milestone by a maintainer.

@CacheMeOwside CacheMeOwside added the bot:review Trigger an emdashbot code review on this PR label Jul 12, 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 is a re-review of PR #1973. Since the last pass, the author has fixed the three blocking/regression concerns from the prior review:

  1. SchemaRegistry.deleteField now clears display_field/date_field in the same transaction when the referenced field is removed, so the content list won't later crash sorting by a dropped column.
  2. The React useMemo dependency arrays in ContentList and ContentPickerModal now include displayField, so client-side title filtering stays reactive.
  3. SchemaRegistry.updateCollection validates that displayField is a text-like field (string, text, slug) and dateField is a datetime field.

The tests added for custom-field sorting, search result titles, and collection metadata behavior cover the happy paths well.

One new issue remains: SchemaRegistry.updateField allows in-place field-type changes as long as the SQLite column type doesn't change (e.g., datetimestring, or texturl). Because all of those types map to TEXT, the change is accepted even when the field is configured as a displayField or dateField, breaking the type invariant this PR establishes. After the change, the admin may sort the "Date" column by arbitrary strings or use a URL as a title. This should be guarded the same way updateCollection guards the initial assignment.

Prior finding #3 — the REST/OpenAPI collection schemas and the admin Content Type editor don't expose displayField/dateField — is still present. It aligns with the seed-file-only scope stated in the PR description and was previously flagged as a follow-up, so I'm not blocking on it here.

Overall the approach is solid and additive; fixing the updateField invariant would make the validation complete.


Findings

  • [needs fixing] packages/core/src/schema/registry.ts:657

    After this block accepts an in-place type change because the SQLite column type stayed the same, the collection's displayField/dateField metadata can become invalid. For example, a datetime field used as dateField can be changed to string (both TEXT), or a text field used as displayField can be changed to url, leaving the metadata claiming a type that validateDisplayDateFields would reject on updateCollection.

    Re-validate against the collection's configured displayField/dateField when input.type changes, or block the change and require the user to clear the metadata first. Add a test that changes a dateField field to a non-datetime type and expects a SchemaError.

    		let nextType = field.type;
    		let nextColumnType = field.columnType;
    		if (input.type !== undefined && input.type !== field.type) {
    			const newColumnType = FIELD_TYPE_TO_COLUMN[input.type];
    			if (newColumnType !== field.columnType) {
    				throw new SchemaError(
    					`Cannot change field "${fieldSlug}" in collection "${collectionSlug}" from type ` +
    						`"${field.type}" to "${input.type}": the underlying column type would change from ` +
    						`${field.columnType} to ${newColumnType}, which requires a manual content migration. ` +
    						`Drop and re-create the field, or migrate the column data, before changing its type.`,
    					"FIELD_TYPE_COLUMN_CHANGE",
    				);
    			}
    			// A same-column-type change can still violate the displayField/dateField
    			// invariants (#1133). Read the collection only when a type change is
    			// actually requested.
    			const collection = await this.getCollection(collectionSlug);
    			if (collection?.displayField === fieldSlug && !DISPLAY_FIELD_TYPES.has(input.type)) {
    				throw new SchemaError(
    					`Cannot change field "${fieldSlug}" to type "${input.type}" because it is the collection's displayField. ` +
    						`Clear displayField or change it to another field first.`,
    					"INVALID_DISPLAY_FIELD",
    				);
    			}
    			if (collection?.dateField === fieldSlug && input.type !== "datetime") {
    				throw new SchemaError(
    					`Cannot change field "${fieldSlug}" to type "${input.type}" because it is the collection's dateField. ` +
    						`Clear dateField or change it to another field first.`,
    					"INVALID_DATE_FIELD",
    				);
    			}
    			nextType = input.type;
    			nextColumnType = newColumnType;
    		}
    

@github-actions

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-rereview Author pushed changes since the last review labels Jul 12, 2026
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/awaiting-author Reviewed; waiting on the author to respond labels Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/admin area/core bot:review Trigger an emdashbot code review on this PR cla: signed overlap review/needs-rereview Author pushed changes since the last review size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(admin): configurable display field and date field for collection list view

1 participant