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>) {
|
function debouncedSave(fields: Partial<Task>) {
|
||||||
clearTimeout(saveTimer);
|
clearTimeout(saveTimer);
|
||||||
const snapshot = { ...task };
|
|
||||||
saveTimer = setTimeout(() => {
|
saveTimer = setTimeout(() => {
|
||||||
app.updateTask({ ...snapshot, ...fields, updated_at: new Date().toISOString() });
|
app.updateTask({ ...task, ...fields, updated_at: new Date().toISOString() });
|
||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
|
|
||||||
let isCompleted = $derived(task.status === "completed");
|
let isCompleted = $derived(task.status === "completed");
|
||||||
let justChecked = $state(false);
|
let justChecked = $state(false);
|
||||||
|
let toggling = $state(false);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const _ = task.status;
|
const _ = task.status;
|
||||||
|
|
@ -35,6 +36,8 @@
|
||||||
|
|
||||||
async function handleToggle(e: MouseEvent) {
|
async function handleToggle(e: MouseEvent) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
if (toggling) return;
|
||||||
|
toggling = true;
|
||||||
justChecked = true;
|
justChecked = true;
|
||||||
await new Promise((r) => setTimeout(r, 300));
|
await new Promise((r) => setTimeout(r, 300));
|
||||||
transitioning = true;
|
transitioning = true;
|
||||||
|
|
@ -42,6 +45,7 @@
|
||||||
await new Promise((r) => setTimeout(r, 200));
|
await new Promise((r) => setTimeout(r, 200));
|
||||||
justChecked = false;
|
justChecked = false;
|
||||||
await app.toggleTask(task.id);
|
await app.toggleTask(task.id);
|
||||||
|
toggling = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTouchStart(e: TouchEvent) {
|
function handleTouchStart(e: TouchEvent) {
|
||||||
|
|
@ -57,14 +61,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTouchEnd() {
|
function handleTouchEnd() {
|
||||||
if (Math.abs(swipeX) > 100) {
|
if (Math.abs(swipeX) > 100 && !toggling) {
|
||||||
swipeX = 0;
|
swipeX = 0;
|
||||||
swiping = false;
|
swiping = false;
|
||||||
|
toggling = true;
|
||||||
justChecked = true;
|
justChecked = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
transitioning = true;
|
transitioning = true;
|
||||||
animateInIds.add(task.id);
|
animateInIds.add(task.id);
|
||||||
setTimeout(() => { justChecked = false; app.toggleTask(task.id); }, 200);
|
setTimeout(() => { justChecked = false; app.toggleTask(task.id).finally(() => { toggling = false; }); }, 200);
|
||||||
}, 300);
|
}, 300);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,8 @@ import type {
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
|
||||||
// Listen for file system changes from the backend watcher.
|
// 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", () => {
|
listen("fs-changed", () => {
|
||||||
loadLists();
|
loadLists();
|
||||||
}).then((unlisten) => {
|
|
||||||
_unlistenFs = unlisten;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Reactive state ───────────────────────────────────────────────────
|
// ── Reactive state ───────────────────────────────────────────────────
|
||||||
|
|
@ -98,7 +94,7 @@ async function switchWorkspace(name: string) {
|
||||||
activeListId = null;
|
activeListId = null;
|
||||||
await loadLists();
|
await loadLists();
|
||||||
const ws = config?.workspaces[name];
|
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;
|
error = null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = String(e);
|
error = String(e);
|
||||||
|
|
@ -200,7 +196,7 @@ async function toggleTask(taskId: string) {
|
||||||
// Move to top of list locally, then persist order in background
|
// Move to top of list locally, then persist order in background
|
||||||
if (updated.status === "backlog") {
|
if (updated.status === "backlog") {
|
||||||
tasks = [updated, ...tasks.filter((t) => t.id !== taskId)];
|
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 {
|
} else {
|
||||||
tasks = tasks.map((t) => (t.id === taskId ? updated : t));
|
tasks = tasks.map((t) => (t.id === taskId ? updated : t));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue