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>
|
||||
) : (
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
ChevronDown,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { SharedList, Thought } from '../../lib/types';
|
||||
import { addDays, formatDue } from '../../lib/due';
|
||||
@@ -32,6 +33,9 @@ interface TriageDeckProps {
|
||||
onDeckChange: () => void;
|
||||
onOpenDocument: (docId: string) => void;
|
||||
onFocusCapture: () => void;
|
||||
// The triage's own trash (TriageTrash): thoughts thrown away while sorting
|
||||
discardedCount: number;
|
||||
onOpenTrash: () => void;
|
||||
}
|
||||
|
||||
interface SwipeableQueueItemProps {
|
||||
@@ -219,6 +223,8 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
lists,
|
||||
onDeckChange,
|
||||
onFocusCapture,
|
||||
discardedCount,
|
||||
onOpenTrash,
|
||||
}) => {
|
||||
const writableLists = lists.filter((l) => l.isOwner || l.role !== 'viewer');
|
||||
const singleWritableList = writableLists.length === 1 ? writableLists[0] : null;
|
||||
@@ -469,6 +475,13 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
Letzte Aktion rückgängig
|
||||
</button>
|
||||
)}
|
||||
|
||||
{discardedCount > 0 && (
|
||||
<button onClick={onOpenTrash} className="btn-action h-10 px-4 text-sm">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
Papierkorb ({discardedCount})
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -488,15 +501,24 @@ export const TriageDeck: React.FC<TriageDeckProps> = ({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{lastTriaged && (
|
||||
<button
|
||||
onClick={handleUndo}
|
||||
className="btn-action h-8 px-3 text-xs"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5 text-accent" />
|
||||
Rückgängig
|
||||
</button>
|
||||
)}
|
||||
<div className="flex items-center gap-2">
|
||||
{lastTriaged && (
|
||||
<button
|
||||
onClick={handleUndo}
|
||||
className="btn-action h-8 px-3 text-xs"
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5 text-accent" />
|
||||
Rückgängig
|
||||
</button>
|
||||
)}
|
||||
{/* Phones reach the trash through the menu */}
|
||||
{discardedCount > 0 && (
|
||||
<button onClick={onOpenTrash} className="hidden sm:inline-flex btn-action h-8 px-3 text-xs" title="Papierkorb der Triage">
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
{discardedCount}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card Stack Container */}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -71,6 +71,8 @@ describe('TriageDeck single-list shortcut', () => {
|
||||
onDeckChange={onDeckChange}
|
||||
onOpenDocument={vi.fn()}
|
||||
onFocusCapture={vi.fn()}
|
||||
discardedCount={0}
|
||||
onOpenTrash={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -100,6 +102,8 @@ describe('TriageDeck single-list shortcut', () => {
|
||||
onDeckChange={onDeckChange}
|
||||
onOpenDocument={vi.fn()}
|
||||
onFocusCapture={vi.fn()}
|
||||
discardedCount={0}
|
||||
onOpenTrash={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -123,6 +127,8 @@ describe('TriageDeck single-list shortcut', () => {
|
||||
onDeckChange={onDeckChange}
|
||||
onOpenDocument={vi.fn()}
|
||||
onFocusCapture={vi.fn()}
|
||||
discardedCount={0}
|
||||
onOpenTrash={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -149,6 +155,8 @@ describe('TriageDeck single-list shortcut', () => {
|
||||
onDeckChange={onDeckChange}
|
||||
onOpenDocument={vi.fn()}
|
||||
onFocusCapture={vi.fn()}
|
||||
discardedCount={0}
|
||||
onOpenTrash={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -171,6 +179,8 @@ describe('TriageDeck single-list shortcut', () => {
|
||||
onDeckChange={onDeckChange}
|
||||
onOpenDocument={vi.fn()}
|
||||
onFocusCapture={vi.fn()}
|
||||
discardedCount={0}
|
||||
onOpenTrash={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
Reference in new issue
Block a user