Skip to content

feat: Archive/Trash for cards - #420

Open
sai80082 wants to merge 3 commits into
kanbn:mainfrom
sai80082:feat/trash-archive-cards
Open

sai80082 wants to merge 3 commits into
kanbn:mainfrom
sai80082:feat/trash-archive-cards

Conversation

@sai80082

@sai80082 sai80082 commented Feb 25, 2026

Copy link
Copy Markdown

This pull request implements the archive card functionality, allowing users to move cards to an archived state instead of deleting them permanently.

This closes #307.

Below are the UI changes that are done for this feature.
image
image
image
image
image
image

@sai80082

Copy link
Copy Markdown
Author

Hey @hjball. Were you able to review this PR?

If there is some thing i need to change do let me know.

@hjball

hjball commented Apr 8, 2026

Copy link
Copy Markdown
Contributor

Sorry for the delay @sai80082 - I plan on reviewing this one in the next couple days

boardPublicId: z.string().min(12),
}),
)
.output(z.custom<Awaited<ReturnType<typeof cardRepo.getArchivedByBoardId>>>())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've deprecated this pattern now - all response types should be standard zod schemas (defined in the schemas dir)

const card = await cardRepo.getWorkspaceAndCardIdByCardPublicId(
ctx.db,
input.cardPublicId,
{ includeDeleted: true },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should guard against deletedAt being null - we only want to allow hard deleting card that have already been soft deleted, right?

card.createdBy,
);

await cardRepo.unarchive(ctx.db, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we handle cases where the list has been archived?

};

const handleHardDelete = (cardPublicId: string) => {
if (confirm(t`Are you sure you want to permanently delete this card? This action cannot be undone.`)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the modal confirmation pattern instead of browser native dialog

]
: []),
{
label: t`Archived cards`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll only want to show these if the user has the correct permissions

"card:delete",
card.createdBy,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good if hard deleting created an activity log for the audit trail

export default function DeletedCardsView({ boardPublicId }: Props) {
const utils = api.useUtils();

const { data: deletedcards, isLoading } = api.board.deletedCards.useQuery(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const { data: deletedcards, isLoading } = api.board.deletedCards.useQuery(
const { data: deletedCards, isLoading } = api.board.deletedCards.useQuery(

// We need to join with lists to filter by board
return db
.select({
id: cards.id,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we exclude the internal ID from the response if it's not needed - the publicId should be sufficient

) => {
return db
.select({
id: cards.id,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same for this one

createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt"),
deletedAt: timestamp("deletedAt"),
archivedAt: timestamp("archivedAt"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to add an index for archivedAt

@hjball hjball left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies again for the delay @sai80082

Great work so far - I've left some comments that will need to be addressed before this can be merged but I don't think it's far off

@aficiomaquinas

Copy link
Copy Markdown

Context: evaluating kan.bn as an audit-first system of record for human + AI-agent work (I also opened #611 on trail integrity — this comment is only about the trash/restore UX surface).

Strong +1 — and thanks for the thorough patch (verified the migration + router changes). A few notes from an audit-focused reviewer, all minor:

  1. Permission for trash purge: hardDelete reuses card:delete via assertCanDelete. Since soft-deleted (trashed) and never-archived cards are different risk tiers, consider a dedicated card:purge (or card:delete-forever) permission — mirroring the nice card:archive split this PR already introduces — so operators can grant agents archive/unarchive rights without granting permanent-erasure rights. If a backfill migration grants card:purge alongside legacy card:delete (same pattern as the card:archive backfill here), existing workspaces keep working unchanged.

  2. Log the purge: restore logs card.restored and delete logs card.deleted — but hardDelete doesn't log anything (the row + its activities are about to vanish). Consider writing a card.purged activity to a durable place before wiping (or at least log server-side), so the trail explains why the card's history ends. This pairs with Audit-trail integrity: workspace hard-delete cascades destroy card_activity (incl. with #420); activity immutability & scoped agent keys #611's point about card_activity cascades.

  3. Deleted-cards listing: DeletedCardsView exists in this PR — if it enumerates deletedAt IS NOT NULL cards per board, that closes the "soft-deleted cards are invisible zombies" gap for audit purposes. If the query is scoped per-board only, a workspace-level "all deleted cards" view would help audits (one place to review everything pending purge).

None of these block the PR — 1 and 3 could be fast-follows. The archive/restore foundation here is exactly what audit-sensitive deployments need.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] Concept of Done/Archive

3 participants