jattac.libs.web.responsive-table 0.9.2 → 0.11.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/README.md +47 -11
- package/dist/Context/TableContext.d.ts +1 -0
- package/dist/Hooks/useTableDataSource.d.ts +10 -0
- package/dist/Hooks/useTablePlugins.d.ts +2 -0
- package/dist/Plugins/FilterPlugin.d.ts +1 -0
- package/dist/Plugins/IResponsiveTablePlugin.d.ts +2 -0
- package/dist/UI/ResponsiveTable.d.ts +17 -5
- package/dist/index.d.ts +4 -3
- package/dist/index.es.js +137 -23
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +136 -21
- package/dist/index.js.map +1 -1
- package/docs/api.md +63 -4
- package/docs/examples.md +148 -1
- package/docs/features.md +7 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -13,45 +13,81 @@ ResponsiveTable is a high-performance, type-safe React component designed for co
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npm install jattac.libs.web.responsive-table
|
|
16
|
+
npm install jattac.libs.web.responsive-table jattac.libs.web.zest-textbox react-icons
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
21
|
+
## Built-in Filter
|
|
22
|
+
|
|
23
|
+
Enable the search box with one prop. A clear (×) button appears automatically when the field has text.
|
|
24
|
+
|
|
25
|
+
**Client-side** — filters the in-memory `data` array and highlights matches:
|
|
26
|
+
```tsx
|
|
27
|
+
<ResponsiveTable
|
|
28
|
+
data={rows}
|
|
29
|
+
columnDefinitions={columns}
|
|
30
|
+
filterProps={{ showFilter: true }}
|
|
31
|
+
/>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Server-side** — when `dataSource` is present, server mode is automatic. The table resets to page 1 and calls your fetch function with the current `filter` string on every change:
|
|
35
|
+
```tsx
|
|
36
|
+
<ResponsiveTable
|
|
37
|
+
dataSource={async ({ page, pageSize, filter }) =>
|
|
38
|
+
api.getUsers({ page, pageSize, search: filter })
|
|
39
|
+
}
|
|
40
|
+
columnDefinitions={columns}
|
|
41
|
+
filterProps={{ showFilter: true }}
|
|
42
|
+
/>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> To force client-side filtering even with a `dataSource`, pass `mode: 'client'`.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
21
49
|
## Delightful Data Fetching: Smart Data Source
|
|
22
50
|
|
|
23
|
-
The
|
|
51
|
+
The `dataSource` pattern makes handling large datasets, server-side sorting, filtering, and infinite scroll completely painless. You provide the fetch logic; the table handles bookkeeping.
|
|
24
52
|
|
|
25
|
-
###
|
|
53
|
+
### Pagination only
|
|
26
54
|
```tsx
|
|
27
55
|
<ResponsiveTable
|
|
28
56
|
dataSource={async ({ page, pageSize }) => {
|
|
29
57
|
const users = await api.getUsers({ page, pageSize });
|
|
30
|
-
return users; //
|
|
58
|
+
return users; // hasMore is auto-detected from page size
|
|
31
59
|
}}
|
|
32
60
|
columnDefinitions={columns}
|
|
33
61
|
/>
|
|
34
62
|
```
|
|
35
63
|
|
|
36
|
-
###
|
|
37
|
-
The table tells you exactly what it needs based on user interaction:
|
|
64
|
+
### Pagination + sorting + filtering
|
|
38
65
|
```tsx
|
|
39
66
|
<ResponsiveTable
|
|
40
|
-
dataSource={async ({ page, pageSize, sort, filter }) =>
|
|
41
|
-
|
|
67
|
+
dataSource={async ({ page, pageSize, sort, filter }) =>
|
|
68
|
+
api.getUsers({
|
|
42
69
|
page,
|
|
43
70
|
limit: pageSize,
|
|
44
71
|
sortBy: sort?.columnId,
|
|
45
72
|
order: sort?.direction,
|
|
46
|
-
search: filter
|
|
47
|
-
})
|
|
48
|
-
}
|
|
73
|
+
search: filter,
|
|
74
|
+
})
|
|
75
|
+
}
|
|
49
76
|
columnDefinitions={columns}
|
|
50
77
|
sortProps={{ initialSortColumn: 'name' }}
|
|
51
78
|
filterProps={{ showFilter: true }}
|
|
52
79
|
/>
|
|
53
80
|
```
|
|
54
81
|
|
|
82
|
+
### With total count (accurate hasMore)
|
|
83
|
+
Return `{ items, totalCount }` instead of a plain array and the table derives `hasMore` precisely:
|
|
84
|
+
```tsx
|
|
85
|
+
dataSource={async ({ page, pageSize }) => {
|
|
86
|
+
const { data, total } = await api.getUsers({ page, pageSize });
|
|
87
|
+
return { items: data, totalCount: total };
|
|
88
|
+
}}
|
|
89
|
+
```
|
|
90
|
+
|
|
55
91
|
---
|
|
56
92
|
|
|
57
93
|
## Basic Implementation
|
|
@@ -9,6 +9,15 @@ interface UseTableDataSourceProps<TData> {
|
|
|
9
9
|
};
|
|
10
10
|
filter?: string;
|
|
11
11
|
}
|
|
12
|
+
export interface DataSourceState<TData> {
|
|
13
|
+
data: TData[];
|
|
14
|
+
currentPage: number;
|
|
15
|
+
hasMore: boolean;
|
|
16
|
+
totalCount?: number;
|
|
17
|
+
isLoading: boolean;
|
|
18
|
+
isFetchingMore: boolean;
|
|
19
|
+
error?: Error;
|
|
20
|
+
}
|
|
12
21
|
interface UseTableDataSourceReturn<TData> {
|
|
13
22
|
data: TData[];
|
|
14
23
|
currentPage: number;
|
|
@@ -18,6 +27,7 @@ interface UseTableDataSourceReturn<TData> {
|
|
|
18
27
|
isFetchingMore: boolean;
|
|
19
28
|
loadNextPage: () => Promise<void>;
|
|
20
29
|
resetAndFetch: () => Promise<void>;
|
|
30
|
+
error?: Error;
|
|
21
31
|
}
|
|
22
32
|
export declare const useTableDataSource: <TData>(props: UseTableDataSourceProps<TData>) => UseTableDataSourceReturn<TData>;
|
|
23
33
|
export {};
|
|
@@ -14,6 +14,7 @@ interface UseTablePluginsProps<TData> {
|
|
|
14
14
|
showFilter?: boolean;
|
|
15
15
|
filterPlaceholder?: string;
|
|
16
16
|
className?: string;
|
|
17
|
+
mode?: 'client' | 'server';
|
|
17
18
|
};
|
|
18
19
|
selectionProps?: {
|
|
19
20
|
onSelectionChange: (selectedItems: TData[]) => void;
|
|
@@ -28,6 +29,7 @@ interface UseTablePluginsProps<TData> {
|
|
|
28
29
|
columnDefinitions: (IResponsiveTableColumnDefinition<TData> | ((data: TData, rowIndex?: number) => IResponsiveTableColumnDefinition<TData>))[];
|
|
29
30
|
getScrollableElement: () => HTMLElement | null;
|
|
30
31
|
infiniteScrollProps?: IInfiniteScrollProps<TData>;
|
|
32
|
+
onFilterChange?: (filterText: string) => void;
|
|
31
33
|
}
|
|
32
34
|
interface UseTablePluginsReturn<TData> {
|
|
33
35
|
processedData: TData[];
|
|
@@ -12,4 +12,5 @@ export declare class FilterPlugin<TData> implements IResponsiveTablePlugin<TData
|
|
|
12
12
|
processData: (data: TData[]) => TData[];
|
|
13
13
|
renderCell: (content: React.ReactNode, _row: TData, _column: IResponsiveTableColumnDefinition<TData>) => React.ReactNode;
|
|
14
14
|
private handleFilterChange;
|
|
15
|
+
private handleClear;
|
|
15
16
|
}
|
|
@@ -28,6 +28,7 @@ export interface IPluginAPI<TData> {
|
|
|
28
28
|
showFilter?: boolean;
|
|
29
29
|
filterPlaceholder?: string;
|
|
30
30
|
className?: string;
|
|
31
|
+
mode?: 'client' | 'server';
|
|
31
32
|
};
|
|
32
33
|
selectionProps?: {
|
|
33
34
|
onSelectionChange: (selectedItems: TData[]) => void;
|
|
@@ -36,4 +37,5 @@ export interface IPluginAPI<TData> {
|
|
|
36
37
|
selectedItems?: TData[];
|
|
37
38
|
selectedRowClassName?: string;
|
|
38
39
|
};
|
|
40
|
+
onFilterChange?: (filterText: string) => void;
|
|
39
41
|
}
|
|
@@ -3,7 +3,13 @@ import { SortDirection } from '../Data/IResponsiveTableColumnDefinition';
|
|
|
3
3
|
import IFooterRowDefinition from '../Data/IFooterRowDefinition';
|
|
4
4
|
import { IResponsiveTablePlugin } from '../Plugins/IResponsiveTablePlugin';
|
|
5
5
|
import { ColumnDefinition, DataSource } from '../Context/TableContext';
|
|
6
|
+
import { DataSourceState } from '../Hooks/useTableDataSource';
|
|
6
7
|
export { ColumnDefinition };
|
|
8
|
+
export interface ResponsiveTableHandle<TData> {
|
|
9
|
+
loadNextPage: () => Promise<void>;
|
|
10
|
+
resetAndFetch: () => Promise<void>;
|
|
11
|
+
getState: () => DataSourceState<TData>;
|
|
12
|
+
}
|
|
7
13
|
interface IInfiniteScrollProps<TData> {
|
|
8
14
|
onLoadMore: (currentData: TData[]) => Promise<TData[] | null>;
|
|
9
15
|
hasMore?: boolean;
|
|
@@ -66,6 +72,8 @@ interface IProps<TData> {
|
|
|
66
72
|
showFilter?: boolean;
|
|
67
73
|
filterPlaceholder?: string;
|
|
68
74
|
className?: string;
|
|
75
|
+
/** Default: 'server' when dataSource is present, 'client' otherwise. Pass 'client' to force in-memory filtering even with a dataSource. */
|
|
76
|
+
mode?: 'client' | 'server';
|
|
69
77
|
};
|
|
70
78
|
/** Configuration for row selection. */
|
|
71
79
|
selectionProps?: {
|
|
@@ -84,10 +92,14 @@ interface IProps<TData> {
|
|
|
84
92
|
sortProps?: ISortProps;
|
|
85
93
|
/** Custom CSS class to apply to each card in mobile view. */
|
|
86
94
|
mobileCardClassName?: string;
|
|
95
|
+
/** Callback fired whenever the dataSource state changes (data, page, loading, error). */
|
|
96
|
+
onDataSourceStateChange?: (state: DataSourceState<TData>) => void;
|
|
97
|
+
/** Callback fired when the current page changes. */
|
|
98
|
+
onPageChange?: (page: number) => void;
|
|
99
|
+
/** Callback fired when a dataSource fetch fails. */
|
|
100
|
+
onDataSourceError?: (error: Error) => void;
|
|
87
101
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
*/
|
|
92
|
-
declare function ResponsiveTable<TData>(props: IProps<TData>): React.JSX.Element;
|
|
102
|
+
declare const ResponsiveTable: <TData>(props: IProps<TData> & {
|
|
103
|
+
ref?: React.Ref<ResponsiveTableHandle<TData>>;
|
|
104
|
+
}) => React.ReactElement;
|
|
93
105
|
export default ResponsiveTable;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import IFooterColumnDefinition from './Data/IFooterColumnDefinition';
|
|
2
2
|
import IFooterRowDefinition from './Data/IFooterRowDefinition';
|
|
3
3
|
import { IResponsiveTableColumnDefinition, SortDirection } from './Data/IResponsiveTableColumnDefinition';
|
|
4
|
-
import ResponsiveTable from './UI/ResponsiveTable';
|
|
5
|
-
import { ColumnDefinition, DataSource, IDataSourceParams, DataSourceResult } from './Context/TableContext';
|
|
4
|
+
import ResponsiveTable, { ResponsiveTableHandle } from './UI/ResponsiveTable';
|
|
5
|
+
import { ColumnDefinition, DataSource, IDataSourceParams, DataSourceResult, useTableContext } from './Context/TableContext';
|
|
6
|
+
import { DataSourceState } from './Hooks/useTableDataSource';
|
|
6
7
|
import { FilterPlugin } from './Plugins/FilterPlugin';
|
|
7
8
|
import { InfiniteScrollPlugin } from './Plugins/InfiniteScrollPlugin';
|
|
8
9
|
import { IResponsiveTablePlugin } from './Plugins/IResponsiveTablePlugin';
|
|
9
10
|
import { SortPlugin } from './Plugins/SortPlugin';
|
|
10
11
|
import { SelectionPlugin } from './Plugins/SelectionPlugin';
|
|
11
|
-
export { SortDirection, IResponsiveTableColumnDefinition, ColumnDefinition, DataSource, IDataSourceParams, DataSourceResult, IFooterColumnDefinition, IFooterRowDefinition, FilterPlugin, InfiniteScrollPlugin, IResponsiveTablePlugin, SortPlugin, SelectionPlugin, };
|
|
12
|
+
export { SortDirection, IResponsiveTableColumnDefinition, ColumnDefinition, DataSource, IDataSourceParams, DataSourceResult, IFooterColumnDefinition, IFooterRowDefinition, FilterPlugin, InfiniteScrollPlugin, IResponsiveTablePlugin, SortPlugin, SelectionPlugin, ResponsiveTableHandle, DataSourceState, useTableContext, };
|
|
12
13
|
export default ResponsiveTable;
|
package/dist/index.es.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import React, { createContext, useCallback, useMemo, useContext, useRef, useEffect, useState } from 'react';
|
|
1
|
+
import React, { createContext, useCallback, useMemo, useContext, useRef, useEffect, useState, forwardRef, useImperativeHandle } from 'react';
|
|
2
|
+
import ZestTextbox from 'jattac.libs.web.zest-textbox';
|
|
3
|
+
import { MdClose } from 'react-icons/md';
|
|
2
4
|
|
|
3
5
|
function styleInject(css, ref) {
|
|
4
6
|
if ( ref === void 0 ) ref = {};
|
|
@@ -484,18 +486,36 @@ class FilterPlugin {
|
|
|
484
486
|
this.api = api;
|
|
485
487
|
};
|
|
486
488
|
this.renderHeader = () => {
|
|
487
|
-
var _a;
|
|
489
|
+
var _a, _b;
|
|
488
490
|
if (!((_a = this.api.filterProps) === null || _a === void 0 ? void 0 : _a.showFilter)) {
|
|
489
491
|
return null;
|
|
490
492
|
}
|
|
491
|
-
return (React.createElement("div", { style: { marginBottom: '1rem' } },
|
|
492
|
-
React.createElement(
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
493
|
+
return (React.createElement("div", { style: { marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' } },
|
|
494
|
+
React.createElement(ZestTextbox, { value: this.filterText, placeholder: (_b = this.api.filterProps.filterPlaceholder) !== null && _b !== void 0 ? _b : 'Search...', onChange: this.handleFilterChange, className: this.api.filterProps.className, zest: { stretch: true } }),
|
|
495
|
+
React.createElement("button", { onClick: this.handleClear, "aria-label": "Clear filter", style: {
|
|
496
|
+
display: 'flex',
|
|
497
|
+
alignItems: 'center',
|
|
498
|
+
justifyContent: 'center',
|
|
499
|
+
minWidth: '2.75rem',
|
|
500
|
+
minHeight: '2.75rem',
|
|
501
|
+
padding: 0,
|
|
502
|
+
border: 'none',
|
|
503
|
+
borderRadius: '50%',
|
|
504
|
+
background: 'transparent',
|
|
505
|
+
cursor: 'pointer',
|
|
506
|
+
color: '#666',
|
|
507
|
+
opacity: this.filterText ? 1 : 0,
|
|
508
|
+
pointerEvents: this.filterText ? 'auto' : 'none',
|
|
509
|
+
transition: 'opacity 0.15s ease',
|
|
510
|
+
flexShrink: 0,
|
|
511
|
+
} },
|
|
512
|
+
React.createElement(MdClose, { size: 20 }))));
|
|
497
513
|
};
|
|
498
514
|
this.processData = (data) => {
|
|
515
|
+
var _a;
|
|
516
|
+
if (((_a = this.api.filterProps) === null || _a === void 0 ? void 0 : _a.mode) === 'server') {
|
|
517
|
+
return data;
|
|
518
|
+
}
|
|
499
519
|
if (!this.filterText || !this.api.columnDefinitions) {
|
|
500
520
|
return data;
|
|
501
521
|
}
|
|
@@ -522,6 +542,10 @@ class FilterPlugin {
|
|
|
522
542
|
_row,
|
|
523
543
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
524
544
|
_column) => {
|
|
545
|
+
var _a;
|
|
546
|
+
if (((_a = this.api.filterProps) === null || _a === void 0 ? void 0 : _a.mode) === 'server') {
|
|
547
|
+
return content;
|
|
548
|
+
}
|
|
525
549
|
if (!this.filterText || typeof content !== 'string') {
|
|
526
550
|
return content;
|
|
527
551
|
}
|
|
@@ -534,10 +558,21 @@ class FilterPlugin {
|
|
|
534
558
|
clearTimeout(this.debounceTimeout);
|
|
535
559
|
}
|
|
536
560
|
this.debounceTimeout = setTimeout(() => {
|
|
561
|
+
var _a, _b;
|
|
537
562
|
this.filterText = currentFilterText;
|
|
538
563
|
this.api.forceUpdate();
|
|
564
|
+
(_b = (_a = this.api).onFilterChange) === null || _b === void 0 ? void 0 : _b.call(_a, currentFilterText);
|
|
539
565
|
}, 300);
|
|
540
566
|
};
|
|
567
|
+
this.handleClear = () => {
|
|
568
|
+
var _a, _b;
|
|
569
|
+
if (this.debounceTimeout) {
|
|
570
|
+
clearTimeout(this.debounceTimeout);
|
|
571
|
+
}
|
|
572
|
+
this.filterText = '';
|
|
573
|
+
this.api.forceUpdate();
|
|
574
|
+
(_b = (_a = this.api).onFilterChange) === null || _b === void 0 ? void 0 : _b.call(_a, '');
|
|
575
|
+
};
|
|
541
576
|
}
|
|
542
577
|
}
|
|
543
578
|
|
|
@@ -757,7 +792,7 @@ class SortPlugin {
|
|
|
757
792
|
}
|
|
758
793
|
|
|
759
794
|
const useTablePlugins = (props) => {
|
|
760
|
-
const { data, plugins, filterProps, selectionProps, sortProps, columnDefinitions, getScrollableElement, infiniteScrollProps, } = props;
|
|
795
|
+
const { data, plugins, filterProps, selectionProps, sortProps, columnDefinitions, getScrollableElement, infiniteScrollProps, onFilterChange, } = props;
|
|
761
796
|
const [processedData, setProcessedData] = useState(data);
|
|
762
797
|
const [activePlugins, setActivePlugins] = useState([]);
|
|
763
798
|
// Persist internal plugins using refs to prevent state loss
|
|
@@ -831,6 +866,7 @@ const useTablePlugins = (props) => {
|
|
|
831
866
|
filterProps: filterProps,
|
|
832
867
|
selectionProps: selectionProps,
|
|
833
868
|
columnDefinitions: columnDefinitions,
|
|
869
|
+
onFilterChange: onFilterChange,
|
|
834
870
|
};
|
|
835
871
|
// Initialize/Refresh all active plugins with the current API
|
|
836
872
|
newActivePlugins.forEach((plugin) => {
|
|
@@ -856,6 +892,7 @@ const useTablePlugins = (props) => {
|
|
|
856
892
|
getScrollableElement,
|
|
857
893
|
infiniteScrollProps,
|
|
858
894
|
getRawColumnDefinition,
|
|
895
|
+
onFilterChange,
|
|
859
896
|
]);
|
|
860
897
|
const forceUpdatePlugins = useCallback(() => {
|
|
861
898
|
setProcessedData(initializePlugins());
|
|
@@ -1026,7 +1063,10 @@ const useTableDataSource = (props) => {
|
|
|
1026
1063
|
const [totalCount, setTotalCount] = useState(undefined);
|
|
1027
1064
|
const [isLoading, setIsLoading] = useState(false);
|
|
1028
1065
|
const [isFetchingMore, setIsFetchingMore] = useState(false);
|
|
1066
|
+
const [error, setError] = useState(undefined);
|
|
1029
1067
|
const isInitialMount = useRef(true);
|
|
1068
|
+
const dataLengthRef = useRef(0);
|
|
1069
|
+
dataLengthRef.current = data.length;
|
|
1030
1070
|
const fetchData = useCallback((page, isAppend) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1031
1071
|
if (!dataSource)
|
|
1032
1072
|
return;
|
|
@@ -1037,6 +1077,7 @@ const useTableDataSource = (props) => {
|
|
|
1037
1077
|
setIsLoading(true);
|
|
1038
1078
|
}
|
|
1039
1079
|
try {
|
|
1080
|
+
setError(undefined);
|
|
1040
1081
|
const params = {
|
|
1041
1082
|
page,
|
|
1042
1083
|
pageSize,
|
|
@@ -1058,7 +1099,7 @@ const useTableDataSource = (props) => {
|
|
|
1058
1099
|
setCurrentPage(page);
|
|
1059
1100
|
// Intelligent hasMore detection
|
|
1060
1101
|
if (newTotalCount !== undefined) {
|
|
1061
|
-
const currentTotalLoaded = (isAppend ?
|
|
1102
|
+
const currentTotalLoaded = (isAppend ? dataLengthRef.current : 0) + newItems.length;
|
|
1062
1103
|
setHasMore(currentTotalLoaded < newTotalCount);
|
|
1063
1104
|
}
|
|
1064
1105
|
else {
|
|
@@ -1066,15 +1107,16 @@ const useTableDataSource = (props) => {
|
|
|
1066
1107
|
setHasMore(newItems.length === pageSize);
|
|
1067
1108
|
}
|
|
1068
1109
|
}
|
|
1069
|
-
catch (
|
|
1070
|
-
|
|
1110
|
+
catch (err) {
|
|
1111
|
+
setError(err);
|
|
1071
1112
|
setHasMore(false);
|
|
1113
|
+
console.error('Error fetching data from dataSource:', err);
|
|
1072
1114
|
}
|
|
1073
1115
|
finally {
|
|
1074
1116
|
setIsLoading(false);
|
|
1075
1117
|
setIsFetchingMore(false);
|
|
1076
1118
|
}
|
|
1077
|
-
}), [dataSource, pageSize, sort, filter
|
|
1119
|
+
}), [dataSource, pageSize, sort, filter]);
|
|
1078
1120
|
const loadNextPage = useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
1079
1121
|
if (isLoading || isFetchingMore || !hasMore || !dataSource)
|
|
1080
1122
|
return;
|
|
@@ -1105,6 +1147,7 @@ const useTableDataSource = (props) => {
|
|
|
1105
1147
|
isFetchingMore,
|
|
1106
1148
|
loadNextPage,
|
|
1107
1149
|
resetAndFetch,
|
|
1150
|
+
error,
|
|
1108
1151
|
};
|
|
1109
1152
|
};
|
|
1110
1153
|
|
|
@@ -1112,8 +1155,9 @@ const useTableDataSource = (props) => {
|
|
|
1112
1155
|
* A highly customizable, mobile-first responsive React table.
|
|
1113
1156
|
* Supports static data or async data sources with built-in infinite scroll.
|
|
1114
1157
|
*/
|
|
1115
|
-
function
|
|
1116
|
-
|
|
1158
|
+
function ResponsiveTableInner(props, ref) {
|
|
1159
|
+
var _a;
|
|
1160
|
+
const { columnDefinitions, data: initialData, dataSource, pageSize, noDataComponent, maxHeight, onRowClick, footerRows, mobileBreakpoint, plugins, enablePageLevelStickyHeader, infiniteScrollProps, filterProps, selectionProps, animationProps, sortProps, mobileCardClassName, onDataSourceStateChange, onPageChange, onDataSourceError, } = props;
|
|
1117
1161
|
const tableContainerRef = useRef(null);
|
|
1118
1162
|
const headerRef = useRef(null);
|
|
1119
1163
|
const { isMobile, isHeaderSticky } = useResponsiveTable({
|
|
@@ -1126,29 +1170,72 @@ function ResponsiveTable(props) {
|
|
|
1126
1170
|
const getScrollableElement = useCallback(() => tableContainerRef.current, []);
|
|
1127
1171
|
// Track active sort state for dataSource
|
|
1128
1172
|
const [activeSort /*, setActiveSort*/] = useState((sortProps === null || sortProps === void 0 ? void 0 : sortProps.initialSortColumn) ? { columnId: sortProps.initialSortColumn, direction: sortProps.initialSortDirection || 'asc' } : undefined);
|
|
1129
|
-
|
|
1173
|
+
// Track active filter state for dataSource
|
|
1174
|
+
const [activeFilter, setActiveFilter] = useState('');
|
|
1175
|
+
const handleFilterChange = useCallback((text) => {
|
|
1176
|
+
setActiveFilter(text);
|
|
1177
|
+
}, []);
|
|
1178
|
+
const isServerFilter = !!dataSource && !!(filterProps === null || filterProps === void 0 ? void 0 : filterProps.showFilter) && (filterProps === null || filterProps === void 0 ? void 0 : filterProps.mode) !== 'client';
|
|
1179
|
+
const resolvedFilterProps = filterProps
|
|
1180
|
+
? Object.assign(Object.assign({}, filterProps), { mode: isServerFilter ? 'server' : ((_a = filterProps.mode) !== null && _a !== void 0 ? _a : 'client') }) : undefined;
|
|
1181
|
+
const { data: sourceData, isLoading: isSourceLoading, isFetchingMore, hasMore, totalCount, currentPage, loadNextPage, error, resetAndFetch, } = useTableDataSource({
|
|
1130
1182
|
dataSource,
|
|
1131
1183
|
pageSize,
|
|
1132
1184
|
initialData,
|
|
1133
1185
|
sort: activeSort,
|
|
1134
|
-
|
|
1186
|
+
filter: isServerFilter ? activeFilter : undefined,
|
|
1135
1187
|
});
|
|
1188
|
+
useImperativeHandle(ref, () => ({
|
|
1189
|
+
loadNextPage: () => loadNextPage(),
|
|
1190
|
+
resetAndFetch: () => resetAndFetch(),
|
|
1191
|
+
getState: () => ({
|
|
1192
|
+
data: sourceData,
|
|
1193
|
+
currentPage,
|
|
1194
|
+
hasMore,
|
|
1195
|
+
totalCount,
|
|
1196
|
+
isLoading: isSourceLoading,
|
|
1197
|
+
isFetchingMore,
|
|
1198
|
+
error,
|
|
1199
|
+
}),
|
|
1200
|
+
}), [loadNextPage, resetAndFetch, sourceData, currentPage, hasMore, totalCount, isSourceLoading, isFetchingMore, error]);
|
|
1136
1201
|
const currentDataToProcess = dataSource ? sourceData : initialData;
|
|
1137
1202
|
const { processedData, activePlugins, visibleColumns } = useTablePlugins({
|
|
1138
1203
|
data: currentDataToProcess,
|
|
1139
1204
|
plugins,
|
|
1140
|
-
|
|
1205
|
+
onFilterChange: isServerFilter ? handleFilterChange : undefined,
|
|
1206
|
+
filterProps: resolvedFilterProps,
|
|
1141
1207
|
selectionProps,
|
|
1142
1208
|
sortProps,
|
|
1143
1209
|
columnDefinitions,
|
|
1144
1210
|
getScrollableElement,
|
|
1145
1211
|
infiniteScrollProps,
|
|
1146
1212
|
});
|
|
1147
|
-
//
|
|
1213
|
+
// Fire onDataSourceStateChange when dataSource state changes
|
|
1214
|
+
useEffect(() => {
|
|
1215
|
+
if (dataSource && onDataSourceStateChange) {
|
|
1216
|
+
onDataSourceStateChange({
|
|
1217
|
+
data: sourceData,
|
|
1218
|
+
currentPage,
|
|
1219
|
+
hasMore,
|
|
1220
|
+
totalCount,
|
|
1221
|
+
isLoading: isSourceLoading,
|
|
1222
|
+
isFetchingMore,
|
|
1223
|
+
error,
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
}, [dataSource, sourceData, currentPage, hasMore, totalCount, isSourceLoading, isFetchingMore, error, onDataSourceStateChange]);
|
|
1227
|
+
// Fire onPageChange when page changes
|
|
1148
1228
|
useEffect(() => {
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1229
|
+
if (dataSource && onPageChange) {
|
|
1230
|
+
onPageChange(currentPage);
|
|
1231
|
+
}
|
|
1232
|
+
}, [dataSource, currentPage, onPageChange]);
|
|
1233
|
+
// Fire onDataSourceError when error occurs
|
|
1234
|
+
useEffect(() => {
|
|
1235
|
+
if (dataSource && error && onDataSourceError) {
|
|
1236
|
+
onDataSourceError(error);
|
|
1237
|
+
}
|
|
1238
|
+
}, [dataSource, error, onDataSourceError]);
|
|
1152
1239
|
const hasData = useMemo(() => processedData.length > 0, [processedData]);
|
|
1153
1240
|
const noDataSvg = (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "#ccc", height: "40", width: "40", viewBox: "0 0 24 24" },
|
|
1154
1241
|
React.createElement("path", { d: "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-14h2v6h-2zm0 8h2v2h-2z" })));
|
|
@@ -1216,6 +1303,31 @@ function ResponsiveTable(props) {
|
|
|
1216
1303
|
if (isLoading && !hasData) {
|
|
1217
1304
|
return React.createElement(SkeletonView, { isMobile: isMobile, columnDefinitions: visibleColumns });
|
|
1218
1305
|
}
|
|
1306
|
+
if (error && !isLoading && !hasData) {
|
|
1307
|
+
return (React.createElement("div", { style: {
|
|
1308
|
+
display: 'flex',
|
|
1309
|
+
flexDirection: 'column',
|
|
1310
|
+
alignItems: 'center',
|
|
1311
|
+
justifyContent: 'center',
|
|
1312
|
+
padding: '4rem 2rem',
|
|
1313
|
+
gap: '1rem',
|
|
1314
|
+
color: '#6c757d',
|
|
1315
|
+
border: '2px dashed #e0e0e0',
|
|
1316
|
+
borderRadius: '12px',
|
|
1317
|
+
backgroundColor: '#f8f9fa',
|
|
1318
|
+
} },
|
|
1319
|
+
React.createElement("div", { style: { fontWeight: 500, fontSize: '1.1rem' } }, "Failed to load data"),
|
|
1320
|
+
React.createElement("div", { style: { fontSize: '0.85rem', textAlign: 'center' } }, error.message),
|
|
1321
|
+
React.createElement("button", { onClick: resetAndFetch, style: {
|
|
1322
|
+
padding: '0.5rem 1.5rem',
|
|
1323
|
+
backgroundColor: '#007bff',
|
|
1324
|
+
color: '#fff',
|
|
1325
|
+
border: 'none',
|
|
1326
|
+
borderRadius: '6px',
|
|
1327
|
+
cursor: 'pointer',
|
|
1328
|
+
fontWeight: 500,
|
|
1329
|
+
} }, "Retry")));
|
|
1330
|
+
}
|
|
1219
1331
|
return (React.createElement(TableProvider, { value: {
|
|
1220
1332
|
data: currentDataToProcess,
|
|
1221
1333
|
processedData,
|
|
@@ -1234,6 +1346,7 @@ function ResponsiveTable(props) {
|
|
|
1234
1346
|
isLoading: isSourceLoading,
|
|
1235
1347
|
isFetchingMore,
|
|
1236
1348
|
loadNextPage,
|
|
1349
|
+
error,
|
|
1237
1350
|
} : undefined,
|
|
1238
1351
|
mobileCardClassName,
|
|
1239
1352
|
} },
|
|
@@ -1243,6 +1356,7 @@ function ResponsiveTable(props) {
|
|
|
1243
1356
|
(hasData || isLoading) && isMobile && (React.createElement(MobileView, { mobileFooter: mobileFooter })),
|
|
1244
1357
|
(hasData || isLoading) && !isMobile && (React.createElement(DesktopView, { maxHeight: maxHeight, isHeaderSticky: isHeaderSticky, tableContainerRef: tableContainerRef, headerRef: headerRef, footerRows: footerRows, renderPluginFooters: renderPluginFooters })))));
|
|
1245
1358
|
}
|
|
1359
|
+
const ResponsiveTable = forwardRef(ResponsiveTableInner);
|
|
1246
1360
|
|
|
1247
1361
|
class InfiniteScrollPlugin {
|
|
1248
1362
|
constructor() {
|
|
@@ -1297,5 +1411,5 @@ class InfiniteScrollPlugin {
|
|
|
1297
1411
|
}
|
|
1298
1412
|
}
|
|
1299
1413
|
|
|
1300
|
-
export { FilterPlugin, InfiniteScrollPlugin, SelectionPlugin, SortPlugin, ResponsiveTable as default };
|
|
1414
|
+
export { FilterPlugin, InfiniteScrollPlugin, SelectionPlugin, SortPlugin, ResponsiveTable as default, useTableContext };
|
|
1301
1415
|
//# sourceMappingURL=index.es.js.map
|