@dxos/react-ui-table 0.8.4-main.5acf9ea → 0.8.4-main.5ea62a8

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 (119) 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-5XLSHLOG.mjs → chunk-RQYXNWWN.mjs} +531 -460
  4. package/dist/lib/browser/chunk-RQYXNWWN.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +685 -623
  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 +6 -4
  9. package/dist/lib/browser/testing/index.mjs.map +3 -3
  10. package/dist/lib/browser/types/index.mjs +3 -3
  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-Y66YLSTC.mjs → chunk-QNDUGIS7.mjs} +531 -460
  14. package/dist/lib/node-esm/chunk-QNDUGIS7.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +685 -623
  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 +6 -4
  19. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  20. package/dist/lib/node-esm/types/index.mjs +3 -3
  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 +1 -1
  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.map +1 -1
  28. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +50 -2
  29. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  30. package/dist/types/src/components/Table/Relations.stories.d.ts +44 -0
  31. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
  32. package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
  33. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  34. package/dist/types/src/components/Table/Table.d.ts +3 -2
  35. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  36. package/dist/types/src/components/Table/Table.stories.d.ts +42 -3
  37. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +7 -3
  40. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +8 -15
  42. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  43. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +39 -3
  44. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  45. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  46. package/dist/types/src/hooks/useAddRow.d.ts +2 -1
  47. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  48. package/dist/types/src/hooks/useTableModel.d.ts +1 -1
  49. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  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 +7 -5
  53. package/dist/types/src/model/table-model.d.ts.map +1 -1
  54. package/dist/types/src/model/table-presentation.d.ts +2 -1
  55. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  56. package/dist/types/src/model/table-sorting.d.ts +2 -2
  57. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  58. package/dist/types/src/testing/useTestTableModel.d.ts +4 -9
  59. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  60. package/dist/types/src/translations.d.ts +2 -2
  61. package/dist/types/src/types/index.d.ts +1 -1
  62. package/dist/types/src/types/index.d.ts.map +1 -1
  63. package/dist/types/src/types/schema.d.ts +24 -4
  64. package/dist/types/src/types/schema.d.ts.map +1 -1
  65. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  66. package/dist/types/src/util/index.d.ts +0 -1
  67. package/dist/types/src/util/index.d.ts.map +1 -1
  68. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  69. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  70. package/dist/types/tsconfig.tsbuildinfo +1 -1
  71. package/package.json +48 -47
  72. package/src/components/Table/ColumnActionsMenu.tsx +1 -1
  73. package/src/components/Table/ColumnSettings.tsx +1 -1
  74. package/src/components/Table/CreateRefPanel.tsx +25 -8
  75. package/src/components/Table/DynamicTable.stories.tsx +7 -4
  76. package/src/components/Table/DynamicTable.tsx +4 -3
  77. package/src/components/Table/{relations.stories.tsx → Relations.stories.tsx} +40 -23
  78. package/src/components/Table/RowActionsMenu.tsx +3 -3
  79. package/src/components/Table/Table.stories.tsx +34 -23
  80. package/src/components/Table/Table.tsx +131 -110
  81. package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
  82. package/src/components/TableCellEditor/FormCellEditor.tsx +143 -42
  83. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +11 -23
  84. package/src/components/TableCellEditor/TableCellEditor.tsx +64 -119
  85. package/src/components/TableToolbar/TableToolbar.tsx +2 -2
  86. package/src/components/testing.ts +1 -1
  87. package/src/hooks/useAddRow.ts +6 -4
  88. package/src/hooks/useTableModel.ts +1 -1
  89. package/src/model/modal-controller.ts +1 -1
  90. package/src/model/selection-model.ts +3 -2
  91. package/src/model/table-model.test.ts +3 -4
  92. package/src/model/table-model.ts +28 -23
  93. package/src/model/table-presentation.test.ts +4 -4
  94. package/src/model/table-presentation.ts +72 -30
  95. package/src/model/table-sorting.ts +3 -3
  96. package/src/playwright/smoke.spec.ts +129 -37
  97. package/src/testing/useTestTableModel.ts +4 -3
  98. package/src/translations.ts +2 -2
  99. package/src/types/index.ts +1 -1
  100. package/src/types/schema.ts +62 -5
  101. package/src/util/dynamic-table.ts +5 -5
  102. package/src/util/index.ts +0 -1
  103. package/src/util/schema.ts +1 -1
  104. package/src/util/table-buttons.ts +4 -3
  105. package/src/util/table-controls.ts +4 -1
  106. package/dist/lib/browser/chunk-5XLSHLOG.mjs.map +0 -7
  107. package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
  108. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
  109. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +0 -20
  110. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +0 -7
  111. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs.map +0 -7
  112. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  113. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  114. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  115. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  116. package/dist/types/src/util/table.d.ts +0 -11
  117. package/dist/types/src/util/table.d.ts.map +0 -1
  118. package/src/components/TableCellEditor/extension.ts +0 -61
  119. package/src/util/table.ts +0 -49
@@ -3,64 +3,60 @@
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
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
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';
14
+ import { type DxGridAxis, type DxGridPosition } from '@dxos/lit-grid';
12
15
  import { useThemeContext } from '@dxos/react-ui';
13
16
  import { createMarkdownExtensions } from '@dxos/react-ui-editor';
14
17
  import {
15
- cellQuery,
16
- editorKeys,
17
- parseCellIndex,
18
- useGridContext,
19
- type DxGridPlanePosition,
18
+ type EditorBlurHandler,
20
19
  type EditorKeyEvent,
21
20
  type EditorKeyOrBlurHandler,
22
- type EditorBlurHandler,
23
21
  GridCellEditor,
24
22
  type GridCellEditorProps,
25
23
  type GridScopedProps,
24
+ editorKeys,
25
+ parseCellIndex,
26
+ useGridContext,
26
27
  } from '@dxos/react-ui-grid';
27
- import { tagPicker, createLinks } from '@dxos/react-ui-tag-picker';
28
+ import { createLinks, tagPicker } from '@dxos/react-ui-tag-picker';
28
29
  import { type FieldProjection } from '@dxos/schema';
29
30
 
31
+ import { type ModalController, type TableModel } from '../../model';
32
+
30
33
  import { CellValidationMessage } from './CellValidationMessage';
31
34
  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
35
 
37
36
  /**
38
37
  * Option to create new object/value.
39
38
  */
40
- export const createOption = (text: string) => ({ [newValue]: true, text });
41
-
42
- const isCreateOption = (data: any) => typeof data === 'object' && data[newValue];
43
-
44
39
  export type QueryResult = Pick<Completion, 'label'> & { data: any };
45
40
 
46
41
  export type TableCellEditorProps = {
47
42
  model?: TableModel;
48
43
  modals?: ModalController;
49
44
  schema?: Schema.AnyNoContext;
50
- onEnter?: (cell: DxGridPlanePosition) => void;
51
- onFocus?: DxGrid['refocus'];
45
+ onFocus?: (axis?: DxGridAxis, delta?: -1 | 0 | 1, cell?: DxGridPosition) => void;
52
46
  onSave?: () => void;
53
- onQuery?: (field: FieldProjection, text: string) => Promise<QueryResult[]>;
47
+ client?: Client;
54
48
  };
55
49
 
50
+ const adaptValidationMessage = (message: string | null) =>
51
+ message ? (message.endsWith('is missing') ? 'Can’t be blank' : message) : null;
52
+
56
53
  export const TableValueEditor = ({
57
54
  model,
58
55
  modals,
59
56
  schema,
60
- onEnter,
61
57
  onFocus,
62
58
  onSave,
63
- onQuery,
59
+ client,
64
60
  __gridScope,
65
61
  }: GridScopedProps<TableCellEditorProps>) => {
66
62
  const { editing } = useGridContext('TableValueEditor', __gridScope);
@@ -79,7 +75,8 @@ export const TableValueEditor = ({
79
75
 
80
76
  if (
81
77
  fieldProjection?.props.type === TypeEnum.Array ||
82
- fieldProjection?.props.format === FormatEnum.SingleSelect
78
+ fieldProjection?.props.format === FormatEnum.SingleSelect ||
79
+ fieldProjection?.props.format === FormatEnum.Ref
83
80
  // TODO(thure): Support `FormatEnum.MultiSelect`
84
81
  ) {
85
82
  return (
@@ -89,22 +86,14 @@ export const TableValueEditor = ({
89
86
  schema={schema}
90
87
  __gridScope={__gridScope}
91
88
  onSave={onSave}
89
+ client={client}
90
+ modals={modals}
92
91
  />
93
92
  );
94
93
  }
95
94
 
96
95
  // For all other types, use the existing cell editor
97
- return (
98
- <TableCellEditor
99
- model={model}
100
- modals={modals}
101
- onEnter={onEnter}
102
- onFocus={onFocus}
103
- onQuery={onQuery}
104
- onSave={onSave}
105
- __gridScope={__gridScope}
106
- />
107
- );
96
+ return <TableCellEditor model={model} modals={modals} onFocus={onFocus} onSave={onSave} __gridScope={__gridScope} />;
108
97
  };
109
98
 
110
99
  const editorSlots = { scroller: { className: '!plb-[--dx-grid-cell-editor-padding-block]' } };
@@ -112,17 +101,15 @@ const editorSlots = { scroller: { className: '!plb-[--dx-grid-cell-editor-paddin
112
101
  export const TableCellEditor = ({
113
102
  model,
114
103
  modals,
115
- onEnter,
116
104
  onFocus,
117
- onQuery,
118
105
  onSave,
119
106
  __gridScope,
120
107
  }: GridScopedProps<TableCellEditorProps>) => {
121
108
  const { id: gridId, editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
122
109
  const suppressNextBlur = useRef(false);
123
110
  const { themeMode } = useThemeContext();
124
- const [_validationError, setValidationError] = useState<string | null>(null);
125
- const [_validationVariant, setValidationVariant] = useState<'error' | 'warning'>('error');
111
+ const [validationError, setValidationError] = useState<string | null>(null);
112
+ const [validationVariant, setValidationVariant] = useState<'error' | 'warning'>('error');
126
113
 
127
114
  const fieldProjection = useMemo<FieldProjection | undefined>(() => {
128
115
  if (!model || !editing) {
@@ -136,38 +123,6 @@ export const TableCellEditor = ({
136
123
  return fieldProjection;
137
124
  }, [model, editing]);
138
125
 
139
- useEffect(() => {
140
- // Check for existing validation errors when editing starts (for draft rows).
141
- if (!model || !editing || !fieldProjection) {
142
- setValidationError(null);
143
- return;
144
- }
145
-
146
- const cell = parseCellIndex(editing.index);
147
- const { row, col } = cell;
148
-
149
- if (model.isDraftCell(cell)) {
150
- const field = model.projection.fields[col];
151
- const hasValidationError = model.hasDraftRowValidationError(row, field.path);
152
-
153
- if (hasValidationError) {
154
- const draftRows = model.draftRows.value;
155
- if (row >= 0 && row < draftRows.length) {
156
- const draftRow = draftRows[row];
157
- const validationError = draftRow.validationErrors?.find((error) => error.path === field.path);
158
- if (validationError) {
159
- setValidationError(validationError.message);
160
- setValidationVariant('warning');
161
- }
162
- }
163
- } else {
164
- setValidationError(null);
165
- }
166
- } else {
167
- setValidationError(null);
168
- }
169
- }, [model, editing, fieldProjection]);
170
-
171
126
  const handleEnter = useCallback(
172
127
  async (value: any) => {
173
128
  if (!model || !editing) {
@@ -180,28 +135,34 @@ export const TableCellEditor = ({
180
135
  if (validationResult.valid) {
181
136
  setValidationError(null);
182
137
  model.setCellData(cell, value);
183
- onEnter?.(cell);
184
138
  onFocus?.();
185
139
  setEditing(null);
186
140
  onSave?.();
187
141
  } else {
188
- setValidationError(validationResult.error);
142
+ setValidationError(adaptValidationMessage(validationResult.error));
189
143
  setValidationVariant('error');
190
144
  }
191
145
  },
192
- [model, editing, onEnter, onFocus, setEditing],
146
+ [model, editing, onFocus, setEditing],
193
147
  );
194
148
 
195
149
  const handleBlur = useCallback<EditorBlurHandler>(
196
150
  async (value) => {
197
- if (!model || !editing) {
198
- return;
199
- }
200
151
  if (suppressNextBlur.current) {
201
152
  suppressNextBlur.current = false;
153
+ } else if (model && editing) {
154
+ // Save silently if validation passes
155
+ const cell = parseCellIndex(editing.index);
156
+ if (value !== undefined) {
157
+ const result = await model.validateCellData(cell, value);
158
+ if (result.valid) {
159
+ setValidationError(null);
160
+ model.setCellData(cell, value);
161
+ setEditing(null);
162
+ onSave?.();
163
+ }
164
+ }
202
165
  }
203
-
204
- // Don't save on blur - let handleClose handle validation and saving
205
166
  },
206
167
  [model, editing],
207
168
  );
@@ -219,19 +180,20 @@ export const TableCellEditor = ({
219
180
  const result = await model.validateCellData(cell, value);
220
181
 
221
182
  if (result.valid) {
183
+ suppressNextBlur.current = true;
222
184
  setValidationError(null);
223
185
  model.setCellData(cell, value);
224
186
  setEditing(null);
225
187
  onSave?.();
226
- onEnter?.(cell);
227
188
  if (event && onFocus) {
228
- onFocus(determineNavigationAxis(event), determineNavigationDelta(event));
189
+ onFocus(determineNavigationAxis(event), determineNavigationDelta(event), cell);
229
190
  }
230
191
  } else {
231
192
  setValidationError(result.error);
232
193
  setValidationVariant('error');
233
194
  }
234
195
  } else {
196
+ suppressNextBlur.current = true;
235
197
  setValidationError(null);
236
198
  setEditing(null);
237
199
  onSave?.();
@@ -240,7 +202,7 @@ export const TableCellEditor = ({
240
202
  }
241
203
  }
242
204
  },
243
- [model, editing, onFocus, onEnter, fieldProjection, setEditing, onSave],
205
+ [model, editing, onFocus, fieldProjection, setEditing, onSave],
244
206
  );
245
207
 
246
208
  const extension = useMemo(() => {
@@ -299,39 +261,26 @@ export const TableCellEditor = ({
299
261
  );
300
262
  }
301
263
 
302
- if (onQuery) {
303
- switch (fieldProjection.props.format) {
304
- case FormatEnum.Ref: {
305
- extension.push([
306
- completion({
307
- onQuery: (text) => onQuery(fieldProjection, text),
308
- onMatch: (data) => {
309
- if (model && editing && modals) {
310
- if (isCreateOption(data)) {
311
- const { field, props } = fieldProjection;
312
- if (props.referenceSchema) {
313
- suppressNextBlur.current = true;
314
- modals.openCreateRef(
315
- props.referenceSchema,
316
- document.querySelector(cellQuery(editing.index, gridId)),
317
- {
318
- [field.referencePath!]: data.text,
319
- },
320
- (data) => {
321
- void handleEnter(data);
322
- },
323
- );
324
- }
325
- } else {
326
- void handleEnter(data);
327
- }
328
- }
329
- },
330
- }),
331
- ]);
332
- break;
333
- }
334
- }
264
+ // Add validation extension to handle content changes
265
+ if (model && editing) {
266
+ extension.push(
267
+ EditorView.updateListener.of(
268
+ debounce((update) => {
269
+ const content = update.state.doc.toString();
270
+ const cell = parseCellIndex(editing.index);
271
+
272
+ // Perform validation on content change
273
+ void model.validateCellData(cell, content).then((result) => {
274
+ if (result.valid) {
275
+ setValidationError(null);
276
+ } else {
277
+ setValidationError(result.error);
278
+ setValidationVariant('error');
279
+ }
280
+ });
281
+ }, 10),
282
+ ),
283
+ );
335
284
  }
336
285
 
337
286
  return extension;
@@ -395,11 +344,7 @@ export const TableCellEditor = ({
395
344
 
396
345
  return (
397
346
  <>
398
- <CellValidationMessage
399
- validationError={_validationError}
400
- variant={_validationVariant}
401
- __gridScope={__gridScope}
402
- />
347
+ <CellValidationMessage validationError={validationError} variant={validationVariant} __gridScope={__gridScope} />
403
348
  <GridCellEditor extension={extension} getCellContent={getCellContent} onBlur={handleBlur} slots={editorSlots} />
404
349
  </>
405
350
  );
@@ -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,7 +5,7 @@
5
5
  import { Schema } from 'effect';
6
6
  import { useEffect } from 'react';
7
7
 
8
- import { FormatEnum, ObjectId, setValue, TypedObject, TypeEnum } from '@dxos/echo-schema';
8
+ import { FormatEnum, ObjectId, TypeEnum, TypedObject, setValue } from '@dxos/echo-schema';
9
9
  import { live } from '@dxos/live-object';
10
10
  import { faker } from '@dxos/random';
11
11
  import { type ProjectionModel } from '@dxos/schema';
@@ -8,6 +8,8 @@ import { useCallback } from 'react';
8
8
  import { type Space } from '@dxos/client/echo';
9
9
  import { Obj } from '@dxos/echo';
10
10
 
11
+ import { type InsertRowResult } from '../model';
12
+
11
13
  export type UseAddRowParams = {
12
14
  space?: Space;
13
15
  schema?: Schema.Schema.AnyNoContext;
@@ -20,16 +22,16 @@ export type UseAddRowParams = {
20
22
  */
21
23
  export const useAddRow = ({ space, schema }: UseAddRowParams) => {
22
24
  return useCallback(
23
- (data?: any) => {
25
+ (data?: any): InsertRowResult => {
24
26
  if (space && schema) {
25
27
  try {
26
28
  space.db.add(Obj.make(schema, data ?? {}));
27
- return true;
29
+ return 'final';
28
30
  } catch (error) {
29
- return false;
31
+ return 'draft';
30
32
  }
31
33
  }
32
- return false;
34
+ return 'final';
33
35
  },
34
36
  [space, schema],
35
37
  );
@@ -13,7 +13,7 @@ import { useSelected, useSelectionActions } from '@dxos/react-ui-attention';
13
13
  import { type DataType, type ProjectionModel } from '@dxos/schema';
14
14
  import { isNonNullable } from '@dxos/util';
15
15
 
16
- import { type TableRow, TableModel, type TableModelProps, type TableRowAction } from '../model';
16
+ import { TableModel, type TableModelProps, type TableRow, type TableRowAction } from '../model';
17
17
 
18
18
  export type UseTableModelParams<T extends TableRow = TableRow> = {
19
19
  view?: DataType.View;
@@ -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,13 +2,14 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { computed, effect, type Signal, 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 {
@@ -6,7 +6,6 @@ import { computed } from '@preact/signals-core';
6
6
  import { Schema } from 'effect';
7
7
  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
8
8
 
9
- import { Obj } from '@dxos/echo';
10
9
  import { TypedObject } from '@dxos/echo-schema';
11
10
  import { updateCounter } from '@dxos/echo-schema/testing';
12
11
  import { registerSignalsRuntime } from '@dxos/echo-signals';
@@ -14,8 +13,9 @@ import { live } from '@dxos/live-object';
14
13
  import { createEchoSchema } from '@dxos/live-object/testing';
15
14
  import { createView } from '@dxos/schema';
16
15
 
16
+ import { Table } from '../types';
17
+
17
18
  import { TableModel, type TableModelProps } from './table-model';
18
- import { TableView } from '../types';
19
19
 
20
20
  // TODO(burdon): Tests are disabled in project.json since they bring in plugin deps.
21
21
  // Restore once factored out into react-ui-table.
@@ -72,7 +72,6 @@ describe('TableModel', () => {
72
72
  describe('reactivity', () => {
73
73
  it('pure signals should nest', () => {
74
74
  const signal$ = live({ arr: [{ thingInside: 1 }, { thingInside: 2 }] });
75
-
76
75
  const computed$ = computed(() => {
77
76
  return signal$.arr.map((row) =>
78
77
  computed(() => {
@@ -107,7 +106,7 @@ class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.
107
106
 
108
107
  const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
109
108
  const schema = createEchoSchema(Test);
110
- const table = Obj.make(TableView, { sizes: {} });
109
+ const table = Table.make();
111
110
  const view = createView({ typename: schema.typename, jsonSchema: schema.jsonSchema, presentation: table });
112
111
  return new TableModel({ view, schema: schema.jsonSchema, ...props });
113
112
  };
@@ -2,40 +2,41 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { computed, effect, signal, type ReadonlySignal } from '@preact/signals-core';
5
+ import { type ReadonlySignal, computed, effect, signal } from '@preact/signals-core';
6
6
 
7
7
  import { Resource } from '@dxos/context';
8
8
  import { Obj, Ref } from '@dxos/echo';
9
9
  import {
10
10
  type FieldSortType,
11
11
  FormatEnum,
12
- getValue,
13
- setValue,
14
12
  type JsonProp,
15
- getSnapshot,
13
+ type JsonSchemaType,
16
14
  getSchema,
15
+ getValue,
16
+ setValue,
17
17
  toEffectSchema,
18
- type JsonSchemaType,
19
18
  } from '@dxos/echo-schema';
20
19
  import { invariant } from '@dxos/invariant';
21
20
  import { ObjectId } from '@dxos/keys';
22
- import { isLiveObject } from '@dxos/live-object';
21
+ import { getSnapshot, isLiveObject } from '@dxos/live-object';
23
22
  import { fullyQualifiedId } from '@dxos/react-client/echo';
23
+ import { type Label } from '@dxos/react-ui';
24
24
  import { formatForEditing, parseValue } from '@dxos/react-ui-form';
25
25
  import {
26
26
  type DxGridAxisMeta,
27
- type DxGridPlaneRange,
28
27
  type DxGridPlanePosition,
28
+ type DxGridPlaneRange,
29
29
  type DxGridPosition,
30
30
  } from '@dxos/react-ui-grid';
31
- import { type DataType, ProjectionModel, type PropertyType, validateSchema, type ValidationError } from '@dxos/schema';
31
+ import { type DataType, ProjectionModel, type PropertyType, type ValidationError, validateSchema } from '@dxos/schema';
32
32
 
33
- import { type SelectionMode, SelectionModel } from './selection-model';
34
- import { TableSorting } from './table-sorting';
35
- import { TableView } from '../types';
33
+ import { Table } from '../types';
36
34
  import { touch } from '../util';
37
35
  import { extractTagIds } from '../util/tag';
38
36
 
37
+ import { type SelectionMode, SelectionModel } from './selection-model';
38
+ import { TableSorting } from './table-sorting';
39
+
39
40
  // Domain types for cell classification
40
41
  export type TableCellType = 'standard' | 'draft' | 'header';
41
42
 
@@ -53,7 +54,7 @@ export type TableRow = Record<JsonProp, any> & { id: string };
53
54
 
54
55
  export type TableRowAction = {
55
56
  id: string;
56
- translationKey: string;
57
+ label: Label;
57
58
  };
58
59
 
59
60
  export type TableFeatures = {
@@ -68,6 +69,8 @@ const defaultFeatures: TableFeatures = {
68
69
  schemaEditable: false,
69
70
  };
70
71
 
72
+ export type InsertRowResult = 'draft' | 'final';
73
+
71
74
  export type TableModelProps<T extends TableRow = TableRow> = {
72
75
  view: DataType.View;
73
76
  schema: JsonSchemaType;
@@ -78,7 +81,7 @@ export type TableModelProps<T extends TableRow = TableRow> = {
78
81
  pinnedRows?: { top: number[]; bottom: number[] };
79
82
  rowActions?: TableRowAction[];
80
83
  onResolveSchema?: (typename: string) => Promise<JsonSchemaType>;
81
- onInsertRow?: (data?: any) => boolean;
84
+ onInsertRow?: (data?: any) => InsertRowResult;
82
85
  onDeleteRows?: (index: number, obj: T[]) => void;
83
86
  onDeleteColumn?: (fieldId: string) => void;
84
87
  onCellUpdate?: (cell: DxGridPosition) => void;
@@ -89,14 +92,14 @@ export type TableModelProps<T extends TableRow = TableRow> = {
89
92
  export class TableModel<T extends TableRow = TableRow> extends Resource {
90
93
  private readonly _view: DataType.View;
91
94
  private readonly _projection: ProjectionModel;
92
- private _table?: TableView;
95
+ private _table?: Table.Table;
93
96
 
94
97
  private readonly _visibleRange = signal<DxGridPlaneRange>({
95
98
  start: { row: 0, col: 0 },
96
99
  end: { row: 0, col: 0 },
97
100
  });
98
101
 
99
- private readonly _onInsertRow?: (data?: any) => boolean;
102
+ private readonly _onInsertRow?: (data?: any) => InsertRowResult;
100
103
  private readonly _onDeleteRows?: TableModelProps<T>['onDeleteRows'];
101
104
  private readonly _onDeleteColumn?: TableModelProps<T>['onDeleteColumn'];
102
105
  private readonly _onCellUpdate?: TableModelProps<T>['onCellUpdate'];
@@ -173,7 +176,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
173
176
  return this._view;
174
177
  }
175
178
 
176
- public get table(): TableView {
179
+ public get table(): Table.Table {
177
180
  invariant(this._table, 'Model not initialized');
178
181
  return this._table;
179
182
  }
@@ -236,7 +239,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
236
239
 
237
240
  protected override async _open(): Promise<void> {
238
241
  const presentation = this._view.presentation.target ?? (await this._view.presentation.load());
239
- invariant(Obj.instanceOf(TableView, presentation));
242
+ invariant(Obj.instanceOf(Table.Table, presentation));
240
243
  this._table = presentation;
241
244
 
242
245
  this.initializeColumnMeta();
@@ -254,7 +257,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
254
257
  return {
255
258
  grid: meta,
256
259
  frozenColsStart: { 0: { size: 30, resizeable: false } },
257
- frozenColsEnd: { 0: { size: 40, resizeable: false } },
260
+ frozenColsEnd: { 0: { size: 32, resizeable: false } },
258
261
  };
259
262
  });
260
263
  }
@@ -334,11 +337,12 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
334
337
 
335
338
  public getColumnCount = (): number => this._projection?.fields.length ?? 0;
336
339
 
337
- public insertRow = (): void => {
340
+ public insertRow = (): InsertRowResult => {
338
341
  const result = this._onInsertRow?.();
339
- if (result === false && this._draftRows.value.length === 0) {
342
+ if (result === 'draft' && this._draftRows.value.length === 0) {
340
343
  this.createDraftRow();
341
344
  }
345
+ return result ?? 'final';
342
346
  };
343
347
 
344
348
  private createDraftRow(): void {
@@ -393,14 +397,14 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
393
397
  return false;
394
398
  }
395
399
 
396
- const success = this._onInsertRow?.(draftRow.data);
400
+ const insertRowResult = this._onInsertRow?.(draftRow.data);
397
401
 
398
- if (success) {
402
+ if (insertRowResult === 'final') {
399
403
  const newDraftRows = this._draftRows.value.filter((_, index) => index !== draftRowIndex);
400
404
  this._draftRows.value = newDraftRows;
401
405
  }
402
406
 
403
- return success ?? false;
407
+ return insertRowResult === 'final';
404
408
  };
405
409
 
406
410
  public deleteRow = (rowIndex: number): void => {
@@ -497,6 +501,7 @@ export class TableModel<T extends TableRow = TableRow> extends Resource {
497
501
  setValue(snapshot, field.path, transformedValue);
498
502
 
499
503
  const validationErrors = this.validateDraftRowData(snapshot);
504
+ // TODO(thure): These errors sometimes result in a useless message like “is missing” (what is missing?)
500
505
  if (validationErrors.length > 0) {
501
506
  const error = validationErrors.find((err) => err.path === field.path);
502
507
  if (error) {
@@ -3,17 +3,17 @@
3
3
  //
4
4
 
5
5
  import { Schema } from 'effect';
6
- import { describe, expect, it, beforeEach } from 'vitest';
6
+ import { beforeEach, describe, expect, it } from 'vitest';
7
7
 
8
- import { Obj } from '@dxos/echo';
9
8
  import { TypedObject } from '@dxos/echo-schema';
10
9
  import { live } from '@dxos/live-object';
11
10
  import { createEchoSchema } from '@dxos/live-object/testing';
12
11
  import { createView } from '@dxos/schema';
13
12
 
13
+ import { Table } from '../types';
14
+
14
15
  import { TableModel, type TableModelProps } from './table-model';
15
16
  import { TablePresentation } from './table-presentation';
16
- import { TableView } from '../types';
17
17
 
18
18
  describe('TablePresentation', () => {
19
19
  describe('row reactivity', () => {
@@ -97,7 +97,7 @@ class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.
97
97
 
98
98
  const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
99
99
  const schema = createEchoSchema(Test);
100
- const table = Obj.make(TableView, { sizes: {} });
100
+ const table = Table.make();
101
101
  const view = createView({ typename: schema.typename, jsonSchema: schema.jsonSchema, presentation: table });
102
102
  return new TableModel({ view, schema: schema.jsonSchema, ...props });
103
103
  };