@dxos/plugin-registry 0.8.4-main.9be5663bfe → 0.8.4-main.abd8ff62ef
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/cli/index.mjs +1 -188
- package/dist/lib/browser/cli/index.mjs.map +4 -4
- package/dist/lib/browser/index.mjs +5 -45
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/translations.mjs +35 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/lib/node-esm/cli/index.mjs +1 -188
- package/dist/lib/node-esm/cli/index.mjs.map +4 -4
- package/dist/lib/node-esm/index.mjs +5 -45
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/translations.mjs +37 -0
- package/dist/lib/node-esm/translations.mjs.map +7 -0
- package/dist/types/src/RegistryPlugin.d.ts +1 -0
- package/dist/types/src/RegistryPlugin.d.ts.map +1 -1
- package/dist/types/src/{cli/plugin.d.ts → RegistryPlugin.node.d.ts} +2 -1
- package/dist/types/src/RegistryPlugin.node.d.ts.map +1 -0
- package/dist/types/src/RegistryPlugin.test.d.ts +2 -0
- package/dist/types/src/RegistryPlugin.test.d.ts.map +1 -0
- package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
- package/dist/types/src/capabilities/index.d.ts +2 -1
- package/dist/types/src/capabilities/index.d.ts.map +1 -1
- package/dist/types/src/capabilities/operation-handler.d.ts +1 -1
- package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -1
- package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
- package/dist/types/src/capabilities/state.d.ts +7 -0
- package/dist/types/src/capabilities/state.d.ts.map +1 -0
- package/dist/types/src/cli/index.d.ts +1 -2
- package/dist/types/src/cli/index.d.ts.map +1 -1
- package/dist/types/src/{cli/commands → commands}/disable.d.ts +2 -2
- package/dist/types/src/commands/disable.d.ts.map +1 -0
- package/dist/types/src/{cli/commands → commands}/enable.d.ts +2 -2
- package/dist/types/src/commands/enable.d.ts.map +1 -0
- package/dist/types/src/commands/index.d.ts.map +1 -0
- package/dist/types/src/commands/list.d.ts +7 -0
- package/dist/types/src/commands/list.d.ts.map +1 -0
- package/dist/types/src/commands/util.d.ts.map +1 -0
- package/dist/types/src/components/PluginDetail/PluginDetail.d.ts +12 -1
- package/dist/types/src/components/PluginDetail/PluginDetail.d.ts.map +1 -1
- package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts +23 -20
- package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts.map +1 -1
- package/dist/types/src/components/PluginList/PluginItem.d.ts +14 -1
- package/dist/types/src/components/PluginList/PluginItem.d.ts.map +1 -1
- package/dist/types/src/components/PluginList/PluginList.d.ts +7 -2
- package/dist/types/src/components/PluginList/PluginList.d.ts.map +1 -1
- package/dist/types/src/components/PluginList/PluginList.stories.d.ts +23 -20
- package/dist/types/src/components/PluginList/PluginList.stories.d.ts.map +1 -1
- package/dist/types/src/containers/CommunityRegistry/CommunityRegistry.d.ts +8 -0
- package/dist/types/src/containers/CommunityRegistry/CommunityRegistry.d.ts.map +1 -0
- package/dist/types/src/containers/CommunityRegistry/index.d.ts +2 -0
- package/dist/types/src/containers/CommunityRegistry/index.d.ts.map +1 -0
- package/dist/types/src/containers/LoadPluginDialog/LoadPluginDialog.d.ts.map +1 -1
- package/dist/types/src/containers/PluginArticle/PluginArticle.d.ts.map +1 -1
- package/dist/types/src/containers/PluginRegistry/PluginRegistry.d.ts +5 -1
- package/dist/types/src/containers/PluginRegistry/PluginRegistry.d.ts.map +1 -1
- package/dist/types/src/containers/index.d.ts +1 -0
- package/dist/types/src/containers/index.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +3 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/use-auto-tags.d.ts +20 -0
- package/dist/types/src/hooks/use-auto-tags.d.ts.map +1 -0
- package/dist/types/src/hooks/use-community-plugins.d.ts +16 -0
- package/dist/types/src/hooks/use-community-plugins.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +2 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/meta.d.ts.map +1 -1
- package/dist/types/src/operations/index.d.ts +1 -1
- package/dist/types/src/operations/index.d.ts.map +1 -1
- package/dist/types/src/operations/open-plugin-registry.d.ts +1 -1
- package/dist/types/src/operations/open-plugin-registry.d.ts.map +1 -1
- package/dist/types/src/storage.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +22 -20
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +12 -1
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +41 -25
- package/src/{cli/plugin.ts → RegistryPlugin.node.ts} +2 -0
- package/src/RegistryPlugin.test.ts +24 -0
- package/src/RegistryPlugin.tsx +9 -3
- package/src/capabilities/app-graph-builder.ts +61 -17
- package/src/capabilities/index.ts +2 -1
- package/src/capabilities/operation-handler.ts +1 -1
- package/src/capabilities/react-surface.tsx +49 -24
- package/src/capabilities/state.ts +38 -0
- package/src/cli/index.ts +1 -2
- package/src/components/PluginDetail/PluginDetail.stories.tsx +2 -1
- package/src/components/PluginDetail/PluginDetail.tsx +27 -5
- package/src/components/PluginList/PluginItem.tsx +71 -8
- package/src/components/PluginList/PluginList.stories.tsx +1 -1
- package/src/components/PluginList/PluginList.tsx +8 -3
- package/src/containers/CommunityRegistry/CommunityRegistry.tsx +181 -0
- package/src/containers/CommunityRegistry/index.ts +5 -0
- package/src/containers/LoadPluginDialog/LoadPluginDialog.tsx +2 -1
- package/src/containers/PluginArticle/PluginArticle.tsx +47 -2
- package/src/containers/PluginRegistry/PluginRegistry.tsx +6 -2
- package/src/containers/index.ts +1 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/use-auto-tags.ts +52 -0
- package/src/hooks/use-community-plugins.ts +35 -0
- package/src/index.ts +5 -1
- package/src/operations/index.ts +1 -1
- package/src/operations/open-plugin-registry.ts +1 -1
- package/src/translations.ts +5 -2
- package/src/types.ts +26 -1
- package/dist/types/src/cli/commands/disable.d.ts.map +0 -1
- package/dist/types/src/cli/commands/enable.d.ts.map +0 -1
- package/dist/types/src/cli/commands/index.d.ts.map +0 -1
- package/dist/types/src/cli/commands/list.d.ts +0 -7
- package/dist/types/src/cli/commands/list.d.ts.map +0 -1
- package/dist/types/src/cli/commands/util.d.ts.map +0 -1
- package/dist/types/src/cli/plugin.d.ts.map +0 -1
- package/dist/types/src/cli/storage.d.ts.map +0 -1
- /package/dist/types/src/{cli/commands → commands}/index.d.ts +0 -0
- /package/dist/types/src/{cli/commands → commands}/util.d.ts +0 -0
- /package/dist/types/src/{cli/storage.d.ts → storage.d.ts} +0 -0
- /package/src/{cli/commands → commands}/disable.ts +0 -0
- /package/src/{cli/commands → commands}/enable.ts +0 -0
- /package/src/{cli/commands → commands}/index.ts +0 -0
- /package/src/{cli/commands → commands}/list.ts +0 -0
- /package/src/{cli/commands → commands}/util.ts +0 -0
- /package/src/{cli/storage.ts → storage.ts} +0 -0
|
@@ -2,10 +2,21 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, { type MouseEvent, useCallback } from 'react';
|
|
5
|
+
import React, { type MouseEvent, useCallback, useMemo } from 'react';
|
|
6
6
|
|
|
7
7
|
import { type Plugin } from '@dxos/app-framework';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
Button,
|
|
10
|
+
type ChromaticPalette,
|
|
11
|
+
Icon,
|
|
12
|
+
IconButton,
|
|
13
|
+
Input,
|
|
14
|
+
Link,
|
|
15
|
+
ListItem,
|
|
16
|
+
type NeutralPalette,
|
|
17
|
+
Tag,
|
|
18
|
+
useTranslation,
|
|
19
|
+
} from '@dxos/react-ui';
|
|
9
20
|
import { mx } from '@dxos/ui-theme';
|
|
10
21
|
import { getStyles } from '@dxos/ui-theme';
|
|
11
22
|
|
|
@@ -14,25 +25,55 @@ import { type RegistryTagType } from '#types';
|
|
|
14
25
|
|
|
15
26
|
export type PluginItemProps = {
|
|
16
27
|
plugin: Plugin.Plugin;
|
|
28
|
+
/** Ids of plugins currently installed (loaded into the manager). */
|
|
17
29
|
installed?: readonly string[];
|
|
30
|
+
/** Ids of plugins whose install is in flight. */
|
|
31
|
+
installing?: readonly string[];
|
|
18
32
|
enabled?: readonly string[];
|
|
33
|
+
/**
|
|
34
|
+
* Derived tags (e.g. `community`, `local`) to display alongside the plugin's own meta.tags.
|
|
35
|
+
* Not persisted to plugin meta; computed per-render by the container.
|
|
36
|
+
*/
|
|
37
|
+
extraTags?: readonly string[];
|
|
19
38
|
onClick?: (id: string) => void;
|
|
20
39
|
onChange?: (id: string, enabled: boolean) => void;
|
|
40
|
+
/**
|
|
41
|
+
* Install handler. When provided and the plugin is not installed, an Install button
|
|
42
|
+
* is rendered in place of the enable switch.
|
|
43
|
+
*/
|
|
44
|
+
onInstall?: (id: string) => void;
|
|
21
45
|
hasSettings?: (id: string) => boolean;
|
|
22
46
|
onSettings?: (id: string) => void;
|
|
23
47
|
};
|
|
24
48
|
|
|
25
49
|
export const PluginItem = ({
|
|
26
50
|
plugin,
|
|
51
|
+
installed,
|
|
52
|
+
installing,
|
|
27
53
|
enabled = [],
|
|
54
|
+
extraTags,
|
|
28
55
|
onClick,
|
|
29
56
|
onChange,
|
|
57
|
+
onInstall,
|
|
30
58
|
hasSettings: hasSettingsProp,
|
|
31
59
|
onSettings,
|
|
32
60
|
}: PluginItemProps) => {
|
|
33
61
|
const { t } = useTranslation(meta.id);
|
|
34
62
|
const { id, name, description, tags, icon = 'ph--circle--regular', iconHue = 'neutral' } = plugin.meta;
|
|
63
|
+
const displayTags = useMemo(() => {
|
|
64
|
+
if (!extraTags || extraTags.length === 0) {
|
|
65
|
+
return tags ?? [];
|
|
66
|
+
}
|
|
67
|
+
const set = new Set<string>(tags ?? []);
|
|
68
|
+
for (const tag of extraTags) {
|
|
69
|
+
set.add(tag);
|
|
70
|
+
}
|
|
71
|
+
return Array.from(set);
|
|
72
|
+
}, [tags, extraTags]);
|
|
35
73
|
const isEnabled = enabled.includes(id);
|
|
74
|
+
const isInstalled = installed ? installed.includes(id) : true;
|
|
75
|
+
const isInstalling = installing?.includes(id) ?? false;
|
|
76
|
+
const showInstallButton = !!onInstall && !isInstalled;
|
|
36
77
|
const inputId = `${id}-input`;
|
|
37
78
|
const labelId = `${id}-label`;
|
|
38
79
|
const descriptionId = `${id}-description`;
|
|
@@ -46,6 +87,14 @@ export const PluginItem = ({
|
|
|
46
87
|
[id, isEnabled, onChange],
|
|
47
88
|
);
|
|
48
89
|
|
|
90
|
+
const handleInstall = useCallback(
|
|
91
|
+
(event: MouseEvent) => {
|
|
92
|
+
event.stopPropagation();
|
|
93
|
+
onInstall?.(id);
|
|
94
|
+
},
|
|
95
|
+
[id, onInstall],
|
|
96
|
+
);
|
|
97
|
+
|
|
49
98
|
const hasSettings = hasSettingsProp?.(id) ?? false;
|
|
50
99
|
const handleSettings = useCallback(() => onSettings?.(id), [id, onSettings]);
|
|
51
100
|
const styles = getStyles(iconHue);
|
|
@@ -79,7 +128,7 @@ export const PluginItem = ({
|
|
|
79
128
|
</div>
|
|
80
129
|
|
|
81
130
|
<div className='flex -ms-0.5 overflow-x-auto scrollbar-none'>
|
|
82
|
-
{
|
|
131
|
+
{displayTags.map((tag) => (
|
|
83
132
|
<Tag key={tag} palette={tagColors[tag as RegistryTagType]} classNames='text-xs uppercase font-thin'>
|
|
84
133
|
{tag}
|
|
85
134
|
</Tag>
|
|
@@ -91,7 +140,7 @@ export const PluginItem = ({
|
|
|
91
140
|
aria-describedby={descriptionId}
|
|
92
141
|
classNames='cursor-pointer'
|
|
93
142
|
icon='ph--gear--regular'
|
|
94
|
-
label={t('settings.label')}
|
|
143
|
+
label={t('plugin-settings.label')}
|
|
95
144
|
iconOnly
|
|
96
145
|
size={4}
|
|
97
146
|
onClick={handleSettings}
|
|
@@ -104,9 +153,21 @@ export const PluginItem = ({
|
|
|
104
153
|
|
|
105
154
|
<div className='grow' />
|
|
106
155
|
<div className='pe-1'>
|
|
107
|
-
|
|
108
|
-
<
|
|
109
|
-
|
|
156
|
+
{showInstallButton ? (
|
|
157
|
+
<Button
|
|
158
|
+
aria-describedby={descriptionId}
|
|
159
|
+
density='fine'
|
|
160
|
+
variant='primary'
|
|
161
|
+
disabled={isInstalling}
|
|
162
|
+
onClick={handleInstall}
|
|
163
|
+
>
|
|
164
|
+
{isInstalling ? t('installing.label') : t('install.label')}
|
|
165
|
+
</Button>
|
|
166
|
+
) : (
|
|
167
|
+
<Input.Root id={inputId}>
|
|
168
|
+
<Input.Switch classNames='self-center' checked={isEnabled} onClick={handleChange} />
|
|
169
|
+
</Input.Root>
|
|
170
|
+
)}
|
|
110
171
|
</div>
|
|
111
172
|
</div>
|
|
112
173
|
</div>
|
|
@@ -114,11 +175,13 @@ export const PluginItem = ({
|
|
|
114
175
|
);
|
|
115
176
|
};
|
|
116
177
|
|
|
117
|
-
const tagColors: Record<RegistryTagType, ChromaticPalette> = {
|
|
178
|
+
const tagColors: Record<RegistryTagType, ChromaticPalette | NeutralPalette> = {
|
|
118
179
|
new: 'rose',
|
|
119
180
|
beta: 'teal',
|
|
120
181
|
labs: 'blue',
|
|
121
182
|
popular: 'green',
|
|
122
183
|
featured: 'pink',
|
|
123
184
|
experimental: 'amber',
|
|
185
|
+
community: 'indigo',
|
|
186
|
+
local: 'neutral',
|
|
124
187
|
};
|
|
@@ -11,9 +11,9 @@ import { ScrollArea } from '@dxos/react-ui';
|
|
|
11
11
|
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
12
12
|
import { getHashHue } from '@dxos/ui-theme';
|
|
13
13
|
|
|
14
|
+
import { translations } from '#translations';
|
|
14
15
|
import { RegistryTagType } from '#types';
|
|
15
16
|
|
|
16
|
-
import { translations } from '../../translations';
|
|
17
17
|
import { PluginList } from './PluginList';
|
|
18
18
|
|
|
19
19
|
random.seed(1);
|
|
@@ -9,15 +9,20 @@ import { List } from '@dxos/react-ui';
|
|
|
9
9
|
|
|
10
10
|
import { PluginItem, type PluginItemProps } from './PluginItem';
|
|
11
11
|
|
|
12
|
-
export type PluginListProps = Omit<PluginItemProps, 'plugin'> & {
|
|
12
|
+
export type PluginListProps = Omit<PluginItemProps, 'plugin' | 'extraTags'> & {
|
|
13
13
|
plugins?: readonly Plugin.Plugin[];
|
|
14
|
+
/**
|
|
15
|
+
* Map from plugin id → extra tags to display (e.g. `community`, `local`).
|
|
16
|
+
* Computed by the container; not persisted to plugin meta.
|
|
17
|
+
*/
|
|
18
|
+
extraTagsById?: Record<string, readonly string[]>;
|
|
14
19
|
};
|
|
15
20
|
|
|
16
|
-
export const PluginList = ({ plugins = [], ...props }: PluginListProps) => {
|
|
21
|
+
export const PluginList = ({ plugins = [], extraTagsById, ...props }: PluginListProps) => {
|
|
17
22
|
return (
|
|
18
23
|
<List classNames='grid grid-cols-[repeat(auto-fill,minmax(18rem,1fr))] auto-rows-[max-content] gap-4 p-4'>
|
|
19
24
|
{plugins.map((plugin) => (
|
|
20
|
-
<PluginItem key={plugin.meta.id} plugin={plugin} {...props} />
|
|
25
|
+
<PluginItem key={plugin.meta.id} plugin={plugin} extraTags={extraTagsById?.[plugin.meta.id]} {...props} />
|
|
21
26
|
))}
|
|
22
27
|
</List>
|
|
23
28
|
);
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useAtomValue } from '@effect-atom/atom-react';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
8
|
+
|
|
9
|
+
import { Plugin, UrlLoader } from '@dxos/app-framework';
|
|
10
|
+
import { useCapabilities, useOperationInvoker, usePluginManager } from '@dxos/app-framework/ui';
|
|
11
|
+
import { AppCapabilities, LayoutOperation, SettingsOperation } from '@dxos/app-toolkit';
|
|
12
|
+
import { runAndForwardErrors } from '@dxos/effect';
|
|
13
|
+
import { ObservabilityOperation } from '@dxos/plugin-observability/operations';
|
|
14
|
+
import { type PluginEntry } from '@dxos/protocols';
|
|
15
|
+
import { ScrollArea, useTranslation } from '@dxos/react-ui';
|
|
16
|
+
import { composable, composableProps } from '@dxos/ui-theme';
|
|
17
|
+
|
|
18
|
+
import { PluginList } from '#components';
|
|
19
|
+
import { getPluginPath, meta } from '#meta';
|
|
20
|
+
|
|
21
|
+
import { useAutoTags, useCommunityPlugins } from '../../hooks';
|
|
22
|
+
|
|
23
|
+
const sortEntries = (a: PluginEntry, b: PluginEntry) =>
|
|
24
|
+
(a.meta.name ?? a.meta.id).localeCompare(b.meta.name ?? b.meta.id);
|
|
25
|
+
|
|
26
|
+
const sortPlugins = (a: Plugin.Plugin, b: Plugin.Plugin) =>
|
|
27
|
+
(a.meta.name ?? a.meta.id).localeCompare(b.meta.name ?? b.meta.id);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Turns a community manifest entry into a minimal Plugin object so we can reuse
|
|
31
|
+
* {@link PluginList} for rendering. The synthesized plugin has no modules — it
|
|
32
|
+
* exists only for display until the user installs it.
|
|
33
|
+
*/
|
|
34
|
+
const toDisplayPlugin = (entry: PluginEntry): Plugin.Plugin =>
|
|
35
|
+
({
|
|
36
|
+
[Plugin.PluginTypeId]: Plugin.PluginTypeId,
|
|
37
|
+
meta: entry.meta,
|
|
38
|
+
modules: [],
|
|
39
|
+
}) as Plugin.Plugin;
|
|
40
|
+
|
|
41
|
+
export type CommunityRegistryProps = {
|
|
42
|
+
id: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const CommunityRegistry = composable<HTMLDivElement, CommunityRegistryProps>(
|
|
46
|
+
({ id, ...props }, forwardedRef) => {
|
|
47
|
+
const { t } = useTranslation(meta.id);
|
|
48
|
+
const manager = usePluginManager();
|
|
49
|
+
const { invoke, invokePromise } = useOperationInvoker();
|
|
50
|
+
const { entries, loading, error } = useCommunityPlugins();
|
|
51
|
+
const enabled = useAtomValue(manager.enabled);
|
|
52
|
+
const plugins = useAtomValue(manager.plugins);
|
|
53
|
+
const installedIds = useMemo(() => plugins.map((plugin) => plugin.meta.id), [plugins]);
|
|
54
|
+
const allSettings = useCapabilities(AppCapabilities.Settings);
|
|
55
|
+
const extraTagsById = useAutoTags(entries);
|
|
56
|
+
|
|
57
|
+
// Snapshot of installed plugin ids at mount time. Used to sort installed
|
|
58
|
+
// plugins to the top without having newly-installed rows jump up mid-session.
|
|
59
|
+
const [installedSnapshot] = useState<ReadonlySet<string>>(
|
|
60
|
+
() => new Set(manager.getPlugins().map((plugin) => plugin.meta.id)),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const [installingIds, setInstallingIds] = useState<readonly string[]>([]);
|
|
64
|
+
|
|
65
|
+
const sortedEntries = useMemo(() => [...entries].sort(sortEntries), [entries]);
|
|
66
|
+
const moduleUrlById = useMemo(() => {
|
|
67
|
+
const map: Record<string, string> = {};
|
|
68
|
+
for (const entry of sortedEntries) {
|
|
69
|
+
map[entry.meta.id] = entry.moduleUrl;
|
|
70
|
+
}
|
|
71
|
+
return map;
|
|
72
|
+
}, [sortedEntries]);
|
|
73
|
+
|
|
74
|
+
// Single list with installed-at-mount plugins sorted to the top. Sort order
|
|
75
|
+
// is based on the mount-time snapshot so rows don't jump as the user
|
|
76
|
+
// installs plugins during the session.
|
|
77
|
+
const items = useMemo(() => {
|
|
78
|
+
const catalogIds = new Set(sortedEntries.map((entry) => entry.meta.id));
|
|
79
|
+
const remoteIds = new Set(UrlLoader.getRemoteEntries().map((entry) => entry.id));
|
|
80
|
+
const fromCatalog = sortedEntries.map(toDisplayPlugin);
|
|
81
|
+
const fromUrlOnly = plugins.filter((plugin) => remoteIds.has(plugin.meta.id) && !catalogIds.has(plugin.meta.id));
|
|
82
|
+
const installedFirst = [...fromCatalog, ...fromUrlOnly].sort((a, b) => {
|
|
83
|
+
const aInstalled = installedSnapshot.has(a.meta.id);
|
|
84
|
+
const bInstalled = installedSnapshot.has(b.meta.id);
|
|
85
|
+
if (aInstalled !== bInstalled) {
|
|
86
|
+
return aInstalled ? -1 : 1;
|
|
87
|
+
}
|
|
88
|
+
return sortPlugins(a, b);
|
|
89
|
+
});
|
|
90
|
+
return installedFirst;
|
|
91
|
+
}, [sortedEntries, installedSnapshot, plugins]);
|
|
92
|
+
|
|
93
|
+
const handleChange = useCallback(
|
|
94
|
+
(pluginId: string, nextEnabled: boolean) =>
|
|
95
|
+
Effect.gen(function* () {
|
|
96
|
+
if (nextEnabled) {
|
|
97
|
+
yield* manager.enable(pluginId);
|
|
98
|
+
} else {
|
|
99
|
+
yield* manager.disable(pluginId);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
yield* invoke(ObservabilityOperation.SendEvent, {
|
|
103
|
+
name: 'plugins.toggle',
|
|
104
|
+
properties: { plugin: pluginId, enabled: nextEnabled, source: 'community' },
|
|
105
|
+
});
|
|
106
|
+
}).pipe(runAndForwardErrors),
|
|
107
|
+
[invoke, manager],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const handleInstall = useCallback(
|
|
111
|
+
(pluginId: string) => {
|
|
112
|
+
const moduleUrl = moduleUrlById[pluginId];
|
|
113
|
+
if (!moduleUrl) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
setInstallingIds((prev) => (prev.includes(pluginId) ? prev : [...prev, pluginId]));
|
|
117
|
+
void void Effect.gen(function* () {
|
|
118
|
+
const plugin = yield* manager.add(moduleUrl);
|
|
119
|
+
yield* manager.enable(plugin.meta.id);
|
|
120
|
+
yield* invoke(ObservabilityOperation.SendEvent, {
|
|
121
|
+
name: 'plugins.install',
|
|
122
|
+
properties: { plugin: plugin.meta.id, source: 'community' },
|
|
123
|
+
});
|
|
124
|
+
}).pipe(
|
|
125
|
+
Effect.ensuring(Effect.sync(() => setInstallingIds((prev) => prev.filter((pid) => pid !== pluginId)))),
|
|
126
|
+
runAndForwardErrors,
|
|
127
|
+
);
|
|
128
|
+
},
|
|
129
|
+
[invoke, manager, moduleUrlById],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const handleClick = useCallback(
|
|
133
|
+
(pluginId: string) =>
|
|
134
|
+
invokePromise(LayoutOperation.Open, {
|
|
135
|
+
subject: [getPluginPath(pluginId)],
|
|
136
|
+
pivotId: id,
|
|
137
|
+
positioning: 'end',
|
|
138
|
+
}),
|
|
139
|
+
[invokePromise, id],
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const hasSettings = useCallback(
|
|
143
|
+
(pluginId: string) => allSettings.some((setting) => setting.prefix === pluginId),
|
|
144
|
+
[allSettings],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const handleSettings = useCallback(
|
|
148
|
+
(pluginId: string) => invokePromise(SettingsOperation.Open, { plugin: pluginId }),
|
|
149
|
+
[invokePromise],
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<ScrollArea.Root {...composableProps(props)} orientation='vertical' ref={forwardedRef}>
|
|
154
|
+
<ScrollArea.Viewport>
|
|
155
|
+
{items.length > 0 ? (
|
|
156
|
+
<PluginList
|
|
157
|
+
plugins={items}
|
|
158
|
+
installed={installedIds}
|
|
159
|
+
installing={installingIds}
|
|
160
|
+
enabled={enabled}
|
|
161
|
+
extraTagsById={extraTagsById}
|
|
162
|
+
onClick={handleClick}
|
|
163
|
+
onChange={handleChange}
|
|
164
|
+
onInstall={handleInstall}
|
|
165
|
+
hasSettings={hasSettings}
|
|
166
|
+
onSettings={handleSettings}
|
|
167
|
+
/>
|
|
168
|
+
) : error ? (
|
|
169
|
+
<div className='p-4 text-description'>Failed to load community plugins: {error.message}</div>
|
|
170
|
+
) : loading ? (
|
|
171
|
+
<div className='p-4 text-description'>{t('loading.label')}</div>
|
|
172
|
+
) : (
|
|
173
|
+
<div className='p-4 text-description'>No community plugins available.</div>
|
|
174
|
+
)}
|
|
175
|
+
</ScrollArea.Viewport>
|
|
176
|
+
</ScrollArea.Root>
|
|
177
|
+
);
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
CommunityRegistry.displayName = 'CommunityRegistry';
|
|
@@ -30,7 +30,8 @@ export const LoadPluginDialog = () => {
|
|
|
30
30
|
setError(null);
|
|
31
31
|
|
|
32
32
|
void Effect.gen(function* () {
|
|
33
|
-
yield* manager.add(url.trim());
|
|
33
|
+
const plugin = yield* manager.add(url.trim());
|
|
34
|
+
yield* manager.enable(plugin.meta.id);
|
|
34
35
|
closeRef.current?.click();
|
|
35
36
|
}).pipe(
|
|
36
37
|
Effect.catchAll((err) =>
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// Copyright 2025 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import { useAtomValue } from '@effect-atom/atom-react';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
6
8
|
|
|
7
9
|
import { type Plugin } from '@dxos/app-framework';
|
|
8
10
|
import { usePluginManager } from '@dxos/app-framework/ui';
|
|
@@ -10,12 +12,31 @@ import { runAndForwardErrors } from '@dxos/effect';
|
|
|
10
12
|
|
|
11
13
|
import { PluginDetail } from '#components';
|
|
12
14
|
|
|
15
|
+
import { useCommunityPlugins, useRemotePluginIds } from '../../hooks';
|
|
16
|
+
|
|
13
17
|
// TODO(burdon): Convert to ECHO type.
|
|
14
18
|
export type PluginArticleProps = { subject: Plugin.Plugin };
|
|
15
19
|
|
|
16
20
|
export const PluginArticle = ({ subject: plugin }: PluginArticleProps) => {
|
|
17
21
|
const manager = usePluginManager();
|
|
22
|
+
const plugins = useAtomValue(manager.plugins);
|
|
23
|
+
const { entries } = useCommunityPlugins();
|
|
24
|
+
const remotePluginIds = useRemotePluginIds();
|
|
25
|
+
const [installing, setInstalling] = useState(false);
|
|
26
|
+
|
|
18
27
|
const enabled = manager.getEnabled().includes(plugin.meta.id);
|
|
28
|
+
const isInstalled = useMemo(
|
|
29
|
+
() => plugins.some((candidate) => candidate.meta.id === plugin.meta.id),
|
|
30
|
+
[plugins, plugin.meta.id],
|
|
31
|
+
);
|
|
32
|
+
const isCore = manager.getCore().includes(plugin.meta.id);
|
|
33
|
+
const canUninstall = isInstalled && !isCore && remotePluginIds.has(plugin.meta.id);
|
|
34
|
+
|
|
35
|
+
const moduleUrl = useMemo(
|
|
36
|
+
() => entries.find((entry) => entry.meta.id === plugin.meta.id)?.moduleUrl,
|
|
37
|
+
[entries, plugin.meta.id],
|
|
38
|
+
);
|
|
39
|
+
|
|
19
40
|
const handleEnableChange = useCallback(
|
|
20
41
|
(enabled: boolean) =>
|
|
21
42
|
enabled
|
|
@@ -24,5 +45,29 @@ export const PluginArticle = ({ subject: plugin }: PluginArticleProps) => {
|
|
|
24
45
|
[manager, plugin.meta.id],
|
|
25
46
|
);
|
|
26
47
|
|
|
27
|
-
|
|
48
|
+
const handleUninstall = useCallback(() => {
|
|
49
|
+
void runAndForwardErrors(manager.remove(plugin.meta.id));
|
|
50
|
+
}, [manager, plugin.meta.id]);
|
|
51
|
+
|
|
52
|
+
const handleInstall = useCallback(() => {
|
|
53
|
+
if (!moduleUrl) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
setInstalling(true);
|
|
57
|
+
void Effect.gen(function* () {
|
|
58
|
+
const added = yield* manager.add(moduleUrl);
|
|
59
|
+
yield* manager.enable(added.meta.id);
|
|
60
|
+
}).pipe(Effect.ensuring(Effect.sync(() => setInstalling(false))), runAndForwardErrors);
|
|
61
|
+
}, [manager, moduleUrl]);
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<PluginDetail
|
|
65
|
+
plugin={plugin}
|
|
66
|
+
enabled={enabled}
|
|
67
|
+
onEnabledChange={handleEnableChange}
|
|
68
|
+
onInstall={!isInstalled && moduleUrl ? handleInstall : undefined}
|
|
69
|
+
installing={installing}
|
|
70
|
+
onUninstall={canUninstall ? handleUninstall : undefined}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
28
73
|
};
|
|
@@ -23,17 +23,20 @@ const sortByPluginMeta = ({ meta: { name: a = '' } }: Plugin.Plugin, { meta: { n
|
|
|
23
23
|
export type PluginRegistryProps = {
|
|
24
24
|
id: string;
|
|
25
25
|
plugins: Plugin.Plugin[];
|
|
26
|
+
/**
|
|
27
|
+
* Map from plugin id → display-only tags (e.g. `community`, `local`) computed by the caller.
|
|
28
|
+
*/
|
|
29
|
+
extraTagsById?: Record<string, readonly string[]>;
|
|
26
30
|
};
|
|
27
31
|
|
|
28
32
|
export const PluginRegistry = composable<HTMLDivElement, PluginRegistryProps>(
|
|
29
|
-
({ id, plugins: pluginsProp, ...props }, forwardedRef) => {
|
|
33
|
+
({ id, plugins: pluginsProp, extraTagsById, ...props }, forwardedRef) => {
|
|
30
34
|
const manager = usePluginManager();
|
|
31
35
|
const { invoke, invokePromise } = useOperationInvoker();
|
|
32
36
|
const plugins = useMemo(() => pluginsProp.sort(sortByPluginMeta), [pluginsProp]);
|
|
33
37
|
const enabled = useAtomValue(manager.enabled);
|
|
34
38
|
const allSettings = useCapabilities(AppCapabilities.Settings);
|
|
35
39
|
|
|
36
|
-
// TODO(wittjosiah): Factor out to an intent?
|
|
37
40
|
const handleChange = useCallback(
|
|
38
41
|
(id: string, enabled: boolean) =>
|
|
39
42
|
Effect.gen(function* () {
|
|
@@ -80,6 +83,7 @@ export const PluginRegistry = composable<HTMLDivElement, PluginRegistryProps>(
|
|
|
80
83
|
<PluginList
|
|
81
84
|
plugins={plugins}
|
|
82
85
|
enabled={enabled}
|
|
86
|
+
extraTagsById={extraTagsById}
|
|
83
87
|
onClick={handleClick}
|
|
84
88
|
onChange={handleChange}
|
|
85
89
|
hasSettings={hasSettings}
|
package/src/containers/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { type ComponentType, lazy } from 'react';
|
|
6
6
|
|
|
7
7
|
export { LOAD_PLUGIN_DIALOG } from './LoadPluginDialog/LoadPluginDialog';
|
|
8
|
+
export const CommunityRegistry: ComponentType<any> = lazy(() => import('./CommunityRegistry'));
|
|
8
9
|
export const LoadPluginDialog: ComponentType<any> = lazy(() => import('./LoadPluginDialog'));
|
|
9
10
|
export const PluginArticle: ComponentType<any> = lazy(() => import('./PluginArticle'));
|
|
10
11
|
export const PluginRegistry: ComponentType<any> = lazy(() => import('./PluginRegistry'));
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useMemo } from 'react';
|
|
6
|
+
|
|
7
|
+
import { UrlLoader } from '@dxos/app-framework';
|
|
8
|
+
import { type PluginEntry } from '@dxos/protocols';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Auto-tags that are derived at display time rather than persisted to `Plugin.Meta`.
|
|
12
|
+
* - `community`: plugin id appears in the community registry manifest.
|
|
13
|
+
* - `local`: plugin was loaded from a `localhost` / `127.0.0.1` / `::1` URL.
|
|
14
|
+
*/
|
|
15
|
+
export type AutoTagsMap = Record<string, readonly string[]>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Computes the per-plugin-id extra tags the UI should display.
|
|
19
|
+
*
|
|
20
|
+
* Reads the persisted remote-plugin entries synchronously to derive `local`, and
|
|
21
|
+
* adds `community` for any plugin id found in the supplied community manifest entries.
|
|
22
|
+
*/
|
|
23
|
+
export const useAutoTags = (communityEntries: readonly PluginEntry[]): AutoTagsMap =>
|
|
24
|
+
useMemo(() => {
|
|
25
|
+
const byId: Record<string, string[]> = {};
|
|
26
|
+
const addTag = (id: string, tag: string) => {
|
|
27
|
+
const existing = byId[id];
|
|
28
|
+
if (!existing) {
|
|
29
|
+
byId[id] = [tag];
|
|
30
|
+
} else if (!existing.includes(tag)) {
|
|
31
|
+
existing.push(tag);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
for (const entry of UrlLoader.getRemoteEntries()) {
|
|
36
|
+
if (UrlLoader.isLocalUrl(entry.url)) {
|
|
37
|
+
addTag(entry.id, 'local');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
for (const entry of communityEntries) {
|
|
41
|
+
addTag(entry.meta.id, 'community');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return byId;
|
|
45
|
+
}, [communityEntries]);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns the set of plugin ids known to originate from a remote URL (not bundled).
|
|
49
|
+
* Used to filter the Official/Recommended sections so third-party plugins are excluded.
|
|
50
|
+
*/
|
|
51
|
+
export const useRemotePluginIds = (): ReadonlySet<string> =>
|
|
52
|
+
useMemo(() => new Set(UrlLoader.getRemoteEntries().map((entry) => entry.id)), []);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Atom, useAtomValue } from '@effect-atom/atom-react';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
|
|
8
|
+
import { useCapabilities } from '@dxos/app-framework/ui';
|
|
9
|
+
import { Context } from '@dxos/context';
|
|
10
|
+
import { type EdgeHttpClient } from '@dxos/edge-client';
|
|
11
|
+
import { type PluginEntry } from '@dxos/protocols';
|
|
12
|
+
|
|
13
|
+
import { RegistryCapabilities, type CommunityPluginsState } from '#types';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Fetches the community plugin catalog from the Edge registry service and
|
|
17
|
+
* filters out unhealthy entries.
|
|
18
|
+
*/
|
|
19
|
+
export const loadCommunityPlugins = (client: EdgeHttpClient): Effect.Effect<readonly PluginEntry[], Error> =>
|
|
20
|
+
Effect.tryPromise({
|
|
21
|
+
try: () => client.getRegistryPlugins(Context.default()),
|
|
22
|
+
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
|
|
23
|
+
}).pipe(Effect.map((body) => body.plugins.filter((entry) => entry.health === 'ok')));
|
|
24
|
+
|
|
25
|
+
const emptyAtom = Atom.make<CommunityPluginsState>({ entries: [], loading: true, error: null });
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Reads the community plugin catalog from the registry state capability. The
|
|
29
|
+
* catalog is loaded once at plugin startup so this hook is purely reactive.
|
|
30
|
+
* Returns the empty/loading state until the state capability activates.
|
|
31
|
+
*/
|
|
32
|
+
export const useCommunityPlugins = (): CommunityPluginsState => {
|
|
33
|
+
const atoms = useCapabilities(RegistryCapabilities.State);
|
|
34
|
+
return useAtomValue(atoms[0] ?? emptyAtom);
|
|
35
|
+
};
|
package/src/index.ts
CHANGED
package/src/operations/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import * as Effect from 'effect/Effect';
|
|
6
6
|
|
|
7
7
|
import { LayoutOperation, SettingsOperation, getSpacePath } from '@dxos/app-toolkit';
|
|
8
|
-
import { Operation } from '@dxos/
|
|
8
|
+
import { Operation } from '@dxos/compute';
|
|
9
9
|
|
|
10
10
|
import { REGISTRY_ID } from '#meta';
|
|
11
11
|
|
package/src/translations.ts
CHANGED
|
@@ -15,7 +15,8 @@ export const translations = [
|
|
|
15
15
|
'home-page.label': 'Website',
|
|
16
16
|
'source.label': 'Source',
|
|
17
17
|
'plugin-registry.label': 'Plugin Registry',
|
|
18
|
-
'
|
|
18
|
+
'plugin-settings.label': 'Plugin Settings',
|
|
19
|
+
'official-plugins.label': 'Official',
|
|
19
20
|
'installed-plugins.label': 'Enabled',
|
|
20
21
|
'recommended-plugins.label': 'Recommended',
|
|
21
22
|
'labs-plugins.label': 'Labs',
|
|
@@ -27,7 +28,9 @@ export const translations = [
|
|
|
27
28
|
'loading.label': 'Loading...',
|
|
28
29
|
'coming-soon.label': 'Coming soon!',
|
|
29
30
|
'open-plugin-registry.label': 'Open plugin registry',
|
|
30
|
-
'
|
|
31
|
+
'install.label': 'Install',
|
|
32
|
+
'installing.label': 'Installing…',
|
|
33
|
+
'uninstall.label': 'Uninstall',
|
|
31
34
|
},
|
|
32
35
|
},
|
|
33
36
|
},
|