From 27cd8f9c259d12575d1aea91ac0c276cbd1c1a96 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Thu, 13 Mar 2025 19:45:11 +0000 Subject: [PATCH] feat: deleted documents bin --- .../dialogs/document-delete-dialog.tsx | 11 ++- .../document-signing-recipient-provider.tsx | 5 -- .../general/document/document-status.tsx | 18 +++-- .../tables/documents-table-empty-state.tsx | 7 +- .../_authenticated+/documents._index.tsx | 10 ++- .../server-only/admin/get-documents-stats.ts | 1 + .../server-only/document/find-documents.ts | 76 +++++++++++++++++-- .../lib/server-only/document/get-stats.ts | 72 ++++++++++++++++-- .../prisma/types/extended-document-status.ts | 1 + .../trpc/server/document-router/schema.ts | 1 + 10 files changed, 173 insertions(+), 29 deletions(-) diff --git a/apps/remix/app/components/dialogs/document-delete-dialog.tsx b/apps/remix/app/components/dialogs/document-delete-dialog.tsx index c89e346a0..81008b30e 100644 --- a/apps/remix/app/components/dialogs/document-delete-dialog.tsx +++ b/apps/remix/app/components/dialogs/document-delete-dialog.tsx @@ -162,7 +162,16 @@ export const DocumentDeleteDialog = ({ )) - .exhaustive()} + // DocumentStatus.REJECTED isnt working currently so this is a fallback to prevent 500 error. + // The union should work but currently its not + .otherwise(() => ( + + + Please note that this action is irreversible. Once confirmed, + this document will be permanently deleted. + + + ))} ) : ( diff --git a/apps/remix/app/components/general/document-signing/document-signing-recipient-provider.tsx b/apps/remix/app/components/general/document-signing/document-signing-recipient-provider.tsx index 96a051d56..93a842637 100644 --- a/apps/remix/app/components/general/document-signing/document-signing-recipient-provider.tsx +++ b/apps/remix/app/components/general/document-signing/document-signing-recipient-provider.tsx @@ -38,11 +38,6 @@ export const DocumentSigningRecipientProvider = ({ recipient, targetSigner = null, }: DocumentSigningRecipientProviderProps) => { - // console.log({ - // recipient, - // targetSigner, - // isAssistantMode: !!targetSigner, - // }); return ( icon: File, color: 'text-yellow-500 dark:text-yellow-200', }, - REJECTED: { - label: msg`Rejected`, - labelExtended: msg`Document rejected`, - icon: XCircle, - color: 'text-red-500 dark:text-red-300', + DELETED: { + label: msg`Deleted`, + labelExtended: msg`Document deleted`, + icon: Trash, + color: 'text-red-700 dark:text-red-500', }, INBOX: { label: msg`Inbox`, @@ -53,6 +53,12 @@ export const FRIENDLY_STATUS_MAP: Record labelExtended: msg`Document All`, color: 'text-muted-foreground', }, + REJECTED: { + label: msg`Rejected`, + labelExtended: msg`Document rejected`, + icon: XCircle, + color: 'text-red-500 dark:text-red-300', + }, }; export type DocumentStatusProps = HTMLAttributes & { diff --git a/apps/remix/app/components/tables/documents-table-empty-state.tsx b/apps/remix/app/components/tables/documents-table-empty-state.tsx index e02a1c2bd..3de1b8a9f 100644 --- a/apps/remix/app/components/tables/documents-table-empty-state.tsx +++ b/apps/remix/app/components/tables/documents-table-empty-state.tsx @@ -1,6 +1,6 @@ import { msg } from '@lingui/core/macro'; import { useLingui } from '@lingui/react'; -import { Bird, CheckCircle2 } from 'lucide-react'; +import { Bird, CheckCircle2, Trash } from 'lucide-react'; import { match } from 'ts-pattern'; import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; @@ -30,6 +30,11 @@ export const DocumentsTableEmptyState = ({ status }: DocumentsTableEmptyStatePro message: msg`You have not yet created or received any documents. To create a document please upload one.`, icon: Bird, })) + .with(ExtendedDocumentStatus.DELETED, () => ({ + title: msg`Nothing in the trash`, + message: msg`There are no documents in the trash.`, + icon: Trash, + })) .otherwise(() => ({ title: msg`Nothing to do`, message: msg`All documents have been processed. Any new documents that are sent or received will show here.`, diff --git a/apps/remix/app/routes/_authenticated+/documents._index.tsx b/apps/remix/app/routes/_authenticated+/documents._index.tsx index e8aa3b8db..eb062d12f 100644 --- a/apps/remix/app/routes/_authenticated+/documents._index.tsx +++ b/apps/remix/app/routes/_authenticated+/documents._index.tsx @@ -1,8 +1,7 @@ import { useEffect, useMemo, useState } from 'react'; import { Trans } from '@lingui/react/macro'; -import { useSearchParams } from 'react-router'; -import { Link } from 'react-router'; +import { Link, useSearchParams } from 'react-router'; import { z } from 'zod'; import { formatAvatarUrl } from '@documenso/lib/utils/avatars'; @@ -51,6 +50,7 @@ export default function DocumentsPage() { [ExtendedDocumentStatus.PENDING]: 0, [ExtendedDocumentStatus.COMPLETED]: 0, [ExtendedDocumentStatus.REJECTED]: 0, + [ExtendedDocumentStatus.DELETED]: 0, [ExtendedDocumentStatus.INBOX]: 0, [ExtendedDocumentStatus.ALL]: 0, }); @@ -114,13 +114,17 @@ export default function DocumentsPage() {
- + {[ ExtendedDocumentStatus.INBOX, ExtendedDocumentStatus.PENDING, ExtendedDocumentStatus.COMPLETED, ExtendedDocumentStatus.DRAFT, + ExtendedDocumentStatus.DELETED, ExtendedDocumentStatus.ALL, ].map((value) => ( { [ExtendedDocumentStatus.COMPLETED]: 0, [ExtendedDocumentStatus.REJECTED]: 0, [ExtendedDocumentStatus.ALL]: 0, + [ExtendedDocumentStatus.DELETED]: 0, }; counts.forEach((stat) => { diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index a7a689ee0..ae23e34fc 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -136,18 +136,26 @@ export const findDocuments = async ({ }; } + const deletedDateRange = + status === ExtendedDocumentStatus.DELETED + ? { + gte: DateTime.now().minus({ days: 30 }).toJSDate(), + lte: DateTime.now().toJSDate(), + } + : null; + let deletedFilter: Prisma.DocumentWhereInput = { AND: { OR: [ { userId: user.id, - deletedAt: null, + deletedAt: deletedDateRange, }, { recipients: { some: { email: user.email, - documentDeletedAt: null, + documentDeletedAt: deletedDateRange, }, }, }, @@ -162,19 +170,19 @@ export const findDocuments = async ({ ? [ { teamId: team.id, - deletedAt: null, + deletedAt: deletedDateRange, }, { user: { email: team.teamEmail.email, }, - deletedAt: null, + deletedAt: deletedDateRange, }, { recipients: { some: { email: team.teamEmail.email, - documentDeletedAt: null, + documentDeletedAt: deletedDateRange, }, }, }, @@ -182,7 +190,7 @@ export const findDocuments = async ({ : [ { teamId: team.id, - deletedAt: null, + deletedAt: deletedDateRange, }, ], }, @@ -297,6 +305,14 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => { }, }, }, + { + status: ExtendedDocumentStatus.REJECTED, + recipients: { + some: { + email: user.email, + }, + }, + }, ], })) .with(ExtendedDocumentStatus.INBOX, () => ({ @@ -368,7 +384,24 @@ const findDocumentsFilter = (status: ExtendedDocumentStatus, user: User) => { recipients: { some: { email: user.email, - signingStatus: SigningStatus.REJECTED, + }, + }, + }, + ], + })) + .with(ExtendedDocumentStatus.DELETED, () => ({ + OR: [ + { + userId: user.id, + deletedAt: { + gte: DateTime.now().minus({ days: 30 }).toJSDate(), + not: null, + }, + }, + { + recipients: { + some: { + email: user.email, }, }, }, @@ -410,7 +443,7 @@ const findTeamDocumentsFilter = ( status: ExtendedDocumentStatus, team: Team & { teamEmail: TeamEmail | null }, visibilityFilters: Prisma.DocumentWhereInput[], -) => { +): Prisma.DocumentWhereInput | null => { const teamEmail = team.teamEmail?.email ?? null; return match(status) @@ -599,5 +632,32 @@ const findTeamDocumentsFilter = ( return filter; }) + .with(ExtendedDocumentStatus.DELETED, () => { + return { + OR: teamEmail + ? [ + { + teamId: team.id, + }, + { + user: { + email: teamEmail, + }, + }, + { + recipients: { + some: { + email: teamEmail, + }, + }, + }, + ] + : [ + { + teamId: team.id, + }, + ], + }; + }) .exhaustive(); }; diff --git a/packages/lib/server-only/document/get-stats.ts b/packages/lib/server-only/document/get-stats.ts index 7b12f72c5..e0febed02 100644 --- a/packages/lib/server-only/document/get-stats.ts +++ b/packages/lib/server-only/document/get-stats.ts @@ -1,7 +1,5 @@ -import { TeamMemberRole } from '@prisma/client'; import type { Prisma, User } from '@prisma/client'; -import { SigningStatus } from '@prisma/client'; -import { DocumentVisibility } from '@prisma/client'; +import { DocumentVisibility, SigningStatus, TeamMemberRole } from '@prisma/client'; import { DateTime } from 'luxon'; import { match } from 'ts-pattern'; @@ -17,7 +15,7 @@ export type GetStatsInput = { search?: string; }; -export const getStats = async ({ user, period, search = '', ...options }: GetStatsInput) => { +export const getStats = async ({ user, period, search, ...options }: GetStatsInput) => { let createdAt: Prisma.DocumentWhereInput['createdAt']; if (period) { @@ -30,7 +28,7 @@ export const getStats = async ({ user, period, search = '', ...options }: GetSta }; } - const [ownerCounts, notSignedCounts, hasSignedCounts] = await (options.team + const [ownerCounts, notSignedCounts, hasSignedCounts, deletedCounts] = await (options.team ? getTeamCounts({ ...options.team, createdAt, @@ -45,6 +43,7 @@ export const getStats = async ({ user, period, search = '', ...options }: GetSta [ExtendedDocumentStatus.PENDING]: 0, [ExtendedDocumentStatus.COMPLETED]: 0, [ExtendedDocumentStatus.REJECTED]: 0, + [ExtendedDocumentStatus.DELETED]: 0, [ExtendedDocumentStatus.INBOX]: 0, [ExtendedDocumentStatus.ALL]: 0, }; @@ -71,6 +70,8 @@ export const getStats = async ({ user, period, search = '', ...options }: GetSta } }); + stats[ExtendedDocumentStatus.DELETED] = deletedCounts || 0; + Object.keys(stats).forEach((key) => { if (key !== ExtendedDocumentStatus.ALL && isExtendedDocumentStatus(key)) { stats[ExtendedDocumentStatus.ALL] += stats[key]; @@ -167,6 +168,32 @@ const getCounts = async ({ user, createdAt, search }: GetCountsOption) => { AND: [searchFilter], }, }), + // Deleted count + prisma.document.count({ + where: { + OR: [ + { + userId: user.id, + deletedAt: { + gte: DateTime.now().minus({ days: 30 }).toJSDate(), + not: null, + }, + }, + { + recipients: { + some: { + email: user.email, + documentDeletedAt: { + gte: DateTime.now().minus({ days: 30 }).toJSDate(), + not: null, + }, + }, + }, + }, + ], + AND: [searchFilter], + }, + }), ]); }; @@ -336,5 +363,40 @@ const getTeamCounts = async (options: GetTeamCountsOption) => { }), notSignedCountsGroupByArgs ? prisma.document.groupBy(notSignedCountsGroupByArgs) : [], hasSignedCountsGroupByArgs ? prisma.document.groupBy(hasSignedCountsGroupByArgs) : [], + prisma.document.count({ + where: { + OR: [ + { + teamId, + userId: userIdWhereClause, + deletedAt: { + gte: DateTime.now().minus({ days: 30 }).toJSDate(), + not: null, + }, + }, + { + user: { + email: teamEmail, + }, + deletedAt: { + gte: DateTime.now().minus({ days: 30 }).toJSDate(), + not: null, + }, + }, + { + recipients: { + some: { + email: teamEmail, + documentDeletedAt: { + gte: DateTime.now().minus({ days: 30 }).toJSDate(), + not: null, + }, + }, + }, + }, + ], + AND: [searchFilter], + }, + }), ]); }; diff --git a/packages/prisma/types/extended-document-status.ts b/packages/prisma/types/extended-document-status.ts index a3576750d..67451299b 100644 --- a/packages/prisma/types/extended-document-status.ts +++ b/packages/prisma/types/extended-document-status.ts @@ -4,6 +4,7 @@ export const ExtendedDocumentStatus = { ...DocumentStatus, INBOX: 'INBOX', ALL: 'ALL', + DELETED: 'DELETED', } as const; export type ExtendedDocumentStatus = diff --git a/packages/trpc/server/document-router/schema.ts b/packages/trpc/server/document-router/schema.ts index ea4d36c57..f8fb40d47 100644 --- a/packages/trpc/server/document-router/schema.ts +++ b/packages/trpc/server/document-router/schema.ts @@ -145,6 +145,7 @@ export const ZFindDocumentsInternalResponseSchema = ZFindResultResponse.extend({ [ExtendedDocumentStatus.PENDING]: z.number(), [ExtendedDocumentStatus.COMPLETED]: z.number(), [ExtendedDocumentStatus.REJECTED]: z.number(), + [ExtendedDocumentStatus.DELETED]: z.number(), [ExtendedDocumentStatus.INBOX]: z.number(), [ExtendedDocumentStatus.ALL]: z.number(), }),