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 <noreply@anthropic.com>
This commit is contained in:
@@ -3,10 +3,7 @@
|
|||||||
import { DEFAULT_OPENAI_MODEL } from "@capakraken/shared";
|
import { DEFAULT_OPENAI_MODEL } from "@capakraken/shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { trpc } from "~/lib/trpc/client.js";
|
import { trpc } from "~/lib/trpc/client.js";
|
||||||
import {
|
import { AiProviderPanel, GenerationSettingsPanel } from "./system-settings/AiSettingsPanels.js";
|
||||||
AiProviderPanel,
|
|
||||||
GenerationSettingsPanel,
|
|
||||||
} from "./system-settings/AiSettingsPanels.js";
|
|
||||||
import { LegacyRuntimeSecretsNotice } from "./system-settings/LegacyRuntimeSecretsNotice.js";
|
import { LegacyRuntimeSecretsNotice } from "./system-settings/LegacyRuntimeSecretsNotice.js";
|
||||||
import {
|
import {
|
||||||
type ImageProvider,
|
type ImageProvider,
|
||||||
@@ -52,13 +49,6 @@ export function SystemSettingsClient() {
|
|||||||
const [imageProvider, setImageProvider] = useState<ImageProvider>("dalle");
|
const [imageProvider, setImageProvider] = useState<ImageProvider>("dalle");
|
||||||
const [geminiModel, setGeminiModel] = useState("");
|
const [geminiModel, setGeminiModel] = useState("");
|
||||||
const [imageSaved, setImageSaved] = useState(false);
|
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<SaveResult | null>(null);
|
|
||||||
const [anonymizationEnabled, setAnonymizationEnabled] = useState(false);
|
const [anonymizationEnabled, setAnonymizationEnabled] = useState(false);
|
||||||
const [anonymizationDomain, setAnonymizationDomain] = useState("superhartmut.de");
|
const [anonymizationDomain, setAnonymizationDomain] = useState("superhartmut.de");
|
||||||
const [anonymizationSaved, setAnonymizationSaved] = useState(false);
|
const [anonymizationSaved, setAnonymizationSaved] = useState(false);
|
||||||
@@ -96,11 +86,6 @@ export function SystemSettingsClient() {
|
|||||||
setDalleEndpoint(settings.azureDalleEndpoint ?? "");
|
setDalleEndpoint(settings.azureDalleEndpoint ?? "");
|
||||||
setImageProvider((settings.imageProvider ?? "dalle") as ImageProvider);
|
setImageProvider((settings.imageProvider ?? "dalle") as ImageProvider);
|
||||||
setGeminiModel(settings.geminiModel ?? "");
|
setGeminiModel(settings.geminiModel ?? "");
|
||||||
setSmtpHost(settings.smtpHost ?? "");
|
|
||||||
setSmtpPort(settings.smtpPort ?? 587);
|
|
||||||
setSmtpUser(settings.smtpUser ?? "");
|
|
||||||
setSmtpFrom(settings.smtpFrom ?? "");
|
|
||||||
setSmtpTls(settings.smtpTls ?? true);
|
|
||||||
setAnonymizationEnabled(settings.anonymizationEnabled ?? false);
|
setAnonymizationEnabled(settings.anonymizationEnabled ?? false);
|
||||||
setAnonymizationDomain(settings.anonymizationDomain ?? "superhartmut.de");
|
setAnonymizationDomain(settings.anonymizationDomain ?? "superhartmut.de");
|
||||||
setVacationDefaultDays(settings.vacationDefaultDays ?? 28);
|
setVacationDefaultDays(settings.vacationDefaultDays ?? 28);
|
||||||
@@ -163,21 +148,6 @@ export function SystemSettingsClient() {
|
|||||||
onSuccess: (data) => setRecomputeResult(data),
|
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({
|
const saveAnonymizationMutation = trpc.settings.updateSystemSettings.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setAnonymizationSaved(true);
|
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() {
|
function handleSaveVacation() {
|
||||||
saveVacationMutation.mutate({ vacationDefaultDays });
|
saveVacationMutation.mutate({ vacationDefaultDays });
|
||||||
}
|
}
|
||||||
@@ -292,8 +252,8 @@ export function SystemSettingsClient() {
|
|||||||
|
|
||||||
function handleClearLegacyRuntimeSecrets() {
|
function handleClearLegacyRuntimeSecrets() {
|
||||||
if (
|
if (
|
||||||
typeof window !== "undefined"
|
typeof window !== "undefined" &&
|
||||||
&& !window.confirm(
|
!window.confirm(
|
||||||
"Clear all legacy runtime secrets from database storage? Environment-based deployment secrets must already be configured.",
|
"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()}
|
onTestGemini={() => testGeminiMutation.mutate()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SmtpSettingsPanel
|
<SmtpSettingsPanel initialSettings={settings} onSettingsSaved={invalidateSystemSettings} />
|
||||||
smtpHost={smtpHost}
|
|
||||||
smtpPort={smtpPort}
|
|
||||||
smtpUser={smtpUser}
|
|
||||||
smtpFrom={smtpFrom}
|
|
||||||
smtpTls={smtpTls}
|
|
||||||
smtpSaved={smtpSaved}
|
|
||||||
smtpTestResult={smtpTestResult}
|
|
||||||
smtpSecret={settings.runtimeSecrets.smtpPassword}
|
|
||||||
isSaving={saveSmtpMutation.isPending}
|
|
||||||
isTesting={testSmtpMutation.isPending}
|
|
||||||
onSmtpHostChange={setSmtpHost}
|
|
||||||
onSmtpPortChange={setSmtpPort}
|
|
||||||
onSmtpUserChange={setSmtpUser}
|
|
||||||
onSmtpFromChange={setSmtpFrom}
|
|
||||||
onSmtpTlsChange={setSmtpTls}
|
|
||||||
onSave={handleSaveSmtp}
|
|
||||||
onTest={() => testSmtpMutation.mutate()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<VacationSettingsPanel
|
<VacationSettingsPanel
|
||||||
vacationDefaultDays={vacationDefaultDays}
|
vacationDefaultDays={vacationDefaultDays}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { trpc } from "~/lib/trpc/client.js";
|
||||||
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
|
import { InfoTooltip } from "~/components/ui/InfoTooltip.js";
|
||||||
import {
|
import {
|
||||||
CHECKBOX_ROW_CLASS,
|
CHECKBOX_ROW_CLASS,
|
||||||
@@ -12,44 +14,58 @@ import {
|
|||||||
} from "./shared.js";
|
} from "./shared.js";
|
||||||
|
|
||||||
type SmtpSettingsPanelProps = {
|
type SmtpSettingsPanelProps = {
|
||||||
smtpHost: string;
|
initialSettings: {
|
||||||
smtpPort: number;
|
smtpHost: string | null;
|
||||||
smtpUser: string;
|
smtpPort: number | null;
|
||||||
smtpFrom: string;
|
smtpUser: string | null;
|
||||||
smtpTls: boolean;
|
smtpFrom: string | null;
|
||||||
smtpSaved: boolean;
|
smtpTls: boolean | null;
|
||||||
smtpTestResult: SaveResult | null;
|
runtimeSecrets: { smtpPassword: RuntimeSecrets["smtpPassword"] };
|
||||||
smtpSecret: RuntimeSecrets["smtpPassword"];
|
};
|
||||||
isSaving: boolean;
|
onSettingsSaved: () => void;
|
||||||
isTesting: boolean;
|
|
||||||
onSmtpHostChange: (value: string) => void;
|
|
||||||
onSmtpPortChange: (value: number) => void;
|
|
||||||
onSmtpUserChange: (value: string) => void;
|
|
||||||
onSmtpFromChange: (value: string) => void;
|
|
||||||
onSmtpTlsChange: (value: boolean) => void;
|
|
||||||
onSave: () => void;
|
|
||||||
onTest: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SmtpSettingsPanel({
|
export function SmtpSettingsPanel({ initialSettings, onSettingsSaved }: SmtpSettingsPanelProps) {
|
||||||
smtpHost,
|
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<SaveResult | null>(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,
|
smtpPort,
|
||||||
smtpUser,
|
smtpUser: smtpUser || undefined,
|
||||||
smtpFrom,
|
smtpFrom: smtpFrom || undefined,
|
||||||
smtpTls,
|
smtpTls,
|
||||||
smtpSaved,
|
});
|
||||||
smtpTestResult,
|
}
|
||||||
smtpSecret,
|
|
||||||
isSaving,
|
|
||||||
isTesting,
|
|
||||||
onSmtpHostChange,
|
|
||||||
onSmtpPortChange,
|
|
||||||
onSmtpUserChange,
|
|
||||||
onSmtpFromChange,
|
|
||||||
onSmtpTlsChange,
|
|
||||||
onSave,
|
|
||||||
onTest,
|
|
||||||
}: SmtpSettingsPanelProps) {
|
|
||||||
return (
|
return (
|
||||||
<div className={PANEL_CLASS}>
|
<div className={PANEL_CLASS}>
|
||||||
<div>
|
<div>
|
||||||
@@ -74,7 +90,7 @@ export function SmtpSettingsPanel({
|
|||||||
type="text"
|
type="text"
|
||||||
className={INPUT_CLASS}
|
className={INPUT_CLASS}
|
||||||
value={smtpHost}
|
value={smtpHost}
|
||||||
onChange={(event) => onSmtpHostChange(event.target.value)}
|
onChange={(event) => setSmtpHost(event.target.value)}
|
||||||
placeholder="smtp.example.com"
|
placeholder="smtp.example.com"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,7 +105,7 @@ export function SmtpSettingsPanel({
|
|||||||
type="number"
|
type="number"
|
||||||
className={INPUT_CLASS}
|
className={INPUT_CLASS}
|
||||||
value={smtpPort}
|
value={smtpPort}
|
||||||
onChange={(event) => onSmtpPortChange(parseInt(event.target.value, 10))}
|
onChange={(event) => setSmtpPort(parseInt(event.target.value, 10))}
|
||||||
min={1}
|
min={1}
|
||||||
max={65535}
|
max={65535}
|
||||||
/>
|
/>
|
||||||
@@ -97,15 +113,14 @@ export function SmtpSettingsPanel({
|
|||||||
<div>
|
<div>
|
||||||
<label className={LABEL_CLASS}>
|
<label className={LABEL_CLASS}>
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
SMTP Username{" "}
|
SMTP Username <InfoTooltip content="Authentication username for the SMTP server." />
|
||||||
<InfoTooltip content="Authentication username for the SMTP server." />
|
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className={INPUT_CLASS}
|
className={INPUT_CLASS}
|
||||||
value={smtpUser}
|
value={smtpUser}
|
||||||
onChange={(event) => onSmtpUserChange(event.target.value)}
|
onChange={(event) => setSmtpUser(event.target.value)}
|
||||||
placeholder="user@example.com"
|
placeholder="user@example.com"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
@@ -121,7 +136,7 @@ export function SmtpSettingsPanel({
|
|||||||
type="email"
|
type="email"
|
||||||
className={INPUT_CLASS}
|
className={INPUT_CLASS}
|
||||||
value={smtpFrom}
|
value={smtpFrom}
|
||||||
onChange={(event) => onSmtpFromChange(event.target.value)}
|
onChange={(event) => setSmtpFrom(event.target.value)}
|
||||||
placeholder="noreply@capakraken.app"
|
placeholder="noreply@capakraken.app"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,7 +145,7 @@ export function SmtpSettingsPanel({
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
id="smtpTls"
|
id="smtpTls"
|
||||||
checked={smtpTls}
|
checked={smtpTls}
|
||||||
onChange={(event) => onSmtpTlsChange(event.target.checked)}
|
onChange={(event) => setSmtpTls(event.target.checked)}
|
||||||
className="rounded border-gray-300 text-brand-600"
|
className="rounded border-gray-300 text-brand-600"
|
||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
@@ -145,39 +160,39 @@ export function SmtpSettingsPanel({
|
|||||||
<RuntimeSecretCard
|
<RuntimeSecretCard
|
||||||
title="SMTP Password"
|
title="SMTP Password"
|
||||||
description="SMTP credentials are provisioned outside the application and injected at runtime."
|
description="SMTP credentials are provisioned outside the application and injected at runtime."
|
||||||
secret={smtpSecret}
|
secret={initialSettings.runtimeSecrets.smtpPassword}
|
||||||
optionalNote="Provision SMTP_PASSWORD in the deployment target used by the API service."
|
optionalNote="Provision SMTP_PASSWORD in the deployment target used by the API service."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onSave}
|
onClick={handleSave}
|
||||||
disabled={isSaving}
|
disabled={saveMutation.isPending}
|
||||||
className={PRIMARY_BUTTON_CLASS}
|
className={PRIMARY_BUTTON_CLASS}
|
||||||
>
|
>
|
||||||
{isSaving ? "Saving…" : "Save SMTP Settings"}
|
{saveMutation.isPending ? "Saving\u2026" : "Save SMTP Settings"}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onTest}
|
onClick={() => testMutation.mutate()}
|
||||||
disabled={isTesting}
|
disabled={testMutation.isPending}
|
||||||
className={SECONDARY_BUTTON_CLASS}
|
className={SECONDARY_BUTTON_CLASS}
|
||||||
>
|
>
|
||||||
{isTesting ? "Testing…" : "Test Connection"}
|
{testMutation.isPending ? "Testing\u2026" : "Test Connection"}
|
||||||
</button>
|
</button>
|
||||||
{smtpSaved ? (
|
{saved ? (
|
||||||
<span className="text-sm font-medium text-green-600 dark:text-green-400">Saved!</span>
|
<span className="text-sm font-medium text-green-600 dark:text-green-400">Saved!</span>
|
||||||
) : null}
|
) : null}
|
||||||
{smtpTestResult ? (
|
{testResult ? (
|
||||||
<span
|
<span
|
||||||
className={`text-sm font-medium ${
|
className={`text-sm font-medium ${
|
||||||
smtpTestResult.ok
|
testResult.ok
|
||||||
? "text-green-600 dark:text-green-400"
|
? "text-green-600 dark:text-green-400"
|
||||||
: "text-red-500 dark:text-red-400"
|
: "text-red-500 dark:text-red-400"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{smtpTestResult.ok ? "✓ Connection successful" : `✗ ${smtpTestResult.error}`}
|
{testResult.ok ? "\u2713 Connection successful" : `\u2717 ${testResult.error}`}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user