@akinon/akifilter 1.7.1 → 1.8.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/dist/cjs/akifilter.d.ts +0 -10
- package/dist/cjs/akifilter.d.ts.map +1 -1
- package/dist/cjs/akifilter.js +14 -108
- package/dist/cjs/common/storage.d.ts +1 -0
- package/dist/cjs/common/storage.d.ts.map +1 -1
- package/dist/cjs/common/storage.js +3 -1
- package/dist/cjs/components/section-fieldset.d.ts +29 -0
- package/dist/cjs/components/section-fieldset.d.ts.map +1 -0
- package/dist/cjs/components/section-fieldset.js +97 -0
- package/dist/cjs/hooks/use-field-visibility.d.ts +45 -0
- package/dist/cjs/hooks/use-field-visibility.d.ts.map +1 -0
- package/dist/cjs/hooks/use-field-visibility.js +129 -0
- package/dist/cjs/styles.css +23 -0
- package/dist/cjs/types.d.ts +8 -1
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/cjs/utils/schema.d.ts +3 -13
- package/dist/cjs/utils/schema.d.ts.map +1 -1
- package/dist/esm/akifilter.d.ts +0 -10
- package/dist/esm/akifilter.d.ts.map +1 -1
- package/dist/esm/akifilter.js +16 -109
- package/dist/esm/common/storage.d.ts +1 -0
- package/dist/esm/common/storage.d.ts.map +1 -1
- package/dist/esm/common/storage.js +1 -0
- package/dist/esm/components/section-fieldset.d.ts +29 -0
- package/dist/esm/components/section-fieldset.d.ts.map +1 -0
- package/dist/esm/components/section-fieldset.js +93 -0
- package/dist/esm/hooks/use-field-visibility.d.ts +45 -0
- package/dist/esm/hooks/use-field-visibility.d.ts.map +1 -0
- package/dist/esm/hooks/use-field-visibility.js +125 -0
- package/dist/esm/styles.css +23 -0
- package/dist/esm/types.d.ts +8 -1
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/esm/utils/schema.d.ts +3 -13
- package/dist/esm/utils/schema.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { FieldValues } from '@akinon/akiform';
|
|
2
|
-
import {
|
|
3
|
-
import type { AkifilterField, AkifilterSchema } from '../types';
|
|
2
|
+
import type { AkifilterField, AkifilterSchema, AkifilterSectionField } from '../types';
|
|
4
3
|
/**
|
|
5
4
|
* Flattens schema by extracting all nested fields from section fields.
|
|
6
5
|
* This is used for storage operations and form value management.
|
|
@@ -11,18 +10,9 @@ export declare const flattenSchema: <TFieldValues extends FieldValues = FieldVal
|
|
|
11
10
|
*/
|
|
12
11
|
export declare const partitionSchema: <TFieldValues extends FieldValues = FieldValues>(schema: AkifilterSchema<TFieldValues>) => {
|
|
13
12
|
regularFields: AkifilterSchema<TFieldValues>;
|
|
14
|
-
sectionFields: Array<
|
|
15
|
-
type: typeof FIELD_TYPES.SECTION;
|
|
16
|
-
fields: AkifilterSchema<TFieldValues>;
|
|
17
|
-
defaultExpanded?: boolean;
|
|
18
|
-
isExcludeSection?: boolean;
|
|
19
|
-
}>;
|
|
13
|
+
sectionFields: Array<AkifilterSectionField<TFieldValues>>;
|
|
20
14
|
};
|
|
21
|
-
export declare const deriveExcludeSectionKeys: <TFieldValues extends FieldValues = FieldValues>(sectionFields: Array<
|
|
22
|
-
type: typeof FIELD_TYPES.SECTION;
|
|
23
|
-
fields: AkifilterSchema<TFieldValues>;
|
|
24
|
-
isExcludeSection?: boolean;
|
|
25
|
-
}>) => Set<string>;
|
|
15
|
+
export declare const deriveExcludeSectionKeys: <TFieldValues extends FieldValues = FieldValues>(sectionFields: Array<AkifilterSectionField<TFieldValues>>) => Set<string>;
|
|
26
16
|
export declare const getDisplayLabel: <TFieldValues extends FieldValues = FieldValues>(field: AkifilterField<TFieldValues>) => string;
|
|
27
17
|
export declare const getFieldAriaLabel: <TFieldValues extends FieldValues = FieldValues>(field: AkifilterField<TFieldValues>) => string;
|
|
28
18
|
export declare const ensureSchemaOrder: <TFieldValues extends FieldValues = FieldValues>(schema: AkifilterSchema<TFieldValues>, keys: string[]) => string[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/utils/schema.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/utils/schema.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAInD,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,qBAAqB,EACtB,MAAM,UAAU,CAAC;AAElB;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,YAAY,SAAS,WAAW,GAAG,WAAW,EAC1E,QAAQ,eAAe,CAAC,YAAY,CAAC,KACpC,eAAe,CAAC,YAAY,CAW9B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,YAAY,SAAS,WAAW,GAAG,WAAW,EAC5E,QAAQ,eAAe,CAAC,YAAY,CAAC,KACpC;IACD,aAAa,EAAE,eAAe,CAAC,YAAY,CAAC,CAAC;IAC7C,aAAa,EAAE,KAAK,CAAC,qBAAqB,CAAC,YAAY,CAAC,CAAC,CAAC;CAc3D,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,eAAe,KAAK,CAAC,qBAAqB,CAAC,YAAY,CAAC,CAAC,KACxD,GAAG,CAAC,MAAM,CAKV,CAAC;AAEJ,eAAO,MAAM,eAAe,GAAI,YAAY,SAAS,WAAW,GAAG,WAAW,EAC5E,OAAO,cAAc,CAAC,YAAY,CAAC,KAClC,MAEF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,OAAO,cAAc,CAAC,YAAY,CAAC,KAClC,MAEF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,QAAQ,eAAe,CAAC,YAAY,CAAC,EACrC,MAAM,MAAM,EAAE,KACb,MAAM,EAGR,CAAC;AAkBF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GACjC,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,OAAO,cAAc,CAAC,YAAY,CAAC,EACnC,aAAa,YAAY,EACzB,iBAAiB,OAAO,KACvB,OAAO,GAAG,SAC8D,CAAC;AAE5E,eAAO,MAAM,wBAAwB,GACnC,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,QAAQ,eAAe,CAAC,YAAY,CAAC,KACpC,MAAM,EAUR,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,QAAQ,eAAe,CAAC,YAAY,CAAC,KACpC,OAAO,CAAC,YAAY,CAQtB,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,YAAY,SAAS,WAAW,EACnE,OAAO,cAAc,CAAC,YAAY,CAAC,KAClC,OAA4C,CAAC;AAEhD,eAAO,MAAM,uBAAuB,GAClC,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,QAAQ,eAAe,CAAC,YAAY,CAAC,EACrC,SAAS,OAAO,CAAC,YAAY,CAAC,KAC7B,OAAO,CAAC,YAAY,CA0CtB,CAAC"}
|
package/dist/esm/akifilter.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import './styles.css';
|
|
2
2
|
import { FieldValues, Path } from '@akinon/akiform';
|
|
3
|
-
import { type PanelProps } from '@akinon/ui-collapse';
|
|
4
3
|
import React from 'react';
|
|
5
4
|
import { type AppliedFilter } from './components/applied-filters';
|
|
6
5
|
import type { AkifilterActionsRef, AkifilterButton, AkifilterSchema } from './types';
|
|
@@ -73,15 +72,6 @@ export type AkifilterProps<TFieldValues extends AkifilterFieldValues = Akifilter
|
|
|
73
72
|
*/
|
|
74
73
|
onFilterButtonsChange?: (activeButtons: AkifilterButton[]) => void;
|
|
75
74
|
};
|
|
76
|
-
export declare const ExpandIcon: ({ isActive }: PanelProps) => {
|
|
77
|
-
name: "chevron_down";
|
|
78
|
-
size: number;
|
|
79
|
-
style: {
|
|
80
|
-
transform: string;
|
|
81
|
-
transition: string;
|
|
82
|
-
marginTop: number;
|
|
83
|
-
};
|
|
84
|
-
};
|
|
85
75
|
export declare const Akifilter: {
|
|
86
76
|
<TFieldValues extends AkifilterFieldValues = FieldValues>(props: AkifilterProps<TFieldValues>): React.JSX.Element;
|
|
87
77
|
displayName: string;
|
|
@@ -1 +1 @@
|
|
|
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;
|
|
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;AAYzB,OAAO,KAAK,MAAM,OAAO,CAAC;AAc1B,OAAO,EACL,KAAK,aAAa,EAEnB,MAAM,8BAA8B,CAAC;AAgBtC,OAAO,KAAK,EACV,mBAAmB,EACnB,eAAe,EAEf,eAAe,EAChB,MAAM,SAAS,CAAC;AAWjB,KAAK,oBAAoB,GAAG,WAAW,CAAC;AA2KxC,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;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IACnC;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IACnC;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;OAEG;IACH,gBAAgB,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAClD;;;OAGG;IACH,sBAAsB,CAAC,EAAE,aAAa,EAAE,CAAC;IACzC;;OAEG;IACH,sBAAsB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C;;;;;OAKG;IACH,aAAa,CAAC,EAAE,eAAe,EAAE,CAAC;IAClC;;OAEG;IACH,qBAAqB,CAAC,EAAE,CAAC,aAAa,EAAE,eAAe,EAAE,KAAK,IAAI,CAAC;CACpE,CAAC;AA+3BF,eAAO,MAAM,SAAS;KACpB,YAAY,SAAS,oBAAoB,uBAElC,cAAc,CAAC,YAAY,CAAC;;CAoBpC,CAAC"}
|
package/dist/esm/akifilter.js
CHANGED
|
@@ -15,7 +15,6 @@ 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';
|
|
18
|
-
import { Collapse } from '@akinon/ui-collapse';
|
|
19
18
|
import { DatePicker } from '@akinon/ui-date-picker';
|
|
20
19
|
import { Input, InputTextArea } from '@akinon/ui-input';
|
|
21
20
|
import { InputNumber } from '@akinon/ui-input-number';
|
|
@@ -25,18 +24,20 @@ import { Upload } from '@akinon/ui-upload';
|
|
|
25
24
|
import { ConfigProvider } from 'antd';
|
|
26
25
|
import React from 'react';
|
|
27
26
|
import { ErrorBoundary } from 'react-error-boundary';
|
|
28
|
-
import { buildStorageKey, clearStoredButtonValues, clearStoredValues, readStoredButtonValues, readStoredValues,
|
|
27
|
+
import { buildStorageKey, clearStoredButtonValues, clearStoredValues, readStoredButtonValues, readStoredValues, writeStoredButtonValues, writeStoredValues } from './common/storage';
|
|
29
28
|
import { themeOverrides } from './common/theme-overrides';
|
|
30
29
|
import { AppliedFilters } from './components/applied-filters';
|
|
31
30
|
import { FilterButtons } from './components/filter-buttons';
|
|
32
31
|
import { FilterToolbar } from './components/filter-toolbar';
|
|
32
|
+
import { SectionFieldset } from './components/section-fieldset';
|
|
33
33
|
import { VisibilityModal } from './components/visibility-modal';
|
|
34
|
-
import { BOOLEAN_STRING,
|
|
34
|
+
import { BOOLEAN_STRING, FIELD_MODES, FIELD_TYPES, FILTER_DEBOUNCE_DELAY } from './constants';
|
|
35
35
|
import { useDebouncedValue } from './hooks/use-debounced-value';
|
|
36
36
|
import { useDynamicVisibility } from './hooks/use-dynamic-visibility';
|
|
37
|
+
import { useFieldVisibility } from './hooks/use-field-visibility';
|
|
37
38
|
import { useVisibilityCleanup } from './hooks/use-visibility-cleanup';
|
|
38
39
|
import { i18n } from './i18n';
|
|
39
|
-
import {
|
|
40
|
+
import { deriveExcludeSectionKeys, extractDefaultValues, flattenSchema, getFieldAriaLabel, normaliseValuesBySchema, partitionSchema } from './utils/schema';
|
|
40
41
|
import { normaliseOutputValues } from './utils/values';
|
|
41
42
|
/**
|
|
42
43
|
* Prefix for applied-filter chip keys that originate from filter buttons.
|
|
@@ -149,10 +150,6 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
|
|
|
149
150
|
defaultValues: mergedDefaultValues
|
|
150
151
|
});
|
|
151
152
|
const formValues = useWatch({ control: formMethods.control });
|
|
152
|
-
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
|
153
|
-
const [searchTerm, setSearchTerm] = React.useState('');
|
|
154
|
-
const [modalPage, setModalPage] = React.useState(1);
|
|
155
|
-
const [modalPageSize, setModalPageSize] = React.useState(DEFAULT_MODAL_PAGE_SIZE);
|
|
156
153
|
// --- Filter buttons (quick-filter toggles) ---------------------------------
|
|
157
154
|
// A button is identified by its `value`, which must be unique across the
|
|
158
155
|
// whole `filterButtons` array.
|
|
@@ -280,41 +277,14 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
|
|
|
280
277
|
value: button.label
|
|
281
278
|
}));
|
|
282
279
|
}, [filterButtons, activeButtonValues]);
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const explicitVisibility = resolveFieldVisibility(field);
|
|
292
|
-
if (explicitVisibility !== undefined) {
|
|
293
|
-
if (explicitVisibility)
|
|
294
|
-
acc.push(key);
|
|
295
|
-
return acc;
|
|
296
|
-
}
|
|
297
|
-
// No explicit boolean visibility, use stored/default
|
|
298
|
-
if (baseKeys.includes(key)) {
|
|
299
|
-
acc.push(key);
|
|
300
|
-
}
|
|
301
|
-
return acc;
|
|
302
|
-
}, []);
|
|
303
|
-
}, [regularFields, storageKey]);
|
|
304
|
-
const [visibleKeys, setVisibleKeys] = React.useState(resolveInitialVisibleKeys);
|
|
305
|
-
React.useEffect(() => {
|
|
306
|
-
setVisibleKeys(previous => {
|
|
307
|
-
const ordered = ensureSchemaOrder(regularFields, previous);
|
|
308
|
-
if (ordered.length) {
|
|
309
|
-
return ordered;
|
|
310
|
-
}
|
|
311
|
-
return resolveInitialVisibleKeys();
|
|
312
|
-
});
|
|
313
|
-
}, [regularFields, resolveInitialVisibleKeys]);
|
|
314
|
-
React.useEffect(() => {
|
|
315
|
-
writeVisibleKeys(storageKey, visibleKeys);
|
|
316
|
-
onVisibleFieldsChange === null || onVisibleFieldsChange === void 0 ? void 0 : onVisibleFieldsChange(visibleKeys);
|
|
317
|
-
}, [visibleKeys, onVisibleFieldsChange, storageKey]);
|
|
280
|
+
const handleVisibleKeysChange = React.useCallback((keys) => {
|
|
281
|
+
onVisibleFieldsChange === null || onVisibleFieldsChange === void 0 ? void 0 : onVisibleFieldsChange(keys);
|
|
282
|
+
}, [onVisibleFieldsChange]);
|
|
283
|
+
const { visibleKeys, setVisibleKeys, visibleFields, openModal, modalProps } = useFieldVisibility({
|
|
284
|
+
fields: regularFields,
|
|
285
|
+
storageKey,
|
|
286
|
+
onVisibleKeysChange: handleVisibleKeysChange
|
|
287
|
+
});
|
|
318
288
|
const normalisedValues = React.useMemo(() => normaliseOutputValues(flattenedSchema, formValues), [flattenedSchema, formValues]);
|
|
319
289
|
const hasInitialValuesRef = React.useRef(false);
|
|
320
290
|
React.useEffect(() => {
|
|
@@ -445,26 +415,6 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
|
|
|
445
415
|
emitValues,
|
|
446
416
|
persistValues
|
|
447
417
|
]);
|
|
448
|
-
const handleOpenModal = React.useCallback(() => {
|
|
449
|
-
setIsModalOpen(true);
|
|
450
|
-
setSearchTerm('');
|
|
451
|
-
setModalPage(1);
|
|
452
|
-
}, []);
|
|
453
|
-
const handleCloseModal = React.useCallback(() => {
|
|
454
|
-
setIsModalOpen(false);
|
|
455
|
-
}, []);
|
|
456
|
-
const handleToggleVisibility = React.useCallback((key) => {
|
|
457
|
-
setVisibleKeys(prev => {
|
|
458
|
-
const exists = prev.includes(key);
|
|
459
|
-
if (exists && prev.length === 1) {
|
|
460
|
-
return prev;
|
|
461
|
-
}
|
|
462
|
-
const nextKeys = exists
|
|
463
|
-
? prev.filter(item => item !== key)
|
|
464
|
-
: [...prev, key];
|
|
465
|
-
return ensureSchemaOrder(regularFields, nextKeys);
|
|
466
|
-
});
|
|
467
|
-
}, [regularFields]);
|
|
468
418
|
const handleRemoveFilter = React.useCallback((key) => {
|
|
469
419
|
const schemaField = flattenedSchema.find(field => String(field.key) === key);
|
|
470
420
|
if (!schemaField) {
|
|
@@ -545,23 +495,6 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
|
|
|
545
495
|
keysArray.forEach(key => handleRemoveFilter(key));
|
|
546
496
|
}
|
|
547
497
|
}), [handleRemoveFilter]);
|
|
548
|
-
const filteredFields = React.useMemo(() => {
|
|
549
|
-
const normalisedTerm = searchTerm.trim().toLowerCase();
|
|
550
|
-
if (!normalisedTerm) {
|
|
551
|
-
return regularFields;
|
|
552
|
-
}
|
|
553
|
-
return regularFields.filter(field => {
|
|
554
|
-
const searchable = [field.label, field.placeholder, String(field.key)]
|
|
555
|
-
.filter(Boolean)
|
|
556
|
-
.map(value => String(value).toLowerCase());
|
|
557
|
-
return searchable.some(text => text.includes(normalisedTerm));
|
|
558
|
-
});
|
|
559
|
-
}, [regularFields, searchTerm]);
|
|
560
|
-
const paginatedFields = React.useMemo(() => {
|
|
561
|
-
const start = (modalPage - 1) * modalPageSize;
|
|
562
|
-
return filteredFields.slice(start, start + modalPageSize);
|
|
563
|
-
}, [filteredFields, modalPage, modalPageSize]);
|
|
564
|
-
const visibleFields = React.useMemo(() => regularFields.filter(field => visibleKeys.includes(String(field.key))), [regularFields, visibleKeys]);
|
|
565
498
|
// Dynamic visibility
|
|
566
499
|
useDynamicVisibility({
|
|
567
500
|
regularFields,
|
|
@@ -652,7 +585,7 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
|
|
|
652
585
|
};
|
|
653
586
|
return (React.createElement(Card, { size: "small", className: "akinon-filter shadow", "data-testid": "akifilter-root" },
|
|
654
587
|
React.createElement(ConfigProvider, { theme: themeOverrides },
|
|
655
|
-
React.createElement(FilterToolbar, { onOpenModal:
|
|
588
|
+
React.createElement(FilterToolbar, { onOpenModal: openModal, enableImportCsv: enableImportCsv, enableImportXls: enableImportXls, onImportCsv: onImportCsv, onImportXls: onImportXls }),
|
|
656
589
|
React.createElement(AppliedFilters, { filters: [
|
|
657
590
|
...appliedFilters,
|
|
658
591
|
...buttonAppliedFilters,
|
|
@@ -663,35 +596,9 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
|
|
|
663
596
|
React.createElement("div", { className: "akinon-filter__form-grid", "data-testid": "akifilter-form-grid" },
|
|
664
597
|
visibleFields.map(renderFormField),
|
|
665
598
|
visibleFields.length === 0 && regularFields.length > 0 ? (React.createElement("div", { className: "akinon-filter__empty" }, i18n.t('form.noVisibleFields'))) : null),
|
|
666
|
-
sectionFields.length > 0 && (React.createElement("div", { className: "akinon-filter__section-fields" }, sectionFields.map(section => {
|
|
667
|
-
|
|
668
|
-
{
|
|
669
|
-
key: section.key,
|
|
670
|
-
label: (React.createElement(Title, { className: "akinon-filter__title", level: 4 }, section.label)),
|
|
671
|
-
headerClass: 'akinon-filter__section-header',
|
|
672
|
-
children: (React.createElement("div", { className: "akinon-filter__form-grid" }, section.fields.map(renderFormField)))
|
|
673
|
-
}
|
|
674
|
-
] }));
|
|
675
|
-
}))))),
|
|
676
|
-
React.createElement(VisibilityModal, { open: isModalOpen, onClose: handleCloseModal, searchTerm: searchTerm, paginatedFields: paginatedFields, filteredCount: filteredFields.length, visibleKeys: visibleKeys, onSearchTermChange: value => {
|
|
677
|
-
setSearchTerm(value);
|
|
678
|
-
setModalPage(1);
|
|
679
|
-
}, onToggleVisibility: handleToggleVisibility, onPaginationChange: (page, pageSize) => {
|
|
680
|
-
setModalPage(page);
|
|
681
|
-
if (pageSize !== modalPageSize) {
|
|
682
|
-
setModalPageSize(pageSize);
|
|
683
|
-
}
|
|
684
|
-
}, page: modalPage, pageSize: modalPageSize }))));
|
|
599
|
+
sectionFields.length > 0 && (React.createElement("div", { className: "akinon-filter__section-fields" }, sectionFields.map(section => (React.createElement(SectionFieldset, { key: String(section.key), section: section, storageKey: storageKey, renderFormField: renderFormField, onRemoveField: handleRemoveFilter, formValues: normalisedValues }))))))),
|
|
600
|
+
React.createElement(VisibilityModal, Object.assign({}, modalProps)))));
|
|
685
601
|
};
|
|
686
|
-
export const ExpandIcon = ({ isActive }) => ({
|
|
687
|
-
name: 'chevron_down',
|
|
688
|
-
size: 10,
|
|
689
|
-
style: {
|
|
690
|
-
transform: isActive ? 'rotate(-180deg)' : 'rotate(0deg)',
|
|
691
|
-
transition: 'transform 0.2s ease-in-out',
|
|
692
|
-
marginTop: 8
|
|
693
|
-
}
|
|
694
|
-
});
|
|
695
602
|
const AkifilterEmptyState = () => {
|
|
696
603
|
const { t } = i18n;
|
|
697
604
|
return (React.createElement(Card, { size: "small", className: "akinon-filter shadow", "data-testid": "akifilter-empty" },
|
|
@@ -3,6 +3,7 @@ import type { AkifilterField } from '../types';
|
|
|
3
3
|
export declare const buildStorageKey: <TFieldValues extends FieldValues>(schema: AkifilterField<TFieldValues>[], namespace?: string) => string;
|
|
4
4
|
export declare const readVisibleKeys: <TFieldValues extends FieldValues>(schema: AkifilterField<TFieldValues>[], storageKey: string) => string[] | null;
|
|
5
5
|
export declare const writeVisibleKeys: (storageKey: string, keys: string[]) => void;
|
|
6
|
+
export declare const buildSectionVisibilityKey: (storageKey: string, sectionKey: string | number) => string;
|
|
6
7
|
export declare const clearVisibleKeys: (storageKey: string) => void;
|
|
7
8
|
export declare const readStoredValues: <TFieldValues extends FieldValues>(schema: AkifilterField<TFieldValues>[], storageKey: string) => Partial<TFieldValues> | null;
|
|
8
9
|
export declare const writeStoredValues: <TFieldValues extends FieldValues>(storageKey: string, values: Partial<TFieldValues>) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../../src/common/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AA4B/C,eAAO,MAAM,eAAe,GAAI,YAAY,SAAS,WAAW,EAC9D,QAAQ,cAAc,CAAC,YAAY,CAAC,EAAE,EACtC,YAAY,MAAM,KACjB,MAKF,CAAC;AAIF,eAAO,MAAM,eAAe,GAAI,YAAY,SAAS,WAAW,EAC9D,QAAQ,cAAc,CAAC,YAAY,CAAC,EAAE,EACtC,YAAY,MAAM,KACjB,MAAM,EAAE,GAAG,IAoBb,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,YAAY,MAAM,EAAE,MAAM,MAAM,EAAE,KAAG,IAQrE,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,YAAY,MAAM,KAAG,IAQrD,CAAC;AAKF,eAAO,MAAM,gBAAgB,GAAI,YAAY,SAAS,WAAW,EAC/D,QAAQ,cAAc,CAAC,YAAY,CAAC,EAAE,EACtC,YAAY,MAAM,KACjB,OAAO,CAAC,YAAY,CAAC,GAAG,IA+B1B,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,YAAY,SAAS,WAAW,EAChE,YAAY,MAAM,EAClB,QAAQ,OAAO,CAAC,YAAY,CAAC,KAC5B,IAeF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,YAAY,MAAM,KAAG,IAQtD,CAAC;AAKF,eAAO,MAAM,sBAAsB,GACjC,YAAY,MAAM,KACjB,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,IAwB3B,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAClC,YAAY,MAAM,EAClB,QAAQ,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,KAC7B,IAcF,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,YAAY,MAAM,KAAG,IAQ5D,CAAC"}
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../../src/common/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AA4B/C,eAAO,MAAM,eAAe,GAAI,YAAY,SAAS,WAAW,EAC9D,QAAQ,cAAc,CAAC,YAAY,CAAC,EAAE,EACtC,YAAY,MAAM,KACjB,MAKF,CAAC;AAIF,eAAO,MAAM,eAAe,GAAI,YAAY,SAAS,WAAW,EAC9D,QAAQ,cAAc,CAAC,YAAY,CAAC,EAAE,EACtC,YAAY,MAAM,KACjB,MAAM,EAAE,GAAG,IAoBb,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,YAAY,MAAM,EAAE,MAAM,MAAM,EAAE,KAAG,IAQrE,CAAC;AAEF,eAAO,MAAM,yBAAyB,GACpC,YAAY,MAAM,EAClB,YAAY,MAAM,GAAG,MAAM,KAC1B,MAAuD,CAAC;AAE3D,eAAO,MAAM,gBAAgB,GAAI,YAAY,MAAM,KAAG,IAQrD,CAAC;AAKF,eAAO,MAAM,gBAAgB,GAAI,YAAY,SAAS,WAAW,EAC/D,QAAQ,cAAc,CAAC,YAAY,CAAC,EAAE,EACtC,YAAY,MAAM,KACjB,OAAO,CAAC,YAAY,CAAC,GAAG,IA+B1B,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,YAAY,SAAS,WAAW,EAChE,YAAY,MAAM,EAClB,QAAQ,OAAO,CAAC,YAAY,CAAC,KAC5B,IAeF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,YAAY,MAAM,KAAG,IAQtD,CAAC;AAKF,eAAO,MAAM,sBAAsB,GACjC,YAAY,MAAM,KACjB,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,IAwB3B,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAClC,YAAY,MAAM,EAClB,QAAQ,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,KAC7B,IAcF,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,YAAY,MAAM,KAAG,IAQ5D,CAAC"}
|
|
@@ -56,6 +56,7 @@ export const writeVisibleKeys = (storageKey, keys) => {
|
|
|
56
56
|
console.warn(i18n.t('errors.storage.writeVisibility'), error);
|
|
57
57
|
}
|
|
58
58
|
};
|
|
59
|
+
export const buildSectionVisibilityKey = (storageKey, sectionKey) => `${storageKey}:section:${String(sectionKey)}`;
|
|
59
60
|
export const clearVisibleKeys = (storageKey) => {
|
|
60
61
|
if (!isBrowser)
|
|
61
62
|
return;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { FieldValues } from '@akinon/akiform';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import type { AkifilterField, AkifilterSectionField } from '../types';
|
|
4
|
+
type SectionFieldsetProps<TFieldValues extends FieldValues = FieldValues> = {
|
|
5
|
+
section: AkifilterSectionField<TFieldValues>;
|
|
6
|
+
/**
|
|
7
|
+
* Base storage key of the host Akifilter; combined with the section key to
|
|
8
|
+
* persist this section's visible-field selection.
|
|
9
|
+
*/
|
|
10
|
+
storageKey: string;
|
|
11
|
+
renderFormField: (field: AkifilterField<TFieldValues>) => React.ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* Clears a field's form value; used to drop the value of a field that gets
|
|
14
|
+
* hidden from the section.
|
|
15
|
+
*/
|
|
16
|
+
onRemoveField: (key: string) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Current normalised form values, needed to resolve dynamic visibility.
|
|
19
|
+
*/
|
|
20
|
+
formValues: Partial<TFieldValues> | undefined;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Renders a single section field. Sections flagged with `selectableFields`
|
|
24
|
+
* expose a funnel icon + visibility modal; all others render every field
|
|
25
|
+
* inline as before.
|
|
26
|
+
*/
|
|
27
|
+
export declare const SectionFieldset: <TFieldValues extends FieldValues = FieldValues>(props: SectionFieldsetProps<TFieldValues>) => React.JSX.Element;
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=section-fieldset.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"section-fieldset.d.ts","sourceRoot":"","sources":["../../../src/components/section-fieldset.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAKnD,OAAO,KAAK,MAAM,OAAO,CAAC;AAO1B,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AA8CtE,KAAK,oBAAoB,CAAC,YAAY,SAAS,WAAW,GAAG,WAAW,IAAI;IAC1E,OAAO,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;IAC7C;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,YAAY,CAAC,KAAK,KAAK,CAAC,SAAS,CAAC;IAC1E;;;OAGG;IACH,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC;;OAEG;IACH,UAAU,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,SAAS,CAAC;CAC/C,CAAC;AAwGF;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,YAAY,SAAS,WAAW,GAAG,WAAW,EAC5E,OAAO,oBAAoB,CAAC,YAAY,CAAC,sBAuB1C,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Icon } from '@akinon/icons';
|
|
2
|
+
import { Button } from '@akinon/ui-button';
|
|
3
|
+
import { Collapse } from '@akinon/ui-collapse';
|
|
4
|
+
import { Title } from '@akinon/ui-typography';
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import { buildSectionVisibilityKey } from '../common/storage';
|
|
7
|
+
import { useDynamicVisibility } from '../hooks/use-dynamic-visibility';
|
|
8
|
+
import { useFieldVisibility } from '../hooks/use-field-visibility';
|
|
9
|
+
import { useVisibilityCleanup } from '../hooks/use-visibility-cleanup';
|
|
10
|
+
import { i18n } from '../i18n';
|
|
11
|
+
import { VisibilityModal } from './visibility-modal';
|
|
12
|
+
const ExpandIcon = ({ isActive }) => ({
|
|
13
|
+
name: 'chevron_down',
|
|
14
|
+
size: 10,
|
|
15
|
+
style: {
|
|
16
|
+
transform: isActive ? 'rotate(-180deg)' : 'rotate(0deg)',
|
|
17
|
+
transition: 'transform 0.2s ease-in-out',
|
|
18
|
+
marginTop: 8
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* Presentational wrapper around the collapsible panel used by every section,
|
|
23
|
+
* so the selectable and plain variants share one markup.
|
|
24
|
+
*/
|
|
25
|
+
const SectionCollapse = ({ sectionKey, label, defaultExpanded, children }) => (React.createElement(Collapse, { expandIconPosition: "end", expandIcon: ExpandIcon, defaultActiveKey: defaultExpanded === false ? [] : [sectionKey], ghost: true, items: [
|
|
26
|
+
{
|
|
27
|
+
key: sectionKey,
|
|
28
|
+
label,
|
|
29
|
+
headerClass: 'akinon-filter__section-header',
|
|
30
|
+
children: React.createElement("div", { className: "akinon-filter__form-grid" }, children)
|
|
31
|
+
}
|
|
32
|
+
] }));
|
|
33
|
+
/**
|
|
34
|
+
* A section whose header exposes a funnel icon that opens the visibility modal,
|
|
35
|
+
* mirroring the top-level filter field-selection behaviour.
|
|
36
|
+
*/
|
|
37
|
+
const SelectableSectionFieldset = ({ section, storageKey, renderFormField, onRemoveField, formValues }) => {
|
|
38
|
+
const sectionStorageKey = React.useMemo(() => buildSectionVisibilityKey(storageKey, section.key), [storageKey, section.key]);
|
|
39
|
+
const { visibleKeys, setVisibleKeys, visibleFields, openModal, modalProps } = useFieldVisibility({
|
|
40
|
+
fields: section.fields,
|
|
41
|
+
storageKey: sectionStorageKey
|
|
42
|
+
});
|
|
43
|
+
useDynamicVisibility({
|
|
44
|
+
regularFields: section.fields,
|
|
45
|
+
formValues,
|
|
46
|
+
setVisibleKeys
|
|
47
|
+
});
|
|
48
|
+
useVisibilityCleanup({
|
|
49
|
+
visibleKeys,
|
|
50
|
+
regularFields: section.fields,
|
|
51
|
+
onRemove: onRemoveField
|
|
52
|
+
});
|
|
53
|
+
// Controlled so we can render our own chevron in the header and keep the
|
|
54
|
+
// funnel button pinned to the far right (after the arrow).
|
|
55
|
+
const sectionKey = String(section.key);
|
|
56
|
+
const [activeKeys, setActiveKeys] = React.useState(section.defaultExpanded === false ? [] : [sectionKey]);
|
|
57
|
+
const isExpanded = activeKeys.includes(sectionKey);
|
|
58
|
+
const label = (React.createElement("div", { className: "akinon-filter__section-header-content" },
|
|
59
|
+
React.createElement(Title, { className: "akinon-filter__title", level: 4 }, section.label),
|
|
60
|
+
React.createElement(Icon, { icon: "chevron_down", size: 10, className: "akinon-filter__section-arrow", style: {
|
|
61
|
+
transform: isExpanded ? 'rotate(-180deg)' : 'rotate(0deg)',
|
|
62
|
+
transition: 'transform 0.2s ease-in-out'
|
|
63
|
+
} }),
|
|
64
|
+
React.createElement(Button, { className: "akinon-filter__toolbar-item akinon-filter__section-funnel", icon: "filter", iconSize: 16, type: "primary", title: i18n.t('header.actions.selectFilters'), "aria-label": i18n.t('header.actions.selectFilters'), onClick: event => {
|
|
65
|
+
// Prevent the header click from toggling the collapse.
|
|
66
|
+
event.stopPropagation();
|
|
67
|
+
openModal();
|
|
68
|
+
} })));
|
|
69
|
+
return (React.createElement(React.Fragment, null,
|
|
70
|
+
React.createElement(Collapse, { activeKey: activeKeys, onChange: keys => setActiveKeys((Array.isArray(keys) ? keys : [keys]).map(String)), ghost: true, items: [
|
|
71
|
+
{
|
|
72
|
+
key: sectionKey,
|
|
73
|
+
label,
|
|
74
|
+
headerClass: 'akinon-filter__section-header akinon-filter__section-header--selectable',
|
|
75
|
+
showArrow: false,
|
|
76
|
+
children: (React.createElement("div", { className: "akinon-filter__form-grid" }, visibleFields.map(renderFormField)))
|
|
77
|
+
}
|
|
78
|
+
] }),
|
|
79
|
+
React.createElement(VisibilityModal, Object.assign({}, modalProps))));
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Renders a single section field. Sections flagged with `selectableFields`
|
|
83
|
+
* expose a funnel icon + visibility modal; all others render every field
|
|
84
|
+
* inline as before.
|
|
85
|
+
*/
|
|
86
|
+
export const SectionFieldset = (props) => {
|
|
87
|
+
const { section, renderFormField } = props;
|
|
88
|
+
if (section.selectableFields) {
|
|
89
|
+
return React.createElement(SelectableSectionFieldset, Object.assign({}, props));
|
|
90
|
+
}
|
|
91
|
+
const label = (React.createElement(Title, { className: "akinon-filter__title", level: 4 }, section.label));
|
|
92
|
+
return (React.createElement(SectionCollapse, { sectionKey: String(section.key), label: label, defaultExpanded: section.defaultExpanded }, section.fields.map(renderFormField)));
|
|
93
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type FieldValues } from '@akinon/akiform';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import type { AkifilterSchema } from '../types';
|
|
4
|
+
type UseFieldVisibilityParams<TFieldValues extends FieldValues> = {
|
|
5
|
+
/**
|
|
6
|
+
* Fields whose visibility this hook manages (top-level filters or the fields
|
|
7
|
+
* of a single section).
|
|
8
|
+
*/
|
|
9
|
+
fields: AkifilterSchema<TFieldValues>;
|
|
10
|
+
/**
|
|
11
|
+
* Storage key under which the visible-field selection is persisted. Callers
|
|
12
|
+
* pass distinct keys per group so selections never collide.
|
|
13
|
+
*/
|
|
14
|
+
storageKey: string;
|
|
15
|
+
/**
|
|
16
|
+
* Notified whenever the resolved visible keys change.
|
|
17
|
+
*/
|
|
18
|
+
onVisibleKeysChange?: (keys: string[]) => void;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Encapsulates the visible-field selection and its accompanying modal state for
|
|
22
|
+
* a single group of fields. Used both for the top-level filters and for each
|
|
23
|
+
* selectable section so the two share one implementation.
|
|
24
|
+
*/
|
|
25
|
+
export declare const useFieldVisibility: <TFieldValues extends FieldValues = FieldValues>({ fields, storageKey, onVisibleKeysChange }: UseFieldVisibilityParams<TFieldValues>) => {
|
|
26
|
+
visibleKeys: string[];
|
|
27
|
+
setVisibleKeys: React.Dispatch<React.SetStateAction<string[]>>;
|
|
28
|
+
visibleFields: import("../types").AkifilterField<TFieldValues>[];
|
|
29
|
+
openModal: () => void;
|
|
30
|
+
modalProps: {
|
|
31
|
+
open: boolean;
|
|
32
|
+
searchTerm: string;
|
|
33
|
+
paginatedFields: import("../types").AkifilterField<TFieldValues>[];
|
|
34
|
+
filteredCount: number;
|
|
35
|
+
visibleKeys: string[];
|
|
36
|
+
onClose: () => void;
|
|
37
|
+
onSearchTermChange: (value: string) => void;
|
|
38
|
+
onToggleVisibility: (key: string) => void;
|
|
39
|
+
onPaginationChange: (page: number, pageSize: number) => void;
|
|
40
|
+
page: number;
|
|
41
|
+
pageSize: number;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export {};
|
|
45
|
+
//# sourceMappingURL=use-field-visibility.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-field-visibility.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-field-visibility.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAOhD,KAAK,wBAAwB,CAAC,YAAY,SAAS,WAAW,IAAI;IAChE;;;OAGG;IACH,MAAM,EAAE,eAAe,CAAC,YAAY,CAAC,CAAC;IACtC;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,mBAAmB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CAChD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC7B,YAAY,SAAS,WAAW,GAAG,WAAW,EAC9C,6CAIC,wBAAwB,CAAC,YAAY,CAAC;;;;;;;;;;;;oCAiHP,MAAM;kCAlD9B,MAAM;mCAuDiB,MAAM,YAAY,MAAM;;;;CA6BxD,CAAC"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { readVisibleKeys, writeVisibleKeys } from '../common/storage';
|
|
3
|
+
import { DEFAULT_MODAL_PAGE_SIZE } from '../constants';
|
|
4
|
+
import { deriveDefaultVisibleKeys, ensureSchemaOrder, resolveFieldVisibility } from '../utils/schema';
|
|
5
|
+
/**
|
|
6
|
+
* Encapsulates the visible-field selection and its accompanying modal state for
|
|
7
|
+
* a single group of fields. Used both for the top-level filters and for each
|
|
8
|
+
* selectable section so the two share one implementation.
|
|
9
|
+
*/
|
|
10
|
+
export const useFieldVisibility = ({ fields, storageKey, onVisibleKeysChange }) => {
|
|
11
|
+
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
|
12
|
+
const [searchTerm, setSearchTerm] = React.useState('');
|
|
13
|
+
const [modalPage, setModalPage] = React.useState(1);
|
|
14
|
+
const [modalPageSize, setModalPageSize] = React.useState(DEFAULT_MODAL_PAGE_SIZE);
|
|
15
|
+
const resolveInitialVisibleKeys = React.useCallback(() => {
|
|
16
|
+
const storedKeys = readVisibleKeys(fields, storageKey);
|
|
17
|
+
const defaultKeys = deriveDefaultVisibleKeys(fields);
|
|
18
|
+
const baseKeys = storedKeys && storedKeys.length > 0
|
|
19
|
+
? ensureSchemaOrder(fields, storedKeys)
|
|
20
|
+
: defaultKeys;
|
|
21
|
+
return fields.reduce((acc, field) => {
|
|
22
|
+
const key = String(field.key);
|
|
23
|
+
const explicitVisibility = resolveFieldVisibility(field);
|
|
24
|
+
if (explicitVisibility !== undefined) {
|
|
25
|
+
if (explicitVisibility)
|
|
26
|
+
acc.push(key);
|
|
27
|
+
return acc;
|
|
28
|
+
}
|
|
29
|
+
// No explicit boolean visibility, use stored/default
|
|
30
|
+
if (baseKeys.includes(key)) {
|
|
31
|
+
acc.push(key);
|
|
32
|
+
}
|
|
33
|
+
return acc;
|
|
34
|
+
}, []);
|
|
35
|
+
}, [fields, storageKey]);
|
|
36
|
+
const [visibleKeys, setVisibleKeys] = React.useState(resolveInitialVisibleKeys);
|
|
37
|
+
React.useEffect(() => {
|
|
38
|
+
setVisibleKeys(previous => {
|
|
39
|
+
const ordered = ensureSchemaOrder(fields, previous);
|
|
40
|
+
if (ordered.length) {
|
|
41
|
+
return ordered;
|
|
42
|
+
}
|
|
43
|
+
return resolveInitialVisibleKeys();
|
|
44
|
+
});
|
|
45
|
+
}, [fields, resolveInitialVisibleKeys]);
|
|
46
|
+
React.useEffect(() => {
|
|
47
|
+
writeVisibleKeys(storageKey, visibleKeys);
|
|
48
|
+
onVisibleKeysChange === null || onVisibleKeysChange === void 0 ? void 0 : onVisibleKeysChange(visibleKeys);
|
|
49
|
+
}, [visibleKeys, onVisibleKeysChange, storageKey]);
|
|
50
|
+
const openModal = React.useCallback(() => {
|
|
51
|
+
setIsModalOpen(true);
|
|
52
|
+
setSearchTerm('');
|
|
53
|
+
setModalPage(1);
|
|
54
|
+
}, []);
|
|
55
|
+
const closeModal = React.useCallback(() => {
|
|
56
|
+
setIsModalOpen(false);
|
|
57
|
+
}, []);
|
|
58
|
+
const handleToggleVisibility = React.useCallback((key) => {
|
|
59
|
+
setVisibleKeys(prev => {
|
|
60
|
+
const exists = prev.includes(key);
|
|
61
|
+
if (exists && prev.length === 1) {
|
|
62
|
+
return prev;
|
|
63
|
+
}
|
|
64
|
+
const nextKeys = exists
|
|
65
|
+
? prev.filter(item => item !== key)
|
|
66
|
+
: [...prev, key];
|
|
67
|
+
return ensureSchemaOrder(fields, nextKeys);
|
|
68
|
+
});
|
|
69
|
+
}, [fields]);
|
|
70
|
+
const filteredFields = React.useMemo(() => {
|
|
71
|
+
const normalisedTerm = searchTerm.trim().toLowerCase();
|
|
72
|
+
if (!normalisedTerm) {
|
|
73
|
+
return fields;
|
|
74
|
+
}
|
|
75
|
+
return fields.filter(field => {
|
|
76
|
+
const searchable = [field.label, field.placeholder, String(field.key)]
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
.map(value => String(value).toLowerCase());
|
|
79
|
+
return searchable.some(text => text.includes(normalisedTerm));
|
|
80
|
+
});
|
|
81
|
+
}, [fields, searchTerm]);
|
|
82
|
+
const paginatedFields = React.useMemo(() => {
|
|
83
|
+
const start = (modalPage - 1) * modalPageSize;
|
|
84
|
+
return filteredFields.slice(start, start + modalPageSize);
|
|
85
|
+
}, [filteredFields, modalPage, modalPageSize]);
|
|
86
|
+
const visibleFields = React.useMemo(() => fields.filter(field => visibleKeys.includes(String(field.key))), [fields, visibleKeys]);
|
|
87
|
+
const modalProps = React.useMemo(() => ({
|
|
88
|
+
open: isModalOpen,
|
|
89
|
+
searchTerm,
|
|
90
|
+
paginatedFields,
|
|
91
|
+
filteredCount: filteredFields.length,
|
|
92
|
+
visibleKeys,
|
|
93
|
+
onClose: closeModal,
|
|
94
|
+
onSearchTermChange: (value) => {
|
|
95
|
+
setSearchTerm(value);
|
|
96
|
+
setModalPage(1);
|
|
97
|
+
},
|
|
98
|
+
onToggleVisibility: handleToggleVisibility,
|
|
99
|
+
onPaginationChange: (page, pageSize) => {
|
|
100
|
+
setModalPage(page);
|
|
101
|
+
if (pageSize !== modalPageSize) {
|
|
102
|
+
setModalPageSize(pageSize);
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
page: modalPage,
|
|
106
|
+
pageSize: modalPageSize
|
|
107
|
+
}), [
|
|
108
|
+
isModalOpen,
|
|
109
|
+
searchTerm,
|
|
110
|
+
paginatedFields,
|
|
111
|
+
filteredFields.length,
|
|
112
|
+
visibleKeys,
|
|
113
|
+
closeModal,
|
|
114
|
+
handleToggleVisibility,
|
|
115
|
+
modalPage,
|
|
116
|
+
modalPageSize
|
|
117
|
+
]);
|
|
118
|
+
return {
|
|
119
|
+
visibleKeys,
|
|
120
|
+
setVisibleKeys,
|
|
121
|
+
visibleFields,
|
|
122
|
+
openModal,
|
|
123
|
+
modalProps
|
|
124
|
+
};
|
|
125
|
+
};
|
package/dist/esm/styles.css
CHANGED
|
@@ -70,6 +70,29 @@
|
|
|
70
70
|
flex: unset !important;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
.akinon-filter__section-header-content {
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
gap: 0.5rem;
|
|
77
|
+
width: 100%;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Let the header text span fill the header so the funnel can sit at the far right. */
|
|
81
|
+
.akinon-filter__section-header--selectable .akinon-collapse-header-text {
|
|
82
|
+
flex: 1 1 auto !important;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.akinon-filter__section-arrow {
|
|
86
|
+
color: var(--color-gray-500);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* Funnel pinned to the far right of the header, after the title and arrow. */
|
|
90
|
+
.akinon-filter__section-funnel.akinon-filter__toolbar-item {
|
|
91
|
+
width: 36px;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
margin-left: auto;
|
|
94
|
+
}
|
|
95
|
+
|
|
73
96
|
.akinon-filter__buttons {
|
|
74
97
|
display: flex;
|
|
75
98
|
align-items: center;
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FieldValues, Path } from '@akinon/akiform';
|
|
2
|
-
import type { FormField } from '@akinon/akiform-builder';
|
|
2
|
+
import type { FormField, SectionField } from '@akinon/akiform-builder';
|
|
3
3
|
import type { IconName } from '@akinon/icons';
|
|
4
4
|
export interface AkifilterFieldConfig<TFieldValues extends FieldValues = FieldValues> {
|
|
5
5
|
/**
|
|
@@ -25,6 +25,13 @@ export type AkifilterField<TFieldValues extends FieldValues = FieldValues> = Dis
|
|
|
25
25
|
*/
|
|
26
26
|
fileAccept?: string;
|
|
27
27
|
};
|
|
28
|
+
/**
|
|
29
|
+
* Section field as consumed by Akifilter. Derived from the shared
|
|
30
|
+
* `SectionField` so flags like `selectableFields`/`isExcludeSection` stay
|
|
31
|
+
* single-sourced, but omits `collapsible` — Akifilter always renders sections
|
|
32
|
+
* as collapsible panels and does not honor that prop.
|
|
33
|
+
*/
|
|
34
|
+
export type AkifilterSectionField<TFieldValues extends FieldValues = FieldValues> = Omit<SectionField<TFieldValues>, 'collapsible'>;
|
|
28
35
|
export type AkifilterFieldKey<TFieldValues extends FieldValues = FieldValues> = Path<TFieldValues>;
|
|
29
36
|
export type AkifilterSchema<TFieldValues extends FieldValues = FieldValues> = AkifilterField<TFieldValues>[];
|
|
30
37
|
/**
|