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
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
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) —
selectedKeysis owned by the parent and persists across page navigation. Used for server-side bulk operations and export. - Client selection (selected-only view) —
useClientSelectionmaintains aMap<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) 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.
Code
| 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 — every custom/wrapped component
- System settings — where
EntityListTableis used for the users list - UI components — custom components on top of Shadcn-Svelte™
- API reference — full prop tables for every component