9b503f0555
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>
58 lines
2.5 KiB
TypeScript
58 lines
2.5 KiB
TypeScript
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);
|
|
});
|
|
});
|