@dxos/react-ui-table 0.3.7-main.e08407b → 0.3.7-main.ec7f1fc
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 +123 -77
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/types/src/components/ComboboxCell.d.ts +12 -0
- package/dist/types/src/components/ComboboxCell.d.ts.map +1 -0
- package/dist/types/src/components/Table.stories.d.ts +4 -3
- package/dist/types/src/components/Table.stories.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +1 -0
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/helpers.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +10 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +1 -1
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +10 -9
- package/src/components/ComboboxCell.tsx +97 -0
- package/src/components/Table.stories.tsx +62 -64
- package/src/components/index.ts +1 -0
- package/src/helpers.tsx +6 -49
- package/src/translations.ts +15 -0
- package/src/types.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-table",
|
|
3
|
-
"version": "0.3.7-main.
|
|
3
|
+
"version": "0.3.7-main.ec7f1fc",
|
|
4
4
|
"description": "A table component.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
@@ -20,13 +20,14 @@
|
|
|
20
20
|
"@tanstack/react-table": "^8.9.3",
|
|
21
21
|
"date-fns": "^2.29.3",
|
|
22
22
|
"lodash.defaultsdeep": "^4.6.1",
|
|
23
|
-
"@dxos/
|
|
24
|
-
"@dxos/
|
|
25
|
-
"@dxos/
|
|
26
|
-
"@dxos/react-ui": "0.3.7-main.
|
|
27
|
-
"@dxos/react-ui
|
|
28
|
-
"@dxos/react-ui-
|
|
29
|
-
"@dxos/
|
|
23
|
+
"@dxos/invariant": "0.3.7-main.ec7f1fc",
|
|
24
|
+
"@dxos/async": "0.3.7-main.ec7f1fc",
|
|
25
|
+
"@dxos/keys": "0.3.7-main.ec7f1fc",
|
|
26
|
+
"@dxos/react-ui-searchlist": "0.3.7-main.ec7f1fc",
|
|
27
|
+
"@dxos/react-ui": "0.3.7-main.ec7f1fc",
|
|
28
|
+
"@dxos/react-ui-types": "0.3.7-main.ec7f1fc",
|
|
29
|
+
"@dxos/react-ui-theme": "0.3.7-main.ec7f1fc",
|
|
30
|
+
"@dxos/util": "0.3.7-main.ec7f1fc"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@faker-js/faker": "^8.0.2",
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
"react-dom": "^18.2.0",
|
|
41
42
|
"vite": "^4.3.9",
|
|
42
43
|
"vite-plugin-turbosnap": "^1.0.2",
|
|
43
|
-
"@dxos/react-ui-theme": "0.3.7-main.
|
|
44
|
+
"@dxos/react-ui-theme": "0.3.7-main.ec7f1fc"
|
|
44
45
|
},
|
|
45
46
|
"peerDependencies": {
|
|
46
47
|
"@phosphor-icons/react": "^2.0.5",
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { CaretDown } from '@phosphor-icons/react';
|
|
6
|
+
import { type RowData } from '@tanstack/react-table';
|
|
7
|
+
import React, { useCallback, useState } from 'react';
|
|
8
|
+
|
|
9
|
+
import { Popover, useTranslation } from '@dxos/react-ui';
|
|
10
|
+
import { PopoverCombobox } from '@dxos/react-ui-searchlist';
|
|
11
|
+
import { descriptionText, getSize, mx, staticPlaceholderText } from '@dxos/react-ui-theme';
|
|
12
|
+
|
|
13
|
+
import { type SelectQueryModel } from '../helpers';
|
|
14
|
+
import { translationKey } from '../translations';
|
|
15
|
+
|
|
16
|
+
export type ComboboxCellItems<TData extends RowData> = Record<string, { label: string; data: TData }>;
|
|
17
|
+
|
|
18
|
+
const initialItems = <TData extends RowData>(model: SelectQueryModel<TData>, value?: TData) =>
|
|
19
|
+
value
|
|
20
|
+
? {
|
|
21
|
+
[model.getId(value)]: { label: model.getText(value), data: value },
|
|
22
|
+
}
|
|
23
|
+
: {};
|
|
24
|
+
|
|
25
|
+
export const ComboboxCell = <TData extends RowData>({
|
|
26
|
+
model,
|
|
27
|
+
value,
|
|
28
|
+
onValueChange,
|
|
29
|
+
}: {
|
|
30
|
+
model: SelectQueryModel<TData>;
|
|
31
|
+
value: TData;
|
|
32
|
+
onValueChange: (value: TData) => void;
|
|
33
|
+
}) => {
|
|
34
|
+
const { t } = useTranslation(translationKey);
|
|
35
|
+
const [items, setItems] = useState<ComboboxCellItems<TData>>(initialItems(model, value));
|
|
36
|
+
const [loading, setLoading] = useState(false);
|
|
37
|
+
const [searchInputValue, setSearchInputValue] = useState('');
|
|
38
|
+
const handleSearchInputValueChange = async (text?: string) => {
|
|
39
|
+
setSearchInputValue(text ?? '');
|
|
40
|
+
setLoading(true);
|
|
41
|
+
const items = await model.query(text);
|
|
42
|
+
setItems(
|
|
43
|
+
items.reduce((acc: ComboboxCellItems<TData>, item) => {
|
|
44
|
+
acc[model.getId(item)] = {
|
|
45
|
+
label: model.getText(item),
|
|
46
|
+
data: item,
|
|
47
|
+
};
|
|
48
|
+
return acc;
|
|
49
|
+
}, initialItems(model, value)),
|
|
50
|
+
);
|
|
51
|
+
setLoading(false);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleValueChange = useCallback(
|
|
55
|
+
(id: string) => {
|
|
56
|
+
onValueChange(items[id]?.data);
|
|
57
|
+
},
|
|
58
|
+
[items, onValueChange],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<PopoverCombobox.Root placeholder={t('no selection label')}>
|
|
63
|
+
<PopoverCombobox.Trigger variant='ghost' classNames='is-full'>
|
|
64
|
+
{value ? (
|
|
65
|
+
<>
|
|
66
|
+
<span
|
|
67
|
+
className={mx('font-normal text-start flex-1 min-is-0 truncate mie-2', !value && staticPlaceholderText)}
|
|
68
|
+
>
|
|
69
|
+
{model.getText(value)}
|
|
70
|
+
</span>
|
|
71
|
+
<CaretDown weight='bold' className={getSize(3)} />
|
|
72
|
+
</>
|
|
73
|
+
) : null}
|
|
74
|
+
</PopoverCombobox.Trigger>
|
|
75
|
+
<PopoverCombobox.Content shouldFilter={false} classNames='is-[--radix-popover-trigger-width]'>
|
|
76
|
+
<PopoverCombobox.Input value={searchInputValue} onValueChange={handleSearchInputValueChange} />
|
|
77
|
+
<PopoverCombobox.List>
|
|
78
|
+
{searchInputValue.length < 1 ? (
|
|
79
|
+
<p className={mx('text-center', descriptionText)}>Start typing to search</p>
|
|
80
|
+
) : loading ? (
|
|
81
|
+
<p className={mx('text-center', descriptionText)}>Searching…</p>
|
|
82
|
+
) : (
|
|
83
|
+
<>
|
|
84
|
+
<PopoverCombobox.Empty classNames={['text-center', descriptionText]}>No results</PopoverCombobox.Empty>
|
|
85
|
+
{Object.entries(items).map(([id, { label }]) => (
|
|
86
|
+
<PopoverCombobox.Item key={id} value={id} onSelect={handleValueChange} classNames='truncate'>
|
|
87
|
+
{label}
|
|
88
|
+
</PopoverCombobox.Item>
|
|
89
|
+
))}
|
|
90
|
+
</>
|
|
91
|
+
)}
|
|
92
|
+
</PopoverCombobox.List>
|
|
93
|
+
<Popover.Arrow />
|
|
94
|
+
</PopoverCombobox.Content>
|
|
95
|
+
</PopoverCombobox.Root>
|
|
96
|
+
);
|
|
97
|
+
};
|
|
@@ -14,7 +14,7 @@ import { DensityProvider } from '@dxos/react-ui';
|
|
|
14
14
|
import { range } from '@dxos/util';
|
|
15
15
|
|
|
16
16
|
import { Table } from './Table';
|
|
17
|
-
import { createColumnBuilder, type ValueUpdater } from '../helpers';
|
|
17
|
+
import { createColumnBuilder, type SelectQueryModel, type ValueUpdater } from '../helpers';
|
|
18
18
|
import { type TableColumnDef } from '../types';
|
|
19
19
|
|
|
20
20
|
// TODO(burdon): Header menu builder.
|
|
@@ -23,11 +23,13 @@ import { type TableColumnDef } from '../types';
|
|
|
23
23
|
type Item = {
|
|
24
24
|
publicKey: PublicKey;
|
|
25
25
|
name: string;
|
|
26
|
+
company?: string;
|
|
26
27
|
count?: number;
|
|
27
28
|
started?: Date;
|
|
28
29
|
complete?: boolean;
|
|
29
30
|
};
|
|
30
31
|
|
|
32
|
+
// TODO(thure): …y’all okay?
|
|
31
33
|
faker.seed(911);
|
|
32
34
|
|
|
33
35
|
const createItems = (count: number) =>
|
|
@@ -46,59 +48,27 @@ const updateItems = <TValue = any,>(items: Item[], key: PublicKey, id: string, v
|
|
|
46
48
|
return items.map((item) => (item.publicKey.equals(key) ? { ...item, [id]: value } : item));
|
|
47
49
|
};
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
{
|
|
69
|
-
id: 'count',
|
|
70
|
-
type: 'number',
|
|
71
|
-
size: 160,
|
|
72
|
-
editable: true,
|
|
73
|
-
resizable: true,
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
},
|
|
77
|
-
{
|
|
78
|
-
id: 'b',
|
|
79
|
-
props: [
|
|
80
|
-
{
|
|
81
|
-
id: 'name',
|
|
82
|
-
type: 'string',
|
|
83
|
-
editable: true,
|
|
84
|
-
resizable: true,
|
|
85
|
-
},
|
|
86
|
-
],
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
id: 'c',
|
|
90
|
-
props: [
|
|
91
|
-
{
|
|
92
|
-
id: 'count',
|
|
93
|
-
type: 'number',
|
|
94
|
-
size: 160,
|
|
95
|
-
editable: true,
|
|
96
|
-
resizable: true,
|
|
97
|
-
},
|
|
98
|
-
],
|
|
51
|
+
const tableStorySelectItems: Record<string, Item> = range(128).reduce((acc: Record<string, Item>, _i) => {
|
|
52
|
+
const id = PublicKey.random();
|
|
53
|
+
acc[id.toHex()] = {
|
|
54
|
+
publicKey: id,
|
|
55
|
+
name: faker.company.name(),
|
|
56
|
+
};
|
|
57
|
+
return acc;
|
|
58
|
+
}, {});
|
|
59
|
+
|
|
60
|
+
const timeout = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
61
|
+
|
|
62
|
+
const tableStorySelectModel: SelectQueryModel<Item> = {
|
|
63
|
+
getId: (object) => object?.publicKey?.toHex() ?? 'never',
|
|
64
|
+
getText: (object) => object?.name ?? 'never',
|
|
65
|
+
query: async (search: string) => {
|
|
66
|
+
await timeout(400);
|
|
67
|
+
return Object.values(tableStorySelectItems).filter((item) => {
|
|
68
|
+
return item.name.toLowerCase().includes(search.toLowerCase());
|
|
69
|
+
});
|
|
99
70
|
},
|
|
100
|
-
|
|
101
|
-
*/
|
|
71
|
+
};
|
|
102
72
|
|
|
103
73
|
const { helper, builder } = createColumnBuilder<Item>();
|
|
104
74
|
const columns = (onUpdate?: ValueUpdater<Item, any>): TableColumnDef<Item, any>[] => [
|
|
@@ -116,10 +86,19 @@ const columns = (onUpdate?: ValueUpdater<Item, any>): TableColumnDef<Item, any>[
|
|
|
116
86
|
'name',
|
|
117
87
|
builder.string({ onUpdate, meta: { expand: true }, footer: (props) => props.table.getRowModel().rows.length }),
|
|
118
88
|
),
|
|
119
|
-
helper.accessor(
|
|
89
|
+
helper.accessor(
|
|
90
|
+
'company',
|
|
91
|
+
builder.select({
|
|
92
|
+
label: 'Company',
|
|
93
|
+
model: tableStorySelectModel,
|
|
94
|
+
onUpdate,
|
|
95
|
+
}),
|
|
96
|
+
),
|
|
97
|
+
helper.accessor('started', builder.date({ relative: true, meta: { resizable: true } })),
|
|
120
98
|
helper.accessor(
|
|
121
99
|
'count',
|
|
122
100
|
builder.number({
|
|
101
|
+
meta: { resizable: true },
|
|
123
102
|
// TODO(burdon): Sorting.
|
|
124
103
|
getGroupingValue: (row) => (row.count ? (row.count < 2_000 ? 'A' : row.count < 5_000 ? 'B' : 'C') : 'D'),
|
|
125
104
|
}),
|
|
@@ -186,18 +165,11 @@ export default {
|
|
|
186
165
|
},
|
|
187
166
|
decorators: [
|
|
188
167
|
(Story: any) => (
|
|
189
|
-
<
|
|
190
|
-
<
|
|
191
|
-
|
|
192
|
-
<Story />
|
|
193
|
-
</DensityProvider>
|
|
194
|
-
</div>
|
|
195
|
-
</div>
|
|
168
|
+
<DensityProvider density='fine'>
|
|
169
|
+
<Story />
|
|
170
|
+
</DensityProvider>
|
|
196
171
|
),
|
|
197
172
|
],
|
|
198
|
-
parameters: {
|
|
199
|
-
layout: 'fullscreen',
|
|
200
|
-
},
|
|
201
173
|
};
|
|
202
174
|
|
|
203
175
|
export const Default = {
|
|
@@ -280,3 +252,29 @@ export const Editable = {
|
|
|
280
252
|
);
|
|
281
253
|
},
|
|
282
254
|
};
|
|
255
|
+
|
|
256
|
+
export const Resizable = {
|
|
257
|
+
render: () => {
|
|
258
|
+
const [items, setItems] = useState<Item[]>(createItems(10));
|
|
259
|
+
const onUpdate: ValueUpdater<Item, any> = (item, prop, value) => {
|
|
260
|
+
setItems((items) => updateItems(items, item.publicKey, prop, value));
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// const handleColumnResize = (state) => {
|
|
264
|
+
// console.log('resize', state);
|
|
265
|
+
// };
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<div className='flex grow overflow-hidden'>
|
|
269
|
+
{/* prettier-ignore */}
|
|
270
|
+
<Table<Item>
|
|
271
|
+
keyAccessor={row => row.publicKey.toHex()}
|
|
272
|
+
columns={columns(onUpdate)}
|
|
273
|
+
data={items}
|
|
274
|
+
fullWidth
|
|
275
|
+
// onColumnResize={handleColumnResize}
|
|
276
|
+
/>
|
|
277
|
+
</div>
|
|
278
|
+
);
|
|
279
|
+
},
|
|
280
|
+
};
|
package/src/components/index.ts
CHANGED
package/src/helpers.tsx
CHANGED
|
@@ -8,13 +8,15 @@ import { createColumnHelper, type ColumnDef, type ColumnMeta, type RowData } fro
|
|
|
8
8
|
import format from 'date-fns/format';
|
|
9
9
|
import formatDistanceToNow from 'date-fns/formatDistanceToNow';
|
|
10
10
|
import defaultsDeep from 'lodash.defaultsdeep';
|
|
11
|
-
import React, {
|
|
11
|
+
import React, { useRef, useState } from 'react';
|
|
12
12
|
|
|
13
13
|
import { type PublicKey } from '@dxos/keys';
|
|
14
|
-
import {
|
|
14
|
+
import { Input, Tooltip } from '@dxos/react-ui';
|
|
15
15
|
import { getSize, mx } from '@dxos/react-ui-theme';
|
|
16
16
|
import { stripUndefinedValues } from '@dxos/util';
|
|
17
17
|
|
|
18
|
+
import { ComboboxCell } from './components';
|
|
19
|
+
|
|
18
20
|
// TODO(burdon): Factor out hack to find next focusable element (extend useFocusFinders)?
|
|
19
21
|
const findNextFocusable = (
|
|
20
22
|
findFirstFocusable: (container: HTMLElement) => HTMLElement | null | undefined,
|
|
@@ -94,51 +96,6 @@ const defaults = <TData extends RowData, TValue>(
|
|
|
94
96
|
return stripUndefinedValues(defaultsDeep({}, options, ...sources));
|
|
95
97
|
};
|
|
96
98
|
|
|
97
|
-
/**
|
|
98
|
-
* Lazy cell selector (click to activate).
|
|
99
|
-
*/
|
|
100
|
-
const CellSelector = <TData extends RowData>({
|
|
101
|
-
model,
|
|
102
|
-
value: _value,
|
|
103
|
-
onUpdate,
|
|
104
|
-
}: {
|
|
105
|
-
model: SelectQueryModel<TData>;
|
|
106
|
-
value: TData;
|
|
107
|
-
onUpdate: (value: TData) => void;
|
|
108
|
-
}) => {
|
|
109
|
-
const [edit, setEdit] = useState(false);
|
|
110
|
-
const [value, setValue] = useState<ComboBoxItem>();
|
|
111
|
-
useEffect(() => setValue(_value ? { id: model.getId(_value), label: model.getText(_value) } : undefined), [_value]);
|
|
112
|
-
const [items, setItems] = useState<ComboBoxItem[]>([]);
|
|
113
|
-
const handleUpdate = async (text?: string) => {
|
|
114
|
-
const items = await model.query(text);
|
|
115
|
-
setItems(items.map((item) => ({ id: model.getId(item), label: model.getText(item), data: item })));
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
if (!edit) {
|
|
119
|
-
const text = _value ? model.getText(_value) : undefined;
|
|
120
|
-
return (
|
|
121
|
-
// TODO(burdon): Hack to prevent div from collapsing.
|
|
122
|
-
<div className={mx('w-full px-2', !text && 'opacity-0')} onClick={() => setEdit(true)}>
|
|
123
|
-
{text ?? '-'}
|
|
124
|
-
</div>
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return (
|
|
129
|
-
<ComboBox.Root items={items} value={value} onChange={(value) => onUpdate(value?.data)} onInputChange={handleUpdate}>
|
|
130
|
-
<ComboBox.Input />
|
|
131
|
-
<ComboBox.Content>
|
|
132
|
-
{items.map((item) => (
|
|
133
|
-
<ComboBox.Item key={item.id} item={item}>
|
|
134
|
-
{item.label}
|
|
135
|
-
</ComboBox.Item>
|
|
136
|
-
))}
|
|
137
|
-
</ComboBox.Content>
|
|
138
|
-
</ComboBox.Root>
|
|
139
|
-
);
|
|
140
|
-
};
|
|
141
|
-
|
|
142
99
|
/**
|
|
143
100
|
* Util to create column definitions.
|
|
144
101
|
*/
|
|
@@ -155,10 +112,10 @@ export class ColumnBuilder<TData extends RowData> {
|
|
|
155
112
|
cell:
|
|
156
113
|
model && onUpdate
|
|
157
114
|
? (cell) => (
|
|
158
|
-
<
|
|
115
|
+
<ComboboxCell<any>
|
|
159
116
|
model={model}
|
|
160
117
|
value={cell.getValue()}
|
|
161
|
-
|
|
118
|
+
onValueChange={(value) => onUpdate?.(cell.row.original, cell.column.id, value)}
|
|
162
119
|
/>
|
|
163
120
|
)
|
|
164
121
|
: (cell) => {
|
package/src/types.ts
CHANGED
|
@@ -30,4 +30,4 @@ declare module '@tanstack/react-table' {
|
|
|
30
30
|
|
|
31
31
|
export type KeyValue<TData extends RowData> = (row: TData) => string;
|
|
32
32
|
|
|
33
|
-
export type TableColumnDef<TData extends RowData, TValue =
|
|
33
|
+
export type TableColumnDef<TData extends RowData, TValue = any> = ColumnDef<TData, TValue>;
|