/** * Host editor sections contributed by plugins. * * A plugin declares host-scope fields in its manifest and core draws them, so * a plugin cannot ship its own form styling and a section disappears when its * plugin does. The enable switch comes first and gates the rest, which is the * shape the hardcoded feature tabs already have. * * Nothing is rendered when no enabled plugin declares host settings, so the * group does not appear as an empty tab. */ import { useMemo } from "react"; import { useTranslation } from "react-i18next"; import { SectionCard } from "@/components/section-card"; import { DocsLink } from "@/components/docs-link"; import { PluginIcon } from "@/lib/plugin-icon"; import { pluginName } from "@/lib/plugin-i18n"; import { type PluginSummary } from "@/api/plugins-api"; import { useInstalledPlugins } from "@/hooks/use-installed-plugins"; import { usePluginScope } from "@/plugin-host/scope"; import { SettingsFieldRow } from "./SettingsFields"; import { hasVisibleFields, isFieldShown } from "./settings-fields-util"; /** Values for every plugin on one host: { [pluginId]: { [key]: value } }. */ export type HostPluginSettings = Record>; /** Enabled plugins declaring host-scope settings. */ export function usePluginHostSections(): PluginSummary[] { const plugins = useInstalledPlugins(); return useMemo( () => plugins.filter((plugin) => { if (!plugin.enabled) return false; const host = plugin.contributes?.settings?.host; return !!host && (hasVisibleFields(host.fields) || !!host.enableKey); }), [plugins], ); } /** What each field reads before the host saves it. */ function hostFieldDefaults(plugin: PluginSummary): Record { const host = plugin.contributes?.settings?.host; if (!host) return {}; const defaults: Record = {}; if (host.enableKey) defaults[host.enableKey] = host.enableDefault ?? false; for (const field of host.fields) { if (field.default !== undefined) defaults[field.key] = field.default; } return defaults; } export interface HostPluginSectionsProps { plugins: PluginSummary[]; values: HostPluginSettings; setValue: (pluginId: string, key: string, value: unknown) => void; } export function HostPluginSections({ plugins, values, setValue, }: HostPluginSectionsProps) { if (plugins.length === 0) return null; return (
{plugins.map((plugin) => ( setValue(plugin.id, key, value)} /> ))}
); } export interface HostFeatureFieldsProps { // The host editor form, as handed to a host editor section. // eslint-disable-next-line @typescript-eslint/no-explicit-any form: any; updateForm: ( patch: (form: Record) => Record, ) => void; } /** * The calling plugin's manifest host settings, for a plugin that registered * its own host editor section and so gets no generated tab. */ export function HostFeatureFields({ form, updateForm, }: HostFeatureFieldsProps) { const pluginId = usePluginScope(); const plugin = usePluginHostSections().find((p) => p.id === pluginId); if (!plugin) return null; const values = (form?.pluginSettings as HostPluginSettings | undefined) ?? {}; return ( updateForm((current) => { const all = (current.pluginSettings ?? {}) as HostPluginSettings; return { ...current, pluginSettings: { ...all, [plugin.id]: { ...(all[plugin.id] ?? {}), [key]: value }, }, }; }) } /> ); } function HostPluginSection({ plugin, values, setValue, }: { plugin: PluginSummary; values: Record; setValue: (key: string, value: unknown) => void; }) { const { t } = useTranslation(); const host = plugin.contributes?.settings?.host; if (!host) return null; const shown = { ...hostFieldDefaults(plugin), ...values }; const running = plugin.enabled && plugin.state !== "failed"; const enableKey = host.enableKey; // With no enable switch the section is always on, which is what a plugin // declaring only plain fields means. const enabled = enableKey ? shown[enableKey] === true : true; return ( } action={} > {!running && (
{t("settings.featureUnavailable", { name: pluginName(t, plugin), })}
)} {enableKey && ( )} {enabled && host.fields .filter((field) => isFieldShown(field, shown)) .map((field) => ( ))}
); }