@dxos/react-ui-table 0.8.3 → 0.8.4-main.28f8d3d

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-L2LSB6KV.mjs +18 -0
  2. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-ZAOSIG3L.mjs +1814 -0
  4. package/dist/lib/browser/chunk-ZAOSIG3L.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +728 -2104
  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 +123 -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-NXZ4CJYX.mjs +20 -0
  12. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-ZOFFI3JJ.mjs +1815 -0
  14. package/dist/lib/node-esm/chunk-ZOFFI3JJ.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +728 -2104
  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 +124 -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 +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 +4 -2
  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 +1 -1
  28. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +1 -1
  30. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/{relations.stories.d.ts → Relations.stories.d.ts} +2 -2
  32. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
  33. package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
  34. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  35. package/dist/types/src/components/Table/Table.d.ts +1 -0
  36. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  37. package/dist/types/src/components/Table/Table.stories.d.ts +1 -1
  38. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +2 -1
  40. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  41. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +2 -1
  42. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  43. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +8 -7
  44. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  45. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +1 -1
  46. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  47. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +1 -1
  48. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  49. package/dist/types/src/components/testing.d.ts +3 -40
  50. package/dist/types/src/components/testing.d.ts.map +1 -1
  51. package/dist/types/src/hooks/index.d.ts +1 -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/useTableModel.d.ts +8 -7
  56. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  57. package/dist/types/src/index.d.ts +1 -2
  58. package/dist/types/src/index.d.ts.map +1 -1
  59. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  60. package/dist/types/src/model/selection-model.d.ts +2 -1
  61. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  62. package/dist/types/src/model/table-model.d.ts +58 -21
  63. package/dist/types/src/model/table-model.d.ts.map +1 -1
  64. package/dist/types/src/model/table-presentation.d.ts +10 -1
  65. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  66. package/dist/types/src/model/table-sorting.d.ts +4 -4
  67. package/dist/types/src/model/table-sorting.d.ts.map +1 -1
  68. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  69. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  70. package/dist/types/src/testing/index.d.ts +2 -0
  71. package/dist/types/src/testing/index.d.ts.map +1 -0
  72. package/dist/types/src/testing/useTestTableModel.d.ts +41 -0
  73. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -0
  74. package/dist/types/src/translations.d.ts +5 -6
  75. package/dist/types/src/translations.d.ts.map +1 -1
  76. package/dist/types/src/types/index.d.ts +1 -1
  77. package/dist/types/src/types/index.d.ts.map +1 -1
  78. package/dist/types/src/types/schema.d.ts +8 -0
  79. package/dist/types/src/types/schema.d.ts.map +1 -0
  80. package/dist/types/src/util/dynamic-table.d.ts +3 -4
  81. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  82. package/dist/types/src/util/table-buttons.d.ts +15 -1
  83. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  84. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  85. package/dist/types/src/util/table.d.ts +9 -11
  86. package/dist/types/src/util/table.d.ts.map +1 -1
  87. package/dist/types/tsconfig.tsbuildinfo +1 -1
  88. package/package.json +52 -40
  89. package/src/components/Table/ColumnActionsMenu.tsx +1 -1
  90. package/src/components/Table/ColumnSettings.tsx +15 -11
  91. package/src/components/Table/CreateRefPanel.tsx +6 -4
  92. package/src/components/Table/DynamicTable.stories.tsx +4 -3
  93. package/src/components/Table/DynamicTable.tsx +17 -10
  94. package/src/components/Table/{relations.stories.tsx → Relations.stories.tsx} +37 -37
  95. package/src/components/Table/RowActionsMenu.tsx +3 -3
  96. package/src/components/Table/Table.stories.tsx +109 -148
  97. package/src/components/Table/Table.tsx +95 -66
  98. package/src/components/TableCellEditor/CellValidationMessage.tsx +7 -2
  99. package/src/components/TableCellEditor/FormCellEditor.tsx +30 -21
  100. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +27 -30
  101. package/src/components/TableCellEditor/TableCellEditor.tsx +88 -34
  102. package/src/components/TableCellEditor/extension.ts +2 -2
  103. package/src/components/TableToolbar/TableToolbar.tsx +2 -2
  104. package/src/components/testing.ts +7 -24
  105. package/src/hooks/index.ts +1 -0
  106. package/src/hooks/useAddRow.ts +36 -0
  107. package/src/hooks/useTableModel.ts +21 -17
  108. package/src/index.ts +1 -2
  109. package/src/model/modal-controller.ts +1 -1
  110. package/src/model/selection-model.ts +6 -3
  111. package/src/model/table-model.test.ts +8 -8
  112. package/src/model/table-model.ts +278 -86
  113. package/src/model/table-presentation.test.ts +8 -8
  114. package/src/model/table-presentation.ts +240 -135
  115. package/src/model/table-sorting.ts +11 -11
  116. package/src/playwright/playwright.config.ts +17 -0
  117. package/src/playwright/smoke.spec.ts +113 -20
  118. package/src/testing/index.ts +5 -0
  119. package/src/testing/useTestTableModel.ts +123 -0
  120. package/src/translations.ts +7 -5
  121. package/src/types/index.ts +2 -2
  122. package/src/types/schema.ts +16 -0
  123. package/src/util/dynamic-table.ts +20 -18
  124. package/src/util/schema.ts +1 -1
  125. package/src/util/table-buttons.ts +26 -2
  126. package/src/util/table-controls.ts +4 -1
  127. package/src/util/table.ts +37 -111
  128. package/dist/lib/browser/chunk-TL7CKQQV.mjs +0 -27
  129. package/dist/lib/browser/chunk-TL7CKQQV.mjs.map +0 -7
  130. package/dist/lib/node/chunk-O2VPBNS5.cjs +0 -50
  131. package/dist/lib/node/chunk-O2VPBNS5.cjs.map +0 -7
  132. package/dist/lib/node/index.cjs +0 -2739
  133. package/dist/lib/node/index.cjs.map +0 -7
  134. package/dist/lib/node/meta.json +0 -1
  135. package/dist/lib/node/types/index.cjs +0 -31
  136. package/dist/lib/node/types/index.cjs.map +0 -7
  137. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs +0 -29
  138. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs.map +0 -7
  139. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  140. package/dist/types/src/types/table.d.ts +0 -56
  141. package/dist/types/src/types/table.d.ts.map +0 -1
  142. package/src/playwright/playwright.config.cts +0 -18
  143. package/src/types/table.ts +0 -27
@@ -4,14 +4,16 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { useGridContext, type GridScopedProps } from '@dxos/react-ui-grid';
7
+ import { type GridScopedProps, useGridContext } from '@dxos/react-ui-grid';
8
8
 
9
9
  export type CellValidationMessageProps = {
10
10
  validationError: string | null;
11
+ variant?: 'error' | 'warning';
11
12
  };
12
13
 
13
14
  export const CellValidationMessage = ({
14
15
  validationError,
16
+ variant = 'error',
15
17
  __gridScope,
16
18
  }: GridScopedProps<CellValidationMessageProps>) => {
17
19
  const { editing, editBox: box } = useGridContext('GridSheetCellEditor', __gridScope);
@@ -20,10 +22,13 @@ export const CellValidationMessage = ({
20
22
  return null;
21
23
  }
22
24
 
25
+ const bgClass = variant === 'error' ? 'bg-errorSurface' : 'bg-warningSurface';
26
+ const textClass = variant === 'error' ? 'text-errorSurfaceText' : 'text-warningSurfaceText';
27
+
23
28
  return (
24
29
  <div
25
30
  role='none'
26
- className='absolute bg-errorSurface text-errorSurfaceText rounded-bs-sm text-xs p-1'
31
+ className={`absolute ${bgClass} ${textClass} rounded-bs-sm text-xs p-1`}
27
32
  style={{
28
33
  ...{ '--dx-gridCellWidth': `${box?.inlineSize ?? 200}px` },
29
34
  zIndex: 10,
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { type Schema } from 'effect';
6
- import React, { useCallback, useEffect, useMemo, useRef } from 'react';
6
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
7
7
 
8
8
  import { getSnapshot } from '@dxos/echo-schema';
9
9
  import { invariant } from '@dxos/invariant';
@@ -20,18 +20,26 @@ type FormCellEditorProps = {
20
20
  fieldProjection: FieldProjection;
21
21
  model?: TableModel;
22
22
  schema?: Schema.Schema.AnyNoContext;
23
+ onSave?: () => void;
23
24
  __gridScope: any;
24
25
  };
25
26
 
26
- export const FormCellEditor = ({ fieldProjection, model, schema, __gridScope }: FormCellEditorProps) => {
27
- const { editing, setEditing } = useGridContext('ArrayEditor', __gridScope);
28
- const cellRef = useRef<HTMLButtonElement | null>(null);
27
+ export const FormCellEditor = ({ fieldProjection, model, schema, onSave, __gridScope }: FormCellEditorProps) => {
28
+ const { editing: contextEditing, setEditing } = useGridContext('ArrayEditor', __gridScope);
29
+ const [editing, setLocalEditing] = useState(false);
30
+ const anchorRef = useRef<HTMLButtonElement | null>(null);
29
31
 
30
32
  useEffect(() => {
31
- if (editing && editing.cellElement) {
32
- cellRef.current = editing.cellElement as HTMLButtonElement;
33
+ if (contextEditing && contextEditing.cellElement) {
34
+ anchorRef.current = (contextEditing.cellElement as HTMLElement).querySelector(
35
+ '.dx-grid__cell__content',
36
+ ) as HTMLButtonElement;
37
+ setLocalEditing(true);
38
+ } else {
39
+ anchorRef.current = null;
40
+ setLocalEditing(false);
33
41
  }
34
- }, [editing?.cellElement]);
42
+ }, [contextEditing]);
35
43
 
36
44
  const narrowedSchema = useMemo(() => {
37
45
  if (!schema) {
@@ -41,8 +49,8 @@ export const FormCellEditor = ({ fieldProjection, model, schema, __gridScope }:
41
49
  }, [JSON.stringify(schema), fieldProjection.field.path]); // TODO(burdon): Avoid stringify.
42
50
 
43
51
  const originalRow = useMemo(() => {
44
- if (model && editing) {
45
- const cell = parseCellIndex(editing.index);
52
+ if (model && contextEditing) {
53
+ const cell = parseCellIndex(contextEditing.index);
46
54
  const row = model.getRowAt(cell.row);
47
55
  invariant(row);
48
56
 
@@ -50,7 +58,7 @@ export const FormCellEditor = ({ fieldProjection, model, schema, __gridScope }:
50
58
  }
51
59
 
52
60
  return undefined;
53
- }, [model, editing]);
61
+ }, [model, contextEditing]);
54
62
 
55
63
  const formValues = useMemo(() => {
56
64
  if (originalRow) {
@@ -65,27 +73,28 @@ export const FormCellEditor = ({ fieldProjection, model, schema, __gridScope }:
65
73
  const value = getDeep(values, [path]);
66
74
  setDeep(originalRow, [path], value);
67
75
  setEditing(null);
76
+ setLocalEditing(false);
77
+ onSave?.();
68
78
  },
69
79
  [fieldProjection.field.path, originalRow],
70
80
  );
71
81
 
72
- const handleOpenChange = useCallback(
73
- (nextOpen: boolean) => {
74
- if (nextOpen === false) {
75
- setEditing(null);
76
- }
77
- },
78
- [setEditing],
79
- );
82
+ const handleOpenChange = useCallback((nextOpen: boolean) => {
83
+ if (nextOpen === false) {
84
+ setEditing(null);
85
+ onSave?.();
86
+ }
87
+ setLocalEditing(nextOpen);
88
+ }, []);
80
89
 
81
90
  if (!editing) {
82
91
  return null;
83
92
  }
84
93
 
85
94
  return (
86
- <Popover.Root open={editing !== null} onOpenChange={handleOpenChange}>
87
- <Popover.VirtualTrigger virtualRef={cellRef} />
88
- <Popover.Content tabIndex={-1} classNames='popover-card-width'>
95
+ <Popover.Root open={editing} onOpenChange={handleOpenChange}>
96
+ <Popover.VirtualTrigger virtualRef={anchorRef} />
97
+ <Popover.Content tabIndex={-1} classNames='popover-card-width density-fine'>
89
98
  <Popover.Arrow />
90
99
  <Popover.Viewport>
91
100
  <Form values={formValues} schema={narrowedSchema as any} onSave={handleSave} />
@@ -4,25 +4,26 @@
4
4
 
5
5
  import '@dxos-theme';
6
6
 
7
- import { type Meta, type StoryObj } from '@storybook/react';
7
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
8
8
  import React, { useEffect, useMemo, useState } from 'react';
9
9
 
10
10
  import { Obj } from '@dxos/echo';
11
11
  import { type EchoSchema, isMutable } from '@dxos/echo-schema';
12
12
  import { invariant } from '@dxos/invariant';
13
13
  import { faker } from '@dxos/random';
14
- import { Filter, useQuery, live } from '@dxos/react-client/echo';
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 { ViewProjection, ViewType } from '@dxos/schema';
16
+ import { Grid, type GridEditing, defaultRowSize } from '@dxos/react-ui-grid';
17
+ import { DataType, createDefaultSchema } 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
- import translations from '../../translations';
24
- import { TableType } from '../../types';
25
- import { initializeTable } from '../../util';
22
+ import { translations } from '../../translations';
23
+ import { TableView } from '../../types';
24
+ import { createTable } from '../../util';
25
+
26
+ import { TableCellEditor, type TableCellEditorProps } from './TableCellEditor';
26
27
 
27
28
  type StoryProps = {
28
29
  editing: GridEditing;
@@ -32,23 +33,16 @@ const DefaultStory = ({ editing }: StoryProps) => {
32
33
  const { space } = useClientProvider();
33
34
  invariant(space);
34
35
 
35
- const tables = useQuery(space, Filter.type(TableType));
36
- const [table, setTable] = useState<TableType>();
36
+ const views = useQuery(space, Filter.type(DataType.View));
37
+ const [view, setView] = useState<DataType.View>();
37
38
  const [schema, setSchema] = useState<EchoSchema>();
38
39
  useEffect(() => {
39
- if (space && tables.length && !table) {
40
- const table = tables[0];
41
- invariant(table.view);
42
- setTable(table);
43
- setSchema(space.db.schemaRegistry.getSchema(table.view.target!.query.typename!));
44
- }
45
- }, [space, tables]);
46
-
47
- const projection = useMemo(() => {
48
- if (schema && table?.view) {
49
- return new ViewProjection(schema.jsonSchema, table.view.target!);
40
+ if (space && views.length && !view) {
41
+ const view = views[0];
42
+ setView(view);
43
+ setSchema(space.db.schemaRegistry.getSchema(view.query.typename!));
50
44
  }
51
- }, [schema, table?.view]);
45
+ }, [space, views, view]);
52
46
 
53
47
  const features: Partial<TableFeatures> = useMemo(
54
48
  () => ({
@@ -59,7 +53,7 @@ const DefaultStory = ({ editing }: StoryProps) => {
59
53
  [schema],
60
54
  );
61
55
 
62
- const model = useTableModel({ table, projection, features });
56
+ const model = useTableModel({ view, schema: schema?.jsonSchema, features });
63
57
 
64
58
  const handleQuery: TableCellEditorProps['onQuery'] = async ({ field }) => {
65
59
  // TODO(dmaretskyi): If no schema query nothing
@@ -73,7 +67,7 @@ const DefaultStory = ({ editing }: StoryProps) => {
73
67
  });
74
68
  };
75
69
 
76
- if (!model || !schema || !table) {
70
+ if (!model || !schema || !view) {
77
71
  return <div />;
78
72
  }
79
73
 
@@ -87,22 +81,25 @@ const DefaultStory = ({ editing }: StoryProps) => {
87
81
  };
88
82
 
89
83
  const meta: Meta<StoryProps> = {
90
- title: 'plugins/plugin-table/TableCellEditor',
84
+ title: 'ui/react-ui-table/TableCellEditor',
91
85
  component: DefaultStory,
92
86
  render: DefaultStory,
93
87
  parameters: { translations, layout: 'centered' },
94
88
  decorators: [
95
89
  withClientProvider({
96
- types: [TableType, ViewType],
90
+ types: [DataType.View, TableView],
97
91
  createIdentity: true,
98
92
  createSpace: true,
99
93
  onSpaceCreated: async ({ client, space }) => {
100
- const table = space.db.add(Obj.make(TableType, {}));
101
- const schema = await initializeTable({ client, space, table });
94
+ const schema = createDefaultSchema();
95
+ const { view } = await createTable({ client, space, typename: schema.typename });
96
+ space.db.add(view);
102
97
  Array.from({ length: 10 }).forEach(() => {
103
98
  space.db.add(
104
- live(schema, {
105
- name: faker.person.fullName(),
99
+ Obj.make(schema, {
100
+ title: faker.person.fullName(),
101
+ status: faker.helpers.arrayElement(['todo', 'in-progress', 'done'] as const),
102
+ description: faker.lorem.sentence(),
106
103
  }),
107
104
  );
108
105
  });
@@ -4,33 +4,33 @@
4
4
 
5
5
  import { type Completion } from '@codemirror/autocomplete';
6
6
  import { type Schema } from 'effect/Schema';
7
- import React, { useCallback, useMemo, useRef, useState } from 'react';
7
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
8
8
 
9
9
  import { FormatEnum, TypeEnum } from '@dxos/echo-schema';
10
10
  import { invariant } from '@dxos/invariant';
11
- import { type DxGrid } from '@dxos/lit-grid';
11
+ import { type DxGridAxis, type DxGridPosition } from '@dxos/lit-grid';
12
12
  import { useThemeContext } from '@dxos/react-ui';
13
13
  import { createMarkdownExtensions } from '@dxos/react-ui-editor';
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
+ cellQuery,
22
+ editorKeys,
23
+ parseCellIndex,
24
+ useGridContext,
26
25
  } from '@dxos/react-ui-grid';
27
- import { tagPickerExtension, createLinks } from '@dxos/react-ui-tag-picker';
26
+ import { createLinks, tagPicker } from '@dxos/react-ui-tag-picker';
28
27
  import { type FieldProjection } from '@dxos/schema';
29
28
 
29
+ import { type ModalController, type TableModel } from '../../model';
30
+
30
31
  import { CellValidationMessage } from './CellValidationMessage';
31
- import { FormCellEditor } from './FormCellEditor';
32
32
  import { completion } from './extension';
33
- import { type TableModel, type ModalController } from '../../model';
33
+ import { FormCellEditor } from './FormCellEditor';
34
34
 
35
35
  const newValue = Symbol.for('newValue');
36
36
 
@@ -47,8 +47,9 @@ export type TableCellEditorProps = {
47
47
  model?: TableModel;
48
48
  modals?: ModalController;
49
49
  schema?: Schema.AnyNoContext;
50
- onEnter?: (cell: DxGridPlanePosition) => void;
51
- onFocus?: DxGrid['refocus'];
50
+ onEnter?: (cell: DxGridPosition) => void;
51
+ onFocus?: (axis?: DxGridAxis, delta?: -1 | 0 | 1, cell?: DxGridPosition) => void;
52
+ onSave?: () => void;
52
53
  onQuery?: (field: FieldProjection, text: string) => Promise<QueryResult[]>;
53
54
  };
54
55
 
@@ -58,6 +59,7 @@ export const TableValueEditor = ({
58
59
  schema,
59
60
  onEnter,
60
61
  onFocus,
62
+ onSave,
61
63
  onQuery,
62
64
  __gridScope,
63
65
  }: GridScopedProps<TableCellEditorProps>) => {
@@ -69,14 +71,26 @@ export const TableValueEditor = ({
69
71
  }
70
72
 
71
73
  const { col } = parseCellIndex(editing.index);
72
- const field = model.projection.view.fields[col];
74
+ const field = model.projection.fields[col];
73
75
  const fieldProjection = model.projection.getFieldProjection(field.id);
74
76
  invariant(fieldProjection);
75
77
  return fieldProjection;
76
78
  }, [model, editing]);
77
79
 
78
- if (fieldProjection?.props.type === TypeEnum.Array) {
79
- return <FormCellEditor fieldProjection={fieldProjection} model={model} schema={schema} __gridScope={__gridScope} />;
80
+ if (
81
+ fieldProjection?.props.type === TypeEnum.Array ||
82
+ fieldProjection?.props.format === FormatEnum.SingleSelect
83
+ // TODO(thure): Support `FormatEnum.MultiSelect`
84
+ ) {
85
+ return (
86
+ <FormCellEditor
87
+ fieldProjection={fieldProjection}
88
+ model={model}
89
+ schema={schema}
90
+ __gridScope={__gridScope}
91
+ onSave={onSave}
92
+ />
93
+ );
80
94
  }
81
95
 
82
96
  // For all other types, use the existing cell editor
@@ -87,23 +101,28 @@ export const TableValueEditor = ({
87
101
  onEnter={onEnter}
88
102
  onFocus={onFocus}
89
103
  onQuery={onQuery}
104
+ onSave={onSave}
90
105
  __gridScope={__gridScope}
91
106
  />
92
107
  );
93
108
  };
94
109
 
110
+ const editorSlots = { scroller: { className: '!plb-[--dx-grid-cell-editor-padding-block]' } };
111
+
95
112
  export const TableCellEditor = ({
96
113
  model,
97
114
  modals,
98
115
  onEnter,
99
116
  onFocus,
100
117
  onQuery,
118
+ onSave,
101
119
  __gridScope,
102
120
  }: GridScopedProps<TableCellEditorProps>) => {
103
121
  const { id: gridId, editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
104
122
  const suppressNextBlur = useRef(false);
105
123
  const { themeMode } = useThemeContext();
106
- const [_validationError, setValidationError] = useState<string | null>(null);
124
+ const [validationError, setValidationError] = useState<string | null>(null);
125
+ const [validationVariant, setValidationVariant] = useState<'error' | 'warning'>('error');
107
126
 
108
127
  const fieldProjection = useMemo<FieldProjection | undefined>(() => {
109
128
  if (!model || !editing) {
@@ -111,12 +130,44 @@ export const TableCellEditor = ({
111
130
  }
112
131
 
113
132
  const { col } = parseCellIndex(editing.index);
114
- const field = model.projection.view.fields[col];
133
+ const field = model.projection.fields[col];
115
134
  const fieldProjection = model.projection.getFieldProjection(field.id);
116
135
  invariant(fieldProjection);
117
136
  return fieldProjection;
118
137
  }, [model, editing]);
119
138
 
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
+
120
171
  const handleEnter = useCallback(
121
172
  async (value: any) => {
122
173
  if (!model || !editing) {
@@ -124,19 +175,18 @@ export const TableCellEditor = ({
124
175
  }
125
176
 
126
177
  const cell = parseCellIndex(editing.index);
178
+ const validationResult = await model.validateCellData(cell, value);
127
179
 
128
- // Validate the value
129
- const result = await model.validateCellData(cell, value);
130
-
131
- if (result.valid) {
180
+ if (validationResult.valid) {
132
181
  setValidationError(null);
133
182
  model.setCellData(cell, value);
134
183
  onEnter?.(cell);
135
184
  onFocus?.();
136
185
  setEditing(null);
186
+ onSave?.();
137
187
  } else {
138
- setValidationError(result.error);
139
- // Editor stays open on validation failure
188
+ setValidationError(validationResult.error);
189
+ setValidationVariant('error');
140
190
  }
141
191
  },
142
192
  [model, editing, onEnter, onFocus, setEditing],
@@ -172,22 +222,25 @@ export const TableCellEditor = ({
172
222
  setValidationError(null);
173
223
  model.setCellData(cell, value);
174
224
  setEditing(null);
225
+ onSave?.();
175
226
  onEnter?.(cell);
176
227
  if (event && onFocus) {
177
- onFocus(determineNavigationAxis(event), determineNavigationDelta(event));
228
+ onFocus(determineNavigationAxis(event), determineNavigationDelta(event), cell);
178
229
  }
179
230
  } else {
180
231
  setValidationError(result.error);
232
+ setValidationVariant('error');
181
233
  }
182
234
  } else {
183
235
  setValidationError(null);
184
236
  setEditing(null);
237
+ onSave?.();
185
238
  if (event && onFocus) {
186
239
  onFocus(determineNavigationAxis(event), determineNavigationDelta(event));
187
240
  }
188
241
  }
189
242
  },
190
- [model, editing, onFocus, onEnter, fieldProjection, setEditing],
243
+ [model, editing, onFocus, onEnter, fieldProjection, setEditing, onSave],
191
244
  );
192
245
 
193
246
  const extension = useMemo(() => {
@@ -214,9 +267,9 @@ export const TableCellEditor = ({
214
267
  const mode = format === FormatEnum.SingleSelect ? ('single-select' as const) : ('multi-select' as const);
215
268
 
216
269
  extension.push(
217
- tagPickerExtension({
270
+ tagPicker({
218
271
  mode,
219
- inGrid: true,
272
+ keymap: false,
220
273
  onSearch: (text, selectedIds) => {
221
274
  return options
222
275
  .filter(
@@ -288,7 +341,7 @@ export const TableCellEditor = ({
288
341
  if (model && editing) {
289
342
  const cell = parseCellIndex(editing.index);
290
343
  const { col } = cell;
291
- const field = model.projection.view.fields[col];
344
+ const field = model.projection.fields[col];
292
345
  const fieldProjection = model.projection.getFieldProjection(field.id);
293
346
 
294
347
  if (
@@ -300,9 +353,10 @@ export const TableCellEditor = ({
300
353
  if (value !== undefined) {
301
354
  const options = fieldProjection.props.options || [];
302
355
 
303
- if (fieldProjection.props.format === FormatEnum.MultiSelect && Array.isArray(value)) {
356
+ if (fieldProjection.props.format === FormatEnum.MultiSelect) {
304
357
  const tagItems = value
305
- .map((id) => {
358
+ .split(',')
359
+ .map((id: string) => {
306
360
  const option = options.find((o) => o.id === id);
307
361
  if (option) {
308
362
  return {
@@ -313,7 +367,7 @@ export const TableCellEditor = ({
313
367
  }
314
368
  return undefined;
315
369
  })
316
- .filter((item): item is { id: any; label: string; hue: any } => item !== undefined);
370
+ .filter((item: any): item is { id: any; label: string; hue: any } => item !== undefined);
317
371
 
318
372
  return createLinks(tagItems);
319
373
  } else {
@@ -341,8 +395,8 @@ export const TableCellEditor = ({
341
395
 
342
396
  return (
343
397
  <>
344
- <CellValidationMessage validationError={_validationError} __gridScope={__gridScope} />
345
- <GridCellEditor extension={extension} getCellContent={getCellContent} onBlur={handleBlur} />
398
+ <CellValidationMessage validationError={validationError} variant={validationVariant} __gridScope={__gridScope} />
399
+ <GridCellEditor extension={extension} getCellContent={getCellContent} onBlur={handleBlur} slots={editorSlots} />
346
400
  </>
347
401
  );
348
402
  };
@@ -3,10 +3,10 @@
3
3
  //
4
4
 
5
5
  import {
6
- autocompletion,
7
- pickedCompletion,
8
6
  type CompletionContext,
9
7
  type CompletionResult,
8
+ autocompletion,
9
+ pickedCompletion,
10
10
  } from '@codemirror/autocomplete';
11
11
  import { type Extension } from '@codemirror/state';
12
12
  import { EditorView } from '@codemirror/view';
@@ -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,13 +5,10 @@
5
5
  import { Schema } from 'effect';
6
6
  import { useEffect } from 'react';
7
7
 
8
- import { Obj, Ref } from '@dxos/echo';
9
- import { FormatEnum, ObjectId, setValue, toJsonSchema, TypedObject, TypeEnum } from '@dxos/echo-schema';
8
+ import { FormatEnum, ObjectId, TypeEnum, TypedObject, setValue } from '@dxos/echo-schema';
9
+ import { live } from '@dxos/live-object';
10
10
  import { faker } from '@dxos/random';
11
- import { live } from '@dxos/react-client/echo';
12
- import { createView, type ViewProjection } from '@dxos/schema';
13
-
14
- import { TableType } from '../types';
11
+ import { type ProjectionModel } from '@dxos/schema';
15
12
 
16
13
  export const TestSchema = TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
17
14
  id: ObjectId,
@@ -21,18 +18,6 @@ export const TestSchema = TypedObject({ typename: 'example.com/type/Test', versi
21
18
  netWorth: Schema.optional(Schema.Number),
22
19
  });
23
20
 
24
- export const createTable = (schema = TestSchema) => {
25
- return Obj.make(TableType, {
26
- view: Ref.make(
27
- createView({
28
- name: 'Test',
29
- typename: schema.typename,
30
- jsonSchema: toJsonSchema(schema),
31
- }),
32
- ),
33
- });
34
- };
35
-
36
21
  export const createItems = (n: number) => {
37
22
  const { data } = live({
38
23
  data: Array.from({ length: n }, () => ({
@@ -47,13 +32,13 @@ export const createItems = (n: number) => {
47
32
  };
48
33
 
49
34
  export type SimulatorProps = {
50
- table: TableType;
35
+ projection: ProjectionModel;
51
36
  items: any[];
52
37
  insertInterval?: number;
53
38
  updateInterval?: number;
54
39
  };
55
40
 
56
- export const useSimulator = ({ items, table, insertInterval, updateInterval }: SimulatorProps) => {
41
+ export const useSimulator = ({ items, projection, insertInterval, updateInterval }: SimulatorProps) => {
57
42
  useEffect(() => {
58
43
  if (!insertInterval) {
59
44
  return;
@@ -74,10 +59,8 @@ export const useSimulator = ({ items, table, insertInterval, updateInterval }: S
74
59
 
75
60
  const i = setInterval(() => {
76
61
  const rowIdx = Math.floor(Math.random() * items.length);
77
- const fields = table.view?.target?.fields ?? [];
62
+ const fields = projection.fields ?? [];
78
63
  const columnIdx = Math.floor(Math.random() * fields.length);
79
- // TODO(ZaymonFC): ... This is borked.
80
- const projection: ViewProjection = (table as any)._projection;
81
64
  const field = fields[columnIdx];
82
65
  const item = items[rowIdx];
83
66
 
@@ -115,5 +98,5 @@ export const useSimulator = ({ items, table, insertInterval, updateInterval }: S
115
98
  }, updateInterval);
116
99
 
117
100
  return () => clearInterval(i);
118
- }, [items, table.view?.target?.fields, updateInterval]);
101
+ }, [items, projection.fields, updateInterval]);
119
102
  };
@@ -3,3 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './useTableModel';
6
+ export * from './useAddRow';
@@ -0,0 +1,36 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Schema } from 'effect';
6
+ import { useCallback } from 'react';
7
+
8
+ import { type Space } from '@dxos/client/echo';
9
+ import { Obj } from '@dxos/echo';
10
+
11
+ export type UseAddRowParams = {
12
+ space?: Space;
13
+ schema?: Schema.Schema.AnyNoContext;
14
+ };
15
+
16
+ /**
17
+ * Hook that provides a safe row creation function that handles schema validation failures.
18
+ * Returns a callback that attempts to create an object and returns boolean success/failure.
19
+ * Can accept optional data to create objects with specific content (used for committing draft rows).
20
+ */
21
+ export const useAddRow = ({ space, schema }: UseAddRowParams) => {
22
+ return useCallback(
23
+ (data?: any) => {
24
+ if (space && schema) {
25
+ try {
26
+ space.db.add(Obj.make(schema, data ?? {}));
27
+ return true;
28
+ } catch (error) {
29
+ return false;
30
+ }
31
+ }
32
+ return false;
33
+ },
34
+ [space, schema],
35
+ );
36
+ };