@akinon/akifilter 1.2.0-next.1 → 1.2.0-next.11
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/dist/cjs/akifilter.d.ts.map +1 -1
- package/dist/cjs/akifilter.js +91 -13
- package/dist/esm/akifilter.d.ts.map +1 -1
- package/dist/esm/akifilter.js +92 -14
- package/package.json +10 -10
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"akifilter.d.ts","sourceRoot":"","sources":["../../src/akifilter.tsx"],"names":[],"mappings":"AAAA,OAAO,cAAc,CAAC;AAGtB,OAAO,EAIL,WAAW,
|
|
1
|
+
{"version":3,"file":"akifilter.d.ts","sourceRoot":"","sources":["../../src/akifilter.tsx"],"names":[],"mappings":"AAAA,OAAO,cAAc,CAAC;AAGtB,OAAO,EAIL,WAAW,EACX,IAAI,EAIL,MAAM,iBAAiB,CAAC;AAWzB,OAAO,KAAK,MAAM,OAAO,CAAC;AA0B1B,OAAO,KAAK,EAAkB,eAAe,EAAE,MAAM,SAAS,CAAC;AAY/D,KAAK,oBAAoB,GAAG,WAAW,CAAC;AA0HxC,MAAM,MAAM,cAAc,CACxB,YAAY,SAAS,oBAAoB,GAAG,oBAAoB,IAC9D;IACF;;OAEG;IACH,YAAY,CAAC,EAAE,eAAe,CAAC,YAAY,CAAC,CAAC;IAC7C;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;OAEG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IACtC;;OAEG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC;IACzD;;OAEG;IACH,qBAAqB,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,KAAK,IAAI,CAAC;IAClE;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAqwBF,eAAO,MAAM,SAAS;KACpB,YAAY,SAAS,oBAAoB,uBAElC,cAAc,CAAC,YAAY,CAAC;;CAmBpC,CAAC"}
|
package/dist/cjs/akifilter.js
CHANGED
|
@@ -37,6 +37,32 @@ const use_debounced_value_1 = require("./hooks/use-debounced-value");
|
|
|
37
37
|
const i18n_1 = require("./i18n");
|
|
38
38
|
const schema_1 = require("./utils/schema");
|
|
39
39
|
const values_1 = require("./utils/values");
|
|
40
|
+
const FilterFormItem = (_a) => {
|
|
41
|
+
var _b, _c;
|
|
42
|
+
var { children, control, name, valuePropName } = _a, props = __rest(_a, ["children", "control", "name", "valuePropName"]);
|
|
43
|
+
const { field, fieldState } = (0, akiform_1.useController)({ name, control });
|
|
44
|
+
const childrenWithProps = react_1.default.Children.map(children, child => {
|
|
45
|
+
if (react_1.default.isValidElement(child)) {
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
+
const typedChild = child;
|
|
48
|
+
return react_1.default.cloneElement(typedChild, Object.assign(Object.assign(Object.assign({}, field), {
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
|
+
onChange: (...params) => {
|
|
51
|
+
var _a, _b;
|
|
52
|
+
(_b = (_a = typedChild.props).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, params);
|
|
53
|
+
field.onChange(...params);
|
|
54
|
+
}, onBlur: () => {
|
|
55
|
+
var _a, _b;
|
|
56
|
+
(_b = (_a = typedChild.props).onBlur) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
57
|
+
field.onBlur();
|
|
58
|
+
}, ref: undefined }), (valuePropName && {
|
|
59
|
+
[valuePropName]: field.value
|
|
60
|
+
})));
|
|
61
|
+
}
|
|
62
|
+
return child;
|
|
63
|
+
});
|
|
64
|
+
return (react_1.default.createElement(akiform_1.Akiform.Item, Object.assign({}, props, { validateStatus: fieldState.invalid ? 'error' : undefined, help: (_c = (_b = fieldState.error) === null || _b === void 0 ? void 0 : _b.message) !== null && _c !== void 0 ? _c : props.help, className: props.className }), childrenWithProps));
|
|
65
|
+
};
|
|
40
66
|
/**
|
|
41
67
|
* Checks if a field should be disabled based on config.disabled property.
|
|
42
68
|
*/
|
|
@@ -86,9 +112,15 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
|
|
|
86
112
|
const schemaDefaults = react_1.default.useMemo(() => (0, schema_1.normaliseValuesBySchema)(flattenedSchema, (0, schema_1.extractDefaultValues)(flattenedSchema)), [flattenedSchema]);
|
|
87
113
|
const externalDefaults = react_1.default.useMemo(() => (0, schema_1.normaliseValuesBySchema)(flattenedSchema, defaultValues), [flattenedSchema, defaultValues]);
|
|
88
114
|
const baseDefaultValues = react_1.default.useMemo(() => (Object.assign(Object.assign({}, schemaDefaults), externalDefaults)), [schemaDefaults, externalDefaults]);
|
|
89
|
-
|
|
115
|
+
// Use state for persistedDefaults to make it reactive to localStorage changes
|
|
116
|
+
const [persistedDefaults, setPersistedDefaults] = react_1.default.useState(() => {
|
|
90
117
|
var _a;
|
|
91
118
|
return (0, schema_1.normaliseValuesBySchema)(flattenedSchema, (_a = (0, storage_1.readStoredValues)(regularFields, storageKey)) !== null && _a !== void 0 ? _a : undefined);
|
|
119
|
+
});
|
|
120
|
+
// Re-read from localStorage when storageKey changes
|
|
121
|
+
react_1.default.useEffect(() => {
|
|
122
|
+
var _a;
|
|
123
|
+
setPersistedDefaults((0, schema_1.normaliseValuesBySchema)(flattenedSchema, (_a = (0, storage_1.readStoredValues)(regularFields, storageKey)) !== null && _a !== void 0 ? _a : undefined));
|
|
92
124
|
}, [flattenedSchema, regularFields, storageKey]);
|
|
93
125
|
const mergedDefaultValues = react_1.default.useMemo(() => (Object.assign(Object.assign({}, baseDefaultValues), persistedDefaults)), [baseDefaultValues, persistedDefaults]);
|
|
94
126
|
const formMethods = (0, akiform_1.useForm)({
|
|
@@ -187,8 +219,20 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
|
|
|
187
219
|
(0, storage_1.writeVisibleKeys)(storageKey, visibleKeys);
|
|
188
220
|
onVisibleFieldsChange === null || onVisibleFieldsChange === void 0 ? void 0 : onVisibleFieldsChange(visibleKeys);
|
|
189
221
|
}, [visibleKeys, onVisibleFieldsChange, storageKey]);
|
|
222
|
+
// Track previous mergedDefaultValues to avoid unnecessary resets
|
|
223
|
+
const previousMergedDefaultValuesRef = react_1.default.useRef(null);
|
|
190
224
|
react_1.default.useEffect(() => {
|
|
191
|
-
|
|
225
|
+
const serialised = JSON.stringify(mergedDefaultValues);
|
|
226
|
+
// Skip if the values haven't actually changed (deep comparison)
|
|
227
|
+
if (previousMergedDefaultValuesRef.current === serialised) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
// Skip reset on initial mount - useForm already uses defaultValues
|
|
231
|
+
if (previousMergedDefaultValuesRef.current === null) {
|
|
232
|
+
previousMergedDefaultValuesRef.current = serialised;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
previousMergedDefaultValuesRef.current = serialised;
|
|
192
236
|
}, [formMethods, mergedDefaultValues]);
|
|
193
237
|
const normalisedValues = react_1.default.useMemo(() => (0, values_1.normaliseOutputValues)(flattenedSchema, formValues), [flattenedSchema, formValues]);
|
|
194
238
|
const hasInitialValuesRef = react_1.default.useRef(false);
|
|
@@ -245,6 +289,38 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
|
|
|
245
289
|
react_1.default.useEffect(() => {
|
|
246
290
|
lastPersistedValuesRef.current = null;
|
|
247
291
|
}, [storageKey]);
|
|
292
|
+
// Track previous serialised values to detect field removals
|
|
293
|
+
const previousSerialisedValuesRef = react_1.default.useRef(serialisedValues);
|
|
294
|
+
// Immediate effect for field removals (bypasses debounce)
|
|
295
|
+
react_1.default.useEffect(() => {
|
|
296
|
+
const previous = previousSerialisedValuesRef.current;
|
|
297
|
+
const current = serialisedValues;
|
|
298
|
+
previousSerialisedValuesRef.current = current;
|
|
299
|
+
// Skip if this is the initial render
|
|
300
|
+
if (!hasEmittedValuesRef.current) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
// Skip if values haven't actually changed
|
|
304
|
+
if (previous === current) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const prevParsed = JSON.parse(previous);
|
|
308
|
+
const currParsed = JSON.parse(current);
|
|
309
|
+
// Detect if a field was removed (existed in prev but not in current)
|
|
310
|
+
const prevKeys = Object.keys(prevParsed);
|
|
311
|
+
const currKeys = Object.keys(currParsed);
|
|
312
|
+
const removedKeys = prevKeys.filter(key => !currKeys.includes(key));
|
|
313
|
+
// If a field was removed, immediately persist and emit (bypass debounce)
|
|
314
|
+
if (removedKeys.length > 0) {
|
|
315
|
+
const nextSerialised = current;
|
|
316
|
+
persistValues(currParsed, nextSerialised);
|
|
317
|
+
lastPersistedValuesRef.current = nextSerialised;
|
|
318
|
+
currentSerialisedValuesRef.current = nextSerialised;
|
|
319
|
+
// Update persistedDefaults to prevent stale mergedDefaultValues on re-render
|
|
320
|
+
setPersistedDefaults(currParsed);
|
|
321
|
+
onValuesChange === null || onValuesChange === void 0 ? void 0 : onValuesChange(currParsed);
|
|
322
|
+
}
|
|
323
|
+
}, [serialisedValues, onValuesChange, persistValues]);
|
|
248
324
|
const handleClearAll = react_1.default.useCallback(() => {
|
|
249
325
|
const clearedDefaults = Object.assign({}, schemaDefaults);
|
|
250
326
|
flattenedSchema.forEach(field => {
|
|
@@ -303,24 +379,26 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
|
|
|
303
379
|
return;
|
|
304
380
|
}
|
|
305
381
|
const defaultValue = schemaDefaults[String(schemaField.key)];
|
|
306
|
-
const fieldPath = schemaField.key;
|
|
307
382
|
const nextValue = resolveClearedFieldValue(schemaField, defaultValue);
|
|
308
|
-
//
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
});
|
|
314
|
-
// Compute the updated values immediately (don't wait for debounce)
|
|
315
|
-
const currentValues = formMethods.getValues() || {};
|
|
316
|
-
const updatedFormValues = Object.assign(Object.assign({}, currentValues), { [fieldPath]: nextValue });
|
|
383
|
+
// Manually construct the updated form values instead of relying on setValue + getValues
|
|
384
|
+
// because setValue is batched and getValues() wouldn't reflect the change immediately
|
|
385
|
+
const currentFormValues = formMethods.getValues();
|
|
386
|
+
const updatedFormValues = Object.assign(Object.assign({}, currentFormValues), { [String(schemaField.key)]: nextValue });
|
|
387
|
+
// Normalize the values (removes empty/undefined entries)
|
|
317
388
|
const nextValues = (0, values_1.normaliseOutputValues)(flattenedSchema, updatedFormValues);
|
|
389
|
+
// Update form state
|
|
390
|
+
formMethods.reset(updatedFormValues, {
|
|
391
|
+
keepDirty: false,
|
|
392
|
+
keepTouched: false,
|
|
393
|
+
keepValues: false
|
|
394
|
+
});
|
|
318
395
|
const nextSerialised = JSON.stringify(nextValues);
|
|
319
396
|
// Persist immediately (bypass debounce for remove action)
|
|
320
397
|
persistValues(nextValues);
|
|
321
398
|
// Update refs to mark this as the latest persisted state
|
|
322
399
|
lastPersistedValuesRef.current = nextSerialised;
|
|
323
400
|
currentSerialisedValuesRef.current = nextSerialised;
|
|
401
|
+
previousSerialisedValuesRef.current = nextSerialised; // Prevent immediate effect from re-emitting
|
|
324
402
|
hasEmittedValuesRef.current = true;
|
|
325
403
|
hasInitialValuesRef.current = Object.keys(nextValues).length > 0;
|
|
326
404
|
// Emit the change to parent
|
|
@@ -387,7 +465,7 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
|
|
|
387
465
|
}
|
|
388
466
|
};
|
|
389
467
|
const renderFormField = (field) => {
|
|
390
|
-
return (react_1.default.createElement(
|
|
468
|
+
return (react_1.default.createElement(FilterFormItem, { key: String(field.key), control: formMethods.control, name: field.key, tooltip: field.tooltip, help: field.help, labelDescription: field.labelDescription, required: Boolean(field.validation), className: "mb-0", valuePropName: field.type === 'checkbox' ? 'checked' : undefined }, renderFieldComponent(field)));
|
|
391
469
|
};
|
|
392
470
|
return (react_1.default.createElement(ui_card_1.Card, { size: "small", className: "akinon-filter shadow", "data-testid": "akifilter-root" },
|
|
393
471
|
react_1.default.createElement(antd_1.ConfigProvider, { theme: theme_overrides_1.themeOverrides },
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"akifilter.d.ts","sourceRoot":"","sources":["../../src/akifilter.tsx"],"names":[],"mappings":"AAAA,OAAO,cAAc,CAAC;AAGtB,OAAO,EAIL,WAAW,
|
|
1
|
+
{"version":3,"file":"akifilter.d.ts","sourceRoot":"","sources":["../../src/akifilter.tsx"],"names":[],"mappings":"AAAA,OAAO,cAAc,CAAC;AAGtB,OAAO,EAIL,WAAW,EACX,IAAI,EAIL,MAAM,iBAAiB,CAAC;AAWzB,OAAO,KAAK,MAAM,OAAO,CAAC;AA0B1B,OAAO,KAAK,EAAkB,eAAe,EAAE,MAAM,SAAS,CAAC;AAY/D,KAAK,oBAAoB,GAAG,WAAW,CAAC;AA0HxC,MAAM,MAAM,cAAc,CACxB,YAAY,SAAS,oBAAoB,GAAG,oBAAoB,IAC9D;IACF;;OAEG;IACH,YAAY,CAAC,EAAE,eAAe,CAAC,YAAY,CAAC,CAAC;IAC7C;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;OAEG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IACtC;;OAEG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC;IACzD;;OAEG;IACH,qBAAqB,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,KAAK,IAAI,CAAC;IAClE;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IACzB;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAqwBF,eAAO,MAAM,SAAS;KACpB,YAAY,SAAS,oBAAoB,uBAElC,cAAc,CAAC,YAAY,CAAC;;CAmBpC,CAAC"}
|
package/dist/esm/akifilter.js
CHANGED
|
@@ -11,7 +11,7 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
11
11
|
};
|
|
12
12
|
import './styles.css';
|
|
13
13
|
import { akidate } from '@akinon/akidate';
|
|
14
|
-
import { Akiform,
|
|
14
|
+
import { Akiform, useController, useForm, useWatch } from '@akinon/akiform';
|
|
15
15
|
import { Button } from '@akinon/ui-button';
|
|
16
16
|
import { Card } from '@akinon/ui-card';
|
|
17
17
|
import { Checkbox } from '@akinon/ui-checkbox';
|
|
@@ -34,6 +34,32 @@ import { useDebouncedValue } from './hooks/use-debounced-value';
|
|
|
34
34
|
import { i18n } from './i18n';
|
|
35
35
|
import { deriveDefaultVisibleKeys, ensureSchemaOrder, extractDefaultValues, flattenSchema, getFieldAriaLabel, normaliseValuesBySchema, partitionSchema } from './utils/schema';
|
|
36
36
|
import { normaliseOutputValues } from './utils/values';
|
|
37
|
+
const FilterFormItem = (_a) => {
|
|
38
|
+
var _b, _c;
|
|
39
|
+
var { children, control, name, valuePropName } = _a, props = __rest(_a, ["children", "control", "name", "valuePropName"]);
|
|
40
|
+
const { field, fieldState } = useController({ name, control });
|
|
41
|
+
const childrenWithProps = React.Children.map(children, child => {
|
|
42
|
+
if (React.isValidElement(child)) {
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
44
|
+
const typedChild = child;
|
|
45
|
+
return React.cloneElement(typedChild, Object.assign(Object.assign(Object.assign({}, field), {
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
+
onChange: (...params) => {
|
|
48
|
+
var _a, _b;
|
|
49
|
+
(_b = (_a = typedChild.props).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, params);
|
|
50
|
+
field.onChange(...params);
|
|
51
|
+
}, onBlur: () => {
|
|
52
|
+
var _a, _b;
|
|
53
|
+
(_b = (_a = typedChild.props).onBlur) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
54
|
+
field.onBlur();
|
|
55
|
+
}, ref: undefined }), (valuePropName && {
|
|
56
|
+
[valuePropName]: field.value
|
|
57
|
+
})));
|
|
58
|
+
}
|
|
59
|
+
return child;
|
|
60
|
+
});
|
|
61
|
+
return (React.createElement(Akiform.Item, Object.assign({}, props, { validateStatus: fieldState.invalid ? 'error' : undefined, help: (_c = (_b = fieldState.error) === null || _b === void 0 ? void 0 : _b.message) !== null && _c !== void 0 ? _c : props.help, className: props.className }), childrenWithProps));
|
|
62
|
+
};
|
|
37
63
|
/**
|
|
38
64
|
* Checks if a field should be disabled based on config.disabled property.
|
|
39
65
|
*/
|
|
@@ -83,9 +109,15 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
|
|
|
83
109
|
const schemaDefaults = React.useMemo(() => normaliseValuesBySchema(flattenedSchema, extractDefaultValues(flattenedSchema)), [flattenedSchema]);
|
|
84
110
|
const externalDefaults = React.useMemo(() => normaliseValuesBySchema(flattenedSchema, defaultValues), [flattenedSchema, defaultValues]);
|
|
85
111
|
const baseDefaultValues = React.useMemo(() => (Object.assign(Object.assign({}, schemaDefaults), externalDefaults)), [schemaDefaults, externalDefaults]);
|
|
86
|
-
|
|
112
|
+
// Use state for persistedDefaults to make it reactive to localStorage changes
|
|
113
|
+
const [persistedDefaults, setPersistedDefaults] = React.useState(() => {
|
|
87
114
|
var _a;
|
|
88
115
|
return normaliseValuesBySchema(flattenedSchema, (_a = readStoredValues(regularFields, storageKey)) !== null && _a !== void 0 ? _a : undefined);
|
|
116
|
+
});
|
|
117
|
+
// Re-read from localStorage when storageKey changes
|
|
118
|
+
React.useEffect(() => {
|
|
119
|
+
var _a;
|
|
120
|
+
setPersistedDefaults(normaliseValuesBySchema(flattenedSchema, (_a = readStoredValues(regularFields, storageKey)) !== null && _a !== void 0 ? _a : undefined));
|
|
89
121
|
}, [flattenedSchema, regularFields, storageKey]);
|
|
90
122
|
const mergedDefaultValues = React.useMemo(() => (Object.assign(Object.assign({}, baseDefaultValues), persistedDefaults)), [baseDefaultValues, persistedDefaults]);
|
|
91
123
|
const formMethods = useForm({
|
|
@@ -184,8 +216,20 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
|
|
|
184
216
|
writeVisibleKeys(storageKey, visibleKeys);
|
|
185
217
|
onVisibleFieldsChange === null || onVisibleFieldsChange === void 0 ? void 0 : onVisibleFieldsChange(visibleKeys);
|
|
186
218
|
}, [visibleKeys, onVisibleFieldsChange, storageKey]);
|
|
219
|
+
// Track previous mergedDefaultValues to avoid unnecessary resets
|
|
220
|
+
const previousMergedDefaultValuesRef = React.useRef(null);
|
|
187
221
|
React.useEffect(() => {
|
|
188
|
-
|
|
222
|
+
const serialised = JSON.stringify(mergedDefaultValues);
|
|
223
|
+
// Skip if the values haven't actually changed (deep comparison)
|
|
224
|
+
if (previousMergedDefaultValuesRef.current === serialised) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Skip reset on initial mount - useForm already uses defaultValues
|
|
228
|
+
if (previousMergedDefaultValuesRef.current === null) {
|
|
229
|
+
previousMergedDefaultValuesRef.current = serialised;
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
previousMergedDefaultValuesRef.current = serialised;
|
|
189
233
|
}, [formMethods, mergedDefaultValues]);
|
|
190
234
|
const normalisedValues = React.useMemo(() => normaliseOutputValues(flattenedSchema, formValues), [flattenedSchema, formValues]);
|
|
191
235
|
const hasInitialValuesRef = React.useRef(false);
|
|
@@ -242,6 +286,38 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
|
|
|
242
286
|
React.useEffect(() => {
|
|
243
287
|
lastPersistedValuesRef.current = null;
|
|
244
288
|
}, [storageKey]);
|
|
289
|
+
// Track previous serialised values to detect field removals
|
|
290
|
+
const previousSerialisedValuesRef = React.useRef(serialisedValues);
|
|
291
|
+
// Immediate effect for field removals (bypasses debounce)
|
|
292
|
+
React.useEffect(() => {
|
|
293
|
+
const previous = previousSerialisedValuesRef.current;
|
|
294
|
+
const current = serialisedValues;
|
|
295
|
+
previousSerialisedValuesRef.current = current;
|
|
296
|
+
// Skip if this is the initial render
|
|
297
|
+
if (!hasEmittedValuesRef.current) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
// Skip if values haven't actually changed
|
|
301
|
+
if (previous === current) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
const prevParsed = JSON.parse(previous);
|
|
305
|
+
const currParsed = JSON.parse(current);
|
|
306
|
+
// Detect if a field was removed (existed in prev but not in current)
|
|
307
|
+
const prevKeys = Object.keys(prevParsed);
|
|
308
|
+
const currKeys = Object.keys(currParsed);
|
|
309
|
+
const removedKeys = prevKeys.filter(key => !currKeys.includes(key));
|
|
310
|
+
// If a field was removed, immediately persist and emit (bypass debounce)
|
|
311
|
+
if (removedKeys.length > 0) {
|
|
312
|
+
const nextSerialised = current;
|
|
313
|
+
persistValues(currParsed, nextSerialised);
|
|
314
|
+
lastPersistedValuesRef.current = nextSerialised;
|
|
315
|
+
currentSerialisedValuesRef.current = nextSerialised;
|
|
316
|
+
// Update persistedDefaults to prevent stale mergedDefaultValues on re-render
|
|
317
|
+
setPersistedDefaults(currParsed);
|
|
318
|
+
onValuesChange === null || onValuesChange === void 0 ? void 0 : onValuesChange(currParsed);
|
|
319
|
+
}
|
|
320
|
+
}, [serialisedValues, onValuesChange, persistValues]);
|
|
245
321
|
const handleClearAll = React.useCallback(() => {
|
|
246
322
|
const clearedDefaults = Object.assign({}, schemaDefaults);
|
|
247
323
|
flattenedSchema.forEach(field => {
|
|
@@ -300,24 +376,26 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
|
|
|
300
376
|
return;
|
|
301
377
|
}
|
|
302
378
|
const defaultValue = schemaDefaults[String(schemaField.key)];
|
|
303
|
-
const fieldPath = schemaField.key;
|
|
304
379
|
const nextValue = resolveClearedFieldValue(schemaField, defaultValue);
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
});
|
|
311
|
-
// Compute the updated values immediately (don't wait for debounce)
|
|
312
|
-
const currentValues = formMethods.getValues() || {};
|
|
313
|
-
const updatedFormValues = Object.assign(Object.assign({}, currentValues), { [fieldPath]: nextValue });
|
|
380
|
+
// Manually construct the updated form values instead of relying on setValue + getValues
|
|
381
|
+
// because setValue is batched and getValues() wouldn't reflect the change immediately
|
|
382
|
+
const currentFormValues = formMethods.getValues();
|
|
383
|
+
const updatedFormValues = Object.assign(Object.assign({}, currentFormValues), { [String(schemaField.key)]: nextValue });
|
|
384
|
+
// Normalize the values (removes empty/undefined entries)
|
|
314
385
|
const nextValues = normaliseOutputValues(flattenedSchema, updatedFormValues);
|
|
386
|
+
// Update form state
|
|
387
|
+
formMethods.reset(updatedFormValues, {
|
|
388
|
+
keepDirty: false,
|
|
389
|
+
keepTouched: false,
|
|
390
|
+
keepValues: false
|
|
391
|
+
});
|
|
315
392
|
const nextSerialised = JSON.stringify(nextValues);
|
|
316
393
|
// Persist immediately (bypass debounce for remove action)
|
|
317
394
|
persistValues(nextValues);
|
|
318
395
|
// Update refs to mark this as the latest persisted state
|
|
319
396
|
lastPersistedValuesRef.current = nextSerialised;
|
|
320
397
|
currentSerialisedValuesRef.current = nextSerialised;
|
|
398
|
+
previousSerialisedValuesRef.current = nextSerialised; // Prevent immediate effect from re-emitting
|
|
321
399
|
hasEmittedValuesRef.current = true;
|
|
322
400
|
hasInitialValuesRef.current = Object.keys(nextValues).length > 0;
|
|
323
401
|
// Emit the change to parent
|
|
@@ -384,7 +462,7 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
|
|
|
384
462
|
}
|
|
385
463
|
};
|
|
386
464
|
const renderFormField = (field) => {
|
|
387
|
-
return (React.createElement(
|
|
465
|
+
return (React.createElement(FilterFormItem, { key: String(field.key), control: formMethods.control, name: field.key, tooltip: field.tooltip, help: field.help, labelDescription: field.labelDescription, required: Boolean(field.validation), className: "mb-0", valuePropName: field.type === 'checkbox' ? 'checked' : undefined }, renderFieldComponent(field)));
|
|
388
466
|
};
|
|
389
467
|
return (React.createElement(Card, { size: "small", className: "akinon-filter shadow", "data-testid": "akifilter-root" },
|
|
390
468
|
React.createElement(ConfigProvider, { theme: themeOverrides },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/akifilter",
|
|
3
|
-
"version": "1.2.0-next.
|
|
3
|
+
"version": "1.2.0-next.11",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Akifilter is a filtering library for Akinon frontend applications.",
|
|
6
6
|
"type": "module",
|
|
@@ -14,31 +14,31 @@
|
|
|
14
14
|
"react-error-boundary": "^6.0.0",
|
|
15
15
|
"@akinon/akiform": "1.1.2",
|
|
16
16
|
"@akinon/akilocale": "1.2.1",
|
|
17
|
-
"@akinon/ui-button": "1.4.0-next.1",
|
|
18
17
|
"@akinon/icons": "1.1.2-next.1",
|
|
18
|
+
"@akinon/ui-button": "1.4.0-next.1",
|
|
19
19
|
"@akinon/ui-card": "1.1.3-next.1",
|
|
20
20
|
"@akinon/ui-checkbox": "1.3.3-next.1",
|
|
21
21
|
"@akinon/ui-collapse": "1.3.2-next.1",
|
|
22
22
|
"@akinon/ui-date-picker": "1.3.3-next.1",
|
|
23
|
-
"@akinon/
|
|
23
|
+
"@akinon/akidate": "1.1.3-next.0",
|
|
24
24
|
"@akinon/ui-input": "1.1.3-next.1",
|
|
25
|
-
"@akinon/ui-
|
|
26
|
-
"@akinon/ui-typography": "1.1.2-next.0",
|
|
25
|
+
"@akinon/ui-input-number": "1.3.3-next.1",
|
|
27
26
|
"@akinon/ui-modal": "1.1.3-next.1",
|
|
27
|
+
"@akinon/ui-space": "1.3.3-next.1",
|
|
28
|
+
"@akinon/ui-select": "1.3.4-next.1",
|
|
28
29
|
"@akinon/ui-pagination": "1.3.4-next.1",
|
|
29
|
-
"@akinon/
|
|
30
|
-
"@akinon/ui-space": "1.3.3-next.1"
|
|
30
|
+
"@akinon/ui-typography": "1.1.2-next.0"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"clean-package": "2.2.0",
|
|
34
34
|
"copyfiles": "^2.4.1",
|
|
35
35
|
"rimraf": "^5.0.5",
|
|
36
36
|
"typescript": "*",
|
|
37
|
-
"@akinon/utils": "1.1.4-next.1",
|
|
38
37
|
"@akinon/akiform-builder": "1.3.5-next.1",
|
|
39
38
|
"@akinon/typescript-config": "1.1.1",
|
|
40
|
-
"@akinon/
|
|
41
|
-
"@akinon/
|
|
39
|
+
"@akinon/ui-theme": "1.1.3-next.1",
|
|
40
|
+
"@akinon/utils": "1.1.4-next.1",
|
|
41
|
+
"@akinon/vitest-config": "1.1.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
44
|
"react": "^18 || ^19",
|