@dxos/plugin-space 0.6.13 → 0.6.14-main.1366248

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 (108) hide show
  1. package/dist/lib/browser/{chunk-LZEGRS7H.mjs → chunk-AVLRQF6L.mjs} +1 -1
  2. package/dist/lib/browser/chunk-AVLRQF6L.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-DTVUOG2C.mjs → chunk-WZAM3FNP.mjs} +24 -5
  4. package/dist/lib/browser/chunk-WZAM3FNP.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +852 -348
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/meta.mjs +1 -1
  9. package/dist/lib/browser/types/index.mjs +7 -3
  10. package/dist/lib/node/{chunk-CVZPI2P3.cjs → chunk-HTAM5LQD.cjs} +30 -9
  11. package/dist/lib/node/chunk-HTAM5LQD.cjs.map +7 -0
  12. package/dist/lib/node/{chunk-6CNYF6YU.cjs → chunk-P4XUXM7Y.cjs} +4 -4
  13. package/dist/lib/node/chunk-P4XUXM7Y.cjs.map +7 -0
  14. package/dist/lib/node/index.cjs +1046 -550
  15. package/dist/lib/node/index.cjs.map +4 -4
  16. package/dist/lib/node/meta.cjs +5 -5
  17. package/dist/lib/node/meta.cjs.map +1 -1
  18. package/dist/lib/node/meta.json +1 -1
  19. package/dist/lib/node/types/index.cjs +14 -10
  20. package/dist/lib/node/types/index.cjs.map +2 -2
  21. package/dist/lib/node-esm/chunk-TRJKV4PK.mjs +116 -0
  22. package/dist/lib/node-esm/chunk-TRJKV4PK.mjs.map +7 -0
  23. package/dist/lib/node-esm/chunk-YPQGKWHJ.mjs +37 -0
  24. package/dist/lib/node-esm/chunk-YPQGKWHJ.mjs.map +7 -0
  25. package/dist/lib/node-esm/index.mjs +3145 -0
  26. package/dist/lib/node-esm/index.mjs.map +7 -0
  27. package/dist/lib/node-esm/meta.json +1 -0
  28. package/dist/lib/node-esm/meta.mjs +14 -0
  29. package/dist/lib/node-esm/meta.mjs.map +7 -0
  30. package/dist/lib/node-esm/types/index.mjs +26 -0
  31. package/dist/lib/node-esm/types/index.mjs.map +7 -0
  32. package/dist/types/src/SpacePlugin.d.ts.map +1 -1
  33. package/dist/types/src/components/DefaultObjectSettings.d.ts +7 -0
  34. package/dist/types/src/components/DefaultObjectSettings.d.ts.map +1 -0
  35. package/dist/types/src/components/MenuFooter.d.ts +1 -1
  36. package/dist/types/src/components/MenuFooter.d.ts.map +1 -1
  37. package/dist/types/src/components/ShareSpaceButton.stories.d.ts +3 -91
  38. package/dist/types/src/components/ShareSpaceButton.stories.d.ts.map +1 -1
  39. package/dist/types/src/components/SpaceMain/SpaceMain.d.ts.map +1 -1
  40. package/dist/types/src/components/SpacePresence.d.ts +4 -2
  41. package/dist/types/src/components/SpacePresence.d.ts.map +1 -1
  42. package/dist/types/src/components/SpacePresence.stories.d.ts +4 -92
  43. package/dist/types/src/components/SpacePresence.stories.d.ts.map +1 -1
  44. package/dist/types/src/components/SpaceSettings.d.ts.map +1 -1
  45. package/dist/types/src/components/SpaceSettingsPanel.d.ts +7 -0
  46. package/dist/types/src/components/SpaceSettingsPanel.d.ts.map +1 -0
  47. package/dist/types/src/components/SyncStatus/Space.d.ts +8 -0
  48. package/dist/types/src/components/SyncStatus/Space.d.ts.map +1 -0
  49. package/dist/types/src/components/SyncStatus/SyncStatus.d.ts +14 -0
  50. package/dist/types/src/components/SyncStatus/SyncStatus.d.ts.map +1 -0
  51. package/dist/types/src/components/SyncStatus/SyncStatus.stories.d.ts +9 -0
  52. package/dist/types/src/components/SyncStatus/SyncStatus.stories.d.ts.map +1 -0
  53. package/dist/types/src/components/SyncStatus/index.d.ts +2 -0
  54. package/dist/types/src/components/SyncStatus/index.d.ts.map +1 -0
  55. package/dist/types/src/components/SyncStatus/save-tracker.d.ts +3 -0
  56. package/dist/types/src/components/SyncStatus/save-tracker.d.ts.map +1 -0
  57. package/dist/types/src/components/SyncStatus/status.d.ts +9 -0
  58. package/dist/types/src/components/SyncStatus/status.d.ts.map +1 -0
  59. package/dist/types/src/components/SyncStatus/sync-state.d.ts +14 -0
  60. package/dist/types/src/components/SyncStatus/sync-state.d.ts.map +1 -0
  61. package/dist/types/src/components/index.d.ts +3 -3
  62. package/dist/types/src/components/index.d.ts.map +1 -1
  63. package/dist/types/src/meta.d.ts.map +1 -1
  64. package/dist/types/src/translations.d.ts +14 -0
  65. package/dist/types/src/translations.d.ts.map +1 -1
  66. package/dist/types/src/types/thread.d.ts +15 -1
  67. package/dist/types/src/types/thread.d.ts.map +1 -1
  68. package/dist/types/src/types/types.d.ts +23 -2
  69. package/dist/types/src/types/types.d.ts.map +1 -1
  70. package/dist/types/src/util.d.ts +4 -7
  71. package/dist/types/src/util.d.ts.map +1 -1
  72. package/package.json +46 -45
  73. package/src/SpacePlugin.tsx +275 -137
  74. package/src/components/DefaultObjectSettings.tsx +33 -0
  75. package/src/components/MenuFooter.tsx +2 -2
  76. package/src/components/ShareSpaceButton.stories.tsx +11 -7
  77. package/src/components/SpaceMain/SpaceMain.tsx +1 -22
  78. package/src/components/SpacePresence.stories.tsx +11 -9
  79. package/src/components/SpacePresence.tsx +34 -23
  80. package/src/components/SpaceSettings.tsx +35 -6
  81. package/src/components/SpaceSettingsPanel.tsx +69 -0
  82. package/src/components/SyncStatus/Space.tsx +109 -0
  83. package/src/components/SyncStatus/SyncStatus.stories.tsx +74 -0
  84. package/src/components/SyncStatus/SyncStatus.tsx +102 -0
  85. package/src/components/SyncStatus/index.ts +5 -0
  86. package/src/components/SyncStatus/save-tracker.ts +71 -0
  87. package/src/components/SyncStatus/status.ts +44 -0
  88. package/src/components/SyncStatus/sync-state.ts +77 -0
  89. package/src/components/index.ts +3 -3
  90. package/src/meta.ts +3 -1
  91. package/src/translations.ts +16 -2
  92. package/src/types/collection.ts +1 -1
  93. package/src/types/thread.ts +12 -2
  94. package/src/types/types.ts +31 -3
  95. package/src/util.tsx +23 -58
  96. package/dist/lib/browser/chunk-DTVUOG2C.mjs.map +0 -7
  97. package/dist/lib/browser/chunk-LZEGRS7H.mjs.map +0 -7
  98. package/dist/lib/node/chunk-6CNYF6YU.cjs.map +0 -7
  99. package/dist/lib/node/chunk-CVZPI2P3.cjs.map +0 -7
  100. package/dist/types/src/components/EmptySpace.d.ts +0 -3
  101. package/dist/types/src/components/EmptySpace.d.ts.map +0 -1
  102. package/dist/types/src/components/EmptyTree.d.ts +0 -3
  103. package/dist/types/src/components/EmptyTree.d.ts.map +0 -1
  104. package/dist/types/src/components/MissingObject.d.ts +0 -5
  105. package/dist/types/src/components/MissingObject.d.ts.map +0 -1
  106. package/src/components/EmptySpace.tsx +0 -25
  107. package/src/components/EmptyTree.tsx +0 -25
  108. package/src/components/MissingObject.tsx +0 -54
@@ -2,63 +2,67 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { type IconProps, Plus, SignIn, CardsThree, Warning } from '@phosphor-icons/react';
6
- import { effect, signal } from '@preact/signals-core';
5
+ import { signal } from '@preact/signals-core';
7
6
  import React from 'react';
8
7
 
9
8
  import {
9
+ type GraphProvides,
10
10
  type IntentDispatcher,
11
11
  type IntentPluginProvides,
12
12
  LayoutAction,
13
- Surface,
13
+ type LayoutProvides,
14
14
  type LocationProvides,
15
15
  NavigationAction,
16
16
  type Plugin,
17
17
  type PluginDefinition,
18
- openIds,
18
+ Surface,
19
19
  firstIdInPart,
20
+ openIds,
21
+ parseGraphPlugin,
20
22
  parseIntentPlugin,
21
- parseNavigationPlugin,
23
+ parseLayoutPlugin,
22
24
  parseMetadataResolverPlugin,
25
+ parseNavigationPlugin,
23
26
  resolvePlugin,
24
- parseGraphPlugin,
25
27
  } from '@dxos/app-framework';
26
28
  import { EventSubscriptions, type Trigger, type UnsubscribeCallback } from '@dxos/async';
27
- import { type Identifiable, isReactiveObject, type EchoReactiveObject } from '@dxos/echo-schema';
29
+ import { type HasId, isReactiveObject } from '@dxos/echo-schema';
30
+ import { scheduledEffect } from '@dxos/echo-signals/core';
28
31
  import { LocalStorageStore } from '@dxos/local-storage';
29
32
  import { log } from '@dxos/log';
30
33
  import { Migrations } from '@dxos/migrations';
31
34
  import { type AttentionPluginProvides, parseAttentionPlugin } from '@dxos/plugin-attention';
32
35
  import { type ClientPluginProvides, parseClientPlugin } from '@dxos/plugin-client';
33
- import { createExtension, isGraphNode, memoize, type Node, toSignal } from '@dxos/plugin-graph';
36
+ import { type Node, createExtension, memoize, toSignal } from '@dxos/plugin-graph';
34
37
  import { ObservabilityAction } from '@dxos/plugin-observability/meta';
35
38
  import { type Client, PublicKey } from '@dxos/react-client';
36
39
  import {
40
+ type EchoReactiveObject,
41
+ Expando,
42
+ Filter,
37
43
  type PropertiesTypeProps,
38
44
  type Space,
45
+ SpaceState,
39
46
  create,
40
- Expando,
41
- Filter,
42
47
  fullyQualifiedId,
43
48
  getSpace,
44
49
  getTypename,
45
50
  isEchoObject,
46
51
  isSpace,
47
52
  loadObjectReferences,
48
- SpaceState,
53
+ parseId,
54
+ FQ_ID_LENGTH,
49
55
  } from '@dxos/react-client/echo';
50
56
  import { Dialog } from '@dxos/react-ui';
51
- import { InvitationManager, type InvitationManagerProps, osTranslations, ClipboardProvider } from '@dxos/shell/react';
57
+ import { ClipboardProvider, InvitationManager, type InvitationManagerProps, osTranslations } from '@dxos/shell/react';
52
58
  import { ComplexMap, nonNullable, reduceGroupBy } from '@dxos/util';
53
59
 
54
60
  import {
55
61
  AwaitingObject,
56
62
  CollectionMain,
57
63
  CollectionSection,
58
- EmptySpace,
59
- EmptyTree,
64
+ DefaultObjectSettings,
60
65
  MenuFooter,
61
- MissingObject,
62
66
  PopoverRenameObject,
63
67
  PopoverRenameSpace,
64
68
  ShareSpaceButton,
@@ -66,10 +70,12 @@ import {
66
70
  SmallPresenceLive,
67
71
  SpacePresence,
68
72
  SpaceSettings,
73
+ SpaceSettingsPanel,
74
+ SyncStatus,
69
75
  } from './components';
70
76
  import meta, { SPACE_PLUGIN, SpaceAction } from './meta';
71
77
  import translations from './translations';
72
- import { CollectionType, type SpacePluginProvides, type SpaceSettingsProps, type PluginState } from './types';
78
+ import { CollectionType, type PluginState, type SpacePluginProvides, type SpaceSettingsProps } from './types';
73
79
  import {
74
80
  COMPOSER_SPACE_LOCK,
75
81
  SHARED,
@@ -87,7 +93,6 @@ import {
87
93
  } from './util';
88
94
 
89
95
  const ACTIVE_NODE_BROADCAST_INTERVAL = 30_000;
90
- const OBJECT_ID_LENGTH = 60; // 33 (space id) + 26 (object id) + 1 (separator).
91
96
  const SPACE_MAX_OBJECTS = 500;
92
97
  // https://stackoverflow.com/a/19016910
93
98
  const DIRECTORY_TYPE = 'text/directory';
@@ -118,11 +123,14 @@ export const SpacePlugin = ({
118
123
  firstRun,
119
124
  onFirstRun,
120
125
  }: SpacePluginOptions = {}): PluginDefinition<SpacePluginProvides> => {
121
- const settings = new LocalStorageStore<SpaceSettingsProps>(SPACE_PLUGIN);
126
+ const settings = new LocalStorageStore<SpaceSettingsProps>(SPACE_PLUGIN, {
127
+ onSpaceCreate: 'dxos.org/plugin/markdown/action/create',
128
+ });
122
129
  const state = new LocalStorageStore<PluginState>(SPACE_PLUGIN, {
123
130
  awaiting: undefined,
124
131
  spaceNames: {},
125
132
  viewersByObject: {},
133
+ // TODO(wittjosiah): Stop using (Complex)Map inside reactive object.
126
134
  viewersByIdentity: new ComplexMap(PublicKey.hash),
127
135
  sdkMigrationRunning: {},
128
136
  });
@@ -131,17 +139,22 @@ export const SpacePlugin = ({
131
139
  const graphSubscriptions = new Map<string, UnsubscribeCallback>();
132
140
 
133
141
  let clientPlugin: Plugin<ClientPluginProvides> | undefined;
142
+ let graphPlugin: Plugin<GraphProvides> | undefined;
134
143
  let intentPlugin: Plugin<IntentPluginProvides> | undefined;
144
+ let layoutPlugin: Plugin<LayoutProvides> | undefined;
135
145
  let navigationPlugin: Plugin<LocationProvides> | undefined;
136
146
  let attentionPlugin: Plugin<AttentionPluginProvides> | undefined;
137
147
 
138
148
  const onSpaceReady = async () => {
139
- if (!clientPlugin || !navigationPlugin || !attentionPlugin) {
149
+ if (!clientPlugin || !intentPlugin || !graphPlugin || !navigationPlugin || !layoutPlugin || !attentionPlugin) {
140
150
  return;
141
151
  }
142
152
 
143
153
  const client = clientPlugin.provides.client;
154
+ const dispatch = intentPlugin.provides.intent.dispatch;
155
+ const graph = graphPlugin.provides.graph;
144
156
  const location = navigationPlugin.provides.location;
157
+ const layout = layoutPlugin.provides.layout;
145
158
  const attention = attentionPlugin.provides.attention;
146
159
  const defaultSpace = client.spaces.default;
147
160
 
@@ -159,6 +172,26 @@ export const SpacePlugin = ({
159
172
  defaultSpace.db.add(create({ key: SHARED, order: [] }));
160
173
  }
161
174
 
175
+ // Await missing objects.
176
+ subscriptions.add(
177
+ scheduledEffect(
178
+ () => ({
179
+ layoutMode: layout.layoutMode,
180
+ soloPart: location.active.solo?.[0],
181
+ }),
182
+ ({ layoutMode, soloPart }) => {
183
+ if (layoutMode !== 'solo' || !soloPart) {
184
+ return;
185
+ }
186
+
187
+ const node = graph.findNode(soloPart.id);
188
+ if (!node && soloPart.id.length === FQ_ID_LENGTH) {
189
+ void dispatch({ plugin: SPACE_PLUGIN, action: SpaceAction.WAIT_FOR_OBJECT, data: { id: soloPart.id } });
190
+ }
191
+ },
192
+ ),
193
+ );
194
+
162
195
  // Cache space names.
163
196
  subscriptions.add(
164
197
  client.spaces.subscribe(async (spaces) => {
@@ -171,9 +204,10 @@ export const SpacePlugin = ({
171
204
  .filter((space) => space.state.get() === SpaceState.SPACE_READY)
172
205
  .forEach((space) => {
173
206
  subscriptions.add(
174
- effect(() => {
175
- state.values.spaceNames[space.id] = space.properties.name;
176
- }),
207
+ scheduledEffect(
208
+ () => ({ name: space.properties.name }),
209
+ ({ name }) => (state.values.spaceNames[space.id] = name),
210
+ ),
177
211
  );
178
212
  });
179
213
  }).unsubscribe,
@@ -181,54 +215,56 @@ export const SpacePlugin = ({
181
215
 
182
216
  // Broadcast active node to other peers in the space.
183
217
  subscriptions.add(
184
- effect(() => {
185
- const send = () => {
186
- const spaces = client.spaces.get();
187
- const identity = client.halo.identity.get();
188
- if (identity && location.active) {
189
- const ids = openIds(location.active);
190
-
191
- // Group parts by space for efficient messaging.
192
- const idsBySpace = reduceGroupBy(ids, (id) => {
193
- const [spaceId] = id.split(':'); // TODO(burdon): Factor out.
194
- return spaceId;
195
- });
196
-
197
- // NOTE: Ensure all spaces are included so that we send the correct `removed` object arrays.
198
- for (const space of spaces) {
199
- if (!idsBySpace.has(space.id)) {
200
- idsBySpace.set(space.id, []);
218
+ scheduledEffect(
219
+ () => ({
220
+ ids: openIds(location.active),
221
+ removed: location.closed ? [location.closed].flat() : [],
222
+ }),
223
+ ({ ids, removed }) => {
224
+ const send = () => {
225
+ const spaces = client.spaces.get();
226
+ const identity = client.halo.identity.get();
227
+ if (identity && location.active) {
228
+ // Group parts by space for efficient messaging.
229
+ const idsBySpace = reduceGroupBy(ids, (id) => {
230
+ const [spaceId] = id.split(':'); // TODO(burdon): Factor out.
231
+ return spaceId;
232
+ });
233
+
234
+ // NOTE: Ensure all spaces are included so that we send the correct `removed` object arrays.
235
+ for (const space of spaces) {
236
+ if (!idsBySpace.has(space.id)) {
237
+ idsBySpace.set(space.id, []);
238
+ }
201
239
  }
202
- }
203
240
 
204
- for (const [spaceId, ids] of idsBySpace) {
205
- const space = spaces.find((space) => space.id === spaceId);
206
- if (!space) {
207
- continue;
208
- }
241
+ for (const [spaceId, ids] of idsBySpace) {
242
+ const space = spaces.find((space) => space.id === spaceId);
243
+ if (!space) {
244
+ continue;
245
+ }
209
246
 
210
- const removed = location.closed ? [location.closed].flat() : [];
211
-
212
- void space
213
- .postMessage('viewing', {
214
- identityKey: identity.identityKey.toHex(),
215
- attended: attention.attended ? [...attention.attended] : [],
216
- added: ids,
217
- // TODO(Zan): When we re-open a part, we should remove it from the removed list in the navigation plugin.
218
- removed: removed.filter((id) => !ids.includes(id)),
219
- })
220
- // TODO(burdon): This seems defensive; why would this fail? Backoff interval.
221
- .catch((err) => {
222
- log.warn('Failed to broadcast active node for presence.', { err: err.message });
223
- });
247
+ void space
248
+ .postMessage('viewing', {
249
+ identityKey: identity.identityKey.toHex(),
250
+ attended: attention.attended ? [...attention.attended] : [],
251
+ added: ids,
252
+ // TODO(Zan): When we re-open a part, we should remove it from the removed list in the navigation plugin.
253
+ removed: removed.filter((id) => !ids.includes(id)),
254
+ })
255
+ // TODO(burdon): This seems defensive; why would this fail? Backoff interval.
256
+ .catch((err) => {
257
+ log.warn('Failed to broadcast active node for presence.', { err: err.message });
258
+ });
259
+ }
224
260
  }
225
- }
226
- };
261
+ };
227
262
 
228
- send();
229
- const interval = setInterval(() => send(), ACTIVE_NODE_BROADCAST_INTERVAL);
230
- return () => clearInterval(interval);
231
- }),
263
+ send();
264
+ const interval = setInterval(() => send(), ACTIVE_NODE_BROADCAST_INTERVAL);
265
+ return () => clearInterval(interval);
266
+ },
267
+ ),
232
268
  );
233
269
 
234
270
  // Listen for active nodes from other peers in the space.
@@ -276,18 +312,11 @@ export const SpacePlugin = ({
276
312
  return {
277
313
  meta,
278
314
  ready: async (plugins) => {
279
- settings.prop({
280
- key: 'showHidden',
281
- storageKey: 'show-hidden',
282
- type: LocalStorageStore.bool({ allowUndefined: true }),
283
- });
284
-
285
- state.prop({
286
- key: 'spaceNames',
287
- storageKey: 'space-names',
288
- type: LocalStorageStore.json<Record<string, string>>(),
289
- });
315
+ settings.prop({ key: 'showHidden', type: LocalStorageStore.bool({ allowUndefined: true }) });
316
+ state.prop({ key: 'spaceNames', type: LocalStorageStore.json<Record<string, string>>() });
290
317
 
318
+ graphPlugin = resolvePlugin(plugins, parseGraphPlugin);
319
+ layoutPlugin = resolvePlugin(plugins, parseLayoutPlugin);
291
320
  navigationPlugin = resolvePlugin(plugins, parseNavigationPlugin);
292
321
  attentionPlugin = resolvePlugin(plugins, parseAttentionPlugin);
293
322
  clientPlugin = resolvePlugin(plugins, parseClientPlugin);
@@ -310,19 +339,20 @@ export const SpacePlugin = ({
310
339
  await onFirstRun?.({ client, dispatch });
311
340
  };
312
341
 
313
- // No need to unsubscribe because this observable completes when spaces are ready.
314
- client.spaces.isReady.subscribe(async (ready) => {
315
- if (ready) {
316
- await clientPlugin?.provides.client.spaces.default.waitUntilReady();
317
- if (firstRun) {
318
- void firstRun?.wait().then(handleFirstRun);
319
- } else {
320
- await handleFirstRun();
321
- }
342
+ subscriptions.add(
343
+ client.spaces.isReady.subscribe(async (ready) => {
344
+ if (ready) {
345
+ await clientPlugin?.provides.client.spaces.default.waitUntilReady();
346
+ if (firstRun) {
347
+ void firstRun?.wait().then(handleFirstRun);
348
+ } else {
349
+ await handleFirstRun();
350
+ }
322
351
 
323
- await onSpaceReady();
324
- }
325
- });
352
+ await onSpaceReady();
353
+ }
354
+ }).unsubscribe,
355
+ );
326
356
  },
327
357
  unload: async () => {
328
358
  settings.close();
@@ -335,13 +365,17 @@ export const SpacePlugin = ({
335
365
  space: state.values,
336
366
  settings: settings.values,
337
367
  translations: [...translations, osTranslations],
368
+ complementary: {
369
+ panels: [
370
+ { id: 'settings', label: ['open settings panel label', { ns: SPACE_PLUGIN }], icon: 'ph--gear--regular' },
371
+ ],
372
+ },
338
373
  root: () => (state.values.awaiting ? <AwaitingObject id={state.values.awaiting} /> : null),
339
374
  metadata: {
340
375
  records: {
341
376
  [CollectionType.typename]: {
342
377
  placeholder: ['unnamed collection label', { ns: SPACE_PLUGIN }],
343
- icon: (props: IconProps) => <CardsThree {...props} />,
344
- iconSymbol: 'ph--cards-three--regular',
378
+ icon: 'ph--cards-three--regular',
345
379
  // TODO(wittjosiah): Move out of metadata.
346
380
  loadReferences: (collection: CollectionType) =>
347
381
  loadObjectReferences(collection, (collection) => [
@@ -356,28 +390,27 @@ export const SpacePlugin = ({
356
390
  },
357
391
  surface: {
358
392
  component: ({ data, role, ...rest }) => {
359
- const primary = data.active ?? data.object;
360
393
  switch (role) {
361
394
  case 'article':
362
- case 'main':
363
395
  // TODO(wittjosiah): Need to avoid shotgun parsing space state everywhere.
364
- return isSpace(primary) && primary.state.get() === SpaceState.SPACE_READY ? (
365
- <Surface data={{ active: primary.properties[CollectionType.typename] }} role={role} {...rest} />
366
- ) : primary instanceof CollectionType ? (
367
- { node: <CollectionMain collection={primary} />, disposition: 'fallback' }
368
- ) : typeof primary === 'string' && primary.length === OBJECT_ID_LENGTH ? (
369
- <MissingObject id={primary} />
396
+ return isSpace(data.object) && data.object.state.get() === SpaceState.SPACE_READY ? (
397
+ <Surface
398
+ data={{ object: data.object.properties[CollectionType.typename], id: data.object.id }}
399
+ role={role}
400
+ {...rest}
401
+ />
402
+ ) : data.object instanceof CollectionType ? (
403
+ {
404
+ node: <CollectionMain collection={data.object} />,
405
+ disposition: 'fallback',
406
+ }
407
+ ) : null;
408
+ case 'complementary--settings':
409
+ return isSpace(data.subject) ? (
410
+ <SpaceSettingsPanel space={data.subject} />
411
+ ) : isEchoObject(data.subject) ? (
412
+ { node: <DefaultObjectSettings object={data.subject} />, disposition: 'fallback' }
370
413
  ) : null;
371
- // TODO(burdon): Add role name syntax to minimal plugin docs.
372
- case 'tree--empty':
373
- switch (true) {
374
- case data.plugin === SPACE_PLUGIN:
375
- return <EmptyTree />;
376
- case isGraphNode(data.activeNode) && isSpace(data.activeNode.data):
377
- return <EmptySpace />;
378
- default:
379
- return null;
380
- }
381
414
  case 'dialog':
382
415
  if (data.component === 'dxos.org/plugin/space/InvitationManagerDialog') {
383
416
  return (
@@ -387,10 +420,9 @@ export const SpacePlugin = ({
387
420
  </ClipboardProvider>
388
421
  </Dialog.Content>
389
422
  );
390
- } else {
391
- return null;
392
423
  }
393
- case 'popover':
424
+ return null;
425
+ case 'popover': {
394
426
  if (data.component === 'dxos.org/plugin/space/RenameSpacePopover' && isSpace(data.subject)) {
395
427
  return <PopoverRenameSpace space={data.subject} />;
396
428
  }
@@ -398,11 +430,16 @@ export const SpacePlugin = ({
398
430
  return <PopoverRenameObject object={data.subject} />;
399
431
  }
400
432
  return null;
433
+ }
434
+ // TODO(burdon): Add role name syntax to minimal plugin docs.
401
435
  case 'presence--glyph': {
402
436
  return isReactiveObject(data.object) ? (
403
- <SmallPresenceLive viewers={state.values.viewersByObject[fullyQualifiedId(data.object)]} />
437
+ <SmallPresenceLive
438
+ id={data.id as string}
439
+ viewers={state.values.viewersByObject[fullyQualifiedId(data.object)]}
440
+ />
404
441
  ) : (
405
- <SmallPresence count={0} />
442
+ <SmallPresence id={data.id as string} count={0} />
406
443
  );
407
444
  }
408
445
  case 'navbar-start': {
@@ -419,6 +456,7 @@ export const SpacePlugin = ({
419
456
  ? (space?.properties[CollectionType.typename] as CollectionType)
420
457
  : undefined
421
458
  : data.object;
459
+
422
460
  return space && object
423
461
  ? {
424
462
  node: (
@@ -436,11 +474,14 @@ export const SpacePlugin = ({
436
474
  case 'settings':
437
475
  return data.plugin === meta.id ? <SpaceSettings settings={settings.values} /> : null;
438
476
  case 'menu-footer':
439
- if (!isEchoObject(data.object)) {
440
- return null;
441
- } else {
477
+ if (isEchoObject(data.object)) {
442
478
  return <MenuFooter object={data.object} />;
479
+ } else {
480
+ return null;
443
481
  }
482
+ case 'status': {
483
+ return <SyncStatus />;
484
+ }
444
485
  default:
445
486
  return null;
446
487
  }
@@ -456,8 +497,7 @@ export const SpacePlugin = ({
456
497
  const dispatch = intentPlugin?.provides.intent.dispatch;
457
498
  const resolve = metadataPlugin?.provides.metadata.resolver;
458
499
  const graph = graphPlugin?.provides.graph;
459
-
460
- if (!graph || !dispatch || !resolve || !client) {
500
+ if (!client || !dispatch || !resolve || !graph) {
461
501
  return [];
462
502
  }
463
503
 
@@ -491,7 +531,6 @@ export const SpacePlugin = ({
491
531
  type: SPACES,
492
532
  properties: {
493
533
  label: ['spaces label', { ns: SPACE_PLUGIN }],
494
- palette: 'teal',
495
534
  testId: 'spacePlugin.spaces',
496
535
  role: 'branch',
497
536
  childrenPersistenceClass: 'echo',
@@ -540,10 +579,10 @@ export const SpacePlugin = ({
540
579
  },
541
580
  properties: {
542
581
  label: ['create space label', { ns: SPACE_PLUGIN }],
543
- icon: (props: IconProps) => <Plus {...props} />,
544
- iconSymbol: 'ph--plus--regular',
545
- disposition: 'toolbar',
582
+ icon: 'ph--plus--regular',
583
+ disposition: 'item',
546
584
  testId: 'spacePlugin.createSpace',
585
+ className: 'pbs-4',
547
586
  },
548
587
  },
549
588
  {
@@ -561,9 +600,10 @@ export const SpacePlugin = ({
561
600
  },
562
601
  properties: {
563
602
  label: ['join space label', { ns: SPACE_PLUGIN }],
564
- icon: (props: IconProps) => <SignIn {...props} />,
565
- iconSymbol: 'ph--sign-in--regular',
603
+ icon: 'ph--sign-in--regular',
604
+ disposition: 'item',
566
605
  testId: 'spacePlugin.joinSpace',
606
+ className: 'pbe-4',
567
607
  },
568
608
  },
569
609
  ],
@@ -707,6 +747,76 @@ export const SpacePlugin = ({
707
747
  .filter(nonNullable);
708
748
  },
709
749
  }),
750
+
751
+ // Create nodes for object settings.
752
+ createExtension({
753
+ id: `${SPACE_PLUGIN}/settings-for-subject`,
754
+ resolver: ({ id }) => {
755
+ // TODO(Zan): Find util (or make one).
756
+ if (!id.endsWith('~settings')) {
757
+ return;
758
+ }
759
+
760
+ const type = 'orphan-settings-for-subject';
761
+ const icon = 'ph--gear--regular';
762
+
763
+ const [subjectId] = id.split('~');
764
+ const { spaceId, objectId } = parseId(subjectId);
765
+ const space = client.spaces.get().find((space) => space.id === spaceId);
766
+ if (!objectId) {
767
+ const label = space
768
+ ? space.properties.name || ['unnamed space label', { ns: SPACE_PLUGIN }]
769
+ : ['unnamed object settings label', { ns: SPACE_PLUGIN }];
770
+
771
+ // TODO(wittjosiah): Support comments for arbitrary subjects.
772
+ // This is to ensure that the comments panel is not stuck on an old object.
773
+ return {
774
+ id,
775
+ type,
776
+ data: null,
777
+ properties: {
778
+ icon,
779
+ label,
780
+ showResolvedThreads: false,
781
+ object: null,
782
+ space,
783
+ },
784
+ };
785
+ }
786
+
787
+ const object = toSignal(
788
+ (onChange) => {
789
+ const timeout = setTimeout(async () => {
790
+ await space?.db.loadObjectById(objectId);
791
+ onChange();
792
+ });
793
+
794
+ return () => clearTimeout(timeout);
795
+ },
796
+ () => space?.db.getObjectById(objectId),
797
+ subjectId,
798
+ );
799
+ if (!object || !subjectId) {
800
+ return;
801
+ }
802
+
803
+ const meta = resolve(getTypename(object) ?? '');
804
+ const label = meta.label?.(object) ||
805
+ object.name ||
806
+ meta.placeholder || ['unnamed object settings label', { ns: SPACE_PLUGIN }];
807
+
808
+ return {
809
+ id,
810
+ type,
811
+ data: null,
812
+ properties: {
813
+ icon,
814
+ label,
815
+ object,
816
+ },
817
+ };
818
+ },
819
+ }),
710
820
  ];
711
821
  },
712
822
  serializer: (plugins) => {
@@ -802,9 +912,21 @@ export const SpacePlugin = ({
802
912
  }
803
913
 
804
914
  return {
805
- data: { space, id: space.id, activeParts: { main: [space.id] } },
806
-
915
+ data: {
916
+ space,
917
+ id: space.id,
918
+ activeParts: { main: [space.id] },
919
+ },
807
920
  intents: [
921
+ ...(settings.values.onSpaceCreate
922
+ ? [
923
+ [
924
+ { action: settings.values.onSpaceCreate, data: { space } },
925
+ { action: SpaceAction.ADD_OBJECT, data: { target: space } },
926
+ { action: NavigationAction.EXPOSE },
927
+ ],
928
+ ]
929
+ : []),
808
930
  [
809
931
  {
810
932
  action: ObservabilityAction.SEND_EVENT,
@@ -825,9 +947,26 @@ export const SpacePlugin = ({
825
947
  const { space } = await client.shell.joinSpace({ invitationCode: intent.data?.invitationCode });
826
948
  if (space) {
827
949
  return {
828
- data: { space, id: space.id, activeParts: { main: [space.id] } },
829
-
950
+ data: {
951
+ space,
952
+ id: space.id,
953
+ activeParts: { main: [space.id] },
954
+ },
830
955
  intents: [
956
+ [
957
+ {
958
+ action: LayoutAction.SET_LAYOUT,
959
+ data: {
960
+ element: 'toast',
961
+ subject: {
962
+ id: `${SPACE_PLUGIN}/join-success`,
963
+ duration: 10_000,
964
+ title: translations[0]['en-US'][SPACE_PLUGIN]['join success label'],
965
+ closeLabel: translations[0]['en-US'][SPACE_PLUGIN]['dismiss label'],
966
+ },
967
+ },
968
+ },
969
+ ],
831
970
  [
832
971
  {
833
972
  action: ObservabilityAction.SEND_EVENT,
@@ -1020,8 +1159,7 @@ export const SpacePlugin = ({
1020
1159
  title: translations[0]['en-US'][SPACE_PLUGIN]['space limit label'],
1021
1160
  description: translations[0]['en-US'][SPACE_PLUGIN]['space limit description'],
1022
1161
  duration: 5_000,
1023
- icon: (props: IconProps) => <Warning {...props} />,
1024
- iconSymbol: 'ph--warning--regular',
1162
+ icon: 'ph--warning--regular',
1025
1163
  actionLabel: translations[0]['en-US'][SPACE_PLUGIN]['remove deleted objects label'],
1026
1164
  actionAlt: translations[0]['en-US'][SPACE_PLUGIN]['remove deleted objects alt'],
1027
1165
  // TODO(wittjosiah): Use OS namespace.
@@ -1047,20 +1185,20 @@ export const SpacePlugin = ({
1047
1185
  }
1048
1186
 
1049
1187
  if (intent.data?.target instanceof CollectionType) {
1050
- intent.data?.target.objects.push(object as Identifiable);
1188
+ intent.data?.target.objects.push(object as HasId);
1051
1189
  } else if (isSpace(intent.data?.target)) {
1052
1190
  const collection = space.properties[CollectionType.typename];
1053
1191
  if (collection instanceof CollectionType) {
1054
- collection.objects.push(object as Identifiable);
1192
+ collection.objects.push(object as HasId);
1055
1193
  } else {
1056
1194
  // TODO(wittjosiah): Can't add non-echo objects by including in a collection because of types.
1057
- const collection = create(CollectionType, { objects: [object as Identifiable], views: {} });
1195
+ const collection = create(CollectionType, { objects: [object as HasId], views: {} });
1058
1196
  space.properties[CollectionType.typename] = collection;
1059
1197
  }
1060
1198
  }
1061
1199
 
1062
1200
  return {
1063
- data: { id: object.id, object, activeParts: { main: [fullyQualifiedId(object)] } },
1201
+ data: { id: fullyQualifiedId(object), object, activeParts: { main: [fullyQualifiedId(object)] } },
1064
1202
  intents: [
1065
1203
  [
1066
1204
  {
@@ -1115,7 +1253,6 @@ export const SpacePlugin = ({
1115
1253
  activeParts: {
1116
1254
  main: deletionData.wasActive,
1117
1255
  sidebar: deletionData.wasActive,
1118
- complementary: deletionData.wasActive,
1119
1256
  },
1120
1257
  },
1121
1258
  });
@@ -1203,11 +1340,12 @@ export const SpacePlugin = ({
1203
1340
  case SpaceAction.DUPLICATE_OBJECT: {
1204
1341
  const originalObject = intent.data?.object ?? intent.data?.result;
1205
1342
  const resolve = resolvePlugin(plugins, parseMetadataResolverPlugin)?.provides.metadata.resolver;
1206
- if (!isEchoObject(originalObject) || !resolve) {
1343
+ const space = isSpace(intent.data?.target) ? intent.data?.target : getSpace(intent.data?.target);
1344
+ if (!isEchoObject(originalObject) || !resolve || !space) {
1207
1345
  return;
1208
1346
  }
1209
1347
 
1210
- const newObject = await cloneObject(originalObject, resolve);
1348
+ const newObject = await cloneObject(originalObject, resolve, space);
1211
1349
  return {
1212
1350
  intents: [
1213
1351
  [{ action: SpaceAction.ADD_OBJECT, data: { object: newObject, target: intent.data?.target } }],