Skip to content

#369 Make Draft Deletion Recoverable, Preserving Historical Answers - #601

Merged
cielbellerose merged 1 commit into
devfrom
369-make-draft-deletion-recoverable
Aug 26, 2026
Merged

#369 Make Draft Deletion Recoverable, Preserving Historical Answers#601
cielbellerose merged 1 commit into
devfrom
369-make-draft-deletion-recoverable

Conversation

@cielbellerose

@cielbellerose cielbellerose commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Closes #369

Summary

  • deleteDraftApplication now soft-deletes (deletedAt/deletedById) instead of hard-deleting the Application row and both answer tables, so an applicant's historical answers survive a delete.
  • A new restoreDraftApplication clears the same fields on the same row — no new row is ever inserted, so @@unique([userId, positionId]) is never at risk and no migration is needed.
  • createDraftApplication, createOrUpdateApplicationAnswer and submitApplication each reject a soft-deleted draft with an actionable message instead of silently writing through it or reporting the wrong reason.
  • /my-applications surfaces a Deleted badge, a muted unlinked title, and a Restore action for a deleted draft; /positions/[id]/apply shows a "You deleted this draft" card with the same Restore control.
  • The delete confirmation copy no longer claims deletion is permanent.
  • Reconciliation after a restore (new global/position questions, changed questions, deleted questions) needed no new code — it reuses the same live-question-list and resolveGlobalAnswerValues paths that already handle a draft whose questions changed underneath it; a new db suite pins that claim.

Changes

  • prisma/actions/applications.ts — soft-delete deleteDraftApplication; new restoreDraftApplication; shared DRAFT_DELETED_MESSAGE; deleted-row guards in createDraftApplication, createOrUpdateApplicationAnswer, submitApplication.
  • prisma/actions/question-files.tsreadAndWriteAnswerValue's inner application lookup now excludes a soft-deleted draft too.
  • prisma/data/applications.tsapplicationSelect gains deletedAt; getMyApplications surfaces deleted drafts (and only drafts); getApplicationForApply stops filtering deletedAt, staying in lockstep with createDraftApplication.
  • lib/types.tsMyApplicationListItem mirrors the new deletedAt field.
  • lib/constants.tsDELETED_APPLICATION_LABEL.
  • components/features/restore-draft-button.tsx — new client leaf, the Restore control shared by the list row and the apply-page card.
  • components/features/my-application-row-actions.tsx — Restore branch for a deleted row; rewritten delete-confirmation copy and toasts.
  • components/features/my-application-primary-action.tsx — no primary action on a deleted row.
  • components/features/my-applications-table.tsx — Deleted badge, muted unlinked title, Status-column sort grouping for deleted rows.
  • components/features/status-badge.tsx — optional deletedAt on ApplicationStatusBadge, outranking status.
  • app/(main)/(auth)/positions/[id]/apply/page.tsx — "You deleted this draft" card between the closed-window branch and the profile-gate branch.
  • app/(main)/(auth)/my-applications/[id]/page.tsx — threads deletedAt through to MyApplicationRowActions.
  • tests/db/draft-recovery.test.ts — new: delete/restore lifecycle, write-path guards, reconciliation after restore, and list/reviewer-query visibility.
  • docs/WORKFLOWS.md — rewrote AP-15, added AP-17, updated AP-5/AP-6/AP-9/AP-10.
  • docs/PERMISSIONS.md — updated the draft row of the application-lifecycle table and added a bullet on recoverable deletion.

Testing plan

  • Start a draft, answer a global and a position question (include a file upload), then delete it — confirmation reads "…will be marked deleted. Your answers are kept…" and the toast offers restore.
  • The deleted row stays at the top of /my-applications with a Deleted badge, an unlinked title, "—" for Applied, and only a Restore action; the title is not a link; /my-applications/<id> still 404s directly.
  • The dashboard draft count and every position's application counts exclude the deleted draft; it appears nowhere in /applications or a position card as a manager/admin.
  • Visit /positions/<id>/apply while deleted → "You deleted this draft" card with Restore, View my applications, and Back to position.
  • Restore from /positions/<id>/apply → the stepper reopens with every answer, including the uploaded file, present and downloadable.
  • Delete again, add a required global question as admin, restore → stepper opens in Customize mode with the new-required-questions callout; submit is blocked naming it; answering it lets submit succeed.
  • Delete again, add a required position question as a manager, restore → step 2 shows it unanswered and blocks submit until answered.
  • Delete again, change an answered global question's type, restore → the answer surfaces via AnswerMismatchNotice instead of vanishing.
  • Delete again, soft-delete an answered question as admin, restore → submit still succeeds; the orphan answer doesn't block it.
  • Two tabs: delete in tab A, then Start application / edit a field / Submit in tab B → each shows "You deleted this draft. Restore it from My Applications to keep working on it." and tab B refreshes onto the restore card.
  • Two tabs: restore in tab A, then Restore in tab B → "This draft can no longer be restored."
  • Restore a draft whose position has since closed → it restores; the apply page shows "Applications are closed" and submit stays blocked.
  • Keyboard-only pass over a deleted row and the apply-page card: Restore reachable, visible focus, spinner state reflected via the disabled button.
  • 375px / 768px / 1280px: the mobile card shows the Deleted badge and Restore without overflow.
  • npm run prettier:check && npm run eslint:check && npm run tsc:check && npm run test (db suite requires local Postgres — not runnable in this sandbox; test:unit passes).

Automated checks

  • npm run prettier:check — pass
  • npm run eslint:check — pass
  • npm run tsc:check — pass
  • npm run test:unit — pass (227/227). The db project, including the new tests/db/draft-recovery.test.ts suite, could not run in this sandbox — the repo's docker-compose.yml binds host port 5432, which was already occupied by an unrelated container, and no DATABASE_URL was otherwise configured. CI's run-tests job runs the full suite against Postgres.

Notes

  • Blobs on a deleted draft are never reclaimed — deliberate, since the file answer is one of the "historical answers" the ticket exists to preserve; cleanupOrphanedBlob never runs on delete.
  • There is no permanent-delete path anymore. An applicant who deleted a draft to start over has to restore it and clear the fields — worth a follow-up ticket if it comes up in practice.
  • The detail route (/my-applications/[id]) stays a 404 for a deleted draft, matching AP-11's existing soft-delete behavior; widening it would also widen downloadQuestionFileAnswer's shared ownership where.

deleteDraftApplication now soft-deletes (deletedAt/deletedById) instead
of hard-deleting the row and both answer tables. restoreDraftApplication
reuses the same row, so the existing (userId, positionId) unique
constraint is never in play. createDraftApplication,
createOrUpdateApplicationAnswer and submitApplication each reject a
soft-deleted draft with actionable copy instead of writing through it.
/my-applications and the apply page surface a Restore action; every
reviewer query already excluded soft-deleted rows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@cielbellerose cielbellerose added the claude Will be worked on by Claude label Aug 25, 2026
@cielbellerose cielbellerose self-assigned this Aug 25, 2026
@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
aplio Ready Ready Preview Aug 25, 2026 6:30pm

@cielbellerose cielbellerose added ready for review PR ready for review agent reviewing Review agent working (in-flight) and removed ready for review PR ready for review agent labels Aug 25, 2026

@cielbellerose cielbellerose left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Code Review — Cycle 1 · approved

0 open — no findings

@cielbellerose cielbellerose added approved Review passed, ready to merge and removed reviewing Review agent working (in-flight) labels Aug 25, 2026
@cielbellerose
cielbellerose merged commit 08649ad into dev Aug 26, 2026
14 of 19 checks passed
@cielbellerose
cielbellerose deleted the 369-make-draft-deletion-recoverable branch August 26, 2026 01:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Review passed, ready to merge claude Will be worked on by Claude

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make Draft Deletion Recoverable, Preserving Historical Answers

1 participant