diff --git a/apps/remix/app/components/dialogs/envelope-delete-dialog.tsx b/apps/remix/app/components/dialogs/envelope-delete-dialog.tsx new file mode 100644 index 000000000..2975643f6 --- /dev/null +++ b/apps/remix/app/components/dialogs/envelope-delete-dialog.tsx @@ -0,0 +1,215 @@ +import { useEffect, useState } from 'react'; + +import { msg } from '@lingui/core/macro'; +import { useLingui } from '@lingui/react/macro'; +import { Trans } from '@lingui/react/macro'; +import { DocumentStatus, EnvelopeType } 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, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; +import { Input } from '@documenso/ui/primitives/input'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +type EnvelopeDeleteDialogProps = { + id: string; + type: EnvelopeType; + trigger?: React.ReactNode; + onDelete?: () => Promise | void; + status: DocumentStatus; + title: string; + canManageDocument: boolean; +}; + +export const EnvelopeDeleteDialog = ({ + id, + type, + trigger, + onDelete, + status, + title, + canManageDocument, +}: EnvelopeDeleteDialogProps) => { + const { toast } = useToast(); + const { refreshLimits } = useLimits(); + const { t } = useLingui(); + + const deleteMessage = msg`delete`; + + const [open, setOpen] = useState(false); + const [inputValue, setInputValue] = useState(''); + const [isDeleteEnabled, setIsDeleteEnabled] = useState(status === DocumentStatus.DRAFT); + + const { mutateAsync: deleteEnvelope, isPending } = trpcReact.envelope.delete.useMutation({ + onSuccess: async () => { + void refreshLimits(); + + toast({ + title: t`Document deleted`, + description: t`"${title}" has been successfully deleted`, + duration: 5000, + }); + + await onDelete?.(); + + setOpen(false); + }, + onError: () => { + toast({ + title: t`Something went wrong`, + description: t`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 === t(deleteMessage)); + }; + + return ( + !isPending && setOpen(value)}> + {trigger} + + + + + Are you sure? + + + + {canManageDocument ? ( + + You are about to delete "{title}" + + ) : ( + + You are about to hide "{title}" + + )} + + + + {canManageDocument ? ( + + {match(status) + .with(DocumentStatus.DRAFT, () => ( + + {type === EnvelopeType.DOCUMENT ? ( + + Please note that this action is irreversible. Once confirmed, + this document will be permanently deleted. + + ) : ( + + Please note that this action is irreversible. Once confirmed, + this template 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/envelope-distribute-dialog.tsx b/apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx index ca244ade4..dd16f941a 100644 --- a/apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx +++ b/apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx @@ -13,6 +13,7 @@ import * as z from 'zod'; import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; +import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth'; import { getRecipientsWithMissingFields } from '@documenso/lib/utils/recipients'; import { trpc, trpc as trpcReact } from '@documenso/trpc/react'; @@ -116,10 +117,15 @@ export const EnvelopeDistributeDialog = ({ } = form; const { data: emailData, isLoading: isLoadingEmails } = - trpc.enterprise.organisation.email.find.useQuery({ - organisationId: organisation.id, - perPage: 100, - }); + trpc.enterprise.organisation.email.find.useQuery( + { + organisationId: organisation.id, + perPage: 100, + }, + { + ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, + }, + ); const emails = emailData?.data || []; diff --git a/apps/remix/app/components/general/document/document-attachments-popover.tsx b/apps/remix/app/components/general/document/document-attachments-popover.tsx index ae3d5c653..89685dee4 100644 --- a/apps/remix/app/components/general/document/document-attachments-popover.tsx +++ b/apps/remix/app/components/general/document/document-attachments-popover.tsx @@ -8,6 +8,7 @@ import { Paperclip, Plus, X } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; +import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { AppError } from '@documenso/lib/errors/app-error'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; @@ -49,9 +50,16 @@ export const DocumentAttachmentsPopover = ({ const utils = trpc.useUtils(); - const { data: attachments } = trpc.envelope.attachment.find.useQuery({ - envelopeId, - }); + const { data: attachments } = trpc.envelope.attachment.find.useQuery( + { + envelopeId, + }, + { + // Note: The invalidation of the query is manually handled by the onSuccess + // callbacks below for create and delete mutations. + ...DO_NOT_INVALIDATE_QUERY_ON_MUTATION, + }, + ); const { mutateAsync: createAttachment, isPending: isCreating } = trpc.envelope.attachment.create.useMutation({ @@ -143,7 +151,7 @@ export const DocumentAttachmentsPopover = ({

Attachments

-

+

Add links to relevant documents or resources.

@@ -153,7 +161,7 @@ export const DocumentAttachmentsPopover = ({ {attachments?.data.map((attachment) => (

{attachment.label}

@@ -161,7 +169,7 @@ export const DocumentAttachmentsPopover = ({ href={attachment.data} target="_blank" rel="noopener noreferrer" - className="text-muted-foreground hover:text-foreground truncate text-xs underline" + className="truncate text-xs text-muted-foreground underline hover:text-foreground" > {attachment.data} diff --git a/apps/remix/app/components/general/document/document-certificate-qr-view.tsx b/apps/remix/app/components/general/document/document-certificate-qr-view.tsx index 1213319bb..108530843 100644 --- a/apps/remix/app/components/general/document/document-certificate-qr-view.tsx +++ b/apps/remix/app/components/general/document/document-certificate-qr-view.tsx @@ -108,10 +108,10 @@ export const DocumentCertificateQRView = ({ version="current" envelope={{ id: envelopeItems[0].envelopeId, - envelopeItems, status: DocumentStatus.COMPLETED, type: EnvelopeType.DOCUMENT, }} + envelopeItems={envelopeItems} token={token} > { const scrollableContainerRef = useRef(null); - const { envelope, editorFields, relativePath } = useCurrentEnvelopeEditor(); + const { envelope, editorFields, relativePath, editorConfig } = useCurrentEnvelopeEditor(); const { currentEnvelopeItem } = useCurrentEnvelopeRender(); @@ -97,14 +97,10 @@ export const EnvelopeEditorFieldsPage = () => { const isMetaSame = isDeepEqual(selectedField.fieldMeta, fieldMeta); - // Todo: Envelopes - Clean up console logs. if (!isMetaSame) { - console.log('TRIGGER UPDATE'); editorFields.updateFieldByFormId(selectedField.formId, { fieldMeta, }); - } else { - console.log('DATA IS SAME, NO UPDATE'); } }; @@ -250,36 +246,40 @@ export const EnvelopeEditorFieldsPage = () => { selectedEnvelopeItemId={currentEnvelopeItem?.id ?? null} /> - + {editorConfig.fields?.allowAIDetection && ( + <> + - + - + + + )} {/* Field details section. */} diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx index c569683c7..ee4c5fe36 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx @@ -30,21 +30,56 @@ import { EnvelopeItemTitleInput } from './envelope-editor-title-input'; export default function EnvelopeEditorHeader() { const { t } = useLingui(); - const { envelope, isDocument, isTemplate, updateEnvelope, autosaveError, relativePath } = - useCurrentEnvelopeEditor(); + const { + envelope, + isDocument, + isTemplate, + isEmbedded, + updateEnvelope, + autosaveError, + relativePath, + editorConfig, + flushAutosave, + } = useCurrentEnvelopeEditor(); + + const { + embeded, + general: { allowConfigureEnvelopeTitle }, + actions: { allowAttachments, allowDistributing }, + } = editorConfig; + + const handleCreateEmbeddedEnvelope = async () => { + const latestEnvelope = await flushAutosave(); + + embeded?.onCreate?.(latestEnvelope); + }; + + const handleUpdateEmbeddedEnvelope = async () => { + const latestEnvelope = await flushAutosave(); + + embeded?.onUpdate?.(latestEnvelope); + }; return ( diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-preview-page.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-preview-page.tsx index 9b89e979a..0ca06ae2e 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-preview-page.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-preview-page.tsx @@ -205,6 +205,7 @@ export const EnvelopeEditorPreviewPage = () => { ({ diff --git a/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx b/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx index 4cb544627..26bc5e5f8 100644 --- a/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx +++ b/apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx @@ -21,7 +21,7 @@ import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounce import { ZEditorRecipientsFormSchema } from '@documenso/lib/client-only/hooks/use-editor-recipients'; import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; -import { useSession } from '@documenso/lib/client-only/providers/session'; +import { useOptionalSession } from '@documenso/lib/client-only/providers/session'; import type { TDetectedRecipientSchema } from '@documenso/lib/server-only/ai/envelope/detect-recipients/schema'; import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth'; import { nanoid } from '@documenso/lib/universal/id'; @@ -63,8 +63,14 @@ import { AiRecipientDetectionDialog } from '~/components/dialogs/ai-recipient-de import { useCurrentTeam } from '~/providers/team'; export const EnvelopeEditorRecipientForm = () => { - const { envelope, setRecipientsDebounced, updateEnvelope, editorRecipients } = - useCurrentEnvelopeEditor(); + const { + envelope, + setRecipientsDebounced, + updateEnvelope, + editorRecipients, + isEmbedded, + editorConfig, + } = useCurrentEnvelopeEditor(); const organisation = useCurrentOrganisation(); const team = useCurrentTeam(); @@ -72,7 +78,9 @@ export const EnvelopeEditorRecipientForm = () => { const { t } = useLingui(); const { toast } = useToast(); const { remaining } = useLimits(); - const { user } = useSession(); + const { sessionData } = useOptionalSession(); + + const user = sessionData?.user; const [searchParams, setSearchParams] = useSearchParams(); const [recipientSearchQuery, setRecipientSearchQuery] = useState(''); @@ -603,37 +611,41 @@ export const EnvelopeEditorRecipientForm = () => {
- - - - + {editorConfig.recipients?.allowAIDetection && ( + + + + - - {team.preferences.aiFeaturesEnabled ? ( - Detect recipients with AI - ) : ( - Enable AI detection - )} - - + + {team.preferences.aiFeaturesEnabled ? ( + Detect recipients with AI + ) : ( + Enable AI detection + )} + + + )} - + {!isEmbedded && ( + + )} - ))} + {tabs.map((tab) => { + if (tab.id === 'email' && !settings.allowConfigureDistribution) { + return null; + } + + return ( + + ); + })}
@@ -377,137 +391,151 @@ export const EnvelopeEditorSettingsDialog = ({ disabled={form.formState.isSubmitting} key={activeTab} > - {match(activeTab) - .with('general', () => ( + {match({ activeTab, settings }) + .with({ activeTab: 'general' }, () => ( <> - ( - - - Language - - - - + {settings.allowConfigureLanguage && ( + ( + + + Language + + + + - - - Controls the language for the document, including the language - to be used for email notifications, and the final certificate - that is generated and attached to the document. - - - - + + + Controls the language for the document, including the language + to be used for email notifications, and the final certificate + that is generated and attached to the document. + + + + - - + + + - - {Object.entries(SUPPORTED_LANGUAGES).map(([code, language]) => ( - - {language.full} - - ))} - - - - - - )} - /> - ( - - - Allowed Signature Types - - + + {Object.entries(SUPPORTED_LANGUAGES).map(([code, language]) => ( + + {language.full} + + ))} + + + + + + )} + /> + )} - - ({ - label: t(option.label), - value: option.value, - }))} - selectedValues={field.value} - onChange={field.onChange} - className="w-full bg-background" - emptySelectionPlaceholder="Select signature types" - /> - + {settings.allowConfigureSignatureTypes && ( + ( + + + Allowed Signature Types + + - - - )} - /> - ( - - - Date Format - + + ({ + label: t(option.label), + value: option.value, + }), + )} + selectedValues={field.value} + onChange={field.onChange} + className="w-full bg-background" + emptySelectionPlaceholder="Select signature types" + /> + - - - + {settings.allowConfigureDateFormat && ( + ( + + + Date Format + - - - )} - /> - ( - - - Time Zone - + + + + + + + )} + /> + )} + + {settings.allowConfigureTimezone && ( + ( + + + Time Zone + + + + value && field.onChange(value)} + disabled={envelopeHasBeenSent} + /> + + + + + )} + /> + )} - - - )} - /> - ( - - - Redirect URL{' '} - - - - - - - - Add a URL to redirect the user to once the document is signed - - - - - - - - - - - - )} - /> - ( - - - Document Distribution Method - - - - - - -

- - Document Distribution Method - -

- -

- - This is how the document will reach the recipients once the - document is ready for signing. - -

- -
    -
  • - - Email - The recipient will be emailed the - document to sign, approve, etc. - -
  • -
  • - - None - We will generate links which you can - send to the recipients manually. - -
  • -
- - - Note - If you use Links in combination with - direct templates, you will need to manually send the links to - the remaining recipients. - -
-
-
- - - - -
- )} - /> - - )) - .with('email', () => ( - <> - {organisation.organisationClaim.flags.emailDomains && ( + {settings.allowConfigureRedirectUrl && ( ( - - Email Sender + + Redirect URL{' '} + + + + + + + + Add a URL to redirect the user to once the document is signed + + + - + @@ -683,82 +597,204 @@ export const EnvelopeEditorSettingsDialog = ({ /> )} - ( - - - - Reply To Email{' '} - (Optional) - - + {settings.allowConfigureDistribution && ( + ( + + + Document Distribution Method + + + + - - - + +

+ + Document Distribution Method + +

- -
- )} - /> +

+ + This is how the document will reach the recipients once the + document is ready for signing. + +

- ( - - - - Subject (Optional) - - +
    +
  • + + Email - The recipient will be emailed the + document to sign, approve, etc. + +
  • +
  • + + None - We will generate links which you + can send to the recipients manually. + +
  • +
- - - + + Note - If you use Links in combination with + direct templates, you will need to manually send the links to + the remaining recipients. + + + +
- -
- )} - /> + +