@pallasoft/ps-table-components 1.18.1 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ps-data-table.js +86 -24
- package/dist/components/ps-table-pagination.d.ts +2 -1
- package/dist/components/ps-table-pagination.js +3 -2
- package/dist/constants/table-config.d.ts +9 -0
- package/dist/constants/table-config.js +6 -0
- package/dist/esm/components/ps-data-table.js +87 -25
- package/dist/esm/components/ps-table-pagination.js +3 -2
- package/dist/esm/constants/table-config.js +6 -0
- package/dist/esm/hooks/use-ps-incremental-count.js +17 -0
- package/dist/esm/hooks/use-ps-scroll-anchor.js +26 -0
- package/dist/esm/hooks/use-ps-streamed-rows.js +58 -15
- package/dist/esm/index.js +3 -0
- package/dist/esm/interfaces/table-scroll-anchor-target.js +1 -0
- package/dist/esm/interfaces/table-scroll-anchor.js +1 -0
- package/dist/esm/services/ps-table-query-service.js +25 -13
- package/dist/esm/services/ps-table-scroll-anchor-service.js +17 -0
- package/dist/esm/services/ps-table-scroll-service.js +18 -0
- package/dist/esm/services/ps-table-search-service.js +14 -4
- package/dist/esm/services/ps-table-stream-service.js +18 -0
- package/dist/hooks/use-ps-incremental-count.d.ts +5 -0
- package/dist/hooks/use-ps-incremental-count.js +21 -0
- package/dist/hooks/use-ps-scroll-anchor.d.ts +2 -0
- package/dist/hooks/use-ps-scroll-anchor.js +30 -0
- package/dist/hooks/use-ps-streamed-rows.js +57 -14
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/interfaces/table-scroll-anchor-target.d.ts +10 -0
- package/dist/interfaces/table-scroll-anchor-target.js +2 -0
- package/dist/interfaces/table-scroll-anchor.d.ts +4 -0
- package/dist/interfaces/table-scroll-anchor.js +2 -0
- package/dist/interfaces/table-stream-state.d.ts +1 -0
- package/dist/services/ps-table-query-service.d.ts +2 -0
- package/dist/services/ps-table-query-service.js +25 -13
- package/dist/services/ps-table-scroll-anchor-service.d.ts +7 -0
- package/dist/services/ps-table-scroll-anchor-service.js +21 -0
- package/dist/services/ps-table-scroll-service.d.ts +5 -0
- package/dist/services/ps-table-scroll-service.js +22 -0
- package/dist/services/ps-table-search-service.d.ts +3 -0
- package/dist/services/ps-table-search-service.js +14 -4
- package/dist/services/ps-table-stream-service.d.ts +2 -0
- package/dist/services/ps-table-stream-service.js +18 -0
- package/package.json +2 -1
|
@@ -7,11 +7,16 @@ const ps_core_components_1 = require("@pallasoft/ps-core-components");
|
|
|
7
7
|
const ps_design_system_1 = require("@pallasoft/ps-design-system");
|
|
8
8
|
const ps_input_components_1 = require("@pallasoft/ps-input-components");
|
|
9
9
|
const ps_motion_components_1 = require("@pallasoft/ps-motion-components");
|
|
10
|
+
const react_virtual_1 = require("@tanstack/react-virtual");
|
|
10
11
|
const table_config_1 = require("../constants/table-config");
|
|
12
|
+
const use_ps_incremental_count_1 = require("../hooks/use-ps-incremental-count");
|
|
13
|
+
const use_ps_scroll_anchor_1 = require("../hooks/use-ps-scroll-anchor");
|
|
14
|
+
const use_ps_table_stream_status_1 = require("../hooks/use-ps-table-stream-status");
|
|
11
15
|
const ps_table_cell_tooltip_service_1 = require("../services/ps-table-cell-tooltip-service");
|
|
12
16
|
const ps_table_column_size_service_1 = require("../services/ps-table-column-size-service");
|
|
13
17
|
const ps_table_group_service_1 = require("../services/ps-table-group-service");
|
|
14
18
|
const ps_table_query_service_1 = require("../services/ps-table-query-service");
|
|
19
|
+
const ps_table_scroll_service_1 = require("../services/ps-table-scroll-service");
|
|
15
20
|
const ps_table_bulk_bar_1 = require("./ps-table-bulk-bar");
|
|
16
21
|
const ps_table_card_list_1 = require("./ps-table-card-list");
|
|
17
22
|
const ps_table_column_picker_1 = require("./ps-table-column-picker");
|
|
@@ -64,15 +69,21 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
|
|
|
64
69
|
const hasToolbar = isSearchable || hasFilters || Boolean(toolbarActions);
|
|
65
70
|
const isSelectable = Boolean(getBulkActions);
|
|
66
71
|
const columnCount = visibleColumns.length + (isSelectable ? 1 : 0);
|
|
72
|
+
const deferredSearch = (0, react_1.useDeferredValue)(search);
|
|
67
73
|
const queriedRows = (0, react_1.useMemo)(() => isServerPaginated
|
|
68
74
|
? rows
|
|
69
|
-
: ps_table_query_service_1.PsTableQueryService.apply(rows, columns, filters,
|
|
70
|
-
const
|
|
71
|
-
const rowCount = isServerPaginated ? totalRows : queriedRows.length;
|
|
72
|
-
const pageCount = pageSize ? Math.max(Math.ceil(rowCount / pageSize), 1) : 1;
|
|
73
|
-
const safePage = Math.min(Math.max(currentPage, 1), pageCount);
|
|
75
|
+
: ps_table_query_service_1.PsTableQueryService.apply(rows, columns, filters, deferredSearch, getSearchText, sort), [columns, filters, rows, deferredSearch, sort, getSearchText, isServerPaginated]);
|
|
76
|
+
const streamStatus = (0, use_ps_table_stream_status_1.usePsTableStreamStatus)();
|
|
74
77
|
const isQueried = search.trim() !== '' ||
|
|
75
78
|
Object.values(filters).some((value) => ps_table_query_service_1.PsTableQueryService.isActive(value));
|
|
79
|
+
const currentPage = page !== null && page !== undefined ? page : uncontrolledPage;
|
|
80
|
+
const loadedRowCount = isServerPaginated ? totalRows : queriedRows.length;
|
|
81
|
+
const rowCount = streamStatus !== null && !isServerPaginated && !isQueried
|
|
82
|
+
? Math.max(streamStatus.total, loadedRowCount)
|
|
83
|
+
: loadedRowCount;
|
|
84
|
+
const pageCountOf = (count) => (pageSize ? Math.max(Math.ceil(count / pageSize), 1) : 1);
|
|
85
|
+
const pageCount = pageCountOf(rowCount);
|
|
86
|
+
const safePage = Math.min(Math.max(currentPage, 1), pageCountOf(loadedRowCount));
|
|
76
87
|
const groups = (0, react_1.useMemo)(() => (grouping ? ps_table_group_service_1.PsTableGroupService.group(queriedRows, grouping.getGroupKey) : []), [grouping, queriedRows]);
|
|
77
88
|
const initialExpandedCount = grouping && grouping.initialExpandedCount !== undefined ? grouping.initialExpandedCount : 1;
|
|
78
89
|
const isGroupExpanded = (groupKey, groupIndex) => isQueried || groupIndex < initialExpandedCount !== toggledGroupKeys.includes(groupKey);
|
|
@@ -149,19 +160,64 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
|
|
|
149
160
|
updateQuery({ filters: {}, search: '' });
|
|
150
161
|
resetPage();
|
|
151
162
|
};
|
|
152
|
-
const
|
|
163
|
+
const selectedIdSet = (0, react_1.useMemo)(() => new Set(selectedIds), [selectedIds]);
|
|
164
|
+
const selectedRows = selectedIds.length === 0 ? [] : queriedRows.filter((row) => selectedIdSet.has(getRowId(row)));
|
|
153
165
|
const pageIds = visibleRows.map((row) => getRowId(row));
|
|
154
|
-
const isPageSelected = pageIds.length > 0 && pageIds.every((rowId) =>
|
|
155
|
-
const togglePage = () => setSelectedIds((current) =>
|
|
156
|
-
|
|
157
|
-
|
|
166
|
+
const isPageSelected = pageIds.length > 0 && pageIds.every((rowId) => selectedIdSet.has(rowId));
|
|
167
|
+
const togglePage = () => setSelectedIds((current) => {
|
|
168
|
+
const pageIdSet = new Set(pageIds);
|
|
169
|
+
return isPageSelected
|
|
170
|
+
? current.filter((rowId) => !pageIdSet.has(rowId))
|
|
171
|
+
: [...new Set([...current, ...pageIds])];
|
|
172
|
+
});
|
|
173
|
+
const bodyRef = (0, react_1.useRef)(null);
|
|
174
|
+
const [scrollParent, setScrollParent] = (0, react_1.useState)(null);
|
|
175
|
+
const [scrollMargin, setScrollMargin] = (0, react_1.useState)(0);
|
|
176
|
+
const shouldVirtualize = !grouping && visibleRows.length > table_config_1.PsTableConfig.virtualizeAfter;
|
|
177
|
+
const virtualOptions = {
|
|
178
|
+
count: shouldVirtualize ? visibleRows.length : 0,
|
|
179
|
+
estimateSize: () => table_config_1.PsTableConfig.rowEstimatePx[density],
|
|
180
|
+
getItemKey: (index) => getRowId(visibleRows[index]),
|
|
181
|
+
initialRect: {
|
|
182
|
+
height: table_config_1.PsTableConfig.virtualizeAfter * table_config_1.PsTableConfig.rowEstimatePx[density],
|
|
183
|
+
width: 0,
|
|
184
|
+
},
|
|
185
|
+
overscan: table_config_1.PsTableConfig.virtualOverscan,
|
|
186
|
+
scrollMargin,
|
|
187
|
+
};
|
|
188
|
+
const elementVirtualizer = (0, react_virtual_1.useVirtualizer)({
|
|
189
|
+
...virtualOptions,
|
|
190
|
+
count: scrollParent === null ? 0 : virtualOptions.count,
|
|
191
|
+
getScrollElement: () => scrollParent,
|
|
192
|
+
});
|
|
193
|
+
const windowVirtualizer = (0, react_virtual_1.useWindowVirtualizer)({
|
|
194
|
+
...virtualOptions,
|
|
195
|
+
count: scrollParent === null ? virtualOptions.count : 0,
|
|
196
|
+
});
|
|
197
|
+
const virtualizer = scrollParent === null ? windowVirtualizer : elementVirtualizer;
|
|
198
|
+
const virtualItems = shouldVirtualize ? virtualizer.getVirtualItems() : [];
|
|
199
|
+
const visibleRowIds = (0, react_1.useMemo)(() => (shouldVirtualize ? visibleRows.map((row) => getRowId(row)) : []), [getRowId, shouldVirtualize, visibleRows]);
|
|
200
|
+
(0, use_ps_scroll_anchor_1.usePsScrollAnchor)(virtualizer, visibleRowIds, shouldVirtualize && streamStatus !== null);
|
|
201
|
+
const virtualTop = virtualItems.length > 0 ? virtualItems[0].start - scrollMargin : 0;
|
|
202
|
+
const virtualBottom = virtualItems.length > 0
|
|
203
|
+
? virtualizer.getTotalSize() - (virtualItems[virtualItems.length - 1].end - scrollMargin)
|
|
204
|
+
: 0;
|
|
205
|
+
const cards = (0, use_ps_incremental_count_1.usePsIncrementalCount)(visibleRows.length, table_config_1.PsTableConfig.cardBatch);
|
|
206
|
+
(0, react_1.useEffect)(() => {
|
|
207
|
+
const body = bodyRef.current;
|
|
208
|
+
if (!shouldVirtualize || body === null)
|
|
209
|
+
return;
|
|
210
|
+
const parent = ps_table_scroll_service_1.PsTableScrollService.parent(body);
|
|
211
|
+
setScrollParent(parent);
|
|
212
|
+
setScrollMargin(ps_table_scroll_service_1.PsTableScrollService.offset(body, parent));
|
|
213
|
+
}, [shouldVirtualize, visibleRows.length]);
|
|
158
214
|
const toggleRow = (rowId) => setSelectedIds((current) => current.includes(rowId) ? current.filter((id) => id !== rowId) : [...current, rowId]);
|
|
159
215
|
const renderCardList = (cardRows, emptyContent) => ((0, jsx_runtime_1.jsx)(ps_table_card_list_1.PsTableCardList, { columns: visibleColumns, emptyContent: emptyContent, expandedRowIds: expandedRowIds, footerColumns: hasFooter ? visibleColumns.filter((column) => column.footer !== undefined) : [], getRowId: getRowId, getRowLabel: (row) => (getRowLabel ? getRowLabel(row) : getRowId(row)), isSelectable: isSelectable, onRowClick: onRowClick, onRowHover: onRowHover, onToggleRow: toggleRow, renderExpandedRow: renderExpandedRow, rows: cardRows, selectedIds: selectedIds }));
|
|
160
|
-
const renderRow = (row, motion) => {
|
|
216
|
+
const renderRow = (row, motion, virtualItem) => {
|
|
161
217
|
const rowId = getRowId(row);
|
|
162
218
|
const isExpanded = expandedRowIds ? expandedRowIds.includes(rowId) : false;
|
|
163
219
|
const rowLabel = getRowLabel ? getRowLabel(row) : rowId;
|
|
164
|
-
return ((0, jsx_runtime_1.jsxs)(react_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("tr", { "aria-expanded": renderExpandedRow ? isExpanded : undefined, "aria-label": onRowClick ? rowLabel : undefined, className: ps_design_system_1.PallasoftClassNameService.merge('align-middle transition-colors', motion ? motion.className : '', onRowClick ? 'ps-focus-ring cursor-pointer hover:bg-mist/60' : 'hover:bg-mist/40', isExpanded ? 'bg-mist/40' : ''), onClick: onRowClick ? () => onRowClick(row) : undefined, onFocus: onRowHover ? () => onRowHover(row) : undefined, onPointerEnter: onRowHover ? () => onRowHover(row) : undefined, onKeyDown: onRowClick
|
|
220
|
+
return ((0, jsx_runtime_1.jsxs)(react_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("tr", { "data-index": virtualItem ? virtualItem.index : undefined, ref: virtualItem ? virtualizer.measureElement : undefined, "aria-expanded": renderExpandedRow ? isExpanded : undefined, "aria-label": onRowClick ? rowLabel : undefined, className: ps_design_system_1.PallasoftClassNameService.merge('align-middle transition-colors', motion ? motion.className : '', onRowClick ? 'ps-focus-ring cursor-pointer hover:bg-mist/60' : 'hover:bg-mist/40', isExpanded ? 'bg-mist/40' : ''), onClick: onRowClick ? () => onRowClick(row) : undefined, onFocus: onRowHover ? () => onRowHover(row) : undefined, onPointerEnter: onRowHover ? () => onRowHover(row) : undefined, onKeyDown: onRowClick
|
|
165
221
|
? (event) => {
|
|
166
222
|
if (event.target !== event.currentTarget)
|
|
167
223
|
return;
|
|
@@ -172,7 +228,7 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
|
|
|
172
228
|
}
|
|
173
229
|
: undefined, style: motion && motion.delayMs !== undefined
|
|
174
230
|
? { animationDelay: `${motion.delayMs}ms` }
|
|
175
|
-
: undefined, tabIndex: onRowClick ? 0 : undefined, children: [isSelectable ? ((0, jsx_runtime_1.jsx)("td", { className: 'w-10 pl-4 pr-0', onClick: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: (0, jsx_runtime_1.jsx)(ps_input_components_1.PsCheckbox, { isSelected:
|
|
231
|
+
: undefined, tabIndex: onRowClick ? 0 : undefined, children: [isSelectable ? ((0, jsx_runtime_1.jsx)("td", { className: 'w-10 pl-4 pr-0', onClick: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: (0, jsx_runtime_1.jsx)(ps_input_components_1.PsCheckbox, { isSelected: selectedIdSet.has(rowId), label: `Select ${rowLabel}`, onChange: () => toggleRow(rowId) }) })) : null, visibleColumns.map((column) => ((0, jsx_runtime_1.jsx)("td", { className: ps_design_system_1.PallasoftClassNameService.merge(densityClassNames[density].cell, alignClassNames[column.align !== null && column.align !== undefined ? column.align : 'left'], column.wrap ? '' : 'whitespace-nowrap', (layout === 'fixed' && !column.wrap) ||
|
|
176
232
|
ps_table_column_size_service_1.PsTableColumnSizeService.maxWidthOf(column) !== undefined
|
|
177
233
|
? 'truncate'
|
|
178
234
|
: '', 'text-sm text-ink'), style: columnStyle(column), title: ps_table_cell_tooltip_service_1.PsTableCellTooltipService.forCell(column, row), children: column.render(row) }, column.id)))] }), renderExpandedRow ? ((0, jsx_runtime_1.jsx)("tr", { "aria-hidden": !isExpanded, className: isExpanded ? 'border-t-0' : 'border-0', children: (0, jsx_runtime_1.jsx)("td", { className: 'p-0', colSpan: columnCount, children: (0, jsx_runtime_1.jsx)(ps_motion_components_1.PsCollapse, { isOpen: isExpanded, children: (0, jsx_runtime_1.jsx)("div", { className: 'w-0 min-w-full border-l-2 border-primary bg-mist/40 px-4 py-4', children: renderExpandedRow(row) }) }) }) })) : null] }, rowId));
|
|
@@ -187,14 +243,14 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
|
|
|
187
243
|
}, onSearchChange: (next) => {
|
|
188
244
|
updateQuery({ search: next });
|
|
189
245
|
resetPage();
|
|
190
|
-
}, 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: [mobileLayout === 'cards' ? ((0, jsx_runtime_1.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
246
|
+
}, 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: [mobileLayout === 'cards' ? ((0, jsx_runtime_1.jsxs)("div", { className: 'sm:hidden', children: [grouping
|
|
247
|
+
? groups.map((group, groupIndex) => {
|
|
248
|
+
const isExpanded = isGroupExpanded(group.key, groupIndex);
|
|
249
|
+
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
|
|
250
|
+
? grouping.renderGroupSummary(group.rows)
|
|
251
|
+
: 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}`));
|
|
252
|
+
})
|
|
253
|
+
: renderCardList(shouldVirtualize ? visibleRows.slice(0, cards.count) : visibleRows, 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-focus-ring mt-2 rounded-full px-3 py-1.5 text-sm font-semibold 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', mobileLayout === 'cards' ? 'hidden sm:block' : ''), children: (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: [isSelectable ? ((0, jsx_runtime_1.jsx)("th", { className: 'w-10 pl-4 pr-0', 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) => {
|
|
198
254
|
const align = column.align !== null && column.align !== undefined ? column.align : 'left';
|
|
199
255
|
const isSorted = sort !== null && sort.columnId === column.id;
|
|
200
256
|
return ((0, jsx_runtime_1.jsx)("th", { "aria-sort": isSorted
|
|
@@ -202,7 +258,7 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
|
|
|
202
258
|
: column.sortValue && isQueryable
|
|
203
259
|
? 'none'
|
|
204
260
|
: undefined, className: ps_design_system_1.PallasoftClassNameService.merge(densityClassNames[density].head, alignClassNames[align], 'whitespace-nowrap'), scope: 'col', style: columnStyle(column), children: column.sortValue && isQueryable ? ((0, jsx_runtime_1.jsx)(ps_table_sort_header_1.PsTableSortHeader, { align: align, columnId: column.id, label: column.label, onSort: updateSort, sort: sort })) : (column.label) }, column.id));
|
|
205
|
-
})] }) }), (0, jsx_runtime_1.jsxs)("tbody", { className: 'divide-y divide-line', children: [visibleRows.length === 0 && isQueried ? ((0, jsx_runtime_1.jsx)("tr", { children: (0, jsx_runtime_1.jsxs)("td", { className: 'px-4 py-12 text-center', colSpan: columnCount, 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)("p", { className: 'mt-1 text-sm text-ink/55', children: table_config_1.PsTableConfig.noMatchesDescription }), (0, jsx_runtime_1.jsx)("button", { className: 'ps-focus-ring mt-3 rounded-full px-3 py-1.5 text-sm font-semibold text-primary hover:underline', onClick: clearFilters, type: 'button', children: "Clear search and filters" })] }) })) : null, grouping
|
|
261
|
+
})] }) }), (0, jsx_runtime_1.jsxs)("tbody", { className: 'divide-y divide-line', ref: bodyRef, children: [visibleRows.length === 0 && isQueried ? ((0, jsx_runtime_1.jsx)("tr", { children: (0, jsx_runtime_1.jsxs)("td", { className: 'px-4 py-12 text-center', colSpan: columnCount, 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)("p", { className: 'mt-1 text-sm text-ink/55', children: table_config_1.PsTableConfig.noMatchesDescription }), (0, jsx_runtime_1.jsx)("button", { className: 'ps-focus-ring mt-3 rounded-full px-3 py-1.5 text-sm font-semibold text-primary hover:underline', onClick: clearFilters, type: 'button', children: "Clear search and filters" })] }) })) : null, grouping
|
|
206
262
|
? groups.map((group, groupIndex) => {
|
|
207
263
|
const isExpanded = isGroupExpanded(group.key, groupIndex);
|
|
208
264
|
return ((0, jsx_runtime_1.jsxs)(react_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("tr", { children: (0, jsx_runtime_1.jsx)("td", { className: 'p-0', colSpan: columnCount, 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
|
|
@@ -211,8 +267,14 @@ const PsDataTable = ({ caption, className, columns, defaultSort, density = 'norm
|
|
|
211
267
|
? group.rows.map((row, rowIndex) => renderRow(row, groupRowMotion(group.key, rowIndex)))
|
|
212
268
|
: null] }, `group-${group.key}`));
|
|
213
269
|
})
|
|
214
|
-
:
|
|
270
|
+
: shouldVirtualize
|
|
271
|
+
? [
|
|
272
|
+
(0, jsx_runtime_1.jsx)("tr", { "aria-hidden": 'true', children: (0, jsx_runtime_1.jsx)("td", { className: 'p-0', colSpan: columnCount, style: { height: virtualTop } }) }, 'virtual-top'),
|
|
273
|
+
...virtualItems.map((item) => renderRow(visibleRows[item.index], undefined, item)),
|
|
274
|
+
(0, jsx_runtime_1.jsx)("tr", { "aria-hidden": 'true', children: (0, jsx_runtime_1.jsx)("td", { className: 'p-0', colSpan: columnCount, style: { height: virtualBottom } }) }, 'virtual-bottom'),
|
|
275
|
+
]
|
|
276
|
+
: visibleRows.map((row) => renderRow(row))] }), hasFooter ? ((0, jsx_runtime_1.jsx)("tfoot", { className: 'border-t-2 border-line bg-mist/50 text-sm font-semibold text-ink', children: (0, jsx_runtime_1.jsxs)("tr", { children: [isSelectable ? (0, jsx_runtime_1.jsx)("td", {}) : null, visibleColumns.map((column) => ((0, jsx_runtime_1.jsx)("td", { className: ps_design_system_1.PallasoftClassNameService.merge(densityClassNames[density].cell, alignClassNames[column.align !== null && column.align !== undefined
|
|
215
277
|
? column.align
|
|
216
|
-
: 'left'], 'whitespace-nowrap'), children: column.footer }, column.id)))] }) })) : null] }) }), pageSize && (!grouping || isServerPaginated) ? ((0, jsx_runtime_1.jsx)(ps_table_pagination_1.PsTablePagination, { onPageChange: changePage, page: safePage, pageCount: pageCount, pageSize: pageSize, totalRows: rowCount })) : null] })] }));
|
|
278
|
+
: 'left'], 'whitespace-nowrap'), children: column.footer }, column.id)))] }) })) : null] }) }), pageSize && (!grouping || isServerPaginated) ? ((0, jsx_runtime_1.jsx)(ps_table_pagination_1.PsTablePagination, { loadedPageCount: pageCountOf(loadedRowCount), onPageChange: changePage, page: safePage, pageCount: pageCount, pageSize: pageSize, totalRows: rowCount })) : null] })] }));
|
|
217
279
|
};
|
|
218
280
|
exports.PsDataTable = PsDataTable;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export interface PsTablePaginationProps {
|
|
2
2
|
className?: string;
|
|
3
|
+
loadedPageCount?: number;
|
|
3
4
|
onPageChange: (page: number) => void;
|
|
4
5
|
page: number;
|
|
5
6
|
pageCount: number;
|
|
6
7
|
pageSize: number;
|
|
7
8
|
totalRows: number;
|
|
8
9
|
}
|
|
9
|
-
export declare const PsTablePagination: ({ className, onPageChange, page, pageCount, pageSize, totalRows, }: PsTablePaginationProps) => import("react").JSX.Element;
|
|
10
|
+
export declare const PsTablePagination: ({ className, loadedPageCount, onPageChange, page, pageCount, pageSize, totalRows, }: PsTablePaginationProps) => import("react").JSX.Element;
|
|
@@ -3,10 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
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
|
-
const PsTablePagination = ({ className, onPageChange, page, pageCount, pageSize, totalRows, }) => {
|
|
6
|
+
const PsTablePagination = ({ className, loadedPageCount, onPageChange, page, pageCount, pageSize, totalRows, }) => {
|
|
7
7
|
const firstRow = totalRows === 0 ? 0 : (page - 1) * pageSize + 1;
|
|
8
8
|
const lastRow = Math.min(page * pageSize, totalRows);
|
|
9
9
|
const buttonClassName = 'ps-focus-ring inline-flex min-h-9 items-center rounded-full border border-line px-3.5 text-sm font-semibold text-ink transition-colors disabled:cursor-not-allowed disabled:opacity-40 enabled:hover:border-primary/40';
|
|
10
|
-
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 ? 'No rows' : `${firstRow}–${lastRow} of ${totalRows}` }), (0, jsx_runtime_1.jsxs)("div", { className: 'flex items-center gap-2', children: [(0, jsx_runtime_1.jsx)("button", { className: buttonClassName, 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: buttonClassName, disabled: page >=
|
|
10
|
+
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 ? 'No rows' : `${firstRow}–${lastRow} of ${totalRows}` }), (0, jsx_runtime_1.jsxs)("div", { className: 'flex items-center gap-2', children: [(0, jsx_runtime_1.jsx)("button", { className: buttonClassName, 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: buttonClassName, disabled: page >=
|
|
11
|
+
(loadedPageCount === undefined ? pageCount : Math.min(loadedPageCount, pageCount)), onClick: () => onPageChange(page + 1), type: 'button', children: "Next" })] })] }));
|
|
11
12
|
};
|
|
12
13
|
exports.PsTablePagination = PsTablePagination;
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
export declare const PsTableConfig: {
|
|
2
|
+
readonly cardBatch: 60;
|
|
2
3
|
readonly groupCloseMs: 160;
|
|
3
4
|
readonly groupOpenMs: 280;
|
|
4
5
|
readonly groupRowStaggerMs: 16;
|
|
5
6
|
readonly groupStaggerLimit: 12;
|
|
6
7
|
readonly noMatchesDescription: "Try a different search or clear the filters.";
|
|
7
8
|
readonly noMatchesTitle: "Nothing matches";
|
|
9
|
+
readonly rowEstimatePx: {
|
|
10
|
+
readonly compact: 37;
|
|
11
|
+
readonly normal: 57;
|
|
12
|
+
};
|
|
8
13
|
readonly searchPlaceholder: "Search";
|
|
9
14
|
readonly skeletonRows: 6;
|
|
15
|
+
readonly streamConcurrency: 3;
|
|
16
|
+
readonly streamFlushMs: 200;
|
|
17
|
+
readonly virtualOverscan: 12;
|
|
18
|
+
readonly virtualizeAfter: 100;
|
|
10
19
|
};
|
|
@@ -2,12 +2,18 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PsTableConfig = void 0;
|
|
4
4
|
exports.PsTableConfig = {
|
|
5
|
+
cardBatch: 60,
|
|
5
6
|
groupCloseMs: 160,
|
|
6
7
|
groupOpenMs: 280,
|
|
7
8
|
groupRowStaggerMs: 16,
|
|
8
9
|
groupStaggerLimit: 12,
|
|
9
10
|
noMatchesDescription: 'Try a different search or clear the filters.',
|
|
10
11
|
noMatchesTitle: 'Nothing matches',
|
|
12
|
+
rowEstimatePx: { compact: 37, normal: 57 },
|
|
11
13
|
searchPlaceholder: 'Search',
|
|
12
14
|
skeletonRows: 6,
|
|
15
|
+
streamConcurrency: 3,
|
|
16
|
+
streamFlushMs: 200,
|
|
17
|
+
virtualOverscan: 12,
|
|
18
|
+
virtualizeAfter: 100,
|
|
13
19
|
};
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Fragment, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { Fragment, useDeferredValue, useEffect, useMemo, useRef, useState, } from 'react';
|
|
3
3
|
import { PsSkeletonTable } from '@pallasoft/ps-core-components';
|
|
4
4
|
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
5
5
|
import { PsCheckbox } from '@pallasoft/ps-input-components';
|
|
6
6
|
import { PsCollapse } from '@pallasoft/ps-motion-components';
|
|
7
|
+
import { useVirtualizer, useWindowVirtualizer } from '@tanstack/react-virtual';
|
|
7
8
|
import { PsTableConfig } from '../constants/table-config';
|
|
9
|
+
import { usePsIncrementalCount } from '../hooks/use-ps-incremental-count';
|
|
10
|
+
import { usePsScrollAnchor } from '../hooks/use-ps-scroll-anchor';
|
|
11
|
+
import { usePsTableStreamStatus } from '../hooks/use-ps-table-stream-status';
|
|
8
12
|
import { PsTableCellTooltipService } from '../services/ps-table-cell-tooltip-service';
|
|
9
13
|
import { PsTableColumnSizeService } from '../services/ps-table-column-size-service';
|
|
10
14
|
import { PsTableGroupService } from '../services/ps-table-group-service';
|
|
11
15
|
import { PsTableQueryService } from '../services/ps-table-query-service';
|
|
16
|
+
import { PsTableScrollService } from '../services/ps-table-scroll-service';
|
|
12
17
|
import { PsTableBulkBar } from './ps-table-bulk-bar';
|
|
13
18
|
import { PsTableCardList } from './ps-table-card-list';
|
|
14
19
|
import { PsTableColumnPicker } from './ps-table-column-picker';
|
|
@@ -61,15 +66,21 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
|
|
|
61
66
|
const hasToolbar = isSearchable || hasFilters || Boolean(toolbarActions);
|
|
62
67
|
const isSelectable = Boolean(getBulkActions);
|
|
63
68
|
const columnCount = visibleColumns.length + (isSelectable ? 1 : 0);
|
|
69
|
+
const deferredSearch = useDeferredValue(search);
|
|
64
70
|
const queriedRows = useMemo(() => isServerPaginated
|
|
65
71
|
? rows
|
|
66
|
-
: PsTableQueryService.apply(rows, columns, filters,
|
|
67
|
-
const
|
|
68
|
-
const rowCount = isServerPaginated ? totalRows : queriedRows.length;
|
|
69
|
-
const pageCount = pageSize ? Math.max(Math.ceil(rowCount / pageSize), 1) : 1;
|
|
70
|
-
const safePage = Math.min(Math.max(currentPage, 1), pageCount);
|
|
72
|
+
: PsTableQueryService.apply(rows, columns, filters, deferredSearch, getSearchText, sort), [columns, filters, rows, deferredSearch, sort, getSearchText, isServerPaginated]);
|
|
73
|
+
const streamStatus = usePsTableStreamStatus();
|
|
71
74
|
const isQueried = search.trim() !== '' ||
|
|
72
75
|
Object.values(filters).some((value) => PsTableQueryService.isActive(value));
|
|
76
|
+
const currentPage = page !== null && page !== undefined ? page : uncontrolledPage;
|
|
77
|
+
const loadedRowCount = isServerPaginated ? totalRows : queriedRows.length;
|
|
78
|
+
const rowCount = streamStatus !== null && !isServerPaginated && !isQueried
|
|
79
|
+
? Math.max(streamStatus.total, loadedRowCount)
|
|
80
|
+
: loadedRowCount;
|
|
81
|
+
const pageCountOf = (count) => (pageSize ? Math.max(Math.ceil(count / pageSize), 1) : 1);
|
|
82
|
+
const pageCount = pageCountOf(rowCount);
|
|
83
|
+
const safePage = Math.min(Math.max(currentPage, 1), pageCountOf(loadedRowCount));
|
|
73
84
|
const groups = useMemo(() => (grouping ? PsTableGroupService.group(queriedRows, grouping.getGroupKey) : []), [grouping, queriedRows]);
|
|
74
85
|
const initialExpandedCount = grouping && grouping.initialExpandedCount !== undefined ? grouping.initialExpandedCount : 1;
|
|
75
86
|
const isGroupExpanded = (groupKey, groupIndex) => isQueried || groupIndex < initialExpandedCount !== toggledGroupKeys.includes(groupKey);
|
|
@@ -146,19 +157,64 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
|
|
|
146
157
|
updateQuery({ filters: {}, search: '' });
|
|
147
158
|
resetPage();
|
|
148
159
|
};
|
|
149
|
-
const
|
|
160
|
+
const selectedIdSet = useMemo(() => new Set(selectedIds), [selectedIds]);
|
|
161
|
+
const selectedRows = selectedIds.length === 0 ? [] : queriedRows.filter((row) => selectedIdSet.has(getRowId(row)));
|
|
150
162
|
const pageIds = visibleRows.map((row) => getRowId(row));
|
|
151
|
-
const isPageSelected = pageIds.length > 0 && pageIds.every((rowId) =>
|
|
152
|
-
const togglePage = () => setSelectedIds((current) =>
|
|
153
|
-
|
|
154
|
-
|
|
163
|
+
const isPageSelected = pageIds.length > 0 && pageIds.every((rowId) => selectedIdSet.has(rowId));
|
|
164
|
+
const togglePage = () => setSelectedIds((current) => {
|
|
165
|
+
const pageIdSet = new Set(pageIds);
|
|
166
|
+
return isPageSelected
|
|
167
|
+
? current.filter((rowId) => !pageIdSet.has(rowId))
|
|
168
|
+
: [...new Set([...current, ...pageIds])];
|
|
169
|
+
});
|
|
170
|
+
const bodyRef = useRef(null);
|
|
171
|
+
const [scrollParent, setScrollParent] = useState(null);
|
|
172
|
+
const [scrollMargin, setScrollMargin] = useState(0);
|
|
173
|
+
const shouldVirtualize = !grouping && visibleRows.length > PsTableConfig.virtualizeAfter;
|
|
174
|
+
const virtualOptions = {
|
|
175
|
+
count: shouldVirtualize ? visibleRows.length : 0,
|
|
176
|
+
estimateSize: () => PsTableConfig.rowEstimatePx[density],
|
|
177
|
+
getItemKey: (index) => getRowId(visibleRows[index]),
|
|
178
|
+
initialRect: {
|
|
179
|
+
height: PsTableConfig.virtualizeAfter * PsTableConfig.rowEstimatePx[density],
|
|
180
|
+
width: 0,
|
|
181
|
+
},
|
|
182
|
+
overscan: PsTableConfig.virtualOverscan,
|
|
183
|
+
scrollMargin,
|
|
184
|
+
};
|
|
185
|
+
const elementVirtualizer = useVirtualizer({
|
|
186
|
+
...virtualOptions,
|
|
187
|
+
count: scrollParent === null ? 0 : virtualOptions.count,
|
|
188
|
+
getScrollElement: () => scrollParent,
|
|
189
|
+
});
|
|
190
|
+
const windowVirtualizer = useWindowVirtualizer({
|
|
191
|
+
...virtualOptions,
|
|
192
|
+
count: scrollParent === null ? virtualOptions.count : 0,
|
|
193
|
+
});
|
|
194
|
+
const virtualizer = scrollParent === null ? windowVirtualizer : elementVirtualizer;
|
|
195
|
+
const virtualItems = shouldVirtualize ? virtualizer.getVirtualItems() : [];
|
|
196
|
+
const visibleRowIds = useMemo(() => (shouldVirtualize ? visibleRows.map((row) => getRowId(row)) : []), [getRowId, shouldVirtualize, visibleRows]);
|
|
197
|
+
usePsScrollAnchor(virtualizer, visibleRowIds, shouldVirtualize && streamStatus !== null);
|
|
198
|
+
const virtualTop = virtualItems.length > 0 ? virtualItems[0].start - scrollMargin : 0;
|
|
199
|
+
const virtualBottom = virtualItems.length > 0
|
|
200
|
+
? virtualizer.getTotalSize() - (virtualItems[virtualItems.length - 1].end - scrollMargin)
|
|
201
|
+
: 0;
|
|
202
|
+
const cards = usePsIncrementalCount(visibleRows.length, PsTableConfig.cardBatch);
|
|
203
|
+
useEffect(() => {
|
|
204
|
+
const body = bodyRef.current;
|
|
205
|
+
if (!shouldVirtualize || body === null)
|
|
206
|
+
return;
|
|
207
|
+
const parent = PsTableScrollService.parent(body);
|
|
208
|
+
setScrollParent(parent);
|
|
209
|
+
setScrollMargin(PsTableScrollService.offset(body, parent));
|
|
210
|
+
}, [shouldVirtualize, visibleRows.length]);
|
|
155
211
|
const toggleRow = (rowId) => setSelectedIds((current) => current.includes(rowId) ? current.filter((id) => id !== rowId) : [...current, rowId]);
|
|
156
212
|
const renderCardList = (cardRows, emptyContent) => (_jsx(PsTableCardList, { columns: visibleColumns, emptyContent: emptyContent, expandedRowIds: expandedRowIds, footerColumns: hasFooter ? visibleColumns.filter((column) => column.footer !== undefined) : [], getRowId: getRowId, getRowLabel: (row) => (getRowLabel ? getRowLabel(row) : getRowId(row)), isSelectable: isSelectable, onRowClick: onRowClick, onRowHover: onRowHover, onToggleRow: toggleRow, renderExpandedRow: renderExpandedRow, rows: cardRows, selectedIds: selectedIds }));
|
|
157
|
-
const renderRow = (row, motion) => {
|
|
213
|
+
const renderRow = (row, motion, virtualItem) => {
|
|
158
214
|
const rowId = getRowId(row);
|
|
159
215
|
const isExpanded = expandedRowIds ? expandedRowIds.includes(rowId) : false;
|
|
160
216
|
const rowLabel = getRowLabel ? getRowLabel(row) : rowId;
|
|
161
|
-
return (_jsxs(Fragment, { children: [_jsxs("tr", { "aria-expanded": renderExpandedRow ? isExpanded : undefined, "aria-label": onRowClick ? rowLabel : undefined, className: PallasoftClassNameService.merge('align-middle transition-colors', motion ? motion.className : '', onRowClick ? 'ps-focus-ring cursor-pointer hover:bg-mist/60' : 'hover:bg-mist/40', isExpanded ? 'bg-mist/40' : ''), onClick: onRowClick ? () => onRowClick(row) : undefined, onFocus: onRowHover ? () => onRowHover(row) : undefined, onPointerEnter: onRowHover ? () => onRowHover(row) : undefined, onKeyDown: onRowClick
|
|
217
|
+
return (_jsxs(Fragment, { children: [_jsxs("tr", { "data-index": virtualItem ? virtualItem.index : undefined, ref: virtualItem ? virtualizer.measureElement : undefined, "aria-expanded": renderExpandedRow ? isExpanded : undefined, "aria-label": onRowClick ? rowLabel : undefined, className: PallasoftClassNameService.merge('align-middle transition-colors', motion ? motion.className : '', onRowClick ? 'ps-focus-ring cursor-pointer hover:bg-mist/60' : 'hover:bg-mist/40', isExpanded ? 'bg-mist/40' : ''), onClick: onRowClick ? () => onRowClick(row) : undefined, onFocus: onRowHover ? () => onRowHover(row) : undefined, onPointerEnter: onRowHover ? () => onRowHover(row) : undefined, onKeyDown: onRowClick
|
|
162
218
|
? (event) => {
|
|
163
219
|
if (event.target !== event.currentTarget)
|
|
164
220
|
return;
|
|
@@ -169,7 +225,7 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
|
|
|
169
225
|
}
|
|
170
226
|
: undefined, style: motion && motion.delayMs !== undefined
|
|
171
227
|
? { animationDelay: `${motion.delayMs}ms` }
|
|
172
|
-
: undefined, tabIndex: onRowClick ? 0 : undefined, children: [isSelectable ? (_jsx("td", { className: 'w-10 pl-4 pr-0', onClick: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: _jsx(PsCheckbox, { isSelected:
|
|
228
|
+
: undefined, tabIndex: onRowClick ? 0 : undefined, children: [isSelectable ? (_jsx("td", { className: 'w-10 pl-4 pr-0', onClick: (event) => event.stopPropagation(), onKeyDown: (event) => event.stopPropagation(), children: _jsx(PsCheckbox, { isSelected: selectedIdSet.has(rowId), label: `Select ${rowLabel}`, onChange: () => toggleRow(rowId) }) })) : null, visibleColumns.map((column) => (_jsx("td", { className: PallasoftClassNameService.merge(densityClassNames[density].cell, alignClassNames[column.align !== null && column.align !== undefined ? column.align : 'left'], column.wrap ? '' : 'whitespace-nowrap', (layout === 'fixed' && !column.wrap) ||
|
|
173
229
|
PsTableColumnSizeService.maxWidthOf(column) !== undefined
|
|
174
230
|
? 'truncate'
|
|
175
231
|
: '', 'text-sm text-ink'), style: columnStyle(column), title: PsTableCellTooltipService.forCell(column, row), children: column.render(row) }, column.id)))] }), renderExpandedRow ? (_jsx("tr", { "aria-hidden": !isExpanded, className: isExpanded ? 'border-t-0' : 'border-0', children: _jsx("td", { className: 'p-0', colSpan: columnCount, children: _jsx(PsCollapse, { isOpen: isExpanded, children: _jsx("div", { className: 'w-0 min-w-full border-l-2 border-primary bg-mist/40 px-4 py-4', children: renderExpandedRow(row) }) }) }) })) : null] }, rowId));
|
|
@@ -184,14 +240,14 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
|
|
|
184
240
|
}, onSearchChange: (next) => {
|
|
185
241
|
updateQuery({ search: next });
|
|
186
242
|
resetPage();
|
|
187
|
-
}, rows: rows, search: search, searchPlaceholder: searchPlaceholder })) : null, _jsxs("div", { className: 'min-w-0 overflow-hidden rounded-[1.25rem] border border-line bg-surface', children: [mobileLayout === 'cards' ? (
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
243
|
+
}, rows: rows, search: search, searchPlaceholder: searchPlaceholder })) : null, _jsxs("div", { className: 'min-w-0 overflow-hidden rounded-[1.25rem] border border-line bg-surface', children: [mobileLayout === 'cards' ? (_jsxs("div", { className: 'sm:hidden', children: [grouping
|
|
244
|
+
? groups.map((group, groupIndex) => {
|
|
245
|
+
const isExpanded = isGroupExpanded(group.key, groupIndex);
|
|
246
|
+
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
|
|
247
|
+
? grouping.renderGroupSummary(group.rows)
|
|
248
|
+
: undefined }), _jsx(PsCollapse, { isOpen: isExpanded && !closingGroupKeys.includes(group.key), children: renderCardList(group.rows, null) })] }, `group-${group.key}`));
|
|
249
|
+
})
|
|
250
|
+
: renderCardList(shouldVirtualize ? visibleRows.slice(0, cards.count) : visibleRows, 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: 'ps-focus-ring mt-2 rounded-full px-3 py-1.5 text-sm font-semibold 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', mobileLayout === 'cards' ? 'hidden sm:block' : ''), children: _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: [isSelectable ? (_jsx("th", { className: 'w-10 pl-4 pr-0', 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) => {
|
|
195
251
|
const align = column.align !== null && column.align !== undefined ? column.align : 'left';
|
|
196
252
|
const isSorted = sort !== null && sort.columnId === column.id;
|
|
197
253
|
return (_jsx("th", { "aria-sort": isSorted
|
|
@@ -199,7 +255,7 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
|
|
|
199
255
|
: column.sortValue && isQueryable
|
|
200
256
|
? 'none'
|
|
201
257
|
: undefined, className: PallasoftClassNameService.merge(densityClassNames[density].head, alignClassNames[align], 'whitespace-nowrap'), scope: 'col', style: columnStyle(column), children: column.sortValue && isQueryable ? (_jsx(PsTableSortHeader, { align: align, columnId: column.id, label: column.label, onSort: updateSort, sort: sort })) : (column.label) }, column.id));
|
|
202
|
-
})] }) }), _jsxs("tbody", { className: 'divide-y divide-line', children: [visibleRows.length === 0 && isQueried ? (_jsx("tr", { children: _jsxs("td", { className: 'px-4 py-12 text-center', colSpan: columnCount, children: [_jsx("p", { className: 'text-sm font-semibold text-ink', children: PsTableConfig.noMatchesTitle }), _jsx("p", { className: 'mt-1 text-sm text-ink/55', children: PsTableConfig.noMatchesDescription }), _jsx("button", { className: 'ps-focus-ring mt-3 rounded-full px-3 py-1.5 text-sm font-semibold text-primary hover:underline', onClick: clearFilters, type: 'button', children: "Clear search and filters" })] }) })) : null, grouping
|
|
258
|
+
})] }) }), _jsxs("tbody", { className: 'divide-y divide-line', ref: bodyRef, children: [visibleRows.length === 0 && isQueried ? (_jsx("tr", { children: _jsxs("td", { className: 'px-4 py-12 text-center', colSpan: columnCount, children: [_jsx("p", { className: 'text-sm font-semibold text-ink', children: PsTableConfig.noMatchesTitle }), _jsx("p", { className: 'mt-1 text-sm text-ink/55', children: PsTableConfig.noMatchesDescription }), _jsx("button", { className: 'ps-focus-ring mt-3 rounded-full px-3 py-1.5 text-sm font-semibold text-primary hover:underline', onClick: clearFilters, type: 'button', children: "Clear search and filters" })] }) })) : null, grouping
|
|
203
259
|
? groups.map((group, groupIndex) => {
|
|
204
260
|
const isExpanded = isGroupExpanded(group.key, groupIndex);
|
|
205
261
|
return (_jsxs(Fragment, { children: [_jsx("tr", { children: _jsx("td", { className: 'p-0', colSpan: columnCount, children: _jsx(PsTableGroupHeader, { count: group.rows.length, isExpanded: isExpanded, label: grouping.getGroupLabel(group.key, group.rows), onToggle: () => toggleGroup(group.key, isExpanded), summary: grouping.renderGroupSummary
|
|
@@ -208,7 +264,13 @@ export const PsDataTable = ({ caption, className, columns, defaultSort, density
|
|
|
208
264
|
? group.rows.map((row, rowIndex) => renderRow(row, groupRowMotion(group.key, rowIndex)))
|
|
209
265
|
: null] }, `group-${group.key}`));
|
|
210
266
|
})
|
|
211
|
-
:
|
|
267
|
+
: shouldVirtualize
|
|
268
|
+
? [
|
|
269
|
+
_jsx("tr", { "aria-hidden": 'true', children: _jsx("td", { className: 'p-0', colSpan: columnCount, style: { height: virtualTop } }) }, 'virtual-top'),
|
|
270
|
+
...virtualItems.map((item) => renderRow(visibleRows[item.index], undefined, item)),
|
|
271
|
+
_jsx("tr", { "aria-hidden": 'true', children: _jsx("td", { className: 'p-0', colSpan: columnCount, style: { height: virtualBottom } }) }, 'virtual-bottom'),
|
|
272
|
+
]
|
|
273
|
+
: visibleRows.map((row) => renderRow(row))] }), hasFooter ? (_jsx("tfoot", { className: 'border-t-2 border-line bg-mist/50 text-sm font-semibold text-ink', children: _jsxs("tr", { children: [isSelectable ? _jsx("td", {}) : null, visibleColumns.map((column) => (_jsx("td", { className: PallasoftClassNameService.merge(densityClassNames[density].cell, alignClassNames[column.align !== null && column.align !== undefined
|
|
212
274
|
? column.align
|
|
213
|
-
: 'left'], 'whitespace-nowrap'), children: column.footer }, column.id)))] }) })) : null] }) }), pageSize && (!grouping || isServerPaginated) ? (_jsx(PsTablePagination, { onPageChange: changePage, page: safePage, pageCount: pageCount, pageSize: pageSize, totalRows: rowCount })) : null] })] }));
|
|
275
|
+
: 'left'], 'whitespace-nowrap'), children: column.footer }, column.id)))] }) })) : null] }) }), pageSize && (!grouping || isServerPaginated) ? (_jsx(PsTablePagination, { loadedPageCount: pageCountOf(loadedRowCount), onPageChange: changePage, page: safePage, pageCount: pageCount, pageSize: pageSize, totalRows: rowCount })) : null] })] }));
|
|
214
276
|
};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
3
|
-
export const PsTablePagination = ({ className, onPageChange, page, pageCount, pageSize, totalRows, }) => {
|
|
3
|
+
export const PsTablePagination = ({ className, loadedPageCount, onPageChange, page, pageCount, pageSize, totalRows, }) => {
|
|
4
4
|
const firstRow = totalRows === 0 ? 0 : (page - 1) * pageSize + 1;
|
|
5
5
|
const lastRow = Math.min(page * pageSize, totalRows);
|
|
6
6
|
const buttonClassName = 'ps-focus-ring inline-flex min-h-9 items-center rounded-full border border-line px-3.5 text-sm font-semibold text-ink transition-colors disabled:cursor-not-allowed disabled:opacity-40 enabled:hover:border-primary/40';
|
|
7
|
-
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 ? 'No rows' : `${firstRow}–${lastRow} of ${totalRows}` }), _jsxs("div", { className: 'flex items-center gap-2', children: [_jsx("button", { className: buttonClassName, 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: buttonClassName, disabled: page >=
|
|
7
|
+
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 ? 'No rows' : `${firstRow}–${lastRow} of ${totalRows}` }), _jsxs("div", { className: 'flex items-center gap-2', children: [_jsx("button", { className: buttonClassName, 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: buttonClassName, disabled: page >=
|
|
8
|
+
(loadedPageCount === undefined ? pageCount : Math.min(loadedPageCount, pageCount)), onClick: () => onPageChange(page + 1), type: 'button', children: "Next" })] })] }));
|
|
8
9
|
};
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
export const PsTableConfig = {
|
|
2
|
+
cardBatch: 60,
|
|
2
3
|
groupCloseMs: 160,
|
|
3
4
|
groupOpenMs: 280,
|
|
4
5
|
groupRowStaggerMs: 16,
|
|
5
6
|
groupStaggerLimit: 12,
|
|
6
7
|
noMatchesDescription: 'Try a different search or clear the filters.',
|
|
7
8
|
noMatchesTitle: 'Nothing matches',
|
|
9
|
+
rowEstimatePx: { compact: 37, normal: 57 },
|
|
8
10
|
searchPlaceholder: 'Search',
|
|
9
11
|
skeletonRows: 6,
|
|
12
|
+
streamConcurrency: 3,
|
|
13
|
+
streamFlushMs: 200,
|
|
14
|
+
virtualOverscan: 12,
|
|
15
|
+
virtualizeAfter: 100,
|
|
10
16
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
export const usePsIncrementalCount = (total, batch) => {
|
|
3
|
+
const [count, setCount] = useState(batch);
|
|
4
|
+
const sentinelRef = useRef(null);
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
const sentinel = sentinelRef.current;
|
|
7
|
+
if (sentinel === null || count >= total)
|
|
8
|
+
return;
|
|
9
|
+
const observer = new IntersectionObserver((entries) => {
|
|
10
|
+
if (entries.some((entry) => entry.isIntersecting))
|
|
11
|
+
setCount((current) => Math.min(current + batch, total));
|
|
12
|
+
});
|
|
13
|
+
observer.observe(sentinel);
|
|
14
|
+
return () => observer.disconnect();
|
|
15
|
+
}, [batch, count, total]);
|
|
16
|
+
return { count: Math.min(count, total), sentinelRef };
|
|
17
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef } from 'react';
|
|
2
|
+
import { PsTableScrollAnchorService } from '../services/ps-table-scroll-anchor-service';
|
|
3
|
+
export const usePsScrollAnchor = (target, rowIds, isActive) => {
|
|
4
|
+
const anchor = useRef(null);
|
|
5
|
+
const previousRowIds = useRef(rowIds);
|
|
6
|
+
useLayoutEffect(() => {
|
|
7
|
+
const offset = target.scrollOffset;
|
|
8
|
+
if (!isActive || offset === null || offset <= 0) {
|
|
9
|
+
anchor.current = null;
|
|
10
|
+
previousRowIds.current = rowIds;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const previous = anchor.current;
|
|
14
|
+
if (previous !== null && previousRowIds.current !== rowIds) {
|
|
15
|
+
const index = PsTableScrollAnchorService.movedTo(previous, previousRowIds.current, rowIds);
|
|
16
|
+
const start = index === null ? undefined : target.getOffsetForIndex(index, 'start');
|
|
17
|
+
previousRowIds.current = rowIds;
|
|
18
|
+
if (start !== undefined) {
|
|
19
|
+
target.scrollToOffset(PsTableScrollAnchorService.offset(start[0], previous));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
previousRowIds.current = rowIds;
|
|
24
|
+
anchor.current = PsTableScrollAnchorService.capture(target.getVirtualItems(), rowIds, offset);
|
|
25
|
+
});
|
|
26
|
+
};
|