feat(tasks): persist list_id/effort/notes + update_task_cmd — close Task 2 metadata gap

This commit is contained in:
2026-04-19 16:23:25 +01:00
parent db5c739f22
commit 9378980639
6 changed files with 272 additions and 26 deletions

View File

@@ -244,6 +244,7 @@ function mapTaskRow(row) {
bucket: row.bucket ?? "inbox",
listId: row.listId ?? null,
effort: row.effort ?? "",
notes: row.notes ?? "",
done: !!row.done,
doneAt: row.doneAt ?? null,
createdAt: row.createdAt ?? new Date().toISOString(),
@@ -294,6 +295,11 @@ export async function addTask(task) {
text: task.text,
bucket: task.bucket || "inbox",
sourceTranscriptId: task.sourceTranscriptId || null,
// Task 2.6: forward list_id / effort so the SQLite row carries the
// metadata callers pass in. Callers that omit these get server
// defaults (NULL for list_id/effort, '' for notes at the column).
listId: task.listId ?? null,
effort: task.effort ?? null,
},
});
tasks.unshift(mapTaskRow(row));
@@ -303,7 +309,16 @@ export async function addTask(task) {
}
}
export function updateTask(id, updates) {
/**
* Apply a partial update to an in-memory task and broadcast the change.
* Used by `completeTask` / `uncompleteTask` after their dedicated server
* commands succeed. Not exported — external callers go through `updateTask`
* which persists via `update_task_cmd`.
*
* @param {string} id
* @param {Record<string, any>} updates
*/
function applyLocalTaskUpdate(id, updates) {
const idx = tasks.findIndex((t) => t.id === id);
if (idx >= 0) {
Object.assign(tasks[idx], updates);
@@ -311,6 +326,47 @@ export function updateTask(id, updates) {
}
}
/**
* Persist a patch to an existing task and refresh the in-memory copy with
* the server's canonical row. `updates` may contain any of
* `{ text, bucket, listId, effort, notes }`; omitted fields are preserved
* server-side via COALESCE. `done` / `doneAt` are ignored by the server's
* UpdateTaskRequest — use `completeTask` / `uncompleteTask` for those.
*
* Closes the Task 2.6 gap: before this, updateTask was a session-only
* mutation, so bucket / effort / listId edits vanished on restart.
*
* @param {string} id
* @param {Record<string, any>} updates
*/
export async function updateTask(id, updates) {
if (!hasTauriRuntime()) {
// Browser preview: fall back to the local-only path so the UI still
// reflects edits during Vite dev runs outside the Tauri shell.
applyLocalTaskUpdate(id, updates);
return;
}
try {
const row = await invoke("update_task_cmd", {
id,
patch: {
text: updates.text ?? null,
bucket: updates.bucket ?? null,
listId: updates.listId ?? null,
effort: updates.effort ?? null,
notes: updates.notes ?? null,
},
});
const idx = tasks.findIndex((t) => t.id === id);
if (idx >= 0) {
tasks[idx] = mapTaskRow(row);
broadcastTasks();
}
} catch (err) {
toasts.error("Couldn't update task", err?.message ?? String(err));
}
}
export async function deleteTask(id) {
if (!hasTauriRuntime()) return;
try {
@@ -329,7 +385,10 @@ export async function completeTask(id) {
if (!hasTauriRuntime()) return;
try {
await invoke("complete_task_cmd", { id });
updateTask(id, { done: true, doneAt: new Date().toISOString() });
// Local-only mirror: complete_task_cmd already stamped done/done_at
// server-side, so we just mirror the state here. Going through the
// persisting `updateTask` path would be a wasted round-trip.
applyLocalTaskUpdate(id, { done: true, doneAt: new Date().toISOString() });
} catch (err) {
toasts.error("Couldn't complete task", err?.message ?? String(err));
}
@@ -339,7 +398,7 @@ export async function uncompleteTask(id) {
if (!hasTauriRuntime()) return;
try {
await invoke("uncomplete_task_cmd", { id });
updateTask(id, { done: false, doneAt: null });
applyLocalTaskUpdate(id, { done: false, doneAt: null });
} catch (err) {
toasts.error("Couldn't uncomplete task", err?.message ?? String(err));
}