@dxos/react-ui-table 0.8.1-staging.5be625a → 0.8.1-staging.97aedb1

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 (172) hide show
  1. package/dist/lib/browser/{chunk-5MKYB7NF.mjs → chunk-YWNRUF4J.mjs} +7 -3
  2. package/dist/lib/browser/{chunk-5MKYB7NF.mjs.map → chunk-YWNRUF4J.mjs.map} +3 -3
  3. package/dist/lib/browser/index.mjs +295 -97
  4. package/dist/lib/browser/index.mjs.map +3 -3
  5. package/dist/lib/browser/meta.json +1 -1
  6. package/dist/lib/browser/types/index.mjs +1 -1
  7. package/dist/lib/node/{chunk-NRFKBRPV.cjs → chunk-TGG6OZ54.cjs} +10 -6
  8. package/dist/lib/node/{chunk-NRFKBRPV.cjs.map → chunk-TGG6OZ54.cjs.map} +3 -3
  9. package/dist/lib/node/index.cjs +293 -93
  10. package/dist/lib/node/index.cjs.map +3 -3
  11. package/dist/lib/node/meta.json +1 -1
  12. package/dist/lib/node/types/index.cjs +3 -3
  13. package/dist/lib/node/types/index.cjs.map +1 -1
  14. package/dist/lib/node-esm/{chunk-SU4D55KI.mjs → chunk-KKLP54L3.mjs} +7 -3
  15. package/dist/lib/node-esm/{chunk-SU4D55KI.mjs.map → chunk-KKLP54L3.mjs.map} +3 -3
  16. package/dist/lib/node-esm/index.mjs +295 -97
  17. package/dist/lib/node-esm/index.mjs.map +3 -3
  18. package/dist/lib/node-esm/meta.json +1 -1
  19. package/dist/lib/node-esm/types/index.mjs +1 -1
  20. package/dist/types/src/components/Table/DynamicTable.d.ts +15 -10
  21. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  22. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +10 -0
  23. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -0
  24. package/dist/types/src/components/Table/Table.d.ts +1 -1
  25. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  26. package/dist/types/src/components/Table/Table.stories.d.ts +2 -3
  27. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  28. package/dist/types/src/components/Table/relations.stories.d.ts +9 -0
  29. package/dist/types/src/components/Table/relations.stories.d.ts.map +1 -0
  30. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  31. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  32. package/dist/types/src/components/testing.d.ts.map +1 -1
  33. package/dist/types/src/hooks/useTableModel.d.ts +6 -10
  34. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  35. package/dist/types/src/model/index.d.ts +1 -0
  36. package/dist/types/src/model/index.d.ts.map +1 -1
  37. package/dist/types/src/model/selection-model.d.ts +6 -3
  38. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  39. package/dist/types/src/model/table-model.d.ts +13 -14
  40. package/dist/types/src/model/table-model.d.ts.map +1 -1
  41. package/dist/types/src/model/table-presentation.d.ts +4 -4
  42. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  43. package/dist/types/src/model/table-sorting.d.ts +2 -2
  44. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  45. package/dist/types/src/types/table.d.ts +2 -1
  46. package/dist/types/src/types/table.d.ts.map +1 -1
  47. package/dist/types/src/util/dynamic-table.d.ts +21 -30
  48. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  49. package/dist/types/src/util/table-controls.d.ts +2 -2
  50. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  51. package/dist/types/src/util/table.d.ts.map +1 -1
  52. package/package.json +33 -32
  53. package/src/components/Table/DynamicTable.stories.tsx +155 -0
  54. package/src/components/Table/DynamicTable.tsx +46 -28
  55. package/src/components/Table/Table.stories.tsx +129 -125
  56. package/src/components/Table/Table.tsx +51 -26
  57. package/src/components/Table/relations.stories.tsx +113 -0
  58. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +8 -3
  59. package/src/components/TableCellEditor/TableCellEditor.tsx +102 -4
  60. package/src/components/testing.ts +0 -1
  61. package/src/hooks/useTableModel.ts +11 -11
  62. package/src/model/index.ts +1 -0
  63. package/src/model/selection-model.ts +12 -2
  64. package/src/model/table-model.test.ts +1 -1
  65. package/src/model/table-model.ts +27 -18
  66. package/src/model/table-presentation.test.ts +1 -1
  67. package/src/model/table-presentation.ts +65 -11
  68. package/src/model/table-sorting.ts +2 -2
  69. package/src/playwright/smoke.spec.ts +2 -3
  70. package/src/types/table.ts +6 -5
  71. package/src/util/dynamic-table.ts +57 -25
  72. package/src/util/table-controls.ts +4 -4
  73. package/src/util/table.ts +2 -1
  74. package/dist/lib/browser/deprecated/index.mjs +0 -1483
  75. package/dist/lib/browser/deprecated/index.mjs.map +0 -7
  76. package/dist/lib/node/deprecated/index.cjs +0 -1511
  77. package/dist/lib/node/deprecated/index.cjs.map +0 -7
  78. package/dist/lib/node-esm/deprecated/index.mjs +0 -1485
  79. package/dist/lib/node-esm/deprecated/index.mjs.map +0 -7
  80. package/dist/types/src/deprecated/components/Cell/Cell.d.ts +0 -12
  81. package/dist/types/src/deprecated/components/Cell/Cell.d.ts.map +0 -1
  82. package/dist/types/src/deprecated/components/Cell/CellCombobox.d.ts +0 -13
  83. package/dist/types/src/deprecated/components/Cell/CellCombobox.d.ts.map +0 -1
  84. package/dist/types/src/deprecated/components/Cell/index.d.ts +0 -2
  85. package/dist/types/src/deprecated/components/Cell/index.d.ts.map +0 -1
  86. package/dist/types/src/deprecated/components/ColumnMenu/ColumnMenu.d.ts +0 -23
  87. package/dist/types/src/deprecated/components/ColumnMenu/ColumnMenu.d.ts.map +0 -1
  88. package/dist/types/src/deprecated/components/ColumnMenu/ColumnMenu.stories.d.ts +0 -36
  89. package/dist/types/src/deprecated/components/ColumnMenu/ColumnMenu.stories.d.ts.map +0 -1
  90. package/dist/types/src/deprecated/components/ColumnMenu/ColumnSettings.d.ts +0 -15
  91. package/dist/types/src/deprecated/components/ColumnMenu/ColumnSettings.d.ts.map +0 -1
  92. package/dist/types/src/deprecated/components/ColumnMenu/ColumnSettings.stories.d.ts +0 -84
  93. package/dist/types/src/deprecated/components/ColumnMenu/ColumnSettings.stories.d.ts.map +0 -1
  94. package/dist/types/src/deprecated/components/ColumnMenu/index.d.ts +0 -3
  95. package/dist/types/src/deprecated/components/ColumnMenu/index.d.ts.map +0 -1
  96. package/dist/types/src/deprecated/components/Table/Table.d.ts +0 -20
  97. package/dist/types/src/deprecated/components/Table/Table.d.ts.map +0 -1
  98. package/dist/types/src/deprecated/components/Table/Table.stories.d.ts +0 -116
  99. package/dist/types/src/deprecated/components/Table/Table.stories.d.ts.map +0 -1
  100. package/dist/types/src/deprecated/components/Table/TableBody.d.ts +0 -12
  101. package/dist/types/src/deprecated/components/Table/TableBody.d.ts.map +0 -1
  102. package/dist/types/src/deprecated/components/Table/TableContext.d.ts +0 -13
  103. package/dist/types/src/deprecated/components/Table/TableContext.d.ts.map +0 -1
  104. package/dist/types/src/deprecated/components/Table/TableHead.d.ts +0 -9
  105. package/dist/types/src/deprecated/components/Table/TableHead.d.ts.map +0 -1
  106. package/dist/types/src/deprecated/components/Table/TableRootContext.d.ts +0 -17
  107. package/dist/types/src/deprecated/components/Table/TableRootContext.d.ts.map +0 -1
  108. package/dist/types/src/deprecated/components/Table/index.d.ts +0 -5
  109. package/dist/types/src/deprecated/components/Table/index.d.ts.map +0 -1
  110. package/dist/types/src/deprecated/components/Table/props.d.ts +0 -42
  111. package/dist/types/src/deprecated/components/Table/props.d.ts.map +0 -1
  112. package/dist/types/src/deprecated/components/index.d.ts +0 -4
  113. package/dist/types/src/deprecated/components/index.d.ts.map +0 -1
  114. package/dist/types/src/deprecated/helpers.d.ts +0 -91
  115. package/dist/types/src/deprecated/helpers.d.ts.map +0 -1
  116. package/dist/types/src/deprecated/hooks/index.d.ts +0 -5
  117. package/dist/types/src/deprecated/hooks/index.d.ts.map +0 -1
  118. package/dist/types/src/deprecated/hooks/useColumnSizing.d.ts +0 -12
  119. package/dist/types/src/deprecated/hooks/useColumnSizing.d.ts.map +0 -1
  120. package/dist/types/src/deprecated/hooks/useColumnSorting.d.ts +0 -15
  121. package/dist/types/src/deprecated/hooks/useColumnSorting.d.ts.map +0 -1
  122. package/dist/types/src/deprecated/hooks/usePinLastRow.d.ts +0 -3
  123. package/dist/types/src/deprecated/hooks/usePinLastRow.d.ts.map +0 -1
  124. package/dist/types/src/deprecated/hooks/useRowSelection.d.ts +0 -7
  125. package/dist/types/src/deprecated/hooks/useRowSelection.d.ts.map +0 -1
  126. package/dist/types/src/deprecated/index.d.ts +0 -8
  127. package/dist/types/src/deprecated/index.d.ts.map +0 -1
  128. package/dist/types/src/deprecated/schema/Schema.stories.d.ts +0 -11
  129. package/dist/types/src/deprecated/schema/Schema.stories.d.ts.map +0 -1
  130. package/dist/types/src/deprecated/schema/column-utils.d.ts +0 -7
  131. package/dist/types/src/deprecated/schema/column-utils.d.ts.map +0 -1
  132. package/dist/types/src/deprecated/schema/index.d.ts +0 -3
  133. package/dist/types/src/deprecated/schema/index.d.ts.map +0 -1
  134. package/dist/types/src/deprecated/schema/types.d.ts +0 -27
  135. package/dist/types/src/deprecated/schema/types.d.ts.map +0 -1
  136. package/dist/types/src/deprecated/theme.d.ts +0 -29
  137. package/dist/types/src/deprecated/theme.d.ts.map +0 -1
  138. package/dist/types/src/deprecated/translations.d.ts +0 -21
  139. package/dist/types/src/deprecated/translations.d.ts.map +0 -1
  140. package/dist/types/src/deprecated/types.d.ts +0 -26
  141. package/dist/types/src/deprecated/types.d.ts.map +0 -1
  142. package/src/deprecated/components/Cell/Cell.tsx +0 -88
  143. package/src/deprecated/components/Cell/CellCombobox.tsx +0 -103
  144. package/src/deprecated/components/Cell/index.ts +0 -5
  145. package/src/deprecated/components/ColumnMenu/ColumnMenu.stories.tsx +0 -129
  146. package/src/deprecated/components/ColumnMenu/ColumnMenu.tsx +0 -182
  147. package/src/deprecated/components/ColumnMenu/ColumnSettings.stories.tsx +0 -108
  148. package/src/deprecated/components/ColumnMenu/ColumnSettings.tsx +0 -207
  149. package/src/deprecated/components/ColumnMenu/index.ts +0 -6
  150. package/src/deprecated/components/Table/Table.stories.tsx +0 -508
  151. package/src/deprecated/components/Table/Table.tsx +0 -301
  152. package/src/deprecated/components/Table/TableBody.tsx +0 -82
  153. package/src/deprecated/components/Table/TableContext.tsx +0 -34
  154. package/src/deprecated/components/Table/TableHead.tsx +0 -78
  155. package/src/deprecated/components/Table/TableRootContext.ts +0 -39
  156. package/src/deprecated/components/Table/index.ts +0 -8
  157. package/src/deprecated/components/Table/props.ts +0 -60
  158. package/src/deprecated/components/index.ts +0 -7
  159. package/src/deprecated/helpers.tsx +0 -526
  160. package/src/deprecated/hooks/index.ts +0 -8
  161. package/src/deprecated/hooks/useColumnSizing.ts +0 -46
  162. package/src/deprecated/hooks/useColumnSorting.ts +0 -64
  163. package/src/deprecated/hooks/usePinLastRow.ts +0 -47
  164. package/src/deprecated/hooks/useRowSelection.ts +0 -40
  165. package/src/deprecated/index.ts +0 -12
  166. package/src/deprecated/schema/Schema.stories.tsx +0 -59
  167. package/src/deprecated/schema/column-utils.ts +0 -54
  168. package/src/deprecated/schema/index.ts +0 -6
  169. package/src/deprecated/schema/types.ts +0 -40
  170. package/src/deprecated/theme.ts +0 -82
  171. package/src/deprecated/translations.ts +0 -26
  172. package/src/deprecated/types.ts +0 -38
@@ -5,7 +5,7 @@
5
5
  import '@dxos-theme';
6
6
 
7
7
  import { type StoryObj, type Meta } from '@storybook/react';
8
- import React, { useCallback, useMemo, useRef, useState } from 'react';
8
+ import React, { useCallback, useMemo, useRef } from 'react';
9
9
 
10
10
  import { FormatEnum, ImmutableSchema } from '@dxos/echo-schema';
11
11
  import { invariant } from '@dxos/invariant';
@@ -14,31 +14,28 @@ import { faker } from '@dxos/random';
14
14
  import { useClient } from '@dxos/react-client';
15
15
  import { Filter, useQuery, useSchema, create } from '@dxos/react-client/echo';
16
16
  import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
17
- import { useDefaultValue } from '@dxos/react-ui';
18
17
  import { ViewEditor } from '@dxos/react-ui-form';
19
18
  import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
20
- import { type SchemaPropertyDefinition, ViewProjection, ViewType } from '@dxos/schema';
19
+ import { getSchemaFromPropertyDefinitions, ViewProjection, ViewType } from '@dxos/schema';
21
20
  import { Testing, createObjectFactory } from '@dxos/schema/testing';
22
21
  import { withLayout, withTheme } from '@dxos/storybook-utils';
23
22
 
24
- import { DynamicTable as DynamicTableComponent } from './DynamicTable';
25
23
  import { Table, type TableController } from './Table';
26
- import { useTableModel, type UseTableModelParams } from '../../hooks';
24
+ import { useTableModel } from '../../hooks';
27
25
  import { TablePresentation } from '../../model';
28
26
  import translations from '../../translations';
29
27
  import { TableType } from '../../types';
30
28
  import { initializeTable } from '../../util';
31
29
  import { TableToolbar } from '../TableToolbar';
32
- import { createItems, createTable, type SimulatorProps, useSimulator } from '../testing';
33
30
 
34
31
  // NOTE(ZaymonFC): We rely on this seed being 0 in the smoke tests.
35
32
  faker.seed(0);
36
33
 
37
- //
38
- // Story components.
39
- //
40
-
41
- const DefaultStory = () => {
34
+ /**
35
+ * Custom hook to create and manage a test table model for storybook demonstrations.
36
+ * Provides table data, schema, and handlers for table operations.
37
+ */
38
+ const useTestTableModel = () => {
42
39
  const client = useClient();
43
40
  const { space } = useClientProvider();
44
41
 
@@ -49,18 +46,31 @@ const DefaultStory = () => {
49
46
 
50
47
  const projection = useMemo(() => {
51
48
  if (schema && table?.view?.target) {
52
- return new ViewProjection(schema, table.view.target);
49
+ return new ViewProjection(schema.jsonSchema, table.view.target);
53
50
  }
54
51
  }, [schema, table?.view?.target]);
55
52
 
56
53
  const features = useMemo(
57
- () => ({ selection: true, dataEditable: true, schemaEditable: !(schema instanceof ImmutableSchema) }),
54
+ () => ({
55
+ selection: { enabled: true, mode: 'multiple' as const },
56
+ dataEditable: true,
57
+ schemaEditable: !(schema instanceof ImmutableSchema),
58
+ }),
58
59
  [schema],
59
60
  );
60
61
 
61
62
  const objects = useQuery(space, schema ? Filter.schema(schema) : Filter.nothing());
62
63
  const filteredObjects = useGlobalFilteredObjects(objects);
63
64
 
65
+ const tableRef = useRef<TableController>(null);
66
+ const handleCellUpdate = useCallback((cell: any) => {
67
+ tableRef.current?.update?.(cell);
68
+ }, []);
69
+
70
+ const handleRowOrderChange = useCallback(() => {
71
+ tableRef.current?.update?.();
72
+ }, []);
73
+
64
74
  const handleInsertRow = useCallback(() => {
65
75
  if (space && schema) {
66
76
  space.db.add(create(schema, {}));
@@ -82,28 +92,19 @@ const DefaultStory = () => {
82
92
  projection.deleteFieldProjection(fieldId);
83
93
  }
84
94
  },
85
- [table, projection],
95
+ [projection],
86
96
  );
87
97
 
88
- const tableRef = useRef<TableController>(null);
89
- const handleCellUpdate = useCallback((cell: any) => {
90
- tableRef.current?.update?.(cell);
91
- }, []);
92
-
93
- const handleRowOrderChanged = useCallback(() => {
94
- tableRef.current?.update?.();
95
- }, []);
96
-
97
98
  const model = useTableModel({
98
99
  table,
99
100
  projection,
100
101
  features,
101
- objects: filteredObjects,
102
+ rows: filteredObjects,
102
103
  onInsertRow: handleInsertRow,
103
104
  onDeleteRows: handleDeleteRows,
104
105
  onDeleteColumn: handleDeleteColumn,
105
106
  onCellUpdate: handleCellUpdate,
106
- onRowOrderChanged: handleRowOrderChanged,
107
+ onRowOrderChange: handleRowOrderChange,
107
108
  });
108
109
 
109
110
  const presentation = useMemo(() => {
@@ -112,7 +113,7 @@ const DefaultStory = () => {
112
113
  }
113
114
  }, [model]);
114
115
 
115
- const handleAction = useCallback(
116
+ const handleToolbarAction = useCallback(
116
117
  (action: { type: string }) => {
117
118
  switch (action.type) {
118
119
  case 'on-thread-create': {
@@ -132,7 +133,23 @@ const DefaultStory = () => {
132
133
  [table, model],
133
134
  );
134
135
 
135
- const onTypenameChanged = useCallback(
136
+ return {
137
+ schema,
138
+ table,
139
+ tableRef,
140
+ model,
141
+ presentation,
142
+ space,
143
+ handleToolbarAction,
144
+ handleDeleteRows,
145
+ handleDeleteColumn,
146
+ };
147
+ };
148
+
149
+ const StoryViewEditor = () => {
150
+ const { table, space, schema, handleDeleteColumn } = useTestTableModel();
151
+
152
+ const handleTypenameChanged = useCallback(
136
153
  (typename: string) => {
137
154
  if (table?.view?.target) {
138
155
  invariant(schema);
@@ -143,6 +160,28 @@ const DefaultStory = () => {
143
160
  [schema, table?.view?.target],
144
161
  );
145
162
 
163
+ if (!table || !schema || !table.view?.target) {
164
+ return null;
165
+ }
166
+
167
+ return (
168
+ <ViewEditor
169
+ registry={space?.db.schemaRegistry}
170
+ schema={schema}
171
+ view={table.view.target!}
172
+ onTypenameChanged={handleTypenameChanged}
173
+ onDelete={handleDeleteColumn}
174
+ />
175
+ );
176
+ };
177
+
178
+ //
179
+ // Story components.
180
+ //
181
+
182
+ const DefaultStory = () => {
183
+ const { schema, table, tableRef, model, presentation, handleToolbarAction } = useTestTableModel();
184
+
146
185
  if (!schema || !table) {
147
186
  return <div />;
148
187
  }
@@ -150,22 +189,13 @@ const DefaultStory = () => {
150
189
  return (
151
190
  <div className='grow grid grid-cols-[1fr_350px]'>
152
191
  <div className='grid grid-rows-[min-content_1fr] min-bs-0 overflow-hidden'>
153
- <TableToolbar classNames='border-b border-separator' onAction={handleAction} />
192
+ <TableToolbar classNames='border-be border-separator' onAction={handleToolbarAction} />
154
193
  <Table.Root>
155
194
  <Table.Main ref={tableRef} model={model} presentation={presentation} ignoreAttention />
156
195
  </Table.Root>
157
196
  </div>
158
197
  <div className='flex flex-col h-full border-l border-separator overflow-y-auto'>
159
- {table.view?.target && (
160
- <ViewEditor
161
- registry={space?.db.schemaRegistry}
162
- schema={schema}
163
- view={table.view.target!}
164
- onTypenameChanged={onTypenameChanged}
165
- onDelete={handleDeleteColumn}
166
- />
167
- )}
168
-
198
+ <StoryViewEditor />
169
199
  <SyntaxHighlighter language='json' className='w-full text-xs'>
170
200
  {JSON.stringify({ view: table.view?.target, schema }, null, 2)}
171
201
  </SyntaxHighlighter>
@@ -174,76 +204,16 @@ const DefaultStory = () => {
174
204
  );
175
205
  };
176
206
 
177
- const DynamicTableStory = () => {
178
- const properties = useMemo<SchemaPropertyDefinition[]>(
179
- () => [
180
- { name: 'name', format: FormatEnum.String },
181
- { name: 'age', format: FormatEnum.Number },
182
- ],
183
- [],
184
- );
185
-
186
- const [objects, _setObjects] = useState<any[]>(
187
- Array.from({ length: 100 }, () => ({
188
- name: faker.person.fullName(),
189
- age: faker.number.int({ min: 18, max: 80 }),
190
- })),
191
- );
192
-
193
- return <DynamicTableComponent properties={properties} data={objects} />;
194
- };
195
-
196
- type StoryProps = {
197
- rows?: number;
198
- } & Pick<SimulatorProps, 'insertInterval' | 'updateInterval'>;
199
-
200
- const _TablePerformanceStory = (props: StoryProps) => {
201
- const getDefaultRows = useCallback(() => 10, []);
202
- const rows = useDefaultValue(props.rows, getDefaultRows);
203
- const table = useMemo(() => createTable(), []);
204
- const items = useMemo(() => createItems(rows), [rows]);
205
- const itemsRef = useRef(items);
206
- const simulatorProps = useMemo(() => ({ table, items, ...props }), [table, items, props]);
207
- useSimulator(simulatorProps);
208
-
209
- const handleDeleteRows = useCallback<NonNullable<UseTableModelParams<any>['onDeleteRows']>>((row) => {
210
- itemsRef.current.splice(row, 1);
211
- }, []);
212
-
213
- const handleDeleteColumn = useCallback<NonNullable<UseTableModelParams<any>['onDeleteColumn']>>(
214
- (fieldId) => {
215
- if (table && table.view?.target) {
216
- const fieldPosition = table.view.target!.fields.findIndex((field) => field.id === fieldId);
217
- table.view.target!.fields.splice(fieldPosition, 1);
218
- }
219
- },
220
- [table],
221
- );
222
-
223
- const tableRef = useRef<TableController>(null);
224
- const model = useTableModel({
225
- table,
226
- objects: items as any[],
227
- onDeleteRows: handleDeleteRows,
228
- onDeleteColumn: handleDeleteColumn,
229
- onCellUpdate: (cell) => tableRef.current?.update?.(cell),
230
- onRowOrderChanged: () => tableRef.current?.update?.(),
231
- });
232
-
233
- return (
234
- <Table.Root>
235
- <Table.Main ref={tableRef} model={model} />
236
- </Table.Root>
237
- );
238
- };
207
+ type StoryProps = { rows?: number };
239
208
 
240
209
  //
241
210
  // Story definitions.
242
211
  //
243
212
 
213
+ // TODO(burdon): Need super simple story.
214
+
244
215
  const meta: Meta<StoryProps> = {
245
216
  title: 'ui/react-ui-table/Table',
246
- component: Table.Main as any,
247
217
  render: DefaultStory,
248
218
  parameters: { translations },
249
219
  decorators: [
@@ -277,17 +247,17 @@ export const StaticSchema: StoryObj = {
277
247
  parameters: { translations },
278
248
  decorators: [
279
249
  withClientProvider({
280
- types: [TableType, ViewType, Testing.ContactType, Testing.OrgType],
250
+ types: [TableType, ViewType, Testing.Contact, Testing.Org],
281
251
  createIdentity: true,
282
252
  createSpace: true,
283
253
  onSpaceCreated: async ({ client, space }) => {
284
254
  const table = space.db.add(create(TableType, {}));
285
- await initializeTable({ client, space, table, typename: Testing.ContactType.typename });
255
+ await initializeTable({ client, space, table, typename: Testing.Contact.typename });
286
256
 
287
257
  const factory = createObjectFactory(space.db, faker as any);
288
258
  await factory([
289
- { type: Testing.ContactType, count: 10 },
290
- { type: Testing.OrgType, count: 1 },
259
+ { type: Testing.Contact, count: 10 },
260
+ { type: Testing.Org, count: 1 },
291
261
  ]);
292
262
  },
293
263
  }),
@@ -296,24 +266,58 @@ export const StaticSchema: StoryObj = {
296
266
  ],
297
267
  };
298
268
 
299
- export const DynamicTable: StoryObj = {
300
- render: DynamicTableStory,
301
- };
269
+ export const Tags: Meta<StoryProps> = {
270
+ title: 'ui/react-ui-table/Table',
271
+ render: DefaultStory,
272
+ parameters: { translations },
273
+ decorators: [
274
+ withClientProvider({
275
+ types: [TableType, ViewType],
276
+ createIdentity: true,
277
+ createSpace: true,
278
+ onSpaceCreated: async ({ client, space }) => {
279
+ // Configure schema.
280
+ const typename = 'example.com/SingleSelect';
281
+ const selectOptions = [
282
+ { id: 'one', title: 'One', color: 'emerald' },
283
+ { id: 'two', title: 'Two', color: 'blue' },
284
+ { id: 'three', title: 'Three', color: 'indigo' },
285
+ { id: 'four', title: 'Four', color: 'red' },
286
+ { id: 'longer_option', title: 'LONGER OPTION', color: 'amber' },
287
+ ];
288
+
289
+ const selectOptionIds = selectOptions.map((o) => o.id);
290
+
291
+ const schema = getSchemaFromPropertyDefinitions(typename, [
292
+ {
293
+ name: 'single',
294
+ format: FormatEnum.SingleSelect,
295
+ config: { options: selectOptions },
296
+ },
297
+ {
298
+ name: 'multiple',
299
+ format: FormatEnum.MultiSelect,
300
+ config: { options: selectOptions },
301
+ },
302
+ ]);
303
+ const [storedSchema] = await space.db.schemaRegistry.register([schema]);
302
304
 
303
- // TODO(ZaymonFC): Restore the performance stories.
304
- // type Story = StoryObj<StoryProps>;
305
- // export const Mutations: Story = {
306
- // render: TablePerformanceStory,
307
- // args: {
308
- // rows: 1000,
309
- // updateInterval: 1,
310
- // },
311
- // };
312
-
313
- // export const RapidInsertions: Story = {
314
- // render: TablePerformanceStory,
315
- // args: {
316
- // rows: 0,
317
- // insertInterval: 100,
318
- // },
319
- // };
305
+ // Initialize table.
306
+ const table = space.db.add(create(TableType, {}));
307
+ await initializeTable({ client, space, table, initialRow: false, typename });
308
+
309
+ // Populate.
310
+ Array.from({ length: 10 }).map(() => {
311
+ return space.db.add(
312
+ create(storedSchema, {
313
+ single: faker.helpers.arrayElement([...selectOptionIds, undefined]),
314
+ multiple: faker.helpers.randomSubset(selectOptionIds),
315
+ }),
316
+ );
317
+ });
318
+ },
319
+ }),
320
+ withTheme,
321
+ withLayout({ fullscreen: true, tooltips: true }),
322
+ ],
323
+ };
@@ -3,14 +3,15 @@
3
3
  //
4
4
 
5
5
  import React, {
6
- forwardRef,
6
+ type MouseEvent,
7
7
  type PropsWithChildren,
8
+ type WheelEvent,
9
+ forwardRef,
8
10
  useCallback,
9
11
  useImperativeHandle,
10
12
  useState,
11
- type WheelEvent,
12
- type MouseEvent,
13
13
  useMemo,
14
+ useEffect,
14
15
  } from 'react';
15
16
 
16
17
  import { getValue } from '@dxos/echo-schema';
@@ -18,7 +19,7 @@ import { invariant } from '@dxos/invariant';
18
19
  import { Filter } from '@dxos/react-client/echo';
19
20
  import { useTranslation } from '@dxos/react-ui';
20
21
  import { useAttention } from '@dxos/react-ui-attention';
21
- import { type DxGridElement, Grid, type GridContentProps, closestCell, type DxGridPosition } from '@dxos/react-ui-grid';
22
+ import { closestCell, type DxGridElement, type DxGridPosition, type GridContentProps, Grid } from '@dxos/react-ui-grid';
22
23
  import { mx } from '@dxos/react-ui-theme';
23
24
  import { isNotFalsy, safeParseInt } from '@dxos/util';
24
25
 
@@ -40,7 +41,7 @@ const blockEndLine =
40
41
  '[&>.dx-grid]:before:absolute [&>.dx-grid]:before:inset-inline-0 [&>.dx-grid]:before:-block-end-px [&>.dx-grid]:before:bs-px [&>.dx-grid]:before:bg-separator';
41
42
 
42
43
  //
43
- // Root
44
+ // Table.Root
44
45
  //
45
46
 
46
47
  export type TableRootProps = PropsWithChildren<{ role?: string }>;
@@ -50,8 +51,8 @@ const TableRoot = ({ children, role }: TableRootProps) => {
50
51
  <div
51
52
  role='none'
52
53
  className={mx(
53
- 'relative border-bs !border-separator',
54
- role === 'section'
54
+ 'relative !border-separator',
55
+ role === 'section' // TODO(burdon): This leaks composer plugin concepts? Standardize for react-ui?
55
56
  ? 'attention-surface overflow-hidden [&_.dx-grid]:max-is-[--dx-grid-content-inline-size]'
56
57
  : '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]',
57
58
  )}
@@ -62,7 +63,7 @@ const TableRoot = ({ children, role }: TableRootProps) => {
62
63
  };
63
64
 
64
65
  //
65
- // Main
66
+ // Table.Main
66
67
  //
67
68
 
68
69
  export type TableController = {
@@ -72,21 +73,45 @@ export type TableController = {
72
73
  export type TableMainProps = {
73
74
  model?: TableModel;
74
75
  presentation?: TablePresentation;
76
+ // TODO(burdon): Rename since attention isn't a useful concept here? Standardize across other components. Pass property into useAttention.
75
77
  ignoreAttention?: boolean;
76
- onRowClicked?: (row: any) => void;
78
+ onRowClick?: (row: any) => void;
77
79
  };
78
80
 
79
81
  const TableMain = forwardRef<TableController, TableMainProps>(
80
- ({ model, presentation, ignoreAttention, onRowClicked }, forwardedRef) => {
82
+ ({ model, presentation, ignoreAttention, onRowClick }, forwardedRef) => {
81
83
  const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
82
84
  const { hasAttention } = useAttention(model?.id ?? 'table');
83
85
  const { t } = useTranslation(translationKey);
84
86
  const modals = useMemo(() => new ModalController(), []);
85
87
 
86
- const frozen = useMemo(
87
- () => ({ frozenRowsStart: 1, frozenColsStart: model?.features.selection ? 1 : 0, frozenColsEnd: 1 }),
88
- [model],
89
- );
88
+ const frozen = useMemo(() => {
89
+ const noActionColumn =
90
+ model?.features.dataEditable === false &&
91
+ model?.features.schemaEditable === false &&
92
+ model.rowActions.length === 0;
93
+
94
+ return {
95
+ frozenRowsStart: 1,
96
+ frozenColsStart: model?.features.selection.enabled ? 1 : 0,
97
+ frozenColsEnd: noActionColumn ? 0 : 1,
98
+ };
99
+ }, [model]);
100
+
101
+ // TODO(burdon): Replace useEffect below.
102
+ // const getCells = useCallback<GridContentProps['getCells']>(
103
+ // (range: DxGridRange, plane: DxGridPlane) => presentation?.getCells(range, plane) ?? {},
104
+ // [presentation],
105
+ // );
106
+
107
+ useEffect(() => {
108
+ if (!presentation || !dxGrid) {
109
+ return;
110
+ }
111
+
112
+ // TODO(burdon): Pass to Grid.Content?
113
+ dxGrid.getCells = (range, plane) => presentation.getCells(range, plane);
114
+ }, [presentation, dxGrid]);
90
115
 
91
116
  /**
92
117
  * Provides an external controller that can be called to repaint the table.
@@ -98,7 +123,6 @@ const TableMain = forwardRef<TableController, TableMainProps>(
98
123
  return {};
99
124
  }
100
125
 
101
- dxGrid.getCells = (range, plane) => presentation.getCells(range, plane);
102
126
  return {
103
127
  update: (cell) => {
104
128
  if (cell) {
@@ -115,11 +139,15 @@ const TableMain = forwardRef<TableController, TableMainProps>(
115
139
 
116
140
  const handleGridClick = useCallback(
117
141
  (event: MouseEvent) => {
118
- if (onRowClicked) {
119
- const rowIndex = safeParseInt((event.target as HTMLElement).ariaRowIndex ?? '');
120
- if (rowIndex != null) {
142
+ const rowIndex = safeParseInt((event.target as HTMLElement).ariaRowIndex ?? '');
143
+ if (rowIndex != null) {
144
+ if (onRowClick) {
121
145
  const row = model?.getRowAt(rowIndex);
122
- row && onRowClicked(row);
146
+ row && onRowClick(row);
147
+ }
148
+
149
+ if (model?.features.selection.enabled && model?.selection.selectionMode === 'single') {
150
+ model.selection.toggleSelectionForRowIndex(rowIndex);
123
151
  }
124
152
  }
125
153
 
@@ -328,32 +356,29 @@ const TableMain = forwardRef<TableController, TableMainProps>(
328
356
  onQuery={handleQuery}
329
357
  />
330
358
  <Grid.Content
331
- onWheelCapture={handleWheel}
332
359
  className={mx('[--dx-grid-base:var(--surface-bg)]', inlineEndLine, blockEndLine)}
333
360
  frozen={frozen}
361
+ // getCells={getCells}
334
362
  columns={model.columnMeta.value}
335
363
  limitRows={model.getRowCount() ?? 0}
336
364
  limitColumns={model.view?.fields?.length ?? 0}
365
+ overscroll='trap'
337
366
  onAxisResize={handleAxisResize}
338
367
  onClick={handleGridClick}
339
368
  onKeyDown={handleKeyDown}
340
- overscroll='trap'
369
+ onWheelCapture={handleWheel}
341
370
  ref={setDxGrid}
342
371
  />
343
372
  <RowActionsMenu model={model} modals={modals} />
344
373
  <ColumnActionsMenu model={model} modals={modals} />
345
374
  <ColumnSettings model={model} modals={modals} onNewColumn={handleNewColumn} />
346
- <RefPanel model={model} modals={modals} />
347
375
  <CreateRefPanel model={model} modals={modals} />
376
+ <RefPanel model={model} modals={modals} />
348
377
  </Grid.Root>
349
378
  );
350
379
  },
351
380
  );
352
381
 
353
- //
354
- // CellEditor
355
- //
356
-
357
382
  export const Table = {
358
383
  Root: TableRoot,
359
384
  Main: TableMain,
@@ -0,0 +1,113 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import '@dxos-theme';
6
+
7
+ import { type StoryObj, type Meta } from '@storybook/react';
8
+ import React, { useEffect, useMemo } from 'react';
9
+
10
+ import { AST, type BaseObject, ImmutableSchema, type BaseSchema, type HasId } from '@dxos/echo-schema';
11
+ import { getAnnotation } from '@dxos/effect';
12
+ import { faker } from '@dxos/random';
13
+ import { create, makeRef, type ReactiveObject } from '@dxos/react-client/echo';
14
+ import { withClientProvider } from '@dxos/react-client/testing';
15
+ import { createView, ViewProjection, ViewType } from '@dxos/schema';
16
+ import { createGenerator, Testing, type ValueGenerator } from '@dxos/schema/testing';
17
+ import { withLayout, withTheme } from '@dxos/storybook-utils';
18
+
19
+ import { Table } from './Table';
20
+ import { useTableModel } from '../../hooks';
21
+ import { TablePresentation } from '../../model';
22
+ import translations from '../../translations';
23
+ import { TableType } from '../../types';
24
+
25
+ faker.seed(1);
26
+ const generator: ValueGenerator = faker as any;
27
+
28
+ // TODO(burdon): Many-to-many relations.
29
+ // TODO(burdon): Mutable and immutable views.
30
+ // TODO(burdon): Reconcile schemas types and utils (see API PR).
31
+ // TODO(burdon): Base type for T (with id); see ECHO API PR?
32
+ const useTestModel = <T extends BaseObject & HasId>(schema: BaseSchema<T>, count: number) => {
33
+ const table = useMemo(() => {
34
+ // const { typename } = pipe(schema.ast, AST.getAnnotation<TypeAnnotation>(TypeAnnotationId), Option.getOrThrow);
35
+ const typename = schema.typename;
36
+ const name = getAnnotation<string>(AST.TitleAnnotationId)(schema.ast) ?? typename;
37
+ const view = createView({ name, typename, jsonSchema: schema.jsonSchema });
38
+ return create(TableType, { view: makeRef(view) });
39
+ }, [schema]);
40
+
41
+ const projection = useMemo(() => {
42
+ if (!table.view?.target) {
43
+ return undefined;
44
+ }
45
+
46
+ // TODO(burdon): Just pass in view? Reuse same jsonSchema instance? View determines if mutable, etc.
47
+ return new ViewProjection(schema.jsonSchema, table.view.target);
48
+ }, [schema, table]);
49
+
50
+ const model = useTableModel({ table, projection, rows: [] });
51
+ useEffect(() => {
52
+ if (!model) {
53
+ return;
54
+ }
55
+
56
+ const objectGenerator = createGenerator(generator, schema, { optional: true });
57
+ const objects: ReactiveObject<T>[] = Array.from({ length: count }).map(() => objectGenerator.createObject());
58
+ model.setRows(objects);
59
+ }, [model]);
60
+
61
+ const presentation = useMemo(() => {
62
+ if (!model) {
63
+ return;
64
+ }
65
+
66
+ return new TablePresentation(model);
67
+ }, [model]);
68
+
69
+ return { model, presentation };
70
+ };
71
+
72
+ const DefaultStory = () => {
73
+ // TODO(burdon): Remove need for ImmutableSchema wrapper at API-level.
74
+ const orgSchema = useMemo(() => new ImmutableSchema(Testing.Org), []);
75
+ const { model: orgModel, presentation: orgPresentation } = useTestModel<Testing.Org>(orgSchema, 50);
76
+
77
+ // TODO(burdon): Generate links with references.
78
+ const contactSchema = useMemo(() => new ImmutableSchema(Testing.Contact), []);
79
+ const { model: contactModel, presentation: contactPresentation } = useTestModel<Testing.Contact>(contactSchema, 50);
80
+
81
+ // TODO(burdon): Scrolling isn't working.
82
+ return (
83
+ <div className='grow grid grid-cols-2 divide-x divide-separator'>
84
+ <Table.Root>
85
+ <Table.Main model={orgModel} presentation={orgPresentation} />
86
+ </Table.Root>
87
+ <Table.Root>
88
+ <Table.Main model={contactModel} presentation={contactPresentation} />
89
+ </Table.Root>
90
+ </div>
91
+ );
92
+ };
93
+
94
+ const meta: Meta<typeof DefaultStory> = {
95
+ title: 'ui/react-ui-table/relations',
96
+ render: DefaultStory,
97
+ parameters: { translations },
98
+ decorators: [
99
+ withClientProvider({
100
+ types: [TableType, ViewType, Testing.Org, Testing.Contact],
101
+ createIdentity: true,
102
+ createSpace: true,
103
+ }),
104
+ withTheme,
105
+ withLayout({ fullscreen: true, tooltips: true }),
106
+ ],
107
+ };
108
+
109
+ export default meta;
110
+
111
+ type Story = StoryObj<typeof DefaultStory>;
112
+
113
+ export const Default: Story = {};
@@ -18,6 +18,7 @@ import { withLayout, withTheme } from '@dxos/storybook-utils';
18
18
 
19
19
  import { TableCellEditor, type TableCellEditorProps } from './TableCellEditor';
20
20
  import { useTableModel } from '../../hooks';
21
+ import { type TableFeatures } from '../../model';
21
22
  import translations from '../../translations';
22
23
  import { TableType } from '../../types';
23
24
  import { initializeTable } from '../../util';
@@ -44,12 +45,16 @@ const DefaultStory = ({ editing }: StoryProps) => {
44
45
 
45
46
  const projection = useMemo(() => {
46
47
  if (schema && table?.view) {
47
- return new ViewProjection(schema, table.view.target!);
48
+ return new ViewProjection(schema.jsonSchema, table.view.target!);
48
49
  }
49
50
  }, [schema, table?.view]);
50
51
 
51
- const features = useMemo(
52
- () => ({ selection: true, dataEditable: true, schemaEditable: !(schema instanceof ImmutableSchema) }),
52
+ const features: Partial<TableFeatures> = useMemo(
53
+ () => ({
54
+ selection: { enabled: true, mode: 'multiple' },
55
+ dataEditable: true,
56
+ schemaEditable: !(schema instanceof ImmutableSchema),
57
+ }),
53
58
  [],
54
59
  );
55
60