@dxos/plugin-space 0.8.2-main.f11618f → 0.8.2-staging.7ac8446

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 (245) hide show
  1. package/dist/lib/browser/{app-graph-builder-ZWNOWRAX.mjs → app-graph-builder-GMFCSOLG.mjs} +7 -61
  2. package/dist/lib/browser/app-graph-builder-GMFCSOLG.mjs.map +7 -0
  3. package/dist/lib/browser/{app-graph-serializer-UUJH7JRN.mjs → app-graph-serializer-DSF2U3A5.mjs} +8 -8
  4. package/dist/lib/browser/app-graph-serializer-DSF2U3A5.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-DIJ7LMCS.mjs → chunk-5BDV575R.mjs} +8 -20
  6. package/dist/lib/browser/chunk-5BDV575R.mjs.map +7 -0
  7. package/dist/lib/browser/{chunk-O7WGQVLQ.mjs → chunk-AYW4IDRT.mjs} +4 -35
  8. package/dist/lib/browser/chunk-AYW4IDRT.mjs.map +7 -0
  9. package/dist/lib/browser/{chunk-4226DMDP.mjs → chunk-JZXWPMLA.mjs} +666 -522
  10. package/dist/lib/browser/chunk-JZXWPMLA.mjs.map +7 -0
  11. package/dist/lib/browser/{chunk-JS6ZV4GV.mjs → chunk-LO5UL6RU.mjs} +141 -60
  12. package/dist/lib/browser/chunk-LO5UL6RU.mjs.map +7 -0
  13. package/dist/lib/browser/{identity-created-NRVE4XLL.mjs → identity-created-JR7BNXFH.mjs} +4 -4
  14. package/dist/lib/browser/identity-created-JR7BNXFH.mjs.map +7 -0
  15. package/dist/lib/browser/index.mjs +33 -12
  16. package/dist/lib/browser/index.mjs.map +3 -3
  17. package/dist/lib/browser/{intent-resolver-NP55M7C7.mjs → intent-resolver-RKYILWWQ.mjs} +82 -92
  18. package/dist/lib/browser/intent-resolver-RKYILWWQ.mjs.map +7 -0
  19. package/dist/lib/browser/meta.json +1 -1
  20. package/dist/lib/browser/{react-root-XKAUZ3X2.mjs → react-root-6H7NX2M2.mjs} +4 -4
  21. package/dist/lib/browser/{react-surface-2DW2UDRX.mjs → react-surface-7AGLOVMK.mjs} +47 -121
  22. package/dist/lib/browser/react-surface-7AGLOVMK.mjs.map +7 -0
  23. package/dist/lib/browser/{settings-MVT32NP6.mjs → settings-PJPTJUPE.mjs} +4 -4
  24. package/dist/lib/browser/settings-PJPTJUPE.mjs.map +7 -0
  25. package/dist/lib/browser/{spaces-ready-ERNSICUW.mjs → spaces-ready-BSSP7HHG.mjs} +5 -5
  26. package/dist/lib/browser/spaces-ready-BSSP7HHG.mjs.map +7 -0
  27. package/dist/lib/browser/{state-CYV6QCTN.mjs → state-X7VLCC6E.mjs} +3 -1
  28. package/dist/lib/browser/state-X7VLCC6E.mjs.map +7 -0
  29. package/dist/lib/browser/types/index.mjs +1 -7
  30. package/dist/lib/node/{app-graph-builder-LLIV422L.cjs → app-graph-builder-DPY7AUZE.cjs} +30 -83
  31. package/dist/lib/node/app-graph-builder-DPY7AUZE.cjs.map +7 -0
  32. package/dist/lib/node/{app-graph-serializer-M6Z2OPA4.cjs → app-graph-serializer-JELGJUAY.cjs} +20 -20
  33. package/dist/lib/node/app-graph-serializer-JELGJUAY.cjs.map +7 -0
  34. package/dist/lib/node/{chunk-ZMQO74LX.cjs → chunk-3GKCNADA.cjs} +655 -513
  35. package/dist/lib/node/chunk-3GKCNADA.cjs.map +7 -0
  36. package/dist/lib/node/{chunk-UOCR4G2D.cjs → chunk-PU2EYH4E.cjs} +12 -24
  37. package/dist/lib/node/chunk-PU2EYH4E.cjs.map +7 -0
  38. package/dist/lib/node/{chunk-XADZLQAJ.cjs → chunk-TUZWEPGX.cjs} +167 -84
  39. package/dist/lib/node/chunk-TUZWEPGX.cjs.map +7 -0
  40. package/dist/lib/node/{chunk-EQ5BPSQ7.cjs → chunk-WDEIFDTX.cjs} +7 -41
  41. package/dist/lib/node/chunk-WDEIFDTX.cjs.map +7 -0
  42. package/dist/lib/node/{identity-created-YDTRMOMX.cjs → identity-created-XU4HFV2T.cjs} +6 -6
  43. package/dist/lib/node/identity-created-XU4HFV2T.cjs.map +7 -0
  44. package/dist/lib/node/index.cjs +89 -68
  45. package/dist/lib/node/index.cjs.map +3 -3
  46. package/dist/lib/node/{intent-resolver-3J52ARFL.cjs → intent-resolver-VJ7YV74L.cjs} +131 -141
  47. package/dist/lib/node/intent-resolver-VJ7YV74L.cjs.map +7 -0
  48. package/dist/lib/node/meta.json +1 -1
  49. package/dist/lib/node/{react-root-XUE2J7HT.cjs → react-root-TEL5RW3N.cjs} +8 -8
  50. package/dist/lib/node/{react-surface-IWSTOZ2E.cjs → react-surface-2H3S5TY5.cjs} +74 -146
  51. package/dist/lib/node/react-surface-2H3S5TY5.cjs.map +7 -0
  52. package/dist/lib/node/{settings-JLV7YT6Q.cjs → settings-WVFP2UEP.cjs} +7 -7
  53. package/dist/lib/node/settings-WVFP2UEP.cjs.map +7 -0
  54. package/dist/lib/node/{spaces-ready-6EBR4SM4.cjs → spaces-ready-7DNZSUOG.cjs} +12 -12
  55. package/dist/lib/node/spaces-ready-7DNZSUOG.cjs.map +7 -0
  56. package/dist/lib/node/{state-JLN7TGRR.cjs → state-5KX6WBJH.cjs} +6 -4
  57. package/dist/lib/node/state-5KX6WBJH.cjs.map +7 -0
  58. package/dist/lib/node/types/index.cjs +12 -18
  59. package/dist/lib/node/types/index.cjs.map +2 -2
  60. package/dist/lib/node-esm/{app-graph-builder-HSGLCS76.mjs → app-graph-builder-42IGWRPL.mjs} +7 -61
  61. package/dist/lib/node-esm/app-graph-builder-42IGWRPL.mjs.map +7 -0
  62. package/dist/lib/node-esm/{app-graph-serializer-J3B4WSXU.mjs → app-graph-serializer-FXONFKOE.mjs} +8 -8
  63. package/dist/lib/node-esm/app-graph-serializer-FXONFKOE.mjs.map +7 -0
  64. package/dist/lib/node-esm/{chunk-OLISVDCF.mjs → chunk-3ZOUV4DF.mjs} +8 -20
  65. package/dist/lib/node-esm/chunk-3ZOUV4DF.mjs.map +7 -0
  66. package/dist/lib/node-esm/{chunk-PQI4D4SH.mjs → chunk-BEWBZ4Q4.mjs} +666 -522
  67. package/dist/lib/node-esm/chunk-BEWBZ4Q4.mjs.map +7 -0
  68. package/dist/lib/node-esm/{chunk-FJPCLEKN.mjs → chunk-FC4UHDPL.mjs} +4 -35
  69. package/dist/lib/node-esm/chunk-FC4UHDPL.mjs.map +7 -0
  70. package/dist/lib/node-esm/{chunk-ABTVMAG5.mjs → chunk-ZGLK25WQ.mjs} +141 -60
  71. package/dist/lib/node-esm/chunk-ZGLK25WQ.mjs.map +7 -0
  72. package/dist/lib/node-esm/{identity-created-EC6SVYB5.mjs → identity-created-WJKAS2PV.mjs} +4 -4
  73. package/dist/lib/node-esm/identity-created-WJKAS2PV.mjs.map +7 -0
  74. package/dist/lib/node-esm/index.mjs +33 -12
  75. package/dist/lib/node-esm/index.mjs.map +3 -3
  76. package/dist/lib/node-esm/{intent-resolver-MXQIFIRC.mjs → intent-resolver-ZFNSA4CM.mjs} +82 -92
  77. package/dist/lib/node-esm/intent-resolver-ZFNSA4CM.mjs.map +7 -0
  78. package/dist/lib/node-esm/meta.json +1 -1
  79. package/dist/lib/node-esm/{react-root-ZBCJCEFS.mjs → react-root-PRBJMWLQ.mjs} +4 -4
  80. package/dist/lib/node-esm/{react-surface-JCHDAPGM.mjs → react-surface-RCZG2PNF.mjs} +47 -121
  81. package/dist/lib/node-esm/react-surface-RCZG2PNF.mjs.map +7 -0
  82. package/dist/lib/node-esm/{settings-AILIMHTE.mjs → settings-FJZPC2TV.mjs} +4 -4
  83. package/dist/lib/node-esm/settings-FJZPC2TV.mjs.map +7 -0
  84. package/dist/lib/node-esm/{spaces-ready-5PXESKHX.mjs → spaces-ready-7X5PGB2V.mjs} +5 -5
  85. package/dist/lib/node-esm/spaces-ready-7X5PGB2V.mjs.map +7 -0
  86. package/dist/lib/node-esm/{state-YZPY5T5A.mjs → state-Z6E2YTNC.mjs} +3 -1
  87. package/dist/lib/node-esm/state-Z6E2YTNC.mjs.map +7 -0
  88. package/dist/lib/node-esm/types/index.mjs +1 -7
  89. package/dist/types/src/SpacePlugin.d.ts.map +1 -1
  90. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  91. package/dist/types/src/capabilities/index.d.ts +0 -1
  92. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  93. package/dist/types/src/capabilities/intent-resolver.d.ts +1 -2
  94. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -1
  95. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  96. package/dist/types/src/capabilities/state.d.ts.map +1 -1
  97. package/dist/types/src/components/CollectionMain.d.ts.map +1 -1
  98. package/dist/types/src/components/CreateDialog/CreateObjectDialog.d.ts +2 -2
  99. package/dist/types/src/components/CreateDialog/CreateObjectDialog.d.ts.map +1 -1
  100. package/dist/types/src/components/CreateDialog/CreateObjectPanel.d.ts.map +1 -1
  101. package/dist/types/src/components/CreateDialog/CreateSpaceDialog.d.ts.map +1 -1
  102. package/dist/types/src/components/MembersContainer.d.ts.map +1 -1
  103. package/dist/types/src/components/ObjectSettingsContainer/AdvancedObjectSettings.d.ts.map +1 -0
  104. package/dist/types/src/components/ObjectSettingsContainer/BaseObjectSettings.d.ts.map +1 -0
  105. package/dist/types/src/components/ObjectSettingsContainer/ForeignKeys.d.ts.map +1 -0
  106. package/dist/types/src/components/ObjectSettingsContainer/ObjectSettingsContainer.d.ts.map +1 -0
  107. package/dist/types/src/components/ObjectSettingsContainer/index.d.ts.map +1 -0
  108. package/dist/types/src/components/PopoverAddSpace.d.ts.map +1 -1
  109. package/dist/types/src/components/PopoverRenameObject.d.ts +2 -2
  110. package/dist/types/src/components/PopoverRenameObject.d.ts.map +1 -1
  111. package/dist/types/src/components/PopoverRenameSpace.d.ts.map +1 -1
  112. package/dist/types/src/components/ShareSpaceButton.d.ts +9 -0
  113. package/dist/types/src/components/ShareSpaceButton.d.ts.map +1 -0
  114. package/dist/types/src/components/ShareSpaceButton.stories.d.ts +10 -0
  115. package/dist/types/src/components/ShareSpaceButton.stories.d.ts.map +1 -0
  116. package/dist/types/src/components/SpacePluginSettings.d.ts.map +1 -1
  117. package/dist/types/src/components/SpacePresence.d.ts.map +1 -1
  118. package/dist/types/src/components/SpaceSettings/SpacePropertiesForm.d.ts +7 -0
  119. package/dist/types/src/components/SpaceSettings/SpacePropertiesForm.d.ts.map +1 -0
  120. package/dist/types/src/components/SpaceSettings/SpacePropertiesForm.stories.d.ts +7 -0
  121. package/dist/types/src/components/SpaceSettings/SpacePropertiesForm.stories.d.ts.map +1 -0
  122. package/dist/types/src/components/SpaceSettings/SpaceSettingsContainer.d.ts +2 -0
  123. package/dist/types/src/components/SpaceSettings/SpaceSettingsContainer.d.ts.map +1 -1
  124. package/dist/types/src/components/SpaceSettings/index.d.ts +1 -0
  125. package/dist/types/src/components/SpaceSettings/index.d.ts.map +1 -1
  126. package/dist/types/src/components/SyncStatus/InlineSyncStatus.d.ts.map +1 -1
  127. package/dist/types/src/components/SyncStatus/Space.d.ts +13 -0
  128. package/dist/types/src/components/SyncStatus/Space.d.ts.map +1 -0
  129. package/dist/types/src/components/SyncStatus/SyncStatus.d.ts +8 -1
  130. package/dist/types/src/components/SyncStatus/SyncStatus.d.ts.map +1 -1
  131. package/dist/types/src/components/SyncStatus/SyncStatusDetail.stories.d.ts +9 -0
  132. package/dist/types/src/components/SyncStatus/SyncStatusDetail.stories.d.ts.map +1 -0
  133. package/dist/types/src/components/SyncStatus/sync-state.d.ts +18 -0
  134. package/dist/types/src/components/SyncStatus/sync-state.d.ts.map +1 -0
  135. package/dist/types/src/components/index.d.ts +2 -2
  136. package/dist/types/src/components/index.d.ts.map +1 -1
  137. package/dist/types/src/hooks/index.d.ts +0 -1
  138. package/dist/types/src/hooks/index.d.ts.map +1 -1
  139. package/dist/types/src/translations.d.ts +9 -45
  140. package/dist/types/src/translations.d.ts.map +1 -1
  141. package/dist/types/src/types/thread.d.ts +17 -241
  142. package/dist/types/src/types/thread.d.ts.map +1 -1
  143. package/dist/types/src/types/types.d.ts +14 -40
  144. package/dist/types/src/types/types.d.ts.map +1 -1
  145. package/dist/types/src/util.d.ts +5 -27
  146. package/dist/types/src/util.d.ts.map +1 -1
  147. package/package.json +43 -44
  148. package/src/SpacePlugin.tsx +13 -3
  149. package/src/capabilities/app-graph-builder.ts +5 -44
  150. package/src/capabilities/app-graph-serializer.ts +4 -4
  151. package/src/capabilities/identity-created.ts +2 -2
  152. package/src/capabilities/intent-resolver.ts +84 -87
  153. package/src/capabilities/react-surface.tsx +37 -106
  154. package/src/capabilities/settings.ts +2 -2
  155. package/src/capabilities/spaces-ready.ts +2 -2
  156. package/src/capabilities/state.ts +2 -0
  157. package/src/components/CollectionMain.tsx +5 -2
  158. package/src/components/CreateDialog/CreateObjectDialog.stories.tsx +2 -2
  159. package/src/components/CreateDialog/CreateObjectDialog.tsx +3 -3
  160. package/src/components/CreateDialog/CreateObjectPanel.tsx +25 -3
  161. package/src/components/CreateDialog/CreateSpaceDialog.tsx +0 -4
  162. package/src/components/MembersContainer.tsx +54 -80
  163. package/src/components/PopoverAddSpace.tsx +3 -9
  164. package/src/components/PopoverRenameObject.tsx +8 -14
  165. package/src/components/PopoverRenameSpace.tsx +0 -8
  166. package/src/components/ShareSpaceButton.stories.tsx +27 -0
  167. package/src/components/ShareSpaceButton.tsx +32 -0
  168. package/src/components/SpacePluginSettings.tsx +2 -27
  169. package/src/components/SpacePresence.tsx +25 -29
  170. package/src/components/SpaceSettings/{SpaceSettingsContainer.stories.tsx → SpacePropertiesForm.stories.tsx} +6 -6
  171. package/src/components/SpaceSettings/SpacePropertiesForm.tsx +136 -0
  172. package/src/components/SpaceSettings/SpaceSettingsContainer.tsx +39 -157
  173. package/src/components/SpaceSettings/index.ts +1 -0
  174. package/src/components/SyncStatus/InlineSyncStatus.tsx +7 -5
  175. package/src/components/SyncStatus/Space.tsx +133 -0
  176. package/src/components/SyncStatus/SyncStatus.tsx +69 -6
  177. package/src/components/SyncStatus/SyncStatusDetail.stories.tsx +89 -0
  178. package/src/components/SyncStatus/sync-state.ts +101 -0
  179. package/src/components/index.ts +2 -2
  180. package/src/hooks/index.ts +0 -1
  181. package/src/translations.ts +5 -18
  182. package/src/types/types.ts +8 -30
  183. package/src/util.tsx +103 -43
  184. package/dist/lib/browser/app-graph-builder-ZWNOWRAX.mjs.map +0 -7
  185. package/dist/lib/browser/app-graph-serializer-UUJH7JRN.mjs.map +0 -7
  186. package/dist/lib/browser/chunk-4226DMDP.mjs.map +0 -7
  187. package/dist/lib/browser/chunk-DIJ7LMCS.mjs.map +0 -7
  188. package/dist/lib/browser/chunk-JS6ZV4GV.mjs.map +0 -7
  189. package/dist/lib/browser/chunk-O7WGQVLQ.mjs.map +0 -7
  190. package/dist/lib/browser/identity-created-NRVE4XLL.mjs.map +0 -7
  191. package/dist/lib/browser/intent-resolver-NP55M7C7.mjs.map +0 -7
  192. package/dist/lib/browser/react-surface-2DW2UDRX.mjs.map +0 -7
  193. package/dist/lib/browser/settings-MVT32NP6.mjs.map +0 -7
  194. package/dist/lib/browser/spaces-ready-ERNSICUW.mjs.map +0 -7
  195. package/dist/lib/browser/state-CYV6QCTN.mjs.map +0 -7
  196. package/dist/lib/node/app-graph-builder-LLIV422L.cjs.map +0 -7
  197. package/dist/lib/node/app-graph-serializer-M6Z2OPA4.cjs.map +0 -7
  198. package/dist/lib/node/chunk-EQ5BPSQ7.cjs.map +0 -7
  199. package/dist/lib/node/chunk-UOCR4G2D.cjs.map +0 -7
  200. package/dist/lib/node/chunk-XADZLQAJ.cjs.map +0 -7
  201. package/dist/lib/node/chunk-ZMQO74LX.cjs.map +0 -7
  202. package/dist/lib/node/identity-created-YDTRMOMX.cjs.map +0 -7
  203. package/dist/lib/node/intent-resolver-3J52ARFL.cjs.map +0 -7
  204. package/dist/lib/node/react-surface-IWSTOZ2E.cjs.map +0 -7
  205. package/dist/lib/node/settings-JLV7YT6Q.cjs.map +0 -7
  206. package/dist/lib/node/spaces-ready-6EBR4SM4.cjs.map +0 -7
  207. package/dist/lib/node/state-JLN7TGRR.cjs.map +0 -7
  208. package/dist/lib/node-esm/app-graph-builder-HSGLCS76.mjs.map +0 -7
  209. package/dist/lib/node-esm/app-graph-serializer-J3B4WSXU.mjs.map +0 -7
  210. package/dist/lib/node-esm/chunk-ABTVMAG5.mjs.map +0 -7
  211. package/dist/lib/node-esm/chunk-FJPCLEKN.mjs.map +0 -7
  212. package/dist/lib/node-esm/chunk-OLISVDCF.mjs.map +0 -7
  213. package/dist/lib/node-esm/chunk-PQI4D4SH.mjs.map +0 -7
  214. package/dist/lib/node-esm/identity-created-EC6SVYB5.mjs.map +0 -7
  215. package/dist/lib/node-esm/intent-resolver-MXQIFIRC.mjs.map +0 -7
  216. package/dist/lib/node-esm/react-surface-JCHDAPGM.mjs.map +0 -7
  217. package/dist/lib/node-esm/settings-AILIMHTE.mjs.map +0 -7
  218. package/dist/lib/node-esm/spaces-ready-5PXESKHX.mjs.map +0 -7
  219. package/dist/lib/node-esm/state-YZPY5T5A.mjs.map +0 -7
  220. package/dist/types/src/components/ObjectSettings/AdvancedObjectSettings.d.ts.map +0 -1
  221. package/dist/types/src/components/ObjectSettings/BaseObjectSettings.d.ts.map +0 -1
  222. package/dist/types/src/components/ObjectSettings/ForeignKeys.d.ts.map +0 -1
  223. package/dist/types/src/components/ObjectSettings/ObjectSettingsContainer.d.ts.map +0 -1
  224. package/dist/types/src/components/ObjectSettings/index.d.ts.map +0 -1
  225. package/dist/types/src/components/SchemaContainer.d.ts +0 -13
  226. package/dist/types/src/components/SchemaContainer.d.ts.map +0 -1
  227. package/dist/types/src/components/SpaceSettings/SpaceSettingsContainer.stories.d.ts +0 -7
  228. package/dist/types/src/components/SpaceSettings/SpaceSettingsContainer.stories.d.ts.map +0 -1
  229. package/dist/types/src/hooks/useInputSurfaceLookup.d.ts +0 -14
  230. package/dist/types/src/hooks/useInputSurfaceLookup.d.ts.map +0 -1
  231. package/src/components/SchemaContainer.tsx +0 -59
  232. package/src/hooks/useInputSurfaceLookup.tsx +0 -31
  233. /package/dist/lib/browser/{react-root-XKAUZ3X2.mjs.map → react-root-6H7NX2M2.mjs.map} +0 -0
  234. /package/dist/lib/node/{react-root-XUE2J7HT.cjs.map → react-root-TEL5RW3N.cjs.map} +0 -0
  235. /package/dist/lib/node-esm/{react-root-ZBCJCEFS.mjs.map → react-root-PRBJMWLQ.mjs.map} +0 -0
  236. /package/dist/types/src/components/{ObjectSettings → ObjectSettingsContainer}/AdvancedObjectSettings.d.ts +0 -0
  237. /package/dist/types/src/components/{ObjectSettings → ObjectSettingsContainer}/BaseObjectSettings.d.ts +0 -0
  238. /package/dist/types/src/components/{ObjectSettings → ObjectSettingsContainer}/ForeignKeys.d.ts +0 -0
  239. /package/dist/types/src/components/{ObjectSettings → ObjectSettingsContainer}/ObjectSettingsContainer.d.ts +0 -0
  240. /package/dist/types/src/components/{ObjectSettings → ObjectSettingsContainer}/index.d.ts +0 -0
  241. /package/src/components/{ObjectSettings → ObjectSettingsContainer}/AdvancedObjectSettings.tsx +0 -0
  242. /package/src/components/{ObjectSettings → ObjectSettingsContainer}/BaseObjectSettings.tsx +0 -0
  243. /package/src/components/{ObjectSettings → ObjectSettingsContainer}/ForeignKeys.tsx +0 -0
  244. /package/src/components/{ObjectSettings → ObjectSettingsContainer}/ObjectSettingsContainer.tsx +0 -0
  245. /package/src/components/{ObjectSettings → ObjectSettingsContainer}/index.ts +0 -0
@@ -2,180 +2,62 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { pipe, Schema as S } from 'effect';
6
- import React, { type ChangeEvent, useCallback, useMemo, useState } from 'react';
5
+ import React, { useCallback, useMemo } from 'react';
7
6
 
8
- import { chain, createIntent, LayoutAction, useIntentDispatcher } from '@dxos/app-framework';
9
- import { log } from '@dxos/log';
10
- import { EdgeReplicationSetting } from '@dxos/protocols/proto/dxos/echo/metadata';
11
- import { useClient } from '@dxos/react-client';
12
- import { SpaceState, type Space } from '@dxos/react-client/echo';
13
- import { Button, Input, useMulticastObservable, useTranslation } from '@dxos/react-ui';
14
- import {
15
- Form,
16
- type InputComponent,
17
- ControlItem,
18
- ControlItemInput,
19
- ControlSection,
20
- ControlPage,
21
- } from '@dxos/react-ui-form';
22
- import { HuePicker, IconPicker } from '@dxos/react-ui-pickers';
7
+ import { Surface, useCapabilities, useCapability } from '@dxos/app-framework';
8
+ import { type Space } from '@dxos/react-client/echo';
9
+ import { toLocalizedString, useTranslation } from '@dxos/react-ui';
10
+ import { ControlSectionHeading } from '@dxos/react-ui-form';
11
+ import { Accordion } from '@dxos/react-ui-list';
23
12
  import { StackItem } from '@dxos/react-ui-stack';
13
+ import { byPosition } from '@dxos/util';
24
14
 
15
+ import { SpaceCapabilities } from '../../capabilities';
25
16
  import { SPACE_PLUGIN } from '../../meta';
26
- import { SpaceAction, SpaceForm } from '../../types';
27
17
 
28
- const FormSchema = SpaceForm.pipe(S.extend(S.Struct({ archived: S.Boolean.annotations({ title: 'Archive space' }) })));
18
+ export const SPACE_SETTINGS_DIALOG = `${SPACE_PLUGIN}/SpaceSettingsDialog`;
19
+
20
+ export type SpaceSettingsTab = 'members' | 'settings';
29
21
 
30
22
  export type SpaceSettingsContainerProps = {
31
23
  space: Space;
32
24
  };
33
25
 
34
- // TODO(wittjosiah): Handle space migrations here?
35
26
  export const SpaceSettingsContainer = ({ space }: SpaceSettingsContainerProps) => {
36
27
  const { t } = useTranslation(SPACE_PLUGIN);
37
- const { dispatchPromise: dispatch } = useIntentDispatcher();
38
- const client = useClient();
39
- const archived = useMulticastObservable(space.state) === SpaceState.SPACE_INACTIVE;
40
- const [edgeReplication, setEdgeReplication] = useState(
41
- space.internal.data.edgeReplication === EdgeReplicationSetting.ENABLED,
42
- );
43
- const toggleEdgeReplication = useCallback(
44
- async (next: boolean) => {
45
- setEdgeReplication(next);
46
- await space?.internal
47
- .setEdgeReplicationPreference(next ? EdgeReplicationSetting.ENABLED : EdgeReplicationSetting.DISABLED)
48
- .catch((err: unknown) => {
49
- log.catch(err);
50
- setEdgeReplication(!next);
51
- });
52
- },
53
- [space],
54
- );
28
+ const state = useCapability(SpaceCapabilities.MutableState);
29
+ const items = useCapabilities(SpaceCapabilities.SettingsSection).toSorted(byPosition);
30
+ const data = useMemo(() => ({ subject: space }), [space]);
55
31
 
56
- const handleSave = useCallback(
57
- (properties: S.Schema.Type<typeof FormSchema>) => {
58
- void toggleEdgeReplication(properties.edgeReplication);
59
- if (properties.name !== space.properties.name) {
60
- space.properties.name = properties.name;
61
- }
62
- if (properties.icon !== space.properties.icon) {
63
- space.properties.icon = properties.icon;
64
- }
65
- if (properties.hue !== space.properties.hue) {
66
- space.properties.hue = properties.hue;
67
- }
68
- if (properties.archived && !archived) {
69
- void dispatch(
70
- pipe(
71
- createIntent(SpaceAction.Close, { space }),
72
- chain(LayoutAction.SwitchWorkspace, { part: 'workspace', subject: client.spaces.default.id }),
73
- ),
74
- );
75
- } else if (!properties.archived && archived) {
76
- void dispatch(createIntent(SpaceAction.Open, { space }));
77
- }
32
+ const handleOpenSectionChange = useCallback(
33
+ (sections: string[]) => {
34
+ state.spaceSettingsOpenSections.splice(0, state.spaceSettingsOpenSections.length, ...sections);
78
35
  },
79
- [space, toggleEdgeReplication, archived],
80
- );
81
-
82
- const values = useMemo(
83
- () => ({
84
- name: space.properties.name,
85
- icon: space.properties.icon,
86
- hue: space.properties.hue,
87
- edgeReplication,
88
- archived,
89
- }),
90
- [space.properties.name, space.properties.icon, space.properties.hue, edgeReplication, archived],
91
- );
92
-
93
- const customElements: Partial<Record<string, InputComponent>> = useMemo(
94
- () => ({
95
- name: ({ type, label, getValue, onValueChange }) => {
96
- const handleChange = useCallback(
97
- ({ target: { value } }: ChangeEvent<HTMLInputElement>) => onValueChange(type, value),
98
- [onValueChange, type],
99
- );
100
- return (
101
- <ControlItemInput title={label} description={t('display name description')}>
102
- <Input.TextInput
103
- value={getValue()}
104
- onChange={handleChange}
105
- placeholder={t('display name input placeholder')}
106
- classNames='min-is-64'
107
- />
108
- </ControlItemInput>
109
- );
110
- },
111
- icon: ({ type, label, getValue, onValueChange }) => {
112
- const handleChange = useCallback((nextEmoji: string) => onValueChange(type, nextEmoji), [onValueChange, type]);
113
- const handleEmojiReset = useCallback(() => onValueChange(type, undefined), [onValueChange, type]);
114
- return (
115
- <ControlItem title={label} description={t('icon description')}>
116
- <IconPicker
117
- value={getValue()}
118
- onChange={handleChange}
119
- onReset={handleEmojiReset}
120
- classNames='justify-self-end'
121
- iconSize={5}
122
- />
123
- </ControlItem>
124
- );
125
- },
126
- hue: ({ type, label, getValue, onValueChange }) => {
127
- const handleChange = useCallback((nextHue: string) => onValueChange(type, nextHue), [onValueChange, type]);
128
- const handleHueReset = useCallback(() => onValueChange(type, undefined), [onValueChange, type]);
129
- return (
130
- <ControlItem title={label} description={t('hue description')}>
131
- <HuePicker
132
- value={getValue()}
133
- onChange={handleChange}
134
- onReset={handleHueReset}
135
- classNames='[--hue-preview-size:1.25rem] justify-self-end'
136
- />
137
- </ControlItem>
138
- );
139
- },
140
- edgeReplication: ({ type, label, getValue, onValueChange }) => {
141
- const handleChange = useCallback((checked: boolean) => onValueChange(type, checked), [onValueChange, type]);
142
- return (
143
- <ControlItemInput title={label} description={t('edge replication description')}>
144
- <Input.Switch checked={getValue()} onCheckedChange={handleChange} classNames='justify-self-end' />
145
- </ControlItemInput>
146
- );
147
- },
148
- archived: ({ type, label, getValue, onValueChange }) => {
149
- const handleChange = useCallback(() => onValueChange(type, !getValue()), [onValueChange, type, getValue]);
150
- return (
151
- <ControlItemInput title={label} description={t('archive space description')}>
152
- <Button disabled={space === client.spaces.default} onClick={handleChange}>
153
- {getValue() ? t('unarchive space label') : t('archive space label')}
154
- </Button>
155
- </ControlItemInput>
156
- );
157
- },
158
- }),
159
- [t, space],
36
+ [state],
160
37
  );
161
38
 
162
39
  return (
163
- <StackItem.Content classNames='block overflow-y-auto pli-2'>
164
- <ControlPage>
165
- <ControlSection
166
- title={t('space properties settings verbose label', { ns: SPACE_PLUGIN })}
167
- description={t('space properties settings description', { ns: SPACE_PLUGIN })}
168
- >
169
- <Form
170
- schema={FormSchema}
171
- values={values}
172
- autoSave
173
- onSave={handleSave}
174
- Custom={customElements}
175
- classNames='p-0 container-max-width [&_[role="form"]]:grid [&_[role="form"]]:grid-cols-1 md:[&_[role="form"]]:grid-cols-[1fr_min-content] [&_[role="form"]]:gap-4'
176
- />
177
- </ControlSection>
178
- </ControlPage>
40
+ <StackItem.Content classNames='p-2 block overflow-y-auto'>
41
+ <Accordion.Root<SpaceCapabilities.SettingsSection>
42
+ items={items}
43
+ value={state.spaceSettingsOpenSections}
44
+ onValueChange={handleOpenSectionChange}
45
+ >
46
+ {({ items }) => (
47
+ <>
48
+ {items.map((item) => (
49
+ <Accordion.Item key={item.id} item={item} classNames='container-max-width'>
50
+ <Accordion.ItemHeader classNames='pie-6' asChild>
51
+ <ControlSectionHeading title={toLocalizedString(item.label, t)} />
52
+ </Accordion.ItemHeader>
53
+ <Accordion.ItemBody>
54
+ <Surface role={`space-settings--${item.id}`} data={data} />
55
+ </Accordion.ItemBody>
56
+ </Accordion.Item>
57
+ ))}
58
+ </>
59
+ )}
60
+ </Accordion.Root>
179
61
  </StackItem.Content>
180
62
  );
181
63
  };
@@ -2,4 +2,5 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
+ export * from './SpacePropertiesForm';
5
6
  export * from './SpaceSettingsContainer';
@@ -5,19 +5,21 @@
5
5
  import React, { useEffect, useState } from 'react';
6
6
 
7
7
  import { useAppGraph } from '@dxos/app-framework';
8
- import { EdgeStatus } from '@dxos/protocols/proto/dxos/client/services';
8
+ import { QueryEdgeStatusResponse } from '@dxos/protocols/proto/dxos/client/services';
9
9
  import { EdgeReplicationSetting } from '@dxos/protocols/proto/dxos/echo/metadata';
10
10
  import { useClient } from '@dxos/react-client';
11
- import { type Space, useSpaceSyncState } from '@dxos/react-client/echo';
11
+ import { type Space } from '@dxos/react-client/echo';
12
12
  import { Tooltip, useTranslation } from '@dxos/react-ui';
13
13
  import { AttentionGlyph, useAttended, useAttention } from '@dxos/react-ui-attention';
14
14
 
15
+ import { useSpaceSyncState } from './sync-state';
15
16
  import { usePath } from '../../hooks';
16
17
  import { SPACE_PLUGIN } from '../../meta';
17
18
 
18
- const useEdgeStatus = (): EdgeStatus => {
19
- const [status, setStatus] = useState(EdgeStatus.NOT_CONNECTED);
19
+ const useEdgeStatus = (): QueryEdgeStatusResponse.EdgeStatus => {
20
+ const [status, setStatus] = useState(QueryEdgeStatusResponse.EdgeStatus.NOT_CONNECTED);
20
21
  const client = useClient();
22
+
21
23
  useEffect(() => {
22
24
  client.services.services.EdgeAgentService?.queryEdgeStatus().subscribe(({ status }) => {
23
25
  setStatus(status);
@@ -42,7 +44,7 @@ export const InlineSyncStatus = ({ space, open }: { space: Space; open?: boolean
42
44
  const path = usePath(graph, startOfAttention);
43
45
  const containsAttended = !open && !isAttended && id && path ? path.includes(id) : false;
44
46
 
45
- const connectedToEdge = useEdgeStatus() === EdgeStatus.CONNECTED;
47
+ const connectedToEdge = useEdgeStatus() === QueryEdgeStatusResponse.EdgeStatus.CONNECTED;
46
48
  // TODO(wittjosiah): This is not reactive.
47
49
  const edgeSyncEnabled = space.internal.data.edgeReplication === EdgeReplicationSetting.ENABLED;
48
50
  const syncState = useSpaceSyncState(space);
@@ -0,0 +1,133 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { type HTMLAttributes, useEffect, useState } from 'react';
6
+
7
+ import { useClient } from '@dxos/react-client';
8
+ import { type SpaceId, useSpace } from '@dxos/react-client/echo';
9
+ import { Icon, toLocalizedString, useTranslation } from '@dxos/react-ui';
10
+ import { type ThemedClassName } from '@dxos/react-ui';
11
+ import { mx } from '@dxos/react-ui-theme';
12
+
13
+ import { type Progress, type PeerSyncState } from './sync-state';
14
+ import { SPACE_PLUGIN } from '../../meta';
15
+ import { getSpaceDisplayName } from '../../util';
16
+
17
+ export const SYNC_STALLED_TIMEOUT = 5_000;
18
+
19
+ // TODO(wittjosiah): Define sematic color tokens.
20
+ const styles = {
21
+ barBg: 'bg-neutral-50 dark:bg-green-900 text-black',
22
+ barFg: 'bg-neutral-100 bg-green-500',
23
+ barHover: 'dark:hover:bg-green-500',
24
+ };
25
+
26
+ const useActive = (count: number) => {
27
+ const [current, setCurrent] = useState(count);
28
+ const [active, setActive] = useState(false);
29
+ useEffect(() => {
30
+ let t: NodeJS.Timeout | undefined;
31
+ if (count !== current) {
32
+ setActive(true);
33
+ setCurrent(count);
34
+ t && clearTimeout(t);
35
+ t = setTimeout(() => {
36
+ setActive(false);
37
+ }, SYNC_STALLED_TIMEOUT);
38
+ }
39
+
40
+ return () => {
41
+ setActive(false);
42
+ clearTimeout(t);
43
+ };
44
+ }, [count, current]);
45
+ return active;
46
+ };
47
+
48
+ export type SpaceRowContainerProps = Omit<SpaceRowProps, 'spaceName'>;
49
+
50
+ export const SpaceRowContainer = ({ spaceId, state }: SpaceRowContainerProps) => {
51
+ const { t } = useTranslation(SPACE_PLUGIN);
52
+ const client = useClient();
53
+ const space = useSpace(spaceId);
54
+ if (!space) {
55
+ return null;
56
+ }
57
+
58
+ const spaceName = toLocalizedString(getSpaceDisplayName(space, { personal: space === client.spaces.default }), t);
59
+
60
+ return <SpaceRow spaceId={spaceId} spaceName={spaceName} state={state} />;
61
+ };
62
+
63
+ export type SpaceRowProps = {
64
+ spaceId: SpaceId;
65
+ spaceName: string;
66
+ state: PeerSyncState;
67
+ };
68
+
69
+ export const SpaceRow = ({
70
+ spaceId,
71
+ spaceName,
72
+ state: { localDocumentCount, remoteDocumentCount, missingOnLocal, missingOnRemote },
73
+ }: SpaceRowProps) => {
74
+ const downActive = useActive(localDocumentCount);
75
+ const upActive = useActive(remoteDocumentCount);
76
+
77
+ return (
78
+ <div
79
+ className='flex items-center mx-0.5 gap-0.5 cursor-pointer'
80
+ title={spaceId}
81
+ onClick={() => {
82
+ void navigator.clipboard.writeText(spaceId);
83
+ }}
84
+ >
85
+ <span className='is-1/2 truncate'>{spaceName}</span>
86
+ <Icon
87
+ icon='ph--arrow-fat-line-left--regular'
88
+ size={3}
89
+ classNames={mx(downActive && 'animate-[pulse_1s_infinite]')}
90
+ />
91
+ <Candle
92
+ up={{ count: remoteDocumentCount, total: remoteDocumentCount + missingOnRemote }}
93
+ down={{ count: localDocumentCount, total: localDocumentCount + missingOnLocal }}
94
+ title={spaceId}
95
+ />
96
+ <Icon
97
+ icon='ph--arrow-fat-line-right--regular'
98
+ size={3}
99
+ classNames={mx(upActive && 'animate-[pulse_1s_step-start_infinite]')}
100
+ />
101
+ </div>
102
+ );
103
+ };
104
+
105
+ type CandleProps = ThemedClassName<Pick<HTMLAttributes<HTMLDivElement>, 'title'>> & { up: Progress; down: Progress };
106
+
107
+ const Candle = ({ classNames, up, down }: CandleProps) => {
108
+ return (
109
+ <div className={mx('grid grid-cols-[1fr_2rem_1fr] w-full h-3', classNames)}>
110
+ <Bar classNames='justify-end' {...up} />
111
+ <div className='relative'>
112
+ <div className={mx('absolute inset-0 flex items-center justify-center text-xs', styles.barBg)}>{up.total}</div>
113
+ </div>
114
+ <Bar {...down} />
115
+ </div>
116
+ );
117
+ };
118
+
119
+ const Bar = ({ classNames, count, total }: ThemedClassName<Progress>) => {
120
+ let p = (count / total) * 100;
121
+ if (count < total) {
122
+ p = Math.min(p, 95);
123
+ }
124
+
125
+ return (
126
+ <div className={mx('relative flex w-full', styles.barBg, classNames)}>
127
+ <div className={mx('shrink-0', styles.barFg)} style={{ width: `${p}%` }}></div>
128
+ {count !== total && (
129
+ <div className='absolute top-0 bottom-0 flex items-center mx-0.5 text-black text-xs'>{count}</div>
130
+ )}
131
+ </div>
132
+ );
133
+ };
@@ -2,19 +2,22 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { useEffect, useState } from 'react';
5
+ import React, { useCallback, useEffect, useState } from 'react';
6
6
 
7
7
  import { StatusBar } from '@dxos/plugin-status-bar';
8
8
  import { useClient } from '@dxos/react-client';
9
- import { getSyncSummary, type SpaceSyncStateMap, useSyncState } from '@dxos/react-client/echo';
10
- import { Icon, useTranslation } from '@dxos/react-ui';
9
+ import { type SpaceId } from '@dxos/react-client/echo';
10
+ import { Icon, Input, Popover, useTranslation } from '@dxos/react-ui';
11
+ import { type ThemedClassName } from '@dxos/react-ui';
12
+ import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
13
+ import { mx } from '@dxos/react-ui-theme';
11
14
 
15
+ import { SpaceRowContainer, SYNC_STALLED_TIMEOUT } from './Space';
12
16
  import { createClientSaveTracker } from './save-tracker';
13
17
  import { getIcon, getStatus } from './status';
18
+ import { type PeerSyncState, type SpaceSyncStateMap, getSyncSummary, useSyncState } from './sync-state';
14
19
  import { SPACE_PLUGIN } from '../../meta';
15
20
 
16
- const SYNC_STALLED_TIMEOUT = 5_000;
17
-
18
21
  export const SyncStatus = () => {
19
22
  const client = useClient();
20
23
  const state = useSyncState();
@@ -54,5 +57,65 @@ export const SyncStatusIndicator = ({ state, saved }: { state: SpaceSyncStateMap
54
57
  const title = t(`${status} label`);
55
58
  const icon = <Icon icon={getIcon(status)} size={4} classNames={classNames} />;
56
59
 
57
- return <StatusBar.Item title={title}>{icon}</StatusBar.Item>;
60
+ if (offline) {
61
+ return <StatusBar.Item title={title}>{icon}</StatusBar.Item>;
62
+ } else {
63
+ return (
64
+ <Popover.Root>
65
+ <Popover.Trigger asChild>
66
+ <StatusBar.Button title={title}>{icon}</StatusBar.Button>
67
+ </Popover.Trigger>
68
+ <Popover.Portal>
69
+ <Popover.Content>
70
+ <SyncStatusDetail state={state} summary={summary} debug={false} />
71
+ <Popover.Arrow />
72
+ </Popover.Content>
73
+ </Popover.Portal>
74
+ </Popover.Root>
75
+ );
76
+ }
77
+ };
78
+
79
+ export type SyncStatusDetailProps = ThemedClassName<{
80
+ state: SpaceSyncStateMap;
81
+ summary: PeerSyncState;
82
+ debug?: boolean;
83
+ }>;
84
+
85
+ // TODO(wittjosiah): This currently does not show `differentDocuments` at all.
86
+ export const SyncStatusDetail = ({ classNames, state, summary, debug }: SyncStatusDetailProps) => {
87
+ const [showAll, setShowAll] = useState(false);
88
+ const { t } = useTranslation(SPACE_PLUGIN);
89
+ const entries = Object.entries(state)
90
+ .filter(([_, value]) => showAll || value.missingOnLocal + value.missingOnRemote > 0)
91
+ .toSorted(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
92
+
93
+ const handleCheckedChange = useCallback((state: boolean) => setShowAll(state), [setShowAll]);
94
+
95
+ // TODO(burdon): Normalize to max document count?
96
+ return (
97
+ <div className={mx('flex flex-col gap-3 p-2 text-xs min-w-[400px]', classNames)}>
98
+ <div role='none' className='flex items-center'>
99
+ <h1 className='flex-1'>{t('sync status title')}</h1>
100
+ <div className='flex items-center gap-2'>
101
+ <Input.Root>
102
+ <Input.Label classNames='text-xs'>{t('show all label')}</Input.Label>
103
+ <Input.Checkbox checked={showAll} onCheckedChange={handleCheckedChange} />
104
+ </Input.Root>
105
+ </div>
106
+ </div>
107
+ <div className='flex flex-col gap-2'>
108
+ {entries.length === 0 && (
109
+ <div role='none' className='flex justify-center'>
110
+ {/* TODO(wittjosiah): This text should be updated once status includes different documents. */}
111
+ {t('no sync status label')}
112
+ </div>
113
+ )}
114
+ {entries.map(([spaceId, state]) => (
115
+ <SpaceRowContainer key={spaceId} spaceId={spaceId as SpaceId} state={state} />
116
+ ))}
117
+ </div>
118
+ {debug && <SyntaxHighlighter language='json'>{JSON.stringify(summary, null, 2)}</SyntaxHighlighter>}
119
+ </div>
120
+ );
58
121
  };
@@ -0,0 +1,89 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxos-theme';
6
+
7
+ import { type Meta, type StoryObj } from '@storybook/react';
8
+ import React, { useState } from 'react';
9
+
10
+ import { type Client } from '@dxos/client';
11
+ import { faker } from '@dxos/random';
12
+ import { useClient } from '@dxos/react-client';
13
+ import { withClientProvider } from '@dxos/react-client/testing';
14
+ import { useAsyncEffect } from '@dxos/react-ui';
15
+ import { withTheme, withLayout } from '@dxos/storybook-utils';
16
+
17
+ import { SyncStatusDetail } from './SyncStatus';
18
+ import { getSyncSummary, type SpaceSyncStateMap } from './sync-state';
19
+ import translations from '../../translations';
20
+
21
+ const random = ({ min, max }: { min: number; max: number }) => min + Math.floor(Math.random() * (max - min));
22
+
23
+ const createSpaceSyncStateMap = async (client: Client): Promise<SpaceSyncStateMap> => {
24
+ const spaces = await Promise.all(
25
+ Array.from({ length: 10 }).map(() => client.spaces.create({ name: faker.company.name() })),
26
+ );
27
+
28
+ return spaces.reduce<SpaceSyncStateMap>((map, space, i) => {
29
+ if (i > 4) {
30
+ const total = random({ min: 10, max: 500 });
31
+ map[space.id] = {
32
+ localDocumentCount: total,
33
+ remoteDocumentCount: total,
34
+ missingOnLocal: 0,
35
+ missingOnRemote: 0,
36
+ differentDocuments: 0,
37
+ };
38
+ return map;
39
+ }
40
+
41
+ const total = random({ min: 10, max: 500 });
42
+ const haveLocal = random({ min: 0, max: total });
43
+ const haveRemote = random({ min: 0, max: total });
44
+ map[space.id] = {
45
+ localDocumentCount: haveLocal,
46
+ remoteDocumentCount: haveRemote,
47
+ missingOnLocal: total - haveLocal,
48
+ missingOnRemote: total - haveRemote,
49
+ differentDocuments: 0,
50
+ };
51
+
52
+ return map;
53
+ }, {});
54
+ };
55
+
56
+ const meta: Meta<typeof SyncStatusDetail> = {
57
+ title: 'plugins/plugin-space/SyncStatusDetail',
58
+ component: SyncStatusDetail,
59
+ decorators: [withTheme, withLayout(), withClientProvider({ createIdentity: true })],
60
+ parameters: {
61
+ translations,
62
+ layout: 'centered',
63
+ },
64
+ args: {
65
+ classNames: 'm-2 border border-separator rounded-md',
66
+ },
67
+ };
68
+
69
+ export default meta;
70
+
71
+ type Story = StoryObj<typeof SyncStatusDetail>;
72
+
73
+ export const Default: Story = {
74
+ render: (args) => {
75
+ return <SyncStatusDetail {...args} state={{}} />;
76
+ },
77
+ };
78
+
79
+ export const Sync: Story = {
80
+ render: (args) => {
81
+ const client = useClient();
82
+ const [state, setState] = useState<SpaceSyncStateMap>({});
83
+ useAsyncEffect(async () => {
84
+ setState(await createSpaceSyncStateMap(client));
85
+ });
86
+
87
+ return <SyncStatusDetail {...args} state={state} summary={getSyncSummary(state)} />;
88
+ },
89
+ };
@@ -0,0 +1,101 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { useEffect, useState } from 'react';
6
+
7
+ import { type Space, type SpaceId, type SpaceSyncState } from '@dxos/client/echo';
8
+ import { Context } from '@dxos/context';
9
+ import { EdgeService } from '@dxos/protocols';
10
+ import { useClient } from '@dxos/react-client';
11
+
12
+ export type Progress = { count: number; total: number };
13
+
14
+ export type PeerSyncState = Omit<SpaceSyncState.PeerState, 'peerId'>;
15
+
16
+ export type SpaceSyncStateMap = Record<SpaceId, PeerSyncState>;
17
+
18
+ export const createEmptyEdgeSyncState = (): PeerSyncState => ({
19
+ missingOnLocal: 0,
20
+ missingOnRemote: 0,
21
+ localDocumentCount: 0,
22
+ remoteDocumentCount: 0,
23
+ differentDocuments: 0,
24
+ });
25
+
26
+ export const getSyncSummary = (syncMap: SpaceSyncStateMap): PeerSyncState => {
27
+ return Object.entries(syncMap).reduce<PeerSyncState>((summary, [_spaceId, peerState]) => {
28
+ summary.missingOnLocal += peerState.missingOnLocal;
29
+ summary.missingOnRemote += peerState.missingOnRemote;
30
+ summary.localDocumentCount += peerState.localDocumentCount;
31
+ summary.remoteDocumentCount += peerState.remoteDocumentCount;
32
+ summary.differentDocuments += peerState.differentDocuments;
33
+ return summary;
34
+ }, createEmptyEdgeSyncState());
35
+ };
36
+
37
+ const isEdgePeerId = (peerId: string, spaceId: SpaceId) =>
38
+ peerId.startsWith(`${EdgeService.AUTOMERGE_REPLICATOR}:${spaceId}`);
39
+
40
+ /**
41
+ * Hook Subscribes to sync state for each space.
42
+ */
43
+ export const useSyncState = (): SpaceSyncStateMap => {
44
+ const client = useClient();
45
+ const [spaceState, setSpaceState] = useState<SpaceSyncStateMap>({});
46
+
47
+ useEffect(() => {
48
+ const ctx = new Context();
49
+ const createSubscriptions = (spaces: Space[]) => {
50
+ for (const space of spaces) {
51
+ if (spaceState[space.id]) {
52
+ continue;
53
+ }
54
+
55
+ ctx.onDispose(
56
+ space.crud.subscribeToSyncState(ctx, ({ peers = [] }) => {
57
+ const syncState = peers.find((state) => isEdgePeerId(state.peerId, space.id));
58
+ if (syncState) {
59
+ setSpaceState((spaceState) => ({ ...spaceState, [space.id]: syncState }));
60
+ }
61
+ }),
62
+ );
63
+ }
64
+ };
65
+
66
+ createSubscriptions(client.spaces.get());
67
+ client.spaces.subscribe((spaces) => {
68
+ createSubscriptions(spaces);
69
+ });
70
+
71
+ return () => {
72
+ void ctx.dispose();
73
+ };
74
+ }, [client]);
75
+
76
+ return spaceState;
77
+ };
78
+
79
+ /**
80
+ * Hook Subscribes to sync state for a single space.
81
+ */
82
+ // TODO(wittjosiah): Reconcile w/ useSyncState.
83
+ export const useSpaceSyncState = (space: Space): PeerSyncState | undefined => {
84
+ const [spaceState, setSpaceState] = useState<PeerSyncState>();
85
+
86
+ useEffect(() => {
87
+ const ctx = new Context();
88
+ space.crud.subscribeToSyncState(ctx, ({ peers = [] }) => {
89
+ const syncState = peers.find((state) => isEdgePeerId(state.peerId, space.id));
90
+ if (syncState) {
91
+ setSpaceState(syncState);
92
+ }
93
+ });
94
+
95
+ return () => {
96
+ void ctx.dispose();
97
+ };
98
+ }, [space]);
99
+
100
+ return spaceState;
101
+ };