import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Avatar, Badge, Box, Button, color, config, Header, IconButton, Input, Menu, MenuItem, PopOut, RectCords, Scroll, Spinner, Switch, Text, toRem } from 'folds';
import { Icon, Icons } from '../../../components/icons';
import { HexColorPicker } from 'react-colorful';
import FocusTrap from 'focus-trap-react';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { PluginTab, PluginMetadata, InstalledPlugin } from './types';
import { SequenceCard } from '../../../components/sequence-card';
import { SettingTile } from '../../../components/setting-tile';
import { SequenceCardStyle } from '../styles.css';
import { pluginMarketplaceManager, pluginRegistry, SettingDefinition } from './PluginAPI';
import { stopPropagation } from '../../../utils/keyboard';
import { fuzzyFilter } from '../../../utils/fuzzy';
/**
* Renders settings UI for a plugin based on its settings schema
*/
function PluginSettingsRenderer({ pluginId }: { pluginId: string }) {
const schema = pluginRegistry.getPluginSettingsSchema(pluginId);
const [, forceUpdate] = useState({});
if (!schema || Object.keys(schema).length === 0) {
return (
No settings available for this plugin.
);
}
const handleChange = (key: string, value: any) => {
pluginRegistry.setPluginSetting(pluginId, key, value);
forceUpdate({});
};
return (
{Object.entries(schema).map(([key, def]) => (
handleChange(key, value)}
/>
))}
);
}
/**
* Renders a single setting based on its type
*/
function SettingRenderer({
settingKey,
definition,
value,
onChange,
}: {
settingKey: string;
definition: SettingDefinition;
value: any;
onChange: (value: any) => void;
}) {
const [colorMenuCords, setColorMenuCords] = useState();
const [selectMenuCords, setSelectMenuCords] = useState();
const label = definition.label || settingKey;
const description = definition.description;
// Boolean - Switch
if (definition.type === 'boolean') {
return (
}
/>
);
}
// String - Input
if (definition.type === 'string') {
return (
onChange(e.target.value)}
placeholder={definition.default}
/>
);
}
// Number - Input
if (definition.type === 'number') {
return (
onChange(parseFloat(e.target.value) || 0)}
/>
);
}
// Select - Dropdown
if (definition.type === 'select' && definition.options) {
const currentValue = value ?? definition.default ?? definition.options[0]?.value;
const selectedOption = definition.options.find(opt => opt.value === currentValue);
return (
}
onClick={(evt) => setSelectMenuCords(evt.currentTarget.getBoundingClientRect())}
>
{selectedOption?.label || currentValue}
setSelectMenuCords(undefined),
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
}
/>
);
}
// Color - Color Picker
if (definition.type === 'color') {
const currentColor = value ?? definition.default ?? '#000000';
return (
}
onClick={(evt) => setColorMenuCords(evt.currentTarget.getBoundingClientRect())}
>
{currentColor}
setColorMenuCords(undefined),
clickOutsideDeactivates: true,
escapeDeactivates: stopPropagation,
}}
>
}
/>
);
}
return null;
}
type PluginsProps = {
requestClose: () => void;
};
export function Plugins({ requestClose }: PluginsProps) {
const [activeTab, setActiveTab] = useState(PluginTab.Marketplace);
const [marketplacePlugins, setMarketplacePlugins] = useState([]);
const [installedPlugins, setInstalledPlugins] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [searchQuery, setSearchQuery] = useState('');
const [expandedSettings, setExpandedSettings] = useState(null);
const refreshInstalledPlugins = useCallback(async () => {
try {
setInstalledPlugins(await pluginMarketplaceManager.listInstalledPlugins());
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to list installed plugins');
console.error('Failed to list installed plugins:', err);
}
}, []);
const fetchMarketplacePlugins = useCallback(async () => {
setLoading(true);
setError(null);
try {
setMarketplacePlugins(await pluginMarketplaceManager.fetchMarketplacePlugins());
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch plugins');
console.error('Error fetching marketplace plugins:', err);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if (activeTab === PluginTab.Marketplace) {
fetchMarketplacePlugins();
}
}, [activeTab, fetchMarketplacePlugins]);
useEffect(() => {
refreshInstalledPlugins();
}, [refreshInstalledPlugins]);
const handleInstall = useCallback(
async (plugin: PluginMetadata) => {
try {
setLoading(true);
setError(null);
await pluginMarketplaceManager.installPlugin(plugin);
await refreshInstalledPlugins();
} catch (err) {
console.error('Failed to install plugin:', err);
setError(err instanceof Error ? err.message : 'Failed to install plugin');
} finally {
setLoading(false);
}
},
[refreshInstalledPlugins]
);
const handleUninstall = useCallback(
async (pluginId: string) => {
try {
setError(null);
await pluginMarketplaceManager.uninstallPlugin(pluginId);
await refreshInstalledPlugins();
} catch (err) {
console.error('Failed to uninstall plugin:', err);
setError(err instanceof Error ? err.message : 'Failed to uninstall plugin');
}
},
[refreshInstalledPlugins]
);
const handleToggleEnabled = useCallback(
async (pluginId: string) => {
const plugin = installedPlugins.find((p) => p.id === pluginId);
if (!plugin) return;
const newEnabledState = !plugin.enabled;
try {
setError(null);
await pluginMarketplaceManager.setPluginEnabled(pluginId, newEnabledState);
await refreshInstalledPlugins();
if (newEnabledState) {
console.log(`[Plugins] Plugin ${pluginId} enabled. Restart app to load it.`);
}
} catch (err) {
console.error(`Failed to toggle plugin ${pluginId}:`, err);
setError(err instanceof Error ? err.message : 'Failed to update plugin state');
}
},
[installedPlugins, refreshInstalledPlugins]
);
const isPluginInstalled = useCallback(
(pluginId: string) => installedPlugins.some((p) => p.id === pluginId),
[installedPlugins]
);
const PluginAvatar = ({ thumbnail, name }: { thumbnail: string; name: string }) => {
const [imageError, setImageError] = useState(false);
return (
{!imageError ? (
setImageError(true)}
/>
) : (
{name.charAt(0).toUpperCase()}
)}
);
};
const filteredMarketplacePlugins = useMemo(() => {
if (!searchQuery.trim()) return marketplacePlugins;
return fuzzyFilter(marketplacePlugins, searchQuery, (plugin) => [
plugin.name,
plugin.description,
plugin.author,
...plugin.tags,
]).map(({ item }) => item);
}, [marketplacePlugins, searchQuery]);
const filteredInstalledPlugins = useMemo(() => {
if (!searchQuery.trim()) return installedPlugins;
return fuzzyFilter(installedPlugins, searchQuery, (plugin) => [
plugin.name,
plugin.description,
plugin.author,
]).map(({ item }) => item);
}, [installedPlugins, searchQuery]);
return (
setActiveTab(PluginTab.Marketplace)}
>
Marketplace
setActiveTab(PluginTab.Installed)}
>
Installed
setSearchQuery(e.target.value)}
before={}
after={
searchQuery && (
setSearchQuery('')}
>
)
}
/>
{activeTab === PluginTab.Marketplace && (
{loading && (
)}
{error && (
Error: {error}
)}
{!loading && !error && filteredMarketplacePlugins.length === 0 && (
{searchQuery ? 'No plugins found matching your search' : 'No plugins available'}
)}
{!loading &&
!error &&
filteredMarketplacePlugins.map((plugin) => (
{
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
}}
>
{plugin.name}
by {plugin.author}
•
v{plugin.version}
{plugin.description}
{plugin.tags.length > 0 && (
{plugin.tags.map((tag) => (
{tag}
))}
)}
))}
)}
{activeTab === PluginTab.Installed && (
{filteredInstalledPlugins.length === 0 && (
{searchQuery ? 'No installed plugins found matching your search' : 'No plugins installed'}
)}
{filteredInstalledPlugins.map((plugin) => {
const hasSettings = pluginRegistry.getPluginSettingsSchema(plugin.id) &&
Object.keys(pluginRegistry.getPluginSettingsSchema(plugin.id)!).length > 0;
const settingsExpanded = expandedSettings === plugin.id;
return (
{
e.currentTarget.parentElement!.style.transform = 'translateY(-2px)';
e.currentTarget.parentElement!.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)';
}}
onMouseLeave={(e) => {
e.currentTarget.parentElement!.style.transform = 'translateY(0)';
e.currentTarget.parentElement!.style.boxShadow = 'none';
}}
>
{plugin.name}
by {plugin.author}
•
v{plugin.version}
{plugin.description}
{plugin.enabled ? 'Enabled' : 'Disabled'}
{hasSettings && (
}
after={}
onClick={() => setExpandedSettings(settingsExpanded ? null : plugin.id)}
>
Settings
)}
{hasSettings && settingsExpanded && (
)}
);
})}
)}
);
}