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/dist/index.js CHANGED
@@ -3,6 +3,8 @@
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var React = require('react');
6
+ var ZestTextbox = require('jattac.libs.web.zest-textbox');
7
+ var md = require('react-icons/md');
6
8
 
7
9
  function styleInject(css, ref) {
8
10
  if ( ref === void 0 ) ref = {};
@@ -488,18 +490,36 @@ class FilterPlugin {
488
490
  this.api = api;
489
491
  };
490
492
  this.renderHeader = () => {
491
- var _a;
493
+ var _a, _b;
492
494
  if (!((_a = this.api.filterProps) === null || _a === void 0 ? void 0 : _a.showFilter)) {
493
495
  return null;
494
496
  }
495
- return (React.createElement("div", { style: { marginBottom: '1rem' } },
496
- React.createElement("input", { type: "text", placeholder: this.api.filterProps.filterPlaceholder || "Search...", onChange: this.handleFilterChange, className: this.api.filterProps.className, style: {
497
- padding: '0.5rem',
498
- border: '1px solid #ccc',
499
- borderRadius: '4px',
500
- } })));
497
+ return (React.createElement("div", { style: { marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' } },
498
+ 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 } }),
499
+ React.createElement("button", { onClick: this.handleClear, "aria-label": "Clear filter", style: {
500
+ display: 'flex',
501
+ alignItems: 'center',
502
+ justifyContent: 'center',
503
+ minWidth: '2.75rem',
504
+ minHeight: '2.75rem',
505
+ padding: 0,
506
+ border: 'none',
507
+ borderRadius: '50%',
508
+ background: 'transparent',
509
+ cursor: 'pointer',
510
+ color: '#666',
511
+ opacity: this.filterText ? 1 : 0,
512
+ pointerEvents: this.filterText ? 'auto' : 'none',
513
+ transition: 'opacity 0.15s ease',
514
+ flexShrink: 0,
515
+ } },
516
+ React.createElement(md.MdClose, { size: 20 }))));
501
517
  };
502
518
  this.processData = (data) => {
519
+ var _a;
520
+ if (((_a = this.api.filterProps) === null || _a === void 0 ? void 0 : _a.mode) === 'server') {
521
+ return data;
522
+ }
503
523
  if (!this.filterText || !this.api.columnDefinitions) {
504
524
  return data;
505
525
  }
@@ -526,6 +546,10 @@ class FilterPlugin {
526
546
  _row,
527
547
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
528
548
  _column) => {
549
+ var _a;
550
+ if (((_a = this.api.filterProps) === null || _a === void 0 ? void 0 : _a.mode) === 'server') {
551
+ return content;
552
+ }
529
553
  if (!this.filterText || typeof content !== 'string') {
530
554
  return content;
531
555
  }
@@ -538,10 +562,21 @@ class FilterPlugin {
538
562
  clearTimeout(this.debounceTimeout);
539
563
  }
540
564
  this.debounceTimeout = setTimeout(() => {
565
+ var _a, _b;
541
566
  this.filterText = currentFilterText;
542
567
  this.api.forceUpdate();
568
+ (_b = (_a = this.api).onFilterChange) === null || _b === void 0 ? void 0 : _b.call(_a, currentFilterText);
543
569
  }, 300);
544
570
  };
571
+ this.handleClear = () => {
572
+ var _a, _b;
573
+ if (this.debounceTimeout) {
574
+ clearTimeout(this.debounceTimeout);
575
+ }
576
+ this.filterText = '';
577
+ this.api.forceUpdate();
578
+ (_b = (_a = this.api).onFilterChange) === null || _b === void 0 ? void 0 : _b.call(_a, '');
579
+ };
545
580
  }
546
581
  }
547
582
 
@@ -761,7 +796,7 @@ class SortPlugin {
761
796
  }
762
797
 
763
798
  const useTablePlugins = (props) => {
764
- const { data, plugins, filterProps, selectionProps, sortProps, columnDefinitions, getScrollableElement, infiniteScrollProps, } = props;
799
+ const { data, plugins, filterProps, selectionProps, sortProps, columnDefinitions, getScrollableElement, infiniteScrollProps, onFilterChange, } = props;
765
800
  const [processedData, setProcessedData] = React.useState(data);
766
801
  const [activePlugins, setActivePlugins] = React.useState([]);
767
802
  // Persist internal plugins using refs to prevent state loss
@@ -835,6 +870,7 @@ const useTablePlugins = (props) => {
835
870
  filterProps: filterProps,
836
871
  selectionProps: selectionProps,
837
872
  columnDefinitions: columnDefinitions,
873
+ onFilterChange: onFilterChange,
838
874
  };
839
875
  // Initialize/Refresh all active plugins with the current API
840
876
  newActivePlugins.forEach((plugin) => {
@@ -860,6 +896,7 @@ const useTablePlugins = (props) => {
860
896
  getScrollableElement,
861
897
  infiniteScrollProps,
862
898
  getRawColumnDefinition,
899
+ onFilterChange,
863
900
  ]);
864
901
  const forceUpdatePlugins = React.useCallback(() => {
865
902
  setProcessedData(initializePlugins());
@@ -1030,7 +1067,10 @@ const useTableDataSource = (props) => {
1030
1067
  const [totalCount, setTotalCount] = React.useState(undefined);
1031
1068
  const [isLoading, setIsLoading] = React.useState(false);
1032
1069
  const [isFetchingMore, setIsFetchingMore] = React.useState(false);
1070
+ const [error, setError] = React.useState(undefined);
1033
1071
  const isInitialMount = React.useRef(true);
1072
+ const dataLengthRef = React.useRef(0);
1073
+ dataLengthRef.current = data.length;
1034
1074
  const fetchData = React.useCallback((page, isAppend) => __awaiter(void 0, void 0, void 0, function* () {
1035
1075
  if (!dataSource)
1036
1076
  return;
@@ -1041,6 +1081,7 @@ const useTableDataSource = (props) => {
1041
1081
  setIsLoading(true);
1042
1082
  }
1043
1083
  try {
1084
+ setError(undefined);
1044
1085
  const params = {
1045
1086
  page,
1046
1087
  pageSize,
@@ -1062,7 +1103,7 @@ const useTableDataSource = (props) => {
1062
1103
  setCurrentPage(page);
1063
1104
  // Intelligent hasMore detection
1064
1105
  if (newTotalCount !== undefined) {
1065
- const currentTotalLoaded = (isAppend ? data.length : 0) + newItems.length;
1106
+ const currentTotalLoaded = (isAppend ? dataLengthRef.current : 0) + newItems.length;
1066
1107
  setHasMore(currentTotalLoaded < newTotalCount);
1067
1108
  }
1068
1109
  else {
@@ -1070,15 +1111,16 @@ const useTableDataSource = (props) => {
1070
1111
  setHasMore(newItems.length === pageSize);
1071
1112
  }
1072
1113
  }
1073
- catch (error) {
1074
- console.error('Error fetching data from dataSource:', error);
1114
+ catch (err) {
1115
+ setError(err);
1075
1116
  setHasMore(false);
1117
+ console.error('Error fetching data from dataSource:', err);
1076
1118
  }
1077
1119
  finally {
1078
1120
  setIsLoading(false);
1079
1121
  setIsFetchingMore(false);
1080
1122
  }
1081
- }), [dataSource, pageSize, sort, filter, data.length]);
1123
+ }), [dataSource, pageSize, sort, filter]);
1082
1124
  const loadNextPage = React.useCallback(() => __awaiter(void 0, void 0, void 0, function* () {
1083
1125
  if (isLoading || isFetchingMore || !hasMore || !dataSource)
1084
1126
  return;
@@ -1109,6 +1151,7 @@ const useTableDataSource = (props) => {
1109
1151
  isFetchingMore,
1110
1152
  loadNextPage,
1111
1153
  resetAndFetch,
1154
+ error,
1112
1155
  };
1113
1156
  };
1114
1157
 
@@ -1116,8 +1159,9 @@ const useTableDataSource = (props) => {
1116
1159
  * A highly customizable, mobile-first responsive React table.
1117
1160
  * Supports static data or async data sources with built-in infinite scroll.
1118
1161
  */
1119
- function ResponsiveTable(props) {
1120
- const { columnDefinitions, data: initialData, dataSource, pageSize, noDataComponent, maxHeight, onRowClick, footerRows, mobileBreakpoint, plugins, enablePageLevelStickyHeader, infiniteScrollProps, filterProps, selectionProps, animationProps, sortProps, mobileCardClassName, } = props;
1162
+ function ResponsiveTableInner(props, ref) {
1163
+ var _a;
1164
+ const { columnDefinitions, data: initialData, dataSource, pageSize, noDataComponent, maxHeight, onRowClick, footerRows, mobileBreakpoint, plugins, enablePageLevelStickyHeader, infiniteScrollProps, filterProps, selectionProps, animationProps, sortProps, mobileCardClassName, onDataSourceStateChange, onPageChange, onDataSourceError, } = props;
1121
1165
  const tableContainerRef = React.useRef(null);
1122
1166
  const headerRef = React.useRef(null);
1123
1167
  const { isMobile, isHeaderSticky } = useResponsiveTable({
@@ -1130,29 +1174,72 @@ function ResponsiveTable(props) {
1130
1174
  const getScrollableElement = React.useCallback(() => tableContainerRef.current, []);
1131
1175
  // Track active sort state for dataSource
1132
1176
  const [activeSort /*, setActiveSort*/] = React.useState((sortProps === null || sortProps === void 0 ? void 0 : sortProps.initialSortColumn) ? { columnId: sortProps.initialSortColumn, direction: sortProps.initialSortDirection || 'asc' } : undefined);
1133
- const { data: sourceData, isLoading: isSourceLoading, isFetchingMore, hasMore, totalCount, currentPage, loadNextPage, } = useTableDataSource({
1177
+ // Track active filter state for dataSource
1178
+ const [activeFilter, setActiveFilter] = React.useState('');
1179
+ const handleFilterChange = React.useCallback((text) => {
1180
+ setActiveFilter(text);
1181
+ }, []);
1182
+ const isServerFilter = !!dataSource && !!(filterProps === null || filterProps === void 0 ? void 0 : filterProps.showFilter) && (filterProps === null || filterProps === void 0 ? void 0 : filterProps.mode) !== 'client';
1183
+ const resolvedFilterProps = filterProps
1184
+ ? Object.assign(Object.assign({}, filterProps), { mode: isServerFilter ? 'server' : ((_a = filterProps.mode) !== null && _a !== void 0 ? _a : 'client') }) : undefined;
1185
+ const { data: sourceData, isLoading: isSourceLoading, isFetchingMore, hasMore, totalCount, currentPage, loadNextPage, error, resetAndFetch, } = useTableDataSource({
1134
1186
  dataSource,
1135
1187
  pageSize,
1136
1188
  initialData,
1137
1189
  sort: activeSort,
1138
- // We'll need to extract filter state if we want to support dataSource filtering
1190
+ filter: isServerFilter ? activeFilter : undefined,
1139
1191
  });
1192
+ React.useImperativeHandle(ref, () => ({
1193
+ loadNextPage: () => loadNextPage(),
1194
+ resetAndFetch: () => resetAndFetch(),
1195
+ getState: () => ({
1196
+ data: sourceData,
1197
+ currentPage,
1198
+ hasMore,
1199
+ totalCount,
1200
+ isLoading: isSourceLoading,
1201
+ isFetchingMore,
1202
+ error,
1203
+ }),
1204
+ }), [loadNextPage, resetAndFetch, sourceData, currentPage, hasMore, totalCount, isSourceLoading, isFetchingMore, error]);
1140
1205
  const currentDataToProcess = dataSource ? sourceData : initialData;
1141
1206
  const { processedData, activePlugins, visibleColumns } = useTablePlugins({
1142
1207
  data: currentDataToProcess,
1143
1208
  plugins,
1144
- filterProps,
1209
+ onFilterChange: isServerFilter ? handleFilterChange : undefined,
1210
+ filterProps: resolvedFilterProps,
1145
1211
  selectionProps,
1146
1212
  sortProps,
1147
1213
  columnDefinitions,
1148
1214
  getScrollableElement,
1149
1215
  infiniteScrollProps,
1150
1216
  });
1151
- // Sync sort state from SortPlugin back to our local state to trigger dataSource re-fetch
1217
+ // Fire onDataSourceStateChange when dataSource state changes
1218
+ React.useEffect(() => {
1219
+ if (dataSource && onDataSourceStateChange) {
1220
+ onDataSourceStateChange({
1221
+ data: sourceData,
1222
+ currentPage,
1223
+ hasMore,
1224
+ totalCount,
1225
+ isLoading: isSourceLoading,
1226
+ isFetchingMore,
1227
+ error,
1228
+ });
1229
+ }
1230
+ }, [dataSource, sourceData, currentPage, hasMore, totalCount, isSourceLoading, isFetchingMore, error, onDataSourceStateChange]);
1231
+ // Fire onPageChange when page changes
1152
1232
  React.useEffect(() => {
1153
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1154
- activePlugins.find(p => p.id === 'sort');
1155
- }, [activePlugins, dataSource]);
1233
+ if (dataSource && onPageChange) {
1234
+ onPageChange(currentPage);
1235
+ }
1236
+ }, [dataSource, currentPage, onPageChange]);
1237
+ // Fire onDataSourceError when error occurs
1238
+ React.useEffect(() => {
1239
+ if (dataSource && error && onDataSourceError) {
1240
+ onDataSourceError(error);
1241
+ }
1242
+ }, [dataSource, error, onDataSourceError]);
1156
1243
  const hasData = React.useMemo(() => processedData.length > 0, [processedData]);
1157
1244
  const noDataSvg = (React.createElement("svg", { xmlns: "http://www.w3.org/2000/svg", fill: "#ccc", height: "40", width: "40", viewBox: "0 0 24 24" },
1158
1245
  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" })));
@@ -1220,6 +1307,31 @@ function ResponsiveTable(props) {
1220
1307
  if (isLoading && !hasData) {
1221
1308
  return React.createElement(SkeletonView, { isMobile: isMobile, columnDefinitions: visibleColumns });
1222
1309
  }
1310
+ if (error && !isLoading && !hasData) {
1311
+ return (React.createElement("div", { style: {
1312
+ display: 'flex',
1313
+ flexDirection: 'column',
1314
+ alignItems: 'center',
1315
+ justifyContent: 'center',
1316
+ padding: '4rem 2rem',
1317
+ gap: '1rem',
1318
+ color: '#6c757d',
1319
+ border: '2px dashed #e0e0e0',
1320
+ borderRadius: '12px',
1321
+ backgroundColor: '#f8f9fa',
1322
+ } },
1323
+ React.createElement("div", { style: { fontWeight: 500, fontSize: '1.1rem' } }, "Failed to load data"),
1324
+ React.createElement("div", { style: { fontSize: '0.85rem', textAlign: 'center' } }, error.message),
1325
+ React.createElement("button", { onClick: resetAndFetch, style: {
1326
+ padding: '0.5rem 1.5rem',
1327
+ backgroundColor: '#007bff',
1328
+ color: '#fff',
1329
+ border: 'none',
1330
+ borderRadius: '6px',
1331
+ cursor: 'pointer',
1332
+ fontWeight: 500,
1333
+ } }, "Retry")));
1334
+ }
1223
1335
  return (React.createElement(TableProvider, { value: {
1224
1336
  data: currentDataToProcess,
1225
1337
  processedData,
@@ -1238,6 +1350,7 @@ function ResponsiveTable(props) {
1238
1350
  isLoading: isSourceLoading,
1239
1351
  isFetchingMore,
1240
1352
  loadNextPage,
1353
+ error,
1241
1354
  } : undefined,
1242
1355
  mobileCardClassName,
1243
1356
  } },
@@ -1247,6 +1360,7 @@ function ResponsiveTable(props) {
1247
1360
  (hasData || isLoading) && isMobile && (React.createElement(MobileView, { mobileFooter: mobileFooter })),
1248
1361
  (hasData || isLoading) && !isMobile && (React.createElement(DesktopView, { maxHeight: maxHeight, isHeaderSticky: isHeaderSticky, tableContainerRef: tableContainerRef, headerRef: headerRef, footerRows: footerRows, renderPluginFooters: renderPluginFooters })))));
1249
1362
  }
1363
+ const ResponsiveTable = React.forwardRef(ResponsiveTableInner);
1250
1364
 
1251
1365
  class InfiniteScrollPlugin {
1252
1366
  constructor() {
@@ -1306,4 +1420,5 @@ exports.InfiniteScrollPlugin = InfiniteScrollPlugin;
1306
1420
  exports.SelectionPlugin = SelectionPlugin;
1307
1421
  exports.SortPlugin = SortPlugin;
1308
1422
  exports.default = ResponsiveTable;
1423
+ exports.useTableContext = useTableContext;
1309
1424
  //# sourceMappingURL=index.js.map