@dxos/plugin-space 0.6.13 → 0.6.14-main.1366248
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/browser/{chunk-LZEGRS7H.mjs → chunk-AVLRQF6L.mjs} +1 -1
- package/dist/lib/browser/chunk-AVLRQF6L.mjs.map +7 -0
- package/dist/lib/browser/{chunk-DTVUOG2C.mjs → chunk-WZAM3FNP.mjs} +24 -5
- package/dist/lib/browser/chunk-WZAM3FNP.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +852 -348
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/meta.mjs +1 -1
- package/dist/lib/browser/types/index.mjs +7 -3
- package/dist/lib/node/{chunk-CVZPI2P3.cjs → chunk-HTAM5LQD.cjs} +30 -9
- package/dist/lib/node/chunk-HTAM5LQD.cjs.map +7 -0
- package/dist/lib/node/{chunk-6CNYF6YU.cjs → chunk-P4XUXM7Y.cjs} +4 -4
- package/dist/lib/node/chunk-P4XUXM7Y.cjs.map +7 -0
- package/dist/lib/node/index.cjs +1046 -550
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.cjs +5 -5
- package/dist/lib/node/meta.cjs.map +1 -1
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/types/index.cjs +14 -10
- package/dist/lib/node/types/index.cjs.map +2 -2
- package/dist/lib/node-esm/chunk-TRJKV4PK.mjs +116 -0
- package/dist/lib/node-esm/chunk-TRJKV4PK.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-YPQGKWHJ.mjs +37 -0
- package/dist/lib/node-esm/chunk-YPQGKWHJ.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +3145 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/lib/node-esm/meta.mjs +14 -0
- package/dist/lib/node-esm/meta.mjs.map +7 -0
- package/dist/lib/node-esm/types/index.mjs +26 -0
- package/dist/lib/node-esm/types/index.mjs.map +7 -0
- package/dist/types/src/SpacePlugin.d.ts.map +1 -1
- package/dist/types/src/components/DefaultObjectSettings.d.ts +7 -0
- package/dist/types/src/components/DefaultObjectSettings.d.ts.map +1 -0
- package/dist/types/src/components/MenuFooter.d.ts +1 -1
- package/dist/types/src/components/MenuFooter.d.ts.map +1 -1
- package/dist/types/src/components/ShareSpaceButton.stories.d.ts +3 -91
- package/dist/types/src/components/ShareSpaceButton.stories.d.ts.map +1 -1
- package/dist/types/src/components/SpaceMain/SpaceMain.d.ts.map +1 -1
- package/dist/types/src/components/SpacePresence.d.ts +4 -2
- package/dist/types/src/components/SpacePresence.d.ts.map +1 -1
- package/dist/types/src/components/SpacePresence.stories.d.ts +4 -92
- package/dist/types/src/components/SpacePresence.stories.d.ts.map +1 -1
- package/dist/types/src/components/SpaceSettings.d.ts.map +1 -1
- package/dist/types/src/components/SpaceSettingsPanel.d.ts +7 -0
- package/dist/types/src/components/SpaceSettingsPanel.d.ts.map +1 -0
- package/dist/types/src/components/SyncStatus/Space.d.ts +8 -0
- package/dist/types/src/components/SyncStatus/Space.d.ts.map +1 -0
- package/dist/types/src/components/SyncStatus/SyncStatus.d.ts +14 -0
- package/dist/types/src/components/SyncStatus/SyncStatus.d.ts.map +1 -0
- package/dist/types/src/components/SyncStatus/SyncStatus.stories.d.ts +9 -0
- package/dist/types/src/components/SyncStatus/SyncStatus.stories.d.ts.map +1 -0
- package/dist/types/src/components/SyncStatus/index.d.ts +2 -0
- package/dist/types/src/components/SyncStatus/index.d.ts.map +1 -0
- package/dist/types/src/components/SyncStatus/save-tracker.d.ts +3 -0
- package/dist/types/src/components/SyncStatus/save-tracker.d.ts.map +1 -0
- package/dist/types/src/components/SyncStatus/status.d.ts +9 -0
- package/dist/types/src/components/SyncStatus/status.d.ts.map +1 -0
- package/dist/types/src/components/SyncStatus/sync-state.d.ts +14 -0
- package/dist/types/src/components/SyncStatus/sync-state.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +3 -3
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/meta.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +14 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/thread.d.ts +15 -1
- package/dist/types/src/types/thread.d.ts.map +1 -1
- package/dist/types/src/types/types.d.ts +23 -2
- package/dist/types/src/types/types.d.ts.map +1 -1
- package/dist/types/src/util.d.ts +4 -7
- package/dist/types/src/util.d.ts.map +1 -1
- package/package.json +46 -45
- package/src/SpacePlugin.tsx +275 -137
- package/src/components/DefaultObjectSettings.tsx +33 -0
- package/src/components/MenuFooter.tsx +2 -2
- package/src/components/ShareSpaceButton.stories.tsx +11 -7
- package/src/components/SpaceMain/SpaceMain.tsx +1 -22
- package/src/components/SpacePresence.stories.tsx +11 -9
- package/src/components/SpacePresence.tsx +34 -23
- package/src/components/SpaceSettings.tsx +35 -6
- package/src/components/SpaceSettingsPanel.tsx +69 -0
- package/src/components/SyncStatus/Space.tsx +109 -0
- package/src/components/SyncStatus/SyncStatus.stories.tsx +74 -0
- package/src/components/SyncStatus/SyncStatus.tsx +102 -0
- package/src/components/SyncStatus/index.ts +5 -0
- package/src/components/SyncStatus/save-tracker.ts +71 -0
- package/src/components/SyncStatus/status.ts +44 -0
- package/src/components/SyncStatus/sync-state.ts +77 -0
- package/src/components/index.ts +3 -3
- package/src/meta.ts +3 -1
- package/src/translations.ts +16 -2
- package/src/types/collection.ts +1 -1
- package/src/types/thread.ts +12 -2
- package/src/types/types.ts +31 -3
- package/src/util.tsx +23 -58
- package/dist/lib/browser/chunk-DTVUOG2C.mjs.map +0 -7
- package/dist/lib/browser/chunk-LZEGRS7H.mjs.map +0 -7
- package/dist/lib/node/chunk-6CNYF6YU.cjs.map +0 -7
- package/dist/lib/node/chunk-CVZPI2P3.cjs.map +0 -7
- package/dist/types/src/components/EmptySpace.d.ts +0 -3
- package/dist/types/src/components/EmptySpace.d.ts.map +0 -1
- package/dist/types/src/components/EmptyTree.d.ts +0 -3
- package/dist/types/src/components/EmptyTree.d.ts.map +0 -1
- package/dist/types/src/components/MissingObject.d.ts +0 -5
- package/dist/types/src/components/MissingObject.d.ts.map +0 -1
- package/src/components/EmptySpace.tsx +0 -25
- package/src/components/EmptyTree.tsx +0 -25
- package/src/components/MissingObject.tsx +0 -54
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { type EchoReactiveObject } from '@dxos/react-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
|
+
// TODO(burdon): Standardize forms.
|
|
19
|
+
return (
|
|
20
|
+
<div role='form' className='flex flex-col w-full p-2 gap-1'>
|
|
21
|
+
<Input.Root>
|
|
22
|
+
<Input.Label>{t('name label')}</Input.Label>
|
|
23
|
+
<Input.TextInput
|
|
24
|
+
placeholder={t('name placeholder')}
|
|
25
|
+
value={object.name ?? ''}
|
|
26
|
+
onChange={(event) => {
|
|
27
|
+
object.name = event.target.value;
|
|
28
|
+
}}
|
|
29
|
+
/>
|
|
30
|
+
</Input.Root>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
@@ -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
|
|
|
@@ -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 {
|
|
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 (
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React from 'react';
|
|
6
|
-
|
|
7
5
|
import '@dxos-theme';
|
|
8
6
|
|
|
7
|
+
import { type Meta } from '@storybook/react';
|
|
8
|
+
import React from 'react';
|
|
9
|
+
|
|
9
10
|
import { PublicKey } from '@dxos/keys';
|
|
10
11
|
import { HaloSpaceMember, SpaceMember } from '@dxos/react-client/echo';
|
|
11
12
|
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
@@ -13,13 +14,6 @@ import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
|
13
14
|
import { FullPresence, type MemberPresenceProps, SmallPresence, type Member } from './SpacePresence';
|
|
14
15
|
import translations from '../translations';
|
|
15
16
|
|
|
16
|
-
export default {
|
|
17
|
-
title: 'plugin-space/SpacePresence',
|
|
18
|
-
decorators: [withTheme, withLayout({ tooltips: true })],
|
|
19
|
-
parameters: { translations },
|
|
20
|
-
actions: { argTypesRegex: '^on.*' },
|
|
21
|
-
};
|
|
22
|
-
|
|
23
17
|
const nViewers = (n: number, currentlyAttended = true): Member[] =>
|
|
24
18
|
Array.from({ length: n }, () => ({
|
|
25
19
|
role: HaloSpaceMember.Role.ADMIN,
|
|
@@ -95,3 +89,11 @@ export const Small = (props: MemberPresenceProps) => {
|
|
|
95
89
|
</div>
|
|
96
90
|
);
|
|
97
91
|
};
|
|
92
|
+
|
|
93
|
+
const meta: Meta = {
|
|
94
|
+
title: 'plugins/plugin-space/SpacePresence',
|
|
95
|
+
decorators: [withTheme, withLayout({ tooltips: true })],
|
|
96
|
+
parameters: { translations },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default meta;
|
|
@@ -17,13 +17,12 @@ import {
|
|
|
17
17
|
type Size,
|
|
18
18
|
type ThemedClassName,
|
|
19
19
|
Tooltip,
|
|
20
|
-
useDensityContext,
|
|
21
20
|
useTranslation,
|
|
22
21
|
List,
|
|
23
22
|
ListItem,
|
|
24
23
|
useDefaultValue,
|
|
25
24
|
} from '@dxos/react-ui';
|
|
26
|
-
import { AttentionGlyph } from '@dxos/react-ui-attention';
|
|
25
|
+
import { AttentionGlyph, useAttention } from '@dxos/react-ui-attention';
|
|
27
26
|
import { ComplexMap, keyToFallback } from '@dxos/util';
|
|
28
27
|
|
|
29
28
|
import { SPACE_PLUGIN } from '../meta';
|
|
@@ -40,7 +39,6 @@ const noViewers = new ComplexMap<PublicKey, ObjectViewerProps>(PublicKey.hash);
|
|
|
40
39
|
const getName = (identity: Identity) => identity.profile?.displayName ?? generateName(identity.identityKey.toHex());
|
|
41
40
|
|
|
42
41
|
export const SpacePresence = ({ object, spaceKey }: { object: Expando; spaceKey?: PublicKey }) => {
|
|
43
|
-
const density = useDensityContext();
|
|
44
42
|
const spacePlugin = usePlugin<SpacePluginProvides>(SPACE_PLUGIN);
|
|
45
43
|
const client = useClient();
|
|
46
44
|
const identity = useIdentity();
|
|
@@ -86,11 +84,7 @@ export const SpacePresence = ({ object, spaceKey }: { object: Expando; spaceKey?
|
|
|
86
84
|
})
|
|
87
85
|
.toSorted((a, b) => a.lastSeen - b.lastSeen);
|
|
88
86
|
|
|
89
|
-
return
|
|
90
|
-
<SmallPresence count={membersForObject.length} />
|
|
91
|
-
) : (
|
|
92
|
-
<FullPresence members={membersForObject} />
|
|
93
|
-
);
|
|
87
|
+
return <FullPresence members={membersForObject} />;
|
|
94
88
|
};
|
|
95
89
|
|
|
96
90
|
export type Member = SpaceMember & {
|
|
@@ -110,7 +104,7 @@ export type MemberPresenceProps = ThemedClassName<{
|
|
|
110
104
|
|
|
111
105
|
export const FullPresence = (props: MemberPresenceProps) => {
|
|
112
106
|
const { size = 9, onMemberClick } = props;
|
|
113
|
-
const members = useDefaultValue(props.members, []);
|
|
107
|
+
const members = useDefaultValue(props.members, () => []);
|
|
114
108
|
|
|
115
109
|
if (members.length === 0) {
|
|
116
110
|
return null;
|
|
@@ -200,28 +194,45 @@ const PrensenceAvatar = ({ identity, showName, match, group, index, onClick }: P
|
|
|
200
194
|
);
|
|
201
195
|
};
|
|
202
196
|
|
|
203
|
-
export const SmallPresenceLive = ({
|
|
204
|
-
|
|
197
|
+
export const SmallPresenceLive = ({
|
|
198
|
+
id,
|
|
199
|
+
viewers,
|
|
200
|
+
}: {
|
|
201
|
+
id?: string;
|
|
202
|
+
viewers?: ComplexMap<PublicKey, ObjectViewerProps>;
|
|
203
|
+
}) => {
|
|
204
|
+
const getActiveViewers = (viewers: ComplexMap<PublicKey, ObjectViewerProps>): ObjectViewerProps[] => {
|
|
205
|
+
const moment = Date.now();
|
|
206
|
+
return Array.from(viewers.values()).filter(({ lastSeen }) => moment - lastSeen < ACTIVITY_DURATION);
|
|
207
|
+
};
|
|
205
208
|
|
|
206
|
-
|
|
207
|
-
useEffect(() => {
|
|
208
|
-
const interval = setInterval(() => setMoment(Date.now()), REFRESH_INTERVAL);
|
|
209
|
-
return () => clearInterval(interval);
|
|
210
|
-
}, []);
|
|
211
|
-
|
|
212
|
-
const activeViewers = viewers
|
|
213
|
-
? Array.from(viewers.values()).filter(({ lastSeen }) => moment - lastSeen < ACTIVITY_DURATION)
|
|
214
|
-
: [];
|
|
209
|
+
const [activeViewers, setActiveViewers] = useState(viewers ? getActiveViewers(viewers) : []);
|
|
215
210
|
|
|
216
|
-
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
if (viewers) {
|
|
213
|
+
setActiveViewers(getActiveViewers(viewers));
|
|
214
|
+
const interval = setInterval(() => {
|
|
215
|
+
setActiveViewers(getActiveViewers(viewers));
|
|
216
|
+
}, REFRESH_INTERVAL);
|
|
217
|
+
return () => clearInterval(interval);
|
|
218
|
+
}
|
|
219
|
+
}, [viewers]);
|
|
220
|
+
|
|
221
|
+
return <SmallPresence id={id} count={activeViewers.length} />;
|
|
217
222
|
};
|
|
218
223
|
|
|
219
|
-
export const SmallPresence = ({ count }: { count: number }) => {
|
|
224
|
+
export const SmallPresence = ({ id, count }: { id?: string; count: number }) => {
|
|
220
225
|
const { t } = useTranslation(SPACE_PLUGIN);
|
|
226
|
+
const { hasAttention, isAncestor, isRelated } = useAttention(id);
|
|
227
|
+
const attention = hasAttention || isAncestor || isRelated;
|
|
228
|
+
|
|
221
229
|
return (
|
|
222
230
|
<Tooltip.Root>
|
|
223
231
|
<Tooltip.Trigger asChild>
|
|
224
|
-
|
|
232
|
+
{/* TODO(wittjosiah): Don't depend on data attribute just pass prop to AttentionGlyph. */}
|
|
233
|
+
<div role='none' className='flex' data-attention={attention}>
|
|
234
|
+
<AttentionGlyph presence={count > 1 ? 'many' : count === 1 ? 'one' : 'none'} classNames='self-center mie-1' />
|
|
235
|
+
</div>
|
|
225
236
|
</Tooltip.Trigger>
|
|
226
237
|
<Tooltip.Portal>
|
|
227
238
|
<Tooltip.Content side='bottom' classNames='z-[70]'>
|
|
@@ -4,20 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
import React from 'react';
|
|
6
6
|
|
|
7
|
-
import { useIntentDispatcher } from '@dxos/app-framework';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { useIntentDispatcher, useResolvePlugins } from '@dxos/app-framework';
|
|
8
|
+
import { Input, Select, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
9
|
+
import { DeprecatedFormInput } from '@dxos/react-ui-data';
|
|
10
10
|
|
|
11
11
|
import { SpaceAction, SPACE_PLUGIN } from '../meta';
|
|
12
|
-
import { type SpaceSettingsProps } from '../types';
|
|
12
|
+
import { parseSpaceInitPlugin, type SpaceSettingsProps } from '../types';
|
|
13
13
|
|
|
14
14
|
export const SpaceSettings = ({ settings }: { settings: SpaceSettingsProps }) => {
|
|
15
15
|
const { t } = useTranslation(SPACE_PLUGIN);
|
|
16
16
|
const dispatch = useIntentDispatcher();
|
|
17
|
+
const plugins = useResolvePlugins(parseSpaceInitPlugin);
|
|
17
18
|
|
|
18
19
|
return (
|
|
19
20
|
<>
|
|
20
|
-
<
|
|
21
|
+
<DeprecatedFormInput label={t('show hidden spaces label')}>
|
|
21
22
|
<Input.Switch
|
|
22
23
|
checked={settings.showHidden}
|
|
23
24
|
onCheckedChange={(checked) =>
|
|
@@ -28,7 +29,35 @@ export const SpaceSettings = ({ settings }: { settings: SpaceSettingsProps }) =>
|
|
|
28
29
|
})
|
|
29
30
|
}
|
|
30
31
|
/>
|
|
31
|
-
</
|
|
32
|
+
</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>
|
|
32
61
|
</>
|
|
33
62
|
);
|
|
34
63
|
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useCallback, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { log } from '@dxos/log';
|
|
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 { DeviceType, useDevices } from '@dxos/react-client/halo';
|
|
12
|
+
import { Input, useTranslation } from '@dxos/react-ui';
|
|
13
|
+
|
|
14
|
+
import { SPACE_PLUGIN } from '../meta';
|
|
15
|
+
|
|
16
|
+
export type SpaceSettingsPanelProps = {
|
|
17
|
+
space: Space;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const SpaceSettingsPanel = ({ space }: SpaceSettingsPanelProps) => {
|
|
21
|
+
const { t } = useTranslation(SPACE_PLUGIN);
|
|
22
|
+
|
|
23
|
+
const client = useClient();
|
|
24
|
+
const devices = useDevices();
|
|
25
|
+
const managedDeviceAvailable = devices.find((device) => device.profile?.type === DeviceType.AGENT_MANAGED);
|
|
26
|
+
const edgeAgents = Boolean(client.config.values.runtime?.client?.edgeFeatures?.agents);
|
|
27
|
+
const edgeReplicationAvailable = edgeAgents && managedDeviceAvailable;
|
|
28
|
+
|
|
29
|
+
const [edgeReplication, setEdgeReplication] = useState(
|
|
30
|
+
space.internal.data.edgeReplication === EdgeReplicationSetting.ENABLED,
|
|
31
|
+
);
|
|
32
|
+
const toggleEdgeReplication = useCallback(
|
|
33
|
+
async (next: boolean) => {
|
|
34
|
+
setEdgeReplication(next);
|
|
35
|
+
await space?.internal
|
|
36
|
+
.setEdgeReplicationPreference(next ? EdgeReplicationSetting.ENABLED : EdgeReplicationSetting.DISABLED)
|
|
37
|
+
.catch((err) => {
|
|
38
|
+
log.catch(err);
|
|
39
|
+
setEdgeReplication(!next);
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
[space],
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div role='form' className='flex flex-col w-full p-2 gap-4'>
|
|
47
|
+
<Input.Root>
|
|
48
|
+
<div role='none' className='flex flex-col gap-1'>
|
|
49
|
+
<Input.Label>{t('name label')}</Input.Label>
|
|
50
|
+
<Input.TextInput
|
|
51
|
+
placeholder={t('name placeholder')}
|
|
52
|
+
value={space.properties.name ?? ''}
|
|
53
|
+
onChange={(event) => {
|
|
54
|
+
space.properties.name = event.target.value;
|
|
55
|
+
}}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
</Input.Root>
|
|
59
|
+
{edgeReplicationAvailable && (
|
|
60
|
+
<Input.Root>
|
|
61
|
+
<div role='none' className='flex justify-between'>
|
|
62
|
+
<Input.Label>{t('edge replication label')}</Input.Label>
|
|
63
|
+
<Input.Switch checked={edgeReplication} onCheckedChange={toggleEdgeReplication} />
|
|
64
|
+
</div>
|
|
65
|
+
</Input.Root>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type HTMLAttributes, useEffect, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Icon } from '@dxos/react-ui';
|
|
8
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
9
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
10
|
+
|
|
11
|
+
import { type Progress, type PeerSyncState } from './sync-state';
|
|
12
|
+
|
|
13
|
+
export const SYNC_STALLED_TIMEOUT = 5_000;
|
|
14
|
+
|
|
15
|
+
// TODO(wittjosiah): Define sematic color tokens.
|
|
16
|
+
const styles = {
|
|
17
|
+
barBg: 'bg-neutral-50 dark:bg-green-900 text-black',
|
|
18
|
+
barFg: 'bg-neutral-100 bg-green-500',
|
|
19
|
+
barHover: 'dark:hover:bg-green-500',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const useActive = (count: number) => {
|
|
23
|
+
const [current, setCurrent] = useState(count);
|
|
24
|
+
const [active, setActive] = useState(false);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
let t: NodeJS.Timeout | undefined;
|
|
27
|
+
if (count !== current) {
|
|
28
|
+
setActive(true);
|
|
29
|
+
setCurrent(count);
|
|
30
|
+
t && clearTimeout(t);
|
|
31
|
+
t = setTimeout(() => {
|
|
32
|
+
setActive(false);
|
|
33
|
+
}, SYNC_STALLED_TIMEOUT);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return () => {
|
|
37
|
+
setActive(false);
|
|
38
|
+
clearTimeout(t);
|
|
39
|
+
};
|
|
40
|
+
}, [count, current]);
|
|
41
|
+
return active;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const SpaceRow = ({
|
|
45
|
+
spaceId,
|
|
46
|
+
state: { localDocumentCount, remoteDocumentCount, missingOnLocal, missingOnRemote },
|
|
47
|
+
}: {
|
|
48
|
+
spaceId: string;
|
|
49
|
+
state: PeerSyncState;
|
|
50
|
+
}) => {
|
|
51
|
+
const downActive = useActive(localDocumentCount);
|
|
52
|
+
const upActive = useActive(remoteDocumentCount);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
className={mx('flex items-center mx-[2px] gap-[2px] cursor-pointer', styles.barHover)}
|
|
57
|
+
title={spaceId}
|
|
58
|
+
onClick={() => {
|
|
59
|
+
void navigator.clipboard.writeText(spaceId);
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<Icon
|
|
63
|
+
icon='ph--arrow-fat-line-left--regular'
|
|
64
|
+
size={3}
|
|
65
|
+
classNames={mx(downActive && 'animate-[pulse_1s_infinite]')}
|
|
66
|
+
/>
|
|
67
|
+
<Candle
|
|
68
|
+
up={{ count: remoteDocumentCount, total: remoteDocumentCount + missingOnRemote }}
|
|
69
|
+
down={{ count: localDocumentCount, total: localDocumentCount + missingOnLocal }}
|
|
70
|
+
title={spaceId}
|
|
71
|
+
/>
|
|
72
|
+
<Icon
|
|
73
|
+
icon='ph--arrow-fat-line-right--regular'
|
|
74
|
+
size={3}
|
|
75
|
+
classNames={mx(upActive && 'animate-[pulse_1s_step-start_infinite]')}
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type CandleProps = ThemedClassName<Pick<HTMLAttributes<HTMLDivElement>, 'title'>> & { up: Progress; down: Progress };
|
|
82
|
+
|
|
83
|
+
const Candle = ({ classNames, up, down }: CandleProps) => {
|
|
84
|
+
return (
|
|
85
|
+
<div className={mx('grid grid-cols-[1fr_2rem_1fr] w-full h-3', classNames)}>
|
|
86
|
+
<Bar classNames='justify-end' {...up} />
|
|
87
|
+
<div className='relative'>
|
|
88
|
+
<div className={mx('absolute inset-0 flex items-center justify-center text-xs', styles.barBg)}>{up.total}</div>
|
|
89
|
+
</div>
|
|
90
|
+
<Bar {...down} />
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const Bar = ({ classNames, count, total }: ThemedClassName<Progress>) => {
|
|
96
|
+
let p = (count / total) * 100;
|
|
97
|
+
if (count < total) {
|
|
98
|
+
p = Math.min(p, 95);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className={mx('relative flex w-full', styles.barBg, classNames)}>
|
|
103
|
+
<div className={mx('shrink-0', styles.barFg)} style={{ width: `${p}%` }}></div>
|
|
104
|
+
{count !== total && (
|
|
105
|
+
<div className='absolute top-0 bottom-0 flex items-center mx-0.5 text-black text-xs'>{count}</div>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
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 from 'react';
|
|
9
|
+
|
|
10
|
+
import { SpaceId } from '@dxos/keys';
|
|
11
|
+
import { withTheme, withLayout } from '@dxos/storybook-utils';
|
|
12
|
+
|
|
13
|
+
import { SyncStatusDetail, SyncStatusIndicator } from './SyncStatus';
|
|
14
|
+
import { getSyncSummary, type SpaceSyncStateMap } from './sync-state';
|
|
15
|
+
import translations from '../../translations';
|
|
16
|
+
|
|
17
|
+
const DefaultStory = (props: any) => {
|
|
18
|
+
return (
|
|
19
|
+
<div className='flex flex-col-reverse p-4 '>
|
|
20
|
+
<SyncStatusIndicator {...props} />
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
};
|
|
24
|
+
|
|
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
|
+
export const Default: StoryObj<typeof SyncStatusIndicator> = {
|
|
43
|
+
args: {
|
|
44
|
+
state,
|
|
45
|
+
saved: true,
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const Saving: StoryObj<typeof SyncStatusIndicator> = {
|
|
50
|
+
args: {
|
|
51
|
+
state,
|
|
52
|
+
saved: false,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
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
|
+
const meta: Meta = {
|
|
67
|
+
title: 'plugins/plugin-space/SyncStatusIndicator',
|
|
68
|
+
component: SyncStatusIndicator,
|
|
69
|
+
render: DefaultStory,
|
|
70
|
+
decorators: [withTheme, withLayout({ fullscreen: true })],
|
|
71
|
+
parameters: { translations },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default meta;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useEffect, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { StatusBar } from '@dxos/plugin-status-bar';
|
|
8
|
+
import { useClient } from '@dxos/react-client';
|
|
9
|
+
import { Icon, Popover, useTranslation } from '@dxos/react-ui';
|
|
10
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
11
|
+
import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
|
|
12
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
13
|
+
|
|
14
|
+
import { SpaceRow, SYNC_STALLED_TIMEOUT } from './Space';
|
|
15
|
+
import { createClientSaveTracker } from './save-tracker';
|
|
16
|
+
import { getIcon, getStatus } from './status';
|
|
17
|
+
import { type PeerSyncState, type SpaceSyncStateMap, getSyncSummary, useSyncState } from './sync-state';
|
|
18
|
+
import { SPACE_PLUGIN } from '../../meta';
|
|
19
|
+
|
|
20
|
+
export const SyncStatus = () => {
|
|
21
|
+
const client = useClient();
|
|
22
|
+
const state = useSyncState();
|
|
23
|
+
const [saved, setSaved] = useState(true);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
return createClientSaveTracker(client, (state) => {
|
|
27
|
+
setSaved(state === 'saved');
|
|
28
|
+
});
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
return <SyncStatusIndicator state={state} saved={saved} />;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const SyncStatusIndicator = ({ state, saved }: { state: SpaceSyncStateMap; saved: Boolean }) => {
|
|
35
|
+
const { t } = useTranslation(SPACE_PLUGIN);
|
|
36
|
+
const summary = getSyncSummary(state);
|
|
37
|
+
const offline = Object.values(state).length === 0;
|
|
38
|
+
const needsToUpload = summary.differentDocuments > 0 || summary.missingOnRemote > 0;
|
|
39
|
+
const needsToDownload = summary.differentDocuments > 0 || summary.missingOnLocal > 0;
|
|
40
|
+
const status = getStatus({ offline, saved, needsToUpload, needsToDownload });
|
|
41
|
+
|
|
42
|
+
const [classNames, setClassNames] = useState<string>();
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
setClassNames(undefined);
|
|
45
|
+
if (offline || (!needsToUpload && !needsToDownload)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const t = setTimeout(() => {
|
|
50
|
+
// TODO(wittjosiah): Use semantic color tokens.
|
|
51
|
+
setClassNames('text-orange-500');
|
|
52
|
+
}, SYNC_STALLED_TIMEOUT);
|
|
53
|
+
return () => clearTimeout(t);
|
|
54
|
+
}, [offline, needsToUpload, needsToDownload]);
|
|
55
|
+
|
|
56
|
+
const title = t(`${status} label`);
|
|
57
|
+
const icon = <Icon icon={getIcon(status)} size={4} classNames={classNames} />;
|
|
58
|
+
|
|
59
|
+
if (offline) {
|
|
60
|
+
return <StatusBar.Item title={title}>{icon}</StatusBar.Item>;
|
|
61
|
+
} else {
|
|
62
|
+
return (
|
|
63
|
+
<Popover.Root>
|
|
64
|
+
<Popover.Trigger asChild>
|
|
65
|
+
<StatusBar.Button title={title}>{icon}</StatusBar.Button>
|
|
66
|
+
</Popover.Trigger>
|
|
67
|
+
<Popover.Portal>
|
|
68
|
+
<Popover.Content sideOffset={16}>
|
|
69
|
+
<SyncStatusDetail state={state} summary={summary} debug={false} />
|
|
70
|
+
</Popover.Content>
|
|
71
|
+
</Popover.Portal>
|
|
72
|
+
</Popover.Root>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const SyncStatusDetail = ({
|
|
78
|
+
classNames,
|
|
79
|
+
state,
|
|
80
|
+
summary,
|
|
81
|
+
debug,
|
|
82
|
+
}: ThemedClassName<{
|
|
83
|
+
state: SpaceSyncStateMap;
|
|
84
|
+
summary: PeerSyncState;
|
|
85
|
+
debug?: boolean;
|
|
86
|
+
}>) => {
|
|
87
|
+
const { t } = useTranslation(SPACE_PLUGIN);
|
|
88
|
+
const entries = Object.entries(state).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
89
|
+
|
|
90
|
+
// TODO(burdon): Normalize to max document count?
|
|
91
|
+
return (
|
|
92
|
+
<div className={mx('flex flex-col gap-3 p-2 text-xs min-w-[16rem]', classNames)}>
|
|
93
|
+
<h1>{t('sync status title')}</h1>
|
|
94
|
+
<div className='flex flex-col gap-2'>
|
|
95
|
+
{entries.map(([spaceId, state]) => (
|
|
96
|
+
<SpaceRow key={spaceId} spaceId={spaceId} state={state} />
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
{debug && <SyntaxHighlighter language='json'>{JSON.stringify(summary, null, 2)}</SyntaxHighlighter>}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
};
|