@aglyn/shared-ui-jsx 1.0.0-beta.199 → 1.0.0-beta.200
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-filter-chips.component.d.ts +2 -2
- package/src/lib/components/list-filter-chips.component.js +6 -28
- package/src/lib/components/list-filter-chips.component.js.map +1 -1
- package/src/lib/components/list-filter-toolbar.component.d.ts +56 -0
- package/src/lib/components/list-filter-toolbar.component.js +280 -0
- package/src/lib/components/list-filter-toolbar.component.js.map +1 -0
- package/src/lib/components/list-query-notices.component.d.ts +18 -0
- package/src/lib/components/list-query-notices.component.js +11 -0
- package/src/lib/components/list-query-notices.component.js.map +1 -1
- package/src/lib/components/list-table.component.d.ts +16 -0
- package/src/lib/components/list-table.component.js +57 -8
- package/src/lib/components/list-table.component.js.map +1 -1
- package/src/lib/const/list-filter-sentence.d.ts +39 -0
- package/src/lib/const/list-filter-sentence.js +107 -0
- package/src/lib/const/list-filter-sentence.js.map +1 -0
- package/src/lib/const/list-filter.d.ts +8 -46
- package/src/lib/const/list-filter.js +54 -113
- package/src/lib/const/list-filter.js.map +1 -1
- package/src/lib/const/list-grid-filter.d.ts +39 -28
- package/src/lib/const/list-grid-filter.js +59 -71
- package/src/lib/const/list-grid-filter.js.map +1 -1
- package/src/lib/const/list-query-plan.d.ts +5 -0
- package/src/lib/const/list-query-plan.js.map +1 -1
- package/src/lib/hooks/use-list-grid-filter.d.ts +7 -11
- package/src/lib/hooks/use-list-grid-filter.js +11 -12
- package/src/lib/hooks/use-list-grid-filter.js.map +1 -1
- package/src/lib/hooks/use-list-rows-filter.d.ts +0 -78
- package/src/lib/hooks/use-list-rows-filter.js +0 -86
- package/src/lib/hooks/use-list-rows-filter.js.map +0 -1
- package/src/lib/hooks/use-paged-rows-filter.d.ts +0 -80
- package/src/lib/hooks/use-paged-rows-filter.js +0 -129
- package/src/lib/hooks/use-paged-rows-filter.js.map +0 -1
|
@@ -166,6 +166,38 @@ function ListQuickButton(props) {
|
|
|
166
166
|
renderCell: ({ row })=>render(row)
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
|
+
const NO_ROWS = [];
|
|
170
|
+
/** No column chooser and no Export: the columns and rows are drawn elsewhere. */ const TOOLBAR_ONLY_PROPS = {
|
|
171
|
+
disableColumnSelector: true
|
|
172
|
+
};
|
|
173
|
+
/** Nothing, for an overlay a toolbar-only grid has no body to draw in. */ const NoOverlay = ()=>null;
|
|
174
|
+
/** A toolbar-only grid's overlays: its body is collapsed, so they are too. */ const TOOLBAR_ONLY_SLOTS = {
|
|
175
|
+
noRowsOverlay: NoOverlay,
|
|
176
|
+
noResultsOverlay: NoOverlay,
|
|
177
|
+
loadingOverlay: NoOverlay
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* The body of a toolbar-only grid, collapsed to nothing.
|
|
181
|
+
*
|
|
182
|
+
* Everything in the main container goes except the panel anchor, which the
|
|
183
|
+
* Filters panel positions itself against: the anchor sits at the top of the
|
|
184
|
+
* collapsed body, which is directly under the toolbar, where the panel opens
|
|
185
|
+
* on every other list. The body keeps no height, so the virtualized rows it
|
|
186
|
+
* would draw into have none to fill.
|
|
187
|
+
*/ const TOOLBAR_ONLY_SX = {
|
|
188
|
+
'& .MuiDataGrid-main': {
|
|
189
|
+
flex: '0 0 0px',
|
|
190
|
+
height: 0,
|
|
191
|
+
minHeight: 0,
|
|
192
|
+
overflow: 'visible'
|
|
193
|
+
},
|
|
194
|
+
'& .MuiDataGrid-main > :not([data-id="gridPanelAnchor"])': {
|
|
195
|
+
display: 'none'
|
|
196
|
+
},
|
|
197
|
+
'& [data-id="gridPanelAnchor"]': {
|
|
198
|
+
top: 0
|
|
199
|
+
}
|
|
200
|
+
};
|
|
169
201
|
/**
|
|
170
202
|
* The grid every FLAT artifact list uses — layouts, components, templates.
|
|
171
203
|
*
|
|
@@ -174,7 +206,7 @@ function ListQuickButton(props) {
|
|
|
174
206
|
* reader actually compares.
|
|
175
207
|
*/ export function ListTable(props) {
|
|
176
208
|
var _ref;
|
|
177
|
-
const { onOpen, sx, initialState, hideFooter, rows, selectable, quickFilter, slotProps } = props, rest = _object_without_properties_loose(props, [
|
|
209
|
+
const { onOpen, sx, initialState, hideFooter, rows, selectable, quickFilter, slotProps, toolbarOnly = false, slots } = props, rest = _object_without_properties_loose(props, [
|
|
178
210
|
"onOpen",
|
|
179
211
|
"sx",
|
|
180
212
|
"initialState",
|
|
@@ -182,7 +214,9 @@ function ListQuickButton(props) {
|
|
|
182
214
|
"rows",
|
|
183
215
|
"selectable",
|
|
184
216
|
"quickFilter",
|
|
185
|
-
"slotProps"
|
|
217
|
+
"slotProps",
|
|
218
|
+
"toolbarOnly",
|
|
219
|
+
"slots"
|
|
186
220
|
]);
|
|
187
221
|
const showQuickFilter = quickFilter != null ? quickFilter : rest.filterMode !== 'server';
|
|
188
222
|
/*==========================================
|
|
@@ -236,8 +270,9 @@ function ListQuickButton(props) {
|
|
|
236
270
|
* server-paged list hands over a different number of rows on every page and
|
|
237
271
|
* initial state is read once. A floor of one because MUI rejects a page
|
|
238
272
|
* size of zero, and an empty grid has nothing to slice anyway.
|
|
239
|
-
*=========================================*/ const rowCount = (_ref = rows == null ? void 0 : rows.length) != null ? _ref : 0;
|
|
240
|
-
const
|
|
273
|
+
*=========================================*/ const rowCount = toolbarOnly ? 0 : (_ref = rows == null ? void 0 : rows.length) != null ? _ref : 0;
|
|
274
|
+
const footerHidden = hideFooter || toolbarOnly;
|
|
275
|
+
const unpaged = footerHidden ? {
|
|
241
276
|
paginationModel: {
|
|
242
277
|
page: 0,
|
|
243
278
|
pageSize: Math.max(rowCount, 1)
|
|
@@ -311,6 +346,9 @@ function ListQuickButton(props) {
|
|
|
311
346
|
alignItems: 'center'
|
|
312
347
|
}
|
|
313
348
|
},
|
|
349
|
+
...toolbarOnly ? [
|
|
350
|
+
TOOLBAR_ONLY_SX
|
|
351
|
+
] : [],
|
|
314
352
|
...Array.isArray(sx) ? sx : sx ? [
|
|
315
353
|
sx
|
|
316
354
|
] : []
|
|
@@ -354,14 +392,25 @@ function ListQuickButton(props) {
|
|
|
354
392
|
*/ slotProps: _extends({}, slotProps, {
|
|
355
393
|
toolbar: _extends({
|
|
356
394
|
showQuickFilter
|
|
357
|
-
}, slotProps == null ? void 0 : slotProps.toolbar
|
|
395
|
+
}, slotProps == null ? void 0 : slotProps.toolbar, toolbarOnly ? {
|
|
396
|
+
csvOptions: {
|
|
397
|
+
disableToolbarButton: true
|
|
398
|
+
},
|
|
399
|
+
printOptions: {
|
|
400
|
+
disableToolbarButton: true
|
|
401
|
+
}
|
|
402
|
+
} : {}),
|
|
358
403
|
pagination: _extends({
|
|
359
404
|
labelRowsPerPage: TABLE_ROWS_PER_PAGE_LABEL
|
|
360
405
|
}, slotProps == null ? void 0 : slotProps.pagination)
|
|
361
406
|
}),
|
|
362
|
-
rows:
|
|
363
|
-
|
|
364
|
-
|
|
407
|
+
// A toolbar-only grid holds no rows: the reader sees them some other
|
|
408
|
+
// way, and rows laid out under a collapsed body would be drawn for no
|
|
409
|
+
// one.
|
|
410
|
+
rows: toolbarOnly ? NO_ROWS : rows,
|
|
411
|
+
hideFooter: footerHidden,
|
|
412
|
+
slots: toolbarOnly ? _extends({}, slots, TOOLBAR_ONLY_SLOTS) : slots
|
|
413
|
+
}, toolbarOnly ? TOOLBAR_ONLY_PROPS : {}, rest, unpaged));
|
|
365
414
|
}
|
|
366
415
|
ListTable.displayName = 'ListTable';
|
|
367
416
|
export default ListTable;
|
|
@@ -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\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 ...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 = rows?.length ?? 0\n const unpaged = hideFooter\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 ...(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: { showQuickFilter, ...slotProps?.toolbar },\n pagination: {\n labelRowsPerPage: TABLE_ROWS_PER_PAGE_LABEL,\n ...slotProps?.pagination,\n },\n }}\n rows={rows}\n hideFooter={hideFooter}\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","ListTable","onOpen","initialState","hideFooter","rows","selectable","quickFilter","slotProps","rest","showQuickFilter","filterMode","selection","rowSelection","checkboxSelection","rowSelectionModel","type","ids","Set","selected","onRowSelectionModelChange","model","all","map","String","$id","onChange","filter","id","has","rowCount","length","unpaged","paginationModel","page","pageSize","Math","max","onPaginationModelChange","pageSizeOptions","getRowId","disableRowSelectionOnClick","onRowClick","closest","autoHeight","cursor","display","Array","isArray","showToolbar","pagination","columns","toolbar","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 * 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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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import type { ListFilterField } from './list-filter';
|
|
18
|
+
import type { ListFilterClause, ListFilterOption } from './list-grid-filter';
|
|
19
|
+
/**
|
|
20
|
+
* How an operator reads on a chip or in a sentence — "Owner is Dana",
|
|
21
|
+
* "Created on or after 1 Jan". One map so every list that shows its
|
|
22
|
+
* clauses back spells them the same way; an operator nothing here names
|
|
23
|
+
* is shown as itself rather than hidden.
|
|
24
|
+
*/
|
|
25
|
+
export declare function listFilterOperatorLabel(op: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* How a clause reads as a sentence — "Owner is Dana", "Created on or after
|
|
28
|
+
* 1 Jan", "Reason is any of Bounced, Complained": its header, its operator,
|
|
29
|
+
* and its value by the label of the option it names.
|
|
30
|
+
*
|
|
31
|
+
* ONE wording, used by the chips over a list and by the notices that say a
|
|
32
|
+
* clause was not applied (`listQueryRefusals`), so the two cannot describe
|
|
33
|
+
* the same clause differently.
|
|
34
|
+
*/
|
|
35
|
+
export declare function listFilterClauseSentence(clause: ListFilterClause, context: {
|
|
36
|
+
fields: readonly ListFilterField[];
|
|
37
|
+
headers?: Readonly<Record<string, string>>;
|
|
38
|
+
options?: Readonly<Record<string, readonly ListFilterOption[]>>;
|
|
39
|
+
}): string;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /*
|
|
17
|
+
* How a list's filter clauses read as words, for the chips over a list and
|
|
18
|
+
* the notices that say a clause was not applied (AGL-3321).
|
|
19
|
+
*
|
|
20
|
+
* ⛔ No `@mui/x-data-grid` here, and no runtime import of `list-filter` or
|
|
21
|
+
* `list-grid-filter`, which build the data grid's operators. The storefront's
|
|
22
|
+
* product grid shows these notices on customers' published pages, and
|
|
23
|
+
* reaching the grid's operators from here would ship the grid's filter
|
|
24
|
+
* inputs with every storefront page.
|
|
25
|
+
*/ /**
|
|
26
|
+
* How an operator reads on a chip or in a sentence — "Owner is Dana",
|
|
27
|
+
* "Created on or after 1 Jan". One map so every list that shows its
|
|
28
|
+
* clauses back spells them the same way; an operator nothing here names
|
|
29
|
+
* is shown as itself rather than hidden.
|
|
30
|
+
*/ export function listFilterOperatorLabel(op) {
|
|
31
|
+
switch(op){
|
|
32
|
+
case 'contains':
|
|
33
|
+
return 'contains';
|
|
34
|
+
case 'doesNotContain':
|
|
35
|
+
return 'does not contain';
|
|
36
|
+
case 'equals':
|
|
37
|
+
case 'is':
|
|
38
|
+
case '=':
|
|
39
|
+
return 'is';
|
|
40
|
+
case 'doesNotEqual':
|
|
41
|
+
case '!=':
|
|
42
|
+
return 'is not';
|
|
43
|
+
case 'startsWith':
|
|
44
|
+
return 'starts with';
|
|
45
|
+
case 'endsWith':
|
|
46
|
+
return 'ends with';
|
|
47
|
+
case 'isAnyOf':
|
|
48
|
+
return 'is any of';
|
|
49
|
+
case 'isEmpty':
|
|
50
|
+
return 'is empty';
|
|
51
|
+
case 'isNotEmpty':
|
|
52
|
+
return 'is set';
|
|
53
|
+
case 'after':
|
|
54
|
+
return 'after';
|
|
55
|
+
case 'onOrAfter':
|
|
56
|
+
return 'on or after';
|
|
57
|
+
case 'before':
|
|
58
|
+
return 'before';
|
|
59
|
+
case 'onOrBefore':
|
|
60
|
+
return 'on or before';
|
|
61
|
+
case '>':
|
|
62
|
+
return 'over';
|
|
63
|
+
case '>=':
|
|
64
|
+
return 'at least';
|
|
65
|
+
case '<':
|
|
66
|
+
return 'under';
|
|
67
|
+
case '<=':
|
|
68
|
+
return 'at most';
|
|
69
|
+
default:
|
|
70
|
+
return op;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Operators that carry no value. */ const VALUELESS_OPERATORS = new Set([
|
|
74
|
+
'isEmpty',
|
|
75
|
+
'isNotEmpty'
|
|
76
|
+
]);
|
|
77
|
+
/** Operators that take several values, comma-joined the way the grammar splits them. */ const MULTI_OPERATORS = new Set([
|
|
78
|
+
'isAnyOf'
|
|
79
|
+
]);
|
|
80
|
+
const dayLabel = (raw)=>{
|
|
81
|
+
const at = new Date(raw);
|
|
82
|
+
return Number.isNaN(at.getTime()) ? raw : at.toLocaleDateString();
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* How a clause reads as a sentence — "Owner is Dana", "Created on or after
|
|
86
|
+
* 1 Jan", "Reason is any of Bounced, Complained": its header, its operator,
|
|
87
|
+
* and its value by the label of the option it names.
|
|
88
|
+
*
|
|
89
|
+
* ONE wording, used by the chips over a list and by the notices that say a
|
|
90
|
+
* clause was not applied (`listQueryRefusals`), so the two cannot describe
|
|
91
|
+
* the same clause differently.
|
|
92
|
+
*/ export function listFilterClauseSentence(clause, context) {
|
|
93
|
+
var _ref, _clause_label;
|
|
94
|
+
var _context_headers, _context_options;
|
|
95
|
+
const field = context.fields.find((entry)=>entry.column === clause.field);
|
|
96
|
+
const header = (_ref = (_context_headers = context.headers) == null ? void 0 : _context_headers[clause.field]) != null ? _ref : clause.field;
|
|
97
|
+
const choices = (_context_options = context.options) == null ? void 0 : _context_options[clause.field];
|
|
98
|
+
const named = (value)=>{
|
|
99
|
+
var _ref;
|
|
100
|
+
var _choices_find;
|
|
101
|
+
return (_ref = choices == null ? void 0 : (_choices_find = choices.find((option)=>option.value === value)) == null ? void 0 : _choices_find.label) != null ? _ref : value;
|
|
102
|
+
};
|
|
103
|
+
const value = VALUELESS_OPERATORS.has(clause.op) ? '' : MULTI_OPERATORS.has(clause.op) ? clause.value.split(',').map((entry)=>entry.trim()).filter(Boolean).map(named).join(', ') : (_clause_label = clause.label) != null ? _clause_label : (field == null ? void 0 : field.kind) === 'date' ? dayLabel(clause.value) : named(clause.value);
|
|
104
|
+
return `${header} ${listFilterOperatorLabel(clause.op)}${value ? ` ${value}` : ''}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
//# sourceMappingURL=list-filter-sentence.js.map
|
|
@@ -0,0 +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 dayLabel = (raw: string): string => {\n const at = new Date(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","dayLabel","raw","at","Date","Number","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,MAAME,WAAW,CAACC;IAChB,MAAMC,KAAK,IAAIC,KAAKF;IACpB,OAAOG,OAAOC,KAAK,CAACH,GAAGI,OAAO,MAAML,MAAMC,GAAGK,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,QAAQxB,oBAAoB2B,GAAG,CAACf,OAAOb,EAAE,IAC3C,KACAG,gBAAgByB,GAAG,CAACf,OAAOb,EAAE,IAC3Ba,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,SAAS/B,SAASS,OAAOY,KAAK,IAAID,MAAMX,OAAOY,KAAK;IAC3F,OAAO,GAAGL,OAAO,CAAC,EAAErB,wBAAwBc,OAAOb,EAAE,IAAIyB,QAAQ,CAAC,CAAC,EAAEA,OAAO,GAAG,IAAI;AACrF"}
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import { type GridFilterOperator } from '@mui/x-data-grid';
|
|
18
17
|
/**
|
|
19
18
|
* ONE declaration of what a paged list can be filtered by (AGL-2501).
|
|
20
19
|
*
|
|
@@ -31,6 +30,13 @@ import { type GridFilterOperator } from '@mui/x-data-grid';
|
|
|
31
30
|
* (`applyListFilter`, server-side, which needs firebase-admin and so cannot
|
|
32
31
|
* live in this file).
|
|
33
32
|
*
|
|
33
|
+
* ⛔ No runtime import of `@mui/x-data-grid` here. This grammar is read by
|
|
34
|
+
* `list-query-plan`, which the storefront's server-side catalog query reaches
|
|
35
|
+
* from a published page, and the grid's filter inputs are `'use client'`
|
|
36
|
+
* modules: imported from the server graph they become client references that
|
|
37
|
+
* every published page downloads. The grid's own operator objects are built
|
|
38
|
+
* in `list-grid-filter` (`listFilterColumn`, `hiddenFilterColumns`).
|
|
39
|
+
*
|
|
34
40
|
* ## Why the menu is shorter than MUI's
|
|
35
41
|
*
|
|
36
42
|
* Firestore has equality, `in`, ranges and `array-contains`. Everything below
|
|
@@ -171,22 +177,6 @@ export interface ListFilterField {
|
|
|
171
177
|
}
|
|
172
178
|
/** The operators a field offers, derived from the paths it actually has. */
|
|
173
179
|
export declare function listFilterOperators(field: ListFilterField): string[];
|
|
174
|
-
/**
|
|
175
|
-
* The MUI operators for a field — the same list `listFilterOperators` names,
|
|
176
|
-
* resolved to the grid's own operator objects so the panel keeps its native
|
|
177
|
-
* inputs (a date picker for a date, a number field for a number).
|
|
178
|
-
*/
|
|
179
|
-
export declare function gridFilterOperators(field: ListFilterField): GridFilterOperator[];
|
|
180
|
-
/**
|
|
181
|
-
* Spread onto a `GridColDef` to make a column filterable exactly as far as the
|
|
182
|
-
* query can serve it. A column with NO declared field is turned off entirely —
|
|
183
|
-
* deliberately, because the alternative is a funnel icon that opens a panel
|
|
184
|
-
* nothing honours.
|
|
185
|
-
*/
|
|
186
|
-
export declare function listFilterColumn(fields: readonly ListFilterField[], column: string): {
|
|
187
|
-
filterable: boolean;
|
|
188
|
-
filterOperators?: GridFilterOperator[];
|
|
189
|
-
};
|
|
190
180
|
/** What a page sends the route: one field, one operator, one value. */
|
|
191
181
|
export interface ListFilterRequest {
|
|
192
182
|
field: string;
|
|
@@ -227,35 +217,7 @@ export declare function gridFilterRequests(model: {
|
|
|
227
217
|
value?: unknown;
|
|
228
218
|
}>;
|
|
229
219
|
}): ListFilterRequest[];
|
|
230
|
-
|
|
231
|
-
* How an operator reads on a chip or in a sentence — "Owner is Dana",
|
|
232
|
-
* "Created on or after 1 Jan". One map so every list that shows its
|
|
233
|
-
* clauses back spells them the same way; an operator nothing here names
|
|
234
|
-
* is shown as itself rather than hidden.
|
|
235
|
-
*/
|
|
236
|
-
export declare function listFilterOperatorLabel(op: string): string;
|
|
237
|
-
/**
|
|
238
|
-
* Fields a reader can filter by that are NOT columns on the table.
|
|
239
|
-
*
|
|
240
|
-
* MUI's filter panel lists COLUMNS — `gridFilterableColumnDefinitionsSelector`
|
|
241
|
-
* reads every column definition, hidden ones included — so a filterable field
|
|
242
|
-
* with no column is a field nobody can reach however well the route answers it.
|
|
243
|
-
* Declaring it as a permanently hidden column keeps one source of truth, the
|
|
244
|
-
* field list, instead of a second list of "extra filters" that drifts from it.
|
|
245
|
-
*
|
|
246
|
-
* `hideable: false` keeps them out of Manage columns as well: a column with no
|
|
247
|
-
* `renderCell` and no width has nothing to show, and a reader who unhid one
|
|
248
|
-
* would get a strip of blank cells for their trouble.
|
|
249
|
-
*
|
|
250
|
-
* Pair with {@link hiddenFilterVisibility}, which is what actually hides them.
|
|
251
|
-
*/
|
|
252
|
-
export declare function hiddenFilterColumns(fields: readonly ListFilterField[], visible: readonly string[], headers?: Readonly<Record<string, string>>): Array<{
|
|
253
|
-
field: string;
|
|
254
|
-
headerName: string;
|
|
255
|
-
hideable: boolean;
|
|
256
|
-
filterable: boolean;
|
|
257
|
-
filterOperators?: GridFilterOperator[];
|
|
258
|
-
}>;
|
|
220
|
+
export { listFilterOperatorLabel } from './list-filter-sentence';
|
|
259
221
|
export declare const listFilterDay: (raw: string) => Date;
|
|
260
222
|
/**
|
|
261
223
|
* Match ONE row against a filter, for a list that cannot push it to a query.
|