fix: harden frontend — toggle race guard, fresh debounce snapshot, error surfacing
Add toggling guard to TaskItem preventing double-toggle from rapid clicks or swipes. Fix debounced save in TaskDetailView to read task at timeout time instead of capturing a stale snapshot at call time. Remove unused _unlistenFs variable. Surface reorder_task and watch_workspace errors instead of silently swallowing them.
This commit is contained in:
parent
3c11539f02
commit
7fde1a09f9
|
|
@ -34,9 +34,8 @@
|
|||
|
||||
function debouncedSave(fields: Partial<Task>) {
|
||||
clearTimeout(saveTimer);
|
||||
const snapshot = { ...task };
|
||||
saveTimer = setTimeout(() => {
|
||||
app.updateTask({ ...snapshot, ...fields, updated_at: new Date().toISOString() });
|
||||
app.updateTask({ ...task, ...fields, updated_at: new Date().toISOString() });
|
||||
}, 400);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
let isCompleted = $derived(task.status === "completed");
|
||||
let justChecked = $state(false);
|
||||
let toggling = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
const _ = task.status;
|
||||
|
|
@ -35,6 +36,8 @@
|
|||
|
||||
async function handleToggle(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
if (toggling) return;
|
||||
toggling = true;
|
||||
justChecked = true;
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
transitioning = true;
|
||||
|
|
@ -42,6 +45,7 @@
|
|||
await new Promise((r) => setTimeout(r, 200));
|
||||
justChecked = false;
|
||||
await app.toggleTask(task.id);
|
||||
toggling = false;
|
||||
}
|
||||
|
||||
function handleTouchStart(e: TouchEvent) {
|
||||
|
|
@ -57,14 +61,15 @@
|
|||
}
|
||||
|
||||
function handleTouchEnd() {
|
||||
if (Math.abs(swipeX) > 100) {
|
||||
if (Math.abs(swipeX) > 100 && !toggling) {
|
||||
swipeX = 0;
|
||||
swiping = false;
|
||||
toggling = true;
|
||||
justChecked = true;
|
||||
setTimeout(() => {
|
||||
transitioning = true;
|
||||
animateInIds.add(task.id);
|
||||
setTimeout(() => { justChecked = false; app.toggleTask(task.id); }, 200);
|
||||
setTimeout(() => { justChecked = false; app.toggleTask(task.id).finally(() => { toggling = false; }); }, 200);
|
||||
}, 300);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,12 +9,8 @@ import type {
|
|||
} from "../types";
|
||||
|
||||
// Listen for file system changes from the backend watcher.
|
||||
// Store the unlisten function so it can be cleaned up if needed.
|
||||
let _unlistenFs: (() => void) | null = null;
|
||||
listen("fs-changed", () => {
|
||||
loadLists();
|
||||
}).then((unlisten) => {
|
||||
_unlistenFs = unlisten;
|
||||
});
|
||||
|
||||
// ── Reactive state ───────────────────────────────────────────────────
|
||||
|
|
@ -98,7 +94,7 @@ async function switchWorkspace(name: string) {
|
|||
activeListId = null;
|
||||
await loadLists();
|
||||
const ws = config?.workspaces[name];
|
||||
if (ws) invoke("watch_workspace", { path: ws.path }).catch(() => {});
|
||||
if (ws) invoke("watch_workspace", { path: ws.path }).catch((e) => console.warn("File watcher failed:", e));
|
||||
error = null;
|
||||
} catch (e) {
|
||||
error = String(e);
|
||||
|
|
@ -200,7 +196,7 @@ async function toggleTask(taskId: string) {
|
|||
// Move to top of list locally, then persist order in background
|
||||
if (updated.status === "backlog") {
|
||||
tasks = [updated, ...tasks.filter((t) => t.id !== taskId)];
|
||||
invoke("reorder_task", { listId: activeListId, taskId, newPosition: 0 }).catch(() => {});
|
||||
invoke("reorder_task", { listId: activeListId, taskId, newPosition: 0 }).catch((e) => { error = String(e); });
|
||||
} else {
|
||||
tasks = tasks.map((t) => (t.id === taskId ? updated : t));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue