refactor: improve drag and drop UX across tasks, lists, and workspace bullets
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s

This commit is contained in:
sandrodach committed 2026-10-25 19:24:29 +01:00
1 parent 35d4657a25
commit 09725611b5
3 files changed
+142 -138

No files matched your search

+44 -90
View File
@@ -25,7 +25,6 @@ import {
Settings,
ChevronDown,
Folder,
GripVertical,
} from 'lucide-react';
import { TaskList, Thought, SubTask, Team, User, TaskListShare, KanbanColumn } from '../../lib/types';
import { api, triggerHaptic } from '../../lib/api';
@@ -69,7 +68,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
// Drag and Drop state
const [draggingTaskId, setDraggingTaskId] = useState<string | null>(null);
const [dragOverTaskTarget, setDragOverTaskTarget] = useState<{ taskId: string; position: 'before' | 'after' } | null>(null);
const [isDragOverTopZone, setIsDragOverTopZone] = useState<boolean>(false);
const [dragOverKanbanCol, setDragOverKanbanCol] = useState<string | null>(null);
// Quick Task Add state
@@ -110,7 +108,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
// Hashtag filtering, grouping and sorting state
const [selectedTag, setSelectedTag] = useState<string | null>(null);
const [groupBy, setGroupBy] = useState<'none' | 'tag' | 'status' | 'priority' | 'due'>('none');
const [sortBy, setSortBy] = useState<'created' | 'due' | 'priority' | 'alpha'>('created');
const [sortBy, setSortBy] = useState<'custom' | 'created' | 'due' | 'priority' | 'alpha'>('custom');
const loadData = async () => {
setLoading(true);
@@ -269,6 +267,14 @@ export const TasksView: React.FC<TasksViewProps> = ({
if (selectedTag) {
result = result.filter((t) => Array.isArray(t.tags) && t.tags.includes(selectedTag));
}
if (sortBy === 'custom') {
return [...result].sort((a, b) => {
const orderA = a.sortOrder ?? 0;
const orderB = b.sortOrder ?? 0;
if (orderA !== orderB) return orderA - orderB;
return b.createdAt.localeCompare(a.createdAt);
});
}
return [...result].sort((a, b) => {
if (sortBy === 'due') {
if (!a.dueDate && !b.dueDate) return 0;
@@ -589,7 +595,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
const position = dragOverTaskTarget?.position || 'after';
setDraggingTaskId(null);
setDragOverTaskTarget(null);
setIsDragOverTopZone(false);
setDragOverKanbanCol(null);
if (!sourceId || sourceId === targetTask.id) return;
@@ -612,55 +617,20 @@ export const TasksView: React.FC<TasksViewProps> = ({
const newTasks = [...remainingTasks];
newTasks.splice(insertIdx, 0, updatedSourceTask);
const reorderPayload = newTasks.map((t, idx) => ({
id: t.id,
const updatedWithSort = newTasks.map((t, idx) => ({
...t,
sortOrder: idx,
kanbanStatus: t.kanbanStatus,
}));
setTasks(newTasks);
setTasks(updatedWithSort);
setSortBy('custom');
try {
await api.reorderTasks(
reorderPayload,
selectedListId !== 'all' ? selectedListId : undefined
);
} catch {
loadData();
}
};
const handleDropAtTop = async (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
const sourceId =
e.dataTransfer.getData('application/mindsift-task-id') ||
e.dataTransfer.getData('text/plain') ||
draggingTaskId;
setDraggingTaskId(null);
setDragOverTaskTarget(null);
setIsDragOverTopZone(false);
setDragOverKanbanCol(null);
if (!sourceId) return;
triggerHaptic(15);
const sourceTask = tasks.find((t) => t.id === sourceId);
if (!sourceTask) return;
const remainingTasks = tasks.filter((t) => t.id !== sourceId);
const newTasks = [sourceTask, ...remainingTasks];
const reorderPayload = newTasks.map((t, idx) => ({
const reorderPayload = updatedWithSort.map((t) => ({
id: t.id,
sortOrder: idx,
sortOrder: t.sortOrder ?? 0,
kanbanStatus: t.kanbanStatus,
}));
setTasks(newTasks);
try {
await api.reorderTasks(
reorderPayload,
@@ -682,7 +652,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
setDraggingTaskId(null);
setDragOverTaskTarget(null);
setIsDragOverTopZone(false);
setDragOverKanbanCol(null);
if (!sourceId) return;
@@ -701,13 +670,19 @@ export const TasksView: React.FC<TasksViewProps> = ({
const remainingTasks = tasks.filter((t) => t.id !== sourceId);
const newTasks = [updatedSourceTask, ...remainingTasks];
const reorderPayload = newTasks.map((t, idx) => ({
id: t.id,
const updatedWithSort = newTasks.map((t, idx) => ({
...t,
sortOrder: idx,
kanbanStatus: t.kanbanStatus,
}));
setTasks(newTasks);
setTasks(updatedWithSort);
setSortBy('custom');
const reorderPayload = updatedWithSort.map((t) => ({
id: t.id,
sortOrder: t.sortOrder ?? 0,
kanbanStatus: t.kanbanStatus,
}));
try {
await api.reorderTasks(
@@ -893,10 +868,21 @@ export const TasksView: React.FC<TasksViewProps> = ({
return (
<div
key={task.id}
draggable
onDragStart={(e) => handleTaskDragStart(e, task)}
onDragEnd={() => {
setDraggingTaskId(null);
setDragOverTaskTarget(null);
setDragOverKanbanCol(null);
}}
onDragOver={(e) => handleTaskDragOver(e, task.id)}
onDragLeave={(e) => handleTaskDragLeave(e, task.id)}
onDrop={(e) => handleDropOnTask(e, task)}
className={`px-3 py-2.5 flex items-center justify-between gap-3 transition-colors hover:bg-panel-hover group border-b border-border last:border-b-0 relative ${
onClick={() => {
setDetailTaskId(task.id);
setShowDetailModal(true);
}}
className={`px-3 py-2.5 flex items-center justify-between gap-3 transition-colors hover:bg-panel-hover group border-b border-border last:border-b-0 relative cursor-pointer cursor-grab active:cursor-grabbing select-none ${
isCompleted ? 'opacity-65' : ''
} ${isDragging ? 'opacity-30 border border-dashed border-accent/40' : ''}`}
>
@@ -909,26 +895,11 @@ export const TasksView: React.FC<TasksViewProps> = ({
)}
<div className="flex items-center gap-2.5 min-w-0 flex-1">
{/* Grip handle for dragging */}
<div
draggable
onDragStart={(e) => handleTaskDragStart(e, task)}
onDragEnd={() => {
setDraggingTaskId(null);
setDragOverTaskTarget(null);
setIsDragOverTopZone(false);
setDragOverKanbanCol(null);
}}
onClick={(e) => e.stopPropagation()}
className="opacity-0 group-hover:opacity-70 hover:opacity-100 text-faint hover:text-accent cursor-grab active:cursor-grabbing flex-shrink-0 -ml-1 transition-opacity"
title="Aufgabe ziehen zum Sortieren"
>
<GripVertical className="w-3.5 h-3.5" />
</div>
{/* Checkbox */}
<button
type="button"
draggable={false}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => {
e.stopPropagation();
handleToggleDone(task);
@@ -1050,9 +1021,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
</div>
{/* Action Controls */}
<div className="flex items-center gap-1 flex-shrink-0">
<div className="flex items-center gap-1 flex-shrink-0" onMouseDown={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}>
<button
type="button"
draggable={false}
onClick={() => {
setDetailTaskId(task.id);
setShowDetailModal(true);
@@ -1064,6 +1036,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
</button>
<button
type="button"
draggable={false}
onClick={() => handleConvertToDoc(task)}
title="Als Notiz vertiefen"
className="text-faint hover:text-accent p-1.5 transition-colors hidden sm:block"
@@ -1072,6 +1045,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
</button>
<button
type="button"
draggable={false}
onClick={() => handleDeletePermanent(task.id)}
title="Aufgabe löschen"
className="text-faint hover:text-status-err p-1.5 transition-colors"
@@ -1347,6 +1321,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
onChange={(e) => setSortBy(e.target.value as any)}
className="h-7 px-2 input-field text-xs bg-panel"
>
<option value="custom">Manuell (Drag & Drop)</option>
<option value="created">Erstelldatum</option>
<option value="due">Fälligkeitsdatum</option>
<option value="priority">Priorität</option>
@@ -1601,7 +1576,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
onDragEnd={() => {
setDraggingTaskId(null);
setDragOverTaskTarget(null);
setIsDragOverTopZone(false);
setDragOverKanbanCol(null);
}}
onDragOver={(e) => handleTaskDragOver(e, task.id)}
@@ -1835,26 +1809,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
</div>
) : (
<div className="space-y-3">
{/* Dedicated Top Drop Zone when dragging a task */}
{draggingTaskId && (
<div
onDragOver={(e) => {
e.preventDefault();
setIsDragOverTopZone(true);
}}
onDragLeave={() => setIsDragOverTopZone(false)}
onDrop={(e) => handleDropAtTop(e)}
className={`p-3 text-center text-xs font-semibold rounded-card border-2 border-dashed transition-all flex items-center justify-center gap-2 cursor-pointer ${
isDragOverTopZone
? 'border-accent bg-accent/20 text-accent scale-[1.01] shadow-lg'
: 'border-accent/40 text-accent bg-accent/5 hover:border-accent'
}`}
>
<ArrowRight className="w-4 h-4 -rotate-90 text-accent animate-bounce" />
<span>Ganz oben in der Liste ablegen</span>
</div>
)}
{groupedTasks ? (
/* GROUPED LIST */
groupedTasks.map((grp) => (