@dxos/react-ui-table 0.8.4-main.c1de068 → 0.8.4-main.e098934

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 (120) 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-BBXDTWCX.mjs} +537 -466
  4. package/dist/lib/browser/chunk-BBXDTWCX.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +730 -669
  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 +9 -6
  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-RNQDU7IQ.mjs} +537 -466
  14. package/dist/lib/node-esm/chunk-RNQDU7IQ.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +730 -669
  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 +9 -6
  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 +9 -7
  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 -3
  57. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  58. package/dist/types/src/testing/useTestTableModel.d.ts +2 -21
  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 +2 -2
  66. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  67. package/dist/types/src/util/index.d.ts +0 -1
  68. package/dist/types/src/util/index.d.ts.map +1 -1
  69. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  70. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  71. package/dist/types/tsconfig.tsbuildinfo +1 -1
  72. package/package.json +47 -42
  73. package/src/components/Table/ColumnActionsMenu.tsx +1 -1
  74. package/src/components/Table/ColumnSettings.tsx +1 -1
  75. package/src/components/Table/CreateRefPanel.tsx +25 -8
  76. package/src/components/Table/DynamicTable.stories.tsx +7 -4
  77. package/src/components/Table/DynamicTable.tsx +4 -3
  78. package/src/components/Table/{relations.stories.tsx → Relations.stories.tsx} +40 -23
  79. package/src/components/Table/RowActionsMenu.tsx +3 -3
  80. package/src/components/Table/Table.stories.tsx +39 -28
  81. package/src/components/Table/Table.tsx +138 -112
  82. package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
  83. package/src/components/TableCellEditor/FormCellEditor.tsx +143 -42
  84. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +12 -24
  85. package/src/components/TableCellEditor/TableCellEditor.tsx +74 -137
  86. package/src/components/TableToolbar/TableToolbar.tsx +2 -2
  87. package/src/components/testing.ts +1 -1
  88. package/src/hooks/useAddRow.ts +6 -4
  89. package/src/hooks/useTableModel.ts +1 -1
  90. package/src/model/modal-controller.ts +1 -1
  91. package/src/model/selection-model.ts +3 -2
  92. package/src/model/table-model.test.ts +9 -5
  93. package/src/model/table-model.ts +35 -24
  94. package/src/model/table-presentation.test.ts +10 -5
  95. package/src/model/table-presentation.ts +72 -30
  96. package/src/model/table-sorting.ts +16 -7
  97. package/src/playwright/smoke.spec.ts +129 -37
  98. package/src/testing/useTestTableModel.ts +7 -5
  99. package/src/translations.ts +2 -2
  100. package/src/types/index.ts +1 -1
  101. package/src/types/schema.ts +62 -5
  102. package/src/util/dynamic-table.ts +10 -9
  103. package/src/util/index.ts +0 -1
  104. package/src/util/schema.ts +1 -1
  105. package/src/util/table-buttons.ts +4 -3
  106. package/src/util/table-controls.ts +4 -1
  107. package/dist/lib/browser/chunk-5XLSHLOG.mjs.map +0 -7
  108. package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
  109. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
  110. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +0 -20
  111. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +0 -7
  112. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs.map +0 -7
  113. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  114. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  115. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  116. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  117. package/dist/types/src/util/table.d.ts +0 -11
  118. package/dist/types/src/util/table.d.ts.map +0 -1
  119. package/src/components/TableCellEditor/extension.ts +0 -61
  120. package/src/util/table.ts +0 -49
@@ -13,16 +13,16 @@ import { invariant } from '@dxos/invariant';
13
13
  import { faker } from '@dxos/random';
14
14
  import { Filter, useQuery } from '@dxos/react-client/echo';
15
15
  import { useClientProvider, withClientProvider } from '@dxos/react-client/testing';
16
- import { defaultRowSize, Grid, type GridEditing } from '@dxos/react-ui-grid';
17
- import { createDefaultSchema, DataType } from '@dxos/schema';
16
+ import { Grid, type GridEditing, defaultRowSize } from '@dxos/react-ui-grid';
17
+ import { DataType, createDefaultSchema, typenameFromQuery } from '@dxos/schema';
18
18
  import { withLayout, withTheme } from '@dxos/storybook-utils';
19
19
 
20
- import { TableCellEditor, type TableCellEditorProps } from './TableCellEditor';
21
20
  import { useTableModel } from '../../hooks';
22
21
  import { type TableFeatures } from '../../model';
23
22
  import { translations } from '../../translations';
24
- import { TableView } from '../../types';
25
- import { createTable } from '../../util';
23
+ import { Table } from '../../types';
24
+
25
+ import { TableCellEditor } from './TableCellEditor';
26
26
 
27
27
  type StoryProps = {
28
28
  editing: GridEditing;
@@ -39,7 +39,7 @@ const DefaultStory = ({ editing }: StoryProps) => {
39
39
  if (space && views.length && !view) {
40
40
  const view = views[0];
41
41
  setView(view);
42
- setSchema(space.db.schemaRegistry.getSchema(view.query.typename!));
42
+ setSchema(space.db.schemaRegistry.getSchema(typenameFromQuery(view.query)!));
43
43
  }
44
44
  }, [space, views, view]);
45
45
 
@@ -54,18 +54,6 @@ const DefaultStory = ({ editing }: StoryProps) => {
54
54
 
55
55
  const model = useTableModel({ view, schema: schema?.jsonSchema, features });
56
56
 
57
- const handleQuery: TableCellEditorProps['onQuery'] = async ({ field }) => {
58
- // TODO(dmaretskyi): If no schema query nothing
59
- const { objects } = await space.db.query(schema ? Filter.type(schema) : Filter.everything()).run();
60
- return objects.map((obj) => {
61
- const label = obj[field.referencePath ?? 'id'];
62
- return {
63
- label,
64
- data: obj,
65
- };
66
- });
67
- };
68
-
69
57
  if (!model || !schema || !view) {
70
58
  return <div />;
71
59
  }
@@ -73,25 +61,25 @@ const DefaultStory = ({ editing }: StoryProps) => {
73
61
  return (
74
62
  <div className='flex w-[300px] border border-separator' style={{ height: defaultRowSize }}>
75
63
  <Grid.Root id='test' editing={editing}>
76
- <TableCellEditor model={model} onQuery={handleQuery} />
64
+ <TableCellEditor model={model} schema={schema} />
77
65
  </Grid.Root>
78
66
  </div>
79
67
  );
80
68
  };
81
69
 
82
- const meta: Meta<StoryProps> = {
70
+ const meta = {
83
71
  title: 'ui/react-ui-table/TableCellEditor',
84
72
  component: DefaultStory,
85
73
  render: DefaultStory,
86
74
  parameters: { translations, layout: 'centered' },
87
75
  decorators: [
88
76
  withClientProvider({
89
- types: [DataType.View, TableView],
77
+ types: [DataType.View, Table.Table],
90
78
  createIdentity: true,
91
79
  createSpace: true,
92
80
  onSpaceCreated: async ({ client, space }) => {
93
81
  const schema = createDefaultSchema();
94
- const { view } = await createTable({ client, space, typename: schema.typename });
82
+ const { view } = await Table.makeView({ client, space, typename: schema.typename });
95
83
  space.db.add(view);
96
84
  Array.from({ length: 10 }).forEach(() => {
97
85
  space.db.add(
@@ -107,11 +95,11 @@ const meta: Meta<StoryProps> = {
107
95
  withTheme,
108
96
  withLayout(),
109
97
  ],
110
- };
98
+ } satisfies Meta<typeof DefaultStory>;
111
99
 
112
100
  export default meta;
113
101
 
114
- type Story = StoryObj<StoryProps>;
102
+ type Story = StoryObj<typeof meta>;
115
103
 
116
104
  export const Default: Story = {
117
105
  args: {
@@ -3,64 +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
- 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';
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 { tagPicker, 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'];
44
+ onFocus?: (axis?: DxGridAxis, delta?: -1 | 0 | 1, cell?: DxGridPosition) => void;
52
45
  onSave?: () => void;
53
- onQuery?: (field: FieldProjection, text: string) => Promise<QueryResult[]>;
46
+ client?: Client;
54
47
  };
55
48
 
49
+ const adaptValidationMessage = (message: string | null) =>
50
+ message ? (message.endsWith('is missing') ? 'Can’t be blank' : message) : null;
51
+
56
52
  export const TableValueEditor = ({
57
53
  model,
58
54
  modals,
59
55
  schema,
60
- onEnter,
61
56
  onFocus,
62
57
  onSave,
63
- onQuery,
58
+ client,
64
59
  __gridScope,
65
60
  }: GridScopedProps<TableCellEditorProps>) => {
66
61
  const { editing } = useGridContext('TableValueEditor', __gridScope);
@@ -79,7 +74,8 @@ export const TableValueEditor = ({
79
74
 
80
75
  if (
81
76
  fieldProjection?.props.type === TypeEnum.Array ||
82
- fieldProjection?.props.format === FormatEnum.SingleSelect
77
+ fieldProjection?.props.format === FormatEnum.SingleSelect ||
78
+ fieldProjection?.props.format === FormatEnum.Ref
83
79
  // TODO(thure): Support `FormatEnum.MultiSelect`
84
80
  ) {
85
81
  return (
@@ -89,40 +85,29 @@ export const TableValueEditor = ({
89
85
  schema={schema}
90
86
  __gridScope={__gridScope}
91
87
  onSave={onSave}
88
+ client={client}
89
+ modals={modals}
92
90
  />
93
91
  );
94
92
  }
95
93
 
96
94
  // 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
- );
95
+ return <TableCellEditor model={model} modals={modals} onFocus={onFocus} onSave={onSave} __gridScope={__gridScope} />;
108
96
  };
109
97
 
110
- const editorSlots = { scroller: { className: '!plb-[--dx-grid-cell-editor-padding-block]' } };
98
+ const editorSlots = { scroll: { className: '!plb-[--dx-grid-cell-editor-padding-block]' } };
111
99
 
112
100
  export const TableCellEditor = ({
113
101
  model,
114
102
  modals,
115
- onEnter,
116
103
  onFocus,
117
- onQuery,
118
104
  onSave,
119
105
  __gridScope,
120
106
  }: GridScopedProps<TableCellEditorProps>) => {
121
- const { id: gridId, editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
107
+ const { editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
122
108
  const suppressNextBlur = useRef(false);
123
- const { themeMode } = useThemeContext();
124
- const [_validationError, setValidationError] = useState<string | null>(null);
125
- const [_validationVariant, setValidationVariant] = useState<'error' | 'warning'>('error');
109
+ const [validationError, setValidationError] = useState<string | null>(null);
110
+ const [validationVariant, setValidationVariant] = useState<'error' | 'warning'>('error');
126
111
 
127
112
  const fieldProjection = useMemo<FieldProjection | undefined>(() => {
128
113
  if (!model || !editing) {
@@ -136,38 +121,6 @@ export const TableCellEditor = ({
136
121
  return fieldProjection;
137
122
  }, [model, editing]);
138
123
 
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
124
  const handleEnter = useCallback(
172
125
  async (value: any) => {
173
126
  if (!model || !editing) {
@@ -180,28 +133,34 @@ export const TableCellEditor = ({
180
133
  if (validationResult.valid) {
181
134
  setValidationError(null);
182
135
  model.setCellData(cell, value);
183
- onEnter?.(cell);
184
136
  onFocus?.();
185
137
  setEditing(null);
186
138
  onSave?.();
187
139
  } else {
188
- setValidationError(validationResult.error);
140
+ setValidationError(adaptValidationMessage(validationResult.error));
189
141
  setValidationVariant('error');
190
142
  }
191
143
  },
192
- [model, editing, onEnter, onFocus, setEditing],
144
+ [model, editing, onFocus, setEditing],
193
145
  );
194
146
 
195
147
  const handleBlur = useCallback<EditorBlurHandler>(
196
148
  async (value) => {
197
- if (!model || !editing) {
198
- return;
199
- }
200
149
  if (suppressNextBlur.current) {
201
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
+ }
202
163
  }
203
-
204
- // Don't save on blur - let handleClose handle validation and saving
205
164
  },
206
165
  [model, editing],
207
166
  );
@@ -213,25 +172,25 @@ export const TableCellEditor = ({
213
172
  }
214
173
 
215
174
  const cell = parseCellIndex(editing.index);
216
-
217
175
  if (value !== undefined) {
218
176
  // Pre-commit validation check.
219
177
  const result = await model.validateCellData(cell, value);
220
178
 
221
179
  if (result.valid) {
180
+ suppressNextBlur.current = true;
222
181
  setValidationError(null);
223
182
  model.setCellData(cell, value);
224
183
  setEditing(null);
225
184
  onSave?.();
226
- onEnter?.(cell);
227
185
  if (event && onFocus) {
228
- onFocus(determineNavigationAxis(event), determineNavigationDelta(event));
186
+ onFocus(determineNavigationAxis(event), determineNavigationDelta(event), cell);
229
187
  }
230
188
  } else {
231
189
  setValidationError(result.error);
232
190
  setValidationVariant('error');
233
191
  }
234
192
  } else {
193
+ suppressNextBlur.current = true;
235
194
  setValidationError(null);
236
195
  setEditing(null);
237
196
  onSave?.();
@@ -240,15 +199,15 @@ export const TableCellEditor = ({
240
199
  }
241
200
  }
242
201
  },
243
- [model, editing, onFocus, onEnter, fieldProjection, setEditing, onSave],
202
+ [model, editing, onFocus, fieldProjection, setEditing, onSave],
244
203
  );
245
204
 
246
- const extension = useMemo(() => {
205
+ const extensions = useMemo(() => {
247
206
  if (!fieldProjection) {
248
207
  return [];
249
208
  }
250
209
 
251
- const extension = [
210
+ const extensions = [
252
211
  editorKeys({
253
212
  onClose: handleClose,
254
213
  ...(editing?.initialContent && { onNav: handleClose }),
@@ -256,17 +215,14 @@ export const TableCellEditor = ({
256
215
  ];
257
216
 
258
217
  const format = fieldProjection.props.format;
259
-
260
218
  if (format === FormatEnum.SingleSelect || format === FormatEnum.MultiSelect) {
261
219
  // TODO(ZaymonFC): Reconcile this with the TagPicker component?
262
- // Add markdown extensions needed by tag picker.
263
- extension.push(createMarkdownExtensions({ themeMode }));
264
-
265
220
  const options = fieldProjection.props.options || [];
266
-
267
221
  const mode = format === FormatEnum.SingleSelect ? ('single-select' as const) : ('multi-select' as const);
268
222
 
269
- extension.push(
223
+ // Add markdown extensions needed by tag picker.
224
+ extensions.push(createMarkdownExtensions());
225
+ extensions.push(
270
226
  tagPicker({
271
227
  mode,
272
228
  keymap: false,
@@ -299,43 +255,30 @@ export const TableCellEditor = ({
299
255
  );
300
256
  }
301
257
 
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
- }
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
+ );
335
278
  }
336
279
 
337
- return extension;
338
- }, [model, modals, editing, fieldProjection, handleClose, themeMode]);
280
+ return extensions;
281
+ }, [model, modals, editing, fieldProjection, handleClose]);
339
282
 
340
283
  const getCellContent = useCallback<GridCellEditorProps['getCellContent']>(() => {
341
284
  if (model && editing) {
@@ -349,10 +292,8 @@ export const TableCellEditor = ({
349
292
  fieldProjection?.props.format === FormatEnum.MultiSelect
350
293
  ) {
351
294
  const value = model.getCellData(cell);
352
-
353
295
  if (value !== undefined) {
354
296
  const options = fieldProjection.props.options || [];
355
-
356
297
  if (fieldProjection.props.format === FormatEnum.MultiSelect) {
357
298
  const tagItems = value
358
299
  .split(',')
@@ -395,12 +336,8 @@ export const TableCellEditor = ({
395
336
 
396
337
  return (
397
338
  <>
398
- <CellValidationMessage
399
- validationError={_validationError}
400
- variant={_validationVariant}
401
- __gridScope={__gridScope}
402
- />
403
- <GridCellEditor extension={extension} getCellContent={getCellContent} onBlur={handleBlur} slots={editorSlots} />
339
+ <CellValidationMessage validationError={validationError} variant={validationVariant} __gridScope={__gridScope} />
340
+ <GridCellEditor extensions={extensions} getCellContent={getCellContent} onBlur={handleBlur} slots={editorSlots} />
404
341
  </>
405
342
  );
406
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,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,7 @@ 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';
9
+ import { Filter, Query } from '@dxos/echo';
10
10
  import { TypedObject } from '@dxos/echo-schema';
11
11
  import { updateCounter } from '@dxos/echo-schema/testing';
12
12
  import { registerSignalsRuntime } from '@dxos/echo-signals';
@@ -14,8 +14,9 @@ import { live } from '@dxos/live-object';
14
14
  import { createEchoSchema } from '@dxos/live-object/testing';
15
15
  import { createView } from '@dxos/schema';
16
16
 
17
+ import { Table } from '../types';
18
+
17
19
  import { TableModel, type TableModelProps } from './table-model';
18
- import { TableView } from '../types';
19
20
 
20
21
  // TODO(burdon): Tests are disabled in project.json since they bring in plugin deps.
21
22
  // Restore once factored out into react-ui-table.
@@ -72,7 +73,6 @@ describe('TableModel', () => {
72
73
  describe('reactivity', () => {
73
74
  it('pure signals should nest', () => {
74
75
  const signal$ = live({ arr: [{ thingInside: 1 }, { thingInside: 2 }] });
75
-
76
76
  const computed$ = computed(() => {
77
77
  return signal$.arr.map((row) =>
78
78
  computed(() => {
@@ -107,7 +107,11 @@ class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.
107
107
 
108
108
  const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
109
109
  const schema = createEchoSchema(Test);
110
- const table = Obj.make(TableView, { sizes: {} });
111
- const view = createView({ typename: schema.typename, jsonSchema: schema.jsonSchema, presentation: table });
110
+ const table = Table.make();
111
+ const view = createView({
112
+ query: Query.select(Filter.type(schema)),
113
+ jsonSchema: schema.jsonSchema,
114
+ presentation: table,
115
+ });
112
116
  return new TableModel({ view, schema: schema.jsonSchema, ...props });
113
117
  };