@vinorcola/dynamic-table 1.2.3 → 1.4.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>;
@@ -5,5 +5,5 @@ interface Props<Item extends BaseItem> {
5
5
  clearFilterState: () => void;
6
6
  clearSortState: () => void;
7
7
  }
8
- export default function Controller<Item extends BaseItem>(props: Props<Item>): import("react/jsx-runtime").JSX.Element;
8
+ export default function Controller<Item extends BaseItem>(props: Props<Item>): import("react").JSX.Element;
9
9
  export {};
@@ -6,90 +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;
9
+ export declare function TableContainer(props: Props): import("react").JSX.Element;
10
10
  export interface ControllerButtonsGroupProps {
11
11
  filterButton: ReactElement;
12
12
  sortButton: ReactElement;
13
13
  columnButton: ReactElement;
14
14
  columnPopup?: ReactElement;
15
15
  }
16
- export declare function ControllerContainer(props: ControllerButtonsGroupProps): import("react/jsx-runtime").JSX.Element;
16
+ export declare function ControllerContainer(props: ControllerButtonsGroupProps): import("react").JSX.Element;
17
17
  export interface ColumnsPopupProps<Item extends BaseItem> {
18
18
  columns: InternalColumns<Item>;
19
19
  onDismiss: () => void;
20
20
  }
21
- export declare function ColumnsPopup<Item extends BaseItem>(props: ColumnsPopupProps<Item>): import("react/jsx-runtime").JSX.Element;
22
- export declare function Table(props: Props): import("react/jsx-runtime").JSX.Element;
23
- export declare function HeaderContainer(props: Props): import("react/jsx-runtime").JSX.Element;
24
- 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;
25
25
  export interface SelectionHeaderProps extends Props {
26
26
  totalSelected: number;
27
27
  unvisibleSelected: number;
28
28
  }
29
- export declare function SelectionHeader(props: SelectionHeaderProps): import("react/jsx-runtime").JSX.Element;
29
+ export declare function SelectionHeader(props: SelectionHeaderProps): import("react").JSX.Element;
30
30
  export interface HeaderProps<Item extends BaseItem, Value extends Primitive> extends Props {
31
31
  column: InternalColumn<Item, Value>;
32
32
  }
33
- 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;
34
34
  export interface HeaderActionsProps {
35
35
  filterType?: FilterType;
36
36
  onFilterPopupToggle?: () => void;
37
37
  onDisplayToggle?: () => void;
38
38
  }
39
- export declare function HeaderActions(props: HeaderActionsProps): import("react/jsx-runtime").JSX.Element;
39
+ export declare function HeaderActions(props: HeaderActionsProps): import("react").JSX.Element;
40
40
  export interface HeaderStatusProps {
41
41
  filterType?: FilterType;
42
42
  sortState?: NonNullable<InternalSortableColumn<BaseItem, Primitive>["sorted"]>;
43
43
  }
44
- 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;
45
45
  export interface SearchFilterPopupProps {
46
46
  searchText: string;
47
47
  onSearchTextChange: (searchText: string) => void;
48
48
  onDismiss: () => void;
49
49
  }
50
- export declare function SearchFilterPopup(props: SearchFilterPopupProps): import("react/jsx-runtime").JSX.Element;
50
+ export declare function SearchFilterPopup(props: SearchFilterPopupProps): import("react").JSX.Element;
51
51
  export interface SelectionFilterPopupProps<Value extends Primitive> {
52
52
  dictionary: Dictionary<Value> | null;
53
53
  hiddenValues: Value[];
54
54
  onHiddenValuesChange: (hiddenValues: Value[]) => void;
55
55
  onDismiss: () => void;
56
56
  }
57
- export declare function SelectionFilterPopup<Value extends Primitive>(props: SelectionFilterPopupProps<Value>): import("react/jsx-runtime").JSX.Element;
58
- export declare function BodyContainer(props: Props): import("react/jsx-runtime").JSX.Element;
59
- 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;
60
60
  interface SelectionCellProps extends Props {
61
61
  selected: boolean;
62
62
  onToggle: () => void;
63
63
  }
64
- export declare function SelectionCell(props: SelectionCellProps): import("react/jsx-runtime").JSX.Element;
65
- 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;
66
66
  export interface ClickableProps extends Props {
67
67
  target: string;
68
68
  }
69
- export declare function ClickableCell(props: ClickableProps): import("react/jsx-runtime").JSX.Element;
69
+ export declare function ClickableCell(props: ClickableProps): import("react").JSX.Element;
70
70
  export interface FooterContainerProps {
71
71
  totalColums: number;
72
72
  pageSelector: ReactNode;
73
73
  itemsPerPageSelector: ReactNode;
74
74
  }
75
- export declare function FooterContainer(props: FooterContainerProps): import("react/jsx-runtime").JSX.Element;
75
+ export declare function FooterContainer(props: FooterContainerProps): import("react").JSX.Element;
76
76
  export interface ItemsPerPageSelectorProps extends Props {
77
77
  itemsPerPage: number;
78
78
  onItemsPerPageChange: (itemsPerPage: number) => void;
79
79
  }
80
- export declare function ItemsPerPageSelector(props: ItemsPerPageSelectorProps): import("react/jsx-runtime").JSX.Element;
80
+ export declare function ItemsPerPageSelector(props: ItemsPerPageSelectorProps): import("react").JSX.Element;
81
81
  export interface PageSelectorProps extends Props {
82
82
  totalPages: number;
83
83
  currentPage: number;
84
84
  onCurrentPageChange: (page: number) => void;
85
85
  }
86
- 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;
87
87
  export interface PageButtonProps extends Props {
88
88
  enabled: boolean;
89
89
  active?: boolean;
90
90
  onClick: () => void;
91
91
  }
92
- export declare function PageButton(props: PageButtonProps): import("react/jsx-runtime").JSX.Element;
92
+ export declare function PageButton(props: PageButtonProps): import("react").JSX.Element;
93
93
  export interface ButtonProps extends Props {
94
94
  className?: string;
95
95
  disabled?: boolean;
@@ -112,11 +112,11 @@ export interface ButtonProps extends Props {
112
112
  * background color (when button is non-hovered) by defining a custom `--bg-default-color`, for example:
113
113
  * `<DynamicTable.Button className="[--bg-default-color:transparent]">...</DynamicTable.Button>`
114
114
  */
115
- export declare function Button(props: ButtonProps): import("react/jsx-runtime").JSX.Element;
115
+ export declare function Button(props: ButtonProps): import("react").JSX.Element;
116
116
  export interface PopupProps extends Props {
117
117
  className?: string;
118
118
  onDismiss: () => void;
119
119
  }
120
- export declare function Popup(props: PopupProps): import("react/jsx-runtime").JSX.Element;
121
- 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;
122
122
  export {};
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;
@@ -0,0 +1,5 @@
1
+ import type { StateWatcherInterface } from ".";
2
+ /**
3
+ * Generates a watcher that will save states in browser's LocalStorage.
4
+ */
5
+ export default function generateLocalStorageWatcher(namespace: string): StateWatcherInterface;
@@ -0,0 +1,38 @@
1
+ function save(key, value) {
2
+ localStorage.setItem(key, JSON.stringify(value));
3
+ }
4
+ function load(key) {
5
+ return new Promise((resolve, reject) => {
6
+ try {
7
+ const storedValue = localStorage.getItem(key);
8
+ if (storedValue === null) {
9
+ return resolve(null);
10
+ }
11
+ return resolve(JSON.parse(storedValue));
12
+ }
13
+ catch (error) {
14
+ reject(error);
15
+ }
16
+ });
17
+ }
18
+ function watch(key) {
19
+ return {
20
+ onChange(filterState) {
21
+ save(key, filterState);
22
+ },
23
+ loadInitial() {
24
+ return load(key);
25
+ },
26
+ };
27
+ }
28
+ /**
29
+ * Generates a watcher that will save states in browser's LocalStorage.
30
+ */
31
+ export default function generateLocalStorageWatcher(namespace) {
32
+ return {
33
+ filterState: watch(`${namespace}-filter-state`),
34
+ sortState: watch(`${namespace}-sort-state`),
35
+ columnsMaskState: watch(`${namespace}-columns-mask-state`),
36
+ paginationState: watch(`${namespace}-pagination-state`),
37
+ };
38
+ }
@@ -0,0 +1,6 @@
1
+ import type { StateWatcherInterface } from ".";
2
+ /**
3
+ * This watcher does nothing.
4
+ */
5
+ declare const NoopWatcher: StateWatcherInterface;
6
+ export default NoopWatcher;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * This watcher does nothing.
3
+ */
4
+ const NoopWatcher = {};
5
+ export default NoopWatcher;
@@ -0,0 +1,14 @@
1
+ import type { FilterState } from "../useFilterState";
2
+ import type { ColumnsMaskState } from "../useMaskableColumns";
3
+ import type { PaginationState } from "../usePagination";
4
+ import type { SortState } from "../useSortState";
5
+ export interface StateWatcherInterface {
6
+ filterState?: SpecificStateWatcher<FilterState>;
7
+ sortState?: SpecificStateWatcher<SortState>;
8
+ columnsMaskState?: SpecificStateWatcher<ColumnsMaskState>;
9
+ paginationState?: SpecificStateWatcher<PaginationState>;
10
+ }
11
+ export interface SpecificStateWatcher<State> {
12
+ onChange?(state: State): void;
13
+ loadInitial?(): Promise<State | null>;
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -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
@@ -1,5 +1,6 @@
1
- import type { Key as ReactKey } from "react";
1
+ import { type Key as ReactKey } from "react";
2
2
  import type { ColumnDefinition } from "./ColumnDefinition.js";
3
+ import type { StateWatcherInterface } from "./StateWatcher/index.js";
3
4
  import { type FilterState } from "./useFilterState.js";
4
5
  import type { ColumnsMaskState } from "./useMaskableColumns.js";
5
6
  import { type PaginationState } from "./usePagination.js";
@@ -12,11 +13,14 @@ export type BaseItem = Record<string, any> & {
12
13
  export type ItemKey<Item extends BaseItem> = Extract<keyof Item, string>;
13
14
  export type { ColumnDefinition, ValueResolver } from "./ColumnDefinition.js";
14
15
  export { default as Dictionary, DictionaryEntry } from "./Dictionary.js";
16
+ export type { StateWatcherInterface } from "./StateWatcher/index.js";
17
+ export { default as LocalStorageWatcher } from "./StateWatcher/LocalStorageWatcher.js";
15
18
  export type { FilterState } from "./useFilterState.js";
16
19
  export type { SortDirection, SortState } from "./useSortState.js";
17
20
  export type { ColumnsMaskState } from "./useMaskableColumns.js";
18
21
  export type { PaginationState } from "./usePagination.js";
19
22
  interface BaseProps<Item extends BaseItem> {
23
+ namespace?: string;
20
24
  items: Item[];
21
25
  columns: ColumnDefinition<Item, Primitive>[];
22
26
  itemTarget?: (item: Item) => string;
@@ -24,9 +28,10 @@ interface BaseProps<Item extends BaseItem> {
24
28
  initialSortState?: SortState;
25
29
  initialColumnsMaskState?: ColumnsMaskState;
26
30
  initialPaginationState?: PaginationState;
31
+ watcher?: StateWatcherInterface;
27
32
  }
28
33
  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;
34
+ declare function DynamicTable<Item extends BaseItem>(props: Props<Item>): import("react").JSX.Element;
30
35
  declare namespace DynamicTable {
31
36
  var TableContainer: typeof import("./DefaultTheme.js").TableContainer;
32
37
  var ControllerContainer: typeof import("./DefaultTheme.js").ControllerContainer;
package/lib/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo } from "react";
2
3
  import Controller from "./Controller.js";
3
4
  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";
5
+ import generateLocalStorageWatcher from "./StateWatcher/LocalStorageWatcher.js";
6
+ import NoopWatcher from "./StateWatcher/NoopWatcher.js";
4
7
  import UniquePopupProvider from "./UniquePopupProvider.js";
5
8
  import useColumns from "./useColumns.js";
6
9
  import useFilterState from "./useFilterState.js";
@@ -10,13 +13,16 @@ import usePagination from "./usePagination.js";
10
13
  import useSelection from "./useSelection.js";
11
14
  import useSortState from "./useSortState.js";
12
15
  export { default as Dictionary } from "./Dictionary.js";
16
+ export { default as LocalStorageWatcher } from "./StateWatcher/LocalStorageWatcher.js";
13
17
  export default function DynamicTable(props) {
14
18
  const columns = useColumns(props.columns);
15
19
  const items = useItems(props.items, props.itemTarget, columns, props.canSelectItem);
16
- const { columns: filteredColumns, items: filteredItems, clearFilterState, } = useFilterState(columns, items, props.initialFilterState);
17
- const { columns: sortedColumns, items: sortedItems, clearSortState, } = useSortState(filteredColumns, filteredItems, props.initialSortState);
18
- const { allColumns, columns: displayedColumns, items: displayedItems, } = useMaskableColumns(sortedColumns, sortedItems, props.initialColumnsMaskState);
19
- const { items: paginatedItems, itemsPerPage, onItemsPerPageChange, totalPages, currentPage, onCurrentPageChange, } = usePagination(displayedItems, props.initialPaginationState);
20
+ const watcher = useMemo(() => props.watcher ??
21
+ (props.namespace === undefined ? NoopWatcher : generateLocalStorageWatcher(props.namespace)), [props.namespace, props.watcher]);
22
+ const { columns: filteredColumns, items: filteredItems, clearFilterState, } = useFilterState(columns, items, props.initialFilterState, watcher.filterState);
23
+ const { columns: sortedColumns, items: sortedItems, clearSortState, } = useSortState(filteredColumns, filteredItems, props.initialSortState, watcher.sortState);
24
+ const { allColumns, columns: displayedColumns, items: displayedItems, } = useMaskableColumns(sortedColumns, sortedItems, props.initialColumnsMaskState, watcher.columnsMaskState);
25
+ const { items: paginatedItems, itemsPerPage, onItemsPerPageChange, totalPages, currentPage, onCurrentPageChange, } = usePagination(displayedItems, props.initialPaginationState, watcher.paginationState);
20
26
  const { isInSelectionMode, totalSelectedQuantity, unvisibleSelectedQuantity, isItemSelected, onItemSelectionToggle, } = useSelection(props, paginatedItems);
21
27
  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 &&
22
28
  (item.isSelectable ? (_jsx(DynamicTable.SelectionCell, { selected: isItemSelected(item), onToggle: onItemSelectionToggle(item) })) : (_jsx(DynamicTable.Cell, {}))), item.target !== null
@@ -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) {
@@ -1,3 +1,4 @@
1
+ import type { SpecificStateWatcher } from "./StateWatcher/index.js";
1
2
  import type { BaseItem, Primitive } from "./index.js";
2
3
  import type { InternalColumn, InternalColumns } from "./useColumns.js";
3
4
  import type { InternalItems } from "./useItems.js";
@@ -54,7 +55,7 @@ export declare function isSelectable<Item extends BaseItem, Value extends Primit
54
55
  * This hook will add filter state & filter control on each given columns, returning decorated columns. It will also
55
56
  * apply those filters on the items list, returning a filtered list.
56
57
  */
57
- export default function useFilterState<Item extends BaseItem>(columns: InternalColumns<Item>, items: InternalItems<Item>, initialFilterState?: FilterState): {
58
+ export default function useFilterState<Item extends BaseItem>(columns: InternalColumns<Item>, items: InternalItems<Item>, initialState: FilterState | undefined, watcher: SpecificStateWatcher<FilterState> | undefined): {
58
59
  columns: InternalFilterableColumn<Item, Primitive>[];
59
60
  items: import("./useItems.js").InternalItem<Item>[];
60
61
  clearFilterState: () => void;
@@ -1,6 +1,7 @@
1
1
  import { drop } from "@vinorcola/utils/object";
2
2
  import { extractSearchableText } from "@vinorcola/utils/text";
3
- import { useCallback, useMemo, useState } from "react";
3
+ import { useCallback, useMemo } from "react";
4
+ import useWatchedState from "./useWatchedState.js";
4
5
  export function isFilterable(column) {
5
6
  return ((column.searchText !== undefined && column.onSearchTextChange !== undefined) ||
6
7
  (column.hiddenValues !== undefined && column.onSelectionChange !== undefined));
@@ -17,8 +18,8 @@ export function isSelectable(column) {
17
18
  * This hook will add filter state & filter control on each given columns, returning decorated columns. It will also
18
19
  * apply those filters on the items list, returning a filtered list.
19
20
  */
20
- export default function useFilterState(columns, items, initialFilterState = {}) {
21
- const [filterState, setFilterState] = useState(initialFilterState);
21
+ export default function useFilterState(columns, items, initialState, watcher) {
22
+ const [filterState, setFilterState] = useWatchedState({}, initialState, watcher);
22
23
  return {
23
24
  columns: useMemo(() => columns.map((column) => {
24
25
  if (column.dictionary === undefined) {
@@ -47,25 +48,26 @@ export default function useFilterState(columns, items, initialFilterState = {})
47
48
  };
48
49
  }
49
50
  }), [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]),
51
+ items: useMemo(() => items.filter((items) => items.values.every((value) => isValueMatchingColumnFilter(filterState, value))), [items, filterState]),
64
52
  clearFilterState: useCallback(() => {
65
53
  setFilterState({});
66
54
  }, []),
67
55
  };
68
56
  }
57
+ function isValueMatchingColumnFilter(filterState, value) {
58
+ if (value.loading) {
59
+ // Keep loading values.
60
+ return true;
61
+ }
62
+ const columnFilterState = filterState[value.column] ?? null;
63
+ if (columnFilterState === null) {
64
+ // There is no filter on this column.
65
+ return true;
66
+ }
67
+ return isSearchedState(columnFilterState)
68
+ ? matchSearch(value, columnFilterState)
69
+ : matchSelection(value, columnFilterState);
70
+ }
69
71
  function isSearchedState(state) {
70
72
  return state.searchText !== undefined;
71
73
  }
@@ -81,5 +83,8 @@ function matchSelection(value, filterState) {
81
83
  // Filter out empty values.
82
84
  return false;
83
85
  }
86
+ if (Array.isArray(value.raw)) {
87
+ return value.raw.some((rawValue) => !filterState.hiddenValues.includes(rawValue));
88
+ }
84
89
  return !filterState.hiddenValues.includes(value.raw);
85
90
  }
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";
@@ -0,0 +1 @@
1
+ export default function useLazyInitializedState<State>(defaultState: State, initial: State | undefined, lazyInitializer: (() => Promise<State | null>) | undefined): [State, import("react").Dispatch<import("react").SetStateAction<State>>];
@@ -0,0 +1,16 @@
1
+ import { useEffect, useState } from "react";
2
+ export default function useLazyInitializedState(defaultState, initial, lazyInitializer) {
3
+ const state = useState(initial ?? defaultState);
4
+ useEffect(() => {
5
+ if (initial !== undefined) {
6
+ return;
7
+ }
8
+ lazyInitializer?.().then((initialState) => {
9
+ if (initialState === null) {
10
+ return;
11
+ }
12
+ state[1](initialState);
13
+ });
14
+ }, []); // eslint-disable-line react-hooks-configurable/exhaustive-deps
15
+ return state;
16
+ }
@@ -1,3 +1,4 @@
1
+ import type { SpecificStateWatcher } from "./StateWatcher/index.js";
1
2
  import type { BaseItem, Primitive } from "./index.js";
2
3
  import type { InternalColumn } from "./useColumns.js";
3
4
  import type { InternalItems } from "./useItems.js";
@@ -22,11 +23,11 @@ export declare function isMaskable<Item extends BaseItem, Value extends Primitiv
22
23
  * Note that the returned `columns` contains only the displayed columns, while `allColumns` contains all the columns
23
24
  * (for control purpose).
24
25
  */
25
- export default function useMaskableColumns<Item extends BaseItem>(columns: InternalColumn<Item, Primitive>[], items: InternalItems<Item>, initialHiddenColumns?: ColumnsMaskState): {
26
+ export default function useMaskableColumns<Item extends BaseItem>(columns: InternalColumn<Item, Primitive>[], items: InternalItems<Item>, initialState: ColumnsMaskState | undefined, watcher: SpecificStateWatcher<ColumnsMaskState> | undefined): {
26
27
  allColumns: InternalMaskableColumn<Item, Primitive>[];
27
28
  columns: InternalMaskableColumn<Item, Primitive>[];
28
29
  items: {
29
- values: import("./useItems.js").InternalValue[];
30
+ values: import("./useItems.js").InternalValue<Primitive | Primitive[]>[];
30
31
  key: import("react").Key;
31
32
  item: Item;
32
33
  isSelectable: boolean;
@@ -1,4 +1,5 @@
1
- import { useMemo, useState } from "react";
1
+ import { useMemo } from "react";
2
+ import useWatchedState from "./useWatchedState.js";
2
3
  export function isMaskable(column) {
3
4
  return column.displayed !== undefined && column.onDisplayToggle !== undefined;
4
5
  }
@@ -11,8 +12,8 @@ export function isMaskable(column) {
11
12
  * Note that the returned `columns` contains only the displayed columns, while `allColumns` contains all the columns
12
13
  * (for control purpose).
13
14
  */
14
- export default function useMaskableColumns(columns, items, initialHiddenColumns = []) {
15
- const [hidden, setHidden] = useState(initialHiddenColumns);
15
+ export default function useMaskableColumns(columns, items, initialState, watcher) {
16
+ const [hidden, setHidden] = useWatchedState([], initialState, watcher);
16
17
  const allColumns = useMemo(() => columns.map((column) => {
17
18
  const displayed = !hidden.includes(column.id);
18
19
  return {
@@ -1,3 +1,4 @@
1
+ import type { SpecificStateWatcher } from "./StateWatcher/index.js";
1
2
  import type { BaseItem } from "./index.js";
2
3
  import type { InternalItems } from "./useItems.js";
3
4
  /**
@@ -12,7 +13,7 @@ export interface PaginationState {
12
13
  *
13
14
  * Return pagination state & pagination control as well as items to display on the current page.
14
15
  */
15
- export default function usePagination<Item extends BaseItem>(items: InternalItems<Item>, initialPaginationState?: PaginationState): {
16
+ export default function usePagination<Item extends BaseItem>(items: InternalItems<Item>, initialState: PaginationState | undefined, watcher: SpecificStateWatcher<PaginationState> | undefined): {
16
17
  items: import("./useItems.js").InternalItem<Item>[];
17
18
  itemsPerPage: number;
18
19
  onItemsPerPageChange: (itemsPerPage: number) => void;
@@ -1,11 +1,12 @@
1
- import { useCallback, useMemo, useState } from "react";
1
+ import { useCallback, useMemo } from "react";
2
+ import useWatchedState from "./useWatchedState.js";
2
3
  /**
3
4
  * Apply pagination.
4
5
  *
5
6
  * Return pagination state & pagination control as well as items to display on the current page.
6
7
  */
7
- export default function usePagination(items, initialPaginationState = { currentPage: 1, itemsPerPage: 12 }) {
8
- const [pagination, setPagination] = useState(initialPaginationState);
8
+ export default function usePagination(items, initialState, watcher) {
9
+ const [pagination, setPagination] = useWatchedState({ currentPage: 1, itemsPerPage: 12 }, initialState, watcher);
9
10
  const totalPages = useMemo(() => Math.ceil(items.length / pagination.itemsPerPage), [items, pagination]);
10
11
  const currentPage = useMemo(() => Math.max(1, Math.min(totalPages, pagination.currentPage)), [totalPages, pagination]);
11
12
  return {
@@ -1,3 +1,4 @@
1
+ import type { SpecificStateWatcher } from "./StateWatcher/index.js";
1
2
  import type { BaseItem, Primitive } from "./index.js";
2
3
  import type { InternalColumn, InternalColumns } from "./useColumns.js";
3
4
  import type { InternalItems, InternalItem } from "./useItems.js";
@@ -34,7 +35,7 @@ export declare function isSortable<Item extends BaseItem, Value extends Primitiv
34
35
  * This hook will add sort state & sort control on each given columns, returning decorated columns. It will also apply
35
36
  * those sorts on the items list, returning a sorted list.
36
37
  */
37
- export default function useSortState<Item extends BaseItem>(columns: InternalColumns<Item>, items: InternalItems<Item>, initialSortState?: SortState): {
38
+ export default function useSortState<Item extends BaseItem>(columns: InternalColumns<Item>, items: InternalItems<Item>, initialState: SortState | undefined, watcher: SpecificStateWatcher<SortState> | undefined): {
38
39
  columns: InternalSortableColumn<Item, Primitive>[];
39
40
  items: InternalItem<Item>[];
40
41
  clearSortState: () => void;
@@ -1,5 +1,6 @@
1
1
  import { dropElement, replaceElement } from "@vinorcola/utils/list";
2
- import { useCallback, useMemo, useState } from "react";
2
+ import { useCallback, useMemo } from "react";
3
+ import useWatchedState from "./useWatchedState.js";
3
4
  export function isSortable(column) {
4
5
  return column.sorted !== undefined && column.onSortToggle !== undefined;
5
6
  }
@@ -9,8 +10,8 @@ export function isSortable(column) {
9
10
  * This hook will add sort state & sort control on each given columns, returning decorated columns. It will also apply
10
11
  * those sorts on the items list, returning a sorted list.
11
12
  */
12
- export default function useSortState(columns, items, initialSortState = []) {
13
- const [sortState, setSortState] = useState(initialSortState);
13
+ export default function useSortState(columns, items, initialState, watcher) {
14
+ const [sortState, setSortState] = useWatchedState([], initialState, watcher);
14
15
  return {
15
16
  columns: useMemo(() => columns.map((column) => {
16
17
  const columnSortStateIndex = sortState.findIndex((columnSortState) => columnSortState.columnId === column.id);
@@ -0,0 +1,3 @@
1
+ import type { Dispatch, SetStateAction } from "react";
2
+ import type { SpecificStateWatcher } from "./StateWatcher";
3
+ export default function useWatchedState<State>(defaultState: State, initial: State | undefined, watcher: SpecificStateWatcher<State> | undefined): readonly [State, Dispatch<SetStateAction<State>>];
@@ -0,0 +1,25 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ export default function useWatchedState(defaultState, initial, watcher) {
3
+ const [state, setState] = useState(initial ?? defaultState);
4
+ useEffect(() => {
5
+ if (initial !== undefined) {
6
+ return;
7
+ }
8
+ watcher?.loadInitial?.().then((initialState) => {
9
+ if (initialState === null) {
10
+ return;
11
+ }
12
+ setState(initialState);
13
+ });
14
+ }, []); // eslint-disable-line react-hooks-configurable/exhaustive-deps
15
+ return [
16
+ state,
17
+ useCallback((updater) => {
18
+ setState((state) => {
19
+ const newState = typeof updater === "function" ? updater(state) : updater;
20
+ watcher?.onChange?.(newState);
21
+ return newState;
22
+ });
23
+ }, []), // eslint-disable-line react-hooks-configurable/exhaustive-deps
24
+ ];
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vinorcola/dynamic-table",
3
- "version": "1.2.3",
3
+ "version": "1.4.0",
4
4
  "description": "A table to ease the display of a filterable, sortable and paginable dataset.",
5
5
  "keywords": [
6
6
  "table",
@@ -44,6 +44,7 @@
44
44
  "devDependencies": {
45
45
  "@types/react": "^19.2.2",
46
46
  "@vinorcola/lint": "^1.0.5",
47
+ "eslint-plugin-react-hooks-configurable": "^7.1.1",
47
48
  "typescript": "^5.9.3"
48
49
  },
49
50
  "peerDependencies": {