Show navigation
List Filters & Views
Azure "All resources" parity for the resource list: a filter-pill row, checkbox multi-select with bulk commands, a column chooser, group-by-type, CSV export, and a refresh button — all state deep-linkable via query params.
The workbench is one component, ResourceListView, and every list route renders it — /all, /favorites and /recents differ only by a source prop that supplies a filter preset and a default sort (resource service menu). Everything below is therefore true of all three; /all is simply the source with no preset.
Filters
- Filter-pill row (
ResourceListFilterBar): each active filter is a pill reading what it holds, "Status: all", that opens a panel to change it and is taken off by its own ✕;+ Add filteris a menu of the remaining filters. A pill stays visible while empty ("all") until its ✕ removes it; a deep-linked value surfaces its pill automatically. A pill of one choice closes on the pick; the Type pill takes several and stays open.- Type — multi-select over
ResourceDefinitionMap(icon + title), bound to thetypesrefuseReadResourcesaccepts. - Status — Published/Draft;
isPublished?: booleanonresourceFilterInputSchema, implemented as anexists/notExistsonresource_publicationsinsidegetResourcesWhere(the one filter source for bothreadResourcesCountandreadResources). - Updated — date-range presets (24h / 7d / 30d / custom),
gte/lteonupdatedAt, resolved at fetch time (getResourceUpdatedRange) so relative presets stay anchored to "now". Custom's bounds are one range field (calendar), each end a day read as its start in the reader's zone. - Tag — name + optional value (tags): a value pins the tag through jsonb containment, a name alone matches any value through key-existence.
- Type — multi-select over
- URL state (
useResourceListFilters):search,types,status,sortBy,pagemirror to query params viauseRouteQuery(defaults drop out of the URL);?search=from Home stays the entry point.sortByserializes tokey:order,…. The widths the reader drags the columns to are acolumnWidthsparam beside them, serialized tokey:width,…and read back dropping any entry that names no column or a width no handle reaches. Named saved views are deferred.
Bulk operations
- A checkbox column, the header's selecting the page and showing it mixed; a selection toolbar replaces the filter row while items are selected (
n selected · Delete (n) · Export CSV · Save as blueprint · Clear).useResourceSelectionremembers full rows selected on other pages, since the table's selection model only carries ids. resource.deleteResources: owner-scopedinArraysoft delete returning the deleted rows, stampingdeletedAtand dropping their publication rows — blobs stay put until purge (recycle bin). The selection's Delete sends at once, as a row's does, because the recycle bin undoes it: its toast restores the whole selection in onerestoreResourceswrite (resource page parity).
Views
- Column chooser ("Columns"): a panel of checkboxes over
ResourceHeaders, hidden set persisted toLocalStorageKey.ResourceListHiddenColumns. Two columns are never offered: the name column, which is the row's identity, and the source's pinned column — the one the view is ordered by, since a sort key the reader can hide is an order nobody can explain.Last accessedis pinned on Recent and hidden by default everywhere else. - Group by type: a toolbar toggle that says whether it is on, grouping the page's rows under headers of type icon + title + count, each opening and closing its rows. Groups start open.
- Footer: the page size, "x–y of N", and the previous and next pages.
- Summary view: a toolbar toggle swapping the table for per-type count cards over the same filters — see summary view.
Rows
- Row click, or Enter on a focused row, opens the resource — the one affordance for it, per resource explorer. Every row is a stop in the tab order, so the menu key opens its context menu too.
- Context menu on right-click, long press or the menu key (the one context menu), the same command list as the row's overflow menu: Open in new tab, Copy link, Save as blueprint, Rename, Delete — the rename/delete dialogs are store-driven singletons (
useListDialogStore). A plain Open command is deliberately absent, since clicking the row already does that. - Export CSV: serializes the current filtered result via
getResourcesCsv, re-querying the same filter in page-sized chunks up toMAX_CSV_EXPORT_ROWS— never a single query with the full count as its limit; hitting the cap truncates the export with a warning notification. Bulk-selection export uses the selected rows. - Refresh, Export CSV and Recycle bin sit in the toolbar's overflow menu on every width; the two view toggles, the column chooser and the close ✕ stay out. Refresh re-runs
readResourceswith the last options. - Empty states: filters active → "No resources match your filters" + Clear-filters action; otherwise the source's own empty state; a failed read with no rows → the error state with Try again, announced as an alert. Loading with no rows yet renders skeleton rows; a later read keeps the rows showing and marks the table busy.
Flow
One filter state, mirrored to the URL, consumed by count + list:
flowchart LR
URL["query params<br/>search · types · status · sortBy · page"] <-->|"useRouteQuery ↔ the table's models"| STATE["useResourceListFilters refs"]
PILLS["FilterBar pills"] --> STATE
STATE --> WHERE["getResourcesWhere<br/>(single filter source)"]
WHERE --> RR["resource.readResources"] --> TABLE["UiDataTable"]
WHERE --> CNT["resource.readResourcesCount"] --> FOOTER["footer x–y of N"]
TABLE -->|"select n → Delete (n)"| BULK["resource.deleteResources"] -->|"deletedAt + publications dropped"| GONE[("recycle bin")]
TABLE -->|"Export CSV (chunked)"| CSV["getResourcesCsv"]
Procedures
| Procedure | Auth | Input | Purpose |
|---|---|---|---|
resource.readResources / resource.readResourcesCount | authed | isPublished?, isFavorite?, isAccessed?: boolean, updatedAfter?/updatedBefore?: Date | status, set and date filters via getResourcesWhere |
resource.deleteResources | authed (owner-scoped where) | ids: string[] (unique, bounded) | bulk soft delete — deletedAt + publication rows |
Key files
| File | Role |
|---|---|
app/components/Resource/List/View.vue | the workbench orchestrator: toolbar, pills, selection, table, singletons |
app/services/resource/list/ResourceListSourceDefinitionMap.ts | per-source filter preset, default sort, pinned column, empty state |
app/components/Resource/List/FilterBar.vue | pill row + + Add filter menu |
app/components/Resource/List/Selection/Toolbar.vue | n selected · Delete (n) · Export CSV · Save as blueprint · Clear |
app/composables/resource/useResourceListFilters.ts | URL-synced filter state |
app/composables/resource/useReadResources.ts | filter input, chunked page reader for CSV export |
app/composables/resource/list/useReadResourcesPage.ts | the shared paged reader: stale guard + filter-keyed count |
app/composables/resource/list/useDebouncedFilter.ts | field ↔ filter bridge that debounces typing |
app/composables/resource/useExportResourcesCsv.ts | selected-rows + chunked full export with truncation warning |
server/trpc/routers/resource.ts | filter schema, bulk delete |
server/services/resource/getResourcesWhere.ts | the one filter source every list read and count shares |
Notes
- Publish status stays off the default columns — it appears only as an opt-in filter pill.
- One filter source: every filter lands in
getResourcesWheresoreadResourcesCountandreadResourcescan never disagree. That includes a source's own preset, which is why Favorites and Recent get the pill row, the total and the summary cards without a line of their own. - The
Last accessedcolumn is sortable because the join that produces it is also the sort space:readResourcesselects the resource columns alongsideresourceAccesses.accessedAtonce and hands that same selection toparseSortByToSql, so a column the list can show is a column it can sort by. - All filters funnel through one key (a JSON of the filter state): a change to it sends the list back to page 1, and the list reads whenever that key, the page, its size or the order changes. That is also why every text filter — the search box and a tag pill's name and value — writes through
useDebouncedFilterinstead of per keystroke: a raw binding would reset to page 1 and re-run both queries on every character. - The total is only ever written by a read. A bulk delete drops its rows from the page optimistically, because those rows are what the user is looking at, but the count is the server's number over the whole filter — so the delete re-reads instead of nudging it. Nudging is wrong in both directions: a refresh landing mid-flight has already re-counted, so the adjustment is applied on top of a number that no longer needs it, and the rollback of a rejected delete then adds back rows the fresh count never included.
- The list also reads on a page turn, a page-size change and a sort change, none of which move the total, so the count is keyed to the filter the user picked (
getResourceFilterKey) and reused until that changes (or a mutation refreshes). The key is deliberately built from the selection, not from the input the queries send: a relative Updated preset anchors its boundary to the current time, so a key holding that resolved date would never repeat and every page turn would re-run the count. For the same reasonuseReadResourcesPageresolves the filter input once per read and hands the same one to both queries, so the total and the rows always describe the same window. The list and the recycle bin share that composable, which owns the keying and the latest-wins stale guard that keeps a slower earlier read from overwriting a fresher one.
Scroll to top