diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index f2342f446..50c25f923 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -31,6 +31,9 @@ jobs: - name: Create the database run: npm run prisma:migrate-dev + - name: Seed the database + run: npm run prisma:seed + - name: Run Playwright tests run: npm run ci diff --git a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx index c3e1f971c..9c3532f88 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx @@ -32,7 +32,7 @@ import { } from '@documenso/ui/primitives/dropdown-menu'; import { ResendDocumentActionItem } from './_action-items/resend-document'; -import { DeleteDraftDocumentDialog } from './delete-draft-document-dialog'; +import { DeleteDocumentDialog } from './delete-document-dialog'; import { DuplicateDocumentDialog } from './duplicate-document-dialog'; export type DataTableActionDropdownProps = { @@ -60,7 +60,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = // const isPending = row.status === DocumentStatus.PENDING; const isComplete = row.status === DocumentStatus.COMPLETED; // const isSigned = recipient?.signingStatus === SigningStatus.SIGNED; - const isDocumentDeletable = isOwner && row.status === DocumentStatus.DRAFT; + const isDocumentDeletable = isOwner; const onDownloadClick = async () => { let document: DocumentWithData | null = null; @@ -161,8 +161,9 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = {isDocumentDeletable && ( - diff --git a/apps/web/src/app/(dashboard)/documents/data-table.tsx b/apps/web/src/app/(dashboard)/documents/data-table.tsx index 9d07b8278..c8adb1422 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table.tsx @@ -8,6 +8,7 @@ import { useSession } from 'next-auth/react'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; import type { FindResultSet } from '@documenso/lib/types/find-result-set'; import type { Document, Recipient, User } from '@documenso/prisma/client'; +import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status'; import { DataTable } from '@documenso/ui/primitives/data-table'; import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; @@ -74,12 +75,14 @@ export const DocumentsDataTable = ({ results }: DocumentsDataTableProps) => { }, { header: 'Actions', - cell: ({ row }) => ( -
- - -
- ), + cell: ({ row }) => + (!row.original.deletedAt || + row.original.status === ExtendedDocumentStatus.COMPLETED) && ( +
+ + +
+ ), }, ]} data={results.data} diff --git a/apps/web/src/app/(dashboard)/documents/delete-draft-document-dialog.tsx b/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx similarity index 53% rename from apps/web/src/app/(dashboard)/documents/delete-draft-document-dialog.tsx rename to apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx index 1a458a13d..5b4a84286 100644 --- a/apps/web/src/app/(dashboard)/documents/delete-draft-document-dialog.tsx +++ b/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx @@ -1,5 +1,8 @@ +import { useState } from 'react'; + import { useRouter } from 'next/navigation'; +import { DocumentStatus } from '@documenso/prisma/client'; import { trpc as trpcReact } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { @@ -10,41 +13,46 @@ import { DialogHeader, DialogTitle, } from '@documenso/ui/primitives/dialog'; +import { Input } from '@documenso/ui/primitives/input'; import { useToast } from '@documenso/ui/primitives/use-toast'; type DeleteDraftDocumentDialogProps = { id: number; open: boolean; onOpenChange: (_open: boolean) => void; + status: DocumentStatus; }; -export const DeleteDraftDocumentDialog = ({ +export const DeleteDocumentDialog = ({ id, open, onOpenChange, + status, }: DeleteDraftDocumentDialogProps) => { const router = useRouter(); const { toast } = useToast(); - const { mutateAsync: deleteDocument, isLoading } = - trpcReact.document.deleteDraftDocument.useMutation({ - onSuccess: () => { - router.refresh(); + const [inputValue, setInputValue] = useState(''); + const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT); - toast({ - title: 'Document deleted', - description: 'Your document has been successfully deleted.', - duration: 5000, - }); + const { mutateAsync: deleteDocument, isLoading } = trpcReact.document.deleteDocument.useMutation({ + onSuccess: () => { + router.refresh(); - onOpenChange(false); - }, - }); + toast({ + title: 'Document deleted', + description: 'Your document has been successfully deleted.', + duration: 5000, + }); - const onDraftDelete = async () => { + onOpenChange(false); + }, + }); + + const onDelete = async () => { try { - await deleteDocument({ id }); + await deleteDocument({ id, status }); } catch { toast({ title: 'Something went wrong', @@ -55,6 +63,11 @@ export const DeleteDraftDocumentDialog = ({ } }; + const onInputChange = (event: React.ChangeEvent) => { + setInputValue(event.target.value); + setIsDeleteEnabled(event.target.value === 'delete'); + }; + return ( !isLoading && onOpenChange(value)}> @@ -67,6 +80,17 @@ export const DeleteDraftDocumentDialog = ({ + {status !== DocumentStatus.DRAFT && ( +
+ +
+ )} +
diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx index b9a8ba6d7..54757667a 100644 --- a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx @@ -67,18 +67,24 @@ export default async function CompletedSigningPage({ />
- {match(document.status) - .with(DocumentStatus.COMPLETED, () => ( + {match({ status: document.status, deletedAt: document.deletedAt }) + .with({ status: DocumentStatus.COMPLETED }, () => (
Everyone has signed
)) - .otherwise(() => ( + .with({ deletedAt: null }, () => (
Waiting for others to sign
+ )) + .otherwise(() => ( +
+ + Document no longer available to sign +
))}

@@ -86,16 +92,22 @@ export default async function CompletedSigningPage({ "{document.title}"

- {match(document.status) - .with(DocumentStatus.COMPLETED, () => ( + {match({ status: document.status, deletedAt: document.deletedAt }) + .with({ status: DocumentStatus.COMPLETED }, () => (

Everyone has signed! You will receive an Email copy of the signed document.

)) - .otherwise(() => ( + .with({ deletedAt: null }, () => (

You will receive an Email copy of the signed document once everyone has signed.

+ )) + .otherwise(() => ( +

+ This document has been cancelled by the owner and is no longer available for others to + sign. +

))}
diff --git a/apps/web/src/app/(signing)/sign/[token]/no-longer-available.tsx b/apps/web/src/app/(signing)/sign/[token]/no-longer-available.tsx new file mode 100644 index 000000000..8c7051caa --- /dev/null +++ b/apps/web/src/app/(signing)/sign/[token]/no-longer-available.tsx @@ -0,0 +1,66 @@ +import React from 'react'; + +import Link from 'next/link'; + +import { Clock8 } from 'lucide-react'; +import { useSession } from 'next-auth/react'; + +import signingCelebration from '@documenso/assets/images/signing-celebration.png'; +import type { Document, Signature } from '@documenso/prisma/client'; +import { SigningCard3D } from '@documenso/ui/components/signing-card'; + +type NoLongerAvailableProps = { + document: Document; + recipientName: string; + recipientSignature: Signature; +}; + +export const NoLongerAvailable = ({ + document, + recipientName, + recipientSignature, +}: NoLongerAvailableProps) => { + const { data: session } = useSession(); + + return ( +
+ + +
+
+ + Document Cancelled +
+ +

+ "{document.title}" + is no longer available to sign +

+ +

+ This document has been cancelled by the owner. +

+ + {session?.user ? ( + + Go Back Home + + ) : ( +

+ Want to send slick signing links like this one?{' '} + + Check out Documenso. + +

+ )} +
+
+ ); +}; diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index 67e679412..17789453e 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -8,6 +8,7 @@ import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document'; import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token'; import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; +import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures'; import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client'; import { Card, CardContent } from '@documenso/ui/primitives/card'; import { ElementVisible } from '@documenso/ui/primitives/element-visible'; @@ -17,6 +18,7 @@ import { DateField } from './date-field'; import { EmailField } from './email-field'; import { SigningForm } from './form'; import { NameField } from './name-field'; +import { NoLongerAvailable } from './no-longer-available'; import { SigningProvider } from './provider'; import { SignatureField } from './signature-field'; @@ -55,6 +57,18 @@ export default async function SigningPage({ params: { token } }: SigningPageProp redirect(`/sign/${token}/complete`); } + const recipientSignature = (await getRecipientSignatures({ recipientId: recipient.id }))[0]; + + if (document.deletedAt) { + return ( + + ); + } + return ( { return ( -