@dxos/react-ui-table 0.8.4-main.67995b8 → 0.8.4-main.70d3990

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 (143) hide show
  1. package/dist/lib/browser/chunk-5XWSB3JP.mjs +71 -0
  2. package/dist/lib/browser/chunk-5XWSB3JP.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-5XLSHLOG.mjs → chunk-KNC7XYT6.mjs} +573 -541
  4. package/dist/lib/browser/chunk-KNC7XYT6.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +625 -698
  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 +16 -24
  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-Y66YLSTC.mjs → chunk-CWSGS3Y2.mjs} +573 -541
  12. package/dist/lib/node-esm/chunk-CWSGS3Y2.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-MKTMOY46.mjs +72 -0
  14. package/dist/lib/node-esm/chunk-MKTMOY46.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +625 -698
  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 +16 -24
  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/DynamicTable.d.ts +2 -2
  26. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +51 -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 +108 -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 +8 -4
  34. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  35. package/dist/types/src/components/Table/Table.stories.d.ts +105 -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.map +1 -1
  38. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +12 -6
  39. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  40. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +12 -17
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  42. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +39 -4
  43. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  44. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  45. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  46. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +2 -2
  47. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  48. package/dist/types/src/components/testing.d.ts +3 -5
  49. package/dist/types/src/components/testing.d.ts.map +1 -1
  50. package/dist/types/src/hooks/index.d.ts +2 -1
  51. package/dist/types/src/hooks/index.d.ts.map +1 -1
  52. package/dist/types/src/hooks/useAddRow.d.ts +3 -2
  53. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  54. package/dist/types/src/hooks/useProjectionModel.d.ts +5 -0
  55. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  56. package/dist/types/src/hooks/useTableModel.d.ts +5 -6
  57. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  58. package/dist/types/src/model/modal-controller.d.ts +0 -7
  59. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  60. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  61. package/dist/types/src/model/table-model.d.ts +14 -14
  62. package/dist/types/src/model/table-model.d.ts.map +1 -1
  63. package/dist/types/src/model/table-presentation.d.ts +2 -1
  64. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  65. package/dist/types/src/model/table-sorting.d.ts +9 -6
  66. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  67. package/dist/types/src/playwright/TableManager.d.ts +1 -1
  68. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  69. package/dist/types/src/testing/useTestTableModel.d.ts +21 -34
  70. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  71. package/dist/types/src/translations.d.ts +2 -2
  72. package/dist/types/src/types/Table.d.ts +28 -0
  73. package/dist/types/src/types/Table.d.ts.map +1 -0
  74. package/dist/types/src/types/index.d.ts +1 -1
  75. package/dist/types/src/types/index.d.ts.map +1 -1
  76. package/dist/types/src/util/dynamic-table.d.ts +9 -9
  77. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  78. package/dist/types/src/util/index.d.ts +0 -1
  79. package/dist/types/src/util/index.d.ts.map +1 -1
  80. package/dist/types/src/util/schema.d.ts +1 -1
  81. package/dist/types/src/util/schema.d.ts.map +1 -1
  82. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  83. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  84. package/dist/types/tsconfig.tsbuildinfo +1 -1
  85. package/package.json +61 -59
  86. package/src/components/Table/ColumnActionsMenu.tsx +1 -1
  87. package/src/components/Table/ColumnSettings.tsx +1 -1
  88. package/src/components/Table/DynamicTable.stories.tsx +21 -17
  89. package/src/components/Table/DynamicTable.tsx +19 -13
  90. package/src/components/Table/Relations.stories.tsx +237 -0
  91. package/src/components/Table/RowActionsMenu.tsx +23 -15
  92. package/src/components/Table/Table.stories.tsx +103 -86
  93. package/src/components/Table/Table.tsx +140 -115
  94. package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
  95. package/src/components/TableCellEditor/FormCellEditor.tsx +153 -42
  96. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +28 -66
  97. package/src/components/TableCellEditor/TableCellEditor.tsx +75 -225
  98. package/src/components/TableCellEditor/index.ts +1 -0
  99. package/src/components/TableToolbar/TableToolbar.tsx +34 -24
  100. package/src/components/testing.ts +4 -4
  101. package/src/hooks/index.ts +2 -1
  102. package/src/hooks/useAddRow.ts +7 -5
  103. package/src/hooks/useProjectionModel.ts +19 -0
  104. package/src/hooks/useTableModel.ts +12 -14
  105. package/src/model/modal-controller.ts +1 -29
  106. package/src/model/selection-model.ts +3 -2
  107. package/src/model/table-model.test.ts +21 -12
  108. package/src/model/table-model.ts +62 -56
  109. package/src/model/table-presentation.test.ts +25 -23
  110. package/src/model/table-presentation.ts +82 -41
  111. package/src/model/table-sorting.ts +32 -14
  112. package/src/playwright/TableManager.ts +3 -3
  113. package/src/playwright/smoke.spec.ts +132 -38
  114. package/src/testing/useTestTableModel.ts +34 -22
  115. package/src/translations.ts +2 -2
  116. package/src/types/Table.ts +89 -0
  117. package/src/types/index.ts +1 -1
  118. package/src/util/dynamic-table.ts +20 -24
  119. package/src/util/index.ts +0 -1
  120. package/src/util/schema.ts +4 -4
  121. package/src/util/table-buttons.ts +4 -3
  122. package/src/util/table-controls.ts +4 -1
  123. package/dist/lib/browser/chunk-5XLSHLOG.mjs.map +0 -7
  124. package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
  125. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
  126. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +0 -20
  127. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +0 -7
  128. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs.map +0 -7
  129. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  130. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  131. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  132. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  133. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  134. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  135. package/dist/types/src/types/schema.d.ts +0 -8
  136. package/dist/types/src/types/schema.d.ts.map +0 -1
  137. package/dist/types/src/util/table.d.ts +0 -11
  138. package/dist/types/src/util/table.d.ts.map +0 -1
  139. package/src/components/Table/CreateRefPanel.tsx +0 -86
  140. package/src/components/Table/relations.stories.tsx +0 -116
  141. package/src/components/TableCellEditor/extension.ts +0 -61
  142. package/src/types/schema.ts +0 -16
  143. package/src/util/table.ts +0 -49
@@ -2,100 +2,60 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
8
- import React, { useEffect, useMemo, useState } from 'react';
6
+ import React from 'react';
9
7
 
10
8
  import { Obj } from '@dxos/echo';
11
- import { type EchoSchema, isMutable } from '@dxos/echo-schema';
12
- import { invariant } from '@dxos/invariant';
13
9
  import { faker } from '@dxos/random';
14
- import { Filter, useQuery } from '@dxos/react-client/echo';
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';
18
- import { withLayout, withTheme } from '@dxos/storybook-utils';
10
+ import { withClientProvider } from '@dxos/react-client/testing';
11
+ import { withTheme } from '@dxos/react-ui/testing';
12
+ import { Grid, type GridEditing, defaultRowSize } from '@dxos/react-ui-grid';
13
+ import { View } from '@dxos/schema';
14
+ import { Task } from '@dxos/types';
19
15
 
20
- import { TableCellEditor, type TableCellEditorProps } from './TableCellEditor';
21
- import { useTableModel } from '../../hooks';
22
- import { type TableFeatures } from '../../model';
16
+ import { useTestTableModel } from '../../testing';
23
17
  import { translations } from '../../translations';
24
- import { TableView } from '../../types';
25
- import { createTable } from '../../util';
18
+ import { Table } from '../../types';
19
+
20
+ import { TableCellEditor } from './TableCellEditor';
26
21
 
27
22
  type StoryProps = {
28
23
  editing: GridEditing;
29
24
  };
30
25
 
31
26
  const DefaultStory = ({ editing }: StoryProps) => {
32
- const { space } = useClientProvider();
33
- invariant(space);
34
-
35
- const views = useQuery(space, Filter.type(DataType.View));
36
- const [view, setView] = useState<DataType.View>();
37
- const [schema, setSchema] = useState<EchoSchema>();
38
- useEffect(() => {
39
- if (space && views.length && !view) {
40
- const view = views[0];
41
- setView(view);
42
- setSchema(space.db.schemaRegistry.getSchema(view.query.typename!));
43
- }
44
- }, [space, views, view]);
45
-
46
- const features: Partial<TableFeatures> = useMemo(
47
- () => ({
48
- selection: { enabled: true, mode: 'multiple' },
49
- dataEditable: true,
50
- schemaEditable: schema && isMutable(schema),
51
- }),
52
- [schema],
53
- );
27
+ const { model, table } = useTestTableModel();
54
28
 
55
- const model = useTableModel({ view, schema: schema?.jsonSchema, features });
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
- if (!model || !schema || !view) {
29
+ if (!model || !table) {
70
30
  return <div />;
71
31
  }
72
32
 
73
33
  return (
74
- <div className='flex w-[300px] border border-separator' style={{ height: defaultRowSize }}>
34
+ <div className='flex is-[300px] border border-separator' style={{ height: defaultRowSize }}>
75
35
  <Grid.Root id='test' editing={editing}>
76
- <TableCellEditor model={model} onQuery={handleQuery} />
36
+ <TableCellEditor model={model} schema={Task.Task} />
77
37
  </Grid.Root>
78
38
  </div>
79
39
  );
80
40
  };
81
41
 
82
- const meta: Meta<StoryProps> = {
42
+ const meta = {
83
43
  title: 'ui/react-ui-table/TableCellEditor',
84
44
  component: DefaultStory,
85
45
  render: DefaultStory,
86
- parameters: { translations, layout: 'centered' },
87
46
  decorators: [
47
+ withTheme,
88
48
  withClientProvider({
89
- types: [DataType.View, TableView],
49
+ types: [View.View, Task.Task, Table.Table],
90
50
  createIdentity: true,
91
51
  createSpace: true,
92
- onSpaceCreated: async ({ client, space }) => {
93
- const schema = createDefaultSchema();
94
- const { view } = await createTable({ client, space, typename: schema.typename });
95
- space.db.add(view);
52
+ onCreateSpace: async ({ client, space }) => {
53
+ const { view, jsonSchema } = await View.makeFromSpace({ client, space, typename: Task.Task.typename });
54
+ const table = Table.make({ view, jsonSchema });
55
+ space.db.add(table);
96
56
  Array.from({ length: 10 }).forEach(() => {
97
57
  space.db.add(
98
- Obj.make(schema, {
58
+ Obj.make(Task.Task, {
99
59
  title: faker.person.fullName(),
100
60
  status: faker.helpers.arrayElement(['todo', 'in-progress', 'done'] as const),
101
61
  description: faker.lorem.sentence(),
@@ -104,14 +64,16 @@ const meta: Meta<StoryProps> = {
104
64
  });
105
65
  },
106
66
  }),
107
- withTheme,
108
- withLayout(),
109
67
  ],
110
- };
68
+ parameters: {
69
+ translations,
70
+ layout: 'centered',
71
+ },
72
+ } satisfies Meta<typeof DefaultStory>;
111
73
 
112
74
  export default meta;
113
75
 
114
- type Story = StoryObj<StoryProps>;
76
+ type Story = StoryObj<typeof meta>;
115
77
 
116
78
  export const Default: Story = {
117
79
  args: {
@@ -3,64 +3,59 @@
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 type * as Schema from 'effect/Schema';
8
+ import React, { useCallback, useMemo, useRef, useState } from 'react';
8
9
 
9
- import { FormatEnum, TypeEnum } from '@dxos/echo-schema';
10
+ import { debounce } from '@dxos/async';
11
+ import type { Client } from '@dxos/client';
12
+ import { Format, TypeEnum } from '@dxos/echo/internal';
10
13
  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';
14
+ import { type DxGridAxis, type DxGridPosition } from '@dxos/lit-grid';
14
15
  import {
15
- cellQuery,
16
- editorKeys,
17
- parseCellIndex,
18
- useGridContext,
19
- type DxGridPlanePosition,
16
+ type EditorBlurHandler,
20
17
  type EditorKeyEvent,
21
18
  type EditorKeyOrBlurHandler,
22
- type EditorBlurHandler,
23
19
  GridCellEditor,
24
20
  type GridCellEditorProps,
25
21
  type GridScopedProps,
22
+ editorKeys,
23
+ parseCellIndex,
24
+ useGridContext,
26
25
  } from '@dxos/react-ui-grid';
27
- import { tagPicker, createLinks } from '@dxos/react-ui-tag-picker';
28
26
  import { type FieldProjection } from '@dxos/schema';
29
27
 
30
- import { CellValidationMessage } from './CellValidationMessage';
31
- import { FormCellEditor } from './FormCellEditor';
32
- import { completion } from './extension';
33
- import { type TableModel, type ModalController } from '../../model';
28
+ import { type ModalController, type TableModel } from '../../model';
34
29
 
35
- const newValue = Symbol.for('newValue');
30
+ import { CellValidationMessage } from './CellValidationMessage';
31
+ import { FormCellEditor, type OnCreateHandler } from './FormCellEditor';
36
32
 
37
33
  /**
38
34
  * Option to create new object/value.
39
35
  */
40
- export const createOption = (text: string) => ({ [newValue]: true, text });
41
-
42
- const isCreateOption = (data: any) => typeof data === 'object' && data[newValue];
43
-
44
36
  export type QueryResult = Pick<Completion, 'label'> & { data: any };
45
37
 
46
38
  export type TableCellEditorProps = {
47
39
  model?: TableModel;
48
40
  modals?: ModalController;
49
- schema?: Schema.AnyNoContext;
50
- onEnter?: (cell: DxGridPlanePosition) => void;
51
- onFocus?: DxGrid['refocus'];
41
+ schema?: Schema.Schema.AnyNoContext;
42
+ onFocus?: (axis?: DxGridAxis, delta?: -1 | 0 | 1, cell?: DxGridPosition) => void;
43
+ onCreate?: OnCreateHandler;
52
44
  onSave?: () => void;
53
- onQuery?: (field: FieldProjection, text: string) => Promise<QueryResult[]>;
45
+ client?: Client;
54
46
  };
55
47
 
48
+ const adaptValidationMessage = (message: string | null) =>
49
+ message ? (message.endsWith('is missing') ? 'Can’t be blank' : message) : null;
50
+
56
51
  export const TableValueEditor = ({
57
52
  model,
58
53
  modals,
59
54
  schema,
60
- onEnter,
61
55
  onFocus,
62
56
  onSave,
63
- onQuery,
57
+ onCreate,
58
+ client,
64
59
  __gridScope,
65
60
  }: GridScopedProps<TableCellEditorProps>) => {
66
61
  const { editing } = useGridContext('TableValueEditor', __gridScope);
@@ -79,8 +74,9 @@ export const TableValueEditor = ({
79
74
 
80
75
  if (
81
76
  fieldProjection?.props.type === TypeEnum.Array ||
82
- fieldProjection?.props.format === FormatEnum.SingleSelect
83
- // TODO(thure): Support `FormatEnum.MultiSelect`
77
+ fieldProjection?.props.format === Format.TypeFormat.SingleSelect ||
78
+ fieldProjection?.props.format === Format.TypeFormat.Ref
79
+ // TODO(thure): Support `Format.TypeFormat.MultiSelect`
84
80
  ) {
85
81
  return (
86
82
  <FormCellEditor
@@ -89,40 +85,30 @@ export const TableValueEditor = ({
89
85
  schema={schema}
90
86
  __gridScope={__gridScope}
91
87
  onSave={onSave}
88
+ onCreate={onCreate}
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
- const editorSlots = { scroller: { className: '!plb-[--dx-grid-cell-editor-padding-block]' } };
99
+ const editorSlots = { scroll: { className: '!plb-[--dx-grid-cell-editor-padding-block]' } };
111
100
 
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
- const { id: gridId, editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
108
+ const { editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
122
109
  const suppressNextBlur = useRef(false);
123
- const { themeMode } = useThemeContext();
124
- const [_validationError, setValidationError] = useState<string | null>(null);
125
- const [_validationVariant, setValidationVariant] = useState<'error' | 'warning'>('error');
110
+ const [validationError, setValidationError] = useState<string | null>(null);
111
+ const [validationVariant, setValidationVariant] = useState<'error' | 'warning'>('error');
126
112
 
127
113
  const fieldProjection = useMemo<FieldProjection | undefined>(() => {
128
114
  if (!model || !editing) {
@@ -136,38 +122,6 @@ export const TableCellEditor = ({
136
122
  return fieldProjection;
137
123
  }, [model, editing]);
138
124
 
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
125
  const handleEnter = useCallback(
172
126
  async (value: any) => {
173
127
  if (!model || !editing) {
@@ -180,28 +134,34 @@ 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?.();
187
140
  } else {
188
- setValidationError(validationResult.error);
141
+ setValidationError(adaptValidationMessage(validationResult.error));
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';