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 filter is 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 the types ref useReadResources accepts.
    • Status — Published/Draft; isPublished?: boolean on resourceFilterInputSchema, implemented as an exists/notExists on resource_publications inside getResourcesWhere (the one filter source for both readResourcesCount and readResources).
    • Updated — date-range presets (24h / 7d / 30d / custom), gte/lte on updatedAt, 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.
  • URL state (useResourceListFilters): search, types, status, sortBy, page mirror to query params via useRouteQuery (defaults drop out of the URL); ?search= from Home stays the entry point. sortBy serializes to key:order,…. The widths the reader drags the columns to are a columnWidths param beside them, serialized to key: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). useResourceSelection remembers full rows selected on other pages, since the table's selection model only carries ids.
  • resource.deleteResources: owner-scoped inArray soft delete returning the deleted rows, stamping deletedAt and 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 one restoreResources write (resource page parity).

Views

  • Column chooser ("Columns"): a panel of checkboxes over ResourceHeaders, hidden set persisted to LocalStorageKey.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 accessed is 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 to MAX_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 readResources with 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

ProcedureAuthInputPurpose
resource.readResources / resource.readResourcesCountauthedisPublished?, isFavorite?, isAccessed?: boolean, updatedAfter?/updatedBefore?: Datestatus, set and date filters via getResourcesWhere
resource.deleteResourcesauthed (owner-scoped where)ids: string[] (unique, bounded)bulk soft delete — deletedAt + publication rows

Key files

FileRole
app/components/Resource/List/View.vuethe workbench orchestrator: toolbar, pills, selection, table, singletons
app/services/resource/list/ResourceListSourceDefinitionMap.tsper-source filter preset, default sort, pinned column, empty state
app/components/Resource/List/FilterBar.vuepill row + + Add filter menu
app/components/Resource/List/Selection/Toolbar.vuen selected · Delete (n) · Export CSV · Save as blueprint · Clear
app/composables/resource/useResourceListFilters.tsURL-synced filter state
app/composables/resource/useReadResources.tsfilter input, chunked page reader for CSV export
app/composables/resource/list/useReadResourcesPage.tsthe shared paged reader: stale guard + filter-keyed count
app/composables/resource/list/useDebouncedFilter.tsfield ↔ filter bridge that debounces typing
app/composables/resource/useExportResourcesCsv.tsselected-rows + chunked full export with truncation warning
server/trpc/routers/resource.tsfilter schema, bulk delete
server/services/resource/getResourcesWhere.tsthe 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 getResourcesWhere so readResourcesCount and readResources can 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 accessed column is sortable because the join that produces it is also the sort space: readResources selects the resource columns alongside resourceAccesses.accessedAt once and hands that same selection to parseSortByToSql, 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 useDebouncedFilter instead 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 reason useReadResourcesPage resolves 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.

Details

Command palette

Keyboard shortcuts