@drivenets/design-system 0.12.1 → 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 +15 -0
- package/dist/index.cjs +1097 -308
- package/dist/index.d.cts +1118 -982
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1118 -982
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1089 -309
- package/dist/index.js.map +1 -1
- package/dist/index.min.css +1 -1
- package/package.json +31 -31
- 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-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 +1 -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,5 +1,9 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ReactNode, useState } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import styles from './checkbox-filter.module.scss';
|
|
2
4
|
import { DsCheckbox } from '../../../../ds-checkbox';
|
|
5
|
+
import { DsTextInput } from '../../../../ds-text-input';
|
|
6
|
+
import { DsIcon } from '../../../../ds-icon';
|
|
3
7
|
|
|
4
8
|
export interface CheckboxFilterItem<TValue = string> {
|
|
5
9
|
value: TValue;
|
|
@@ -7,11 +11,30 @@ export interface CheckboxFilterItem<TValue = string> {
|
|
|
7
11
|
metadata?: Record<string, unknown>;
|
|
8
12
|
}
|
|
9
13
|
|
|
14
|
+
export interface CheckboxFilterLocale {
|
|
15
|
+
/** @default 'All' */
|
|
16
|
+
allLabel?: string;
|
|
17
|
+
/** @default 'Search' */
|
|
18
|
+
searchPlaceholder?: string;
|
|
19
|
+
/** @default 'No matches' */
|
|
20
|
+
noMatchesLabel?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
10
23
|
export interface CheckboxFilterProps<TValue> {
|
|
11
24
|
items: CheckboxFilterItem<TValue>[];
|
|
25
|
+
/** Custom row renderer; defaults to `item.label`. */
|
|
12
26
|
renderer?: (item: CheckboxFilterItem<TValue>) => ReactNode;
|
|
13
27
|
selectedItems: CheckboxFilterItem<TValue>[];
|
|
14
28
|
onSelectionChange: (selectedItems: CheckboxFilterItem<TValue>[]) => void;
|
|
29
|
+
/** Show a case-insensitive substring search above the list. @default false */
|
|
30
|
+
searchable?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Show an "All" toggle. Checked when every visible item is selected,
|
|
33
|
+
* indeterminate when some are, unchecked otherwise. @default false
|
|
34
|
+
*/
|
|
35
|
+
selectAll?: boolean;
|
|
36
|
+
locale?: CheckboxFilterLocale;
|
|
37
|
+
className?: string;
|
|
15
38
|
}
|
|
16
39
|
|
|
17
40
|
export const CheckboxFilter = <TValue,>({
|
|
@@ -19,7 +42,19 @@ export const CheckboxFilter = <TValue,>({
|
|
|
19
42
|
renderer,
|
|
20
43
|
selectedItems,
|
|
21
44
|
onSelectionChange,
|
|
45
|
+
searchable = false,
|
|
46
|
+
selectAll = false,
|
|
47
|
+
locale,
|
|
48
|
+
className,
|
|
22
49
|
}: CheckboxFilterProps<TValue>) => {
|
|
50
|
+
const { allLabel = 'All', searchPlaceholder = 'Search', noMatchesLabel = 'No matches' } = locale ?? {};
|
|
51
|
+
|
|
52
|
+
const [query, setQuery] = useState('');
|
|
53
|
+
|
|
54
|
+
const visibleItems = searchable
|
|
55
|
+
? items.filter((item) => item.label.toLowerCase().includes(query.trim().toLowerCase()))
|
|
56
|
+
: items;
|
|
57
|
+
|
|
23
58
|
const handleCheckedChange = (selected: CheckboxFilterItem<TValue>, checked: boolean) => {
|
|
24
59
|
if (checked) {
|
|
25
60
|
onSelectionChange([...selectedItems, selected]);
|
|
@@ -28,16 +63,60 @@ export const CheckboxFilter = <TValue,>({
|
|
|
28
63
|
}
|
|
29
64
|
};
|
|
30
65
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
);
|
|
42
|
-
|
|
66
|
+
const visibleSelectedCount = visibleItems.filter((item) =>
|
|
67
|
+
selectedItems.some((selected) => selected.value === item.value),
|
|
68
|
+
).length;
|
|
69
|
+
|
|
70
|
+
const allVisibleSelected = visibleItems.length > 0 && visibleSelectedCount === visibleItems.length;
|
|
71
|
+
const someVisibleSelected = visibleSelectedCount > 0 && !allVisibleSelected;
|
|
72
|
+
|
|
73
|
+
const handleSelectAllChange = (next: boolean | 'indeterminate') => {
|
|
74
|
+
const shouldSelect = next === true;
|
|
75
|
+
const visibleValues = new Set(visibleItems.map((item) => item.value));
|
|
76
|
+
const withoutVisible = selectedItems.filter((item) => !visibleValues.has(item.value));
|
|
77
|
+
onSelectionChange(shouldSelect ? [...withoutVisible, ...visibleItems] : withoutVisible);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className={classNames(styles.root, className)}>
|
|
82
|
+
{searchable && (
|
|
83
|
+
<DsTextInput
|
|
84
|
+
value={query}
|
|
85
|
+
placeholder={searchPlaceholder}
|
|
86
|
+
onValueChange={setQuery}
|
|
87
|
+
slots={{
|
|
88
|
+
startAdornment: <DsIcon icon="search" size="tiny" />,
|
|
89
|
+
}}
|
|
90
|
+
/>
|
|
91
|
+
)}
|
|
92
|
+
|
|
93
|
+
<div className={styles.list} role="group">
|
|
94
|
+
{selectAll && visibleItems.length > 0 && (
|
|
95
|
+
<DsCheckbox
|
|
96
|
+
className={styles.checkboxItem}
|
|
97
|
+
label={allLabel}
|
|
98
|
+
checked={allVisibleSelected ? true : someVisibleSelected ? 'indeterminate' : false}
|
|
99
|
+
onCheckedChange={handleSelectAllChange}
|
|
100
|
+
/>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
{visibleItems.length === 0 && <div className={styles.empty}>{noMatchesLabel}</div>}
|
|
104
|
+
|
|
105
|
+
{visibleItems.map((item) => {
|
|
106
|
+
const label = renderer ? renderer(item) : item.label;
|
|
107
|
+
const checked = selectedItems.some((selected) => selected.value === item.value);
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<DsCheckbox
|
|
111
|
+
key={JSON.stringify(item.value)}
|
|
112
|
+
className={styles.checkboxItem}
|
|
113
|
+
label={label}
|
|
114
|
+
checked={checked}
|
|
115
|
+
onCheckedChange={(next) => handleCheckedChange(item, next === true)}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
})}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
43
122
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { CheckboxFilter } from './checkbox-filter';
|
|
2
|
-
export type { CheckboxFilterProps, CheckboxFilterItem } from './checkbox-filter';
|
|
2
|
+
export type { CheckboxFilterProps, CheckboxFilterItem, CheckboxFilterLocale } from './checkbox-filter';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
.trigger {
|
|
2
|
+
min-width: var(--icon-width-tiny);
|
|
3
|
+
padding: 0;
|
|
4
|
+
opacity: 0;
|
|
5
|
+
transition: opacity 0.2s;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.triggerActive,
|
|
9
|
+
.triggerOpen {
|
|
10
|
+
opacity: 1;
|
|
11
|
+
color: var(--icon-information-main, var(--color-dap-primary-500));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.content {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
gap: var(--xs);
|
|
18
|
+
min-width: 280px;
|
|
19
|
+
max-width: 320px;
|
|
20
|
+
padding: var(--standard);
|
|
21
|
+
background: var(--background);
|
|
22
|
+
border: 1px solid var(--border-secondary);
|
|
23
|
+
border-radius: 4px;
|
|
24
|
+
box-shadow: 0 5px 15px 0 rgba(0, 0, 0, 0.15);
|
|
25
|
+
z-index: 1000;
|
|
26
|
+
|
|
27
|
+
&[hidden] {
|
|
28
|
+
display: none;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.body {
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.footer {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
justify-content: flex-end;
|
|
41
|
+
gap: var(--xs);
|
|
42
|
+
}
|
package/src/components/ds-table/filters/components/column-filter-popover/column-filter-popover.tsx
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type MouseEvent, useState } from 'react';
|
|
2
|
+
import { Popover } from '@ark-ui/react/popover';
|
|
3
|
+
import { Portal } from '@ark-ui/react/portal';
|
|
4
|
+
import classNames from 'classnames';
|
|
5
|
+
import styles from './column-filter-popover.module.scss';
|
|
6
|
+
import type { DsTableColumnFilterPopoverProps } from './column-filter-popover.types';
|
|
7
|
+
import { DsIcon } from '../../../../ds-icon';
|
|
8
|
+
import { DsButtonV3 } from '../../../../ds-button-v3';
|
|
9
|
+
|
|
10
|
+
// React portals bubble events through the React tree, so clicks inside the
|
|
11
|
+
// popover would otherwise reach the parent `<TableHead onClick={sortHandler}>`
|
|
12
|
+
// and toggle column sorting when applying or clearing the filter.
|
|
13
|
+
const stopPropagation = (event: MouseEvent) => event.stopPropagation();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Per-column filter popover anchored to a header cell: funnel trigger,
|
|
17
|
+
* adapter UI as the body, Clear / Save footer wired to `useTableFilters`.
|
|
18
|
+
*/
|
|
19
|
+
export const DsTableColumnFilterPopover = ({
|
|
20
|
+
children,
|
|
21
|
+
hasActiveFilter,
|
|
22
|
+
onApply,
|
|
23
|
+
onClear,
|
|
24
|
+
onCancel,
|
|
25
|
+
locale,
|
|
26
|
+
className,
|
|
27
|
+
}: DsTableColumnFilterPopoverProps) => {
|
|
28
|
+
const { triggerAriaLabel = 'Filter column', clearLabel = 'Clear', saveLabel = 'Save' } = locale ?? {};
|
|
29
|
+
|
|
30
|
+
const [open, setOpen] = useState(false);
|
|
31
|
+
|
|
32
|
+
const handleOpenChange = (details: { open: boolean }) => {
|
|
33
|
+
if (open && !details.open) {
|
|
34
|
+
onCancel();
|
|
35
|
+
}
|
|
36
|
+
setOpen(details.open);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const handleSave = () => {
|
|
40
|
+
onApply();
|
|
41
|
+
setOpen(false);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleClear = () => {
|
|
45
|
+
onClear();
|
|
46
|
+
setOpen(false);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Popover.Root
|
|
51
|
+
open={open}
|
|
52
|
+
onOpenChange={handleOpenChange}
|
|
53
|
+
positioning={{ placement: 'bottom-end', gutter: 4 }}
|
|
54
|
+
>
|
|
55
|
+
<Popover.Trigger asChild>
|
|
56
|
+
<DsButtonV3
|
|
57
|
+
variant="tertiary"
|
|
58
|
+
size="tiny"
|
|
59
|
+
className={classNames(
|
|
60
|
+
styles.trigger,
|
|
61
|
+
hasActiveFilter && styles.triggerActive,
|
|
62
|
+
open && styles.triggerOpen,
|
|
63
|
+
)}
|
|
64
|
+
aria-label={triggerAriaLabel}
|
|
65
|
+
data-active={hasActiveFilter ? 'true' : undefined}
|
|
66
|
+
onClick={(event) => event.stopPropagation()}
|
|
67
|
+
>
|
|
68
|
+
<DsIcon icon="filter_alt" variant="outlined" filled={hasActiveFilter} size="tiny" />
|
|
69
|
+
</DsButtonV3>
|
|
70
|
+
</Popover.Trigger>
|
|
71
|
+
|
|
72
|
+
<Portal>
|
|
73
|
+
<Popover.Positioner>
|
|
74
|
+
<Popover.Content className={classNames(styles.content, className)} onClick={stopPropagation}>
|
|
75
|
+
<div className={styles.body}>{children}</div>
|
|
76
|
+
<div className={styles.footer}>
|
|
77
|
+
<DsButtonV3 variant="secondary" size="small" onClick={handleClear}>
|
|
78
|
+
{clearLabel}
|
|
79
|
+
</DsButtonV3>
|
|
80
|
+
<DsButtonV3 variant="primary" size="small" onClick={handleSave}>
|
|
81
|
+
{saveLabel}
|
|
82
|
+
</DsButtonV3>
|
|
83
|
+
</div>
|
|
84
|
+
</Popover.Content>
|
|
85
|
+
</Popover.Positioner>
|
|
86
|
+
</Portal>
|
|
87
|
+
</Popover.Root>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface ColumnFilterPopoverLocale {
|
|
4
|
+
/** @default 'Filter column' */
|
|
5
|
+
triggerAriaLabel?: string;
|
|
6
|
+
/** @default 'Clear' */
|
|
7
|
+
clearLabel?: string;
|
|
8
|
+
/** @default 'Save' */
|
|
9
|
+
saveLabel?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DsTableColumnFilterPopoverProps {
|
|
13
|
+
/** Body rendered above the Clear / Save footer. */
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
/** Whether the column has an applied filter; drives the trigger's active state. */
|
|
16
|
+
hasActiveFilter: boolean;
|
|
17
|
+
/** Commit the draft for this column. */
|
|
18
|
+
onApply: () => void;
|
|
19
|
+
/** Clear the draft and the applied value for this column. */
|
|
20
|
+
onClear: () => void;
|
|
21
|
+
/** Discard the draft when the popover closes without Save. */
|
|
22
|
+
onCancel: () => void;
|
|
23
|
+
locale?: ColumnFilterPopoverLocale;
|
|
24
|
+
className?: string;
|
|
25
|
+
}
|
|
@@ -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 {};
|