feat(kanban): archive automatico para cards Done +30 dias (issue 0092)
Adds an archive layer separate from the trash. Cards in is_done columns that have been there for more than 30 days are auto-archived on the next board load (throttled to once every 30 minutes). Archived cards leave the board but stay in the DB and are listed in a new sidebar drawer "Hecho (archivo)" below the existing Papelera, with a one-click restore. Schema (migration 012_card_archived.sql): - ALTER TABLE cards ADD COLUMN archived_at TEXT; - NULL = active, ISO timestamp = archived. Independent from deleted_at. Backend: - Card.ArchivedAt + JSON; ListCardsWithTime filters archived_at IS NULL. - New methods: ArchiveCard, UnarchiveCard, ListArchivedCards, AutoArchiveDoneOlderThan. - New endpoints: GET /api/archive, POST /api/cards/:id/archive, POST /api/cards/:id/unarchive. - handleGetBoard invokes maybeAutoArchive (atomic throttle, 30 min sweep, 30 day cutoff). Errors logged but never block the board response. Frontend: - Card type + api.ts add the new field and helpers. - App.tsx state for archive list, reload, archive/unarchive handlers. - New sidebar drawer with toggle, count badge, restore button. - KanbanCard gains an "Archivar" menu item (gated on isDone + onArchive prop) for manual archiving of any done card. Tests: - Playwright e2e/archive.spec.ts: manual archive via menu, drawer toggle, unarchive. Picks a done card via /api/board introspection so it stays stable regardless of board state. - Auto-archive of >30d cards: not under e2e (real time travel needed); covered by code review of the SQL query and the throttle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
IconArchive,
|
||||
IconCalendarDue,
|
||||
IconCheck,
|
||||
IconClock,
|
||||
@@ -50,6 +51,7 @@ interface Props {
|
||||
onAssign: (id: string, assignee_id: string | null) => void;
|
||||
onSetDeadline?: (id: string, deadline: string | null) => void;
|
||||
onSetRequester?: (id: string, requester: string) => void;
|
||||
onArchive?: (id: string) => void;
|
||||
requesterOptions?: string[];
|
||||
onOpenCustomColor?: (cardId: string, current: string) => void;
|
||||
activeSticker?: string | null;
|
||||
@@ -100,6 +102,7 @@ interface CardBodyProps {
|
||||
onAssign: (id: string, assignee_id: string | null) => void;
|
||||
onSetDeadline?: (id: string, deadline: string | null) => void;
|
||||
onSetRequester?: (id: string, requester: string) => void;
|
||||
onArchive?: (id: string) => void;
|
||||
onOpenCustomColor?: (cardId: string, current: string) => void;
|
||||
onRemoveSticker?: (cardId: string, index: number) => void;
|
||||
onMoveSticker?: (cardId: string, index: number, x: number, y: number) => void;
|
||||
@@ -127,6 +130,7 @@ const KanbanCardBody = memo(function KanbanCardBody({
|
||||
onAssign,
|
||||
onSetDeadline,
|
||||
onSetRequester,
|
||||
onArchive,
|
||||
onOpenCustomColor,
|
||||
onRemoveSticker,
|
||||
onMoveSticker,
|
||||
@@ -321,6 +325,15 @@ const KanbanCardBody = memo(function KanbanCardBody({
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
)}
|
||||
{isDone && onArchive && (
|
||||
<Menu.Item
|
||||
leftSection={<IconArchive size={14} />}
|
||||
color="teal"
|
||||
onClick={() => { setMenuOpen(false); onArchive(card.id); }}
|
||||
>
|
||||
Archivar
|
||||
</Menu.Item>
|
||||
)}
|
||||
<Menu.Divider />
|
||||
<Menu.Item leftSection={<IconTrash size={14} />} color="red" onClick={() => { setMenuOpen(false); onDelete(card.id); }}>Borrar</Menu.Item>
|
||||
</>
|
||||
@@ -463,6 +476,7 @@ function KanbanCardImpl({
|
||||
onAssign,
|
||||
onSetDeadline,
|
||||
onSetRequester,
|
||||
onArchive,
|
||||
requesterOptions,
|
||||
onOpenCustomColor,
|
||||
activeSticker,
|
||||
@@ -593,6 +607,7 @@ function KanbanCardImpl({
|
||||
onAssign={onAssign}
|
||||
onSetDeadline={onSetDeadline}
|
||||
onSetRequester={onSetRequester}
|
||||
onArchive={onArchive}
|
||||
onOpenCustomColor={onOpenCustomColor}
|
||||
onRemoveSticker={onRemoveSticker}
|
||||
onMoveSticker={onMoveSticker}
|
||||
|
||||
@@ -82,6 +82,7 @@ interface Props {
|
||||
onAssignCard: (id: string, assignee_id: string | null) => void;
|
||||
onSetCardDeadline?: (id: string, deadline: string | null) => void;
|
||||
onSetRequester?: (id: string, requester: string) => void;
|
||||
onArchiveCard?: (id: string) => void;
|
||||
requesterOptions?: string[];
|
||||
onOpenCustomCardColor?: (cardId: string, current: string) => void;
|
||||
activeSticker?: string | null;
|
||||
@@ -117,6 +118,7 @@ function KanbanColumnImpl({
|
||||
onAssignCard,
|
||||
onSetCardDeadline,
|
||||
onSetRequester,
|
||||
onArchiveCard,
|
||||
requesterOptions,
|
||||
onOpenCustomCardColor,
|
||||
activeSticker,
|
||||
@@ -593,6 +595,7 @@ function KanbanColumnImpl({
|
||||
onAssign={onAssignCard}
|
||||
onSetDeadline={onSetCardDeadline}
|
||||
onSetRequester={onSetRequester}
|
||||
onArchive={onArchiveCard}
|
||||
requesterOptions={requesterOptions}
|
||||
onOpenCustomColor={onOpenCustomCardColor}
|
||||
users={users}
|
||||
|
||||
Reference in New Issue
Block a user