@dxos/plugin-space 0.7.5-main.9d26e3a → 0.7.5-main.9d2a38b

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 (200) hide show
  1. package/dist/lib/browser/app-graph-builder-F6XSETHX.mjs +365 -0
  2. package/dist/lib/browser/app-graph-builder-F6XSETHX.mjs.map +7 -0
  3. package/dist/lib/browser/app-graph-serializer-FYJE23GN.mjs +80 -0
  4. package/dist/lib/browser/app-graph-serializer-FYJE23GN.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-IZ7QKQ2E.mjs +523 -0
  6. package/dist/lib/browser/chunk-IZ7QKQ2E.mjs.map +7 -0
  7. package/dist/lib/browser/chunk-SOXNANA6.mjs +12 -0
  8. package/dist/lib/browser/chunk-SOXNANA6.mjs.map +7 -0
  9. package/dist/lib/browser/{chunk-54VE4GTA.mjs → chunk-SSJ772GK.mjs} +9 -12
  10. package/dist/lib/browser/chunk-SSJ772GK.mjs.map +7 -0
  11. package/dist/lib/browser/chunk-T36CIHPG.mjs +133 -0
  12. package/dist/lib/browser/chunk-T36CIHPG.mjs.map +7 -0
  13. package/dist/lib/browser/chunk-UH5P4UL3.mjs +21 -0
  14. package/dist/lib/browser/chunk-UH5P4UL3.mjs.map +7 -0
  15. package/dist/lib/browser/chunk-XXD33C4E.mjs +1672 -0
  16. package/dist/lib/browser/chunk-XXD33C4E.mjs.map +7 -0
  17. package/dist/lib/browser/identity-created-4Q4PFIC5.mjs +28 -0
  18. package/dist/lib/browser/identity-created-4Q4PFIC5.mjs.map +7 -0
  19. package/dist/lib/browser/index.mjs +186 -3481
  20. package/dist/lib/browser/index.mjs.map +4 -4
  21. package/dist/lib/browser/intent-resolver-XRZYCXXX.mjs +459 -0
  22. package/dist/lib/browser/intent-resolver-XRZYCXXX.mjs.map +7 -0
  23. package/dist/lib/browser/meta.json +1 -1
  24. package/dist/lib/browser/react-root-BDOPFJGJ.mjs +28 -0
  25. package/dist/lib/browser/react-root-BDOPFJGJ.mjs.map +7 -0
  26. package/dist/lib/browser/react-surface-EGOL2JBL.mjs +238 -0
  27. package/dist/lib/browser/react-surface-EGOL2JBL.mjs.map +7 -0
  28. package/dist/lib/browser/settings-WF67QZSD.mjs +24 -0
  29. package/dist/lib/browser/settings-WF67QZSD.mjs.map +7 -0
  30. package/dist/lib/browser/spaces-ready-WVU7US3C.mjs +200 -0
  31. package/dist/lib/browser/spaces-ready-WVU7US3C.mjs.map +7 -0
  32. package/dist/lib/browser/state-MS4KYJWI.mjs +47 -0
  33. package/dist/lib/browser/state-MS4KYJWI.mjs.map +7 -0
  34. package/dist/lib/browser/types/index.mjs +6 -6
  35. package/dist/lib/node/app-graph-builder-GKLVZ4PM.cjs +368 -0
  36. package/dist/lib/node/app-graph-builder-GKLVZ4PM.cjs.map +7 -0
  37. package/dist/lib/node/app-graph-serializer-RMTU5YSC.cjs +88 -0
  38. package/dist/lib/node/app-graph-serializer-RMTU5YSC.cjs.map +7 -0
  39. package/dist/lib/node/chunk-56NGXG2A.cjs +41 -0
  40. package/dist/lib/node/chunk-56NGXG2A.cjs.map +7 -0
  41. package/dist/lib/node/chunk-5D4RWKTV.cjs +551 -0
  42. package/dist/lib/node/chunk-5D4RWKTV.cjs.map +7 -0
  43. package/dist/lib/node/chunk-AJRP7AD6.cjs +1669 -0
  44. package/dist/lib/node/chunk-AJRP7AD6.cjs.map +7 -0
  45. package/dist/lib/node/{chunk-46S3JOES.cjs → chunk-AO4EW2RX.cjs} +7 -11
  46. package/dist/lib/node/chunk-AO4EW2RX.cjs.map +7 -0
  47. package/dist/lib/node/chunk-UENH2YBM.cjs +150 -0
  48. package/dist/lib/node/chunk-UENH2YBM.cjs.map +7 -0
  49. package/dist/lib/node/{chunk-YF2AQ7KP.cjs → chunk-YCBBGTFD.cjs} +16 -19
  50. package/dist/lib/node/chunk-YCBBGTFD.cjs.map +7 -0
  51. package/dist/lib/node/identity-created-QQWX7WX3.cjs +44 -0
  52. package/dist/lib/node/identity-created-QQWX7WX3.cjs.map +7 -0
  53. package/dist/lib/node/index.cjs +171 -3469
  54. package/dist/lib/node/index.cjs.map +4 -4
  55. package/dist/lib/node/intent-resolver-3NI6AUAI.cjs +458 -0
  56. package/dist/lib/node/intent-resolver-3NI6AUAI.cjs.map +7 -0
  57. package/dist/lib/node/meta.json +1 -1
  58. package/dist/lib/node/react-root-ZTR2J2I3.cjs +50 -0
  59. package/dist/lib/node/react-root-ZTR2J2I3.cjs.map +7 -0
  60. package/dist/lib/node/react-surface-75KRPQYT.cjs +233 -0
  61. package/dist/lib/node/react-surface-75KRPQYT.cjs.map +7 -0
  62. package/dist/lib/node/{meta.cjs → settings-KOVSPA3S.cjs} +19 -13
  63. package/dist/lib/node/settings-KOVSPA3S.cjs.map +7 -0
  64. package/dist/lib/node/spaces-ready-ILVGUHJH.cjs +211 -0
  65. package/dist/lib/node/spaces-ready-ILVGUHJH.cjs.map +7 -0
  66. package/dist/lib/node/state-4UIOUKLJ.cjs +61 -0
  67. package/dist/lib/node/state-4UIOUKLJ.cjs.map +7 -0
  68. package/dist/lib/node/types/index.cjs +18 -18
  69. package/dist/lib/node/types/index.cjs.map +1 -1
  70. package/dist/lib/node-esm/app-graph-builder-NEHQ5Z63.mjs +366 -0
  71. package/dist/lib/node-esm/app-graph-builder-NEHQ5Z63.mjs.map +7 -0
  72. package/dist/lib/node-esm/app-graph-serializer-UWWS5OVC.mjs +81 -0
  73. package/dist/lib/node-esm/app-graph-serializer-UWWS5OVC.mjs.map +7 -0
  74. package/dist/lib/node-esm/chunk-375RB3CZ.mjs +22 -0
  75. package/dist/lib/node-esm/chunk-375RB3CZ.mjs.map +7 -0
  76. package/dist/lib/node-esm/chunk-5QMAPAZD.mjs +524 -0
  77. package/dist/lib/node-esm/chunk-5QMAPAZD.mjs.map +7 -0
  78. package/dist/lib/node-esm/{chunk-2MNFEB23.mjs → chunk-7FUVU45N.mjs} +3 -6
  79. package/dist/lib/node-esm/chunk-7FUVU45N.mjs.map +7 -0
  80. package/dist/lib/node-esm/chunk-E5DWIQ3N.mjs +134 -0
  81. package/dist/lib/node-esm/chunk-E5DWIQ3N.mjs.map +7 -0
  82. package/dist/lib/node-esm/chunk-GVOPDPS2.mjs +1673 -0
  83. package/dist/lib/node-esm/chunk-GVOPDPS2.mjs.map +7 -0
  84. package/dist/lib/node-esm/{chunk-CDZETPO7.mjs → chunk-HTBGWQEU.mjs} +9 -12
  85. package/dist/lib/node-esm/chunk-HTBGWQEU.mjs.map +7 -0
  86. package/dist/lib/node-esm/identity-created-HMNY2MPB.mjs +29 -0
  87. package/dist/lib/node-esm/identity-created-HMNY2MPB.mjs.map +7 -0
  88. package/dist/lib/node-esm/index.mjs +186 -3481
  89. package/dist/lib/node-esm/index.mjs.map +4 -4
  90. package/dist/lib/node-esm/intent-resolver-P5EVBOGP.mjs +460 -0
  91. package/dist/lib/node-esm/intent-resolver-P5EVBOGP.mjs.map +7 -0
  92. package/dist/lib/node-esm/meta.json +1 -1
  93. package/dist/lib/node-esm/react-root-OUPJA4RY.mjs +29 -0
  94. package/dist/lib/node-esm/react-root-OUPJA4RY.mjs.map +7 -0
  95. package/dist/lib/node-esm/react-surface-7EVWCKIP.mjs +239 -0
  96. package/dist/lib/node-esm/react-surface-7EVWCKIP.mjs.map +7 -0
  97. package/dist/lib/node-esm/settings-EDK6WI3V.mjs +25 -0
  98. package/dist/lib/node-esm/settings-EDK6WI3V.mjs.map +7 -0
  99. package/dist/lib/node-esm/spaces-ready-CH3W7OGN.mjs +201 -0
  100. package/dist/lib/node-esm/spaces-ready-CH3W7OGN.mjs.map +7 -0
  101. package/dist/lib/node-esm/state-BMISGQ2O.mjs +48 -0
  102. package/dist/lib/node-esm/state-BMISGQ2O.mjs.map +7 -0
  103. package/dist/lib/node-esm/types/index.mjs +6 -6
  104. package/dist/types/src/SpacePlugin.d.ts +1 -24
  105. package/dist/types/src/SpacePlugin.d.ts.map +1 -1
  106. package/dist/types/src/capabilities/app-graph-builder.d.ts +181 -0
  107. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
  108. package/dist/types/src/capabilities/app-graph-serializer.d.ts +4 -0
  109. package/dist/types/src/capabilities/app-graph-serializer.d.ts.map +1 -0
  110. package/dist/types/src/capabilities/capabilities.d.ts +15 -0
  111. package/dist/types/src/capabilities/capabilities.d.ts.map +1 -0
  112. package/dist/types/src/capabilities/identity-created.d.ts +4 -0
  113. package/dist/types/src/capabilities/identity-created.d.ts.map +1 -0
  114. package/dist/types/src/capabilities/index.d.ts +195 -0
  115. package/dist/types/src/capabilities/index.d.ts.map +1 -0
  116. package/dist/types/src/capabilities/intent-resolver.d.ts +8 -0
  117. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -0
  118. package/dist/types/src/capabilities/react-root.d.ts +7 -0
  119. package/dist/types/src/capabilities/react-root.d.ts.map +1 -0
  120. package/dist/types/src/capabilities/react-surface.d.ts +7 -0
  121. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  122. package/dist/types/src/capabilities/settings.d.ts +4 -0
  123. package/dist/types/src/capabilities/settings.d.ts.map +1 -0
  124. package/dist/types/src/capabilities/spaces-ready.d.ts +4 -0
  125. package/dist/types/src/capabilities/spaces-ready.d.ts.map +1 -0
  126. package/dist/types/src/capabilities/state.d.ts +5 -0
  127. package/dist/types/src/capabilities/state.d.ts.map +1 -0
  128. package/dist/types/src/components/AdvancedObjectSettings/AdvancedObjectSettings.d.ts +7 -0
  129. package/dist/types/src/components/AdvancedObjectSettings/AdvancedObjectSettings.d.ts.map +1 -0
  130. package/dist/types/src/components/AdvancedObjectSettings/ForeignKeys.d.ts +8 -0
  131. package/dist/types/src/components/AdvancedObjectSettings/ForeignKeys.d.ts.map +1 -0
  132. package/dist/types/src/components/AdvancedObjectSettings/index.d.ts +2 -0
  133. package/dist/types/src/components/AdvancedObjectSettings/index.d.ts.map +1 -0
  134. package/dist/types/src/components/AwaitingObject.d.ts.map +1 -1
  135. package/dist/types/src/components/BaseObjectSettings.d.ts +7 -0
  136. package/dist/types/src/components/BaseObjectSettings.d.ts.map +1 -0
  137. package/dist/types/src/components/CreateDialog/CreateObjectDialog.d.ts +1 -2
  138. package/dist/types/src/components/CreateDialog/CreateObjectDialog.d.ts.map +1 -1
  139. package/dist/types/src/components/CreateDialog/CreateObjectPanel.d.ts +2 -3
  140. package/dist/types/src/components/CreateDialog/CreateObjectPanel.d.ts.map +1 -1
  141. package/dist/types/src/components/SpacePluginSettings.d.ts.map +1 -1
  142. package/dist/types/src/components/SpacePresence.d.ts.map +1 -1
  143. package/dist/types/src/components/index.d.ts +2 -1
  144. package/dist/types/src/components/index.d.ts.map +1 -1
  145. package/dist/types/src/events.d.ts +5 -0
  146. package/dist/types/src/events.d.ts.map +1 -0
  147. package/dist/types/src/index.d.ts +3 -5
  148. package/dist/types/src/index.d.ts.map +1 -1
  149. package/dist/types/src/meta.d.ts +1 -4
  150. package/dist/types/src/meta.d.ts.map +1 -1
  151. package/dist/types/src/translations.d.ts +12 -0
  152. package/dist/types/src/translations.d.ts.map +1 -1
  153. package/dist/types/src/types/types.d.ts +5 -26
  154. package/dist/types/src/types/types.d.ts.map +1 -1
  155. package/dist/types/src/util.d.ts +7 -6
  156. package/dist/types/src/util.d.ts.map +1 -1
  157. package/dist/types/tsconfig.tsbuildinfo +1 -1
  158. package/package.json +37 -46
  159. package/src/SpacePlugin.tsx +115 -1412
  160. package/src/capabilities/app-graph-builder.ts +392 -0
  161. package/src/capabilities/app-graph-serializer.ts +73 -0
  162. package/src/capabilities/capabilities.ts +23 -0
  163. package/src/capabilities/identity-created.ts +26 -0
  164. package/src/capabilities/index.ts +17 -0
  165. package/src/capabilities/intent-resolver.ts +420 -0
  166. package/src/capabilities/react-root.tsx +20 -0
  167. package/src/capabilities/react-surface.tsx +234 -0
  168. package/src/capabilities/settings.ts +17 -0
  169. package/src/capabilities/spaces-ready.ts +231 -0
  170. package/src/capabilities/state.ts +45 -0
  171. package/src/components/AdvancedObjectSettings/AdvancedObjectSettings.tsx +72 -0
  172. package/src/components/AdvancedObjectSettings/ForeignKeys.tsx +51 -0
  173. package/src/components/AdvancedObjectSettings/index.ts +5 -0
  174. package/src/components/AwaitingObject.tsx +4 -10
  175. package/src/components/{DefaultObjectSettings.tsx → BaseObjectSettings.tsx} +2 -2
  176. package/src/components/CreateDialog/CreateObjectDialog.tsx +5 -5
  177. package/src/components/CreateDialog/CreateObjectPanel.tsx +54 -17
  178. package/src/components/SpacePluginSettings.tsx +2 -7
  179. package/src/components/SpacePresence.tsx +6 -5
  180. package/src/components/index.ts +2 -1
  181. package/src/events.ts +12 -0
  182. package/src/index.ts +3 -7
  183. package/src/meta.ts +1 -3
  184. package/src/translations.ts +4 -0
  185. package/src/types/types.ts +11 -49
  186. package/src/util.tsx +31 -25
  187. package/dist/lib/browser/chunk-54VE4GTA.mjs.map +0 -7
  188. package/dist/lib/browser/chunk-73BCBSLP.mjs +0 -15
  189. package/dist/lib/browser/chunk-73BCBSLP.mjs.map +0 -7
  190. package/dist/lib/browser/meta.mjs +0 -11
  191. package/dist/lib/browser/meta.mjs.map +0 -7
  192. package/dist/lib/node/chunk-46S3JOES.cjs.map +0 -7
  193. package/dist/lib/node/chunk-YF2AQ7KP.cjs.map +0 -7
  194. package/dist/lib/node/meta.cjs.map +0 -7
  195. package/dist/lib/node-esm/chunk-2MNFEB23.mjs.map +0 -7
  196. package/dist/lib/node-esm/chunk-CDZETPO7.mjs.map +0 -7
  197. package/dist/lib/node-esm/meta.mjs +0 -12
  198. package/dist/lib/node-esm/meta.mjs.map +0 -7
  199. package/dist/types/src/components/DefaultObjectSettings.d.ts +0 -7
  200. package/dist/types/src/components/DefaultObjectSettings.d.ts.map +0 -1
@@ -0,0 +1,231 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { contributes, createIntent, openIds, type PluginsContext, Capabilities } from '@dxos/app-framework';
6
+ import { EventSubscriptions } from '@dxos/async';
7
+ import { Expando } from '@dxos/echo-schema';
8
+ import { scheduledEffect } from '@dxos/echo-signals/core';
9
+ import { create } from '@dxos/live-object';
10
+ import { log } from '@dxos/log';
11
+ import { AttentionCapabilities } from '@dxos/plugin-attention';
12
+ import { ClientCapabilities } from '@dxos/plugin-client';
13
+ import { EdgeReplicationSetting } from '@dxos/protocols/proto/dxos/echo/metadata';
14
+ import { PublicKey } from '@dxos/react-client';
15
+ import { Filter, FQ_ID_LENGTH, parseFullyQualifiedId, SpaceState } from '@dxos/react-client/echo';
16
+ import { ComplexMap, reduceGroupBy } from '@dxos/util';
17
+
18
+ import { SpaceCapabilities } from './capabilities';
19
+ import { SpaceAction } from '../types';
20
+ import { COMPOSER_SPACE_LOCK, SHARED } from '../util';
21
+
22
+ const ACTIVE_NODE_BROADCAST_INTERVAL = 30_000;
23
+ const WAIT_FOR_OBJECT_TIMEOUT = 1000;
24
+
25
+ export default async (context: PluginsContext) => {
26
+ const subscriptions = new EventSubscriptions();
27
+ const spaceSubscriptions = new EventSubscriptions();
28
+
29
+ const { dispatchPromise: dispatch } = context.requestCapability(Capabilities.IntentDispatcher);
30
+ const { graph } = context.requestCapability(Capabilities.AppGraph);
31
+ const layout = context.requestCapability(Capabilities.Layout);
32
+ const location = context.requestCapability(Capabilities.Location);
33
+ const attention = context.requestCapability(AttentionCapabilities.Attention);
34
+ const state = context.requestCapability(SpaceCapabilities.MutableState);
35
+ const client = context.requestCapability(ClientCapabilities.Client);
36
+
37
+ const defaultSpace = client.spaces.default;
38
+ await defaultSpace.waitUntilReady();
39
+
40
+ // Initialize space sharing lock in default space.
41
+ if (typeof defaultSpace.properties[COMPOSER_SPACE_LOCK] !== 'boolean') {
42
+ defaultSpace.properties[COMPOSER_SPACE_LOCK] = true;
43
+ }
44
+
45
+ const {
46
+ objects: [spacesOrder],
47
+ } = await defaultSpace.db.query(Filter.schema(Expando, { key: SHARED })).run();
48
+ if (!spacesOrder) {
49
+ // TODO(wittjosiah): Cannot be a Folder because Spaces are not TypedObjects so can't be saved in the database.
50
+ // Instead, we store order as an array of space ids.
51
+ defaultSpace.db.add(create({ key: SHARED, order: [] }));
52
+ }
53
+
54
+ // Await missing objects.
55
+ subscriptions.add(
56
+ scheduledEffect(
57
+ () => ({
58
+ layoutMode: layout.layoutMode,
59
+ soloPart: location.active.solo?.[0],
60
+ }),
61
+ ({ layoutMode, soloPart }) => {
62
+ if (layoutMode !== 'solo' || !soloPart) {
63
+ return;
64
+ }
65
+
66
+ const node = graph.findNode(soloPart.id);
67
+ if (!node && soloPart.id.length === FQ_ID_LENGTH) {
68
+ const timeout = setTimeout(async () => {
69
+ const node = graph.findNode(soloPart.id);
70
+ if (!node) {
71
+ await dispatch(createIntent(SpaceAction.WaitForObject, { id: soloPart.id }));
72
+ }
73
+ }, WAIT_FOR_OBJECT_TIMEOUT);
74
+
75
+ return () => clearTimeout(timeout);
76
+ }
77
+ },
78
+ ),
79
+ );
80
+
81
+ // Cache space names.
82
+ subscriptions.add(
83
+ client.spaces.subscribe(async (spaces) => {
84
+ // TODO(wittjosiah): Remove. This is a hack to be able to migrate the default space properties.
85
+ if (defaultSpace.state.get() === SpaceState.SPACE_REQUIRES_MIGRATION) {
86
+ await defaultSpace.internal.migrate();
87
+ }
88
+
89
+ spaces
90
+ .filter((space) => space.state.get() === SpaceState.SPACE_READY)
91
+ .forEach((space) => {
92
+ subscriptions.add(
93
+ scheduledEffect(
94
+ () => ({ name: space.properties.name }),
95
+ ({ name }) => (state.spaceNames[space.id] = name),
96
+ ),
97
+ );
98
+ });
99
+ }).unsubscribe,
100
+ );
101
+
102
+ // Broadcast active node to other peers in the space.
103
+ subscriptions.add(
104
+ scheduledEffect(
105
+ () => ({
106
+ open: openIds(location.active, layout.layoutMode === 'solo' ? ['solo'] : ['main']),
107
+ closed: [...location.closed],
108
+ }),
109
+ ({ open, closed }) => {
110
+ const send = () => {
111
+ const spaces = client.spaces.get();
112
+ const identity = client.halo.identity.get();
113
+ if (identity && location.active) {
114
+ // Group parts by space for efficient messaging.
115
+ const idsBySpace = reduceGroupBy(open, (id) => {
116
+ try {
117
+ const [spaceId] = parseFullyQualifiedId(id);
118
+ return spaceId;
119
+ } catch {
120
+ return null;
121
+ }
122
+ });
123
+
124
+ const removedBySpace = reduceGroupBy(closed, (id) => {
125
+ try {
126
+ const [spaceId] = parseFullyQualifiedId(id);
127
+ return spaceId;
128
+ } catch {
129
+ return null;
130
+ }
131
+ });
132
+
133
+ // NOTE: Ensure all spaces are included so that we send the correct `removed` object arrays.
134
+ for (const space of spaces) {
135
+ if (!idsBySpace.has(space.id)) {
136
+ idsBySpace.set(space.id, []);
137
+ }
138
+ }
139
+
140
+ for (const [spaceId, added] of idsBySpace) {
141
+ const removed = removedBySpace.get(spaceId) ?? [];
142
+ const space = spaces.find((space) => space.id === spaceId);
143
+ if (!space) {
144
+ continue;
145
+ }
146
+
147
+ void space
148
+ .postMessage('viewing', {
149
+ identityKey: identity.identityKey.toHex(),
150
+ attended: attention.current ? [...attention.current] : [],
151
+ added,
152
+ removed,
153
+ })
154
+ // TODO(burdon): This seems defensive; why would this fail? Backoff interval.
155
+ .catch((err) => {
156
+ log.warn('Failed to broadcast active node for presence.', { err: err.message });
157
+ });
158
+ }
159
+ }
160
+ };
161
+
162
+ send();
163
+ // Send at interval to allow peers to expire entries if they become disconnected.
164
+ const interval = setInterval(() => send(), ACTIVE_NODE_BROADCAST_INTERVAL);
165
+ return () => clearInterval(interval);
166
+ },
167
+ ),
168
+ );
169
+
170
+ // Listen for active nodes from other peers in the space.
171
+ subscriptions.add(
172
+ client.spaces.subscribe((spaces) => {
173
+ spaceSubscriptions.clear();
174
+ spaces.forEach((space) => {
175
+ spaceSubscriptions.add(
176
+ space.listen('viewing', (message) => {
177
+ const { added, removed, attended } = message.payload;
178
+
179
+ const identityKey = PublicKey.safeFrom(message.payload.identityKey);
180
+ const currentIdentity = client.halo.identity.get();
181
+ if (
182
+ identityKey &&
183
+ !currentIdentity?.identityKey.equals(identityKey) &&
184
+ Array.isArray(added) &&
185
+ Array.isArray(removed)
186
+ ) {
187
+ added.forEach((id) => {
188
+ if (typeof id === 'string') {
189
+ if (!(id in state.viewersByObject)) {
190
+ state.viewersByObject[id] = new ComplexMap(PublicKey.hash);
191
+ }
192
+ state.viewersByObject[id]!.set(identityKey, {
193
+ lastSeen: Date.now(),
194
+ currentlyAttended: new Set(attended).has(id),
195
+ });
196
+ if (!state.viewersByIdentity.has(identityKey)) {
197
+ state.viewersByIdentity.set(identityKey, new Set());
198
+ }
199
+ state.viewersByIdentity.get(identityKey)!.add(id);
200
+ }
201
+ });
202
+
203
+ removed.forEach((id) => {
204
+ if (typeof id === 'string') {
205
+ state.viewersByObject[id]?.delete(identityKey);
206
+ state.viewersByIdentity.get(identityKey)?.delete(id);
207
+ // It’s okay for these to be empty sets/maps, reduces churn.
208
+ }
209
+ });
210
+ }
211
+ }),
212
+ );
213
+ });
214
+ }).unsubscribe,
215
+ );
216
+
217
+ // Enable edge replication for all spaces.
218
+ try {
219
+ await Promise.all(
220
+ client.spaces.get().map((space) => space.internal.setEdgeReplicationPreference(EdgeReplicationSetting.ENABLED)),
221
+ );
222
+ state.enabledEdgeReplication = true;
223
+ } catch (err) {
224
+ log.catch(err);
225
+ }
226
+
227
+ return contributes(Capabilities.Null, null, () => {
228
+ spaceSubscriptions.clear();
229
+ subscriptions.clear();
230
+ });
231
+ };
@@ -0,0 +1,45 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { effect } from '@preact/signals-core';
6
+
7
+ import { Capabilities, contributes, type PluginsContext } from '@dxos/app-framework';
8
+ import { PublicKey } from '@dxos/keys';
9
+ import { LocalStorageStore } from '@dxos/local-storage';
10
+ import { ComplexMap } from '@dxos/util';
11
+
12
+ import { SpaceCapabilities } from './capabilities';
13
+ import { SPACE_PLUGIN } from '../meta';
14
+ import { type PluginState } from '../types';
15
+
16
+ export default (context: PluginsContext) => {
17
+ const state = new LocalStorageStore<PluginState>(SPACE_PLUGIN, {
18
+ awaiting: undefined,
19
+ spaceNames: {},
20
+ viewersByObject: {},
21
+ // TODO(wittjosiah): Stop using (Complex)Map inside reactive object.
22
+ viewersByIdentity: new ComplexMap(PublicKey.hash),
23
+ sdkMigrationRunning: {},
24
+ navigableCollections: false,
25
+ enabledEdgeReplication: false,
26
+ });
27
+
28
+ state
29
+ .prop({ key: 'spaceNames', type: LocalStorageStore.json<Record<string, string>>() })
30
+ .prop({ key: 'enabledEdgeReplication', type: LocalStorageStore.bool() });
31
+
32
+ const manager = context.requestCapability(Capabilities.PluginManager);
33
+ const unsubscribe = effect(() => {
34
+ // TODO(wittjosiah): Find a way to make this capability-based.
35
+ const enabled = manager.enabled.includes('dxos.org/plugin/stack');
36
+ if (enabled !== state.values.navigableCollections) {
37
+ state.values.navigableCollections = enabled;
38
+ }
39
+ });
40
+
41
+ return contributes(SpaceCapabilities.State, state.values, () => {
42
+ unsubscribe();
43
+ state.close();
44
+ });
45
+ };
@@ -0,0 +1,72 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback, useState } from 'react';
6
+
7
+ import { type ForeignKey, ForeignKeySchema } from '@dxos/echo-schema';
8
+ import { getMeta, type ReactiveEchoObject } from '@dxos/react-client/echo';
9
+ import { IconButton, useTranslation, Separator } from '@dxos/react-ui';
10
+ import { Form } from '@dxos/react-ui-form';
11
+
12
+ import { ForeignKeys } from './ForeignKeys';
13
+ import { SPACE_PLUGIN } from '../../meta';
14
+
15
+ const initialValues = {
16
+ source: '',
17
+ id: '',
18
+ };
19
+
20
+ export type AdvancedObjectSettingsProps = {
21
+ object: ReactiveEchoObject<any>;
22
+ };
23
+
24
+ export const AdvancedObjectSettings = ({ object }: AdvancedObjectSettingsProps) => {
25
+ const { t } = useTranslation(SPACE_PLUGIN);
26
+ const [adding, setAdding] = useState(false);
27
+ const keys = getMeta(object).keys;
28
+
29
+ const handleNew = useCallback(() => setAdding(true), []);
30
+ const handleCancel = useCallback(() => setAdding(false), []);
31
+ const handleSave = useCallback(
32
+ (key: ForeignKey) => {
33
+ const index = keys.findIndex(({ source, id }) => source === key.source && id === key.id);
34
+ if (index === -1) {
35
+ keys.push(key);
36
+ }
37
+ setAdding(false);
38
+ },
39
+ [keys],
40
+ );
41
+ const handleDelete = useCallback(
42
+ (key: ForeignKey) => {
43
+ const index = keys.findIndex(({ source, id }) => source === key.source && id === key.id);
44
+ if (index !== -1) {
45
+ keys.splice(index, 1);
46
+ }
47
+ },
48
+ [keys],
49
+ );
50
+
51
+ // TODO(wittjosiah): This should be wrapped in an "Advanced" accordion.
52
+ return (
53
+ <>
54
+ <Separator />
55
+ <div className='p-2 flex flex-col gap-4'>
56
+ <h2>{t('advanced settings label')}</h2>
57
+ <div className='flex items-center'>
58
+ <h3 className='text-sm font-semibold'>{t('foreign keys')}</h3>
59
+ <div className='grow' />
60
+ <IconButton
61
+ classNames={adding && 'invisible'}
62
+ icon='ph--plus--regular'
63
+ label={t('add key')}
64
+ onClick={handleNew}
65
+ />
66
+ </div>
67
+ {!adding && <ForeignKeys keys={keys} onDelete={handleDelete} />}
68
+ </div>
69
+ {adding && <Form schema={ForeignKeySchema} values={initialValues} onSave={handleSave} onCancel={handleCancel} />}
70
+ </>
71
+ );
72
+ };
@@ -0,0 +1,51 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback } from 'react';
6
+
7
+ import { type ForeignKey } from '@dxos/echo-schema';
8
+ import { IconButton, List, ListItem, useTranslation } from '@dxos/react-ui';
9
+
10
+ import { SPACE_PLUGIN } from '../../meta';
11
+
12
+ export type ForeignKeysProps = {
13
+ keys: ForeignKey[];
14
+ onDelete?: (key: ForeignKey) => void;
15
+ };
16
+
17
+ // TODO(wittjosiah): This is a clone of `TokenManager`. Consider a form variant for arrays of read-only objects.
18
+ export const ForeignKeys = ({ keys, onDelete }: ForeignKeysProps) => {
19
+ return (
20
+ <List classNames='flex flex-col gap-2'>
21
+ {keys.map((key) => (
22
+ <KeyItem key={key.id} forignKey={key} onDelete={onDelete} />
23
+ ))}
24
+ </List>
25
+ );
26
+ };
27
+
28
+ type KeyItemProps = {
29
+ forignKey: ForeignKey;
30
+ onDelete?: (key: ForeignKey) => void;
31
+ };
32
+
33
+ const KeyItem = ({ forignKey, onDelete }: KeyItemProps) => {
34
+ const { t } = useTranslation(SPACE_PLUGIN);
35
+
36
+ const handleDelete = useCallback(() => {
37
+ onDelete?.(forignKey);
38
+ }, [forignKey, onDelete]);
39
+
40
+ return (
41
+ <ListItem.Root classNames='px-2'>
42
+ <ListItem.Heading classNames='flex flex-col grow truncate'>
43
+ <div>{forignKey.source}</div>
44
+ <div className='text-description text-sm truncate'>{forignKey.id}</div>
45
+ </ListItem.Heading>
46
+ <ListItem.Endcap>
47
+ <IconButton iconOnly icon='ph--x--regular' variant='ghost' label={t('delete key')} onClick={handleDelete} />
48
+ </ListItem.Endcap>
49
+ </ListItem.Root>
50
+ );
51
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './AdvancedObjectSettings';
@@ -5,13 +5,7 @@
5
5
  import { CheckCircle, CircleDashed, CircleNotch } from '@phosphor-icons/react';
6
6
  import React, { useCallback, useEffect, useState } from 'react';
7
7
 
8
- import {
9
- useResolvePlugin,
10
- parseNavigationPlugin,
11
- NavigationAction,
12
- useIntentDispatcher,
13
- createIntent,
14
- } from '@dxos/app-framework';
8
+ import { NavigationAction, useIntentDispatcher, createIntent, useCapability, Capabilities } from '@dxos/app-framework';
15
9
  import { useClient } from '@dxos/react-client';
16
10
  import { Filter, fullyQualifiedId, useQuery } from '@dxos/react-client/echo';
17
11
  import { Button, Toast, useTranslation } from '@dxos/react-ui';
@@ -29,7 +23,7 @@ export const AwaitingObject = ({ id }: { id: string }) => {
29
23
  const [found, setFound] = useState(false);
30
24
  const { t } = useTranslation(SPACE_PLUGIN);
31
25
  const { dispatchPromise: dispatch } = useIntentDispatcher();
32
- const navigationPlugin = useResolvePlugin(parseNavigationPlugin);
26
+ const location = useCapability(Capabilities.Location);
33
27
 
34
28
  const client = useClient();
35
29
  const objects = useQuery(client.spaces, Filter.all());
@@ -50,11 +44,11 @@ export const AwaitingObject = ({ id }: { id: string }) => {
50
44
  if (objects.findIndex((object) => fullyQualifiedId(object) === id) > -1) {
51
45
  setFound(true);
52
46
 
53
- if (navigationPlugin?.provides.location.active.solo?.[0].id === id) {
47
+ if (location.active.solo?.[0].id === id) {
54
48
  setOpen(false);
55
49
  }
56
50
  }
57
- }, [id, objects, navigationPlugin]);
51
+ }, [id, objects, location]);
58
52
 
59
53
  const handleClose = useCallback(
60
54
  async () => dispatch(createIntent(SpaceAction.WaitForObject, { id: undefined })),
@@ -9,11 +9,11 @@ import { Input, useTranslation } from '@dxos/react-ui';
9
9
 
10
10
  import { SPACE_PLUGIN } from '../meta';
11
11
 
12
- export type DefaultObjectSettingsProps = {
12
+ export type BaseObjectSettingsProps = {
13
13
  object: ReactiveEchoObject<any>;
14
14
  };
15
15
 
16
- export const DefaultObjectSettings = ({ object }: DefaultObjectSettingsProps) => {
16
+ export const BaseObjectSettings = ({ object }: BaseObjectSettingsProps) => {
17
17
  const { t } = useTranslation(SPACE_PLUGIN);
18
18
  // TODO(burdon): Standardize forms.
19
19
  return (
@@ -5,7 +5,7 @@
5
5
  import { pipe } from 'effect';
6
6
  import React, { useCallback, useRef } from 'react';
7
7
 
8
- import { chain, createIntent, type MetadataResolver, NavigationAction, useIntentDispatcher } from '@dxos/app-framework';
8
+ import { chain, createIntent, NavigationAction, useIntentDispatcher } from '@dxos/app-framework';
9
9
  import { useClient } from '@dxos/react-client';
10
10
  import {
11
11
  getSpace,
@@ -24,7 +24,7 @@ import { CollectionType, SpaceAction } from '../../types';
24
24
  export const CREATE_OBJECT_DIALOG = `${SPACE_PLUGIN}/CreateObjectDialog`;
25
25
 
26
26
  export type CreateObjectDialogProps = Pick<CreateObjectPanelProps, 'schemas' | 'target' | 'typename' | 'name'> & {
27
- resolve?: MetadataResolver;
27
+ resolve?: (typename: string) => Record<string, any>;
28
28
  shouldNavigate?: (object: ReactiveObject<any>) => boolean;
29
29
  };
30
30
 
@@ -46,11 +46,11 @@ export const CreateObjectDialog = ({
46
46
  async ({
47
47
  schema,
48
48
  target: _target,
49
- name,
49
+ data,
50
50
  }: {
51
51
  schema: TypedObject;
52
52
  target: CreateObjectPanelProps['target'];
53
- name?: string;
53
+ data?: Record<string, any>;
54
54
  }) => {
55
55
  const target = isSpace(_target)
56
56
  ? (_target.properties[CollectionType.typename]?.target as CollectionType | undefined)
@@ -65,7 +65,7 @@ export const CreateObjectDialog = ({
65
65
  closeRef.current?.click();
66
66
 
67
67
  const space = isSpace(target) ? target : getSpace(target);
68
- const result = await dispatch(createObjectIntent({ name, space }));
68
+ const result = await dispatch(createObjectIntent(data, { space }));
69
69
  const object = result.data?.object;
70
70
  if (isReactiveObject(object)) {
71
71
  const addObjectIntent = createIntent(SpaceAction.AddObject, { target, object });
@@ -2,13 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { useCallback, useState } from 'react';
5
+ import React, { useCallback, useMemo, useState } from 'react';
6
6
 
7
- import { type MetadataResolver } from '@dxos/app-framework';
7
+ import { Surface, isSurfaceAvailable, usePluginManager } from '@dxos/app-framework';
8
8
  import { type TypedObject, getObjectAnnotation, S } from '@dxos/echo-schema';
9
9
  import { type SpaceId, type Space, isSpace } from '@dxos/react-client/echo';
10
10
  import { Icon, IconButton, Input, toLocalizedString, useTranslation } from '@dxos/react-ui';
11
- import { Form, InputHeader } from '@dxos/react-ui-form';
11
+ import { Form, InputHeader, type InputProps } from '@dxos/react-ui-form';
12
12
  import { SearchList } from '@dxos/react-ui-searchlist';
13
13
  import { nonNullable, type MaybePromise } from '@dxos/util';
14
14
 
@@ -23,14 +23,37 @@ export type CreateObjectPanelProps = {
23
23
  target?: Space | CollectionType;
24
24
  name?: string;
25
25
  defaultSpaceId?: SpaceId;
26
- resolve?: MetadataResolver;
26
+ resolve?: (typename: string) => Record<string, any>;
27
27
  onCreateObject?: (params: {
28
28
  schema: TypedObject;
29
29
  target: Space | CollectionType;
30
- name?: string;
30
+ data: Record<string, any>;
31
31
  }) => MaybePromise<void>;
32
32
  };
33
33
 
34
+ // TODO(ZaymonFC): Move this if you find yourself needing it elsewhere.
35
+ /**
36
+ * Creates a surface input component based on plugin context.
37
+ * @param baseData Additional data that will be merged with form data and passed to the surface.
38
+ * This allows providing more context to the surface than what's available from the form itself.
39
+ */
40
+ const useInputSurfaceLookup = (baseData?: Record<string, any>) => {
41
+ const pluginManager = usePluginManager();
42
+
43
+ return useCallback(
44
+ ({ prop, schema, inputProps }: { prop: string; schema: S.Schema<any>; inputProps: InputProps<any> }) => {
45
+ const composedData = { prop, schema, ...baseData };
46
+
47
+ if (!isSurfaceAvailable(pluginManager.context, { role: 'form-input', data: composedData })) {
48
+ return undefined;
49
+ }
50
+
51
+ return <Surface role='form-input' data={composedData} {...inputProps} />;
52
+ },
53
+ [pluginManager, baseData],
54
+ );
55
+ };
56
+
34
57
  export const CreateObjectPanel = ({
35
58
  schemas,
36
59
  spaces,
@@ -51,16 +74,22 @@ export const CreateObjectPanel = ({
51
74
  const handleClearTarget = useCallback(() => setTarget(undefined), []);
52
75
 
53
76
  const handleCreateObject = useCallback(
54
- async ({ name }: { name?: string }) => {
77
+ async (props: Record<string, any>) => {
55
78
  if (!schema || !target) {
56
79
  return;
57
80
  }
58
-
59
- await onCreateObject?.({ schema, target, name });
81
+ await onCreateObject?.({ schema, target, data: props });
60
82
  },
61
83
  [onCreateObject, schema, target],
62
84
  );
63
85
 
86
+ const metadata = useMemo(() => {
87
+ if (!typename) {
88
+ return;
89
+ }
90
+ return resolve?.(typename);
91
+ }, [resolve, typename]);
92
+
64
93
  // TODO(wittjosiah): All of these inputs should be rolled into a `Form` once it supports the necessary variants.
65
94
  const schemaInput = (
66
95
  <SearchList.Root label={t('schema input label')} classNames='flex flex-col grow overflow-hidden'>
@@ -113,15 +142,23 @@ export const CreateObjectPanel = ({
113
142
  </SearchList.Root>
114
143
  );
115
144
 
116
- const form = (
117
- <Form
118
- autoFocus
119
- values={{ name: initialName }}
120
- schema={S.Struct({ name: S.optional(S.String) })}
121
- testId='create-object-form'
122
- onSave={handleCreateObject}
123
- />
124
- );
145
+ const inputSurfaceLookup = useInputSurfaceLookup({ target });
146
+
147
+ const form = useMemo(() => {
148
+ // TODO(ZaymonFC): Move this default object creation schema somewhere?
149
+ const schema = (metadata?.creationSchema ?? S.Struct({ name: S.optional(S.String) })) as S.Schema<any>;
150
+
151
+ return (
152
+ <Form
153
+ autoFocus
154
+ values={{ name: initialName }}
155
+ schema={schema}
156
+ testId='create-object-form'
157
+ onSave={handleCreateObject}
158
+ lookupComponent={inputSurfaceLookup}
159
+ />
160
+ );
161
+ }, [initialName, handleCreateObject, metadata]);
125
162
 
126
163
  return (
127
164
  <div role='form' className='flex flex-col gap-2'>
@@ -4,24 +4,19 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { createIntent, useIntentDispatcher } from '@dxos/app-framework';
8
7
  import { Input, useTranslation } from '@dxos/react-ui';
9
8
  import { DeprecatedFormInput } from '@dxos/react-ui-form';
10
9
 
11
10
  import { SPACE_PLUGIN } from '../meta';
12
- import { SpaceAction, type SpaceSettingsProps } from '../types';
11
+ import { type SpaceSettingsProps } from '../types';
13
12
 
14
13
  export const SpacePluginSettings = ({ settings }: { settings: SpaceSettingsProps }) => {
15
14
  const { t } = useTranslation(SPACE_PLUGIN);
16
- const { dispatchPromise: dispatch } = useIntentDispatcher();
17
15
 
18
16
  return (
19
17
  <>
20
18
  <DeprecatedFormInput label={t('show hidden spaces label')}>
21
- <Input.Switch
22
- checked={settings.showHidden}
23
- onCheckedChange={(checked) => dispatch(createIntent(SpaceAction.ToggleHidden, { state: !!checked }))}
24
- />
19
+ <Input.Switch checked={settings.showHidden} onCheckedChange={(checked) => (settings.showHidden = !!checked)} />
25
20
  </DeprecatedFormInput>
26
21
  </>
27
22
  );