@dxos/react-ui-table 0.8.4-main.f9ba587 → 0.8.4-main.fd6878d

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 (122) hide show
  1. package/dist/lib/browser/chunk-L2LSB6KV.mjs +18 -0
  2. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-ZAOSIG3L.mjs +1814 -0
  4. package/dist/lib/browser/chunk-ZAOSIG3L.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +658 -2359
  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 +123 -0
  9. package/dist/lib/browser/testing/index.mjs.map +7 -0
  10. package/dist/lib/browser/types/index.mjs +3 -5
  11. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +20 -0
  12. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-ZOFFI3JJ.mjs +1815 -0
  14. package/dist/lib/node-esm/chunk-ZOFFI3JJ.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +658 -2359
  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 +124 -0
  19. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  20. package/dist/lib/node-esm/types/index.mjs +3 -5
  21. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -1
  22. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/ColumnSettings.d.ts +4 -2
  24. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/CreateRefPanel.d.ts +1 -1
  26. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.d.ts +1 -1
  28. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +1 -1
  30. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/{relations.stories.d.ts → Relations.stories.d.ts} +2 -2
  32. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
  33. package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
  34. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  35. package/dist/types/src/components/Table/Table.d.ts +1 -0
  36. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  37. package/dist/types/src/components/Table/Table.stories.d.ts +1 -1
  38. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  40. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +2 -1
  41. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  42. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +8 -7
  43. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  44. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  45. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +1 -1
  46. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  47. package/dist/types/src/components/testing.d.ts +3 -40
  48. package/dist/types/src/components/testing.d.ts.map +1 -1
  49. package/dist/types/src/hooks/useTableModel.d.ts +8 -7
  50. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  51. package/dist/types/src/index.d.ts +0 -1
  52. package/dist/types/src/index.d.ts.map +1 -1
  53. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  54. package/dist/types/src/model/selection-model.d.ts +2 -1
  55. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  56. package/dist/types/src/model/table-model.d.ts +17 -16
  57. package/dist/types/src/model/table-model.d.ts.map +1 -1
  58. package/dist/types/src/model/table-presentation.d.ts +2 -1
  59. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  60. package/dist/types/src/model/table-sorting.d.ts +4 -4
  61. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  62. package/dist/types/src/testing/index.d.ts +2 -0
  63. package/dist/types/src/testing/index.d.ts.map +1 -0
  64. package/dist/types/src/testing/useTestTableModel.d.ts +41 -0
  65. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -0
  66. package/dist/types/src/translations.d.ts +2 -2
  67. package/dist/types/src/types/index.d.ts +1 -1
  68. package/dist/types/src/types/index.d.ts.map +1 -1
  69. package/dist/types/src/types/schema.d.ts +8 -0
  70. package/dist/types/src/types/schema.d.ts.map +1 -0
  71. package/dist/types/src/util/dynamic-table.d.ts +3 -4
  72. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  73. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  74. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  75. package/dist/types/src/util/table.d.ts +9 -11
  76. package/dist/types/src/util/table.d.ts.map +1 -1
  77. package/dist/types/tsconfig.tsbuildinfo +1 -1
  78. package/package.json +50 -36
  79. package/src/components/Table/ColumnActionsMenu.tsx +1 -1
  80. package/src/components/Table/ColumnSettings.tsx +15 -11
  81. package/src/components/Table/CreateRefPanel.tsx +6 -4
  82. package/src/components/Table/DynamicTable.stories.tsx +3 -2
  83. package/src/components/Table/DynamicTable.tsx +17 -10
  84. package/src/components/Table/{relations.stories.tsx → Relations.stories.tsx} +36 -35
  85. package/src/components/Table/RowActionsMenu.tsx +3 -3
  86. package/src/components/Table/Table.stories.tsx +99 -144
  87. package/src/components/Table/Table.tsx +87 -71
  88. package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
  89. package/src/components/TableCellEditor/FormCellEditor.tsx +30 -21
  90. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +24 -27
  91. package/src/components/TableCellEditor/TableCellEditor.tsx +42 -27
  92. package/src/components/TableCellEditor/extension.ts +2 -2
  93. package/src/components/TableToolbar/TableToolbar.tsx +2 -2
  94. package/src/components/testing.ts +7 -24
  95. package/src/hooks/useTableModel.ts +21 -17
  96. package/src/index.ts +0 -1
  97. package/src/model/modal-controller.ts +1 -1
  98. package/src/model/selection-model.ts +6 -3
  99. package/src/model/table-model.test.ts +8 -8
  100. package/src/model/table-model.ts +68 -62
  101. package/src/model/table-presentation.test.ts +8 -8
  102. package/src/model/table-presentation.ts +35 -13
  103. package/src/model/table-sorting.ts +11 -11
  104. package/src/playwright/smoke.spec.ts +105 -14
  105. package/src/testing/index.ts +5 -0
  106. package/src/testing/useTestTableModel.ts +123 -0
  107. package/src/translations.ts +2 -2
  108. package/src/types/index.ts +2 -2
  109. package/src/types/schema.ts +16 -0
  110. package/src/util/dynamic-table.ts +20 -18
  111. package/src/util/schema.ts +1 -1
  112. package/src/util/table-buttons.ts +2 -1
  113. package/src/util/table-controls.ts +4 -1
  114. package/src/util/table.ts +37 -111
  115. package/dist/lib/browser/chunk-Q5QQXJUO.mjs +0 -27
  116. package/dist/lib/browser/chunk-Q5QQXJUO.mjs.map +0 -7
  117. package/dist/lib/node-esm/chunk-ZNOU6LHG.mjs +0 -29
  118. package/dist/lib/node-esm/chunk-ZNOU6LHG.mjs.map +0 -7
  119. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  120. package/dist/types/src/types/table.d.ts +0 -56
  121. package/dist/types/src/types/table.d.ts.map +0 -1
  122. package/src/types/table.ts +0 -27
@@ -4,150 +4,91 @@
4
4
 
5
5
  import '@dxos-theme';
6
6
 
7
- import { type StoryObj, type Meta } from '@storybook/react-vite';
7
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
8
8
  import { Schema } from 'effect';
9
- import React, { useCallback, useMemo, useRef } from 'react';
9
+ import React, { useCallback } from 'react';
10
10
 
11
11
  import { Obj, Type } from '@dxos/echo';
12
- import { FormatEnum, isMutable, toJsonSchema, EchoObject, GeneratorAnnotation } from '@dxos/echo-schema';
12
+ import {
13
+ EchoObject,
14
+ FormatAnnotation,
15
+ FormatEnum,
16
+ GeneratorAnnotation,
17
+ LabelAnnotation,
18
+ PropertyMetaAnnotationId,
19
+ } from '@dxos/echo-schema';
13
20
  import { invariant } from '@dxos/invariant';
14
- import { useGlobalFilteredObjects } from '@dxos/plugin-search';
15
21
  import { faker } from '@dxos/random';
16
- import { useClient } from '@dxos/react-client';
17
- import { Filter, useQuery, useSchema, live } from '@dxos/react-client/echo';
18
- import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
22
+ import { PublicKey } from '@dxos/react-client';
23
+ import { type Space, live } from '@dxos/react-client/echo';
24
+ import { withClientProvider } from '@dxos/react-client/testing';
19
25
  import { ViewEditor } from '@dxos/react-ui-form';
20
26
  import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
21
- import { getSchemaFromPropertyDefinitions, ViewProjection, ViewType } from '@dxos/schema';
27
+ import { DataType, getSchemaFromPropertyDefinitions } from '@dxos/schema';
22
28
  import { Testing, createObjectFactory } from '@dxos/schema/testing';
23
29
  import { withLayout, withTheme } from '@dxos/storybook-utils';
24
30
 
25
- import { Table, type TableController } from './Table';
26
- import { useTableModel, useAddRow } from '../../hooks';
27
- import { TablePresentation } from '../../model';
31
+ import { useTestTableModel } from '../../testing';
28
32
  import { translations } from '../../translations';
29
- import { TableType } from '../../types';
30
- import { initializeTable } from '../../util';
33
+ import { TableView } from '../../types';
34
+ import { createTable } from '../../util';
31
35
  import { TableToolbar } from '../TableToolbar';
32
36
 
33
- faker.seed(0); // NOTE(ZaymonFC): Required for smoke tests.
34
-
35
- /**
36
- * Custom hook to create and manage a test table model for storybook demonstrations.
37
- * Provides table data, schema, and handlers for table operations.
38
- */
39
- const useTestTableModel = () => {
40
- const client = useClient();
41
- const { space } = useClientProvider();
42
-
43
- const filter = useMemo(() => Filter.type(TableType), []);
44
- const tables = useQuery(space, filter);
45
- const table = useMemo(() => tables.at(0), [tables]);
46
- const schema = useSchema(client, space, table?.view?.target?.query.typename);
47
-
48
- const projection = useMemo(() => {
49
- if (schema && table?.view?.target) {
50
- return new ViewProjection(toJsonSchema(schema), table.view.target);
51
- }
52
- }, [schema, table?.view?.target]);
53
-
54
- const features = useMemo(
55
- () => ({
56
- selection: { enabled: true, mode: 'multiple' as const },
57
- dataEditable: true,
58
- schemaEditable: schema && isMutable(schema),
59
- }),
60
- [schema],
61
- );
62
-
63
- const objects = useQuery(space, schema ? Filter.type(schema) : Filter.nothing());
64
- const filteredObjects = useGlobalFilteredObjects(objects);
65
-
66
- const tableRef = useRef<TableController>(null);
67
- const handleCellUpdate = useCallback((cell: any) => {
68
- tableRef.current?.update?.(cell);
69
- }, []);
70
-
71
- const handleRowOrderChange = useCallback(() => {
72
- tableRef.current?.update?.();
73
- }, []);
74
-
75
- const addRow = useAddRow({ space, schema });
76
-
77
- const handleDeleteRows = useCallback(
78
- (_: number, objects: any[]) => {
79
- for (const object of objects) {
80
- space?.db.remove(object);
81
- }
82
- },
83
- [space],
84
- );
85
-
86
- const handleDeleteColumn = useCallback(
87
- (fieldId: string) => {
88
- if (projection) {
89
- projection.deleteFieldProjection(fieldId);
90
- }
91
- },
92
- [projection],
93
- );
94
-
95
- const model = useTableModel({
96
- table,
97
- projection,
98
- features,
99
- rows: filteredObjects,
100
- onInsertRow: addRow,
101
- onDeleteRows: handleDeleteRows,
102
- onDeleteColumn: handleDeleteColumn,
103
- onCellUpdate: handleCellUpdate,
104
- onRowOrderChange: handleRowOrderChange,
105
- });
106
-
107
- const handleInsertRow = useCallback(() => {
108
- model?.insertRow();
109
- }, [model]);
110
-
111
- const handleSaveView = useCallback(() => {
112
- model?.saveView();
113
- }, [model]);
114
-
115
- const presentation = useMemo(() => {
116
- if (model) {
117
- return new TablePresentation(model);
118
- }
119
- }, [model]);
120
-
121
- return {
122
- schema,
123
- table,
124
- tableRef,
125
- model,
126
- presentation,
127
- space,
128
- client,
129
- handleInsertRow,
130
- handleSaveView,
131
- handleDeleteRows,
132
- handleDeleteColumn,
133
- };
134
- };
135
-
136
- const StoryViewEditor = () => {
137
- const { table, space, schema, handleDeleteColumn } = useTestTableModel();
138
-
37
+ import { Table } from './Table';
38
+
39
+ const TestSchema = Schema.Struct({
40
+ // TODO(wittjosiah): Should be title. Currently name to work with default label.
41
+ name: Schema.optional(Schema.String).annotations({ title: 'Title' }),
42
+ urgent: Schema.optional(Schema.Boolean).annotations({ title: 'Urgent' }),
43
+ status: Schema.optional(
44
+ Schema.Literal('todo', 'in-progress', 'done')
45
+ .pipe(FormatAnnotation.set(FormatEnum.SingleSelect))
46
+ .annotations({
47
+ title: 'Status',
48
+ [PropertyMetaAnnotationId]: {
49
+ singleSelect: {
50
+ options: [
51
+ { id: 'todo', title: 'Todo', color: 'indigo' },
52
+ { id: 'in-progress', title: 'In Progress', color: 'purple' },
53
+ { id: 'done', title: 'Done', color: 'amber' },
54
+ ],
55
+ },
56
+ },
57
+ }),
58
+ ),
59
+ description: Schema.optional(Schema.String).annotations({ title: 'Description' }),
60
+ parent: Schema.optional(Schema.suspend((): Type.Ref<TestSchema> => Type.Ref(TestSchema))).annotations({
61
+ title: 'Parent',
62
+ }),
63
+ }).pipe(
64
+ Type.Obj({ typename: `example.com/type/${PublicKey.random().truncate()}`, version: '0.1.0' }),
65
+ LabelAnnotation.set(['name']),
66
+ );
67
+ interface TestSchema extends Schema.Schema.Type<typeof TestSchema> {}
68
+
69
+ const StoryViewEditor = ({
70
+ view,
71
+ schema,
72
+ space,
73
+ handleDeleteColumn,
74
+ }: {
75
+ view?: DataType.View;
76
+ schema?: Schema.Schema.AnyNoContext;
77
+ space?: Space;
78
+ handleDeleteColumn: (fieldId: string) => void;
79
+ }) => {
139
80
  const handleTypenameChanged = useCallback(
140
81
  (typename: string) => {
141
82
  invariant(schema);
142
83
  invariant(Type.isMutable(schema));
143
84
  schema.updateTypename(typename);
144
- invariant(table?.view?.target);
145
- table.view.target.query.typename = typename;
85
+ invariant(view);
86
+ view.query.typename = typename;
146
87
  },
147
- [schema, table?.view?.target],
88
+ [schema, view],
148
89
  );
149
90
 
150
- if (!table || !schema || !table.view?.target) {
91
+ if (!view || !schema) {
151
92
  return null;
152
93
  }
153
94
 
@@ -155,7 +96,7 @@ const StoryViewEditor = () => {
155
96
  <ViewEditor
156
97
  registry={space?.db.schemaRegistry}
157
98
  schema={schema}
158
- view={table.view.target!}
99
+ view={view}
159
100
  onTypenameChanged={handleTypenameChanged}
160
101
  onDelete={handleDeleteColumn}
161
102
  />
@@ -167,9 +108,20 @@ const StoryViewEditor = () => {
167
108
  //
168
109
 
169
110
  const DefaultStory = () => {
170
- const { schema, table, tableRef, model, presentation, client, handleInsertRow, handleSaveView } = useTestTableModel();
111
+ const {
112
+ space,
113
+ schema,
114
+ view,
115
+ tableRef,
116
+ model,
117
+ presentation,
118
+ client,
119
+ handleInsertRow,
120
+ handleSaveView,
121
+ handleDeleteColumn,
122
+ } = useTestTableModel();
171
123
 
172
- if (!schema || !table) {
124
+ if (!schema || !view) {
173
125
  return <div />;
174
126
  }
175
127
 
@@ -189,9 +141,9 @@ const DefaultStory = () => {
189
141
  </Table.Root>
190
142
  </div>
191
143
  <div className='flex flex-col h-full border-l border-separator overflow-y-auto'>
192
- <StoryViewEditor />
144
+ <StoryViewEditor view={view} schema={schema} space={space} handleDeleteColumn={handleDeleteColumn} />
193
145
  <SyntaxHighlighter language='json' className='w-full text-xs'>
194
- {JSON.stringify({ view: table.view?.target, schema }, null, 2)}
146
+ {JSON.stringify({ view, schema }, null, 2)}
195
147
  </SyntaxHighlighter>
196
148
  </div>
197
149
  </div>
@@ -220,16 +172,20 @@ const meta: Meta<StoryProps> = {
220
172
  withTheme,
221
173
  withLayout({ fullscreen: true }),
222
174
  withClientProvider({
223
- types: [TableType, ViewType],
175
+ types: [DataType.View, TableView],
224
176
  createIdentity: true,
225
177
  createSpace: true,
226
178
  onSpaceCreated: async ({ client, space }) => {
227
- const table = space.db.add(Obj.make(TableType, {}));
228
- const schema = await initializeTable({ client, space, table, initialRow: false });
179
+ const [schema] = await space.db.schemaRegistry.register([TestSchema]);
180
+ const { view } = await createTable({ client, space, typename: schema.typename });
181
+ space.db.add(view);
182
+
229
183
  Array.from({ length: 10 }).map(() => {
230
184
  return space.db.add(
231
- live(schema, {
232
- name: faker.person.fullName(),
185
+ Obj.make(schema, {
186
+ name: faker.lorem.sentence(),
187
+ status: faker.helpers.arrayElement(['todo', 'in-progress', 'done'] as const),
188
+ description: faker.lorem.paragraph(),
233
189
  }),
234
190
  );
235
191
  });
@@ -247,18 +203,17 @@ export const StaticSchema: StoryObj = {
247
203
  parameters: { translations },
248
204
  decorators: [
249
205
  withClientProvider({
250
- types: [TableType, ViewType, Testing.Contact, Testing.Organization],
206
+ types: [DataType.View, TableView, Testing.Contact, Testing.Organization],
251
207
  createIdentity: true,
252
208
  createSpace: true,
253
209
  onSpaceCreated: async ({ client, space }) => {
254
- const table = space.db.add(Obj.make(TableType, {}));
255
- await initializeTable({ client, space, table, typename: Testing.Organization.typename });
210
+ const { view } = await createTable({ client, space, typename: Testing.Contact.typename });
211
+ space.db.add(view);
256
212
 
257
213
  const factory = createObjectFactory(space.db, faker as any);
258
214
  await factory([
259
- // { type: Testing.Contact, count: 10 },
215
+ { type: Testing.Contact, count: 10 },
260
216
  // { type: Testing.Organization, count: 1 },
261
- { type: ContactWithArrayOfEmails, count: 10 },
262
217
  ]);
263
218
  },
264
219
  }),
@@ -289,12 +244,12 @@ export const ArrayOfObjects: StoryObj = {
289
244
  parameters: { translations },
290
245
  decorators: [
291
246
  withClientProvider({
292
- types: [TableType, ViewType, Testing.Contact, Testing.Organization, ContactWithArrayOfEmails],
247
+ types: [DataType.View, TableView, Testing.Contact, Testing.Organization, ContactWithArrayOfEmails],
293
248
  createIdentity: true,
294
249
  createSpace: true,
295
250
  onSpaceCreated: async ({ client, space }) => {
296
- const table = space.db.add(Obj.make(TableType, {}));
297
- await initializeTable({ client, space, table, typename: ContactWithArrayOfEmails.typename });
251
+ const { view } = await createTable({ client, space, typename: ContactWithArrayOfEmails.typename });
252
+ space.db.add(view);
298
253
 
299
254
  const factory = createObjectFactory(space.db, faker as any);
300
255
  await factory([
@@ -315,7 +270,7 @@ export const Tags: Meta<StoryProps> = {
315
270
  parameters: { translations },
316
271
  decorators: [
317
272
  withClientProvider({
318
- types: [TableType, ViewType],
273
+ types: [DataType.View, TableView],
319
274
  createIdentity: true,
320
275
  createSpace: true,
321
276
  onSpaceCreated: async ({ client, space }) => {
@@ -346,8 +301,8 @@ export const Tags: Meta<StoryProps> = {
346
301
  const [storedSchema] = await space.db.schemaRegistry.register([schema]);
347
302
 
348
303
  // Initialize table.
349
- const table = space.db.add(Obj.make(TableType, {}));
350
- await initializeTable({ client, space, table, initialRow: false, typename });
304
+ const { view } = await createTable({ client, space, typename });
305
+ space.db.add(view);
351
306
 
352
307
  // Populate.
353
308
  Array.from({ length: 10 }).map(() => {
@@ -9,40 +9,43 @@ import React, {
9
9
  type WheelEvent,
10
10
  forwardRef,
11
11
  useCallback,
12
+ useEffect,
12
13
  useImperativeHandle,
13
- useState,
14
14
  useMemo,
15
- useEffect,
15
+ useState,
16
16
  } from 'react';
17
17
 
18
18
  import { type Client } from '@dxos/client';
19
+ import { Filter } from '@dxos/echo';
19
20
  import { getValue } from '@dxos/echo-schema';
20
21
  import { invariant } from '@dxos/invariant';
21
- import { Filter } from '@dxos/react-client/echo';
22
+ // TODO(wittjosiah): Remove dependency on react-client.
23
+ import { getSpace } from '@dxos/react-client/echo';
22
24
  import { useTranslation } from '@dxos/react-ui';
23
25
  import { useAttention } from '@dxos/react-ui-attention';
24
26
  import {
25
- closestCell,
26
27
  type DxGridElement,
27
- type DxGridPosition,
28
- type GridContentProps,
29
- Grid,
30
28
  type DxGridPlane,
31
29
  type DxGridPlaneRange,
32
- gridSeparatorInlineEnd,
30
+ type DxGridPosition,
31
+ Grid,
32
+ type GridContentProps,
33
+ closestCell,
33
34
  gridSeparatorBlockEnd,
35
+ gridSeparatorInlineEnd,
34
36
  } from '@dxos/react-ui-grid';
35
37
  import { mx } from '@dxos/react-ui-theme';
36
38
  import { isNotFalsy, safeParseInt } from '@dxos/util';
37
39
 
40
+ import { ModalController, type TableModel, type TablePresentation } from '../../model';
41
+ import { translationKey } from '../../translations';
42
+ import { tableButtons, tableControls } from '../../util';
43
+ import { type TableCellEditorProps, TableValueEditor, createOption } from '../TableCellEditor';
44
+
38
45
  import { ColumnActionsMenu } from './ColumnActionsMenu';
39
46
  import { ColumnSettings } from './ColumnSettings';
40
47
  import { CreateRefPanel } from './CreateRefPanel';
41
48
  import { RowActionsMenu } from './RowActionsMenu';
42
- import { ModalController, type TableModel, type TablePresentation } from '../../model';
43
- import { translationKey } from '../../translations';
44
- import { tableButtons, tableControls } from '../../util';
45
- import { createOption, TableValueEditor, type TableCellEditorProps } from '../TableCellEditor';
46
49
 
47
50
  //
48
51
  // Table.Root
@@ -50,15 +53,17 @@ import { createOption, TableValueEditor, type TableCellEditorProps } from '../Ta
50
53
 
51
54
  export type TableRootProps = PropsWithChildren<{ role?: string }>;
52
55
 
53
- const TableRoot = ({ children, role }: TableRootProps) => {
56
+ const TableRoot = ({ children, role = 'article' }: TableRootProps) => {
54
57
  return (
55
58
  <div
56
59
  role='none'
57
60
  className={mx(
58
- 'relative !border-separator',
59
- role === 'section' // TODO(burdon): This leaks composer plugin concepts? Standardize for react-ui?
60
- ? 'attention-surface overflow-hidden [&_.dx-grid]:max-is-[--dx-grid-content-inline-size]'
61
- : 'flex flex-col [&_.dx-grid]:grow [&_.dx-grid]:max-is-[--dx-grid-content-inline-size] [&_.dx-grid]:bs-0 [&_.dx-grid]:max-bs-[--dx-grid-content-block-size]',
61
+ 'relative !border-separator [&_.dx-grid]:max-is-[--dx-grid-content-inline-size] [&_.dx-grid]:max-bs-[--dx-grid-content-block-size]',
62
+ role === 'card--popover' && 'popover-card-height',
63
+ role === 'section' && 'attention-surface',
64
+ role === 'card--intrinsic' && '[&_.dx-grid]:bs-[--dx-grid-content-block-size]',
65
+ ['card--popover', 'section', 'card--extrinsic'].includes(role) && 'overflow-hidden',
66
+ ['article', 'slide'].includes(role) && 'flex flex-col [&_.dx-grid]:grow [&_.dx-grid]:bs-0',
62
67
  )}
63
68
  >
64
69
  {children}
@@ -72,6 +77,7 @@ const TableRoot = ({ children, role }: TableRootProps) => {
72
77
 
73
78
  export type TableController = {
74
79
  update?: (cell?: DxGridPosition) => void;
80
+ focusDraft?: () => void;
75
81
  };
76
82
 
77
83
  export type TableMainProps = {
@@ -122,30 +128,46 @@ const TableMain = forwardRef<TableController, TableMainProps>(
122
128
  /**
123
129
  * Provides an external controller that can be called to repaint the table.
124
130
  */
125
- useImperativeHandle<TableController, TableController>(
126
- forwardedRef,
127
- () => {
128
- if (!presentation || !dxGrid) {
129
- return {};
130
- }
131
+ useImperativeHandle<TableController, TableController>(forwardedRef, () => {
132
+ if (!presentation || !dxGrid) {
133
+ return {};
134
+ }
131
135
 
132
- return {
133
- update: (cell) => {
134
- if (cell) {
135
- dxGrid.updateIfWithinBounds(cell);
136
- } else {
137
- dxGrid.updateCells(true);
138
- dxGrid.requestUpdate();
139
- }
140
- },
141
- };
136
+ return {
137
+ update: (cell) => {
138
+ if (cell) {
139
+ dxGrid.updateIfWithinBounds(cell, true);
140
+ } else {
141
+ dxGrid.updateCells(true);
142
+ dxGrid.requestUpdate();
143
+ }
144
+ },
145
+ focusDraft: () => {
146
+ requestAnimationFrame(() => {
147
+ dxGrid.setFocus({ plane: 'frozenRowsEnd', col: 0, row: 0 });
148
+ dxGrid.refocus();
149
+ });
150
+ },
151
+ };
152
+ }, [presentation, dxGrid]);
153
+
154
+ const handleSaveDraftRow = useCallback(
155
+ (rowIndex = 0) => {
156
+ if (model && dxGrid) {
157
+ const didCommitSuccessfully = model.commitDraftRow(rowIndex);
158
+ if (didCommitSuccessfully) {
159
+ requestAnimationFrame(() => {
160
+ dxGrid.scrollToEndRow();
161
+ });
162
+ }
163
+ }
142
164
  },
143
- [presentation, dxGrid],
165
+ [model, dxGrid],
144
166
  );
145
167
 
146
168
  const handleGridClick = useCallback(
147
169
  (event: MouseEvent) => {
148
- const rowIndex = safeParseInt((event.target as HTMLElement).ariaRowIndex ?? '');
170
+ const rowIndex = safeParseInt((event.target as HTMLElement).closest('[aria-rowindex]')?.ariaRowIndex ?? '');
149
171
  if (rowIndex != null) {
150
172
  if (onRowClick) {
151
173
  const row = model?.getRowAt(rowIndex);
@@ -192,14 +214,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
192
214
  break;
193
215
  }
194
216
  case 'saveDraftRow': {
195
- if (model) {
196
- const didCommitSuccessfully = model.commitDraftRow(data.rowIndex);
197
- if (dxGrid && didCommitSuccessfully) {
198
- requestAnimationFrame(() => {
199
- dxGrid.scrollToEndRow();
200
- });
201
- }
202
- }
217
+ handleSaveDraftRow();
203
218
  break;
204
219
  }
205
220
  }
@@ -240,32 +255,23 @@ const TableMain = forwardRef<TableController, TableMainProps>(
240
255
  );
241
256
 
242
257
  const handleFocus = useCallback<NonNullable<TableCellEditorProps['onFocus']>>(
243
- (increment, delta) => {
244
- if (dxGrid) {
245
- dxGrid.refocus(increment, delta);
246
- }
247
- },
248
- [dxGrid],
249
- );
250
-
251
- const handleEnter = useCallback<NonNullable<TableCellEditorProps['onEnter']>>(
252
- (cell) => {
253
- if (!model?.features.dataEditable) {
254
- return;
255
- }
256
-
257
- // TODO(burdon): Insert row only if bottom row isn't completely blank already.
258
- if (model && cell.row === model.getRowCount() - 1) {
259
- model.insertRow();
260
- if (dxGrid) {
261
- requestAnimationFrame(() => {
262
- dxGrid?.scrollToRow(cell.row + 1);
263
- dxGrid?.refocus('row', 1);
264
- });
258
+ (increment, delta, cell) => {
259
+ 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 });
265
+ } else if (cell?.plane === 'frozenRowsEnd' && increment === 'row') {
266
+ handleSaveDraftRow(cell.row);
267
+ model.insertRow();
268
+ dxGrid.setFocus({ plane: 'frozenRowsEnd', col: 0, row: 0 });
269
+ } else {
270
+ dxGrid.refocus(increment, delta);
265
271
  }
266
272
  }
267
273
  },
268
- [model, dxGrid],
274
+ [dxGrid, model],
269
275
  );
270
276
 
271
277
  const handleKeyDown = useCallback<NonNullable<GridContentProps['onKeyDown']>>(
@@ -282,7 +288,12 @@ const TableMain = forwardRef<TableController, TableMainProps>(
282
288
  switch (event.key) {
283
289
  case 'Backspace':
284
290
  case 'Delete': {
285
- model.setCellData(cell, undefined);
291
+ try {
292
+ model.setCellData(cell, undefined);
293
+ event.preventDefault();
294
+ } catch {
295
+ // Delete results in a validation error; don’t prevent default so dx-grid can emit an edit request.
296
+ }
286
297
  break;
287
298
  }
288
299
  }
@@ -321,7 +332,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
321
332
  const handleQuery = useCallback<NonNullable<TableCellEditorProps['onQuery']>>(
322
333
  async ({ field, props }, text) => {
323
334
  if (model && props.referenceSchema && field.referencePath) {
324
- const space = model.space;
335
+ const space = getSpace(model.view);
325
336
  invariant(space);
326
337
 
327
338
  let schema;
@@ -363,6 +374,11 @@ const TableMain = forwardRef<TableController, TableMainProps>(
363
374
  [model, client, t],
364
375
  );
365
376
 
377
+ const handleSave = useCallback(() => {
378
+ dxGrid?.updateCells(true);
379
+ dxGrid?.requestUpdate();
380
+ }, [dxGrid]);
381
+
366
382
  if (!model || !modals) {
367
383
  return <span role='none' className='attention-surface' />;
368
384
  }
@@ -373,20 +389,20 @@ const TableMain = forwardRef<TableController, TableMainProps>(
373
389
  model={model}
374
390
  modals={modals}
375
391
  schema={schema}
376
- onEnter={handleEnter}
377
392
  onFocus={handleFocus}
378
393
  onQuery={handleQuery}
394
+ onSave={handleSave}
379
395
  />
380
396
  <Grid.Content
381
397
  className={mx('[--dx-grid-base:var(--baseSurface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
382
398
  frozen={frozen}
383
399
  columns={model.columnMeta.value}
384
400
  limitRows={model.getRowCount() ?? 0}
385
- limitColumns={model.view?.fields?.length ?? 0}
401
+ limitColumns={model.projection.fields.length}
386
402
  overscroll='trap'
387
403
  onAxisResize={handleAxisResize}
388
404
  onClick={handleGridClick}
389
- onKeyDown={handleKeyDown}
405
+ onKeyDownCapture={handleKeyDown}
390
406
  onWheelCapture={handleWheel}
391
407
  ref={setDxGrid}
392
408
  />
@@ -4,7 +4,7 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { useGridContext, type GridScopedProps } from '@dxos/react-ui-grid';
7
+ import { type GridScopedProps, useGridContext } from '@dxos/react-ui-grid';
8
8
 
9
9
  export type CellValidationMessageProps = {
10
10
  validationError: string | null;