@pushwoosh/smart-components-kit 2.1.4 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/Table/components/BulkSelectionBar/BulkSelectionBar.d.ts +11 -0
  2. package/Table/components/BulkSelectionBar/BulkSelectionBar.js +55 -0
  3. package/Table/components/BulkSelectionBar/index.d.ts +1 -0
  4. package/Table/components/BulkSelectionBar/index.js +1 -0
  5. package/Table/components/DataTable/DataTable.js +19 -2
  6. package/Table/components/DataTable/types.d.ts +10 -0
  7. package/Table/components/DataTableList/DataTableList.d.ts +1 -1
  8. package/Table/components/DataTableList/DataTableList.js +25 -2
  9. package/Table/components/DataTableList/types.d.ts +2 -0
  10. package/Table/components/Loading/Loading.d.ts +4 -1
  11. package/Table/components/Loading/Loading.js +11 -4
  12. package/Table/components/SelectionRow/SelectionRow.d.ts +19 -0
  13. package/Table/components/SelectionRow/SelectionRow.js +76 -0
  14. package/Table/components/SelectionRow/index.d.ts +2 -0
  15. package/Table/components/SelectionRow/index.js +2 -0
  16. package/Table/components/SelectionRow/styles.d.ts +19 -0
  17. package/Table/components/SelectionRow/styles.js +35 -0
  18. package/Table/hooks/index.d.ts +1 -0
  19. package/Table/hooks/index.js +2 -1
  20. package/Table/hooks/useBulkSelection.d.ts +22 -0
  21. package/Table/hooks/useBulkSelection.js +46 -0
  22. package/Table/index.d.ts +3 -1
  23. package/Table/index.js +2 -1
  24. package/Table/polyglot/messages_ar.js +5 -1
  25. package/Table/polyglot/messages_bn.js +5 -1
  26. package/Table/polyglot/messages_de.js +5 -1
  27. package/Table/polyglot/messages_en.js +5 -1
  28. package/Table/polyglot/messages_es.js +5 -1
  29. package/Table/polyglot/messages_fr.js +5 -1
  30. package/Table/polyglot/messages_he.js +5 -1
  31. package/Table/polyglot/messages_hi.js +5 -1
  32. package/Table/polyglot/messages_ja.js +5 -1
  33. package/Table/polyglot/messages_ko.js +5 -1
  34. package/Table/polyglot/messages_pt.js +5 -1
  35. package/Table/polyglot/messages_ru.js +5 -1
  36. package/Table/polyglot/messages_th.js +5 -1
  37. package/Table/polyglot/messages_zh.js +5 -1
  38. package/Table/polyglot/types.d.ts +8 -0
  39. package/Table/selection.d.ts +9 -0
  40. package/Table/selection.js +11 -0
  41. package/package.json +5 -2
@@ -0,0 +1,11 @@
1
+ import { type ReactElement } from 'react';
2
+ import type { BulkSelectionPageCheckbox } from '../../hooks/useBulkSelection';
3
+ /** Props of {@link BulkSelectionBar}. */
4
+ export type BulkSelectionBarProps = {
5
+ selectedCount: number;
6
+ offPageCount: number;
7
+ onClearSelection: () => void;
8
+ pageCheckbox?: BulkSelectionPageCheckbox;
9
+ };
10
+ /** Bulk mode status row: "Select all on this page" | N selected | M not on this page | Clear selection. */
11
+ export declare function BulkSelectionBar(props: BulkSelectionBarProps): ReactElement;
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import styled from 'styled-components';
3
+ import { Checkbox, Link } from '@pushwoosh/dumb-components';
4
+ import { Color } from '@pushwoosh/kit-constants';
5
+ import { Horizontal } from '@pushwoosh/kit-helpers';
6
+ import { Text } from '@pushwoosh/kit-typography';
7
+ import { useLang } from '../../polyglot';
8
+ const VerticalDivider = styled.span.withConfig({
9
+ displayName: "VerticalDivider",
10
+ componentId: "sc-6wj5m8-0"
11
+ })(["align-self:stretch;width:1px;background:", ";"], Color.DIVIDER);
12
+ /** Bulk mode status row: "Select all on this page" | N selected | M not on this page | Clear selection. */
13
+ export function BulkSelectionBar(props) {
14
+ const {
15
+ selectedCount,
16
+ offPageCount,
17
+ onClearSelection,
18
+ pageCheckbox
19
+ } = props;
20
+ const lang = useLang();
21
+ return _jsxs(Horizontal, {
22
+ "$alignItems": "center",
23
+ "$justifyContent": "start",
24
+ "$gap": 16,
25
+ children: [pageCheckbox && _jsxs(_Fragment, {
26
+ children: [_jsx(Checkbox, {
27
+ label: lang({
28
+ message: 'SMART.TABLE.BULK.SELECT_PAGE'
29
+ }),
30
+ isChecked: pageCheckbox.isChecked,
31
+ isIndeterminate: pageCheckbox.isIndeterminate,
32
+ isDisabled: pageCheckbox.isDisabled,
33
+ onChange: pageCheckbox.onChange
34
+ }), _jsx(VerticalDivider, {})]
35
+ }), _jsx(Text, {
36
+ "$variant": "h3",
37
+ children: lang({
38
+ message: 'SMART.TABLE.BULK.SELECTED_COUNT',
39
+ count: selectedCount
40
+ })
41
+ }), offPageCount > 0 && _jsx(Text, {
42
+ "$color": Color.LOCKED,
43
+ children: lang({
44
+ message: 'SMART.TABLE.BULK.OFF_PAGE',
45
+ count: offPageCount
46
+ })
47
+ }), selectedCount > 0 && _jsx(Link, {
48
+ as: "button",
49
+ onClick: onClearSelection,
50
+ children: lang({
51
+ message: 'SMART.TABLE.BULK.CLEAR_SELECTION'
52
+ })
53
+ })]
54
+ });
55
+ }
@@ -0,0 +1 @@
1
+ export { BulkSelectionBar, type BulkSelectionBarProps } from './BulkSelectionBar';
@@ -0,0 +1 @@
1
+ export { BulkSelectionBar } from './BulkSelectionBar';
@@ -6,9 +6,11 @@ import { WarningRoundIcon } from '@pushwoosh/kit-icons';
6
6
  import { Text } from '@pushwoosh/kit-typography';
7
7
  import { DataTableContext } from '../../context';
8
8
  import { useLang } from '../../polyglot';
9
+ import { getPageSelection } from '../../selection';
9
10
  import { LockedText, Table } from '../../styles';
10
11
  import { DataTableList } from '../DataTableList';
11
12
  import { DataTablePagination } from '../DataTablePagination';
13
+ import { SelectionHead } from '../SelectionRow';
12
14
  export function DataTable(props) {
13
15
  const {
14
16
  columns,
@@ -30,6 +32,20 @@ export function DataTable(props) {
30
32
  params
31
33
  } = context;
32
34
  const isFiltered = data.value?.isFiltered || params.search;
35
+ const selection = 'renderItem' in props ? props.selection : undefined;
36
+ const renderHead = () => {
37
+ if (!('renderHead' in props)) return null;
38
+ if (!selection) return props.renderHead();
39
+ const pageRows = data.loading ? [] : data.value?.items ?? [];
40
+ const page = getPageSelection(pageRows, selection.selectedKeys, selection.getKey, selection.isSelectable);
41
+ return _jsx(SelectionHead, {
42
+ isChecked: page.isChecked,
43
+ isIndeterminate: page.isIndeterminate,
44
+ isDisabled: page.isDisabled,
45
+ onChange: isChecked => selection.onToggle(page.selectable, isChecked),
46
+ children: props.renderHead()
47
+ });
48
+ };
33
49
  const onClearRequestDefault = () => {
34
50
  const keys = ['perPage', 'order', 'direction'];
35
51
  context.onChangeParams(keys.reduce((acc, key) => ({
@@ -47,8 +63,9 @@ export function DataTable(props) {
47
63
  width: width,
48
64
  isBordered: isBordered,
49
65
  "$bgColor": bgColor,
50
- children: [renderTableTop?.(context), 'renderHead' in props && props.renderHead(), 'renderList' in props ? props.renderList(context) : _jsx(DataTableList, {
51
- renderItem: props.renderItem
66
+ children: [renderTableTop?.(context), renderHead(), 'renderList' in props ? props.renderList(context) : _jsx(DataTableList, {
67
+ renderItem: props.renderItem,
68
+ selection: selection
52
69
  }), !hidePagination && _jsxs(DataTablePagination, {
53
70
  allLimits: allLimits ?? [10, 20, 50, 100],
54
71
  padding: paginationPadding,
@@ -15,6 +15,14 @@ export type DataTableListProps<I, P extends DataTableParams> = {
15
15
  renderAfter?: (ctxValue: DataTableContextType<I, P>) => ReactNode;
16
16
  onClearRequest?: () => void;
17
17
  };
18
+ /** Row selection of {@link DataTable}: the whole row is the checkbox, its content goes inert. */
19
+ export type DataTableSelection<I> = {
20
+ selectedKeys: ReadonlySet<string>;
21
+ getKey: (row: I) => string;
22
+ onToggle: (rows: I[], isChecked: boolean) => void;
23
+ /** Omitted: every row is selectable, as in flows that check eligibility on the chosen batch. */
24
+ isSelectable?: (row: I) => boolean;
25
+ };
18
26
  export type DataTableItemProps<I, P extends DataTableParams> = {
19
27
  columns: string;
20
28
  width?: string | number;
@@ -30,5 +38,7 @@ export type DataTableItemProps<I, P extends DataTableParams> = {
30
38
  renderItem: (item: I) => ReactNode;
31
39
  renderAfter?: (ctxValue: DataTableContextType<I, P>) => ReactNode;
32
40
  onClearRequest?: () => void;
41
+ /** Turns on bulk selection: a checkbox gutter is prepended to the head and to every row. */
42
+ selection?: DataTableSelection<I>;
33
43
  };
34
44
  export type DataTableProps<I, P extends DataTableParams> = DataTableListProps<I, P> | DataTableItemProps<I, P>;
@@ -1,2 +1,2 @@
1
1
  import type { DataTableListProps } from './types';
2
- export declare function DataTableList<T>({ renderItem }: DataTableListProps<T>): import("react").JSX.Element;
2
+ export declare function DataTableList<T>({ renderItem, selection }: DataTableListProps<T>): import("react").JSX.Element;
@@ -9,8 +9,10 @@ import { DataTableContext } from '../../context';
9
9
  import { useLang } from '../../polyglot';
10
10
  import { LockedText } from '../../styles';
11
11
  import { Loading } from '../Loading';
12
+ import { SelectableRow } from '../SelectionRow';
12
13
  export function DataTableList({
13
- renderItem
14
+ renderItem,
15
+ selection
14
16
  }) {
15
17
  const {
16
18
  data,
@@ -18,7 +20,28 @@ export function DataTableList({
18
20
  } = useContext(DataTableContext);
19
21
  const lang = useLang();
20
22
  if (data.loading) {
21
- return _jsx(Loading, {});
23
+ return selection ? _jsx(Loading, {
24
+ hasGutter: true
25
+ }) : _jsx(Loading, {});
26
+ }
27
+ if (data.value?.items?.length && selection) {
28
+ const {
29
+ selectedKeys,
30
+ getKey,
31
+ onToggle,
32
+ isSelectable
33
+ } = selection;
34
+ return _jsx(_Fragment, {
35
+ children: data.value.items.map(item => {
36
+ const key = getKey(item);
37
+ return _jsx(SelectableRow, {
38
+ isSelected: selectedKeys.has(key),
39
+ isSelectable: isSelectable?.(item) ?? true,
40
+ onToggle: isChecked => onToggle([item], isChecked),
41
+ children: renderItem(item)
42
+ }, key);
43
+ })
44
+ });
22
45
  }
23
46
  if (data.value?.items?.length) {
24
47
  return _jsx(_Fragment, {
@@ -1,4 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
+ import type { DataTableSelection } from '../DataTable/types';
2
3
  export type DataTableListProps<T> = {
3
4
  renderItem: (item: T) => ReactNode;
5
+ selection?: DataTableSelection<T>;
4
6
  };
@@ -1 +1,4 @@
1
- export declare const Loading: () => import("react").JSX.Element;
1
+ /** Skeleton rows of a loading table; `hasGutter` shifts them past the selection checkbox gutter. */
2
+ export declare const Loading: ({ hasGutter }?: {
3
+ hasGutter?: boolean;
4
+ }) => import("react").JSX.Element;
@@ -2,19 +2,26 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { TextSkeleton } from '@pushwoosh/dumb-components';
3
3
  import { Horizontal } from '@pushwoosh/kit-helpers';
4
4
  import { Wrapper } from './styles';
5
+ import { SELECTION_GUTTER_WIDTH } from '../SelectionRow';
6
+ const ROW_PADDING = 24;
5
7
  const Row = ({
6
- width
8
+ width,
9
+ hasGutter
7
10
  }) => _jsx(Horizontal, {
8
11
  "$alignItems": "center",
9
- "$padding": "0 24px",
12
+ "$padding": hasGutter ? `0 ${ROW_PADDING}px 0 ${SELECTION_GUTTER_WIDTH + ROW_PADDING}px` : `0 ${ROW_PADDING}px`,
10
13
  children: _jsx(TextSkeleton, {
11
14
  width: width
12
15
  })
13
16
  });
14
- export const Loading = () => _jsx(Wrapper, {
17
+ /** Skeleton rows of a loading table; `hasGutter` shifts them past the selection checkbox gutter. */
18
+ export const Loading = ({
19
+ hasGutter
20
+ } = {}) => _jsx(Wrapper, {
15
21
  children: Array.from({
16
22
  length: 4
17
23
  }).map((_, index) => _jsx(Row, {
18
- width: `${index % 2 ? 35 : 50}%`
24
+ width: `${index % 2 ? 35 : 50}%`,
25
+ hasGutter: hasGutter
19
26
  }, index))
20
27
  });
@@ -0,0 +1,19 @@
1
+ import { type ReactElement, type ReactNode } from 'react';
2
+ type SelectableRowProps = {
3
+ isSelected: boolean;
4
+ isSelectable: boolean;
5
+ onToggle: (isChecked: boolean) => void;
6
+ children: ReactNode;
7
+ };
8
+ /** A table row that is itself the checkbox: click, Space or Enter toggle it, its content is inert. */
9
+ export declare function SelectableRow({ isSelected, isSelectable, onToggle, children, }: SelectableRowProps): ReactElement;
10
+ type SelectionHeadProps = {
11
+ isChecked: boolean;
12
+ isIndeterminate: boolean;
13
+ isDisabled: boolean;
14
+ onChange: (isChecked: boolean) => void;
15
+ children: ReactNode;
16
+ };
17
+ /** The table head in selection mode: the gutter holds the "select the page" checkbox. */
18
+ export declare function SelectionHead({ isChecked, isIndeterminate, isDisabled, onChange, children, }: SelectionHeadProps): ReactElement;
19
+ export {};
@@ -0,0 +1,76 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useId } from 'react';
3
+ import { Checkbox } from '@pushwoosh/dumb-components';
4
+ import { SelectionContent, SelectionGutter, SelectionRowBox } from './styles';
5
+ /** A table row that is itself the checkbox: click, Space or Enter toggle it, its content is inert. */
6
+ export function SelectableRow({
7
+ isSelected,
8
+ isSelectable,
9
+ onToggle,
10
+ children
11
+ }) {
12
+ const contentId = useId();
13
+ const toggle = () => {
14
+ if (isSelectable) {
15
+ onToggle(!isSelected);
16
+ }
17
+ };
18
+ const onClickCapture = event => {
19
+ event.preventDefault();
20
+ event.stopPropagation();
21
+ toggle();
22
+ };
23
+ const onKeyDown = event => {
24
+ if (event.target === event.currentTarget && (event.key === ' ' || event.key === 'Enter')) {
25
+ event.preventDefault();
26
+ toggle();
27
+ }
28
+ };
29
+ return _jsxs(SelectionRowBox, {
30
+ role: "checkbox",
31
+ "aria-checked": isSelected,
32
+ "aria-disabled": isSelectable ? undefined : true,
33
+ "aria-labelledby": contentId,
34
+ tabIndex: isSelectable ? 0 : -1,
35
+ "$isSelected": isSelected,
36
+ "$isSelectable": isSelectable,
37
+ "$isInteractive": true,
38
+ onClickCapture: onClickCapture,
39
+ onKeyDown: onKeyDown,
40
+ children: [_jsx(SelectionGutter, {
41
+ "aria-hidden": true,
42
+ inert: true,
43
+ "$isInert": true,
44
+ children: _jsx(Checkbox, {
45
+ isChecked: isSelected,
46
+ isDisabled: !isSelectable
47
+ })
48
+ }), _jsx(SelectionContent, {
49
+ id: contentId,
50
+ inert: true,
51
+ "$isInert": true,
52
+ children: children
53
+ })]
54
+ });
55
+ }
56
+ /** The table head in selection mode: the gutter holds the "select the page" checkbox. */
57
+ export function SelectionHead({
58
+ isChecked,
59
+ isIndeterminate,
60
+ isDisabled,
61
+ onChange,
62
+ children
63
+ }) {
64
+ return _jsxs(SelectionRowBox, {
65
+ children: [_jsx(SelectionGutter, {
66
+ children: _jsx(Checkbox, {
67
+ isChecked: isChecked,
68
+ isIndeterminate: isIndeterminate,
69
+ isDisabled: isDisabled,
70
+ onChange: onChange
71
+ })
72
+ }), _jsx(SelectionContent, {
73
+ children: children
74
+ })]
75
+ });
76
+ }
@@ -0,0 +1,2 @@
1
+ export { SelectableRow, SelectionHead } from './SelectionRow';
2
+ export { SELECTION_GUTTER_WIDTH, SELECTION_ROW_PADDING } from './styles';
@@ -0,0 +1,2 @@
1
+ export { SelectableRow, SelectionHead } from './SelectionRow';
2
+ export { SELECTION_GUTTER_WIDTH, SELECTION_ROW_PADDING } from './styles';
@@ -0,0 +1,19 @@
1
+ export declare const SELECTION_GUTTER_WIDTH = 42;
2
+ export declare const SELECTION_ROW_PADDING = 12;
3
+ export declare const SelectionRowBox: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Merged<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "style"> & {
4
+ style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
5
+ }, {
6
+ $isSelected?: boolean;
7
+ $isSelectable?: boolean;
8
+ $isInteractive?: boolean;
9
+ }>> & string;
10
+ export declare const SelectionGutter: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Merged<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "style"> & {
11
+ style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
12
+ }, {
13
+ $isInert?: boolean;
14
+ }>> & string;
15
+ export declare const SelectionContent: import("styled-components/dist/types").IStyledComponentBase<"web", import("styled-components/dist/types").Merged<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "style"> & {
16
+ style?: import("react").CSSProperties | import("styled-components/dist/types").CSSPropertiesWithVars | undefined;
17
+ }, {
18
+ $isInert?: boolean;
19
+ }>> & string;
@@ -0,0 +1,35 @@
1
+ import styled from 'styled-components';
2
+ import { Color } from '@pushwoosh/kit-constants';
3
+ export const SELECTION_GUTTER_WIDTH = 42;
4
+ export const SELECTION_ROW_PADDING = 12;
5
+ export const SelectionRowBox = styled.div.withConfig({
6
+ displayName: "SelectionRowBox",
7
+ componentId: "sc-1yh7kn7-0"
8
+ })(["display:grid;grid-template-columns:", "px minmax(0,1fr);padding-left:", "px;background-color:", ";", " ", " &:hover{", "}&:focus-visible{outline:2px solid ", ";outline-offset:-2px;}"], SELECTION_GUTTER_WIDTH, SELECTION_ROW_PADDING, ({
9
+ $isSelected
10
+ }) => $isSelected ? Color.BRIGHT_LIGHT : 'transparent', ({
11
+ $isInteractive,
12
+ $isSelectable
13
+ }) => $isInteractive ? `cursor: ${$isSelectable ? 'pointer' : 'default'};` : '', ({
14
+ $isInteractive,
15
+ $isSelectable
16
+ }) => $isInteractive && !$isSelectable ? 'opacity: 0.5;' : '', ({
17
+ $isInteractive,
18
+ $isSelectable,
19
+ $isSelected
20
+ }) => {
21
+ if (!$isInteractive || !$isSelectable) return '';
22
+ return `background-color: ${$isSelected ? Color.BRIGHT_LIGHT_HOVER : Color.ROW_HOVER};`;
23
+ }, Color.PRIMARY);
24
+ export const SelectionGutter = styled.div.withConfig({
25
+ displayName: "SelectionGutter",
26
+ componentId: "sc-1yh7kn7-1"
27
+ })(["display:flex;align-items:center;padding-left:", "px;", ""], SELECTION_ROW_PADDING, ({
28
+ $isInert
29
+ }) => $isInert ? 'pointer-events: none;' : '');
30
+ export const SelectionContent = styled.div.withConfig({
31
+ displayName: "SelectionContent",
32
+ componentId: "sc-1yh7kn7-2"
33
+ })(["min-width:0;", " && > *{padding-left:0;}"], ({
34
+ $isInert
35
+ }) => $isInert ? 'pointer-events: none; user-select: none;' : '');
@@ -1,3 +1,4 @@
1
1
  export { useDataTableContext } from './useDataTableContext';
2
2
  export { useDataTableContextSync } from './useDataTableContextSync';
3
3
  export { useDataTableGetParams } from './useDataTableGetParams';
4
+ export { useBulkSelection, type BulkSelection, type BulkSelectionPageCheckbox } from './useBulkSelection';
@@ -1,3 +1,4 @@
1
1
  export { useDataTableContext } from './useDataTableContext';
2
2
  export { useDataTableContextSync } from './useDataTableContextSync';
3
- export { useDataTableGetParams } from './useDataTableGetParams';
3
+ export { useDataTableGetParams } from './useDataTableGetParams';
4
+ export { useBulkSelection } from './useBulkSelection';
@@ -0,0 +1,22 @@
1
+ /** Props of the "select the page" checkbox of `BulkSelectionBar`. */
2
+ export type BulkSelectionPageCheckbox = {
3
+ isChecked: boolean;
4
+ isIndeterminate: boolean;
5
+ isDisabled?: boolean;
6
+ onChange: (isChecked: boolean) => void;
7
+ };
8
+ /** What {@link useBulkSelection} returns. */
9
+ export type BulkSelection<T, M extends string> = {
10
+ mode: M | null;
11
+ isSelecting: boolean;
12
+ selected: T[];
13
+ selectedKeys: ReadonlySet<string>;
14
+ start: (mode: M) => void;
15
+ cancel: () => void;
16
+ clear: () => void;
17
+ toggle: (rows: T[], isChecked: boolean) => void;
18
+ offPageCount: (pageRows: readonly T[]) => number;
19
+ pageCheckbox: (pageRows: readonly T[], isSelectable?: (row: T) => boolean) => BulkSelectionPageCheckbox;
20
+ };
21
+ /** Bulk mode and the rows picked in it, kept across pages; `getKey` should be stable. */
22
+ export declare function useBulkSelection<T, M extends string = string>(getKey: (row: T) => string): BulkSelection<T, M>;
@@ -0,0 +1,46 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import { getPageSelection } from '../selection';
3
+ /** Bulk mode and the rows picked in it, kept across pages; `getKey` should be stable. */
4
+ export function useBulkSelection(getKey) {
5
+ const [mode, setMode] = useState(null);
6
+ const [selected, setSelected] = useState([]);
7
+ const selectedKeys = useMemo(() => new Set(selected.map(getKey)), [selected, getKey]);
8
+ const start = useCallback(nextMode => {
9
+ setSelected([]);
10
+ setMode(nextMode);
11
+ }, []);
12
+ const cancel = useCallback(() => {
13
+ setSelected([]);
14
+ setMode(null);
15
+ }, []);
16
+ const clear = useCallback(() => setSelected([]), []);
17
+ const toggle = useCallback((rows, isChecked) => {
18
+ const toggledKeys = new Set(rows.map(getKey));
19
+ setSelected(prev => {
20
+ const rest = prev.filter(row => !toggledKeys.has(getKey(row)));
21
+ return isChecked ? [...rest, ...rows] : rest;
22
+ });
23
+ }, [getKey]);
24
+ const offPageCount = useCallback(pageRows => selected.length - pageRows.filter(row => selectedKeys.has(getKey(row))).length, [selected, selectedKeys, getKey]);
25
+ const pageCheckbox = useCallback((pageRows, isSelectable) => {
26
+ const page = getPageSelection(pageRows, selectedKeys, getKey, isSelectable);
27
+ return {
28
+ isChecked: page.isChecked,
29
+ isIndeterminate: page.isIndeterminate,
30
+ isDisabled: page.isDisabled,
31
+ onChange: isChecked => toggle(page.selectable, isChecked)
32
+ };
33
+ }, [selectedKeys, getKey, toggle]);
34
+ return {
35
+ mode,
36
+ isSelecting: mode !== null,
37
+ selected,
38
+ selectedKeys,
39
+ start,
40
+ cancel,
41
+ clear,
42
+ toggle,
43
+ offPageCount,
44
+ pageCheckbox
45
+ };
46
+ }
package/Table/index.d.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  export { DataTable } from './components/DataTable';
2
+ export { BulkSelectionBar, type BulkSelectionBarProps } from './components/BulkSelectionBar';
2
3
  export { DataTableGetContext } from './components/DataTableGetContext';
3
4
  export { DataTablePagination } from './components/DataTablePagination';
4
5
  export { DataTableSearchInput } from './components/DataTableSearchInput';
5
6
  export { DataTableSortableCell } from './components/DataTableSortableCell';
6
7
  export { Loading } from './components/Loading';
7
8
  export { TablePagination } from './components/TablePagination';
8
- export { useDataTableContext, useDataTableGetParams } from './hooks';
9
+ export { useDataTableContext, useDataTableGetParams, useBulkSelection, type BulkSelection, type BulkSelectionPageCheckbox, } from './hooks';
9
10
  export { Table, Td, Tr, Th, ElementHover, ItemName, LockedText, Description, } from './styles';
10
11
  export { ItemDetailLink } from './components/ItemDetailLink';
11
12
  export type { DataTableParams, DataTableData, DataTableContextType } from './types';
13
+ export type { DataTableSelection } from './components/DataTable/types';
package/Table/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  export { DataTable } from './components/DataTable';
2
+ export { BulkSelectionBar } from './components/BulkSelectionBar';
2
3
  export { DataTableGetContext } from './components/DataTableGetContext';
3
4
  export { DataTablePagination } from './components/DataTablePagination';
4
5
  export { DataTableSearchInput } from './components/DataTableSearchInput';
5
6
  export { DataTableSortableCell } from './components/DataTableSortableCell';
6
7
  export { Loading } from './components/Loading';
7
8
  export { TablePagination } from './components/TablePagination';
8
- export { useDataTableContext, useDataTableGetParams } from './hooks';
9
+ export { useDataTableContext, useDataTableGetParams, useBulkSelection } from './hooks';
9
10
  export { Table, Td, Tr, Th, ElementHover, ItemName, LockedText, Description } from './styles';
10
11
  export { ItemDetailLink } from './components/ItemDetailLink';
@@ -14,5 +14,9 @@ export const messages = {
14
14
  'SMART.TABLE.EMPTY_IMG_ALT': 'صندوق فارغ',
15
15
  'SMART.TABLE.EMPTY_TITLE': 'هذه القائمة فارغة',
16
16
  'SMART.TABLE.EMPTY_DESCRIPTION': 'يبدو أنه لا يوجد شيء هنا بعد. حان وقت الإنشاء — لنمنح هذه القائمة الحياة!',
17
- 'SMART.TABLE.SHOW_ON_PAGE': 'عرض في الصفحة:'
17
+ 'SMART.TABLE.SHOW_ON_PAGE': 'عرض في الصفحة:',
18
+ 'SMART.TABLE.BULK.SELECT_PAGE': 'تحديد الكل في هذه الصفحة',
19
+ 'SMART.TABLE.BULK.SELECTED_COUNT': 'تم تحديد {{count}}',
20
+ 'SMART.TABLE.BULK.OFF_PAGE': 'منها {{count}} ليست في هذه الصفحة',
21
+ 'SMART.TABLE.BULK.CLEAR_SELECTION': 'مسح التحديد'
18
22
  };
@@ -14,5 +14,9 @@ export const messages = {
14
14
  'SMART.TABLE.EMPTY_IMG_ALT': 'খালি বাক্স',
15
15
  'SMART.TABLE.EMPTY_TITLE': 'এই তালিকাটি খালি',
16
16
  'SMART.TABLE.EMPTY_DESCRIPTION': 'মনে হচ্ছে এখানে এখনও কিছু নেই। কিছু তৈরি করে এই তালিকাটিকে প্রাণবন্ত করে তুলুন!',
17
- 'SMART.TABLE.SHOW_ON_PAGE': 'প্রতি পেজে দেখান:'
17
+ 'SMART.TABLE.SHOW_ON_PAGE': 'প্রতি পেজে দেখান:',
18
+ 'SMART.TABLE.BULK.SELECT_PAGE': 'এই পৃষ্ঠার সব নির্বাচন করুন',
19
+ 'SMART.TABLE.BULK.SELECTED_COUNT': '{{count}}টি নির্বাচিত',
20
+ 'SMART.TABLE.BULK.OFF_PAGE': 'এর মধ্যে {{count}}টি এই পৃষ্ঠায় নেই',
21
+ 'SMART.TABLE.BULK.CLEAR_SELECTION': 'নির্বাচন মুছুন'
18
22
  };
@@ -14,5 +14,9 @@ export const messages = {
14
14
  'SMART.TABLE.EMPTY_IMG_ALT': 'Leere Schachtel',
15
15
  'SMART.TABLE.EMPTY_TITLE': 'Diese Liste ist leer',
16
16
  'SMART.TABLE.EMPTY_DESCRIPTION': 'Hier ist noch nichts. Zeit, etwas zu erstellen und diese Liste mit Leben zu füllen!',
17
- 'SMART.TABLE.SHOW_ON_PAGE': 'Pro Seite anzeigen:'
17
+ 'SMART.TABLE.SHOW_ON_PAGE': 'Pro Seite anzeigen:',
18
+ 'SMART.TABLE.BULK.SELECT_PAGE': 'Alle auf dieser Seite auswählen',
19
+ 'SMART.TABLE.BULK.SELECTED_COUNT': '{{count}} ausgewählt',
20
+ 'SMART.TABLE.BULK.OFF_PAGE': 'davon {{count}} nicht auf dieser Seite',
21
+ 'SMART.TABLE.BULK.CLEAR_SELECTION': 'Auswahl aufheben'
18
22
  };
@@ -14,5 +14,9 @@ export const messages = {
14
14
  'SMART.TABLE.EMPTY_IMG_ALT': 'Empty box',
15
15
  'SMART.TABLE.EMPTY_TITLE': 'This list is empty',
16
16
  'SMART.TABLE.EMPTY_DESCRIPTION': 'Looks like there’s nothing here yet. Time to create — let’s bring this list to life!',
17
- 'SMART.TABLE.SHOW_ON_PAGE': 'Show on page:'
17
+ 'SMART.TABLE.SHOW_ON_PAGE': 'Show on page:',
18
+ 'SMART.TABLE.BULK.SELECT_PAGE': 'Select all on this page',
19
+ 'SMART.TABLE.BULK.SELECTED_COUNT': '{{count}} selected',
20
+ 'SMART.TABLE.BULK.OFF_PAGE': 'incl. {{count}} not on this page',
21
+ 'SMART.TABLE.BULK.CLEAR_SELECTION': 'Clear selection'
18
22
  };
@@ -14,5 +14,9 @@ export const messages = {
14
14
  'SMART.TABLE.EMPTY_IMG_ALT': 'Caja vacía',
15
15
  'SMART.TABLE.EMPTY_TITLE': 'Esta lista está vacía',
16
16
  'SMART.TABLE.EMPTY_DESCRIPTION': 'Parece que aún no hay nada aquí. ¡Es hora de crear algo y dar vida a esta lista!',
17
- 'SMART.TABLE.SHOW_ON_PAGE': 'Mostrar por página:'
17
+ 'SMART.TABLE.SHOW_ON_PAGE': 'Mostrar por página:',
18
+ 'SMART.TABLE.BULK.SELECT_PAGE': 'Seleccionar todo en esta página',
19
+ 'SMART.TABLE.BULK.SELECTED_COUNT': '{{count}} {{count | pluralize: "seleccionado", "seleccionados"}}',
20
+ 'SMART.TABLE.BULK.OFF_PAGE': 'incl. {{count}} fuera de esta página',
21
+ 'SMART.TABLE.BULK.CLEAR_SELECTION': 'Borrar selección'
18
22
  };
@@ -14,5 +14,9 @@ export const messages = {
14
14
  'SMART.TABLE.EMPTY_IMG_ALT': 'Boîte vide',
15
15
  'SMART.TABLE.EMPTY_TITLE': 'Cette liste est vide',
16
16
  'SMART.TABLE.EMPTY_DESCRIPTION': 'Il n’y a encore rien ici. C’est le moment de créer quelque chose et de donner vie à cette liste !',
17
- 'SMART.TABLE.SHOW_ON_PAGE': 'Afficher par page :'
17
+ 'SMART.TABLE.SHOW_ON_PAGE': 'Afficher par page :',
18
+ 'SMART.TABLE.BULK.SELECT_PAGE': 'Tout sélectionner sur cette page',
19
+ 'SMART.TABLE.BULK.SELECTED_COUNT': '{{count}} {{count | pluralize: "sélectionné", "sélectionnés"}}',
20
+ 'SMART.TABLE.BULK.OFF_PAGE': 'dont {{count}} hors de cette page',
21
+ 'SMART.TABLE.BULK.CLEAR_SELECTION': 'Effacer la sélection'
18
22
  };
@@ -14,5 +14,9 @@ export const messages = {
14
14
  'SMART.TABLE.EMPTY_IMG_ALT': 'קופסה ריקה',
15
15
  'SMART.TABLE.EMPTY_TITLE': 'הרשימה ריקה',
16
16
  'SMART.TABLE.EMPTY_DESCRIPTION': 'נראה שאין כאן עדיין כלום. זה הזמן ליצור משהו ולהפיח חיים ברשימה!',
17
- 'SMART.TABLE.SHOW_ON_PAGE': 'הצגה בעמוד:'
17
+ 'SMART.TABLE.SHOW_ON_PAGE': 'הצגה בעמוד:',
18
+ 'SMART.TABLE.BULK.SELECT_PAGE': 'בחירת הכול בעמוד זה',
19
+ 'SMART.TABLE.BULK.SELECTED_COUNT': '{{count}} נבחרו',
20
+ 'SMART.TABLE.BULK.OFF_PAGE': 'כולל {{count}} שאינם בעמוד זה',
21
+ 'SMART.TABLE.BULK.CLEAR_SELECTION': 'ניקוי הבחירה'
18
22
  };
@@ -14,5 +14,9 @@ export const messages = {
14
14
  'SMART.TABLE.EMPTY_IMG_ALT': 'खाली बॉक्स',
15
15
  'SMART.TABLE.EMPTY_TITLE': 'यह सूची खाली है',
16
16
  'SMART.TABLE.EMPTY_DESCRIPTION': 'लगता है यहाँ अभी कुछ नहीं है। कुछ बनाइए और इस सूची में जान डाल दीजिए!',
17
- 'SMART.TABLE.SHOW_ON_PAGE': 'प्रति पेज दिखाएँ:'
17
+ 'SMART.TABLE.SHOW_ON_PAGE': 'प्रति पेज दिखाएँ:',
18
+ 'SMART.TABLE.BULK.SELECT_PAGE': 'इस पेज पर सभी चुनें',
19
+ 'SMART.TABLE.BULK.SELECTED_COUNT': '{{count}} चयनित',
20
+ 'SMART.TABLE.BULK.OFF_PAGE': 'इनमें से {{count}} इस पेज पर नहीं',
21
+ 'SMART.TABLE.BULK.CLEAR_SELECTION': 'चयन साफ़ करें'
18
22
  };
@@ -14,5 +14,9 @@ export const messages = {
14
14
  'SMART.TABLE.EMPTY_IMG_ALT': '空の箱',
15
15
  'SMART.TABLE.EMPTY_TITLE': 'このリストは空です',
16
16
  'SMART.TABLE.EMPTY_DESCRIPTION': 'まだ何もありません。さっそく作成して、このリストを充実させましょう!',
17
- 'SMART.TABLE.SHOW_ON_PAGE': 'ページあたりの表示件数:'
17
+ 'SMART.TABLE.SHOW_ON_PAGE': 'ページあたりの表示件数:',
18
+ 'SMART.TABLE.BULK.SELECT_PAGE': 'このページをすべて選択',
19
+ 'SMART.TABLE.BULK.SELECTED_COUNT': '{{count}} 件選択中',
20
+ 'SMART.TABLE.BULK.OFF_PAGE': 'うち {{count}} 件はこのページ外',
21
+ 'SMART.TABLE.BULK.CLEAR_SELECTION': '選択を解除'
18
22
  };
@@ -14,5 +14,9 @@ export const messages = {
14
14
  'SMART.TABLE.EMPTY_IMG_ALT': '빈 상자',
15
15
  'SMART.TABLE.EMPTY_TITLE': '목록이 비어 있습니다',
16
16
  'SMART.TABLE.EMPTY_DESCRIPTION': '아직 아무것도 없습니다. 지금 만들어서 이 목록을 채워 보세요!',
17
- 'SMART.TABLE.SHOW_ON_PAGE': '페이지당 표시:'
17
+ 'SMART.TABLE.SHOW_ON_PAGE': '페이지당 표시:',
18
+ 'SMART.TABLE.BULK.SELECT_PAGE': '이 페이지 전체 선택',
19
+ 'SMART.TABLE.BULK.SELECTED_COUNT': '{{count}}개 선택됨',
20
+ 'SMART.TABLE.BULK.OFF_PAGE': '이 페이지에 없는 {{count}}개 포함',
21
+ 'SMART.TABLE.BULK.CLEAR_SELECTION': '선택 해제'
18
22
  };
@@ -14,5 +14,9 @@ export const messages = {
14
14
  'SMART.TABLE.EMPTY_IMG_ALT': 'Caixa vazia',
15
15
  'SMART.TABLE.EMPTY_TITLE': 'Esta lista está vazia',
16
16
  'SMART.TABLE.EMPTY_DESCRIPTION': 'Parece que ainda não há nada aqui. Hora de criar algo e dar vida a esta lista!',
17
- 'SMART.TABLE.SHOW_ON_PAGE': 'Mostrar por página:'
17
+ 'SMART.TABLE.SHOW_ON_PAGE': 'Mostrar por página:',
18
+ 'SMART.TABLE.BULK.SELECT_PAGE': 'Selecionar tudo nesta página',
19
+ 'SMART.TABLE.BULK.SELECTED_COUNT': '{{count}} {{count | pluralize: "selecionado", "selecionados"}}',
20
+ 'SMART.TABLE.BULK.OFF_PAGE': 'incl. {{count}} fora desta página',
21
+ 'SMART.TABLE.BULK.CLEAR_SELECTION': 'Limpar seleção'
18
22
  };
@@ -14,5 +14,9 @@ export const messages = {
14
14
  'SMART.TABLE.EMPTY_IMG_ALT': 'Пустой список',
15
15
  'SMART.TABLE.EMPTY_TITLE': 'Список пуст',
16
16
  'SMART.TABLE.EMPTY_DESCRIPTION': 'Похоже, здесь пока ничего нет. Самое время создать что-нибудь и оживить этот список!',
17
- 'SMART.TABLE.SHOW_ON_PAGE': 'Показывать на странице:'
17
+ 'SMART.TABLE.SHOW_ON_PAGE': 'Показывать на странице:',
18
+ 'SMART.TABLE.BULK.SELECT_PAGE': 'Выбрать все на этой странице',
19
+ 'SMART.TABLE.BULK.SELECTED_COUNT': 'Выбрано: {{count}}',
20
+ 'SMART.TABLE.BULK.OFF_PAGE': 'в т. ч. {{count}} не на этой странице',
21
+ 'SMART.TABLE.BULK.CLEAR_SELECTION': 'Сбросить выбор'
18
22
  };
@@ -14,5 +14,9 @@ export const messages = {
14
14
  'SMART.TABLE.EMPTY_IMG_ALT': 'กล่องเปล่า',
15
15
  'SMART.TABLE.EMPTY_TITLE': 'รายการนี้ว่างเปล่า',
16
16
  'SMART.TABLE.EMPTY_DESCRIPTION': 'ดูเหมือนว่ายังไม่มีอะไรที่นี่ ถึงเวลาสร้างสรรค์แล้ว มาเติมชีวิตให้รายการนี้กันเถอะ!',
17
- 'SMART.TABLE.SHOW_ON_PAGE': 'แสดงต่อหน้า:'
17
+ 'SMART.TABLE.SHOW_ON_PAGE': 'แสดงต่อหน้า:',
18
+ 'SMART.TABLE.BULK.SELECT_PAGE': 'เลือกทั้งหมดในหน้านี้',
19
+ 'SMART.TABLE.BULK.SELECTED_COUNT': 'เลือกแล้ว {{count}} รายการ',
20
+ 'SMART.TABLE.BULK.OFF_PAGE': 'รวม {{count}} รายการที่ไม่อยู่ในหน้านี้',
21
+ 'SMART.TABLE.BULK.CLEAR_SELECTION': 'ล้างการเลือก'
18
22
  };
@@ -14,5 +14,9 @@ export const messages = {
14
14
  'SMART.TABLE.EMPTY_IMG_ALT': '空盒子',
15
15
  'SMART.TABLE.EMPTY_TITLE': '此列表为空',
16
16
  'SMART.TABLE.EMPTY_DESCRIPTION': '这里似乎还什么都没有。现在就开始创建,让这个列表丰富起来吧!',
17
- 'SMART.TABLE.SHOW_ON_PAGE': '每页显示:'
17
+ 'SMART.TABLE.SHOW_ON_PAGE': '每页显示:',
18
+ 'SMART.TABLE.BULK.SELECT_PAGE': '选择本页全部',
19
+ 'SMART.TABLE.BULK.SELECTED_COUNT': '已选择 {{count}} 项',
20
+ 'SMART.TABLE.BULK.OFF_PAGE': '含 {{count}} 项不在本页',
21
+ 'SMART.TABLE.BULK.CLEAR_SELECTION': '清除选择'
18
22
  };
@@ -18,4 +18,12 @@ export type TableMessages = {
18
18
  'SMART.TABLE.EMPTY_TITLE': NoParams;
19
19
  'SMART.TABLE.EMPTY_DESCRIPTION': NoParams;
20
20
  'SMART.TABLE.SHOW_ON_PAGE': NoParams;
21
+ 'SMART.TABLE.BULK.SELECT_PAGE': NoParams;
22
+ 'SMART.TABLE.BULK.SELECTED_COUNT': {
23
+ count: number;
24
+ };
25
+ 'SMART.TABLE.BULK.OFF_PAGE': {
26
+ count: number;
27
+ };
28
+ 'SMART.TABLE.BULK.CLEAR_SELECTION': NoParams;
21
29
  };
@@ -0,0 +1,9 @@
1
+ /** State of a "select the whole page" checkbox over the rows that can be selected. */
2
+ export type PageSelection<T> = {
3
+ selectable: T[];
4
+ isChecked: boolean;
5
+ isIndeterminate: boolean;
6
+ isDisabled: boolean;
7
+ };
8
+ /** Checked, indeterminate or disabled "select page" state; rows failing `isSelectable` are left out. */
9
+ export declare function getPageSelection<T>(rows: readonly T[], selectedKeys: ReadonlySet<string>, getKey: (row: T) => string, isSelectable?: (row: T) => boolean): PageSelection<T>;
@@ -0,0 +1,11 @@
1
+ /** Checked, indeterminate or disabled "select page" state; rows failing `isSelectable` are left out. */
2
+ export function getPageSelection(rows, selectedKeys, getKey, isSelectable) {
3
+ const selectable = isSelectable ? rows.filter(isSelectable) : [...rows];
4
+ const selectedCount = selectable.filter(row => selectedKeys.has(getKey(row))).length;
5
+ return {
6
+ selectable,
7
+ isChecked: selectable.length > 0 && selectedCount === selectable.length,
8
+ isIndeterminate: selectedCount > 0 && selectedCount < selectable.length,
9
+ isDisabled: !selectable.length
10
+ };
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushwoosh/smart-components-kit",
3
- "version": "2.1.4",
3
+ "version": "2.2.0",
4
4
  "description": "Stateful, data-connected React components shared by Pushwoosh micro-frontends",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,7 +17,7 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@floating-ui/react-dom": "^2.1.2",
20
- "@pushwoosh/dumb-components": "^4.6.0",
20
+ "@pushwoosh/dumb-components": "^4.7.0",
21
21
  "@pushwoosh/kit-constants": "^3.0.0",
22
22
  "@pushwoosh/kit-helpers": "^4.16.0",
23
23
  "@pushwoosh/kit-icons": "^2.3.15",
@@ -30,11 +30,14 @@
30
30
  "@pushwoosh/frontend-builder-engine": "^4.0.0",
31
31
  "@pushwoosh/spa-context": "^3.3.1",
32
32
  "@svgr/webpack": "^8.1.0",
33
+ "@testing-library/dom": "^10.4.2",
34
+ "@testing-library/react": "^16.3.0",
33
35
  "@types/node": "^24.19.1",
34
36
  "@types/react": "^19.3.0",
35
37
  "@types/react-dom": "^19.3.0",
36
38
  "@types/react-router-dom": "^5.3.3",
37
39
  "@types/webpack-env": "^1.18.8",
40
+ "jsdom": "^26.1.0",
38
41
  "react": "^19.3.0",
39
42
  "react-dom": "^19.3.0",
40
43
  "react-router-dom": "^5.3.4",