Skip to content

feat(list): free-text search box + register_search_expansion seam - #12

Closed
jcfrei wants to merge 1 commit into
masterfrom
feat/list-search
Closed

feat(list): free-text search box + register_search_expansion seam#12
jcfrei wants to merge 1 commit into
masterfrom
feat/list-search

Conversation

@jcfrei

@jcfrei jcfrei commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Why

The lead list (and every generic list) had no way to search by company name / UID / town — only the record ID typeahead. Requested for the CRM, where you also want to find a lead by one of its contacts.

What

Config-driven search box. A doctype declares searchFields: string[]; the list renders a debounced search box (commits to the URL q ~300ms after you stop typing — no per-keystroke refetch). The list/count/adjacent endpoints accept search + search_fields (a CSV validated to real columns, same contract as filters/order_by). Search is ANDed with the other filters, and prev/next (DocPager) steps through the searched result set too. Omit searchFields → no box, no behavior change.

register_search_expansion(doctype_slug, fn) seam. Same-table LIKE can't reach a related table the core doesn't know about. This seam lets a plugin add matches: fn(query, db) returns the doctype's PK names whose related rows match, OR-ed into the search as name IN (...). The internal CRM registers one to find a Lead by its Contacts (name/email/phone).

Notes

  • Case-insensitive via LOWER(col) LIKE LOWER(?) (consistent SQLite + Postgres).
  • search_fields kept to real columns before hitting SQL; expansion results de-duplicated.
  • The get_all fast path can't express an OR-group, so a search drops to the raw-SQL list path (same one offset/date-range already use).

Tests

tests/test_search.py — same-table search (case-insensitive, ANDed with filters), the expansion seam (match only via the related table), count parity, unknown-field 400, and adjacent tracking the searched list. Runs on SQLite + Postgres in CI.

🤖 Generated with Claude Code

A doctype config declares searchFields (columns matched case-insensitively);
the list shows a debounced search box (commits to URL `q` after a pause, no
per-keystroke refetch). list/count/adjacent accept search + search_fields (CSV
validated to real columns), ANDed with the other filters, and prev/next steps
through the searched set too.

register_search_expansion(slug, fn) lets a plugin match a doctype by a related
table the core doesn't know: fn(query, db) returns PK names OR-ed into the
search (name IN (...)). The internal CRM uses it to find a Lead by its Contacts.

tests/test_search.py: same-table search, expansion seam, count parity, adjacent
(SQLite + Postgres in CI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jcfrei added a commit that referenced this pull request Jul 29, 2026
feat(list): free-text search box + register_search_expansion seam (#12)

Config-declared searchFields render a debounced list search box (commits to
URL `q`, no per-keystroke refetch); list/count/adjacent take search +
search_fields ANDed with filters. register_search_expansion(slug, fn) lets a
plugin also match via a related table (name IN (...)) — the internal CRM finds
a Lead by its Contacts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jcfrei

jcfrei commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Squash-merged into master as 13b49b8 and released as v0.6.8.

@jcfrei jcfrei closed this Jul 29, 2026
@jcfrei
jcfrei deleted the feat/list-search branch July 29, 2026 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant