@get-asset/sdk
Contacts
Headless compound for the Customers and Vendors directories. Both are the same surface, so one compound serves both and kind picks the endpoints, the scopes and the copy — "customer" or "vendor". It lists the business's contacts (name, email, phone, active/disabled) with cursor pagination behind LoadMore, filters by a name search and by active/disabled, and owns the whole write flow: CreateTrigger opens a blank draft, EditTrigger opens an existing contact in the same draft, and Save POSTs or PATCHes accordingly. Only name is required — a cleared email or phone is sent as null so it actually clears. DisableTrigger flips a contact’s is_disabled and the disabled listing is a separate view.
Anatomy
Import the primitive and compose the parts you need.
<Contacts.Root kind="customer">{/* Header controls — always rendered, outside the state gates */}<Contacts.Title /><Contacts.Search /><Contacts.StatusFilter><Select> {/* any Select-Root-shaped child */}<Contacts.StatusFilterItems><Contacts.StatusFilterItem /></Contacts.StatusFilterItems></Select></Contacts.StatusFilter><Contacts.CreateTrigger />{/* The add panel — a brand-new draft, above the table */}<Contacts.Create><Contacts.EditorTitle /><Contacts.EditorError /><Contacts.NameInput /><Contacts.EmailInput /><Contacts.PhoneInput /><Contacts.Cancel /><Contacts.Save /></Contacts.Create><Contacts.Loading>{/* skeleton rows */}</Contacts.Loading><Contacts.Error><Contacts.ErrorMessage /><Contacts.Retry /></Contacts.Error><Contacts.Empty>{/* empty copy — plus <Contacts.ClearSearch /> when a search is active */}</Contacts.Empty><Contacts.Success><Contacts.Rows><Contacts.Name /><Contacts.Email /><Contacts.Phone /><Contacts.Status /><Contacts.EditTrigger /><Contacts.DisableTrigger />{/* Editing expands from the row it belongs to */}<Contacts.RowEditing><Contacts.EditorTitle />{/* the same fields as the add panel */}</Contacts.RowEditing></Contacts.Rows><Contacts.LoadMore /></Contacts.Success></Contacts.Root>
Required scopes
The access token used by AssetProvider must include these scopes for this primitive to fetch data successfully:
customer:readcustomer:createcustomer:updatevendor:readvendor:createvendor:update
API reference
Root props
| Prop | Type | Default | Description |
|---|---|---|---|
kindrequired | "customer" | "vendor" | — | Which directory this is. Picks the endpoints, the labels (Customer/Vendor, singular and plural), and the half of the scopes that gets used. |
pageSize | number | 25 | Rows per request. Further pages are fetched by LoadMore. |
contacts | ContactOverride[] | — | Override the fetched contacts (skips the API call). The search and active/disabled filters are still applied, client-side, so an offline story behaves like the live component. Used by storybook/tests. |
state | "loading" | "error" | "empty" | — | Force a specific state for testing. Also skips the API call. |
hasMore | boolean | — | Force the LoadMore affordance to render. Only consulted alongside contacts — live, pagination comes from the API's next page token. |
defaultCreating | boolean | false | Open the add-contact draft on mount. |
childrenrequired | ReactNode | — | Compound parts. |
Parts
Container
Always rendered. Fetches the list, owns the draft and the create/update/disable mutations, and provides context. Contacts is a management surface rather than an entitled report, so it has no locked state.
| Part | Description |
|---|---|
Contacts.Root | Owns fetching, pagination, the search and status filters, the open draft, and the disable toggle. |
Header controls
Always available — these read the root context, not a row, so they sit outside the state gates and keep working while the list is loading, empty or errored.
| Part | Description |
|---|---|
Contacts.TitleasChild? | The directory's heading — "Customers" / "Vendors". |
Contacts.SearchasChild?, placeholder? | Name filter input. Injects value, onChange and a kind-aware placeholder ("Search customers by name") into an input-shaped child; renders a bare <input type="search"> otherwise. Typing is debounced before the list is re-requested (see Search and filters). |
Contacts.ClearSearchasChild? | Clears the name filter. Renders nothing when the search is already empty, which makes it safe to drop into an Empty state unconditionally. Default label "Clear search". |
Contacts.CreateTriggerasChild? | Opens a blank draft. Sets aria-expanded while any draft is open. Default label "Add customer" / "Add vendor". |
Contacts.StatusFilter | Wires the active/disabled filter to a single Select-Root-shaped child, cloning it with value ("active" | "disabled") and onValueChange. |
Contacts.StatusFilterItems | Renders its children once per option, providing the option as context. Put it inside the select's content. |
Contacts.StatusFilterItemasChild? | One option — sets value and renders the label ("Active" / "Disabled"). |
Success
Renders when at least one contact matches the current filters.
| Part | Description |
|---|---|
Contacts.Success | State wrapper. Compose the table inside. |
Contacts.Rows | Iterates the contacts, providing a per-row context to each. |
Contacts.NameasChild? | The contact's name. |
Contacts.EmailasChild? | The contact's email, or an em dash when there isn't one. Sets data-empty on the fallback so a stacked layout can drop the line instead. |
Contacts.PhoneasChild? | The contact's phone, with the same em-dash fallback and data-empty. |
Contacts.StatusasChild? | Renders "Active" / "Disabled" and sets data-status. Every row in a given view carries the same status — the listing is one set or the other. |
Contacts.RowDisabled | Renders children only when the row's contact is disabled. |
Contacts.EditTriggerasChild? | Opens the row's contact in the editor. Names itself "Edit <name>" via aria-label unless you pass your own. Default label "Edit". |
Contacts.DisableTriggerasChild? | Disables an active contact, or re-enables a disabled one. Label and aria-label follow the row ("Disable" / "Re-enable"), and the trigger disables itself while that row's write is in flight. |
Editor
One draft at a time, shared by add and edit. Opening or cancelling a draft clears the previous save failure, so a fresh panel never inherits one.
| Part | Description |
|---|---|
Contacts.Editor | Renders children while any draft is open — create or edit. |
Contacts.Create | Renders children only while a new contact is being authored (the add panel). |
Contacts.RowEditing | Renders children only when this row is the one being edited. Use inside Rows to expand the editor from the row. |
Contacts.EditorTitleasChild? | "Add customer" / "Edit customer", matching the open draft. |
Contacts.NameInputasChild? | The draft's name field — the only required one. Injects value, onChange and required into an input-shaped child; renders a bare input otherwise. |
Contacts.EmailInputasChild? | The draft's email field. Optional. |
Contacts.PhoneInputasChild? | The draft's phone field. Optional. |
Contacts.SaveasChild? | Submits the draft — creating or updating, depending on which draft is open. Disabled until a name is present and while the write is in flight; label follows suit ("Create customer" / "Save changes" / "Saving…"). A successful save closes the draft and refetches the list. |
Contacts.CancelasChild? | Discards the draft. Default label "Cancel". |
Contacts.EditorErrorasChild? | Renders the save failure, and nothing when the last save succeeded. Scoped to the editor — a failed disable never shows up here. |
Pagination
| Part | Description |
|---|---|
Contacts.LoadMoreasChild? | Fetches and appends the next page. Renders nothing once the list is fully loaded. Default label "Load more". |
Loading
Renders while the list fetch is in flight.
| Part | Description |
|---|---|
Contacts.Loading | State wrapper. Render skeleton rows inside. |
Empty
Renders when nothing matches. Read search and showDisabled from useContacts() to tell a search that matched nothing from an empty disabled listing from a directory with no one in it.
| Part | Description |
|---|---|
Contacts.Empty | State wrapper. Render the empty-state message inside. |
Error
Renders on list-fetch error. Write failures surface on EditorError instead.
| Part | Description |
|---|---|
Contacts.Error | State wrapper. Compose the error UI inside. |
Contacts.ErrorMessageasChild?, format? | Renders the error message string. |
Contacts.RetryasChild? | Button that re-runs the list fetch. |
Hooks
| Part | Description |
|---|---|
useContacts() | Hook returning the full context: kind plus its singular/plural labels, contacts, the four state flags, retry, hasMore/loadMore, search/setSearch, showDisabled/setShowDisabled, the draft and creating flag with startCreate/startEdit/cancelEdit/setDraftField/save/canSave/saving/saveError, and setDisabled/pendingDisableId. |
useContactRow() | Hook returning the current contact inside <Rows>. |
useContactRowEditing() | Hook returning whether this row is the one being edited. RowEditing unmounts its children, which can't animate out — this gives the same state as a boolean, for driving an expand transition. |
Search and filters
Search filters on a case-insensitive name substring, server-side. The input stays immediate while the request only re-keys once typing settles (300ms) — otherwise every keystroke is a request, and the Loading gate replaces the list under the cursor.
The active/disabled filter is a switch between two listings, not a superset: the API returns the active contacts or the disabled ones, never both. That's why Status reads the same on every row of a given view, and why the empty disabled listing is worth its own copy.
Both filters are applied to the contacts override too, client-side, so a storybook story or test doesn’t end up showing controls that do nothing.
Disabling contacts
DisableTrigger PATCHes is_disabled, which hides the contact from the default listing and from pickers while leaving existing invoices and bills intact. The same trigger re-enables a disabled contact.
The disable write is deliberately kept apart from the editor's save: it reports progress through pendingDisableId (which only ever names the row that set it), so toggling a row can’t flip an open draft into a saving or failed state.