@dxos/react-ui-table 0.4.10-main.0302e13 → 0.4.10-main.05dbd64
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.
- package/dist/lib/browser/index.mjs +775 -384
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/ColumnMenu/ColumnMenu.d.ts +19 -0
- package/dist/types/src/components/ColumnMenu/ColumnMenu.d.ts.map +1 -0
- package/dist/types/src/components/{ColumnMenu.stories.d.ts → ColumnMenu/ColumnMenu.stories.d.ts} +10 -2
- package/dist/types/src/components/ColumnMenu/ColumnMenu.stories.d.ts.map +1 -0
- package/dist/types/src/components/ColumnMenu/ColumnSettingsForm.d.ts +11 -0
- package/dist/types/src/components/ColumnMenu/ColumnSettingsForm.d.ts.map +1 -0
- package/dist/types/src/components/ColumnMenu/index.d.ts +3 -0
- package/dist/types/src/components/ColumnMenu/index.d.ts.map +1 -0
- package/dist/types/src/components/Table/Table.d.ts +17 -4
- package/dist/types/src/components/Table/Table.d.ts.map +1 -1
- package/dist/types/src/components/Table/Table.stories.d.ts +32 -1
- package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/types/src/components/Table/TableBody.d.ts.map +1 -1
- package/dist/types/src/components/Table/TableRootContext.d.ts +17 -0
- package/dist/types/src/components/Table/TableRootContext.d.ts.map +1 -0
- package/dist/types/src/components/Table/index.d.ts +1 -0
- package/dist/types/src/components/Table/index.d.ts.map +1 -1
- package/dist/types/src/components/Table/props.d.ts +2 -3
- package/dist/types/src/components/Table/props.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/helpers.d.ts +4 -0
- package/dist/types/src/helpers.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +5 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useColumnSizing.d.ts +12 -0
- package/dist/types/src/hooks/useColumnSizing.d.ts.map +1 -0
- package/dist/types/src/hooks/useColumnSorting.d.ts +9 -0
- package/dist/types/src/hooks/useColumnSorting.d.ts.map +1 -0
- package/dist/types/src/hooks/usePinLastRow.d.ts +3 -0
- package/dist/types/src/hooks/usePinLastRow.d.ts.map +1 -0
- package/dist/types/src/hooks/useRowSelection.d.ts +7 -0
- package/dist/types/src/hooks/useRowSelection.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +2 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/schema/SchemaToTable.stories.d.ts +11 -0
- package/dist/types/src/schema/SchemaToTable.stories.d.ts.map +1 -0
- package/dist/types/src/schema/index.d.ts +3 -0
- package/dist/types/src/schema/index.d.ts.map +1 -0
- package/dist/types/src/schema/schema.d.ts +5 -0
- package/dist/types/src/schema/schema.d.ts.map +1 -0
- package/dist/types/src/schema/schema.test.d.ts +2 -0
- package/dist/types/src/schema/schema.test.d.ts.map +1 -0
- package/dist/types/src/schema/schemaToColumns.d.ts +4 -0
- package/dist/types/src/schema/schemaToColumns.d.ts.map +1 -0
- package/dist/types/src/{schema.d.ts → schema/types.d.ts} +1 -1
- package/dist/types/src/schema/types.d.ts.map +1 -0
- package/package.json +22 -16
- package/src/components/ColumnMenu/ColumnMenu.stories.tsx +127 -0
- package/src/components/ColumnMenu/ColumnMenu.tsx +168 -0
- package/src/components/ColumnMenu/ColumnSettingsForm.tsx +214 -0
- package/src/components/ColumnMenu/index.ts +6 -0
- package/src/components/Table/Table.stories.tsx +244 -74
- package/src/components/Table/Table.tsx +79 -96
- package/src/components/Table/TableBody.tsx +6 -1
- package/src/components/Table/TableRootContext.ts +39 -0
- package/src/components/Table/index.ts +1 -0
- package/src/components/Table/props.ts +17 -24
- package/src/components/index.ts +1 -1
- package/src/helpers.tsx +32 -8
- package/src/hooks/index.ts +8 -0
- package/src/hooks/useColumnSizing.ts +45 -0
- package/src/hooks/useColumnSorting.ts +54 -0
- package/src/hooks/usePinLastRow.ts +64 -0
- package/src/hooks/useRowSelection.ts +39 -0
- package/src/index.ts +3 -1
- package/src/schema/SchemaToTable.stories.tsx +71 -0
- package/src/schema/index.ts +6 -0
- package/src/schema/schema.test.ts +148 -0
- package/src/schema/schema.ts +92 -0
- package/src/schema/schemaToColumns.ts +48 -0
- package/src/theme.ts +1 -1
- package/dist/types/src/components/ColumnMenu.d.ts +0 -13
- package/dist/types/src/components/ColumnMenu.d.ts.map +0 -1
- package/dist/types/src/components/ColumnMenu.stories.d.ts.map +0 -1
- package/dist/types/src/components/SortButton.d.ts +0 -8
- package/dist/types/src/components/SortButton.d.ts.map +0 -1
- package/dist/types/src/components/hooks/usePinLastRow.d.ts +0 -3
- package/dist/types/src/components/hooks/usePinLastRow.d.ts.map +0 -1
- package/dist/types/src/schema.d.ts.map +0 -1
- package/src/components/ColumnMenu.stories.tsx +0 -46
- package/src/components/ColumnMenu.tsx +0 -248
- package/src/components/SortButton.tsx +0 -34
- package/src/components/hooks/usePinLastRow.ts +0 -43
- /package/src/{schema.ts → schema/types.ts} +0 -0
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import { useArrowNavigationGroup } from '@fluentui/react-tabster';
|
|
6
6
|
import { flexRender, type Row, type RowData } from '@tanstack/react-table';
|
|
7
|
-
import React from 'react';
|
|
7
|
+
import React, { useContext } from 'react';
|
|
8
8
|
|
|
9
9
|
import { useTableContext } from './TableContext';
|
|
10
|
+
import { TableRootContext } from './TableRootContext';
|
|
10
11
|
import { tbodyTr } from '../../theme';
|
|
11
12
|
import { Cell } from '../Cell/Cell';
|
|
12
13
|
|
|
@@ -23,6 +24,8 @@ const TableBody = ({ rows }: TableBodyProps) => {
|
|
|
23
24
|
const domAttributes = useArrowNavigationGroup({ axis: 'grid' });
|
|
24
25
|
const canBeCurrent = !isGrid && !!onDatumClick;
|
|
25
26
|
|
|
27
|
+
const { virtualizer } = useContext(TableRootContext);
|
|
28
|
+
|
|
26
29
|
return (
|
|
27
30
|
<tbody {...(isGrid && domAttributes)}>
|
|
28
31
|
{rows.map((row) => {
|
|
@@ -37,6 +40,8 @@ const TableBody = ({ rows }: TableBodyProps) => {
|
|
|
37
40
|
<tr
|
|
38
41
|
key={keyAccessor ? keyAccessor(row.original) : row.id}
|
|
39
42
|
className={classNames}
|
|
43
|
+
data-index={row.index}
|
|
44
|
+
ref={virtualizer.measureElement}
|
|
40
45
|
{...(isCurrent && { 'aria-current': 'location' })}
|
|
41
46
|
{...(isSelected && { 'aria-selected': 'true' })}
|
|
42
47
|
{...(canBeCurrent && {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useVirtualizer } from '@tanstack/react-virtual';
|
|
6
|
+
import { createContext, useRef, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
type TableRootContextActions = { type: 'updateTableCount'; count: number };
|
|
9
|
+
|
|
10
|
+
export const useTableRootContext = () => {
|
|
11
|
+
const scrollContextRef = useRef<HTMLDivElement>(null);
|
|
12
|
+
const [tableCount, setTableCount] = useState(0);
|
|
13
|
+
|
|
14
|
+
const virtualizer = useVirtualizer({
|
|
15
|
+
getScrollElement: () => scrollContextRef?.current ?? null,
|
|
16
|
+
count: tableCount,
|
|
17
|
+
overscan: 8,
|
|
18
|
+
estimateSize: () => 40,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const dispatch = (action: TableRootContextActions) => {
|
|
22
|
+
switch (action.type) {
|
|
23
|
+
case 'updateTableCount':
|
|
24
|
+
setTableCount(action.count);
|
|
25
|
+
break;
|
|
26
|
+
|
|
27
|
+
default:
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return { scrollContextRef, virtualizer, dispatch };
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type TableRootContextValue = ReturnType<typeof useTableRootContext>;
|
|
36
|
+
|
|
37
|
+
// Create the context without a default value.
|
|
38
|
+
// The expectation is that the provider will always supply a value, so no need to define a default here.
|
|
39
|
+
export const TableRootContext = createContext<TableRootContextValue>({} as TableRootContextValue);
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type RowData, type RowSelectionState, type Table, type VisibilityState } from '@tanstack/react-table';
|
|
6
|
-
import { type VirtualizerOptions } from '@tanstack/react-virtual';
|
|
7
6
|
|
|
8
7
|
import { type ClassNameValue } from '@dxos/react-ui-types';
|
|
9
8
|
|
|
@@ -31,30 +30,24 @@ export type TableCurrent<TData extends RowData> = Partial<{
|
|
|
31
30
|
onDatumClick: (datum: TData) => void;
|
|
32
31
|
}>;
|
|
33
32
|
|
|
34
|
-
export type TableProps<
|
|
35
|
-
TData extends RowData,
|
|
36
|
-
ScrollElement extends Element | Window = Element,
|
|
37
|
-
ItemElement extends Element = HTMLTableRowElement,
|
|
38
|
-
> = TableFlags &
|
|
33
|
+
export type TableProps<TData extends RowData> = TableFlags &
|
|
39
34
|
TableCurrent<TData> &
|
|
40
|
-
Partial<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
} & Pick<VirtualizerOptions<ScrollElement, ItemElement>, 'getScrollElement'>
|
|
57
|
-
>;
|
|
35
|
+
Partial<{
|
|
36
|
+
keyAccessor: KeyValue<TData>;
|
|
37
|
+
data: TData[];
|
|
38
|
+
columns: TableColumnDef<TData>[];
|
|
39
|
+
onColumnResize: (state: Record<string, number>) => void;
|
|
40
|
+
columnVisibility: VisibilityState;
|
|
41
|
+
// Controllable row selection
|
|
42
|
+
rowSelection: RowSelectionState;
|
|
43
|
+
defaultRowSelection: RowSelectionState;
|
|
44
|
+
onRowSelectionChange: (rowSelection: RowSelectionState) => void;
|
|
45
|
+
// Derived from row selection
|
|
46
|
+
onDataSelectionChange: (dataSelection: TData[]) => void;
|
|
47
|
+
// `table` element props
|
|
48
|
+
classNames: ClassNameValue;
|
|
49
|
+
pinLastRow: boolean;
|
|
50
|
+
}>;
|
|
58
51
|
|
|
59
52
|
export type { RowSelectionState, VisibilityState, TableColumnDef, KeyValue };
|
|
60
53
|
|
package/src/components/index.ts
CHANGED
package/src/helpers.tsx
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from '@tanstack/react-table';
|
|
15
15
|
import { format } from 'date-fns/format';
|
|
16
16
|
import { formatDistanceToNow } from 'date-fns/formatDistanceToNow';
|
|
17
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
17
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
18
18
|
|
|
19
19
|
import { type PublicKey } from '@dxos/keys';
|
|
20
20
|
import { log } from '@dxos/log';
|
|
@@ -102,14 +102,16 @@ const StringBuilderCell = <TData extends RowData>(cellContext: CellContext<TData
|
|
|
102
102
|
|
|
103
103
|
// https://tanstack.com/table/v8/docs/examples/react/editable-data
|
|
104
104
|
const initialValue = cellContext.getValue();
|
|
105
|
-
|
|
105
|
+
|
|
106
|
+
const getInitialValue = useCallback(() => {
|
|
106
107
|
// TODO(burdon): Temporary shim for automerge.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
return (initialValue as any)?.content ?? initialValue;
|
|
109
|
+
}, [initialValue]);
|
|
110
|
+
|
|
111
|
+
const [value, setValue] = useState(getInitialValue);
|
|
110
112
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
// Update value if initialValue changes externally (cell reactivity).
|
|
114
|
+
useEffect(() => setValue(getInitialValue()), [getInitialValue]);
|
|
113
115
|
|
|
114
116
|
const handleSave = () => {
|
|
115
117
|
ref.current && document.activeElement === ref.current && findPrevFocusable(ref.current)?.focus();
|
|
@@ -340,7 +342,7 @@ export class ColumnBuilder<TData extends RowData> {
|
|
|
340
342
|
...props.meta,
|
|
341
343
|
onUpdate,
|
|
342
344
|
digits,
|
|
343
|
-
header: {
|
|
345
|
+
header: { ...props.meta?.header },
|
|
344
346
|
cell: { ...props.meta?.cell, classNames },
|
|
345
347
|
},
|
|
346
348
|
};
|
|
@@ -498,4 +500,26 @@ export class ColumnBuilder<TData extends RowData> {
|
|
|
498
500
|
...props,
|
|
499
501
|
};
|
|
500
502
|
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* This display column type is for nested/unhandled data to appear as JSON.
|
|
506
|
+
*/
|
|
507
|
+
display({ label, classNames, ...props }: KeyColumnOptions<TData> = {}): Partial<ColumnDef<TData, PublicKey>> {
|
|
508
|
+
return {
|
|
509
|
+
size: 120,
|
|
510
|
+
minSize: 120,
|
|
511
|
+
header: (cell) => label ?? cell.header.id,
|
|
512
|
+
cell: (cell) => {
|
|
513
|
+
const value = cell.getValue();
|
|
514
|
+
|
|
515
|
+
return (
|
|
516
|
+
<div className='overflow-scroll'>
|
|
517
|
+
<code className='text-xs'>{JSON.stringify(value)}</code>
|
|
518
|
+
</div>
|
|
519
|
+
);
|
|
520
|
+
},
|
|
521
|
+
...props,
|
|
522
|
+
meta: { ...props.meta, cell: { ...props.meta?.cell, classNames: ['font-mono', textPadding, classNames] } },
|
|
523
|
+
};
|
|
524
|
+
}
|
|
501
525
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
import { type ColumnSizingInfoState, type ColumnSizingState } from '@tanstack/react-table';
|
|
5
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { useOnTransition } from '@dxos/react-ui';
|
|
8
|
+
|
|
9
|
+
import { type TableColumnDef } from '../components/Table';
|
|
10
|
+
|
|
11
|
+
export const useColumnResizing = ({
|
|
12
|
+
columns,
|
|
13
|
+
onColumnResize,
|
|
14
|
+
}: {
|
|
15
|
+
columns: TableColumnDef<any>[];
|
|
16
|
+
onColumnResize: ((state: Record<string, number>) => void) | undefined;
|
|
17
|
+
}) => {
|
|
18
|
+
const [columnsInitialised, setColumnsInitialised] = useState(false);
|
|
19
|
+
const [columnSizing, setColumnSizing] = useState<ColumnSizingState>({});
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (columnsInitialised) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setColumnSizing(
|
|
27
|
+
columns
|
|
28
|
+
.filter((column) => !!column.size && (column as any).prop !== undefined)
|
|
29
|
+
.reduce<ColumnSizingState>((state, column) => {
|
|
30
|
+
state[(column as any).prop] = column.size!;
|
|
31
|
+
return state;
|
|
32
|
+
}, {}),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
setColumnsInitialised(true);
|
|
36
|
+
}, [columns, setColumnSizing]);
|
|
37
|
+
|
|
38
|
+
const [columnSizingInfo, setColumnSizingInfo] = useState<ColumnSizingInfoState>({} as ColumnSizingInfoState);
|
|
39
|
+
|
|
40
|
+
// Notify on column resize.
|
|
41
|
+
const notifyColumnResize = useCallback(() => onColumnResize?.(columnSizing), [onColumnResize, columnSizing]);
|
|
42
|
+
useOnTransition(columnSizingInfo.isResizingColumn, (v) => typeof v === 'string', false, notifyColumnResize);
|
|
43
|
+
|
|
44
|
+
return { columnSizing, columnSizingInfo, setColumnSizing, setColumnSizingInfo };
|
|
45
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type SortDirection, type Column } from '@tanstack/react-table';
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
|
|
8
|
+
export const useColumnSorting = (column: Column<any>) => {
|
|
9
|
+
const [sortDirection, setSortDirection] = useState<SortDirection | false>(column.getIsSorted());
|
|
10
|
+
const [firstSort, setFirstSort] = useState<SortDirection | undefined>();
|
|
11
|
+
|
|
12
|
+
const canSort = column.getCanSort();
|
|
13
|
+
|
|
14
|
+
const updateColumnSort = (direction: SortDirection | false) => {
|
|
15
|
+
setSortDirection(direction);
|
|
16
|
+
|
|
17
|
+
switch (direction) {
|
|
18
|
+
case false:
|
|
19
|
+
column.clearSorting();
|
|
20
|
+
break;
|
|
21
|
+
case 'asc':
|
|
22
|
+
column.toggleSorting(false); // descending false
|
|
23
|
+
break;
|
|
24
|
+
case 'desc':
|
|
25
|
+
column.toggleSorting(true); // descending true
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Whichever sort is explicitly set first, will be the first sort direction
|
|
31
|
+
// When toggling, we always go to the other direction, and then to unsorted.
|
|
32
|
+
const onToggleSort = () => {
|
|
33
|
+
if (sortDirection === false) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (sortDirection === firstSort) {
|
|
38
|
+
updateColumnSort(firstSort === 'asc' ? 'desc' : 'asc');
|
|
39
|
+
} else {
|
|
40
|
+
updateColumnSort(false);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const onSelectSort = (direction: SortDirection) => {
|
|
45
|
+
setFirstSort(direction);
|
|
46
|
+
updateColumnSort(direction);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const onClearSort = () => {
|
|
50
|
+
updateColumnSort(false);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return { canSort, sortDirection, onSelectSort, onToggleSort, onClearSort };
|
|
54
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Row, type Table } from '@tanstack/react-table';
|
|
6
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { useTableRootContext } from '../components';
|
|
9
|
+
|
|
10
|
+
export const usePinLastRow = (pinLastRow: boolean | undefined, table: Table<any>, data: any[]) => {
|
|
11
|
+
const { scrollContextRef } = useTableRootContext();
|
|
12
|
+
const [pinnedRowKey, setPinnedRowKey] = useState<string>();
|
|
13
|
+
|
|
14
|
+
// Scrolls to the bottom of a scrollable element.
|
|
15
|
+
const scrollToBottom = useCallback(() => {
|
|
16
|
+
const element = scrollContextRef.current;
|
|
17
|
+
const scrollHeight = element?.scrollHeight;
|
|
18
|
+
|
|
19
|
+
if (element && scrollHeight) {
|
|
20
|
+
requestAnimationFrame(() => element.scrollTo({ top: scrollHeight }));
|
|
21
|
+
}
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
const pinRow = useCallback(
|
|
25
|
+
(rowToPin: Row<any>) => {
|
|
26
|
+
table.resetRowPinning();
|
|
27
|
+
rowToPin.pin('bottom');
|
|
28
|
+
setPinnedRowKey(rowToPin.id);
|
|
29
|
+
},
|
|
30
|
+
[table],
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const clearRowPinning = useCallback(() => {
|
|
34
|
+
table.resetRowPinning();
|
|
35
|
+
setPinnedRowKey(undefined);
|
|
36
|
+
}, [table]);
|
|
37
|
+
|
|
38
|
+
// Define the logic for pinning the last row in a callback.
|
|
39
|
+
const pinLastRowCallback = useCallback(() => {
|
|
40
|
+
const rows = table.getRowModel().rows;
|
|
41
|
+
|
|
42
|
+
if (rows.length === 0) {
|
|
43
|
+
return clearRowPinning();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const lastRow = rows[rows.length - 1];
|
|
47
|
+
if (lastRow.id === pinnedRowKey) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pinRow(lastRow);
|
|
52
|
+
|
|
53
|
+
if (pinnedRowKey !== undefined) {
|
|
54
|
+
scrollToBottom();
|
|
55
|
+
}
|
|
56
|
+
}, [table, scrollToBottom, pinnedRowKey]);
|
|
57
|
+
|
|
58
|
+
// Effect to handle pinning logic when data changes.
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (pinLastRow) {
|
|
61
|
+
pinLastRowCallback();
|
|
62
|
+
}
|
|
63
|
+
}, [data, pinLastRow, pinLastRowCallback]);
|
|
64
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
import { useControllableState } from '@radix-ui/react-use-controllable-state';
|
|
5
|
+
import { type OnChangeFn, type RowSelectionState } from '@tanstack/react-table';
|
|
6
|
+
import { useCallback } from 'react';
|
|
7
|
+
|
|
8
|
+
import { type TableProps } from '../components/Table';
|
|
9
|
+
|
|
10
|
+
export const useRowSelection = (
|
|
11
|
+
props: Pick<
|
|
12
|
+
TableProps<any>,
|
|
13
|
+
'rowsSelectable' | 'defaultRowSelection' | 'rowSelection' | 'onRowSelectionChange' | 'rowsSelectable'
|
|
14
|
+
>,
|
|
15
|
+
) => {
|
|
16
|
+
const [rowSelection = {}, setRowSelection] = useControllableState({
|
|
17
|
+
prop: props.rowSelection,
|
|
18
|
+
onChange: props.onRowSelectionChange,
|
|
19
|
+
defaultProp: props.defaultRowSelection,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// TODO(thure): Does @tanstack/react-table really need this intervention? It did seem necessary to enforce single-selection...
|
|
23
|
+
const handleRowSelectionChange = useCallback<OnChangeFn<RowSelectionState>>(
|
|
24
|
+
(updaterOrValue) => {
|
|
25
|
+
const nextRowSelection = typeof updaterOrValue === 'function' ? updaterOrValue(rowSelection) : updaterOrValue;
|
|
26
|
+
if (props.rowsSelectable === 'multi') {
|
|
27
|
+
setRowSelection(nextRowSelection);
|
|
28
|
+
} else if (props.rowsSelectable) {
|
|
29
|
+
const nextRowSelectionKey = Object.keys(nextRowSelection).filter((id) => !rowSelection[id])[0];
|
|
30
|
+
setRowSelection(nextRowSelectionKey ? { [nextRowSelectionKey]: true } : {});
|
|
31
|
+
} else {
|
|
32
|
+
setRowSelection({});
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
[props.rowsSelectable, setRowSelection, rowSelection],
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return { rowSelection, handleRowSelectionChange };
|
|
39
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
// Copyright 2022 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
+
export { default as translations } from './translations';
|
|
6
|
+
|
|
5
7
|
export * from './components';
|
|
6
8
|
export * from './helpers';
|
|
9
|
+
export * from './hooks';
|
|
7
10
|
export * from './schema';
|
|
8
11
|
export * from './theme';
|
|
9
|
-
export { default as translations } from './translations';
|
|
10
12
|
export * from './types';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxosTheme';
|
|
6
|
+
|
|
7
|
+
import * as Arbitrary from '@effect/schema/Arbitrary';
|
|
8
|
+
import * as fc from 'fast-check';
|
|
9
|
+
import React from 'react';
|
|
10
|
+
|
|
11
|
+
import { S } from '@dxos/echo-schema';
|
|
12
|
+
import { DensityProvider } from '@dxos/react-ui';
|
|
13
|
+
import { withTheme } from '@dxos/storybook-utils';
|
|
14
|
+
|
|
15
|
+
import { schemaToColumnDefs } from './schemaToColumns';
|
|
16
|
+
import { Table } from '../components';
|
|
17
|
+
|
|
18
|
+
export default {
|
|
19
|
+
title: 'react-ui-table/SchemaTable',
|
|
20
|
+
args: {},
|
|
21
|
+
decorators: [
|
|
22
|
+
withTheme,
|
|
23
|
+
(Story: any) => (
|
|
24
|
+
<DensityProvider density='fine'>
|
|
25
|
+
<Story />
|
|
26
|
+
</DensityProvider>
|
|
27
|
+
),
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const exampleSchema = S.struct({
|
|
32
|
+
field1: S.string,
|
|
33
|
+
field2: S.number,
|
|
34
|
+
field3: S.Date,
|
|
35
|
+
field4: S.optional(S.string.pipe(S.length(10))),
|
|
36
|
+
field5: S.optional(
|
|
37
|
+
S.struct({
|
|
38
|
+
innerField1: S.string,
|
|
39
|
+
innerField2: S.number,
|
|
40
|
+
}),
|
|
41
|
+
),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
type ExampleSchema = S.Schema.Type<typeof exampleSchema>;
|
|
45
|
+
|
|
46
|
+
const columns = schemaToColumnDefs(exampleSchema);
|
|
47
|
+
|
|
48
|
+
const exampleSchemaArbitrary = Arbitrary.make(exampleSchema)(fc);
|
|
49
|
+
|
|
50
|
+
const items = fc.sample(exampleSchemaArbitrary, 10);
|
|
51
|
+
|
|
52
|
+
export const SchemaTable = {
|
|
53
|
+
render: () => {
|
|
54
|
+
return (
|
|
55
|
+
<Table.Root>
|
|
56
|
+
<Table.Viewport classNames='inset-0 fixed'>
|
|
57
|
+
<Table.Table<ExampleSchema>
|
|
58
|
+
role='grid'
|
|
59
|
+
rowsSelectable='multi'
|
|
60
|
+
keyAccessor={(row) => JSON.stringify(row)}
|
|
61
|
+
columns={columns}
|
|
62
|
+
data={items}
|
|
63
|
+
fullWidth
|
|
64
|
+
stickyHeader
|
|
65
|
+
border
|
|
66
|
+
/>
|
|
67
|
+
</Table.Viewport>
|
|
68
|
+
</Table.Root>
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { expect } from 'chai';
|
|
6
|
+
|
|
7
|
+
import { S } from '@dxos/echo-schema';
|
|
8
|
+
import { describe, test } from '@dxos/test';
|
|
9
|
+
|
|
10
|
+
import { classifySchemaProperties } from './schema';
|
|
11
|
+
|
|
12
|
+
describe('schema->column-type', () => {
|
|
13
|
+
test('basic', () => {
|
|
14
|
+
const simpleSchema = S.struct({
|
|
15
|
+
field1: S.string,
|
|
16
|
+
field2: S.number,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const columns = classifySchemaProperties(simpleSchema);
|
|
20
|
+
|
|
21
|
+
expect(columns).to.deep.equal([
|
|
22
|
+
['field1', 'string'],
|
|
23
|
+
['field2', 'number'],
|
|
24
|
+
]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('optional properties', () => {
|
|
28
|
+
const simpleSchema = S.struct({
|
|
29
|
+
field1: S.optional(S.string),
|
|
30
|
+
field2: S.optional(S.number),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const columns = classifySchemaProperties(simpleSchema);
|
|
34
|
+
|
|
35
|
+
expect(columns).to.deep.equal([
|
|
36
|
+
['field1', 'string'],
|
|
37
|
+
['field2', 'number'],
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('dates', () => {
|
|
42
|
+
const simpleSchema = S.struct({
|
|
43
|
+
field1: S.Date,
|
|
44
|
+
field2: S.optional(S.Date),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const columns = classifySchemaProperties(simpleSchema);
|
|
48
|
+
|
|
49
|
+
expect(columns).to.deep.equal([
|
|
50
|
+
['field1', 'date'],
|
|
51
|
+
['field2', 'date'],
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('Piped validators', () => {
|
|
56
|
+
const simpleSchema = S.struct({
|
|
57
|
+
name: S.optional(S.string.pipe(S.nonEmpty(), S.maxLength(10))),
|
|
58
|
+
age: S.number.pipe(S.negative()),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const columns = classifySchemaProperties(simpleSchema);
|
|
62
|
+
|
|
63
|
+
expect(columns).to.deep.equal([
|
|
64
|
+
['name', 'string'],
|
|
65
|
+
['age', 'number'],
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('nested schema', () => {
|
|
70
|
+
const simpleSchema = S.struct({
|
|
71
|
+
name: S.string,
|
|
72
|
+
nested: S.struct({
|
|
73
|
+
age: S.number,
|
|
74
|
+
}),
|
|
75
|
+
someUnion: S.union(S.string, S.number),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const columns = classifySchemaProperties(simpleSchema);
|
|
79
|
+
|
|
80
|
+
expect(columns).to.deep.equal([
|
|
81
|
+
['name', 'string'],
|
|
82
|
+
['nested.age', 'number'],
|
|
83
|
+
['someUnion', 'display'],
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('deeply nested schema', () => {
|
|
88
|
+
const simpleSchema = S.struct({
|
|
89
|
+
name: S.string,
|
|
90
|
+
nested: S.struct({
|
|
91
|
+
age: S.number,
|
|
92
|
+
deeplyNested: S.struct({
|
|
93
|
+
height: S.number,
|
|
94
|
+
}),
|
|
95
|
+
}),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const columns = classifySchemaProperties(simpleSchema);
|
|
99
|
+
|
|
100
|
+
expect(columns).to.deep.equal([
|
|
101
|
+
['name', 'string'],
|
|
102
|
+
['nested.age', 'number'],
|
|
103
|
+
['nested.deeplyNested.height', 'number'],
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('even more deeply nested schema with optionals', () => {
|
|
108
|
+
const simpleSchema = S.struct({
|
|
109
|
+
name: S.string,
|
|
110
|
+
nested: S.struct({
|
|
111
|
+
age: S.number,
|
|
112
|
+
deeplyNested: S.optional(
|
|
113
|
+
S.struct({
|
|
114
|
+
height: S.optional(S.number),
|
|
115
|
+
}),
|
|
116
|
+
),
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const columns = classifySchemaProperties(simpleSchema);
|
|
121
|
+
|
|
122
|
+
expect(columns).to.deep.equal([
|
|
123
|
+
['name', 'string'],
|
|
124
|
+
['nested.age', 'number'],
|
|
125
|
+
['nested.deeplyNested.height', 'number'],
|
|
126
|
+
]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('multiple-nested schemata as sibblings', () => {
|
|
130
|
+
const simpleSchema = S.struct({
|
|
131
|
+
name: S.string,
|
|
132
|
+
nested: S.struct({
|
|
133
|
+
age: S.number,
|
|
134
|
+
}),
|
|
135
|
+
nested2: S.struct({
|
|
136
|
+
height: S.number,
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const columns = classifySchemaProperties(simpleSchema);
|
|
141
|
+
|
|
142
|
+
expect(columns).to.deep.equal([
|
|
143
|
+
['name', 'string'],
|
|
144
|
+
['nested.age', 'number'],
|
|
145
|
+
['nested2.height', 'number'],
|
|
146
|
+
]);
|
|
147
|
+
});
|
|
148
|
+
});
|