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

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 (158) hide show
  1. package/dist/lib/browser/chunk-DSR2MRZB.mjs +80 -0
  2. package/dist/lib/browser/chunk-DSR2MRZB.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-PTHMYAAM.mjs +1751 -0
  4. package/dist/lib/browser/chunk-PTHMYAAM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1190 -1139
  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 +31 -38
  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-GI72MQSG.mjs +1753 -0
  12. package/dist/lib/node-esm/chunk-GI72MQSG.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-HTZJSULI.mjs +81 -0
  14. package/dist/lib/node-esm/chunk-HTZJSULI.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1190 -1139
  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 +31 -38
  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 +2 -3
  22. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/ColumnSettings.d.ts +3 -3
  24. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/DynamicTable.d.ts +7 -7
  26. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +37 -3
  28. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/Relations.stories.d.ts +111 -0
  30. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
  31. package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
  32. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  33. package/dist/types/src/components/Table/Table.d.ts +17 -13
  34. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  35. package/dist/types/src/components/Table/Table.stories.d.ts +108 -4
  36. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +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 +13 -9
  40. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +11 -18
  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 -4
  44. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  45. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  46. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  47. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +5 -5
  48. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  49. package/dist/types/src/components/testing.d.ts +23 -16
  50. package/dist/types/src/components/testing.d.ts.map +1 -1
  51. package/dist/types/src/hooks/index.d.ts +2 -1
  52. package/dist/types/src/hooks/index.d.ts.map +1 -1
  53. package/dist/types/src/hooks/useAddRow.d.ts +6 -6
  54. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  55. package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
  56. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  57. package/dist/types/src/hooks/useTableModel.d.ts +10 -10
  58. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  59. package/dist/types/src/model/modal-controller.d.ts +5 -8
  60. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  61. package/dist/types/src/model/selection-model.d.ts +17 -8
  62. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  63. package/dist/types/src/model/table-model.d.ts +112 -30
  64. package/dist/types/src/model/table-model.d.ts.map +1 -1
  65. package/dist/types/src/model/table-presentation.d.ts +5 -2
  66. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  67. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  68. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  69. package/dist/types/src/testing/useTestTableModel.d.ts +18 -34
  70. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  71. package/dist/types/src/translations.d.ts +4 -4
  72. package/dist/types/src/translations.d.ts.map +1 -1
  73. package/dist/types/src/types/Table.d.ts +31 -0
  74. package/dist/types/src/types/Table.d.ts.map +1 -0
  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/util/dynamic-table.d.ts +14 -12
  78. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  79. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  80. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  81. package/dist/types/src/util/index.d.ts +2 -1
  82. package/dist/types/src/util/index.d.ts.map +1 -1
  83. package/dist/types/src/util/query-ast.d.ts +7 -0
  84. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  85. package/dist/types/src/util/schema.d.ts +1 -1
  86. package/dist/types/src/util/schema.d.ts.map +1 -1
  87. package/dist/types/src/util/sort.d.ts +12 -0
  88. package/dist/types/src/util/sort.d.ts.map +1 -0
  89. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  90. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  91. package/dist/types/tsconfig.tsbuildinfo +1 -1
  92. package/package.json +61 -56
  93. package/src/components/Table/ColumnActionsMenu.tsx +11 -7
  94. package/src/components/Table/ColumnSettings.tsx +9 -10
  95. package/src/components/Table/DynamicTable.stories.tsx +28 -24
  96. package/src/components/Table/DynamicTable.tsx +33 -27
  97. package/src/components/Table/Relations.stories.tsx +240 -0
  98. package/src/components/Table/RowActionsMenu.tsx +26 -17
  99. package/src/components/Table/Table.stories.tsx +146 -116
  100. package/src/components/Table/Table.tsx +353 -297
  101. package/src/components/TableCellEditor/CellValidationMessage.tsx +6 -6
  102. package/src/components/TableCellEditor/FormCellEditor.tsx +134 -40
  103. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +31 -66
  104. package/src/components/TableCellEditor/TableCellEditor.tsx +88 -238
  105. package/src/components/TableCellEditor/index.ts +1 -0
  106. package/src/components/TableToolbar/TableToolbar.tsx +54 -38
  107. package/src/components/testing.ts +39 -13
  108. package/src/hooks/index.ts +2 -1
  109. package/src/hooks/useAddRow.ts +14 -14
  110. package/src/hooks/useProjectionModel.ts +39 -0
  111. package/src/hooks/useTableModel.ts +36 -34
  112. package/src/model/modal-controller.ts +24 -41
  113. package/src/model/selection-model.ts +75 -43
  114. package/src/model/table-model.test.ts +92 -55
  115. package/src/model/table-model.ts +453 -175
  116. package/src/model/table-presentation.test.ts +70 -58
  117. package/src/model/table-presentation.ts +111 -64
  118. package/src/playwright/TableManager.ts +13 -6
  119. package/src/playwright/playwright.config.ts +1 -1
  120. package/src/playwright/smoke.spec.ts +171 -33
  121. package/src/testing/useTestTableModel.ts +46 -35
  122. package/src/translations.ts +3 -3
  123. package/src/types/Table.ts +95 -0
  124. package/src/types/index.ts +1 -1
  125. package/src/util/dynamic-table.test.ts +70 -0
  126. package/src/util/dynamic-table.ts +46 -33
  127. package/src/util/index.ts +2 -1
  128. package/src/util/query-ast.ts +22 -0
  129. package/src/util/schema.ts +4 -4
  130. package/src/util/sort.ts +90 -0
  131. package/src/util/table-buttons.ts +4 -3
  132. package/src/util/table-controls.ts +6 -3
  133. package/dist/lib/browser/chunk-5XLSHLOG.mjs +0 -1751
  134. package/dist/lib/browser/chunk-5XLSHLOG.mjs.map +0 -7
  135. package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
  136. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
  137. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +0 -20
  138. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +0 -7
  139. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs +0 -1752
  140. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs.map +0 -7
  141. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  142. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  143. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  144. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  145. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  146. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  147. package/dist/types/src/model/table-sorting.d.ts +0 -53
  148. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  149. package/dist/types/src/types/schema.d.ts +0 -8
  150. package/dist/types/src/types/schema.d.ts.map +0 -1
  151. package/dist/types/src/util/table.d.ts +0 -11
  152. package/dist/types/src/util/table.d.ts.map +0 -1
  153. package/src/components/Table/CreateRefPanel.tsx +0 -86
  154. package/src/components/Table/relations.stories.tsx +0 -116
  155. package/src/components/TableCellEditor/extension.ts +0 -61
  156. package/src/model/table-sorting.ts +0 -216
  157. package/src/types/schema.ts +0 -16
  158. package/src/util/table.ts +0 -49
@@ -3,66 +3,63 @@
3
3
  //
4
4
 
5
5
  import { type Completion } from '@codemirror/autocomplete';
6
- import { type Schema } from 'effect/Schema';
7
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
+ import { EditorView } from '@codemirror/view';
7
+ import React, { useCallback, useMemo, useRef, useState } from 'react';
8
8
 
9
- import { FormatEnum, TypeEnum } from '@dxos/echo-schema';
9
+ import { debounce } from '@dxos/async';
10
+ import { type Type } from '@dxos/echo';
11
+ import { Format, TypeEnum } from '@dxos/echo/internal';
10
12
  import { invariant } from '@dxos/invariant';
11
- import { type DxGrid } from '@dxos/lit-grid';
12
- import { useThemeContext } from '@dxos/react-ui';
13
- import { createMarkdownExtensions } from '@dxos/react-ui-editor';
13
+ import { type DxGridAxis, type DxGridPosition } from '@dxos/lit-grid';
14
14
  import {
15
- cellQuery,
16
- editorKeys,
17
- parseCellIndex,
18
- useGridContext,
19
- type DxGridPlanePosition,
15
+ type EditorBlurHandler,
20
16
  type EditorKeyEvent,
21
17
  type EditorKeyOrBlurHandler,
22
- type EditorBlurHandler,
23
18
  GridCellEditor,
24
19
  type GridCellEditorProps,
25
20
  type GridScopedProps,
21
+ editorKeys,
22
+ parseCellIndex,
23
+ useGridContext,
26
24
  } from '@dxos/react-ui-grid';
27
- import { tagPicker, createLinks } from '@dxos/react-ui-tag-picker';
28
25
  import { type FieldProjection } from '@dxos/schema';
29
26
 
27
+ import { type ModalController, type TableModel } from '../../model';
28
+
30
29
  import { CellValidationMessage } from './CellValidationMessage';
31
- import { FormCellEditor } from './FormCellEditor';
32
- import { completion } from './extension';
33
- import { type TableModel, type ModalController } from '../../model';
30
+ import { FormCellEditor, type OnCreateHandler } from './FormCellEditor';
34
31
 
35
- const newValue = Symbol.for('newValue');
32
+ const editorSlots = {
33
+ scroll: {
34
+ className: '!py-(--dx-grid-cell-editor-padding-block)',
35
+ },
36
+ };
36
37
 
37
38
  /**
38
39
  * Option to create new object/value.
39
40
  */
40
- export const createOption = (text: string) => ({ [newValue]: true, text });
41
-
42
- const isCreateOption = (data: any) => typeof data === 'object' && data[newValue];
43
-
44
- export type QueryResult = Pick<Completion, 'label'> & { data: any };
41
+ export type QueryResult = Pick<Completion, 'label'> & {
42
+ data: any;
43
+ };
45
44
 
46
- export type TableCellEditorProps = {
45
+ export type TableCellEditorProps<T extends Type.AnyEntity = Type.AnyEntity> = {
46
+ schema?: T;
47
47
  model?: TableModel;
48
48
  modals?: ModalController;
49
- schema?: Schema.AnyNoContext;
50
- onEnter?: (cell: DxGridPlanePosition) => void;
51
- onFocus?: DxGrid['refocus'];
49
+ onFocus?: (axis?: DxGridAxis, delta?: -1 | 0 | 1, cell?: DxGridPosition) => void;
50
+ onCreate?: OnCreateHandler;
52
51
  onSave?: () => void;
53
- onQuery?: (field: FieldProjection, text: string) => Promise<QueryResult[]>;
54
52
  };
55
53
 
56
- export const TableValueEditor = ({
54
+ export const TableValueEditor = <T extends Type.AnyEntity = Type.AnyEntity>({
55
+ __gridScope,
56
+ schema,
57
57
  model,
58
58
  modals,
59
- schema,
60
- onEnter,
61
59
  onFocus,
62
60
  onSave,
63
- onQuery,
64
- __gridScope,
65
- }: GridScopedProps<TableCellEditorProps>) => {
61
+ onCreate,
62
+ }: GridScopedProps<TableCellEditorProps<T>>) => {
66
63
  const { editing } = useGridContext('TableValueEditor', __gridScope);
67
64
 
68
65
  const fieldProjection = useMemo<FieldProjection | undefined>(() => {
@@ -71,7 +68,7 @@ export const TableValueEditor = ({
71
68
  }
72
69
 
73
70
  const { col } = parseCellIndex(editing.index);
74
- const field = model.projection.fields[col];
71
+ const field = model.projection.getFields()[col];
75
72
  const fieldProjection = model.projection.getFieldProjection(field.id);
76
73
  invariant(fieldProjection);
77
74
  return fieldProjection;
@@ -79,50 +76,38 @@ export const TableValueEditor = ({
79
76
 
80
77
  if (
81
78
  fieldProjection?.props.type === TypeEnum.Array ||
82
- fieldProjection?.props.format === FormatEnum.SingleSelect
83
- // TODO(thure): Support `FormatEnum.MultiSelect`
79
+ fieldProjection?.props.format === Format.TypeFormat.SingleSelect ||
80
+ fieldProjection?.props.format === Format.TypeFormat.Ref
81
+ // TODO(thure): Support `Format.TypeFormat.MultiSelect`
84
82
  ) {
85
83
  return (
86
- <FormCellEditor
87
- fieldProjection={fieldProjection}
88
- model={model}
89
- schema={schema}
84
+ <FormCellEditor<T>
90
85
  __gridScope={__gridScope}
86
+ schema={schema}
87
+ model={model}
88
+ fieldProjection={fieldProjection}
89
+ modals={modals}
91
90
  onSave={onSave}
91
+ onCreate={onCreate}
92
92
  />
93
93
  );
94
94
  }
95
95
 
96
- // 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
+ // For all other types, use the existing cell editor.
97
+ return <TableCellEditor model={model} modals={modals} onFocus={onFocus} onSave={onSave} __gridScope={__gridScope} />;
108
98
  };
109
99
 
110
- const editorSlots = { scroller: { className: '!plb-[--dx-grid-cell-editor-padding-block]' } };
111
-
112
100
  export const TableCellEditor = ({
101
+ __gridScope,
113
102
  model,
114
103
  modals,
115
- onEnter,
116
104
  onFocus,
117
- onQuery,
118
105
  onSave,
119
- __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) {
@@ -130,44 +115,13 @@ export const TableCellEditor = ({
130
115
  }
131
116
 
132
117
  const { col } = parseCellIndex(editing.index);
133
- const field = model.projection.fields[col];
118
+ const field = model.projection.getFields()[col];
134
119
  const fieldProjection = model.projection.getFieldProjection(field.id);
135
120
  invariant(fieldProjection);
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
-
124
+ // TOOD(burdon): Attach to event handler?
171
125
  const handleEnter = useCallback(
172
126
  async (value: any) => {
173
127
  if (!model || !editing) {
@@ -180,7 +134,6 @@ export const TableCellEditor = ({
180
134
  if (validationResult.valid) {
181
135
  setValidationError(null);
182
136
  model.setCellData(cell, value);
183
- onEnter?.(cell);
184
137
  onFocus?.();
185
138
  setEditing(null);
186
139
  onSave?.();
@@ -189,19 +142,26 @@ export const TableCellEditor = ({
189
142
  setValidationVariant('error');
190
143
  }
191
144
  },
192
- [model, editing, onEnter, onFocus, setEditing],
145
+ [model, editing, onFocus, setEditing],
193
146
  );
194
147
 
195
148
  const handleBlur = useCallback<EditorBlurHandler>(
196
149
  async (value) => {
197
- if (!model || !editing) {
198
- return;
199
- }
200
150
  if (suppressNextBlur.current) {
201
151
  suppressNextBlur.current = false;
152
+ } else if (model && editing) {
153
+ // Save silently if validation passes
154
+ const cell = parseCellIndex(editing.index);
155
+ if (value !== undefined) {
156
+ const result = await model.validateCellData(cell, value);
157
+ if (result.valid) {
158
+ setValidationError(null);
159
+ model.setCellData(cell, value);
160
+ setEditing(null);
161
+ onSave?.();
162
+ }
163
+ }
202
164
  }
203
-
204
- // Don't save on blur - let handleClose handle validation and saving
205
165
  },
206
166
  [model, editing],
207
167
  );
@@ -213,25 +173,25 @@ export const TableCellEditor = ({
213
173
  }
214
174
 
215
175
  const cell = parseCellIndex(editing.index);
216
-
217
176
  if (value !== undefined) {
218
177
  // Pre-commit validation check.
219
178
  const result = await model.validateCellData(cell, value);
220
179
 
221
180
  if (result.valid) {
181
+ suppressNextBlur.current = true;
222
182
  setValidationError(null);
223
183
  model.setCellData(cell, value);
224
184
  setEditing(null);
225
185
  onSave?.();
226
- onEnter?.(cell);
227
186
  if (event && onFocus) {
228
- onFocus(determineNavigationAxis(event), determineNavigationDelta(event));
187
+ onFocus(determineNavigationAxis(event), determineNavigationDelta(event), cell);
229
188
  }
230
189
  } else {
231
190
  setValidationError(result.error);
232
191
  setValidationVariant('error');
233
192
  }
234
193
  } else {
194
+ suppressNextBlur.current = true;
235
195
  setValidationError(null);
236
196
  setEditing(null);
237
197
  onSave?.();
@@ -240,154 +200,48 @@ export const TableCellEditor = ({
240
200
  }
241
201
  }
242
202
  },
243
- [model, editing, onFocus, onEnter, fieldProjection, setEditing, onSave],
203
+ [model, editing, onFocus, fieldProjection, setEditing, onSave],
244
204
  );
245
205
 
246
- const extension = useMemo(() => {
206
+ const extensions = useMemo(() => {
247
207
  if (!fieldProjection) {
248
208
  return [];
249
209
  }
250
210
 
251
- const extension = [
211
+ const extensions = [
252
212
  editorKeys({
253
213
  onClose: handleClose,
254
214
  ...(editing?.initialContent && { onNav: handleClose }),
255
215
  }),
256
216
  ];
257
-
258
- const format = fieldProjection.props.format;
259
-
260
- if (format === FormatEnum.SingleSelect || format === FormatEnum.MultiSelect) {
261
- // TODO(ZaymonFC): Reconcile this with the TagPicker component?
262
- // Add markdown extensions needed by tag picker.
263
- extension.push(createMarkdownExtensions({ themeMode }));
264
-
265
- const options = fieldProjection.props.options || [];
266
-
267
- const mode = format === FormatEnum.SingleSelect ? ('single-select' as const) : ('multi-select' as const);
268
-
269
- extension.push(
270
- tagPicker({
271
- mode,
272
- keymap: false,
273
- onSearch: (text, selectedIds) => {
274
- return options
275
- .filter(
276
- (option) =>
277
- selectedIds.indexOf(option.id) === -1 &&
278
- (text.length === 0 || option.title.toLowerCase().includes(text.toLowerCase())),
279
- )
280
- .map((option) => ({
281
- id: option.id,
282
- label: option.title,
283
- hue: option.color as any,
284
- }));
285
- },
286
- onUpdate: (ids) => {
287
- if (model && editing) {
288
- if (ids.length === 0) {
289
- return;
290
- }
291
- if (mode === 'single-select') {
292
- void handleEnter(ids[0]);
217
+ // Add validation extension to handle content changes.
218
+ if (model && editing) {
219
+ extensions.push(
220
+ EditorView.updateListener.of(
221
+ debounce((update) => {
222
+ const content = update.state.doc.toString();
223
+ const cell = parseCellIndex(editing.index);
224
+
225
+ // Perform validation on content change.
226
+ void model.validateCellData(cell, content).then((result) => {
227
+ if (result.valid) {
228
+ setValidationError(null);
293
229
  } else {
294
- void handleEnter(ids);
230
+ setValidationError(result.error);
231
+ setValidationVariant('error');
295
232
  }
296
- }
297
- },
298
- }),
233
+ });
234
+ }, 10),
235
+ ),
299
236
  );
300
237
  }
301
238
 
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
- }
335
- }
336
-
337
- return extension;
338
- }, [model, modals, editing, fieldProjection, handleClose, themeMode]);
239
+ return extensions;
240
+ }, [model, modals, editing, fieldProjection, handleClose]);
339
241
 
340
242
  const getCellContent = useCallback<GridCellEditorProps['getCellContent']>(() => {
341
243
  if (model && editing) {
342
244
  const cell = parseCellIndex(editing.index);
343
- const { col } = cell;
344
- const field = model.projection.fields[col];
345
- const fieldProjection = model.projection.getFieldProjection(field.id);
346
-
347
- if (
348
- fieldProjection?.props.format === FormatEnum.SingleSelect ||
349
- fieldProjection?.props.format === FormatEnum.MultiSelect
350
- ) {
351
- const value = model.getCellData(cell);
352
-
353
- if (value !== undefined) {
354
- const options = fieldProjection.props.options || [];
355
-
356
- if (fieldProjection.props.format === FormatEnum.MultiSelect) {
357
- const tagItems = value
358
- .split(',')
359
- .map((id: string) => {
360
- const option = options.find((o) => o.id === id);
361
- if (option) {
362
- return {
363
- id,
364
- label: option.title,
365
- hue: option.color as any,
366
- };
367
- }
368
- return undefined;
369
- })
370
- .filter((item: any): item is { id: any; label: string; hue: any } => item !== undefined);
371
-
372
- return createLinks(tagItems);
373
- } else {
374
- const option = options.find((o) => o.id === value);
375
-
376
- if (option) {
377
- const tagItem = {
378
- id: value,
379
- label: option.title,
380
- hue: option.color as any,
381
- };
382
-
383
- return createLinks([tagItem]);
384
- }
385
- }
386
- }
387
-
388
- return '';
389
- }
390
-
391
245
  const value = model.getCellData(cell);
392
246
  return value !== undefined ? String(value) : '';
393
247
  }
@@ -395,12 +249,8 @@ export const TableCellEditor = ({
395
249
 
396
250
  return (
397
251
  <>
398
- <CellValidationMessage
399
- validationError={_validationError}
400
- variant={_validationVariant}
401
- __gridScope={__gridScope}
402
- />
403
- <GridCellEditor extension={extension} getCellContent={getCellContent} onBlur={handleBlur} slots={editorSlots} />
252
+ <CellValidationMessage validationError={validationError} variant={validationVariant} __gridScope={__gridScope} />
253
+ <GridCellEditor extensions={extensions} getCellContent={getCellContent} onBlur={handleBlur} slots={editorSlots} />
404
254
  </>
405
255
  );
406
256
  };
@@ -3,3 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './TableCellEditor';
6
+ export { type OnCreateHandler } from './FormCellEditor';
@@ -2,23 +2,20 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Rx } from '@effect-rx/rx-react';
6
- import React, { useMemo } from 'react';
5
+ import { Atom, RegistryContext } from '@effect-atom/atom-react';
6
+ import React, { useContext, useEffect, useMemo } from 'react';
7
7
 
8
- import { live } from '@dxos/live-object';
9
8
  import { type ThemedClassName } from '@dxos/react-ui';
10
9
  import {
11
10
  type ActionGraphEdges,
12
11
  type ActionGraphNodes,
13
12
  type ActionGraphProps,
13
+ Menu,
14
+ type MenuAction,
14
15
  createGapSeparator,
15
16
  createMenuAction,
16
- type MenuAction,
17
- MenuProvider,
18
- ToolbarMenu,
19
17
  useMenuActions,
20
18
  } from '@dxos/react-ui-menu';
21
- import { rxFromSignal } from '@dxos/react-ui-menu';
22
19
 
23
20
  import { translationKey } from '../../translations';
24
21
 
@@ -30,49 +27,60 @@ export type TableToolbarActionType = 'add-row' | 'comment' | 'save-view';
30
27
 
31
28
  type TableToolbarState = Partial<{ viewDirty: boolean }>;
32
29
 
30
+ // TODO(burdon): Radix style layout.
31
+
33
32
  export type TableToolbarProps = ThemedClassName<
34
33
  TableToolbarState & {
35
- onAdd: () => void;
36
- onSave: () => void;
34
+ onAdd?: () => void;
35
+ onSave?: () => void;
37
36
  attendableId?: string;
38
- customActions?: Rx.Rx<ActionGraphProps>;
37
+ customActions?: Atom.Atom<ActionGraphProps>;
39
38
  }
40
39
  >;
41
40
 
42
41
  const createTableToolbarActions = ({
43
- state,
42
+ stateAtom,
44
43
  onAdd,
45
44
  onSave,
46
45
  customActions,
47
46
  }: {
48
- state: TableToolbarState;
49
- onAdd: () => void;
50
- onSave: () => void;
51
- customActions?: Rx.Rx<ActionGraphProps>;
47
+ stateAtom: Atom.Atom<TableToolbarState>;
48
+ onAdd?: () => void;
49
+ onSave?: () => void;
50
+ customActions?: Atom.Atom<ActionGraphProps>;
52
51
  }) =>
53
- Rx.make((get) => {
54
- const add = createMenuAction<TableToolbarActionProperties>('add-row', onAdd, {
55
- type: 'add-row' as const,
56
- icon: 'ph--plus--regular',
57
- label: ['add row', { ns: translationKey }],
58
- testId: 'table.toolbar.add-row',
59
- });
60
- const save = createMenuAction<TableToolbarActionProperties>('save-view', onSave, {
61
- type: 'save-view' as const,
62
- icon: 'ph--floppy-disk--regular',
63
- label: ['save view label', { ns: translationKey }],
64
- testId: 'table.toolbar.save-view',
65
- iconOnly: false,
66
- hidden: get(rxFromSignal(() => !state.viewDirty)),
67
- });
52
+ Atom.make((get) => {
53
+ const nodes: ActionGraphNodes = [];
54
+ if (onAdd) {
55
+ const add = createMenuAction<TableToolbarActionProperties>('add-row', onAdd, {
56
+ type: 'add-row' as const,
57
+ icon: 'ph--plus--regular',
58
+ label: ['add row label', { ns: translationKey }],
59
+ testId: 'table.toolbar.add-row',
60
+ });
61
+ nodes.push(add);
62
+ }
63
+ if (onSave) {
64
+ const state = get(stateAtom);
65
+ const save = createMenuAction<TableToolbarActionProperties>('save-view', onSave, {
66
+ type: 'save-view' as const,
67
+ icon: 'ph--floppy-disk--regular',
68
+ label: ['save view label', { ns: translationKey }],
69
+ testId: 'table.toolbar.save-view',
70
+ iconOnly: false,
71
+ hidden: !state.viewDirty,
72
+ });
73
+ nodes.push(save);
74
+ }
68
75
  const gap = createGapSeparator();
69
- const nodes: ActionGraphNodes = [add, save, ...gap.nodes];
70
- const edges: ActionGraphEdges = nodes.map(({ id: target }) => ({ source: 'root', target }));
76
+ nodes.push(...gap.nodes);
77
+ const edges: ActionGraphEdges = nodes.map(({ id: target }) => ({ source: 'root', target, relation: 'child' }));
71
78
  if (customActions) {
72
79
  const custom = get(customActions);
73
80
  nodes.push(...custom.nodes);
74
81
  edges.push(...custom.edges);
75
82
  }
83
+
76
84
  return {
77
85
  nodes,
78
86
  edges,
@@ -86,17 +94,25 @@ export const TableToolbar = ({
86
94
  onAdd,
87
95
  onSave,
88
96
  customActions,
97
+ ...props
89
98
  }: TableToolbarProps) => {
90
- const state = useMemo(() => live<TableToolbarState>({ viewDirty }), []);
99
+ const registry = useContext(RegistryContext);
100
+ const stateAtom = useMemo(() => Atom.make<TableToolbarState>({ viewDirty }), []);
101
+
102
+ // Update state.viewDirty when the prop changes.
103
+ useEffect(() => {
104
+ registry.set(stateAtom, { viewDirty });
105
+ }, [registry, stateAtom, viewDirty]);
106
+
91
107
  const actionsCreator = useMemo(
92
- () => createTableToolbarActions({ state, onAdd, onSave, customActions }),
93
- [state, onAdd, onSave, customActions],
108
+ () => createTableToolbarActions({ stateAtom, onAdd, onSave, customActions }),
109
+ [stateAtom, onAdd, onSave, customActions],
94
110
  );
95
111
  const menu = useMenuActions(actionsCreator);
96
112
 
97
113
  return (
98
- <MenuProvider {...menu} attendableId={attendableId}>
99
- <ToolbarMenu classNames={classNames} />
100
- </MenuProvider>
114
+ <Menu.Root {...menu} attendableId={attendableId}>
115
+ <Menu.Toolbar {...props} classNames={classNames} />
116
+ </Menu.Root>
101
117
  );
102
118
  };