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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-11-01 15:44:34 +01:00
1 parent a927a94a95
commit 5a9c5bac37
36 files changed
+1356 -175

No files matched your search

@@ -0,0 +1,94 @@
import React, { useEffect, useState } from 'react';
import { Loader2, RotateCcw, Trash2 } from 'lucide-react';
import { api, triggerHaptic } from '../../lib/api';
import { formatDeletedAt } from '../../lib/due';
import type { Thought } from '../../lib/types';
import { ReadingModal } from '../../components/ReadingModal';
interface TriageTrashProps {
onClose: () => void;
// Something was restored or deleted: counters and the deck change
onChanged: () => void;
}
// The triage's own trash: thoughts thrown away while sorting. Back into the Quick Dump, or deleted for
// good (one by one, all at once, or after the delay from Kontoeinstellungen → Aufbewahrung). Deleting
// for good takes a second tap on the same button, like the notes trash.
export const TriageTrash: React.FC<TriageTrashProps> = ({ onClose, onChanged }) => {
const [items, setItems] = useState<Thought[] | null>(null);
const [confirmId, setConfirmId] = useState<string | null>(null);
const load = async () => {
const list = await api.getThoughts('discarded').catch(() => []);
setItems(Array.isArray(list) ? list : []);
};
useEffect(() => {
void load();
}, []);
const restore = async (item: Thought) => {
triggerHaptic(15);
await api.triageAction(item.id, 'restore');
await load();
onChanged();
};
const purge = async (id: string) => {
if (confirmId !== id) {
setConfirmId(id);
return;
}
triggerHaptic(20);
setConfirmId(null);
if (id === '__all__') await api.purgeDiscarded();
else await api.deleteThought(id);
await load();
onChanged();
};
return (
<ReadingModal title="Papierkorb der Triage" onClose={onClose}>
{items === null ? (
<div className="py-6 flex justify-center text-muted">
<Loader2 className="w-5 h-5 animate-spin" />
</div>
) : items.length === 0 ? (
<p className="py-6 text-center text-muted">Keine verworfenen Gedanken.</p>
) : (
<div className="space-y-3">
<div className="divide-y divide-border border border-border rounded-card overflow-hidden">
{items.map((item) => (
<div key={item.id} className="px-3 py-2 flex items-center justify-between gap-3">
<div className="min-w-0 flex-1">
<p className="text-sm text-main truncate">{item.content}</p>
{item.triagedAt && <p className="text-[11px] text-faint">verworfen {formatDeletedAt(item.triagedAt)}</p>}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<button type="button" onClick={() => restore(item)} className="btn-action min-h-[38px] sm:min-h-0">
<RotateCcw className="w-3 h-3 text-accent" /> Zurück
</button>
<button
type="button"
onClick={() => purge(item.id)}
className={`btn-icon btn-icon-danger min-h-[38px] min-w-[38px] sm:min-h-0 sm:min-w-0 ${confirmId === item.id ? 'text-status-err' : ''}`}
title={confirmId === item.id ? 'Wirklich endgültig löschen?' : 'Endgültig löschen'}
aria-label="Endgültig löschen"
>
{confirmId === item.id ? <span className="text-[11px] font-semibold px-1">Wirklich?</span> : <Trash2 className="w-3.5 h-3.5" />}
</button>
</div>
</div>
))}
</div>
<div className="flex justify-end">
<button type="button" onClick={() => purge('__all__')} className="btn-action min-h-[38px] sm:min-h-0 text-status-err">
<Trash2 className="w-3.5 h-3.5" />
{confirmId === '__all__' ? `Wirklich ${items.length} endgültig löschen?` : 'Papierkorb leeren'}
</button>
</div>
</div>
)}
</ReadingModal>
);
};