Skip to content

Commit 2b043ac

Browse files
committed
feat: add saving state and error handling to NoteWidget, update localization for save messages
1 parent 32421a5 commit 2b043ac

5 files changed

Lines changed: 46 additions & 6 deletions

File tree

src/i18n/locales/en/widgets.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@
116116
"untitled": "Untitled note",
117117
"savedAt": "Saved at",
118118
"unsaved": "Unsaved changes",
119+
"saving": "Saving...",
120+
"saveFailed": "Could not save changes",
119121
"recovered": "Notes restored from backup",
120122
"restore": "Restore from backup"
121123
},

src/i18n/locales/zh-CN/widgets.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@
116116
"untitled": "未命名便签",
117117
"savedAt": "已保存于",
118118
"unsaved": "未保存的更改",
119+
"saving": "保存中...",
120+
"saveFailed": "保存失败",
119121
"recovered": "已从备份恢复便签",
120122
"restore": "从备份恢复"
121123
},

src/i18n/locales/zh-TW/widgets.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@
116116
"untitled": "未命名便箋",
117117
"savedAt": "儲存於",
118118
"unsaved": "有未儲存的變更",
119+
"saving": "儲存中...",
120+
"saveFailed": "無法儲存變更",
119121
"recovered": "便箋已從備份還原",
120122
"restore": "從備份還原"
121123
},

src/widgets/NoteWidget.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export default function NoteWidget({ widgetId }: Props) {
5757
const [selectedId, setSelectedId] = useState<string>("");
5858
const [draft, setDraft] = useState<string>("");
5959
const [lastSavedAt, setLastSavedAt] = useState<Date | null>(null);
60+
const [saveState, setSaveState] = useState<"idle" | "saving" | "error">("idle");
6061
const [showRecovered, setShowRecovered] = useState(false);
6162
const [autoBlur, setAutoBlur] = useState(() => {
6263
try {
@@ -160,8 +161,9 @@ export default function NoteWidget({ widgetId }: Props) {
160161
client.setState(BACKUP_KEY, serializeNotes(nextNotes)),
161162
]);
162163
setLastSavedAt(new Date());
164+
return true;
163165
} catch {
164-
// Keep local state even if gateway write fails.
166+
return false;
165167
}
166168
}, [client]);
167169

@@ -250,12 +252,14 @@ export default function NoteWidget({ widgetId }: Props) {
250252

251253
const saveCurrent = async () => {
252254
if (!selectedNote) return;
255+
setSaveState("saving");
253256
const next = notes.map((n) =>
254257
n.id === selectedNote.id
255258
? { ...n, content: draft, updatedAt: new Date().toISOString() }
256259
: n
257260
);
258-
await persist(next);
261+
const saved = await persist(next);
262+
setSaveState(saved ? "idle" : "error");
259263
};
260264

261265
const deleteCurrent = async () => {
@@ -387,22 +391,32 @@ export default function NoteWidget({ widgetId }: Props) {
387391
<>
388392
<textarea
389393
value={draft}
390-
onChange={(e) => setDraft(e.target.value)}
394+
onChange={(e) => {
395+
setDraft(e.target.value);
396+
setLastSavedAt(null);
397+
setSaveState("idle");
398+
}}
391399
placeholder={t("note.placeholder")}
392400
className="ui-field flex-1 min-h-0 resize-none leading-relaxed"
393401
/>
394402
<div className="flex justify-between items-center">
395-
<span className={clsx("text-[11px] transition-colors", lastSavedAt ? "text-accent-green" : "text-text-muted")}>
396-
{lastSavedAt
403+
<span className={clsx(
404+
"text-[11px] transition-colors",
405+
saveState === "error" ? "text-accent-red" : lastSavedAt ? "text-accent-green" : "text-text-muted"
406+
)}>
407+
{saveState === "error"
408+
? t("note.saveFailed")
409+
: lastSavedAt
397410
? `${t("note.savedAt")} ${lastSavedAt.toLocaleTimeString()}`
398411
: t("note.unsaved")}
399412
</span>
400413
<button
401414
onClick={() => void saveCurrent()}
415+
disabled={saveState === "saving"}
402416
className="inline-flex items-center gap-1.5 text-xs px-2.5 py-1 rounded-lg border border-accent-blue/50 text-accent-blue hover:bg-accent-blue/10 transition-colors"
403417
>
404418
<Save size={12} />
405-
{t("note.save")}
419+
{saveState === "saving" ? t("note.saving") : t("note.save")}
406420
</button>
407421
</div>
408422
</>

src/widgets/__tests__/NoteWidget.test.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,26 @@ describe("NoteWidget", () => {
7171
window.removeEventListener("timelens-widget-auto-blur-changed", onAutoBlurChanged);
7272
});
7373

74+
it("saves edited note content when Save is clicked", async () => {
75+
const notes = JSON.stringify([
76+
{ id: "n1", content: "First note", updatedAt: new Date().toISOString() },
77+
]);
78+
mockGatewayState({ notes, notes_backup: notes });
79+
renderWithProviders(<NoteWidget widgetId="note-test" />);
80+
81+
const editor = await screen.findByRole("textbox");
82+
await userEvent.type(editor, " updated");
83+
await userEvent.click(screen.getByRole("button", { name: "Save" }));
84+
85+
await waitFor(() => {
86+
const writeCalls = mockTauriApi.widgetGatewayRequest.mock.calls.filter(
87+
(call) => call[0].request_type === "state_write"
88+
);
89+
expect(writeCalls).toHaveLength(2);
90+
});
91+
expect(screen.getByText(/^Saved at/)).toBeInTheDocument();
92+
});
93+
7494
it("deletes the current note", async () => {
7595
const notes = JSON.stringify([
7696
{ id: "n1", content: "First note", updatedAt: new Date().toISOString() },

0 commit comments

Comments
 (0)