Skip to content

fix(admin): extend server-side content search to the content picker and MCP - #752

Open
edrpls wants to merge 5 commits into
emdash-cms:mainfrom
edrpls:fix/content-list-search
Open

fix(admin): extend server-side content search to the content picker and MCP#752
edrpls wants to merge 5 commits into
emdash-cms:mainfrom
edrpls:fix/content-list-search

Conversation

@edrpls

@edrpls edrpls commented Apr 24, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Extends the server-side content list search (?q=) — which shipped via #1226 — to two surfaces that still post-filtered in memory:

  • ContentPickerModal (used when linking content from the editor) now pushes its search box to the server (search option → ?q=) instead of filtering only the rows already loaded, so it finds entries anywhere in a large collection. It uses keepPreviousData so the list doesn't flash to empty between keystrokes, and keeps load-more available while searching (results can span multiple pages).
  • MCP content_list tool gains a q parameter, so agents can search a collection server-side rather than post-filtering a single page of results.

This branch was rebased onto main after #1226 merged an independent implementation of the core feature. The overlapping core-search work (handler, repository, schemas, ContentList, router, API client, index migration) is now provided by #1226; this PR keeps only the picker + MCP extensions on top of it.

Related: #1219, #1226

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: https://github.com/emdash-cms/emdash/discussions/...

AI-generated code disclosure

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

Screenshots / test output

MCP integration tests pass, including a new case asserting content_list filters by q:

Test Files  1 passed (1)
     Tests  27 passed (27)
@changeset-bot

changeset-bot Bot commented Apr 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3b98339

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 1,912 lines across 29 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

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
packages/admin/src/locales/ar/messages.po Localization changed, will be marked as complete.
packages/admin/src/locales/de/messages.po Localization changed, will be marked as complete.
packages/admin/src/locales/en/messages.po Source changed, localizations will be marked as outdated.
packages/admin/src/locales/es-419/messages.po Localization changed, will be marked as complete.
packages/admin/src/locales/eu/messages.po Localization changed, will be marked as complete.
packages/admin/src/locales/fa/messages.po Localization changed, will be marked as complete.
packages/admin/src/locales/fr/messages.po Localization changed, will be marked as complete.
packages/admin/src/locales/ja/messages.po Localization changed, will be marked as complete.
packages/admin/src/locales/ko/messages.po Localization changed, will be marked as complete.
packages/admin/src/locales/pseudo/messages.po Localization changed, will be marked as complete.
packages/admin/src/locales/pt-BR/messages.po Localization changed, will be marked as complete.
packages/admin/src/locales/zh-CN/messages.po Localization changed, will be marked as complete.
packages/admin/src/locales/zh-TW/messages.po Localization changed, will be marked as complete.
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.
@pkg-pr-new

pkg-pr-new Bot commented Apr 24, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-verification

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 3b98339

@github-actions

github-actions Bot commented May 8, 2026

Copy link
Copy Markdown
Contributor

This PR has been inactive for 14 days. It will be closed automatically in 7 days if there is no further activity.

If you're still working on this, please push an update or leave a comment.

@edrpls

edrpls commented May 16, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on upstream main (f28eb7b). Resolved orthogonal conflicts with #750 (sort headers, since merged) — <ContentList> now takes both sort/onSortChange and searchQuery/onSearchChange props; router threads sort + debounced search through the same TanStack Query key; fetchContentList accepts both orderBy/order and q; describe("orderBy"), describe("search"), and describe("sortable headers") test blocks coexist. Locale .po files reset to upstream for Lunaria re-extraction.

Local test runs after rebase, on top of f28eb7b:

  • packages/core/tests/database/repositories/content.test.ts: 51/51 pass (includes 6 new search tests)
  • packages/core/tests/unit/api/content-handlers.test.ts: 33/33 pass
  • packages/admin/tests/components/ContentList.test.tsx: 38/38 pass

Bumping out of stale-warning territory.

@edrpls

edrpls commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

Update — scope narrowed after rebase. Since my review note above, #1226 merged an independent implementation of the core server-side content search (?q=), including the locale-aware list index migration. I've rebased this branch onto main and dropped the overlapping core work; this PR is now just the two surfaces #1226 didn't cover:

  • ContentPickerModal — pushes its search box to the server instead of in-memory filtering.
  • MCP content_list — gains a q parameter so agents search server-side.

That moots most of the items above. Re-mapping each:

Design calls — now #1226's territory

  1. Non-ASCII case-insensitive search on SQLite — no longer in this PR; the core search ships via fix: server-side content list search + locale-aware list indexes #1226. If the ASCII-only LOWER() folding is still worth addressing, it's a follow-up against fix: server-side content list search + locale-aware list indexes #1226's implementation rather than a blocker here. Happy to open a separate issue.
  2. Hardcoded ["title","name","slug"] vs the field schemafix: server-side content list search + locale-aware list indexes #1226 partly addresses this: its resolveSearchColumns derives columns from _emdash_fields (adds title/name only when the collection defines them) rather than a flat constant. Whether to further key off the searchable flag / unify with the FTS layer is best tracked against that code.

Lower-priority findings

  • Postgres table_schema = 'public' hardcode — moot; the listTableColumns helper that had it was part of the discarded implementation and is gone.
  • ContentPickerModal load-more wiped by background refetchfixed in c4cb0e99 — the picker now derives its list from useInfiniteQuery pages instead of a manual accumulator, so a refocus/invalidation can't drop loaded pages.
  • ContentPickerModal load-more cursor/query mismatchfixed in the same commit — search lives in the query key, so a search change starts a fresh page chain rather than firing load-more with a stale cursor.
  • ContentList zero-result CTA flash — moot; that component's search now ships via fix: server-side content list search + locale-aware list indexes #1226.
  • escapeLike duplication — moot here (the copy was in the discarded code); still a reasonable standalone cleanup if anyone wants to pick it up.

All inline threads have been resolved accordingly. No maintainer decision is blocking this PR anymore — it's a focused follow-up on top of #1226. 🤖 update drafted with Claude Code.

@github-actions github-actions Bot added stale and removed stale labels Jun 27, 2026
@ascorbic ascorbic removed the bot:review Trigger an emdashbot code review on this PR label Jul 1, 2026
@github-actions github-actions Bot added the stale label Jul 15, 2026
@edrpls
edrpls force-pushed the fix/content-list-search branch from c4cb0e9 to e45bf3b Compare July 15, 2026 20:53
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 20, 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 the right fix for a real bug. Moving the content picker and MCP content_list to the server-side q search introduced in #1226 is the correct architecture, and the implementation fits EmDash's patterns. The picker now uses useInfiniteQuery with keepPreviousData to avoid flashing empty lists between keystrokes, and it correctly keeps the load-more button available while searching. The MCP tool exposes q and passes it through to the handler layer, reusing the same SQL-safe repository path as the REST route.

I checked the picker refactor, the MCP schema and tool wiring, the new MCP integration test, the underlying handleContentList/ContentRepository.findMany search implementation, and the current ContentList component. The four findings from the prior review are addressed: the three ContentList issues (trusting the server total in search mode, keeping the search input visible, and showing a "no results" state instead of the empty-collection prompt for zero-result searches) are already fixed on this branch, and this PR fixes the picker's load-more-during-search issue.

Headline conclusion: The PR is clean and ready to land after addressing two minor nits — an MCP schema inconsistency and a test organization issue. No blocking logic, security, or convention problems.

Comment thread packages/core/src/mcp/server.ts
Comment thread packages/core/tests/integration/mcp/content-misc.test.ts
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026
edrpls added a commit to edrpls/emdash that referenced this pull request Jul 20, 2026
…h test

Address review feedback on emdash-cms#752:

- MCP `content_list` `q` now uses `.trim().min(1).max(200)`, matching the
  REST `contentListQuery` contract. It previously accepted empty/whitespace
  `q` and silently ignored it; it now rejects it, so the MCP and REST
  interfaces accept and reject the same inputs.
- Move the `content_list` search test out of the unrelated
  "soft-delete visibility" describe into a dedicated "content_list search"
  block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@edrpls

edrpls commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Merged current main (36e9e66) — the branch was 2 commits behind after the last sync.

On the two red E2E checks from the previous run: they're the invite-flow spec (user appears in the users listtext=invited-user@example.com not visible within 15s), which this PR cannot plausibly affect. The diff against main is 4 files — ContentPickerModal.tsx, mcp/server.ts, the MCP test, and the changeset — and touches nothing in the users, invite, or auth path. Main's own CI is green on 11 of its last 12 runs, and the same E2E suite passes on my other open PRs, so this reads as a flaky/timing failure rather than a regression. I don't have rerun rights on the repo, so this merge doubles as a fresh CI trigger — if the shard goes green on this run that settles it.

Local verification on the merged head: full packages/core suite green (5094 passing), MCP integration suite 321 passing, lint and typecheck clean for both emdash and @emdash-cms/admin.

edrpls added a commit to edrpls/emdash that referenced this pull request Aug 8, 2026
…h test

Address review feedback on emdash-cms#752:

- MCP `content_list` `q` now uses `.trim().min(1).max(200)`, matching the
  REST `contentListQuery` contract. It previously accepted empty/whitespace
  `q` and silently ignored it; it now rejects it, so the MCP and REST
  interfaces accept and reject the same inputs.
- Move the `content_list` search test out of the unrelated
  "soft-delete visibility" describe into a dedicated "content_list search"
  block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@edrpls
edrpls force-pushed the fix/content-list-search branch from 36e9e66 to 02e5c05 Compare August 8, 2026 14:14
@edrpls

edrpls commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@ascorbic gentle ping on this one — it's been open since April and is in a ready state: rebased onto current main, CI fully green, both emdashbot review passes addressed (the second concluded "clean and ready to land after two nits", and those nits landed with regression tests). Zero unresolved threads.

If you'd rather route it through the bot first, I can't apply bot:review myself (no label permissions) — reapplying it would kick a fresh pass. Happy to rebase again if it drifts before you get to it.

@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Aug 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 the right fix for a real bug. Moving ContentPickerModal and the MCP content_list tool onto the server-side ?q= search introduced in #1226 is the correct architecture for large collections, and the implementation follows EmDash’s existing patterns.

I checked the picker refactor, the MCP tool/schema wiring, the new MCP integration tests, the underlying handleContentList/ContentRepository search path, and the REST contentListQuery schema. The four ContentList issues from the prior review remain resolved on this branch, and the previous MCP schema inconsistency is now fixed: the MCP q shape matches the REST schema exactly (z.string().trim().min(1).max(200).optional()).

Headline conclusion: the PR is clean and nearly ready to land. There are a couple of remaining low-grade nits — one stale test-file header and an explanatory JSX comment that violates the comment-discipline convention — plus a minor UX inconsistency in the picker empty state. None of these are blockers.


Findings

  • [suggestion] packages/admin/src/components/ContentPickerModal.tsx:203

    This JSX comment narrates the change rather than explaining non-obvious code. AGENTS.md says comments must not be PR/reviewer-facing narrative; the hasNextPage && condition already expresses that load-more stays available. Remove it.

    							{hasNextPage && (
    
  • [suggestion] packages/admin/src/components/ContentPickerModal.tsx:156

    The empty-state uses the raw searchQuery to decide whether to show the search-specific prompt. Because searchParam falls back to undefined when the input is whitespace-only, a user typing only spaces sees "Try adjusting your search" even though the server is not actually filtering. Use the effective search term instead.

    						{searchParam ? (
    
  • [suggestion] packages/core/tests/integration/mcp/content-misc.test.ts:2

    The file header no longer matches the file contents: the new content_list search describe block is not listed. Update the docblock so future readers know this file covers the content_list q parameter.

    /**
     * MCP content tools — coverage for the remaining tools and edges.
     *
     * Covers:
     *   - content_duplicate
     *   - content_permanent_delete
     *   - content_translations + locale handling on create/get
     *   - _rev optimistic concurrency (happy + race)
     *   - Soft-delete visibility (content_get / content_list filtering)
     *   - content_list search by `q`
     *   - Edit-while-trashed
     *   - Idempotency (publish twice, unpublish-on-draft, schedule + publish)
     */
    
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Aug 12, 2026
@edrpls

edrpls commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

All three findings from the fresh review pass are addressed in 12f892e:

  • Picker empty-state now gates on the effective searchParam — a whitespace-only query no longer claims a search is filtering.
  • Narrative JSX comment above the load-more condition removed.
  • MCP test header now lists the content_list search-by-q coverage.

Verified locally: lint clean, MCP suite 29/29, full admin browser suite 1363/1363.

edrpls and others added 4 commits August 16, 2026 00:45
The core server-side content search (?q=) landed via emdash-cms#1226. Extend it to
two surfaces that still post-filtered in memory:

- ContentPickerModal now pushes its search box to the server (search
  option -> ?q=), so it finds entries anywhere in a large collection
  instead of only the rows already loaded. Uses keepPreviousData to
  avoid flashing to empty between keystrokes and keeps load-more
  available while searching.
- MCP content_list gains a q parameter, so agents search server-side
  rather than post-filtering a page of results.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the manual accumulator (allItems/nextCursor mirrored from a
useQuery) with useInfiniteQuery, matching the ContentList pattern. The
accumulator could lose loaded pages on any background refetch (window
focus, cache invalidation) and, with keepPreviousData, could fire
load-more with a stale cursor against a freshly-changed search. Deriving
the list straight from query pages removes both windows; search stays in
the query key so changing it starts a fresh page chain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…h test

Address review feedback on emdash-cms#752:

- MCP `content_list` `q` now uses `.trim().min(1).max(200)`, matching the
  REST `contentListQuery` contract. It previously accepted empty/whitespace
  `q` and silently ignored it; it now rejects it, so the MCP and REST
  interfaces accept and reject the same inputs.
- Move the `content_list` search test out of the unrelated
  "soft-delete visibility" describe into a dedicated "content_list search"
  block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The q schema gained min(1) to match the REST contract, but nothing
asserted it — the constraint could be dropped without a test noticing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@edrpls
edrpls force-pushed the fix/content-list-search branch from 12f892e to 822fd3f Compare August 16, 2026 06:46
A whitespace-only query showed "Try adjusting your search" while the
server wasn't filtering. Also drops a narrative JSX comment and lists
the content_list search coverage in the MCP test header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0175x2Nu2m7LZUznaEQUAVQd
@edrpls
edrpls force-pushed the fix/content-list-search branch from 822fd3f to 3b98339 Compare August 16, 2026 07:12
@github-actions github-actions Bot added stale and removed stale labels Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

2 participants