Filters and tabs
Quick filters, link tabs, the filter builder, search and sorting of a table view.
A TableView narrows its rows with quick filters, tabs, the filter builder and the search box, and orders them with its sort.
Quick filters
quickFilters draws one dropdown button per entry in the toolbar, listing the values of a filterable column: a select's items, a boolean's two labels, a tags column's suggestions, or the rows a relation points to (read from the select endpoint its data type's selectSource names, by its label and value keys unless keyMapping maps them, as its picker reads them). Picking a value sets the column's filter, the same one the filters row edits, with the mode array_contains_string for a multiple select or relation and for a type whose default compare mode is array_contains_string (tags), is otherwise, or the declared mode ("is" | "is_not" | "include" | "exclude" | "array_contains_string"); "All" (allLabel) clears it. Being a column filter, the value is kept with the table's state, counted as an active filter, shown as a chip in the filters row and cleared by "Clear filters". A relation filter's chip names the rows it keeps ("Acme"), read once from the relation's select endpoint as its picker does, rather than their ids; an id the endpoint does not answer shows as is. In the compact layout, which has no filters row, the button names the column and its value ("Role: Admin") next to a clear button. Registration throws for an unknown or non-filterable column.
A quick filter needs values to list, so only these columns can carry one: a SelectType with items, a BooleanType, a StatusType, and a relation (RelationType, CascaderRelationType), or a custom type whose input declares items or a searchUrl. A free-text column (a StringType, a number, a date, a select without items) has nothing to pick from: the toolbar leaves its quick filter out, and registration logs a warning naming the table and the column. Filter such a column from the filters row or the search, or declare it as a select.
Link tabs
A tab can open another page instead of filtering (it then takes no filter), for sibling lists such as Members and Invitations. Give it to (a page controller, whose URL and permission it follows, or a dashboard path) and countFrom (a data controller, or its location) for its counter. A tab with the id "all" stands in for the implicit "all" tab, at its own position, with its own label and icon.
Any tab, filter or link, can be gated like a custom button: permission names an action of this table ("list" → <table permission id>.list) or is an Action of any component; permissionId is an absolute permission id ("settings.workspace.invites"), the usual gate of a link tab to a path, and wins over permission. A link tab to a page controller follows that page's own permission without either.
tabs: [
{ id: "all", label: "$page.members.tabs.members", icon: "i-ph-users", navBadge: true },
{ id: "invites", label: "$page.members.tabs.invites", icon: "i-ph-envelope-simple",
to: "/settings/workspace/invites", permissionId: "settings.workspace.invites",
countFrom: "/api/tables/admin-invites" },
],
A link tab the caller may not open is left out when the layout is served; one whose list the caller may not read shows no counter.
navBadge: true publishes the tab's counter as the navigation badge of the page it stands for: this table's page for a filter tab, the linked page for a link tab, which then needs a page controller to and a data controller countFrom (a path or a location logs a warning and publishes nothing). The server counts it on each page visit, with the caller's own list permission on that table and only for the pages the caller can open, and the badge shows in the main navigation and the settings one. A count of zero shows no badge, and a page's static MenuOptions.badge wins over it. While the table is on screen, it keeps the badge fresh with the tab's count over the whole page: the tab's counter itself while nothing narrows it, else a count of the tab's filter alone, sent in the same request, without the view, the user's filters, the search or the URL filters. TableView.fromSource tables cannot publish one.
The badge is a neutral grey pill. To draw it in a tone, give navBadge an object instead of true, as for a tab of rows to check:
{ id: "pending", label: "$orders.tabs.pending", filter: { accessorKey: "status", value: "pending", mode: "is" },
navBadge: { tone: "warning" } },
The tones are the dashboard's, drawn with the status pills' colours. When several counts land on one page they add up, and the badge takes the strongest tone, in this order: error, warning, primary, secondary, success, info, neutral.
Filtering, search, and sorting
Filtering, search, and sorting need no page-side configuration. They come from the controller's decorators (Data controller): a filterable column gets a filter offering its DataType's operators, @Searchable fields feed the search box, and @Sortable columns get sortable headers.
While a search, a filter or a quick filter narrows the rows, the filters row and the "No results" empty state offer one clear action, named after what it clears ("Clear filters", "Clear search" or "Clear filters and search"). It drops the added filters, puts the defaultFilters back to their value, empties the search and the quick filters, and returns to page 1; sort, page size, tab, display, group-by and the archive view stay as they are. A chip's × removes that filter only.
What the page declares are pre-applied filters, in two forms. defaultFilters applies filters when the table opens, visible in the toolbar. tabs adds a tab strip where each tab applies its own filter, on one column; an implicit "all" tab is added automatically.
A tab pairs an id and a label (plus optional icon, iconColor, textColor) with its filter, on a single column: an accessorKey, a mode, and usually a value (several values of a list joined with commas). A tab filters or links (to, see Link tabs): registration throws for a tab giving both. The tabs array below sits inside the TableView(controller, { ... }) options object:
tabs: [
{ id: "pending", label: "Pending", icon: "i-ph-clock",
filter: { accessorKey: "status", value: "pending", mode: "is" } },
{ id: "high", label: "High priority", icon: "i-ph-fire", iconColor: "error",
filter: { accessorKey: "priority", value: "high", mode: "is" } },
],
Each tab shows a row counter, and filter tabs with counters require the controller to mount countBatch: TableViewRoutes.CountBatch (GET <location>/count/batch). TableView() logs a warning at registration when a table declares tabs without it. Each repeated query value holds an escaped counter id, ?, and its list query string. A request accepts up to 50 entries and 8,192 UTF-8 bytes in the encoded path and search. The client packs larger batches into several bounded GET requests, and a single oversized query fails with 414.
A tab's counter is the number of rows a click on it would list, the rest of the table's state kept: the open view's filters (as the user edited them), the filters the user picks, the search, the page's queryParamFilters and routeParamFilters, and the tab's own filter. With a view "Mine" open, the tabs read what each of them lists under it, and a tab whose filter no row of the view passes reads 0. The counters follow the table: changing the view, the filters or the search counts again, in one count/batch request once the change settles (picks made in a row are counted once), and so do realtime events and refreshes. Sorting or paging asks nothing; switching tabs counts the views again. A link tab still counts its whole countFrom list.