@eml-payments/ui-kit 1.3.2 → 1.4.3
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/index.css +1 -1
- package/dist/src/components/Alert/Alert.js +9 -5
- package/dist/src/components/Button/Button.js +1 -1
- package/dist/src/components/Button/Button.stories.d.ts +2 -0
- package/dist/src/components/Button/Button.stories.js +7 -1
- package/dist/src/components/Button/Button.types.d.ts +1 -1
- package/dist/src/components/Button/ButtonVariants.js +9 -9
- package/dist/src/components/DatePicker/DatePicker.js +7 -3
- package/dist/src/components/Dialog/DialogContainer.stories.js +5 -4
- package/dist/src/components/Dropdown/Dropdown.stories.js +11 -9
- package/dist/src/components/DropdownWrapper/DropdownWrapper.stories.js +20 -18
- package/dist/src/components/Table/BaseTable/BaseTable.d.ts +2 -0
- package/dist/src/components/Table/BaseTable/BaseTable.js +8 -0
- package/dist/src/components/Table/BaseTable/BaseTable.stories.d.ts +7 -0
- package/dist/src/components/Table/BaseTable/BaseTable.stories.js +42 -0
- package/dist/src/components/Table/BaseTable/BaseTable.types.d.ts +0 -0
- package/dist/src/components/Table/BaseTable/BaseTable.types.js +1 -0
- package/dist/src/components/Table/BaseTable/TableHeader.d.ts +7 -0
- package/dist/src/components/Table/BaseTable/TableHeader.js +15 -0
- package/dist/src/components/Table/BaseTable/index.d.ts +1 -0
- package/dist/src/components/Table/BaseTable/index.js +1 -0
- package/dist/src/components/Table/InfiniteTable/InfiniteTable.d.ts +4 -0
- package/dist/src/components/Table/InfiniteTable/InfiniteTable.js +49 -0
- package/dist/src/components/Table/InfiniteTable/InfiniteTable.props.d.ts +32 -0
- package/dist/src/components/Table/InfiniteTable/InfiniteTable.props.js +1 -0
- package/dist/src/components/Table/InfiniteTable/InfiniteTable.stories.d.ts +8 -0
- package/dist/src/components/Table/InfiniteTable/InfiniteTable.stories.js +109 -0
- package/dist/src/components/Table/InfiniteTable/index.d.ts +2 -0
- package/dist/src/components/Table/InfiniteTable/index.js +1 -0
- package/dist/src/components/Table/InfiniteTable/useInfiniteScrollTrigger.d.ts +12 -0
- package/dist/src/components/Table/InfiniteTable/useInfiniteScrollTrigger.js +31 -0
- package/dist/src/components/Table/InfiniteTable/useInfiniteScrolling.d.ts +29 -0
- package/dist/src/components/Table/InfiniteTable/useInfiniteScrolling.js +103 -0
- package/dist/src/components/Table/InfiniteTable/useInfiniteTableController.d.ts +14 -0
- package/dist/src/components/Table/InfiniteTable/useInfiniteTableController.js +76 -0
- package/dist/src/components/Table/Pagination/PageSizeSelector.d.ts +2 -0
- package/dist/src/components/Table/Pagination/PageSizeSelector.js +9 -0
- package/dist/src/components/Table/Pagination/Pagination.types.d.ts +21 -0
- package/dist/src/components/Table/Pagination/Pagination.types.js +1 -0
- package/dist/src/components/Table/Pagination/PaginationFooter.d.ts +2 -0
- package/dist/src/components/Table/Pagination/PaginationFooter.js +10 -0
- package/dist/src/components/Table/Pagination/index.d.ts +3 -2
- package/dist/src/components/Table/Pagination/index.js +3 -2
- package/dist/src/components/Table/Pagination/usePaginationController.d.ts +16 -0
- package/dist/src/components/Table/Pagination/usePaginationController.js +33 -0
- package/dist/src/components/Table/StandardTable/StandardTable.d.ts +4 -0
- package/dist/src/components/Table/StandardTable/StandardTable.js +47 -0
- package/dist/src/components/Table/StandardTable/StandardTable.stories.d.ts +25 -0
- package/dist/src/components/Table/StandardTable/StandardTable.stories.js +268 -0
- package/dist/src/components/Table/StandardTable/StandardTable.types.d.ts +4 -0
- package/dist/src/components/Table/StandardTable/StandardTable.types.js +1 -0
- package/dist/src/components/Table/StandardTable/index.d.ts +2 -0
- package/dist/src/components/Table/StandardTable/index.js +1 -0
- package/dist/src/components/Table/StandardTable/useStandardTableController.d.ts +22 -0
- package/dist/src/components/Table/StandardTable/useStandardTableController.js +100 -0
- package/dist/src/components/Table/Table.types.d.ts +72 -29
- package/dist/src/components/Table/body/Body.types.d.ts +0 -0
- package/dist/src/components/Table/body/Body.types.js +1 -0
- package/dist/src/components/Table/body/renderGroupRow.d.ts +13 -0
- package/dist/src/components/Table/body/renderGroupRow.js +14 -0
- package/dist/src/components/Table/body/renderLeafRow.d.ts +11 -0
- package/dist/src/components/Table/body/renderLeafRow.js +17 -0
- package/dist/src/components/Table/body/renderTableBody.d.ts +22 -0
- package/dist/src/components/Table/body/renderTableBody.js +21 -0
- package/dist/src/components/Table/body/utils/getColSpan.d.ts +2 -0
- package/dist/src/components/Table/body/utils/getColSpan.js +4 -0
- package/dist/src/components/Table/body/utils/getTableBodyState.d.ts +16 -0
- package/dist/src/components/Table/body/utils/getTableBodyState.js +9 -0
- package/dist/src/components/Table/body/utils/index.d.ts +2 -0
- package/dist/src/components/Table/body/utils/index.js +2 -0
- package/dist/src/components/Table/hooks/useTableColumns.d.ts +2 -0
- package/dist/src/components/Table/hooks/useTableColumns.js +5 -0
- package/dist/src/components/Table/hooks/useTableLayout.d.ts +5 -0
- package/dist/src/components/Table/hooks/useTableLayout.js +21 -0
- package/dist/src/components/Table/hooks/useTableSorting.d.ts +5 -0
- package/dist/src/components/Table/hooks/useTableSorting.js +14 -0
- package/dist/src/components/Table/hooks/useUrlPaginationSync.d.ts +4 -5
- package/dist/src/components/Table/hooks/useUrlPaginationSync.js +23 -9
- package/dist/src/components/Table/index.d.ts +5 -2
- package/dist/src/components/Table/index.js +2 -2
- package/dist/src/components/Table/render-rows/renderTableRows.d.ts +22 -0
- package/dist/src/components/Table/render-rows/renderTableRows.js +31 -0
- package/dist/src/components/Table/table.helpers.d.ts +5 -2
- package/dist/src/components/Table/table.helpers.js +13 -7
- package/dist/src/components/UICreditCard/UICreditCard.d.ts +1 -1
- package/dist/src/components/UICreditCard/UICreditCard.js +4 -2
- package/dist/src/components/UICreditCard/UICreditCard.stories.d.ts +1 -0
- package/dist/src/components/UICreditCard/UICreditCard.stories.js +12 -4
- package/dist/src/components/UICreditCard/UICreditCard.types.d.ts +3 -0
- package/package.json +3 -3
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { PaginationFooter } from '../Pagination';
|
|
3
|
+
import { BaseTable } from '../BaseTable/BaseTable';
|
|
4
|
+
import { renderTableBody } from '../body/renderTableBody';
|
|
5
|
+
import { useStandardTableController } from './useStandardTableController';
|
|
6
|
+
import { getTableBodyState, getColSpan } from '../body/utils';
|
|
7
|
+
import { useMemo } from 'react';
|
|
8
|
+
export function StandardTable(props) {
|
|
9
|
+
var _a;
|
|
10
|
+
const { table, height, showHeader, parentScrollRef, pagination, searchBelowMinLength, hasCachedData } = useStandardTableController(props);
|
|
11
|
+
const rows = table.getRowModel().rows;
|
|
12
|
+
const minLen = (_a = props.minSearchLength) !== null && _a !== void 0 ? _a : 3;
|
|
13
|
+
const getNoResultsMessage = () => {
|
|
14
|
+
var _a, _b;
|
|
15
|
+
if (searchBelowMinLength) {
|
|
16
|
+
return `Please enter at least ${minLen} characters to search`;
|
|
17
|
+
}
|
|
18
|
+
if (props.isSearchActive) {
|
|
19
|
+
return (_a = props.noSearchResultsMessage) !== null && _a !== void 0 ? _a : 'No results meet your search criteria';
|
|
20
|
+
}
|
|
21
|
+
return (_b = props.noRowsMessage) !== null && _b !== void 0 ? _b : 'No rows to display';
|
|
22
|
+
};
|
|
23
|
+
const noResultsMessage = getNoResultsMessage();
|
|
24
|
+
const colSpan = getColSpan(table);
|
|
25
|
+
const bodyState = searchBelowMinLength
|
|
26
|
+
? { type: 'empty', message: noResultsMessage }
|
|
27
|
+
: getTableBodyState({
|
|
28
|
+
isLoading: props.isLoading && !hasCachedData,
|
|
29
|
+
rows,
|
|
30
|
+
noResultsMessage,
|
|
31
|
+
});
|
|
32
|
+
const totalRows = useMemo(() => {
|
|
33
|
+
var _a;
|
|
34
|
+
return props.paginationMode === 'server'
|
|
35
|
+
? ((_a = props.totalServerRows) !== null && _a !== void 0 ? _a : 0)
|
|
36
|
+
: table.getFilteredRowModel().rows.length;
|
|
37
|
+
}, [props.paginationMode, props.totalServerRows, table]);
|
|
38
|
+
return (_jsx("div", { ref: parentScrollRef, children: _jsx(BaseTable, { table: table, height: height, showHeader: showHeader, className: props.className, isLoading: props.isLoading, renderBody: () => renderTableBody({
|
|
39
|
+
table,
|
|
40
|
+
state: bodyState,
|
|
41
|
+
colSpan,
|
|
42
|
+
onRowClick: props.onRowClick,
|
|
43
|
+
isRowClickable: props.isRowClickable,
|
|
44
|
+
onGroupRowClick: props.onGroupRowClick,
|
|
45
|
+
tableActionsDropdown: props.tableActionsDropdown,
|
|
46
|
+
}), footer: props.paginationMode === 'client' || props.paginationMode === 'server' ? (_jsx(PaginationFooter, { tableId: props.id, pageIndex: pagination.pageIndex, pageSize: pagination.pageSize, totalRows: totalRows, canNextPage: pagination.canNextPage, canPrevPage: pagination.canPrevPage, onNextPage: pagination.onNextPage, onPrevPage: pagination.onPrevPage, onPageSizeChange: pagination.setPageSize })) : null }) }));
|
|
47
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { StandardTable } from './StandardTable';
|
|
3
|
+
type User = {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
email: string;
|
|
7
|
+
role: string;
|
|
8
|
+
};
|
|
9
|
+
declare const meta: Meta<typeof StandardTable<User>>;
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof StandardTable<User>>;
|
|
12
|
+
export declare const Basic: Story;
|
|
13
|
+
export declare const EmptyState: Story;
|
|
14
|
+
export declare const Loading: Story;
|
|
15
|
+
export declare const ClientPagination: Story;
|
|
16
|
+
export declare const ServerPagination: Story;
|
|
17
|
+
export declare const RowClick: Story;
|
|
18
|
+
export declare const ControlledSorting: Story;
|
|
19
|
+
export declare const GroupedWithClicks: Story;
|
|
20
|
+
export declare const BasicWithActions: Story;
|
|
21
|
+
export declare const WithSearch: Story;
|
|
22
|
+
export declare const ServerSideSearch: Story;
|
|
23
|
+
export declare const WithQueryParamsSync: Story;
|
|
24
|
+
export declare const ServerPaginationWithQueryParams: Story;
|
|
25
|
+
export declare const TwoTablesWithQueryParams: Story;
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
3
|
+
import { StandardTable } from './StandardTable';
|
|
4
|
+
import { SearchInput } from '../../SearchInput';
|
|
5
|
+
import { useLocation } from 'react-router-dom';
|
|
6
|
+
const data = Array.from({ length: 10 }).map((_, i) => ({
|
|
7
|
+
id: `user-${i + 1}`,
|
|
8
|
+
name: `User ${i + 1}`,
|
|
9
|
+
email: `user${i + 1}@example.com`,
|
|
10
|
+
role: i % 2 === 0 ? 'Admin' : 'User',
|
|
11
|
+
}));
|
|
12
|
+
const columns = [
|
|
13
|
+
{
|
|
14
|
+
accessorKey: 'id',
|
|
15
|
+
header: 'ID',
|
|
16
|
+
flex: 1,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
accessorKey: 'name',
|
|
20
|
+
header: 'Name',
|
|
21
|
+
flex: 1,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
accessorKey: 'email',
|
|
25
|
+
header: 'Email',
|
|
26
|
+
flex: 2,
|
|
27
|
+
cell: ({ row }) => (_jsx("a", { href: `mailto:${row.original.email}`, className: "text-blue-600 underline", children: row.original.email })),
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
accessorKey: 'role',
|
|
31
|
+
header: 'Role',
|
|
32
|
+
flex: 1,
|
|
33
|
+
cell: ({ row }) => (_jsx("span", { className: row.original.role === 'Admin' ? 'font-bold text-red-500' : undefined, children: row.original.role })),
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
const meta = {
|
|
37
|
+
title: 'UIKit/Table/StandardTable',
|
|
38
|
+
component: StandardTable,
|
|
39
|
+
tags: ['autodocs'],
|
|
40
|
+
};
|
|
41
|
+
export default meta;
|
|
42
|
+
export const Basic = {
|
|
43
|
+
render: () => _jsx(StandardTable, { id: "basic-table", data: data, columns: columns, paginationMode: "client" }),
|
|
44
|
+
};
|
|
45
|
+
export const EmptyState = {
|
|
46
|
+
render: () => (_jsx(StandardTable, { id: "empty-table", data: [], columns: columns, noRowsMessage: "No users found", paginationMode: "client" })),
|
|
47
|
+
};
|
|
48
|
+
export const Loading = {
|
|
49
|
+
render: () => _jsx(StandardTable, { id: "loading-table", data: [], columns: columns, isLoading: true, paginationMode: "client" }),
|
|
50
|
+
};
|
|
51
|
+
export const ClientPagination = {
|
|
52
|
+
render: () => (_jsx(StandardTable, { id: "client-pagination", data: data, columns: columns, paginationMode: "client", rowsPerPage: 5 })),
|
|
53
|
+
};
|
|
54
|
+
function ServerPaginationExample() {
|
|
55
|
+
const [rows, setRows] = useState([]);
|
|
56
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
57
|
+
const fetchData = useCallback(async (pageIndex, pageSize) => {
|
|
58
|
+
setIsLoading(true);
|
|
59
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
60
|
+
const start = pageIndex * pageSize;
|
|
61
|
+
const result = Array.from({ length: pageSize }).map((_, i) => {
|
|
62
|
+
const id = start + i + 1;
|
|
63
|
+
return {
|
|
64
|
+
id: `user-${id}`,
|
|
65
|
+
name: `User ${id}`,
|
|
66
|
+
email: `user${id}@example.com`,
|
|
67
|
+
role: id % 2 === 0 ? 'Admin' : 'User',
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
setRows(result);
|
|
71
|
+
setIsLoading(false);
|
|
72
|
+
}, []);
|
|
73
|
+
return (_jsx(StandardTable, { id: "server-pagination", data: rows, columns: columns, paginationMode: "server", totalServerRows: 100, onRefetch: fetchData, isLoading: isLoading }));
|
|
74
|
+
}
|
|
75
|
+
export const ServerPagination = {
|
|
76
|
+
render: () => _jsx(ServerPaginationExample, {}),
|
|
77
|
+
};
|
|
78
|
+
export const RowClick = {
|
|
79
|
+
render: () => (_jsx(StandardTable, { id: "row-click", data: data, columns: columns, isRowClickable: (row) => row.role !== 'Admin', onRowClick: (row) => {
|
|
80
|
+
alert(`Clicked ${row.name}`);
|
|
81
|
+
}, paginationMode: "client" })),
|
|
82
|
+
};
|
|
83
|
+
function ControlledSortingExample() {
|
|
84
|
+
const [sorting, setSorting] = useState([{ id: 'name', desc: true }]);
|
|
85
|
+
return (_jsx(StandardTable, { id: "controlled-sorting", data: data, columns: columns, sorting: sorting, onSortingChange: setSorting, paginationMode: "client" }));
|
|
86
|
+
}
|
|
87
|
+
export const ControlledSorting = {
|
|
88
|
+
render: () => _jsx(ControlledSortingExample, {}),
|
|
89
|
+
};
|
|
90
|
+
const dates = [
|
|
91
|
+
new Date().toISOString(),
|
|
92
|
+
new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
93
|
+
new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(),
|
|
94
|
+
];
|
|
95
|
+
function getRandomInt(max) {
|
|
96
|
+
if (max <= 0) {
|
|
97
|
+
throw new Error('max must be positive');
|
|
98
|
+
}
|
|
99
|
+
const maxUint32 = 0xffffffff;
|
|
100
|
+
const acceptableRange = Math.floor(maxUint32 / max) * max;
|
|
101
|
+
while (true) {
|
|
102
|
+
const value = crypto.getRandomValues(new Uint32Array(1))[0];
|
|
103
|
+
if (value < acceptableRange) {
|
|
104
|
+
return value % max;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const groupingData = Array.from({ length: 5 }).map((_, i) => ({
|
|
109
|
+
id: `row-${i + 1}`,
|
|
110
|
+
date: dates[i % dates.length],
|
|
111
|
+
name: `User ${i + 1}`,
|
|
112
|
+
amount: getRandomInt(1000) + 100,
|
|
113
|
+
}));
|
|
114
|
+
const groupingColumns = [
|
|
115
|
+
{
|
|
116
|
+
accessorKey: 'name',
|
|
117
|
+
header: 'Name',
|
|
118
|
+
flex: 1,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
accessorKey: 'amount',
|
|
122
|
+
header: 'Amount',
|
|
123
|
+
flex: 1,
|
|
124
|
+
cell: ({ row }) => (_jsxs("span", { style: { textAlign: 'right', display: 'block' }, children: ["$", row.original.amount.toFixed(2)] })),
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
accessorKey: 'date',
|
|
128
|
+
header: 'Last Updated',
|
|
129
|
+
flex: 1,
|
|
130
|
+
enableGrouping: true,
|
|
131
|
+
getGroupingValue: (row) => new Date(row.date).toLocaleDateString('en-GB'),
|
|
132
|
+
meta: {
|
|
133
|
+
bgColor: '#f5f5f5',
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
export const GroupedWithClicks = {
|
|
138
|
+
render: () => (_jsx(StandardTable, { id: "grouped-click-handlers", data: groupingData, columns: groupingColumns, showHeader: false, grouping: ['date'], paginationMode: "client", onRowClick: (row) => {
|
|
139
|
+
alert(`Row clicked: ${row.name} ($${row.amount})`);
|
|
140
|
+
}, onGroupRowClick: ({ value, rows }) => {
|
|
141
|
+
const total = rows.reduce((sum, r) => sum + r.amount, 0);
|
|
142
|
+
alert(`Group ${value}\n` + `Rows: ${rows.length}\n` + `Total amount: $${total}`);
|
|
143
|
+
} })),
|
|
144
|
+
};
|
|
145
|
+
export const BasicWithActions = {
|
|
146
|
+
render: () => (_jsx(StandardTable, { id: "table-with-actions", data: data, columns: columns, paginationMode: "client", tableActionsDropdown: {
|
|
147
|
+
getOptions: (row) => [
|
|
148
|
+
{ label: `Edit ${row.name}`, onClick: () => alert(`Edit ${row.name}`) },
|
|
149
|
+
{ label: `Delete ${row.name}`, onClick: () => alert(`Delete ${row.name}`) },
|
|
150
|
+
],
|
|
151
|
+
isDisabled: (row) => row.role === 'Admin',
|
|
152
|
+
} })),
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Reusable hook for client-side search with minimum character gating.
|
|
156
|
+
* The table only needs `isSearchActive` — filtering is done externally.
|
|
157
|
+
*/
|
|
158
|
+
function useSearch(sourceData, selector, minChars = 3) {
|
|
159
|
+
const [query, setQuery] = useState('');
|
|
160
|
+
const isSearchActive = query.trim().length >= minChars;
|
|
161
|
+
const filteredData = useMemo(() => {
|
|
162
|
+
if (!isSearchActive)
|
|
163
|
+
return sourceData;
|
|
164
|
+
const lowerQuery = query.toLowerCase();
|
|
165
|
+
return sourceData.filter((item) => selector(item).toLowerCase().includes(lowerQuery));
|
|
166
|
+
}, [query, isSearchActive, sourceData, selector]);
|
|
167
|
+
return { query, setQuery, filteredData, isSearchActive };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Client-side search using SearchInput + useSearch.
|
|
171
|
+
* The table receives pre-filtered data and `isSearchActive` for the empty message.
|
|
172
|
+
* No `searchQuery` / `minSearchLength` needed — filtering happens outside the table.
|
|
173
|
+
*/
|
|
174
|
+
function WithSearchExample() {
|
|
175
|
+
const { setQuery, filteredData, isSearchActive } = useSearch(data, (user) => [user.name, user.email, user.role].join(' '));
|
|
176
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { style: { width: '300px' }, children: _jsx(SearchInput, { onSearch: (q) => setQuery(q), onClear: () => setQuery(''), placeholder: "Search by name..." }) }), _jsx(StandardTable, { id: "search-table", paginationMode: "client", data: filteredData, columns: columns, isSearchActive: isSearchActive, rowsPerPage: 5 })] }));
|
|
177
|
+
}
|
|
178
|
+
export const WithSearch = {
|
|
179
|
+
render: () => _jsx(WithSearchExample, {}),
|
|
180
|
+
};
|
|
181
|
+
function ServerSideSearchExample() {
|
|
182
|
+
const [rows, setRows] = useState([]);
|
|
183
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
184
|
+
const [query, setQuery] = useState('');
|
|
185
|
+
const [total, setTotal] = useState(0);
|
|
186
|
+
const fetchData = useCallback(async (pageIndex, pageSize) => {
|
|
187
|
+
setIsLoading(true);
|
|
188
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
189
|
+
const filtered = data.filter((user) => [user.name, user.email, user.role].join(' ').toLowerCase().includes(query.toLowerCase()));
|
|
190
|
+
const start = pageIndex * pageSize;
|
|
191
|
+
const paginated = filtered.slice(start, start + pageSize);
|
|
192
|
+
setRows(paginated);
|
|
193
|
+
setTotal(filtered.length);
|
|
194
|
+
setIsLoading(false);
|
|
195
|
+
}, [query]);
|
|
196
|
+
return (_jsxs("div", { className: "space-y-4", children: [_jsx("div", { style: { width: '300px' }, children: _jsx(SearchInput, { onSearch: (q) => setQuery(q), onClear: () => setQuery(''), placeholder: "Search users..." }) }), _jsx(StandardTable, { id: "server-search-table", data: rows, columns: columns, paginationMode: "server", rowsPerPage: 5, totalServerRows: total, onRefetch: fetchData, isLoading: isLoading, isSearchActive: query.trim().length >= 3, noSearchResultsMessage: "No users match your search" })] }));
|
|
197
|
+
}
|
|
198
|
+
export const ServerSideSearch = {
|
|
199
|
+
render: () => _jsx(ServerSideSearchExample, {}),
|
|
200
|
+
};
|
|
201
|
+
const CurrentUrl = () => {
|
|
202
|
+
const location = useLocation();
|
|
203
|
+
return (_jsxs("div", { style: { fontSize: 12, marginBottom: 8 }, children: ["Current URL: ", _jsx("code", { children: location.search })] }));
|
|
204
|
+
};
|
|
205
|
+
export const WithQueryParamsSync = {
|
|
206
|
+
decorators: [
|
|
207
|
+
(Story) => {
|
|
208
|
+
const query = `?clients_pageNo=2&clients_pageSize=5`;
|
|
209
|
+
globalThis.history.replaceState({}, '', query);
|
|
210
|
+
return _jsx(Story, {});
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
render: () => (_jsxs(_Fragment, { children: [_jsx(CurrentUrl, {}), _jsx(StandardTable, { id: "clients", data: data, columns: columns, paginationMode: "client" })] })),
|
|
214
|
+
};
|
|
215
|
+
/** Simulates a server fetch with latency. Used by server-side query-param stories. */
|
|
216
|
+
async function fetchServerData(pageIndex, pageSize) {
|
|
217
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
218
|
+
const start = pageIndex * pageSize;
|
|
219
|
+
return Array.from({ length: pageSize }).map((_, i) => {
|
|
220
|
+
const id = start + i + 1;
|
|
221
|
+
return {
|
|
222
|
+
id: `user-${id}`,
|
|
223
|
+
name: `User ${id}`,
|
|
224
|
+
email: `user${id}@example.com`,
|
|
225
|
+
role: id % 2 === 0 ? 'Admin' : 'User',
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
function ServerPaginationWithQueryParamsExample() {
|
|
230
|
+
const [rows, setRows] = useState([]);
|
|
231
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
232
|
+
const fetchData = useCallback(async (pageIndex, pageSize) => {
|
|
233
|
+
setIsLoading(true);
|
|
234
|
+
const results = await fetchServerData(pageIndex, pageSize);
|
|
235
|
+
setRows(results);
|
|
236
|
+
setIsLoading(false);
|
|
237
|
+
}, []);
|
|
238
|
+
return (_jsxs(_Fragment, { children: [_jsx(CurrentUrl, {}), _jsx(StandardTable, { id: "clients", data: rows, columns: columns, paginationMode: "server", totalServerRows: 100, onRefetch: fetchData, isLoading: isLoading })] }));
|
|
239
|
+
}
|
|
240
|
+
export const ServerPaginationWithQueryParams = {
|
|
241
|
+
decorators: [
|
|
242
|
+
(Story) => {
|
|
243
|
+
globalThis.history.replaceState({}, '', '?clients_pageNo=2&clients_pageSize=5');
|
|
244
|
+
return _jsx(Story, {});
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
render: () => _jsx(ServerPaginationWithQueryParamsExample, {}),
|
|
248
|
+
};
|
|
249
|
+
function TwoTablesWithQueryParamsExample() {
|
|
250
|
+
const [usersRows, setUsersRows] = useState([]);
|
|
251
|
+
const [isLoadingUsers, setIsLoadingUsers] = useState(false);
|
|
252
|
+
const fetchUsersData = useCallback(async (pageIndex, pageSize) => {
|
|
253
|
+
setIsLoadingUsers(true);
|
|
254
|
+
const results = await fetchServerData(pageIndex, pageSize);
|
|
255
|
+
setUsersRows(results);
|
|
256
|
+
setIsLoadingUsers(false);
|
|
257
|
+
}, []);
|
|
258
|
+
return (_jsxs(_Fragment, { children: [_jsx(CurrentUrl, {}), _jsxs("div", { style: { display: 'flex', gap: '2rem' }, children: [_jsx(StandardTable, { id: "clients", data: data, columns: columns, paginationMode: "client" }), _jsx(StandardTable, { id: "users", data: usersRows, columns: columns, paginationMode: "server", totalServerRows: 100, onRefetch: fetchUsersData, isLoading: isLoadingUsers })] })] }));
|
|
259
|
+
}
|
|
260
|
+
export const TwoTablesWithQueryParams = {
|
|
261
|
+
decorators: [
|
|
262
|
+
(Story) => {
|
|
263
|
+
globalThis.history.replaceState({}, '', '?clients_pageNo=2&clients_pageSize=5&users_pageNo=3&users_pageSize=10');
|
|
264
|
+
return _jsx(Story, {});
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
render: () => _jsx(TwoTablesWithQueryParamsExample, {}),
|
|
268
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { StandardTable } from './StandardTable';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { StandardTableProps } from '../Table.types';
|
|
2
|
+
export declare function useStandardTableController<T extends {
|
|
3
|
+
id: string;
|
|
4
|
+
}>(props: StandardTableProps<T>): {
|
|
5
|
+
table: import("@tanstack/table-core").Table<T>;
|
|
6
|
+
height: string | number | undefined;
|
|
7
|
+
showHeader: boolean;
|
|
8
|
+
parentScrollRef: import("react").MutableRefObject<HTMLDivElement | null>;
|
|
9
|
+
setPageIndex: import("react").Dispatch<import("react").SetStateAction<number>>;
|
|
10
|
+
setPageSize: import("react").Dispatch<import("react").SetStateAction<number>>;
|
|
11
|
+
pagination: {
|
|
12
|
+
pageIndex: number;
|
|
13
|
+
pageSize: number;
|
|
14
|
+
canNextPage: boolean;
|
|
15
|
+
canPrevPage: boolean;
|
|
16
|
+
onNextPage: () => void;
|
|
17
|
+
onPrevPage: () => void;
|
|
18
|
+
setPageSize: (size: number) => void;
|
|
19
|
+
};
|
|
20
|
+
searchBelowMinLength: boolean;
|
|
21
|
+
hasCachedData: boolean;
|
|
22
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useReactTable, getCoreRowModel, getSortedRowModel, getPaginationRowModel, getGroupedRowModel, getExpandedRowModel, } from '@tanstack/react-table';
|
|
3
|
+
import { applyFlexSizes } from '../table.helpers';
|
|
4
|
+
import { useUrlPaginationSync } from '../hooks/useUrlPaginationSync';
|
|
5
|
+
import { usePaginationController } from '../Pagination/usePaginationController';
|
|
6
|
+
export function useStandardTableController(props) {
|
|
7
|
+
const { id, data, columns, rowIdKey = 'id', height, showHeader = true,
|
|
8
|
+
// pagination
|
|
9
|
+
paginationMode = 'client', rowsPerPage = 10, totalServerRows, onRefetch,
|
|
10
|
+
// sorting / grouping
|
|
11
|
+
sorting, onSortingChange, grouping,
|
|
12
|
+
// search
|
|
13
|
+
searchQuery, minSearchLength = 3, tableActionsDropdown, } = props;
|
|
14
|
+
// Keep previous data while loading to prevent UI jumps on page change
|
|
15
|
+
const prevDataRef = useRef([]);
|
|
16
|
+
// Always cache the latest non-empty data
|
|
17
|
+
if (data.length > 0) {
|
|
18
|
+
prevDataRef.current = data;
|
|
19
|
+
}
|
|
20
|
+
// While loading, show cached data; otherwise show current data
|
|
21
|
+
const hasCachedData = Boolean(props.isLoading && prevDataRef.current.length > 0);
|
|
22
|
+
const displayData = hasCachedData ? prevDataRef.current : data;
|
|
23
|
+
// When a searchQuery is provided but below the minimum length, suppress refetch calls
|
|
24
|
+
const searchBelowMinLength = searchQuery !== undefined && searchQuery.length > 0 && searchQuery.length < minSearchLength;
|
|
25
|
+
const gatedOnRefetch = useCallback((pageIndex, pageSize) => {
|
|
26
|
+
if (searchBelowMinLength) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
onRefetch === null || onRefetch === void 0 ? void 0 : onRefetch(pageIndex, pageSize);
|
|
30
|
+
}, [onRefetch, searchBelowMinLength]);
|
|
31
|
+
const parentScrollRef = useRef(null);
|
|
32
|
+
const [containerWidth, setContainerWidth] = useState(0);
|
|
33
|
+
useLayoutEffect(() => {
|
|
34
|
+
if (!parentScrollRef.current) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const ro = new ResizeObserver(([entry]) => {
|
|
38
|
+
setContainerWidth(entry.contentRect.width);
|
|
39
|
+
});
|
|
40
|
+
ro.observe(parentScrollRef.current);
|
|
41
|
+
return () => ro.disconnect();
|
|
42
|
+
}, []);
|
|
43
|
+
const { initialPageIndex, initialPageSize, syncToUrl } = useUrlPaginationSync(id, rowsPerPage);
|
|
44
|
+
const [pageIndex, setPageIndex] = useState(initialPageIndex);
|
|
45
|
+
const [pageSize, setPageSize] = useState(initialPageSize);
|
|
46
|
+
const [internalSorting, setInternalSorting] = useState(sorting !== null && sorting !== void 0 ? sorting : []);
|
|
47
|
+
const stableGrouping = useMemo(() => grouping !== null && grouping !== void 0 ? grouping : [], [grouping]);
|
|
48
|
+
const columnVisibility = useMemo(() => Object.fromEntries(stableGrouping.map((colId) => [colId, false])), [stableGrouping]);
|
|
49
|
+
const baseColumns = useMemo(() => applyFlexSizes(columns, containerWidth, false // no virtualization in standard table
|
|
50
|
+
), [columns, containerWidth]);
|
|
51
|
+
const table = useReactTable({
|
|
52
|
+
data: displayData,
|
|
53
|
+
columns: baseColumns,
|
|
54
|
+
getRowId: (row) => String(row[rowIdKey]),
|
|
55
|
+
state: {
|
|
56
|
+
pagination: { pageIndex, pageSize },
|
|
57
|
+
sorting: sorting !== null && sorting !== void 0 ? sorting : internalSorting,
|
|
58
|
+
grouping: stableGrouping,
|
|
59
|
+
columnVisibility,
|
|
60
|
+
},
|
|
61
|
+
getCoreRowModel: getCoreRowModel(),
|
|
62
|
+
getSortedRowModel: getSortedRowModel(),
|
|
63
|
+
getGroupedRowModel: getGroupedRowModel(),
|
|
64
|
+
getExpandedRowModel: getExpandedRowModel(),
|
|
65
|
+
getPaginationRowModel: paginationMode === 'client' ? getPaginationRowModel() : undefined,
|
|
66
|
+
manualPagination: paginationMode === 'server',
|
|
67
|
+
pageCount: paginationMode === 'server' ? Math.ceil((totalServerRows !== null && totalServerRows !== void 0 ? totalServerRows : 0) / pageSize) : undefined,
|
|
68
|
+
autoResetPageIndex: false,
|
|
69
|
+
onPaginationChange: (updater) => {
|
|
70
|
+
const next = typeof updater === 'function' ? updater({ pageIndex, pageSize }) : updater;
|
|
71
|
+
setPageIndex(next.pageIndex);
|
|
72
|
+
setPageSize(next.pageSize);
|
|
73
|
+
syncToUrl(next.pageIndex, next.pageSize);
|
|
74
|
+
},
|
|
75
|
+
onSortingChange: (updater) => {
|
|
76
|
+
const next = typeof updater === 'function' ? updater(sorting !== null && sorting !== void 0 ? sorting : internalSorting) : updater;
|
|
77
|
+
setInternalSorting(next);
|
|
78
|
+
onSortingChange === null || onSortingChange === void 0 ? void 0 : onSortingChange(next);
|
|
79
|
+
},
|
|
80
|
+
meta: {
|
|
81
|
+
hasActions: Boolean(tableActionsDropdown),
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
const pagination = usePaginationController({
|
|
85
|
+
table,
|
|
86
|
+
paginationMode,
|
|
87
|
+
onRefetch: gatedOnRefetch,
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
table,
|
|
91
|
+
height,
|
|
92
|
+
showHeader,
|
|
93
|
+
parentScrollRef,
|
|
94
|
+
setPageIndex,
|
|
95
|
+
setPageSize,
|
|
96
|
+
pagination,
|
|
97
|
+
searchBelowMinLength,
|
|
98
|
+
hasCachedData,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SortingState, ColumnDef, Table } from '@tanstack/react-table';
|
|
2
2
|
import type { DropdownOption, DropdownStructure } from '../DropdownWrapper/DropdownWrapper.types';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
declare module '@tanstack/react-table' {
|
|
5
|
+
interface TableMeta<TData> {
|
|
6
|
+
hasActions?: boolean;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
3
9
|
export type FlexColumnDef<T> = ColumnDef<T> & {
|
|
4
10
|
flex?: number;
|
|
5
11
|
};
|
|
@@ -12,47 +18,84 @@ export type InfiniteScrollOptions = {
|
|
|
12
18
|
loadingMoreMessage?: string;
|
|
13
19
|
noMoreDataMessage?: string;
|
|
14
20
|
};
|
|
15
|
-
export type
|
|
16
|
-
export interface VirtualizationOptions {
|
|
21
|
+
export type VirtualizationOptions = {
|
|
17
22
|
enabled: boolean;
|
|
18
23
|
rowEstimate?: number;
|
|
19
24
|
overscan?: number;
|
|
25
|
+
};
|
|
26
|
+
export type ClientPaginationProps = {
|
|
27
|
+
paginationMode?: 'client' | 'none';
|
|
28
|
+
rowsPerPage?: number;
|
|
29
|
+
onRefetch?: never;
|
|
30
|
+
totalServerRows?: never;
|
|
31
|
+
};
|
|
32
|
+
export type ServerPaginationProps = {
|
|
33
|
+
paginationMode: 'server';
|
|
34
|
+
rowsPerPage?: number;
|
|
35
|
+
onRefetch: (pageIndex: number, pageSize: number) => void;
|
|
36
|
+
totalServerRows: number;
|
|
37
|
+
};
|
|
38
|
+
export type StandardPaginationProps = ClientPaginationProps | ServerPaginationProps;
|
|
39
|
+
export interface ITableActionsDropdownProps<T> {
|
|
40
|
+
getOptions: (row: T) => DropdownOption[];
|
|
41
|
+
structure?: DropdownStructure;
|
|
42
|
+
renderCustomTrigger?: (row: T) => ReactNode;
|
|
43
|
+
menuAlignment?: 'start' | 'end' | 'center';
|
|
44
|
+
isDisabled?: boolean | ((row: T) => boolean);
|
|
20
45
|
}
|
|
21
|
-
export
|
|
46
|
+
export type GroupRowClickHandler<T> = (args: {
|
|
47
|
+
columnId: string;
|
|
48
|
+
value: unknown;
|
|
49
|
+
rows: T[];
|
|
50
|
+
}) => void;
|
|
51
|
+
export interface TableInputProps<T> {
|
|
22
52
|
id: string;
|
|
23
|
-
height?: number | string;
|
|
24
53
|
data: T[];
|
|
25
54
|
columns: FlexColumnDef<T>[];
|
|
26
55
|
rowIdKey?: keyof T | 'id';
|
|
56
|
+
height?: number | string;
|
|
57
|
+
showHeader?: boolean;
|
|
27
58
|
className?: string;
|
|
28
|
-
|
|
29
|
-
showSkeletonRows?: boolean;
|
|
30
|
-
checkboxSelection?: boolean;
|
|
31
|
-
checkboxPosition?: 'start' | 'end';
|
|
59
|
+
/** Sorting */
|
|
32
60
|
sorting?: SortingState;
|
|
33
61
|
onSortingChange?: (s: SortingState) => void;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
totalServerRows?: number;
|
|
39
|
-
paginationMode?: PaginationMode;
|
|
40
|
-
noRowsMessage?: string;
|
|
41
|
-
rowsPerPage?: number;
|
|
42
|
-
selectedRowIds?: string[];
|
|
43
|
-
tableActionsDropdown?: {
|
|
44
|
-
getOptions: (row: T) => DropdownOption[];
|
|
45
|
-
structure?: DropdownStructure;
|
|
46
|
-
renderCustomTrigger?: (row: T) => React.ReactNode;
|
|
47
|
-
menuAlignment?: 'start' | 'end' | 'center' | undefined;
|
|
48
|
-
isDisabled?: boolean | ((row: T) => boolean);
|
|
49
|
-
};
|
|
62
|
+
/** Grouping */
|
|
63
|
+
grouping?: string[];
|
|
64
|
+
isLoading?: boolean;
|
|
65
|
+
/** Row interactions */
|
|
50
66
|
onRowClick?: (row: T) => void;
|
|
51
67
|
isRowClickable?: (row: T) => boolean;
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
onGroupRowClick?: GroupRowClickHandler<T>;
|
|
69
|
+
/** Search */
|
|
54
70
|
isSearchActive?: boolean;
|
|
71
|
+
searchQuery?: string;
|
|
72
|
+
minSearchLength?: number;
|
|
55
73
|
noSearchResultsMessage?: string;
|
|
56
|
-
|
|
57
|
-
|
|
74
|
+
noRowsMessage?: string;
|
|
75
|
+
/** Actions */
|
|
76
|
+
tableActionsDropdown?: ITableActionsDropdownProps<T>;
|
|
58
77
|
}
|
|
78
|
+
export interface BaseTableProps<T> {
|
|
79
|
+
table: Table<T>;
|
|
80
|
+
height?: number | string;
|
|
81
|
+
showHeader?: boolean;
|
|
82
|
+
className?: string;
|
|
83
|
+
isLoading?: boolean;
|
|
84
|
+
renderBody: () => ReactNode;
|
|
85
|
+
footer?: ReactNode;
|
|
86
|
+
containerRef?: React.Ref<HTMLDivElement>;
|
|
87
|
+
}
|
|
88
|
+
export type StandardTableProps<T extends {
|
|
89
|
+
id: string;
|
|
90
|
+
}> = TableInputProps<T> & StandardPaginationProps;
|
|
91
|
+
export type InfiniteScrollTableProps<T extends {
|
|
92
|
+
id: string;
|
|
93
|
+
}> = TableInputProps<T> & {
|
|
94
|
+
infiniteScroll: InfiniteScrollOptions;
|
|
95
|
+
virtualization?: VirtualizationOptions;
|
|
96
|
+
/** explicitly not supported */
|
|
97
|
+
paginationMode?: never;
|
|
98
|
+
rowsPerPage?: never;
|
|
99
|
+
onRefetch?: never;
|
|
100
|
+
totalServerRows?: never;
|
|
101
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Row } from '@tanstack/react-table';
|
|
2
|
+
interface RenderGroupRowProps<T> {
|
|
3
|
+
row: Row<T> & {
|
|
4
|
+
groupingColumnId: string;
|
|
5
|
+
};
|
|
6
|
+
onGroupRowClick?: (args: {
|
|
7
|
+
columnId: string;
|
|
8
|
+
value: unknown;
|
|
9
|
+
rows: T[];
|
|
10
|
+
}) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function renderGroupRow<T>({ row, onGroupRowClick }: RenderGroupRowProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { classNames } from '../../../utils';
|
|
3
|
+
export function renderGroupRow({ row, onGroupRowClick }) {
|
|
4
|
+
const columnId = row.groupingColumnId;
|
|
5
|
+
const groupValue = row.getGroupingValue(columnId);
|
|
6
|
+
const clickable = Boolean(onGroupRowClick);
|
|
7
|
+
return (_jsx("tr", { className: classNames('border-none', clickable && 'cursor-pointer hover:bg-muted/40'), onClick: clickable
|
|
8
|
+
? () => onGroupRowClick === null || onGroupRowClick === void 0 ? void 0 : onGroupRowClick({
|
|
9
|
+
columnId,
|
|
10
|
+
value: groupValue,
|
|
11
|
+
rows: row.subRows.map((r) => r.original),
|
|
12
|
+
})
|
|
13
|
+
: undefined, children: _jsx("td", { colSpan: row.getVisibleCells().length, className: "p-0", children: _jsx("div", { className: "px-4 py-4 text-sm text-muted-foreground", children: String(groupValue) }) }) }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Row } from '@tanstack/react-table';
|
|
2
|
+
import type { ITableActionsDropdownProps } from '../Table.types';
|
|
3
|
+
export declare function renderLeafRow<T extends {
|
|
4
|
+
id: string;
|
|
5
|
+
}>({ row, onRowClick, isRowClickable, hasActions, tableActionsDropdown, }: {
|
|
6
|
+
row: Row<T>;
|
|
7
|
+
onRowClick?: (row: T) => void;
|
|
8
|
+
isRowClickable?: (row: T) => boolean;
|
|
9
|
+
hasActions?: boolean;
|
|
10
|
+
tableActionsDropdown?: ITableActionsDropdownProps<T>;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|