From cb3c1ffb9390c973d89b548fd4036b6d17815afe Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Tue, 27 Jan 2026 01:32:14 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(todo):=20replace?= =?UTF-8?q?=20edit=20modal=20with=20inline=20task=20editing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redesign TaskItem to expand inline for editing instead of opening a separate modal. Improves UX by keeping user context and reducing visual interruption. Removes modal-related code from pages. Co-Authored-By: Claude Opus 4.5 --- .../src/lib/components/AuthGateModal.svelte | 2 +- .../src/lib/components/TaskEditModal.svelte | 9 +- .../web/src/lib/components/TaskItem.svelte | 926 +++++++++++++++--- .../web/src/lib/components/TaskList.svelte | 51 +- .../apps/web/src/routes/(app)/+layout.svelte | 3 - .../apps/web/src/routes/(app)/+page.svelte | 62 +- .../src/routes/(app)/tag/[id]/+page.svelte | 58 +- 7 files changed, 848 insertions(+), 263 deletions(-) diff --git a/apps/todo/apps/web/src/lib/components/AuthGateModal.svelte b/apps/todo/apps/web/src/lib/components/AuthGateModal.svelte index 1f31fb474..81739d864 100644 --- a/apps/todo/apps/web/src/lib/components/AuthGateModal.svelte +++ b/apps/todo/apps/web/src/lib/components/AuthGateModal.svelte @@ -68,7 +68,7 @@ {#if visible}
- import type { Task } from '@todo/shared'; + import type { + Task, + Subtask, + TaskPriority, + TaskStatus, + EffectiveDuration, + UpdateTaskInput, + } from '@todo/shared'; + import type { ContactReference, ContactOrManual } from '@manacore/shared-types'; + import { STATUS_OPTIONS, RECURRENCE_OPTIONS } from '@todo/shared'; import { format, isToday, isPast, isTomorrow } from 'date-fns'; import { de } from 'date-fns/locale'; import { projectsStore } from '$lib/stores/projects.svelte'; - import { ContactAvatar } from '@manacore/shared-ui'; + import { contactsStore } from '$lib/stores/contacts.svelte'; + import { ContactAvatar, ContactSelector } from '@manacore/shared-ui'; + import SubtaskList from './SubtaskList.svelte'; + import { + PrioritySelector, + StorypointsSelector, + DurationPicker, + FunRatingPicker, + TagSelector, + } from './form'; interface Props { task: Task; showCompleted?: boolean; animateComplete?: boolean; + isExpanded?: boolean; onToggleComplete: () => void; onDelete: () => void; - onEdit?: () => void; + onExpand?: () => void; + onCollapse?: () => void; + onSave?: (data: UpdateTaskInput) => void; } let { task, showCompleted = false, animateComplete = false, + isExpanded = false, onToggleComplete, onDelete, - onEdit, + onExpand, + onCollapse, + onSave, }: Props = $props(); + // Form state for expanded mode + let title = $state(''); + let description = $state(''); + let dueDate = $state(''); + let dueTime = $state(''); + let startDate = $state(''); + let priority = $state('medium'); + let status = $state('pending'); + let projectId = $state(null); + let selectedLabelIds = $state([]); + let subtasks = $state([]); + let recurrenceRule = $state(''); + let notes = $state(''); + let storyPoints = $state(null); + let effectiveDuration = $state(null); + let funRating = $state(null); + let assignee = $state([]); + let involvedContacts = $state([]); + let contactsAvailable = $state(null); + let isLoading = $state(false); + let showDeleteConfirm = $state(false); + let titleInputRef = $state(null); + + // Focus title input when expanded + $effect(() => { + if (isExpanded && titleInputRef) { + // Small delay to ensure DOM is ready + setTimeout(() => titleInputRef?.focus(), 50); + } + }); + + // Initialize form when expanded + $effect(() => { + if (isExpanded && task) { + title = task.title || ''; + description = task.description || ''; + dueDate = task.dueDate ? format(new Date(task.dueDate), 'yyyy-MM-dd') : ''; + dueTime = task.dueTime || ''; + startDate = task.startDate ? format(new Date(task.startDate), 'yyyy-MM-dd') : ''; + priority = task.priority || 'medium'; + status = task.status || 'pending'; + projectId = task.projectId || null; + selectedLabelIds = task.labels?.map((l) => l.id) || []; + subtasks = task.subtasks ? [...task.subtasks] : []; + recurrenceRule = task.recurrenceRule || ''; + notes = task.metadata?.notes || ''; + storyPoints = task.metadata?.storyPoints ?? null; + effectiveDuration = task.metadata?.effectiveDuration ?? null; + funRating = task.metadata?.funRating ?? null; + assignee = task.metadata?.assignee ? [task.metadata.assignee] : []; + involvedContacts = task.metadata?.involvedContacts || []; + showDeleteConfirm = false; + + contactsStore.checkAvailability().then((available) => { + contactsAvailable = available; + }); + } + }); + // Animation state for completing let isAnimatingComplete = $state(false); @@ -48,11 +131,79 @@ } function handleContentClick() { - if (onEdit) { - onEdit(); + if (onExpand) { + onExpand(); } } + function handleKeydown(e: KeyboardEvent) { + if (!isExpanded) return; + if (e.key === 'Escape') { + onCollapse?.(); + } else if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { + e.preventDefault(); + handleSave(); + } + } + + function toContactReference(contact: ContactOrManual): ContactReference | null { + if ('isManual' in contact && contact.isManual) { + return null; + } + return contact as ContactReference; + } + + async function handleSave() { + if (!title.trim() || !onSave) return; + + isLoading = true; + try { + const assigneeRef = assignee.length > 0 ? toContactReference(assignee[0]) : null; + const involvedRefs = involvedContacts + .map(toContactReference) + .filter((c): c is ContactReference => c !== null); + + const data: UpdateTaskInput = { + title: title.trim(), + description: description.trim() || null, + dueDate: dueDate ? new Date(dueDate).toISOString() : null, + dueTime: dueTime || null, + startDate: startDate ? new Date(startDate).toISOString() : null, + priority, + status, + projectId: projectId || null, + subtasks: subtasks.length > 0 ? subtasks : null, + recurrenceRule: recurrenceRule || null, + metadata: { + ...task.metadata, + notes: notes.trim() || undefined, + storyPoints: storyPoints ?? undefined, + effectiveDuration: effectiveDuration ?? undefined, + funRating: funRating ?? undefined, + assignee: assigneeRef ?? undefined, + involvedContacts: involvedRefs.length > 0 ? involvedRefs : undefined, + }, + labelIds: selectedLabelIds, + }; + + onSave(data); + } finally { + isLoading = false; + } + } + + function handleDeleteClick() { + if (showDeleteConfirm) { + onDelete(); + } else { + showDeleteConfirm = true; + } + } + + function handleSubtasksChange(newSubtasks: Subtask[]) { + subtasks = newSubtasks; + } + // Priority colors const priorityColors: Record = { low: '#22c55e', @@ -89,140 +240,412 @@ const completed = task.subtasks.filter((s) => s.isCompleted).length; return `${completed}/${task.subtasks.length}`; }); + + // Only allow drag from the drag handle + function handlePointerDown(e: PointerEvent) { + const target = e.target as HTMLElement; + const isDragHandle = target.closest('.drag-handle'); + if (!isDragHandle) { + // Prevent drag from starting if not on drag handle + e.stopPropagation(); + } + } -
- -
- - - -
+ - +
- - - + + + + + + {#if task.metadata?.assignee || (task.metadata?.involvedContacts && task.metadata.involvedContacts.length > 0)} +
+ {#if task.metadata?.assignee} +
+ +
+ {/if} + {#if task.metadata?.involvedContacts && task.metadata.involvedContacts.length > 0} +
+ {#each task.metadata.involvedContacts.slice(0, 2) as contact} +
+ +
+ {/each} + {#if task.metadata.involvedContacts.length > 2} + +{task.metadata.involvedContacts.length - 2} + {/if} +
+ {/if} +
+ {/if} + + + {#if dueDateText()} + + {dueDateText()} + + {/if} + + + {#if projectColor()} +
+ {/if} + + + + +
- - - - {#if task.metadata?.assignee || (task.metadata?.involvedContacts && task.metadata.involvedContacts.length > 0)} -
- {#if task.metadata?.assignee} -
- + +
+ + +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
- {/if} - {#if task.metadata?.involvedContacts && task.metadata.involvedContacts.length > 0} -
- {#each task.metadata.involvedContacts.slice(0, 2) as contact} -
- -
+
+ + +
+ + (priority = p)} /> +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + (selectedLabelIds = ids)} /> +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + (assignee = contacts)} + onSearch={(q) => contactsStore.searchContacts(q)} + singleSelect={true} + allowManualEntry={false} + placeholder="Person zuweisen..." + addLabel="Zuweisen" + searchPlaceholder="Name oder E-Mail..." + isAvailable={contactsAvailable ?? false} + /> +
+ + +
+ + (involvedContacts = contacts)} + onSearch={(q) => contactsStore.searchContacts(q)} + allowManualEntry={false} + placeholder="Personen hinzufügen..." + addLabel="Person hinzufügen" + searchPlaceholder="Name oder E-Mail..." + isAvailable={contactsAvailable ?? false} + /> +
+ + +
+ + +
+ + +
+
+ + (storyPoints = v)} />
- {/if} +
+ + (effectiveDuration = v)} /> +
+
+ + (funRating = v)} /> +
+
+ + +
+ +
+ + +
+
{/if} - - - {#if dueDateText()} - - {dueDateText()} - - {/if} - - - {#if projectColor()} -
- {/if} - - -
diff --git a/apps/todo/apps/web/src/lib/components/TaskList.svelte b/apps/todo/apps/web/src/lib/components/TaskList.svelte index 5530aa3ab..5cfb5ace7 100644 --- a/apps/todo/apps/web/src/lib/components/TaskList.svelte +++ b/apps/todo/apps/web/src/lib/components/TaskList.svelte @@ -1,6 +1,6 @@ @@ -141,7 +98,7 @@

Offen ({incompleteTasks.length})

- + {/if} @@ -151,7 +108,7 @@

Erledigt ({completedTasks.length})

- + {/if} @@ -162,17 +119,6 @@ {/if}
- -{#if editingTask} - -{/if} -