@wallarm-org/design-system 0.66.3 → 0.67.1

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 (39) hide show
  1. package/dist/components/FilterInput/FilterInputContext/types.d.ts +6 -0
  2. package/dist/components/FilterInput/FilterInputContext/useFilterInputContextValue.d.ts +2 -0
  3. package/dist/components/FilterInput/FilterInputContext/useFilterInputContextValue.js +4 -0
  4. package/dist/components/FilterInput/FilterInputErrors/parseFilterInputErrors.js +11 -2
  5. package/dist/components/FilterInput/FilterInputField/ChipsWithGaps.js +4 -2
  6. package/dist/components/FilterInput/FilterInputField/FilterInputChip/FilterInputChip.d.ts +7 -1
  7. package/dist/components/FilterInput/FilterInputField/FilterInputChip/FilterInputChip.js +44 -6
  8. package/dist/components/FilterInput/FilterInputField/FilterInputChip/PairSeparator.d.ts +3 -0
  9. package/dist/components/FilterInput/FilterInputField/FilterInputChip/PairSeparator.js +16 -0
  10. package/dist/components/FilterInput/FilterInputField/FilterInputChip/context/EditingContext.d.ts +2 -0
  11. package/dist/components/FilterInput/FilterInputField/FilterInputChip/index.d.ts +3 -2
  12. package/dist/components/FilterInput/FilterInputField/FilterInputChip/index.js +3 -2
  13. package/dist/components/FilterInput/FilterInputField/FilterInputChip/segmentVariant.d.ts +2 -0
  14. package/dist/components/FilterInput/FilterInputField/FilterInputChip/segmentVariant.js +2 -1
  15. package/dist/components/FilterInput/FilterInputField/FilterInputField.js +3 -1
  16. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/lib/deriveAutocompleteValues.d.ts +8 -1
  17. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/lib/deriveAutocompleteValues.js +22 -3
  18. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useAutocompleteState.d.ts +10 -0
  19. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useAutocompleteState.js +6 -0
  20. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useChipEditing.d.ts +2 -0
  21. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useChipEditing.js +31 -0
  22. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.d.ts +2 -0
  23. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.js +14 -3
  24. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow/types.d.ts +9 -0
  25. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow/useOperatorFlow.d.ts +1 -1
  26. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow/useOperatorFlow.js +22 -7
  27. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow/useValueFlow.d.ts +1 -1
  28. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow/useValueFlow.js +77 -13
  29. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useResetState.d.ts +3 -1
  30. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useResetState.js +5 -1
  31. package/dist/components/FilterInput/hooks/useFilterInputExpression/buildChips.js +26 -1
  32. package/dist/components/FilterInput/hooks/useFilterInputExpression/expression.d.ts +9 -3
  33. package/dist/components/FilterInput/hooks/useFilterInputExpression/expression.js +75 -9
  34. package/dist/components/FilterInput/hooks/useFilterInputExpression/useFilterInputExpression.d.ts +1 -1
  35. package/dist/components/FilterInput/hooks/useFilterInputExpression/useFilterInputExpression.js +99 -71
  36. package/dist/components/FilterInput/lib/constants.js +2 -2
  37. package/dist/components/FilterInput/types.d.ts +25 -1
  38. package/dist/metadata/components.json +8 -2
  39. package/package.json +1 -1
@@ -1,10 +1,13 @@
1
1
  import type { RefObject } from 'react';
2
2
  import type { ChipSegment } from '../../../FilterInputField/FilterInputChip';
3
3
  import type { Condition, FieldMetadata, FilterOperator, MenuState, UpsertCondition } from '../../../types';
4
+ import type { BuildingBase } from '../useAutocompleteState';
4
5
  export interface MenuFlowDeps {
5
6
  editing: {
6
7
  editingChipId: string | null;
7
8
  editingSegment: ChipSegment | null;
9
+ /** Which triplet is being edited: 0 = base, 1 = paired second. */
10
+ editingSide: 0 | 1;
8
11
  /** Pre-derived chipId === null && segment !== null marker — single source
9
12
  * of truth, avoids re-deriving in every consumer. */
10
13
  isBuildingEdit: boolean;
@@ -36,6 +39,12 @@ export interface MenuFlowDeps {
36
39
  setInputText: (text: string) => void;
37
40
  setMenuState: (state: MenuState) => void;
38
41
  setBuildingMultiValue: (val: string | undefined) => void;
42
+ /** Which triplet is being built: 0 = base, 1 = paired second. */
43
+ buildingSide: 0 | 1;
44
+ setBuildingSide: (side: 0 | 1) => void;
45
+ /** Base triplet stashed while building a paired chip's second value. */
46
+ buildingBase: BuildingBase | null;
47
+ setBuildingBase: (base: BuildingBase | null) => void;
39
48
  }
40
49
  /** Shared shape: each sub-hook receives the same deps plus a ref to `conditions`
41
50
  * that stays fresh without forcing callbacks to recreate on every keystroke. */
@@ -5,7 +5,7 @@ import type { MenuFlowInternalDeps } from './types';
5
5
  * `handleCustomOperatorCommit` is the keyboard-typed path — it resolves the
6
6
  * typed text to a known operator (label / raw key / symbol) and delegates.
7
7
  */
8
- export declare const useOperatorFlow: ({ editing, selectedField, selectedOperator, insertIndex, upsertCondition, conditionsRef, resetState, setSelectedOperator, setMenuState, setBuildingMultiValue, setInputText, }: MenuFlowInternalDeps) => {
8
+ export declare const useOperatorFlow: ({ editing, selectedField, selectedOperator, insertIndex, upsertCondition, conditionsRef, resetState, setSelectedOperator, setMenuState, setBuildingMultiValue, setInputText, buildingSide, buildingBase, setBuildingBase, setBuildingSide, }: MenuFlowInternalDeps) => {
9
9
  handleOperatorSelect: (operator: FilterOperator) => void;
10
10
  handleCustomOperatorCommit: (customText: string) => void;
11
11
  };
@@ -1,8 +1,8 @@
1
1
  import { useCallback } from "react";
2
2
  import { SEGMENT_VARIANT } from "../../../FilterInputField/FilterInputChip/index.js";
3
3
  import { OPERATOR_SYMBOLS, chipIdToConditionIndex, getFieldOperators, getOperatorFromLabel, isNoValueOperator, isOperatorAllowedForField, isValueShapeCompatible } from "../../../lib/index.js";
4
- const useOperatorFlow = ({ editing, selectedField, selectedOperator, insertIndex, upsertCondition, conditionsRef, resetState, setSelectedOperator, setMenuState, setBuildingMultiValue, setInputText })=>{
5
- const { editingChipId, editingSegment, isBuildingEdit, setEditingSegment, setSegmentFilterText, clearEditing } = editing;
4
+ const useOperatorFlow = ({ editing, selectedField, selectedOperator, insertIndex, upsertCondition, conditionsRef, resetState, setSelectedOperator, setMenuState, setBuildingMultiValue, setInputText, buildingSide, buildingBase, setBuildingBase, setBuildingSide })=>{
5
+ const { editingChipId, editingSegment, editingSide, isBuildingEdit, setEditingSegment, setSegmentFilterText, clearEditing } = editing;
6
6
  const handleOperatorSelect = useCallback((operator)=>{
7
7
  if (!selectedField) return;
8
8
  if (isBuildingEdit && editingSegment === SEGMENT_VARIANT.operator) {
@@ -19,8 +19,16 @@ const useOperatorFlow = ({ editing, selectedField, selectedOperator, insertIndex
19
19
  return;
20
20
  }
21
21
  if (isNoValueOperator(operator)) {
22
+ if (1 === buildingSide && buildingBase && !editingChipId) {
23
+ upsertCondition(buildingBase.field, buildingBase.operator, buildingBase.value, null, insertIndex);
24
+ upsertCondition(selectedField, operator, null, null, void 0, void 0, void 0, 1);
25
+ setBuildingBase(null);
26
+ setBuildingSide(0);
27
+ resetState(true);
28
+ return;
29
+ }
22
30
  const isEditing = !!editingChipId;
23
- upsertCondition(selectedField, operator, null, editingChipId, isEditing ? void 0 : insertIndex);
31
+ upsertCondition(selectedField, operator, null, editingChipId, isEditing ? void 0 : insertIndex, void 0, void 0, editingSide);
24
32
  resetState(!isEditing);
25
33
  return;
26
34
  }
@@ -28,13 +36,15 @@ const useOperatorFlow = ({ editing, selectedField, selectedOperator, insertIndex
28
36
  const idx = chipIdToConditionIndex(editingChipId);
29
37
  const condition = null !== idx ? conditionsRef.current[idx] : null;
30
38
  if (condition) {
31
- const hasValue = null !== condition.value && '' !== condition.value;
39
+ const currentValue = 1 === editingSide ? condition.pair?.value : condition.value;
40
+ const currentDateOrigin = 1 === editingSide ? condition.pair?.dateOrigin : condition.dateOrigin;
41
+ const hasValue = null !== currentValue && '' !== currentValue && void 0 !== currentValue;
32
42
  if (hasValue) {
33
- upsertCondition(selectedField, operator, condition.value, editingChipId, void 0, void 0, condition.dateOrigin);
43
+ upsertCondition(selectedField, operator, currentValue, editingChipId, void 0, void 0, currentDateOrigin, editingSide);
34
44
  resetState();
35
45
  return;
36
46
  }
37
- upsertCondition(selectedField, operator, null, editingChipId);
47
+ upsertCondition(selectedField, operator, 1 === editingSide ? '' : null, editingChipId, void 0, void 0, void 0, editingSide);
38
48
  setEditingSegment(SEGMENT_VARIANT.value);
39
49
  setSegmentFilterText('');
40
50
  }
@@ -45,6 +55,7 @@ const useOperatorFlow = ({ editing, selectedField, selectedOperator, insertIndex
45
55
  }, [
46
56
  editingChipId,
47
57
  editingSegment,
58
+ editingSide,
48
59
  isBuildingEdit,
49
60
  setEditingSegment,
50
61
  setSegmentFilterText,
@@ -58,7 +69,11 @@ const useOperatorFlow = ({ editing, selectedField, selectedOperator, insertIndex
58
69
  setSelectedOperator,
59
70
  setMenuState,
60
71
  setBuildingMultiValue,
61
- setInputText
72
+ setInputText,
73
+ buildingSide,
74
+ buildingBase,
75
+ setBuildingBase,
76
+ setBuildingSide
62
77
  ]);
63
78
  const handleCustomOperatorCommit = useCallback((customText)=>{
64
79
  if (!selectedField || !customText.trim()) return;
@@ -4,7 +4,7 @@ import type { MenuFlowInternalDeps } from './types';
4
4
  * keyboard-typed custom-value commit (which routes through the field-type
5
5
  * specific resolver). Also handles multi-select preview/toggle plumbing.
6
6
  */
7
- export declare const useValueFlow: ({ editing, selectedField, selectedOperator, insertIndex, upsertCondition, conditionsRef, resetState, dateRange, setBuildingMultiValue, setInputText, }: MenuFlowInternalDeps) => {
7
+ export declare const useValueFlow: ({ editing, selectedField, selectedOperator, insertIndex, upsertCondition, conditionsRef, resetState, dateRange, buildingSide, setBuildingSide, buildingBase, setBuildingBase, setSelectedField, setSelectedOperator, setMenuState, setBuildingMultiValue, setInputText, }: MenuFlowInternalDeps) => {
8
8
  handleValueSelect: (val: string | number | boolean) => void;
9
9
  handleMultiCommit: (values: Array<string | number | boolean>) => void;
10
10
  handleBuildingValueChange: (preview: string | undefined) => void;
@@ -2,28 +2,77 @@ import { useCallback } from "react";
2
2
  import { SEGMENT_VARIANT } from "../../../FilterInputField/FilterInputChip/index.js";
3
3
  import { isBetweenOperator, isMultiSelectOperator } from "../../../lib/index.js";
4
4
  import { resolveDateRangeValue, resolveDateValue, resolveMultiValues, resolveSingleValue } from "../lib/index.js";
5
- const useValueFlow = ({ editing, selectedField, selectedOperator, insertIndex, upsertCondition, conditionsRef, resetState, dateRange, setBuildingMultiValue, setInputText })=>{
6
- const { editingChipId, editingSegment, resetSegmentTyping } = editing;
5
+ const useValueFlow = ({ editing, selectedField, selectedOperator, insertIndex, upsertCondition, conditionsRef, resetState, dateRange, buildingSide, setBuildingSide, buildingBase, setBuildingBase, setSelectedField, setSelectedOperator, setMenuState, setBuildingMultiValue, setInputText })=>{
6
+ const { editingChipId, editingSegment, editingSide, resetSegmentTyping } = editing;
7
+ const stashAndAdvance = useCallback((value)=>{
8
+ const pairedField = selectedField?.pairedField;
9
+ if (0 !== buildingSide || !pairedField || editingChipId || !selectedField || !selectedOperator) return false;
10
+ setBuildingBase({
11
+ field: selectedField,
12
+ operator: selectedOperator,
13
+ value
14
+ });
15
+ setSelectedField(pairedField);
16
+ setSelectedOperator(null);
17
+ setBuildingSide(1);
18
+ setInputText('');
19
+ setMenuState('operator');
20
+ return true;
21
+ }, [
22
+ selectedField,
23
+ selectedOperator,
24
+ buildingSide,
25
+ editingChipId,
26
+ setBuildingBase,
27
+ setSelectedField,
28
+ setSelectedOperator,
29
+ setBuildingSide,
30
+ setInputText,
31
+ setMenuState
32
+ ]);
33
+ const commitPairedSecond = useCallback((value, error, dateOrigin)=>{
34
+ if (1 !== buildingSide || !buildingBase || !selectedField || !selectedOperator) return false;
35
+ upsertCondition(buildingBase.field, buildingBase.operator, buildingBase.value, null, insertIndex);
36
+ upsertCondition(selectedField, selectedOperator, value, null, void 0, error, dateOrigin, 1);
37
+ setBuildingBase(null);
38
+ setBuildingSide(0);
39
+ resetState(true);
40
+ return true;
41
+ }, [
42
+ buildingSide,
43
+ buildingBase,
44
+ selectedField,
45
+ selectedOperator,
46
+ insertIndex,
47
+ upsertCondition,
48
+ setBuildingBase,
49
+ setBuildingSide,
50
+ resetState
51
+ ]);
7
52
  const handleValueSelect = useCallback((val)=>{
8
53
  if (!selectedField || !selectedOperator) return;
54
+ let committedValue = val;
9
55
  if (isBetweenOperator(selectedOperator) && 'date' === selectedField.type) {
10
56
  const result = dateRange.selectValue(String(val));
11
57
  if (!result) return;
12
- upsertCondition(selectedField, selectedOperator, result, editingChipId);
13
- resetState(!editingChipId);
14
- return;
58
+ committedValue = result;
15
59
  }
60
+ if (stashAndAdvance(committedValue)) return;
61
+ if (commitPairedSecond(committedValue)) return;
16
62
  const isEditing = !!editingChipId;
17
- upsertCondition(selectedField, selectedOperator, val, editingChipId, isEditing ? void 0 : insertIndex);
63
+ upsertCondition(selectedField, selectedOperator, committedValue, editingChipId, isEditing ? void 0 : insertIndex, void 0, void 0, editingSide);
18
64
  resetState(!isEditing);
19
65
  }, [
20
66
  selectedField,
21
67
  selectedOperator,
22
68
  editingChipId,
69
+ editingSide,
23
70
  dateRange,
24
71
  insertIndex,
25
72
  upsertCondition,
26
- resetState
73
+ resetState,
74
+ stashAndAdvance,
75
+ commitPairedSecond
27
76
  ]);
28
77
  const handleMultiCommit = useCallback((values)=>{
29
78
  if (!selectedField || !selectedOperator || 0 === values.length) return;
@@ -71,29 +120,44 @@ const useValueFlow = ({ editing, selectedField, selectedOperator, insertIndex, u
71
120
  if (!selectedField || !selectedOperator || !customText.trim()) return;
72
121
  const trimmed = customText.trim();
73
122
  const isEditing = !!editingChipId;
123
+ let resolvedValue = trimmed;
124
+ let valueError;
125
+ let dateOrigin;
74
126
  if (isMultiSelectOperator(selectedOperator)) {
75
127
  const { resolved, error } = resolveMultiValues(selectedField, trimmed);
76
- upsertCondition(selectedField, selectedOperator, resolved, editingChipId, isEditing ? void 0 : insertIndex, error ? SEGMENT_VARIANT.value : void 0);
128
+ resolvedValue = resolved;
129
+ valueError = error ? SEGMENT_VARIANT.value : void 0;
77
130
  } else if ('date' === selectedField.type) if (isBetweenOperator(selectedOperator)) {
78
131
  const rangeValue = resolveDateRangeValue(trimmed);
79
- upsertCondition(selectedField, selectedOperator, rangeValue ?? trimmed, editingChipId, isEditing ? void 0 : insertIndex, rangeValue ? void 0 : SEGMENT_VARIANT.value, 'absolute');
132
+ resolvedValue = rangeValue ?? trimmed;
133
+ valueError = rangeValue ? void 0 : SEGMENT_VARIANT.value;
134
+ dateOrigin = 'absolute';
80
135
  } else {
81
- const { error, dateOrigin } = resolveDateValue(trimmed, editingChipId, conditionsRef.current);
82
- upsertCondition(selectedField, selectedOperator, trimmed, editingChipId, isEditing ? void 0 : insertIndex, error ? SEGMENT_VARIANT.value : void 0, dateOrigin);
136
+ const resolved = resolveDateValue(trimmed, editingChipId, conditionsRef.current);
137
+ resolvedValue = trimmed;
138
+ valueError = resolved.error ? SEGMENT_VARIANT.value : void 0;
139
+ dateOrigin = resolved.dateOrigin;
83
140
  }
84
141
  else {
85
142
  const { resolved, error } = resolveSingleValue(selectedField, trimmed);
86
- upsertCondition(selectedField, selectedOperator, resolved, editingChipId, isEditing ? void 0 : insertIndex, error ? SEGMENT_VARIANT.value : void 0);
143
+ resolvedValue = resolved;
144
+ valueError = error ? SEGMENT_VARIANT.value : void 0;
87
145
  }
146
+ if (stashAndAdvance(resolvedValue)) return;
147
+ if (commitPairedSecond(resolvedValue, valueError, dateOrigin)) return;
148
+ upsertCondition(selectedField, selectedOperator, resolvedValue, editingChipId, isEditing ? void 0 : insertIndex, valueError, dateOrigin, editingSide);
88
149
  resetState(!isEditing);
89
150
  }, [
90
151
  selectedField,
91
152
  selectedOperator,
92
153
  editingChipId,
154
+ editingSide,
93
155
  insertIndex,
94
156
  conditionsRef,
95
157
  upsertCondition,
96
- resetState
158
+ resetState,
159
+ stashAndAdvance,
160
+ commitPairedSecond
97
161
  ]);
98
162
  return {
99
163
  handleValueSelect,
@@ -14,6 +14,8 @@ interface UseResetStateDeps {
14
14
  setSelectedField: Dispatch<SetStateAction<FieldMetadata | null>>;
15
15
  setSelectedOperator: Dispatch<SetStateAction<FilterOperator | null>>;
16
16
  setBuildingMultiValue: Dispatch<SetStateAction<string | undefined>>;
17
+ setBuildingSide: (side: 0 | 1) => void;
18
+ setBuildingBase: (base: null) => void;
17
19
  setInsertIndex: Dispatch<SetStateAction<number | null>>;
18
20
  setInsertAfterConnector: Dispatch<SetStateAction<boolean>>;
19
21
  setMenuState: Dispatch<SetStateAction<MenuState>>;
@@ -29,5 +31,5 @@ interface UseResetStateDeps {
29
31
  * vs. genuine outside click). If unifying these, preserve the re-render
30
32
  * refocus case or commit chains break. AS-882.
31
33
  */
32
- export declare const useResetState: ({ editing, dateRange, containerRef, inputRef, resetMenuAnchor, setInputText, setSelectedField, setSelectedOperator, setBuildingMultiValue, setInsertIndex, setInsertAfterConnector, setMenuState, }: UseResetStateDeps) => (continueBuilding?: boolean) => void;
34
+ export declare const useResetState: ({ editing, dateRange, containerRef, inputRef, resetMenuAnchor, setInputText, setSelectedField, setSelectedOperator, setBuildingMultiValue, setBuildingSide, setBuildingBase, setInsertIndex, setInsertAfterConnector, setMenuState, }: UseResetStateDeps) => (continueBuilding?: boolean) => void;
33
35
  export {};
@@ -1,7 +1,7 @@
1
1
  import { useCallback } from "react";
2
2
  import { flushSync } from "react-dom";
3
3
  import { isMenuRelated } from "../../lib/index.js";
4
- const useResetState = ({ editing, dateRange, containerRef, inputRef, resetMenuAnchor, setInputText, setSelectedField, setSelectedOperator, setBuildingMultiValue, setInsertIndex, setInsertAfterConnector, setMenuState })=>{
4
+ const useResetState = ({ editing, dateRange, containerRef, inputRef, resetMenuAnchor, setInputText, setSelectedField, setSelectedOperator, setBuildingMultiValue, setBuildingSide, setBuildingBase, setInsertIndex, setInsertAfterConnector, setMenuState })=>{
5
5
  const resetState = useCallback((continueBuilding = false)=>{
6
6
  const doReset = ()=>{
7
7
  setInputText('');
@@ -10,6 +10,8 @@ const useResetState = ({ editing, dateRange, containerRef, inputRef, resetMenuAn
10
10
  editing.clearEditing();
11
11
  dateRange.reset();
12
12
  setBuildingMultiValue(void 0);
13
+ setBuildingSide(0);
14
+ setBuildingBase(null);
13
15
  setInsertIndex(null);
14
16
  setInsertAfterConnector(false);
15
17
  resetMenuAnchor();
@@ -32,6 +34,8 @@ const useResetState = ({ editing, dateRange, containerRef, inputRef, resetMenuAn
32
34
  setSelectedField,
33
35
  setSelectedOperator,
34
36
  setBuildingMultiValue,
37
+ setBuildingSide,
38
+ setBuildingBase,
35
39
  setInsertIndex,
36
40
  setInsertAfterConnector,
37
41
  setMenuState
@@ -82,7 +82,21 @@ const buildMultiValueChip = (baseChip, condition, field, fields, chipError)=>{
82
82
  }
83
83
  };
84
84
  };
85
- const makeConditionChip = (i, conditions, fields, error)=>{
85
+ const buildPairChip = (condition, field, fields)=>{
86
+ if (!condition.pair || !field?.pairedField) return;
87
+ const pf = field.pairedField;
88
+ const { operator, value, error } = condition.pair;
89
+ const displayValue = operator && isNoValueOperator(operator) ? NO_VALUE_PLACEHOLDER : resolveValueLabel(value, pf, fields) ?? String(value ?? '');
90
+ return {
91
+ attribute: pf.label || pf.name,
92
+ operator: operator ? getOperatorLabel(operator, pf.type || DEFAULT_FIELD_TYPE) : void 0,
93
+ value: displayValue,
94
+ ...error && {
95
+ error
96
+ }
97
+ };
98
+ };
99
+ const makeConditionChipBase = (i, conditions, fields, error)=>{
86
100
  const condition = conditions[i];
87
101
  if (!condition) return makeEmptyChip(i, error);
88
102
  const chipError = condition.error || (error ? true : void 0);
@@ -101,6 +115,17 @@ const makeConditionChip = (i, conditions, fields, error)=>{
101
115
  error: chipError
102
116
  };
103
117
  };
118
+ const makeConditionChip = (i, conditions, fields, error)=>{
119
+ const base = makeConditionChipBase(i, conditions, fields, error);
120
+ const condition = conditions[i];
121
+ if (!condition) return base;
122
+ const field = fields.find((f)=>f.name === condition.field);
123
+ const pair = buildPairChip(condition, field, fields);
124
+ return pair ? {
125
+ ...base,
126
+ pair
127
+ } : base;
128
+ };
104
129
  const buildChips = (conditions, connectors, fields, error)=>{
105
130
  if (0 === conditions.length) return [];
106
131
  const hasMixed = connectors.includes('and') && connectors.includes('or');
@@ -1,15 +1,21 @@
1
- import type { Condition, ExprNode } from '../../types';
1
+ import type { Condition, ExprNode, FieldMetadata } from '../../types';
2
2
  /**
3
3
  * Build an ExprNode from flat conditions + per-gap connectors.
4
4
  * AND has higher precedence than OR: conditions connected by AND
5
5
  * are grouped together, then those groups are joined by OR.
6
+ *
7
+ * Pass `fields` to expand paired conditions into two AND-joined conditions
8
+ * (the serialization contract). Omit it to keep paired conditions intact.
6
9
  */
7
- export declare const buildExpression: (conditions: Condition[], connectors: Array<"and" | "or">) => ExprNode | null;
10
+ export declare const buildExpression: (rawConditions: Condition[], rawConnectors: Array<"and" | "or">, fields?: FieldMetadata[]) => ExprNode | null;
8
11
  /**
9
12
  * Flatten a (possibly nested) ExprNode back to flat conditions + connectors.
10
13
  * Walks the tree depth-first; between siblings inserts the parent's operator.
14
+ *
15
+ * Pass `fields` to re-pair AND-adjacent conditions into paired conditions (the
16
+ * inverse of `buildExpression`'s expansion). Omit it to keep them separate.
11
17
  */
12
- export declare const expressionToConditions: (expr: ExprNode | null) => {
18
+ export declare const expressionToConditions: (expr: ExprNode | null, fields?: FieldMetadata[]) => {
13
19
  conditions: Condition[];
14
20
  connectors: Array<"and" | "or">;
15
21
  };
@@ -1,4 +1,39 @@
1
- const buildExpression = (conditions, connectors)=>{
1
+ const expandPairs = (conditions, connectors, fields)=>{
2
+ const outConditions = [];
3
+ const outConnectors = [];
4
+ conditions.forEach((condition, i)=>{
5
+ if (i > 0) outConnectors.push(connectors[i - 1] ?? 'and');
6
+ const field = fields.find((f)=>f.name === condition.field);
7
+ if (condition.pair && field?.pairedField) {
8
+ const { pair, ...base } = condition;
9
+ outConditions.push(base);
10
+ outConnectors.push('and');
11
+ outConditions.push({
12
+ type: 'condition',
13
+ field: field.pairedField.name,
14
+ ...pair.operator && {
15
+ operator: pair.operator
16
+ },
17
+ value: pair.value,
18
+ ...pair.error && {
19
+ error: pair.error
20
+ },
21
+ ...pair.dateOrigin && {
22
+ dateOrigin: pair.dateOrigin
23
+ }
24
+ });
25
+ } else outConditions.push(condition);
26
+ });
27
+ return {
28
+ conditions: outConditions,
29
+ connectors: outConnectors
30
+ };
31
+ };
32
+ const buildExpression = (rawConditions, rawConnectors, fields)=>{
33
+ const { conditions, connectors } = fields ? expandPairs(rawConditions, rawConnectors, fields) : {
34
+ conditions: rawConditions,
35
+ connectors: rawConnectors
36
+ };
2
37
  if (0 === conditions.length) return null;
3
38
  if (1 === conditions.length) return conditions[0] ?? null;
4
39
  const first = conditions[0];
@@ -27,17 +62,48 @@ const buildExpression = (conditions, connectors)=>{
27
62
  children: andNodes
28
63
  };
29
64
  };
30
- const expressionToConditions = (expr)=>{
65
+ const repairPairs = (conditions, connectors, fields)=>{
66
+ const outConditions = [];
67
+ const outConnectors = [];
68
+ let i = 0;
69
+ while(i < conditions.length){
70
+ const condition = conditions[i];
71
+ if (i > 0) outConnectors.push(connectors[i - 1] ?? 'and');
72
+ const field = fields.find((f)=>f.name === condition.field);
73
+ const next = conditions[i + 1];
74
+ const canMerge = field?.pairedField && !condition.pair && 'and' === connectors[i] && next && next.field === field.pairedField.name;
75
+ if (canMerge && next) {
76
+ outConditions.push({
77
+ ...condition,
78
+ pair: {
79
+ ...next.operator && {
80
+ operator: next.operator
81
+ },
82
+ value: next.value,
83
+ ...next.error && {
84
+ error: next.error
85
+ },
86
+ ...next.dateOrigin && {
87
+ dateOrigin: next.dateOrigin
88
+ }
89
+ }
90
+ });
91
+ i += 2;
92
+ } else {
93
+ outConditions.push(condition);
94
+ i += 1;
95
+ }
96
+ }
97
+ return {
98
+ conditions: outConditions,
99
+ connectors: outConnectors
100
+ };
101
+ };
102
+ const expressionToConditions = (expr, fields)=>{
31
103
  if (!expr) return {
32
104
  conditions: [],
33
105
  connectors: []
34
106
  };
35
- if ('condition' === expr.type) return {
36
- conditions: [
37
- expr
38
- ],
39
- connectors: []
40
- };
41
107
  const conditions = [];
42
108
  const connectors = [];
43
109
  const walk = (node)=>{
@@ -48,7 +114,7 @@ const expressionToConditions = (expr)=>{
48
114
  });
49
115
  };
50
116
  walk(expr);
51
- return {
117
+ return fields ? repairPairs(conditions, connectors, fields) : {
52
118
  conditions,
53
119
  connectors
54
120
  };
@@ -10,7 +10,7 @@ export declare const useFilterInputExpression: ({ fields, value, onChange, error
10
10
  conditions: Condition[];
11
11
  connectors: ("and" | "or")[];
12
12
  chips: import("../..").FilterInputChipData[];
13
- upsertCondition: (field: FieldMetadata, operator: FilterOperator | undefined, val: string | number | boolean | null | Array<string | number | boolean>, editingChipId?: string | null, atIndex?: number, error?: ChipErrorSegment, dateOrigin?: "relative" | "absolute") => void;
13
+ upsertCondition: (field: FieldMetadata, operator: FilterOperator | undefined, val: string | number | boolean | null | Array<string | number | boolean>, editingChipId?: string | null, atIndex?: number, error?: ChipErrorSegment, dateOrigin?: "relative" | "absolute", side?: 0 | 1) => void;
14
14
  removeCondition: (chipId: string) => void;
15
15
  removeConditionAtIndex: (idx: number) => void;
16
16
  clearAll: () => void;