@pushwoosh/smart-components-kit 2.1.3 → 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 (46) hide show
  1. package/DynamicContentPicker/DynamicContentPicker.js +14 -9
  2. package/MediaStorage/CanvaImportDrawer.js +5 -3
  3. package/MediaStorage/MediaStorage.js +7 -3
  4. package/PickerSort/usePagedItems.js +1 -1
  5. package/Table/components/BulkSelectionBar/BulkSelectionBar.d.ts +11 -0
  6. package/Table/components/BulkSelectionBar/BulkSelectionBar.js +55 -0
  7. package/Table/components/BulkSelectionBar/index.d.ts +1 -0
  8. package/Table/components/BulkSelectionBar/index.js +1 -0
  9. package/Table/components/DataTable/DataTable.js +19 -2
  10. package/Table/components/DataTable/types.d.ts +10 -0
  11. package/Table/components/DataTableList/DataTableList.d.ts +1 -1
  12. package/Table/components/DataTableList/DataTableList.js +25 -2
  13. package/Table/components/DataTableList/types.d.ts +2 -0
  14. package/Table/components/Loading/Loading.d.ts +4 -1
  15. package/Table/components/Loading/Loading.js +11 -4
  16. package/Table/components/SelectionRow/SelectionRow.d.ts +19 -0
  17. package/Table/components/SelectionRow/SelectionRow.js +76 -0
  18. package/Table/components/SelectionRow/index.d.ts +2 -0
  19. package/Table/components/SelectionRow/index.js +2 -0
  20. package/Table/components/SelectionRow/styles.d.ts +19 -0
  21. package/Table/components/SelectionRow/styles.js +35 -0
  22. package/Table/hooks/index.d.ts +1 -0
  23. package/Table/hooks/index.js +2 -1
  24. package/Table/hooks/useBulkSelection.d.ts +22 -0
  25. package/Table/hooks/useBulkSelection.js +46 -0
  26. package/Table/hooks/useDataTableGetParams.js +22 -10
  27. package/Table/index.d.ts +3 -1
  28. package/Table/index.js +2 -1
  29. package/Table/polyglot/messages_ar.js +5 -1
  30. package/Table/polyglot/messages_bn.js +5 -1
  31. package/Table/polyglot/messages_de.js +5 -1
  32. package/Table/polyglot/messages_en.js +5 -1
  33. package/Table/polyglot/messages_es.js +5 -1
  34. package/Table/polyglot/messages_fr.js +5 -1
  35. package/Table/polyglot/messages_he.js +5 -1
  36. package/Table/polyglot/messages_hi.js +5 -1
  37. package/Table/polyglot/messages_ja.js +5 -1
  38. package/Table/polyglot/messages_ko.js +5 -1
  39. package/Table/polyglot/messages_pt.js +5 -1
  40. package/Table/polyglot/messages_ru.js +5 -1
  41. package/Table/polyglot/messages_th.js +5 -1
  42. package/Table/polyglot/messages_zh.js +5 -1
  43. package/Table/polyglot/types.d.ts +8 -0
  44. package/Table/selection.d.ts +9 -0
  45. package/Table/selection.js +11 -0
  46. package/package.json +5 -2
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { arrow, autoPlacement, offset, useFloating } from '@floating-ui/react-dom';
3
- import { useEffect, useMemo, useReducer, useRef, useState } from 'react';
3
+ import { useEffect, useMemo, useReducer, useState } from 'react';
4
4
  import { Color } from '@pushwoosh/kit-constants';
5
5
  import { elementAddListener, Horizontal, usePersistentFunction } from '@pushwoosh/kit-helpers';
6
6
  import { WarningRoundIcon } from '@pushwoosh/kit-icons';
@@ -32,10 +32,15 @@ export const DynamicContentPicker = ({
32
32
  error,
33
33
  renderDynamicContent
34
34
  }) => {
35
- const arrowRef = useRef(null);
35
+ const [arrowElement, setArrowElement] = useState(null);
36
36
  const [activeVar, setActiveVar] = useState();
37
37
  const {
38
- refs,
38
+ refs: {
39
+ reference,
40
+ floating,
41
+ setReference,
42
+ setFloating
43
+ },
39
44
  floatingStyles,
40
45
  update,
41
46
  middlewareData,
@@ -45,7 +50,7 @@ export const DynamicContentPicker = ({
45
50
  middleware: [offset(8), autoPlacement({
46
51
  allowedPlacements: ['left', 'left-start', 'left-end', 'right', 'right-start', 'right-end']
47
52
  }), arrow({
48
- element: arrowRef
53
+ element: arrowElement
49
54
  })]
50
55
  });
51
56
  const [state, dispatch] = useReducer(reducer, initState || initialState);
@@ -74,14 +79,14 @@ export const DynamicContentPicker = ({
74
79
  return;
75
80
  }
76
81
  return elementAddListener(document, ['mousedown', 'touchstart', 'focusin'], event => {
77
- if (refs.floating.current instanceof HTMLElement && !refs.floating.current.contains(event.target)) {
82
+ if (floating.current instanceof HTMLElement && !floating.current.contains(event.target)) {
78
83
  clickAwayHandler();
79
84
  }
80
85
  });
81
- }, [clickAwayHandler, activeVar, refs.reference, refs.floating]);
86
+ }, [clickAwayHandler, activeVar, reference, floating]);
82
87
  return _jsxs(_Fragment, {
83
88
  children: [_jsxs(FieldPlace, {
84
- ref: refs.setReference,
89
+ ref: setReference,
85
90
  children: [_jsxs(Horizontal, {
86
91
  "$padding": "8px 16px",
87
92
  "$justifyContent": "space-between",
@@ -121,7 +126,7 @@ export const DynamicContentPicker = ({
121
126
  })
122
127
  })]
123
128
  }), !!activeVar && _jsxs("div", {
124
- ref: refs.setFloating,
129
+ ref: setFloating,
125
130
  style: floatingStyles,
126
131
  children: [renderDynamicContent ? renderDynamicContent({
127
132
  activeVar,
@@ -144,7 +149,7 @@ export const DynamicContentPicker = ({
144
149
  update: update
145
150
  }, activeVar), _jsx(Arrow, {
146
151
  "$direction": placementToDirection(placement),
147
- ref: arrowRef,
152
+ ref: setArrowElement,
148
153
  style: {
149
154
  left: middlewareData.arrow?.x,
150
155
  top: middlewareData.arrow?.y
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from 'react';
2
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
3
3
  import { Button, Chip, Drawer, DrawerBody, DrawerHeader, DrawerTitle, NativeInput, useToast } from '@pushwoosh/dumb-components';
4
4
  import { FlexContainer, useAsync, Vertical } from '@pushwoosh/kit-helpers';
5
5
  import { CloseIcon } from '@pushwoosh/kit-icons';
@@ -40,8 +40,10 @@ export const CanvaImportDrawer = ({
40
40
  const runRef = useRef(0);
41
41
  const queryRef = useRef('');
42
42
  const isOpenRef = useRef(isOpen);
43
- isOpenRef.current = isOpen;
44
- queryRef.current = debouncedQuery;
43
+ useLayoutEffect(() => {
44
+ isOpenRef.current = isOpen;
45
+ queryRef.current = debouncedQuery;
46
+ }, [isOpen, debouncedQuery]);
45
47
  const isReady = status.connected && status.tokenValid;
46
48
  useEffect(() => {
47
49
  const timer = window.setTimeout(() => setDebouncedQuery(query.trim()), SEARCH_DELAY_MS);
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from 'react';
2
+ import { useEffect, useLayoutEffect, useRef, useState } from 'react';
3
3
  import { Button, Checkbox, DropdownMenuButton, DropdownMenuItem, DropdownMenuSection, ModalConfirm, Tab, TabBar, TabPanel, Tabs, useToast } from '@pushwoosh/dumb-components';
4
4
  import { FlexContainer, useAsync } from '@pushwoosh/kit-helpers';
5
5
  import { CheckIcon, EditIcon, OpenIcon, TrashIcon, UploadIcon } from '@pushwoosh/kit-icons';
@@ -63,7 +63,9 @@ export const MediaStorage = ({
63
63
  // `uploadFiles` resolves asynchronously; read the latest page from the ref so
64
64
  // the post-upload decision isn't made on a value captured at click time.
65
65
  const pageRef = useRef(page);
66
- pageRef.current = page;
66
+ useLayoutEffect(() => {
67
+ pageRef.current = page;
68
+ }, [page]);
67
69
  const [cropsByUuid, setCropsByUuid] = useState({});
68
70
  // Bumped to force a reload of the current page when the originals list changes
69
71
  // without the page prop changing (upload, delete of an original).
@@ -138,7 +140,9 @@ export const MediaStorage = ({
138
140
  const total = listAsync.value?.total ?? 0;
139
141
  // Read inside the async delete handler to avoid a stale-closure decision.
140
142
  const filesCountRef = useRef(files.length);
141
- filesCountRef.current = files.length;
143
+ useLayoutEffect(() => {
144
+ filesCountRef.current = files.length;
145
+ }, [files.length]);
142
146
  useEffect(() => {
143
147
  if (listAsync.error) {
144
148
  openToast({
@@ -55,7 +55,7 @@ export function usePagedItems(loadPage, resetKey, enabled = true) {
55
55
  }));
56
56
  requestPage(page);
57
57
  }, [requestPage]);
58
- const resetDeps = [resetKey, enabled, requestPage];
58
+ const resetDeps = [resetKey, enabled, load];
59
59
  const [prevResetDeps, setPrevResetDeps] = useState(null);
60
60
  if (prevResetDeps === null || resetDeps.some((dep, index) => dep !== prevResetDeps[index])) {
61
61
  setPrevResetDeps(resetDeps);
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import { useMemo, useRef } from 'react';
1
+ import { useMemo, useState } from 'react';
2
2
  import { useHistory, useLocation } from 'react-router-dom';
3
3
  import { usePersistentFunction } from '@pushwoosh/kit-helpers';
4
4
  import { mapKeys, shallowEqual } from './helpers';
@@ -13,10 +13,7 @@ export function useDataTableGetParams({
13
13
  const {
14
14
  search: locationSearch
15
15
  } = useLocation();
16
- const customizeParamsRef = useRef(customizeParams);
17
- customizeParamsRef.current = customizeParams;
18
- const prevDataTableParamsRef = useRef({});
19
- const dataTableParams = useMemo(() => {
16
+ const urlParams = useMemo(() => {
20
17
  const searchParams = new URLSearchParams(locationSearch);
21
18
  const searchParamsWoPrefix = mapKeys(Object.fromEntries(searchParams), key => key.startsWith(prefix) ? key.slice(prefix.length) : key);
22
19
  // eslint-disable-next-line no-nested-ternary
@@ -28,13 +25,28 @@ export function useDataTableGetParams({
28
25
  page: +searchParamsWoPrefix.page || 1,
29
26
  perPage: +searchParamsWoPrefix.perPage || defaultPerPage
30
27
  };
28
+ return {
29
+ searchParamsWoPrefix,
30
+ params
31
+ };
32
+ }, [locationSearch, defaultDirection, defaultOrder, defaultPerPage, prefix]);
33
+ const readParams = () => customizeParams ? customizeParams(urlParams.searchParamsWoPrefix, urlParams.params) : urlParams.params;
34
+ const [stableParams, setStableParams] = useState(() => ({
35
+ urlParams,
36
+ params: readParams()
37
+ }));
38
+ let dataTableParams = stableParams.params;
39
+ if (stableParams.urlParams !== urlParams) {
40
+ const resultParams = readParams();
31
41
  // it is necessary to compare with the previous parameters so that there are no extra rerenders
32
- const resultParams = customizeParamsRef.current ? customizeParamsRef.current(searchParamsWoPrefix, params) : params;
33
- if (!shallowEqual(resultParams, prevDataTableParamsRef.current)) {
34
- prevDataTableParamsRef.current = resultParams;
42
+ if (!shallowEqual(resultParams, dataTableParams)) {
43
+ dataTableParams = resultParams;
35
44
  }
36
- return prevDataTableParamsRef.current;
37
- }, [locationSearch, defaultDirection, defaultOrder, defaultPerPage, prefix]);
45
+ setStableParams({
46
+ urlParams,
47
+ params: dataTableParams
48
+ });
49
+ }
38
50
  const changeDataTableParams = usePersistentFunction(params => {
39
51
  const searchParams = new URLSearchParams(locationSearch);
40
52
  const paramsWithPrefix = mapKeys(params, key => `${prefix}${key}`);
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.3",
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",