@dxos/react-ui-table 0.10.0 → 0.11.0

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 (110) hide show
  1. package/dist/lib/chunk-types.mjs +65 -0
  2. package/dist/lib/chunk-types.mjs.map +1 -0
  3. package/dist/lib/chunk-useTableModel.mjs +1595 -0
  4. package/dist/lib/chunk-useTableModel.mjs.map +1 -0
  5. package/dist/lib/index.mjs +1133 -0
  6. package/dist/lib/index.mjs.map +1 -0
  7. package/dist/lib/testing.mjs +88 -0
  8. package/dist/lib/testing.mjs.map +1 -0
  9. package/dist/lib/translations.mjs +30 -0
  10. package/dist/lib/translations.mjs.map +1 -0
  11. package/dist/lib/types.mjs +2 -0
  12. package/dist/types/src/components/Table/DynamicTable.d.ts +4 -1
  13. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  14. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +3 -1
  15. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  16. package/dist/types/src/components/Table/Relations.stories.d.ts +1 -0
  17. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
  18. package/dist/types/src/components/Table/Table.d.ts +3 -9
  19. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  20. package/dist/types/src/components/Table/Table.stories.d.ts +2 -0
  21. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  22. package/dist/types/src/components/Table/TableContent.d.ts +1 -1
  23. package/dist/types/src/components/Table/TableContent.d.ts.map +1 -1
  24. package/dist/types/src/components/Table/TableContext.d.ts +10 -0
  25. package/dist/types/src/components/Table/TableContext.d.ts.map +1 -0
  26. package/dist/types/src/components/Table/TableToolbar.d.ts +1 -1
  27. package/dist/types/src/components/Table/TableToolbar.d.ts.map +1 -1
  28. package/dist/types/src/components/Table/index.d.ts +1 -0
  29. package/dist/types/src/components/Table/index.d.ts.map +1 -1
  30. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  31. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  32. package/dist/types/src/components/testing.d.ts +1 -1
  33. package/dist/types/src/components/testing.d.ts.map +1 -1
  34. package/dist/types/src/hooks/useProjectionModel.d.ts +1 -1
  35. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -1
  36. package/dist/types/src/hooks/useTableModel.d.ts +8 -1
  37. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  38. package/dist/types/src/model/modal-controller.d.ts +1 -1
  39. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  40. package/dist/types/src/model/selection-model.d.ts +1 -1
  41. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  42. package/dist/types/src/model/table-model.d.ts +42 -7
  43. package/dist/types/src/model/table-model.d.ts.map +1 -1
  44. package/dist/types/src/model/table-presentation.d.ts +20 -5
  45. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  46. package/dist/types/src/model/table-view-state.d.ts +5 -1
  47. package/dist/types/src/model/table-view-state.d.ts.map +1 -1
  48. package/dist/types/src/types/index.d.ts.map +1 -1
  49. package/dist/types/src/util/dynamic-table.d.ts +1 -1
  50. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  51. package/dist/types/src/util/table-controls.d.ts +5 -3
  52. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  53. package/dist/types/tsconfig.tsbuildinfo +1 -1
  54. package/package.json +48 -50
  55. package/src/components/Table/DynamicTable.stories.tsx +59 -1
  56. package/src/components/Table/DynamicTable.tsx +16 -8
  57. package/src/components/Table/Relations.stories.tsx +2 -2
  58. package/src/components/Table/Table.stories.tsx +76 -2
  59. package/src/components/Table/Table.tsx +3 -15
  60. package/src/components/Table/TableContent.tsx +6 -5
  61. package/src/components/Table/TableContext.ts +21 -0
  62. package/src/components/Table/index.ts +2 -0
  63. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +2 -3
  64. package/src/components/TableCellEditor/TableCellEditor.tsx +27 -22
  65. package/src/components/testing.ts +1 -1
  66. package/src/hooks/useProjectionModel.ts +1 -1
  67. package/src/hooks/useTableModel.ts +18 -6
  68. package/src/model/modal-controller.ts +1 -1
  69. package/src/model/selection-model.ts +1 -1
  70. package/src/model/table-model.test.ts +3 -3
  71. package/src/model/table-model.ts +148 -31
  72. package/src/model/table-presentation.test.ts +59 -1
  73. package/src/model/table-presentation.ts +171 -81
  74. package/src/model/table-view-state.ts +2 -2
  75. package/src/testing/useTestTableModel.ts +1 -1
  76. package/src/types/index.ts +4 -0
  77. package/src/util/dynamic-table.test.ts +1 -1
  78. package/src/util/dynamic-table.ts +1 -1
  79. package/src/util/table-buttons.ts +1 -1
  80. package/src/util/table-controls.ts +18 -10
  81. package/dist/lib/browser/chunk-J5LGTIGS.mjs +0 -10
  82. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +0 -7
  83. package/dist/lib/browser/chunk-K4J2CKTZ.mjs +0 -1751
  84. package/dist/lib/browser/chunk-K4J2CKTZ.mjs.map +0 -7
  85. package/dist/lib/browser/chunk-VV3P3ADH.mjs +0 -67
  86. package/dist/lib/browser/chunk-VV3P3ADH.mjs.map +0 -7
  87. package/dist/lib/browser/index.mjs +0 -1416
  88. package/dist/lib/browser/index.mjs.map +0 -7
  89. package/dist/lib/browser/meta.json +0 -1
  90. package/dist/lib/browser/testing/index.mjs +0 -115
  91. package/dist/lib/browser/testing/index.mjs.map +0 -7
  92. package/dist/lib/browser/translations.mjs +0 -39
  93. package/dist/lib/browser/translations.mjs.map +0 -7
  94. package/dist/lib/browser/types/index.mjs +0 -8
  95. package/dist/lib/browser/types/index.mjs.map +0 -7
  96. package/dist/lib/node-esm/chunk-E6V274ZW.mjs +0 -1753
  97. package/dist/lib/node-esm/chunk-E6V274ZW.mjs.map +0 -7
  98. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  99. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  100. package/dist/lib/node-esm/chunk-RPEWNOZZ.mjs +0 -68
  101. package/dist/lib/node-esm/chunk-RPEWNOZZ.mjs.map +0 -7
  102. package/dist/lib/node-esm/index.mjs +0 -1417
  103. package/dist/lib/node-esm/index.mjs.map +0 -7
  104. package/dist/lib/node-esm/meta.json +0 -1
  105. package/dist/lib/node-esm/testing/index.mjs +0 -116
  106. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  107. package/dist/lib/node-esm/translations.mjs +0 -40
  108. package/dist/lib/node-esm/translations.mjs.map +0 -7
  109. package/dist/lib/node-esm/types/index.mjs +0 -9
  110. package/dist/lib/node-esm/types/index.mjs.map +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-table",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "A table component.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -19,26 +19,22 @@
19
19
  ".": {
20
20
  "source": "./src/index.ts",
21
21
  "types": "./dist/types/src/index.d.ts",
22
- "browser": "./dist/lib/browser/index.mjs",
23
- "node": "./dist/lib/node-esm/index.mjs"
22
+ "import": "./dist/lib/index.mjs"
23
+ },
24
+ "./types": {
25
+ "source": "./src/types/index.ts",
26
+ "types": "./dist/types/src/types/index.d.ts",
27
+ "import": "./dist/lib/types.mjs"
24
28
  },
25
29
  "./testing": {
26
30
  "source": "./src/testing/index.ts",
27
31
  "types": "./dist/types/src/testing/index.d.ts",
28
- "browser": "./dist/lib/browser/testing/index.mjs",
29
- "node": "./dist/lib/node-esm/testing/index.mjs"
32
+ "import": "./dist/lib/testing.mjs"
30
33
  },
31
34
  "./translations": {
32
35
  "source": "./src/translations.ts",
33
36
  "types": "./dist/types/src/translations.d.ts",
34
- "browser": "./dist/lib/browser/translations.mjs",
35
- "node": "./dist/lib/node-esm/translations.mjs"
36
- },
37
- "./types": {
38
- "source": "./src/types/index.ts",
39
- "types": "./dist/types/src/types/index.d.ts",
40
- "browser": "./dist/lib/browser/types/index.mjs",
41
- "node": "./dist/lib/node-esm/types/index.mjs"
37
+ "import": "./dist/lib/translations.mjs"
42
38
  }
43
39
  },
44
40
  "types": "dist/types/src/index.d.ts",
@@ -47,9 +43,10 @@
47
43
  "src"
48
44
  ],
49
45
  "dependencies": {
50
- "@codemirror/autocomplete": "^6.20.2",
51
- "@codemirror/state": "^6.6.0",
52
- "@codemirror/view": "^6.43.0",
46
+ "@codemirror/autocomplete": "^6.20.3",
47
+ "@codemirror/state": "^6.7.1",
48
+ "@codemirror/view": "^6.43.6",
49
+ "@effect-atom/atom": "^0.5.3",
53
50
  "@effect-atom/atom-react": "^0.5.0",
54
51
  "@fluentui/react-tabster": "9.26.11",
55
52
  "@radix-ui/react-context": "1.1.1",
@@ -58,30 +55,31 @@
58
55
  "@radix-ui/react-use-controllable-state": "1.1.0",
59
56
  "date-fns": "^3.6.0",
60
57
  "lodash.orderby": "^4.6.0",
61
- "@dxos/context": "0.10.0",
62
- "@dxos/debug": "0.10.0",
63
- "@dxos/async": "0.10.0",
64
- "@dxos/echo": "0.10.0",
65
- "@dxos/effect": "0.10.0",
66
- "@dxos/lit-grid": "0.10.0",
67
- "@dxos/invariant": "0.10.0",
68
- "@dxos/random": "0.10.0",
69
- "@dxos/log": "0.10.0",
70
- "@dxos/react-client": "0.10.0",
71
- "@dxos/react-ui-components": "0.10.0",
72
- "@dxos/react-ui-form": "0.10.0",
73
- "@dxos/react-ui-editor": "0.10.0",
74
- "@dxos/react-ui-list": "0.10.0",
75
- "@dxos/react-ui-menu": "0.10.0",
76
- "@dxos/react-ui-search": "0.10.0",
77
- "@dxos/react-ui-grid": "0.10.0",
78
- "@dxos/react-ui-syntax-highlighter": "0.10.0",
79
- "@dxos/schema": "0.10.0",
80
- "@dxos/keys": "0.10.0",
81
- "@dxos/types": "0.10.0",
82
- "@dxos/util": "0.10.0",
83
- "@dxos/ui-types": "0.10.0",
84
- "@dxos/react-ui-attention": "0.10.0"
58
+ "@dxos/context": "0.11.0",
59
+ "@dxos/async": "0.11.0",
60
+ "@dxos/debug": "0.11.0",
61
+ "@dxos/echo": "0.11.0",
62
+ "@dxos/echo-react": "0.11.0",
63
+ "@dxos/effect": "0.11.0",
64
+ "@dxos/lit-grid": "0.11.0",
65
+ "@dxos/invariant": "0.11.0",
66
+ "@dxos/keys": "0.11.0",
67
+ "@dxos/log": "0.11.0",
68
+ "@dxos/random": "0.11.0",
69
+ "@dxos/react-client": "0.11.0",
70
+ "@dxos/react-ui-attention": "0.11.0",
71
+ "@dxos/react-ui-components": "0.11.0",
72
+ "@dxos/react-ui-editor": "0.11.0",
73
+ "@dxos/react-ui-form": "0.11.0",
74
+ "@dxos/react-ui-grid": "0.11.0",
75
+ "@dxos/react-ui-list": "0.11.0",
76
+ "@dxos/react-ui-menu": "0.11.0",
77
+ "@dxos/react-ui-search": "0.11.0",
78
+ "@dxos/react-ui-syntax-highlighter": "0.11.0",
79
+ "@dxos/schema": "0.11.0",
80
+ "@dxos/types": "0.11.0",
81
+ "@dxos/ui-types": "0.11.0",
82
+ "@dxos/util": "0.11.0"
85
83
  },
86
84
  "devDependencies": {
87
85
  "@effect/platform": "0.96.2",
@@ -92,14 +90,14 @@
92
90
  "fast-check": "^3.19.0",
93
91
  "react": "~19.2.7",
94
92
  "react-dom": "~19.2.7",
95
- "vite": "^8.0.16",
96
- "@dxos/random": "0.10.0",
97
- "@dxos/lit-ui": "0.10.0",
98
- "@dxos/react-client": "0.10.0",
99
- "@dxos/react-ui": "0.10.0",
100
- "@dxos/storybook-utils": "0.10.0",
101
- "@dxos/test-utils": "0.10.0",
102
- "@dxos/ui-theme": "0.10.0"
93
+ "vite": "^8.1.4",
94
+ "@dxos/lit-ui": "0.11.0",
95
+ "@dxos/random": "0.11.0",
96
+ "@dxos/react-ui": "0.11.0",
97
+ "@dxos/react-client": "0.11.0",
98
+ "@dxos/storybook-utils": "0.11.0",
99
+ "@dxos/test-utils": "0.11.0",
100
+ "@dxos/ui-theme": "0.11.0"
103
101
  },
104
102
  "peerDependencies": {
105
103
  "@effect-atom/atom-react": "^0.5.0",
@@ -107,8 +105,8 @@
107
105
  "effect": "3.21.4",
108
106
  "react": "~19.2.7",
109
107
  "react-dom": "~19.2.7",
110
- "@dxos/ui-theme": "0.10.0",
111
- "@dxos/react-ui": "0.10.0"
108
+ "@dxos/ui-theme": "^0.11.0",
109
+ "@dxos/react-ui": "^0.11.0"
112
110
  },
113
111
  "publishConfig": {
114
112
  "access": "public"
@@ -7,9 +7,9 @@ import type * as Types from 'effect/Types';
7
7
  import React, { useMemo, useState } from 'react';
8
8
 
9
9
  import { Filter, type JsonSchema, Obj, Type } from '@dxos/echo';
10
+ import { useQuery, useType } from '@dxos/echo-react';
10
11
  import { Format } from '@dxos/echo/Format';
11
12
  import { random } from '@dxos/random';
12
- import { useQuery, useType } from '@dxos/react-client/echo';
13
13
  import { useClientStory, withClientProvider } from '@dxos/react-client/testing';
14
14
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
15
15
  import { type SchemaPropertyDefinition } from '@dxos/schema';
@@ -52,6 +52,36 @@ const DynamicTableStory = () => {
52
52
  return <DynamicTable properties={properties} rows={rows} />;
53
53
  };
54
54
 
55
+ const useWidePropertiesAndObjects = () => {
56
+ const properties = useMemo<SchemaPropertyDefinition[]>(
57
+ () => [
58
+ { name: 'name', format: Format.TypeFormat.String },
59
+ { name: 'email', format: Format.TypeFormat.String },
60
+ { name: 'company', format: Format.TypeFormat.String },
61
+ { name: 'product', format: Format.TypeFormat.String },
62
+ { name: 'city', format: Format.TypeFormat.String },
63
+ { name: 'url', format: Format.TypeFormat.String },
64
+ { name: 'age', format: Format.TypeFormat.Number },
65
+ ],
66
+ [],
67
+ );
68
+
69
+ const [rows] = useState<any[]>(
70
+ Array.from({ length: 20 }, () => ({
71
+ id: random.string.uuid(),
72
+ name: random.person.fullName(),
73
+ email: random.internet.email(),
74
+ company: random.company.name(),
75
+ product: random.commerce.productName(),
76
+ city: random.geo.airport().name,
77
+ url: random.internet.url(),
78
+ age: random.number.int({ min: 18, max: 80 }),
79
+ })),
80
+ );
81
+
82
+ return { properties, rows };
83
+ };
84
+
55
85
  //
56
86
  // Story definitions.
57
87
  //
@@ -100,6 +130,34 @@ export const WithClickToSelect: StoryObj = {
100
130
  },
101
131
  };
102
132
 
133
+ export const WithPinnedColumns: StoryObj = {
134
+ render: () => {
135
+ const { properties, rows } = useWidePropertiesAndObjects();
136
+
137
+ // Pin the first data column (Name) so it stays visible while the rest scroll horizontally.
138
+ const features = useMemo<Partial<TableFeatures>>(
139
+ () => ({ selection: { enabled: true, mode: 'multiple' as const }, dataEditable: false, pinColumns: 1 }),
140
+ [],
141
+ );
142
+
143
+ return <DynamicTable properties={properties} rows={rows} features={features} />;
144
+ },
145
+ };
146
+
147
+ export const WithMultiplePinnedColumns: StoryObj = {
148
+ render: () => {
149
+ const { properties, rows } = useWidePropertiesAndObjects();
150
+
151
+ // Pin the first two data columns (Name, Email).
152
+ const features = useMemo<Partial<TableFeatures>>(
153
+ () => ({ selection: { enabled: false }, dataEditable: false, pinColumns: 2 }),
154
+ [],
155
+ );
156
+
157
+ return <DynamicTable properties={properties} rows={rows} features={features} />;
158
+ },
159
+ };
160
+
103
161
  export const WithJsonSchema: StoryObj = {
104
162
  render: () => {
105
163
  const schema = useMemo<Types.DeepMutable<JsonSchema.JsonSchema>>(
@@ -9,7 +9,6 @@ import React, { useCallback, useContext, useEffect, useMemo, useRef, useState }
9
9
  import { type JsonSchema, type Type } from '@dxos/echo';
10
10
  import { type ThemedClassName, useDefaultValue } from '@dxos/react-ui';
11
11
  import { type ProjectionModel } from '@dxos/schema';
12
- import { mx } from '@dxos/ui-theme';
13
12
 
14
13
  import { useTableModel } from '../../hooks';
15
14
  import { type TableFeatures, TablePresentation, type TableRowAction } from '../../model';
@@ -25,8 +24,11 @@ export type DynamicTableProps<T extends Type.AnyEntity = Type.AnyEntity> = Theme
25
24
  jsonSchema?: Types.DeepMutable<JsonSchema.JsonSchema>;
26
25
  features?: Partial<TableFeatures>;
27
26
  rowActions?: TableRowAction[];
27
+ /** Rows selected when the table mounts; see `useTableModel`'s `selection`. */
28
+ selection?: readonly string[];
28
29
  onRowClick?: (row: any) => void;
29
30
  onRowAction?: (actionId: string, datum: any) => void;
31
+ onSelectionChanged?: (selection: string[]) => void;
30
32
  }>;
31
33
 
32
34
  /**
@@ -42,8 +44,10 @@ export const DynamicTable = <T extends Type.AnyEntity = Type.AnyEntity>({
42
44
  properties,
43
45
  jsonSchema: jsonSchemaProp,
44
46
  rowActions,
47
+ selection,
45
48
  onRowClick,
46
49
  onRowAction,
50
+ onSelectionChanged,
47
51
  ...props
48
52
  }: DynamicTableProps<T>) => {
49
53
  const registry = useContext(RegistryContext);
@@ -83,9 +87,11 @@ export const DynamicTable = <T extends Type.AnyEntity = Type.AnyEntity>({
83
87
  projection: dynamicTable?.projection,
84
88
  features,
85
89
  rowActions,
90
+ selection,
86
91
  onCellUpdate: handleCellUpdate,
87
92
  onRowOrderChange: handleRowOrderChange,
88
93
  onRowAction,
94
+ onSelectionChanged,
89
95
  });
90
96
 
91
97
  const presentation = useMemo(() => {
@@ -95,12 +101,14 @@ export const DynamicTable = <T extends Type.AnyEntity = Type.AnyEntity>({
95
101
  }, [registry, model]);
96
102
 
97
103
  return (
98
- <div className={mx('dx-expander grid', classNames)}>
99
- <div className='grid min-h-0 overflow-hidden'>
100
- <Table.Root ref={tableRef}>
101
- <Table.Content model={model} presentation={presentation} ignoreAttention onRowClick={onRowClick} />
102
- </Table.Root>
103
- </div>
104
- </div>
104
+ <Table.Root ref={tableRef}>
105
+ <Table.Content
106
+ classNames={classNames}
107
+ model={model}
108
+ presentation={presentation}
109
+ ignoreAttention
110
+ onRowClick={onRowClick}
111
+ />
112
+ </Table.Root>
105
113
  );
106
114
  };
@@ -18,8 +18,8 @@ import { withLayout, withTheme } from '@dxos/react-ui/testing';
18
18
  import { ViewModel } from '@dxos/schema';
19
19
  import { type ValueGenerator, createAsyncGenerator } from '@dxos/schema/testing';
20
20
  import { withRegistry } from '@dxos/storybook-utils';
21
- import { Organization, Person } from '@dxos/types';
22
21
  import '@dxos/lit-ui/dx-tag-picker.pcss';
22
+ import { Organization, Person } from '@dxos/types';
23
23
 
24
24
  import { translations } from '#translations';
25
25
 
@@ -40,7 +40,7 @@ const useTestModel = <S extends Type.AnyObj>(schema: S, count: number) => {
40
40
  const { space } = useClientStory();
41
41
  const [object, setObject] = useState<Table.Table>();
42
42
 
43
- const features = useMemo<TableFeatures>(
43
+ const features = useMemo<Partial<TableFeatures>>(
44
44
  () => ({ schemaEditable: false, dataEditable: true, selection: { enabled: false } }),
45
45
  [],
46
46
  );
@@ -7,13 +7,14 @@ import * as Schema from 'effect/Schema';
7
7
  import React, { useCallback } from 'react';
8
8
  import { expect, userEvent, waitFor, within } from 'storybook/test';
9
9
 
10
- import { Annotation, type Database, DXN, Format, Obj, type QueryAST, Ref, Type, View } from '@dxos/echo';
10
+ import { Annotation, type Database, DXN, Filter, Format, Obj, type QueryAST, Ref, Type, View } from '@dxos/echo';
11
+ import { useQuery } from '@dxos/echo-react';
11
12
  import { type Mutable, PropertyMetaAnnotationId } from '@dxos/echo/internal';
12
13
  import { invariant } from '@dxos/invariant';
13
14
  import { random } from '@dxos/random';
14
15
  import { PublicKey } from '@dxos/react-client';
15
16
  import { withClientProvider } from '@dxos/react-client/testing';
16
- import { Panel, ScrollArea } from '@dxos/react-ui';
17
+ import { Panel, ScrollArea, Toolbar } from '@dxos/react-ui';
17
18
  import { ViewEditor } from '@dxos/react-ui-form';
18
19
  import { translations as formTranslations } from '@dxos/react-ui-form/translations';
19
20
  import { JsonHighlighter } from '@dxos/react-ui-syntax-highlighter';
@@ -338,6 +339,79 @@ export const RequiredSchema: StoryObj = {
338
339
  },
339
340
  };
340
341
 
342
+ // Reproduces the "form → table" reactivity path: an ECHO object edited *outside* the grid (as the selected-objects
343
+ // companion form does via `Obj.update`) must refresh the corresponding cell. The table model subscribes to each
344
+ // visible row via `Obj.subscribe`, bumping a `cellUpdate` atom the grid re-renders on.
345
+ const ExternalMutationStory = () => {
346
+ const { schema, table, tableRef, model, presentation, db } = useTestTableModel<typeof TestSchema.Person>();
347
+ const objects = useQuery(db, schema ? Filter.type(schema) : Filter.nothing());
348
+ const target = objects.at(0);
349
+
350
+ const handleMutate = useCallback(() => {
351
+ if (!target) {
352
+ return;
353
+ }
354
+ Obj.update(target, (target) => {
355
+ target.name = 'Zoe Mutated';
356
+ });
357
+ }, [target]);
358
+
359
+ if (!schema || !table?.view.target) {
360
+ return <div />;
361
+ }
362
+
363
+ return (
364
+ <div className='flex flex-col h-full'>
365
+ <Toolbar.Root>
366
+ <Toolbar.Button onClick={handleMutate}>Mutate row externally</Toolbar.Button>
367
+ </Toolbar.Root>
368
+ <TableComponent.Root ref={tableRef}>
369
+ <Panel.Root>
370
+ <Panel.Content asChild>
371
+ <TableComponent.Content schema={schema} model={model} presentation={presentation} ignoreAttention />
372
+ </Panel.Content>
373
+ </Panel.Root>
374
+ </TableComponent.Root>
375
+ </div>
376
+ );
377
+ };
378
+
379
+ export const ExternalMutation: StoryObj = {
380
+ render: () => <ExternalMutationStory />,
381
+ decorators: [
382
+ withClientProvider({
383
+ types: [View.View, Table.Table, TestSchema.Person],
384
+ createIdentity: true,
385
+ createSpace: true,
386
+ onCreateSpace: async ({ space }) => {
387
+ const { view, jsonSchema } = await ViewModel.makeFromDatabase({
388
+ db: space.db,
389
+ typename: Type.getTypename(TestSchema.Person),
390
+ });
391
+ space.db.add(Table.make({ view, jsonSchema }));
392
+ space.db.add(Obj.make(TestSchema.Person, { name: 'Alice' }));
393
+ },
394
+ }),
395
+ ],
396
+ parameters: {
397
+ layout: 'fullscreen',
398
+ translations: [...translations, ...formTranslations],
399
+ },
400
+ play: async ({ canvasElement }) => {
401
+ const canvas = within(canvasElement);
402
+ const cellText = () => canvas.getByTestId('grid.0.0').querySelector('.dx-grid__cell__content')?.textContent;
403
+
404
+ // Wait for the client/space/table and the initial row to render (30s covers ECHO setup).
405
+ await waitFor(() => expect(cellText()).toBe('Alice'), { timeout: 30_000 });
406
+
407
+ // Mutate the object outside the grid (as an external/companion edit would).
408
+ await userEvent.click(await canvas.findByRole('button', { name: /mutate row externally/i }));
409
+
410
+ // The cell must reflect the external mutation.
411
+ await waitFor(() => expect(cellText()).toBe('Zoe Mutated'), { timeout: 5_000 });
412
+ },
413
+ };
414
+
341
415
  export const Tags: Meta<StoryArgs> = {
342
416
  title: 'ui/react-ui-table/Table',
343
417
  render: DefaultStory,
@@ -2,14 +2,14 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Atom } from '@effect-atom/atom-react';
6
- import { createContext } from '@radix-ui/react-context';
7
- import React, { type PropsWithChildren, type RefObject, forwardRef, useImperativeHandle, useRef } from 'react';
5
+ import { Atom } from '@effect-atom/atom';
6
+ import React, { type PropsWithChildren, forwardRef, useImperativeHandle, useRef } from 'react';
8
7
 
9
8
  import { type DxGridAxisMeta, type DxGridPosition } from '@dxos/react-ui-grid';
10
9
 
11
10
  import { type InsertRowResult } from '../../model';
12
11
  import { TableContent } from './TableContent';
12
+ import { TableContextProvider } from './TableContext';
13
13
  import { TableToolbar } from './TableToolbar';
14
14
 
15
15
  const columnDefault = { grid: { minSize: 80, maxSize: 640 } };
@@ -17,16 +17,6 @@ const rowDefault = { frozenRowsStart: { readonly: true, focusUnfurl: false } };
17
17
  const emptyColumnMeta = Atom.make<DxGridAxisMeta>({ grid: {} });
18
18
 
19
19
  //
20
- // Context
21
- //
22
-
23
- type TableContextValue = {
24
- /** Mutable ref populated by Content so Root can expose the controller. */
25
- controllerRef: RefObject<TableController>;
26
- };
27
-
28
- const [TableContextProvider, useTableContext] = createContext<TableContextValue>('Table');
29
-
30
20
  //
31
21
  // Controller
32
22
  //
@@ -73,5 +63,3 @@ export const Table = {
73
63
 
74
64
  export type { TableController, TableRootProps };
75
65
  export type { TableExportFormat } from './TableToolbar';
76
-
77
- export { useTableContext };
@@ -30,7 +30,8 @@ import { type OnCreateHandler, type TableCellEditorProps, TableValueEditor } fro
30
30
  import { ColumnActionsMenu } from './ColumnActionsMenu';
31
31
  import { ColumnSettings } from './ColumnSettings';
32
32
  import { RowActionsMenu } from './RowActionsMenu';
33
- import { TableController, useTableContext } from './Table';
33
+ import { type TableController } from './Table';
34
+ import { useTableContext } from './TableContext';
34
35
 
35
36
  const columnDefault = { grid: { minSize: 80, maxSize: 640 } };
36
37
  const rowDefault = { frozenRowsStart: { readonly: true, focusUnfurl: false } };
@@ -84,10 +85,10 @@ export const TableContent = composable<HTMLDivElement, TableContentProps>(
84
85
  return {
85
86
  frozenRowsStart: 1,
86
87
  frozenRowsEnd: Math.max(1, draftRowCount),
87
- frozenColsStart: model?.features.selection.enabled ? 1 : 0,
88
+ frozenColsStart: model?.frozenColsStart ?? 0,
88
89
  frozenColsEnd: noActionColumn ? 0 : 1,
89
90
  };
90
- }, [model, draftRowCount]);
91
+ }, [model, draftRowCount, columnCount]);
91
92
 
92
93
  const getCells = useCallback<NonNullable<GridContentProps['getCells']>>(
93
94
  (range: DxGridPlaneRange, plane: DxGridPlane) => presentation?.getCells(range, plane) ?? {},
@@ -232,7 +233,7 @@ export const TableContent = composable<HTMLDivElement, TableContentProps>(
232
233
  }
233
234
  case 'switch': {
234
235
  if (model) {
235
- model.updateCellData({ row: data.rowIndex, col: data.colIndex }, (value) => !value);
236
+ model.updateCellData({ row: data.rowIndex, col: data.colIndex, plane: data.plane }, (value) => !value);
236
237
  }
237
238
  break;
238
239
  }
@@ -341,7 +342,7 @@ export const TableContent = composable<HTMLDivElement, TableContentProps>(
341
342
  (event) => {
342
343
  if (event.axis === 'col') {
343
344
  const columnIndex = parseInt(event.index, 10);
344
- model?.setColumnWidth(columnIndex, event.size);
345
+ model?.setColumnWidth(columnIndex, event.size, event.plane);
345
346
  }
346
347
  },
347
348
  [model],
@@ -0,0 +1,21 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { createContext } from '@radix-ui/react-context';
6
+ import { type RefObject } from 'react';
7
+
8
+ import { type TableController } from './Table';
9
+
10
+ // Kept out of `Table.tsx`: react-refresh only fast-refreshes a module whose exports are all
11
+ // components, so a context and its hook exported beside them force a full page reload on every edit.
12
+
13
+ // Context
14
+ //
15
+
16
+ export type TableContextValue = {
17
+ /** Mutable ref populated by Content so Root can expose the controller. */
18
+ controllerRef: RefObject<TableController>;
19
+ };
20
+
21
+ export const [TableContextProvider, useTableContext] = createContext<TableContextValue>('Table');
@@ -2,5 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ export { useTableContext } from './TableContext';
6
+
5
7
  export * from './Table';
6
8
  export * from './DynamicTable';
@@ -9,7 +9,7 @@ import { Obj, Type, View } from '@dxos/echo';
9
9
  import { random } from '@dxos/random';
10
10
  import { withClientProvider } from '@dxos/react-client/testing';
11
11
  import { Grid, type GridEditing, defaultRowSize } from '@dxos/react-ui-grid';
12
- import { withLayout, withTheme } from '@dxos/react-ui/testing';
12
+ import { Loading, withLayout, withTheme } from '@dxos/react-ui/testing';
13
13
  import { ViewModel } from '@dxos/schema';
14
14
  import { Task } from '@dxos/types';
15
15
 
@@ -26,9 +26,8 @@ type StoryArgs = {
26
26
  // TODO(burdon): Broken layout.
27
27
  const DefaultStory = ({ editing }: StoryArgs) => {
28
28
  const { model, table } = useTestTableModel();
29
-
30
29
  if (!model || !table) {
31
- return <div />;
30
+ return <Loading />;
32
31
  }
33
32
 
34
33
  return (
@@ -17,6 +17,7 @@ import {
17
17
  type EditorKeyOrBlurHandler,
18
18
  GridCellEditor,
19
19
  type GridCellEditorProps,
20
+ type GridEditing,
20
21
  type GridScopedProps,
21
22
  editorKeys,
22
23
  parseCellIndex,
@@ -44,6 +45,30 @@ export type TableCellEditorProps<T extends Type.AnyEntity = Type.AnyEntity> = {
44
45
  onSave?: () => void;
45
46
  };
46
47
 
48
+ /**
49
+ * Resolves the field projection for the cell being edited, guarding against an out-of-range field
50
+ * index (e.g. a stale cell, or a pinned plane whose `getFieldIndex` maps beyond the projection).
51
+ */
52
+ const resolveFieldProjection = (
53
+ model: TableModel | undefined,
54
+ editing: GridEditing | undefined,
55
+ ): FieldProjection | undefined => {
56
+ if (!model || !editing) {
57
+ return undefined;
58
+ }
59
+
60
+ const { col, plane } = parseCellIndex(editing.index);
61
+ const fieldIndex = model.getFieldIndex(plane, col);
62
+ const fields = model.projection.getFields();
63
+ if (fieldIndex === undefined || fieldIndex < 0 || fieldIndex >= fields.length) {
64
+ return undefined;
65
+ }
66
+
67
+ const fieldProjection = model.projection.getFieldProjection(fields[fieldIndex].id);
68
+ invariant(fieldProjection);
69
+ return fieldProjection;
70
+ };
71
+
47
72
  export const TableValueEditor = <T extends Type.AnyEntity = Type.AnyEntity>({
48
73
  __gridScope,
49
74
  schema,
@@ -55,17 +80,7 @@ export const TableValueEditor = <T extends Type.AnyEntity = Type.AnyEntity>({
55
80
  }: GridScopedProps<TableCellEditorProps<T>>) => {
56
81
  const { editing } = useGridContext('TableValueEditor', __gridScope);
57
82
 
58
- const fieldProjection = useMemo<FieldProjection | undefined>(() => {
59
- if (!model || !editing) {
60
- return;
61
- }
62
-
63
- const { col } = parseCellIndex(editing.index);
64
- const field = model.projection.getFields()[col];
65
- const fieldProjection = model.projection.getFieldProjection(field.id);
66
- invariant(fieldProjection);
67
- return fieldProjection;
68
- }, [model, editing]);
83
+ const fieldProjection = useMemo(() => resolveFieldProjection(model, editing), [model, editing]);
69
84
 
70
85
  if (
71
86
  fieldProjection?.props.type === TypeEnum.Array ||
@@ -108,17 +123,7 @@ export const TableCellEditor = ({
108
123
  const [validationError, setValidationError] = useState<string | null>(null);
109
124
  const [validationVariant, setValidationVariant] = useState<'error' | 'warning'>('error');
110
125
 
111
- const fieldProjection = useMemo<FieldProjection | undefined>(() => {
112
- if (!model || !editing) {
113
- return;
114
- }
115
-
116
- const { col } = parseCellIndex(editing.index);
117
- const field = model.projection.getFields()[col];
118
- const fieldProjection = model.projection.getFieldProjection(field.id);
119
- invariant(fieldProjection);
120
- return fieldProjection;
121
- }, [model, editing]);
126
+ const fieldProjection = useMemo(() => resolveFieldProjection(model, editing), [model, editing]);
122
127
 
123
128
  // TOOD(burdon): Attach to event handler?
124
129
  const handleEnter = useCallback(
@@ -2,7 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Atom } from '@effect-atom/atom-react';
5
+ import { Atom } from '@effect-atom/atom';
6
6
  import * as Schema from 'effect/Schema';
7
7
  import { useEffect } from 'react';
8
8
 
@@ -2,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { type Registry } from '@effect-atom/atom-react';
5
+ import { type Registry } from '@effect-atom/atom';
6
6
  import { useState } from 'react';
7
7
 
8
8
  import { Type } from '@dxos/echo';