entry-list Component
SSR-backed collection data table with URL-driven search, schema-aware smart filters, multi-column sort, card mode, keyboard grid navigation, and connection-aware preloading.
On this page
File: src/components/collection-display/entry-list.svelte
Primary table for collection view mode. Data comes from +page.server.ts props; the component does not perform client-side collection fetching.
Platform: Built on the unified Smart Table (
createSmartTableserver mode) + Collection Filtering. Design-system tables share the same headless controller, multi-column sorting, and pagination primitive.
Architecture
Features (as implemented)
| Feature | Implementation |
|---|---|
| Server-driven data | entries and pagination props from SSR; tableData = $derived(serverEntries) |
| URL state | search, page, pageSize, filter_{field}, sort fields via goto() |
| Debounced search | globalSearchValue synced to URL; resets to page 1 on new search |
| Smart column filters | createSmartFilter maps widgets → text/select/date/numberRange/boolean; URL keys filter_{name} |
| Advanced filters modal | AdvancedFilterModal compound AND/OR multi-clause query builder across all schema fields with type-aware operators |
| Active filter badges | Dismissible chips + clear-all + advanced count badge; aria-live count for accessibility |
| Status badges | System status column + <Status> badge (draft, publish, unpublish, schedule, archive) unless the schema declares its own status field |
| Status facets | statusFacets from getStatusFacets → SmartTableStatusFacets chips |
| Saved views | SmartTableSavedViewsMenu — named filter/sort/layout presets per collection |
| Column resize | Header drag handles via ColumnResizeHandle + layoutKey: entry-list:{id} |
| List metrics | SSR listMetrics + SmartTableMetricsBadge when ?debug=table |
| Multi-column sort | Shift+Click header sort; ordinal rank badges (1, 2, 3); URL query serialization sort=col1:asc,col2:desc |
| Responsive card mode | View mode toggle (table vs card) in toolbar; persisted in layout prefs; adaptive mobile card presentation |
| Keyboard grid traversal | WCAG 2.2 AA / ATAG 2.0 roving tabindex; arrow navigation, Home/End, PageUp/PageDown, Space for row selection, Enter to edit |
| Export filtered data | Alt+X keyboard shortcut and MultiButton action for streaming CSV export bounded to selected entries or active filter query. Formula-leading text cells are prefixed. |
| Pagination | TablePagination; page sizes via rowsPerPage URL param |
| Multi-select | Shift+click range selection; select-all checkbox; ID-based virtualization-safe selection |
| Bulk actions | POST /api/collections/:id/batch (delete, status, clone, bulkEdit) via entry-list-multi-button; CSV export is client-side (Alt+X) |
| Archive semantics | showDeleted changes delete behavior (archive vs permanent) and the toolbar icon; it does not filter the list |
| Column manager | Show/hide columns; drag-and-drop reorder (@thisux/sveltednd) |
| Density | normal / compact / comfortable via TableFilter → entryListPaginationSettings.density |
| localStorage | Per-collection entryListPaginationSettings_{collectionId} + Smart Table layout prefs (viewMode, density, columnWidths) |
| Plugin columns | availablePlugins → dynamic headers → PluginComponent cells |
| Translation UI | Embeds translation-status.svelte in filter toolbar |
| Hover preload | preloadEntry() from @utils/navigation on row hover |
| Idle warm-cache | First 5 visible rows → POST /api/collections/warm-cache (one ID per request) |
| Connection-aware | Disables preload on slow-2g / 2g / saveData (Network Information API) |
| Row virtualization | On pages with >25 rows, only visible rows + buffer (5 above/below) render as DOM elements; spacers maintain scroll height |
URL Parameters
| Param | Purpose |
|---|---|
search |
Global text search (server-side) |
page |
Current page number |
pageSize |
Rows per page |
filter_{fieldName} |
Per-column filter value (from createSmartFilter.toURLParams()) |
| Sort fields | Written by onSortChange (see entryListPaginationSettings.sorting) |
Smart Filters (createSmartFilter)
Files:
src/components/collection-display/create-smart-filter.svelte.ts— reactive factory + pure helperssrc/components/collection-display/smart-filter-row.svelte— type-aware filter row + badges
Filter control types are derived from collection schema widgets (database-agnostic — no adapter imports):
| Control type | When used | UI control |
|---|---|---|
text |
Default scalar fields | Floating search input |
select |
status, Select/Radio widgets |
Native select with options |
date |
Date widgets, createdAt / updatedAt |
<input type="date"> |
numberRange |
Number / Price / Currency / Rating | Min–max inputs (min:max in URL) |
boolean |
Checkbox / toggle widgets | Yes / No select |
Unsafe widgets (media, rich text, relations, groups, etc.) are excluded via safeForFiltering: false.
Client API
const smartFilter = createSmartFilter(() => collection.value);
smartFilter.setFilter("status", "publish");
smartFilter.clearFilter("status");
smartFilter.clearAll();
// URL deep-link params: { filter_status: "publish", filter_title: null, ... }
updateURL({ ...smartFilter.toURLParams(), page: "1" });
// Adapter-agnostic server payload (contains operator — adapters interpret)
smartFilter.toFilterQuery(); // { status: { contains: "publish" } }
Security & architecture (must stay enforced server-side)
| Concern | Responsibility |
|---|---|
| Query safety | parseCollectionListQuery + whitelistFilterParams drop unknown field names before the DB. Client allowedFieldIds is UX-only. |
| RBAC / tenant | Auth + permission hooks; CollectionService injects tenantId when multi-tenant is enabled. |
| Caching (L1/L2 SWR) | getOrSetSWR with key collection:{id}:query:{hash}:… (60s fresh / 5m stale). Empty pages are not Bloom-negative-cached. |
| Prefix invalidation | Mutations → cacheService.invalidateCollection(id) clears all filter/search/page variants. |
| Server hooks | Authentication and authorization run before the collection page loader. |
Number ranges are stored as adapter-agnostic strings (min:max). Today the page loader maps every filter_* to { contains: value }. Backend operators (gte/lte) can be added later without changing the frontend encoding.
Shared modules
| Module | Role |
|---|---|
@utils/collection-query-filters |
URL parse, schema whitelist, stable queryHash, cache key builder |
create-smart-filter.svelte.ts |
Reactive filter UI state + control types from schema widgets |
system/table/table-filter.svelte |
Toolbar (search, density, filter/column toggles) |
ui/table/filter.svelte |
Generic table toolbar primitive (design system) |
@components/ui/smart-table |
Shared SmartTable component suite (selection, sorting, layout prefs) |
Architecture docs
- Collection Filtering Platform — core capability (schema, FLAC, QueryBuilder, cache)
- Cache System — L1/L2, getOrSetSWR, prefix invalidation, negative caching rules
- Content API — list query params —
search/filter_*/ global search vs column filters - Data Operations — import/sync/apply must invalidate collection list caches
Props
interface EntryListProps {
entries: Entry[];
pagination: {
currentPage: number;
pageSize: number;
totalItems: number;
pagesCount: number;
};
contentLanguage: string;
breadcrumb?: Array<{ name: string; path: string }>;
collectionStats?: {
_id: string;
name: string;
count: number;
lastModified: string;
} | null;
}
Keyboard Shortcuts & Grid Navigation
Shortcuts and accessible keyboard grid navigation follow WCAG 2.2 AA and ATAG 2.0 standards:
Global Action Shortcuts
| Shortcut | Action | Context |
|---|---|---|
Alt+N |
Create new entry | Global |
Alt+P |
Publish selected | Requires selected entries |
Alt+U |
Unpublish selected | Requires selected entries |
Alt+D |
Move selected to draft | Requires selected entries |
Alt+E |
Bulk edit selected fields | Opens BulkFieldEditModal |
Alt+X |
Export Filtered Data | Streamed CSV download to disk |
Alt+Del |
Delete / archive selected | Requires confirmation modal |
Accessible Grid Traversal (Roving Tabindex)
When focused on the table body or card view container:
| Key | Action |
|---|---|
↓ / ↑ |
Traverse active row focus down / up with dynamic focus ring |
Home / End |
Jump focus directly to the first / last row on the page |
PageDown |
Leap forward by 10 rows |
PageUp |
Leap backward by 10 rows |
Space |
Toggle multi-select checkbox for currently focused row |
Enter |
Navigate directly to edit mode (reflectModeInURL('edit', id)) for the focused row |
Multi-Column Sort & Ordinal Ranks
Clicking a table header toggles directional sort (asc → desc → none). Holding Shift + Click composes multi-column sorting:
- Ordinal Indicators: Badges (
1,2,3) appear adjacent to sort direction chevrons to indicate priority order. - Server Serialization: Multi-sort queries serialize to URL parameter
sort=field1:asc,field2:descwhich the SSR loader consumes. - Client Cascading: In client mode, secondary and tertiary comparators break ties sequentially.
Responsive Card Mode
The table toolbar includes a View Mode switcher allowing seamless toggling between Table View and Responsive Card Grid View:
- Card Presentation: Each card renders the selection checkbox, title/ID, status badge, top visible schema fields, relative timestamps, and quick edit action.
- Persistence: Saved per-collection in
SmartTableLayoutPrefs(layoutKey: entry-list:{collectionId}). - Mobile Viewports: Optimized for touch targets (≥40px) and auto-collapses dense multi-column headers on small viewports (<768px).
Advanced Filter Operators Modal
In addition to inline scalar column filters, clicking Advanced Filters opens AdvancedFilterModal:
- Compound Logic: Select between
ALL conditions (AND)andANY condition (OR). - Field-Specific Operators: Text fields support
equals,contains,starts_with,ends_with,is_empty, andis_not_empty. Numeric/date fields support=,≠,>,≥,<,≤. - Badge Indicators: The active filter count badge reflects total applied compound clauses.
Row Virtualization
When the page size exceeds 25 rows (useRowVirtualization = $derived(pageSize > 25)), the component activates row virtualization to avoid rendering all rows into the DOM:
- Row height: 44px per row (defined in
src/utils/table-constants.ts) - Buffer: 5 rows rendered above and below the visible viewport to prevent blank flashes during scroll
- Scroll container:
max-h-[calc(100dvh)]withoverflow-auto; scroll position tracked viaonscrollhandler - Index mapping:
{@const realIndex = virtualStartIndex + idx}preserves selection state across virtual/non-virtual modes content-visibility: auto: Applied to virtualized rows for additional browser-level rendering optimization
Key state variables:
| Variable | Type | Purpose |
|---|---|---|
virtualScrollTop |
$state |
Current scroll position in pixels |
containerHeight |
$state |
Visible height of the scroll container |
virtualStartIndex |
$derived |
First visible row index (minus buffer) |
virtualEndIndex |
$derived |
Last visible row index (plus buffer) |
visibleRows |
$derived |
tableData.slice(virtualStartIndex, virtualEndIndex) — rows to render |
spacerTop / spacerBottom |
$derived |
Padding rows above/below the visible window |
Virtualization is opt-in — it only activates when pageSize > 25. Below that threshold, the table renders all rows directly with no overhead from scroll tracking.
Draft, Status & Scheduled Publishing
The list always renders a system status column unless the collection schema already declares a field named status. The row badge maps the stored values as follows:
| Stored value | Label | Badge presentation |
|---|---|---|
draft |
Draft | outline + pencil |
publish |
Published | primary + thumbs-up |
unpublish |
Unpublished | warning + pause |
schedule |
Scheduled | error + clock |
archive |
Archived | surface + archive |
clone |
Clone | secondary + clipboard |
delete |
Delete | surface + trash |
The schedule timestamp _scheduled lives inside the entry data payload and is not shown in the list.
Changing status from the list
- Clicking the status cell proposes the next status and asks for confirmation:
draft → publish,publish → unpublish,unpublish → publish,schedule → publish, any other value →publish. - The update is a single-entry
PATCH /api/collections/:id/:entryId/statuswith{ status }. Publishing is gated server-side by required-field validation and the workflow publication gate. - Bulk Published / Unpublished / Draft actions and
Alt+P/Alt+U/Alt+Dsend onePOST /api/collections/:id/batchwithaction: "status". Bulk status changes do not ask for confirmation. - Collection mutations require
collections:write; administrators bypass endpoint permission checks.
Status facets
- Facet counts are streamed from the server and are tenant-wide. They are not narrowed by the current search, column filters, or advanced filters.
- Counts cover
publish,draft,unpublish,schedule, andarchiveonly. Zero-count chips are hidden unless they are the active filter. - Selecting a chip writes
filter_statusand opens the filter row. - The legacy importer value
"published"is not counted by thePublishedfacet or filter — only"publish"is.
Scheduled publishing
The date/time/action picker is opened from the single-entry editor (header-edit on mobile, right-sidebar on desktop), not from the list. See fields for the full workflow.
The list’s Schedule action is not a scheduler. It immediately sends the current timestamp with status: draft through the batch status endpoint, opens no modal, and creates no status-transition job. Entries handled this way are not published later.
Known limitations of the list surface:
- Bulk scheduling has no future-date picker.
- Cancelling a schedule is only available from the desktop sidebar and does not delete the already-created job.
- Re-saving a scheduled entry creates another job; jobs are not deduplicated.
- Scheduled unpublish/delete selections are not persisted — the job always targets
publish.
Autosave interaction
Leaving the editor with unsaved changes runs the navigation-guard autosave. Existing entries keep their loaded status (and schedule timestamp); only new entries are saved as draft. If the edited payload fails the publish gate, the autosave is rejected and navigation stays on the form. See fields.
Archive toggle
showDeleted does not switch the list to archived entries. It only changes delete semantics (archive vs permanent delete) and the toolbar icon; the SSR loader ignores it.
Plugin Columns
Plugins register ui.columns in their definition. entry-list adds headers and renders cells with:
<PluginComponent
pluginId={...}
componentName={header.component}
{...mapPluginProps(header.props, entry)}
/>
Zone for list-level plugin UI: dashboard slot list_actions (see plugin docs).
Related Documentation
- MultiButton
- translation-status
- fields
- Collection page load tiers
- Cache System — L1/L2 SWR +
collection:{id}:query:{hash} - Content API query params —
search/filter_*/ global search - Data Operations — invalidate list caches after import/sync
- Unit tests:
tests/unit/components/create-smart-filter.test.ts,tests/unit/utils/collection-query-filters.test.ts