@drivenets/design-system 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/dist/index.cjs +1652 -455
- package/dist/index.d.cts +1226 -989
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1226 -989
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1643 -456
- package/dist/index.js.map +1 -1
- package/dist/index.min.css +1 -1
- package/package.json +31 -31
- package/src/components/ds-catalog-layout/catalog-layout-empty-illustration.tsx +57 -0
- package/src/components/ds-catalog-layout/ds-catalog-layout.context.ts +19 -0
- package/src/components/ds-catalog-layout/ds-catalog-layout.module.scss +167 -0
- package/src/components/ds-catalog-layout/ds-catalog-layout.tsx +130 -0
- package/src/components/ds-catalog-layout/ds-catalog-layout.types.ts +62 -0
- package/src/components/ds-catalog-layout/index.ts +9 -0
- package/src/components/ds-date-input/ds-date-input.utils.ts +2 -2
- package/src/components/ds-drawer/ds-drawer.tsx +2 -0
- package/src/components/ds-form-control/ds-form-control.tsx +14 -11
- package/src/components/ds-form-control/ds-form-control.types.ts +4 -0
- package/src/components/ds-grid/ds-grid.tsx +1 -2
- package/src/components/ds-modal/ds-modal.tsx +3 -0
- package/src/components/ds-smart-tabs/ds-smart-tabs.module.scss +1 -0
- package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.module.scss +21 -2
- package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.tsx +14 -11
- package/src/components/ds-table/components/ds-table-body-virtualized/ds-table-body-virtualized.types.ts +1 -6
- package/src/components/ds-table/components/ds-table-bulk-actions/ds-table-bulk-actions.module.scss +2 -2
- package/src/components/ds-table/components/ds-table-cell/ds-table-cell.module.scss +2 -0
- package/src/components/ds-table/components/ds-table-cell/ds-table-cell.tsx +42 -11
- package/src/components/ds-table/components/ds-table-header/ds-table-header.module.scss +24 -4
- package/src/components/ds-table/components/ds-table-header/ds-table-header.tsx +22 -1
- package/src/components/ds-table/components/ds-table-row/ds-table-row.module.scss +22 -0
- package/src/components/ds-table/components/ds-table-row/ds-table-row.tsx +1 -0
- package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.module.scss +19 -0
- package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.tsx +2 -1
- package/src/components/ds-table/components/ds-table-row-virtualized/ds-table-row-virtualized.types.ts +1 -1
- package/src/components/ds-table/components/edit/cell-editors/cell-editors.module.scss +3 -0
- package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-checkbox.tsx +27 -0
- package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-date.tsx +50 -0
- package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-number.tsx +51 -0
- package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-select.tsx +36 -0
- package/src/components/ds-table/components/edit/cell-editors/ds-table-edit-cell-text.tsx +50 -0
- package/src/components/ds-table/components/edit/cell-editors/index.ts +14 -0
- package/src/components/ds-table/components/edit/cell-editors/table-edit-form-control.tsx +31 -0
- package/src/components/ds-table/components/edit/ds-table-editable-cell/ds-table-editable-cell.module.scss +28 -0
- package/src/components/ds-table/components/edit/ds-table-editable-cell/ds-table-editable-cell.tsx +39 -0
- package/src/components/ds-table/components/edit/ds-table-editable-cell/ds-table-editable-cell.types.ts +7 -0
- package/src/components/ds-table/components/edit/ds-table-editable-cell/index.ts +2 -0
- package/src/components/ds-table/components/edit/ds-table-editing-cell/ds-table-editing-cell.module.scss +48 -0
- package/src/components/ds-table/components/edit/ds-table-editing-cell/ds-table-editing-cell.tsx +76 -0
- package/src/components/ds-table/components/edit/ds-table-editing-cell/ds-table-editing-cell.types.ts +5 -0
- package/src/components/ds-table/components/edit/ds-table-editing-cell/index.ts +2 -0
- package/src/components/ds-table/context/ds-table-context.ts +79 -1
- package/src/components/ds-table/ds-table-column.types.ts +12 -0
- package/src/components/ds-table/ds-table.module.scss +100 -10
- package/src/components/ds-table/ds-table.tsx +18 -19
- package/src/components/ds-table/ds-table.types.ts +52 -0
- package/src/components/ds-table/filters/adapters/create-checkbox-filter-adapter.tsx +24 -40
- package/src/components/ds-table/filters/adapters/create-dual-range-filter-adapter.tsx +10 -31
- package/src/components/ds-table/filters/adapters/create-filter-adapter.ts +6 -98
- package/src/components/ds-table/filters/components/checkbox-filter/checkbox-filter.module.scss +36 -0
- package/src/components/ds-table/filters/components/checkbox-filter/checkbox-filter.tsx +92 -13
- package/src/components/ds-table/filters/components/checkbox-filter/index.ts +1 -1
- package/src/components/ds-table/filters/components/column-filter-popover/column-filter-popover.module.scss +42 -0
- package/src/components/ds-table/filters/components/column-filter-popover/column-filter-popover.tsx +89 -0
- package/src/components/ds-table/filters/components/column-filter-popover/column-filter-popover.types.ts +25 -0
- package/src/components/ds-table/filters/components/column-filter-popover/index.ts +5 -0
- package/src/components/ds-table/filters/hooks/use-table-filters.ts +113 -78
- package/src/components/ds-table/filters/index.ts +2 -44
- package/src/components/ds-table/filters/types/column-meta.augmentation.ts +19 -0
- package/src/components/ds-table/filters/types/filter-adapter.types.ts +48 -81
- package/src/components/ds-table/hooks/use-cell-editor.ts +81 -0
- package/src/components/ds-table/index.ts +29 -0
- package/src/components/ds-table/utils/column-size.ts +9 -8
- package/src/components/ds-tag/ds-tag.tsx +2 -2
- package/src/components/ds-workspace-layout/ds-workspace-layout.context.ts +19 -0
- package/src/components/ds-workspace-layout/ds-workspace-layout.module.scss +166 -0
- package/src/components/ds-workspace-layout/ds-workspace-layout.tsx +135 -0
- package/src/components/ds-workspace-layout/ds-workspace-layout.types.ts +52 -0
- package/src/components/ds-workspace-layout/index.ts +11 -0
- package/src/index.ts +2 -1
- package/src/styles/_scrollbars.scss +37 -0
- package/src/utils/restore-document-body-modal-styles.ts +44 -0
- package/src/utils/use-ark-dialog-body-lock-cleanup.ts +36 -0
- package/src/components/ds-workspace/ds-workspace.module.scss +0 -55
- package/src/components/ds-workspace/ds-workspace.tsx +0 -37
- package/src/components/ds-workspace/ds-workspace.types.ts +0 -17
- package/src/components/ds-workspace/index.ts +0 -8
|
@@ -1,107 +1,77 @@
|
|
|
1
1
|
import { type ReactNode, useState } from 'react';
|
|
2
2
|
import type { CellContext, ColumnDef } from '@tanstack/react-table';
|
|
3
|
-
import type {
|
|
3
|
+
import type { TagFilterItem } from '../../../ds-tag-filter';
|
|
4
4
|
import type {
|
|
5
5
|
AnyAdapter,
|
|
6
|
+
ColumnFilterMeta,
|
|
6
7
|
ColumnFilterState,
|
|
7
8
|
FilterAdapter,
|
|
8
9
|
FilterNavItem,
|
|
9
10
|
FilterState,
|
|
11
|
+
ResolvedColumnFilter,
|
|
10
12
|
} from '../types/filter-adapter.types';
|
|
11
13
|
|
|
14
|
+
const resolveColumnAdapterId = (
|
|
15
|
+
meta: ColumnFilterMeta | ResolvedColumnFilter | undefined,
|
|
16
|
+
): string | undefined => {
|
|
17
|
+
if (!meta) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
if ('render' in meta) {
|
|
21
|
+
return meta.filterId;
|
|
22
|
+
}
|
|
23
|
+
if (meta.adapter) {
|
|
24
|
+
return meta.adapter.id;
|
|
25
|
+
}
|
|
26
|
+
return meta.adapterId;
|
|
27
|
+
};
|
|
28
|
+
|
|
12
29
|
export interface UseTableFiltersOptions<TData, TValue, TCellValue> {
|
|
13
|
-
/**
|
|
14
|
-
* Array of filter adapters that define filter behavior
|
|
15
|
-
*/
|
|
16
30
|
filterAdapters: FilterAdapter<TData, TValue, TCellValue>[] | AnyAdapter[];
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Base column definitions
|
|
20
|
-
*/
|
|
21
31
|
baseColumns?: ColumnDef<TData>[];
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Controlled mode: External applied filters (source of truth).
|
|
25
|
-
* When provided with onFiltersChange, the hook operates in controlled mode.
|
|
26
|
-
* Keys must match adapter IDs.
|
|
27
|
-
*/
|
|
32
|
+
/** Controlled applied filters, keyed by adapter id. Pair with `onFiltersChange`. */
|
|
28
33
|
appliedFilters?: FilterState<TValue>;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Controlled mode: Callback when filters change.
|
|
32
|
-
* Called by applyFilters(), deleteChip(), clearAll().
|
|
33
|
-
*/
|
|
34
|
+
/** Fires from `applyFilters`, `deleteChip`, and `clearAll` in controlled mode. */
|
|
34
35
|
onFiltersChange?: (filters: FilterState<TValue>) => void;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export interface UseTableFiltersResult<TData, TValue> {
|
|
38
|
-
/**
|
|
39
|
-
* Current draft filter state (uncommitted user selections)
|
|
40
|
-
*/
|
|
39
|
+
/** Draft filter state (uncommitted). */
|
|
41
40
|
filterState: FilterState<TValue>;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Column filters (derived from applied state)
|
|
45
|
-
*/
|
|
41
|
+
/** Applied filters mapped to TanStack's column-filters shape. */
|
|
46
42
|
columnFilters: ColumnFilterState<TValue>[];
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
*/
|
|
51
|
-
filterChips: ChipItem[];
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Filter navigation items with active counts
|
|
55
|
-
*/
|
|
43
|
+
/** Chips for `DsTagFilter`, derived from applied state. */
|
|
44
|
+
filterChips: TagFilterItem[];
|
|
45
|
+
/** Filter-nav items with active counts. */
|
|
56
46
|
filterNavItems: FilterNavItem[];
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Enhanced column definitions with filter functions
|
|
60
|
-
*/
|
|
47
|
+
/** `baseColumns` with `filterFn`, cell renderer and per-column meta wired in. */
|
|
61
48
|
enhancedColumns: ColumnDef<TData>[];
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Handlers for filter operations
|
|
65
|
-
*/
|
|
66
49
|
handlers: {
|
|
67
|
-
/** Update a
|
|
50
|
+
/** Update a filter's draft value. */
|
|
68
51
|
updateFilter: (filterId: string, value: TValue) => void;
|
|
69
|
-
/** Commit
|
|
52
|
+
/** Commit all drafts to applied state. */
|
|
70
53
|
applyFilters: () => void;
|
|
71
|
-
/** Clear all filters */
|
|
54
|
+
/** Clear all filters. */
|
|
72
55
|
clearAll: () => void;
|
|
73
|
-
/**
|
|
74
|
-
deleteChip: (chip:
|
|
56
|
+
/** Remove a chip's effect from applied state. */
|
|
57
|
+
deleteChip: (chip: TagFilterItem) => void;
|
|
58
|
+
/** Commit one column's draft (per-column popover Save). */
|
|
59
|
+
applyColumnFilter: (filterId: string) => void;
|
|
60
|
+
/** Discard one column's draft without committing. */
|
|
61
|
+
cancelColumnFilter: (filterId: string) => void;
|
|
62
|
+
/** Clear one applied filter (per-column popover Clear). */
|
|
63
|
+
clearColumnFilter: (filterId: string) => void;
|
|
75
64
|
};
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
*/
|
|
65
|
+
/** Renders the adapter UI for a column id, or `null` if no match. */
|
|
66
|
+
renderColumnFilter: (filterId: string) => ReactNode;
|
|
67
|
+
/** Renders the adapter UI for a `FilterNavItem`. */
|
|
80
68
|
renderFilterContent: (selectedFilter: FilterNavItem) => ReactNode;
|
|
81
69
|
}
|
|
82
70
|
|
|
83
71
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* **Uncontrolled mode** (default): Filters are managed internally.
|
|
88
|
-
* **Controlled mode**: Filters are managed externally via appliedFilters/onFiltersChange.
|
|
89
|
-
*
|
|
90
|
-
* @example
|
|
91
|
-
* // Uncontrolled mode
|
|
92
|
-
* const { filterChips, handlers } = useTableFilters({
|
|
93
|
-
* filterAdapters: adapters,
|
|
94
|
-
* baseColumns: columns,
|
|
95
|
-
* });
|
|
96
|
-
*
|
|
97
|
-
* @example
|
|
98
|
-
* // Controlled mode (e.g., URL-driven filtering)
|
|
99
|
-
* const { filterChips, handlers } = useTableFilters({
|
|
100
|
-
* filterAdapters: adapters,
|
|
101
|
-
* baseColumns: columns,
|
|
102
|
-
* appliedFilters: filtersFromUrl,
|
|
103
|
-
* onFiltersChange: (filters) => updateUrlAndRefetch(filters),
|
|
104
|
-
* });
|
|
72
|
+
* Orchestrates table filtering: drafts → applied state, chips, nav counts, and
|
|
73
|
+
* column wiring. Uncontrolled by default; pass `appliedFilters` +
|
|
74
|
+
* `onFiltersChange` for URL-driven / external state.
|
|
105
75
|
*/
|
|
106
76
|
export function useTableFilters<TData, TValue, TCellValue>({
|
|
107
77
|
filterAdapters,
|
|
@@ -109,7 +79,6 @@ export function useTableFilters<TData, TValue, TCellValue>({
|
|
|
109
79
|
appliedFilters: externalAppliedFilters,
|
|
110
80
|
onFiltersChange: setExternalAppliedFilters,
|
|
111
81
|
}: UseTableFiltersOptions<TData, TValue, TCellValue>): UseTableFiltersResult<TData, TValue> {
|
|
112
|
-
// Internal variable just to avoid assignment issues of `any`.
|
|
113
82
|
const _filterAdapters = filterAdapters as FilterAdapter<TData, TValue, TCellValue>[];
|
|
114
83
|
|
|
115
84
|
const isControlled = externalAppliedFilters !== undefined && setExternalAppliedFilters !== undefined;
|
|
@@ -148,18 +117,41 @@ export function useTableFilters<TData, TValue, TCellValue>({
|
|
|
148
117
|
const enhancedColumns: ColumnDef<TData>[] = !baseColumns
|
|
149
118
|
? []
|
|
150
119
|
: baseColumns.map((col) => {
|
|
151
|
-
const
|
|
120
|
+
const metaFilter = col.meta?.filter as ColumnFilterMeta | undefined;
|
|
121
|
+
const adapterIdFromMeta = resolveColumnAdapterId(metaFilter);
|
|
122
|
+
const adapter =
|
|
123
|
+
(adapterIdFromMeta && _filterAdapters.find((a) => a.id === adapterIdFromMeta)) ||
|
|
124
|
+
_filterAdapters.find((a) => a.id === col.id);
|
|
125
|
+
|
|
152
126
|
if (!adapter) {
|
|
153
127
|
return col;
|
|
154
128
|
}
|
|
155
129
|
|
|
156
130
|
const cellRenderer = adapter.cellRenderer;
|
|
131
|
+
const appliedValue = appliedFilters[adapter.id];
|
|
132
|
+
const resolvedFilter: ResolvedColumnFilter | undefined = metaFilter
|
|
133
|
+
? {
|
|
134
|
+
filterId: adapter.id,
|
|
135
|
+
hasActiveFilter: appliedValue !== undefined && adapter.getActiveFiltersCount(appliedValue) > 0,
|
|
136
|
+
render: () => renderForId(adapter.id),
|
|
137
|
+
onApply: () => applyColumnFilter(adapter.id),
|
|
138
|
+
onClear: () => clearColumnFilter(adapter.id),
|
|
139
|
+
onCancel: () => cancelColumnFilter(adapter.id),
|
|
140
|
+
}
|
|
141
|
+
: undefined;
|
|
142
|
+
|
|
157
143
|
return {
|
|
158
144
|
...col,
|
|
159
145
|
filterFn: adapter.columnFilterFn,
|
|
160
146
|
...(cellRenderer && {
|
|
161
147
|
cell: (info: CellContext<TData, TCellValue>) => cellRenderer(info.getValue()),
|
|
162
148
|
}),
|
|
149
|
+
...(resolvedFilter && {
|
|
150
|
+
meta: {
|
|
151
|
+
...col.meta,
|
|
152
|
+
filter: resolvedFilter,
|
|
153
|
+
},
|
|
154
|
+
}),
|
|
163
155
|
};
|
|
164
156
|
});
|
|
165
157
|
|
|
@@ -185,7 +177,43 @@ export function useTableFilters<TData, TValue, TCellValue>({
|
|
|
185
177
|
setAppliedFilters({});
|
|
186
178
|
};
|
|
187
179
|
|
|
188
|
-
const
|
|
180
|
+
const omitKey = (source: FilterState<TValue>, filterId: string): FilterState<TValue> =>
|
|
181
|
+
Object.fromEntries(Object.entries(source).filter(([key]) => key !== filterId));
|
|
182
|
+
|
|
183
|
+
const applyColumnFilter = (filterId: string) => {
|
|
184
|
+
const adapter = _filterAdapters.find((a) => a.id === filterId);
|
|
185
|
+
if (!adapter) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const draftValue = draftFilters[filterId] as TValue;
|
|
190
|
+
const hasActive = adapter.getActiveFiltersCount(draftValue) > 0;
|
|
191
|
+
const next = hasActive
|
|
192
|
+
? { ...appliedFilters, [filterId]: draftValue }
|
|
193
|
+
: omitKey(appliedFilters, filterId);
|
|
194
|
+
|
|
195
|
+
setAppliedFilters(next);
|
|
196
|
+
setPendingFilters((prev) => omitKey(prev, filterId));
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const cancelColumnFilter = (filterId: string) => {
|
|
200
|
+
setPendingFilters((prev) => omitKey(prev, filterId));
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const clearColumnFilter = (filterId: string) => {
|
|
204
|
+
const adapter = _filterAdapters.find((a) => a.id === filterId);
|
|
205
|
+
if (!adapter) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
setPendingFilters((prev) => ({ ...omitKey(prev, filterId), [filterId]: adapter.initialValue }));
|
|
210
|
+
|
|
211
|
+
if (appliedFilters[filterId] !== undefined) {
|
|
212
|
+
setAppliedFilters(omitKey(appliedFilters, filterId));
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const deleteChip = (chip: TagFilterItem) => {
|
|
189
217
|
const filterKey = typeof chip.metadata?.key === 'string' ? chip.metadata.key : undefined;
|
|
190
218
|
if (!filterKey) {
|
|
191
219
|
return;
|
|
@@ -211,8 +239,8 @@ export function useTableFilters<TData, TValue, TCellValue>({
|
|
|
211
239
|
setAppliedFilters(newFilters);
|
|
212
240
|
};
|
|
213
241
|
|
|
214
|
-
const
|
|
215
|
-
const adapter = _filterAdapters.find((a) => a.id ===
|
|
242
|
+
const renderForId = (filterId: string): ReactNode => {
|
|
243
|
+
const adapter = _filterAdapters.find((a) => a.id === filterId);
|
|
216
244
|
if (!adapter) {
|
|
217
245
|
return null;
|
|
218
246
|
}
|
|
@@ -223,6 +251,9 @@ export function useTableFilters<TData, TValue, TCellValue>({
|
|
|
223
251
|
return adapter.renderFilter(value, onChange);
|
|
224
252
|
};
|
|
225
253
|
|
|
254
|
+
const renderFilterContent = (selectedFilter: FilterNavItem) => renderForId(selectedFilter.id);
|
|
255
|
+
const renderColumnFilter = (filterId: string) => renderForId(filterId);
|
|
256
|
+
|
|
226
257
|
return {
|
|
227
258
|
filterState: draftFilters,
|
|
228
259
|
columnFilters,
|
|
@@ -234,7 +265,11 @@ export function useTableFilters<TData, TValue, TCellValue>({
|
|
|
234
265
|
applyFilters,
|
|
235
266
|
clearAll,
|
|
236
267
|
deleteChip,
|
|
268
|
+
applyColumnFilter,
|
|
269
|
+
cancelColumnFilter,
|
|
270
|
+
clearColumnFilter,
|
|
237
271
|
},
|
|
238
272
|
renderFilterContent,
|
|
273
|
+
renderColumnFilter,
|
|
239
274
|
};
|
|
240
275
|
}
|
|
@@ -1,50 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
* Table Filters - Filter Adapter Pattern
|
|
3
|
-
*
|
|
4
|
-
* A plug-and-play filter system that eliminates boilerplate and centralizes filter logic.
|
|
5
|
-
*
|
|
6
|
-
* ## Quick Start:
|
|
7
|
-
*
|
|
8
|
-
* ```typescript
|
|
9
|
-
* // 1. Create filter adapters in a config file
|
|
10
|
-
* import { createCheckboxFilterAdapter, createDualRangeFilterAdapter } from '../filters';
|
|
11
|
-
*
|
|
12
|
-
* export const statusFilter = createCheckboxFilterAdapter({
|
|
13
|
-
* id: 'status',
|
|
14
|
-
* label: 'Status',
|
|
15
|
-
* items: [{ value: 'active', label: 'Active' }],
|
|
16
|
-
* });
|
|
17
|
-
*
|
|
18
|
-
* export const myFilters = [statusFilter];
|
|
19
|
-
*
|
|
20
|
-
* // 2. Use in component with useTableFilters hook
|
|
21
|
-
* import { useTableFilters } from '../filters';
|
|
22
|
-
*
|
|
23
|
-
* const { columnFilters, filterChips, enhancedColumns, handlers } =
|
|
24
|
-
* useTableFilters(myFilters, columns);
|
|
25
|
-
* ```
|
|
26
|
-
*
|
|
27
|
-
* ## Available Exports:
|
|
28
|
-
*
|
|
29
|
-
* ### Factory Functions:
|
|
30
|
-
* - `createCheckboxFilterAdapter` - Multi-select checkbox filters
|
|
31
|
-
* - `createDualRangeFilterAdapter` - Numeric range filters (with multiple fields)
|
|
32
|
-
* - `createFilterAdapter` - Base function for custom filters (full control)
|
|
33
|
-
*
|
|
34
|
-
* ### Hook:
|
|
35
|
-
* - `useTableFilters` - Orchestrates multiple filters, manages state
|
|
36
|
-
*
|
|
37
|
-
* ### Types:
|
|
38
|
-
* - `FilterAdapter` - Core adapter interface
|
|
39
|
-
* - `CheckboxFilterItem` - Item type for checkbox filters
|
|
40
|
-
* - `RangeFilterValue` - Value type for range filters
|
|
41
|
-
*
|
|
42
|
-
* ## Example:
|
|
43
|
-
* See `stories/filters-panel.stories.tsx` and `stories/filters-panel/workflow-filters.config.tsx`
|
|
44
|
-
*/
|
|
1
|
+
import './types/column-meta.augmentation';
|
|
45
2
|
|
|
46
3
|
export * from './adapters';
|
|
47
4
|
export * from './types/filter-adapter.types';
|
|
48
5
|
export * from './hooks';
|
|
49
6
|
export type { CheckboxFilterItem, CheckboxFilterProps } from './components/checkbox-filter';
|
|
50
7
|
export type { RangeFilterValue, RangeFilterProps } from './components/range-filter';
|
|
8
|
+
export * from './components/column-filter-popover';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import '@tanstack/react-table';
|
|
2
|
+
import type { ColumnFilterMeta, ResolvedColumnFilter } from './filter-adapter.types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Adds `column.meta.filter`. Consumers set `ColumnFilterMeta`;
|
|
6
|
+
* `useTableFilters` swaps it for `ResolvedColumnFilter` on `enhancedColumns`
|
|
7
|
+
* so the header can render the trigger + popover. The header narrows the
|
|
8
|
+
* union by checking for `render`.
|
|
9
|
+
*/
|
|
10
|
+
declare module '@tanstack/react-table' {
|
|
11
|
+
// `TData` / `TValue` must mirror the original `ColumnMeta` signature for
|
|
12
|
+
// the augmentation to merge.
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
14
|
+
interface ColumnMeta<TData, TValue> {
|
|
15
|
+
filter?: ColumnFilterMeta | ResolvedColumnFilter;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export {};
|
|
@@ -1,117 +1,84 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import type { Row } from '@tanstack/react-table';
|
|
3
|
-
import type {
|
|
3
|
+
import type { TagFilterItem } from '../../../ds-tag-filter';
|
|
4
4
|
|
|
5
5
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
6
|
export type AnyAdapter = FilterAdapter<any, any, any>;
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* @template TData - The row data type
|
|
13
|
-
* @template TFilterValue - The filter state value type
|
|
14
|
-
* @template TCellValue - The actual cell value type from the table (defaults to unknown)
|
|
9
|
+
* Contract every filter implements. `TData` is the row type, `TFilterValue`
|
|
10
|
+
* the adapter's state, `TCellValue` the cell value returned by the column.
|
|
15
11
|
*/
|
|
16
12
|
export interface FilterAdapter<TData, TFilterValue, TCellValue = unknown> {
|
|
17
|
-
/**
|
|
18
|
-
* Unique identifier for the filter (should match column accessorKey)
|
|
19
|
-
*/
|
|
13
|
+
/** Unique id, should match the column `accessorKey`. */
|
|
20
14
|
id: string;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Display label for the filter in the navigation
|
|
24
|
-
*/
|
|
15
|
+
/** Display label used in the filter navigation. */
|
|
25
16
|
label: string;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Initial/default value for the filter state
|
|
29
|
-
*/
|
|
17
|
+
/** Initial / reset value. */
|
|
30
18
|
initialValue: TFilterValue;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* TanStack Table filter function
|
|
34
|
-
* Determines if a row matches the current filter value
|
|
35
|
-
*/
|
|
19
|
+
/** TanStack `filterFn` — returns whether a row matches the current value. */
|
|
36
20
|
columnFilterFn: (row: Row<TData>, columnId: string, filterValue: TFilterValue) => boolean;
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Optional custom cell renderer for the table column
|
|
40
|
-
* Receives the actual cell value from the table, not the filter value
|
|
41
|
-
* If not provided, default rendering will be used
|
|
42
|
-
*/
|
|
21
|
+
/** Optional cell renderer; receives the cell value, not the filter value. */
|
|
43
22
|
cellRenderer?: (value: TCellValue) => ReactNode;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Convert filter value to filter chips for display
|
|
47
|
-
* Returns empty array if no chips should be shown
|
|
48
|
-
*/
|
|
49
|
-
toChips: (value: TFilterValue) => ChipItem[];
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Remove a chip from the filter value
|
|
53
|
-
* Returns updated filter value with the chip's effect removed
|
|
54
|
-
*/
|
|
55
|
-
fromChip: (chip: ChipItem, currentValue: TFilterValue) => TFilterValue;
|
|
56
|
-
|
|
57
23
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* Can also be used to check if filter is active (count > 0)
|
|
24
|
+
* Convert the filter value into chips for `DsTagFilter`. Each chip's
|
|
25
|
+
* `metadata.key` / `metadata.value` must let `fromChip` undo it.
|
|
61
26
|
*/
|
|
27
|
+
toChips: (value: TFilterValue) => TagFilterItem[];
|
|
28
|
+
/** Remove a chip's effect from the current value. */
|
|
29
|
+
fromChip: (chip: TagFilterItem, currentValue: TFilterValue) => TFilterValue;
|
|
30
|
+
/** Number of active sub-filters; `0` means the filter is inactive. */
|
|
62
31
|
getActiveFiltersCount: (value: TFilterValue) => number;
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Reset filter to initial state
|
|
66
|
-
*/
|
|
32
|
+
/** Return the initial value. */
|
|
67
33
|
reset: () => TFilterValue;
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Render the filter UI component
|
|
71
|
-
* Receives current value and onChange callback
|
|
72
|
-
*/
|
|
34
|
+
/** Render the filter UI for a given value + onChange. */
|
|
73
35
|
renderFilter: (value: TFilterValue, onChange: (value: TFilterValue) => void) => ReactNode;
|
|
74
36
|
}
|
|
75
37
|
|
|
76
|
-
/**
|
|
77
|
-
* Filter navigation item used to render filter tabs in the UI
|
|
78
|
-
*/
|
|
79
38
|
export interface FilterNavItem {
|
|
80
|
-
/**
|
|
81
|
-
* Unique identifier for the filter (matches FilterAdapter.id)
|
|
82
|
-
*/
|
|
39
|
+
/** Matches `FilterAdapter.id`. */
|
|
83
40
|
id: string;
|
|
84
|
-
/**
|
|
85
|
-
* Display label for the filter tab
|
|
86
|
-
*/
|
|
87
41
|
label: string;
|
|
88
|
-
/**
|
|
89
|
-
* Optional count badge to display active filters
|
|
90
|
-
*/
|
|
42
|
+
/** Active filter count badge. */
|
|
91
43
|
count?: number;
|
|
92
|
-
/**
|
|
93
|
-
* Whether this filter is disabled
|
|
94
|
-
*/
|
|
95
44
|
disabled?: boolean;
|
|
96
45
|
}
|
|
97
46
|
|
|
98
|
-
/**
|
|
99
|
-
* Filter state managed by useTableFilters hook
|
|
100
|
-
*/
|
|
47
|
+
/** Map of filter id → adapter value. */
|
|
101
48
|
export interface FilterState<TValue> {
|
|
102
49
|
[filterId: string]: TValue;
|
|
103
50
|
}
|
|
104
51
|
|
|
105
|
-
/**
|
|
106
|
-
* Column filter for TanStack Table
|
|
107
|
-
*/
|
|
108
52
|
export interface ColumnFilterState<TValue> {
|
|
109
|
-
/**
|
|
110
|
-
* Column identifier this filter applies to (matches the column `accessorKey`).
|
|
111
|
-
*/
|
|
53
|
+
/** Column id (matches the column `accessorKey`). */
|
|
112
54
|
id: string;
|
|
113
|
-
/**
|
|
114
|
-
* Current filter value, passed to the column's filter function.
|
|
115
|
-
*/
|
|
55
|
+
/** Value passed to the column's filter function. */
|
|
116
56
|
value: TValue;
|
|
117
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set by the consumer on `column.meta.filter` to opt a column into the
|
|
61
|
+
* per-column filter popover. Provide an `adapter` inline or just `adapterId`
|
|
62
|
+
* to resolve against `useTableFilters({ filterAdapters })`.
|
|
63
|
+
*/
|
|
64
|
+
export interface ColumnFilterMeta {
|
|
65
|
+
/** Mutually exclusive with `adapterId`. */
|
|
66
|
+
adapter?: AnyAdapter;
|
|
67
|
+
/** Mutually exclusive with `adapter`. */
|
|
68
|
+
adapterId?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* What `useTableFilters` injects onto `column.meta.filter` of each enhanced
|
|
73
|
+
* column. Consumers don't build this — they set `ColumnFilterMeta` and the
|
|
74
|
+
* hook swaps it for this resolved shape on `enhancedColumns`.
|
|
75
|
+
*/
|
|
76
|
+
export interface ResolvedColumnFilter {
|
|
77
|
+
filterId: string;
|
|
78
|
+
hasActiveFilter: boolean;
|
|
79
|
+
/** Renders the adapter's filter UI for this column. */
|
|
80
|
+
render: () => ReactNode;
|
|
81
|
+
onApply: () => void;
|
|
82
|
+
onClear: () => void;
|
|
83
|
+
onCancel: () => void;
|
|
84
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { type KeyboardEventHandler, type MouseEvent, type MouseEventHandler } from 'react';
|
|
2
|
+
import type { CellContext, RowData } from '@tanstack/react-table';
|
|
3
|
+
import { useDsTableContext } from '../context/ds-table-context';
|
|
4
|
+
|
|
5
|
+
export interface UseCellEditorOptions<TData extends RowData, TValue> {
|
|
6
|
+
/**
|
|
7
|
+
* The TanStack Table CellContext for this cell. Pass through
|
|
8
|
+
* the argument received in `ColumnDef.editCell`.
|
|
9
|
+
*/
|
|
10
|
+
cellContext: CellContext<TData, TValue>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UseCellEditorContainerProps {
|
|
14
|
+
onKeyDown: KeyboardEventHandler<HTMLElement>;
|
|
15
|
+
onClick: MouseEventHandler;
|
|
16
|
+
onDoubleClick: MouseEventHandler;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UseCellEditorResult<TValue> {
|
|
20
|
+
/** The in-progress draft value for the active editor. */
|
|
21
|
+
value: TValue;
|
|
22
|
+
/** Validation error from the most recent commit attempt, if any. */
|
|
23
|
+
error: string | null;
|
|
24
|
+
/** Update the in-progress draft value. */
|
|
25
|
+
setValue: (value: TValue) => void;
|
|
26
|
+
/** Validate and commit the draft. */
|
|
27
|
+
commit: (overrideValue?: TValue) => void;
|
|
28
|
+
/** Discard the draft and exit edit mode. */
|
|
29
|
+
cancel: () => void;
|
|
30
|
+
/** Stop mouse events from triggering row-level handlers. */
|
|
31
|
+
stopPropagation: (event: MouseEvent) => void;
|
|
32
|
+
/** Spread on the custom `editCell` root for pointer isolation from row handlers. */
|
|
33
|
+
editorContainerProps: UseCellEditorContainerProps;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Wires a TanStack Table `editCell` renderer into DsTable's hoisted editing state.
|
|
38
|
+
* Only mount this hook inside `ColumnDef.editCell` — the table cell layer handles
|
|
39
|
+
* entering edit mode on double-click.
|
|
40
|
+
*/
|
|
41
|
+
export const useCellEditor = <TData extends RowData, TValue>({
|
|
42
|
+
cellContext,
|
|
43
|
+
}: UseCellEditorOptions<TData, TValue>): UseCellEditorResult<TValue> => {
|
|
44
|
+
const ctx = useDsTableContext<TData, TValue>();
|
|
45
|
+
const { row, column, getValue } = cellContext;
|
|
46
|
+
|
|
47
|
+
const isActiveCell =
|
|
48
|
+
ctx.editing !== null && ctx.editing.cell.row.id === row.id && ctx.editing.cell.column.id === column.id;
|
|
49
|
+
const value = isActiveCell && ctx.editing ? ctx.editing.draftValue : getValue();
|
|
50
|
+
const error = isActiveCell && ctx.editing ? ctx.editing.error : null;
|
|
51
|
+
|
|
52
|
+
const setValue = (next: TValue) => {
|
|
53
|
+
ctx.setDraft(next);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const commit = (overrideValue?: TValue) => {
|
|
57
|
+
ctx.commit(overrideValue);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const cancel = () => {
|
|
61
|
+
ctx.cancel();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const stopPropagation = (event: MouseEvent) => {
|
|
65
|
+
event.stopPropagation();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
value,
|
|
70
|
+
error,
|
|
71
|
+
setValue,
|
|
72
|
+
commit,
|
|
73
|
+
cancel,
|
|
74
|
+
stopPropagation,
|
|
75
|
+
editorContainerProps: {
|
|
76
|
+
onKeyDown: () => {},
|
|
77
|
+
onClick: stopPropagation,
|
|
78
|
+
onDoubleClick: stopPropagation,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
};
|
|
@@ -5,3 +5,32 @@ export * from './filters';
|
|
|
5
5
|
|
|
6
6
|
export { default as DsTable } from './ds-table';
|
|
7
7
|
export * from './ds-table.types';
|
|
8
|
+
|
|
9
|
+
export { useCellEditor } from './hooks/use-cell-editor';
|
|
10
|
+
export type {
|
|
11
|
+
UseCellEditorOptions,
|
|
12
|
+
UseCellEditorResult,
|
|
13
|
+
UseCellEditorContainerProps,
|
|
14
|
+
} from './hooks/use-cell-editor';
|
|
15
|
+
|
|
16
|
+
export { DsTableEditableCell } from './components/edit/ds-table-editable-cell';
|
|
17
|
+
export type { DsTableEditableCellProps } from './components/edit/ds-table-editable-cell';
|
|
18
|
+
|
|
19
|
+
export { DsTableEditingCell } from './components/edit/ds-table-editing-cell';
|
|
20
|
+
export type { DsTableEditingCellProps } from './components/edit/ds-table-editing-cell';
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
DsTableEditCellText,
|
|
24
|
+
DsTableEditCellNumber,
|
|
25
|
+
DsTableEditCellSelect,
|
|
26
|
+
DsTableEditCellDate,
|
|
27
|
+
DsTableEditCellCheckbox,
|
|
28
|
+
} from './components/edit/cell-editors';
|
|
29
|
+
export type {
|
|
30
|
+
DsTableEditCellTextProps,
|
|
31
|
+
DsTableEditCellNumberProps,
|
|
32
|
+
DsTableEditCellSelectProps,
|
|
33
|
+
DsTableEditCellDateProps,
|
|
34
|
+
DsTableEditCellDateValue,
|
|
35
|
+
DsTableEditCellCheckboxProps,
|
|
36
|
+
} from './components/edit/cell-editors';
|
|
@@ -2,19 +2,20 @@ import type { CSSProperties } from 'react';
|
|
|
2
2
|
import { defaultColumnSizing } from '@tanstack/react-table';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Generates the appropriate style object for a table column/cell based on its size
|
|
5
|
+
* Generates the appropriate style object for a table column/cell based on its size
|
|
6
|
+
*
|
|
7
|
+
* Custom-sized columns get a fixed `width` plus `minWidth` and `flexShrink: 0` so
|
|
8
|
+
* they enforce horizontal overflow on the table container. Default-sized columns
|
|
9
|
+
* grow to fill the row evenly via `flex: 1`.
|
|
6
10
|
*
|
|
7
11
|
* @param columnSize - The size of the column
|
|
8
|
-
* @
|
|
9
|
-
* @returns Style object or undefined if no custom styling is needed
|
|
12
|
+
* @returns Style object for the column/cell
|
|
10
13
|
*/
|
|
11
|
-
export const getColumnSizeStyle = (columnSize: number
|
|
14
|
+
export const getColumnSizeStyle = (columnSize: number): CSSProperties => {
|
|
12
15
|
const hasCustomSize = columnSize !== defaultColumnSizing.size;
|
|
13
16
|
if (hasCustomSize) {
|
|
14
|
-
return { width: columnSize };
|
|
17
|
+
return { width: columnSize, minWidth: columnSize, flexShrink: 0 };
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
return { flex: 1 };
|
|
19
|
-
}
|
|
20
|
+
return { flex: 1, minWidth: 0 };
|
|
20
21
|
};
|
|
@@ -43,7 +43,7 @@ const DsTag = ({
|
|
|
43
43
|
const handleKeyDownTag = (event: KeyboardEvent<HTMLElement>) => {
|
|
44
44
|
if (onClick && (event.key === 'Enter' || event.key === ' ')) {
|
|
45
45
|
event.preventDefault();
|
|
46
|
-
onClick(event
|
|
46
|
+
onClick(event);
|
|
47
47
|
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
@@ -66,7 +66,7 @@ const DsTag = ({
|
|
|
66
66
|
const handleClick = (event: MouseEvent<HTMLElement>) => {
|
|
67
67
|
if (onClick) {
|
|
68
68
|
event.preventDefault();
|
|
69
|
-
onClick(event
|
|
69
|
+
onClick(event);
|
|
70
70
|
}
|
|
71
71
|
};
|
|
72
72
|
|