@aglyn/shared-ui-jsx 1.0.0-beta.200 → 1.0.0-beta.201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/lib/components/list-table.component.js +13 -0
- package/src/lib/components/list-table.component.js.map +1 -1
- package/src/lib/const/list-filter-sentence.d.ts +1 -0
- package/src/lib/const/list-filter-sentence.js +18 -2
- package/src/lib/const/list-filter-sentence.js.map +1 -1
- package/src/lib/const/list-filter.d.ts +1 -2
- package/src/lib/const/list-filter.js +3 -70
- package/src/lib/const/list-filter.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/shared-ui-jsx",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.201",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-ui-theme": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-util-vendor": "1.0.0-beta.
|
|
28
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.201",
|
|
29
|
+
"@aglyn/shared-ui-theme": "1.0.0-beta.201",
|
|
30
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.201",
|
|
31
|
+
"@aglyn/shared-util-vendor": "1.0.0-beta.201",
|
|
32
32
|
"@mdi/js": "^7.4.47",
|
|
33
33
|
"@swc/helpers": "0.5.23",
|
|
34
34
|
"change-case": "^5.4.4",
|
|
@@ -304,6 +304,19 @@ const NO_ROWS = [];
|
|
|
304
304
|
onOpen(String(id), row);
|
|
305
305
|
} : undefined,
|
|
306
306
|
/*
|
|
307
|
+
* ENTER OPENS IT TOO. The grid moves focus cell by cell with the arrow
|
|
308
|
+
* keys, and a row a mouse can open must open from the keyboard as
|
|
309
|
+
* well. Only when the CELL itself holds focus: Enter on a link or a
|
|
310
|
+
* button inside one is that control's, and neither the actions cell
|
|
311
|
+
* nor the grid's own columns (the selection checkbox) open the record.
|
|
312
|
+
*/ onCellKeyDown: onOpen ? (params, event)=>{
|
|
313
|
+
if (event.key !== 'Enter') return;
|
|
314
|
+
if (params.field === LIST_ACTIONS_FIELD || params.field.startsWith('__')) return;
|
|
315
|
+
if (event.target !== event.currentTarget) return;
|
|
316
|
+
event.defaultMuiPrevented = true;
|
|
317
|
+
onOpen(String(params.id), params.row);
|
|
318
|
+
} : undefined,
|
|
319
|
+
/*
|
|
307
320
|
* The list GROWS; the PAGE scrolls.
|
|
308
321
|
*
|
|
309
322
|
* `DataTableComponent` gives its wrapper a fixed `height: 400`, which
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/list-table.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { AppLink } from './app-link'\nimport { MdiIcon } from './mdi-icon/mdi-icon'\nimport {\n DataTableComponent,\n type DataTableProps,\n} from './data-table.component'\nimport { IconButton, Stack, Tooltip } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport type { ReactNode } from 'react'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from './row-actions-menu.component'\nimport {\n TABLE_PAGE_SIZE_DEFAULT,\n TABLE_PAGE_SIZE_OPTIONS,\n TABLE_ROWS_PER_PAGE_LABEL,\n} from '../const/table-pagination'\n\n/**\n * ONE row grammar for every artifact list (AGL-2501).\n *\n *\n * They did. Three of the four are `DataTableComponent`, but each declared its\n * own column set, its own action arrangement and its own row-click, and the\n * fourth — screens — is a hand-rolled tree because it has to be: drag to\n * reorder, expand to nest, no grid can do that. So \"the same table\" cannot\n * literally be one component for all four, and pretending otherwise would mean\n * either losing the hierarchy or bolting a tree onto a grid.\n *\n * What IS shared is the row GRAMMAR, and that is what actually differed on\n * screen. Every artifact row now reads the same way:\n *\n * ```\n * [ drag · expand ] name id description updated created [ ⇗ ⋮ ]\n * screens only quick menu\n * ```\n *\n * - **The row opens the detail view.** Clicking anywhere that is not a control\n * navigates. It is the primary action on every one of these lists and it was\n * previously a link buried in one column.\n * - **Rows are NOT selectable.** The grid lists shipped with selection on, so\n * a click both navigated AND tinted the row, and the footer counted \"1 row\n * selected\" for a selection nothing could act on. A selection with no bulk\n * action is a state the reader has to dismiss and cannot use.\n * - **Exactly one quick action, then the menu.** Everything else moves behind\n * the overflow — a delete sitting inline is one mis-click from the row's own\n * open handler, which is the argument AGL-701 already made for components and\n * never carried to the rest.\n *\n * The quick action is the one thing worth a direct click on that kind of\n * artifact: **Open live page** for a screen, **Preview** for anything that has\n * no address of its own. It is the only per-kind variation in the trailing\n * cluster, and it is chosen by the caller rather than inferred here.\n *\n * A screen is the only artifact of the four with a route the public can reach,\n * so it is the only one whose quick action says \"live\". Layouts, components\n * and templates render inside something else and have no address to open, so\n * theirs is a **Preview** into the console's own canvas render.\n *\n * The quick action is ALSO the menu's first entry — see\n * {@link quickActionMenuItem}. The icon alone is a glyph with a tooltip; the\n * menu is where every action on the row is spelled out, and an action missing\n * from it reads as an action the row does not have.\n */\n\n/**\n * The single trailing icon beside the overflow menu.\n *\n * Three shapes, because the destinations genuinely differ: an EXTERNAL live\n * page (new tab), an IN-APP preview route, and a plain handler for the rare\n * action that opens a dialog. `unavailableReason` is the fourth state and the\n * one most easily got wrong — a screen with no single live page (a collection\n * template renders under routes it does not own) must still show the control,\n * disabled, saying why. Hiding it would read as the feature being absent.\n */\nexport interface ListQuickAction {\n /** `mdi` icon path. */\n icon: string\n /** Accessible name; also the tooltip when the action is unavailable. */\n label: string\n /** A live, external URL — opened in a new tab. */\n href?: string\n /** An in-app route — a normal client navigation. */\n to?: string\n onClick?: () => void\n /**\n * Why this row cannot use the action. Renders it DISABLED with the reason as\n * its tooltip, rather than removing it — an absent control and an\n * inapplicable one look identical, and only one of them is honest.\n */\n unavailableReason?: string\n}\n\nexport interface ListRowActionsProps {\n /** The artifact's name — every control is labelled with it, because these\n * Repeat once per row and \"More actions\" alone says nothing about which. */\n label: string\n quick?: ListQuickAction | null\n items: RowActionsMenuItem[]\n}\n\n/**\n * The quick action, restated as the menu's first entry.\n *\n * The icon and the menu are two ways into ONE action, not two features: the\n * icon is a bare glyph, so a reader who does not recognize it has the overflow\n * as the place every action is named. Deriving the item from the same\n * {@link ListQuickAction} is what keeps the two in step — availability,\n * destination and the reason it is refused are read once and cannot drift.\n */\nfunction quickActionMenuItem(\n quick: ListQuickAction,\n): RowActionsMenuItem {\n return {\n key: 'quick',\n label: quick.label,\n icon: <MdiIcon path={quick.icon} size={0.8} />,\n // `href` is off-site and opens a new tab; `to` is an in-app route.\n href: quick.href ?? quick.to,\n external: Boolean(quick.href),\n onClick: quick.href || quick.to ? undefined : quick.onClick,\n disabled: Boolean(quick.unavailableReason),\n disabledReason: quick.unavailableReason,\n }\n}\n\n/** The trailing cluster: one quick action, then the overflow menu. */\nexport function ListRowActions(props: ListRowActionsProps) {\n const { label, quick, items } = props\n const menuItems = quick ? [quickActionMenuItem(quick), ...items] : items\n return (\n <Stack\n direction=\"row\"\n spacing={0.5}\n /*\n `height: '100%'` is what actually centres these (AGL-2501). A DataGrid\n cell is a flex box, but `renderCell` content is auto-height inside it —\n so at the taller `TABLE_ROW_HEIGHT` these icons ride at the top of the\n row while the text beside them is centred. `alignItems` alone cannot\n fix it: there is no spare height to distribute until the child claims\n the row.\n */\n sx={{\n height: '100%',\n alignItems: 'center',\n justifyContent: 'flex-end',\n }}\n // The row's own click opens the detail view; without this every control\n // in here would navigate out from under the thing it just opened.\n onClick={(event) => event.stopPropagation()}\n >\n {quick ? <ListQuickButton label={label} action={quick} /> : null}\n <RowActionsMenu label={label} items={menuItems} />\n </Stack>\n )\n}\nListRowActions.displayName = 'ListRowActions'\n\nfunction ListQuickButton(props: {\n label: string\n action: ListQuickAction\n}) {\n const { label, action } = props\n const icon = <MdiIcon path={action.icon} size={0.8} />\n const name = `${action.label} — ${label}`\n\n if (action.unavailableReason) {\n return (\n <Tooltip title={action.unavailableReason}>\n {/* span: a disabled button emits no events, so the tooltip needs a\n wrapper that does. */}\n <span>\n <IconButton size=\"small\" aria-label={name} disabled>\n {icon}\n </IconButton>\n </span>\n </Tooltip>\n )\n }\n if (action.href) {\n return (\n <Tooltip title={action.label}>\n <AppLink\n componentVariant=\"icon-button\"\n size=\"small\"\n aria-label={name}\n href={action.href}\n target=\"_blank\"\n rel=\"noreferrer\"\n >\n {icon}\n </AppLink>\n </Tooltip>\n )\n }\n if (action.to) {\n return (\n <Tooltip title={action.label}>\n <AppLink\n componentVariant=\"icon-button\"\n size=\"small\"\n aria-label={name}\n href={action.to}\n >\n {icon}\n </AppLink>\n </Tooltip>\n )\n }\n return (\n <Tooltip title={action.label}>\n <IconButton size=\"small\" aria-label={name} onClick={action.onClick}>\n {icon}\n </IconButton>\n </Tooltip>\n )\n}\n\n/** The field `listActionsColumn` draws, which a row click never opens through. */\nexport const LIST_ACTIONS_FIELD = 'actions'\n\n/**\n * The trailing ACTIONS column, identical on every grid list.\n *\n * Deliberately NOT MUI's `type: 'actions'`. That type renders each\n * `GridActionsCellItem` as its own icon and moves the ones marked `showInMenu`\n * into a menu it owns — which is how the three lists ended up with two, three\n * and five inline icons respectively, each a different width, and how the\n * screens table (which cannot use it at all) grew a second implementation.\n * One `renderCell` gives every list the same cluster and the same width.\n */\nexport function listActionsColumn(\n render: (row: any) => ReactNode,\n options: { width?: number } = {},\n): GridColDef {\n return {\n field: LIST_ACTIONS_FIELD,\n headerName: 'Actions',\n width: options.width ?? 110,\n align: 'right',\n headerAlign: 'right',\n sortable: false,\n filterable: false,\n hideable: false,\n disableColumnMenu: true,\n renderCell: ({ row }: any) => render(row),\n }\n}\n\n/**\n * Row selection, opted into by a list that has something to DO with a\n * selection (AGL-2595).\n *\n * Controlled, and in the caller's vocabulary — an array of `$id`s in, an\n * array out — rather than MUI's own model. The grid's model is a\n * `{ type, ids: Set }` whose `exclude` form appears the moment the header\n * checkbox is ticked, and a caller that had to fold \"everything except\n * these\" back into \"these\" would do it wrong on the page boundary; the\n * table holds the rows and does the fold once, here.\n */\nexport interface ListTableSelection {\n /** The `$id`s currently selected; ids not in `rows` are ignored. */\n selected: readonly string[]\n /** Every change, as the full new selection. */\n onChange: (ids: string[]) => void\n}\n\nexport interface ListTableProps extends DataTableProps {\n /** Row click → the artifact's detail view. */\n onOpen?: (id: string, row: any) => void\n /**\n * The toolbar's free-text search box.\n *\n * Defaults to shown where the GRID answers it, which is client filtering.\n * Under `filterMode=\"server\"` the grid applies no filter at all and the\n * model goes to the caller, so the box defaults to hidden: a list whose\n * handler reads only the column filter would otherwise offer a search that\n * narrows nothing, which reads as \"no match\" rather than \"not supported\".\n * Pass it on a server-filtered list whose handler reads `quickFilterValues`.\n */\n quickFilter?: boolean\n /**\n * Present → a checkbox column and a controlled selection. Absent → the\n * navigation-only grid every list had before, checkbox column and all.\n * The row click keeps opening the record either way: a checkbox is the\n * only thing that selects, so the two gestures cannot collide.\n */\n selectable?: ListTableSelection\n /**\n * The toolbar, its Filters panel and its search, and none of the rows\n * (AGL-3327).\n *\n * For a list the reader can also see some other way — the media library's\n * thumbnail grid — that must filter exactly as its table view does. The\n * panel, the quick search and Export all live inside the grid, bound to\n * its API, so a second, hand-built toolbar for the other view would be a\n * second filter UI to keep in step with this one. Instead the grid is\n * drawn with its body collapsed and no rows: the toolbar stays, and the\n * panel still opens under it (its anchor is kept, at the top of the\n * collapsed body). The columns chooser and Export go — there are no\n * columns on screen to choose, and the rows are the other view's to draw\n * and to export.\n */\n toolbarOnly?: boolean\n}\n\nconst NO_ROWS = [] as const\n\n/** No column chooser and no Export: the columns and rows are drawn elsewhere. */\nconst TOOLBAR_ONLY_PROPS = { disableColumnSelector: true }\n\n/** Nothing, for an overlay a toolbar-only grid has no body to draw in. */\nconst NoOverlay = () => null\n\n/** A toolbar-only grid's overlays: its body is collapsed, so they are too. */\nconst TOOLBAR_ONLY_SLOTS = {\n noRowsOverlay: NoOverlay,\n noResultsOverlay: NoOverlay,\n loadingOverlay: NoOverlay,\n}\n\n/**\n * The body of a toolbar-only grid, collapsed to nothing.\n *\n * Everything in the main container goes except the panel anchor, which the\n * Filters panel positions itself against: the anchor sits at the top of the\n * collapsed body, which is directly under the toolbar, where the panel opens\n * on every other list. The body keeps no height, so the virtualized rows it\n * would draw into have none to fill.\n */\nconst TOOLBAR_ONLY_SX = {\n '& .MuiDataGrid-main': {\n flex: '0 0 0px',\n height: 0,\n minHeight: 0,\n overflow: 'visible',\n },\n '& .MuiDataGrid-main > :not([data-id=\"gridPanelAnchor\"])': {\n display: 'none',\n },\n '& [data-id=\"gridPanelAnchor\"]': { top: 0 },\n} as const\n\n/**\n * The grid every FLAT artifact list uses — layouts, components, templates.\n *\n * Screens is the deliberate exception and keeps its own tree; it adopts\n * {@link ListRowActions} so the two still read alike, which is the part a\n * reader actually compares.\n */\nexport function ListTable(props: ListTableProps) {\n const {\n onOpen,\n sx,\n initialState,\n hideFooter,\n rows,\n selectable,\n quickFilter,\n slotProps,\n toolbarOnly = false,\n slots,\n ...rest\n } = props\n const showQuickFilter = quickFilter ?? rest.filterMode !== 'server'\n /*==========================================\n * SELECTION IS OPT-IN, AND NAVIGATION-ONLY IS THE DEFAULT.\n *\n * Without `selectable`, the three properties that make a row a NAVIGATION\n * target rather than a selectable record: `disableRowSelectionOnClick`\n * alone is not enough — it stops the click selecting, and leaves the\n * checkbox column and the \"n rows selected\" footer behind.\n *\n * With it, the grid's model is built from the caller's id list on every\n * render and folded back into one on every change. MUI's model since v8 is\n * `{ type: 'include' | 'exclude', ids }`, and `exclude` is what the header\n * checkbox produces — \"everything but these\" — so the fold has to consult\n * the rows the grid was given, which is why it lives here and not in the\n * caller. `disableRowSelectionOnClick` stays on in this branch too: a row\n * click still opens the record, and only the checkbox selects.\n *=========================================*/\n const selection = selectable\n ? {\n rowSelection: true,\n checkboxSelection: true,\n rowSelectionModel: {\n type: 'include' as const,\n ids: new Set<string>(selectable.selected),\n },\n onRowSelectionModelChange: (model: {\n type: 'include' | 'exclude'\n ids: Set<unknown>\n }) => {\n const all = (rows ?? []).map((row: any) => String(row.$id))\n selectable.onChange(\n model.type === 'include'\n ? all.filter((id) => model.ids.has(id))\n : all.filter((id) => !model.ids.has(id)),\n )\n },\n }\n : { rowSelection: false, checkboxSelection: false }\n /*==========================================\n * A HIDDEN FOOTER STILL PAGED THE ROWS.\n *\n * `hideFooter` is how a caller says \"something else owns the page\" — a\n * server-paged staff list, a cursor feed, a card whose `ListPagination`\n * sits under the grid. It hides the CONTROL and nothing else: the free\n * DataGrid always paginates, so the grid went on slicing at the shared\n * default and rows eleven onward were drawn by nothing and reachable by\n * nothing. The outer footer then described the rows it was HANDED, so it\n * reported a page two rows longer than the one on screen and offered no\n * way to see the difference.\n *\n * Eight lists pass `hideFooter`, and every one of them was losing rows past\n * the tenth. It went unnoticed because a list has to hold more than ten\n * rows to show a symptom, and most of these do not yet.\n *\n * So when the footer is hidden the grid takes ONE page as tall as the rows\n * it was given. CONTROLLED rather than `initialState`, because a\n * server-paged list hands over a different number of rows on every page and\n * initial state is read once. A floor of one because MUI rejects a page\n * size of zero, and an empty grid has nothing to slice anyway.\n *=========================================*/\n const rowCount = toolbarOnly ? 0 : (rows?.length ?? 0)\n const footerHidden = hideFooter || toolbarOnly\n const unpaged = footerHidden\n ? {\n paginationModel: { page: 0, pageSize: Math.max(rowCount, 1) },\n // A controlled model with no handler logs a MUI warning, and the\n // grid must not move off page zero in any case: the page is not\n // this component's to change while somebody else owns the footer.\n onPaginationModelChange: () => undefined,\n // Keeps the size out of the \"not preset in pageSizeOptions\" warning.\n // Nothing renders it — the footer carrying the menu is hidden.\n pageSizeOptions: [Math.max(rowCount, 1)],\n }\n : {}\n return (\n <DataTableComponent\n getRowId={(row: any) => row.$id}\n {...selection}\n disableRowSelectionOnClick\n onRowClick={\n onOpen\n ? ({ id, row }, event) => {\n /*\n * A press anywhere in the actions cell is aimed at the actions.\n * `ListRowActions` stops its own clicks, but the cell is wider\n * and taller than the cluster, and a press on the padding\n * beside the menu would otherwise open the record from under\n * the reader.\n */\n const target = event.target as Element | null\n if (target?.closest?.(`[data-field=\"${LIST_ACTIONS_FIELD}\"]`)) return\n onOpen(String(id), row)\n }\n : undefined\n }\n /*\n * The list GROWS; the PAGE scrolls.\n *\n * `DataTableComponent` gives its wrapper a fixed `height: 400`, which\n * is right for a grid sitting in a pane of its own and wrong for a\n * card in a column of cards: the table got its own scrollbar, the card\n * stopped at 400px whatever it held, and the last row was cut through\n * the middle. Two scroll regions on one page is the thing to avoid —\n * a reader scrolling the page expects the page to move.\n *\n * `autoHeight` sizes the grid to its rows; clearing the wrapper's\n * height is what lets the card follow it. Caller `sx` is spread after,\n * so a list that genuinely wants a viewport can still take one.\n */\n autoHeight\n sx={[\n { height: 'auto' },\n { '& .MuiDataGrid-row': { cursor: onOpen ? 'pointer' : 'default' } },\n /*\n A cell whose content is taller than one line sets the row's height,\n and every OTHER cell then draws its content at the top of it — so a\n two-line date column leaves the counts beside it riding high while\n `ListRowActions`, which claims `height: '100%'` for itself, sits\n centred. One row, three vertical positions.\n\n Centring the cell itself fixes every column at once, including the\n ones a caller renders with a bare `<Typography>`; `alignItems`\n alone cannot, because an auto-height child has no spare height to\n distribute until something claims the row. `justifyContent` is\n deliberately not set here — a column's own `align` is what decides\n its horizontal placement, and overriding it would silently undo\n every right-aligned figure.\n */\n {\n '& .MuiDataGrid-cell': {\n display: 'flex',\n alignItems: 'center',\n },\n },\n ...(toolbarOnly ? [TOOLBAR_ONLY_SX] : []),\n ...(Array.isArray(sx) ? sx : sx ? [sx] : []),\n ]}\n /*\n * The column menu can hide a column; only the toolbar can bring it\n * back.\n *\n * Every column here is hideable, `Manage columns` lives in the toolbar,\n * and the toolbar was off — so hiding the last column left a grid with\n * no columns and no control that could restore one. Not a rare misuse\n * either: the per-column menu offers `Hide column` on every column, and\n * the way out was never on screen.\n *\n * The Actions column is the reason the grid was not left completely\n * blank — `listActionsColumn` marks it `hideable: false` — and \"one\n * unhideable column\" is not a recovery path.\n */\n showToolbar\n pagination\n // The console's ONE footer (AGL-2501): same options, same default, same\n // label.\n pageSizeOptions={TABLE_PAGE_SIZE_OPTIONS}\n /*\n * MERGED, not replaced. `initialState` used to ride in through `rest`,\n * so a caller that set anything in it — a column visibility model, a\n * sort — silently dropped the shared page size along with it. The two\n * keys a caller is likely to set are spread individually so that a page\n * size and a hidden column can coexist.\n */\n initialState={{\n ...initialState,\n pagination: {\n paginationModel: { pageSize: TABLE_PAGE_SIZE_DEFAULT },\n ...initialState?.pagination,\n },\n columns: { ...initialState?.columns },\n }}\n /*\n * MERGED with the caller's, for the reason `initialState` is: a caller\n * setting one slot's props must not drop the shared footer label or the\n * search box's filter-mode default along with the object they live in.\n */\n slotProps={{\n ...slotProps,\n toolbar: {\n showQuickFilter,\n ...slotProps?.toolbar,\n ...(toolbarOnly\n ? {\n csvOptions: { disableToolbarButton: true },\n printOptions: { disableToolbarButton: true },\n }\n : {}),\n },\n pagination: {\n labelRowsPerPage: TABLE_ROWS_PER_PAGE_LABEL,\n ...slotProps?.pagination,\n },\n }}\n // A toolbar-only grid holds no rows: the reader sees them some other\n // way, and rows laid out under a collapsed body would be drawn for no\n // one.\n rows={toolbarOnly ? NO_ROWS : rows}\n hideFooter={footerHidden}\n slots={toolbarOnly ? { ...slots, ...TOOLBAR_ONLY_SLOTS } : slots}\n {...(toolbarOnly ? TOOLBAR_ONLY_PROPS : {})}\n {...rest}\n /*\n * AFTER `rest`, deliberately. \"The footer is hidden, so every row I was\n * handed is on screen\" is an invariant of this component, not a default\n * a call site may talk it out of — a caller that could re-slice here\n * would reintroduce exactly the rows nobody can reach.\n */\n {...unpaged}\n />\n )\n}\nListTable.displayName = 'ListTable'\n\nexport default ListTable\n"],"names":["AppLink","MdiIcon","DataTableComponent","IconButton","Stack","Tooltip","RowActionsMenu","TABLE_PAGE_SIZE_DEFAULT","TABLE_PAGE_SIZE_OPTIONS","TABLE_ROWS_PER_PAGE_LABEL","quickActionMenuItem","quick","key","label","icon","path","size","href","to","external","Boolean","onClick","undefined","disabled","unavailableReason","disabledReason","ListRowActions","props","items","menuItems","direction","spacing","sx","height","alignItems","justifyContent","event","stopPropagation","ListQuickButton","action","displayName","name","title","span","aria-label","componentVariant","target","rel","LIST_ACTIONS_FIELD","listActionsColumn","render","options","field","headerName","width","align","headerAlign","sortable","filterable","hideable","disableColumnMenu","renderCell","row","NO_ROWS","TOOLBAR_ONLY_PROPS","disableColumnSelector","NoOverlay","TOOLBAR_ONLY_SLOTS","noRowsOverlay","noResultsOverlay","loadingOverlay","TOOLBAR_ONLY_SX","flex","minHeight","overflow","display","top","ListTable","onOpen","initialState","hideFooter","rows","selectable","quickFilter","slotProps","toolbarOnly","slots","rest","showQuickFilter","filterMode","selection","rowSelection","checkboxSelection","rowSelectionModel","type","ids","Set","selected","onRowSelectionModelChange","model","all","map","String","$id","onChange","filter","id","has","rowCount","length","footerHidden","unpaged","paginationModel","page","pageSize","Math","max","onPaginationModelChange","pageSizeOptions","getRowId","disableRowSelectionOnClick","onRowClick","closest","autoHeight","cursor","Array","isArray","showToolbar","pagination","columns","toolbar","csvOptions","disableToolbarButton","printOptions","labelRowsPerPage"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/list-table.component.tsx"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n'use client'\n\nimport { AppLink } from './app-link'\nimport { MdiIcon } from './mdi-icon/mdi-icon'\nimport {\n DataTableComponent,\n type DataTableProps,\n} from './data-table.component'\nimport { IconButton, Stack, Tooltip } from '@mui/material'\nimport type { GridColDef } from '@mui/x-data-grid'\nimport type { ReactNode } from 'react'\nimport RowActionsMenu, {\n type RowActionsMenuItem,\n} from './row-actions-menu.component'\nimport {\n TABLE_PAGE_SIZE_DEFAULT,\n TABLE_PAGE_SIZE_OPTIONS,\n TABLE_ROWS_PER_PAGE_LABEL,\n} from '../const/table-pagination'\n\n/**\n * ONE row grammar for every artifact list (AGL-2501).\n *\n *\n * They did. Three of the four are `DataTableComponent`, but each declared its\n * own column set, its own action arrangement and its own row-click, and the\n * fourth — screens — is a hand-rolled tree because it has to be: drag to\n * reorder, expand to nest, no grid can do that. So \"the same table\" cannot\n * literally be one component for all four, and pretending otherwise would mean\n * either losing the hierarchy or bolting a tree onto a grid.\n *\n * What IS shared is the row GRAMMAR, and that is what actually differed on\n * screen. Every artifact row now reads the same way:\n *\n * ```\n * [ drag · expand ] name id description updated created [ ⇗ ⋮ ]\n * screens only quick menu\n * ```\n *\n * - **The row opens the detail view.** Clicking anywhere that is not a control\n * navigates. It is the primary action on every one of these lists and it was\n * previously a link buried in one column.\n * - **Rows are NOT selectable.** The grid lists shipped with selection on, so\n * a click both navigated AND tinted the row, and the footer counted \"1 row\n * selected\" for a selection nothing could act on. A selection with no bulk\n * action is a state the reader has to dismiss and cannot use.\n * - **Exactly one quick action, then the menu.** Everything else moves behind\n * the overflow — a delete sitting inline is one mis-click from the row's own\n * open handler, which is the argument AGL-701 already made for components and\n * never carried to the rest.\n *\n * The quick action is the one thing worth a direct click on that kind of\n * artifact: **Open live page** for a screen, **Preview** for anything that has\n * no address of its own. It is the only per-kind variation in the trailing\n * cluster, and it is chosen by the caller rather than inferred here.\n *\n * A screen is the only artifact of the four with a route the public can reach,\n * so it is the only one whose quick action says \"live\". Layouts, components\n * and templates render inside something else and have no address to open, so\n * theirs is a **Preview** into the console's own canvas render.\n *\n * The quick action is ALSO the menu's first entry — see\n * {@link quickActionMenuItem}. The icon alone is a glyph with a tooltip; the\n * menu is where every action on the row is spelled out, and an action missing\n * from it reads as an action the row does not have.\n */\n\n/**\n * The single trailing icon beside the overflow menu.\n *\n * Three shapes, because the destinations genuinely differ: an EXTERNAL live\n * page (new tab), an IN-APP preview route, and a plain handler for the rare\n * action that opens a dialog. `unavailableReason` is the fourth state and the\n * one most easily got wrong — a screen with no single live page (a collection\n * template renders under routes it does not own) must still show the control,\n * disabled, saying why. Hiding it would read as the feature being absent.\n */\nexport interface ListQuickAction {\n /** `mdi` icon path. */\n icon: string\n /** Accessible name; also the tooltip when the action is unavailable. */\n label: string\n /** A live, external URL — opened in a new tab. */\n href?: string\n /** An in-app route — a normal client navigation. */\n to?: string\n onClick?: () => void\n /**\n * Why this row cannot use the action. Renders it DISABLED with the reason as\n * its tooltip, rather than removing it — an absent control and an\n * inapplicable one look identical, and only one of them is honest.\n */\n unavailableReason?: string\n}\n\nexport interface ListRowActionsProps {\n /** The artifact's name — every control is labelled with it, because these\n * Repeat once per row and \"More actions\" alone says nothing about which. */\n label: string\n quick?: ListQuickAction | null\n items: RowActionsMenuItem[]\n}\n\n/**\n * The quick action, restated as the menu's first entry.\n *\n * The icon and the menu are two ways into ONE action, not two features: the\n * icon is a bare glyph, so a reader who does not recognize it has the overflow\n * as the place every action is named. Deriving the item from the same\n * {@link ListQuickAction} is what keeps the two in step — availability,\n * destination and the reason it is refused are read once and cannot drift.\n */\nfunction quickActionMenuItem(\n quick: ListQuickAction,\n): RowActionsMenuItem {\n return {\n key: 'quick',\n label: quick.label,\n icon: <MdiIcon path={quick.icon} size={0.8} />,\n // `href` is off-site and opens a new tab; `to` is an in-app route.\n href: quick.href ?? quick.to,\n external: Boolean(quick.href),\n onClick: quick.href || quick.to ? undefined : quick.onClick,\n disabled: Boolean(quick.unavailableReason),\n disabledReason: quick.unavailableReason,\n }\n}\n\n/** The trailing cluster: one quick action, then the overflow menu. */\nexport function ListRowActions(props: ListRowActionsProps) {\n const { label, quick, items } = props\n const menuItems = quick ? [quickActionMenuItem(quick), ...items] : items\n return (\n <Stack\n direction=\"row\"\n spacing={0.5}\n /*\n `height: '100%'` is what actually centres these (AGL-2501). A DataGrid\n cell is a flex box, but `renderCell` content is auto-height inside it —\n so at the taller `TABLE_ROW_HEIGHT` these icons ride at the top of the\n row while the text beside them is centred. `alignItems` alone cannot\n fix it: there is no spare height to distribute until the child claims\n the row.\n */\n sx={{\n height: '100%',\n alignItems: 'center',\n justifyContent: 'flex-end',\n }}\n // The row's own click opens the detail view; without this every control\n // in here would navigate out from under the thing it just opened.\n onClick={(event) => event.stopPropagation()}\n >\n {quick ? <ListQuickButton label={label} action={quick} /> : null}\n <RowActionsMenu label={label} items={menuItems} />\n </Stack>\n )\n}\nListRowActions.displayName = 'ListRowActions'\n\nfunction ListQuickButton(props: {\n label: string\n action: ListQuickAction\n}) {\n const { label, action } = props\n const icon = <MdiIcon path={action.icon} size={0.8} />\n const name = `${action.label} — ${label}`\n\n if (action.unavailableReason) {\n return (\n <Tooltip title={action.unavailableReason}>\n {/* span: a disabled button emits no events, so the tooltip needs a\n wrapper that does. */}\n <span>\n <IconButton size=\"small\" aria-label={name} disabled>\n {icon}\n </IconButton>\n </span>\n </Tooltip>\n )\n }\n if (action.href) {\n return (\n <Tooltip title={action.label}>\n <AppLink\n componentVariant=\"icon-button\"\n size=\"small\"\n aria-label={name}\n href={action.href}\n target=\"_blank\"\n rel=\"noreferrer\"\n >\n {icon}\n </AppLink>\n </Tooltip>\n )\n }\n if (action.to) {\n return (\n <Tooltip title={action.label}>\n <AppLink\n componentVariant=\"icon-button\"\n size=\"small\"\n aria-label={name}\n href={action.to}\n >\n {icon}\n </AppLink>\n </Tooltip>\n )\n }\n return (\n <Tooltip title={action.label}>\n <IconButton size=\"small\" aria-label={name} onClick={action.onClick}>\n {icon}\n </IconButton>\n </Tooltip>\n )\n}\n\n/** The field `listActionsColumn` draws, which a row click never opens through. */\nexport const LIST_ACTIONS_FIELD = 'actions'\n\n/**\n * The trailing ACTIONS column, identical on every grid list.\n *\n * Deliberately NOT MUI's `type: 'actions'`. That type renders each\n * `GridActionsCellItem` as its own icon and moves the ones marked `showInMenu`\n * into a menu it owns — which is how the three lists ended up with two, three\n * and five inline icons respectively, each a different width, and how the\n * screens table (which cannot use it at all) grew a second implementation.\n * One `renderCell` gives every list the same cluster and the same width.\n */\nexport function listActionsColumn(\n render: (row: any) => ReactNode,\n options: { width?: number } = {},\n): GridColDef {\n return {\n field: LIST_ACTIONS_FIELD,\n headerName: 'Actions',\n width: options.width ?? 110,\n align: 'right',\n headerAlign: 'right',\n sortable: false,\n filterable: false,\n hideable: false,\n disableColumnMenu: true,\n renderCell: ({ row }: any) => render(row),\n }\n}\n\n/**\n * Row selection, opted into by a list that has something to DO with a\n * selection (AGL-2595).\n *\n * Controlled, and in the caller's vocabulary — an array of `$id`s in, an\n * array out — rather than MUI's own model. The grid's model is a\n * `{ type, ids: Set }` whose `exclude` form appears the moment the header\n * checkbox is ticked, and a caller that had to fold \"everything except\n * these\" back into \"these\" would do it wrong on the page boundary; the\n * table holds the rows and does the fold once, here.\n */\nexport interface ListTableSelection {\n /** The `$id`s currently selected; ids not in `rows` are ignored. */\n selected: readonly string[]\n /** Every change, as the full new selection. */\n onChange: (ids: string[]) => void\n}\n\nexport interface ListTableProps extends DataTableProps {\n /** Row click → the artifact's detail view. */\n onOpen?: (id: string, row: any) => void\n /**\n * The toolbar's free-text search box.\n *\n * Defaults to shown where the GRID answers it, which is client filtering.\n * Under `filterMode=\"server\"` the grid applies no filter at all and the\n * model goes to the caller, so the box defaults to hidden: a list whose\n * handler reads only the column filter would otherwise offer a search that\n * narrows nothing, which reads as \"no match\" rather than \"not supported\".\n * Pass it on a server-filtered list whose handler reads `quickFilterValues`.\n */\n quickFilter?: boolean\n /**\n * Present → a checkbox column and a controlled selection. Absent → the\n * navigation-only grid every list had before, checkbox column and all.\n * The row click keeps opening the record either way: a checkbox is the\n * only thing that selects, so the two gestures cannot collide.\n */\n selectable?: ListTableSelection\n /**\n * The toolbar, its Filters panel and its search, and none of the rows\n * (AGL-3327).\n *\n * For a list the reader can also see some other way — the media library's\n * thumbnail grid — that must filter exactly as its table view does. The\n * panel, the quick search and Export all live inside the grid, bound to\n * its API, so a second, hand-built toolbar for the other view would be a\n * second filter UI to keep in step with this one. Instead the grid is\n * drawn with its body collapsed and no rows: the toolbar stays, and the\n * panel still opens under it (its anchor is kept, at the top of the\n * collapsed body). The columns chooser and Export go — there are no\n * columns on screen to choose, and the rows are the other view's to draw\n * and to export.\n */\n toolbarOnly?: boolean\n}\n\nconst NO_ROWS = [] as const\n\n/** No column chooser and no Export: the columns and rows are drawn elsewhere. */\nconst TOOLBAR_ONLY_PROPS = { disableColumnSelector: true }\n\n/** Nothing, for an overlay a toolbar-only grid has no body to draw in. */\nconst NoOverlay = () => null\n\n/** A toolbar-only grid's overlays: its body is collapsed, so they are too. */\nconst TOOLBAR_ONLY_SLOTS = {\n noRowsOverlay: NoOverlay,\n noResultsOverlay: NoOverlay,\n loadingOverlay: NoOverlay,\n}\n\n/**\n * The body of a toolbar-only grid, collapsed to nothing.\n *\n * Everything in the main container goes except the panel anchor, which the\n * Filters panel positions itself against: the anchor sits at the top of the\n * collapsed body, which is directly under the toolbar, where the panel opens\n * on every other list. The body keeps no height, so the virtualized rows it\n * would draw into have none to fill.\n */\nconst TOOLBAR_ONLY_SX = {\n '& .MuiDataGrid-main': {\n flex: '0 0 0px',\n height: 0,\n minHeight: 0,\n overflow: 'visible',\n },\n '& .MuiDataGrid-main > :not([data-id=\"gridPanelAnchor\"])': {\n display: 'none',\n },\n '& [data-id=\"gridPanelAnchor\"]': { top: 0 },\n} as const\n\n/**\n * The grid every FLAT artifact list uses — layouts, components, templates.\n *\n * Screens is the deliberate exception and keeps its own tree; it adopts\n * {@link ListRowActions} so the two still read alike, which is the part a\n * reader actually compares.\n */\nexport function ListTable(props: ListTableProps) {\n const {\n onOpen,\n sx,\n initialState,\n hideFooter,\n rows,\n selectable,\n quickFilter,\n slotProps,\n toolbarOnly = false,\n slots,\n ...rest\n } = props\n const showQuickFilter = quickFilter ?? rest.filterMode !== 'server'\n /*==========================================\n * SELECTION IS OPT-IN, AND NAVIGATION-ONLY IS THE DEFAULT.\n *\n * Without `selectable`, the three properties that make a row a NAVIGATION\n * target rather than a selectable record: `disableRowSelectionOnClick`\n * alone is not enough — it stops the click selecting, and leaves the\n * checkbox column and the \"n rows selected\" footer behind.\n *\n * With it, the grid's model is built from the caller's id list on every\n * render and folded back into one on every change. MUI's model since v8 is\n * `{ type: 'include' | 'exclude', ids }`, and `exclude` is what the header\n * checkbox produces — \"everything but these\" — so the fold has to consult\n * the rows the grid was given, which is why it lives here and not in the\n * caller. `disableRowSelectionOnClick` stays on in this branch too: a row\n * click still opens the record, and only the checkbox selects.\n *=========================================*/\n const selection = selectable\n ? {\n rowSelection: true,\n checkboxSelection: true,\n rowSelectionModel: {\n type: 'include' as const,\n ids: new Set<string>(selectable.selected),\n },\n onRowSelectionModelChange: (model: {\n type: 'include' | 'exclude'\n ids: Set<unknown>\n }) => {\n const all = (rows ?? []).map((row: any) => String(row.$id))\n selectable.onChange(\n model.type === 'include'\n ? all.filter((id) => model.ids.has(id))\n : all.filter((id) => !model.ids.has(id)),\n )\n },\n }\n : { rowSelection: false, checkboxSelection: false }\n /*==========================================\n * A HIDDEN FOOTER STILL PAGED THE ROWS.\n *\n * `hideFooter` is how a caller says \"something else owns the page\" — a\n * server-paged staff list, a cursor feed, a card whose `ListPagination`\n * sits under the grid. It hides the CONTROL and nothing else: the free\n * DataGrid always paginates, so the grid went on slicing at the shared\n * default and rows eleven onward were drawn by nothing and reachable by\n * nothing. The outer footer then described the rows it was HANDED, so it\n * reported a page two rows longer than the one on screen and offered no\n * way to see the difference.\n *\n * Eight lists pass `hideFooter`, and every one of them was losing rows past\n * the tenth. It went unnoticed because a list has to hold more than ten\n * rows to show a symptom, and most of these do not yet.\n *\n * So when the footer is hidden the grid takes ONE page as tall as the rows\n * it was given. CONTROLLED rather than `initialState`, because a\n * server-paged list hands over a different number of rows on every page and\n * initial state is read once. A floor of one because MUI rejects a page\n * size of zero, and an empty grid has nothing to slice anyway.\n *=========================================*/\n const rowCount = toolbarOnly ? 0 : (rows?.length ?? 0)\n const footerHidden = hideFooter || toolbarOnly\n const unpaged = footerHidden\n ? {\n paginationModel: { page: 0, pageSize: Math.max(rowCount, 1) },\n // A controlled model with no handler logs a MUI warning, and the\n // grid must not move off page zero in any case: the page is not\n // this component's to change while somebody else owns the footer.\n onPaginationModelChange: () => undefined,\n // Keeps the size out of the \"not preset in pageSizeOptions\" warning.\n // Nothing renders it — the footer carrying the menu is hidden.\n pageSizeOptions: [Math.max(rowCount, 1)],\n }\n : {}\n return (\n <DataTableComponent\n getRowId={(row: any) => row.$id}\n {...selection}\n disableRowSelectionOnClick\n onRowClick={\n onOpen\n ? ({ id, row }, event) => {\n /*\n * A press anywhere in the actions cell is aimed at the actions.\n * `ListRowActions` stops its own clicks, but the cell is wider\n * and taller than the cluster, and a press on the padding\n * beside the menu would otherwise open the record from under\n * the reader.\n */\n const target = event.target as Element | null\n if (target?.closest?.(`[data-field=\"${LIST_ACTIONS_FIELD}\"]`)) return\n onOpen(String(id), row)\n }\n : undefined\n }\n /*\n * ENTER OPENS IT TOO. The grid moves focus cell by cell with the arrow\n * keys, and a row a mouse can open must open from the keyboard as\n * well. Only when the CELL itself holds focus: Enter on a link or a\n * button inside one is that control's, and neither the actions cell\n * nor the grid's own columns (the selection checkbox) open the record.\n */\n onCellKeyDown={\n onOpen\n ? (params, event) => {\n if (event.key !== 'Enter') return\n if (params.field === LIST_ACTIONS_FIELD || params.field.startsWith('__')) return\n if (event.target !== event.currentTarget) return\n event.defaultMuiPrevented = true\n onOpen(String(params.id), params.row)\n }\n : undefined\n }\n /*\n * The list GROWS; the PAGE scrolls.\n *\n * `DataTableComponent` gives its wrapper a fixed `height: 400`, which\n * is right for a grid sitting in a pane of its own and wrong for a\n * card in a column of cards: the table got its own scrollbar, the card\n * stopped at 400px whatever it held, and the last row was cut through\n * the middle. Two scroll regions on one page is the thing to avoid —\n * a reader scrolling the page expects the page to move.\n *\n * `autoHeight` sizes the grid to its rows; clearing the wrapper's\n * height is what lets the card follow it. Caller `sx` is spread after,\n * so a list that genuinely wants a viewport can still take one.\n */\n autoHeight\n sx={[\n { height: 'auto' },\n { '& .MuiDataGrid-row': { cursor: onOpen ? 'pointer' : 'default' } },\n /*\n A cell whose content is taller than one line sets the row's height,\n and every OTHER cell then draws its content at the top of it — so a\n two-line date column leaves the counts beside it riding high while\n `ListRowActions`, which claims `height: '100%'` for itself, sits\n centred. One row, three vertical positions.\n\n Centring the cell itself fixes every column at once, including the\n ones a caller renders with a bare `<Typography>`; `alignItems`\n alone cannot, because an auto-height child has no spare height to\n distribute until something claims the row. `justifyContent` is\n deliberately not set here — a column's own `align` is what decides\n its horizontal placement, and overriding it would silently undo\n every right-aligned figure.\n */\n {\n '& .MuiDataGrid-cell': {\n display: 'flex',\n alignItems: 'center',\n },\n },\n ...(toolbarOnly ? [TOOLBAR_ONLY_SX] : []),\n ...(Array.isArray(sx) ? sx : sx ? [sx] : []),\n ]}\n /*\n * The column menu can hide a column; only the toolbar can bring it\n * back.\n *\n * Every column here is hideable, `Manage columns` lives in the toolbar,\n * and the toolbar was off — so hiding the last column left a grid with\n * no columns and no control that could restore one. Not a rare misuse\n * either: the per-column menu offers `Hide column` on every column, and\n * the way out was never on screen.\n *\n * The Actions column is the reason the grid was not left completely\n * blank — `listActionsColumn` marks it `hideable: false` — and \"one\n * unhideable column\" is not a recovery path.\n */\n showToolbar\n pagination\n // The console's ONE footer (AGL-2501): same options, same default, same\n // label.\n pageSizeOptions={TABLE_PAGE_SIZE_OPTIONS}\n /*\n * MERGED, not replaced. `initialState` used to ride in through `rest`,\n * so a caller that set anything in it — a column visibility model, a\n * sort — silently dropped the shared page size along with it. The two\n * keys a caller is likely to set are spread individually so that a page\n * size and a hidden column can coexist.\n */\n initialState={{\n ...initialState,\n pagination: {\n paginationModel: { pageSize: TABLE_PAGE_SIZE_DEFAULT },\n ...initialState?.pagination,\n },\n columns: { ...initialState?.columns },\n }}\n /*\n * MERGED with the caller's, for the reason `initialState` is: a caller\n * setting one slot's props must not drop the shared footer label or the\n * search box's filter-mode default along with the object they live in.\n */\n slotProps={{\n ...slotProps,\n toolbar: {\n showQuickFilter,\n ...slotProps?.toolbar,\n ...(toolbarOnly\n ? {\n csvOptions: { disableToolbarButton: true },\n printOptions: { disableToolbarButton: true },\n }\n : {}),\n },\n pagination: {\n labelRowsPerPage: TABLE_ROWS_PER_PAGE_LABEL,\n ...slotProps?.pagination,\n },\n }}\n // A toolbar-only grid holds no rows: the reader sees them some other\n // way, and rows laid out under a collapsed body would be drawn for no\n // one.\n rows={toolbarOnly ? NO_ROWS : rows}\n hideFooter={footerHidden}\n slots={toolbarOnly ? { ...slots, ...TOOLBAR_ONLY_SLOTS } : slots}\n {...(toolbarOnly ? TOOLBAR_ONLY_PROPS : {})}\n {...rest}\n /*\n * AFTER `rest`, deliberately. \"The footer is hidden, so every row I was\n * handed is on screen\" is an invariant of this component, not a default\n * a call site may talk it out of — a caller that could re-slice here\n * would reintroduce exactly the rows nobody can reach.\n */\n {...unpaged}\n />\n )\n}\nListTable.displayName = 'ListTable'\n\nexport default ListTable\n"],"names":["AppLink","MdiIcon","DataTableComponent","IconButton","Stack","Tooltip","RowActionsMenu","TABLE_PAGE_SIZE_DEFAULT","TABLE_PAGE_SIZE_OPTIONS","TABLE_ROWS_PER_PAGE_LABEL","quickActionMenuItem","quick","key","label","icon","path","size","href","to","external","Boolean","onClick","undefined","disabled","unavailableReason","disabledReason","ListRowActions","props","items","menuItems","direction","spacing","sx","height","alignItems","justifyContent","event","stopPropagation","ListQuickButton","action","displayName","name","title","span","aria-label","componentVariant","target","rel","LIST_ACTIONS_FIELD","listActionsColumn","render","options","field","headerName","width","align","headerAlign","sortable","filterable","hideable","disableColumnMenu","renderCell","row","NO_ROWS","TOOLBAR_ONLY_PROPS","disableColumnSelector","NoOverlay","TOOLBAR_ONLY_SLOTS","noRowsOverlay","noResultsOverlay","loadingOverlay","TOOLBAR_ONLY_SX","flex","minHeight","overflow","display","top","ListTable","onOpen","initialState","hideFooter","rows","selectable","quickFilter","slotProps","toolbarOnly","slots","rest","showQuickFilter","filterMode","selection","rowSelection","checkboxSelection","rowSelectionModel","type","ids","Set","selected","onRowSelectionModelChange","model","all","map","String","$id","onChange","filter","id","has","rowCount","length","footerHidden","unpaged","paginationModel","page","pageSize","Math","max","onPaginationModelChange","pageSizeOptions","getRowId","disableRowSelectionOnClick","onRowClick","closest","onCellKeyDown","params","startsWith","currentTarget","defaultMuiPrevented","autoHeight","cursor","Array","isArray","showToolbar","pagination","columns","toolbar","csvOptions","disableToolbarButton","printOptions","labelRowsPerPage"],"mappings":"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"}
|
|
@@ -23,6 +23,7 @@ import type { ListFilterClause, ListFilterOption } from './list-grid-filter';
|
|
|
23
23
|
* is shown as itself rather than hidden.
|
|
24
24
|
*/
|
|
25
25
|
export declare function listFilterOperatorLabel(op: string): string;
|
|
26
|
+
export declare const listFilterDay: (raw: string) => Date;
|
|
26
27
|
/**
|
|
27
28
|
* How a clause reads as a sentence — "Owner is Dana", "Created on or after
|
|
28
29
|
* 1 Jan", "Reason is any of Bounced, Complained": its header, its operator,
|
|
@@ -77,8 +77,24 @@
|
|
|
77
77
|
/** Operators that take several values, comma-joined the way the grammar splits them. */ const MULTI_OPERATORS = new Set([
|
|
78
78
|
'isAnyOf'
|
|
79
79
|
]);
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
/**
|
|
81
|
+
* The calendar day a date clause names, as a LOCAL date.
|
|
82
|
+
*
|
|
83
|
+
* The panel's date input holds a day, not an instant, and hands it over as
|
|
84
|
+
* `YYYY-MM-DD` — which `new Date` reads as UTC midnight, the previous
|
|
85
|
+
* evening anywhere west of UTC. Travelled through `toISOString`, the same
|
|
86
|
+
* day arrives as `YYYY-MM-DDT00:00:00.000Z`. Either spelling is read by its
|
|
87
|
+
* date parts, so "on or after Sep 17" starts on Sep 17 wherever the reader
|
|
88
|
+
* is. Any other value is an instant and is read as one. The query
|
|
89
|
+
* translators read a date clause through it too, so the day a query asks for
|
|
90
|
+
* is the day the rows are matched by.
|
|
91
|
+
*/ const DAY_ONLY = /^(\d{4})-(\d{2})-(\d{2})(?:T00:00:00(?:\.000)?Z)?$/;
|
|
92
|
+
export const listFilterDay = (raw)=>{
|
|
93
|
+
const day = DAY_ONLY.exec(raw);
|
|
94
|
+
return day ? new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3])) : new Date(raw);
|
|
95
|
+
};
|
|
96
|
+
/** A date clause's value as the day it names, read the way the query reads it. */ const dayLabel = (raw)=>{
|
|
97
|
+
const at = listFilterDay(raw);
|
|
82
98
|
return Number.isNaN(at.getTime()) ? raw : at.toLocaleDateString();
|
|
83
99
|
};
|
|
84
100
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/const/list-filter-sentence.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ListFilterField } from './list-filter'\nimport type { ListFilterClause, ListFilterOption } from './list-grid-filter'\n\n/*\n * How a list's filter clauses read as words, for the chips over a list and\n * the notices that say a clause was not applied (AGL-3321).\n *\n * ⛔ No `@mui/x-data-grid` here, and no runtime import of `list-filter` or\n * `list-grid-filter`, which build the data grid's operators. The storefront's\n * product grid shows these notices on customers' published pages, and\n * reaching the grid's operators from here would ship the grid's filter\n * inputs with every storefront page.\n */\n\n/**\n * How an operator reads on a chip or in a sentence — \"Owner is Dana\",\n * \"Created on or after 1 Jan\". One map so every list that shows its\n * clauses back spells them the same way; an operator nothing here names\n * is shown as itself rather than hidden.\n */\nexport function listFilterOperatorLabel(op: string): string {\n switch (op) {\n case 'contains':\n return 'contains'\n case 'doesNotContain':\n return 'does not contain'\n case 'equals':\n case 'is':\n case '=':\n return 'is'\n case 'doesNotEqual':\n case '!=':\n return 'is not'\n case 'startsWith':\n return 'starts with'\n case 'endsWith':\n return 'ends with'\n case 'isAnyOf':\n return 'is any of'\n case 'isEmpty':\n return 'is empty'\n case 'isNotEmpty':\n return 'is set'\n case 'after':\n return 'after'\n case 'onOrAfter':\n return 'on or after'\n case 'before':\n return 'before'\n case 'onOrBefore':\n return 'on or before'\n case '>':\n return 'over'\n case '>=':\n return 'at least'\n case '<':\n return 'under'\n case '<=':\n return 'at most'\n default:\n return op\n }\n}\n\n/** Operators that carry no value. */\nconst VALUELESS_OPERATORS = new Set(['isEmpty', 'isNotEmpty'])\n/** Operators that take several values, comma-joined the way the grammar splits them. */\nconst MULTI_OPERATORS = new Set(['isAnyOf'])\n\nconst
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/const/list-filter-sentence.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ListFilterField } from './list-filter'\nimport type { ListFilterClause, ListFilterOption } from './list-grid-filter'\n\n/*\n * How a list's filter clauses read as words, for the chips over a list and\n * the notices that say a clause was not applied (AGL-3321).\n *\n * ⛔ No `@mui/x-data-grid` here, and no runtime import of `list-filter` or\n * `list-grid-filter`, which build the data grid's operators. The storefront's\n * product grid shows these notices on customers' published pages, and\n * reaching the grid's operators from here would ship the grid's filter\n * inputs with every storefront page.\n */\n\n/**\n * How an operator reads on a chip or in a sentence — \"Owner is Dana\",\n * \"Created on or after 1 Jan\". One map so every list that shows its\n * clauses back spells them the same way; an operator nothing here names\n * is shown as itself rather than hidden.\n */\nexport function listFilterOperatorLabel(op: string): string {\n switch (op) {\n case 'contains':\n return 'contains'\n case 'doesNotContain':\n return 'does not contain'\n case 'equals':\n case 'is':\n case '=':\n return 'is'\n case 'doesNotEqual':\n case '!=':\n return 'is not'\n case 'startsWith':\n return 'starts with'\n case 'endsWith':\n return 'ends with'\n case 'isAnyOf':\n return 'is any of'\n case 'isEmpty':\n return 'is empty'\n case 'isNotEmpty':\n return 'is set'\n case 'after':\n return 'after'\n case 'onOrAfter':\n return 'on or after'\n case 'before':\n return 'before'\n case 'onOrBefore':\n return 'on or before'\n case '>':\n return 'over'\n case '>=':\n return 'at least'\n case '<':\n return 'under'\n case '<=':\n return 'at most'\n default:\n return op\n }\n}\n\n/** Operators that carry no value. */\nconst VALUELESS_OPERATORS = new Set(['isEmpty', 'isNotEmpty'])\n/** Operators that take several values, comma-joined the way the grammar splits them. */\nconst MULTI_OPERATORS = new Set(['isAnyOf'])\n\n/**\n * The calendar day a date clause names, as a LOCAL date.\n *\n * The panel's date input holds a day, not an instant, and hands it over as\n * `YYYY-MM-DD` — which `new Date` reads as UTC midnight, the previous\n * evening anywhere west of UTC. Travelled through `toISOString`, the same\n * day arrives as `YYYY-MM-DDT00:00:00.000Z`. Either spelling is read by its\n * date parts, so \"on or after Sep 17\" starts on Sep 17 wherever the reader\n * is. Any other value is an instant and is read as one. The query\n * translators read a date clause through it too, so the day a query asks for\n * is the day the rows are matched by.\n */\nconst DAY_ONLY = /^(\\d{4})-(\\d{2})-(\\d{2})(?:T00:00:00(?:\\.000)?Z)?$/\nexport const listFilterDay = (raw: string): Date => {\n const day = DAY_ONLY.exec(raw)\n return day ? new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3])) : new Date(raw)\n}\n\n/** A date clause's value as the day it names, read the way the query reads it. */\nconst dayLabel = (raw: string): string => {\n const at = listFilterDay(raw)\n return Number.isNaN(at.getTime()) ? raw : at.toLocaleDateString()\n}\n\n/**\n * How a clause reads as a sentence — \"Owner is Dana\", \"Created on or after\n * 1 Jan\", \"Reason is any of Bounced, Complained\": its header, its operator,\n * and its value by the label of the option it names.\n *\n * ONE wording, used by the chips over a list and by the notices that say a\n * clause was not applied (`listQueryRefusals`), so the two cannot describe\n * the same clause differently.\n */\nexport function listFilterClauseSentence(\n clause: ListFilterClause,\n context: {\n fields: readonly ListFilterField[]\n headers?: Readonly<Record<string, string>>\n options?: Readonly<Record<string, readonly ListFilterOption[]>>\n },\n): string {\n const field = context.fields.find((entry) => entry.column === clause.field)\n const header = context.headers?.[clause.field] ?? clause.field\n const choices = context.options?.[clause.field]\n const named = (value: string) =>\n choices?.find((option) => option.value === value)?.label ?? value\n const value = VALUELESS_OPERATORS.has(clause.op)\n ? ''\n : MULTI_OPERATORS.has(clause.op)\n ? clause.value\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean)\n .map(named)\n .join(', ')\n : clause.label ?? (field?.kind === 'date' ? dayLabel(clause.value) : named(clause.value))\n return `${header} ${listFilterOperatorLabel(clause.op)}${value ? ` ${value}` : ''}`\n}\n"],"names":["listFilterOperatorLabel","op","VALUELESS_OPERATORS","Set","MULTI_OPERATORS","DAY_ONLY","listFilterDay","raw","day","exec","Date","Number","dayLabel","at","isNaN","getTime","toLocaleDateString","listFilterClauseSentence","clause","context","field","fields","find","entry","column","header","headers","choices","options","named","value","option","label","has","split","map","trim","filter","Boolean","join","kind"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAKD;;;;;;;;;CASC,GAED;;;;;CAKC,GACD,OAAO,SAASA,wBAAwBC,EAAU;IAChD,OAAQA;QACN,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;QACL,KAAK;QACL,KAAK;YACH,OAAO;QACT,KAAK;QACL,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT;YACE,OAAOA;IACX;AACF;AAEA,mCAAmC,GACnC,MAAMC,sBAAsB,IAAIC,IAAI;IAAC;IAAW;CAAa;AAC7D,sFAAsF,GACtF,MAAMC,kBAAkB,IAAID,IAAI;IAAC;CAAU;AAE3C;;;;;;;;;;;CAWC,GACD,MAAME,WAAW;AACjB,OAAO,MAAMC,gBAAgB,CAACC;IAC5B,MAAMC,MAAMH,SAASI,IAAI,CAACF;IAC1B,OAAOC,MAAM,IAAIE,KAAKC,OAAOH,GAAG,CAAC,EAAE,GAAGG,OAAOH,GAAG,CAAC,EAAE,IAAI,GAAGG,OAAOH,GAAG,CAAC,EAAE,KAAK,IAAIE,KAAKH;AACvF,EAAC;AAED,gFAAgF,GAChF,MAAMK,WAAW,CAACL;IAChB,MAAMM,KAAKP,cAAcC;IACzB,OAAOI,OAAOG,KAAK,CAACD,GAAGE,OAAO,MAAMR,MAAMM,GAAGG,kBAAkB;AACjE;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC,yBACdC,MAAwB,EACxBC,OAIC;cAgBKD;QAbSC,kBACCA;IAFhB,MAAMC,QAAQD,QAAQE,MAAM,CAACC,IAAI,CAAC,CAACC,QAAUA,MAAMC,MAAM,KAAKN,OAAOE,KAAK;IAC1E,MAAMK,kBAASN,mBAAAA,QAAQO,OAAO,qBAAfP,gBAAiB,CAACD,OAAOE,KAAK,CAAC,mBAAIF,OAAOE,KAAK;IAC9D,MAAMO,WAAUR,mBAAAA,QAAQS,OAAO,qBAAfT,gBAAiB,CAACD,OAAOE,KAAK,CAAC;IAC/C,MAAMS,QAAQ,CAACC;;YACbH;uBAAAA,4BAAAA,gBAAAA,QAASL,IAAI,CAAC,CAACS,SAAWA,OAAOD,KAAK,KAAKA,2BAA3CH,cAAmDK,KAAK,mBAAIF;;IAC9D,MAAMA,QAAQ5B,oBAAoB+B,GAAG,CAACf,OAAOjB,EAAE,IAC3C,KACAG,gBAAgB6B,GAAG,CAACf,OAAOjB,EAAE,IAC3BiB,OAAOY,KAAK,CACTI,KAAK,CAAC,KACNC,GAAG,CAAC,CAACZ,QAAUA,MAAMa,IAAI,IACzBC,MAAM,CAACC,SACPH,GAAG,CAACN,OACJU,IAAI,CAAC,SACRrB,gBAAAA,OAAOc,KAAK,YAAZd,gBAAiBE,CAAAA,yBAAAA,MAAOoB,IAAI,MAAK,SAAS5B,SAASM,OAAOY,KAAK,IAAID,MAAMX,OAAOY,KAAK;IAC3F,OAAO,GAAGL,OAAO,CAAC,EAAEzB,wBAAwBkB,OAAOjB,EAAE,IAAI6B,QAAQ,CAAC,CAAC,EAAEA,OAAO,GAAG,IAAI;AACrF"}
|
|
@@ -217,8 +217,7 @@ export declare function gridFilterRequests(model: {
|
|
|
217
217
|
value?: unknown;
|
|
218
218
|
}>;
|
|
219
219
|
}): ListFilterRequest[];
|
|
220
|
-
export { listFilterOperatorLabel } from './list-filter-sentence';
|
|
221
|
-
export declare const listFilterDay: (raw: string) => Date;
|
|
220
|
+
export { listFilterDay, listFilterOperatorLabel } from './list-filter-sentence';
|
|
222
221
|
/**
|
|
223
222
|
* Match ONE row against a filter, for a list that cannot push it to a query.
|
|
224
223
|
*
|
|
@@ -13,59 +13,8 @@
|
|
|
13
13
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
*
|
|
19
|
-
* A paged list filters on the SERVER or it does not filter: `filterMode="server"`
|
|
20
|
-
* stops the grid narrowing anything itself, so the panel shows the rows already
|
|
21
|
-
* fetched — ten of them by default — and an operator the route does not answer
|
|
22
|
-
* becomes a control that quietly does nothing. On a staff list whose whole job
|
|
23
|
-
* is that nobody is missing, "no matches" is the one answer it must never give
|
|
24
|
-
* wrongly.
|
|
25
|
-
*
|
|
26
|
-
* So a list declares its filterable fields ONCE, in a module both its route and
|
|
27
|
-
* its page import. The declaration decides two things that must never disagree:
|
|
28
|
-
* which operators the menu offers (here) and which predicates the query builds
|
|
29
|
-
* (`applyListFilter`, server-side, which needs firebase-admin and so cannot
|
|
30
|
-
* live in this file).
|
|
31
|
-
*
|
|
32
|
-
* ⛔ No runtime import of `@mui/x-data-grid` here. This grammar is read by
|
|
33
|
-
* `list-query-plan`, which the storefront's server-side catalog query reaches
|
|
34
|
-
* from a published page, and the grid's filter inputs are `'use client'`
|
|
35
|
-
* modules: imported from the server graph they become client references that
|
|
36
|
-
* every published page downloads. The grid's own operator objects are built
|
|
37
|
-
* in `list-grid-filter` (`listFilterColumn`, `hiddenFilterColumns`).
|
|
38
|
-
*
|
|
39
|
-
* ## Why the menu is shorter than MUI's
|
|
40
|
-
*
|
|
41
|
-
* Firestore has equality, `in`, ranges and `array-contains`. Everything below
|
|
42
|
-
* is built from those four:
|
|
43
|
-
*
|
|
44
|
-
* equals / isAnyOf equality and `in` (Firestore caps `in` at 30 values)
|
|
45
|
-
* startsWith a range over the normalized lower-case key
|
|
46
|
-
* endsWith a range over the key stored REVERSED — a range is
|
|
47
|
-
* anchored at the FRONT of a value, so storing the value
|
|
48
|
-
* backwards is the only way to ask about its end
|
|
49
|
-
* contains `array-contains` over word-prefix tokens, so it is
|
|
50
|
-
* WORD-level: "coffee" finds "Acme Coffee", "offee" does
|
|
51
|
-
* not find anything
|
|
52
|
-
* before / after ranges on a timestamp or a number
|
|
53
|
-
* isNotEmpty `!= null`, which in Firestore also requires the field to
|
|
54
|
-
* EXIST — which is the meaning wanted
|
|
55
|
-
*
|
|
56
|
-
* ⛔ `doesNotContain` is offered nowhere and cannot be. Firestore has no
|
|
57
|
-
* negated substring match, and no denormalization fixes it: the inverse of a
|
|
58
|
-
* token array is every string the value does not contain, which is unbounded.
|
|
59
|
-
*
|
|
60
|
-
* ⚠️ The two empty operators depend on how a field's WRITERS behave, which is
|
|
61
|
-
* what `presence` declares. Firestore cannot query for absence at all:
|
|
62
|
-
* `where(path, '==', null)` matches an explicit null and never a missing field,
|
|
63
|
-
* while `where(path, '!=', null)` additionally requires the field to exist.
|
|
64
|
-
* So a field that writers omit can answer `isNotEmpty` exactly and can never
|
|
65
|
-
* answer `isEmpty` — it would say "none" to a question with real answers — and
|
|
66
|
-
* a field that is always written answers both, but each with a foregone
|
|
67
|
-
* result. Only a `nullable` field earns both operators for real.
|
|
68
|
-
*/ /** How a field can be queried, which decides which operators it offers. */ /** The empty-operators a field can honestly offer. See `presence`. */ const emptyOperators = (field)=>{
|
|
16
|
+
*/ import { listFilterDay } from "./list-filter-sentence.js";
|
|
17
|
+
/** The empty-operators a field can honestly offer. See `presence`. */ const emptyOperators = (field)=>{
|
|
69
18
|
var _field_presence;
|
|
70
19
|
switch((_field_presence = field.presence) != null ? _field_presence : 'sparse'){
|
|
71
20
|
case 'always':
|
|
@@ -195,7 +144,7 @@ const gridItemRequest = (item)=>{
|
|
|
195
144
|
var _model_items;
|
|
196
145
|
return ((_model_items = model.items) != null ? _model_items : []).filter(gridItemUsable).map(gridItemRequest);
|
|
197
146
|
}
|
|
198
|
-
export { listFilterOperatorLabel } from "./list-filter-sentence.js";
|
|
147
|
+
export { listFilterDay, listFilterOperatorLabel } from "./list-filter-sentence.js";
|
|
199
148
|
const readPath = (row, path)=>path.split('.').reduce((value, key)=>value == null ? undefined : value[key], row);
|
|
200
149
|
const asTime = (value)=>{
|
|
201
150
|
if (value == null) return null;
|
|
@@ -207,22 +156,6 @@ const asTime = (value)=>{
|
|
|
207
156
|
const parsed = new Date(String(value)).getTime();
|
|
208
157
|
return Number.isNaN(parsed) ? null : parsed;
|
|
209
158
|
};
|
|
210
|
-
/**
|
|
211
|
-
* The calendar day a date clause names, as a LOCAL date.
|
|
212
|
-
*
|
|
213
|
-
* The panel's date input holds a day, not an instant, and hands it over as
|
|
214
|
-
* `YYYY-MM-DD` — which `new Date` reads as UTC midnight, the previous
|
|
215
|
-
* evening anywhere west of UTC. Travelled through `toISOString`, the same
|
|
216
|
-
* day arrives as `YYYY-MM-DDT00:00:00.000Z`. Either spelling is read by its
|
|
217
|
-
* date parts, so "on or after Sep 17" starts on Sep 17 wherever the reader
|
|
218
|
-
* is. Any other value is an instant and is read as one. The query
|
|
219
|
-
* translators read a date clause through it too, so the day a query asks for
|
|
220
|
-
* is the day the rows are matched by.
|
|
221
|
-
*/ const DAY_ONLY = /^(\d{4})-(\d{2})-(\d{2})(?:T00:00:00(?:\.000)?Z)?$/;
|
|
222
|
-
export const listFilterDay = (raw)=>{
|
|
223
|
-
const day = DAY_ONLY.exec(raw);
|
|
224
|
-
return day ? new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3])) : new Date(raw);
|
|
225
|
-
};
|
|
226
159
|
/**
|
|
227
160
|
* Match ONE row against a filter, for a list that cannot push it to a query.
|
|
228
161
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/const/list-filter.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n\n/**\n * ONE declaration of what a paged list can be filtered by (AGL-2501).\n *\n * A paged list filters on the SERVER or it does not filter: `filterMode=\"server\"`\n * stops the grid narrowing anything itself, so the panel shows the rows already\n * fetched — ten of them by default — and an operator the route does not answer\n * becomes a control that quietly does nothing. On a staff list whose whole job\n * is that nobody is missing, \"no matches\" is the one answer it must never give\n * wrongly.\n *\n * So a list declares its filterable fields ONCE, in a module both its route and\n * its page import. The declaration decides two things that must never disagree:\n * which operators the menu offers (here) and which predicates the query builds\n * (`applyListFilter`, server-side, which needs firebase-admin and so cannot\n * live in this file).\n *\n * ⛔ No runtime import of `@mui/x-data-grid` here. This grammar is read by\n * `list-query-plan`, which the storefront's server-side catalog query reaches\n * from a published page, and the grid's filter inputs are `'use client'`\n * modules: imported from the server graph they become client references that\n * every published page downloads. The grid's own operator objects are built\n * in `list-grid-filter` (`listFilterColumn`, `hiddenFilterColumns`).\n *\n * ## Why the menu is shorter than MUI's\n *\n * Firestore has equality, `in`, ranges and `array-contains`. Everything below\n * is built from those four:\n *\n * equals / isAnyOf equality and `in` (Firestore caps `in` at 30 values)\n * startsWith a range over the normalized lower-case key\n * endsWith a range over the key stored REVERSED — a range is\n * anchored at the FRONT of a value, so storing the value\n * backwards is the only way to ask about its end\n * contains `array-contains` over word-prefix tokens, so it is\n * WORD-level: \"coffee\" finds \"Acme Coffee\", \"offee\" does\n * not find anything\n * before / after ranges on a timestamp or a number\n * isNotEmpty `!= null`, which in Firestore also requires the field to\n * EXIST — which is the meaning wanted\n *\n * ⛔ `doesNotContain` is offered nowhere and cannot be. Firestore has no\n * negated substring match, and no denormalization fixes it: the inverse of a\n * token array is every string the value does not contain, which is unbounded.\n *\n * ⚠️ The two empty operators depend on how a field's WRITERS behave, which is\n * what `presence` declares. Firestore cannot query for absence at all:\n * `where(path, '==', null)` matches an explicit null and never a missing field,\n * while `where(path, '!=', null)` additionally requires the field to exist.\n * So a field that writers omit can answer `isNotEmpty` exactly and can never\n * answer `isEmpty` — it would say \"none\" to a question with real answers — and\n * a field that is always written answers both, but each with a foregone\n * result. Only a `nullable` field earns both operators for real.\n */\n\n/** How a field can be queried, which decides which operators it offers. */\nexport type ListFilterKind =\n | 'text'\n | 'exact'\n | 'boolean'\n | 'number'\n | 'date'\n | 'id'\n\nexport interface ListFilterField {\n /** The grid column's `field` — what the client sends back. */\n column: string\n kind: ListFilterKind\n /** The Firestore field path the predicate targets. */\n path: string\n /**\n * The normalized lower-case twin, for `equals` and `startsWith` on text.\n * Without one a text field offers neither: a raw value is case-sensitive, so\n * it would miss rather than fail.\n */\n lowerPath?: string\n /** Word-prefix token array, for `contains`. */\n tokensPath?: string\n /**\n * The token array holds OPAQUE IDS, matched byte for byte.\n *\n * A word token array is lower-cased by its writer, so a `contains` folds\n * the typed value the same way before it looks. An id array — the forms a\n * contact came in through, the companies they are linked to — is not: the\n * ids are minted with mixed case, and folding one would look up an id\n * nothing wrote. A field that says so is matched on the value exactly as\n * given, in the query and in memory alike.\n */\n verbatimTokens?: boolean\n /**\n * What a `contains` orders by. Defaults to `lowerPath`, then `path`.\n *\n * A field whose `tokensPath` IS its own value — a real tag array, matched\n * whole rather than by word prefix — has no lower-case twin to sort by, and\n * defaulting to `path` would order by the array itself. Naming a scalar here\n * keeps the composite index that combination needs an ordinary one.\n */\n containsOrderBy?: string\n /**\n * Which way a `contains` orders by `containsOrderBy`. Ascending unless the\n * field says otherwise; a list sorted newest-first orders its `contains`\n * the same way, so the composite it needs is the one it already declares\n * (AGL-3321).\n */\n containsOrderDirection?: 'asc' | 'desc'\n /** The reversed key, for `endsWith`. */\n reversedPath?: string\n /**\n * How the field's writers treat \"no value\", which is the only thing that\n * decides whether the empty operators mean anything:\n *\n * `sparse` (default) writers OMIT the field. `isNotEmpty` is exact;\n * `isEmpty` is unanswerable and so is not offered.\n * `nullable` writers store `null`. Both operators are exact and both can\n * return rows — the only case where offering them is useful.\n * `always` the field is never absent and never null. Both operators have\n * a foregone answer (all rows, then none), so neither is\n * offered: a control whose result is known is a control that\n * wastes the reader's time.\n */\n presence?: 'sparse' | 'nullable' | 'always'\n /**\n * The operators this field offers, replacing the derived list.\n *\n * Not every list is a Firestore query. The staff account list is Firebase\n * Auth, which cannot filter at ALL — so a filter there is answered by\n * scanning the pools and matching in memory, and plain JavaScript can do\n * things no index can: a mid-string `contains`, and `doesNotContain`. A\n * list whose substrate is more capable says so here rather than being held\n * to Firestore's limits by a shared default.\n *\n * Pair with {@link matchListFilter}, which is the matcher those lists use.\n */\n operators?: readonly string[]\n /**\n * Where to read this field from a ROW, when the list matches in memory.\n * Defaults to `path`. Only needed when the row's shape differs from the\n * document's — an Auth record's `staff` lives under `customClaims`.\n */\n rowPath?: string\n /**\n * The field lives where no query can reach it, so the list matches it\n * over the rows it has loaded and the translators refuse it.\n *\n * A contact's owner, stage and custom values sit on the viewing group's\n * FACET — a map keyed per group — and a `where` on a per-group path would\n * need an index per group. Declaring the field at all is what puts it in\n * the menu and in a saved view; saying it is window-only is what keeps a\n * clause on it from being sent to a query that would answer with a\n * permission error or nothing. The list says which clauses narrowed only\n * the window — see `listFilterPlan`.\n */\n windowOnly?: boolean\n /**\n * A `date` field stored as epoch MILLISECONDS (`…AtMs`) rather than as a\n * Timestamp. The query twins compare numbers for it, and the matcher reads\n * both already (AGL-3321).\n */\n storedAs?: 'timestamp' | 'millis'\n /**\n * The stored value is a PRESENCE MAP — `{ form: true, order: true }` —\n * and the filter asks about its keys.\n *\n * A contact's `sources` is such a map: one key per door the person came\n * in through, never an array. Matched as though it were the array of its\n * truthy keys, so `equals form` and `isAnyOf form,order` read the way a\n * reader expects; a query can serve `equals` as `sources.form == true`.\n */\n keysOf?: boolean\n}\n\n/** The empty-operators a field can honestly offer. See `presence`. */\nconst emptyOperators = (field: ListFilterField): string[] => {\n switch (field.presence ?? 'sparse') {\n case 'always':\n return []\n case 'nullable':\n return ['isEmpty', 'isNotEmpty']\n default:\n return ['isNotEmpty']\n }\n}\n\n/** The operators a field offers, derived from the paths it actually has. */\nexport function listFilterOperators(field: ListFilterField): string[] {\n if (field.operators) return [...field.operators]\n const empties = emptyOperators(field)\n switch (field.kind) {\n case 'text':\n return [\n ...(field.tokensPath ? ['contains'] : []),\n ...(field.lowerPath ? ['equals', 'startsWith'] : []),\n ...(field.reversedPath ? ['endsWith'] : []),\n ...empties,\n ]\n case 'exact':\n return ['equals', 'isAnyOf', ...empties]\n case 'id':\n // An id is never absent, so the empty operators would answer nothing.\n return ['equals', 'startsWith', 'isAnyOf']\n case 'boolean':\n return ['is']\n case 'number':\n return ['=', '!=', '>', '>=', '<', '<=', ...empties]\n case 'date':\n return ['is', 'after', 'onOrAfter', 'before', 'onOrBefore', ...empties]\n default:\n return []\n }\n}\n\n/** What a page sends the route: one field, one operator, one value. */\nexport interface ListFilterRequest {\n field: string\n op: string\n value: string\n}\n\n/** Operators that carry no value — the grid leaves `item.value` undefined. */\nconst VALUELESS = ['isEmpty', 'isNotEmpty']\n\n/**\n * The one filter item a server-paged list can act on, read off a grid model.\n *\n * ONE, because Firestore composes predicates only through indexes nobody built\n * for every pair of columns; the first usable item wins and the rest are the\n * reader's to remove. Every list read this off the model itself, and each did\n * it slightly differently — the common miss being `isEmpty`, which sets no\n * value at all and so was skipped by any check that required one.\n */\nexport function gridFilterRequest(model: {\n items?: Array<{ field?: unknown; operator?: unknown; value?: unknown }>\n}): ListFilterRequest | null {\n const usable = (model.items ?? []).find((item) => {\n if (!item.field || !item.operator) return false\n if (VALUELESS.includes(String(item.operator))) return true\n if (item.value === undefined || item.value === null) return false\n return Array.isArray(item.value)\n ? item.value.length > 0\n : String(item.value).trim() !== ''\n })\n if (!usable) return null\n return gridItemRequest(usable)\n}\n\n/** Whether a grid filter item asks anything at all — see `gridFilterRequest`. */\nconst gridItemUsable = (item: {\n field?: unknown\n operator?: unknown\n value?: unknown\n}): boolean => {\n if (!item.field || !item.operator) return false\n if (VALUELESS.includes(String(item.operator))) return true\n if (item.value === undefined || item.value === null) return false\n return Array.isArray(item.value)\n ? item.value.length > 0\n : String(item.value).trim() !== ''\n}\n\nconst gridItemRequest = (item: {\n field?: unknown\n operator?: unknown\n value?: unknown\n}): ListFilterRequest => {\n const raw = item.value\n const value = Array.isArray(raw)\n ? raw.join(',')\n : // A date column hands back a `Date`, whose default string form is a\n // locale rendering the server would have to guess at. ISO travels\n // unambiguously and `new Date` reads it back exactly.\n raw instanceof Date\n ? raw.toISOString()\n : raw === undefined || raw === null\n ? ''\n : String(raw)\n return { field: String(item.field), op: String(item.operator), value }\n}\n\n/**\n * EVERY usable filter item on a grid model, in the order the panel holds\n * them — for a list that keeps more than one clause (AGL-2617).\n *\n * The singular reader above picks the first because a query serves one;\n * a saved view carries a list of clauses and a list that applies it puts\n * the first servable one on the query and matches the rest over the rows\n * it loaded, which `listFilterPlan` in the instance library decides. The\n * two readers share one notion of \"usable\", so a clause the singular one\n * would take is a clause this one keeps.\n */\nexport function gridFilterRequests(model: {\n items?: Array<{ field?: unknown; operator?: unknown; value?: unknown }>\n}): ListFilterRequest[] {\n return (model.items ?? []).filter(gridItemUsable).map(gridItemRequest)\n}\n\nexport { listFilterOperatorLabel } from './list-filter-sentence'\n\nconst readPath = (row: unknown, path: string): unknown =>\n path\n .split('.')\n .reduce<any>(\n (value, key) => (value == null ? undefined : value[key]),\n row as any,\n )\n\nconst asTime = (value: unknown): number | null => {\n if (value == null) return null\n if (value instanceof Date) return value.getTime()\n if (typeof value === 'number') return value\n // A Firestore timestamp, in either the admin shape or the serialized one.\n const seconds = (value as { seconds?: unknown }).seconds\n if (typeof seconds === 'number') return seconds * 1000\n const parsed = new Date(String(value)).getTime()\n return Number.isNaN(parsed) ? null : parsed\n}\n\n/**\n * The calendar day a date clause names, as a LOCAL date.\n *\n * The panel's date input holds a day, not an instant, and hands it over as\n * `YYYY-MM-DD` — which `new Date` reads as UTC midnight, the previous\n * evening anywhere west of UTC. Travelled through `toISOString`, the same\n * day arrives as `YYYY-MM-DDT00:00:00.000Z`. Either spelling is read by its\n * date parts, so \"on or after Sep 17\" starts on Sep 17 wherever the reader\n * is. Any other value is an instant and is read as one. The query\n * translators read a date clause through it too, so the day a query asks for\n * is the day the rows are matched by.\n */\nconst DAY_ONLY = /^(\\d{4})-(\\d{2})-(\\d{2})(?:T00:00:00(?:\\.000)?Z)?$/\nexport const listFilterDay = (raw: string): Date => {\n const day = DAY_ONLY.exec(raw)\n return day ? new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3])) : new Date(raw)\n}\n\n/**\n * Match ONE row against a filter, for a list that cannot push it to a query.\n *\n * The staff account list is the reason this exists: it is Firebase Auth, whose\n * `listUsers` takes a page size and a cursor and nothing else — no predicate,\n * no ordering, no search. A filter there is answered by walking the pools and\n * matching here, which is why that list can offer a mid-string `contains` and\n * a `doesNotContain` that a Firestore-backed list cannot.\n *\n * The declaration is still the same `ListFilterField`, so a list swapping\n * substrates changes where the matching happens and not what it offers.\n */\nexport function matchListFilter(\n row: unknown,\n fields: readonly ListFilterField[],\n input: ListFilterRequest | null,\n): boolean {\n if (!input) return true\n const field = fields.find((entry) => entry.column === input.field)\n if (!field) return true\n const stored = readPath(row, field.rowPath ?? field.path)\n // A presence map is matched as the array of its truthy keys — see `keysOf`.\n const value =\n field.keysOf && stored && typeof stored === 'object' && !Array.isArray(stored)\n ? Object.keys(stored as Record<string, unknown>).filter((key) =>\n Boolean((stored as Record<string, unknown>)[key]),\n )\n : stored\n const op = input.op\n\n const blank =\n value == null || value === '' || (Array.isArray(value) && value.length === 0)\n if (op === 'isEmpty') return blank\n if (op === 'isNotEmpty') return !blank\n\n const raw = (input.value ?? '').trim()\n if (field.kind === 'boolean') {\n if (raw !== 'true' && raw !== 'false') return true\n return Boolean(value) === (raw === 'true')\n }\n if (!raw) return true\n\n /*\n * An array the QUERY serves whole is matched by whole member, never by\n * substring of its joined form. A tag array asked `contains vip` holds the\n * tag `vip` or it does not — `vip-gold` is a different tag, and the\n * query's `array-contains` says so too, so the in-memory answer and the\n * served answer agree. An id array keeps its case (`verbatimTokens`); a\n * word array is folded the way its writer folded it. `isAnyOf` is any\n * member in the asked list, which is what a saved segment's \"any of these\n * tags\" becomes as a view clause. A presence map's keys are members too.\n *\n * A plain array with no token path is served by nothing, so there is no\n * query to agree with, and it falls through to the scalar match over its\n * joined text — the staff account list's providers, where `contains\n * google` finding `google.com` is the mid-string answer that list\n * documents (`operators`).\n */\n if (Array.isArray(value) && (field.tokensPath || field.keysOf)) {\n const fold = (entry: unknown) =>\n field.verbatimTokens ? String(entry) : String(entry).toLowerCase()\n const members = value.map(fold)\n const asked = fold(raw)\n switch (op) {\n case 'contains':\n case 'equals':\n case 'is':\n return members.includes(asked)\n case 'doesNotContain':\n case 'doesNotEqual':\n return !members.includes(asked)\n case 'isAnyOf':\n return asked\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean)\n .some((entry) => members.includes(entry))\n default:\n return true\n }\n }\n\n if (field.kind === 'date') {\n const at = asTime(value)\n const day = listFilterDay(raw)\n if (at === null || Number.isNaN(day.getTime())) return false\n const start = new Date(day)\n start.setHours(0, 0, 0, 0)\n const end = new Date(start)\n end.setDate(end.getDate() + 1)\n switch (op) {\n // A day, not an instant — a stored time never equals midnight.\n case 'is':\n return at >= start.getTime() && at < end.getTime()\n case 'after':\n return at >= end.getTime()\n case 'onOrAfter':\n return at >= start.getTime()\n case 'before':\n return at < start.getTime()\n case 'onOrBefore':\n return at < end.getTime()\n default:\n return true\n }\n }\n\n if (field.kind === 'number') {\n const asked = Number(raw)\n const held = Number(value)\n if (!Number.isFinite(asked) || !Number.isFinite(held)) return false\n switch (op) {\n case '=':\n return held === asked\n case '!=':\n return held !== asked\n case '>':\n return held > asked\n case '>=':\n return held >= asked\n case '<':\n return held < asked\n case '<=':\n return held <= asked\n default:\n return true\n }\n }\n\n // A scalar id is matched as typed — see `verbatimTokens`; the array case\n // was answered above.\n if (field.verbatimTokens && op === 'contains') return value === raw\n const held = value == null ? '' : String(value).toLowerCase()\n const asked = raw.toLowerCase()\n switch (op) {\n case 'contains':\n return held.includes(asked)\n case 'doesNotContain':\n return !held.includes(asked)\n case 'equals':\n return held === asked\n case 'doesNotEqual':\n return held !== asked\n case 'startsWith':\n return held.startsWith(asked)\n case 'endsWith':\n return held.endsWith(asked)\n case 'isAnyOf':\n return asked\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean)\n .includes(held)\n default:\n // An operator nothing here answers must not narrow the list to\n // nothing — that reads as \"no such record\" rather than \"not supported\".\n return true\n }\n}\n\n/** The visibility model that hides what {@link hiddenFilterColumns} added. */\nexport function hiddenFilterVisibility(\n fields: readonly ListFilterField[],\n visible: readonly string[],\n): Record<string, boolean> {\n return Object.fromEntries(\n fields\n .filter((field) => !visible.includes(field.column))\n .map((field) => [field.column, false]),\n )\n}\n"],"names":["emptyOperators","field","presence","listFilterOperators","operators","empties","kind","tokensPath","lowerPath","reversedPath","VALUELESS","gridFilterRequest","model","usable","items","find","item","operator","includes","String","value","undefined","Array","isArray","length","trim","gridItemRequest","gridItemUsable","raw","join","Date","toISOString","op","gridFilterRequests","filter","map","listFilterOperatorLabel","readPath","row","path","split","reduce","key","asTime","getTime","seconds","parsed","Number","isNaN","DAY_ONLY","listFilterDay","day","exec","matchListFilter","fields","input","entry","column","stored","rowPath","keysOf","Object","keys","Boolean","blank","fold","verbatimTokens","toLowerCase","members","asked","some","at","start","setHours","end","setDate","getDate","held","isFinite","startsWith","endsWith","hiddenFilterVisibility","visible","fromEntries"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/const/list-filter.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { listFilterDay } from './list-filter-sentence'\n\n/**\n * ONE declaration of what a paged list can be filtered by (AGL-2501).\n *\n * A paged list filters on the SERVER or it does not filter: `filterMode=\"server\"`\n * stops the grid narrowing anything itself, so the panel shows the rows already\n * fetched — ten of them by default — and an operator the route does not answer\n * becomes a control that quietly does nothing. On a staff list whose whole job\n * is that nobody is missing, \"no matches\" is the one answer it must never give\n * wrongly.\n *\n * So a list declares its filterable fields ONCE, in a module both its route and\n * its page import. The declaration decides two things that must never disagree:\n * which operators the menu offers (here) and which predicates the query builds\n * (`applyListFilter`, server-side, which needs firebase-admin and so cannot\n * live in this file).\n *\n * ⛔ No runtime import of `@mui/x-data-grid` here. This grammar is read by\n * `list-query-plan`, which the storefront's server-side catalog query reaches\n * from a published page, and the grid's filter inputs are `'use client'`\n * modules: imported from the server graph they become client references that\n * every published page downloads. The grid's own operator objects are built\n * in `list-grid-filter` (`listFilterColumn`, `hiddenFilterColumns`).\n *\n * ## Why the menu is shorter than MUI's\n *\n * Firestore has equality, `in`, ranges and `array-contains`. Everything below\n * is built from those four:\n *\n * equals / isAnyOf equality and `in` (Firestore caps `in` at 30 values)\n * startsWith a range over the normalized lower-case key\n * endsWith a range over the key stored REVERSED — a range is\n * anchored at the FRONT of a value, so storing the value\n * backwards is the only way to ask about its end\n * contains `array-contains` over word-prefix tokens, so it is\n * WORD-level: \"coffee\" finds \"Acme Coffee\", \"offee\" does\n * not find anything\n * before / after ranges on a timestamp or a number\n * isNotEmpty `!= null`, which in Firestore also requires the field to\n * EXIST — which is the meaning wanted\n *\n * ⛔ `doesNotContain` is offered nowhere and cannot be. Firestore has no\n * negated substring match, and no denormalization fixes it: the inverse of a\n * token array is every string the value does not contain, which is unbounded.\n *\n * ⚠️ The two empty operators depend on how a field's WRITERS behave, which is\n * what `presence` declares. Firestore cannot query for absence at all:\n * `where(path, '==', null)` matches an explicit null and never a missing field,\n * while `where(path, '!=', null)` additionally requires the field to exist.\n * So a field that writers omit can answer `isNotEmpty` exactly and can never\n * answer `isEmpty` — it would say \"none\" to a question with real answers — and\n * a field that is always written answers both, but each with a foregone\n * result. Only a `nullable` field earns both operators for real.\n */\n\n/** How a field can be queried, which decides which operators it offers. */\nexport type ListFilterKind =\n | 'text'\n | 'exact'\n | 'boolean'\n | 'number'\n | 'date'\n | 'id'\n\nexport interface ListFilterField {\n /** The grid column's `field` — what the client sends back. */\n column: string\n kind: ListFilterKind\n /** The Firestore field path the predicate targets. */\n path: string\n /**\n * The normalized lower-case twin, for `equals` and `startsWith` on text.\n * Without one a text field offers neither: a raw value is case-sensitive, so\n * it would miss rather than fail.\n */\n lowerPath?: string\n /** Word-prefix token array, for `contains`. */\n tokensPath?: string\n /**\n * The token array holds OPAQUE IDS, matched byte for byte.\n *\n * A word token array is lower-cased by its writer, so a `contains` folds\n * the typed value the same way before it looks. An id array — the forms a\n * contact came in through, the companies they are linked to — is not: the\n * ids are minted with mixed case, and folding one would look up an id\n * nothing wrote. A field that says so is matched on the value exactly as\n * given, in the query and in memory alike.\n */\n verbatimTokens?: boolean\n /**\n * What a `contains` orders by. Defaults to `lowerPath`, then `path`.\n *\n * A field whose `tokensPath` IS its own value — a real tag array, matched\n * whole rather than by word prefix — has no lower-case twin to sort by, and\n * defaulting to `path` would order by the array itself. Naming a scalar here\n * keeps the composite index that combination needs an ordinary one.\n */\n containsOrderBy?: string\n /**\n * Which way a `contains` orders by `containsOrderBy`. Ascending unless the\n * field says otherwise; a list sorted newest-first orders its `contains`\n * the same way, so the composite it needs is the one it already declares\n * (AGL-3321).\n */\n containsOrderDirection?: 'asc' | 'desc'\n /** The reversed key, for `endsWith`. */\n reversedPath?: string\n /**\n * How the field's writers treat \"no value\", which is the only thing that\n * decides whether the empty operators mean anything:\n *\n * `sparse` (default) writers OMIT the field. `isNotEmpty` is exact;\n * `isEmpty` is unanswerable and so is not offered.\n * `nullable` writers store `null`. Both operators are exact and both can\n * return rows — the only case where offering them is useful.\n * `always` the field is never absent and never null. Both operators have\n * a foregone answer (all rows, then none), so neither is\n * offered: a control whose result is known is a control that\n * wastes the reader's time.\n */\n presence?: 'sparse' | 'nullable' | 'always'\n /**\n * The operators this field offers, replacing the derived list.\n *\n * Not every list is a Firestore query. The staff account list is Firebase\n * Auth, which cannot filter at ALL — so a filter there is answered by\n * scanning the pools and matching in memory, and plain JavaScript can do\n * things no index can: a mid-string `contains`, and `doesNotContain`. A\n * list whose substrate is more capable says so here rather than being held\n * to Firestore's limits by a shared default.\n *\n * Pair with {@link matchListFilter}, which is the matcher those lists use.\n */\n operators?: readonly string[]\n /**\n * Where to read this field from a ROW, when the list matches in memory.\n * Defaults to `path`. Only needed when the row's shape differs from the\n * document's — an Auth record's `staff` lives under `customClaims`.\n */\n rowPath?: string\n /**\n * The field lives where no query can reach it, so the list matches it\n * over the rows it has loaded and the translators refuse it.\n *\n * A contact's owner, stage and custom values sit on the viewing group's\n * FACET — a map keyed per group — and a `where` on a per-group path would\n * need an index per group. Declaring the field at all is what puts it in\n * the menu and in a saved view; saying it is window-only is what keeps a\n * clause on it from being sent to a query that would answer with a\n * permission error or nothing. The list says which clauses narrowed only\n * the window — see `listFilterPlan`.\n */\n windowOnly?: boolean\n /**\n * A `date` field stored as epoch MILLISECONDS (`…AtMs`) rather than as a\n * Timestamp. The query twins compare numbers for it, and the matcher reads\n * both already (AGL-3321).\n */\n storedAs?: 'timestamp' | 'millis'\n /**\n * The stored value is a PRESENCE MAP — `{ form: true, order: true }` —\n * and the filter asks about its keys.\n *\n * A contact's `sources` is such a map: one key per door the person came\n * in through, never an array. Matched as though it were the array of its\n * truthy keys, so `equals form` and `isAnyOf form,order` read the way a\n * reader expects; a query can serve `equals` as `sources.form == true`.\n */\n keysOf?: boolean\n}\n\n/** The empty-operators a field can honestly offer. See `presence`. */\nconst emptyOperators = (field: ListFilterField): string[] => {\n switch (field.presence ?? 'sparse') {\n case 'always':\n return []\n case 'nullable':\n return ['isEmpty', 'isNotEmpty']\n default:\n return ['isNotEmpty']\n }\n}\n\n/** The operators a field offers, derived from the paths it actually has. */\nexport function listFilterOperators(field: ListFilterField): string[] {\n if (field.operators) return [...field.operators]\n const empties = emptyOperators(field)\n switch (field.kind) {\n case 'text':\n return [\n ...(field.tokensPath ? ['contains'] : []),\n ...(field.lowerPath ? ['equals', 'startsWith'] : []),\n ...(field.reversedPath ? ['endsWith'] : []),\n ...empties,\n ]\n case 'exact':\n return ['equals', 'isAnyOf', ...empties]\n case 'id':\n // An id is never absent, so the empty operators would answer nothing.\n return ['equals', 'startsWith', 'isAnyOf']\n case 'boolean':\n return ['is']\n case 'number':\n return ['=', '!=', '>', '>=', '<', '<=', ...empties]\n case 'date':\n return ['is', 'after', 'onOrAfter', 'before', 'onOrBefore', ...empties]\n default:\n return []\n }\n}\n\n/** What a page sends the route: one field, one operator, one value. */\nexport interface ListFilterRequest {\n field: string\n op: string\n value: string\n}\n\n/** Operators that carry no value — the grid leaves `item.value` undefined. */\nconst VALUELESS = ['isEmpty', 'isNotEmpty']\n\n/**\n * The one filter item a server-paged list can act on, read off a grid model.\n *\n * ONE, because Firestore composes predicates only through indexes nobody built\n * for every pair of columns; the first usable item wins and the rest are the\n * reader's to remove. Every list read this off the model itself, and each did\n * it slightly differently — the common miss being `isEmpty`, which sets no\n * value at all and so was skipped by any check that required one.\n */\nexport function gridFilterRequest(model: {\n items?: Array<{ field?: unknown; operator?: unknown; value?: unknown }>\n}): ListFilterRequest | null {\n const usable = (model.items ?? []).find((item) => {\n if (!item.field || !item.operator) return false\n if (VALUELESS.includes(String(item.operator))) return true\n if (item.value === undefined || item.value === null) return false\n return Array.isArray(item.value)\n ? item.value.length > 0\n : String(item.value).trim() !== ''\n })\n if (!usable) return null\n return gridItemRequest(usable)\n}\n\n/** Whether a grid filter item asks anything at all — see `gridFilterRequest`. */\nconst gridItemUsable = (item: {\n field?: unknown\n operator?: unknown\n value?: unknown\n}): boolean => {\n if (!item.field || !item.operator) return false\n if (VALUELESS.includes(String(item.operator))) return true\n if (item.value === undefined || item.value === null) return false\n return Array.isArray(item.value)\n ? item.value.length > 0\n : String(item.value).trim() !== ''\n}\n\nconst gridItemRequest = (item: {\n field?: unknown\n operator?: unknown\n value?: unknown\n}): ListFilterRequest => {\n const raw = item.value\n const value = Array.isArray(raw)\n ? raw.join(',')\n : // A date column hands back a `Date`, whose default string form is a\n // locale rendering the server would have to guess at. ISO travels\n // unambiguously and `new Date` reads it back exactly.\n raw instanceof Date\n ? raw.toISOString()\n : raw === undefined || raw === null\n ? ''\n : String(raw)\n return { field: String(item.field), op: String(item.operator), value }\n}\n\n/**\n * EVERY usable filter item on a grid model, in the order the panel holds\n * them — for a list that keeps more than one clause (AGL-2617).\n *\n * The singular reader above picks the first because a query serves one;\n * a saved view carries a list of clauses and a list that applies it puts\n * the first servable one on the query and matches the rest over the rows\n * it loaded, which `listFilterPlan` in the instance library decides. The\n * two readers share one notion of \"usable\", so a clause the singular one\n * would take is a clause this one keeps.\n */\nexport function gridFilterRequests(model: {\n items?: Array<{ field?: unknown; operator?: unknown; value?: unknown }>\n}): ListFilterRequest[] {\n return (model.items ?? []).filter(gridItemUsable).map(gridItemRequest)\n}\n\nexport { listFilterDay, listFilterOperatorLabel } from './list-filter-sentence'\n\nconst readPath = (row: unknown, path: string): unknown =>\n path\n .split('.')\n .reduce<any>(\n (value, key) => (value == null ? undefined : value[key]),\n row as any,\n )\n\nconst asTime = (value: unknown): number | null => {\n if (value == null) return null\n if (value instanceof Date) return value.getTime()\n if (typeof value === 'number') return value\n // A Firestore timestamp, in either the admin shape or the serialized one.\n const seconds = (value as { seconds?: unknown }).seconds\n if (typeof seconds === 'number') return seconds * 1000\n const parsed = new Date(String(value)).getTime()\n return Number.isNaN(parsed) ? null : parsed\n}\n\n\n/**\n * Match ONE row against a filter, for a list that cannot push it to a query.\n *\n * The staff account list is the reason this exists: it is Firebase Auth, whose\n * `listUsers` takes a page size and a cursor and nothing else — no predicate,\n * no ordering, no search. A filter there is answered by walking the pools and\n * matching here, which is why that list can offer a mid-string `contains` and\n * a `doesNotContain` that a Firestore-backed list cannot.\n *\n * The declaration is still the same `ListFilterField`, so a list swapping\n * substrates changes where the matching happens and not what it offers.\n */\nexport function matchListFilter(\n row: unknown,\n fields: readonly ListFilterField[],\n input: ListFilterRequest | null,\n): boolean {\n if (!input) return true\n const field = fields.find((entry) => entry.column === input.field)\n if (!field) return true\n const stored = readPath(row, field.rowPath ?? field.path)\n // A presence map is matched as the array of its truthy keys — see `keysOf`.\n const value =\n field.keysOf && stored && typeof stored === 'object' && !Array.isArray(stored)\n ? Object.keys(stored as Record<string, unknown>).filter((key) =>\n Boolean((stored as Record<string, unknown>)[key]),\n )\n : stored\n const op = input.op\n\n const blank =\n value == null || value === '' || (Array.isArray(value) && value.length === 0)\n if (op === 'isEmpty') return blank\n if (op === 'isNotEmpty') return !blank\n\n const raw = (input.value ?? '').trim()\n if (field.kind === 'boolean') {\n if (raw !== 'true' && raw !== 'false') return true\n return Boolean(value) === (raw === 'true')\n }\n if (!raw) return true\n\n /*\n * An array the QUERY serves whole is matched by whole member, never by\n * substring of its joined form. A tag array asked `contains vip` holds the\n * tag `vip` or it does not — `vip-gold` is a different tag, and the\n * query's `array-contains` says so too, so the in-memory answer and the\n * served answer agree. An id array keeps its case (`verbatimTokens`); a\n * word array is folded the way its writer folded it. `isAnyOf` is any\n * member in the asked list, which is what a saved segment's \"any of these\n * tags\" becomes as a view clause. A presence map's keys are members too.\n *\n * A plain array with no token path is served by nothing, so there is no\n * query to agree with, and it falls through to the scalar match over its\n * joined text — the staff account list's providers, where `contains\n * google` finding `google.com` is the mid-string answer that list\n * documents (`operators`).\n */\n if (Array.isArray(value) && (field.tokensPath || field.keysOf)) {\n const fold = (entry: unknown) =>\n field.verbatimTokens ? String(entry) : String(entry).toLowerCase()\n const members = value.map(fold)\n const asked = fold(raw)\n switch (op) {\n case 'contains':\n case 'equals':\n case 'is':\n return members.includes(asked)\n case 'doesNotContain':\n case 'doesNotEqual':\n return !members.includes(asked)\n case 'isAnyOf':\n return asked\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean)\n .some((entry) => members.includes(entry))\n default:\n return true\n }\n }\n\n if (field.kind === 'date') {\n const at = asTime(value)\n const day = listFilterDay(raw)\n if (at === null || Number.isNaN(day.getTime())) return false\n const start = new Date(day)\n start.setHours(0, 0, 0, 0)\n const end = new Date(start)\n end.setDate(end.getDate() + 1)\n switch (op) {\n // A day, not an instant — a stored time never equals midnight.\n case 'is':\n return at >= start.getTime() && at < end.getTime()\n case 'after':\n return at >= end.getTime()\n case 'onOrAfter':\n return at >= start.getTime()\n case 'before':\n return at < start.getTime()\n case 'onOrBefore':\n return at < end.getTime()\n default:\n return true\n }\n }\n\n if (field.kind === 'number') {\n const asked = Number(raw)\n const held = Number(value)\n if (!Number.isFinite(asked) || !Number.isFinite(held)) return false\n switch (op) {\n case '=':\n return held === asked\n case '!=':\n return held !== asked\n case '>':\n return held > asked\n case '>=':\n return held >= asked\n case '<':\n return held < asked\n case '<=':\n return held <= asked\n default:\n return true\n }\n }\n\n // A scalar id is matched as typed — see `verbatimTokens`; the array case\n // was answered above.\n if (field.verbatimTokens && op === 'contains') return value === raw\n const held = value == null ? '' : String(value).toLowerCase()\n const asked = raw.toLowerCase()\n switch (op) {\n case 'contains':\n return held.includes(asked)\n case 'doesNotContain':\n return !held.includes(asked)\n case 'equals':\n return held === asked\n case 'doesNotEqual':\n return held !== asked\n case 'startsWith':\n return held.startsWith(asked)\n case 'endsWith':\n return held.endsWith(asked)\n case 'isAnyOf':\n return asked\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean)\n .includes(held)\n default:\n // An operator nothing here answers must not narrow the list to\n // nothing — that reads as \"no such record\" rather than \"not supported\".\n return true\n }\n}\n\n/** The visibility model that hides what {@link hiddenFilterColumns} added. */\nexport function hiddenFilterVisibility(\n fields: readonly ListFilterField[],\n visible: readonly string[],\n): Record<string, boolean> {\n return Object.fromEntries(\n fields\n .filter((field) => !visible.includes(field.column))\n .map((field) => [field.column, false]),\n )\n}\n"],"names":["listFilterDay","emptyOperators","field","presence","listFilterOperators","operators","empties","kind","tokensPath","lowerPath","reversedPath","VALUELESS","gridFilterRequest","model","usable","items","find","item","operator","includes","String","value","undefined","Array","isArray","length","trim","gridItemRequest","gridItemUsable","raw","join","Date","toISOString","op","gridFilterRequests","filter","map","listFilterOperatorLabel","readPath","row","path","split","reduce","key","asTime","getTime","seconds","parsed","Number","isNaN","matchListFilter","fields","input","entry","column","stored","rowPath","keysOf","Object","keys","Boolean","blank","fold","verbatimTokens","toLowerCase","members","asked","some","at","day","start","setHours","end","setDate","getDate","held","isFinite","startsWith","endsWith","hiddenFilterVisibility","visible","fromEntries"],"mappings":"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"}
|