mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 22:25:24 +00:00
* fix: show plugin names and descriptions in the UI language Read plugin.name and plugin.description from each plugin's own locale namespace, falling back to the manifest, everywhere core shows a plugin's name: the Plugins manager, onboarding picker, host editor tabs and sections, host filters and bulk actions, beta updates, interface presets and plugin crash/loading notices. * docs(sdk): document plugin.name and plugin.description locale keys --------- Co-authored-by: ZacharyZcR <zkl205305@gmail.com>
190 lines
5.8 KiB
TypeScript
190 lines
5.8 KiB
TypeScript
/**
|
|
* 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<string, Record<string, unknown>>;
|
|
|
|
/** 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<string, unknown> {
|
|
const host = plugin.contributes?.settings?.host;
|
|
if (!host) return {};
|
|
const defaults: Record<string, unknown> = {};
|
|
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 (
|
|
<div className="flex flex-col gap-2">
|
|
{plugins.map((plugin) => (
|
|
<HostPluginSection
|
|
key={plugin.id}
|
|
plugin={plugin}
|
|
values={values[plugin.id] ?? {}}
|
|
setValue={(key, value) => setValue(plugin.id, key, value)}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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<string, unknown>) => Record<string, unknown>,
|
|
) => 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 (
|
|
<HostPluginSection
|
|
plugin={plugin}
|
|
values={values[plugin.id] ?? {}}
|
|
setValue={(key, value) =>
|
|
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<string, unknown>;
|
|
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 (
|
|
<SectionCard
|
|
title={pluginName(t, plugin)}
|
|
icon={<PluginIcon name={plugin.icon} className="size-3.5" />}
|
|
action={<DocsLink href={plugin.docs} />}
|
|
>
|
|
{!running && (
|
|
<div className="my-3 border border-warning/40 bg-warning/10 px-3 py-2 text-xs text-warning">
|
|
{t("settings.featureUnavailable", {
|
|
name: pluginName(t, plugin),
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{enableKey && (
|
|
<SettingsFieldRow
|
|
pluginId={plugin.id}
|
|
field={{
|
|
key: enableKey,
|
|
type: "boolean",
|
|
labelKey: host.enableLabelKey,
|
|
descriptionKey: host.enableDescriptionKey,
|
|
}}
|
|
values={shown}
|
|
setValue={setValue}
|
|
running={running}
|
|
defaultKey={`${plugin.id}.${enableKey}`}
|
|
/>
|
|
)}
|
|
|
|
{enabled &&
|
|
host.fields
|
|
.filter((field) => isFieldShown(field, shown))
|
|
.map((field) => (
|
|
<SettingsFieldRow
|
|
key={field.key}
|
|
pluginId={plugin.id}
|
|
field={field}
|
|
values={shown}
|
|
setValue={setValue}
|
|
running={running}
|
|
defaultKey={`${plugin.id}.${field.key}`}
|
|
/>
|
|
))}
|
|
</SectionCard>
|
|
);
|
|
}
|