@dxos/plugin-registry 0.8.4-main.f9ba587 → 0.8.4-main.fcfe5033a5
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 +219 -0
- package/dist/lib/browser/cli/index.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +52 -52
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/cli/index.mjs +221 -0
- package/dist/lib/node-esm/cli/index.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +53 -52
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/RegistryPlugin.d.ts +2 -1
- package/dist/types/src/RegistryPlugin.d.ts.map +1 -1
- package/dist/types/src/capabilities/app-graph-builder.d.ts +4 -2
- package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
- package/dist/types/src/capabilities/index.d.ts +5 -3
- package/dist/types/src/capabilities/index.d.ts.map +1 -1
- package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
- package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
- package/dist/types/src/capabilities/react-surface.d.ts +3 -2
- package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
- package/dist/types/src/cli/commands/disable.d.ts +11 -0
- package/dist/types/src/cli/commands/disable.d.ts.map +1 -0
- package/dist/types/src/cli/commands/enable.d.ts +11 -0
- package/dist/types/src/cli/commands/enable.d.ts.map +1 -0
- package/dist/types/src/cli/commands/index.d.ts +3 -0
- package/dist/types/src/cli/commands/index.d.ts.map +1 -0
- package/dist/types/src/cli/commands/list.d.ts +7 -0
- package/dist/types/src/cli/commands/list.d.ts.map +1 -0
- package/dist/types/src/cli/commands/util.d.ts +11 -0
- package/dist/types/src/cli/commands/util.d.ts.map +1 -0
- package/dist/types/src/cli/index.d.ts +3 -0
- package/dist/types/src/cli/index.d.ts.map +1 -0
- package/dist/types/src/cli/plugin.d.ts +3 -0
- package/dist/types/src/cli/plugin.d.ts.map +1 -0
- package/dist/types/src/cli/storage.d.ts +18 -0
- package/dist/types/src/cli/storage.d.ts.map +1 -0
- package/dist/types/src/components/PluginDetail/PluginDetail.d.ts +11 -0
- package/dist/types/src/components/PluginDetail/PluginDetail.d.ts.map +1 -0
- package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts +39 -0
- package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts.map +1 -0
- package/dist/types/src/components/PluginDetail/index.d.ts +2 -0
- package/dist/types/src/components/PluginDetail/index.d.ts.map +1 -0
- package/dist/types/src/components/PluginList/PluginItem.d.ts +18 -0
- package/dist/types/src/components/PluginList/PluginItem.d.ts.map +1 -0
- package/dist/types/src/components/PluginList/PluginList.d.ts +13 -0
- package/dist/types/src/components/PluginList/PluginList.d.ts.map +1 -0
- package/dist/types/src/components/PluginList/PluginList.stories.d.ts +38 -0
- package/dist/types/src/components/PluginList/PluginList.stories.d.ts.map +1 -0
- package/dist/types/src/components/PluginList/index.d.ts +2 -0
- package/dist/types/src/components/PluginList/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +0 -1
- package/dist/types/src/components/index.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 +4 -0
- package/dist/types/src/containers/LoadPluginDialog/LoadPluginDialog.d.ts.map +1 -0
- package/dist/types/src/containers/LoadPluginDialog/index.d.ts +2 -0
- package/dist/types/src/containers/LoadPluginDialog/index.d.ts.map +1 -0
- package/dist/types/src/containers/PluginArticle/PluginArticle.d.ts +7 -0
- package/dist/types/src/containers/PluginArticle/PluginArticle.d.ts.map +1 -0
- package/dist/types/src/containers/PluginArticle/index.d.ts +2 -0
- package/dist/types/src/containers/PluginArticle/index.d.ts.map +1 -0
- package/dist/types/src/containers/PluginRegistry/PluginRegistry.d.ts +14 -0
- package/dist/types/src/containers/PluginRegistry/PluginRegistry.d.ts.map +1 -0
- package/dist/types/src/containers/PluginRegistry/index.d.ts +2 -0
- package/dist/types/src/containers/PluginRegistry/index.d.ts.map +1 -0
- package/dist/types/src/containers/index.d.ts +7 -0
- package/dist/types/src/containers/index.d.ts.map +1 -0
- 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 +15 -0
- package/dist/types/src/hooks/use-community-plugins.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +7 -4
- package/dist/types/src/meta.d.ts.map +1 -1
- package/dist/types/src/operations/index.d.ts +3 -0
- package/dist/types/src/operations/index.d.ts.map +1 -0
- package/dist/types/src/operations/open-plugin-registry.d.ts +5 -0
- package/dist/types/src/operations/open-plugin-registry.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +19 -15
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +3 -1
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +64 -35
- package/src/RegistryPlugin.tsx +12 -26
- package/src/capabilities/app-graph-builder.ts +138 -136
- package/src/capabilities/index.ts +8 -4
- package/src/capabilities/operation-handler.ts +16 -0
- package/src/capabilities/react-surface.tsx +108 -78
- package/src/cli/commands/disable.ts +47 -0
- package/src/cli/commands/enable.ts +44 -0
- package/src/cli/commands/index.ts +14 -0
- package/src/cli/commands/list.ts +41 -0
- package/src/cli/commands/util.ts +24 -0
- package/src/cli/index.ts +6 -0
- package/src/cli/plugin.ts +17 -0
- package/src/cli/storage.ts +50 -0
- package/src/components/PluginDetail/PluginDetail.stories.tsx +43 -0
- package/src/components/PluginDetail/PluginDetail.tsx +84 -0
- package/src/components/PluginDetail/index.ts +5 -0
- package/src/components/PluginList/PluginItem.tsx +152 -0
- package/src/components/PluginList/PluginList.stories.tsx +87 -0
- package/src/components/PluginList/PluginList.tsx +29 -0
- package/src/components/PluginList/index.ts +5 -0
- package/src/components/index.ts +0 -1
- package/src/containers/CommunityRegistry/CommunityRegistry.tsx +147 -0
- package/src/containers/CommunityRegistry/index.ts +5 -0
- package/src/containers/LoadPluginDialog/LoadPluginDialog.tsx +85 -0
- package/src/containers/LoadPluginDialog/index.ts +5 -0
- package/src/containers/PluginArticle/PluginArticle.tsx +28 -0
- package/src/containers/PluginArticle/index.ts +5 -0
- package/src/containers/PluginRegistry/PluginRegistry.tsx +98 -0
- package/src/containers/PluginRegistry/index.ts +5 -0
- package/src/containers/index.ts +11 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/use-auto-tags.ts +52 -0
- package/src/hooks/use-community-plugins.ts +77 -0
- package/src/meta.ts +19 -9
- package/src/operations/index.ts +7 -0
- package/src/operations/open-plugin-registry.ts +21 -0
- package/src/translations.ts +19 -15
- package/src/types.ts +14 -1
- package/dist/lib/browser/app-graph-builder-LKQBQDUA.mjs +0 -161
- package/dist/lib/browser/app-graph-builder-LKQBQDUA.mjs.map +0 -7
- package/dist/lib/browser/chunk-AEBASNOI.mjs +0 -16
- package/dist/lib/browser/chunk-AEBASNOI.mjs.map +0 -7
- package/dist/lib/browser/intent-resolver-W67CIJI3.mjs +0 -23
- package/dist/lib/browser/intent-resolver-W67CIJI3.mjs.map +0 -7
- package/dist/lib/browser/react-surface-SRFVJQKE.mjs +0 -335
- package/dist/lib/browser/react-surface-SRFVJQKE.mjs.map +0 -7
- package/dist/lib/node-esm/app-graph-builder-N44OGVQA.mjs +0 -162
- package/dist/lib/node-esm/app-graph-builder-N44OGVQA.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-R3GXMWPJ.mjs +0 -18
- package/dist/lib/node-esm/chunk-R3GXMWPJ.mjs.map +0 -7
- package/dist/lib/node-esm/intent-resolver-M2CBVUVQ.mjs +0 -24
- package/dist/lib/node-esm/intent-resolver-M2CBVUVQ.mjs.map +0 -7
- package/dist/lib/node-esm/react-surface-5XAG2EYX.mjs +0 -336
- package/dist/lib/node-esm/react-surface-5XAG2EYX.mjs.map +0 -7
- package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
- package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
- package/dist/types/src/components/Icon.stories.d.ts +0 -6
- package/dist/types/src/components/Icon.stories.d.ts.map +0 -1
- package/dist/types/src/components/PluginDetail.d.ts +0 -9
- package/dist/types/src/components/PluginDetail.d.ts.map +0 -1
- package/dist/types/src/components/PluginDetail.stories.d.ts +0 -8
- package/dist/types/src/components/PluginDetail.stories.d.ts.map +0 -1
- package/dist/types/src/components/PluginItem.d.ts +0 -13
- package/dist/types/src/components/PluginItem.d.ts.map +0 -1
- package/dist/types/src/components/PluginList.d.ts +0 -8
- package/dist/types/src/components/PluginList.d.ts.map +0 -1
- package/dist/types/src/components/PluginList.stories.d.ts +0 -11
- package/dist/types/src/components/PluginList.stories.d.ts.map +0 -1
- package/dist/types/src/components/RegistryContainer.d.ts +0 -7
- package/dist/types/src/components/RegistryContainer.d.ts.map +0 -1
- package/src/capabilities/intent-resolver.ts +0 -27
- package/src/components/Icon.stories.tsx +0 -107
- package/src/components/PluginDetail.stories.tsx +0 -45
- package/src/components/PluginDetail.tsx +0 -63
- package/src/components/PluginItem.tsx +0 -119
- package/src/components/PluginList.stories.tsx +0 -82
- package/src/components/PluginList.tsx +0 -26
- package/src/components/RegistryContainer.tsx +0 -84
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type MouseEvent, useCallback, useMemo } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type Plugin } from '@dxos/app-framework';
|
|
8
|
+
import {
|
|
9
|
+
type ChromaticPalette,
|
|
10
|
+
Icon,
|
|
11
|
+
IconButton,
|
|
12
|
+
Input,
|
|
13
|
+
Link,
|
|
14
|
+
ListItem,
|
|
15
|
+
type NeutralPalette,
|
|
16
|
+
Tag,
|
|
17
|
+
useTranslation,
|
|
18
|
+
} from '@dxos/react-ui';
|
|
19
|
+
import { mx } from '@dxos/ui-theme';
|
|
20
|
+
import { getStyles } from '@dxos/ui-theme';
|
|
21
|
+
|
|
22
|
+
import { meta } from '#meta';
|
|
23
|
+
import { type RegistryTagType } from '#types';
|
|
24
|
+
|
|
25
|
+
export type PluginItemProps = {
|
|
26
|
+
plugin: Plugin.Plugin;
|
|
27
|
+
installed?: readonly string[];
|
|
28
|
+
enabled?: readonly string[];
|
|
29
|
+
/**
|
|
30
|
+
* Derived tags (e.g. `community`, `local`) to display alongside the plugin's own meta.tags.
|
|
31
|
+
* Not persisted to plugin meta; computed per-render by the container.
|
|
32
|
+
*/
|
|
33
|
+
extraTags?: readonly string[];
|
|
34
|
+
onClick?: (id: string) => void;
|
|
35
|
+
onChange?: (id: string, enabled: boolean) => void;
|
|
36
|
+
hasSettings?: (id: string) => boolean;
|
|
37
|
+
onSettings?: (id: string) => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const PluginItem = ({
|
|
41
|
+
plugin,
|
|
42
|
+
enabled = [],
|
|
43
|
+
extraTags,
|
|
44
|
+
onClick,
|
|
45
|
+
onChange,
|
|
46
|
+
hasSettings: hasSettingsProp,
|
|
47
|
+
onSettings,
|
|
48
|
+
}: PluginItemProps) => {
|
|
49
|
+
const { t } = useTranslation(meta.id);
|
|
50
|
+
const { id, name, description, tags, icon = 'ph--circle--regular', iconHue = 'neutral' } = plugin.meta;
|
|
51
|
+
const displayTags = useMemo(() => {
|
|
52
|
+
if (!extraTags || extraTags.length === 0) {
|
|
53
|
+
return tags ?? [];
|
|
54
|
+
}
|
|
55
|
+
const set = new Set<string>(tags ?? []);
|
|
56
|
+
for (const tag of extraTags) {
|
|
57
|
+
set.add(tag);
|
|
58
|
+
}
|
|
59
|
+
return Array.from(set);
|
|
60
|
+
}, [tags, extraTags]);
|
|
61
|
+
const isEnabled = enabled.includes(id);
|
|
62
|
+
const inputId = `${id}-input`;
|
|
63
|
+
const labelId = `${id}-label`;
|
|
64
|
+
const descriptionId = `${id}-description`;
|
|
65
|
+
const handleClick = useCallback(() => onClick?.(id), [id, onClick]);
|
|
66
|
+
|
|
67
|
+
const handleChange = useCallback(
|
|
68
|
+
(event: MouseEvent) => {
|
|
69
|
+
event.stopPropagation();
|
|
70
|
+
onChange?.(id, !isEnabled);
|
|
71
|
+
},
|
|
72
|
+
[id, isEnabled, onChange],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const hasSettings = hasSettingsProp?.(id) ?? false;
|
|
76
|
+
const handleSettings = useCallback(() => onSettings?.(id), [id, onSettings]);
|
|
77
|
+
const styles = getStyles(iconHue);
|
|
78
|
+
const gridCols = 'grid grid-cols-[5rem_1fr]';
|
|
79
|
+
const gridRows = 'grid grid-rows-[40px_1fr_min-content_40px]';
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<ListItem.Root
|
|
83
|
+
key={id}
|
|
84
|
+
labelId={labelId}
|
|
85
|
+
data-testid={`pluginList.${id}`}
|
|
86
|
+
aria-describedby={descriptionId}
|
|
87
|
+
classNames={mx(gridCols, 'h-[14rem] w-full gap-3 pe-2 bg-modal-surface rounded-md overflow-hidden')}
|
|
88
|
+
>
|
|
89
|
+
<div className={mx(gridRows, 'justify-center rounded-l-md', styles.surface)}>
|
|
90
|
+
<Icon
|
|
91
|
+
classNames={mx('row-start-2 cursor-pointer', styles.surfaceText)}
|
|
92
|
+
icon={icon}
|
|
93
|
+
size={14}
|
|
94
|
+
onClick={handleClick}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div className={mx(gridRows)}>
|
|
99
|
+
<div className='flex items-center overflow-hidden cursor-pointer' onClick={handleClick}>
|
|
100
|
+
<span className='text-lg truncate'>{name ?? id}</span>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<div>
|
|
104
|
+
<p className={mx('text-description', 'line-clamp-4 min-w-0')}>{description}</p>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div className='flex -ms-0.5 overflow-x-auto scrollbar-none'>
|
|
108
|
+
{displayTags.map((tag) => (
|
|
109
|
+
<Tag key={tag} palette={tagColors[tag as RegistryTagType]} classNames='text-xs uppercase font-thin'>
|
|
110
|
+
{tag}
|
|
111
|
+
</Tag>
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div className='flex gap-2 items-center text-sm'>
|
|
116
|
+
<IconButton
|
|
117
|
+
aria-describedby={descriptionId}
|
|
118
|
+
classNames='cursor-pointer'
|
|
119
|
+
icon='ph--gear--regular'
|
|
120
|
+
label={t('settings.label')}
|
|
121
|
+
iconOnly
|
|
122
|
+
size={4}
|
|
123
|
+
onClick={handleSettings}
|
|
124
|
+
disabled={!hasSettings}
|
|
125
|
+
/>
|
|
126
|
+
|
|
127
|
+
<Link aria-describedby={descriptionId} classNames='text-description cursor-pointer' onClick={handleClick}>
|
|
128
|
+
{t('details.label')}
|
|
129
|
+
</Link>
|
|
130
|
+
|
|
131
|
+
<div className='grow' />
|
|
132
|
+
<div className='pe-1'>
|
|
133
|
+
<Input.Root id={inputId}>
|
|
134
|
+
<Input.Switch classNames='self-center' checked={isEnabled} onClick={handleChange} />
|
|
135
|
+
</Input.Root>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</ListItem.Root>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const tagColors: Record<RegistryTagType, ChromaticPalette | NeutralPalette> = {
|
|
144
|
+
new: 'rose',
|
|
145
|
+
beta: 'teal',
|
|
146
|
+
labs: 'blue',
|
|
147
|
+
popular: 'green',
|
|
148
|
+
featured: 'pink',
|
|
149
|
+
experimental: 'amber',
|
|
150
|
+
community: 'indigo',
|
|
151
|
+
local: 'neutral',
|
|
152
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import React, { useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { type Plugin, Plugin as PluginNS } from '@dxos/app-framework';
|
|
9
|
+
import { random } from '@dxos/random';
|
|
10
|
+
import { ScrollArea } from '@dxos/react-ui';
|
|
11
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
12
|
+
import { getHashHue } from '@dxos/ui-theme';
|
|
13
|
+
|
|
14
|
+
import { RegistryTagType } from '#types';
|
|
15
|
+
|
|
16
|
+
import { translations } from '../../translations';
|
|
17
|
+
import { PluginList } from './PluginList';
|
|
18
|
+
|
|
19
|
+
random.seed(1);
|
|
20
|
+
|
|
21
|
+
const icons = [
|
|
22
|
+
'ph--bug--regular',
|
|
23
|
+
'ph--compass--regular',
|
|
24
|
+
'ph--kanban--regular',
|
|
25
|
+
'ph--table--regular',
|
|
26
|
+
'ph--gear--regular',
|
|
27
|
+
'ph--github-logo--regular',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const DefaultStory = () => {
|
|
31
|
+
const [plugins] = useState<Plugin.Plugin[]>(
|
|
32
|
+
random.helpers.multiple(
|
|
33
|
+
() =>
|
|
34
|
+
PluginNS.define({
|
|
35
|
+
id: `org.dxos.plugin.plugin-${random.string.uuid()}`,
|
|
36
|
+
name: `${random.commerce.productName()}`,
|
|
37
|
+
description: random.lorem.sentences(Math.ceil(Math.random() * 3)),
|
|
38
|
+
tags: random.helpers.uniqueArray(RegistryTagType.literals as any, Math.floor(Math.random() * 3)),
|
|
39
|
+
icon: random.helpers.arrayElement(icons),
|
|
40
|
+
iconHue: getHashHue(random.string.uuid()),
|
|
41
|
+
homePage: random.datatype.boolean({ probability: 0.5 }) ? random.internet.url() : undefined,
|
|
42
|
+
source: random.internet.url(),
|
|
43
|
+
}).pipe(PluginNS.make)(),
|
|
44
|
+
{ count: 32 },
|
|
45
|
+
),
|
|
46
|
+
);
|
|
47
|
+
const [enabled, setEnabled] = useState<string[]>([]);
|
|
48
|
+
|
|
49
|
+
const handleChange = (id: string, enabled: boolean) => {
|
|
50
|
+
setEnabled((plugins) => (enabled ? [...plugins, id] : plugins.filter((plugin) => plugin === id)));
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<ScrollArea.Root orientation='vertical'>
|
|
55
|
+
<ScrollArea.Viewport>
|
|
56
|
+
<PluginList plugins={plugins} enabled={enabled} onChange={handleChange} hasSettings={() => true} />
|
|
57
|
+
</ScrollArea.Viewport>
|
|
58
|
+
</ScrollArea.Root>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const meta = {
|
|
63
|
+
title: 'plugins/plugin-registry/components/PluginList',
|
|
64
|
+
component: PluginList,
|
|
65
|
+
render: DefaultStory,
|
|
66
|
+
parameters: {
|
|
67
|
+
translations,
|
|
68
|
+
},
|
|
69
|
+
} satisfies Meta<typeof PluginList>;
|
|
70
|
+
|
|
71
|
+
export default meta;
|
|
72
|
+
|
|
73
|
+
type Story = StoryObj<typeof meta>;
|
|
74
|
+
|
|
75
|
+
export const Default: Story = {
|
|
76
|
+
decorators: [withTheme(), withLayout({ layout: 'column', classNames: 'bg-deck-surface' })],
|
|
77
|
+
parameters: {
|
|
78
|
+
layout: 'fullscreen',
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const FullScreen: Story = {
|
|
83
|
+
decorators: [withTheme(), withLayout({ scroll: true })],
|
|
84
|
+
parameters: {
|
|
85
|
+
layout: 'fullscreen',
|
|
86
|
+
},
|
|
87
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { type Plugin } from '@dxos/app-framework';
|
|
8
|
+
import { List } from '@dxos/react-ui';
|
|
9
|
+
|
|
10
|
+
import { PluginItem, type PluginItemProps } from './PluginItem';
|
|
11
|
+
|
|
12
|
+
export type PluginListProps = Omit<PluginItemProps, 'plugin' | 'extraTags'> & {
|
|
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[]>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const PluginList = ({ plugins = [], extraTagsById, ...props }: PluginListProps) => {
|
|
22
|
+
return (
|
|
23
|
+
<List classNames='grid grid-cols-[repeat(auto-fill,minmax(18rem,1fr))] auto-rows-[max-content] gap-4 p-4'>
|
|
24
|
+
{plugins.map((plugin) => (
|
|
25
|
+
<PluginItem key={plugin.meta.id} plugin={plugin} extraTags={extraTagsById?.[plugin.meta.id]} {...props} />
|
|
26
|
+
))}
|
|
27
|
+
</List>
|
|
28
|
+
);
|
|
29
|
+
};
|
package/src/components/index.ts
CHANGED
|
@@ -0,0 +1,147 @@
|
|
|
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 } 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
|
+
/**
|
|
27
|
+
* Turns a community manifest entry into a minimal Plugin object so we can reuse
|
|
28
|
+
* {@link PluginList} for rendering. The synthesized plugin has no modules — it
|
|
29
|
+
* exists only for display until the user installs it.
|
|
30
|
+
*/
|
|
31
|
+
const toDisplayPlugin = (entry: PluginEntry): Plugin.Plugin =>
|
|
32
|
+
({
|
|
33
|
+
[Plugin.PluginTypeId]: Plugin.PluginTypeId,
|
|
34
|
+
meta: entry.meta,
|
|
35
|
+
modules: [],
|
|
36
|
+
}) as Plugin.Plugin;
|
|
37
|
+
|
|
38
|
+
export type CommunityRegistryProps = {
|
|
39
|
+
id: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const CommunityRegistry = composable<HTMLDivElement, CommunityRegistryProps>(
|
|
43
|
+
({ id, ...props }, forwardedRef) => {
|
|
44
|
+
const { t } = useTranslation(meta.id);
|
|
45
|
+
const manager = usePluginManager();
|
|
46
|
+
const { invoke, invokePromise } = useOperationInvoker();
|
|
47
|
+
const { entries, loading, error } = useCommunityPlugins();
|
|
48
|
+
const enabled = useAtomValue(manager.enabled);
|
|
49
|
+
const plugins = useAtomValue(manager.plugins);
|
|
50
|
+
const installedIds = useMemo(() => new Set(plugins.map((plugin) => plugin.meta.id)), [plugins]);
|
|
51
|
+
const allSettings = useCapabilities(AppCapabilities.Settings);
|
|
52
|
+
const extraTagsById = useAutoTags(entries);
|
|
53
|
+
|
|
54
|
+
const sortedEntries = useMemo(() => [...entries].sort(sortEntries), [entries]);
|
|
55
|
+
const moduleUrlById = useMemo(() => {
|
|
56
|
+
const map: Record<string, string> = {};
|
|
57
|
+
for (const entry of sortedEntries) {
|
|
58
|
+
map[entry.meta.id] = entry.moduleUrl;
|
|
59
|
+
}
|
|
60
|
+
return map;
|
|
61
|
+
}, [sortedEntries]);
|
|
62
|
+
|
|
63
|
+
// Plugins the user loaded by URL that aren't in the Edge catalog belong in
|
|
64
|
+
// the community section — otherwise they'd be invisible (they're filtered
|
|
65
|
+
// out of Official/Recommended by `useRemotePluginIds`).
|
|
66
|
+
const displayPlugins = useMemo(() => {
|
|
67
|
+
const catalogIds = new Set(sortedEntries.map((entry) => entry.meta.id));
|
|
68
|
+
const remoteIds = new Set(UrlLoader.getRemoteEntries().map((entry) => entry.id));
|
|
69
|
+
const fromCatalog = sortedEntries.map(toDisplayPlugin);
|
|
70
|
+
const fromUrl = plugins.filter((plugin) => remoteIds.has(plugin.meta.id) && !catalogIds.has(plugin.meta.id));
|
|
71
|
+
return [...fromCatalog, ...fromUrl].sort((a, b) =>
|
|
72
|
+
(a.meta.name ?? a.meta.id).localeCompare(b.meta.name ?? b.meta.id),
|
|
73
|
+
);
|
|
74
|
+
}, [sortedEntries, plugins]);
|
|
75
|
+
|
|
76
|
+
const handleChange = useCallback(
|
|
77
|
+
(pluginId: string, nextEnabled: boolean) =>
|
|
78
|
+
Effect.gen(function* () {
|
|
79
|
+
if (nextEnabled) {
|
|
80
|
+
if (installedIds.has(pluginId)) {
|
|
81
|
+
yield* manager.enable(pluginId);
|
|
82
|
+
} else {
|
|
83
|
+
const moduleUrl = moduleUrlById[pluginId];
|
|
84
|
+
if (!moduleUrl) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
yield* manager.add(moduleUrl);
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
yield* manager.disable(pluginId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
yield* invoke(ObservabilityOperation.SendEvent, {
|
|
94
|
+
name: 'plugins.toggle',
|
|
95
|
+
properties: { plugin: pluginId, enabled: nextEnabled, source: 'community' },
|
|
96
|
+
});
|
|
97
|
+
}).pipe(runAndForwardErrors),
|
|
98
|
+
[invoke, manager, installedIds, moduleUrlById],
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const handleClick = useCallback(
|
|
102
|
+
(pluginId: string) =>
|
|
103
|
+
invokePromise(LayoutOperation.Open, {
|
|
104
|
+
subject: [getPluginPath(pluginId)],
|
|
105
|
+
pivotId: id,
|
|
106
|
+
positioning: 'end',
|
|
107
|
+
}),
|
|
108
|
+
[invokePromise, id],
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const hasSettings = useCallback(
|
|
112
|
+
(pluginId: string) => allSettings.some((setting) => setting.prefix === pluginId),
|
|
113
|
+
[allSettings],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const handleSettings = useCallback(
|
|
117
|
+
(pluginId: string) => invokePromise(SettingsOperation.Open, { plugin: pluginId }),
|
|
118
|
+
[invokePromise],
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<ScrollArea.Root {...composableProps(props)} orientation='vertical' ref={forwardedRef}>
|
|
123
|
+
<ScrollArea.Viewport>
|
|
124
|
+
{displayPlugins.length > 0 ? (
|
|
125
|
+
<PluginList
|
|
126
|
+
plugins={displayPlugins}
|
|
127
|
+
enabled={enabled}
|
|
128
|
+
extraTagsById={extraTagsById}
|
|
129
|
+
onClick={handleClick}
|
|
130
|
+
onChange={handleChange}
|
|
131
|
+
hasSettings={hasSettings}
|
|
132
|
+
onSettings={handleSettings}
|
|
133
|
+
/>
|
|
134
|
+
) : error ? (
|
|
135
|
+
<div className='p-4 text-description'>Failed to load community plugins: {error.message}</div>
|
|
136
|
+
) : loading ? (
|
|
137
|
+
<div className='p-4 text-description'>{t('loading.label')}</div>
|
|
138
|
+
) : (
|
|
139
|
+
<div className='p-4 text-description'>No community plugins available.</div>
|
|
140
|
+
)}
|
|
141
|
+
</ScrollArea.Viewport>
|
|
142
|
+
</ScrollArea.Root>
|
|
143
|
+
);
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
CommunityRegistry.displayName = 'CommunityRegistry';
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as Effect from 'effect/Effect';
|
|
6
|
+
import React, { useCallback, useRef, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { usePluginManager } from '@dxos/app-framework/ui';
|
|
9
|
+
import { runAndForwardErrors } from '@dxos/effect';
|
|
10
|
+
import { Button, Dialog, Input, useTranslation } from '@dxos/react-ui';
|
|
11
|
+
|
|
12
|
+
import { meta } from '../../meta';
|
|
13
|
+
|
|
14
|
+
export const LOAD_PLUGIN_DIALOG = `${meta.id}.LoadPluginDialog`;
|
|
15
|
+
|
|
16
|
+
export const LoadPluginDialog = () => {
|
|
17
|
+
const manager = usePluginManager();
|
|
18
|
+
const { t } = useTranslation(meta.id);
|
|
19
|
+
const [url, setUrl] = useState('');
|
|
20
|
+
const [loading, setLoading] = useState(false);
|
|
21
|
+
const [error, setError] = useState<string | null>(null);
|
|
22
|
+
const closeRef = useRef<HTMLButtonElement | null>(null);
|
|
23
|
+
|
|
24
|
+
const handleLoad = useCallback(() => {
|
|
25
|
+
if (!url.trim()) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setLoading(true);
|
|
30
|
+
setError(null);
|
|
31
|
+
|
|
32
|
+
void Effect.gen(function* () {
|
|
33
|
+
yield* manager.add(url.trim());
|
|
34
|
+
closeRef.current?.click();
|
|
35
|
+
}).pipe(
|
|
36
|
+
Effect.catchAll((err) =>
|
|
37
|
+
Effect.sync(() => {
|
|
38
|
+
setError(String(err));
|
|
39
|
+
}),
|
|
40
|
+
),
|
|
41
|
+
Effect.tap(() => Effect.sync(() => setLoading(false))),
|
|
42
|
+
runAndForwardErrors,
|
|
43
|
+
);
|
|
44
|
+
}, [url, manager]);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<Dialog.Content>
|
|
48
|
+
<Dialog.Header>
|
|
49
|
+
<Dialog.Title>{t('load-by-url-dialog.title')}</Dialog.Title>
|
|
50
|
+
<Dialog.Close asChild>
|
|
51
|
+
<Dialog.CloseIconButton ref={closeRef} />
|
|
52
|
+
</Dialog.Close>
|
|
53
|
+
</Dialog.Header>
|
|
54
|
+
<Dialog.Body>
|
|
55
|
+
{/* TODO(burdon): Form section. */}
|
|
56
|
+
<div className='flex flex-col gap-4'>
|
|
57
|
+
<Input.Root validationValence={error ? 'error' : undefined}>
|
|
58
|
+
<Input.Label>{t('plugin-url.label')}</Input.Label>
|
|
59
|
+
<Input.TextInput
|
|
60
|
+
placeholder='https://example.com/plugin.mjs'
|
|
61
|
+
value={url}
|
|
62
|
+
onChange={(event) => {
|
|
63
|
+
setUrl(event.target.value);
|
|
64
|
+
setError(null);
|
|
65
|
+
}}
|
|
66
|
+
onKeyDown={(event) => {
|
|
67
|
+
if (event.key === 'Enter') {
|
|
68
|
+
handleLoad();
|
|
69
|
+
}
|
|
70
|
+
}}
|
|
71
|
+
disabled={loading}
|
|
72
|
+
autoFocus
|
|
73
|
+
/>
|
|
74
|
+
{error && <Input.DescriptionAndValidation>{error}</Input.DescriptionAndValidation>}
|
|
75
|
+
</Input.Root>
|
|
76
|
+
<div className='flex justify-end'>
|
|
77
|
+
<Button variant='primary' disabled={!url.trim() || loading} onClick={handleLoad}>
|
|
78
|
+
{loading ? t('loading.label') : t('load-plugin.label')}
|
|
79
|
+
</Button>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</Dialog.Body>
|
|
83
|
+
</Dialog.Content>
|
|
84
|
+
);
|
|
85
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { useCallback } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type Plugin } from '@dxos/app-framework';
|
|
8
|
+
import { usePluginManager } from '@dxos/app-framework/ui';
|
|
9
|
+
import { runAndForwardErrors } from '@dxos/effect';
|
|
10
|
+
|
|
11
|
+
import { PluginDetail } from '#components';
|
|
12
|
+
|
|
13
|
+
// TODO(burdon): Convert to ECHO type.
|
|
14
|
+
export type PluginArticleProps = { subject: Plugin.Plugin };
|
|
15
|
+
|
|
16
|
+
export const PluginArticle = ({ subject: plugin }: PluginArticleProps) => {
|
|
17
|
+
const manager = usePluginManager();
|
|
18
|
+
const enabled = manager.getEnabled().includes(plugin.meta.id);
|
|
19
|
+
const handleEnableChange = useCallback(
|
|
20
|
+
(enabled: boolean) =>
|
|
21
|
+
enabled
|
|
22
|
+
? runAndForwardErrors(manager.enable(plugin.meta.id))
|
|
23
|
+
: runAndForwardErrors(manager.disable(plugin.meta.id)),
|
|
24
|
+
[manager, plugin.meta.id],
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return <PluginDetail plugin={plugin} enabled={enabled} onEnabledChange={handleEnableChange} />;
|
|
28
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useAtomValue } from '@effect-atom/atom-react';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import React, { useCallback, useMemo } from 'react';
|
|
8
|
+
|
|
9
|
+
import { type Plugin } 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 { ScrollArea } from '@dxos/react-ui';
|
|
15
|
+
import { composable, composableProps } from '@dxos/ui-theme';
|
|
16
|
+
|
|
17
|
+
import { PluginList } from '#components';
|
|
18
|
+
import { getPluginPath } from '#meta';
|
|
19
|
+
|
|
20
|
+
const sortByPluginMeta = ({ meta: { name: a = '' } }: Plugin.Plugin, { meta: { name: b = '' } }: Plugin.Plugin) =>
|
|
21
|
+
a.localeCompare(b);
|
|
22
|
+
|
|
23
|
+
export type PluginRegistryProps = {
|
|
24
|
+
id: string;
|
|
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[]>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const PluginRegistry = composable<HTMLDivElement, PluginRegistryProps>(
|
|
33
|
+
({ id, plugins: pluginsProp, extraTagsById, ...props }, forwardedRef) => {
|
|
34
|
+
const manager = usePluginManager();
|
|
35
|
+
const { invoke, invokePromise } = useOperationInvoker();
|
|
36
|
+
const plugins = useMemo(() => pluginsProp.sort(sortByPluginMeta), [pluginsProp]);
|
|
37
|
+
const enabled = useAtomValue(manager.enabled);
|
|
38
|
+
const allSettings = useCapabilities(AppCapabilities.Settings);
|
|
39
|
+
|
|
40
|
+
const handleChange = useCallback(
|
|
41
|
+
(id: string, enabled: boolean) =>
|
|
42
|
+
Effect.gen(function* () {
|
|
43
|
+
if (enabled) {
|
|
44
|
+
yield* manager.enable(id);
|
|
45
|
+
} else {
|
|
46
|
+
yield* manager.disable(id);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
yield* invoke(ObservabilityOperation.SendEvent, {
|
|
50
|
+
name: 'plugins.toggle',
|
|
51
|
+
properties: {
|
|
52
|
+
plugin: id,
|
|
53
|
+
enabled,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}).pipe(runAndForwardErrors),
|
|
57
|
+
[invoke, manager],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const handleClick = useCallback(
|
|
61
|
+
(pluginId: string) =>
|
|
62
|
+
invokePromise(LayoutOperation.Open, {
|
|
63
|
+
subject: [getPluginPath(pluginId)],
|
|
64
|
+
pivotId: id,
|
|
65
|
+
positioning: 'end',
|
|
66
|
+
}),
|
|
67
|
+
[invokePromise, id],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const hasSettings = useCallback(
|
|
71
|
+
(pluginId: string) => allSettings.some((s) => s.prefix === pluginId),
|
|
72
|
+
[allSettings],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const handleSettings = useCallback(
|
|
76
|
+
(pluginId: string) => invokePromise(SettingsOperation.Open, { plugin: pluginId }),
|
|
77
|
+
[invokePromise],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<ScrollArea.Root {...composableProps(props)} orientation='vertical' ref={forwardedRef}>
|
|
82
|
+
<ScrollArea.Viewport>
|
|
83
|
+
<PluginList
|
|
84
|
+
plugins={plugins}
|
|
85
|
+
enabled={enabled}
|
|
86
|
+
extraTagsById={extraTagsById}
|
|
87
|
+
onClick={handleClick}
|
|
88
|
+
onChange={handleChange}
|
|
89
|
+
hasSettings={hasSettings}
|
|
90
|
+
onSettings={handleSettings}
|
|
91
|
+
/>
|
|
92
|
+
</ScrollArea.Viewport>
|
|
93
|
+
</ScrollArea.Root>
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
PluginRegistry.displayName = 'PluginRegistry';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type ComponentType, lazy } from 'react';
|
|
6
|
+
|
|
7
|
+
export { LOAD_PLUGIN_DIALOG } from './LoadPluginDialog/LoadPluginDialog';
|
|
8
|
+
export const CommunityRegistry: ComponentType<any> = lazy(() => import('./CommunityRegistry'));
|
|
9
|
+
export const LoadPluginDialog: ComponentType<any> = lazy(() => import('./LoadPluginDialog'));
|
|
10
|
+
export const PluginArticle: ComponentType<any> = lazy(() => import('./PluginArticle'));
|
|
11
|
+
export const PluginRegistry: ComponentType<any> = lazy(() => import('./PluginRegistry'));
|