# Entity list table


`EntityListTable` is the generic, reusable component every module uses to
render entity lists. It is generic over a row type `TRow extends
Record<string, unknown>` and lives in
`src/lib/components/entity-list-table/`.

**Route pages do not wire `EntityListTable` directly** — they render
`EntityTablePage` (see [Route-level usage](#route-level-usage-entitytablepage)
below), which owns data/state via `useEntityList` and feeds this component.
Use `EntityListTable` directly only in composite hosts (multiple list
instances, extra UI).

## Component overview

<Mermaid chart={`flowchart TB
  ELT["EntityListTable<TRow>"]
  ELT --> Toolbar
  ELT --> Views
  ELT --> Dialogs
  ELT --> Panels
  Toolbar --> SearchBar
  Toolbar --> FilterBar
  Toolbar --> BulkActions
  Toolbar --> ViewModeToggle
  Views --> TableView["EntityListTableTableView"]
  Views --> CardView["EntityListTableCardView"]
  Dialogs --> Delete["Delete / Restore / Duplicate"]
  Dialogs --> Bulk["BulkDelete / BulkRestore"]
  Dialogs --> Export["Export / HtmlExport"]
  Panels --> Columns["Columns panel"]
  Panels --> Filters["Filters panel"]
  Panels --> SearchIn["SearchIn panel"]
  Panels --> Preview["Preview panel"]
  Panels --> Versions["Version history panel"]
`} />

The component is **controlled**: the parent route owns the data and most
state (rows, page, search, sort, filters, selection) and passes them in as
props with `on*Change` callbacks. A few props (`selectedKeys`,
`filtersOpen`, `deletionFilterMode`, `datetimeIanaModeByKey`) are bindable
for two-way sync.

## Key props

| Group | Props |
|-------|-------|
| Identity | `uid`, `entity`, `translationKey`, `columnOrderStorageKey` |
| Columns | `columns` — the full root-level meta array via `orderedColumns(meta.columns)` |
| Data | `rows`, `total`, `metaLoading`, `rowsLoading`, `error` |
| Pagination | `page`, `pageSize`, `pageSizeOptions`, `onPageChange`, `onPageSizeChange` |
| Search | `search`, `onSearchInput`, `searchInKeys`, `onSearchInKeysChange` |
| Sorting | `sortKey`, `sortDir`, `onSortChange`, `defaultSort` |
| View | `defaultView` (`meta.table.default_view`) |
| Column visibility | `visibleKeys`, `onVisibleKeysChange`, `onResetColumnVisibility` |
| Selection | `selectedKeys` (bindable), `onSelectedKeysChange`, `rowSelectionEnabled` |
| Actions | `onRefresh`, `rowActions` (snippet), `entityActions` (`meta.actions`), `entityCustomActions` (`meta.table.row_custom_actions`), `customActionHandlers`, `onCreateAction`, `onEditAction` |
| Filters | `filtersOpen` (bindable), `filterValues`, `onFilterValuesChange`, `advancedFilters`, `onAdvancedFiltersChange` |
| Deletion filter | `deletionFilterMode` (bindable), `onDeletionFilterModeChange` |
| Customization | `cell` (snippet), `metaLoadingView`, `rowsLoadingView`, `emptyView`, `errorView` |

Columns arrive as the single root-level `columns` array from the entity
meta. Sticky/audited groups are derived internally from the `sticky` and
`audited` column flags — there are no parallel group arrays.

## Composable architecture

Behavior is split into composables under `composables/`, each following
the composable state exposure pattern (read-only `state` getter, mutator
functions, individual `$derived` getters).

| Composable | Responsibility |
|------------|----------------|
| `useColumnOrder` | Column ordering within sticky/data/auditing groups; persists to sessionStorage |
| `useViewMode` | `table` / `cards` / `cards_list` mode; persists to sessionStorage |
| `useDeletionFilter` | `non_deleted` / `deleted` / `all` mode; persists to sessionStorage |
| `useToolbarMode` | Auto-switches toolbar between `filters` and `bulk` modes |
| `useClientSelection` | Selected-only view with client-side pagination |
| `useRowRangeSelection` | Mouse-drag brush selection across rows |
| `useKeyboardNavigation` | Arrow keys, space, enter, escape shortcuts |
| `useStickyColumns` | Sticky column positioning |
| `useScrollPreservation` | Preserves scroll position across re-renders |
| `useSheetPanels` | Orchestrates columns/filters/search-in panels (wraps `useSheetPanelManagement`) |
| `useDialogs` | Centralized dialog open/close state |
| `useBulkActions` | Bulk delete/restore/duplicate; dispatch on `meta.actions[].endpoint` with `/api/v1/entities/{entity}` fallback |
| `useRowActions` | Single-row edit/preview/delete/restore/duplicate; declared-endpoint dispatch + purge detection |
| `useExport` | XLSX/CSV export with HTML preview via the declared `export` endpoint |
| `usePreviewPanel` | Preview row, inline edit mode, prev/next navigation |

List data/state (meta fetch, rows, search/sort/page/filter state,
persistence, sync) is NOT a composable of this component — it is owned by
`useEntityList`, consumed directly by `EntityTablePage` or by composite
hosts.

## View modes

Three view modes: `table`, `cards`, `cards_list`. The initial mode comes
from `meta.table.default_view` via the `defaultView` prop; the active mode
is stored in sessionStorage under `{columnOrderStorageKey}:viewMode`
(fallback `pb.entityList:{uid}:viewMode`). The toolbar's `ViewModeToggle`
switches between them. Rendering is handled by `EntityListTableTableView`
and `EntityListTableCardView`.

## Selection model

Two complementary models:

- **Server selection (default)** — `selectedKeys` is owned by the parent
  and persists across page navigation. Used for server-side bulk operations
  and export.
- **Client selection (selected-only view)** — `useClientSelection`
  maintains a `Map<string, TRow>` of selected rows and provides client-side
  pagination for the selected-only view. It automatically exits this view
  on server reload, when selection empties, or when row selection is
  disabled.

Selection interactions: row checkboxes, header select-all, mouse-drag range
selection (`useRowRangeSelection`), and keyboard space to toggle the
focused row. The toolbar auto-switches to bulk mode when items are
selected.

## Dialogs

All dialogs are centralized through `useDialogs` and rendered as separate
Svelte components in `dialogs/`:

| Dialog | Action |
|--------|--------|
| `DeleteDialog` | Single-row soft delete |
| `RestoreDialog` | Single-row restore |
| `DuplicateDialog` | Single or bulk duplicate (50-item limit for bulk) |
| `BulkDeleteDialog` | Bulk soft delete |
| `BulkRestoreDialog` | Bulk restore |
| `ExportDialog` | Export format (XLSX/CSV) and scope (selected/all) |
| `ExportPreviewDialog` | HTML export preview (PDF/email modes) |
| `HtmlExportDialog` | HTML export confirmation |

On success, dialogs refresh the list, clear selection, close, and switch
the toolbar back to filters mode. Errors go through `pushNotification`.

## Sheet panels

Panels are mounted via the global `SheetHost` (see
[App shell](/frontend/dev/app-shell#global-side-sheets)) and orchestrated
by `useSheetPanels`:

| Panel ID | Component | Purpose |
|----------|-----------|---------|
| `entity.columns` | `ColumnsPanel` | Column visibility checkboxes + drag-and-drop reordering within groups |
| `entity.filters` | `FiltersPanel` | Basic filters + advanced filter builder with operators (`=`, `!=`, `>`, `<`, `>=`, `<=`, `contains`, `startsWith`, `endsWith`, `BETWEEN`) and AND/OR connector |
| `entity.searchIn` | `SearchInPanel` | Toggles which columns are included in search |
| preview | `PreviewPanel` | Full row preview with inline editing and prev/next navigation |
| version history | `VersionHistoryPanel` | Audit trail from `/api/v1/entities/{entity}/{uuid}/audit` with a timeline UI |

## Column types

`MetaColumn` defines a column — the root-level entity field dictionary
shared by table, form, and preview pages. All fields are snake_case:

| Field | Purpose |
|-------|---------|
| `key` | Column identifier (row field key) |
| `label_key` | i18n key for the header/form label |
| `type` | `text`, `badge`, `date`, `datetime`, `color`, `boolean`, `number` |
| `order` | Required `0..N` position within its group |
| `sticky` | Pinned to the left (typically `meta.display_field`) |
| `audited` | System audit column — rendered last |
| `sortable`, `searchable`, `filterable`, `hideable` | Capability flags |
| `default_visible` | `false` = selectable but hidden by default (e.g. `uuid`) |
| `badge` | Per-value `{ label_key, label_text, color }` mapping |
| `datetime_iana_toggle` | `{ record_iana_field }` for IANA timezone toggle |
| `tooltip`, `tooltip_priority`, `tooltip_title` | Tooltip configuration |
| `show_form_tooltip`, `show_list_tooltip` | Tooltip visibility per context |

Utility functions in `src/lib/entity-list/types.ts`:
`orderedColumns()` (sticky → normal → audited, each by `order`),
`defaultVisibleColumnKeys()`, `sanitizeVisibleKeys()`,
`searchableTextColumns()`, `getOperatorsForColumnType()`.

The default "all fields" search covers **visible ∩ searchable ∩ `text`
columns**: hidden-by-default columns (e.g. `uuid`) are excluded until the
user shows them or picks them explicitly in the search-scope selector.

## Route-level usage — EntityTablePage

`EntityTablePage` (`src/lib/components/entity-table-page/`) is the
route-level host for the table archetype. A page is ~50 lines: config props
+ snippets, no data logic.

```svelte
<script lang="ts">
  import EntityTablePage from '$lib/components/entity-table-page/EntityTablePage.svelte';
  import AppPageBreadcrumb from '$lib/components/AppPageBreadcrumb.svelte';

  type MyRow = { uuid: string } & Record<string, unknown>;
</script>

<EntityTablePage
  basePath="/api/v1/entities/my_entity"
  storageKeyPrefix="pb:my-entity:list:"
  syncChannel="primebrick_my_entity_sync"
  errorScopeKey="app.common.errors.scope.myEntitiesList"
  navStrategy="goto"
  createPath="/system/my-entity/create"
  editPath="/system/my-entity/:uuid"
>
  {#snippet breadcrumb()}
    <AppPageBreadcrumb segments={[{ label: 'My entities' }]} />
  {/snippet}
</EntityTablePage>
```

| Group | Props |
|-------|-------|
| Data | `basePath` (entity API base; `/ws/<svc>/api/v1/entities/…` for module entities), `fixedFilters`, `onRowsLoaded` |
| List behavior | `deletionFilter={false}` (no `deleted_records` param — e.g. `role_mapping`), `searchGate`, `searchDebounceMs` |
| Persistence | `storageKeyPrefix`, `persist` (`'session'` default) + `legacyPersist` (read-only legacy medium, `'local'` default) |
| Errors | `errorScopeKey`, `errorScopeKeyInit` |
| Sync | `syncChannel` — legacy BroadcastChannel name (receiver mode; child form pages post `refresh`) |
| Navigation | `navStrategy` (`'goto'` default or `'popup'` — new tab + focus), `createPath`, `editPath` (`:uuid` placeholder), `onCreateAction`/`onEditAction` overrides |
| Create CTA | Derived automatically: `EntityTablePage` matches the route census `kind:"create"` route for the current entity and forwards `createPath` + `create_cta` (`label_key`/`icon` from the census route) to the toolbar `entity-create-cta`. Pass `createPath` only to override the census-derived route. |
| Row actions | `customActionHandlers` keyed by `meta.table.row_custom_actions[].action_name`, `rowActions={false}` hides the column |
| Selection labels | `selectionLabelKey` / `selectionLabelSingularKey` / `selectionLabelText` / `selectionLabelSingularText` |
| Snippets | `breadcrumb`, `headerExtras`, `headerActions(meta)` (non-entity page CTAs — entity create belongs to the census-derived toolbar CTA), `cell`, `extraDialogs(uid)`, `metaLoadingView`, `rowsLoadingView`, `emptyView`, `errorView` |

Canonical examples: `system/settings/users` (popup nav + `extraDialogs`),
`system/settings/roles` (`deletionFilter={false}`), `crm/customers`
(census-derived create CTA).

## Persistence

User preferences are persisted to **sessionStorage** (cleared when the tab
closes) — column order, view mode, search scope keys, sort, filter values,
advanced filters, deletion filter mode, and datetime IANA toggle mode.
`useEntityList` derives keys from `storageKeyPrefix` (e.g.
`pb:users:list:sort`); `EntityListTable` derives its internals from
`columnOrderStorageKey`. Legacy localStorage keys are read once as fallback
then orphaned — never rewritten.

## Next steps

- [Component catalog](/frontend/dev/components) — every custom/wrapped component
- [System settings](/frontend/dev/settings) — where `EntityListTable` is used for the users list
- [UI components](/frontend/dev/ui-components) — custom components on top of Shadcn-Svelte™
- [API reference](/frontend/dev/api-reference) — full prop tables for every component
