@dxos/react-ui-table 0.7.4 → 0.7.5-labs.a279d8c

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 (116) hide show
  1. package/dist/lib/browser/{chunk-IYCSJDMB.mjs → chunk-3FR6NHPD.mjs} +7 -5
  2. package/dist/lib/browser/chunk-3FR6NHPD.mjs.map +7 -0
  3. package/dist/lib/browser/index.mjs +1405 -1067
  4. package/dist/lib/browser/index.mjs.map +4 -4
  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-MHBMVCFZ.cjs → chunk-NB3SFHGA.cjs} +9 -7
  8. package/dist/lib/node/chunk-NB3SFHGA.cjs.map +7 -0
  9. package/dist/lib/node/index.cjs +1409 -1051
  10. package/dist/lib/node/index.cjs.map +4 -4
  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-BMWGPXPL.mjs → chunk-RLVVOZW5.mjs} +7 -5
  15. package/dist/lib/node-esm/chunk-RLVVOZW5.mjs.map +7 -0
  16. package/dist/lib/node-esm/index.mjs +1405 -1067
  17. package/dist/lib/node-esm/index.mjs.map +4 -4
  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/_deprecated/hooks/useColumnSorting.d.ts +1 -1
  21. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +4 -3
  22. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/ColumnSettings.d.ts +3 -2
  24. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/CreateRefPanel.d.ts +3 -2
  26. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/RefPanel.d.ts +4 -3
  28. package/dist/types/src/components/Table/RefPanel.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/RowActionsMenu.d.ts +3 -2
  30. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/Table.d.ts +2 -1
  32. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  33. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  34. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +3 -2
  35. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  36. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +18 -0
  38. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -0
  39. package/dist/types/src/components/TableToolbar/index.d.ts +2 -0
  40. package/dist/types/src/components/TableToolbar/index.d.ts.map +1 -0
  41. package/dist/types/src/components/index.d.ts +1 -1
  42. package/dist/types/src/components/index.d.ts.map +1 -1
  43. package/dist/types/src/components/testing.d.ts +31 -71
  44. package/dist/types/src/components/testing.d.ts.map +1 -1
  45. package/dist/types/src/index.d.ts +1 -0
  46. package/dist/types/src/index.d.ts.map +1 -1
  47. package/dist/types/src/model/index.d.ts +1 -0
  48. package/dist/types/src/model/index.d.ts.map +1 -1
  49. package/dist/types/src/model/modal-controller.d.ts +6 -2
  50. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  51. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  52. package/dist/types/src/model/table-model.d.ts +19 -31
  53. package/dist/types/src/model/table-model.d.ts.map +1 -1
  54. package/dist/types/src/model/table-presentation.d.ts +22 -0
  55. package/dist/types/src/model/table-presentation.d.ts.map +1 -0
  56. package/dist/types/src/model/table-presentation.test.d.ts +2 -0
  57. package/dist/types/src/model/table-presentation.test.d.ts.map +1 -0
  58. package/dist/types/src/model/table-sorting.d.ts +53 -0
  59. package/dist/types/src/model/table-sorting.d.ts.map +1 -0
  60. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  61. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  62. package/dist/types/src/translations.d.ts +2 -1
  63. package/dist/types/src/translations.d.ts.map +1 -1
  64. package/dist/types/src/types/table.d.ts +46 -154
  65. package/dist/types/src/types/table.d.ts.map +1 -1
  66. package/dist/types/src/util/index.d.ts +1 -0
  67. package/dist/types/src/util/index.d.ts.map +1 -1
  68. package/dist/types/src/util/table-buttons.d.ts +63 -19
  69. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  70. package/dist/types/src/util/table-controls.d.ts +32 -9
  71. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  72. package/dist/types/src/util/table.d.ts +3 -2
  73. package/dist/types/src/util/table.d.ts.map +1 -1
  74. package/dist/types/tsconfig.tsbuildinfo +1 -0
  75. package/package.json +30 -29
  76. package/src/components/Table/ColumnActionsMenu.tsx +12 -12
  77. package/src/components/Table/ColumnSettings.tsx +11 -11
  78. package/src/components/Table/CreateRefPanel.tsx +13 -14
  79. package/src/components/Table/RefPanel.tsx +9 -9
  80. package/src/components/Table/RowActionsMenu.tsx +6 -7
  81. package/src/components/Table/Table.stories.tsx +67 -38
  82. package/src/components/Table/Table.tsx +239 -148
  83. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +13 -12
  84. package/src/components/TableCellEditor/TableCellEditor.tsx +6 -4
  85. package/src/components/TableToolbar/TableToolbar.tsx +72 -0
  86. package/src/components/{Toolbar → TableToolbar}/index.ts +1 -1
  87. package/src/components/index.ts +1 -1
  88. package/src/components/testing.ts +10 -8
  89. package/src/index.ts +2 -0
  90. package/src/model/index.ts +1 -0
  91. package/src/model/modal-controller.ts +15 -48
  92. package/src/model/selection-model.ts +12 -1
  93. package/src/model/table-model.test.ts +4 -76
  94. package/src/model/table-model.ts +73 -304
  95. package/src/model/table-presentation.test.ts +102 -0
  96. package/src/model/table-presentation.ts +223 -0
  97. package/src/model/table-sorting.ts +201 -0
  98. package/src/playwright/TableManager.ts +5 -1
  99. package/src/playwright/smoke.spec.ts +94 -9
  100. package/src/translations.ts +2 -1
  101. package/src/types/table.ts +4 -4
  102. package/src/util/index.ts +1 -0
  103. package/src/util/table-buttons.ts +127 -48
  104. package/src/util/table-controls.ts +71 -7
  105. package/src/util/table.ts +93 -52
  106. package/dist/lib/browser/chunk-IYCSJDMB.mjs.map +0 -7
  107. package/dist/lib/node/chunk-MHBMVCFZ.cjs.map +0 -7
  108. package/dist/lib/node-esm/chunk-BMWGPXPL.mjs.map +0 -7
  109. package/dist/types/src/components/Toolbar/Toolbar.d.ts +0 -21
  110. package/dist/types/src/components/Toolbar/Toolbar.d.ts.map +0 -1
  111. package/dist/types/src/components/Toolbar/common.d.ts +0 -14
  112. package/dist/types/src/components/Toolbar/common.d.ts.map +0 -1
  113. package/dist/types/src/components/Toolbar/index.d.ts +0 -2
  114. package/dist/types/src/components/Toolbar/index.d.ts.map +0 -1
  115. package/src/components/Toolbar/Toolbar.tsx +0 -87
  116. package/src/components/Toolbar/common.tsx +0 -71
@@ -9,6 +9,8 @@ import React, {
9
9
  useImperativeHandle,
10
10
  useState,
11
11
  type WheelEvent,
12
+ type MouseEvent,
13
+ useMemo,
12
14
  } from 'react';
13
15
 
14
16
  import { getValue } from '@dxos/echo-schema';
@@ -25,8 +27,9 @@ import { ColumnSettings } from './ColumnSettings';
25
27
  import { CreateRefPanel } from './CreateRefPanel';
26
28
  import { RefPanel } from './RefPanel';
27
29
  import { RowActionsMenu } from './RowActionsMenu';
28
- import { type TableModel } from '../../model';
30
+ import { ModalController, type TableModel, type TablePresentation } from '../../model';
29
31
  import { translationKey } from '../../translations';
32
+ import { tableButtons, tableControls } from '../../util';
30
33
  import { createOption, TableCellEditor, type TableCellEditorProps } from '../TableCellEditor';
31
34
 
32
35
  // NOTE(Zan): These fragments add border to inline-end and block-end of the grid using pseudo-elements.
@@ -70,174 +73,262 @@ export type TableController = {
70
73
 
71
74
  export type TableMainProps = {
72
75
  model?: TableModel;
76
+ presentation?: TablePresentation;
73
77
  ignoreAttention?: boolean;
74
78
  };
75
79
 
76
- const TableMain = forwardRef<TableController, TableMainProps>(({ model, ignoreAttention }, forwardedRef) => {
77
- const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
78
- const { hasAttention } = useAttention(model?.table ? fullyQualifiedId(model.table) : 'table');
79
- const { t } = useTranslation(translationKey);
80
-
81
- /**
82
- * Provides an external controller that can be called to repaint the table.
83
- */
84
- useImperativeHandle<TableController, TableController>(
85
- forwardedRef,
86
- () => {
87
- if (!model || !dxGrid) {
88
- return {};
89
- }
80
+ const TableMain = forwardRef<TableController, TableMainProps>(
81
+ ({ model, presentation, ignoreAttention }, forwardedRef) => {
82
+ const [dxGrid, setDxGrid] = useState<DxGridElement | null>(null);
83
+ const { hasAttention } = useAttention(model?.table ? fullyQualifiedId(model.table) : 'table');
84
+ const { t } = useTranslation(translationKey);
85
+ const modals = useMemo(() => new ModalController(), []);
86
+
87
+ /**
88
+ * Provides an external controller that can be called to repaint the table.
89
+ */
90
+ useImperativeHandle<TableController, TableController>(
91
+ forwardedRef,
92
+ () => {
93
+ if (!presentation || !dxGrid) {
94
+ return {};
95
+ }
96
+
97
+ dxGrid.getCells = (range, plane) => presentation.getCells(range, plane);
98
+ return {
99
+ update: (cell) => {
100
+ if (cell) {
101
+ dxGrid.updateIfWithinBounds(cell);
102
+ } else {
103
+ dxGrid.updateCells(true);
104
+ dxGrid.requestUpdate();
105
+ }
106
+ },
107
+ };
108
+ },
109
+ [presentation, dxGrid],
110
+ );
111
+
112
+ const handleGridClick = useCallback(
113
+ (event: MouseEvent) => {
114
+ if (!modals) {
115
+ return;
116
+ }
90
117
 
91
- dxGrid.getCells = model.getCells.bind(model);
92
- return {
93
- update: (cell) => {
94
- if (cell) {
95
- dxGrid.updateIfWithinBounds(cell);
96
- } else {
97
- dxGrid.updateCells(true);
118
+ const buttonSelector = Object.values(tableButtons)
119
+ .map((button) => `button[${button.attr}]`)
120
+ .join(',');
121
+
122
+ const tableButtonElement = (event.target as HTMLElement).closest(buttonSelector) as HTMLElement;
123
+ if (tableButtonElement) {
124
+ const button = Object.values(tableButtons).find((btn) => tableButtonElement.hasAttribute(btn.attr));
125
+ if (!button) {
126
+ return;
98
127
  }
99
- },
100
- };
101
- },
102
- [model, dxGrid],
103
- );
104
128
 
105
- const handleFocus = useCallback<NonNullable<TableCellEditorProps['onFocus']>>(
106
- (increment, delta) => {
107
- if (dxGrid) {
108
- dxGrid.refocus(increment, delta);
109
- }
110
- },
111
- [dxGrid],
112
- );
129
+ modals.setTrigger(tableButtonElement);
130
+ const data = button.getData(tableButtonElement);
131
+ switch (data.type) {
132
+ case 'rowMenu': {
133
+ modals.showRowMenu(data.rowIndex);
134
+ break;
135
+ }
136
+ case 'columnSettings': {
137
+ modals.showColumnMenu(data.fieldId);
138
+ break;
139
+ }
140
+ case 'newColumn': {
141
+ modals.showColumnCreator();
142
+ break;
143
+ }
144
+ case 'referencedCell': {
145
+ modals.showReferencePanel(data.targetId, data.schemaId);
146
+ break;
147
+ }
148
+ case 'sort': {
149
+ model?.sorting?.toggleSort(data.fieldId);
150
+ break;
151
+ }
152
+ }
153
+ return;
154
+ }
113
155
 
114
- const handleEnter = useCallback<NonNullable<TableCellEditorProps['onEnter']>>(
115
- (cell) => {
116
- // TODO(burdon): Insert row only if bottom row isn't completely blank already.
117
- if (model && cell.row === model.getRowCount() - 1) {
118
- model.insertRow(cell.row);
156
+ const controlSelector = Object.values(tableControls)
157
+ .map((control) => `input[${control.attr}]`)
158
+ .join(',');
159
+
160
+ const controlElement = (event.target as HTMLElement).closest(controlSelector) as HTMLElement;
161
+ if (controlElement) {
162
+ const control = Object.values(tableControls).find((ctrl) => controlElement.hasAttribute(ctrl.attr));
163
+ if (!control) {
164
+ return;
165
+ }
166
+
167
+ const data = control.getData(controlElement);
168
+ switch (data.type) {
169
+ case 'checkbox': {
170
+ if (data.header) {
171
+ model?.selection.setSelection(model.selection.allRowsSeleted.value ? 'none' : 'all');
172
+ } else {
173
+ model?.selection.toggleSelectionForRowIndex(data.rowIndex);
174
+ }
175
+ break;
176
+ }
177
+ case 'switch': {
178
+ if (model) {
179
+ model.updateCellData({ row: data.rowIndex, col: data.colIndex }, (value) => !value);
180
+ }
181
+ break;
182
+ }
183
+ }
184
+ }
185
+ },
186
+ [model, modals],
187
+ );
188
+
189
+ const handleFocus = useCallback<NonNullable<TableCellEditorProps['onFocus']>>(
190
+ (increment, delta) => {
119
191
  if (dxGrid) {
120
- requestAnimationFrame(() => {
121
- dxGrid?.scrollToRow(cell.row + 1);
122
- dxGrid?.refocus('row', 1);
123
- });
192
+ dxGrid.refocus(increment, delta);
124
193
  }
125
- }
126
- },
127
- [model, dxGrid],
128
- );
194
+ },
195
+ [dxGrid],
196
+ );
129
197
 
130
- const handleKeyDown = useCallback<NonNullable<GridContentProps['onKeyDown']>>(
131
- (event) => {
132
- const cell = closestCell(event.target);
133
- if (!model || !cell) {
134
- return;
135
- }
198
+ const handleEnter = useCallback<NonNullable<TableCellEditorProps['onEnter']>>(
199
+ (cell) => {
200
+ // TODO(burdon): Insert row only if bottom row isn't completely blank already.
201
+ if (model && cell.row === model.getRowCount() - 1) {
202
+ model.insertRow(cell.row);
203
+ if (dxGrid) {
204
+ requestAnimationFrame(() => {
205
+ dxGrid?.scrollToRow(cell.row + 1);
206
+ dxGrid?.refocus('row', 1);
207
+ });
208
+ }
209
+ }
210
+ },
211
+ [model, dxGrid],
212
+ );
136
213
 
137
- switch (event.key) {
138
- case 'Backspace':
139
- case 'Delete': {
140
- model.setCellData(cell, undefined);
141
- break;
214
+ const handleKeyDown = useCallback<NonNullable<GridContentProps['onKeyDown']>>(
215
+ (event) => {
216
+ const cell = closestCell(event.target);
217
+ if (!model || !cell) {
218
+ return;
142
219
  }
143
- }
144
- },
145
- [model],
146
- );
147
220
 
148
- const handleAxisResize = useCallback<NonNullable<GridContentProps['onAxisResize']>>(
149
- (event) => {
150
- if (event.axis === 'col') {
151
- const columnIndex = parseInt(event.index, 10);
152
- model?.setColumnWidth(columnIndex, event.size);
153
- }
154
- },
155
- [model],
156
- );
221
+ switch (event.key) {
222
+ case 'Backspace':
223
+ case 'Delete': {
224
+ model.setCellData(cell, undefined);
225
+ break;
226
+ }
227
+ }
228
+ },
229
+ [model],
230
+ );
231
+
232
+ const handleAxisResize = useCallback<NonNullable<GridContentProps['onAxisResize']>>(
233
+ (event) => {
234
+ if (event.axis === 'col') {
235
+ const columnIndex = parseInt(event.index, 10);
236
+ model?.setColumnWidth(columnIndex, event.size);
237
+ }
238
+ },
239
+ [model],
240
+ );
241
+
242
+ const handleWheel = useCallback(
243
+ (event: WheelEvent) => {
244
+ if (!ignoreAttention && !hasAttention) {
245
+ event.stopPropagation();
246
+ }
247
+ },
248
+ [hasAttention, ignoreAttention],
249
+ );
157
250
 
158
- const handleWheel = useCallback(
159
- (event: WheelEvent) => {
160
- if (!ignoreAttention && !hasAttention) {
161
- event.stopPropagation();
251
+ const handleNewColumn = useCallback(() => {
252
+ const columns = model?.getColumnCount();
253
+ if (dxGrid && columns) {
254
+ dxGrid.scrollToColumn(columns - 1);
162
255
  }
163
- },
164
- [hasAttention, ignoreAttention],
165
- );
256
+ }, [model, dxGrid]);
166
257
 
167
- const handleNewColumn = useCallback(() => {
168
- const columns = model?.getColumnCount();
169
- if (dxGrid && columns) {
170
- dxGrid.scrollToColumn(columns - 1);
171
- }
172
- }, [model, dxGrid]);
173
-
174
- // TODO(burdon): Factor out?
175
- // TODO(burdon): Generalize to handle other value types (e.g., enums).
176
- const handleQuery = useCallback<NonNullable<TableCellEditorProps['onQuery']>>(
177
- async ({ field, props }, text) => {
178
- if (model && props.referenceSchema && field.referencePath) {
179
- const space = getSpace(model.table);
180
- invariant(space);
181
- const schema = space.db.schemaRegistry.getSchema(props.referenceSchema);
182
- if (schema) {
183
- const { objects } = await space.db.query(Filter.schema(schema)).run();
184
- const options = objects
185
- .map((obj) => {
186
- const value = getValue(obj, field.referencePath!);
187
- if (!value || typeof value !== 'string') {
188
- return undefined;
189
- }
258
+ // TODO(burdon): Factor out?
259
+ // TODO(burdon): Generalize to handle other value types (e.g., enums).
260
+ const handleQuery = useCallback<NonNullable<TableCellEditorProps['onQuery']>>(
261
+ async ({ field, props }, text) => {
262
+ if (model && props.referenceSchema && field.referencePath) {
263
+ const space = getSpace(model.table);
264
+ invariant(space);
265
+ const schema = space.db.schemaRegistry.getSchema(props.referenceSchema);
266
+ if (schema) {
267
+ const { objects } = await space.db.query(Filter.schema(schema)).run();
268
+ const options = objects
269
+ .map((obj) => {
270
+ const value = getValue(obj, field.referencePath!);
271
+ if (!value || typeof value !== 'string') {
272
+ return undefined;
273
+ }
274
+
275
+ return {
276
+ label: value,
277
+ data: obj,
278
+ };
279
+ })
280
+ .filter(isNotFalsy);
190
281
 
191
- return {
192
- label: value,
193
- data: obj,
194
- };
195
- })
196
- .filter(isNotFalsy);
197
-
198
- return [
199
- ...options,
200
- {
201
- label: t('create new object label', { text }),
202
- data: createOption(text),
203
- },
204
- ];
282
+ return [
283
+ ...options,
284
+ {
285
+ label: t('create new object label', { text }),
286
+ data: createOption(text),
287
+ },
288
+ ];
289
+ }
205
290
  }
206
- }
207
291
 
208
- return [];
209
- },
210
- [model],
211
- );
292
+ return [];
293
+ },
294
+ [model],
295
+ );
212
296
 
213
- if (!model) {
214
- return <span role='none' className='attention-surface' />;
215
- }
297
+ if (!model || !modals) {
298
+ return <span role='none' className='attention-surface' />;
299
+ }
216
300
 
217
- return (
218
- <Grid.Root id={model.table.id ?? 'table-grid'}>
219
- <TableCellEditor model={model} onEnter={handleEnter} onFocus={handleFocus} onQuery={handleQuery} />
220
- <Grid.Content
221
- onWheelCapture={handleWheel}
222
- className={mx('[--dx-grid-base:var(--surface-bg)]', inlineEndLine, blockEndLine)}
223
- frozen={frozen}
224
- columns={model.columnMeta.value}
225
- limitRows={model.getRowCount() ?? 0}
226
- limitColumns={model.table.view?.fields?.length ?? 0}
227
- onAxisResize={handleAxisResize}
228
- onClick={model?.handleGridClick}
229
- onKeyDown={handleKeyDown}
230
- overscroll='trap'
231
- ref={setDxGrid}
232
- />
233
- <RowActionsMenu model={model} />
234
- <ColumnActionsMenu model={model} />
235
- <ColumnSettings model={model} onNewColumn={handleNewColumn} />
236
- <RefPanel model={model} />
237
- <CreateRefPanel model={model} />
238
- </Grid.Root>
239
- );
240
- });
301
+ return (
302
+ <Grid.Root id={model.table.id ?? 'table-grid'}>
303
+ <TableCellEditor
304
+ model={model}
305
+ modals={modals}
306
+ onEnter={handleEnter}
307
+ onFocus={handleFocus}
308
+ onQuery={handleQuery}
309
+ />
310
+ <Grid.Content
311
+ onWheelCapture={handleWheel}
312
+ className={mx('[--dx-grid-base:var(--surface-bg)]', inlineEndLine, blockEndLine)}
313
+ frozen={frozen}
314
+ columns={model.columnMeta.value}
315
+ limitRows={model.getRowCount() ?? 0}
316
+ limitColumns={model.table.view?.target?.fields?.length ?? 0}
317
+ onAxisResize={handleAxisResize}
318
+ onClick={handleGridClick}
319
+ onKeyDown={handleKeyDown}
320
+ overscroll='trap'
321
+ ref={setDxGrid}
322
+ />
323
+ <RowActionsMenu model={model} modals={modals} />
324
+ <ColumnActionsMenu model={model} modals={modals} />
325
+ <ColumnSettings model={model} modals={modals} onNewColumn={handleNewColumn} />
326
+ <RefPanel model={model} modals={modals} />
327
+ <CreateRefPanel model={model} modals={modals} />
328
+ </Grid.Root>
329
+ );
330
+ },
331
+ );
241
332
 
242
333
  //
243
334
  // CellEditor
@@ -7,11 +7,11 @@ import '@dxos-theme';
7
7
  import { type Meta, type StoryObj } from '@storybook/react';
8
8
  import React, { useEffect, useMemo, useState } from 'react';
9
9
 
10
- import { type MutableSchema } from '@dxos/echo-schema';
10
+ import { type EchoSchema } from '@dxos/echo-schema';
11
11
  import { invariant } from '@dxos/invariant';
12
12
  import { faker } from '@dxos/random';
13
- import { Filter, useSpaces, useQuery, create } from '@dxos/react-client/echo';
14
- import { withClientProvider } from '@dxos/react-client/testing';
13
+ import { Filter, useQuery, create } from '@dxos/react-client/echo';
14
+ import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
15
15
  import { defaultSizeRow, Grid, type GridEditing } from '@dxos/react-ui-grid';
16
16
  import { ViewProjection, ViewType } from '@dxos/schema';
17
17
  import { withLayout, withTheme } from '@dxos/storybook-utils';
@@ -27,23 +27,24 @@ type StoryProps = {
27
27
  };
28
28
 
29
29
  const DefaultStory = ({ editing }: StoryProps) => {
30
- const spaces = useSpaces();
31
- const space = spaces[spaces.length - 1];
30
+ const { space } = useClientProvider();
31
+ invariant(space);
32
+
32
33
  const tables = useQuery(space, Filter.schema(TableType));
33
34
  const [table, setTable] = useState<TableType>();
34
- const [schema, setSchema] = useState<MutableSchema>();
35
+ const [schema, setSchema] = useState<EchoSchema>();
35
36
  useEffect(() => {
36
- if (tables.length && !table) {
37
+ if (space && tables.length && !table) {
37
38
  const table = tables[0];
38
39
  invariant(table.view);
39
40
  setTable(table);
40
- setSchema(space.db.schemaRegistry.getSchema(table.view.query.type));
41
+ setSchema(space.db.schemaRegistry.getSchema(table.view.target!.query.type));
41
42
  }
42
- }, [tables]);
43
+ }, [space, tables]);
43
44
 
44
45
  const projection = useMemo(() => {
45
46
  if (schema && table?.view) {
46
- return new ViewProjection(schema, table.view);
47
+ return new ViewProjection(schema, table.view.target!);
47
48
  }
48
49
  }, [schema, table?.view]);
49
50
 
@@ -83,9 +84,9 @@ const meta: Meta<StoryProps> = {
83
84
  types: [TableType, ViewType],
84
85
  createIdentity: true,
85
86
  createSpace: true,
86
- onSpaceCreated: ({ space }) => {
87
+ onSpaceCreated: async ({ space }) => {
87
88
  const table = space.db.add(create(TableType, {}));
88
- const schema = initializeTable({ space, table });
89
+ const schema = await initializeTable({ space, table });
89
90
  Array.from({ length: 10 }).forEach(() => {
90
91
  space.db.add(
91
92
  create(schema, {
@@ -24,7 +24,7 @@ import {
24
24
  import { type FieldProjection } from '@dxos/schema';
25
25
 
26
26
  import { completion } from './extension';
27
- import { type TableModel } from '../../model';
27
+ import { type TableModel, type ModalController } from '../../model';
28
28
 
29
29
  const newValue = Symbol.for('newValue');
30
30
 
@@ -39,6 +39,7 @@ export type QueryResult = Pick<Completion, 'label'> & { data: any };
39
39
 
40
40
  export type TableCellEditorProps = {
41
41
  model?: TableModel;
42
+ modals?: ModalController;
42
43
  onEnter?: (cell: DxGridPlanePosition) => void;
43
44
  onFocus?: DxGrid['refocus'];
44
45
  onQuery?: (field: FieldProjection, text: string) => Promise<QueryResult[]>;
@@ -46,6 +47,7 @@ export type TableCellEditorProps = {
46
47
 
47
48
  export const TableCellEditor = ({
48
49
  model,
50
+ modals,
49
51
  onEnter,
50
52
  onFocus,
51
53
  onQuery,
@@ -133,12 +135,12 @@ export const TableCellEditor = ({
133
135
  completion({
134
136
  onQuery: (text) => onQuery(fieldProjection, text),
135
137
  onMatch: (data) => {
136
- if (model && editing) {
138
+ if (model && editing && modals) {
137
139
  if (isCreateOption(data)) {
138
140
  const { field, props } = fieldProjection;
139
141
  if (props.referenceSchema) {
140
142
  suppressNextBlur.current = true;
141
- model.modalController.openCreateRef(
143
+ modals.openCreateRef(
142
144
  props.referenceSchema,
143
145
  document.querySelector(cellQuery(editing.index, gridId)),
144
146
  {
@@ -162,7 +164,7 @@ export const TableCellEditor = ({
162
164
  }
163
165
 
164
166
  return extension;
165
- }, [model, editing, fieldProjection, handleClose]);
167
+ }, [model, modals, editing, fieldProjection, handleClose]);
166
168
 
167
169
  const getCellContent = useCallback<GridCellEditorProps['getCellContent']>(() => {
168
170
  if (model && editing) {
@@ -0,0 +1,72 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback, useMemo } from 'react';
6
+
7
+ import { create } from '@dxos/live-object';
8
+ import { type ThemedClassName } from '@dxos/react-ui';
9
+ import {
10
+ createGapSeparator,
11
+ createMenuAction,
12
+ type MenuAction,
13
+ type MenuActionHandler,
14
+ MenuProvider,
15
+ ToolbarMenu,
16
+ useMenuActions,
17
+ } from '@dxos/react-ui-menu';
18
+
19
+ import { translationKey } from '../../translations';
20
+
21
+ export type TableToolbarActionProperties = { type: TableToolbarActionType };
22
+
23
+ export type TableToolbarAction = MenuAction<TableToolbarActionProperties>;
24
+
25
+ export type TableToolbarActionType = 'add-row' | 'comment' | 'save-view';
26
+
27
+ type TableToolbarState = Partial<{ viewDirty: boolean }>;
28
+
29
+ export type TableToolbarProps = ThemedClassName<
30
+ TableToolbarState & { onAction?: MenuActionHandler<TableToolbarAction>; attendableId?: string }
31
+ >;
32
+
33
+ const createTableToolbarActions = (state: TableToolbarState) => {
34
+ const add = createMenuAction<TableToolbarActionProperties>('add-row', {
35
+ type: 'add-row',
36
+ icon: 'ph--plus--regular',
37
+ label: ['add row', { ns: translationKey }],
38
+ testId: 'table.toolbar.add-row',
39
+ });
40
+ const save = createMenuAction<TableToolbarActionProperties>('save-view', {
41
+ type: 'save-view',
42
+ icon: 'ph--floppy-disk--regular',
43
+ label: ['save view label', { ns: translationKey }],
44
+ testId: 'table.toolbar.save-view',
45
+ iconOnly: false,
46
+ hidden: !state.viewDirty,
47
+ });
48
+ const gap = createGapSeparator();
49
+ const comment = createMenuAction('comment', {
50
+ type: 'comment',
51
+ icon: 'ph--chat-text--regular',
52
+ label: ['create comment', { ns: translationKey }],
53
+ testId: 'table.toolbar.comment',
54
+ });
55
+ const nodes = [add, save, ...gap.nodes, comment];
56
+ return {
57
+ nodes,
58
+ edges: nodes.map(({ id: target }) => ({ source: 'root', target })),
59
+ };
60
+ };
61
+
62
+ export const TableToolbar = ({ classNames, viewDirty, attendableId, onAction }: TableToolbarProps) => {
63
+ const state = useMemo(() => create<TableToolbarState>({ viewDirty }), []);
64
+ const actionsCreator = useCallback(() => createTableToolbarActions(state), [state]);
65
+ const menu = useMenuActions(actionsCreator);
66
+
67
+ return (
68
+ <MenuProvider {...menu} attendableId={attendableId} onAction={onAction as MenuActionHandler}>
69
+ <ToolbarMenu classNames={classNames} />
70
+ </MenuProvider>
71
+ );
72
+ };
@@ -2,4 +2,4 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './Toolbar';
5
+ export * from './TableToolbar';
@@ -3,4 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './Table';
6
- export * from './Toolbar';
6
+ export * from './TableToolbar';
@@ -6,7 +6,7 @@ import { useEffect } from 'react';
6
6
 
7
7
  import { setValue, toJsonSchema, S, TypeEnum, TypedObject, FormatEnum } from '@dxos/echo-schema';
8
8
  import { faker } from '@dxos/random';
9
- import { create } from '@dxos/react-client/echo';
9
+ import { create, makeRef } from '@dxos/react-client/echo';
10
10
  import { createView, type ViewProjection } from '@dxos/schema';
11
11
  import {} from '@dxos/schema';
12
12
 
@@ -22,11 +22,13 @@ export const TestSchema = TypedObject({ typename: 'example.com/type/Test', versi
22
22
 
23
23
  export const createTable = (schema = TestSchema) => {
24
24
  return create(TableType, {
25
- view: createView({
26
- name: 'Test',
27
- typename: schema.typename,
28
- jsonSchema: toJsonSchema(schema),
29
- }),
25
+ view: makeRef(
26
+ createView({
27
+ name: 'Test',
28
+ typename: schema.typename,
29
+ jsonSchema: toJsonSchema(schema),
30
+ }),
31
+ ),
30
32
  });
31
33
  };
32
34
 
@@ -71,7 +73,7 @@ export const useSimulator = ({ items, table, insertInterval, updateInterval }: S
71
73
 
72
74
  const i = setInterval(() => {
73
75
  const rowIdx = Math.floor(Math.random() * items.length);
74
- const fields = table.view?.fields ?? [];
76
+ const fields = table.view?.target?.fields ?? [];
75
77
  const columnIdx = Math.floor(Math.random() * fields.length);
76
78
  const projection: ViewProjection = (table as any)._projection;
77
79
  const field = fields[columnIdx];
@@ -111,5 +113,5 @@ export const useSimulator = ({ items, table, insertInterval, updateInterval }: S
111
113
  }, updateInterval);
112
114
 
113
115
  return () => clearInterval(i);
114
- }, [items, table.view?.fields, updateInterval]);
116
+ }, [items, table.view?.target?.fields, updateInterval]);
115
117
  };