@j2-health/design-system 0.2.1 → 0.3.0

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 (31) hide show
  1. package/README.md +38 -9
  2. package/dist/index.cjs +6 -0
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.js +6 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/src/components/barchart/BarChart.cjs +8 -1
  7. package/dist/src/components/barchart/BarChart.cjs.map +1 -1
  8. package/dist/src/components/barchart/BarChart.js +8 -1
  9. package/dist/src/components/barchart/BarChart.js.map +1 -1
  10. package/dist/src/components/filterform/FilterInput.cjs +66 -3
  11. package/dist/src/components/filterform/FilterInput.cjs.map +1 -1
  12. package/dist/src/components/filterform/FilterInput.d.ts +1 -0
  13. package/dist/src/components/filterform/FilterInput.js +68 -5
  14. package/dist/src/components/filterform/FilterInput.js.map +1 -1
  15. package/dist/src/components/filterform/filterHelpers.cjs +24 -1
  16. package/dist/src/components/filterform/filterHelpers.cjs.map +1 -1
  17. package/dist/src/components/filterform/filterHelpers.d.ts +18 -4
  18. package/dist/src/components/filterform/filterHelpers.js +24 -1
  19. package/dist/src/components/filterform/filterHelpers.js.map +1 -1
  20. package/dist/src/components/filterform/index.d.ts +3 -1
  21. package/dist/src/components/filterform/types.d.ts +4 -2
  22. package/dist/src/components/filterform/useFilterField.cjs +54 -19
  23. package/dist/src/components/filterform/useFilterField.cjs.map +1 -1
  24. package/dist/src/components/filterform/useFilterField.d.ts +17 -1
  25. package/dist/src/components/filterform/useFilterField.js +55 -20
  26. package/dist/src/components/filterform/useFilterField.js.map +1 -1
  27. package/dist/src/components/vennDiagram/VennDiagram.cjs +7 -1
  28. package/dist/src/components/vennDiagram/VennDiagram.cjs.map +1 -1
  29. package/dist/src/components/vennDiagram/VennDiagram.js +7 -1
  30. package/dist/src/components/vennDiagram/VennDiagram.js.map +1 -1
  31. package/package.json +6 -6
@@ -1,5 +1,22 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const isMultiValueTextOperator = (operator) => operator === "isAnyOf" || operator === "isNoneOf";
4
+ const normalizeMultiValues = (raw) => {
5
+ const seen = /* @__PURE__ */ new Set();
6
+ const out = [];
7
+ for (const value of raw ?? []) {
8
+ if (value === null || value === void 0) continue;
9
+ const trimmed = String(value).trim();
10
+ if (trimmed === "") continue;
11
+ const key = trimmed.toLowerCase();
12
+ if (seen.has(key)) continue;
13
+ seen.add(key);
14
+ out.push(trimmed);
15
+ }
16
+ return out;
17
+ };
18
+ const tooManyValuesMessage = (maxValues) => `Up to ${maxValues} values`;
19
+ const exceedsMaxValues = (values, limits) => (limits == null ? void 0 : limits.maxValues) !== void 0 && values !== void 0 && values.length > limits.maxValues;
3
20
  const isEmptyFilter = (filter) => {
4
21
  if (filter.operator === "blank" || filter.operator === "notBlank") {
5
22
  return false;
@@ -22,7 +39,7 @@ const isEmptyFormFilter = (filter) => {
22
39
  }
23
40
  return true;
24
41
  };
25
- const validateFormFilter = (filter) => {
42
+ const validateFormFilter = (filter, limits) => {
26
43
  const errors = [];
27
44
  if (!filter.operator) {
28
45
  errors.push("Operator is required");
@@ -31,6 +48,9 @@ const validateFormFilter = (filter) => {
31
48
  if (isEmptyFormFilter(filter)) {
32
49
  errors.push("Value is required");
33
50
  }
51
+ if (exceedsMaxValues(filter.values, limits)) {
52
+ errors.push(tooManyValuesMessage(limits.maxValues));
53
+ }
34
54
  if (filter.type === "number" && filter.operator === "inRange") {
35
55
  if (filter.values && filter.values.length !== 2) {
36
56
  errors.push("Value is required");
@@ -44,5 +64,8 @@ const isFilter = (filter) => {
44
64
  exports.isEmptyFilter = isEmptyFilter;
45
65
  exports.isEmptyFormFilter = isEmptyFormFilter;
46
66
  exports.isFilter = isFilter;
67
+ exports.isMultiValueTextOperator = isMultiValueTextOperator;
68
+ exports.normalizeMultiValues = normalizeMultiValues;
69
+ exports.tooManyValuesMessage = tooManyValuesMessage;
47
70
  exports.validateFormFilter = validateFormFilter;
48
71
  //# sourceMappingURL=filterHelpers.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"filterHelpers.cjs","sources":["../../../../src/components/filterform/filterHelpers.ts"],"sourcesContent":["import { Filter } from './types'\n\nexport const isEmptyFilter = (filter: Filter) => {\n if (filter.operator === 'blank' || filter.operator === 'notBlank') {\n return false\n }\n\n if (\n filter.values &&\n filter.values.length > 0 &&\n filter.values.every(\n (value) => value !== '' && value !== undefined && value !== null\n )\n ) {\n return false\n }\n\n return true\n}\n\nexport const isValidFilter = (filter: Filter) => {\n return validateFilterField(filter).length === 0\n}\n\nexport const validateFilterField = (\n filter: Filter\n): { field: string; message: string }[] => {\n const errors = []\n\n if (!filter.operator) {\n errors.push({ field: filter.field, message: 'Operator is required' })\n return errors\n }\n\n if (isEmptyFilter(filter)) {\n errors.push({ field: filter.field, message: 'Value is required' })\n }\n\n if (filter.type === 'number' && filter.operator === 'inRange') {\n if (filter.values && filter.values.length !== 2) {\n errors.push({ field: filter.field, message: 'Value is required' })\n }\n }\n\n return errors\n}\n\nimport { FormFilter } from './types'\n\nexport const isEmptyFormFilter = (filter: FormFilter) => {\n if (filter.operator === 'blank' || filter.operator === 'notBlank') {\n return false\n }\n\n if (\n filter.values &&\n filter.values.length > 0 &&\n filter.values.every(\n (value) => value !== '' && value !== undefined && value !== null\n )\n ) {\n return false\n }\n\n return true\n}\n\nexport const isValidFormFilter = (filter: FormFilter) => {\n return validateFormFilter(filter).length === 0\n}\n\nexport const validateFormFilter = (filter: FormFilter): string[] => {\n const errors = []\n\n if (!filter.operator) {\n errors.push('Operator is required')\n return errors\n }\n\n if (isEmptyFormFilter(filter)) {\n errors.push('Value is required')\n }\n\n if (filter.type === 'number' && filter.operator === 'inRange') {\n if (filter.values && filter.values.length !== 2) {\n errors.push('Value is required')\n }\n }\n\n return errors\n}\n\nexport const isFilter = (filter: FormFilter): filter is Filter => {\n return (\n filter.operator !== undefined &&\n filter.field !== undefined &&\n filter.type !== undefined\n )\n}\n"],"names":[],"mappings":";;AAEO,MAAM,gBAAgB,CAAC,WAAmB;AAC/C,MAAI,OAAO,aAAa,WAAW,OAAO,aAAa,YAAY;AACjE,WAAO;AAAA,EACT;AAEA,MACE,OAAO,UACP,OAAO,OAAO,SAAS,KACvB,OAAO,OAAO;AAAA,IACZ,CAAC,UAAU,UAAU,MAAM,UAAU,UAAa,UAAU;AAAA,EAAA,GAE9D;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AA+BO,MAAM,oBAAoB,CAAC,WAAuB;AACvD,MAAI,OAAO,aAAa,WAAW,OAAO,aAAa,YAAY;AACjE,WAAO;AAAA,EACT;AAEA,MACE,OAAO,UACP,OAAO,OAAO,SAAS,KACvB,OAAO,OAAO;AAAA,IACZ,CAAC,UAAU,UAAU,MAAM,UAAU,UAAa,UAAU;AAAA,EAAA,GAE9D;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAMO,MAAM,qBAAqB,CAAC,WAAiC;AAClE,QAAM,SAAS,CAAA;AAEf,MAAI,CAAC,OAAO,UAAU;AACpB,WAAO,KAAK,sBAAsB;AAClC,WAAO;AAAA,EACT;AAEA,MAAI,kBAAkB,MAAM,GAAG;AAC7B,WAAO,KAAK,mBAAmB;AAAA,EACjC;AAEA,MAAI,OAAO,SAAS,YAAY,OAAO,aAAa,WAAW;AAC7D,QAAI,OAAO,UAAU,OAAO,OAAO,WAAW,GAAG;AAC/C,aAAO,KAAK,mBAAmB;AAAA,IACjC;AAAA,EACF;AAEA,SAAO;AACT;AAEO,MAAM,WAAW,CAAC,WAAyC;AAChE,SACE,OAAO,aAAa,UACpB,OAAO,UAAU,UACjB,OAAO,SAAS;AAEpB;;;;;"}
1
+ {"version":3,"file":"filterHelpers.cjs","sources":["../../../../src/components/filterform/filterHelpers.ts"],"sourcesContent":["import { Filter } from './types'\n\n// Multi-value text operators — the value is a chip list, not a single string.\nexport const isMultiValueTextOperator = (\n operator: string | undefined\n): boolean => operator === 'isAnyOf' || operator === 'isNoneOf'\n\n/**\n * Normalize a chip list: trim, drop empties, dedupe case-insensitively (the\n * match is case-insensitive, so `ABC` and `abc` are the same rule). Keeps\n * first-seen casing and order so the chips read the way they were pasted.\n *\n * Applied when values enter reducer state (`useFilterField`), so the chips\n * on screen, the validation count and the submitted `values` are one array.\n */\nexport const normalizeMultiValues = (\n raw: (string | number | undefined | null)[] | undefined\n): string[] => {\n const seen = new Set<string>()\n const out: string[] = []\n for (const value of raw ?? []) {\n if (value === null || value === undefined) continue\n const trimmed = String(value).trim()\n if (trimmed === '') continue\n const key = trimmed.toLowerCase()\n if (seen.has(key)) continue\n seen.add(key)\n out.push(trimmed)\n }\n return out\n}\n\n// The only `FilterConfig` fields validation consults. Passed separately from\n// the filter because a submitted `Filter` doesn't carry its config.\nexport type FilterValueLimits = { maxValues?: number }\n\nexport const tooManyValuesMessage = (maxValues: number) =>\n `Up to ${maxValues} values`\n\nconst exceedsMaxValues = (\n values: unknown[] | undefined,\n limits: FilterValueLimits | undefined\n): limits is { maxValues: number } =>\n limits?.maxValues !== undefined &&\n values !== undefined &&\n values.length > limits.maxValues\n\nexport const isEmptyFilter = (filter: Filter) => {\n if (filter.operator === 'blank' || filter.operator === 'notBlank') {\n return false\n }\n\n if (\n filter.values &&\n filter.values.length > 0 &&\n filter.values.every(\n (value) => value !== '' && value !== undefined && value !== null\n )\n ) {\n return false\n }\n\n return true\n}\n\nexport const isValidFilter = (filter: Filter, limits?: FilterValueLimits) => {\n return validateFilterField(filter, limits).length === 0\n}\n\nexport const validateFilterField = (\n filter: Filter,\n limits?: FilterValueLimits\n): { field: string; message: string }[] => {\n const errors = []\n\n if (!filter.operator) {\n errors.push({ field: filter.field, message: 'Operator is required' })\n return errors\n }\n\n if (isEmptyFilter(filter)) {\n errors.push({ field: filter.field, message: 'Value is required' })\n }\n\n if (exceedsMaxValues(filter.values, limits)) {\n errors.push({\n field: filter.field,\n message: tooManyValuesMessage(limits.maxValues),\n })\n }\n\n if (filter.type === 'number' && filter.operator === 'inRange') {\n if (filter.values && filter.values.length !== 2) {\n errors.push({ field: filter.field, message: 'Value is required' })\n }\n }\n\n return errors\n}\n\nimport { FormFilter } from './types'\n\nexport const isEmptyFormFilter = (filter: FormFilter) => {\n if (filter.operator === 'blank' || filter.operator === 'notBlank') {\n return false\n }\n\n if (\n filter.values &&\n filter.values.length > 0 &&\n filter.values.every(\n (value) => value !== '' && value !== undefined && value !== null\n )\n ) {\n return false\n }\n\n return true\n}\n\nexport const isValidFormFilter = (\n filter: FormFilter,\n limits?: FilterValueLimits\n) => {\n return validateFormFilter(filter, limits).length === 0\n}\n\nexport const validateFormFilter = (\n filter: FormFilter,\n limits?: FilterValueLimits\n): string[] => {\n const errors = []\n\n if (!filter.operator) {\n errors.push('Operator is required')\n return errors\n }\n\n if (isEmptyFormFilter(filter)) {\n errors.push('Value is required')\n }\n\n if (exceedsMaxValues(filter.values, limits)) {\n errors.push(tooManyValuesMessage(limits.maxValues))\n }\n\n if (filter.type === 'number' && filter.operator === 'inRange') {\n if (filter.values && filter.values.length !== 2) {\n errors.push('Value is required')\n }\n }\n\n return errors\n}\n\nexport const isFilter = (filter: FormFilter): filter is Filter => {\n return (\n filter.operator !== undefined &&\n filter.field !== undefined &&\n filter.type !== undefined\n )\n}\n"],"names":[],"mappings":";;AAGO,MAAM,2BAA2B,CACtC,aACY,aAAa,aAAa,aAAa;AAU9C,MAAM,uBAAuB,CAClC,QACa;AACb,QAAM,2BAAW,IAAA;AACjB,QAAM,MAAgB,CAAA;AACtB,aAAW,SAAS,OAAO,IAAI;AAC7B,QAAI,UAAU,QAAQ,UAAU,OAAW;AAC3C,UAAM,UAAU,OAAO,KAAK,EAAE,KAAA;AAC9B,QAAI,YAAY,GAAI;AACpB,UAAM,MAAM,QAAQ,YAAA;AACpB,QAAI,KAAK,IAAI,GAAG,EAAG;AACnB,SAAK,IAAI,GAAG;AACZ,QAAI,KAAK,OAAO;AAAA,EAClB;AACA,SAAO;AACT;AAMO,MAAM,uBAAuB,CAAC,cACnC,SAAS,SAAS;AAEpB,MAAM,mBAAmB,CACvB,QACA,YAEA,iCAAQ,eAAc,UACtB,WAAW,UACX,OAAO,SAAS,OAAO;AAElB,MAAM,gBAAgB,CAAC,WAAmB;AAC/C,MAAI,OAAO,aAAa,WAAW,OAAO,aAAa,YAAY;AACjE,WAAO;AAAA,EACT;AAEA,MACE,OAAO,UACP,OAAO,OAAO,SAAS,KACvB,OAAO,OAAO;AAAA,IACZ,CAAC,UAAU,UAAU,MAAM,UAAU,UAAa,UAAU;AAAA,EAAA,GAE9D;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAuCO,MAAM,oBAAoB,CAAC,WAAuB;AACvD,MAAI,OAAO,aAAa,WAAW,OAAO,aAAa,YAAY;AACjE,WAAO;AAAA,EACT;AAEA,MACE,OAAO,UACP,OAAO,OAAO,SAAS,KACvB,OAAO,OAAO;AAAA,IACZ,CAAC,UAAU,UAAU,MAAM,UAAU,UAAa,UAAU;AAAA,EAAA,GAE9D;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AASO,MAAM,qBAAqB,CAChC,QACA,WACa;AACb,QAAM,SAAS,CAAA;AAEf,MAAI,CAAC,OAAO,UAAU;AACpB,WAAO,KAAK,sBAAsB;AAClC,WAAO;AAAA,EACT;AAEA,MAAI,kBAAkB,MAAM,GAAG;AAC7B,WAAO,KAAK,mBAAmB;AAAA,EACjC;AAEA,MAAI,iBAAiB,OAAO,QAAQ,MAAM,GAAG;AAC3C,WAAO,KAAK,qBAAqB,OAAO,SAAS,CAAC;AAAA,EACpD;AAEA,MAAI,OAAO,SAAS,YAAY,OAAO,aAAa,WAAW;AAC7D,QAAI,OAAO,UAAU,OAAO,OAAO,WAAW,GAAG;AAC/C,aAAO,KAAK,mBAAmB;AAAA,IACjC;AAAA,EACF;AAEA,SAAO;AACT;AAEO,MAAM,WAAW,CAAC,WAAyC;AAChE,SACE,OAAO,aAAa,UACpB,OAAO,UAAU,UACjB,OAAO,SAAS;AAEpB;;;;;;;;"}
@@ -1,11 +1,25 @@
1
1
  import { Filter, FormFilter } from './types.js';
2
+ export declare const isMultiValueTextOperator: (operator: string | undefined) => boolean;
3
+ /**
4
+ * Normalize a chip list: trim, drop empties, dedupe case-insensitively (the
5
+ * match is case-insensitive, so `ABC` and `abc` are the same rule). Keeps
6
+ * first-seen casing and order so the chips read the way they were pasted.
7
+ *
8
+ * Applied when values enter reducer state (`useFilterField`), so the chips
9
+ * on screen, the validation count and the submitted `values` are one array.
10
+ */
11
+ export declare const normalizeMultiValues: (raw: (string | number | undefined | null)[] | undefined) => string[];
12
+ export type FilterValueLimits = {
13
+ maxValues?: number;
14
+ };
15
+ export declare const tooManyValuesMessage: (maxValues: number) => string;
2
16
  export declare const isEmptyFilter: (filter: Filter) => boolean;
3
- export declare const isValidFilter: (filter: Filter) => boolean;
4
- export declare const validateFilterField: (filter: Filter) => {
17
+ export declare const isValidFilter: (filter: Filter, limits?: FilterValueLimits) => boolean;
18
+ export declare const validateFilterField: (filter: Filter, limits?: FilterValueLimits) => {
5
19
  field: string;
6
20
  message: string;
7
21
  }[];
8
22
  export declare const isEmptyFormFilter: (filter: FormFilter) => boolean;
9
- export declare const isValidFormFilter: (filter: FormFilter) => boolean;
10
- export declare const validateFormFilter: (filter: FormFilter) => string[];
23
+ export declare const isValidFormFilter: (filter: FormFilter, limits?: FilterValueLimits) => boolean;
24
+ export declare const validateFormFilter: (filter: FormFilter, limits?: FilterValueLimits) => string[];
11
25
  export declare const isFilter: (filter: FormFilter) => filter is Filter;
@@ -1,3 +1,20 @@
1
+ const isMultiValueTextOperator = (operator) => operator === "isAnyOf" || operator === "isNoneOf";
2
+ const normalizeMultiValues = (raw) => {
3
+ const seen = /* @__PURE__ */ new Set();
4
+ const out = [];
5
+ for (const value of raw ?? []) {
6
+ if (value === null || value === void 0) continue;
7
+ const trimmed = String(value).trim();
8
+ if (trimmed === "") continue;
9
+ const key = trimmed.toLowerCase();
10
+ if (seen.has(key)) continue;
11
+ seen.add(key);
12
+ out.push(trimmed);
13
+ }
14
+ return out;
15
+ };
16
+ const tooManyValuesMessage = (maxValues) => `Up to ${maxValues} values`;
17
+ const exceedsMaxValues = (values, limits) => (limits == null ? void 0 : limits.maxValues) !== void 0 && values !== void 0 && values.length > limits.maxValues;
1
18
  const isEmptyFilter = (filter) => {
2
19
  if (filter.operator === "blank" || filter.operator === "notBlank") {
3
20
  return false;
@@ -20,7 +37,7 @@ const isEmptyFormFilter = (filter) => {
20
37
  }
21
38
  return true;
22
39
  };
23
- const validateFormFilter = (filter) => {
40
+ const validateFormFilter = (filter, limits) => {
24
41
  const errors = [];
25
42
  if (!filter.operator) {
26
43
  errors.push("Operator is required");
@@ -29,6 +46,9 @@ const validateFormFilter = (filter) => {
29
46
  if (isEmptyFormFilter(filter)) {
30
47
  errors.push("Value is required");
31
48
  }
49
+ if (exceedsMaxValues(filter.values, limits)) {
50
+ errors.push(tooManyValuesMessage(limits.maxValues));
51
+ }
32
52
  if (filter.type === "number" && filter.operator === "inRange") {
33
53
  if (filter.values && filter.values.length !== 2) {
34
54
  errors.push("Value is required");
@@ -43,6 +63,9 @@ export {
43
63
  isEmptyFilter,
44
64
  isEmptyFormFilter,
45
65
  isFilter,
66
+ isMultiValueTextOperator,
67
+ normalizeMultiValues,
68
+ tooManyValuesMessage,
46
69
  validateFormFilter
47
70
  };
48
71
  //# sourceMappingURL=filterHelpers.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"filterHelpers.js","sources":["../../../../src/components/filterform/filterHelpers.ts"],"sourcesContent":["import { Filter } from './types'\n\nexport const isEmptyFilter = (filter: Filter) => {\n if (filter.operator === 'blank' || filter.operator === 'notBlank') {\n return false\n }\n\n if (\n filter.values &&\n filter.values.length > 0 &&\n filter.values.every(\n (value) => value !== '' && value !== undefined && value !== null\n )\n ) {\n return false\n }\n\n return true\n}\n\nexport const isValidFilter = (filter: Filter) => {\n return validateFilterField(filter).length === 0\n}\n\nexport const validateFilterField = (\n filter: Filter\n): { field: string; message: string }[] => {\n const errors = []\n\n if (!filter.operator) {\n errors.push({ field: filter.field, message: 'Operator is required' })\n return errors\n }\n\n if (isEmptyFilter(filter)) {\n errors.push({ field: filter.field, message: 'Value is required' })\n }\n\n if (filter.type === 'number' && filter.operator === 'inRange') {\n if (filter.values && filter.values.length !== 2) {\n errors.push({ field: filter.field, message: 'Value is required' })\n }\n }\n\n return errors\n}\n\nimport { FormFilter } from './types'\n\nexport const isEmptyFormFilter = (filter: FormFilter) => {\n if (filter.operator === 'blank' || filter.operator === 'notBlank') {\n return false\n }\n\n if (\n filter.values &&\n filter.values.length > 0 &&\n filter.values.every(\n (value) => value !== '' && value !== undefined && value !== null\n )\n ) {\n return false\n }\n\n return true\n}\n\nexport const isValidFormFilter = (filter: FormFilter) => {\n return validateFormFilter(filter).length === 0\n}\n\nexport const validateFormFilter = (filter: FormFilter): string[] => {\n const errors = []\n\n if (!filter.operator) {\n errors.push('Operator is required')\n return errors\n }\n\n if (isEmptyFormFilter(filter)) {\n errors.push('Value is required')\n }\n\n if (filter.type === 'number' && filter.operator === 'inRange') {\n if (filter.values && filter.values.length !== 2) {\n errors.push('Value is required')\n }\n }\n\n return errors\n}\n\nexport const isFilter = (filter: FormFilter): filter is Filter => {\n return (\n filter.operator !== undefined &&\n filter.field !== undefined &&\n filter.type !== undefined\n )\n}\n"],"names":[],"mappings":"AAEO,MAAM,gBAAgB,CAAC,WAAmB;AAC/C,MAAI,OAAO,aAAa,WAAW,OAAO,aAAa,YAAY;AACjE,WAAO;AAAA,EACT;AAEA,MACE,OAAO,UACP,OAAO,OAAO,SAAS,KACvB,OAAO,OAAO;AAAA,IACZ,CAAC,UAAU,UAAU,MAAM,UAAU,UAAa,UAAU;AAAA,EAAA,GAE9D;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AA+BO,MAAM,oBAAoB,CAAC,WAAuB;AACvD,MAAI,OAAO,aAAa,WAAW,OAAO,aAAa,YAAY;AACjE,WAAO;AAAA,EACT;AAEA,MACE,OAAO,UACP,OAAO,OAAO,SAAS,KACvB,OAAO,OAAO;AAAA,IACZ,CAAC,UAAU,UAAU,MAAM,UAAU,UAAa,UAAU;AAAA,EAAA,GAE9D;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAMO,MAAM,qBAAqB,CAAC,WAAiC;AAClE,QAAM,SAAS,CAAA;AAEf,MAAI,CAAC,OAAO,UAAU;AACpB,WAAO,KAAK,sBAAsB;AAClC,WAAO;AAAA,EACT;AAEA,MAAI,kBAAkB,MAAM,GAAG;AAC7B,WAAO,KAAK,mBAAmB;AAAA,EACjC;AAEA,MAAI,OAAO,SAAS,YAAY,OAAO,aAAa,WAAW;AAC7D,QAAI,OAAO,UAAU,OAAO,OAAO,WAAW,GAAG;AAC/C,aAAO,KAAK,mBAAmB;AAAA,IACjC;AAAA,EACF;AAEA,SAAO;AACT;AAEO,MAAM,WAAW,CAAC,WAAyC;AAChE,SACE,OAAO,aAAa,UACpB,OAAO,UAAU,UACjB,OAAO,SAAS;AAEpB;"}
1
+ {"version":3,"file":"filterHelpers.js","sources":["../../../../src/components/filterform/filterHelpers.ts"],"sourcesContent":["import { Filter } from './types'\n\n// Multi-value text operators — the value is a chip list, not a single string.\nexport const isMultiValueTextOperator = (\n operator: string | undefined\n): boolean => operator === 'isAnyOf' || operator === 'isNoneOf'\n\n/**\n * Normalize a chip list: trim, drop empties, dedupe case-insensitively (the\n * match is case-insensitive, so `ABC` and `abc` are the same rule). Keeps\n * first-seen casing and order so the chips read the way they were pasted.\n *\n * Applied when values enter reducer state (`useFilterField`), so the chips\n * on screen, the validation count and the submitted `values` are one array.\n */\nexport const normalizeMultiValues = (\n raw: (string | number | undefined | null)[] | undefined\n): string[] => {\n const seen = new Set<string>()\n const out: string[] = []\n for (const value of raw ?? []) {\n if (value === null || value === undefined) continue\n const trimmed = String(value).trim()\n if (trimmed === '') continue\n const key = trimmed.toLowerCase()\n if (seen.has(key)) continue\n seen.add(key)\n out.push(trimmed)\n }\n return out\n}\n\n// The only `FilterConfig` fields validation consults. Passed separately from\n// the filter because a submitted `Filter` doesn't carry its config.\nexport type FilterValueLimits = { maxValues?: number }\n\nexport const tooManyValuesMessage = (maxValues: number) =>\n `Up to ${maxValues} values`\n\nconst exceedsMaxValues = (\n values: unknown[] | undefined,\n limits: FilterValueLimits | undefined\n): limits is { maxValues: number } =>\n limits?.maxValues !== undefined &&\n values !== undefined &&\n values.length > limits.maxValues\n\nexport const isEmptyFilter = (filter: Filter) => {\n if (filter.operator === 'blank' || filter.operator === 'notBlank') {\n return false\n }\n\n if (\n filter.values &&\n filter.values.length > 0 &&\n filter.values.every(\n (value) => value !== '' && value !== undefined && value !== null\n )\n ) {\n return false\n }\n\n return true\n}\n\nexport const isValidFilter = (filter: Filter, limits?: FilterValueLimits) => {\n return validateFilterField(filter, limits).length === 0\n}\n\nexport const validateFilterField = (\n filter: Filter,\n limits?: FilterValueLimits\n): { field: string; message: string }[] => {\n const errors = []\n\n if (!filter.operator) {\n errors.push({ field: filter.field, message: 'Operator is required' })\n return errors\n }\n\n if (isEmptyFilter(filter)) {\n errors.push({ field: filter.field, message: 'Value is required' })\n }\n\n if (exceedsMaxValues(filter.values, limits)) {\n errors.push({\n field: filter.field,\n message: tooManyValuesMessage(limits.maxValues),\n })\n }\n\n if (filter.type === 'number' && filter.operator === 'inRange') {\n if (filter.values && filter.values.length !== 2) {\n errors.push({ field: filter.field, message: 'Value is required' })\n }\n }\n\n return errors\n}\n\nimport { FormFilter } from './types'\n\nexport const isEmptyFormFilter = (filter: FormFilter) => {\n if (filter.operator === 'blank' || filter.operator === 'notBlank') {\n return false\n }\n\n if (\n filter.values &&\n filter.values.length > 0 &&\n filter.values.every(\n (value) => value !== '' && value !== undefined && value !== null\n )\n ) {\n return false\n }\n\n return true\n}\n\nexport const isValidFormFilter = (\n filter: FormFilter,\n limits?: FilterValueLimits\n) => {\n return validateFormFilter(filter, limits).length === 0\n}\n\nexport const validateFormFilter = (\n filter: FormFilter,\n limits?: FilterValueLimits\n): string[] => {\n const errors = []\n\n if (!filter.operator) {\n errors.push('Operator is required')\n return errors\n }\n\n if (isEmptyFormFilter(filter)) {\n errors.push('Value is required')\n }\n\n if (exceedsMaxValues(filter.values, limits)) {\n errors.push(tooManyValuesMessage(limits.maxValues))\n }\n\n if (filter.type === 'number' && filter.operator === 'inRange') {\n if (filter.values && filter.values.length !== 2) {\n errors.push('Value is required')\n }\n }\n\n return errors\n}\n\nexport const isFilter = (filter: FormFilter): filter is Filter => {\n return (\n filter.operator !== undefined &&\n filter.field !== undefined &&\n filter.type !== undefined\n )\n}\n"],"names":[],"mappings":"AAGO,MAAM,2BAA2B,CACtC,aACY,aAAa,aAAa,aAAa;AAU9C,MAAM,uBAAuB,CAClC,QACa;AACb,QAAM,2BAAW,IAAA;AACjB,QAAM,MAAgB,CAAA;AACtB,aAAW,SAAS,OAAO,IAAI;AAC7B,QAAI,UAAU,QAAQ,UAAU,OAAW;AAC3C,UAAM,UAAU,OAAO,KAAK,EAAE,KAAA;AAC9B,QAAI,YAAY,GAAI;AACpB,UAAM,MAAM,QAAQ,YAAA;AACpB,QAAI,KAAK,IAAI,GAAG,EAAG;AACnB,SAAK,IAAI,GAAG;AACZ,QAAI,KAAK,OAAO;AAAA,EAClB;AACA,SAAO;AACT;AAMO,MAAM,uBAAuB,CAAC,cACnC,SAAS,SAAS;AAEpB,MAAM,mBAAmB,CACvB,QACA,YAEA,iCAAQ,eAAc,UACtB,WAAW,UACX,OAAO,SAAS,OAAO;AAElB,MAAM,gBAAgB,CAAC,WAAmB;AAC/C,MAAI,OAAO,aAAa,WAAW,OAAO,aAAa,YAAY;AACjE,WAAO;AAAA,EACT;AAEA,MACE,OAAO,UACP,OAAO,OAAO,SAAS,KACvB,OAAO,OAAO;AAAA,IACZ,CAAC,UAAU,UAAU,MAAM,UAAU,UAAa,UAAU;AAAA,EAAA,GAE9D;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAuCO,MAAM,oBAAoB,CAAC,WAAuB;AACvD,MAAI,OAAO,aAAa,WAAW,OAAO,aAAa,YAAY;AACjE,WAAO;AAAA,EACT;AAEA,MACE,OAAO,UACP,OAAO,OAAO,SAAS,KACvB,OAAO,OAAO;AAAA,IACZ,CAAC,UAAU,UAAU,MAAM,UAAU,UAAa,UAAU;AAAA,EAAA,GAE9D;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AASO,MAAM,qBAAqB,CAChC,QACA,WACa;AACb,QAAM,SAAS,CAAA;AAEf,MAAI,CAAC,OAAO,UAAU;AACpB,WAAO,KAAK,sBAAsB;AAClC,WAAO;AAAA,EACT;AAEA,MAAI,kBAAkB,MAAM,GAAG;AAC7B,WAAO,KAAK,mBAAmB;AAAA,EACjC;AAEA,MAAI,iBAAiB,OAAO,QAAQ,MAAM,GAAG;AAC3C,WAAO,KAAK,qBAAqB,OAAO,SAAS,CAAC;AAAA,EACpD;AAEA,MAAI,OAAO,SAAS,YAAY,OAAO,aAAa,WAAW;AAC7D,QAAI,OAAO,UAAU,OAAO,OAAO,WAAW,GAAG;AAC/C,aAAO,KAAK,mBAAmB;AAAA,IACjC;AAAA,EACF;AAEA,SAAO;AACT;AAEO,MAAM,WAAW,CAAC,WAAyC;AAChE,SACE,OAAO,aAAa,UACpB,OAAO,UAAU,UACjB,OAAO,SAAS;AAEpB;"}
@@ -1,2 +1,4 @@
1
1
  export { FiltersForm } from './FiltersForm.js';
2
- export type { FilterForm, FilterConfig, FilterType, Filter, FilterGroup, } from './types.js';
2
+ export type { FilterForm, FilterConfig, FilterType, Filter, FilterGroup, Operator, } from './types.js';
3
+ export { MultiValueTextOperatorOptions, operatorOptionsFor, } from './useFilterField.js';
4
+ export { isMultiValueTextOperator, normalizeMultiValues } from './filterHelpers.js';
@@ -28,8 +28,10 @@ export type FilterConfig = {
28
28
  type: FilterType;
29
29
  disabled?: boolean;
30
30
  options?: SelectOptionsType | NumberOptionsType;
31
+ operators?: Operator[];
32
+ maxValues?: number;
31
33
  };
32
- export type Operator = 'equals' | 'notEqual' | 'blank' | 'notBlank' | 'greaterThan' | 'greaterThanOrEqual' | 'lessThan' | 'lessThanOrEqual' | 'inRange' | 'contains' | 'notContains' | 'startsWith' | 'endsWith';
34
+ export type Operator = 'equals' | 'notEqual' | 'blank' | 'notBlank' | 'greaterThan' | 'greaterThanOrEqual' | 'lessThan' | 'lessThanOrEqual' | 'inRange' | 'contains' | 'notContains' | 'startsWith' | 'endsWith' | 'isAnyOf' | 'isNoneOf';
33
35
  type SelectFilter = {
34
36
  field: string;
35
37
  type: 'select';
@@ -45,7 +47,7 @@ type NumberFilter = {
45
47
  type TextFilter = {
46
48
  field: string;
47
49
  type: 'text';
48
- operator: Extract<Operator, 'contains' | 'notContains' | 'startsWith' | 'endsWith' | 'blank' | 'notBlank'> | undefined;
50
+ operator: Extract<Operator, 'contains' | 'notContains' | 'startsWith' | 'endsWith' | 'blank' | 'notBlank' | 'isAnyOf' | 'isNoneOf'> | undefined;
49
51
  values: string[];
50
52
  };
51
53
  export type Filter = SelectFilter | NumberFilter | TextFilter;
@@ -46,6 +46,19 @@ const TypeToOperatorOptions = {
46
46
  { label: "has any value", value: "notBlank" }
47
47
  ]
48
48
  };
49
+ const MultiValueTextOperatorOptions = [
50
+ { label: "is any of", value: "isAnyOf" },
51
+ { label: "is not any of", value: "isNoneOf" }
52
+ ];
53
+ const operatorOptionsFor = (config) => {
54
+ const defaults = TypeToOperatorOptions[config.type];
55
+ if (!config.operators) return defaults;
56
+ const known = config.type === "text" ? [...defaults, ...MultiValueTextOperatorOptions] : defaults;
57
+ return config.operators.flatMap((operator) => {
58
+ const option = known.find((candidate) => candidate.value === operator);
59
+ return option ? [option] : [];
60
+ });
61
+ };
49
62
  const buildValueInputConfig = (config, operator) => {
50
63
  if (config.type === "select") {
51
64
  if (operator === "blank" || operator === "notBlank") return;
@@ -65,29 +78,31 @@ const buildValueInputConfig = (config, operator) => {
65
78
  if (config.type === "text") {
66
79
  if (operator === "blank" || operator === "notBlank") return;
67
80
  return {
68
- type: "text"
81
+ type: "text",
82
+ multiValue: filterHelpers.isMultiValueTextOperator(operator),
83
+ maxValues: config.maxValues
69
84
  };
70
85
  }
71
86
  };
72
87
  const filterFieldReducer = (state, action) => {
73
- var _a, _b, _c;
88
+ var _a, _b, _c, _d, _e;
74
89
  switch (action.type) {
75
90
  case "setField": {
76
91
  const config = state.filterConfigs.find(
77
92
  (config2) => config2.field === action.payload
78
93
  );
79
94
  if (!config) return state;
80
- const operatorOptions = config ? TypeToOperatorOptions[config.type] : [];
95
+ const operatorOptions = operatorOptionsFor(config);
81
96
  const newFilter = {
82
97
  ...state.filter,
83
98
  type: config.type,
84
99
  field: action.payload,
85
- operator: operatorOptions[0].value,
100
+ operator: (_a = operatorOptions[0]) == null ? void 0 : _a.value,
86
101
  values: []
87
102
  };
88
103
  const pendingFilter = {
89
104
  ...newFilter,
90
- errors: filterHelpers.validateFormFilter(newFilter)
105
+ errors: filterHelpers.validateFormFilter(newFilter, config)
91
106
  };
92
107
  return {
93
108
  ...state,
@@ -109,42 +124,59 @@ const filterFieldReducer = (state, action) => {
109
124
  ...state,
110
125
  filter: {
111
126
  ...pendingFilter,
112
- errors: filterHelpers.validateFormFilter(pendingFilter)
127
+ errors: filterHelpers.validateFormFilter(pendingFilter, state.config)
113
128
  },
114
129
  valueInputConfig: buildValueInputConfig(state.config, action.payload)
115
130
  };
116
131
  }
117
132
  case "setValues": {
118
133
  if (!state.filter) return state;
119
- const values = state.filter.type === "number" ? action.payload.map((value) => Number(value)) : action.payload.filter(
134
+ const values = state.filter.type === "number" ? action.payload.map((value) => Number(value)) : filterHelpers.isMultiValueTextOperator(state.filter.operator) ? (
135
+ // Normalized on the way into state, not just for display —
136
+ // validation counts and `onSubmit` emit exactly these chips.
137
+ filterHelpers.normalizeMultiValues(action.payload)
138
+ ) : action.payload.filter(
120
139
  (value) => value !== null && value !== void 0
121
140
  );
122
141
  const pendingFilter = {
123
142
  ...state.filter,
124
143
  values
125
144
  };
126
- pendingFilter.errors = filterHelpers.validateFormFilter(pendingFilter);
145
+ pendingFilter.errors = filterHelpers.validateFormFilter(pendingFilter, state.config);
127
146
  return {
128
147
  ...state,
129
148
  filter: pendingFilter
130
149
  };
131
150
  }
132
151
  case "initialize": {
133
- const field = ((_a = state.filter) == null ? void 0 : _a.field) || ((_b = state.filterConfigs.find((config2) => !config2.disabled)) == null ? void 0 : _b.field);
152
+ const field = ((_b = state.filter) == null ? void 0 : _b.field) || ((_c = state.filterConfigs.find((config2) => !config2.disabled)) == null ? void 0 : _c.field);
134
153
  const config = state.filterConfigs.find(
135
154
  (config2) => config2.field === field
136
155
  );
137
156
  if (!config) return state;
138
- const operatorOptions = config ? TypeToOperatorOptions[config.type] : [];
139
- const operator = ((_c = state.filter) == null ? void 0 : _c.operator) || operatorOptions[0].value;
140
- const filter = state.filter ?? {
141
- field,
142
- type: config.type,
143
- operator,
144
- values: []
145
- };
146
- const errors = filterHelpers.validateFormFilter(filter);
147
- filter.errors = errors;
157
+ const operatorOptions = operatorOptionsFor(config);
158
+ const persisted = (_d = state.filter) == null ? void 0 : _d.operator;
159
+ const persistedAllowed = persisted !== void 0 && operatorOptions.some((option) => option.value === persisted);
160
+ const operator = persistedAllowed ? persisted : (_e = operatorOptions[0]) == null ? void 0 : _e.value;
161
+ let filter;
162
+ if (!state.filter) {
163
+ filter = {
164
+ field,
165
+ type: config.type,
166
+ operator,
167
+ values: []
168
+ };
169
+ } else if (!persistedAllowed) {
170
+ filter = { ...state.filter, operator, values: [] };
171
+ } else if (filterHelpers.isMultiValueTextOperator(operator)) {
172
+ filter = {
173
+ ...state.filter,
174
+ values: filterHelpers.normalizeMultiValues(state.filter.values)
175
+ };
176
+ } else {
177
+ filter = { ...state.filter };
178
+ }
179
+ filter.errors = filterHelpers.validateFormFilter(filter, config);
148
180
  return {
149
181
  ...state,
150
182
  config,
@@ -205,6 +237,9 @@ const useFilterField = ({
205
237
  handleValuesChange
206
238
  };
207
239
  };
240
+ exports.isMultiValueTextOperator = filterHelpers.isMultiValueTextOperator;
241
+ exports.MultiValueTextOperatorOptions = MultiValueTextOperatorOptions;
208
242
  exports.TypeToOperatorOptions = TypeToOperatorOptions;
243
+ exports.operatorOptionsFor = operatorOptionsFor;
209
244
  exports.useFilterField = useFilterField;
210
245
  //# sourceMappingURL=useFilterField.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"useFilterField.cjs","sources":["../../../../src/components/filterform/useFilterField.tsx"],"sourcesContent":["import * as React from 'react'\nimport {\n FilterConfig,\n FilterType,\n FormFilter,\n NumberOptionsType,\n Operator,\n SelectOptionsType,\n} from './types'\nimport { validateFormFilter } from './filterHelpers'\nexport type UseFilterFieldInputs = {\n filterConfigs: FilterConfig[]\n filter?: FormFilter\n}\n\ntype UseFilterFieldOutputs = {\n filter: FormFilter | undefined\n config: FilterConfig | undefined\n operatorOptions: { label: string; value: string }[]\n valueInputConfig: ValueInputConfig | undefined\n handleFieldChange: (value: string) => void\n handleOperatorChange: (value: string) => void\n handleValuesChange: (value: (string | number | undefined | null)[]) => void\n}\n\nexport const TypeToOperatorOptions: Record<\n FilterType,\n { label: string; value: Operator }[]\n> = {\n select: [\n { label: 'is any of', value: 'equals' },\n { label: 'is not any of', value: 'notEqual' },\n { label: 'has no value', value: 'blank' },\n { label: 'has any value', value: 'notBlank' },\n ],\n number: [\n { label: 'is', value: 'equals' },\n { label: 'is not', value: 'notEqual' },\n { label: 'is greater than', value: 'greaterThan' },\n { label: 'is greater than or equal to', value: 'greaterThanOrEqual' },\n { label: 'is less than', value: 'lessThan' },\n { label: 'is less than or equal to', value: 'lessThanOrEqual' },\n { label: 'has no value', value: 'blank' },\n { label: 'has any value', value: 'notBlank' },\n { label: 'is between', value: 'inRange' },\n ],\n text: [\n { label: 'contains', value: 'contains' },\n { label: 'does not contain', value: 'notContains' },\n { label: 'starts with', value: 'startsWith' },\n { label: 'ends with', value: 'endsWith' },\n { label: 'has no value', value: 'blank' },\n { label: 'has any value', value: 'notBlank' },\n ],\n}\n\ntype SelectValueInputConfig = {\n type: 'select'\n valueOptions: { label: string; value: string }[]\n}\n\ntype NumberValueInputConfig = {\n type: 'number'\n inputCount: number\n numberOptions?: { max: number; min: number; step: number }\n}\n\ntype TextValueInputConfig = {\n type: 'text'\n}\n\nexport type ValueInputConfig =\n SelectValueInputConfig | NumberValueInputConfig | TextValueInputConfig\n\ntype FilterFieldState = {\n filter: FormFilter | undefined\n filterConfigs: FilterConfig[]\n config: FilterConfig | undefined\n operatorOptions: { label: string; value: string }[]\n valueInputConfig: ValueInputConfig | undefined\n}\n\ntype Action = {\n type: 'setField' | 'setOperator' | 'initialize'\n payload: string | undefined\n}\ntype SetValuesAction = {\n type: 'setValues'\n payload: (string | number | undefined | null)[]\n}\n\nconst buildValueInputConfig = (\n config: FilterConfig,\n operator: string\n): ValueInputConfig | undefined => {\n if (config.type === 'select') {\n if (operator === 'blank' || operator === 'notBlank') return\n\n return {\n type: 'select',\n valueOptions: (config.options as SelectOptionsType) || [],\n }\n }\n\n if (config.type === 'number') {\n if (operator === 'blank' || operator === 'notBlank') return\n\n return {\n type: 'number',\n inputCount: operator === 'inRange' ? 2 : 1,\n numberOptions: config.options as NumberOptionsType,\n }\n }\n\n if (config.type === 'text') {\n if (operator === 'blank' || operator === 'notBlank') return\n\n return {\n type: 'text',\n }\n }\n}\n\nconst filterFieldReducer = (\n state: FilterFieldState,\n action: Action | SetValuesAction\n) => {\n switch (action.type) {\n case 'setField': {\n const config = state.filterConfigs.find(\n (config) => config.field === action.payload\n )\n\n if (!config) return state\n\n const operatorOptions = config ? TypeToOperatorOptions[config.type] : []\n\n const newFilter = {\n ...state.filter,\n type: config.type,\n field: action.payload,\n operator: operatorOptions[0].value,\n values: [],\n } as FormFilter\n\n const pendingFilter = {\n ...newFilter,\n errors: validateFormFilter(newFilter),\n }\n\n return {\n ...state,\n filter: pendingFilter,\n config,\n operatorOptions,\n valueInputConfig:\n operatorOptions.length > 0\n ? buildValueInputConfig(config, operatorOptions[0].value)\n : undefined,\n }\n }\n case 'setOperator': {\n if (!state.config) return state\n if (!action.payload) return state\n\n const pendingFilter = {\n ...state.filter,\n operator: action.payload,\n values: [],\n } as FormFilter\n\n return {\n ...state,\n filter: {\n ...pendingFilter,\n errors: validateFormFilter(pendingFilter),\n },\n valueInputConfig: buildValueInputConfig(state.config, action.payload),\n }\n }\n case 'setValues': {\n if (!state.filter) return state\n\n const values =\n state.filter.type === 'number'\n ? action.payload.map((value) => Number(value))\n : (action.payload.filter(\n (value) => value !== null && value !== undefined\n ) as string[])\n\n const pendingFilter = {\n ...state.filter,\n values: values,\n }\n\n pendingFilter.errors = validateFormFilter(pendingFilter)\n\n return {\n ...state,\n filter: pendingFilter,\n }\n }\n case 'initialize': {\n const field =\n state.filter?.field ||\n state.filterConfigs.find((config) => !config.disabled)?.field\n const config = state.filterConfigs.find(\n (config) => config.field === field\n )\n\n if (!config) return state\n\n const operatorOptions = config ? TypeToOperatorOptions[config.type] : []\n\n const operator = state.filter?.operator || operatorOptions[0].value\n\n const filter =\n state.filter ??\n ({\n field,\n type: config.type,\n operator,\n values: [],\n } as FormFilter)\n\n const errors = validateFormFilter(filter)\n filter.errors = errors\n\n return {\n ...state,\n config,\n filter,\n operatorOptions,\n valueInputConfig:\n operatorOptions.length > 0\n ? buildValueInputConfig(config, operator)\n : undefined,\n }\n }\n default:\n return state\n }\n}\n\nexport const useFilterField = ({\n filterConfigs,\n filter: initialFilter,\n}: UseFilterFieldInputs): UseFilterFieldOutputs => {\n const [{ filter, config, operatorOptions, valueInputConfig }, dispatch] =\n React.useReducer(\n filterFieldReducer,\n {\n filter: initialFilter,\n filterConfigs,\n config: undefined,\n operatorOptions: [],\n valueInputConfig: undefined,\n },\n (state) => {\n return filterFieldReducer(state, {\n type: 'initialize',\n payload: undefined,\n })\n }\n )\n\n const handleFieldChange = (value: string) => {\n dispatch({\n type: 'setField',\n payload: value,\n })\n }\n\n const handleOperatorChange = (value: string) => {\n dispatch({\n type: 'setOperator',\n payload: value,\n })\n }\n\n const handleValuesChange = (\n value: (string | number | undefined | null)[]\n ) => {\n dispatch({\n type: 'setValues',\n payload: value,\n })\n }\n\n return {\n filter,\n config,\n operatorOptions,\n valueInputConfig,\n handleFieldChange,\n handleOperatorChange,\n handleValuesChange,\n }\n}\n"],"names":["config","validateFormFilter","React"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAyBO,MAAM,wBAGT;AAAA,EACF,QAAQ;AAAA,IACN,EAAE,OAAO,aAAa,OAAO,SAAA;AAAA,IAC7B,EAAE,OAAO,iBAAiB,OAAO,WAAA;AAAA,IACjC,EAAE,OAAO,gBAAgB,OAAO,QAAA;AAAA,IAChC,EAAE,OAAO,iBAAiB,OAAO,WAAA;AAAA,EAAW;AAAA,EAE9C,QAAQ;AAAA,IACN,EAAE,OAAO,MAAM,OAAO,SAAA;AAAA,IACtB,EAAE,OAAO,UAAU,OAAO,WAAA;AAAA,IAC1B,EAAE,OAAO,mBAAmB,OAAO,cAAA;AAAA,IACnC,EAAE,OAAO,+BAA+B,OAAO,qBAAA;AAAA,IAC/C,EAAE,OAAO,gBAAgB,OAAO,WAAA;AAAA,IAChC,EAAE,OAAO,4BAA4B,OAAO,kBAAA;AAAA,IAC5C,EAAE,OAAO,gBAAgB,OAAO,QAAA;AAAA,IAChC,EAAE,OAAO,iBAAiB,OAAO,WAAA;AAAA,IACjC,EAAE,OAAO,cAAc,OAAO,UAAA;AAAA,EAAU;AAAA,EAE1C,MAAM;AAAA,IACJ,EAAE,OAAO,YAAY,OAAO,WAAA;AAAA,IAC5B,EAAE,OAAO,oBAAoB,OAAO,cAAA;AAAA,IACpC,EAAE,OAAO,eAAe,OAAO,aAAA;AAAA,IAC/B,EAAE,OAAO,aAAa,OAAO,WAAA;AAAA,IAC7B,EAAE,OAAO,gBAAgB,OAAO,QAAA;AAAA,IAChC,EAAE,OAAO,iBAAiB,OAAO,WAAA;AAAA,EAAW;AAEhD;AAqCA,MAAM,wBAAwB,CAC5B,QACA,aACiC;AACjC,MAAI,OAAO,SAAS,UAAU;AAC5B,QAAI,aAAa,WAAW,aAAa,WAAY;AAErD,WAAO;AAAA,MACL,MAAM;AAAA,MACN,cAAe,OAAO,WAAiC,CAAA;AAAA,IAAC;AAAA,EAE5D;AAEA,MAAI,OAAO,SAAS,UAAU;AAC5B,QAAI,aAAa,WAAW,aAAa,WAAY;AAErD,WAAO;AAAA,MACL,MAAM;AAAA,MACN,YAAY,aAAa,YAAY,IAAI;AAAA,MACzC,eAAe,OAAO;AAAA,IAAA;AAAA,EAE1B;AAEA,MAAI,OAAO,SAAS,QAAQ;AAC1B,QAAI,aAAa,WAAW,aAAa,WAAY;AAErD,WAAO;AAAA,MACL,MAAM;AAAA,IAAA;AAAA,EAEV;AACF;AAEA,MAAM,qBAAqB,CACzB,OACA,WACG;;AACH,UAAQ,OAAO,MAAA;AAAA,IACb,KAAK,YAAY;AACf,YAAM,SAAS,MAAM,cAAc;AAAA,QACjC,CAACA,YAAWA,QAAO,UAAU,OAAO;AAAA,MAAA;AAGtC,UAAI,CAAC,OAAQ,QAAO;AAEpB,YAAM,kBAAkB,SAAS,sBAAsB,OAAO,IAAI,IAAI,CAAA;AAEtE,YAAM,YAAY;AAAA,QAChB,GAAG,MAAM;AAAA,QACT,MAAM,OAAO;AAAA,QACb,OAAO,OAAO;AAAA,QACd,UAAU,gBAAgB,CAAC,EAAE;AAAA,QAC7B,QAAQ,CAAA;AAAA,MAAC;AAGX,YAAM,gBAAgB;AAAA,QACpB,GAAG;AAAA,QACH,QAAQC,cAAAA,mBAAmB,SAAS;AAAA,MAAA;AAGtC,aAAO;AAAA,QACL,GAAG;AAAA,QACH,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA,kBACE,gBAAgB,SAAS,IACrB,sBAAsB,QAAQ,gBAAgB,CAAC,EAAE,KAAK,IACtD;AAAA,MAAA;AAAA,IAEV;AAAA,IACA,KAAK,eAAe;AAClB,UAAI,CAAC,MAAM,OAAQ,QAAO;AAC1B,UAAI,CAAC,OAAO,QAAS,QAAO;AAE5B,YAAM,gBAAgB;AAAA,QACpB,GAAG,MAAM;AAAA,QACT,UAAU,OAAO;AAAA,QACjB,QAAQ,CAAA;AAAA,MAAC;AAGX,aAAO;AAAA,QACL,GAAG;AAAA,QACH,QAAQ;AAAA,UACN,GAAG;AAAA,UACH,QAAQA,cAAAA,mBAAmB,aAAa;AAAA,QAAA;AAAA,QAE1C,kBAAkB,sBAAsB,MAAM,QAAQ,OAAO,OAAO;AAAA,MAAA;AAAA,IAExE;AAAA,IACA,KAAK,aAAa;AAChB,UAAI,CAAC,MAAM,OAAQ,QAAO;AAE1B,YAAM,SACJ,MAAM,OAAO,SAAS,WAClB,OAAO,QAAQ,IAAI,CAAC,UAAU,OAAO,KAAK,CAAC,IAC1C,OAAO,QAAQ;AAAA,QACd,CAAC,UAAU,UAAU,QAAQ,UAAU;AAAA,MAAA;AAG/C,YAAM,gBAAgB;AAAA,QACpB,GAAG,MAAM;AAAA,QACT;AAAA,MAAA;AAGF,oBAAc,SAASA,cAAAA,mBAAmB,aAAa;AAEvD,aAAO;AAAA,QACL,GAAG;AAAA,QACH,QAAQ;AAAA,MAAA;AAAA,IAEZ;AAAA,IACA,KAAK,cAAc;AACjB,YAAM,UACJ,WAAM,WAAN,mBAAc,YACd,WAAM,cAAc,KAAK,CAACD,YAAW,CAACA,QAAO,QAAQ,MAArD,mBAAwD;AAC1D,YAAM,SAAS,MAAM,cAAc;AAAA,QACjC,CAACA,YAAWA,QAAO,UAAU;AAAA,MAAA;AAG/B,UAAI,CAAC,OAAQ,QAAO;AAEpB,YAAM,kBAAkB,SAAS,sBAAsB,OAAO,IAAI,IAAI,CAAA;AAEtE,YAAM,aAAW,WAAM,WAAN,mBAAc,aAAY,gBAAgB,CAAC,EAAE;AAE9D,YAAM,SACJ,MAAM,UACL;AAAA,QACC;AAAA,QACA,MAAM,OAAO;AAAA,QACb;AAAA,QACA,QAAQ,CAAA;AAAA,MAAC;AAGb,YAAM,SAASC,cAAAA,mBAAmB,MAAM;AACxC,aAAO,SAAS;AAEhB,aAAO;AAAA,QACL,GAAG;AAAA,QACH;AAAA,QACA;AAAA,QACA;AAAA,QACA,kBACE,gBAAgB,SAAS,IACrB,sBAAsB,QAAQ,QAAQ,IACtC;AAAA,MAAA;AAAA,IAEV;AAAA,IACA;AACE,aAAO;AAAA,EAAA;AAEb;AAEO,MAAM,iBAAiB,CAAC;AAAA,EAC7B;AAAA,EACA,QAAQ;AACV,MAAmD;AACjD,QAAM,CAAC,EAAE,QAAQ,QAAQ,iBAAiB,oBAAoB,QAAQ,IACpEC,iBAAM;AAAA,IACJ;AAAA,IACA;AAAA,MACE,QAAQ;AAAA,MACR;AAAA,MACA,QAAQ;AAAA,MACR,iBAAiB,CAAA;AAAA,MACjB,kBAAkB;AAAA,IAAA;AAAA,IAEpB,CAAC,UAAU;AACT,aAAO,mBAAmB,OAAO;AAAA,QAC/B,MAAM;AAAA,QACN,SAAS;AAAA,MAAA,CACV;AAAA,IACH;AAAA,EAAA;AAGJ,QAAM,oBAAoB,CAAC,UAAkB;AAC3C,aAAS;AAAA,MACP,MAAM;AAAA,MACN,SAAS;AAAA,IAAA,CACV;AAAA,EACH;AAEA,QAAM,uBAAuB,CAAC,UAAkB;AAC9C,aAAS;AAAA,MACP,MAAM;AAAA,MACN,SAAS;AAAA,IAAA,CACV;AAAA,EACH;AAEA,QAAM,qBAAqB,CACzB,UACG;AACH,aAAS;AAAA,MACP,MAAM;AAAA,MACN,SAAS;AAAA,IAAA,CACV;AAAA,EACH;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA;AAEJ;;;"}
1
+ {"version":3,"file":"useFilterField.cjs","sources":["../../../../src/components/filterform/useFilterField.tsx"],"sourcesContent":["import * as React from 'react'\nimport {\n FilterConfig,\n FilterType,\n FormFilter,\n NumberOptionsType,\n Operator,\n SelectOptionsType,\n} from './types'\nimport {\n isMultiValueTextOperator,\n normalizeMultiValues,\n validateFormFilter,\n} from './filterHelpers'\nexport { isMultiValueTextOperator } from './filterHelpers'\nexport type UseFilterFieldInputs = {\n filterConfigs: FilterConfig[]\n filter?: FormFilter\n}\n\ntype UseFilterFieldOutputs = {\n filter: FormFilter | undefined\n config: FilterConfig | undefined\n operatorOptions: { label: string; value: string }[]\n valueInputConfig: ValueInputConfig | undefined\n handleFieldChange: (value: string) => void\n handleOperatorChange: (value: string) => void\n handleValuesChange: (value: (string | number | undefined | null)[]) => void\n}\n\nexport const TypeToOperatorOptions: Record<\n FilterType,\n { label: string; value: Operator }[]\n> = {\n select: [\n { label: 'is any of', value: 'equals' },\n { label: 'is not any of', value: 'notEqual' },\n { label: 'has no value', value: 'blank' },\n { label: 'has any value', value: 'notBlank' },\n ],\n number: [\n { label: 'is', value: 'equals' },\n { label: 'is not', value: 'notEqual' },\n { label: 'is greater than', value: 'greaterThan' },\n { label: 'is greater than or equal to', value: 'greaterThanOrEqual' },\n { label: 'is less than', value: 'lessThan' },\n { label: 'is less than or equal to', value: 'lessThanOrEqual' },\n { label: 'has no value', value: 'blank' },\n { label: 'has any value', value: 'notBlank' },\n { label: 'is between', value: 'inRange' },\n ],\n text: [\n { label: 'contains', value: 'contains' },\n { label: 'does not contain', value: 'notContains' },\n { label: 'starts with', value: 'startsWith' },\n { label: 'ends with', value: 'endsWith' },\n { label: 'has no value', value: 'blank' },\n { label: 'has any value', value: 'notBlank' },\n ],\n}\n\n// Multi-value exact-match operators for text fields. Deliberately NOT part of\n// `TypeToOperatorOptions.text`: a field only offers them when its\n// `FilterConfig.operators` lists them, so every existing consumer keeps the\n// operator list it has today (see the `operators` doc in types.ts).\nexport const MultiValueTextOperatorOptions: {\n label: string\n value: Operator\n}[] = [\n { label: 'is any of', value: 'isAnyOf' },\n { label: 'is not any of', value: 'isNoneOf' },\n]\n\n/**\n * Operator options a field offers: its type's default list unless the config\n * names its own (`operators`), in which case that list — in the given order,\n * dropping anything the type can't express. `operators` may therefore both\n * narrow the defaults and add the opt-in multi-value text operators.\n */\nexport const operatorOptionsFor = (\n config: FilterConfig\n): { label: string; value: Operator }[] => {\n const defaults = TypeToOperatorOptions[config.type]\n if (!config.operators) return defaults\n const known =\n config.type === 'text'\n ? [...defaults, ...MultiValueTextOperatorOptions]\n : defaults\n return config.operators.flatMap((operator) => {\n const option = known.find((candidate) => candidate.value === operator)\n return option ? [option] : []\n })\n}\n\ntype SelectValueInputConfig = {\n type: 'select'\n valueOptions: { label: string; value: string }[]\n}\n\ntype NumberValueInputConfig = {\n type: 'number'\n inputCount: number\n numberOptions?: { max: number; min: number; step: number }\n}\n\ntype TextValueInputConfig = {\n type: 'text'\n // True for `isAnyOf` / `isNoneOf`: the value input is a chips list rather\n // than a single text box. `maxValues` is the config's cap, if any.\n multiValue?: boolean\n maxValues?: number\n}\n\nexport type ValueInputConfig =\n SelectValueInputConfig | NumberValueInputConfig | TextValueInputConfig\n\ntype FilterFieldState = {\n filter: FormFilter | undefined\n filterConfigs: FilterConfig[]\n config: FilterConfig | undefined\n operatorOptions: { label: string; value: string }[]\n valueInputConfig: ValueInputConfig | undefined\n}\n\ntype Action = {\n type: 'setField' | 'setOperator' | 'initialize'\n payload: string | undefined\n}\ntype SetValuesAction = {\n type: 'setValues'\n payload: (string | number | undefined | null)[]\n}\n\nconst buildValueInputConfig = (\n config: FilterConfig,\n operator: string\n): ValueInputConfig | undefined => {\n if (config.type === 'select') {\n if (operator === 'blank' || operator === 'notBlank') return\n\n return {\n type: 'select',\n valueOptions: (config.options as SelectOptionsType) || [],\n }\n }\n\n if (config.type === 'number') {\n if (operator === 'blank' || operator === 'notBlank') return\n\n return {\n type: 'number',\n inputCount: operator === 'inRange' ? 2 : 1,\n numberOptions: config.options as NumberOptionsType,\n }\n }\n\n if (config.type === 'text') {\n if (operator === 'blank' || operator === 'notBlank') return\n\n return {\n type: 'text',\n multiValue: isMultiValueTextOperator(operator),\n maxValues: config.maxValues,\n }\n }\n}\n\nconst filterFieldReducer = (\n state: FilterFieldState,\n action: Action | SetValuesAction\n) => {\n switch (action.type) {\n case 'setField': {\n const config = state.filterConfigs.find(\n (config) => config.field === action.payload\n )\n\n if (!config) return state\n\n const operatorOptions = operatorOptionsFor(config)\n\n const newFilter = {\n ...state.filter,\n type: config.type,\n field: action.payload,\n operator: operatorOptions[0]?.value,\n values: [],\n } as FormFilter\n\n const pendingFilter = {\n ...newFilter,\n errors: validateFormFilter(newFilter, config),\n }\n\n return {\n ...state,\n filter: pendingFilter,\n config,\n operatorOptions,\n valueInputConfig:\n operatorOptions.length > 0\n ? buildValueInputConfig(config, operatorOptions[0].value)\n : undefined,\n }\n }\n case 'setOperator': {\n if (!state.config) return state\n if (!action.payload) return state\n\n const pendingFilter = {\n ...state.filter,\n operator: action.payload,\n values: [],\n } as FormFilter\n\n return {\n ...state,\n filter: {\n ...pendingFilter,\n errors: validateFormFilter(pendingFilter, state.config),\n },\n valueInputConfig: buildValueInputConfig(state.config, action.payload),\n }\n }\n case 'setValues': {\n if (!state.filter) return state\n\n const values =\n state.filter.type === 'number'\n ? action.payload.map((value) => Number(value))\n : isMultiValueTextOperator(state.filter.operator)\n ? // Normalized on the way into state, not just for display —\n // validation counts and `onSubmit` emit exactly these chips.\n normalizeMultiValues(action.payload)\n : (action.payload.filter(\n (value) => value !== null && value !== undefined\n ) as string[])\n\n const pendingFilter = {\n ...state.filter,\n values: values,\n }\n\n pendingFilter.errors = validateFormFilter(pendingFilter, state.config)\n\n return {\n ...state,\n filter: pendingFilter,\n }\n }\n case 'initialize': {\n const field =\n state.filter?.field ||\n state.filterConfigs.find((config) => !config.disabled)?.field\n const config = state.filterConfigs.find(\n (config) => config.field === field\n )\n\n if (!config) return state\n\n const operatorOptions = operatorOptionsFor(config)\n\n // A persisted filter's operator must still be one this field offers.\n // Configs change between sessions (a field narrowed to `contains`, or\n // one that never opted into the multi-value operators), and the\n // backend may not understand a stale operator — so it falls back to\n // the field's first operator with its values cleared, rather than\n // re-submitting something the config no longer allows.\n const persisted = state.filter?.operator\n const persistedAllowed =\n persisted !== undefined &&\n operatorOptions.some((option) => option.value === persisted)\n const operator = persistedAllowed ? persisted : operatorOptions[0]?.value\n\n let filter: FormFilter\n if (!state.filter) {\n filter = {\n field,\n type: config.type,\n operator,\n values: [],\n } as FormFilter\n } else if (!persistedAllowed) {\n filter = { ...state.filter, operator, values: [] } as FormFilter\n } else if (isMultiValueTextOperator(operator)) {\n // Same normalization as `setValues`, so a persisted chip list that\n // predates it (or was hand-edited) can't show one chip and count two.\n filter = {\n ...state.filter,\n values: normalizeMultiValues(state.filter.values),\n }\n } else {\n // A copy: the incoming object is the caller's, and writing `errors`\n // onto it would hide the correction from `FilterInput`'s comparison\n // of what it was given against what it initialized to.\n filter = { ...state.filter }\n }\n\n filter.errors = validateFormFilter(filter, config)\n\n return {\n ...state,\n config,\n filter,\n operatorOptions,\n valueInputConfig:\n operatorOptions.length > 0\n ? buildValueInputConfig(config, operator)\n : undefined,\n }\n }\n default:\n return state\n }\n}\n\nexport const useFilterField = ({\n filterConfigs,\n filter: initialFilter,\n}: UseFilterFieldInputs): UseFilterFieldOutputs => {\n const [{ filter, config, operatorOptions, valueInputConfig }, dispatch] =\n React.useReducer(\n filterFieldReducer,\n {\n filter: initialFilter,\n filterConfigs,\n config: undefined,\n operatorOptions: [],\n valueInputConfig: undefined,\n },\n (state) => {\n return filterFieldReducer(state, {\n type: 'initialize',\n payload: undefined,\n })\n }\n )\n\n const handleFieldChange = (value: string) => {\n dispatch({\n type: 'setField',\n payload: value,\n })\n }\n\n const handleOperatorChange = (value: string) => {\n dispatch({\n type: 'setOperator',\n payload: value,\n })\n }\n\n const handleValuesChange = (\n value: (string | number | undefined | null)[]\n ) => {\n dispatch({\n type: 'setValues',\n payload: value,\n })\n }\n\n return {\n filter,\n config,\n operatorOptions,\n valueInputConfig,\n handleFieldChange,\n handleOperatorChange,\n handleValuesChange,\n }\n}\n"],"names":["isMultiValueTextOperator","config","validateFormFilter","normalizeMultiValues","React"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { FilterConfig, FilterType, FormFilter, Operator } from './types.js';
2
+ export { isMultiValueTextOperator } from './filterHelpers.js';
2
3
  export type UseFilterFieldInputs = {
3
4
  filterConfigs: FilterConfig[];
4
5
  filter?: FormFilter;
@@ -19,6 +20,20 @@ export declare const TypeToOperatorOptions: Record<FilterType, {
19
20
  label: string;
20
21
  value: Operator;
21
22
  }[]>;
23
+ export declare const MultiValueTextOperatorOptions: {
24
+ label: string;
25
+ value: Operator;
26
+ }[];
27
+ /**
28
+ * Operator options a field offers: its type's default list unless the config
29
+ * names its own (`operators`), in which case that list — in the given order,
30
+ * dropping anything the type can't express. `operators` may therefore both
31
+ * narrow the defaults and add the opt-in multi-value text operators.
32
+ */
33
+ export declare const operatorOptionsFor: (config: FilterConfig) => {
34
+ label: string;
35
+ value: Operator;
36
+ }[];
22
37
  type SelectValueInputConfig = {
23
38
  type: 'select';
24
39
  valueOptions: {
@@ -37,7 +52,8 @@ type NumberValueInputConfig = {
37
52
  };
38
53
  type TextValueInputConfig = {
39
54
  type: 'text';
55
+ multiValue?: boolean;
56
+ maxValues?: number;
40
57
  };
41
58
  export type ValueInputConfig = SelectValueInputConfig | NumberValueInputConfig | TextValueInputConfig;
42
59
  export declare const useFilterField: ({ filterConfigs, filter: initialFilter, }: UseFilterFieldInputs) => UseFilterFieldOutputs;
43
- export {};
@@ -1,5 +1,5 @@
1
1
  import * as React from "react";
2
- import { validateFormFilter } from "./filterHelpers.js";
2
+ import { isMultiValueTextOperator, normalizeMultiValues, validateFormFilter } from "./filterHelpers.js";
3
3
  const TypeToOperatorOptions = {
4
4
  select: [
5
5
  { label: "is any of", value: "equals" },
@@ -27,6 +27,19 @@ const TypeToOperatorOptions = {
27
27
  { label: "has any value", value: "notBlank" }
28
28
  ]
29
29
  };
30
+ const MultiValueTextOperatorOptions = [
31
+ { label: "is any of", value: "isAnyOf" },
32
+ { label: "is not any of", value: "isNoneOf" }
33
+ ];
34
+ const operatorOptionsFor = (config) => {
35
+ const defaults = TypeToOperatorOptions[config.type];
36
+ if (!config.operators) return defaults;
37
+ const known = config.type === "text" ? [...defaults, ...MultiValueTextOperatorOptions] : defaults;
38
+ return config.operators.flatMap((operator) => {
39
+ const option = known.find((candidate) => candidate.value === operator);
40
+ return option ? [option] : [];
41
+ });
42
+ };
30
43
  const buildValueInputConfig = (config, operator) => {
31
44
  if (config.type === "select") {
32
45
  if (operator === "blank" || operator === "notBlank") return;
@@ -46,29 +59,31 @@ const buildValueInputConfig = (config, operator) => {
46
59
  if (config.type === "text") {
47
60
  if (operator === "blank" || operator === "notBlank") return;
48
61
  return {
49
- type: "text"
62
+ type: "text",
63
+ multiValue: isMultiValueTextOperator(operator),
64
+ maxValues: config.maxValues
50
65
  };
51
66
  }
52
67
  };
53
68
  const filterFieldReducer = (state, action) => {
54
- var _a, _b, _c;
69
+ var _a, _b, _c, _d, _e;
55
70
  switch (action.type) {
56
71
  case "setField": {
57
72
  const config = state.filterConfigs.find(
58
73
  (config2) => config2.field === action.payload
59
74
  );
60
75
  if (!config) return state;
61
- const operatorOptions = config ? TypeToOperatorOptions[config.type] : [];
76
+ const operatorOptions = operatorOptionsFor(config);
62
77
  const newFilter = {
63
78
  ...state.filter,
64
79
  type: config.type,
65
80
  field: action.payload,
66
- operator: operatorOptions[0].value,
81
+ operator: (_a = operatorOptions[0]) == null ? void 0 : _a.value,
67
82
  values: []
68
83
  };
69
84
  const pendingFilter = {
70
85
  ...newFilter,
71
- errors: validateFormFilter(newFilter)
86
+ errors: validateFormFilter(newFilter, config)
72
87
  };
73
88
  return {
74
89
  ...state,
@@ -90,42 +105,59 @@ const filterFieldReducer = (state, action) => {
90
105
  ...state,
91
106
  filter: {
92
107
  ...pendingFilter,
93
- errors: validateFormFilter(pendingFilter)
108
+ errors: validateFormFilter(pendingFilter, state.config)
94
109
  },
95
110
  valueInputConfig: buildValueInputConfig(state.config, action.payload)
96
111
  };
97
112
  }
98
113
  case "setValues": {
99
114
  if (!state.filter) return state;
100
- const values = state.filter.type === "number" ? action.payload.map((value) => Number(value)) : action.payload.filter(
115
+ const values = state.filter.type === "number" ? action.payload.map((value) => Number(value)) : isMultiValueTextOperator(state.filter.operator) ? (
116
+ // Normalized on the way into state, not just for display —
117
+ // validation counts and `onSubmit` emit exactly these chips.
118
+ normalizeMultiValues(action.payload)
119
+ ) : action.payload.filter(
101
120
  (value) => value !== null && value !== void 0
102
121
  );
103
122
  const pendingFilter = {
104
123
  ...state.filter,
105
124
  values
106
125
  };
107
- pendingFilter.errors = validateFormFilter(pendingFilter);
126
+ pendingFilter.errors = validateFormFilter(pendingFilter, state.config);
108
127
  return {
109
128
  ...state,
110
129
  filter: pendingFilter
111
130
  };
112
131
  }
113
132
  case "initialize": {
114
- const field = ((_a = state.filter) == null ? void 0 : _a.field) || ((_b = state.filterConfigs.find((config2) => !config2.disabled)) == null ? void 0 : _b.field);
133
+ const field = ((_b = state.filter) == null ? void 0 : _b.field) || ((_c = state.filterConfigs.find((config2) => !config2.disabled)) == null ? void 0 : _c.field);
115
134
  const config = state.filterConfigs.find(
116
135
  (config2) => config2.field === field
117
136
  );
118
137
  if (!config) return state;
119
- const operatorOptions = config ? TypeToOperatorOptions[config.type] : [];
120
- const operator = ((_c = state.filter) == null ? void 0 : _c.operator) || operatorOptions[0].value;
121
- const filter = state.filter ?? {
122
- field,
123
- type: config.type,
124
- operator,
125
- values: []
126
- };
127
- const errors = validateFormFilter(filter);
128
- filter.errors = errors;
138
+ const operatorOptions = operatorOptionsFor(config);
139
+ const persisted = (_d = state.filter) == null ? void 0 : _d.operator;
140
+ const persistedAllowed = persisted !== void 0 && operatorOptions.some((option) => option.value === persisted);
141
+ const operator = persistedAllowed ? persisted : (_e = operatorOptions[0]) == null ? void 0 : _e.value;
142
+ let filter;
143
+ if (!state.filter) {
144
+ filter = {
145
+ field,
146
+ type: config.type,
147
+ operator,
148
+ values: []
149
+ };
150
+ } else if (!persistedAllowed) {
151
+ filter = { ...state.filter, operator, values: [] };
152
+ } else if (isMultiValueTextOperator(operator)) {
153
+ filter = {
154
+ ...state.filter,
155
+ values: normalizeMultiValues(state.filter.values)
156
+ };
157
+ } else {
158
+ filter = { ...state.filter };
159
+ }
160
+ filter.errors = validateFormFilter(filter, config);
129
161
  return {
130
162
  ...state,
131
163
  config,
@@ -187,7 +219,10 @@ const useFilterField = ({
187
219
  };
188
220
  };
189
221
  export {
222
+ MultiValueTextOperatorOptions,
190
223
  TypeToOperatorOptions,
224
+ isMultiValueTextOperator,
225
+ operatorOptionsFor,
191
226
  useFilterField
192
227
  };
193
228
  //# sourceMappingURL=useFilterField.js.map