m4: global API-error surface (no more silent failures)
The api client now catches network failures and non-JSON bodies robustly, and routes unexpected errors (offline / 5xx) to a global toast — 4xx stay with the caller so forms keep their inline messages. Toast actions are now optional (undo toasts keep their button; error toasts are message-only), and ToastHost moved from AppShell to the app root so toasts show everywhere, including the login screen. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
@@ -106,7 +106,8 @@ export const useNotesStore = defineStore("notes", () => {
|
||||
const setPinned = (id: string, pinned: boolean) => mutate(id, { pinned });
|
||||
const setArchived = async (id: string, archived: boolean): Promise<void> => {
|
||||
await mutate(id, { archived });
|
||||
if (archived) useUiStore().showToast("Note archived", "Undo", () => void setArchived(id, false));
|
||||
if (archived)
|
||||
useUiStore().showToast("Note archived", { label: "Undo", run: () => void setArchived(id, false) });
|
||||
};
|
||||
const setColor = (id: string, color: NoteColor) => mutate(id, { color });
|
||||
const setKind = (id: string, kind: NoteKind) => mutate(id, { kind });
|
||||
@@ -176,7 +177,7 @@ export const useNotesStore = defineStore("notes", () => {
|
||||
|
||||
async function trash(id: string): Promise<void> {
|
||||
reconcile(await api.post<Note>(`/api/notes/${id}/trash`));
|
||||
useUiStore().showToast("Note moved to trash", "Undo", () => void restore(id));
|
||||
useUiStore().showToast("Note moved to trash", { label: "Undo", run: () => void restore(id) });
|
||||
}
|
||||
|
||||
async function restore(id: string): Promise<void> {
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
|
||||
interface ToastAction {
|
||||
label: string;
|
||||
run: () => void;
|
||||
}
|
||||
|
||||
interface Toast {
|
||||
id: number;
|
||||
message: string;
|
||||
actionLabel: string;
|
||||
action: () => void;
|
||||
action?: ToastAction;
|
||||
}
|
||||
|
||||
// Cross-component UI signals that don't belong to any single view (e.g. a
|
||||
@@ -22,10 +26,10 @@ export const useUiStore = defineStore("ui", () => {
|
||||
let toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||||
let toastSeq = 0;
|
||||
|
||||
function showToast(message: string, actionLabel: string, action: () => void) {
|
||||
function showToast(message: string, action?: ToastAction) {
|
||||
toastSeq += 1;
|
||||
const id = toastSeq;
|
||||
toast.value = { id, message, actionLabel, action };
|
||||
toast.value = { id, message, action };
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => {
|
||||
if (toast.value?.id === id) toast.value = null;
|
||||
@@ -38,9 +42,9 @@ export const useUiStore = defineStore("ui", () => {
|
||||
}
|
||||
|
||||
function runToastAction() {
|
||||
const action = toast.value?.action;
|
||||
const run = toast.value?.action?.run;
|
||||
dismissToast();
|
||||
action?.();
|
||||
run?.();
|
||||
}
|
||||
|
||||
return { composeTick, requestCompose, toast, showToast, dismissToast, runToastAction };
|
||||
|
||||
Reference in New Issue
Block a user