@dxos/plugin-space 0.7.2 → 0.7.3-main.2dd075e

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 (95) hide show
  1. package/dist/lib/browser/{chunk-DJE2HYFV.mjs → chunk-FTKV32QZ.mjs} +9 -2
  2. package/dist/lib/browser/chunk-FTKV32QZ.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-OWZKSWMX.mjs → chunk-MWKXNS5S.mjs} +13 -3
  4. package/dist/lib/browser/{chunk-OWZKSWMX.mjs.map → chunk-MWKXNS5S.mjs.map} +3 -3
  5. package/dist/lib/browser/index.mjs +1056 -674
  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 +3 -1
  9. package/dist/lib/browser/types/index.mjs +5 -3
  10. package/dist/lib/node/{chunk-FYWGZYJB.cjs → chunk-6SNOZF7Y.cjs} +18 -7
  11. package/dist/lib/node/chunk-6SNOZF7Y.cjs.map +7 -0
  12. package/dist/lib/node/{chunk-JFDDZI4Y.cjs → chunk-QNVEU2UD.cjs} +12 -4
  13. package/dist/lib/node/chunk-QNVEU2UD.cjs.map +7 -0
  14. package/dist/lib/node/index.cjs +1160 -789
  15. package/dist/lib/node/index.cjs.map +4 -4
  16. package/dist/lib/node/meta.cjs +7 -5
  17. package/dist/lib/node/meta.cjs.map +2 -2
  18. package/dist/lib/node/meta.json +1 -1
  19. package/dist/lib/node/types/index.cjs +14 -12
  20. package/dist/lib/node/types/index.cjs.map +2 -2
  21. package/dist/lib/node-esm/{chunk-MCEAI4CV.mjs → chunk-OHEAWSCA.mjs} +13 -3
  22. package/dist/lib/node-esm/{chunk-MCEAI4CV.mjs.map → chunk-OHEAWSCA.mjs.map} +3 -3
  23. package/dist/lib/node-esm/{chunk-DVUZ7A7G.mjs → chunk-UMV7XREB.mjs} +9 -2
  24. package/dist/lib/node-esm/chunk-UMV7XREB.mjs.map +7 -0
  25. package/dist/lib/node-esm/index.mjs +1056 -674
  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 +3 -1
  29. package/dist/lib/node-esm/types/index.mjs +5 -3
  30. package/dist/types/src/SpacePlugin.d.ts.map +1 -1
  31. package/dist/types/src/components/CreateDialog/CreateObjectDialog.d.ts +9 -0
  32. package/dist/types/src/components/CreateDialog/CreateObjectDialog.d.ts.map +1 -0
  33. package/dist/types/src/components/CreateDialog/CreateObjectDialog.stories.d.ts +10 -0
  34. package/dist/types/src/components/CreateDialog/CreateObjectDialog.stories.d.ts.map +1 -0
  35. package/dist/types/src/components/CreateDialog/CreateObjectPanel.d.ts +22 -0
  36. package/dist/types/src/components/CreateDialog/CreateObjectPanel.d.ts.map +1 -0
  37. package/dist/types/src/components/CreateDialog/CreateSpaceDialog.d.ts +3 -0
  38. package/dist/types/src/components/CreateDialog/CreateSpaceDialog.d.ts.map +1 -0
  39. package/dist/types/src/components/CreateDialog/index.d.ts +3 -0
  40. package/dist/types/src/components/CreateDialog/index.d.ts.map +1 -0
  41. package/dist/types/src/components/PopoverRenameObject.d.ts +1 -1
  42. package/dist/types/src/components/SpacePluginSettings.d.ts.map +1 -1
  43. package/dist/types/src/components/SpaceSettings/SpaceSettingsPanel.d.ts.map +1 -1
  44. package/dist/types/src/components/SyncStatus/InlineSyncStatus.d.ts +6 -0
  45. package/dist/types/src/components/SyncStatus/InlineSyncStatus.d.ts.map +1 -0
  46. package/dist/types/src/components/SyncStatus/Space.d.ts +8 -3
  47. package/dist/types/src/components/SyncStatus/Space.d.ts.map +1 -1
  48. package/dist/types/src/components/SyncStatus/SyncStatus.d.ts +3 -2
  49. package/dist/types/src/components/SyncStatus/SyncStatus.d.ts.map +1 -1
  50. package/dist/types/src/components/SyncStatus/SyncStatus.stories.d.ts +1 -2
  51. package/dist/types/src/components/SyncStatus/SyncStatus.stories.d.ts.map +1 -1
  52. package/dist/types/src/components/SyncStatus/SyncStatusDetail.stories.d.ts +8 -0
  53. package/dist/types/src/components/SyncStatus/SyncStatusDetail.stories.d.ts.map +1 -0
  54. package/dist/types/src/components/SyncStatus/index.d.ts +1 -0
  55. package/dist/types/src/components/SyncStatus/index.d.ts.map +1 -1
  56. package/dist/types/src/components/SyncStatus/sync-state.d.ts +5 -1
  57. package/dist/types/src/components/SyncStatus/sync-state.d.ts.map +1 -1
  58. package/dist/types/src/components/index.d.ts +1 -0
  59. package/dist/types/src/components/index.d.ts.map +1 -1
  60. package/dist/types/src/meta.d.ts +5 -0
  61. package/dist/types/src/meta.d.ts.map +1 -1
  62. package/dist/types/src/translations.d.ts +224 -0
  63. package/dist/types/src/translations.d.ts.map +1 -1
  64. package/dist/types/src/types/types.d.ts +14 -14
  65. package/dist/types/src/types/types.d.ts.map +1 -1
  66. package/dist/types/src/util.d.ts +3 -13
  67. package/dist/types/src/util.d.ts.map +1 -1
  68. package/package.json +38 -35
  69. package/src/SpacePlugin.tsx +169 -75
  70. package/src/components/AwaitingObject.tsx +2 -2
  71. package/src/components/CreateDialog/CreateObjectDialog.stories.tsx +83 -0
  72. package/src/components/CreateDialog/CreateObjectDialog.tsx +97 -0
  73. package/src/components/CreateDialog/CreateObjectPanel.tsx +169 -0
  74. package/src/components/CreateDialog/CreateSpaceDialog.tsx +57 -0
  75. package/src/components/CreateDialog/index.ts +6 -0
  76. package/src/components/PopoverRenameObject.tsx +1 -1
  77. package/src/components/SpacePluginSettings.tsx +3 -32
  78. package/src/components/SpaceSettings/SpaceSettingsDialog.tsx +1 -1
  79. package/src/components/SpaceSettings/SpaceSettingsPanel.tsx +2 -6
  80. package/src/components/SyncStatus/InlineSyncStatus.tsx +45 -0
  81. package/src/components/SyncStatus/Space.tsx +30 -6
  82. package/src/components/SyncStatus/SyncStatus.stories.tsx +3 -32
  83. package/src/components/SyncStatus/SyncStatus.tsx +32 -14
  84. package/src/components/SyncStatus/SyncStatusDetail.stories.tsx +83 -0
  85. package/src/components/SyncStatus/index.ts +1 -0
  86. package/src/components/SyncStatus/sync-state.ts +24 -0
  87. package/src/components/index.ts +1 -0
  88. package/src/meta.ts +6 -0
  89. package/src/translations.ts +15 -0
  90. package/src/types/types.ts +20 -16
  91. package/src/util.tsx +51 -141
  92. package/dist/lib/browser/chunk-DJE2HYFV.mjs.map +0 -7
  93. package/dist/lib/node/chunk-FYWGZYJB.cjs.map +0 -7
  94. package/dist/lib/node/chunk-JFDDZI4Y.cjs.map +0 -7
  95. package/dist/lib/node-esm/chunk-DVUZ7A7G.mjs.map +0 -7
@@ -0,0 +1,97 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback, useRef } from 'react';
6
+
7
+ import { type MetadataResolver, NavigationAction, useIntentDispatcher } from '@dxos/app-framework';
8
+ import { useClient } from '@dxos/react-client';
9
+ import { type AbstractTypedObject, isReactiveObject, isSpace, useSpaces } from '@dxos/react-client/echo';
10
+ import { Button, Dialog, Icon, useTranslation } from '@dxos/react-ui';
11
+
12
+ import { CreateObjectPanel, type CreateObjectPanelProps } from './CreateObjectPanel';
13
+ import { SPACE_PLUGIN, SpaceAction } from '../../meta';
14
+ import { CollectionType } from '../../types';
15
+
16
+ export type CreateObjectDialogProps = Pick<CreateObjectPanelProps, 'schemas' | 'target' | 'typename' | 'name'> & {
17
+ resolve?: MetadataResolver;
18
+ navigableCollections?: boolean;
19
+ };
20
+
21
+ export const CreateObjectDialog = ({
22
+ schemas,
23
+ target,
24
+ typename,
25
+ name,
26
+ navigableCollections,
27
+ resolve,
28
+ }: CreateObjectDialogProps) => {
29
+ const closeRef = useRef<HTMLButtonElement | null>(null);
30
+ const { t } = useTranslation(SPACE_PLUGIN);
31
+ const client = useClient();
32
+ const spaces = useSpaces();
33
+ const dispatch = useIntentDispatcher();
34
+
35
+ const handleCreateObject = useCallback(
36
+ async ({
37
+ schema,
38
+ target: _target,
39
+ name,
40
+ }: {
41
+ schema: AbstractTypedObject;
42
+ target: CreateObjectPanelProps['target'];
43
+ name?: string;
44
+ }) => {
45
+ const target = isSpace(_target) ? (_target.properties[CollectionType.typename] as CollectionType) : _target;
46
+ const createObjectAction = resolve?.(schema.typename)?.createObject;
47
+ if (!createObjectAction || !target) {
48
+ // TODO(wittjosiah): UI feedback.
49
+ return;
50
+ }
51
+
52
+ // NOTE: Must close before navigating or attention won't follow object.
53
+ closeRef.current?.click();
54
+
55
+ const result = await dispatch({ action: createObjectAction, data: { name } });
56
+ const object = result?.data;
57
+ if (isReactiveObject(object)) {
58
+ await dispatch([
59
+ {
60
+ plugin: SPACE_PLUGIN,
61
+ action: SpaceAction.ADD_OBJECT,
62
+ data: { target, object },
63
+ },
64
+ ...(!(object instanceof CollectionType) || navigableCollections ? [{ action: NavigationAction.OPEN }] : []),
65
+ ]);
66
+ }
67
+ },
68
+ [dispatch, resolve],
69
+ );
70
+
71
+ return (
72
+ // TODO(wittjosiah): The tablist dialog pattern is copied from @dxos/plugin-manager.
73
+ // Consider factoring it out to the tabs package.
74
+ <Dialog.Content classNames='p-0 bs-content min-bs-[15rem] max-bs-full md:max-is-[40rem] overflow-hidden'>
75
+ <div role='none' className='flex justify-between pbs-3 pis-2 pie-3 @md:pbs-4 @md:pis-4 @md:pie-5'>
76
+ <Dialog.Title>{t('create object dialog title')}</Dialog.Title>
77
+ <Dialog.Close asChild>
78
+ <Button ref={closeRef} density='fine' variant='ghost' autoFocus>
79
+ <Icon icon='ph--x--regular' size={4} />
80
+ </Button>
81
+ </Dialog.Close>
82
+ </div>
83
+ <div className='p-4'>
84
+ <CreateObjectPanel
85
+ schemas={schemas}
86
+ spaces={spaces}
87
+ target={target}
88
+ typename={typename}
89
+ name={name}
90
+ defaultSpaceId={client.spaces.default.id}
91
+ resolve={resolve}
92
+ onCreateObject={handleCreateObject}
93
+ />
94
+ </div>
95
+ </Dialog.Content>
96
+ );
97
+ };
@@ -0,0 +1,169 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback, useState } from 'react';
6
+
7
+ import { type MetadataResolver } from '@dxos/app-framework';
8
+ import { type AbstractTypedObject, getObjectAnnotation, S } from '@dxos/echo-schema';
9
+ import { type SpaceId, type Space, isSpace } from '@dxos/react-client/echo';
10
+ import { Icon, IconButton, Input, toLocalizedString, useTranslation } from '@dxos/react-ui';
11
+ import { Form, InputHeader } from '@dxos/react-ui-form';
12
+ import { SearchList } from '@dxos/react-ui-searchlist';
13
+ import { nonNullable, type MaybePromise } from '@dxos/util';
14
+
15
+ import { SPACE_PLUGIN } from '../../meta';
16
+ import { type CollectionType } from '../../types';
17
+ import { getSpaceDisplayName } from '../../util';
18
+
19
+ export type CreateObjectPanelProps = {
20
+ schemas: AbstractTypedObject[];
21
+ spaces: Space[];
22
+ typename?: string;
23
+ target?: Space | CollectionType;
24
+ name?: string;
25
+ defaultSpaceId?: SpaceId;
26
+ resolve?: MetadataResolver;
27
+ onCreateObject?: (params: {
28
+ schema: AbstractTypedObject;
29
+ target: Space | CollectionType;
30
+ name?: string;
31
+ }) => MaybePromise<void>;
32
+ };
33
+
34
+ export const CreateObjectPanel = ({
35
+ schemas,
36
+ spaces,
37
+ typename: initialTypename,
38
+ target: initialTarget,
39
+ name: initialName,
40
+ defaultSpaceId,
41
+ resolve,
42
+ onCreateObject,
43
+ }: CreateObjectPanelProps) => {
44
+ const { t } = useTranslation(SPACE_PLUGIN);
45
+ const [typename, setTypename] = useState<string | undefined>(initialTypename);
46
+ const [target, setTarget] = useState<Space | CollectionType | undefined>(initialTarget);
47
+ const schema = schemas.find((schema) => getObjectAnnotation(schema)?.typename === typename);
48
+ const options = schemas.map(getObjectAnnotation).filter(nonNullable);
49
+
50
+ const handleClearSchema = useCallback(() => setTypename(undefined), []);
51
+ const handleClearTarget = useCallback(() => setTarget(undefined), []);
52
+
53
+ const handleCreateObject = useCallback(
54
+ async ({ name }: { name?: string }) => {
55
+ if (!schema || !target) {
56
+ return;
57
+ }
58
+
59
+ await onCreateObject?.({ schema, target, name });
60
+ },
61
+ [onCreateObject, schema, target],
62
+ );
63
+
64
+ // TODO(wittjosiah): All of these inputs should be rolled into a `Form` once it supports the necessary variants.
65
+ const schemaInput = (
66
+ <SearchList.Root label={t('schema input label')} classNames='flex flex-col grow overflow-hidden my-2 px-2'>
67
+ <SearchList.Input
68
+ autoFocus
69
+ data-testid='create-object-form.schema-input'
70
+ placeholder={t('schema input placeholder')}
71
+ classNames='px-1 my-2'
72
+ />
73
+ <SearchList.Content classNames='max-bs-[24rem] overflow-auto'>
74
+ {options.map((option) => (
75
+ <SearchList.Item
76
+ key={option.typename}
77
+ value={t('typename label', { ns: option.typename, defaultValue: option.typename })}
78
+ onSelect={() => setTypename(option.typename)}
79
+ classNames='flex items-center gap-2'
80
+ >
81
+ <span className='flex gap-2 items-center grow truncate'>
82
+ <Icon icon={resolve?.(option.typename).icon ?? 'ph--placeholder--regular'} size={5} />
83
+ {t('typename label', { ns: option.typename, defaultValue: option.typename })}
84
+ </span>
85
+ </SearchList.Item>
86
+ ))}
87
+ </SearchList.Content>
88
+ </SearchList.Root>
89
+ );
90
+
91
+ const spaceInput = (
92
+ <SearchList.Root label={t('space input label')} classNames='flex flex-col grow overflow-hidden my-2 px-2'>
93
+ <SearchList.Input
94
+ autoFocus
95
+ data-testid='create-object-form.space-input'
96
+ placeholder={t('space input placeholder')}
97
+ classNames='px-1 my-2'
98
+ />
99
+ <SearchList.Content classNames='max-bs-[24rem] overflow-auto'>
100
+ {spaces.map((space) => (
101
+ <SearchList.Item
102
+ key={space.id}
103
+ value={toLocalizedString(getSpaceDisplayName(space, { personal: space.id === defaultSpaceId }), t)}
104
+ onSelect={() => setTarget(space)}
105
+ classNames='flex items-center gap-2'
106
+ >
107
+ <span className='grow truncate'>
108
+ {toLocalizedString(getSpaceDisplayName(space, { personal: space.id === defaultSpaceId }), t)}
109
+ </span>
110
+ </SearchList.Item>
111
+ ))}
112
+ </SearchList.Content>
113
+ </SearchList.Root>
114
+ );
115
+
116
+ const form = (
117
+ <Form
118
+ autoFocus
119
+ values={{ name: initialName }}
120
+ schema={S.Struct({ name: S.optional(S.String) })}
121
+ testId='create-object-form'
122
+ onSave={handleCreateObject}
123
+ />
124
+ );
125
+
126
+ return (
127
+ <div role='form' className='flex flex-col gap-2'>
128
+ {target && (
129
+ <div role='none' className='px-2'>
130
+ <Input.Root>
131
+ <InputHeader>
132
+ <Input.Label>
133
+ {t(isSpace(target) ? 'creating in space label' : 'creating in collection label')}
134
+ </Input.Label>
135
+ </InputHeader>
136
+ <div role='none' className='flex gap-2'>
137
+ <Input.TextInput
138
+ disabled
139
+ value={
140
+ isSpace(target)
141
+ ? toLocalizedString(getSpaceDisplayName(target, { personal: target.id === defaultSpaceId }), t)
142
+ : target.name || t('unnamed collection label')
143
+ }
144
+ />
145
+ <IconButton iconOnly icon='ph--x--regular' label={t('clear input label')} onClick={handleClearTarget} />
146
+ </div>
147
+ </Input.Root>
148
+ </div>
149
+ )}
150
+ {schema && (
151
+ <div role='none' className='px-2'>
152
+ <Input.Root>
153
+ <InputHeader>
154
+ <Input.Label>{t('creating object type label')}</Input.Label>
155
+ </InputHeader>
156
+ <div role='none' className='flex gap-2'>
157
+ <Input.TextInput
158
+ disabled
159
+ value={t('typename label', { ns: schema.typename, defaultValue: schema.typename })}
160
+ />
161
+ <IconButton iconOnly icon='ph--x--regular' label={t('clear input label')} onClick={handleClearSchema} />
162
+ </div>
163
+ </Input.Root>
164
+ </div>
165
+ )}
166
+ {!schema ? schemaInput : !target ? spaceInput : form}
167
+ </div>
168
+ );
169
+ };
@@ -0,0 +1,57 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback, useRef } from 'react';
6
+
7
+ import { useIntentDispatcher } from '@dxos/app-framework';
8
+ import { type S } from '@dxos/echo-schema';
9
+ import { Button, Dialog, Icon, useTranslation } from '@dxos/react-ui';
10
+ import { Form } from '@dxos/react-ui-form';
11
+
12
+ import { SPACE_PLUGIN, SpaceAction } from '../../meta';
13
+ import { SpaceForm } from '../../types';
14
+
15
+ type FormValues = S.Schema.Type<typeof SpaceForm>;
16
+ const initialValues: FormValues = { edgeReplication: true };
17
+
18
+ export const CreateSpaceDialog = () => {
19
+ const closeRef = useRef<HTMLButtonElement | null>(null);
20
+ const { t } = useTranslation(SPACE_PLUGIN);
21
+ const dispatch = useIntentDispatcher();
22
+
23
+ const handleCreateSpace = useCallback(
24
+ async (data: FormValues) => {
25
+ const result = await dispatch({
26
+ action: SpaceAction.CREATE,
27
+ data,
28
+ });
29
+ const target = result?.data.space;
30
+ if (target) {
31
+ await dispatch({
32
+ action: SpaceAction.OPEN_CREATE_OBJECT,
33
+ data: { target },
34
+ });
35
+ }
36
+ },
37
+ [dispatch],
38
+ );
39
+
40
+ return (
41
+ // TODO(wittjosiah): The tablist dialog pattern is copied from @dxos/plugin-manager.
42
+ // Consider factoring it out to the tabs package.
43
+ <Dialog.Content classNames='p-0 bs-content min-bs-[15rem] max-bs-full md:max-is-[40rem] overflow-hidden'>
44
+ <div role='none' className='flex justify-between pbs-3 pis-2 pie-3 @md:pbs-4 @md:pis-4 @md:pie-5'>
45
+ <Dialog.Title>{t('create space dialog title')}</Dialog.Title>
46
+ <Dialog.Close asChild>
47
+ <Button ref={closeRef} density='fine' variant='ghost' autoFocus>
48
+ <Icon icon='ph--x--regular' size={4} />
49
+ </Button>
50
+ </Dialog.Close>
51
+ </div>
52
+ <div className='p-4'>
53
+ <Form testId='create-space-form' values={initialValues} schema={SpaceForm} onSave={handleCreateSpace} />
54
+ </div>
55
+ </Dialog.Content>
56
+ );
57
+ };
@@ -0,0 +1,6 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './CreateObjectDialog';
6
+ export * from './CreateSpaceDialog';
@@ -4,7 +4,7 @@
4
4
 
5
5
  import React, { useCallback, useRef, useState } from 'react';
6
6
 
7
- import { type ReactiveObject } from '@dxos/echo-schema';
7
+ import { type ReactiveObject } from '@dxos/live-object';
8
8
  import { log } from '@dxos/log';
9
9
  import { Button, Input, Popover, useTranslation } from '@dxos/react-ui';
10
10
 
@@ -4,17 +4,16 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { useIntentDispatcher, useResolvePlugins } from '@dxos/app-framework';
8
- import { Input, Select, toLocalizedString, useTranslation } from '@dxos/react-ui';
7
+ import { useIntentDispatcher } from '@dxos/app-framework';
8
+ import { Input, useTranslation } from '@dxos/react-ui';
9
9
  import { DeprecatedFormInput } from '@dxos/react-ui-form';
10
10
 
11
11
  import { SpaceAction, SPACE_PLUGIN } from '../meta';
12
- import { parseSpaceInitPlugin, type SpaceSettingsProps } from '../types';
12
+ import { type SpaceSettingsProps } from '../types';
13
13
 
14
14
  export const SpacePluginSettings = ({ settings }: { settings: SpaceSettingsProps }) => {
15
15
  const { t } = useTranslation(SPACE_PLUGIN);
16
16
  const dispatch = useIntentDispatcher();
17
- const plugins = useResolvePlugins(parseSpaceInitPlugin);
18
17
 
19
18
  return (
20
19
  <>
@@ -30,34 +29,6 @@ export const SpacePluginSettings = ({ settings }: { settings: SpaceSettingsProps
30
29
  }
31
30
  />
32
31
  </DeprecatedFormInput>
33
-
34
- <DeprecatedFormInput label={t('default on space create label')}>
35
- <Select.Root
36
- value={settings.onSpaceCreate}
37
- onValueChange={(value) => {
38
- settings.onSpaceCreate = value;
39
- }}
40
- >
41
- <Select.TriggerButton />
42
- <Select.Portal>
43
- <Select.Content>
44
- <Select.Viewport>
45
- {plugins.map(
46
- ({
47
- provides: {
48
- space: { onSpaceCreate },
49
- },
50
- }) => (
51
- <Select.Option key={onSpaceCreate.action} value={onSpaceCreate.action}>
52
- {toLocalizedString(onSpaceCreate.label, t)}
53
- </Select.Option>
54
- ),
55
- )}
56
- </Select.Viewport>
57
- </Select.Content>
58
- </Select.Portal>
59
- </Select.Root>
60
- </DeprecatedFormInput>
61
32
  </>
62
33
  );
63
34
  };
@@ -62,7 +62,7 @@ export const SpaceSettingsDialog = ({
62
62
  </Dialog.Title>
63
63
  <Dialog.Close asChild>
64
64
  <Button density='fine' variant='ghost' autoFocus>
65
- <Icon icon='ph--x--regular' size={3} />
65
+ <Icon icon='ph--x--regular' size={4} />
66
66
  </Button>
67
67
  </Dialog.Close>
68
68
  </div>
@@ -8,7 +8,6 @@ import { log } from '@dxos/log';
8
8
  import { EdgeReplicationSetting } from '@dxos/protocols/proto/dxos/echo/metadata';
9
9
  import { useClient } from '@dxos/react-client';
10
10
  import { type Space } from '@dxos/react-client/echo';
11
- import { DeviceType, useDevices } from '@dxos/react-client/halo';
12
11
  import { Input, useTranslation } from '@dxos/react-ui';
13
12
  import { DeprecatedFormInput } from '@dxos/react-ui-form';
14
13
 
@@ -22,10 +21,7 @@ export const SpaceSettingsPanel = ({ space }: SpaceSettingsPanelProps) => {
22
21
  const { t } = useTranslation(SPACE_PLUGIN);
23
22
 
24
23
  const client = useClient();
25
- const devices = useDevices();
26
- const managedDeviceAvailable = devices.find((device) => device.profile?.type === DeviceType.AGENT_MANAGED);
27
- const edgeAgents = Boolean(client.config.values.runtime?.client?.edgeFeatures?.agents);
28
- const edgeReplicationAvailable = edgeAgents && managedDeviceAvailable;
24
+ const edgeEnabled = Boolean(client.config.values.runtime?.client?.edgeFeatures?.echoReplicator);
29
25
 
30
26
  const [edgeReplication, setEdgeReplication] = useState(
31
27
  space.internal.data.edgeReplication === EdgeReplicationSetting.ENABLED,
@@ -54,7 +50,7 @@ export const SpaceSettingsPanel = ({ space }: SpaceSettingsPanelProps) => {
54
50
  }}
55
51
  />
56
52
  </DeprecatedFormInput>
57
- {edgeReplicationAvailable && (
53
+ {edgeEnabled && (
58
54
  <DeprecatedFormInput label={t('edge replication label')}>
59
55
  <Input.Switch checked={edgeReplication} onCheckedChange={toggleEdgeReplication} />
60
56
  </DeprecatedFormInput>
@@ -0,0 +1,45 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useEffect, useState } from 'react';
6
+
7
+ import { QueryEdgeStatusResponse } from '@dxos/protocols/proto/dxos/client/services';
8
+ import { EdgeReplicationSetting } from '@dxos/protocols/proto/dxos/echo/metadata';
9
+ import { useClient } from '@dxos/react-client';
10
+ import { type Space } from '@dxos/react-client/echo';
11
+ import { Icon, useTranslation } from '@dxos/react-ui';
12
+
13
+ import { useSpaceSyncState } from './sync-state';
14
+ import { SPACE_PLUGIN } from '../../meta';
15
+
16
+ const useEdgeStatus = (): QueryEdgeStatusResponse.EdgeStatus => {
17
+ const [status, setStatus] = useState(QueryEdgeStatusResponse.EdgeStatus.NOT_CONNECTED);
18
+ const client = useClient();
19
+
20
+ useEffect(() => {
21
+ client.services.services.EdgeAgentService?.queryEdgeStatus().subscribe(({ status }) => {
22
+ setStatus(status);
23
+ });
24
+ }, [client]);
25
+
26
+ return status;
27
+ };
28
+
29
+ export const InlineSyncStatus = ({ space }: { space: Space }) => {
30
+ const { t } = useTranslation(SPACE_PLUGIN);
31
+
32
+ const connectedToEdge = useEdgeStatus() === QueryEdgeStatusResponse.EdgeStatus.CONNECTED;
33
+ // TODO(wittjosiah): This is not reactive.
34
+ const edgeSyncEnabled = space.internal.data.edgeReplication === EdgeReplicationSetting.ENABLED;
35
+ const syncState = useSpaceSyncState(space);
36
+ if (!connectedToEdge || !edgeSyncEnabled || !syncState || syncState.missingOnLocal === 0) {
37
+ return null;
38
+ }
39
+
40
+ return (
41
+ <div role='status' aria-label={t('syncing message')} className='flex items-center'>
42
+ <Icon icon='ph--arrows-clockwise--regular' size={3} classNames='animate-spin' />
43
+ </div>
44
+ );
45
+ };
@@ -4,11 +4,15 @@
4
4
 
5
5
  import React, { type HTMLAttributes, useEffect, useState } from 'react';
6
6
 
7
- import { Icon } from '@dxos/react-ui';
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';
8
10
  import { type ThemedClassName } from '@dxos/react-ui';
9
11
  import { mx } from '@dxos/react-ui-theme';
10
12
 
11
13
  import { type Progress, type PeerSyncState } from './sync-state';
14
+ import { SPACE_PLUGIN } from '../../meta';
15
+ import { getSpaceDisplayName } from '../../util';
12
16
 
13
17
  export const SYNC_STALLED_TIMEOUT = 5_000;
14
18
 
@@ -41,24 +45,44 @@ const useActive = (count: number) => {
41
45
  return active;
42
46
  };
43
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
+
44
69
  export const SpaceRow = ({
45
70
  spaceId,
71
+ spaceName,
46
72
  state: { localDocumentCount, remoteDocumentCount, missingOnLocal, missingOnRemote },
47
- }: {
48
- spaceId: string;
49
- state: PeerSyncState;
50
- }) => {
73
+ }: SpaceRowProps) => {
51
74
  const downActive = useActive(localDocumentCount);
52
75
  const upActive = useActive(remoteDocumentCount);
53
76
 
54
77
  return (
55
78
  <div
56
- className={mx('flex items-center mx-[2px] gap-[2px] cursor-pointer', styles.barHover)}
79
+ className='flex items-center mx-0.5 gap-0.5 cursor-pointer'
57
80
  title={spaceId}
58
81
  onClick={() => {
59
82
  void navigator.clipboard.writeText(spaceId);
60
83
  }}
61
84
  >
85
+ <span className='is-1/2 truncate'>{spaceName}</span>
62
86
  <Icon
63
87
  icon='ph--arrow-fat-line-left--regular'
64
88
  size={3}
@@ -7,11 +7,9 @@ import '@dxos-theme';
7
7
  import { type Meta, type StoryObj } from '@storybook/react';
8
8
  import React from 'react';
9
9
 
10
- import { SpaceId } from '@dxos/keys';
11
10
  import { withTheme, withLayout } from '@dxos/storybook-utils';
12
11
 
13
- import { SyncStatusDetail, SyncStatusIndicator } from './SyncStatus';
14
- import { getSyncSummary, type SpaceSyncStateMap } from './sync-state';
12
+ import { SyncStatusIndicator } from './SyncStatus';
15
13
  import translations from '../../translations';
16
14
 
17
15
  const DefaultStory = (props: any) => {
@@ -22,47 +20,20 @@ const DefaultStory = (props: any) => {
22
20
  );
23
21
  };
24
22
 
25
- const random = ({ min, max }: { min: number; max: number }) => min + Math.floor(Math.random() * (max - min));
26
-
27
- const state: SpaceSyncStateMap = Array.from({ length: 5 }).reduce<SpaceSyncStateMap>((map) => {
28
- const total = random({ min: 10, max: 500 });
29
- const haveLocal = random({ min: 0, max: total });
30
- const haveRemote = random({ min: 0, max: total });
31
- map[SpaceId.random()] = {
32
- localDocumentCount: haveLocal,
33
- remoteDocumentCount: haveRemote,
34
- missingOnLocal: total - haveLocal,
35
- missingOnRemote: total - haveRemote,
36
- differentDocuments: 0,
37
- };
38
-
39
- return map;
40
- }, {});
41
-
42
23
  export const Default: StoryObj<typeof SyncStatusIndicator> = {
43
24
  args: {
44
- state,
25
+ state: {},
45
26
  saved: true,
46
27
  },
47
28
  };
48
29
 
49
30
  export const Saving: StoryObj<typeof SyncStatusIndicator> = {
50
31
  args: {
51
- state,
32
+ state: {},
52
33
  saved: false,
53
34
  },
54
35
  };
55
36
 
56
- // TODO(wittjosiah): Separate story path for separate component.
57
- export const Detail: StoryObj<typeof SyncStatusDetail> = {
58
- render: SyncStatusDetail,
59
- args: {
60
- state,
61
- summary: getSyncSummary(state),
62
- classNames: 'm-2 w-[200px] border border-separator rounded-md',
63
- },
64
- };
65
-
66
37
  const meta: Meta = {
67
38
  title: 'plugins/plugin-space/SyncStatusIndicator',
68
39
  component: SyncStatusIndicator,