feat(todo-web): auto-save task edits, remove save/cancel buttons

Changes in the expanded inline task editor are now saved automatically
with a 500ms debounce. Removes the need to manually click "Speichern".
The delete button remains for intentional destructive actions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-23 21:24:00 +01:00
parent ee3e815f1d
commit 49457c354a
2 changed files with 42 additions and 70 deletions

View file

@ -69,6 +69,8 @@
let isLoading = $state(false);
let showDeleteConfirm = $state(false);
let titleInputRef = $state<HTMLInputElement | null>(null);
let autoSaveTimer: ReturnType<typeof setTimeout> | null = null;
let isInitializing = $state(false);
// Focus title input when expanded
$effect(() => {
@ -81,6 +83,7 @@
// Initialize form when expanded
$effect(() => {
if (isExpanded && task) {
isInitializing = true;
title = task.title || '';
description = task.description || '';
dueDate = task.dueDate ? format(new Date(task.dueDate), 'yyyy-MM-dd') : '';
@ -103,9 +106,48 @@
contactsStore.checkAvailability().then((available) => {
contactsAvailable = available;
});
// Allow a tick for all state to settle before enabling auto-save
setTimeout(() => {
isInitializing = false;
}, 0);
}
});
// Auto-save: debounce changes and save automatically
function scheduleAutoSave() {
if (isInitializing || !isExpanded) return;
if (autoSaveTimer) clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(() => {
handleSave();
}, 500);
}
// Watch all form fields for changes and trigger auto-save
$effect(() => {
// Read all reactive form fields to create dependencies
void [
title,
description,
dueDate,
dueTime,
startDate,
priority,
status,
projectId,
selectedLabelIds,
subtasks,
recurrenceRule,
notes,
storyPoints,
effectiveDuration,
funRating,
assignee,
involvedContacts,
];
scheduleAutoSave();
});
// Animation state for completing
let isAnimatingComplete = $state(false);
@ -140,9 +182,6 @@
if (!isExpanded) return;
if (e.key === 'Escape') {
onCollapse?.();
} else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
handleSave();
}
}
@ -566,23 +605,6 @@
>
{showDeleteConfirm ? 'Wirklich löschen?' : 'Löschen'}
</button>
<div class="actions-right">
<button type="button" class="btn btn-secondary" onclick={onCollapse} disabled={isLoading}>
Abbrechen
</button>
<button
type="button"
class="btn btn-primary"
onclick={handleSave}
disabled={isLoading || !title.trim()}
>
{#if isLoading}
<div class="spinner"></div>
{:else}
Speichern
{/if}
</button>
</div>
</div>
</div>
{/if}
@ -1110,11 +1132,6 @@
border-top-color: rgba(255, 255, 255, 0.1);
}
.actions-right {
display: flex;
gap: 0.5rem;
}
.btn {
display: flex;
align-items: center;
@ -1134,33 +1151,6 @@
cursor: not-allowed;
}
.btn-primary {
background: #8b5cf6;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #7c3aed;
}
.btn-secondary {
background: rgba(0, 0, 0, 0.05);
color: #374151;
}
:global(.dark) .btn-secondary {
background: rgba(255, 255, 255, 0.1);
color: #e5e7eb;
}
.btn-secondary:hover:not(:disabled) {
background: rgba(0, 0, 0, 0.1);
}
:global(.dark) .btn-secondary:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.15);
}
.btn-danger {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
@ -1170,19 +1160,4 @@
background: #ef4444;
color: white;
}
.spinner {
width: 0.875rem;
height: 0.875rem;
border: 2px solid transparent;
border-top-color: white;
border-radius: 9999px;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>

View file

@ -51,9 +51,6 @@
if (data.labelIds !== undefined) {
await tasksStore.updateLabels(taskId, data.labelIds);
}
// Collapse after save
expandedTaskId = null;
} catch (error) {
console.error('Failed to save task:', error);
}