@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 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 local-rules/prefer-event-specific-callback-naming
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
- react.useEffect(() => {
1481
- if (!defaultSelectedIds) {
1482
- return;
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
- const initialSelection = defaultSelectedIds.reduce((selection, id) => {
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 local-rules/prefer-event-specific-callback-naming
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
- useEffect(() => {
1480
- if (!defaultSelectedIds) {
1481
- return;
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
- const initialSelection = defaultSelectedIds.reduce((selection, id) => {
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.37-alpha-3fb17ca5f15.0",
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.33-alpha-3fb17ca5f15.0",
18
- "@trackunit/shared-utils": "1.13.49-alpha-3fb17ca5f15.0",
19
- "@trackunit/css-class-variance-utilities": "1.11.49-alpha-3fb17ca5f15.0",
20
- "@trackunit/ui-icons": "1.11.48-alpha-3fb17ca5f15.0",
21
- "@trackunit/react-table-base-components": "1.13.36-alpha-3fb17ca5f15.0",
22
- "@trackunit/react-form-components": "1.14.36-alpha-3fb17ca5f15.0",
23
- "@trackunit/i18n-library-translation": "1.12.35-alpha-3fb17ca5f15.0",
24
- "@trackunit/iris-app-runtime-core-api": "1.12.30-alpha-3fb17ca5f15.0",
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";
@@ -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
  };