@pallasoft/ps-table-components 1.26.0 → 1.27.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.
Files changed (40) hide show
  1. package/dist/components/ps-data-table.d.ts +8 -1
  2. package/dist/components/ps-data-table.js +78 -12
  3. package/dist/components/ps-select-all-matching-banner.d.ts +10 -0
  4. package/dist/components/ps-select-all-matching-banner.js +10 -0
  5. package/dist/components/ps-table-pagination.d.ts +3 -1
  6. package/dist/components/ps-table-pagination.js +11 -3
  7. package/dist/constants/ps-server-rows-config.d.ts +8 -0
  8. package/dist/constants/ps-server-rows-config.js +11 -0
  9. package/dist/esm/components/ps-data-table.js +79 -13
  10. package/dist/esm/components/ps-select-all-matching-banner.js +6 -0
  11. package/dist/esm/components/ps-table-pagination.js +11 -3
  12. package/dist/esm/constants/ps-server-rows-config.js +8 -0
  13. package/dist/esm/hooks/use-ps-server-rows.js +209 -0
  14. package/dist/esm/index.js +10 -0
  15. package/dist/esm/interfaces/ps-server-entry.js +1 -0
  16. package/dist/esm/interfaces/ps-server-page.js +1 -0
  17. package/dist/esm/interfaces/ps-server-rows-input.js +1 -0
  18. package/dist/esm/interfaces/ps-server-rows-result.js +1 -0
  19. package/dist/esm/interfaces/ps-table-cursor-paging.js +1 -0
  20. package/dist/esm/interfaces/ps-table-select-all-matching.js +1 -0
  21. package/dist/esm/services/ps-server-rows-service.js +36 -0
  22. package/dist/hooks/use-ps-server-rows.d.ts +3 -0
  23. package/dist/hooks/use-ps-server-rows.js +213 -0
  24. package/dist/index.d.ts +10 -0
  25. package/dist/index.js +10 -0
  26. package/dist/interfaces/ps-server-entry.d.ts +6 -0
  27. package/dist/interfaces/ps-server-entry.js +2 -0
  28. package/dist/interfaces/ps-server-page.d.ts +7 -0
  29. package/dist/interfaces/ps-server-page.js +2 -0
  30. package/dist/interfaces/ps-server-rows-input.d.ts +13 -0
  31. package/dist/interfaces/ps-server-rows-input.js +2 -0
  32. package/dist/interfaces/ps-server-rows-result.d.ts +18 -0
  33. package/dist/interfaces/ps-server-rows-result.js +2 -0
  34. package/dist/interfaces/ps-table-cursor-paging.d.ts +12 -0
  35. package/dist/interfaces/ps-table-cursor-paging.js +2 -0
  36. package/dist/interfaces/ps-table-select-all-matching.d.ts +7 -0
  37. package/dist/interfaces/ps-table-select-all-matching.js +2 -0
  38. package/dist/services/ps-server-rows-service.d.ts +11 -0
  39. package/dist/services/ps-server-rows-service.js +40 -0
  40. package/package.json +1 -1
@@ -1,5 +1,7 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { PsTablePaginationPosition } from '../constants/ps-table-pagination-position';
3
+ import type { PsTableCursorPaging } from '../interfaces/ps-table-cursor-paging';
4
+ import type { PsTableSelectAllMatching } from '../interfaces/ps-table-select-all-matching';
3
5
  import type { PsTableColumn } from '../interfaces/table-column';
4
6
  import type { PsTableGrouping } from '../interfaces/table-grouping';
5
7
  import type { PsTableQuery } from '../interfaces/table-query';
@@ -9,7 +11,9 @@ import type { PsTableSort } from '../interfaces/table-sort';
9
11
  export interface PsDataTableProps<RowData> {
10
12
  caption?: string;
11
13
  className?: string;
14
+ clearSelectionKey?: number | string;
12
15
  columns: PsTableColumn<RowData>[];
16
+ cursorPaging?: PsTableCursorPaging;
13
17
  defaultSort?: PsTableSort;
14
18
  density?: 'compact' | 'normal';
15
19
  expandedRowIds?: string[];
@@ -21,6 +25,7 @@ export interface PsDataTableProps<RowData> {
21
25
  grouping?: PsTableGrouping<RowData>;
22
26
  isLoading?: boolean;
23
27
  isPending?: boolean;
28
+ isRefreshing?: boolean;
24
29
  layout?: 'auto' | 'fixed';
25
30
  onPageChange?: (page: number) => void;
26
31
  onQueryChange?: (query: PsTableQuery) => void;
@@ -36,7 +41,9 @@ export interface PsDataTableProps<RowData> {
36
41
  rows: RowData[];
37
42
  searchingLabel?: string;
38
43
  searchPlaceholder?: string;
44
+ selectAllMatching?: PsTableSelectAllMatching;
45
+ stickyTop?: string;
39
46
  toolbarActions?: ReactNode;
40
47
  totalRows?: number;
41
48
  }
42
- export declare const PsDataTable: <RowData>({ caption, className, columns, defaultSort, density, expandedRowIds, mobileLayout, getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading, isPending, layout, onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, paginationPosition, pinFirstColumn, query, renderExpandedRow, reorder, rows, searchingLabel, searchPlaceholder, toolbarActions, totalRows, }: PsDataTableProps<RowData>) => import("react").JSX.Element;
49
+ export declare const PsDataTable: <RowData>({ caption, className, clearSelectionKey, columns, cursorPaging, defaultSort, density, expandedRowIds, mobileLayout, getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading, isPending, isRefreshing, layout, onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, paginationPosition, pinFirstColumn, query, renderExpandedRow, reorder, rows, searchingLabel, searchPlaceholder, selectAllMatching, stickyTop, toolbarActions, totalRows, }: PsDataTableProps<RowData>) => import("react").JSX.Element;
@@ -56,6 +56,7 @@ const ps_table_pin_service_1 = require("../services/ps-table-pin-service");
56
56
  const ps_table_query_service_1 = require("../services/ps-table-query-service");
57
57
  const ps_table_reorder_service_1 = require("../services/ps-table-reorder-service");
58
58
  const ps_table_scroll_service_1 = require("../services/ps-table-scroll-service");
59
+ const ps_select_all_matching_banner_1 = require("./ps-select-all-matching-banner");
59
60
  const ps_table_bulk_bar_1 = require("./ps-table-bulk-bar");
60
61
  const ps_table_card_list_1 = require("./ps-table-card-list");
61
62
  const ps_table_column_picker_1 = require("./ps-table-column-picker");
@@ -85,7 +86,7 @@ const alignClassNames = {
85
86
  left: 'text-left',
86
87
  right: 'text-right tabular-nums',
87
88
  };
88
- const PsDataTable = ({ caption, className, columns, defaultSort, density = 'normal', expandedRowIds, mobileLayout = 'cards', getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading = false, isPending = false, layout = 'auto', onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, paginationPosition = ps_table_pagination_position_1.PsTablePaginationPosition.bottom, pinFirstColumn = true, query, renderExpandedRow, reorder, rows, searchingLabel = table_config_1.PsTableConfig.searchingLabel, searchPlaceholder = table_config_1.PsTableConfig.searchPlaceholder, toolbarActions, totalRows, }) => {
89
+ const PsDataTable = ({ caption, className, clearSelectionKey, columns, cursorPaging, defaultSort, density = 'normal', expandedRowIds, mobileLayout = 'cards', getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading = false, isPending = false, isRefreshing = false, layout = 'auto', onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, paginationPosition = ps_table_pagination_position_1.PsTablePaginationPosition.bottom, pinFirstColumn = true, query, renderExpandedRow, reorder, rows, searchingLabel = table_config_1.PsTableConfig.searchingLabel, searchPlaceholder = table_config_1.PsTableConfig.searchPlaceholder, selectAllMatching, stickyTop, toolbarActions, totalRows, }) => {
89
90
  const containerRef = (0, react_1.useRef)(null);
90
91
  const [uncontrolledPage, setUncontrolledPage] = (0, react_1.useState)(1);
91
92
  const [uncontrolledQuery, setUncontrolledQuery] = (0, react_1.useState)(() => ps_table_query_service_1.PsTableQueryService.empty(defaultSort ? defaultSort : null));
@@ -93,7 +94,50 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
93
94
  const [toggledGroupKeys, setToggledGroupKeys] = (0, react_1.useState)([]);
94
95
  const [closingGroupKeys, setClosingGroupKeys] = (0, react_1.useState)([]);
95
96
  const [openedGroupKeys, setOpenedGroupKeys] = (0, react_1.useState)([]);
97
+ const [seenClearKey, setSeenClearKey] = (0, react_1.useState)(clearSelectionKey);
98
+ const stickyRegionRef = (0, react_1.useRef)(null);
99
+ const stickySentinelRef = (0, react_1.useRef)(null);
100
+ const [stickyHeight, setStickyHeight] = (0, react_1.useState)(0);
101
+ const [isStuck, setIsStuck] = (0, react_1.useState)(false);
96
102
  const currentQuery = query ? query : uncontrolledQuery;
103
+ if (seenClearKey !== clearSelectionKey) {
104
+ setSeenClearKey(clearSelectionKey);
105
+ setSelectedIds([]);
106
+ }
107
+ (0, react_1.useLayoutEffect)(() => {
108
+ const region = stickyRegionRef.current;
109
+ if (stickyTop === undefined || region === null)
110
+ return;
111
+ const measure = () => setStickyHeight(region.offsetHeight);
112
+ const observer = new ResizeObserver(measure);
113
+ measure();
114
+ observer.observe(region);
115
+ return () => observer.disconnect();
116
+ }, [stickyTop]);
117
+ (0, react_1.useEffect)(() => {
118
+ const region = stickyRegionRef.current;
119
+ const sentinel = stickySentinelRef.current;
120
+ if (stickyTop === undefined || region === null || sentinel === null)
121
+ return;
122
+ let frame = 0;
123
+ const check = () => {
124
+ frame = 0;
125
+ setIsStuck(sentinel.getBoundingClientRect().top < Number.parseFloat(getComputedStyle(region).top));
126
+ };
127
+ const schedule = () => {
128
+ if (frame === 0)
129
+ frame = window.requestAnimationFrame(check);
130
+ };
131
+ check();
132
+ window.addEventListener('scroll', schedule, { passive: true });
133
+ window.addEventListener('resize', schedule);
134
+ return () => {
135
+ window.removeEventListener('scroll', schedule);
136
+ window.removeEventListener('resize', schedule);
137
+ if (frame !== 0)
138
+ window.cancelAnimationFrame(frame);
139
+ };
140
+ }, [stickyTop]);
97
141
  const { filters, hiddenColumnIds, search, sort } = currentQuery;
98
142
  const visibleColumns = columns.filter((column) => !hiddenColumnIds.includes(column.id));
99
143
  const fittedWidths = (0, react_1.useMemo)(() => new Map(columns
@@ -235,7 +279,7 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
235
279
  })
236
280
  : [], [getRowId, getRowLabel, reorder, reorderKey, visibleRows]);
237
281
  const showsPagination = Boolean(pageSize) && (!grouping || isServerPaginated);
238
- const renderPagination = (paginationClassName) => pageSize ? ((0, jsx_runtime_1.jsx)(ps_table_pagination_1.PsTablePagination, { className: paginationClassName, isPartial: isPartial, loadedPageCount: pageCountOf(loadedRowCount), onPageChange: changePage, page: safePage, pageCount: pageCount, pageSize: pageSize, totalRows: rowCount })) : null;
282
+ const renderPagination = (paginationClassName) => pageSize ? ((0, jsx_runtime_1.jsx)(ps_table_pagination_1.PsTablePagination, { className: paginationClassName, cursor: cursorPaging, isPartial: isPartial, loadedPageCount: pageCountOf(loadedRowCount), onPageChange: changePage, page: safePage, pageCount: pageCount, pageSize: pageSize, totalRows: rowCount })) : null;
239
283
  const renderOverlayRow = (rowId, size) => {
240
284
  const row = visibleRows.find((candidate) => getRowId(candidate) === rowId);
241
285
  return row === undefined ? null : ((0, jsx_runtime_1.jsxs)("table", { className: 'pointer-events-none w-full table-fixed border-collapse bg-surface text-left', style: size === null ? undefined : { width: size.width }, children: [size === null ? null : ((0, jsx_runtime_1.jsx)("colgroup", { children: size.columnWidths.map((width, index) => ((0, jsx_runtime_1.jsx)("col", { style: { width } }, index))) })), (0, jsx_runtime_1.jsx)("tbody", { children: renderRow(ps_table_reorder_service_1.PsTableReorderService.plain, row, undefined, undefined, true) })] }));
@@ -269,12 +313,16 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
269
313
  const selectedRows = selectedIds.length === 0 ? [] : queriedRows.filter((row) => selectedIdSet.has(getRowId(row)));
270
314
  const pageIds = visibleRows.map((row) => getRowId(row));
271
315
  const isPageSelected = pageIds.length > 0 && pageIds.every((rowId) => selectedIdSet.has(rowId));
272
- const togglePage = () => setSelectedIds((current) => {
273
- const pageIdSet = new Set(pageIds);
274
- return isPageSelected
275
- ? current.filter((rowId) => !pageIdSet.has(rowId))
276
- : [...new Set([...current, ...pageIds])];
277
- });
316
+ const togglePage = () => {
317
+ if (isPageSelected && selectAllMatching !== undefined && selectAllMatching.isActive)
318
+ selectAllMatching.onDeactivate();
319
+ setSelectedIds((current) => {
320
+ const pageIdSet = new Set(pageIds);
321
+ return isPageSelected
322
+ ? current.filter((rowId) => !pageIdSet.has(rowId))
323
+ : [...new Set([...current, ...pageIds])];
324
+ });
325
+ };
278
326
  const bodyRef = (0, react_1.useRef)(null);
279
327
  const [scrollParent, setScrollParent] = (0, react_1.useState)(null);
280
328
  const [scrollMargin, setScrollMargin] = (0, react_1.useState)(0);
@@ -352,7 +400,18 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
352
400
  : entries.map((entry) => renderRow(api, entry.row, entry.motion, entry.virtualItem));
353
401
  if (isLoading)
354
402
  return ((0, jsx_runtime_1.jsx)(ps_core_components_1.PsSkeletonTable, { className: className, columns: visibleColumns.length, hasToolbar: hasToolbar, rows: table_config_1.PsTableConfig.skeletonRows }));
355
- return ((0, jsx_runtime_1.jsxs)("div", { ref: containerRef, className: ps_design_system_1.PallasoftClassNameService.merge('flex min-w-0 flex-col gap-3', className), children: [isSelectable && selectedRows.length > 0 && getBulkActions ? ((0, jsx_runtime_1.jsx)(ps_table_bulk_bar_1.PsTableBulkBar, { actions: getBulkActions(selectedRows), onClear: () => setSelectedIds([]), selectedCount: selectedRows.length })) : hasToolbar || columns.some((column) => column.canHide) ? ((0, jsx_runtime_1.jsx)(ps_table_toolbar_1.PsTableToolbar, { actions: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [toolbarActions, columns.some((column) => column.canHide) ? ((0, jsx_runtime_1.jsx)(ps_table_column_picker_1.PsTableColumnPicker, { columns: columns
403
+ const clearSelection = () => {
404
+ setSelectedIds([]);
405
+ if (selectAllMatching !== undefined && selectAllMatching.isActive)
406
+ selectAllMatching.onDeactivate();
407
+ };
408
+ const banner = selectAllMatching !== undefined &&
409
+ selectedRows.length > 0 &&
410
+ selectAllMatching.total > pageIds.length &&
411
+ (isPageSelected || selectAllMatching.isActive) ? ((0, jsx_runtime_1.jsx)(ps_select_all_matching_banner_1.PsSelectAllMatchingBanner, { isActive: selectAllMatching.isActive, noun: selectAllMatching.noun, onActivate: selectAllMatching.onActivate, onDeactivate: clearSelection, pageCount: pageIds.length, total: selectAllMatching.total })) : null;
412
+ const topContent = ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [isSelectable && selectedRows.length > 0 && getBulkActions ? ((0, jsx_runtime_1.jsx)(ps_table_bulk_bar_1.PsTableBulkBar, { actions: getBulkActions(selectedRows), onClear: clearSelection, selectedCount: selectAllMatching !== undefined && selectAllMatching.isActive
413
+ ? selectAllMatching.total
414
+ : selectedRows.length })) : hasToolbar || columns.some((column) => column.canHide) ? ((0, jsx_runtime_1.jsx)(ps_table_toolbar_1.PsTableToolbar, { actions: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [toolbarActions, columns.some((column) => column.canHide) ? ((0, jsx_runtime_1.jsx)(ps_table_column_picker_1.PsTableColumnPicker, { columns: columns
356
415
  .filter((column) => column.canHide)
357
416
  .map((column) => ({ id: column.id, label: column.label })), hiddenColumnIds: hiddenColumnIds, onChange: (nextHidden) => updateQuery({ hiddenColumnIds: nextHidden }) })) : null] }), columns: hasFilters ? columns : [], filters: filters, isSearchable: isSearchable, onClearFilters: clearFilters, onFilterChange: (columnId, value) => {
358
417
  updateQuery({ filters: { ...filters, [columnId]: value } });
@@ -360,16 +419,23 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
360
419
  }, onSearchChange: (next) => {
361
420
  updateQuery({ search: next });
362
421
  resetPage();
363
- }, rows: rows, search: search, searchPlaceholder: searchPlaceholder })) : null, (0, jsx_runtime_1.jsxs)("div", { className: 'min-w-0 overflow-hidden rounded-[1.25rem] border border-line bg-surface', children: [showsPagination && paginationPosition !== ps_table_pagination_position_1.PsTablePaginationPosition.bottom
422
+ }, rows: rows, search: search, searchPlaceholder: searchPlaceholder })) : null, banner] }));
423
+ return ((0, jsx_runtime_1.jsxs)("div", { ref: containerRef, className: ps_design_system_1.PallasoftClassNameService.merge('flex min-w-0 flex-col gap-3', className), children: [stickyTop === undefined ? (topContent) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { "aria-hidden": 'true', className: '-mb-3 h-0', ref: stickySentinelRef }), (0, jsx_runtime_1.jsx)("div", { className: ps_design_system_1.PallasoftClassNameService.merge('sticky z-sticky -mx-1 flex flex-col gap-3 bg-canvas px-1 pb-2 pt-1 transition-shadow', isStuck ? 'shadow-[0_8px_8px_-8px_rgb(var(--ps-color-ink-rgb,15_23_42)/0.18)]' : ''), ref: stickyRegionRef, style: { top: stickyTop }, children: topContent })] })), (0, jsx_runtime_1.jsxs)("div", { "aria-busy": isRefreshing, className: ps_design_system_1.PallasoftClassNameService.merge('min-w-0 rounded-[1.25rem] border border-line bg-surface', stickyTop === undefined ? 'overflow-hidden' : 'overflow-clip'), children: [showsPagination && paginationPosition !== ps_table_pagination_position_1.PsTablePaginationPosition.bottom
364
424
  ? renderPagination('border-b border-t-0')
365
- : null, mobileLayout === 'cards' ? ((0, jsx_runtime_1.jsxs)("div", { className: 'sm:hidden', children: [reorder && reorder.cardsReason ? ((0, jsx_runtime_1.jsx)("p", { className: 'border-b border-line px-4 py-2 text-xs text-ink/55', children: reorder.cardsReason })) : null, grouping
425
+ : null, mobileLayout === 'cards' ? ((0, jsx_runtime_1.jsxs)("div", { className: ps_design_system_1.PallasoftClassNameService.merge('transition-opacity duration-150 sm:hidden', isRefreshing ? 'opacity-60' : ''), children: [reorder && reorder.cardsReason ? ((0, jsx_runtime_1.jsx)("p", { className: 'border-b border-line px-4 py-2 text-xs text-ink/55', children: reorder.cardsReason })) : null, grouping
366
426
  ? groups.map((group, groupIndex) => {
367
427
  const isExpanded = isGroupExpanded(group.key, groupIndex);
368
428
  return ((0, jsx_runtime_1.jsxs)("div", { className: 'border-b border-line last:border-b-0', children: [(0, jsx_runtime_1.jsx)(ps_table_group_header_1.PsTableGroupHeader, { count: group.rows.length, isExpanded: isExpanded, label: grouping.getGroupLabel(group.key, group.rows), onToggle: () => toggleGroup(group.key, isExpanded), summary: grouping.renderGroupSummary
369
429
  ? grouping.renderGroupSummary(group.rows)
370
430
  : undefined }), (0, jsx_runtime_1.jsx)(ps_motion_components_1.PsCollapse, { isOpen: isExpanded && !closingGroupKeys.includes(group.key), children: renderCardList(group.rows, null) })] }, `group-${group.key}`));
371
431
  })
372
- : renderCardList(shouldVirtualize ? visibleRows.slice(0, cards.count) : visibleRows, visibleRows.length === 0 && isSearching ? ((0, jsx_runtime_1.jsx)(ps_table_searching_state_1.PsTableSearchingState, { label: searchingLabel, skeletonRows: table_config_1.PsTableConfig.searchingSkeletonRows })) : visibleRows.length === 0 && isQueried ? ((0, jsx_runtime_1.jsxs)("div", { className: 'px-4 py-10 text-center', children: [(0, jsx_runtime_1.jsx)("p", { className: 'text-sm font-semibold text-ink', children: table_config_1.PsTableConfig.noMatchesTitle }), (0, jsx_runtime_1.jsx)("button", { className: ps_design_system_1.PallasoftClassNameService.merge(ps_table_control_classes_1.PsTableControlClasses.textButton, 'mt-2 text-primary'), onClick: clearFilters, type: 'button', children: "Clear search and filters" })] })) : null), !grouping && shouldVirtualize && cards.count < visibleRows.length ? ((0, jsx_runtime_1.jsx)("div", { "aria-hidden": 'true', className: 'h-px', ref: cards.sentinelRef })) : null] })) : null, (0, jsx_runtime_1.jsx)("div", { className: ps_design_system_1.PallasoftClassNameService.merge('ps-scrollbar overflow-x-auto overflow-y-hidden overscroll-x-contain', mobileLayout === 'cards' ? 'hidden sm:block' : ''), "data-overflow-end": overflow.edges.end, "data-overflow-start": overflow.edges.start, onScroll: overflow.update, ref: overflow.ref, children: renderReorder((api) => ((0, jsx_runtime_1.jsxs)("table", { className: ps_design_system_1.PallasoftClassNameService.merge('w-full border-collapse text-left', layout === 'fixed' ? 'table-fixed' : ''), children: [caption ? (0, jsx_runtime_1.jsx)("caption", { className: 'sr-only', children: caption }) : null, (0, jsx_runtime_1.jsx)("thead", { className: 'bg-mist/70 text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-ink/45', children: (0, jsx_runtime_1.jsxs)("tr", { children: [reorder ? ((0, jsx_runtime_1.jsx)("th", { className: ps_design_system_1.PallasoftClassNameService.merge('w-9 min-w-9 pl-3 pr-0', pins.reorder), scope: 'col', children: reorder.enabled ? ((0, jsx_runtime_1.jsx)("span", { className: 'sr-only', children: reorder.columnLabel })) : reorder.disabledReason ? ((0, jsx_runtime_1.jsx)("span", { "aria-label": reorder.disabledReason, className: 'inline-flex cursor-help opacity-40', role: 'img', title: reorder.disabledReason, children: (0, jsx_runtime_1.jsx)(ps_core_components_1.PsDragGrip, {}) })) : null })) : null, isSelectable ? ((0, jsx_runtime_1.jsx)("th", { className: ps_design_system_1.PallasoftClassNameService.merge('w-10 min-w-10 pl-4 pr-0', pins.select), scope: 'col', children: (0, jsx_runtime_1.jsx)(ps_input_components_1.PsCheckbox, { isIndeterminate: !isPageSelected && pageIds.some((rowId) => selectedIdSet.has(rowId)), isSelected: isPageSelected, label: 'Select all rows on this page', onChange: togglePage }) })) : null, visibleColumns.map((column, columnIndex) => {
432
+ : renderCardList(shouldVirtualize ? visibleRows.slice(0, cards.count) : visibleRows, visibleRows.length === 0 && isSearching ? ((0, jsx_runtime_1.jsx)(ps_table_searching_state_1.PsTableSearchingState, { label: searchingLabel, skeletonRows: table_config_1.PsTableConfig.searchingSkeletonRows })) : visibleRows.length === 0 && isQueried ? ((0, jsx_runtime_1.jsxs)("div", { className: 'px-4 py-10 text-center', children: [(0, jsx_runtime_1.jsx)("p", { className: 'text-sm font-semibold text-ink', children: table_config_1.PsTableConfig.noMatchesTitle }), (0, jsx_runtime_1.jsx)("button", { className: ps_design_system_1.PallasoftClassNameService.merge(ps_table_control_classes_1.PsTableControlClasses.textButton, 'mt-2 text-primary'), onClick: clearFilters, type: 'button', children: "Clear search and filters" })] })) : null), !grouping && shouldVirtualize && cards.count < visibleRows.length ? ((0, jsx_runtime_1.jsx)("div", { "aria-hidden": 'true', className: 'h-px', ref: cards.sentinelRef })) : null] })) : null, (0, jsx_runtime_1.jsx)("div", { className: ps_design_system_1.PallasoftClassNameService.merge(stickyTop === undefined
433
+ ? 'ps-scrollbar overflow-x-auto overflow-y-hidden overscroll-x-contain'
434
+ : 'overflow-x-clip', 'transition-opacity duration-150', isRefreshing ? 'opacity-60' : '', mobileLayout === 'cards' ? 'hidden sm:block' : ''), "data-overflow-end": overflow.edges.end, "data-overflow-start": overflow.edges.start, onScroll: overflow.update, ref: overflow.ref, children: renderReorder((api) => ((0, jsx_runtime_1.jsxs)("table", { className: ps_design_system_1.PallasoftClassNameService.merge('w-full border-collapse text-left', layout === 'fixed' ? 'table-fixed' : ''), children: [caption ? (0, jsx_runtime_1.jsx)("caption", { className: 'sr-only', children: caption }) : null, (0, jsx_runtime_1.jsx)("thead", { className: ps_design_system_1.PallasoftClassNameService.merge('bg-mist/70 text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-ink/45', stickyTop === undefined
435
+ ? ''
436
+ : 'sticky z-docked bg-surface [&_th]:border-b [&_th]:border-line [&>tr]:bg-mist/70'), style: stickyTop === undefined
437
+ ? undefined
438
+ : { top: `calc(${stickyTop} + ${stickyHeight}px)` }, children: (0, jsx_runtime_1.jsxs)("tr", { children: [reorder ? ((0, jsx_runtime_1.jsx)("th", { className: ps_design_system_1.PallasoftClassNameService.merge('w-9 min-w-9 pl-3 pr-0', pins.reorder), scope: 'col', children: reorder.enabled ? ((0, jsx_runtime_1.jsx)("span", { className: 'sr-only', children: reorder.columnLabel })) : reorder.disabledReason ? ((0, jsx_runtime_1.jsx)("span", { "aria-label": reorder.disabledReason, className: 'inline-flex cursor-help opacity-40', role: 'img', title: reorder.disabledReason, children: (0, jsx_runtime_1.jsx)(ps_core_components_1.PsDragGrip, {}) })) : null })) : null, isSelectable ? ((0, jsx_runtime_1.jsx)("th", { className: ps_design_system_1.PallasoftClassNameService.merge('w-10 min-w-10 pl-4 pr-0', pins.select), scope: 'col', children: (0, jsx_runtime_1.jsx)(ps_input_components_1.PsCheckbox, { isIndeterminate: !isPageSelected && pageIds.some((rowId) => selectedIdSet.has(rowId)), isSelected: isPageSelected, label: 'Select all rows on this page', onChange: togglePage }) })) : null, visibleColumns.map((column, columnIndex) => {
373
439
  const align = column.align !== null && column.align !== undefined ? column.align : 'left';
374
440
  const isSorted = sort !== null && sort.columnId === column.id;
375
441
  return ((0, jsx_runtime_1.jsx)("th", { "aria-sort": isSorted
@@ -0,0 +1,10 @@
1
+ export interface PsSelectAllMatchingBannerProps {
2
+ className?: string;
3
+ isActive: boolean;
4
+ noun?: string;
5
+ onActivate: () => void;
6
+ onDeactivate: () => void;
7
+ pageCount: number;
8
+ total: number;
9
+ }
10
+ export declare const PsSelectAllMatchingBanner: ({ className, isActive, noun, onActivate, onDeactivate, pageCount, total, }: PsSelectAllMatchingBannerProps) => import("react").JSX.Element;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsSelectAllMatchingBanner = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const ps_design_system_1 = require("@pallasoft/ps-design-system");
6
+ const ps_table_control_classes_1 = require("../constants/ps-table-control-classes");
7
+ const PsSelectAllMatchingBanner = ({ className, isActive, noun = 'rows', onActivate, onDeactivate, pageCount, total, }) => ((0, jsx_runtime_1.jsxs)("div", { "aria-live": 'polite', className: ps_design_system_1.PallasoftClassNameService.merge('flex flex-wrap items-center justify-center gap-x-2 gap-y-1 rounded-ps-control bg-primary/10 px-3 py-2 text-sm text-ink', className), role: 'status', children: [(0, jsx_runtime_1.jsx)("span", { children: isActive
8
+ ? `All ${total.toLocaleString()} ${noun} matching are selected.`
9
+ : `${pageCount.toLocaleString()} ${noun} on this page are selected.` }), (0, jsx_runtime_1.jsx)("button", { className: ps_design_system_1.PallasoftClassNameService.merge(ps_table_control_classes_1.PsTableControlClasses.textButton, 'text-primary'), onClick: isActive ? onDeactivate : onActivate, type: 'button', children: isActive ? 'Clear selection' : `Select all ${total.toLocaleString()} matching` })] }));
10
+ exports.PsSelectAllMatchingBanner = PsSelectAllMatchingBanner;
@@ -1,5 +1,7 @@
1
+ import type { PsTableCursorPaging } from '../interfaces/ps-table-cursor-paging';
1
2
  export interface PsTablePaginationProps {
2
3
  className?: string;
4
+ cursor?: PsTableCursorPaging;
3
5
  isPartial?: boolean;
4
6
  loadedPageCount?: number;
5
7
  onPageChange: (page: number) => void;
@@ -8,4 +10,4 @@ export interface PsTablePaginationProps {
8
10
  pageSize: number;
9
11
  totalRows: number;
10
12
  }
11
- export declare const PsTablePagination: ({ className, isPartial, loadedPageCount, onPageChange, page, pageCount, pageSize, totalRows, }: PsTablePaginationProps) => import("react").JSX.Element;
13
+ export declare const PsTablePagination: ({ className, cursor, isPartial, loadedPageCount, onPageChange, page, pageCount, pageSize, totalRows, }: PsTablePaginationProps) => import("react").JSX.Element;
@@ -4,12 +4,20 @@ exports.PsTablePagination = void 0;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const ps_design_system_1 = require("@pallasoft/ps-design-system");
6
6
  const ps_table_control_classes_1 = require("../constants/ps-table-control-classes");
7
- const PsTablePagination = ({ className, isPartial = false, loadedPageCount, onPageChange, page, pageCount, pageSize, totalRows, }) => {
7
+ const PsTablePagination = ({ className, cursor, isPartial = false, loadedPageCount, onPageChange, page, pageCount, pageSize, totalRows, }) => {
8
8
  const firstRow = totalRows === 0 ? 0 : (page - 1) * pageSize + 1;
9
9
  const lastRow = Math.min(page * pageSize, totalRows);
10
+ const approximately = cursor !== undefined && cursor.isEstimate ? 'about ' : '';
11
+ const pageNumber = cursor === undefined ? Math.min(page, pageCount) : cursor.pageNumber;
12
+ const cursorFirst = cursor === undefined ? firstRow : (cursor.pageNumber - 1) * pageSize + 1;
13
+ const cursorLast = cursor === undefined ? lastRow : Math.min(cursor.pageNumber * pageSize, totalRows);
10
14
  return ((0, jsx_runtime_1.jsxs)("nav", { "aria-label": 'Pagination', className: ps_design_system_1.PallasoftClassNameService.merge('flex flex-wrap items-center justify-between gap-3 border-t border-line px-4 py-3', className), children: [(0, jsx_runtime_1.jsx)("p", { className: 'text-sm text-ink/55', children: totalRows === 0
11
15
  ? 'No rows'
12
- : `${firstRow}–${lastRow} of ${totalRows}${isPartial ? '+' : ''}` }), (0, jsx_runtime_1.jsxs)("div", { className: 'flex items-center gap-2', children: [(0, jsx_runtime_1.jsx)("button", { className: ps_table_control_classes_1.PsTableControlClasses.paginationButton, disabled: page <= 1, onClick: () => onPageChange(page - 1), type: 'button', children: "Previous" }), (0, jsx_runtime_1.jsxs)("span", { className: 'hidden text-sm text-ink/65 sm:inline', children: ["Page ", Math.min(page, pageCount), " of ", pageCount] }), (0, jsx_runtime_1.jsx)("button", { className: ps_table_control_classes_1.PsTableControlClasses.paginationButton, disabled: page >=
13
- (loadedPageCount === undefined ? pageCount : Math.min(loadedPageCount, pageCount)), onClick: () => onPageChange(page + 1), type: 'button', children: "Next" })] })] }));
16
+ : cursor === undefined
17
+ ? `${firstRow}–${lastRow} of ${totalRows}${isPartial ? '+' : ''}`
18
+ : `${cursorFirst}–${cursorLast} of ${approximately}${totalRows.toLocaleString()}` }), (0, jsx_runtime_1.jsxs)("div", { className: 'flex items-center gap-2', children: [cursor !== undefined && cursor.pageSizeOptions !== undefined && cursor.onPageSizeChange ? ((0, jsx_runtime_1.jsxs)("label", { className: 'flex items-center gap-2 text-sm text-ink/65', children: [(0, jsx_runtime_1.jsx)("span", { className: 'hidden sm:inline', children: "Rows per page" }), (0, jsx_runtime_1.jsx)("select", { "aria-label": 'Rows per page', className: 'ps-input-control ps-focus-ring ps-control ps-control-padding rounded-ps-control text-sm', onChange: (event) => cursor.onPageSizeChange?.(Number(event.target.value)), value: pageSize, children: cursor.pageSizeOptions.map((option) => ((0, jsx_runtime_1.jsx)("option", { value: option, children: option }, option))) })] })) : null, (0, jsx_runtime_1.jsx)("button", { className: ps_table_control_classes_1.PsTableControlClasses.paginationButton, disabled: cursor === undefined ? page <= 1 : !cursor.hasPrevious, onClick: () => (cursor === undefined ? onPageChange(page - 1) : cursor.onPrevious()), type: 'button', children: "Previous" }), (0, jsx_runtime_1.jsxs)("span", { className: 'hidden text-sm text-ink/65 sm:inline', children: ["Page ", pageNumber, " of ", approximately, pageCount] }), (0, jsx_runtime_1.jsx)("button", { className: ps_table_control_classes_1.PsTableControlClasses.paginationButton, disabled: cursor === undefined
19
+ ? page >=
20
+ (loadedPageCount === undefined ? pageCount : Math.min(loadedPageCount, pageCount))
21
+ : !cursor.hasNext, onClick: () => (cursor === undefined ? onPageChange(page + 1) : cursor.onNext()), onFocus: cursor === undefined ? undefined : cursor.onNextIntent, onPointerEnter: cursor === undefined ? undefined : cursor.onNextIntent, type: 'button', children: "Next" })] })] }));
14
22
  };
15
23
  exports.PsTablePagination = PsTablePagination;
@@ -0,0 +1,8 @@
1
+ export declare const PsServerRowsConfig: {
2
+ readonly cacheSize: 8;
3
+ readonly debounceMs: 250;
4
+ readonly defaultPageSize: 100;
5
+ readonly idleFallbackMs: 200;
6
+ readonly idleTimeoutMs: 1000;
7
+ readonly pageSizes: readonly [50, 100, 200];
8
+ };
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsServerRowsConfig = void 0;
4
+ exports.PsServerRowsConfig = {
5
+ cacheSize: 8,
6
+ debounceMs: 250,
7
+ defaultPageSize: 100,
8
+ idleFallbackMs: 200,
9
+ idleTimeoutMs: 1000,
10
+ pageSizes: [50, 100, 200],
11
+ };
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Fragment, Suspense, lazy, useCallback, useDeferredValue, useEffect, useMemo, useRef, useState, } from 'react';
2
+ import { Fragment, Suspense, lazy, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
3
3
  import { PsDragGrip, PsSkeletonTable } from '@pallasoft/ps-core-components';
4
4
  import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
5
5
  import { PsCheckbox } from '@pallasoft/ps-input-components';
@@ -20,6 +20,7 @@ import { PsTablePinService } from '../services/ps-table-pin-service';
20
20
  import { PsTableQueryService } from '../services/ps-table-query-service';
21
21
  import { PsTableReorderService } from '../services/ps-table-reorder-service';
22
22
  import { PsTableScrollService } from '../services/ps-table-scroll-service';
23
+ import { PsSelectAllMatchingBanner } from './ps-select-all-matching-banner';
23
24
  import { PsTableBulkBar } from './ps-table-bulk-bar';
24
25
  import { PsTableCardList } from './ps-table-card-list';
25
26
  import { PsTableColumnPicker } from './ps-table-column-picker';
@@ -49,7 +50,7 @@ const alignClassNames = {
49
50
  left: 'text-left',
50
51
  right: 'text-right tabular-nums',
51
52
  };
52
- export const PsDataTable = ({ caption, className, columns, defaultSort, density = 'normal', expandedRowIds, mobileLayout = 'cards', getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading = false, isPending = false, layout = 'auto', onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, paginationPosition = PsTablePaginationPosition.bottom, pinFirstColumn = true, query, renderExpandedRow, reorder, rows, searchingLabel = PsTableConfig.searchingLabel, searchPlaceholder = PsTableConfig.searchPlaceholder, toolbarActions, totalRows, }) => {
53
+ export const PsDataTable = ({ caption, className, clearSelectionKey, columns, cursorPaging, defaultSort, density = 'normal', expandedRowIds, mobileLayout = 'cards', getBulkActions, getRowId, getRowLabel, getSearchText, grouping, isLoading = false, isPending = false, isRefreshing = false, layout = 'auto', onPageChange, onQueryChange, onRowClick, onRowHover, page, pageSize, paginationPosition = PsTablePaginationPosition.bottom, pinFirstColumn = true, query, renderExpandedRow, reorder, rows, searchingLabel = PsTableConfig.searchingLabel, searchPlaceholder = PsTableConfig.searchPlaceholder, selectAllMatching, stickyTop, toolbarActions, totalRows, }) => {
53
54
  const containerRef = useRef(null);
54
55
  const [uncontrolledPage, setUncontrolledPage] = useState(1);
55
56
  const [uncontrolledQuery, setUncontrolledQuery] = useState(() => PsTableQueryService.empty(defaultSort ? defaultSort : null));
@@ -57,7 +58,50 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
57
58
  const [toggledGroupKeys, setToggledGroupKeys] = useState([]);
58
59
  const [closingGroupKeys, setClosingGroupKeys] = useState([]);
59
60
  const [openedGroupKeys, setOpenedGroupKeys] = useState([]);
61
+ const [seenClearKey, setSeenClearKey] = useState(clearSelectionKey);
62
+ const stickyRegionRef = useRef(null);
63
+ const stickySentinelRef = useRef(null);
64
+ const [stickyHeight, setStickyHeight] = useState(0);
65
+ const [isStuck, setIsStuck] = useState(false);
60
66
  const currentQuery = query ? query : uncontrolledQuery;
67
+ if (seenClearKey !== clearSelectionKey) {
68
+ setSeenClearKey(clearSelectionKey);
69
+ setSelectedIds([]);
70
+ }
71
+ useLayoutEffect(() => {
72
+ const region = stickyRegionRef.current;
73
+ if (stickyTop === undefined || region === null)
74
+ return;
75
+ const measure = () => setStickyHeight(region.offsetHeight);
76
+ const observer = new ResizeObserver(measure);
77
+ measure();
78
+ observer.observe(region);
79
+ return () => observer.disconnect();
80
+ }, [stickyTop]);
81
+ useEffect(() => {
82
+ const region = stickyRegionRef.current;
83
+ const sentinel = stickySentinelRef.current;
84
+ if (stickyTop === undefined || region === null || sentinel === null)
85
+ return;
86
+ let frame = 0;
87
+ const check = () => {
88
+ frame = 0;
89
+ setIsStuck(sentinel.getBoundingClientRect().top < Number.parseFloat(getComputedStyle(region).top));
90
+ };
91
+ const schedule = () => {
92
+ if (frame === 0)
93
+ frame = window.requestAnimationFrame(check);
94
+ };
95
+ check();
96
+ window.addEventListener('scroll', schedule, { passive: true });
97
+ window.addEventListener('resize', schedule);
98
+ return () => {
99
+ window.removeEventListener('scroll', schedule);
100
+ window.removeEventListener('resize', schedule);
101
+ if (frame !== 0)
102
+ window.cancelAnimationFrame(frame);
103
+ };
104
+ }, [stickyTop]);
61
105
  const { filters, hiddenColumnIds, search, sort } = currentQuery;
62
106
  const visibleColumns = columns.filter((column) => !hiddenColumnIds.includes(column.id));
63
107
  const fittedWidths = useMemo(() => new Map(columns
@@ -199,7 +243,7 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
199
243
  })
200
244
  : [], [getRowId, getRowLabel, reorder, reorderKey, visibleRows]);
201
245
  const showsPagination = Boolean(pageSize) && (!grouping || isServerPaginated);
202
- const renderPagination = (paginationClassName) => pageSize ? (_jsx(PsTablePagination, { className: paginationClassName, isPartial: isPartial, loadedPageCount: pageCountOf(loadedRowCount), onPageChange: changePage, page: safePage, pageCount: pageCount, pageSize: pageSize, totalRows: rowCount })) : null;
246
+ const renderPagination = (paginationClassName) => pageSize ? (_jsx(PsTablePagination, { className: paginationClassName, cursor: cursorPaging, isPartial: isPartial, loadedPageCount: pageCountOf(loadedRowCount), onPageChange: changePage, page: safePage, pageCount: pageCount, pageSize: pageSize, totalRows: rowCount })) : null;
203
247
  const renderOverlayRow = (rowId, size) => {
204
248
  const row = visibleRows.find((candidate) => getRowId(candidate) === rowId);
205
249
  return row === undefined ? null : (_jsxs("table", { className: 'pointer-events-none w-full table-fixed border-collapse bg-surface text-left', style: size === null ? undefined : { width: size.width }, children: [size === null ? null : (_jsx("colgroup", { children: size.columnWidths.map((width, index) => (_jsx("col", { style: { width } }, index))) })), _jsx("tbody", { children: renderRow(PsTableReorderService.plain, row, undefined, undefined, true) })] }));
@@ -233,12 +277,16 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
233
277
  const selectedRows = selectedIds.length === 0 ? [] : queriedRows.filter((row) => selectedIdSet.has(getRowId(row)));
234
278
  const pageIds = visibleRows.map((row) => getRowId(row));
235
279
  const isPageSelected = pageIds.length > 0 && pageIds.every((rowId) => selectedIdSet.has(rowId));
236
- const togglePage = () => setSelectedIds((current) => {
237
- const pageIdSet = new Set(pageIds);
238
- return isPageSelected
239
- ? current.filter((rowId) => !pageIdSet.has(rowId))
240
- : [...new Set([...current, ...pageIds])];
241
- });
280
+ const togglePage = () => {
281
+ if (isPageSelected && selectAllMatching !== undefined && selectAllMatching.isActive)
282
+ selectAllMatching.onDeactivate();
283
+ setSelectedIds((current) => {
284
+ const pageIdSet = new Set(pageIds);
285
+ return isPageSelected
286
+ ? current.filter((rowId) => !pageIdSet.has(rowId))
287
+ : [...new Set([...current, ...pageIds])];
288
+ });
289
+ };
242
290
  const bodyRef = useRef(null);
243
291
  const [scrollParent, setScrollParent] = useState(null);
244
292
  const [scrollMargin, setScrollMargin] = useState(0);
@@ -316,7 +364,18 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
316
364
  : entries.map((entry) => renderRow(api, entry.row, entry.motion, entry.virtualItem));
317
365
  if (isLoading)
318
366
  return (_jsx(PsSkeletonTable, { className: className, columns: visibleColumns.length, hasToolbar: hasToolbar, rows: PsTableConfig.skeletonRows }));
319
- return (_jsxs("div", { ref: containerRef, className: PallasoftClassNameService.merge('flex min-w-0 flex-col gap-3', className), children: [isSelectable && selectedRows.length > 0 && getBulkActions ? (_jsx(PsTableBulkBar, { actions: getBulkActions(selectedRows), onClear: () => setSelectedIds([]), selectedCount: selectedRows.length })) : hasToolbar || columns.some((column) => column.canHide) ? (_jsx(PsTableToolbar, { actions: _jsxs(_Fragment, { children: [toolbarActions, columns.some((column) => column.canHide) ? (_jsx(PsTableColumnPicker, { columns: columns
367
+ const clearSelection = () => {
368
+ setSelectedIds([]);
369
+ if (selectAllMatching !== undefined && selectAllMatching.isActive)
370
+ selectAllMatching.onDeactivate();
371
+ };
372
+ const banner = selectAllMatching !== undefined &&
373
+ selectedRows.length > 0 &&
374
+ selectAllMatching.total > pageIds.length &&
375
+ (isPageSelected || selectAllMatching.isActive) ? (_jsx(PsSelectAllMatchingBanner, { isActive: selectAllMatching.isActive, noun: selectAllMatching.noun, onActivate: selectAllMatching.onActivate, onDeactivate: clearSelection, pageCount: pageIds.length, total: selectAllMatching.total })) : null;
376
+ const topContent = (_jsxs(_Fragment, { children: [isSelectable && selectedRows.length > 0 && getBulkActions ? (_jsx(PsTableBulkBar, { actions: getBulkActions(selectedRows), onClear: clearSelection, selectedCount: selectAllMatching !== undefined && selectAllMatching.isActive
377
+ ? selectAllMatching.total
378
+ : selectedRows.length })) : hasToolbar || columns.some((column) => column.canHide) ? (_jsx(PsTableToolbar, { actions: _jsxs(_Fragment, { children: [toolbarActions, columns.some((column) => column.canHide) ? (_jsx(PsTableColumnPicker, { columns: columns
320
379
  .filter((column) => column.canHide)
321
380
  .map((column) => ({ id: column.id, label: column.label })), hiddenColumnIds: hiddenColumnIds, onChange: (nextHidden) => updateQuery({ hiddenColumnIds: nextHidden }) })) : null] }), columns: hasFilters ? columns : [], filters: filters, isSearchable: isSearchable, onClearFilters: clearFilters, onFilterChange: (columnId, value) => {
322
381
  updateQuery({ filters: { ...filters, [columnId]: value } });
@@ -324,16 +383,23 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
324
383
  }, onSearchChange: (next) => {
325
384
  updateQuery({ search: next });
326
385
  resetPage();
327
- }, rows: rows, search: search, searchPlaceholder: searchPlaceholder })) : null, _jsxs("div", { className: 'min-w-0 overflow-hidden rounded-[1.25rem] border border-line bg-surface', children: [showsPagination && paginationPosition !== PsTablePaginationPosition.bottom
386
+ }, rows: rows, search: search, searchPlaceholder: searchPlaceholder })) : null, banner] }));
387
+ return (_jsxs("div", { ref: containerRef, className: PallasoftClassNameService.merge('flex min-w-0 flex-col gap-3', className), children: [stickyTop === undefined ? (topContent) : (_jsxs(_Fragment, { children: [_jsx("div", { "aria-hidden": 'true', className: '-mb-3 h-0', ref: stickySentinelRef }), _jsx("div", { className: PallasoftClassNameService.merge('sticky z-sticky -mx-1 flex flex-col gap-3 bg-canvas px-1 pb-2 pt-1 transition-shadow', isStuck ? 'shadow-[0_8px_8px_-8px_rgb(var(--ps-color-ink-rgb,15_23_42)/0.18)]' : ''), ref: stickyRegionRef, style: { top: stickyTop }, children: topContent })] })), _jsxs("div", { "aria-busy": isRefreshing, className: PallasoftClassNameService.merge('min-w-0 rounded-[1.25rem] border border-line bg-surface', stickyTop === undefined ? 'overflow-hidden' : 'overflow-clip'), children: [showsPagination && paginationPosition !== PsTablePaginationPosition.bottom
328
388
  ? renderPagination('border-b border-t-0')
329
- : null, mobileLayout === 'cards' ? (_jsxs("div", { className: 'sm:hidden', children: [reorder && reorder.cardsReason ? (_jsx("p", { className: 'border-b border-line px-4 py-2 text-xs text-ink/55', children: reorder.cardsReason })) : null, grouping
389
+ : null, mobileLayout === 'cards' ? (_jsxs("div", { className: PallasoftClassNameService.merge('transition-opacity duration-150 sm:hidden', isRefreshing ? 'opacity-60' : ''), children: [reorder && reorder.cardsReason ? (_jsx("p", { className: 'border-b border-line px-4 py-2 text-xs text-ink/55', children: reorder.cardsReason })) : null, grouping
330
390
  ? groups.map((group, groupIndex) => {
331
391
  const isExpanded = isGroupExpanded(group.key, groupIndex);
332
392
  return (_jsxs("div", { className: 'border-b border-line last:border-b-0', children: [_jsx(PsTableGroupHeader, { count: group.rows.length, isExpanded: isExpanded, label: grouping.getGroupLabel(group.key, group.rows), onToggle: () => toggleGroup(group.key, isExpanded), summary: grouping.renderGroupSummary
333
393
  ? grouping.renderGroupSummary(group.rows)
334
394
  : undefined }), _jsx(PsCollapse, { isOpen: isExpanded && !closingGroupKeys.includes(group.key), children: renderCardList(group.rows, null) })] }, `group-${group.key}`));
335
395
  })
336
- : renderCardList(shouldVirtualize ? visibleRows.slice(0, cards.count) : visibleRows, visibleRows.length === 0 && isSearching ? (_jsx(PsTableSearchingState, { label: searchingLabel, skeletonRows: PsTableConfig.searchingSkeletonRows })) : visibleRows.length === 0 && isQueried ? (_jsxs("div", { className: 'px-4 py-10 text-center', children: [_jsx("p", { className: 'text-sm font-semibold text-ink', children: PsTableConfig.noMatchesTitle }), _jsx("button", { className: PallasoftClassNameService.merge(PsTableControlClasses.textButton, 'mt-2 text-primary'), onClick: clearFilters, type: 'button', children: "Clear search and filters" })] })) : null), !grouping && shouldVirtualize && cards.count < visibleRows.length ? (_jsx("div", { "aria-hidden": 'true', className: 'h-px', ref: cards.sentinelRef })) : null] })) : null, _jsx("div", { className: PallasoftClassNameService.merge('ps-scrollbar overflow-x-auto overflow-y-hidden overscroll-x-contain', mobileLayout === 'cards' ? 'hidden sm:block' : ''), "data-overflow-end": overflow.edges.end, "data-overflow-start": overflow.edges.start, onScroll: overflow.update, ref: overflow.ref, children: renderReorder((api) => (_jsxs("table", { className: PallasoftClassNameService.merge('w-full border-collapse text-left', layout === 'fixed' ? 'table-fixed' : ''), children: [caption ? _jsx("caption", { className: 'sr-only', children: caption }) : null, _jsx("thead", { className: 'bg-mist/70 text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-ink/45', children: _jsxs("tr", { children: [reorder ? (_jsx("th", { className: PallasoftClassNameService.merge('w-9 min-w-9 pl-3 pr-0', pins.reorder), scope: 'col', children: reorder.enabled ? (_jsx("span", { className: 'sr-only', children: reorder.columnLabel })) : reorder.disabledReason ? (_jsx("span", { "aria-label": reorder.disabledReason, className: 'inline-flex cursor-help opacity-40', role: 'img', title: reorder.disabledReason, children: _jsx(PsDragGrip, {}) })) : null })) : null, isSelectable ? (_jsx("th", { className: PallasoftClassNameService.merge('w-10 min-w-10 pl-4 pr-0', pins.select), scope: 'col', children: _jsx(PsCheckbox, { isIndeterminate: !isPageSelected && pageIds.some((rowId) => selectedIdSet.has(rowId)), isSelected: isPageSelected, label: 'Select all rows on this page', onChange: togglePage }) })) : null, visibleColumns.map((column, columnIndex) => {
396
+ : renderCardList(shouldVirtualize ? visibleRows.slice(0, cards.count) : visibleRows, visibleRows.length === 0 && isSearching ? (_jsx(PsTableSearchingState, { label: searchingLabel, skeletonRows: PsTableConfig.searchingSkeletonRows })) : visibleRows.length === 0 && isQueried ? (_jsxs("div", { className: 'px-4 py-10 text-center', children: [_jsx("p", { className: 'text-sm font-semibold text-ink', children: PsTableConfig.noMatchesTitle }), _jsx("button", { className: PallasoftClassNameService.merge(PsTableControlClasses.textButton, 'mt-2 text-primary'), onClick: clearFilters, type: 'button', children: "Clear search and filters" })] })) : null), !grouping && shouldVirtualize && cards.count < visibleRows.length ? (_jsx("div", { "aria-hidden": 'true', className: 'h-px', ref: cards.sentinelRef })) : null] })) : null, _jsx("div", { className: PallasoftClassNameService.merge(stickyTop === undefined
397
+ ? 'ps-scrollbar overflow-x-auto overflow-y-hidden overscroll-x-contain'
398
+ : 'overflow-x-clip', 'transition-opacity duration-150', isRefreshing ? 'opacity-60' : '', mobileLayout === 'cards' ? 'hidden sm:block' : ''), "data-overflow-end": overflow.edges.end, "data-overflow-start": overflow.edges.start, onScroll: overflow.update, ref: overflow.ref, children: renderReorder((api) => (_jsxs("table", { className: PallasoftClassNameService.merge('w-full border-collapse text-left', layout === 'fixed' ? 'table-fixed' : ''), children: [caption ? _jsx("caption", { className: 'sr-only', children: caption }) : null, _jsx("thead", { className: PallasoftClassNameService.merge('bg-mist/70 text-[0.68rem] font-semibold uppercase tracking-[0.18em] text-ink/45', stickyTop === undefined
399
+ ? ''
400
+ : 'sticky z-docked bg-surface [&_th]:border-b [&_th]:border-line [&>tr]:bg-mist/70'), style: stickyTop === undefined
401
+ ? undefined
402
+ : { top: `calc(${stickyTop} + ${stickyHeight}px)` }, children: _jsxs("tr", { children: [reorder ? (_jsx("th", { className: PallasoftClassNameService.merge('w-9 min-w-9 pl-3 pr-0', pins.reorder), scope: 'col', children: reorder.enabled ? (_jsx("span", { className: 'sr-only', children: reorder.columnLabel })) : reorder.disabledReason ? (_jsx("span", { "aria-label": reorder.disabledReason, className: 'inline-flex cursor-help opacity-40', role: 'img', title: reorder.disabledReason, children: _jsx(PsDragGrip, {}) })) : null })) : null, isSelectable ? (_jsx("th", { className: PallasoftClassNameService.merge('w-10 min-w-10 pl-4 pr-0', pins.select), scope: 'col', children: _jsx(PsCheckbox, { isIndeterminate: !isPageSelected && pageIds.some((rowId) => selectedIdSet.has(rowId)), isSelected: isPageSelected, label: 'Select all rows on this page', onChange: togglePage }) })) : null, visibleColumns.map((column, columnIndex) => {
337
403
  const align = column.align !== null && column.align !== undefined ? column.align : 'left';
338
404
  const isSorted = sort !== null && sort.columnId === column.id;
339
405
  return (_jsx("th", { "aria-sort": isSorted
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
3
+ import { PsTableControlClasses } from '../constants/ps-table-control-classes';
4
+ export const PsSelectAllMatchingBanner = ({ className, isActive, noun = 'rows', onActivate, onDeactivate, pageCount, total, }) => (_jsxs("div", { "aria-live": 'polite', className: PallasoftClassNameService.merge('flex flex-wrap items-center justify-center gap-x-2 gap-y-1 rounded-ps-control bg-primary/10 px-3 py-2 text-sm text-ink', className), role: 'status', children: [_jsx("span", { children: isActive
5
+ ? `All ${total.toLocaleString()} ${noun} matching are selected.`
6
+ : `${pageCount.toLocaleString()} ${noun} on this page are selected.` }), _jsx("button", { className: PallasoftClassNameService.merge(PsTableControlClasses.textButton, 'text-primary'), onClick: isActive ? onDeactivate : onActivate, type: 'button', children: isActive ? 'Clear selection' : `Select all ${total.toLocaleString()} matching` })] }));
@@ -1,11 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
3
3
  import { PsTableControlClasses } from '../constants/ps-table-control-classes';
4
- export const PsTablePagination = ({ className, isPartial = false, loadedPageCount, onPageChange, page, pageCount, pageSize, totalRows, }) => {
4
+ export const PsTablePagination = ({ className, cursor, isPartial = false, loadedPageCount, onPageChange, page, pageCount, pageSize, totalRows, }) => {
5
5
  const firstRow = totalRows === 0 ? 0 : (page - 1) * pageSize + 1;
6
6
  const lastRow = Math.min(page * pageSize, totalRows);
7
+ const approximately = cursor !== undefined && cursor.isEstimate ? 'about ' : '';
8
+ const pageNumber = cursor === undefined ? Math.min(page, pageCount) : cursor.pageNumber;
9
+ const cursorFirst = cursor === undefined ? firstRow : (cursor.pageNumber - 1) * pageSize + 1;
10
+ const cursorLast = cursor === undefined ? lastRow : Math.min(cursor.pageNumber * pageSize, totalRows);
7
11
  return (_jsxs("nav", { "aria-label": 'Pagination', className: PallasoftClassNameService.merge('flex flex-wrap items-center justify-between gap-3 border-t border-line px-4 py-3', className), children: [_jsx("p", { className: 'text-sm text-ink/55', children: totalRows === 0
8
12
  ? 'No rows'
9
- : `${firstRow}–${lastRow} of ${totalRows}${isPartial ? '+' : ''}` }), _jsxs("div", { className: 'flex items-center gap-2', children: [_jsx("button", { className: PsTableControlClasses.paginationButton, disabled: page <= 1, onClick: () => onPageChange(page - 1), type: 'button', children: "Previous" }), _jsxs("span", { className: 'hidden text-sm text-ink/65 sm:inline', children: ["Page ", Math.min(page, pageCount), " of ", pageCount] }), _jsx("button", { className: PsTableControlClasses.paginationButton, disabled: page >=
10
- (loadedPageCount === undefined ? pageCount : Math.min(loadedPageCount, pageCount)), onClick: () => onPageChange(page + 1), type: 'button', children: "Next" })] })] }));
13
+ : cursor === undefined
14
+ ? `${firstRow}–${lastRow} of ${totalRows}${isPartial ? '+' : ''}`
15
+ : `${cursorFirst}–${cursorLast} of ${approximately}${totalRows.toLocaleString()}` }), _jsxs("div", { className: 'flex items-center gap-2', children: [cursor !== undefined && cursor.pageSizeOptions !== undefined && cursor.onPageSizeChange ? (_jsxs("label", { className: 'flex items-center gap-2 text-sm text-ink/65', children: [_jsx("span", { className: 'hidden sm:inline', children: "Rows per page" }), _jsx("select", { "aria-label": 'Rows per page', className: 'ps-input-control ps-focus-ring ps-control ps-control-padding rounded-ps-control text-sm', onChange: (event) => cursor.onPageSizeChange?.(Number(event.target.value)), value: pageSize, children: cursor.pageSizeOptions.map((option) => (_jsx("option", { value: option, children: option }, option))) })] })) : null, _jsx("button", { className: PsTableControlClasses.paginationButton, disabled: cursor === undefined ? page <= 1 : !cursor.hasPrevious, onClick: () => (cursor === undefined ? onPageChange(page - 1) : cursor.onPrevious()), type: 'button', children: "Previous" }), _jsxs("span", { className: 'hidden text-sm text-ink/65 sm:inline', children: ["Page ", pageNumber, " of ", approximately, pageCount] }), _jsx("button", { className: PsTableControlClasses.paginationButton, disabled: cursor === undefined
16
+ ? page >=
17
+ (loadedPageCount === undefined ? pageCount : Math.min(loadedPageCount, pageCount))
18
+ : !cursor.hasNext, onClick: () => (cursor === undefined ? onPageChange(page + 1) : cursor.onNext()), onFocus: cursor === undefined ? undefined : cursor.onNextIntent, onPointerEnter: cursor === undefined ? undefined : cursor.onNextIntent, type: 'button', children: "Next" })] })] }));
11
19
  };
@@ -0,0 +1,8 @@
1
+ export const PsServerRowsConfig = {
2
+ cacheSize: 8,
3
+ debounceMs: 250,
4
+ defaultPageSize: 100,
5
+ idleFallbackMs: 200,
6
+ idleTimeoutMs: 1000,
7
+ pageSizes: [50, 100, 200],
8
+ };
@@ -0,0 +1,209 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { PsServerRowsConfig } from '../constants/ps-server-rows-config';
3
+ import { PsServerRowsService } from '../services/ps-server-rows-service';
4
+ const scheduleIdle = (run) => {
5
+ if (typeof window.requestIdleCallback === 'function') {
6
+ const handle = window.requestIdleCallback(run, { timeout: PsServerRowsConfig.idleTimeoutMs });
7
+ return () => window.cancelIdleCallback(handle);
8
+ }
9
+ const timer = window.setTimeout(run, PsServerRowsConfig.idleFallbackMs);
10
+ return () => window.clearTimeout(timer);
11
+ };
12
+ export const usePsServerRows = ({ columns, debounceMs = PsServerRowsConfig.debounceMs, getSearchText, initialPage = null, initialQuery, isPrefetching = true, load, pageSize, }) => {
13
+ const [query, setQuery] = useState(initialQuery);
14
+ const [search, setSearch] = useState(initialQuery.search);
15
+ const [reloadToken, setReloadToken] = useState(0);
16
+ const [isLoading, setIsLoading] = useState(initialPage === null);
17
+ const [error, setError] = useState(null);
18
+ const queryKey = PsServerRowsService.queryKey(query, search, pageSize);
19
+ const [navigation, setNavigation] = useState({
20
+ cursor: null,
21
+ pageNumber: 1,
22
+ queryKey,
23
+ });
24
+ const [entry, setEntry] = useState(() => initialPage === null
25
+ ? null
26
+ : {
27
+ key: PsServerRowsService.cacheKey(queryKey, null),
28
+ page: initialPage,
29
+ queryKey,
30
+ });
31
+ const cache = useRef(new Map());
32
+ const inflight = useRef(new Map());
33
+ const loadRef = useRef(load);
34
+ const limitRef = useRef(pageSize);
35
+ const queryRef = useRef(query);
36
+ const searchRef = useRef(search);
37
+ const isPrimed = useRef(false);
38
+ loadRef.current = load;
39
+ limitRef.current = pageSize;
40
+ queryRef.current = query;
41
+ searchRef.current = search;
42
+ if (initialPage !== null && !isPrimed.current) {
43
+ isPrimed.current = true;
44
+ PsServerRowsService.remember(cache.current, PsServerRowsService.cacheKey(queryKey, null), initialPage);
45
+ }
46
+ if (navigation.queryKey !== queryKey)
47
+ setNavigation({ cursor: null, pageNumber: 1, queryKey });
48
+ useEffect(() => {
49
+ if (query.search === search)
50
+ return;
51
+ const timer = window.setTimeout(() => setSearch(query.search), debounceMs);
52
+ return () => window.clearTimeout(timer);
53
+ }, [debounceMs, query.search, search]);
54
+ const fetchPage = useCallback((cursor, signal, key) => {
55
+ const pending = inflight.current.get(key);
56
+ if (pending !== undefined)
57
+ return pending;
58
+ const request = loadRef.current({ ...queryRef.current, search: searchRef.current }, cursor, signal, limitRef.current);
59
+ const tracked = request.then((page) => {
60
+ inflight.current.delete(key);
61
+ if (!signal.aborted)
62
+ PsServerRowsService.remember(cache.current, key, page);
63
+ return page;
64
+ }, (failure) => {
65
+ inflight.current.delete(key);
66
+ throw failure;
67
+ });
68
+ inflight.current.set(key, tracked);
69
+ return tracked;
70
+ }, []);
71
+ const generation = useRef(null);
72
+ useEffect(() => {
73
+ const controller = new AbortController();
74
+ generation.current = controller;
75
+ return () => {
76
+ controller.abort();
77
+ inflight.current.clear();
78
+ };
79
+ }, [queryKey, reloadToken]);
80
+ useEffect(() => {
81
+ const controller = generation.current;
82
+ if (controller === null)
83
+ return;
84
+ const key = PsServerRowsService.cacheKey(queryKey, navigation.cursor);
85
+ const cached = PsServerRowsService.recall(cache.current, key);
86
+ let isStale = false;
87
+ if (cached !== undefined) {
88
+ setEntry({ key, page: cached, queryKey });
89
+ setIsLoading(false);
90
+ setError(null);
91
+ return;
92
+ }
93
+ setIsLoading(true);
94
+ setError(null);
95
+ fetchPage(navigation.cursor, controller.signal, key).then((page) => {
96
+ if (isStale || controller.signal.aborted)
97
+ return;
98
+ setEntry({ key, page, queryKey });
99
+ setIsLoading(false);
100
+ }, (failure) => {
101
+ if (isStale || controller.signal.aborted)
102
+ return;
103
+ setError(failure);
104
+ setIsLoading(false);
105
+ });
106
+ return () => {
107
+ isStale = true;
108
+ };
109
+ }, [fetchPage, navigation.cursor, queryKey, reloadToken]);
110
+ const nextCursor = entry !== null && entry.queryKey === queryKey ? entry.page.nextCursor : null;
111
+ const prefetch = useCallback(() => {
112
+ const controller = generation.current;
113
+ if (controller === null || controller.signal.aborted || nextCursor === null)
114
+ return;
115
+ const key = PsServerRowsService.cacheKey(queryKey, nextCursor);
116
+ if (cache.current.has(key) || inflight.current.has(key))
117
+ return;
118
+ fetchPage(nextCursor, controller.signal, key).catch(() => undefined);
119
+ }, [fetchPage, nextCursor, queryKey]);
120
+ useEffect(() => {
121
+ if (!isPrefetching || isLoading || nextCursor === null)
122
+ return;
123
+ return scheduleIdle(prefetch);
124
+ }, [isLoading, isPrefetching, nextCursor, prefetch]);
125
+ const onQueryChange = useCallback((next) => setQuery(next), []);
126
+ const goNext = useCallback(() => {
127
+ if (nextCursor === null)
128
+ return;
129
+ setNavigation((current) => ({
130
+ ...current,
131
+ cursor: nextCursor,
132
+ pageNumber: current.pageNumber + 1,
133
+ }));
134
+ }, [nextCursor]);
135
+ const previousCursor = entry !== null && entry.queryKey === queryKey ? entry.page.prevCursor : null;
136
+ const goPrevious = useCallback(() => {
137
+ if (previousCursor === null)
138
+ return;
139
+ setNavigation((current) => ({
140
+ ...current,
141
+ cursor: previousCursor,
142
+ pageNumber: Math.max(current.pageNumber - 1, 1),
143
+ }));
144
+ }, [previousCursor]);
145
+ const reload = useCallback(() => {
146
+ cache.current.clear();
147
+ setNavigation((current) => ({ ...current, cursor: null, pageNumber: 1 }));
148
+ setReloadToken((current) => current + 1);
149
+ }, []);
150
+ const retry = useCallback(() => setReloadToken((current) => current + 1), []);
151
+ const updateRows = useCallback((update, totalDelta = 0) => {
152
+ setEntry((current) => {
153
+ if (current === null)
154
+ return current;
155
+ const page = {
156
+ ...current.page,
157
+ rows: update(current.page.rows),
158
+ total: Math.max(current.page.total + totalDelta, 0),
159
+ };
160
+ cache.current.clear();
161
+ PsServerRowsService.remember(cache.current, current.key, page);
162
+ return { ...current, page };
163
+ });
164
+ }, []);
165
+ const isCurrent = entry !== null &&
166
+ entry.queryKey === queryKey &&
167
+ entry.key === PsServerRowsService.cacheKey(queryKey, navigation.cursor);
168
+ const baseRows = useMemo(() => (entry === null ? [] : entry.page.rows), [entry]);
169
+ const isSameQuery = entry !== null && entry.queryKey === queryKey;
170
+ const rows = useMemo(() => isSameQuery ? baseRows : PsServerRowsService.preview(baseRows, columns, query, getSearchText), [baseRows, columns, getSearchText, isSameQuery, query]);
171
+ const total = entry === null ? 0 : entry.page.total;
172
+ const totalIsEstimate = entry === null ? false : entry.page.totalIsEstimate;
173
+ const isPending = isLoading || query.search !== search || !isCurrent;
174
+ const cursorPaging = useMemo(() => ({
175
+ hasNext: nextCursor !== null,
176
+ hasPrevious: previousCursor !== null,
177
+ isEstimate: totalIsEstimate,
178
+ isPending,
179
+ onNext: goNext,
180
+ onNextIntent: prefetch,
181
+ onPrevious: goPrevious,
182
+ pageNumber: navigation.pageNumber,
183
+ }), [
184
+ goNext,
185
+ goPrevious,
186
+ isPending,
187
+ navigation.pageNumber,
188
+ nextCursor,
189
+ prefetch,
190
+ previousCursor,
191
+ totalIsEstimate,
192
+ ]);
193
+ return {
194
+ cursorPaging,
195
+ error,
196
+ hasFailed: error !== null,
197
+ isPending,
198
+ onQueryChange,
199
+ pageCount: PsServerRowsService.pageCount(total, pageSize),
200
+ pageNumber: navigation.pageNumber,
201
+ query,
202
+ reload,
203
+ retry,
204
+ rows,
205
+ total,
206
+ totalIsEstimate,
207
+ updateRows,
208
+ };
209
+ };
package/dist/esm/index.js CHANGED
@@ -49,3 +49,13 @@ export * from './constants/ps-table-drop-placement';
49
49
  export * from './services/ps-table-reorder-service';
50
50
  export * from './constants/ps-table-pagination-position';
51
51
  export * from './interfaces/table-drag-size';
52
+ export * from './components/ps-select-all-matching-banner';
53
+ export * from './constants/ps-server-rows-config';
54
+ export * from './hooks/use-ps-server-rows';
55
+ export * from './interfaces/ps-server-entry';
56
+ export * from './interfaces/ps-server-page';
57
+ export * from './interfaces/ps-server-rows-input';
58
+ export * from './interfaces/ps-server-rows-result';
59
+ export * from './interfaces/ps-table-cursor-paging';
60
+ export * from './interfaces/ps-table-select-all-matching';
61
+ export * from './services/ps-server-rows-service';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,36 @@
1
+ import { PsServerRowsConfig } from '../constants/ps-server-rows-config';
2
+ import { PsTableQueryService } from './ps-table-query-service';
3
+ export class PsServerRowsService {
4
+ static queryKey(query, search, pageSize) {
5
+ const filters = Object.entries(query.filters)
6
+ .filter(([, value]) => PsTableQueryService.isActive(value))
7
+ .sort(([left], [right]) => left.localeCompare(right));
8
+ return JSON.stringify([filters, search.trim(), query.sort, pageSize]);
9
+ }
10
+ static cacheKey(queryKey, cursor) {
11
+ return `${queryKey}|${cursor === null ? '' : cursor}`;
12
+ }
13
+ static remember(cache, key, page) {
14
+ cache.delete(key);
15
+ cache.set(key, page);
16
+ for (const oldest of cache.keys()) {
17
+ if (cache.size <= PsServerRowsConfig.cacheSize)
18
+ return;
19
+ cache.delete(oldest);
20
+ }
21
+ }
22
+ static recall(cache, key) {
23
+ const page = cache.get(key);
24
+ if (page === undefined)
25
+ return undefined;
26
+ cache.delete(key);
27
+ cache.set(key, page);
28
+ return page;
29
+ }
30
+ static preview(rows, columns, query, getSearchText) {
31
+ return PsTableQueryService.apply(rows, columns, query.filters, query.search, getSearchText, query.sort);
32
+ }
33
+ static pageCount(total, pageSize) {
34
+ return Math.max(Math.ceil(total / pageSize), 1);
35
+ }
36
+ }
@@ -0,0 +1,3 @@
1
+ import type { PsServerRowsInput } from '../interfaces/ps-server-rows-input';
2
+ import type { PsServerRowsResult } from '../interfaces/ps-server-rows-result';
3
+ export declare const usePsServerRows: <RowData>({ columns, debounceMs, getSearchText, initialPage, initialQuery, isPrefetching, load, pageSize, }: PsServerRowsInput<RowData>) => PsServerRowsResult<RowData>;
@@ -0,0 +1,213 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.usePsServerRows = void 0;
4
+ const react_1 = require("react");
5
+ const ps_server_rows_config_1 = require("../constants/ps-server-rows-config");
6
+ const ps_server_rows_service_1 = require("../services/ps-server-rows-service");
7
+ const scheduleIdle = (run) => {
8
+ if (typeof window.requestIdleCallback === 'function') {
9
+ const handle = window.requestIdleCallback(run, { timeout: ps_server_rows_config_1.PsServerRowsConfig.idleTimeoutMs });
10
+ return () => window.cancelIdleCallback(handle);
11
+ }
12
+ const timer = window.setTimeout(run, ps_server_rows_config_1.PsServerRowsConfig.idleFallbackMs);
13
+ return () => window.clearTimeout(timer);
14
+ };
15
+ const usePsServerRows = ({ columns, debounceMs = ps_server_rows_config_1.PsServerRowsConfig.debounceMs, getSearchText, initialPage = null, initialQuery, isPrefetching = true, load, pageSize, }) => {
16
+ const [query, setQuery] = (0, react_1.useState)(initialQuery);
17
+ const [search, setSearch] = (0, react_1.useState)(initialQuery.search);
18
+ const [reloadToken, setReloadToken] = (0, react_1.useState)(0);
19
+ const [isLoading, setIsLoading] = (0, react_1.useState)(initialPage === null);
20
+ const [error, setError] = (0, react_1.useState)(null);
21
+ const queryKey = ps_server_rows_service_1.PsServerRowsService.queryKey(query, search, pageSize);
22
+ const [navigation, setNavigation] = (0, react_1.useState)({
23
+ cursor: null,
24
+ pageNumber: 1,
25
+ queryKey,
26
+ });
27
+ const [entry, setEntry] = (0, react_1.useState)(() => initialPage === null
28
+ ? null
29
+ : {
30
+ key: ps_server_rows_service_1.PsServerRowsService.cacheKey(queryKey, null),
31
+ page: initialPage,
32
+ queryKey,
33
+ });
34
+ const cache = (0, react_1.useRef)(new Map());
35
+ const inflight = (0, react_1.useRef)(new Map());
36
+ const loadRef = (0, react_1.useRef)(load);
37
+ const limitRef = (0, react_1.useRef)(pageSize);
38
+ const queryRef = (0, react_1.useRef)(query);
39
+ const searchRef = (0, react_1.useRef)(search);
40
+ const isPrimed = (0, react_1.useRef)(false);
41
+ loadRef.current = load;
42
+ limitRef.current = pageSize;
43
+ queryRef.current = query;
44
+ searchRef.current = search;
45
+ if (initialPage !== null && !isPrimed.current) {
46
+ isPrimed.current = true;
47
+ ps_server_rows_service_1.PsServerRowsService.remember(cache.current, ps_server_rows_service_1.PsServerRowsService.cacheKey(queryKey, null), initialPage);
48
+ }
49
+ if (navigation.queryKey !== queryKey)
50
+ setNavigation({ cursor: null, pageNumber: 1, queryKey });
51
+ (0, react_1.useEffect)(() => {
52
+ if (query.search === search)
53
+ return;
54
+ const timer = window.setTimeout(() => setSearch(query.search), debounceMs);
55
+ return () => window.clearTimeout(timer);
56
+ }, [debounceMs, query.search, search]);
57
+ const fetchPage = (0, react_1.useCallback)((cursor, signal, key) => {
58
+ const pending = inflight.current.get(key);
59
+ if (pending !== undefined)
60
+ return pending;
61
+ const request = loadRef.current({ ...queryRef.current, search: searchRef.current }, cursor, signal, limitRef.current);
62
+ const tracked = request.then((page) => {
63
+ inflight.current.delete(key);
64
+ if (!signal.aborted)
65
+ ps_server_rows_service_1.PsServerRowsService.remember(cache.current, key, page);
66
+ return page;
67
+ }, (failure) => {
68
+ inflight.current.delete(key);
69
+ throw failure;
70
+ });
71
+ inflight.current.set(key, tracked);
72
+ return tracked;
73
+ }, []);
74
+ const generation = (0, react_1.useRef)(null);
75
+ (0, react_1.useEffect)(() => {
76
+ const controller = new AbortController();
77
+ generation.current = controller;
78
+ return () => {
79
+ controller.abort();
80
+ inflight.current.clear();
81
+ };
82
+ }, [queryKey, reloadToken]);
83
+ (0, react_1.useEffect)(() => {
84
+ const controller = generation.current;
85
+ if (controller === null)
86
+ return;
87
+ const key = ps_server_rows_service_1.PsServerRowsService.cacheKey(queryKey, navigation.cursor);
88
+ const cached = ps_server_rows_service_1.PsServerRowsService.recall(cache.current, key);
89
+ let isStale = false;
90
+ if (cached !== undefined) {
91
+ setEntry({ key, page: cached, queryKey });
92
+ setIsLoading(false);
93
+ setError(null);
94
+ return;
95
+ }
96
+ setIsLoading(true);
97
+ setError(null);
98
+ fetchPage(navigation.cursor, controller.signal, key).then((page) => {
99
+ if (isStale || controller.signal.aborted)
100
+ return;
101
+ setEntry({ key, page, queryKey });
102
+ setIsLoading(false);
103
+ }, (failure) => {
104
+ if (isStale || controller.signal.aborted)
105
+ return;
106
+ setError(failure);
107
+ setIsLoading(false);
108
+ });
109
+ return () => {
110
+ isStale = true;
111
+ };
112
+ }, [fetchPage, navigation.cursor, queryKey, reloadToken]);
113
+ const nextCursor = entry !== null && entry.queryKey === queryKey ? entry.page.nextCursor : null;
114
+ const prefetch = (0, react_1.useCallback)(() => {
115
+ const controller = generation.current;
116
+ if (controller === null || controller.signal.aborted || nextCursor === null)
117
+ return;
118
+ const key = ps_server_rows_service_1.PsServerRowsService.cacheKey(queryKey, nextCursor);
119
+ if (cache.current.has(key) || inflight.current.has(key))
120
+ return;
121
+ fetchPage(nextCursor, controller.signal, key).catch(() => undefined);
122
+ }, [fetchPage, nextCursor, queryKey]);
123
+ (0, react_1.useEffect)(() => {
124
+ if (!isPrefetching || isLoading || nextCursor === null)
125
+ return;
126
+ return scheduleIdle(prefetch);
127
+ }, [isLoading, isPrefetching, nextCursor, prefetch]);
128
+ const onQueryChange = (0, react_1.useCallback)((next) => setQuery(next), []);
129
+ const goNext = (0, react_1.useCallback)(() => {
130
+ if (nextCursor === null)
131
+ return;
132
+ setNavigation((current) => ({
133
+ ...current,
134
+ cursor: nextCursor,
135
+ pageNumber: current.pageNumber + 1,
136
+ }));
137
+ }, [nextCursor]);
138
+ const previousCursor = entry !== null && entry.queryKey === queryKey ? entry.page.prevCursor : null;
139
+ const goPrevious = (0, react_1.useCallback)(() => {
140
+ if (previousCursor === null)
141
+ return;
142
+ setNavigation((current) => ({
143
+ ...current,
144
+ cursor: previousCursor,
145
+ pageNumber: Math.max(current.pageNumber - 1, 1),
146
+ }));
147
+ }, [previousCursor]);
148
+ const reload = (0, react_1.useCallback)(() => {
149
+ cache.current.clear();
150
+ setNavigation((current) => ({ ...current, cursor: null, pageNumber: 1 }));
151
+ setReloadToken((current) => current + 1);
152
+ }, []);
153
+ const retry = (0, react_1.useCallback)(() => setReloadToken((current) => current + 1), []);
154
+ const updateRows = (0, react_1.useCallback)((update, totalDelta = 0) => {
155
+ setEntry((current) => {
156
+ if (current === null)
157
+ return current;
158
+ const page = {
159
+ ...current.page,
160
+ rows: update(current.page.rows),
161
+ total: Math.max(current.page.total + totalDelta, 0),
162
+ };
163
+ cache.current.clear();
164
+ ps_server_rows_service_1.PsServerRowsService.remember(cache.current, current.key, page);
165
+ return { ...current, page };
166
+ });
167
+ }, []);
168
+ const isCurrent = entry !== null &&
169
+ entry.queryKey === queryKey &&
170
+ entry.key === ps_server_rows_service_1.PsServerRowsService.cacheKey(queryKey, navigation.cursor);
171
+ const baseRows = (0, react_1.useMemo)(() => (entry === null ? [] : entry.page.rows), [entry]);
172
+ const isSameQuery = entry !== null && entry.queryKey === queryKey;
173
+ const rows = (0, react_1.useMemo)(() => isSameQuery ? baseRows : ps_server_rows_service_1.PsServerRowsService.preview(baseRows, columns, query, getSearchText), [baseRows, columns, getSearchText, isSameQuery, query]);
174
+ const total = entry === null ? 0 : entry.page.total;
175
+ const totalIsEstimate = entry === null ? false : entry.page.totalIsEstimate;
176
+ const isPending = isLoading || query.search !== search || !isCurrent;
177
+ const cursorPaging = (0, react_1.useMemo)(() => ({
178
+ hasNext: nextCursor !== null,
179
+ hasPrevious: previousCursor !== null,
180
+ isEstimate: totalIsEstimate,
181
+ isPending,
182
+ onNext: goNext,
183
+ onNextIntent: prefetch,
184
+ onPrevious: goPrevious,
185
+ pageNumber: navigation.pageNumber,
186
+ }), [
187
+ goNext,
188
+ goPrevious,
189
+ isPending,
190
+ navigation.pageNumber,
191
+ nextCursor,
192
+ prefetch,
193
+ previousCursor,
194
+ totalIsEstimate,
195
+ ]);
196
+ return {
197
+ cursorPaging,
198
+ error,
199
+ hasFailed: error !== null,
200
+ isPending,
201
+ onQueryChange,
202
+ pageCount: ps_server_rows_service_1.PsServerRowsService.pageCount(total, pageSize),
203
+ pageNumber: navigation.pageNumber,
204
+ query,
205
+ reload,
206
+ retry,
207
+ rows,
208
+ total,
209
+ totalIsEstimate,
210
+ updateRows,
211
+ };
212
+ };
213
+ exports.usePsServerRows = usePsServerRows;
package/dist/index.d.ts CHANGED
@@ -49,3 +49,13 @@ export * from './constants/ps-table-drop-placement';
49
49
  export * from './services/ps-table-reorder-service';
50
50
  export * from './constants/ps-table-pagination-position';
51
51
  export * from './interfaces/table-drag-size';
52
+ export * from './components/ps-select-all-matching-banner';
53
+ export * from './constants/ps-server-rows-config';
54
+ export * from './hooks/use-ps-server-rows';
55
+ export * from './interfaces/ps-server-entry';
56
+ export * from './interfaces/ps-server-page';
57
+ export * from './interfaces/ps-server-rows-input';
58
+ export * from './interfaces/ps-server-rows-result';
59
+ export * from './interfaces/ps-table-cursor-paging';
60
+ export * from './interfaces/ps-table-select-all-matching';
61
+ export * from './services/ps-server-rows-service';
package/dist/index.js CHANGED
@@ -65,3 +65,13 @@ __exportStar(require("./constants/ps-table-drop-placement"), exports);
65
65
  __exportStar(require("./services/ps-table-reorder-service"), exports);
66
66
  __exportStar(require("./constants/ps-table-pagination-position"), exports);
67
67
  __exportStar(require("./interfaces/table-drag-size"), exports);
68
+ __exportStar(require("./components/ps-select-all-matching-banner"), exports);
69
+ __exportStar(require("./constants/ps-server-rows-config"), exports);
70
+ __exportStar(require("./hooks/use-ps-server-rows"), exports);
71
+ __exportStar(require("./interfaces/ps-server-entry"), exports);
72
+ __exportStar(require("./interfaces/ps-server-page"), exports);
73
+ __exportStar(require("./interfaces/ps-server-rows-input"), exports);
74
+ __exportStar(require("./interfaces/ps-server-rows-result"), exports);
75
+ __exportStar(require("./interfaces/ps-table-cursor-paging"), exports);
76
+ __exportStar(require("./interfaces/ps-table-select-all-matching"), exports);
77
+ __exportStar(require("./services/ps-server-rows-service"), exports);
@@ -0,0 +1,6 @@
1
+ import type { PsServerPage } from './ps-server-page';
2
+ export interface PsServerEntry<RowData> {
3
+ key: string;
4
+ page: PsServerPage<RowData>;
5
+ queryKey: string;
6
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,7 @@
1
+ export interface PsServerPage<RowData> {
2
+ nextCursor: string | null;
3
+ prevCursor: string | null;
4
+ rows: RowData[];
5
+ total: number;
6
+ totalIsEstimate: boolean;
7
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,13 @@
1
+ import type { PsServerPage } from './ps-server-page';
2
+ import type { PsTableColumn } from './table-column';
3
+ import type { PsTableQuery } from './table-query';
4
+ export interface PsServerRowsInput<RowData> {
5
+ columns: PsTableColumn<RowData>[];
6
+ debounceMs?: number;
7
+ getSearchText?: (row: RowData) => string;
8
+ initialPage?: PsServerPage<RowData> | null;
9
+ initialQuery: PsTableQuery;
10
+ isPrefetching?: boolean;
11
+ load: (query: PsTableQuery, cursor: string | null, signal: AbortSignal, limit: number) => Promise<PsServerPage<RowData>>;
12
+ pageSize: number;
13
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,18 @@
1
+ import type { PsTableCursorPaging } from './ps-table-cursor-paging';
2
+ import type { PsTableQuery } from './table-query';
3
+ export interface PsServerRowsResult<RowData> {
4
+ cursorPaging: PsTableCursorPaging;
5
+ error: unknown;
6
+ hasFailed: boolean;
7
+ isPending: boolean;
8
+ onQueryChange: (query: PsTableQuery) => void;
9
+ pageCount: number;
10
+ pageNumber: number;
11
+ query: PsTableQuery;
12
+ reload: () => void;
13
+ retry: () => void;
14
+ rows: RowData[];
15
+ total: number;
16
+ totalIsEstimate: boolean;
17
+ updateRows: (update: (rows: RowData[]) => RowData[], totalDelta?: number) => void;
18
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,12 @@
1
+ export interface PsTableCursorPaging {
2
+ hasNext: boolean;
3
+ hasPrevious: boolean;
4
+ isEstimate: boolean;
5
+ isPending?: boolean;
6
+ onNext: () => void;
7
+ onNextIntent?: () => void;
8
+ onPageSizeChange?: (pageSize: number) => void;
9
+ onPrevious: () => void;
10
+ pageNumber: number;
11
+ pageSizeOptions?: number[];
12
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,7 @@
1
+ export interface PsTableSelectAllMatching {
2
+ isActive: boolean;
3
+ noun?: string;
4
+ onActivate: () => void;
5
+ onDeactivate: () => void;
6
+ total: number;
7
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,11 @@
1
+ import type { PsServerPage } from '../interfaces/ps-server-page';
2
+ import type { PsTableColumn } from '../interfaces/table-column';
3
+ import type { PsTableQuery } from '../interfaces/table-query';
4
+ export declare class PsServerRowsService {
5
+ static queryKey(query: PsTableQuery, search: string, pageSize: number): string;
6
+ static cacheKey(queryKey: string, cursor: string | null): string;
7
+ static remember<RowData>(cache: Map<string, PsServerPage<RowData>>, key: string, page: PsServerPage<RowData>): void;
8
+ static recall<RowData>(cache: Map<string, PsServerPage<RowData>>, key: string): PsServerPage<RowData> | undefined;
9
+ static preview<RowData>(rows: RowData[], columns: PsTableColumn<RowData>[], query: PsTableQuery, getSearchText: ((row: RowData) => string) | undefined): RowData[];
10
+ static pageCount(total: number, pageSize: number): number;
11
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsServerRowsService = void 0;
4
+ const ps_server_rows_config_1 = require("../constants/ps-server-rows-config");
5
+ const ps_table_query_service_1 = require("./ps-table-query-service");
6
+ class PsServerRowsService {
7
+ static queryKey(query, search, pageSize) {
8
+ const filters = Object.entries(query.filters)
9
+ .filter(([, value]) => ps_table_query_service_1.PsTableQueryService.isActive(value))
10
+ .sort(([left], [right]) => left.localeCompare(right));
11
+ return JSON.stringify([filters, search.trim(), query.sort, pageSize]);
12
+ }
13
+ static cacheKey(queryKey, cursor) {
14
+ return `${queryKey}|${cursor === null ? '' : cursor}`;
15
+ }
16
+ static remember(cache, key, page) {
17
+ cache.delete(key);
18
+ cache.set(key, page);
19
+ for (const oldest of cache.keys()) {
20
+ if (cache.size <= ps_server_rows_config_1.PsServerRowsConfig.cacheSize)
21
+ return;
22
+ cache.delete(oldest);
23
+ }
24
+ }
25
+ static recall(cache, key) {
26
+ const page = cache.get(key);
27
+ if (page === undefined)
28
+ return undefined;
29
+ cache.delete(key);
30
+ cache.set(key, page);
31
+ return page;
32
+ }
33
+ static preview(rows, columns, query, getSearchText) {
34
+ return ps_table_query_service_1.PsTableQueryService.apply(rows, columns, query.filters, query.search, getSearchText, query.sort);
35
+ }
36
+ static pageCount(total, pageSize) {
37
+ return Math.max(Math.ceil(total / pageSize), 1);
38
+ }
39
+ }
40
+ exports.PsServerRowsService = PsServerRowsService;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pallasoft/ps-table-components",
3
- "version": "1.26.0",
3
+ "version": "1.27.0",
4
4
  "private": false,
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/esm/index.js",