mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
fix: add preview page
This commit is contained in:
@ -1,10 +1,20 @@
|
|||||||
import { lazy, useEffect, useState } from 'react';
|
import { lazy, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { faker } from '@faker-js/faker/locale/en';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { ConstructionIcon, FileTextIcon } from 'lucide-react';
|
import { FieldType } from '@prisma/client';
|
||||||
|
import { FileTextIcon } from 'lucide-react';
|
||||||
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
import { useCurrentEnvelopeEditor } from '@documenso/lib/client-only/providers/envelope-editor-provider';
|
||||||
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
import {
|
||||||
|
EnvelopeRenderProvider,
|
||||||
|
useCurrentEnvelopeRender,
|
||||||
|
} from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||||
|
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
|
import { extractFieldInsertionValues } from '@documenso/lib/utils/envelope-signing';
|
||||||
|
import { toCheckboxCustomText } from '@documenso/lib/utils/fields';
|
||||||
|
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||||
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, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
@ -15,15 +25,169 @@ import { EnvelopeRendererFileSelector } from './envelope-file-selector';
|
|||||||
|
|
||||||
const EnvelopeGenericPageRenderer = lazy(async () => import('./envelope-generic-page-renderer'));
|
const EnvelopeGenericPageRenderer = lazy(async () => import('./envelope-generic-page-renderer'));
|
||||||
|
|
||||||
|
// Todo: Envelopes - Dynamically import faker
|
||||||
export const EnvelopeEditorPreviewPage = () => {
|
export const EnvelopeEditorPreviewPage = () => {
|
||||||
const { envelope, editorFields } = useCurrentEnvelopeEditor();
|
const { envelope, editorFields } = useCurrentEnvelopeEditor();
|
||||||
|
|
||||||
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
const { currentEnvelopeItem, fields } = useCurrentEnvelopeRender();
|
||||||
|
|
||||||
const [selectedPreviewMode, setSelectedPreviewMode] = useState<'recipient' | 'signed'>(
|
const [selectedPreviewMode, setSelectedPreviewMode] = useState<'recipient' | 'signed'>(
|
||||||
'recipient',
|
'recipient',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const fieldsWithPlaceholders = useMemo(() => {
|
||||||
|
return fields.map((field) => {
|
||||||
|
const fieldMeta = ZFieldAndMetaSchema.parse(field);
|
||||||
|
|
||||||
|
const recipient = envelope.recipients.find((recipient) => recipient.id === field.recipientId);
|
||||||
|
|
||||||
|
if (!recipient) {
|
||||||
|
throw new Error('Recipient not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
faker.seed(recipient.id);
|
||||||
|
|
||||||
|
const recipientName = recipient.name || faker.person.fullName();
|
||||||
|
const recipientEmail = recipient.email || faker.internet.email();
|
||||||
|
|
||||||
|
faker.seed(recipient.id + field.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...field,
|
||||||
|
inserted: true,
|
||||||
|
...match(fieldMeta)
|
||||||
|
.with({ type: FieldType.TEXT }, ({ fieldMeta }) => {
|
||||||
|
let text = fieldMeta?.text || faker.lorem.words(5);
|
||||||
|
|
||||||
|
if (fieldMeta?.characterLimit) {
|
||||||
|
text = text.slice(0, fieldMeta?.characterLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
customText: text,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.with({ type: FieldType.NUMBER }, ({ fieldMeta }) => {
|
||||||
|
let number = fieldMeta?.value ?? '';
|
||||||
|
|
||||||
|
if (number === '') {
|
||||||
|
number = faker.number
|
||||||
|
.int({
|
||||||
|
min: fieldMeta?.minValue ?? 0,
|
||||||
|
max: fieldMeta?.maxValue ?? 1000,
|
||||||
|
})
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
customText: number,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.with({ type: FieldType.DATE }, () => {
|
||||||
|
const date = extractFieldInsertionValues({
|
||||||
|
fieldValue: {
|
||||||
|
type: FieldType.DATE,
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
field,
|
||||||
|
documentMeta: envelope.documentMeta,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
customText: date.customText,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.with({ type: FieldType.EMAIL }, () => {
|
||||||
|
return {
|
||||||
|
customText: recipientEmail,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.with({ type: FieldType.NAME }, () => {
|
||||||
|
return {
|
||||||
|
customText: recipientName,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.with({ type: FieldType.INITIALS }, () => {
|
||||||
|
return {
|
||||||
|
customText: extractInitials(recipientName),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.with({ type: FieldType.RADIO }, ({ fieldMeta }) => {
|
||||||
|
const values = fieldMeta?.values ?? [];
|
||||||
|
|
||||||
|
if (values.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let customText = '';
|
||||||
|
|
||||||
|
const preselectedValue = values.findIndex((value) => value.checked);
|
||||||
|
|
||||||
|
if (preselectedValue !== -1) {
|
||||||
|
customText = preselectedValue.toString();
|
||||||
|
} else {
|
||||||
|
const randomIndex = faker.number.int({ min: 0, max: values.length - 1 });
|
||||||
|
customText = randomIndex.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
customText,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.with({ type: FieldType.CHECKBOX }, ({ fieldMeta }) => {
|
||||||
|
let checkedValues: number[] = [];
|
||||||
|
|
||||||
|
const values = fieldMeta?.values ?? [];
|
||||||
|
|
||||||
|
values.forEach((value, index) => {
|
||||||
|
if (value.checked) {
|
||||||
|
checkedValues.push(index);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (checkedValues.length === 0 && values.length > 0) {
|
||||||
|
const numberOfValues = fieldMeta?.validationLength || 1;
|
||||||
|
|
||||||
|
checkedValues = Array.from({ length: numberOfValues }, (_, index) => index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
customText: toCheckboxCustomText(checkedValues),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.with({ type: FieldType.DROPDOWN }, ({ fieldMeta }) => {
|
||||||
|
const values = fieldMeta?.values ?? [];
|
||||||
|
|
||||||
|
let customText = fieldMeta?.defaultValue || '';
|
||||||
|
|
||||||
|
if (!customText && values.length > 0) {
|
||||||
|
const randomIndex = faker.number.int({ min: 0, max: values.length - 1 });
|
||||||
|
customText = values[randomIndex].value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
customText,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.with({ type: FieldType.SIGNATURE }, () => {
|
||||||
|
return {
|
||||||
|
customText: '',
|
||||||
|
signature: {
|
||||||
|
signatureImageAsBase64: '',
|
||||||
|
typedSignature: recipientName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.with({ type: FieldType.FREE_SIGNATURE }, () => {
|
||||||
|
return {
|
||||||
|
customText: '',
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.exhaustive(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [fields, envelope, envelope.recipients, envelope.documentMeta]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the selected recipient to the first recipient in the envelope.
|
* Set the selected recipient to the first recipient in the envelope.
|
||||||
*/
|
*/
|
||||||
@ -31,40 +195,38 @@ export const EnvelopeEditorPreviewPage = () => {
|
|||||||
editorFields.setSelectedRecipient(envelope.recipients[0]?.id ?? null);
|
editorFields.setSelectedRecipient(envelope.recipients[0]?.id ?? null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Override the parent renderer provider so we can inject custom fields.
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full">
|
<EnvelopeRenderProvider
|
||||||
<div className="flex w-full flex-col overflow-y-auto">
|
envelope={envelope}
|
||||||
{/* Horizontal envelope item selector */}
|
token={undefined}
|
||||||
<EnvelopeRendererFileSelector fields={editorFields.localFields} />
|
fields={fieldsWithPlaceholders}
|
||||||
|
recipientIds={envelope.recipients.map((recipient) => recipient.id)}
|
||||||
|
overrideSettings={{
|
||||||
|
mode: 'export',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="relative flex h-full">
|
||||||
|
<div className="flex w-full flex-col overflow-y-auto">
|
||||||
|
{/* Horizontal envelope item selector */}
|
||||||
|
<EnvelopeRendererFileSelector fields={editorFields.localFields} />
|
||||||
|
|
||||||
{/* Document View */}
|
{/* Document View */}
|
||||||
<div className="mt-4 flex flex-col items-center justify-center">
|
<div className="mt-4 flex flex-col items-center justify-center">
|
||||||
<Alert variant="warning" className="mb-4 max-w-[800px]">
|
<Alert variant="warning" className="mb-4 max-w-[800px]">
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans>Preview Mode</Trans>
|
<Trans>Preview Mode</Trans>
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans>Preview what the signed document will look like with placeholder data</Trans>
|
<Trans>Preview what the signed document will look like with placeholder data</Trans>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
{/* Coming soon section */}
|
|
||||||
<div className="border-border bg-card hover:bg-accent/10 flex w-full max-w-[800px] items-center gap-4 rounded-lg border p-4 transition-colors">
|
|
||||||
<div className="flex w-full flex-col items-center justify-center gap-2 py-32">
|
|
||||||
<ConstructionIcon className="text-muted-foreground h-10 w-10" />
|
|
||||||
<h3 className="text-foreground text-sm font-semibold">
|
|
||||||
<Trans>Coming soon</Trans>
|
|
||||||
</h3>
|
|
||||||
<p className="text-muted-foreground text-sm">
|
|
||||||
<Trans>This feature is coming soon</Trans>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Todo: Envelopes - Remove div after preview mode is implemented */}
|
|
||||||
<div className="hidden">
|
|
||||||
{currentEnvelopeItem !== null ? (
|
{currentEnvelopeItem !== null ? (
|
||||||
<PDFViewerKonvaLazy customPageRenderer={EnvelopeGenericPageRenderer} />
|
<PDFViewerKonvaLazy
|
||||||
|
renderer="editor"
|
||||||
|
customPageRenderer={EnvelopeGenericPageRenderer}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col items-center justify-center py-32">
|
<div className="flex flex-col items-center justify-center py-32">
|
||||||
<FileTextIcon className="text-muted-foreground h-10 w-10" />
|
<FileTextIcon className="text-muted-foreground h-10 w-10" />
|
||||||
@ -78,27 +240,28 @@ export const EnvelopeEditorPreviewPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right Section - Form Fields Panel */}
|
{/* Right Section - Form Fields Panel */}
|
||||||
{currentEnvelopeItem && false && (
|
{currentEnvelopeItem && false && (
|
||||||
<div className="sticky top-0 h-full w-80 flex-shrink-0 overflow-y-auto border-l border-gray-200 bg-white py-4">
|
<div className="sticky top-0 h-full w-80 flex-shrink-0 overflow-y-auto border-l border-gray-200 bg-white py-4">
|
||||||
{/* Add fields section. */}
|
{/* Add fields section. */}
|
||||||
<section className="px-4">
|
<section className="px-4">
|
||||||
{/* <h3 className="mb-2 text-sm font-semibold text-gray-900">
|
{/* <h3 className="mb-2 text-sm font-semibold text-gray-900">
|
||||||
<Trans>Preivew Mode</Trans>
|
<Trans>Preivew Mode</Trans>
|
||||||
</h3> */}
|
</h3> */}
|
||||||
|
|
||||||
<Alert variant="neutral">
|
<Alert variant="neutral">
|
||||||
<AlertTitle>
|
<AlertTitle>
|
||||||
<Trans>Preview Mode</Trans>
|
<Trans>Preview Mode</Trans>
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
<Trans>Preview what the signed document will look like with placeholder data</Trans>
|
<Trans>
|
||||||
</AlertDescription>
|
Preview what the signed document will look like with placeholder data
|
||||||
</Alert>
|
</Trans>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
{/* <Alert variant="neutral">
|
{/* <Alert variant="neutral">
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
className="gap-y-1"
|
className="gap-y-1"
|
||||||
value={selectedPreviewMode}
|
value={selectedPreviewMode}
|
||||||
@ -137,36 +300,37 @@ export const EnvelopeEditorPreviewPage = () => {
|
|||||||
<div>Preview what a recipient will see</div>
|
<div>Preview what a recipient will see</div>
|
||||||
|
|
||||||
<div>Preview the signed document</div> */}
|
<div>Preview the signed document</div> */}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{false && (
|
{false && (
|
||||||
<AnimateGenericFadeInOut key={selectedPreviewMode}>
|
<AnimateGenericFadeInOut key={selectedPreviewMode}>
|
||||||
{selectedPreviewMode === 'recipient' && (
|
{selectedPreviewMode === 'recipient' && (
|
||||||
<>
|
<>
|
||||||
<Separator className="my-4" />
|
<Separator className="my-4" />
|
||||||
|
|
||||||
{/* Recipient selector section. */}
|
{/* Recipient selector section. */}
|
||||||
<section className="px-4">
|
<section className="px-4">
|
||||||
<h3 className="mb-2 text-sm font-semibold text-gray-900">
|
<h3 className="mb-2 text-sm font-semibold text-gray-900">
|
||||||
<Trans>Selected Recipient</Trans>
|
<Trans>Selected Recipient</Trans>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<RecipientSelector
|
<RecipientSelector
|
||||||
selectedRecipient={editorFields.selectedRecipient}
|
selectedRecipient={editorFields.selectedRecipient}
|
||||||
onSelectedRecipientChange={(recipient) =>
|
onSelectedRecipientChange={(recipient) =>
|
||||||
editorFields.setSelectedRecipient(recipient.id)
|
editorFields.setSelectedRecipient(recipient.id)
|
||||||
}
|
}
|
||||||
recipients={envelope.recipients}
|
recipients={envelope.recipients}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
align="end"
|
align="end"
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</AnimateGenericFadeInOut>
|
</AnimateGenericFadeInOut>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</EnvelopeRenderProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -174,7 +174,7 @@ export const EnvelopeEditorSettingsDialog = ({
|
|||||||
const { t, i18n } = useLingui();
|
const { t, i18n } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const { envelope } = useCurrentEnvelopeEditor();
|
const { envelope, updateEnvelopeAsync } = useCurrentEnvelopeEditor();
|
||||||
|
|
||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
const organisation = useCurrentOrganisation();
|
const organisation = useCurrentOrganisation();
|
||||||
@ -186,14 +186,12 @@ export const EnvelopeEditorSettingsDialog = ({
|
|||||||
documentAuth: envelope.authOptions,
|
documentAuth: envelope.authOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
const form = useForm<TAddSettingsFormSchema>({
|
const createDefaultValues = () => {
|
||||||
resolver: zodResolver(ZAddSettingsFormSchema),
|
return {
|
||||||
defaultValues: {
|
externalId: envelope.externalId || '',
|
||||||
externalId: envelope.externalId || '', // Todo: String or undefined?
|
|
||||||
visibility: envelope.visibility || '',
|
visibility: envelope.visibility || '',
|
||||||
globalAccessAuth: documentAuthOption?.globalAccessAuth || [],
|
globalAccessAuth: documentAuthOption?.globalAccessAuth || [],
|
||||||
globalActionAuth: documentAuthOption?.globalActionAuth || [],
|
globalActionAuth: documentAuthOption?.globalActionAuth || [],
|
||||||
|
|
||||||
meta: {
|
meta: {
|
||||||
subject: envelope.documentMeta.subject ?? '',
|
subject: envelope.documentMeta.subject ?? '',
|
||||||
message: envelope.documentMeta.message ?? '',
|
message: envelope.documentMeta.message ?? '',
|
||||||
@ -210,10 +208,13 @@ export const EnvelopeEditorSettingsDialog = ({
|
|||||||
emailSettings: ZDocumentEmailSettingsSchema.parse(envelope.documentMeta.emailSettings),
|
emailSettings: ZDocumentEmailSettingsSchema.parse(envelope.documentMeta.emailSettings),
|
||||||
signatureTypes: extractTeamSignatureSettings(envelope.documentMeta),
|
signatureTypes: extractTeamSignatureSettings(envelope.documentMeta),
|
||||||
},
|
},
|
||||||
},
|
};
|
||||||
});
|
};
|
||||||
|
|
||||||
const { mutateAsync: updateEnvelope } = trpc.envelope.update.useMutation();
|
const form = useForm<TAddSettingsFormSchema>({
|
||||||
|
resolver: zodResolver(ZAddSettingsFormSchema),
|
||||||
|
defaultValues: createDefaultValues(),
|
||||||
|
});
|
||||||
|
|
||||||
const envelopeHasBeenSent =
|
const envelopeHasBeenSent =
|
||||||
envelope.type === EnvelopeType.DOCUMENT &&
|
envelope.type === EnvelopeType.DOCUMENT &&
|
||||||
@ -239,8 +240,7 @@ export const EnvelopeEditorSettingsDialog = ({
|
|||||||
.safeParse(data.globalAccessAuth);
|
.safeParse(data.globalAccessAuth);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateEnvelope({
|
await updateEnvelopeAsync({
|
||||||
envelopeId: envelope.id,
|
|
||||||
data: {
|
data: {
|
||||||
externalId: data.externalId || null,
|
externalId: data.externalId || null,
|
||||||
visibility: data.visibility,
|
visibility: data.visibility,
|
||||||
@ -295,7 +295,7 @@ export const EnvelopeEditorSettingsDialog = ({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset();
|
form.reset(createDefaultValues());
|
||||||
setActiveTab('general');
|
setActiveTab('general');
|
||||||
}, [open, form]);
|
}, [open, form]);
|
||||||
|
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
|
|||||||
export default function EnvelopeGenericPageRenderer() {
|
export default function EnvelopeGenericPageRenderer() {
|
||||||
const { i18n } = useLingui();
|
const { i18n } = useLingui();
|
||||||
|
|
||||||
const { currentEnvelopeItem, fields, getRecipientColorKey, setRenderError } =
|
const { currentEnvelopeItem, fields, getRecipientColorKey, setRenderError, overrideSettings } =
|
||||||
useCurrentEnvelopeRender();
|
useCurrentEnvelopeRender();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -50,12 +50,11 @@ export default function EnvelopeGenericPageRenderer() {
|
|||||||
field: {
|
field: {
|
||||||
renderId: field.id.toString(),
|
renderId: field.id.toString(),
|
||||||
...field,
|
...field,
|
||||||
customText: '',
|
|
||||||
width: Number(field.width),
|
width: Number(field.width),
|
||||||
height: Number(field.height),
|
height: Number(field.height),
|
||||||
positionX: Number(field.positionX),
|
positionX: Number(field.positionX),
|
||||||
positionY: Number(field.positionY),
|
positionY: Number(field.positionY),
|
||||||
inserted: false,
|
customText: field.inserted ? field.customText : '',
|
||||||
fieldMeta: field.fieldMeta,
|
fieldMeta: field.fieldMeta,
|
||||||
},
|
},
|
||||||
translations: getClientSideFieldTranslations(i18n),
|
translations: getClientSideFieldTranslations(i18n),
|
||||||
@ -63,7 +62,7 @@ export default function EnvelopeGenericPageRenderer() {
|
|||||||
pageHeight: unscaledViewport.height,
|
pageHeight: unscaledViewport.height,
|
||||||
color: getRecipientColorKey(field.recipientId),
|
color: getRecipientColorKey(field.recipientId),
|
||||||
editable: false,
|
editable: false,
|
||||||
mode: 'sign',
|
mode: overrideSettings?.mode ?? 'sign',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,7 @@
|
|||||||
"@documenso/trpc": "*",
|
"@documenso/trpc": "*",
|
||||||
"@documenso/ui": "*",
|
"@documenso/ui": "*",
|
||||||
"@epic-web/remember": "^1.1.0",
|
"@epic-web/remember": "^1.1.0",
|
||||||
|
"@faker-js/faker": "^10.1.0",
|
||||||
"@hono/node-server": "^1.13.7",
|
"@hono/node-server": "^1.13.7",
|
||||||
"@hono/trpc-server": "^0.3.4",
|
"@hono/trpc-server": "^0.3.4",
|
||||||
"@hookform/resolvers": "^3.1.0",
|
"@hookform/resolvers": "^3.1.0",
|
||||||
@ -104,4 +105,4 @@
|
|||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
},
|
},
|
||||||
"version": "1.13.1"
|
"version": "1.13.1"
|
||||||
}
|
}
|
||||||
|
|||||||
17
package-lock.json
generated
17
package-lock.json
generated
@ -113,6 +113,7 @@
|
|||||||
"@documenso/trpc": "*",
|
"@documenso/trpc": "*",
|
||||||
"@documenso/ui": "*",
|
"@documenso/ui": "*",
|
||||||
"@epic-web/remember": "^1.1.0",
|
"@epic-web/remember": "^1.1.0",
|
||||||
|
"@faker-js/faker": "^10.1.0",
|
||||||
"@hono/node-server": "^1.13.7",
|
"@hono/node-server": "^1.13.7",
|
||||||
"@hono/trpc-server": "^0.3.4",
|
"@hono/trpc-server": "^0.3.4",
|
||||||
"@hookform/resolvers": "^3.1.0",
|
"@hookform/resolvers": "^3.1.0",
|
||||||
@ -3512,6 +3513,22 @@
|
|||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@faker-js/faker": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-C3mrr3b5dRVlKPJdfrAXS8+dq+rq8Qm5SNRazca0JKgw1HQERFmrVb0towvMmw5uu8hHKNiQasMaR/tydf3Zsg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fakerjs"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0",
|
||||||
|
"npm": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@floating-ui/core": {
|
"node_modules/@floating-ui/core": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz",
|
||||||
|
|||||||
@ -25,8 +25,7 @@ import { DocumentStatus } from '@prisma/client';
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js';
|
import * as pdfjsLib from 'pdfjs-dist/legacy/build/pdf.js';
|
||||||
|
import { getEnvelopeDownloadUrl } from '@documenso/lib/utils/envelope-download';
|
||||||
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { seedAlignmentTestDocument } from '@documenso/prisma/seed/initial-seed';
|
import { seedAlignmentTestDocument } from '@documenso/prisma/seed/initial-seed';
|
||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
@ -95,7 +94,13 @@ test('field placement visual regression', async ({ page }, testInfo) => {
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
completedDocument.envelopeItems.map(async (item) => {
|
completedDocument.envelopeItems.map(async (item) => {
|
||||||
const pdfData = await getFile(item.documentData);
|
const documentUrl = getEnvelopeDownloadUrl({
|
||||||
|
envelopeItem: item,
|
||||||
|
token,
|
||||||
|
version: 'signed',
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfData = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
|
||||||
|
|
||||||
const loadedImages = storedImages
|
const loadedImages = storedImages
|
||||||
.filter((image) => image.includes(item.title))
|
.filter((image) => image.includes(item.title))
|
||||||
@ -103,7 +108,7 @@ test('field placement visual regression', async ({ page }, testInfo) => {
|
|||||||
|
|
||||||
await compareSignedPdfWithImages({
|
await compareSignedPdfWithImages({
|
||||||
id: item.title.replaceAll(' ', '-').toLowerCase(),
|
id: item.title.replaceAll(' ', '-').toLowerCase(),
|
||||||
pdfData,
|
pdfData: new Uint8Array(pdfData),
|
||||||
images: loadedImages,
|
images: loadedImages,
|
||||||
testInfo,
|
testInfo,
|
||||||
});
|
});
|
||||||
@ -174,9 +179,15 @@ test.skip('download envelope images', async ({ page }) => {
|
|||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
completedDocument.envelopeItems.map(async (item) => {
|
completedDocument.envelopeItems.map(async (item) => {
|
||||||
const pdfData = await getFile(item.documentData);
|
const documentUrl = getEnvelopeDownloadUrl({
|
||||||
|
envelopeItem: item,
|
||||||
|
token,
|
||||||
|
version: 'signed',
|
||||||
|
});
|
||||||
|
|
||||||
const pdfImages = await renderPdfToImage(pdfData);
|
const pdfData = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
|
||||||
|
|
||||||
|
const pdfImages = await renderPdfToImage(new Uint8Array(pdfData));
|
||||||
|
|
||||||
for (const [index, { image }] of pdfImages.entries()) {
|
for (const [index, { image }] of pdfImages.entries()) {
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test';
|
|||||||
import { DocumentStatus, FieldType } from '@prisma/client';
|
import { DocumentStatus, FieldType } from '@prisma/client';
|
||||||
|
|
||||||
import { getDocumentByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
import { getDocumentByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import { getFile } from '@documenso/lib/universal/upload/get-file';
|
import { getEnvelopeDownloadUrl } from '@documenso/lib/utils/envelope-download';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
|
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
|
||||||
import { seedTeam } from '@documenso/prisma/seed/teams';
|
import { seedTeam } from '@documenso/prisma/seed/teams';
|
||||||
@ -25,20 +25,25 @@ test.describe('Signing Certificate Tests', () => {
|
|||||||
teamId: team.id,
|
teamId: team.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const documentData = await prisma.documentData
|
const recipient = recipients[0];
|
||||||
|
|
||||||
|
const documentData = await prisma.envelopeItem
|
||||||
.findFirstOrThrow({
|
.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
envelopeItem: {
|
envelopeId: document.id,
|
||||||
envelopeId: document.id,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(async (data) => getFile(data));
|
.then(async (data) => {
|
||||||
|
const documentUrl = getEnvelopeDownloadUrl({
|
||||||
|
envelopeItem: data,
|
||||||
|
token: recipient.token,
|
||||||
|
version: 'signed',
|
||||||
|
});
|
||||||
|
return fetch(documentUrl).then(async (res) => await res.arrayBuffer());
|
||||||
|
});
|
||||||
|
|
||||||
const originalPdf = await PDFDocument.load(documentData);
|
const originalPdf = await PDFDocument.load(documentData);
|
||||||
|
|
||||||
const recipient = recipients[0];
|
|
||||||
|
|
||||||
// Sign the document
|
// Sign the document
|
||||||
await page.goto(`/sign/${recipient.token}`);
|
await page.goto(`/sign/${recipient.token}`);
|
||||||
|
|
||||||
@ -78,9 +83,17 @@ test.describe('Signing Certificate Tests', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const firstDocumentData = completedDocument.envelopeItems[0].documentData;
|
const firstDocumentData = completedDocument.envelopeItems[0];
|
||||||
|
|
||||||
const completedDocumentData = await getFile(firstDocumentData);
|
const documentUrl = getEnvelopeDownloadUrl({
|
||||||
|
envelopeItem: firstDocumentData,
|
||||||
|
token: recipient.token,
|
||||||
|
version: 'signed',
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfData = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
|
||||||
|
|
||||||
|
const completedDocumentData = new Uint8Array(pdfData);
|
||||||
|
|
||||||
// Load the PDF and check number of pages
|
// Load the PDF and check number of pages
|
||||||
const pdfDoc = await PDFDocument.load(completedDocumentData);
|
const pdfDoc = await PDFDocument.load(completedDocumentData);
|
||||||
@ -117,20 +130,25 @@ test.describe('Signing Certificate Tests', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const documentData = await prisma.documentData
|
const recipient = recipients[0];
|
||||||
|
|
||||||
|
const documentData = await prisma.envelopeItem
|
||||||
.findFirstOrThrow({
|
.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
envelopeItem: {
|
envelopeId: document.id,
|
||||||
envelopeId: document.id,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(async (data) => getFile(data));
|
.then(async (data) => {
|
||||||
|
const documentUrl = getEnvelopeDownloadUrl({
|
||||||
|
envelopeItem: data,
|
||||||
|
token: recipient.token,
|
||||||
|
version: 'signed',
|
||||||
|
});
|
||||||
|
return fetch(documentUrl).then(async (res) => await res.arrayBuffer());
|
||||||
|
});
|
||||||
|
|
||||||
const originalPdf = await PDFDocument.load(documentData);
|
const originalPdf = await PDFDocument.load(documentData);
|
||||||
|
|
||||||
const recipient = recipients[0];
|
|
||||||
|
|
||||||
// Sign the document
|
// Sign the document
|
||||||
await page.goto(`/sign/${recipient.token}`);
|
await page.goto(`/sign/${recipient.token}`);
|
||||||
|
|
||||||
@ -168,9 +186,17 @@ test.describe('Signing Certificate Tests', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const firstDocumentData = completedDocument.envelopeItems[0].documentData;
|
const firstDocumentData = completedDocument.envelopeItems[0];
|
||||||
|
|
||||||
const completedDocumentData = await getFile(firstDocumentData);
|
const documentUrl = getEnvelopeDownloadUrl({
|
||||||
|
envelopeItem: firstDocumentData,
|
||||||
|
token: recipient.token,
|
||||||
|
version: 'signed',
|
||||||
|
});
|
||||||
|
|
||||||
|
const pdfData = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
|
||||||
|
|
||||||
|
const completedDocumentData = new Uint8Array(pdfData);
|
||||||
|
|
||||||
// Load the PDF and check number of pages
|
// Load the PDF and check number of pages
|
||||||
const completedPdf = await PDFDocument.load(completedDocumentData);
|
const completedPdf = await PDFDocument.load(completedDocumentData);
|
||||||
@ -207,19 +233,24 @@ test.describe('Signing Certificate Tests', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const documentData = await prisma.documentData
|
const recipient = recipients[0];
|
||||||
|
|
||||||
|
const documentData = await prisma.envelopeItem
|
||||||
.findFirstOrThrow({
|
.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
envelopeItem: {
|
envelopeId: document.id,
|
||||||
envelopeId: document.id,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(async (data) => getFile(data));
|
.then(async (data) => {
|
||||||
|
const documentUrl = getEnvelopeDownloadUrl({
|
||||||
|
envelopeItem: data,
|
||||||
|
token: recipient.token,
|
||||||
|
version: 'signed',
|
||||||
|
});
|
||||||
|
return fetch(documentUrl).then(async (res) => await res.arrayBuffer());
|
||||||
|
});
|
||||||
|
|
||||||
const originalPdf = await PDFDocument.load(documentData);
|
const originalPdf = await PDFDocument.load(new Uint8Array(documentData));
|
||||||
|
|
||||||
const recipient = recipients[0];
|
|
||||||
|
|
||||||
// Sign the document
|
// Sign the document
|
||||||
await page.goto(`/sign/${recipient.token}`);
|
await page.goto(`/sign/${recipient.token}`);
|
||||||
@ -258,7 +289,15 @@ test.describe('Signing Certificate Tests', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const completedDocumentData = await getFile(completedDocument.envelopeItems[0].documentData);
|
const documentUrl = getEnvelopeDownloadUrl({
|
||||||
|
envelopeItem: completedDocument.envelopeItems[0],
|
||||||
|
token: recipient.token,
|
||||||
|
version: 'signed',
|
||||||
|
});
|
||||||
|
|
||||||
|
const completedDocumentData = await fetch(documentUrl).then(
|
||||||
|
async (res) => await res.arrayBuffer(),
|
||||||
|
);
|
||||||
|
|
||||||
// Load the PDF and check number of pages
|
// Load the PDF and check number of pages
|
||||||
const completedPdf = await PDFDocument.load(completedDocumentData);
|
const completedPdf = await PDFDocument.load(completedDocumentData);
|
||||||
|
|||||||
@ -46,6 +46,7 @@ type EnvelopeEditorProviderValue = {
|
|||||||
setLocalEnvelope: (localEnvelope: Partial<TEnvelope>) => void;
|
setLocalEnvelope: (localEnvelope: Partial<TEnvelope>) => void;
|
||||||
|
|
||||||
updateEnvelope: (envelopeUpdates: UpdateEnvelopePayload) => void;
|
updateEnvelope: (envelopeUpdates: UpdateEnvelopePayload) => void;
|
||||||
|
updateEnvelopeAsync: (envelopeUpdates: UpdateEnvelopePayload) => Promise<void>;
|
||||||
setRecipientsDebounced: (recipients: TSetEnvelopeRecipientsRequest['recipients']) => void;
|
setRecipientsDebounced: (recipients: TSetEnvelopeRecipientsRequest['recipients']) => void;
|
||||||
setRecipientsAsync: (recipients: TSetEnvelopeRecipientsRequest['recipients']) => Promise<void>;
|
setRecipientsAsync: (recipients: TSetEnvelopeRecipientsRequest['recipients']) => Promise<void>;
|
||||||
|
|
||||||
@ -66,8 +67,6 @@ type EnvelopeEditorProviderValue = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
syncEnvelope: () => Promise<void>;
|
syncEnvelope: () => Promise<void>;
|
||||||
// refetchEnvelope: () => Promise<void>;
|
|
||||||
// updateEnvelope: (envelope: TEnvelope) => Promise<void>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface EnvelopeEditorProviderProps {
|
interface EnvelopeEditorProviderProps {
|
||||||
@ -236,6 +235,13 @@ export const EnvelopeEditorProvider = ({
|
|||||||
setEnvelopeDebounced(envelopeUpdates);
|
setEnvelopeDebounced(envelopeUpdates);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateEnvelopeAsync = async (envelopeUpdates: UpdateEnvelopePayload) => {
|
||||||
|
await envelopeUpdateMutationQuery.mutateAsync({
|
||||||
|
envelopeId: envelope.id,
|
||||||
|
...envelopeUpdates,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const getRecipientColorKey = useCallback(
|
const getRecipientColorKey = useCallback(
|
||||||
(recipientId: number) => {
|
(recipientId: number) => {
|
||||||
const recipientIndex = envelope.recipients.findIndex(
|
const recipientIndex = envelope.recipients.findIndex(
|
||||||
@ -323,6 +329,7 @@ export const EnvelopeEditorProvider = ({
|
|||||||
setLocalEnvelope,
|
setLocalEnvelope,
|
||||||
getRecipientColorKey,
|
getRecipientColorKey,
|
||||||
updateEnvelope,
|
updateEnvelope,
|
||||||
|
updateEnvelopeAsync,
|
||||||
setRecipientsDebounced,
|
setRecipientsDebounced,
|
||||||
setRecipientsAsync,
|
setRecipientsAsync,
|
||||||
editorFields,
|
editorFields,
|
||||||
|
|||||||
@ -16,6 +16,10 @@ type FileData =
|
|||||||
status: 'loaded';
|
status: 'loaded';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type EnvelopeRenderOverrideSettings = {
|
||||||
|
mode: 'edit' | 'sign' | 'export';
|
||||||
|
};
|
||||||
|
|
||||||
type EnvelopeRenderItem = TEnvelope['envelopeItems'][number];
|
type EnvelopeRenderItem = TEnvelope['envelopeItems'][number];
|
||||||
|
|
||||||
type EnvelopeRenderProviderValue = {
|
type EnvelopeRenderProviderValue = {
|
||||||
@ -28,10 +32,12 @@ type EnvelopeRenderProviderValue = {
|
|||||||
|
|
||||||
renderError: boolean;
|
renderError: boolean;
|
||||||
setRenderError: (renderError: boolean) => void;
|
setRenderError: (renderError: boolean) => void;
|
||||||
|
overrideSettings?: EnvelopeRenderOverrideSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface EnvelopeRenderProviderProps {
|
interface EnvelopeRenderProviderProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
|
||||||
envelope: Pick<TEnvelope, 'envelopeItems'>;
|
envelope: Pick<TEnvelope, 'envelopeItems'>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,6 +60,11 @@ interface EnvelopeRenderProviderProps {
|
|||||||
* If not provided, it will be assumed that the current user can access the document.
|
* If not provided, it will be assumed that the current user can access the document.
|
||||||
*/
|
*/
|
||||||
token: string | undefined;
|
token: string | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom override settings for generic page renderers.
|
||||||
|
*/
|
||||||
|
overrideSettings?: EnvelopeRenderOverrideSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const EnvelopeRenderContext = createContext<EnvelopeRenderProviderValue | null>(null);
|
const EnvelopeRenderContext = createContext<EnvelopeRenderProviderValue | null>(null);
|
||||||
@ -77,6 +88,7 @@ export const EnvelopeRenderProvider = ({
|
|||||||
fields,
|
fields,
|
||||||
token,
|
token,
|
||||||
recipientIds = [],
|
recipientIds = [],
|
||||||
|
overrideSettings,
|
||||||
}: EnvelopeRenderProviderProps) => {
|
}: EnvelopeRenderProviderProps) => {
|
||||||
// Indexed by documentDataId.
|
// Indexed by documentDataId.
|
||||||
const [files, setFiles] = useState<Record<string, FileData>>({});
|
const [files, setFiles] = useState<Record<string, FileData>>({});
|
||||||
@ -185,6 +197,7 @@ export const EnvelopeRenderProvider = ({
|
|||||||
getRecipientColorKey,
|
getRecipientColorKey,
|
||||||
renderError,
|
renderError,
|
||||||
setRenderError,
|
setRenderError,
|
||||||
|
overrideSettings,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -58,7 +58,7 @@ const Combobox = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
|
|
||||||
<PopoverContent className="p-0" side="bottom" align="start">
|
<PopoverContent className="z-[1001] p-0" side="bottom" align="start">
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput placeholder={value || placeholderValue} />
|
<CommandInput placeholder={value || placeholderValue} />
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user