@dxos/react-ui-table 0.8.3-staging.0fa589b → 0.8.4-main.28f8d3d

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 (143) 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 +728 -2104
  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 +728 -2104
  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 +2 -1
  40. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  41. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +2 -1
  42. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  43. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +8 -7
  44. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  45. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +1 -1
  46. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  47. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +1 -1
  48. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  49. package/dist/types/src/components/testing.d.ts +3 -40
  50. package/dist/types/src/components/testing.d.ts.map +1 -1
  51. package/dist/types/src/hooks/index.d.ts +1 -0
  52. package/dist/types/src/hooks/index.d.ts.map +1 -1
  53. package/dist/types/src/hooks/useAddRow.d.ts +13 -0
  54. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -0
  55. package/dist/types/src/hooks/useTableModel.d.ts +8 -7
  56. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  57. package/dist/types/src/index.d.ts +1 -2
  58. package/dist/types/src/index.d.ts.map +1 -1
  59. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  60. package/dist/types/src/model/selection-model.d.ts +2 -1
  61. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  62. package/dist/types/src/model/table-model.d.ts +58 -21
  63. package/dist/types/src/model/table-model.d.ts.map +1 -1
  64. package/dist/types/src/model/table-presentation.d.ts +10 -1
  65. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  66. package/dist/types/src/model/table-sorting.d.ts +4 -4
  67. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  68. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  69. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  70. package/dist/types/src/testing/index.d.ts +2 -0
  71. package/dist/types/src/testing/index.d.ts.map +1 -0
  72. package/dist/types/src/testing/useTestTableModel.d.ts +41 -0
  73. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -0
  74. package/dist/types/src/translations.d.ts +5 -6
  75. package/dist/types/src/translations.d.ts.map +1 -1
  76. package/dist/types/src/types/index.d.ts +1 -1
  77. package/dist/types/src/types/index.d.ts.map +1 -1
  78. package/dist/types/src/types/schema.d.ts +8 -0
  79. package/dist/types/src/types/schema.d.ts.map +1 -0
  80. package/dist/types/src/util/dynamic-table.d.ts +3 -4
  81. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  82. package/dist/types/src/util/table-buttons.d.ts +15 -1
  83. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  84. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  85. package/dist/types/src/util/table.d.ts +9 -11
  86. package/dist/types/src/util/table.d.ts.map +1 -1
  87. package/dist/types/tsconfig.tsbuildinfo +1 -1
  88. package/package.json +52 -40
  89. package/src/components/Table/ColumnActionsMenu.tsx +1 -1
  90. package/src/components/Table/ColumnSettings.tsx +15 -11
  91. package/src/components/Table/CreateRefPanel.tsx +6 -4
  92. package/src/components/Table/DynamicTable.stories.tsx +4 -3
  93. package/src/components/Table/DynamicTable.tsx +17 -10
  94. package/src/components/Table/{relations.stories.tsx → Relations.stories.tsx} +37 -37
  95. package/src/components/Table/RowActionsMenu.tsx +3 -3
  96. package/src/components/Table/Table.stories.tsx +109 -148
  97. package/src/components/Table/Table.tsx +95 -66
  98. package/src/components/TableCellEditor/CellValidationMessage.tsx +7 -2
  99. package/src/components/TableCellEditor/FormCellEditor.tsx +30 -21
  100. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +27 -30
  101. package/src/components/TableCellEditor/TableCellEditor.tsx +88 -34
  102. package/src/components/TableCellEditor/extension.ts +2 -2
  103. package/src/components/TableToolbar/TableToolbar.tsx +2 -2
  104. package/src/components/testing.ts +7 -24
  105. package/src/hooks/index.ts +1 -0
  106. package/src/hooks/useAddRow.ts +36 -0
  107. package/src/hooks/useTableModel.ts +21 -17
  108. package/src/index.ts +1 -2
  109. package/src/model/modal-controller.ts +1 -1
  110. package/src/model/selection-model.ts +6 -3
  111. package/src/model/table-model.test.ts +8 -8
  112. package/src/model/table-model.ts +278 -86
  113. package/src/model/table-presentation.test.ts +8 -8
  114. package/src/model/table-presentation.ts +240 -135
  115. package/src/model/table-sorting.ts +11 -11
  116. package/src/playwright/playwright.config.ts +17 -0
  117. package/src/playwright/smoke.spec.ts +113 -20
  118. package/src/testing/index.ts +5 -0
  119. package/src/testing/useTestTableModel.ts +123 -0
  120. package/src/translations.ts +7 -5
  121. package/src/types/index.ts +2 -2
  122. package/src/types/schema.ts +16 -0
  123. package/src/util/dynamic-table.ts +20 -18
  124. package/src/util/schema.ts +1 -1
  125. package/src/util/table-buttons.ts +26 -2
  126. package/src/util/table-controls.ts +4 -1
  127. package/src/util/table.ts +37 -111
  128. package/dist/lib/browser/chunk-TL7CKQQV.mjs +0 -27
  129. package/dist/lib/browser/chunk-TL7CKQQV.mjs.map +0 -7
  130. package/dist/lib/node/chunk-O2VPBNS5.cjs +0 -50
  131. package/dist/lib/node/chunk-O2VPBNS5.cjs.map +0 -7
  132. package/dist/lib/node/index.cjs +0 -2739
  133. package/dist/lib/node/index.cjs.map +0 -7
  134. package/dist/lib/node/meta.json +0 -1
  135. package/dist/lib/node/types/index.cjs +0 -31
  136. package/dist/lib/node/types/index.cjs.map +0 -7
  137. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs +0 -29
  138. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs.map +0 -7
  139. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  140. package/dist/types/src/types/table.d.ts +0 -56
  141. package/dist/types/src/types/table.d.ts.map +0 -1
  142. package/src/playwright/playwright.config.cts +0 -18
  143. 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';
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 } from '../../hooks';
27
- import { TablePresentation } from '../../model';
28
- import translations from '../../translations';
29
- import { TableType } from '../../types';
30
- import { initializeTable } from '../../util';
31
+ import { useTestTableModel } from '../../testing';
32
+ import { translations } from '../../translations';
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 handleInsertRow = useCallback(() => {
76
- if (space && schema) {
77
- space.db.add(live(schema, {}));
78
- }
79
- }, [space, schema]);
80
-
81
- const handleDeleteRows = useCallback(
82
- (_: number, objects: any[]) => {
83
- for (const object of objects) {
84
- space?.db.remove(object);
85
- }
86
- },
87
- [space],
88
- );
89
-
90
- const handleDeleteColumn = useCallback(
91
- (fieldId: string) => {
92
- if (projection) {
93
- projection.deleteFieldProjection(fieldId);
94
- }
95
- },
96
- [projection],
97
- );
98
-
99
- const model = useTableModel({
100
- table,
101
- projection,
102
- features,
103
- rows: filteredObjects,
104
- onInsertRow: handleInsertRow,
105
- onDeleteRows: handleDeleteRows,
106
- onDeleteColumn: handleDeleteColumn,
107
- onCellUpdate: handleCellUpdate,
108
- onRowOrderChange: handleRowOrderChange,
109
- });
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>
@@ -209,26 +161,36 @@ type StoryProps = { rows?: number };
209
161
  const meta: Meta<StoryProps> = {
210
162
  title: 'ui/react-ui-table/Table',
211
163
  render: DefaultStory,
212
- parameters: { translations, controls: { disable: true } },
164
+ parameters: {
165
+ translations,
166
+ layout: 'fullscreen',
167
+ controls: {
168
+ disable: true,
169
+ },
170
+ },
213
171
  decorators: [
172
+ withTheme,
173
+ withLayout({ fullscreen: true }),
214
174
  withClientProvider({
215
- types: [TableType, ViewType],
175
+ types: [DataType.View, TableView],
216
176
  createIdentity: true,
217
177
  createSpace: true,
218
178
  onSpaceCreated: async ({ client, space }) => {
219
- const table = space.db.add(Obj.make(TableType, {}));
220
- 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
+
221
183
  Array.from({ length: 10 }).map(() => {
222
184
  return space.db.add(
223
- live(schema, {
224
- 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(),
225
189
  }),
226
190
  );
227
191
  });
228
192
  },
229
193
  }),
230
- withLayout({ fullscreen: true }),
231
- withTheme,
232
194
  ],
233
195
  };
234
196
 
@@ -241,18 +203,17 @@ export const StaticSchema: StoryObj = {
241
203
  parameters: { translations },
242
204
  decorators: [
243
205
  withClientProvider({
244
- types: [TableType, ViewType, Testing.Contact, Testing.Organization],
206
+ types: [DataType.View, TableView, Testing.Contact, Testing.Organization],
245
207
  createIdentity: true,
246
208
  createSpace: true,
247
209
  onSpaceCreated: async ({ client, space }) => {
248
- const table = space.db.add(Obj.make(TableType, {}));
249
- 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);
250
212
 
251
213
  const factory = createObjectFactory(space.db, faker as any);
252
214
  await factory([
253
- // { type: Testing.Contact, count: 10 },
215
+ { type: Testing.Contact, count: 10 },
254
216
  // { type: Testing.Organization, count: 1 },
255
- { type: ContactWithArrayOfEmails, count: 10 },
256
217
  ]);
257
218
  },
258
219
  }),
@@ -283,12 +244,12 @@ export const ArrayOfObjects: StoryObj = {
283
244
  parameters: { translations },
284
245
  decorators: [
285
246
  withClientProvider({
286
- types: [TableType, ViewType, Testing.Contact, Testing.Organization, ContactWithArrayOfEmails],
247
+ types: [DataType.View, TableView, Testing.Contact, Testing.Organization, ContactWithArrayOfEmails],
287
248
  createIdentity: true,
288
249
  createSpace: true,
289
250
  onSpaceCreated: async ({ client, space }) => {
290
- const table = space.db.add(Obj.make(TableType, {}));
291
- await initializeTable({ client, space, table, typename: ContactWithArrayOfEmails.typename });
251
+ const { view } = await createTable({ client, space, typename: ContactWithArrayOfEmails.typename });
252
+ space.db.add(view);
292
253
 
293
254
  const factory = createObjectFactory(space.db, faker as any);
294
255
  await factory([
@@ -309,7 +270,7 @@ export const Tags: Meta<StoryProps> = {
309
270
  parameters: { translations },
310
271
  decorators: [
311
272
  withClientProvider({
312
- types: [TableType, ViewType],
273
+ types: [DataType.View, TableView],
313
274
  createIdentity: true,
314
275
  createSpace: true,
315
276
  onSpaceCreated: async ({ client, space }) => {
@@ -340,8 +301,8 @@ export const Tags: Meta<StoryProps> = {
340
301
  const [storedSchema] = await space.db.schemaRegistry.register([schema]);
341
302
 
342
303
  // Initialize table.
343
- const table = space.db.add(Obj.make(TableType, {}));
344
- await initializeTable({ client, space, table, initialRow: false, typename });
304
+ const { view } = await createTable({ client, space, typename });
305
+ space.db.add(view);
345
306
 
346
307
  // Populate.
347
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 = {
@@ -91,6 +97,8 @@ const TableMain = forwardRef<TableController, TableMainProps>(
91
97
  const { t } = useTranslation(translationKey);
92
98
  const modals = useMemo(() => new ModalController(), []);
93
99
 
100
+ const draftRowCount = model?.getDraftRowCount() ?? 0;
101
+
94
102
  const frozen = useMemo(() => {
95
103
  const noActionColumn =
96
104
  model?.features.dataEditable === false &&
@@ -99,10 +107,11 @@ const TableMain = forwardRef<TableController, TableMainProps>(
99
107
 
100
108
  return {
101
109
  frozenRowsStart: 1,
110
+ frozenRowsEnd: draftRowCount,
102
111
  frozenColsStart: model?.features.selection.enabled ? 1 : 0,
103
112
  frozenColsEnd: noActionColumn ? 0 : 1,
104
113
  };
105
- }, [model]);
114
+ }, [model, draftRowCount]);
106
115
 
107
116
  const getCells = useCallback<NonNullable<GridContentProps['getCells']>>(
108
117
  (range: DxGridPlaneRange, plane: DxGridPlane) => presentation?.getCells(range, plane) ?? {},
@@ -119,30 +128,46 @@ const TableMain = forwardRef<TableController, TableMainProps>(
119
128
  /**
120
129
  * Provides an external controller that can be called to repaint the table.
121
130
  */
122
- useImperativeHandle<TableController, TableController>(
123
- forwardedRef,
124
- () => {
125
- if (!presentation || !dxGrid) {
126
- return {};
127
- }
131
+ useImperativeHandle<TableController, TableController>(forwardedRef, () => {
132
+ if (!presentation || !dxGrid) {
133
+ return {};
134
+ }
128
135
 
129
- return {
130
- update: (cell) => {
131
- if (cell) {
132
- dxGrid.updateIfWithinBounds(cell);
133
- } else {
134
- dxGrid.updateCells(true);
135
- dxGrid.requestUpdate();
136
- }
137
- },
138
- };
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
+ }
139
164
  },
140
- [presentation, dxGrid],
165
+ [model, dxGrid],
141
166
  );
142
167
 
143
168
  const handleGridClick = useCallback(
144
169
  (event: MouseEvent) => {
145
- const rowIndex = safeParseInt((event.target as HTMLElement).ariaRowIndex ?? '');
170
+ const rowIndex = safeParseInt((event.target as HTMLElement).closest('[aria-rowindex]')?.ariaRowIndex ?? '');
146
171
  if (rowIndex != null) {
147
172
  if (onRowClick) {
148
173
  const row = model?.getRowAt(rowIndex);
@@ -188,6 +213,10 @@ const TableMain = forwardRef<TableController, TableMainProps>(
188
213
  model?.sorting?.toggleSort(data.fieldId);
189
214
  break;
190
215
  }
216
+ case 'saveDraftRow': {
217
+ handleSaveDraftRow();
218
+ break;
219
+ }
191
220
  }
192
221
  return;
193
222
  }
@@ -222,36 +251,27 @@ const TableMain = forwardRef<TableController, TableMainProps>(
222
251
  }
223
252
  }
224
253
  },
225
- [model, modals],
254
+ [model, modals, dxGrid],
226
255
  );
227
256
 
228
257
  const handleFocus = useCallback<NonNullable<TableCellEditorProps['onFocus']>>(
229
- (increment, delta) => {
230
- if (dxGrid) {
231
- dxGrid.refocus(increment, delta);
232
- }
233
- },
234
- [dxGrid],
235
- );
236
-
237
- const handleEnter = useCallback<NonNullable<TableCellEditorProps['onEnter']>>(
238
- (cell) => {
239
- if (!model?.features.dataEditable) {
240
- return;
241
- }
242
-
243
- // TODO(burdon): Insert row only if bottom row isn't completely blank already.
244
- if (model && cell.row === model.getRowCount() - 1) {
245
- model.insertRow(cell.row);
246
- if (dxGrid) {
247
- requestAnimationFrame(() => {
248
- dxGrid?.scrollToRow(cell.row + 1);
249
- dxGrid?.refocus('row', 1);
250
- });
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);
251
271
  }
252
272
  }
253
273
  },
254
- [model, dxGrid],
274
+ [dxGrid, model],
255
275
  );
256
276
 
257
277
  const handleKeyDown = useCallback<NonNullable<GridContentProps['onKeyDown']>>(
@@ -268,7 +288,12 @@ const TableMain = forwardRef<TableController, TableMainProps>(
268
288
  switch (event.key) {
269
289
  case 'Backspace':
270
290
  case 'Delete': {
271
- 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
+ }
272
297
  break;
273
298
  }
274
299
  }
@@ -307,7 +332,7 @@ const TableMain = forwardRef<TableController, TableMainProps>(
307
332
  const handleQuery = useCallback<NonNullable<TableCellEditorProps['onQuery']>>(
308
333
  async ({ field, props }, text) => {
309
334
  if (model && props.referenceSchema && field.referencePath) {
310
- const space = model.space;
335
+ const space = getSpace(model.view);
311
336
  invariant(space);
312
337
 
313
338
  let schema;
@@ -349,6 +374,11 @@ const TableMain = forwardRef<TableController, TableMainProps>(
349
374
  [model, client, t],
350
375
  );
351
376
 
377
+ const handleSave = useCallback(() => {
378
+ dxGrid?.updateCells(true);
379
+ dxGrid?.requestUpdate();
380
+ }, [dxGrid]);
381
+
352
382
  if (!model || !modals) {
353
383
  return <span role='none' className='attention-surface' />;
354
384
  }
@@ -359,21 +389,20 @@ const TableMain = forwardRef<TableController, TableMainProps>(
359
389
  model={model}
360
390
  modals={modals}
361
391
  schema={schema}
362
- onEnter={handleEnter}
363
392
  onFocus={handleFocus}
364
393
  onQuery={handleQuery}
394
+ onSave={handleSave}
365
395
  />
366
396
  <Grid.Content
367
397
  className={mx('[--dx-grid-base:var(--baseSurface)]', gridSeparatorInlineEnd, gridSeparatorBlockEnd)}
368
398
  frozen={frozen}
369
- // getCells={getCells}
370
399
  columns={model.columnMeta.value}
371
400
  limitRows={model.getRowCount() ?? 0}
372
- limitColumns={model.view?.fields?.length ?? 0}
401
+ limitColumns={model.projection.fields.length}
373
402
  overscroll='trap'
374
403
  onAxisResize={handleAxisResize}
375
404
  onClick={handleGridClick}
376
- onKeyDown={handleKeyDown}
405
+ onKeyDownCapture={handleKeyDown}
377
406
  onWheelCapture={handleWheel}
378
407
  ref={setDxGrid}
379
408
  />