"use client"; import { useRef, useState, type RefObject } from "react"; import { IconArrowLeft } from "@tabler/icons-react"; import { Trans, useTranslation } from "react-i18next"; import { Badge } from "@kandev/ui/badge"; import { Button } from "@kandev/ui/button"; import { CardContent, CardHeader, CardTitle } from "@kandev/ui/card"; import { Separator } from "@kandev/ui/separator"; import { PluginSlot } from "@/components/plugins/plugin-slot"; import Link from "@/components/routing/app-link"; import { useRouter } from "@/lib/routing/client-router"; import { useResponsiveBreakpoint } from "@/hooks/use-responsive-breakpoint"; import { usePlugins } from "@/hooks/domains/plugins/use-plugins"; import { useIsAdmin } from "@/hooks/domains/auth/use-is-admin"; import { SettingsCard } from "@/components/settings/settings-card"; import { useSettingsSaveContributor } from "@/components/settings/settings-save-provider"; import { PluginConfigForm } from "./plugin-config-form"; import { PluginManifestCard } from "./plugin-manifest-card"; import { PluginShortcutsCard } from "./plugin-shortcuts-card"; import { PluginRepoLink } from "./plugin-repo-link"; import { PluginStatusBadge } from "./plugin-status-badge"; import { PluginErrorDiagnostic } from "./plugin-error-diagnostic"; import { PluginUninstallConfirmation } from "./uninstall-plugin-dialog"; import { usePluginActions } from "./use-plugin-actions"; import { usePluginConfigForm } from "./use-plugin-config-form"; import type { PluginRecord } from "@/lib/types/plugins"; import { SETTINGS_TYPOGRAPHY } from "@/components/settings/settings-typography"; import { controlSizingClassName } from "@kandev/ui/control-sizing"; const PLUGINS_SETTINGS_HREF = "/settings/plugins"; /** * Per-plugin settings page (Settings > Plugins > ): manifest * overview plus the schema-driven settings form declared by the plugin * author via the manifest's config_schema (e.g. a GitHub plugin's PAT). * Saving restarts a running plugin so it re-reads config via the Host * GetConfig RPC. */ export function PluginDetail({ pluginId }: { pluginId: string }) { const canManage = useIsAdmin(); const { items, loaded } = usePlugins(); const router = useRouter(); const { isFinePointer } = useResponsiveBreakpoint(); const actions = usePluginActions(); const plugin = items.find((p) => p.id === pluginId) ?? null; const [confirmingUninstall, setConfirmingUninstall] = useState(false); const uninstallAnchorRef = useRef(null); const form = usePluginConfigForm(canManage ? plugin : null); useSettingsSaveContributor({ id: `plugin-config:${pluginId}`, revision: form.revision, isDirty: canManage && form.isDirty, canSave: canManage && form.canSave, invalidReason: form.invalidReason, save: form.handleSave, discard: form.discard, }); if (!plugin) { return loaded ? : null; } return (
{canManage && ( <> {/* Owner-scoped inline slot for the plugin's own settings UI, at the top (see PLUGIN-API.md). */} )} {canManage && ( <> { setConfirmingUninstall(true); }} /> { setConfirmingUninstall(false); }} onConfirm={async () => { const uninstalled = await actions.confirmUninstall(plugin); if (uninstalled) router.push(PLUGINS_SETTINGS_HREF); }} /> )}
); } type PluginDetailHeaderProps = { plugin: PluginRecord; }; function PluginDetailHeader({ plugin }: PluginDetailHeaderProps) { const { t } = useTranslation(); return (
{t("common:plugins")}

{plugin.display_name}

{plugin.signed === false && ( {t("plugins:unsigned")} )}
{plugin.id} · v{plugin.version}
{plugin.description && (

{plugin.description}

)}
); } type PluginSettingsCardProps = { plugin: PluginRecord; form: ReturnType; busy: boolean; }; function PluginSettingsCard({ plugin, form, busy }: PluginSettingsCardProps) { const { t } = useTranslation(); return ( {t("plugins:settings")} ); } function PluginSettingsBody({ plugin, form, busy }: PluginSettingsCardProps) { const { t } = useTranslation(); if (form.fields.length === 0) { return (

{/* `config_schema` is a manifest key — the contract, not copy. */} This plugin does not declare any settings (no config_schema in its manifest).

); } if (form.configError) { return

{form.configError}

; } if (form.configLoading) { return

{t("plugins:loadingSettings")}

; } return (
{plugin.status === "active" && (

{t("plugins:savingRestartsPlugin")}

)}
); } type PluginDangerZoneProps = { plugin: PluginRecord; actions: ReturnType; isFinePointer: boolean; confirmingUninstall: boolean; uninstallAnchorRef: RefObject; onUninstall: () => void; }; function PluginDangerZone({ plugin, actions, isFinePointer, confirmingUninstall, uninstallAnchorRef, onUninstall, }: PluginDangerZoneProps) { const { t } = useTranslation(); const { isMobile } = useResponsiveBreakpoint(); const busy = actions.busyId === plugin.id || actions.uninstallBusy; const canEnable = plugin.status === "disabled" || plugin.status === "registered" || plugin.status === "error"; const canDisable = plugin.status === "active" || plugin.status === "error"; return (
{canEnable && ( )} {canDisable && ( )} {(isMobile || isFinePointer || !confirmingUninstall) && ( )}
); } function PluginNotFound({ pluginId }: { pluginId: string }) { const { t } = useTranslation(); return (
{t("common:plugins")}
{/* The plugin id is an identifier the user typed or followed, never copy. */} No installed plugin with id {pluginId}.
); }