@douglasneuroinformatics/libui 4.9.1 → 5.0.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/dist/components.d.ts +5 -48
- package/dist/components.js +1110 -723
- package/dist/components.js.map +1 -1
- package/dist/hooks.d.ts +4 -2
- package/dist/{types-9zYgx7C8.d.ts → types-CQ7qbFhC.d.ts} +57 -1
- package/package.json +3 -2
- package/src/components/DataTable/DataTable.stories.tsx +207 -37
- package/src/components/DataTable/DataTable.tsx +22 -279
- package/src/components/DataTable/DataTableBody.tsx +69 -0
- package/src/components/DataTable/DataTableContent.tsx +36 -0
- package/src/components/DataTable/DataTableControls.tsx +55 -0
- package/src/components/DataTable/DataTableEmptyState.tsx +25 -0
- package/src/components/DataTable/DataTableHead.tsx +58 -0
- package/src/components/DataTable/DataTablePagination.tsx +62 -0
- package/src/components/DataTable/DataTableRowActionCell.tsx +67 -0
- package/src/components/DataTable/__tests__/DataTable.spec.tsx +60 -0
- package/src/components/DataTable/constants.ts +7 -0
- package/src/components/DataTable/context.ts +5 -0
- package/src/components/DataTable/hooks.ts +60 -0
- package/src/components/DataTable/store.ts +203 -0
- package/src/components/DataTable/types.ts +99 -0
- package/src/components/DataTable/utils.tsx +138 -0
- package/src/hooks/useDestructiveAction/useDestructiveActionStore.test.ts +2 -7
- package/src/hooks/useNotificationsStore/useNotificationsStore.test.ts +1 -8
- package/src/testing/setup-tests.ts +1 -3
- package/src/components/DataTable/DestructiveActionDialog.tsx +0 -67
- package/src/components/DataTable/RowActionsDropdown.tsx +0 -64
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { faker } from '@faker-js/faker';
|
|
2
|
+
import type { ColumnDef } from '@tanstack/table-core';
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
4
|
+
import { range } from 'lodash-es';
|
|
5
|
+
import { describe, expect, it } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { DataTable } from '../DataTable';
|
|
8
|
+
|
|
9
|
+
type PaymentStatus = 'failed' | 'pending' | 'processing' | 'success';
|
|
10
|
+
|
|
11
|
+
type Payment = {
|
|
12
|
+
amount: number;
|
|
13
|
+
email: string;
|
|
14
|
+
id: string;
|
|
15
|
+
status: PaymentStatus;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const columns: ColumnDef<Payment>[] = [
|
|
19
|
+
{
|
|
20
|
+
accessorKey: 'status',
|
|
21
|
+
enableSorting: false,
|
|
22
|
+
filterFn: (row, id, filter: PaymentStatus[]) => {
|
|
23
|
+
return filter.includes(row.getValue(id));
|
|
24
|
+
},
|
|
25
|
+
header: 'Status'
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
accessorKey: 'email',
|
|
29
|
+
header: 'Email'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
accessorKey: 'amount',
|
|
33
|
+
header: 'Amount'
|
|
34
|
+
}
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const statuses: readonly PaymentStatus[] = Object.freeze(['failed', 'pending', 'processing', 'success']);
|
|
38
|
+
|
|
39
|
+
const data: Payment[] = range(20).map((i) => ({
|
|
40
|
+
amount: faker.number.int({ max: 100, min: 0 }),
|
|
41
|
+
email: faker.internet.email(),
|
|
42
|
+
id: String(i + 1),
|
|
43
|
+
status: faker.helpers.arrayElement(statuses)
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
describe('DataTable', () => {
|
|
47
|
+
it('should render with 10 visible rows', () => {
|
|
48
|
+
render(<DataTable columns={columns} data={data} />);
|
|
49
|
+
expect(screen.getByTestId('data-table')).toBeInTheDocument();
|
|
50
|
+
expect(screen.getAllByTestId('data-table-row').length).toBe(10);
|
|
51
|
+
});
|
|
52
|
+
it('should search', async () => {
|
|
53
|
+
render(<DataTable columns={columns} data={data} />);
|
|
54
|
+
const searchBar = screen.getByTestId('data-table-search-bar').querySelector('input')!;
|
|
55
|
+
fireEvent.change(searchBar, { target: { value: data[0]!.email } });
|
|
56
|
+
await waitFor(() => expect(screen.getAllByTestId('data-table-row').length).toBe(1));
|
|
57
|
+
fireEvent.change(searchBar, { target: { value: '' } });
|
|
58
|
+
await waitFor(() => expect(screen.getAllByTestId('data-table-row').length).toBe(10));
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useContext, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useStore } from 'zustand';
|
|
4
|
+
import { useStoreWithEqualityFn } from 'zustand/traditional';
|
|
5
|
+
|
|
6
|
+
import { MEMOIZED_HANDLE_ID } from './constants';
|
|
7
|
+
import { DataTableContext } from './context';
|
|
8
|
+
|
|
9
|
+
import type { DataTableStore } from './types';
|
|
10
|
+
|
|
11
|
+
export function useContainerRef() {
|
|
12
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
13
|
+
const setContainerWidth = useDataTableStore((state) => state.setContainerWidth);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
let timeout: ReturnType<typeof setTimeout>;
|
|
17
|
+
let delay = 0;
|
|
18
|
+
|
|
19
|
+
const observer = new ResizeObserver(([entry]) => {
|
|
20
|
+
clearTimeout(timeout);
|
|
21
|
+
timeout = setTimeout(() => {
|
|
22
|
+
delay = 100;
|
|
23
|
+
if (entry?.contentBoxSize[0]?.inlineSize) {
|
|
24
|
+
const containerWidth = entry.contentBoxSize[0].inlineSize;
|
|
25
|
+
setContainerWidth(containerWidth);
|
|
26
|
+
}
|
|
27
|
+
}, delay);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (containerRef.current) {
|
|
31
|
+
observer.observe(containerRef.current);
|
|
32
|
+
}
|
|
33
|
+
return () => {
|
|
34
|
+
observer.disconnect();
|
|
35
|
+
clearTimeout(timeout);
|
|
36
|
+
};
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
return containerRef;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function useDataTableStore<T>(selector: (state: DataTableStore) => T) {
|
|
43
|
+
const context = useContext(DataTableContext);
|
|
44
|
+
return useStore(context.store, selector);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function useDataTableHandle<TKey extends keyof DataTableStore['$handles']>(key: TKey, forceRender = false) {
|
|
48
|
+
const context = useContext(DataTableContext);
|
|
49
|
+
const { handle } = useStoreWithEqualityFn(
|
|
50
|
+
context.store,
|
|
51
|
+
// the function is already updated by the time of equality check, so we cache it here
|
|
52
|
+
(store) => ({
|
|
53
|
+
globalKey: store._key,
|
|
54
|
+
handle: store.$handles[key],
|
|
55
|
+
handleKey: store.$handles[key][MEMOIZED_HANDLE_ID]
|
|
56
|
+
}),
|
|
57
|
+
forceRender ? (a, b) => a.globalKey === b.globalKey : (a, b) => a.handleKey === b.handleKey
|
|
58
|
+
);
|
|
59
|
+
return handle();
|
|
60
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createTable,
|
|
3
|
+
getCoreRowModel,
|
|
4
|
+
getFilteredRowModel,
|
|
5
|
+
getPaginationRowModel,
|
|
6
|
+
getSortedRowModel
|
|
7
|
+
} from '@tanstack/table-core';
|
|
8
|
+
import type { GlobalFilter, TableState, Updater } from '@tanstack/table-core';
|
|
9
|
+
import { createStore } from 'zustand';
|
|
10
|
+
|
|
11
|
+
import { ROW_ACTIONS_METADATA_KEY, TABLE_NAME_METADATA_KEY } from './constants';
|
|
12
|
+
import {
|
|
13
|
+
applyUpdater,
|
|
14
|
+
calculateColumnSizing,
|
|
15
|
+
defineMemoizedHandle,
|
|
16
|
+
getColumnsWithActions,
|
|
17
|
+
getTanstackTableState
|
|
18
|
+
} from './utils';
|
|
19
|
+
|
|
20
|
+
import type { DataTableStore, DataTableStoreParams } from './types';
|
|
21
|
+
|
|
22
|
+
export function createDataTableStore<T>(params: DataTableStoreParams<T>) {
|
|
23
|
+
return createStore<DataTableStore>((set, get) => {
|
|
24
|
+
const _state = getTanstackTableState(params);
|
|
25
|
+
|
|
26
|
+
const invalidateHandles = <TKey extends keyof DataTableStore['$handles']>(keys: TKey[] | void) => {
|
|
27
|
+
set((state) => {
|
|
28
|
+
(keys ?? (Object.keys(state.$handles) as TKey[])).forEach((key) => {
|
|
29
|
+
state.$handles[key].invalidate();
|
|
30
|
+
});
|
|
31
|
+
return { _key: Symbol() };
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const setTableState = <TKey extends keyof TableState>(key: TKey, updaterOrValue: Updater<TableState[TKey]>) => {
|
|
36
|
+
const state = table.getState();
|
|
37
|
+
const value = applyUpdater(updaterOrValue, state[key]);
|
|
38
|
+
table.setOptions((prev) => ({ ...prev, state: { ...prev.state, [key]: value } }));
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const updateColumnSizing = () => {
|
|
42
|
+
const { _containerWidth } = get();
|
|
43
|
+
if (!_containerWidth) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
setTableState('columnSizing', calculateColumnSizing(table, _containerWidth));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const updateStyle = () => {
|
|
50
|
+
set((state) => {
|
|
51
|
+
const headers = table.getFlatHeaders();
|
|
52
|
+
const style: React.CSSProperties & { [key: string]: any } = {
|
|
53
|
+
width: table.getTotalSize()
|
|
54
|
+
};
|
|
55
|
+
if (state._containerWidth === null) {
|
|
56
|
+
style['--table-container-width'] = state._containerWidth;
|
|
57
|
+
style.visibility = 'hidden';
|
|
58
|
+
} else {
|
|
59
|
+
style['--table-container-width'] = state._containerWidth;
|
|
60
|
+
style.visibility = 'visible';
|
|
61
|
+
}
|
|
62
|
+
for (const header of headers) {
|
|
63
|
+
style[`--header-${header.id}-size`] = header.getSize();
|
|
64
|
+
style[`--col-${header.column.id}-size`] = header.column.getSize();
|
|
65
|
+
}
|
|
66
|
+
return { style };
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const table = createTable<any>({
|
|
71
|
+
columnResizeMode: 'onChange',
|
|
72
|
+
columns: getColumnsWithActions(params),
|
|
73
|
+
data: params.data,
|
|
74
|
+
enableSortingRemoval: false,
|
|
75
|
+
getCoreRowModel: getCoreRowModel(),
|
|
76
|
+
getFilteredRowModel: getFilteredRowModel(),
|
|
77
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
78
|
+
getSortedRowModel: getSortedRowModel(),
|
|
79
|
+
meta: {
|
|
80
|
+
...params.meta,
|
|
81
|
+
[ROW_ACTIONS_METADATA_KEY]: params.rowActions,
|
|
82
|
+
[TABLE_NAME_METADATA_KEY]: params.tableName
|
|
83
|
+
},
|
|
84
|
+
onColumnFiltersChange: (updaterOrValue) => {
|
|
85
|
+
setTableState('columnFilters', updaterOrValue);
|
|
86
|
+
invalidateHandles();
|
|
87
|
+
},
|
|
88
|
+
onColumnPinningChange: (updaterOrValue) => {
|
|
89
|
+
setTableState('columnPinning', updaterOrValue);
|
|
90
|
+
invalidateHandles();
|
|
91
|
+
},
|
|
92
|
+
onColumnSizingChange: (updaterOrValue) => {
|
|
93
|
+
const { _containerWidth: containerWidth } = get();
|
|
94
|
+
const { columnSizing: prevColumnSizing } = table.getState();
|
|
95
|
+
if (!containerWidth) {
|
|
96
|
+
console.error('Cannot set column sizing: container width is null');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const updatedColumnSizing = applyUpdater(updaterOrValue, prevColumnSizing);
|
|
100
|
+
const computedWidth = table.getVisibleLeafColumns().reduce((previous, current) => {
|
|
101
|
+
return previous + (updatedColumnSizing[current.id] ?? current.getSize());
|
|
102
|
+
}, 0);
|
|
103
|
+
if (Number.isNaN(computedWidth)) {
|
|
104
|
+
console.error('Failed to compute width for columns');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (containerWidth > computedWidth) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
setTableState('columnSizing', updatedColumnSizing);
|
|
111
|
+
updateStyle();
|
|
112
|
+
invalidateHandles();
|
|
113
|
+
},
|
|
114
|
+
onColumnSizingInfoChange: (updaterOrValue) => {
|
|
115
|
+
setTableState('columnSizingInfo', updaterOrValue);
|
|
116
|
+
updateStyle();
|
|
117
|
+
invalidateHandles();
|
|
118
|
+
},
|
|
119
|
+
onColumnVisibilityChange: (updaterOrValue) => {
|
|
120
|
+
setTableState('columnVisibility', updaterOrValue);
|
|
121
|
+
updateColumnSizing();
|
|
122
|
+
updateStyle();
|
|
123
|
+
invalidateHandles();
|
|
124
|
+
},
|
|
125
|
+
onGlobalFilterChange: (updaterOrValue: Updater<GlobalFilter>) => {
|
|
126
|
+
setTableState('globalFilter', updaterOrValue);
|
|
127
|
+
invalidateHandles();
|
|
128
|
+
},
|
|
129
|
+
onPaginationChange: (updaterOrValue) => {
|
|
130
|
+
setTableState('pagination', updaterOrValue);
|
|
131
|
+
invalidateHandles();
|
|
132
|
+
},
|
|
133
|
+
onSortingChange: (updaterOrValue) => {
|
|
134
|
+
setTableState('sorting', updaterOrValue);
|
|
135
|
+
invalidateHandles();
|
|
136
|
+
},
|
|
137
|
+
onStateChange: (updaterOrValue) => {
|
|
138
|
+
const prevState = table.getState();
|
|
139
|
+
table.setOptions((prev) => ({
|
|
140
|
+
...prev,
|
|
141
|
+
state: typeof updaterOrValue === 'function' ? updaterOrValue(prevState) : updaterOrValue
|
|
142
|
+
}));
|
|
143
|
+
invalidateHandles();
|
|
144
|
+
},
|
|
145
|
+
renderFallbackValue: null,
|
|
146
|
+
state: _state
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
$handles: {
|
|
151
|
+
headerGroups: defineMemoizedHandle(() => table.getHeaderGroups()),
|
|
152
|
+
paginationInfo: defineMemoizedHandle(() => {
|
|
153
|
+
const { pagination } = table.getState();
|
|
154
|
+
return {
|
|
155
|
+
pageCount: table.getPageCount(),
|
|
156
|
+
pageIndex: pagination.pageIndex
|
|
157
|
+
};
|
|
158
|
+
}),
|
|
159
|
+
rowCount: defineMemoizedHandle(() => table.getRowCount()),
|
|
160
|
+
rows: defineMemoizedHandle(() => {
|
|
161
|
+
const { rows } = table.getRowModel();
|
|
162
|
+
return rows;
|
|
163
|
+
}),
|
|
164
|
+
table: defineMemoizedHandle(() => table),
|
|
165
|
+
tableMeta: defineMemoizedHandle(() => table.options.meta ?? {})
|
|
166
|
+
},
|
|
167
|
+
_containerWidth: null,
|
|
168
|
+
_key: Symbol(),
|
|
169
|
+
reset: (params) => {
|
|
170
|
+
table.setOptions((options) => ({
|
|
171
|
+
...options,
|
|
172
|
+
columns: getColumnsWithActions(params),
|
|
173
|
+
data: params.data,
|
|
174
|
+
meta: {
|
|
175
|
+
...params.meta,
|
|
176
|
+
[ROW_ACTIONS_METADATA_KEY]: params.rowActions,
|
|
177
|
+
[TABLE_NAME_METADATA_KEY]: params.tableName
|
|
178
|
+
},
|
|
179
|
+
state: getTanstackTableState(params)
|
|
180
|
+
}));
|
|
181
|
+
invalidateHandles();
|
|
182
|
+
},
|
|
183
|
+
setContainerWidth: (containerWidth) => {
|
|
184
|
+
set(() => {
|
|
185
|
+
return { _containerWidth: containerWidth };
|
|
186
|
+
});
|
|
187
|
+
updateColumnSizing();
|
|
188
|
+
updateStyle();
|
|
189
|
+
},
|
|
190
|
+
setGlobalFilter: (globalFilter) => {
|
|
191
|
+
table.setGlobalFilter(globalFilter);
|
|
192
|
+
},
|
|
193
|
+
setPageIndex: (index) => {
|
|
194
|
+
table.setPageIndex(index);
|
|
195
|
+
},
|
|
196
|
+
style: {
|
|
197
|
+
visibility: 'hidden'
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export type DataTableStoreApi = ReturnType<typeof createDataTableStore>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ColumnDef,
|
|
3
|
+
ColumnFiltersState,
|
|
4
|
+
ColumnPinningState,
|
|
5
|
+
GlobalFilter,
|
|
6
|
+
HeaderGroup,
|
|
7
|
+
Row,
|
|
8
|
+
RowData,
|
|
9
|
+
SortingState,
|
|
10
|
+
Table,
|
|
11
|
+
TableMeta
|
|
12
|
+
} from '@tanstack/table-core';
|
|
13
|
+
import type { Promisable } from 'type-fest';
|
|
14
|
+
|
|
15
|
+
import type { MEMOIZED_HANDLE_ID, ROW_ACTIONS_METADATA_KEY, TABLE_NAME_METADATA_KEY } from './constants';
|
|
16
|
+
import type { DataTableEmptyStateProps } from './DataTableEmptyState';
|
|
17
|
+
|
|
18
|
+
declare module '@tanstack/table-core' {
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions, @typescript-eslint/no-unused-vars
|
|
20
|
+
export interface ColumnMeta<TData, TValue> {
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type GlobalFilter = string | undefined;
|
|
25
|
+
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
27
|
+
export interface TableMeta<TData extends RowData> {
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
[ROW_ACTIONS_METADATA_KEY]?: DataTableRowAction<TData>[];
|
|
30
|
+
[TABLE_NAME_METADATA_KEY]?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
|
34
|
+
export interface TableState {
|
|
35
|
+
globalFilter: GlobalFilter;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type DataTableInitialState = {
|
|
40
|
+
columnFilters?: ColumnFiltersState;
|
|
41
|
+
columnPinning?: ColumnPinningState;
|
|
42
|
+
sorting?: SortingState;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type DataTableProps<T extends RowData> = DataTableContentProps<T> & DataTableStoreParams<T>;
|
|
46
|
+
|
|
47
|
+
export type DataTableContentProps<T extends RowData> = {
|
|
48
|
+
emptyStateProps?: Partial<DataTableEmptyStateProps>;
|
|
49
|
+
onSearchChange?: SearchChangeHandler<NoInfer<T>>;
|
|
50
|
+
togglesComponent?: React.FC<{ table: Table<T> }>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export type DataTableHandles<T extends { [key: string]: unknown }> = {
|
|
54
|
+
[K in keyof T]: MemoizedHandle<() => T[K]>;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type DataTableRowAction<T extends RowData> = {
|
|
58
|
+
destructive?: boolean;
|
|
59
|
+
disabled?: ((row: T) => boolean) | boolean;
|
|
60
|
+
label: string;
|
|
61
|
+
onSelect: (row: T, table: Table<T>) => Promisable<void>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type DataTableStore = {
|
|
65
|
+
$handles: DataTableHandles<{
|
|
66
|
+
headerGroups: HeaderGroup<any>[];
|
|
67
|
+
paginationInfo: {
|
|
68
|
+
pageCount: number;
|
|
69
|
+
pageIndex: number;
|
|
70
|
+
};
|
|
71
|
+
rowCount: number;
|
|
72
|
+
rows: Row<any>[];
|
|
73
|
+
table: Table<any>;
|
|
74
|
+
tableMeta: TableMeta<any>;
|
|
75
|
+
}>;
|
|
76
|
+
_containerWidth: null | number;
|
|
77
|
+
_key: symbol;
|
|
78
|
+
reset: (params: DataTableStoreParams<any>) => void;
|
|
79
|
+
setContainerWidth: (containerWidth: number) => void;
|
|
80
|
+
setGlobalFilter: (globalFilter: GlobalFilter) => void;
|
|
81
|
+
setPageIndex: (index: number) => void;
|
|
82
|
+
style: React.CSSProperties;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type DataTableStoreParams<T extends RowData> = {
|
|
86
|
+
columns: ColumnDef<NoInfer<T>>[];
|
|
87
|
+
data: T[];
|
|
88
|
+
initialState?: DataTableInitialState;
|
|
89
|
+
meta?: TableMeta<NoInfer<T>>;
|
|
90
|
+
rowActions?: DataTableRowAction<NoInfer<T>>[];
|
|
91
|
+
tableName?: string;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type MemoizedHandle<T extends (...args: any[]) => any> = T & {
|
|
95
|
+
invalidate(): void;
|
|
96
|
+
[MEMOIZED_HANDLE_ID]: symbol;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export type SearchChangeHandler<T = any> = (value: string, table: Table<T>) => void;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { ColumnDef, ColumnSizingState, RowData, Table, TableState, Updater } from '@tanstack/table-core';
|
|
2
|
+
import { sum } from 'lodash-es';
|
|
3
|
+
|
|
4
|
+
import { ACTIONS_COLUMN_ID, MEMOIZED_HANDLE_ID } from './constants';
|
|
5
|
+
import { DataTableRowActionCell } from './DataTableRowActionCell';
|
|
6
|
+
|
|
7
|
+
import type { DataTableStoreParams, MemoizedHandle } from './types';
|
|
8
|
+
|
|
9
|
+
function applyUpdater<T>(updater: Updater<T>, current: T): T {
|
|
10
|
+
return typeof updater === 'function' ? (updater as (prev: T) => T)(current) : updater;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function calculateColumnSizing(table: Table<any>, containerWidth: number) {
|
|
14
|
+
const updatedColumnSizing: ColumnSizingState = {};
|
|
15
|
+
|
|
16
|
+
const visibleCenterLeafColumns = table.getCenterLeafColumns().filter((column) => column.getIsVisible());
|
|
17
|
+
const visibleCenterLeafColumnIds = visibleCenterLeafColumns.map((column) => column.id);
|
|
18
|
+
const visibleNonCenteredLeafColumns = table.getVisibleLeafColumns().filter((column) => {
|
|
19
|
+
return !visibleCenterLeafColumnIds.includes(column.id);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
visibleNonCenteredLeafColumns.forEach((column) => {
|
|
23
|
+
const defaultSize = column.columnDef.size;
|
|
24
|
+
if (!defaultSize) {
|
|
25
|
+
console.error(`Size must be specified for pinned column with ID '${column.id}', defaulting to 200px`);
|
|
26
|
+
updatedColumnSizing[column.id] = 200;
|
|
27
|
+
} else {
|
|
28
|
+
updatedColumnSizing[column.id] = defaultSize;
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const nonCenteredColumnsSize = sum(Object.values(updatedColumnSizing));
|
|
33
|
+
const availableCenterSize = containerWidth - nonCenteredColumnsSize;
|
|
34
|
+
let maxCenterColumns: number;
|
|
35
|
+
if (containerWidth < 512) {
|
|
36
|
+
maxCenterColumns = 1;
|
|
37
|
+
} else if (containerWidth < 768) {
|
|
38
|
+
maxCenterColumns = 2;
|
|
39
|
+
} else if (containerWidth < 1024) {
|
|
40
|
+
maxCenterColumns = 3;
|
|
41
|
+
} else if (containerWidth < 1280) {
|
|
42
|
+
maxCenterColumns = 4;
|
|
43
|
+
} else {
|
|
44
|
+
maxCenterColumns = 5;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const centerColumnsToDisplay = Math.min(visibleCenterLeafColumns.length, maxCenterColumns);
|
|
48
|
+
if (centerColumnsToDisplay) {
|
|
49
|
+
visibleCenterLeafColumns.forEach((column) => {
|
|
50
|
+
updatedColumnSizing[column.id] = availableCenterSize / centerColumnsToDisplay;
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
visibleNonCenteredLeafColumns.forEach((column) => {
|
|
54
|
+
updatedColumnSizing[column.id] = containerWidth / visibleNonCenteredLeafColumns.length;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return updatedColumnSizing;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function defineMemoizedHandle<T extends (...args: any[]) => any>(target: T) {
|
|
62
|
+
const handle = target as MemoizedHandle<T>;
|
|
63
|
+
handle[MEMOIZED_HANDLE_ID] = Symbol();
|
|
64
|
+
handle.invalidate = function () {
|
|
65
|
+
this[MEMOIZED_HANDLE_ID] = Symbol();
|
|
66
|
+
};
|
|
67
|
+
return handle;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function flexRender<TProps extends object>(
|
|
71
|
+
Comp: React.ComponentType<TProps> | React.ReactNode,
|
|
72
|
+
props: TProps
|
|
73
|
+
): React.JSX.Element | React.ReactNode {
|
|
74
|
+
return !Comp ? null : isReactComponent<TProps>(Comp) ? <Comp {...props} /> : Comp;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getColumnsWithActions<T extends RowData>({ columns, rowActions }: DataTableStoreParams<T>): ColumnDef<T>[] {
|
|
78
|
+
if (!rowActions) {
|
|
79
|
+
return columns;
|
|
80
|
+
}
|
|
81
|
+
return [
|
|
82
|
+
...columns,
|
|
83
|
+
{
|
|
84
|
+
cell: DataTableRowActionCell,
|
|
85
|
+
enableHiding: false,
|
|
86
|
+
enableResizing: false,
|
|
87
|
+
id: ACTIONS_COLUMN_ID,
|
|
88
|
+
size: 64
|
|
89
|
+
}
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function getTanstackTableState<T>({ initialState, rowActions }: DataTableStoreParams<T>): TableState {
|
|
94
|
+
const { columnFilters = [], columnPinning = {}, sorting = [] } = initialState ?? {};
|
|
95
|
+
const state: TableState = {
|
|
96
|
+
columnFilters,
|
|
97
|
+
columnOrder: [],
|
|
98
|
+
columnPinning,
|
|
99
|
+
columnSizing: {},
|
|
100
|
+
columnSizingInfo: {
|
|
101
|
+
columnSizingStart: [],
|
|
102
|
+
deltaOffset: null,
|
|
103
|
+
deltaPercentage: null,
|
|
104
|
+
isResizingColumn: false,
|
|
105
|
+
startOffset: null,
|
|
106
|
+
startSize: null
|
|
107
|
+
},
|
|
108
|
+
columnVisibility: {},
|
|
109
|
+
expanded: {},
|
|
110
|
+
globalFilter: undefined,
|
|
111
|
+
grouping: [],
|
|
112
|
+
pagination: {
|
|
113
|
+
pageIndex: 0,
|
|
114
|
+
pageSize: 10
|
|
115
|
+
},
|
|
116
|
+
rowPinning: {},
|
|
117
|
+
rowSelection: {},
|
|
118
|
+
sorting
|
|
119
|
+
};
|
|
120
|
+
if (rowActions) {
|
|
121
|
+
state.columnPinning.right ??= [];
|
|
122
|
+
state.columnPinning.right.push(ACTIONS_COLUMN_ID);
|
|
123
|
+
}
|
|
124
|
+
return state;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isReactComponent<TProps>(component: unknown): component is React.ComponentType<TProps> {
|
|
128
|
+
return typeof component === 'function';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export {
|
|
132
|
+
applyUpdater,
|
|
133
|
+
calculateColumnSizing,
|
|
134
|
+
defineMemoizedHandle,
|
|
135
|
+
flexRender,
|
|
136
|
+
getColumnsWithActions,
|
|
137
|
+
getTanstackTableState
|
|
138
|
+
};
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
import { act, renderHook } from '@testing-library/react';
|
|
2
|
-
import { afterEach,
|
|
3
|
-
import * as zustand from 'zustand';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
4
3
|
|
|
5
4
|
import { useDestructiveActionStore } from './useDestructiveActionStore';
|
|
6
5
|
|
|
7
6
|
import type { DestructiveAction, DestructiveActionOptions } from './useDestructiveActionStore';
|
|
8
7
|
|
|
9
8
|
describe('useDestructiveActionStore', () => {
|
|
10
|
-
beforeAll(() => {
|
|
11
|
-
vi.spyOn(zustand, 'create');
|
|
12
|
-
});
|
|
13
|
-
|
|
14
9
|
afterEach(() => {
|
|
15
|
-
|
|
10
|
+
useDestructiveActionStore.setState(useDestructiveActionStore.getInitialState());
|
|
16
11
|
});
|
|
17
12
|
|
|
18
13
|
it('should render and return an object', () => {
|
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
import { act, renderHook } from '@testing-library/react';
|
|
2
|
-
import {
|
|
3
|
-
import * as zustand from 'zustand';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
4
3
|
|
|
5
4
|
import { useNotificationsStore } from './useNotificationsStore';
|
|
6
5
|
|
|
7
6
|
describe('useNotificationsStore', () => {
|
|
8
|
-
beforeAll(() => {
|
|
9
|
-
vi.spyOn(zustand, 'create');
|
|
10
|
-
});
|
|
11
|
-
afterEach(() => {
|
|
12
|
-
vi.clearAllMocks();
|
|
13
|
-
});
|
|
14
7
|
it('should render and return an object', () => {
|
|
15
8
|
const { result } = renderHook(() => useNotificationsStore());
|
|
16
9
|
expect(result.current).toBeTypeOf('object');
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { cleanup } from '@testing-library/react';
|
|
2
|
-
import { afterEach
|
|
2
|
+
import { afterEach } from 'vitest';
|
|
3
3
|
|
|
4
4
|
import { i18n } from '@/i18n';
|
|
5
5
|
|
|
6
6
|
import '@testing-library/jest-dom/vitest';
|
|
7
7
|
|
|
8
|
-
vi.mock('zustand');
|
|
9
|
-
|
|
10
8
|
i18n.init({ translations: {} });
|
|
11
9
|
|
|
12
10
|
// Since we're not using vitest globals, we need to explicitly call cleanup()
|