From 6946ee4415c99e151ebc9d8ea7c64e24351c4eee Mon Sep 17 00:00:00 2001 From: claude_code Date: Tue, 23 Jun 2026 02:26:13 +0300 Subject: [PATCH] feat(ai-settings): add per-card "Save and test" button The single global "Save endpoints" button sat far below the fold and the per-card "Test endpoint" button probed the server-stored settings, so it ignored unsaved form edits. Replace each endpoint card's "Test endpoint" button with a combined "Save and test" button that persists the whole form first and only runs the card's connection probe on a successful save; the global "Save endpoints" button is kept for save-only. - Add handleSaveAndTest: save (rethrows on failure) then probe; skip the test if the save fails (the mutation already surfaces the error). - Add savingTestCapability state so only the clicked card spins during the shared save while all save controls stay disabled (no concurrent saves). - Reset the previous probe result when a new save+test starts. - Add the "Save and test" en-US translation key. Co-Authored-By: Claude Opus 4.8 --- .../public/locales/en-US/translation.json | 1 + .../components/ai-provider-settings.tsx | 56 ++++++++++++++++--- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index e1ac4abb..a4dd886b 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -1205,6 +1205,7 @@ "Transcribe as you speak, cutting on pauses": "Transcribe as you speak, cutting on pauses", "Voice dictation is not available yet.": "Voice dictation is not available yet.", "Test endpoint": "Test endpoint", + "Save and test": "Save and test", "Save endpoints": "Save endpoints", "Configured and enabled": "Configured and enabled", "Configured but disabled": "Configured but disabled", diff --git a/apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx b/apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx index f57348a1..713d9b65 100644 --- a/apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx +++ b/apps/client/src/features/workspace/components/settings/components/ai-provider-settings.tsx @@ -35,6 +35,7 @@ import { useUpdateAiSettingsMutation, } from "@/features/workspace/queries/ai-settings-query.ts"; import { + AiTestCapability, IAiSettingsUpdate, SttApiStyle, } from "@/features/workspace/services/ai-settings-service.ts"; @@ -252,6 +253,12 @@ export default function AiProviderSettings() { const embedTest = useTestAiConnectionMutation(); const sttTest = useTestAiConnectionMutation(); + // Which card's "Save and test" is currently mid-save. The save mutation is + // shared, so without this every save-and-test button would spin at once; + // this lets only the clicked card's button show the spinner during the save. + const [savingTestCapability, setSavingTestCapability] = + useState(null); + // Agent roles drive the public-share assistant identity picker. Admin-gated // (the component returns early for non-admins), same as the AI settings query. const { data: roles } = useAiRolesQuery(isAdmin); @@ -408,6 +415,28 @@ export default function AiProviderSettings() { form.resetDirty(); } + // "Save and test" for a single card: the connection test probes the + // SERVER-STORED settings, so the whole form must be persisted before testing. + // Save first (handleSubmit rethrows on failure and the mutation already shows + // its own error notification); only run the probe on a successful save. + async function handleSaveAndTest( + capability: AiTestCapability, + test: ReturnType, + ) { + setSavingTestCapability(capability); + // Clear any previous probe result so the stale "successful/failed" text does + // not linger next to the spinner while the (now preceding) save runs. + test.reset(); + try { + await handleSubmit(form.values); + } catch { + return; // save failed — error already surfaced; do not test stale settings + } finally { + setSavingTestCapability(null); + } + test.mutate(capability); + } + function handleClearKey() { setKeyCleared(true); setHasApiKey(false); @@ -780,10 +809,13 @@ export default function AiProviderSettings() { {chatTest.data && (chatTest.data.ok ? ( @@ -905,10 +937,13 @@ export default function AiProviderSettings() { {embedTest.data && (embedTest.data.ok ? ( @@ -1099,10 +1134,13 @@ export default function AiProviderSettings() { {sttTest.data && (sttTest.data.ok ? (