@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-table",
3
- "version": "0.3.7-main.e08407b",
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/async": "0.3.7-main.e08407b",
24
- "@dxos/keys": "0.3.7-main.e08407b",
25
- "@dxos/invariant": "0.3.7-main.e08407b",
26
- "@dxos/react-ui": "0.3.7-main.e08407b",
27
- "@dxos/react-ui-types": "0.3.7-main.e08407b",
28
- "@dxos/react-ui-theme": "0.3.7-main.e08407b",
29
- "@dxos/util": "0.3.7-main.e08407b"
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.e08407b"
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
- // TODO(burdon): Remove.
51
- const schamas: TableSchema[] = [
52
- {
53
- id: 'a',
54
- props: [
55
- {
56
- id: 'complete',
57
- type: 'boolean',
58
- label: 'ok',
59
- fixed: true,
60
- editable: true,
61
- },
62
- {
63
- id: 'name',
64
- type: 'string',
65
- editable: true,
66
- resizable: true,
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('started', builder.date({ relative: true })),
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
- <div className='flex flex-col items-center h-screen w-full overflow-hidden bg-zinc-200'>
190
- <div className='flex w-[800px] h-full'>
191
- <DensityProvider density='fine'>
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
+ };
@@ -4,3 +4,4 @@
4
4
 
5
5
  export * from './ColumnMenu';
6
6
  export * from './Table';
7
+ export * from './ComboboxCell';
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, { useEffect, useRef, useState } from 'react';
11
+ import React, { useRef, useState } from 'react';
12
12
 
13
13
  import { type PublicKey } from '@dxos/keys';
14
- import { ComboBox, type ComboBoxItem, Input, Tooltip } from '@dxos/react-ui';
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
- <CellSelector<any>
115
+ <ComboboxCell<any>
159
116
  model={model}
160
117
  value={cell.getValue()}
161
- onUpdate={(value) => onUpdate?.(cell.row.original, cell.column.id, value)}
118
+ onValueChange={(value) => onUpdate?.(cell.row.original, cell.column.id, value)}
162
119
  />
163
120
  )
164
121
  : (cell) => {
@@ -0,0 +1,15 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export const translationKey = 'table';
6
+
7
+ export default [
8
+ {
9
+ 'en-US': {
10
+ [translationKey]: {
11
+ 'no selection label': ' ',
12
+ },
13
+ },
14
+ },
15
+ ];
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 = unknown> = ColumnDef<TData, TValue>;
33
+ export type TableColumnDef<TData extends RowData, TValue = any> = ColumnDef<TData, TValue>;