From c9be7c9bbfec912a0b47de72f8b48f8fff599ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Sat, 11 Apr 2026 23:20:36 +0200 Subject: [PATCH] refactor(web): make SmtpSettingsPanel self-contained, eliminating prop drilling SmtpSettingsPanel now owns its form state, save/test mutations, and feedback state internally. Props reduced from 17 to 2 (initialSettings + onSettingsSaved callback). Removes 7 useState declarations, 2 mutation definitions, and 1 handler from the parent. Co-Authored-By: Claude Opus 4.6 --- .../components/admin/SystemSettingsClient.tsx | 66 +--------- .../system-settings/SmtpSettingsPanel.tsx | 123 ++++++++++-------- 2 files changed, 73 insertions(+), 116 deletions(-) diff --git a/apps/web/src/components/admin/SystemSettingsClient.tsx b/apps/web/src/components/admin/SystemSettingsClient.tsx index efa5236..220b524 100644 --- a/apps/web/src/components/admin/SystemSettingsClient.tsx +++ b/apps/web/src/components/admin/SystemSettingsClient.tsx @@ -3,10 +3,7 @@ import { DEFAULT_OPENAI_MODEL } from "@capakraken/shared"; import { useEffect, useState } from "react"; import { trpc } from "~/lib/trpc/client.js"; -import { - AiProviderPanel, - GenerationSettingsPanel, -} from "./system-settings/AiSettingsPanels.js"; +import { AiProviderPanel, GenerationSettingsPanel } from "./system-settings/AiSettingsPanels.js"; import { LegacyRuntimeSecretsNotice } from "./system-settings/LegacyRuntimeSecretsNotice.js"; import { type ImageProvider, @@ -52,13 +49,6 @@ export function SystemSettingsClient() { const [imageProvider, setImageProvider] = useState("dalle"); const [geminiModel, setGeminiModel] = useState(""); const [imageSaved, setImageSaved] = useState(false); - const [smtpHost, setSmtpHost] = useState(""); - const [smtpPort, setSmtpPort] = useState(587); - const [smtpUser, setSmtpUser] = useState(""); - const [smtpFrom, setSmtpFrom] = useState(""); - const [smtpTls, setSmtpTls] = useState(true); - const [smtpSaved, setSmtpSaved] = useState(false); - const [smtpTestResult, setSmtpTestResult] = useState(null); const [anonymizationEnabled, setAnonymizationEnabled] = useState(false); const [anonymizationDomain, setAnonymizationDomain] = useState("superhartmut.de"); const [anonymizationSaved, setAnonymizationSaved] = useState(false); @@ -96,11 +86,6 @@ export function SystemSettingsClient() { setDalleEndpoint(settings.azureDalleEndpoint ?? ""); setImageProvider((settings.imageProvider ?? "dalle") as ImageProvider); setGeminiModel(settings.geminiModel ?? ""); - setSmtpHost(settings.smtpHost ?? ""); - setSmtpPort(settings.smtpPort ?? 587); - setSmtpUser(settings.smtpUser ?? ""); - setSmtpFrom(settings.smtpFrom ?? ""); - setSmtpTls(settings.smtpTls ?? true); setAnonymizationEnabled(settings.anonymizationEnabled ?? false); setAnonymizationDomain(settings.anonymizationDomain ?? "superhartmut.de"); setVacationDefaultDays(settings.vacationDefaultDays ?? 28); @@ -163,21 +148,6 @@ export function SystemSettingsClient() { onSuccess: (data) => setRecomputeResult(data), }); - const saveSmtpMutation = trpc.settings.updateSystemSettings.useMutation({ - onSuccess: () => { - setSmtpSaved(true); - setSmtpTestResult(null); - setLegacyCleanupResult(null); - invalidateSystemSettings(); - setTimeout(() => setSmtpSaved(false), 3000); - }, - }); - - const testSmtpMutation = trpc.settings.testSmtpConnection.useMutation({ - onSuccess: (data) => setSmtpTestResult(data), - onError: (error) => setSmtpTestResult({ ok: false, error: error.message }), - }); - const saveAnonymizationMutation = trpc.settings.updateSystemSettings.useMutation({ onSuccess: () => { setAnonymizationSaved(true); @@ -254,16 +224,6 @@ export function SystemSettingsClient() { }); } - function handleSaveSmtp() { - saveSmtpMutation.mutate({ - smtpHost: smtpHost || undefined, - smtpPort, - smtpUser: smtpUser || undefined, - smtpFrom: smtpFrom || undefined, - smtpTls, - }); - } - function handleSaveVacation() { saveVacationMutation.mutate({ vacationDefaultDays }); } @@ -292,8 +252,8 @@ export function SystemSettingsClient() { function handleClearLegacyRuntimeSecrets() { if ( - typeof window !== "undefined" - && !window.confirm( + typeof window !== "undefined" && + !window.confirm( "Clear all legacy runtime secrets from database storage? Environment-based deployment secrets must already be configured.", ) ) { @@ -423,25 +383,7 @@ export function SystemSettingsClient() { onTestGemini={() => testGeminiMutation.mutate()} /> - testSmtpMutation.mutate()} - /> + void; - onSmtpPortChange: (value: number) => void; - onSmtpUserChange: (value: string) => void; - onSmtpFromChange: (value: string) => void; - onSmtpTlsChange: (value: boolean) => void; - onSave: () => void; - onTest: () => void; + initialSettings: { + smtpHost: string | null; + smtpPort: number | null; + smtpUser: string | null; + smtpFrom: string | null; + smtpTls: boolean | null; + runtimeSecrets: { smtpPassword: RuntimeSecrets["smtpPassword"] }; + }; + onSettingsSaved: () => void; }; -export function SmtpSettingsPanel({ - smtpHost, - smtpPort, - smtpUser, - smtpFrom, - smtpTls, - smtpSaved, - smtpTestResult, - smtpSecret, - isSaving, - isTesting, - onSmtpHostChange, - onSmtpPortChange, - onSmtpUserChange, - onSmtpFromChange, - onSmtpTlsChange, - onSave, - onTest, -}: SmtpSettingsPanelProps) { +export function SmtpSettingsPanel({ initialSettings, onSettingsSaved }: SmtpSettingsPanelProps) { + const [smtpHost, setSmtpHost] = useState(""); + const [smtpPort, setSmtpPort] = useState(587); + const [smtpUser, setSmtpUser] = useState(""); + const [smtpFrom, setSmtpFrom] = useState(""); + const [smtpTls, setSmtpTls] = useState(true); + const [saved, setSaved] = useState(false); + const [testResult, setTestResult] = useState(null); + + useEffect(() => { + setSmtpHost(initialSettings.smtpHost ?? ""); + setSmtpPort(initialSettings.smtpPort ?? 587); + setSmtpUser(initialSettings.smtpUser ?? ""); + setSmtpFrom(initialSettings.smtpFrom ?? ""); + setSmtpTls(initialSettings.smtpTls ?? true); + }, [initialSettings]); + + const saveMutation = trpc.settings.updateSystemSettings.useMutation({ + onSuccess: () => { + setSaved(true); + setTestResult(null); + onSettingsSaved(); + setTimeout(() => setSaved(false), 3000); + }, + }); + + const testMutation = trpc.settings.testSmtpConnection.useMutation({ + onSuccess: (data) => setTestResult(data), + onError: (error) => setTestResult({ ok: false, error: error.message }), + }); + + function handleSave() { + saveMutation.mutate({ + smtpHost: smtpHost || undefined, + smtpPort, + smtpUser: smtpUser || undefined, + smtpFrom: smtpFrom || undefined, + smtpTls, + }); + } + return (
@@ -74,7 +90,7 @@ export function SmtpSettingsPanel({ type="text" className={INPUT_CLASS} value={smtpHost} - onChange={(event) => onSmtpHostChange(event.target.value)} + onChange={(event) => setSmtpHost(event.target.value)} placeholder="smtp.example.com" />
@@ -89,7 +105,7 @@ export function SmtpSettingsPanel({ type="number" className={INPUT_CLASS} value={smtpPort} - onChange={(event) => onSmtpPortChange(parseInt(event.target.value, 10))} + onChange={(event) => setSmtpPort(parseInt(event.target.value, 10))} min={1} max={65535} /> @@ -97,15 +113,14 @@ export function SmtpSettingsPanel({
onSmtpUserChange(event.target.value)} + onChange={(event) => setSmtpUser(event.target.value)} placeholder="user@example.com" autoComplete="off" /> @@ -121,7 +136,7 @@ export function SmtpSettingsPanel({ type="email" className={INPUT_CLASS} value={smtpFrom} - onChange={(event) => onSmtpFromChange(event.target.value)} + onChange={(event) => setSmtpFrom(event.target.value)} placeholder="noreply@capakraken.app" />
@@ -130,7 +145,7 @@ export function SmtpSettingsPanel({ type="checkbox" id="smtpTls" checked={smtpTls} - onChange={(event) => onSmtpTlsChange(event.target.checked)} + onChange={(event) => setSmtpTls(event.target.checked)} className="rounded border-gray-300 text-brand-600" />