@aglyn/shared-ui-jsx 1.0.0-beta.186 → 1.0.0-beta.188
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 +57 -0
- package/src/lib/components/list-filter-chips.component.js +93 -0
- package/src/lib/components/list-filter-chips.component.js.map +1 -0
- package/src/lib/const/list-grid-filter.d.ts +90 -0
- package/src/lib/const/list-grid-filter.js +213 -0
- package/src/lib/const/list-grid-filter.js.map +1 -0
- package/src/lib/hooks/use-list-grid-filter.d.ts +85 -0
- package/src/lib/hooks/use-list-grid-filter.js +158 -0
- package/src/lib/hooks/use-list-grid-filter.js.map +1 -0
- package/src/lib/hooks/use-list-rows-filter.d.ts +75 -0
- package/src/lib/hooks/use-list-rows-filter.js +83 -0
- package/src/lib/hooks/use-list-rows-filter.js.map +1 -0
- package/src/lib/hooks/use-paged-rows-filter.d.ts +77 -0
- package/src/lib/hooks/use-paged-rows-filter.js +126 -0
- package/src/lib/hooks/use-paged-rows-filter.js.map +1 -0
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.188",
|
|
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.188",
|
|
29
|
+
"@aglyn/shared-ui-theme": "1.0.0-beta.188",
|
|
30
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.188",
|
|
31
|
+
"@aglyn/shared-util-vendor": "1.0.0-beta.188",
|
|
32
32
|
"@mdi/js": "^7.4.47",
|
|
33
33
|
"@swc/helpers": "0.5.23",
|
|
34
34
|
"change-case": "^5.4.4",
|
|
@@ -0,0 +1,57 @@
|
|
|
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 '../const/list-filter';
|
|
18
|
+
import type { ListFilterClause, ListFilterOption } from '../const/list-grid-filter';
|
|
19
|
+
export interface ListFilterChipsProps {
|
|
20
|
+
/** The list's grammar — every field a clause may name. */
|
|
21
|
+
fields: readonly ListFilterField[];
|
|
22
|
+
/** How a field reads on a chip; a field without one reads as its column. */
|
|
23
|
+
headers: Readonly<Record<string, string>>;
|
|
24
|
+
clauses: readonly ListFilterClause[];
|
|
25
|
+
onChange: (clauses: ListFilterClause[]) => void;
|
|
26
|
+
/** Choices per field, so a chip names a picked value by its label. */
|
|
27
|
+
options?: Readonly<Record<string, readonly ListFilterOption[]>>;
|
|
28
|
+
/**
|
|
29
|
+
* The field the query is serving, marked so the reader knows which reached
|
|
30
|
+
* everything. Omitted on a list whose every clause narrows the same rows.
|
|
31
|
+
*/
|
|
32
|
+
servedField?: string | null;
|
|
33
|
+
/**
|
|
34
|
+
* Whether chips carry the served / loaded-window distinction at all.
|
|
35
|
+
* Defaults to whether `servedField` was passed: a list that answers every
|
|
36
|
+
* clause over the same rows has nothing to distinguish.
|
|
37
|
+
*/
|
|
38
|
+
marksServed?: boolean;
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Every clause a list is narrowed by, as chips over the grid (AGL-2617,
|
|
43
|
+
* AGL-3313; shared since AGL-3317).
|
|
44
|
+
*
|
|
45
|
+
* The clauses are ADDED through the grid's own Filters panel; this bar is
|
|
46
|
+
* what shows the SET. The free DataGrid's panel holds one item at a time
|
|
47
|
+
* and a saved view holds several, so without it a list narrowed by two
|
|
48
|
+
* fields would show one of the two and hide the other. Each
|
|
49
|
+
* chip reads as a sentence — "Owner is Dana", "Created on or after 1 Jan"
|
|
50
|
+
* — and removes its clause; where some clauses reach the query and others
|
|
51
|
+
* narrow the loaded rows, the served one is marked.
|
|
52
|
+
*/
|
|
53
|
+
export declare function ListFilterChips(props: ListFilterChipsProps): import("react").JSX.Element;
|
|
54
|
+
export declare namespace ListFilterChips {
|
|
55
|
+
var displayName: string;
|
|
56
|
+
}
|
|
57
|
+
export default ListFilterChips;
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
*/ 'use client';
|
|
17
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
18
|
+
import { Chip, Stack, Tooltip } from "@mui/material";
|
|
19
|
+
import { listFilterOperatorLabel } from "../const/list-filter.js";
|
|
20
|
+
/** Operators that carry no value. */ const VALUELESS = new Set([
|
|
21
|
+
'isEmpty',
|
|
22
|
+
'isNotEmpty'
|
|
23
|
+
]);
|
|
24
|
+
/** Operators that take several values, comma-joined the way the grammar splits them. */ const MULTI = new Set([
|
|
25
|
+
'isAnyOf'
|
|
26
|
+
]);
|
|
27
|
+
const dayLabel = (raw)=>{
|
|
28
|
+
const at = new Date(raw);
|
|
29
|
+
return Number.isNaN(at.getTime()) ? raw : at.toLocaleDateString();
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Every clause a list is narrowed by, as chips over the grid (AGL-2617,
|
|
33
|
+
* AGL-3313; shared since AGL-3317).
|
|
34
|
+
*
|
|
35
|
+
* The clauses are ADDED through the grid's own Filters panel; this bar is
|
|
36
|
+
* what shows the SET. The free DataGrid's panel holds one item at a time
|
|
37
|
+
* and a saved view holds several, so without it a list narrowed by two
|
|
38
|
+
* fields would show one of the two and hide the other. Each
|
|
39
|
+
* chip reads as a sentence — "Owner is Dana", "Created on or after 1 Jan"
|
|
40
|
+
* — and removes its clause; where some clauses reach the query and others
|
|
41
|
+
* narrow the loaded rows, the served one is marked.
|
|
42
|
+
*/ export function ListFilterChips(props) {
|
|
43
|
+
const { fields, headers, clauses, onChange, options = {}, servedField = null, marksServed = props.servedField !== undefined, disabled = false } = props;
|
|
44
|
+
const header = (column)=>{
|
|
45
|
+
var _headers_column;
|
|
46
|
+
return (_headers_column = headers[column]) != null ? _headers_column : column;
|
|
47
|
+
};
|
|
48
|
+
/** How a clause reads: its header, its operator, and its value by label. */ const sentence = (clause)=>{
|
|
49
|
+
var _clause_label;
|
|
50
|
+
const field = fields.find((entry)=>entry.column === clause.field);
|
|
51
|
+
const choices = options[clause.field];
|
|
52
|
+
const named = (value)=>{
|
|
53
|
+
var _ref;
|
|
54
|
+
var _choices_find;
|
|
55
|
+
return (_ref = choices == null ? void 0 : (_choices_find = choices.find((option)=>option.value === value)) == null ? void 0 : _choices_find.label) != null ? _ref : value;
|
|
56
|
+
};
|
|
57
|
+
const value = VALUELESS.has(clause.op) ? '' : MULTI.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);
|
|
58
|
+
return `${header(clause.field)} ${listFilterOperatorLabel(clause.op)}${value ? ` ${value}` : ''}`;
|
|
59
|
+
};
|
|
60
|
+
if (!clauses.length) return null;
|
|
61
|
+
return /*#__PURE__*/ _jsx(Stack, {
|
|
62
|
+
direction: "row",
|
|
63
|
+
spacing: 1,
|
|
64
|
+
useFlexGap: true,
|
|
65
|
+
sx: {
|
|
66
|
+
alignItems: 'center',
|
|
67
|
+
flexWrap: 'wrap'
|
|
68
|
+
},
|
|
69
|
+
role: "list",
|
|
70
|
+
"aria-label": "Filters",
|
|
71
|
+
children: clauses.map((clause, index)=>{
|
|
72
|
+
const served = marksServed && servedField !== null && clause.field === servedField;
|
|
73
|
+
const key = `${clause.field}-${clause.op}-${index}`;
|
|
74
|
+
const chip = /*#__PURE__*/ _jsx(Chip, {
|
|
75
|
+
role: "listitem",
|
|
76
|
+
size: "small",
|
|
77
|
+
variant: served ? 'filled' : 'outlined',
|
|
78
|
+
color: served ? 'primary' : 'default',
|
|
79
|
+
label: sentence(clause),
|
|
80
|
+
disabled: disabled,
|
|
81
|
+
onDelete: ()=>onChange(clauses.filter((_entry, at)=>at !== index))
|
|
82
|
+
}, key);
|
|
83
|
+
return marksServed ? /*#__PURE__*/ _jsx(Tooltip, {
|
|
84
|
+
title: served ? 'Searched across every record' : 'Narrows the records already loaded',
|
|
85
|
+
children: chip
|
|
86
|
+
}, key) : chip;
|
|
87
|
+
})
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
ListFilterChips.displayName = 'ListFilterChips';
|
|
91
|
+
export default ListFilterChips;
|
|
92
|
+
|
|
93
|
+
//# sourceMappingURL=list-filter-chips.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/list-filter-chips.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\n'use client'\n\nimport { Chip, Stack, Tooltip } from '@mui/material'\nimport { type ListFilterField, listFilterOperatorLabel } from '../const/list-filter'\nimport type { ListFilterClause, ListFilterOption } from '../const/list-grid-filter'\n\nexport interface ListFilterChipsProps {\n /** The list's grammar — every field a clause may name. */\n fields: readonly ListFilterField[]\n /** How a field reads on a chip; a field without one reads as its column. */\n headers: Readonly<Record<string, string>>\n clauses: readonly ListFilterClause[]\n onChange: (clauses: ListFilterClause[]) => void\n /** Choices per field, so a chip names a picked value by its label. */\n options?: Readonly<Record<string, readonly ListFilterOption[]>>\n /**\n * The field the query is serving, marked so the reader knows which reached\n * everything. Omitted on a list whose every clause narrows the same rows.\n */\n servedField?: string | null\n /**\n * Whether chips carry the served / loaded-window distinction at all.\n * Defaults to whether `servedField` was passed: a list that answers every\n * clause over the same rows has nothing to distinguish.\n */\n marksServed?: boolean\n disabled?: boolean\n}\n\n/** Operators that carry no value. */\nconst VALUELESS = new Set(['isEmpty', 'isNotEmpty'])\n/** Operators that take several values, comma-joined the way the grammar splits them. */\nconst MULTI = 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 * Every clause a list is narrowed by, as chips over the grid (AGL-2617,\n * AGL-3313; shared since AGL-3317).\n *\n * The clauses are ADDED through the grid's own Filters panel; this bar is\n * what shows the SET. The free DataGrid's panel holds one item at a time\n * and a saved view holds several, so without it a list narrowed by two\n * fields would show one of the two and hide the other. Each\n * chip reads as a sentence — \"Owner is Dana\", \"Created on or after 1 Jan\"\n * — and removes its clause; where some clauses reach the query and others\n * narrow the loaded rows, the served one is marked.\n */\nexport function ListFilterChips(props: ListFilterChipsProps) {\n const {\n fields,\n headers,\n clauses,\n onChange,\n options = {},\n servedField = null,\n marksServed = props.servedField !== undefined,\n disabled = false,\n } = props\n const header = (column: string) => headers[column] ?? column\n\n /** How a clause reads: its header, its operator, and its value by label. */\n const sentence = (clause: ListFilterClause): string => {\n const field = fields.find((entry) => entry.column === clause.field)\n const choices = options[clause.field]\n const named = (value: string) =>\n choices?.find((option) => option.value === value)?.label ?? value\n const value = VALUELESS.has(clause.op)\n ? ''\n : MULTI.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(clause.field)} ${listFilterOperatorLabel(clause.op)}${value ? ` ${value}` : ''}`\n }\n\n if (!clauses.length) return null\n return (\n <Stack\n direction=\"row\"\n spacing={1}\n useFlexGap\n sx={{ alignItems: 'center', flexWrap: 'wrap' }}\n role=\"list\"\n aria-label=\"Filters\"\n >\n {clauses.map((clause, index) => {\n const served = marksServed && servedField !== null && clause.field === servedField\n const key = `${clause.field}-${clause.op}-${index}`\n const chip = (\n <Chip\n key={key}\n role=\"listitem\"\n size=\"small\"\n variant={served ? 'filled' : 'outlined'}\n color={served ? 'primary' : 'default'}\n label={sentence(clause)}\n disabled={disabled}\n onDelete={() => onChange(clauses.filter((_entry, at) => at !== index))}\n />\n )\n return marksServed ? (\n <Tooltip\n key={key}\n title={served ? 'Searched across every record' : 'Narrows the records already loaded'}\n >\n {chip}\n </Tooltip>\n ) : (\n chip\n )\n })}\n </Stack>\n )\n}\nListFilterChips.displayName = 'ListFilterChips'\n\nexport default ListFilterChips\n"],"names":["Chip","Stack","Tooltip","listFilterOperatorLabel","VALUELESS","Set","MULTI","dayLabel","raw","at","Date","Number","isNaN","getTime","toLocaleDateString","ListFilterChips","props","fields","headers","clauses","onChange","options","servedField","marksServed","undefined","disabled","header","column","sentence","clause","field","find","entry","choices","named","value","option","label","has","op","split","map","trim","filter","Boolean","join","kind","length","direction","spacing","useFlexGap","sx","alignItems","flexWrap","role","aria-label","index","served","key","chip","size","variant","color","onDelete","_entry","title","displayName"],"mappings":"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"}
|
|
@@ -0,0 +1,90 @@
|
|
|
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 GridColDef, type GridFilterItem } from '@mui/x-data-grid';
|
|
18
|
+
import { type ListFilterField, type ListFilterRequest } from './list-filter';
|
|
19
|
+
/** One clause a list is narrowed by; `label` names a picked value on a chip. */
|
|
20
|
+
export interface ListFilterClause extends ListFilterRequest {
|
|
21
|
+
label?: string;
|
|
22
|
+
}
|
|
23
|
+
/** A choice for a field whose value is picked rather than typed. */
|
|
24
|
+
export interface ListFilterOption {
|
|
25
|
+
value: string;
|
|
26
|
+
label: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* A field of a list that holds its rows and answers the panel in memory.
|
|
30
|
+
*
|
|
31
|
+
* The grammar's derived operators describe what a FIRESTORE query can
|
|
32
|
+
* serve, so a text field with no lower-case twin offers nothing. A list
|
|
33
|
+
* that matches over rows it already has is not held to that: plain
|
|
34
|
+
* JavaScript answers a mid-string `contains` and both empty operators, so
|
|
35
|
+
* the field names them (`operators`), and `matchListFilter` answers them.
|
|
36
|
+
* `select` is a field picked from choices — the list passes them as the
|
|
37
|
+
* field's options, and the panel shows a select over them.
|
|
38
|
+
*/
|
|
39
|
+
export declare function inMemoryListField(column: string, kind: 'text' | 'select' | 'number' | 'date' | 'boolean', path?: string): ListFilterField;
|
|
40
|
+
/**
|
|
41
|
+
* How one field's stored clause and the panel's item translate. A field
|
|
42
|
+
* with choices uses {@link listSelectCodec}; a typed one,
|
|
43
|
+
* {@link listPlainCodec}. A list whose clauses predate the panel supplies
|
|
44
|
+
* its own.
|
|
45
|
+
*/
|
|
46
|
+
export interface ListGridFilterCodec {
|
|
47
|
+
toItem: (clause: ListFilterClause) => Pick<GridFilterItem, 'operator' | 'value'> | null;
|
|
48
|
+
toClause: (item: GridFilterItem) => ListFilterClause | null;
|
|
49
|
+
}
|
|
50
|
+
/** A typed field: the stored operator IS the grid's. */
|
|
51
|
+
export declare const listPlainCodec: ListGridFilterCodec;
|
|
52
|
+
/** A picked field: `equals` shows as `is`, and `isAnyOf` holds a comma list. */
|
|
53
|
+
export declare const listSelectCodec: ListGridFilterCodec;
|
|
54
|
+
/** The select operators a field's allowed clause operators map to. */
|
|
55
|
+
export declare function listSelectOperators(allowed: readonly string[]): import("@mui/x-data-grid").GridFilterOperator<any, any, any, import("@mui/x-data-grid").GridFilterInputValueProps>[];
|
|
56
|
+
/**
|
|
57
|
+
* The list's columns, each made filterable exactly as far as its field is
|
|
58
|
+
* declared, and every declared field that is not a column added as a
|
|
59
|
+
* permanently hidden one — the panel lists columns, so a field with none
|
|
60
|
+
* could not be reached (`hiddenFilterColumns`).
|
|
61
|
+
*
|
|
62
|
+
* A field with choices becomes a `singleSelect` over them, whatever the
|
|
63
|
+
* column rendered before; one without keeps the grammar's operators; a
|
|
64
|
+
* column no field declares offers no filter, because the list, not the
|
|
65
|
+
* grid, answers every filter and would have nothing to answer it with.
|
|
66
|
+
*/
|
|
67
|
+
export declare function listFilterGridColumns(columns: readonly GridColDef[], fields: readonly ListFilterField[], options?: Readonly<Record<string, readonly ListFilterOption[]>>, headers?: Readonly<Record<string, string>>): GridColDef[];
|
|
68
|
+
/**
|
|
69
|
+
* Whether a row answers a quick search: every whitespace-separated word
|
|
70
|
+
* appears, case-insensitively, in SOME of the row's searched values. A
|
|
71
|
+
* blank search matches every row, so an emptied box is the list unfiltered.
|
|
72
|
+
* An array value (tags) is searched member by member.
|
|
73
|
+
*/
|
|
74
|
+
export declare function listRowMatchesSearch(row: object, paths: readonly string[], words: readonly string[]): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* The rows that answer every clause and the quick search — for a list that
|
|
77
|
+
* holds its whole data set (or the window its query already narrowed) and
|
|
78
|
+
* so answers the panel itself. A clause `skip` names is one the query
|
|
79
|
+
* already served, and is not matched again.
|
|
80
|
+
*/
|
|
81
|
+
export declare function filterListRows<Row extends object>(rows: readonly Row[], fields: readonly ListFilterField[], clauses: readonly ListFilterClause[], search: {
|
|
82
|
+
paths: readonly string[];
|
|
83
|
+
words: readonly string[];
|
|
84
|
+
}, skip?: (clause: ListFilterClause) => boolean): Row[];
|
|
85
|
+
/**
|
|
86
|
+
* The clauses with one field's clause set: replaced in place where the field
|
|
87
|
+
* already had one, appended where it did not, removed when `next` is null.
|
|
88
|
+
* A field holds ONE clause, which is what the panel edits.
|
|
89
|
+
*/
|
|
90
|
+
export declare function upsertListFilterClause<Clause extends ListFilterClause>(clauses: readonly Clause[], field: string, next: Clause | null): Clause[];
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { getGridSingleSelectOperators } from "@mui/x-data-grid";
|
|
18
|
+
import { gridFilterRequests, hiddenFilterColumns, listFilterColumn, listFilterOperators, matchListFilter } from "./list-filter.js";
|
|
19
|
+
/**
|
|
20
|
+
* A field of a list that holds its rows and answers the panel in memory.
|
|
21
|
+
*
|
|
22
|
+
* The grammar's derived operators describe what a FIRESTORE query can
|
|
23
|
+
* serve, so a text field with no lower-case twin offers nothing. A list
|
|
24
|
+
* that matches over rows it already has is not held to that: plain
|
|
25
|
+
* JavaScript answers a mid-string `contains` and both empty operators, so
|
|
26
|
+
* the field names them (`operators`), and `matchListFilter` answers them.
|
|
27
|
+
* `select` is a field picked from choices — the list passes them as the
|
|
28
|
+
* field's options, and the panel shows a select over them.
|
|
29
|
+
*/ export function inMemoryListField(column, kind, path = column) {
|
|
30
|
+
switch(kind){
|
|
31
|
+
case 'text':
|
|
32
|
+
return {
|
|
33
|
+
column,
|
|
34
|
+
path,
|
|
35
|
+
kind: 'text',
|
|
36
|
+
operators: [
|
|
37
|
+
'contains',
|
|
38
|
+
'doesNotContain',
|
|
39
|
+
'equals',
|
|
40
|
+
'startsWith',
|
|
41
|
+
'endsWith',
|
|
42
|
+
'isEmpty',
|
|
43
|
+
'isNotEmpty'
|
|
44
|
+
]
|
|
45
|
+
};
|
|
46
|
+
case 'select':
|
|
47
|
+
return {
|
|
48
|
+
column,
|
|
49
|
+
path,
|
|
50
|
+
kind: 'exact',
|
|
51
|
+
operators: [
|
|
52
|
+
'equals',
|
|
53
|
+
'doesNotEqual',
|
|
54
|
+
'isAnyOf'
|
|
55
|
+
]
|
|
56
|
+
};
|
|
57
|
+
case 'number':
|
|
58
|
+
return {
|
|
59
|
+
column,
|
|
60
|
+
path,
|
|
61
|
+
kind: 'number',
|
|
62
|
+
presence: 'nullable'
|
|
63
|
+
};
|
|
64
|
+
case 'date':
|
|
65
|
+
return {
|
|
66
|
+
column,
|
|
67
|
+
path,
|
|
68
|
+
kind: 'date',
|
|
69
|
+
presence: 'nullable'
|
|
70
|
+
};
|
|
71
|
+
default:
|
|
72
|
+
return {
|
|
73
|
+
column,
|
|
74
|
+
path,
|
|
75
|
+
kind: 'boolean'
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/** The grid operator each stored select operator shows as. */ const SELECT_TO_GRID = {
|
|
80
|
+
equals: 'is',
|
|
81
|
+
doesNotEqual: 'not',
|
|
82
|
+
isAnyOf: 'isAnyOf'
|
|
83
|
+
};
|
|
84
|
+
const SELECT_FROM_GRID = {
|
|
85
|
+
is: 'equals',
|
|
86
|
+
not: 'doesNotEqual',
|
|
87
|
+
isAnyOf: 'isAnyOf'
|
|
88
|
+
};
|
|
89
|
+
/** The panel's item as a clause, through the grammar's one notion of "usable". */ const plainClause = (item)=>{
|
|
90
|
+
var _gridFilterRequests_;
|
|
91
|
+
return (_gridFilterRequests_ = gridFilterRequests({
|
|
92
|
+
items: [
|
|
93
|
+
item
|
|
94
|
+
]
|
|
95
|
+
})[0]) != null ? _gridFilterRequests_ : null;
|
|
96
|
+
};
|
|
97
|
+
/** A typed field: the stored operator IS the grid's. */ export const listPlainCodec = {
|
|
98
|
+
toItem: (clause)=>({
|
|
99
|
+
operator: clause.op,
|
|
100
|
+
value: clause.value === '' ? undefined : clause.value
|
|
101
|
+
}),
|
|
102
|
+
toClause: plainClause
|
|
103
|
+
};
|
|
104
|
+
/** A picked field: `equals` shows as `is`, and `isAnyOf` holds a comma list. */ export const listSelectCodec = {
|
|
105
|
+
toItem: (clause)=>{
|
|
106
|
+
const operator = SELECT_TO_GRID[clause.op];
|
|
107
|
+
if (!operator) return null;
|
|
108
|
+
return {
|
|
109
|
+
operator,
|
|
110
|
+
value: operator === 'isAnyOf' ? clause.value.split(',').map((entry)=>entry.trim()).filter(Boolean) : clause.value
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
toClause: (item)=>{
|
|
114
|
+
const op = SELECT_FROM_GRID[String(item.operator)];
|
|
115
|
+
const request = op ? plainClause(item) : null;
|
|
116
|
+
return request && op ? _extends({}, request, {
|
|
117
|
+
op
|
|
118
|
+
}) : null;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
/** The select operators a field's allowed clause operators map to. */ export function listSelectOperators(allowed) {
|
|
122
|
+
const shown = allowed.map((op)=>SELECT_TO_GRID[op]).filter(Boolean);
|
|
123
|
+
return getGridSingleSelectOperators().filter((operator)=>shown.includes(operator.value));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The list's columns, each made filterable exactly as far as its field is
|
|
127
|
+
* declared, and every declared field that is not a column added as a
|
|
128
|
+
* permanently hidden one — the panel lists columns, so a field with none
|
|
129
|
+
* could not be reached (`hiddenFilterColumns`).
|
|
130
|
+
*
|
|
131
|
+
* A field with choices becomes a `singleSelect` over them, whatever the
|
|
132
|
+
* column rendered before; one without keeps the grammar's operators; a
|
|
133
|
+
* column no field declares offers no filter, because the list, not the
|
|
134
|
+
* grid, answers every filter and would have nothing to answer it with.
|
|
135
|
+
*/ export function listFilterGridColumns(columns, fields, options = {}, headers = {}) {
|
|
136
|
+
const selectProps = (field)=>{
|
|
137
|
+
var _options_field_column;
|
|
138
|
+
const choices = (_options_field_column = options[field.column]) != null ? _options_field_column : [];
|
|
139
|
+
const operators = listSelectOperators(listFilterOperators(field));
|
|
140
|
+
return {
|
|
141
|
+
type: 'singleSelect',
|
|
142
|
+
valueOptions: choices.map((choice)=>({
|
|
143
|
+
value: choice.value,
|
|
144
|
+
label: choice.label
|
|
145
|
+
})),
|
|
146
|
+
filterable: choices.length > 0 && operators.length > 0,
|
|
147
|
+
filterOperators: operators
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
const shown = columns.map((column)=>{
|
|
151
|
+
if (column.filterable === false && column.hideable === false) return column;
|
|
152
|
+
const field = fields.find((entry)=>entry.column === column.field);
|
|
153
|
+
if (!field) return _extends({}, column, {
|
|
154
|
+
filterable: false
|
|
155
|
+
});
|
|
156
|
+
if (options[field.column]) return _extends({}, column, selectProps(field));
|
|
157
|
+
return _extends({}, column, listFilterColumn(fields, field.column));
|
|
158
|
+
});
|
|
159
|
+
const present = columns.map((column)=>column.field);
|
|
160
|
+
const hidden = hiddenFilterColumns(fields, present, headers).map((column)=>{
|
|
161
|
+
const field = fields.find((entry)=>entry.column === column.field);
|
|
162
|
+
return field && options[field.column] ? _extends({}, column, selectProps(field)) : column;
|
|
163
|
+
});
|
|
164
|
+
return [
|
|
165
|
+
...shown,
|
|
166
|
+
...hidden
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Whether a row answers a quick search: every whitespace-separated word
|
|
171
|
+
* appears, case-insensitively, in SOME of the row's searched values. A
|
|
172
|
+
* blank search matches every row, so an emptied box is the list unfiltered.
|
|
173
|
+
* An array value (tags) is searched member by member.
|
|
174
|
+
*/ export function listRowMatchesSearch(row, paths, words) {
|
|
175
|
+
const asked = words.map((word)=>word.trim().toLowerCase()).filter(Boolean);
|
|
176
|
+
if (!asked.length) return true;
|
|
177
|
+
const values = paths.flatMap((path)=>{
|
|
178
|
+
const value = path.split('.').reduce((at, key)=>at == null ? undefined : at[key], row);
|
|
179
|
+
return Array.isArray(value) ? value : [
|
|
180
|
+
value
|
|
181
|
+
];
|
|
182
|
+
}).filter((value)=>typeof value === 'string' || typeof value === 'number').map((value)=>String(value).toLowerCase()).filter(Boolean);
|
|
183
|
+
return asked.every((word)=>values.some((value)=>value.includes(word)));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The rows that answer every clause and the quick search — for a list that
|
|
187
|
+
* holds its whole data set (or the window its query already narrowed) and
|
|
188
|
+
* so answers the panel itself. A clause `skip` names is one the query
|
|
189
|
+
* already served, and is not matched again.
|
|
190
|
+
*/ export function filterListRows(rows, fields, clauses, search, skip) {
|
|
191
|
+
const applied = skip ? clauses.filter((clause)=>!skip(clause)) : clauses;
|
|
192
|
+
return rows.filter((row)=>applied.every((clause)=>matchListFilter(row, fields, clause)) && listRowMatchesSearch(row, search.paths, search.words));
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* The clauses with one field's clause set: replaced in place where the field
|
|
196
|
+
* already had one, appended where it did not, removed when `next` is null.
|
|
197
|
+
* A field holds ONE clause, which is what the panel edits.
|
|
198
|
+
*/ export function upsertListFilterClause(clauses, field, next) {
|
|
199
|
+
const at = clauses.findIndex((clause)=>clause.field === field);
|
|
200
|
+
const rest = clauses.filter((clause)=>clause.field !== field);
|
|
201
|
+
if (!next) return rest;
|
|
202
|
+
if (at === -1) return [
|
|
203
|
+
...rest,
|
|
204
|
+
next
|
|
205
|
+
];
|
|
206
|
+
return [
|
|
207
|
+
...rest.slice(0, at),
|
|
208
|
+
next,
|
|
209
|
+
...rest.slice(at)
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
//# sourceMappingURL=list-grid-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/const/list-grid-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 {\n getGridSingleSelectOperators,\n type GridColDef,\n type GridFilterItem,\n} from '@mui/x-data-grid'\nimport {\n gridFilterRequests,\n hiddenFilterColumns,\n type ListFilterField,\n type ListFilterRequest,\n listFilterColumn,\n listFilterOperators,\n matchListFilter,\n} from './list-filter'\n\n/*\n * ONE path from a list's filter clauses to the data grid's own Filters panel\n * and quick search, and back (AGL-3313, shared by every list since AGL-3317).\n *\n * A list stores what it is narrowed by as clauses — `{ field, op, value }`,\n * the shape saved views hold — and edits them through the grid's toolbar\n * rather than controls of its own. What a list declares is its fields, as\n * the `ListFilterField` grammar (`./list-filter`), plus the choices of the\n * fields whose values are picked rather than typed. The grid then offers\n * each as a typed column: a `singleSelect` with its choices where there are\n * choices, the operators the grammar allows otherwise.\n *\n * The clauses keep their stored shape, so a view saved before the panel\n * reads unchanged: the panel shows `equals` as the select's `is` and writes\n * `is` back as `equals`, and a list whose clauses were stored in some other\n * shape says so with a codec.\n */\n\n/** One clause a list is narrowed by; `label` names a picked value on a chip. */\nexport interface ListFilterClause extends ListFilterRequest {\n label?: string\n}\n\n/** A choice for a field whose value is picked rather than typed. */\nexport interface ListFilterOption {\n value: string\n label: string\n}\n\n/**\n * A field of a list that holds its rows and answers the panel in memory.\n *\n * The grammar's derived operators describe what a FIRESTORE query can\n * serve, so a text field with no lower-case twin offers nothing. A list\n * that matches over rows it already has is not held to that: plain\n * JavaScript answers a mid-string `contains` and both empty operators, so\n * the field names them (`operators`), and `matchListFilter` answers them.\n * `select` is a field picked from choices — the list passes them as the\n * field's options, and the panel shows a select over them.\n */\nexport function inMemoryListField(\n column: string,\n kind: 'text' | 'select' | 'number' | 'date' | 'boolean',\n path: string = column,\n): ListFilterField {\n switch (kind) {\n case 'text':\n return {\n column,\n path,\n kind: 'text',\n operators: ['contains', 'doesNotContain', 'equals', 'startsWith', 'endsWith', 'isEmpty', 'isNotEmpty'],\n }\n case 'select':\n return { column, path, kind: 'exact', operators: ['equals', 'doesNotEqual', 'isAnyOf'] }\n case 'number':\n return { column, path, kind: 'number', presence: 'nullable' }\n case 'date':\n return { column, path, kind: 'date', presence: 'nullable' }\n default:\n return { column, path, kind: 'boolean' }\n }\n}\n\n/** The grid operator each stored select operator shows as. */\nconst SELECT_TO_GRID: Readonly<Record<string, string>> = {\n equals: 'is',\n doesNotEqual: 'not',\n isAnyOf: 'isAnyOf',\n}\nconst SELECT_FROM_GRID: Readonly<Record<string, string>> = {\n is: 'equals',\n not: 'doesNotEqual',\n isAnyOf: 'isAnyOf',\n}\n\n/**\n * How one field's stored clause and the panel's item translate. A field\n * with choices uses {@link listSelectCodec}; a typed one,\n * {@link listPlainCodec}. A list whose clauses predate the panel supplies\n * its own.\n */\nexport interface ListGridFilterCodec {\n toItem: (clause: ListFilterClause) => Pick<GridFilterItem, 'operator' | 'value'> | null\n toClause: (item: GridFilterItem) => ListFilterClause | null\n}\n\n/** The panel's item as a clause, through the grammar's one notion of \"usable\". */\nconst plainClause = (item: GridFilterItem): ListFilterClause | null =>\n gridFilterRequests({ items: [item] })[0] ?? null\n\n/** A typed field: the stored operator IS the grid's. */\nexport const listPlainCodec: ListGridFilterCodec = {\n toItem: (clause) => ({\n operator: clause.op,\n value: clause.value === '' ? undefined : clause.value,\n }),\n toClause: plainClause,\n}\n\n/** A picked field: `equals` shows as `is`, and `isAnyOf` holds a comma list. */\nexport const listSelectCodec: ListGridFilterCodec = {\n toItem: (clause) => {\n const operator = SELECT_TO_GRID[clause.op]\n if (!operator) return null\n return {\n operator,\n value:\n operator === 'isAnyOf'\n ? clause.value.split(',').map((entry) => entry.trim()).filter(Boolean)\n : clause.value,\n }\n },\n toClause: (item) => {\n const op = SELECT_FROM_GRID[String(item.operator)]\n const request = op ? plainClause(item) : null\n return request && op ? { ...request, op } : null\n },\n}\n\n/** The select operators a field's allowed clause operators map to. */\nexport function listSelectOperators(allowed: readonly string[]) {\n const shown = allowed.map((op) => SELECT_TO_GRID[op]).filter(Boolean)\n return getGridSingleSelectOperators().filter((operator) =>\n shown.includes(operator.value),\n )\n}\n\n/**\n * The list's columns, each made filterable exactly as far as its field is\n * declared, and every declared field that is not a column added as a\n * permanently hidden one — the panel lists columns, so a field with none\n * could not be reached (`hiddenFilterColumns`).\n *\n * A field with choices becomes a `singleSelect` over them, whatever the\n * column rendered before; one without keeps the grammar's operators; a\n * column no field declares offers no filter, because the list, not the\n * grid, answers every filter and would have nothing to answer it with.\n */\nexport function listFilterGridColumns(\n columns: readonly GridColDef[],\n fields: readonly ListFilterField[],\n options: Readonly<Record<string, readonly ListFilterOption[]>> = {},\n headers: Readonly<Record<string, string>> = {},\n): GridColDef[] {\n const selectProps = (field: ListFilterField) => {\n const choices = options[field.column] ?? []\n const operators = listSelectOperators(listFilterOperators(field))\n return {\n type: 'singleSelect' as const,\n valueOptions: choices.map((choice) => ({ value: choice.value, label: choice.label })),\n filterable: choices.length > 0 && operators.length > 0,\n filterOperators: operators,\n }\n }\n const shown = columns.map((column): GridColDef => {\n if (column.filterable === false && column.hideable === false) return column\n const field = fields.find((entry) => entry.column === column.field)\n if (!field) return { ...column, filterable: false }\n if (options[field.column]) return { ...column, ...selectProps(field) } as GridColDef\n return { ...column, ...listFilterColumn(fields, field.column) }\n })\n const present = columns.map((column) => column.field)\n const hidden = hiddenFilterColumns(fields, present, headers).map(\n (column): GridColDef => {\n const field = fields.find((entry) => entry.column === column.field)\n return field && options[field.column]\n ? ({ ...column, ...selectProps(field) } as GridColDef)\n : column\n },\n )\n return [...shown, ...hidden]\n}\n\n/**\n * Whether a row answers a quick search: every whitespace-separated word\n * appears, case-insensitively, in SOME of the row's searched values. A\n * blank search matches every row, so an emptied box is the list unfiltered.\n * An array value (tags) is searched member by member.\n */\nexport function listRowMatchesSearch(\n row: object,\n paths: readonly string[],\n words: readonly string[],\n): boolean {\n const asked = words.map((word) => word.trim().toLowerCase()).filter(Boolean)\n if (!asked.length) return true\n const values = paths\n .flatMap((path) => {\n const value = path\n .split('.')\n .reduce<unknown>(\n (at, key) => (at == null ? undefined : (at as Record<string, unknown>)[key]),\n row,\n )\n return Array.isArray(value) ? value : [value]\n })\n .filter((value): value is string | number => typeof value === 'string' || typeof value === 'number')\n .map((value) => String(value).toLowerCase())\n .filter(Boolean)\n return asked.every((word) => values.some((value) => value.includes(word)))\n}\n\n/**\n * The rows that answer every clause and the quick search — for a list that\n * holds its whole data set (or the window its query already narrowed) and\n * so answers the panel itself. A clause `skip` names is one the query\n * already served, and is not matched again.\n */\nexport function filterListRows<Row extends object>(\n rows: readonly Row[],\n fields: readonly ListFilterField[],\n clauses: readonly ListFilterClause[],\n search: { paths: readonly string[]; words: readonly string[] },\n skip?: (clause: ListFilterClause) => boolean,\n): Row[] {\n const applied = skip ? clauses.filter((clause) => !skip(clause)) : clauses\n return rows.filter(\n (row) =>\n applied.every((clause) => matchListFilter(row, fields, clause)) &&\n listRowMatchesSearch(row, search.paths, search.words),\n )\n}\n\n/**\n * The clauses with one field's clause set: replaced in place where the field\n * already had one, appended where it did not, removed when `next` is null.\n * A field holds ONE clause, which is what the panel edits.\n */\nexport function upsertListFilterClause<Clause extends ListFilterClause>(\n clauses: readonly Clause[],\n field: string,\n next: Clause | null,\n): Clause[] {\n const at = clauses.findIndex((clause) => clause.field === field)\n const rest = clauses.filter((clause) => clause.field !== field)\n if (!next) return rest\n if (at === -1) return [...rest, next]\n return [...rest.slice(0, at), next, ...rest.slice(at)]\n}\n"],"names":["getGridSingleSelectOperators","gridFilterRequests","hiddenFilterColumns","listFilterColumn","listFilterOperators","matchListFilter","inMemoryListField","column","kind","path","operators","presence","SELECT_TO_GRID","equals","doesNotEqual","isAnyOf","SELECT_FROM_GRID","is","not","plainClause","item","items","listPlainCodec","toItem","clause","operator","op","value","undefined","toClause","listSelectCodec","split","map","entry","trim","filter","Boolean","String","request","listSelectOperators","allowed","shown","includes","listFilterGridColumns","columns","fields","options","headers","selectProps","field","choices","type","valueOptions","choice","label","filterable","length","filterOperators","hideable","find","present","hidden","listRowMatchesSearch","row","paths","words","asked","word","toLowerCase","values","flatMap","reduce","at","key","Array","isArray","every","some","filterListRows","rows","clauses","search","skip","applied","upsertListFilterClause","next","findIndex","rest","slice"],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
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 { GridFilterModel } from '@mui/x-data-grid';
|
|
18
|
+
import { type ListFilterClause, type ListGridFilterCodec } from '../const/list-grid-filter';
|
|
19
|
+
export interface ListGridFilterOptions {
|
|
20
|
+
/**
|
|
21
|
+
* The clauses the list is narrowed by — a saved view's working filters.
|
|
22
|
+
* Omitted, the hook holds them itself, for a list with no saved views.
|
|
23
|
+
*/
|
|
24
|
+
clauses?: readonly ListFilterClause[];
|
|
25
|
+
onChange?: (clauses: ListFilterClause[]) => void;
|
|
26
|
+
/** Fields whose values are picked, which the panel shows as a select. */
|
|
27
|
+
selectFields?: readonly string[];
|
|
28
|
+
/** A field whose stored clauses predate the panel, translated its own way. */
|
|
29
|
+
codecs?: Readonly<Record<string, ListGridFilterCodec>>;
|
|
30
|
+
/**
|
|
31
|
+
* The query serves ONE clause, so the panel's replaces every other — a
|
|
32
|
+
* server-paged list, which has no loaded window to narrow further.
|
|
33
|
+
* `keepAlongside` names the clauses that do narrow the loaded page and so
|
|
34
|
+
* may stand beside it.
|
|
35
|
+
*/
|
|
36
|
+
single?: boolean;
|
|
37
|
+
keepAlongside?: (clause: ListFilterClause) => boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The quick search's words, held by the list when it needs them before
|
|
40
|
+
* the grid's columns exist; the hook holds them itself otherwise.
|
|
41
|
+
*/
|
|
42
|
+
search?: {
|
|
43
|
+
words: string[];
|
|
44
|
+
onChange: (words: string[]) => void;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** What the grid is handed, and what the list filters its rows by. */
|
|
48
|
+
export interface ListGridFilter {
|
|
49
|
+
/** Controlled: the panel's one item, and the quick search's words. */
|
|
50
|
+
filterModel: GridFilterModel;
|
|
51
|
+
onFilterModelChange: (model: GridFilterModel) => void;
|
|
52
|
+
/** The quick search's words, for the list to match its rows against. */
|
|
53
|
+
searchWords: string[];
|
|
54
|
+
/** Every clause in force — the ones given, or the ones the hook holds. */
|
|
55
|
+
clauses: readonly ListFilterClause[];
|
|
56
|
+
/** Replaces every clause; what the chips call to remove one. */
|
|
57
|
+
setClauses: (clauses: ListFilterClause[]) => void;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The grid's Filters panel and quick search, bound to a list's clauses
|
|
61
|
+
* (AGL-3313; shared since AGL-3317). See `const/list-grid-filter.ts` for the
|
|
62
|
+
* shape both sides keep.
|
|
63
|
+
*
|
|
64
|
+
* ## One field, one clause; the panel edits one at a time
|
|
65
|
+
*
|
|
66
|
+
* The free DataGrid's panel holds a single item, while a list may be
|
|
67
|
+
* narrowed by several clauses. So each FIELD holds one clause, and the panel
|
|
68
|
+
* is a window onto whichever field was last filtered: setting a value writes
|
|
69
|
+
* that field's clause, clearing it or deleting the row removes it, and
|
|
70
|
+
* choosing another column in the panel moves the window without touching
|
|
71
|
+
* the clause the reader just set. Filtering one field and then another is
|
|
72
|
+
* therefore both, which is what a reader who does it means; the chips over
|
|
73
|
+
* the grid (`ListFilterChips`) show every clause and remove any of them.
|
|
74
|
+
*
|
|
75
|
+
* ## The list answers, never the grid
|
|
76
|
+
*
|
|
77
|
+
* Every list passes `filterMode="server"`: the grid holds one page, or a
|
|
78
|
+
* window the query already narrowed, so a filter the grid ran itself would
|
|
79
|
+
* answer "no match" for a row on the next page. The list reads the clauses
|
|
80
|
+
* — onto its query where it can, over its loaded rows where it cannot
|
|
81
|
+
* (`filterListRows`) — and the quick search's words, and hands the grid the
|
|
82
|
+
* rows that answer.
|
|
83
|
+
*/
|
|
84
|
+
export declare function useListGridFilter(options?: ListGridFilterOptions): ListGridFilter;
|
|
85
|
+
export default useListGridFilter;
|
|
@@ -0,0 +1,158 @@
|
|
|
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
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { useCallback, useMemo, useState } from "react";
|
|
19
|
+
import { listPlainCodec, listSelectCodec, upsertListFilterClause } from "../const/list-grid-filter.js";
|
|
20
|
+
const NO_CLAUSES = [];
|
|
21
|
+
/**
|
|
22
|
+
* The grid's Filters panel and quick search, bound to a list's clauses
|
|
23
|
+
* (AGL-3313; shared since AGL-3317). See `const/list-grid-filter.ts` for the
|
|
24
|
+
* shape both sides keep.
|
|
25
|
+
*
|
|
26
|
+
* ## One field, one clause; the panel edits one at a time
|
|
27
|
+
*
|
|
28
|
+
* The free DataGrid's panel holds a single item, while a list may be
|
|
29
|
+
* narrowed by several clauses. So each FIELD holds one clause, and the panel
|
|
30
|
+
* is a window onto whichever field was last filtered: setting a value writes
|
|
31
|
+
* that field's clause, clearing it or deleting the row removes it, and
|
|
32
|
+
* choosing another column in the panel moves the window without touching
|
|
33
|
+
* the clause the reader just set. Filtering one field and then another is
|
|
34
|
+
* therefore both, which is what a reader who does it means; the chips over
|
|
35
|
+
* the grid (`ListFilterChips`) show every clause and remove any of them.
|
|
36
|
+
*
|
|
37
|
+
* ## The list answers, never the grid
|
|
38
|
+
*
|
|
39
|
+
* Every list passes `filterMode="server"`: the grid holds one page, or a
|
|
40
|
+
* window the query already narrowed, so a filter the grid ran itself would
|
|
41
|
+
* answer "no match" for a row on the next page. The list reads the clauses
|
|
42
|
+
* — onto its query where it can, over its loaded rows where it cannot
|
|
43
|
+
* (`filterListRows`) — and the quick search's words, and hands the grid the
|
|
44
|
+
* rows that answer.
|
|
45
|
+
*/ export function useListGridFilter(options = {}) {
|
|
46
|
+
var _options_clauses, _ref, _ref1;
|
|
47
|
+
var _options_search, _options_search1;
|
|
48
|
+
const { selectFields = [], codecs = {}, single = false, keepAlongside } = options;
|
|
49
|
+
const [ownClauses, setOwnClauses] = useState([]);
|
|
50
|
+
const controlled = options.clauses !== undefined;
|
|
51
|
+
const clauses = (_options_clauses = options.clauses) != null ? _options_clauses : controlled ? NO_CLAUSES : ownClauses;
|
|
52
|
+
const onChange = useMemo(()=>{
|
|
53
|
+
var _options_onChange;
|
|
54
|
+
return controlled ? (_options_onChange = options.onChange) != null ? _options_onChange : ()=>undefined : setOwnClauses;
|
|
55
|
+
}, [
|
|
56
|
+
controlled,
|
|
57
|
+
options.onChange
|
|
58
|
+
]);
|
|
59
|
+
const codecFor = useCallback((field)=>{
|
|
60
|
+
var _codecs_field;
|
|
61
|
+
return (_codecs_field = codecs[field]) != null ? _codecs_field : selectFields.includes(field) ? listSelectCodec : listPlainCodec;
|
|
62
|
+
}, [
|
|
63
|
+
codecs,
|
|
64
|
+
selectFields
|
|
65
|
+
]);
|
|
66
|
+
/** The field the panel shows, once the reader has pointed it anywhere. */ const [focus, setFocus] = useState(null);
|
|
67
|
+
/** The panel's row while it holds no value yet — a column chosen, a value not. */ const [pending, setPending] = useState(null);
|
|
68
|
+
const [ownWords, setOwnWords] = useState([]);
|
|
69
|
+
const searchWords = (_ref = (_options_search = options.search) == null ? void 0 : _options_search.words) != null ? _ref : ownWords;
|
|
70
|
+
const setSearchWords = (_ref1 = (_options_search1 = options.search) == null ? void 0 : _options_search1.onChange) != null ? _ref1 : setOwnWords;
|
|
71
|
+
/** Whatever the panel last pointed at, else the newest clause it can show. */ const shownField = useMemo(()=>{
|
|
72
|
+
if (focus) return focus;
|
|
73
|
+
for(let at = clauses.length - 1; at >= 0; at -= 1){
|
|
74
|
+
if (codecFor(clauses[at].field).toItem(clauses[at])) return clauses[at].field;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}, [
|
|
78
|
+
focus,
|
|
79
|
+
clauses,
|
|
80
|
+
codecFor
|
|
81
|
+
]);
|
|
82
|
+
const filterModel = useMemo(()=>{
|
|
83
|
+
const clause = shownField ? clauses.find((entry)=>entry.field === shownField) : undefined;
|
|
84
|
+
const shown = clause ? codecFor(clause.field).toItem(clause) : null;
|
|
85
|
+
const item = pending && pending.field === shownField ? pending : clause && shown ? _extends({
|
|
86
|
+
id: 'list',
|
|
87
|
+
field: clause.field
|
|
88
|
+
}, shown) : null;
|
|
89
|
+
return {
|
|
90
|
+
items: item ? [
|
|
91
|
+
item
|
|
92
|
+
] : [],
|
|
93
|
+
quickFilterValues: searchWords
|
|
94
|
+
};
|
|
95
|
+
}, [
|
|
96
|
+
shownField,
|
|
97
|
+
clauses,
|
|
98
|
+
codecFor,
|
|
99
|
+
pending,
|
|
100
|
+
searchWords
|
|
101
|
+
]);
|
|
102
|
+
const onFilterModelChange = useCallback((model)=>{
|
|
103
|
+
var _model_quickFilterValues;
|
|
104
|
+
const words = ((_model_quickFilterValues = model.quickFilterValues) != null ? _model_quickFilterValues : []).map((word)=>String(word));
|
|
105
|
+
if (words.join(' ') !== searchWords.join(' ')) setSearchWords(words);
|
|
106
|
+
const item = model.items[0];
|
|
107
|
+
// The row deleted: the field it showed is no longer filtered.
|
|
108
|
+
if (!item) {
|
|
109
|
+
if (shownField && clauses.some((clause)=>clause.field === shownField)) {
|
|
110
|
+
onChange(upsertListFilterClause(clauses, shownField, null));
|
|
111
|
+
}
|
|
112
|
+
setPending(null);
|
|
113
|
+
setFocus(null);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const field = String(item.field);
|
|
117
|
+
const moved = field !== shownField;
|
|
118
|
+
setFocus(field);
|
|
119
|
+
const clause = codecFor(field).toClause(item);
|
|
120
|
+
if (!clause) {
|
|
121
|
+
// No value yet. A value cleared on the field already shown removes
|
|
122
|
+
// its clause; a column just chosen leaves the last one standing.
|
|
123
|
+
setPending(_extends({}, item, {
|
|
124
|
+
id: 'list'
|
|
125
|
+
}));
|
|
126
|
+
if (!moved && clauses.some((entry)=>entry.field === field)) {
|
|
127
|
+
onChange(upsertListFilterClause(clauses, field, null));
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
setPending(null);
|
|
132
|
+
// A clause that stands alongside the served one is added beside it;
|
|
133
|
+
// any other replaces whatever the query was serving.
|
|
134
|
+
const base = single && !(keepAlongside == null ? void 0 : keepAlongside(clause)) ? clauses.filter((entry)=>entry.field === field || (keepAlongside == null ? void 0 : keepAlongside(entry))) : clauses;
|
|
135
|
+
const next = upsertListFilterClause(base, field, clause);
|
|
136
|
+
const same = next.length === clauses.length && next.every((entry, at)=>entry.field === clauses[at].field && entry.op === clauses[at].op && entry.value === clauses[at].value);
|
|
137
|
+
if (!same) onChange(next);
|
|
138
|
+
}, [
|
|
139
|
+
searchWords,
|
|
140
|
+
setSearchWords,
|
|
141
|
+
shownField,
|
|
142
|
+
clauses,
|
|
143
|
+
onChange,
|
|
144
|
+
codecFor,
|
|
145
|
+
single,
|
|
146
|
+
keepAlongside
|
|
147
|
+
]);
|
|
148
|
+
return {
|
|
149
|
+
filterModel,
|
|
150
|
+
onFilterModelChange,
|
|
151
|
+
searchWords,
|
|
152
|
+
clauses,
|
|
153
|
+
setClauses: onChange
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
export default useListGridFilter;
|
|
157
|
+
|
|
158
|
+
//# sourceMappingURL=use-list-grid-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/hooks/use-list-grid-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'use client'\n\nimport type { GridFilterItem, GridFilterModel } from '@mui/x-data-grid'\nimport { useCallback, useMemo, useState } from 'react'\nimport {\n type ListFilterClause,\n type ListGridFilterCodec,\n listPlainCodec,\n listSelectCodec,\n upsertListFilterClause,\n} from '../const/list-grid-filter'\n\nexport interface ListGridFilterOptions {\n /**\n * The clauses the list is narrowed by — a saved view's working filters.\n * Omitted, the hook holds them itself, for a list with no saved views.\n */\n clauses?: readonly ListFilterClause[]\n onChange?: (clauses: ListFilterClause[]) => void\n /** Fields whose values are picked, which the panel shows as a select. */\n selectFields?: readonly string[]\n /** A field whose stored clauses predate the panel, translated its own way. */\n codecs?: Readonly<Record<string, ListGridFilterCodec>>\n /**\n * The query serves ONE clause, so the panel's replaces every other — a\n * server-paged list, which has no loaded window to narrow further.\n * `keepAlongside` names the clauses that do narrow the loaded page and so\n * may stand beside it.\n */\n single?: boolean\n keepAlongside?: (clause: ListFilterClause) => boolean\n /**\n * The quick search's words, held by the list when it needs them before\n * the grid's columns exist; the hook holds them itself otherwise.\n */\n search?: { words: string[]; onChange: (words: string[]) => void }\n}\n\n/** What the grid is handed, and what the list filters its rows by. */\nexport interface ListGridFilter {\n /** Controlled: the panel's one item, and the quick search's words. */\n filterModel: GridFilterModel\n onFilterModelChange: (model: GridFilterModel) => void\n /** The quick search's words, for the list to match its rows against. */\n searchWords: string[]\n /** Every clause in force — the ones given, or the ones the hook holds. */\n clauses: readonly ListFilterClause[]\n /** Replaces every clause; what the chips call to remove one. */\n setClauses: (clauses: ListFilterClause[]) => void\n}\n\nconst NO_CLAUSES: readonly ListFilterClause[] = []\n\n/**\n * The grid's Filters panel and quick search, bound to a list's clauses\n * (AGL-3313; shared since AGL-3317). See `const/list-grid-filter.ts` for the\n * shape both sides keep.\n *\n * ## One field, one clause; the panel edits one at a time\n *\n * The free DataGrid's panel holds a single item, while a list may be\n * narrowed by several clauses. So each FIELD holds one clause, and the panel\n * is a window onto whichever field was last filtered: setting a value writes\n * that field's clause, clearing it or deleting the row removes it, and\n * choosing another column in the panel moves the window without touching\n * the clause the reader just set. Filtering one field and then another is\n * therefore both, which is what a reader who does it means; the chips over\n * the grid (`ListFilterChips`) show every clause and remove any of them.\n *\n * ## The list answers, never the grid\n *\n * Every list passes `filterMode=\"server\"`: the grid holds one page, or a\n * window the query already narrowed, so a filter the grid ran itself would\n * answer \"no match\" for a row on the next page. The list reads the clauses\n * — onto its query where it can, over its loaded rows where it cannot\n * (`filterListRows`) — and the quick search's words, and hands the grid the\n * rows that answer.\n */\nexport function useListGridFilter(options: ListGridFilterOptions = {}): ListGridFilter {\n const { selectFields = [], codecs = {}, single = false, keepAlongside } = options\n\n const [ownClauses, setOwnClauses] = useState<ListFilterClause[]>([])\n const controlled = options.clauses !== undefined\n const clauses = options.clauses ?? (controlled ? NO_CLAUSES : ownClauses)\n const onChange = useMemo(\n () => (controlled ? (options.onChange ?? (() => undefined)) : setOwnClauses),\n [controlled, options.onChange],\n )\n\n const codecFor = useCallback(\n (field: string): ListGridFilterCodec =>\n codecs[field] ?? (selectFields.includes(field) ? listSelectCodec : listPlainCodec),\n [codecs, selectFields],\n )\n\n /** The field the panel shows, once the reader has pointed it anywhere. */\n const [focus, setFocus] = useState<string | null>(null)\n /** The panel's row while it holds no value yet — a column chosen, a value not. */\n const [pending, setPending] = useState<GridFilterItem | null>(null)\n const [ownWords, setOwnWords] = useState<string[]>([])\n const searchWords = options.search?.words ?? ownWords\n const setSearchWords = options.search?.onChange ?? setOwnWords\n\n /** Whatever the panel last pointed at, else the newest clause it can show. */\n const shownField = useMemo(() => {\n if (focus) return focus\n for (let at = clauses.length - 1; at >= 0; at -= 1) {\n if (codecFor(clauses[at].field).toItem(clauses[at])) return clauses[at].field\n }\n return null\n }, [focus, clauses, codecFor])\n\n const filterModel = useMemo<GridFilterModel>(() => {\n const clause = shownField\n ? clauses.find((entry) => entry.field === shownField)\n : undefined\n const shown = clause ? codecFor(clause.field).toItem(clause) : null\n const item: GridFilterItem | null =\n pending && pending.field === shownField\n ? pending\n : clause && shown\n ? { id: 'list', field: clause.field, ...shown }\n : null\n return { items: item ? [item] : [], quickFilterValues: searchWords }\n }, [shownField, clauses, codecFor, pending, searchWords])\n\n const onFilterModelChange = useCallback(\n (model: GridFilterModel) => {\n const words = (model.quickFilterValues ?? []).map((word) => String(word))\n if (words.join(' ') !== searchWords.join(' ')) setSearchWords(words)\n\n const item = model.items[0]\n // The row deleted: the field it showed is no longer filtered.\n if (!item) {\n if (shownField && clauses.some((clause) => clause.field === shownField)) {\n onChange(upsertListFilterClause(clauses, shownField, null))\n }\n setPending(null)\n setFocus(null)\n return\n }\n const field = String(item.field)\n const moved = field !== shownField\n setFocus(field)\n const clause = codecFor(field).toClause(item)\n if (!clause) {\n // No value yet. A value cleared on the field already shown removes\n // its clause; a column just chosen leaves the last one standing.\n setPending({ ...item, id: 'list' })\n if (!moved && clauses.some((entry) => entry.field === field)) {\n onChange(upsertListFilterClause(clauses, field, null))\n }\n return\n }\n setPending(null)\n // A clause that stands alongside the served one is added beside it;\n // any other replaces whatever the query was serving.\n const base =\n single && !keepAlongside?.(clause)\n ? clauses.filter((entry) => entry.field === field || keepAlongside?.(entry))\n : clauses\n const next = upsertListFilterClause(base, field, clause)\n const same =\n next.length === clauses.length &&\n next.every(\n (entry, at) =>\n entry.field === clauses[at].field &&\n entry.op === clauses[at].op &&\n entry.value === clauses[at].value,\n )\n if (!same) onChange(next)\n },\n [searchWords, setSearchWords, shownField, clauses, onChange, codecFor, single, keepAlongside],\n )\n\n return { filterModel, onFilterModelChange, searchWords, clauses, setClauses: onChange }\n}\n\nexport default useListGridFilter\n"],"names":["useCallback","useMemo","useState","listPlainCodec","listSelectCodec","upsertListFilterClause","NO_CLAUSES","useListGridFilter","options","selectFields","codecs","single","keepAlongside","ownClauses","setOwnClauses","controlled","clauses","undefined","onChange","codecFor","field","includes","focus","setFocus","pending","setPending","ownWords","setOwnWords","searchWords","search","words","setSearchWords","shownField","at","length","toItem","filterModel","clause","find","entry","shown","item","id","items","quickFilterValues","onFilterModelChange","model","map","word","String","join","some","moved","toClause","base","filter","next","same","every","op","value","setClauses"],"mappings":"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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
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 { GridColDef, GridFilterModel } from '@mui/x-data-grid';
|
|
18
|
+
import type { ListFilterField } from '../const/list-filter';
|
|
19
|
+
import { type ListFilterClause, type ListFilterOption } from '../const/list-grid-filter';
|
|
20
|
+
import { type ListGridFilter } from './use-list-grid-filter';
|
|
21
|
+
export interface ListRowsFilterOptions<Row extends object> {
|
|
22
|
+
/** Every row the list read — the whole set, or the window its query capped. */
|
|
23
|
+
rows: readonly Row[];
|
|
24
|
+
/** What the panel offers, in the `ListFilterField` grammar. */
|
|
25
|
+
fields: readonly ListFilterField[];
|
|
26
|
+
/** Choices per picked field; each field named here is shown as a select. */
|
|
27
|
+
options?: Readonly<Record<string, readonly ListFilterOption[]>>;
|
|
28
|
+
/** How a field reads on a chip and as a hidden column's header. */
|
|
29
|
+
headers?: Readonly<Record<string, string>>;
|
|
30
|
+
/** The row paths the quick search reads. */
|
|
31
|
+
search: readonly string[];
|
|
32
|
+
/** A clause the list's query already served, not matched again. */
|
|
33
|
+
served?: (clause: ListFilterClause) => boolean;
|
|
34
|
+
/** A saved view's clauses, when the list keeps them; held here otherwise. */
|
|
35
|
+
clauses?: readonly ListFilterClause[];
|
|
36
|
+
onChange?: (clauses: ListFilterClause[]) => void;
|
|
37
|
+
}
|
|
38
|
+
/** What a list that holds its rows hands its grid and its chips. */
|
|
39
|
+
export interface ListRowsFilter<Row> {
|
|
40
|
+
/** The rows that answer every clause and the search. */
|
|
41
|
+
rows: Row[];
|
|
42
|
+
/** Whether anything narrows the list — a clause or a search word. */
|
|
43
|
+
filtering: boolean;
|
|
44
|
+
gridFilter: ListGridFilter;
|
|
45
|
+
/** Spread onto `ListTable`: the controlled panel, answered by the list. */
|
|
46
|
+
gridProps: {
|
|
47
|
+
filterMode: 'server';
|
|
48
|
+
filterModel: GridFilterModel;
|
|
49
|
+
onFilterModelChange: (model: GridFilterModel) => void;
|
|
50
|
+
quickFilter: true;
|
|
51
|
+
};
|
|
52
|
+
/** Spread onto `ListFilterChips`. */
|
|
53
|
+
chipsProps: {
|
|
54
|
+
fields: readonly ListFilterField[];
|
|
55
|
+
headers: Readonly<Record<string, string>>;
|
|
56
|
+
clauses: readonly ListFilterClause[];
|
|
57
|
+
onChange: (clauses: ListFilterClause[]) => void;
|
|
58
|
+
options: Readonly<Record<string, readonly ListFilterOption[]>>;
|
|
59
|
+
};
|
|
60
|
+
/** The list's columns, typed for the panel (`listFilterGridColumns`). */
|
|
61
|
+
filterColumns: (columns: readonly GridColDef[]) => GridColDef[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The grid's Filters panel and quick search for a list that HOLDS its rows
|
|
65
|
+
* (AGL-3317) — every row it read, or the window its query capped — and so
|
|
66
|
+
* answers both over that whole set rather than the page on screen.
|
|
67
|
+
*
|
|
68
|
+
* It is `useListGridFilter` with the rest of the recipe every such list
|
|
69
|
+
* repeated: the rows matched through `filterListRows`, the columns typed
|
|
70
|
+
* through `listFilterGridColumns`, and the props the grid and the chips
|
|
71
|
+
* take. A list whose query can serve a clause names it in `served`, puts it
|
|
72
|
+
* on the query, and the rows here are not matched against it again.
|
|
73
|
+
*/
|
|
74
|
+
export declare function useListRowsFilter<Row extends object>(options: ListRowsFilterOptions<Row>): ListRowsFilter<Row>;
|
|
75
|
+
export default useListRowsFilter;
|
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
*/ 'use client';
|
|
17
|
+
import { useCallback, useMemo } from "react";
|
|
18
|
+
import { filterListRows, listFilterGridColumns } from "../const/list-grid-filter.js";
|
|
19
|
+
import { useListGridFilter } from "./use-list-grid-filter.js";
|
|
20
|
+
const NO_OPTIONS = {};
|
|
21
|
+
const NO_HEADERS = {};
|
|
22
|
+
/**
|
|
23
|
+
* The grid's Filters panel and quick search for a list that HOLDS its rows
|
|
24
|
+
* (AGL-3317) — every row it read, or the window its query capped — and so
|
|
25
|
+
* answers both over that whole set rather than the page on screen.
|
|
26
|
+
*
|
|
27
|
+
* It is `useListGridFilter` with the rest of the recipe every such list
|
|
28
|
+
* repeated: the rows matched through `filterListRows`, the columns typed
|
|
29
|
+
* through `listFilterGridColumns`, and the props the grid and the chips
|
|
30
|
+
* take. A list whose query can serve a clause names it in `served`, puts it
|
|
31
|
+
* on the query, and the rows here are not matched against it again.
|
|
32
|
+
*/ export function useListRowsFilter(options) {
|
|
33
|
+
const { rows, fields, options: choices = NO_OPTIONS, headers = NO_HEADERS, search, served } = options;
|
|
34
|
+
const selectFields = useMemo(()=>Object.keys(choices), [
|
|
35
|
+
choices
|
|
36
|
+
]);
|
|
37
|
+
const gridFilter = useListGridFilter({
|
|
38
|
+
clauses: options.clauses,
|
|
39
|
+
onChange: options.onChange,
|
|
40
|
+
selectFields
|
|
41
|
+
});
|
|
42
|
+
const searchKey = gridFilter.searchWords.join(' ');
|
|
43
|
+
const matched = useMemo(()=>filterListRows(rows, fields, gridFilter.clauses, {
|
|
44
|
+
paths: search,
|
|
45
|
+
words: gridFilter.searchWords
|
|
46
|
+
}, served), // `searchKey` stands for the words, which are a new array each render.
|
|
47
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
48
|
+
[
|
|
49
|
+
rows,
|
|
50
|
+
fields,
|
|
51
|
+
gridFilter.clauses,
|
|
52
|
+
searchKey,
|
|
53
|
+
search,
|
|
54
|
+
served
|
|
55
|
+
]);
|
|
56
|
+
const filterColumns = useCallback((columns)=>listFilterGridColumns(columns, fields, choices, headers), [
|
|
57
|
+
fields,
|
|
58
|
+
choices,
|
|
59
|
+
headers
|
|
60
|
+
]);
|
|
61
|
+
return {
|
|
62
|
+
rows: matched,
|
|
63
|
+
filtering: gridFilter.clauses.length > 0 || gridFilter.searchWords.length > 0,
|
|
64
|
+
gridFilter,
|
|
65
|
+
gridProps: {
|
|
66
|
+
filterMode: 'server',
|
|
67
|
+
filterModel: gridFilter.filterModel,
|
|
68
|
+
onFilterModelChange: gridFilter.onFilterModelChange,
|
|
69
|
+
quickFilter: true
|
|
70
|
+
},
|
|
71
|
+
chipsProps: {
|
|
72
|
+
fields,
|
|
73
|
+
headers,
|
|
74
|
+
clauses: gridFilter.clauses,
|
|
75
|
+
onChange: gridFilter.setClauses,
|
|
76
|
+
options: choices
|
|
77
|
+
},
|
|
78
|
+
filterColumns
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export default useListRowsFilter;
|
|
82
|
+
|
|
83
|
+
//# sourceMappingURL=use-list-rows-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/hooks/use-list-rows-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'use client'\n\nimport type { GridColDef, GridFilterModel } from '@mui/x-data-grid'\nimport { useCallback, useMemo } from 'react'\nimport type { ListFilterField } from '../const/list-filter'\nimport {\n filterListRows,\n type ListFilterClause,\n type ListFilterOption,\n listFilterGridColumns,\n} from '../const/list-grid-filter'\nimport { type ListGridFilter, useListGridFilter } from './use-list-grid-filter'\n\nexport interface ListRowsFilterOptions<Row extends object> {\n /** Every row the list read — the whole set, or the window its query capped. */\n rows: readonly Row[]\n /** What the panel offers, in the `ListFilterField` grammar. */\n fields: readonly ListFilterField[]\n /** Choices per picked field; each field named here is shown as a select. */\n options?: Readonly<Record<string, readonly ListFilterOption[]>>\n /** How a field reads on a chip and as a hidden column's header. */\n headers?: Readonly<Record<string, string>>\n /** The row paths the quick search reads. */\n search: readonly string[]\n /** A clause the list's query already served, not matched again. */\n served?: (clause: ListFilterClause) => boolean\n /** A saved view's clauses, when the list keeps them; held here otherwise. */\n clauses?: readonly ListFilterClause[]\n onChange?: (clauses: ListFilterClause[]) => void\n}\n\n/** What a list that holds its rows hands its grid and its chips. */\nexport interface ListRowsFilter<Row> {\n /** The rows that answer every clause and the search. */\n rows: Row[]\n /** Whether anything narrows the list — a clause or a search word. */\n filtering: boolean\n gridFilter: ListGridFilter\n /** Spread onto `ListTable`: the controlled panel, answered by the list. */\n gridProps: {\n filterMode: 'server'\n filterModel: GridFilterModel\n onFilterModelChange: (model: GridFilterModel) => void\n quickFilter: true\n }\n /** Spread onto `ListFilterChips`. */\n chipsProps: {\n fields: readonly ListFilterField[]\n headers: Readonly<Record<string, string>>\n clauses: readonly ListFilterClause[]\n onChange: (clauses: ListFilterClause[]) => void\n options: Readonly<Record<string, readonly ListFilterOption[]>>\n }\n /** The list's columns, typed for the panel (`listFilterGridColumns`). */\n filterColumns: (columns: readonly GridColDef[]) => GridColDef[]\n}\n\nconst NO_OPTIONS: Readonly<Record<string, readonly ListFilterOption[]>> = {}\nconst NO_HEADERS: Readonly<Record<string, string>> = {}\n\n/**\n * The grid's Filters panel and quick search for a list that HOLDS its rows\n * (AGL-3317) — every row it read, or the window its query capped — and so\n * answers both over that whole set rather than the page on screen.\n *\n * It is `useListGridFilter` with the rest of the recipe every such list\n * repeated: the rows matched through `filterListRows`, the columns typed\n * through `listFilterGridColumns`, and the props the grid and the chips\n * take. A list whose query can serve a clause names it in `served`, puts it\n * on the query, and the rows here are not matched against it again.\n */\nexport function useListRowsFilter<Row extends object>(\n options: ListRowsFilterOptions<Row>,\n): ListRowsFilter<Row> {\n const {\n rows,\n fields,\n options: choices = NO_OPTIONS,\n headers = NO_HEADERS,\n search,\n served,\n } = options\n const selectFields = useMemo(() => Object.keys(choices), [choices])\n const gridFilter = useListGridFilter({\n clauses: options.clauses,\n onChange: options.onChange,\n selectFields,\n })\n const searchKey = gridFilter.searchWords.join(' ')\n const matched = useMemo(\n () =>\n filterListRows(\n rows,\n fields,\n gridFilter.clauses,\n { paths: search, words: gridFilter.searchWords },\n served,\n ),\n // `searchKey` stands for the words, which are a new array each render.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [rows, fields, gridFilter.clauses, searchKey, search, served],\n )\n const filterColumns = useCallback(\n (columns: readonly GridColDef[]) =>\n listFilterGridColumns(columns, fields, choices, headers),\n [fields, choices, headers],\n )\n return {\n rows: matched,\n filtering: gridFilter.clauses.length > 0 || gridFilter.searchWords.length > 0,\n gridFilter,\n gridProps: {\n filterMode: 'server',\n filterModel: gridFilter.filterModel,\n onFilterModelChange: gridFilter.onFilterModelChange,\n quickFilter: true,\n },\n chipsProps: {\n fields,\n headers,\n clauses: gridFilter.clauses,\n onChange: gridFilter.setClauses,\n options: choices,\n },\n filterColumns,\n }\n}\n\nexport default useListRowsFilter\n"],"names":["useCallback","useMemo","filterListRows","listFilterGridColumns","useListGridFilter","NO_OPTIONS","NO_HEADERS","useListRowsFilter","options","rows","fields","choices","headers","search","served","selectFields","Object","keys","gridFilter","clauses","onChange","searchKey","searchWords","join","matched","paths","words","filterColumns","columns","filtering","length","gridProps","filterMode","filterModel","onFilterModelChange","quickFilter","chipsProps","setClauses"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;AAGA,SAASA,WAAW,EAAEC,OAAO,QAAQ,QAAO;AAE5C,SACEC,cAAc,EAGdC,qBAAqB,QAChB,+BAA2B;AAClC,SAA8BC,iBAAiB,QAAQ,4BAAwB;AA8C/E,MAAMC,aAAoE,CAAC;AAC3E,MAAMC,aAA+C,CAAC;AAEtD;;;;;;;;;;CAUC,GACD,OAAO,SAASC,kBACdC,OAAmC;IAEnC,MAAM,EACJC,IAAI,EACJC,MAAM,EACNF,SAASG,UAAUN,UAAU,EAC7BO,UAAUN,UAAU,EACpBO,MAAM,EACNC,MAAM,EACP,GAAGN;IACJ,MAAMO,eAAed,QAAQ,IAAMe,OAAOC,IAAI,CAACN,UAAU;QAACA;KAAQ;IAClE,MAAMO,aAAad,kBAAkB;QACnCe,SAASX,QAAQW,OAAO;QACxBC,UAAUZ,QAAQY,QAAQ;QAC1BL;IACF;IACA,MAAMM,YAAYH,WAAWI,WAAW,CAACC,IAAI,CAAC;IAC9C,MAAMC,UAAUvB,QACd,IACEC,eACEO,MACAC,QACAQ,WAAWC,OAAO,EAClB;YAAEM,OAAOZ;YAAQa,OAAOR,WAAWI,WAAW;QAAC,GAC/CR,SAEJ,uEAAuE;IACvE,uDAAuD;IACvD;QAACL;QAAMC;QAAQQ,WAAWC,OAAO;QAAEE;QAAWR;QAAQC;KAAO;IAE/D,MAAMa,gBAAgB3B,YACpB,CAAC4B,UACCzB,sBAAsByB,SAASlB,QAAQC,SAASC,UAClD;QAACF;QAAQC;QAASC;KAAQ;IAE5B,OAAO;QACLH,MAAMe;QACNK,WAAWX,WAAWC,OAAO,CAACW,MAAM,GAAG,KAAKZ,WAAWI,WAAW,CAACQ,MAAM,GAAG;QAC5EZ;QACAa,WAAW;YACTC,YAAY;YACZC,aAAaf,WAAWe,WAAW;YACnCC,qBAAqBhB,WAAWgB,mBAAmB;YACnDC,aAAa;QACf;QACAC,YAAY;YACV1B;YACAE;YACAO,SAASD,WAAWC,OAAO;YAC3BC,UAAUF,WAAWmB,UAAU;YAC/B7B,SAASG;QACX;QACAgB;IACF;AACF;AAEA,eAAepB,kBAAiB"}
|
|
@@ -0,0 +1,77 @@
|
|
|
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 ListRowsFilter, type ListRowsFilterOptions } from './use-list-rows-filter';
|
|
18
|
+
/** How many rows a paged list reads while it is filtered — see below. */
|
|
19
|
+
export declare const PAGED_FILTER_WINDOW = 100;
|
|
20
|
+
/**
|
|
21
|
+
* A paged window over a live list — the shape `usePagedCollection` returns:
|
|
22
|
+
* every row from the first page to the one being read (plus, possibly, one
|
|
23
|
+
* probe row past it), the page, and whether more exist.
|
|
24
|
+
*/
|
|
25
|
+
export interface PagedRowsWindow<Row> {
|
|
26
|
+
/** Every row read so far, page 0 to the current page; may hold a probe row. */
|
|
27
|
+
data?: readonly Row[] | null;
|
|
28
|
+
/** The current page's rows, when nothing narrows the list. */
|
|
29
|
+
rows: readonly Row[];
|
|
30
|
+
hasMore: boolean;
|
|
31
|
+
page: number;
|
|
32
|
+
setPage: (page: number) => void;
|
|
33
|
+
pageSize: number;
|
|
34
|
+
setPageSize: (pageSize: number) => void;
|
|
35
|
+
}
|
|
36
|
+
/** What the list's grid, chips and footer take while it is paged. */
|
|
37
|
+
export interface PagedRowsFilter<Row> extends Omit<ListRowsFilter<Row>, 'rows'> {
|
|
38
|
+
/** The rows the grid shows: the page, or the page of the matches. */
|
|
39
|
+
rows: Row[];
|
|
40
|
+
/** How many rows the window read — what a filter has looked through. */
|
|
41
|
+
read: number;
|
|
42
|
+
/** Spread onto `ListPagination`. */
|
|
43
|
+
pagination: {
|
|
44
|
+
page: number;
|
|
45
|
+
pageSize: number;
|
|
46
|
+
rowCount: number;
|
|
47
|
+
hasMore: boolean;
|
|
48
|
+
onPageChange: (page: number) => void;
|
|
49
|
+
onPageSizeChange: (pageSize: number) => void;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The grid's Filters panel and quick search over a PAGED live list
|
|
54
|
+
* (AGL-3317), one that reads a window growing a page at a time rather than
|
|
55
|
+
* every row.
|
|
56
|
+
*
|
|
57
|
+
* Unfiltered it is the pager it wraps: the grid shows the page, and the
|
|
58
|
+
* footer turns it. While a clause or a search word is in force the list
|
|
59
|
+
* matches over EVERYTHING the window has read — every page up to the
|
|
60
|
+
* current one, not the page on screen — and pages the matches instead;
|
|
61
|
+
* turning past the last match widens the window by one more page, so a
|
|
62
|
+
* filter reaches further the further the reader goes. `read` says how far
|
|
63
|
+
* it has looked, for the list to say so while more exist.
|
|
64
|
+
*
|
|
65
|
+
* ## The first filter widens the window once
|
|
66
|
+
*
|
|
67
|
+
* A window of one ten-row page is too little to filter: a search would look
|
|
68
|
+
* through ten rows and call the rest "no match". So the first clause or word
|
|
69
|
+
* widens the window to `filterWindow` rows (default
|
|
70
|
+
* {@link PAGED_FILTER_WINDOW}) — one listener that much wider, for as long as
|
|
71
|
+
* the list is narrowed — and clearing the last one puts the reader back on
|
|
72
|
+
* the page they left.
|
|
73
|
+
*/
|
|
74
|
+
export declare function usePagedRowsFilter<Row extends object>(paged: PagedRowsWindow<Row>, options: Omit<ListRowsFilterOptions<Row>, 'rows'> & {
|
|
75
|
+
filterWindow?: number;
|
|
76
|
+
}): PagedRowsFilter<Row>;
|
|
77
|
+
export default usePagedRowsFilter;
|
|
@@ -0,0 +1,126 @@
|
|
|
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
|
+
*/ 'use client';
|
|
17
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
18
|
+
import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
|
|
19
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
20
|
+
import { useListRowsFilter } from "./use-list-rows-filter.js";
|
|
21
|
+
/** How many rows a paged list reads while it is filtered — see below. */ export const PAGED_FILTER_WINDOW = 100;
|
|
22
|
+
/**
|
|
23
|
+
* The grid's Filters panel and quick search over a PAGED live list
|
|
24
|
+
* (AGL-3317), one that reads a window growing a page at a time rather than
|
|
25
|
+
* every row.
|
|
26
|
+
*
|
|
27
|
+
* Unfiltered it is the pager it wraps: the grid shows the page, and the
|
|
28
|
+
* footer turns it. While a clause or a search word is in force the list
|
|
29
|
+
* matches over EVERYTHING the window has read — every page up to the
|
|
30
|
+
* current one, not the page on screen — and pages the matches instead;
|
|
31
|
+
* turning past the last match widens the window by one more page, so a
|
|
32
|
+
* filter reaches further the further the reader goes. `read` says how far
|
|
33
|
+
* it has looked, for the list to say so while more exist.
|
|
34
|
+
*
|
|
35
|
+
* ## The first filter widens the window once
|
|
36
|
+
*
|
|
37
|
+
* A window of one ten-row page is too little to filter: a search would look
|
|
38
|
+
* through ten rows and call the rest "no match". So the first clause or word
|
|
39
|
+
* widens the window to `filterWindow` rows (default
|
|
40
|
+
* {@link PAGED_FILTER_WINDOW}) — one listener that much wider, for as long as
|
|
41
|
+
* the list is narrowed — and clearing the last one puts the reader back on
|
|
42
|
+
* the page they left.
|
|
43
|
+
*/ export function usePagedRowsFilter(paged, options) {
|
|
44
|
+
const { pageSize } = paged;
|
|
45
|
+
const { filterWindow = PAGED_FILTER_WINDOW } = options, filterOptions = _object_without_properties_loose(options, [
|
|
46
|
+
"filterWindow"
|
|
47
|
+
]);
|
|
48
|
+
const windowRows = useMemo(()=>{
|
|
49
|
+
var _paged_data;
|
|
50
|
+
return ((_paged_data = paged.data) != null ? _paged_data : paged.rows).slice(0, pageSize * (paged.page + 1));
|
|
51
|
+
}, [
|
|
52
|
+
paged.data,
|
|
53
|
+
paged.rows,
|
|
54
|
+
pageSize,
|
|
55
|
+
paged.page
|
|
56
|
+
]);
|
|
57
|
+
const filter = useListRowsFilter(_extends({}, filterOptions, {
|
|
58
|
+
rows: windowRows
|
|
59
|
+
}));
|
|
60
|
+
const [matchPage, setMatchPage] = useState(0);
|
|
61
|
+
const searchKey = filter.gridFilter.searchWords.join(' ');
|
|
62
|
+
// A narrowed list starts again at its first page.
|
|
63
|
+
useEffect(()=>setMatchPage(0), [
|
|
64
|
+
filter.gridFilter.clauses,
|
|
65
|
+
searchKey
|
|
66
|
+
]);
|
|
67
|
+
/** The page the reader was on before the list was narrowed. */ const before = useRef(null);
|
|
68
|
+
const { filtering } = filter;
|
|
69
|
+
useEffect(()=>{
|
|
70
|
+
if (filtering && before.current === null) {
|
|
71
|
+
before.current = paged.page;
|
|
72
|
+
const widened = Math.max(0, Math.ceil(filterWindow / pageSize) - 1);
|
|
73
|
+
if (widened > paged.page) paged.setPage(widened);
|
|
74
|
+
} else if (!filtering && before.current !== null) {
|
|
75
|
+
paged.setPage(before.current);
|
|
76
|
+
before.current = null;
|
|
77
|
+
}
|
|
78
|
+
// Only the transition into and out of a filter moves the window.
|
|
79
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
80
|
+
}, [
|
|
81
|
+
filtering
|
|
82
|
+
]);
|
|
83
|
+
if (!filter.filtering) {
|
|
84
|
+
return _extends({}, filter, {
|
|
85
|
+
rows: [
|
|
86
|
+
...paged.rows
|
|
87
|
+
],
|
|
88
|
+
read: windowRows.length,
|
|
89
|
+
pagination: {
|
|
90
|
+
page: paged.page,
|
|
91
|
+
pageSize,
|
|
92
|
+
rowCount: paged.rows.length,
|
|
93
|
+
hasMore: paged.hasMore,
|
|
94
|
+
onPageChange: paged.setPage,
|
|
95
|
+
onPageSizeChange: paged.setPageSize
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const matches = filter.rows;
|
|
100
|
+
const shown = matches.slice(matchPage * pageSize, (matchPage + 1) * pageSize);
|
|
101
|
+
const lastMatchPage = Math.max(0, Math.ceil(matches.length / pageSize) - 1);
|
|
102
|
+
return _extends({}, filter, {
|
|
103
|
+
rows: shown,
|
|
104
|
+
read: windowRows.length,
|
|
105
|
+
pagination: {
|
|
106
|
+
page: matchPage,
|
|
107
|
+
pageSize,
|
|
108
|
+
rowCount: shown.length,
|
|
109
|
+
hasMore: paged.hasMore || matchPage < lastMatchPage,
|
|
110
|
+
onPageChange: (next)=>{
|
|
111
|
+
// Past the matches read so far, the window reads another page first.
|
|
112
|
+
if ((next + 1) * pageSize > matches.length && paged.hasMore) {
|
|
113
|
+
paged.setPage(paged.page + 1);
|
|
114
|
+
}
|
|
115
|
+
setMatchPage(next);
|
|
116
|
+
},
|
|
117
|
+
onPageSizeChange: (next)=>{
|
|
118
|
+
paged.setPageSize(next);
|
|
119
|
+
setMatchPage(0);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
export default usePagedRowsFilter;
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=use-paged-rows-filter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/hooks/use-paged-rows-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'use client'\n\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport {\n type ListRowsFilter,\n type ListRowsFilterOptions,\n useListRowsFilter,\n} from './use-list-rows-filter'\n\n/** How many rows a paged list reads while it is filtered — see below. */\nexport const PAGED_FILTER_WINDOW = 100\n\n/**\n * A paged window over a live list — the shape `usePagedCollection` returns:\n * every row from the first page to the one being read (plus, possibly, one\n * probe row past it), the page, and whether more exist.\n */\nexport interface PagedRowsWindow<Row> {\n /** Every row read so far, page 0 to the current page; may hold a probe row. */\n data?: readonly Row[] | null\n /** The current page's rows, when nothing narrows the list. */\n rows: readonly Row[]\n hasMore: boolean\n page: number\n setPage: (page: number) => void\n pageSize: number\n setPageSize: (pageSize: number) => void\n}\n\n/** What the list's grid, chips and footer take while it is paged. */\nexport interface PagedRowsFilter<Row> extends Omit<ListRowsFilter<Row>, 'rows'> {\n /** The rows the grid shows: the page, or the page of the matches. */\n rows: Row[]\n /** How many rows the window read — what a filter has looked through. */\n read: number\n /** Spread onto `ListPagination`. */\n pagination: {\n page: number\n pageSize: number\n rowCount: number\n hasMore: boolean\n onPageChange: (page: number) => void\n onPageSizeChange: (pageSize: number) => void\n }\n}\n\n/**\n * The grid's Filters panel and quick search over a PAGED live list\n * (AGL-3317), one that reads a window growing a page at a time rather than\n * every row.\n *\n * Unfiltered it is the pager it wraps: the grid shows the page, and the\n * footer turns it. While a clause or a search word is in force the list\n * matches over EVERYTHING the window has read — every page up to the\n * current one, not the page on screen — and pages the matches instead;\n * turning past the last match widens the window by one more page, so a\n * filter reaches further the further the reader goes. `read` says how far\n * it has looked, for the list to say so while more exist.\n *\n * ## The first filter widens the window once\n *\n * A window of one ten-row page is too little to filter: a search would look\n * through ten rows and call the rest \"no match\". So the first clause or word\n * widens the window to `filterWindow` rows (default\n * {@link PAGED_FILTER_WINDOW}) — one listener that much wider, for as long as\n * the list is narrowed — and clearing the last one puts the reader back on\n * the page they left.\n */\nexport function usePagedRowsFilter<Row extends object>(\n paged: PagedRowsWindow<Row>,\n options: Omit<ListRowsFilterOptions<Row>, 'rows'> & { filterWindow?: number },\n): PagedRowsFilter<Row> {\n const { pageSize } = paged\n const { filterWindow = PAGED_FILTER_WINDOW, ...filterOptions } = options\n const windowRows = useMemo(\n () => (paged.data ?? paged.rows).slice(0, pageSize * (paged.page + 1)),\n [paged.data, paged.rows, pageSize, paged.page],\n )\n const filter = useListRowsFilter({ ...filterOptions, rows: windowRows })\n const [matchPage, setMatchPage] = useState(0)\n const searchKey = filter.gridFilter.searchWords.join(' ')\n // A narrowed list starts again at its first page.\n useEffect(() => setMatchPage(0), [filter.gridFilter.clauses, searchKey])\n\n /** The page the reader was on before the list was narrowed. */\n const before = useRef<number | null>(null)\n const { filtering } = filter\n useEffect(() => {\n if (filtering && before.current === null) {\n before.current = paged.page\n const widened = Math.max(0, Math.ceil(filterWindow / pageSize) - 1)\n if (widened > paged.page) paged.setPage(widened)\n } else if (!filtering && before.current !== null) {\n paged.setPage(before.current)\n before.current = null\n }\n // Only the transition into and out of a filter moves the window.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [filtering])\n\n if (!filter.filtering) {\n return {\n ...filter,\n rows: [...paged.rows],\n read: windowRows.length,\n pagination: {\n page: paged.page,\n pageSize,\n rowCount: paged.rows.length,\n hasMore: paged.hasMore,\n onPageChange: paged.setPage,\n onPageSizeChange: paged.setPageSize,\n },\n }\n }\n const matches = filter.rows\n const shown = matches.slice(matchPage * pageSize, (matchPage + 1) * pageSize)\n const lastMatchPage = Math.max(0, Math.ceil(matches.length / pageSize) - 1)\n return {\n ...filter,\n rows: shown,\n read: windowRows.length,\n pagination: {\n page: matchPage,\n pageSize,\n rowCount: shown.length,\n hasMore: paged.hasMore || matchPage < lastMatchPage,\n onPageChange: (next) => {\n // Past the matches read so far, the window reads another page first.\n if ((next + 1) * pageSize > matches.length && paged.hasMore) {\n paged.setPage(paged.page + 1)\n }\n setMatchPage(next)\n },\n onPageSizeChange: (next) => {\n paged.setPageSize(next)\n setMatchPage(0)\n },\n },\n }\n}\n\nexport default usePagedRowsFilter\n"],"names":["useEffect","useMemo","useRef","useState","useListRowsFilter","PAGED_FILTER_WINDOW","usePagedRowsFilter","paged","options","pageSize","filterWindow","filterOptions","windowRows","data","rows","slice","page","filter","matchPage","setMatchPage","searchKey","gridFilter","searchWords","join","clauses","before","filtering","current","widened","Math","max","ceil","setPage","read","length","pagination","rowCount","hasMore","onPageChange","onPageSizeChange","setPageSize","matches","shown","lastMatchPage","next"],"mappings":"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"}
|