fix(gui): wire sync credentials, fix memory leak, race condition, add delete confirmations

- Add load_credentials Tauri command and use it in triggerSync() instead
  of passing empty username/password strings
- Replace raw __TAURI_INTERNALS__.invoke() with proper invoke import in
  SettingsScreen
- Wrap window event listeners in $effect() with cleanup to prevent
  memory leak on component remount
- Return created Task from createTask() and use it directly in
  NewTaskInput instead of guessing from array index
- Add confirm() dialogs before deleting tasks, lists, and workspaces
This commit is contained in:
Tristan Michael 2026-03-30 16:14:40 -07:00
parent a54e427cd9
commit c138a8bcf6
6 changed files with 35 additions and 18 deletions

View file

@ -310,6 +310,11 @@ fn store_credentials(
webdav::store_credentials(&domain, &username, &password).map_err(|e| e.to_string()) webdav::store_credentials(&domain, &username, &password).map_err(|e| e.to_string())
} }
#[tauri::command]
fn load_credentials(domain: String) -> Result<(String, String), String> {
webdav::load_credentials(&domain).map_err(|e| e.to_string())
}
#[tauri::command] #[tauri::command]
async fn test_webdav_connection( async fn test_webdav_connection(
url: String, url: String,
@ -373,6 +378,7 @@ pub fn run() {
reorder_task, reorder_task,
set_webdav_config, set_webdav_config,
store_credentials, store_credentials,
load_credentials,
test_webdav_connection, test_webdav_connection,
sync_workspace, sync_workspace,
]) ])

View file

@ -15,9 +15,8 @@
async function handleSubmit() { async function handleSubmit() {
if (!title.trim()) return; if (!title.trim()) return;
await app.createTask(title.trim(), description.trim() || undefined); const created = await app.createTask(title.trim(), description.trim() || undefined);
if (dueDate && app.tasks.length > 0) { if (dueDate && created) {
const created = app.tasks[app.tasks.length - 1];
await app.updateTask({ ...created, due_date: dueDate, updated_at: new Date().toISOString() }); await app.updateTask({ ...created, due_date: dueDate, updated_at: new Date().toISOString() });
} }
title = ""; title = "";

View file

@ -50,6 +50,7 @@
async function handleDelete() { async function handleDelete() {
showMenu = false; showMenu = false;
if (!confirm(`Delete task "${task.title}"?`)) return;
await app.deleteTask(task.id); await app.deleteTask(task.id);
onback(); onback();
} }

View file

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { invoke } from "@tauri-apps/api/core";
import { app } from "../stores/app.svelte"; import { app } from "../stores/app.svelte";
let { onclose }: { onclose?: () => void } = $props(); let { onclose }: { onclose?: () => void } = $props();
@ -11,7 +12,7 @@
async function testConnection() { async function testConnection() {
testStatus = "testing"; testStatus = "testing";
try { try {
await (globalThis as any).__TAURI_INTERNALS__.invoke("test_webdav_connection", { await invoke("test_webdav_connection", {
url: webdavUrl, url: webdavUrl,
username: webdavUser, username: webdavUser,
password: webdavPass, password: webdavPass,
@ -24,18 +25,19 @@
async function saveWebdav() { async function saveWebdav() {
if (!app.config?.current_workspace || !webdavUrl.trim()) return; if (!app.config?.current_workspace || !webdavUrl.trim()) return;
await (globalThis as any).__TAURI_INTERNALS__.invoke("set_webdav_config", { await invoke("set_webdav_config", {
workspaceName: app.config.current_workspace, workspaceName: app.config.current_workspace,
webdavUrl: webdavUrl.trim(), webdavUrl: webdavUrl.trim(),
}); });
if (webdavUser && webdavPass) { if (webdavUser && webdavPass) {
const domain = new URL(webdavUrl).hostname; const domain = new URL(webdavUrl).hostname;
await (globalThis as any).__TAURI_INTERNALS__.invoke("store_credentials", { await invoke("store_credentials", {
domain, domain,
username: webdavUser, username: webdavUser,
password: webdavPass, password: webdavPass,
}); });
} }
await app.loadConfig();
} }
</script> </script>

View file

@ -39,9 +39,6 @@
if (wsMenuName && !target.closest("[data-ws-menu]")) wsMenuName = null; if (wsMenuName && !target.closest("[data-ws-menu]")) wsMenuName = null;
} }
if (typeof window !== "undefined") {
window.addEventListener("mousedown", handleWindowClick);
}
let newListName = $state(""); let newListName = $state("");
let showCompleted = $state(false); let showCompleted = $state(false);
let completedVisible = $state(false); let completedVisible = $state(false);
@ -52,13 +49,19 @@
let resizing = $state(false); let resizing = $state(false);
let resizeTimer: ReturnType<typeof setTimeout>; let resizeTimer: ReturnType<typeof setTimeout>;
if (typeof window !== "undefined") { $effect(() => {
window.addEventListener("resize", () => { window.addEventListener("mousedown", handleWindowClick);
const handleResize = () => {
resizing = true; resizing = true;
clearTimeout(resizeTimer); clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => (resizing = false), 150); resizeTimer = setTimeout(() => (resizing = false), 150);
}); };
} window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("mousedown", handleWindowClick);
window.removeEventListener("resize", handleResize);
};
});
async function handleNewList() { async function handleNewList() {
if (!newListName.trim()) return; if (!newListName.trim()) return;
@ -69,6 +72,8 @@
async function handleDeleteList(id: string) { async function handleDeleteList(id: string) {
listMenuId = null; listMenuId = null;
const list = app.lists.find(l => l.id === id);
if (!confirm(`Delete list "${list?.title ?? id}" and all its tasks?`)) return;
await app.deleteList(id); await app.deleteList(id);
} }
@ -270,7 +275,7 @@
{#if wsMenuName === name} {#if wsMenuName === name}
<div class="absolute right-0 top-full z-40 mt-1 min-w-[140px] rounded-lg border border-border-light bg-surface-light py-1 shadow-lg dark:border-border-dark dark:bg-surface-dark"> <div class="absolute right-0 top-full z-40 mt-1 min-w-[140px] rounded-lg border border-border-light bg-surface-light py-1 shadow-lg dark:border-border-dark dark:bg-surface-dark">
<button <button
onclick={() => { wsMenuName = null; app.removeWorkspace(name); }} onclick={() => { wsMenuName = null; if (confirm(`Remove workspace "${name}"? (Files remain on disk)`)) app.removeWorkspace(name); }}
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-danger hover:bg-black/5 dark:hover:bg-white/10" class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-danger hover:bg-black/5 dark:hover:bg-white/10"
> >
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor"> <svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">

View file

@ -140,8 +140,8 @@ async function deleteList(id: string) {
} }
} }
async function createTask(title: string, description?: string) { async function createTask(title: string, description?: string): Promise<Task | null> {
if (!activeListId) return; if (!activeListId) return null;
try { try {
const task = await invoke<Task>("create_task", { const task = await invoke<Task>("create_task", {
listId: activeListId, listId: activeListId,
@ -150,8 +150,10 @@ async function createTask(title: string, description?: string) {
}); });
tasks = [...tasks, task]; tasks = [...tasks, task];
error = null; error = null;
return task;
} catch (e) { } catch (e) {
error = String(e); error = String(e);
return null;
} }
} }
@ -214,11 +216,13 @@ async function triggerSync() {
syncing = true; syncing = true;
error = null; error = null;
try { try {
const domain = new URL(ws.webdav_url).hostname;
const [username, password] = await invoke<[string, string]>("load_credentials", { domain });
const result = await invoke<SyncResult>("sync_workspace", { const result = await invoke<SyncResult>("sync_workspace", {
workspacePath: ws.path, workspacePath: ws.path,
webdavUrl: ws.webdav_url, webdavUrl: ws.webdav_url,
username: "", username,
password: "", password,
}); });
if (result.errors.length > 0) { if (result.errors.length > 0) {
error = result.errors.join("; "); error = result.errors.join("; ");