Skip to content

feat(admin): explicit collection order in the sidebar - #2267

Open
DavidPivert wants to merge 2 commits into
emdash-cms:mainfrom
DavidPivert:feat/collection-sort-order
Open

feat(admin): explicit collection order in the sidebar#2267
DavidPivert wants to merge 2 commits into
emdash-cms:mainfrom
DavidPivert:feat/collection-sort-order

Conversation

@DavidPivert

Copy link
Copy Markdown

What does this PR do?

Lets a site choose the order its collections appear in the admin sidebar, instead of always sorting alphabetically by slug.

Today listCollections sorts by slug, and that order flows through the manifest into the sidebar. The issue's example is a portfolio site that always renders "Certifications → Education → Endorsements → Pages → Positions → Posts → Projects", with no way out short of renaming slugs (breaking URLs and queries) or forking the admin.

Two ways to set it:

  • Drag the rows on the Content Types screen.
  • sortOrder in a seed file, for sites that provision their schema from code:
{ "slug": "projects", "label": "Projects", "sortOrder": 1, "fields": [] }

Three decisions worth flagging for review:

  • The column is nullable, not DEFAULT 0. NULL means "no explicit position": those collections keep the alphabetical order and are listed after the ordered ones. With a 0 default, pinning two collections would have left every other one sorting ahead of them, which is the opposite of what pinning means. It also makes the migration a no-op visually — a site that never reorders renders exactly as before.
  • Reads materialise the fallback with COALESCE rather than relying on NULL ordering. SQLite sorts NULL first on ASC, Postgres sorts it last, so a bare ORDER BY sort_order would have given D1 and Postgres sites different sidebars. packages/core/tests/integration/database/dialect-compat.test.ts covers the dialect pair generally; the ordering itself is asserted in the registry tests.
  • reorder is now a reserved collection slug. The static POST /schema/collections/reorder route is injected before the dynamic [slug] route, so a collection called reorder could never be addressed at its own URL. Reserving it at the data layer mirrors what byline-fields/reorder already does. Sites with an existing collection by that name are unaffected — the reservation only rejects new creates.

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 rather than a sparse set the UI has to reconcile. Unknown and duplicate slugs are rejected rather than silently applied.

On accessibility: the drag handles are real buttons with an accessible name per row (Reorder Posts, not a bare icon), and the KeyboardSensor is wired with sortableKeyboardCoordinates, so the whole reorder is doable from the keyboard — same pattern as the widgets and repeater lists.

Closes #474

Type of change

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

Discussion: #1751 — the Roadmap discussion mirroring #474. The issue is on the 1.0 milestone, labelled roadmap/1.0 / roadmap/editor-experience. #795 was closed as a duplicate of it.

Scope note: this is flat ordering only. #1023 (sidebar menu tree with collection grouping) is a larger reshaping of the same nav — sort_order is orthogonal to it and would carry over as the order within a group, but say the word if you'd rather this wait for that design to land.

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: Feature: Collection sort order in admin sidebar #1751

AI-generated code disclosure

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

Screenshots / test output

15 new tests:

  • packages/core/tests/unit/schema/registry.test.ts — default is unordered; ordered collections come first and the rest stay alphabetical; the same order applies on the listCollectionsWithFields (manifest) path; reorderCollections assigns positions, clears omitted ones, and rejects unknown/duplicate slugs without touching the order; update preserves the position when sortOrder is omitted and clears it on null; reorder is rejected as a slug.
  • packages/core/tests/unit/seed/apply.test.tssortOrder is applied from a seed and drives the resulting order.
  • packages/admin/tests/components/ContentTypeList.test.tsx — a labelled drag handle per row when reordering is enabled, none without it or for a single collection, rows render in the given order, plus the pure moveCollection reducer (down, up, and the no-op cases that must skip the network call).
packages/core   vitest run tests/unit                    3432 passed, 1 failed
packages/core   vitest run tests/integration/openapi       14 passed
packages/admin  vitest run                               1249 passed
pnpm --filter @emdash-cms/core typecheck                 clean
pnpm --filter @emdash-cms/admin typecheck                clean
oxlint --type-aware --deny-warnings                      clean
oxfmt --check && prettier --check .                      clean

Two notes on that output, both checked against a clean main by stashing this branch:

  • The core failure (createVirtualModulesPlugin scheduler wiring watches resolved sandbox plugin entries) reproduces on unmodified main on this machine — pre-existing, unrelated to this change.
  • An earlier full admin run showed one failure in SeoPanel.test.tsx ("expected 1 times, got 2"). It passes in isolation and passed on a repeat full run (1249/1249); SeoPanel is untouched here. Flagging it as a flake I saw rather than leaving it out.

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 emdash-cms#474

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

changeset-bot Bot commented Jul 28, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 616ef4e

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

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 692 lines across 22 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

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

Feature: Collection sort order in admin sidebar

1 participant