@dxos/react-ui-table 0.8.4-main.28f8d3d → 0.8.4-main.406dc2a

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 (108) hide show
  1. package/dist/lib/browser/chunk-4K7JPALT.mjs +66 -0
  2. package/dist/lib/browser/chunk-4K7JPALT.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-ZAOSIG3L.mjs → chunk-OZTXLZGM.mjs} +526 -536
  4. package/dist/lib/browser/chunk-OZTXLZGM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +373 -481
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/testing/index.mjs +11 -8
  9. package/dist/lib/browser/testing/index.mjs.map +3 -3
  10. package/dist/lib/browser/types/index.mjs +3 -3
  11. package/dist/lib/node-esm/chunk-MK6OVUF6.mjs +67 -0
  12. package/dist/lib/node-esm/chunk-MK6OVUF6.mjs.map +7 -0
  13. package/dist/lib/node-esm/{chunk-ZOFFI3JJ.mjs → chunk-S56Z4V4X.mjs} +526 -536
  14. package/dist/lib/node-esm/chunk-S56Z4V4X.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +373 -481
  16. package/dist/lib/node-esm/index.mjs.map +4 -4
  17. package/dist/lib/node-esm/meta.json +1 -1
  18. package/dist/lib/node-esm/testing/index.mjs +11 -8
  19. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  20. package/dist/lib/node-esm/types/index.mjs +3 -3
  21. package/dist/types/src/components/Table/DynamicTable.d.ts +2 -2
  22. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +51 -3
  24. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/Relations.stories.d.ts +104 -5
  26. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/Table.d.ts +8 -5
  28. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/Table.stories.d.ts +41 -3
  30. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +12 -6
  32. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  33. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +8 -13
  34. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  35. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +39 -4
  36. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  38. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  39. package/dist/types/src/components/testing.d.ts +3 -3
  40. package/dist/types/src/components/testing.d.ts.map +1 -1
  41. package/dist/types/src/hooks/useAddRow.d.ts +3 -2
  42. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  43. package/dist/types/src/hooks/useTableModel.d.ts +1 -1
  44. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  45. package/dist/types/src/model/modal-controller.d.ts +0 -7
  46. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  47. package/dist/types/src/model/table-model.d.ts +7 -6
  48. package/dist/types/src/model/table-model.d.ts.map +1 -1
  49. package/dist/types/src/model/table-presentation.d.ts +1 -1
  50. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  51. package/dist/types/src/model/table-sorting.d.ts +1 -2
  52. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  53. package/dist/types/src/testing/useTestTableModel.d.ts +3 -22
  54. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  55. package/dist/types/src/types/index.d.ts +1 -1
  56. package/dist/types/src/types/index.d.ts.map +1 -1
  57. package/dist/types/src/types/schema.d.ts +25 -5
  58. package/dist/types/src/types/schema.d.ts.map +1 -1
  59. package/dist/types/src/util/dynamic-table.d.ts +4 -5
  60. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  61. package/dist/types/src/util/index.d.ts +0 -1
  62. package/dist/types/src/util/index.d.ts.map +1 -1
  63. package/dist/types/src/util/schema.d.ts +1 -1
  64. package/dist/types/src/util/schema.d.ts.map +1 -1
  65. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  66. package/dist/types/tsconfig.tsbuildinfo +1 -1
  67. package/package.json +51 -50
  68. package/src/components/Table/DynamicTable.stories.tsx +12 -9
  69. package/src/components/Table/DynamicTable.tsx +4 -7
  70. package/src/components/Table/Relations.stories.tsx +162 -38
  71. package/src/components/Table/Table.stories.tsx +59 -52
  72. package/src/components/Table/Table.tsx +109 -90
  73. package/src/components/TableCellEditor/FormCellEditor.tsx +153 -42
  74. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +22 -63
  75. package/src/components/TableCellEditor/TableCellEditor.tsx +62 -208
  76. package/src/components/TableCellEditor/index.ts +1 -0
  77. package/src/components/testing.ts +2 -2
  78. package/src/hooks/useAddRow.ts +7 -5
  79. package/src/hooks/useTableModel.ts +1 -1
  80. package/src/model/modal-controller.ts +0 -28
  81. package/src/model/table-model.test.ts +18 -10
  82. package/src/model/table-model.ts +26 -17
  83. package/src/model/table-presentation.test.ts +21 -20
  84. package/src/model/table-presentation.ts +58 -39
  85. package/src/model/table-sorting.ts +15 -6
  86. package/src/playwright/smoke.spec.ts +46 -46
  87. package/src/testing/useTestTableModel.ts +9 -6
  88. package/src/types/index.ts +1 -1
  89. package/src/types/schema.ts +63 -6
  90. package/src/util/dynamic-table.ts +11 -11
  91. package/src/util/index.ts +0 -1
  92. package/src/util/schema.ts +4 -4
  93. package/src/util/table-buttons.ts +3 -3
  94. package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
  95. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
  96. package/dist/lib/browser/chunk-ZAOSIG3L.mjs.map +0 -7
  97. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +0 -20
  98. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +0 -7
  99. package/dist/lib/node-esm/chunk-ZOFFI3JJ.mjs.map +0 -7
  100. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  101. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  102. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  103. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  104. package/dist/types/src/util/table.d.ts +0 -12
  105. package/dist/types/src/util/table.d.ts.map +0 -1
  106. package/src/components/Table/CreateRefPanel.tsx +0 -86
  107. package/src/components/TableCellEditor/extension.ts +0 -61
  108. package/src/util/table.ts +0 -53
@@ -2,13 +2,11 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
8
- import { Schema } from 'effect';
6
+ import * as Schema from 'effect/Schema';
9
7
  import React, { useCallback } from 'react';
10
8
 
11
- import { Obj, Type } from '@dxos/echo';
9
+ import { Obj, type QueryAST, Type } from '@dxos/echo';
12
10
  import {
13
11
  EchoObject,
14
12
  FormatAnnotation,
@@ -16,25 +14,25 @@ import {
16
14
  GeneratorAnnotation,
17
15
  LabelAnnotation,
18
16
  PropertyMetaAnnotationId,
19
- } from '@dxos/echo-schema';
17
+ } from '@dxos/echo/internal';
18
+ import { live } from '@dxos/echo/internal';
20
19
  import { invariant } from '@dxos/invariant';
21
20
  import { faker } from '@dxos/random';
22
21
  import { PublicKey } from '@dxos/react-client';
23
- import { type Space, live } from '@dxos/react-client/echo';
22
+ import { type Space } from '@dxos/react-client/echo';
24
23
  import { withClientProvider } from '@dxos/react-client/testing';
24
+ import { withTheme } from '@dxos/react-ui/testing';
25
25
  import { ViewEditor } from '@dxos/react-ui-form';
26
26
  import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
27
- import { DataType, getSchemaFromPropertyDefinitions } from '@dxos/schema';
27
+ import { DataType, getSchemaFromPropertyDefinitions, getTypenameFromQuery } from '@dxos/schema';
28
28
  import { Testing, createObjectFactory } from '@dxos/schema/testing';
29
- import { withLayout, withTheme } from '@dxos/storybook-utils';
30
29
 
31
30
  import { useTestTableModel } from '../../testing';
32
31
  import { translations } from '../../translations';
33
- import { TableView } from '../../types';
34
- import { createTable } from '../../util';
32
+ import { Table } from '../../types';
35
33
  import { TableToolbar } from '../TableToolbar';
36
34
 
37
- import { Table } from './Table';
35
+ import { Table as TableComponent } from './Table';
38
36
 
39
37
  const TestSchema = Schema.Struct({
40
38
  // TODO(wittjosiah): Should be title. Currently name to work with default label.
@@ -77,13 +75,13 @@ const StoryViewEditor = ({
77
75
  space?: Space;
78
76
  handleDeleteColumn: (fieldId: string) => void;
79
77
  }) => {
80
- const handleTypenameChanged = useCallback(
81
- (typename: string) => {
78
+ const handleQueryChanged = useCallback(
79
+ (newQuery: QueryAST.Query) => {
82
80
  invariant(schema);
83
81
  invariant(Type.isMutable(schema));
84
- schema.updateTypename(typename);
82
+ schema.updateTypename(getTypenameFromQuery(newQuery));
85
83
  invariant(view);
86
- view.query.typename = typename;
84
+ view.query.ast = newQuery;
87
85
  },
88
86
  [schema, view],
89
87
  );
@@ -97,7 +95,7 @@ const StoryViewEditor = ({
97
95
  registry={space?.db.schemaRegistry}
98
96
  schema={schema}
99
97
  view={view}
100
- onTypenameChanged={handleTypenameChanged}
98
+ onQueryChanged={handleQueryChanged}
101
99
  onDelete={handleDeleteColumn}
102
100
  />
103
101
  );
@@ -129,8 +127,8 @@ const DefaultStory = () => {
129
127
  <div className='grow grid grid-cols-[1fr_350px]'>
130
128
  <div className='grid grid-rows-[min-content_1fr] min-bs-0 overflow-hidden'>
131
129
  <TableToolbar classNames='border-be border-subduedSeparator' onAdd={handleInsertRow} onSave={handleSaveView} />
132
- <Table.Root>
133
- <Table.Main
130
+ <TableComponent.Root>
131
+ <TableComponent.Main
134
132
  ref={tableRef}
135
133
  model={model}
136
134
  presentation={presentation}
@@ -138,11 +136,11 @@ const DefaultStory = () => {
138
136
  client={client}
139
137
  ignoreAttention
140
138
  />
141
- </Table.Root>
139
+ </TableComponent.Root>
142
140
  </div>
143
141
  <div className='flex flex-col h-full border-l border-separator overflow-y-auto'>
144
142
  <StoryViewEditor view={view} schema={schema} space={space} handleDeleteColumn={handleDeleteColumn} />
145
- <SyntaxHighlighter language='json' className='w-full text-xs'>
143
+ <SyntaxHighlighter language='json' className='text-xs'>
146
144
  {JSON.stringify({ view, schema }, null, 2)}
147
145
  </SyntaxHighlighter>
148
146
  </div>
@@ -158,26 +156,23 @@ type StoryProps = { rows?: number };
158
156
 
159
157
  // TODO(burdon): Need super simple story.
160
158
 
161
- const meta: Meta<StoryProps> = {
159
+ const meta = {
162
160
  title: 'ui/react-ui-table/Table',
163
161
  render: DefaultStory,
164
- parameters: {
165
- translations,
166
- layout: 'fullscreen',
167
- controls: {
168
- disable: true,
169
- },
170
- },
171
162
  decorators: [
172
163
  withTheme,
173
- withLayout({ fullscreen: true }),
174
164
  withClientProvider({
175
- types: [DataType.View, TableView],
165
+ types: [DataType.View, Table.Table],
176
166
  createIdentity: true,
177
167
  createSpace: true,
178
- onSpaceCreated: async ({ client, space }) => {
168
+ onCreateSpace: async ({ client, space }) => {
179
169
  const [schema] = await space.db.schemaRegistry.register([TestSchema]);
180
- const { view } = await createTable({ client, space, typename: schema.typename });
170
+ const { view } = await Table.makeView({ client, space, typename: schema.typename });
171
+ view.projection.fields = [
172
+ view.projection.fields.find((field) => field.path === 'name')!,
173
+ ...view.projection.fields.filter((field) => field.path !== 'name'),
174
+ ];
175
+
181
176
  space.db.add(view);
182
177
 
183
178
  Array.from({ length: 10 }).map(() => {
@@ -192,22 +187,30 @@ const meta: Meta<StoryProps> = {
192
187
  },
193
188
  }),
194
189
  ],
195
- };
190
+ parameters: {
191
+ layout: 'fullscreen',
192
+ controls: {
193
+ disable: true,
194
+ },
195
+ translations,
196
+ },
197
+ } satisfies Meta<typeof TableComponent>;
196
198
 
197
199
  export default meta;
198
200
 
199
- export const Default = {};
201
+ type Story = StoryObj<typeof meta>;
202
+
203
+ export const Default: Story = {};
200
204
 
201
205
  export const StaticSchema: StoryObj = {
202
206
  render: DefaultStory,
203
- parameters: { translations },
204
207
  decorators: [
205
208
  withClientProvider({
206
- types: [DataType.View, TableView, Testing.Contact, Testing.Organization],
209
+ types: [DataType.View, Table.Table, Testing.Contact, Testing.Organization],
207
210
  createIdentity: true,
208
211
  createSpace: true,
209
- onSpaceCreated: async ({ client, space }) => {
210
- const { view } = await createTable({ client, space, typename: Testing.Contact.typename });
212
+ onCreateSpace: async ({ client, space }) => {
213
+ const { view } = await Table.makeView({ client, space, typename: Testing.Contact.typename });
211
214
  space.db.add(view);
212
215
 
213
216
  const factory = createObjectFactory(space.db, faker as any);
@@ -217,9 +220,11 @@ export const StaticSchema: StoryObj = {
217
220
  ]);
218
221
  },
219
222
  }),
220
- withLayout({ fullscreen: true }),
221
- withTheme,
222
223
  ],
224
+ parameters: {
225
+ layout: 'fullscreen',
226
+ translations,
227
+ },
223
228
  };
224
229
 
225
230
  const ContactWithArrayOfEmails = Schema.Struct({
@@ -241,14 +246,13 @@ const ContactWithArrayOfEmails = Schema.Struct({
241
246
 
242
247
  export const ArrayOfObjects: StoryObj = {
243
248
  render: DefaultStory,
244
- parameters: { translations },
245
249
  decorators: [
246
250
  withClientProvider({
247
- types: [DataType.View, TableView, Testing.Contact, Testing.Organization, ContactWithArrayOfEmails],
251
+ types: [DataType.View, Table.Table, Testing.Contact, Testing.Organization, ContactWithArrayOfEmails],
248
252
  createIdentity: true,
249
253
  createSpace: true,
250
- onSpaceCreated: async ({ client, space }) => {
251
- const { view } = await createTable({ client, space, typename: ContactWithArrayOfEmails.typename });
254
+ onCreateSpace: async ({ client, space }) => {
255
+ const { view } = await Table.makeView({ client, space, typename: ContactWithArrayOfEmails.typename });
252
256
  space.db.add(view);
253
257
 
254
258
  const factory = createObjectFactory(space.db, faker as any);
@@ -259,21 +263,22 @@ export const ArrayOfObjects: StoryObj = {
259
263
  ]);
260
264
  },
261
265
  }),
262
- withLayout({ fullscreen: true }),
263
- withTheme,
264
266
  ],
267
+ parameters: {
268
+ layout: 'fullscreen',
269
+ translations,
270
+ },
265
271
  };
266
272
 
267
273
  export const Tags: Meta<StoryProps> = {
268
274
  title: 'ui/react-ui-table/Table',
269
275
  render: DefaultStory,
270
- parameters: { translations },
271
276
  decorators: [
272
277
  withClientProvider({
273
- types: [DataType.View, TableView],
278
+ types: [DataType.View, Table.Table],
274
279
  createIdentity: true,
275
280
  createSpace: true,
276
- onSpaceCreated: async ({ client, space }) => {
281
+ onCreateSpace: async ({ client, space }) => {
277
282
  // Configure schema.
278
283
  const typename = 'example.com/SingleSelect';
279
284
  const selectOptions = [
@@ -301,7 +306,7 @@ export const Tags: Meta<StoryProps> = {
301
306
  const [storedSchema] = await space.db.schemaRegistry.register([schema]);
302
307
 
303
308
  // Initialize table.
304
- const { view } = await createTable({ client, space, typename });
309
+ const { view } = await Table.makeView({ client, space, typename });
305
310
  space.db.add(view);
306
311
 
307
312
  // Populate.
@@ -315,7 +320,9 @@ export const Tags: Meta<StoryProps> = {
315
320
  });
316
321
  },
317
322
  }),
318
- withLayout({ fullscreen: true }),
319
- withTheme,
320
323
  ],
324
+ parameters: {
325
+ layout: 'fullscreen',
326
+ translations,
327
+ },
321
328
  };
@@ -2,7 +2,8 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type Schema } from 'effect/Schema';
5
+ import type * as Schema from 'effect/Schema';
6
+ import * as String from 'effect/String';
6
7
  import React, {
7
8
  type MouseEvent,
8
9
  type PropsWithChildren,
@@ -15,13 +16,8 @@ import React, {
15
16
  useState,
16
17
  } from 'react';
17
18
 
18
- import { type Client } from '@dxos/client';
19
- import { Filter } from '@dxos/echo';
20
- import { getValue } from '@dxos/echo-schema';
21
- import { invariant } from '@dxos/invariant';
22
19
  // TODO(wittjosiah): Remove dependency on react-client.
23
- import { getSpace } from '@dxos/react-client/echo';
24
- import { useTranslation } from '@dxos/react-ui';
20
+ import { type Client } from '@dxos/react-client';
25
21
  import { useAttention } from '@dxos/react-ui-attention';
26
22
  import {
27
23
  type DxGridElement,
@@ -34,19 +30,20 @@ import {
34
30
  gridSeparatorBlockEnd,
35
31
  gridSeparatorInlineEnd,
36
32
  } from '@dxos/react-ui-grid';
33
+ import { DxEditRequest } from '@dxos/react-ui-grid';
37
34
  import { mx } from '@dxos/react-ui-theme';
38
- import { isNotFalsy, safeParseInt } from '@dxos/util';
39
35
 
40
- import { ModalController, type TableModel, type TablePresentation } from '../../model';
41
- import { translationKey } from '../../translations';
36
+ import { type InsertRowResult, ModalController, type TableModel, type TablePresentation } from '../../model';
42
37
  import { tableButtons, tableControls } from '../../util';
43
- import { type TableCellEditorProps, TableValueEditor, createOption } from '../TableCellEditor';
38
+ import { type OnCreateHandler, type TableCellEditorProps, TableValueEditor } from '../TableCellEditor';
44
39
 
45
40
  import { ColumnActionsMenu } from './ColumnActionsMenu';
46
41
  import { ColumnSettings } from './ColumnSettings';
47
- import { CreateRefPanel } from './CreateRefPanel';
48
42
  import { RowActionsMenu } from './RowActionsMenu';
49
43
 
44
+ const columnDefault = { grid: { minSize: 80, maxSize: 640 } };
45
+ const rowDefault = { frozenRowsStart: { readonly: true, focusUnfurl: false } };
46
+
50
47
  //
51
48
  // Table.Root
52
49
  //
@@ -77,28 +74,34 @@ const TableRoot = ({ children, role = 'article' }: TableRootProps) => {
77
74
 
78
75
  export type TableController = {
79
76
  update?: (cell?: DxGridPosition) => void;
80
- focusDraft?: () => void;
77
+ handleInsertRowResult?: (insertRowResult?: InsertRowResult) => void;
81
78
  };
82
79
 
83
80
  export type TableMainProps = {
84
81
  model?: TableModel;
85
82
  presentation?: TablePresentation;
86
- schema?: Schema.AnyNoContext;
83
+ schema?: Schema.Schema.AnyNoContext;
87
84
  client?: Client;
88
85
  // TODO(burdon): Rename since attention isn't a useful concept here? Standardize across other components. Pass property into useAttention.
89
86
  ignoreAttention?: boolean;
87
+ onCreate?: OnCreateHandler;
90
88
  onRowClick?: (row: any) => void;
89
+ testId?: string;
91
90
  };
92
91
 
93
92
  const TableMain = forwardRef<TableController, TableMainProps>(
94
- ({ model, presentation, ignoreAttention, schema, client, onRowClick }, forwardedRef) => {
93
+ ({ model, presentation, ignoreAttention, schema, client, onCreate, onRowClick, testId }, forwardedRef) => {
95
94
  const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
96
95
  const { hasAttention } = useAttention(model?.id ?? 'table');
97
- const { t } = useTranslation(translationKey);
98
96
  const modals = useMemo(() => new ModalController(), []);
99
97
 
100
98
  const draftRowCount = model?.getDraftRowCount() ?? 0;
101
99
 
100
+ const handleSave = useCallback(() => {
101
+ dxGrid?.updateCells(true);
102
+ dxGrid?.requestUpdate();
103
+ }, [dxGrid]);
104
+
102
105
  const frozen = useMemo(() => {
103
106
  const noActionColumn =
104
107
  model?.features.dataEditable === false &&
@@ -107,7 +110,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
107
110
 
108
111
  return {
109
112
  frozenRowsStart: 1,
110
- frozenRowsEnd: draftRowCount,
113
+ frozenRowsEnd: Math.max(1, draftRowCount),
111
114
  frozenColsStart: model?.features.selection.enabled ? 1 : 0,
112
115
  frozenColsEnd: noActionColumn ? 0 : 1,
113
116
  };
@@ -125,6 +128,23 @@ const TableMain = forwardRef<TableController, TableMainProps>(
125
128
  dxGrid.getCells = getCells;
126
129
  }, [presentation, dxGrid, getCells]);
127
130
 
131
+ const handleInsertRowResult = useCallback(
132
+ (insertResult?: InsertRowResult) => {
133
+ if (insertResult === 'draft') {
134
+ requestAnimationFrame(() => {
135
+ dxGrid?.setFocus({ plane: 'frozenRowsEnd', col: 0, row: 0 });
136
+ dxGrid?.refocus();
137
+ });
138
+ } else {
139
+ requestAnimationFrame(() => {
140
+ dxGrid?.setFocus({ plane: 'grid', col: 0, row: model ? model.getRowCount() - 1 : 0 });
141
+ dxGrid?.refocus();
142
+ });
143
+ }
144
+ },
145
+ [model, dxGrid],
146
+ );
147
+
128
148
  /**
129
149
  * Provides an external controller that can be called to repaint the table.
130
150
  */
@@ -142,14 +162,9 @@ const TableMain = forwardRef<TableController, TableMainProps>(
142
162
  dxGrid.requestUpdate();
143
163
  }
144
164
  },
145
- focusDraft: () => {
146
- requestAnimationFrame(() => {
147
- dxGrid.setFocus({ plane: 'frozenRowsEnd', col: 0, row: 0 });
148
- dxGrid.refocus();
149
- });
150
- },
165
+ handleInsertRowResult,
151
166
  };
152
- }, [presentation, dxGrid]);
167
+ }, [presentation, dxGrid, model]);
153
168
 
154
169
  const handleSaveDraftRow = useCallback(
155
170
  (rowIndex = 0) => {
@@ -167,11 +182,16 @@ const TableMain = forwardRef<TableController, TableMainProps>(
167
182
 
168
183
  const handleGridClick = useCallback(
169
184
  (event: MouseEvent) => {
170
- const rowIndex = safeParseInt((event.target as HTMLElement).closest('[aria-rowindex]')?.ariaRowIndex ?? '');
171
- if (rowIndex != null) {
185
+ const cell = closestCell(event.target as HTMLElement);
186
+ if (cell) {
187
+ const { row: rowIndex, plane } = cell;
172
188
  if (onRowClick) {
173
- const row = model?.getRowAt(rowIndex);
174
- row && onRowClick(row);
189
+ if (plane === 'grid') {
190
+ const row = model?.getRowAt(rowIndex);
191
+ row && onRowClick(row);
192
+ } else {
193
+ onRowClick(cell);
194
+ }
175
195
  }
176
196
 
177
197
  if (model?.features.selection.enabled && model?.selection.selectionMode === 'single') {
@@ -257,15 +277,11 @@ const TableMain = forwardRef<TableController, TableMainProps>(
257
277
  const handleFocus = useCallback<NonNullable<TableCellEditorProps['onFocus']>>(
258
278
  (increment, delta, cell) => {
259
279
  if (dxGrid && model) {
260
- if (cell?.plane === 'grid' && cell?.row >= model.getRowCount() - 1) {
261
- if (draftRowCount < 1) {
262
- model.insertRow();
263
- }
264
- dxGrid.setFocus({ plane: 'frozenRowsEnd', col: 0, row: 0 });
280
+ if (cell?.plane === 'grid' && cell?.row >= model.getRowCount() - 1 && increment !== 'col') {
281
+ handleInsertRowResult(draftRowCount < 1 ? model.insertRow() : 'final');
265
282
  } else if (cell?.plane === 'frozenRowsEnd' && increment === 'row') {
266
283
  handleSaveDraftRow(cell.row);
267
- model.insertRow();
268
- dxGrid.setFocus({ plane: 'frozenRowsEnd', col: 0, row: 0 });
284
+ handleInsertRowResult(model.insertRow());
269
285
  } else {
270
286
  dxGrid.refocus(increment, delta);
271
287
  }
@@ -285,12 +301,64 @@ const TableMain = forwardRef<TableController, TableMainProps>(
285
301
  return;
286
302
  }
287
303
 
304
+ // Handle Meta+C (Copy) and Meta+V (Paste) commands
305
+ if (event.metaKey || event.ctrlKey) {
306
+ switch (event.key) {
307
+ case 'c': {
308
+ // Copy focused cell's text content to clipboard
309
+ try {
310
+ const cellData = model.getCellData(cell);
311
+ const textContent = cellData?.toString() ?? '';
312
+ void navigator.clipboard.writeText(textContent);
313
+ event.preventDefault();
314
+ } catch (error) {
315
+ console.warn('Failed to copy cell content:', error);
316
+ }
317
+ break;
318
+ }
319
+ case 'v': {
320
+ // Paste clipboard content to focused cell
321
+ event.preventDefault();
322
+ void navigator.clipboard.readText().then((clipboardText) => {
323
+ try {
324
+ // Attempt to set the cell's content to clipboard content
325
+ model.setCellData(cell, String.trim(clipboardText).replace(/[\n\r]+/, ' '));
326
+ handleSave();
327
+ } catch {
328
+ // If validation fails, emit a DxEditRequest event with initialContent from clipboard
329
+ // TODO(thure): Should `dx-grid` expose a method like this?
330
+ const cellElement = (event.target as HTMLElement).closest(
331
+ '[data-dx-grid-action="cell"]',
332
+ ) as HTMLElement;
333
+ if (cellElement) {
334
+ const rect = cellElement.getBoundingClientRect();
335
+ const editRequest = new DxEditRequest({
336
+ cellIndex: `${cell.plane},${cell.col},${cell.row}`,
337
+ cellBox: {
338
+ insetInlineStart: rect.left,
339
+ insetBlockStart: rect.top,
340
+ inlineSize: rect.width,
341
+ blockSize: rect.height,
342
+ },
343
+ cellElement,
344
+ initialContent: clipboardText,
345
+ });
346
+ cellElement.dispatchEvent(editRequest);
347
+ }
348
+ }
349
+ });
350
+ break;
351
+ }
352
+ }
353
+ }
354
+
288
355
  switch (event.key) {
289
356
  case 'Backspace':
290
357
  case 'Delete': {
291
358
  try {
292
359
  model.setCellData(cell, undefined);
293
360
  event.preventDefault();
361
+ handleSave();
294
362
  } catch {
295
363
  // Delete results in a validation error; don’t prevent default so dx-grid can emit an edit request.
296
364
  }
@@ -298,7 +366,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
298
366
  }
299
367
  }
300
368
  },
301
- [model],
369
+ [model, dxGrid, handleSave],
302
370
  );
303
371
 
304
372
  const handleAxisResize = useCallback<NonNullable<GridContentProps['onAxisResize']>>(
@@ -327,58 +395,6 @@ const TableMain = forwardRef<TableController, TableMainProps>(
327
395
  }
328
396
  }, [model, dxGrid]);
329
397
 
330
- // TODO(burdon): Factor out?
331
- // TODO(burdon): Generalize to handle other value types (e.g., enums).
332
- const handleQuery = useCallback<NonNullable<TableCellEditorProps['onQuery']>>(
333
- async ({ field, props }, text) => {
334
- if (model && props.referenceSchema && field.referencePath) {
335
- const space = getSpace(model.view);
336
- invariant(space);
337
-
338
- let schema;
339
- if (client) {
340
- schema = client.graph.schemaRegistry.getSchema(props.referenceSchema);
341
- }
342
- if (!schema) {
343
- schema = space.db.schemaRegistry.getSchema(props.referenceSchema);
344
- }
345
-
346
- if (schema) {
347
- const { objects } = await space.db.query(Filter.type(schema)).run();
348
- const options = objects
349
- .map((obj) => {
350
- const value = getValue(obj, field.referencePath!);
351
- if (!value || typeof value !== 'string') {
352
- return undefined;
353
- }
354
-
355
- return {
356
- label: value,
357
- data: obj,
358
- };
359
- })
360
- .filter(isNotFalsy);
361
-
362
- return [
363
- ...options,
364
- {
365
- label: t('create new object label', { text }),
366
- data: createOption(text),
367
- },
368
- ];
369
- }
370
- }
371
-
372
- return [];
373
- },
374
- [model, client, t],
375
- );
376
-
377
- const handleSave = useCallback(() => {
378
- dxGrid?.updateCells(true);
379
- dxGrid?.requestUpdate();
380
- }, [dxGrid]);
381
-
382
398
  if (!model || !modals) {
383
399
  return <span role='none' className='attention-surface' />;
384
400
  }
@@ -390,13 +406,16 @@ const TableMain = forwardRef<TableController, TableMainProps>(
390
406
  modals={modals}
391
407
  schema={schema}
392
408
  onFocus={handleFocus}
393
- onQuery={handleQuery}
394
409
  onSave={handleSave}
410
+ onCreate={onCreate}
411
+ client={client}
395
412
  />
396
413
  <Grid.Content
397
414
  className={mx('[--dx-grid-base:var(--baseSurface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
398
415
  frozen={frozen}
399
416
  columns={model.columnMeta.value}
417
+ columnDefault={columnDefault}
418
+ rowDefault={rowDefault}
400
419
  limitRows={model.getRowCount() ?? 0}
401
420
  limitColumns={model.projection.fields.length}
402
421
  overscroll='trap'
@@ -404,12 +423,12 @@ const TableMain = forwardRef<TableController, TableMainProps>(
404
423
  onClick={handleGridClick}
405
424
  onKeyDownCapture={handleKeyDown}
406
425
  onWheelCapture={handleWheel}
426
+ {...(testId && { 'data-testid': testId })}
407
427
  ref={setDxGrid}
408
428
  />
409
429
  <RowActionsMenu model={model} modals={modals} />
410
430
  <ColumnActionsMenu model={model} modals={modals} />
411
431
  <ColumnSettings model={model} modals={modals} onNewColumn={handleNewColumn} />
412
- <CreateRefPanel model={model} modals={modals} />
413
432
  </Grid.Root>
414
433
  );
415
434
  },