@dxos/react-ui-table 0.8.4-main.ae835ea → 0.8.4-main.bc674ce

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 (129) hide show
  1. package/dist/lib/browser/{chunk-G7MGPBSR.mjs → chunk-I5IFW7UF.mjs} +633 -663
  2. package/dist/lib/browser/chunk-I5IFW7UF.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-OZBLY7XM.mjs +76 -0
  4. package/dist/lib/browser/chunk-OZBLY7XM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1131 -999
  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 +29 -41
  9. package/dist/lib/browser/testing/index.mjs.map +3 -3
  10. package/dist/lib/browser/types/index.mjs +3 -3
  11. package/dist/lib/node-esm/{chunk-LD5JEBA6.mjs → chunk-DDJKYJFY.mjs} +633 -662
  12. package/dist/lib/node-esm/chunk-DDJKYJFY.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-WNZSKIRF.mjs +77 -0
  14. package/dist/lib/node-esm/chunk-WNZSKIRF.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1131 -999
  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 +29 -41
  19. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  20. package/dist/lib/node-esm/types/index.mjs +3 -3
  21. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +1 -2
  22. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/ColumnSettings.d.ts +2 -2
  24. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/DynamicTable.d.ts +4 -5
  26. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +4 -18
  28. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  29. package/dist/types/src/components/Table/Relations.stories.d.ts +12 -9
  30. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  32. package/dist/types/src/components/Table/Table.d.ts +8 -8
  33. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  34. package/dist/types/src/components/Table/Table.stories.d.ts +71 -5
  35. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  36. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
  37. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +10 -12
  38. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +5 -7
  40. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +4 -4
  42. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +4 -4
  44. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  45. package/dist/types/src/components/testing.d.ts +22 -15
  46. package/dist/types/src/components/testing.d.ts.map +1 -1
  47. package/dist/types/src/hooks/index.d.ts +2 -1
  48. package/dist/types/src/hooks/index.d.ts.map +1 -1
  49. package/dist/types/src/hooks/useAddRow.d.ts +5 -6
  50. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  51. package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
  52. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  53. package/dist/types/src/hooks/useTableModel.d.ts +8 -8
  54. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  55. package/dist/types/src/model/modal-controller.d.ts +5 -1
  56. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  57. package/dist/types/src/model/selection-model.d.ts +17 -8
  58. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  59. package/dist/types/src/model/table-model.d.ts +102 -23
  60. package/dist/types/src/model/table-model.d.ts.map +1 -1
  61. package/dist/types/src/model/table-presentation.d.ts +3 -1
  62. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  63. package/dist/types/src/playwright/TableManager.d.ts +1 -1
  64. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  65. package/dist/types/src/testing/useTestTableModel.d.ts +18 -15
  66. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  67. package/dist/types/src/translations.d.ts +4 -4
  68. package/dist/types/src/translations.d.ts.map +1 -1
  69. package/dist/types/src/types/Table.d.ts +31 -0
  70. package/dist/types/src/types/Table.d.ts.map +1 -0
  71. package/dist/types/src/types/index.d.ts +1 -1
  72. package/dist/types/src/types/index.d.ts.map +1 -1
  73. package/dist/types/src/util/dynamic-table.d.ts +13 -11
  74. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  75. package/dist/types/src/util/index.d.ts +2 -0
  76. package/dist/types/src/util/index.d.ts.map +1 -1
  77. package/dist/types/src/util/query-ast.d.ts +7 -0
  78. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  79. package/dist/types/src/util/sort.d.ts +12 -0
  80. package/dist/types/src/util/sort.d.ts.map +1 -0
  81. package/dist/types/tsconfig.tsbuildinfo +1 -1
  82. package/package.json +53 -53
  83. package/src/components/Table/ColumnActionsMenu.tsx +10 -6
  84. package/src/components/Table/ColumnSettings.tsx +6 -6
  85. package/src/components/Table/DynamicTable.stories.tsx +12 -13
  86. package/src/components/Table/DynamicTable.tsx +21 -14
  87. package/src/components/Table/Relations.stories.tsx +35 -39
  88. package/src/components/Table/RowActionsMenu.tsx +24 -15
  89. package/src/components/Table/Table.stories.tsx +71 -75
  90. package/src/components/Table/Table.tsx +335 -305
  91. package/src/components/TableCellEditor/CellValidationMessage.tsx +1 -1
  92. package/src/components/TableCellEditor/FormCellEditor.tsx +110 -133
  93. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +16 -12
  94. package/src/components/TableCellEditor/TableCellEditor.tsx +31 -31
  95. package/src/components/TableToolbar/TableToolbar.tsx +44 -31
  96. package/src/components/testing.ts +38 -12
  97. package/src/hooks/index.ts +2 -1
  98. package/src/hooks/useAddRow.ts +8 -10
  99. package/src/hooks/useProjectionModel.ts +39 -0
  100. package/src/hooks/useTableModel.ts +35 -33
  101. package/src/model/modal-controller.ts +23 -12
  102. package/src/model/selection-model.ts +75 -44
  103. package/src/model/table-model.test.ts +82 -54
  104. package/src/model/table-model.ts +428 -163
  105. package/src/model/table-presentation.test.ts +59 -49
  106. package/src/model/table-presentation.ts +31 -26
  107. package/src/playwright/TableManager.ts +5 -5
  108. package/src/playwright/playwright.config.ts +1 -1
  109. package/src/playwright/smoke.spec.ts +8 -5
  110. package/src/testing/useTestTableModel.ts +44 -37
  111. package/src/translations.ts +3 -3
  112. package/src/types/Table.ts +90 -0
  113. package/src/types/index.ts +1 -1
  114. package/src/util/dynamic-table.ts +39 -27
  115. package/src/util/index.ts +2 -0
  116. package/src/util/query-ast.ts +19 -0
  117. package/src/util/sort.ts +90 -0
  118. package/dist/lib/browser/chunk-4K7JPALT.mjs +0 -66
  119. package/dist/lib/browser/chunk-4K7JPALT.mjs.map +0 -7
  120. package/dist/lib/browser/chunk-G7MGPBSR.mjs.map +0 -7
  121. package/dist/lib/node-esm/chunk-LD5JEBA6.mjs.map +0 -7
  122. package/dist/lib/node-esm/chunk-MK6OVUF6.mjs +0 -67
  123. package/dist/lib/node-esm/chunk-MK6OVUF6.mjs.map +0 -7
  124. package/dist/types/src/model/table-sorting.d.ts +0 -52
  125. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  126. package/dist/types/src/types/schema.d.ts +0 -28
  127. package/dist/types/src/types/schema.d.ts.map +0 -1
  128. package/src/model/table-sorting.ts +0 -225
  129. package/src/types/schema.ts +0 -73
@@ -12,9 +12,9 @@ export type CellValidationMessageProps = {
12
12
  };
13
13
 
14
14
  export const CellValidationMessage = ({
15
+ __gridScope,
15
16
  validationError,
16
17
  variant = 'error',
17
- __gridScope,
18
18
  }: GridScopedProps<CellValidationMessageProps>) => {
19
19
  const { editing, editBox: box } = useGridContext('GridSheetCellEditor', __gridScope);
20
20
 
@@ -2,123 +2,48 @@
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 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';
17
+ import { type ModalController, type TableModel, type TableRow } from '../../model';
21
18
  import { translationKey } from '../../translations';
22
19
  import { narrowSchema } from '../../util';
23
20
 
24
- export type OnCreateHandler = (schema: Schema.Schema.AnyNoContext, values: any) => Parameters<typeof Ref.make>[0];
21
+ const createOptionLabel: Label = ['create new object label', { ns: translationKey }];
25
22
 
26
- export type FormCellEditorProps = {
27
- fieldProjection: FieldProjection;
23
+ export type OnCreateHandler = (schema: Type.Entity.Any, values: any) => Parameters<typeof Ref.make>[0];
24
+
25
+ export type FormCellEditorProps<T extends Type.Entity.Any = Type.Entity.Any> = {
26
+ __gridScope: any;
27
+ schema?: T;
28
28
  model?: TableModel;
29
- schema?: Schema.Schema.AnyNoContext;
29
+ fieldProjection: FieldProjection;
30
+ modals?: ModalController; // TODO(burdon): Not used. Remove?
30
31
  onSave?: () => void;
31
32
  onCreate?: OnCreateHandler;
32
- client?: Client;
33
- modals?: ModalController;
34
- __gridScope: any;
35
- } & Omit<FormProps<any>, 'values' | 'schema' | 'onCreate'>;
36
-
37
- const createOptionLabel = ['create new object label', { ns: translationKey }] as [string, { ns: string }];
33
+ } & Omit<FormRootProps<any>, 'values' | 'schema' | 'onCreate'>;
38
34
 
39
- export const FormCellEditor = ({
40
- fieldProjection,
41
- model,
35
+ export const FormCellEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
36
+ __gridScope,
42
37
  schema,
38
+ model,
39
+ fieldProjection,
43
40
  onSave,
44
- client,
45
- modals,
46
41
  onCreate,
47
- __gridScope,
48
42
  ...formProps
49
- }: FormCellEditorProps) => {
50
- const { editing: contextEditing, setEditing, id: gridId } = useGridContext('ArrayEditor', __gridScope);
51
- const [editing, setLocalEditing] = useState(false);
43
+ }: FormCellEditorProps<T>) => {
52
44
  const anchorRef = useRef<HTMLButtonElement | null>(null);
53
-
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
- );
70
-
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
- }
85
-
86
- return [];
87
- },
88
- [client, model],
89
- );
90
-
91
- const originalRow = useMemo(() => {
92
- if (model && contextEditing) {
93
- const cell = parseCellIndex(contextEditing.index);
94
-
95
- // Check if this is a draft cell and get the appropriate row data
96
- if (model.isDraftCell(cell)) {
97
- const draftRow = model.draftRows.value[cell.row];
98
- invariant(draftRow);
99
- return draftRow.data;
100
- } else {
101
- const row = model.getRowAt(cell.row);
102
- invariant(row);
103
- return row;
104
- }
105
- }
106
-
107
- return undefined;
108
- }, [model, contextEditing]);
109
-
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
- );
45
+ const { id: _gridId, editing: contextEditing, setEditing } = useGridContext('ArrayEditor', __gridScope);
46
+ const [editing, setLocalEditing] = useState(false);
122
47
 
123
48
  useEffect(() => {
124
49
  if (contextEditing && contextEditing.cellElement) {
@@ -132,15 +57,35 @@ export const FormCellEditor = ({
132
57
  }
133
58
  }, [contextEditing]);
134
59
 
135
- const narrowedSchema = useMemo(() => {
60
+ const narrowedSchema = useMemo<Schema.Schema.AnyNoContext | undefined>(() => {
136
61
  if (!schema) {
137
62
  return undefined;
138
63
  }
64
+
139
65
  return narrowSchema(schema, [fieldProjection.field.path]);
140
66
  }, [JSON.stringify(schema), fieldProjection.field.path]); // TODO(burdon): Avoid stringify.
141
67
 
142
- // NOTE: Important to get a snapshot to eject from the live object.
143
- const formValues = useMemo(() => (originalRow ? getSnapshot(originalRow) : {}), [originalRow]);
68
+ const originalRow = useMemo<TableRow | undefined>(() => {
69
+ if (model && contextEditing) {
70
+ // Check if this is a draft cell and get the appropriate row data
71
+ const cell = parseCellIndex(contextEditing.index);
72
+ if (model.isDraftCell(cell)) {
73
+ const draftRow = model.getDraftRows()[cell.row];
74
+ invariant(draftRow);
75
+ return draftRow.data;
76
+ } else {
77
+ const row = model.getRowAt(cell.row);
78
+ invariant(row);
79
+ return row;
80
+ }
81
+ }
82
+
83
+ return undefined;
84
+ }, [model, contextEditing]);
85
+
86
+ // NOTE: Important to get a mutable deep clone to eject from the echo object.
87
+ // TODO(wittjosiah): Consider using something like Obj.clone for this use case.
88
+ const formValues = useMemo(() => (originalRow ? JSON.parse(JSON.stringify(originalRow)) : {}), [originalRow]);
144
89
 
145
90
  const handleOpenChange = useCallback((nextOpen: boolean) => {
146
91
  if (nextOpen === false) {
@@ -150,27 +95,41 @@ export const FormCellEditor = ({
150
95
  setLocalEditing(nextOpen);
151
96
  }, []);
152
97
 
153
- const createSchema = useMemo(() => {
154
- if (fieldProjection.props.format === FormatEnum.Ref && fieldProjection.props.referenceSchema) {
155
- const { schema: refSchema } = getSchema({
156
- typename: fieldProjection.props.referenceSchema,
157
- });
158
- if (!refSchema) {
159
- return null;
98
+ const handleSave = useCallback<NonNullable<FormRootProps<any>['onSave']>>(
99
+ (values) => {
100
+ if (!originalRow) {
101
+ return;
160
102
  }
161
- const omit = Schema.omit<any, any, ['id']>('id');
162
- return omit(refSchema);
163
- }
164
- return null;
165
- }, [fieldProjection.props.format, fieldProjection.props.referenceSchema, getSchema]);
166
-
167
- const handleCreate = useCallback(
168
- (values: any) => {
169
- if (schema && onCreate) {
170
- const objectWithId = onCreate(schema, values);
171
- if (objectWithId) {
172
- const ref = Ref.make(objectWithId);
173
- const path = fieldProjection.field.path;
103
+ const path = fieldProjection.field.path;
104
+ const value = getDeep(values, [path]);
105
+ // Use model's changeRow for consistent mutation handling.
106
+ if (model) {
107
+ model.changeRow(originalRow, (mutableRow) => {
108
+ setDeep(mutableRow, [path], value);
109
+ });
110
+ } else {
111
+ setDeep(originalRow, [path], value);
112
+ }
113
+ contextEditing?.cellElement?.focus();
114
+ setEditing(null);
115
+ setLocalEditing(false);
116
+ onSave?.();
117
+ },
118
+ [fieldProjection.field.path, onSave, contextEditing, originalRow, model],
119
+ );
120
+
121
+ const handleCreate = useCallback<NonNullable<FormRootProps<any>['onCreate']>>(
122
+ (schema, values) => {
123
+ const objectWithId = onCreate?.(schema, values);
124
+ if (objectWithId && originalRow) {
125
+ const ref = Ref.make(objectWithId);
126
+ const path = fieldProjection.field.path;
127
+ // Use model's changeRow for consistent mutation handling.
128
+ if (model) {
129
+ model.changeRow(originalRow, (mutableRow) => {
130
+ setDeep(mutableRow, [path], ref);
131
+ });
132
+ } else {
174
133
  setDeep(originalRow, [path], ref);
175
134
  }
176
135
  }
@@ -179,7 +138,20 @@ export const FormCellEditor = ({
179
138
  setLocalEditing(false);
180
139
  onSave?.();
181
140
  },
182
- [fieldProjection.field.path, onSave, contextEditing, originalRow, createSchema, onCreate],
141
+ [fieldProjection.field.path, onSave, contextEditing, originalRow, onCreate, model],
142
+ );
143
+
144
+ const getOptions = useCallback<NonNullable<RefFieldProps['getOptions']>>(
145
+ (results) =>
146
+ results
147
+ .map((obj) => {
148
+ return {
149
+ id: Entity.getDXN(obj).toString(),
150
+ label: getValue(obj, fieldProjection.field.referencePath!) || obj.id.toString(),
151
+ };
152
+ })
153
+ .filter(isTruthy),
154
+ [fieldProjection],
183
155
  );
184
156
 
185
157
  if (!editing) {
@@ -193,21 +165,26 @@ export const FormCellEditor = ({
193
165
  <Popover.Content tabIndex={-1} classNames='popover-card-width density-fine'>
194
166
  <Popover.Arrow />
195
167
  <Popover.Viewport>
196
- <Form
168
+ <Form.Root
169
+ {...formProps}
197
170
  autoFocus
171
+ schema={narrowedSchema}
198
172
  values={formValues}
199
- schema={narrowedSchema as any}
173
+ createInitialValuePath={fieldProjection.field.referencePath}
174
+ createOptionIcon='ph--plus--regular'
175
+ createOptionLabel={createOptionLabel}
176
+ db={model?.db}
177
+ getOptions={getOptions}
178
+ onCreate={handleCreate}
200
179
  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
- />
180
+ >
181
+ <Form.Viewport>
182
+ <Form.Content>
183
+ <Form.FieldSet />
184
+ <Form.Actions />
185
+ </Form.Content>
186
+ </Form.Viewport>
187
+ </Form.Root>
211
188
  </Popover.Viewport>
212
189
  </Popover.Content>
213
190
  </Popover.Portal>
@@ -8,9 +8,10 @@ import React from 'react';
8
8
  import { Obj } from '@dxos/echo';
9
9
  import { faker } from '@dxos/random';
10
10
  import { withClientProvider } from '@dxos/react-client/testing';
11
- import { withTheme } from '@dxos/react-ui/testing';
11
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
12
12
  import { Grid, type GridEditing, defaultRowSize } from '@dxos/react-ui-grid';
13
- import { DataType } from '@dxos/schema';
13
+ import { View } from '@dxos/schema';
14
+ import { Task } from '@dxos/types';
14
15
 
15
16
  import { useTestTableModel } from '../../testing';
16
17
  import { translations } from '../../translations';
@@ -22,17 +23,18 @@ type StoryProps = {
22
23
  editing: GridEditing;
23
24
  };
24
25
 
26
+ // TODO(burdon): Broken layout.
25
27
  const DefaultStory = ({ editing }: StoryProps) => {
26
- const { model, view } = useTestTableModel();
28
+ const { model, table } = useTestTableModel();
27
29
 
28
- if (!model || !view) {
30
+ if (!model || !table) {
29
31
  return <div />;
30
32
  }
31
33
 
32
34
  return (
33
- <div className='flex w-[300px] border border-separator' style={{ height: defaultRowSize }}>
35
+ <div className='border border-separator' style={{ height: defaultRowSize }}>
34
36
  <Grid.Root id='test' editing={editing}>
35
- <TableCellEditor model={model} schema={DataType.Task} />
37
+ <TableCellEditor model={model} schema={Task.Task} />
36
38
  </Grid.Root>
37
39
  </div>
38
40
  );
@@ -44,16 +46,18 @@ const meta = {
44
46
  render: DefaultStory,
45
47
  decorators: [
46
48
  withTheme,
49
+ withLayout({ layout: 'column' }),
47
50
  withClientProvider({
48
- types: [DataType.View, DataType.Task, Table.Table],
51
+ types: [View.View, Task.Task, Table.Table],
49
52
  createIdentity: true,
50
53
  createSpace: true,
51
- onCreateSpace: async ({ client, space }) => {
52
- const { view } = await Table.makeView({ client, space, typename: DataType.Task.typename });
53
- space.db.add(view);
54
+ onCreateSpace: async ({ space }) => {
55
+ const { view, jsonSchema } = await View.makeFromDatabase({ db: space.db, typename: Task.Task.typename });
56
+ const table = Table.make({ view, jsonSchema });
57
+ space.db.add(table);
54
58
  Array.from({ length: 10 }).forEach(() => {
55
59
  space.db.add(
56
- Obj.make(DataType.Task, {
60
+ Obj.make(Task.Task, {
57
61
  title: faker.person.fullName(),
58
62
  status: faker.helpers.arrayElement(['todo', 'in-progress', 'done'] as const),
59
63
  description: faker.lorem.sentence(),
@@ -64,8 +68,8 @@ const meta = {
64
68
  }),
65
69
  ],
66
70
  parameters: {
71
+ layout: 'fullscreen',
67
72
  translations,
68
- layout: 'centered',
69
73
  },
70
74
  } satisfies Meta<typeof DefaultStory>;
71
75
 
@@ -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 {
@@ -30,34 +29,37 @@ import { type ModalController, type TableModel } from '../../model';
30
29
  import { CellValidationMessage } from './CellValidationMessage';
31
30
  import { FormCellEditor, type OnCreateHandler } from './FormCellEditor';
32
31
 
32
+ const editorSlots = {
33
+ scroll: {
34
+ className: '!plb-[--dx-grid-cell-editor-padding-block]',
35
+ },
36
+ };
37
+
33
38
  /**
34
39
  * Option to create new object/value.
35
40
  */
36
- export type QueryResult = Pick<Completion, 'label'> & { data: any };
41
+ export type QueryResult = Pick<Completion, 'label'> & {
42
+ data: any;
43
+ };
37
44
 
38
- export type TableCellEditorProps = {
45
+ export type TableCellEditorProps<T extends Type.Entity.Any = Type.Entity.Any> = {
46
+ schema?: T;
39
47
  model?: TableModel;
40
48
  modals?: ModalController;
41
- schema?: Schema.Schema.AnyNoContext;
42
49
  onFocus?: (axis?: DxGridAxis, delta?: -1 | 0 | 1, cell?: DxGridPosition) => void;
43
50
  onCreate?: OnCreateHandler;
44
51
  onSave?: () => void;
45
- client?: Client;
46
52
  };
47
53
 
48
- const adaptValidationMessage = (message: string | null) =>
49
- message ? (message.endsWith('is missing') ? 'Can’t be blank' : message) : null;
50
-
51
- export const TableValueEditor = ({
54
+ export const TableValueEditor = <T extends Type.Entity.Any = Type.Entity.Any>({
55
+ __gridScope,
56
+ schema,
52
57
  model,
53
58
  modals,
54
- schema,
55
59
  onFocus,
56
60
  onSave,
57
61
  onCreate,
58
- client,
59
- __gridScope,
60
- }: GridScopedProps<TableCellEditorProps>) => {
62
+ }: GridScopedProps<TableCellEditorProps<T>>) => {
61
63
  const { editing } = useGridContext('TableValueEditor', __gridScope);
62
64
 
63
65
  const fieldProjection = useMemo<FieldProjection | undefined>(() => {
@@ -66,7 +68,7 @@ export const TableValueEditor = ({
66
68
  }
67
69
 
68
70
  const { col } = parseCellIndex(editing.index);
69
- const field = model.projection.fields[col];
71
+ const field = model.projection.getFields()[col];
70
72
  const fieldProjection = model.projection.getFieldProjection(field.id);
71
73
  invariant(fieldProjection);
72
74
  return fieldProjection;
@@ -74,36 +76,33 @@ export const TableValueEditor = ({
74
76
 
75
77
  if (
76
78
  fieldProjection?.props.type === TypeEnum.Array ||
77
- fieldProjection?.props.format === FormatEnum.SingleSelect ||
78
- fieldProjection?.props.format === FormatEnum.Ref
79
- // TODO(thure): Support `FormatEnum.MultiSelect`
79
+ fieldProjection?.props.format === Format.TypeFormat.SingleSelect ||
80
+ fieldProjection?.props.format === Format.TypeFormat.Ref
81
+ // TODO(thure): Support `Format.TypeFormat.MultiSelect`
80
82
  ) {
81
83
  return (
82
- <FormCellEditor
83
- fieldProjection={fieldProjection}
84
- model={model}
85
- schema={schema}
84
+ <FormCellEditor<T>
86
85
  __gridScope={__gridScope}
86
+ schema={schema}
87
+ model={model}
88
+ fieldProjection={fieldProjection}
89
+ modals={modals}
87
90
  onSave={onSave}
88
91
  onCreate={onCreate}
89
- client={client}
90
- modals={modals}
91
92
  />
92
93
  );
93
94
  }
94
95
 
95
- // For all other types, use the existing cell editor
96
+ // For all other types, use the existing cell editor.
96
97
  return <TableCellEditor model={model} modals={modals} onFocus={onFocus} onSave={onSave} __gridScope={__gridScope} />;
97
98
  };
98
99
 
99
- const editorSlots = { scroll: { className: '!plb-[--dx-grid-cell-editor-padding-block]' } };
100
-
101
100
  export const TableCellEditor = ({
101
+ __gridScope,
102
102
  model,
103
103
  modals,
104
104
  onFocus,
105
105
  onSave,
106
- __gridScope,
107
106
  }: GridScopedProps<TableCellEditorProps>) => {
108
107
  const { editing, setEditing } = useGridContext('TableCellEditor', __gridScope);
109
108
  const suppressNextBlur = useRef(false);
@@ -116,12 +115,13 @@ export const TableCellEditor = ({
116
115
  }
117
116
 
118
117
  const { col } = parseCellIndex(editing.index);
119
- const field = model.projection.fields[col];
118
+ const field = model.projection.getFields()[col];
120
119
  const fieldProjection = model.projection.getFieldProjection(field.id);
121
120
  invariant(fieldProjection);
122
121
  return fieldProjection;
123
122
  }, [model, editing]);
124
123
 
124
+ // TOOD(burdon): Attach to event handler?
125
125
  const handleEnter = useCallback(
126
126
  async (value: any) => {
127
127
  if (!model || !editing) {
@@ -138,7 +138,7 @@ export const TableCellEditor = ({
138
138
  setEditing(null);
139
139
  onSave?.();
140
140
  } else {
141
- setValidationError(adaptValidationMessage(validationResult.error));
141
+ setValidationError(validationResult.error);
142
142
  setValidationVariant('error');
143
143
  }
144
144
  },
@@ -2,10 +2,9 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Rx } from '@effect-rx/rx-react';
6
- import React, { useMemo } from 'react';
5
+ import { Atom, RegistryContext } from '@effect-atom/atom-react';
6
+ import React, { useContext, useEffect, useMemo } from 'react';
7
7
 
8
- import { live } from '@dxos/live-object';
9
8
  import { type ThemedClassName } from '@dxos/react-ui';
10
9
  import {
11
10
  type ActionGraphEdges,
@@ -18,7 +17,6 @@ import {
18
17
  createMenuAction,
19
18
  useMenuActions,
20
19
  } from '@dxos/react-ui-menu';
21
- import { rxFromSignal } from '@dxos/react-ui-menu';
22
20
 
23
21
  import { translationKey } from '../../translations';
24
22
 
@@ -34,41 +32,49 @@ type TableToolbarState = Partial<{ viewDirty: boolean }>;
34
32
 
35
33
  export type TableToolbarProps = ThemedClassName<
36
34
  TableToolbarState & {
37
- onAdd: () => void;
38
- onSave: () => void;
35
+ onAdd?: () => void;
36
+ onSave?: () => void;
39
37
  attendableId?: string;
40
- customActions?: Rx.Rx<ActionGraphProps>;
38
+ customActions?: Atom.Atom<ActionGraphProps>;
41
39
  }
42
40
  >;
43
41
 
44
42
  const createTableToolbarActions = ({
45
- state,
43
+ stateAtom,
46
44
  onAdd,
47
45
  onSave,
48
46
  customActions,
49
47
  }: {
50
- state: TableToolbarState;
51
- onAdd: () => void;
52
- onSave: () => void;
53
- customActions?: Rx.Rx<ActionGraphProps>;
48
+ stateAtom: Atom.Atom<TableToolbarState>;
49
+ onAdd?: () => void;
50
+ onSave?: () => void;
51
+ customActions?: Atom.Atom<ActionGraphProps>;
54
52
  }) =>
55
- Rx.make((get) => {
56
- const add = createMenuAction<TableToolbarActionProperties>('add-row', onAdd, {
57
- type: 'add-row' as const,
58
- icon: 'ph--plus--regular',
59
- label: ['add row', { ns: translationKey }],
60
- testId: 'table.toolbar.add-row',
61
- });
62
- const save = createMenuAction<TableToolbarActionProperties>('save-view', onSave, {
63
- type: 'save-view' as const,
64
- icon: 'ph--floppy-disk--regular',
65
- label: ['save view label', { ns: translationKey }],
66
- testId: 'table.toolbar.save-view',
67
- iconOnly: false,
68
- hidden: get(rxFromSignal(() => !state.viewDirty)),
69
- });
53
+ Atom.make((get) => {
54
+ const nodes: ActionGraphNodes = [];
55
+ if (onAdd) {
56
+ const add = createMenuAction<TableToolbarActionProperties>('add-row', onAdd, {
57
+ type: 'add-row' as const,
58
+ icon: 'ph--plus--regular',
59
+ label: ['add row label', { ns: translationKey }],
60
+ testId: 'table.toolbar.add-row',
61
+ });
62
+ nodes.push(add);
63
+ }
64
+ if (onSave) {
65
+ const state = get(stateAtom);
66
+ const save = createMenuAction<TableToolbarActionProperties>('save-view', onSave, {
67
+ type: 'save-view' as const,
68
+ icon: 'ph--floppy-disk--regular',
69
+ label: ['save view label', { ns: translationKey }],
70
+ testId: 'table.toolbar.save-view',
71
+ iconOnly: false,
72
+ hidden: !state.viewDirty,
73
+ });
74
+ nodes.push(save);
75
+ }
70
76
  const gap = createGapSeparator();
71
- const nodes: ActionGraphNodes = [add, save, ...gap.nodes];
77
+ nodes.push(...gap.nodes);
72
78
  const edges: ActionGraphEdges = nodes.map(({ id: target }) => ({ source: 'root', target }));
73
79
  if (customActions) {
74
80
  const custom = get(customActions);
@@ -90,10 +96,17 @@ export const TableToolbar = ({
90
96
  onSave,
91
97
  customActions,
92
98
  }: TableToolbarProps) => {
93
- const state = useMemo(() => live<TableToolbarState>({ viewDirty }), []);
99
+ const registry = useContext(RegistryContext);
100
+ const stateAtom = useMemo(() => Atom.make<TableToolbarState>({ viewDirty }), []);
101
+
102
+ // Update state.viewDirty when the prop changes.
103
+ useEffect(() => {
104
+ registry.set(stateAtom, { viewDirty });
105
+ }, [registry, stateAtom, viewDirty]);
106
+
94
107
  const actionsCreator = useMemo(
95
- () => createTableToolbarActions({ state, onAdd, onSave, customActions }),
96
- [state, onAdd, onSave, customActions],
108
+ () => createTableToolbarActions({ stateAtom, onAdd, onSave, customActions }),
109
+ [stateAtom, onAdd, onSave, customActions],
97
110
  );
98
111
  const menu = useMenuActions(actionsCreator);
99
112