@akinon/akifilter 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +306 -0
  2. package/dist/cjs/akifilter.d.ts +19 -0
  3. package/dist/cjs/akifilter.d.ts.map +1 -1
  4. package/dist/cjs/akifilter.js +74 -17
  5. package/dist/cjs/common/storage.d.ts.map +1 -1
  6. package/dist/cjs/common/storage.js +47 -2
  7. package/dist/cjs/components/file-import-button.d.ts +33 -0
  8. package/dist/cjs/components/file-import-button.d.ts.map +1 -0
  9. package/dist/cjs/components/file-import-button.js +37 -0
  10. package/dist/cjs/components/filter-toolbar.d.ts.map +1 -1
  11. package/dist/cjs/components/filter-toolbar.js +4 -18
  12. package/dist/cjs/components/section-fieldset.d.ts +16 -2
  13. package/dist/cjs/components/section-fieldset.d.ts.map +1 -1
  14. package/dist/cjs/components/section-fieldset.js +104 -32
  15. package/dist/cjs/components/visibility-modal.js +1 -1
  16. package/dist/cjs/constants.d.ts +6 -0
  17. package/dist/cjs/constants.d.ts.map +1 -1
  18. package/dist/cjs/constants.js +7 -1
  19. package/dist/cjs/hooks/use-field-visibility.d.ts.map +1 -1
  20. package/dist/cjs/hooks/use-field-visibility.js +10 -1
  21. package/dist/cjs/i18n/translations/en.d.ts +8 -0
  22. package/dist/cjs/i18n/translations/en.d.ts.map +1 -1
  23. package/dist/cjs/i18n/translations/en.js +8 -0
  24. package/dist/cjs/i18n/translations/tr.d.ts +8 -0
  25. package/dist/cjs/i18n/translations/tr.d.ts.map +1 -1
  26. package/dist/cjs/i18n/translations/tr.js +8 -0
  27. package/dist/cjs/styles.css +36 -11
  28. package/dist/cjs/types.d.ts +146 -3
  29. package/dist/cjs/types.d.ts.map +1 -1
  30. package/dist/cjs/utils/schema.d.ts +10 -0
  31. package/dist/cjs/utils/schema.d.ts.map +1 -1
  32. package/dist/cjs/utils/schema.js +13 -1
  33. package/dist/esm/akifilter.d.ts +19 -0
  34. package/dist/esm/akifilter.d.ts.map +1 -1
  35. package/dist/esm/akifilter.js +75 -18
  36. package/dist/esm/common/storage.d.ts.map +1 -1
  37. package/dist/esm/common/storage.js +47 -2
  38. package/dist/esm/components/file-import-button.d.ts +33 -0
  39. package/dist/esm/components/file-import-button.d.ts.map +1 -0
  40. package/dist/esm/components/file-import-button.js +33 -0
  41. package/dist/esm/components/filter-toolbar.d.ts.map +1 -1
  42. package/dist/esm/components/filter-toolbar.js +4 -18
  43. package/dist/esm/components/section-fieldset.d.ts +16 -2
  44. package/dist/esm/components/section-fieldset.d.ts.map +1 -1
  45. package/dist/esm/components/section-fieldset.js +104 -32
  46. package/dist/esm/components/visibility-modal.js +2 -2
  47. package/dist/esm/constants.d.ts +6 -0
  48. package/dist/esm/constants.d.ts.map +1 -1
  49. package/dist/esm/constants.js +6 -0
  50. package/dist/esm/hooks/use-field-visibility.d.ts.map +1 -1
  51. package/dist/esm/hooks/use-field-visibility.js +10 -1
  52. package/dist/esm/i18n/translations/en.d.ts +8 -0
  53. package/dist/esm/i18n/translations/en.d.ts.map +1 -1
  54. package/dist/esm/i18n/translations/en.js +8 -0
  55. package/dist/esm/i18n/translations/tr.d.ts +8 -0
  56. package/dist/esm/i18n/translations/tr.d.ts.map +1 -1
  57. package/dist/esm/i18n/translations/tr.js +8 -0
  58. package/dist/esm/styles.css +36 -11
  59. package/dist/esm/types.d.ts +146 -3
  60. package/dist/esm/types.d.ts.map +1 -1
  61. package/dist/esm/utils/schema.d.ts +10 -0
  62. package/dist/esm/utils/schema.d.ts.map +1 -1
  63. package/dist/esm/utils/schema.js +10 -0
  64. package/package.json +7 -7
@@ -2,28 +2,14 @@ import { Button } from '@akinon/ui-button';
2
2
  import { Space } from '@akinon/ui-space';
3
3
  import { Title } from '@akinon/ui-typography';
4
4
  import React from 'react';
5
+ import { CSV_ACCEPT, XLS_ACCEPT } from '../constants';
5
6
  import { i18n } from '../i18n';
6
- const FileImportButton = ({ accept, label, onImport }) => {
7
- const inputRef = React.useRef(null);
8
- const handleClick = () => { var _a; return (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.click(); };
9
- const handleChange = (e) => {
10
- var _a;
11
- const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
12
- if (file) {
13
- onImport(file);
14
- }
15
- // Reset so same file can be re-selected
16
- e.target.value = '';
17
- };
18
- return (React.createElement(React.Fragment, null,
19
- React.createElement("input", { ref: inputRef, type: "file", accept: accept, style: { display: 'none' }, onChange: handleChange }),
20
- React.createElement(Button, { className: "akinon-filter__toolbar-item akinon-filter__toolbar-item--file", icon: "filter", iconSize: 13, onClick: handleClick, type: "primary", title: label, "aria-label": label }, label)));
21
- };
7
+ import { FileImportButton } from './file-import-button';
22
8
  export const FilterToolbar = ({ onOpenModal, enableImportCsv, enableImportXls, onImportCsv, onImportXls }) => {
23
9
  return (React.createElement("div", { className: "akinon-filter__top" },
24
10
  React.createElement(Title, { className: "akinon-filter__title", level: 4 }, i18n.t('header.title')),
25
11
  React.createElement(Space, { className: "akinon-filter__toolbar", size: 6 },
26
- enableImportCsv && onImportCsv ? (React.createElement(FileImportButton, { accept: ".csv", label: i18n.t('header.actions.importCsv'), onImport: onImportCsv })) : null,
27
- enableImportXls && onImportXls ? (React.createElement(FileImportButton, { accept: ".xls,.xlsx", label: i18n.t('header.actions.importXls'), onImport: onImportXls })) : null,
12
+ enableImportCsv && onImportCsv ? (React.createElement(FileImportButton, { accept: CSV_ACCEPT, label: i18n.t('header.actions.importCsv'), onImport: onImportCsv })) : null,
13
+ enableImportXls && onImportXls ? (React.createElement(FileImportButton, { accept: XLS_ACCEPT, label: i18n.t('header.actions.importXls'), onImport: onImportXls })) : null,
28
14
  React.createElement(Button, { className: "akinon-filter__toolbar-item", icon: "filter", iconSize: 16, onClick: onOpenModal, type: "primary", title: i18n.t('header.actions.selectFilters'), "aria-label": i18n.t('header.actions.selectFilters') }))));
29
15
  };
@@ -1,7 +1,19 @@
1
1
  import type { FieldValues } from '@akinon/akiform';
2
2
  import React from 'react';
3
3
  import type { AkifilterField, AkifilterSectionField } from '../types';
4
- type SectionFieldsetProps<TFieldValues extends FieldValues = FieldValues> = {
4
+ type SectionImportHandlers = {
5
+ /**
6
+ * Called with the CSV file picked from the section header. Only rendered when
7
+ * the section also opts in through `enableImportCsv`.
8
+ */
9
+ onImportCsv?: (file: File) => void;
10
+ /**
11
+ * Called with the XLS/XLSX file picked from the section header. Only rendered
12
+ * when the section also opts in through `enableImportXls`.
13
+ */
14
+ onImportXls?: (file: File) => void;
15
+ };
16
+ type SectionFieldsetProps<TFieldValues extends FieldValues = FieldValues> = SectionImportHandlers & {
5
17
  section: AkifilterSectionField<TFieldValues>;
6
18
  /**
7
19
  * Base storage key of the host Akifilter; combined with the section key to
@@ -22,7 +34,9 @@ type SectionFieldsetProps<TFieldValues extends FieldValues = FieldValues> = {
22
34
  /**
23
35
  * Renders a single section field. Sections flagged with `selectableFields`
24
36
  * expose a funnel icon + visibility modal; all others render every field
25
- * inline as before.
37
+ * inline as before. Both variants render CSV/XLS import buttons in the header
38
+ * when the section sets `enableImportCsv` / `enableImportXls` and the host
39
+ * supplies the matching handler.
26
40
  */
27
41
  export declare const SectionFieldset: <TFieldValues extends FieldValues = FieldValues>(props: SectionFieldsetProps<TFieldValues>) => React.JSX.Element;
28
42
  export {};
@@ -1 +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"}
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;AAQ1B,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AA+CtE,KAAK,qBAAqB,GAAG;IAC3B;;;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;CACpC,CAAC;AA8JF,KAAK,oBAAoB,CAAC,YAAY,SAAS,WAAW,GAAG,WAAW,IACtE,qBAAqB,GAAG;IACtB,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;AAiHJ;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAAI,YAAY,SAAS,WAAW,GAAG,WAAW,EAC5E,OAAO,oBAAoB,CAAC,YAAY,CAAC,sBAO1C,CAAC"}
@@ -4,10 +4,12 @@ import { Collapse } from '@akinon/ui-collapse';
4
4
  import { Title } from '@akinon/ui-typography';
5
5
  import React from 'react';
6
6
  import { buildSectionVisibilityKey } from '../common/storage';
7
+ import { CSV_ACCEPT, XLS_ACCEPT } from '../constants';
7
8
  import { useDynamicVisibility } from '../hooks/use-dynamic-visibility';
8
9
  import { useFieldVisibility } from '../hooks/use-field-visibility';
9
10
  import { useVisibilityCleanup } from '../hooks/use-visibility-cleanup';
10
11
  import { i18n } from '../i18n';
12
+ import { FileImportButton } from './file-import-button';
11
13
  import { VisibilityModal } from './visibility-modal';
12
14
  const ExpandIcon = ({ isActive }) => ({
13
15
  name: 'chevron_down',
@@ -30,11 +32,83 @@ const SectionCollapse = ({ sectionKey, label, defaultExpanded, children }) => (R
30
32
  children: React.createElement("div", { className: "akinon-filter__form-grid" }, children)
31
33
  }
32
34
  ] }));
35
+ /**
36
+ * Resolves the action buttons a section header should render. Import buttons
37
+ * require both the schema flag and a handler — mirroring how the top-level
38
+ * toolbar pairs `enableImportCsv` with `onImportCsv` — so a flag without a
39
+ * handler renders nothing instead of a dead button.
40
+ */
41
+ const resolveSectionHeaderActions = ({ section, onImportCsv, onImportXls, onOpenVisibilityModal }) => {
42
+ var _a;
43
+ const sectionLabel = String((_a = section.label) !== null && _a !== void 0 ? _a : section.key);
44
+ const showCsv = Boolean(section.enableImportCsv && onImportCsv);
45
+ const showXls = Boolean(section.enableImportXls && onImportXls);
46
+ const showFunnel = Boolean(section.selectableFields && onOpenVisibilityModal);
47
+ if (!showCsv && !showXls && !showFunnel) {
48
+ return null;
49
+ }
50
+ return (
51
+ // rc-collapse puts both `onClick` and an Enter-handling `onKeyDown` on the
52
+ // panel header, and the header is this group's ancestor. Guarding the group
53
+ // once covers every header action — mouse and keyboard alike — so
54
+ // activating one never toggles the panel underneath it.
55
+ React.createElement("div", { className: "akinon-filter__section-actions", onClick: event => event.stopPropagation(), onKeyDown: event => {
56
+ if (event.key === 'Enter') {
57
+ // Only the bubbling is stopped; the button's own activation still runs.
58
+ event.stopPropagation();
59
+ }
60
+ }, role: "presentation" },
61
+ showCsv && onImportCsv ? (React.createElement(FileImportButton, { accept: CSV_ACCEPT, label: i18n.t('section.actions.importCsv'), title: i18n.t('section.actions.importCsvTooltip', {
62
+ section: sectionLabel
63
+ }), onImport: onImportCsv, className: "akinon-filter__section-import akinon-filter__section-import--csv" })) : null,
64
+ showXls && onImportXls ? (React.createElement(FileImportButton, { accept: XLS_ACCEPT, label: i18n.t('section.actions.importXls'), title: i18n.t('section.actions.importXlsTooltip', {
65
+ section: sectionLabel
66
+ }), onImport: onImportXls, className: "akinon-filter__section-import akinon-filter__section-import--xls" })) : null,
67
+ showFunnel && onOpenVisibilityModal ? (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 => {
68
+ // Prevent the header click from toggling the collapse.
69
+ event.stopPropagation();
70
+ onOpenVisibilityModal();
71
+ } })) : null));
72
+ };
73
+ /**
74
+ * Collapse variant used whenever a section header carries action buttons.
75
+ * Controlled so we can render our own chevron and keep the actions pinned to
76
+ * the far right of the header (after the arrow).
77
+ */
78
+ const SectionActionsCollapse = ({ sectionKey, label, defaultExpanded, actions, isSelectable, children }) => {
79
+ const [activeKeys, setActiveKeys] = React.useState(defaultExpanded === false ? [] : [sectionKey]);
80
+ const isExpanded = activeKeys.includes(sectionKey);
81
+ const header = (React.createElement("div", { className: "akinon-filter__section-header-content" },
82
+ React.createElement(Title, { className: "akinon-filter__title", level: 4 }, label),
83
+ React.createElement(Icon, { icon: "chevron_down", size: 10, className: "akinon-filter__section-arrow", style: {
84
+ transform: isExpanded ? 'rotate(-180deg)' : 'rotate(0deg)',
85
+ transition: 'transform 0.2s ease-in-out'
86
+ } }),
87
+ actions));
88
+ return (React.createElement(Collapse, { activeKey: activeKeys, onChange: keys => setActiveKeys((Array.isArray(keys) ? keys : [keys]).map(String)), ghost: true, items: [
89
+ {
90
+ key: sectionKey,
91
+ label: header,
92
+ headerClass: [
93
+ 'akinon-filter__section-header',
94
+ 'akinon-filter__section-header--actions',
95
+ // Kept for backwards compatibility with host styles that target
96
+ // the original selectable-section modifier.
97
+ isSelectable ? 'akinon-filter__section-header--selectable' : null
98
+ ]
99
+ .filter(Boolean)
100
+ .join(' '),
101
+ showArrow: false,
102
+ children: React.createElement("div", { className: "akinon-filter__form-grid" }, children)
103
+ }
104
+ ] }));
105
+ };
33
106
  /**
34
107
  * A section whose header exposes a funnel icon that opens the visibility modal,
35
- * mirroring the top-level filter field-selection behaviour.
108
+ * mirroring the top-level filter field-selection behaviour. Import buttons are
109
+ * rendered alongside the funnel when the section opts into them.
36
110
  */
37
- const SelectableSectionFieldset = ({ section, storageKey, renderFormField, onRemoveField, formValues }) => {
111
+ const SelectableSectionFieldset = ({ section, storageKey, renderFormField, onRemoveField, formValues, onImportCsv, onImportXls }) => {
38
112
  const sectionStorageKey = React.useMemo(() => buildSectionVisibilityKey(storageKey, section.key), [storageKey, section.key]);
39
113
  const { visibleKeys, setVisibleKeys, visibleFields, openModal, modalProps } = useFieldVisibility({
40
114
  fields: section.fields,
@@ -50,44 +124,42 @@ const SelectableSectionFieldset = ({ section, storageKey, renderFormField, onRem
50
124
  regularFields: section.fields,
51
125
  onRemove: onRemoveField
52
126
  });
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
- } })));
127
+ const actions = resolveSectionHeaderActions({
128
+ section,
129
+ onImportCsv,
130
+ onImportXls,
131
+ onOpenVisibilityModal: openModal
132
+ });
69
133
  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
- ] }),
134
+ React.createElement(SectionActionsCollapse, { sectionKey: String(section.key), label: section.label, defaultExpanded: section.defaultExpanded, actions: actions, isSelectable: true }, visibleFields.map(renderFormField)),
79
135
  React.createElement(VisibilityModal, Object.assign({}, modalProps))));
80
136
  };
137
+ /**
138
+ * A section that always renders every one of its fields. Falls back to the
139
+ * plain collapse markup unless the header needs action buttons (e.g. import).
140
+ */
141
+ const PlainSectionFieldset = ({ section, renderFormField, onImportCsv, onImportXls }) => {
142
+ const actions = resolveSectionHeaderActions({
143
+ section,
144
+ onImportCsv,
145
+ onImportXls
146
+ });
147
+ const fields = section.fields.map(field => renderFormField(field));
148
+ if (actions) {
149
+ return (React.createElement(SectionActionsCollapse, { sectionKey: String(section.key), label: section.label, defaultExpanded: section.defaultExpanded, actions: actions, isSelectable: false }, fields));
150
+ }
151
+ return (React.createElement(SectionCollapse, { sectionKey: String(section.key), label: React.createElement(Title, { className: "akinon-filter__title", level: 4 }, section.label), defaultExpanded: section.defaultExpanded }, fields));
152
+ };
81
153
  /**
82
154
  * Renders a single section field. Sections flagged with `selectableFields`
83
155
  * expose a funnel icon + visibility modal; all others render every field
84
- * inline as before.
156
+ * inline as before. Both variants render CSV/XLS import buttons in the header
157
+ * when the section sets `enableImportCsv` / `enableImportXls` and the host
158
+ * supplies the matching handler.
85
159
  */
86
160
  export const SectionFieldset = (props) => {
87
- const { section, renderFormField } = props;
88
- if (section.selectableFields) {
161
+ if (props.section.selectableFields) {
89
162
  return React.createElement(SelectableSectionFieldset, Object.assign({}, props));
90
163
  }
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)));
164
+ return React.createElement(PlainSectionFieldset, Object.assign({}, props));
93
165
  };
@@ -6,7 +6,7 @@ import { Pagination } from '@akinon/ui-pagination';
6
6
  import { Text } from '@akinon/ui-typography';
7
7
  import React from 'react';
8
8
  import { i18n } from '../i18n';
9
- import { getDisplayLabel } from '../utils/schema';
9
+ import { getVisibilityLabel } from '../utils/schema';
10
10
  export const VisibilityModal = ({ open, searchTerm, paginatedFields, filteredCount, visibleKeys, onClose, onSearchTermChange, onToggleVisibility, onPaginationChange, page, pageSize }) => {
11
11
  const handleSearchChange = React.useCallback(event => {
12
12
  onSearchTermChange(event.target.value);
@@ -19,7 +19,7 @@ export const VisibilityModal = ({ open, searchTerm, paginatedFields, filteredCou
19
19
  }, size: "large" })),
20
20
  React.createElement("div", { className: "akinon-filter__modal-list" }, paginatedFields.length === 0 ? (React.createElement(Text, { type: "secondary" }, i18n.t('modal.empty'))) : (paginatedFields.map(field => {
21
21
  const key = String(field.key);
22
- return (React.createElement(Checkbox, { key: key, checked: visibleKeys.includes(key), onChange: () => onToggleVisibility(key), className: "akinon-filter__modal-checkbox" }, getDisplayLabel(field)));
22
+ return (React.createElement(Checkbox, { key: key, checked: visibleKeys.includes(key), onChange: () => onToggleVisibility(key), className: "akinon-filter__modal-checkbox" }, getVisibilityLabel(field)));
23
23
  }))),
24
24
  filteredCount > pageSize ? (React.createElement("div", { className: "akinon-filter__modal-pagination" },
25
25
  React.createElement(Pagination, { current: page, pageSize: pageSize, total: filteredCount, showSizeChanger: false, onChange: onPaginationChange }))) : null));
@@ -1,6 +1,12 @@
1
1
  export declare const DEFAULT_VISIBLE_COUNT = 8;
2
2
  export declare const DEFAULT_MODAL_PAGE_SIZE = 40;
3
3
  export declare const FILTER_DEBOUNCE_DELAY = 300;
4
+ /**
5
+ * `accept` values for the CSV / XLS import buttons. Shared by the top-level
6
+ * toolbar and the per-section import buttons so both accept the same files.
7
+ */
8
+ export declare const CSV_ACCEPT = ".csv";
9
+ export declare const XLS_ACCEPT = ".xls,.xlsx";
4
10
  export declare const DATE_FORMAT = "YYYY-MM-DD";
5
11
  export declare const DATETIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
6
12
  export declare const BOOLEAN_STRING: {
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,IAAI,CAAC;AACvC,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAC1C,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC,eAAO,MAAM,WAAW,eAAe,CAAC;AACxC,eAAO,MAAM,eAAe,wBAAwB,CAAC;AAErD,eAAO,MAAM,cAAc;;;CAGjB,CAAC;AAEX,eAAO,MAAM,WAAW;;;;;;;;;;CAUd,CAAC;AAEX,eAAO,MAAM,WAAW;;;CAGd,CAAC"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,IAAI,CAAC;AACvC,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAC1C,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC;;;GAGG;AACH,eAAO,MAAM,UAAU,SAAS,CAAC;AACjC,eAAO,MAAM,UAAU,eAAe,CAAC;AAEvC,eAAO,MAAM,WAAW,eAAe,CAAC;AACxC,eAAO,MAAM,eAAe,wBAAwB,CAAC;AAErD,eAAO,MAAM,cAAc;;;CAGjB,CAAC;AAEX,eAAO,MAAM,WAAW;;;;;;;;;;CAUd,CAAC;AAEX,eAAO,MAAM,WAAW;;;CAGd,CAAC"}
@@ -1,6 +1,12 @@
1
1
  export const DEFAULT_VISIBLE_COUNT = 8;
2
2
  export const DEFAULT_MODAL_PAGE_SIZE = 40;
3
3
  export const FILTER_DEBOUNCE_DELAY = 300;
4
+ /**
5
+ * `accept` values for the CSV / XLS import buttons. Shared by the top-level
6
+ * toolbar and the per-section import buttons so both accept the same files.
7
+ */
8
+ export const CSV_ACCEPT = '.csv';
9
+ export const XLS_ACCEPT = '.xls,.xlsx';
4
10
  export const DATE_FORMAT = 'YYYY-MM-DD';
5
11
  export const DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
6
12
  export const BOOLEAN_STRING = {
@@ -1 +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"}
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;;;;;;;;;;;;oCA0HP,MAAM;kCA3D9B,MAAM;mCAgEiB,MAAM,YAAY,MAAM;;;;CA6BxD,CAAC"}
@@ -73,7 +73,16 @@ export const useFieldVisibility = ({ fields, storageKey, onVisibleKeysChange })
73
73
  return fields;
74
74
  }
75
75
  return fields.filter(field => {
76
- const searchable = [field.label, field.placeholder, String(field.key)]
76
+ // `visibilityLabel` first: the modal renders it in place of `label`, so
77
+ // searching has to match the text the user can actually see. `label`
78
+ // stays searchable too — an overridden field is still findable by the
79
+ // name the rest of the form uses.
80
+ const searchable = [
81
+ field.visibilityLabel,
82
+ field.label,
83
+ field.placeholder,
84
+ String(field.key)
85
+ ]
77
86
  .filter(Boolean)
78
87
  .map(value => String(value).toLowerCase());
79
88
  return searchable.some(text => text.includes(normalisedTerm));
@@ -7,6 +7,14 @@ declare const translations: {
7
7
  readonly importXls: "XLS";
8
8
  };
9
9
  };
10
+ readonly section: {
11
+ readonly actions: {
12
+ readonly importCsv: "CSV";
13
+ readonly importXls: "XLS";
14
+ readonly importCsvTooltip: "Import {{section}} filters from a CSV file";
15
+ readonly importXlsTooltip: "Import {{section}} filters from an XLS file";
16
+ };
17
+ };
10
18
  readonly applied: {
11
19
  readonly label: "Active filters:";
12
20
  readonly empty: "No filters applied yet.";
@@ -1 +1 @@
1
- {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../../../src/i18n/translations/en.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4CR,CAAC;AAEX,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../../../src/i18n/translations/en.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoDR,CAAC;AAEX,eAAe,YAAY,CAAC"}
@@ -7,6 +7,14 @@ const translations = {
7
7
  importXls: 'XLS'
8
8
  }
9
9
  },
10
+ section: {
11
+ actions: {
12
+ importCsv: 'CSV',
13
+ importXls: 'XLS',
14
+ importCsvTooltip: 'Import {{section}} filters from a CSV file',
15
+ importXlsTooltip: 'Import {{section}} filters from an XLS file'
16
+ }
17
+ },
10
18
  applied: {
11
19
  label: 'Active filters:',
12
20
  empty: 'No filters applied yet.',
@@ -7,6 +7,14 @@ declare const translations: {
7
7
  readonly importXls: "XLS";
8
8
  };
9
9
  };
10
+ readonly section: {
11
+ readonly actions: {
12
+ readonly importCsv: "CSV";
13
+ readonly importXls: "XLS";
14
+ readonly importCsvTooltip: "{{section}} filtrelerini CSV dosyasından içe aktar";
15
+ readonly importXlsTooltip: "{{section}} filtrelerini XLS dosyasından içe aktar";
16
+ };
17
+ };
10
18
  readonly applied: {
11
19
  readonly label: "Aktif filtreler:";
12
20
  readonly empty: "Henüz filtre uygulanmadı.";
@@ -1 +1 @@
1
- {"version":3,"file":"tr.d.ts","sourceRoot":"","sources":["../../../../src/i18n/translations/tr.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4CR,CAAC;AAEX,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"tr.d.ts","sourceRoot":"","sources":["../../../../src/i18n/translations/tr.ts"],"names":[],"mappings":"AAAA,QAAA,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAoDR,CAAC;AAEX,eAAe,YAAY,CAAC"}
@@ -7,6 +7,14 @@ const translations = {
7
7
  importXls: 'XLS'
8
8
  }
9
9
  },
10
+ section: {
11
+ actions: {
12
+ importCsv: 'CSV',
13
+ importXls: 'XLS',
14
+ importCsvTooltip: '{{section}} filtrelerini CSV dosyasından içe aktar',
15
+ importXlsTooltip: '{{section}} filtrelerini XLS dosyasından içe aktar'
16
+ }
17
+ },
10
18
  applied: {
11
19
  label: 'Aktif filtreler:',
12
20
  empty: 'Henüz filtre uygulanmadı.',
@@ -26,15 +26,39 @@
26
26
  height: 36px;
27
27
  }
28
28
 
29
+ /*
30
+ * Every filter action button is the same 36px square so the toolbar and the
31
+ * section headers line up on both axes. The `.akinon-filter` prefix is what
32
+ * makes the width stick: antd sizes icon-only buttons from its own
33
+ * `controlHeight` at two-class specificity, which a lone class cannot beat.
34
+ */
35
+ .akinon-filter .akinon-filter__toolbar-item {
36
+ width: 36px;
37
+ justify-content: center;
38
+ }
39
+
40
+ /*
41
+ * File import buttons stay square like every other toolbar action, with the
42
+ * icon stacked above its short label. Both must be centred by hand: antd lays
43
+ * the button out as a flex row and separates icon from label with
44
+ * `margin-inline-*`, which reads as a stray horizontal offset — the icon
45
+ * drifting off centre — once the direction is column.
46
+ */
29
47
  .akinon-filter__toolbar-item--file {
30
48
  width: 36px;
31
- font-size: 11px;
32
- font-weight: bold;
33
49
  flex-direction: column;
34
- justify-content: flex-end;
50
+ align-items: center;
51
+ justify-content: center;
35
52
  gap: 2px;
36
- padding-inline: 0;
37
- padding-block-end: 4px;
53
+ padding: 0;
54
+ font-size: 9px;
55
+ font-weight: 700;
56
+ line-height: 1;
57
+ }
58
+
59
+ .akinon-filter__toolbar-item--file > span {
60
+ margin-inline: 0 !important;
61
+ line-height: 1;
38
62
  }
39
63
 
40
64
  .akinon-filter__applied {
@@ -77,8 +101,8 @@
77
101
  width: 100%;
78
102
  }
79
103
 
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 {
104
+ /* Let the header text span fill the header so the actions can sit at the far right. */
105
+ .akinon-filter__section-header--actions .akinon-collapse-header-text {
82
106
  flex: 1 1 auto !important;
83
107
  }
84
108
 
@@ -86,10 +110,11 @@
86
110
  color: var(--color-gray-500);
87
111
  }
88
112
 
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;
113
+ /* Action buttons pinned to the far right of the header, after title and arrow. */
114
+ .akinon-filter__section-actions {
115
+ display: flex;
116
+ align-items: center;
117
+ gap: 6px;
93
118
  margin-left: auto;
94
119
  }
95
120
 
@@ -13,8 +13,40 @@ export interface AkifilterFieldConfig<TFieldValues extends FieldValues = FieldVa
13
13
  */
14
14
  visible?: boolean | ((formValues: TFieldValues, currentVisible?: boolean) => boolean);
15
15
  }
16
- type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
17
- export type AkifilterField<TFieldValues extends FieldValues = FieldValues> = DistributiveOmit<FormField<TFieldValues>, 'config'> & {
16
+ /**
17
+ * One applied-filter chip produced by a field that owns several filters at
18
+ * once (see `appliedChips`).
19
+ */
20
+ export type AkifilterFieldChip = {
21
+ /**
22
+ * Identifies the chip inside its field. Passed back to
23
+ * `onRemoveAppliedChip` when the chip is removed, so it has to be stable for
24
+ * the same logical entry (e.g. the filter type it represents) and unique
25
+ * within the field — two chips sharing an id cannot be told apart on
26
+ * removal.
27
+ */
28
+ id: string;
29
+ /**
30
+ * Text before the colon. Defaults to the field's applied label
31
+ * (`appliedLabel`, else `label`).
32
+ */
33
+ label?: string;
34
+ /**
35
+ * Text after the colon — already formatted, since only the field knows how
36
+ * to render its own value.
37
+ */
38
+ value: string;
39
+ /**
40
+ * Renders the chip with the exclude styling. Defaults to whether the field
41
+ * belongs to an exclude section.
42
+ */
43
+ isExclude?: boolean;
44
+ };
45
+ /**
46
+ * Props Akifilter adds on top of the shared `FormField`. Kept separate so the
47
+ * mapping below can apply them to nested fields as well as top-level ones.
48
+ */
49
+ type AkifilterFieldExtras<TFieldValues extends FieldValues = FieldValues> = {
18
50
  /**
19
51
  * Configuration options for field behavior (disabled, visible).
20
52
  */
@@ -24,14 +56,109 @@ export type AkifilterField<TFieldValues extends FieldValues = FieldValues> = Dis
24
56
  * Only applicable when `type` is `'file'`.
25
57
  */
26
58
  fileAccept?: string;
59
+ /**
60
+ * Expands this field's value into one chip per entry, instead of the single
61
+ * chip Akifilter derives from the value itself.
62
+ *
63
+ * This is what lets one field own several filters at once — a value that
64
+ * holds a bucket per filter type, a range that reads better as two chips,
65
+ * and so on. Because the buckets stay inside the field's form value,
66
+ * Akifilter persists and rehydrates them like any other value; nothing has
67
+ * to be mirrored outside the component.
68
+ *
69
+ * Pair it with `onRemoveAppliedChip` to make the chips removable. Return an
70
+ * empty array for "no chips right now".
71
+ *
72
+ * Only called while the field's own value is non-empty: the emptiness rules
73
+ * that suppress a plain chip (`null`/`undefined`, a blank string, an empty
74
+ * array) suppress these too. `formValues` is there to *format* the chips —
75
+ * a unit or a mode held by a sibling field — not to derive them from a
76
+ * sibling while this field holds nothing.
77
+ */
78
+ appliedChips?: (context: {
79
+ field: AkifilterField<TFieldValues>;
80
+ value: unknown;
81
+ formValues: Partial<TFieldValues>;
82
+ }) => AkifilterFieldChip[];
83
+ /**
84
+ * Maps the removal of one `appliedChips` chip back onto a new value for the
85
+ * field, which Akifilter then commits, persists and emits.
86
+ *
87
+ * Return the value the field should hold after the removal — `null` to clear
88
+ * the field entirely. Without this callback the chips render but cannot be
89
+ * removed.
90
+ */
91
+ onRemoveAppliedChip?: (context: {
92
+ field: AkifilterField<TFieldValues>;
93
+ value: unknown;
94
+ id: string;
95
+ }) => unknown;
96
+ /**
97
+ * Renders a CSV import button in the section header, paired with the
98
+ * `onSectionImportCsv` callback. Section fields only.
99
+ */
100
+ enableImportCsv?: boolean;
101
+ /**
102
+ * Renders an XLS/XLSX import button in the section header, paired with the
103
+ * `onSectionImportXls` callback. Section fields only.
104
+ */
105
+ enableImportXls?: boolean;
106
+ /**
107
+ * Overrides `label` for this field's applied-filter chip only.
108
+ *
109
+ * The form input and the "select visible filters" modal keep using `label`.
110
+ * Use this when the chip needs a shorter or more explicit name than the form
111
+ * does (e.g. `label: 'Created At'` next to a date range, `appliedLabel:
112
+ * 'Order date'` in the chip).
113
+ */
114
+ appliedLabel?: string;
115
+ /**
116
+ * Overrides `label` for this field's row in the "select visible filters"
117
+ * modal only. Also what the modal's search box matches against.
118
+ *
119
+ * The form input and the applied-filter chip keep using `label`. Use this
120
+ * when the modal lists fields out of context and needs a fully qualified
121
+ * name (e.g. `label: 'Status'`, `visibilityLabel: 'Order status'`).
122
+ */
123
+ visibilityLabel?: string;
27
124
  };
125
+ /**
126
+ * Applies `AkifilterFieldExtras` to one member of the `FormField` union.
127
+ *
128
+ * Container fields (`section`, `field-array`, `column`) get their `fields`
129
+ * re-typed recursively: without that, the shared `FormField[]` child type
130
+ * would reject every Akifilter-only prop — a field inside a section could not
131
+ * declare `config.visible`, `fileAccept`, or a label override at all.
132
+ */
133
+ type Akifiltered<TField, TFieldValues extends FieldValues> = TField extends {
134
+ fields: unknown[];
135
+ } ? Omit<TField, 'config' | 'fields'> & AkifilterFieldExtras<TFieldValues> & {
136
+ fields: AkifilterField<TFieldValues>[];
137
+ } : Omit<TField, 'config'> & AkifilterFieldExtras<TFieldValues>;
138
+ /**
139
+ * A single Akifilter schema entry. `TField` below is a naked type parameter,
140
+ * so the conditional distributes over every member of the `FormField` union
141
+ * and each variant keeps its own discriminating `type` literal.
142
+ */
143
+ export type AkifilterField<TFieldValues extends FieldValues = FieldValues> = Akifiltered<FormField<TFieldValues>, TFieldValues>;
28
144
  /**
29
145
  * Section field as consumed by Akifilter. Derived from the shared
30
146
  * `SectionField` so flags like `selectableFields`/`isExcludeSection` stay
31
147
  * single-sourced, but omits `collapsible` — Akifilter always renders sections
32
148
  * as collapsible panels and does not honor that prop.
33
149
  */
34
- export type AkifilterSectionField<TFieldValues extends FieldValues = FieldValues> = Omit<SectionField<TFieldValues>, 'collapsible'>;
150
+ export type AkifilterSectionField<TFieldValues extends FieldValues = FieldValues> = Omit<SectionField<TFieldValues>, 'collapsible' | 'fields'> & {
151
+ /**
152
+ * Child fields, typed as Akifilter fields rather than the shared
153
+ * `FormField` so section children accept the same Akifilter-only props as
154
+ * top-level ones (`config.visible`, `fileAccept`, the label overrides).
155
+ */
156
+ fields: AkifilterField<TFieldValues>[];
157
+ /** Renders a CSV import button in the section header. */
158
+ enableImportCsv?: boolean;
159
+ /** Renders an XLS/XLSX import button in the section header. */
160
+ enableImportXls?: boolean;
161
+ };
35
162
  export type AkifilterFieldKey<TFieldValues extends FieldValues = FieldValues> = Path<TFieldValues>;
36
163
  export type AkifilterSchema<TFieldValues extends FieldValues = FieldValues> = AkifilterField<TFieldValues>[];
37
164
  /**
@@ -88,5 +215,21 @@ export interface AkifilterActionsRef {
88
215
  */
89
216
  clearValue: (keys: string | string[]) => void;
90
217
  }
218
+ declare module '@akinon/akiform-builder' {
219
+ interface FieldExtensions {
220
+ /** @see AkifilterField.appliedLabel */
221
+ appliedLabel?: AkifilterField['appliedLabel'];
222
+ /** @see AkifilterField.visibilityLabel */
223
+ visibilityLabel?: AkifilterField['visibilityLabel'];
224
+ /** @see AkifilterField.appliedChips */
225
+ appliedChips?: AkifilterField['appliedChips'];
226
+ /** @see AkifilterField.onRemoveAppliedChip */
227
+ onRemoveAppliedChip?: AkifilterField['onRemoveAppliedChip'];
228
+ /** @see AkifilterField.enableImportCsv */
229
+ enableImportCsv?: boolean;
230
+ /** @see AkifilterField.enableImportXls */
231
+ enableImportXls?: boolean;
232
+ }
233
+ }
91
234
  export {};
92
235
  //# sourceMappingURL=types.d.ts.map