@ram_28/kf-ai-sdk 1.0.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.
- package/LICENSE +21 -0
- package/README.md +840 -0
- package/dist/api/client.d.ts +78 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/datetime.d.ts +21 -0
- package/dist/api/datetime.d.ts.map +1 -0
- package/dist/api/index.d.ts +7 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/metadata.d.ts +75 -0
- package/dist/api/metadata.d.ts.map +1 -0
- package/dist/components/hooks/index.d.ts +8 -0
- package/dist/components/hooks/index.d.ts.map +1 -0
- package/dist/components/hooks/useFilter/index.d.ts +5 -0
- package/dist/components/hooks/useFilter/index.d.ts.map +1 -0
- package/dist/components/hooks/useFilter/payloadBuilder.utils.d.ts +33 -0
- package/dist/components/hooks/useFilter/payloadBuilder.utils.d.ts.map +1 -0
- package/dist/components/hooks/useFilter/types.d.ts +137 -0
- package/dist/components/hooks/useFilter/types.d.ts.map +1 -0
- package/dist/components/hooks/useFilter/useFilter.d.ts +3 -0
- package/dist/components/hooks/useFilter/useFilter.d.ts.map +1 -0
- package/dist/components/hooks/useFilter/validation.utils.d.ts +38 -0
- package/dist/components/hooks/useFilter/validation.utils.d.ts.map +1 -0
- package/dist/components/hooks/useForm/apiClient.d.ts +71 -0
- package/dist/components/hooks/useForm/apiClient.d.ts.map +1 -0
- package/dist/components/hooks/useForm/expressionValidator.utils.d.ts +28 -0
- package/dist/components/hooks/useForm/expressionValidator.utils.d.ts.map +1 -0
- package/dist/components/hooks/useForm/index.d.ts +6 -0
- package/dist/components/hooks/useForm/index.d.ts.map +1 -0
- package/dist/components/hooks/useForm/optimizedExpressionValidator.utils.d.ts +88 -0
- package/dist/components/hooks/useForm/optimizedExpressionValidator.utils.d.ts.map +1 -0
- package/dist/components/hooks/useForm/ruleClassifier.utils.d.ts +28 -0
- package/dist/components/hooks/useForm/ruleClassifier.utils.d.ts.map +1 -0
- package/dist/components/hooks/useForm/schemaParser.utils.d.ts +29 -0
- package/dist/components/hooks/useForm/schemaParser.utils.d.ts.map +1 -0
- package/dist/components/hooks/useForm/types.d.ts +412 -0
- package/dist/components/hooks/useForm/types.d.ts.map +1 -0
- package/dist/components/hooks/useForm/useForm.d.ts +3 -0
- package/dist/components/hooks/useForm/useForm.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/apiClient.d.ts +99 -0
- package/dist/components/hooks/useKanban/apiClient.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/context.d.ts +4 -0
- package/dist/components/hooks/useKanban/context.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/dragDropManager.d.ts +27 -0
- package/dist/components/hooks/useKanban/dragDropManager.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/index.d.ts +6 -0
- package/dist/components/hooks/useKanban/index.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/types.d.ts +438 -0
- package/dist/components/hooks/useKanban/types.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/useKanban.d.ts +3 -0
- package/dist/components/hooks/useKanban/useKanban.d.ts.map +1 -0
- package/dist/components/hooks/useKanban/useKanbanSimple.d.ts +62 -0
- package/dist/components/hooks/useKanban/useKanbanSimple.d.ts.map +1 -0
- package/dist/components/hooks/useTable/index.d.ts +3 -0
- package/dist/components/hooks/useTable/index.d.ts.map +1 -0
- package/dist/components/hooks/useTable/types.d.ts +107 -0
- package/dist/components/hooks/useTable/types.d.ts.map +1 -0
- package/dist/components/hooks/useTable/useTable.d.ts +8 -0
- package/dist/components/hooks/useTable/useTable.d.ts.map +1 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/ui/index.d.ts +2 -0
- package/dist/components/ui/index.d.ts.map +1 -0
- package/dist/components/ui/kanban/Kanban.d.ts +12 -0
- package/dist/components/ui/kanban/Kanban.d.ts.map +1 -0
- package/dist/components/ui/kanban/index.d.ts +2 -0
- package/dist/components/ui/kanban/index.d.ts.map +1 -0
- package/dist/index.cjs +45 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +6522 -0
- package/dist/types/base-fields.d.ts +182 -0
- package/dist/types/base-fields.d.ts.map +1 -0
- package/dist/types/common.d.ts +238 -0
- package/dist/types/common.d.ts.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/cn.d.ts +7 -0
- package/dist/utils/cn.d.ts.map +1 -0
- package/dist/utils/formatting.d.ts +52 -0
- package/dist/utils/formatting.d.ts.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/package.json +98 -0
- package/sdk/api/client.ts +447 -0
- package/sdk/api/datetime.ts +33 -0
- package/sdk/api/index.ts +61 -0
- package/sdk/api/metadata.ts +148 -0
- package/sdk/components/hooks/index.ts +34 -0
- package/sdk/components/hooks/useFilter/index.ts +37 -0
- package/sdk/components/hooks/useFilter/payloadBuilder.utils.ts +298 -0
- package/sdk/components/hooks/useFilter/types.ts +158 -0
- package/sdk/components/hooks/useFilter/useFilter.llm.txt +497 -0
- package/sdk/components/hooks/useFilter/useFilter.ts +494 -0
- package/sdk/components/hooks/useFilter/validation.utils.ts +401 -0
- package/sdk/components/hooks/useForm/apiClient.ts +441 -0
- package/sdk/components/hooks/useForm/expressionValidator.utils.ts +444 -0
- package/sdk/components/hooks/useForm/index.ts +64 -0
- package/sdk/components/hooks/useForm/optimizedExpressionValidator.utils.ts +482 -0
- package/sdk/components/hooks/useForm/ruleClassifier.utils.ts +424 -0
- package/sdk/components/hooks/useForm/schemaParser.utils.ts +519 -0
- package/sdk/components/hooks/useForm/types.ts +630 -0
- package/sdk/components/hooks/useForm/useForm.llm.txt +340 -0
- package/sdk/components/hooks/useForm/useForm.ts +821 -0
- package/sdk/components/hooks/useKanban/apiClient.ts +494 -0
- package/sdk/components/hooks/useKanban/context.ts +14 -0
- package/sdk/components/hooks/useKanban/dragDropManager.ts +529 -0
- package/sdk/components/hooks/useKanban/index.ts +63 -0
- package/sdk/components/hooks/useKanban/types.ts +606 -0
- package/sdk/components/hooks/useKanban/useKanban.llm.txt +482 -0
- package/sdk/components/hooks/useKanban/useKanban.ts +725 -0
- package/sdk/components/hooks/useKanban/useKanbanSimple.ts +389 -0
- package/sdk/components/hooks/useTable/index.ts +5 -0
- package/sdk/components/hooks/useTable/types.ts +154 -0
- package/sdk/components/hooks/useTable/useTable.llm.txt +344 -0
- package/sdk/components/hooks/useTable/useTable.ts +413 -0
- package/sdk/components/index.ts +15 -0
- package/sdk/components/ui/index.ts +2 -0
- package/sdk/components/ui/kanban/Kanban.tsx +134 -0
- package/sdk/components/ui/kanban/index.ts +11 -0
- package/sdk/index.ts +13 -0
- package/sdk/types/base-fields.ts +221 -0
- package/sdk/types/common.ts +306 -0
- package/sdk/types/index.ts +5 -0
- package/sdk/utils/cn.ts +10 -0
- package/sdk/utils/formatting.ts +212 -0
- package/sdk/utils/index.ts +5 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import { useState, useCallback, useMemo, useEffect } from "react";
|
|
2
|
+
import type { Filter, LogicalOperator } from "../../../types/common";
|
|
3
|
+
import type {
|
|
4
|
+
FilterConditionWithId,
|
|
5
|
+
TypedFilterConditionInput,
|
|
6
|
+
FieldDefinition,
|
|
7
|
+
ValidationResult,
|
|
8
|
+
ValidationError,
|
|
9
|
+
FilterState,
|
|
10
|
+
UseFilterOptions,
|
|
11
|
+
UseFilterReturn,
|
|
12
|
+
} from "./types";
|
|
13
|
+
|
|
14
|
+
// ============================================================
|
|
15
|
+
// VALIDATION HELPERS
|
|
16
|
+
// ============================================================
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generate a unique ID for conditions
|
|
20
|
+
*/
|
|
21
|
+
const generateId = (): string => {
|
|
22
|
+
return crypto.randomUUID();
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Helper to check if operator is a logical operator
|
|
27
|
+
*/
|
|
28
|
+
const isLogicalOperator = (operator: string): operator is LogicalOperator => {
|
|
29
|
+
return operator === 'And' || operator === 'Or' || operator === 'Not';
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Validate a filter condition (supports both simple conditions and nested logical groups)
|
|
34
|
+
*/
|
|
35
|
+
const validateFilterCondition = <T>(
|
|
36
|
+
condition: Partial<FilterConditionWithId>,
|
|
37
|
+
fieldDefinitions?: Record<keyof T, FieldDefinition>
|
|
38
|
+
): ValidationResult => {
|
|
39
|
+
const errors: string[] = [];
|
|
40
|
+
|
|
41
|
+
// Check required fields
|
|
42
|
+
if (!condition.operator) {
|
|
43
|
+
errors.push('Operator is required');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check if this is a logical operator (nested group)
|
|
47
|
+
if (condition.operator && isLogicalOperator(condition.operator)) {
|
|
48
|
+
// Validate logical group
|
|
49
|
+
if (!condition.children || !Array.isArray(condition.children)) {
|
|
50
|
+
errors.push('Logical operators require a children array');
|
|
51
|
+
} else if (condition.children.length === 0) {
|
|
52
|
+
errors.push('Logical operators require at least one child condition');
|
|
53
|
+
} else if (condition.operator === 'Not' && condition.children.length > 1) {
|
|
54
|
+
errors.push('Not operator can only have one child condition');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Recursively validate children
|
|
58
|
+
if (condition.children && Array.isArray(condition.children)) {
|
|
59
|
+
condition.children.forEach((child, index) => {
|
|
60
|
+
const childValidation = validateFilterCondition(child, fieldDefinitions);
|
|
61
|
+
if (!childValidation.isValid) {
|
|
62
|
+
errors.push(...childValidation.errors.map(err => `Child ${index + 1}: ${err}`));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Logical operators should not have lhsField or rhsValue
|
|
68
|
+
if (condition.lhsField) {
|
|
69
|
+
errors.push('Logical operators should not have lhsField');
|
|
70
|
+
}
|
|
71
|
+
if (condition.rhsValue !== undefined) {
|
|
72
|
+
errors.push('Logical operators should not have rhsValue');
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
// Validate simple condition
|
|
76
|
+
if (!condition.lhsField) {
|
|
77
|
+
errors.push('Field is required for condition operators');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Validate operator-specific requirements
|
|
81
|
+
if (condition.operator && condition.rhsValue !== undefined) {
|
|
82
|
+
switch (condition.operator) {
|
|
83
|
+
case 'Between':
|
|
84
|
+
case 'NotBetween':
|
|
85
|
+
if (!Array.isArray(condition.rhsValue) || condition.rhsValue.length !== 2) {
|
|
86
|
+
errors.push('Between operators require an array of two values');
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
case 'IN':
|
|
90
|
+
case 'NIN':
|
|
91
|
+
if (!Array.isArray(condition.rhsValue) || condition.rhsValue.length === 0) {
|
|
92
|
+
errors.push('IN/NIN operators require a non-empty array');
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
case 'Empty':
|
|
96
|
+
case 'NotEmpty':
|
|
97
|
+
// These operators don't need RHS values
|
|
98
|
+
break;
|
|
99
|
+
default:
|
|
100
|
+
if (condition.rhsValue === null || condition.rhsValue === undefined || condition.rhsValue === '') {
|
|
101
|
+
errors.push('Value is required for this operator');
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Field-specific validation
|
|
108
|
+
if (fieldDefinitions && condition.lhsField && condition.operator) {
|
|
109
|
+
const fieldDef = fieldDefinitions[condition.lhsField as keyof T];
|
|
110
|
+
if (fieldDef) {
|
|
111
|
+
// Check if operator is allowed for this field
|
|
112
|
+
if (!fieldDef.allowedOperators.includes(condition.operator as any)) {
|
|
113
|
+
errors.push(`Operator ${condition.operator} is not allowed for field ${condition.lhsField}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Custom field validation
|
|
117
|
+
if (fieldDef.validateValue && condition.rhsValue !== undefined) {
|
|
118
|
+
const fieldValidation = fieldDef.validateValue(condition.rhsValue, condition.operator as any);
|
|
119
|
+
if (!fieldValidation.isValid) {
|
|
120
|
+
errors.push(...fieldValidation.errors);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Simple conditions should not have children
|
|
127
|
+
if (condition.children && condition.children.length > 0) {
|
|
128
|
+
errors.push('Condition operators should not have children');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
isValid: errors.length === 0,
|
|
134
|
+
errors
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Convert a single FilterConditionWithId to API format (FilterCondition or FilterLogical)
|
|
140
|
+
*/
|
|
141
|
+
const convertConditionToAPI = (condition: FilterConditionWithId): any => {
|
|
142
|
+
// Check if this is a logical operator (nested group)
|
|
143
|
+
if (isLogicalOperator(condition.operator)) {
|
|
144
|
+
// Build nested logical filter
|
|
145
|
+
return {
|
|
146
|
+
Operator: condition.operator,
|
|
147
|
+
Condition: (condition.children || [])
|
|
148
|
+
.filter(child => child.isValid)
|
|
149
|
+
.map(child => convertConditionToAPI(child))
|
|
150
|
+
};
|
|
151
|
+
} else {
|
|
152
|
+
// Build simple condition
|
|
153
|
+
return {
|
|
154
|
+
Operator: condition.operator,
|
|
155
|
+
LHSField: condition.lhsField,
|
|
156
|
+
RHSValue: condition.rhsValue,
|
|
157
|
+
RHSType: condition.rhsType || "Constant"
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Convert filter state to SDK Filter format
|
|
164
|
+
* Supports both flat and nested filter structures
|
|
165
|
+
*/
|
|
166
|
+
const buildFilterPayload = (
|
|
167
|
+
conditions: FilterConditionWithId[],
|
|
168
|
+
logicalOperator: LogicalOperator
|
|
169
|
+
): Filter | undefined => {
|
|
170
|
+
if (conditions.length === 0) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const validConditions = conditions.filter(c => c.isValid);
|
|
175
|
+
if (validConditions.length === 0) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
Operator: logicalOperator,
|
|
181
|
+
Condition: validConditions.map(c => convertConditionToAPI(c))
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// ============================================================
|
|
186
|
+
// MAIN HOOK
|
|
187
|
+
// ============================================================
|
|
188
|
+
|
|
189
|
+
export function useFilter<T = any>(
|
|
190
|
+
options: UseFilterOptions<T> = {}
|
|
191
|
+
): UseFilterReturn<T> {
|
|
192
|
+
// ============================================================
|
|
193
|
+
// STATE MANAGEMENT
|
|
194
|
+
// ============================================================
|
|
195
|
+
|
|
196
|
+
const [filterState, setFilterState] = useState<FilterState>({
|
|
197
|
+
logicalOperator: options.initialLogicalOperator || "And",
|
|
198
|
+
conditions: options.initialConditions || []
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Store initial state for reset functionality
|
|
202
|
+
const [initialState] = useState<FilterState>({
|
|
203
|
+
logicalOperator: options.initialLogicalOperator || "And",
|
|
204
|
+
conditions: options.initialConditions || []
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ============================================================
|
|
208
|
+
// VALIDATION
|
|
209
|
+
// ============================================================
|
|
210
|
+
|
|
211
|
+
const validateCondition = useCallback((condition: Partial<FilterConditionWithId>): ValidationResult => {
|
|
212
|
+
return validateFilterCondition(condition, options.fieldDefinitions);
|
|
213
|
+
}, [options.fieldDefinitions]);
|
|
214
|
+
|
|
215
|
+
const validateAllConditions = useCallback((): ValidationResult => {
|
|
216
|
+
const allErrors: string[] = [];
|
|
217
|
+
|
|
218
|
+
filterState.conditions.forEach(condition => {
|
|
219
|
+
const validation = validateCondition(condition);
|
|
220
|
+
if (!validation.isValid) {
|
|
221
|
+
allErrors.push(...validation.errors.map(err => `Condition ${condition.id}: ${err}`));
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
isValid: allErrors.length === 0,
|
|
227
|
+
errors: allErrors
|
|
228
|
+
};
|
|
229
|
+
}, [filterState.conditions, validateCondition]);
|
|
230
|
+
|
|
231
|
+
// ============================================================
|
|
232
|
+
// CONDITION MANAGEMENT
|
|
233
|
+
// ============================================================
|
|
234
|
+
|
|
235
|
+
const addCondition = useCallback((condition: TypedFilterConditionInput<T>): string => {
|
|
236
|
+
const id = generateId();
|
|
237
|
+
// Convert typed input to internal format (using unknown for type narrowing)
|
|
238
|
+
const internalCondition = condition as unknown as Omit<FilterConditionWithId, 'id' | 'isValid'>;
|
|
239
|
+
const validation = validateCondition(internalCondition);
|
|
240
|
+
|
|
241
|
+
const newCondition: FilterConditionWithId = {
|
|
242
|
+
...internalCondition,
|
|
243
|
+
id,
|
|
244
|
+
isValid: validation.isValid,
|
|
245
|
+
validationErrors: validation.errors
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
setFilterState(prev => ({
|
|
249
|
+
...prev,
|
|
250
|
+
conditions: [...prev.conditions, newCondition]
|
|
251
|
+
}));
|
|
252
|
+
|
|
253
|
+
if (options.onConditionAdd) {
|
|
254
|
+
options.onConditionAdd(newCondition);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return id;
|
|
258
|
+
}, [validateCondition, options]);
|
|
259
|
+
|
|
260
|
+
const updateCondition = useCallback((id: string, updates: Partial<TypedFilterConditionInput<T>>): boolean => {
|
|
261
|
+
let found = false;
|
|
262
|
+
// Convert typed input to internal format (using unknown for type narrowing)
|
|
263
|
+
const internalUpdates = updates as unknown as Partial<FilterConditionWithId>;
|
|
264
|
+
|
|
265
|
+
setFilterState(prev => ({
|
|
266
|
+
...prev,
|
|
267
|
+
conditions: prev.conditions.map(condition => {
|
|
268
|
+
if (condition.id === id) {
|
|
269
|
+
found = true;
|
|
270
|
+
const updatedCondition = { ...condition, ...internalUpdates };
|
|
271
|
+
const validation = validateCondition(updatedCondition);
|
|
272
|
+
|
|
273
|
+
const finalCondition = {
|
|
274
|
+
...updatedCondition,
|
|
275
|
+
isValid: validation.isValid,
|
|
276
|
+
validationErrors: validation.errors
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
if (options.onConditionUpdate) {
|
|
280
|
+
options.onConditionUpdate(finalCondition);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return finalCondition;
|
|
284
|
+
}
|
|
285
|
+
return condition;
|
|
286
|
+
})
|
|
287
|
+
}));
|
|
288
|
+
|
|
289
|
+
return found;
|
|
290
|
+
}, [validateCondition, options]);
|
|
291
|
+
|
|
292
|
+
const removeCondition = useCallback((id: string): boolean => {
|
|
293
|
+
let found = false;
|
|
294
|
+
|
|
295
|
+
setFilterState(prev => ({
|
|
296
|
+
...prev,
|
|
297
|
+
conditions: prev.conditions.filter(condition => {
|
|
298
|
+
if (condition.id === id) {
|
|
299
|
+
found = true;
|
|
300
|
+
if (options.onConditionRemove) {
|
|
301
|
+
options.onConditionRemove(id);
|
|
302
|
+
}
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
return true;
|
|
306
|
+
})
|
|
307
|
+
}));
|
|
308
|
+
|
|
309
|
+
return found;
|
|
310
|
+
}, [options]);
|
|
311
|
+
|
|
312
|
+
const clearConditions = useCallback(() => {
|
|
313
|
+
setFilterState(prev => ({
|
|
314
|
+
...prev,
|
|
315
|
+
conditions: []
|
|
316
|
+
}));
|
|
317
|
+
}, []);
|
|
318
|
+
|
|
319
|
+
const getCondition = useCallback((id: string): FilterConditionWithId | undefined => {
|
|
320
|
+
return filterState.conditions.find(condition => condition.id === id);
|
|
321
|
+
}, [filterState.conditions]);
|
|
322
|
+
|
|
323
|
+
// ============================================================
|
|
324
|
+
// LOGICAL OPERATOR MANAGEMENT
|
|
325
|
+
// ============================================================
|
|
326
|
+
|
|
327
|
+
const setLogicalOperator = useCallback((operator: LogicalOperator) => {
|
|
328
|
+
setFilterState(prev => ({
|
|
329
|
+
...prev,
|
|
330
|
+
logicalOperator: operator
|
|
331
|
+
}));
|
|
332
|
+
}, []);
|
|
333
|
+
|
|
334
|
+
// ============================================================
|
|
335
|
+
// BULK OPERATIONS
|
|
336
|
+
// ============================================================
|
|
337
|
+
|
|
338
|
+
const setConditions = useCallback((conditions: FilterConditionWithId[]) => {
|
|
339
|
+
const validatedConditions = conditions.map(condition => {
|
|
340
|
+
const validation = validateCondition(condition);
|
|
341
|
+
return {
|
|
342
|
+
...condition,
|
|
343
|
+
isValid: validation.isValid,
|
|
344
|
+
validationErrors: validation.errors
|
|
345
|
+
};
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
setFilterState(prev => ({
|
|
349
|
+
...prev,
|
|
350
|
+
conditions: validatedConditions
|
|
351
|
+
}));
|
|
352
|
+
}, [validateCondition]);
|
|
353
|
+
|
|
354
|
+
const replaceCondition = useCallback((id: string, newCondition: TypedFilterConditionInput<T>): boolean => {
|
|
355
|
+
let found = false;
|
|
356
|
+
// Convert typed input to internal format (using unknown for type narrowing)
|
|
357
|
+
const internalCondition = newCondition as unknown as Omit<FilterConditionWithId, 'id' | 'isValid'>;
|
|
358
|
+
|
|
359
|
+
setFilterState(prev => ({
|
|
360
|
+
...prev,
|
|
361
|
+
conditions: prev.conditions.map(condition => {
|
|
362
|
+
if (condition.id === id) {
|
|
363
|
+
found = true;
|
|
364
|
+
const validation = validateCondition(internalCondition);
|
|
365
|
+
return {
|
|
366
|
+
...internalCondition,
|
|
367
|
+
id,
|
|
368
|
+
isValid: validation.isValid,
|
|
369
|
+
validationErrors: validation.errors
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
return condition;
|
|
373
|
+
})
|
|
374
|
+
}));
|
|
375
|
+
|
|
376
|
+
return found;
|
|
377
|
+
}, [validateCondition]);
|
|
378
|
+
|
|
379
|
+
// ============================================================
|
|
380
|
+
// STATE MANAGEMENT
|
|
381
|
+
// ============================================================
|
|
382
|
+
|
|
383
|
+
const exportState = useCallback((): FilterState => ({
|
|
384
|
+
logicalOperator: filterState.logicalOperator,
|
|
385
|
+
conditions: filterState.conditions.map(condition => ({ ...condition })) // Deep copy
|
|
386
|
+
}), [filterState]);
|
|
387
|
+
|
|
388
|
+
const importState = useCallback((state: FilterState) => {
|
|
389
|
+
const validatedConditions = state.conditions.map(condition => {
|
|
390
|
+
const validation = validateCondition(condition);
|
|
391
|
+
return {
|
|
392
|
+
...condition,
|
|
393
|
+
isValid: validation.isValid,
|
|
394
|
+
validationErrors: validation.errors
|
|
395
|
+
};
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
setFilterState({
|
|
399
|
+
logicalOperator: state.logicalOperator,
|
|
400
|
+
conditions: validatedConditions
|
|
401
|
+
});
|
|
402
|
+
}, [validateCondition]);
|
|
403
|
+
|
|
404
|
+
const resetToInitial = useCallback(() => {
|
|
405
|
+
setFilterState({ ...initialState });
|
|
406
|
+
}, [initialState]);
|
|
407
|
+
|
|
408
|
+
// ============================================================
|
|
409
|
+
// COMPUTED VALUES
|
|
410
|
+
// ============================================================
|
|
411
|
+
|
|
412
|
+
const filterPayload = useMemo(() =>
|
|
413
|
+
buildFilterPayload(filterState.conditions, filterState.logicalOperator),
|
|
414
|
+
[filterState.conditions, filterState.logicalOperator]
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const validationErrors = useMemo((): ValidationError[] => {
|
|
418
|
+
const errors: ValidationError[] = [];
|
|
419
|
+
|
|
420
|
+
filterState.conditions.forEach(condition => {
|
|
421
|
+
if (!condition.isValid && condition.validationErrors) {
|
|
422
|
+
condition.validationErrors.forEach(error => {
|
|
423
|
+
errors.push({
|
|
424
|
+
conditionId: condition.id,
|
|
425
|
+
field: condition.lhsField || '', // Empty string for logical operators
|
|
426
|
+
message: error
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
return errors;
|
|
433
|
+
}, [filterState.conditions]);
|
|
434
|
+
|
|
435
|
+
const isValid = useMemo(() =>
|
|
436
|
+
filterState.conditions.every(condition => condition.isValid),
|
|
437
|
+
[filterState.conditions]
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
const getConditionCount = useCallback(() => filterState.conditions.length, [filterState.conditions]);
|
|
441
|
+
const hasConditions = useMemo(() => filterState.conditions.length > 0, [filterState.conditions]);
|
|
442
|
+
const canAddCondition = useMemo(() => true, []); // Can always add more conditions
|
|
443
|
+
|
|
444
|
+
// ============================================================
|
|
445
|
+
// VALIDATION ERROR CALLBACK
|
|
446
|
+
// ============================================================
|
|
447
|
+
|
|
448
|
+
useEffect(() => {
|
|
449
|
+
if (options.onValidationError && validationErrors.length > 0) {
|
|
450
|
+
options.onValidationError(validationErrors);
|
|
451
|
+
}
|
|
452
|
+
}, [validationErrors, options]);
|
|
453
|
+
|
|
454
|
+
// ============================================================
|
|
455
|
+
// RETURN OBJECT
|
|
456
|
+
// ============================================================
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
// Current state
|
|
460
|
+
conditions: filterState.conditions,
|
|
461
|
+
logicalOperator: filterState.logicalOperator,
|
|
462
|
+
filterPayload,
|
|
463
|
+
isValid,
|
|
464
|
+
validationErrors,
|
|
465
|
+
|
|
466
|
+
// Condition management
|
|
467
|
+
addCondition,
|
|
468
|
+
updateCondition,
|
|
469
|
+
removeCondition,
|
|
470
|
+
clearConditions,
|
|
471
|
+
getCondition,
|
|
472
|
+
|
|
473
|
+
// Logical operator management
|
|
474
|
+
setLogicalOperator,
|
|
475
|
+
|
|
476
|
+
// Bulk operations
|
|
477
|
+
setConditions,
|
|
478
|
+
replaceCondition,
|
|
479
|
+
|
|
480
|
+
// Validation
|
|
481
|
+
validateCondition,
|
|
482
|
+
validateAllConditions,
|
|
483
|
+
|
|
484
|
+
// State management
|
|
485
|
+
exportState,
|
|
486
|
+
importState,
|
|
487
|
+
resetToInitial,
|
|
488
|
+
|
|
489
|
+
// Utilities
|
|
490
|
+
getConditionCount,
|
|
491
|
+
hasConditions,
|
|
492
|
+
canAddCondition
|
|
493
|
+
};
|
|
494
|
+
}
|