@wallarm-org/design-system 0.60.0 → 0.61.0

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.
@@ -19,6 +19,21 @@ export interface FilterInputProps extends Omit<HTMLAttributes<HTMLDivElement>, '
19
19
  onChange?: (expression: ExprNode | null) => void;
20
20
  placeholder?: string;
21
21
  error?: boolean;
22
+ /**
23
+ * Field names whose values were rejected by the backend. Matching chips are
24
+ * rendered with a value error (red). Purely presentational: conditions and
25
+ * `onChange` output are unaffected, and the consumer is expected to render
26
+ * its own message (e.g. an alert with the backend error text) and clear the
27
+ * prop when the filter changes or the query succeeds.
28
+ */
29
+ externalErrors?: string[];
30
+ /**
31
+ * Notified whenever the set of validation messages FilterInput renders below
32
+ * the input changes (empty array = no visible error). Lets a consumer avoid
33
+ * stacking its own message (e.g. a backend-error alert) on top of one the
34
+ * input already shows. Pass a stable (memoized) callback.
35
+ */
36
+ onErrorsChange?: (errors: string[]) => void;
22
37
  showKeyboardHint?: boolean;
23
38
  }
24
39
  export declare const FilterInput: FC<FilterInputProps>;
@@ -7,7 +7,7 @@ import { FilterInputField } from "./FilterInputField/index.js";
7
7
  import { FilterInputMenu } from "./FilterInputMenu/FilterInputMenu.js";
8
8
  import { useFilterInputAutocomplete, useFilterInputExpression, useFilterInputSelection } from "./hooks/index.js";
9
9
  import { applyKnownFieldHelpers } from "./lib/applyKnownFieldHelpers.js";
10
- const FilterInput = ({ fields: rawFields = [], value, onChange, placeholder = 'Type to filter...', error = false, showKeyboardHint = false, className, ...props })=>{
10
+ const FilterInput = ({ fields: rawFields = [], value, onChange, placeholder = 'Type to filter...', error = false, externalErrors, onErrorsChange, showKeyboardHint = false, className, ...props })=>{
11
11
  const inputRef = useRef(null);
12
12
  const containerRef = useRef(null);
13
13
  const buildingChipRef = useRef(null);
@@ -23,7 +23,8 @@ const FilterInput = ({ fields: rawFields = [], value, onChange, placeholder = 'T
23
23
  fields,
24
24
  value,
25
25
  onChange,
26
- error
26
+ error,
27
+ externalErrors
27
28
  });
28
29
  const autocomplete = useFilterInputAutocomplete({
29
30
  fields,
@@ -82,6 +83,12 @@ const FilterInput = ({ fields: rawFields = [], value, onChange, placeholder = 'T
82
83
  pasteError,
83
84
  fieldErrors
84
85
  ]);
86
+ useEffect(()=>{
87
+ onErrorsChange?.(errors);
88
+ }, [
89
+ errors,
90
+ onErrorsChange
91
+ ]);
85
92
  return /*#__PURE__*/ jsxs("div", {
86
93
  ref: containerRef,
87
94
  className: cn('group/filter-input relative flex w-full flex-col gap-4', className),
@@ -1,5 +1,5 @@
1
1
  import { useCallback, useRef } from "react";
2
- import { OPERATOR_SYMBOLS, applyAcceptChar, getOperatorFromLabel, hasFieldValues, nextBuildingMenu } from "../../lib/index.js";
2
+ import { OPERATOR_SYMBOLS, applyAcceptChar, getOperatorFromLabel, hasStaticAllowlist, nextBuildingMenu } from "../../lib/index.js";
3
3
  const useInputHandlers = ({ inputText, menuState, selectedField, selectedOperator, isFocused, fields, inputRef, blurCommitRef, commitBuildingForceRef, conditionsRef, conditionsLengthRef, effectiveInsertIndexRef, setInputText, setMenuState, setInsertIndex, resetMenuAnchor, removeConditionAtIndex, handleFieldSelect, handleOperatorSelect, handleCustomValueCommit, stepBackBuildingMenu })=>{
4
4
  const menuRef = useRef(null);
5
5
  const handleInputChange = useCallback((e)=>{
@@ -82,7 +82,7 @@ const useInputHandlers = ({ inputText, menuState, selectedField, selectedOperato
82
82
  return;
83
83
  }
84
84
  }
85
- if ('Enter' === e.key && 'value' === menuState && selectedField && !hasFieldValues(selectedField) && inputText.trim()) {
85
+ if ('Enter' === e.key && 'value' === menuState && selectedField && !hasStaticAllowlist(selectedField) && inputText.trim()) {
86
86
  e.preventDefault();
87
87
  handleCustomValueCommit(inputText);
88
88
  return;
@@ -0,0 +1,11 @@
1
+ import type { Condition, FilterInputChipData } from '../../types';
2
+ /**
3
+ * Display-only overlay for backend-rejected fields: marks built chips whose
4
+ * condition's field is listed, leaving conditions untouched. Working on chips
5
+ * (not conditions) keeps `condition.error` clear, so the flag never leaks into
6
+ * `onChange` round-trips, `parseFilterInputErrors` produces no duplicate
7
+ * message, and `buildChips`' cross-field label resolution (which keys off
8
+ * `condition.error`) is not falsely activated for raw freeform values.
9
+ * The consumer owns the error text (e.g. an alert with the backend response).
10
+ */
11
+ export declare const applyExternalErrors: (chips: FilterInputChipData[], conditions: Condition[], externalErrors: string[] | undefined) => FilterInputChipData[];
@@ -0,0 +1,16 @@
1
+ import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
2
+ const applyExternalErrors = (chips, conditions, externalErrors)=>{
3
+ if (!externalErrors?.length) return chips;
4
+ const errored = new Set(externalErrors);
5
+ let conditionIdx = -1;
6
+ return chips.map((chip)=>{
7
+ if ('chip' !== chip.variant) return chip;
8
+ conditionIdx += 1;
9
+ const condition = conditions[conditionIdx];
10
+ return condition && !chip.disabled && !chip.error && errored.has(condition.field) ? {
11
+ ...chip,
12
+ error: SEGMENT_VARIANT.value
13
+ } : chip;
14
+ });
15
+ };
16
+ export { applyExternalErrors };
@@ -22,15 +22,14 @@ const makeEmptyChip = (i, error)=>({
22
22
  value: '',
23
23
  error: error || void 0
24
24
  });
25
- const resolveValueLabel = (value, field, fields, crossField)=>{
25
+ const resolveValueLabel = (value, field, fields)=>{
26
26
  const own = findOptionByValue(field, value)?.label;
27
27
  if (void 0 !== own) return own;
28
- if (!crossField) return;
29
28
  return findValueLabelInFields(value, fields);
30
29
  };
31
- const resolveDisplayValue = (condition, field, fields, crossField)=>{
30
+ const resolveDisplayValue = (condition, field, fields)=>{
32
31
  const raw = String(condition.value ?? '');
33
- return resolveValueLabel(condition.value, field, fields, crossField) ?? raw;
32
+ return resolveValueLabel(condition.value, field, fields) ?? raw;
34
33
  };
35
34
  const buildBaseChip = (i, condition, field)=>({
36
35
  id: chipId(i),
@@ -67,9 +66,9 @@ const buildDateChip = (baseChip, condition, chipError)=>{
67
66
  error: chipError || (displayValue === INVALID_DATE ? SEGMENT_VARIANT.value : void 0)
68
67
  };
69
68
  };
70
- const buildMultiValueChip = (baseChip, condition, field, fields, chipError, crossField)=>{
69
+ const buildMultiValueChip = (baseChip, condition, field, fields, chipError)=>{
71
70
  const values = condition.value;
72
- const valueParts = values.map((v)=>resolveValueLabel(v, field, fields, crossField) ?? String(v));
71
+ const valueParts = values.map((v)=>resolveValueLabel(v, field, fields) ?? String(v));
73
72
  const invalidIndices = field ? getInvalidValueIndices(field, values) : [];
74
73
  return {
75
74
  ...baseChip,
@@ -89,17 +88,16 @@ const makeConditionChip = (i, conditions, fields, error)=>{
89
88
  const chipError = condition.error || (error ? true : void 0);
90
89
  const field = fields.find((f)=>f.name === condition.field);
91
90
  const baseChip = buildBaseChip(i, condition, field);
92
- const crossField = chipError === SEGMENT_VARIANT.value || true === chipError;
93
91
  if (condition.operator && isNoValueOperator(condition.operator)) return {
94
92
  ...baseChip,
95
93
  value: NO_VALUE_PLACEHOLDER,
96
94
  error: chipError
97
95
  };
98
96
  if (field?.type === 'date') return Array.isArray(condition.value) ? buildDateRangeChip(baseChip, condition, chipError) : buildDateChip(baseChip, condition, chipError);
99
- if (Array.isArray(condition.value)) return buildMultiValueChip(baseChip, condition, field, fields, chipError, crossField);
97
+ if (Array.isArray(condition.value)) return buildMultiValueChip(baseChip, condition, field, fields, chipError);
100
98
  return {
101
99
  ...baseChip,
102
- value: resolveDisplayValue(condition, field, fields, crossField),
100
+ value: resolveDisplayValue(condition, field, fields),
103
101
  error: chipError
104
102
  };
105
103
  };
@@ -4,8 +4,9 @@ interface UseFilterInputExpressionOptions {
4
4
  value?: ExprNode | null;
5
5
  onChange?: (expression: ExprNode | null) => void;
6
6
  error: boolean;
7
+ externalErrors?: string[];
7
8
  }
8
- export declare const useFilterInputExpression: ({ fields, value, onChange, error, }: UseFilterInputExpressionOptions) => {
9
+ export declare const useFilterInputExpression: ({ fields, value, onChange, error, externalErrors, }: UseFilterInputExpressionOptions) => {
9
10
  conditions: Condition[];
10
11
  connectors: ("and" | "or")[];
11
12
  chips: import("../..").FilterInputChipData[];
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo, useState } from "react";
2
2
  import { SEGMENT_VARIANT } from "../../FilterInputField/FilterInputChip/index.js";
3
3
  import { CONNECTOR_ID_PATTERN, chipIdToConditionIndex, validateValueForField } from "../../lib/index.js";
4
+ import { applyExternalErrors } from "./applyExternalErrors.js";
4
5
  import { buildChips } from "./buildChips.js";
5
6
  import { buildExpression, expressionToConditions } from "./expression.js";
6
7
  const EMPTY_STATE = {
@@ -91,7 +92,7 @@ const addConnectorIfNeeded = (connectors, newConditionsLength, editingChipId, at
91
92
  ...new Array(missing).fill(DEFAULT_CONNECTOR)
92
93
  ] : connectors;
93
94
  };
94
- const useFilterInputExpression = ({ fields, value, onChange, error })=>{
95
+ const useFilterInputExpression = ({ fields, value, onChange, error, externalErrors })=>{
95
96
  const [state, setState] = useState(EMPTY_STATE);
96
97
  useEffect(()=>{
97
98
  if (void 0 !== value) {
@@ -105,11 +106,12 @@ const useFilterInputExpression = ({ fields, value, onChange, error })=>{
105
106
  value,
106
107
  fields
107
108
  ]);
108
- const chips = useMemo(()=>buildChips(state.conditions, state.connectors, fields, error), [
109
+ const chips = useMemo(()=>applyExternalErrors(buildChips(state.conditions, state.connectors, fields, error), state.conditions, externalErrors), [
109
110
  state.conditions,
110
111
  state.connectors,
111
112
  fields,
112
- error
113
+ error,
114
+ externalErrors
113
115
  ]);
114
116
  const upsertCondition = useCallback((field, operator, val, editingChipId, atIndex, error, dateOrigin)=>{
115
117
  const condition = buildCondition(field, operator, val, error, dateOrigin);
@@ -27,6 +27,7 @@ export declare const getFieldValues: (field: FieldMetadata, inputText?: string,
27
27
  export declare const hasFieldValues: (field: FieldMetadata) => boolean;
28
28
  /**
29
29
  * True if the field has an exhaustive static allowlist. getSuggestions
30
- * fields return false — their list is a hint, not a strict allowlist.
30
+ * fields and `strictValues: false` fields return false — their list is a
31
+ * hint, not a strict allowlist.
31
32
  */
32
33
  export declare const hasStaticAllowlist: (field: FieldMetadata) => boolean;
@@ -26,6 +26,7 @@ const hasFieldValues = (field)=>{
26
26
  return (field.options?.length ?? 0) > 0;
27
27
  };
28
28
  const hasStaticAllowlist = (field)=>{
29
+ if (false === field.strictValues) return false;
29
30
  if (field.getSuggestions) return false;
30
31
  if ((field.values?.length ?? 0) > 0) return true;
31
32
  return (field.options?.length ?? 0) > 0;
@@ -68,6 +68,13 @@ export interface FieldMetadata {
68
68
  * Empty array `[]` means freeform input — no dropdown, user types any value.
69
69
  */
70
70
  options?: string[];
71
+ /**
72
+ * When `false`, `values`/`options` are suggestions rather than an exhaustive
73
+ * allowlist: the dropdown still offers them, but any typed value commits
74
+ * without an allowlist error. Data-type validation (`isValueOfType`) still
75
+ * applies. Defaults to `true` (options are a strict allowlist).
76
+ */
77
+ strictValues?: boolean;
71
78
  /**
72
79
  * Optional callback to compute value suggestions dynamically from the current
73
80
  * input text. When provided, takes precedence over `values` and `options`.
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.59.0",
3
- "generatedAt": "2026-06-14T20:20:21.089Z",
2
+ "version": "0.60.0",
3
+ "generatedAt": "2026-06-16T08:44:33.684Z",
4
4
  "components": [
5
5
  {
6
6
  "name": "Accordion",
@@ -22031,6 +22031,12 @@
22031
22031
  "required": false,
22032
22032
  "defaultValue": "false"
22033
22033
  },
22034
+ {
22035
+ "name": "externalErrors",
22036
+ "type": "string[] | undefined",
22037
+ "required": false,
22038
+ "description": "Field names whose values were rejected by the backend. Matching chips are\nrendered with a value error (red). Purely presentational: conditions and\n`onChange` output are unaffected, and the consumer is expected to render\nits own message (e.g. an alert with the backend error text) and clear the\nprop when the filter changes or the query succeeds."
22039
+ },
22034
22040
  {
22035
22041
  "name": "showKeyboardHint",
22036
22042
  "type": "boolean | undefined",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wallarm-org/design-system",
3
- "version": "0.60.0",
3
+ "version": "0.61.0",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",