@dxos/plugin-space 0.6.13 → 0.6.14-main.2b6a0f3

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 (94) hide show
  1. package/dist/lib/browser/{chunk-DTVUOG2C.mjs → chunk-47SVNCZM.mjs} +24 -5
  2. package/dist/lib/browser/chunk-47SVNCZM.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-LZEGRS7H.mjs → chunk-AVLRQF6L.mjs} +1 -1
  4. package/dist/lib/browser/chunk-AVLRQF6L.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +609 -250
  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-CTYDNFGG.cjs} +30 -9
  11. package/dist/lib/node/chunk-CTYDNFGG.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 +827 -471
  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-PLPMYTLC.mjs +116 -0
  22. package/dist/lib/node-esm/chunk-PLPMYTLC.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 +3000 -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/SaveStatus.d.ts +3 -0
  38. package/dist/types/src/components/SaveStatus.d.ts.map +1 -0
  39. package/dist/types/src/components/ShareSpaceButton.stories.d.ts +3 -91
  40. package/dist/types/src/components/ShareSpaceButton.stories.d.ts.map +1 -1
  41. package/dist/types/src/components/SpaceMain/SpaceMain.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/SyncStatus/SyncStatus.d.ts +13 -0
  46. package/dist/types/src/components/SyncStatus/SyncStatus.d.ts.map +1 -0
  47. package/dist/types/src/components/SyncStatus/SyncStatus.stories.d.ts +24 -0
  48. package/dist/types/src/components/SyncStatus/SyncStatus.stories.d.ts.map +1 -0
  49. package/dist/types/src/components/SyncStatus/index.d.ts +2 -0
  50. package/dist/types/src/components/SyncStatus/index.d.ts.map +1 -0
  51. package/dist/types/src/components/SyncStatus/types.d.ts +14 -0
  52. package/dist/types/src/components/SyncStatus/types.d.ts.map +1 -0
  53. package/dist/types/src/components/index.d.ts +3 -2
  54. package/dist/types/src/components/index.d.ts.map +1 -1
  55. package/dist/types/src/meta.d.ts.map +1 -1
  56. package/dist/types/src/translations.d.ts +6 -0
  57. package/dist/types/src/translations.d.ts.map +1 -1
  58. package/dist/types/src/types/thread.d.ts +15 -1
  59. package/dist/types/src/types/thread.d.ts.map +1 -1
  60. package/dist/types/src/types/types.d.ts +18 -1
  61. package/dist/types/src/types/types.d.ts.map +1 -1
  62. package/dist/types/src/util.d.ts +3 -6
  63. package/dist/types/src/util.d.ts.map +1 -1
  64. package/package.json +45 -36
  65. package/src/SpacePlugin.tsx +173 -90
  66. package/src/components/DefaultObjectSettings.tsx +32 -0
  67. package/src/components/MenuFooter.tsx +2 -2
  68. package/src/components/SaveStatus.tsx +95 -0
  69. package/src/components/ShareSpaceButton.stories.tsx +11 -7
  70. package/src/components/SpaceMain/SpaceMain.tsx +1 -22
  71. package/src/components/SpacePresence.stories.tsx +11 -9
  72. package/src/components/SpacePresence.tsx +1 -1
  73. package/src/components/SpaceSettings.tsx +32 -3
  74. package/src/components/SyncStatus/SyncStatus.stories.tsx +65 -0
  75. package/src/components/SyncStatus/SyncStatus.tsx +188 -0
  76. package/src/components/SyncStatus/index.ts +5 -0
  77. package/src/components/SyncStatus/types.ts +77 -0
  78. package/src/components/index.ts +3 -2
  79. package/src/meta.ts +3 -1
  80. package/src/translations.ts +6 -0
  81. package/src/types/collection.ts +1 -1
  82. package/src/types/thread.ts +12 -2
  83. package/src/types/types.ts +25 -1
  84. package/src/util.tsx +20 -55
  85. package/dist/lib/browser/chunk-DTVUOG2C.mjs.map +0 -7
  86. package/dist/lib/browser/chunk-LZEGRS7H.mjs.map +0 -7
  87. package/dist/lib/node/chunk-6CNYF6YU.cjs.map +0 -7
  88. package/dist/lib/node/chunk-CVZPI2P3.cjs.map +0 -7
  89. package/dist/types/src/components/EmptySpace.d.ts +0 -3
  90. package/dist/types/src/components/EmptySpace.d.ts.map +0 -1
  91. package/dist/types/src/components/EmptyTree.d.ts +0 -3
  92. package/dist/types/src/components/EmptyTree.d.ts.map +0 -1
  93. package/src/components/EmptySpace.tsx +0 -25
  94. package/src/components/EmptyTree.tsx +0 -25
@@ -2,74 +2,77 @@
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 {
10
9
  type IntentDispatcher,
11
10
  type IntentPluginProvides,
12
11
  LayoutAction,
13
- Surface,
14
12
  type LocationProvides,
15
13
  NavigationAction,
16
14
  type Plugin,
17
15
  type PluginDefinition,
18
- openIds,
16
+ Surface,
19
17
  firstIdInPart,
18
+ openIds,
19
+ parseGraphPlugin,
20
20
  parseIntentPlugin,
21
- parseNavigationPlugin,
22
21
  parseMetadataResolverPlugin,
22
+ parseNavigationPlugin,
23
23
  resolvePlugin,
24
- parseGraphPlugin,
25
24
  } from '@dxos/app-framework';
26
25
  import { EventSubscriptions, type Trigger, type UnsubscribeCallback } from '@dxos/async';
27
- import { type Identifiable, isReactiveObject, type EchoReactiveObject } from '@dxos/echo-schema';
26
+ import { type HasId, isReactiveObject } from '@dxos/echo-schema';
27
+ import { scheduledEffect } from '@dxos/echo-signals/core';
28
28
  import { LocalStorageStore } from '@dxos/local-storage';
29
29
  import { log } from '@dxos/log';
30
30
  import { Migrations } from '@dxos/migrations';
31
31
  import { type AttentionPluginProvides, parseAttentionPlugin } from '@dxos/plugin-attention';
32
32
  import { type ClientPluginProvides, parseClientPlugin } from '@dxos/plugin-client';
33
- import { createExtension, isGraphNode, memoize, type Node, toSignal } from '@dxos/plugin-graph';
33
+ import { type Node, createExtension, memoize, toSignal } from '@dxos/plugin-graph';
34
34
  import { ObservabilityAction } from '@dxos/plugin-observability/meta';
35
35
  import { type Client, PublicKey } from '@dxos/react-client';
36
36
  import {
37
+ Expando,
38
+ type EchoReactiveObject,
39
+ Filter,
37
40
  type PropertiesTypeProps,
38
41
  type Space,
42
+ SpaceState,
39
43
  create,
40
- Expando,
41
- Filter,
42
44
  fullyQualifiedId,
43
45
  getSpace,
44
46
  getTypename,
45
47
  isEchoObject,
46
48
  isSpace,
47
49
  loadObjectReferences,
48
- SpaceState,
50
+ parseFullyQualifiedId,
49
51
  } from '@dxos/react-client/echo';
50
52
  import { Dialog } from '@dxos/react-ui';
51
- import { InvitationManager, type InvitationManagerProps, osTranslations, ClipboardProvider } from '@dxos/shell/react';
53
+ import { ClipboardProvider, InvitationManager, type InvitationManagerProps, osTranslations } from '@dxos/shell/react';
52
54
  import { ComplexMap, nonNullable, reduceGroupBy } from '@dxos/util';
53
55
 
54
56
  import {
55
57
  AwaitingObject,
56
58
  CollectionMain,
57
59
  CollectionSection,
58
- EmptySpace,
59
- EmptyTree,
60
+ DefaultObjectSettings,
60
61
  MenuFooter,
61
62
  MissingObject,
62
63
  PopoverRenameObject,
63
64
  PopoverRenameSpace,
65
+ SaveStatus,
64
66
  ShareSpaceButton,
65
67
  SmallPresence,
66
68
  SmallPresenceLive,
67
69
  SpacePresence,
68
70
  SpaceSettings,
71
+ SyncStatus,
69
72
  } from './components';
70
73
  import meta, { SPACE_PLUGIN, SpaceAction } from './meta';
71
74
  import translations from './translations';
72
- import { CollectionType, type SpacePluginProvides, type SpaceSettingsProps, type PluginState } from './types';
75
+ import { CollectionType, type PluginState, type SpacePluginProvides, type SpaceSettingsProps } from './types';
73
76
  import {
74
77
  COMPOSER_SPACE_LOCK,
75
78
  SHARED,
@@ -118,7 +121,9 @@ export const SpacePlugin = ({
118
121
  firstRun,
119
122
  onFirstRun,
120
123
  }: SpacePluginOptions = {}): PluginDefinition<SpacePluginProvides> => {
121
- const settings = new LocalStorageStore<SpaceSettingsProps>(SPACE_PLUGIN);
124
+ const settings = new LocalStorageStore<SpaceSettingsProps>(SPACE_PLUGIN, {
125
+ onSpaceCreate: 'dxos.org/plugin/markdown/action/create',
126
+ });
122
127
  const state = new LocalStorageStore<PluginState>(SPACE_PLUGIN, {
123
128
  awaiting: undefined,
124
129
  spaceNames: {},
@@ -171,9 +176,10 @@ export const SpacePlugin = ({
171
176
  .filter((space) => space.state.get() === SpaceState.SPACE_READY)
172
177
  .forEach((space) => {
173
178
  subscriptions.add(
174
- effect(() => {
175
- state.values.spaceNames[space.id] = space.properties.name;
176
- }),
179
+ scheduledEffect(
180
+ () => ({ name: space.properties.name }),
181
+ ({ name }) => (state.values.spaceNames[space.id] = name),
182
+ ),
177
183
  );
178
184
  });
179
185
  }).unsubscribe,
@@ -181,54 +187,56 @@ export const SpacePlugin = ({
181
187
 
182
188
  // Broadcast active node to other peers in the space.
183
189
  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, []);
190
+ scheduledEffect(
191
+ () => ({
192
+ ids: openIds(location.active),
193
+ removed: location.closed ? [location.closed].flat() : [],
194
+ }),
195
+ ({ ids, removed }) => {
196
+ const send = () => {
197
+ const spaces = client.spaces.get();
198
+ const identity = client.halo.identity.get();
199
+ if (identity && location.active) {
200
+ // Group parts by space for efficient messaging.
201
+ const idsBySpace = reduceGroupBy(ids, (id) => {
202
+ const [spaceId] = id.split(':'); // TODO(burdon): Factor out.
203
+ return spaceId;
204
+ });
205
+
206
+ // NOTE: Ensure all spaces are included so that we send the correct `removed` object arrays.
207
+ for (const space of spaces) {
208
+ if (!idsBySpace.has(space.id)) {
209
+ idsBySpace.set(space.id, []);
210
+ }
201
211
  }
202
- }
203
212
 
204
- for (const [spaceId, ids] of idsBySpace) {
205
- const space = spaces.find((space) => space.id === spaceId);
206
- if (!space) {
207
- continue;
208
- }
213
+ for (const [spaceId, ids] of idsBySpace) {
214
+ const space = spaces.find((space) => space.id === spaceId);
215
+ if (!space) {
216
+ continue;
217
+ }
209
218
 
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
- });
219
+ void space
220
+ .postMessage('viewing', {
221
+ identityKey: identity.identityKey.toHex(),
222
+ attended: attention.attended ? [...attention.attended] : [],
223
+ added: ids,
224
+ // TODO(Zan): When we re-open a part, we should remove it from the removed list in the navigation plugin.
225
+ removed: removed.filter((id) => !ids.includes(id)),
226
+ })
227
+ // TODO(burdon): This seems defensive; why would this fail? Backoff interval.
228
+ .catch((err) => {
229
+ log.warn('Failed to broadcast active node for presence.', { err: err.message });
230
+ });
231
+ }
224
232
  }
225
- }
226
- };
233
+ };
227
234
 
228
- send();
229
- const interval = setInterval(() => send(), ACTIVE_NODE_BROADCAST_INTERVAL);
230
- return () => clearInterval(interval);
231
- }),
235
+ send();
236
+ const interval = setInterval(() => send(), ACTIVE_NODE_BROADCAST_INTERVAL);
237
+ return () => clearInterval(interval);
238
+ },
239
+ ),
232
240
  );
233
241
 
234
242
  // Listen for active nodes from other peers in the space.
@@ -340,8 +348,7 @@ export const SpacePlugin = ({
340
348
  records: {
341
349
  [CollectionType.typename]: {
342
350
  placeholder: ['unnamed collection label', { ns: SPACE_PLUGIN }],
343
- icon: (props: IconProps) => <CardsThree {...props} />,
344
- iconSymbol: 'ph--cards-three--regular',
351
+ icon: 'ph--cards-three--regular',
345
352
  // TODO(wittjosiah): Move out of metadata.
346
353
  loadReferences: (collection: CollectionType) =>
347
354
  loadObjectReferences(collection, (collection) => [
@@ -362,22 +369,18 @@ export const SpacePlugin = ({
362
369
  case 'main':
363
370
  // TODO(wittjosiah): Need to avoid shotgun parsing space state everywhere.
364
371
  return isSpace(primary) && primary.state.get() === SpaceState.SPACE_READY ? (
365
- <Surface data={{ active: primary.properties[CollectionType.typename] }} role={role} {...rest} />
372
+ <Surface
373
+ data={{ active: primary.properties[CollectionType.typename], id: primary.id }}
374
+ role={role}
375
+ {...rest}
376
+ />
366
377
  ) : primary instanceof CollectionType ? (
367
378
  { node: <CollectionMain collection={primary} />, disposition: 'fallback' }
368
379
  ) : typeof primary === 'string' && primary.length === OBJECT_ID_LENGTH ? (
369
380
  <MissingObject id={primary} />
370
381
  ) : 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
- }
382
+ case 'complementary--settings':
383
+ return isEchoObject(data.subject) ? <DefaultObjectSettings object={data.subject} /> : null;
381
384
  case 'dialog':
382
385
  if (data.component === 'dxos.org/plugin/space/InvitationManagerDialog') {
383
386
  return (
@@ -398,6 +401,7 @@ export const SpacePlugin = ({
398
401
  return <PopoverRenameObject object={data.subject} />;
399
402
  }
400
403
  return null;
404
+ // TODO(burdon): Add role name syntax to minimal plugin docs.
401
405
  case 'presence--glyph': {
402
406
  return isReactiveObject(data.object) ? (
403
407
  <SmallPresenceLive viewers={state.values.viewersByObject[fullyQualifiedId(data.object)]} />
@@ -441,6 +445,14 @@ export const SpacePlugin = ({
441
445
  } else {
442
446
  return <MenuFooter object={data.object} />;
443
447
  }
448
+ case 'status': {
449
+ return (
450
+ <>
451
+ <SyncStatus />
452
+ <SaveStatus />
453
+ </>
454
+ );
455
+ }
444
456
  default:
445
457
  return null;
446
458
  }
@@ -540,8 +552,7 @@ export const SpacePlugin = ({
540
552
  },
541
553
  properties: {
542
554
  label: ['create space label', { ns: SPACE_PLUGIN }],
543
- icon: (props: IconProps) => <Plus {...props} />,
544
- iconSymbol: 'ph--plus--regular',
555
+ icon: 'ph--plus--regular',
545
556
  disposition: 'toolbar',
546
557
  testId: 'spacePlugin.createSpace',
547
558
  },
@@ -561,8 +572,7 @@ export const SpacePlugin = ({
561
572
  },
562
573
  properties: {
563
574
  label: ['join space label', { ns: SPACE_PLUGIN }],
564
- icon: (props: IconProps) => <SignIn {...props} />,
565
- iconSymbol: 'ph--sign-in--regular',
575
+ icon: 'ph--sign-in--regular',
566
576
  testId: 'spacePlugin.joinSpace',
567
577
  },
568
578
  },
@@ -707,6 +717,52 @@ export const SpacePlugin = ({
707
717
  .filter(nonNullable);
708
718
  },
709
719
  }),
720
+
721
+ // Create nodes for object settings.
722
+ createExtension({
723
+ id: `${SPACE_PLUGIN}/settings-for-subject`,
724
+ resolver: ({ id }) => {
725
+ // TODO(Zan): Find util (or make one).
726
+ if (!id.endsWith('~settings')) {
727
+ return;
728
+ }
729
+
730
+ const [subjectId] = id.split('~');
731
+ const [spaceId, objectId] = parseFullyQualifiedId(subjectId);
732
+ const space = client.spaces.get().find((space) => space.id === spaceId);
733
+ const object = toSignal(
734
+ (onChange) => {
735
+ const timeout = setTimeout(async () => {
736
+ await space?.db.loadObjectById(objectId);
737
+ onChange();
738
+ });
739
+
740
+ return () => clearTimeout(timeout);
741
+ },
742
+ () => space?.db.getObjectById(objectId),
743
+ subjectId,
744
+ );
745
+ if (!object || !subjectId) {
746
+ return;
747
+ }
748
+
749
+ const meta = resolve(getTypename(object) ?? '');
750
+ const label = meta.label?.(object) ||
751
+ object.name ||
752
+ meta.placeholder || ['unnamed object settings label', { ns: SPACE_PLUGIN }];
753
+
754
+ return {
755
+ id,
756
+ type: 'orphan-settings-for-subject',
757
+ data: null,
758
+ properties: {
759
+ icon: 'ph--gear--regular',
760
+ label,
761
+ object,
762
+ },
763
+ };
764
+ },
765
+ }),
710
766
  ];
711
767
  },
712
768
  serializer: (plugins) => {
@@ -802,9 +858,21 @@ export const SpacePlugin = ({
802
858
  }
803
859
 
804
860
  return {
805
- data: { space, id: space.id, activeParts: { main: [space.id] } },
806
-
861
+ data: {
862
+ space,
863
+ id: space.id,
864
+ activeParts: { main: [space.id] },
865
+ },
807
866
  intents: [
867
+ ...(settings.values.onSpaceCreate
868
+ ? [
869
+ [
870
+ { action: settings.values.onSpaceCreate, data: { space } },
871
+ { action: SpaceAction.ADD_OBJECT, data: { target: space } },
872
+ { action: NavigationAction.EXPOSE },
873
+ ],
874
+ ]
875
+ : []),
808
876
  [
809
877
  {
810
878
  action: ObservabilityAction.SEND_EVENT,
@@ -825,9 +893,26 @@ export const SpacePlugin = ({
825
893
  const { space } = await client.shell.joinSpace({ invitationCode: intent.data?.invitationCode });
826
894
  if (space) {
827
895
  return {
828
- data: { space, id: space.id, activeParts: { main: [space.id] } },
829
-
896
+ data: {
897
+ space,
898
+ id: space.id,
899
+ activeParts: { main: [space.id] },
900
+ },
830
901
  intents: [
902
+ [
903
+ {
904
+ action: LayoutAction.SET_LAYOUT,
905
+ data: {
906
+ element: 'toast',
907
+ subject: {
908
+ id: `${SPACE_PLUGIN}/join-success`,
909
+ duration: 10_000,
910
+ title: translations[0]['en-US'][SPACE_PLUGIN]['join success label'],
911
+ closeLabel: translations[0]['en-US'][SPACE_PLUGIN]['dismiss label'],
912
+ },
913
+ },
914
+ },
915
+ ],
831
916
  [
832
917
  {
833
918
  action: ObservabilityAction.SEND_EVENT,
@@ -1020,8 +1105,7 @@ export const SpacePlugin = ({
1020
1105
  title: translations[0]['en-US'][SPACE_PLUGIN]['space limit label'],
1021
1106
  description: translations[0]['en-US'][SPACE_PLUGIN]['space limit description'],
1022
1107
  duration: 5_000,
1023
- icon: (props: IconProps) => <Warning {...props} />,
1024
- iconSymbol: 'ph--warning--regular',
1108
+ icon: 'ph--warning--regular',
1025
1109
  actionLabel: translations[0]['en-US'][SPACE_PLUGIN]['remove deleted objects label'],
1026
1110
  actionAlt: translations[0]['en-US'][SPACE_PLUGIN]['remove deleted objects alt'],
1027
1111
  // TODO(wittjosiah): Use OS namespace.
@@ -1047,20 +1131,20 @@ export const SpacePlugin = ({
1047
1131
  }
1048
1132
 
1049
1133
  if (intent.data?.target instanceof CollectionType) {
1050
- intent.data?.target.objects.push(object as Identifiable);
1134
+ intent.data?.target.objects.push(object as HasId);
1051
1135
  } else if (isSpace(intent.data?.target)) {
1052
1136
  const collection = space.properties[CollectionType.typename];
1053
1137
  if (collection instanceof CollectionType) {
1054
- collection.objects.push(object as Identifiable);
1138
+ collection.objects.push(object as HasId);
1055
1139
  } else {
1056
1140
  // 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: {} });
1141
+ const collection = create(CollectionType, { objects: [object as HasId], views: {} });
1058
1142
  space.properties[CollectionType.typename] = collection;
1059
1143
  }
1060
1144
  }
1061
1145
 
1062
1146
  return {
1063
- data: { id: object.id, object, activeParts: { main: [fullyQualifiedId(object)] } },
1147
+ data: { id: fullyQualifiedId(object), object, activeParts: { main: [fullyQualifiedId(object)] } },
1064
1148
  intents: [
1065
1149
  [
1066
1150
  {
@@ -1115,7 +1199,6 @@ export const SpacePlugin = ({
1115
1199
  activeParts: {
1116
1200
  main: deletionData.wasActive,
1117
1201
  sidebar: deletionData.wasActive,
1118
- complementary: deletionData.wasActive,
1119
1202
  },
1120
1203
  },
1121
1204
  });
@@ -0,0 +1,32 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { type EchoReactiveObject } from '@dxos/client/echo';
8
+ import { Input, useTranslation } from '@dxos/react-ui';
9
+
10
+ import { SPACE_PLUGIN } from '../meta';
11
+
12
+ export type DefaultObjectSettingsProps = {
13
+ object: EchoReactiveObject<any>;
14
+ };
15
+
16
+ export const DefaultObjectSettings = ({ object }: DefaultObjectSettingsProps) => {
17
+ const { t } = useTranslation(SPACE_PLUGIN);
18
+ return (
19
+ <div role='form' className='flex flex-col w-full p-2 gap-1'>
20
+ <Input.Root>
21
+ <Input.Label>{t('name label')}</Input.Label>
22
+ <Input.TextInput
23
+ placeholder={t('name placeholder')}
24
+ value={object.name}
25
+ onChange={(event) => {
26
+ object.name = event.target.value;
27
+ }}
28
+ />
29
+ </Input.Root>
30
+ </div>
31
+ );
32
+ };
@@ -1,11 +1,11 @@
1
1
  //
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
+
4
5
  import { Planet } from '@phosphor-icons/react';
5
6
  import React from 'react';
6
7
 
7
- import { getSpace } from '@dxos/client/echo';
8
- import type { EchoReactiveObject } from '@dxos/echo-schema';
8
+ import { type EchoReactiveObject, getSpace } from '@dxos/client/echo';
9
9
  import { useClient } from '@dxos/react-client';
10
10
  import { DropdownMenu, toLocalizedString, useTranslation } from '@dxos/react-ui';
11
11
 
@@ -0,0 +1,95 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useEffect, useState } from 'react';
6
+
7
+ import { type UnsubscribeCallback } from '@dxos/async';
8
+ import { type Client } from '@dxos/client';
9
+ import { type Space, type SpaceId } from '@dxos/client/echo';
10
+ import { Context } from '@dxos/context';
11
+ import { StatusBar } from '@dxos/plugin-status-bar';
12
+ import { useClient } from '@dxos/react-client';
13
+ import { Icon, useTranslation } from '@dxos/react-ui';
14
+
15
+ import { SPACE_PLUGIN } from '../meta';
16
+
17
+ export const SaveStatus = () => {
18
+ const { t } = useTranslation(SPACE_PLUGIN);
19
+ const client = useClient();
20
+ const [state, setState] = useState<'saved' | 'saving'>('saved');
21
+ useEffect(() => {
22
+ return createClientSaveTracker(client, (state) => {
23
+ setState(state);
24
+ });
25
+ }, []);
26
+
27
+ return (
28
+ <StatusBar.Item title={state === 'saving' ? t('saving label') : t('saved label')}>
29
+ <Icon icon={state === 'saving' ? 'ph--arrows-clockwise--regular' : 'ph--check-circle--regular'} size={4} />
30
+ </StatusBar.Item>
31
+ );
32
+ };
33
+
34
+ const createClientSaveTracker = (client: Client, cb: (state: 'saved' | 'saving') => void) => {
35
+ const unsubscribeCallbacks: Record<SpaceId, UnsubscribeCallback> = {};
36
+ const state: Record<SpaceId, 'saved' | 'saving'> = {};
37
+
38
+ const install = (spaces: Space[]) => {
39
+ for (const space of spaces) {
40
+ if (state[space.id]) {
41
+ continue;
42
+ }
43
+
44
+ state[space.id] = 'saved';
45
+ unsubscribeCallbacks[space.id] = createSpaceSaveTracker(space, (s) => {
46
+ state[space.id] = s;
47
+ cb(Object.values(state).some((s) => s === 'saving') ? 'saving' : 'saved');
48
+ });
49
+ }
50
+ };
51
+ client.spaces.subscribe((spaces) => {
52
+ install(spaces);
53
+ });
54
+ install(client.spaces.get());
55
+
56
+ return () => {
57
+ for (const unsubscribe of Object.values(unsubscribeCallbacks)) {
58
+ unsubscribe();
59
+ }
60
+ };
61
+ };
62
+
63
+ const createSpaceSaveTracker = (space: Space, cb: (state: 'saved' | 'saving') => void): UnsubscribeCallback => {
64
+ const ctx = new Context();
65
+
66
+ void space.waitUntilReady().then(() => {
67
+ if (ctx.disposed) {
68
+ return;
69
+ }
70
+
71
+ let hasUnsavedChanges = false;
72
+ let lastFlushPromise: Promise<void> | undefined;
73
+ space.crud.saveStateChanged.on(ctx, ({ unsavedDocuments }) => {
74
+ hasUnsavedChanges = unsavedDocuments.length > 0;
75
+ });
76
+ space.crud.saveStateChanged.debounce(500).on(ctx, () => {
77
+ if (hasUnsavedChanges) {
78
+ lastFlushPromise = undefined;
79
+ cb('saving');
80
+ } else {
81
+ const flushPromise = space.crud.flush();
82
+ lastFlushPromise = flushPromise;
83
+ void flushPromise.then(() => {
84
+ if (lastFlushPromise === flushPromise) {
85
+ cb('saved');
86
+ }
87
+ });
88
+ }
89
+ });
90
+ });
91
+
92
+ return () => {
93
+ void ctx.dispose();
94
+ };
95
+ };
@@ -4,20 +4,24 @@
4
4
 
5
5
  import '@dxos-theme';
6
6
 
7
+ import { type Meta } from '@storybook/react';
8
+
7
9
  import { withTheme } from '@dxos/storybook-utils';
8
10
 
9
11
  import { ShareSpaceButtonImpl } from './ShareSpaceButton';
10
12
  import translations from '../translations';
11
13
 
12
- export default {
13
- title: 'plugin-space/ShareSpaceButton',
14
- component: ShareSpaceButtonImpl,
15
- decorators: [withTheme],
16
- parameters: { translations },
17
- };
18
-
19
14
  export const Default = {
20
15
  args: {
21
16
  onClick: () => console.log('clicked'),
22
17
  },
23
18
  };
19
+
20
+ const meta: Meta = {
21
+ title: 'plugins/plugin-space/ShareSpaceButton',
22
+ component: ShareSpaceButtonImpl,
23
+ decorators: [withTheme],
24
+ parameters: { translations },
25
+ };
26
+
27
+ export default meta;
@@ -6,35 +6,14 @@ import { Command } from '@phosphor-icons/react';
6
6
  import React from 'react';
7
7
 
8
8
  import { Surface } from '@dxos/app-framework';
9
- import { type Action } from '@dxos/plugin-graph';
10
9
  import { SpaceState, type Space } from '@dxos/react-client/echo';
11
- import { Button, Main, useTranslation, toLocalizedString } from '@dxos/react-ui';
10
+ import { Main, useTranslation } from '@dxos/react-ui';
12
11
  import { getSize, mx, topbarBlockPaddingStart } from '@dxos/react-ui-theme';
13
12
  import { ClipboardProvider } from '@dxos/shell/react';
14
13
 
15
14
  import { SpaceMembersSection } from './SpaceMembersSection';
16
15
  import { SPACE_PLUGIN } from '../../meta';
17
16
 
18
- const _InFlowSpaceActions = ({ actionsMap }: { actionsMap: Record<string, Action> }) => {
19
- const { t } = useTranslation(SPACE_PLUGIN);
20
- return (
21
- <section className='mbe-4 col-start-2 col-end-4 md:col-end-7 grid gap-2 auto-rows-min grid-cols-[repeat(auto-fill,minmax(8rem,1fr))]'>
22
- {Object.entries(actionsMap)
23
- .filter(([_, { properties }]) => properties?.mainAreaDisposition === 'in-flow')
24
- .map(([actionId, { data: invoke, properties }]) => {
25
- const Icon = properties?.icon;
26
- const label = properties?.label;
27
- return (
28
- <Button key={actionId} classNames='block text-center plb-2 font-normal'>
29
- {Icon && <Icon size={5} className='mli-auto' />}
30
- <p>{toLocalizedString(label, t)}</p>
31
- </Button>
32
- );
33
- })}
34
- </section>
35
- );
36
- };
37
-
38
17
  const KeyShortcuts = () => {
39
18
  const { t } = useTranslation(SPACE_PLUGIN);
40
19
  return (