@dxos/react-ui-table 0.8.4-main.e8ec1fe → 0.8.4-main.effb148878

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 (166) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/chunk-J5LGTIGS.mjs +10 -0
  3. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +7 -0
  4. package/dist/lib/browser/{chunk-GS2UNGJK.mjs → chunk-KC62K4WS.mjs} +643 -722
  5. package/dist/lib/browser/chunk-KC62K4WS.mjs.map +7 -0
  6. package/dist/lib/browser/chunk-VWQWF3CP.mjs +67 -0
  7. package/dist/lib/browser/chunk-VWQWF3CP.mjs.map +7 -0
  8. package/dist/lib/browser/index.mjs +1103 -1023
  9. package/dist/lib/browser/index.mjs.map +4 -4
  10. package/dist/lib/browser/meta.json +1 -1
  11. package/dist/lib/browser/testing/index.mjs +35 -46
  12. package/dist/lib/browser/testing/index.mjs.map +3 -3
  13. package/dist/lib/browser/translations.mjs +36 -0
  14. package/dist/lib/browser/translations.mjs.map +7 -0
  15. package/dist/lib/browser/types/index.mjs +4 -3
  16. package/dist/lib/node-esm/{chunk-TRR3C5NX.mjs → chunk-62FOU4M7.mjs} +643 -721
  17. package/dist/lib/node-esm/chunk-62FOU4M7.mjs.map +7 -0
  18. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
  19. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
  20. package/dist/lib/node-esm/chunk-UBXOF64L.mjs +68 -0
  21. package/dist/lib/node-esm/chunk-UBXOF64L.mjs.map +7 -0
  22. package/dist/lib/node-esm/index.mjs +1103 -1023
  23. package/dist/lib/node-esm/index.mjs.map +4 -4
  24. package/dist/lib/node-esm/meta.json +1 -1
  25. package/dist/lib/node-esm/testing/index.mjs +35 -46
  26. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  27. package/dist/lib/node-esm/translations.mjs +37 -0
  28. package/dist/lib/node-esm/translations.mjs.map +7 -0
  29. package/dist/lib/node-esm/types/index.mjs +4 -3
  30. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -2
  31. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  32. package/dist/types/src/components/Table/ColumnSettings.d.ts +2 -2
  33. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  34. package/dist/types/src/components/Table/DynamicTable.d.ts +6 -7
  35. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  36. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +22 -37
  37. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/Table/Relations.stories.d.ts +85 -80
  39. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
  40. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  41. package/dist/types/src/components/Table/Table.d.ts +20 -21
  42. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  43. package/dist/types/src/components/Table/Table.stories.d.ts +93 -26
  44. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  45. package/dist/types/src/components/Table/TableContent.d.ts +18 -0
  46. package/dist/types/src/components/Table/TableContent.d.ts.map +1 -0
  47. package/dist/types/src/components/Table/TableToolbar.d.ts +21 -0
  48. package/dist/types/src/components/Table/TableToolbar.d.ts.map +1 -0
  49. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
  50. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  51. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +10 -12
  52. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  53. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +5 -7
  54. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  55. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +24 -26
  56. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  57. package/dist/types/src/components/index.d.ts +0 -1
  58. package/dist/types/src/components/index.d.ts.map +1 -1
  59. package/dist/types/src/components/testing.d.ts +22 -15
  60. package/dist/types/src/components/testing.d.ts.map +1 -1
  61. package/dist/types/src/hooks/index.d.ts +2 -1
  62. package/dist/types/src/hooks/index.d.ts.map +1 -1
  63. package/dist/types/src/hooks/useAddRow.d.ts +5 -6
  64. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  65. package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
  66. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  67. package/dist/types/src/hooks/useTableModel.d.ts +9 -9
  68. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  69. package/dist/types/src/index.d.ts +0 -2
  70. package/dist/types/src/index.d.ts.map +1 -1
  71. package/dist/types/src/model/modal-controller.d.ts +5 -1
  72. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  73. package/dist/types/src/model/selection-model.d.ts +18 -9
  74. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  75. package/dist/types/src/model/table-model.d.ts +103 -24
  76. package/dist/types/src/model/table-model.d.ts.map +1 -1
  77. package/dist/types/src/model/table-presentation.d.ts +3 -1
  78. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  79. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  80. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  81. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -1
  82. package/dist/types/src/testing/useTestTableModel.d.ts +18 -15
  83. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  84. package/dist/types/src/translations.d.ts +21 -23
  85. package/dist/types/src/translations.d.ts.map +1 -1
  86. package/dist/types/src/types/Table.d.ts +32 -0
  87. package/dist/types/src/types/Table.d.ts.map +1 -0
  88. package/dist/types/src/types/index.d.ts +1 -1
  89. package/dist/types/src/types/index.d.ts.map +1 -1
  90. package/dist/types/src/util/dynamic-table.d.ts +14 -11
  91. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  92. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  93. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  94. package/dist/types/src/util/index.d.ts +2 -0
  95. package/dist/types/src/util/index.d.ts.map +1 -1
  96. package/dist/types/src/util/query-ast.d.ts +7 -0
  97. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  98. package/dist/types/src/util/schema.d.ts.map +1 -1
  99. package/dist/types/src/util/signals.d.ts.map +1 -1
  100. package/dist/types/src/util/sort.d.ts +12 -0
  101. package/dist/types/src/util/sort.d.ts.map +1 -0
  102. package/dist/types/src/util/table-buttons.d.ts +20 -20
  103. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  104. package/dist/types/src/util/table-controls.d.ts +4 -4
  105. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  106. package/dist/types/src/util/tag.d.ts.map +1 -1
  107. package/dist/types/tsconfig.tsbuildinfo +1 -1
  108. package/package.json +69 -79
  109. package/src/components/Table/ColumnActionsMenu.tsx +17 -12
  110. package/src/components/Table/ColumnSettings.tsx +8 -9
  111. package/src/components/Table/DynamicTable.stories.tsx +32 -32
  112. package/src/components/Table/DynamicTable.tsx +32 -33
  113. package/src/components/Table/Relations.stories.tsx +104 -64
  114. package/src/components/Table/RowActionsMenu.tsx +6 -4
  115. package/src/components/Table/Table.stories.tsx +125 -112
  116. package/src/components/Table/Table.tsx +43 -407
  117. package/src/components/Table/TableContent.tsx +426 -0
  118. package/src/components/{TableToolbar → Table}/TableToolbar.tsx +44 -45
  119. package/src/components/TableCellEditor/CellValidationMessage.tsx +5 -6
  120. package/src/components/TableCellEditor/FormCellEditor.tsx +120 -136
  121. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +25 -19
  122. package/src/components/TableCellEditor/TableCellEditor.tsx +35 -33
  123. package/src/components/index.ts +0 -1
  124. package/src/components/testing.ts +38 -17
  125. package/src/hooks/index.ts +2 -1
  126. package/src/hooks/useAddRow.ts +8 -10
  127. package/src/hooks/useProjectionModel.ts +38 -0
  128. package/src/hooks/useTableModel.ts +36 -34
  129. package/src/index.ts +0 -4
  130. package/src/model/modal-controller.ts +23 -12
  131. package/src/model/selection-model.ts +75 -44
  132. package/src/model/table-model.test.ts +77 -55
  133. package/src/model/table-model.ts +431 -168
  134. package/src/model/table-presentation.test.ts +54 -50
  135. package/src/model/table-presentation.ts +44 -40
  136. package/src/playwright/TableManager.ts +13 -6
  137. package/src/playwright/playwright.config.ts +1 -1
  138. package/src/playwright/smoke.spec.ts +52 -8
  139. package/src/testing/useTestTableModel.ts +49 -43
  140. package/src/translations.ts +20 -22
  141. package/src/types/Table.ts +81 -0
  142. package/src/types/index.ts +1 -1
  143. package/src/util/dynamic-table.test.ts +63 -0
  144. package/src/util/dynamic-table.ts +48 -33
  145. package/src/util/index.ts +2 -0
  146. package/src/util/query-ast.ts +22 -0
  147. package/src/util/sort.ts +90 -0
  148. package/src/util/table-buttons.ts +1 -1
  149. package/src/util/table-controls.ts +2 -2
  150. package/dist/lib/browser/chunk-2PFAPQL4.mjs +0 -66
  151. package/dist/lib/browser/chunk-2PFAPQL4.mjs.map +0 -7
  152. package/dist/lib/browser/chunk-GS2UNGJK.mjs.map +0 -7
  153. package/dist/lib/node-esm/chunk-6POOFFYF.mjs +0 -67
  154. package/dist/lib/node-esm/chunk-6POOFFYF.mjs.map +0 -7
  155. package/dist/lib/node-esm/chunk-TRR3C5NX.mjs.map +0 -7
  156. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +0 -21
  157. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +0 -1
  158. package/dist/types/src/components/TableToolbar/index.d.ts +0 -2
  159. package/dist/types/src/components/TableToolbar/index.d.ts.map +0 -1
  160. package/dist/types/src/model/table-sorting.d.ts +0 -52
  161. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  162. package/dist/types/src/types/schema.d.ts +0 -28
  163. package/dist/types/src/types/schema.d.ts.map +0 -1
  164. package/src/components/TableToolbar/index.ts +0 -5
  165. package/src/model/table-sorting.ts +0 -225
  166. package/src/types/schema.ts +0 -73
@@ -2,99 +2,76 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import * as Schema from 'effect/Schema';
5
+ import type * as Schema from 'effect/Schema';
6
6
  import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
7
7
 
8
- import { Filter, Obj } from '@dxos/echo';
9
- import { FormatEnum, Ref, type TypeAnnotation, getValue } from '@dxos/echo/internal';
8
+ import { Entity, Type } from '@dxos/echo';
9
+ import { Ref, getValue } from '@dxos/echo/internal';
10
10
  import { invariant } from '@dxos/invariant';
11
- import { getSnapshot } from '@dxos/live-object';
12
- import { type Client } from '@dxos/react-client';
13
- import { getSpace } from '@dxos/react-client/echo';
14
- import { Popover } from '@dxos/react-ui';
15
- import { Form, type FormProps } from '@dxos/react-ui-form';
11
+ import { type Label, Popover } from '@dxos/react-ui';
12
+ import { Form, type FormRootProps, type RefFieldProps } from '@dxos/react-ui-form';
16
13
  import { parseCellIndex, useGridContext } from '@dxos/react-ui-grid';
17
14
  import { type FieldProjection } from '@dxos/schema';
18
15
  import { getDeep, isTruthy, setDeep } from '@dxos/util';
19
16
 
20
- import { type ModalController, type TableModel } from '../../model';
21
- import { translationKey } from '../../translations';
17
+ import { translationKey } from '#translations';
18
+
19
+ import { type ModalController, type TableModel, type TableRow } from '../../model';
22
20
  import { narrowSchema } from '../../util';
23
21
 
24
- export type OnCreateHandler = (schema: Schema.Schema.AnyNoContext, values: any) => Parameters<typeof Ref.make>[0];
22
+ const createOptionLabel: Label = ['create-new-object.label', { ns: translationKey }];
25
23
 
26
- export type FormCellEditorProps = {
27
- fieldProjection: FieldProjection;
24
+ export type OnCreateHandler = (schema: Type.AnyEntity, values: any) => Parameters<typeof Ref.make>[0];
25
+
26
+ export type FormCellEditorProps<T extends Type.AnyEntity = Type.AnyEntity> = {
27
+ __gridScope: any;
28
+ schema?: T;
28
29
  model?: TableModel;
29
- schema?: Schema.Schema.AnyNoContext;
30
+ fieldProjection: FieldProjection;
31
+ modals?: ModalController; // TODO(burdon): Not used. Remove?
30
32
  onSave?: () => void;
31
33
  onCreate?: OnCreateHandler;
32
- client?: Client;
33
- modals?: ModalController;
34
- __gridScope: any;
35
- } & Omit<FormProps<any>, 'values' | 'schema' | 'onCreate'>;
34
+ } & Omit<FormRootProps<any>, 'values' | 'schema' | 'onCreate'>;
36
35
 
37
- const createOptionLabel = ['create new object label', { ns: translationKey }] as [string, { ns: string }];
38
-
39
- export const FormCellEditor = ({
40
- fieldProjection,
41
- model,
36
+ export const FormCellEditor = <T extends Type.AnyEntity = Type.AnyEntity>({
37
+ __gridScope,
42
38
  schema,
39
+ model,
40
+ fieldProjection,
43
41
  onSave,
44
- client,
45
- modals,
46
42
  onCreate,
47
- __gridScope,
48
43
  ...formProps
49
- }: FormCellEditorProps) => {
50
- const { editing: contextEditing, setEditing, id: gridId } = useGridContext('ArrayEditor', __gridScope);
51
- const [editing, setLocalEditing] = useState(false);
44
+ }: FormCellEditorProps<T>) => {
52
45
  const anchorRef = useRef<HTMLButtonElement | null>(null);
46
+ const { id: _gridId, editing: contextEditing, setEditing } = useGridContext('ArrayEditor', __gridScope);
47
+ const [editing, setLocalEditing] = useState(false);
53
48
 
54
- const getSchema = useCallback(
55
- (typeAnnotation: Pick<TypeAnnotation, 'typename'>) => {
56
- const space = getSpace(model!.view);
57
- invariant(space);
58
-
59
- let schema;
60
- if (client) {
61
- schema = client.graph.schemaRegistry.getSchema(typeAnnotation.typename);
62
- }
63
- if (!schema) {
64
- schema = space.db.schemaRegistry.getSchema(typeAnnotation.typename);
65
- }
66
- return { space, schema };
67
- },
68
- [client, model],
69
- );
49
+ useEffect(() => {
50
+ if (contextEditing && contextEditing.cellElement) {
51
+ anchorRef.current = (contextEditing.cellElement as HTMLElement).querySelector(
52
+ '.dx-grid__cell__content',
53
+ ) as HTMLButtonElement;
54
+ setLocalEditing(true);
55
+ } else {
56
+ anchorRef.current = null;
57
+ setLocalEditing(false);
58
+ }
59
+ }, [contextEditing]);
70
60
 
71
- const handleQueryRefOptions = useCallback(
72
- async (typeAnnotation: TypeAnnotation) => {
73
- const { schema, space } = getSchema(typeAnnotation);
74
- if (model && schema && space) {
75
- const { objects } = await space.db.query(Filter.type(schema)).run();
76
- return objects
77
- .map((obj) => {
78
- return {
79
- dxn: Obj.getDXN(obj),
80
- label: getValue(obj, fieldProjection.field.referencePath!) || obj.id.toString(),
81
- };
82
- })
83
- .filter(isTruthy);
84
- }
61
+ const narrowedSchema = useMemo<Schema.Schema.AnyNoContext | undefined>(() => {
62
+ if (!schema) {
63
+ return undefined;
64
+ }
85
65
 
86
- return [];
87
- },
88
- [client, model],
89
- );
66
+ return narrowSchema(Type.getSchema(schema), [fieldProjection.field.path]);
67
+ }, [JSON.stringify(schema), fieldProjection.field.path]); // TODO(burdon): Avoid stringify.
90
68
 
91
- const originalRow = useMemo(() => {
69
+ const originalRow = useMemo<TableRow | undefined>(() => {
92
70
  if (model && contextEditing) {
93
- const cell = parseCellIndex(contextEditing.index);
94
-
95
71
  // Check if this is a draft cell and get the appropriate row data
72
+ const cell = parseCellIndex(contextEditing.index);
96
73
  if (model.isDraftCell(cell)) {
97
- const draftRow = model.draftRows.value[cell.row];
74
+ const draftRow = model.getDraftRows()[cell.row];
98
75
  invariant(draftRow);
99
76
  return draftRow.data;
100
77
  } else {
@@ -107,41 +84,14 @@ export const FormCellEditor = ({
107
84
  return undefined;
108
85
  }, [model, contextEditing]);
109
86
 
110
- const handleSave = useCallback(
111
- (values: any) => {
112
- const path = fieldProjection.field.path;
113
- const value = getDeep(values, [path]);
114
- setDeep(originalRow, [path], value);
115
- contextEditing?.cellElement?.focus();
116
- setEditing(null);
117
- setLocalEditing(false);
118
- onSave?.();
119
- },
120
- [fieldProjection.field.path, onSave, contextEditing, originalRow],
121
- );
122
-
123
- useEffect(() => {
124
- if (contextEditing && contextEditing.cellElement) {
125
- anchorRef.current = (contextEditing.cellElement as HTMLElement).querySelector(
126
- '.dx-grid__cell__content',
127
- ) as HTMLButtonElement;
128
- setLocalEditing(true);
129
- } else {
130
- anchorRef.current = null;
131
- setLocalEditing(false);
132
- }
133
- }, [contextEditing]);
134
-
135
- const narrowedSchema = useMemo(() => {
136
- if (!schema) {
137
- return undefined;
138
- }
139
- return narrowSchema(schema, [fieldProjection.field.path]);
140
- }, [JSON.stringify(schema), fieldProjection.field.path]); // TODO(burdon): Avoid stringify.
141
-
142
- // NOTE: Important to get a snapshot to eject from the live object.
143
- const formValues = useMemo(() => (originalRow ? getSnapshot(originalRow) : {}), [originalRow]);
87
+ // NOTE: Important to get a mutable deep clone to eject from the echo object.
88
+ // TODO(wittjosiah): Consider using something like Obj.clone for this use case.
89
+ const initialFormValues = useMemo(() => (originalRow ? JSON.parse(JSON.stringify(originalRow)) : {}), [originalRow]);
90
+ const [formValues, setFormValues] = useState<any>(initialFormValues);
144
91
 
92
+ const handleValuesChanged = useCallback<NonNullable<FormRootProps<any>['onValuesChanged']>>((values) => {
93
+ setFormValues(values);
94
+ }, []);
145
95
  const handleOpenChange = useCallback((nextOpen: boolean) => {
146
96
  if (nextOpen === false) {
147
97
  setEditing(null);
@@ -150,27 +100,41 @@ export const FormCellEditor = ({
150
100
  setLocalEditing(nextOpen);
151
101
  }, []);
152
102
 
153
- const refSchema = useMemo(() => {
154
- if (fieldProjection.props.format === FormatEnum.Ref && fieldProjection.props.referenceSchema) {
155
- const { schema: refSchema } = getSchema({
156
- typename: fieldProjection.props.referenceSchema,
157
- });
158
- return refSchema;
159
- }
160
- return null;
161
- }, [fieldProjection.props.format, fieldProjection.props.referenceSchema, getSchema]);
162
-
163
- const createSchema = useMemo(() => {
164
- return refSchema ? Schema.omit<any, any, ['id']>('id')(refSchema) : null;
165
- }, [refSchema]);
166
-
167
- const handleCreate = useCallback(
168
- (values: any) => {
169
- if (refSchema && onCreate) {
170
- const objectWithId = onCreate(refSchema, values);
171
- if (objectWithId) {
172
- const ref = Ref.make(objectWithId);
173
- const path = fieldProjection.field.path;
103
+ const handleSave = useCallback<NonNullable<FormRootProps<any>['onSave']>>(
104
+ (values) => {
105
+ if (!originalRow) {
106
+ return;
107
+ }
108
+ const path = fieldProjection.field.path;
109
+ const value = getDeep(values, [path]);
110
+ // Use model's changeRow for consistent mutation handling.
111
+ if (model) {
112
+ model.changeRow(originalRow, (mutableRow) => {
113
+ setDeep(mutableRow, [path], value);
114
+ });
115
+ } else {
116
+ setDeep(originalRow, [path], value);
117
+ }
118
+ contextEditing?.cellElement?.focus();
119
+ setEditing(null);
120
+ setLocalEditing(false);
121
+ onSave?.();
122
+ },
123
+ [fieldProjection.field.path, onSave, contextEditing, originalRow, model],
124
+ );
125
+
126
+ const handleCreate = useCallback<NonNullable<FormRootProps<any>['onCreate']>>(
127
+ (schema, values) => {
128
+ const objectWithId = onCreate?.(schema, values);
129
+ if (objectWithId && originalRow) {
130
+ const ref = Ref.make(objectWithId);
131
+ const path = fieldProjection.field.path;
132
+ // Use model's changeRow for consistent mutation handling.
133
+ if (model) {
134
+ model.changeRow(originalRow, (mutableRow) => {
135
+ setDeep(mutableRow, [path], ref);
136
+ });
137
+ } else {
174
138
  setDeep(originalRow, [path], ref);
175
139
  }
176
140
  }
@@ -179,7 +143,20 @@ export const FormCellEditor = ({
179
143
  setLocalEditing(false);
180
144
  onSave?.();
181
145
  },
182
- [fieldProjection.field.path, onSave, contextEditing, originalRow, refSchema, onCreate],
146
+ [fieldProjection.field.path, onSave, contextEditing, originalRow, onCreate, model],
147
+ );
148
+
149
+ const getOptions = useCallback<NonNullable<RefFieldProps['getOptions']>>(
150
+ (results) =>
151
+ results
152
+ .map((obj) => {
153
+ return {
154
+ id: Entity.getURI(obj),
155
+ label: getValue(obj, fieldProjection.field.referencePath!) || obj.id.toString(),
156
+ };
157
+ })
158
+ .filter(isTruthy),
159
+ [fieldProjection],
183
160
  );
184
161
 
185
162
  if (!editing) {
@@ -190,24 +167,31 @@ export const FormCellEditor = ({
190
167
  <Popover.Root open={editing} onOpenChange={handleOpenChange}>
191
168
  <Popover.VirtualTrigger virtualRef={anchorRef} />
192
169
  <Popover.Portal>
193
- <Popover.Content tabIndex={-1} classNames='popover-card-width density-fine'>
170
+ <Popover.Content tabIndex={-1} classNames='dx-card-popover-width dx-density-md'>
194
171
  <Popover.Arrow />
195
172
  <Popover.Viewport>
196
- <Form
173
+ <Form.Root
174
+ {...formProps}
197
175
  autoFocus
176
+ schema={narrowedSchema}
198
177
  values={formValues}
199
- schema={narrowedSchema as any}
178
+ onValuesChanged={handleValuesChanged}
179
+ projection={model?.projection}
180
+ createInitialValuePath={fieldProjection.field.referencePath}
181
+ createOptionIcon='ph--plus--regular'
182
+ createOptionLabel={createOptionLabel}
183
+ db={model?.db}
184
+ getOptions={getOptions}
185
+ onCreate={handleCreate}
200
186
  onSave={handleSave}
201
- {...formProps}
202
- onQueryRefOptions={handleQueryRefOptions}
203
- {...(createSchema && {
204
- onCreate: handleCreate,
205
- createSchema,
206
- createInitialValuePath: fieldProjection.field.referencePath,
207
- createOptionIcon: 'ph--plus--regular',
208
- createOptionLabel,
209
- })}
210
- />
187
+ >
188
+ <Form.Viewport>
189
+ <Form.Content>
190
+ <Form.FieldSet />
191
+ <Form.Actions />
192
+ </Form.Content>
193
+ </Form.Viewport>
194
+ </Form.Root>
211
195
  </Popover.Viewport>
212
196
  </Popover.Content>
213
197
  </Popover.Portal>
@@ -5,33 +5,34 @@
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import React from 'react';
7
7
 
8
- import { Obj } from '@dxos/echo';
9
- import { faker } from '@dxos/random';
8
+ import { Obj, Type, View } from '@dxos/echo';
9
+ import { random } from '@dxos/random';
10
10
  import { withClientProvider } from '@dxos/react-client/testing';
11
- import { withTheme } from '@dxos/react-ui/testing';
12
11
  import { Grid, type GridEditing, defaultRowSize } from '@dxos/react-ui-grid';
13
- import { View } from '@dxos/schema';
12
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
13
+ import { ViewModel } from '@dxos/schema';
14
14
  import { Task } from '@dxos/types';
15
15
 
16
+ import { translations } from '#translations';
17
+
16
18
  import { useTestTableModel } from '../../testing';
17
- import { translations } from '../../translations';
18
19
  import { Table } from '../../types';
19
-
20
20
  import { TableCellEditor } from './TableCellEditor';
21
21
 
22
- type StoryProps = {
22
+ type DefaultStoryProps = {
23
23
  editing: GridEditing;
24
24
  };
25
25
 
26
- const DefaultStory = ({ editing }: StoryProps) => {
27
- const { model, view } = useTestTableModel();
26
+ // TODO(burdon): Broken layout.
27
+ const DefaultStory = ({ editing }: DefaultStoryProps) => {
28
+ const { model, table } = useTestTableModel();
28
29
 
29
- if (!model || !view) {
30
+ if (!model || !table) {
30
31
  return <div />;
31
32
  }
32
33
 
33
34
  return (
34
- <div className='flex is-[300px] border border-separator' style={{ height: defaultRowSize }}>
35
+ <div className='border border-separator' style={{ height: defaultRowSize }}>
35
36
  <Grid.Root id='test' editing={editing}>
36
37
  <TableCellEditor model={model} schema={Task.Task} />
37
38
  </Grid.Root>
@@ -44,20 +45,25 @@ const meta = {
44
45
  component: DefaultStory,
45
46
  render: DefaultStory,
46
47
  decorators: [
47
- withTheme,
48
+ withTheme(),
49
+ withLayout({ layout: 'column' }),
48
50
  withClientProvider({
49
51
  types: [View.View, Task.Task, Table.Table],
50
52
  createIdentity: true,
51
53
  createSpace: true,
52
- onCreateSpace: async ({ client, space }) => {
53
- const { view } = await Table.makeView({ client, space, typename: Task.Task.typename });
54
- space.db.add(view);
54
+ onCreateSpace: async ({ space }) => {
55
+ const { view, jsonSchema } = await ViewModel.makeFromDatabase({
56
+ db: space.db,
57
+ typename: Type.getTypename(Task.Task),
58
+ });
59
+ const table = Table.make({ view, jsonSchema });
60
+ space.db.add(table);
55
61
  Array.from({ length: 10 }).forEach(() => {
56
62
  space.db.add(
57
63
  Obj.make(Task.Task, {
58
- title: faker.person.fullName(),
59
- status: faker.helpers.arrayElement(['todo', 'in-progress', 'done'] as const),
60
- description: faker.lorem.sentence(),
64
+ title: random.person.fullName(),
65
+ status: random.helpers.arrayElement(['todo', 'in-progress', 'done'] as const),
66
+ description: random.lorem.sentence(),
61
67
  }),
62
68
  );
63
69
  });
@@ -65,8 +71,8 @@ const meta = {
65
71
  }),
66
72
  ],
67
73
  parameters: {
74
+ layout: 'fullscreen',
68
75
  translations,
69
- layout: 'centered',
70
76
  },
71
77
  } satisfies Meta<typeof DefaultStory>;
72
78
 
@@ -4,12 +4,11 @@
4
4
 
5
5
  import { type Completion } from '@codemirror/autocomplete';
6
6
  import { EditorView } from '@codemirror/view';
7
- import type * as Schema from 'effect/Schema';
8
7
  import React, { useCallback, useMemo, useRef, useState } from 'react';
9
8
 
10
9
  import { debounce } from '@dxos/async';
11
- import type { Client } from '@dxos/client';
12
- import { FormatEnum, TypeEnum } from '@dxos/echo/internal';
10
+ import { type Type } from '@dxos/echo';
11
+ import { Format, TypeEnum } from '@dxos/echo/internal';
13
12
  import { invariant } from '@dxos/invariant';
14
13
  import { type DxGridAxis, type DxGridPosition } from '@dxos/lit-grid';
15
14
  import {
@@ -26,38 +25,34 @@ import {
26
25
  import { type FieldProjection } from '@dxos/schema';
27
26
 
28
27
  import { type ModalController, type TableModel } from '../../model';
29
-
30
28
  import { CellValidationMessage } from './CellValidationMessage';
31
29
  import { FormCellEditor, type OnCreateHandler } from './FormCellEditor';
32
30
 
33
31
  /**
34
32
  * Option to create new object/value.
35
33
  */
36
- export type QueryResult = Pick<Completion, 'label'> & { data: any };
34
+ export type QueryResult = Pick<Completion, 'label'> & {
35
+ data: any;
36
+ };
37
37
 
38
- export type TableCellEditorProps = {
38
+ export type TableCellEditorProps<T extends Type.AnyEntity = Type.AnyEntity> = {
39
+ schema?: T;
39
40
  model?: TableModel;
40
41
  modals?: ModalController;
41
- schema?: Schema.Schema.AnyNoContext;
42
42
  onFocus?: (axis?: DxGridAxis, delta?: -1 | 0 | 1, cell?: DxGridPosition) => void;
43
43
  onCreate?: OnCreateHandler;
44
44
  onSave?: () => void;
45
- client?: Client;
46
45
  };
47
46
 
48
- const adaptValidationMessage = (message: string | null) =>
49
- message ? (message.endsWith('is missing') ? 'Can’t be blank' : message) : null;
50
-
51
- export const TableValueEditor = ({
47
+ export const TableValueEditor = <T extends Type.AnyEntity = Type.AnyEntity>({
48
+ __gridScope,
49
+ schema,
52
50
  model,
53
51
  modals,
54
- schema,
55
52
  onFocus,
56
53
  onSave,
57
54
  onCreate,
58
- client,
59
- __gridScope,
60
- }: GridScopedProps<TableCellEditorProps>) => {
55
+ }: GridScopedProps<TableCellEditorProps<T>>) => {
61
56
  const { editing } = useGridContext('TableValueEditor', __gridScope);
62
57
 
63
58
  const fieldProjection = useMemo<FieldProjection | undefined>(() => {
@@ -66,7 +61,7 @@ export const TableValueEditor = ({
66
61
  }
67
62
 
68
63
  const { col } = parseCellIndex(editing.index);
69
- const field = model.projection.fields[col];
64
+ const field = model.projection.getFields()[col];
70
65
  const fieldProjection = model.projection.getFieldProjection(field.id);
71
66
  invariant(fieldProjection);
72
67
  return fieldProjection;
@@ -74,36 +69,33 @@ export const TableValueEditor = ({
74
69
 
75
70
  if (
76
71
  fieldProjection?.props.type === TypeEnum.Array ||
77
- fieldProjection?.props.format === FormatEnum.SingleSelect ||
78
- fieldProjection?.props.format === FormatEnum.Ref
79
- // TODO(thure): Support `FormatEnum.MultiSelect`
72
+ fieldProjection?.props.format === Format.TypeFormat.SingleSelect ||
73
+ fieldProjection?.props.format === Format.TypeFormat.Ref
74
+ // TODO(thure): Support `Format.TypeFormat.MultiSelect`
80
75
  ) {
81
76
  return (
82
- <FormCellEditor
83
- fieldProjection={fieldProjection}
84
- model={model}
85
- schema={schema}
77
+ <FormCellEditor<T>
86
78
  __gridScope={__gridScope}
79
+ schema={schema}
80
+ model={model}
81
+ fieldProjection={fieldProjection}
82
+ modals={modals}
87
83
  onSave={onSave}
88
84
  onCreate={onCreate}
89
- client={client}
90
- modals={modals}
91
85
  />
92
86
  );
93
87
  }
94
88
 
95
- // For all other types, use the existing cell editor
89
+ // For all other types, use the existing cell editor.
96
90
  return <TableCellEditor model={model} modals={modals} onFocus={onFocus} onSave={onSave} __gridScope={__gridScope} />;
97
91
  };
98
92
 
99
- const editorSlots = { scroll: { className: '!plb-[--dx-grid-cell-editor-padding-block]' } };
100
-
101
93
  export const TableCellEditor = ({
94
+ __gridScope,
102
95
  model,
103
96
  modals,
104
97
  onFocus,
105
98
  onSave,
106
- __gridScope,
107
99
  }: GridScopedProps<TableCellEditorProps>) => {
108
100
  const { editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
109
101
  const suppressNextBlur = useRef(false);
@@ -116,12 +108,13 @@ export const TableCellEditor = ({
116
108
  }
117
109
 
118
110
  const { col } = parseCellIndex(editing.index);
119
- const field = model.projection.fields[col];
111
+ const field = model.projection.getFields()[col];
120
112
  const fieldProjection = model.projection.getFieldProjection(field.id);
121
113
  invariant(fieldProjection);
122
114
  return fieldProjection;
123
115
  }, [model, editing]);
124
116
 
117
+ // TOOD(burdon): Attach to event handler?
125
118
  const handleEnter = useCallback(
126
119
  async (value: any) => {
127
120
  if (!model || !editing) {
@@ -138,7 +131,7 @@ export const TableCellEditor = ({
138
131
  setEditing(null);
139
132
  onSave?.();
140
133
  } else {
141
- setValidationError(adaptValidationMessage(validationResult.error));
134
+ setValidationError(validationResult.error);
142
135
  setValidationVariant('error');
143
136
  }
144
137
  },
@@ -250,7 +243,16 @@ export const TableCellEditor = ({
250
243
  return (
251
244
  <>
252
245
  <CellValidationMessage validationError={validationError} variant={validationVariant} __gridScope={__gridScope} />
253
- <GridCellEditor extensions={extensions} getCellContent={getCellContent} onBlur={handleBlur} slots={editorSlots} />
246
+ <GridCellEditor
247
+ extensions={extensions}
248
+ slots={{
249
+ content: {
250
+ className: '!py-(--dx-grid-cell-editor-padding-block)',
251
+ },
252
+ }}
253
+ getCellContent={getCellContent}
254
+ onBlur={handleBlur}
255
+ />
254
256
  </>
255
257
  );
256
258
  };
@@ -3,4 +3,3 @@
3
3
  //
4
4
 
5
5
  export * from './Table';
6
- export * from './TableToolbar';
@@ -2,33 +2,54 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ import { Atom } from '@effect-atom/atom-react';
5
6
  import * as Schema from 'effect/Schema';
6
7
  import { useEffect } from 'react';
7
8
 
8
- import { FormatEnum, ObjectId, TypeEnum, TypedObject, setValue } from '@dxos/echo/internal';
9
- import { live } from '@dxos/live-object';
10
- import { faker } from '@dxos/random';
9
+ import { DXN, Format, Type } from '@dxos/echo';
10
+ import { TypeEnum } from '@dxos/echo/internal';
11
+ import { setValue } from '@dxos/effect';
12
+ import { random } from '@dxos/random';
11
13
  import { type ProjectionModel } from '@dxos/schema';
12
14
 
13
- export const TestSchema = TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
14
- id: ObjectId,
15
+ export const TestSchema = Schema.Struct({
15
16
  name: Schema.optional(Schema.String),
16
17
  age: Schema.optional(Schema.Number),
17
18
  active: Schema.optional(Schema.Boolean),
18
19
  netWorth: Schema.optional(Schema.Number),
19
- });
20
+ }).pipe(Type.makeObject(DXN.make('com.example.type.test', '0.1.0')));
20
21
 
21
- export const createItems = (n: number) => {
22
- const { data } = live({
22
+ export type TestItem = {
23
+ name: string;
24
+ age: number;
25
+ active: boolean;
26
+ netWorth: number;
27
+ };
28
+
29
+ /**
30
+ * Creates an Atom containing test items data.
31
+ */
32
+ export const createItemsAtom = (n: number) => {
33
+ return Atom.make<{ data: TestItem[] }>({
23
34
  data: Array.from({ length: n }, () => ({
24
- name: faker.person.fullName(),
25
- age: faker.number.int({ min: 20, max: 70 }),
26
- active: faker.datatype.boolean(),
27
- netWorth: faker.number.int(),
35
+ name: random.person.fullName(),
36
+ age: random.number.int({ min: 20, max: 70 }),
37
+ active: random.datatype.boolean(),
38
+ netWorth: random.number.int(),
28
39
  })),
29
- });
40
+ }).pipe(Atom.keepAlive);
41
+ };
30
42
 
31
- return data;
43
+ /**
44
+ * Creates plain test items (non-reactive).
45
+ */
46
+ export const createItems = (n: number): TestItem[] => {
47
+ return Array.from({ length: n }, () => ({
48
+ name: random.person.fullName(),
49
+ age: random.number.int({ min: 20, max: 70 }),
50
+ active: random.datatype.boolean(),
51
+ netWorth: random.number.int(),
52
+ }));
32
53
  };
33
54
 
34
55
  export type SimulatorProps = {
@@ -59,7 +80,7 @@ export const useSimulator = ({ items, projection, insertInterval, updateInterval
59
80
 
60
81
  const i = setInterval(() => {
61
82
  const rowIdx = Math.floor(Math.random() * items.length);
62
- const fields = projection.fields ?? [];
83
+ const fields = projection.getFields() ?? [];
63
84
  const columnIdx = Math.floor(Math.random() * fields.length);
64
85
  const field = fields[columnIdx];
65
86
  const item = items[rowIdx];
@@ -88,7 +109,7 @@ export const useSimulator = ({ items, projection, insertInterval, updateInterval
88
109
 
89
110
  if (format) {
90
111
  switch (format) {
91
- case FormatEnum.Currency: {
112
+ case Format.TypeFormat.Currency: {
92
113
  item[path] = Math.floor(Math.random() * 1000);
93
114
  break;
94
115
  }
@@ -98,5 +119,5 @@ export const useSimulator = ({ items, projection, insertInterval, updateInterval
98
119
  }, updateInterval);
99
120
 
100
121
  return () => clearInterval(i);
101
- }, [items, projection.fields, updateInterval]);
122
+ }, [items, projection.getFields(), updateInterval]);
102
123
  };