diff --git a/apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx b/apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx deleted file mode 100644 index 694f2d56a..000000000 --- a/apps/remix/app/components/dialogs/document-move-to-folder-dialog.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; -import { FolderType } from '@documenso/lib/types/folder-type'; -import { formatDocumentsPath } from '@documenso/lib/utils/teams'; -import { trpc } from '@documenso/trpc/react'; -import { Button } from '@documenso/ui/primitives/button'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@documenso/ui/primitives/dialog'; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form'; -import { Input } from '@documenso/ui/primitives/input'; -import { useToast } from '@documenso/ui/primitives/use-toast'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { msg } from '@lingui/core/macro'; -import { useLingui } from '@lingui/react'; -import { Trans } from '@lingui/react/macro'; -import type * as DialogPrimitive from '@radix-ui/react-dialog'; -import { FolderIcon, HomeIcon, Loader2, Search } from 'lucide-react'; -import { useEffect, useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { useNavigate } from 'react-router'; -import { z } from 'zod'; - -import { useCurrentTeam } from '~/providers/team'; - -export type DocumentMoveToFolderDialogProps = { - documentId: number; - open: boolean; - onOpenChange: (open: boolean) => void; - currentFolderId?: string; -} & Omit; - -const ZMoveDocumentFormSchema = z.object({ - folderId: z.string().nullable().optional(), -}); - -type TMoveDocumentFormSchema = z.infer; - -export const DocumentMoveToFolderDialog = ({ - documentId, - open, - onOpenChange, - currentFolderId, - ...props -}: DocumentMoveToFolderDialogProps) => { - const { _ } = useLingui(); - const { toast } = useToast(); - - const navigate = useNavigate(); - const team = useCurrentTeam(); - - const [searchTerm, setSearchTerm] = useState(''); - - const form = useForm({ - resolver: zodResolver(ZMoveDocumentFormSchema), - defaultValues: { - folderId: currentFolderId, - }, - }); - - const { data: folders, isLoading: isFoldersLoading } = trpc.folder.findFoldersInternal.useQuery( - { - parentId: currentFolderId, - type: FolderType.DOCUMENT, - }, - { - enabled: open, - }, - ); - - const { mutateAsync: updateDocument } = trpc.document.update.useMutation(); - - useEffect(() => { - if (!open) { - form.reset(); - setSearchTerm(''); - } else { - form.reset({ folderId: currentFolderId }); - } - }, [open, currentFolderId, form]); - - const onSubmit = async (data: TMoveDocumentFormSchema) => { - try { - await updateDocument({ - documentId, - data: { - folderId: data.folderId ?? null, - }, - }); - - const documentsPath = formatDocumentsPath(team.url); - - if (data.folderId) { - await navigate(`${documentsPath}/f/${data.folderId}`); - } else { - await navigate(documentsPath); - } - - toast({ - title: _(msg`Document moved`), - description: _(msg`The document has been moved successfully.`), - variant: 'default', - }); - - onOpenChange(false); - } catch (err) { - const error = AppError.parseError(err); - - if (error.code === AppErrorCode.NOT_FOUND) { - toast({ - title: _(msg`Error`), - description: _(msg`The folder you are trying to move the document to does not exist.`), - variant: 'destructive', - }); - - return; - } - - if (error.code === AppErrorCode.UNAUTHORIZED) { - toast({ - title: _(msg`Error`), - description: _(msg`You are not allowed to move this document.`), - variant: 'destructive', - }); - - return; - } - - toast({ - title: _(msg`Error`), - description: _(msg`An error occurred while moving the document.`), - variant: 'destructive', - }); - } - }; - - const filteredFolders = folders?.data.filter((folder) => - folder.name.toLowerCase().includes(searchTerm.toLowerCase()), - ); - - return ( - - - - - Move Document to Folder - - - - Select a folder to move this document to. - - - -
- - setSearchTerm(e.target.value)} - className="pl-8" - /> -
- -
- - ( - - - Folder - - - -
- {isFoldersLoading ? ( -
- -
- ) : ( - <> - - - {filteredFolders?.map((folder) => ( - - ))} - - {searchTerm && filteredFolders?.length === 0 && ( -
- No folders found -
- )} - - )} -
-
- -
- )} - /> - - - - - - - - -
-
- ); -}; diff --git a/apps/remix/app/components/dialogs/document-resend-dialog.tsx b/apps/remix/app/components/dialogs/document-resend-dialog.tsx deleted file mode 100644 index 493d07138..000000000 --- a/apps/remix/app/components/dialogs/document-resend-dialog.tsx +++ /dev/null @@ -1,203 +0,0 @@ -import { useSession } from '@documenso/lib/client-only/providers/session'; -import { getRecipientType } from '@documenso/lib/client-only/recipient-type'; -import { AppError } from '@documenso/lib/errors/app-error'; -import type { TRecipientLite } from '@documenso/lib/types/recipient'; -import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter'; -import type { Document } from '@documenso/prisma/types/document-legacy-schema'; -import { trpc as trpcReact } from '@documenso/trpc/react'; -import { cn } from '@documenso/ui/lib/utils'; -import { Button } from '@documenso/ui/primitives/button'; -import { Checkbox } from '@documenso/ui/primitives/checkbox'; -import { - Dialog, - DialogClose, - DialogContent, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '@documenso/ui/primitives/dialog'; -import { DropdownMenuItem } from '@documenso/ui/primitives/dropdown-menu'; -import { Form, FormControl, FormField, FormItem, FormLabel } from '@documenso/ui/primitives/form/form'; -import { useToast } from '@documenso/ui/primitives/use-toast'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { msg } from '@lingui/core/macro'; -import { useLingui } from '@lingui/react'; -import { Trans } from '@lingui/react/macro'; -import { SigningStatus, type Team, type User } from '@prisma/client'; -import { History } from 'lucide-react'; -import { useState } from 'react'; -import { useForm, useWatch } from 'react-hook-form'; -import * as z from 'zod'; - -import { useCurrentTeam } from '~/providers/team'; -import { getDistributeErrorMessage } from '~/utils/toast-error-messages'; - -import { StackAvatar } from '../general/stack-avatar'; - -const FORM_ID = 'resend-email'; - -export type DocumentResendDialogProps = { - document: Pick & { - user: Pick; - recipients: TRecipientLite[]; - team: Pick | null; - }; - recipients: TRecipientLite[]; -}; - -export const ZResendDocumentFormSchema = z.object({ - recipients: z.array(z.number()).min(1, { - message: 'You must select at least one item.', - }), -}); - -export type TResendDocumentFormSchema = z.infer; - -export const DocumentResendDialog = ({ document, recipients }: DocumentResendDialogProps) => { - const { user } = useSession(); - const team = useCurrentTeam(); - - const { toast } = useToast(); - const { _ } = useLingui(); - - const [isOpen, setIsOpen] = useState(false); - const isOwner = document.userId === user.id; - const isCurrentTeamDocument = team && document.team?.url === team.url; - - const isDisabled = - (!isOwner && !isCurrentTeamDocument) || - document.status !== 'PENDING' || - !recipients.some((r) => r.signingStatus === SigningStatus.NOT_SIGNED); - - const { mutateAsync: resendDocument } = trpcReact.document.redistribute.useMutation(); - - const form = useForm({ - resolver: zodResolver(ZResendDocumentFormSchema), - defaultValues: { - recipients: [], - }, - }); - - const { - handleSubmit, - formState: { isSubmitting }, - } = form; - - const selectedRecipients = useWatch({ - control: form.control, - name: 'recipients', - }); - - const onFormSubmit = async ({ recipients }: TResendDocumentFormSchema) => { - try { - await resendDocument({ documentId: document.id, recipients }); - - toast({ - title: _(msg`Document re-sent`), - description: _(msg`Your document has been re-sent successfully.`), - duration: 5000, - }); - - setIsOpen(false); - } catch (err) { - const error = AppError.parseError(err); - const errorMessage = getDistributeErrorMessage(error.code); - - toast({ - title: _(errorMessage.title), - description: _(errorMessage.description), - variant: 'destructive', - duration: 7500, - }); - } - }; - - return ( - - - e.preventDefault()}> - - Resend - - - - - - -

- Who do you want to remind? -

-
-
- -
- - ( - <> - {recipients.map((recipient) => ( - - - - {recipient.email} - - - - - checked - ? onChange([...value, recipient.id]) - : onChange(value.filter((v) => v !== recipient.id)) - } - /> - - - ))} - - )} - /> - - - - -
- - - - - -
-
-
-
- ); -}; diff --git a/apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx b/apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx index 161fd94d0..1a54c7893 100644 --- a/apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx +++ b/apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx @@ -25,14 +25,16 @@ import { Trans, useLingui } from '@lingui/react/macro'; import { DocumentStatus, EnvelopeType, SigningStatus } from '@prisma/client'; import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; +import { match } from 'ts-pattern'; import * as z from 'zod'; import { getDistributeErrorMessage } from '~/utils/toast-error-messages'; import { StackAvatar } from '../general/stack-avatar'; export type EnvelopeRedistributeDialogProps = { - envelope: Pick & { + envelope: Pick & { recipients: TEnvelopeRecipientLite[]; }; + envelopeType?: EnvelopeType; trigger?: React.ReactNode; }; @@ -44,7 +46,7 @@ export const ZEnvelopeRedistributeFormSchema = z.object({ export type TEnvelopeRedistributeFormSchema = z.infer; -export const EnvelopeRedistributeDialog = ({ envelope, trigger }: EnvelopeRedistributeDialogProps) => { +export const EnvelopeRedistributeDialog = ({ envelope, envelopeType, trigger }: EnvelopeRedistributeDialogProps) => { const recipients = envelope.recipients; const { toast } = useToast(); @@ -70,9 +72,23 @@ export const EnvelopeRedistributeDialog = ({ envelope, trigger }: EnvelopeRedist try { await redistributeEnvelope({ envelopeId: envelope.id, recipients }); + const successMessage = match(envelopeType) + .with(EnvelopeType.DOCUMENT, () => ({ + title: t`Document resent`, + description: t`Your document has been resent successfully.`, + })) + .with(EnvelopeType.TEMPLATE, () => ({ + title: t`Template resent`, + description: t`Your template has been resent successfully.`, + })) + .otherwise(() => ({ + title: t`Envelope resent`, + description: t`Your envelope has been resent successfully.`, + })); + toast({ - title: t`Envelope resent`, - description: t`Your envelope has been resent successfully.`, + title: successMessage.title, + description: successMessage.description, duration: 5000, }); diff --git a/apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx b/apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx index f0c81da77..69dbb8793 100644 --- a/apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx +++ b/apps/remix/app/components/dialogs/envelopes-bulk-move-dialog.tsx @@ -28,7 +28,7 @@ export type EnvelopesBulkMoveDialogProps = { open: boolean; onOpenChange: (open: boolean) => void; currentFolderId?: string; - onSuccess?: () => void; + onSuccess?: (folderId: string | null) => Promise | void; } & Omit; const ZBulkMoveFormSchema = z.object({ @@ -99,11 +99,12 @@ export const EnvelopesBulkMoveDialog = ({ await trpcUtils.template.findTemplates.invalidate(); } + await onSuccess?.(data.folderId); + toast({ description: t`Selected items have been moved.`, }); - onSuccess?.(); onOpenChange(false); } catch (err) { const error = AppError.parseError(err); diff --git a/apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx b/apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx deleted file mode 100644 index df619f149..000000000 --- a/apps/remix/app/components/dialogs/template-move-to-folder-dialog.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; -import { FolderType } from '@documenso/lib/types/folder-type'; -import { formatTemplatesPath } from '@documenso/lib/utils/teams'; -import { trpc } from '@documenso/trpc/react'; -import { Button } from '@documenso/ui/primitives/button'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from '@documenso/ui/primitives/dialog'; -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form'; -import { Input } from '@documenso/ui/primitives/input'; -import { useToast } from '@documenso/ui/primitives/use-toast'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { msg } from '@lingui/core/macro'; -import { useLingui } from '@lingui/react'; -import { Trans } from '@lingui/react/macro'; -import type * as DialogPrimitive from '@radix-ui/react-dialog'; -import { FolderIcon, HomeIcon, Loader2, Search } from 'lucide-react'; -import { useEffect, useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { useNavigate } from 'react-router'; -import { z } from 'zod'; - -import { useCurrentTeam } from '~/providers/team'; - -export type TemplateMoveToFolderDialogProps = { - templateId: number; - templateTitle: string; - isOpen: boolean; - onOpenChange: (open: boolean) => void; - currentFolderId?: string | null; -} & Omit; - -const ZMoveTemplateFormSchema = z.object({ - folderId: z.string().nullable().optional(), -}); - -type TMoveTemplateFormSchema = z.infer; - -export function TemplateMoveToFolderDialog({ - templateId, - templateTitle, - isOpen, - onOpenChange, - currentFolderId, - ...props -}: TemplateMoveToFolderDialogProps) { - const { _ } = useLingui(); - const { toast } = useToast(); - - const navigate = useNavigate(); - const team = useCurrentTeam(); - - const [searchTerm, setSearchTerm] = useState(''); - - const form = useForm({ - resolver: zodResolver(ZMoveTemplateFormSchema), - defaultValues: { - folderId: currentFolderId ?? null, - }, - }); - - const { data: folders, isLoading: isFoldersLoading } = trpc.folder.findFoldersInternal.useQuery( - { - parentId: currentFolderId ?? null, - type: FolderType.TEMPLATE, - }, - { - enabled: isOpen, - }, - ); - - const { mutateAsync: updateTemplate } = trpc.template.updateTemplate.useMutation(); - - useEffect(() => { - if (!isOpen) { - form.reset(); - setSearchTerm(''); - } else { - form.reset({ folderId: currentFolderId ?? null }); - } - }, [isOpen, currentFolderId, form]); - - const onSubmit = async (data: TMoveTemplateFormSchema) => { - try { - await updateTemplate({ - templateId, - data: { - folderId: data.folderId ?? null, - }, - }); - - toast({ - title: _(msg`Template moved`), - description: _(msg`The template has been moved successfully.`), - variant: 'default', - }); - - onOpenChange(false); - - const templatesPath = formatTemplatesPath(team.url); - - if (data.folderId) { - void navigate(`${templatesPath}/f/${data.folderId}`); - } else { - void navigate(templatesPath); - } - } catch (err) { - const error = AppError.parseError(err); - - if (error.code === AppErrorCode.NOT_FOUND) { - toast({ - title: _(msg`Error`), - description: _(msg`The folder you are trying to move the template to does not exist.`), - variant: 'destructive', - }); - - return; - } - - toast({ - title: _(msg`Error`), - description: _(msg`An error occurred while moving the template.`), - variant: 'destructive', - }); - } - }; - - const filteredFolders = folders?.data?.filter((folder) => - folder.name.toLowerCase().includes(searchTerm.toLowerCase()), - ); - - return ( - - - - - Move Template to Folder - - - - Move "{templateTitle}" to a folder - - - -
- - setSearchTerm(e.target.value)} - className="pl-8" - /> -
- -
- - ( - - - Folder - - - -
- {isFoldersLoading ? ( -
- -
- ) : ( - <> - - - {filteredFolders?.map((folder) => ( - - ))} - - {searchTerm && filteredFolders?.length === 0 && ( -
- No folders found -
- )} - - )} -
-
- -
- )} - /> - - - - - - - - -
-
- ); -} 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 e18852d95..668a0cd2e 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 @@ -19,6 +19,7 @@ import { Download, Edit, FileOutputIcon, + History, Loader, MoreHorizontal, Pencil, @@ -29,10 +30,10 @@ import { import { useState } from 'react'; import { Link, useNavigate } from 'react-router'; -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 { EnvelopeRedistributeDialog } from '~/components/dialogs/envelope-redistribute-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'; @@ -67,8 +68,6 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP const documentsPath = formatDocumentsPath(team.url); - const nonSignedRecipients = envelope.recipients.filter((item) => item.signingStatus !== 'SIGNED'); - return ( @@ -172,13 +171,20 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP /> )} - + {canManageDocument && ( + e.preventDefault()}> +
+ + Resend +
+ + } + /> + )} item.signingStatus !== 'SIGNED'); - return ( @@ -244,7 +243,25 @@ export const DocumentsTableActionDropdown = ({ row, onMoveDocument }: DocumentsT /> )} - + {canManageDocument && ( + e.preventDefault()}> +
+ + Resend +
+ + } + /> + )} void; + onMoveDocument?: (envelopeId: string) => void; enableSelection?: boolean; rowSelection?: RowSelectionState; onRowSelectionChange?: (selection: RowSelectionState) => void; @@ -117,7 +117,7 @@ export const DocumentsTable = ({ onMoveDocument(row.original.id) : undefined} + onMoveDocument={onMoveDocument ? () => onMoveDocument(row.original.envelopeId) : undefined} /> ), diff --git a/apps/remix/app/components/tables/templates-table-action-dropdown.tsx b/apps/remix/app/components/tables/templates-table-action-dropdown.tsx index 8872a1a78..9d8a288f5 100644 --- a/apps/remix/app/components/tables/templates-table-action-dropdown.tsx +++ b/apps/remix/app/components/tables/templates-table-action-dropdown.tsx @@ -11,15 +11,15 @@ import { Trans } from '@lingui/react/macro'; import { DocumentStatus, EnvelopeType, type TemplateDirectLink } from '@prisma/client'; import { Copy, Download, Edit, FolderIcon, MoreHorizontal, Pencil, Share2Icon, Trash2, Upload } from 'lucide-react'; import { useState } from 'react'; -import { Link } from 'react-router'; +import { Link, useNavigate } from 'react-router'; import { EnvelopeDeleteDialog } from '../dialogs/envelope-delete-dialog'; import { EnvelopeDownloadDialog } from '../dialogs/envelope-download-dialog'; import { EnvelopeDuplicateDialog } from '../dialogs/envelope-duplicate-dialog'; import { EnvelopeRenameDialog } from '../dialogs/envelope-rename-dialog'; +import { EnvelopesBulkMoveDialog } from '../dialogs/envelopes-bulk-move-dialog'; import { TemplateBulkSendDialog } from '../dialogs/template-bulk-send-dialog'; import { TemplateDirectLinkDialog } from '../dialogs/template-direct-link-dialog'; -import { TemplateMoveToFolderDialog } from '../dialogs/template-move-to-folder-dialog'; export type TemplatesTableActionDropdownProps = { row: { @@ -44,6 +44,7 @@ export const TemplatesTableActionDropdown = ({ onDelete, }: TemplatesTableActionDropdownProps) => { const trpcUtils = trpcReact.useUtils(); + const navigate = useNavigate(); const [isRenameDialogOpen, setRenameDialogOpen] = useState(false); const [isMoveToFolderDialogOpen, setMoveToFolderDialogOpen] = useState(false); @@ -153,12 +154,13 @@ export const TemplatesTableActionDropdown = ({ )} - navigate(folderId ? `${templateRootPath}/f/${folderId}` : templateRootPath)} /> (null); + const [documentToMove, setDocumentToMove] = useState(null); const [rowSelection, setRowSelection] = useSessionStorage('documents-bulk-selection', {}); const [isBulkMoveDialogOpen, setIsBulkMoveDialogOpen] = useState(false); @@ -200,8 +202,8 @@ export default function DocumentsPage() { data={data} isLoading={isLoading} isLoadingError={isLoadingError} - onMoveDocument={(documentId) => { - setDocumentToMove(documentId); + onMoveDocument={(envelopeId) => { + setDocumentToMove(envelopeId); setIsMovingDocument(true); }} enableSelection @@ -213,8 +215,9 @@ export default function DocumentsPage() { {documentToMove && ( - { @@ -224,6 +227,9 @@ export default function DocumentsPage() { setDocumentToMove(null); } }} + onSuccess={(destinationFolderId) => + navigate(destinationFolderId ? `${documentsPath}/f/${destinationFolderId}` : documentsPath) + } /> )} diff --git a/packages/app-tests/e2e/envelopes/envelope-expiration-send.spec.ts b/packages/app-tests/e2e/envelopes/envelope-expiration-send.spec.ts index 5aaaa3a58..7af9a2310 100644 --- a/packages/app-tests/e2e/envelopes/envelope-expiration-send.spec.ts +++ b/packages/app-tests/e2e/envelopes/envelope-expiration-send.spec.ts @@ -270,7 +270,7 @@ test('[ENVELOPE_EXPIRATION]: resending refreshes expiresAt', async ({ page }) => await page.getByLabel('test.documenso.com').first().click(); await page.getByRole('button', { name: 'Send reminder' }).click(); - await expect(page.getByText('Document re-sent', { exact: true })).toBeVisible({ + await expect(page.getByText('Document resent', { exact: true })).toBeVisible({ timeout: 10_000, }); diff --git a/packages/app-tests/e2e/teams/team-documents.spec.ts b/packages/app-tests/e2e/teams/team-documents.spec.ts index 3e8192b78..6d5f2ca08 100644 --- a/packages/app-tests/e2e/teams/team-documents.spec.ts +++ b/packages/app-tests/e2e/teams/team-documents.spec.ts @@ -238,7 +238,7 @@ test('[TEAMS]: resend pending team document', async ({ page }) => { await page.getByLabel('test.documenso.com').first().click(); await page.getByRole('button', { name: 'Send reminder' }).click(); - await expectToastTextToBeVisible(page, 'Document re-sent'); + await expectToastTextToBeVisible(page, 'Document resent'); }); test('[TEAMS]: delete draft team document', async ({ page }) => {