Skip to content

feat: filter the admin content list by byline - #2312

Draft
MA2153 wants to merge 2 commits into
emdash-cms:mainfrom
MA2153:feat/content-list-byline-filter
Draft

feat: filter the admin content list by byline#2312
MA2153 wants to merge 2 commits into
emdash-cms:mainfrom
MA2153:feat/content-list-byline-filter

Conversation

@MA2153

@MA2153 MA2153 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds byline filtering to the admin content list, alongside the existing status, author, and date filters. Selecting one or more bylines matches entries credited to any of them (OR); a No byline assigned option matches entries with no credit at all.

Bylines are searched server-side in the picker rather than listed exhaustively, so the filter works across the whole byline directory rather than one page.

Inferred bylines are excluded by default. An entry with no explicit credit still renders the byline linked to its author (see hydrateBylinesMany), but filtering usually means "who is credited", not "whose name happens to show". A single Include inferred bylines switch opts into the wider behaviour, and it widens consistently: with it on, "No byline assigned" means nothing is rendered, so entries whose author resolves to a byline drop out too.

Filter values are translation_groups — what _emdash_content_bylines.byline_id has stored since migration 040 — so a selection matches a byline across every locale it exists in.

Important

No Discussion exists for this yet, so the feature checklist line below is deliberately unticked and this is opened as a draft. Happy to open one in Ideas and hold this until it's approved — flagging rather than assuming, given CONTRIBUTING.md's policy on feature PRs.

Closes #

No migration required

Verified before writing any code, with EXPLAIN QUERY PLAN against a freshly-migrated DB rather than by inspection. The UNIQUE(collection_slug, content_id, byline_id) constraint from migration 031 already creates an index of exactly the right shape:

### include (EXISTS ... byline_id IN (...))
SEARCH ec_post USING INDEX idx_ec_post_loc_upd (deleted_at=? AND locale=?)
SEARCH cb EXISTS USING COVERING INDEX sqlite_autoindex__emdash_content_bylines_2
    (collection_slug=? AND content_id=? AND byline_id=?)

### no-byline (NOT EXISTS)  → COVERING INDEX idx_content_bylines_content
### inferred opt-in         → COVERING INDEX idx_bylines_user_id_locale_unique

Each probe is index-only, and the outer query keeps its sort-ordered composite index, so LIMIT still short-circuits with no temp B-tree.

Two consequences are load-bearing enough that they're recorded in comments rather than just here:

  • The correlated EXISTS shape matters. Driving from the pivot side (FROM _emdash_content_bylines JOIN ec_*) cannot use that index for the byline and adds USE TEMP B-TREE FOR ORDER BY. Written as an EXISTS from the content table, no new index is needed; written the other way, no index rescues it.
  • "No byline" tests the junction, not primary_byline_id. The two agree — both junction write paths stamp the column in the same call — but not atomically (D1 has no transactions), so the junction stays authoritative, mirroring how migration 051 treats the denormalized taxonomy columns as advisory and re-checks on read.

Known limitation, deliberately not addressed: the EXISTS plan walks the collection's sort index and probes per row, so a byline matching very few entries in a very large collection reads a lot before filling LIMIT — the shape #1834/migration 051 fixed for taxonomies. Making that seek-optimal needs denormalization, i.e. a migration. This is the authenticated admin list rather than the logged-out hot path, and every probe is index-only, so it didn't seem worth paying now. Happy to revisit.

No queries were added to any logged-out route.

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.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion — none yet; see the note above.

AI-generated code disclosure

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

Screenshots / test output

Verified in the browser against demos/simple: the filter renders in the existing filter bar as an "All bylines" dropdown containing a search box, the exclusive "No byline assigned" checkbox, the byline list, and the "Include inferred bylines" switch.

Also checked in Arabic per the RTL guidance — document.documentElement.dir is rtl, and the popover, checkboxes, switch, and caret all mirror correctly with no broken directionality. (New strings render in English until the extraction workflow picks them up on merge; no messages.po changes are included here.)

Screenshots can be attached on request — omitted here since I can't upload to GitHub's CDN from the CLI.

Tests

11 new integration tests (content-list-byline-filter.test.ts), run against both dialects via describeEachDialect, covering single/multi-byline OR matching, the no-byline filter, inferred credits on and off, an explicit credit suppressing author inference, composition with the status filter, and total reflecting the filter.

One of them caught a real bug during development: the empty-selection guard used eb.val(false), which better-sqlite3 refuses to bind (SQLite3 can only bind numbers, strings, bigints, buffers, and null). It now emits a literal 1 = 0 predicate instead.

Behaviour verified end-to-end against demos/simple, filtering a 16-entry collection:

Query total
no filter 16
bylines=<A> 4
bylines=<A>,<B> 6
bylines=none 10
bylines=<unknown-id> 0
bylines=<A>&status=published 4

6 + 10 = 16 — the include and no-byline filters partition the collection exactly.

Wire contract:

Input Result
includeInferredBylines=1 / true / 0 / false accepted, parsed strictly
includeInferredBylines=yes 400 VALIDATION_ERROR
26 byline ids 400 — "at most 25 bylines may be selected"
bylines= (empty) 400 VALIDATION_ERROR
duplicate ids collapsed

The 25-id cap keeps the IN (...) clause clear of D1's bound-parameter ceiling once the rest of the list query's placeholders are counted.

Adds byline filtering alongside the existing status, author, and date
filters. Selecting several bylines matches entries credited to any of
them; "No byline assigned" matches entries with no credit.

Credits inferred from an entry's author (rendered when an entry has no
explicit credit) are excluded unless opted into, so the filter matches
assigned bylines by default.

No migration is required. The UNIQUE(collection_slug, content_id,
byline_id) index from migration 031 covers every filter shape: EXPLAIN
QUERY PLAN shows a covering seek for the include, exclude, and no-byline
probes while the outer query keeps its sort-ordered composite index, so
LIMIT still short-circuits without a temp B-tree. Correlating EXISTS from
the content table is what makes that hold — driving from the pivot side
cannot use the index for the byline and forces a temp sort — hence the
note in applyBylineFilter.

Filter values are translation_groups (what the junction has stored since
migration 040), so a selection matches a byline across every locale.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 28101af

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

This PR includes changesets to release 17 packages
Name Type
emdash Patch
@emdash-cms/admin Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke 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 Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash 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

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 674 lines across 12 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.

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

@pkg-pr-new

pkg-pr-new Bot commented Jul 31, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-verification

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 28101af

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Temporary tooling for exercising the byline filter by hand. Not intended
to ship with the feature -- revert this commit before the PR goes up.

Adds a "Set byline" picker to the existing bulk-selection toolbar. The
picked bylines replace each selected entry's credit set rather than
merging into it: list items hydrate credits with strict locale matching,
so an entry whose byline has no row in the entry's locale comes back with
an empty `bylines` array, and a client-side merge silently drops those
credits on write.

Requests fan out through runBulkAction like the other bulk actions, so
failed ids stay selected for a retry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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