;
-export const SigningDisclosure = ({ className, ...props }: SigningDisclosureProps) => {
+export const DocumentSigningDisclosure = ({
+ className,
+ ...props
+}: DocumentSigningDisclosureProps) => {
return (
@@ -22,7 +24,7 @@ export const SigningDisclosure = ({ className, ...props }: SigningDisclosureProp
Read the full{' '}
signature disclosure
diff --git a/apps/web/src/app/(signing)/sign/[token]/dropdown-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-dropdown-field.tsx
similarity index 84%
rename from apps/web/src/app/(signing)/sign/[token]/dropdown-field.tsx
rename to apps/remix/app/components/general/document-signing/document-signing-dropdown-field.tsx
index 5f4e1a444..b2d5a4b0f 100644
--- a/apps/web/src/app/(signing)/sign/[token]/dropdown-field.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-dropdown-field.tsx
@@ -1,18 +1,14 @@
-'use client';
+import { useEffect, useState } from 'react';
-import { useEffect, useState, useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Loader } from 'lucide-react';
+import { useRevalidator } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZDropdownFieldMeta } from '@documenso/lib/types/field-meta';
-import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -29,29 +25,28 @@ import {
} from '@documenso/ui/primitives/select';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-import { SigningFieldContainer } from './signing-field-container';
+import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
+import { DocumentSigningFieldContainer } from './document-signing-field-container';
+import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
-export type DropdownFieldProps = {
+export type DocumentSigningDropdownFieldProps = {
field: FieldWithSignatureAndFieldMeta;
- recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
};
-export const DropdownField = ({
+export const DocumentSigningDropdownField = ({
field,
- recipient,
onSignField,
onUnsignField,
-}: DropdownFieldProps) => {
+}: DocumentSigningDropdownFieldProps) => {
const { _ } = useLingui();
const { toast } = useToast();
+ const { revalidate } = useRevalidator();
- const router = useRouter();
- const [isPending, startTransition] = useTransition();
+ const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
- const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
+ const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
const parsedFieldMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
const isReadOnly = parsedFieldMeta?.readOnly;
@@ -66,7 +61,7 @@ export const DropdownField = ({
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
+ const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const shouldAutoSignField =
(!field.inserted && localChoice) || (!field.inserted && isReadOnly && defaultValue);
@@ -91,7 +86,8 @@ export const DropdownField = ({
}
setLocalChoice('');
- startTransition(() => router.refresh());
+
+ await revalidate();
} catch (err) {
const error = AppError.parseError(err);
@@ -103,7 +99,9 @@ export const DropdownField = ({
toast({
title: _(msg`Error`),
- description: _(msg`An error occurred while signing the document.`),
+ description: isAssistantMode
+ ? _(msg`An error occurred while signing as assistant.`)
+ : _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@@ -128,13 +126,14 @@ export const DropdownField = ({
}
setLocalChoice('');
- startTransition(() => router.refresh());
+
+ await revalidate();
} catch (err) {
console.error(err);
toast({
title: _(msg`Error`),
- description: _(msg`An error occurred while removing the signature.`),
+ description: _(msg`An error occurred while removing the field.`),
variant: 'destructive',
});
}
@@ -164,7 +163,7 @@ export const DropdownField = ({
return (
-
)}
-
+
);
};
diff --git a/apps/web/src/app/(signing)/sign/[token]/email-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-email-field.tsx
similarity index 60%
rename from apps/web/src/app/(signing)/sign/[token]/email-field.tsx
rename to apps/remix/app/components/general/document-signing/document-signing-email-field.tsx
index 9300aef63..a7ebc1dbe 100644
--- a/apps/web/src/app/(signing)/sign/[token]/email-field.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-email-field.tsx
@@ -1,44 +1,44 @@
-'use client';
-
-import { useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
+import { useRevalidator } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
-import type { Recipient } from '@documenso/prisma/client';
+import { ZEmailFieldMeta } from '@documenso/lib/types/field-meta';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
import type {
TRemovedSignedFieldWithTokenMutationSchema,
TSignFieldWithTokenMutationSchema,
} from '@documenso/trpc/server/field-router/schema';
+import { cn } from '@documenso/ui/lib/utils';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { useRequiredSigningContext } from './provider';
-import { SigningFieldContainer } from './signing-field-container';
+import { DocumentSigningFieldContainer } from './document-signing-field-container';
+import { useRequiredDocumentSigningContext } from './document-signing-provider';
+import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
-export type EmailFieldProps = {
+export type DocumentSigningEmailFieldProps = {
field: FieldWithSignature;
- recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
};
-export const EmailField = ({ field, recipient, onSignField, onUnsignField }: EmailFieldProps) => {
- const router = useRouter();
-
+export const DocumentSigningEmailField = ({
+ field,
+ onSignField,
+ onUnsignField,
+}: DocumentSigningEmailFieldProps) => {
const { _ } = useLingui();
const { toast } = useToast();
+ const { revalidate } = useRevalidator();
- const { email: providedEmail } = useRequiredSigningContext();
+ const { email: providedEmail } = useRequiredDocumentSigningContext();
- const [isPending, startTransition] = useTransition();
+ const { recipient, targetSigner, isAssistantMode } = useDocumentSigningRecipientContext();
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
@@ -48,7 +48,10 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
+ const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
+
+ const safeFieldMeta = ZEmailFieldMeta.safeParse(field.fieldMeta);
+ const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
const onSign = async (authOptions?: TRecipientActionAuth) => {
try {
@@ -69,7 +72,7 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
await signFieldWithToken(payload);
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
const error = AppError.parseError(err);
@@ -81,7 +84,9 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
toast({
title: _(msg`Error`),
- description: _(msg`An error occurred while signing the document.`),
+ description: isAssistantMode
+ ? _(msg`An error occurred while signing as assistant.`)
+ : _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@@ -101,20 +106,20 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
await removeSignedFieldWithToken(payload);
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
console.error(err);
toast({
title: _(msg`Error`),
- description: _(msg`An error occurred while removing the signature.`),
+ description: _(msg`An error occurred while removing the field.`),
variant: 'destructive',
});
}
};
return (
-
+
{isLoading && (
@@ -128,10 +133,22 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
)}
{field.inserted && (
-
- {field.customText}
-
+
+
+ {field.customText}
+
+
)}
-
+
);
};
diff --git a/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx b/apps/remix/app/components/general/document-signing/document-signing-field-container.tsx
similarity index 85%
rename from apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx
rename to apps/remix/app/components/general/document-signing/document-signing-field-container.tsx
index cf8403696..14fe95c44 100644
--- a/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-field-container.tsx
@@ -1,21 +1,19 @@
-'use client';
-
import React from 'react';
-import { Trans } from '@lingui/macro';
+import { Trans } from '@lingui/react/macro';
+import { FieldType } from '@prisma/client';
import { X } from 'lucide-react';
import { type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
-import { FieldType } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { FieldRootContainer } from '@documenso/ui/components/field/field';
import { cn } from '@documenso/ui/lib/utils';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
+import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
-export type SignatureFieldProps = {
+export type DocumentSigningFieldContainerProps = {
field: FieldWithSignature;
loading?: boolean;
children: React.ReactNode;
@@ -46,6 +44,7 @@ export type SignatureFieldProps = {
| 'Email'
| 'Name'
| 'Signature'
+ | 'Text'
| 'Radio'
| 'Dropdown'
| 'Number'
@@ -53,7 +52,7 @@ export type SignatureFieldProps = {
tooltipText?: string | null;
};
-export const SigningFieldContainer = ({
+export const DocumentSigningFieldContainer = ({
field,
loading,
onPreSign,
@@ -62,8 +61,9 @@ export const SigningFieldContainer = ({
children,
type,
tooltipText,
-}: SignatureFieldProps) => {
- const { executeActionAuthProcedure, isAuthRedirectRequired } = useRequiredDocumentAuthContext();
+}: DocumentSigningFieldContainerProps) => {
+ const { executeActionAuthProcedure, isAuthRedirectRequired } =
+ useRequiredDocumentSigningAuthContext();
const parsedFieldMeta = field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined;
const readOnlyField = parsedFieldMeta?.readOnly || false;
@@ -181,6 +181,23 @@ export const SigningFieldContainer = ({
)}
+ {(field.type === FieldType.RADIO || field.type === FieldType.CHECKBOX) &&
+ field.fieldMeta?.label && (
+
+ {field.fieldMeta.label}
+
+ )}
+
{children}
diff --git a/apps/remix/app/components/general/document-signing/document-signing-form.tsx b/apps/remix/app/components/general/document-signing/document-signing-form.tsx
new file mode 100644
index 000000000..a293afac8
--- /dev/null
+++ b/apps/remix/app/components/general/document-signing/document-signing-form.tsx
@@ -0,0 +1,410 @@
+import { useId, useMemo, useState } from 'react';
+
+import { msg } from '@lingui/core/macro';
+import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
+import { type Field, FieldType, type Recipient, RecipientRole } from '@prisma/client';
+import { Controller, useForm } from 'react-hook-form';
+import { useNavigate } from 'react-router';
+
+import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
+import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
+import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
+import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
+import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
+import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
+import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
+import { trpc } from '@documenso/trpc/react';
+import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
+import { cn } from '@documenso/ui/lib/utils';
+import { Button } from '@documenso/ui/primitives/button';
+import { Card, CardContent } from '@documenso/ui/primitives/card';
+import { Input } from '@documenso/ui/primitives/input';
+import { Label } from '@documenso/ui/primitives/label';
+import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
+import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+
+import { AssistantConfirmationDialog } from '../../dialogs/assistant-confirmation-dialog';
+import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog';
+import { useRequiredDocumentSigningContext } from './document-signing-provider';
+
+export type DocumentSigningFormProps = {
+ document: DocumentAndSender;
+ recipient: Recipient;
+ fields: Field[];
+ redirectUrl?: string | null;
+ isRecipientsTurn: boolean;
+ allRecipients?: RecipientWithFields[];
+ setSelectedSignerId?: (id: number | null) => void;
+};
+
+export const DocumentSigningForm = ({
+ document,
+ recipient,
+ fields,
+ redirectUrl,
+ isRecipientsTurn,
+ allRecipients = [],
+ setSelectedSignerId,
+}: DocumentSigningFormProps) => {
+ const { sessionData } = useOptionalSession();
+ const user = sessionData?.user;
+
+ const { _ } = useLingui();
+ const { toast } = useToast();
+
+ const navigate = useNavigate();
+ const analytics = useAnalytics();
+
+ const assistantSignersId = useId();
+
+ const { fullName, signature, setFullName, setSignature, signatureValid, setSignatureValid } =
+ useRequiredDocumentSigningContext();
+
+ const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
+ const [isConfirmationDialogOpen, setIsConfirmationDialogOpen] = useState(false);
+ const [isAssistantSubmitting, setIsAssistantSubmitting] = useState(false);
+
+ const { mutateAsync: completeDocumentWithToken } =
+ trpc.recipient.completeDocumentWithToken.useMutation();
+
+ const assistantForm = useForm<{ selectedSignerId: number | undefined }>({
+ defaultValues: {
+ selectedSignerId: undefined,
+ },
+ });
+
+ const { handleSubmit, formState } = useForm();
+
+ // Keep the loading state going if successful since the redirect may take some time.
+ const isSubmitting = formState.isSubmitting || formState.isSubmitSuccessful;
+
+ const fieldsRequiringValidation = useMemo(
+ () => fields.filter(isFieldUnsignedAndRequired),
+ [fields],
+ );
+
+ const hasSignatureField = fields.some((field) => field.type === FieldType.SIGNATURE);
+
+ const uninsertedFields = useMemo(() => {
+ return sortFieldsByPosition(fieldsRequiringValidation.filter((field) => !field.inserted));
+ }, [fieldsRequiringValidation]);
+
+ const uninsertedRecipientFields = useMemo(() => {
+ return fieldsRequiringValidation.filter((field) => field.recipientId === recipient.id);
+ }, [fieldsRequiringValidation, recipient]);
+
+ const fieldsValidated = () => {
+ setValidateUninsertedFields(true);
+ validateFieldsInserted(fieldsRequiringValidation);
+ };
+
+ const onFormSubmit = async () => {
+ setValidateUninsertedFields(true);
+
+ const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation);
+
+ if (hasSignatureField && !signatureValid) {
+ return;
+ }
+
+ if (!isFieldsValid) {
+ return;
+ }
+
+ await completeDocument();
+ };
+
+ const onAssistantFormSubmit = () => {
+ if (uninsertedRecipientFields.length > 0) {
+ return;
+ }
+
+ setIsConfirmationDialogOpen(true);
+ };
+
+ const handleAssistantConfirmDialogSubmit = async () => {
+ setIsAssistantSubmitting(true);
+
+ try {
+ await completeDocument();
+ } catch (err) {
+ toast({
+ title: 'Error',
+ description: 'An error occurred while completing the document. Please try again.',
+ variant: 'destructive',
+ });
+
+ setIsAssistantSubmitting(false);
+ setIsConfirmationDialogOpen(false);
+ }
+ };
+
+ const completeDocument = async (authOptions?: TRecipientActionAuth) => {
+ await completeDocumentWithToken({
+ token: recipient.token,
+ documentId: document.id,
+ authOptions,
+ });
+
+ analytics.capture('App: Recipient has completed signing', {
+ signerId: recipient.id,
+ documentId: document.id,
+ timestamp: new Date().toISOString(),
+ });
+
+ if (redirectUrl) {
+ window.location.href = redirectUrl;
+ } else {
+ await navigate(`/sign/${recipient.token}/complete`);
+ }
+ };
+
+ return (
+
+ {validateUninsertedFields && uninsertedFields[0] && (
+
+ Click to insert field
+
+ )}
+
+
+
+
+ {recipient.role === RecipientRole.VIEWER && View Document}
+ {recipient.role === RecipientRole.SIGNER && Sign Document}
+ {recipient.role === RecipientRole.APPROVER && Approve Document}
+ {recipient.role === RecipientRole.ASSISTANT && Assist Document}
+
+
+ {recipient.role === RecipientRole.VIEWER ? (
+ <>
+
+ Please mark as viewed to complete
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ ) : recipient.role === RecipientRole.ASSISTANT ? (
+ <>
+
+ >
+ ) : (
+ <>
+
+ >
+ )}
+
+
+
+ );
+};
diff --git a/apps/web/src/app/(signing)/sign/[token]/initials-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-initials-field.tsx
similarity index 75%
rename from apps/web/src/app/(signing)/sign/[token]/initials-field.tsx
rename to apps/remix/app/components/general/document-signing/document-signing-initials-field.tsx
index b63418076..532b0cc4b 100644
--- a/apps/web/src/app/(signing)/sign/[token]/initials-field.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-initials-field.tsx
@@ -1,18 +1,13 @@
-'use client';
-
-import { useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
+import { useRevalidator } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
-import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -21,31 +16,30 @@ import type {
} from '@documenso/trpc/server/field-router/schema';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { useRequiredSigningContext } from './provider';
-import { SigningFieldContainer } from './signing-field-container';
+import { DocumentSigningFieldContainer } from './document-signing-field-container';
+import { useRequiredDocumentSigningContext } from './document-signing-provider';
+import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
-export type InitialsFieldProps = {
+export type DocumentSigningInitialsFieldProps = {
field: FieldWithSignature;
- recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
};
-export const InitialsField = ({
+export const DocumentSigningInitialsField = ({
field,
- recipient,
onSignField,
onUnsignField,
-}: InitialsFieldProps) => {
- const router = useRouter();
+}: DocumentSigningInitialsFieldProps) => {
const { toast } = useToast();
const { _ } = useLingui();
+ const { revalidate } = useRevalidator();
+
+ const { fullName } = useRequiredDocumentSigningContext();
+ const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
- const { fullName } = useRequiredSigningContext();
const initials = extractInitials(fullName);
- const [isPending, startTransition] = useTransition();
-
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
@@ -54,7 +48,7 @@ export const InitialsField = ({
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
+ const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const onSign = async (authOptions?: TRecipientActionAuth) => {
try {
@@ -75,7 +69,7 @@ export const InitialsField = ({
await signFieldWithToken(payload);
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
const error = AppError.parseError(err);
@@ -87,7 +81,9 @@ export const InitialsField = ({
toast({
title: _(msg`Error`),
- description: _(msg`An error occurred while signing the document.`),
+ description: isAssistantMode
+ ? _(msg`An error occurred while signing as assistant.`)
+ : _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@@ -107,7 +103,7 @@ export const InitialsField = ({
await removeSignedFieldWithToken(payload);
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
console.error(err);
@@ -120,7 +116,12 @@ export const InitialsField = ({
};
return (
-
+
{isLoading && (
@@ -138,6 +139,6 @@ export const InitialsField = ({
{field.customText}
)}
-
+
);
};
diff --git a/apps/web/src/app/(signing)/sign/[token]/name-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-name-field.tsx
similarity index 71%
rename from apps/web/src/app/(signing)/sign/[token]/name-field.tsx
rename to apps/remix/app/components/general/document-signing/document-signing-name-field.tsx
index bc83e5a49..7c0246c97 100644
--- a/apps/web/src/app/(signing)/sign/[token]/name-field.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-name-field.tsx
@@ -1,52 +1,54 @@
-'use client';
+import { useState } from 'react';
-import { useState, useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
+import { useRevalidator } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
-import { type Recipient } from '@documenso/prisma/client';
+import { ZNameFieldMeta } from '@documenso/lib/types/field-meta';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
import type {
TRemovedSignedFieldWithTokenMutationSchema,
TSignFieldWithTokenMutationSchema,
} from '@documenso/trpc/server/field-router/schema';
+import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-import { useRequiredSigningContext } from './provider';
-import { SigningFieldContainer } from './signing-field-container';
+import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
+import { DocumentSigningFieldContainer } from './document-signing-field-container';
+import { useRequiredDocumentSigningContext } from './document-signing-provider';
+import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
-export type NameFieldProps = {
+export type DocumentSigningNameFieldProps = {
field: FieldWithSignature;
- recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise
| void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
};
-export const NameField = ({ field, recipient, onSignField, onUnsignField }: NameFieldProps) => {
- const router = useRouter();
-
+export const DocumentSigningNameField = ({
+ field,
+ onSignField,
+ onUnsignField,
+}: DocumentSigningNameFieldProps) => {
const { _ } = useLingui();
const { toast } = useToast();
+ const { revalidate } = useRevalidator();
const { fullName: providedFullName, setFullName: setProvidedFullName } =
- useRequiredSigningContext();
+ useRequiredDocumentSigningContext();
- const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
+ const { recipient, isAssistantMode } = useDocumentSigningRecipientContext();
- const [isPending, startTransition] = useTransition();
+ const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
@@ -56,13 +58,16 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
+ const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
+
+ const safeFieldMeta = ZNameFieldMeta.safeParse(field.fieldMeta);
+ const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
const [showFullNameModal, setShowFullNameModal] = useState(false);
const [localFullName, setLocalFullName] = useState('');
const onPreSign = () => {
- if (!providedFullName) {
+ if (!providedFullName && !isAssistantMode) {
setShowFullNameModal(true);
return false;
}
@@ -85,9 +90,9 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
const onSign = async (authOptions?: TRecipientActionAuth, name?: string) => {
try {
- const value = name || providedFullName;
+ const value = name || providedFullName || '';
- if (!value) {
+ if (!value && !isAssistantMode) {
setShowFullNameModal(true);
return;
}
@@ -107,7 +112,7 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
await signFieldWithToken(payload);
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
const error = AppError.parseError(err);
@@ -119,7 +124,9 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
toast({
title: _(msg`Error`),
- description: _(msg`An error occurred while signing the document.`),
+ description: isAssistantMode
+ ? _(msg`An error occurred while signing as assistant.`)
+ : _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
@@ -139,20 +146,20 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
await removeSignedFieldWithToken(payload);
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
console.error(err);
toast({
title: _(msg`Error`),
- description: _(msg`An error occurred while removing the signature.`),
+ description: _(msg`An error occurred while removing the field.`),
variant: 'destructive',
});
}
};
return (
-
- {field.customText}
-
+
+
+ {field.customText}
+
+
)}
-
+
);
};
diff --git a/apps/web/src/app/(signing)/sign/[token]/number-field.tsx b/apps/remix/app/components/general/document-signing/document-signing-number-field.tsx
similarity index 80%
rename from apps/web/src/app/(signing)/sign/[token]/number-field.tsx
rename to apps/remix/app/components/general/document-signing/document-signing-number-field.tsx
index ffd90df64..307225778 100644
--- a/apps/web/src/app/(signing)/sign/[token]/number-field.tsx
+++ b/apps/remix/app/components/general/document-signing/document-signing-number-field.tsx
@@ -1,19 +1,16 @@
-'use client';
+import { useEffect, useState } from 'react';
-import { useEffect, useState, useTransition } from 'react';
-
-import { useRouter } from 'next/navigation';
-
-import { Trans, msg } from '@lingui/macro';
+import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
+import { Trans } from '@lingui/react/macro';
import { Hash, Loader } from 'lucide-react';
+import { useRevalidator } from 'react-router';
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZNumberFieldMeta } from '@documenso/lib/types/field-meta';
-import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
import type {
@@ -26,8 +23,9 @@ import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/
import { Input } from '@documenso/ui/primitives/input';
import { useToast } from '@documenso/ui/primitives/use-toast';
-import { useRequiredDocumentAuthContext } from './document-auth-provider';
-import { SigningFieldContainer } from './signing-field-container';
+import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
+import { DocumentSigningFieldContainer } from './document-signing-field-container';
+import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
type ValidationErrors = {
isNumber: string[];
@@ -37,23 +35,28 @@ type ValidationErrors = {
numberFormat: string[];
};
-export type NumberFieldProps = {
+export type DocumentSigningNumberFieldProps = {
field: FieldWithSignature;
- recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise | void;
};
-export const NumberField = ({ field, recipient, onSignField, onUnsignField }: NumberFieldProps) => {
+export const DocumentSigningNumberField = ({
+ field,
+ onSignField,
+ onUnsignField,
+}: DocumentSigningNumberFieldProps) => {
const { _ } = useLingui();
const { toast } = useToast();
+ const { revalidate } = useRevalidator();
- const router = useRouter();
- const [isPending, startTransition] = useTransition();
- const [showRadioModal, setShowRadioModal] = useState(false);
+ const { recipient, targetSigner, isAssistantMode } = useDocumentSigningRecipientContext();
+
+ const [showNumberModal, setShowNumberModal] = useState(false);
+
+ const safeFieldMeta = ZNumberFieldMeta.safeParse(field.fieldMeta);
+ const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
- const parsedFieldMeta = field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null;
- const isReadOnly = parsedFieldMeta?.readOnly;
const defaultValue = parsedFieldMeta?.value;
const [localNumber, setLocalNumber] = useState(
parsedFieldMeta?.value ? String(parsedFieldMeta.value) : '0',
@@ -69,7 +72,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
const [errors, setErrors] = useState(initialErrors);
- const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
+ const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
@@ -79,7 +82,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
isPending: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
- const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
+ const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const handleNumberChange = (e: React.ChangeEvent) => {
const text = e.target.value;
@@ -104,7 +107,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
};
const onDialogSignClick = () => {
- setShowRadioModal(false);
+ setShowNumberModal(false);
void executeActionAuthProcedure({
onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
@@ -135,7 +138,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
setLocalNumber('');
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
const error = AppError.parseError(err);
@@ -147,14 +150,20 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
toast({
title: _(msg`Error`),
- description: _(msg`An error occurred while signing the document.`),
+ description: isAssistantMode
+ ? _(msg`An error occurred while signing as assistant.`)
+ : _(msg`An error occurred while signing the document.`),
variant: 'destructive',
});
}
};
const onPreSign = () => {
- setShowRadioModal(true);
+ if (isAssistantMode) {
+ return true;
+ }
+
+ setShowNumberModal(true);
if (localNumber && parsedFieldMeta) {
const validationErrors = validateNumberField(localNumber, parsedFieldMeta, true);
@@ -186,29 +195,29 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
setLocalNumber(parsedFieldMeta?.value ? String(parsedFieldMeta?.value) : '');
- startTransition(() => router.refresh());
+ await revalidate();
} catch (err) {
console.error(err);
toast({
title: _(msg`Error`),
- description: _(msg`An error occurred while removing the signature.`),
+ description: _(msg`An error occurred while removing the field.`),
variant: 'destructive',
});
}
};
useEffect(() => {
- if (!showRadioModal) {
+ if (!showNumberModal) {
setLocalNumber(parsedFieldMeta?.value ? String(parsedFieldMeta.value) : '0');
setErrors(initialErrors);
}
- }, [showRadioModal]);
+ }, [showNumberModal]);
useEffect(() => {
if (
(!field.inserted && defaultValue && localNumber) ||
- (!field.inserted && isReadOnly && defaultValue)
+ (!field.inserted && parsedFieldMeta?.readOnly && defaultValue)
) {
void executeActionAuthProcedure({
onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
@@ -221,20 +230,20 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
if (parsedFieldMeta?.label) {
fieldDisplayName =
- parsedFieldMeta.label.length > 10
- ? parsedFieldMeta.label.substring(0, 10) + '...'
+ parsedFieldMeta.label.length > 20
+ ? parsedFieldMeta.label.substring(0, 20) + '...'
: parsedFieldMeta.label;
}
const userInputHasErrors = Object.values(errors).some((error) => error.length > 0);
return (
-
{isLoading && (
@@ -260,12 +269,24 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
)}
{field.inserted && (
-
- {field.customText}
-
+
+
+ {field.customText}
+
+
)}
-
diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
new file mode 100644
index 000000000..7d207d34f
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id._index.tsx
@@ -0,0 +1,5 @@
+import TemplatePage, { loader } from '~/routes/_authenticated+/templates.$id._index';
+
+export { loader };
+
+export default TemplatePage;
diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id.edit.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id.edit.tsx
new file mode 100644
index 000000000..7f4b0b459
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates.$id.edit.tsx
@@ -0,0 +1,5 @@
+import TemplateEditPage, { loader } from '~/routes/_authenticated+/templates.$id.edit';
+
+export { loader };
+
+export default TemplateEditPage;
diff --git a/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
new file mode 100644
index 000000000..7781ededd
--- /dev/null
+++ b/apps/remix/app/routes/_authenticated+/t.$teamUrl+/templates._index.tsx
@@ -0,0 +1,5 @@
+import TemplatesPage, { meta } from '~/routes/_authenticated+/templates._index';
+
+export { meta };
+
+export default TemplatesPage;
diff --git a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view.tsx b/apps/remix/app/routes/_authenticated+/templates.$id._index.tsx
similarity index 63%
rename from apps/web/src/app/(dashboard)/templates/[id]/template-page-view.tsx
rename to apps/remix/app/routes/_authenticated+/templates.$id._index.tsx
index 895eed438..b8fc390b6 100644
--- a/apps/web/src/app/(dashboard)/templates/[id]/template-page-view.tsx
+++ b/apps/remix/app/routes/_authenticated+/templates.$id._index.tsx
@@ -1,37 +1,40 @@
-import Link from 'next/link';
-import { redirect } from 'next/navigation';
-
-import { Trans } from '@lingui/macro';
+import { Trans } from '@lingui/react/macro';
+import { DocumentSigningOrder, SigningStatus } from '@prisma/client';
import { ChevronLeft, LucideEdit } from 'lucide-react';
+import { Link, redirect, useNavigate } from 'react-router';
-import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
+import { getSession } from '@documenso/auth/server/lib/utils/get-session';
+import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
-import { DocumentSigningOrder, SigningStatus, type Team } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
-import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
+import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
-import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields';
-import { TemplateType } from '~/components/formatter/template-type';
+import { TemplateBulkSendDialog } from '~/components/dialogs/template-bulk-send-dialog';
+import { TemplateDirectLinkDialogWrapper } from '~/components/dialogs/template-direct-link-dialog-wrapper';
+import { TemplateUseDialog } from '~/components/dialogs/template-use-dialog';
+import { DocumentReadOnlyFields } from '~/components/general/document/document-read-only-fields';
+import { TemplateDirectLinkBadge } from '~/components/general/template/template-direct-link-badge';
+import { TemplatePageViewDocumentsTable } from '~/components/general/template/template-page-view-documents-table';
+import { TemplatePageViewInformation } from '~/components/general/template/template-page-view-information';
+import { TemplatePageViewRecentActivity } from '~/components/general/template/template-page-view-recent-activity';
+import { TemplatePageViewRecipients } from '~/components/general/template/template-page-view-recipients';
+import { TemplateType } from '~/components/general/template/template-type';
+import { TemplatesTableActionDropdown } from '~/components/tables/templates-table-action-dropdown';
+import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
-import { DataTableActionDropdown } from '../data-table-action-dropdown';
-import { TemplateDirectLinkBadge } from '../template-direct-link-badge';
-import { UseTemplateDialog } from '../use-template-dialog';
-import { TemplateDirectLinkDialogWrapper } from './template-direct-link-dialog-wrapper';
-import { TemplatePageViewDocumentsTable } from './template-page-view-documents-table';
-import { TemplatePageViewInformation } from './template-page-view-information';
-import { TemplatePageViewRecentActivity } from './template-page-view-recent-activity';
-import { TemplatePageViewRecipients } from './template-page-view-recipients';
+import type { Route } from './+types/templates.$id._index';
-export type TemplatePageViewProps = {
- params: {
- id: string;
- };
- team?: Team;
-};
+export async function loader({ params, request }: Route.LoaderArgs) {
+ const { user } = await getSession(request);
+
+ let team: TGetTeamByUrlResponse | null = null;
+
+ if (params.teamUrl) {
+ team = await getTeamByUrl({ userId: user.id, teamUrl: params.teamUrl });
+ }
-export const TemplatePageView = async ({ params, team }: TemplatePageViewProps) => {
const { id } = params;
const templateId = Number(id);
@@ -39,11 +42,9 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
const documentRootPath = formatDocumentsPath(team?.url);
if (!templateId || Number.isNaN(templateId)) {
- redirect(templateRootPath);
+ throw redirect(templateRootPath);
}
- const { user } = await getRequiredServerComponentSession();
-
const template = await getTemplateById({
id: templateId,
userId: user.id,
@@ -51,11 +52,26 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
}).catch(() => null);
if (!template || !template.templateDocumentData || (template?.teamId && !team?.url)) {
- redirect(templateRootPath);
+ throw redirect(templateRootPath);
}
+ return superLoaderJson({
+ user,
+ team,
+ template,
+ templateRootPath,
+ documentRootPath,
+ });
+}
+
+export default function TemplatePage() {
+ const { user, team, template, templateRootPath, documentRootPath } =
+ useSuperLoaderData();
+
const { templateDocumentData, fields, recipients, templateMeta } = template;
+ const navigate = useNavigate();
+
// Remap to fit the DocumentReadOnlyFields component.
const readOnlyFields = fields.map((field) => {
const recipient = recipients.find((recipient) => recipient.id === field.recipientId) || {
@@ -81,7 +97,7 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
return (
-
+
Templates
@@ -111,8 +127,10 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
+
+
-
+
Edit Template
@@ -126,11 +144,7 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
gradient
>
-
+
@@ -149,10 +163,14 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
- navigate(templateRootPath)}
+ onMove={async ({ teamUrl, templateId }) =>
+ navigate(`${formatTemplatesPath(teamUrl)}/${templateId}`)
+ }
/>
@@ -162,7 +180,7 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
-
@@ -197,8 +214,8 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
Documents created from template
-
+