@aglyn/shared-ui-jsx 1.0.0-beta.229 → 1.0.0-beta.231

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.
Files changed (40) hide show
  1. package/package.json +5 -5
  2. package/src/lib/components/copy-field.component.d.ts +25 -0
  3. package/src/lib/components/copy-field.component.js +77 -0
  4. package/src/lib/components/copy-field.component.js.map +1 -0
  5. package/src/lib/components/download-cost-badge.component.d.ts +37 -0
  6. package/src/lib/components/download-cost-badge.component.js +89 -0
  7. package/src/lib/components/download-cost-badge.component.js.map +1 -0
  8. package/src/lib/components/file-drop-zone.component.d.ts +48 -0
  9. package/src/lib/components/file-drop-zone.component.js +173 -0
  10. package/src/lib/components/file-drop-zone.component.js.map +1 -0
  11. package/src/lib/components/list-query-notices.component.d.ts +1 -18
  12. package/src/lib/components/list-query-notices.component.js +1 -11
  13. package/src/lib/components/list-query-notices.component.js.map +1 -1
  14. package/src/lib/components/searchable-virtual-list.component.d.ts +72 -0
  15. package/src/lib/components/searchable-virtual-list.component.js +212 -0
  16. package/src/lib/components/searchable-virtual-list.component.js.map +1 -0
  17. package/src/lib/components/site-live-notice.component.d.ts +39 -0
  18. package/src/lib/components/site-live-notice.component.js +95 -0
  19. package/src/lib/components/site-live-notice.component.js.map +1 -0
  20. package/src/lib/components/status-chip.component.d.ts +26 -0
  21. package/src/lib/components/status-chip.component.js +48 -0
  22. package/src/lib/components/status-chip.component.js.map +1 -0
  23. package/src/lib/const/list-filter-sentence.d.ts +1 -24
  24. package/src/lib/const/list-filter-sentence.js +3 -105
  25. package/src/lib/const/list-filter-sentence.js.map +1 -1
  26. package/src/lib/const/list-filter.d.ts +1 -219
  27. package/src/lib/const/list-filter.js +4 -282
  28. package/src/lib/const/list-filter.js.map +1 -1
  29. package/src/lib/const/list-grid-filter.d.ts +4 -25
  30. package/src/lib/const/list-grid-filter.js +20 -44
  31. package/src/lib/const/list-grid-filter.js.map +1 -1
  32. package/src/lib/const/list-query-plan.d.ts +1 -154
  33. package/src/lib/const/list-query-plan.js +4 -610
  34. package/src/lib/const/list-query-plan.js.map +1 -1
  35. package/src/lib/hooks/use-client-pagination.d.ts +25 -0
  36. package/src/lib/hooks/use-client-pagination.js +65 -0
  37. package/src/lib/hooks/use-client-pagination.js.map +1 -0
  38. package/src/lib/hooks/use-in-view.d.ts +16 -0
  39. package/src/lib/hooks/use-in-view.js +66 -0
  40. package/src/lib/hooks/use-in-view.js.map +1 -0
@@ -14,110 +14,8 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */ /*
17
- * How a list's filter clauses read as words, for the chips over a list and
18
- * the notices that say a clause was not applied (AGL-3321).
19
- *
20
- * ⛔ No `@mui/x-data-grid` here, and no runtime import of `list-filter` or
21
- * `list-grid-filter`, which build the data grid's operators. The storefront's
22
- * product grid shows these notices on customers' published pages, and
23
- * reaching the grid's operators from here would ship the grid's filter
24
- * inputs with every storefront page.
25
- */ /**
26
- * How an operator reads on a chip or in a sentence — "Owner is Dana",
27
- * "Created on or after 1 Jan". One map so every list that shows its
28
- * clauses back spells them the same way; an operator nothing here names
29
- * is shown as itself rather than hidden.
30
- */ export function listFilterOperatorLabel(op) {
31
- switch(op){
32
- case 'contains':
33
- return 'contains';
34
- case 'doesNotContain':
35
- return 'does not contain';
36
- case 'equals':
37
- case 'is':
38
- case '=':
39
- return 'is';
40
- case 'doesNotEqual':
41
- case '!=':
42
- return 'is not';
43
- case 'startsWith':
44
- return 'starts with';
45
- case 'endsWith':
46
- return 'ends with';
47
- case 'isAnyOf':
48
- return 'is any of';
49
- case 'isEmpty':
50
- return 'is empty';
51
- case 'isNotEmpty':
52
- return 'is set';
53
- case 'after':
54
- return 'after';
55
- case 'onOrAfter':
56
- return 'on or after';
57
- case 'before':
58
- return 'before';
59
- case 'onOrBefore':
60
- return 'on or before';
61
- case '>':
62
- return 'over';
63
- case '>=':
64
- return 'at least';
65
- case '<':
66
- return 'under';
67
- case '<=':
68
- return 'at most';
69
- default:
70
- return op;
71
- }
72
- }
73
- /** Operators that carry no value. */ const VALUELESS_OPERATORS = new Set([
74
- 'isEmpty',
75
- 'isNotEmpty'
76
- ]);
77
- /** Operators that take several values, comma-joined the way the grammar splits them. */ const MULTI_OPERATORS = new Set([
78
- 'isAnyOf'
79
- ]);
80
- /**
81
- * The calendar day a date clause names, as a LOCAL date.
82
- *
83
- * The panel's date input holds a day, not an instant, and hands it over as
84
- * `YYYY-MM-DD` — which `new Date` reads as UTC midnight, the previous
85
- * evening anywhere west of UTC. Travelled through `toISOString`, the same
86
- * day arrives as `YYYY-MM-DDT00:00:00.000Z`. Either spelling is read by its
87
- * date parts, so "on or after Sep 17" starts on Sep 17 wherever the reader
88
- * is. Any other value is an instant and is read as one. The query
89
- * translators read a date clause through it too, so the day a query asks for
90
- * is the day the rows are matched by.
91
- */ const DAY_ONLY = /^(\d{4})-(\d{2})-(\d{2})(?:T00:00:00(?:\.000)?Z)?$/;
92
- export const listFilterDay = (raw)=>{
93
- const day = DAY_ONLY.exec(raw);
94
- return day ? new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3])) : new Date(raw);
95
- };
96
- /** A date clause's value as the day it names, read the way the query reads it. */ const dayLabel = (raw)=>{
97
- const at = listFilterDay(raw);
98
- return Number.isNaN(at.getTime()) ? raw : at.toLocaleDateString();
99
- };
100
- /**
101
- * How a clause reads as a sentence — "Owner is Dana", "Created on or after
102
- * 1 Jan", "Reason is any of Bounced, Complained": its header, its operator,
103
- * and its value by the label of the option it names.
104
- *
105
- * ONE wording, used by the chips over a list and by the notices that say a
106
- * clause was not applied (`listQueryRefusals`), so the two cannot describe
107
- * the same clause differently.
108
- */ export function listFilterClauseSentence(clause, context) {
109
- var _ref, _clause_label;
110
- var _context_headers, _context_options;
111
- const field = context.fields.find((entry)=>entry.column === clause.field);
112
- const header = (_ref = (_context_headers = context.headers) == null ? void 0 : _context_headers[clause.field]) != null ? _ref : clause.field;
113
- const choices = (_context_options = context.options) == null ? void 0 : _context_options[clause.field];
114
- const named = (value)=>{
115
- var _ref;
116
- var _choices_find;
117
- return (_ref = choices == null ? void 0 : (_choices_find = choices.find((option)=>option.value === value)) == null ? void 0 : _choices_find.label) != null ? _ref : value;
118
- };
119
- const value = VALUELESS_OPERATORS.has(clause.op) ? '' : MULTI_OPERATORS.has(clause.op) ? clause.value.split(',').map((entry)=>entry.trim()).filter(Boolean).map(named).join(', ') : (_clause_label = clause.label) != null ? _clause_label : (field == null ? void 0 : field.kind) === 'date' ? dayLabel(clause.value) : named(clause.value);
120
- return `${header} ${listFilterOperatorLabel(clause.op)}${value ? ` ${value}` : ''}`;
121
- }
17
+ * The list filter grammar lives in a pure module, so a surface with no web UI
18
+ * (the native app) plans the same queries; this path stays its web entry.
19
+ */ export * from "@aglyn/shared-util-tools/list-query/list-filter-sentence";
122
20
 
123
21
  //# sourceMappingURL=list-filter-sentence.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/const/list-filter-sentence.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { ListFilterField } from './list-filter'\nimport type { ListFilterClause, ListFilterOption } from './list-grid-filter'\n\n/*\n * How a list's filter clauses read as words, for the chips over a list and\n * the notices that say a clause was not applied (AGL-3321).\n *\n * ⛔ No `@mui/x-data-grid` here, and no runtime import of `list-filter` or\n * `list-grid-filter`, which build the data grid's operators. The storefront's\n * product grid shows these notices on customers' published pages, and\n * reaching the grid's operators from here would ship the grid's filter\n * inputs with every storefront page.\n */\n\n/**\n * How an operator reads on a chip or in a sentence — \"Owner is Dana\",\n * \"Created on or after 1 Jan\". One map so every list that shows its\n * clauses back spells them the same way; an operator nothing here names\n * is shown as itself rather than hidden.\n */\nexport function listFilterOperatorLabel(op: string): string {\n switch (op) {\n case 'contains':\n return 'contains'\n case 'doesNotContain':\n return 'does not contain'\n case 'equals':\n case 'is':\n case '=':\n return 'is'\n case 'doesNotEqual':\n case '!=':\n return 'is not'\n case 'startsWith':\n return 'starts with'\n case 'endsWith':\n return 'ends with'\n case 'isAnyOf':\n return 'is any of'\n case 'isEmpty':\n return 'is empty'\n case 'isNotEmpty':\n return 'is set'\n case 'after':\n return 'after'\n case 'onOrAfter':\n return 'on or after'\n case 'before':\n return 'before'\n case 'onOrBefore':\n return 'on or before'\n case '>':\n return 'over'\n case '>=':\n return 'at least'\n case '<':\n return 'under'\n case '<=':\n return 'at most'\n default:\n return op\n }\n}\n\n/** Operators that carry no value. */\nconst VALUELESS_OPERATORS = new Set(['isEmpty', 'isNotEmpty'])\n/** Operators that take several values, comma-joined the way the grammar splits them. */\nconst MULTI_OPERATORS = new Set(['isAnyOf'])\n\n/**\n * The calendar day a date clause names, as a LOCAL date.\n *\n * The panel's date input holds a day, not an instant, and hands it over as\n * `YYYY-MM-DD` — which `new Date` reads as UTC midnight, the previous\n * evening anywhere west of UTC. Travelled through `toISOString`, the same\n * day arrives as `YYYY-MM-DDT00:00:00.000Z`. Either spelling is read by its\n * date parts, so \"on or after Sep 17\" starts on Sep 17 wherever the reader\n * is. Any other value is an instant and is read as one. The query\n * translators read a date clause through it too, so the day a query asks for\n * is the day the rows are matched by.\n */\nconst DAY_ONLY = /^(\\d{4})-(\\d{2})-(\\d{2})(?:T00:00:00(?:\\.000)?Z)?$/\nexport const listFilterDay = (raw: string): Date => {\n const day = DAY_ONLY.exec(raw)\n return day ? new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3])) : new Date(raw)\n}\n\n/** A date clause's value as the day it names, read the way the query reads it. */\nconst dayLabel = (raw: string): string => {\n const at = listFilterDay(raw)\n return Number.isNaN(at.getTime()) ? raw : at.toLocaleDateString()\n}\n\n/**\n * How a clause reads as a sentence — \"Owner is Dana\", \"Created on or after\n * 1 Jan\", \"Reason is any of Bounced, Complained\": its header, its operator,\n * and its value by the label of the option it names.\n *\n * ONE wording, used by the chips over a list and by the notices that say a\n * clause was not applied (`listQueryRefusals`), so the two cannot describe\n * the same clause differently.\n */\nexport function listFilterClauseSentence(\n clause: ListFilterClause,\n context: {\n fields: readonly ListFilterField[]\n headers?: Readonly<Record<string, string>>\n options?: Readonly<Record<string, readonly ListFilterOption[]>>\n },\n): string {\n const field = context.fields.find((entry) => entry.column === clause.field)\n const header = context.headers?.[clause.field] ?? clause.field\n const choices = context.options?.[clause.field]\n const named = (value: string) =>\n choices?.find((option) => option.value === value)?.label ?? value\n const value = VALUELESS_OPERATORS.has(clause.op)\n ? ''\n : MULTI_OPERATORS.has(clause.op)\n ? clause.value\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean)\n .map(named)\n .join(', ')\n : clause.label ?? (field?.kind === 'date' ? dayLabel(clause.value) : named(clause.value))\n return `${header} ${listFilterOperatorLabel(clause.op)}${value ? ` ${value}` : ''}`\n}\n"],"names":["listFilterOperatorLabel","op","VALUELESS_OPERATORS","Set","MULTI_OPERATORS","DAY_ONLY","listFilterDay","raw","day","exec","Date","Number","dayLabel","at","isNaN","getTime","toLocaleDateString","listFilterClauseSentence","clause","context","field","fields","find","entry","column","header","headers","choices","options","named","value","option","label","has","split","map","trim","filter","Boolean","join","kind"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAKD;;;;;;;;;CASC,GAED;;;;;CAKC,GACD,OAAO,SAASA,wBAAwBC,EAAU;IAChD,OAAQA;QACN,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;QACL,KAAK;QACL,KAAK;YACH,OAAO;QACT,KAAK;QACL,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT,KAAK;YACH,OAAO;QACT;YACE,OAAOA;IACX;AACF;AAEA,mCAAmC,GACnC,MAAMC,sBAAsB,IAAIC,IAAI;IAAC;IAAW;CAAa;AAC7D,sFAAsF,GACtF,MAAMC,kBAAkB,IAAID,IAAI;IAAC;CAAU;AAE3C;;;;;;;;;;;CAWC,GACD,MAAME,WAAW;AACjB,OAAO,MAAMC,gBAAgB,CAACC;IAC5B,MAAMC,MAAMH,SAASI,IAAI,CAACF;IAC1B,OAAOC,MAAM,IAAIE,KAAKC,OAAOH,GAAG,CAAC,EAAE,GAAGG,OAAOH,GAAG,CAAC,EAAE,IAAI,GAAGG,OAAOH,GAAG,CAAC,EAAE,KAAK,IAAIE,KAAKH;AACvF,EAAC;AAED,gFAAgF,GAChF,MAAMK,WAAW,CAACL;IAChB,MAAMM,KAAKP,cAAcC;IACzB,OAAOI,OAAOG,KAAK,CAACD,GAAGE,OAAO,MAAMR,MAAMM,GAAGG,kBAAkB;AACjE;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASC,yBACdC,MAAwB,EACxBC,OAIC;cAgBKD;QAbSC,kBACCA;IAFhB,MAAMC,QAAQD,QAAQE,MAAM,CAACC,IAAI,CAAC,CAACC,QAAUA,MAAMC,MAAM,KAAKN,OAAOE,KAAK;IAC1E,MAAMK,kBAASN,mBAAAA,QAAQO,OAAO,qBAAfP,gBAAiB,CAACD,OAAOE,KAAK,CAAC,mBAAIF,OAAOE,KAAK;IAC9D,MAAMO,WAAUR,mBAAAA,QAAQS,OAAO,qBAAfT,gBAAiB,CAACD,OAAOE,KAAK,CAAC;IAC/C,MAAMS,QAAQ,CAACC;;YACbH;uBAAAA,4BAAAA,gBAAAA,QAASL,IAAI,CAAC,CAACS,SAAWA,OAAOD,KAAK,KAAKA,2BAA3CH,cAAmDK,KAAK,mBAAIF;;IAC9D,MAAMA,QAAQ5B,oBAAoB+B,GAAG,CAACf,OAAOjB,EAAE,IAC3C,KACAG,gBAAgB6B,GAAG,CAACf,OAAOjB,EAAE,IAC3BiB,OAAOY,KAAK,CACTI,KAAK,CAAC,KACNC,GAAG,CAAC,CAACZ,QAAUA,MAAMa,IAAI,IACzBC,MAAM,CAACC,SACPH,GAAG,CAACN,OACJU,IAAI,CAAC,SACRrB,gBAAAA,OAAOc,KAAK,YAAZd,gBAAiBE,CAAAA,yBAAAA,MAAOoB,IAAI,MAAK,SAAS5B,SAASM,OAAOY,KAAK,IAAID,MAAMX,OAAOY,KAAK;IAC3F,OAAO,GAAGL,OAAO,CAAC,EAAEzB,wBAAwBkB,OAAOjB,EAAE,IAAI6B,QAAQ,CAAC,CAAC,EAAEA,OAAO,GAAG,IAAI;AACrF"}
1
+ {"version":3,"sources":["../../../../../../../../libs/shared/ui/jsx/src/lib/const/list-filter-sentence.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*\n * The list filter grammar lives in a pure module, so a surface with no web UI\n * (the native app) plans the same queries; this path stays its web entry.\n */\nexport * from '@aglyn/shared-util-tools/list-query/list-filter-sentence'\n"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;CAGC,GACD,cAAc,2DAA0D"}
@@ -14,222 +14,4 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- /**
18
- * ONE declaration of what a paged list can be filtered by (AGL-2501).
19
- *
20
- * A paged list filters on the SERVER or it does not filter: `filterMode="server"`
21
- * stops the grid narrowing anything itself, so the panel shows the rows already
22
- * fetched — ten of them by default — and an operator the route does not answer
23
- * becomes a control that quietly does nothing. On a staff list whose whole job
24
- * is that nobody is missing, "no matches" is the one answer it must never give
25
- * wrongly.
26
- *
27
- * So a list declares its filterable fields ONCE, in a module both its route and
28
- * its page import. The declaration decides two things that must never disagree:
29
- * which operators the menu offers (here) and which predicates the query builds
30
- * (`applyListFilter`, server-side, which needs firebase-admin and so cannot
31
- * live in this file).
32
- *
33
- * ⛔ No runtime import of `@mui/x-data-grid` here. This grammar is read by
34
- * `list-query-plan`, which the storefront's server-side catalog query reaches
35
- * from a published page, and the grid's filter inputs are `'use client'`
36
- * modules: imported from the server graph they become client references that
37
- * every published page downloads. The grid's own operator objects are built
38
- * in `list-grid-filter` (`listFilterColumn`, `hiddenFilterColumns`).
39
- *
40
- * ## Why the menu is shorter than MUI's
41
- *
42
- * Firestore has equality, `in`, ranges and `array-contains`. Everything below
43
- * is built from those four:
44
- *
45
- * equals / isAnyOf equality and `in` (Firestore caps `in` at 30 values)
46
- * startsWith a range over the normalized lower-case key
47
- * endsWith a range over the key stored REVERSED — a range is
48
- * anchored at the FRONT of a value, so storing the value
49
- * backwards is the only way to ask about its end
50
- * contains `array-contains` over word-prefix tokens, so it is
51
- * WORD-level: "coffee" finds "Acme Coffee", "offee" does
52
- * not find anything
53
- * before / after ranges on a timestamp or a number
54
- * isNotEmpty `!= null`, which in Firestore also requires the field to
55
- * EXIST — which is the meaning wanted
56
- *
57
- * ⛔ `doesNotContain` is offered nowhere and cannot be. Firestore has no
58
- * negated substring match, and no denormalization fixes it: the inverse of a
59
- * token array is every string the value does not contain, which is unbounded.
60
- *
61
- * ⚠️ The two empty operators depend on how a field's WRITERS behave, which is
62
- * what `presence` declares. Firestore cannot query for absence at all:
63
- * `where(path, '==', null)` matches an explicit null and never a missing field,
64
- * while `where(path, '!=', null)` additionally requires the field to exist.
65
- * So a field that writers omit can answer `isNotEmpty` exactly and can never
66
- * answer `isEmpty` — it would say "none" to a question with real answers — and
67
- * a field that is always written answers both, but each with a foregone
68
- * result. Only a `nullable` field earns both operators for real.
69
- */
70
- /** How a field can be queried, which decides which operators it offers. */
71
- export type ListFilterKind = 'text' | 'exact' | 'boolean' | 'number' | 'date' | 'id';
72
- export interface ListFilterField {
73
- /** The grid column's `field` — what the client sends back. */
74
- column: string;
75
- kind: ListFilterKind;
76
- /** The Firestore field path the predicate targets. */
77
- path: string;
78
- /**
79
- * The normalized lower-case twin, for `equals` and `startsWith` on text.
80
- * Without one a text field offers neither: a raw value is case-sensitive, so
81
- * it would miss rather than fail.
82
- */
83
- lowerPath?: string;
84
- /** Word-prefix token array, for `contains`. */
85
- tokensPath?: string;
86
- /**
87
- * The token array holds OPAQUE IDS, matched byte for byte.
88
- *
89
- * A word token array is lower-cased by its writer, so a `contains` folds
90
- * the typed value the same way before it looks. An id array — the forms a
91
- * contact came in through, the companies they are linked to — is not: the
92
- * ids are minted with mixed case, and folding one would look up an id
93
- * nothing wrote. A field that says so is matched on the value exactly as
94
- * given, in the query and in memory alike.
95
- */
96
- verbatimTokens?: boolean;
97
- /**
98
- * What a `contains` orders by. Defaults to `lowerPath`, then `path`.
99
- *
100
- * A field whose `tokensPath` IS its own value — a real tag array, matched
101
- * whole rather than by word prefix — has no lower-case twin to sort by, and
102
- * defaulting to `path` would order by the array itself. Naming a scalar here
103
- * keeps the composite index that combination needs an ordinary one.
104
- */
105
- containsOrderBy?: string;
106
- /**
107
- * Which way a `contains` orders by `containsOrderBy`. Ascending unless the
108
- * field says otherwise; a list sorted newest-first orders its `contains`
109
- * the same way, so the composite it needs is the one it already declares
110
- * (AGL-3321).
111
- */
112
- containsOrderDirection?: 'asc' | 'desc';
113
- /** The reversed key, for `endsWith`. */
114
- reversedPath?: string;
115
- /**
116
- * How the field's writers treat "no value", which is the only thing that
117
- * decides whether the empty operators mean anything:
118
- *
119
- * `sparse` (default) writers OMIT the field. `isNotEmpty` is exact;
120
- * `isEmpty` is unanswerable and so is not offered.
121
- * `nullable` writers store `null`. Both operators are exact and both can
122
- * return rows — the only case where offering them is useful.
123
- * `always` the field is never absent and never null. Both operators have
124
- * a foregone answer (all rows, then none), so neither is
125
- * offered: a control whose result is known is a control that
126
- * wastes the reader's time.
127
- */
128
- presence?: 'sparse' | 'nullable' | 'always';
129
- /**
130
- * The operators this field offers, replacing the derived list.
131
- *
132
- * Not every list is a Firestore query. The staff account list is Firebase
133
- * Auth, which cannot filter at ALL — so a filter there is answered by
134
- * scanning the pools and matching in memory, and plain JavaScript can do
135
- * things no index can: a mid-string `contains`, and `doesNotContain`. A
136
- * list whose substrate is more capable says so here rather than being held
137
- * to Firestore's limits by a shared default.
138
- *
139
- * Pair with {@link matchListFilter}, which is the matcher those lists use.
140
- */
141
- operators?: readonly string[];
142
- /**
143
- * Where to read this field from a ROW, when the list matches in memory.
144
- * Defaults to `path`. Only needed when the row's shape differs from the
145
- * document's — an Auth record's `staff` lives under `customClaims`.
146
- */
147
- rowPath?: string;
148
- /**
149
- * The field lives where no query can reach it, so the list matches it
150
- * over the rows it has loaded and the translators refuse it.
151
- *
152
- * A contact's owner, stage and custom values sit on the viewing group's
153
- * FACET — a map keyed per group — and a `where` on a per-group path would
154
- * need an index per group. Declaring the field at all is what puts it in
155
- * the menu and in a saved view; saying it is window-only is what keeps a
156
- * clause on it from being sent to a query that would answer with a
157
- * permission error or nothing. The list says which clauses narrowed only
158
- * the window — see `listFilterPlan`.
159
- */
160
- windowOnly?: boolean;
161
- /**
162
- * A `date` field stored as epoch MILLISECONDS (`…AtMs`) rather than as a
163
- * Timestamp. The query twins compare numbers for it, and the matcher reads
164
- * both already (AGL-3321).
165
- */
166
- storedAs?: 'timestamp' | 'millis';
167
- /**
168
- * The stored value is a PRESENCE MAP — `{ form: true, order: true }` —
169
- * and the filter asks about its keys.
170
- *
171
- * A contact's `sources` is such a map: one key per door the person came
172
- * in through, never an array. Matched as though it were the array of its
173
- * truthy keys, so `equals form` and `isAnyOf form,order` read the way a
174
- * reader expects; a query can serve `equals` as `sources.form == true`.
175
- */
176
- keysOf?: boolean;
177
- }
178
- /** The operators a field offers, derived from the paths it actually has. */
179
- export declare function listFilterOperators(field: ListFilterField): string[];
180
- /** What a page sends the route: one field, one operator, one value. */
181
- export interface ListFilterRequest {
182
- field: string;
183
- op: string;
184
- value: string;
185
- }
186
- /**
187
- * The one filter item a server-paged list can act on, read off a grid model.
188
- *
189
- * ONE, because Firestore composes predicates only through indexes nobody built
190
- * for every pair of columns; the first usable item wins and the rest are the
191
- * reader's to remove. Every list read this off the model itself, and each did
192
- * it slightly differently — the common miss being `isEmpty`, which sets no
193
- * value at all and so was skipped by any check that required one.
194
- */
195
- export declare function gridFilterRequest(model: {
196
- items?: Array<{
197
- field?: unknown;
198
- operator?: unknown;
199
- value?: unknown;
200
- }>;
201
- }): ListFilterRequest | null;
202
- /**
203
- * EVERY usable filter item on a grid model, in the order the panel holds
204
- * them — for a list that keeps more than one clause (AGL-2617).
205
- *
206
- * The singular reader above picks the first because a query serves one;
207
- * a saved view carries a list of clauses and a list that applies it puts
208
- * the first servable one on the query and matches the rest over the rows
209
- * it loaded, which `listFilterPlan` in the instance library decides. The
210
- * two readers share one notion of "usable", so a clause the singular one
211
- * would take is a clause this one keeps.
212
- */
213
- export declare function gridFilterRequests(model: {
214
- items?: Array<{
215
- field?: unknown;
216
- operator?: unknown;
217
- value?: unknown;
218
- }>;
219
- }): ListFilterRequest[];
220
- export { listFilterDay, listFilterOperatorLabel } from './list-filter-sentence';
221
- /**
222
- * Match ONE row against a filter, for a list that cannot push it to a query.
223
- *
224
- * The staff account list is the reason this exists: it is Firebase Auth, whose
225
- * `listUsers` takes a page size and a cursor and nothing else — no predicate,
226
- * no ordering, no search. A filter there is answered by walking the pools and
227
- * matching here, which is why that list can offer a mid-string `contains` and
228
- * a `doesNotContain` that a Firestore-backed list cannot.
229
- *
230
- * The declaration is still the same `ListFilterField`, so a list swapping
231
- * substrates changes where the matching happens and not what it offers.
232
- */
233
- export declare function matchListFilter(row: unknown, fields: readonly ListFilterField[], input: ListFilterRequest | null): boolean;
234
- /** The visibility model that hides what {@link hiddenFilterColumns} added. */
235
- export declare function hiddenFilterVisibility(fields: readonly ListFilterField[], visible: readonly string[]): Record<string, boolean>;
17
+ export * from '@aglyn/shared-util-tools/list-query/list-filter';
@@ -13,287 +13,9 @@
13
13
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
- */ import { listFilterDay } from "./list-filter-sentence.js";
17
- /** The empty-operators a field can honestly offer. See `presence`. */ const emptyOperators = (field)=>{
18
- var _field_presence;
19
- switch((_field_presence = field.presence) != null ? _field_presence : 'sparse'){
20
- case 'always':
21
- return [];
22
- case 'nullable':
23
- return [
24
- 'isEmpty',
25
- 'isNotEmpty'
26
- ];
27
- default:
28
- return [
29
- 'isNotEmpty'
30
- ];
31
- }
32
- };
33
- /** The operators a field offers, derived from the paths it actually has. */ export function listFilterOperators(field) {
34
- if (field.operators) return [
35
- ...field.operators
36
- ];
37
- const empties = emptyOperators(field);
38
- switch(field.kind){
39
- case 'text':
40
- return [
41
- ...field.tokensPath ? [
42
- 'contains'
43
- ] : [],
44
- ...field.lowerPath ? [
45
- 'equals',
46
- 'startsWith'
47
- ] : [],
48
- ...field.reversedPath ? [
49
- 'endsWith'
50
- ] : [],
51
- ...empties
52
- ];
53
- case 'exact':
54
- return [
55
- 'equals',
56
- 'isAnyOf',
57
- ...empties
58
- ];
59
- case 'id':
60
- // An id is never absent, so the empty operators would answer nothing.
61
- return [
62
- 'equals',
63
- 'startsWith',
64
- 'isAnyOf'
65
- ];
66
- case 'boolean':
67
- return [
68
- 'is'
69
- ];
70
- case 'number':
71
- return [
72
- '=',
73
- '!=',
74
- '>',
75
- '>=',
76
- '<',
77
- '<=',
78
- ...empties
79
- ];
80
- case 'date':
81
- return [
82
- 'is',
83
- 'after',
84
- 'onOrAfter',
85
- 'before',
86
- 'onOrBefore',
87
- ...empties
88
- ];
89
- default:
90
- return [];
91
- }
92
- }
93
- /** Operators that carry no value — the grid leaves `item.value` undefined. */ const VALUELESS = [
94
- 'isEmpty',
95
- 'isNotEmpty'
96
- ];
97
- /**
98
- * The one filter item a server-paged list can act on, read off a grid model.
99
- *
100
- * ONE, because Firestore composes predicates only through indexes nobody built
101
- * for every pair of columns; the first usable item wins and the rest are the
102
- * reader's to remove. Every list read this off the model itself, and each did
103
- * it slightly differently — the common miss being `isEmpty`, which sets no
104
- * value at all and so was skipped by any check that required one.
105
- */ export function gridFilterRequest(model) {
106
- var _model_items;
107
- const usable = ((_model_items = model.items) != null ? _model_items : []).find((item)=>{
108
- if (!item.field || !item.operator) return false;
109
- if (VALUELESS.includes(String(item.operator))) return true;
110
- if (item.value === undefined || item.value === null) return false;
111
- return Array.isArray(item.value) ? item.value.length > 0 : String(item.value).trim() !== '';
112
- });
113
- if (!usable) return null;
114
- return gridItemRequest(usable);
115
- }
116
- /** Whether a grid filter item asks anything at all — see `gridFilterRequest`. */ const gridItemUsable = (item)=>{
117
- if (!item.field || !item.operator) return false;
118
- if (VALUELESS.includes(String(item.operator))) return true;
119
- if (item.value === undefined || item.value === null) return false;
120
- return Array.isArray(item.value) ? item.value.length > 0 : String(item.value).trim() !== '';
121
- };
122
- const gridItemRequest = (item)=>{
123
- const raw = item.value;
124
- const value = Array.isArray(raw) ? raw.join(',') : // locale rendering the server would have to guess at. ISO travels
125
- // unambiguously and `new Date` reads it back exactly.
126
- raw instanceof Date ? raw.toISOString() : raw === undefined || raw === null ? '' : String(raw);
127
- return {
128
- field: String(item.field),
129
- op: String(item.operator),
130
- value
131
- };
132
- };
133
- /**
134
- * EVERY usable filter item on a grid model, in the order the panel holds
135
- * them — for a list that keeps more than one clause (AGL-2617).
136
- *
137
- * The singular reader above picks the first because a query serves one;
138
- * a saved view carries a list of clauses and a list that applies it puts
139
- * the first servable one on the query and matches the rest over the rows
140
- * it loaded, which `listFilterPlan` in the instance library decides. The
141
- * two readers share one notion of "usable", so a clause the singular one
142
- * would take is a clause this one keeps.
143
- */ export function gridFilterRequests(model) {
144
- var _model_items;
145
- return ((_model_items = model.items) != null ? _model_items : []).filter(gridItemUsable).map(gridItemRequest);
146
- }
147
- export { listFilterDay, listFilterOperatorLabel } from "./list-filter-sentence.js";
148
- const readPath = (row, path)=>path.split('.').reduce((value, key)=>value == null ? undefined : value[key], row);
149
- const asTime = (value)=>{
150
- if (value == null) return null;
151
- if (value instanceof Date) return value.getTime();
152
- if (typeof value === 'number') return value;
153
- // A Firestore timestamp, in either the admin shape or the serialized one.
154
- const seconds = value.seconds;
155
- if (typeof seconds === 'number') return seconds * 1000;
156
- const parsed = new Date(String(value)).getTime();
157
- return Number.isNaN(parsed) ? null : parsed;
158
- };
159
- /**
160
- * Match ONE row against a filter, for a list that cannot push it to a query.
161
- *
162
- * The staff account list is the reason this exists: it is Firebase Auth, whose
163
- * `listUsers` takes a page size and a cursor and nothing else — no predicate,
164
- * no ordering, no search. A filter there is answered by walking the pools and
165
- * matching here, which is why that list can offer a mid-string `contains` and
166
- * a `doesNotContain` that a Firestore-backed list cannot.
167
- *
168
- * The declaration is still the same `ListFilterField`, so a list swapping
169
- * substrates changes where the matching happens and not what it offers.
170
- */ export function matchListFilter(row, fields, input) {
171
- var _field_rowPath, _input_value;
172
- if (!input) return true;
173
- const field = fields.find((entry)=>entry.column === input.field);
174
- if (!field) return true;
175
- const stored = readPath(row, (_field_rowPath = field.rowPath) != null ? _field_rowPath : field.path);
176
- // A presence map is matched as the array of its truthy keys — see `keysOf`.
177
- const value = field.keysOf && stored && typeof stored === 'object' && !Array.isArray(stored) ? Object.keys(stored).filter((key)=>Boolean(stored[key])) : stored;
178
- const op = input.op;
179
- const blank = value == null || value === '' || Array.isArray(value) && value.length === 0;
180
- if (op === 'isEmpty') return blank;
181
- if (op === 'isNotEmpty') return !blank;
182
- const raw = ((_input_value = input.value) != null ? _input_value : '').trim();
183
- if (field.kind === 'boolean') {
184
- if (raw !== 'true' && raw !== 'false') return true;
185
- return Boolean(value) === (raw === 'true');
186
- }
187
- if (!raw) return true;
188
- /*
189
- * An array the QUERY serves whole is matched by whole member, never by
190
- * substring of its joined form. A tag array asked `contains vip` holds the
191
- * tag `vip` or it does not — `vip-gold` is a different tag, and the
192
- * query's `array-contains` says so too, so the in-memory answer and the
193
- * served answer agree. An id array keeps its case (`verbatimTokens`); a
194
- * word array is folded the way its writer folded it. `isAnyOf` is any
195
- * member in the asked list, which is what a saved segment's "any of these
196
- * tags" becomes as a view clause. A presence map's keys are members too.
197
- *
198
- * A plain array with no token path is served by nothing, so there is no
199
- * query to agree with, and it falls through to the scalar match over its
200
- * joined text — the staff account list's providers, where `contains
201
- * google` finding `google.com` is the mid-string answer that list
202
- * documents (`operators`).
203
- */ if (Array.isArray(value) && (field.tokensPath || field.keysOf)) {
204
- const fold = (entry)=>field.verbatimTokens ? String(entry) : String(entry).toLowerCase();
205
- const members = value.map(fold);
206
- const asked = fold(raw);
207
- switch(op){
208
- case 'contains':
209
- case 'equals':
210
- case 'is':
211
- return members.includes(asked);
212
- case 'doesNotContain':
213
- case 'doesNotEqual':
214
- return !members.includes(asked);
215
- case 'isAnyOf':
216
- return asked.split(',').map((entry)=>entry.trim()).filter(Boolean).some((entry)=>members.includes(entry));
217
- default:
218
- return true;
219
- }
220
- }
221
- if (field.kind === 'date') {
222
- const at = asTime(value);
223
- const day = listFilterDay(raw);
224
- if (at === null || Number.isNaN(day.getTime())) return false;
225
- const start = new Date(day);
226
- start.setHours(0, 0, 0, 0);
227
- const end = new Date(start);
228
- end.setDate(end.getDate() + 1);
229
- switch(op){
230
- // A day, not an instant — a stored time never equals midnight.
231
- case 'is':
232
- return at >= start.getTime() && at < end.getTime();
233
- case 'after':
234
- return at >= end.getTime();
235
- case 'onOrAfter':
236
- return at >= start.getTime();
237
- case 'before':
238
- return at < start.getTime();
239
- case 'onOrBefore':
240
- return at < end.getTime();
241
- default:
242
- return true;
243
- }
244
- }
245
- if (field.kind === 'number') {
246
- const asked = Number(raw);
247
- const held = Number(value);
248
- if (!Number.isFinite(asked) || !Number.isFinite(held)) return false;
249
- switch(op){
250
- case '=':
251
- return held === asked;
252
- case '!=':
253
- return held !== asked;
254
- case '>':
255
- return held > asked;
256
- case '>=':
257
- return held >= asked;
258
- case '<':
259
- return held < asked;
260
- case '<=':
261
- return held <= asked;
262
- default:
263
- return true;
264
- }
265
- }
266
- // A scalar id is matched as typed — see `verbatimTokens`; the array case
267
- // was answered above.
268
- if (field.verbatimTokens && op === 'contains') return value === raw;
269
- const held = value == null ? '' : String(value).toLowerCase();
270
- const asked = raw.toLowerCase();
271
- switch(op){
272
- case 'contains':
273
- return held.includes(asked);
274
- case 'doesNotContain':
275
- return !held.includes(asked);
276
- case 'equals':
277
- return held === asked;
278
- case 'doesNotEqual':
279
- return held !== asked;
280
- case 'startsWith':
281
- return held.startsWith(asked);
282
- case 'endsWith':
283
- return held.endsWith(asked);
284
- case 'isAnyOf':
285
- return asked.split(',').map((entry)=>entry.trim()).filter(Boolean).includes(held);
286
- default:
287
- // An operator nothing here answers must not narrow the list to
288
- // nothing — that reads as "no such record" rather than "not supported".
289
- return true;
290
- }
291
- }
292
- /** The visibility model that hides what {@link hiddenFilterColumns} added. */ export function hiddenFilterVisibility(fields, visible) {
293
- return Object.fromEntries(fields.filter((field)=>!visible.includes(field.column)).map((field)=>[
294
- field.column,
295
- false
296
- ]));
297
- }
16
+ */ /*
17
+ * The list filter grammar lives in a pure module, so a surface with no web UI
18
+ * (the native app) plans the same queries; this path stays its web entry.
19
+ */ export * from "@aglyn/shared-util-tools/list-query/list-filter";
298
20
 
299
21
  //# sourceMappingURL=list-filter.js.map