@dxos/react-ui-table 0.8.4-main.c1de068 → 0.8.4-main.c85a9c8dae

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 (158) hide show
  1. package/dist/lib/browser/chunk-DSR2MRZB.mjs +80 -0
  2. package/dist/lib/browser/chunk-DSR2MRZB.mjs.map +7 -0
  3. package/dist/lib/browser/chunk-PTHMYAAM.mjs +1751 -0
  4. package/dist/lib/browser/chunk-PTHMYAAM.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +1190 -1139
  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 +31 -38
  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-GI72MQSG.mjs +1753 -0
  12. package/dist/lib/node-esm/chunk-GI72MQSG.mjs.map +7 -0
  13. package/dist/lib/node-esm/chunk-HTZJSULI.mjs +81 -0
  14. package/dist/lib/node-esm/chunk-HTZJSULI.mjs.map +7 -0
  15. package/dist/lib/node-esm/index.mjs +1190 -1139
  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 +31 -38
  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 +2 -3
  22. package/dist/types/src/components/Table/ColumnActionsMenu.d.ts.map +1 -1
  23. package/dist/types/src/components/Table/ColumnSettings.d.ts +3 -3
  24. package/dist/types/src/components/Table/ColumnSettings.d.ts.map +1 -1
  25. package/dist/types/src/components/Table/DynamicTable.d.ts +7 -7
  26. package/dist/types/src/components/Table/DynamicTable.d.ts.map +1 -1
  27. package/dist/types/src/components/Table/DynamicTable.stories.d.ts +37 -3
  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 +111 -0
  30. package/dist/types/src/components/Table/Relations.stories.d.ts.map +1 -0
  31. package/dist/types/src/components/Table/RowActionsMenu.d.ts +1 -1
  32. package/dist/types/src/components/Table/RowActionsMenu.d.ts.map +1 -1
  33. package/dist/types/src/components/Table/Table.d.ts +17 -13
  34. package/dist/types/src/components/Table/Table.d.ts.map +1 -1
  35. package/dist/types/src/components/Table/Table.stories.d.ts +108 -4
  36. package/dist/types/src/components/Table/Table.stories.d.ts.map +1 -1
  37. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts +1 -1
  38. package/dist/types/src/components/TableCellEditor/CellValidationMessage.d.ts.map +1 -1
  39. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts +13 -9
  40. package/dist/types/src/components/TableCellEditor/FormCellEditor.d.ts.map +1 -1
  41. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts +11 -18
  42. package/dist/types/src/components/TableCellEditor/TableCellEditor.d.ts.map +1 -1
  43. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts +39 -4
  44. package/dist/types/src/components/TableCellEditor/TableCellEditor.stories.d.ts.map +1 -1
  45. package/dist/types/src/components/TableCellEditor/index.d.ts +1 -0
  46. package/dist/types/src/components/TableCellEditor/index.d.ts.map +1 -1
  47. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts +5 -5
  48. package/dist/types/src/components/TableToolbar/TableToolbar.d.ts.map +1 -1
  49. package/dist/types/src/components/testing.d.ts +23 -16
  50. package/dist/types/src/components/testing.d.ts.map +1 -1
  51. package/dist/types/src/hooks/index.d.ts +2 -1
  52. package/dist/types/src/hooks/index.d.ts.map +1 -1
  53. package/dist/types/src/hooks/useAddRow.d.ts +6 -6
  54. package/dist/types/src/hooks/useAddRow.d.ts.map +1 -1
  55. package/dist/types/src/hooks/useProjectionModel.d.ts +6 -0
  56. package/dist/types/src/hooks/useProjectionModel.d.ts.map +1 -0
  57. package/dist/types/src/hooks/useTableModel.d.ts +10 -10
  58. package/dist/types/src/hooks/useTableModel.d.ts.map +1 -1
  59. package/dist/types/src/model/modal-controller.d.ts +5 -8
  60. package/dist/types/src/model/modal-controller.d.ts.map +1 -1
  61. package/dist/types/src/model/selection-model.d.ts +17 -8
  62. package/dist/types/src/model/selection-model.d.ts.map +1 -1
  63. package/dist/types/src/model/table-model.d.ts +112 -30
  64. package/dist/types/src/model/table-model.d.ts.map +1 -1
  65. package/dist/types/src/model/table-presentation.d.ts +5 -2
  66. package/dist/types/src/model/table-presentation.d.ts.map +1 -1
  67. package/dist/types/src/playwright/TableManager.d.ts +2 -1
  68. package/dist/types/src/playwright/TableManager.d.ts.map +1 -1
  69. package/dist/types/src/testing/useTestTableModel.d.ts +18 -34
  70. package/dist/types/src/testing/useTestTableModel.d.ts.map +1 -1
  71. package/dist/types/src/translations.d.ts +4 -4
  72. package/dist/types/src/translations.d.ts.map +1 -1
  73. package/dist/types/src/types/Table.d.ts +31 -0
  74. package/dist/types/src/types/Table.d.ts.map +1 -0
  75. package/dist/types/src/types/index.d.ts +1 -1
  76. package/dist/types/src/types/index.d.ts.map +1 -1
  77. package/dist/types/src/util/dynamic-table.d.ts +14 -12
  78. package/dist/types/src/util/dynamic-table.d.ts.map +1 -1
  79. package/dist/types/src/util/dynamic-table.test.d.ts +2 -0
  80. package/dist/types/src/util/dynamic-table.test.d.ts.map +1 -0
  81. package/dist/types/src/util/index.d.ts +2 -1
  82. package/dist/types/src/util/index.d.ts.map +1 -1
  83. package/dist/types/src/util/query-ast.d.ts +7 -0
  84. package/dist/types/src/util/query-ast.d.ts.map +1 -0
  85. package/dist/types/src/util/schema.d.ts +1 -1
  86. package/dist/types/src/util/schema.d.ts.map +1 -1
  87. package/dist/types/src/util/sort.d.ts +12 -0
  88. package/dist/types/src/util/sort.d.ts.map +1 -0
  89. package/dist/types/src/util/table-buttons.d.ts.map +1 -1
  90. package/dist/types/src/util/table-controls.d.ts.map +1 -1
  91. package/dist/types/tsconfig.tsbuildinfo +1 -1
  92. package/package.json +61 -56
  93. package/src/components/Table/ColumnActionsMenu.tsx +11 -7
  94. package/src/components/Table/ColumnSettings.tsx +9 -10
  95. package/src/components/Table/DynamicTable.stories.tsx +28 -24
  96. package/src/components/Table/DynamicTable.tsx +33 -27
  97. package/src/components/Table/Relations.stories.tsx +240 -0
  98. package/src/components/Table/RowActionsMenu.tsx +26 -17
  99. package/src/components/Table/Table.stories.tsx +146 -116
  100. package/src/components/Table/Table.tsx +353 -297
  101. package/src/components/TableCellEditor/CellValidationMessage.tsx +6 -6
  102. package/src/components/TableCellEditor/FormCellEditor.tsx +134 -40
  103. package/src/components/TableCellEditor/TableCellEditor.stories.tsx +31 -66
  104. package/src/components/TableCellEditor/TableCellEditor.tsx +88 -238
  105. package/src/components/TableCellEditor/index.ts +1 -0
  106. package/src/components/TableToolbar/TableToolbar.tsx +54 -38
  107. package/src/components/testing.ts +39 -13
  108. package/src/hooks/index.ts +2 -1
  109. package/src/hooks/useAddRow.ts +14 -14
  110. package/src/hooks/useProjectionModel.ts +39 -0
  111. package/src/hooks/useTableModel.ts +36 -34
  112. package/src/model/modal-controller.ts +24 -41
  113. package/src/model/selection-model.ts +75 -43
  114. package/src/model/table-model.test.ts +92 -55
  115. package/src/model/table-model.ts +453 -175
  116. package/src/model/table-presentation.test.ts +70 -58
  117. package/src/model/table-presentation.ts +111 -64
  118. package/src/playwright/TableManager.ts +13 -6
  119. package/src/playwright/playwright.config.ts +1 -1
  120. package/src/playwright/smoke.spec.ts +171 -33
  121. package/src/testing/useTestTableModel.ts +46 -35
  122. package/src/translations.ts +3 -3
  123. package/src/types/Table.ts +95 -0
  124. package/src/types/index.ts +1 -1
  125. package/src/util/dynamic-table.test.ts +70 -0
  126. package/src/util/dynamic-table.ts +46 -33
  127. package/src/util/index.ts +2 -1
  128. package/src/util/query-ast.ts +22 -0
  129. package/src/util/schema.ts +4 -4
  130. package/src/util/sort.ts +90 -0
  131. package/src/util/table-buttons.ts +4 -3
  132. package/src/util/table-controls.ts +6 -3
  133. package/dist/lib/browser/chunk-5XLSHLOG.mjs +0 -1751
  134. package/dist/lib/browser/chunk-5XLSHLOG.mjs.map +0 -7
  135. package/dist/lib/browser/chunk-L2LSB6KV.mjs +0 -18
  136. package/dist/lib/browser/chunk-L2LSB6KV.mjs.map +0 -7
  137. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs +0 -20
  138. package/dist/lib/node-esm/chunk-NXZ4CJYX.mjs.map +0 -7
  139. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs +0 -1752
  140. package/dist/lib/node-esm/chunk-Y66YLSTC.mjs.map +0 -7
  141. package/dist/types/src/components/Table/CreateRefPanel.d.ts +0 -9
  142. package/dist/types/src/components/Table/CreateRefPanel.d.ts.map +0 -1
  143. package/dist/types/src/components/Table/relations.stories.d.ts +0 -9
  144. package/dist/types/src/components/Table/relations.stories.d.ts.map +0 -1
  145. package/dist/types/src/components/TableCellEditor/extension.d.ts +0 -13
  146. package/dist/types/src/components/TableCellEditor/extension.d.ts.map +0 -1
  147. package/dist/types/src/model/table-sorting.d.ts +0 -53
  148. package/dist/types/src/model/table-sorting.d.ts.map +0 -1
  149. package/dist/types/src/types/schema.d.ts +0 -8
  150. package/dist/types/src/types/schema.d.ts.map +0 -1
  151. package/dist/types/src/util/table.d.ts +0 -11
  152. package/dist/types/src/util/table.d.ts.map +0 -1
  153. package/src/components/Table/CreateRefPanel.tsx +0 -86
  154. package/src/components/Table/relations.stories.tsx +0 -116
  155. package/src/components/TableCellEditor/extension.ts +0 -61
  156. package/src/model/table-sorting.ts +0 -216
  157. package/src/types/schema.ts +0 -16
  158. package/src/util/table.ts +0 -49
@@ -2,33 +2,59 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { Schema } from 'effect';
5
+ import { Atom } from '@effect-atom/atom-react';
6
+ import * as Schema from 'effect/Schema';
6
7
  import { useEffect } from 'react';
7
8
 
8
- import { FormatEnum, ObjectId, setValue, TypedObject, TypeEnum } from '@dxos/echo-schema';
9
- import { live } from '@dxos/live-object';
9
+ import { Format, Type } from '@dxos/echo';
10
+ import { TypeEnum } from '@dxos/echo/internal';
11
+ import { setValue } from '@dxos/effect';
10
12
  import { faker } 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(
21
+ Type.object({
22
+ typename: 'example.com/type/Test',
23
+ version: '0.1.0',
24
+ }),
25
+ );
20
26
 
21
- export const createItems = (n: number) => {
22
- const { data } = live({
27
+ export type TestItem = {
28
+ name: string;
29
+ age: number;
30
+ active: boolean;
31
+ netWorth: number;
32
+ };
33
+
34
+ /**
35
+ * Creates an Atom containing test items data.
36
+ */
37
+ export const createItemsAtom = (n: number) => {
38
+ return Atom.make<{ data: TestItem[] }>({
23
39
  data: Array.from({ length: n }, () => ({
24
40
  name: faker.person.fullName(),
25
41
  age: faker.number.int({ min: 20, max: 70 }),
26
42
  active: faker.datatype.boolean(),
27
43
  netWorth: faker.number.int(),
28
44
  })),
29
- });
45
+ }).pipe(Atom.keepAlive);
46
+ };
30
47
 
31
- return data;
48
+ /**
49
+ * Creates plain test items (non-reactive).
50
+ */
51
+ export const createItems = (n: number): TestItem[] => {
52
+ return Array.from({ length: n }, () => ({
53
+ name: faker.person.fullName(),
54
+ age: faker.number.int({ min: 20, max: 70 }),
55
+ active: faker.datatype.boolean(),
56
+ netWorth: faker.number.int(),
57
+ }));
32
58
  };
33
59
 
34
60
  export type SimulatorProps = {
@@ -59,7 +85,7 @@ export const useSimulator = ({ items, projection, insertInterval, updateInterval
59
85
 
60
86
  const i = setInterval(() => {
61
87
  const rowIdx = Math.floor(Math.random() * items.length);
62
- const fields = projection.fields ?? [];
88
+ const fields = projection.getFields() ?? [];
63
89
  const columnIdx = Math.floor(Math.random() * fields.length);
64
90
  const field = fields[columnIdx];
65
91
  const item = items[rowIdx];
@@ -88,7 +114,7 @@ export const useSimulator = ({ items, projection, insertInterval, updateInterval
88
114
 
89
115
  if (format) {
90
116
  switch (format) {
91
- case FormatEnum.Currency: {
117
+ case Format.TypeFormat.Currency: {
92
118
  item[path] = Math.floor(Math.random() * 1000);
93
119
  break;
94
120
  }
@@ -98,5 +124,5 @@ export const useSimulator = ({ items, projection, insertInterval, updateInterval
98
124
  }, updateInterval);
99
125
 
100
126
  return () => clearInterval(i);
101
- }, [items, projection.fields, updateInterval]);
127
+ }, [items, projection.getFields(), updateInterval]);
102
128
  };
@@ -2,5 +2,6 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- export * from './useTableModel';
6
5
  export * from './useAddRow';
6
+ export * from './useProjectionModel';
7
+ export * from './useTableModel';
@@ -2,15 +2,15 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { type Schema } from 'effect';
6
5
  import { useCallback } from 'react';
7
6
 
8
- import { type Space } from '@dxos/client/echo';
9
- import { Obj } from '@dxos/echo';
7
+ import { type Database, Obj, type Type } from '@dxos/echo';
10
8
 
11
- export type UseAddRowParams = {
12
- space?: Space;
13
- schema?: Schema.Schema.AnyNoContext;
9
+ import { type InsertRowResult } from '../model';
10
+
11
+ export type UseAddRowProps = {
12
+ db?: Database.Database;
13
+ schema?: Type.AnyObj;
14
14
  };
15
15
 
16
16
  /**
@@ -18,19 +18,19 @@ export type UseAddRowParams = {
18
18
  * Returns a callback that attempts to create an object and returns boolean success/failure.
19
19
  * Can accept optional data to create objects with specific content (used for committing draft rows).
20
20
  */
21
- export const useAddRow = ({ space, schema }: UseAddRowParams) => {
21
+ export const useAddRow = ({ db, schema }: UseAddRowProps) => {
22
22
  return useCallback(
23
- (data?: any) => {
24
- if (space && schema) {
23
+ (data?: any): InsertRowResult => {
24
+ if (db && schema) {
25
25
  try {
26
- space.db.add(Obj.make(schema, data ?? {}));
27
- return true;
26
+ db.add(Obj.make(schema, data ?? {}));
27
+ return 'final';
28
28
  } catch (error) {
29
- return false;
29
+ return 'draft';
30
30
  }
31
31
  }
32
- return false;
32
+ return 'final';
33
33
  },
34
- [space, schema],
34
+ [db, schema],
35
35
  );
36
36
  };
@@ -0,0 +1,39 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Registry } from '@effect-atom/atom-react';
6
+ import { useState } from 'react';
7
+
8
+ import { JsonSchema, Type } from '@dxos/echo';
9
+ import { useAsyncEffect } from '@dxos/react-ui';
10
+ import { ProjectionModel, createEchoChangeCallback } from '@dxos/schema';
11
+
12
+ import { type Table } from '../types';
13
+
14
+ export const useProjectionModel = <S extends Type.AnyEntity>(
15
+ schema: S | undefined,
16
+ table: Table.Table | undefined,
17
+ registry: Registry.Registry,
18
+ ) => {
19
+ const [projection, setProjection] = useState<ProjectionModel | undefined>();
20
+
21
+ useAsyncEffect(async () => {
22
+ if (schema && table) {
23
+ const view = await table.view.load();
24
+ // For mutable schemas (EchoSchema), use the live jsonSchema reference for reactivity.
25
+ // For immutable schemas, create a snapshot.
26
+ const jsonSchema = Type.isMutable(schema) ? schema.jsonSchema : JsonSchema.toJsonSchema(schema);
27
+
28
+ // Always use createEchoChangeCallback since the view is ECHO-backed.
29
+ // Pass schema only when mutable to allow schema mutations.
30
+ const change = createEchoChangeCallback(view, Type.isMutable(schema) ? schema : undefined);
31
+
32
+ const projection = new ProjectionModel({ registry, view, baseSchema: jsonSchema, change });
33
+ projection.normalizeView();
34
+ setProjection(projection);
35
+ }
36
+ }, [schema, table, registry]);
37
+
38
+ return projection;
39
+ };
@@ -2,58 +2,65 @@
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, useMemo, useState } from 'react';
5
+ import { RegistryContext } from '@effect-atom/atom-react';
6
+ import { useContext, useEffect, useMemo, useState } from 'react';
8
7
 
9
- import { type JsonSchemaType } from '@dxos/echo-schema';
10
- import { type Live } from '@dxos/live-object';
11
- import { fullyQualifiedId } from '@dxos/react-client/echo';
8
+ import { type Database, Obj } from '@dxos/echo';
12
9
  import { useSelected, useSelectionActions } from '@dxos/react-ui-attention';
13
- import { type DataType, type ProjectionModel } from '@dxos/schema';
10
+ import { type ProjectionModel } from '@dxos/schema';
14
11
  import { isNonNullable } from '@dxos/util';
15
12
 
16
- import { type TableRow, TableModel, type TableModelProps, type TableRowAction } from '../model';
13
+ import {
14
+ TableModel,
15
+ type TableModelProps,
16
+ type TableRow,
17
+ type TableRowAction,
18
+ createEchoChangeCallback,
19
+ } from '../model';
20
+ import { type Table } from '../types';
17
21
 
18
- export type UseTableModelParams<T extends TableRow = TableRow> = {
19
- view?: DataType.View;
20
- schema?: JsonSchemaType;
22
+ export type UseTableModelProps<T extends TableRow = TableRow> = {
23
+ object?: Table.Table;
21
24
  projection?: ProjectionModel;
22
- rows?: Live<T>[];
25
+ db?: Database.Database;
26
+ rows?: T[];
23
27
  rowActions?: TableRowAction[];
24
28
  onSelectionChanged?: (selection: string[]) => void;
25
29
  onRowAction?: (actionId: string, data: T) => void;
26
30
  } & Pick<
27
31
  TableModelProps<T>,
28
- 'features' | 'onInsertRow' | 'onDeleteRows' | 'onDeleteColumn' | 'onCellUpdate' | 'onRowOrderChange'
32
+ 'features' | 'onInsertRow' | 'onDeleteRows' | 'onColumnDelete' | 'onCellUpdate' | 'onRowOrderChange'
29
33
  >;
30
34
 
31
35
  export const useTableModel = <T extends TableRow = TableRow>({
32
- view,
33
- schema,
36
+ object,
34
37
  projection,
38
+ db,
35
39
  rows,
36
40
  rowActions,
37
41
  features,
38
42
  onSelectionChanged,
39
43
  onRowAction,
40
44
  ...props
41
- }: UseTableModelParams<T>): TableModel<T> | undefined => {
42
- const selected = useSelected(view && fullyQualifiedId(view), 'multi');
43
- const initialSelection = useMemo(() => selected, [view]);
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]);
44
49
 
45
50
  const [model, setModel] = useState<TableModel<T>>();
46
51
  useEffect(() => {
47
- if (!view || !schema) {
52
+ if (!object || !projection) {
48
53
  return;
49
54
  }
50
55
 
51
56
  let model: TableModel<T> | undefined;
52
57
  const t = setTimeout(async () => {
53
58
  model = new TableModel<T>({
54
- view,
55
- schema,
59
+ registry,
60
+ object,
56
61
  projection,
62
+ db,
63
+ change: createEchoChangeCallback<T>(object),
57
64
  features,
58
65
  rowActions,
59
66
  initialSelection,
@@ -68,18 +75,13 @@ export const useTableModel = <T extends TableRow = TableRow>({
68
75
  clearTimeout(t);
69
76
  void model?.close();
70
77
  };
71
- }, [view, schema, projection, features, rowActions, initialSelection]); // TODO(burdon): Trigger if callbacks change?
78
+ // TODO(burdon): Trigger if callbacks change?
79
+ }, [registry, object, projection, features, rowActions, initialSelection]);
72
80
 
73
- // Update data.
81
+ // Update data when rows change.
74
82
  useEffect(() => {
75
- if (rows) {
76
- // TODO(ZaymonFC): Remove this workaround once unstable query ordering issue is resolved
77
- /*
78
- * Sort all objects by string id field as a temporary workaround for query ordering issues
79
- * Reference: https://github.com/dxos/dxos/pull/9409
80
- */
81
- const sortedRows = orderBy(rows, [(row) => String(row.id)], ['asc']);
82
- model?.setRows(sortedRows);
83
+ if (rows && model) {
84
+ model.setRows(rows);
83
85
  }
84
86
  }, [model, rows]);
85
87
 
@@ -90,8 +92,8 @@ export const useTableModel = <T extends TableRow = TableRow>({
90
92
  return;
91
93
  }
92
94
 
93
- const unsubscribe = effect(() => {
94
- const selectedItems = [...model.selection.selection.value];
95
+ const unsubscribe = registry.subscribe(model.selection.selectionAtom, () => {
96
+ const selectedItems = [...model.selection.selection];
95
97
  multiSelect(selectedItems);
96
98
  onSelectionChanged?.(selectedItems);
97
99
  });
@@ -101,7 +103,7 @@ export const useTableModel = <T extends TableRow = TableRow>({
101
103
  clear();
102
104
  unsubscribe();
103
105
  };
104
- }, [model, onSelectionChanged]);
106
+ }, [registry, model, onSelectionChanged]);
105
107
 
106
108
  return model;
107
109
  };
@@ -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,89 +2,120 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { computed, effect, type Signal, 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>>;
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 _initialSelection: string[],
42
- private readonly _onSelectionChanged?: () => void,
43
- ) {
44
- super();
45
- this._selection = signal(new Set(this._initialSelection));
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
+ });
46
60
  }
47
61
 
48
62
  protected override async _open(): Promise<void> {
49
- const selectionWatcher = effect(() => {
50
- touch(this._selection.value);
63
+ const selectionUnsubscribe = this._registry.subscribe(this._selection, () => {
51
64
  this._onSelectionChanged?.();
52
65
  });
53
- this._ctx.onDispose(selectionWatcher);
66
+ this._ctx.onDispose(selectionUnsubscribe);
54
67
  }
55
68
 
56
69
  //
57
70
  // Getters
58
71
  //
59
72
 
60
- public get selection(): ReadonlySignal<Set<string>> {
73
+ /** Get the selection atom for reactive access. */
74
+ public get selectionAtom(): Atom.Atom<Set<string>> {
61
75
  return this._selection;
62
76
  }
63
77
 
64
- 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> {
65
85
  return this._hasSelection;
66
86
  }
67
87
 
68
- 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> {
69
95
  return this._allSelected;
70
96
  }
71
97
 
98
+ /** Check if all rows are selected. */
99
+ public get allRowsSelected(): boolean {
100
+ return this._registry.get(this._allSelected);
101
+ }
102
+
72
103
  public get selectionMode(): SelectionMode {
73
104
  return this._selectionMode;
74
105
  }
75
106
 
76
107
  public getSelectedRows = (): T[] => {
77
- const selectedIds = this._validSelectedIds.value;
78
- 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));
79
110
  };
80
111
 
81
112
  public isRowIndexSelected = (rowIndex: number): boolean => {
82
- const row = this._rows.value[rowIndex];
83
- 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);
84
115
  };
85
116
 
86
117
  public isObjectSelected = (object: T): boolean => {
87
- return this._selection.value.has(object.id);
118
+ return this._registry.get(this._selection).has(object.id);
88
119
  };
89
120
 
90
121
  //
@@ -92,8 +123,9 @@ export class SelectionModel<T extends TableRow> extends Resource {
92
123
  //
93
124
 
94
125
  public toggleSelectionForRowIndex = (rowIndex: number): void => {
95
- const row = this._rows.value[rowIndex];
96
- 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);
97
129
 
98
130
  if (newSelection.has(row.id)) {
99
131
  newSelection.delete(row.id);
@@ -104,17 +136,17 @@ export class SelectionModel<T extends TableRow> extends Resource {
104
136
  newSelection.add(row.id);
105
137
  }
106
138
 
107
- this._selection.value = newSelection;
139
+ this._registry.set(this._selection, newSelection);
108
140
  };
109
141
 
110
142
  public setSelection = (mode: 'all' | 'none'): void => {
111
143
  switch (mode) {
112
144
  case 'all': {
113
- 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)));
114
146
  break;
115
147
  }
116
148
  case 'none': {
117
- this._selection.value = new Set();
149
+ this._registry.set(this._selection, new Set());
118
150
  break;
119
151
  }
120
152
  }