* i18n(fr): translate in French the 89 missing keys
* example.com
Co-authored-by: emdashbot[bot] <273199577+emdashbot[bot]@users.noreply.github.com>
---------
Co-authored-by: emdashbot[bot] <273199577+emdashbot[bot]@users.noreply.github.com>
Co-authored-by: Matt Kane <mkane@cloudflare.com>
* fix(taxonomies): respect active locale in admin surfaces
Group translated definitions by logical identity before rendering admin navigation and editor choices. Scope visible term counts and cache entries to the resolved content locale.
* fix(taxonomies): preserve locale in admin term views
* ci: update query-count snapshots
* fix(admin): preserve locale sidebar active state
* fix(taxonomies): preserve exact locale updates
* test(perf): update locale query snapshots
---------
Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
* fix(scheduler): surface missed scheduled work
Persist maintenance heartbeats so authenticated editors are warned when overdue content indicates the scheduler has stopped.
* fix(scheduler): keep heartbeat writes non-fatal
A transient operator-health write must not reject maintenance that already completed successfully.
* test(scheduler): cover heartbeat upsert failures
Keep the non-fatal heartbeat contract exercised for both initial inserts and existing-row updates.
* fix(admin): parse SQLite timestamps as UTC
SQLite datetime('now') stores timestamps with no timezone suffix, which new Date() reads as local time.
Revision and dashboard timestamps in the admin UI drifted by the viewer's offset.
Closes#919
* fix(admin): route content list and editor dates through parseTimestamp
* fix(admin): route the scheduled publish date through parseTimestamp
---------
Co-authored-by: Matt Kane <mkane@cloudflare.com>
* feat(content): support indexed custom field sorting
* fix(content): remove redundant indexed cursor cast
* fix(content): use generated custom field indexes
* fix(content): keep indexed cursor scans ordered
* test(content): capture qualified list queries
* fix(admin): clear the indexed flag for non-indexable field types
* test(admin): cover clearing the indexed flag on type change
* refactor: drop comments that address the reviewer
Three blocks defend sending `indexed: false` or narrate a Playwright
workaround. The tests already name the contract they cover. The bind-budget
note above the seed batch size stays: it records a limit a reader would
otherwise raise.
* test(core): cover indexed ordering on PostgreSQL
* refactor(schema): simplify indexable field comment
* fix(test): import the indexed migration from its current slot
The file moved to 061 when upstream took 059; this import still named 060,
so the suite could not load the module at all.
* fix(seed): reject unsupported indexed field types
* test(core): allow PostgreSQL ordering setup
* fix(content): preserve indexed field contracts
* fix(test): import indexed migration from slot 066
* fix(core): keep localized custom ordering seekable
---------
Co-authored-by: logelog <194732487+logelog@users.noreply.github.com>
Co-authored-by: Matt Kane <mkane@cloudflare.com>
* fix: exclude image/svg+xml from default media upload allowlist
GLOBAL_UPLOAD_ALLOWLIST used a bare "image/" prefix match, which
unintentionally included image/svg+xml. There is no upload-time content
validation for SVG, so a Contributor+ user could upload an SVG with an
embedded <script>. Enumerate the safe raster types explicitly instead.
Field-specific allowedMimeTypes overrides are unaffected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(admin): match Images upload preset to the raster-only allowlist
The Images preset in AllowedTypesEditor still stored a bare "image/"
prefix, which prefix-matches image/svg+xml. Since upload routes widen
the global allowlist to a field's own allowedMimeTypes, picking this
preset re-admits SVG uploads and undermines the new default that
excludes them.
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Matt Kane <mkane@cloudflare.com>
* feat(admin): allow trusted plugins to add editor panels
* fix(admin): use Kumo button in panel fallback
* fix(admin): integrate plugin panels with sortable settings
* test(admin): cover plugin panel retry recovery
* fix(admin): reset a failed plugin panel when the entry changes
The boundary kept its error state across entries, so a panel that threw on
one entry stayed replaced by the fallback after navigating to another. Keying
it on collection and entry id rebuilds it with the content it renders.
Retry already recovers: React unmounts the subtree when the boundary catches,
so clearing the flag mounts the panel fresh. The added test asserts that
mount count rather than assuming it.
* fix(admin): localize plugin panel titles
---------
Co-authored-by: logelog <194732487+logelog@users.noreply.github.com>
* feat(admin): refine featured image field
* feat(admin): inset featured image preview
* feat(admin): stack featured image details
* fix(admin): preserve featured image preview aspect ratio
* chore: use media library project changeset
* fix(media): align image value schemas
* fix(admin): clarify featured image actions
---------
Co-authored-by: Matt Kane <mkane@cloudflare.com>
* feat: filter the admin content list by byline
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>
* tmp: bulk-apply bylines from the content list
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>
* Revert "tmp: bulk-apply bylines from the content list"
This reverts commit 28101afdf5f32b88c5939387e7f5768fdf4aea80.
* fix: resolve inferred bylines at the list's locale
The inferred-byline branch of the content-list filter matched on
`author_id` alone, against a set of users resolved from every locale row
of the selected bylines. Byline hydration is strict per locale, so an
entry whose author owns a byline with no row at the list's locale renders
as uncredited while still matching a filter on that byline -- and matched
"No byline" at the same time, since that branch did correlate on locale.
Resolve the credit the same way the list renders it: a correlated EXISTS
on `_emdash_bylines` scoped to the locale the list is showing, falling
back to each entry's own locale when the list spans locales. This drops
the pre-resolution query the handler ran for `includeInferredBylines`, so
the opt-in no longer costs an extra round-trip.
Also pluralize the selected-byline count through Lingui's `plural` rather
than a bare interpolation, which only reads correctly in languages with a
single plural form.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: trim the byline filter doc comment to its invariants
Drop the rejected-alternative narrative and the migration reference;
keep why the EXISTS correlates from the content table and why "none"
tests the junction rather than primary_byline_id.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: drop the default-justification line from ContentBylineFilter
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs: drop the design rationale from the BylineFilter docstring
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Update packages/admin/src/components/ContentList.tsx
Co-authored-by: emdashbot[bot] <273199577+emdashbot[bot]@users.noreply.github.com>
* fix: scope the byline filter's explicit credits to the list's locale
The credit EXISTS matched a junction row by translation_group alone,
but a credit only renders where that group has a byline row at the
list's locale. Junction rows are copied to every translation of an
entry, so a group that exists only in `en` still matched
`bylines=[group]` on the `fr` list, where the entry renders
uncredited — and was excluded from "no byline" at the same time.
The credit test now repeats the join `getContentBylinesMany` makes,
with the same list-locale / entry-locale fallback as the inferred
branch. The author fallback keeps a separate, deliberately
locale-agnostic junction probe: hydration suppresses inference on the
presence of any credit, not on one that renders here, so an entry
whose explicit credit fails to resolve stays uncredited rather than
falling through to its author.
Adds a query-plan test covering all four filter shapes: every probe
is still an indexed seek and no shape sorts in a temp b-tree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore: drop the issue reference from the filter-state comment
Comments are for future readers of the code; the issue number is
narrative that belongs in the commit and PR description.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: emdashbot[bot] <273199577+emdashbot[bot]@users.noreply.github.com>
* fix(admin): localize the editor word-count footer
The word, character, and reading-time metrics under the Portable Text
editor were raw string literals, so they stayed English in every admin
language and used hand-rolled one/other pluralization. Route them
through Lingui so the catalogs pick them up and each language's plural
rules apply.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(admin): reset i18n locale after footer test; drop test-export comment
Co-authored-by: emdashbot[bot] <273199577+emdashbot[bot]@users.noreply.github.com>
* fix(admin): dedupe EditorFooter test export
Applying the review suggestion anchored to the comment line only, which
left the export statement duplicated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(admin): pluralize the reading-time footer message
English "min" is invariant, but the flat t-string kept translators from
branching on number. Use the same plural() macro as the other two metrics
so locales that spell out "minutes" get ICU plural categories.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: emdashbot[bot] <273199577+emdashbot[bot]@users.noreply.github.com>
* feat(admin): show configured fields in content lists
* fix(admin): use the .js specifier on the content list export
* fix(schema): enforce list column limit
---------
Co-authored-by: logelog <194732487+logelog@users.noreply.github.com>
* fix(content): search searchable custom fields
* docs(content): describe what the search filter actually matches
`searchColumns` no longer comes from display fields alone. It also carries
the slug and every field marked searchable, so the docstring was telling
callers the wrong thing about what `q` will match.
---------
Co-authored-by: logelog <194732487+logelog@users.noreply.github.com>
* fix(admin): show published content as "Published", not "Publish"
The status badge and the status filter labeled published content with
the action verb. Translated locales made it plainly wrong (German badge
read "Publizieren"). Both sites now use the existing "Published" msgid,
so existing translations apply automatically.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(admin): label the dashboard published count with the state for screen readers
Applies the review suggestion: the published CountBadge sr-only label now
uses the state "Published" like the badge and filter, matching the
adjacent "Drafts" state label.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(admin): explicit collection order in the sidebar
The sidebar order came straight from `listCollections`, which sorted by
slug. A site's collections then appear in an order nobody chose —
"Certifications, Education, Endorsements, Pages, Positions, Posts,
Projects" — with no way out short of renaming slugs (breaking URLs and
queries) or forking the admin.
Adds `sort_order` on `_emdash_collections`, settable by dragging rows on
the Content Types screen or via `sortOrder` in a seed file. The column is
nullable rather than defaulting to 0: NULL means "no explicit position",
and those collections keep the alphabetical order behind the ordered
ones, so a site that never reorders renders exactly as before. Reads
materialise the fallback with COALESCE instead of relying on NULL
ordering, which SQLite puts first and Postgres last on ASC.
`reorderCollections` takes the full desired order and clears the position
of anything left out, so the stored state stays a faithful picture of
what the admin renders instead of a sparse set the UI has to reconcile.
`reorder` becomes a reserved collection slug: the static
POST /schema/collections/reorder route would otherwise shadow a
collection by that name. Same defence in depth already applied to
byline-fields/reorder.
Closes#474
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(admin): expose sortOrder on the schema export, tighten comments
Pre-empts the review notes already raised on the sibling `hidden` PR.
The full schema export the CLI reads for `emdash types` builds its
collection shape by hand and omitted the new field. The comment changes
apply AGENTS.md: drop justification and narrative, keep the non-obvious
constraints (route ordering, NULL sorting across dialects), and remove
issue references from test titles. The migration docstring also pointed
at a symbol name that never existed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>