|
| 1 | +# Universal Search |
| 2 | + |
| 3 | +Ephemeral, debounced multi‑column search for List pages. A lightweight input (injected at `beforeActionButtons`) sends the term with each list request; a hook expands it server‑side into a single `OR` filter group over your configured columns. The term never enters the standard filter store, so: |
| 4 | + |
| 5 | +- No extra badge count |
| 6 | +- No URL pollution |
| 7 | +- No UI filter chips to manage |
| 8 | + |
| 9 | +Ideal for quick, multi‑field lookup without opening the filter panel. |
| 10 | + |
| 11 | +## Installation |
| 12 | + |
| 13 | +```bash |
| 14 | +npm i @adminforth/universal-search --save |
| 15 | +``` |
| 16 | + |
| 17 | +## Basic Usage |
| 18 | + |
| 19 | +Add the plugin to any resource. Place it inside the `plugins` array. It injects a component at the `beforeActionButtons` list page injection point (already available in AdminForth if you are on a recent version). |
| 20 | + |
| 21 | +```ts title="./resources/apartments.ts" |
| 22 | +// diff-add |
| 23 | +import UniversalSearchPlugin from '@adminforth/universal-search'; |
| 24 | + |
| 25 | +export const admin = new AdminForth({ |
| 26 | + ..., |
| 27 | + resources: [ |
| 28 | + { |
| 29 | + resourceId: 'aparts', |
| 30 | + table: 'apartments', |
| 31 | + columns: [ |
| 32 | + { name: 'id', primaryKey: true }, |
| 33 | + { name: 'title' }, |
| 34 | + { name: 'description' }, |
| 35 | + { name: 'country' }, |
| 36 | + { name: 'price' }, |
| 37 | + ], |
| 38 | + plugins: [ |
| 39 | + // diff-add |
| 40 | + new UniversalSearchPlugin({ |
| 41 | + // diff-add |
| 42 | + columns: [ |
| 43 | + // diff-add |
| 44 | + { name: 'title' }, |
| 45 | + // diff-add |
| 46 | + { name: 'description' }, |
| 47 | + // diff-add |
| 48 | + { name: 'country', caseSensitive: true }, |
| 49 | + // diff-add |
| 50 | + { name: 'price', exact: true }, |
| 51 | + // diff-add |
| 52 | + ], |
| 53 | + // diff-add |
| 54 | + debounceMs: 400, // optional (default 500) |
| 55 | + // diff-add |
| 56 | + placeholder: 'Search apartments…' // optional (default empty string) |
| 57 | + // diff-add |
| 58 | + }), |
| 59 | + ] |
| 60 | + } |
| 61 | + ] |
| 62 | +}); |
| 63 | +``` |
| 64 | + |
| 65 | +Type into the input and (after debounce) the backend receives the term (as an internal field) and the plugin hook rewrites it into a single composite filter like this: |
| 66 | + |
| 67 | +```json |
| 68 | +{ |
| 69 | + "operator": "or", |
| 70 | + "subFilters": [ |
| 71 | + { "field": "title", "operator": "ilike", "value": "%pent%" }, |
| 72 | + { "field": "description", "operator": "ilike", "value": "%pent%" }, |
| 73 | + { "field": "country", "operator": "like", "value": "%pent%" } |
| 74 | + ] |
| 75 | +} |
| 76 | +``` |
| 77 | + |
| 78 | +`price` (marked `exact`) will be compared for exact match (no wildcards). Numeric heuristics may be applied in future versions (current implementation sends the same OR group regardless of numeric content — adjust logic in hook if you need number detection). |
| 79 | + |
| 80 | +Press Enter to apply immediately without waiting for the debounce delay. Clearing the input removes the universal filter group entirely. |
| 81 | + |
| 82 | +## Options |
| 83 | + |
| 84 | +```ts |
| 85 | +new UniversalSearchPlugin({ |
| 86 | + columns: [ |
| 87 | + { |
| 88 | + name: string; // required column name |
| 89 | + caseSensitive?: boolean; // default false |
| 90 | + exact?: boolean; // exact match (no wildcards) |
| 91 | + searchBy?: 'valueOnly' | 'keyOnly' | 'both' (reserved; not exposed yet in public docs) |
| 92 | + } |
| 93 | + ], |
| 94 | + debounceMs?: number; // default 500 |
| 95 | + placeholder?: string; // input placeholder (default "") |
| 96 | +}); |
| 97 | +``` |
| 98 | + |
| 99 | +Notes: |
| 100 | +- The virtual field name (`_universal_search`) and ephemeral behavior are fixed and not configurable. |
| 101 | +- The input does not create visible filters; clearing it removes internal search state. |
| 102 | + |
| 103 | +## Debounce Behavior |
| 104 | + |
| 105 | +- Default delay: 500 ms (configurable via `debounceMs`). |
| 106 | +- Enter key: applies immediately. |
| 107 | +- Input cleared: universal OR filter group removed. |
| 108 | + |
| 109 | +## How It Works Internally |
| 110 | + |
| 111 | +1. Component writes the current term to a transient global (`adminforth.__universalSearchTerm`). |
| 112 | +2. List request body includes `__universal_search_term`. |
| 113 | +3. Plugin `beforeDatasourceRequest` hook adds a temporary virtual filter. |
| 114 | +4. Hook expands that virtual filter to an `OR` group across configured columns. |
| 115 | +5. The expanded group is executed; the temporary filter is not shown in the UI. |
| 116 | + |
| 117 | +This means the UI stays clean while the backend still receives a standard filter structure. |
| 118 | + |
| 119 | +## Roadmap / Future Enhancements |
| 120 | + |
| 121 | +- Optional multi-term splitting (turn "foo bar" into two groups) |
| 122 | +- Enum label / foreign key label search exposure |
| 123 | +- Loading indicator & progress feedback |
| 124 | +- Optional persistence of the last term per resource |
| 125 | + |
| 126 | +## License |
| 127 | + |
| 128 | +MIT |
0 commit comments