@dxos/react-ui-table 0.3.3-main.006ca4d

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.
Files changed (36) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +3 -0
  3. package/dist/lib/browser/index.mjs +844 -0
  4. package/dist/lib/browser/index.mjs.map +7 -0
  5. package/dist/lib/browser/meta.json +1 -0
  6. package/dist/types/src/components/ColumnMenu.d.ts +13 -0
  7. package/dist/types/src/components/ColumnMenu.d.ts.map +1 -0
  8. package/dist/types/src/components/ColumnMenu.stories.d.ts +16 -0
  9. package/dist/types/src/components/ColumnMenu.stories.d.ts.map +1 -0
  10. package/dist/types/src/components/Table.d.ts +29 -0
  11. package/dist/types/src/components/Table.d.ts.map +1 -0
  12. package/dist/types/src/components/Table.stories.d.ts +97 -0
  13. package/dist/types/src/components/Table.stories.d.ts.map +1 -0
  14. package/dist/types/src/components/index.d.ts +3 -0
  15. package/dist/types/src/components/index.d.ts.map +1 -0
  16. package/dist/types/src/helpers.d.ts +81 -0
  17. package/dist/types/src/helpers.d.ts.map +1 -0
  18. package/dist/types/src/index.d.ts +6 -0
  19. package/dist/types/src/index.d.ts.map +1 -0
  20. package/dist/types/src/schema.d.ts +21 -0
  21. package/dist/types/src/schema.d.ts.map +1 -0
  22. package/dist/types/src/theme.d.ts +34 -0
  23. package/dist/types/src/theme.d.ts.map +1 -0
  24. package/dist/types/src/types.d.ts +23 -0
  25. package/dist/types/src/types.d.ts.map +1 -0
  26. package/package.json +56 -0
  27. package/src/components/ColumnMenu.stories.tsx +35 -0
  28. package/src/components/ColumnMenu.tsx +239 -0
  29. package/src/components/Table.stories.tsx +328 -0
  30. package/src/components/Table.tsx +547 -0
  31. package/src/components/index.ts +6 -0
  32. package/src/helpers.tsx +443 -0
  33. package/src/index.ts +9 -0
  34. package/src/schema.ts +39 -0
  35. package/src/theme.ts +70 -0
  36. package/src/types.ts +33 -0
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@dxos/react-ui-table",
3
+ "version": "0.3.3-main.006ca4d",
4
+ "description": "A table component.",
5
+ "homepage": "https://dxos.org",
6
+ "bugs": "https://github.com/dxos/dxos/issues",
7
+ "license": "MIT",
8
+ "author": "DXOS.org",
9
+ "main": "dist/lib/node/index.cjs",
10
+ "browser": {
11
+ "./dist/lib/node/index.cjs": "./dist/lib/browser/index.mjs"
12
+ },
13
+ "types": "dist/types/src/index.d.ts",
14
+ "files": [
15
+ "dist",
16
+ "src"
17
+ ],
18
+ "dependencies": {
19
+ "@fluentui/react-tabster": "^9.12.0",
20
+ "@tanstack/react-table": "^8.9.3",
21
+ "date-fns": "^2.29.3",
22
+ "lodash.defaultsdeep": "^4.6.1",
23
+ "@dxos/async": "0.3.3-main.006ca4d",
24
+ "@dxos/invariant": "0.3.3-main.006ca4d",
25
+ "@dxos/keys": "0.3.3-main.006ca4d",
26
+ "@dxos/react-ui": "0.3.3-main.006ca4d",
27
+ "@dxos/react-ui-theme": "0.3.3-main.006ca4d",
28
+ "@dxos/react-ui-types": "0.3.3-main.006ca4d",
29
+ "@dxos/util": "0.3.3-main.006ca4d"
30
+ },
31
+ "devDependencies": {
32
+ "@faker-js/faker": "^8.0.2",
33
+ "@phosphor-icons/react": "^2.0.5",
34
+ "@types/lodash.defaultsdeep": "^4.6.6",
35
+ "@types/react": "^18.0.21",
36
+ "@types/react-dom": "^18.0.6",
37
+ "chromatic": "^6.17.0",
38
+ "deepsignal": "^1.3.6",
39
+ "react": "^18.2.0",
40
+ "react-dom": "^18.2.0",
41
+ "vite": "^4.3.9",
42
+ "vite-plugin-turbosnap": "^1.0.2",
43
+ "@dxos/react-ui-theme": "0.3.3-main.006ca4d"
44
+ },
45
+ "peerDependencies": {
46
+ "@phosphor-icons/react": "^2.0.5",
47
+ "react": "^18.2.0",
48
+ "react-dom": "^18.2.0"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "scripts": {
54
+ "chromatic": "true"
55
+ }
56
+ }
@@ -0,0 +1,35 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxosTheme';
6
+
7
+ import React from 'react';
8
+
9
+ import { ColumnPanel } from './ColumnMenu';
10
+ import { type ColumnProps } from '../schema';
11
+
12
+ export default {
13
+ component: ColumnPanel,
14
+ };
15
+
16
+ export const Default = {
17
+ args: {
18
+ column: {
19
+ id: 'test',
20
+ label: 'test',
21
+ } as ColumnProps,
22
+ },
23
+ decorators: [
24
+ (Story: any) => (
25
+ <div className='flex flex-col items-center h-screen w-full overflow-hidden'>
26
+ <div className='flex w-[250px] m-8 bg-white'>
27
+ <Story />
28
+ </div>
29
+ </div>
30
+ ),
31
+ ],
32
+ parameters: {
33
+ layout: 'fullscreen',
34
+ },
35
+ };
@@ -0,0 +1,239 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { Check, CaretDown, Trash, X } from '@phosphor-icons/react';
6
+ import { type HeaderContext, type RowData } from '@tanstack/react-table';
7
+ import React, { type FC, type PropsWithChildren, useRef, useState } from 'react';
8
+
9
+ import { Button, DensityProvider, Input, Popover, Select, Separator } from '@dxos/react-ui';
10
+ import { getSize, mx } from '@dxos/react-ui-theme';
11
+ import { safeParseInt } from '@dxos/util';
12
+
13
+ import { type TableDef, type ColumnProps, columnTypes } from '../schema';
14
+
15
+ export type ColumnMenuProps<TData extends RowData, TValue> = {
16
+ context: HeaderContext<TData, TValue>;
17
+ tableDefs: TableDef[];
18
+ tableDef: TableDef;
19
+ column: ColumnProps;
20
+ onUpdate?: (id: string, column: ColumnProps) => void;
21
+ onDelete?: (id: string) => void;
22
+ };
23
+
24
+ export const ColumnMenu = <TData extends RowData, TValue>({ column, ...props }: ColumnMenuProps<TData, TValue>) => {
25
+ const title = column.label?.length ? column.label : column.id;
26
+
27
+ return (
28
+ <div className='flex grow items-center overflow-hidden'>
29
+ <div className='truncate' title={title}>
30
+ {title}
31
+ </div>
32
+ <div className='grow' />
33
+ <div className='flex shrink-0'>
34
+ <ColumnPanel {...props} column={column} />
35
+ </div>
36
+ </div>
37
+ );
38
+ };
39
+
40
+ const Section: FC<PropsWithChildren & { className?: string }> = ({ children, className }) => (
41
+ <div role='none' className={mx('p-2', className)}>
42
+ {children}
43
+ </div>
44
+ );
45
+
46
+ export const ColumnPanel = <TData extends RowData, TValue>({
47
+ context,
48
+ tableDefs,
49
+ tableDef,
50
+ column,
51
+ onUpdate,
52
+ onDelete,
53
+ }: ColumnMenuProps<TData, TValue>) => {
54
+ const [open, setOpen] = useState(false);
55
+ const [prop, setProp] = useState(column.id);
56
+ const [refTable, setRefTable] = useState(column.refTable);
57
+ const [refProp, setRefProp] = useState(column.refProp);
58
+ const [type, setType] = useState(String(column.type));
59
+ const [label, setLabel] = useState(column.label);
60
+ const [digits, setDigits] = useState(String(column.digits ?? '0'));
61
+ const propRef = useRef<HTMLInputElement>(null);
62
+
63
+ const handleCancel = () => {
64
+ setProp(column.id);
65
+ setOpen(false);
66
+ };
67
+
68
+ const handleSave = () => {
69
+ // Check valid.
70
+ if (!prop.length || !prop.match(/^[a-zA-Z_].+/i)) {
71
+ propRef.current?.focus();
72
+ return;
73
+ }
74
+
75
+ // If already exists then check same type.
76
+ const current = tableDef.columns.find((c) => c.id !== column.id && c.id === prop);
77
+ if (current) {
78
+ // TODO(burdon): Allow multiple columns with different refProps. Ensure correct table prop is updated.
79
+ if (current.type !== 'ref' /* || current.ref !== refTable */) {
80
+ // TODO(burdon): Show error.
81
+ propRef.current?.focus();
82
+ return;
83
+ }
84
+ }
85
+
86
+ onUpdate?.(prop, {
87
+ id: prop, // TODO(burdon): Make unique.
88
+ prop,
89
+ label,
90
+ type: type as ColumnProps['type'],
91
+ refTable,
92
+ refProp,
93
+ digits: safeParseInt(digits),
94
+ });
95
+
96
+ setOpen(false);
97
+ };
98
+
99
+ return (
100
+ <Popover.Root open={open} onOpenChange={(nextOpen) => setOpen(nextOpen)}>
101
+ <Popover.Trigger asChild>
102
+ <Button variant='ghost' classNames='p-0'>
103
+ <CaretDown className={getSize(4)} />
104
+ </Button>
105
+ </Popover.Trigger>
106
+ <Popover.Portal>
107
+ <Popover.Content>
108
+ <Popover.Viewport classNames='w-60'>
109
+ <DensityProvider density='fine'>
110
+ <div className='flex flex-col gap-1'>
111
+ <Section>
112
+ <Input.Root>
113
+ <Input.Label classNames='mbe-1'>Label</Input.Label>
114
+ <Input.TextInput
115
+ placeholder='Column label'
116
+ value={label ?? ''}
117
+ onChange={(event) => setLabel(event.target.value)}
118
+ autoFocus
119
+ />
120
+ </Input.Root>
121
+ <Input.Root>
122
+ <Input.Label classNames='mbe-1 mbs-3'>Property</Input.Label>
123
+ <Input.TextInput
124
+ ref={propRef}
125
+ placeholder='Property key'
126
+ // TODO(burdon): Provide hooks for value normalization, ENTER, ESC, etc.
127
+ value={prop ?? ''}
128
+ onChange={(event) => setProp(event.target.value.replace(/[^\w_]/g, ''))}
129
+ />
130
+ </Input.Root>
131
+ <Input.Root>
132
+ <Input.Label classNames='mbe-1 mbs-3'>Type</Input.Label>
133
+ <Select.Root value={type} onValueChange={setType}>
134
+ <Select.TriggerButton placeholder='Type' classNames='is-full' />
135
+ <Select.Portal>
136
+ <Select.Content>
137
+ <Select.Viewport>
138
+ {Array.from(columnTypes.entries()).map(([type, label]) => (
139
+ <Select.Option key={type} value={type}>
140
+ {label}
141
+ </Select.Option>
142
+ ))}
143
+ </Select.Viewport>
144
+ </Select.Content>
145
+ </Select.Portal>
146
+ </Select.Root>
147
+ </Input.Root>
148
+ </Section>
149
+
150
+ {type === 'number' && (
151
+ <>
152
+ <Section>
153
+ <Input.Root>
154
+ <Input.Label classNames='mbe-1 mbs-3'>Decimal places</Input.Label>
155
+ {/* TODO(burdon): Constrain input to numbers. */}
156
+ <Input.TextInput value={digits ?? ''} onChange={(event) => setDigits(event.target.value)} />
157
+ </Input.Root>
158
+ </Section>
159
+ </>
160
+ )}
161
+
162
+ {/* TODO(burdon): Selectors on same line. */}
163
+ {type === 'ref' && (
164
+ <>
165
+ <Section>
166
+ <Input.Root>
167
+ <Input.Label classNames='mbe-1 mbs-3'>Table</Input.Label>
168
+ <Select.Root value={refTable} onValueChange={setRefTable}>
169
+ <Select.TriggerButton placeholder='Table' classNames='is-full' />
170
+ <Select.Portal>
171
+ <Select.Content>
172
+ <Select.Viewport>
173
+ {tableDefs
174
+ .filter((t) => t.id !== tableDef.id)
175
+ .map(({ id, name }) => (
176
+ <Select.Option key={id} value={id}>
177
+ {name ?? id}
178
+ </Select.Option>
179
+ ))}
180
+ </Select.Viewport>
181
+ </Select.Content>
182
+ </Select.Portal>
183
+ </Select.Root>
184
+ </Input.Root>
185
+
186
+ {refTable && (
187
+ <Input.Root>
188
+ <Input.Label classNames='mbe-1 mbs-3'>Table property</Input.Label>
189
+ <Select.Root value={refProp} onValueChange={setRefProp}>
190
+ <Select.TriggerButton placeholder='Property' classNames='is-full' />
191
+ <Select.Portal>
192
+ <Select.Content>
193
+ <Select.Viewport>
194
+ {tableDefs
195
+ .find((tableDef) => tableDef.id === refTable)
196
+ ?.columns.map(({ id, label }) => (
197
+ <Select.Option key={id} value={id}>
198
+ {label ?? id}
199
+ </Select.Option>
200
+ ))}
201
+ </Select.Viewport>
202
+ </Select.Content>
203
+ </Select.Portal>
204
+ </Select.Root>
205
+ </Input.Root>
206
+ )}
207
+ </Section>
208
+ </>
209
+ )}
210
+
211
+ <Separator classNames='mli-2' />
212
+
213
+ <Section className='space-b-1.5'>
214
+ {/* TODO(burdon): Style as DropdownMenuItem. */}
215
+ <Button variant='primary' classNames='is-full flex gap-2' onClick={handleSave}>
216
+ <span>Save</span>
217
+ <div className='grow' />
218
+ <Check className={getSize(5)} />
219
+ </Button>
220
+ <Button classNames='is-full flex gap-2' onClick={handleCancel}>
221
+ <span>Cancel</span>
222
+ <div className='grow' />
223
+ <X className={getSize(5)} />
224
+ </Button>
225
+ <Button classNames='is-full flex gap-2' onClick={() => onDelete?.(column.id)}>
226
+ <span>Delete</span>
227
+ <div className='grow' />
228
+ <Trash className={getSize(5)} />
229
+ </Button>
230
+ </Section>
231
+ </div>
232
+ </DensityProvider>
233
+ </Popover.Viewport>
234
+ <Popover.Arrow />
235
+ </Popover.Content>
236
+ </Popover.Portal>
237
+ </Popover.Root>
238
+ );
239
+ };
@@ -0,0 +1,328 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxosTheme';
6
+
7
+ import { faker } from '@faker-js/faker';
8
+ import { Plugs, PlugsConnected } from '@phosphor-icons/react';
9
+ import { deepSignal } from 'deepsignal';
10
+ import React, { useEffect, useState } from 'react';
11
+
12
+ import { PublicKey } from '@dxos/keys';
13
+ import { DensityProvider } from '@dxos/react-ui';
14
+ import { range } from '@dxos/util';
15
+
16
+ import { Table } from './Table';
17
+ import { createColumnBuilder, type ValueUpdater } from '../helpers';
18
+ import { type TableColumnDef } from '../types';
19
+
20
+ // TODO(burdon): Header menu builder.
21
+ // TODO(burdon): Expand width if not all columns have explicit size.
22
+
23
+ type Item = {
24
+ publicKey: PublicKey;
25
+ name: string;
26
+ count?: number;
27
+ started?: Date;
28
+ complete?: boolean;
29
+ };
30
+
31
+ faker.seed(911);
32
+
33
+ const createItems = (count: number) =>
34
+ range(count).map(
35
+ () =>
36
+ deepSignal<Item>({
37
+ publicKey: PublicKey.random(),
38
+ name: faker.commerce.productName(),
39
+ count: faker.datatype.boolean({ probability: 0.9 }) ? faker.number.int({ min: 0, max: 10_000 }) : undefined,
40
+ started: faker.date.recent(),
41
+ complete: faker.datatype.boolean() ? true : faker.datatype.boolean() ? false : undefined,
42
+ }) as Item,
43
+ );
44
+
45
+ const updateItems = <TValue = any,>(items: Item[], key: PublicKey, id: string, value: TValue) => {
46
+ return items.map((item) => (item.publicKey.equals(key) ? { ...item, [id]: value } : item));
47
+ };
48
+
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
+ ],
99
+ },
100
+ ];
101
+ */
102
+
103
+ const { helper, builder } = createColumnBuilder<Item>();
104
+ const columns = (onUpdate?: ValueUpdater<Item, any>): TableColumnDef<Item, any>[] => [
105
+ helper.accessor(
106
+ 'complete',
107
+ builder.checkbox({
108
+ // enableGrouping: true,
109
+ getGroupingValue: (row) => row.complete === true,
110
+ label: '',
111
+ onUpdate,
112
+ }),
113
+ ),
114
+ helper.accessor((item) => item.publicKey, { id: 'key', ...builder.key({ tooltip: true }) }),
115
+ helper.accessor(
116
+ 'name',
117
+ builder.string({ onUpdate, meta: { expand: true }, footer: (props) => props.table.getRowModel().rows.length }),
118
+ ),
119
+ helper.accessor('started', builder.date({ relative: true })),
120
+ helper.accessor(
121
+ 'count',
122
+ builder.number({
123
+ // TODO(burdon): Sorting.
124
+ getGroupingValue: (row) => (row.count ? (row.count < 2_000 ? 'A' : row.count < 5_000 ? 'B' : 'C') : 'D'),
125
+ }),
126
+ ),
127
+ helper.accessor('complete', builder.icon({ id: 'done', label: '' })),
128
+ helper.accessor(
129
+ 'complete',
130
+ builder.icon({
131
+ id: 'connected',
132
+ label: '',
133
+ on: { Icon: PlugsConnected, className: 'text-blue-500' },
134
+ off: { Icon: Plugs, className: 'text-blue-200' },
135
+ }),
136
+ ),
137
+ ];
138
+
139
+ //
140
+ // Tests
141
+ //
142
+
143
+ export default {
144
+ component: Table,
145
+ args: {
146
+ header: true,
147
+ keyAccessor: (item: Item) => item.publicKey.toHex(),
148
+ },
149
+ argTypes: {
150
+ header: {
151
+ control: 'boolean',
152
+ },
153
+ footer: {
154
+ control: 'boolean',
155
+ },
156
+ border: {
157
+ control: 'boolean',
158
+ },
159
+ fullWidth: {
160
+ control: 'boolean',
161
+ },
162
+ pinToBottom: {
163
+ control: 'boolean',
164
+ },
165
+ grouping: {
166
+ control: 'select',
167
+ options: ['none', 'complete', 'count'],
168
+ mapping: {
169
+ none: undefined,
170
+ complete: ['complete'],
171
+ count: ['count'],
172
+ },
173
+ },
174
+ columnVisibility: {
175
+ control: 'select',
176
+ options: ['all', 'limited'],
177
+ mapping: {
178
+ all: undefined,
179
+ limited: { key: false, started: false },
180
+ },
181
+ },
182
+ select: {
183
+ control: 'select',
184
+ options: ['single', 'single-toggle', 'multiple', 'multiple-toggle'],
185
+ },
186
+ },
187
+ decorators: [
188
+ (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>
196
+ ),
197
+ ],
198
+ parameters: {
199
+ layout: 'fullscreen',
200
+ },
201
+ };
202
+
203
+ export const Default = {
204
+ args: {
205
+ columns: columns(),
206
+ data: createItems(20),
207
+ },
208
+ };
209
+
210
+ export const Scrolling = {
211
+ args: {
212
+ columns: columns(),
213
+ data: createItems(200),
214
+ },
215
+ };
216
+
217
+ export const Visibility = {
218
+ args: {
219
+ columns: columns(),
220
+ data: createItems(10),
221
+ columnVisibility: { key: false, started: false },
222
+ },
223
+ };
224
+
225
+ export const Empty = {
226
+ args: {
227
+ columns: columns(),
228
+ },
229
+ };
230
+
231
+ export const Dynamic = {
232
+ render: () => {
233
+ const [items, setItems] = useState<Item[]>(createItems(50));
234
+ useEffect(() => {
235
+ const interval = setInterval(() => {
236
+ setItems((items) => {
237
+ if (items.length >= 200) {
238
+ clearInterval(interval);
239
+ }
240
+
241
+ return [...items, ...createItems(1)];
242
+ });
243
+ }, 500);
244
+ return () => clearInterval(interval);
245
+ }, []);
246
+
247
+ return (
248
+ <div className='flex grow overflow-hidden'>
249
+ {/* prettier-ignore */}
250
+ <Table<Item>
251
+ keyAccessor={row => row.publicKey.toHex()}
252
+ columns={columns()}
253
+ data={items}
254
+ fullWidth
255
+ pinToBottom
256
+ footer
257
+ />
258
+ </div>
259
+ );
260
+ },
261
+ };
262
+
263
+ export const Editable = {
264
+ render: () => {
265
+ const [items, setItems] = useState<Item[]>(createItems(10));
266
+ const onUpdate: ValueUpdater<Item, any> = (item, prop, value) => {
267
+ setItems((items) => updateItems(items, item.publicKey, prop, value));
268
+ };
269
+
270
+ return (
271
+ <div className='flex grow overflow-hidden'>
272
+ {/* prettier-ignore */}
273
+ <Table<Item>
274
+ keyAccessor={row => row.publicKey.toHex()}
275
+ columns={columns(onUpdate)}
276
+ data={items}
277
+ fullWidth
278
+ />
279
+ </div>
280
+ );
281
+ },
282
+ };
283
+
284
+ /*
285
+ // TODO(burdon): Remove.
286
+ export const Schema = {
287
+ render: () => {
288
+ const [schema, setSchema] = useState(schamas[0]);
289
+ const [items, setItems] = useState(createItems(10));
290
+
291
+ const columns = createColumns<Item>(schamas, schema, {
292
+ onColumnUpdate: (id, column) => {
293
+ setSchema(({ props, ...rest }) => ({
294
+ props: props.map((c) => (c.id === id ? column : c)),
295
+ ...rest,
296
+ }));
297
+ },
298
+ onColumnDelete: (id) => {
299
+ setSchema(({ props, ...rest }) => ({ props: props.filter((c) => c.id !== id), ...rest }));
300
+ },
301
+ onUpdate: (item, prop, value) => {
302
+ setItems((items) => updateItems(items, item.publicKey, prop, value));
303
+ },
304
+ });
305
+
306
+ const actionColumn = createActionColumn<Item>(schema, {
307
+ isDeletable: (row) => !!row.publicKey,
308
+ onColumnCreate: (column) => {
309
+ setSchema(({ props, ...rest }) => ({ props: [...props, column], ...rest }));
310
+ },
311
+ onRowDelete: (row) => {
312
+ setItems((items) => items.filter((item) => !item.publicKey.equals(row.publicKey)));
313
+ },
314
+ });
315
+
316
+ return (
317
+ <div className='flex grow overflow-hidden'>
318
+ <Table<Item>
319
+ keyAccessor={row => row.publicKey.toHex()}
320
+ columns={[...columns, actionColumn]}
321
+ data={items}
322
+ border
323
+ />
324
+ </div>
325
+ );
326
+ },
327
+ };
328
+ */