(feat) Add ability to disable plugins and show bundled plugins (#337)

This commit is contained in:
Gregory Schier
2026-01-01 09:32:48 -08:00
committed by GitHub
parent 07ea1ea7dc
commit 92a8da03af
41 changed files with 515 additions and 1183 deletions

View File

@@ -5,6 +5,7 @@ import { useAtomValue } from 'jotai';
import type { CSSProperties, ReactNode } from 'react';
import { useCallback, useMemo } from 'react';
import { allRequestsAtom } from '../hooks/useAllRequests';
import { useFolderActions } from '../hooks/useFolderActions';
import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse';
import { sendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { showDialog } from '../lib/dialog';
@@ -30,6 +31,12 @@ interface Props {
export function FolderLayout({ folder, style }: Props) {
const folders = useAtomValue(foldersAtom);
const requests = useAtomValue(allRequestsAtom);
const folderActions = useFolderActions();
const sendAllAction = useMemo(
() => folderActions.find((a) => a.label === 'Send All'),
[folderActions],
);
const children = useMemo(() => {
return [
...folders.filter((f) => f.folderId === folder.id),
@@ -37,6 +44,10 @@ export function FolderLayout({ folder, style }: Props) {
];
}, [folder.id, folders, requests]);
const handleSendAll = useCallback(() => {
sendAllAction?.call(folder);
}, [sendAllAction, folder]);
return (
<div style={style} className="p-6 pt-4 overflow-y-auto @container">
<HStack space={2} alignItems="center">
@@ -48,6 +59,8 @@ export function FolderLayout({ folder, style }: Props) {
color="secondary"
size="sm"
variant="border"
onClick={handleSendAll}
disabled={sendAllAction == null}
>
Send All
</Button>

View File

@@ -1,7 +1,7 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import { openUrl } from '@tauri-apps/plugin-opener';
import type { Plugin } from '@yaakapp-internal/models';
import { pluginsAtom } from '@yaakapp-internal/models';
import { patchModel, pluginsAtom } from '@yaakapp-internal/models';
import type { PluginVersion } from '@yaakapp-internal/plugins';
import {
checkPluginUpdates,
@@ -18,6 +18,7 @@ import { usePluginsKey, useRefreshPlugins } from '../../hooks/usePlugins';
import { showConfirmDelete } from '../../lib/confirm';
import { minPromiseMillis } from '../../lib/minPromiseMillis';
import { Button } from '../core/Button';
import { Checkbox } from '../core/Checkbox';
import { CountBadge } from '../core/CountBadge';
import { Icon } from '../core/Icon';
import { IconButton } from '../core/IconButton';
@@ -34,6 +35,8 @@ import { SelectFile } from '../SelectFile';
export function SettingsPlugins() {
const [directory, setDirectory] = useState<string | null>(null);
const plugins = useAtomValue(pluginsAtom);
const bundledPlugins = plugins.filter((p) => p.url == null);
const installedPlugins = plugins.filter((p) => p.url != null);
const createPlugin = useInstallPlugin();
const refreshPlugins = useRefreshPlugins();
const [tab, setTab] = useState<string>();
@@ -49,7 +52,12 @@ export function SettingsPlugins() {
{
label: 'Installed',
value: 'installed',
rightSlot: <CountBadge count={plugins.length} />,
rightSlot: <CountBadge count={installedPlugins.length} />,
},
{
label: 'Bundled',
value: 'bundled',
rightSlot: <CountBadge count={bundledPlugins.length} />,
},
]}
>
@@ -101,6 +109,9 @@ export function SettingsPlugins() {
</footer>
</div>
</TabContent>
<TabContent value="bundled" className="pb-0">
<BundledPlugins />
</TabContent>
</Tabs>
</div>
);
@@ -119,6 +130,27 @@ function PluginTableRowForInstalledPlugin({ plugin }: { plugin: Plugin }) {
name={info.name}
displayName={info.displayName}
url={plugin.url}
showCheckbox={true}
showUninstall={true}
/>
);
}
function PluginTableRowForBundledPlugin({ plugin }: { plugin: Plugin }) {
const info = usePluginInfo(plugin.id).data;
if (info == null) {
return null;
}
return (
<PluginTableRow
plugin={plugin}
version={info.version}
name={info.name}
displayName={info.displayName}
url={plugin.url}
showCheckbox={true}
showUninstall={false}
/>
);
}
@@ -134,6 +166,7 @@ function PluginTableRowForRemotePluginVersion({ pluginVersion }: { pluginVersion
name={pluginVersion.name}
displayName={pluginVersion.displayName}
url={pluginVersion.url}
showCheckbox={false}
/>
);
}
@@ -144,12 +177,16 @@ function PluginTableRow({
version,
displayName,
url,
showCheckbox = true,
showUninstall = true,
}: {
plugin: Plugin | null;
name: string;
version: string;
displayName: string;
url: string | null;
showCheckbox?: boolean;
showUninstall?: boolean;
}) {
const updates = usePluginUpdates();
const latestVersion = updates.data?.plugins.find((u) => u.name === name)?.version;
@@ -158,9 +195,26 @@ function PluginTableRow({
mutationFn: (name: string) => installPlugin(name, null),
});
const uninstall = usePromptUninstall(plugin?.id ?? null, displayName);
const refreshPlugins = useRefreshPlugins();
return (
<TableRow>
{showCheckbox && (
<TableCell className="!py-0">
<Checkbox
hideLabel
title={plugin?.enabled ? 'Disable plugin' : 'Enable plugin'}
checked={plugin?.enabled ?? false}
disabled={plugin == null}
onChange={async (enabled) => {
if (plugin) {
await patchModel(plugin, { enabled });
refreshPlugins.mutate();
}
}}
/>
</TableCell>
)}
<TableCell className="font-semibold">
{url ? (
<Link noUnderline href={url}>
@@ -170,6 +224,9 @@ function PluginTableRow({
displayName
)}
</TableCell>
<TableCell>
<InlineCode>{name}</InlineCode>
</TableCell>
<TableCell>
<HStack space={1.5}>
<InlineCode>{version}</InlineCode>
@@ -206,7 +263,7 @@ function PluginTableRow({
Install
</Button>
) : null}
{uninstall != null && (
{showUninstall && uninstall != null && (
<Button
size="xs"
title="Uninstall plugin"
@@ -253,6 +310,7 @@ function PluginSearch() {
<Table scrollable>
<TableHead>
<TableRow>
<TableHeaderCell>Display Name</TableHeaderCell>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Version</TableHeaderCell>
<TableHeaderCell />
@@ -271,7 +329,7 @@ function PluginSearch() {
}
function InstalledPlugins() {
const plugins = useAtomValue(pluginsAtom);
const plugins = useAtomValue(pluginsAtom).filter((p) => p.url != null);
return plugins.length === 0 ? (
<div className="pb-4">
@@ -285,6 +343,8 @@ function InstalledPlugins() {
<Table scrollable>
<TableHead>
<TableRow>
<TableHeaderCell className="w-0" />
<TableHeaderCell>Display Name</TableHeaderCell>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Version</TableHeaderCell>
<TableHeaderCell />
@@ -299,6 +359,33 @@ function InstalledPlugins() {
);
}
function BundledPlugins() {
const plugins = useAtomValue(pluginsAtom).filter((p) => p.url == null);
return plugins.length === 0 ? (
<div className="pb-4">
<EmptyStateText className="text-center">No bundled plugins found.</EmptyStateText>
</div>
) : (
<Table scrollable>
<TableHead>
<TableRow>
<TableHeaderCell className="w-0" />
<TableHeaderCell>Display Name</TableHeaderCell>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Version</TableHeaderCell>
<TableHeaderCell />
</TableRow>
</TableHead>
<tbody className="divide-y divide-surface-highlight">
{plugins.map((p) => (
<PluginTableRowForBundledPlugin key={p.id} plugin={p} />
))}
</tbody>
</Table>
);
}
function usePromptUninstall(pluginId: string | null, name: string) {
const mut = useMutation({
mutationKey: ['uninstall_plugin', pluginId],

View File

@@ -27,8 +27,6 @@ import { selectAtom } from 'jotai/utils';
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { moveToWorkspace } from '../commands/moveToWorkspace';
import { openFolderSettings } from '../commands/openFolderSettings';
import { activeCookieJarAtom } from '../hooks/useActiveCookieJar';
import { activeEnvironmentAtom } from '../hooks/useActiveEnvironment';
import { activeFolderIdAtom } from '../hooks/useActiveFolderId';
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
import { activeWorkspaceAtom, activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
@@ -49,7 +47,6 @@ import { jotaiStore } from '../lib/jotai';
import { resolvedModelName } from '../lib/resolvedModelName';
import { isSidebarFocused } from '../lib/scopes';
import { navigateToRequestOrFolderOrWorkspace } from '../lib/setWorkspaceSearchParams';
import { invokeCmd } from '../lib/tauri';
import type { ContextMenuProps, DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import type { FieldDef } from './core/Editor/filter/extension';
@@ -331,20 +328,6 @@ function Sidebar({ className }: { className?: string }) {
leftSlot: <Icon icon="folder_cog" />,
onSelect: () => openFolderSettings(child.id),
},
{
label: 'Send All',
hidden: !(items.length === 1 && child.model === 'folder'),
leftSlot: <Icon icon="send_horizontal" />,
onSelect: () => {
const environment = jotaiStore.get(activeEnvironmentAtom);
const cookieJar = jotaiStore.get(activeCookieJarAtom);
invokeCmd('cmd_send_folder', {
folderId: child.id,
environmentId: environment?.id,
cookieJarId: cookieJar?.id,
});
},
},
{
label: 'Send',
hotKeyAction: 'request.send',

View File

@@ -5,12 +5,16 @@ import { jotaiStore } from '../lib/jotai';
import { minPromiseMillis } from '../lib/minPromiseMillis';
import { invokeCmd } from '../lib/tauri';
import { activeWorkspaceIdAtom } from './useActiveWorkspace';
import { useDebouncedValue } from './useDebouncedValue';
import { invalidateAllPluginInfo } from './usePluginInfo';
export function usePluginsKey() {
return useAtomValue(pluginsAtom)
const pluginKey = useAtomValue(pluginsAtom)
.map((p) => p.id + p.updatedAt)
.join(',');
// Debounce plugins both for efficiency and to give plugins a chance to reload after the DB updates
return useDebouncedValue(pluginKey, 1000);
}
/**