@wallarm-org/design-system 0.29.2-rc-feature-AS-882.1 → 0.29.2

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.
@@ -1,5 +1,5 @@
1
1
  import type { RefObject } from 'react';
2
- import type { ChipErrorSegment, FieldMetadata, FilterOperator } from '../../types';
2
+ import type { FieldMetadata, FilterOperator, UpsertCondition } from '../../types';
3
3
  interface UseBlurCommitDeps {
4
4
  selectedField: FieldMetadata | null;
5
5
  selectedOperator: FilterOperator | null;
@@ -7,7 +7,7 @@ interface UseBlurCommitDeps {
7
7
  editingChipId: string | null;
8
8
  effectiveInsertIndexRef: RefObject<number>;
9
9
  handleCustomValueCommit: (text: string) => void;
10
- upsertCondition: (field: FieldMetadata, operator: FilterOperator | undefined, val: string | number | boolean | null | Array<string | number | boolean>, editingChipId?: string | null, atIndex?: number, error?: ChipErrorSegment, dateOrigin?: 'relative' | 'absolute') => void;
10
+ upsertCondition: UpsertCondition;
11
11
  resetState: () => void;
12
12
  /**
13
13
  * Indirection ref consumed by useMenuFlow to break the circular dependency
@@ -6,22 +6,29 @@ const useBlurCommit = ({ selectedField, selectedOperator, inputText, editingChip
6
6
  selectedOperatorRef.current = selectedOperator;
7
7
  const inputTextRef = useRef(inputText);
8
8
  inputTextRef.current = inputText;
9
+ const committingRef = useRef(false);
9
10
  const commitBuildingOnBlur = useCallback(()=>{
11
+ if (committingRef.current) return false;
10
12
  const field = selectedFieldRef.current;
11
13
  const operator = selectedOperatorRef.current;
12
14
  const text = inputTextRef.current.trim();
13
15
  if (!field) return false;
14
16
  if (editingChipId) return false;
15
- selectedFieldRef.current = null;
16
- selectedOperatorRef.current = null;
17
- inputTextRef.current = '';
18
- if (operator && text) {
19
- handleCustomValueCommit(text);
17
+ committingRef.current = true;
18
+ try {
19
+ selectedFieldRef.current = null;
20
+ selectedOperatorRef.current = null;
21
+ inputTextRef.current = '';
22
+ if (operator && text) {
23
+ handleCustomValueCommit(text);
24
+ return true;
25
+ }
26
+ upsertCondition(field, operator ?? void 0, null, void 0, effectiveInsertIndexRef.current, true);
27
+ resetState();
20
28
  return true;
29
+ } finally{
30
+ committingRef.current = false;
21
31
  }
22
- upsertCondition(field, operator ?? void 0, null, void 0, effectiveInsertIndexRef.current, true);
23
- resetState();
24
- return true;
25
32
  }, [
26
33
  editingChipId,
27
34
  handleCustomValueCommit,
@@ -1,6 +1,6 @@
1
1
  import type { RefObject } from 'react';
2
2
  import { type ChipSegment } from '../../FilterInputField/FilterInputChip';
3
- import type { Condition, FieldMetadata, FilterInputChipData, FilterOperator, MenuState } from '../../types';
3
+ import type { Condition, FieldMetadata, FilterInputChipData, FilterOperator, MenuState, UpsertCondition } from '../../types';
4
4
  interface UseChipEditingOptions {
5
5
  conditions: Condition[];
6
6
  chips: FilterInputChipData[];
@@ -10,7 +10,7 @@ interface UseChipEditingOptions {
10
10
  setSelectedField: (field: FieldMetadata | null) => void;
11
11
  setSelectedOperator: (op: FilterOperator | null) => void;
12
12
  setMenuState: (state: MenuState) => void;
13
- upsertCondition: (field: FieldMetadata, operator: FilterOperator | undefined, val: string | number | boolean | null | Array<string | number | boolean>, editingChipId?: string | null) => void;
13
+ upsertCondition: UpsertCondition;
14
14
  }
15
15
  /**
16
16
  * Manages editing of existing filter chips.
@@ -1,10 +1,10 @@
1
1
  import type { RefObject } from 'react';
2
- import type { ChipErrorSegment, Condition, FieldMetadata, FilterInputChipData, FilterOperator, MenuState } from '../../types';
2
+ import type { Condition, FieldMetadata, FilterInputChipData, FilterOperator, MenuState, UpsertCondition } from '../../types';
3
3
  interface UseFilterInputAutocompleteOptions {
4
4
  fields: FieldMetadata[];
5
5
  conditions: Condition[];
6
6
  chips: FilterInputChipData[];
7
- upsertCondition: (field: FieldMetadata, operator: FilterOperator | undefined, val: string | number | boolean | null | Array<string | number | boolean>, editingChipId?: string | null, atIndex?: number, error?: ChipErrorSegment, dateOrigin?: 'relative' | 'absolute') => void;
7
+ upsertCondition: UpsertCondition;
8
8
  removeCondition: (chipId: string) => void;
9
9
  removeConditionAtIndex: (index: number) => void;
10
10
  clearAll: () => void;
@@ -58,7 +58,12 @@ const useFocusManagement = ({ menuState, isFocused, conditionsLength, inputText,
58
58
  const active = document.activeElement;
59
59
  if (active && !container.contains(active) && !isMenuRelated(active)) return;
60
60
  if (editingSegment) {
61
- const segmentInput = editingSegment === SEGMENT_VARIANT.attribute ? segmentAttributeInputRef.current : editingSegment === SEGMENT_VARIANT.operator ? segmentOperatorInputRef.current : editingSegment === SEGMENT_VARIANT.value ? segmentValueInputRef.current : null;
61
+ const segmentInputRefs = {
62
+ [SEGMENT_VARIANT.attribute]: segmentAttributeInputRef,
63
+ [SEGMENT_VARIANT.operator]: segmentOperatorInputRef,
64
+ [SEGMENT_VARIANT.value]: segmentValueInputRef
65
+ };
66
+ const segmentInput = segmentInputRefs[editingSegment]?.current ?? null;
62
67
  if (segmentInput && document.activeElement !== segmentInput) {
63
68
  segmentInput.focus();
64
69
  segmentInput.select();
@@ -1,6 +1,6 @@
1
1
  import type { RefObject } from 'react';
2
2
  import { type ChipSegment } from '../../FilterInputField/FilterInputChip';
3
- import type { ChipErrorSegment, Condition, FieldMetadata, FilterOperator, MenuState } from '../../types';
3
+ import type { Condition, FieldMetadata, FilterOperator, MenuState, UpsertCondition } from '../../types';
4
4
  interface MenuFlowDeps {
5
5
  editing: {
6
6
  editingChipId: string | null;
@@ -14,7 +14,7 @@ interface MenuFlowDeps {
14
14
  fields: FieldMetadata[];
15
15
  inputRef: RefObject<HTMLInputElement | null>;
16
16
  insertIndex: number;
17
- upsertCondition: (field: FieldMetadata, operator: FilterOperator | undefined, val: string | number | boolean | null | Array<string | number | boolean>, editingChipId?: string | null, atIndex?: number, error?: ChipErrorSegment, dateOrigin?: 'relative' | 'absolute') => void;
17
+ upsertCondition: UpsertCondition;
18
18
  conditions: Condition[];
19
19
  resetState: (continueBuilding?: boolean) => void;
20
20
  /** Try to commit the building chip on menu close. Returns true if committed. */
@@ -21,11 +21,22 @@ interface UseResetStateDeps {
21
21
  /**
22
22
  * Resets all autocomplete state and conditionally returns focus to the main input.
23
23
  *
24
- * Focus restoration policy: only refocus when activeElement still "belongs" to us
25
- * inside container, inside a FilterInput-owned menu, or `document.body`. The
26
- * body case here means activeElement just dropped because a chip/menu was removed
27
- * on re-render keep focus in the input. Contrast with useFocusManagement's rAF
28
- * guard, which treats body-focus as outside. AS-882.
24
+ * ── Body-focus policy (READ THIS) ─────────────────────────────────────────────
25
+ * `document.activeElement === document.body` is treated **as "stayed inside"** here
26
+ * i.e. we DO refocus the main input. The reasoning: state mutations in
27
+ * `doReset()` (e.g. `editing.clearEditing()`) can synchronously unmount the
28
+ * element that previously had focus (a chip's inline input, a menu item), and
29
+ * the browser drops focus to body in that case. We want to honor "user is still
30
+ * working in the FilterInput" intent and put the caret back in the input.
31
+ *
32
+ * The opposite policy lives in `useFocusManagement.ts`'s rAF effect: there,
33
+ * body-focus means "user clicked outside" (e.g. tenant switcher) and we MUST
34
+ * NOT recapture. The two policies coexist because the triggers are different
35
+ * (state-driven re-render vs. genuine outside click).
36
+ *
37
+ * If you find yourself unifying these — make sure the "DOM dropped focus on
38
+ * re-render" case still refocuses, otherwise resetState during commit chains
39
+ * breaks. AS-882.
29
40
  */
30
41
  export declare const useResetState: ({ editing, dateRange, containerRef, inputRef, resetMenuOffset, setInputText, setSelectedField, setSelectedOperator, setBuildingMultiValue, setInsertIndex, setInsertAfterConnector, setMenuState, }: UseResetStateDeps) => (continueBuilding?: boolean) => void;
31
42
  export {};
@@ -1,7 +1,8 @@
1
1
  import { useCallback } from "react";
2
2
  import { flushSync } from "react-dom";
3
3
  import { isMenuRelated } from "../../lib/index.js";
4
- const useResetState = ({ editing, dateRange, containerRef, inputRef, resetMenuOffset, setInputText, setSelectedField, setSelectedOperator, setBuildingMultiValue, setInsertIndex, setInsertAfterConnector, setMenuState })=>useCallback((continueBuilding = false)=>{
4
+ const useResetState = ({ editing, dateRange, containerRef, inputRef, resetMenuOffset, setInputText, setSelectedField, setSelectedOperator, setBuildingMultiValue, setInsertIndex, setInsertAfterConnector, setMenuState })=>{
5
+ const resetState = useCallback((continueBuilding = false)=>{
5
6
  const doReset = ()=>{
6
7
  setInputText('');
7
8
  setSelectedField(null);
@@ -35,4 +36,6 @@ const useResetState = ({ editing, dateRange, containerRef, inputRef, resetMenuOf
35
36
  setInsertAfterConnector,
36
37
  setMenuState
37
38
  ]);
39
+ return resetState;
40
+ };
38
41
  export { useResetState };
@@ -1,5 +1,5 @@
1
1
  import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
2
- import { getDateDisplayLabel, getOperatorLabel } from "../../lib/index.js";
2
+ import { findOptionByValue, getDateDisplayLabel, getOperatorLabel } 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 = ' – ';
@@ -25,9 +25,7 @@ const makeEmptyChip = (i, error)=>({
25
25
  });
26
26
  const resolveDisplayValue = (condition, field)=>{
27
27
  const raw = String(condition.value ?? '');
28
- if (!field?.values) return raw;
29
- const opt = field.values.find((o)=>String(o.value) === raw);
30
- return opt?.label ?? raw;
28
+ return findOptionByValue(field, condition.value)?.label ?? raw;
31
29
  };
32
30
  const buildBaseChip = (i, condition, field)=>({
33
31
  id: chipId(i),
@@ -66,10 +64,7 @@ const buildDateChip = (baseChip, condition, chipError)=>{
66
64
  };
67
65
  const buildMultiValueChip = (baseChip, condition, field, chipError)=>{
68
66
  const values = condition.value;
69
- const valueParts = values.map((v)=>{
70
- const key = String(v);
71
- return field?.values?.find((opt)=>String(opt.value) === key)?.label ?? key;
72
- });
67
+ const valueParts = values.map((v)=>findOptionByValue(field, v)?.label ?? String(v));
73
68
  const invalidIndices = field ? getInvalidValueIndices(field, values) : [];
74
69
  return {
75
70
  ...baseChip,
@@ -1,4 +1,13 @@
1
1
  import type { FieldMetadata, FieldValueOption } from '../types';
2
+ /**
3
+ * Find an option in `field.values` matching the given value, using stringified
4
+ * comparison. Loose-match is required because parser/serializer round-trip
5
+ * coerces typed primitives to strings (e.g. integer field value `5` → string
6
+ * `"5"` after `(field = "5")` parses back), and strict `===` would miss the
7
+ * canonical option. Returns undefined when there is no match or the field
8
+ * has no `values` allowlist.
9
+ */
10
+ export declare const findOptionByValue: (field: FieldMetadata | undefined, value: string | number | boolean | null | undefined) => FieldValueOption | undefined;
2
11
  /**
3
12
  * Get value options for a field.
4
13
  * Priority: `getSuggestions(inputText, context)` > `values` > `options` (converted to `{value, label}`).
@@ -1,3 +1,8 @@
1
+ const findOptionByValue = (field, value)=>{
2
+ if (!field?.values || null == value) return;
3
+ const key = String(value);
4
+ return field.values.find((opt)=>String(opt.value) === key);
5
+ };
1
6
  const getFieldValues = (field, inputText = '', context)=>{
2
7
  if (field.getSuggestions) return field.getSuggestions(inputText, context);
3
8
  const fromValues = field.values ?? [];
@@ -17,4 +22,4 @@ const hasStaticAllowlist = (field)=>{
17
22
  if ((field.values?.length ?? 0) > 0) return true;
18
23
  return (field.options?.length ?? 0) > 0;
19
24
  };
20
- export { getFieldValues, hasFieldValues, hasStaticAllowlist };
25
+ export { findOptionByValue, getFieldValues, hasFieldValues, hasStaticAllowlist };
@@ -6,7 +6,7 @@ export { applyKnownFieldHelpers } from './applyKnownFieldHelpers';
6
6
  export { chipIdToConditionIndex, findChipSplitIndex } from './conditions';
7
7
  export { CONNECTOR_ID_PATTERN, NO_VALUE_OPERATORS, OPERATOR_LABELS, OPERATOR_LABELS_BY_TYPE, OPERATOR_SYMBOLS, OPERATORS_BY_TYPE, QUERY_BAR_SELECTOR, VARIANT_LABELS, } from './constants';
8
8
  export { buildContainerAnchoredRect, isMenuRelated } from './dom';
9
- export { getFieldValues, hasFieldValues, hasStaticAllowlist } from './fields';
9
+ export { findOptionByValue, getFieldValues, hasFieldValues, hasStaticAllowlist } from './fields';
10
10
  export { filterAndSort } from './filterSort';
11
11
  export { getCurrentValueTokenText, getValueFilterText } from './menuFilterText';
12
12
  export { getOperatorFromLabel, getOperatorLabel, isBetweenOperator, isMultiSelectOperator, isNoValueOperator, } from './operators';
@@ -5,11 +5,11 @@ import { applyKnownFieldHelpers } from "./applyKnownFieldHelpers.js";
5
5
  import { chipIdToConditionIndex, findChipSplitIndex } from "./conditions.js";
6
6
  import { CONNECTOR_ID_PATTERN, NO_VALUE_OPERATORS, OPERATORS_BY_TYPE, OPERATOR_LABELS, OPERATOR_LABELS_BY_TYPE, OPERATOR_SYMBOLS, QUERY_BAR_SELECTOR, VARIANT_LABELS } from "./constants.js";
7
7
  import { buildContainerAnchoredRect, isMenuRelated } from "./dom.js";
8
- import { getFieldValues, hasFieldValues, hasStaticAllowlist } from "./fields.js";
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
11
  import { getOperatorFromLabel, getOperatorLabel, isBetweenOperator, isMultiSelectOperator, isNoValueOperator } 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, formatDateForChip, getCurrentValueTokenText, getDateDisplayLabel, getFieldValues, getOperatorFromLabel, getOperatorLabel, getValueFilterText, hasFieldValues, hasStaticAllowlist, isBetweenOperator, isDatePreset, isFilterParseError, isMenuRelated, isMultiSelectOperator, isNoValueOperator, parseExpression, serializeExpression };
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, getOperatorFromLabel, getOperatorLabel, getValueFilterText, hasFieldValues, hasStaticAllowlist, isBetweenOperator, isDatePreset, isFilterParseError, isMenuRelated, isMultiSelectOperator, isNoValueOperator, parseExpression, serializeExpression };
@@ -9,6 +9,12 @@ export type FilterInputChipVariant = 'chip' | 'and' | 'or' | '(' | ')';
9
9
  */
10
10
  /** Which segment of a chip has an error: attribute or value (true = whole chip) */
11
11
  export type ChipErrorSegment = boolean | 'attribute' | 'value';
12
+ /**
13
+ * Shared signature of the upsertCondition callback owned by useFilterInputExpression.
14
+ * Re-declared in several option interfaces — exported here to keep the source of
15
+ * truth single-rooted (changes to the signature reach all consumers).
16
+ */
17
+ export type UpsertCondition = (field: FieldMetadata, operator: FilterOperator | undefined, val: string | number | boolean | null | Array<string | number | boolean>, editingChipId?: string | null, atIndex?: number, error?: ChipErrorSegment, dateOrigin?: 'relative' | 'absolute') => void;
12
18
  export interface FilterInputChipData {
13
19
  id: string;
14
20
  variant: FilterInputChipVariant;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "0.29.1",
3
- "generatedAt": "2026-04-27T08:22:17.184Z",
3
+ "generatedAt": "2026-04-27T09:32:45.403Z",
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.29.2-rc-feature-AS-882.1",
3
+ "version": "0.29.2",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",