feat(lists): drag tasks and list entries onto another list; remember open list and view; live pill in lists
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m3s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-31 10:01:41 +01:00
1 parent 6abcfa655b
commit 25bb787a8e
13 files changed
+298 -52

No files matched your search

+62 -3
View File
@@ -30,6 +30,7 @@ import {
} from 'lucide-react';
import { TaskList, Thought, SubTask, Team, User, TaskListMember, KanbanColumn, PriorityLevel, Recurrence, RECURRENCE_LABELS } from '../../lib/types';
import { formatDue, isDueToday, isOverdue, localDay, splitDue } from '../../lib/due';
import { readViewSession, writeViewSession } from '../../lib/viewSession';
import { api, triggerHaptic } from '../../lib/api';
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
import { TaskDetailModal } from './TaskDetailModal';
@@ -66,6 +67,8 @@ interface TasksViewProps {
const KANBAN_WIDE_KEY = 'mindsift:kanban_wide';
type TasksSession = { listId: string; viewMode: 'list' | 'kanban' };
export const TasksView: React.FC<TasksViewProps> = ({
currentUser,
onRefreshStats,
@@ -75,7 +78,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
openTasksCount,
onWideChange,
}) => {
const [viewMode, setViewMode] = useState<'list' | 'kanban'>('list');
// Open list and view survive switching to other tabs (like the workspace's open note)
const [viewMode, setViewMode] = useState<'list' | 'kanban'>(() =>
readViewSession<TasksSession>('tasks').viewMode === 'kanban' ? 'kanban' : 'list'
);
// Wide Kanban: the board takes the whole window and shows every column in one row
const [kanbanWide, setKanbanWide] = useState(() => {
try {
@@ -101,7 +107,12 @@ export const TasksView: React.FC<TasksViewProps> = ({
const [filter, setFilter] = useState<'open' | 'done' | 'discarded'>('open');
const [tasks, setTasks] = useState<Thought[]>([]);
const [taskLists, setTaskLists] = useState<TaskList[]>([]);
const [selectedListId, setSelectedListId] = useState<string>('all');
const [selectedListId, setSelectedListId] = useState<string>(
() => readViewSession<TasksSession>('tasks').listId || 'all'
);
useEffect(() => {
writeViewSession<TasksSession>('tasks', { listId: selectedListId, viewMode });
}, [selectedListId, viewMode]);
const [teams, setTeams] = useState<Team[]>([]);
const [collaborators, setCollaborators] = useState<User[]>([]);
const [discarded, setDiscarded] = useState<Thought[]>([]);
@@ -111,6 +122,8 @@ export const TasksView: React.FC<TasksViewProps> = ({
const [draggingTaskId, setDraggingTaskId] = useState<string | null>(null);
const [dragOverTaskTarget, setDragOverTaskTarget] = useState<{ taskId: string; position: 'before' | 'after' } | null>(null);
const [dragOverKanbanCol, setDragOverKanbanCol] = useState<string | null>(null);
// List tab a dragged task hovers over (dropping moves it into that list)
const [dragOverListTab, setDragOverListTab] = useState<string | null>(null);
// Quick Task Add state
const [quickTitle, setQuickTitle] = useState('');
@@ -190,6 +203,10 @@ export const TasksView: React.FC<TasksViewProps> = ({
loadedListRef.current = listId;
setTasks(Array.isArray(allTasks) ? allTasks : []);
setTaskLists(Array.isArray(allLists) ? allLists : []);
// The remembered list was deleted or unshared meanwhile
if (listId !== 'all' && Array.isArray(allLists) && !allLists.some((l) => l.id === listId)) {
setSelectedListId('all');
}
setTeams(Array.isArray(allTeams) ? allTeams : []);
setCollaborators(Array.isArray(allCollabs) ? allCollabs : []);
setDiscarded(Array.isArray(allDiscarded) ? allDiscarded : []);
@@ -535,6 +552,35 @@ export const TasksView: React.FC<TasksViewProps> = ({
onRefreshStats();
};
// A task dragged onto a list tab lands in that list like a new task: it keeps its Kanban status
// if the list has that column, otherwise it starts in the list's first column
const handleDropOnListTab = async (e: React.DragEvent, list: TaskList) => {
e.preventDefault();
const sourceId = e.dataTransfer.getData('application/mindsift-task-id') || draggingTaskId;
setDraggingTaskId(null);
setDragOverTaskTarget(null);
setDragOverKanbanCol(null);
setDragOverListTab(null);
const task = tasks.find((t) => t.id === sourceId);
if (!task || task.taskListId === list.id || list.permission === 'viewer') return;
triggerHaptic(15);
const columns = Array.isArray(list.columns) && list.columns.length > 0 ? list.columns : globalColumns;
const keepStatus = !task.kanbanStatus || columns.some((c) => c.id === task.kanbanStatus);
const kanbanStatus = keepStatus ? task.kanbanStatus : (columns.find((c) => !c.isDone) || columns[0])?.id;
setTasks((prev) =>
selectedListId !== 'all'
? prev.filter((t) => t.id !== task.id)
: prev.map((t) => (t.id === task.id ? { ...t, taskListId: list.id, kanbanStatus } : t))
);
try {
await api.updateThought(task.id, { taskListId: list.id, ...(keepStatus ? {} : { kanbanStatus }) });
} catch {
loadData();
}
api.getTaskLists().then((lists) => setTaskLists(Array.isArray(lists) ? lists : [])).catch(() => {});
onRefreshStats();
};
const handleUpdateTaskNotes = async (task: Thought, newNotes: string) => {
setTasks((prev) => prev.map((t) => (t.id === task.id ? { ...t, notes: newNotes } : t)));
await api.updateThought(task.id, { notes: newNotes });
@@ -1137,6 +1183,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
setDraggingTaskId(null);
setDragOverTaskTarget(null);
setDragOverKanbanCol(null);
setDragOverListTab(null);
}}
onDragOver={(e) => handleTaskDragOver(e, task.id)}
onDragLeave={(e) => handleTaskDragLeave(e, task.id)}
@@ -1385,9 +1432,20 @@ export const TasksView: React.FC<TasksViewProps> = ({
{safeTaskLists.map((tl) => (
<div
key={tl.id}
onDragOver={(e) => {
const task = draggingTaskId ? tasks.find((t) => t.id === draggingTaskId) : null;
if (!task || task.taskListId === tl.id || tl.permission === 'viewer') return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (dragOverListTab !== tl.id) setDragOverListTab(tl.id);
}}
onDragLeave={() => {
if (dragOverListTab === tl.id) setDragOverListTab(null);
}}
onDrop={(e) => handleDropOnListTab(e, tl)}
className={`relative group h-8 flex items-center border rounded-btn transition-colors ${
selectedListId === tl.id ? 'bg-panel border-accent text-main pr-1' : 'bg-canvas border-border text-muted hover:text-main'
}`}
} ${dragOverListTab === tl.id ? 'ring-2 ring-accent/60 bg-accent/10 text-main' : ''}`}
>
<button
onClick={() => setSelectedListId(tl.id)}
@@ -2221,6 +2279,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
setDraggingTaskId(null);
setDragOverTaskTarget(null);
setDragOverKanbanCol(null);
setDragOverListTab(null);
}}
onDragOver={(e) => handleTaskDragOver(e, task.id)}
onDragLeave={(e) => handleTaskDragLeave(e, task.id)}