@dxos/react-ui-table 0.8.3-staging.0fa589b → 0.8.4-main.1da679c

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 (148) hide show
  1. package/dist/lib/browser/chunk-63UUF4IG.mjs +66 -0
  2. package/dist/lib/browser/chunk-63UUF4IG.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-BBXDTWCX.mjs +1822 -0
  4. package/dist/lib/browser/chunk-BBXDTWCX.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +816 -2152
  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 +124 -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-K734DJB7.mjs +67 -0
  12. package/dist/lib/node-esm/chunk-K734DJB7.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-RNQDU7IQ.mjs +1823 -0
  14. package/dist/lib/node-esm/chunk-RNQDU7IQ.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +816 -2152
  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 +125 -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 +50 -2
  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 +44 -0
  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 +3 -2
  36. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  37. package/dist/types/src/components/Table/Table.stories.d.ts +42 -3
  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 +8 -3
  42. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  43. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +9 -15
  44. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  45. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +39 -3
  46. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  47. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  48. package/dist/types/src/components/testing.d.ts +3 -40
  49. package/dist/types/src/components/testing.d.ts.map +1 -1
  50. package/dist/types/src/hooks/index.d.ts +1 -0
  51. package/dist/types/src/hooks/index.d.ts.map +1 -1
  52. package/dist/types/src/hooks/useAddRow.d.ts +14 -0
  53. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -0
  54. package/dist/types/src/hooks/useTableModel.d.ts +8 -7
  55. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  56. package/dist/types/src/index.d.ts +1 -2
  57. package/dist/types/src/index.d.ts.map +1 -1
  58. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  59. package/dist/types/src/model/selection-model.d.ts +2 -1
  60. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  61. package/dist/types/src/model/table-model.d.ts +59 -21
  62. package/dist/types/src/model/table-model.d.ts.map +1 -1
  63. package/dist/types/src/model/table-presentation.d.ts +10 -1
  64. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  65. package/dist/types/src/model/table-sorting.d.ts +3 -4
  66. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  67. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  68. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  69. package/dist/types/src/testing/index.d.ts +2 -0
  70. package/dist/types/src/testing/index.d.ts.map +1 -0
  71. package/dist/types/src/testing/useTestTableModel.d.ts +22 -0
  72. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -0
  73. package/dist/types/src/translations.d.ts +5 -6
  74. package/dist/types/src/translations.d.ts.map +1 -1
  75. package/dist/types/src/types/index.d.ts +1 -1
  76. package/dist/types/src/types/index.d.ts.map +1 -1
  77. package/dist/types/src/types/schema.d.ts +28 -0
  78. package/dist/types/src/types/schema.d.ts.map +1 -0
  79. package/dist/types/src/util/dynamic-table.d.ts +4 -5
  80. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  81. package/dist/types/src/util/index.d.ts +0 -1
  82. package/dist/types/src/util/index.d.ts.map +1 -1
  83. package/dist/types/src/util/table-buttons.d.ts +15 -1
  84. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  85. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  86. package/dist/types/tsconfig.tsbuildinfo +1 -1
  87. package/package.json +57 -44
  88. package/src/components/Table/ColumnActionsMenu.tsx +1 -1
  89. package/src/components/Table/ColumnSettings.tsx +15 -11
  90. package/src/components/Table/CreateRefPanel.tsx +30 -11
  91. package/src/components/Table/DynamicTable.stories.tsx +8 -5
  92. package/src/components/Table/DynamicTable.tsx +17 -10
  93. package/src/components/Table/{relations.stories.tsx → Relations.stories.tsx} +56 -47
  94. package/src/components/Table/RowActionsMenu.tsx +3 -3
  95. package/src/components/Table/Table.stories.tsx +126 -159
  96. package/src/components/Table/Table.tsx +169 -123
  97. package/src/components/TableCellEditor/CellValidationMessage.tsx +7 -2
  98. package/src/components/TableCellEditor/FormCellEditor.tsx +158 -48
  99. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +31 -47
  100. package/src/components/TableCellEditor/TableCellEditor.tsx +106 -111
  101. package/src/components/TableToolbar/TableToolbar.tsx +2 -2
  102. package/src/components/testing.ts +7 -24
  103. package/src/hooks/index.ts +1 -0
  104. package/src/hooks/useAddRow.ts +38 -0
  105. package/src/hooks/useTableModel.ts +21 -17
  106. package/src/index.ts +1 -2
  107. package/src/model/modal-controller.ts +1 -1
  108. package/src/model/selection-model.ts +6 -3
  109. package/src/model/table-model.test.ts +12 -9
  110. package/src/model/table-model.ts +288 -88
  111. package/src/model/table-presentation.test.ts +12 -8
  112. package/src/model/table-presentation.ts +263 -138
  113. package/src/model/table-sorting.ts +22 -13
  114. package/src/playwright/playwright.config.ts +17 -0
  115. package/src/playwright/smoke.spec.ts +137 -44
  116. package/src/testing/index.ts +5 -0
  117. package/src/testing/useTestTableModel.ts +124 -0
  118. package/src/translations.ts +7 -5
  119. package/src/types/index.ts +2 -2
  120. package/src/types/schema.ts +73 -0
  121. package/src/util/dynamic-table.ts +24 -21
  122. package/src/util/index.ts +0 -1
  123. package/src/util/schema.ts +1 -1
  124. package/src/util/table-buttons.ts +28 -4
  125. package/src/util/table-controls.ts +4 -1
  126. package/dist/lib/browser/chunk-TL7CKQQV.mjs +0 -27
  127. package/dist/lib/browser/chunk-TL7CKQQV.mjs.map +0 -7
  128. package/dist/lib/node/chunk-O2VPBNS5.cjs +0 -50
  129. package/dist/lib/node/chunk-O2VPBNS5.cjs.map +0 -7
  130. package/dist/lib/node/index.cjs +0 -2739
  131. package/dist/lib/node/index.cjs.map +0 -7
  132. package/dist/lib/node/meta.json +0 -1
  133. package/dist/lib/node/types/index.cjs +0 -31
  134. package/dist/lib/node/types/index.cjs.map +0 -7
  135. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs +0 -29
  136. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs.map +0 -7
  137. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  138. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  139. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  140. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  141. package/dist/types/src/types/table.d.ts +0 -56
  142. package/dist/types/src/types/table.d.ts.map +0 -1
  143. package/dist/types/src/util/table.d.ts +0 -14
  144. package/dist/types/src/util/table.d.ts.map +0 -1
  145. package/src/components/TableCellEditor/extension.ts +0 -61
  146. package/src/playwright/playwright.config.cts +0 -18
  147. package/src/types/table.ts +0 -27
  148. package/src/util/table.ts +0 -127
@@ -3,62 +3,59 @@
3
3
  //
4
4
 
5
5
  import { type Completion } from '@codemirror/autocomplete';
6
+ import { EditorView } from '@codemirror/view';
6
7
  import { type Schema } from 'effect/Schema';
7
8
  import React, { useCallback, useMemo, useRef, useState } from 'react';
8
9
 
10
+ import { debounce } from '@dxos/async';
11
+ import type { Client } from '@dxos/client';
9
12
  import { FormatEnum, TypeEnum } from '@dxos/echo-schema';
10
13
  import { invariant } from '@dxos/invariant';
11
- import { type DxGrid } from '@dxos/lit-grid';
12
- import { useThemeContext } from '@dxos/react-ui';
14
+ import { type DxGridAxis, type DxGridPosition } from '@dxos/lit-grid';
13
15
  import { createMarkdownExtensions } from '@dxos/react-ui-editor';
14
16
  import {
15
- cellQuery,
16
- editorKeys,
17
- parseCellIndex,
18
- useGridContext,
19
- type DxGridPlanePosition,
17
+ type EditorBlurHandler,
20
18
  type EditorKeyEvent,
21
19
  type EditorKeyOrBlurHandler,
22
- type EditorBlurHandler,
23
20
  GridCellEditor,
24
21
  type GridCellEditorProps,
25
22
  type GridScopedProps,
23
+ editorKeys,
24
+ parseCellIndex,
25
+ useGridContext,
26
26
  } from '@dxos/react-ui-grid';
27
- import { tagPickerExtension, createLinks } from '@dxos/react-ui-tag-picker';
27
+ import { createLinks, tagPicker } from '@dxos/react-ui-tag-picker';
28
28
  import { type FieldProjection } from '@dxos/schema';
29
29
 
30
+ import { type ModalController, type TableModel } from '../../model';
31
+
30
32
  import { CellValidationMessage } from './CellValidationMessage';
31
33
  import { FormCellEditor } from './FormCellEditor';
32
- import { completion } from './extension';
33
- import { type TableModel, type ModalController } from '../../model';
34
-
35
- const newValue = Symbol.for('newValue');
36
34
 
37
35
  /**
38
36
  * Option to create new object/value.
39
37
  */
40
- export const createOption = (text: string) => ({ [newValue]: true, text });
41
-
42
- const isCreateOption = (data: any) => typeof data === 'object' && data[newValue];
43
-
44
38
  export type QueryResult = Pick<Completion, 'label'> & { data: any };
45
39
 
46
40
  export type TableCellEditorProps = {
47
41
  model?: TableModel;
48
42
  modals?: ModalController;
49
43
  schema?: Schema.AnyNoContext;
50
- onEnter?: (cell: DxGridPlanePosition) => void;
51
- onFocus?: DxGrid['refocus'];
52
- onQuery?: (field: FieldProjection, text: string) => Promise<QueryResult[]>;
44
+ onFocus?: (axis?: DxGridAxis, delta?: -1 | 0 | 1, cell?: DxGridPosition) => void;
45
+ onSave?: () => void;
46
+ client?: Client;
53
47
  };
54
48
 
49
+ const adaptValidationMessage = (message: string | null) =>
50
+ message ? (message.endsWith('is missing') ? 'Can’t be blank' : message) : null;
51
+
55
52
  export const TableValueEditor = ({
56
53
  model,
57
54
  modals,
58
55
  schema,
59
- onEnter,
60
56
  onFocus,
61
- onQuery,
57
+ onSave,
58
+ client,
62
59
  __gridScope,
63
60
  }: GridScopedProps<TableCellEditorProps>) => {
64
61
  const { editing } = useGridContext('TableValueEditor', __gridScope);
@@ -69,41 +66,48 @@ export const TableValueEditor = ({
69
66
  }
70
67
 
71
68
  const { col } = parseCellIndex(editing.index);
72
- const field = model.projection.view.fields[col];
69
+ const field = model.projection.fields[col];
73
70
  const fieldProjection = model.projection.getFieldProjection(field.id);
74
71
  invariant(fieldProjection);
75
72
  return fieldProjection;
76
73
  }, [model, editing]);
77
74
 
78
- if (fieldProjection?.props.type === TypeEnum.Array) {
79
- return <FormCellEditor fieldProjection={fieldProjection} model={model} schema={schema} __gridScope={__gridScope} />;
75
+ if (
76
+ fieldProjection?.props.type === TypeEnum.Array ||
77
+ fieldProjection?.props.format === FormatEnum.SingleSelect ||
78
+ fieldProjection?.props.format === FormatEnum.Ref
79
+ // TODO(thure): Support `FormatEnum.MultiSelect`
80
+ ) {
81
+ return (
82
+ <FormCellEditor
83
+ fieldProjection={fieldProjection}
84
+ model={model}
85
+ schema={schema}
86
+ __gridScope={__gridScope}
87
+ onSave={onSave}
88
+ client={client}
89
+ modals={modals}
90
+ />
91
+ );
80
92
  }
81
93
 
82
94
  // For all other types, use the existing cell editor
83
- return (
84
- <TableCellEditor
85
- model={model}
86
- modals={modals}
87
- onEnter={onEnter}
88
- onFocus={onFocus}
89
- onQuery={onQuery}
90
- __gridScope={__gridScope}
91
- />
92
- );
95
+ return <TableCellEditor model={model} modals={modals} onFocus={onFocus} onSave={onSave} __gridScope={__gridScope} />;
93
96
  };
94
97
 
98
+ const editorSlots = { scroll: { className: '!plb-[--dx-grid-cell-editor-padding-block]' } };
99
+
95
100
  export const TableCellEditor = ({
96
101
  model,
97
102
  modals,
98
- onEnter,
99
103
  onFocus,
100
- onQuery,
104
+ onSave,
101
105
  __gridScope,
102
106
  }: GridScopedProps<TableCellEditorProps>) => {
103
- const { id: gridId, editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
107
+ const { editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
104
108
  const suppressNextBlur = useRef(false);
105
- const { themeMode } = useThemeContext();
106
- const [_validationError, setValidationError] = useState<string | null>(null);
109
+ const [validationError, setValidationError] = useState<string | null>(null);
110
+ const [validationVariant, setValidationVariant] = useState<'error' | 'warning'>('error');
107
111
 
108
112
  const fieldProjection = useMemo<FieldProjection | undefined>(() => {
109
113
  if (!model || !editing) {
@@ -111,7 +115,7 @@ export const TableCellEditor = ({
111
115
  }
112
116
 
113
117
  const { col } = parseCellIndex(editing.index);
114
- const field = model.projection.view.fields[col];
118
+ const field = model.projection.fields[col];
115
119
  const fieldProjection = model.projection.getFieldProjection(field.id);
116
120
  invariant(fieldProjection);
117
121
  return fieldProjection;
@@ -124,34 +128,39 @@ export const TableCellEditor = ({
124
128
  }
125
129
 
126
130
  const cell = parseCellIndex(editing.index);
131
+ const validationResult = await model.validateCellData(cell, value);
127
132
 
128
- // Validate the value
129
- const result = await model.validateCellData(cell, value);
130
-
131
- if (result.valid) {
133
+ if (validationResult.valid) {
132
134
  setValidationError(null);
133
135
  model.setCellData(cell, value);
134
- onEnter?.(cell);
135
136
  onFocus?.();
136
137
  setEditing(null);
138
+ onSave?.();
137
139
  } else {
138
- setValidationError(result.error);
139
- // Editor stays open on validation failure
140
+ setValidationError(adaptValidationMessage(validationResult.error));
141
+ setValidationVariant('error');
140
142
  }
141
143
  },
142
- [model, editing, onEnter, onFocus, setEditing],
144
+ [model, editing, onFocus, setEditing],
143
145
  );
144
146
 
145
147
  const handleBlur = useCallback<EditorBlurHandler>(
146
148
  async (value) => {
147
- if (!model || !editing) {
148
- return;
149
- }
150
149
  if (suppressNextBlur.current) {
151
150
  suppressNextBlur.current = false;
151
+ } else if (model && editing) {
152
+ // Save silently if validation passes
153
+ const cell = parseCellIndex(editing.index);
154
+ if (value !== undefined) {
155
+ const result = await model.validateCellData(cell, value);
156
+ if (result.valid) {
157
+ setValidationError(null);
158
+ model.setCellData(cell, value);
159
+ setEditing(null);
160
+ onSave?.();
161
+ }
162
+ }
152
163
  }
153
-
154
- // Don't save on blur - let handleClose handle validation and saving
155
164
  },
156
165
  [model, editing],
157
166
  );
@@ -163,39 +172,42 @@ export const TableCellEditor = ({
163
172
  }
164
173
 
165
174
  const cell = parseCellIndex(editing.index);
166
-
167
175
  if (value !== undefined) {
168
176
  // Pre-commit validation check.
169
177
  const result = await model.validateCellData(cell, value);
170
178
 
171
179
  if (result.valid) {
180
+ suppressNextBlur.current = true;
172
181
  setValidationError(null);
173
182
  model.setCellData(cell, value);
174
183
  setEditing(null);
175
- onEnter?.(cell);
184
+ onSave?.();
176
185
  if (event && onFocus) {
177
- onFocus(determineNavigationAxis(event), determineNavigationDelta(event));
186
+ onFocus(determineNavigationAxis(event), determineNavigationDelta(event), cell);
178
187
  }
179
188
  } else {
180
189
  setValidationError(result.error);
190
+ setValidationVariant('error');
181
191
  }
182
192
  } else {
193
+ suppressNextBlur.current = true;
183
194
  setValidationError(null);
184
195
  setEditing(null);
196
+ onSave?.();
185
197
  if (event && onFocus) {
186
198
  onFocus(determineNavigationAxis(event), determineNavigationDelta(event));
187
199
  }
188
200
  }
189
201
  },
190
- [model, editing, onFocus, onEnter, fieldProjection, setEditing],
202
+ [model, editing, onFocus, fieldProjection, setEditing, onSave],
191
203
  );
192
204
 
193
- const extension = useMemo(() => {
205
+ const extensions = useMemo(() => {
194
206
  if (!fieldProjection) {
195
207
  return [];
196
208
  }
197
209
 
198
- const extension = [
210
+ const extensions = [
199
211
  editorKeys({
200
212
  onClose: handleClose,
201
213
  ...(editing?.initialContent && { onNav: handleClose }),
@@ -203,20 +215,17 @@ export const TableCellEditor = ({
203
215
  ];
204
216
 
205
217
  const format = fieldProjection.props.format;
206
-
207
218
  if (format === FormatEnum.SingleSelect || format === FormatEnum.MultiSelect) {
208
219
  // TODO(ZaymonFC): Reconcile this with the TagPicker component?
209
- // Add markdown extensions needed by tag picker.
210
- extension.push(createMarkdownExtensions({ themeMode }));
211
-
212
220
  const options = fieldProjection.props.options || [];
213
-
214
221
  const mode = format === FormatEnum.SingleSelect ? ('single-select' as const) : ('multi-select' as const);
215
222
 
216
- extension.push(
217
- tagPickerExtension({
223
+ // Add markdown extensions needed by tag picker.
224
+ extensions.push(createMarkdownExtensions());
225
+ extensions.push(
226
+ tagPicker({
218
227
  mode,
219
- inGrid: true,
228
+ keymap: false,
220
229
  onSearch: (text, selectedIds) => {
221
230
  return options
222
231
  .filter(
@@ -246,49 +255,36 @@ export const TableCellEditor = ({
246
255
  );
247
256
  }
248
257
 
249
- if (onQuery) {
250
- switch (fieldProjection.props.format) {
251
- case FormatEnum.Ref: {
252
- extension.push([
253
- completion({
254
- onQuery: (text) => onQuery(fieldProjection, text),
255
- onMatch: (data) => {
256
- if (model && editing && modals) {
257
- if (isCreateOption(data)) {
258
- const { field, props } = fieldProjection;
259
- if (props.referenceSchema) {
260
- suppressNextBlur.current = true;
261
- modals.openCreateRef(
262
- props.referenceSchema,
263
- document.querySelector(cellQuery(editing.index, gridId)),
264
- {
265
- [field.referencePath!]: data.text,
266
- },
267
- (data) => {
268
- void handleEnter(data);
269
- },
270
- );
271
- }
272
- } else {
273
- void handleEnter(data);
274
- }
275
- }
276
- },
277
- }),
278
- ]);
279
- break;
280
- }
281
- }
258
+ // Add validation extension to handle content changes.
259
+ if (model && editing) {
260
+ extensions.push(
261
+ EditorView.updateListener.of(
262
+ debounce((update) => {
263
+ const content = update.state.doc.toString();
264
+ const cell = parseCellIndex(editing.index);
265
+
266
+ // Perform validation on content change.
267
+ void model.validateCellData(cell, content).then((result) => {
268
+ if (result.valid) {
269
+ setValidationError(null);
270
+ } else {
271
+ setValidationError(result.error);
272
+ setValidationVariant('error');
273
+ }
274
+ });
275
+ }, 10),
276
+ ),
277
+ );
282
278
  }
283
279
 
284
- return extension;
285
- }, [model, modals, editing, fieldProjection, handleClose, themeMode]);
280
+ return extensions;
281
+ }, [model, modals, editing, fieldProjection, handleClose]);
286
282
 
287
283
  const getCellContent = useCallback<GridCellEditorProps['getCellContent']>(() => {
288
284
  if (model && editing) {
289
285
  const cell = parseCellIndex(editing.index);
290
286
  const { col } = cell;
291
- const field = model.projection.view.fields[col];
287
+ const field = model.projection.fields[col];
292
288
  const fieldProjection = model.projection.getFieldProjection(field.id);
293
289
 
294
290
  if (
@@ -296,13 +292,12 @@ export const TableCellEditor = ({
296
292
  fieldProjection?.props.format === FormatEnum.MultiSelect
297
293
  ) {
298
294
  const value = model.getCellData(cell);
299
-
300
295
  if (value !== undefined) {
301
296
  const options = fieldProjection.props.options || [];
302
-
303
- if (fieldProjection.props.format === FormatEnum.MultiSelect && Array.isArray(value)) {
297
+ if (fieldProjection.props.format === FormatEnum.MultiSelect) {
304
298
  const tagItems = value
305
- .map((id) => {
299
+ .split(',')
300
+ .map((id: string) => {
306
301
  const option = options.find((o) => o.id === id);
307
302
  if (option) {
308
303
  return {
@@ -313,7 +308,7 @@ export const TableCellEditor = ({
313
308
  }
314
309
  return undefined;
315
310
  })
316
- .filter((item): item is { id: any; label: string; hue: any } => item !== undefined);
311
+ .filter((item: any): item is { id: any; label: string; hue: any } => item !== undefined);
317
312
 
318
313
  return createLinks(tagItems);
319
314
  } else {
@@ -341,8 +336,8 @@ export const TableCellEditor = ({
341
336
 
342
337
  return (
343
338
  <>
344
- <CellValidationMessage validationError={_validationError} __gridScope={__gridScope} />
345
- <GridCellEditor extension={extension} getCellContent={getCellContent} onBlur={handleBlur} />
339
+ <CellValidationMessage validationError={validationError} variant={validationVariant} __gridScope={__gridScope} />
340
+ <GridCellEditor extensions={extensions} getCellContent={getCellContent} onBlur={handleBlur} slots={editorSlots} />
346
341
  </>
347
342
  );
348
343
  };
@@ -11,11 +11,11 @@ import {
11
11
  type ActionGraphEdges,
12
12
  type ActionGraphNodes,
13
13
  type ActionGraphProps,
14
- createGapSeparator,
15
- createMenuAction,
16
14
  type MenuAction,
17
15
  MenuProvider,
18
16
  ToolbarMenu,
17
+ createGapSeparator,
18
+ createMenuAction,
19
19
  useMenuActions,
20
20
  } from '@dxos/react-ui-menu';
21
21
  import { rxFromSignal } from '@dxos/react-ui-menu';
@@ -5,13 +5,10 @@
5
5
  import { Schema } from 'effect';
6
6
  import { useEffect } from 'react';
7
7
 
8
- import { Obj, Ref } from '@dxos/echo';
9
- import { FormatEnum, ObjectId, setValue, toJsonSchema, TypedObject, TypeEnum } from '@dxos/echo-schema';
8
+ import { FormatEnum, ObjectId, TypeEnum, TypedObject, setValue } from '@dxos/echo-schema';
9
+ import { live } from '@dxos/live-object';
10
10
  import { faker } from '@dxos/random';
11
- import { live } from '@dxos/react-client/echo';
12
- import { createView, type ViewProjection } from '@dxos/schema';
13
-
14
- import { TableType } from '../types';
11
+ import { type ProjectionModel } from '@dxos/schema';
15
12
 
16
13
  export const TestSchema = TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
17
14
  id: ObjectId,
@@ -21,18 +18,6 @@ export const TestSchema = TypedObject({ typename: 'example.com/type/Test', versi
21
18
  netWorth: Schema.optional(Schema.Number),
22
19
  });
23
20
 
24
- export const createTable = (schema = TestSchema) => {
25
- return Obj.make(TableType, {
26
- view: Ref.make(
27
- createView({
28
- name: 'Test',
29
- typename: schema.typename,
30
- jsonSchema: toJsonSchema(schema),
31
- }),
32
- ),
33
- });
34
- };
35
-
36
21
  export const createItems = (n: number) => {
37
22
  const { data } = live({
38
23
  data: Array.from({ length: n }, () => ({
@@ -47,13 +32,13 @@ export const createItems = (n: number) => {
47
32
  };
48
33
 
49
34
  export type SimulatorProps = {
50
- table: TableType;
35
+ projection: ProjectionModel;
51
36
  items: any[];
52
37
  insertInterval?: number;
53
38
  updateInterval?: number;
54
39
  };
55
40
 
56
- export const useSimulator = ({ items, table, insertInterval, updateInterval }: SimulatorProps) => {
41
+ export const useSimulator = ({ items, projection, insertInterval, updateInterval }: SimulatorProps) => {
57
42
  useEffect(() => {
58
43
  if (!insertInterval) {
59
44
  return;
@@ -74,10 +59,8 @@ export const useSimulator = ({ items, table, insertInterval, updateInterval }: S
74
59
 
75
60
  const i = setInterval(() => {
76
61
  const rowIdx = Math.floor(Math.random() * items.length);
77
- const fields = table.view?.target?.fields ?? [];
62
+ const fields = projection.fields ?? [];
78
63
  const columnIdx = Math.floor(Math.random() * fields.length);
79
- // TODO(ZaymonFC): ... This is borked.
80
- const projection: ViewProjection = (table as any)._projection;
81
64
  const field = fields[columnIdx];
82
65
  const item = items[rowIdx];
83
66
 
@@ -115,5 +98,5 @@ export const useSimulator = ({ items, table, insertInterval, updateInterval }: S
115
98
  }, updateInterval);
116
99
 
117
100
  return () => clearInterval(i);
118
- }, [items, table.view?.target?.fields, updateInterval]);
101
+ }, [items, projection.fields, updateInterval]);
119
102
  };
@@ -3,3 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './useTableModel';
6
+ export * from './useAddRow';
@@ -0,0 +1,38 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Schema } from 'effect';
6
+ import { useCallback } from 'react';
7
+
8
+ import { type Space } from '@dxos/client/echo';
9
+ import { Obj } from '@dxos/echo';
10
+
11
+ import { type InsertRowResult } from '../model';
12
+
13
+ export type UseAddRowParams = {
14
+ space?: Space;
15
+ schema?: Schema.Schema.AnyNoContext;
16
+ };
17
+
18
+ /**
19
+ * Hook that provides a safe row creation function that handles schema validation failures.
20
+ * Returns a callback that attempts to create an object and returns boolean success/failure.
21
+ * Can accept optional data to create objects with specific content (used for committing draft rows).
22
+ */
23
+ export const useAddRow = ({ space, schema }: UseAddRowParams) => {
24
+ return useCallback(
25
+ (data?: any): InsertRowResult => {
26
+ if (space && schema) {
27
+ try {
28
+ space.db.add(Obj.make(schema, data ?? {}));
29
+ return 'final';
30
+ } catch (error) {
31
+ return 'draft';
32
+ }
33
+ }
34
+ return 'final';
35
+ },
36
+ [space, schema],
37
+ );
38
+ };
@@ -4,19 +4,21 @@
4
4
 
5
5
  import { effect } from '@preact/signals-core';
6
6
  import orderBy from 'lodash.orderby';
7
- import { useEffect, useState } from 'react';
7
+ import { useEffect, useMemo, useState } from 'react';
8
8
 
9
- import { fullyQualifiedId, getSpace, type Live } from '@dxos/react-client/echo';
10
- import { useSelectionActions } from '@dxos/react-ui-attention';
11
- import { type ViewProjection } from '@dxos/schema';
9
+ import { type JsonSchemaType } from '@dxos/echo-schema';
10
+ import { type Live } from '@dxos/live-object';
11
+ import { fullyQualifiedId } from '@dxos/react-client/echo';
12
+ import { useSelected, useSelectionActions } from '@dxos/react-ui-attention';
13
+ import { type DataType, type ProjectionModel } from '@dxos/schema';
12
14
  import { isNonNullable } from '@dxos/util';
13
15
 
14
- import { type TableRow, TableModel, type TableModelProps, type TableRowAction } from '../model';
15
- import { type TableType } from '../types';
16
+ import { TableModel, type TableModelProps, type TableRow, type TableRowAction } from '../model';
16
17
 
17
18
  export type UseTableModelParams<T extends TableRow = TableRow> = {
18
- table?: TableType;
19
- projection?: ViewProjection;
19
+ view?: DataType.View;
20
+ schema?: JsonSchemaType;
21
+ projection?: ProjectionModel;
20
22
  rows?: Live<T>[];
21
23
  rowActions?: TableRowAction[];
22
24
  onSelectionChanged?: (selection: string[]) => void;
@@ -27,7 +29,8 @@ export type UseTableModelParams<T extends TableRow = TableRow> = {
27
29
  >;
28
30
 
29
31
  export const useTableModel = <T extends TableRow = TableRow>({
30
- table,
32
+ view,
33
+ schema,
31
34
  projection,
32
35
  rows,
33
36
  rowActions,
@@ -36,21 +39,24 @@ export const useTableModel = <T extends TableRow = TableRow>({
36
39
  onRowAction,
37
40
  ...props
38
41
  }: UseTableModelParams<T>): TableModel<T> | undefined => {
42
+ const selected = useSelected(view && fullyQualifiedId(view), 'multi');
43
+ const initialSelection = useMemo(() => selected, [view]);
44
+
39
45
  const [model, setModel] = useState<TableModel<T>>();
40
46
  useEffect(() => {
41
- if (!table || !projection) {
47
+ if (!view || !schema) {
42
48
  return;
43
49
  }
44
50
 
45
51
  let model: TableModel<T> | undefined;
46
52
  const t = setTimeout(async () => {
47
53
  model = new TableModel<T>({
48
- id: fullyQualifiedId(table),
49
- space: getSpace(table),
50
- view: table.view?.target,
54
+ view,
55
+ schema,
51
56
  projection,
52
57
  features,
53
58
  rowActions,
59
+ initialSelection,
54
60
  onRowAction,
55
61
  ...props,
56
62
  });
@@ -62,7 +68,7 @@ export const useTableModel = <T extends TableRow = TableRow>({
62
68
  clearTimeout(t);
63
69
  void model?.close();
64
70
  };
65
- }, [table, projection, table?.view?.target, features, rowActions]); // TODO(burdon): Trigger if callbacks change?
71
+ }, [view, schema, projection, features, rowActions, initialSelection]); // TODO(burdon): Trigger if callbacks change?
66
72
 
67
73
  // Update data.
68
74
  useEffect(() => {
@@ -77,9 +83,7 @@ export const useTableModel = <T extends TableRow = TableRow>({
77
83
  }
78
84
  }, [model, rows]);
79
85
 
80
- const { multiSelect, clear } = useSelectionActions(
81
- [table?.id, table?.view?.target?.query.typename].filter(isNonNullable),
82
- );
86
+ const { multiSelect, clear } = useSelectionActions([model?.id].filter(isNonNullable));
83
87
 
84
88
  useEffect(() => {
85
89
  if (!model) {
package/src/index.ts CHANGED
@@ -2,12 +2,11 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import translations from './translations';
5
+ import { translations } from './translations';
6
6
 
7
7
  export * from './components';
8
8
  export * from './hooks';
9
9
  export * from './model';
10
- export * from './types';
11
10
  export * from './util';
12
11
 
13
12
  export { translations };
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { signal } from '@preact/signals-core';
6
- import { type RefObject, type MutableRefObject } from 'react';
6
+ import { type MutableRefObject, type RefObject } from 'react';
7
7
 
8
8
  import { type Live } from '@dxos/live-object';
9
9
  import { log } from '@dxos/log';
@@ -2,17 +2,18 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { computed, effect, signal, type ReadonlySignal } from '@preact/signals-core';
5
+ import { type ReadonlySignal, type Signal, computed, effect, signal } from '@preact/signals-core';
6
6
 
7
7
  import { Resource } from '@dxos/context';
8
8
 
9
- import { type TableRow } from './table-model';
10
9
  import { touch } from '../util';
11
10
 
11
+ import { type TableRow } from './table-model';
12
+
12
13
  export type SelectionMode = 'single' | 'multiple';
13
14
 
14
15
  export class SelectionModel<T extends TableRow> extends Resource {
15
- private readonly _selection = signal<Set<string>>(new Set());
16
+ private readonly _selection: Signal<Set<string>>;
16
17
 
17
18
  private readonly _validSelectedIds = computed<Set<string>>(() => {
18
19
  const rows = this._rows.value;
@@ -38,9 +39,11 @@ export class SelectionModel<T extends TableRow> extends Resource {
38
39
  constructor(
39
40
  private readonly _rows: ReadonlySignal<T[]>,
40
41
  private readonly _selectionMode: SelectionMode,
42
+ private readonly _initialSelection: string[],
41
43
  private readonly _onSelectionChanged?: () => void,
42
44
  ) {
43
45
  super();
46
+ this._selection = signal(new Set(this._initialSelection));
44
47
  }
45
48
 
46
49
  protected override async _open(): Promise<void> {