@dxos/react-ui-table 0.8.3 → 0.8.4-main.03d5cd7b56

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 (194) hide show
  1. package/dist/lib/browser/chunk-FBURZBPH.mjs +70 -0
  2. package/dist/lib/browser/chunk-FBURZBPH.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-J5LGTIGS.mjs +10 -0
  4. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-YGC7LZJN.mjs +1695 -0
  6. package/dist/lib/browser/chunk-YGC7LZJN.mjs.map +7 -0
  7. package/dist/lib/browser/index.mjs +1162 -2554
  8. package/dist/lib/browser/index.mjs.map +4 -4
  9. package/dist/lib/browser/meta.json +1 -1
  10. package/dist/lib/browser/testing/index.mjs +115 -0
  11. package/dist/lib/browser/testing/index.mjs.map +7 -0
  12. package/dist/lib/browser/translations.mjs +36 -0
  13. package/dist/lib/browser/translations.mjs.map +7 -0
  14. package/dist/lib/browser/types/index.mjs +4 -5
  15. package/dist/lib/node-esm/chunk-DKKMSVKP.mjs +71 -0
  16. package/dist/lib/node-esm/chunk-DKKMSVKP.mjs.map +7 -0
  17. package/dist/lib/node-esm/chunk-GKIWDEZ7.mjs +1697 -0
  18. package/dist/lib/node-esm/chunk-GKIWDEZ7.mjs.map +7 -0
  19. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +11 -0
  20. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +7 -0
  21. package/dist/lib/node-esm/index.mjs +1162 -2554
  22. package/dist/lib/node-esm/index.mjs.map +4 -4
  23. package/dist/lib/node-esm/meta.json +1 -1
  24. package/dist/lib/node-esm/testing/index.mjs +116 -0
  25. package/dist/lib/node-esm/testing/index.mjs.map +7 -0
  26. package/dist/lib/node-esm/translations.mjs +37 -0
  27. package/dist/lib/node-esm/translations.mjs.map +7 -0
  28. package/dist/lib/node-esm/types/index.mjs +4 -5
  29. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts +2 -3
  30. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  31. package/dist/types/src/components/Table/ColumnSettings.d.ts +4 -2
  32. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  33. package/dist/types/src/components/Table/DynamicTable.d.ts +7 -7
  34. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  35. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +35 -3
  36. package/dist/types/src/components/Table/DynamicTable.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/Table/Relations.stories.d.ts +111 -0
  38. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
  39. package/dist/types/src/components/Table/RowActionsMenu.d.ts +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 +21 -18
  42. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  43. package/dist/types/src/components/Table/Table.stories.d.ts +110 -6
  44. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  45. package/dist/types/src/components/Table/TableContent.d.ts +17 -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 +2 -1
  50. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  51. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +14 -9
  52. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  53. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +12 -18
  54. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  55. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +38 -5
  56. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  57. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  58. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  59. package/dist/types/src/components/index.d.ts +0 -1
  60. package/dist/types/src/components/index.d.ts.map +1 -1
  61. package/dist/types/src/components/testing.d.ts +26 -56
  62. package/dist/types/src/components/testing.d.ts.map +1 -1
  63. package/dist/types/src/hooks/index.d.ts +2 -0
  64. package/dist/types/src/hooks/index.d.ts.map +1 -1
  65. package/dist/types/src/hooks/useAddRow.d.ts +13 -0
  66. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -0
  67. package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
  68. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  69. package/dist/types/src/hooks/useTableModel.d.ts +11 -10
  70. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  71. package/dist/types/src/index.d.ts +0 -3
  72. package/dist/types/src/index.d.ts.map +1 -1
  73. package/dist/types/src/model/modal-controller.d.ts +5 -8
  74. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  75. package/dist/types/src/model/selection-model.d.ts +18 -8
  76. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  77. package/dist/types/src/model/table-model.d.ts +150 -32
  78. package/dist/types/src/model/table-model.d.ts.map +1 -1
  79. package/dist/types/src/model/table-presentation.d.ts +13 -2
  80. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  81. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  82. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  83. package/dist/types/src/playwright/playwright.config.d.ts +3 -0
  84. package/dist/types/src/playwright/playwright.config.d.ts.map +1 -0
  85. package/dist/types/src/testing/index.d.ts +2 -0
  86. package/dist/types/src/testing/index.d.ts.map +1 -0
  87. package/dist/types/src/testing/useTestTableModel.d.ts +25 -0
  88. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -0
  89. package/dist/types/src/translations.d.ts +22 -25
  90. package/dist/types/src/translations.d.ts.map +1 -1
  91. package/dist/types/src/types/Table.d.ts +26 -0
  92. package/dist/types/src/types/Table.d.ts.map +1 -0
  93. package/dist/types/src/types/index.d.ts +1 -1
  94. package/dist/types/src/types/index.d.ts.map +1 -1
  95. package/dist/types/src/util/dynamic-table.d.ts +15 -14
  96. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  97. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  98. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  99. package/dist/types/src/util/index.d.ts +2 -1
  100. package/dist/types/src/util/index.d.ts.map +1 -1
  101. package/dist/types/src/util/query-ast.d.ts +7 -0
  102. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  103. package/dist/types/src/util/schema.d.ts +1 -1
  104. package/dist/types/src/util/schema.d.ts.map +1 -1
  105. package/dist/types/src/util/signals.d.ts.map +1 -1
  106. package/dist/types/src/util/sort.d.ts +12 -0
  107. package/dist/types/src/util/sort.d.ts.map +1 -0
  108. package/dist/types/src/util/table-buttons.d.ts +31 -17
  109. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  110. package/dist/types/src/util/table-controls.d.ts +4 -4
  111. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  112. package/dist/types/src/util/tag.d.ts.map +1 -1
  113. package/dist/types/tsconfig.tsbuildinfo +1 -1
  114. package/package.json +76 -72
  115. package/src/components/Table/ColumnActionsMenu.tsx +18 -13
  116. package/src/components/Table/ColumnSettings.tsx +18 -15
  117. package/src/components/Table/DynamicTable.stories.tsx +40 -36
  118. package/src/components/Table/DynamicTable.tsx +38 -34
  119. package/src/components/Table/Relations.stories.tsx +281 -0
  120. package/src/components/Table/RowActionsMenu.tsx +29 -19
  121. package/src/components/Table/Table.stories.tsx +198 -214
  122. package/src/components/Table/Table.tsx +47 -363
  123. package/src/components/Table/TableContent.tsx +421 -0
  124. package/src/components/Table/TableToolbar.tsx +111 -0
  125. package/src/components/TableCellEditor/CellValidationMessage.tsx +9 -5
  126. package/src/components/TableCellEditor/FormCellEditor.tsx +153 -49
  127. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +40 -79
  128. package/src/components/TableCellEditor/TableCellEditor.tsx +109 -199
  129. package/src/components/TableCellEditor/index.ts +1 -0
  130. package/src/components/index.ts +0 -1
  131. package/src/components/testing.ts +46 -37
  132. package/src/hooks/index.ts +2 -0
  133. package/src/hooks/useAddRow.ts +36 -0
  134. package/src/hooks/useProjectionModel.ts +39 -0
  135. package/src/hooks/useTableModel.ts +41 -35
  136. package/src/index.ts +0 -5
  137. package/src/model/modal-controller.ts +24 -41
  138. package/src/model/selection-model.ts +75 -41
  139. package/src/model/table-model.test.ts +91 -56
  140. package/src/model/table-model.ts +629 -162
  141. package/src/model/table-presentation.test.ts +69 -59
  142. package/src/model/table-presentation.ts +283 -154
  143. package/src/playwright/TableManager.ts +13 -6
  144. package/src/playwright/playwright.config.ts +17 -0
  145. package/src/playwright/smoke.spec.ts +181 -42
  146. package/src/testing/index.ts +5 -0
  147. package/src/testing/useTestTableModel.ts +133 -0
  148. package/src/translations.ts +24 -24
  149. package/src/types/Table.ts +79 -0
  150. package/src/types/index.ts +2 -2
  151. package/src/util/dynamic-table.test.ts +70 -0
  152. package/src/util/dynamic-table.ts +51 -36
  153. package/src/util/index.ts +2 -1
  154. package/src/util/query-ast.ts +22 -0
  155. package/src/util/schema.ts +4 -4
  156. package/src/util/sort.ts +90 -0
  157. package/src/util/table-buttons.ts +28 -4
  158. package/src/util/table-controls.ts +6 -3
  159. package/dist/lib/browser/chunk-TL7CKQQV.mjs +0 -27
  160. package/dist/lib/browser/chunk-TL7CKQQV.mjs.map +0 -7
  161. package/dist/lib/node/chunk-O2VPBNS5.cjs +0 -50
  162. package/dist/lib/node/chunk-O2VPBNS5.cjs.map +0 -7
  163. package/dist/lib/node/index.cjs +0 -2739
  164. package/dist/lib/node/index.cjs.map +0 -7
  165. package/dist/lib/node/meta.json +0 -1
  166. package/dist/lib/node/types/index.cjs +0 -31
  167. package/dist/lib/node/types/index.cjs.map +0 -7
  168. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs +0 -29
  169. package/dist/lib/node-esm/chunk-GBT6GD4G.mjs.map +0 -7
  170. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  171. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  172. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  173. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  174. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  175. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  176. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +0 -21
  177. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +0 -1
  178. package/dist/types/src/components/TableToolbar/index.d.ts +0 -2
  179. package/dist/types/src/components/TableToolbar/index.d.ts.map +0 -1
  180. package/dist/types/src/model/table-sorting.d.ts +0 -53
  181. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  182. package/dist/types/src/types/table.d.ts +0 -56
  183. package/dist/types/src/types/table.d.ts.map +0 -1
  184. package/dist/types/src/util/table.d.ts +0 -14
  185. package/dist/types/src/util/table.d.ts.map +0 -1
  186. package/src/components/Table/CreateRefPanel.tsx +0 -84
  187. package/src/components/Table/relations.stories.tsx +0 -124
  188. package/src/components/TableCellEditor/extension.ts +0 -61
  189. package/src/components/TableToolbar/TableToolbar.tsx +0 -102
  190. package/src/components/TableToolbar/index.ts +0 -5
  191. package/src/model/table-sorting.ts +0 -216
  192. package/src/playwright/playwright.config.cts +0 -18
  193. package/src/types/table.ts +0 -27
  194. package/src/util/table.ts +0 -127
@@ -2,55 +2,68 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { effect } from '@preact/signals-core';
6
- import orderBy from 'lodash.orderby';
7
- import { useEffect, useState } from 'react';
5
+ import { RegistryContext } from '@effect-atom/atom-react';
6
+ import { useContext, useEffect, useMemo, useState } from 'react';
8
7
 
9
- import { fullyQualifiedId, getSpace, type Live } from '@dxos/react-client/echo';
10
- import { useSelectionActions } from '@dxos/react-ui-attention';
11
- import { type ViewProjection } from '@dxos/schema';
8
+ import { type Database, Obj } from '@dxos/echo';
9
+ import { useSelected, useSelectionActions } from '@dxos/react-ui-attention';
10
+ import { type ProjectionModel } from '@dxos/schema';
12
11
  import { isNonNullable } from '@dxos/util';
13
12
 
14
- import { type TableRow, TableModel, type TableModelProps, type TableRowAction } from '../model';
15
- import { type TableType } from '../types';
13
+ import {
14
+ TableModel,
15
+ type TableModelProps,
16
+ type TableRow,
17
+ type TableRowAction,
18
+ createEchoChangeCallback,
19
+ } from '../model';
20
+ import { type Table } from '../types';
16
21
 
17
- export type UseTableModelParams<T extends TableRow = TableRow> = {
18
- table?: TableType;
19
- projection?: ViewProjection;
20
- rows?: Live<T>[];
22
+ export type UseTableModelProps<T extends TableRow = TableRow> = {
23
+ object?: Table.Table;
24
+ projection?: ProjectionModel;
25
+ db?: Database.Database;
26
+ rows?: T[];
21
27
  rowActions?: TableRowAction[];
22
28
  onSelectionChanged?: (selection: string[]) => void;
23
29
  onRowAction?: (actionId: string, data: T) => void;
24
30
  } & Pick<
25
31
  TableModelProps<T>,
26
- 'features' | 'onInsertRow' | 'onDeleteRows' | 'onDeleteColumn' | 'onCellUpdate' | 'onRowOrderChange'
32
+ 'features' | 'onInsertRow' | 'onDeleteRows' | 'onColumnDelete' | 'onCellUpdate' | 'onRowOrderChange'
27
33
  >;
28
34
 
29
35
  export const useTableModel = <T extends TableRow = TableRow>({
30
- table,
36
+ object,
31
37
  projection,
38
+ db,
32
39
  rows,
33
40
  rowActions,
34
41
  features,
35
42
  onSelectionChanged,
36
43
  onRowAction,
37
44
  ...props
38
- }: UseTableModelParams<T>): TableModel<T> | undefined => {
45
+ }: UseTableModelProps<T>): TableModel<T> | undefined => {
46
+ const registry = useContext(RegistryContext);
47
+ const selected = useSelected(object && Obj.getDXN(object).toString(), 'multi');
48
+ const initialSelection = useMemo(() => selected, [object]);
49
+
39
50
  const [model, setModel] = useState<TableModel<T>>();
40
51
  useEffect(() => {
41
- if (!table || !projection) {
52
+ if (!object || !projection) {
42
53
  return;
43
54
  }
44
55
 
45
56
  let model: TableModel<T> | undefined;
46
57
  const t = setTimeout(async () => {
47
58
  model = new TableModel<T>({
48
- id: fullyQualifiedId(table),
49
- space: getSpace(table),
50
- view: table.view?.target,
59
+ registry,
60
+ object,
51
61
  projection,
62
+ db,
63
+ change: createEchoChangeCallback<T>(object),
52
64
  features,
53
65
  rowActions,
66
+ initialSelection,
54
67
  onRowAction,
55
68
  ...props,
56
69
  });
@@ -62,32 +75,25 @@ export const useTableModel = <T extends TableRow = TableRow>({
62
75
  clearTimeout(t);
63
76
  void model?.close();
64
77
  };
65
- }, [table, projection, table?.view?.target, features, rowActions]); // TODO(burdon): Trigger if callbacks change?
78
+ // TODO(burdon): Trigger if callbacks change?
79
+ }, [registry, object, projection, features, rowActions, initialSelection]);
66
80
 
67
- // Update data.
81
+ // Update data when rows change.
68
82
  useEffect(() => {
69
- if (rows) {
70
- // TODO(ZaymonFC): Remove this workaround once unstable query ordering issue is resolved
71
- /*
72
- * Sort all objects by string id field as a temporary workaround for query ordering issues
73
- * Reference: https://github.com/dxos/dxos/pull/9409
74
- */
75
- const sortedRows = orderBy(rows, [(row) => String(row.id)], ['asc']);
76
- model?.setRows(sortedRows);
83
+ if (rows && model) {
84
+ model.setRows(rows);
77
85
  }
78
86
  }, [model, rows]);
79
87
 
80
- const { multiSelect, clear } = useSelectionActions(
81
- [table?.id, table?.view?.target?.query.typename].filter(isNonNullable),
82
- );
88
+ const { multiSelect, clear } = useSelectionActions([model?.id].filter(isNonNullable));
83
89
 
84
90
  useEffect(() => {
85
91
  if (!model) {
86
92
  return;
87
93
  }
88
94
 
89
- const unsubscribe = effect(() => {
90
- const selectedItems = [...model.selection.selection.value];
95
+ const unsubscribe = registry.subscribe(model.selection.selectionAtom, () => {
96
+ const selectedItems = [...model.selection.selection];
91
97
  multiSelect(selectedItems);
92
98
  onSelectionChanged?.(selectedItems);
93
99
  });
@@ -97,7 +103,7 @@ export const useTableModel = <T extends TableRow = TableRow>({
97
103
  clear();
98
104
  unsubscribe();
99
105
  };
100
- }, [model, onSelectionChanged]);
106
+ }, [registry, model, onSelectionChanged]);
101
107
 
102
108
  return model;
103
109
  };
package/src/index.ts CHANGED
@@ -2,12 +2,7 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import translations from './translations';
6
-
7
5
  export * from './components';
8
6
  export * from './hooks';
9
7
  export * from './model';
10
- export * from './types';
11
8
  export * from './util';
12
-
13
- export { translations };
@@ -2,11 +2,8 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { signal } from '@preact/signals-core';
6
- import { type RefObject, type MutableRefObject } from 'react';
7
-
8
- import { type Live } from '@dxos/live-object';
9
- import { log } from '@dxos/log';
5
+ import { Atom, type Registry } from '@effect-atom/atom-react';
6
+ import { type MutableRefObject, type RefObject } from 'react';
10
7
 
11
8
  export type ColumnSettingsMode = { type: 'create' } | { type: 'edit'; fieldId: string };
12
9
 
@@ -14,23 +11,27 @@ export type ModalState =
14
11
  | { type: 'row'; rowIndex: number }
15
12
  | { type: 'column'; fieldId: string }
16
13
  | { type: 'refPanel'; targetId: string; typename: string }
17
- | {
18
- type: 'createRefPanel';
19
- typename: string;
20
- initialValues?: Record<string, string>;
21
- onCreate?: (obj: Live<any>) => void;
22
- }
23
14
  | { type: 'columnSettings'; mode: ColumnSettingsMode }
24
15
  | { type: 'closed' };
25
16
 
26
17
  export class ModalController {
27
- private readonly _state = signal<ModalState>({ type: 'closed' });
18
+ private readonly _registry: Registry.Registry;
19
+ private readonly _state: Atom.Writable<ModalState>;
28
20
  private readonly _triggerRef: MutableRefObject<HTMLElement> = { current: null as unknown as HTMLElement };
29
21
 
30
- public get state() {
22
+ constructor(registry: Registry.Registry) {
23
+ this._registry = registry;
24
+ this._state = Atom.make<ModalState>({ type: 'closed' });
25
+ }
26
+
27
+ public get state(): Atom.Atom<ModalState> {
31
28
  return this._state;
32
29
  }
33
30
 
31
+ public get stateValue(): ModalState {
32
+ return this._registry.get(this._state);
33
+ }
34
+
34
35
  public get trigger() {
35
36
  return this._triggerRef as RefObject<HTMLButtonElement>;
36
37
  }
@@ -40,54 +41,36 @@ export class ModalController {
40
41
  };
41
42
 
42
43
  public showRowMenu = (rowIndex: number) => {
43
- this._state.value = { type: 'row', rowIndex };
44
+ this._registry.set(this._state, { type: 'row', rowIndex });
44
45
  };
45
46
 
46
47
  public showColumnMenu = (fieldId: string) => {
47
- this._state.value = { type: 'column', fieldId };
48
+ this._registry.set(this._state, { type: 'column', fieldId });
48
49
  };
49
50
 
50
51
  public showColumnCreator = () => {
51
- this._state.value = { type: 'columnSettings', mode: { type: 'create' } };
52
+ this._registry.set(this._state, { type: 'columnSettings', mode: { type: 'create' } });
52
53
  };
53
54
 
54
55
  public showReferencePanel = (targetId: string, schemaId: string) => {
55
- this._state.value = { type: 'refPanel', targetId, typename: schemaId };
56
+ this._registry.set(this._state, { type: 'refPanel', targetId, typename: schemaId });
56
57
  };
57
58
 
58
59
  public openColumnSettings = () => {
59
- if (this._state.value.type === 'column') {
60
- const fieldId = this._state.value.fieldId;
60
+ const currentState = this._registry.get(this._state);
61
+ if (currentState.type === 'column') {
62
+ const fieldId = currentState.fieldId;
61
63
  // Queue next render to let the column action modal close completely.
62
64
  requestAnimationFrame(() => {
63
- this._state.value = {
65
+ this._registry.set(this._state, {
64
66
  type: 'columnSettings',
65
67
  mode: { type: 'edit', fieldId },
66
- };
68
+ });
67
69
  });
68
70
  }
69
71
  };
70
72
 
71
- public openCreateRef = (
72
- typename: string,
73
- anchorCell: Element | null,
74
- initialValues?: Record<string, string>,
75
- onCreate?: (obj: Live<any>) => void,
76
- ) => {
77
- if (anchorCell) {
78
- this._triggerRef.current = anchorCell as HTMLElement;
79
- this._state.value = {
80
- type: 'createRefPanel',
81
- typename,
82
- initialValues,
83
- onCreate,
84
- };
85
- } else {
86
- log.warn('anchor cell not found while creating new ref');
87
- }
88
- };
89
-
90
73
  public close = () => {
91
- this._state.value = { type: 'closed' };
74
+ this._registry.set(this._state, { type: 'closed' });
92
75
  };
93
76
  }
@@ -2,87 +2,120 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { computed, effect, signal, type ReadonlySignal } from '@preact/signals-core';
5
+ import { Atom, type Registry } from '@effect-atom/atom-react';
6
6
 
7
7
  import { Resource } from '@dxos/context';
8
8
 
9
9
  import { type TableRow } from './table-model';
10
- import { touch } from '../util';
11
10
 
12
11
  export type SelectionMode = 'single' | 'multiple';
13
12
 
14
13
  export class SelectionModel<T extends TableRow> extends Resource {
15
- private readonly _selection = signal<Set<string>>(new Set());
14
+ private readonly _registry: Registry.Registry;
15
+ private readonly _rows: Atom.Atom<T[]>;
16
+ private readonly _selection: Atom.Writable<Set<string>>;
17
+ private readonly _validSelectedIds: Atom.Atom<Set<string>>;
18
+ private readonly _hasSelection: Atom.Atom<boolean>;
19
+ private readonly _allSelected: Atom.Atom<boolean>;
20
+ private readonly _selectionMode: SelectionMode;
21
+ private readonly _onSelectionChanged?: () => void;
16
22
 
17
- private readonly _validSelectedIds = computed<Set<string>>(() => {
18
- const rows = this._rows.value;
19
- if (!rows) {
20
- return new Set<string>();
21
- }
23
+ constructor(
24
+ registry: Registry.Registry,
25
+ rows: Atom.Atom<T[]>,
26
+ selectionMode: SelectionMode,
27
+ initialSelection: string[],
28
+ onSelectionChanged?: () => void,
29
+ ) {
30
+ super();
31
+ this._registry = registry;
32
+ this._rows = rows;
33
+ this._selectionMode = selectionMode;
34
+ this._onSelectionChanged = onSelectionChanged;
22
35
 
23
- const validIds = new Set(rows.map((row) => row.id));
24
- return new Set([...this._selection.value].filter((id) => validIds.has(id)));
25
- });
36
+ this._selection = Atom.make<Set<string>>(new Set(initialSelection));
26
37
 
27
- private readonly _hasSelection = computed<boolean>(() => this._validSelectedIds.value.size > 0);
38
+ this._validSelectedIds = Atom.make((get) => {
39
+ const rows = get(this._rows);
40
+ if (!rows) {
41
+ return new Set<string>();
42
+ }
28
43
 
29
- private readonly _allSelected = computed<boolean>(() => {
30
- const rows = this._rows.value;
31
- if (rows.length === 0) {
32
- return false;
33
- }
44
+ const validIds = new Set(rows.map((row) => row.id));
45
+ const selection = get(this._selection);
46
+ return new Set([...selection].filter((id) => validIds.has(id)));
47
+ });
34
48
 
35
- return rows.every((row) => this._selection.value.has(row.id));
36
- });
49
+ this._hasSelection = Atom.make((get) => get(this._validSelectedIds).size > 0);
37
50
 
38
- constructor(
39
- private readonly _rows: ReadonlySignal<T[]>,
40
- private readonly _selectionMode: SelectionMode,
41
- private readonly _onSelectionChanged?: () => void,
42
- ) {
43
- super();
51
+ this._allSelected = Atom.make((get) => {
52
+ const rows = get(this._rows);
53
+ if (rows.length === 0) {
54
+ return false;
55
+ }
56
+
57
+ const selection = get(this._selection);
58
+ return rows.every((row) => selection.has(row.id));
59
+ });
44
60
  }
45
61
 
46
62
  protected override async _open(): Promise<void> {
47
- const selectionWatcher = effect(() => {
48
- touch(this._selection.value);
63
+ const selectionUnsubscribe = this._registry.subscribe(this._selection, () => {
49
64
  this._onSelectionChanged?.();
50
65
  });
51
- this._ctx.onDispose(selectionWatcher);
66
+ this._ctx.onDispose(selectionUnsubscribe);
52
67
  }
53
68
 
54
69
  //
55
70
  // Getters
56
71
  //
57
72
 
58
- public get selection(): ReadonlySignal<Set<string>> {
73
+ /** Get the selection atom for reactive access. */
74
+ public get selectionAtom(): Atom.Atom<Set<string>> {
59
75
  return this._selection;
60
76
  }
61
77
 
62
- public get hasSelection(): ReadonlySignal<boolean> {
78
+ /** Get the current selection value. */
79
+ public get selection(): Set<string> {
80
+ return this._registry.get(this._selection);
81
+ }
82
+
83
+ /** Get the hasSelection atom for reactive access. */
84
+ public get hasSelectionAtom(): Atom.Atom<boolean> {
63
85
  return this._hasSelection;
64
86
  }
65
87
 
66
- public get allRowsSeleted(): ReadonlySignal<boolean> {
88
+ /** Check if there is any selection. */
89
+ public get hasSelection(): boolean {
90
+ return this._registry.get(this._hasSelection);
91
+ }
92
+
93
+ /** Get the allRowsSelected atom for reactive access. */
94
+ public get allRowsSelectedAtom(): Atom.Atom<boolean> {
67
95
  return this._allSelected;
68
96
  }
69
97
 
98
+ /** Check if all rows are selected. */
99
+ public get allRowsSelected(): boolean {
100
+ return this._registry.get(this._allSelected);
101
+ }
102
+
70
103
  public get selectionMode(): SelectionMode {
71
104
  return this._selectionMode;
72
105
  }
73
106
 
74
107
  public getSelectedRows = (): T[] => {
75
- const selectedIds = this._validSelectedIds.value;
76
- return this._rows.value.filter((row) => selectedIds.has(row.id));
108
+ const selectedIds = this._registry.get(this._validSelectedIds);
109
+ return this._registry.get(this._rows).filter((row) => selectedIds.has(row.id));
77
110
  };
78
111
 
79
112
  public isRowIndexSelected = (rowIndex: number): boolean => {
80
- const row = this._rows.value[rowIndex];
81
- return this._selection.value.has(row.id);
113
+ const row = this._registry.get(this._rows)[rowIndex];
114
+ return this._registry.get(this._selection).has(row.id);
82
115
  };
83
116
 
84
117
  public isObjectSelected = (object: T): boolean => {
85
- return this._selection.value.has(object.id);
118
+ return this._registry.get(this._selection).has(object.id);
86
119
  };
87
120
 
88
121
  //
@@ -90,8 +123,9 @@ export class SelectionModel<T extends TableRow> extends Resource {
90
123
  //
91
124
 
92
125
  public toggleSelectionForRowIndex = (rowIndex: number): void => {
93
- const row = this._rows.value[rowIndex];
94
- const newSelection = new Set(this._selection.value);
126
+ const row = this._registry.get(this._rows)[rowIndex];
127
+ const currentSelection = this._registry.get(this._selection);
128
+ const newSelection = new Set(currentSelection);
95
129
 
96
130
  if (newSelection.has(row.id)) {
97
131
  newSelection.delete(row.id);
@@ -102,17 +136,17 @@ export class SelectionModel<T extends TableRow> extends Resource {
102
136
  newSelection.add(row.id);
103
137
  }
104
138
 
105
- this._selection.value = newSelection;
139
+ this._registry.set(this._selection, newSelection);
106
140
  };
107
141
 
108
142
  public setSelection = (mode: 'all' | 'none'): void => {
109
143
  switch (mode) {
110
144
  case 'all': {
111
- this._selection.value = new Set(this._rows.value.map((row) => row.id));
145
+ this._registry.set(this._selection, new Set(this._registry.get(this._rows).map((row) => row.id)));
112
146
  break;
113
147
  }
114
148
  case 'none': {
115
- this._selection.value = new Set();
149
+ this._registry.set(this._selection, new Set());
116
150
  break;
117
151
  }
118
152
  }
@@ -2,36 +2,41 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { computed } from '@preact/signals-core';
6
- import { Schema } from 'effect';
7
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+ import { Registry } from '@effect-atom/atom-react';
6
+ import * as Schema from 'effect/Schema';
7
+ import { afterEach, beforeEach, describe, expect, test } from 'vitest';
8
8
 
9
- import { Obj, Ref } from '@dxos/echo';
10
- import { TypedObject } from '@dxos/echo-schema';
11
- import { updateCounter } from '@dxos/echo-schema/testing';
12
- import { registerSignalsRuntime } from '@dxos/echo-signals';
13
- import { createEchoSchema } from '@dxos/live-object/testing';
14
- import { live } from '@dxos/react-client/echo';
15
- import { createView, ViewProjection } from '@dxos/schema';
9
+ import { Filter, JsonSchema, Query, Type } from '@dxos/echo';
10
+ import { createEchoSchema } from '@dxos/echo/testing';
11
+ import { ProjectionModel, ViewModel, createDirectChangeCallback } from '@dxos/schema';
16
12
 
17
- import { TableModel, type TableModelProps } from './table-model';
18
- import { TableType } from '../types';
13
+ import { Table } from '../types';
14
+ import {
15
+ TableModel,
16
+ type TableModelProps,
17
+ createDirectChangeCallback as createTableDirectChangeCallback,
18
+ } from './table-model';
19
19
 
20
20
  // TODO(burdon): Tests are disabled in project.json since they bring in plugin deps.
21
21
  // Restore once factored out into react-ui-table.
22
22
 
23
- registerSignalsRuntime();
24
-
25
23
  describe('TableModel', () => {
26
24
  let updateCount = 0;
27
- let model: TableModel;
25
+ let model: any;
26
+ let registry: Registry.Registry;
28
27
 
29
28
  beforeEach(async () => {
30
29
  updateCount = 0;
31
- model = createTableModel({
30
+ registry = Registry.make();
31
+ model = createTableModel(registry, {
32
32
  onCellUpdate: () => updateCount++,
33
33
  });
34
34
  await model.open();
35
+ model.setRows([
36
+ { id: '1', title: 'Test', completed: false },
37
+ { id: '2', title: 'Test 2', completed: true },
38
+ { id: '3', title: 'Test 3', completed: false },
39
+ ]);
35
40
  });
36
41
 
37
42
  afterEach(async () => {
@@ -39,76 +44,106 @@ describe('TableModel', () => {
39
44
  });
40
45
 
41
46
  describe('methods', () => {
42
- it('should pin a row', () => {
47
+ test('should pin a row', () => {
43
48
  model.pinRow(1, 'top');
44
49
  expect(model.pinnedRows.top).toContain(1);
45
50
  });
46
51
 
47
- it('should unpin a row', () => {
52
+ test('should unpin a row', () => {
48
53
  model.pinRow(1, 'top');
49
54
  model.unpinRow(1);
50
55
  expect(model.pinnedRows.top).not.toContain(1);
51
56
  });
52
57
 
53
- it('should toggle row selection', () => {
58
+ test('should toggle row selection', () => {
54
59
  model.selection.toggleSelectionForRowIndex(2);
55
- expect(model.selection.hasSelection.value).toBe(true);
60
+ expect(model.selection.hasSelection).toBe(true);
56
61
  });
57
62
 
58
- it('should deselect a row', () => {
63
+ test('should deselect a row', () => {
59
64
  model.selection.toggleSelectionForRowIndex(2);
60
65
  model.selection.toggleSelectionForRowIndex(2);
61
- expect(model.selection.hasSelection.value).toBe(false);
66
+ expect(model.selection.hasSelection).toBe(false);
62
67
  });
63
68
 
64
- it('should bulk select rows', () => {
69
+ test('should bulk select rows', () => {
65
70
  model.selection.setSelection('all');
66
- expect(model.selection.allRowsSeleted.value).toBe(true);
71
+ expect(model.selection.allRowsSelected).toBe(true);
67
72
  model.selection.setSelection('none');
68
- expect(model.selection.hasSelection.value).toBe(false);
73
+ expect(model.selection.hasSelection).toBe(false);
69
74
  });
70
75
  });
71
76
 
72
77
  describe('reactivity', () => {
73
- it('pure signals should nest', () => {
74
- const signal$ = live({ arr: [{ thingInside: 1 }, { thingInside: 2 }] });
75
-
76
- const computed$ = computed(() => {
77
- return signal$.arr.map((row) =>
78
- computed(() => {
79
- return row.thingInside;
80
- }),
81
- );
82
- });
83
-
84
- using outerCounter = updateCounter(() => {
85
- computed$.value.map((c) => c.value);
86
- });
87
-
88
- using innerCounter = updateCounter(() => {
89
- computed$.value[0].value;
90
- });
78
+ test('rows should update when setRows is called', () => {
79
+ model.setRows([{ id: '4', title: 'Test 4', completed: false }]);
80
+ expect(model.getRows()).toHaveLength(1);
81
+ expect(model.getRows()[0].id).toBe('4');
82
+
83
+ model.setRows([
84
+ { id: '5', title: 'Test 5', completed: false },
85
+ { id: '6', title: 'Test 6', completed: true },
86
+ ]);
87
+ expect(model.getRows()).toHaveLength(2);
88
+ expect(model.getRows()[0].id).toBe('5');
89
+ expect(model.getRows()[1].id).toBe('6');
90
+ });
91
91
 
92
- expect(computed$.value.map((v) => v.value)).toEqual([1, 2]);
92
+ test('selection should update when toggled', () => {
93
+ model.selection.toggleSelectionForRowIndex(0);
94
+ expect(model.selection.hasSelection).toBe(true);
95
+ expect(model.selection.selection.size).toBe(1);
93
96
 
94
- signal$.arr[0].thingInside = 3;
97
+ model.selection.toggleSelectionForRowIndex(0);
98
+ expect(model.selection.hasSelection).toBe(false);
99
+ expect(model.selection.selection.size).toBe(0);
100
+ });
95
101
 
96
- expect(computed$.value.map((v) => v.value)).toEqual([3, 2]);
97
- expect(outerCounter.count).toBe(1);
98
- expect(innerCounter.count).toBe(1);
102
+ test('derived atoms should reflect current state', () => {
103
+ // Set initial rows.
104
+ model.setRows([
105
+ { id: '1', title: 'Zebra', completed: false },
106
+ { id: '2', title: 'Apple', completed: true },
107
+ { id: '3', title: 'Mango', completed: false },
108
+ ]);
109
+
110
+ // Verify rows are accessible.
111
+ const rows = model.getRows();
112
+ expect(rows).toHaveLength(3);
113
+ expect(rows[0].id).toBeDefined();
99
114
  });
100
115
  });
101
116
  });
102
117
 
103
- class Test extends TypedObject({ typename: 'example.com/type/Test', version: '0.1.0' })({
118
+ const Test = Schema.Struct({
104
119
  title: Schema.String,
105
120
  completed: Schema.Boolean,
106
- }) {}
107
-
108
- const createTableModel = (props: Partial<TableModelProps> = {}): TableModel => {
121
+ }).pipe(
122
+ Type.object({
123
+ typename: 'com.example.type.test',
124
+ version: '0.1.0',
125
+ }),
126
+ );
127
+
128
+ const createTableModel = (registry: Registry.Registry, props: Partial<TableModelProps> = {}): TableModel => {
109
129
  const schema = createEchoSchema(Test);
110
- const view = createView({ name: 'Test', typename: schema.typename, jsonSchema: schema.jsonSchema });
111
- const projection = new ViewProjection(schema.jsonSchema, view);
112
- const table = Obj.make(TableType, { view: Ref.make(view) });
113
- return new TableModel({ id: table.id, space: undefined, view, projection, ...props });
130
+ const view = ViewModel.make({
131
+ query: Query.select(Filter.type(schema)),
132
+ jsonSchema: schema.jsonSchema,
133
+ });
134
+ const object = Table.make({ view });
135
+ const projection = new ProjectionModel({
136
+ registry,
137
+ view,
138
+ baseSchema: schema.jsonSchema,
139
+ change: createDirectChangeCallback(view.projection, JsonSchema.toJsonSchema(schema)),
140
+ });
141
+ projection.normalizeView();
142
+ return new TableModel({
143
+ registry,
144
+ object,
145
+ projection,
146
+ change: createTableDirectChangeCallback(object),
147
+ ...props,
148
+ });
114
149
  };