/** * Gives every feature with manifest-declared host settings its own host editor * tab, named after the feature. A plugin that registered its own host editor * section already edits its settings there, so it gets no generated tab. * * Re-run whenever the plugin list changes so tabs come and go without a reload. */ import { PluginIcon } from "@/lib/plugin-icon"; import { hostEditorSectionList, registerHostEditorSection, unregisterHostEditorSection, } from "@/sidebar/HostManagerTabs"; import type { PluginSettingsField, PluginSummary } from "@/api/plugins-api"; import { HostPluginSections, type HostPluginSettings, } from "./HostPluginSections"; import { hasVisibleFields } from "./settings-fields-util"; import { isDefaultableField } from "@/sidebar/host-defaults/catalog"; const TAB_PREFIX = "feature:"; export function hostFeatureTabId(pluginId: string): string { return `${TAB_PREFIX}${pluginId}`; } interface HostFormLike { pluginSettings?: HostPluginSettings; } function hasDefaultableFields(host: { enableKey?: string; fields: PluginSettingsField[]; }): boolean { return ( !!host.enableKey || host.fields.some((field) => !field.hidden && isDefaultableField(field)) ); } /** Adds, updates and removes generated tabs to match the plugin list. */ export function syncHostFeatureTabs(plugins: PluginSummary[]): string[] { const existing = hostEditorSectionList(); const own = existing.filter( (section) => section.pluginId && !section.id.startsWith(TAB_PREFIX), ); const ownSections = new Set(own.map((section) => section.pluginId)); // The defaults tab sits where the plugin's own section does on a host. const ownPlacement = new Map< string, { group: "top" | "ssh"; order?: number } >(); for (const section of own) { if (!ownPlacement.has(section.pluginId!)) { ownPlacement.set(section.pluginId!, { group: section.group, order: section.order, }); } } // A plugin whose own sections are all about one host still gets its // switches in the defaults editor, through a generated tab shown only there. const ownDefaultsSections = new Set( own .filter((section) => section.defaults) .map((section) => section.pluginId), ); const contributors = plugins.filter((plugin) => { if (!plugin.enabled || ownDefaultsSections.has(plugin.id)) return false; const host = plugin.contributes?.settings?.host; if (!host) return false; // Its own section edits it per host, so only a default is left to offer. if (ownSections.has(plugin.id)) return hasDefaultableFields(host); return hasVisibleFields(host.fields) || !!host.enableKey; }); const wanted = new Set( contributors.map((plugin) => hostFeatureTabId(plugin.id)), ); for (const section of existing) { if (section.id.startsWith(TAB_PREFIX) && !wanted.has(section.id)) { unregisterHostEditorSection(section.id); } } for (const plugin of contributors) { const host = plugin.contributes!.settings!.host!; const icon = plugin.icon; const placement = ownPlacement.get(plugin.id); registerHostEditorSection({ id: hostFeatureTabId(plugin.id), pluginId: plugin.id, group: placement?.group ?? host.editorGroup ?? "top", order: placement?.order ?? host.editorOrder ?? 100, labelKey: plugin.name, label: plugin.name, // Nothing but the plugin's host settings, so it works for defaults too. defaults: ownSections.has(plugin.id) ? "only" : true, icon: icon ? ({ className }: { className?: string }) => ( ) : undefined, component: ({ form, setField }) => ( { const current = (form as HostFormLike)?.pluginSettings ?? {}; setField("pluginSettings", { ...current, [pluginId]: { ...(current[pluginId] ?? {}), [key]: value }, }); }} /> ), }); } return [...wanted]; }