ui: undo toast after trash / archive
CI & Build / Python lint (push) Successful in 3s
CI & Build / TypeScript typecheck (push) Successful in 5s
CI & Build / Python tests (push) Successful in 8s
CI & Build / Build & push image (push) Successful in 29s

A global ToastHost (mounted in the app shell) shows a transient toast with
an Undo action, driven by the ui store (showToast/dismiss/runAction, 5s
auto-dismiss). The notes store fires it centrally: trash → Undo restores;
archive → Undo unarchives — so it works from card, editor, and keyboard.

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:
2026-07-20 13:15:19 -04:00
co-authored by Claude Opus 4.8
parent f321c2bf70
commit 9baa9217c7
4 changed files with 81 additions and 2 deletions
+6 -1
View File
@@ -1,6 +1,7 @@
import { defineStore } from "pinia";
import { ref } from "vue";
import { api } from "../api/client";
import { useUiStore } from "./ui";
import type { NoteColor } from "../notes/colors";
export type NoteView = "active" | "archived" | "trash";
@@ -103,7 +104,10 @@ export const useNotesStore = defineStore("notes", () => {
}
const setPinned = (id: string, pinned: boolean) => mutate(id, { pinned });
const setArchived = (id: string, archived: boolean) => mutate(id, { archived });
const setArchived = async (id: string, archived: boolean): Promise<void> => {
await mutate(id, { archived });
if (archived) useUiStore().showToast("Note archived", "Undo", () => void setArchived(id, false));
};
const setColor = (id: string, color: NoteColor) => mutate(id, { color });
const setKind = (id: string, kind: NoteKind) => mutate(id, { kind });
const setReminder = (id: string, remindAt: string | null) => mutate(id, { remind_at: remindAt });
@@ -172,6 +176,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));
}
async function restore(id: string): Promise<void> {
+34 -1
View File
@@ -1,6 +1,13 @@
import { defineStore } from "pinia";
import { ref } from "vue";
interface Toast {
id: number;
message: string;
actionLabel: string;
action: () => void;
}
// Cross-component UI signals that don't belong to any single view (e.g. a
// global shortcut in the app shell asking the board to act).
export const useUiStore = defineStore("ui", () => {
@@ -10,5 +17,31 @@ export const useUiStore = defineStore("ui", () => {
composeTick.value++;
}
return { composeTick, requestCompose };
// Transient undo toast (e.g. after trash/archive).
const toast = ref<Toast | null>(null);
let toastTimer: ReturnType<typeof setTimeout> | undefined;
let toastSeq = 0;
function showToast(message: string, actionLabel: string, action: () => void) {
toastSeq += 1;
const id = toastSeq;
toast.value = { id, message, actionLabel, action };
clearTimeout(toastTimer);
toastTimer = setTimeout(() => {
if (toast.value?.id === id) toast.value = null;
}, 5000);
}
function dismissToast() {
clearTimeout(toastTimer);
toast.value = null;
}
function runToastAction() {
const action = toast.value?.action;
dismissToast();
action?.();
}
return { composeTick, requestCompose, toast, showToast, dismissToast, runToastAction };
});