feat: select text across bullets and delete it like a text editor; Ctrl+Z in tasks; notes and lists without emoji; custom done columns complete tasks
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m33s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-11-01 16:27:43 +01:00
1 parent e2b9daba75
commit 6fb98d4002
27 files changed
+535 -64

No files matched your search

+112 -6
View File
@@ -596,8 +596,83 @@ export const TasksView: React.FC<TasksViewProps> = ({
// --- TASK ACTIONS ---
// --- UNDO (Strg+Z / Cmd+Z): deleting, moving and ticking off, newest first; deleted tasks come back
// from the task trash. Deleting also shows "Rückgängig" for phones.
type UndoEntry =
| { kind: 'delete'; ids: string[] }
| { kind: 'order'; snapshot: { id: string; sortOrder: number; kanbanStatus?: string; isCompleted: boolean }[] }
| { kind: 'status'; id: string; isCompleted: boolean; kanbanStatus?: string };
const undoStackRef = useRef<UndoEntry[]>([]);
const [toast, setToast] = useState<{ text: string; undo: boolean } | null>(null);
const pushUndo = (entry: UndoEntry) => {
undoStackRef.current = [...undoStackRef.current, entry].slice(-20);
setToast(null); // its "Rückgängig" would now undo this newer step
};
const orderSnapshot = () =>
tasks.map((t) => ({ id: t.id, sortOrder: t.sortOrder ?? 0, kanbanStatus: t.kanbanStatus, isCompleted: !!t.isCompleted }));
const toastTimerRef = useRef<number | null>(null);
const showToast = (text: string, undo = false) => {
setToast({ text, undo });
if (toastTimerRef.current !== null) window.clearTimeout(toastTimerRef.current);
toastTimerRef.current = window.setTimeout(() => setToast(null), undo ? 8000 : 3000);
};
useEffect(() => () => {
if (toastTimerRef.current !== null) window.clearTimeout(toastTimerRef.current);
}, []);
const undoLast = async () => {
const entry = undoStackRef.current.pop();
if (!entry) return;
setToast(null);
triggerHaptic(10);
try {
if (entry.kind === 'delete') {
for (const id of entry.ids) await api.restoreTrashedTask(id).catch(() => {});
showToast(entry.ids.length === 1 ? 'Aufgabe wiederhergestellt' : `${entry.ids.length} Aufgaben wiederhergestellt`);
} else if (entry.kind === 'order') {
const byId = new Map(entry.snapshot.map((x) => [x.id, x]));
setTasks((prev) =>
prev.map((t) => {
const old = byId.get(t.id);
return old ? { ...t, sortOrder: old.sortOrder, kanbanStatus: old.kanbanStatus, isCompleted: old.isCompleted } : t;
})
);
await api.reorderTasks(
entry.snapshot.map((x) => ({ id: x.id, sortOrder: x.sortOrder, kanbanStatus: x.kanbanStatus })),
selectedListId !== 'all' ? selectedListId : undefined
);
showToast('Verschieben rückgängig gemacht');
} else {
setTasks((prev) =>
prev.map((t) => (t.id === entry.id ? { ...t, isCompleted: entry.isCompleted, kanbanStatus: entry.kanbanStatus } : t))
);
await api.updateThought(entry.id, { isCompleted: entry.isCompleted, kanbanStatus: entry.kanbanStatus });
showToast('Rückgängig gemacht');
}
} finally {
await loadData();
onRefreshStats();
}
};
const undoLastRef = useRef(undoLast);
undoLastRef.current = undoLast;
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
if (!(e.ctrlKey || e.metaKey) || e.shiftKey || e.altKey || e.key.toLowerCase() !== 'z') return;
// Text fields keep their own undo
const el = e.target as HTMLElement | null;
if (el && (el.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(el.tagName))) return;
if (undoStackRef.current.length === 0) return;
e.preventDefault();
void undoLastRef.current();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, []);
const handleToggleDone = async (task: Thought) => {
triggerHaptic(15);
pushUndo({ kind: 'status', id: task.id, isCompleted: !!task.isCompleted, kanbanStatus: task.kanbanStatus });
const nextState = !task.isCompleted;
const cols = columnsFor(task);
const doneCol = cols.find((c) => c.isDone) || cols[cols.length - 1];
@@ -615,6 +690,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
const handleSetKanbanStatus = async (task: Thought, nextStatus: string) => {
triggerHaptic(15);
pushUndo({ kind: 'status', id: task.id, isCompleted: !!task.isCompleted, kanbanStatus: task.kanbanStatus });
const targetCol = columnsFor(task).find((c) => c.id === nextStatus);
const isCompleted = targetCol ? !!targetCol.isDone : nextStatus === 'done';
@@ -784,6 +860,8 @@ export const TasksView: React.FC<TasksViewProps> = ({
const handleDeletePermanent = async (id: string) => {
triggerHaptic(20);
await api.deleteThought(id);
pushUndo({ kind: 'delete', ids: [id] });
showToast('Aufgabe im Papierkorb', true);
await loadData();
onRefreshStats();
};
@@ -803,7 +881,13 @@ export const TasksView: React.FC<TasksViewProps> = ({
const handleClearDone = async () => {
triggerHaptic(20);
// The ones that will go (completed and editable here), for undo
const ids = doneTasks.filter((t) => !isTaskReadOnly(t)).map((t) => t.id);
await api.clearDoneTasks(selectedListId === 'all' ? '' : selectedListId);
if (ids.length > 0) {
pushUndo({ kind: 'delete', ids });
showToast(ids.length === 1 ? 'Aufgabe im Papierkorb' : `${ids.length} Aufgaben im Papierkorb`, true);
}
await loadData();
onRefreshStats();
};
@@ -967,6 +1051,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
sortOrder: idx,
}));
pushUndo({ kind: 'order', snapshot: orderSnapshot() });
setTasks(updatedWithSort);
setSortBy('custom');
@@ -1103,6 +1188,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
sortOrder: idx,
}));
pushUndo({ kind: 'order', snapshot: orderSnapshot() });
setTasks(updatedWithSort);
setSortBy('custom');
@@ -1333,7 +1419,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
},
...safeTaskLists.map((tl) => ({
id: `list-${tl.id}`,
label: `${tl.icon || '📋'} ${tl.title}`,
label: tl.icon ? `${tl.icon} ${tl.title}` : tl.title,
badge: tl.tasksCount || undefined,
active: selectedListId === tl.id,
onClick: () => setSelectedListId(tl.id),
@@ -1846,7 +1932,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
<div className="flex sm:hidden items-center justify-between gap-2 px-3 py-2 min-h-[54px] rounded-btn bg-panel border border-border !mt-0">
<div className="flex items-center gap-2 min-w-0">
<span className="text-sm font-semibold text-main truncate">
{currentSelectedList ? `${currentSelectedList.icon} ${currentSelectedList.title}` : '📋 Alle Aufgaben'}
{currentSelectedList ? [currentSelectedList.icon, currentSelectedList.title].filter(Boolean).join(' ') : '📋 Alle Aufgaben'}
</span>
<span className="badge-neutral text-[11px] flex-shrink-0">
{viewMode === 'kanban'
@@ -2101,7 +2187,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
<option value="">Standard (Alle Aufgaben)</option>
{safeTaskLists.map((tl) => (
<option key={tl.id} value={tl.id}>
{tl.icon || '📋'} {tl.title}
{tl.icon ? `${tl.icon} ${tl.title}` : tl.title}
</option>
))}
</select>
@@ -2341,7 +2427,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
<div>
<label className="block text-xs text-muted mb-1">Titel der Liste</label>
<div className="flex items-center gap-1.5">
<EmojiIconButton value={newListIcon} onChange={setNewListIcon} fallback="📋" />
<EmojiIconButton value={newListIcon} onChange={setNewListIcon} />
<input
type="text"
value={newListTitle}
@@ -2356,6 +2442,26 @@ export const TasksView: React.FC<TasksViewProps> = ({
</FormSheet>
)}
{/* Short notice after deleting or undoing (same look as in the workspace) */}
{toast && (
<div
role="status"
className="fixed left-1/2 -translate-x-1/2 bottom-[calc(5.5rem+env(safe-area-inset-bottom))] sm:bottom-6 z-50 max-w-[calc(100vw-1.5rem)] px-3 py-2 rounded-card text-xs text-main bg-panel-elevated border border-border shadow-panel flex items-center gap-3"
>
<span>{toast.text}</span>
{toast.undo && (
<button
type="button"
onClick={() => void undoLast()}
className="font-semibold text-accent hover:underline min-h-[44px] sm:min-h-0 shrink-0"
title="Rückgängig (Strg+Z)"
>
Rückgängig
</button>
)}
</div>
)}
{/* UNIFIED SHARE LIST MODAL */}
{showShareListModal && sharingTaskList && (
<UnifiedShareModal
@@ -2366,7 +2472,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
}}
title={`Aufgabenliste teilen: ${sharingTaskList.title}`}
resourceName={sharingTaskList.title}
resourceIcon={sharingTaskList.icon || '📋'}
resourceIcon={sharingTaskList.icon || undefined}
isOwner={sharingTaskList.isOwner !== false}
currentUserId={currentUser?.id}
shares={(sharingTaskList.shares || []).map((sh) => ({
@@ -2817,7 +2923,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
<p className="text-[11px] text-faint truncate">
{selectedListId === 'all' && (
<>
{listOfItem ? `${listOfItem.icon || '📋'} ${listOfItem.title}` : 'Persönlich'}
{listOfItem ? (listOfItem.icon ? `${listOfItem.icon} ${listOfItem.title}` : listOfItem.title) : 'Persönlich'}
{' · '}
</>
)}