@vinorcola/dynamic-table 1.2.2 → 1.3.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.
@@ -1,11 +1,11 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { BaseItem, Dictionary, ItemKey, Primitive } from "./index.js";
3
- export type ValueDecorator<Item extends BaseItem, Value extends Primitive> = (value: Value, defaultDisplay: ReactNode, item: Item) => ReactNode;
4
- export type ValueResolver<Item extends BaseItem, Value extends Primitive> = (item: Item) => Value | null;
3
+ export type ValueDecorator<Item extends BaseItem, Value extends Primitive | Primitive[]> = (value: Value, defaultDisplay: ReactNode, item: Item) => ReactNode;
4
+ export type ValueResolver<Item extends BaseItem, Value extends Primitive | Primitive[]> = (item: Item) => Value | null;
5
5
  interface BaseColumneDefinition<Item extends BaseItem, Value extends Primitive> {
6
6
  readonly title: string;
7
7
  readonly dictionary?: Dictionary<Value> | Promise<Dictionary<Value>>;
8
- readonly decorateValue?: ValueDecorator<Item, Value>;
8
+ readonly decorateValue?: ValueDecorator<Item, Value | Value[]>;
9
9
  readonly decorateNoValue?: () => ReactNode;
10
10
  }
11
11
  /**
@@ -19,7 +19,7 @@ interface AccessorColumnDefinition<Item extends BaseItem, Value extends Primitiv
19
19
  */
20
20
  interface ResolvedColumnDefinition<Item extends BaseItem, Value extends Primitive> extends BaseColumneDefinition<Item, Value> {
21
21
  readonly id: string;
22
- readonly resolveValue: ValueResolver<Item, Value>;
22
+ readonly resolveValue: ValueResolver<Item, Value | Value[]>;
23
23
  }
24
24
  export type ColumnDefinition<Item extends BaseItem, Value extends Primitive> = AccessorColumnDefinition<Item, Value> | ResolvedColumnDefinition<Item, Value>;
25
25
  export declare function isAccessorColumnDefinition<Item extends BaseItem, Value extends Primitive>(definition: ColumnDefinition<Item, Value>): definition is AccessorColumnDefinition<Item, Value>;
@@ -0,0 +1,9 @@
1
+ import type { BaseItem } from "./index.js";
2
+ import type { InternalColumns } from "./useColumns.js";
3
+ interface Props<Item extends BaseItem> {
4
+ columns: InternalColumns<Item>;
5
+ clearFilterState: () => void;
6
+ clearSortState: () => void;
7
+ }
8
+ export default function Controller<Item extends BaseItem>(props: Props<Item>): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import cx from "@vinorcola/utils/classNames";
3
+ import { CloseIcon, ColumnsIcon, FilterIcon, SortIcon } from "./Icon.js";
4
+ import { usePopup } from "./UniquePopupProvider.js";
5
+ import DynamicTable from "./index.js";
6
+ import { isSearchable, isSelectable } from "./useFilterState.js";
7
+ import { isMaskable } from "./useMaskableColumns.js";
8
+ import { isSortable } from "./useSortState.js";
9
+ export default function Controller(props) {
10
+ const columnListPopup = usePopup();
11
+ const hasHiddenColumns = props.columns.some((column) => isMaskable(column) && !column.displayed);
12
+ const hasFilteredColumns = props.columns.some((column) => (isSearchable(column) && column.searchText !== null) ||
13
+ (isSelectable(column) && column.hiddenValues.length > 0));
14
+ const hasHiddenFilteredColumns = hasHiddenColumns &&
15
+ hasFilteredColumns &&
16
+ props.columns.some((column) => isMaskable(column) &&
17
+ !column.displayed &&
18
+ ((isSearchable(column) && column.searchText !== null) ||
19
+ (isSelectable(column) && column.hiddenValues.length > 0)));
20
+ const hasSortedColumns = props.columns.some((column) => isSortable(column) && column.sorted !== null);
21
+ const hasHiddenSortedColumns = hasHiddenColumns &&
22
+ hasSortedColumns &&
23
+ props.columns.some((column) => isMaskable(column) && !column.displayed && isSortable(column) && column.sorted !== null);
24
+ return (_jsx(DynamicTable.ControllerContainer, { filterButton: _jsx(DynamicTable.Button, { className: cx(hasHiddenFilteredColumns
25
+ ? "[--bg-color:var(--color-orange-400)]"
26
+ : hasFilteredColumns && "[--bg-color:var(--color-lime-400)]"), disabled: !hasFilteredColumns, hoverChildren: _jsx(CloseIcon, {}), onClick: props.clearFilterState, children: _jsx(FilterIcon, {}) }), sortButton: _jsx(DynamicTable.Button, { className: cx(hasHiddenSortedColumns
27
+ ? "[--bg-color:var(--color-orange-400)]"
28
+ : hasSortedColumns && "[--bg-color:var(--color-sky-400)]"), disabled: !hasSortedColumns, hoverChildren: _jsx(CloseIcon, {}), onClick: props.clearSortState, children: _jsx(SortIcon, {}) }), columnButton: _jsx(DynamicTable.Button, { className: cx(hasHiddenColumns ? "[--bg-color:var(--color-orange-400)]" : "[--bg-default-color:transparent]"), onClick: columnListPopup.show, children: _jsx(ColumnsIcon, {}) }), columnPopup: columnListPopup.display ? (_jsx(DynamicTable.ColumnsPopup, { columns: props.columns, onDismiss: columnListPopup.dismiss })) : undefined }));
29
+ }
@@ -1,4 +1,4 @@
1
- import { type ReactNode } from "react";
1
+ import type { ReactElement, ReactNode } from "react";
2
2
  import type { BaseItem, Dictionary, Primitive } from "./index.js";
3
3
  import type { InternalColumn, InternalColumns } from "./useColumns.js";
4
4
  import { type FilterType } from "./useFilterState.js";
@@ -6,89 +6,90 @@ import { type InternalSortableColumn } from "./useSortState.js";
6
6
  export interface Props {
7
7
  children?: ReactNode;
8
8
  }
9
- export declare function TableContainer(props: Props): import("react/jsx-runtime").JSX.Element;
10
- export interface ControllerProps<Item extends BaseItem> {
11
- columns: InternalColumns<Item>;
12
- clearFilterState: () => void;
13
- clearSortState: () => void;
9
+ export declare function TableContainer(props: Props): import("react").JSX.Element;
10
+ export interface ControllerButtonsGroupProps {
11
+ filterButton: ReactElement;
12
+ sortButton: ReactElement;
13
+ columnButton: ReactElement;
14
+ columnPopup?: ReactElement;
14
15
  }
15
- export declare function Controller<Item extends BaseItem>(props: ControllerProps<Item>): import("react/jsx-runtime").JSX.Element;
16
+ export declare function ControllerContainer(props: ControllerButtonsGroupProps): import("react").JSX.Element;
16
17
  export interface ColumnsPopupProps<Item extends BaseItem> {
17
18
  columns: InternalColumns<Item>;
18
19
  onDismiss: () => void;
19
20
  }
20
- export declare function ColumnsPopup<Item extends BaseItem>(props: ColumnsPopupProps<Item>): import("react/jsx-runtime").JSX.Element;
21
- export declare function Table(props: Props): import("react/jsx-runtime").JSX.Element;
22
- export declare function HeaderContainer(props: Props): import("react/jsx-runtime").JSX.Element;
23
- export declare function HeaderLine(props: Props): import("react/jsx-runtime").JSX.Element;
21
+ export declare function ColumnsPopup<Item extends BaseItem>(props: ColumnsPopupProps<Item>): import("react").JSX.Element;
22
+ export declare function Table(props: Props): import("react").JSX.Element;
23
+ export declare function HeaderContainer(props: Props): import("react").JSX.Element;
24
+ export declare function HeaderLine(props: Props): import("react").JSX.Element;
24
25
  export interface SelectionHeaderProps extends Props {
25
26
  totalSelected: number;
26
27
  unvisibleSelected: number;
27
28
  }
28
- export declare function SelectionHeader(props: SelectionHeaderProps): import("react/jsx-runtime").JSX.Element;
29
+ export declare function SelectionHeader(props: SelectionHeaderProps): import("react").JSX.Element;
29
30
  export interface HeaderProps<Item extends BaseItem, Value extends Primitive> extends Props {
30
31
  column: InternalColumn<Item, Value>;
31
32
  }
32
- export declare function Header<Item extends BaseItem, Value extends Primitive>(props: HeaderProps<Item, Value>): import("react/jsx-runtime").JSX.Element;
33
+ export declare function Header<Item extends BaseItem, Value extends Primitive>(props: HeaderProps<Item, Value>): import("react").JSX.Element;
33
34
  export interface HeaderActionsProps {
34
35
  filterType?: FilterType;
35
36
  onFilterPopupToggle?: () => void;
36
37
  onDisplayToggle?: () => void;
37
38
  }
38
- export declare function HeaderActions(props: HeaderActionsProps): import("react/jsx-runtime").JSX.Element;
39
+ export declare function HeaderActions(props: HeaderActionsProps): import("react").JSX.Element;
39
40
  export interface HeaderStatusProps {
40
41
  filterType?: FilterType;
41
42
  sortState?: NonNullable<InternalSortableColumn<BaseItem, Primitive>["sorted"]>;
42
43
  }
43
- export declare function HeaderStatus(props: HeaderStatusProps): import("react/jsx-runtime").JSX.Element | null;
44
+ export declare function HeaderStatus(props: HeaderStatusProps): import("react").JSX.Element | null;
44
45
  export interface SearchFilterPopupProps {
45
46
  searchText: string;
46
47
  onSearchTextChange: (searchText: string) => void;
47
48
  onDismiss: () => void;
48
49
  }
49
- export declare function SearchFilterPopup(props: SearchFilterPopupProps): import("react/jsx-runtime").JSX.Element;
50
+ export declare function SearchFilterPopup(props: SearchFilterPopupProps): import("react").JSX.Element;
50
51
  export interface SelectionFilterPopupProps<Value extends Primitive> {
51
52
  dictionary: Dictionary<Value> | null;
52
53
  hiddenValues: Value[];
53
54
  onHiddenValuesChange: (hiddenValues: Value[]) => void;
54
55
  onDismiss: () => void;
55
56
  }
56
- export declare function SelectionFilterPopup<Value extends Primitive>(props: SelectionFilterPopupProps<Value>): import("react/jsx-runtime").JSX.Element;
57
- export declare function BodyContainer(props: Props): import("react/jsx-runtime").JSX.Element;
58
- export declare function Line(props: Props): import("react/jsx-runtime").JSX.Element;
57
+ export declare function SelectionFilterPopup<Value extends Primitive>(props: SelectionFilterPopupProps<Value>): import("react").JSX.Element;
58
+ export declare function BodyContainer(props: Props): import("react").JSX.Element;
59
+ export declare function Line(props: Props): import("react").JSX.Element;
59
60
  interface SelectionCellProps extends Props {
60
61
  selected: boolean;
61
62
  onToggle: () => void;
62
63
  }
63
- export declare function SelectionCell(props: SelectionCellProps): import("react/jsx-runtime").JSX.Element;
64
- export declare function Cell(props: Props): import("react/jsx-runtime").JSX.Element;
64
+ export declare function SelectionCell(props: SelectionCellProps): import("react").JSX.Element;
65
+ export declare function Cell(props: Props): import("react").JSX.Element;
65
66
  export interface ClickableProps extends Props {
66
67
  target: string;
67
68
  }
68
- export declare function ClickableCell(props: ClickableProps): import("react/jsx-runtime").JSX.Element;
69
+ export declare function ClickableCell(props: ClickableProps): import("react").JSX.Element;
69
70
  export interface FooterContainerProps {
70
71
  totalColums: number;
71
72
  pageSelector: ReactNode;
72
73
  itemsPerPageSelector: ReactNode;
73
74
  }
74
- export declare function FooterContainer(props: FooterContainerProps): import("react/jsx-runtime").JSX.Element;
75
+ export declare function FooterContainer(props: FooterContainerProps): import("react").JSX.Element;
75
76
  export interface ItemsPerPageSelectorProps extends Props {
76
77
  itemsPerPage: number;
77
78
  onItemsPerPageChange: (itemsPerPage: number) => void;
78
79
  }
79
- export declare function ItemsPerPageSelector(props: ItemsPerPageSelectorProps): import("react/jsx-runtime").JSX.Element;
80
+ export declare function ItemsPerPageSelector(props: ItemsPerPageSelectorProps): import("react").JSX.Element;
80
81
  export interface PageSelectorProps extends Props {
81
82
  totalPages: number;
82
83
  currentPage: number;
83
84
  onCurrentPageChange: (page: number) => void;
84
85
  }
85
- export declare function PageSelector(props: PageSelectorProps): import("react/jsx-runtime").JSX.Element | null;
86
+ export declare function PageSelector(props: PageSelectorProps): import("react").JSX.Element | null;
86
87
  export interface PageButtonProps extends Props {
87
88
  enabled: boolean;
88
89
  active?: boolean;
89
90
  onClick: () => void;
90
91
  }
91
- export declare function PageButton(props: PageButtonProps): import("react/jsx-runtime").JSX.Element;
92
+ export declare function PageButton(props: PageButtonProps): import("react").JSX.Element;
92
93
  export interface ButtonProps extends Props {
93
94
  className?: string;
94
95
  disabled?: boolean;
@@ -111,11 +112,11 @@ export interface ButtonProps extends Props {
111
112
  * background color (when button is non-hovered) by defining a custom `--bg-default-color`, for example:
112
113
  * `<DynamicTable.Button className="[--bg-default-color:transparent]">...</DynamicTable.Button>`
113
114
  */
114
- export declare function Button(props: ButtonProps): import("react/jsx-runtime").JSX.Element;
115
+ export declare function Button(props: ButtonProps): import("react").JSX.Element;
115
116
  export interface PopupProps extends Props {
116
117
  className?: string;
117
118
  onDismiss: () => void;
118
119
  }
119
- export declare function Popup(props: PopupProps): import("react/jsx-runtime").JSX.Element;
120
- export declare function Loader(): import("react/jsx-runtime").JSX.Element;
120
+ export declare function Popup(props: PopupProps): import("react").JSX.Element;
121
+ export declare function Loader(): import("react").JSX.Element;
121
122
  export {};
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import cx from "@vinorcola/utils/classNames";
3
3
  import { useEffect, useMemo } from "react";
4
- import { AscSortIcon, CheckedIcon, CloseIcon, ColumnsIcon, DescSortIcon, FilterIcon, HideIcon, NextIcon, PreviousIcon, SearchIcon, SortIcon, UncheckedIcon, } from "./Icon.js";
4
+ import { AscSortIcon, CheckedIcon, DescSortIcon, FilterIcon, HideIcon, NextIcon, PreviousIcon, SearchIcon, UncheckedIcon, } from "./Icon.js";
5
5
  import { usePopup } from "./UniquePopupProvider.js";
6
6
  import DynamicTable from "./index.js";
7
7
  import { isSearchable, isSelectable } from "./useFilterState.js";
@@ -10,28 +10,8 @@ import { isSortable } from "./useSortState.js";
10
10
  export function TableContainer(props) {
11
11
  return _jsx("div", { children: props.children });
12
12
  }
13
- export function Controller(props) {
14
- const columnListPopup = usePopup();
15
- const hasHiddenColumns = props.columns.some((column) => isMaskable(column) && !column.displayed);
16
- const hasFilteredColumns = props.columns.some((column) => (isSearchable(column) && column.searchText !== null) ||
17
- (isSelectable(column) && column.hiddenValues.length > 0));
18
- const hasHiddenFilteredColumns = hasHiddenColumns &&
19
- hasFilteredColumns &&
20
- props.columns.some((column) => isMaskable(column) &&
21
- !column.displayed &&
22
- ((isSearchable(column) && column.searchText !== null) ||
23
- (isSelectable(column) && column.hiddenValues.length > 0)));
24
- const hasSortedColumns = props.columns.some((column) => isSortable(column) && column.sorted !== null);
25
- const hasHiddenSortedColumns = hasHiddenColumns &&
26
- hasSortedColumns &&
27
- props.columns.some((column) => isMaskable(column) && !column.displayed && isSortable(column) && column.sorted !== null);
28
- return (_jsxs("div", { className: "relative", children: [_jsxs("ul", { className: "flex justify-end", children: [_jsx("li", { children: _jsx(DynamicTable.Button, { className: cx(hasHiddenFilteredColumns
29
- ? "[--bg-color:var(--color-orange-400)]"
30
- : hasFilteredColumns && "[--bg-color:var(--color-lime-400)]"), disabled: !hasFilteredColumns, hoverChildren: _jsx(CloseIcon, {}), onClick: props.clearFilterState, children: _jsx(FilterIcon, {}) }) }), _jsx("li", { children: _jsx(DynamicTable.Button, { className: cx(hasHiddenSortedColumns
31
- ? "[--bg-color:var(--color-orange-400)]"
32
- : hasSortedColumns && "[--bg-color:var(--color-sky-400)]"), disabled: !hasSortedColumns, hoverChildren: _jsx(CloseIcon, {}), onClick: props.clearSortState, children: _jsx(SortIcon, {}) }) }), _jsx("li", { children: _jsx(DynamicTable.Button, { className: cx(hasHiddenColumns
33
- ? "[--bg-color:var(--color-orange-400)]"
34
- : "[--bg-default-color:transparent]"), onClick: columnListPopup.show, children: _jsx(ColumnsIcon, {}) }) })] }), columnListPopup.display && (_jsx(DynamicTable.ColumnsPopup, { columns: props.columns, onDismiss: columnListPopup.dismiss }))] }));
13
+ export function ControllerContainer(props) {
14
+ return (_jsxs("div", { className: "relative", children: [_jsxs("ul", { className: "flex justify-end", children: [_jsx("li", { children: props.filterButton }), _jsx("li", { children: props.sortButton }), _jsx("li", { children: props.columnButton })] }), props.columnPopup] }));
35
15
  }
36
16
  export function ColumnsPopup(props) {
37
17
  return (_jsx(DynamicTable.Popup, { className: "right-2", onDismiss: props.onDismiss, children: _jsx("main", { children: _jsx("ul", { className: "min-w-40", children: props.columns.map((column) => {
package/lib/Icon.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  export declare const DEFAULT_CLASS = "inline grow-0 shrink-0 size-[1em]";
2
- export declare function CheckedIcon(): import("react/jsx-runtime").JSX.Element;
3
- export declare function UncheckedIcon(): import("react/jsx-runtime").JSX.Element;
4
- export declare function CloseIcon(): import("react/jsx-runtime").JSX.Element;
5
- export declare function ColumnsIcon(): import("react/jsx-runtime").JSX.Element;
6
- export declare function FilterIcon(): import("react/jsx-runtime").JSX.Element;
7
- export declare function HideIcon(): import("react/jsx-runtime").JSX.Element;
8
- export declare function NextIcon(): import("react/jsx-runtime").JSX.Element;
9
- export declare function PreviousIcon(): import("react/jsx-runtime").JSX.Element;
10
- export declare function SearchIcon(): import("react/jsx-runtime").JSX.Element;
11
- export declare function SortIcon(): import("react/jsx-runtime").JSX.Element;
12
- export declare function AscSortIcon(): import("react/jsx-runtime").JSX.Element;
13
- export declare function DescSortIcon(): import("react/jsx-runtime").JSX.Element;
2
+ export declare function CheckedIcon(): import("react").JSX.Element;
3
+ export declare function UncheckedIcon(): import("react").JSX.Element;
4
+ export declare function CloseIcon(): import("react").JSX.Element;
5
+ export declare function ColumnsIcon(): import("react").JSX.Element;
6
+ export declare function FilterIcon(): import("react").JSX.Element;
7
+ export declare function HideIcon(): import("react").JSX.Element;
8
+ export declare function NextIcon(): import("react").JSX.Element;
9
+ export declare function PreviousIcon(): import("react").JSX.Element;
10
+ export declare function SearchIcon(): import("react").JSX.Element;
11
+ export declare function SortIcon(): import("react").JSX.Element;
12
+ export declare function AscSortIcon(): import("react").JSX.Element;
13
+ export declare function DescSortIcon(): import("react").JSX.Element;
@@ -18,5 +18,5 @@ export declare function usePopup(): PopupState;
18
18
  interface Props {
19
19
  children?: ReactNode;
20
20
  }
21
- export default function UniquePopupProvider(props: Props): import("react/jsx-runtime").JSX.Element;
21
+ export default function UniquePopupProvider(props: Props): import("react").JSX.Element;
22
22
  export {};
@@ -11,21 +11,25 @@ const PopupContext = createContext(createRef());
11
11
  */
12
12
  // eslint-disable-next-line react-refresh/only-export-components
13
13
  export function usePopup() {
14
- const dismiss = useContext(PopupContext);
14
+ const dismissRef = useContext(PopupContext);
15
15
  const [display, setDisplay] = useState(false);
16
16
  return {
17
17
  display,
18
18
  show: useCallback(() => {
19
- if (dismiss.current !== null) {
20
- dismiss.current();
19
+ if (dismissRef.current !== null) {
20
+ dismissRef.current();
21
21
  }
22
- dismiss.current = () => {
22
+ dismissRef.current = () => {
23
23
  setDisplay(false);
24
- dismiss.current = null;
24
+ dismissRef.current = null;
25
25
  };
26
26
  setDisplay(true);
27
- }, [dismiss]),
28
- dismiss: dismiss.current,
27
+ }, [dismissRef]),
28
+ dismiss: useCallback(() => {
29
+ if (dismissRef.current !== null) {
30
+ dismissRef.current();
31
+ }
32
+ }, [dismissRef]),
29
33
  };
30
34
  }
31
35
  export default function UniquePopupProvider(props) {
@@ -0,0 +1,7 @@
1
+ import type { ReactNode } from "react";
2
+ interface Props {
3
+ separator?: string;
4
+ items: ReactNode[];
5
+ }
6
+ export default function ValueList(props: Props): ReactNode[];
7
+ export {};
@@ -0,0 +1,10 @@
1
+ export default function ValueList(props) {
2
+ const separator = props.separator ?? ", ";
3
+ return props.items.reduce((nodes, item) => {
4
+ if (nodes.length > 0) {
5
+ nodes.push(separator);
6
+ }
7
+ nodes.push(item);
8
+ return nodes;
9
+ }, []);
10
+ }
package/lib/index.d.ts CHANGED
@@ -26,10 +26,10 @@ interface BaseProps<Item extends BaseItem> {
26
26
  initialPaginationState?: PaginationState;
27
27
  }
28
28
  type Props<Item extends BaseItem> = BaseProps<Item> & SelectionOptions<Item>;
29
- declare function DynamicTable<Item extends BaseItem>(props: Props<Item>): import("react/jsx-runtime").JSX.Element;
29
+ declare function DynamicTable<Item extends BaseItem>(props: Props<Item>): import("react").JSX.Element;
30
30
  declare namespace DynamicTable {
31
31
  var TableContainer: typeof import("./DefaultTheme.js").TableContainer;
32
- var Controller: typeof import("./DefaultTheme.js").Controller;
32
+ var ControllerContainer: typeof import("./DefaultTheme.js").ControllerContainer;
33
33
  var ColumnsPopup: typeof import("./DefaultTheme.js").ColumnsPopup;
34
34
  var Table: typeof import("./DefaultTheme.js").Table;
35
35
  var HeaderContainer: typeof import("./DefaultTheme.js").HeaderContainer;
package/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { BodyContainer, Button, Cell, ClickableCell, ColumnsPopup, Controller, FooterContainer, Header, HeaderActions, HeaderContainer, HeaderLine, HeaderStatus, ItemsPerPageSelector, Line, Loader, PageButton, PageSelector, Popup, SearchFilterPopup, SelectionCell, SelectionFilterPopup, SelectionHeader, Table, TableContainer, } from "./DefaultTheme.js";
2
+ import Controller from "./Controller.js";
3
+ import { BodyContainer, Button, Cell, ClickableCell, ColumnsPopup, ControllerContainer, FooterContainer, Header, HeaderActions, HeaderContainer, HeaderLine, HeaderStatus, ItemsPerPageSelector, Line, Loader, PageButton, PageSelector, Popup, SearchFilterPopup, SelectionCell, SelectionFilterPopup, SelectionHeader, Table, TableContainer, } from "./DefaultTheme.js";
3
4
  import UniquePopupProvider from "./UniquePopupProvider.js";
4
5
  import useColumns from "./useColumns.js";
5
6
  import useFilterState from "./useFilterState.js";
@@ -17,13 +18,13 @@ export default function DynamicTable(props) {
17
18
  const { allColumns, columns: displayedColumns, items: displayedItems, } = useMaskableColumns(sortedColumns, sortedItems, props.initialColumnsMaskState);
18
19
  const { items: paginatedItems, itemsPerPage, onItemsPerPageChange, totalPages, currentPage, onCurrentPageChange, } = usePagination(displayedItems, props.initialPaginationState);
19
20
  const { isInSelectionMode, totalSelectedQuantity, unvisibleSelectedQuantity, isItemSelected, onItemSelectionToggle, } = useSelection(props, paginatedItems);
20
- return (_jsx(UniquePopupProvider, { children: _jsxs(DynamicTable.TableContainer, { children: [_jsx(DynamicTable.Controller, { columns: allColumns, clearFilterState: clearFilterState, clearSortState: clearSortState }), _jsxs(DynamicTable.Table, { children: [_jsx(DynamicTable.HeaderContainer, { children: _jsxs(DynamicTable.HeaderLine, { children: [isInSelectionMode && (_jsx(DynamicTable.SelectionHeader, { totalSelected: totalSelectedQuantity, unvisibleSelected: unvisibleSelectedQuantity })), displayedColumns.map((column) => (_jsx(DynamicTable.Header, { column: column, children: column.title }, column.id)))] }) }), _jsx(DynamicTable.BodyContainer, { children: paginatedItems.map((item) => (_jsxs(DynamicTable.Line, { children: [isInSelectionMode &&
21
+ return (_jsx(UniquePopupProvider, { children: _jsxs(DynamicTable.TableContainer, { children: [_jsx(Controller, { columns: allColumns, clearFilterState: clearFilterState, clearSortState: clearSortState }), _jsxs(DynamicTable.Table, { children: [_jsx(DynamicTable.HeaderContainer, { children: _jsxs(DynamicTable.HeaderLine, { children: [isInSelectionMode && (_jsx(DynamicTable.SelectionHeader, { totalSelected: totalSelectedQuantity, unvisibleSelected: unvisibleSelectedQuantity })), displayedColumns.map((column) => (_jsx(DynamicTable.Header, { column: column, children: column.title }, column.id)))] }) }), _jsx(DynamicTable.BodyContainer, { children: paginatedItems.map((item) => (_jsxs(DynamicTable.Line, { children: [isInSelectionMode &&
21
22
  (item.isSelectable ? (_jsx(DynamicTable.SelectionCell, { selected: isItemSelected(item), onToggle: onItemSelectionToggle(item) })) : (_jsx(DynamicTable.Cell, {}))), item.target !== null
22
23
  ? item.values.map((value) => (_jsx(DynamicTable.ClickableCell, { target: item.target, children: value.loading ? _jsx(DynamicTable.Loader, {}) : value.display }, value.column)))
23
24
  : item.values.map((value) => (_jsx(DynamicTable.Cell, { children: value.loading ? _jsx(DynamicTable.Loader, {}) : value.display }, value.column)))] }, item.key))) }), _jsx(DynamicTable.FooterContainer, { totalColums: displayedColumns.length, pageSelector: _jsx(DynamicTable.PageSelector, { totalPages: totalPages, currentPage: currentPage, onCurrentPageChange: onCurrentPageChange }), itemsPerPageSelector: _jsx(DynamicTable.ItemsPerPageSelector, { itemsPerPage: itemsPerPage, onItemsPerPageChange: onItemsPerPageChange }) })] })] }) }));
24
25
  }
25
26
  DynamicTable.TableContainer = TableContainer;
26
- DynamicTable.Controller = Controller;
27
+ DynamicTable.ControllerContainer = ControllerContainer;
27
28
  DynamicTable.ColumnsPopup = ColumnsPopup;
28
29
  DynamicTable.Table = Table;
29
30
  DynamicTable.HeaderContainer = HeaderContainer;
@@ -9,8 +9,8 @@ export interface LoadingInternalColumn<Item extends BaseItem, Value extends Prim
9
9
  readonly title: string;
10
10
  readonly loadingDictionary: true;
11
11
  readonly dictionary: Promise<Dictionary<Value>>;
12
- readonly resolveValue: ValueResolver<Item, Value>;
13
- readonly decorateValue?: ValueDecorator<Item, Value>;
12
+ readonly resolveValue: ValueResolver<Item, Value | Value[]>;
13
+ readonly decorateValue?: ValueDecorator<Item, Value | Value[]>;
14
14
  readonly decorateNoValue?: () => ReactNode;
15
15
  }
16
16
  /**
@@ -21,8 +21,8 @@ export interface LoadedInternalColumn<Item extends BaseItem, Value extends Primi
21
21
  readonly title: string;
22
22
  readonly loadingDictionary: false;
23
23
  readonly dictionary?: Dictionary<Value>;
24
- readonly resolveValue: ValueResolver<Item, Value>;
25
- readonly decorateValue?: ValueDecorator<Item, Value>;
24
+ readonly resolveValue: ValueResolver<Item, Value | Value[]>;
25
+ readonly decorateValue?: ValueDecorator<Item, Value | Value[]>;
26
26
  readonly decorateNoValue?: () => ReactNode;
27
27
  }
28
28
  /**
package/lib/useColumns.js CHANGED
@@ -14,6 +14,7 @@ export default function useColumns(definitions) {
14
14
  useEffect(() => {
15
15
  definitionVersion.current++;
16
16
  const currentVersion = definitionVersion.current;
17
+ // eslint-disable-next-line react-hooks/set-state-in-effect
17
18
  setColumns(resolveInitialColumnState(definitions));
18
19
  definitions.forEach((definition, index) => {
19
20
  if (definition.dictionary instanceof Promise) {
@@ -47,25 +47,26 @@ export default function useFilterState(columns, items, initialFilterState = {})
47
47
  };
48
48
  }
49
49
  }), [columns, filterState]),
50
- items: useMemo(() => items.filter((items) => items.values.every((value) => {
51
- if (value.loading) {
52
- // Keep loading values.
53
- return true;
54
- }
55
- const columnFilterState = filterState[value.column] ?? null;
56
- if (columnFilterState === null) {
57
- // There is no filter on this column.
58
- return true;
59
- }
60
- return isSearchedState(columnFilterState)
61
- ? matchSearch(value, columnFilterState)
62
- : matchSelection(value, columnFilterState);
63
- })), [items, filterState]),
50
+ items: useMemo(() => items.filter((items) => items.values.every((value) => isValueMatchingColumnFilter(filterState, value))), [items, filterState]),
64
51
  clearFilterState: useCallback(() => {
65
52
  setFilterState({});
66
53
  }, []),
67
54
  };
68
55
  }
56
+ function isValueMatchingColumnFilter(filterState, value) {
57
+ if (value.loading) {
58
+ // Keep loading values.
59
+ return true;
60
+ }
61
+ const columnFilterState = filterState[value.column] ?? null;
62
+ if (columnFilterState === null) {
63
+ // There is no filter on this column.
64
+ return true;
65
+ }
66
+ return isSearchedState(columnFilterState)
67
+ ? matchSearch(value, columnFilterState)
68
+ : matchSelection(value, columnFilterState);
69
+ }
69
70
  function isSearchedState(state) {
70
71
  return state.searchText !== undefined;
71
72
  }
@@ -81,5 +82,8 @@ function matchSelection(value, filterState) {
81
82
  // Filter out empty values.
82
83
  return false;
83
84
  }
85
+ if (Array.isArray(value.raw)) {
86
+ return value.raw.some((rawValue) => !filterState.hiddenValues.includes(rawValue));
87
+ }
84
88
  return !filterState.hiddenValues.includes(value.raw);
85
89
  }
package/lib/useItems.d.ts CHANGED
@@ -5,18 +5,18 @@ import type { SortableValue } from "./useSortState.js";
5
5
  /**
6
6
  * A value that is loading (waiting for an async column dictionary to be available).
7
7
  */
8
- export interface LoadingInternalValue {
8
+ export interface LoadingInternalValue<Value extends Primitive | Primitive[] = Primitive | Primitive[]> {
9
9
  readonly column: string;
10
10
  readonly loading: true;
11
- readonly raw: Primitive | null;
11
+ readonly raw: Value | null;
12
12
  }
13
13
  /**
14
14
  * A loaded value, ready for filter, sort and display.
15
15
  */
16
- export interface LoadedInternalValue {
16
+ export interface LoadedInternalValue<Value extends Primitive | Primitive[] = Primitive | Primitive[]> {
17
17
  readonly column: string;
18
18
  readonly loading: false;
19
- readonly raw: Primitive | null;
19
+ readonly raw: Value | null;
20
20
  readonly search: string | null;
21
21
  readonly sort: SortableValue;
22
22
  readonly display: ReactNode;
@@ -24,7 +24,7 @@ export interface LoadedInternalValue {
24
24
  /**
25
25
  * A value, either in loading state ou in loaded state.
26
26
  */
27
- export type InternalValue = LoadingInternalValue | LoadedInternalValue;
27
+ export type InternalValue<Value extends Primitive | Primitive[] = Primitive | Primitive[]> = LoadingInternalValue<Value> | LoadedInternalValue<Value>;
28
28
  /**
29
29
  * An internal item.
30
30
  */
package/lib/useItems.js CHANGED
@@ -1,7 +1,8 @@
1
- import { Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { displayInteger } from "@vinorcola/utils/number";
3
3
  import { extractSearchableText } from "@vinorcola/utils/text";
4
- import { useMemo } from "react";
4
+ import { Fragment, useMemo } from "react";
5
+ import ValueList from "./ValueList.js";
5
6
  export default function useItems(items, itemTarget, columns, canSelectItem) {
6
7
  return useMemo(() => items.map((item) => ({
7
8
  key: item.id,
@@ -12,35 +13,46 @@ export default function useItems(items, itemTarget, columns, canSelectItem) {
12
13
  })), [items, itemTarget, columns, canSelectItem]);
13
14
  }
14
15
  function resolveInternalValue(item, column) {
15
- const raw = column.resolveValue(item);
16
+ const value = column.resolveValue(item);
16
17
  if (column.loadingDictionary) {
17
18
  return {
18
19
  column: column.id,
19
20
  loading: true,
20
- raw,
21
+ raw: value,
21
22
  };
22
23
  }
23
- let display = resolveDisplayableValue(raw, column.dictionary);
24
- if (raw === null || raw === undefined || raw === "") {
24
+ let display;
25
+ if (value === null || value === undefined || value === "") {
25
26
  if (column.decorateNoValue !== undefined) {
26
27
  display = column.decorateNoValue();
27
28
  }
29
+ else {
30
+ display = null;
31
+ }
32
+ }
33
+ else if (column.decorateValue !== undefined) {
34
+ display = column.decorateValue(value, display, item);
28
35
  }
29
36
  else {
30
- if (column.decorateValue !== undefined) {
31
- display = column.decorateValue(raw, display, item);
32
- }
37
+ display = resolveDisplayableValue(value, column.dictionary);
33
38
  }
34
39
  return {
35
40
  column: column.id,
36
41
  loading: false,
37
- raw,
38
- search: resolveSearchableValue(raw, column.dictionary),
39
- sort: resolveSortableValue(raw, column.dictionary),
42
+ raw: value,
43
+ search: resolveSearchableValue(value, column.dictionary),
44
+ sort: resolveSortableValue(value, column.dictionary),
40
45
  display,
41
46
  };
42
47
  }
48
+ const ARBITRARY_SEARCH_SEPARATOR = "#|!$#"; // To avoid searching text across several values, we separate each value by
49
+ // // this arbitrary (unlikely searched by users) text.
43
50
  function resolveSearchableValue(value, dictionary) {
51
+ return Array.isArray(value)
52
+ ? value.map((value) => resolveSearchableSingleValue(value, dictionary)).join(ARBITRARY_SEARCH_SEPARATOR)
53
+ : resolveSearchableSingleValue(value, dictionary);
54
+ }
55
+ function resolveSearchableSingleValue(value, dictionary) {
44
56
  if (dictionary !== undefined) {
45
57
  // Columns with dictionary are selectable, not searchable.
46
58
  return null;
@@ -60,6 +72,13 @@ function resolveSearchableValue(value, dictionary) {
60
72
  return extractSearchableText(value);
61
73
  }
62
74
  function resolveSortableValue(value, dictionary) {
75
+ return Array.isArray(value)
76
+ ? value.length === 0
77
+ ? null
78
+ : resolveSortableSingleValue(value[0], dictionary) // Sort using first value (until we find a better way!)
79
+ : resolveSortableSingleValue(value, dictionary);
80
+ }
81
+ function resolveSortableSingleValue(value, dictionary) {
63
82
  if (value === null) {
64
83
  return null;
65
84
  }
@@ -78,6 +97,9 @@ function resolveSortableValue(value, dictionary) {
78
97
  return value.toLocaleLowerCase();
79
98
  }
80
99
  function resolveDisplayableValue(value, dictionary) {
100
+ return Array.isArray(value) ? (_jsx(ValueList, { items: value.map((value, index) => resolveDisplayableSingleValue(value, dictionary, index)) })) : (resolveDisplayableSingleValue(value, dictionary));
101
+ }
102
+ function resolveDisplayableSingleValue(value, dictionary, nodeKey) {
81
103
  if (value === null) {
82
104
  return null;
83
105
  }
@@ -86,7 +108,7 @@ function resolveDisplayableValue(value, dictionary) {
86
108
  if (dictionaryEntry === undefined) {
87
109
  return dictionary.unknownMessage;
88
110
  }
89
- return dictionaryEntry.prepend !== undefined ? (_jsxs(_Fragment, { children: [dictionaryEntry.prepend, dictionaryEntry.title] })) : (dictionaryEntry.title);
111
+ return dictionaryEntry.prepend !== undefined ? (_jsxs(Fragment, { children: [dictionaryEntry.prepend, dictionaryEntry.title] }, nodeKey)) : (dictionaryEntry.title);
90
112
  }
91
113
  if (typeof value === "boolean") {
92
114
  return value ? "true" : "false";
@@ -26,7 +26,7 @@ export default function useMaskableColumns<Item extends BaseItem>(columns: Inter
26
26
  allColumns: InternalMaskableColumn<Item, Primitive>[];
27
27
  columns: InternalMaskableColumn<Item, Primitive>[];
28
28
  items: {
29
- values: import("./useItems.js").InternalValue[];
29
+ values: import("./useItems.js").InternalValue<Primitive | Primitive[]>[];
30
30
  key: import("react").Key;
31
31
  item: Item;
32
32
  isSelectable: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vinorcola/dynamic-table",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "A table to ease the display of a filterable, sortable and paginable dataset.",
5
5
  "keywords": [
6
6
  "table",