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:
@@ -0,0 +1,57 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { pw_kanban_login } from "../../../../frontend/functions/browser/pw_kanban_login";
|
||||
|
||||
const USER = process.env.KANBAN_USER || "e2e_user";
|
||||
const PWD = process.env.KANBAN_PWD || "e2e_test_pw_2026";
|
||||
|
||||
/**
|
||||
* Issue 0092: cards en columnas DONE con >30 dias se mueven al cajon "Hecho".
|
||||
* Test cubre: archivar via menu manual, listar archivo, des-archivar.
|
||||
*/
|
||||
test.describe("kanban archive (issue 0092)", () => {
|
||||
test("archiva una done card via menu y la des-archiva desde el cajon", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await pw_kanban_login(page, { username: USER, password: PWD });
|
||||
|
||||
// Pick a card from a done column (queried directly from the API).
|
||||
const board = await page.request.get("/api/board").then((r) => r.json());
|
||||
const doneCol = (board.columns as Array<{ id: string; is_done: boolean }>).find((c) => c.is_done);
|
||||
if (!doneCol) test.skip(true, "no done column in board");
|
||||
const cardInDone = (board.cards as Array<{ id: string; column_id: string }>).find(
|
||||
(c) => c.column_id === doneCol!.id
|
||||
);
|
||||
if (!cardInDone) test.skip(true, "no card in a done column");
|
||||
const targetId = cardInDone!.id;
|
||||
|
||||
const cardSel = `[data-card-id="${targetId}"]`;
|
||||
const card = page.locator(cardSel).first();
|
||||
await expect(card).toBeVisible();
|
||||
|
||||
// Open the per-card menu. Use dispatchEvent so we ignore viewport scroll constraints.
|
||||
await card.locator('button[aria-label="Acciones"]').dispatchEvent("click");
|
||||
const archiveItem = page.getByRole("menuitem", { name: /Archivar/i }).first();
|
||||
await expect(archiveItem).toBeVisible();
|
||||
await archiveItem.click();
|
||||
|
||||
// Card disappears from board.
|
||||
await expect(card).toHaveCount(0, { timeout: 5000 });
|
||||
|
||||
// Archive drawer toggle visible + opens.
|
||||
const archiveToggle = page.locator('[data-test="archive-toggle"]');
|
||||
await archiveToggle.scrollIntoViewIfNeeded();
|
||||
await archiveToggle.dispatchEvent("click");
|
||||
|
||||
// Archived row appears in the drawer.
|
||||
const archivedRow = page.locator(`[data-archived-card-id="${targetId}"]`);
|
||||
await expect(archivedRow).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Restore from archive (force click — sidebar can be scrollable / off-viewport).
|
||||
await archivedRow.locator("button").first().dispatchEvent("click");
|
||||
|
||||
// Back on board.
|
||||
await expect(page.locator(cardSel).first()).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// No longer in archive.
|
||||
await expect(archivedRow).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user