@wallarm-org/design-system 0.68.2 → 0.68.3-rc-feature-AS-1179.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 (19) hide show
  1. package/dist/components/FilterInput/FilterInputErrors/parseFilterInputErrors.js +9 -2
  2. package/dist/components/FilterInput/FilterInputField/FilterInputChip/ChipSearchInput.js +2 -1
  3. package/dist/components/FilterInput/FilterInputField/FilterInputChip/FilterInputChip.js +9 -6
  4. package/dist/components/FilterInput/FilterInputField/FilterInputChip/classes.d.ts +5 -1
  5. package/dist/components/FilterInput/FilterInputField/FilterInputChip/classes.js +1 -1
  6. package/dist/components/FilterInput/FilterInputField/FilterInputChip/constants.d.ts +7 -0
  7. package/dist/components/FilterInput/FilterInputField/FilterInputChip/constants.js +2 -1
  8. package/dist/components/FilterInput/FilterInputField/FilterInputChip/model/useSizerWidth.d.ts +5 -2
  9. package/dist/components/FilterInput/FilterInputField/FilterInputChip/model/useSizerWidth.js +5 -4
  10. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useBlurCommit.d.ts +8 -1
  11. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useBlurCommit.js +18 -2
  12. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.d.ts +5 -4
  13. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.js +38 -2
  14. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useResumeBuilding.d.ts +35 -0
  15. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useResumeBuilding.js +77 -0
  16. package/dist/components/FilterInput/hooks/useFilterInputExpression/buildChips.js +17 -4
  17. package/dist/components/FilterInput/hooks/useFilterInputExpression/useFilterInputExpression.js +5 -0
  18. package/dist/metadata/components.json +2 -2
  19. package/package.json +1 -1
@@ -13,9 +13,16 @@ const parseFilterInputErrors = (conditions, fields)=>{
13
13
  const field = fields.find((f)=>f.name === condition.field);
14
14
  const baseIsNoValue = null != condition.operator && isNoValueOperator(condition.operator);
15
15
  if (field?.pairedField && !baseIsNoValue && isPairValueMissing(condition)) errors.push(`${field.pairedField.label} is required`);
16
- if (!condition.error) continue;
17
16
  const label = field?.label || condition.field;
18
- switch(condition.error){
17
+ if (field && !field.pairedField && !condition.error) {
18
+ const operatorMissing = !condition.operator;
19
+ const valueMissing = !baseIsNoValue && !operatorMissing && (null == condition.value || '' === condition.value || Array.isArray(condition.value) && 0 === condition.value.length);
20
+ if (operatorMissing || valueMissing) {
21
+ errors.push(`${label} is incomplete`);
22
+ continue;
23
+ }
24
+ }
25
+ if (condition.error) switch(condition.error){
19
26
  case SEGMENT_VARIANT.attribute:
20
27
  errors.push(`Unknown field ${condition.field}`);
21
28
  break;
@@ -20,6 +20,7 @@ const ChipSearchInput = ()=>{
20
20
  ref: inputRef,
21
21
  type: "text",
22
22
  role: "combobox",
23
+ size: 1,
23
24
  "aria-expanded": menuOpen,
24
25
  "aria-invalid": error,
25
26
  "aria-label": "Filter value",
@@ -31,7 +32,7 @@ const ChipSearchInput = ()=>{
31
32
  style: {
32
33
  width: `${inputWidth}px`
33
34
  },
34
- className: "h-22 border-none bg-transparent p-0 text-sm shadow-none outline-none ring-0"
35
+ className: "h-22 min-w-0 border-none bg-transparent p-0 text-sm shadow-none outline-none ring-0"
35
36
  }),
36
37
  /*#__PURE__*/ jsx("span", {
37
38
  ref: sizerRef,
@@ -10,11 +10,13 @@ import { Segment } from "./Segment.js";
10
10
  import { SEGMENT_VARIANT } from "./segmentVariant.js";
11
11
  const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = false, valueParts, valueSeparator, errorValueIndices, building = false, disabled = false, pair, onRemove, onSegmentClick, onPairSegmentClick, className, ...props })=>{
12
12
  const interactive = !disabled;
13
- const hasError = !!error;
14
13
  const internalRef = useRef(null);
15
14
  const editing = useEditingContext();
16
15
  const isEditingThisChip = null != editing && null != editing.editingSegment && (building ? null == editing.editingChipId : null != chipId && editing.editingChipId === chipId);
17
16
  const activeSegment = isEditingThisChip ? editing.editingSegment : null;
17
+ const effectiveError = isEditingThisChip ? false : error;
18
+ const effectivePairError = isEditingThisChip ? void 0 : pair?.error;
19
+ const hasError = !!effectiveError || !!effectivePairError;
18
20
  const handleSegmentClick = useCallback((segment, e)=>{
19
21
  if (!onSegmentClick) return;
20
22
  if (activeSegment === segment) return;
@@ -62,7 +64,7 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
62
64
  interactive,
63
65
  disabled,
64
66
  building
65
- }), 'max-w-[320px]', className),
67
+ }), pair ? 'max-w-[380px]' : 'max-w-[320px]', className),
66
68
  "data-slot": "filter-input-condition-chip",
67
69
  ...building && {
68
70
  'data-building': ''
@@ -72,7 +74,7 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
72
74
  /*#__PURE__*/ jsx(Segment, {
73
75
  variant: SEGMENT_VARIANT.attribute,
74
76
  className: "shrink-0",
75
- error: true === error || error === SEGMENT_VARIANT.attribute,
77
+ error: true === effectiveError || effectiveError === SEGMENT_VARIANT.attribute,
76
78
  onClick: interactive ? (e)=>handleSegmentClick(SEGMENT_VARIANT.attribute, e) : void 0,
77
79
  onMouseDown: interactive && building ? handleSegmentMouseDown : void 0,
78
80
  ...segmentEditProps(SEGMENT_VARIANT.attribute),
@@ -88,8 +90,8 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
88
90
  }),
89
91
  (value || baseActiveSegment === SEGMENT_VARIANT.value) && /*#__PURE__*/ jsx(Segment, {
90
92
  variant: SEGMENT_VARIANT.value,
91
- className: "min-w-0",
92
- error: baseActiveSegment !== SEGMENT_VARIANT.value && (true === error || error === SEGMENT_VARIANT.value),
93
+ className: pair ? 'max-w-[90px] shrink-0' : 'min-w-0',
94
+ error: baseActiveSegment !== SEGMENT_VARIANT.value && (true === effectiveError || effectiveError === SEGMENT_VARIANT.value),
93
95
  valueParts: valueParts,
94
96
  valueSeparator: valueSeparator,
95
97
  errorValueIndices: errorValueIndices,
@@ -104,6 +106,7 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
104
106
  /*#__PURE__*/ jsx(Segment, {
105
107
  variant: SEGMENT_VARIANT.attribute,
106
108
  className: "shrink-0",
109
+ onClick: interactive && pair.error ? (e)=>handlePairSegmentClick(SEGMENT_VARIANT.value, e) : void 0,
107
110
  children: pair.attribute
108
111
  }),
109
112
  (pair.operator || pairActiveSegment === SEGMENT_VARIANT.operator) && /*#__PURE__*/ jsx(Segment, {
@@ -116,7 +119,7 @@ const FilterInputChip = ({ ref, chipId, attribute, operator, value, error = fals
116
119
  (null != pair.value || pairActiveSegment === SEGMENT_VARIANT.value) && /*#__PURE__*/ jsx(Segment, {
117
120
  variant: SEGMENT_VARIANT.value,
118
121
  className: "min-w-0",
119
- error: pairActiveSegment !== SEGMENT_VARIANT.value && (true === pair.error || pair.error === SEGMENT_VARIANT.value),
122
+ error: pairActiveSegment !== SEGMENT_VARIANT.value && (true === effectivePairError || effectivePairError === SEGMENT_VARIANT.value),
120
123
  onClick: interactive ? (e)=>handlePairSegmentClick('value', e) : void 0,
121
124
  ...segmentEditProps(SEGMENT_VARIANT.value, 1),
122
125
  children: pair.value ?? ''
@@ -12,7 +12,11 @@ export declare const segmentTextVariants: (props?: ({
12
12
  variant?: "value" | "operator" | "attribute" | null | undefined;
13
13
  error?: boolean | null | undefined;
14
14
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
15
- /** Remove button styles — hidden by default, shown on chip hover or button focus */
15
+ /** Remove button styles — hidden by default, shown on chip hover or button focus.
16
+ * It is `opacity-0` until hover/focus, but sits over the chip's right edge and the
17
+ * field area just past it; `pointer-events-none` while invisible stops it from
18
+ * swallowing clicks meant for the chip/input and silently deleting the chip
19
+ * (AS-1179). Hover/focus restore both visibility and clickability. */
16
20
  export declare const removeButtonVariants: (props?: ({
17
21
  error?: boolean | null | undefined;
18
22
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
@@ -68,7 +68,7 @@ const segmentTextVariants = cva('truncate text-sm', {
68
68
  error: false
69
69
  }
70
70
  });
71
- const removeButtonVariants = cva(`absolute -right-[13px] top-[-1px] bottom-[-1px] flex items-center justify-center p-0 cursor-pointer w-[18px] border border-solid border-l-0 rounded-r-8 opacity-0 group-hover/chip:opacity-100 focus:opacity-100 transition-opacity ${hiddenWhenSelected}`, {
71
+ const removeButtonVariants = cva(`absolute -right-[13px] top-[-1px] bottom-[-1px] flex items-center justify-center p-0 cursor-pointer w-[18px] border border-solid border-l-0 rounded-r-8 opacity-0 pointer-events-none group-hover/chip:opacity-100 group-hover/chip:pointer-events-auto focus:opacity-100 focus:pointer-events-auto transition-opacity ${hiddenWhenSelected}`, {
72
72
  variants: {
73
73
  error: {
74
74
  true: 'border-border-danger bg-bg-light-danger text-text-danger',
@@ -1,5 +1,12 @@
1
1
  /** Minimum input width in px */
2
2
  export declare const MIN_INPUT_WIDTH = 4;
3
+ /**
4
+ * Maximum input width in px. Caps the content-sized edit/search input so a long
5
+ * value can't grow the input past the chip (`max-w-[320px]`) and spill its text
6
+ * outside the chip border. Beyond this width the input scrolls its content
7
+ * natively instead of widening.
8
+ */
9
+ export declare const MAX_INPUT_WIDTH = 280;
3
10
  /** Extra pixels added to measured width to prevent text clipping */
4
11
  export declare const WIDTH_OFFSET = 1;
5
12
  /** Approximate width of a single character in px (text-sm fallback) */
@@ -1,4 +1,5 @@
1
1
  const MIN_INPUT_WIDTH = 4;
2
+ const MAX_INPUT_WIDTH = 280;
2
3
  const WIDTH_OFFSET = 1;
3
4
  const CHAR_WIDTH_PX = 8;
4
- export { CHAR_WIDTH_PX, MIN_INPUT_WIDTH, WIDTH_OFFSET };
5
+ export { CHAR_WIDTH_PX, MAX_INPUT_WIDTH, MIN_INPUT_WIDTH, WIDTH_OFFSET };
@@ -6,11 +6,14 @@ interface UseSizerWidthOptions {
6
6
  text: string;
7
7
  /** Minimum width floor (defaults to MIN_INPUT_WIDTH) */
8
8
  minWidth?: number;
9
+ /** Maximum width ceiling (defaults to MAX_INPUT_WIDTH) */
10
+ maxWidth?: number;
9
11
  }
10
12
  /**
11
13
  * Measures input width from a hidden sizer span.
12
14
  * Falls back to character count * CHAR_WIDTH_PX when the DOM element is unavailable.
13
- * Returns the measured width + WIDTH_OFFSET to prevent text clipping.
15
+ * Returns the measured width + WIDTH_OFFSET to prevent text clipping, clamped to
16
+ * [minWidth, maxWidth] so a long value can't widen the input past the chip.
14
17
  */
15
- export declare const useSizerWidth: ({ sizerRef, text, minWidth, }: UseSizerWidthOptions) => number;
18
+ export declare const useSizerWidth: ({ sizerRef, text, minWidth, maxWidth, }: UseSizerWidthOptions) => number;
16
19
  export {};
@@ -1,13 +1,14 @@
1
1
  import { useEffect, useState } from "react";
2
- import { CHAR_WIDTH_PX, MIN_INPUT_WIDTH, WIDTH_OFFSET } from "../constants.js";
3
- const useSizerWidth = ({ sizerRef, text, minWidth = MIN_INPUT_WIDTH })=>{
2
+ import { CHAR_WIDTH_PX, MAX_INPUT_WIDTH, MIN_INPUT_WIDTH, WIDTH_OFFSET } from "../constants.js";
3
+ const useSizerWidth = ({ sizerRef, text, minWidth = MIN_INPUT_WIDTH, maxWidth = MAX_INPUT_WIDTH })=>{
4
4
  const [width, setWidth] = useState(minWidth);
5
5
  useEffect(()=>{
6
6
  const sizerWidth = sizerRef.current?.getBoundingClientRect().width ?? text.length * CHAR_WIDTH_PX;
7
- setWidth(Math.max(minWidth, sizerWidth));
7
+ setWidth(Math.min(maxWidth, Math.max(minWidth, sizerWidth)));
8
8
  }, [
9
9
  text,
10
- minWidth
10
+ minWidth,
11
+ maxWidth
11
12
  ]);
12
13
  return width + WIDTH_OFFSET;
13
14
  };
@@ -1,5 +1,6 @@
1
1
  import type { RefObject } from 'react';
2
2
  import type { FieldMetadata, FilterOperator, UpsertCondition } from '../../types';
3
+ import type { BuildingBase } from './useAutocompleteState';
3
4
  interface UseBlurCommitDeps {
4
5
  selectedField: FieldMetadata | null;
5
6
  selectedOperator: FilterOperator | null;
@@ -9,6 +10,12 @@ interface UseBlurCommitDeps {
9
10
  handleCustomValueCommit: (text: string) => void;
10
11
  upsertCondition: UpsertCondition;
11
12
  resetState: () => void;
13
+ /** Which triplet is being built: 0 = base, 1 = paired second. */
14
+ buildingSide: 0 | 1;
15
+ setBuildingSide: (side: 0 | 1) => void;
16
+ /** Base triplet stashed while building a paired chip's second value. */
17
+ buildingBase: BuildingBase | null;
18
+ setBuildingBase: (base: BuildingBase | null) => void;
12
19
  /** Indirection ref breaking the useMenuFlow ↔ useBlurCommit cycle. */
13
20
  commitBuildingOnBlurRef: RefObject<() => boolean>;
14
21
  /** Same indirection for force-commit (area-click → incomplete becomes error). */
@@ -20,7 +27,7 @@ interface UseBlurCommitDeps {
20
27
  * editable. Re-entry-guarded: refs cleared synchronously so concurrent callers
21
28
  * (multiple onOpenChange + blur in one tick) don't create duplicate chips.
22
29
  */
23
- export declare const useBlurCommit: ({ selectedField, selectedOperator, inputText, editingChipId, effectiveInsertIndexRef, handleCustomValueCommit, upsertCondition, resetState, commitBuildingOnBlurRef, commitBuildingForceRef, }: UseBlurCommitDeps) => {
30
+ export declare const useBlurCommit: ({ selectedField, selectedOperator, inputText, editingChipId, effectiveInsertIndexRef, handleCustomValueCommit, upsertCondition, resetState, buildingSide, setBuildingSide, buildingBase, setBuildingBase, commitBuildingOnBlurRef, commitBuildingForceRef, }: UseBlurCommitDeps) => {
24
31
  commitBuildingOnBlur: () => boolean;
25
32
  hasIncompleteBuilding: () => boolean;
26
33
  };
@@ -1,14 +1,18 @@
1
1
  import { useCallback, useLayoutEffect, useRef } from "react";
2
2
  import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
3
3
  import { isBuildingComplete, isNoValueOperator } from "../../lib/index.js";
4
- const useBlurCommit = ({ selectedField, selectedOperator, inputText, editingChipId, effectiveInsertIndexRef, handleCustomValueCommit, upsertCondition, resetState, commitBuildingOnBlurRef, commitBuildingForceRef })=>{
4
+ const useBlurCommit = ({ selectedField, selectedOperator, inputText, editingChipId, effectiveInsertIndexRef, handleCustomValueCommit, upsertCondition, resetState, buildingSide, setBuildingSide, buildingBase, setBuildingBase, commitBuildingOnBlurRef, commitBuildingForceRef })=>{
5
5
  const selectedFieldRef = useRef(selectedField);
6
6
  const selectedOperatorRef = useRef(selectedOperator);
7
7
  const inputTextRef = useRef(inputText);
8
+ const buildingSideRef = useRef(buildingSide);
9
+ const buildingBaseRef = useRef(buildingBase);
8
10
  useLayoutEffect(()=>{
9
11
  selectedFieldRef.current = selectedField;
10
12
  selectedOperatorRef.current = selectedOperator;
11
13
  inputTextRef.current = inputText;
14
+ buildingSideRef.current = buildingSide;
15
+ buildingBaseRef.current = buildingBase;
12
16
  });
13
17
  const committingRef = useRef(false);
14
18
  const commitBuildingOnBlur = useCallback(()=>{
@@ -68,6 +72,16 @@ const useBlurCommit = ({ selectedField, selectedOperator, inputText, editingChip
68
72
  handleCustomValueCommit(text);
69
73
  return true;
70
74
  }
75
+ if (1 === buildingSideRef.current && buildingBaseRef.current) {
76
+ const base = buildingBaseRef.current;
77
+ upsertCondition(base.field, base.operator, base.value, null, effectiveInsertIndexRef.current);
78
+ const pairError = operator ? SEGMENT_VARIANT.value : true;
79
+ upsertCondition(field, operator ?? void 0, null, void 0, void 0, pairError, void 0, 1);
80
+ setBuildingBase(null);
81
+ setBuildingSide(0);
82
+ resetState();
83
+ return true;
84
+ }
71
85
  if (isBuildingComplete(field, operator, null)) {
72
86
  upsertCondition(field, operator, null, void 0, effectiveInsertIndexRef.current);
73
87
  resetState();
@@ -85,7 +99,9 @@ const useBlurCommit = ({ selectedField, selectedOperator, inputText, editingChip
85
99
  handleCustomValueCommit,
86
100
  upsertCondition,
87
101
  resetState,
88
- effectiveInsertIndexRef
102
+ effectiveInsertIndexRef,
103
+ setBuildingBase,
104
+ setBuildingSide
89
105
  ]);
90
106
  useLayoutEffect(()=>{
91
107
  commitBuildingForceRef.current = commitBuildingForce;
@@ -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, UpsertCondition } from '../../types';
3
4
  interface UseFilterInputAutocompleteOptions {
4
5
  fields: FieldMetadata[];
@@ -49,10 +50,10 @@ export declare const useFilterInputAutocomplete: ({ fields, conditions, chips, u
49
50
  handleMenuDiscard: () => void;
50
51
  /** Hard reset for paste/clipboard flows — scraps in-progress building. */
51
52
  resetAutocompleteState: (continueBuilding?: boolean) => void;
52
- handleChipClick: (chipId: string, segment: import("../../FilterInputField").ChipSegment, anchorEl: HTMLElement) => void;
53
- handlePairChipClick: (chipId: string, segment: import("../../FilterInputField").ChipSegment, anchorEl: HTMLElement) => void;
54
- handleBuildingChipClick: (segment: import("../../FilterInputField").ChipSegment, anchorEl: HTMLElement) => void;
55
- switchEditSegment: (targetSegment: import("../../FilterInputField").ChipSegment) => boolean;
53
+ handleChipClick: (chipId: string, segment: ChipSegment, anchorEl: HTMLElement) => void;
54
+ handlePairChipClick: (chipId: string, segment: ChipSegment, anchorEl: HTMLElement) => void;
55
+ handleBuildingChipClick: (segment: ChipSegment, anchorEl: HTMLElement) => void;
56
+ switchEditSegment: (targetSegment: ChipSegment) => boolean;
56
57
  removeEditingChip: () => void;
57
58
  handleConnectorChange: (connectorId: string, value: "and" | "or") => void;
58
59
  handleChipRemove: (chipId: string) => void;
@@ -1,3 +1,4 @@
1
+ import { useCallback } from "react";
1
2
  import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
2
3
  import { useDateRange } from "../../FilterInputMenu/FilterInputDateValueMenu/hooks.js";
3
4
  import { deriveAutocompleteValues } from "./lib/index.js";
@@ -11,6 +12,7 @@ import { useInputHandlers } from "./useInputHandlers.js";
11
12
  import { useMenuFlow } from "./useMenuFlow/index.js";
12
13
  import { useMenuPositioning } from "./useMenuPositioning.js";
13
14
  import { useResetState } from "./useResetState.js";
15
+ import { useResumeBuilding } from "./useResumeBuilding.js";
14
16
  import { useSegmentEditFlow } from "./useSegmentEditFlow.js";
15
17
  const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition, removeCondition, removeConditionAtIndex, clearAll, setConnectorValue, containerRef, buildingChipRef, inputRef })=>{
16
18
  const state = useAutocompleteState({
@@ -123,6 +125,10 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
123
125
  handleCustomValueCommit,
124
126
  upsertCondition,
125
127
  resetState,
128
+ buildingSide,
129
+ setBuildingSide,
130
+ buildingBase,
131
+ setBuildingBase,
126
132
  commitBuildingOnBlurRef,
127
133
  commitBuildingForceRef
128
134
  });
@@ -170,6 +176,36 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
170
176
  setMenuState,
171
177
  setMenuAnchor
172
178
  });
179
+ const tryResumeBuilding = useResumeBuilding({
180
+ conditions,
181
+ fields,
182
+ removeConditionAtIndex,
183
+ setInsertIndex,
184
+ setBuildingBase,
185
+ setBuildingSide,
186
+ setSelectedField,
187
+ setSelectedOperator,
188
+ setBuildingMultiValue,
189
+ setInputText,
190
+ setMenuState,
191
+ resetMenuAnchor,
192
+ clearEditing: editing.clearEditing,
193
+ inputRef
194
+ });
195
+ const handleChipClick = useCallback((chipId, segment, anchorEl)=>{
196
+ if (tryResumeBuilding(chipId)) return;
197
+ editing.handleChipClick(chipId, segment, anchorEl);
198
+ }, [
199
+ tryResumeBuilding,
200
+ editing.handleChipClick
201
+ ]);
202
+ const handlePairChipClick = useCallback((chipId, segment, anchorEl)=>{
203
+ if (tryResumeBuilding(chipId)) return;
204
+ editing.handlePairChipClick(chipId, segment, anchorEl);
205
+ }, [
206
+ tryResumeBuilding,
207
+ editing.handlePairChipClick
208
+ ]);
173
209
  const { isBuilding, buildingChipData, editingMultiValues, editingSingleValue, editingDateRange } = deriveAutocompleteValues({
174
210
  editingChipId: editing.editingChipId,
175
211
  selectedField,
@@ -204,8 +240,8 @@ const useFilterInputAutocomplete = ({ fields, conditions, chips, upsertCondition
204
240
  handleMenuClose,
205
241
  handleMenuDiscard,
206
242
  resetAutocompleteState: resetState,
207
- handleChipClick: editing.handleChipClick,
208
- handlePairChipClick: editing.handlePairChipClick,
243
+ handleChipClick,
244
+ handlePairChipClick,
209
245
  handleBuildingChipClick,
210
246
  switchEditSegment,
211
247
  removeEditingChip,
@@ -0,0 +1,35 @@
1
+ import type { RefObject } from 'react';
2
+ import type { Condition, FieldMetadata, FilterOperator, MenuState } from '../../types';
3
+ import type { BuildingBase } from './useAutocompleteState';
4
+ interface UseResumeBuildingDeps {
5
+ conditions: Condition[];
6
+ fields: FieldMetadata[];
7
+ removeConditionAtIndex: (index: number) => void;
8
+ setInsertIndex: (index: number | null) => void;
9
+ setBuildingBase: (base: BuildingBase | null) => void;
10
+ setBuildingSide: (side: 0 | 1) => void;
11
+ setSelectedField: (field: FieldMetadata | null) => void;
12
+ setSelectedOperator: (op: FilterOperator | null) => void;
13
+ setBuildingMultiValue: (val: string | undefined) => void;
14
+ setInputText: (text: string) => void;
15
+ setMenuState: (state: MenuState) => void;
16
+ resetMenuAnchor: () => void;
17
+ /** Exit any inline-edit so the resumed flow is a clean building chip. */
18
+ clearEditing: () => void;
19
+ inputRef: RefObject<HTMLInputElement | null>;
20
+ }
21
+ /**
22
+ * Resume building a committed-but-incomplete chip instead of inline-editing it.
23
+ *
24
+ * A chip left half-built (e.g. force-committed via an area click) keeps its
25
+ * triplets but lacks a required value. Clicking it converts the committed
26
+ * condition back into a building chip and reopens the menu at the first missing
27
+ * step, so the normal building cascade carries on — including the paired
28
+ * second triplet for two-step fields. This is what makes "press anywhere → red
29
+ * chip → click to continue" work for paired fields (AS-1179).
30
+ *
31
+ * Returns a predicate: `true` when it took over (caller must not also run the
32
+ * inline-edit handler), `false` when the chip is complete (edit it normally).
33
+ */
34
+ export declare const useResumeBuilding: ({ conditions, fields, removeConditionAtIndex, setInsertIndex, setBuildingBase, setBuildingSide, setSelectedField, setSelectedOperator, setBuildingMultiValue, setInputText, setMenuState, resetMenuAnchor, clearEditing, inputRef, }: UseResumeBuildingDeps) => (chipId: string) => boolean;
35
+ export {};
@@ -0,0 +1,77 @@
1
+ import { useCallback } from "react";
2
+ import { chipIdToConditionIndex, isNoValueOperator } from "../../lib/index.js";
3
+ const isEmptyValue = (value)=>null == value || '' === value || Array.isArray(value) && 0 === value.length;
4
+ const useResumeBuilding = ({ conditions, fields, removeConditionAtIndex, setInsertIndex, setBuildingBase, setBuildingSide, setSelectedField, setSelectedOperator, setBuildingMultiValue, setInputText, setMenuState, resetMenuAnchor, clearEditing, inputRef })=>useCallback((chipId)=>{
5
+ const idx = chipIdToConditionIndex(chipId);
6
+ if (null === idx) return false;
7
+ const condition = conditions[idx];
8
+ if (!condition || condition.disabled) return false;
9
+ const field = fields.find((f)=>f.name === condition.field);
10
+ if (!field) return false;
11
+ const baseOperatorMissing = !condition.operator;
12
+ const baseOperatorTakesValue = null != condition.operator && !isNoValueOperator(condition.operator);
13
+ const baseValueMissing = baseOperatorTakesValue && isEmptyValue(condition.value);
14
+ const startBuilding = (params)=>{
15
+ clearEditing();
16
+ removeConditionAtIndex(idx);
17
+ setInsertIndex(idx);
18
+ setBuildingBase(params.base);
19
+ setBuildingSide(params.side);
20
+ setSelectedField(params.selectedField);
21
+ setSelectedOperator(params.selectedOperator);
22
+ setBuildingMultiValue(void 0);
23
+ setInputText('');
24
+ resetMenuAnchor();
25
+ setMenuState(params.menuState);
26
+ requestAnimationFrame(()=>inputRef.current?.focus());
27
+ };
28
+ if (baseOperatorMissing) return false;
29
+ if (baseValueMissing) {
30
+ if (!field.pairedField) return false;
31
+ startBuilding({
32
+ base: null,
33
+ side: 0,
34
+ selectedField: field,
35
+ selectedOperator: condition.operator ?? null,
36
+ menuState: 'value'
37
+ });
38
+ return true;
39
+ }
40
+ if (field.pairedField && !isNoValueOperator(condition.operator)) {
41
+ const pairOperator = condition.pair?.operator;
42
+ const pairOperatorMissing = !pairOperator;
43
+ const pairOperatorTakesValue = null != pairOperator && !isNoValueOperator(pairOperator);
44
+ const pairValueMissing = pairOperatorTakesValue && isEmptyValue(condition.pair?.value);
45
+ if (pairOperatorMissing || pairValueMissing) {
46
+ startBuilding({
47
+ base: {
48
+ field,
49
+ operator: condition.operator,
50
+ value: condition.value
51
+ },
52
+ side: 1,
53
+ selectedField: field.pairedField,
54
+ selectedOperator: pairOperator ?? null,
55
+ menuState: pairOperatorMissing ? 'operator' : 'value'
56
+ });
57
+ return true;
58
+ }
59
+ }
60
+ return false;
61
+ }, [
62
+ conditions,
63
+ fields,
64
+ removeConditionAtIndex,
65
+ setInsertIndex,
66
+ setBuildingBase,
67
+ setBuildingSide,
68
+ setSelectedField,
69
+ setSelectedOperator,
70
+ setBuildingMultiValue,
71
+ setInputText,
72
+ setMenuState,
73
+ resetMenuAnchor,
74
+ clearEditing,
75
+ inputRef
76
+ ]);
77
+ export { useResumeBuilding };
@@ -84,23 +84,36 @@ const buildMultiValueChip = (baseChip, condition, field, fields, chipError)=>{
84
84
  };
85
85
  };
86
86
  const buildPairChip = (condition, field, fields)=>{
87
- if (!condition.pair || !field?.pairedField) return;
87
+ if (!field?.pairedField) return;
88
+ if (null != condition.operator && isNoValueOperator(condition.operator)) return;
88
89
  const pf = field.pairedField;
90
+ if (!condition.pair) {
91
+ const baseComplete = null != condition.operator && null != condition.value && '' !== condition.value && !(Array.isArray(condition.value) && 0 === condition.value.length);
92
+ if (!baseComplete) return;
93
+ return {
94
+ attribute: pf.label || pf.name,
95
+ value: '',
96
+ error: SEGMENT_VARIANT.value
97
+ };
98
+ }
89
99
  const { operator, value, error } = condition.pair;
90
100
  const displayValue = operator && isNoValueOperator(operator) ? NO_VALUE_PLACEHOLDER : resolveValueLabel(value, pf, fields) ?? String(value ?? '');
101
+ const valueRequiredButMissing = !(operator && isNoValueOperator(operator)) && (null == value || '' === value || Array.isArray(value) && 0 === value.length);
102
+ const pairError = error || (valueRequiredButMissing ? SEGMENT_VARIANT.value : void 0);
91
103
  return {
92
104
  attribute: pf.label || pf.name,
93
105
  operator: operator ? getOperatorLabel(operator, pf.type || DEFAULT_FIELD_TYPE) : void 0,
94
106
  value: displayValue,
95
- ...error && {
96
- error
107
+ ...pairError && {
108
+ error: pairError
97
109
  }
98
110
  };
99
111
  };
100
112
  const makeConditionChipBase = (i, conditions, fields, error)=>{
101
113
  const condition = conditions[i];
102
114
  if (!condition) return makeEmptyChip(i, error);
103
- const chipError = condition.error || (error ? true : void 0);
115
+ const incompleteError = condition.operator ? isNoValueOperator(condition.operator) ? void 0 : null == condition.value || '' === condition.value || Array.isArray(condition.value) && 0 === condition.value.length ? SEGMENT_VARIANT.value : void 0 : true;
116
+ const chipError = condition.error || (error ? true : void 0) || incompleteError;
104
117
  const field = fields.find((f)=>f.name === condition.field);
105
118
  const baseChip = buildBaseChip(i, condition, field);
106
119
  if (condition.operator && isNoValueOperator(condition.operator)) return {
@@ -155,6 +155,11 @@ const useFilterInputExpression = ({ fields, value, onChange, error, externalErro
155
155
  return;
156
156
  }
157
157
  const condition = buildCondition(field, operator, val, error, dateOrigin);
158
+ if (editingChipId) {
159
+ const idx = chipIdToConditionIndex(editingChipId);
160
+ const prevCondition = null !== idx ? prev.conditions[idx] : void 0;
161
+ if (prevCondition?.pair && prevCondition.field === condition.field) condition.pair = prevCondition.pair;
162
+ }
158
163
  const newConditions = applyCondition(prev.conditions, condition, editingChipId, atIndex);
159
164
  const newConnectors = addConnectorIfNeeded(prev.connectors, newConditions.length, editingChipId, atIndex, prev.conditions.length);
160
165
  applyState({
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.68.1",
3
- "generatedAt": "2026-06-29T10:19:12.580Z",
2
+ "version": "0.68.2",
3
+ "generatedAt": "2026-06-29T12:17:10.042Z",
4
4
  "components": [
5
5
  {
6
6
  "name": "Accordion",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wallarm-org/design-system",
3
- "version": "0.68.2",
3
+ "version": "0.68.3-rc-feature-AS-1179.1",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",