Skip to content

Commit 88a53b2

Browse files
committed
feat: add success toast notification after saving settings
1 parent 1ceea13 commit 88a53b2

File tree

2 files changed

+100
-25
lines changed

2 files changed

+100
-25
lines changed

apps/desktop/src/features/settings/components/ProjectSettings.tsx

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { Agent, AuthOverview, ProviderModelOption } from "@opengoat/contracts";
22
import {
3-
CheckIcon,
43
CpuIcon,
54
GlobeIcon,
65
LoaderCircleIcon,
@@ -73,27 +72,24 @@ export function ProjectSettings({
7372
// ---- General form state ----
7473
const [name, setName] = useState(agent.name);
7574
const [isSaving, setIsSaving] = useState(false);
76-
const [saveMessage, setSaveMessage] = useState<{ type: "success" | "error"; text: string } | null>(null);
7775

7876
// Sync form when agent changes (e.g. switching projects)
7977
useEffect(() => {
8078
setName(agent.name);
81-
setSaveMessage(null);
8279
}, [agent.id, agent.name]);
8380

8481
const hasGeneralChanges = name !== agent.name;
8582

8683
const handleSaveGeneral = useCallback(async () => {
8784
setIsSaving(true);
88-
setSaveMessage(null);
8985

9086
try {
9187
const updated = await client.updateAgent(agent.id, { name });
9288
onAgentUpdated(updated);
93-
setSaveMessage({ type: "success", text: "Settings saved." });
89+
toast.success("Settings saved.");
9490
} catch (err) {
9591
console.error("Failed to save settings", err);
96-
setSaveMessage({ type: "error", text: "Failed to save. Please try again." });
92+
toast.error("Failed to save. Please try again.");
9793
} finally {
9894
setIsSaving(false);
9995
}
@@ -314,10 +310,7 @@ export function ProjectSettings({
314310
id="settings-name"
315311
className="h-9 text-[13px]"
316312
value={name}
317-
onChange={(e) => {
318-
setName(e.target.value);
319-
if (saveMessage) setSaveMessage(null);
320-
}}
313+
onChange={(e) => setName(e.target.value)}
321314
/>
322315
</div>
323316

@@ -338,21 +331,6 @@ export function ProjectSettings({
338331
</div>
339332
</div>
340333

341-
{saveMessage ? (
342-
<div
343-
className={`flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-[12px] ${
344-
saveMessage.type === "success"
345-
? "bg-success/8 text-success dark:bg-success/[0.06]"
346-
: "bg-destructive/8 text-destructive dark:bg-destructive/[0.06]"
347-
}`}
348-
>
349-
{saveMessage.type === "success" ? (
350-
<CheckIcon className="size-3 shrink-0" />
351-
) : null}
352-
{saveMessage.text}
353-
</div>
354-
) : null}
355-
356334
<div className="flex justify-end">
357335
<Button
358336
size="sm"
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import assert from "node:assert/strict";
2+
import test from "node:test";
3+
import { readFileSync } from "node:fs";
4+
import { resolve } from "node:path";
5+
6+
const src = readFileSync(
7+
resolve(import.meta.dirname, "ProjectSettings.tsx"),
8+
"utf-8",
9+
);
10+
11+
// ---------------------------------------------------------------------------
12+
// AC1: Clicking Save shows a visible success toast confirming the save
13+
// ---------------------------------------------------------------------------
14+
15+
void test("handleSaveGeneral calls toast.success on successful save", () => {
16+
assert.ok(
17+
src.includes("toast.success"),
18+
"Save handler must call toast.success() to show a success notification",
19+
);
20+
});
21+
22+
void test("Success toast message indicates settings were saved", () => {
23+
assert.ok(
24+
src.includes('toast.success("Settings saved'),
25+
"Success toast must display 'Settings saved' message",
26+
);
27+
});
28+
29+
// ---------------------------------------------------------------------------
30+
// AC2: The toast auto-dismisses (Sonner default is ~4s, acceptable)
31+
// ---------------------------------------------------------------------------
32+
33+
void test("Uses sonner toast which auto-dismisses by default", () => {
34+
assert.ok(
35+
src.includes('from "sonner"'),
36+
"Must import toast from sonner which provides auto-dismiss behavior",
37+
);
38+
});
39+
40+
// ---------------------------------------------------------------------------
41+
// AC3: If the save fails, an error toast appears instead
42+
// ---------------------------------------------------------------------------
43+
44+
void test("handleSaveGeneral calls toast.error on failed save", () => {
45+
assert.ok(
46+
src.includes("toast.error") && src.includes("Failed to save"),
47+
"Save handler must call toast.error() with a failure message on error",
48+
);
49+
});
50+
51+
// ---------------------------------------------------------------------------
52+
// AC4: No inline saveMessage display — replaced by toast
53+
// ---------------------------------------------------------------------------
54+
55+
void test("No inline saveMessage state for General save feedback", () => {
56+
assert.ok(
57+
!src.includes("setSaveMessage"),
58+
"Inline saveMessage state must be removed — feedback is via toast now",
59+
);
60+
});
61+
62+
void test("No inline save message div rendering", () => {
63+
assert.ok(
64+
!src.includes("saveMessage.type"),
65+
"Inline saveMessage rendering must be removed — feedback is via toast now",
66+
);
67+
});
68+
69+
// ---------------------------------------------------------------------------
70+
// AC5: Toast styling matches app's design language (uses existing Sonner config)
71+
// ---------------------------------------------------------------------------
72+
73+
void test("Uses the existing Sonner toast infrastructure (not a custom component)", () => {
74+
const toastImport = src.includes('import { toast } from "sonner"');
75+
assert.ok(
76+
toastImport,
77+
"Must use the existing sonner toast import for consistent styling",
78+
);
79+
});
80+
81+
// ---------------------------------------------------------------------------
82+
// AC6: No console errors — clean imports
83+
// ---------------------------------------------------------------------------
84+
85+
void test("CheckIcon import is removed if no longer used inline", () => {
86+
// CheckIcon was only used in the inline saveMessage success indicator.
87+
// If saveMessage is removed, CheckIcon should also be removed (unless used elsewhere).
88+
const checkIconUsages = src.match(/CheckIcon/g);
89+
// If CheckIcon is still imported, it must be used somewhere in JSX
90+
if (checkIconUsages) {
91+
const inJsx = src.includes("<CheckIcon");
92+
assert.ok(
93+
inJsx,
94+
"If CheckIcon is imported, it must be used in JSX — otherwise remove the unused import",
95+
);
96+
}
97+
});

0 commit comments

Comments
 (0)