@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
package/README.md CHANGED
@@ -42,3 +42,309 @@
42
42
  8. **Testing & Quality Targets**
43
43
  - Cover all interactive flows (form entry, chip removal, modal toggles, persistence, conditional schema changes, CSV/XLS hooks) with Vitest and Testing Library, targeting ≥90% coverage without regressing existing suites.
44
44
  - Document sample integration snippets so partner apps can verify behavior during development.
45
+
46
+ ---
47
+
48
+ ## Section Fields
49
+
50
+ Fields declared with `type: 'section'` render as collapsible panels below the
51
+ main filter grid. Every field inside a section is part of the same form state as
52
+ the top-level filters, so section values are emitted through `onValuesChange`
53
+ and persisted to local storage exactly like regular filters.
54
+
55
+ ```tsx
56
+ const filterSchema = [
57
+ { key: 'sku', type: 'text', label: 'SKU', config: { visible: true } },
58
+ {
59
+ key: 'excludeSection',
60
+ type: 'section',
61
+ label: 'Exclude Filters',
62
+ isExcludeSection: true,
63
+ fields: [{ key: 'excludeSku', type: 'text', label: 'Exclude SKU' }]
64
+ }
65
+ ];
66
+ ```
67
+
68
+ ### Section flags
69
+
70
+ | Flag | Effect |
71
+ | ------------------- | ---------------------------------------------------------------------------------------- |
72
+ | `defaultExpanded` | `false` starts the panel collapsed. |
73
+ | `isExcludeSection` | Chips for the section's fields render with the exclude styling (red chip + exclude icon). |
74
+ | `selectableFields` | Adds a funnel button that opens a per-section visibility modal. |
75
+ | `enableImportCsv` | Adds a CSV import button to the section header. Requires `onSectionImportCsv`. |
76
+ | `enableImportXls` | Adds an XLS/XLSX import button to the section header. Requires `onSectionImportXls`. |
77
+
78
+ > Only Akifilter reads these flags. `isExcludeSection` and `selectableFields`
79
+ > are declared on `SectionField` in `@akinon/akiform-builder` for historical
80
+ > reasons and have builder setters of their own; `enableImportCsv` /
81
+ > `enableImportXls` are Akifilter's own props and reach a builder-made section
82
+ > through `field().extend({ enableImportCsv: true })`, which is where any new
83
+ > Akifilter-only prop belongs. See the note on `SectionField` in that package's
84
+ > `types.ts`.
85
+
86
+ ### Persistence
87
+
88
+ Section values are stored under the same `…:values` local-storage entry as the
89
+ top-level filters and rehydrated on the next mount, so a page refresh keeps
90
+ exclude filters filled in. Per-section visibility selections (`selectableFields`)
91
+ are stored separately under `…:section:<sectionKey>`.
92
+
93
+ The storage key is hashed from the **top-level** field signature only, so editing
94
+ a section's fields does not invalidate the entry (and existing entries survive an
95
+ upgrade). Stale values from an older schema are instead rejected on read: a
96
+ persisted value whose shape cannot fit its field's current `type`/`mode` is
97
+ dropped rather than restored — e.g. an array left behind by a
98
+ `select`/`multiple` field is not fed into a field that is now `text`. `file`
99
+ fields are never restored at all, since a `File` cannot survive JSON
100
+ serialisation.
101
+
102
+ ### Section-level CSV / XLS import
103
+
104
+ A section can expose its own file-import buttons in the collapse header — the
105
+ same affordance the top-level toolbar offers through `enableImportCsv` /
106
+ `onImportCsv`, and rendered conditionally in the same way as the
107
+ `selectableFields` funnel. This is typically used to exclude a large set of
108
+ records by uploading a file instead of typing values one by one.
109
+
110
+ A button renders only when **both** halves are present: the schema flag on the
111
+ section and the matching handler on the component. The handler receives the
112
+ section's `key` first, so a single handler can serve every section.
113
+
114
+ ```tsx
115
+ <Akifilter
116
+ filterSchema={[
117
+ {
118
+ key: 'excludeSection',
119
+ type: 'section',
120
+ label: 'Exclude Filters',
121
+ isExcludeSection: true,
122
+ enableImportCsv: true,
123
+ enableImportXls: true,
124
+ fields: [{ key: 'excludeSku', type: 'text', label: 'Exclude SKU' }]
125
+ }
126
+ ]}
127
+ onSectionImportCsv={(sectionKey, file) => uploadExcludeFile(sectionKey, file)}
128
+ onSectionImportXls={(sectionKey, file) => uploadExcludeFile(sectionKey, file)}
129
+ />
130
+ ```
131
+
132
+ Akifilter never reads or parses the file — it only surfaces the picker and hands
133
+ the `File` to the host application. To show the upload as an active filter, feed
134
+ it back through `externalAppliedFilters` (set `isExclude: true` on the chip to
135
+ match the exclude styling) and clear it from `onRemoveExternalFilter`.
136
+
137
+ Import buttons combine freely with `selectableFields`: the funnel is rendered
138
+ after the file buttons inside the same header action group. Both mouse clicks and
139
+ Enter presses are stopped from bubbling out of that group, so activating a header
140
+ action never toggles the panel underneath it.
141
+
142
+ With the builder API:
143
+
144
+ ```ts
145
+ field()
146
+ .key('excludeSection')
147
+ .type('section')
148
+ .label('Exclude Filters')
149
+ .isExcludeSection(true)
150
+ // Akifilter's own flags travel through `extend()`; `isExcludeSection` and
151
+ // `selectableFields` predate it and keep their setters.
152
+ .extend({ enableImportCsv: true, enableImportXls: true })
153
+ .fields([field().key('excludeSku').type('text').label('Exclude SKU').build()])
154
+ .build();
155
+ ```
156
+
157
+ ### Localization
158
+
159
+ Section header action copy lives under the `section.actions` namespace
160
+ (`importCsv`, `importXls`, `importCsvTooltip`, `importXlsTooltip`). The tooltip
161
+ keys interpolate `{{section}}` with the section's label.
162
+
163
+ ---
164
+
165
+ ## Field Labels
166
+
167
+ A field's `label` drives its form input. Two optional props override it for one
168
+ surface each, so the chip and the picker can say something different from the
169
+ input:
170
+
171
+ | Prop | Overrides | Falls back to |
172
+ | ----------------- | ---------------------------------------------- | ------------- |
173
+ | `appliedLabel` | The applied-filter chip (and its remove button's accessible name). | `label` → `placeholder` → `key` |
174
+ | `visibilityLabel` | The field's row in the "Select visible filters" modal. | `label` → `placeholder` → `key` |
175
+
176
+ ```tsx
177
+ const filterSchema: AkifilterSchema = [
178
+ {
179
+ key: 'orderNo',
180
+ type: 'text',
181
+ label: 'Order No', // form input
182
+ appliedLabel: 'Order number', // chip: "Order number: 1001"
183
+ visibilityLabel: 'Order reference no' // modal row
184
+ },
185
+ {
186
+ key: 'excludeSection',
187
+ type: 'section',
188
+ label: 'Exclude Filters',
189
+ isExcludeSection: true,
190
+ fields: [
191
+ {
192
+ key: 'excludeStatus',
193
+ type: 'text',
194
+ label: 'Status',
195
+ // The chip is rendered outside the section, so it carries the context
196
+ // the section header would otherwise provide.
197
+ appliedLabel: 'Excluded status'
198
+ }
199
+ ]
200
+ }
201
+ ];
202
+ ```
203
+
204
+ Notes:
205
+
206
+ - Both props are optional. A field that sets neither behaves exactly as before,
207
+ so adding them to one field never affects the rest of the schema.
208
+ - Section children support them too, including inside exclude sections.
209
+ - The visibility modal's search box matches `visibilityLabel`, `label`,
210
+ `placeholder` and `key`, so an overridden field stays findable under either
211
+ name.
212
+ - Neither prop affects the form input's own label or its accessible name — use
213
+ `label` for that.
214
+ - Storage is keyed by field `key`, never by any label, so renaming or
215
+ overriding labels does not invalidate persisted values or visibility
216
+ selections.
217
+ - They are Akifilter's own props, not part of the shared form schema. With the
218
+ fluent builder they travel through its generic `extend()` hatch:
219
+
220
+ ```tsx
221
+ import { field } from '@akinon/akiform-builder';
222
+
223
+ field()
224
+ .key('orderNo')
225
+ .type('text')
226
+ .label('Order No')
227
+ .extend({ appliedLabel: 'Order number' })
228
+ .build();
229
+ ```
230
+
231
+ The call is type-checked even though the builder knows nothing about filters:
232
+ Akifilter declares its props on the builder's `FieldExtensions` interface, and
233
+ `extend()` accepts *only* declared keys — a typo or a wrong type is a compile
234
+ error, and an application that wants to hang its own metadata on a field
235
+ declares it the same way:
236
+
237
+ ```ts
238
+ declare module '@akinon/akiform-builder' {
239
+ interface FieldExtensions {
240
+ requestKey?: string;
241
+ }
242
+ }
243
+ ```
244
+
245
+ `AkiformBuilder` ignores all of it — it renders the form label from `label`
246
+ alone.
247
+
248
+ See the `With Distinct Labels` Storybook story for a live example.
249
+
250
+ ---
251
+
252
+ ## Multi-Value Filters and Switchable Filter Types
253
+
254
+ ### Tags input out of the box
255
+
256
+ A `select` field with `mode: 'tags'` and no options is a free-form multi-value
257
+ input: type a value, press Enter to turn it into a removable tag, repeat. The
258
+ applied chip joins the entries with `, `.
259
+
260
+ ```tsx
261
+ {
262
+ key: 'keywords',
263
+ type: 'select',
264
+ label: 'Keywords',
265
+ mode: 'tags',
266
+ options: []
267
+ }
268
+ ```
269
+
270
+ `mode` is part of the storage signature, so switching a field between single
271
+ and multi-value modes invalidates its persisted entry instead of restoring an
272
+ incompatible shape.
273
+
274
+ ### One field, several chips (`appliedChips`)
275
+
276
+ By default a field contributes one applied-filter chip. A field that owns
277
+ several filters at once — a bucket per filter type, a range that reads better as
278
+ two chips — can produce its own instead:
279
+
280
+ ```tsx
281
+ {
282
+ key: 'attributeName',
283
+ type: 'custom',
284
+ label: 'Attribute Name',
285
+ // Value shape is the field's own business, e.g.
286
+ // { mode: 'exact', buckets: { contains: ['a', 'b'], exact: ['c'] } }
287
+ appliedChips: ({ field, value, formValues }) => [
288
+ { id: 'contains', label: 'Attribute Name (Contains)', value: 'a, b' },
289
+ { id: 'exact', label: 'Attribute Name (Exact match)', value: 'c' }
290
+ ],
291
+ // Return the value the field should hold after the chip is removed,
292
+ // or `null` to clear the field.
293
+ onRemoveAppliedChip: ({ field, value, id }) => dropBucket(value, id),
294
+ render: ({ control }) => <MyInput control={control} />
295
+ }
296
+ ```
297
+
298
+ | Prop | Behaviour |
299
+ | ---- | --------- |
300
+ | `appliedChips` | Replaces the field's single chip with one chip per returned entry. Return `[]` for "no chips right now". Each entry is `{ id, value, label?, isExclude? }`; `label` defaults to the field's applied label, `isExclude` to whether the field sits in an exclude section. |
301
+ | `onRemoveAppliedChip` | Maps one chip's × back onto a new field value, which Akifilter commits, persists and emits. Without it the chips render but cannot be removed. |
302
+
303
+ `id` must be stable for the same logical entry and unique within the field. Chip
304
+ keys are `<field key>::<chip id>` internally and are split at the first
305
+ separator, so an id may contain `::` — a field key may not.
306
+
307
+ `appliedChips` is only called while the field's own value is non-empty — the
308
+ emptiness rules that suppress a plain chip (`null`/`undefined`, a blank string,
309
+ an empty array) suppress these too. Use `formValues` to *format* the chips (a
310
+ unit or a mode held by a sibling field), not to derive them from a sibling while
311
+ this field holds nothing.
312
+
313
+ With the fluent builder both props travel the same route as the label
314
+ overrides: `field().…​.extend({ appliedChips, onRemoveAppliedChip })`.
315
+
316
+ Because every chip is derived from the field's **form value**, persistence comes
317
+ for free: Akifilter stores and rehydrates the composite value like any other
318
+ one, and `Clear all` clears the field like any other. Nothing has to be mirrored
319
+ into host state or written to storage by the application.
320
+
321
+ Declaring `appliedChips` also tells the storage layer that the field owns its
322
+ value shape, so a composite (object) value is rehydrated instead of being
323
+ rejected as schema drift — the same exemption `type: 'custom'` gets. Without the
324
+ prop, only `custom` fields may hold object values across a reload.
325
+
326
+ ### A filter whose type the user can switch
327
+
328
+ Some filters carry a *type* next to their value — contains / exact match /
329
+ starts with — where each type emits under its own request key and several types
330
+ can be active at the same time. The `With Tag Type Selector` Storybook story
331
+ implements the whole flow on top of the pieces above:
332
+
333
+ | Concern | Mechanism |
334
+ | ------- | --------- |
335
+ | The input (tags + the type switcher on its right) | A `custom` field. Bind it with `useController({ control: renderProps.control, name })`. |
336
+ | Where the state lives | One form value holding a bucket per type plus the active type — so Akifilter persists and rehydrates all of it, active type included. |
337
+ | One chip per type, each with its own label | `appliedChips`. |
338
+ | Removing a single type | `onRemoveAppliedChip`, returning the value without that bucket (`null` when the last one goes). |
339
+ | Mapping buckets onto request keys | Host logic, since one logical filter can emit several keys at once. |
340
+
341
+ The flow the story demonstrates:
342
+
343
+ 1. With `Contains` active, entering `filter a` and `filter b` produces
344
+ `Attribute Name (Contains): filter a, filter b`.
345
+ 2. Switching the type leaves that bucket with its own chip and shows the new
346
+ type's (empty) bucket in the input.
347
+ 3. Entering `filter c` adds `Attribute Name (Exact match): filter c`, and the
348
+ payload carries both request keys.
349
+ 4. Switching back shows the earlier bucket in the input again.
350
+ 5. A page refresh restores every bucket **and** the active type.
@@ -71,6 +71,25 @@ export type AkifilterProps<TFieldValues extends AkifilterFieldValues = Akifilter
71
71
  * Notified whenever the set of active filter buttons changes.
72
72
  */
73
73
  onFilterButtonsChange?: (activeButtons: AkifilterButton[]) => void;
74
+ /**
75
+ * Triggered when a user picks a CSV file from a section header's import
76
+ * button. Receives the section's `key` and the selected file, so one handler
77
+ * can serve every section.
78
+ *
79
+ * The button only renders for sections that set `enableImportCsv: true` —
80
+ * mirroring how the top-level toolbar pairs `enableImportCsv` with
81
+ * `onImportCsv`. Akifilter never parses the file; the host application
82
+ * decides what to do with it (e.g. call an exclude-by-file endpoint and
83
+ * surface a chip through `externalAppliedFilters`).
84
+ */
85
+ onSectionImportCsv?: (sectionKey: string, file: File) => void;
86
+ /**
87
+ * Triggered when a user picks an XLS/XLSX file from a section header's import
88
+ * button. Receives the section's `key` and the selected file.
89
+ *
90
+ * The button only renders for sections that set `enableImportXls: true`.
91
+ */
92
+ onSectionImportXls?: (sectionKey: string, file: File) => void;
74
93
  };
75
94
  export declare const Akifilter: {
76
95
  <TFieldValues extends AkifilterFieldValues = FieldValues>(props: AkifilterProps<TFieldValues>): React.JSX.Element;
@@ -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;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"}
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;AAYjB,KAAK,oBAAoB,GAAG,WAAW,CAAC;AAiLxC,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;IACnE;;;;;;;;;;OAUG;IACH,kBAAkB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAC9D;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;CAC/D,CAAC;AAk9BF,eAAO,MAAM,SAAS;KACpB,YAAY,SAAS,oBAAoB,uBAElC,cAAc,CAAC,YAAY,CAAC;;CAoBpC,CAAC"}
@@ -48,6 +48,12 @@ const values_1 = require("./utils/values");
48
48
  * routed back to the owning button.
49
49
  */
50
50
  const BUTTON_CHIP_PREFIX = 'akifilter-button:';
51
+ // Chips a field produced through `appliedChips`. The key carries both halves —
52
+ // `<field key>::<chip id>` — so a removal can be routed back to the field that
53
+ // owns it. Split at the *first* separator, so a chip id may contain one; a
54
+ // field key may not, which holds for form paths.
55
+ const FIELD_CHIP_PREFIX = 'akifilter-field:';
56
+ const FIELD_CHIP_SEPARATOR = '::';
51
57
  const FilterFormItem = (_a) => {
52
58
  var _b, _c;
53
59
  var { children, control, name, valuePropName } = _a, props = __rest(_a, ["children", "control", "name", "valuePropName"]);
@@ -128,7 +134,7 @@ const resolveClearedFieldValue = (field, defaultValue) => {
128
134
  return undefined;
129
135
  }
130
136
  };
131
- const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onValuesChange, onVisibleFieldsChange, onImportCsv, onImportXls, onClearAll, enableImportCsv, enableImportXls, filterActionsRef, externalAppliedFilters, onRemoveExternalFilter, filterButtons, onFilterButtonsChange }) => {
137
+ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onValuesChange, onVisibleFieldsChange, onImportCsv, onImportXls, onClearAll, enableImportCsv, enableImportXls, filterActionsRef, externalAppliedFilters, onRemoveExternalFilter, filterButtons, onFilterButtonsChange, onSectionImportCsv, onSectionImportXls }) => {
132
138
  // Separate regular fields from section fields
133
139
  const { regularFields, sectionFields } = react_1.default.useMemo(() => (0, schema_1.partitionSchema)(filterSchema), [filterSchema]);
134
140
  const excludeSectionKeys = react_1.default.useMemo(() => (0, schema_1.deriveExcludeSectionKeys)(sectionFields), [sectionFields]);
@@ -138,16 +144,19 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
138
144
  const schemaDefaults = react_1.default.useMemo(() => (0, schema_1.normaliseValuesBySchema)(flattenedSchema, (0, schema_1.extractDefaultValues)(flattenedSchema)), [flattenedSchema]);
139
145
  const externalDefaults = react_1.default.useMemo(() => (0, schema_1.normaliseValuesBySchema)(flattenedSchema, defaultValues), [flattenedSchema, defaultValues]);
140
146
  const baseDefaultValues = react_1.default.useMemo(() => (Object.assign(Object.assign({}, schemaDefaults), externalDefaults)), [schemaDefaults, externalDefaults]);
141
- // Use state for persistedDefaults to make it reactive to localStorage changes
147
+ // Use state for persistedDefaults to make it reactive to localStorage changes.
148
+ // Read against the *flattened* schema: `readStoredValues` drops keys that are
149
+ // not in the schema it is given, so passing only the top-level fields would
150
+ // silently discard every section field's persisted value on reload.
142
151
  const [persistedDefaults, setPersistedDefaults] = react_1.default.useState(() => {
143
152
  var _a;
144
- return (0, schema_1.normaliseValuesBySchema)(flattenedSchema, (_a = (0, storage_1.readStoredValues)(regularFields, storageKey)) !== null && _a !== void 0 ? _a : undefined);
153
+ return (0, schema_1.normaliseValuesBySchema)(flattenedSchema, (_a = (0, storage_1.readStoredValues)(flattenedSchema, storageKey)) !== null && _a !== void 0 ? _a : undefined);
145
154
  });
146
155
  // Re-read from localStorage when storageKey changes
147
156
  react_1.default.useEffect(() => {
148
157
  var _a;
149
- setPersistedDefaults((0, schema_1.normaliseValuesBySchema)(flattenedSchema, (_a = (0, storage_1.readStoredValues)(regularFields, storageKey)) !== null && _a !== void 0 ? _a : undefined));
150
- }, [flattenedSchema, regularFields, storageKey]);
158
+ setPersistedDefaults((0, schema_1.normaliseValuesBySchema)(flattenedSchema, (_a = (0, storage_1.readStoredValues)(flattenedSchema, storageKey)) !== null && _a !== void 0 ? _a : undefined));
159
+ }, [flattenedSchema, storageKey]);
151
160
  const mergedDefaultValues = react_1.default.useMemo(() => (Object.assign(Object.assign({}, baseDefaultValues), persistedDefaults)), [baseDefaultValues, persistedDefaults]);
152
161
  const formMethods = (0, akiform_1.useForm)({
153
162
  defaultValues: mergedDefaultValues
@@ -200,7 +209,23 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
200
209
  if (field.type === constants_1.FIELD_TYPES.CHECKBOX && currentValue !== true) {
201
210
  return acc;
202
211
  }
203
- const label = field.label || field.placeholder || key;
212
+ const label = (0, schema_1.getAppliedLabel)(field);
213
+ // A field that owns several filters renders its own chips: only it knows
214
+ // how to split its value and how to format each part.
215
+ if (typeof field.appliedChips === 'function') {
216
+ field
217
+ .appliedChips({ field, value: currentValue, formValues })
218
+ .forEach(chip => {
219
+ var _a, _b;
220
+ acc.push({
221
+ key: `${FIELD_CHIP_PREFIX}${key}${FIELD_CHIP_SEPARATOR}${chip.id}`,
222
+ label: (_a = chip.label) !== null && _a !== void 0 ? _a : label,
223
+ value: chip.value,
224
+ isExclude: (_b = chip.isExclude) !== null && _b !== void 0 ? _b : excludeSectionKeys.has(key)
225
+ });
226
+ });
227
+ return acc;
228
+ }
204
229
  const fieldOptions = 'options' in field && field.options ? field.options : undefined;
205
230
  const resolveLabel = (val) => {
206
231
  var _a;
@@ -418,17 +443,14 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
418
443
  emitValues,
419
444
  persistValues
420
445
  ]);
421
- const handleRemoveFilter = react_1.default.useCallback((key) => {
422
- const schemaField = flattenedSchema.find(field => String(field.key) === key);
423
- if (!schemaField) {
424
- return;
425
- }
426
- const defaultValue = schemaDefaults[String(schemaField.key)];
427
- const nextValue = resolveClearedFieldValue(schemaField, defaultValue);
446
+ // Writes one field's value and pushes the result through the whole
447
+ // persist/emit path immediately, bypassing the debounce the way a removal
448
+ // has to. Shared by chip removals — whole-field and per-chip alike.
449
+ const commitFieldValue = react_1.default.useCallback((fieldKey, nextValue) => {
428
450
  // Manually construct the updated form values instead of relying on setValue + getValues
429
451
  // because setValue is batched and getValues() wouldn't reflect the change immediately
430
452
  const currentFormValues = formMethods.getValues();
431
- const updatedFormValues = Object.assign(Object.assign({}, currentFormValues), { [String(schemaField.key)]: nextValue });
453
+ const updatedFormValues = Object.assign(Object.assign({}, currentFormValues), { [fieldKey]: nextValue });
432
454
  // Normalize the values (removes empty/undefined entries)
433
455
  const nextValues = (0, values_1.normaliseOutputValues)(flattenedSchema, updatedFormValues);
434
456
  // updatedFormValues holds an explicit cleared value (e.g. `null` for custom/file
@@ -450,7 +472,36 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
450
472
  hasInitialValuesRef.current = Object.keys(nextValues).length > 0;
451
473
  // Emit the change to parent
452
474
  emitValues(nextValues);
453
- }, [schemaDefaults, flattenedSchema, formMethods, emitValues, persistValues]);
475
+ }, [flattenedSchema, formMethods, emitValues, persistValues]);
476
+ const handleRemoveFilter = react_1.default.useCallback((key) => {
477
+ const schemaField = flattenedSchema.find(field => String(field.key) === key);
478
+ if (!schemaField) {
479
+ return;
480
+ }
481
+ const defaultValue = schemaDefaults[String(schemaField.key)];
482
+ commitFieldValue(String(schemaField.key), resolveClearedFieldValue(schemaField, defaultValue));
483
+ }, [schemaDefaults, flattenedSchema, commitFieldValue]);
484
+ // Removal of a single chip produced by a field's own `appliedChips`. The
485
+ // field decides what its value becomes; Akifilter just commits it.
486
+ const handleFieldChipRemove = react_1.default.useCallback((chipKey) => {
487
+ const separatorIndex = chipKey.indexOf(FIELD_CHIP_SEPARATOR);
488
+ if (separatorIndex === -1) {
489
+ return;
490
+ }
491
+ const fieldKey = chipKey.slice(0, separatorIndex);
492
+ const chipId = chipKey.slice(separatorIndex + FIELD_CHIP_SEPARATOR.length);
493
+ const schemaField = flattenedSchema.find(field => String(field.key) === fieldKey);
494
+ if (!schemaField ||
495
+ typeof schemaField.onRemoveAppliedChip !== 'function') {
496
+ return;
497
+ }
498
+ const currentValue = formMethods.getValues()[fieldKey];
499
+ commitFieldValue(fieldKey, schemaField.onRemoveAppliedChip({
500
+ field: schemaField,
501
+ value: currentValue,
502
+ id: chipId
503
+ }));
504
+ }, [flattenedSchema, formMethods, commitFieldValue]);
454
505
  // Toggle a filter button. Button state lives outside the form, so this syncs
455
506
  // the value ref, persists, and emits immediately (bypassing the form debounce).
456
507
  const handleButtonToggle = react_1.default.useCallback((button) => {
@@ -514,6 +565,9 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
514
565
  if (key.startsWith(BUTTON_CHIP_PREFIX)) {
515
566
  handleButtonRemove(key.slice(BUTTON_CHIP_PREFIX.length));
516
567
  }
568
+ else if (key.startsWith(FIELD_CHIP_PREFIX)) {
569
+ handleFieldChipRemove(key.slice(FIELD_CHIP_PREFIX.length));
570
+ }
517
571
  else if (externalAppliedFilters === null || externalAppliedFilters === void 0 ? void 0 : externalAppliedFilters.some(f => f.key === key)) {
518
572
  onRemoveExternalFilter === null || onRemoveExternalFilter === void 0 ? void 0 : onRemoveExternalFilter(key);
519
573
  }
@@ -524,7 +578,8 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
524
578
  externalAppliedFilters,
525
579
  onRemoveExternalFilter,
526
580
  handleRemoveFilter,
527
- handleButtonRemove
581
+ handleButtonRemove,
582
+ handleFieldChipRemove
528
583
  ]);
529
584
  const handleClearAllApplied = react_1.default.useCallback(() => {
530
585
  // Reset filter buttons before clearing the form so the merged emission in
@@ -599,7 +654,9 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
599
654
  react_1.default.createElement("div", { className: "akinon-filter__form-grid", "data-testid": "akifilter-form-grid" },
600
655
  visibleFields.map(renderFormField),
601
656
  visibleFields.length === 0 && regularFields.length > 0 ? (react_1.default.createElement("div", { className: "akinon-filter__empty" }, i18n_1.i18n.t('form.noVisibleFields'))) : null),
602
- sectionFields.length > 0 && (react_1.default.createElement("div", { className: "akinon-filter__section-fields" }, sectionFields.map(section => (react_1.default.createElement(section_fieldset_1.SectionFieldset, { key: String(section.key), section: section, storageKey: storageKey, renderFormField: renderFormField, onRemoveField: handleRemoveFilter, formValues: normalisedValues }))))))),
657
+ sectionFields.length > 0 && (react_1.default.createElement("div", { className: "akinon-filter__section-fields" }, sectionFields.map(section => (react_1.default.createElement(section_fieldset_1.SectionFieldset, { key: String(section.key), section: section, storageKey: storageKey, renderFormField: renderFormField, onRemoveField: handleRemoveFilter, formValues: normalisedValues, onImportCsv: onSectionImportCsv &&
658
+ (file => onSectionImportCsv(String(section.key), file)), onImportXls: onSectionImportXls &&
659
+ (file => onSectionImportXls(String(section.key), file)) }))))))),
603
660
  react_1.default.createElement(visibility_modal_1.VisibilityModal, Object.assign({}, modalProps)))));
604
661
  };
605
662
  const AkifilterEmptyState = () => {
@@ -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,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"}
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../../src/common/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAInD,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;AA0DF,eAAO,MAAM,gBAAgB,GAAI,YAAY,SAAS,WAAW,EAC/D,QAAQ,cAAc,CAAC,YAAY,CAAC,EAAE,EACtC,YAAY,MAAM,KACjB,OAAO,CAAC,YAAY,CAAC,GAAG,IAqC1B,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,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.clearStoredButtonValues = exports.writeStoredButtonValues = exports.readStoredButtonValues = exports.clearStoredValues = exports.writeStoredValues = exports.readStoredValues = exports.clearVisibleKeys = exports.buildSectionVisibilityKey = exports.writeVisibleKeys = exports.readVisibleKeys = exports.buildStorageKey = void 0;
4
+ const constants_1 = require("../constants");
4
5
  const i18n_1 = require("../i18n");
5
6
  const STORAGE_PREFIX = 'akifilter:v1';
6
7
  const VALUES_SUFFIX = ':values';
@@ -76,6 +77,46 @@ const clearVisibleKeys = (storageKey) => {
76
77
  };
77
78
  exports.clearVisibleKeys = clearVisibleKeys;
78
79
  const buildValuesStorageKey = (storageKey) => `${storageKey}${VALUES_SUFFIX}`;
80
+ /**
81
+ * Guards a persisted value against schema drift.
82
+ *
83
+ * The storage key is hashed from the top-level field signature only, so a
84
+ * section field whose `type`/`mode` changed between releases keeps the same key
85
+ * and its stale value would otherwise be restored into an incompatible widget
86
+ * (e.g. a multi-select's array landing in a text input, which then renders
87
+ * `a,b` and is re-emitted as an array to a host expecting a string).
88
+ *
89
+ * Deliberately conservative: it only rejects shapes that no widget of that type
90
+ * can hold, and never polices string-vs-number.
91
+ */
92
+ const isStoredValueCompatible = (field, value) => {
93
+ // `File` objects stringify to `{}`, so a persisted file value is always junk.
94
+ if (field.type === constants_1.FIELD_TYPES.FILE) {
95
+ return false;
96
+ }
97
+ // Custom fields own their value shape; the package cannot judge it. So does
98
+ // any field that renders its own chips: `appliedChips` means the value is a
99
+ // composite only that field interprets, whatever the field's type is.
100
+ if (field.type === constants_1.FIELD_TYPES.CUSTOM ||
101
+ typeof field.appliedChips === 'function') {
102
+ return true;
103
+ }
104
+ const isMultiSelect = field.type === constants_1.FIELD_TYPES.SELECT &&
105
+ (field.mode === constants_1.FIELD_MODES.MULTIPLE || field.mode === constants_1.FIELD_MODES.TAGS);
106
+ if (Array.isArray(value)) {
107
+ // Only multi-selects hold arrays. A bare scalar is still fine for them —
108
+ // `normaliseValuesBySchema` wraps it.
109
+ return isMultiSelect;
110
+ }
111
+ // Nothing else round-trips through a filter widget as a plain object.
112
+ if (value !== null && typeof value === 'object') {
113
+ return false;
114
+ }
115
+ if (field.type === constants_1.FIELD_TYPES.CHECKBOX) {
116
+ return typeof value === 'boolean';
117
+ }
118
+ return true;
119
+ };
79
120
  const readStoredValues = (schema, storageKey) => {
80
121
  if (!isBrowser)
81
122
  return null;
@@ -88,9 +129,13 @@ const readStoredValues = (schema, storageKey) => {
88
129
  if (!parsed || typeof parsed !== 'object') {
89
130
  return null;
90
131
  }
91
- const schemaKeys = new Set(schema.map(field => String(field.key)));
132
+ const fieldsByKey = new Map(schema.map(field => [String(field.key), field]));
92
133
  return Object.entries(parsed).reduce((acc, [key, value]) => {
93
- if (schemaKeys.has(String(key))) {
134
+ const field = fieldsByKey.get(String(key));
135
+ // Unknown keys belong to a different schema version; values whose shape no
136
+ // longer fits their field are stale leftovers of one (see
137
+ // `isStoredValueCompatible`).
138
+ if (field && isStoredValueCompatible(field, value)) {
94
139
  acc[key] =
95
140
  value;
96
141
  }
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ export type FileImportButtonProps = {
3
+ /**
4
+ * Accepted file extensions, forwarded to the hidden file input
5
+ * (e.g. `'.csv'`, `'.xls,.xlsx'`).
6
+ */
7
+ accept: string;
8
+ /**
9
+ * Text rendered inside the button (e.g. `'CSV'`).
10
+ */
11
+ label: string;
12
+ /**
13
+ * Tooltip / accessible name. Falls back to `label` when omitted, which is
14
+ * what the top-level toolbar relies on.
15
+ */
16
+ title?: string;
17
+ /**
18
+ * Called with the picked file. The component never uploads anything itself;
19
+ * the host application decides what to do with the file.
20
+ */
21
+ onImport: (file: File) => void;
22
+ /**
23
+ * Extra class names appended to the button.
24
+ */
25
+ className?: string;
26
+ };
27
+ /**
28
+ * Button that opens the native file picker and hands the selected file to
29
+ * `onImport`. Shared by the top-level filter toolbar and the section headers so
30
+ * both surfaces behave identically.
31
+ */
32
+ export declare const FileImportButton: ({ accept, label, title, onImport, className }: FileImportButtonProps) => React.JSX.Element;
33
+ //# sourceMappingURL=file-import-button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-import-button.d.ts","sourceRoot":"","sources":["../../../src/components/file-import-button.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,MAAM,qBAAqB,GAAG;IAClC;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IAC/B;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAAI,+CAM9B,qBAAqB,sBA6CvB,CAAC"}