@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
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FileImportButton = void 0;
4
+ const ui_button_1 = require("@akinon/ui-button");
5
+ const react_1 = require("react");
6
+ /**
7
+ * Button that opens the native file picker and hands the selected file to
8
+ * `onImport`. Shared by the top-level filter toolbar and the section headers so
9
+ * both surfaces behave identically.
10
+ */
11
+ const FileImportButton = ({ accept, label, title, onImport, className }) => {
12
+ const inputRef = react_1.default.useRef(null);
13
+ const resolvedTitle = title !== null && title !== void 0 ? title : label;
14
+ const handleClick = () => { var _a; return (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.click(); };
15
+ const handleChange = (event) => {
16
+ var _a;
17
+ const file = (_a = event.target.files) === null || _a === void 0 ? void 0 : _a[0];
18
+ if (file) {
19
+ onImport(file);
20
+ }
21
+ // Reset so same file can be re-selected
22
+ event.target.value = '';
23
+ };
24
+ return (react_1.default.createElement(react_1.default.Fragment, null,
25
+ react_1.default.createElement("input", { ref: inputRef, type: "file", accept: accept, style: { display: 'none' }, onChange: handleChange }),
26
+ react_1.default.createElement(ui_button_1.Button, { className: [
27
+ 'akinon-filter__toolbar-item',
28
+ 'akinon-filter__toolbar-item--file',
29
+ className
30
+ ]
31
+ .filter(Boolean)
32
+ .join(' '), icon: "filter",
33
+ // Smaller than the icon-only toolbar buttons: the icon shares its 36px
34
+ // square with the label underneath it.
35
+ iconSize: 12, onClick: handleClick, type: "primary", title: resolvedTitle, "aria-label": resolvedTitle }, label)));
36
+ };
37
+ exports.FileImportButton = FileImportButton;
@@ -1 +1 @@
1
- {"version":3,"file":"filter-toolbar.d.ts","sourceRoot":"","sources":["../../../src/components/filter-toolbar.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,KAAK,kBAAkB,GAAG;IACxB,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IACnC,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;CACpC,CAAC;AAgDF,eAAO,MAAM,aAAa,GAAI,6EAM3B,kBAAkB,sBAoCpB,CAAC"}
1
+ {"version":3,"file":"filter-toolbar.d.ts","sourceRoot":"","sources":["../../../src/components/filter-toolbar.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,MAAM,OAAO,CAAC;AAM1B,KAAK,kBAAkB,GAAG;IACxB,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IACnC,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;CACpC,CAAC;AAEF,eAAO,MAAM,aAAa,GAAI,6EAM3B,kBAAkB,sBAoCpB,CAAC"}
@@ -5,29 +5,15 @@ const ui_button_1 = require("@akinon/ui-button");
5
5
  const ui_space_1 = require("@akinon/ui-space");
6
6
  const ui_typography_1 = require("@akinon/ui-typography");
7
7
  const react_1 = require("react");
8
+ const constants_1 = require("../constants");
8
9
  const i18n_1 = require("../i18n");
9
- const FileImportButton = ({ accept, label, onImport }) => {
10
- const inputRef = react_1.default.useRef(null);
11
- const handleClick = () => { var _a; return (_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.click(); };
12
- const handleChange = (e) => {
13
- var _a;
14
- const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
15
- if (file) {
16
- onImport(file);
17
- }
18
- // Reset so same file can be re-selected
19
- e.target.value = '';
20
- };
21
- return (react_1.default.createElement(react_1.default.Fragment, null,
22
- react_1.default.createElement("input", { ref: inputRef, type: "file", accept: accept, style: { display: 'none' }, onChange: handleChange }),
23
- react_1.default.createElement(ui_button_1.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)));
24
- };
10
+ const file_import_button_1 = require("./file-import-button");
25
11
  const FilterToolbar = ({ onOpenModal, enableImportCsv, enableImportXls, onImportCsv, onImportXls }) => {
26
12
  return (react_1.default.createElement("div", { className: "akinon-filter__top" },
27
13
  react_1.default.createElement(ui_typography_1.Title, { className: "akinon-filter__title", level: 4 }, i18n_1.i18n.t('header.title')),
28
14
  react_1.default.createElement(ui_space_1.Space, { className: "akinon-filter__toolbar", size: 6 },
29
- enableImportCsv && onImportCsv ? (react_1.default.createElement(FileImportButton, { accept: ".csv", label: i18n_1.i18n.t('header.actions.importCsv'), onImport: onImportCsv })) : null,
30
- enableImportXls && onImportXls ? (react_1.default.createElement(FileImportButton, { accept: ".xls,.xlsx", label: i18n_1.i18n.t('header.actions.importXls'), onImport: onImportXls })) : null,
15
+ enableImportCsv && onImportCsv ? (react_1.default.createElement(file_import_button_1.FileImportButton, { accept: constants_1.CSV_ACCEPT, label: i18n_1.i18n.t('header.actions.importCsv'), onImport: onImportCsv })) : null,
16
+ enableImportXls && onImportXls ? (react_1.default.createElement(file_import_button_1.FileImportButton, { accept: constants_1.XLS_ACCEPT, label: i18n_1.i18n.t('header.actions.importXls'), onImport: onImportXls })) : null,
31
17
  react_1.default.createElement(ui_button_1.Button, { className: "akinon-filter__toolbar-item", icon: "filter", iconSize: 16, onClick: onOpenModal, type: "primary", title: i18n_1.i18n.t('header.actions.selectFilters'), "aria-label": i18n_1.i18n.t('header.actions.selectFilters') }))));
32
18
  };
33
19
  exports.FilterToolbar = FilterToolbar;
@@ -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"}
@@ -7,10 +7,12 @@ const ui_collapse_1 = require("@akinon/ui-collapse");
7
7
  const ui_typography_1 = require("@akinon/ui-typography");
8
8
  const react_1 = require("react");
9
9
  const storage_1 = require("../common/storage");
10
+ const constants_1 = require("../constants");
10
11
  const use_dynamic_visibility_1 = require("../hooks/use-dynamic-visibility");
11
12
  const use_field_visibility_1 = require("../hooks/use-field-visibility");
12
13
  const use_visibility_cleanup_1 = require("../hooks/use-visibility-cleanup");
13
14
  const i18n_1 = require("../i18n");
15
+ const file_import_button_1 = require("./file-import-button");
14
16
  const visibility_modal_1 = require("./visibility-modal");
15
17
  const ExpandIcon = ({ isActive }) => ({
16
18
  name: 'chevron_down',
@@ -33,11 +35,83 @@ const SectionCollapse = ({ sectionKey, label, defaultExpanded, children }) => (r
33
35
  children: react_1.default.createElement("div", { className: "akinon-filter__form-grid" }, children)
34
36
  }
35
37
  ] }));
38
+ /**
39
+ * Resolves the action buttons a section header should render. Import buttons
40
+ * require both the schema flag and a handler — mirroring how the top-level
41
+ * toolbar pairs `enableImportCsv` with `onImportCsv` — so a flag without a
42
+ * handler renders nothing instead of a dead button.
43
+ */
44
+ const resolveSectionHeaderActions = ({ section, onImportCsv, onImportXls, onOpenVisibilityModal }) => {
45
+ var _a;
46
+ const sectionLabel = String((_a = section.label) !== null && _a !== void 0 ? _a : section.key);
47
+ const showCsv = Boolean(section.enableImportCsv && onImportCsv);
48
+ const showXls = Boolean(section.enableImportXls && onImportXls);
49
+ const showFunnel = Boolean(section.selectableFields && onOpenVisibilityModal);
50
+ if (!showCsv && !showXls && !showFunnel) {
51
+ return null;
52
+ }
53
+ return (
54
+ // rc-collapse puts both `onClick` and an Enter-handling `onKeyDown` on the
55
+ // panel header, and the header is this group's ancestor. Guarding the group
56
+ // once covers every header action — mouse and keyboard alike — so
57
+ // activating one never toggles the panel underneath it.
58
+ react_1.default.createElement("div", { className: "akinon-filter__section-actions", onClick: event => event.stopPropagation(), onKeyDown: event => {
59
+ if (event.key === 'Enter') {
60
+ // Only the bubbling is stopped; the button's own activation still runs.
61
+ event.stopPropagation();
62
+ }
63
+ }, role: "presentation" },
64
+ showCsv && onImportCsv ? (react_1.default.createElement(file_import_button_1.FileImportButton, { accept: constants_1.CSV_ACCEPT, label: i18n_1.i18n.t('section.actions.importCsv'), title: i18n_1.i18n.t('section.actions.importCsvTooltip', {
65
+ section: sectionLabel
66
+ }), onImport: onImportCsv, className: "akinon-filter__section-import akinon-filter__section-import--csv" })) : null,
67
+ showXls && onImportXls ? (react_1.default.createElement(file_import_button_1.FileImportButton, { accept: constants_1.XLS_ACCEPT, label: i18n_1.i18n.t('section.actions.importXls'), title: i18n_1.i18n.t('section.actions.importXlsTooltip', {
68
+ section: sectionLabel
69
+ }), onImport: onImportXls, className: "akinon-filter__section-import akinon-filter__section-import--xls" })) : null,
70
+ showFunnel && onOpenVisibilityModal ? (react_1.default.createElement(ui_button_1.Button, { className: "akinon-filter__toolbar-item akinon-filter__section-funnel", icon: "filter", iconSize: 16, type: "primary", title: i18n_1.i18n.t('header.actions.selectFilters'), "aria-label": i18n_1.i18n.t('header.actions.selectFilters'), onClick: event => {
71
+ // Prevent the header click from toggling the collapse.
72
+ event.stopPropagation();
73
+ onOpenVisibilityModal();
74
+ } })) : null));
75
+ };
76
+ /**
77
+ * Collapse variant used whenever a section header carries action buttons.
78
+ * Controlled so we can render our own chevron and keep the actions pinned to
79
+ * the far right of the header (after the arrow).
80
+ */
81
+ const SectionActionsCollapse = ({ sectionKey, label, defaultExpanded, actions, isSelectable, children }) => {
82
+ const [activeKeys, setActiveKeys] = react_1.default.useState(defaultExpanded === false ? [] : [sectionKey]);
83
+ const isExpanded = activeKeys.includes(sectionKey);
84
+ const header = (react_1.default.createElement("div", { className: "akinon-filter__section-header-content" },
85
+ react_1.default.createElement(ui_typography_1.Title, { className: "akinon-filter__title", level: 4 }, label),
86
+ react_1.default.createElement(icons_1.Icon, { icon: "chevron_down", size: 10, className: "akinon-filter__section-arrow", style: {
87
+ transform: isExpanded ? 'rotate(-180deg)' : 'rotate(0deg)',
88
+ transition: 'transform 0.2s ease-in-out'
89
+ } }),
90
+ actions));
91
+ return (react_1.default.createElement(ui_collapse_1.Collapse, { activeKey: activeKeys, onChange: keys => setActiveKeys((Array.isArray(keys) ? keys : [keys]).map(String)), ghost: true, items: [
92
+ {
93
+ key: sectionKey,
94
+ label: header,
95
+ headerClass: [
96
+ 'akinon-filter__section-header',
97
+ 'akinon-filter__section-header--actions',
98
+ // Kept for backwards compatibility with host styles that target
99
+ // the original selectable-section modifier.
100
+ isSelectable ? 'akinon-filter__section-header--selectable' : null
101
+ ]
102
+ .filter(Boolean)
103
+ .join(' '),
104
+ showArrow: false,
105
+ children: react_1.default.createElement("div", { className: "akinon-filter__form-grid" }, children)
106
+ }
107
+ ] }));
108
+ };
36
109
  /**
37
110
  * A section whose header exposes a funnel icon that opens the visibility modal,
38
- * mirroring the top-level filter field-selection behaviour.
111
+ * mirroring the top-level filter field-selection behaviour. Import buttons are
112
+ * rendered alongside the funnel when the section opts into them.
39
113
  */
40
- const SelectableSectionFieldset = ({ section, storageKey, renderFormField, onRemoveField, formValues }) => {
114
+ const SelectableSectionFieldset = ({ section, storageKey, renderFormField, onRemoveField, formValues, onImportCsv, onImportXls }) => {
41
115
  const sectionStorageKey = react_1.default.useMemo(() => (0, storage_1.buildSectionVisibilityKey)(storageKey, section.key), [storageKey, section.key]);
42
116
  const { visibleKeys, setVisibleKeys, visibleFields, openModal, modalProps } = (0, use_field_visibility_1.useFieldVisibility)({
43
117
  fields: section.fields,
@@ -53,45 +127,43 @@ const SelectableSectionFieldset = ({ section, storageKey, renderFormField, onRem
53
127
  regularFields: section.fields,
54
128
  onRemove: onRemoveField
55
129
  });
56
- // Controlled so we can render our own chevron in the header and keep the
57
- // funnel button pinned to the far right (after the arrow).
58
- const sectionKey = String(section.key);
59
- const [activeKeys, setActiveKeys] = react_1.default.useState(section.defaultExpanded === false ? [] : [sectionKey]);
60
- const isExpanded = activeKeys.includes(sectionKey);
61
- const label = (react_1.default.createElement("div", { className: "akinon-filter__section-header-content" },
62
- react_1.default.createElement(ui_typography_1.Title, { className: "akinon-filter__title", level: 4 }, section.label),
63
- react_1.default.createElement(icons_1.Icon, { icon: "chevron_down", size: 10, className: "akinon-filter__section-arrow", style: {
64
- transform: isExpanded ? 'rotate(-180deg)' : 'rotate(0deg)',
65
- transition: 'transform 0.2s ease-in-out'
66
- } }),
67
- react_1.default.createElement(ui_button_1.Button, { className: "akinon-filter__toolbar-item akinon-filter__section-funnel", icon: "filter", iconSize: 16, type: "primary", title: i18n_1.i18n.t('header.actions.selectFilters'), "aria-label": i18n_1.i18n.t('header.actions.selectFilters'), onClick: event => {
68
- // Prevent the header click from toggling the collapse.
69
- event.stopPropagation();
70
- openModal();
71
- } })));
130
+ const actions = resolveSectionHeaderActions({
131
+ section,
132
+ onImportCsv,
133
+ onImportXls,
134
+ onOpenVisibilityModal: openModal
135
+ });
72
136
  return (react_1.default.createElement(react_1.default.Fragment, null,
73
- react_1.default.createElement(ui_collapse_1.Collapse, { activeKey: activeKeys, onChange: keys => setActiveKeys((Array.isArray(keys) ? keys : [keys]).map(String)), ghost: true, items: [
74
- {
75
- key: sectionKey,
76
- label,
77
- headerClass: 'akinon-filter__section-header akinon-filter__section-header--selectable',
78
- showArrow: false,
79
- children: (react_1.default.createElement("div", { className: "akinon-filter__form-grid" }, visibleFields.map(renderFormField)))
80
- }
81
- ] }),
137
+ react_1.default.createElement(SectionActionsCollapse, { sectionKey: String(section.key), label: section.label, defaultExpanded: section.defaultExpanded, actions: actions, isSelectable: true }, visibleFields.map(renderFormField)),
82
138
  react_1.default.createElement(visibility_modal_1.VisibilityModal, Object.assign({}, modalProps))));
83
139
  };
140
+ /**
141
+ * A section that always renders every one of its fields. Falls back to the
142
+ * plain collapse markup unless the header needs action buttons (e.g. import).
143
+ */
144
+ const PlainSectionFieldset = ({ section, renderFormField, onImportCsv, onImportXls }) => {
145
+ const actions = resolveSectionHeaderActions({
146
+ section,
147
+ onImportCsv,
148
+ onImportXls
149
+ });
150
+ const fields = section.fields.map(field => renderFormField(field));
151
+ if (actions) {
152
+ return (react_1.default.createElement(SectionActionsCollapse, { sectionKey: String(section.key), label: section.label, defaultExpanded: section.defaultExpanded, actions: actions, isSelectable: false }, fields));
153
+ }
154
+ return (react_1.default.createElement(SectionCollapse, { sectionKey: String(section.key), label: react_1.default.createElement(ui_typography_1.Title, { className: "akinon-filter__title", level: 4 }, section.label), defaultExpanded: section.defaultExpanded }, fields));
155
+ };
84
156
  /**
85
157
  * Renders a single section field. Sections flagged with `selectableFields`
86
158
  * expose a funnel icon + visibility modal; all others render every field
87
- * inline as before.
159
+ * inline as before. Both variants render CSV/XLS import buttons in the header
160
+ * when the section sets `enableImportCsv` / `enableImportXls` and the host
161
+ * supplies the matching handler.
88
162
  */
89
163
  const SectionFieldset = (props) => {
90
- const { section, renderFormField } = props;
91
- if (section.selectableFields) {
164
+ if (props.section.selectableFields) {
92
165
  return react_1.default.createElement(SelectableSectionFieldset, Object.assign({}, props));
93
166
  }
94
- const label = (react_1.default.createElement(ui_typography_1.Title, { className: "akinon-filter__title", level: 4 }, section.label));
95
- return (react_1.default.createElement(SectionCollapse, { sectionKey: String(section.key), label: label, defaultExpanded: section.defaultExpanded }, section.fields.map(renderFormField)));
167
+ return react_1.default.createElement(PlainSectionFieldset, Object.assign({}, props));
96
168
  };
97
169
  exports.SectionFieldset = SectionFieldset;
@@ -22,7 +22,7 @@ const VisibilityModal = ({ open, searchTerm, paginatedFields, filteredCount, vis
22
22
  }, size: "large" })),
23
23
  react_1.default.createElement("div", { className: "akinon-filter__modal-list" }, paginatedFields.length === 0 ? (react_1.default.createElement(ui_typography_1.Text, { type: "secondary" }, i18n_1.i18n.t('modal.empty'))) : (paginatedFields.map(field => {
24
24
  const key = String(field.key);
25
- return (react_1.default.createElement(ui_checkbox_1.Checkbox, { key: key, checked: visibleKeys.includes(key), onChange: () => onToggleVisibility(key), className: "akinon-filter__modal-checkbox" }, (0, schema_1.getDisplayLabel)(field)));
25
+ return (react_1.default.createElement(ui_checkbox_1.Checkbox, { key: key, checked: visibleKeys.includes(key), onChange: () => onToggleVisibility(key), className: "akinon-filter__modal-checkbox" }, (0, schema_1.getVisibilityLabel)(field)));
26
26
  }))),
27
27
  filteredCount > pageSize ? (react_1.default.createElement("div", { className: "akinon-filter__modal-pagination" },
28
28
  react_1.default.createElement(ui_pagination_1.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,9 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FIELD_MODES = exports.FIELD_TYPES = exports.BOOLEAN_STRING = exports.DATETIME_FORMAT = exports.DATE_FORMAT = exports.FILTER_DEBOUNCE_DELAY = exports.DEFAULT_MODAL_PAGE_SIZE = exports.DEFAULT_VISIBLE_COUNT = void 0;
3
+ exports.FIELD_MODES = exports.FIELD_TYPES = exports.BOOLEAN_STRING = exports.DATETIME_FORMAT = exports.DATE_FORMAT = exports.XLS_ACCEPT = exports.CSV_ACCEPT = exports.FILTER_DEBOUNCE_DELAY = exports.DEFAULT_MODAL_PAGE_SIZE = exports.DEFAULT_VISIBLE_COUNT = void 0;
4
4
  exports.DEFAULT_VISIBLE_COUNT = 8;
5
5
  exports.DEFAULT_MODAL_PAGE_SIZE = 40;
6
6
  exports.FILTER_DEBOUNCE_DELAY = 300;
7
+ /**
8
+ * `accept` values for the CSV / XLS import buttons. Shared by the top-level
9
+ * toolbar and the per-section import buttons so both accept the same files.
10
+ */
11
+ exports.CSV_ACCEPT = '.csv';
12
+ exports.XLS_ACCEPT = '.xls,.xlsx';
7
13
  exports.DATE_FORMAT = 'YYYY-MM-DD';
8
14
  exports.DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';
9
15
  exports.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"}
@@ -76,7 +76,16 @@ const useFieldVisibility = ({ fields, storageKey, onVisibleKeysChange }) => {
76
76
  return fields;
77
77
  }
78
78
  return fields.filter(field => {
79
- const searchable = [field.label, field.placeholder, String(field.key)]
79
+ // `visibilityLabel` first: the modal renders it in place of `label`, so
80
+ // searching has to match the text the user can actually see. `label`
81
+ // stays searchable too — an overridden field is still findable by the
82
+ // name the rest of the form uses.
83
+ const searchable = [
84
+ field.visibilityLabel,
85
+ field.label,
86
+ field.placeholder,
87
+ String(field.key)
88
+ ]
80
89
  .filter(Boolean)
81
90
  .map(value => String(value).toLowerCase());
82
91
  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"}
@@ -9,6 +9,14 @@ const translations = {
9
9
  importXls: 'XLS'
10
10
  }
11
11
  },
12
+ section: {
13
+ actions: {
14
+ importCsv: 'CSV',
15
+ importXls: 'XLS',
16
+ importCsvTooltip: 'Import {{section}} filters from a CSV file',
17
+ importXlsTooltip: 'Import {{section}} filters from an XLS file'
18
+ }
19
+ },
12
20
  applied: {
13
21
  label: 'Active filters:',
14
22
  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"}
@@ -9,6 +9,14 @@ const translations = {
9
9
  importXls: 'XLS'
10
10
  }
11
11
  },
12
+ section: {
13
+ actions: {
14
+ importCsv: 'CSV',
15
+ importXls: 'XLS',
16
+ importCsvTooltip: '{{section}} filtrelerini CSV dosyasından içe aktar',
17
+ importXlsTooltip: '{{section}} filtrelerini XLS dosyasından içe aktar'
18
+ }
19
+ },
12
20
  applied: {
13
21
  label: 'Aktif filtreler:',
14
22
  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