@aglyn/shared-ui-jsx 1.0.0-beta.199 → 1.0.0-beta.201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/lib/components/list-filter-chips.component.d.ts +2 -2
- package/src/lib/components/list-filter-chips.component.js +6 -28
- package/src/lib/components/list-filter-chips.component.js.map +1 -1
- package/src/lib/components/list-filter-toolbar.component.d.ts +56 -0
- package/src/lib/components/list-filter-toolbar.component.js +280 -0
- package/src/lib/components/list-filter-toolbar.component.js.map +1 -0
- package/src/lib/components/list-query-notices.component.d.ts +18 -0
- package/src/lib/components/list-query-notices.component.js +11 -0
- package/src/lib/components/list-query-notices.component.js.map +1 -1
- package/src/lib/components/list-table.component.d.ts +16 -0
- package/src/lib/components/list-table.component.js +70 -8
- package/src/lib/components/list-table.component.js.map +1 -1
- package/src/lib/const/list-filter-sentence.d.ts +40 -0
- package/src/lib/const/list-filter-sentence.js +123 -0
- package/src/lib/const/list-filter-sentence.js.map +1 -0
- package/src/lib/const/list-filter.d.ts +8 -47
- package/src/lib/const/list-filter.js +2 -128
- package/src/lib/const/list-filter.js.map +1 -1
- package/src/lib/const/list-grid-filter.d.ts +39 -28
- package/src/lib/const/list-grid-filter.js +59 -71
- package/src/lib/const/list-grid-filter.js.map +1 -1
- package/src/lib/const/list-query-plan.d.ts +5 -0
- package/src/lib/const/list-query-plan.js.map +1 -1
- package/src/lib/hooks/use-list-grid-filter.d.ts +7 -11
- package/src/lib/hooks/use-list-grid-filter.js +11 -12
- package/src/lib/hooks/use-list-grid-filter.js.map +1 -1
- package/src/lib/hooks/use-list-rows-filter.d.ts +0 -78
- package/src/lib/hooks/use-list-rows-filter.js +0 -86
- package/src/lib/hooks/use-list-rows-filter.js.map +0 -1
- package/src/lib/hooks/use-paged-rows-filter.d.ts +0 -80
- package/src/lib/hooks/use-paged-rows-filter.js +0 -129
- package/src/lib/hooks/use-paged-rows-filter.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aglyn/shared-ui-jsx",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.201",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"homepage": "https://aglyn.com",
|
|
6
6
|
"repository": {
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
"./package.json": "./package.json"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@aglyn/shared-data-mdi": "1.0.0-beta.
|
|
29
|
-
"@aglyn/shared-ui-theme": "1.0.0-beta.
|
|
30
|
-
"@aglyn/shared-util-tools": "1.0.0-beta.
|
|
31
|
-
"@aglyn/shared-util-vendor": "1.0.0-beta.
|
|
28
|
+
"@aglyn/shared-data-mdi": "1.0.0-beta.201",
|
|
29
|
+
"@aglyn/shared-ui-theme": "1.0.0-beta.201",
|
|
30
|
+
"@aglyn/shared-util-tools": "1.0.0-beta.201",
|
|
31
|
+
"@aglyn/shared-util-vendor": "1.0.0-beta.201",
|
|
32
32
|
"@mdi/js": "^7.4.47",
|
|
33
33
|
"@swc/helpers": "0.5.23",
|
|
34
34
|
"change-case": "^5.4.4",
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
-
import {
|
|
18
|
-
import type
|
|
17
|
+
import type { ListFilterField } from '../const/list-filter';
|
|
18
|
+
import { type ListFilterClause, type ListFilterOption } from '../const/list-grid-filter';
|
|
19
19
|
export interface ListFilterChipsProps {
|
|
20
20
|
/** The list's grammar — every field a clause may name. */
|
|
21
21
|
fields: readonly ListFilterField[];
|
|
@@ -16,18 +16,7 @@
|
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
18
18
|
import { Chip, Stack, Tooltip } from "@mui/material";
|
|
19
|
-
import {
|
|
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
|
-
};
|
|
19
|
+
import { listFilterClauseSentence } from "../const/list-grid-filter.js";
|
|
31
20
|
/**
|
|
32
21
|
* Every clause a list is narrowed by, as chips over the grid (AGL-2617,
|
|
33
22
|
* AGL-3313; shared since AGL-3317).
|
|
@@ -41,22 +30,11 @@ const dayLabel = (raw)=>{
|
|
|
41
30
|
* narrow the loaded rows, the served one is marked.
|
|
42
31
|
*/ export function ListFilterChips(props) {
|
|
43
32
|
const { fields, headers, clauses, onChange, options = {}, servedField = null, marksServed = props.servedField !== undefined, disabled = false } = props;
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
};
|
|
33
|
+
/** How a clause reads — the one wording the notices use too. */ const sentence = (clause)=>listFilterClauseSentence(clause, {
|
|
34
|
+
fields,
|
|
35
|
+
headers,
|
|
36
|
+
options
|
|
37
|
+
});
|
|
60
38
|
if (!clauses.length) return null;
|
|
61
39
|
return /*#__PURE__*/ _jsx(Stack, {
|
|
62
40
|
direction: "row",
|
|
@@ -1 +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 {
|
|
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 } from '../const/list-filter'\nimport {\n type ListFilterClause,\n type ListFilterOption,\n listFilterClauseSentence,\n} 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/**\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 /** How a clause reads — the one wording the notices use too. */\n const sentence = (clause: ListFilterClause): string =>\n listFilterClauseSentence(clause, { fields, headers, options })\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","listFilterClauseSentence","ListFilterChips","props","fields","headers","clauses","onChange","options","servedField","marksServed","undefined","disabled","sentence","clause","length","direction","spacing","useFlexGap","sx","alignItems","flexWrap","role","aria-label","map","index","served","field","key","op","chip","size","variant","color","label","onDelete","filter","_entry","at","title","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;AAEA,SAASA,IAAI,EAAEC,KAAK,EAAEC,OAAO,QAAQ,gBAAe;AAEpD,SAGEC,wBAAwB,QACnB,+BAA2B;AAyBlC;;;;;;;;;;;CAWC,GACD,OAAO,SAASC,gBAAgBC,KAA2B;IACzD,MAAM,EACJC,MAAM,EACNC,OAAO,EACPC,OAAO,EACPC,QAAQ,EACRC,UAAU,CAAC,CAAC,EACZC,cAAc,IAAI,EAClBC,cAAcP,MAAMM,WAAW,KAAKE,SAAS,EAC7CC,WAAW,KAAK,EACjB,GAAGT;IACJ,8DAA8D,GAC9D,MAAMU,WAAW,CAACC,SAChBb,yBAAyBa,QAAQ;YAAEV;YAAQC;YAASG;QAAQ;IAE9D,IAAI,CAACF,QAAQS,MAAM,EAAE,OAAO;IAC5B,qBACE,KAAChB;QACCiB,WAAU;QACVC,SAAS;QACTC,UAAU;QACVC,IAAI;YAAEC,YAAY;YAAUC,UAAU;QAAO;QAC7CC,MAAK;QACLC,cAAW;kBAEVjB,QAAQkB,GAAG,CAAC,CAACV,QAAQW;YACpB,MAAMC,SAAShB,eAAeD,gBAAgB,QAAQK,OAAOa,KAAK,KAAKlB;YACvE,MAAMmB,MAAM,GAAGd,OAAOa,KAAK,CAAC,CAAC,EAAEb,OAAOe,EAAE,CAAC,CAAC,EAAEJ,OAAO;YACnD,MAAMK,qBACJ,KAAChC;gBAECwB,MAAK;gBACLS,MAAK;gBACLC,SAASN,SAAS,WAAW;gBAC7BO,OAAOP,SAAS,YAAY;gBAC5BQ,OAAOrB,SAASC;gBAChBF,UAAUA;gBACVuB,UAAU,IAAM5B,SAASD,QAAQ8B,MAAM,CAAC,CAACC,QAAQC,KAAOA,OAAOb;eAP1DG;YAUT,OAAOlB,4BACL,KAACV;gBAECuC,OAAOb,SAAS,iCAAiC;0BAEhDI;eAHIF,OAMPE;QAEJ;;AAGN;AACA5B,gBAAgBsC,WAAW,GAAG;AAE9B,eAAetC,gBAAe"}
|
|
@@ -0,0 +1,56 @@
|
|
|
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, type ListFilterOption } from '../const/list-grid-filter';
|
|
19
|
+
export interface ListFilterToolbarProps {
|
|
20
|
+
/** The list's grammar — every field the Filters panel may name. */
|
|
21
|
+
fields: readonly ListFilterField[];
|
|
22
|
+
/** How a field reads in the panel and on its chip. */
|
|
23
|
+
headers: Readonly<Record<string, string>>;
|
|
24
|
+
/** Choices per field, for a field whose value is picked rather than typed. */
|
|
25
|
+
options?: Readonly<Record<string, readonly ListFilterOption[]>>;
|
|
26
|
+
clauses: readonly ListFilterClause[];
|
|
27
|
+
onChange: (clauses: ListFilterClause[]) => void;
|
|
28
|
+
/** The quick search. Absent, the list offers no search box. */
|
|
29
|
+
search?: {
|
|
30
|
+
words: readonly string[];
|
|
31
|
+
onChange: (words: string[]) => void;
|
|
32
|
+
placeholder?: string;
|
|
33
|
+
};
|
|
34
|
+
disabled?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* THE LIST TOOLBAR, FOR A LIST THAT IS NOT A TABLE (AGL-3321).
|
|
38
|
+
*
|
|
39
|
+
* A table's search box and Filters panel are the grid's own toolbar. A list
|
|
40
|
+
* drawn as cards, tiles or a feed has no grid, and must not become one to get
|
|
41
|
+
* them — its presentation is not the filter's to change. This is the same two
|
|
42
|
+
* controls over any presentation: a search box whose words the list puts on
|
|
43
|
+
* its query, and a Filters button whose panel adds one clause per field, in
|
|
44
|
+
* the same grammar (`ListFilterField`) and the same clause shape
|
|
45
|
+
* (`ListFilterClause`) the grid's panel produces. The clauses in force show
|
|
46
|
+
* as `ListFilterChips` beside it, and what the query could not take as
|
|
47
|
+
* `ListQueryNotices`, exactly as over a table.
|
|
48
|
+
*
|
|
49
|
+
* It offers only the operators each field declares (`listFilterOperators`),
|
|
50
|
+
* which are the ones the list's query can serve.
|
|
51
|
+
*/
|
|
52
|
+
export declare function ListFilterToolbar(props: ListFilterToolbarProps): import("react").JSX.Element;
|
|
53
|
+
export declare namespace ListFilterToolbar {
|
|
54
|
+
var displayName: string;
|
|
55
|
+
}
|
|
56
|
+
export default ListFilterToolbar;
|
|
@@ -0,0 +1,280 @@
|
|
|
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, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
18
|
+
import { Badge, Button, MenuItem, Popover, Stack, TextField } from "@mui/material";
|
|
19
|
+
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
20
|
+
import { listFilterOperatorLabel, listFilterOperators } from "../const/list-filter.js";
|
|
21
|
+
import { upsertListFilterClause } from "../const/list-grid-filter.js";
|
|
22
|
+
/** Operators that carry no value. */ const VALUELESS = new Set([
|
|
23
|
+
'isEmpty',
|
|
24
|
+
'isNotEmpty'
|
|
25
|
+
]);
|
|
26
|
+
/** How long the search box waits after the last keystroke before asking. */ const SEARCH_SETTLE_MS = 300;
|
|
27
|
+
/**
|
|
28
|
+
* THE LIST TOOLBAR, FOR A LIST THAT IS NOT A TABLE (AGL-3321).
|
|
29
|
+
*
|
|
30
|
+
* A table's search box and Filters panel are the grid's own toolbar. A list
|
|
31
|
+
* drawn as cards, tiles or a feed has no grid, and must not become one to get
|
|
32
|
+
* them — its presentation is not the filter's to change. This is the same two
|
|
33
|
+
* controls over any presentation: a search box whose words the list puts on
|
|
34
|
+
* its query, and a Filters button whose panel adds one clause per field, in
|
|
35
|
+
* the same grammar (`ListFilterField`) and the same clause shape
|
|
36
|
+
* (`ListFilterClause`) the grid's panel produces. The clauses in force show
|
|
37
|
+
* as `ListFilterChips` beside it, and what the query could not take as
|
|
38
|
+
* `ListQueryNotices`, exactly as over a table.
|
|
39
|
+
*
|
|
40
|
+
* It offers only the operators each field declares (`listFilterOperators`),
|
|
41
|
+
* which are the ones the list's query can serve.
|
|
42
|
+
*/ export function ListFilterToolbar(props) {
|
|
43
|
+
var _ref, _ref1, _operators_, _search_placeholder, _search_placeholder1, _ref2;
|
|
44
|
+
var _fields_;
|
|
45
|
+
const { fields, headers, options = {}, clauses, onChange, search, disabled = false } = props;
|
|
46
|
+
const panelId = useId();
|
|
47
|
+
const anchor = useRef(null);
|
|
48
|
+
const [open, setOpen] = useState(false);
|
|
49
|
+
// The search box is the reader's until they pause; then its words are asked.
|
|
50
|
+
const [typed, setTyped] = useState(()=>{
|
|
51
|
+
var _ref;
|
|
52
|
+
return ((_ref = search == null ? void 0 : search.words) != null ? _ref : []).join(' ');
|
|
53
|
+
});
|
|
54
|
+
const asked = ((_ref = search == null ? void 0 : search.words) != null ? _ref : []).join(' ');
|
|
55
|
+
const onSearch = search == null ? void 0 : search.onChange;
|
|
56
|
+
useEffect(()=>{
|
|
57
|
+
if (!onSearch) return;
|
|
58
|
+
const words = typed.split(/\s+/).filter(Boolean);
|
|
59
|
+
if (words.join(' ') === asked) return;
|
|
60
|
+
const timer = setTimeout(()=>onSearch(words), SEARCH_SETTLE_MS);
|
|
61
|
+
return ()=>clearTimeout(timer);
|
|
62
|
+
}, [
|
|
63
|
+
typed,
|
|
64
|
+
asked,
|
|
65
|
+
onSearch
|
|
66
|
+
]);
|
|
67
|
+
const label = (column)=>{
|
|
68
|
+
var _headers_column;
|
|
69
|
+
return (_headers_column = headers[column]) != null ? _headers_column : column;
|
|
70
|
+
};
|
|
71
|
+
const [column, setColumn] = useState((_ref1 = (_fields_ = fields[0]) == null ? void 0 : _fields_.column) != null ? _ref1 : '');
|
|
72
|
+
const field = useMemo(()=>{
|
|
73
|
+
var _fields_find;
|
|
74
|
+
return (_fields_find = fields.find((entry)=>entry.column === column)) != null ? _fields_find : fields[0];
|
|
75
|
+
}, [
|
|
76
|
+
fields,
|
|
77
|
+
column
|
|
78
|
+
]);
|
|
79
|
+
const operators = useMemo(()=>field ? listFilterOperators(field) : [], [
|
|
80
|
+
field
|
|
81
|
+
]);
|
|
82
|
+
const [op, setOp] = useState((_operators_ = operators[0]) != null ? _operators_ : '');
|
|
83
|
+
const [value, setValue] = useState('');
|
|
84
|
+
// Opening the panel on a field shows the clause it already holds, if any.
|
|
85
|
+
const pick = (next)=>{
|
|
86
|
+
var _nextOperators_, _ref;
|
|
87
|
+
const nextField = fields.find((entry)=>entry.column === next);
|
|
88
|
+
const held = clauses.find((clause)=>clause.field === next);
|
|
89
|
+
const nextOperators = nextField ? listFilterOperators(nextField) : [];
|
|
90
|
+
setColumn(next);
|
|
91
|
+
setOp(held && nextOperators.includes(held.op) ? held.op : (_nextOperators_ = nextOperators[0]) != null ? _nextOperators_ : '');
|
|
92
|
+
setValue((_ref = held == null ? void 0 : held.value) != null ? _ref : '');
|
|
93
|
+
};
|
|
94
|
+
useEffect(()=>{
|
|
95
|
+
if (open) pick(column);
|
|
96
|
+
// Re-read only when the panel opens.
|
|
97
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
98
|
+
}, [
|
|
99
|
+
open
|
|
100
|
+
]);
|
|
101
|
+
const choices = field ? options[field.column] : undefined;
|
|
102
|
+
const booleanChoices = [
|
|
103
|
+
{
|
|
104
|
+
value: 'true',
|
|
105
|
+
label: 'Yes'
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
value: 'false',
|
|
109
|
+
label: 'No'
|
|
110
|
+
}
|
|
111
|
+
];
|
|
112
|
+
const picked = choices != null ? choices : (field == null ? void 0 : field.kind) === 'boolean' ? booleanChoices : undefined;
|
|
113
|
+
const valueless = VALUELESS.has(op);
|
|
114
|
+
const ready = Boolean(field && op && (valueless || value.trim()));
|
|
115
|
+
const apply = ()=>{
|
|
116
|
+
if (!field || !ready) return;
|
|
117
|
+
onChange(upsertListFilterClause(clauses, field.column, {
|
|
118
|
+
field: field.column,
|
|
119
|
+
op,
|
|
120
|
+
value: valueless ? '' : value.trim()
|
|
121
|
+
}));
|
|
122
|
+
setOpen(false);
|
|
123
|
+
};
|
|
124
|
+
return /*#__PURE__*/ _jsxs(Stack, {
|
|
125
|
+
direction: {
|
|
126
|
+
xs: 'column',
|
|
127
|
+
sm: 'row'
|
|
128
|
+
},
|
|
129
|
+
spacing: 1,
|
|
130
|
+
sx: {
|
|
131
|
+
alignItems: {
|
|
132
|
+
sm: 'center'
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
children: [
|
|
136
|
+
search ? /*#__PURE__*/ _jsx(TextField, {
|
|
137
|
+
size: "small",
|
|
138
|
+
type: "search",
|
|
139
|
+
value: typed,
|
|
140
|
+
disabled: disabled,
|
|
141
|
+
onChange: (event)=>setTyped(event.target.value),
|
|
142
|
+
placeholder: (_search_placeholder = search.placeholder) != null ? _search_placeholder : 'Search…',
|
|
143
|
+
slotProps: {
|
|
144
|
+
htmlInput: {
|
|
145
|
+
'aria-label': (_search_placeholder1 = search.placeholder) != null ? _search_placeholder1 : 'Search'
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
sx: {
|
|
149
|
+
minWidth: {
|
|
150
|
+
sm: 280
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}) : null,
|
|
154
|
+
fields.length ? /*#__PURE__*/ _jsxs(_Fragment, {
|
|
155
|
+
children: [
|
|
156
|
+
/*#__PURE__*/ _jsx(Badge, {
|
|
157
|
+
badgeContent: clauses.length,
|
|
158
|
+
color: "primary",
|
|
159
|
+
overlap: "rectangular",
|
|
160
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
161
|
+
ref: anchor,
|
|
162
|
+
variant: "outlined",
|
|
163
|
+
size: "medium",
|
|
164
|
+
disabled: disabled,
|
|
165
|
+
"aria-haspopup": "dialog",
|
|
166
|
+
"aria-controls": open ? panelId : undefined,
|
|
167
|
+
"aria-expanded": open,
|
|
168
|
+
onClick: ()=>setOpen(true),
|
|
169
|
+
children: 'Filters'
|
|
170
|
+
})
|
|
171
|
+
}),
|
|
172
|
+
/*#__PURE__*/ _jsx(Popover, {
|
|
173
|
+
id: panelId,
|
|
174
|
+
open: open,
|
|
175
|
+
anchorEl: anchor.current,
|
|
176
|
+
onClose: ()=>setOpen(false),
|
|
177
|
+
anchorOrigin: {
|
|
178
|
+
vertical: 'bottom',
|
|
179
|
+
horizontal: 'left'
|
|
180
|
+
},
|
|
181
|
+
slotProps: {
|
|
182
|
+
paper: {
|
|
183
|
+
role: 'dialog',
|
|
184
|
+
'aria-label': 'Filters'
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
children: /*#__PURE__*/ _jsxs(Stack, {
|
|
188
|
+
component: "form",
|
|
189
|
+
spacing: 1.5,
|
|
190
|
+
sx: {
|
|
191
|
+
p: 2,
|
|
192
|
+
width: {
|
|
193
|
+
xs: 'calc(100vw - 32px)',
|
|
194
|
+
sm: 340
|
|
195
|
+
},
|
|
196
|
+
maxWidth: 340
|
|
197
|
+
},
|
|
198
|
+
onSubmit: (event)=>{
|
|
199
|
+
event.preventDefault();
|
|
200
|
+
apply();
|
|
201
|
+
},
|
|
202
|
+
children: [
|
|
203
|
+
/*#__PURE__*/ _jsx(TextField, {
|
|
204
|
+
select: true,
|
|
205
|
+
size: "small",
|
|
206
|
+
label: "Field",
|
|
207
|
+
value: (_ref2 = field == null ? void 0 : field.column) != null ? _ref2 : '',
|
|
208
|
+
onChange: (event)=>pick(event.target.value),
|
|
209
|
+
children: fields.map((entry)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
210
|
+
value: entry.column,
|
|
211
|
+
children: label(entry.column)
|
|
212
|
+
}, entry.column))
|
|
213
|
+
}),
|
|
214
|
+
operators.length > 1 ? /*#__PURE__*/ _jsx(TextField, {
|
|
215
|
+
select: true,
|
|
216
|
+
size: "small",
|
|
217
|
+
label: "Operator",
|
|
218
|
+
value: op,
|
|
219
|
+
onChange: (event)=>setOp(event.target.value),
|
|
220
|
+
children: operators.map((entry)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
221
|
+
value: entry,
|
|
222
|
+
children: listFilterOperatorLabel(entry)
|
|
223
|
+
}, entry))
|
|
224
|
+
}) : null,
|
|
225
|
+
valueless ? null : picked ? /*#__PURE__*/ _jsx(TextField, {
|
|
226
|
+
select: true,
|
|
227
|
+
size: "small",
|
|
228
|
+
label: "Value",
|
|
229
|
+
value: value,
|
|
230
|
+
onChange: (event)=>setValue(event.target.value),
|
|
231
|
+
children: picked.map((choice)=>/*#__PURE__*/ _jsx(MenuItem, {
|
|
232
|
+
value: choice.value,
|
|
233
|
+
children: choice.label
|
|
234
|
+
}, choice.value))
|
|
235
|
+
}) : /*#__PURE__*/ _jsx(TextField, {
|
|
236
|
+
size: "small",
|
|
237
|
+
label: "Value",
|
|
238
|
+
type: (field == null ? void 0 : field.kind) === 'date' ? 'date' : (field == null ? void 0 : field.kind) === 'number' ? 'number' : 'text',
|
|
239
|
+
value: value,
|
|
240
|
+
onChange: (event)=>setValue(event.target.value),
|
|
241
|
+
slotProps: (field == null ? void 0 : field.kind) === 'date' ? {
|
|
242
|
+
inputLabel: {
|
|
243
|
+
shrink: true
|
|
244
|
+
}
|
|
245
|
+
} : undefined
|
|
246
|
+
}),
|
|
247
|
+
/*#__PURE__*/ _jsxs(Stack, {
|
|
248
|
+
direction: "row",
|
|
249
|
+
spacing: 1,
|
|
250
|
+
sx: {
|
|
251
|
+
justifyContent: 'flex-end'
|
|
252
|
+
},
|
|
253
|
+
children: [
|
|
254
|
+
field && clauses.some((clause)=>clause.field === field.column) ? /*#__PURE__*/ _jsx(Button, {
|
|
255
|
+
onClick: ()=>{
|
|
256
|
+
onChange(upsertListFilterClause(clauses, field.column, null));
|
|
257
|
+
setOpen(false);
|
|
258
|
+
},
|
|
259
|
+
children: 'Remove'
|
|
260
|
+
}) : null,
|
|
261
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
262
|
+
type: "submit",
|
|
263
|
+
variant: "contained",
|
|
264
|
+
disabled: !ready,
|
|
265
|
+
children: 'Apply'
|
|
266
|
+
})
|
|
267
|
+
]
|
|
268
|
+
})
|
|
269
|
+
]
|
|
270
|
+
})
|
|
271
|
+
})
|
|
272
|
+
]
|
|
273
|
+
}) : null
|
|
274
|
+
]
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
ListFilterToolbar.displayName = 'ListFilterToolbar';
|
|
278
|
+
export default ListFilterToolbar;
|
|
279
|
+
|
|
280
|
+
//# sourceMappingURL=list-filter-toolbar.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/list-filter-toolbar.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 {\n Badge,\n Button,\n MenuItem,\n Popover,\n Stack,\n TextField,\n} from '@mui/material'\nimport { useEffect, useId, useMemo, useRef, useState } from 'react'\nimport {\n type ListFilterField,\n listFilterOperatorLabel,\n listFilterOperators,\n} from '../const/list-filter'\nimport {\n type ListFilterClause,\n type ListFilterOption,\n upsertListFilterClause,\n} from '../const/list-grid-filter'\n\n/** Operators that carry no value. */\nconst VALUELESS = new Set(['isEmpty', 'isNotEmpty'])\n\n/** How long the search box waits after the last keystroke before asking. */\nconst SEARCH_SETTLE_MS = 300\n\nexport interface ListFilterToolbarProps {\n /** The list's grammar — every field the Filters panel may name. */\n fields: readonly ListFilterField[]\n /** How a field reads in the panel and on its chip. */\n headers: Readonly<Record<string, string>>\n /** Choices per field, for a field whose value is picked rather than typed. */\n options?: Readonly<Record<string, readonly ListFilterOption[]>>\n clauses: readonly ListFilterClause[]\n onChange: (clauses: ListFilterClause[]) => void\n /** The quick search. Absent, the list offers no search box. */\n search?: {\n words: readonly string[]\n onChange: (words: string[]) => void\n placeholder?: string\n }\n disabled?: boolean\n}\n\n/**\n * THE LIST TOOLBAR, FOR A LIST THAT IS NOT A TABLE (AGL-3321).\n *\n * A table's search box and Filters panel are the grid's own toolbar. A list\n * drawn as cards, tiles or a feed has no grid, and must not become one to get\n * them — its presentation is not the filter's to change. This is the same two\n * controls over any presentation: a search box whose words the list puts on\n * its query, and a Filters button whose panel adds one clause per field, in\n * the same grammar (`ListFilterField`) and the same clause shape\n * (`ListFilterClause`) the grid's panel produces. The clauses in force show\n * as `ListFilterChips` beside it, and what the query could not take as\n * `ListQueryNotices`, exactly as over a table.\n *\n * It offers only the operators each field declares (`listFilterOperators`),\n * which are the ones the list's query can serve.\n */\nexport function ListFilterToolbar(props: ListFilterToolbarProps) {\n const { fields, headers, options = {}, clauses, onChange, search, disabled = false } = props\n const panelId = useId()\n const anchor = useRef<HTMLButtonElement | null>(null)\n const [open, setOpen] = useState(false)\n\n // The search box is the reader's until they pause; then its words are asked.\n const [typed, setTyped] = useState(() => (search?.words ?? []).join(' '))\n const asked = (search?.words ?? []).join(' ')\n const onSearch = search?.onChange\n useEffect(() => {\n if (!onSearch) return\n const words = typed.split(/\\s+/).filter(Boolean)\n if (words.join(' ') === asked) return\n const timer = setTimeout(() => onSearch(words), SEARCH_SETTLE_MS)\n return () => clearTimeout(timer)\n }, [typed, asked, onSearch])\n\n const label = (column: string) => headers[column] ?? column\n const [column, setColumn] = useState<string>(fields[0]?.column ?? '')\n const field = useMemo(\n () => fields.find((entry) => entry.column === column) ?? fields[0],\n [fields, column],\n )\n const operators = useMemo(() => (field ? listFilterOperators(field) : []), [field])\n const [op, setOp] = useState<string>(operators[0] ?? '')\n const [value, setValue] = useState('')\n\n // Opening the panel on a field shows the clause it already holds, if any.\n const pick = (next: string) => {\n const nextField = fields.find((entry) => entry.column === next)\n const held = clauses.find((clause) => clause.field === next)\n const nextOperators = nextField ? listFilterOperators(nextField) : []\n setColumn(next)\n setOp(held && nextOperators.includes(held.op) ? held.op : (nextOperators[0] ?? ''))\n setValue(held?.value ?? '')\n }\n useEffect(() => {\n if (open) pick(column)\n // Re-read only when the panel opens.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [open])\n\n const choices = field ? options[field.column] : undefined\n const booleanChoices: readonly ListFilterOption[] = [\n { value: 'true', label: 'Yes' },\n { value: 'false', label: 'No' },\n ]\n const picked = choices ?? (field?.kind === 'boolean' ? booleanChoices : undefined)\n const valueless = VALUELESS.has(op)\n const ready = Boolean(field && op && (valueless || value.trim()))\n\n const apply = () => {\n if (!field || !ready) return\n onChange(\n upsertListFilterClause(clauses, field.column, {\n field: field.column,\n op,\n value: valueless ? '' : value.trim(),\n }),\n )\n setOpen(false)\n }\n\n return (\n <Stack\n direction={{ xs: 'column', sm: 'row' }}\n spacing={1}\n sx={{ alignItems: { sm: 'center' } }}\n >\n {search ? (\n <TextField\n size=\"small\"\n type=\"search\"\n value={typed}\n disabled={disabled}\n onChange={(event) => setTyped(event.target.value)}\n placeholder={search.placeholder ?? 'Search…'}\n slotProps={{ htmlInput: { 'aria-label': search.placeholder ?? 'Search' } }}\n sx={{ minWidth: { sm: 280 } }}\n />\n ) : null}\n {fields.length ? (\n <>\n <Badge badgeContent={clauses.length} color=\"primary\" overlap=\"rectangular\">\n <Button\n ref={anchor}\n variant=\"outlined\"\n size=\"medium\"\n disabled={disabled}\n aria-haspopup=\"dialog\"\n aria-controls={open ? panelId : undefined}\n aria-expanded={open}\n onClick={() => setOpen(true)}\n >\n {'Filters'}\n </Button>\n </Badge>\n <Popover\n id={panelId}\n open={open}\n anchorEl={anchor.current}\n onClose={() => setOpen(false)}\n anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}\n slotProps={{ paper: { role: 'dialog', 'aria-label': 'Filters' } as never }}\n >\n <Stack\n component=\"form\"\n spacing={1.5}\n sx={{ p: 2, width: { xs: 'calc(100vw - 32px)', sm: 340 }, maxWidth: 340 }}\n onSubmit={(event) => {\n event.preventDefault()\n apply()\n }}\n >\n <TextField\n select\n size=\"small\"\n label=\"Field\"\n value={field?.column ?? ''}\n onChange={(event) => pick(event.target.value)}\n >\n {fields.map((entry) => (\n <MenuItem key={entry.column} value={entry.column}>\n {label(entry.column)}\n </MenuItem>\n ))}\n </TextField>\n {operators.length > 1 ? (\n <TextField\n select\n size=\"small\"\n label=\"Operator\"\n value={op}\n onChange={(event) => setOp(event.target.value)}\n >\n {operators.map((entry) => (\n <MenuItem key={entry} value={entry}>\n {listFilterOperatorLabel(entry)}\n </MenuItem>\n ))}\n </TextField>\n ) : null}\n {valueless ? null : picked ? (\n <TextField\n select\n size=\"small\"\n label=\"Value\"\n value={value}\n onChange={(event) => setValue(event.target.value)}\n >\n {picked.map((choice) => (\n <MenuItem key={choice.value} value={choice.value}>\n {choice.label}\n </MenuItem>\n ))}\n </TextField>\n ) : (\n <TextField\n size=\"small\"\n label=\"Value\"\n type={field?.kind === 'date' ? 'date' : field?.kind === 'number' ? 'number' : 'text'}\n value={value}\n onChange={(event) => setValue(event.target.value)}\n slotProps={field?.kind === 'date' ? { inputLabel: { shrink: true } } : undefined}\n />\n )}\n <Stack direction=\"row\" spacing={1} sx={{ justifyContent: 'flex-end' }}>\n {field && clauses.some((clause) => clause.field === field.column) ? (\n <Button\n onClick={() => {\n onChange(upsertListFilterClause(clauses, field.column, null))\n setOpen(false)\n }}\n >\n {'Remove'}\n </Button>\n ) : null}\n <Button type=\"submit\" variant=\"contained\" disabled={!ready}>\n {'Apply'}\n </Button>\n </Stack>\n </Stack>\n </Popover>\n </>\n ) : null}\n </Stack>\n )\n}\nListFilterToolbar.displayName = 'ListFilterToolbar'\n\nexport default ListFilterToolbar\n"],"names":["Badge","Button","MenuItem","Popover","Stack","TextField","useEffect","useId","useMemo","useRef","useState","listFilterOperatorLabel","listFilterOperators","upsertListFilterClause","VALUELESS","Set","SEARCH_SETTLE_MS","ListFilterToolbar","props","operators","search","fields","headers","options","clauses","onChange","disabled","panelId","anchor","open","setOpen","typed","setTyped","words","join","asked","onSearch","split","filter","Boolean","timer","setTimeout","clearTimeout","label","column","setColumn","field","find","entry","op","setOp","value","setValue","pick","next","nextOperators","nextField","held","clause","includes","choices","undefined","booleanChoices","picked","kind","valueless","has","ready","trim","apply","direction","xs","sm","spacing","sx","alignItems","size","type","event","target","placeholder","slotProps","htmlInput","minWidth","length","badgeContent","color","overlap","ref","variant","aria-haspopup","aria-controls","aria-expanded","onClick","id","anchorEl","current","onClose","anchorOrigin","vertical","horizontal","paper","role","component","p","width","maxWidth","onSubmit","preventDefault","select","map","choice","inputLabel","shrink","justifyContent","some","displayName"],"mappings":"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"}
|
|
@@ -14,6 +14,24 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
+
import type { ListFilterField } from '../const/list-filter';
|
|
18
|
+
import type { ListFilterClause, ListFilterOption } from '../const/list-grid-filter';
|
|
19
|
+
/**
|
|
20
|
+
* A plan's refusals as the notices read them: each refused clause by the
|
|
21
|
+
* same sentence its chip reads ("Reason is Bounced"), and the search as
|
|
22
|
+
* "Search". Every list maps `plan.refused` through this and nothing else.
|
|
23
|
+
*/
|
|
24
|
+
export declare function listQueryRefusals(refused: ReadonlyArray<{
|
|
25
|
+
clause: ListFilterClause | 'search';
|
|
26
|
+
reason: string;
|
|
27
|
+
}>, context: {
|
|
28
|
+
fields: readonly ListFilterField[];
|
|
29
|
+
headers?: Readonly<Record<string, string>>;
|
|
30
|
+
options?: Readonly<Record<string, readonly ListFilterOption[]>>;
|
|
31
|
+
}): Array<{
|
|
32
|
+
label: string;
|
|
33
|
+
reason: string;
|
|
34
|
+
}>;
|
|
17
35
|
export interface ListQueryNoticesProps {
|
|
18
36
|
/**
|
|
19
37
|
* What the list was asked and could not put on its query, each as the thing
|
|
@@ -16,6 +16,17 @@
|
|
|
16
16
|
*/ 'use client';
|
|
17
17
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
18
18
|
import { Alert, Stack } from "@mui/material";
|
|
19
|
+
import { listFilterClauseSentence } from "../const/list-filter-sentence.js";
|
|
20
|
+
/**
|
|
21
|
+
* A plan's refusals as the notices read them: each refused clause by the
|
|
22
|
+
* same sentence its chip reads ("Reason is Bounced"), and the search as
|
|
23
|
+
* "Search". Every list maps `plan.refused` through this and nothing else.
|
|
24
|
+
*/ export function listQueryRefusals(refused, context) {
|
|
25
|
+
return refused.map((entry)=>({
|
|
26
|
+
label: entry.clause === 'search' ? 'Search' : listFilterClauseSentence(entry.clause, context),
|
|
27
|
+
reason: entry.reason
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
19
30
|
/**
|
|
20
31
|
* WHAT THE QUERY DID NOT TAKE, said above the list (AGL-3321).
|
|
21
32
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/list-query-notices.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 { Alert, Stack } from '@mui/material'\n\nexport interface ListQueryNoticesProps {\n /**\n * What the list was asked and could not put on its query, each as the thing\n * asked (\"Tags contains vip\", \"Search\") and why.\n */\n refused: ReadonlyArray<{ label: string; reason: string }>\n /** Said about what WAS served, e.g. that search matched one word. */\n notices?: readonly string[]\n}\n\n/**\n * WHAT THE QUERY DID NOT TAKE, said above the list (AGL-3321).\n *\n * Every filter and search word a list applies is on its Firestore query, and\n * one query holds one array clause, one range and thirty disjunctions. A\n * combination past that is not applied at all rather than applied to the\n * rows that happen to be loaded — and a filter the reader set that is not\n * applied must say so, or the list reads as the answer to a question it did\n * not ask.\n */\nexport function ListQueryNotices(props: ListQueryNoticesProps) {\n const { refused, notices = [] } = props\n if (!refused.length && !notices.length) return null\n return (\n <Stack spacing={1} role=\"status\" aria-label=\"Filter notices\">\n {refused.map((entry) => (\n <Alert key={`${entry.label}:${entry.reason}`} severity=\"warning\">\n {`${entry.label} is not applied: ${entry.reason}.`}\n </Alert>\n ))}\n {notices.map((notice) => (\n <Alert key={notice} severity=\"info\">\n {notice}\n </Alert>\n ))}\n </Stack>\n )\n}\nListQueryNotices.displayName = 'ListQueryNotices'\n\nexport default ListQueryNotices\n"],"names":["Alert","Stack","
|
|
1
|
+
{"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/components/list-query-notices.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 { Alert, Stack } from '@mui/material'\nimport type { ListFilterField } from '../const/list-filter'\nimport { listFilterClauseSentence } from '../const/list-filter-sentence'\nimport type { ListFilterClause, ListFilterOption } from '../const/list-grid-filter'\n\n/**\n * A plan's refusals as the notices read them: each refused clause by the\n * same sentence its chip reads (\"Reason is Bounced\"), and the search as\n * \"Search\". Every list maps `plan.refused` through this and nothing else.\n */\nexport function listQueryRefusals(\n refused: ReadonlyArray<{ clause: ListFilterClause | 'search'; reason: string }>,\n context: {\n fields: readonly ListFilterField[]\n headers?: Readonly<Record<string, string>>\n options?: Readonly<Record<string, readonly ListFilterOption[]>>\n },\n): Array<{ label: string; reason: string }> {\n return refused.map((entry) => ({\n label: entry.clause === 'search' ? 'Search' : listFilterClauseSentence(entry.clause, context),\n reason: entry.reason,\n }))\n}\n\nexport interface ListQueryNoticesProps {\n /**\n * What the list was asked and could not put on its query, each as the thing\n * asked (\"Tags contains vip\", \"Search\") and why.\n */\n refused: ReadonlyArray<{ label: string; reason: string }>\n /** Said about what WAS served, e.g. that search matched one word. */\n notices?: readonly string[]\n}\n\n/**\n * WHAT THE QUERY DID NOT TAKE, said above the list (AGL-3321).\n *\n * Every filter and search word a list applies is on its Firestore query, and\n * one query holds one array clause, one range and thirty disjunctions. A\n * combination past that is not applied at all rather than applied to the\n * rows that happen to be loaded — and a filter the reader set that is not\n * applied must say so, or the list reads as the answer to a question it did\n * not ask.\n */\nexport function ListQueryNotices(props: ListQueryNoticesProps) {\n const { refused, notices = [] } = props\n if (!refused.length && !notices.length) return null\n return (\n <Stack spacing={1} role=\"status\" aria-label=\"Filter notices\">\n {refused.map((entry) => (\n <Alert key={`${entry.label}:${entry.reason}`} severity=\"warning\">\n {`${entry.label} is not applied: ${entry.reason}.`}\n </Alert>\n ))}\n {notices.map((notice) => (\n <Alert key={notice} severity=\"info\">\n {notice}\n </Alert>\n ))}\n </Stack>\n )\n}\nListQueryNotices.displayName = 'ListQueryNotices'\n\nexport default ListQueryNotices\n"],"names":["Alert","Stack","listFilterClauseSentence","listQueryRefusals","refused","context","map","entry","label","clause","reason","ListQueryNotices","props","notices","length","spacing","role","aria-label","severity","notice","displayName"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;AAEA,SAASA,KAAK,EAAEC,KAAK,QAAQ,gBAAe;AAE5C,SAASC,wBAAwB,QAAQ,mCAA+B;AAGxE;;;;CAIC,GACD,OAAO,SAASC,kBACdC,OAA+E,EAC/EC,OAIC;IAED,OAAOD,QAAQE,GAAG,CAAC,CAACC,QAAW,CAAA;YAC7BC,OAAOD,MAAME,MAAM,KAAK,WAAW,WAAWP,yBAAyBK,MAAME,MAAM,EAAEJ;YACrFK,QAAQH,MAAMG,MAAM;QACtB,CAAA;AACF;AAYA;;;;;;;;;CASC,GACD,OAAO,SAASC,iBAAiBC,KAA4B;IAC3D,MAAM,EAAER,OAAO,EAAES,UAAU,EAAE,EAAE,GAAGD;IAClC,IAAI,CAACR,QAAQU,MAAM,IAAI,CAACD,QAAQC,MAAM,EAAE,OAAO;IAC/C,qBACE,MAACb;QAAMc,SAAS;QAAGC,MAAK;QAASC,cAAW;;YACzCb,QAAQE,GAAG,CAAC,CAACC,sBACZ,KAACP;oBAA6CkB,UAAS;8BACpD,GAAGX,MAAMC,KAAK,CAAC,iBAAiB,EAAED,MAAMG,MAAM,CAAC,CAAC,CAAC;mBADxC,GAAGH,MAAMC,KAAK,CAAC,CAAC,EAAED,MAAMG,MAAM,EAAE;YAI7CG,QAAQP,GAAG,CAAC,CAACa,uBACZ,KAACnB;oBAAmBkB,UAAS;8BAC1BC;mBADSA;;;AAMpB;AACAR,iBAAiBS,WAAW,GAAG;AAE/B,eAAeT,iBAAgB"}
|
|
@@ -140,6 +140,22 @@ export interface ListTableProps extends DataTableProps {
|
|
|
140
140
|
* only thing that selects, so the two gestures cannot collide.
|
|
141
141
|
*/
|
|
142
142
|
selectable?: ListTableSelection;
|
|
143
|
+
/**
|
|
144
|
+
* The toolbar, its Filters panel and its search, and none of the rows
|
|
145
|
+
* (AGL-3327).
|
|
146
|
+
*
|
|
147
|
+
* For a list the reader can also see some other way — the media library's
|
|
148
|
+
* thumbnail grid — that must filter exactly as its table view does. The
|
|
149
|
+
* panel, the quick search and Export all live inside the grid, bound to
|
|
150
|
+
* its API, so a second, hand-built toolbar for the other view would be a
|
|
151
|
+
* second filter UI to keep in step with this one. Instead the grid is
|
|
152
|
+
* drawn with its body collapsed and no rows: the toolbar stays, and the
|
|
153
|
+
* panel still opens under it (its anchor is kept, at the top of the
|
|
154
|
+
* collapsed body). The columns chooser and Export go — there are no
|
|
155
|
+
* columns on screen to choose, and the rows are the other view's to draw
|
|
156
|
+
* and to export.
|
|
157
|
+
*/
|
|
158
|
+
toolbarOnly?: boolean;
|
|
143
159
|
}
|
|
144
160
|
/**
|
|
145
161
|
* The grid every FLAT artifact list uses — layouts, components, templates.
|