@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
@@ -5,6 +5,8 @@ export interface BuildingChipData {
5
5
  attribute: string;
6
6
  operator?: string;
7
7
  value?: string;
8
+ /** Paired second triplet shown while building a two-step field's second value. */
9
+ pair?: FilterInputChipData['pair'];
8
10
  }
9
11
  export interface FilterInputContextValue {
10
12
  chips: FilterInputChipData[];
@@ -26,6 +28,8 @@ export interface FilterInputContextValue {
26
28
  onAreaClick: () => void;
27
29
  onGapClick: (conditionIndex: number, afterConnector: boolean) => void;
28
30
  onChipClick: (chipId: string, segment: ChipSegment, anchorEl: HTMLElement) => void;
31
+ /** Click on an editable paired (second-triplet) segment — edits the pair. */
32
+ onPairChipClick: (chipId: string, segment: ChipSegment, anchorEl: HTMLElement) => void;
29
33
  /** Click on a building-chip segment — reopens its menu and enters inline-edit. */
30
34
  onBuildingChipClick: (segment: ChipSegment, anchorEl: HTMLElement) => void;
31
35
  /** Move inline-edit to another segment of the edited chip (Backspace cascade). */
@@ -37,6 +41,8 @@ export interface FilterInputContextValue {
37
41
  onClear: () => void;
38
42
  editingChipId: string | null;
39
43
  editingSegment: ChipSegment | null;
44
+ /** Which triplet is being edited: 0 = base, 1 = paired second. */
45
+ editingSide: 0 | 1;
40
46
  segmentFilterText: string;
41
47
  onSegmentFilterChange: (text: string) => void;
42
48
  onCancelSegmentEdit: () => void;
@@ -11,6 +11,7 @@ interface AutocompleteForContext {
11
11
  handleInputClick: () => void;
12
12
  handleAreaClick: () => void;
13
13
  handleChipClick: (chipId: string, segment: ChipSegment, anchorEl: HTMLElement) => void;
14
+ handlePairChipClick: (chipId: string, segment: ChipSegment, anchorEl: HTMLElement) => void;
14
15
  handleBuildingChipClick: (segment: ChipSegment, anchorEl: HTMLElement) => void;
15
16
  switchEditSegment: (targetSegment: ChipSegment) => boolean;
16
17
  removeEditingChip: () => void;
@@ -22,6 +23,7 @@ interface AutocompleteForContext {
22
23
  insertAfterConnector: boolean;
23
24
  editingChipId: string | null;
24
25
  editingSegment: ChipSegment | null;
26
+ editingSide: 0 | 1;
25
27
  segmentFilterText: string;
26
28
  handleSegmentFilterChange: (text: string) => void;
27
29
  cancelSegmentEdit: () => void;
@@ -17,6 +17,7 @@ const useFilterInputContextValue = ({ chips, autocomplete, buildingChipRef, inpu
17
17
  onInputClick: autocomplete.handleInputClick,
18
18
  onAreaClick: autocomplete.handleAreaClick,
19
19
  onChipClick: autocomplete.handleChipClick,
20
+ onPairChipClick: autocomplete.handlePairChipClick,
20
21
  onBuildingChipClick: autocomplete.handleBuildingChipClick,
21
22
  onSwitchEditSegment: autocomplete.switchEditSegment,
22
23
  onRemoveEditingChip: autocomplete.removeEditingChip,
@@ -25,6 +26,7 @@ const useFilterInputContextValue = ({ chips, autocomplete, buildingChipRef, inpu
25
26
  onClear: autocomplete.handleClear,
26
27
  editingChipId: autocomplete.editingChipId,
27
28
  editingSegment: autocomplete.editingSegment,
29
+ editingSide: autocomplete.editingSide,
28
30
  segmentFilterText: autocomplete.segmentFilterText,
29
31
  onSegmentFilterChange: autocomplete.handleSegmentFilterChange,
30
32
  onCancelSegmentEdit: autocomplete.cancelSegmentEdit,
@@ -50,6 +52,7 @@ const useFilterInputContextValue = ({ chips, autocomplete, buildingChipRef, inpu
50
52
  autocomplete.handleInputClick,
51
53
  autocomplete.handleAreaClick,
52
54
  autocomplete.handleChipClick,
55
+ autocomplete.handlePairChipClick,
53
56
  autocomplete.handleBuildingChipClick,
54
57
  autocomplete.switchEditSegment,
55
58
  autocomplete.removeEditingChip,
@@ -58,6 +61,7 @@ const useFilterInputContextValue = ({ chips, autocomplete, buildingChipRef, inpu
58
61
  autocomplete.handleClear,
59
62
  autocomplete.editingChipId,
60
63
  autocomplete.editingSegment,
64
+ autocomplete.editingSide,
61
65
  autocomplete.segmentFilterText,
62
66
  autocomplete.handleSegmentFilterChange,
63
67
  autocomplete.cancelSegmentEdit,
@@ -1,10 +1,19 @@
1
1
  import { SEGMENT_VARIANT } from "../FilterInputField/FilterInputChip/index.js";
2
- import { getFieldValues, hasStaticAllowlist, isValidFieldValue } from "../lib/index.js";
2
+ import { getFieldValues, hasStaticAllowlist, isNoValueOperator, isValidFieldValue } from "../lib/index.js";
3
+ const isPairValueMissing = (condition)=>{
4
+ const pair = condition.pair;
5
+ if (!pair) return true;
6
+ if (pair.operator && isNoValueOperator(pair.operator)) return false;
7
+ const value = pair.value;
8
+ return null == value || '' === value || Array.isArray(value) && 0 === value.length;
9
+ };
3
10
  const parseFilterInputErrors = (conditions, fields)=>{
4
11
  const errors = [];
5
12
  for (const condition of conditions){
6
- if (!condition.error) continue;
7
13
  const field = fields.find((f)=>f.name === condition.field);
14
+ const baseIsNoValue = null != condition.operator && isNoValueOperator(condition.operator);
15
+ if (field?.pairedField && !baseIsNoValue && isPairValueMissing(condition)) errors.push(`${field.pairedField.label} is required`);
16
+ if (!condition.error) continue;
8
17
  const label = field?.label || condition.field;
9
18
  switch(condition.error){
10
19
  case SEGMENT_VARIANT.attribute:
@@ -6,7 +6,7 @@ import { FilterInputChip } from "./FilterInputChip/FilterInputChip.js";
6
6
  import { FilterInputConnectorChip } from "./FilterInputConnectorChip/index.js";
7
7
  import { InsertionGap } from "./InsertionGap/index.js";
8
8
  const ChipsWithGaps = ({ chips, hideLeadingGap, hideTrailingGap, onChipClick, onConnectorChange, onChipRemove, onGapClick })=>{
9
- const { registerChipRef } = useFilterInputContext();
9
+ const { registerChipRef, onPairChipClick } = useFilterInputContext();
10
10
  const chipRef = useCallback((id)=>(el)=>registerChipRef(id, el), [
11
11
  registerChipRef
12
12
  ]);
@@ -29,8 +29,10 @@ const ChipsWithGaps = ({ chips, hideLeadingGap, hideTrailingGap, onChipClick, on
29
29
  valueSeparator: chip.valueSeparator,
30
30
  errorValueIndices: chip.errorValueIndices,
31
31
  disabled: chip.disabled,
32
+ pair: chip.pair,
32
33
  onRemove: chip.disabled ? void 0 : ()=>onChipRemove(chip.id),
33
- onSegmentClick: chip.disabled ? void 0 : (segment, anchorEl)=>onChipClick(chip.id, segment, anchorEl)
34
+ onSegmentClick: chip.disabled ? void 0 : (segment, anchorEl)=>onChipClick(chip.id, segment, anchorEl),
35
+ onPairSegmentClick: chip.disabled ? void 0 : (segment, anchorEl)=>onPairChipClick(chip.id, segment, anchorEl)
34
36
  })
35
37
  }, chip.id));
36
38
  else if (isConnector) {
@@ -1,7 +1,9 @@
1
1
  import type { FC, HTMLAttributes, Ref } from 'react';
2
- import type { ChipErrorSegment } from '../../types';
2
+ import type { ChipErrorSegment, FilterInputChipData } from '../../types';
3
3
  import { type SegmentVariant } from './segmentVariant';
4
4
  export type ChipSegment = SegmentVariant;
5
+ /** Editable segments of the paired (second) triplet. The paired attribute is fixed. */
6
+ export type PairSegment = Extract<SegmentVariant, 'operator' | 'value'>;
5
7
  export interface FilterInputChipProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
6
8
  ref?: Ref<HTMLDivElement>;
7
9
  chipId?: string;
@@ -15,7 +17,11 @@ export interface FilterInputChipProps extends Omit<HTMLAttributes<HTMLDivElement
15
17
  building?: boolean;
16
18
  /** When true, the chip cannot be edited or removed (dimmed appearance) */
17
19
  disabled?: boolean;
20
+ /** Second paired triplet (two-step fields). The paired attribute is fixed. */
21
+ pair?: FilterInputChipData['pair'];
18
22
  onRemove?: () => void;
19
23
  onSegmentClick?: (segment: ChipSegment, anchorEl: HTMLElement) => void;
24
+ /** Click on an editable paired segment (operator/value of the second triplet). */
25
+ onPairSegmentClick?: (segment: PairSegment, anchorEl: HTMLElement) => void;
20
26
  }
21
27
  export declare const FilterInputChip: FC<FilterInputChipProps>;
@@ -1,13 +1,14 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useRef } from "react";
3
3
  import { cn } from "../../../../utils/cn.js";
4
4
  import { ChipSearchInput } from "./ChipSearchInput.js";
5
5
  import { chipVariants } from "./classes.js";
6
6
  import { useEditingContext } from "./context/EditingContext.js";
7
7
  import { FilterInputRemoveButton } from "./FilterInputRemoveButton.js";
8
+ import { PairSeparator } from "./PairSeparator.js";
8
9
  import { Segment } from "./Segment.js";
9
10
  import { SEGMENT_VARIANT } from "./segmentVariant.js";
10
- const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = false, valueParts, valueSeparator, errorValueIndices, building = false, disabled = false, onRemove, onSegmentClick, className, ...props })=>{
11
+ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = false, valueParts, valueSeparator, errorValueIndices, building = false, disabled = false, pair, onRemove, onSegmentClick, onPairSegmentClick, className, ...props })=>{
11
12
  const interactive = !disabled;
12
13
  const hasError = !!error;
13
14
  const internalRef = useRef(null);
@@ -25,16 +26,28 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
25
26
  onSegmentClick,
26
27
  activeSegment
27
28
  ]);
29
+ const handlePairSegmentClick = useCallback((segment, e)=>{
30
+ if (!onPairSegmentClick) return;
31
+ e.stopPropagation();
32
+ const anchorEl = internalRef.current;
33
+ if (!anchorEl) return;
34
+ onPairSegmentClick(segment, anchorEl);
35
+ }, [
36
+ onPairSegmentClick
37
+ ]);
28
38
  const handleSegmentMouseDown = useCallback((e)=>{
29
39
  e.preventDefault();
30
40
  }, []);
31
- const segmentEditProps = (segment)=>isEditingThisChip && editing.editingSegment === segment ? {
41
+ const editingSide = editing?.editingSide ?? 0;
42
+ const segmentEditProps = (segment, side = 0)=>isEditingThisChip && editing.editingSegment === segment && editingSide === side ? {
32
43
  editing: true,
33
44
  editText: editing.segmentFilterText,
34
45
  onEditChange: editing.onSegmentFilterChange,
35
46
  onEditKeyDown: editing.onSegmentEditKeyDown,
36
47
  onEditBlur: editing.onSegmentEditBlur
37
48
  } : {};
49
+ const baseActiveSegment = 0 === editingSide ? activeSegment : null;
50
+ const pairActiveSegment = 1 === editingSide ? activeSegment : null;
38
51
  const setRefs = useCallback((node)=>{
39
52
  internalRef.current = node;
40
53
  if ('function' == typeof ref) ref(node);
@@ -65,7 +78,7 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
65
78
  ...segmentEditProps(SEGMENT_VARIANT.attribute),
66
79
  children: attribute
67
80
  }),
68
- (operator || activeSegment === SEGMENT_VARIANT.operator) && /*#__PURE__*/ jsx(Segment, {
81
+ (operator || baseActiveSegment === SEGMENT_VARIANT.operator) && /*#__PURE__*/ jsx(Segment, {
69
82
  variant: SEGMENT_VARIANT.operator,
70
83
  className: "shrink-0",
71
84
  onClick: interactive ? (e)=>handleSegmentClick(SEGMENT_VARIANT.operator, e) : void 0,
@@ -73,10 +86,10 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
73
86
  ...segmentEditProps(SEGMENT_VARIANT.operator),
74
87
  children: operator ?? ''
75
88
  }),
76
- (value || activeSegment === SEGMENT_VARIANT.value) && /*#__PURE__*/ jsx(Segment, {
89
+ (value || baseActiveSegment === SEGMENT_VARIANT.value) && /*#__PURE__*/ jsx(Segment, {
77
90
  variant: SEGMENT_VARIANT.value,
78
91
  className: "min-w-0",
79
- error: activeSegment !== SEGMENT_VARIANT.value && (true === error || error === SEGMENT_VARIANT.value),
92
+ error: baseActiveSegment !== SEGMENT_VARIANT.value && (true === error || error === SEGMENT_VARIANT.value),
80
93
  valueParts: valueParts,
81
94
  valueSeparator: valueSeparator,
82
95
  errorValueIndices: errorValueIndices,
@@ -85,6 +98,31 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
85
98
  ...segmentEditProps(SEGMENT_VARIANT.value),
86
99
  children: value ?? ''
87
100
  }),
101
+ pair && /*#__PURE__*/ jsxs(Fragment, {
102
+ children: [
103
+ /*#__PURE__*/ jsx(PairSeparator, {}),
104
+ /*#__PURE__*/ jsx(Segment, {
105
+ variant: SEGMENT_VARIANT.attribute,
106
+ className: "shrink-0",
107
+ children: pair.attribute
108
+ }),
109
+ (pair.operator || pairActiveSegment === SEGMENT_VARIANT.operator) && /*#__PURE__*/ jsx(Segment, {
110
+ variant: SEGMENT_VARIANT.operator,
111
+ className: "shrink-0",
112
+ onClick: interactive ? (e)=>handlePairSegmentClick('operator', e) : void 0,
113
+ ...segmentEditProps(SEGMENT_VARIANT.operator, 1),
114
+ children: pair.operator ?? ''
115
+ }),
116
+ (null != pair.value || pairActiveSegment === SEGMENT_VARIANT.value) && /*#__PURE__*/ jsx(Segment, {
117
+ variant: SEGMENT_VARIANT.value,
118
+ className: "min-w-0",
119
+ error: pairActiveSegment !== SEGMENT_VARIANT.value && (true === pair.error || pair.error === SEGMENT_VARIANT.value),
120
+ onClick: interactive ? (e)=>handlePairSegmentClick('value', e) : void 0,
121
+ ...segmentEditProps(SEGMENT_VARIANT.value, 1),
122
+ children: pair.value ?? ''
123
+ })
124
+ ]
125
+ }),
88
126
  building && !isEditingThisChip && /*#__PURE__*/ jsx(ChipSearchInput, {}),
89
127
  onRemove && !disabled && /*#__PURE__*/ jsx(FilterInputRemoveButton, {
90
128
  error: hasError,
@@ -0,0 +1,3 @@
1
+ import type { FC } from 'react';
2
+ /** Decorative ";" between the two triplets of a paired chip. Non-interactive. */
3
+ export declare const PairSeparator: FC;
@@ -0,0 +1,16 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { segmentContainer, segmentTextVariants } from "./classes.js";
3
+ import { PAIR_SEPARATOR_SLOT } from "./segmentVariant.js";
4
+ const PairSeparator = ()=>/*#__PURE__*/ jsx("div", {
5
+ className: `${segmentContainer} shrink-0`,
6
+ "data-slot": PAIR_SEPARATOR_SLOT,
7
+ "aria-hidden": true,
8
+ children: /*#__PURE__*/ jsx("p", {
9
+ className: segmentTextVariants({
10
+ variant: 'operator'
11
+ }),
12
+ children: ";"
13
+ })
14
+ });
15
+ PairSeparator.displayName = 'PairSeparator';
16
+ export { PairSeparator };
@@ -3,6 +3,8 @@ import type { ChipSegment } from '../FilterInputChip';
3
3
  export interface EditingContextValue {
4
4
  editingChipId: string | null;
5
5
  editingSegment: ChipSegment | null;
6
+ /** Which triplet is being edited: 0 = base, 1 = paired second. */
7
+ editingSide: 0 | 1;
6
8
  segmentFilterText: string;
7
9
  onSegmentFilterChange: (text: string) => void;
8
10
  onSegmentEditKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void;
@@ -1,6 +1,7 @@
1
1
  export { type ConnectorVariant, FilterInputConnectorChip, type FilterInputConnectorChipProps, } from '../FilterInputConnectorChip';
2
2
  export { type EditingContextValue, EditingProvider, useEditingContext, } from './context/EditingContext';
3
- export { type ChipSegment, FilterInputChip, type FilterInputChipProps } from './FilterInputChip';
3
+ export { type ChipSegment, FilterInputChip, type FilterInputChipProps, type PairSegment, } from './FilterInputChip';
4
4
  export { FilterInputRemoveButton } from './FilterInputRemoveButton';
5
+ export { PairSeparator } from './PairSeparator';
5
6
  export { Segment, type SegmentProps } from './Segment';
6
- export { SEGMENT_VARIANT, type SegmentVariant } from './segmentVariant';
7
+ export { PAIR_SEPARATOR_SLOT, SEGMENT_VARIANT, type SegmentVariant } from './segmentVariant';
@@ -2,6 +2,7 @@ import { FilterInputConnectorChip } from "../FilterInputConnectorChip/index.js";
2
2
  import { EditingProvider, useEditingContext } from "./context/EditingContext.js";
3
3
  import { FilterInputChip } from "./FilterInputChip.js";
4
4
  import { FilterInputRemoveButton } from "./FilterInputRemoveButton.js";
5
+ import { PairSeparator } from "./PairSeparator.js";
5
6
  import { Segment } from "./Segment.js";
6
- import { SEGMENT_VARIANT } from "./segmentVariant.js";
7
- export { EditingProvider, FilterInputChip, FilterInputConnectorChip, FilterInputRemoveButton, SEGMENT_VARIANT, Segment, useEditingContext };
7
+ import { PAIR_SEPARATOR_SLOT, SEGMENT_VARIANT } from "./segmentVariant.js";
8
+ export { EditingProvider, FilterInputChip, FilterInputConnectorChip, FilterInputRemoveButton, PAIR_SEPARATOR_SLOT, PairSeparator, SEGMENT_VARIANT, Segment, useEditingContext };
@@ -4,3 +4,5 @@ export declare const SEGMENT_VARIANT: {
4
4
  readonly value: "value";
5
5
  };
6
6
  export type SegmentVariant = (typeof SEGMENT_VARIANT)[keyof typeof SEGMENT_VARIANT];
7
+ /** data-slot for the decorative ";" between paired triplets (non-interactive). */
8
+ export declare const PAIR_SEPARATOR_SLOT = "segment-separator";
@@ -3,4 +3,5 @@ const SEGMENT_VARIANT = {
3
3
  operator: 'operator',
4
4
  value: 'value'
5
5
  };
6
- export { SEGMENT_VARIANT };
6
+ const PAIR_SEPARATOR_SLOT = 'segment-separator';
7
+ export { PAIR_SEPARATOR_SLOT, SEGMENT_VARIANT };
@@ -13,7 +13,7 @@ import { useChipsSplitting } from "./hooks/useChipsSplitting.js";
13
13
  import { useExpandCollapse } from "./hooks/useExpandCollapse.js";
14
14
  import { useSegmentEditKeyboard } from "./hooks/useSegmentEditKeyboard.js";
15
15
  const FilterInputField = ({ className, ...props })=>{
16
- const { chips, buildingChipData, buildingChipRef, insertIndex, insertAfterConnector, error, onAreaClick, onGapClick, onChipClick, onBuildingChipClick, onConnectorChange, onChipRemove, editingChipId, editingSegment, segmentFilterText, onSegmentFilterChange, onCancelSegmentEdit, onCustomValueCommit, onCustomAttributeCommit, onCustomOperatorCommit, onSwitchEditSegment, onRemoveEditingChip, menuRef } = useFilterInputContext();
16
+ const { chips, buildingChipData, buildingChipRef, insertIndex, insertAfterConnector, error, onAreaClick, onGapClick, onChipClick, onBuildingChipClick, onConnectorChange, onChipRemove, editingChipId, editingSegment, editingSide, segmentFilterText, onSegmentFilterChange, onCancelSegmentEdit, onCustomValueCommit, onCustomAttributeCommit, onCustomOperatorCommit, onSwitchEditSegment, onRemoveEditingChip, menuRef } = useFilterInputContext();
17
17
  const hasContent = chips.length > 0 || null != buildingChipData;
18
18
  const { isExpanded, isOverflowing, innerRef, toggleExpand, multiRow } = useExpandCollapse();
19
19
  const { chipsBefore, chipsAfter, hideTrailingGap, hideLeadingGap } = useChipsSplitting(chips, insertIndex, insertAfterConnector);
@@ -40,6 +40,7 @@ const FilterInputField = ({ className, ...props })=>{
40
40
  return /*#__PURE__*/ jsx(EditingProvider, {
41
41
  editingChipId: editingChipId,
42
42
  editingSegment: editingSegment,
43
+ editingSide: editingSide,
43
44
  segmentFilterText: segmentFilterText,
44
45
  onSegmentFilterChange: onSegmentFilterChange,
45
46
  onSegmentEditKeyDown: handleSegmentEditKeyDown,
@@ -98,6 +99,7 @@ const FilterInputField = ({ className, ...props })=>{
98
99
  attribute: buildingChipData.attribute,
99
100
  operator: buildingChipData.operator,
100
101
  value: buildingChipData.value,
102
+ pair: buildingChipData.pair,
101
103
  onSegmentClick: onBuildingChipClick,
102
104
  className: "mx-4"
103
105
  }) : /*#__PURE__*/ jsx(FilterInputSearch, {
@@ -1,5 +1,6 @@
1
1
  import type { BuildingChipData } from '../../../FilterInputContext/types';
2
2
  import type { Condition, FieldMetadata, FilterOperator } from '../../../types';
3
+ import type { BuildingBase } from '../useAutocompleteState';
3
4
  interface DeriveOptions {
4
5
  editingChipId: string | null;
5
6
  selectedField: FieldMetadata | null;
@@ -9,6 +10,12 @@ interface DeriveOptions {
9
10
  dateRangeFromValue: string | null | undefined;
10
11
  /** Segment text when inline-editing a value. */
11
12
  segmentFilterText?: string;
13
+ /** Which triplet is being built: 0 = base, 1 = paired second. */
14
+ buildingSide?: 0 | 1;
15
+ /** Stashed base triplet while building the paired second value. */
16
+ buildingBase?: BuildingBase | null;
17
+ /** Which triplet is being edited: 0 = base, 1 = paired second. */
18
+ editingSide?: 0 | 1;
12
19
  }
13
20
  export interface DerivedAutocompleteValues {
14
21
  isBuilding: boolean;
@@ -18,5 +25,5 @@ export interface DerivedAutocompleteValues {
18
25
  /** [from, to] ISO strings when editing a "between" date condition */
19
26
  editingDateRange: [string, string] | undefined;
20
27
  }
21
- export declare const deriveAutocompleteValues: ({ editingChipId, selectedField, selectedOperator, conditions, buildingMultiValue, dateRangeFromValue, segmentFilterText, }: DeriveOptions) => DerivedAutocompleteValues;
28
+ export declare const deriveAutocompleteValues: ({ editingChipId, selectedField, selectedOperator, conditions, buildingMultiValue, dateRangeFromValue, segmentFilterText, buildingSide, buildingBase, editingSide, }: DeriveOptions) => DerivedAutocompleteValues;
22
29
  export {};
@@ -1,12 +1,22 @@
1
1
  import { NO_VALUE_PLACEHOLDER, chipIdToConditionIndex, getDateDisplayLabel, getFieldValues, getOperatorLabel, hasStaticAllowlist, isMultiSelectOperator, isNoValueOperator } from "../../../lib/index.js";
2
+ const displayBaseValue = (value, field)=>{
3
+ const options = getFieldValues(field);
4
+ const resolve = (v)=>options.find((opt)=>String(opt.value) === String(v))?.label ?? String(v ?? '');
5
+ return Array.isArray(value) ? value.map(resolve).join(', ') : resolve(value);
6
+ };
2
7
  const getEditingCondition = (editingChipId, conditions)=>{
3
8
  if (!editingChipId) return null;
4
9
  const idx = chipIdToConditionIndex(editingChipId);
5
10
  return null !== idx ? conditions[idx] ?? null : null;
6
11
  };
7
- const deriveAutocompleteValues = ({ editingChipId, selectedField, selectedOperator, conditions, buildingMultiValue, dateRangeFromValue, segmentFilterText })=>{
12
+ const deriveAutocompleteValues = ({ editingChipId, selectedField, selectedOperator, conditions, buildingMultiValue, dateRangeFromValue, segmentFilterText, buildingSide, buildingBase, editingSide })=>{
8
13
  const isBuilding = null !== selectedField && !editingChipId;
9
- const editingCondition = getEditingCondition(editingChipId, conditions);
14
+ const rawEditingCondition = getEditingCondition(editingChipId, conditions);
15
+ const editingCondition = rawEditingCondition && 1 === editingSide && rawEditingCondition.pair ? {
16
+ ...rawEditingCondition,
17
+ value: rawEditingCondition.pair.value,
18
+ dateOrigin: rawEditingCondition.pair.dateOrigin
19
+ } : rawEditingCondition;
10
20
  const editingMultiValues = (()=>{
11
21
  if (!editingCondition || !selectedOperator || !isMultiSelectOperator(selectedOperator)) return [];
12
22
  const values = Array.isArray(editingCondition.value) ? editingCondition.value : null != editingCondition.value ? [
@@ -34,7 +44,16 @@ const deriveAutocompleteValues = ({ editingChipId, selectedField, selectedOperat
34
44
  if (dateRangeFromValue && 'between' === selectedOperator) return `${getDateDisplayLabel(dateRangeFromValue)} – ...`;
35
45
  if (selectedOperator && isNoValueOperator(selectedOperator)) return NO_VALUE_PLACEHOLDER;
36
46
  })();
37
- const buildingChipData = isBuilding ? {
47
+ const buildingChipData = isBuilding ? 1 === buildingSide && buildingBase ? {
48
+ attribute: buildingBase.field.label,
49
+ operator: buildingBase.operator ? getOperatorLabel(buildingBase.operator, buildingBase.field.type) : void 0,
50
+ value: displayBaseValue(buildingBase.value, buildingBase.field),
51
+ pair: {
52
+ attribute: selectedField.label,
53
+ operator: selectedOperator ? getOperatorLabel(selectedOperator, selectedField.type) : void 0,
54
+ value: buildingValue
55
+ }
56
+ } : {
38
57
  attribute: selectedField.label,
39
58
  operator: selectedOperator ? getOperatorLabel(selectedOperator, selectedField.type) : void 0,
40
59
  value: buildingValue
@@ -2,6 +2,12 @@ import type { Condition, FieldMetadata, FilterOperator, MenuState } from '../../
2
2
  interface UseAutocompleteStateOptions {
3
3
  conditions: Condition[];
4
4
  }
5
+ /** Base triplet stashed while building a paired chip's second value. */
6
+ export interface BuildingBase {
7
+ field: FieldMetadata;
8
+ operator: FilterOperator | undefined;
9
+ value: string | number | boolean | null | Array<string | number | boolean>;
10
+ }
5
11
  /**
6
12
  * Owns the useState/useRef surface of the autocomplete hook. Refs mirror
7
13
  * state so sub-hooks read synchronously without recreating callbacks.
@@ -19,6 +25,10 @@ export declare const useAutocompleteState: ({ conditions }: UseAutocompleteState
19
25
  setIsFocused: import("react").Dispatch<import("react").SetStateAction<boolean>>;
20
26
  buildingMultiValue: string | undefined;
21
27
  setBuildingMultiValue: import("react").Dispatch<import("react").SetStateAction<string | undefined>>;
28
+ buildingSide: 0 | 1;
29
+ setBuildingSide: import("react").Dispatch<import("react").SetStateAction<0 | 1>>;
30
+ buildingBase: BuildingBase | null;
31
+ setBuildingBase: import("react").Dispatch<import("react").SetStateAction<BuildingBase | null>>;
22
32
  insertIndex: number | null;
23
33
  setInsertIndex: import("react").Dispatch<import("react").SetStateAction<number | null>>;
24
34
  insertAfterConnector: boolean;
@@ -6,6 +6,8 @@ const useAutocompleteState = ({ conditions })=>{
6
6
  const [selectedOperator, setSelectedOperator] = useState(null);
7
7
  const [isFocused, setIsFocused] = useState(false);
8
8
  const [buildingMultiValue, setBuildingMultiValue] = useState(void 0);
9
+ const [buildingSide, setBuildingSide] = useState(0);
10
+ const [buildingBase, setBuildingBase] = useState(null);
9
11
  const [insertIndex, setInsertIndex] = useState(null);
10
12
  const [insertAfterConnector, setInsertAfterConnector] = useState(false);
11
13
  const effectiveInsertIndex = insertIndex ?? conditions.length;
@@ -36,6 +38,10 @@ const useAutocompleteState = ({ conditions })=>{
36
38
  setIsFocused,
37
39
  buildingMultiValue,
38
40
  setBuildingMultiValue,
41
+ buildingSide,
42
+ setBuildingSide,
43
+ buildingBase,
44
+ setBuildingBase,
39
45
  insertIndex,
40
46
  setInsertIndex,
41
47
  insertAfterConnector,
@@ -17,6 +17,7 @@ interface UseChipEditingOptions {
17
17
  export declare const useChipEditing: ({ conditions, chips, fields, setMenuAnchor, setSelectedField, setSelectedOperator, setMenuState, upsertCondition, }: UseChipEditingOptions) => {
18
18
  editingChipId: string | null;
19
19
  editingSegment: import("../../FilterInputField/FilterInputChip").SegmentVariant | null;
20
+ editingSide: 0 | 1;
20
21
  isBuildingEdit: boolean;
21
22
  setEditingSegment: import("react").Dispatch<import("react").SetStateAction<import("../../FilterInputField/FilterInputChip").SegmentVariant | null>>;
22
23
  segmentFilterText: string;
@@ -24,6 +25,7 @@ export declare const useChipEditing: ({ conditions, chips, fields, setMenuAnchor
24
25
  setSegmentFilterText: (text: string) => void;
25
26
  resetSegmentTyping: () => void;
26
27
  handleChipClick: (chipId: string, segment: ChipSegment, anchorEl: HTMLElement) => void;
28
+ handlePairChipClick: (chipId: string, segment: ChipSegment, anchorEl: HTMLElement) => void;
27
29
  startBuildingEdit: (segment: ChipSegment, currentText: string) => void;
28
30
  switchEditSegment: (segment: ChipSegment, currentText: string) => void;
29
31
  clearEditing: () => void;
@@ -15,6 +15,7 @@ const getFirstIncompleteSegment = (condition, fields)=>{
15
15
  const useChipEditing = ({ conditions, chips, fields, setMenuAnchor, setSelectedField, setSelectedOperator, setMenuState, upsertCondition })=>{
16
16
  const [editingChipId, setEditingChipId] = useState(null);
17
17
  const [editingSegment, setEditingSegment] = useState(null);
18
+ const [editingSide, setEditingSide] = useState(0);
18
19
  const [segmentFilterText, setSegmentFilterText] = useState('');
19
20
  const [userHasTyped, setUserHasTyped] = useState(false);
20
21
  const conditionsRef = useRef(conditions);
@@ -49,6 +50,7 @@ const useChipEditing = ({ conditions, chips, fields, setMenuAnchor, setSelectedF
49
50
  };
50
51
  setSegmentFilterText(segmentText[targetSegment]);
51
52
  setUserHasTyped(false);
53
+ setEditingSide(0);
52
54
  setMenuState(SEGMENT_TO_MENU[targetSegment]);
53
55
  }, [
54
56
  setMenuAnchor,
@@ -57,20 +59,45 @@ const useChipEditing = ({ conditions, chips, fields, setMenuAnchor, setSelectedF
57
59
  setMenuState,
58
60
  upsertCondition
59
61
  ]);
62
+ const handlePairChipClick = useCallback((chipId, segment, anchorEl)=>{
63
+ const condition = getConditionByChipId(chipId, conditionsRef.current);
64
+ const field = condition && fieldsRef.current.find((f)=>f.name === condition.field);
65
+ const pairedField = field ? field.pairedField : void 0;
66
+ const chip = chipsRef.current.find((c)=>c.id === chipId);
67
+ if (!condition?.pair || !pairedField || !chip?.pair) return;
68
+ setMenuAnchor(anchorEl);
69
+ setEditingChipId(chipId);
70
+ setEditingSide(1);
71
+ setSelectedField(pairedField);
72
+ const rawOperator = getOperatorFromLabel(chip.pair.operator || '', pairedField.type) ?? condition.pair.operator ?? null;
73
+ setSelectedOperator(segment === SEGMENT_VARIANT.value ? rawOperator : null);
74
+ setEditingSegment(segment);
75
+ setSegmentFilterText(segment === SEGMENT_VARIANT.operator ? chip.pair.operator ?? '' : chip.pair.value ?? '');
76
+ setUserHasTyped(false);
77
+ setMenuState(SEGMENT_TO_MENU[segment]);
78
+ }, [
79
+ setMenuAnchor,
80
+ setSelectedField,
81
+ setSelectedOperator,
82
+ setMenuState
83
+ ]);
60
84
  const clearEditing = useCallback(()=>{
61
85
  setEditingChipId(null);
62
86
  setEditingSegment(null);
87
+ setEditingSide(0);
63
88
  setSegmentFilterText('');
64
89
  setUserHasTyped(false);
65
90
  }, []);
66
91
  const startBuildingEdit = useCallback((segment, currentText)=>{
67
92
  setEditingChipId(null);
68
93
  setEditingSegment(segment);
94
+ setEditingSide(0);
69
95
  setSegmentFilterText(currentText);
70
96
  setUserHasTyped(false);
71
97
  }, []);
72
98
  const switchEditSegment = useCallback((segment, currentText)=>{
73
99
  setEditingSegment(segment);
100
+ setEditingSide(0);
74
101
  setSegmentFilterText(currentText);
75
102
  setUserHasTyped(false);
76
103
  }, []);
@@ -87,6 +114,7 @@ const useChipEditing = ({ conditions, chips, fields, setMenuAnchor, setSelectedF
87
114
  return useMemo(()=>({
88
115
  editingChipId,
89
116
  editingSegment,
117
+ editingSide,
90
118
  isBuildingEdit,
91
119
  setEditingSegment,
92
120
  segmentFilterText: segmentDisplayText,
@@ -94,18 +122,21 @@ const useChipEditing = ({ conditions, chips, fields, setMenuAnchor, setSelectedF
94
122
  setSegmentFilterText: handleSegmentFilterChange,
95
123
  resetSegmentTyping,
96
124
  handleChipClick,
125
+ handlePairChipClick,
97
126
  startBuildingEdit,
98
127
  switchEditSegment,
99
128
  clearEditing
100
129
  }), [
101
130
  editingChipId,
102
131
  editingSegment,
132
+ editingSide,
103
133
  isBuildingEdit,
104
134
  segmentDisplayText,
105
135
  segmentMenuFilterText,
106
136
  handleSegmentFilterChange,
107
137
  resetSegmentTyping,
108
138
  handleChipClick,
139
+ handlePairChipClick,
109
140
  startBuildingEdit,
110
141
  switchEditSegment,
111
142
  clearEditing
@@ -50,6 +50,7 @@ export declare const useFilterInputAutocomplete: ({ fields, conditions, chips, u
50
50
  /** Hard reset for paste/clipboard flows — scraps in-progress building. */
51
51
  resetAutocompleteState: (continueBuilding?: boolean) => void;
52
52
  handleChipClick: (chipId: string, segment: import("../../FilterInputField").ChipSegment, anchorEl: HTMLElement) => void;
53
+ handlePairChipClick: (chipId: string, segment: import("../../FilterInputField").ChipSegment, anchorEl: HTMLElement) => void;
53
54
  handleBuildingChipClick: (segment: import("../../FilterInputField").ChipSegment, anchorEl: HTMLElement) => void;
54
55
  switchEditSegment: (targetSegment: import("../../FilterInputField").ChipSegment) => boolean;
55
56
  removeEditingChip: () => void;
@@ -67,6 +68,7 @@ export declare const useFilterInputAutocomplete: ({ fields, conditions, chips, u
67
68
  insertAfterConnector: boolean;
68
69
  editingChipId: string | null;
69
70
  editingSegment: import("../../FilterInputField/FilterInputChip").SegmentVariant | null;
71
+ editingSide: 0 | 1;
70
72
  segmentFilterText: string;
71
73
  segmentMenuFilterText: string;
72
74
  handleSegmentFilterChange: (text: string) => void;
@@ -16,7 +16,7 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
16
16
  const state = useAutocompleteState({
17
17
  conditions
18
18
  });
19
- const { inputText, setInputText, menuState, setMenuState, selectedField, setSelectedField, selectedOperator, setSelectedOperator, isFocused, setIsFocused, buildingMultiValue, setBuildingMultiValue, insertIndex, setInsertIndex, insertAfterConnector, setInsertAfterConnector, effectiveInsertIndex, effectiveInsertIndexRef, conditionsRef, conditionsLengthRef, blurCommitRef, segmentAttributeInputRef, segmentOperatorInputRef, segmentValueInputRef, commitBuildingOnBlurRef, commitBuildingForceRef } = state;
19
+ const { inputText, setInputText, menuState, setMenuState, selectedField, setSelectedField, selectedOperator, setSelectedOperator, isFocused, setIsFocused, buildingMultiValue, setBuildingMultiValue, buildingSide, setBuildingSide, buildingBase, setBuildingBase, insertIndex, setInsertIndex, insertAfterConnector, setInsertAfterConnector, effectiveInsertIndex, effectiveInsertIndexRef, conditionsRef, conditionsLengthRef, blurCommitRef, segmentAttributeInputRef, segmentOperatorInputRef, segmentValueInputRef, commitBuildingOnBlurRef, commitBuildingForceRef } = state;
20
20
  const { menuPositioning, setMenuAnchor, resetMenuAnchor } = useMenuPositioning({
21
21
  containerRef,
22
22
  buildingChipRef,
@@ -47,6 +47,8 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
47
47
  setSelectedField,
48
48
  setSelectedOperator,
49
49
  setBuildingMultiValue,
50
+ setBuildingSide,
51
+ setBuildingBase,
50
52
  setInsertIndex,
51
53
  setInsertAfterConnector,
52
54
  setMenuState
@@ -67,7 +69,11 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
67
69
  setSelectedOperator,
68
70
  setInputText,
69
71
  setMenuState,
70
- setBuildingMultiValue
72
+ setBuildingMultiValue,
73
+ buildingSide,
74
+ setBuildingSide,
75
+ buildingBase,
76
+ setBuildingBase
71
77
  });
72
78
  const { switchEditSegment, removeEditingChip, stepBackBuildingMenu } = useChipCascade({
73
79
  editing,
@@ -171,7 +177,10 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
171
177
  conditions,
172
178
  buildingMultiValue,
173
179
  dateRangeFromValue: dateRange.fromValue,
174
- segmentFilterText: editing.editingSegment === SEGMENT_VARIANT.value ? editing.segmentMenuFilterText : void 0
180
+ segmentFilterText: editing.editingSegment === SEGMENT_VARIANT.value ? editing.segmentMenuFilterText : void 0,
181
+ buildingSide,
182
+ buildingBase,
183
+ editingSide: editing.editingSide
175
184
  });
176
185
  return {
177
186
  inputText,
@@ -196,6 +205,7 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
196
205
  handleMenuDiscard,
197
206
  resetAutocompleteState: resetState,
198
207
  handleChipClick: editing.handleChipClick,
208
+ handlePairChipClick: editing.handlePairChipClick,
199
209
  handleBuildingChipClick,
200
210
  switchEditSegment,
201
211
  removeEditingChip,
@@ -213,6 +223,7 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
213
223
  insertAfterConnector,
214
224
  editingChipId: editing.editingChipId,
215
225
  editingSegment: editing.editingSegment,
226
+ editingSide: editing.editingSide,
216
227
  segmentFilterText: editing.segmentFilterText,
217
228
  segmentMenuFilterText: editing.segmentMenuFilterText,
218
229
  handleSegmentFilterChange,