@wallarm-org/design-system 0.20.0 → 0.21.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.
Files changed (56) hide show
  1. package/dist/components/FilterInput/FilterInput.d.ts +0 -25
  2. package/dist/components/FilterInput/FilterInput.js +48 -6
  3. package/dist/components/FilterInput/FilterInputContext/types.d.ts +2 -0
  4. package/dist/components/FilterInput/FilterInputContext/useFilterInputContextValue.d.ts +2 -1
  5. package/dist/components/FilterInput/FilterInputContext/useFilterInputContextValue.js +4 -2
  6. package/dist/components/FilterInput/FilterInputField/ChipsWithGaps.d.ts +1 -1
  7. package/dist/components/FilterInput/FilterInputField/ChipsWithGaps.js +8 -0
  8. package/dist/components/FilterInput/FilterInputField/FilterInputChip/classes.js +4 -2
  9. package/dist/components/FilterInput/FilterInputField/FilterInputSearch.d.ts +0 -1
  10. package/dist/components/FilterInput/FilterInputField/FilterInputSearch.js +4 -3
  11. package/dist/components/FilterInput/FilterInputField/classes.js +1 -1
  12. package/dist/components/FilterInput/FilterInputMenu/FilterInputFieldMenu/FilterInputFieldMenu.js +2 -1
  13. package/dist/components/FilterInput/hooks/index.d.ts +1 -0
  14. package/dist/components/FilterInput/hooks/index.js +2 -1
  15. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.d.ts +1 -0
  16. package/dist/components/FilterInput/hooks/useFilterInputAutocomplete/useFilterInputAutocomplete.js +2 -1
  17. package/dist/components/FilterInput/hooks/useFilterInputSelection/index.d.ts +1 -0
  18. package/dist/components/FilterInput/hooks/useFilterInputSelection/index.js +2 -0
  19. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/constants.d.ts +3 -0
  20. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/constants.js +4 -0
  21. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/dom.d.ts +8 -0
  22. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/dom.js +34 -0
  23. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/index.d.ts +3 -0
  24. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/index.js +4 -0
  25. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/serialize.d.ts +3 -0
  26. package/dist/components/FilterInput/hooks/useFilterInputSelection/lib/serialize.js +15 -0
  27. package/dist/components/FilterInput/hooks/useFilterInputSelection/useDragSelection.d.ts +10 -0
  28. package/dist/components/FilterInput/hooks/useFilterInputSelection/useDragSelection.js +44 -0
  29. package/dist/components/FilterInput/hooks/useFilterInputSelection/useFilterInputSelection.d.ts +25 -0
  30. package/dist/components/FilterInput/hooks/useFilterInputSelection/useFilterInputSelection.js +54 -0
  31. package/dist/components/FilterInput/hooks/useFilterInputSelection/useSelectionClipboard.d.ts +19 -0
  32. package/dist/components/FilterInput/hooks/useFilterInputSelection/useSelectionClipboard.js +77 -0
  33. package/dist/components/FilterInput/hooks/useFilterInputSelection/useSelectionKeyboard.d.ts +14 -0
  34. package/dist/components/FilterInput/hooks/useFilterInputSelection/useSelectionKeyboard.js +40 -0
  35. package/dist/components/FilterInput/index.d.ts +1 -0
  36. package/dist/components/FilterInput/index.js +2 -1
  37. package/dist/components/FilterInput/lib/index.d.ts +2 -0
  38. package/dist/components/FilterInput/lib/index.js +3 -1
  39. package/dist/components/FilterInput/lib/parseExpression/error.d.ts +6 -0
  40. package/dist/components/FilterInput/lib/parseExpression/error.js +6 -0
  41. package/dist/components/FilterInput/lib/parseExpression/index.d.ts +2 -0
  42. package/dist/components/FilterInput/lib/parseExpression/index.js +3 -0
  43. package/dist/components/FilterInput/lib/parseExpression/parseExpression.d.ts +8 -0
  44. package/dist/components/FilterInput/lib/parseExpression/parseExpression.js +21 -0
  45. package/dist/components/FilterInput/lib/parseExpression/parser.d.ts +4 -0
  46. package/dist/components/FilterInput/lib/parseExpression/parser.js +146 -0
  47. package/dist/components/FilterInput/lib/parseExpression/tokenizer.d.ts +8 -0
  48. package/dist/components/FilterInput/lib/parseExpression/tokenizer.js +101 -0
  49. package/dist/components/FilterInput/lib/parseExpression/types.d.ts +7 -0
  50. package/dist/components/FilterInput/lib/parseExpression/types.js +0 -0
  51. package/dist/components/FilterInput/lib/parseExpression/validators.d.ts +5 -0
  52. package/dist/components/FilterInput/lib/parseExpression/validators.js +28 -0
  53. package/dist/components/FilterInput/lib/serializeExpression.d.ts +6 -0
  54. package/dist/components/FilterInput/lib/serializeExpression.js +36 -0
  55. package/dist/metadata/components.json +3 -9
  56. package/package.json +1 -1
@@ -0,0 +1,77 @@
1
+ import { useCallback } from "react";
2
+ import { isFilterParseError, parseExpression } from "../../lib/index.js";
3
+ import { serializeSelectedOrAll } from "./lib/index.js";
4
+ const formatPasteError = (err)=>{
5
+ if (!isFilterParseError(err)) return 'Could not parse the pasted filter text';
6
+ const msg = err.message;
7
+ if (msg.startsWith('Unknown field:')) {
8
+ const field = msg.replace('Unknown field: ', '');
9
+ return `Unknown filter field "${field}". Check the field name and try again.`;
10
+ }
11
+ if (msg.includes('not allowed for field')) return `${msg}. Check the operator and try again.`;
12
+ if (msg.startsWith('Invalid value')) return `${msg}. Check the allowed values and try again.`;
13
+ if (msg.startsWith('Expected')) return `Invalid filter format: ${msg.toLowerCase()}. Expected format: (field operator value) AND (field operator value)`;
14
+ if ('Empty filter text' === msg) return 'The pasted text is empty';
15
+ return `Could not parse filter: ${msg}`;
16
+ };
17
+ const useSelectionClipboard = ({ conditions, connectors, fields, chipRegistryRef, clearSelection, setPasteError, setInputText, closeMenu, onChange })=>{
18
+ const handleCopy = useCallback((e)=>{
19
+ if (0 === conditions.length) return;
20
+ e.preventDefault();
21
+ const text = serializeSelectedOrAll(conditions, connectors, chipRegistryRef.current);
22
+ e.clipboardData.setData('text/plain', text);
23
+ }, [
24
+ conditions,
25
+ connectors,
26
+ chipRegistryRef
27
+ ]);
28
+ const handlePaste = useCallback((e)=>{
29
+ const text = e.clipboardData.getData('text/plain').trim();
30
+ if (!text) return;
31
+ if (!text.includes('(') && !text.includes('=') && !text.includes(' in ')) return;
32
+ e.preventDefault();
33
+ try {
34
+ const expr = parseExpression(text, fields);
35
+ onChange?.(expr);
36
+ clearSelection();
37
+ setPasteError(null);
38
+ } catch (err) {
39
+ setInputText(text);
40
+ closeMenu();
41
+ setPasteError(formatPasteError(err));
42
+ }
43
+ }, [
44
+ fields,
45
+ onChange,
46
+ clearSelection,
47
+ setPasteError,
48
+ setInputText,
49
+ closeMenu
50
+ ]);
51
+ const retryParse = useCallback((text)=>{
52
+ const trimmed = text.trim();
53
+ if (!trimmed) return void setPasteError(null);
54
+ if (!trimmed.includes('(') && !trimmed.includes('=') && !trimmed.includes(' in ')) return;
55
+ try {
56
+ const expr = parseExpression(trimmed, fields);
57
+ onChange?.(expr);
58
+ setInputText('');
59
+ closeMenu();
60
+ setPasteError(null);
61
+ } catch (err) {
62
+ setPasteError(formatPasteError(err));
63
+ }
64
+ }, [
65
+ fields,
66
+ onChange,
67
+ setPasteError,
68
+ setInputText,
69
+ closeMenu
70
+ ]);
71
+ return {
72
+ handleCopy,
73
+ handlePaste,
74
+ retryParse
75
+ };
76
+ };
77
+ export { useSelectionClipboard };
@@ -0,0 +1,14 @@
1
+ import type { KeyboardEvent, RefObject } from 'react';
2
+ interface UseSelectionKeyboardOptions {
3
+ allSelected: boolean;
4
+ conditionsCount: number;
5
+ chipRegistryRef: RefObject<Map<string, HTMLElement>>;
6
+ inputRef: RefObject<HTMLInputElement | null>;
7
+ clearAll: () => void;
8
+ clearSelection: () => void;
9
+ onSelectAll: () => void;
10
+ }
11
+ export declare const useSelectionKeyboard: ({ allSelected, conditionsCount, chipRegistryRef, inputRef, clearAll, clearSelection, onSelectAll, }: UseSelectionKeyboardOptions) => {
12
+ handleKeyDown: (e: KeyboardEvent<HTMLDivElement>) => void;
13
+ };
14
+ export {};
@@ -0,0 +1,40 @@
1
+ import { useCallback } from "react";
2
+ import { hasDragSelection } from "./lib/index.js";
3
+ const useSelectionKeyboard = ({ allSelected, conditionsCount, chipRegistryRef, inputRef, clearAll, clearSelection, onSelectAll })=>{
4
+ const handleKeyDown = useCallback((e)=>{
5
+ const isMod = e.metaKey || e.ctrlKey;
6
+ const hasSelection = allSelected || hasDragSelection(chipRegistryRef.current);
7
+ if (isMod && 'a' === e.key && conditionsCount > 0) {
8
+ e.preventDefault();
9
+ clearSelection();
10
+ onSelectAll();
11
+ inputRef.current?.blur();
12
+ return;
13
+ }
14
+ if (hasSelection && ('Delete' === e.key || 'Backspace' === e.key)) {
15
+ e.preventDefault();
16
+ clearAll();
17
+ clearSelection();
18
+ return;
19
+ }
20
+ if (hasSelection && 'Escape' === e.key) {
21
+ e.preventDefault();
22
+ clearSelection();
23
+ inputRef.current?.focus();
24
+ return;
25
+ }
26
+ if (hasSelection && !isMod) clearSelection();
27
+ }, [
28
+ allSelected,
29
+ conditionsCount,
30
+ chipRegistryRef,
31
+ inputRef,
32
+ clearAll,
33
+ clearSelection,
34
+ onSelectAll
35
+ ]);
36
+ return {
37
+ handleKeyDown
38
+ };
39
+ };
40
+ export { useSelectionKeyboard };
@@ -1,4 +1,5 @@
1
1
  export { FilterInput, type FilterInputProps } from './FilterInput';
2
2
  export { FilterInputChip, type FilterInputChipProps } from './FilterInputField';
3
3
  export { FilterInputFieldMenu, type FilterInputFieldMenuProps, FilterInputOperatorMenu, type FilterInputOperatorMenuProps, FilterInputValueMenu, type FilterInputValueMenuProps, type ValueOption, } from './FilterInputMenu';
4
+ export { type FilterParseError, isFilterParseError, parseExpression, serializeExpression, } from './lib';
4
5
  export type { Condition, ExprNode, FieldMetadata, FieldType, FieldValueOption, FilterInputChipData, FilterInputChipVariant, FilterOperator, Group, } from './types';
@@ -1,4 +1,5 @@
1
1
  import { FilterInput } from "./FilterInput.js";
2
2
  import { FilterInputChip } from "./FilterInputField/index.js";
3
3
  import { FilterInputFieldMenu, FilterInputOperatorMenu, FilterInputValueMenu } from "./FilterInputMenu/index.js";
4
- export { FilterInput, FilterInputChip, FilterInputFieldMenu, FilterInputOperatorMenu, FilterInputValueMenu };
4
+ import { isFilterParseError, parseExpression, serializeExpression } from "./lib/index.js";
5
+ export { FilterInput, FilterInputChip, FilterInputFieldMenu, FilterInputOperatorMenu, FilterInputValueMenu, isFilterParseError, parseExpression, serializeExpression };
@@ -7,3 +7,5 @@ export { getFieldValues, hasFieldValues } from './fields';
7
7
  export { filterAndSort } from './filterSort';
8
8
  export { getValueFilterText } from './menuFilterText';
9
9
  export { getOperatorFromLabel, getOperatorLabel, isBetweenOperator, isMultiSelectOperator, isNoValueOperator, } from './operators';
10
+ export { type FilterParseError, isFilterParseError, parseExpression } from './parseExpression';
11
+ export { serializeExpression } from './serializeExpression';
@@ -6,4 +6,6 @@ import { getFieldValues, hasFieldValues } from "./fields.js";
6
6
  import { filterAndSort } from "./filterSort.js";
7
7
  import { getValueFilterText } from "./menuFilterText.js";
8
8
  import { getOperatorFromLabel, getOperatorLabel, isBetweenOperator, isMultiSelectOperator, isNoValueOperator } from "./operators.js";
9
- 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, buildContainerAnchoredRect, chipIdToConditionIndex, filterAndSort, findChipSplitIndex, formatDateForChip, getDateDisplayLabel, getFieldValues, getOperatorFromLabel, getOperatorLabel, getValueFilterText, hasFieldValues, isBetweenOperator, isDatePreset, isMenuRelated, isMultiSelectOperator, isNoValueOperator };
9
+ import { isFilterParseError, parseExpression } from "./parseExpression/index.js";
10
+ import { serializeExpression } from "./serializeExpression.js";
11
+ 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, buildContainerAnchoredRect, chipIdToConditionIndex, filterAndSort, findChipSplitIndex, formatDateForChip, getDateDisplayLabel, getFieldValues, getOperatorFromLabel, getOperatorLabel, getValueFilterText, hasFieldValues, isBetweenOperator, isDatePreset, isFilterParseError, isMenuRelated, isMultiSelectOperator, isNoValueOperator, parseExpression, serializeExpression };
@@ -0,0 +1,6 @@
1
+ export interface FilterParseError {
2
+ readonly _tag: 'FilterParseError';
3
+ readonly message: string;
4
+ }
5
+ export declare const FilterParseError: (message: string) => FilterParseError;
6
+ export declare const isFilterParseError: (value: unknown) => value is FilterParseError;
@@ -0,0 +1,6 @@
1
+ const FilterParseError = (message)=>({
2
+ _tag: 'FilterParseError',
3
+ message
4
+ });
5
+ const isFilterParseError = (value)=>'object' == typeof value && null !== value && '_tag' in value && 'FilterParseError' === value._tag;
6
+ export { FilterParseError, isFilterParseError };
@@ -0,0 +1,2 @@
1
+ export { type FilterParseError, isFilterParseError } from './error';
2
+ export { parseExpression } from './parseExpression';
@@ -0,0 +1,3 @@
1
+ import { isFilterParseError } from "./error.js";
2
+ import { parseExpression } from "./parseExpression.js";
3
+ export { isFilterParseError, parseExpression };
@@ -0,0 +1,8 @@
1
+ import type { ExprNode, FieldMetadata } from '../../types';
2
+ /**
3
+ * Parse a filter text string into an expression tree.
4
+ * Validates field names, operators, and values against the provided field metadata.
5
+ *
6
+ * @throws {FilterParseError} on invalid input
7
+ */
8
+ export declare const parseExpression: (text: string, fields: FieldMetadata[]) => ExprNode;
@@ -0,0 +1,21 @@
1
+ import { FilterParseError } from "./error.js";
2
+ import { parseExpr } from "./parser.js";
3
+ import { tokenize } from "./tokenizer.js";
4
+ const parseExpression = (text, fields)=>{
5
+ const trimmed = text.trim();
6
+ if (!trimmed) throw FilterParseError('Empty filter text');
7
+ const tokens = tokenize(trimmed);
8
+ if (0 === tokens.length) throw FilterParseError('Empty filter text');
9
+ const state = {
10
+ tokens,
11
+ pos: 0,
12
+ fields
13
+ };
14
+ const expr = parseExpr(state);
15
+ if (state.pos < tokens.length) {
16
+ const remaining = tokens[state.pos];
17
+ throw FilterParseError(`Unexpected token '${remaining.value}' at position ${remaining.pos}`);
18
+ }
19
+ return expr;
20
+ };
21
+ export { parseExpression };
@@ -0,0 +1,4 @@
1
+ import type { ExprNode } from '../../types';
2
+ import type { ParserState } from './types';
3
+ /** expression → term ( OR term )* */
4
+ export declare const parseExpr: (s: ParserState) => ExprNode;
@@ -0,0 +1,146 @@
1
+ import { FilterParseError } from "./error.js";
2
+ import { validateField, validateOperator, validateValues } from "./validators.js";
3
+ const peek = (s)=>s.tokens[s.pos];
4
+ const advance = (s)=>{
5
+ const token = s.tokens[s.pos];
6
+ if (!token) throw FilterParseError('Unexpected end of input');
7
+ s.pos++;
8
+ return token;
9
+ };
10
+ const expect = (s, type)=>{
11
+ const token = peek(s);
12
+ if (!token || token.type !== type) {
13
+ const found = token ? `'${token.value}' at position ${token.pos}` : 'end of input';
14
+ throw FilterParseError(`Expected ${type}, found ${found}`);
15
+ }
16
+ return advance(s);
17
+ };
18
+ const parseValueList = (s)=>{
19
+ expect(s, 'LBRACKET');
20
+ const values = [];
21
+ if (peek(s)?.type !== 'RBRACKET') {
22
+ values.push(expect(s, 'IDENT').value);
23
+ while(peek(s)?.type === 'COMMA'){
24
+ advance(s);
25
+ values.push(expect(s, 'IDENT').value);
26
+ }
27
+ }
28
+ expect(s, 'RBRACKET');
29
+ return values;
30
+ };
31
+ const parseCondition = (s)=>{
32
+ const fieldToken = expect(s, 'IDENT');
33
+ validateField(s, fieldToken.value);
34
+ const opToken = peek(s);
35
+ if (!opToken || 'OPERATOR' !== opToken.type) throw FilterParseError(`Expected operator after field '${fieldToken.value}'`);
36
+ advance(s);
37
+ const operator = validateOperator(s, opToken.value, fieldToken.value);
38
+ if ('is_null' === operator || 'is_not_null' === operator) return {
39
+ type: 'condition',
40
+ field: fieldToken.value,
41
+ operator,
42
+ value: null
43
+ };
44
+ if (peek(s)?.type === 'LBRACKET') {
45
+ const values = parseValueList(s);
46
+ validateValues(s, fieldToken.value, values);
47
+ return {
48
+ type: 'condition',
49
+ field: fieldToken.value,
50
+ operator,
51
+ value: values
52
+ };
53
+ }
54
+ const valueToken = expect(s, 'IDENT');
55
+ validateValues(s, fieldToken.value, [
56
+ valueToken.value
57
+ ]);
58
+ return {
59
+ type: 'condition',
60
+ field: fieldToken.value,
61
+ operator,
62
+ value: valueToken.value
63
+ };
64
+ };
65
+ const parseFactor = (s)=>{
66
+ if (peek(s)?.type === 'LPAREN') {
67
+ advance(s);
68
+ if (peek(s)?.type === 'LPAREN') {
69
+ const expr = parseExpr(s);
70
+ expect(s, 'RPAREN');
71
+ return expr;
72
+ }
73
+ const condition = parseCondition(s);
74
+ const next = peek(s);
75
+ if (next?.type === 'RPAREN') {
76
+ advance(s);
77
+ return condition;
78
+ }
79
+ if (next?.type === 'AND' || next?.type === 'OR') {
80
+ const children = [
81
+ condition
82
+ ];
83
+ const groupOp = 'AND' === next.type ? 'and' : 'or';
84
+ while(peek(s)?.type === 'AND' || peek(s)?.type === 'OR'){
85
+ advance(s);
86
+ children.push(parseFactor(s));
87
+ }
88
+ expect(s, 'RPAREN');
89
+ return {
90
+ type: 'group',
91
+ operator: groupOp,
92
+ children
93
+ };
94
+ }
95
+ const found = next ? `'${next.value}' at position ${next.pos}` : 'end of input';
96
+ throw FilterParseError(`Expected ')' or connector after condition, found ${found}`);
97
+ }
98
+ if (peek(s)?.type === 'IDENT') return parseCondition(s);
99
+ const token = peek(s);
100
+ throw FilterParseError(token ? `Unexpected token '${token.value}' at position ${token.pos}` : 'Unexpected end of input');
101
+ };
102
+ const parseTerm = (s)=>{
103
+ let left = parseFactor(s);
104
+ while(peek(s)?.type === 'AND'){
105
+ advance(s);
106
+ const right = parseFactor(s);
107
+ left = 'group' === left.type && 'and' === left.operator ? {
108
+ ...left,
109
+ children: [
110
+ ...left.children,
111
+ right
112
+ ]
113
+ } : {
114
+ type: 'group',
115
+ operator: 'and',
116
+ children: [
117
+ left,
118
+ right
119
+ ]
120
+ };
121
+ }
122
+ return left;
123
+ };
124
+ const parseExpr = (s)=>{
125
+ let left = parseTerm(s);
126
+ while(peek(s)?.type === 'OR'){
127
+ advance(s);
128
+ const right = parseTerm(s);
129
+ left = 'group' === left.type && 'or' === left.operator ? {
130
+ ...left,
131
+ children: [
132
+ ...left.children,
133
+ right
134
+ ]
135
+ } : {
136
+ type: 'group',
137
+ operator: 'or',
138
+ children: [
139
+ left,
140
+ right
141
+ ]
142
+ };
143
+ }
144
+ return left;
145
+ };
146
+ export { parseExpr };
@@ -0,0 +1,8 @@
1
+ export type TokenType = 'LPAREN' | 'RPAREN' | 'LBRACKET' | 'RBRACKET' | 'COMMA' | 'AND' | 'OR' | 'OPERATOR' | 'IDENT';
2
+ export interface Token {
3
+ type: TokenType;
4
+ value: string;
5
+ pos: number;
6
+ }
7
+ export declare const OPERATORS: ReadonlySet<string>;
8
+ export declare const tokenize: (input: string) => Token[];
@@ -0,0 +1,101 @@
1
+ import { FilterParseError } from "./error.js";
2
+ const OPERATORS = new Set([
3
+ '=',
4
+ '!=',
5
+ '>',
6
+ '<',
7
+ '>=',
8
+ '<=',
9
+ 'in',
10
+ 'not_in',
11
+ 'like',
12
+ 'not_like',
13
+ 'is_null',
14
+ 'is_not_null',
15
+ 'between'
16
+ ]);
17
+ const TWO_CHAR_OPS = new Set([
18
+ '!=',
19
+ '>=',
20
+ '<='
21
+ ]);
22
+ const isWhitespace = (ch)=>' \t\n\r'.includes(ch);
23
+ const isIdentChar = (ch)=>'' !== ch && !isWhitespace(ch) && !'()[],!><='.includes(ch);
24
+ const tokenize = (input)=>{
25
+ const tokens = [];
26
+ let i = 0;
27
+ const push = (type, value, pos, len = 1)=>{
28
+ tokens.push({
29
+ type,
30
+ value,
31
+ pos
32
+ });
33
+ i += len;
34
+ };
35
+ while(i < input.length){
36
+ const ch = input[i];
37
+ if (isWhitespace(ch)) {
38
+ i++;
39
+ continue;
40
+ }
41
+ if ('(' === ch) {
42
+ push('LPAREN', '(', i);
43
+ continue;
44
+ }
45
+ if (')' === ch) {
46
+ push('RPAREN', ')', i);
47
+ continue;
48
+ }
49
+ if ('[' === ch) {
50
+ push('LBRACKET', '[', i);
51
+ continue;
52
+ }
53
+ if (']' === ch) {
54
+ push('RBRACKET', ']', i);
55
+ continue;
56
+ }
57
+ if (',' === ch) {
58
+ push('COMMA', ',', i);
59
+ continue;
60
+ }
61
+ const two = input.slice(i, i + 2);
62
+ if (TWO_CHAR_OPS.has(two)) {
63
+ push('OPERATOR', two, i, 2);
64
+ continue;
65
+ }
66
+ if ('=><'.includes(ch)) {
67
+ push('OPERATOR', ch, i);
68
+ continue;
69
+ }
70
+ if (isIdentChar(ch)) {
71
+ const start = i;
72
+ while(i < input.length && isIdentChar(input[i]))i++;
73
+ const word = input.slice(start, i);
74
+ const upper = word.toUpperCase();
75
+ if ('AND' === upper) tokens.push({
76
+ type: 'AND',
77
+ value: 'AND',
78
+ pos: start
79
+ });
80
+ else if ('OR' === upper) tokens.push({
81
+ type: 'OR',
82
+ value: 'OR',
83
+ pos: start
84
+ });
85
+ else if (OPERATORS.has(word)) tokens.push({
86
+ type: 'OPERATOR',
87
+ value: word,
88
+ pos: start
89
+ });
90
+ else tokens.push({
91
+ type: 'IDENT',
92
+ value: word,
93
+ pos: start
94
+ });
95
+ continue;
96
+ }
97
+ throw FilterParseError(`Unexpected character '${ch}' at position ${i}`);
98
+ }
99
+ return tokens;
100
+ };
101
+ export { OPERATORS, tokenize };
@@ -0,0 +1,7 @@
1
+ import type { FieldMetadata } from '../../types';
2
+ import type { Token } from './tokenizer';
3
+ export interface ParserState {
4
+ tokens: Token[];
5
+ pos: number;
6
+ fields: FieldMetadata[];
7
+ }
@@ -0,0 +1,5 @@
1
+ import type { FilterOperator } from '../../types';
2
+ import type { ParserState } from './types';
3
+ export declare const validateField: (s: ParserState, name: string) => void;
4
+ export declare const validateOperator: (s: ParserState, op: string, fieldName: string) => FilterOperator;
5
+ export declare const validateValues: (s: ParserState, fieldName: string, values: Array<string | number>) => void;
@@ -0,0 +1,28 @@
1
+ import { FilterParseError } from "./error.js";
2
+ import { OPERATORS } from "./tokenizer.js";
3
+ const validateField = (s, name)=>{
4
+ if (!s.fields.some((f)=>f.name === name)) throw FilterParseError(`Unknown field: ${name}`);
5
+ };
6
+ const validateOperator = (s, op, fieldName)=>{
7
+ if (!OPERATORS.has(op)) throw FilterParseError(`Unknown operator: ${op}`);
8
+ const field = s.fields.find((f)=>f.name === fieldName);
9
+ if (field?.operators && !field.operators.includes(op)) throw FilterParseError(`Operator '${op}' is not allowed for field '${fieldName}'`);
10
+ return op;
11
+ };
12
+ const getAllowedValues = (field)=>{
13
+ if (field.values && field.values.length > 0) return new Set(field.values.map((v)=>String(v.value)));
14
+ if (field.options && field.options.length > 0) return new Set(field.options);
15
+ return null;
16
+ };
17
+ const validateValues = (s, fieldName, values)=>{
18
+ const field = s.fields.find((f)=>f.name === fieldName);
19
+ if (!field) return;
20
+ const allowed = getAllowedValues(field);
21
+ if (!allowed) return;
22
+ const invalid = values.filter((v)=>!allowed.has(String(v)));
23
+ if (invalid.length > 0) {
24
+ const formatted = invalid.map((v)=>`"${v}"`).join(', ');
25
+ throw FilterParseError(`Invalid value ${formatted} for field '${fieldName}'`);
26
+ }
27
+ };
28
+ export { validateField, validateOperator, validateValues };
@@ -0,0 +1,6 @@
1
+ import type { ExprNode } from '../types';
2
+ /**
3
+ * Serialize an expression tree to a canonical text string.
4
+ * Top-level conditions are sorted alphabetically by field name.
5
+ */
6
+ export declare const serializeExpression: (expr: ExprNode | null) => string;
@@ -0,0 +1,36 @@
1
+ const serializeValue = (condition)=>{
2
+ if ('is_null' === condition.operator || 'is_not_null' === condition.operator) return '';
3
+ if (Array.isArray(condition.value)) {
4
+ const parts = condition.value.map(String);
5
+ return `[${parts.join(', ')}]`;
6
+ }
7
+ return String(condition.value ?? '');
8
+ };
9
+ const serializeCondition = (condition)=>{
10
+ const field = condition.field;
11
+ const operator = condition.operator ?? '=';
12
+ const value = serializeValue(condition);
13
+ if ('is_null' === operator || 'is_not_null' === operator) return `(${field} ${operator})`;
14
+ return `(${field} ${operator} ${value})`;
15
+ };
16
+ const sortConditions = (children)=>[
17
+ ...children
18
+ ].sort((a, b)=>{
19
+ const aField = 'condition' === a.type ? a.field : '';
20
+ const bField = 'condition' === b.type ? b.field : '';
21
+ return aField.localeCompare(bField);
22
+ });
23
+ const serializeGroup = (group, isTopLevel)=>{
24
+ const children = isTopLevel ? sortConditions(group.children) : group.children;
25
+ const connector = 'or' === group.operator ? ' OR ' : ' AND ';
26
+ return children.map((child)=>serializeNode(child, false)).join(connector);
27
+ };
28
+ const serializeNode = (node, isTopLevel)=>{
29
+ if ('condition' === node.type) return serializeCondition(node);
30
+ return serializeGroup(node, isTopLevel);
31
+ };
32
+ const serializeExpression = (expr)=>{
33
+ if (!expr) return '';
34
+ return serializeNode(expr, true);
35
+ };
36
+ export { serializeExpression };
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "0.19.1",
3
- "generatedAt": "2026-04-03T12:43:34.959Z",
2
+ "version": "0.20.0",
3
+ "generatedAt": "2026-04-07T08:00:47.141Z",
4
4
  "components": [
5
5
  {
6
6
  "name": "Alert",
@@ -12833,41 +12833,35 @@
12833
12833
  },
12834
12834
  {
12835
12835
  "name": "FilterInput",
12836
- "description": "FilterInput - Self-contained filter component.\nHandles autocomplete flow (field → operator → value), chip creation, and expression management.\nSupports multiple conditions joined by AND/OR connectors.\nJust pass `fields` config from backend API and it works.",
12837
12836
  "importPath": "@wallarm-org/design-system/FilterInput",
12838
12837
  "props": [
12839
12838
  {
12840
12839
  "name": "fields",
12841
12840
  "type": "FieldMetadata[] | undefined",
12842
12841
  "required": false,
12843
- "description": "Available fields from backend API config",
12844
12842
  "defaultValue": "[]"
12845
12843
  },
12846
12844
  {
12847
12845
  "name": "value",
12848
12846
  "type": "ExprNode | null | undefined",
12849
- "required": false,
12850
- "description": "Current filter expression value (controlled mode)"
12847
+ "required": false
12851
12848
  },
12852
12849
  {
12853
12850
  "name": "placeholder",
12854
12851
  "type": "string | undefined",
12855
12852
  "required": false,
12856
- "description": "Placeholder text to display when field is empty",
12857
12853
  "defaultValue": "Type to filter..."
12858
12854
  },
12859
12855
  {
12860
12856
  "name": "error",
12861
12857
  "type": "boolean | undefined",
12862
12858
  "required": false,
12863
- "description": "Whether the field has a validation error",
12864
12859
  "defaultValue": "false"
12865
12860
  },
12866
12861
  {
12867
12862
  "name": "showKeyboardHint",
12868
12863
  "type": "boolean | undefined",
12869
12864
  "required": false,
12870
- "description": "Whether to show the keyboard hint (⌘K or Ctrl+K)",
12871
12865
  "defaultValue": "false"
12872
12866
  },
12873
12867
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wallarm-org/design-system",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",