feat(task): undo toast for completing — mis-taps are one tap to take back
completeTask returns an undo closure that restores status + sort slot and trashes the occurrence a repeat spawned. One global snackbar (5s, replace-on-new) mounted in the root layout; every checkbox and Clarify's save-as-done go through completeWithUndo. bun tests for the store. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YE5FmDZmFnWxH6RXCZLcjj
This commit is contained in:
@@ -136,6 +136,11 @@ export function createTask(data: Partial<Task> & { title: string }): Task {
|
||||
}
|
||||
|
||||
export function updateTask(id: string, patch: Partial<Task>) {
|
||||
applyTaskUpdate(id, patch);
|
||||
}
|
||||
|
||||
/** Applies a patch; returns the next occurrence if completing spawned one. */
|
||||
function applyTaskUpdate(id: string, patch: Partial<Task>): Task | null {
|
||||
const before = state.tasks.find((t) => t.id === id);
|
||||
setState({
|
||||
tasks: state.tasks.map((t) => {
|
||||
@@ -152,11 +157,28 @@ export function updateTask(id: string, patch: Partial<Task>) {
|
||||
// done row later leaves the spawn in place — it's one tap to trash, versus
|
||||
// silently deleting a row the user may have edited.
|
||||
if (before && before.repeat && before.status !== "done" && patch.status === "done") {
|
||||
spawnNextOccurrence(before);
|
||||
return spawnNextOccurrence(before);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function spawnNextOccurrence(t: Task) {
|
||||
/**
|
||||
* Complete a task and hand back an undo. Undo puts the task back where it was
|
||||
* (same status and sort slot) and trashes the occurrence a repeat spawned, so
|
||||
* a mis-tap leaves no trace. Returns null when there's nothing to complete.
|
||||
*/
|
||||
export function completeTask(id: string, revertTo?: Task["status"]): (() => void) | null {
|
||||
const before = state.tasks.find((t) => t.id === id);
|
||||
if (!before || before.status === "done") return null;
|
||||
const spawned = applyTaskUpdate(id, { status: "done" });
|
||||
const back = revertTo ?? before.status;
|
||||
return () => {
|
||||
updateTask(id, { status: back });
|
||||
if (spawned) trashTask(spawned.id);
|
||||
};
|
||||
}
|
||||
|
||||
function spawnNextOccurrence(t: Task): Task {
|
||||
const today = todayIso();
|
||||
const due = t.due_date ? nextOccurrence(t.due_date, t.repeat!, today) : null;
|
||||
// Keep the same lead time between defer and due (e.g. "surfaces 3 days before").
|
||||
@@ -168,7 +190,7 @@ function spawnNextOccurrence(t: Task) {
|
||||
: t.defer_date
|
||||
? nextOccurrence(t.defer_date, t.repeat!, today)
|
||||
: null;
|
||||
createTask({
|
||||
return createTask({
|
||||
title: t.title,
|
||||
notes: t.notes,
|
||||
status: t.status,
|
||||
|
||||
Reference in New Issue
Block a user