feat: task trash, triage trash and retention settings for every area; admin can forbid keeping things for good
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m39s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m39s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a927a94a95
commit
5a9c5bac37
36 files changed
+1356
-175
No files matched your search
@@ -28,8 +28,8 @@ import {
|
||||
Filter,
|
||||
Repeat,
|
||||
} 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 { TaskList, Thought, SubTask, Team, User, TaskListMember, KanbanColumn, PriorityLevel, Recurrence, RECURRENCE_LABELS, TrashedTask } from '../../lib/types';
|
||||
import { formatDeletedAt, formatDue, isDueToday, isOverdue, localDay, splitDue } from '../../lib/due';
|
||||
import { readViewSession, writeViewSession } from '../../lib/viewSession';
|
||||
import { useKanbanTouchDrag } from './useKanbanTouchDrag';
|
||||
import { api, triggerHaptic } from '../../lib/api';
|
||||
@@ -129,7 +129,8 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
}, [selectedListId, viewMode]);
|
||||
const [teams, setTeams] = useState<Team[]>([]);
|
||||
const [collaborators, setCollaborators] = useState<User[]>([]);
|
||||
const [discarded, setDiscarded] = useState<Thought[]>([]);
|
||||
// Task trash of the selected list (or all): deleted tasks and cleared or cleaned-up completed ones
|
||||
const [taskTrash, setTaskTrash] = useState<TrashedTask[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Drag and Drop state
|
||||
@@ -215,12 +216,12 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
const listId = selectedListId;
|
||||
if (loadedListRef.current !== listId) setLoading(true);
|
||||
try {
|
||||
const [allTasks, allLists, allTeams, allCollabs, allDiscarded] = await Promise.all([
|
||||
const [allTasks, allLists, allTeams, allCollabs, trash] = await Promise.all([
|
||||
api.getTasks(listId === 'all' ? undefined : listId).catch(() => []),
|
||||
api.getTaskLists().catch(() => []),
|
||||
api.getUserTeams().catch(() => []),
|
||||
api.getCollaborators().catch(() => []),
|
||||
api.getThoughts('discarded').catch(() => []),
|
||||
api.getTaskTrash(listId === 'all' ? '' : listId).catch(() => []),
|
||||
]);
|
||||
if (seq !== loadSeqRef.current) return; // a newer reload is under way
|
||||
loadedListRef.current = listId;
|
||||
@@ -232,7 +233,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
}
|
||||
setTeams(Array.isArray(allTeams) ? allTeams : []);
|
||||
setCollaborators(Array.isArray(allCollabs) ? allCollabs : []);
|
||||
setDiscarded(Array.isArray(allDiscarded) ? allDiscarded : []);
|
||||
setTaskTrash(Array.isArray(trash) ? trash : []);
|
||||
} catch {
|
||||
// Prevent unhandled promise rejection from crashing view
|
||||
} finally {
|
||||
@@ -447,7 +448,6 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
|
||||
const safeTeams = Array.isArray(teams) ? teams : [];
|
||||
const safeCollaborators = Array.isArray(collaborators) ? collaborators : [];
|
||||
const safeDiscarded = Array.isArray(discarded) ? discarded : [];
|
||||
|
||||
// Available unique hashtags from current tasks
|
||||
const allAvailableTags = useMemo(() => {
|
||||
@@ -751,13 +751,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestore = async (item: Thought) => {
|
||||
triggerHaptic(15);
|
||||
await api.triageAction(item.id, 'restore');
|
||||
await loadData();
|
||||
onRefreshStats();
|
||||
};
|
||||
|
||||
// Deleting a task puts it into the task trash (task_trash.go)
|
||||
const handleDeletePermanent = async (id: string) => {
|
||||
triggerHaptic(20);
|
||||
await api.deleteThought(id);
|
||||
@@ -765,37 +759,32 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
onRefreshStats();
|
||||
};
|
||||
|
||||
// Completed tasks: cleared by hand or deleted after the owner's delay (done_cleanup.go)
|
||||
const [doneRetentionDays, setDoneRetentionDays] = useState<number>(currentUser?.doneRetentionDays ?? 30);
|
||||
useEffect(() => {
|
||||
if (currentUser?.doneRetentionDays !== undefined) setDoneRetentionDays(currentUser.doneRetentionDays);
|
||||
}, [currentUser?.doneRetentionDays]);
|
||||
const handleRestoreTrashed = async (item: TrashedTask) => {
|
||||
triggerHaptic(15);
|
||||
await api.restoreTrashedTask(item.id);
|
||||
await loadData();
|
||||
onRefreshStats();
|
||||
};
|
||||
|
||||
const handleChangeDoneRetention = async (days: number) => {
|
||||
const previous = doneRetentionDays;
|
||||
setDoneRetentionDays(days);
|
||||
try {
|
||||
await api.updateSettings({ doneRetentionDays: days });
|
||||
} catch {
|
||||
setDoneRetentionDays(previous);
|
||||
}
|
||||
const handlePurgeTrashed = async (item: TrashedTask) => {
|
||||
triggerHaptic(20);
|
||||
await api.purgeTrashedTask(item.id);
|
||||
await loadData();
|
||||
};
|
||||
|
||||
const handleClearDone = async () => {
|
||||
const scope = selectedListId === 'all' ? 'aller Listen, die du bearbeiten darfst,' : `aus „${currentSelectedList?.title || 'dieser Liste'}“`;
|
||||
if (!window.confirm(`Alle erledigten Aufgaben ${scope} endgültig löschen?`)) return;
|
||||
triggerHaptic(20);
|
||||
await api.clearDoneTasks(selectedListId === 'all' ? '' : selectedListId);
|
||||
await loadData();
|
||||
onRefreshStats();
|
||||
};
|
||||
|
||||
const handlePurgeAllDiscarded = async () => {
|
||||
if (!window.confirm('Alle verworfenen Gedanken endgültig löschen?')) return;
|
||||
const handleEmptyTaskTrash = async () => {
|
||||
const scope = selectedListId === 'all' ? 'aller Listen, die du bearbeiten darfst,' : `von „${currentSelectedList?.title || 'dieser Liste'}“`;
|
||||
if (!window.confirm(`Den Papierkorb ${scope} endgültig leeren?`)) return;
|
||||
triggerHaptic(20);
|
||||
await api.purgeDiscarded();
|
||||
await api.emptyTaskTrash(selectedListId === 'all' ? '' : selectedListId);
|
||||
await loadData();
|
||||
onRefreshStats();
|
||||
};
|
||||
|
||||
const handleQuickAddTask = async (e: React.FormEvent) => {
|
||||
@@ -1278,7 +1267,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
id: 'filter-discarded',
|
||||
label: 'Papierkorb',
|
||||
icon: <Trash2 className="w-4 h-4" />,
|
||||
badge: safeDiscarded.length,
|
||||
badge: taskTrash.length,
|
||||
active: viewMode === 'list' && filter === 'discarded',
|
||||
onClick: () => {
|
||||
setViewMode('list');
|
||||
@@ -1357,7 +1346,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
sortBy,
|
||||
openTasks.length,
|
||||
doneTasks.length,
|
||||
safeDiscarded.length,
|
||||
taskTrash.length,
|
||||
kanbanTasks.length,
|
||||
safeTaskLists,
|
||||
openTasksCount,
|
||||
@@ -1837,7 +1826,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
? `Offen (${openTasks.length})`
|
||||
: filter === 'done'
|
||||
? `Erledigt (${doneTasks.length})`
|
||||
: `Papierkorb (${safeDiscarded.length})`}
|
||||
: `Papierkorb (${taskTrash.length})`}
|
||||
</span>
|
||||
</div>
|
||||
{currentSelectedList && (
|
||||
@@ -2755,12 +2744,12 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
filter === 'discarded' ? 'bg-panel text-main font-semibold' : 'text-muted hover:text-main'
|
||||
}`}
|
||||
>
|
||||
Papierkorb ({safeDiscarded.length})
|
||||
Papierkorb ({taskTrash.length})
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{filter === 'discarded' && safeDiscarded.length > 0 && (
|
||||
<button onClick={handlePurgeAllDiscarded} className="btn-action min-h-[38px] sm:min-h-0">
|
||||
{filter === 'discarded' && taskTrash.some((t) => t.canEdit) && (
|
||||
<button onClick={handleEmptyTaskTrash} className="btn-action min-h-[38px] sm:min-h-0 ml-auto">
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
Papierkorb leeren
|
||||
</button>
|
||||
@@ -2773,68 +2762,51 @@ export const TasksView: React.FC<TasksViewProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* What happens to completed tasks; the delay of the list owner counts */}
|
||||
{filter === 'done' && (
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted px-1">
|
||||
{currentSelectedList && currentSelectedList.isOwner === false ? (
|
||||
<span>
|
||||
Erledigte Aufgaben dieser Liste räumt die Einstellung von {currentSelectedList.ownerName || 'der Eigentümerin bzw. dem Eigentümer'} auf.
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<span>Erledigte Aufgaben löschen</span>
|
||||
<select
|
||||
value={doneRetentionDays}
|
||||
onChange={(e) => handleChangeDoneRetention(Number(e.target.value))}
|
||||
className="h-9 sm:h-6 px-1.5 input-field text-xs bg-panel"
|
||||
aria-label="Erledigte Aufgaben löschen nach"
|
||||
>
|
||||
<option value={7}>nach 7 Tagen</option>
|
||||
<option value={30}>nach 30 Tagen</option>
|
||||
<option value={90}>nach 90 Tagen</option>
|
||||
<option value={365}>nach einem Jahr</option>
|
||||
<option value={0}>nie</option>
|
||||
</select>
|
||||
<span className="text-faint">
|
||||
{doneRetentionDays > 0 ? '(gezählt ab dem Abhaken; gilt für deine eigenen Listen)' : '(bleiben, bis du sie leerst)'}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="p-6 text-center text-faint text-sm">Lade Aufgaben…</div>
|
||||
) : filter === 'discarded' ? (
|
||||
<div className="panel-surface divide-y divide-border">
|
||||
{safeDiscarded.length === 0 ? (
|
||||
<div className="px-4 py-6 text-center text-muted text-sm">
|
||||
Keine verworfenen Gedanken im Papierkorb.
|
||||
</div>
|
||||
{taskTrash.length === 0 ? (
|
||||
<div className="px-4 py-6 text-center text-muted text-sm">Der Papierkorb ist leer.</div>
|
||||
) : (
|
||||
safeDiscarded.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="px-3 py-2 flex items-center justify-between gap-3 hover:bg-panel-hover"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
||||
<Trash2 className="w-4 h-4 text-faint flex-shrink-0" />
|
||||
<p className="text-sm text-muted line-through truncate">{item.content}</p>
|
||||
taskTrash.map((item) => {
|
||||
const listOfItem = item.taskListId ? safeTaskLists.find((l) => l.id === item.taskListId) : undefined;
|
||||
return (
|
||||
<div key={item.id} className="px-3 py-2 flex items-center justify-between gap-3 hover:bg-panel-hover">
|
||||
<div className="flex items-center gap-2.5 min-w-0 flex-1">
|
||||
<Trash2 className="w-4 h-4 text-faint flex-shrink-0" />
|
||||
<div className="min-w-0">
|
||||
<p className={`text-sm truncate ${item.isCompleted ? 'text-muted line-through' : 'text-main'}`}>{item.content}</p>
|
||||
<p className="text-[11px] text-faint truncate">
|
||||
{selectedListId === 'all' && (
|
||||
<>
|
||||
{listOfItem ? `${listOfItem.icon || '📋'} ${listOfItem.title}` : 'Persönlich'}
|
||||
{' · '}
|
||||
</>
|
||||
)}
|
||||
gelöscht {formatDeletedAt(item.trashedAt)}
|
||||
{item.trashedBy ? ` von ${item.trashedBy}` : ' (automatisch)'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{item.canEdit && (
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<button onClick={() => handleRestoreTrashed(item)} className="btn-action min-h-[38px] sm:min-h-0">
|
||||
<RotateCcw className="w-3 h-3 text-accent" /> Zurück
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handlePurgeTrashed(item)}
|
||||
className="btn-icon btn-icon-danger min-h-[38px] min-w-[38px] sm:min-h-0 sm:min-w-0"
|
||||
title="Endgültig löschen"
|
||||
aria-label="Endgültig löschen"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<button onClick={() => handleRestore(item)} className="btn-action">
|
||||
<RotateCcw className="w-3 h-3 text-accent" /> Zurück
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeletePermanent(item.id)}
|
||||
className="btn-icon btn-icon-danger"
|
||||
title="Endgültig löschen"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in new issue
Block a user