Compare commits

..

1 Commits

Author SHA1 Message Date
606e73c254 chore: add translations 2025-10-31 20:04:06 +00:00
32 changed files with 4772 additions and 3612 deletions

View File

@ -152,18 +152,6 @@ export const EditorFieldTextForm = ({
className="h-auto" className="h-auto"
placeholder={t`Add text to the field`} placeholder={t`Add text to the field`}
{...field} {...field}
onChange={(e) => {
const values = form.getValues();
const characterLimit = values.characterLimit || 0;
let textValue = e.target.value;
if (characterLimit > 0 && textValue.length > characterLimit) {
textValue = textValue.slice(0, characterLimit);
}
e.target.value = textValue;
field.onChange(e);
}}
rows={1} rows={1}
/> />
</FormControl> </FormControl>
@ -187,18 +175,6 @@ export const EditorFieldTextForm = ({
className="bg-background" className="bg-background"
placeholder={t`Field character limit`} placeholder={t`Field character limit`}
{...field} {...field}
onChange={(e) => {
field.onChange(e);
const values = form.getValues();
const characterLimit = parseInt(e.target.value, 10) || 0;
const textValue = values.text || '';
if (characterLimit > 0 && textValue.length > characterLimit) {
form.setValue('text', textValue.slice(0, characterLimit));
}
}}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@ -89,10 +89,7 @@ export const DirectTemplatePageView = ({
setStep('sign'); setStep('sign');
}; };
const onSignDirectTemplateSubmit = async ( const onSignDirectTemplateSubmit = async (fields: DirectTemplateLocalField[]) => {
fields: DirectTemplateLocalField[],
nextSigner?: { name: string; email: string },
) => {
try { try {
let directTemplateExternalId = searchParams?.get('externalId') || undefined; let directTemplateExternalId = searchParams?.get('externalId') || undefined;
@ -101,7 +98,6 @@ export const DirectTemplatePageView = ({
} }
const { token } = await createDocumentFromDirectTemplate({ const { token } = await createDocumentFromDirectTemplate({
nextSigner,
directTemplateToken, directTemplateToken,
directTemplateExternalId, directTemplateExternalId,
directRecipientName: fullName, directRecipientName: fullName,

View File

@ -55,13 +55,10 @@ import { DocumentSigningRecipientProvider } from '../document-signing/document-s
export type DirectTemplateSigningFormProps = { export type DirectTemplateSigningFormProps = {
flowStep: DocumentFlowStep; flowStep: DocumentFlowStep;
directRecipient: Pick<Recipient, 'authOptions' | 'email' | 'role' | 'name' | 'token' | 'id'>; directRecipient: Pick<Recipient, 'authOptions' | 'email' | 'role' | 'name' | 'token'>;
directRecipientFields: Field[]; directRecipientFields: Field[];
template: Omit<TTemplate, 'user'>; template: Omit<TTemplate, 'user'>;
onSubmit: ( onSubmit: (_data: DirectTemplateLocalField[]) => Promise<void>;
_data: DirectTemplateLocalField[],
_nextSigner?: { name: string; email: string },
) => Promise<void>;
}; };
export type DirectTemplateLocalField = Field & { export type DirectTemplateLocalField = Field & {
@ -152,7 +149,7 @@ export const DirectTemplateSigningForm = ({
validateFieldsInserted(fieldsRequiringValidation); validateFieldsInserted(fieldsRequiringValidation);
}; };
const handleSubmit = async (nextSigner?: { name: string; email: string }) => { const handleSubmit = async () => {
setValidateUninsertedFields(true); setValidateUninsertedFields(true);
const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation); const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation);
@ -164,7 +161,7 @@ export const DirectTemplateSigningForm = ({
setIsSubmitting(true); setIsSubmitting(true);
try { try {
await onSubmit(localFields, nextSigner); await onSubmit(localFields);
} catch { } catch {
setIsSubmitting(false); setIsSubmitting(false);
} }
@ -221,30 +218,6 @@ export const DirectTemplateSigningForm = ({
setLocalFields(updatedFields); setLocalFields(updatedFields);
}, []); }, []);
const nextRecipient = useMemo(() => {
if (
!template.templateMeta?.signingOrder ||
template.templateMeta.signingOrder !== 'SEQUENTIAL' ||
!template.templateMeta.allowDictateNextSigner
) {
return undefined;
}
const sortedRecipients = template.recipients.sort((a, b) => {
// Sort by signingOrder first (nulls last), then by id
if (a.signingOrder === null && b.signingOrder === null) return a.id - b.id;
if (a.signingOrder === null) return 1;
if (b.signingOrder === null) return -1;
if (a.signingOrder === b.signingOrder) return a.id - b.id;
return a.signingOrder - b.signingOrder;
});
const currentIndex = sortedRecipients.findIndex((r) => r.id === directRecipient.id);
return currentIndex !== -1 && currentIndex < sortedRecipients.length - 1
? sortedRecipients[currentIndex + 1]
: undefined;
}, [template.templateMeta?.signingOrder, template.recipients, directRecipient.id]);
return ( return (
<DocumentSigningRecipientProvider recipient={directRecipient}> <DocumentSigningRecipientProvider recipient={directRecipient}>
<DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} /> <DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} />
@ -444,15 +417,11 @@ export const DirectTemplateSigningForm = ({
<DocumentSigningCompleteDialog <DocumentSigningCompleteDialog
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
onSignatureComplete={async (nextSigner) => handleSubmit(nextSigner)} onSignatureComplete={async () => handleSubmit()}
documentTitle={template.title} documentTitle={template.title}
fields={localFields} fields={localFields}
fieldsValidated={fieldsValidated} fieldsValidated={fieldsValidated}
recipient={directRecipient} recipient={directRecipient}
allowDictateNextSigner={nextRecipient && template.templateMeta?.allowDictateNextSigner}
defaultNextSigner={
nextRecipient ? { name: nextRecipient.name, email: nextRecipient.email } : undefined
}
/> />
</div> </div>
</DocumentFlowFormContainerFooter> </DocumentFlowFormContainerFooter>

View File

@ -9,7 +9,7 @@ import { Button } from '@documenso/ui/primitives/button';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type DocumentSigningAuthPageViewProps = { export type DocumentSigningAuthPageViewProps = {
email?: string; email: string;
emailHasAccount?: boolean; emailHasAccount?: boolean;
}; };
@ -22,18 +22,12 @@ export const DocumentSigningAuthPageView = ({
const [isSigningOut, setIsSigningOut] = useState(false); const [isSigningOut, setIsSigningOut] = useState(false);
const handleChangeAccount = async (email?: string) => { const handleChangeAccount = async (email: string) => {
try { try {
setIsSigningOut(true); setIsSigningOut(true);
let redirectPath = '/signin';
if (email) {
redirectPath = emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`;
}
await authClient.signOut({ await authClient.signOut({
redirectPath, redirectPath: emailHasAccount ? `/signin#email=${email}` : `/signup#email=${email}`,
}); });
} catch { } catch {
toast({ toast({
@ -55,13 +49,9 @@ export const DocumentSigningAuthPageView = ({
</h1> </h1>
<p className="text-muted-foreground mt-2 text-sm"> <p className="text-muted-foreground mt-2 text-sm">
{email ? ( <Trans>
<Trans> You need to be logged in as <strong>{email}</strong> to view this page.
You need to be logged in as <strong>{email}</strong> to view this page. </Trans>
</Trans>
) : (
<Trans>You need to be logged in to view this page.</Trans>
)}
</p> </p>
<Button <Button

View File

@ -24,10 +24,7 @@ type PasskeyData = {
isError: boolean; isError: boolean;
}; };
type SigningAuthRecipient = Pick< type SigningAuthRecipient = Pick<Recipient, 'authOptions' | 'email' | 'role' | 'name' | 'token'>;
Recipient,
'authOptions' | 'email' | 'role' | 'name' | 'token' | 'id'
>;
export type DocumentSigningAuthContextValue = { export type DocumentSigningAuthContextValue = {
executeActionAuthProcedure: (_value: ExecuteActionAuthProcedureOptions) => Promise<void>; executeActionAuthProcedure: (_value: ExecuteActionAuthProcedureOptions) => Promise<void>;

View File

@ -304,6 +304,7 @@ export const DocumentSigningCompleteDialog = ({
<form onSubmit={form.handleSubmit(onFormSubmit)}> <form onSubmit={form.handleSubmit(onFormSubmit)}>
{allowDictateNextSigner && defaultNextSigner && ( {allowDictateNextSigner && defaultNextSigner && (
<div className="mb-4 flex flex-col gap-4"> <div className="mb-4 flex flex-col gap-4">
{/* Todo: Envelopes - Should we say "The next recipient to sign this document will be"? */}
<div className="flex flex-col gap-4 md:flex-row"> <div className="flex flex-col gap-4 md:flex-row">
<FormField <FormField
control={form.control} control={form.control}

View File

@ -13,7 +13,6 @@ import { prop, sortBy } from 'remeda';
import { isBase64Image } from '@documenso/lib/constants/signatures'; import { isBase64Image } from '@documenso/lib/constants/signatures';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import type { EnvelopeForSigningResponse } from '@documenso/lib/server-only/envelope/get-envelope-for-recipient-signing'; import type { EnvelopeForSigningResponse } from '@documenso/lib/server-only/envelope/get-envelope-for-recipient-signing';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { import {
isFieldUnsignedAndRequired, isFieldUnsignedAndRequired,
isRequiredField, isRequiredField,
@ -52,11 +51,7 @@ export type EnvelopeSigningContextValue = {
setSelectedAssistantRecipientId: (_value: number | null) => void; setSelectedAssistantRecipientId: (_value: number | null) => void;
selectedAssistantRecipient: EnvelopeForSigningResponse['envelope']['recipients'][number] | null; selectedAssistantRecipient: EnvelopeForSigningResponse['envelope']['recipients'][number] | null;
signField: ( signField: (_fieldId: number, _value: TSignEnvelopeFieldValue) => Promise<void>;
_fieldId: number,
_value: TSignEnvelopeFieldValue,
authOptions?: TRecipientActionAuth,
) => Promise<void>;
}; };
const EnvelopeSigningContext = createContext<EnvelopeSigningContextValue | null>(null); const EnvelopeSigningContext = createContext<EnvelopeSigningContextValue | null>(null);
@ -289,11 +284,9 @@ export const EnvelopeSigningProvider = ({
: null; : null;
}, [envelope.documentMeta?.signingOrder, envelope.recipients, recipient.id]); }, [envelope.documentMeta?.signingOrder, envelope.recipients, recipient.id]);
const signField = async ( const signField = async (fieldId: number, fieldValue: TSignEnvelopeFieldValue) => {
fieldId: number, console.log('insertField', fieldId, fieldValue);
fieldValue: TSignEnvelopeFieldValue,
authOptions?: TRecipientActionAuth,
) => {
// Set the field locally for direct templates. // Set the field locally for direct templates.
if (isDirectTemplate) { if (isDirectTemplate) {
handleDirectTemplateFieldInsertion(fieldId, fieldValue); handleDirectTemplateFieldInsertion(fieldId, fieldValue);
@ -304,7 +297,7 @@ export const EnvelopeSigningProvider = ({
token: envelopeData.recipient.token, token: envelopeData.recipient.token,
fieldId, fieldId,
fieldValue, fieldValue,
authOptions, authOptions: undefined,
}); });
}; };

View File

@ -103,6 +103,7 @@ export default function EnvelopeEditorFieldsPageRenderer() {
fieldUpdates.height = fieldPageHeight; fieldUpdates.height = fieldPageHeight;
} }
// Todo: envelopes Use id
editorFields.updateFieldByFormId(fieldFormId, fieldUpdates); editorFields.updateFieldByFormId(fieldFormId, fieldUpdates);
// Select the field if it is not already selected. // Select the field if it is not already selected.

View File

@ -27,8 +27,7 @@ import type {
import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients'; import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out'; import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy'; import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy';
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import { RecipientSelector } from '@documenso/ui/primitives/recipient-selector'; import { RecipientSelector } from '@documenso/ui/primitives/recipient-selector';
import { Separator } from '@documenso/ui/primitives/separator'; import { Separator } from '@documenso/ui/primitives/separator';
@ -113,29 +112,7 @@ export const EnvelopeEditorFieldsPage = () => {
<EnvelopeRendererFileSelector fields={editorFields.localFields} /> <EnvelopeRendererFileSelector fields={editorFields.localFields} />
{/* Document View */} {/* Document View */}
<div className="mt-4 flex flex-col items-center justify-center"> <div className="mt-4 flex h-full justify-center p-4">
{envelope.recipients.length === 0 && (
<Alert
variant="neutral"
className="border-border bg-background mb-4 flex max-w-[800px] flex-row items-center justify-between space-y-0 rounded-sm border"
>
<div className="flex flex-col gap-1">
<AlertTitle>
<Trans>Missing Recipients</Trans>
</AlertTitle>
<AlertDescription>
<Trans>You need at least one recipient to add fields</Trans>
</AlertDescription>
</div>
<Button asChild variant="outline">
<Link to={`${relativePath.editorPath}`}>
<Trans>Add Recipients</Trans>
</Link>
</Button>
</Alert>
)}
{currentEnvelopeItem !== null ? ( {currentEnvelopeItem !== null ? (
<PDFViewerKonvaLazy customPageRenderer={EnvelopeEditorFieldsPageRenderer} /> <PDFViewerKonvaLazy customPageRenderer={EnvelopeEditorFieldsPageRenderer} />
) : ( ) : (
@ -153,7 +130,7 @@ export const EnvelopeEditorFieldsPage = () => {
</div> </div>
{/* Right Section - Form Fields Panel */} {/* Right Section - Form Fields Panel */}
{currentEnvelopeItem && envelope.recipients.length > 0 && ( {currentEnvelopeItem && (
<div className="bg-background border-border sticky top-0 h-full w-80 flex-shrink-0 overflow-y-auto border-l py-4"> <div className="bg-background border-border sticky top-0 h-full w-80 flex-shrink-0 overflow-y-auto border-l py-4">
{/* Recipient selector section. */} {/* Recipient selector section. */}
<section className="px-4"> <section className="px-4">
@ -161,15 +138,29 @@ export const EnvelopeEditorFieldsPage = () => {
<Trans>Selected Recipient</Trans> <Trans>Selected Recipient</Trans>
</h3> </h3>
<RecipientSelector {envelope.recipients.length === 0 ? (
selectedRecipient={editorFields.selectedRecipient} <Alert variant="warning">
onSelectedRecipientChange={(recipient) => <AlertDescription className="flex flex-col gap-2">
editorFields.setSelectedRecipient(recipient.id) <Trans>You need at least one recipient to add fields</Trans>
}
recipients={envelope.recipients} <Link to={`${relativePath.editorPath}`} className="text-sm">
className="w-full" <p>
align="end" <Trans>Click here to add a recipient</Trans>
/> </p>
</Link>
</AlertDescription>
</Alert>
) : (
<RecipientSelector
selectedRecipient={editorFields.selectedRecipient}
onSelectedRecipientChange={(recipient) =>
editorFields.setSelectedRecipient(recipient.id)
}
recipients={envelope.recipients}
className="w-full"
align="end"
/>
)}
{editorFields.selectedRecipient && {editorFields.selectedRecipient &&
!canRecipientFieldsBeModified(editorFields.selectedRecipient, envelope.fields) && ( !canRecipientFieldsBeModified(editorFields.selectedRecipient, envelope.fields) && (

View File

@ -323,7 +323,7 @@ export const EnvelopeEditorSettingsDialog = ({
<DialogContent className="flex w-full !max-w-5xl flex-row gap-0 p-0"> <DialogContent className="flex w-full !max-w-5xl flex-row gap-0 p-0">
{/* Sidebar. */} {/* Sidebar. */}
<div className="bg-accent/20 flex w-80 flex-col border-r"> <div className="flex w-80 flex-col border-r bg-gray-50">
<DialogHeader className="p-6 pb-4"> <DialogHeader className="p-6 pb-4">
<DialogTitle>Document Settings</DialogTitle> <DialogTitle>Document Settings</DialogTitle>
</DialogHeader> </DialogHeader>

View File

@ -203,6 +203,7 @@ export const EnvelopeEditorUploadPage = () => {
debouncedUpdateEnvelopeItems(items); debouncedUpdateEnvelopeItems(items);
}; };
// Todo: Envelopes - Sync into envelopes data
const debouncedUpdateEnvelopeItems = useDebounceFunction((files: LocalFile[]) => { const debouncedUpdateEnvelopeItems = useDebounceFunction((files: LocalFile[]) => {
void updateEnvelopeItems({ void updateEnvelopeItems({
envelopeId: envelope.id, envelopeId: envelope.id,

View File

@ -10,17 +10,14 @@ import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-rende
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider'; import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session'; import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { DIRECT_TEMPLATE_RECIPIENT_EMAIL } from '@documenso/lib/constants/direct-templates'; import { DIRECT_TEMPLATE_RECIPIENT_EMAIL } from '@documenso/lib/constants/direct-templates';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZFullFieldSchema } from '@documenso/lib/types/field'; import { ZFullFieldSchema } from '@documenso/lib/types/field';
import { createSpinner } from '@documenso/lib/universal/field-renderer/field-generic-items'; import { createSpinner } from '@documenso/lib/universal/field-renderer/field-generic-items';
import { renderField } from '@documenso/lib/universal/field-renderer/render-field'; import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers'; import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields'; import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import type { TSignEnvelopeFieldValue } from '@documenso/trpc/server/envelope-router/sign-envelope-field.types';
import { EnvelopeFieldToolTip } from '@documenso/ui/components/field/envelope-field-tooltip'; import { EnvelopeFieldToolTip } from '@documenso/ui/components/field/envelope-field-tooltip';
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors'; import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { handleCheckboxFieldClick } from '~/utils/field-signing/checkbox-field'; import { handleCheckboxFieldClick } from '~/utils/field-signing/checkbox-field';
import { handleDropdownFieldClick } from '~/utils/field-signing/dropdown-field'; import { handleDropdownFieldClick } from '~/utils/field-signing/dropdown-field';
@ -31,24 +28,20 @@ import { handleNumberFieldClick } from '~/utils/field-signing/number-field';
import { handleSignatureFieldClick } from '~/utils/field-signing/signature-field'; import { handleSignatureFieldClick } from '~/utils/field-signing/signature-field';
import { handleTextFieldClick } from '~/utils/field-signing/text-field'; import { handleTextFieldClick } from '~/utils/field-signing/text-field';
import { useRequiredDocumentSigningAuthContext } from '../document-signing/document-signing-auth-provider';
import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider'; import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider';
export default function EnvelopeSignerPageRenderer() { export default function EnvelopeSignerPageRenderer() {
const { t, i18n } = useLingui(); const { i18n } = useLingui();
const { currentEnvelopeItem } = useCurrentEnvelopeRender(); const { currentEnvelopeItem } = useCurrentEnvelopeRender();
const { sessionData } = useOptionalSession(); const { sessionData } = useOptionalSession();
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
const { toast } = useToast();
const { const {
envelopeData, envelopeData,
recipient, recipient,
recipientFields, recipientFields,
recipientFieldsRemaining, recipientFieldsRemaining,
showPendingFieldTooltip, showPendingFieldTooltip,
signField: signFieldInternal, signField,
email, email,
setEmail, setEmail,
fullName, fullName,
@ -325,6 +318,7 @@ export default function EnvelopeSignerPageRenderer() {
* SIGNATURE FIELD. * SIGNATURE FIELD.
*/ */
.with({ type: FieldType.SIGNATURE }, (field) => { .with({ type: FieldType.SIGNATURE }, (field) => {
// Todo: Envelopes - Reauth
handleSignatureFieldClick({ handleSignatureFieldClick({
field, field,
signature, signature,
@ -335,21 +329,11 @@ export default function EnvelopeSignerPageRenderer() {
.then(async (payload) => { .then(async (payload) => {
if (payload) { if (payload) {
fieldGroup.add(loadingSpinnerGroup); fieldGroup.add(loadingSpinnerGroup);
await signField(field.id, payload);
}
if (payload.value) { if (payload?.value) {
void executeActionAuthProcedure({ setSignature(payload.value);
onReauthFormSubmit: async (authOptions) => {
await signField(field.id, payload, authOptions);
loadingSpinnerGroup.destroy();
},
actionTarget: field.type,
});
setSignature(payload.value);
} else {
await signField(field.id, payload);
}
} }
}) })
.finally(() => { .finally(() => {
@ -363,26 +347,6 @@ export default function EnvelopeSignerPageRenderer() {
fieldGroup.on('pointerdown', handleFieldGroupClick); fieldGroup.on('pointerdown', handleFieldGroupClick);
}; };
const signField = async (
fieldId: number,
payload: TSignEnvelopeFieldValue,
authOptions?: TRecipientActionAuth,
) => {
try {
await signFieldInternal(fieldId, payload, authOptions);
} catch (err) {
console.error(err);
toast({
title: t`Error`,
description: t`An error occurred while signing the field.`,
variant: 'destructive',
});
throw err;
}
};
/** /**
* Initialize the Konva page canvas and all fields and interactions. * Initialize the Konva page canvas and all fields and interactions.
*/ */

View File

@ -127,7 +127,6 @@ export const EnvelopeSignerCompleteDialog = () => {
isBase64, isBase64,
}; };
}), }),
nextSigner,
}); });
const redirectUrl = envelope.documentMeta.redirectUrl; const redirectUrl = envelope.documentMeta.redirectUrl;

View File

@ -8,6 +8,7 @@ import { EnvelopeRenderProvider } from '@documenso/lib/client-only/providers/env
import { useOptionalSession } from '@documenso/lib/client-only/providers/session'; import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeForDirectTemplateSigning } from '@documenso/lib/server-only/envelope/get-envelope-for-direct-template-signing'; import { getEnvelopeForDirectTemplateSigning } from '@documenso/lib/server-only/envelope/get-envelope-for-direct-template-signing';
import { getEnvelopeRequiredAccessData } from '@documenso/lib/server-only/envelope/get-envelope-required-access-data';
import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token'; import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token';
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth'; import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth'; import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
@ -97,12 +98,15 @@ const handleV2Loader = async ({ params, request }: Route.LoaderArgs) => {
envelopeForSigning, envelopeForSigning,
} as const; } as const;
}) })
.catch((e) => { .catch(async (e) => {
const error = AppError.parseError(e); const error = AppError.parseError(e);
if (error.code === AppErrorCode.UNAUTHORIZED) { if (error.code === AppErrorCode.UNAUTHORIZED) {
const requiredAccessData = await getEnvelopeRequiredAccessData({ token });
return { return {
isDocumentAccessValid: false, isDocumentAccessValid: false,
...requiredAccessData,
} as const; } as const;
} }
@ -222,21 +226,20 @@ const DirectSigningPageV2 = ({ data }: { data: Awaited<ReturnType<typeof handleV
const user = sessionData?.user; const user = sessionData?.user;
if (!data.isDocumentAccessValid) { if (!data.isDocumentAccessValid) {
return <DocumentSigningAuthPageView email={''} emailHasAccount={true} />; return (
<DocumentSigningAuthPageView
email={data.recipientEmail}
emailHasAccount={!!data.recipientHasAccount}
/>
);
} }
const { envelope, recipient } = data.envelopeForSigning; const { envelope, recipient } = data.envelopeForSigning;
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
documentAuth: envelope.authOptions,
});
const isEmailForced = derivedRecipientAccessAuth.includes(DocumentAccessAuth.ACCOUNT);
return ( return (
<EnvelopeSigningProvider <EnvelopeSigningProvider
envelopeData={data.envelopeForSigning} envelopeData={data.envelopeForSigning}
email={isEmailForced ? user?.email || '' : ''} // Doing this allows us to let users change the email if they want to for non-auth templates. email={''} // Doing this allows us to let users change the email if they want to.
fullName={user?.name} fullName={user?.name}
signature={user?.signature} signature={user?.signature}
> >

View File

@ -78,6 +78,7 @@ test.describe('Signing Certificate Tests', () => {
}, },
}); });
// Todo: Envelopes
const firstDocumentData = completedDocument.envelopeItems[0].documentData; const firstDocumentData = completedDocument.envelopeItems[0].documentData;
const completedDocumentData = await getFile(firstDocumentData); const completedDocumentData = await getFile(firstDocumentData);
@ -168,6 +169,7 @@ test.describe('Signing Certificate Tests', () => {
}, },
}); });
// Todo: Envelopes
const firstDocumentData = completedDocument.envelopeItems[0].documentData; const firstDocumentData = completedDocument.envelopeItems[0].documentData;
const completedDocumentData = await getFile(firstDocumentData); const completedDocumentData = await getFile(firstDocumentData);

View File

@ -1,12 +1,9 @@
import { expect, test } from '@playwright/test'; import { expect, test } from '@playwright/test';
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { customAlphabet } from 'nanoid'; import { customAlphabet } from 'nanoid';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { createDocumentAuthOptions } from '@documenso/lib/utils/document-auth'; import { createDocumentAuthOptions } from '@documenso/lib/utils/document-auth';
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
import { formatDirectTemplatePath } from '@documenso/lib/utils/templates'; import { formatDirectTemplatePath } from '@documenso/lib/utils/templates';
import { prisma } from '@documenso/prisma';
import { seedTeam } from '@documenso/prisma/seed/teams'; import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedDirectTemplate, seedTemplate } from '@documenso/prisma/seed/templates'; import { seedDirectTemplate, seedTemplate } from '@documenso/prisma/seed/templates';
import { seedTestEmail, seedUser } from '@documenso/prisma/seed/users'; import { seedTestEmail, seedUser } from '@documenso/prisma/seed/users';
@ -124,7 +121,7 @@ test('[DIRECT_TEMPLATES]: delete direct template link', async ({ page }) => {
await expect(page.getByText('404 not found')).toBeVisible(); await expect(page.getByText('404 not found')).toBeVisible();
}); });
test('[DIRECT_TEMPLATES]: V1 direct template link auth access', async ({ page }) => { test('[DIRECT_TEMPLATES]: direct template link auth access', async ({ page }) => {
const { user, team } = await seedUser(); const { user, team } = await seedUser();
const directTemplateWithAuth = await seedDirectTemplate({ const directTemplateWithAuth = await seedDirectTemplate({
@ -156,53 +153,6 @@ test('[DIRECT_TEMPLATES]: V1 direct template link auth access', async ({ page })
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible(); await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
await expect(page.getByLabel('Email')).toBeDisabled(); await expect(page.getByLabel('Email')).toBeDisabled();
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(/\/sign/);
await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();
});
test('[DIRECT_TEMPLATES]: V2 direct template link auth access', async ({ page }) => {
const { user, team } = await seedUser();
const directTemplateWithAuth = await seedDirectTemplate({
title: 'Personal direct template link',
userId: user.id,
teamId: team.id,
internalVersion: 2,
createTemplateOptions: {
authOptions: createDocumentAuthOptions({
globalAccessAuth: ['ACCOUNT'],
globalActionAuth: [],
}),
},
});
const directTemplatePath = formatDirectTemplatePath(
directTemplateWithAuth.directLink?.token || '',
);
await page.goto(directTemplatePath);
await expect(page.getByText('Authentication required')).toBeVisible();
await apiSignin({
page,
email: user.email,
});
await page.goto(directTemplatePath);
await expect(page.getByRole('heading', { name: 'Personal direct template link' })).toBeVisible();
await page.getByRole('button', { name: 'Complete' }).click();
await expect(page.getByLabel('Your Email')).not.toBeVisible();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(/\/sign/);
await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();
}); });
test('[DIRECT_TEMPLATES]: use direct template link with 1 recipient', async ({ page }) => { test('[DIRECT_TEMPLATES]: use direct template link with 1 recipient', async ({ page }) => {
@ -225,9 +175,6 @@ test('[DIRECT_TEMPLATES]: use direct template link with 1 recipient', async ({ p
await page.getByPlaceholder('recipient@documenso.com').fill(seedTestEmail()); await page.getByPlaceholder('recipient@documenso.com').fill(seedTestEmail());
await page.getByRole('button', { name: 'Continue' }).click(); await page.getByRole('button', { name: 'Continue' }).click();
await expect(page.getByText('Next Recipient Name')).not.toBeVisible();
await page.getByRole('button', { name: 'Complete' }).click(); await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click(); await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(/\/sign/); await page.waitForURL(/\/sign/);
@ -236,173 +183,3 @@ test('[DIRECT_TEMPLATES]: use direct template link with 1 recipient', async ({ p
// Add a longer waiting period to ensure document status is updated // Add a longer waiting period to ensure document status is updated
await page.waitForTimeout(3000); await page.waitForTimeout(3000);
}); });
test('[DIRECT_TEMPLATES]: V1 use direct template link with 2 recipients with next signer dictation', async ({
page,
}) => {
const { team, owner, organisation } = await seedTeam({
createTeamMembers: 1,
});
// Should be visible to team members.
const template = await seedDirectTemplate({
title: 'Team direct template link 1',
userId: owner.id,
teamId: team.id,
});
await prisma.documentMeta.update({
where: {
id: template.documentMetaId,
},
data: {
allowDictateNextSigner: true,
signingOrder: DocumentSigningOrder.SEQUENTIAL,
},
});
const originalName = 'Signer 2';
const originalSecondSignerEmail = seedTestEmail();
// Add another signer
await prisma.recipient.create({
data: {
signingOrder: 2,
envelopeId: template.id,
email: originalSecondSignerEmail,
name: originalName,
token: Math.random().toString().slice(2, 7),
role: RecipientRole.SIGNER,
},
});
// Check that the direct template link is accessible.
await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
await page.waitForTimeout(100);
await page.getByPlaceholder('recipient@documenso.com').fill(seedTestEmail());
await page.getByRole('button', { name: 'Continue' }).click();
await page.getByRole('button', { name: 'Complete' }).click();
await expect(page.getByText('Next Recipient Name')).toBeVisible();
const nextRecipientNameInputValue = await page.getByLabel('Next Recipient Name').inputValue();
expect(nextRecipientNameInputValue).toBe(originalName);
const nextRecipientEmailInputValue = await page.getByLabel('Next Recipient Email').inputValue();
expect(nextRecipientEmailInputValue).toBe(originalSecondSignerEmail);
const newName = 'Hello';
const newSecondSignerEmail = seedTestEmail();
await page.getByLabel('Next Recipient Email').fill(newSecondSignerEmail);
await page.getByLabel('Next Recipient Name').fill(newName);
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(/\/sign/);
await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();
const createdEnvelopeRecipients = await prisma.recipient.findMany({
where: {
envelope: {
templateId: mapSecondaryIdToTemplateId(template.secondaryId),
},
},
});
const updatedSecondRecipient = createdEnvelopeRecipients.find(
(recipient) => recipient.signingOrder === 2,
);
expect(updatedSecondRecipient?.name).toBe(newName);
expect(updatedSecondRecipient?.email).toBe(newSecondSignerEmail);
});
test('[DIRECT_TEMPLATES]: V2 use direct template link with 2 recipients with next signer dictation', async ({
page,
}) => {
const { team, owner, organisation } = await seedTeam({
createTeamMembers: 1,
});
// Should be visible to team members.
const template = await seedDirectTemplate({
title: 'Team direct template link 1',
userId: owner.id,
teamId: team.id,
internalVersion: 2,
});
await prisma.documentMeta.update({
where: {
id: template.documentMetaId,
},
data: {
allowDictateNextSigner: true,
signingOrder: DocumentSigningOrder.SEQUENTIAL,
},
});
const originalName = 'Signer 2';
const originalSecondSignerEmail = seedTestEmail();
// Add another signer
await prisma.recipient.create({
data: {
signingOrder: 2,
envelopeId: template.id,
email: originalSecondSignerEmail,
name: originalName,
token: Math.random().toString().slice(2, 7),
role: RecipientRole.SIGNER,
},
});
// Check that the direct template link is accessible.
await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
await expect(page.getByRole('heading', { name: 'Team direct template link 1' })).toBeVisible();
await page.waitForTimeout(100);
await page.getByRole('button', { name: 'Complete' }).click();
const currentName = 'John Doe';
const currentEmail = seedTestEmail();
await page.getByPlaceholder('Enter Your Name').fill(currentName);
await page.getByPlaceholder('Enter Your Email').fill(currentEmail);
await expect(page.getByText('Next Recipient Name')).toBeVisible();
const nextRecipientNameInputValue = await page.getByLabel('Next Recipient Name').inputValue();
expect(nextRecipientNameInputValue).toBe(originalName);
const nextRecipientEmailInputValue = await page.getByLabel('Next Recipient Email').inputValue();
expect(nextRecipientEmailInputValue).toBe(originalSecondSignerEmail);
const newName = 'Hello';
const newSecondSignerEmail = seedTestEmail();
await page.getByLabel('Next Recipient Email').fill(newSecondSignerEmail);
await page.getByLabel('Next Recipient Name').fill(newName);
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(/\/sign/);
await expect(page.getByRole('heading', { name: 'Document Signed' })).toBeVisible();
const createdEnvelopeRecipients = await prisma.recipient.findMany({
where: {
envelope: {
templateId: mapSecondaryIdToTemplateId(template.secondaryId),
},
},
});
const updatedSecondRecipient = createdEnvelopeRecipients.find(
(recipient) => recipient.signingOrder === 2,
);
expect(updatedSecondRecipient?.name).toBe(newName);
expect(updatedSecondRecipient?.email).toBe(newSecondSignerEmail);
});

View File

@ -1,11 +1,10 @@
import { DocumentStatus, EnvelopeType } from '@prisma/client'; import { DocumentStatus, EnvelopeType } from '@prisma/client';
import { match } from 'ts-pattern';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error'; import { AppError, AppErrorCode } from '../../errors/app-error';
import { DocumentAccessAuth, type TDocumentAuthMethods } from '../../types/document-auth'; import type { TDocumentAuthMethods } from '../../types/document-auth';
import { extractDocumentAuthMethods } from '../../utils/document-auth'; import { isRecipientAuthorized } from '../document/is-recipient-authorized';
import { getTeamSettings } from '../team/get-team-settings'; import { getTeamSettings } from '../team/get-team-settings';
import type { EnvelopeForSigningResponse } from './get-envelope-for-recipient-signing'; import type { EnvelopeForSigningResponse } from './get-envelope-for-recipient-signing';
import { ZEnvelopeForSigningResponse } from './get-envelope-for-recipient-signing'; import { ZEnvelopeForSigningResponse } from './get-envelope-for-recipient-signing';
@ -99,28 +98,14 @@ export const getEnvelopeForDirectTemplateSigning = async ({
}); });
} }
// Currently not using this since for direct templates "User" access means they just need to be const documentAccessValid = await isRecipientAuthorized({
// logged in. type: 'ACCESS',
// const documentAccessValid = await isRecipientAuthorized({ documentAuthOptions: envelope.authOptions,
// type: 'ACCESS', recipient,
// documentAuthOptions: envelope.authOptions, userId,
// recipient, authOptions: accessAuth,
// userId,
// authOptions: accessAuth,
// });
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
documentAuth: envelope.authOptions,
}); });
// Ensure typesafety when we add more options.
const documentAccessValid = derivedRecipientAccessAuth.every((auth) =>
match(auth)
.with(DocumentAccessAuth.ACCOUNT, () => Boolean(userId))
.with(DocumentAccessAuth.TWO_FACTOR_AUTH, () => true)
.exhaustive(),
);
if (!documentAccessValid) { if (!documentAccessValid) {
throw new AppError(AppErrorCode.UNAUTHORIZED, { throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'Invalid access values', message: 'Invalid access values',

View File

@ -54,3 +54,54 @@ export const getEnvelopeRequiredAccessData = async ({ token }: { token: string }
recipientHasAccount: Boolean(recipientUserAccount), recipientHasAccount: Boolean(recipientUserAccount),
} as const; } as const;
}; };
export const getEnvelopeDirectTemplateRequiredAccessData = async ({ token }: { token: string }) => {
const envelope = await prisma.envelope.findFirst({
where: {
type: EnvelopeType.TEMPLATE,
directLink: {
enabled: true,
token,
},
status: DocumentStatus.DRAFT,
},
include: {
recipients: {
where: {
token,
},
},
directLink: true,
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
const recipient = envelope.recipients.find(
(r) => r.id === envelope.directLink?.directTemplateRecipientId,
);
if (!recipient) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Recipient not found',
});
}
const recipientUserAccount = await prisma.user.findFirst({
where: {
email: recipient.email.toLowerCase(),
},
select: {
id: true,
},
});
return {
recipientEmail: recipient.email,
recipientHasAccount: Boolean(recipientUserAccount),
} as const;
};

View File

@ -3,7 +3,6 @@ import { createElement } from 'react';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import type { Field, Signature } from '@prisma/client'; import type { Field, Signature } from '@prisma/client';
import { import {
DocumentSigningOrder,
DocumentSource, DocumentSource,
DocumentStatus, DocumentStatus,
EnvelopeType, EnvelopeType,
@ -27,7 +26,7 @@ import type { TSignFieldWithTokenMutationSchema } from '@documenso/trpc/server/f
import { getI18nInstance } from '../../client-only/providers/i18n-server'; import { getI18nInstance } from '../../client-only/providers/i18n-server';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { AppError, AppErrorCode } from '../../errors/app-error'; import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE, RECIPIENT_DIFF_TYPE } from '../../types/document-audit-logs'; import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import type { TRecipientActionAuthTypes } from '../../types/document-auth'; import type { TRecipientActionAuthTypes } from '../../types/document-auth';
import { DocumentAccessAuth, ZRecipientAuthOptionsSchema } from '../../types/document-auth'; import { DocumentAccessAuth, ZRecipientAuthOptionsSchema } from '../../types/document-auth';
import { ZFieldMetaSchema } from '../../types/field-meta'; import { ZFieldMetaSchema } from '../../types/field-meta';
@ -69,10 +68,6 @@ export type CreateDocumentFromDirectTemplateOptions = {
name?: string; name?: string;
email: string; email: string;
}; };
nextSigner?: {
email: string;
name: string;
};
}; };
type CreatedDirectRecipientField = { type CreatedDirectRecipientField = {
@ -97,7 +92,6 @@ export const createDocumentFromDirectTemplate = async ({
directTemplateExternalId, directTemplateExternalId,
signedFieldValues, signedFieldValues,
templateUpdatedAt, templateUpdatedAt,
nextSigner,
requestMetadata, requestMetadata,
user, user,
}: CreateDocumentFromDirectTemplateOptions): Promise<TCreateDocumentFromDirectTemplateResponse> => { }: CreateDocumentFromDirectTemplateOptions): Promise<TCreateDocumentFromDirectTemplateResponse> => {
@ -134,17 +128,6 @@ export const createDocumentFromDirectTemplate = async ({
throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Invalid or missing template' }); throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Invalid or missing template' });
} }
if (
nextSigner &&
(!directTemplateEnvelope.documentMeta?.allowDictateNextSigner ||
directTemplateEnvelope.documentMeta?.signingOrder !== DocumentSigningOrder.SEQUENTIAL)
) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message:
'You need to enable allowDictateNextSigner and sequential signing to dictate the next signer',
});
}
const directTemplateEnvelopeLegacyId = mapSecondaryIdToTemplateId( const directTemplateEnvelopeLegacyId = mapSecondaryIdToTemplateId(
directTemplateEnvelope.secondaryId, directTemplateEnvelope.secondaryId,
); );
@ -647,77 +630,6 @@ export const createDocumentFromDirectTemplate = async ({
}), }),
]; ];
if (nextSigner) {
const pendingRecipients = await tx.recipient.findMany({
select: {
id: true,
signingOrder: true,
name: true,
email: true,
role: true,
},
where: {
envelopeId: createdEnvelope.id,
signingStatus: {
not: SigningStatus.SIGNED,
},
role: {
not: RecipientRole.CC,
},
},
// Composite sort so our next recipient is always the one with the lowest signing order or id
// if there is a tie.
orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
});
const nextRecipient = pendingRecipients[0];
if (nextRecipient) {
auditLogsToCreate.push(
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
envelopeId: createdEnvelope.id,
user: {
name: user?.name || directRecipientName || '',
email: user?.email || directRecipientEmail,
},
metadata: requestMetadata,
data: {
recipientEmail: nextRecipient.email,
recipientName: nextRecipient.name,
recipientId: nextRecipient.id,
recipientRole: nextRecipient.role,
changes: [
{
type: RECIPIENT_DIFF_TYPE.NAME,
from: nextRecipient.name,
to: nextSigner.name,
},
{
type: RECIPIENT_DIFF_TYPE.EMAIL,
from: nextRecipient.email,
to: nextSigner.email,
},
],
},
}),
);
await tx.recipient.update({
where: { id: nextRecipient.id },
data: {
sendStatus: SendStatus.SENT,
...(nextSigner && documentMeta?.allowDictateNextSigner
? {
name: nextSigner.name,
email: nextSigner.email,
}
: {}),
},
});
}
}
await tx.documentAuditLog.createMany({ await tx.documentAuditLog.createMany({
data: auditLogsToCreate, data: auditLogsToCreate,
}); });

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -62,15 +62,16 @@ export const renderCheckboxFieldElement = (
const rectWidth = fieldRect.width() * groupScaleX; const rectWidth = fieldRect.width() * groupScaleX;
const rectHeight = fieldRect.height() * groupScaleY; const rectHeight = fieldRect.height() * groupScaleY;
// Todo: Envelopes - check sorting more than 10
// arr.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
const squares = fieldGroup const squares = fieldGroup
.find('.checkbox-square') .find('.checkbox-square')
.sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true })); .sort((a, b) => a.id().localeCompare(b.id()));
const checkmarks = fieldGroup const checkmarks = fieldGroup
.find('.checkbox-checkmark') .find('.checkbox-checkmark')
.sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true })); .sort((a, b) => a.id().localeCompare(b.id()));
const text = fieldGroup const text = fieldGroup.find('.checkbox-text').sort((a, b) => a.id().localeCompare(b.id()));
.find('.checkbox-text')
.sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true }));
const groupedItems = squares.map((square, i) => ({ const groupedItems = squares.map((square, i) => ({
squareElement: square, squareElement: square,

View File

@ -8,9 +8,9 @@ import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
import type { TFieldMetaSchema } from '../../types/field-meta'; import type { TFieldMetaSchema } from '../../types/field-meta';
import { renderCheckboxFieldElement } from './render-checkbox-field'; import { renderCheckboxFieldElement } from './render-checkbox-field';
import { renderDropdownFieldElement } from './render-dropdown-field'; import { renderDropdownFieldElement } from './render-dropdown-field';
import { renderGenericTextFieldElement } from './render-generic-text-field';
import { renderRadioFieldElement } from './render-radio-field'; import { renderRadioFieldElement } from './render-radio-field';
import { renderSignatureFieldElement } from './render-signature-field'; import { renderSignatureFieldElement } from './render-signature-field';
import { renderTextFieldElement } from './render-text-field';
export const MIN_FIELD_HEIGHT_PX = 12; export const MIN_FIELD_HEIGHT_PX = 12;
export const MIN_FIELD_WIDTH_PX = 36; export const MIN_FIELD_WIDTH_PX = 36;
@ -43,9 +43,9 @@ type RenderFieldOptions = {
* *
* @default 'edit' * @default 'edit'
* *
* - `edit` - The field is rendered in editor page. * - `edit` - The field is rendered in edit mode.
* - `sign` - The field is rendered for the signing page. * - `sign` - The field is rendered in sign mode. No interactive elements.
* - `export` - The field is rendered for exporting and sealing into the PDF. No backgrounds, interactive elements, etc. * - `export` - The field is rendered in export mode. No backgrounds, interactive elements, etc.
*/ */
mode: 'edit' | 'sign' | 'export'; mode: 'edit' | 'sign' | 'export';
@ -76,21 +76,10 @@ export const renderField = ({
}; };
return match(field.type) return match(field.type)
.with( .with(FieldType.TEXT, () => renderTextFieldElement(field, options))
FieldType.INITIALS,
FieldType.NAME,
FieldType.EMAIL,
FieldType.DATE,
FieldType.TEXT,
FieldType.NUMBER,
() => renderGenericTextFieldElement(field, options),
)
.with(FieldType.CHECKBOX, () => renderCheckboxFieldElement(field, options)) .with(FieldType.CHECKBOX, () => renderCheckboxFieldElement(field, options))
.with(FieldType.RADIO, () => renderRadioFieldElement(field, options)) .with(FieldType.RADIO, () => renderRadioFieldElement(field, options))
.with(FieldType.DROPDOWN, () => renderDropdownFieldElement(field, options)) .with(FieldType.DROPDOWN, () => renderDropdownFieldElement(field, options))
.with(FieldType.SIGNATURE, () => renderSignatureFieldElement(field, options)) .with(FieldType.SIGNATURE, () => renderSignatureFieldElement(field, options))
.with(FieldType.FREE_SIGNATURE, () => { .otherwise(() => renderTextFieldElement(field, options)); // Todo: Envelopes
throw new Error('Free signature fields are not supported');
})
.exhaustive();
}; };

View File

@ -12,8 +12,6 @@ import {
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer'; import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
import { calculateFieldPosition } from './field-renderer'; import { calculateFieldPosition } from './field-renderer';
const DEFAULT_TEXT_ALIGN = 'left';
const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Text => { const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Text => {
const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options; const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options;
@ -33,8 +31,8 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
// Calculate text positioning based on alignment // Calculate text positioning based on alignment
const textX = 0; const textX = 0;
const textY = 0; const textY = 0;
let textAlign: 'left' | 'center' | 'right' = textMeta?.textAlign || DEFAULT_TEXT_ALIGN; let textAlign: 'left' | 'center' | 'right' = textMeta?.textAlign || 'left';
const textVerticalAlign: 'top' | 'middle' | 'bottom' = 'middle'; let textVerticalAlign: 'top' | 'middle' | 'bottom' = 'top';
const textFontSize = textMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE; const textFontSize = textMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
const textPadding = 10; const textPadding = 10;
@ -42,33 +40,51 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
// Handle edit mode. // Handle edit mode.
if (mode === 'edit') { if (mode === 'edit') {
if (textMeta?.text) { textToRender = fieldTypeName;
textToRender = textMeta.text; textAlign = 'center';
} else if (textMeta?.label) { textVerticalAlign = 'middle';
if (textMeta?.label) {
textToRender = textMeta.label; textToRender = textMeta.label;
} else { } else if (textMeta?.text) {
// Show field name which is centered for the edit mode if no label/text is avaliable. textToRender = textMeta.text;
textToRender = fieldTypeName; textAlign = textMeta.textAlign || 'center'; // Todo: Envelopes - What is the default
textAlign = 'center';
// Todo: Envelopes - Handle this on signatures
if (textMeta.characterLimit) {
textToRender = textToRender.slice(0, textMeta.characterLimit);
}
} }
} }
// Handle sign mode. // Handle sign mode.
if (mode === 'sign' || mode === 'export') { if (mode === 'sign' || mode === 'export') {
if (!field.inserted) { textToRender = fieldTypeName;
if (textMeta?.text) { textAlign = 'center';
textToRender = textMeta.text; textVerticalAlign = 'middle';
} else if (textMeta?.label) {
textToRender = textMeta.label; if (textMeta?.label) {
} else if (mode === 'sign') { textToRender = textMeta.label;
// Only show the field name in sign mode if no text/label is avaliable. }
textToRender = fieldTypeName;
textAlign = 'center'; if (textMeta?.text) {
textToRender = textMeta.text;
textAlign = textMeta.textAlign || 'center'; // Todo: Envelopes - What is the default
// Todo: Envelopes - Handle this on signatures
if (textMeta.characterLimit) {
textToRender = textToRender.slice(0, textMeta.characterLimit);
} }
} }
if (field.inserted) { if (field.inserted) {
textToRender = field.customText; textToRender = field.customText;
textAlign = textMeta?.textAlign || 'center'; // Todo: Envelopes - What is the default
// Todo: Envelopes - Handle this on signatures
if (textMeta?.characterLimit) {
textToRender = textToRender.slice(0, textMeta.characterLimit);
}
} }
} }
@ -90,7 +106,7 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
return fieldText; return fieldText;
}; };
export const renderGenericTextFieldElement = ( export const renderTextFieldElement = (
field: FieldToRender, field: FieldToRender,
options: RenderFieldElementOptions, options: RenderFieldElementOptions,
) => { ) => {

View File

@ -28,7 +28,6 @@ type SeedTemplateOptions = {
title?: string; title?: string;
userId: number; userId: number;
teamId: number; teamId: number;
internalVersion?: 1 | 2;
createTemplateOptions?: Partial<Prisma.EnvelopeUncheckedCreateInput>; createTemplateOptions?: Partial<Prisma.EnvelopeUncheckedCreateInput>;
}; };
@ -168,7 +167,7 @@ export const seedDirectTemplate = async (options: SeedTemplateOptions) => {
data: { data: {
id: prefixedId('envelope'), id: prefixedId('envelope'),
secondaryId: templateId.formattedTemplateId, secondaryId: templateId.formattedTemplateId,
internalVersion: options.internalVersion ?? 1, internalVersion: 1,
type: EnvelopeType.TEMPLATE, type: EnvelopeType.TEMPLATE,
title, title,
envelopeItems: { envelopeItems: {
@ -185,7 +184,6 @@ export const seedDirectTemplate = async (options: SeedTemplateOptions) => {
teamId, teamId,
recipients: { recipients: {
create: { create: {
signingOrder: 1,
email: DIRECT_TEMPLATE_RECIPIENT_EMAIL, email: DIRECT_TEMPLATE_RECIPIENT_EMAIL,
name: DIRECT_TEMPLATE_RECIPIENT_NAME, name: DIRECT_TEMPLATE_RECIPIENT_NAME,
token: Math.random().toString().slice(2, 7), token: Math.random().toString().slice(2, 7),

View File

@ -133,49 +133,6 @@ export const signEnvelopeFieldRoute = procedure
const insertionValues = extractFieldInsertionValues({ fieldValue, field, documentMeta }); const insertionValues = extractFieldInsertionValues({ fieldValue, field, documentMeta });
// Early return for uninserting fields.
if (!insertionValues.inserted) {
return await prisma.$transaction(async (tx) => {
const updatedField = await tx.field.update({
where: {
id: field.id,
},
data: {
customText: '',
inserted: false,
},
});
await tx.signature.deleteMany({
where: {
fieldId: field.id,
},
});
if (recipient.role !== RecipientRole.ASSISTANT) {
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_UNINSERTED,
envelopeId: envelope.id,
user: {
name: recipient.name,
email: recipient.email,
},
requestMetadata: metadata.requestMetadata,
data: {
field: field.type,
fieldId: field.secondaryId,
},
}),
});
}
return {
signedField: updatedField,
};
});
}
const derivedRecipientActionAuth = await validateFieldAuth({ const derivedRecipientActionAuth = await validateFieldAuth({
documentAuthOptions: envelope.authOptions, documentAuthOptions: envelope.authOptions,
recipient, recipient,

View File

@ -519,7 +519,6 @@ export const templateRouter = router({
directTemplateExternalId, directTemplateExternalId,
signedFieldValues, signedFieldValues,
templateUpdatedAt, templateUpdatedAt,
nextSigner,
} = input; } = input;
ctx.logger.info({ ctx.logger.info({
@ -542,7 +541,6 @@ export const templateRouter = router({
email: ctx.user.email, email: ctx.user.email,
} }
: undefined, : undefined,
nextSigner,
requestMetadata: ctx.metadata, requestMetadata: ctx.metadata,
}); });
}), }),

View File

@ -90,12 +90,6 @@ export const ZCreateDocumentFromDirectTemplateRequestSchema = z.object({
directTemplateExternalId: z.string().optional(), directTemplateExternalId: z.string().optional(),
signedFieldValues: z.array(ZSignFieldWithTokenMutationSchema), signedFieldValues: z.array(ZSignFieldWithTokenMutationSchema),
templateUpdatedAt: z.date(), templateUpdatedAt: z.date(),
nextSigner: z
.object({
email: z.string().email().max(254),
name: z.string().min(1).max(255),
})
.optional(),
}); });
export const ZCreateDocumentFromTemplateRequestSchema = z.object({ export const ZCreateDocumentFromTemplateRequestSchema = z.object({