From 2f637acf126276ab7cc7c5789418ca3fb35c441e Mon Sep 17 00:00:00 2001 From: kipavy Date: Tue, 25 Aug 2026 23:39:08 +0000 Subject: [PATCH] fix(plugins): give installed plugins the same enable toggle as bundled ones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Whether a plugin could be disabled depended on how it reached the disk, not on what it is. The toggle rendered only on rows built from getLoadedPlugins() minus installedMeta — bundled first-party plugins — so installing that same plugin from the catalogue moved it to the external list and it lost the control. Themes, which are only ever installed, therefore never had one. The rule is now a property of the plugin: a theme-only plugin contributes nothing but themes, so disabled and not-installed would mean the same thing and it gets no toggle; everything else gets one, bundled or installed. Both row types render the same EnableToggle so the two cannot drift apart. Installed rows now also dim when disabled, matching the bundled rows. --- .../PluginsSection.enableToggle.test.tsx | 120 ++++++++++++++++++ .../settings/sections/PluginsSection.tsx | 31 ++++- 2 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 src/components/settings/sections/PluginsSection.enableToggle.test.tsx diff --git a/src/components/settings/sections/PluginsSection.enableToggle.test.tsx b/src/components/settings/sections/PluginsSection.enableToggle.test.tsx new file mode 100644 index 000000000..e54e5f6ed --- /dev/null +++ b/src/components/settings/sections/PluginsSection.enableToggle.test.tsx @@ -0,0 +1,120 @@ +import { test, expect, vi, beforeEach, afterEach } from "vitest"; +import { render, screen, cleanup, fireEvent } from "@testing-library/react"; +import type { PluginManifest } from "@/plugins/api"; + +const manifest = (id: string, permissions: string[]): PluginManifest => + ({ id, name: id, version: "1.0.0", description: "", permissions } as PluginManifest); + +const DOCKER = manifest("plugin-docker", ["docker:read"]); +const THEME = manifest("emerald-night", ["themes"]); + +const loaded = vi.hoisted(() => ({ list: [] as PluginManifest[] })); +const runtime = vi.hoisted(() => ({ setPluginActive: vi.fn() })); +vi.mock("@/plugins/runtime", () => ({ + getLoadedPlugins: () => loaded.list, + setPluginActive: runtime.setPluginActive, + pluginStorageGet: vi.fn(async () => null), + pluginStorageSet: vi.fn(async () => {}), +})); +const marketplaceState = { + installedMeta: [] as unknown[], catalog: [] as unknown[], + installing: new Set(), + uninstallPlugin: vi.fn(async () => {}), + uninstallSeededPlugin: vi.fn(async () => {}), + reloadPlugin: vi.fn(async () => {}), + scanLocal: vi.fn(async () => {}), + installPlugin: vi.fn(async () => {}), + fetchManifest: vi.fn(async () => ({ manifest: { permissions: [] }, manifestText: "" })), + appVersion: null as string | null, + loadAppVersion: vi.fn(async () => {}), +}; +const FIRST_PARTY_SOURCE = vi.hoisted(() => ({ id: "voltius", name: "Voltius Marketplace", url: "", enabled: true, deletable: false })); +vi.mock("@/stores/marketplaceStore", () => ({ + useMarketplaceStore: (selector?: (s: typeof marketplaceState) => unknown) => + selector ? selector(marketplaceState) : marketplaceState, + FIRST_PARTY_SOURCE, +})); +vi.mock("@/stores/notificationStore", () => ({ + useNotificationStore: Object.assign(() => ({ push: vi.fn() }), { getState: () => ({ push: vi.fn() }) }), +})); +vi.mock("@/stores/toggleSettingsStore", () => ({ getToggle: () => false, useToggle: () => false })); +vi.mock("@/components/shared/ToolbarViewControls", () => ({ useFilterShortcut: () => {} })); +vi.mock("@/components/shared/Toggle", () => ({ + Toggle: ({ checked, onChange }: { checked: boolean; onChange: () => void }) => ( + - handleToggle(manifest.id, enabled)} /> + {manifest.permissions.length > 0 && (
@@ -501,6 +520,11 @@ export function InstalledTab() { {filteredExternal.map((meta) => { const manifest = externalManifests.find((m) => m.id === meta.id); const isLoaded = loadedIds.has(meta.id); + // `true` mirrors marketplaceStore's externalPluginActive: installing IS the + // opt-in, so an installed plugin is on unless the user turned it off. Kept + // as a literal rather than imported because that helper reads the store via + // getState(), which would not re-render this row when the override changes. + const enabled = isLoaded && isEnabled(meta.id, true); const isReloading = reloading.has(meta.id); const isUninstalling = uninstalling.has(meta.id); const update = availableUpdate(meta, catalog); @@ -511,11 +535,11 @@ export function InstalledTab() {
- +
@@ -580,6 +604,7 @@ export function InstalledTab() { > + {manifest && }
{manifest && manifest.permissions.length > 0 && (