/* eslint-disable react-refresh/only-export-components */ /** * Manifest-declared settings shown as ordinary sections in the profile and * admin panels, one per feature, named after it. */ import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Loader2 } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/button"; import { SectionCard } from "@/components/section-card"; import { AccordionSection } from "@/sidebar/AdminSettingsShared"; import { pluginKey, pluginName } from "@/lib/plugin-i18n"; import { PluginIcon } from "@/lib/plugin-icon"; import { getPluginAdminSettings, getPluginUserSettings, updatePluginAdminSettings, updatePluginUserSettings, PluginSettingsValidationError, type PluginSettingsErrors, type PluginSettingsField, type PluginSummary, } from "@/api/plugins-api"; import { useInstalledPlugins } from "@/hooks/use-installed-plugins"; import { SettingsFieldRow } from "./SettingsFields"; import { hasVisibleFields, isFieldShown } from "./settings-fields-util"; export type FeatureSettingsScope = "admin" | "user"; export type FeatureSectionId = `feature:${string}`; export function featureSectionId(pluginId: string): FeatureSectionId { return `feature:${pluginId}`; } /** Enabled features with fields in this scope, sorted by name. */ export function useFeatureSettings( scope: FeatureSettingsScope, ): PluginSummary[] { const plugins = useInstalledPlugins(); return useMemo( () => plugins .filter( (plugin) => plugin.enabled && hasVisibleFields(plugin.contributes?.settings?.[scope] ?? []), ) .sort((a, b) => a.name.localeCompare(b.name)), [plugins, scope], ); } export function FeatureSettingsSection({ plugin, scope, open, onToggle, }: { plugin: PluginSummary; scope: FeatureSettingsScope; open: boolean; onToggle: () => void; }) { const { t } = useTranslation(); return ( } open={open} onToggle={onToggle} > ); } export function FeatureSettingsForm({ plugin, scope, fields, }: { plugin: PluginSummary; scope: FeatureSettingsScope; fields: PluginSettingsField[]; }) { const { t } = useTranslation(); const [values, setValues] = useState>({}); const [errors, setErrors] = useState({}); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [dirty, setDirty] = useState(false); const running = plugin.enabled && plugin.state !== "failed"; const load = useCallback(async () => { setLoading(true); try { const loaded = scope === "admin" ? await getPluginAdminSettings(plugin.id) : await getPluginUserSettings(plugin.id); setValues(loaded); setErrors({}); setDirty(false); } catch { toast.error(t("settings.pluginSettingsLoadFailed")); } finally { setLoading(false); } }, [plugin.id, scope, t]); useEffect(() => { void load(); }, [load]); const setValue = (key: string, value: unknown) => { setValues((current) => ({ ...current, [key]: value })); setErrors((current) => { if (!current[key]) return current; const next = { ...current }; delete next[key]; return next; }); setDirty(true); }; const save = async () => { setSaving(true); try { const saved = scope === "admin" ? await updatePluginAdminSettings(plugin.id, values) : await updatePluginUserSettings(plugin.id, values); setValues(saved); setErrors({}); setDirty(false); toast.success(t("settings.pluginSettingsSaved")); } catch (error) { if (error instanceof PluginSettingsValidationError) { setErrors(error.errors); toast.error(t("settings.pluginSettingsInvalid")); } else { toast.error(t("settings.pluginSettingsSaveFailed")); } } finally { setSaving(false); } }; // Groups appear in the order of their first field, so the manifest sets the layout. const groups = useMemo(() => { const ordered: { key?: string; fields: PluginSettingsField[] }[] = []; for (const field of fields) { const existing = ordered.find((group) => group.key === field.group); if (existing) existing.fields.push(field); else ordered.push({ key: field.group, fields: [field] }); } return ordered; }, [fields]); if (loading) { return (
{t("common.loading")}
); } const rows = (group: PluginSettingsField[]) => group .filter((field) => isFieldShown(field, values)) .map((field) => ( )); const icon = ; return (
{!running && (
{t("settings.featureUnavailable", { name: pluginName(t, plugin), })}
)} {groups.map((group, index) => group.key ? ( {rows(group.fields)} ) : (
{rows(group.fields)}
), )}
); }