@trackunit/react-table 1.13.37-alpha-3fb17ca5f15.0 → 1.13.38
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/index.cjs.js +81 -16
- package/index.esm.js +82 -18
- package/package.json +9 -9
- package/src/SelectAllBanner.d.ts +59 -0
- package/src/Table.d.ts +5 -0
- package/src/index.d.ts +1 -0
- package/src/translation.d.ts +2 -2
package/index.cjs.js
CHANGED
|
@@ -45,6 +45,10 @@ var defaultTranslations = {
|
|
|
45
45
|
"table.rowDensity.spacious": "Spacious",
|
|
46
46
|
"table.search.placeholder": "Search...",
|
|
47
47
|
"table.searchPlaceholder": "Search...",
|
|
48
|
+
"table.selectAll.allSelected": "<strong>{{totalCount}} items</strong> are selected.",
|
|
49
|
+
"table.selectAll.clearSelection": "Clear selection",
|
|
50
|
+
"table.selectAll.pageSelected": "<strong>{{count}} items</strong> on this page are selected.",
|
|
51
|
+
"table.selectAll.selectAll": "Select all items",
|
|
48
52
|
"table.selection.label": "Selection",
|
|
49
53
|
"table.sorting.ascending": "Ascending",
|
|
50
54
|
"table.sorting.descending": "Descending",
|
|
@@ -90,6 +94,10 @@ const translations = {
|
|
|
90
94
|
* Local useTranslation for this specific library
|
|
91
95
|
*/
|
|
92
96
|
const useTranslation = () => i18nLibraryTranslation.useNamespaceTranslation(namespace);
|
|
97
|
+
/**
|
|
98
|
+
* Trans for this specific library.
|
|
99
|
+
*/
|
|
100
|
+
const Trans = (props) => (jsxRuntime.jsx(i18nLibraryTranslation.NamespaceTrans, { ...props, namespace: namespace }));
|
|
93
101
|
/**
|
|
94
102
|
* Registers the translations for this library
|
|
95
103
|
*/
|
|
@@ -359,7 +367,7 @@ const ActionContainerAndOverflow = ({ actions, dropdownActions, moreActions, "da
|
|
|
359
367
|
const ActionSheet = ({ actions, dropdownActions, moreActions = [], selections, resetSelection, className, "data-testid": dataTestId, }) => {
|
|
360
368
|
const [t] = useTranslation();
|
|
361
369
|
return (jsxRuntime.jsxs("div", { className: cvaActionSheet({ className }), "data-testid": dataTestId, children: [jsxRuntime.jsx(reactComponents.Button, { className: "row-start-1", "data-testid": "XButton",
|
|
362
|
-
// eslint-disable-next-line
|
|
370
|
+
// eslint-disable-next-line @trackunit/prefer-event-specific-callback-naming
|
|
363
371
|
onClick: resetSelection, prefix: jsxRuntime.jsx(reactComponents.Icon, { color: "white", "data-testid": "close-icon", name: "XMark", size: "small" }), children: t("table.actionsheet.selected", { count: selections.length }) }), jsxRuntime.jsx(reactComponents.Spacer, { border: true, className: cvaDivider() }), jsxRuntime.jsx(ActionContainerAndOverflow, { "data-testid": dataTestId,
|
|
364
372
|
actions,
|
|
365
373
|
dropdownActions,
|
|
@@ -572,6 +580,56 @@ const Sorting = ({ columns, }) => {
|
|
|
572
580
|
return (jsxRuntime.jsx(reactComponents.Tooltip, { label: t("table.sorting.toolip"), placement: "bottom", children: jsxRuntime.jsxs(reactComponents.Popover, { placement: "bottom-start", children: [jsxRuntime.jsx(reactComponents.PopoverTrigger, { children: jsxRuntime.jsx(reactComponents.Button, { prefix: currentSortDirection === "asc" ? (jsxRuntime.jsx(reactComponents.Icon, { name: "BarsArrowUp", size: "small" })) : (jsxRuntime.jsx(reactComponents.Icon, { name: "BarsArrowDown", size: "small" })), size: "small", variant: "ghost-neutral", children: jsxRuntime.jsx("span", { className: "hidden sm:block", children: t("table.sorting.label") }) }) }), jsxRuntime.jsx(reactComponents.PopoverContent, { children: jsxRuntime.jsxs(reactComponents.MenuList, { className: "max-h-[55vh] overflow-y-auto", children: [jsxRuntime.jsx(reactFormComponents.RadioGroup, { id: "sortProperty", onChange: handleSelectionChange, value: currentSortValue ?? "", children: sortableColumns.map(column => (jsxRuntime.jsx(reactFormComponents.RadioItem, { className: "w-full", label: column.columnDef.header?.toString() ?? "", value: column.id }, column.id))) }), jsxRuntime.jsxs(reactFormComponents.RadioGroup, { id: "sortOrder", onChange: onSelectOrder, value: currentSortDirection, children: [jsxRuntime.jsx(reactFormComponents.RadioItem, { className: "w-full", label: t("table.sorting.ascending"), value: "asc" }), jsxRuntime.jsx(reactFormComponents.RadioItem, { className: "w-full", label: t("table.sorting.descending"), value: "desc" })] })] }) })] }) }));
|
|
573
581
|
};
|
|
574
582
|
|
|
583
|
+
/**
|
|
584
|
+
* A generic banner displayed inside a table's `subHeaderActions` slot when all
|
|
585
|
+
* rows on the current page are selected. It offers the user two actions:
|
|
586
|
+
*
|
|
587
|
+
* 1. **Select all** across every page (when `areAllSelected` is `false`).
|
|
588
|
+
* 2. **Clear selection** (when `areAllSelected` is `true`).
|
|
589
|
+
*
|
|
590
|
+
* Domain-specific data-fetching and state management are left to the consumer
|
|
591
|
+
* via the `onClickSelectAll` / `onClickClearSelection` callbacks.
|
|
592
|
+
*
|
|
593
|
+
* ### When to use
|
|
594
|
+
*
|
|
595
|
+
* - The table is **paginated** and users need to select items beyond the
|
|
596
|
+
* visible page (e.g. bulk assign, bulk delete).
|
|
597
|
+
* - The full set of matching IDs is fetched lazily via a separate lightweight
|
|
598
|
+
* query when the user explicitly clicks "Select all".
|
|
599
|
+
* - The table supports **filters** and the selection should respect the
|
|
600
|
+
* currently applied filter set. Use the `selectAllLabel` prop to communicate
|
|
601
|
+
* that only matching items will be selected (e.g. "Select all matching assets
|
|
602
|
+
* in Service Plans").
|
|
603
|
+
*
|
|
604
|
+
* ### When **not** to use
|
|
605
|
+
*
|
|
606
|
+
* - The table loads **all data at once** (no pagination). In that case the
|
|
607
|
+
* built-in header checkbox from `useTableSelection` already selects every
|
|
608
|
+
* row — there is nothing extra to select.
|
|
609
|
+
* - The dataset is small enough that all rows fit on a single page.
|
|
610
|
+
* - Selection is limited to a single row (e.g. a details panel use-case).
|
|
611
|
+
*
|
|
612
|
+
* @example
|
|
613
|
+
* ```tsx
|
|
614
|
+
* <Table
|
|
615
|
+
* subHeaderActions={
|
|
616
|
+
* allPageRowsSelected ? (
|
|
617
|
+
* <SelectAllBanner
|
|
618
|
+
* areAllSelected={selectedIds.length >= totalCount}
|
|
619
|
+
* selectedCount={selectedIds.length}
|
|
620
|
+
* onClickSelectAll={handleSelectAll}
|
|
621
|
+
* onClickClearSelection={handleClearSelection}
|
|
622
|
+
* />
|
|
623
|
+
* ) : undefined
|
|
624
|
+
* }
|
|
625
|
+
* />
|
|
626
|
+
* ```
|
|
627
|
+
*/
|
|
628
|
+
const SelectAllBanner = ({ areAllSelected, selectedCount, onClickSelectAll, onClickClearSelection, selectAllLabel, }) => {
|
|
629
|
+
const [t] = useTranslation();
|
|
630
|
+
return (jsxRuntime.jsx("div", { className: "flex w-full items-center justify-center gap-1 border-y border-neutral-200 bg-neutral-100 px-4 py-2 text-xs", "data-testid": "select-all-banner", children: areAllSelected ? (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { className: "text-neutral-600", children: jsxRuntime.jsx(Trans, { components: { strong: jsxRuntime.jsx("span", { className: "font-medium" }) }, i18nKey: "table.selectAll.allSelected", values: { totalCount: selectedCount } }) }), jsxRuntime.jsx(reactComponents.Button, { onClick: onClickClearSelection, size: "small", variant: "ghost", children: t("table.selectAll.clearSelection") })] })) : (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [jsxRuntime.jsx("span", { className: "text-neutral-600", children: jsxRuntime.jsx(Trans, { components: { strong: jsxRuntime.jsx("span", { className: "font-medium" }) }, i18nKey: "table.selectAll.pageSelected", values: { count: selectedCount } }) }), jsxRuntime.jsx(reactComponents.Button, { onClick: onClickSelectAll, size: "small", variant: "ghost", children: selectAllLabel ?? t("table.selectAll.selectAll") })] })) }));
|
|
631
|
+
};
|
|
632
|
+
|
|
575
633
|
/**
|
|
576
634
|
* This component is used to display a text with a tooltip.
|
|
577
635
|
* The tooltip is displayed if the text is truncated with ellipsis.
|
|
@@ -1127,7 +1185,7 @@ const Table = ({ rowHeight = 50, ...props }) => {
|
|
|
1127
1185
|
header.column.getToggleSortingHandler()?.(event);
|
|
1128
1186
|
}
|
|
1129
1187
|
}, [props]);
|
|
1130
|
-
return (jsxRuntime.jsxs(reactComponents.Card, { className: tailwindMerge.twMerge("table-compact flex flex-col overflow-hidden", props.className), "data-testid": props["data-testid"], children: [props.headerLeftActions || props.headerRightActions ? (jsxRuntime.jsxs("div", { className: "z-default flex justify-between gap-2 p-2", children: [jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: props.headerLeftActions }), jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: props.headerRightActions })] })) : null, jsxRuntime.jsx("div", { className: tailwindMerge.twMerge("h-full overflow-x-auto overflow-y-scroll border-b border-neutral-200", props.headerLeftActions || props.headerRightActions ? "border-t" : ""), ref: tableScrollElementRef, children: jsxRuntime.jsxs(reactTableBaseComponents.TableRoot, { style: {
|
|
1188
|
+
return (jsxRuntime.jsxs(reactComponents.Card, { className: tailwindMerge.twMerge("table-compact flex flex-col overflow-hidden", props.className), "data-testid": props["data-testid"], children: [props.headerLeftActions || props.headerRightActions ? (jsxRuntime.jsxs("div", { className: "z-default flex justify-between gap-2 p-2", children: [jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: props.headerLeftActions }), jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: props.headerRightActions })] })) : null, props.subHeaderActions, jsxRuntime.jsx("div", { className: tailwindMerge.twMerge("h-full overflow-x-auto overflow-y-scroll border-b border-neutral-200", !props.subHeaderActions && (props.headerLeftActions || props.headerRightActions) ? "border-t" : ""), ref: tableScrollElementRef, children: jsxRuntime.jsxs(reactTableBaseComponents.TableRoot, { style: {
|
|
1131
1189
|
height: hasResults ? "auto" : "100%",
|
|
1132
1190
|
width: "100%",
|
|
1133
1191
|
position: "relative",
|
|
@@ -1443,6 +1501,15 @@ const useTable = ({ onTableStateChange, initialState, columns, ...reactTableProp
|
|
|
1443
1501
|
}, [table]);
|
|
1444
1502
|
};
|
|
1445
1503
|
|
|
1504
|
+
const buildSelectionFromIds = (ids) => {
|
|
1505
|
+
if (!ids) {
|
|
1506
|
+
return {};
|
|
1507
|
+
}
|
|
1508
|
+
return ids.reduce((selection, id) => {
|
|
1509
|
+
selection[String(id)] = true;
|
|
1510
|
+
return selection;
|
|
1511
|
+
}, {});
|
|
1512
|
+
};
|
|
1446
1513
|
/**
|
|
1447
1514
|
* `useTableSelection` provides row selection state management for the Table component.
|
|
1448
1515
|
* It returns a selection checkbox column definition, row selection state, and props to spread onto `useTable`.
|
|
@@ -1476,21 +1543,18 @@ const useTable = ({ onTableStateChange, initialState, columns, ...reactTableProp
|
|
|
1476
1543
|
*/
|
|
1477
1544
|
const useTableSelection = ({ data, idKey, defaultSelectedIds, enableRowSelection = true, }) => {
|
|
1478
1545
|
const [t] = useTranslation();
|
|
1479
|
-
const [rowSelection, setRowSelection] = react.useState(
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1546
|
+
const [rowSelection, setRowSelection] = react.useState(() => buildSelectionFromIds(defaultSelectedIds));
|
|
1547
|
+
const defaultIdsKey = defaultSelectedIds?.slice().sort().join(",") ?? "";
|
|
1548
|
+
const [prevDefaultIdsKey, setPrevDefaultIdsKey] = react.useState(defaultIdsKey);
|
|
1549
|
+
if (defaultIdsKey !== prevDefaultIdsKey) {
|
|
1550
|
+
setPrevDefaultIdsKey(defaultIdsKey);
|
|
1551
|
+
const initialSelection = buildSelectionFromIds(defaultSelectedIds);
|
|
1552
|
+
const hasChanged = sharedUtils.objectKeys(rowSelection).length !== sharedUtils.objectKeys(initialSelection).length ||
|
|
1553
|
+
sharedUtils.objectEntries(initialSelection).some(([key, value]) => rowSelection[key] !== value);
|
|
1554
|
+
if (hasChanged) {
|
|
1555
|
+
setRowSelection(initialSelection);
|
|
1483
1556
|
}
|
|
1484
|
-
|
|
1485
|
-
selection[String(id)] = true;
|
|
1486
|
-
return selection;
|
|
1487
|
-
}, {});
|
|
1488
|
-
setRowSelection(prev => {
|
|
1489
|
-
const hasChanged = sharedUtils.objectKeys(prev).length !== sharedUtils.objectKeys(initialSelection).length ||
|
|
1490
|
-
sharedUtils.objectEntries(initialSelection).some(([key, value]) => prev[key] !== value);
|
|
1491
|
-
return hasChanged ? { ...initialSelection } : prev;
|
|
1492
|
-
});
|
|
1493
|
-
}, [defaultSelectedIds]);
|
|
1557
|
+
}
|
|
1494
1558
|
const toggleRowSelectionState = react.useCallback((id) => {
|
|
1495
1559
|
setRowSelection(prevRowSelection => {
|
|
1496
1560
|
const stringId = String(id);
|
|
@@ -1598,6 +1662,7 @@ Object.defineProperty(exports, "createColumnHelper", {
|
|
|
1598
1662
|
});
|
|
1599
1663
|
exports.ActionSheet = ActionSheet;
|
|
1600
1664
|
exports.ColumnFilter = ColumnFilter;
|
|
1665
|
+
exports.SelectAllBanner = SelectAllBanner;
|
|
1601
1666
|
exports.Sorting = Sorting;
|
|
1602
1667
|
exports.Table = Table;
|
|
1603
1668
|
exports.fromTUSortToTanStack = fromTUSortToTanStack;
|
package/index.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
2
|
-
import { registerTranslations, useNamespaceTranslation } from '@trackunit/i18n-library-translation';
|
|
2
|
+
import { registerTranslations, useNamespaceTranslation, NamespaceTrans } from '@trackunit/i18n-library-translation';
|
|
3
3
|
import { MenuItem, Icon, Button, Tooltip, useOverflowItems, MoreMenu, MenuList, Spacer, cvaInteractableItem, Text, Popover, PopoverTrigger, IconButton, PopoverContent, useInfiniteScroll, noPagination, Card, Spinner, EmptyState } from '@trackunit/react-components';
|
|
4
4
|
import { objectValues, nonNullable, objectKeys, objectEntries } from '@trackunit/shared-utils';
|
|
5
5
|
import { useMemo, Children, cloneElement, useRef, useState, useEffect, useCallback, createElement } from 'react';
|
|
@@ -44,6 +44,10 @@ var defaultTranslations = {
|
|
|
44
44
|
"table.rowDensity.spacious": "Spacious",
|
|
45
45
|
"table.search.placeholder": "Search...",
|
|
46
46
|
"table.searchPlaceholder": "Search...",
|
|
47
|
+
"table.selectAll.allSelected": "<strong>{{totalCount}} items</strong> are selected.",
|
|
48
|
+
"table.selectAll.clearSelection": "Clear selection",
|
|
49
|
+
"table.selectAll.pageSelected": "<strong>{{count}} items</strong> on this page are selected.",
|
|
50
|
+
"table.selectAll.selectAll": "Select all items",
|
|
47
51
|
"table.selection.label": "Selection",
|
|
48
52
|
"table.sorting.ascending": "Ascending",
|
|
49
53
|
"table.sorting.descending": "Descending",
|
|
@@ -89,6 +93,10 @@ const translations = {
|
|
|
89
93
|
* Local useTranslation for this specific library
|
|
90
94
|
*/
|
|
91
95
|
const useTranslation = () => useNamespaceTranslation(namespace);
|
|
96
|
+
/**
|
|
97
|
+
* Trans for this specific library.
|
|
98
|
+
*/
|
|
99
|
+
const Trans = (props) => (jsx(NamespaceTrans, { ...props, namespace: namespace }));
|
|
92
100
|
/**
|
|
93
101
|
* Registers the translations for this library
|
|
94
102
|
*/
|
|
@@ -358,7 +366,7 @@ const ActionContainerAndOverflow = ({ actions, dropdownActions, moreActions, "da
|
|
|
358
366
|
const ActionSheet = ({ actions, dropdownActions, moreActions = [], selections, resetSelection, className, "data-testid": dataTestId, }) => {
|
|
359
367
|
const [t] = useTranslation();
|
|
360
368
|
return (jsxs("div", { className: cvaActionSheet({ className }), "data-testid": dataTestId, children: [jsx(Button, { className: "row-start-1", "data-testid": "XButton",
|
|
361
|
-
// eslint-disable-next-line
|
|
369
|
+
// eslint-disable-next-line @trackunit/prefer-event-specific-callback-naming
|
|
362
370
|
onClick: resetSelection, prefix: jsx(Icon, { color: "white", "data-testid": "close-icon", name: "XMark", size: "small" }), children: t("table.actionsheet.selected", { count: selections.length }) }), jsx(Spacer, { border: true, className: cvaDivider() }), jsx(ActionContainerAndOverflow, { "data-testid": dataTestId,
|
|
363
371
|
actions,
|
|
364
372
|
dropdownActions,
|
|
@@ -571,6 +579,56 @@ const Sorting = ({ columns, }) => {
|
|
|
571
579
|
return (jsx(Tooltip, { label: t("table.sorting.toolip"), placement: "bottom", children: jsxs(Popover, { placement: "bottom-start", children: [jsx(PopoverTrigger, { children: jsx(Button, { prefix: currentSortDirection === "asc" ? (jsx(Icon, { name: "BarsArrowUp", size: "small" })) : (jsx(Icon, { name: "BarsArrowDown", size: "small" })), size: "small", variant: "ghost-neutral", children: jsx("span", { className: "hidden sm:block", children: t("table.sorting.label") }) }) }), jsx(PopoverContent, { children: jsxs(MenuList, { className: "max-h-[55vh] overflow-y-auto", children: [jsx(RadioGroup, { id: "sortProperty", onChange: handleSelectionChange, value: currentSortValue ?? "", children: sortableColumns.map(column => (jsx(RadioItem, { className: "w-full", label: column.columnDef.header?.toString() ?? "", value: column.id }, column.id))) }), jsxs(RadioGroup, { id: "sortOrder", onChange: onSelectOrder, value: currentSortDirection, children: [jsx(RadioItem, { className: "w-full", label: t("table.sorting.ascending"), value: "asc" }), jsx(RadioItem, { className: "w-full", label: t("table.sorting.descending"), value: "desc" })] })] }) })] }) }));
|
|
572
580
|
};
|
|
573
581
|
|
|
582
|
+
/**
|
|
583
|
+
* A generic banner displayed inside a table's `subHeaderActions` slot when all
|
|
584
|
+
* rows on the current page are selected. It offers the user two actions:
|
|
585
|
+
*
|
|
586
|
+
* 1. **Select all** across every page (when `areAllSelected` is `false`).
|
|
587
|
+
* 2. **Clear selection** (when `areAllSelected` is `true`).
|
|
588
|
+
*
|
|
589
|
+
* Domain-specific data-fetching and state management are left to the consumer
|
|
590
|
+
* via the `onClickSelectAll` / `onClickClearSelection` callbacks.
|
|
591
|
+
*
|
|
592
|
+
* ### When to use
|
|
593
|
+
*
|
|
594
|
+
* - The table is **paginated** and users need to select items beyond the
|
|
595
|
+
* visible page (e.g. bulk assign, bulk delete).
|
|
596
|
+
* - The full set of matching IDs is fetched lazily via a separate lightweight
|
|
597
|
+
* query when the user explicitly clicks "Select all".
|
|
598
|
+
* - The table supports **filters** and the selection should respect the
|
|
599
|
+
* currently applied filter set. Use the `selectAllLabel` prop to communicate
|
|
600
|
+
* that only matching items will be selected (e.g. "Select all matching assets
|
|
601
|
+
* in Service Plans").
|
|
602
|
+
*
|
|
603
|
+
* ### When **not** to use
|
|
604
|
+
*
|
|
605
|
+
* - The table loads **all data at once** (no pagination). In that case the
|
|
606
|
+
* built-in header checkbox from `useTableSelection` already selects every
|
|
607
|
+
* row — there is nothing extra to select.
|
|
608
|
+
* - The dataset is small enough that all rows fit on a single page.
|
|
609
|
+
* - Selection is limited to a single row (e.g. a details panel use-case).
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* ```tsx
|
|
613
|
+
* <Table
|
|
614
|
+
* subHeaderActions={
|
|
615
|
+
* allPageRowsSelected ? (
|
|
616
|
+
* <SelectAllBanner
|
|
617
|
+
* areAllSelected={selectedIds.length >= totalCount}
|
|
618
|
+
* selectedCount={selectedIds.length}
|
|
619
|
+
* onClickSelectAll={handleSelectAll}
|
|
620
|
+
* onClickClearSelection={handleClearSelection}
|
|
621
|
+
* />
|
|
622
|
+
* ) : undefined
|
|
623
|
+
* }
|
|
624
|
+
* />
|
|
625
|
+
* ```
|
|
626
|
+
*/
|
|
627
|
+
const SelectAllBanner = ({ areAllSelected, selectedCount, onClickSelectAll, onClickClearSelection, selectAllLabel, }) => {
|
|
628
|
+
const [t] = useTranslation();
|
|
629
|
+
return (jsx("div", { className: "flex w-full items-center justify-center gap-1 border-y border-neutral-200 bg-neutral-100 px-4 py-2 text-xs", "data-testid": "select-all-banner", children: areAllSelected ? (jsxs(Fragment, { children: [jsx("span", { className: "text-neutral-600", children: jsx(Trans, { components: { strong: jsx("span", { className: "font-medium" }) }, i18nKey: "table.selectAll.allSelected", values: { totalCount: selectedCount } }) }), jsx(Button, { onClick: onClickClearSelection, size: "small", variant: "ghost", children: t("table.selectAll.clearSelection") })] })) : (jsxs(Fragment, { children: [jsx("span", { className: "text-neutral-600", children: jsx(Trans, { components: { strong: jsx("span", { className: "font-medium" }) }, i18nKey: "table.selectAll.pageSelected", values: { count: selectedCount } }) }), jsx(Button, { onClick: onClickSelectAll, size: "small", variant: "ghost", children: selectAllLabel ?? t("table.selectAll.selectAll") })] })) }));
|
|
630
|
+
};
|
|
631
|
+
|
|
574
632
|
/**
|
|
575
633
|
* This component is used to display a text with a tooltip.
|
|
576
634
|
* The tooltip is displayed if the text is truncated with ellipsis.
|
|
@@ -1126,7 +1184,7 @@ const Table = ({ rowHeight = 50, ...props }) => {
|
|
|
1126
1184
|
header.column.getToggleSortingHandler()?.(event);
|
|
1127
1185
|
}
|
|
1128
1186
|
}, [props]);
|
|
1129
|
-
return (jsxs(Card, { className: twMerge("table-compact flex flex-col overflow-hidden", props.className), "data-testid": props["data-testid"], children: [props.headerLeftActions || props.headerRightActions ? (jsxs("div", { className: "z-default flex justify-between gap-2 p-2", children: [jsx("div", { className: "flex items-center gap-2", children: props.headerLeftActions }), jsx("div", { className: "flex items-center gap-2", children: props.headerRightActions })] })) : null, jsx("div", { className: twMerge("h-full overflow-x-auto overflow-y-scroll border-b border-neutral-200", props.headerLeftActions || props.headerRightActions ? "border-t" : ""), ref: tableScrollElementRef, children: jsxs(TableRoot, { style: {
|
|
1187
|
+
return (jsxs(Card, { className: twMerge("table-compact flex flex-col overflow-hidden", props.className), "data-testid": props["data-testid"], children: [props.headerLeftActions || props.headerRightActions ? (jsxs("div", { className: "z-default flex justify-between gap-2 p-2", children: [jsx("div", { className: "flex items-center gap-2", children: props.headerLeftActions }), jsx("div", { className: "flex items-center gap-2", children: props.headerRightActions })] })) : null, props.subHeaderActions, jsx("div", { className: twMerge("h-full overflow-x-auto overflow-y-scroll border-b border-neutral-200", !props.subHeaderActions && (props.headerLeftActions || props.headerRightActions) ? "border-t" : ""), ref: tableScrollElementRef, children: jsxs(TableRoot, { style: {
|
|
1130
1188
|
height: hasResults ? "auto" : "100%",
|
|
1131
1189
|
width: "100%",
|
|
1132
1190
|
position: "relative",
|
|
@@ -1442,6 +1500,15 @@ const useTable = ({ onTableStateChange, initialState, columns, ...reactTableProp
|
|
|
1442
1500
|
}, [table]);
|
|
1443
1501
|
};
|
|
1444
1502
|
|
|
1503
|
+
const buildSelectionFromIds = (ids) => {
|
|
1504
|
+
if (!ids) {
|
|
1505
|
+
return {};
|
|
1506
|
+
}
|
|
1507
|
+
return ids.reduce((selection, id) => {
|
|
1508
|
+
selection[String(id)] = true;
|
|
1509
|
+
return selection;
|
|
1510
|
+
}, {});
|
|
1511
|
+
};
|
|
1445
1512
|
/**
|
|
1446
1513
|
* `useTableSelection` provides row selection state management for the Table component.
|
|
1447
1514
|
* It returns a selection checkbox column definition, row selection state, and props to spread onto `useTable`.
|
|
@@ -1475,21 +1542,18 @@ const useTable = ({ onTableStateChange, initialState, columns, ...reactTableProp
|
|
|
1475
1542
|
*/
|
|
1476
1543
|
const useTableSelection = ({ data, idKey, defaultSelectedIds, enableRowSelection = true, }) => {
|
|
1477
1544
|
const [t] = useTranslation();
|
|
1478
|
-
const [rowSelection, setRowSelection] = useState(
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1545
|
+
const [rowSelection, setRowSelection] = useState(() => buildSelectionFromIds(defaultSelectedIds));
|
|
1546
|
+
const defaultIdsKey = defaultSelectedIds?.slice().sort().join(",") ?? "";
|
|
1547
|
+
const [prevDefaultIdsKey, setPrevDefaultIdsKey] = useState(defaultIdsKey);
|
|
1548
|
+
if (defaultIdsKey !== prevDefaultIdsKey) {
|
|
1549
|
+
setPrevDefaultIdsKey(defaultIdsKey);
|
|
1550
|
+
const initialSelection = buildSelectionFromIds(defaultSelectedIds);
|
|
1551
|
+
const hasChanged = objectKeys(rowSelection).length !== objectKeys(initialSelection).length ||
|
|
1552
|
+
objectEntries(initialSelection).some(([key, value]) => rowSelection[key] !== value);
|
|
1553
|
+
if (hasChanged) {
|
|
1554
|
+
setRowSelection(initialSelection);
|
|
1482
1555
|
}
|
|
1483
|
-
|
|
1484
|
-
selection[String(id)] = true;
|
|
1485
|
-
return selection;
|
|
1486
|
-
}, {});
|
|
1487
|
-
setRowSelection(prev => {
|
|
1488
|
-
const hasChanged = objectKeys(prev).length !== objectKeys(initialSelection).length ||
|
|
1489
|
-
objectEntries(initialSelection).some(([key, value]) => prev[key] !== value);
|
|
1490
|
-
return hasChanged ? { ...initialSelection } : prev;
|
|
1491
|
-
});
|
|
1492
|
-
}, [defaultSelectedIds]);
|
|
1556
|
+
}
|
|
1493
1557
|
const toggleRowSelectionState = useCallback((id) => {
|
|
1494
1558
|
setRowSelection(prevRowSelection => {
|
|
1495
1559
|
const stringId = String(id);
|
|
@@ -1591,4 +1655,4 @@ const fromTanStackToTUSort = (input) => {
|
|
|
1591
1655
|
*/
|
|
1592
1656
|
setupLibraryTranslations();
|
|
1593
1657
|
|
|
1594
|
-
export { ActionSheet, ColumnFilter, Sorting, Table, fromTUSortToTanStack, fromTanStackToTUSort, useColumnHelper, useTable, useTableSelection };
|
|
1658
|
+
export { ActionSheet, ColumnFilter, SelectAllBanner, Sorting, Table, fromTUSortToTanStack, fromTanStackToTUSort, useColumnHelper, useTable, useTableSelection };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trackunit/react-table",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.38",
|
|
4
4
|
"repository": "https://github.com/Trackunit/manager",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
6
6
|
"engines": {
|
|
@@ -14,14 +14,14 @@
|
|
|
14
14
|
"react-dnd-html5-backend": "16.0.1",
|
|
15
15
|
"@tanstack/react-router": "1.114.29",
|
|
16
16
|
"tailwind-merge": "^2.0.0",
|
|
17
|
-
"@trackunit/react-components": "1.17.
|
|
18
|
-
"@trackunit/shared-utils": "1.13.49
|
|
19
|
-
"@trackunit/css-class-variance-utilities": "1.11.49
|
|
20
|
-
"@trackunit/ui-icons": "1.11.48
|
|
21
|
-
"@trackunit/react-table-base-components": "1.13.
|
|
22
|
-
"@trackunit/react-form-components": "1.14.
|
|
23
|
-
"@trackunit/i18n-library-translation": "1.12.35
|
|
24
|
-
"@trackunit/iris-app-runtime-core-api": "1.12.30
|
|
17
|
+
"@trackunit/react-components": "1.17.34",
|
|
18
|
+
"@trackunit/shared-utils": "1.13.49",
|
|
19
|
+
"@trackunit/css-class-variance-utilities": "1.11.49",
|
|
20
|
+
"@trackunit/ui-icons": "1.11.48",
|
|
21
|
+
"@trackunit/react-table-base-components": "1.13.37",
|
|
22
|
+
"@trackunit/react-form-components": "1.14.37",
|
|
23
|
+
"@trackunit/i18n-library-translation": "1.12.35",
|
|
24
|
+
"@trackunit/iris-app-runtime-core-api": "1.12.30",
|
|
25
25
|
"graphql": "^16.10.0"
|
|
26
26
|
},
|
|
27
27
|
"module": "./index.esm.js",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { MouseEventHandler, ReactElement } from "react";
|
|
2
|
+
export interface SelectAllBannerProps {
|
|
3
|
+
/** Whether all items across all pages are currently selected */
|
|
4
|
+
readonly areAllSelected: boolean;
|
|
5
|
+
/** Number of items currently selected (on this page or total) */
|
|
6
|
+
readonly selectedCount: number;
|
|
7
|
+
/** Called when user clicks "Select all" to select all items across pages */
|
|
8
|
+
readonly onClickSelectAll: MouseEventHandler<HTMLButtonElement>;
|
|
9
|
+
/** Called when user clicks "Clear selection" */
|
|
10
|
+
readonly onClickClearSelection: MouseEventHandler<HTMLButtonElement>;
|
|
11
|
+
/** Optional label override for the "Select all" button */
|
|
12
|
+
readonly selectAllLabel?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A generic banner displayed inside a table's `subHeaderActions` slot when all
|
|
16
|
+
* rows on the current page are selected. It offers the user two actions:
|
|
17
|
+
*
|
|
18
|
+
* 1. **Select all** across every page (when `areAllSelected` is `false`).
|
|
19
|
+
* 2. **Clear selection** (when `areAllSelected` is `true`).
|
|
20
|
+
*
|
|
21
|
+
* Domain-specific data-fetching and state management are left to the consumer
|
|
22
|
+
* via the `onClickSelectAll` / `onClickClearSelection` callbacks.
|
|
23
|
+
*
|
|
24
|
+
* ### When to use
|
|
25
|
+
*
|
|
26
|
+
* - The table is **paginated** and users need to select items beyond the
|
|
27
|
+
* visible page (e.g. bulk assign, bulk delete).
|
|
28
|
+
* - The full set of matching IDs is fetched lazily via a separate lightweight
|
|
29
|
+
* query when the user explicitly clicks "Select all".
|
|
30
|
+
* - The table supports **filters** and the selection should respect the
|
|
31
|
+
* currently applied filter set. Use the `selectAllLabel` prop to communicate
|
|
32
|
+
* that only matching items will be selected (e.g. "Select all matching assets
|
|
33
|
+
* in Service Plans").
|
|
34
|
+
*
|
|
35
|
+
* ### When **not** to use
|
|
36
|
+
*
|
|
37
|
+
* - The table loads **all data at once** (no pagination). In that case the
|
|
38
|
+
* built-in header checkbox from `useTableSelection` already selects every
|
|
39
|
+
* row — there is nothing extra to select.
|
|
40
|
+
* - The dataset is small enough that all rows fit on a single page.
|
|
41
|
+
* - Selection is limited to a single row (e.g. a details panel use-case).
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```tsx
|
|
45
|
+
* <Table
|
|
46
|
+
* subHeaderActions={
|
|
47
|
+
* allPageRowsSelected ? (
|
|
48
|
+
* <SelectAllBanner
|
|
49
|
+
* areAllSelected={selectedIds.length >= totalCount}
|
|
50
|
+
* selectedCount={selectedIds.length}
|
|
51
|
+
* onClickSelectAll={handleSelectAll}
|
|
52
|
+
* onClickClearSelection={handleClearSelection}
|
|
53
|
+
* />
|
|
54
|
+
* ) : undefined
|
|
55
|
+
* }
|
|
56
|
+
* />
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare const SelectAllBanner: ({ areAllSelected, selectedCount, onClickSelectAll, onClickClearSelection, selectAllLabel, }: SelectAllBannerProps) => ReactElement;
|
package/src/Table.d.ts
CHANGED
|
@@ -18,6 +18,11 @@ export interface TableProps<TData extends object> extends ReactTable<TData>, Com
|
|
|
18
18
|
* Commonly used for primary actions for the table.
|
|
19
19
|
*/
|
|
20
20
|
headerRightActions?: ReactNode;
|
|
21
|
+
/**
|
|
22
|
+
* ReactNode rendered below the header actions row and above the column headers.
|
|
23
|
+
* Useful for contextual banners like bulk selection indicators. See SelectAllBanner component for an example.
|
|
24
|
+
*/
|
|
25
|
+
subHeaderActions?: ReactNode;
|
|
21
26
|
/**
|
|
22
27
|
* ReactNode rendered on the right side of the table footer.
|
|
23
28
|
* Commonly used for export actions.
|
package/src/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from "./ActionSheet/Actions";
|
|
|
3
3
|
export * from "./ActionSheet/ActionSheet";
|
|
4
4
|
export * from "./menus/ColumnFilter";
|
|
5
5
|
export * from "./menus/Sorting";
|
|
6
|
+
export * from "./SelectAllBanner";
|
|
6
7
|
export * from "./Table";
|
|
7
8
|
export * from "./types";
|
|
8
9
|
export * from "./useColumnHelper";
|
package/src/translation.d.ts
CHANGED
|
@@ -14,8 +14,8 @@ export declare const translations: TranslationResource<TranslationKeys>;
|
|
|
14
14
|
/**
|
|
15
15
|
* Local useTranslation for this specific library
|
|
16
16
|
*/
|
|
17
|
-
export declare const useTranslation: () => [TransForLibs<"layout.actions.reset" | "table.actionsheet.selected" | "table.columnActions.clearSorting" | "table.columnActions.hideColumn" | "table.columnActions.pinColumn" | "table.columnActions.sortAscending" | "table.columnActions.sortDescending" | "table.columnActions.unPinColumn" | "table.columnFilters.columns" | "table.columnFilters.hiddenColumnCount" | "table.columnFilters.hiddenColumnsCount" | "table.columnFilters.title" | "table.columnFilters.tooltip" | "table.error" | "table.exportFileName" | "table.format" | "table.noResults" | "table.pagination.full" | "table.pagination.full.capped" | "table.pagination.of" | "table.pagination.page" | "table.result" | "table.results.plural" | "table.results.plural.capped" | "table.rowDensity.compact" | "table.rowDensity.spacious" | "table.search.placeholder" | "table.searchPlaceholder" | "table.selection.label" | "table.sorting.ascending" | "table.sorting.descending" | "table.sorting.label" | "table.sorting.order" | "table.sorting.toolip" | "table.spacing" | "table.spacing.toolip">, import("i18next").i18n, boolean] & {
|
|
18
|
-
t: TransForLibs<"layout.actions.reset" | "table.actionsheet.selected" | "table.columnActions.clearSorting" | "table.columnActions.hideColumn" | "table.columnActions.pinColumn" | "table.columnActions.sortAscending" | "table.columnActions.sortDescending" | "table.columnActions.unPinColumn" | "table.columnFilters.columns" | "table.columnFilters.hiddenColumnCount" | "table.columnFilters.hiddenColumnsCount" | "table.columnFilters.title" | "table.columnFilters.tooltip" | "table.error" | "table.exportFileName" | "table.format" | "table.noResults" | "table.pagination.full" | "table.pagination.full.capped" | "table.pagination.of" | "table.pagination.page" | "table.result" | "table.results.plural" | "table.results.plural.capped" | "table.rowDensity.compact" | "table.rowDensity.spacious" | "table.search.placeholder" | "table.searchPlaceholder" | "table.selection.label" | "table.sorting.ascending" | "table.sorting.descending" | "table.sorting.label" | "table.sorting.order" | "table.sorting.toolip" | "table.spacing" | "table.spacing.toolip">;
|
|
17
|
+
export declare const useTranslation: () => [TransForLibs<"layout.actions.reset" | "table.actionsheet.selected" | "table.columnActions.clearSorting" | "table.columnActions.hideColumn" | "table.columnActions.pinColumn" | "table.columnActions.sortAscending" | "table.columnActions.sortDescending" | "table.columnActions.unPinColumn" | "table.columnFilters.columns" | "table.columnFilters.hiddenColumnCount" | "table.columnFilters.hiddenColumnsCount" | "table.columnFilters.title" | "table.columnFilters.tooltip" | "table.error" | "table.exportFileName" | "table.format" | "table.noResults" | "table.pagination.full" | "table.pagination.full.capped" | "table.pagination.of" | "table.pagination.page" | "table.result" | "table.results.plural" | "table.results.plural.capped" | "table.rowDensity.compact" | "table.rowDensity.spacious" | "table.search.placeholder" | "table.searchPlaceholder" | "table.selectAll.allSelected" | "table.selectAll.clearSelection" | "table.selectAll.pageSelected" | "table.selectAll.selectAll" | "table.selection.label" | "table.sorting.ascending" | "table.sorting.descending" | "table.sorting.label" | "table.sorting.order" | "table.sorting.toolip" | "table.spacing" | "table.spacing.toolip">, import("i18next").i18n, boolean] & {
|
|
18
|
+
t: TransForLibs<"layout.actions.reset" | "table.actionsheet.selected" | "table.columnActions.clearSorting" | "table.columnActions.hideColumn" | "table.columnActions.pinColumn" | "table.columnActions.sortAscending" | "table.columnActions.sortDescending" | "table.columnActions.unPinColumn" | "table.columnFilters.columns" | "table.columnFilters.hiddenColumnCount" | "table.columnFilters.hiddenColumnsCount" | "table.columnFilters.title" | "table.columnFilters.tooltip" | "table.error" | "table.exportFileName" | "table.format" | "table.noResults" | "table.pagination.full" | "table.pagination.full.capped" | "table.pagination.of" | "table.pagination.page" | "table.result" | "table.results.plural" | "table.results.plural.capped" | "table.rowDensity.compact" | "table.rowDensity.spacious" | "table.search.placeholder" | "table.searchPlaceholder" | "table.selectAll.allSelected" | "table.selectAll.clearSelection" | "table.selectAll.pageSelected" | "table.selectAll.selectAll" | "table.selection.label" | "table.sorting.ascending" | "table.sorting.descending" | "table.sorting.label" | "table.sorting.order" | "table.sorting.toolip" | "table.spacing" | "table.spacing.toolip">;
|
|
19
19
|
i18n: import("i18next").i18n;
|
|
20
20
|
ready: boolean;
|
|
21
21
|
};
|