diff --git a/apps/remix/app/components/dialogs/document-delete-dialog.tsx b/apps/remix/app/components/dialogs/document-delete-dialog.tsx deleted file mode 100644 index a802387ef..000000000 --- a/apps/remix/app/components/dialogs/document-delete-dialog.tsx +++ /dev/null @@ -1,201 +0,0 @@ -import { useEffect, useState } from 'react'; - -import { msg } from '@lingui/core/macro'; -import { useLingui } from '@lingui/react'; -import { Trans } from '@lingui/react/macro'; -import { DocumentStatus } from '@prisma/client'; -import { P, match } from 'ts-pattern'; - -import { useLimits } from '@documenso/ee/server-only/limits/provider/client'; -import { trpc as trpcReact } from '@documenso/trpc/react'; -import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; -import { Button } from '@documenso/ui/primitives/button'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@documenso/ui/primitives/dialog'; -import { Input } from '@documenso/ui/primitives/input'; -import { useToast } from '@documenso/ui/primitives/use-toast'; - -type DocumentDeleteDialogProps = { - id: number; - open: boolean; - onOpenChange: (_open: boolean) => void; - onDelete?: () => Promise | void; - status: DocumentStatus; - documentTitle: string; - canManageDocument: boolean; -}; - -export const DocumentDeleteDialog = ({ - id, - open, - onOpenChange, - onDelete, - status, - documentTitle, - canManageDocument, -}: DocumentDeleteDialogProps) => { - const { toast } = useToast(); - const { refreshLimits } = useLimits(); - const { _ } = useLingui(); - - const deleteMessage = msg`delete`; - - const [inputValue, setInputValue] = useState(''); - const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT); - - const { mutateAsync: deleteDocument, isPending } = trpcReact.document.delete.useMutation({ - onSuccess: async () => { - void refreshLimits(); - - toast({ - title: _(msg`Document deleted`), - description: _(msg`"${documentTitle}" has been successfully deleted`), - duration: 5000, - }); - - await onDelete?.(); - - onOpenChange(false); - }, - onError: () => { - toast({ - title: _(msg`Something went wrong`), - description: _(msg`This document could not be deleted at this time. Please try again.`), - variant: 'destructive', - duration: 7500, - }); - }, - }); - - useEffect(() => { - if (open) { - setInputValue(''); - setIsDeleteEnabled(status === DocumentStatus.DRAFT); - } - }, [open, status]); - - const onInputChange = (event: React.ChangeEvent) => { - setInputValue(event.target.value); - setIsDeleteEnabled(event.target.value === _(deleteMessage)); - }; - - return ( - !isPending && onOpenChange(value)}> - - - - Are you sure? - - - - {canManageDocument ? ( - - You are about to delete "{documentTitle}" - - ) : ( - - You are about to hide "{documentTitle}" - - )} - - - - {canManageDocument ? ( - - {match(status) - .with(DocumentStatus.DRAFT, () => ( - - - Please note that this action is irreversible. Once confirmed, - this document will be permanently deleted. - - - )) - .with(DocumentStatus.PENDING, () => ( - -

- - Please note that this action is irreversible. - -

- -

- Once confirmed, the following will occur: -

- -
    -
  • - Document will be permanently deleted -
  • -
  • - Document signing process will be cancelled -
  • -
  • - All inserted signatures will be voided -
  • -
  • - All recipients will be notified -
  • -
-
- )) - .with(P.union(DocumentStatus.COMPLETED, DocumentStatus.REJECTED), () => ( - -

- By deleting this document, the following will occur: -

- -
    -
  • - The document will be hidden from your account -
  • -
  • - Recipients will still retain their copy of the document -
  • -
-
- )) - .exhaustive()} -
- ) : ( - - - Please contact support if you would like to revert this action. - - - )} - - {status !== DocumentStatus.DRAFT && canManageDocument && ( - - )} - - - - - - -
-
- ); -}; diff --git a/apps/remix/app/components/dialogs/document-duplicate-dialog.tsx b/apps/remix/app/components/dialogs/document-duplicate-dialog.tsx deleted file mode 100644 index 9b7c82404..000000000 --- a/apps/remix/app/components/dialogs/document-duplicate-dialog.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { msg } from '@lingui/core/macro'; -import { useLingui } from '@lingui/react'; -import { Trans } from '@lingui/react/macro'; -import { useNavigate } from 'react-router'; - -import { formatDocumentsPath } from '@documenso/lib/utils/teams'; -import { trpc as trpcReact } from '@documenso/trpc/react'; -import { Button } from '@documenso/ui/primitives/button'; -import { - Dialog, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@documenso/ui/primitives/dialog'; -import { useToast } from '@documenso/ui/primitives/use-toast'; - -import { useCurrentTeam } from '~/providers/team'; - -type DocumentDuplicateDialogProps = { - id: string; - token?: string; - open: boolean; - onOpenChange: (_open: boolean) => void; -}; - -export const DocumentDuplicateDialog = ({ - id, - token, - open, - onOpenChange, -}: DocumentDuplicateDialogProps) => { - const navigate = useNavigate(); - - const { toast } = useToast(); - const { _ } = useLingui(); - - const team = useCurrentTeam(); - - const documentsPath = formatDocumentsPath(team.url); - - const { mutateAsync: duplicateEnvelope, isPending: isDuplicating } = - trpcReact.envelope.duplicate.useMutation({ - onSuccess: async ({ id }) => { - toast({ - title: _(msg`Document Duplicated`), - description: _(msg`Your document has been successfully duplicated.`), - duration: 5000, - }); - - await navigate(`${documentsPath}/${id}/edit`); - onOpenChange(false); - }, - }); - - const onDuplicate = async () => { - try { - await duplicateEnvelope({ envelopeId: id }); - } catch { - toast({ - title: _(msg`Something went wrong`), - description: _(msg`This document could not be duplicated at this time. Please try again.`), - variant: 'destructive', - duration: 7500, - }); - } - }; - - return ( - !isDuplicating && onOpenChange(value)}> - - - - Duplicate - - - - -
- - - -
-
-
-
- ); -}; diff --git a/apps/remix/app/components/dialogs/envelope-delete-dialog.tsx b/apps/remix/app/components/dialogs/envelope-delete-dialog.tsx index 2975643f6..3a8cf7dea 100644 --- a/apps/remix/app/components/dialogs/envelope-delete-dialog.tsx +++ b/apps/remix/app/components/dialogs/envelope-delete-dialog.tsx @@ -52,13 +52,23 @@ export const EnvelopeDeleteDialog = ({ const [inputValue, setInputValue] = useState(''); const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT); + const isDocument = type === EnvelopeType.DOCUMENT; + const { mutateAsync: deleteEnvelope, isPending } = trpcReact.envelope.delete.useMutation({ onSuccess: async () => { void refreshLimits(); toast({ - title: t`Document deleted`, - description: t`"${title}" has been successfully deleted`, + title: canManageDocument + ? isDocument + ? t`Document deleted` + : t`Template deleted` + : isDocument + ? t`Document hidden` + : t`Template hidden`, + description: canManageDocument + ? t`"${title}" has been successfully deleted` + : t`"${title}" has been successfully hidden`, duration: 5000, }); @@ -69,7 +79,9 @@ export const EnvelopeDeleteDialog = ({ onError: () => { toast({ title: t`Something went wrong`, - description: t`This document could not be deleted at this time. Please try again.`, + description: isDocument + ? t`This document could not be deleted at this time. Please try again.` + : t`This template could not be deleted at this time. Please try again.`, variant: 'destructive', duration: 7500, }); diff --git a/apps/remix/app/components/dialogs/envelope-duplicate-dialog.tsx b/apps/remix/app/components/dialogs/envelope-duplicate-dialog.tsx index b327a1549..88274c0fa 100644 --- a/apps/remix/app/components/dialogs/envelope-duplicate-dialog.tsx +++ b/apps/remix/app/components/dialogs/envelope-duplicate-dialog.tsx @@ -1,7 +1,6 @@ import { useState } from 'react'; -import { useLingui } from '@lingui/react/macro'; -import { Trans } from '@lingui/react/macro'; +import { Trans, useLingui } from '@lingui/react/macro'; import { EnvelopeType } from '@prisma/client'; import { useNavigate } from 'react-router'; @@ -10,6 +9,7 @@ import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, + DialogClose, DialogContent, DialogDescription, DialogFooter, @@ -41,19 +41,20 @@ export const EnvelopeDuplicateDialog = ({ const team = useCurrentTeam(); + const isDocument = envelopeType === EnvelopeType.DOCUMENT; + const { mutateAsync: duplicateEnvelope, isPending: isDuplicating } = trpc.envelope.duplicate.useMutation({ onSuccess: async ({ id }) => { toast({ - title: t`Envelope Duplicated`, - description: t`Your envelope has been successfully duplicated.`, + title: isDocument ? t`Document Duplicated` : t`Template Duplicated`, + description: isDocument + ? t`Your document has been successfully duplicated.` + : t`Your template has been successfully duplicated.`, duration: 5000, }); - const path = - envelopeType === EnvelopeType.DOCUMENT - ? formatDocumentsPath(team.url) - : formatTemplatesPath(team.url); + const path = isDocument ? formatDocumentsPath(team.url) : formatTemplatesPath(team.url); await navigate(`${path}/${id}/edit`); setOpen(false); @@ -66,7 +67,9 @@ export const EnvelopeDuplicateDialog = ({ } catch { toast({ title: t`Something went wrong`, - description: t`This document could not be duplicated at this time. Please try again.`, + description: isDocument + ? t`This document could not be duplicated at this time. Please try again.` + : t`This template could not be duplicated at this time. Please try again.`, variant: 'destructive', duration: 7500, }); @@ -78,30 +81,25 @@ export const EnvelopeDuplicateDialog = ({ {trigger && {trigger}} - {envelopeType === EnvelopeType.DOCUMENT ? ( - - - Duplicate Document - - + + + {isDocument ? Duplicate Document : Duplicate Template} + + + {isDocument ? ( This document will be duplicated. - - - ) : ( - - - Duplicate Template - - + ) : ( This template will be duplicated. - - - )} + )} + + - + + + - - - - - - ); -}; diff --git a/apps/remix/app/components/dialogs/template-duplicate-dialog.tsx b/apps/remix/app/components/dialogs/template-duplicate-dialog.tsx deleted file mode 100644 index 1e999129f..000000000 --- a/apps/remix/app/components/dialogs/template-duplicate-dialog.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { msg } from '@lingui/core/macro'; -import { useLingui } from '@lingui/react'; -import { Trans } from '@lingui/react/macro'; - -import { trpc as trpcReact } from '@documenso/trpc/react'; -import { Button } from '@documenso/ui/primitives/button'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@documenso/ui/primitives/dialog'; -import { useToast } from '@documenso/ui/primitives/use-toast'; - -type TemplateDuplicateDialogProps = { - id: number; - open: boolean; - onOpenChange: (_open: boolean) => void; -}; - -export const TemplateDuplicateDialog = ({ - id, - open, - onOpenChange, -}: TemplateDuplicateDialogProps) => { - const { _ } = useLingui(); - const { toast } = useToast(); - - const { mutateAsync: duplicateTemplate, isPending } = - trpcReact.template.duplicateTemplate.useMutation({ - onSuccess: () => { - toast({ - title: _(msg`Template duplicated`), - description: _(msg`Your template has been duplicated successfully.`), - duration: 5000, - }); - - onOpenChange(false); - }, - onError: () => { - toast({ - title: _(msg`Error`), - description: _(msg`An error occurred while duplicating template.`), - variant: 'destructive', - }); - }, - }); - - return ( - !isPending && onOpenChange(value)}> - - - - Do you want to duplicate this template? - - - - Your template will be duplicated. - - - - - - - - - - - ); -}; diff --git a/apps/remix/app/components/general/document/document-page-view-dropdown.tsx b/apps/remix/app/components/general/document/document-page-view-dropdown.tsx index 69d85148b..1cd7c8dcb 100644 --- a/apps/remix/app/components/general/document/document-page-view-dropdown.tsx +++ b/apps/remix/app/components/general/document/document-page-view-dropdown.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { Trans } from '@lingui/react/macro'; -import { DocumentStatus } from '@prisma/client'; +import { DocumentStatus, EnvelopeType } from '@prisma/client'; import { Copy, Download, @@ -34,10 +34,10 @@ import { DropdownMenuTrigger, } from '@documenso/ui/primitives/dropdown-menu'; -import { DocumentDeleteDialog } from '~/components/dialogs/document-delete-dialog'; -import { DocumentDuplicateDialog } from '~/components/dialogs/document-duplicate-dialog'; import { DocumentResendDialog } from '~/components/dialogs/document-resend-dialog'; +import { EnvelopeDeleteDialog } from '~/components/dialogs/envelope-delete-dialog'; import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog'; +import { EnvelopeDuplicateDialog } from '~/components/dialogs/envelope-duplicate-dialog'; import { EnvelopeRenameDialog } from '~/components/dialogs/envelope-rename-dialog'; import { EnvelopeSaveAsTemplateDialog } from '~/components/dialogs/envelope-save-as-template-dialog'; import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/document-recipient-link-copy-dialog'; @@ -55,8 +55,6 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP const trpcUtils = trpcReact.useUtils(); - const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false); const [isRenameDialogOpen, setRenameDialogOpen] = useState(false); const recipient = envelope.recipients.find((recipient) => recipient.email === user.email); @@ -124,10 +122,18 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP - setDuplicateDialogOpen(true)}> - - Duplicate - + e.preventDefault()}> +
+ + Duplicate +
+ + } + /> - setDeleteDialogOpen(true)} disabled={isDeleted}> - - Delete - + { + void navigate(documentsPath); + }} + trigger={ + e.preventDefault()}> +
+ + Delete +
+
+ } + /> Share @@ -187,27 +207,6 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP /> - { - void navigate(documentsPath); - }} - /> - - {isDuplicateDialogOpen && ( - - )} - - setDuplicateDialogOpen(true)}> - - Duplicate - + e.preventDefault()}> +
+ + Duplicate +
+ + } + /> */} - setDeleteDialogOpen(true)}> - - {canManageDocument ? _(msg`Delete`) : _(msg`Hide`)} - + e.preventDefault()}> +
+ + {canManageDocument ? _(msg`Delete`) : _(msg`Hide`)} +
+ + } + /> Share @@ -233,22 +250,6 @@ export const DocumentsTableActionDropdown = ({ /> - - - - { const trpcUtils = trpcReact.useUtils(); - const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false); const [isRenameDialogOpen, setRenameDialogOpen] = useState(false); const [isMoveToFolderDialogOpen, setMoveToFolderDialogOpen] = useState(false); @@ -87,10 +90,20 @@ export const TemplatesTableActionDropdown = ({ )} - setDuplicateDialogOpen(true)}> - - Duplicate - + {canMutate && ( + e.preventDefault()}> +
+ + Duplicate +
+ + } + /> + )} {canMutate && ( )} - setDeleteDialogOpen(true)}> - - Delete - + {canMutate && ( + e.preventDefault()}> +
+ + Delete +
+ + } + /> + )} - - - - { await page.getByRole('button', { name: 'Duplicate' }).click(); // Assert toast appears. - await expectToastTextToBeVisible(page, 'Envelope Duplicated'); + await expectToastTextToBeVisible(page, 'Document Duplicated'); // The page should have navigated to the new document's edit page. await expect(page).toHaveURL(/\/documents\/.*\/edit/); @@ -422,7 +422,7 @@ test.describe('template editor', () => { await page.getByRole('button', { name: 'Duplicate' }).click(); // Assert toast appears. - await expectToastTextToBeVisible(page, 'Envelope Duplicated'); + await expectToastTextToBeVisible(page, 'Template Duplicated'); // The page should have navigated to the new template's edit page. await expect(page).toHaveURL(/\/templates\/.*\/edit/); diff --git a/packages/app-tests/e2e/templates/manage-templates.spec.ts b/packages/app-tests/e2e/templates/manage-templates.spec.ts index 4081c3e6d..171c44a8b 100644 --- a/packages/app-tests/e2e/templates/manage-templates.spec.ts +++ b/packages/app-tests/e2e/templates/manage-templates.spec.ts @@ -117,7 +117,13 @@ test('[TEMPLATES]: duplicate template', async ({ page }) => { await expect(page.getByRole('menuitem', { name: 'Duplicate' })).toBeVisible(); await page.getByRole('menuitem', { name: 'Duplicate' }).click(); await page.getByRole('button', { name: 'Duplicate' }).click(); - await expect(page.getByText('Template duplicated').first()).toBeVisible(); + await expect(page.getByText('Template Duplicated').first()).toBeVisible(); + + // The dialog should navigate to the new template's edit page. + await page.waitForURL(/\/templates\/.*\/edit/); + + // Navigate back to the templates list and verify the count is now 2. + await page.goto(`/t/${team.url}/templates`); await expect(page.getByTestId('data-table-count')).toContainText('Showing 2 results'); }); diff --git a/packages/ui/components/document/document-share-button.tsx b/packages/ui/components/document/document-share-button.tsx index c04cb28d3..96ec7f010 100644 --- a/packages/ui/components/document/document-share-button.tsx +++ b/packages/ui/components/document/document-share-button.tsx @@ -9,6 +9,7 @@ import { FaXTwitter } from 'react-icons/fa6'; import { useCopyShareLink } from '@documenso/lib/client-only/hooks/use-copy-share-link'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; +import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { generateTwitterIntent } from '@documenso/lib/universal/generate-twitter-intent'; import { trpc } from '@documenso/trpc/react'; @@ -60,7 +61,9 @@ export const DocumentShareButton = ({ mutateAsync: createOrGetShareLink, data: shareLink, isPending: isCreatingOrGettingShareLink, - } = trpc.document.share.useMutation(); + } = trpc.document.share.useMutation({ + ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, + }); const isLoading = isCreatingOrGettingShareLink || isCopyingShareLink; @@ -138,7 +141,7 @@ export const DocumentShareButton = ({ - + Share your signing experience! @@ -166,7 +169,7 @@ export const DocumentShareButton = ({