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 <noreply@anthropic.com>
This commit is contained in:
claude_code
2026-06-23 02:26:13 +03:00
parent 7c308728de
commit 6946ee4415
2 changed files with 48 additions and 9 deletions

View File

@@ -1205,6 +1205,7 @@
"Transcribe as you speak, cutting on pauses": "Transcribe as you speak, cutting on pauses", "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.", "Voice dictation is not available yet.": "Voice dictation is not available yet.",
"Test endpoint": "Test endpoint", "Test endpoint": "Test endpoint",
"Save and test": "Save and test",
"Save endpoints": "Save endpoints", "Save endpoints": "Save endpoints",
"Configured and enabled": "Configured and enabled", "Configured and enabled": "Configured and enabled",
"Configured but disabled": "Configured but disabled", "Configured but disabled": "Configured but disabled",

View File

@@ -35,6 +35,7 @@ import {
useUpdateAiSettingsMutation, useUpdateAiSettingsMutation,
} from "@/features/workspace/queries/ai-settings-query.ts"; } from "@/features/workspace/queries/ai-settings-query.ts";
import { import {
AiTestCapability,
IAiSettingsUpdate, IAiSettingsUpdate,
SttApiStyle, SttApiStyle,
} from "@/features/workspace/services/ai-settings-service.ts"; } from "@/features/workspace/services/ai-settings-service.ts";
@@ -252,6 +253,12 @@ export default function AiProviderSettings() {
const embedTest = useTestAiConnectionMutation(); const embedTest = useTestAiConnectionMutation();
const sttTest = 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<AiTestCapability | null>(null);
// Agent roles drive the public-share assistant identity picker. Admin-gated // Agent roles drive the public-share assistant identity picker. Admin-gated
// (the component returns early for non-admins), same as the AI settings query. // (the component returns early for non-admins), same as the AI settings query.
const { data: roles } = useAiRolesQuery(isAdmin); const { data: roles } = useAiRolesQuery(isAdmin);
@@ -408,6 +415,28 @@ export default function AiProviderSettings() {
form.resetDirty(); 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<typeof useTestAiConnectionMutation>,
) {
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() { function handleClearKey() {
setKeyCleared(true); setKeyCleared(true);
setHasApiKey(false); setHasApiKey(false);
@@ -780,10 +809,13 @@ export default function AiProviderSettings() {
<Button <Button
variant="default" variant="default"
size="sm" size="sm"
loading={chatTest.isPending} loading={savingTestCapability === "chat" || chatTest.isPending}
onClick={() => chatTest.mutate("chat")} disabled={
updateMutation.isPending || chatTest.isPending || !form.isValid()
}
onClick={() => void handleSaveAndTest("chat", chatTest)}
> >
{t("Test endpoint")} {t("Save and test")}
</Button> </Button>
{chatTest.data && {chatTest.data &&
(chatTest.data.ok ? ( (chatTest.data.ok ? (
@@ -905,10 +937,13 @@ export default function AiProviderSettings() {
<Button <Button
variant="default" variant="default"
size="sm" size="sm"
loading={embedTest.isPending} loading={savingTestCapability === "embeddings" || embedTest.isPending}
onClick={() => embedTest.mutate("embeddings")} disabled={
updateMutation.isPending || embedTest.isPending || !form.isValid()
}
onClick={() => void handleSaveAndTest("embeddings", embedTest)}
> >
{t("Test endpoint")} {t("Save and test")}
</Button> </Button>
{embedTest.data && {embedTest.data &&
(embedTest.data.ok ? ( (embedTest.data.ok ? (
@@ -1099,10 +1134,13 @@ export default function AiProviderSettings() {
<Button <Button
variant="default" variant="default"
size="sm" size="sm"
loading={sttTest.isPending} loading={savingTestCapability === "stt" || sttTest.isPending}
onClick={() => sttTest.mutate("stt")} disabled={
updateMutation.isPending || sttTest.isPending || !form.isValid()
}
onClick={() => void handleSaveAndTest("stt", sttTest)}
> >
{t("Test endpoint")} {t("Save and test")}
</Button> </Button>
{sttTest.data && {sttTest.data &&
(sttTest.data.ok ? ( (sttTest.data.ok ? (