@vinorcola/dynamic-table 1.1.1 → 1.2.1

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,21 +1,25 @@
1
+ import type { ReactNode } from "react";
1
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;
2
4
  export type ValueResolver<Item extends BaseItem, Value extends Primitive> = (item: Item) => Value | null;
3
- interface BaseColumneDefinition<Value extends Primitive> {
5
+ interface BaseColumneDefinition<Item extends BaseItem, Value extends Primitive> {
4
6
  readonly title: string;
5
7
  readonly dictionary?: Dictionary<Value> | Promise<Dictionary<Value>>;
8
+ readonly decorateValue?: ValueDecorator<Item, Value>;
9
+ readonly decorateNoValue?: () => ReactNode;
6
10
  }
7
11
  /**
8
12
  * A column that will access an item's attribute.
9
13
  */
10
- interface AccessorColumnDefinition<Item extends BaseItem, Value extends Primitive> extends BaseColumneDefinition<Value> {
14
+ interface AccessorColumnDefinition<Item extends BaseItem, Value extends Primitive> extends BaseColumneDefinition<Item, Value> {
11
15
  readonly id: ItemKey<Item>;
12
16
  }
13
17
  /**
14
18
  * A column where the value must be resolved.
15
19
  */
16
- interface ResolvedColumnDefinition<Item extends BaseItem, Value extends Primitive> extends BaseColumneDefinition<Value> {
20
+ interface ResolvedColumnDefinition<Item extends BaseItem, Value extends Primitive> extends BaseColumneDefinition<Item, Value> {
17
21
  readonly id: string;
18
- readonly resolveValue: (item: Item) => Value | null;
22
+ readonly resolveValue: ValueResolver<Item, Value>;
19
23
  }
20
24
  export type ColumnDefinition<Item extends BaseItem, Value extends Primitive> = AccessorColumnDefinition<Item, Value> | ResolvedColumnDefinition<Item, Value>;
21
25
  export declare function isAccessorColumnDefinition<Item extends BaseItem, Value extends Primitive>(definition: ColumnDefinition<Item, Value>): definition is AccessorColumnDefinition<Item, Value>;
@@ -62,7 +62,7 @@ export function HeaderLine(props) {
62
62
  return _jsx("tr", { className: "bg-table-header", children: props.children });
63
63
  }
64
64
  export function SelectionHeader(props) {
65
- return (_jsxs("th", { className: "relative", children: [props.totalSelected, _jsxs("span", { className: "text-sm text-stone-500 italic", children: ["(", props.unvisibleSelected, " cach\u00E9", props.unvisibleSelected > 1 ? "s" : "", ")"] })] }));
65
+ return (_jsxs("th", { className: "relative", children: [props.totalSelected, _jsxs("span", { className: "block text-xs text-stone-400 italic", children: ["(", props.unvisibleSelected, " cach\u00E9", props.unvisibleSelected > 1 ? "s" : "", ")"] })] }));
66
66
  }
67
67
  export function Header(props) {
68
68
  const filterPopup = usePopup();
@@ -110,7 +110,7 @@ export function Line(props) {
110
110
  return (_jsx("tr", { className: "even:[&>*]:bg-table-line-even odd:[&>*]:bg-table-line-odd hover:bg-table-line-hover", children: props.children }));
111
111
  }
112
112
  export function SelectionCell(props) {
113
- return (_jsx("td", { onClick: (event) => {
113
+ return (_jsx("td", { className: "cursor-pointer", onClick: (event) => {
114
114
  event.preventDefault();
115
115
  event.stopPropagation();
116
116
  props.onToggle();
@@ -120,14 +120,14 @@ export function Cell(props) {
120
120
  return _jsx("td", { children: props.children });
121
121
  }
122
122
  export function ClickableCell(props) {
123
- return (_jsx("td", { className: "!p-0", children: _jsx("a", { href: props.target, className: "block size-full px-4 py-2", children: props.children }) }));
123
+ return (_jsx("td", { className: "p-0!", children: _jsx("a", { href: props.target, className: "block size-full px-4 py-2", children: props.children }) }));
124
124
  }
125
125
  export function FooterContainer(props) {
126
- return (_jsx("tfoot", { children: _jsx("tr", { children: _jsxs("td", { className: "relative py-0! px-16 h-9 text-center", colSpan: props.totalColums, children: [props.pageSelector, props.itemsPerPageSelector] }) }) }));
126
+ return (_jsx("tfoot", { children: _jsx("tr", { className: "bg-transparent", children: _jsxs("td", { className: "relative py-0! px-16 h-9 text-center", colSpan: props.totalColums, children: [props.pageSelector, props.itemsPerPageSelector] }) }) }));
127
127
  }
128
128
  const ITEM_PER_PAGE_CHOICES = [12, 24, 50, 100, 250, 500];
129
129
  export function ItemsPerPageSelector(props) {
130
- return (_jsx("select", { className: "absolute top-0 right-0 h-full w-14 bg-white outline-none", name: "items-per-page", value: props.itemsPerPage, onChange: (event) => {
130
+ return (_jsx("select", { className: "absolute top-0 right-0 h-full w-14 bg-white outline-none", value: props.itemsPerPage, onChange: (event) => {
131
131
  props.onItemsPerPageChange(Number.parseInt(event.target.value, 10));
132
132
  }, children: ITEM_PER_PAGE_CHOICES.map((choice) => (_jsx("option", { value: choice, children: choice }, choice))) }));
133
133
  }
package/lib/index.js CHANGED
@@ -11,13 +11,14 @@ import useSortState from "./useSortState.js";
11
11
  export { default as Dictionary } from "./Dictionary.js";
12
12
  export default function DynamicTable(props) {
13
13
  const columns = useColumns(props.columns);
14
- const items = useItems(props.items, props.itemTarget, columns);
14
+ const items = useItems(props.items, props.itemTarget, columns, props.canSelectItem);
15
15
  const { columns: filteredColumns, items: filteredItems, clearFilterState, } = useFilterState(columns, items, props.initialFilterState);
16
16
  const { columns: sortedColumns, items: sortedItems, clearSortState, } = useSortState(filteredColumns, filteredItems, props.initialSortState);
17
17
  const { allColumns, columns: displayedColumns, items: displayedItems, } = useMaskableColumns(sortedColumns, sortedItems, props.initialColumnsMaskState);
18
18
  const { items: paginatedItems, itemsPerPage, onItemsPerPageChange, totalPages, currentPage, onCurrentPageChange, } = usePagination(displayedItems, props.initialPaginationState);
19
19
  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 && (_jsx(DynamicTable.SelectionCell, { selected: isItemSelected(item), onToggle: onItemSelectionToggle(item) })), item.target !== null
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
+ (item.isSelectable ? (_jsx(DynamicTable.SelectionCell, { selected: isItemSelected(item), onToggle: onItemSelectionToggle(item) })) : (_jsx(DynamicTable.Cell, {}))), item.target !== null
21
22
  ? item.values.map((value) => (_jsx(DynamicTable.ClickableCell, { target: item.target, children: value.loading ? _jsx(DynamicTable.Loader, {}) : value.display }, value.column)))
22
23
  : 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 }) })] })] }) }));
23
24
  }
@@ -1,3 +1,5 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ValueDecorator } from "./ColumnDefinition.js";
1
3
  import type { BaseItem, ColumnDefinition, Dictionary, Primitive, ValueResolver } from "./index.js";
2
4
  /**
3
5
  * A column that is loading a dictionary.
@@ -8,6 +10,8 @@ export interface LoadingInternalColumn<Item extends BaseItem, Value extends Prim
8
10
  readonly loadingDictionary: true;
9
11
  readonly dictionary: Promise<Dictionary<Value>>;
10
12
  readonly resolveValue: ValueResolver<Item, Value>;
13
+ readonly decorateValue?: ValueDecorator<Item, Value>;
14
+ readonly decorateNoValue?: () => ReactNode;
11
15
  }
12
16
  /**
13
17
  * A column fully loaded and usable.
@@ -18,6 +22,8 @@ export interface LoadedInternalColumn<Item extends BaseItem, Value extends Primi
18
22
  readonly loadingDictionary: false;
19
23
  readonly dictionary?: Dictionary<Value>;
20
24
  readonly resolveValue: ValueResolver<Item, Value>;
25
+ readonly decorateValue?: ValueDecorator<Item, Value>;
26
+ readonly decorateNoValue?: () => ReactNode;
21
27
  }
22
28
  /**
23
29
  * A column, either in loading state or in loaded state.
package/lib/useColumns.js CHANGED
@@ -29,6 +29,8 @@ export default function useColumns(definitions) {
29
29
  resolveValue: isAccessorColumnDefinition(definition)
30
30
  ? (item) => item[definition.id]
31
31
  : definition.resolveValue,
32
+ decorateValue: definition.decorateValue,
33
+ decorateNoValue: definition.decorateNoValue,
32
34
  }));
33
35
  }
34
36
  });
@@ -47,6 +49,8 @@ function resolveInitialColumnState(definitions) {
47
49
  resolveValue: isAccessorColumnDefinition(definition)
48
50
  ? (item) => item[definition.id]
49
51
  : definition.resolveValue,
52
+ decorateValue: definition.decorateValue,
53
+ decorateNoValue: definition.decorateNoValue,
50
54
  }
51
55
  : {
52
56
  id: definition.id,
@@ -56,5 +60,7 @@ function resolveInitialColumnState(definitions) {
56
60
  resolveValue: isAccessorColumnDefinition(definition)
57
61
  ? (item) => item[definition.id]
58
62
  : definition.resolveValue,
63
+ decorateValue: definition.decorateValue,
64
+ decorateNoValue: definition.decorateNoValue,
59
65
  });
60
66
  }
package/lib/useItems.d.ts CHANGED
@@ -31,6 +31,7 @@ export type InternalValue = LoadingInternalValue | LoadedInternalValue;
31
31
  export interface InternalItem<Item extends BaseItem> {
32
32
  readonly key: Key;
33
33
  readonly item: Item;
34
+ readonly isSelectable: boolean;
34
35
  readonly target: string | null;
35
36
  readonly values: InternalValue[];
36
37
  }
@@ -38,4 +39,4 @@ export interface InternalItem<Item extends BaseItem> {
38
39
  * A list of items.
39
40
  */
40
41
  export type InternalItems<Item extends BaseItem> = readonly InternalItem<Item>[];
41
- export default function useItems<Item extends BaseItem>(items: Item[], itemTarget: ((item: Item) => string) | undefined, columns: InternalColumns<Item>): InternalItems<Item>;
42
+ export default function useItems<Item extends BaseItem>(items: Item[], itemTarget: ((item: Item) => string) | undefined, columns: InternalColumns<Item>, canSelectItem?: (item: Item) => boolean): InternalItems<Item>;
package/lib/useItems.js CHANGED
@@ -2,30 +2,43 @@ import { Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { displayInteger } from "@vinorcola/utils/number";
3
3
  import { extractSearchableText } from "@vinorcola/utils/text";
4
4
  import { useMemo } from "react";
5
- export default function useItems(items, itemTarget, columns) {
5
+ export default function useItems(items, itemTarget, columns, canSelectItem) {
6
6
  return useMemo(() => items.map((item) => ({
7
7
  key: item.id,
8
8
  item,
9
+ isSelectable: canSelectItem === undefined ? true : canSelectItem(item),
9
10
  target: itemTarget === undefined ? null : itemTarget(item),
10
11
  values: columns.map((column) => resolveInternalValue(item, column)),
11
- })), [items, itemTarget, columns]);
12
+ })), [items, itemTarget, columns, canSelectItem]);
12
13
  }
13
14
  function resolveInternalValue(item, column) {
14
15
  const raw = column.resolveValue(item);
15
- return column.loadingDictionary
16
- ? {
16
+ if (column.loadingDictionary) {
17
+ return {
17
18
  column: column.id,
18
19
  loading: true,
19
20
  raw,
20
- }
21
- : {
22
- column: column.id,
23
- loading: false,
24
- raw,
25
- search: resolveSearchableValue(raw, column.dictionary),
26
- sort: resolveSortableValue(raw, column.dictionary),
27
- display: resolveDisplayableValue(raw, column.dictionary),
28
21
  };
22
+ }
23
+ let display = resolveDisplayableValue(raw, column.dictionary);
24
+ if (raw === null || raw === undefined || raw === "") {
25
+ if (column.decorateNoValue !== undefined) {
26
+ display = column.decorateNoValue();
27
+ }
28
+ }
29
+ else {
30
+ if (column.decorateValue !== undefined) {
31
+ display = column.decorateValue(raw, display, item);
32
+ }
33
+ }
34
+ return {
35
+ column: column.id,
36
+ loading: false,
37
+ raw,
38
+ search: resolveSearchableValue(raw, column.dictionary),
39
+ sort: resolveSortableValue(raw, column.dictionary),
40
+ display,
41
+ };
29
42
  }
30
43
  function resolveSearchableValue(value, dictionary) {
31
44
  if (dictionary !== undefined) {
@@ -29,6 +29,7 @@ export default function useMaskableColumns<Item extends BaseItem>(columns: Inter
29
29
  values: import("./useItems.js").InternalValue[];
30
30
  key: import("react").Key;
31
31
  item: Item;
32
+ isSelectable: boolean;
32
33
  target: string | null;
33
34
  }[];
34
35
  };
@@ -4,10 +4,12 @@ type ItemSelection<Item extends BaseItem> = Set<Item["id"]>;
4
4
  interface SelectableOptions<Item extends BaseItem> {
5
5
  selectedItems: ItemSelection<Item>;
6
6
  onSelectionChange: (selected: ItemSelection<Item>) => void;
7
+ canSelectItem?: (item: Item) => boolean;
7
8
  }
8
9
  interface NonSelectableOptions {
9
10
  selectedItems?: never;
10
11
  onSelectionChange?: never;
12
+ canSelectItem?: never;
11
13
  }
12
14
  export type SelectionOptions<Item extends BaseItem> = SelectableOptions<Item> | NonSelectableOptions;
13
15
  export default function useSelection<Item extends BaseItem>(options: SelectionOptions<Item>, visibleItems: InternalItems<Item>): {
@@ -3,28 +3,32 @@ function inSelectionMode(props) {
3
3
  return (props.selectedItems !== undefined &&
4
4
  props.onSelectionChange !== undefined);
5
5
  }
6
+ const EMPTY_SELECTION = new Set();
7
+ const DEFAULT_SELECTION_CHANGE_HANDLER = () => { };
6
8
  export default function useSelection(options, visibleItems) {
7
9
  const isInSelectionMode = inSelectionMode(options);
8
- const visibleSelected = useMemo(() => isInSelectionMode
9
- ? visibleItems.reduce((quantity, item) => quantity - (options.selectedItems.has(item.item.id) ? 1 : 0), 0)
10
- : 0, [isInSelectionMode, visibleItems, options.selectedItems]);
10
+ const selectedItems = isInSelectionMode ? options.selectedItems : EMPTY_SELECTION;
11
+ const onSelectionChange = options.onSelectionChange ?? DEFAULT_SELECTION_CHANGE_HANDLER;
12
+ const unvisibleSelectedQuantity = useMemo(() => selectedItems.size > 0
13
+ ? visibleItems.reduce((quantity, item) => quantity - (selectedItems.has(item.item.id) ? 1 : 0), selectedItems.size)
14
+ : 0, [visibleItems, selectedItems]);
11
15
  return {
12
16
  isInSelectionMode,
13
- totalSelectedQuantity: isInSelectionMode ? options.selectedItems.size : 0,
14
- unvisibleSelectedQuantity: isInSelectionMode ? options.selectedItems.size - visibleSelected : 0,
17
+ totalSelectedQuantity: selectedItems.size,
18
+ unvisibleSelectedQuantity,
15
19
  isItemSelected: useCallback((item) => isInSelectionMode && options.selectedItems.has(item.item.id), [isInSelectionMode, options.selectedItems]),
16
20
  onItemSelectionToggle: useCallback((item) => () => {
17
- if (!isInSelectionMode) {
21
+ if (!isInSelectionMode || !item.isSelectable) {
18
22
  return;
19
23
  }
20
- const selection = new Set(options.selectedItems);
24
+ const selection = new Set(selectedItems);
21
25
  if (selection.has(item.item.id)) {
22
26
  selection.delete(item.item.id);
23
27
  }
24
28
  else {
25
29
  selection.add(item.item.id);
26
30
  }
27
- options.onSelectionChange(selection);
28
- }, [isInSelectionMode, options]),
31
+ onSelectionChange(selection);
32
+ }, [isInSelectionMode, selectedItems, onSelectionChange]),
29
33
  };
30
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vinorcola/dynamic-table",
3
- "version": "1.1.1",
3
+ "version": "1.2.1",
4
4
  "description": "A table to ease the display of a filterable, sortable and paginable dataset.",
5
5
  "keywords": [
6
6
  "table",