@dxos/react-ui-table 0.8.3 → 0.8.4-main.1068cf700f

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 (171) hide show
  1. package/dist/lib/browser/chunk-GTGYBRJA.mjs +1748 -0
  2. package/dist/lib/browser/chunk-GTGYBRJA.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-OZBLY7XM.mjs +76 -0
  4. package/dist/lib/browser/chunk-OZBLY7XM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1156 -2508
  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 +114 -0
  9. package/dist/lib/browser/testing/index.mjs.map +7 -0
  10. package/dist/lib/browser/types/index.mjs +3 -5
  11. package/dist/lib/node-esm/chunk-DPNSZHU5.mjs +1750 -0
  12. package/dist/lib/node-esm/chunk-DPNSZHU5.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-WNZSKIRF.mjs +77 -0
  14. package/dist/lib/node-esm/chunk-WNZSKIRF.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1156 -2508
  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 +115 -0
  19. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  20. package/dist/lib/node-esm/types/index.mjs +3 -5
  21. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +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 +4 -2
  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 +13 -9
  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 +2 -1
  38. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +14 -9
  40. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +12 -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 +4 -4
  48. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  49. package/dist/types/src/components/testing.d.ts +26 -56
  50. package/dist/types/src/components/testing.d.ts.map +1 -1
  51. package/dist/types/src/hooks/index.d.ts +2 -0
  52. package/dist/types/src/hooks/index.d.ts.map +1 -1
  53. package/dist/types/src/hooks/useAddRow.d.ts +13 -0
  54. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -0
  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 +11 -10
  58. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  59. package/dist/types/src/index.d.ts +1 -2
  60. package/dist/types/src/index.d.ts.map +1 -1
  61. package/dist/types/src/model/modal-controller.d.ts +5 -8
  62. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  63. package/dist/types/src/model/selection-model.d.ts +17 -7
  64. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  65. package/dist/types/src/model/table-model.d.ts +149 -32
  66. package/dist/types/src/model/table-model.d.ts.map +1 -1
  67. package/dist/types/src/model/table-presentation.d.ts +13 -2
  68. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  69. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  70. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  71. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  72. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  73. package/dist/types/src/testing/index.d.ts +2 -0
  74. package/dist/types/src/testing/index.d.ts.map +1 -0
  75. package/dist/types/src/testing/useTestTableModel.d.ts +25 -0
  76. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -0
  77. package/dist/types/src/translations.d.ts +5 -6
  78. package/dist/types/src/translations.d.ts.map +1 -1
  79. package/dist/types/src/types/Table.d.ts +31 -0
  80. package/dist/types/src/types/Table.d.ts.map +1 -0
  81. package/dist/types/src/types/index.d.ts +1 -1
  82. package/dist/types/src/types/index.d.ts.map +1 -1
  83. package/dist/types/src/util/dynamic-table.d.ts +15 -14
  84. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  85. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  86. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  87. package/dist/types/src/util/index.d.ts +2 -1
  88. package/dist/types/src/util/index.d.ts.map +1 -1
  89. package/dist/types/src/util/query-ast.d.ts +7 -0
  90. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  91. package/dist/types/src/util/schema.d.ts +1 -1
  92. package/dist/types/src/util/schema.d.ts.map +1 -1
  93. package/dist/types/src/util/sort.d.ts +12 -0
  94. package/dist/types/src/util/sort.d.ts.map +1 -0
  95. package/dist/types/src/util/table-buttons.d.ts +15 -1
  96. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  97. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  98. package/dist/types/tsconfig.tsbuildinfo +1 -1
  99. package/package.json +69 -56
  100. package/src/components/Table/ColumnActionsMenu.tsx +11 -7
  101. package/src/components/Table/ColumnSettings.tsx +16 -12
  102. package/src/components/Table/DynamicTable.stories.tsx +29 -25
  103. package/src/components/Table/DynamicTable.tsx +34 -22
  104. package/src/components/Table/Relations.stories.tsx +237 -0
  105. package/src/components/Table/RowActionsMenu.tsx +26 -17
  106. package/src/components/Table/Table.stories.tsx +171 -194
  107. package/src/components/Table/Table.tsx +364 -286
  108. package/src/components/TableCellEditor/CellValidationMessage.tsx +8 -3
  109. package/src/components/TableCellEditor/FormCellEditor.tsx +146 -49
  110. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +36 -76
  111. package/src/components/TableCellEditor/TableCellEditor.tsx +106 -198
  112. package/src/components/TableCellEditor/index.ts +1 -0
  113. package/src/components/TableToolbar/TableToolbar.tsx +49 -33
  114. package/src/components/testing.ts +41 -32
  115. package/src/hooks/index.ts +2 -0
  116. package/src/hooks/useAddRow.ts +36 -0
  117. package/src/hooks/useProjectionModel.ts +39 -0
  118. package/src/hooks/useTableModel.ts +41 -35
  119. package/src/index.ts +1 -2
  120. package/src/model/modal-controller.ts +24 -41
  121. package/src/model/selection-model.ts +75 -41
  122. package/src/model/table-model.test.ts +92 -56
  123. package/src/model/table-model.ts +628 -162
  124. package/src/model/table-presentation.test.ts +70 -59
  125. package/src/model/table-presentation.ts +278 -149
  126. package/src/playwright/TableManager.ts +13 -6
  127. package/src/playwright/playwright.config.ts +17 -0
  128. package/src/playwright/smoke.spec.ts +180 -41
  129. package/src/testing/index.ts +5 -0
  130. package/src/testing/useTestTableModel.ts +133 -0
  131. package/src/translations.ts +7 -5
  132. package/src/types/Table.ts +90 -0
  133. package/src/types/index.ts +2 -2
  134. package/src/util/dynamic-table.test.ts +70 -0
  135. package/src/util/dynamic-table.ts +50 -36
  136. package/src/util/index.ts +2 -1
  137. package/src/util/query-ast.ts +19 -0
  138. package/src/util/schema.ts +4 -4
  139. package/src/util/sort.ts +90 -0
  140. package/src/util/table-buttons.ts +28 -4
  141. package/src/util/table-controls.ts +4 -1
  142. package/dist/lib/browser/chunk-TL7CKQQV.mjs +0 -27
  143. package/dist/lib/browser/chunk-TL7CKQQV.mjs.map +0 -7
  144. package/dist/lib/node/chunk-O2VPBNS5.cjs +0 -50
  145. package/dist/lib/node/chunk-O2VPBNS5.cjs.map +0 -7
  146. package/dist/lib/node/index.cjs +0 -2739
  147. package/dist/lib/node/index.cjs.map +0 -7
  148. package/dist/lib/node/meta.json +0 -1
  149. package/dist/lib/node/types/index.cjs +0 -31
  150. package/dist/lib/node/types/index.cjs.map +0 -7
  151. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs +0 -29
  152. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs.map +0 -7
  153. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  154. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  155. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  156. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  157. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  158. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  159. package/dist/types/src/model/table-sorting.d.ts +0 -53
  160. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  161. package/dist/types/src/types/table.d.ts +0 -56
  162. package/dist/types/src/types/table.d.ts.map +0 -1
  163. package/dist/types/src/util/table.d.ts +0 -14
  164. package/dist/types/src/util/table.d.ts.map +0 -1
  165. package/src/components/Table/CreateRefPanel.tsx +0 -84
  166. package/src/components/Table/relations.stories.tsx +0 -124
  167. package/src/components/TableCellEditor/extension.ts +0 -61
  168. package/src/model/table-sorting.ts +0 -216
  169. package/src/playwright/playwright.config.cts +0 -18
  170. package/src/types/table.ts +0 -27
  171. package/src/util/table.ts +0 -127
@@ -3,64 +3,63 @@
3
3
  //
4
4
 
5
5
  import { type Completion } from '@codemirror/autocomplete';
6
- import { type Schema } from 'effect/Schema';
6
+ import { EditorView } from '@codemirror/view';
7
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 { tagPickerExtension, 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: '!plb-[--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.Entity.Any = Type.Entity.Any> = {
46
+ schema?: T;
47
47
  model?: TableModel;
48
48
  modals?: ModalController;
49
- schema?: Schema.AnyNoContext;
50
- onEnter?: (cell: DxGridPlanePosition) => void;
51
- onFocus?: DxGrid['refocus'];
52
- onQuery?: (field: FieldProjection, text: string) => Promise<QueryResult[]>;
49
+ onFocus?: (axis?: DxGridAxis, delta?: -1 | 0 | 1, cell?: DxGridPosition) => void;
50
+ onCreate?: OnCreateHandler;
51
+ onSave?: () => void;
53
52
  };
54
53
 
55
- export const TableValueEditor = ({
54
+ export const TableValueEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
55
+ __gridScope,
56
+ schema,
56
57
  model,
57
58
  modals,
58
- schema,
59
- onEnter,
60
59
  onFocus,
61
- onQuery,
62
- __gridScope,
63
- }: GridScopedProps<TableCellEditorProps>) => {
60
+ onSave,
61
+ onCreate,
62
+ }: GridScopedProps<TableCellEditorProps<T>>) => {
64
63
  const { editing } = useGridContext('TableValueEditor', __gridScope);
65
64
 
66
65
  const fieldProjection = useMemo<FieldProjection | undefined>(() => {
@@ -69,41 +68,46 @@ export const TableValueEditor = ({
69
68
  }
70
69
 
71
70
  const { col } = parseCellIndex(editing.index);
72
- const field = model.projection.view.fields[col];
71
+ const field = model.projection.getFields()[col];
73
72
  const fieldProjection = model.projection.getFieldProjection(field.id);
74
73
  invariant(fieldProjection);
75
74
  return fieldProjection;
76
75
  }, [model, editing]);
77
76
 
78
- if (fieldProjection?.props.type === TypeEnum.Array) {
79
- return <FormCellEditor fieldProjection={fieldProjection} model={model} schema={schema} __gridScope={__gridScope} />;
77
+ if (
78
+ fieldProjection?.props.type === TypeEnum.Array ||
79
+ fieldProjection?.props.format === Format.TypeFormat.SingleSelect ||
80
+ fieldProjection?.props.format === Format.TypeFormat.Ref
81
+ // TODO(thure): Support `Format.TypeFormat.MultiSelect`
82
+ ) {
83
+ return (
84
+ <FormCellEditor<T>
85
+ __gridScope={__gridScope}
86
+ schema={schema}
87
+ model={model}
88
+ fieldProjection={fieldProjection}
89
+ modals={modals}
90
+ onSave={onSave}
91
+ onCreate={onCreate}
92
+ />
93
+ );
80
94
  }
81
95
 
82
- // For all other types, use the existing cell editor
83
- return (
84
- <TableCellEditor
85
- model={model}
86
- modals={modals}
87
- onEnter={onEnter}
88
- onFocus={onFocus}
89
- onQuery={onQuery}
90
- __gridScope={__gridScope}
91
- />
92
- );
96
+ // For all other types, use the existing cell editor.
97
+ return <TableCellEditor model={model} modals={modals} onFocus={onFocus} onSave={onSave} __gridScope={__gridScope} />;
93
98
  };
94
99
 
95
100
  export const TableCellEditor = ({
101
+ __gridScope,
96
102
  model,
97
103
  modals,
98
- onEnter,
99
104
  onFocus,
100
- onQuery,
101
- __gridScope,
105
+ onSave,
102
106
  }: GridScopedProps<TableCellEditorProps>) => {
103
- const { id: gridId, editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
107
+ const { editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
104
108
  const suppressNextBlur = useRef(false);
105
- const { themeMode } = useThemeContext();
106
- const [_validationError, setValidationError] = useState<string | null>(null);
109
+ const [validationError, setValidationError] = useState<string | null>(null);
110
+ const [validationVariant, setValidationVariant] = useState<'error' | 'warning'>('error');
107
111
 
108
112
  const fieldProjection = useMemo<FieldProjection | undefined>(() => {
109
113
  if (!model || !editing) {
@@ -111,12 +115,13 @@ export const TableCellEditor = ({
111
115
  }
112
116
 
113
117
  const { col } = parseCellIndex(editing.index);
114
- const field = model.projection.view.fields[col];
118
+ const field = model.projection.getFields()[col];
115
119
  const fieldProjection = model.projection.getFieldProjection(field.id);
116
120
  invariant(fieldProjection);
117
121
  return fieldProjection;
118
122
  }, [model, editing]);
119
123
 
124
+ // TOOD(burdon): Attach to event handler?
120
125
  const handleEnter = useCallback(
121
126
  async (value: any) => {
122
127
  if (!model || !editing) {
@@ -124,34 +129,39 @@ export const TableCellEditor = ({
124
129
  }
125
130
 
126
131
  const cell = parseCellIndex(editing.index);
132
+ const validationResult = await model.validateCellData(cell, value);
127
133
 
128
- // Validate the value
129
- const result = await model.validateCellData(cell, value);
130
-
131
- if (result.valid) {
134
+ if (validationResult.valid) {
132
135
  setValidationError(null);
133
136
  model.setCellData(cell, value);
134
- onEnter?.(cell);
135
137
  onFocus?.();
136
138
  setEditing(null);
139
+ onSave?.();
137
140
  } else {
138
- setValidationError(result.error);
139
- // Editor stays open on validation failure
141
+ setValidationError(validationResult.error);
142
+ setValidationVariant('error');
140
143
  }
141
144
  },
142
- [model, editing, onEnter, onFocus, setEditing],
145
+ [model, editing, onFocus, setEditing],
143
146
  );
144
147
 
145
148
  const handleBlur = useCallback<EditorBlurHandler>(
146
149
  async (value) => {
147
- if (!model || !editing) {
148
- return;
149
- }
150
150
  if (suppressNextBlur.current) {
151
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
+ }
152
164
  }
153
-
154
- // Don't save on blur - let handleClose handle validation and saving
155
165
  },
156
166
  [model, editing],
157
167
  );
@@ -163,177 +173,75 @@ export const TableCellEditor = ({
163
173
  }
164
174
 
165
175
  const cell = parseCellIndex(editing.index);
166
-
167
176
  if (value !== undefined) {
168
177
  // Pre-commit validation check.
169
178
  const result = await model.validateCellData(cell, value);
170
179
 
171
180
  if (result.valid) {
181
+ suppressNextBlur.current = true;
172
182
  setValidationError(null);
173
183
  model.setCellData(cell, value);
174
184
  setEditing(null);
175
- onEnter?.(cell);
185
+ onSave?.();
176
186
  if (event && onFocus) {
177
- onFocus(determineNavigationAxis(event), determineNavigationDelta(event));
187
+ onFocus(determineNavigationAxis(event), determineNavigationDelta(event), cell);
178
188
  }
179
189
  } else {
180
190
  setValidationError(result.error);
191
+ setValidationVariant('error');
181
192
  }
182
193
  } else {
194
+ suppressNextBlur.current = true;
183
195
  setValidationError(null);
184
196
  setEditing(null);
197
+ onSave?.();
185
198
  if (event && onFocus) {
186
199
  onFocus(determineNavigationAxis(event), determineNavigationDelta(event));
187
200
  }
188
201
  }
189
202
  },
190
- [model, editing, onFocus, onEnter, fieldProjection, setEditing],
203
+ [model, editing, onFocus, fieldProjection, setEditing, onSave],
191
204
  );
192
205
 
193
- const extension = useMemo(() => {
206
+ const extensions = useMemo(() => {
194
207
  if (!fieldProjection) {
195
208
  return [];
196
209
  }
197
210
 
198
- const extension = [
211
+ const extensions = [
199
212
  editorKeys({
200
213
  onClose: handleClose,
201
214
  ...(editing?.initialContent && { onNav: handleClose }),
202
215
  }),
203
216
  ];
204
-
205
- const format = fieldProjection.props.format;
206
-
207
- if (format === FormatEnum.SingleSelect || format === FormatEnum.MultiSelect) {
208
- // TODO(ZaymonFC): Reconcile this with the TagPicker component?
209
- // Add markdown extensions needed by tag picker.
210
- extension.push(createMarkdownExtensions({ themeMode }));
211
-
212
- const options = fieldProjection.props.options || [];
213
-
214
- const mode = format === FormatEnum.SingleSelect ? ('single-select' as const) : ('multi-select' as const);
215
-
216
- extension.push(
217
- tagPickerExtension({
218
- mode,
219
- inGrid: true,
220
- onSearch: (text, selectedIds) => {
221
- return options
222
- .filter(
223
- (option) =>
224
- selectedIds.indexOf(option.id) === -1 &&
225
- (text.length === 0 || option.title.toLowerCase().includes(text.toLowerCase())),
226
- )
227
- .map((option) => ({
228
- id: option.id,
229
- label: option.title,
230
- hue: option.color as any,
231
- }));
232
- },
233
- onUpdate: (ids) => {
234
- if (model && editing) {
235
- if (ids.length === 0) {
236
- return;
237
- }
238
- if (mode === 'single-select') {
239
- 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);
240
229
  } else {
241
- void handleEnter(ids);
230
+ setValidationError(result.error);
231
+ setValidationVariant('error');
242
232
  }
243
- }
244
- },
245
- }),
233
+ });
234
+ }, 10),
235
+ ),
246
236
  );
247
237
  }
248
238
 
249
- if (onQuery) {
250
- switch (fieldProjection.props.format) {
251
- case FormatEnum.Ref: {
252
- extension.push([
253
- completion({
254
- onQuery: (text) => onQuery(fieldProjection, text),
255
- onMatch: (data) => {
256
- if (model && editing && modals) {
257
- if (isCreateOption(data)) {
258
- const { field, props } = fieldProjection;
259
- if (props.referenceSchema) {
260
- suppressNextBlur.current = true;
261
- modals.openCreateRef(
262
- props.referenceSchema,
263
- document.querySelector(cellQuery(editing.index, gridId)),
264
- {
265
- [field.referencePath!]: data.text,
266
- },
267
- (data) => {
268
- void handleEnter(data);
269
- },
270
- );
271
- }
272
- } else {
273
- void handleEnter(data);
274
- }
275
- }
276
- },
277
- }),
278
- ]);
279
- break;
280
- }
281
- }
282
- }
283
-
284
- return extension;
285
- }, [model, modals, editing, fieldProjection, handleClose, themeMode]);
239
+ return extensions;
240
+ }, [model, modals, editing, fieldProjection, handleClose]);
286
241
 
287
242
  const getCellContent = useCallback<GridCellEditorProps['getCellContent']>(() => {
288
243
  if (model && editing) {
289
244
  const cell = parseCellIndex(editing.index);
290
- const { col } = cell;
291
- const field = model.projection.view.fields[col];
292
- const fieldProjection = model.projection.getFieldProjection(field.id);
293
-
294
- if (
295
- fieldProjection?.props.format === FormatEnum.SingleSelect ||
296
- fieldProjection?.props.format === FormatEnum.MultiSelect
297
- ) {
298
- const value = model.getCellData(cell);
299
-
300
- if (value !== undefined) {
301
- const options = fieldProjection.props.options || [];
302
-
303
- if (fieldProjection.props.format === FormatEnum.MultiSelect && Array.isArray(value)) {
304
- const tagItems = value
305
- .map((id) => {
306
- const option = options.find((o) => o.id === id);
307
- if (option) {
308
- return {
309
- id,
310
- label: option.title,
311
- hue: option.color as any,
312
- };
313
- }
314
- return undefined;
315
- })
316
- .filter((item): item is { id: any; label: string; hue: any } => item !== undefined);
317
-
318
- return createLinks(tagItems);
319
- } else {
320
- const option = options.find((o) => o.id === value);
321
-
322
- if (option) {
323
- const tagItem = {
324
- id: value,
325
- label: option.title,
326
- hue: option.color as any,
327
- };
328
-
329
- return createLinks([tagItem]);
330
- }
331
- }
332
- }
333
-
334
- return '';
335
- }
336
-
337
245
  const value = model.getCellData(cell);
338
246
  return value !== undefined ? String(value) : '';
339
247
  }
@@ -341,8 +249,8 @@ export const TableCellEditor = ({
341
249
 
342
250
  return (
343
251
  <>
344
- <CellValidationMessage validationError={_validationError} __gridScope={__gridScope} />
345
- <GridCellEditor extension={extension} getCellContent={getCellContent} onBlur={handleBlur} />
252
+ <CellValidationMessage validationError={validationError} variant={validationVariant} __gridScope={__gridScope} />
253
+ <GridCellEditor extensions={extensions} getCellContent={getCellContent} onBlur={handleBlur} slots={editorSlots} />
346
254
  </>
347
255
  );
348
256
  };
@@ -3,3 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './TableCellEditor';
6
+ export { type OnCreateHandler } from './FormCellEditor';
@@ -2,23 +2,21 @@
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,
14
- createGapSeparator,
15
- createMenuAction,
16
13
  type MenuAction,
17
14
  MenuProvider,
18
15
  ToolbarMenu,
16
+ createGapSeparator,
17
+ createMenuAction,
19
18
  useMenuActions,
20
19
  } from '@dxos/react-ui-menu';
21
- import { rxFromSignal } from '@dxos/react-ui-menu';
22
20
 
23
21
  import { translationKey } from '../../translations';
24
22
 
@@ -30,49 +28,60 @@ export type TableToolbarActionType = 'add-row' | 'comment' | 'save-view';
30
28
 
31
29
  type TableToolbarState = Partial<{ viewDirty: boolean }>;
32
30
 
31
+ // TODO(burdon): Radix style layout.
32
+
33
33
  export type TableToolbarProps = ThemedClassName<
34
34
  TableToolbarState & {
35
- onAdd: () => void;
36
- onSave: () => void;
35
+ onAdd?: () => void;
36
+ onSave?: () => void;
37
37
  attendableId?: string;
38
- customActions?: Rx.Rx<ActionGraphProps>;
38
+ customActions?: Atom.Atom<ActionGraphProps>;
39
39
  }
40
40
  >;
41
41
 
42
42
  const createTableToolbarActions = ({
43
- state,
43
+ stateAtom,
44
44
  onAdd,
45
45
  onSave,
46
46
  customActions,
47
47
  }: {
48
- state: TableToolbarState;
49
- onAdd: () => void;
50
- onSave: () => void;
51
- customActions?: Rx.Rx<ActionGraphProps>;
48
+ stateAtom: Atom.Atom<TableToolbarState>;
49
+ onAdd?: () => void;
50
+ onSave?: () => void;
51
+ customActions?: Atom.Atom<ActionGraphProps>;
52
52
  }) =>
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
- });
53
+ Atom.make((get) => {
54
+ const nodes: ActionGraphNodes = [];
55
+ if (onAdd) {
56
+ const add = createMenuAction<TableToolbarActionProperties>('add-row', onAdd, {
57
+ type: 'add-row' as const,
58
+ icon: 'ph--plus--regular',
59
+ label: ['add row label', { ns: translationKey }],
60
+ testId: 'table.toolbar.add-row',
61
+ });
62
+ nodes.push(add);
63
+ }
64
+ if (onSave) {
65
+ const state = get(stateAtom);
66
+ const save = createMenuAction<TableToolbarActionProperties>('save-view', onSave, {
67
+ type: 'save-view' as const,
68
+ icon: 'ph--floppy-disk--regular',
69
+ label: ['save view label', { ns: translationKey }],
70
+ testId: 'table.toolbar.save-view',
71
+ iconOnly: false,
72
+ hidden: !state.viewDirty,
73
+ });
74
+ nodes.push(save);
75
+ }
68
76
  const gap = createGapSeparator();
69
- const nodes: ActionGraphNodes = [add, save, ...gap.nodes];
77
+ nodes.push(...gap.nodes);
70
78
  const edges: ActionGraphEdges = nodes.map(({ id: target }) => ({ source: 'root', target }));
71
79
  if (customActions) {
72
80
  const custom = get(customActions);
73
81
  nodes.push(...custom.nodes);
74
82
  edges.push(...custom.edges);
75
83
  }
84
+
76
85
  return {
77
86
  nodes,
78
87
  edges,
@@ -87,10 +96,17 @@ export const TableToolbar = ({
87
96
  onSave,
88
97
  customActions,
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