@wallarm-org/design-system 0.36.0 → 0.37.0-rc-feature-AS-970.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 (29) hide show
  1. package/dist/components/FilterInput/FilterInput.js +1 -1
  2. package/dist/components/FilterInput/FilterInputContext/types.d.ts +4 -0
  3. package/dist/components/FilterInput/FilterInputContext/useFilterInputContextValue.d.ts +2 -0
  4. package/dist/components/FilterInput/FilterInputContext/useFilterInputContextValue.js +4 -0
  5. package/dist/components/FilterInput/FilterInputField/FilterInputChip/FilterInputChip.js +11 -4
  6. package/dist/components/FilterInput/FilterInputField/FilterInputChip/classes.d.ts +1 -0
  7. package/dist/components/FilterInput/FilterInputField/FilterInputChip/classes.js +7 -2
  8. package/dist/components/FilterInput/FilterInputField/FilterInputField.js +9 -2
  9. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/deriveAutocompleteValues.js +2 -1
  10. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useBlurCommit.d.ts +4 -1
  11. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useBlurCommit.js +12 -3
  12. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useChipActions.js +2 -0
  13. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useChipEditing.d.ts +1 -0
  14. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useChipEditing.js +11 -2
  15. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.d.ts +9 -2
  16. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.js +45 -4
  17. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFocusManagement.d.ts +9 -2
  18. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFocusManagement.js +21 -4
  19. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useInputHandlers.d.ts +4 -1
  20. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useInputHandlers.js +6 -6
  21. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow.d.ts +4 -0
  22. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useMenuFlow.js +66 -7
  23. package/dist/components/FilterInput/hooks/useFilterInputExpression/buildChips.js +6 -1
  24. package/dist/components/FilterInput/lib/index.d.ts +1 -1
  25. package/dist/components/FilterInput/lib/index.js +2 -2
  26. package/dist/components/FilterInput/lib/operators.d.ts +32 -1
  27. package/dist/components/FilterInput/lib/operators.js +25 -2
  28. package/dist/metadata/components.json +2 -2
  29. package/package.json +1 -1
@@ -49,7 +49,7 @@ const FilterInput = ({ fields: rawFields = [], value, onChange, placeholder = 'T
49
49
  setInputText: autocomplete.setInputText,
50
50
  closeMenu: autocomplete.closeAutocompleteMenu,
51
51
  replaceExpression,
52
- resetAutocompleteState: autocomplete.handleMenuDiscard
52
+ resetAutocompleteState: autocomplete.resetAutocompleteState
53
53
  });
54
54
  const contextValue = useFilterInputContextValue({
55
55
  chips,
@@ -23,6 +23,9 @@ export interface FilterInputContextValue {
23
23
  onInputClick: () => void;
24
24
  onGapClick: (conditionIndex: number, afterConnector: boolean) => void;
25
25
  onChipClick: (chipId: string, segment: ChipSegment, anchorRect: DOMRect) => void;
26
+ /** Click on a segment of the *building* (in-progress) chip — re-opens the
27
+ * corresponding menu and enters inline-edit without committing the chip. */
28
+ onBuildingChipClick: (segment: ChipSegment, anchorRect: DOMRect) => void;
26
29
  onConnectorChange: (chipId: string, value: 'and' | 'or') => void;
27
30
  onChipRemove: (chipId: string) => void;
28
31
  onClear: () => void;
@@ -33,6 +36,7 @@ export interface FilterInputContextValue {
33
36
  onCancelSegmentEdit: () => void;
34
37
  onCustomValueCommit: (customText: string) => void;
35
38
  onCustomAttributeCommit: (customText: string) => void;
39
+ onCustomOperatorCommit: (customText: string) => void;
36
40
  /** Ref to the currently open menu content element */
37
41
  menuRef: RefObject<HTMLDivElement | null>;
38
42
  /** Close autocomplete menu (used by connector chip to enforce single-dropdown constraint) */
@@ -10,6 +10,7 @@ interface AutocompleteForContext {
10
10
  handleKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
11
11
  handleInputClick: () => void;
12
12
  handleChipClick: (chipId: string, segment: ChipSegment, anchorRect: DOMRect) => void;
13
+ handleBuildingChipClick: (segment: ChipSegment, anchorRect: DOMRect) => void;
13
14
  handleConnectorChange: (chipId: string, value: 'and' | 'or') => void;
14
15
  handleChipRemove: (chipId: string) => void;
15
16
  handleClear: () => void;
@@ -23,6 +24,7 @@ interface AutocompleteForContext {
23
24
  cancelSegmentEdit: () => void;
24
25
  handleCustomValueCommit: (customText: string) => void;
25
26
  handleCustomAttributeCommit: (customText: string) => void;
27
+ handleCustomOperatorCommit: (customText: string) => void;
26
28
  menuRef: RefObject<HTMLDivElement | null>;
27
29
  closeAutocompleteMenu: () => void;
28
30
  segmentAttributeInputRef: RefObject<HTMLInputElement | null>;
@@ -16,6 +16,7 @@ const useFilterInputContextValue = ({ chips, autocomplete, buildingChipRef, inpu
16
16
  onInputKeyDown: autocomplete.handleKeyDown,
17
17
  onInputClick: autocomplete.handleInputClick,
18
18
  onChipClick: autocomplete.handleChipClick,
19
+ onBuildingChipClick: autocomplete.handleBuildingChipClick,
19
20
  onConnectorChange: autocomplete.handleConnectorChange,
20
21
  onChipRemove: autocomplete.handleChipRemove,
21
22
  onClear: autocomplete.handleClear,
@@ -26,6 +27,7 @@ const useFilterInputContextValue = ({ chips, autocomplete, buildingChipRef, inpu
26
27
  onCancelSegmentEdit: autocomplete.cancelSegmentEdit,
27
28
  onCustomValueCommit: autocomplete.handleCustomValueCommit,
28
29
  onCustomAttributeCommit: autocomplete.handleCustomAttributeCommit,
30
+ onCustomOperatorCommit: autocomplete.handleCustomOperatorCommit,
29
31
  menuRef: autocomplete.menuRef,
30
32
  closeAutocompleteMenu: autocomplete.closeAutocompleteMenu,
31
33
  registerChipRef,
@@ -44,6 +46,7 @@ const useFilterInputContextValue = ({ chips, autocomplete, buildingChipRef, inpu
44
46
  autocomplete.handleKeyDown,
45
47
  autocomplete.handleInputClick,
46
48
  autocomplete.handleChipClick,
49
+ autocomplete.handleBuildingChipClick,
47
50
  autocomplete.handleConnectorChange,
48
51
  autocomplete.handleChipRemove,
49
52
  autocomplete.handleClear,
@@ -54,6 +57,7 @@ const useFilterInputContextValue = ({ chips, autocomplete, buildingChipRef, inpu
54
57
  autocomplete.cancelSegmentEdit,
55
58
  autocomplete.handleCustomValueCommit,
56
59
  autocomplete.handleCustomAttributeCommit,
60
+ autocomplete.handleCustomOperatorCommit,
57
61
  autocomplete.menuRef,
58
62
  autocomplete.closeAutocompleteMenu,
59
63
  autocomplete.segmentAttributeInputRef,
@@ -8,11 +8,11 @@ import { FilterInputRemoveButton } from "./FilterInputRemoveButton.js";
8
8
  import { Segment } from "./Segment.js";
9
9
  import { SEGMENT_VARIANT } from "./segmentVariant.js";
10
10
  const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = false, valueParts, valueSeparator, errorValueIndices, building = false, disabled = false, onRemove, onSegmentClick, className, ...props })=>{
11
- const interactive = !building && !disabled;
11
+ const interactive = !disabled;
12
12
  const hasError = !!error;
13
13
  const internalRef = useRef(null);
14
14
  const editing = useEditingContext();
15
- const isEditingThisChip = null != editing && null != chipId && editing.editingChipId === chipId;
15
+ const isEditingThisChip = null != editing && null != editing.editingSegment && (building ? null == editing.editingChipId : null != chipId && editing.editingChipId === chipId);
16
16
  const activeSegment = isEditingThisChip ? editing.editingSegment : null;
17
17
  const handleSegmentClick = useCallback((segment, e)=>{
18
18
  if (!onSegmentClick) return;
@@ -25,6 +25,9 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
25
25
  onSegmentClick,
26
26
  activeSegment
27
27
  ]);
28
+ const handleSegmentMouseDown = useCallback((e)=>{
29
+ e.preventDefault();
30
+ }, []);
28
31
  const segmentEditProps = (segment)=>isEditingThisChip && editing.editingSegment === segment ? {
29
32
  editing: true,
30
33
  editText: editing.segmentFilterText,
@@ -44,7 +47,8 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
44
47
  className: cn(chipVariants({
45
48
  error: hasError,
46
49
  interactive,
47
- disabled
50
+ disabled,
51
+ building
48
52
  }), 'max-w-[320px]', className),
49
53
  "data-slot": "filter-input-condition-chip",
50
54
  ...props,
@@ -54,6 +58,7 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
54
58
  className: "shrink-0",
55
59
  error: true === error || error === SEGMENT_VARIANT.attribute,
56
60
  onClick: interactive ? (e)=>handleSegmentClick(SEGMENT_VARIANT.attribute, e) : void 0,
61
+ onMouseDown: interactive && building ? handleSegmentMouseDown : void 0,
57
62
  ...segmentEditProps(SEGMENT_VARIANT.attribute),
58
63
  children: attribute
59
64
  }),
@@ -61,6 +66,7 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
61
66
  variant: SEGMENT_VARIANT.operator,
62
67
  className: "shrink-0",
63
68
  onClick: interactive ? (e)=>handleSegmentClick(SEGMENT_VARIANT.operator, e) : void 0,
69
+ onMouseDown: interactive && building ? handleSegmentMouseDown : void 0,
64
70
  ...segmentEditProps(SEGMENT_VARIANT.operator),
65
71
  children: operator ?? ''
66
72
  }),
@@ -72,10 +78,11 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
72
78
  valueSeparator: valueSeparator,
73
79
  errorValueIndices: errorValueIndices,
74
80
  onClick: interactive ? (e)=>handleSegmentClick(SEGMENT_VARIANT.value, e) : void 0,
81
+ onMouseDown: interactive && building ? handleSegmentMouseDown : void 0,
75
82
  ...segmentEditProps(SEGMENT_VARIANT.value),
76
83
  children: value ?? ''
77
84
  }),
78
- building && /*#__PURE__*/ jsx(ChipSearchInput, {}),
85
+ building && !isEditingThisChip && /*#__PURE__*/ jsx(ChipSearchInput, {}),
79
86
  onRemove && !disabled && /*#__PURE__*/ jsx(FilterInputRemoveButton, {
80
87
  error: hasError,
81
88
  onRemove: onRemove
@@ -3,6 +3,7 @@ export declare const chipVariants: (props?: ({
3
3
  error?: boolean | null | undefined;
4
4
  interactive?: boolean | null | undefined;
5
5
  disabled?: boolean | null | undefined;
6
+ building?: boolean | null | undefined;
6
7
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
8
  /** Segment container */
8
9
  export declare const segmentContainer = "flex flex-col justify-center overflow-hidden leading-none";
@@ -14,11 +14,15 @@ const chipVariants = cva(`h-22 group/chip relative flex items-center justify-cen
14
14
  disabled: {
15
15
  true: 'opacity-50 cursor-default',
16
16
  false: ''
17
+ },
18
+ building: {
19
+ true: '',
20
+ false: ''
17
21
  }
18
22
  },
19
23
  compoundVariants: [
20
24
  {
21
- interactive: false,
25
+ building: true,
22
26
  error: false,
23
27
  className: 'border-border-strong-primary'
24
28
  }
@@ -26,7 +30,8 @@ const chipVariants = cva(`h-22 group/chip relative flex items-center justify-cen
26
30
  defaultVariants: {
27
31
  error: false,
28
32
  interactive: false,
29
- disabled: false
33
+ disabled: false,
34
+ building: false
30
35
  }
31
36
  });
32
37
  const segmentContainer = 'flex flex-col justify-center overflow-hidden leading-none';
@@ -15,7 +15,7 @@ import { FilterInputSearch } from "./FilterInputSearch.js";
15
15
  import { useChipsSplitting } from "./hooks/useChipsSplitting.js";
16
16
  import { useExpandCollapse } from "./hooks/useExpandCollapse.js";
17
17
  const FilterInputField = ({ className, ...props })=>{
18
- const { chips, buildingChipData, buildingChipRef, insertIndex, insertAfterConnector, error, onInputClick, onGapClick, onChipClick, onConnectorChange, onChipRemove, editingChipId, editingSegment, segmentFilterText, onSegmentFilterChange, onCancelSegmentEdit, onCustomValueCommit, onCustomAttributeCommit, menuRef } = useFilterInputContext();
18
+ const { chips, buildingChipData, buildingChipRef, insertIndex, insertAfterConnector, error, onInputClick, onGapClick, onChipClick, onBuildingChipClick, onConnectorChange, onChipRemove, editingChipId, editingSegment, segmentFilterText, onSegmentFilterChange, onCancelSegmentEdit, onCustomValueCommit, onCustomAttributeCommit, onCustomOperatorCommit, menuRef } = useFilterInputContext();
19
19
  const hasContent = chips.length > 0 || null != buildingChipData;
20
20
  const { isExpanded, isOverflowing, innerRef, toggleExpand, multiRow } = useExpandCollapse();
21
21
  const { chipsBefore, chipsAfter, hideTrailingGap, hideLeadingGap } = useChipsSplitting(chips, insertIndex, insertAfterConnector);
@@ -42,6 +42,11 @@ const FilterInputField = ({ className, ...props })=>{
42
42
  onCustomAttributeCommit(segmentFilterText);
43
43
  return;
44
44
  }
45
+ if (editingSegment === SEGMENT_VARIANT.operator) {
46
+ e.preventDefault();
47
+ onCustomOperatorCommit(segmentFilterText);
48
+ return;
49
+ }
45
50
  }
46
51
  if ('ArrowDown' === e.key) {
47
52
  e.preventDefault();
@@ -53,6 +58,7 @@ const FilterInputField = ({ className, ...props })=>{
53
58
  segmentFilterText,
54
59
  onCustomValueCommit,
55
60
  onCustomAttributeCommit,
61
+ onCustomOperatorCommit,
56
62
  menuRef
57
63
  ]);
58
64
  const handleSegmentEditBlur = useCallback((e)=>{
@@ -108,9 +114,10 @@ const FilterInputField = ({ className, ...props })=>{
108
114
  buildingChipData ? /*#__PURE__*/ jsx(FilterInputChip, {
109
115
  ref: buildingChipRef,
110
116
  building: true,
111
- attribute: buildingChipData.attribute ?? '',
117
+ attribute: buildingChipData.attribute,
112
118
  operator: buildingChipData.operator,
113
119
  value: buildingChipData.value,
120
+ onSegmentClick: onBuildingChipClick,
114
121
  className: "mx-4"
115
122
  }) : /*#__PURE__*/ jsx(FilterInputSearch, {
116
123
  hasContent: hasContent
@@ -1,4 +1,4 @@
1
- import { chipIdToConditionIndex, getDateDisplayLabel, getFieldValues, getOperatorLabel, hasStaticAllowlist, isMultiSelectOperator } from "../../lib/index.js";
1
+ import { NO_VALUE_PLACEHOLDER, chipIdToConditionIndex, getDateDisplayLabel, getFieldValues, getOperatorLabel, hasStaticAllowlist, isMultiSelectOperator, isNoValueOperator } from "../../lib/index.js";
2
2
  const getEditingCondition = (editingChipId, conditions)=>{
3
3
  if (!editingChipId) return null;
4
4
  const idx = chipIdToConditionIndex(editingChipId);
@@ -32,6 +32,7 @@ const deriveAutocompleteValues = ({ editingChipId, selectedField, selectedOperat
32
32
  const buildingValue = (()=>{
33
33
  if (buildingMultiValue) return buildingMultiValue;
34
34
  if (dateRangeFromValue && 'between' === selectedOperator) return `${getDateDisplayLabel(dateRangeFromValue)} – ...`;
35
+ if (selectedOperator && isNoValueOperator(selectedOperator)) return NO_VALUE_PLACEHOLDER;
35
36
  })();
36
37
  const buildingChipData = isBuilding ? {
37
38
  attribute: selectedField.label,
@@ -29,5 +29,8 @@ interface UseBlurCommitDeps {
29
29
  * plus a blur in the same tick) short-circuit on re-entry instead of creating
30
30
  * duplicate error chips.
31
31
  */
32
- export declare const useBlurCommit: ({ selectedField, selectedOperator, inputText, editingChipId, effectiveInsertIndexRef, handleCustomValueCommit, upsertCondition, resetState, commitBuildingOnBlurRef, }: UseBlurCommitDeps) => () => boolean;
32
+ export declare const useBlurCommit: ({ selectedField, selectedOperator, inputText, editingChipId, effectiveInsertIndexRef, handleCustomValueCommit, upsertCondition, resetState, commitBuildingOnBlurRef, }: UseBlurCommitDeps) => {
33
+ commitBuildingOnBlur: () => boolean;
34
+ hasIncompleteBuilding: () => boolean;
35
+ };
33
36
  export {};
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useRef } from "react";
2
+ import { isBuildingComplete, isNoValueOperator } from "../../lib/index.js";
2
3
  const useBlurCommit = ({ selectedField, selectedOperator, inputText, editingChipId, effectiveInsertIndexRef, handleCustomValueCommit, upsertCondition, resetState, commitBuildingOnBlurRef })=>{
3
4
  const selectedFieldRef = useRef(selectedField);
4
5
  selectedFieldRef.current = selectedField;
@@ -14,16 +15,18 @@ const useBlurCommit = ({ selectedField, selectedOperator, inputText, editingChip
14
15
  const text = inputTextRef.current.trim();
15
16
  if (!field) return false;
16
17
  if (editingChipId) return false;
18
+ const hasTypedValue = !!operator && !isNoValueOperator(operator) && text.length > 0;
19
+ if (!isBuildingComplete(field, operator, null) && !hasTypedValue) return false;
17
20
  committingRef.current = true;
18
21
  try {
19
22
  selectedFieldRef.current = null;
20
23
  selectedOperatorRef.current = null;
21
24
  inputTextRef.current = '';
22
- if (operator && text) {
25
+ if (hasTypedValue) {
23
26
  handleCustomValueCommit(text);
24
27
  return true;
25
28
  }
26
- upsertCondition(field, operator ?? void 0, null, void 0, effectiveInsertIndexRef.current, true);
29
+ upsertCondition(field, operator, null, void 0, effectiveInsertIndexRef.current);
27
30
  resetState();
28
31
  return true;
29
32
  } finally{
@@ -37,6 +40,12 @@ const useBlurCommit = ({ selectedField, selectedOperator, inputText, editingChip
37
40
  effectiveInsertIndexRef
38
41
  ]);
39
42
  commitBuildingOnBlurRef.current = commitBuildingOnBlur;
40
- return commitBuildingOnBlur;
43
+ const hasIncompleteBuilding = useCallback(()=>null !== selectedFieldRef.current && !editingChipId, [
44
+ editingChipId
45
+ ]);
46
+ return {
47
+ commitBuildingOnBlur,
48
+ hasIncompleteBuilding
49
+ };
41
50
  };
42
51
  export { useBlurCommit };
@@ -25,6 +25,7 @@ const useChipActions = ({ effectiveInsertIndexRef, inputRef, removeCondition, cl
25
25
  resetState
26
26
  ]);
27
27
  const handleGapClick = useCallback((conditionIndex, afterConnector)=>{
28
+ resetState();
28
29
  flushSync(()=>{
29
30
  setInsertIndex(conditionIndex);
30
31
  setInsertAfterConnector(afterConnector);
@@ -34,6 +35,7 @@ const useChipActions = ({ effectiveInsertIndexRef, inputRef, removeCondition, cl
34
35
  setMenuState('field');
35
36
  inputRef.current?.focus();
36
37
  }, [
38
+ resetState,
37
39
  resetMenuOffset,
38
40
  inputRef,
39
41
  setInsertIndex,
@@ -26,6 +26,7 @@ export declare const useChipEditing: ({ conditions, chips, fields, containerRef,
26
26
  setSegmentFilterText: (text: string) => void;
27
27
  resetSegmentTyping: () => void;
28
28
  handleChipClick: (chipId: string, segment: ChipSegment, anchorRect: DOMRect) => void;
29
+ startBuildingEdit: (segment: ChipSegment, currentText: string) => void;
29
30
  clearEditing: () => void;
30
31
  };
31
32
  export {};
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useMemo, useRef, useState } from "react";
2
2
  import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
3
- import { chipIdToConditionIndex, getOperatorFromLabel } from "../../lib/index.js";
3
+ import { chipIdToConditionIndex, getOperatorFromLabel, isNoValueOperator } from "../../lib/index.js";
4
4
  const getConditionByChipId = (chipId, conditions)=>{
5
5
  const idx = chipIdToConditionIndex(chipId);
6
6
  return null !== idx ? conditions[idx] ?? null : null;
@@ -35,7 +35,8 @@ const useChipEditing = ({ conditions, chips, fields, containerRef, setMenuOffset
35
35
  const chip = chipsRef.current.find((c)=>c.id === chipId);
36
36
  if (!chip || 'chip' !== chip.variant) return;
37
37
  const incompleteSegment = condition.error ? getFirstIncompleteSegment(condition, fieldsRef.current) : null;
38
- const targetSegment = incompleteSegment ?? segment;
38
+ const isPlaceholderValueClick = segment === SEGMENT_VARIANT.value && null != condition.operator && isNoValueOperator(condition.operator);
39
+ const targetSegment = incompleteSegment ?? (isPlaceholderValueClick ? SEGMENT_VARIANT.operator : segment);
39
40
  if (!field && targetSegment !== SEGMENT_VARIANT.attribute) return;
40
41
  if (incompleteSegment && field) upsertCondition(field, condition.operator, condition.value, chipId);
41
42
  const containerRect = containerRef.current?.getBoundingClientRect();
@@ -67,6 +68,12 @@ const useChipEditing = ({ conditions, chips, fields, containerRef, setMenuOffset
67
68
  setSegmentFilterText('');
68
69
  setUserHasTyped(false);
69
70
  }, []);
71
+ const startBuildingEdit = useCallback((segment, currentText)=>{
72
+ setEditingChipId(null);
73
+ setEditingSegment(segment);
74
+ setSegmentFilterText(currentText);
75
+ setUserHasTyped(false);
76
+ }, []);
70
77
  const handleSegmentFilterChange = useCallback((text)=>{
71
78
  setSegmentFilterText(text);
72
79
  setUserHasTyped(true);
@@ -85,6 +92,7 @@ const useChipEditing = ({ conditions, chips, fields, containerRef, setMenuOffset
85
92
  setSegmentFilterText: handleSegmentFilterChange,
86
93
  resetSegmentTyping,
87
94
  handleChipClick,
95
+ startBuildingEdit,
88
96
  clearEditing
89
97
  }), [
90
98
  editingChipId,
@@ -94,6 +102,7 @@ const useChipEditing = ({ conditions, chips, fields, containerRef, setMenuOffset
94
102
  handleSegmentFilterChange,
95
103
  resetSegmentTyping,
96
104
  handleChipClick,
105
+ startBuildingEdit,
97
106
  clearEditing
98
107
  ]);
99
108
  };
@@ -1,4 +1,5 @@
1
1
  import type { RefObject } from 'react';
2
+ import { type ChipSegment } from '../../FilterInputField/FilterInputChip';
2
3
  import type { Condition, FieldMetadata, FilterInputChipData, FilterOperator, MenuState, UpsertCondition } from '../../types';
3
4
  interface UseFilterInputAutocompleteOptions {
4
5
  fields: FieldMetadata[];
@@ -46,8 +47,13 @@ export declare const useFilterInputAutocomplete: ({ fields, conditions, chips, u
46
47
  handleBuildingValueChange: (preview: string | undefined) => void;
47
48
  handleMultiSelectToggle: () => void;
48
49
  handleMenuClose: () => void;
49
- handleMenuDiscard: (continueBuilding?: boolean) => void;
50
- handleChipClick: (chipId: string, segment: import("../../FilterInputField").ChipSegment, anchorRect: DOMRect) => void;
50
+ handleMenuDiscard: () => void;
51
+ /** Hard reset of autocomplete state used by paste/clipboard flows where
52
+ * the conditions array is replaced and any in-progress building must be
53
+ * scrapped, regardless of inline-edit mode. */
54
+ resetAutocompleteState: (continueBuilding?: boolean) => void;
55
+ handleChipClick: (chipId: string, segment: ChipSegment, anchorRect: DOMRect) => void;
56
+ handleBuildingChipClick: (segment: ChipSegment, anchorRect: DOMRect) => void;
51
57
  handleConnectorChange: (connectorId: string, value: "and" | "or") => void;
52
58
  handleChipRemove: (chipId: string) => void;
53
59
  handleClear: () => void;
@@ -67,6 +73,7 @@ export declare const useFilterInputAutocomplete: ({ fields, conditions, chips, u
67
73
  cancelSegmentEdit: () => void;
68
74
  handleCustomValueCommit: (customText: string) => void;
69
75
  handleCustomAttributeCommit: (customText: string) => void;
76
+ handleCustomOperatorCommit: (customText: string) => void;
70
77
  menuRef: RefObject<HTMLDivElement | null>;
71
78
  closeAutocompleteMenu: () => void;
72
79
  blurCommitRef: RefObject<(() => boolean) | null>;
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useRef, useState } from "react";
2
2
  import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
3
3
  import { useDateRange } from "../../FilterInputMenu/FilterInputDateValueMenu/hooks.js";
4
- import { applyAcceptChar } from "../../lib/index.js";
4
+ import { applyAcceptChar, getOperatorLabel } from "../../lib/index.js";
5
5
  import { deriveAutocompleteValues } from "./deriveAutocompleteValues.js";
6
6
  import { useBlurCommit } from "./useBlurCommit.js";
7
7
  import { useChipActions } from "./useChipActions.js";
@@ -65,7 +65,7 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
65
65
  setInsertAfterConnector,
66
66
  setMenuState
67
67
  });
68
- const { handleMenuClose, handleFieldSelect, handleOperatorSelect, handleValueSelect, handleMultiCommit, handleBuildingValueChange, handleMultiSelectToggle, handleRangeSelect, handleCustomValueCommit, handleCustomAttributeCommit } = useMenuFlow({
68
+ const { handleMenuClose, handleFieldSelect, handleOperatorSelect, handleValueSelect, handleMultiCommit, handleBuildingValueChange, handleMultiSelectToggle, handleRangeSelect, handleCustomValueCommit, handleCustomAttributeCommit, handleCustomOperatorCommit } = useMenuFlow({
69
69
  editing,
70
70
  selectedField,
71
71
  selectedOperator,
@@ -87,6 +87,7 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
87
87
  inputText,
88
88
  menuState,
89
89
  selectedField,
90
+ selectedOperator,
90
91
  isFocused,
91
92
  fields,
92
93
  inputRef,
@@ -102,7 +103,7 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
102
103
  handleOperatorSelect,
103
104
  handleCustomValueCommit
104
105
  });
105
- const commitBuildingOnBlur = useBlurCommit({
106
+ const { commitBuildingOnBlur, hasIncompleteBuilding } = useBlurCommit({
106
107
  selectedField,
107
108
  selectedOperator,
108
109
  inputText,
@@ -118,6 +119,8 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
118
119
  isFocused,
119
120
  conditionsLength: conditions.length,
120
121
  inputText,
122
+ selectedField,
123
+ selectedOperator,
121
124
  containerRef,
122
125
  inputRef,
123
126
  editingSegment: editing.editingSegment,
@@ -126,6 +129,7 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
126
129
  segmentValueInputRef,
127
130
  blurCommitRef,
128
131
  commitBuildingOnBlur,
132
+ hasIncompleteBuilding,
129
133
  setIsFocused,
130
134
  setMenuState,
131
135
  resetMenuOffset,
@@ -160,6 +164,12 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
160
164
  editing
161
165
  ]);
162
166
  const cancelSegmentEdit = useCallback(()=>{
167
+ const isBuildingEdit = !editing.editingChipId && null !== editing.editingSegment;
168
+ if (isBuildingEdit) {
169
+ editing.clearEditing();
170
+ setMenuState('closed');
171
+ return;
172
+ }
163
173
  setSelectedField(null);
164
174
  setSelectedOperator(null);
165
175
  editing.clearEditing();
@@ -167,6 +177,34 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
167
177
  }, [
168
178
  editing
169
179
  ]);
180
+ const handleMenuDiscard = useCallback(()=>{
181
+ const isBuildingEdit = !editing.editingChipId && null !== editing.editingSegment;
182
+ if (isBuildingEdit) {
183
+ editing.clearEditing();
184
+ setMenuState('closed');
185
+ return;
186
+ }
187
+ resetState();
188
+ }, [
189
+ editing,
190
+ resetState
191
+ ]);
192
+ const handleBuildingChipClick = useCallback((segment, anchorRect)=>{
193
+ if (!selectedField) return;
194
+ const containerRect = containerRef.current?.getBoundingClientRect();
195
+ setMenuOffset(containerRect ? anchorRect.left - containerRect.left : 0);
196
+ const initialText = segment === SEGMENT_VARIANT.attribute ? selectedField.label : segment === SEGMENT_VARIANT.operator ? selectedOperator ? getOperatorLabel(selectedOperator, selectedField.type) : '' : buildingMultiValue ?? '';
197
+ editing.startBuildingEdit(segment, initialText);
198
+ setInputText('');
199
+ setMenuState(segment === SEGMENT_VARIANT.attribute ? 'field' : segment === SEGMENT_VARIANT.operator ? 'operator' : 'value');
200
+ }, [
201
+ selectedField,
202
+ selectedOperator,
203
+ buildingMultiValue,
204
+ containerRef,
205
+ setMenuOffset,
206
+ editing
207
+ ]);
170
208
  return {
171
209
  inputText,
172
210
  menuState,
@@ -187,8 +225,10 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
187
225
  handleBuildingValueChange,
188
226
  handleMultiSelectToggle,
189
227
  handleMenuClose,
190
- handleMenuDiscard: resetState,
228
+ handleMenuDiscard,
229
+ resetAutocompleteState: resetState,
191
230
  handleChipClick: editing.handleChipClick,
231
+ handleBuildingChipClick,
192
232
  handleConnectorChange: setConnectorValue,
193
233
  handleChipRemove,
194
234
  handleClear,
@@ -208,6 +248,7 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
208
248
  cancelSegmentEdit,
209
249
  handleCustomValueCommit,
210
250
  handleCustomAttributeCommit,
251
+ handleCustomOperatorCommit,
211
252
  menuRef,
212
253
  closeAutocompleteMenu,
213
254
  blurCommitRef,
@@ -1,11 +1,15 @@
1
1
  import type { FocusEvent, RefObject } from 'react';
2
2
  import { type ChipSegment } from '../../FilterInputField/FilterInputChip';
3
- import type { MenuState } from '../../types';
3
+ import type { FieldMetadata, FilterOperator, MenuState } from '../../types';
4
4
  interface UseFocusManagementDeps {
5
5
  menuState: MenuState;
6
6
  isFocused: boolean;
7
7
  conditionsLength: number;
8
8
  inputText: string;
9
+ /** In-progress building-chip state — needed so refocus resumes at the next
10
+ * missing segment instead of (incorrectly) reopening the field menu. */
11
+ selectedField: FieldMetadata | null;
12
+ selectedOperator: FilterOperator | null;
9
13
  containerRef: RefObject<HTMLElement | null>;
10
14
  inputRef: RefObject<HTMLInputElement | null>;
11
15
  editingSegment: ChipSegment | null;
@@ -19,12 +23,15 @@ interface UseFocusManagementDeps {
19
23
  blurCommitRef: RefObject<(() => boolean) | null>;
20
24
  /** Tries to commit a building chip's freeform value on blur. Returns true if committed. */
21
25
  commitBuildingOnBlur: () => boolean;
26
+ /** True if there's an in-progress building chip that the blur/close path
27
+ * must preserve (skip resetState). */
28
+ hasIncompleteBuilding: () => boolean;
22
29
  setIsFocused: (focused: boolean) => void;
23
30
  setMenuState: (state: MenuState) => void;
24
31
  resetMenuOffset: () => void;
25
32
  resetState: (continueBuilding?: boolean) => void;
26
33
  }
27
- export declare const useFocusManagement: ({ menuState, isFocused, conditionsLength, inputText, containerRef, inputRef, editingSegment, segmentAttributeInputRef, segmentOperatorInputRef, segmentValueInputRef, blurCommitRef, commitBuildingOnBlur, setIsFocused, setMenuState, resetMenuOffset, resetState, }: UseFocusManagementDeps) => {
34
+ export declare const useFocusManagement: ({ menuState, isFocused, conditionsLength, inputText, selectedField, selectedOperator, containerRef, inputRef, editingSegment, segmentAttributeInputRef, segmentOperatorInputRef, segmentValueInputRef, blurCommitRef, commitBuildingOnBlur, hasIncompleteBuilding, setIsFocused, setMenuState, resetMenuOffset, resetState, }: UseFocusManagementDeps) => {
28
35
  handleFocus: (e: FocusEvent) => void;
29
36
  handleBlur: (e: FocusEvent) => void;
30
37
  };
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect, useRef } from "react";
2
2
  import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
3
- import { isMenuRelated } from "../../lib/index.js";
4
- const useFocusManagement = ({ menuState, isFocused, conditionsLength, inputText, containerRef, inputRef, editingSegment, segmentAttributeInputRef, segmentOperatorInputRef, segmentValueInputRef, blurCommitRef, commitBuildingOnBlur, setIsFocused, setMenuState, resetMenuOffset, resetState })=>{
3
+ import { isMenuRelated, nextBuildingMenu } from "../../lib/index.js";
4
+ const useFocusManagement = ({ menuState, isFocused, conditionsLength, inputText, selectedField, selectedOperator, containerRef, inputRef, editingSegment, segmentAttributeInputRef, segmentOperatorInputRef, segmentValueInputRef, blurCommitRef, commitBuildingOnBlur, hasIncompleteBuilding, setIsFocused, setMenuState, resetMenuOffset, resetState })=>{
5
5
  const handleFocus = useCallback((e)=>{
6
6
  if (e.target?.closest?.('[data-slot="filter-input-connector-chip"]')) return;
7
7
  setIsFocused(true);
@@ -18,7 +18,8 @@ const useFocusManagement = ({ menuState, isFocused, conditionsLength, inputText,
18
18
  try {
19
19
  setIsFocused(false);
20
20
  const committed = blurCommitRef.current?.() || commitBuildingOnBlur();
21
- if (!committed) resetState();
21
+ if (!committed) if (hasIncompleteBuilding()) setMenuState('closed');
22
+ else resetState();
22
23
  related?.focus();
23
24
  if (document.activeElement === inputRef.current) inputRef.current?.blur();
24
25
  } finally{
@@ -28,13 +29,26 @@ const useFocusManagement = ({ menuState, isFocused, conditionsLength, inputText,
28
29
  containerRef,
29
30
  blurCommitRef,
30
31
  commitBuildingOnBlur,
32
+ hasIncompleteBuilding,
31
33
  resetState,
32
34
  setIsFocused,
35
+ setMenuState,
33
36
  inputRef
34
37
  ]);
35
38
  const prevFocusedRef = useRef(false);
36
39
  useEffect(()=>{
37
- if (isFocused && !prevFocusedRef.current && 0 === conditionsLength && '' === inputText) {
40
+ if (!isFocused || prevFocusedRef.current) {
41
+ prevFocusedRef.current = isFocused;
42
+ return;
43
+ }
44
+ if (editingSegment) {
45
+ prevFocusedRef.current = isFocused;
46
+ return;
47
+ }
48
+ if (selectedField) {
49
+ resetMenuOffset();
50
+ setMenuState(nextBuildingMenu(selectedField, selectedOperator));
51
+ } else if (0 === conditionsLength && '' === inputText) {
38
52
  resetMenuOffset();
39
53
  setMenuState('field');
40
54
  }
@@ -43,6 +57,9 @@ const useFocusManagement = ({ menuState, isFocused, conditionsLength, inputText,
43
57
  isFocused,
44
58
  conditionsLength,
45
59
  inputText,
60
+ selectedField,
61
+ selectedOperator,
62
+ editingSegment,
46
63
  resetMenuOffset,
47
64
  setMenuState
48
65
  ]);
@@ -4,6 +4,9 @@ interface UseInputHandlersDeps {
4
4
  inputText: string;
5
5
  menuState: MenuState;
6
6
  selectedField: FieldMetadata | null;
7
+ /** Needed so a click into the main input while a building chip is alive
8
+ * resumes at the next missing segment instead of doing nothing. */
9
+ selectedOperator: FilterOperator | null;
7
10
  isFocused: boolean;
8
11
  fields: FieldMetadata[];
9
12
  inputRef: RefObject<HTMLInputElement | null>;
@@ -19,7 +22,7 @@ interface UseInputHandlersDeps {
19
22
  handleOperatorSelect: (operator: FilterOperator) => void;
20
23
  handleCustomValueCommit: (text: string) => void;
21
24
  }
22
- export declare const useInputHandlers: ({ inputText, menuState, selectedField, isFocused, fields, inputRef, conditionsRef, conditionsLengthRef, effectiveInsertIndexRef, setInputText, setMenuState, setInsertIndex, resetMenuOffset, removeConditionAtIndex, handleFieldSelect, handleOperatorSelect, handleCustomValueCommit, }: UseInputHandlersDeps) => {
25
+ export declare const useInputHandlers: ({ inputText, menuState, selectedField, selectedOperator, isFocused, fields, inputRef, conditionsRef, conditionsLengthRef, effectiveInsertIndexRef, setInputText, setMenuState, setInsertIndex, resetMenuOffset, removeConditionAtIndex, handleFieldSelect, handleOperatorSelect, handleCustomValueCommit, }: UseInputHandlersDeps) => {
23
26
  handleInputChange: (e: ChangeEvent<HTMLInputElement>) => void;
24
27
  handleInputClick: () => void;
25
28
  handleKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void;
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useRef } from "react";
2
- import { OPERATOR_SYMBOLS, applyAcceptChar, getOperatorFromLabel, hasFieldValues } from "../../lib/index.js";
3
- const useInputHandlers = ({ inputText, menuState, selectedField, isFocused, fields, inputRef, conditionsRef, conditionsLengthRef, effectiveInsertIndexRef, setInputText, setMenuState, setInsertIndex, resetMenuOffset, removeConditionAtIndex, handleFieldSelect, handleOperatorSelect, handleCustomValueCommit })=>{
2
+ import { OPERATOR_SYMBOLS, applyAcceptChar, getOperatorFromLabel, hasFieldValues, nextBuildingMenu } from "../../lib/index.js";
3
+ const useInputHandlers = ({ inputText, menuState, selectedField, selectedOperator, isFocused, fields, inputRef, conditionsRef, conditionsLengthRef, effectiveInsertIndexRef, setInputText, setMenuState, setInsertIndex, resetMenuOffset, removeConditionAtIndex, handleFieldSelect, handleOperatorSelect, handleCustomValueCommit })=>{
4
4
  const menuRef = useRef(null);
5
5
  const handleInputChange = useCallback((e)=>{
6
6
  let text = e.target.value;
@@ -21,13 +21,13 @@ const useInputHandlers = ({ inputText, menuState, selectedField, isFocused, fiel
21
21
  ]);
22
22
  const handleInputClick = useCallback(()=>{
23
23
  inputRef.current?.focus();
24
- if ('closed' === menuState && !selectedField) {
25
- resetMenuOffset();
26
- setMenuState('field');
27
- }
24
+ if ('closed' !== menuState) return;
25
+ resetMenuOffset();
26
+ setMenuState(nextBuildingMenu(selectedField, selectedOperator));
28
27
  }, [
29
28
  menuState,
30
29
  selectedField,
30
+ selectedOperator,
31
31
  resetMenuOffset,
32
32
  inputRef,
33
33
  setMenuState
@@ -8,6 +8,9 @@ interface MenuFlowDeps {
8
8
  setEditingSegment: (segment: ChipSegment | null) => void;
9
9
  setSegmentFilterText: (text: string) => void;
10
10
  resetSegmentTyping: () => void;
11
+ /** Exit inline-edit and the building-edit marker. Called when switching
12
+ * filter/operator in the building chip lands on the next menu. */
13
+ clearEditing: () => void;
11
14
  };
12
15
  selectedField: FieldMetadata | null;
13
16
  selectedOperator: FilterOperator | null;
@@ -38,6 +41,7 @@ export declare const useMenuFlow: ({ editing, selectedField, selectedOperator, f
38
41
  handleMultiSelectToggle: () => void;
39
42
  handleRangeSelect: (from: string, to: string) => void;
40
43
  handleCustomValueCommit: (customText: string) => void;
44
+ handleCustomOperatorCommit: (customText: string) => void;
41
45
  handleCustomAttributeCommit: (customText: string) => void;
42
46
  };
43
47
  export {};
@@ -1,6 +1,6 @@
1
1
  import { useCallback, useRef } from "react";
2
2
  import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
3
- import { chipIdToConditionIndex, isBetweenOperator, isMultiSelectOperator, isNoValueOperator } from "../../lib/index.js";
3
+ import { OPERATOR_SYMBOLS, chipIdToConditionIndex, getFieldOperators, getOperatorFromLabel, isBetweenOperator, isMultiSelectOperator, isNoValueOperator, isOperatorAllowedForField, isValueShapeCompatible } from "../../lib/index.js";
4
4
  import { resolveDateRangeValue, resolveDateValue, resolveMultiValues, resolveSingleValue, validateValueForField } from "./valueCommitHelpers.js";
5
5
  const useMenuFlow = ({ editing, selectedField, selectedOperator, fields, inputRef, insertIndex, upsertCondition, conditions, resetState, commitBuildingOnBlur, dateRange, setSelectedField, setSelectedOperator, setInputText, setMenuState, setBuildingMultiValue })=>{
6
6
  const conditionsRef = useRef(conditions);
@@ -8,11 +8,17 @@ const useMenuFlow = ({ editing, selectedField, selectedOperator, fields, inputRe
8
8
  const handleMenuClose = useCallback(()=>{
9
9
  if (document.activeElement === inputRef.current) return;
10
10
  if (document.activeElement?.closest?.('[data-slot^="segment-"]')) return;
11
- if (!commitBuildingOnBlur()) resetState();
11
+ if (commitBuildingOnBlur()) return;
12
+ const hasIncompleteBuilding = null !== selectedField && !editing.editingChipId;
13
+ if (hasIncompleteBuilding) return void setMenuState('closed');
14
+ resetState();
12
15
  }, [
13
16
  commitBuildingOnBlur,
14
17
  resetState,
15
- inputRef
18
+ inputRef,
19
+ selectedField,
20
+ editing.editingChipId,
21
+ setMenuState
16
22
  ]);
17
23
  const handleFieldSelect = useCallback((field)=>{
18
24
  if (editing.editingChipId && editing.editingSegment === SEGMENT_VARIANT.attribute) {
@@ -25,19 +31,44 @@ const useMenuFlow = ({ editing, selectedField, selectedOperator, fields, inputRe
25
31
  resetState();
26
32
  return;
27
33
  }
34
+ const isBuildingEdit = !editing.editingChipId && editing.editingSegment === SEGMENT_VARIANT.attribute;
35
+ if (isBuildingEdit) {
36
+ setSelectedField(field);
37
+ const keepOperator = selectedOperator ? isOperatorAllowedForField(field, selectedOperator) : false;
38
+ if (!keepOperator) setSelectedOperator(null);
39
+ editing.clearEditing();
40
+ setMenuState(keepOperator ? 'value' : 'operator');
41
+ return;
42
+ }
28
43
  setSelectedField(field);
29
44
  setInputText('');
30
45
  setMenuState('operator');
31
46
  }, [
32
47
  editing,
48
+ selectedOperator,
33
49
  upsertCondition,
34
50
  resetState,
35
51
  setSelectedField,
52
+ setSelectedOperator,
36
53
  setInputText,
37
54
  setMenuState
38
55
  ]);
39
56
  const handleOperatorSelect = useCallback((operator)=>{
40
57
  if (!selectedField) return;
58
+ const isBuildingEdit = !editing.editingChipId && editing.editingSegment === SEGMENT_VARIANT.operator;
59
+ if (isBuildingEdit) {
60
+ const shapeCompatible = isValueShapeCompatible(selectedOperator, operator);
61
+ if (!shapeCompatible) setBuildingMultiValue(void 0);
62
+ setSelectedOperator(operator);
63
+ editing.clearEditing();
64
+ if (isNoValueOperator(operator)) {
65
+ upsertCondition(selectedField, operator, null, null, insertIndex);
66
+ resetState(true);
67
+ return;
68
+ }
69
+ setMenuState('value');
70
+ return;
71
+ }
41
72
  if (isNoValueOperator(operator)) {
42
73
  const isEditing = !!editing.editingChipId;
43
74
  upsertCondition(selectedField, operator, null, editing.editingChipId, isEditing ? void 0 : insertIndex);
@@ -65,11 +96,14 @@ const useMenuFlow = ({ editing, selectedField, selectedOperator, fields, inputRe
65
96
  }, [
66
97
  editing,
67
98
  selectedField,
99
+ selectedOperator,
68
100
  insertIndex,
69
101
  upsertCondition,
70
102
  resetState,
71
103
  setSelectedOperator,
72
- setMenuState
104
+ setMenuState,
105
+ setBuildingMultiValue,
106
+ setInputText
73
107
  ]);
74
108
  const handleValueSelect = useCallback((val)=>{
75
109
  if (!selectedField || !selectedOperator) return;
@@ -161,12 +195,16 @@ const useMenuFlow = ({ editing, selectedField, selectedOperator, fields, inputRe
161
195
  resetState
162
196
  ]);
163
197
  const handleCustomAttributeCommit = useCallback((customText)=>{
164
- if (!editing.editingChipId || !customText.trim()) return;
198
+ if (!customText.trim()) return;
165
199
  const trimmed = customText.trim();
200
+ const matchedField = fields.find((f)=>f.label.toLowerCase() === trimmed.toLowerCase() || f.name.toLowerCase() === trimmed.toLowerCase());
201
+ if (!editing.editingChipId) {
202
+ if (matchedField) handleFieldSelect(matchedField);
203
+ return;
204
+ }
166
205
  const idx = chipIdToConditionIndex(editing.editingChipId);
167
206
  const condition = null !== idx ? conditionsRef.current[idx] : null;
168
207
  if (!condition) return;
169
- const matchedField = fields.find((f)=>f.label.toLowerCase() === trimmed.toLowerCase() || f.name.toLowerCase() === trimmed.toLowerCase());
170
208
  if (matchedField) {
171
209
  const hasValueError = validateValueForField(matchedField, condition.value);
172
210
  upsertCondition(matchedField, condition.operator, condition.value, editing.editingChipId, void 0, hasValueError ? SEGMENT_VARIANT.value : void 0, condition.dateOrigin);
@@ -183,7 +221,27 @@ const useMenuFlow = ({ editing, selectedField, selectedOperator, fields, inputRe
183
221
  editing,
184
222
  fields,
185
223
  upsertCondition,
186
- resetState
224
+ resetState,
225
+ handleFieldSelect
226
+ ]);
227
+ const handleCustomOperatorCommit = useCallback((customText)=>{
228
+ if (!selectedField || !customText.trim()) return;
229
+ const trimmed = customText.trim();
230
+ const allowed = getFieldOperators(selectedField);
231
+ let matched = getOperatorFromLabel(trimmed, selectedField.type);
232
+ if (!matched) {
233
+ const symbolMatch = allowed.find((op)=>OPERATOR_SYMBOLS[op].toLowerCase() === trimmed.toLowerCase());
234
+ if (symbolMatch) matched = symbolMatch;
235
+ }
236
+ if (!matched) {
237
+ const rawMatch = allowed.find((op)=>op.toLowerCase() === trimmed.toLowerCase());
238
+ if (rawMatch) matched = rawMatch;
239
+ }
240
+ if (!matched || !isOperatorAllowedForField(selectedField, matched)) return;
241
+ handleOperatorSelect(matched);
242
+ }, [
243
+ selectedField,
244
+ handleOperatorSelect
187
245
  ]);
188
246
  return {
189
247
  handleMenuClose,
@@ -195,6 +253,7 @@ const useMenuFlow = ({ editing, selectedField, selectedOperator, fields, inputRe
195
253
  handleMultiSelectToggle,
196
254
  handleRangeSelect,
197
255
  handleCustomValueCommit,
256
+ handleCustomOperatorCommit,
198
257
  handleCustomAttributeCommit
199
258
  };
200
259
  };
@@ -1,5 +1,5 @@
1
1
  import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
2
- import { findOptionByValue, getDateDisplayLabel, getOperatorLabel } from "../../lib/index.js";
2
+ import { NO_VALUE_PLACEHOLDER, findOptionByValue, getDateDisplayLabel, getOperatorLabel, isNoValueOperator } from "../../lib/index.js";
3
3
  import { getInvalidValueIndices } from "../useFilterInputAutocomplete/valueCommitHelpers.js";
4
4
  const INVALID_DATE = 'Invalid Date';
5
5
  const DATE_RANGE_SEPARATOR = ' – ';
@@ -84,6 +84,11 @@ const makeConditionChip = (i, conditions, fields, error)=>{
84
84
  const chipError = condition.error || (error ? true : void 0);
85
85
  const field = fields.find((f)=>f.name === condition.field);
86
86
  const baseChip = buildBaseChip(i, condition, field);
87
+ if (condition.operator && isNoValueOperator(condition.operator)) return {
88
+ ...baseChip,
89
+ value: NO_VALUE_PLACEHOLDER,
90
+ error: chipError
91
+ };
87
92
  if (field?.type === 'date') return Array.isArray(condition.value) ? buildDateRangeChip(baseChip, condition, chipError) : buildDateChip(baseChip, condition, chipError);
88
93
  if (Array.isArray(condition.value)) return buildMultiValueChip(baseChip, condition, field, chipError);
89
94
  return {
@@ -9,7 +9,7 @@ export { buildContainerAnchoredRect, isMenuRelated } from './dom';
9
9
  export { findOptionByValue, getFieldValues, hasFieldValues, hasStaticAllowlist } from './fields';
10
10
  export { filterAndSort } from './filterSort';
11
11
  export { getCurrentValueTokenText, getValueFilterText } from './menuFilterText';
12
- export { getOperatorFromLabel, getOperatorLabel, isBetweenOperator, isMultiSelectOperator, isNoValueOperator, } from './operators';
12
+ export { getFieldOperators, getOperatorFromLabel, getOperatorLabel, isBetweenOperator, isBuildingComplete, isMultiSelectOperator, isNoValueOperator, isOperatorAllowedForField, isValueShapeCompatible, NO_VALUE_PLACEHOLDER, nextBuildingMenu, } from './operators';
13
13
  export { type FilterParseError, isFilterParseError, parseExpression } from './parseExpression';
14
14
  export { serializeExpression } from './serializeExpression';
15
15
  export { createStatusCodeInputFilter, createStatusCodeNormalizer, createStatusCodeSerializer, createStatusCodeSuggestions, createStatusCodeValidator, } from './statusCode';
@@ -8,8 +8,8 @@ import { buildContainerAnchoredRect, isMenuRelated } from "./dom.js";
8
8
  import { findOptionByValue, getFieldValues, hasFieldValues, hasStaticAllowlist } from "./fields.js";
9
9
  import { filterAndSort } from "./filterSort.js";
10
10
  import { getCurrentValueTokenText, getValueFilterText } from "./menuFilterText.js";
11
- import { getOperatorFromLabel, getOperatorLabel, isBetweenOperator, isMultiSelectOperator, isNoValueOperator } from "./operators.js";
11
+ import { NO_VALUE_PLACEHOLDER, getFieldOperators, getOperatorFromLabel, getOperatorLabel, isBetweenOperator, isBuildingComplete, isMultiSelectOperator, isNoValueOperator, isOperatorAllowedForField, isValueShapeCompatible, nextBuildingMenu } from "./operators.js";
12
12
  import { isFilterParseError, parseExpression } from "./parseExpression/index.js";
13
13
  import { serializeExpression } from "./serializeExpression.js";
14
14
  import { createStatusCodeInputFilter, createStatusCodeNormalizer, createStatusCodeSerializer, createStatusCodeSuggestions, createStatusCodeValidator } from "./statusCode/index.js";
15
- export { CONNECTOR_ID_PATTERN, DATE_PRESETS, NO_VALUE_OPERATORS, OPERATORS_BY_TYPE, OPERATOR_LABELS, OPERATOR_LABELS_BY_TYPE, OPERATOR_SYMBOLS, QUERY_BAR_SELECTOR, VARIANT_LABELS, applyAcceptChar, applyFieldValueTransforms, applyKnownFieldHelpers, buildContainerAnchoredRect, chipIdToConditionIndex, createStatusCodeInputFilter, createStatusCodeNormalizer, createStatusCodeSerializer, createStatusCodeSuggestions, createStatusCodeValidator, filterAndSort, findChipSplitIndex, findOptionByValue, formatDateForChip, getCurrentValueTokenText, getDateDisplayLabel, getFieldValues, getKnownFieldSerializer, getOperatorFromLabel, getOperatorLabel, getValueFilterText, hasFieldValues, hasStaticAllowlist, isBetweenOperator, isDatePreset, isFilterParseError, isMenuRelated, isMultiSelectOperator, isNoValueOperator, parseExpression, serializeExpression };
15
+ export { CONNECTOR_ID_PATTERN, DATE_PRESETS, NO_VALUE_OPERATORS, NO_VALUE_PLACEHOLDER, OPERATORS_BY_TYPE, OPERATOR_LABELS, OPERATOR_LABELS_BY_TYPE, OPERATOR_SYMBOLS, QUERY_BAR_SELECTOR, VARIANT_LABELS, applyAcceptChar, applyFieldValueTransforms, applyKnownFieldHelpers, buildContainerAnchoredRect, chipIdToConditionIndex, createStatusCodeInputFilter, createStatusCodeNormalizer, createStatusCodeSerializer, createStatusCodeSuggestions, createStatusCodeValidator, filterAndSort, findChipSplitIndex, findOptionByValue, formatDateForChip, getCurrentValueTokenText, getDateDisplayLabel, getFieldOperators, getFieldValues, getKnownFieldSerializer, getOperatorFromLabel, getOperatorLabel, getValueFilterText, hasFieldValues, hasStaticAllowlist, isBetweenOperator, isBuildingComplete, isDatePreset, isFilterParseError, isMenuRelated, isMultiSelectOperator, isNoValueOperator, isOperatorAllowedForField, isValueShapeCompatible, nextBuildingMenu, parseExpression, serializeExpression };
@@ -1,4 +1,11 @@
1
- import type { FieldType, FilterOperator } from '../types';
1
+ import type { FieldMetadata, FieldType, FilterOperator, MenuState } from '../types';
2
+ /**
3
+ * Filler text shown in the value slot of no-value operator chips so every
4
+ * chip visually has three segments. Kept here next to `isNoValueOperator`
5
+ * so committed (buildChips) and building-preview (deriveAutocompleteValues)
6
+ * paths can never drift.
7
+ */
8
+ export declare const NO_VALUE_PLACEHOLDER = "\u2014";
2
9
  /**
3
10
  * Helper to get operator label for specific field type
4
11
  */
@@ -13,3 +20,27 @@ export declare const isMultiSelectOperator: (op: FilterOperator | null) => op is
13
20
  export declare const isNoValueOperator: (op: FilterOperator) => boolean;
14
21
  /** Check if operator is a between/range operator */
15
22
  export declare const isBetweenOperator: (op: FilterOperator | null) => boolean;
23
+ /** Get the list of operators a field allows (custom override or full type list) */
24
+ export declare const getFieldOperators: (field: FieldMetadata) => FilterOperator[];
25
+ /** Check whether an operator is supported by a field's type/override list */
26
+ export declare const isOperatorAllowedForField: (field: FieldMetadata, operator: FilterOperator) => boolean;
27
+ /**
28
+ * The menu that should open to continue building a chip from its current
29
+ * state. Returns null when the chip is fully built and nothing should open.
30
+ * Reused by the refocus path (useFocusManagement) and the main-input click
31
+ * path (useInputHandlers) so both stay in sync.
32
+ */
33
+ export declare const nextBuildingMenu: (field: FieldMetadata | null, operator: FilterOperator | null) => MenuState | null;
34
+ /**
35
+ * Decide whether the in-progress (field, operator, value) triple is fully
36
+ * built — i.e. has all the segments the chip needs. No-value operators
37
+ * (is_null / is_not_null) are complete without a value: the chip renders a
38
+ * value-placeholder in that slot so it still visually has three segments.
39
+ */
40
+ export declare const isBuildingComplete: (field: FieldMetadata | null, operator: FilterOperator | null, value: string | number | boolean | Array<string | number | boolean> | null | undefined) => boolean;
41
+ /**
42
+ * Check whether two operators handle values in compatible shapes
43
+ * (multi-select / between / no-value categories match), meaning a value
44
+ * preview built for `a` can be reused as-is for `b`.
45
+ */
46
+ export declare const isValueShapeCompatible: (a: FilterOperator | null, b: FilterOperator | null) => boolean;
@@ -1,4 +1,5 @@
1
- import { MULTI_SELECT_OPERATORS, NO_VALUE_OPERATORS, OPERATOR_LABELS, OPERATOR_LABELS_BY_TYPE } from "./constants.js";
1
+ import { MULTI_SELECT_OPERATORS, NO_VALUE_OPERATORS, OPERATORS_BY_TYPE, OPERATOR_LABELS, OPERATOR_LABELS_BY_TYPE } from "./constants.js";
2
+ const NO_VALUE_PLACEHOLDER = '—';
2
3
  const getOperatorLabel = (operator, fieldType)=>OPERATOR_LABELS_BY_TYPE[fieldType]?.[operator] ?? OPERATOR_LABELS[operator];
3
4
  const getOperatorFromLabel = (label, fieldType)=>{
4
5
  const typeLabels = OPERATOR_LABELS_BY_TYPE[fieldType];
@@ -10,4 +11,26 @@ const getOperatorFromLabel = (label, fieldType)=>{
10
11
  const isMultiSelectOperator = (op)=>null !== op && MULTI_SELECT_OPERATORS.includes(op);
11
12
  const isNoValueOperator = (op)=>NO_VALUE_OPERATORS.includes(op);
12
13
  const isBetweenOperator = (op)=>'between' === op;
13
- export { getOperatorFromLabel, getOperatorLabel, isBetweenOperator, isMultiSelectOperator, isNoValueOperator };
14
+ const getFieldOperators = (field)=>field.operators ?? OPERATORS_BY_TYPE[field.type].flat();
15
+ const isOperatorAllowedForField = (field, operator)=>getFieldOperators(field).includes(operator);
16
+ const nextBuildingMenu = (field, operator)=>{
17
+ if (!field) return 'field';
18
+ if (!operator) return 'operator';
19
+ return 'value';
20
+ };
21
+ const isBuildingComplete = (field, operator, value)=>{
22
+ if (!field || !operator) return false;
23
+ if (isNoValueOperator(operator)) return true;
24
+ if (null == value) return false;
25
+ if (Array.isArray(value)) return value.length > 0;
26
+ return '' !== value;
27
+ };
28
+ const isValueShapeCompatible = (a, b)=>{
29
+ if (a === b) return true;
30
+ if (null == a || null == b) return false;
31
+ if (isMultiSelectOperator(a) !== isMultiSelectOperator(b)) return false;
32
+ if (isBetweenOperator(a) !== isBetweenOperator(b)) return false;
33
+ if (isNoValueOperator(a) !== isNoValueOperator(b)) return false;
34
+ return true;
35
+ };
36
+ export { NO_VALUE_PLACEHOLDER, getFieldOperators, getOperatorFromLabel, getOperatorLabel, isBetweenOperator, isBuildingComplete, isMultiSelectOperator, isNoValueOperator, isOperatorAllowedForField, isValueShapeCompatible, nextBuildingMenu };
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.35.0",
3
- "generatedAt": "2026-05-15T08:59:28.441Z",
2
+ "version": "0.36.0",
3
+ "generatedAt": "2026-05-15T12:04:58.806Z",
4
4
  "components": [
5
5
  {
6
6
  "name": "Alert",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wallarm-org/design-system",
3
- "version": "0.36.0",
3
+ "version": "0.37.0-rc-feature-AS-970.1",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",