@dxos/plugin-space 0.6.14-main.8b352a0 → 0.6.14-staging.3e2eaca

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-AVLRQF6L.mjs → chunk-DJE2HYFV.mjs} +3 -2
  2. package/dist/lib/browser/{chunk-AVLRQF6L.mjs.map → chunk-DJE2HYFV.mjs.map} +2 -2
  3. package/dist/lib/browser/{chunk-FOI7DAUV.mjs → chunk-OWZKSWMX.mjs} +1 -1
  4. package/dist/lib/browser/{chunk-FOI7DAUV.mjs.map → chunk-OWZKSWMX.mjs.map} +2 -2
  5. package/dist/lib/browser/index.mjs +817 -762
  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 +1 -1
  10. package/dist/lib/node/{chunk-OTDRTHT4.cjs → chunk-FYWGZYJB.cjs} +4 -4
  11. package/dist/lib/node/{chunk-OTDRTHT4.cjs.map → chunk-FYWGZYJB.cjs.map} +2 -2
  12. package/dist/lib/node/{chunk-P4XUXM7Y.cjs → chunk-JFDDZI4Y.cjs} +6 -5
  13. package/dist/lib/node/{chunk-P4XUXM7Y.cjs.map → chunk-JFDDZI4Y.cjs.map} +2 -2
  14. package/dist/lib/node/index.cjs +1009 -957
  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 +11 -11
  20. package/dist/lib/node/types/index.cjs.map +1 -1
  21. package/dist/lib/node-esm/{chunk-YPQGKWHJ.mjs → chunk-DVUZ7A7G.mjs} +3 -2
  22. package/dist/lib/node-esm/{chunk-YPQGKWHJ.mjs.map → chunk-DVUZ7A7G.mjs.map} +2 -2
  23. package/dist/lib/node-esm/{chunk-FYDGMPSC.mjs → chunk-MCEAI4CV.mjs} +1 -1
  24. package/dist/lib/node-esm/{chunk-FYDGMPSC.mjs.map → chunk-MCEAI4CV.mjs.map} +2 -2
  25. package/dist/lib/node-esm/index.mjs +817 -762
  26. package/dist/lib/node-esm/index.mjs.map +4 -4
  27. package/dist/lib/node-esm/meta.json +1 -1
  28. package/dist/lib/node-esm/meta.mjs +1 -1
  29. package/dist/lib/node-esm/types/index.mjs +1 -1
  30. package/dist/types/src/SpacePlugin.d.ts +9 -1
  31. package/dist/types/src/SpacePlugin.d.ts.map +1 -1
  32. package/dist/types/src/components/JoinDialog.d.ts +7 -0
  33. package/dist/types/src/components/JoinDialog.d.ts.map +1 -0
  34. package/dist/types/src/components/ShareSpaceButton.d.ts +3 -2
  35. package/dist/types/src/components/ShareSpaceButton.d.ts.map +1 -1
  36. package/dist/types/src/components/{SpaceSettings.d.ts → SpacePluginSettings.d.ts} +2 -2
  37. package/dist/types/src/components/SpacePluginSettings.d.ts.map +1 -0
  38. package/dist/types/src/components/SpaceSettings/SpaceSettingsDialog.d.ts +10 -0
  39. package/dist/types/src/components/SpaceSettings/SpaceSettingsDialog.d.ts.map +1 -0
  40. package/dist/types/src/components/SpaceSettings/SpaceSettingsDialog.stories.d.ts +7 -0
  41. package/dist/types/src/components/SpaceSettings/SpaceSettingsDialog.stories.d.ts.map +1 -0
  42. package/dist/types/src/components/SpaceSettings/SpaceSettingsPanel.d.ts.map +1 -0
  43. package/dist/types/src/components/SpaceSettings/SpaceSettingsPanel.stories.d.ts +7 -0
  44. package/dist/types/src/components/SpaceSettings/SpaceSettingsPanel.stories.d.ts.map +1 -0
  45. package/dist/types/src/components/SpaceSettings/index.d.ts +3 -0
  46. package/dist/types/src/components/SpaceSettings/index.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 +3 -2
  50. package/dist/types/src/components/SyncStatus/SyncStatus.d.ts.map +1 -1
  51. package/dist/types/src/components/SyncStatus/SyncStatus.stories.d.ts +5 -20
  52. package/dist/types/src/components/SyncStatus/SyncStatus.stories.d.ts.map +1 -1
  53. package/dist/types/src/components/SyncStatus/save-tracker.d.ts +3 -0
  54. package/dist/types/src/components/SyncStatus/save-tracker.d.ts.map +1 -0
  55. package/dist/types/src/components/SyncStatus/status.d.ts +9 -0
  56. package/dist/types/src/components/SyncStatus/status.d.ts.map +1 -0
  57. package/dist/types/src/components/SyncStatus/{types.d.ts → sync-state.d.ts} +1 -1
  58. package/dist/types/src/components/SyncStatus/sync-state.d.ts.map +1 -0
  59. package/dist/types/src/components/index.d.ts +2 -4
  60. package/dist/types/src/components/index.d.ts.map +1 -1
  61. package/dist/types/src/meta.d.ts +3 -2
  62. package/dist/types/src/meta.d.ts.map +1 -1
  63. package/dist/types/src/translations.d.ts +10 -0
  64. package/dist/types/src/translations.d.ts.map +1 -1
  65. package/dist/types/src/types/types.d.ts +7 -1
  66. package/dist/types/src/types/types.d.ts.map +1 -1
  67. package/dist/types/src/util.d.ts +8 -4
  68. package/dist/types/src/util.d.ts.map +1 -1
  69. package/package.json +35 -33
  70. package/src/SpacePlugin.tsx +279 -175
  71. package/src/components/AwaitingObject.tsx +1 -1
  72. package/src/components/JoinDialog.tsx +100 -0
  73. package/src/components/ShareSpaceButton.tsx +10 -6
  74. package/src/components/{SpaceSettings.tsx → SpacePluginSettings.tsx} +6 -6
  75. package/src/components/SpaceSettings/SpaceSettingsDialog.stories.tsx +44 -0
  76. package/src/components/SpaceSettings/SpaceSettingsDialog.tsx +103 -0
  77. package/src/components/SpaceSettings/SpaceSettingsPanel.stories.tsx +32 -0
  78. package/src/components/{SpaceSettingsPanel.tsx → SpaceSettings/SpaceSettingsPanel.tsx} +25 -20
  79. package/src/components/SpaceSettings/index.ts +6 -0
  80. package/src/components/SyncStatus/Space.tsx +109 -0
  81. package/src/components/SyncStatus/SyncStatus.stories.tsx +13 -4
  82. package/src/components/SyncStatus/SyncStatus.tsx +43 -129
  83. package/src/components/{SaveStatus.tsx → SyncStatus/save-tracker.ts} +1 -25
  84. package/src/components/SyncStatus/status.ts +44 -0
  85. package/src/components/index.ts +2 -4
  86. package/src/meta.ts +2 -1
  87. package/src/translations.ts +10 -0
  88. package/src/types/types.ts +9 -1
  89. package/src/util.tsx +51 -23
  90. package/dist/types/src/components/MissingObject.d.ts +0 -5
  91. package/dist/types/src/components/MissingObject.d.ts.map +0 -1
  92. package/dist/types/src/components/SaveStatus.d.ts +0 -3
  93. package/dist/types/src/components/SaveStatus.d.ts.map +0 -1
  94. package/dist/types/src/components/SpaceMain/SpaceMain.d.ts +0 -10
  95. package/dist/types/src/components/SpaceMain/SpaceMain.d.ts.map +0 -1
  96. package/dist/types/src/components/SpaceMain/SpaceMembersSection.d.ts +0 -6
  97. package/dist/types/src/components/SpaceMain/SpaceMembersSection.d.ts.map +0 -1
  98. package/dist/types/src/components/SpaceMain/index.d.ts +0 -2
  99. package/dist/types/src/components/SpaceMain/index.d.ts.map +0 -1
  100. package/dist/types/src/components/SpaceSettings.d.ts.map +0 -1
  101. package/dist/types/src/components/SpaceSettingsPanel.d.ts.map +0 -1
  102. package/dist/types/src/components/SyncStatus/types.d.ts.map +0 -1
  103. package/src/components/MissingObject.tsx +0 -54
  104. package/src/components/SpaceMain/SpaceMain.tsx +0 -60
  105. package/src/components/SpaceMain/SpaceMembersSection.tsx +0 -205
  106. package/src/components/SpaceMain/index.ts +0 -5
  107. /package/dist/types/src/components/{SpaceSettingsPanel.d.ts → SpaceSettings/SpaceSettingsPanel.d.ts} +0 -0
  108. /package/src/components/SyncStatus/{types.ts → sync-state.ts} +0 -0
@@ -6,18 +6,22 @@ import { signal } from '@preact/signals-core';
6
6
  import React from 'react';
7
7
 
8
8
  import {
9
+ type GraphProvides,
9
10
  type IntentDispatcher,
10
11
  type IntentPluginProvides,
11
12
  LayoutAction,
13
+ type LayoutProvides,
12
14
  type LocationProvides,
13
15
  NavigationAction,
14
16
  type Plugin,
15
17
  type PluginDefinition,
16
18
  Surface,
19
+ findPlugin,
17
20
  firstIdInPart,
18
21
  openIds,
19
22
  parseGraphPlugin,
20
23
  parseIntentPlugin,
24
+ parseLayoutPlugin,
21
25
  parseMetadataResolverPlugin,
22
26
  parseNavigationPlugin,
23
27
  resolvePlugin,
@@ -25,6 +29,7 @@ import {
25
29
  import { EventSubscriptions, type Trigger, type UnsubscribeCallback } from '@dxos/async';
26
30
  import { type HasId, isReactiveObject } from '@dxos/echo-schema';
27
31
  import { scheduledEffect } from '@dxos/echo-signals/core';
32
+ import { invariant } from '@dxos/invariant';
28
33
  import { LocalStorageStore } from '@dxos/local-storage';
29
34
  import { log } from '@dxos/log';
30
35
  import { Migrations } from '@dxos/migrations';
@@ -48,9 +53,9 @@ import {
48
53
  isSpace,
49
54
  loadObjectReferences,
50
55
  parseId,
56
+ FQ_ID_LENGTH,
51
57
  } from '@dxos/react-client/echo';
52
- import { Dialog } from '@dxos/react-ui';
53
- import { ClipboardProvider, InvitationManager, type InvitationManagerProps, osTranslations } from '@dxos/shell/react';
58
+ import { type JoinPanelProps, osTranslations } from '@dxos/shell/react';
54
59
  import { ComplexMap, nonNullable, reduceGroupBy } from '@dxos/util';
55
60
 
56
61
  import {
@@ -58,18 +63,19 @@ import {
58
63
  CollectionMain,
59
64
  CollectionSection,
60
65
  DefaultObjectSettings,
66
+ JoinDialog,
61
67
  MenuFooter,
62
- MissingObject,
63
68
  PopoverRenameObject,
64
69
  PopoverRenameSpace,
65
- SaveStatus,
66
70
  ShareSpaceButton,
67
71
  SmallPresence,
68
72
  SmallPresenceLive,
69
73
  SpacePresence,
70
- SpaceSettings,
74
+ SpacePluginSettings,
71
75
  SpaceSettingsPanel,
72
76
  SyncStatus,
77
+ SpaceSettingsDialog,
78
+ type SpaceSettingsDialogProps,
73
79
  } from './components';
74
80
  import meta, { SPACE_PLUGIN, SpaceAction } from './meta';
75
81
  import translations from './translations';
@@ -91,7 +97,7 @@ import {
91
97
  } from './util';
92
98
 
93
99
  const ACTIVE_NODE_BROADCAST_INTERVAL = 30_000;
94
- const OBJECT_ID_LENGTH = 60; // 33 (space id) + 26 (object id) + 1 (separator).
100
+ const WAIT_FOR_OBJECT_TIMEOUT = 1000;
95
101
  const SPACE_MAX_OBJECTS = 500;
96
102
  // https://stackoverflow.com/a/19016910
97
103
  const DIRECTORY_TYPE = 'text/directory';
@@ -100,6 +106,16 @@ export const parseSpacePlugin = (plugin?: Plugin) =>
100
106
  Array.isArray((plugin?.provides as any).space?.enabled) ? (plugin as Plugin<SpacePluginProvides>) : undefined;
101
107
 
102
108
  export type SpacePluginOptions = {
109
+ /**
110
+ * Base URL for the invitation link.
111
+ */
112
+ invitationUrl?: string;
113
+
114
+ /**
115
+ * Query parameter for the invitation code.
116
+ */
117
+ invitationParam?: string;
118
+
103
119
  /**
104
120
  * Fired when first run logic should be executed.
105
121
  *
@@ -119,6 +135,8 @@ export type SpacePluginOptions = {
119
135
  };
120
136
 
121
137
  export const SpacePlugin = ({
138
+ invitationUrl = window.location.origin,
139
+ invitationParam = 'spaceInvitationCode',
122
140
  firstRun,
123
141
  onFirstRun,
124
142
  }: SpacePluginOptions = {}): PluginDefinition<SpacePluginProvides> => {
@@ -132,23 +150,35 @@ export const SpacePlugin = ({
132
150
  // TODO(wittjosiah): Stop using (Complex)Map inside reactive object.
133
151
  viewersByIdentity: new ComplexMap(PublicKey.hash),
134
152
  sdkMigrationRunning: {},
153
+ navigableCollections: false,
135
154
  });
136
155
  const subscriptions = new EventSubscriptions();
137
156
  const spaceSubscriptions = new EventSubscriptions();
138
157
  const graphSubscriptions = new Map<string, UnsubscribeCallback>();
139
158
 
140
159
  let clientPlugin: Plugin<ClientPluginProvides> | undefined;
160
+ let graphPlugin: Plugin<GraphProvides> | undefined;
141
161
  let intentPlugin: Plugin<IntentPluginProvides> | undefined;
162
+ let layoutPlugin: Plugin<LayoutProvides> | undefined;
142
163
  let navigationPlugin: Plugin<LocationProvides> | undefined;
143
164
  let attentionPlugin: Plugin<AttentionPluginProvides> | undefined;
144
165
 
166
+ const createSpaceInvitationUrl = (invitationCode: string) => {
167
+ const baseUrl = new URL(invitationUrl);
168
+ baseUrl.searchParams.set(invitationParam, invitationCode);
169
+ return baseUrl.toString();
170
+ };
171
+
145
172
  const onSpaceReady = async () => {
146
- if (!clientPlugin || !navigationPlugin || !attentionPlugin) {
173
+ if (!clientPlugin || !intentPlugin || !graphPlugin || !navigationPlugin || !layoutPlugin || !attentionPlugin) {
147
174
  return;
148
175
  }
149
176
 
150
177
  const client = clientPlugin.provides.client;
178
+ const dispatch = intentPlugin.provides.intent.dispatch;
179
+ const graph = graphPlugin.provides.graph;
151
180
  const location = navigationPlugin.provides.location;
181
+ const layout = layoutPlugin.provides.layout;
152
182
  const attention = attentionPlugin.provides.attention;
153
183
  const defaultSpace = client.spaces.default;
154
184
 
@@ -166,6 +196,37 @@ export const SpacePlugin = ({
166
196
  defaultSpace.db.add(create({ key: SHARED, order: [] }));
167
197
  }
168
198
 
199
+ // Await missing objects.
200
+ subscriptions.add(
201
+ scheduledEffect(
202
+ () => ({
203
+ layoutMode: layout.layoutMode,
204
+ soloPart: location.active.solo?.[0],
205
+ }),
206
+ ({ layoutMode, soloPart }) => {
207
+ if (layoutMode !== 'solo' || !soloPart) {
208
+ return;
209
+ }
210
+
211
+ const node = graph.findNode(soloPart.id);
212
+ if (!node && soloPart.id.length === FQ_ID_LENGTH) {
213
+ const timeout = setTimeout(async () => {
214
+ const node = graph.findNode(soloPart.id);
215
+ if (!node) {
216
+ await dispatch({
217
+ plugin: SPACE_PLUGIN,
218
+ action: SpaceAction.WAIT_FOR_OBJECT,
219
+ data: { id: soloPart.id },
220
+ });
221
+ }
222
+ }, WAIT_FOR_OBJECT_TIMEOUT);
223
+
224
+ return () => clearTimeout(timeout);
225
+ }
226
+ },
227
+ ),
228
+ );
229
+
169
230
  // Cache space names.
170
231
  subscriptions.add(
171
232
  client.spaces.subscribe(async (spaces) => {
@@ -289,6 +350,14 @@ export const SpacePlugin = ({
289
350
  settings.prop({ key: 'showHidden', type: LocalStorageStore.bool({ allowUndefined: true }) });
290
351
  state.prop({ key: 'spaceNames', type: LocalStorageStore.json<Record<string, string>>() });
291
352
 
353
+ // TODO(wittjosiah): Hardcoded due to circular dependency.
354
+ // Should be based on a provides interface.
355
+ if (findPlugin(plugins, 'dxos.org/plugin/stack')) {
356
+ state.values.navigableCollections = true;
357
+ }
358
+
359
+ graphPlugin = resolvePlugin(plugins, parseGraphPlugin);
360
+ layoutPlugin = resolvePlugin(plugins, parseLayoutPlugin);
292
361
  navigationPlugin = resolvePlugin(plugins, parseNavigationPlugin);
293
362
  attentionPlugin = resolvePlugin(plugins, parseAttentionPlugin);
294
363
  clientPlugin = resolvePlugin(plugins, parseClientPlugin);
@@ -311,19 +380,20 @@ export const SpacePlugin = ({
311
380
  await onFirstRun?.({ client, dispatch });
312
381
  };
313
382
 
314
- // No need to unsubscribe because this observable completes when spaces are ready.
315
- client.spaces.isReady.subscribe(async (ready) => {
316
- if (ready) {
317
- await clientPlugin?.provides.client.spaces.default.waitUntilReady();
318
- if (firstRun) {
319
- void firstRun?.wait().then(handleFirstRun);
320
- } else {
321
- await handleFirstRun();
322
- }
383
+ subscriptions.add(
384
+ client.spaces.isReady.subscribe(async (ready) => {
385
+ if (ready) {
386
+ await clientPlugin?.provides.client.spaces.default.waitUntilReady();
387
+ if (firstRun) {
388
+ void firstRun?.wait().then(handleFirstRun);
389
+ } else {
390
+ await handleFirstRun();
391
+ }
323
392
 
324
- await onSpaceReady();
325
- }
326
- });
393
+ await onSpaceReady();
394
+ }
395
+ }).unsubscribe,
396
+ );
327
397
  },
328
398
  unload: async () => {
329
399
  settings.close();
@@ -336,6 +406,11 @@ export const SpacePlugin = ({
336
406
  space: state.values,
337
407
  settings: settings.values,
338
408
  translations: [...translations, osTranslations],
409
+ complementary: {
410
+ panels: [
411
+ { id: 'settings', label: ['open settings panel label', { ns: SPACE_PLUGIN }], icon: 'ph--gear--regular' },
412
+ ],
413
+ },
339
414
  root: () => (state.values.awaiting ? <AwaitingObject id={state.values.awaiting} /> : null),
340
415
  metadata: {
341
416
  records: {
@@ -356,24 +431,20 @@ export const SpacePlugin = ({
356
431
  },
357
432
  surface: {
358
433
  component: ({ data, role, ...rest }) => {
359
- const primary = data.active ?? data.object;
360
434
  switch (role) {
361
435
  case 'article':
362
- case 'main':
363
436
  // TODO(wittjosiah): Need to avoid shotgun parsing space state everywhere.
364
- return isSpace(primary) && primary.state.get() === SpaceState.SPACE_READY ? (
437
+ return isSpace(data.object) && data.object.state.get() === SpaceState.SPACE_READY ? (
365
438
  <Surface
366
- data={{ active: primary.properties[CollectionType.typename], id: primary.id }}
439
+ data={{ object: data.object.properties[CollectionType.typename], id: data.object.id }}
367
440
  role={role}
368
441
  {...rest}
369
442
  />
370
- ) : primary instanceof CollectionType ? (
443
+ ) : data.object instanceof CollectionType ? (
371
444
  {
372
- node: <CollectionMain collection={primary} />,
445
+ node: <CollectionMain collection={data.object} />,
373
446
  disposition: 'fallback',
374
447
  }
375
- ) : typeof primary === 'string' && primary.length === OBJECT_ID_LENGTH ? (
376
- <MissingObject id={primary} />
377
448
  ) : null;
378
449
  case 'complementary--settings':
379
450
  return isSpace(data.subject) ? (
@@ -382,14 +453,15 @@ export const SpacePlugin = ({
382
453
  { node: <DefaultObjectSettings object={data.subject} />, disposition: 'fallback' }
383
454
  ) : null;
384
455
  case 'dialog':
385
- if (data.component === 'dxos.org/plugin/space/InvitationManagerDialog') {
456
+ if (data.component === 'dxos.org/plugin/space/SpaceSettingsDialog') {
386
457
  return (
387
- <Dialog.Content>
388
- <ClipboardProvider>
389
- <InvitationManager active {...(data.subject as InvitationManagerProps)} />
390
- </ClipboardProvider>
391
- </Dialog.Content>
458
+ <SpaceSettingsDialog
459
+ {...(data.subject as SpaceSettingsDialogProps)}
460
+ createInvitationUrl={createSpaceInvitationUrl}
461
+ />
392
462
  );
463
+ } else if (data.component === 'dxos.org/plugin/space/JoinDialog') {
464
+ return <JoinDialog {...(data.subject as JoinPanelProps)} />;
393
465
  }
394
466
  return null;
395
467
  case 'popover': {
@@ -432,7 +504,7 @@ export const SpacePlugin = ({
432
504
  node: (
433
505
  <>
434
506
  <SpacePresence object={object} />
435
- {space.properties[COMPOSER_SPACE_LOCK] ? null : <ShareSpaceButton spaceId={space.id} />}
507
+ {space.properties[COMPOSER_SPACE_LOCK] ? null : <ShareSpaceButton space={space} />}
436
508
  </>
437
509
  ),
438
510
  disposition: 'hoist',
@@ -442,7 +514,7 @@ export const SpacePlugin = ({
442
514
  case 'section':
443
515
  return data.object instanceof CollectionType ? <CollectionSection collection={data.object} /> : null;
444
516
  case 'settings':
445
- return data.plugin === meta.id ? <SpaceSettings settings={settings.values} /> : null;
517
+ return data.plugin === meta.id ? <SpacePluginSettings settings={settings.values} /> : null;
446
518
  case 'menu-footer':
447
519
  if (isEchoObject(data.object)) {
448
520
  return <MenuFooter object={data.object} />;
@@ -450,12 +522,7 @@ export const SpacePlugin = ({
450
522
  return null;
451
523
  }
452
524
  case 'status': {
453
- return (
454
- <>
455
- <SyncStatus />
456
- <SaveStatus />
457
- </>
458
- );
525
+ return <SyncStatus />;
459
526
  }
460
527
  default:
461
528
  return null;
@@ -508,6 +575,7 @@ export const SpacePlugin = ({
508
575
  label: ['spaces label', { ns: SPACE_PLUGIN }],
509
576
  testId: 'spacePlugin.spaces',
510
577
  role: 'branch',
578
+ disabled: true,
511
579
  childrenPersistenceClass: 'echo',
512
580
  onRearrangeChildren: async (nextOrder: Space[]) => {
513
581
  // NOTE: This is needed to ensure order is updated by next animation frame.
@@ -542,43 +610,32 @@ export const SpacePlugin = ({
542
610
  {
543
611
  id: SpaceAction.CREATE,
544
612
  data: async () => {
545
- await dispatch([
546
- {
547
- plugin: SPACE_PLUGIN,
548
- action: SpaceAction.CREATE,
549
- },
550
- {
551
- action: NavigationAction.OPEN,
552
- },
553
- ]);
613
+ await dispatch({
614
+ plugin: SPACE_PLUGIN,
615
+ action: SpaceAction.CREATE,
616
+ });
554
617
  },
555
618
  properties: {
556
619
  label: ['create space label', { ns: SPACE_PLUGIN }],
557
620
  icon: 'ph--plus--regular',
558
621
  disposition: 'item',
559
622
  testId: 'spacePlugin.createSpace',
560
- className: 'pbs-4',
623
+ className: 'border-t border-separator',
561
624
  },
562
625
  },
563
626
  {
564
627
  id: SpaceAction.JOIN,
565
628
  data: async () => {
566
- await dispatch([
567
- {
568
- plugin: SPACE_PLUGIN,
569
- action: SpaceAction.JOIN,
570
- },
571
- {
572
- action: NavigationAction.OPEN,
573
- },
574
- ]);
629
+ await dispatch({
630
+ plugin: SPACE_PLUGIN,
631
+ action: SpaceAction.JOIN,
632
+ });
575
633
  },
576
634
  properties: {
577
635
  label: ['join space label', { ns: SPACE_PLUGIN }],
578
636
  icon: 'ph--sign-in--regular',
579
637
  disposition: 'item',
580
638
  testId: 'spacePlugin.joinSpace',
581
- className: 'pbe-4',
582
639
  },
583
640
  },
584
641
  ],
@@ -597,26 +654,30 @@ export const SpacePlugin = ({
597
654
  return;
598
655
  }
599
656
 
600
- const [spacesOrder] = memoizeQuery(client.spaces.default, Filter.schema(Expando, { key: SHARED }));
601
- const order: string[] = spacesOrder?.order ?? [];
602
- const orderMap = new Map(order.map((id, index) => [id, index]));
603
- return [
604
- ...spaces
605
- .filter((space) => orderMap.has(space.id))
606
- .sort((a, b) => orderMap.get(a.id)! - orderMap.get(b.id)!),
607
- ...spaces.filter((space) => !orderMap.has(space.id)),
608
- ]
609
- .filter((space) =>
610
- settings.values.showHidden ? true : space.state.get() !== SpaceState.SPACE_INACTIVE,
611
- )
612
- .map((space) =>
613
- constructSpaceNode({
614
- space,
615
- personal: space === client.spaces.default,
616
- namesCache: state.values.spaceNames,
617
- resolve,
618
- }),
619
- );
657
+ // TODO(wittjosiah): During client reset, accessing default space throws.
658
+ try {
659
+ const [spacesOrder] = memoizeQuery(client.spaces.default, Filter.schema(Expando, { key: SHARED }));
660
+ const order: string[] = spacesOrder?.order ?? [];
661
+ const orderMap = new Map(order.map((id, index) => [id, index]));
662
+ return [
663
+ ...spaces
664
+ .filter((space) => orderMap.has(space.id))
665
+ .sort((a, b) => orderMap.get(a.id)! - orderMap.get(b.id)!),
666
+ ...spaces.filter((space) => !orderMap.has(space.id)),
667
+ ]
668
+ .filter((space) =>
669
+ settings.values.showHidden ? true : space.state.get() !== SpaceState.SPACE_INACTIVE,
670
+ )
671
+ .map((space) =>
672
+ constructSpaceNode({
673
+ space,
674
+ navigable: state.values.navigableCollections,
675
+ personal: space === client.spaces.default,
676
+ namesCache: state.values.spaceNames,
677
+ resolve,
678
+ }),
679
+ );
680
+ } catch {}
620
681
  },
621
682
  }),
622
683
 
@@ -630,12 +691,12 @@ export const SpacePlugin = ({
630
691
  return;
631
692
  }
632
693
 
633
- const state = toSignal(
694
+ const spaceState = toSignal(
634
695
  (onChange) => space.state.subscribe(() => onChange()).unsubscribe,
635
696
  () => space.state.get(),
636
697
  space.id,
637
698
  );
638
- if (state !== SpaceState.SPACE_READY) {
699
+ if (spaceState !== SpaceState.SPACE_READY) {
639
700
  return;
640
701
  }
641
702
 
@@ -650,7 +711,7 @@ export const SpacePlugin = ({
650
711
  return;
651
712
  }
652
713
 
653
- return createObjectNode({ object, space, resolve });
714
+ return createObjectNode({ object, space, resolve, navigable: state.values.navigableCollections });
654
715
  },
655
716
  }),
656
717
 
@@ -658,7 +719,12 @@ export const SpacePlugin = ({
658
719
  createExtension({
659
720
  id: `${SPACE_PLUGIN}/actions`,
660
721
  filter: (node): node is Node<Space> => isSpace(node.data),
661
- actionGroups: ({ node }) => constructSpaceActionGroups({ space: node.data, dispatch }),
722
+ actionGroups: ({ node }) =>
723
+ constructSpaceActionGroups({
724
+ space: node.data,
725
+ dispatch,
726
+ navigable: state.values.navigableCollections,
727
+ }),
662
728
  actions: ({ node }) => {
663
729
  const space = node.data;
664
730
  return constructSpaceActions({
@@ -676,12 +742,12 @@ export const SpacePlugin = ({
676
742
  filter: (node): node is Node<Space> => isSpace(node.data),
677
743
  connector: ({ node }) => {
678
744
  const space = node.data;
679
- const state = toSignal(
745
+ const spaceState = toSignal(
680
746
  (onChange) => space.state.subscribe(() => onChange()).unsubscribe,
681
747
  () => space.state.get(),
682
748
  space.id,
683
749
  );
684
- if (state !== SpaceState.SPACE_READY) {
750
+ if (spaceState !== SpaceState.SPACE_READY) {
685
751
  return;
686
752
  }
687
753
 
@@ -692,7 +758,9 @@ export const SpacePlugin = ({
692
758
 
693
759
  return collection.objects
694
760
  .filter(nonNullable)
695
- .map((object) => createObjectNode({ object, space, resolve }))
761
+ .map((object) =>
762
+ createObjectNode({ object, space, resolve, navigable: state.values.navigableCollections }),
763
+ )
696
764
  .filter(nonNullable);
697
765
  },
698
766
  }),
@@ -701,7 +769,12 @@ export const SpacePlugin = ({
701
769
  createExtension({
702
770
  id: `${SPACE_PLUGIN}/object-actions`,
703
771
  filter: (node): node is Node<EchoReactiveObject<any>> => isEchoObject(node.data),
704
- actionGroups: ({ node }) => constructObjectActionGroups({ object: node.data, dispatch }),
772
+ actionGroups: ({ node }) =>
773
+ constructObjectActionGroups({
774
+ object: node.data,
775
+ dispatch,
776
+ navigable: state.values.navigableCollections,
777
+ }),
705
778
  actions: ({ node }) => constructObjectActions({ node, dispatch }),
706
779
  }),
707
780
 
@@ -718,7 +791,9 @@ export const SpacePlugin = ({
718
791
 
719
792
  return collection.objects
720
793
  .filter(nonNullable)
721
- .map((object) => createObjectNode({ object, space, resolve }))
794
+ .map((object) =>
795
+ createObjectNode({ object, space, resolve, navigable: state.values.navigableCollections }),
796
+ )
722
797
  .filter(nonNullable);
723
798
  },
724
799
  }),
@@ -898,6 +973,7 @@ export const SpacePlugin = ({
898
973
  [
899
974
  { action: settings.values.onSpaceCreate, data: { space } },
900
975
  { action: SpaceAction.ADD_OBJECT, data: { target: space } },
976
+ { action: NavigationAction.OPEN },
901
977
  { action: NavigationAction.EXPOSE },
902
978
  ],
903
979
  ]
@@ -918,69 +994,60 @@ export const SpacePlugin = ({
918
994
  }
919
995
 
920
996
  case SpaceAction.JOIN: {
921
- if (client) {
922
- const { space } = await client.shell.joinSpace({ invitationCode: intent.data?.invitationCode });
923
- if (space) {
924
- return {
925
- data: {
926
- space,
927
- id: space.id,
928
- activeParts: { main: [space.id] },
997
+ return {
998
+ data: true,
999
+ intents: [
1000
+ [
1001
+ {
1002
+ action: LayoutAction.SET_LAYOUT,
1003
+ data: {
1004
+ element: 'dialog',
1005
+ component: 'dxos.org/plugin/space/JoinDialog',
1006
+ dialogBlockAlign: 'start',
1007
+ subject: {
1008
+ initialInvitationCode: intent.data?.invitationCode,
1009
+ } satisfies Partial<JoinPanelProps>,
1010
+ },
929
1011
  },
930
- intents: [
931
- [
932
- {
933
- action: LayoutAction.SET_LAYOUT,
934
- data: {
935
- element: 'toast',
936
- subject: {
937
- id: `${SPACE_PLUGIN}/join-success`,
938
- duration: 10_000,
939
- title: translations[0]['en-US'][SPACE_PLUGIN]['join success label'],
940
- closeLabel: translations[0]['en-US'][SPACE_PLUGIN]['dismiss label'],
941
- },
942
- },
943
- },
944
- ],
945
- [
946
- {
947
- action: ObservabilityAction.SEND_EVENT,
948
- data: {
949
- name: 'space.join',
950
- properties: {
951
- spaceId: space.id,
952
- },
953
- },
954
- },
955
- ],
956
- ],
957
- };
958
- }
959
- }
1012
+ ],
1013
+ ],
1014
+ };
960
1015
  break;
961
1016
  }
962
1017
 
963
1018
  case SpaceAction.SHARE: {
964
- const spaceId = intent.data?.spaceId;
965
- if (clientPlugin && typeof spaceId === 'string') {
966
- if (!navigationPlugin?.provides.location.active) {
967
- return;
968
- }
969
- const target = firstIdInPart(navigationPlugin?.provides.location.active, 'main');
970
- const result = await clientPlugin.provides.client.shell.shareSpace({ spaceId, target });
1019
+ const space = intent.data?.space;
1020
+ if (isSpace(space) && !space.properties[COMPOSER_SPACE_LOCK]) {
1021
+ const active = navigationPlugin?.provides.location.active;
1022
+ const mode = layoutPlugin?.provides.layout.layoutMode;
1023
+ const target = active ? firstIdInPart(active, mode === 'solo' ? 'solo' : 'main') : undefined;
1024
+
971
1025
  return {
972
- data: result,
1026
+ data: true,
973
1027
  intents: [
1028
+ [
1029
+ {
1030
+ action: LayoutAction.SET_LAYOUT,
1031
+ data: {
1032
+ element: 'dialog',
1033
+ component: 'dxos.org/plugin/space/SpaceSettingsDialog',
1034
+ dialogBlockAlign: 'start',
1035
+ subject: {
1036
+ space,
1037
+ target,
1038
+ initialTab: 'members',
1039
+ createInvitationUrl: createSpaceInvitationUrl,
1040
+ } satisfies Partial<SpaceSettingsDialogProps>,
1041
+ },
1042
+ },
1043
+ ],
974
1044
  [
975
1045
  {
976
1046
  action: ObservabilityAction.SEND_EVENT,
977
1047
  data: {
978
1048
  name: 'space.share',
979
1049
  properties: {
980
- spaceId,
981
- members: result.members?.length,
982
- error: result.error?.message,
983
- cancelled: result.cancelled,
1050
+ space: space.id,
984
1051
  },
985
1052
  },
986
1053
  },
@@ -1061,6 +1128,33 @@ export const SpacePlugin = ({
1061
1128
  break;
1062
1129
  }
1063
1130
 
1131
+ case SpaceAction.OPEN_SETTINGS: {
1132
+ const space = intent.data?.space;
1133
+ if (isSpace(space)) {
1134
+ return {
1135
+ data: true,
1136
+ intents: [
1137
+ [
1138
+ {
1139
+ action: LayoutAction.SET_LAYOUT,
1140
+ data: {
1141
+ element: 'dialog',
1142
+ component: 'dxos.org/plugin/space/SpaceSettingsDialog',
1143
+ dialogBlockAlign: 'start',
1144
+ subject: {
1145
+ space,
1146
+ initialTab: 'settings',
1147
+ createInvitationUrl: createSpaceInvitationUrl,
1148
+ } satisfies Partial<SpaceSettingsDialogProps>,
1149
+ },
1150
+ },
1151
+ ],
1152
+ ],
1153
+ };
1154
+ }
1155
+ break;
1156
+ }
1157
+
1064
1158
  case SpaceAction.OPEN: {
1065
1159
  const space = intent.data?.space;
1066
1160
  if (isSpace(space)) {
@@ -1192,10 +1286,13 @@ export const SpacePlugin = ({
1192
1286
  };
1193
1287
  }
1194
1288
 
1195
- case SpaceAction.REMOVE_OBJECT: {
1196
- const object = intent.data?.object ?? intent.data?.result;
1197
- const space = getSpace(object);
1198
- if (!(isEchoObject(object) && space)) {
1289
+ case SpaceAction.REMOVE_OBJECTS: {
1290
+ const objects = intent.data?.objects ?? intent.data?.result;
1291
+ invariant(Array.isArray(objects));
1292
+
1293
+ // All objects must be a member of the same space.
1294
+ const space = getSpace(objects[0]);
1295
+ if (!space || !objects.every((obj) => isEchoObject(obj) && getSpace(obj) === space)) {
1199
1296
  return;
1200
1297
  }
1201
1298
 
@@ -1204,23 +1301,22 @@ export const SpacePlugin = ({
1204
1301
  const openObjectIds = new Set<string>(openIds(activeParts ?? {}));
1205
1302
 
1206
1303
  if (!intent.undo && resolve) {
1207
- // Capture the current state for undo.
1208
1304
  const parentCollection = intent.data?.collection ?? space.properties[CollectionType.typename];
1209
- const nestedObjects = await getNestedObjects(object, resolve);
1305
+ const nestedObjectsList = await Promise.all(objects.map((obj) => getNestedObjects(obj, resolve)));
1306
+
1210
1307
  const deletionData = {
1211
- object,
1308
+ objects,
1212
1309
  parentCollection,
1213
- index:
1214
- parentCollection instanceof CollectionType
1215
- ? parentCollection.objects.indexOf(object as Expando)
1216
- : -1,
1217
- nestedObjects,
1218
- wasActive: [object, ...nestedObjects]
1310
+ indices: objects.map((obj) =>
1311
+ parentCollection instanceof CollectionType ? parentCollection.objects.indexOf(obj as Expando) : -1,
1312
+ ),
1313
+ nestedObjectsList,
1314
+ wasActive: objects
1315
+ .flatMap((obj, i) => [obj, ...nestedObjectsList[i]])
1219
1316
  .map((obj) => fullyQualifiedId(obj))
1220
1317
  .filter((id) => openObjectIds.has(id)),
1221
1318
  };
1222
1319
 
1223
- // If the item is active, navigate to "nowhere" to avoid navigating to a removed item.
1224
1320
  if (deletionData.wasActive.length > 0) {
1225
1321
  await intentPlugin?.provides.intent.dispatch({
1226
1322
  action: NavigationAction.CLOSE,
@@ -1233,48 +1329,56 @@ export const SpacePlugin = ({
1233
1329
  });
1234
1330
  }
1235
1331
 
1236
- if (parentCollection instanceof CollectionType) {
1237
- // TODO(Zan): Is there a nicer way to do this without casting to Expando?
1238
- const index = parentCollection.objects.indexOf(object as Expando);
1239
- if (index !== -1) {
1240
- parentCollection.objects.splice(index, 1);
1241
- }
1332
+ if (deletionData.parentCollection instanceof CollectionType) {
1333
+ [...deletionData.indices]
1334
+ .sort((a, b) => b - a)
1335
+ .forEach((index: number) => {
1336
+ if (index !== -1) {
1337
+ deletionData.parentCollection.objects.splice(index, 1);
1338
+ }
1339
+ });
1242
1340
  }
1243
1341
 
1244
- // If the object is a collection, also delete its nested objects.
1245
- deletionData.nestedObjects.forEach((obj) => {
1342
+ deletionData.nestedObjectsList.flat().forEach((obj) => {
1246
1343
  space.db.remove(obj);
1247
1344
  });
1248
- space.db.remove(object);
1345
+ objects.forEach((obj) => space.db.remove(obj));
1249
1346
 
1250
- const undoMessageKey =
1251
- object instanceof CollectionType ? 'collection deleted label' : 'object deleted label';
1347
+ const undoMessageKey = objects.some((obj) => obj instanceof CollectionType)
1348
+ ? 'collection deleted label'
1349
+ : objects.length > 1
1350
+ ? 'objects deleted label'
1351
+ : 'object deleted label';
1252
1352
 
1253
1353
  return {
1254
1354
  data: true,
1255
1355
  undoable: {
1256
- // Consider using a translation key here.
1356
+ // TODO(ZaymonFC): Pluralize if more than one object.
1257
1357
  message: translations[0]['en-US'][SPACE_PLUGIN][undoMessageKey],
1258
1358
  data: deletionData,
1259
1359
  },
1260
1360
  };
1261
1361
  } else {
1262
1362
  const undoData = intent.data;
1263
- if (undoData && isEchoObject(undoData.object) && undoData.parentCollection instanceof CollectionType) {
1363
+ if (
1364
+ undoData?.objects?.length &&
1365
+ undoData.objects.every(isEchoObject) &&
1366
+ undoData.parentCollection instanceof CollectionType
1367
+ ) {
1264
1368
  // Restore the object to the space.
1265
- const restoredObject = space.db.add(undoData.object);
1369
+ const restoredObjects = undoData.objects.map((obj: Expando) => space.db.add(obj));
1266
1370
 
1267
- // Restore nested objects if the object was a collection.
1268
- undoData.nestedObjects.forEach((obj: Expando) => {
1371
+ // Restore nested objects to the space.
1372
+ undoData.nestedObjectsList.flat().forEach((obj: Expando) => {
1269
1373
  space.db.add(obj);
1270
1374
  });
1271
1375
 
1272
- // Restore the object to its original position in the collection.
1273
- if (undoData.index !== -1) {
1274
- undoData.parentCollection.objects.splice(undoData.index, 0, restoredObject as Expando);
1275
- }
1376
+ undoData.indices.forEach((index: number, i: number) => {
1377
+ if (index !== -1) {
1378
+ undoData.parentCollection.objects.splice(index, 0, restoredObjects[i] as Expando);
1379
+ }
1380
+ });
1276
1381
 
1277
- // Restore active state if it was active before removal.
1278
1382
  if (undoData.wasActive.length > 0) {
1279
1383
  await intentPlugin?.provides.intent.dispatch({
1280
1384
  action: NavigationAction.OPEN,