fix: server-side content list search + locale-aware list indexes#1226
fix: server-side content list search + locale-aware list indexes#1226scottbuscemi wants to merge 1 commit into
Conversation
The admin content list filtered only the rows already loaded on the current page, so entries far back in a large collection were unfindable until you navigated near them. Add a `q` query param to the content list endpoint that performs a case-insensitive substring search across the collection's title/name/slug columns (LIKE wildcards escaped), and wire the admin search box to drive it (debounced) instead of filtering in memory. Add locale-aware composite indexes so i18n-filtered lists stay index-served on large tables. Closes #1219.
🦋 Changeset detectedLatest commit: 7545b03 The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
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 |
PR template validation failedPlease fix the following issues by editing your PR description:
See CONTRIBUTING.md for the full contribution policy. |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/blocks
@emdash-cms/cloudflare
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | 7545b03 | May 29 2026, 11:03 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
docs | 7545b03 | May 29 2026, 11:03 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | 7545b03 | May 29 2026, 11:05 PM |
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
There was a problem hiding this comment.
This is the right fix for the stated problem: moving content-list search server-side so large collections are fully searchable, plus locale-aware composite indexes to keep list queries index-served. The implementation follows EmDash conventions well—Lingui-wrapped labels, sql.ref() / validateIdentifier() for SQL safety, a proper changeset, describeEachDialect integration tests, and admin component tests. The handler-resolved searchColumns approach keeps the repository generic while avoiding missing-column errors.
However, the PR introduces three regressions that should be fixed before merge:
-
Postgres index-name truncation / collision. The new index names
idx_{table}_deleted_locale_updated_idandidx_{table}_deleted_locale_created_iddiffer only after a 26-character suffix. Postgres truncates identifiers to 63 bytes, so for any collection slug ≥ 40 characters (ec_+ slug = table name ≥ 43), the distinguishing part is truncated away and both names collide. In the migrationIF NOT EXISTSsilently skips the second index; inSchemaRegistry(noIF NOT EXISTS) collection creation hard-fails with “relation already exists”. The names need to be shortened so the divergence survives truncation. -
Search box disappears on empty server-side results. The search
<Input>is guarded by{items.length > 0 && ...}. With server-side search, a query that returns zero matches makesitemsempty, unmounting the search box. The user then cannot edit or clear their query. -
Pagination count shows loaded page size instead of total during server search.
renderItemCountreceivesfilteredCount: filteredItems.length. In server-search modefilteredItemsis just the currently loaded API page, so the count text displays e.g. “20 items matching …” even when the server reports a total of 100+. The caller already passes the correcttotalprop; the count text should use it in server mode.
|
@scottbuscemi tell ya boy to use the PR template! |
Addresses #1219.
Problem
On collections with 1000+ entries:
ContentListfiltereddata.pages.flatMap(...).filter(byTitle)client-side; the API client never sent a search term; the list query/handler/repository had no search filter.)Fix
Server-side search
contentListQueryschema gains an optionalq(trimmed, 1–200 chars).ContentRepository.findMany/countapply a case-insensitive substring filter across handler-resolvedsearchColumns, OR'd.lower()on both sides for SQLite/Postgres parity; LIKE wildcards (% _ \) in the query are escaped withESCAPE '\'.handleContentListresolves the searchable columns from the collection's fields — alwaysslug, plustitle/namewhen defined (mirrors the admin's title resolution) — so collections without those columns don't error.fetchContentListsendsq;ContentListdebounces the search box (300ms) and reports it up via a newonSearchChangeprop; the route feeds it into the infinite query key + fetch. Client-side filtering is retained only as legacy behavior whenonSearchChangeisn't supplied.Navigation
041_content_locale_list_indexaddsidx_{table}_deleted_locale_updated_idand…_created_idacross allec_*tables (idempotent, forward-only), mirrored inSchemaRegistryfor new collections.Deliberately out of scope
The research flagged the UI's page-number pagination over forward-only cursors as the bigger driver of deep-navigation latency; that's a larger UX refactor and is deferred. I also did not skip the per-page COUNT — there's an explicit regression guard requiring
totalon every page, so that optimization would break documented behavior.Testing
packages/core/tests/integration/content/content-list-search.test.ts(describeEachDialect): finds a deep entry, case-insensitive, slug match, wildcard-escape ("50%"matches only the literal), and unfiltered fallback. Passing on SQLite locally; Postgres in CI.ContentList.test.tsx: server mode reports the debounced query and does not client-filter.pnpm lint:jsonclean;emdash+@emdash-cms/admintypecheck pass. Admin browser tests run in CI (couldn't launch a local browser here).Manual verification
Try this PR
Open a fresh playground →
A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.
Tracks
fix/1219-content-list-search. Updated automatically when the playground redeploys.