Custodian Portal Search
Extends Custodian Portal UX with a global search in the app bar and saved searches a team can share.
Two features, one vocabulary
| Global search (app bar) | List search (on a page) | |
|---|---|---|
| Job | Jump to a record from anywhere | Filter the list on People, Credentials, or Intake |
| Input | Free text | Free text + that list’s filters |
| Result | A grouped popover of records | The page’s own table |
| Saved? | Offers your saved and recent searches | Yes — this is what gets saved |
They meet at one action: pressing Enter on free text in the app bar opens the top list page for the
query, and the page shows it as the active search. A search is always the same object —
{ text, filters } — whether typed in the app bar, read from a URL, or loaded from a saved search.
Documents and groups are jump-to only: neither has a list page to search, so there’s no saved search for them.
The search object
{ "v": 1, "text": "smith", "filters": { "status": "NOT_INVITED" } }Filter keys are an allow-list per scope, enforced by the server (an unknown key/value is
400 INVALID_SEARCH):
| Scope | Page | Text matches | Filters |
|---|---|---|---|
people | /people | name, email, reference ID, an active group’s name | status, documents, group (an id filter) |
credentials | /credentials | credential type, holder name/email, internal code, id prefix | status |
batches | / (Intake) | batch id prefix, date, a document’s filename | — |
An id filter (e.g. People’s group) accepts any UUID and resolves it inside the caller’s own
custodian — a foreign or unknown id is a 404, never data. Raw extracted document content is
never searched — it’s unvetted personal data and would bypass the audited per-person detail view.
Global search results
GET /api/v1/custodian/me/search?q= groups People, Groups, Documents, Credentials,
Batches — at most 5 per group, 25 total, minimum 2 characters, every group scoped to the
caller’s own custodian_id. Opening a result goes through the existing audited detail endpoints —
search adds no new way to read a person’s records.
Saved searches
| Action | Viewer | Operator | Admin |
|---|---|---|---|
| Create a private search | yes | yes | yes |
| Share with the team | — | yes | yes |
| Edit/rename/unshare/delete your own | yes | yes | yes |
| Edit/delete someone else’s shared search | — | — | yes |
| Use a shared search, or make it your default | yes | yes | yes |
A TEAM search is visible to every active team member of the same custodian, never to another
custodian. If an owner leaves the team, their shared searches stay (with their name kept as
written); their private searches and defaults are left in place, unreachable.
Default search
One default per user per scope. It applies only when the page is opened with no search in the URL — a link, an app-bar result, or “Show all” always wins — and it’s always visible as an “Applying your default” chip with a way to show all without deleting the default.
Recent and most-used
A search is recorded when committed (Enter in the app bar, or a filter/text change on a list page that survives a short pause) — never per keystroke. History is deduplicated per user and scope, pruned to the 50 most recently used. The empty search box shows, in order: Saved (default first, then most-used, then name), Most used, Recent.
Deep links
The batch review dialog and credential detail are URL state, not component state
(/batches/:id?item=:itemId, /credentials?tab=register&credential=:id) — so a document or
credential is linkable and Back closes it.
Interface
- App bar: brand and sections left, search centered, account and theme right.
- Keys: ⌘K/Ctrl+K and / (no field focused) open it; arrows move; Enter opens; Esc closes and returns focus.
- List pages carry no search box in the page body — the Filter (or Saved searches) menu sits in the header beside the primary action; a chip row under the header shows the active text/filters, the loaded saved-search pill, and Save/Update/Clear.