Compare commits
24 Commits
717fa8f870
...
v2.0.6
| Author | SHA1 | Date | |
|---|---|---|---|
| ff44ffbc03 | |||
| 441842d2bd | |||
| ca0b83579f | |||
| 6c0d1da91e | |||
| 805982f3e8 | |||
| e2f5e570cf | |||
| 9fd9613076 | |||
| 0977c16e33 | |||
| 88d5a636c3 | |||
| 1e6292b1d9 | |||
| d65866156d | |||
| fe8915162f | |||
| 37a2634aca | |||
| eff7d90f43 | |||
| db5524f8ce | |||
| 3d539b20ad | |||
| 48626b9169 | |||
| 88371b665a | |||
| 1650c55b19 | |||
| 60d73e0921 | |||
| 4a779ec81e | |||
| 7f19ec1265 | |||
| d6a2f5a4c9 | |||
| d05bfa9fed |
@ -19,13 +19,15 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
import { useCurrentTeam } from '~/providers/team';
|
import { useCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
type DocumentDuplicateDialogProps = {
|
type DocumentDuplicateDialogProps = {
|
||||||
id: number;
|
id: string;
|
||||||
|
token?: string;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (_open: boolean) => void;
|
onOpenChange: (_open: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentDuplicateDialog = ({
|
export const DocumentDuplicateDialog = ({
|
||||||
id,
|
id,
|
||||||
|
token,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: DocumentDuplicateDialogProps) => {
|
}: DocumentDuplicateDialogProps) => {
|
||||||
@ -36,27 +38,23 @@ export const DocumentDuplicateDialog = ({
|
|||||||
|
|
||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
|
|
||||||
const { data: document, isLoading } = trpcReact.document.get.useQuery(
|
const { data: envelopeItemsPayload, isLoading: isLoadingEnvelopeItems } =
|
||||||
{
|
trpcReact.envelope.item.getManyByToken.useQuery(
|
||||||
documentId: id,
|
{
|
||||||
},
|
envelopeId: id,
|
||||||
{
|
access: token ? { type: 'recipient', token } : { type: 'user' },
|
||||||
queryHash: `document-duplicate-dialog-${id}`,
|
},
|
||||||
enabled: open === true,
|
{
|
||||||
},
|
enabled: open,
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const documentData = document?.documentData
|
const envelopeItems = envelopeItemsPayload?.data || [];
|
||||||
? {
|
|
||||||
...document.documentData,
|
|
||||||
data: document.documentData.initialData,
|
|
||||||
}
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const documentsPath = formatDocumentsPath(team.url);
|
const documentsPath = formatDocumentsPath(team.url);
|
||||||
|
|
||||||
const { mutateAsync: duplicateDocument, isPending: isDuplicateLoading } =
|
const { mutateAsync: duplicateEnvelope, isPending: isDuplicating } =
|
||||||
trpcReact.document.duplicate.useMutation({
|
trpcReact.envelope.duplicate.useMutation({
|
||||||
onSuccess: async ({ id }) => {
|
onSuccess: async ({ id }) => {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Document Duplicated`),
|
title: _(msg`Document Duplicated`),
|
||||||
@ -71,7 +69,7 @@ export const DocumentDuplicateDialog = ({
|
|||||||
|
|
||||||
const onDuplicate = async () => {
|
const onDuplicate = async () => {
|
||||||
try {
|
try {
|
||||||
await duplicateDocument({ documentId: id });
|
await duplicateEnvelope({ envelopeId: id });
|
||||||
} catch {
|
} catch {
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Something went wrong`),
|
title: _(msg`Something went wrong`),
|
||||||
@ -83,14 +81,14 @@ export const DocumentDuplicateDialog = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={(value) => !isLoading && onOpenChange(value)}>
|
<Dialog open={open} onOpenChange={(value) => !isDuplicating && onOpenChange(value)}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
<Trans>Duplicate</Trans>
|
<Trans>Duplicate</Trans>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{!documentData || isLoading ? (
|
{isLoadingEnvelopeItems || !envelopeItems || envelopeItems.length === 0 ? (
|
||||||
<div className="mx-auto -mt-4 flex w-full max-w-screen-xl flex-col px-4 md:px-8">
|
<div className="mx-auto -mt-4 flex w-full max-w-screen-xl flex-col px-4 md:px-8">
|
||||||
<h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl">
|
<h1 className="mt-4 grow-0 truncate text-2xl font-semibold md:text-3xl">
|
||||||
<Trans>Loading Document...</Trans>
|
<Trans>Loading Document...</Trans>
|
||||||
@ -98,7 +96,12 @@ export const DocumentDuplicateDialog = ({
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="p-2 [&>div]:h-[50vh] [&>div]:overflow-y-scroll">
|
<div className="p-2 [&>div]:h-[50vh] [&>div]:overflow-y-scroll">
|
||||||
<PDFViewer key={document?.id} documentData={documentData} />
|
<PDFViewer
|
||||||
|
key={envelopeItems[0].id}
|
||||||
|
envelopeItem={envelopeItems[0]}
|
||||||
|
token={undefined}
|
||||||
|
version="original"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -115,8 +118,8 @@ export const DocumentDuplicateDialog = ({
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isDuplicateLoading || isLoading}
|
disabled={isDuplicating}
|
||||||
loading={isDuplicateLoading}
|
loading={isDuplicating}
|
||||||
onClick={onDuplicate}
|
onClick={onDuplicate}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -336,7 +336,7 @@ export const EnvelopeDistributeDialog = ({
|
|||||||
<Trans>Message</Trans>{' '}
|
<Trans>Message</Trans>{' '}
|
||||||
<span className="text-muted-foreground">(Optional)</span>
|
<span className="text-muted-foreground">(Optional)</span>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger type="button">
|
||||||
<InfoIcon className="mx-2 h-4 w-4" />
|
<InfoIcon className="mx-2 h-4 w-4" />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent className="text-muted-foreground p-4">
|
<TooltipContent className="text-muted-foreground p-4">
|
||||||
|
|||||||
@ -2,11 +2,10 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { type DocumentData, DocumentStatus, type EnvelopeItem } from '@prisma/client';
|
import { DocumentStatus, type EnvelopeItem } from '@prisma/client';
|
||||||
import { DownloadIcon, FileTextIcon } from 'lucide-react';
|
import { DownloadIcon, FileTextIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { downloadFile } from '@documenso/lib/client-only/download-file';
|
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
@ -20,9 +19,7 @@ import {
|
|||||||
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
import { Skeleton } from '@documenso/ui/primitives/skeleton';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
type EnvelopeItemToDownload = Pick<EnvelopeItem, 'id' | 'title' | 'order'> & {
|
type EnvelopeItemToDownload = Pick<EnvelopeItem, 'id' | 'envelopeId' | 'title' | 'order'>;
|
||||||
documentData: DocumentData;
|
|
||||||
};
|
|
||||||
|
|
||||||
type EnvelopeDownloadDialogProps = {
|
type EnvelopeDownloadDialogProps = {
|
||||||
envelopeId: string;
|
envelopeId: string;
|
||||||
@ -64,12 +61,12 @@ export const EnvelopeDownloadDialog = ({
|
|||||||
access: token ? { type: 'recipient', token } : { type: 'user' },
|
access: token ? { type: 'recipient', token } : { type: 'user' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
initialData: initialEnvelopeItems ? { envelopeItems: initialEnvelopeItems } : undefined,
|
initialData: initialEnvelopeItems ? { data: initialEnvelopeItems } : undefined,
|
||||||
enabled: open,
|
enabled: open,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const envelopeItems = envelopeItemsPayload?.envelopeItems || [];
|
const envelopeItems = envelopeItemsPayload?.data || [];
|
||||||
|
|
||||||
const onDownload = async (
|
const onDownload = async (
|
||||||
envelopeItem: EnvelopeItemToDownload,
|
envelopeItem: EnvelopeItemToDownload,
|
||||||
@ -87,19 +84,11 @@ export const EnvelopeDownloadDialog = ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const downloadUrl = token
|
await downloadPDF({
|
||||||
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${envelopeItemId}/download/${version}`
|
envelopeItem,
|
||||||
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${envelopeItemId}/download/${version}`;
|
token,
|
||||||
|
fileName: envelopeItem.title,
|
||||||
const blob = await fetch(downloadUrl).then(async (res) => await res.blob());
|
version,
|
||||||
|
|
||||||
const baseTitle = envelopeItem.title.replace(/\.pdf$/, '');
|
|
||||||
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
|
|
||||||
const filename = `${baseTitle}${suffix}`;
|
|
||||||
|
|
||||||
downloadFile({
|
|
||||||
filename,
|
|
||||||
data: blob,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsDownloadingState((prev) => ({
|
setIsDownloadingState((prev) => ({
|
||||||
@ -140,7 +129,7 @@ export const EnvelopeDownloadDialog = ({
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{isLoadingEnvelopeItems ? (
|
{isLoadingEnvelopeItems ? (
|
||||||
<>
|
<>
|
||||||
{Array.from({ length: 2 }).map((_, index) => (
|
{Array.from({ length: 1 }).map((_, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className="border-border bg-card flex items-center gap-2 rounded-lg border p-4"
|
className="border-border bg-card flex items-center gap-2 rounded-lg border p-4"
|
||||||
@ -169,6 +158,7 @@ export const EnvelopeDownloadDialog = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
|
{/* Todo: Envelopes - Fix overflow */}
|
||||||
<h4 className="text-foreground truncate text-sm font-medium">{item.title}</h4>
|
<h4 className="text-foreground truncate text-sm font-medium">{item.title}</h4>
|
||||||
<p className="text-muted-foreground mt-0.5 text-xs">
|
<p className="text-muted-foreground mt-0.5 text-xs">
|
||||||
<Trans>PDF Document</Trans>
|
<Trans>PDF Document</Trans>
|
||||||
|
|||||||
@ -43,7 +43,7 @@ export const EnvelopeDuplicateDialog = ({
|
|||||||
|
|
||||||
const { mutateAsync: duplicateEnvelope, isPending: isDuplicating } =
|
const { mutateAsync: duplicateEnvelope, isPending: isDuplicating } =
|
||||||
trpc.envelope.duplicate.useMutation({
|
trpc.envelope.duplicate.useMutation({
|
||||||
onSuccess: async ({ duplicatedEnvelopeId }) => {
|
onSuccess: async ({ id }) => {
|
||||||
toast({
|
toast({
|
||||||
title: t`Envelope Duplicated`,
|
title: t`Envelope Duplicated`,
|
||||||
description: t`Your envelope has been successfully duplicated.`,
|
description: t`Your envelope has been successfully duplicated.`,
|
||||||
@ -55,7 +55,7 @@ export const EnvelopeDuplicateDialog = ({
|
|||||||
? formatDocumentsPath(team.url)
|
? formatDocumentsPath(team.url)
|
||||||
: formatTemplatesPath(team.url);
|
: formatTemplatesPath(team.url);
|
||||||
|
|
||||||
await navigate(`${path}/${duplicatedEnvelopeId}/edit`);
|
await navigate(`${path}/${id}/edit`);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -185,6 +185,10 @@ export const OrganisationMemberInviteDialog = ({
|
|||||||
return 'form';
|
return 'form';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fullOrganisation.members.length < fullOrganisation.organisationClaim.memberCount) {
|
||||||
|
return 'form';
|
||||||
|
}
|
||||||
|
|
||||||
// This is probably going to screw us over in the future.
|
// This is probably going to screw us over in the future.
|
||||||
if (fullOrganisation.organisationClaim.originalSubscriptionClaimId !== INTERNAL_CLAIM_ID.TEAM) {
|
if (fullOrganisation.organisationClaim.originalSubscriptionClaimId !== INTERNAL_CLAIM_ID.TEAM) {
|
||||||
return 'alert';
|
return 'alert';
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { createCallable } from 'react-call';
|
import { createCallable } from 'react-call';
|
||||||
@ -28,49 +27,71 @@ import {
|
|||||||
} from '@documenso/ui/primitives/form/form';
|
} from '@documenso/ui/primitives/form/form';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
|
||||||
const createNumberFieldSchema = (fieldMeta: TNumberFieldMeta) => {
|
|
||||||
let schema = z.coerce.number({
|
|
||||||
invalid_type_error: msg`Please enter a valid number`.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { numberFormat, minValue, maxValue } = fieldMeta;
|
|
||||||
|
|
||||||
if (typeof minValue === 'number') {
|
|
||||||
schema = schema.min(minValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof maxValue === 'number') {
|
|
||||||
schema = schema.max(maxValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (numberFormat) {
|
|
||||||
const foundRegex = numberFormatValues.find((item) => item.value === numberFormat)?.regex;
|
|
||||||
|
|
||||||
if (!foundRegex) {
|
|
||||||
return schema;
|
|
||||||
}
|
|
||||||
|
|
||||||
return schema.refine(
|
|
||||||
(value) => {
|
|
||||||
return foundRegex.test(value.toString());
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: msg`Number needs to be formatted as ${numberFormat}`.id,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return schema;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type SignFieldNumberDialogProps = {
|
export type SignFieldNumberDialogProps = {
|
||||||
fieldMeta: TNumberFieldMeta;
|
fieldMeta: TNumberFieldMeta;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SignFieldNumberDialog = createCallable<SignFieldNumberDialogProps, number | null>(
|
export const SignFieldNumberDialog = createCallable<SignFieldNumberDialogProps, string | null>(
|
||||||
({ call, fieldMeta }) => {
|
({ call, fieldMeta }) => {
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
// Needs to be inside dialog for translation purposes.
|
||||||
|
const createNumberFieldSchema = (fieldMeta: TNumberFieldMeta) => {
|
||||||
|
const { numberFormat, minValue, maxValue } = fieldMeta;
|
||||||
|
|
||||||
|
if (numberFormat) {
|
||||||
|
const foundRegex = numberFormatValues.find((item) => item.value === numberFormat)?.regex;
|
||||||
|
|
||||||
|
if (foundRegex) {
|
||||||
|
return z.string().refine(
|
||||||
|
(value) => {
|
||||||
|
return foundRegex.test(value.toString());
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: t`Number needs to be formatted as ${numberFormat}`,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not gong to work with min/max numbers + number format
|
||||||
|
// Since currently doesn't work in V1 going to ignore for now.
|
||||||
|
return z.string().superRefine((value, ctx) => {
|
||||||
|
const isValidNumber = /^[0-9,.]+$/.test(value.toString());
|
||||||
|
|
||||||
|
if (!isValidNumber) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: t`Please enter a valid number`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof minValue === 'number' && parseFloat(value) < minValue) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.too_small,
|
||||||
|
minimum: minValue,
|
||||||
|
inclusive: true,
|
||||||
|
type: 'number',
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof maxValue === 'number' && parseFloat(value) > maxValue) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.too_big,
|
||||||
|
maximum: maxValue,
|
||||||
|
inclusive: true,
|
||||||
|
type: 'number',
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const ZSignFieldNumberFormSchema = z.object({
|
const ZSignFieldNumberFormSchema = z.object({
|
||||||
number: createNumberFieldSchema(fieldMeta),
|
number: createNumberFieldSchema(fieldMeta),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -143,7 +143,7 @@ export function TemplateUseDialog({
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const envelopeItems = response?.envelopeItems ?? [];
|
const envelopeItems = response?.data ?? [];
|
||||||
|
|
||||||
const { mutateAsync: createDocumentFromTemplate } =
|
const { mutateAsync: createDocumentFromTemplate } =
|
||||||
trpc.template.createDocumentFromTemplate.useMutation();
|
trpc.template.createDocumentFromTemplate.useMutation();
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useLingui } from '@lingui/react';
|
|||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { DocumentData, FieldType } from '@prisma/client';
|
import type { DocumentData, FieldType } from '@prisma/client';
|
||||||
import { ReadStatus, type Recipient, SendStatus, SigningStatus } from '@prisma/client';
|
import { ReadStatus, type Recipient, SendStatus, SigningStatus } from '@prisma/client';
|
||||||
|
import { base64 } from '@scure/base';
|
||||||
import { ChevronsUpDown } from 'lucide-react';
|
import { ChevronsUpDown } from 'lucide-react';
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
@ -12,7 +13,6 @@ import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-c
|
|||||||
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
||||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { type TFieldMetaSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
import { type TFieldMetaSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||||
import { base64 } from '@documenso/lib/universal/base64';
|
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
import { ADVANCED_FIELD_TYPES_WITH_OPTIONAL_SETTING } from '@documenso/lib/utils/advanced-fields-helpers';
|
import { ADVANCED_FIELD_TYPES_WITH_OPTIONAL_SETTING } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||||
import { useRecipientColors } from '@documenso/ui/lib/recipient-colors';
|
import { useRecipientColors } from '@documenso/ui/lib/recipient-colors';
|
||||||
@ -83,21 +83,14 @@ export const ConfigureFieldsView = ({
|
|||||||
|
|
||||||
const normalizedDocumentData = useMemo(() => {
|
const normalizedDocumentData = useMemo(() => {
|
||||||
if (documentData) {
|
if (documentData) {
|
||||||
return documentData;
|
return documentData.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!configData.documentData) {
|
if (!configData.documentData) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = base64.encode(configData.documentData?.data);
|
return base64.encode(configData.documentData.data);
|
||||||
|
|
||||||
return {
|
|
||||||
id: 'preview',
|
|
||||||
type: 'BYTES_64',
|
|
||||||
data,
|
|
||||||
initialData: data,
|
|
||||||
} satisfies DocumentData;
|
|
||||||
}, [configData.documentData]);
|
}, [configData.documentData]);
|
||||||
|
|
||||||
const recipients = useMemo(() => {
|
const recipients = useMemo(() => {
|
||||||
@ -541,7 +534,15 @@ export const ConfigureFieldsView = ({
|
|||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
{normalizedDocumentData && (
|
{normalizedDocumentData && (
|
||||||
<div>
|
<div>
|
||||||
<PDFViewer documentData={normalizedDocumentData} />
|
<PDFViewer
|
||||||
|
overrideData={normalizedDocumentData}
|
||||||
|
envelopeItem={{
|
||||||
|
id: '',
|
||||||
|
envelopeId: '',
|
||||||
|
}}
|
||||||
|
token={undefined}
|
||||||
|
version="signed"
|
||||||
|
/>
|
||||||
|
|
||||||
<ElementVisible
|
<ElementVisible
|
||||||
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
|
target={`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${highestPageNumber}"]`}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ export type EmbedAuthenticationRequiredProps = {
|
|||||||
email?: string;
|
email?: string;
|
||||||
returnTo: string;
|
returnTo: string;
|
||||||
isGoogleSSOEnabled?: boolean;
|
isGoogleSSOEnabled?: boolean;
|
||||||
|
isMicrosoftSSOEnabled?: boolean;
|
||||||
isOIDCSSOEnabled?: boolean;
|
isOIDCSSOEnabled?: boolean;
|
||||||
oidcProviderLabel?: string;
|
oidcProviderLabel?: string;
|
||||||
};
|
};
|
||||||
@ -17,6 +18,7 @@ export const EmbedAuthenticationRequired = ({
|
|||||||
email,
|
email,
|
||||||
returnTo,
|
returnTo,
|
||||||
// isGoogleSSOEnabled,
|
// isGoogleSSOEnabled,
|
||||||
|
// isMicrosoftSSOEnabled,
|
||||||
// isOIDCSSOEnabled,
|
// isOIDCSSOEnabled,
|
||||||
// oidcProviderLabel,
|
// oidcProviderLabel,
|
||||||
}: EmbedAuthenticationRequiredProps) => {
|
}: EmbedAuthenticationRequiredProps) => {
|
||||||
@ -37,6 +39,7 @@ export const EmbedAuthenticationRequired = ({
|
|||||||
<SignInForm
|
<SignInForm
|
||||||
// Embed currently not supported.
|
// Embed currently not supported.
|
||||||
// isGoogleSSOEnabled={isGoogleSSOEnabled}
|
// isGoogleSSOEnabled={isGoogleSSOEnabled}
|
||||||
|
// isMicrosoftSSOEnabled={isMicrosoftSSOEnabled}
|
||||||
// isOIDCSSOEnabled={isOIDCSSOEnabled}
|
// isOIDCSSOEnabled={isOIDCSSOEnabled}
|
||||||
// oidcProviderLabel={oidcProviderLabel}
|
// oidcProviderLabel={oidcProviderLabel}
|
||||||
className="mt-4"
|
className="mt-4"
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import { useEffect, useLayoutEffect, useState } from 'react';
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { DocumentMeta, Recipient, Signature } from '@prisma/client';
|
import type { DocumentMeta, EnvelopeItem, Recipient, Signature } from '@prisma/client';
|
||||||
import { type DocumentData, type Field, FieldType } from '@prisma/client';
|
import { type Field, FieldType } from '@prisma/client';
|
||||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { useSearchParams } from 'react-router';
|
import { useSearchParams } from 'react-router';
|
||||||
@ -47,7 +47,7 @@ export type EmbedDirectTemplateClientPageProps = {
|
|||||||
token: string;
|
token: string;
|
||||||
envelopeId: string;
|
envelopeId: string;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
documentData: DocumentData;
|
envelopeItems: Pick<EnvelopeItem, 'id' | 'envelopeId'>[];
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
metadata?: DocumentMeta | null;
|
metadata?: DocumentMeta | null;
|
||||||
@ -59,7 +59,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
token,
|
token,
|
||||||
envelopeId,
|
envelopeId,
|
||||||
updatedAt,
|
updatedAt,
|
||||||
documentData,
|
envelopeItems,
|
||||||
recipient,
|
recipient,
|
||||||
fields,
|
fields,
|
||||||
metadata,
|
metadata,
|
||||||
@ -335,7 +335,9 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
{/* Viewer */}
|
{/* Viewer */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<PDFViewer
|
<PDFViewer
|
||||||
documentData={documentData}
|
envelopeItem={envelopeItems[0]}
|
||||||
|
token={recipient.token}
|
||||||
|
version="signed"
|
||||||
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -3,14 +3,8 @@ import { useEffect, useId, useLayoutEffect, useMemo, useState } from 'react';
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { DocumentMeta } from '@prisma/client';
|
import type { DocumentMeta, EnvelopeItem } from '@prisma/client';
|
||||||
import {
|
import { type Field, FieldType, RecipientRole, SigningStatus } from '@prisma/client';
|
||||||
type DocumentData,
|
|
||||||
type Field,
|
|
||||||
FieldType,
|
|
||||||
RecipientRole,
|
|
||||||
SigningStatus,
|
|
||||||
} from '@prisma/client';
|
|
||||||
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
|
||||||
|
|
||||||
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
import { useThrottleFn } from '@documenso/lib/client-only/hooks/use-throttle-fn';
|
||||||
@ -46,11 +40,11 @@ import { EmbedDocumentCompleted } from './embed-document-completed';
|
|||||||
import { EmbedDocumentFields } from './embed-document-fields';
|
import { EmbedDocumentFields } from './embed-document-fields';
|
||||||
import { EmbedDocumentRejected } from './embed-document-rejected';
|
import { EmbedDocumentRejected } from './embed-document-rejected';
|
||||||
|
|
||||||
export type EmbedSignDocumentClientPageProps = {
|
export type EmbedSignDocumentV1ClientPageProps = {
|
||||||
token: string;
|
token: string;
|
||||||
documentId: number;
|
documentId: number;
|
||||||
envelopeId: string;
|
envelopeId: string;
|
||||||
documentData: DocumentData;
|
envelopeItems: Pick<EnvelopeItem, 'id' | 'envelopeId'>[];
|
||||||
recipient: RecipientWithFields;
|
recipient: RecipientWithFields;
|
||||||
fields: Field[];
|
fields: Field[];
|
||||||
completedFields: DocumentField[];
|
completedFields: DocumentField[];
|
||||||
@ -61,11 +55,11 @@ export type EmbedSignDocumentClientPageProps = {
|
|||||||
allRecipients?: RecipientWithFields[];
|
allRecipients?: RecipientWithFields[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EmbedSignDocumentClientPage = ({
|
export const EmbedSignDocumentV1ClientPage = ({
|
||||||
token,
|
token,
|
||||||
documentId,
|
documentId,
|
||||||
envelopeId,
|
envelopeId,
|
||||||
documentData,
|
envelopeItems,
|
||||||
recipient,
|
recipient,
|
||||||
fields,
|
fields,
|
||||||
completedFields,
|
completedFields,
|
||||||
@ -74,7 +68,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
hidePoweredBy = false,
|
hidePoweredBy = false,
|
||||||
allowWhitelabelling = false,
|
allowWhitelabelling = false,
|
||||||
allRecipients = [],
|
allRecipients = [],
|
||||||
}: EmbedSignDocumentClientPageProps) => {
|
}: EmbedSignDocumentV1ClientPageProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@ -293,7 +287,9 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
{/* Viewer */}
|
{/* Viewer */}
|
||||||
<div className="embed--DocumentViewer flex-1">
|
<div className="embed--DocumentViewer flex-1">
|
||||||
<PDFViewer
|
<PDFViewer
|
||||||
documentData={documentData}
|
envelopeItem={envelopeItems[0]}
|
||||||
|
token={token}
|
||||||
|
version="signed"
|
||||||
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
onDocumentLoad={() => setHasDocumentLoaded(true)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -0,0 +1,232 @@
|
|||||||
|
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
|
||||||
|
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||||
|
|
||||||
|
import { ZSignDocumentEmbedDataSchema } from '~/types/embed-document-sign-schema';
|
||||||
|
import { injectCss } from '~/utils/css-vars';
|
||||||
|
|
||||||
|
import { DocumentSigningPageViewV2 } from '../general/document-signing/document-signing-page-view-v2';
|
||||||
|
import { useRequiredEnvelopeSigningContext } from '../general/document-signing/envelope-signing-provider';
|
||||||
|
import { EmbedClientLoading } from './embed-client-loading';
|
||||||
|
import { EmbedDocumentCompleted } from './embed-document-completed';
|
||||||
|
import { EmbedDocumentRejected } from './embed-document-rejected';
|
||||||
|
import { EmbedSigningProvider } from './embed-signing-context';
|
||||||
|
|
||||||
|
export type EmbedSignDocumentV2ClientPageProps = {
|
||||||
|
hidePoweredBy?: boolean;
|
||||||
|
allowWhitelabelling?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmbedSignDocumentV2ClientPage = ({
|
||||||
|
hidePoweredBy = false,
|
||||||
|
allowWhitelabelling = false,
|
||||||
|
}: EmbedSignDocumentV2ClientPageProps) => {
|
||||||
|
const { _ } = useLingui();
|
||||||
|
|
||||||
|
const { envelope, recipient, envelopeData, setFullName, fullName } =
|
||||||
|
useRequiredEnvelopeSigningContext();
|
||||||
|
|
||||||
|
const { isCompleted, isRejected, recipientSignature } = envelopeData;
|
||||||
|
|
||||||
|
// !: Not used at the moment, may be removed in the future.
|
||||||
|
// const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
||||||
|
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||||
|
const [allowDocumentRejection, setAllowDocumentRejection] = useState(false);
|
||||||
|
const [isNameLocked, setIsNameLocked] = useState(false);
|
||||||
|
|
||||||
|
const onDocumentCompleted = (data: {
|
||||||
|
token: string;
|
||||||
|
documentId: number;
|
||||||
|
envelopeId: string;
|
||||||
|
recipientId: number;
|
||||||
|
}) => {
|
||||||
|
if (window.parent) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: 'document-completed',
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDocumentError = () => {
|
||||||
|
if (window.parent) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: 'document-error',
|
||||||
|
data: null,
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDocumentReady = () => {
|
||||||
|
if (window.parent) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: 'document-ready',
|
||||||
|
data: null,
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFieldSigned = (data: { fieldId?: number; value?: string; isBase64?: boolean }) => {
|
||||||
|
if (window.parent) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: 'field-signed',
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFieldUnsigned = (data: { fieldId?: number }) => {
|
||||||
|
if (window.parent) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: 'field-unsigned',
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDocumentRejected = (data: {
|
||||||
|
token: string;
|
||||||
|
documentId: number;
|
||||||
|
envelopeId: string;
|
||||||
|
recipientId: number;
|
||||||
|
reason?: string;
|
||||||
|
}) => {
|
||||||
|
if (window.parent) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: 'document-rejected',
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const hash = window.location.hash.slice(1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = ZSignDocumentEmbedDataSchema.parse(JSON.parse(decodeURIComponent(atob(hash))));
|
||||||
|
|
||||||
|
if (!isCompleted && data.name) {
|
||||||
|
setFullName(data.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since a recipient can be provided a name we can lock it without requiring
|
||||||
|
// a to be provided by the parent application, unlike direct templates.
|
||||||
|
setIsNameLocked(!!data.lockName);
|
||||||
|
setAllowDocumentRejection(!!data.allowDocumentRejection);
|
||||||
|
|
||||||
|
if (data.darkModeDisabled) {
|
||||||
|
document.documentElement.classList.add('dark-mode-disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowWhitelabelling) {
|
||||||
|
injectCss({
|
||||||
|
css: data.css,
|
||||||
|
cssVars: data.cssVars,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasFinishedInit(true);
|
||||||
|
|
||||||
|
// !: While the setters are stable we still want to ensure we're avoiding
|
||||||
|
// !: re-renders.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [allowWhitelabelling]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasFinishedInit) {
|
||||||
|
onDocumentReady();
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [hasFinishedInit]);
|
||||||
|
|
||||||
|
// Listen for document completion events from the envelope signing context
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCompleted) {
|
||||||
|
onDocumentCompleted({
|
||||||
|
token: recipient.token,
|
||||||
|
envelopeId: envelope.id,
|
||||||
|
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
||||||
|
recipientId: recipient.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isCompleted, envelope.id, recipient.id, recipient.token]);
|
||||||
|
|
||||||
|
// Listen for document rejection events
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRejected) {
|
||||||
|
onDocumentRejected({
|
||||||
|
token: recipient.token,
|
||||||
|
envelopeId: envelope.id,
|
||||||
|
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
||||||
|
recipientId: recipient.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isRejected, envelope.id, recipient.id, recipient.token]);
|
||||||
|
|
||||||
|
if (isRejected) {
|
||||||
|
return <EmbedDocumentRejected />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCompleted) {
|
||||||
|
return (
|
||||||
|
<EmbedDocumentCompleted
|
||||||
|
name={fullName}
|
||||||
|
signature={
|
||||||
|
recipientSignature
|
||||||
|
? {
|
||||||
|
id: 1,
|
||||||
|
fieldId: 1,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
created: new Date(),
|
||||||
|
signatureImageAsBase64: recipientSignature.signatureImageAsBase64,
|
||||||
|
typedSignature: recipientSignature.typedSignature,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EmbedSigningProvider
|
||||||
|
isNameLocked={isNameLocked}
|
||||||
|
hidePoweredBy={hidePoweredBy}
|
||||||
|
allowDocumentRejection={allowDocumentRejection}
|
||||||
|
onDocumentCompleted={onDocumentCompleted}
|
||||||
|
onDocumentError={onDocumentError}
|
||||||
|
onDocumentRejected={onDocumentRejected}
|
||||||
|
onDocumentReady={onDocumentReady}
|
||||||
|
onFieldSigned={onFieldSigned}
|
||||||
|
onFieldUnsigned={onFieldUnsigned}
|
||||||
|
>
|
||||||
|
<div className="embed--Root relative">
|
||||||
|
{!hasFinishedInit && <EmbedClientLoading />}
|
||||||
|
|
||||||
|
<DocumentSigningPageViewV2 />
|
||||||
|
</div>
|
||||||
|
</EmbedSigningProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
101
apps/remix/app/components/embed/embed-signing-context.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
export type EmbedSigningContextValue = {
|
||||||
|
isEmbed: true;
|
||||||
|
allowDocumentRejection: boolean;
|
||||||
|
isNameLocked: boolean;
|
||||||
|
isEmailLocked: boolean;
|
||||||
|
hidePoweredBy: boolean;
|
||||||
|
onDocumentCompleted: (data: {
|
||||||
|
token: string;
|
||||||
|
documentId: number;
|
||||||
|
envelopeId: string;
|
||||||
|
recipientId: number;
|
||||||
|
}) => void;
|
||||||
|
onDocumentError: () => void;
|
||||||
|
onDocumentRejected: (data: {
|
||||||
|
token: string;
|
||||||
|
documentId: number;
|
||||||
|
envelopeId: string;
|
||||||
|
recipientId: number;
|
||||||
|
reason?: string;
|
||||||
|
}) => void;
|
||||||
|
onDocumentReady: () => void;
|
||||||
|
onFieldSigned: (data: { fieldId?: number; value?: string; isBase64?: boolean }) => void;
|
||||||
|
onFieldUnsigned: (data: { fieldId?: number }) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmbedSigningContext = createContext<EmbedSigningContextValue | null>(null);
|
||||||
|
|
||||||
|
export const useEmbedSigningContext = () => {
|
||||||
|
return useContext(EmbedSigningContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useRequiredEmbedSigningContext = () => {
|
||||||
|
const context = useEmbedSigningContext();
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useRequiredEmbedSigningContext must be used within EmbedSigningProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EmbedSigningProviderProps = {
|
||||||
|
allowDocumentRejection?: boolean;
|
||||||
|
isNameLocked?: boolean;
|
||||||
|
isEmailLocked?: boolean;
|
||||||
|
hidePoweredBy?: boolean;
|
||||||
|
onDocumentCompleted: (data: {
|
||||||
|
token: string;
|
||||||
|
documentId: number;
|
||||||
|
envelopeId: string;
|
||||||
|
recipientId: number;
|
||||||
|
}) => void;
|
||||||
|
onDocumentError: () => void;
|
||||||
|
onDocumentRejected: (data: {
|
||||||
|
token: string;
|
||||||
|
documentId: number;
|
||||||
|
envelopeId: string;
|
||||||
|
recipientId: number;
|
||||||
|
reason?: string;
|
||||||
|
}) => void;
|
||||||
|
onDocumentReady: () => void;
|
||||||
|
onFieldSigned: (data: { fieldId?: number; value?: string; isBase64?: boolean }) => void;
|
||||||
|
onFieldUnsigned: (data: { fieldId?: number }) => void;
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EmbedSigningProvider = ({
|
||||||
|
allowDocumentRejection = false,
|
||||||
|
isNameLocked = false,
|
||||||
|
isEmailLocked = true,
|
||||||
|
hidePoweredBy = false,
|
||||||
|
onDocumentCompleted,
|
||||||
|
onDocumentError,
|
||||||
|
onDocumentRejected,
|
||||||
|
onDocumentReady,
|
||||||
|
onFieldSigned,
|
||||||
|
onFieldUnsigned,
|
||||||
|
children,
|
||||||
|
}: EmbedSigningProviderProps) => {
|
||||||
|
return (
|
||||||
|
<EmbedSigningContext.Provider
|
||||||
|
value={{
|
||||||
|
isEmbed: true,
|
||||||
|
allowDocumentRejection,
|
||||||
|
isNameLocked,
|
||||||
|
isEmailLocked,
|
||||||
|
hidePoweredBy,
|
||||||
|
onDocumentCompleted,
|
||||||
|
onDocumentError,
|
||||||
|
onDocumentRejected,
|
||||||
|
onDocumentReady,
|
||||||
|
onFieldSigned,
|
||||||
|
onFieldUnsigned,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</EmbedSigningContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -226,7 +226,9 @@ export const MultiSignDocumentSigningView = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<PDFViewer
|
<PDFViewer
|
||||||
documentData={document.documentData}
|
envelopeItem={document.envelopeItems[0]}
|
||||||
|
token={token}
|
||||||
|
version="signed"
|
||||||
onDocumentLoad={() => {
|
onDocumentLoad={() => {
|
||||||
setHasDocumentLoaded(true);
|
setHasDocumentLoaded(true);
|
||||||
onDocumentReady?.();
|
onDocumentReady?.();
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import type { z } from 'zod';
|
|||||||
import {
|
import {
|
||||||
DEFAULT_FIELD_FONT_SIZE,
|
DEFAULT_FIELD_FONT_SIZE,
|
||||||
type TDateFieldMeta as DateFieldMeta,
|
type TDateFieldMeta as DateFieldMeta,
|
||||||
|
FIELD_DEFAULT_GENERIC_ALIGN,
|
||||||
ZDateFieldMeta,
|
ZDateFieldMeta,
|
||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
import { Form } from '@documenso/ui/primitives/form/form';
|
import { Form } from '@documenso/ui/primitives/form/form';
|
||||||
@ -39,7 +40,7 @@ export const EditorFieldDateForm = ({
|
|||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
||||||
textAlign: value.textAlign || 'left',
|
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import type { z } from 'zod';
|
|||||||
import {
|
import {
|
||||||
DEFAULT_FIELD_FONT_SIZE,
|
DEFAULT_FIELD_FONT_SIZE,
|
||||||
type TEmailFieldMeta as EmailFieldMeta,
|
type TEmailFieldMeta as EmailFieldMeta,
|
||||||
|
FIELD_DEFAULT_GENERIC_ALIGN,
|
||||||
ZEmailFieldMeta,
|
ZEmailFieldMeta,
|
||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
import { Form } from '@documenso/ui/primitives/form/form';
|
import { Form } from '@documenso/ui/primitives/form/form';
|
||||||
@ -39,7 +40,7 @@ export const EditorFieldEmailForm = ({
|
|||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
||||||
textAlign: value.textAlign || 'left',
|
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,10 @@ import { useEffect } from 'react';
|
|||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import { type Control, useFormContext } from 'react-hook-form';
|
import { type Control, useFormContext } from 'react-hook-form';
|
||||||
|
|
||||||
|
import { FIELD_MIN_LINE_HEIGHT } from '@documenso/lib/types/field-meta';
|
||||||
|
import { FIELD_MAX_LINE_HEIGHT } from '@documenso/lib/types/field-meta';
|
||||||
|
import { FIELD_MIN_LETTER_SPACING } from '@documenso/lib/types/field-meta';
|
||||||
|
import { FIELD_MAX_LETTER_SPACING } from '@documenso/lib/types/field-meta';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
import { Checkbox } from '@documenso/ui/primitives/checkbox';
|
||||||
import {
|
import {
|
||||||
@ -107,6 +111,119 @@ export const EditorGenericTextAlignField = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const EditorGenericVerticalAlignField = ({
|
||||||
|
formControl,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
formControl: FormControlType;
|
||||||
|
className?: string;
|
||||||
|
}) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
control={formControl}
|
||||||
|
name="verticalAlign"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className={className}>
|
||||||
|
<FormLabel>
|
||||||
|
<Trans>Vertical Align</Trans>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select {...field} onValueChange={field.onChange}>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={t`Select vertical align`} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="top">
|
||||||
|
<Trans>Top</Trans>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="middle">
|
||||||
|
<Trans>Middle</Trans>
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="bottom">
|
||||||
|
<Trans>Bottom</Trans>
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditorGenericLineHeightField = ({
|
||||||
|
formControl,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
formControl: FormControlType;
|
||||||
|
className?: string;
|
||||||
|
}) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
control={formControl}
|
||||||
|
name="lineHeight"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className={className}>
|
||||||
|
<FormLabel>
|
||||||
|
<Trans>Line Height</Trans>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={FIELD_MIN_LINE_HEIGHT}
|
||||||
|
max={FIELD_MAX_LINE_HEIGHT}
|
||||||
|
className="bg-background"
|
||||||
|
placeholder={t`Line height`}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EditorGenericLetterSpacingField = ({
|
||||||
|
formControl,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
formControl: FormControlType;
|
||||||
|
className?: string;
|
||||||
|
}) => {
|
||||||
|
const { t } = useLingui();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
control={formControl}
|
||||||
|
name="letterSpacing"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className={className}>
|
||||||
|
<FormLabel>
|
||||||
|
<Trans>Letter Spacing</Trans>
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={FIELD_MIN_LETTER_SPACING}
|
||||||
|
max={FIELD_MAX_LETTER_SPACING}
|
||||||
|
className="bg-background"
|
||||||
|
placeholder={t`Letter spacing`}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const EditorGenericRequiredField = ({
|
export const EditorGenericRequiredField = ({
|
||||||
formControl,
|
formControl,
|
||||||
className,
|
className,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import type { z } from 'zod';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_FIELD_FONT_SIZE,
|
DEFAULT_FIELD_FONT_SIZE,
|
||||||
|
FIELD_DEFAULT_GENERIC_ALIGN,
|
||||||
type TInitialsFieldMeta as InitialsFieldMeta,
|
type TInitialsFieldMeta as InitialsFieldMeta,
|
||||||
ZInitialsFieldMeta,
|
ZInitialsFieldMeta,
|
||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
@ -39,7 +40,7 @@ export const EditorFieldInitialsForm = ({
|
|||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
||||||
textAlign: value.textAlign || 'left',
|
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import type { z } from 'zod';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_FIELD_FONT_SIZE,
|
DEFAULT_FIELD_FONT_SIZE,
|
||||||
|
FIELD_DEFAULT_GENERIC_ALIGN,
|
||||||
type TNameFieldMeta as NameFieldMeta,
|
type TNameFieldMeta as NameFieldMeta,
|
||||||
ZNameFieldMeta,
|
ZNameFieldMeta,
|
||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
@ -39,7 +40,7 @@ export const EditorFieldNameForm = ({
|
|||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
||||||
textAlign: value.textAlign || 'left',
|
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,11 @@ import { useForm, useWatch } from 'react-hook-form';
|
|||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_FIELD_FONT_SIZE,
|
||||||
|
FIELD_DEFAULT_GENERIC_ALIGN,
|
||||||
|
FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN,
|
||||||
|
FIELD_DEFAULT_LETTER_SPACING,
|
||||||
|
FIELD_DEFAULT_LINE_HEIGHT,
|
||||||
type TNumberFieldMeta as NumberFieldMeta,
|
type TNumberFieldMeta as NumberFieldMeta,
|
||||||
ZNumberFieldMeta,
|
ZNumberFieldMeta,
|
||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
@ -31,9 +36,12 @@ import { Separator } from '@documenso/ui/primitives/separator';
|
|||||||
import {
|
import {
|
||||||
EditorGenericFontSizeField,
|
EditorGenericFontSizeField,
|
||||||
EditorGenericLabelField,
|
EditorGenericLabelField,
|
||||||
|
EditorGenericLetterSpacingField,
|
||||||
|
EditorGenericLineHeightField,
|
||||||
EditorGenericReadOnlyField,
|
EditorGenericReadOnlyField,
|
||||||
EditorGenericRequiredField,
|
EditorGenericRequiredField,
|
||||||
EditorGenericTextAlignField,
|
EditorGenericTextAlignField,
|
||||||
|
EditorGenericVerticalAlignField,
|
||||||
} from './editor-field-generic-field-forms';
|
} from './editor-field-generic-field-forms';
|
||||||
|
|
||||||
const ZNumberFieldFormSchema = ZNumberFieldMeta.pick({
|
const ZNumberFieldFormSchema = ZNumberFieldMeta.pick({
|
||||||
@ -43,6 +51,9 @@ const ZNumberFieldFormSchema = ZNumberFieldMeta.pick({
|
|||||||
numberFormat: true,
|
numberFormat: true,
|
||||||
fontSize: true,
|
fontSize: true,
|
||||||
textAlign: true,
|
textAlign: true,
|
||||||
|
lineHeight: true,
|
||||||
|
letterSpacing: true,
|
||||||
|
verticalAlign: true,
|
||||||
required: true,
|
required: true,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
minValue: true,
|
minValue: true,
|
||||||
@ -99,8 +110,11 @@ export const EditorFieldNumberForm = ({
|
|||||||
placeholder: value.placeholder || '',
|
placeholder: value.placeholder || '',
|
||||||
value: value.value || '',
|
value: value.value || '',
|
||||||
numberFormat: value.numberFormat || null,
|
numberFormat: value.numberFormat || null,
|
||||||
fontSize: value.fontSize || 14,
|
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
||||||
textAlign: value.textAlign || 'left',
|
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN,
|
||||||
|
lineHeight: value.lineHeight ?? FIELD_DEFAULT_LINE_HEIGHT,
|
||||||
|
letterSpacing: value.letterSpacing ?? FIELD_DEFAULT_LETTER_SPACING,
|
||||||
|
verticalAlign: value.verticalAlign ?? FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN,
|
||||||
required: value.required || false,
|
required: value.required || false,
|
||||||
readOnly: value.readOnly || false,
|
readOnly: value.readOnly || false,
|
||||||
minValue: value.minValue,
|
minValue: value.minValue,
|
||||||
@ -118,6 +132,10 @@ export const EditorFieldNumberForm = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const validatedFormValues = ZNumberFieldFormSchema.safeParse(formValues);
|
const validatedFormValues = ZNumberFieldFormSchema.safeParse(formValues);
|
||||||
|
|
||||||
|
if (formValues.readOnly && !formValues.value) {
|
||||||
|
void form.trigger('value');
|
||||||
|
}
|
||||||
|
|
||||||
if (validatedFormValues.success) {
|
if (validatedFormValues.success) {
|
||||||
onValueChange({
|
onValueChange({
|
||||||
type: 'number',
|
type: 'number',
|
||||||
@ -130,10 +148,12 @@ export const EditorFieldNumberForm = ({
|
|||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form>
|
<form>
|
||||||
<fieldset className="flex flex-col gap-2">
|
<fieldset className="flex flex-col gap-2">
|
||||||
<div className="flex w-full flex-row gap-x-4">
|
<EditorGenericFontSizeField className="w-full" formControl={form.control} />
|
||||||
<EditorGenericFontSizeField className="w-full" formControl={form.control} />
|
|
||||||
|
|
||||||
|
<div className="flex w-full flex-row gap-x-4">
|
||||||
<EditorGenericTextAlignField className="w-full" formControl={form.control} />
|
<EditorGenericTextAlignField className="w-full" formControl={form.control} />
|
||||||
|
|
||||||
|
<EditorGenericVerticalAlignField className="w-full" formControl={form.control} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EditorGenericLabelField formControl={form.control} />
|
<EditorGenericLabelField formControl={form.control} />
|
||||||
@ -204,6 +224,12 @@ export const EditorFieldNumberForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="flex w-full flex-row gap-x-4">
|
||||||
|
<EditorGenericLineHeightField className="w-full" formControl={form.control} />
|
||||||
|
|
||||||
|
<EditorGenericLetterSpacingField className="w-full" formControl={form.control} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<EditorGenericRequiredField formControl={form.control} />
|
<EditorGenericRequiredField formControl={form.control} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,11 +5,8 @@ import { Trans } from '@lingui/react/macro';
|
|||||||
import { useForm, useWatch } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import { DEFAULT_SIGNATURE_TEXT_FONT_SIZE } from '@documenso/lib/constants/pdf';
|
||||||
DEFAULT_FIELD_FONT_SIZE,
|
import { type TSignatureFieldMeta, ZSignatureFieldMeta } from '@documenso/lib/types/field-meta';
|
||||||
type TSignatureFieldMeta,
|
|
||||||
ZSignatureFieldMeta,
|
|
||||||
} from '@documenso/lib/types/field-meta';
|
|
||||||
import { Form } from '@documenso/ui/primitives/form/form';
|
import { Form } from '@documenso/ui/primitives/form/form';
|
||||||
|
|
||||||
import { EditorGenericFontSizeField } from './editor-field-generic-field-forms';
|
import { EditorGenericFontSizeField } from './editor-field-generic-field-forms';
|
||||||
@ -35,7 +32,7 @@ export const EditorFieldSignatureForm = ({
|
|||||||
resolver: zodResolver(ZSignatureFieldFormSchema),
|
resolver: zodResolver(ZSignatureFieldFormSchema),
|
||||||
mode: 'onChange',
|
mode: 'onChange',
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
fontSize: value.fontSize || DEFAULT_SIGNATURE_TEXT_FONT_SIZE,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -3,11 +3,16 @@ import { useEffect } from 'react';
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import { useForm, useWatch } from 'react-hook-form';
|
import { useForm, useWatch } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_FIELD_FONT_SIZE,
|
DEFAULT_FIELD_FONT_SIZE,
|
||||||
|
FIELD_DEFAULT_GENERIC_ALIGN,
|
||||||
|
FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN,
|
||||||
|
FIELD_DEFAULT_LETTER_SPACING,
|
||||||
|
FIELD_DEFAULT_LINE_HEIGHT,
|
||||||
type TTextFieldMeta as TextFieldMeta,
|
type TTextFieldMeta as TextFieldMeta,
|
||||||
|
ZTextFieldMeta,
|
||||||
} from '@documenso/lib/types/field-meta';
|
} from '@documenso/lib/types/field-meta';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@ -22,32 +27,36 @@ import { Textarea } from '@documenso/ui/primitives/textarea';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
EditorGenericFontSizeField,
|
EditorGenericFontSizeField,
|
||||||
|
EditorGenericLetterSpacingField,
|
||||||
|
EditorGenericLineHeightField,
|
||||||
EditorGenericReadOnlyField,
|
EditorGenericReadOnlyField,
|
||||||
EditorGenericRequiredField,
|
EditorGenericRequiredField,
|
||||||
EditorGenericTextAlignField,
|
EditorGenericTextAlignField,
|
||||||
|
EditorGenericVerticalAlignField,
|
||||||
} from './editor-field-generic-field-forms';
|
} from './editor-field-generic-field-forms';
|
||||||
|
|
||||||
const ZTextFieldFormSchema = z
|
const ZTextFieldFormSchema = ZTextFieldMeta.pick({
|
||||||
.object({
|
label: true,
|
||||||
label: z.string().optional(),
|
placeholder: true,
|
||||||
placeholder: z.string().optional(),
|
text: true,
|
||||||
text: z.string().optional(),
|
characterLimit: true,
|
||||||
characterLimit: z.coerce.number().min(0).optional(),
|
fontSize: true,
|
||||||
fontSize: z.coerce.number().min(8).max(96).optional(),
|
textAlign: true,
|
||||||
textAlign: z.enum(['left', 'center', 'right']).optional(),
|
lineHeight: true,
|
||||||
required: z.boolean().optional(),
|
letterSpacing: true,
|
||||||
readOnly: z.boolean().optional(),
|
verticalAlign: true,
|
||||||
})
|
required: true,
|
||||||
.refine(
|
readOnly: true,
|
||||||
(data) => {
|
}).refine(
|
||||||
// A read-only field must have text
|
(data) => {
|
||||||
return !data.readOnly || (data.text && data.text.length > 0);
|
// A read-only field must have text
|
||||||
},
|
return !data.readOnly || (data.text && data.text.length > 0);
|
||||||
{
|
},
|
||||||
message: 'A read-only field must have text',
|
{
|
||||||
path: ['text'],
|
message: 'A read-only field must have text',
|
||||||
},
|
path: ['text'],
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
|
||||||
type TTextFieldFormSchema = z.infer<typeof ZTextFieldFormSchema>;
|
type TTextFieldFormSchema = z.infer<typeof ZTextFieldFormSchema>;
|
||||||
|
|
||||||
@ -73,7 +82,10 @@ export const EditorFieldTextForm = ({
|
|||||||
text: value.text || '',
|
text: value.text || '',
|
||||||
characterLimit: value.characterLimit || 0,
|
characterLimit: value.characterLimit || 0,
|
||||||
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
fontSize: value.fontSize || DEFAULT_FIELD_FONT_SIZE,
|
||||||
textAlign: value.textAlign || 'left',
|
textAlign: value.textAlign ?? FIELD_DEFAULT_GENERIC_ALIGN,
|
||||||
|
lineHeight: value.lineHeight ?? FIELD_DEFAULT_LINE_HEIGHT,
|
||||||
|
letterSpacing: value.letterSpacing ?? FIELD_DEFAULT_LETTER_SPACING,
|
||||||
|
verticalAlign: value.verticalAlign ?? FIELD_DEFAULT_GENERIC_VERTICAL_ALIGN,
|
||||||
required: value.required || false,
|
required: value.required || false,
|
||||||
readOnly: value.readOnly || false,
|
readOnly: value.readOnly || false,
|
||||||
},
|
},
|
||||||
@ -89,6 +101,10 @@ export const EditorFieldTextForm = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const validatedFormValues = ZTextFieldFormSchema.safeParse(formValues);
|
const validatedFormValues = ZTextFieldFormSchema.safeParse(formValues);
|
||||||
|
|
||||||
|
if (formValues.readOnly && !formValues.text) {
|
||||||
|
void form.trigger('text');
|
||||||
|
}
|
||||||
|
|
||||||
if (validatedFormValues.success) {
|
if (validatedFormValues.success) {
|
||||||
onValueChange({
|
onValueChange({
|
||||||
type: 'text',
|
type: 'text',
|
||||||
@ -101,10 +117,12 @@ export const EditorFieldTextForm = ({
|
|||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form>
|
<form>
|
||||||
<fieldset className="flex flex-col gap-2">
|
<fieldset className="flex flex-col gap-2">
|
||||||
<div className="flex w-full flex-row gap-x-4">
|
<EditorGenericFontSizeField className="w-full" formControl={form.control} />
|
||||||
<EditorGenericFontSizeField className="w-full" formControl={form.control} />
|
|
||||||
|
|
||||||
|
<div className="flex w-full flex-row gap-x-4">
|
||||||
<EditorGenericTextAlignField className="w-full" formControl={form.control} />
|
<EditorGenericTextAlignField className="w-full" formControl={form.control} />
|
||||||
|
|
||||||
|
<EditorGenericVerticalAlignField className="w-full" formControl={form.control} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
@ -182,17 +200,16 @@ export const EditorFieldTextForm = ({
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
className="bg-background"
|
className="bg-background"
|
||||||
placeholder={t`Field character limit`}
|
placeholder={t`Character limit`}
|
||||||
{...field}
|
{...field}
|
||||||
|
value={field.value || ''}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
field.onChange(e);
|
|
||||||
|
|
||||||
const values = form.getValues();
|
const values = form.getValues();
|
||||||
const characterLimit = parseInt(e.target.value, 10) || 0;
|
const characterLimit = parseInt(e.target.value, 10) || 0;
|
||||||
|
|
||||||
|
field.onChange(characterLimit || '');
|
||||||
|
|
||||||
const textValue = values.text || '';
|
const textValue = values.text || '';
|
||||||
|
|
||||||
if (characterLimit > 0 && textValue.length > characterLimit) {
|
if (characterLimit > 0 && textValue.length > characterLimit) {
|
||||||
@ -206,6 +223,12 @@ export const EditorFieldTextForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className="flex w-full flex-row gap-x-4">
|
||||||
|
<EditorGenericLineHeightField className="w-full" formControl={form.control} />
|
||||||
|
|
||||||
|
<EditorGenericLetterSpacingField className="w-full" formControl={form.control} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<EditorGenericRequiredField formControl={form.control} />
|
<EditorGenericRequiredField formControl={form.control} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -92,6 +92,7 @@ export const SignInForm = ({
|
|||||||
|
|
||||||
const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] =
|
const [isTwoFactorAuthenticationDialogOpen, setIsTwoFactorAuthenticationDialogOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [isEmbeddedRedirect, setIsEmbeddedRedirect] = useState(false);
|
||||||
|
|
||||||
const [twoFactorAuthenticationMethod, setTwoFactorAuthenticationMethod] = useState<
|
const [twoFactorAuthenticationMethod, setTwoFactorAuthenticationMethod] = useState<
|
||||||
'totp' | 'backup'
|
'totp' | 'backup'
|
||||||
@ -317,6 +318,8 @@ export const SignInForm = ({
|
|||||||
if (email) {
|
if (email) {
|
||||||
form.setValue('email', email);
|
form.setValue('email', email);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsEmbeddedRedirect(params.get('embedded') === 'true');
|
||||||
}, [form]);
|
}, [form]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -383,56 +386,64 @@ export const SignInForm = ({
|
|||||||
{isSubmitting ? <Trans>Signing in...</Trans> : <Trans>Sign In</Trans>}
|
{isSubmitting ? <Trans>Signing in...</Trans> : <Trans>Sign In</Trans>}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{hasSocialAuthEnabled && (
|
{!isEmbeddedRedirect && (
|
||||||
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
<>
|
||||||
<div className="bg-border h-px flex-1" />
|
{hasSocialAuthEnabled && (
|
||||||
<span className="text-muted-foreground bg-transparent">
|
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
|
||||||
<Trans>Or continue with</Trans>
|
<div className="bg-border h-px flex-1" />
|
||||||
</span>
|
<span className="text-muted-foreground bg-transparent">
|
||||||
<div className="bg-border h-px flex-1" />
|
<Trans>Or continue with</Trans>
|
||||||
</div>
|
</span>
|
||||||
)}
|
<div className="bg-border h-px flex-1" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isGoogleSSOEnabled && (
|
{isGoogleSSOEnabled && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="bg-background text-muted-foreground border"
|
className="bg-background text-muted-foreground border"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onClick={onSignInWithGoogleClick}
|
onClick={onSignInWithGoogleClick}
|
||||||
>
|
>
|
||||||
<FcGoogle className="mr-2 h-5 w-5" />
|
<FcGoogle className="mr-2 h-5 w-5" />
|
||||||
Google
|
Google
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isMicrosoftSSOEnabled && (
|
{isMicrosoftSSOEnabled && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="bg-background text-muted-foreground border"
|
className="bg-background text-muted-foreground border"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onClick={onSignInWithMicrosoftClick}
|
onClick={onSignInWithMicrosoftClick}
|
||||||
>
|
>
|
||||||
<img className="mr-2 h-4 w-4" alt="Microsoft Logo" src={'/static/microsoft.svg'} />
|
<img
|
||||||
Microsoft
|
className="mr-2 h-4 w-4"
|
||||||
</Button>
|
alt="Microsoft Logo"
|
||||||
)}
|
src={'/static/microsoft.svg'}
|
||||||
|
/>
|
||||||
|
Microsoft
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{isOIDCSSOEnabled && (
|
{isOIDCSSOEnabled && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="bg-background text-muted-foreground border"
|
className="bg-background text-muted-foreground border"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
onClick={onSignInWithOIDCClick}
|
onClick={onSignInWithOIDCClick}
|
||||||
>
|
>
|
||||||
<FaIdCardClip className="mr-2 h-5 w-5" />
|
<FaIdCardClip className="mr-2 h-5 w-5" />
|
||||||
{oidcProviderLabel || 'OIDC'}
|
{oidcProviderLabel || 'OIDC'}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -68,6 +68,7 @@ export type SignUpFormProps = {
|
|||||||
isGoogleSSOEnabled?: boolean;
|
isGoogleSSOEnabled?: boolean;
|
||||||
isMicrosoftSSOEnabled?: boolean;
|
isMicrosoftSSOEnabled?: boolean;
|
||||||
isOIDCSSOEnabled?: boolean;
|
isOIDCSSOEnabled?: boolean;
|
||||||
|
returnTo?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SignUpForm = ({
|
export const SignUpForm = ({
|
||||||
@ -76,6 +77,7 @@ export const SignUpForm = ({
|
|||||||
isGoogleSSOEnabled,
|
isGoogleSSOEnabled,
|
||||||
isMicrosoftSSOEnabled,
|
isMicrosoftSSOEnabled,
|
||||||
isOIDCSSOEnabled,
|
isOIDCSSOEnabled,
|
||||||
|
returnTo,
|
||||||
}: SignUpFormProps) => {
|
}: SignUpFormProps) => {
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@ -110,7 +112,7 @@ export const SignUpForm = ({
|
|||||||
signature,
|
signature,
|
||||||
});
|
});
|
||||||
|
|
||||||
await navigate(`/unverified-account`);
|
await navigate(returnTo ? returnTo : '/unverified-account');
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: _(msg`Registration Successful`),
|
title: _(msg`Registration Successful`),
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
|
|||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { Theme, useTheme } from 'remix-themes';
|
import { Theme, useTheme } from 'remix-themes';
|
||||||
|
|
||||||
|
import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value';
|
||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
|
||||||
import {
|
import {
|
||||||
@ -63,10 +64,12 @@ export function AppCommandMenu({ open, onOpenChange }: AppCommandMenuProps) {
|
|||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [pages, setPages] = useState<string[]>([]);
|
const [pages, setPages] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const debouncedSearch = useDebouncedValue(search, 200);
|
||||||
|
|
||||||
const { data: searchDocumentsData, isPending: isSearchingDocuments } =
|
const { data: searchDocumentsData, isPending: isSearchingDocuments } =
|
||||||
trpcReact.document.search.useQuery(
|
trpcReact.document.search.useQuery(
|
||||||
{
|
{
|
||||||
query: search,
|
query: debouncedSearch,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
placeholderData: (previousData) => previousData,
|
placeholderData: (previousData) => previousData,
|
||||||
@ -232,6 +235,7 @@ export function AppCommandMenu({ open, onOpenChange }: AppCommandMenuProps) {
|
|||||||
<Trans>No results found.</Trans>
|
<Trans>No results found.</Trans>
|
||||||
</CommandEmpty>
|
</CommandEmpty>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!currentPage && (
|
{!currentPage && (
|
||||||
<>
|
<>
|
||||||
{documentPageLinks.length > 0 && (
|
{documentPageLinks.length > 0 && (
|
||||||
@ -239,14 +243,17 @@ export function AppCommandMenu({ open, onOpenChange }: AppCommandMenuProps) {
|
|||||||
<Commands push={push} pages={documentPageLinks} />
|
<Commands push={push} pages={documentPageLinks} />
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{templatePageLinks.length > 0 && (
|
{templatePageLinks.length > 0 && (
|
||||||
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Templates`)}>
|
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Templates`)}>
|
||||||
<Commands push={push} pages={templatePageLinks} />
|
<Commands push={push} pages={templatePageLinks} />
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Settings`)}>
|
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Settings`)}>
|
||||||
<Commands push={push} pages={SETTINGS_PAGES} />
|
<Commands push={push} pages={SETTINGS_PAGES} />
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
|
||||||
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Preferences`)}>
|
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Preferences`)}>
|
||||||
<CommandItem className="-mx-2 -my-1 rounded-lg" onSelect={() => addPage('language')}>
|
<CommandItem className="-mx-2 -my-1 rounded-lg" onSelect={() => addPage('language')}>
|
||||||
Change language
|
Change language
|
||||||
@ -255,6 +262,7 @@ export function AppCommandMenu({ open, onOpenChange }: AppCommandMenuProps) {
|
|||||||
Change theme
|
Change theme
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
</CommandGroup>
|
</CommandGroup>
|
||||||
|
|
||||||
{searchResults.length > 0 && (
|
{searchResults.length > 0 && (
|
||||||
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Your documents`)}>
|
<CommandGroup className="mx-2 p-0 pb-2" heading={_(msg`Your documents`)}>
|
||||||
<Commands push={push} pages={searchResults} />
|
<Commands push={push} pages={searchResults} />
|
||||||
|
|||||||
@ -153,7 +153,9 @@ export const DirectTemplatePageView = ({
|
|||||||
<CardContent className="p-2">
|
<CardContent className="p-2">
|
||||||
<PDFViewer
|
<PDFViewer
|
||||||
key={template.id}
|
key={template.id}
|
||||||
documentData={template.templateDocumentData}
|
envelopeItem={template.envelopeItems[0]}
|
||||||
|
token={directTemplateRecipient.token}
|
||||||
|
version="signed"
|
||||||
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
|
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export const DocumentSigningAuthAccount = ({
|
|||||||
actionVerb = 'sign',
|
actionVerb = 'sign',
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: DocumentSigningAuthAccountProps) => {
|
}: DocumentSigningAuthAccountProps) => {
|
||||||
const { recipient } = useRequiredDocumentSigningAuthContext();
|
const { recipient, isDirectTemplate } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
|
||||||
@ -34,8 +34,10 @@ export const DocumentSigningAuthAccount = ({
|
|||||||
try {
|
try {
|
||||||
setIsSigningOut(true);
|
setIsSigningOut(true);
|
||||||
|
|
||||||
|
const currentPath = `${window.location.pathname}${window.location.search}${window.location.hash}`;
|
||||||
|
|
||||||
await authClient.signOut({
|
await authClient.signOut({
|
||||||
redirectPath: `/signin#email=${email}`,
|
redirectPath: `/signin?returnTo=${encodeURIComponent(currentPath)}#embedded=true&email=${isDirectTemplate ? '' : email}`,
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
setIsSigningOut(false);
|
setIsSigningOut(false);
|
||||||
@ -55,16 +57,28 @@ export const DocumentSigningAuthAccount = ({
|
|||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{actionTarget === 'DOCUMENT' && recipient.role === RecipientRole.VIEWER ? (
|
{actionTarget === 'DOCUMENT' && recipient.role === RecipientRole.VIEWER ? (
|
||||||
<span>
|
<span>
|
||||||
<Trans>
|
{isDirectTemplate ? (
|
||||||
To mark this document as viewed, you need to be logged in as{' '}
|
<Trans>To mark this document as viewed, you need to be logged in.</Trans>
|
||||||
<strong>{recipient.email}</strong>
|
) : (
|
||||||
</Trans>
|
<Trans>
|
||||||
|
To mark this document as viewed, you need to be logged in as{' '}
|
||||||
|
<strong>{recipient.email}</strong>
|
||||||
|
</Trans>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span>
|
<span>
|
||||||
{/* Todo: Translate */}
|
{isDirectTemplate ? (
|
||||||
To {actionVerb.toLowerCase()} this {actionTarget.toLowerCase()}, you need to be logged
|
<Trans>
|
||||||
in as <strong>{recipient.email}</strong>
|
To {actionVerb.toLowerCase()} this {actionTarget.toLowerCase()}, you need to be
|
||||||
|
logged in.
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>
|
||||||
|
To {actionVerb.toLowerCase()} this {actionTarget.toLowerCase()}, you need to be
|
||||||
|
logged in as <strong>{recipient.email}</strong>
|
||||||
|
</Trans>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
|
|||||||
@ -47,7 +47,8 @@ export const DocumentSigningAuthDialog = ({
|
|||||||
onOpenChange,
|
onOpenChange,
|
||||||
onReauthFormSubmit,
|
onReauthFormSubmit,
|
||||||
}: DocumentSigningAuthDialogProps) => {
|
}: DocumentSigningAuthDialogProps) => {
|
||||||
const { recipient, user, isCurrentlyAuthenticating } = useRequiredDocumentSigningAuthContext();
|
const { recipient, user, isCurrentlyAuthenticating, isDirectTemplate } =
|
||||||
|
useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
// Filter out EXPLICIT_NONE from available auth types for the chooser
|
// Filter out EXPLICIT_NONE from available auth types for the chooser
|
||||||
const validAuthTypes = availableAuthTypes.filter(
|
const validAuthTypes = availableAuthTypes.filter(
|
||||||
@ -168,7 +169,11 @@ export const DocumentSigningAuthDialog = ({
|
|||||||
match({ documentAuthType: selectedAuthType, user })
|
match({ documentAuthType: selectedAuthType, user })
|
||||||
.with(
|
.with(
|
||||||
{ documentAuthType: DocumentAuth.ACCOUNT },
|
{ documentAuthType: DocumentAuth.ACCOUNT },
|
||||||
{ user: P.when((user) => !user || user.email !== recipient.email) }, // Assume all current auth methods requires them to be logged in.
|
{
|
||||||
|
user: P.when(
|
||||||
|
(user) => !user || (user.email !== recipient.email && !isDirectTemplate),
|
||||||
|
),
|
||||||
|
}, // Assume all current auth methods requires them to be logged in.
|
||||||
() => <DocumentSigningAuthAccount onOpenChange={onOpenChange} />,
|
() => <DocumentSigningAuthAccount onOpenChange={onOpenChange} />,
|
||||||
)
|
)
|
||||||
.with({ documentAuthType: DocumentAuth.PASSKEY }, () => (
|
.with({ documentAuthType: DocumentAuth.PASSKEY }, () => (
|
||||||
|
|||||||
@ -40,6 +40,7 @@ export type DocumentSigningAuthContextValue = {
|
|||||||
derivedRecipientAccessAuth: TRecipientAccessAuthTypes[];
|
derivedRecipientAccessAuth: TRecipientAccessAuthTypes[];
|
||||||
derivedRecipientActionAuth: TRecipientActionAuthTypes[];
|
derivedRecipientActionAuth: TRecipientActionAuthTypes[];
|
||||||
isAuthRedirectRequired: boolean;
|
isAuthRedirectRequired: boolean;
|
||||||
|
isDirectTemplate?: boolean;
|
||||||
isCurrentlyAuthenticating: boolean;
|
isCurrentlyAuthenticating: boolean;
|
||||||
setIsCurrentlyAuthenticating: (_value: boolean) => void;
|
setIsCurrentlyAuthenticating: (_value: boolean) => void;
|
||||||
passkeyData: PasskeyData;
|
passkeyData: PasskeyData;
|
||||||
@ -68,6 +69,7 @@ export const useRequiredDocumentSigningAuthContext = () => {
|
|||||||
export interface DocumentSigningAuthProviderProps {
|
export interface DocumentSigningAuthProviderProps {
|
||||||
documentAuthOptions: Envelope['authOptions'];
|
documentAuthOptions: Envelope['authOptions'];
|
||||||
recipient: SigningAuthRecipient;
|
recipient: SigningAuthRecipient;
|
||||||
|
isDirectTemplate?: boolean;
|
||||||
user?: SessionUser | null;
|
user?: SessionUser | null;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
@ -75,6 +77,7 @@ export interface DocumentSigningAuthProviderProps {
|
|||||||
export const DocumentSigningAuthProvider = ({
|
export const DocumentSigningAuthProvider = ({
|
||||||
documentAuthOptions: initialDocumentAuthOptions,
|
documentAuthOptions: initialDocumentAuthOptions,
|
||||||
recipient: initialRecipient,
|
recipient: initialRecipient,
|
||||||
|
isDirectTemplate = false,
|
||||||
user,
|
user,
|
||||||
children,
|
children,
|
||||||
}: DocumentSigningAuthProviderProps) => {
|
}: DocumentSigningAuthProviderProps) => {
|
||||||
@ -204,6 +207,7 @@ export const DocumentSigningAuthProvider = ({
|
|||||||
derivedRecipientAccessAuth,
|
derivedRecipientAccessAuth,
|
||||||
derivedRecipientActionAuth,
|
derivedRecipientActionAuth,
|
||||||
isAuthRedirectRequired,
|
isAuthRedirectRequired,
|
||||||
|
isDirectTemplate,
|
||||||
isCurrentlyAuthenticating,
|
isCurrentlyAuthenticating,
|
||||||
setIsCurrentlyAuthenticating,
|
setIsCurrentlyAuthenticating,
|
||||||
passkeyData,
|
passkeyData,
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import {
|
|||||||
} from '@documenso/ui/primitives/form/form';
|
} from '@documenso/ui/primitives/form/form';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
|
||||||
|
import { useEmbedSigningContext } from '~/components/embed/embed-signing-context';
|
||||||
import { AccessAuth2FAForm } from '~/components/general/document-signing/access-auth-2fa-form';
|
import { AccessAuth2FAForm } from '~/components/general/document-signing/access-auth-2fa-form';
|
||||||
import { DocumentSigningDisclosure } from '~/components/general/document-signing/document-signing-disclosure';
|
import { DocumentSigningDisclosure } from '~/components/general/document-signing/document-signing-disclosure';
|
||||||
|
|
||||||
@ -102,6 +103,8 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
|
|
||||||
const { derivedRecipientAccessAuth } = useRequiredDocumentSigningAuthContext();
|
const { derivedRecipientAccessAuth } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
|
const { isNameLocked, isEmailLocked } = useEmbedSigningContext() || {};
|
||||||
|
|
||||||
const form = useForm<TNextSignerFormSchema>({
|
const form = useForm<TNextSignerFormSchema>({
|
||||||
resolver: allowDictateNextSigner ? zodResolver(ZNextSignerFormSchema) : undefined,
|
resolver: allowDictateNextSigner ? zodResolver(ZNextSignerFormSchema) : undefined,
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -267,7 +270,12 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
<Trans>Your Name</Trans>
|
<Trans>Your Name</Trans>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input {...field} className="mt-2" placeholder={t`Enter your name`} />
|
<Input
|
||||||
|
{...field}
|
||||||
|
className="mt-2"
|
||||||
|
placeholder={t`Enter your name`}
|
||||||
|
disabled={isNameLocked}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@ -289,6 +297,7 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
type="email"
|
type="email"
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
placeholder={t`Enter your email`}
|
placeholder={t`Enter your email`}
|
||||||
|
disabled={!!field.value && isEmailLocked}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|||||||
@ -8,6 +8,9 @@ import { match } from 'ts-pattern';
|
|||||||
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
|
import { useEmbedSigningContext } from '~/components/embed/embed-signing-context';
|
||||||
|
|
||||||
|
import { BrandingLogo } from '../branding-logo';
|
||||||
import EnvelopeSignerForm from '../envelope-signing/envelope-signer-form';
|
import EnvelopeSignerForm from '../envelope-signing/envelope-signer-form';
|
||||||
import { EnvelopeSignerCompleteDialog } from '../envelope-signing/envelope-signing-complete-dialog';
|
import { EnvelopeSignerCompleteDialog } from '../envelope-signing/envelope-signing-complete-dialog';
|
||||||
import { useRequiredEnvelopeSigningContext } from './envelope-signing-provider';
|
import { useRequiredEnvelopeSigningContext } from './envelope-signing-provider';
|
||||||
@ -15,6 +18,8 @@ import { useRequiredEnvelopeSigningContext } from './envelope-signing-provider';
|
|||||||
export const DocumentSigningMobileWidget = () => {
|
export const DocumentSigningMobileWidget = () => {
|
||||||
const [isExpanded, setIsExpanded] = useState(false);
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
|
||||||
|
const { hidePoweredBy = true } = useEmbedSigningContext() || {};
|
||||||
|
|
||||||
const { recipientFieldsRemaining, recipient, requiredRecipientFields } =
|
const { recipientFieldsRemaining, recipient, requiredRecipientFields } =
|
||||||
useRequiredEnvelopeSigningContext();
|
useRequiredEnvelopeSigningContext();
|
||||||
|
|
||||||
@ -29,7 +34,7 @@ export const DocumentSigningMobileWidget = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pointer-events-none fixed bottom-0 left-0 right-0 z-50 flex justify-center px-2 pb-2 sm:px-4 sm:pb-6">
|
<div className="pointer-events-none fixed bottom-0 left-0 right-0 z-50 flex justify-center px-2 pb-2 sm:px-4 sm:pb-6">
|
||||||
<div className="pointer-events-auto w-full max-w-2xl">
|
<div className="pointer-events-auto w-full max-w-[760px]">
|
||||||
<div className="bg-card border-border overflow-hidden rounded-xl border shadow-2xl">
|
<div className="bg-card border-border overflow-hidden rounded-xl border shadow-2xl">
|
||||||
{/* Main Header Bar */}
|
{/* Main Header Bar */}
|
||||||
<div className="flex items-center justify-between gap-4 p-4">
|
<div className="flex items-center justify-between gap-4 p-4">
|
||||||
@ -114,6 +119,13 @@ export const DocumentSigningMobileWidget = () => {
|
|||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="border-border animate-in slide-in-from-bottom-2 border-t p-4 duration-200">
|
<div className="border-border animate-in slide-in-from-bottom-2 border-t p-4 duration-200">
|
||||||
<EnvelopeSignerForm />
|
<EnvelopeSignerForm />
|
||||||
|
|
||||||
|
{!hidePoweredBy && (
|
||||||
|
<div className="bg-primary text-primary-foreground mt-2 inline-block rounded px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100 lg:hidden">
|
||||||
|
<span>Powered by</span>
|
||||||
|
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -245,7 +245,12 @@ export const DocumentSigningPageViewV1 = ({
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<Card className="rounded-xl before:rounded-xl" gradient>
|
<Card className="rounded-xl before:rounded-xl" gradient>
|
||||||
<CardContent className="p-2">
|
<CardContent className="p-2">
|
||||||
<PDFViewer key={documentData.id} documentData={documentData} document={document} />
|
<PDFViewer
|
||||||
|
key={document.envelopeItems[0].id}
|
||||||
|
envelopeItem={document.envelopeItems[0]}
|
||||||
|
token={recipient.token}
|
||||||
|
version="signed"
|
||||||
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -22,7 +22,9 @@ import { SignFieldNameDialog } from '~/components/dialogs/sign-field-name-dialog
|
|||||||
import { SignFieldNumberDialog } from '~/components/dialogs/sign-field-number-dialog';
|
import { SignFieldNumberDialog } from '~/components/dialogs/sign-field-number-dialog';
|
||||||
import { SignFieldSignatureDialog } from '~/components/dialogs/sign-field-signature-dialog';
|
import { SignFieldSignatureDialog } from '~/components/dialogs/sign-field-signature-dialog';
|
||||||
import { SignFieldTextDialog } from '~/components/dialogs/sign-field-text-dialog';
|
import { SignFieldTextDialog } from '~/components/dialogs/sign-field-text-dialog';
|
||||||
|
import { useEmbedSigningContext } from '~/components/embed/embed-signing-context';
|
||||||
|
|
||||||
|
import { BrandingLogo } from '../branding-logo';
|
||||||
import { DocumentSigningAttachmentsPopover } from '../document-signing/document-signing-attachments-popover';
|
import { DocumentSigningAttachmentsPopover } from '../document-signing/document-signing-attachments-popover';
|
||||||
import { EnvelopeItemSelector } from '../envelope-editor/envelope-file-selector';
|
import { EnvelopeItemSelector } from '../envelope-editor/envelope-file-selector';
|
||||||
import EnvelopeSignerForm from '../envelope-signing/envelope-signer-form';
|
import EnvelopeSignerForm from '../envelope-signing/envelope-signer-form';
|
||||||
@ -48,6 +50,13 @@ export const DocumentSigningPageViewV2 = () => {
|
|||||||
selectedAssistantRecipientFields,
|
selectedAssistantRecipientFields,
|
||||||
} = useRequiredEnvelopeSigningContext();
|
} = useRequiredEnvelopeSigningContext();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isEmbed = false,
|
||||||
|
allowDocumentRejection = true,
|
||||||
|
hidePoweredBy = true,
|
||||||
|
onDocumentRejected,
|
||||||
|
} = useEmbedSigningContext() || {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The total remaining fields remaining for the current recipient or selected assistant recipient.
|
* The total remaining fields remaining for the current recipient or selected assistant recipient.
|
||||||
*
|
*
|
||||||
@ -77,7 +86,7 @@ export const DocumentSigningPageViewV2 = () => {
|
|||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div className="flex h-[calc(100vh-4rem)] w-screen">
|
<div className="flex h-[calc(100vh-4rem)] w-screen">
|
||||||
{/* Left Section - Step Navigation */}
|
{/* Left Section - Step Navigation */}
|
||||||
<div className="bg-background border-border hidden w-80 flex-shrink-0 flex-col overflow-y-auto border-r py-4 lg:flex">
|
<div className="embed--DocumentWidgetContainer bg-background border-border hidden w-80 flex-shrink-0 flex-col overflow-y-auto border-r py-4 lg:flex">
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
<h3 className="text-foreground flex items-end justify-between text-sm font-semibold">
|
<h3 className="text-foreground flex items-end justify-between text-sm font-semibold">
|
||||||
{match(recipient.role)
|
{match(recipient.role)
|
||||||
@ -107,7 +116,7 @@ export const DocumentSigningPageViewV2 = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 space-y-3">
|
<div className="embed--DocumentWidgetContent mt-6 space-y-3">
|
||||||
<EnvelopeSignerForm />
|
<EnvelopeSignerForm />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -116,7 +125,7 @@ export const DocumentSigningPageViewV2 = () => {
|
|||||||
|
|
||||||
{/* Quick Actions. */}
|
{/* Quick Actions. */}
|
||||||
{!isDirectTemplate && (
|
{!isDirectTemplate && (
|
||||||
<div className="space-y-3 px-4">
|
<div className="embed--Actions space-y-3 px-4">
|
||||||
<h4 className="text-foreground text-sm font-semibold">
|
<h4 className="text-foreground text-sm font-semibold">
|
||||||
<Trans>Actions</Trans>
|
<Trans>Actions</Trans>
|
||||||
</h4>
|
</h4>
|
||||||
@ -145,10 +154,21 @@ export const DocumentSigningPageViewV2 = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{envelope.type === EnvelopeType.DOCUMENT && (
|
{envelope.type === EnvelopeType.DOCUMENT && allowDocumentRejection && (
|
||||||
<DocumentSigningRejectDialog
|
<DocumentSigningRejectDialog
|
||||||
documentId={mapSecondaryIdToDocumentId(envelope.secondaryId)}
|
documentId={mapSecondaryIdToDocumentId(envelope.secondaryId)}
|
||||||
token={recipient.token}
|
token={recipient.token}
|
||||||
|
onRejected={
|
||||||
|
onDocumentRejected &&
|
||||||
|
((reason) =>
|
||||||
|
onDocumentRejected({
|
||||||
|
token: recipient.token,
|
||||||
|
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
||||||
|
envelopeId: envelope.id,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
reason,
|
||||||
|
}))
|
||||||
|
}
|
||||||
trigger={
|
trigger={
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -164,18 +184,22 @@ export const DocumentSigningPageViewV2 = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Footer of left sidebar. */}
|
<div className="embed--DocumentWidgetFooter mt-auto">
|
||||||
<div className="mt-auto px-4">
|
{/* Footer of left sidebar. */}
|
||||||
<Button asChild variant="ghost" className="w-full justify-start">
|
{!isEmbed && (
|
||||||
<Link to="/">
|
<div className="px-4">
|
||||||
<ArrowLeftIcon className="mr-2 h-4 w-4" />
|
<Button asChild variant="ghost" className="w-full justify-start">
|
||||||
<Trans>Return</Trans>
|
<Link to="/">
|
||||||
</Link>
|
<ArrowLeftIcon className="mr-2 h-4 w-4" />
|
||||||
</Button>
|
<Trans>Return</Trans>
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="embed--DocumentContainer flex-1 overflow-y-auto">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
{/* Horizontal envelope item selector */}
|
{/* Horizontal envelope item selector */}
|
||||||
{envelopeItems.length > 1 && (
|
{envelopeItems.length > 1 && (
|
||||||
@ -202,12 +226,11 @@ export const DocumentSigningPageViewV2 = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Document View */}
|
{/* Document View */}
|
||||||
<div className="flex flex-col items-center justify-center p-2 sm:mt-4 sm:p-4">
|
<div className="embed--DocumentViewer flex flex-col items-center justify-center p-2 sm:mt-4 sm:p-4">
|
||||||
{currentEnvelopeItem ? (
|
{currentEnvelopeItem ? (
|
||||||
<PDFViewerKonvaLazy
|
<PDFViewerKonvaLazy
|
||||||
renderer="signing"
|
renderer="signing"
|
||||||
key={currentEnvelopeItem.id}
|
key={currentEnvelopeItem.id}
|
||||||
documentDataId={currentEnvelopeItem.documentDataId}
|
|
||||||
customPageRenderer={EnvelopeSignerPageRenderer}
|
customPageRenderer={EnvelopeSignerPageRenderer}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
@ -219,9 +242,20 @@ export const DocumentSigningPageViewV2 = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Mobile widget - Additional padding to allow users to scroll */}
|
{/* Mobile widget - Additional padding to allow users to scroll */}
|
||||||
<div className="block pb-16 md:hidden">
|
<div className="block pb-28 lg:hidden">
|
||||||
<DocumentSigningMobileWidget />
|
<DocumentSigningMobileWidget />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!hidePoweredBy && (
|
||||||
|
<a
|
||||||
|
href="https://documenso.com"
|
||||||
|
target="_blank"
|
||||||
|
className="bg-primary text-primary-foreground fixed bottom-0 right-0 z-40 hidden cursor-pointer rounded-tl px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100 lg:block"
|
||||||
|
>
|
||||||
|
<span>Powered by</span>
|
||||||
|
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -56,7 +56,7 @@ export type EnvelopeSigningContextValue = {
|
|||||||
_fieldId: number,
|
_fieldId: number,
|
||||||
_value: TSignEnvelopeFieldValue,
|
_value: TSignEnvelopeFieldValue,
|
||||||
authOptions?: TRecipientActionAuth,
|
authOptions?: TRecipientActionAuth,
|
||||||
) => Promise<void>;
|
) => Promise<Pick<Field, 'id' | 'inserted'>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EnvelopeSigningContext = createContext<EnvelopeSigningContextValue | null>(null);
|
const EnvelopeSigningContext = createContext<EnvelopeSigningContextValue | null>(null);
|
||||||
@ -296,16 +296,19 @@ export const EnvelopeSigningProvider = ({
|
|||||||
) => {
|
) => {
|
||||||
// Set the field locally for direct templates.
|
// Set the field locally for direct templates.
|
||||||
if (isDirectTemplate) {
|
if (isDirectTemplate) {
|
||||||
handleDirectTemplateFieldInsertion(fieldId, fieldValue);
|
const signedField = handleDirectTemplateFieldInsertion(fieldId, fieldValue);
|
||||||
return;
|
|
||||||
|
return signedField;
|
||||||
}
|
}
|
||||||
|
|
||||||
await signEnvelopeField({
|
const { signedField } = await signEnvelopeField({
|
||||||
token: envelopeData.recipient.token,
|
token: envelopeData.recipient.token,
|
||||||
fieldId,
|
fieldId,
|
||||||
fieldValue,
|
fieldValue,
|
||||||
authOptions,
|
authOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return signedField;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDirectTemplateFieldInsertion = (
|
const handleDirectTemplateFieldInsertion = (
|
||||||
@ -363,6 +366,8 @@ export const EnvelopeSigningProvider = ({
|
|||||||
fields: prev.recipient.fields.map((field) => (field.id === fieldId ? updatedField : field)),
|
fields: prev.recipient.fields.map((field) => (field.id === fieldId ? updatedField : field)),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
return updatedField;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { DocumentData, EnvelopeItem } from '@prisma/client';
|
import { type DocumentData, DocumentStatus, type EnvelopeItem, EnvelopeType } from '@prisma/client';
|
||||||
|
import { DownloadIcon } from 'lucide-react';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -22,9 +23,10 @@ import {
|
|||||||
} from '@documenso/ui/primitives/dialog';
|
} from '@documenso/ui/primitives/dialog';
|
||||||
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
|
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
|
||||||
|
|
||||||
|
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
|
||||||
|
|
||||||
import { EnvelopeRendererFileSelector } from '../envelope-editor/envelope-file-selector';
|
import { EnvelopeRendererFileSelector } from '../envelope-editor/envelope-file-selector';
|
||||||
import EnvelopeGenericPageRenderer from '../envelope-editor/envelope-generic-page-renderer';
|
import EnvelopeGenericPageRenderer from '../envelope-editor/envelope-generic-page-renderer';
|
||||||
import { ShareDocumentDownloadButton } from '../share-document-download-button';
|
|
||||||
|
|
||||||
export type DocumentCertificateQRViewProps = {
|
export type DocumentCertificateQRViewProps = {
|
||||||
documentId: number;
|
documentId: number;
|
||||||
@ -34,6 +36,7 @@ export type DocumentCertificateQRViewProps = {
|
|||||||
documentTeamUrl: string;
|
documentTeamUrl: string;
|
||||||
recipientCount?: number;
|
recipientCount?: number;
|
||||||
completedDate?: Date;
|
completedDate?: Date;
|
||||||
|
token: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DocumentCertificateQRView = ({
|
export const DocumentCertificateQRView = ({
|
||||||
@ -44,6 +47,7 @@ export const DocumentCertificateQRView = ({
|
|||||||
documentTeamUrl,
|
documentTeamUrl,
|
||||||
recipientCount = 0,
|
recipientCount = 0,
|
||||||
completedDate,
|
completedDate,
|
||||||
|
token,
|
||||||
}: DocumentCertificateQRViewProps) => {
|
}: DocumentCertificateQRViewProps) => {
|
||||||
const { data: documentViaUser } = trpc.document.get.useQuery({
|
const { data: documentViaUser } = trpc.document.get.useQuery({
|
||||||
documentId,
|
documentId,
|
||||||
@ -96,11 +100,19 @@ export const DocumentCertificateQRView = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{internalVersion === 2 ? (
|
{internalVersion === 2 ? (
|
||||||
<EnvelopeRenderProvider envelope={{ envelopeItems }}>
|
<EnvelopeRenderProvider
|
||||||
|
envelope={{
|
||||||
|
envelopeItems,
|
||||||
|
status: DocumentStatus.COMPLETED,
|
||||||
|
type: EnvelopeType.DOCUMENT,
|
||||||
|
}}
|
||||||
|
token={token}
|
||||||
|
>
|
||||||
<DocumentCertificateQrV2
|
<DocumentCertificateQrV2
|
||||||
title={title}
|
title={title}
|
||||||
recipientCount={recipientCount}
|
recipientCount={recipientCount}
|
||||||
formattedDate={formattedDate}
|
formattedDate={formattedDate}
|
||||||
|
token={token}
|
||||||
/>
|
/>
|
||||||
</EnvelopeRenderProvider>
|
</EnvelopeRenderProvider>
|
||||||
) : (
|
) : (
|
||||||
@ -119,14 +131,27 @@ export const DocumentCertificateQRView = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ShareDocumentDownloadButton
|
<EnvelopeDownloadDialog
|
||||||
title={title}
|
envelopeId={envelopeItems[0].envelopeId}
|
||||||
documentData={envelopeItems[0].documentData}
|
envelopeStatus={DocumentStatus.COMPLETED}
|
||||||
|
envelopeItems={envelopeItems}
|
||||||
|
token={token}
|
||||||
|
trigger={
|
||||||
|
<Button type="button" variant="outline" className="w-fit">
|
||||||
|
<DownloadIcon className="mr-2 h-5 w-5" />
|
||||||
|
<Trans>Download</Trans>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-12 w-full">
|
<div className="mt-12 w-full">
|
||||||
<PDFViewer key={envelopeItems[0].id} documentData={envelopeItems[0].documentData} />
|
<PDFViewer
|
||||||
|
key={envelopeItems[0].id}
|
||||||
|
envelopeItem={envelopeItems[0]}
|
||||||
|
token={token}
|
||||||
|
version="signed"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@ -138,14 +163,16 @@ type DocumentCertificateQrV2Props = {
|
|||||||
title: string;
|
title: string;
|
||||||
recipientCount: number;
|
recipientCount: number;
|
||||||
formattedDate: string;
|
formattedDate: string;
|
||||||
|
token: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DocumentCertificateQrV2 = ({
|
const DocumentCertificateQrV2 = ({
|
||||||
title,
|
title,
|
||||||
recipientCount,
|
recipientCount,
|
||||||
formattedDate,
|
formattedDate,
|
||||||
|
token,
|
||||||
}: DocumentCertificateQrV2Props) => {
|
}: DocumentCertificateQrV2Props) => {
|
||||||
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
|
const { currentEnvelopeItem, envelopeItems } = useCurrentEnvelopeRender();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col items-start">
|
<div className="flex min-h-screen flex-col items-start">
|
||||||
@ -163,12 +190,18 @@ const DocumentCertificateQrV2 = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{currentEnvelopeItem && (
|
<EnvelopeDownloadDialog
|
||||||
<ShareDocumentDownloadButton
|
envelopeId={envelopeItems[0].envelopeId}
|
||||||
title={title}
|
envelopeStatus={DocumentStatus.COMPLETED}
|
||||||
documentData={currentEnvelopeItem.documentData}
|
envelopeItems={envelopeItems}
|
||||||
/>
|
token={token}
|
||||||
)}
|
trigger={
|
||||||
|
<Button type="button" variant="outline" className="w-fit">
|
||||||
|
<DownloadIcon className="mr-2 h-5 w-5" />
|
||||||
|
<Trans>Download</Trans>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-12 w-full">
|
<div className="mt-12 w-full">
|
||||||
|
|||||||
@ -441,9 +441,10 @@ export const DocumentEditForm = ({
|
|||||||
>
|
>
|
||||||
<CardContent className="p-2">
|
<CardContent className="p-2">
|
||||||
<PDFViewer
|
<PDFViewer
|
||||||
key={document.documentData.id}
|
key={document.envelopeItems[0].id}
|
||||||
documentData={document.documentData}
|
envelopeItem={document.envelopeItems[0]}
|
||||||
document={document}
|
token={undefined}
|
||||||
|
version="signed"
|
||||||
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
|
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -1,18 +1,14 @@
|
|||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||||
import { CheckCircle, Download, EyeIcon, Pencil } from 'lucide-react';
|
import { CheckCircle, Download, EyeIcon, Pencil } from 'lucide-react';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
|
||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
|
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
|
||||||
|
|
||||||
@ -23,9 +19,6 @@ export type DocumentPageViewButtonProps = {
|
|||||||
export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps) => {
|
export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps) => {
|
||||||
const { user } = useSession();
|
const { user } = useSession();
|
||||||
|
|
||||||
const { toast } = useToast();
|
|
||||||
const { _ } = useLingui();
|
|
||||||
|
|
||||||
const recipient = envelope.recipients.find((recipient) => recipient.email === user.email);
|
const recipient = envelope.recipients.find((recipient) => recipient.email === user.email);
|
||||||
|
|
||||||
const isRecipient = !!recipient;
|
const isRecipient = !!recipient;
|
||||||
@ -37,25 +30,6 @@ export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps
|
|||||||
const documentsPath = formatDocumentsPath(envelope.team.url);
|
const documentsPath = formatDocumentsPath(envelope.team.url);
|
||||||
const formatPath = `${documentsPath}/${envelope.id}/edit`;
|
const formatPath = `${documentsPath}/${envelope.id}/edit`;
|
||||||
|
|
||||||
const onDownloadClick = async () => {
|
|
||||||
try {
|
|
||||||
// Todo; Envelopes - Support multiple items
|
|
||||||
const envelopeItem = envelope.envelopeItems[0];
|
|
||||||
|
|
||||||
if (!envelopeItem.documentData) {
|
|
||||||
throw new Error('No document available');
|
|
||||||
}
|
|
||||||
|
|
||||||
await downloadPDF({ documentData: envelopeItem.documentData, fileName: envelopeItem.title });
|
|
||||||
} catch (err) {
|
|
||||||
toast({
|
|
||||||
title: _(msg`Something went wrong`),
|
|
||||||
description: _(msg`An error occurred while downloading your document.`),
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return match({
|
return match({
|
||||||
isRecipient,
|
isRecipient,
|
||||||
isPending,
|
isPending,
|
||||||
@ -95,7 +69,7 @@ export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
))
|
))
|
||||||
.with({ isComplete: true, internalVersion: 2 }, () => (
|
.with({ isComplete: true }, () => (
|
||||||
<EnvelopeDownloadDialog
|
<EnvelopeDownloadDialog
|
||||||
envelopeId={envelope.id}
|
envelopeId={envelope.id}
|
||||||
envelopeStatus={envelope.status}
|
envelopeStatus={envelope.status}
|
||||||
@ -109,11 +83,5 @@ export const DocumentPageViewButton = ({ envelope }: DocumentPageViewButtonProps
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with({ isComplete: true }, () => (
|
|
||||||
<Button className="w-full" onClick={onDownloadClick}>
|
|
||||||
<Download className="-ml-1 mr-2 inline h-4 w-4" />
|
|
||||||
<Trans>Download</Trans>
|
|
||||||
</Button>
|
|
||||||
))
|
|
||||||
.otherwise(() => null);
|
.otherwise(() => null);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { DocumentStatus } from '@prisma/client';
|
import { DocumentStatus } from '@prisma/client';
|
||||||
@ -16,13 +15,11 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Link, useNavigate } from 'react-router';
|
import { Link, useNavigate } from 'react-router';
|
||||||
|
|
||||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
|
||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
|
||||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@ -67,64 +64,6 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP
|
|||||||
|
|
||||||
const documentsPath = formatDocumentsPath(team.url);
|
const documentsPath = formatDocumentsPath(team.url);
|
||||||
|
|
||||||
const onDownloadClick = async () => {
|
|
||||||
try {
|
|
||||||
const documentWithData = await trpcClient.document.get.query(
|
|
||||||
{
|
|
||||||
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
context: {
|
|
||||||
teamId: team?.id?.toString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const documentData = documentWithData?.documentData;
|
|
||||||
|
|
||||||
if (!documentData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await downloadPDF({ documentData, fileName: envelope.title });
|
|
||||||
} catch (err) {
|
|
||||||
toast({
|
|
||||||
title: _(msg`Something went wrong`),
|
|
||||||
description: _(msg`An error occurred while downloading your document.`),
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDownloadOriginalClick = async () => {
|
|
||||||
try {
|
|
||||||
const documentWithData = await trpcClient.document.get.query(
|
|
||||||
{
|
|
||||||
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
context: {
|
|
||||||
teamId: team?.id?.toString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const documentData = documentWithData?.documentData;
|
|
||||||
|
|
||||||
if (!documentData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await downloadPDF({ documentData, fileName: envelope.title, version: 'original' });
|
|
||||||
} catch (err) {
|
|
||||||
toast({
|
|
||||||
title: _(msg`Something went wrong`),
|
|
||||||
description: _(msg`An error occurred while downloading your document.`),
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const nonSignedRecipients = envelope.recipients.filter((item) => item.signingStatus !== 'SIGNED');
|
const nonSignedRecipients = envelope.recipients.filter((item) => item.signingStatus !== 'SIGNED');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -147,36 +86,20 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{envelope.internalVersion === 2 ? (
|
<EnvelopeDownloadDialog
|
||||||
<EnvelopeDownloadDialog
|
envelopeId={envelope.id}
|
||||||
envelopeId={envelope.id}
|
envelopeStatus={envelope.status}
|
||||||
envelopeStatus={envelope.status}
|
token={recipient?.token}
|
||||||
token={recipient?.token}
|
envelopeItems={envelope.envelopeItems}
|
||||||
envelopeItems={envelope.envelopeItems}
|
trigger={
|
||||||
trigger={
|
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
|
||||||
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
|
<div>
|
||||||
<div>
|
|
||||||
<Download className="mr-2 h-4 w-4" />
|
|
||||||
<Trans>Download</Trans>
|
|
||||||
</div>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{isComplete && (
|
|
||||||
<DropdownMenuItem onClick={onDownloadClick}>
|
|
||||||
<Download className="mr-2 h-4 w-4" />
|
<Download className="mr-2 h-4 w-4" />
|
||||||
<Trans>Download</Trans>
|
<Trans>Download</Trans>
|
||||||
</DropdownMenuItem>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<DropdownMenuItem onClick={onDownloadOriginalClick}>
|
|
||||||
<Download className="mr-2 h-4 w-4" />
|
|
||||||
<Trans>Download Original</Trans>
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</>
|
}
|
||||||
)}
|
/>
|
||||||
|
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link to={`${documentsPath}/${envelope.id}/logs`}>
|
<Link to={`${documentsPath}/${envelope.id}/logs`}>
|
||||||
@ -250,7 +173,8 @@ export const DocumentPageViewDropdown = ({ envelope }: DocumentPageViewDropdownP
|
|||||||
|
|
||||||
{isDuplicateDialogOpen && (
|
{isDuplicateDialogOpen && (
|
||||||
<DocumentDuplicateDialog
|
<DocumentDuplicateDialog
|
||||||
id={mapSecondaryIdToDocumentId(envelope.secondaryId)}
|
id={envelope.id}
|
||||||
|
token={recipient?.token}
|
||||||
open={isDuplicateDialogOpen}
|
open={isDuplicateDialogOpen}
|
||||||
onOpenChange={setDuplicateDialogOpen}
|
onOpenChange={setDuplicateDialogOpen}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { DateTime } from 'luxon';
|
|||||||
|
|
||||||
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
|
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
|
||||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||||
|
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||||
|
|
||||||
export type DocumentPageViewInformationProps = {
|
export type DocumentPageViewInformationProps = {
|
||||||
userId: number;
|
userId: number;
|
||||||
@ -40,6 +41,10 @@ export const DocumentPageViewInformation = ({
|
|||||||
.setLocale(i18n.locales?.[0] || i18n.locale)
|
.setLocale(i18n.locales?.[0] || i18n.locale)
|
||||||
.toRelative(),
|
.toRelative(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: msg`Document ID (Legacy)`,
|
||||||
|
value: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isMounted, envelope, userId]);
|
}, [isMounted, envelope, userId]);
|
||||||
|
|||||||
@ -616,13 +616,14 @@ export default function EnvelopeEditorFieldsPageRenderer() {
|
|||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
zIndex: 50,
|
zIndex: 50,
|
||||||
}}
|
}}
|
||||||
className="text-muted-foreground grid w-max grid-cols-5 gap-x-1 gap-y-0.5 rounded-md border bg-white p-1 shadow-sm"
|
// Don't use darkmode for this component, it should look the same for both light/dark modes.
|
||||||
|
className="grid w-max grid-cols-5 gap-x-1 gap-y-0.5 rounded-md border border-gray-300 bg-white p-1 text-gray-500 shadow-sm"
|
||||||
>
|
>
|
||||||
{fieldButtonList.map((field) => (
|
{fieldButtonList.map((field) => (
|
||||||
<button
|
<button
|
||||||
key={field.type}
|
key={field.type}
|
||||||
onClick={() => createFieldFromPendingTemplate(pendingFieldCreation, field.type)}
|
onClick={() => createFieldFromPendingTemplate(pendingFieldCreation, field.type)}
|
||||||
className="hover:text-foreground col-span-1 w-full flex-shrink-0 rounded-sm px-2 py-1 text-xs hover:bg-gray-100"
|
className="col-span-1 w-full flex-shrink-0 rounded-sm px-2 py-1 text-xs hover:bg-gray-100 hover:text-gray-600"
|
||||||
>
|
>
|
||||||
{t(field.name)}
|
{t(field.name)}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -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, SigningStatus } 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,41 @@ 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}
|
||||||
|
recipients={envelope.recipients.map((recipient) => ({
|
||||||
|
...recipient,
|
||||||
|
signingStatus: SigningStatus.SIGNED,
|
||||||
|
}))}
|
||||||
|
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 +243,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 +303,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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -212,7 +212,7 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const hasDocumentBeenSent = recipients.some(
|
const hasDocumentBeenSent = recipients.some(
|
||||||
(recipient) => recipient.sendStatus === SendStatus.SENT,
|
(recipient) => recipient.role !== RecipientRole.CC && recipient.sendStatus === SendStatus.SENT,
|
||||||
);
|
);
|
||||||
|
|
||||||
const canRecipientBeModified = (recipientId?: number) => {
|
const canRecipientBeModified = (recipientId?: number) => {
|
||||||
@ -482,30 +482,46 @@ export const EnvelopeEditorRecipientForm = () => {
|
|||||||
|
|
||||||
const { data } = validatedFormValues;
|
const { data } = validatedFormValues;
|
||||||
|
|
||||||
|
// Weird edge case where the whole envelope is created via API
|
||||||
|
// with no signing order. If they come to this page it will show an error
|
||||||
|
// since they aren't equal and the recipient is no longer editable.
|
||||||
|
const envelopeRecipients = data.signers.map((recipient) => {
|
||||||
|
if (!canRecipientBeModified(recipient.id)) {
|
||||||
|
return {
|
||||||
|
...recipient,
|
||||||
|
signingOrder: recipient.signingOrder,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return recipient;
|
||||||
|
});
|
||||||
|
|
||||||
const hasSigningOrderChanged = envelope.documentMeta.signingOrder !== data.signingOrder;
|
const hasSigningOrderChanged = envelope.documentMeta.signingOrder !== data.signingOrder;
|
||||||
const hasAllowDictateNextSignerChanged =
|
const hasAllowDictateNextSignerChanged =
|
||||||
envelope.documentMeta.allowDictateNextSigner !== data.allowDictateNextSigner;
|
envelope.documentMeta.allowDictateNextSigner !== data.allowDictateNextSigner;
|
||||||
|
|
||||||
const hasSignersChanged =
|
const hasSignersChanged =
|
||||||
data.signers.length !== recipients.length ||
|
envelopeRecipients.length !== recipients.length ||
|
||||||
data.signers.some((signer) => {
|
envelopeRecipients.some((signer) => {
|
||||||
const recipient = recipients.find((recipient) => recipient.id === signer.id);
|
const recipient = recipients.find((recipient) => recipient.id === signer.id);
|
||||||
|
|
||||||
if (!recipient) {
|
if (!recipient) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const signerActionAuth = signer.actionAuth;
|
||||||
|
const recipientActionAuth = recipient.authOptions?.actionAuth || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
signer.email !== recipient.email ||
|
signer.email !== recipient.email ||
|
||||||
signer.name !== recipient.name ||
|
signer.name !== recipient.name ||
|
||||||
signer.role !== recipient.role ||
|
signer.role !== recipient.role ||
|
||||||
signer.signingOrder !== recipient.signingOrder ||
|
signer.signingOrder !== recipient.signingOrder ||
|
||||||
!isDeepEqual(signer.actionAuth, recipient.authOptions?.actionAuth)
|
!isDeepEqual(signerActionAuth, recipientActionAuth)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hasSignersChanged) {
|
if (hasSignersChanged) {
|
||||||
setRecipientsDebounced(validatedFormValues.data.signers);
|
setRecipientsDebounced(envelopeRecipients);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasSigningOrderChanged || hasAllowDictateNextSignerChanged) {
|
if (hasSigningOrderChanged || hasAllowDictateNextSignerChanged) {
|
||||||
|
|||||||
@ -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]);
|
||||||
|
|
||||||
|
|||||||
@ -18,9 +18,9 @@ import {
|
|||||||
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
|
||||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
|
||||||
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
|
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import type { TCreateEnvelopeItemsPayload } from '@documenso/trpc/server/envelope-router/create-envelope-items.types';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@ -49,7 +49,7 @@ export const EnvelopeEditorUploadPage = () => {
|
|||||||
const organisation = useCurrentOrganisation();
|
const organisation = useCurrentOrganisation();
|
||||||
|
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
const { envelope, setLocalEnvelope, relativePath } = useCurrentEnvelopeEditor();
|
const { envelope, setLocalEnvelope, relativePath, editorFields } = useCurrentEnvelopeEditor();
|
||||||
const { maximumEnvelopeItemCount, remaining } = useLimits();
|
const { maximumEnvelopeItemCount, remaining } = useLimits();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@ -67,8 +67,8 @@ export const EnvelopeEditorUploadPage = () => {
|
|||||||
|
|
||||||
const { mutateAsync: createEnvelopeItems, isPending: isCreatingEnvelopeItems } =
|
const { mutateAsync: createEnvelopeItems, isPending: isCreatingEnvelopeItems } =
|
||||||
trpc.envelope.item.createMany.useMutation({
|
trpc.envelope.item.createMany.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: ({ data }) => {
|
||||||
const createdEnvelopes = data.createdEnvelopeItems.filter(
|
const createdEnvelopes = data.filter(
|
||||||
(item) => !envelope.envelopeItems.find((envelopeItem) => envelopeItem.id === item.id),
|
(item) => !envelope.envelopeItems.find((envelopeItem) => envelopeItem.id === item.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -79,10 +79,10 @@ export const EnvelopeEditorUploadPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: updateEnvelopeItems } = trpc.envelope.item.updateMany.useMutation({
|
const { mutateAsync: updateEnvelopeItems } = trpc.envelope.item.updateMany.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: ({ data }) => {
|
||||||
setLocalEnvelope({
|
setLocalEnvelope({
|
||||||
envelopeItems: envelope.envelopeItems.map((originalItem) => {
|
envelopeItems: envelope.envelopeItems.map((originalItem) => {
|
||||||
const updatedItem = data.updatedEnvelopeItems.find((item) => item.id === originalItem.id);
|
const updatedItem = data.find((item) => item.id === originalItem.id);
|
||||||
|
|
||||||
if (updatedItem) {
|
if (updatedItem) {
|
||||||
return {
|
return {
|
||||||
@ -114,36 +114,19 @@ export const EnvelopeEditorUploadPage = () => {
|
|||||||
|
|
||||||
setLocalFiles((prev) => [...prev, ...newUploadingFiles]);
|
setLocalFiles((prev) => [...prev, ...newUploadingFiles]);
|
||||||
|
|
||||||
const result = await Promise.all(
|
const payload = {
|
||||||
files.map(async (file, index) => {
|
|
||||||
try {
|
|
||||||
const response = await putPdfFile(file);
|
|
||||||
|
|
||||||
// Mark as uploaded (remove from uploading state)
|
|
||||||
return {
|
|
||||||
title: file.name,
|
|
||||||
documentDataId: response.id,
|
|
||||||
};
|
|
||||||
} catch (_error) {
|
|
||||||
setLocalFiles((prev) =>
|
|
||||||
prev.map((uploadingFile) =>
|
|
||||||
uploadingFile.id === newUploadingFiles[index].id
|
|
||||||
? { ...uploadingFile, isError: true, isUploading: false }
|
|
||||||
: uploadingFile,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const envelopeItemsToCreate = result.filter(
|
|
||||||
(item): item is { title: string; documentDataId: string } => item !== undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
const { createdEnvelopeItems } = await createEnvelopeItems({
|
|
||||||
envelopeId: envelope.id,
|
envelopeId: envelope.id,
|
||||||
data: envelopeItemsToCreate,
|
} satisfies TCreateEnvelopeItemsPayload;
|
||||||
}).catch((error) => {
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append('payload', JSON.stringify(payload));
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
formData.append('files', file);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await createEnvelopeItems(formData).catch((error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|
||||||
// Set error state on files in batch upload.
|
// Set error state on files in batch upload.
|
||||||
@ -165,7 +148,7 @@ export const EnvelopeEditorUploadPage = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return filteredFiles.concat(
|
return filteredFiles.concat(
|
||||||
createdEnvelopeItems.map((item) => ({
|
data.map((item) => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
envelopeItemId: item.id,
|
envelopeItemId: item.id,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
@ -182,9 +165,17 @@ export const EnvelopeEditorUploadPage = () => {
|
|||||||
const onFileDelete = (envelopeItemId: string) => {
|
const onFileDelete = (envelopeItemId: string) => {
|
||||||
setLocalFiles((prev) => prev.filter((uploadingFile) => uploadingFile.id !== envelopeItemId));
|
setLocalFiles((prev) => prev.filter((uploadingFile) => uploadingFile.id !== envelopeItemId));
|
||||||
|
|
||||||
|
const fieldsWithoutDeletedItem = envelope.fields.filter(
|
||||||
|
(field) => field.envelopeItemId !== envelopeItemId,
|
||||||
|
);
|
||||||
|
|
||||||
setLocalEnvelope({
|
setLocalEnvelope({
|
||||||
envelopeItems: envelope.envelopeItems.filter((item) => item.id !== envelopeItemId),
|
envelopeItems: envelope.envelopeItems.filter((item) => item.id !== envelopeItemId),
|
||||||
|
fields: envelope.fields.filter((field) => field.envelopeItemId !== envelopeItemId),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reset editor fields.
|
||||||
|
editorFields.resetForm(fieldsWithoutDeletedItem);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export const EnvelopeItemSelector = ({
|
|||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium ${
|
className={`flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full text-xs font-medium ${
|
||||||
isSelected ? 'bg-green-100 text-green-600' : 'bg-gray-200 text-gray-600'
|
isSelected ? 'bg-green-100 text-green-600' : 'bg-gray-200 text-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
|
import { DocumentStatus, type Recipient, SigningStatus } from '@prisma/client';
|
||||||
import type Konva from 'konva';
|
import type Konva from 'konva';
|
||||||
|
|
||||||
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
|
import { usePageRenderer } from '@documenso/lib/client-only/hooks/use-page-renderer';
|
||||||
@ -8,12 +9,24 @@ import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/e
|
|||||||
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||||
import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
|
import { renderField } from '@documenso/lib/universal/field-renderer/render-field';
|
||||||
import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
|
import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
|
||||||
|
import { EnvelopeRecipientFieldTooltip } from '@documenso/ui/components/document/envelope-recipient-field-tooltip';
|
||||||
|
|
||||||
|
type GenericLocalField = TEnvelope['fields'][number] & {
|
||||||
|
recipient: Pick<Recipient, 'id' | 'name' | 'email' | 'signingStatus'>;
|
||||||
|
};
|
||||||
|
|
||||||
export default function EnvelopeGenericPageRenderer() {
|
export default function EnvelopeGenericPageRenderer() {
|
||||||
const { i18n } = useLingui();
|
const { i18n } = useLingui();
|
||||||
|
|
||||||
const { currentEnvelopeItem, fields, getRecipientColorKey, setRenderError } =
|
const {
|
||||||
useCurrentEnvelopeRender();
|
envelopeStatus,
|
||||||
|
currentEnvelopeItem,
|
||||||
|
fields,
|
||||||
|
recipients,
|
||||||
|
getRecipientColorKey,
|
||||||
|
setRenderError,
|
||||||
|
overrideSettings,
|
||||||
|
} = useCurrentEnvelopeRender();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
stage,
|
stage,
|
||||||
@ -29,45 +42,73 @@ export default function EnvelopeGenericPageRenderer() {
|
|||||||
|
|
||||||
const { _className, scale } = pageContext;
|
const { _className, scale } = pageContext;
|
||||||
|
|
||||||
const localPageFields = useMemo(
|
const localPageFields = useMemo((): GenericLocalField[] => {
|
||||||
() =>
|
if (envelopeStatus === DocumentStatus.COMPLETED) {
|
||||||
fields.filter(
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields
|
||||||
|
.filter(
|
||||||
(field) =>
|
(field) =>
|
||||||
field.page === pageContext.pageNumber && field.envelopeItemId === currentEnvelopeItem?.id,
|
field.page === pageContext.pageNumber && field.envelopeItemId === currentEnvelopeItem?.id,
|
||||||
),
|
)
|
||||||
[fields, pageContext.pageNumber],
|
.map((field) => {
|
||||||
);
|
const recipient = recipients.find((recipient) => recipient.id === field.recipientId);
|
||||||
|
|
||||||
const unsafeRenderFieldOnLayer = (field: TEnvelope['fields'][number]) => {
|
if (!recipient) {
|
||||||
|
throw new Error(`Recipient not found for field ${field.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isInserted = recipient.signingStatus === SigningStatus.SIGNED && field.inserted;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...field,
|
||||||
|
inserted: isInserted,
|
||||||
|
customText: isInserted ? field.customText : '',
|
||||||
|
recipient,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(
|
||||||
|
({ inserted, fieldMeta, recipient }) =>
|
||||||
|
(recipient.signingStatus === SigningStatus.SIGNED ? inserted : true) ||
|
||||||
|
fieldMeta?.readOnly,
|
||||||
|
);
|
||||||
|
}, [fields, pageContext.pageNumber, currentEnvelopeItem?.id, recipients]);
|
||||||
|
|
||||||
|
const unsafeRenderFieldOnLayer = (field: GenericLocalField) => {
|
||||||
if (!pageLayer.current) {
|
if (!pageLayer.current) {
|
||||||
console.error('Layer not loaded yet');
|
console.error('Layer not loaded yet');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fieldTranslations = getClientSideFieldTranslations(i18n);
|
||||||
|
|
||||||
renderField({
|
renderField({
|
||||||
scale,
|
scale,
|
||||||
pageLayer: pageLayer.current,
|
pageLayer: pageLayer.current,
|
||||||
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,
|
|
||||||
fieldMeta: field.fieldMeta,
|
fieldMeta: field.fieldMeta,
|
||||||
|
signature: {
|
||||||
|
signatureImageAsBase64: '',
|
||||||
|
typedSignature: fieldTranslations.SIGNATURE,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
translations: getClientSideFieldTranslations(i18n),
|
translations: fieldTranslations,
|
||||||
pageWidth: unscaledViewport.width,
|
pageWidth: unscaledViewport.width,
|
||||||
pageHeight: unscaledViewport.height,
|
pageHeight: unscaledViewport.height,
|
||||||
color: getRecipientColorKey(field.recipientId),
|
color: getRecipientColorKey(field.recipientId),
|
||||||
editable: false,
|
editable: false,
|
||||||
mode: 'sign',
|
mode: overrideSettings?.mode ?? 'edit',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderFieldOnLayer = (field: TEnvelope['fields'][number]) => {
|
const renderFieldOnLayer = (field: GenericLocalField) => {
|
||||||
try {
|
try {
|
||||||
unsafeRenderFieldOnLayer(field);
|
unsafeRenderFieldOnLayer(field);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -123,6 +164,16 @@ export default function EnvelopeGenericPageRenderer() {
|
|||||||
className="relative w-full"
|
className="relative w-full"
|
||||||
key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`}
|
key={`${currentEnvelopeItem.id}-renderer-${pageContext.pageNumber}`}
|
||||||
>
|
>
|
||||||
|
{overrideSettings?.showRecipientTooltip &&
|
||||||
|
localPageFields.map((field) => (
|
||||||
|
<EnvelopeRecipientFieldTooltip
|
||||||
|
key={field.id}
|
||||||
|
field={field}
|
||||||
|
showFieldStatus={overrideSettings?.showRecipientSigningStatus}
|
||||||
|
showRecipientTooltip={overrideSettings?.showRecipientTooltip}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
{/* The element Konva will inject it's canvas into. */}
|
{/* The element Konva will inject it's canvas into. */}
|
||||||
<div className="konva-container absolute inset-0 z-10 w-full" ref={konvaContainer}></div>
|
<div className="konva-container absolute inset-0 z-10 w-full" ref={konvaContainer}></div>
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import { Label } from '@documenso/ui/primitives/label';
|
|||||||
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
|
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
|
||||||
import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog';
|
import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog';
|
||||||
|
|
||||||
|
import { useEmbedSigningContext } from '~/components/embed/embed-signing-context';
|
||||||
|
|
||||||
import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider';
|
import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider';
|
||||||
|
|
||||||
export default function EnvelopeSignerForm() {
|
export default function EnvelopeSignerForm() {
|
||||||
@ -25,6 +27,8 @@ export default function EnvelopeSignerForm() {
|
|||||||
setSelectedAssistantRecipientId,
|
setSelectedAssistantRecipientId,
|
||||||
} = useRequiredEnvelopeSigningContext();
|
} = useRequiredEnvelopeSigningContext();
|
||||||
|
|
||||||
|
const { isNameLocked, isEmailLocked } = useEmbedSigningContext() || {};
|
||||||
|
|
||||||
const hasSignatureField = useMemo(() => {
|
const hasSignatureField = useMemo(() => {
|
||||||
return recipientFields.some((field) => field.type === FieldType.SIGNATURE);
|
return recipientFields.some((field) => field.type === FieldType.SIGNATURE);
|
||||||
}, [recipientFields]);
|
}, [recipientFields]);
|
||||||
@ -37,7 +41,7 @@ export default function EnvelopeSignerForm() {
|
|||||||
|
|
||||||
if (recipient.role === RecipientRole.ASSISTANT) {
|
if (recipient.role === RecipientRole.ASSISTANT) {
|
||||||
return (
|
return (
|
||||||
<fieldset className="dark:bg-background border-border rounded-2xl sm:border sm:p-3">
|
<fieldset className="embed--DocumentWidgetForm dark:bg-background border-border rounded-2xl sm:border sm:p-3">
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
className="gap-0 space-y-2 shadow-none sm:space-y-3"
|
className="gap-0 space-y-2 shadow-none sm:space-y-3"
|
||||||
value={selectedAssistantRecipient?.id?.toString()}
|
value={selectedAssistantRecipient?.id?.toString()}
|
||||||
@ -101,7 +105,8 @@ export default function EnvelopeSignerForm() {
|
|||||||
id="full-name"
|
id="full-name"
|
||||||
className="bg-background mt-2"
|
className="bg-background mt-2"
|
||||||
value={fullName}
|
value={fullName}
|
||||||
onChange={(e) => setFullName(e.target.value.trimStart())}
|
disabled={isNameLocked}
|
||||||
|
onChange={(e) => !isNameLocked && setFullName(e.target.value.trimStart())}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
import { Separator } from '@documenso/ui/primitives/separator';
|
import { Separator } from '@documenso/ui/primitives/separator';
|
||||||
|
|
||||||
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
|
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
|
||||||
|
import { useEmbedSigningContext } from '~/components/embed/embed-signing-context';
|
||||||
import { BrandingLogo } from '~/components/general/branding-logo';
|
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||||
|
|
||||||
import { BrandingLogoIcon } from '../branding-logo-icon';
|
import { BrandingLogoIcon } from '../branding-logo-icon';
|
||||||
@ -28,7 +29,7 @@ export const EnvelopeSignerHeader = () => {
|
|||||||
useRequiredEnvelopeSigningContext();
|
useRequiredEnvelopeSigningContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="bg-background border-border max-w-screen flex flex-row justify-between border-b px-4 py-3 md:px-6">
|
<nav className="embed--DocumentWidgetHeader bg-background border-border max-w-screen flex flex-row justify-between border-b px-4 py-3 md:px-6">
|
||||||
{/* Left side - Logo and title */}
|
{/* Left side - Logo and title */}
|
||||||
<div className="flex min-w-0 flex-1 items-center space-x-2 md:w-auto md:flex-none">
|
<div className="flex min-w-0 flex-1 items-center space-x-2 md:w-auto md:flex-none">
|
||||||
<Link to="/" className="flex-shrink-0">
|
<Link to="/" className="flex-shrink-0">
|
||||||
@ -72,7 +73,7 @@ export const EnvelopeSignerHeader = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right side - Desktop content */}
|
{/* Right side - Desktop content */}
|
||||||
<div className="hidden items-center space-x-2 md:flex">
|
<div className="hidden items-center space-x-2 lg:flex">
|
||||||
<p className="text-muted-foreground mr-2 flex-shrink-0 text-sm">
|
<p className="text-muted-foreground mr-2 flex-shrink-0 text-sm">
|
||||||
<Plural
|
<Plural
|
||||||
one="1 Field Remaining"
|
one="1 Field Remaining"
|
||||||
@ -85,7 +86,7 @@ export const EnvelopeSignerHeader = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mobile Actions button */}
|
{/* Mobile Actions button */}
|
||||||
<div className="flex-shrink-0 md:hidden">
|
<div className="flex-shrink-0 lg:hidden">
|
||||||
<MobileDropdownMenu />
|
<MobileDropdownMenu />
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@ -95,6 +96,8 @@ export const EnvelopeSignerHeader = () => {
|
|||||||
const MobileDropdownMenu = () => {
|
const MobileDropdownMenu = () => {
|
||||||
const { envelope, recipient } = useRequiredEnvelopeSigningContext();
|
const { envelope, recipient } = useRequiredEnvelopeSigningContext();
|
||||||
|
|
||||||
|
const { allowDocumentRejection } = useEmbedSigningContext() || {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@ -119,7 +122,7 @@ const MobileDropdownMenu = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{envelope.type === EnvelopeType.DOCUMENT && (
|
{envelope.type === EnvelopeType.DOCUMENT && allowDocumentRejection !== false && (
|
||||||
<DocumentSigningRejectDialog
|
<DocumentSigningRejectDialog
|
||||||
documentId={mapSecondaryIdToDocumentId(envelope.secondaryId)}
|
documentId={mapSecondaryIdToDocumentId(envelope.secondaryId)}
|
||||||
token={recipient.token}
|
token={recipient.token}
|
||||||
|
|||||||
@ -1,7 +1,14 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { Trans, useLingui } from '@lingui/react/macro';
|
import { Trans, useLingui } from '@lingui/react/macro';
|
||||||
import { type Field, FieldType, RecipientRole, type Signature } from '@prisma/client';
|
import {
|
||||||
|
type Field,
|
||||||
|
FieldType,
|
||||||
|
type Recipient,
|
||||||
|
RecipientRole,
|
||||||
|
type Signature,
|
||||||
|
SigningStatus,
|
||||||
|
} from '@prisma/client';
|
||||||
import type Konva from 'konva';
|
import type Konva from 'konva';
|
||||||
import type { KonvaEventObject } from 'konva/lib/Node';
|
import type { KonvaEventObject } from 'konva/lib/Node';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
@ -10,7 +17,9 @@ 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 { isBase64Image } from '@documenso/lib/constants/signatures';
|
||||||
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
|
import type { TEnvelope } from '@documenso/lib/types/envelope';
|
||||||
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';
|
||||||
@ -18,10 +27,12 @@ import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields
|
|||||||
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 type { TSignEnvelopeFieldValue } from '@documenso/trpc/server/envelope-router/sign-envelope-field.types';
|
||||||
|
import { EnvelopeRecipientFieldTooltip } from '@documenso/ui/components/document/envelope-recipient-field-tooltip';
|
||||||
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 { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { useEmbedSigningContext } from '~/components/embed/embed-signing-context';
|
||||||
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';
|
||||||
import { handleEmailFieldClick } from '~/utils/field-signing/email-field';
|
import { handleEmailFieldClick } from '~/utils/field-signing/email-field';
|
||||||
@ -34,6 +45,10 @@ import { handleTextFieldClick } from '~/utils/field-signing/text-field';
|
|||||||
import { useRequiredDocumentSigningAuthContext } from '../document-signing/document-signing-auth-provider';
|
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';
|
||||||
|
|
||||||
|
type GenericLocalField = TEnvelope['fields'][number] & {
|
||||||
|
recipient: Pick<Recipient, 'id' | 'name' | 'email' | 'signingStatus'>;
|
||||||
|
};
|
||||||
|
|
||||||
export default function EnvelopeSignerPageRenderer() {
|
export default function EnvelopeSignerPageRenderer() {
|
||||||
const { t, i18n } = useLingui();
|
const { t, i18n } = useLingui();
|
||||||
const { currentEnvelopeItem, setRenderError } = useCurrentEnvelopeRender();
|
const { currentEnvelopeItem, setRenderError } = useCurrentEnvelopeRender();
|
||||||
@ -60,6 +75,8 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
isDirectTemplate,
|
isDirectTemplate,
|
||||||
} = useRequiredEnvelopeSigningContext();
|
} = useRequiredEnvelopeSigningContext();
|
||||||
|
|
||||||
|
const { onFieldSigned, onFieldUnsigned } = useEmbedSigningContext() || {};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
stage,
|
stage,
|
||||||
pageLayer,
|
pageLayer,
|
||||||
@ -87,6 +104,36 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
);
|
);
|
||||||
}, [recipientFields, selectedAssistantRecipientFields, pageContext.pageNumber]);
|
}, [recipientFields, selectedAssistantRecipientFields, pageContext.pageNumber]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns fields that have been fully signed by other recipients for this specific
|
||||||
|
* page.
|
||||||
|
*/
|
||||||
|
const localPageOtherRecipientFields = useMemo((): GenericLocalField[] => {
|
||||||
|
const signedRecipients = envelope.recipients.filter(
|
||||||
|
(recipient) => recipient.signingStatus === SigningStatus.SIGNED,
|
||||||
|
);
|
||||||
|
|
||||||
|
return signedRecipients.flatMap((recipient) => {
|
||||||
|
return recipient.fields
|
||||||
|
.filter(
|
||||||
|
(field) =>
|
||||||
|
field.page === pageContext.pageNumber &&
|
||||||
|
field.envelopeItemId === currentEnvelopeItem?.id &&
|
||||||
|
(field.inserted || field.fieldMeta?.readOnly),
|
||||||
|
)
|
||||||
|
.map((field) => ({
|
||||||
|
...field,
|
||||||
|
recipient: {
|
||||||
|
id: recipient.id,
|
||||||
|
name: recipient.name,
|
||||||
|
email: recipient.email,
|
||||||
|
signingStatus: recipient.signingStatus,
|
||||||
|
role: recipient.role,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}, [envelope.recipients, pageContext.pageNumber]);
|
||||||
|
|
||||||
const unsafeRenderFieldOnLayer = (unparsedField: Field & { signature?: Signature | null }) => {
|
const unsafeRenderFieldOnLayer = (unparsedField: Field & { signature?: Signature | null }) => {
|
||||||
if (!pageLayer.current) {
|
if (!pageLayer.current) {
|
||||||
console.error('Layer not loaded yet');
|
console.error('Layer not loaded yet');
|
||||||
@ -372,13 +419,65 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderFields = () => {
|
||||||
|
if (!pageLayer.current) {
|
||||||
|
console.error('Layer not loaded yet');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render current recipient fields.
|
||||||
|
for (const field of localPageFields) {
|
||||||
|
renderFieldOnLayer(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render other recipient signed and inserted fields.
|
||||||
|
for (const field of localPageOtherRecipientFields) {
|
||||||
|
try {
|
||||||
|
renderField({
|
||||||
|
scale,
|
||||||
|
pageLayer: pageLayer.current,
|
||||||
|
field: {
|
||||||
|
renderId: field.id.toString(),
|
||||||
|
...field,
|
||||||
|
width: Number(field.width),
|
||||||
|
height: Number(field.height),
|
||||||
|
positionX: Number(field.positionX),
|
||||||
|
positionY: Number(field.positionY),
|
||||||
|
fieldMeta: field.fieldMeta,
|
||||||
|
},
|
||||||
|
translations: getClientSideFieldTranslations(i18n),
|
||||||
|
pageWidth: unscaledViewport.width,
|
||||||
|
pageHeight: unscaledViewport.height,
|
||||||
|
color: 'readOnly',
|
||||||
|
editable: false,
|
||||||
|
mode: 'sign',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Unable to render one or more fields belonging to other recipients.');
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const signField = async (
|
const signField = async (
|
||||||
fieldId: number,
|
fieldId: number,
|
||||||
payload: TSignEnvelopeFieldValue,
|
payload: TSignEnvelopeFieldValue,
|
||||||
authOptions?: TRecipientActionAuth,
|
authOptions?: TRecipientActionAuth,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await signFieldInternal(fieldId, payload, authOptions);
|
const { inserted } = await signFieldInternal(fieldId, payload, authOptions);
|
||||||
|
|
||||||
|
// ?: The two callbacks below are used within the embedding context
|
||||||
|
if (inserted && onFieldSigned) {
|
||||||
|
const value = payload.value ? JSON.stringify(payload.value) : undefined;
|
||||||
|
const isBase64 = value ? isBase64Image(value) : undefined;
|
||||||
|
|
||||||
|
onFieldSigned({ fieldId, value, isBase64 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inserted && onFieldUnsigned) {
|
||||||
|
onFieldUnsigned({ fieldId });
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
@ -396,11 +495,7 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
* Initialize the Konva page canvas and all fields and interactions.
|
* Initialize the Konva page canvas and all fields and interactions.
|
||||||
*/
|
*/
|
||||||
const createPageCanvas = (currentStage: Konva.Stage, currentPageLayer: Konva.Layer) => {
|
const createPageCanvas = (currentStage: Konva.Stage, currentPageLayer: Konva.Layer) => {
|
||||||
// Render the fields.
|
renderFields();
|
||||||
for (const field of localPageFields) {
|
|
||||||
renderFieldOnLayer(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentPageLayer.batchDraw();
|
currentPageLayer.batchDraw();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -412,10 +507,7 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
localPageFields.forEach((field) => {
|
renderFields();
|
||||||
console.log('Field changed/inserted, rendering on canvas');
|
|
||||||
renderFieldOnLayer(field);
|
|
||||||
});
|
|
||||||
|
|
||||||
pageLayer.current.batchDraw();
|
pageLayer.current.batchDraw();
|
||||||
}, [localPageFields, showPendingFieldTooltip, fullName, signature, email]);
|
}, [localPageFields, showPendingFieldTooltip, fullName, signature, email]);
|
||||||
@ -431,9 +523,7 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
// Rerender the whole page.
|
// Rerender the whole page.
|
||||||
pageLayer.current.destroyChildren();
|
pageLayer.current.destroyChildren();
|
||||||
|
|
||||||
localPageFields.forEach((field) => {
|
renderFields();
|
||||||
renderFieldOnLayer(field);
|
|
||||||
});
|
|
||||||
|
|
||||||
pageLayer.current.batchDraw();
|
pageLayer.current.batchDraw();
|
||||||
}, [selectedAssistantRecipient]);
|
}, [selectedAssistantRecipient]);
|
||||||
@ -460,6 +550,15 @@ export default function EnvelopeSignerPageRenderer() {
|
|||||||
</EnvelopeFieldToolTip>
|
</EnvelopeFieldToolTip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{localPageOtherRecipientFields.map((field) => (
|
||||||
|
<EnvelopeRecipientFieldTooltip
|
||||||
|
key={field.id}
|
||||||
|
field={field}
|
||||||
|
showFieldStatus={true}
|
||||||
|
showRecipientTooltip={true}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
{/* The element Konva will inject it's canvas into. */}
|
{/* The element Konva will inject it's canvas into. */}
|
||||||
<div className="konva-container absolute inset-0 z-10 w-full" ref={konvaContainer}></div>
|
<div className="konva-container absolute inset-0 z-10 w-full" ref={konvaContainer}></div>
|
||||||
|
|
||||||
|
|||||||
@ -2,16 +2,19 @@ import { useMemo } from 'react';
|
|||||||
|
|
||||||
import { useLingui } from '@lingui/react/macro';
|
import { useLingui } from '@lingui/react/macro';
|
||||||
import { FieldType } from '@prisma/client';
|
import { FieldType } from '@prisma/client';
|
||||||
import { useNavigate, useSearchParams } from 'react-router';
|
import { useNavigate, useRevalidator, useSearchParams } from 'react-router';
|
||||||
|
|
||||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||||
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
import { useCurrentEnvelopeRender } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||||
import { isBase64Image } from '@documenso/lib/constants/signatures';
|
import { isBase64Image } from '@documenso/lib/constants/signatures';
|
||||||
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
import type { TRecipientAccessAuth } from '@documenso/lib/types/document-auth';
|
import type { TRecipientAccessAuth } from '@documenso/lib/types/document-auth';
|
||||||
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { useEmbedSigningContext } from '~/components/embed/embed-signing-context';
|
||||||
|
|
||||||
import { DocumentSigningCompleteDialog } from '../document-signing/document-signing-complete-dialog';
|
import { DocumentSigningCompleteDialog } from '../document-signing/document-signing-complete-dialog';
|
||||||
import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider';
|
import { useRequiredEnvelopeSigningContext } from '../document-signing/envelope-signing-provider';
|
||||||
|
|
||||||
@ -19,8 +22,9 @@ export const EnvelopeSignerCompleteDialog = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const analytics = useAnalytics();
|
const analytics = useAnalytics();
|
||||||
|
|
||||||
const { toast } = useToast();
|
|
||||||
const { t } = useLingui();
|
const { t } = useLingui();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const { revalidate } = useRevalidator();
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
@ -37,6 +41,8 @@ export const EnvelopeSignerCompleteDialog = () => {
|
|||||||
|
|
||||||
const { currentEnvelopeItem, setCurrentEnvelopeItem } = useCurrentEnvelopeRender();
|
const { currentEnvelopeItem, setCurrentEnvelopeItem } = useCurrentEnvelopeRender();
|
||||||
|
|
||||||
|
const { onDocumentCompleted, onDocumentError } = useEmbedSigningContext() || {};
|
||||||
|
|
||||||
const { mutateAsync: completeDocument, isPending } =
|
const { mutateAsync: completeDocument, isPending } =
|
||||||
trpc.recipient.completeDocumentWithToken.useMutation();
|
trpc.recipient.completeDocumentWithToken.useMutation();
|
||||||
|
|
||||||
@ -68,25 +74,54 @@ export const EnvelopeSignerCompleteDialog = () => {
|
|||||||
nextSigner?: { name: string; email: string },
|
nextSigner?: { name: string; email: string },
|
||||||
accessAuthOptions?: TRecipientAccessAuth,
|
accessAuthOptions?: TRecipientAccessAuth,
|
||||||
) => {
|
) => {
|
||||||
const payload = {
|
try {
|
||||||
token: recipient.token,
|
const payload = {
|
||||||
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
token: recipient.token,
|
||||||
authOptions: accessAuthOptions,
|
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
||||||
...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}),
|
authOptions: accessAuthOptions,
|
||||||
};
|
...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
await completeDocument(payload);
|
await completeDocument(payload);
|
||||||
|
|
||||||
analytics.capture('App: Recipient has completed signing', {
|
analytics.capture('App: Recipient has completed signing', {
|
||||||
signerId: recipient.id,
|
signerId: recipient.id,
|
||||||
documentId: envelope.id,
|
documentId: envelope.id,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (envelope.documentMeta.redirectUrl) {
|
if (onDocumentCompleted) {
|
||||||
window.location.href = envelope.documentMeta.redirectUrl;
|
onDocumentCompleted({
|
||||||
} else {
|
token: recipient.token,
|
||||||
await navigate(`/sign/${recipient.token}/complete`);
|
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
||||||
|
recipientId: recipient.id,
|
||||||
|
envelopeId: envelope.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await revalidate();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (envelope.documentMeta.redirectUrl) {
|
||||||
|
window.location.href = envelope.documentMeta.redirectUrl;
|
||||||
|
} else {
|
||||||
|
await navigate(`/sign/${recipient.token}/complete`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
|
if (error.code !== AppErrorCode.TWO_FACTOR_AUTH_FAILED) {
|
||||||
|
toast({
|
||||||
|
title: t`Something went wrong`,
|
||||||
|
description: t`We were unable to submit this document at this time. Please try again later.`,
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
|
||||||
|
onDocumentError?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -105,8 +140,12 @@ export const EnvelopeSignerCompleteDialog = () => {
|
|||||||
directTemplateExternalId = decodeURIComponent(directTemplateExternalId);
|
directTemplateExternalId = decodeURIComponent(directTemplateExternalId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!recipient.directToken) {
|
||||||
|
throw new Error('Recipient direct token is required');
|
||||||
|
}
|
||||||
|
|
||||||
const { token } = await createDocumentFromDirectTemplate({
|
const { token } = await createDocumentFromDirectTemplate({
|
||||||
directTemplateToken: recipient.token, // The direct template token is inserted into the recipient token for ease of use.
|
directTemplateToken: recipient.directToken, // The direct template token is inserted into the recipient token for ease of use.
|
||||||
directTemplateExternalId,
|
directTemplateExternalId,
|
||||||
directRecipientName: recipientDetails?.name || fullName,
|
directRecipientName: recipientDetails?.name || fullName,
|
||||||
directRecipientEmail: recipientDetails?.email || email,
|
directRecipientEmail: recipientDetails?.email || email,
|
||||||
@ -132,18 +171,31 @@ export const EnvelopeSignerCompleteDialog = () => {
|
|||||||
|
|
||||||
const redirectUrl = envelope.documentMeta.redirectUrl;
|
const redirectUrl = envelope.documentMeta.redirectUrl;
|
||||||
|
|
||||||
|
if (onDocumentCompleted) {
|
||||||
|
await navigate({
|
||||||
|
pathname: `/embed/sign/${token}`,
|
||||||
|
search: window.location.search,
|
||||||
|
hash: window.location.hash,
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (redirectUrl) {
|
if (redirectUrl) {
|
||||||
window.location.href = redirectUrl;
|
window.location.href = redirectUrl;
|
||||||
} else {
|
} else {
|
||||||
await navigate(`/sign/${token}/complete`);
|
await navigate(`/sign/${token}/complete`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
console.log('err', err);
|
||||||
toast({
|
toast({
|
||||||
title: t`Something went wrong`,
|
title: t`Something went wrong`,
|
||||||
description: t`We were unable to submit this document at this time. Please try again later.`,
|
description: t`We were unable to submit this document at this time. Please try again later.`,
|
||||||
variant: 'destructive',
|
variant: 'destructive',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onDocumentError?.();
|
||||||
|
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,49 +0,0 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
|
||||||
import { Download } from 'lucide-react';
|
|
||||||
|
|
||||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
|
||||||
import type { DocumentData } from '@documenso/prisma/client';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
export type ShareDocumentDownloadButtonProps = {
|
|
||||||
title: string;
|
|
||||||
documentData: DocumentData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ShareDocumentDownloadButton = ({
|
|
||||||
title,
|
|
||||||
documentData,
|
|
||||||
}: ShareDocumentDownloadButtonProps) => {
|
|
||||||
const { _ } = useLingui();
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
|
||||||
|
|
||||||
const onDownloadClick = async () => {
|
|
||||||
try {
|
|
||||||
setIsDownloading(true);
|
|
||||||
|
|
||||||
await downloadPDF({ documentData, fileName: title });
|
|
||||||
} catch (err) {
|
|
||||||
toast({
|
|
||||||
title: _(msg`Something went wrong`),
|
|
||||||
description: _(msg`An error occurred while downloading your document.`),
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setIsDownloading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button loading={isDownloading} onClick={onDownloadClick}>
|
|
||||||
{!isDownloading && <Download className="mr-2 h-4 w-4" />}
|
|
||||||
<Trans>Download</Trans>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@ -313,8 +313,10 @@ export const TemplateEditForm = ({
|
|||||||
>
|
>
|
||||||
<CardContent className="p-2">
|
<CardContent className="p-2">
|
||||||
<PDFViewer
|
<PDFViewer
|
||||||
key={templateDocumentData.id}
|
key={template.envelopeItems[0].id}
|
||||||
documentData={templateDocumentData}
|
envelopeItem={template.envelopeItems[0]}
|
||||||
|
token={undefined}
|
||||||
|
version="signed"
|
||||||
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
|
onDocumentLoad={() => setIsDocumentPdfLoaded(true)}
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@ -7,11 +7,13 @@ import type { User } from '@prisma/client';
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
|
import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted';
|
||||||
|
import { mapSecondaryIdToTemplateId } from '@documenso/lib/utils/envelope';
|
||||||
|
|
||||||
export type TemplatePageViewInformationProps = {
|
export type TemplatePageViewInformationProps = {
|
||||||
userId: number;
|
userId: number;
|
||||||
template: {
|
template: {
|
||||||
userId: number;
|
userId: number;
|
||||||
|
secondaryId: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
user: Pick<User, 'id' | 'name' | 'email'>;
|
user: Pick<User, 'id' | 'name' | 'email'>;
|
||||||
@ -43,6 +45,10 @@ export const TemplatePageViewInformation = ({
|
|||||||
.setLocale(i18n.locales?.[0] || i18n.locale)
|
.setLocale(i18n.locales?.[0] || i18n.locale)
|
||||||
.toRelative(),
|
.toRelative(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
description: msg`Template ID (Legacy)`,
|
||||||
|
value: mapSecondaryIdToTemplateId(template.secondaryId),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [isMounted, template, userId]);
|
}, [isMounted, template, userId]);
|
||||||
|
|||||||
@ -1,19 +1,14 @@
|
|||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||||
import { CheckCircle, Download, Edit, EyeIcon, Pencil } from 'lucide-react';
|
import { CheckCircle, Download, Edit, EyeIcon, Pencil } from 'lucide-react';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
|
||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document';
|
import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document';
|
||||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
import { useCurrentTeam } from '~/providers/team';
|
import { useCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
@ -25,8 +20,6 @@ export type DocumentsTableActionButtonProps = {
|
|||||||
|
|
||||||
export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonProps) => {
|
export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonProps) => {
|
||||||
const { user } = useSession();
|
const { user } = useSession();
|
||||||
const { toast } = useToast();
|
|
||||||
const { _ } = useLingui();
|
|
||||||
|
|
||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
|
|
||||||
@ -44,39 +37,6 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr
|
|||||||
const documentsPath = formatDocumentsPath(team.url);
|
const documentsPath = formatDocumentsPath(team.url);
|
||||||
const formatPath = `${documentsPath}/${row.envelopeId}/edit`;
|
const formatPath = `${documentsPath}/${row.envelopeId}/edit`;
|
||||||
|
|
||||||
const onDownloadClick = async () => {
|
|
||||||
try {
|
|
||||||
const document = !recipient
|
|
||||||
? await trpcClient.document.get.query(
|
|
||||||
{
|
|
||||||
documentId: row.id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
context: {
|
|
||||||
teamId: team?.id?.toString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: await trpcClient.document.getDocumentByToken.query({
|
|
||||||
token: recipient.token,
|
|
||||||
});
|
|
||||||
|
|
||||||
const documentData = document?.documentData;
|
|
||||||
|
|
||||||
if (!documentData) {
|
|
||||||
throw Error('No document available');
|
|
||||||
}
|
|
||||||
|
|
||||||
await downloadPDF({ documentData, fileName: row.title });
|
|
||||||
} catch (err) {
|
|
||||||
toast({
|
|
||||||
title: _(msg`Something went wrong`),
|
|
||||||
description: _(msg`An error occurred while downloading your document.`),
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Consider if want to keep this logic for hiding viewing for CC'ers
|
// TODO: Consider if want to keep this logic for hiding viewing for CC'ers
|
||||||
if (recipient?.role === RecipientRole.CC && isComplete === false) {
|
if (recipient?.role === RecipientRole.CC && isComplete === false) {
|
||||||
return null;
|
return null;
|
||||||
@ -134,7 +94,7 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr
|
|||||||
<Trans>View</Trans>
|
<Trans>View</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
))
|
))
|
||||||
.with({ isComplete: true, internalVersion: 2 }, () => (
|
.with({ isComplete: true }, () => (
|
||||||
<EnvelopeDownloadDialog
|
<EnvelopeDownloadDialog
|
||||||
envelopeId={row.envelopeId}
|
envelopeId={row.envelopeId}
|
||||||
envelopeStatus={row.status}
|
envelopeStatus={row.status}
|
||||||
@ -147,11 +107,5 @@ export const DocumentsTableActionButton = ({ row }: DocumentsTableActionButtonPr
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.with({ isComplete: true }, () => (
|
|
||||||
<Button className="w-32" onClick={onDownloadClick}>
|
|
||||||
<Download className="-ml-1 mr-2 inline h-4 w-4" />
|
|
||||||
<Trans>Download</Trans>
|
|
||||||
</Button>
|
|
||||||
))
|
|
||||||
.otherwise(() => <div></div>);
|
.otherwise(() => <div></div>);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
Edit,
|
Edit,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
FileDown,
|
|
||||||
FolderInput,
|
FolderInput,
|
||||||
Loader,
|
Loader,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
@ -20,12 +19,10 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
|
|
||||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
|
||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document';
|
import type { TDocumentMany as TDocumentRow } from '@documenso/lib/types/document';
|
||||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
|
||||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
@ -34,7 +31,6 @@ import {
|
|||||||
DropdownMenuLabel,
|
DropdownMenuLabel,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@documenso/ui/primitives/dropdown-menu';
|
} from '@documenso/ui/primitives/dropdown-menu';
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
||||||
|
|
||||||
import { DocumentDeleteDialog } from '~/components/dialogs/document-delete-dialog';
|
import { DocumentDeleteDialog } from '~/components/dialogs/document-delete-dialog';
|
||||||
import { DocumentDuplicateDialog } from '~/components/dialogs/document-duplicate-dialog';
|
import { DocumentDuplicateDialog } from '~/components/dialogs/document-duplicate-dialog';
|
||||||
@ -56,7 +52,6 @@ export const DocumentsTableActionDropdown = ({
|
|||||||
const { user } = useSession();
|
const { user } = useSession();
|
||||||
const team = useCurrentTeam();
|
const team = useCurrentTeam();
|
||||||
|
|
||||||
const { toast } = useToast();
|
|
||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
@ -76,58 +71,6 @@ export const DocumentsTableActionDropdown = ({
|
|||||||
const documentsPath = formatDocumentsPath(team.url);
|
const documentsPath = formatDocumentsPath(team.url);
|
||||||
const formatPath = `${documentsPath}/${row.envelopeId}/edit`;
|
const formatPath = `${documentsPath}/${row.envelopeId}/edit`;
|
||||||
|
|
||||||
const onDownloadClick = async () => {
|
|
||||||
try {
|
|
||||||
const document = !recipient
|
|
||||||
? await trpcClient.document.get.query({
|
|
||||||
documentId: row.id,
|
|
||||||
})
|
|
||||||
: await trpcClient.document.getDocumentByToken.query({
|
|
||||||
token: recipient.token,
|
|
||||||
});
|
|
||||||
|
|
||||||
const documentData = document?.documentData;
|
|
||||||
|
|
||||||
if (!documentData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await downloadPDF({ documentData, fileName: row.title });
|
|
||||||
} catch (err) {
|
|
||||||
toast({
|
|
||||||
title: _(msg`Something went wrong`),
|
|
||||||
description: _(msg`An error occurred while downloading your document.`),
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDownloadOriginalClick = async () => {
|
|
||||||
try {
|
|
||||||
const document = !recipient
|
|
||||||
? await trpcClient.document.get.query({
|
|
||||||
documentId: row.id,
|
|
||||||
})
|
|
||||||
: await trpcClient.document.getDocumentByToken.query({
|
|
||||||
token: recipient.token,
|
|
||||||
});
|
|
||||||
|
|
||||||
const documentData = document?.documentData;
|
|
||||||
|
|
||||||
if (!documentData) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await downloadPDF({ documentData, fileName: row.title, version: 'original' });
|
|
||||||
} catch (err) {
|
|
||||||
toast({
|
|
||||||
title: _(msg`Something went wrong`),
|
|
||||||
description: _(msg`An error occurred while downloading your document.`),
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const nonSignedRecipients = row.recipients.filter((item) => item.signingStatus !== 'SIGNED');
|
const nonSignedRecipients = row.recipients.filter((item) => item.signingStatus !== 'SIGNED');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -178,33 +121,19 @@ export const DocumentsTableActionDropdown = ({
|
|||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
{row.internalVersion === 2 ? (
|
<EnvelopeDownloadDialog
|
||||||
<EnvelopeDownloadDialog
|
envelopeId={row.envelopeId}
|
||||||
envelopeId={row.envelopeId}
|
envelopeStatus={row.status}
|
||||||
envelopeStatus={row.status}
|
token={recipient?.token}
|
||||||
token={recipient?.token}
|
trigger={
|
||||||
trigger={
|
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
|
||||||
<DropdownMenuItem asChild onSelect={(e) => e.preventDefault()}>
|
<div>
|
||||||
<div>
|
<Download className="mr-2 h-4 w-4" />
|
||||||
<Download className="mr-2 h-4 w-4" />
|
<Trans>Download</Trans>
|
||||||
<Trans>Download</Trans>
|
</div>
|
||||||
</div>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<DropdownMenuItem disabled={!isComplete} onClick={onDownloadClick}>
|
|
||||||
<Download className="mr-2 h-4 w-4" />
|
|
||||||
<Trans>Download</Trans>
|
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
}
|
||||||
<DropdownMenuItem onClick={onDownloadOriginalClick}>
|
/>
|
||||||
<FileDown className="mr-2 h-4 w-4" />
|
|
||||||
<Trans>Download Original</Trans>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DropdownMenuItem onClick={() => setDuplicateDialogOpen(true)}>
|
<DropdownMenuItem onClick={() => setDuplicateDialogOpen(true)}>
|
||||||
<Copy className="mr-2 h-4 w-4" />
|
<Copy className="mr-2 h-4 w-4" />
|
||||||
@ -273,7 +202,8 @@ export const DocumentsTableActionDropdown = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<DocumentDuplicateDialog
|
<DocumentDuplicateDialog
|
||||||
id={row.id}
|
id={row.envelopeId}
|
||||||
|
token={recipient?.token}
|
||||||
open={isDuplicateDialogOpen}
|
open={isDuplicateDialogOpen}
|
||||||
onOpenChange={setDuplicateDialogOpen}
|
onOpenChange={setDuplicateDialogOpen}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -10,11 +10,9 @@ import { DateTime } from 'luxon';
|
|||||||
import { Link, useSearchParams } from 'react-router';
|
import { Link, useSearchParams } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
|
||||||
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params';
|
||||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import type { TFindInboxResponse } from '@documenso/trpc/server/document-router/find-inbox.types';
|
import type { TFindInboxResponse } from '@documenso/trpc/server/document-router/find-inbox.types';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -28,6 +26,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
import { DocumentStatus } from '~/components/general/document/document-status';
|
import { DocumentStatus } from '~/components/general/document/document-status';
|
||||||
import { useOptionalCurrentTeam } from '~/providers/team';
|
import { useOptionalCurrentTeam } from '~/providers/team';
|
||||||
|
|
||||||
|
import { EnvelopeDownloadDialog } from '../dialogs/envelope-download-dialog';
|
||||||
import { StackAvatarsWithTooltip } from '../general/stack-avatars-with-tooltip';
|
import { StackAvatarsWithTooltip } from '../general/stack-avatars-with-tooltip';
|
||||||
|
|
||||||
export type DocumentsTableProps = {
|
export type DocumentsTableProps = {
|
||||||
@ -199,28 +198,6 @@ export const InboxTableActionButton = ({ row }: InboxTableActionButtonProps) =>
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onDownloadClick = async () => {
|
|
||||||
try {
|
|
||||||
const document = await trpcClient.document.getDocumentByToken.query({
|
|
||||||
token: recipient.token,
|
|
||||||
});
|
|
||||||
|
|
||||||
const documentData = document?.documentData;
|
|
||||||
|
|
||||||
if (!documentData) {
|
|
||||||
throw Error('No document available');
|
|
||||||
}
|
|
||||||
|
|
||||||
await downloadPDF({ documentData, fileName: row.title });
|
|
||||||
} catch (err) {
|
|
||||||
toast({
|
|
||||||
title: _(msg`Something went wrong`),
|
|
||||||
description: _(msg`An error occurred while downloading your document.`),
|
|
||||||
variant: 'destructive',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Consider if want to keep this logic for hiding viewing for CC'ers
|
// TODO: Consider if want to keep this logic for hiding viewing for CC'ers
|
||||||
if (recipient?.role === RecipientRole.CC && isComplete === false) {
|
if (recipient?.role === RecipientRole.CC && isComplete === false) {
|
||||||
return null;
|
return null;
|
||||||
@ -230,6 +207,7 @@ export const InboxTableActionButton = ({ row }: InboxTableActionButtonProps) =>
|
|||||||
isPending,
|
isPending,
|
||||||
isComplete,
|
isComplete,
|
||||||
isSigned,
|
isSigned,
|
||||||
|
internalVersion: row.internalVersion,
|
||||||
})
|
})
|
||||||
.with({ isPending: true, isSigned: false }, () => (
|
.with({ isPending: true, isSigned: false }, () => (
|
||||||
<Button className="w-32" asChild>
|
<Button className="w-32" asChild>
|
||||||
@ -263,10 +241,17 @@ export const InboxTableActionButton = ({ row }: InboxTableActionButtonProps) =>
|
|||||||
</Button>
|
</Button>
|
||||||
))
|
))
|
||||||
.with({ isComplete: true }, () => (
|
.with({ isComplete: true }, () => (
|
||||||
<Button className="w-32" onClick={onDownloadClick}>
|
<EnvelopeDownloadDialog
|
||||||
<DownloadIcon className="-ml-1 mr-2 inline h-4 w-4" />
|
envelopeId={row.envelopeId}
|
||||||
<Trans>Download</Trans>
|
envelopeStatus={row.status}
|
||||||
</Button>
|
token={recipient?.token}
|
||||||
|
trigger={
|
||||||
|
<Button className="w-32">
|
||||||
|
<DownloadIcon className="-ml-1 mr-2 inline h-4 w-4" />
|
||||||
|
<Trans>Download</Trans>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
.otherwise(() => <div></div>);
|
.otherwise(() => <div></div>);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -147,8 +147,13 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
|
|||||||
<div className="relative col-span-12 lg:col-span-6 xl:col-span-7">
|
<div className="relative col-span-12 lg:col-span-6 xl:col-span-7">
|
||||||
<EnvelopeRenderProvider
|
<EnvelopeRenderProvider
|
||||||
envelope={envelope}
|
envelope={envelope}
|
||||||
fields={envelope.status == DocumentStatus.COMPLETED ? [] : envelope.fields}
|
token={undefined}
|
||||||
recipientIds={envelope.recipients.map((recipient) => recipient.id)}
|
fields={envelope.fields}
|
||||||
|
recipients={envelope.recipients}
|
||||||
|
overrideSettings={{
|
||||||
|
showRecipientSigningStatus: true,
|
||||||
|
showRecipientTooltip: true,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{isMultiEnvelopeItem && (
|
{isMultiEnvelopeItem && (
|
||||||
<EnvelopeRendererFileSelector fields={envelope.fields} className="mb-4 p-0" />
|
<EnvelopeRendererFileSelector fields={envelope.fields} className="mb-4 p-0" />
|
||||||
@ -181,9 +186,10 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<PDFViewer
|
<PDFViewer
|
||||||
document={envelope}
|
envelopeItem={envelope.envelopeItems[0]}
|
||||||
|
token={undefined}
|
||||||
key={envelope.envelopeItems[0].id}
|
key={envelope.envelopeItems[0].id}
|
||||||
documentData={envelope.envelopeItems[0].documentData}
|
version="signed"
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -101,8 +101,9 @@ export default function EnvelopeEditorPage({ params }: Route.ComponentProps) {
|
|||||||
<EnvelopeEditorProvider initialEnvelope={envelope}>
|
<EnvelopeEditorProvider initialEnvelope={envelope}>
|
||||||
<EnvelopeRenderProvider
|
<EnvelopeRenderProvider
|
||||||
envelope={envelope}
|
envelope={envelope}
|
||||||
|
token={undefined}
|
||||||
fields={envelope.fields}
|
fields={envelope.fields}
|
||||||
recipientIds={envelope.recipients.map((recipient) => recipient.id)}
|
recipients={envelope.recipients}
|
||||||
>
|
>
|
||||||
<EnvelopeEditor />
|
<EnvelopeEditor />
|
||||||
</EnvelopeRenderProvider>
|
</EnvelopeRenderProvider>
|
||||||
|
|||||||
@ -170,8 +170,12 @@ export default function TemplatePage({ params }: Route.ComponentProps) {
|
|||||||
<div className="relative col-span-12 lg:col-span-6 xl:col-span-7">
|
<div className="relative col-span-12 lg:col-span-6 xl:col-span-7">
|
||||||
<EnvelopeRenderProvider
|
<EnvelopeRenderProvider
|
||||||
envelope={envelope}
|
envelope={envelope}
|
||||||
|
token={undefined}
|
||||||
fields={envelope.fields}
|
fields={envelope.fields}
|
||||||
recipientIds={envelope.recipients.map((recipient) => recipient.id)}
|
recipients={envelope.recipients}
|
||||||
|
overrideSettings={{
|
||||||
|
showRecipientTooltip: true,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{isMultiEnvelopeItem && (
|
{isMultiEnvelopeItem && (
|
||||||
<EnvelopeRendererFileSelector fields={envelope.fields} className="mb-4 p-0" />
|
<EnvelopeRendererFileSelector fields={envelope.fields} className="mb-4 p-0" />
|
||||||
@ -203,9 +207,10 @@ export default function TemplatePage({ params }: Route.ComponentProps) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<PDFViewer
|
<PDFViewer
|
||||||
document={envelope}
|
envelopeItem={envelope.envelopeItems[0]}
|
||||||
|
token={undefined}
|
||||||
|
version="signed"
|
||||||
key={envelope.envelopeItems[0].id}
|
key={envelope.envelopeItems[0].id}
|
||||||
documentData={envelope.envelopeItems[0].documentData}
|
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -184,6 +184,7 @@ const DirectSigningPageV1 = ({ data }: { data: Awaited<ReturnType<typeof handleV
|
|||||||
<DocumentSigningAuthProvider
|
<DocumentSigningAuthProvider
|
||||||
documentAuthOptions={template.authOptions}
|
documentAuthOptions={template.authOptions}
|
||||||
recipient={directTemplateRecipient}
|
recipient={directTemplateRecipient}
|
||||||
|
isDirectTemplate={true}
|
||||||
user={user}
|
user={user}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
@ -245,7 +246,7 @@ const DirectSigningPageV2 = ({ data }: { data: Awaited<ReturnType<typeof handleV
|
|||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
user={user}
|
user={user}
|
||||||
>
|
>
|
||||||
<EnvelopeRenderProvider envelope={envelope}>
|
<EnvelopeRenderProvider envelope={envelope} token={recipient.token}>
|
||||||
<DocumentSigningPageViewV2 />
|
<DocumentSigningPageViewV2 />
|
||||||
</EnvelopeRenderProvider>
|
</EnvelopeRenderProvider>
|
||||||
</DocumentSigningAuthProvider>
|
</DocumentSigningAuthProvider>
|
||||||
|
|||||||
@ -492,7 +492,7 @@ const SigningPageV2 = ({ data }: { data: Awaited<ReturnType<typeof handleV2Loade
|
|||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
user={user}
|
user={user}
|
||||||
>
|
>
|
||||||
<EnvelopeRenderProvider envelope={envelope}>
|
<EnvelopeRenderProvider envelope={envelope} token={recipient.token}>
|
||||||
<DocumentSigningPageViewV2 />
|
<DocumentSigningPageViewV2 />
|
||||||
</EnvelopeRenderProvider>
|
</EnvelopeRenderProvider>
|
||||||
</DocumentSigningAuthProvider>
|
</DocumentSigningAuthProvider>
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { msg } from '@lingui/core/macro';
|
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
|
import { DocumentStatus, FieldType, RecipientRole } from '@prisma/client';
|
||||||
import { CheckCircle2, Clock8, FileSearch } from 'lucide-react';
|
import { CheckCircle2, Clock8, DownloadIcon } from 'lucide-react';
|
||||||
import { Link, useRevalidator } from 'react-router';
|
import { Link, useRevalidator } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
@ -20,14 +19,13 @@ import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-emai
|
|||||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||||
import { env } from '@documenso/lib/utils/env';
|
import { env } from '@documenso/lib/utils/env';
|
||||||
import type { Document } from '@documenso/prisma/types/document-legacy-schema';
|
import type { Document } from '@documenso/prisma/types/document-legacy-schema';
|
||||||
import DocumentDialog from '@documenso/ui/components/document/document-dialog';
|
|
||||||
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
|
||||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||||
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Badge } from '@documenso/ui/primitives/badge';
|
import { Badge } from '@documenso/ui/primitives/badge';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
|
import { EnvelopeDownloadDialog } from '~/components/dialogs/envelope-download-dialog';
|
||||||
import { ClaimAccount } from '~/components/general/claim-account';
|
import { ClaimAccount } from '~/components/general/claim-account';
|
||||||
import { DocumentSigningAuthPageView } from '~/components/general/document-signing/document-signing-auth-page';
|
import { DocumentSigningAuthPageView } from '~/components/general/document-signing/document-signing-auth-page';
|
||||||
|
|
||||||
@ -207,24 +205,16 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
|
|||||||
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
|
<div className="mt-8 flex w-full max-w-sm items-center justify-center gap-4">
|
||||||
<DocumentShareButton documentId={document.id} token={recipient.token} />
|
<DocumentShareButton documentId={document.id} token={recipient.token} />
|
||||||
|
|
||||||
{isDocumentCompleted(document.status) ? (
|
{isDocumentCompleted(document.status) && (
|
||||||
<DocumentDownloadButton
|
<EnvelopeDownloadDialog
|
||||||
className="flex-1"
|
envelopeId={document.envelopeId}
|
||||||
fileName={document.title}
|
envelopeStatus={document.status}
|
||||||
documentData={document.documentData}
|
envelopeItems={document.envelopeItems}
|
||||||
disabled={!isDocumentCompleted(document.status)}
|
token={recipient?.token}
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<DocumentDialog
|
|
||||||
documentData={document.documentData}
|
|
||||||
trigger={
|
trigger={
|
||||||
<Button
|
<Button type="button" variant="outline" className="flex-1">
|
||||||
className="text-[11px]"
|
<DownloadIcon className="mr-2 h-5 w-5" />
|
||||||
title={_(msg`Signatures will appear once the document has been completed`)}
|
<Trans>Download</Trans>
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
<FileSearch className="mr-2 h-5 w-5" strokeWidth={1.7} />
|
|
||||||
<Trans>View Original Document</Trans>
|
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -60,6 +60,7 @@ export const loader = async ({ request, params: { slug } }: Route.LoaderArgs) =>
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
document,
|
document,
|
||||||
|
token: slug,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,7 +75,7 @@ export const loader = async ({ request, params: { slug } }: Route.LoaderArgs) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function SharePage() {
|
export default function SharePage() {
|
||||||
const { document } = useLoaderData<typeof loader>();
|
const { document, token } = useLoaderData<typeof loader>();
|
||||||
|
|
||||||
if (document) {
|
if (document) {
|
||||||
return (
|
return (
|
||||||
@ -86,6 +87,7 @@ export default function SharePage() {
|
|||||||
envelopeItems={document.envelopeItems}
|
envelopeItems={document.envelopeItems}
|
||||||
recipientCount={document.recipientCount}
|
recipientCount={document.recipientCount}
|
||||||
completedDate={document.completedAt ?? undefined}
|
completedDate={document.completedAt ?? undefined}
|
||||||
|
token={token}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import { Link, redirect } from 'react-router';
|
import { Link, redirect } from 'react-router';
|
||||||
|
|
||||||
@ -9,6 +11,7 @@ import {
|
|||||||
OIDC_PROVIDER_LABEL,
|
OIDC_PROVIDER_LABEL,
|
||||||
} from '@documenso/lib/constants/auth';
|
} from '@documenso/lib/constants/auth';
|
||||||
import { env } from '@documenso/lib/utils/env';
|
import { env } from '@documenso/lib/utils/env';
|
||||||
|
import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
|
||||||
|
|
||||||
import { SignInForm } from '~/components/forms/signin';
|
import { SignInForm } from '~/components/forms/signin';
|
||||||
import { appMetaTags } from '~/utils/meta';
|
import { appMetaTags } from '~/utils/meta';
|
||||||
@ -28,8 +31,12 @@ export async function loader({ request }: Route.LoaderArgs) {
|
|||||||
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
|
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
|
||||||
const oidcProviderLabel = OIDC_PROVIDER_LABEL;
|
const oidcProviderLabel = OIDC_PROVIDER_LABEL;
|
||||||
|
|
||||||
|
let returnTo = new URL(request.url).searchParams.get('returnTo') ?? undefined;
|
||||||
|
|
||||||
|
returnTo = isValidReturnTo(returnTo) ? normalizeReturnTo(returnTo) : undefined;
|
||||||
|
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
throw redirect('/');
|
throw redirect(returnTo || '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -37,12 +44,28 @@ export async function loader({ request }: Route.LoaderArgs) {
|
|||||||
isMicrosoftSSOEnabled,
|
isMicrosoftSSOEnabled,
|
||||||
isOIDCSSOEnabled,
|
isOIDCSSOEnabled,
|
||||||
oidcProviderLabel,
|
oidcProviderLabel,
|
||||||
|
returnTo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SignIn({ loaderData }: Route.ComponentProps) {
|
export default function SignIn({ loaderData }: Route.ComponentProps) {
|
||||||
const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, oidcProviderLabel } =
|
const {
|
||||||
loaderData;
|
isGoogleSSOEnabled,
|
||||||
|
isMicrosoftSSOEnabled,
|
||||||
|
isOIDCSSOEnabled,
|
||||||
|
oidcProviderLabel,
|
||||||
|
returnTo,
|
||||||
|
} = loaderData;
|
||||||
|
|
||||||
|
const [isEmbeddedRedirect, setIsEmbeddedRedirect] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const hash = window.location.hash.slice(1);
|
||||||
|
|
||||||
|
const params = new URLSearchParams(hash);
|
||||||
|
|
||||||
|
setIsEmbeddedRedirect(params.get('embedded') === 'true');
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-screen max-w-lg px-4">
|
<div className="w-screen max-w-lg px-4">
|
||||||
@ -61,13 +84,17 @@ export default function SignIn({ loaderData }: Route.ComponentProps) {
|
|||||||
isMicrosoftSSOEnabled={isMicrosoftSSOEnabled}
|
isMicrosoftSSOEnabled={isMicrosoftSSOEnabled}
|
||||||
isOIDCSSOEnabled={isOIDCSSOEnabled}
|
isOIDCSSOEnabled={isOIDCSSOEnabled}
|
||||||
oidcProviderLabel={oidcProviderLabel}
|
oidcProviderLabel={oidcProviderLabel}
|
||||||
|
returnTo={returnTo}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{env('NEXT_PUBLIC_DISABLE_SIGNUP') !== 'true' && (
|
{!isEmbeddedRedirect && env('NEXT_PUBLIC_DISABLE_SIGNUP') !== 'true' && (
|
||||||
<p className="text-muted-foreground mt-6 text-center text-sm">
|
<p className="text-muted-foreground mt-6 text-center text-sm">
|
||||||
<Trans>
|
<Trans>
|
||||||
Don't have an account?{' '}
|
Don't have an account?{' '}
|
||||||
<Link to="/signup" className="text-documenso-700 duration-200 hover:opacity-70">
|
<Link
|
||||||
|
to={returnTo ? `/signup?returnTo=${encodeURIComponent(returnTo)}` : '/signup'}
|
||||||
|
className="text-documenso-700 duration-200 hover:opacity-70"
|
||||||
|
>
|
||||||
Sign up
|
Sign up
|
||||||
</Link>
|
</Link>
|
||||||
</Trans>
|
</Trans>
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
IS_OIDC_SSO_ENABLED,
|
IS_OIDC_SSO_ENABLED,
|
||||||
} from '@documenso/lib/constants/auth';
|
} from '@documenso/lib/constants/auth';
|
||||||
import { env } from '@documenso/lib/utils/env';
|
import { env } from '@documenso/lib/utils/env';
|
||||||
|
import { isValidReturnTo, normalizeReturnTo } from '@documenso/lib/utils/is-valid-return-to';
|
||||||
|
|
||||||
import { SignUpForm } from '~/components/forms/signup';
|
import { SignUpForm } from '~/components/forms/signup';
|
||||||
import { appMetaTags } from '~/utils/meta';
|
import { appMetaTags } from '~/utils/meta';
|
||||||
@ -16,7 +17,7 @@ export function meta() {
|
|||||||
return appMetaTags('Sign Up');
|
return appMetaTags('Sign Up');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loader() {
|
export function loader({ request }: Route.LoaderArgs) {
|
||||||
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
|
const NEXT_PUBLIC_DISABLE_SIGNUP = env('NEXT_PUBLIC_DISABLE_SIGNUP');
|
||||||
|
|
||||||
// SSR env variables.
|
// SSR env variables.
|
||||||
@ -28,15 +29,20 @@ export function loader() {
|
|||||||
throw redirect('/signin');
|
throw redirect('/signin');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let returnTo = new URL(request.url).searchParams.get('returnTo') ?? undefined;
|
||||||
|
|
||||||
|
returnTo = isValidReturnTo(returnTo) ? normalizeReturnTo(returnTo) : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isGoogleSSOEnabled,
|
isGoogleSSOEnabled,
|
||||||
isMicrosoftSSOEnabled,
|
isMicrosoftSSOEnabled,
|
||||||
isOIDCSSOEnabled,
|
isOIDCSSOEnabled,
|
||||||
|
returnTo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SignUp({ loaderData }: Route.ComponentProps) {
|
export default function SignUp({ loaderData }: Route.ComponentProps) {
|
||||||
const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled } = loaderData;
|
const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, returnTo } = loaderData;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SignUpForm
|
<SignUpForm
|
||||||
@ -44,6 +50,7 @@ export default function SignUp({ loaderData }: Route.ComponentProps) {
|
|||||||
isGoogleSSOEnabled={isGoogleSSOEnabled}
|
isGoogleSSOEnabled={isGoogleSSOEnabled}
|
||||||
isMicrosoftSSOEnabled={isMicrosoftSSOEnabled}
|
isMicrosoftSSOEnabled={isMicrosoftSSOEnabled}
|
||||||
isOIDCSSOEnabled={isOIDCSSOEnabled}
|
isOIDCSSOEnabled={isOIDCSSOEnabled}
|
||||||
|
returnTo={returnTo}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,11 +2,14 @@ import { Outlet, isRouteErrorResponse, useRouteError } from 'react-router';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
IS_GOOGLE_SSO_ENABLED,
|
IS_GOOGLE_SSO_ENABLED,
|
||||||
|
IS_MICROSOFT_SSO_ENABLED,
|
||||||
IS_OIDC_SSO_ENABLED,
|
IS_OIDC_SSO_ENABLED,
|
||||||
OIDC_PROVIDER_LABEL,
|
OIDC_PROVIDER_LABEL,
|
||||||
} from '@documenso/lib/constants/auth';
|
} from '@documenso/lib/constants/auth';
|
||||||
|
|
||||||
import { EmbedAuthenticationRequired } from '~/components/embed/embed-authentication-required';
|
import { EmbedAuthenticationRequired } from '~/components/embed/embed-authentication-required';
|
||||||
|
import { EmbedDocumentCompleted } from '~/components/embed/embed-document-completed';
|
||||||
|
import { EmbedDocumentRejected } from '~/components/embed/embed-document-rejected';
|
||||||
import { EmbedDocumentWaitingForTurn } from '~/components/embed/embed-document-waiting-for-turn';
|
import { EmbedDocumentWaitingForTurn } from '~/components/embed/embed-document-waiting-for-turn';
|
||||||
import { EmbedPaywall } from '~/components/embed/embed-paywall';
|
import { EmbedPaywall } from '~/components/embed/embed-paywall';
|
||||||
|
|
||||||
@ -29,11 +32,13 @@ export function headers({ loaderHeaders }: Route.HeadersArgs) {
|
|||||||
export function loader() {
|
export function loader() {
|
||||||
// SSR env variables.
|
// SSR env variables.
|
||||||
const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED;
|
const isGoogleSSOEnabled = IS_GOOGLE_SSO_ENABLED;
|
||||||
|
const isMicrosoftSSOEnabled = IS_MICROSOFT_SSO_ENABLED;
|
||||||
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
|
const isOIDCSSOEnabled = IS_OIDC_SSO_ENABLED;
|
||||||
const oidcProviderLabel = OIDC_PROVIDER_LABEL;
|
const oidcProviderLabel = OIDC_PROVIDER_LABEL;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isGoogleSSOEnabled,
|
isGoogleSSOEnabled,
|
||||||
|
isMicrosoftSSOEnabled,
|
||||||
isOIDCSSOEnabled,
|
isOIDCSSOEnabled,
|
||||||
oidcProviderLabel,
|
oidcProviderLabel,
|
||||||
};
|
};
|
||||||
@ -44,15 +49,19 @@ export default function Layout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorBoundary({ loaderData }: Route.ErrorBoundaryProps) {
|
export function ErrorBoundary({ loaderData }: Route.ErrorBoundaryProps) {
|
||||||
const { isGoogleSSOEnabled, isOIDCSSOEnabled, oidcProviderLabel } = loaderData || {};
|
const { isGoogleSSOEnabled, isMicrosoftSSOEnabled, isOIDCSSOEnabled, oidcProviderLabel } =
|
||||||
|
loaderData || {};
|
||||||
|
|
||||||
const error = useRouteError();
|
const error = useRouteError();
|
||||||
|
|
||||||
|
console.log({ routeError: error });
|
||||||
|
|
||||||
if (isRouteErrorResponse(error)) {
|
if (isRouteErrorResponse(error)) {
|
||||||
if (error.status === 401 && error.data.type === 'embed-authentication-required') {
|
if (error.status === 401 && error.data.type === 'embed-authentication-required') {
|
||||||
return (
|
return (
|
||||||
<EmbedAuthenticationRequired
|
<EmbedAuthenticationRequired
|
||||||
isGoogleSSOEnabled={isGoogleSSOEnabled}
|
isGoogleSSOEnabled={isGoogleSSOEnabled}
|
||||||
|
isMicrosoftSSOEnabled={isMicrosoftSSOEnabled}
|
||||||
isOIDCSSOEnabled={isOIDCSSOEnabled}
|
isOIDCSSOEnabled={isOIDCSSOEnabled}
|
||||||
oidcProviderLabel={oidcProviderLabel}
|
oidcProviderLabel={oidcProviderLabel}
|
||||||
email={error.data.email}
|
email={error.data.email}
|
||||||
@ -68,6 +77,16 @@ export function ErrorBoundary({ loaderData }: Route.ErrorBoundaryProps) {
|
|||||||
if (error.status === 403 && error.data.type === 'embed-waiting-for-turn') {
|
if (error.status === 403 && error.data.type === 'embed-waiting-for-turn') {
|
||||||
return <EmbedDocumentWaitingForTurn />;
|
return <EmbedDocumentWaitingForTurn />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// !: Not used at the moment, may be removed in the future.
|
||||||
|
if (error.status === 403 && error.data.type === 'embed-document-rejected') {
|
||||||
|
return <EmbedDocumentRejected />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// !: Not used at the moment, may be removed in the future.
|
||||||
|
if (error.status === 403 && error.data.type === 'embed-document-completed') {
|
||||||
|
return <EmbedDocumentCompleted name={error.data.name} signature={error.data.signature} />;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>Not Found</div>;
|
return <div>Not Found</div>;
|
||||||
|
|||||||
332
apps/remix/app/routes/embed+/_v0+/direct.$token.tsx
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
import { data } from 'react-router';
|
||||||
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||||
|
import { EnvelopeRenderProvider } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||||
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
|
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 { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims';
|
||||||
|
import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token';
|
||||||
|
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
||||||
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
import { EmbedDirectTemplateClientPage } from '~/components/embed/embed-direct-template-client-page';
|
||||||
|
import { EmbedSignDocumentV2ClientPage } from '~/components/embed/embed-document-signing-page-v2';
|
||||||
|
import { DocumentSigningAuthProvider } from '~/components/general/document-signing/document-signing-auth-provider';
|
||||||
|
import { DocumentSigningProvider } from '~/components/general/document-signing/document-signing-provider';
|
||||||
|
import { DocumentSigningRecipientProvider } from '~/components/general/document-signing/document-signing-recipient-provider';
|
||||||
|
import { EnvelopeSigningProvider } from '~/components/general/document-signing/envelope-signing-provider';
|
||||||
|
import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
|
||||||
|
|
||||||
|
import type { Route } from './+types/direct.$token';
|
||||||
|
|
||||||
|
async function handleV1Loader({ params, request }: Route.LoaderArgs) {
|
||||||
|
if (!params.token) {
|
||||||
|
throw new Response('Not found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = params.token;
|
||||||
|
|
||||||
|
const template = await getTemplateByDirectLinkToken({
|
||||||
|
token,
|
||||||
|
}).catch(() => null);
|
||||||
|
|
||||||
|
// `template.directLink` is always available but we're doing this to
|
||||||
|
// satisfy the type checker.
|
||||||
|
if (!template || !template.directLink) {
|
||||||
|
throw new Response('Not found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const organisationClaim = await getOrganisationClaimByTeamId({ teamId: template.teamId });
|
||||||
|
|
||||||
|
const allowEmbedSigningWhitelabel = organisationClaim.flags.embedSigningWhiteLabel;
|
||||||
|
const hidePoweredBy = organisationClaim.flags.hidePoweredBy;
|
||||||
|
|
||||||
|
// TODO: Make this more robust, we need to ensure the owner is either
|
||||||
|
// TODO: the member of a team that has an active subscription, is an early
|
||||||
|
// TODO: adopter or is an enterprise user.
|
||||||
|
if (IS_BILLING_ENABLED() && !organisationClaim.flags.embedSigning) {
|
||||||
|
throw data(
|
||||||
|
{
|
||||||
|
type: 'embed-paywall',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user } = await getOptionalSession(request);
|
||||||
|
|
||||||
|
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||||
|
documentAuth: template.authOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isAccessAuthValid = derivedRecipientAccessAuth.every((auth) =>
|
||||||
|
match(auth)
|
||||||
|
.with(DocumentAccessAuth.ACCOUNT, () => !!user)
|
||||||
|
.with(DocumentAccessAuth.TWO_FACTOR_AUTH, () => false) // Not supported for direct links
|
||||||
|
.exhaustive(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isAccessAuthValid) {
|
||||||
|
throw data(
|
||||||
|
{
|
||||||
|
type: 'embed-authentication-required',
|
||||||
|
returnTo: `/embed/direct/${token}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { directTemplateRecipientId } = template.directLink;
|
||||||
|
|
||||||
|
const recipient = template.recipients.find(
|
||||||
|
(recipient) => recipient.id === directTemplateRecipientId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!recipient) {
|
||||||
|
throw new Response('Not found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = template.fields.filter((field) => field.recipientId === directTemplateRecipientId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
template,
|
||||||
|
recipient,
|
||||||
|
fields,
|
||||||
|
hidePoweredBy,
|
||||||
|
allowEmbedSigningWhitelabel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleV2Loader({ params, request }: Route.LoaderArgs) {
|
||||||
|
if (!params.token) {
|
||||||
|
throw new Response('Not found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = params.token;
|
||||||
|
|
||||||
|
const { user } = await getOptionalSession(request);
|
||||||
|
|
||||||
|
const envelopeForSigning = await getEnvelopeForDirectTemplateSigning({
|
||||||
|
token,
|
||||||
|
userId: user?.id,
|
||||||
|
})
|
||||||
|
.then((envelopeForSigning) => {
|
||||||
|
return {
|
||||||
|
isDocumentAccessValid: true,
|
||||||
|
...envelopeForSigning,
|
||||||
|
} as const;
|
||||||
|
})
|
||||||
|
.catch(async (e) => {
|
||||||
|
const error = AppError.parseError(e);
|
||||||
|
|
||||||
|
if (error.code === AppErrorCode.UNAUTHORIZED) {
|
||||||
|
const requiredAccessData = await getEnvelopeRequiredAccessData({ token });
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDocumentAccessValid: false,
|
||||||
|
...requiredAccessData,
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Response('Not Found', { status: 404 });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!envelopeForSigning.isDocumentAccessValid) {
|
||||||
|
throw data(
|
||||||
|
{
|
||||||
|
type: 'embed-authentication-required',
|
||||||
|
email: envelopeForSigning.recipientEmail,
|
||||||
|
returnTo: `/embed/direct/${token}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { envelope, recipient } = envelopeForSigning;
|
||||||
|
|
||||||
|
const organisationClaim = await getOrganisationClaimByTeamId({ teamId: envelope.teamId });
|
||||||
|
|
||||||
|
const allowEmbedSigningWhitelabel = organisationClaim.flags.embedSigningWhiteLabel;
|
||||||
|
const hidePoweredBy = organisationClaim.flags.hidePoweredBy;
|
||||||
|
|
||||||
|
if (IS_BILLING_ENABLED() && !organisationClaim.flags.embedSigning) {
|
||||||
|
throw data(
|
||||||
|
{
|
||||||
|
type: 'embed-paywall',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||||
|
documentAuth: envelope.authOptions,
|
||||||
|
recipientAuth: recipient.authOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isAccessAuthValid = derivedRecipientAccessAuth.every((accesssAuth) =>
|
||||||
|
match(accesssAuth)
|
||||||
|
.with(DocumentAccessAuth.ACCOUNT, () => user && user.email === recipient.email)
|
||||||
|
.with(DocumentAccessAuth.TWO_FACTOR_AUTH, () => false) // Not supported for direct links
|
||||||
|
.exhaustive(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isAccessAuthValid) {
|
||||||
|
throw data(
|
||||||
|
{
|
||||||
|
type: 'embed-authentication-required',
|
||||||
|
email: user?.email || recipient.email,
|
||||||
|
returnTo: `/embed/direct/${token}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
envelopeForSigning,
|
||||||
|
hidePoweredBy,
|
||||||
|
allowEmbedSigningWhitelabel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loader(loaderArgs: Route.LoaderArgs) {
|
||||||
|
const { token } = loaderArgs.params;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new Response('Not Found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not efficient but works for now until we remove v1.
|
||||||
|
const foundDirectLink = await prisma.templateDirectLink.findFirst({
|
||||||
|
where: {
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
envelope: {
|
||||||
|
select: {
|
||||||
|
internalVersion: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!foundDirectLink) {
|
||||||
|
throw new Response('Not Found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundDirectLink.envelope.internalVersion === 2) {
|
||||||
|
const payloadV2 = await handleV2Loader(loaderArgs);
|
||||||
|
|
||||||
|
return superLoaderJson({
|
||||||
|
version: 2,
|
||||||
|
payload: payloadV2,
|
||||||
|
} as const);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloadV1 = await handleV1Loader(loaderArgs);
|
||||||
|
|
||||||
|
return superLoaderJson({
|
||||||
|
version: 1,
|
||||||
|
payload: payloadV1,
|
||||||
|
} as const);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EmbedDirectTemplatePage() {
|
||||||
|
const { version, payload } = useSuperLoaderData<typeof loader>();
|
||||||
|
|
||||||
|
if (version === 1) {
|
||||||
|
return <EmbedDirectTemplatePageV1 data={payload} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <EmbedDirectTemplatePageV2 data={payload} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmbedDirectTemplatePageV1 = ({
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
data: Awaited<ReturnType<typeof handleV1Loader>>;
|
||||||
|
}) => {
|
||||||
|
const { token, user, template, recipient, fields, hidePoweredBy, allowEmbedSigningWhitelabel } =
|
||||||
|
data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocumentSigningProvider
|
||||||
|
email={user?.email}
|
||||||
|
fullName={user?.name}
|
||||||
|
signature={user?.signature}
|
||||||
|
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
|
||||||
|
uploadSignatureEnabled={template.templateMeta?.uploadSignatureEnabled}
|
||||||
|
drawSignatureEnabled={template.templateMeta?.drawSignatureEnabled}
|
||||||
|
>
|
||||||
|
<DocumentSigningAuthProvider
|
||||||
|
documentAuthOptions={template.authOptions}
|
||||||
|
recipient={recipient}
|
||||||
|
user={user}
|
||||||
|
>
|
||||||
|
<DocumentSigningRecipientProvider recipient={recipient}>
|
||||||
|
<EmbedDirectTemplateClientPage
|
||||||
|
token={token}
|
||||||
|
envelopeId={template.envelopeId}
|
||||||
|
updatedAt={template.updatedAt}
|
||||||
|
envelopeItems={template.envelopeItems}
|
||||||
|
recipient={recipient}
|
||||||
|
fields={fields}
|
||||||
|
metadata={template.templateMeta}
|
||||||
|
hidePoweredBy={hidePoweredBy}
|
||||||
|
allowWhiteLabelling={allowEmbedSigningWhitelabel}
|
||||||
|
/>
|
||||||
|
</DocumentSigningRecipientProvider>
|
||||||
|
</DocumentSigningAuthProvider>
|
||||||
|
</DocumentSigningProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmbedDirectTemplatePageV2 = ({
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
data: Awaited<ReturnType<typeof handleV2Loader>>;
|
||||||
|
}) => {
|
||||||
|
const { token, user, envelopeForSigning, hidePoweredBy, allowEmbedSigningWhitelabel } = data;
|
||||||
|
|
||||||
|
const { envelope, recipient } = envelopeForSigning;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnvelopeSigningProvider
|
||||||
|
envelopeData={envelopeForSigning}
|
||||||
|
email={user?.email}
|
||||||
|
fullName={user?.name}
|
||||||
|
signature={user?.signature}
|
||||||
|
>
|
||||||
|
<DocumentSigningAuthProvider
|
||||||
|
documentAuthOptions={envelope.authOptions}
|
||||||
|
recipient={recipient}
|
||||||
|
user={user}
|
||||||
|
isDirectTemplate={true}
|
||||||
|
>
|
||||||
|
<EnvelopeRenderProvider envelope={envelope} token={recipient.token}>
|
||||||
|
<EmbedSignDocumentV2ClientPage
|
||||||
|
hidePoweredBy={hidePoweredBy}
|
||||||
|
allowWhitelabelling={allowEmbedSigningWhitelabel}
|
||||||
|
/>
|
||||||
|
</EnvelopeRenderProvider>
|
||||||
|
</DocumentSigningAuthProvider>
|
||||||
|
</EnvelopeSigningProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,138 +0,0 @@
|
|||||||
import { data } from 'react-router';
|
|
||||||
import { match } from 'ts-pattern';
|
|
||||||
|
|
||||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
|
||||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
|
||||||
import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims';
|
|
||||||
import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token';
|
|
||||||
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
|
||||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
|
||||||
|
|
||||||
import { EmbedDirectTemplateClientPage } from '~/components/embed/embed-direct-template-client-page';
|
|
||||||
import { DocumentSigningAuthProvider } from '~/components/general/document-signing/document-signing-auth-provider';
|
|
||||||
import { DocumentSigningProvider } from '~/components/general/document-signing/document-signing-provider';
|
|
||||||
import { DocumentSigningRecipientProvider } from '~/components/general/document-signing/document-signing-recipient-provider';
|
|
||||||
import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
|
|
||||||
|
|
||||||
import type { Route } from './+types/direct.$url';
|
|
||||||
|
|
||||||
export async function loader({ params, request }: Route.LoaderArgs) {
|
|
||||||
if (!params.url) {
|
|
||||||
throw new Response('Not found', { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = params.url;
|
|
||||||
|
|
||||||
const template = await getTemplateByDirectLinkToken({
|
|
||||||
token,
|
|
||||||
}).catch(() => null);
|
|
||||||
|
|
||||||
// `template.directLink` is always available but we're doing this to
|
|
||||||
// satisfy the type checker.
|
|
||||||
if (!template || !template.directLink) {
|
|
||||||
throw new Response('Not found', { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const organisationClaim = await getOrganisationClaimByTeamId({ teamId: template.teamId });
|
|
||||||
|
|
||||||
const allowEmbedSigningWhitelabel = organisationClaim.flags.embedSigningWhiteLabel;
|
|
||||||
const hidePoweredBy = organisationClaim.flags.hidePoweredBy;
|
|
||||||
|
|
||||||
// TODO: Make this more robust, we need to ensure the owner is either
|
|
||||||
// TODO: the member of a team that has an active subscription, is an early
|
|
||||||
// TODO: adopter or is an enterprise user.
|
|
||||||
if (IS_BILLING_ENABLED() && !organisationClaim.flags.embedSigning) {
|
|
||||||
throw data(
|
|
||||||
{
|
|
||||||
type: 'embed-paywall',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 403,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { user } = await getOptionalSession(request);
|
|
||||||
|
|
||||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
|
||||||
documentAuth: template.authOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isAccessAuthValid = derivedRecipientAccessAuth.every((auth) =>
|
|
||||||
match(auth)
|
|
||||||
.with(DocumentAccessAuth.ACCOUNT, () => !!user)
|
|
||||||
.with(DocumentAccessAuth.TWO_FACTOR_AUTH, () => false) // Not supported for direct links
|
|
||||||
.exhaustive(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isAccessAuthValid) {
|
|
||||||
throw data(
|
|
||||||
{
|
|
||||||
type: 'embed-authentication-required',
|
|
||||||
email: user?.email,
|
|
||||||
returnTo: `/embed/direct/${token}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 401,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { directTemplateRecipientId } = template.directLink;
|
|
||||||
|
|
||||||
const recipient = template.recipients.find(
|
|
||||||
(recipient) => recipient.id === directTemplateRecipientId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!recipient) {
|
|
||||||
throw new Response('Not found', { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const fields = template.fields.filter((field) => field.recipientId === directTemplateRecipientId);
|
|
||||||
|
|
||||||
return superLoaderJson({
|
|
||||||
token,
|
|
||||||
user,
|
|
||||||
template,
|
|
||||||
recipient,
|
|
||||||
fields,
|
|
||||||
hidePoweredBy,
|
|
||||||
allowEmbedSigningWhitelabel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function EmbedDirectTemplatePage() {
|
|
||||||
const { token, user, template, recipient, fields, hidePoweredBy, allowEmbedSigningWhitelabel } =
|
|
||||||
useSuperLoaderData<typeof loader>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DocumentSigningProvider
|
|
||||||
email={user?.email}
|
|
||||||
fullName={user?.name}
|
|
||||||
signature={user?.signature}
|
|
||||||
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
|
|
||||||
uploadSignatureEnabled={template.templateMeta?.uploadSignatureEnabled}
|
|
||||||
drawSignatureEnabled={template.templateMeta?.drawSignatureEnabled}
|
|
||||||
>
|
|
||||||
<DocumentSigningAuthProvider
|
|
||||||
documentAuthOptions={template.authOptions}
|
|
||||||
recipient={recipient}
|
|
||||||
user={user}
|
|
||||||
>
|
|
||||||
<DocumentSigningRecipientProvider recipient={recipient}>
|
|
||||||
<EmbedDirectTemplateClientPage
|
|
||||||
token={token}
|
|
||||||
envelopeId={template.envelopeId}
|
|
||||||
updatedAt={template.updatedAt}
|
|
||||||
documentData={template.templateDocumentData}
|
|
||||||
recipient={recipient}
|
|
||||||
fields={fields}
|
|
||||||
metadata={template.templateMeta}
|
|
||||||
hidePoweredBy={hidePoweredBy}
|
|
||||||
allowWhiteLabelling={allowEmbedSigningWhitelabel}
|
|
||||||
/>
|
|
||||||
</DocumentSigningRecipientProvider>
|
|
||||||
</DocumentSigningAuthProvider>
|
|
||||||
</DocumentSigningProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
394
apps/remix/app/routes/embed+/_v0+/sign.$token.tsx
Normal file
@ -0,0 +1,394 @@
|
|||||||
|
import { RecipientRole } from '@prisma/client';
|
||||||
|
import { data } from 'react-router';
|
||||||
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||||
|
import { EnvelopeRenderProvider } from '@documenso/lib/client-only/providers/envelope-render-provider';
|
||||||
|
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||||
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
|
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
|
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
||||||
|
import { getEnvelopeForRecipientSigning } from '@documenso/lib/server-only/envelope/get-envelope-for-recipient-signing';
|
||||||
|
import { getEnvelopeRequiredAccessData } from '@documenso/lib/server-only/envelope/get-envelope-required-access-data';
|
||||||
|
import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get-completed-fields-for-token';
|
||||||
|
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||||
|
import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims';
|
||||||
|
import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
|
||||||
|
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||||
|
import { getRecipientsForAssistant } from '@documenso/lib/server-only/recipient/get-recipients-for-assistant';
|
||||||
|
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
||||||
|
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||||
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
import { EmbedSignDocumentV1ClientPage } from '~/components/embed/embed-document-signing-page-v1';
|
||||||
|
import { EmbedSignDocumentV2ClientPage } from '~/components/embed/embed-document-signing-page-v2';
|
||||||
|
import { DocumentSigningAuthProvider } from '~/components/general/document-signing/document-signing-auth-provider';
|
||||||
|
import { DocumentSigningProvider } from '~/components/general/document-signing/document-signing-provider';
|
||||||
|
import { EnvelopeSigningProvider } from '~/components/general/document-signing/envelope-signing-provider';
|
||||||
|
import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
|
||||||
|
|
||||||
|
import { getOptionalLoaderContext } from '../../../../server/utils/get-loader-session';
|
||||||
|
import type { Route } from './+types/sign.$token';
|
||||||
|
|
||||||
|
async function handleV1Loader({ params, request }: Route.LoaderArgs) {
|
||||||
|
const { requestMetadata } = getOptionalLoaderContext();
|
||||||
|
|
||||||
|
if (!params.token) {
|
||||||
|
throw new Response('Not found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = params.token;
|
||||||
|
|
||||||
|
const { user } = await getOptionalSession(request);
|
||||||
|
|
||||||
|
const [document, fields, recipient, completedFields] = await Promise.all([
|
||||||
|
getDocumentAndSenderByToken({
|
||||||
|
token,
|
||||||
|
userId: user?.id,
|
||||||
|
requireAccessAuth: false,
|
||||||
|
}).catch(() => null),
|
||||||
|
getFieldsForToken({ token }),
|
||||||
|
getRecipientByToken({ token }).catch(() => null),
|
||||||
|
getCompletedFieldsForToken({ token }).catch(() => []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// `document.directLink` is always available but we're doing this to
|
||||||
|
// satisfy the type checker.
|
||||||
|
if (!document || !recipient) {
|
||||||
|
throw new Response('Not found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const organisationClaim = await getOrganisationClaimByTeamId({ teamId: document.teamId });
|
||||||
|
|
||||||
|
const allowEmbedSigningWhitelabel = organisationClaim.flags.embedSigningWhiteLabel;
|
||||||
|
const hidePoweredBy = organisationClaim.flags.hidePoweredBy;
|
||||||
|
|
||||||
|
// TODO: Make this more robust, we need to ensure the owner is either
|
||||||
|
// TODO: the member of a team that has an active subscription, is an early
|
||||||
|
// TODO: adopter or is an enterprise user.
|
||||||
|
if (IS_BILLING_ENABLED() && !organisationClaim.flags.embedSigning) {
|
||||||
|
throw data(
|
||||||
|
{
|
||||||
|
type: 'embed-paywall',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||||
|
documentAuth: document.authOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isAccessAuthValid = derivedRecipientAccessAuth.every((accesssAuth) =>
|
||||||
|
match(accesssAuth)
|
||||||
|
.with(DocumentAccessAuth.ACCOUNT, () => user && user.email === recipient.email)
|
||||||
|
.with(DocumentAccessAuth.TWO_FACTOR_AUTH, () => true) // Allow without account requirement
|
||||||
|
.exhaustive(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isAccessAuthValid) {
|
||||||
|
throw data(
|
||||||
|
{
|
||||||
|
type: 'embed-authentication-required',
|
||||||
|
email: user?.email || recipient.email,
|
||||||
|
returnTo: `/embed/sign/${token}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRecipientsTurnToSign = await getIsRecipientsTurnToSign({ token });
|
||||||
|
|
||||||
|
if (!isRecipientsTurnToSign) {
|
||||||
|
throw data(
|
||||||
|
{
|
||||||
|
type: 'embed-waiting-for-turn',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await viewedDocument({
|
||||||
|
token,
|
||||||
|
requestMetadata,
|
||||||
|
recipientAccessAuth: derivedRecipientAccessAuth,
|
||||||
|
});
|
||||||
|
|
||||||
|
const allRecipients =
|
||||||
|
recipient.role === RecipientRole.ASSISTANT
|
||||||
|
? await getRecipientsForAssistant({
|
||||||
|
token,
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
document,
|
||||||
|
allRecipients,
|
||||||
|
recipient,
|
||||||
|
fields,
|
||||||
|
completedFields,
|
||||||
|
hidePoweredBy,
|
||||||
|
allowEmbedSigningWhitelabel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleV2Loader({ params, request }: Route.LoaderArgs) {
|
||||||
|
const { requestMetadata } = getOptionalLoaderContext();
|
||||||
|
|
||||||
|
if (!params.token) {
|
||||||
|
throw new Response('Not found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = params.token;
|
||||||
|
|
||||||
|
const { user } = await getOptionalSession(request);
|
||||||
|
|
||||||
|
const envelopeForSigning = await getEnvelopeForRecipientSigning({
|
||||||
|
token,
|
||||||
|
userId: user?.id,
|
||||||
|
})
|
||||||
|
.then((envelopeForSigning) => {
|
||||||
|
return {
|
||||||
|
isDocumentAccessValid: true,
|
||||||
|
...envelopeForSigning,
|
||||||
|
} as const;
|
||||||
|
})
|
||||||
|
.catch(async (e) => {
|
||||||
|
const error = AppError.parseError(e);
|
||||||
|
|
||||||
|
if (error.code === AppErrorCode.UNAUTHORIZED) {
|
||||||
|
const requiredAccessData = await getEnvelopeRequiredAccessData({ token });
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDocumentAccessValid: false,
|
||||||
|
...requiredAccessData,
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Response('Not Found', { status: 404 });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!envelopeForSigning.isDocumentAccessValid) {
|
||||||
|
throw data(
|
||||||
|
{
|
||||||
|
type: 'embed-authentication-required',
|
||||||
|
email: envelopeForSigning.recipientEmail,
|
||||||
|
returnTo: `/embed/sign/${token}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { envelope, recipient, isRecipientsTurn } = envelopeForSigning;
|
||||||
|
|
||||||
|
const organisationClaim = await getOrganisationClaimByTeamId({ teamId: envelope.teamId });
|
||||||
|
|
||||||
|
const allowEmbedSigningWhitelabel = organisationClaim.flags.embedSigningWhiteLabel;
|
||||||
|
const hidePoweredBy = organisationClaim.flags.hidePoweredBy;
|
||||||
|
|
||||||
|
if (IS_BILLING_ENABLED() && !organisationClaim.flags.embedSigning) {
|
||||||
|
throw data(
|
||||||
|
{
|
||||||
|
type: 'embed-paywall',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isRecipientsTurn) {
|
||||||
|
throw data(
|
||||||
|
{
|
||||||
|
type: 'embed-waiting-for-turn',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||||
|
documentAuth: envelope.authOptions,
|
||||||
|
recipientAuth: recipient.authOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isAccessAuthValid = derivedRecipientAccessAuth.every((accesssAuth) =>
|
||||||
|
match(accesssAuth)
|
||||||
|
.with(DocumentAccessAuth.ACCOUNT, () => user && user.email === recipient.email)
|
||||||
|
.with(DocumentAccessAuth.TWO_FACTOR_AUTH, () => true)
|
||||||
|
.exhaustive(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isAccessAuthValid) {
|
||||||
|
throw data(
|
||||||
|
{
|
||||||
|
type: 'embed-authentication-required',
|
||||||
|
email: user?.email || recipient.email,
|
||||||
|
returnTo: `/embed/sign/${token}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await viewedDocument({
|
||||||
|
token,
|
||||||
|
requestMetadata,
|
||||||
|
recipientAccessAuth: derivedRecipientAccessAuth,
|
||||||
|
}).catch(() => null);
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
envelopeForSigning,
|
||||||
|
hidePoweredBy,
|
||||||
|
allowEmbedSigningWhitelabel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loader(loaderArgs: Route.LoaderArgs) {
|
||||||
|
const { token } = loaderArgs.params;
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new Response('Not Found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not efficient but works for now until we remove v1.
|
||||||
|
const foundRecipient = await prisma.recipient.findFirst({
|
||||||
|
where: {
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
envelope: {
|
||||||
|
select: {
|
||||||
|
internalVersion: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!foundRecipient) {
|
||||||
|
throw new Response('Not Found', { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundRecipient.envelope.internalVersion === 2) {
|
||||||
|
const payloadV2 = await handleV2Loader(loaderArgs);
|
||||||
|
|
||||||
|
return superLoaderJson({
|
||||||
|
version: 2,
|
||||||
|
payload: payloadV2,
|
||||||
|
} as const);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payloadV1 = await handleV1Loader(loaderArgs);
|
||||||
|
|
||||||
|
return superLoaderJson({
|
||||||
|
version: 1,
|
||||||
|
payload: payloadV1,
|
||||||
|
} as const);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EmbedSignDocumentPage() {
|
||||||
|
const { version, payload } = useSuperLoaderData<typeof loader>();
|
||||||
|
|
||||||
|
if (version === 1) {
|
||||||
|
return <EmbedSignDocumentPageV1 data={payload} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <EmbedSignDocumentPageV2 data={payload} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EmbedSignDocumentPageV1 = ({
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
data: Awaited<ReturnType<typeof handleV1Loader>>;
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
token,
|
||||||
|
user,
|
||||||
|
document,
|
||||||
|
allRecipients,
|
||||||
|
recipient,
|
||||||
|
fields,
|
||||||
|
completedFields,
|
||||||
|
hidePoweredBy,
|
||||||
|
allowEmbedSigningWhitelabel,
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocumentSigningProvider
|
||||||
|
email={recipient.email}
|
||||||
|
fullName={user?.email === recipient.email ? user?.name : recipient.name}
|
||||||
|
signature={user?.email === recipient.email ? user?.signature : undefined}
|
||||||
|
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
|
||||||
|
uploadSignatureEnabled={document.documentMeta?.uploadSignatureEnabled}
|
||||||
|
drawSignatureEnabled={document.documentMeta?.drawSignatureEnabled}
|
||||||
|
>
|
||||||
|
<DocumentSigningAuthProvider
|
||||||
|
documentAuthOptions={document.authOptions}
|
||||||
|
recipient={recipient}
|
||||||
|
user={user}
|
||||||
|
>
|
||||||
|
<EmbedSignDocumentV1ClientPage
|
||||||
|
token={token}
|
||||||
|
documentId={document.id}
|
||||||
|
envelopeId={document.envelopeId}
|
||||||
|
envelopeItems={document.envelopeItems}
|
||||||
|
recipient={recipient}
|
||||||
|
fields={fields}
|
||||||
|
completedFields={completedFields}
|
||||||
|
metadata={document.documentMeta}
|
||||||
|
isCompleted={isDocumentCompleted(document.status)}
|
||||||
|
hidePoweredBy={hidePoweredBy}
|
||||||
|
allowWhitelabelling={allowEmbedSigningWhitelabel}
|
||||||
|
allRecipients={allRecipients}
|
||||||
|
/>
|
||||||
|
</DocumentSigningAuthProvider>
|
||||||
|
</DocumentSigningProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmbedSignDocumentPageV2 = ({
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
data: Awaited<ReturnType<typeof handleV2Loader>>;
|
||||||
|
}) => {
|
||||||
|
const { token, user, envelopeForSigning, hidePoweredBy, allowEmbedSigningWhitelabel } = data;
|
||||||
|
|
||||||
|
const { envelope, recipient } = envelopeForSigning;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnvelopeSigningProvider
|
||||||
|
envelopeData={envelopeForSigning}
|
||||||
|
email={recipient.email}
|
||||||
|
fullName={user?.email === recipient.email ? user?.name : recipient.name}
|
||||||
|
signature={user?.email === recipient.email ? user?.signature : undefined}
|
||||||
|
>
|
||||||
|
<DocumentSigningAuthProvider
|
||||||
|
documentAuthOptions={envelope.authOptions}
|
||||||
|
recipient={recipient}
|
||||||
|
user={user}
|
||||||
|
>
|
||||||
|
<EnvelopeRenderProvider envelope={envelope} token={token}>
|
||||||
|
<EmbedSignDocumentV2ClientPage
|
||||||
|
hidePoweredBy={hidePoweredBy}
|
||||||
|
allowWhitelabelling={allowEmbedSigningWhitelabel}
|
||||||
|
/>
|
||||||
|
</EnvelopeRenderProvider>
|
||||||
|
</DocumentSigningAuthProvider>
|
||||||
|
</EnvelopeSigningProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,181 +0,0 @@
|
|||||||
import { RecipientRole } from '@prisma/client';
|
|
||||||
import { data } from 'react-router';
|
|
||||||
import { getOptionalLoaderContext } from 'server/utils/get-loader-session';
|
|
||||||
import { match } from 'ts-pattern';
|
|
||||||
|
|
||||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
|
||||||
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
|
||||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
|
||||||
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
|
||||||
import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get-completed-fields-for-token';
|
|
||||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
|
||||||
import { getOrganisationClaimByTeamId } from '@documenso/lib/server-only/organisation/get-organisation-claims';
|
|
||||||
import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
|
|
||||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
|
||||||
import { getRecipientsForAssistant } from '@documenso/lib/server-only/recipient/get-recipients-for-assistant';
|
|
||||||
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
|
|
||||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
|
||||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
|
||||||
|
|
||||||
import { EmbedSignDocumentClientPage } from '~/components/embed/embed-document-signing-page';
|
|
||||||
import { DocumentSigningAuthProvider } from '~/components/general/document-signing/document-signing-auth-provider';
|
|
||||||
import { DocumentSigningProvider } from '~/components/general/document-signing/document-signing-provider';
|
|
||||||
import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
|
|
||||||
|
|
||||||
import type { Route } from './+types/sign.$url';
|
|
||||||
|
|
||||||
export async function loader({ params, request }: Route.LoaderArgs) {
|
|
||||||
const { requestMetadata } = getOptionalLoaderContext();
|
|
||||||
|
|
||||||
if (!params.url) {
|
|
||||||
throw new Response('Not found', { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = params.url;
|
|
||||||
|
|
||||||
const { user } = await getOptionalSession(request);
|
|
||||||
|
|
||||||
const [document, fields, recipient, completedFields] = await Promise.all([
|
|
||||||
getDocumentAndSenderByToken({
|
|
||||||
token,
|
|
||||||
userId: user?.id,
|
|
||||||
requireAccessAuth: false,
|
|
||||||
}).catch(() => null),
|
|
||||||
getFieldsForToken({ token }),
|
|
||||||
getRecipientByToken({ token }).catch(() => null),
|
|
||||||
getCompletedFieldsForToken({ token }).catch(() => []),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// `document.directLink` is always available but we're doing this to
|
|
||||||
// satisfy the type checker.
|
|
||||||
if (!document || !recipient) {
|
|
||||||
throw new Response('Not found', { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const organisationClaim = await getOrganisationClaimByTeamId({ teamId: document.teamId });
|
|
||||||
|
|
||||||
const allowEmbedSigningWhitelabel = organisationClaim.flags.embedSigningWhiteLabel;
|
|
||||||
const hidePoweredBy = organisationClaim.flags.hidePoweredBy;
|
|
||||||
|
|
||||||
// TODO: Make this more robust, we need to ensure the owner is either
|
|
||||||
// TODO: the member of a team that has an active subscription, is an early
|
|
||||||
// TODO: adopter or is an enterprise user.
|
|
||||||
if (IS_BILLING_ENABLED() && !organisationClaim.flags.embedSigning) {
|
|
||||||
throw data(
|
|
||||||
{
|
|
||||||
type: 'embed-paywall',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 403,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
|
||||||
documentAuth: document.authOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isAccessAuthValid = derivedRecipientAccessAuth.every((accesssAuth) =>
|
|
||||||
match(accesssAuth)
|
|
||||||
.with(DocumentAccessAuth.ACCOUNT, () => user && user.email === recipient.email)
|
|
||||||
.with(DocumentAccessAuth.TWO_FACTOR_AUTH, () => true) // Allow without account requirement
|
|
||||||
.exhaustive(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isAccessAuthValid) {
|
|
||||||
throw data(
|
|
||||||
{
|
|
||||||
type: 'embed-authentication-required',
|
|
||||||
email: user?.email || recipient.email,
|
|
||||||
returnTo: `/embed/sign/${token}`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 401,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isRecipientsTurnToSign = await getIsRecipientsTurnToSign({ token });
|
|
||||||
|
|
||||||
if (!isRecipientsTurnToSign) {
|
|
||||||
throw data(
|
|
||||||
{
|
|
||||||
type: 'embed-waiting-for-turn',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
status: 403,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await viewedDocument({
|
|
||||||
token,
|
|
||||||
requestMetadata,
|
|
||||||
recipientAccessAuth: derivedRecipientAccessAuth,
|
|
||||||
});
|
|
||||||
|
|
||||||
const allRecipients =
|
|
||||||
recipient.role === RecipientRole.ASSISTANT
|
|
||||||
? await getRecipientsForAssistant({
|
|
||||||
token,
|
|
||||||
})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
return superLoaderJson({
|
|
||||||
token,
|
|
||||||
user,
|
|
||||||
document,
|
|
||||||
allRecipients,
|
|
||||||
recipient,
|
|
||||||
fields,
|
|
||||||
completedFields,
|
|
||||||
hidePoweredBy,
|
|
||||||
allowEmbedSigningWhitelabel,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function EmbedSignDocumentPage() {
|
|
||||||
const {
|
|
||||||
token,
|
|
||||||
user,
|
|
||||||
document,
|
|
||||||
allRecipients,
|
|
||||||
recipient,
|
|
||||||
fields,
|
|
||||||
completedFields,
|
|
||||||
hidePoweredBy,
|
|
||||||
allowEmbedSigningWhitelabel,
|
|
||||||
} = useSuperLoaderData<typeof loader>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DocumentSigningProvider
|
|
||||||
email={recipient.email}
|
|
||||||
fullName={user?.email === recipient.email ? user?.name : recipient.name}
|
|
||||||
signature={user?.email === recipient.email ? user?.signature : undefined}
|
|
||||||
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
|
|
||||||
uploadSignatureEnabled={document.documentMeta?.uploadSignatureEnabled}
|
|
||||||
drawSignatureEnabled={document.documentMeta?.drawSignatureEnabled}
|
|
||||||
>
|
|
||||||
<DocumentSigningAuthProvider
|
|
||||||
documentAuthOptions={document.authOptions}
|
|
||||||
recipient={recipient}
|
|
||||||
user={user}
|
|
||||||
>
|
|
||||||
<EmbedSignDocumentClientPage
|
|
||||||
token={token}
|
|
||||||
documentId={document.id}
|
|
||||||
envelopeId={document.envelopeId}
|
|
||||||
documentData={document.documentData}
|
|
||||||
recipient={recipient}
|
|
||||||
fields={fields}
|
|
||||||
completedFields={completedFields}
|
|
||||||
metadata={document.documentMeta}
|
|
||||||
isCompleted={isDocumentCompleted(document.status)}
|
|
||||||
hidePoweredBy={hidePoweredBy}
|
|
||||||
allowWhitelabelling={allowEmbedSigningWhitelabel}
|
|
||||||
allRecipients={allRecipients}
|
|
||||||
/>
|
|
||||||
</DocumentSigningAuthProvider>
|
|
||||||
</DocumentSigningProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -67,6 +67,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
|||||||
export default function MultisignPage() {
|
export default function MultisignPage() {
|
||||||
const { envelopes, user, hidePoweredBy, allowWhitelabelling } =
|
const { envelopes, user, hidePoweredBy, allowWhitelabelling } =
|
||||||
useSuperLoaderData<typeof loader>();
|
useSuperLoaderData<typeof loader>();
|
||||||
|
|
||||||
const revalidator = useRevalidator();
|
const revalidator = useRevalidator();
|
||||||
|
|
||||||
const [selectedDocument, setSelectedDocument] = useState<
|
const [selectedDocument, setSelectedDocument] = useState<
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { SignFieldNumberDialog } from '~/components/dialogs/sign-field-number-di
|
|||||||
|
|
||||||
type HandleNumberFieldClickOptions = {
|
type HandleNumberFieldClickOptions = {
|
||||||
field: TFieldNumber;
|
field: TFieldNumber;
|
||||||
number: number | null;
|
number: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleNumberFieldClick = async (
|
export const handleNumberFieldClick = async (
|
||||||
|
|||||||
@ -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",
|
||||||
@ -40,6 +41,7 @@
|
|||||||
"@simplewebauthn/server": "^9.0.3",
|
"@simplewebauthn/server": "^9.0.3",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"colord": "^2.9.3",
|
"colord": "^2.9.3",
|
||||||
|
"content-disposition": "^0.5.4",
|
||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"hono": "4.7.0",
|
"hono": "4.7.0",
|
||||||
"hono-rate-limiter": "^0.4.2",
|
"hono-rate-limiter": "^0.4.2",
|
||||||
@ -86,6 +88,7 @@
|
|||||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||||
"@rollup/plugin-typescript": "^12.1.2",
|
"@rollup/plugin-typescript": "^12.1.2",
|
||||||
"@simplewebauthn/types": "^9.0.1",
|
"@simplewebauthn/types": "^9.0.1",
|
||||||
|
"@types/content-disposition": "^0.5.9",
|
||||||
"@types/formidable": "^2.0.6",
|
"@types/formidable": "^2.0.6",
|
||||||
"@types/luxon": "^3.3.1",
|
"@types/luxon": "^3.3.1",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
@ -103,5 +106,5 @@
|
|||||||
"vite-plugin-babel-macros": "^1.0.6",
|
"vite-plugin-babel-macros": "^1.0.6",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
},
|
},
|
||||||
"version": "1.13.1"
|
"version": "2.0.6"
|
||||||
}
|
}
|
||||||
|
|||||||
192
apps/remix/server/api/download/download.ts
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import { sValidator } from '@hono/standard-validator';
|
||||||
|
import { EnvelopeType } from '@prisma/client';
|
||||||
|
import { Hono } from 'hono';
|
||||||
|
|
||||||
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
|
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
|
||||||
|
import { getApiTokenByToken } from '@documenso/lib/server-only/public-api/get-api-token-by-token';
|
||||||
|
import { buildTeamWhereQuery } from '@documenso/lib/utils/teams';
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
import type { HonoEnv } from '../../router';
|
||||||
|
import { handleEnvelopeItemFileRequest } from '../files/files.helpers';
|
||||||
|
import {
|
||||||
|
ZDownloadDocumentRequestParamsSchema,
|
||||||
|
ZDownloadEnvelopeItemRequestParamsSchema,
|
||||||
|
} from './download.types';
|
||||||
|
|
||||||
|
export const downloadRoute = new Hono<HonoEnv>()
|
||||||
|
/**
|
||||||
|
* Download an envelope item by its ID.
|
||||||
|
* Requires API key authentication via Authorization header.
|
||||||
|
*/
|
||||||
|
.get(
|
||||||
|
'/envelopeItem/:envelopeItemId/download',
|
||||||
|
sValidator('param', ZDownloadEnvelopeItemRequestParamsSchema),
|
||||||
|
async (c) => {
|
||||||
|
const logger = c.get('logger');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { envelopeItemId, version } = c.req.valid('param');
|
||||||
|
const authorizationHeader = c.req.header('authorization');
|
||||||
|
|
||||||
|
// Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx"
|
||||||
|
const [token] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||||
|
message: 'API token was not provided',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiToken = await getApiTokenByToken({ token });
|
||||||
|
|
||||||
|
if (apiToken.user.disabled) {
|
||||||
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||||
|
message: 'User is disabled',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
auth: 'api',
|
||||||
|
source: 'apiV2',
|
||||||
|
path: c.req.path,
|
||||||
|
userId: apiToken.user.id,
|
||||||
|
apiTokenId: apiToken.id,
|
||||||
|
envelopeItemId,
|
||||||
|
version,
|
||||||
|
});
|
||||||
|
|
||||||
|
const envelopeItem = await prisma.envelopeItem.findFirst({
|
||||||
|
where: {
|
||||||
|
id: envelopeItemId,
|
||||||
|
envelope: {
|
||||||
|
team: buildTeamWhereQuery({ teamId: apiToken.teamId, userId: apiToken.user.id }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
envelope: true,
|
||||||
|
documentData: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!envelopeItem) {
|
||||||
|
return c.json({ error: 'Envelope item not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!envelopeItem.documentData) {
|
||||||
|
return c.json({ error: 'Document data not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await handleEnvelopeItemFileRequest({
|
||||||
|
title: envelopeItem.title,
|
||||||
|
status: envelopeItem.envelope.status,
|
||||||
|
documentData: envelopeItem.documentData,
|
||||||
|
version: version || 'signed',
|
||||||
|
isDownload: true,
|
||||||
|
context: c,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
|
||||||
|
if (error instanceof AppError) {
|
||||||
|
if (error.code === AppErrorCode.UNAUTHORIZED) {
|
||||||
|
return c.json({ error: error.message }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({ error: error.message }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
/**
|
||||||
|
* Download a document by its ID.
|
||||||
|
* Requires API key authentication via Authorization header.
|
||||||
|
*/
|
||||||
|
.get(
|
||||||
|
'/document/:documentId/download',
|
||||||
|
sValidator('param', ZDownloadDocumentRequestParamsSchema),
|
||||||
|
async (c) => {
|
||||||
|
const logger = c.get('logger');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { documentId, version } = c.req.valid('param');
|
||||||
|
const authorizationHeader = c.req.header('authorization');
|
||||||
|
|
||||||
|
// Support for both "Authorization: Bearer api_xxx" and "Authorization: api_xxx"
|
||||||
|
const [token] = (authorizationHeader || '').split('Bearer ').filter((s) => s.length > 0);
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||||
|
message: 'API token was not provided',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiToken = await getApiTokenByToken({ token });
|
||||||
|
|
||||||
|
if (apiToken.user.disabled) {
|
||||||
|
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||||
|
message: 'User is disabled',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
auth: 'api',
|
||||||
|
source: 'apiV2',
|
||||||
|
path: c.req.path,
|
||||||
|
userId: apiToken.user.id,
|
||||||
|
apiTokenId: apiToken.id,
|
||||||
|
documentId,
|
||||||
|
version,
|
||||||
|
});
|
||||||
|
|
||||||
|
const envelope = await getEnvelopeById({
|
||||||
|
id: {
|
||||||
|
type: 'documentId',
|
||||||
|
id: documentId,
|
||||||
|
},
|
||||||
|
type: EnvelopeType.DOCUMENT,
|
||||||
|
userId: apiToken.user.id,
|
||||||
|
teamId: apiToken.teamId,
|
||||||
|
}).catch(() => null);
|
||||||
|
|
||||||
|
if (!envelope) {
|
||||||
|
return c.json({ error: 'Document not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the first envelope item (documents have exactly one)
|
||||||
|
const [envelopeItem] = envelope.envelopeItems;
|
||||||
|
|
||||||
|
if (!envelopeItem) {
|
||||||
|
return c.json({ error: 'Document item not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!envelopeItem.documentData) {
|
||||||
|
return c.json({ error: 'Document data not found' }, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await handleEnvelopeItemFileRequest({
|
||||||
|
title: envelopeItem.title,
|
||||||
|
status: envelope.status,
|
||||||
|
documentData: envelopeItem.documentData,
|
||||||
|
version: version || 'signed',
|
||||||
|
isDownload: true,
|
||||||
|
context: c,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
|
||||||
|
if (error instanceof AppError) {
|
||||||
|
if (error.code === AppErrorCode.UNAUTHORIZED) {
|
||||||
|
return c.json({ error: error.message }, 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({ error: error.message }, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({ error: 'Internal server error' }, 500);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
29
apps/remix/server/api/download/download.types.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const ZDownloadEnvelopeItemRequestParamsSchema = z.object({
|
||||||
|
envelopeItemId: z.string().describe('The ID of the envelope item to download.'),
|
||||||
|
version: z
|
||||||
|
.enum(['original', 'signed'])
|
||||||
|
.optional()
|
||||||
|
.default('signed')
|
||||||
|
.describe(
|
||||||
|
'The version of the envelope item to download. "signed" returns the completed document with signatures, "original" returns the original uploaded document.',
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TDownloadEnvelopeItemRequestParams = z.infer<
|
||||||
|
typeof ZDownloadEnvelopeItemRequestParamsSchema
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const ZDownloadDocumentRequestParamsSchema = z.object({
|
||||||
|
documentId: z.coerce.number().describe('The ID of the document to download.'),
|
||||||
|
version: z
|
||||||
|
.enum(['original', 'signed'])
|
||||||
|
.optional()
|
||||||
|
.default('signed')
|
||||||
|
.describe(
|
||||||
|
'The version of the document to download. "signed" returns the completed document with signatures, "original" returns the original uploaded document.',
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TDownloadDocumentRequestParams = z.infer<typeof ZDownloadDocumentRequestParamsSchema>;
|
||||||
@ -1,10 +1,11 @@
|
|||||||
import { type DocumentDataType, DocumentStatus } from '@prisma/client';
|
import { type DocumentDataType, DocumentStatus } from '@prisma/client';
|
||||||
|
import contentDisposition from 'content-disposition';
|
||||||
import { type Context } from 'hono';
|
import { type Context } from 'hono';
|
||||||
|
|
||||||
import { sha256 } from '@documenso/lib/universal/crypto';
|
import { sha256 } from '@documenso/lib/universal/crypto';
|
||||||
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
|
import { getFileServerSide } from '@documenso/lib/universal/upload/get-file.server';
|
||||||
|
|
||||||
import type { HonoEnv } from '../router';
|
import type { HonoEnv } from '../../router';
|
||||||
|
|
||||||
type HandleEnvelopeItemFileRequestOptions = {
|
type HandleEnvelopeItemFileRequestOptions = {
|
||||||
title: string;
|
title: string;
|
||||||
@ -34,7 +35,7 @@ export const handleEnvelopeItemFileRequest = async ({
|
|||||||
|
|
||||||
const etag = Buffer.from(sha256(documentDataToUse)).toString('hex');
|
const etag = Buffer.from(sha256(documentDataToUse)).toString('hex');
|
||||||
|
|
||||||
if (c.req.header('If-None-Match') === etag) {
|
if (c.req.header('If-None-Match') === etag && !isDownload) {
|
||||||
return c.body(null, 304);
|
return c.body(null, 304);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,15 +53,13 @@ export const handleEnvelopeItemFileRequest = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.header('Content-Type', 'application/pdf');
|
c.header('Content-Type', 'application/pdf');
|
||||||
c.header('Content-Length', file.length.toString());
|
|
||||||
c.header('ETag', etag);
|
c.header('ETag', etag);
|
||||||
|
|
||||||
if (!isDownload) {
|
if (!isDownload) {
|
||||||
if (status === DocumentStatus.COMPLETED) {
|
if (status === DocumentStatus.COMPLETED) {
|
||||||
c.header('Cache-Control', 'public, max-age=31536000, immutable');
|
c.header('Cache-Control', 'public, max-age=31536000, immutable');
|
||||||
} else {
|
} else {
|
||||||
// Set a tiny 1 minute cache, with must-revalidate to ensure the client always checks for updates.
|
c.header('Cache-Control', 'public, max-age=0, must-revalidate');
|
||||||
c.header('Cache-Control', 'public, max-age=60, must-revalidate');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +69,7 @@ export const handleEnvelopeItemFileRequest = async ({
|
|||||||
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
|
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
|
||||||
const filename = `${baseTitle}${suffix}`;
|
const filename = `${baseTitle}${suffix}`;
|
||||||
|
|
||||||
c.header('Content-Disposition', `attachment; filename="${filename}"`);
|
c.header('Content-Disposition', contentDisposition(filename));
|
||||||
|
|
||||||
// For downloads, prevent caching to ensure fresh data
|
// For downloads, prevent caching to ensure fresh data
|
||||||
c.header('Cache-Control', 'no-cache, no-store, must-revalidate');
|
c.header('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import { sValidator } from '@hono/standard-validator';
|
import { sValidator } from '@hono/standard-validator';
|
||||||
|
import type { Prisma } from '@prisma/client';
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
|
|
||||||
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||||
@ -9,7 +10,7 @@ import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/
|
|||||||
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
|
import { getPresignPostUrl } from '@documenso/lib/universal/upload/server-actions';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import type { HonoEnv } from '../router';
|
import type { HonoEnv } from '../../router';
|
||||||
import { handleEnvelopeItemFileRequest } from './files.helpers';
|
import { handleEnvelopeItemFileRequest } from './files.helpers';
|
||||||
import {
|
import {
|
||||||
type TGetPresignedPostUrlResponse,
|
type TGetPresignedPostUrlResponse,
|
||||||
@ -206,17 +207,28 @@ export const filesRoute = new Hono<HonoEnv>()
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const { token, envelopeItemId } = c.req.valid('param');
|
const { token, envelopeItemId } = c.req.valid('param');
|
||||||
|
|
||||||
const envelopeItem = await prisma.envelopeItem.findFirst({
|
let envelopeWhereQuery: Prisma.EnvelopeItemWhereUniqueInput = {
|
||||||
where: {
|
id: envelopeItemId,
|
||||||
id: envelopeItemId,
|
envelope: {
|
||||||
envelope: {
|
recipients: {
|
||||||
recipients: {
|
some: {
|
||||||
some: {
|
token,
|
||||||
token,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (token.startsWith('qr_')) {
|
||||||
|
envelopeWhereQuery = {
|
||||||
|
id: envelopeItemId,
|
||||||
|
envelope: {
|
||||||
|
qrToken: token,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const envelopeItem = await prisma.envelopeItem.findUnique({
|
||||||
|
where: envelopeWhereQuery,
|
||||||
include: {
|
include: {
|
||||||
envelope: true,
|
envelope: true,
|
||||||
documentData: true,
|
documentData: true,
|
||||||
@ -247,17 +259,28 @@ export const filesRoute = new Hono<HonoEnv>()
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const { token, envelopeItemId, version } = c.req.valid('param');
|
const { token, envelopeItemId, version } = c.req.valid('param');
|
||||||
|
|
||||||
const envelopeItem = await prisma.envelopeItem.findFirst({
|
let envelopeWhereQuery: Prisma.EnvelopeItemWhereUniqueInput = {
|
||||||
where: {
|
id: envelopeItemId,
|
||||||
id: envelopeItemId,
|
envelope: {
|
||||||
envelope: {
|
recipients: {
|
||||||
recipients: {
|
some: {
|
||||||
some: {
|
token,
|
||||||
token,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (token.startsWith('qr_')) {
|
||||||
|
envelopeWhereQuery = {
|
||||||
|
id: envelopeItemId,
|
||||||
|
envelope: {
|
||||||
|
qrToken: token,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const envelopeItem = await prisma.envelopeItem.findUnique({
|
||||||
|
where: envelopeWhereQuery,
|
||||||
include: {
|
include: {
|
||||||
envelope: true,
|
envelope: true,
|
||||||
documentData: true,
|
documentData: true,
|
||||||
@ -14,7 +14,8 @@ import { getIpAddress } from '@documenso/lib/universal/get-ip-address';
|
|||||||
import { logger } from '@documenso/lib/utils/logger';
|
import { logger } from '@documenso/lib/utils/logger';
|
||||||
import { openApiDocument } from '@documenso/trpc/server/open-api';
|
import { openApiDocument } from '@documenso/trpc/server/open-api';
|
||||||
|
|
||||||
import { filesRoute } from './api/files';
|
import { downloadRoute } from './api/download/download';
|
||||||
|
import { filesRoute } from './api/files/files';
|
||||||
import { type AppContext, appContext } from './context';
|
import { type AppContext, appContext } from './context';
|
||||||
import { appMiddleware } from './middleware';
|
import { appMiddleware } from './middleware';
|
||||||
import { openApiTrpcServerHandler } from './trpc/hono-trpc-open-api';
|
import { openApiTrpcServerHandler } from './trpc/hono-trpc-open-api';
|
||||||
@ -92,6 +93,8 @@ app.use('/api/trpc/*', reactRouterTrpcServer);
|
|||||||
// Unstable API server routes. Order matters for these two.
|
// Unstable API server routes. Order matters for these two.
|
||||||
app.get(`${API_V2_URL}/openapi.json`, (c) => c.json(openApiDocument));
|
app.get(`${API_V2_URL}/openapi.json`, (c) => c.json(openApiDocument));
|
||||||
app.use(`${API_V2_URL}/*`, cors());
|
app.use(`${API_V2_URL}/*`, cors());
|
||||||
|
// Shadows the download routes that tRPC defines since tRPC-to-openapi doesn't support their return types.
|
||||||
|
app.route(`${API_V2_URL}`, downloadRoute);
|
||||||
app.use(`${API_V2_URL}/*`, async (c) =>
|
app.use(`${API_V2_URL}/*`, async (c) =>
|
||||||
openApiTrpcServerHandler(c, {
|
openApiTrpcServerHandler(c, {
|
||||||
isBeta: false,
|
isBeta: false,
|
||||||
@ -101,6 +104,8 @@ app.use(`${API_V2_URL}/*`, async (c) =>
|
|||||||
// Unstable API server routes. Order matters for these two.
|
// Unstable API server routes. Order matters for these two.
|
||||||
app.get(`${API_V2_BETA_URL}/openapi.json`, (c) => c.json(openApiDocument));
|
app.get(`${API_V2_BETA_URL}/openapi.json`, (c) => c.json(openApiDocument));
|
||||||
app.use(`${API_V2_BETA_URL}/*`, cors());
|
app.use(`${API_V2_BETA_URL}/*`, cors());
|
||||||
|
// Shadows the download routes that tRPC defines since tRPC-to-openapi doesn't support their return types.
|
||||||
|
app.route(`${API_V2_BETA_URL}`, downloadRoute);
|
||||||
app.use(`${API_V2_BETA_URL}/*`, async (c) =>
|
app.use(`${API_V2_BETA_URL}/*`, async (c) =>
|
||||||
openApiTrpcServerHandler(c, {
|
openApiTrpcServerHandler(c, {
|
||||||
isBeta: true,
|
isBeta: true,
|
||||||
|
|||||||
195
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.13.1",
|
"version": "2.0.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.13.1",
|
"version": "2.0.6",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@ -45,6 +45,7 @@
|
|||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prisma": "^6.18.0",
|
"prisma": "^6.18.0",
|
||||||
"prisma-extension-kysely": "^3.0.0",
|
"prisma-extension-kysely": "^3.0.0",
|
||||||
|
"prisma-json-types-generator": "^3.6.2",
|
||||||
"prisma-kysely": "^1.8.0",
|
"prisma-kysely": "^1.8.0",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"superjson": "^2.2.5",
|
"superjson": "^2.2.5",
|
||||||
@ -99,7 +100,7 @@
|
|||||||
},
|
},
|
||||||
"apps/remix": {
|
"apps/remix": {
|
||||||
"name": "@documenso/remix",
|
"name": "@documenso/remix",
|
||||||
"version": "1.13.1",
|
"version": "2.0.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cantoo/pdf-lib": "^2.5.2",
|
"@cantoo/pdf-lib": "^2.5.2",
|
||||||
"@documenso/api": "*",
|
"@documenso/api": "*",
|
||||||
@ -112,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",
|
||||||
@ -127,6 +129,7 @@
|
|||||||
"@simplewebauthn/server": "^9.0.3",
|
"@simplewebauthn/server": "^9.0.3",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"colord": "^2.9.3",
|
"colord": "^2.9.3",
|
||||||
|
"content-disposition": "^0.5.4",
|
||||||
"framer-motion": "^10.12.8",
|
"framer-motion": "^10.12.8",
|
||||||
"hono": "4.7.0",
|
"hono": "4.7.0",
|
||||||
"hono-rate-limiter": "^0.4.2",
|
"hono-rate-limiter": "^0.4.2",
|
||||||
@ -173,6 +176,7 @@
|
|||||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||||
"@rollup/plugin-typescript": "^12.1.2",
|
"@rollup/plugin-typescript": "^12.1.2",
|
||||||
"@simplewebauthn/types": "^9.0.1",
|
"@simplewebauthn/types": "^9.0.1",
|
||||||
|
"@types/content-disposition": "^0.5.9",
|
||||||
"@types/formidable": "^2.0.6",
|
"@types/formidable": "^2.0.6",
|
||||||
"@types/luxon": "^3.3.1",
|
"@types/luxon": "^3.3.1",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
@ -3511,6 +3515,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",
|
||||||
@ -7599,6 +7619,18 @@
|
|||||||
"empathic": "2.0.0"
|
"empathic": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@prisma/debug": {
|
||||||
|
"version": "6.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz",
|
||||||
|
"integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/@prisma/dmmf": {
|
||||||
|
"version": "6.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/dmmf/-/dmmf-6.18.0.tgz",
|
||||||
|
"integrity": "sha512-x0ItbLDxAnciEMFnGUm90Bkpzx2ja5Lp8Lz+9UkepIClZOMC4WvtMUHaMMfCFqfoBb+KUbxa/xV+FJ6EAaw4wQ==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/@prisma/engines": {
|
"node_modules/@prisma/engines": {
|
||||||
"version": "6.18.0",
|
"version": "6.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.18.0.tgz",
|
||||||
@ -7618,12 +7650,6 @@
|
|||||||
"integrity": "sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ==",
|
"integrity": "sha512-T7Af4QsJQnSgWN1zBbX+Cha5t4qjHRxoeoWpK4JugJzG/ipmmDMY5S+O0N1ET6sCBNVkf6lz+Y+ZNO9+wFU8pQ==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/engines/node_modules/@prisma/debug": {
|
|
||||||
"version": "6.18.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz",
|
|
||||||
"integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==",
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/@prisma/fetch-engine": {
|
"node_modules/@prisma/fetch-engine": {
|
||||||
"version": "6.18.0",
|
"version": "6.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.18.0.tgz",
|
||||||
@ -7635,10 +7661,10 @@
|
|||||||
"@prisma/get-platform": "6.18.0"
|
"@prisma/get-platform": "6.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/fetch-engine/node_modules/@prisma/debug": {
|
"node_modules/@prisma/generator": {
|
||||||
"version": "6.18.0",
|
"version": "6.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/generator/-/generator-6.18.0.tgz",
|
||||||
"integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==",
|
"integrity": "sha512-8Sz9z8d/X/42uL07qYF4no2hnSSMPk8g6+w0zpINCN7lvnILby5b56xA0uPKRFfR3Wfn3NtFLnyEpMQE9fXeeg==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/generator-helper": {
|
"node_modules/@prisma/generator-helper": {
|
||||||
@ -7719,12 +7745,6 @@
|
|||||||
"@prisma/debug": "6.18.0"
|
"@prisma/debug": "6.18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@prisma/get-platform/node_modules/@prisma/debug": {
|
|
||||||
"version": "6.18.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz",
|
|
||||||
"integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==",
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/@prisma/internals": {
|
"node_modules/@prisma/internals": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/internals/-/internals-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/internals/-/internals-5.3.1.tgz",
|
||||||
@ -12297,6 +12317,13 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/content-disposition": {
|
||||||
|
"version": "0.5.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.9.tgz",
|
||||||
|
"integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/cross-spawn": {
|
"node_modules/@types/cross-spawn": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cross-spawn/-/cross-spawn-6.0.2.tgz",
|
||||||
@ -28646,6 +28673,54 @@
|
|||||||
"@prisma/client": "latest"
|
"@prisma/client": "latest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prisma-json-types-generator": {
|
||||||
|
"version": "3.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prisma-json-types-generator/-/prisma-json-types-generator-3.6.2.tgz",
|
||||||
|
"integrity": "sha512-WX/oENQ0S74r/Wgd2uuHT5i3KbnwLFCP2Fq5ISzrXkus/htOC4uCaQPYuGP2m/wSeKZZCw1RxptTlD+ib7Ht/A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/generator-helper": "^6.16.1",
|
||||||
|
"semver": "^7.7.2",
|
||||||
|
"tslib": "^2.8.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"prisma-json-types-generator": "index.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/arthurfiorette/prisma-json-types-generator?sponsor=1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@prisma/client": "^6.14",
|
||||||
|
"prisma": "^6.14",
|
||||||
|
"typescript": "^5.9.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prisma-json-types-generator/node_modules/@prisma/generator-helper": {
|
||||||
|
"version": "6.18.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-6.18.0.tgz",
|
||||||
|
"integrity": "sha512-kmlCDRRewLwBkHpkAjzyuNHD5ISlDLzUTcTsZbwmjDilQVt/S72xvvCAa+hxY16APTkqbtDYn3p7zL/XFO+C0A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@prisma/debug": "6.18.0",
|
||||||
|
"@prisma/dmmf": "6.18.0",
|
||||||
|
"@prisma/generator": "6.18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prisma-json-types-generator/node_modules/semver": {
|
||||||
|
"version": "7.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||||
|
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prisma-kysely": {
|
"node_modules/prisma-kysely": {
|
||||||
"version": "1.8.0",
|
"version": "1.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/prisma-kysely/-/prisma-kysely-1.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/prisma-kysely/-/prisma-kysely-1.8.0.tgz",
|
||||||
@ -36456,24 +36531,6 @@
|
|||||||
"zod": "^3.25.0 || ^4.0.0"
|
"zod": "^3.25.0 || ^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod-prisma-types/node_modules/@prisma/debug": {
|
|
||||||
"version": "6.18.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz",
|
|
||||||
"integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==",
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/zod-prisma-types/node_modules/@prisma/dmmf": {
|
|
||||||
"version": "6.18.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/dmmf/-/dmmf-6.18.0.tgz",
|
|
||||||
"integrity": "sha512-x0ItbLDxAnciEMFnGUm90Bkpzx2ja5Lp8Lz+9UkepIClZOMC4WvtMUHaMMfCFqfoBb+KUbxa/xV+FJ6EAaw4wQ==",
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/zod-prisma-types/node_modules/@prisma/generator": {
|
|
||||||
"version": "6.18.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/generator/-/generator-6.18.0.tgz",
|
|
||||||
"integrity": "sha512-8Sz9z8d/X/42uL07qYF4no2hnSSMPk8g6+w0zpINCN7lvnILby5b56xA0uPKRFfR3Wfn3NtFLnyEpMQE9fXeeg==",
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/zod-prisma-types/node_modules/@prisma/generator-helper": {
|
"node_modules/zod-prisma-types/node_modules/@prisma/generator-helper": {
|
||||||
"version": "6.18.0",
|
"version": "6.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-6.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-6.18.0.tgz",
|
||||||
@ -37130,72 +37187,6 @@
|
|||||||
"typescript": "5.6.2"
|
"typescript": "5.6.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/prisma/node_modules/@prisma/debug": {
|
|
||||||
"version": "6.18.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.18.0.tgz",
|
|
||||||
"integrity": "sha512-PMVPMmxPj0ps1VY75DIrT430MoOyQx9hmm174k6cmLZpcI95rAPXOQ+pp8ANQkJtNyLVDxnxVJ0QLbrm/ViBcg==",
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"packages/prisma/node_modules/@prisma/dmmf": {
|
|
||||||
"version": "6.18.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/dmmf/-/dmmf-6.18.0.tgz",
|
|
||||||
"integrity": "sha512-x0ItbLDxAnciEMFnGUm90Bkpzx2ja5Lp8Lz+9UkepIClZOMC4WvtMUHaMMfCFqfoBb+KUbxa/xV+FJ6EAaw4wQ==",
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"packages/prisma/node_modules/@prisma/generator": {
|
|
||||||
"version": "6.18.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/generator/-/generator-6.18.0.tgz",
|
|
||||||
"integrity": "sha512-8Sz9z8d/X/42uL07qYF4no2hnSSMPk8g6+w0zpINCN7lvnILby5b56xA0uPKRFfR3Wfn3NtFLnyEpMQE9fXeeg==",
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"packages/prisma/node_modules/@prisma/generator-helper": {
|
|
||||||
"version": "6.18.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-6.18.0.tgz",
|
|
||||||
"integrity": "sha512-kmlCDRRewLwBkHpkAjzyuNHD5ISlDLzUTcTsZbwmjDilQVt/S72xvvCAa+hxY16APTkqbtDYn3p7zL/XFO+C0A==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@prisma/debug": "6.18.0",
|
|
||||||
"@prisma/dmmf": "6.18.0",
|
|
||||||
"@prisma/generator": "6.18.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/prisma/node_modules/prisma-json-types-generator": {
|
|
||||||
"version": "3.6.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/prisma-json-types-generator/-/prisma-json-types-generator-3.6.2.tgz",
|
|
||||||
"integrity": "sha512-WX/oENQ0S74r/Wgd2uuHT5i3KbnwLFCP2Fq5ISzrXkus/htOC4uCaQPYuGP2m/wSeKZZCw1RxptTlD+ib7Ht/A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@prisma/generator-helper": "^6.16.1",
|
|
||||||
"semver": "^7.7.2",
|
|
||||||
"tslib": "^2.8.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"prisma-json-types-generator": "index.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/arthurfiorette/prisma-json-types-generator?sponsor=1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@prisma/client": "^6.14",
|
|
||||||
"prisma": "^6.14",
|
|
||||||
"typescript": "^5.9.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/prisma/node_modules/semver": {
|
|
||||||
"version": "7.7.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
|
||||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
|
||||||
"license": "ISC",
|
|
||||||
"bin": {
|
|
||||||
"semver": "bin/semver.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"packages/prisma/node_modules/ts-pattern": {
|
"packages/prisma/node_modules/ts-pattern": {
|
||||||
"version": "5.7.1",
|
"version": "5.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/ts-pattern/-/ts-pattern-5.7.1.tgz",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.13.1",
|
"version": "2.0.6",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"dev": "turbo run dev --filter=@documenso/remix",
|
"dev": "turbo run dev --filter=@documenso/remix",
|
||||||
@ -56,6 +56,7 @@
|
|||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prisma": "^6.18.0",
|
"prisma": "^6.18.0",
|
||||||
"prisma-extension-kysely": "^3.0.0",
|
"prisma-extension-kysely": "^3.0.0",
|
||||||
|
"prisma-json-types-generator": "^3.6.2",
|
||||||
"prisma-kysely": "^1.8.0",
|
"prisma-kysely": "^1.8.0",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"turbo": "^1.9.3",
|
"turbo": "^1.9.3",
|
||||||
@ -94,4 +95,4 @@
|
|||||||
"trigger.dev": {
|
"trigger.dev": {
|
||||||
"endpointId": "documenso-app"
|
"endpointId": "documenso-app"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import { FieldType } from '@prisma/client';
|
import { FieldType } from '@prisma/client';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
import type { TFieldAndMeta } from '@documenso/lib/types/field-meta';
|
import type { TFieldAndMeta } from '@documenso/lib/types/field-meta';
|
||||||
import { toCheckboxCustomText } from '@documenso/lib/utils/fields';
|
import { toCheckboxCustomText } from '@documenso/lib/utils/fields';
|
||||||
@ -13,11 +15,66 @@ export type FieldTestData = TFieldAndMeta & {
|
|||||||
signature?: string;
|
signature?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const columnWidth = 19.125;
|
export const signatureBase64Demo = `data:image/png;base64,${fs.readFileSync(
|
||||||
const rowHeight = 6.7;
|
path.join(__dirname, '../../../packages/assets/', 'logo_icon.png'),
|
||||||
|
'base64',
|
||||||
|
)}`;
|
||||||
|
|
||||||
const alignmentGridStartX = 31;
|
const columnWidth = 19.125;
|
||||||
const alignmentGridStartY = 19.02;
|
const fullColumnWidth = 57.37499999999998;
|
||||||
|
const rowHeight = 6.7;
|
||||||
|
const rowPadding = 0;
|
||||||
|
|
||||||
|
const calculatePositionPageOne = (
|
||||||
|
row: number,
|
||||||
|
column: number,
|
||||||
|
width: 'full' | 'column' = 'column',
|
||||||
|
) => {
|
||||||
|
const alignmentGridStartX = 31;
|
||||||
|
const alignmentGridStartY = 19;
|
||||||
|
|
||||||
|
return {
|
||||||
|
height: rowHeight,
|
||||||
|
width: width === 'full' ? fullColumnWidth : columnWidth,
|
||||||
|
positionX: alignmentGridStartX + (column ?? 0) * columnWidth,
|
||||||
|
positionY: alignmentGridStartY + row * (rowHeight + rowPadding),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculatePositionPageTwo = (
|
||||||
|
row: number,
|
||||||
|
column: number,
|
||||||
|
width: 'full' | 'column' = 'column',
|
||||||
|
) => {
|
||||||
|
const alignmentGridStartX = 31;
|
||||||
|
const alignmentGridStartY = 16.35;
|
||||||
|
|
||||||
|
return {
|
||||||
|
height: rowHeight,
|
||||||
|
width: width === 'full' ? fullColumnWidth : columnWidth,
|
||||||
|
positionX: alignmentGridStartX + (column ?? 0) * columnWidth,
|
||||||
|
positionY: alignmentGridStartY + row * (rowHeight + rowPadding),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculatePositionPageThree = (
|
||||||
|
row: number,
|
||||||
|
column: number,
|
||||||
|
width: 'full' | 'column' = 'column',
|
||||||
|
rowQuantity: number = 1,
|
||||||
|
) => {
|
||||||
|
const alignmentGridStartX = 31;
|
||||||
|
const alignmentGridStartY = 16.4;
|
||||||
|
|
||||||
|
const rowHeight = 6.8;
|
||||||
|
|
||||||
|
return {
|
||||||
|
height: rowHeight * rowQuantity,
|
||||||
|
width: width === 'full' ? fullColumnWidth : columnWidth,
|
||||||
|
positionX: alignmentGridStartX + (column ?? 0) * columnWidth,
|
||||||
|
positionY: alignmentGridStartY + row * (rowHeight + rowPadding),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
||||||
/**
|
/**
|
||||||
@ -31,10 +88,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'email',
|
type: 'email',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(0, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'admin@documenso.com',
|
customText: 'admin@documenso.com',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -44,10 +98,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'email',
|
type: 'email',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(0, 1),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'admin@documenso.com',
|
customText: 'admin@documenso.com',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -58,10 +109,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'email',
|
type: 'email',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(0, 2),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'admin@documenso.com',
|
customText: 'admin@documenso.com',
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -75,10 +123,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'name',
|
type: 'name',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(1, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'John Doe',
|
customText: 'John Doe',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -88,10 +133,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'name',
|
type: 'name',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(1, 1),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'John Doe',
|
customText: 'John Doe',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -102,10 +144,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'name',
|
type: 'name',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(1, 2),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'John Doe',
|
customText: 'John Doe',
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -119,10 +158,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'date',
|
type: 'date',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(2, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '123456789',
|
customText: '123456789',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -132,10 +168,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'date',
|
type: 'date',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(2, 1),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '123456789',
|
customText: '123456789',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -146,10 +179,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'date',
|
type: 'date',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(2, 2),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '123456789',
|
customText: '123456789',
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -163,10 +193,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(3, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '123456789',
|
customText: '123456789',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -176,10 +203,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(3, 1),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '123456789',
|
customText: '123456789',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -190,10 +214,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(3, 2),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '123456789',
|
customText: '123456789',
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -207,10 +228,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(4, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '123456789',
|
customText: '123456789',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -220,10 +238,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(4, 1),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '123456789',
|
customText: '123456789',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -234,10 +249,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(4, 2),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '123456789',
|
customText: '123456789',
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -251,10 +263,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'initials',
|
type: 'initials',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(5, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'JD',
|
customText: 'JD',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -264,10 +273,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'initials',
|
type: 'initials',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(5, 1),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'JD',
|
customText: 'JD',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -278,10 +284,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'initials',
|
type: 'initials',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(5, 2),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'JD',
|
customText: 'JD',
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -299,10 +302,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(6, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '0',
|
customText: '0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -312,15 +312,12 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'radio',
|
type: 'radio',
|
||||||
values: [
|
values: [
|
||||||
{ id: 1, checked: false, value: 'Option 1' },
|
{ id: 1, checked: false, value: 'Option 1' },
|
||||||
{ id: 2, checked: true, value: 'Option 2' },
|
{ id: 2, checked: false, value: 'Option 2' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(6, 1),
|
||||||
width: columnWidth,
|
customText: '',
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '2',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.RADIO,
|
type: FieldType.RADIO,
|
||||||
@ -330,15 +327,12 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'radio',
|
type: 'radio',
|
||||||
values: [
|
values: [
|
||||||
{ id: 1, checked: false, value: 'Option 1' },
|
{ id: 1, checked: false, value: 'Option 1' },
|
||||||
{ id: 2, checked: false, value: 'Option 2' },
|
{ id: 2, checked: true, value: 'Option 2' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(6, 2),
|
||||||
width: columnWidth,
|
customText: '1',
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '',
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Row 8 Checkbox
|
* Row 8 Checkbox
|
||||||
@ -355,10 +349,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(7, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: toCheckboxCustomText([0]),
|
customText: toCheckboxCustomText([0]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -368,15 +359,12 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
values: [
|
values: [
|
||||||
{ id: 1, checked: false, value: 'Option 1' },
|
{ id: 1, checked: false, value: 'Option 1' },
|
||||||
{ id: 2, checked: true, value: 'Option 2' },
|
{ id: 2, checked: false, value: 'Option 2' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(7, 1),
|
||||||
width: columnWidth,
|
customText: '',
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: toCheckboxCustomText([1]),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.CHECKBOX,
|
type: FieldType.CHECKBOX,
|
||||||
@ -386,15 +374,12 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
values: [
|
values: [
|
||||||
{ id: 1, checked: false, value: 'Option 1' },
|
{ id: 1, checked: false, value: 'Option 1' },
|
||||||
{ id: 2, checked: false, value: 'Option 2' },
|
{ id: 2, checked: true, value: 'Option 2' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(7, 2),
|
||||||
width: columnWidth,
|
customText: toCheckboxCustomText([1]),
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '',
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Row 8 Dropdown
|
* Row 8 Dropdown
|
||||||
@ -407,10 +392,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(8, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'Option 1',
|
customText: 'Option 1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -420,10 +402,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(8, 1),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'Option 1',
|
customText: 'Option 1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -434,10 +413,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(8, 2),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: 'Option 1',
|
customText: 'Option 1',
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -450,10 +426,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'signature',
|
type: 'signature',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(9, 0),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '',
|
customText: '',
|
||||||
signature: 'My Signature',
|
signature: 'My Signature',
|
||||||
},
|
},
|
||||||
@ -463,10 +436,7 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'signature',
|
type: 'signature',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(9, 1),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '',
|
customText: '',
|
||||||
signature: 'My Signature',
|
signature: 'My Signature',
|
||||||
},
|
},
|
||||||
@ -477,22 +447,295 @@ export const ALIGNMENT_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'signature',
|
type: 'signature',
|
||||||
},
|
},
|
||||||
page: 1,
|
page: 1,
|
||||||
height: rowHeight,
|
...calculatePositionPageOne(9, 2),
|
||||||
width: columnWidth,
|
|
||||||
positionX: 0,
|
|
||||||
positionY: 0,
|
|
||||||
customText: '',
|
customText: '',
|
||||||
signature: 'My Signature',
|
signature: 'My Signature',
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||||
|
*
|
||||||
|
* PAGE 2
|
||||||
|
*
|
||||||
|
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||||
|
*/
|
||||||
|
// TEXT GRID ROW 1
|
||||||
|
{
|
||||||
|
type: FieldType.TEXT,
|
||||||
|
fieldMeta: {
|
||||||
|
textAlign: 'left',
|
||||||
|
type: 'text',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(0, 0),
|
||||||
|
customText: 'SOME TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FieldType.TEXT,
|
||||||
|
fieldMeta: {
|
||||||
|
textAlign: 'center',
|
||||||
|
type: 'text',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(0, 1),
|
||||||
|
customText: 'SOME TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FieldType.TEXT,
|
||||||
|
fieldMeta: {
|
||||||
|
textAlign: 'right',
|
||||||
|
type: 'text',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(0, 2),
|
||||||
|
customText: 'SOME TEXT',
|
||||||
|
},
|
||||||
|
// TEXT GRID ROW 2
|
||||||
|
{
|
||||||
|
type: FieldType.TEXT,
|
||||||
|
fieldMeta: {
|
||||||
|
textAlign: 'left',
|
||||||
|
type: 'text',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(1, 0),
|
||||||
|
customText: 'SOME TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FieldType.TEXT,
|
||||||
|
fieldMeta: {
|
||||||
|
textAlign: 'center',
|
||||||
|
type: 'text',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(1, 1),
|
||||||
|
customText: 'SOME TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FieldType.TEXT,
|
||||||
|
fieldMeta: {
|
||||||
|
textAlign: 'right',
|
||||||
|
type: 'text',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(1, 2),
|
||||||
|
customText: 'SOME TEXT',
|
||||||
|
},
|
||||||
|
// TEXT GRID ROW 3
|
||||||
|
{
|
||||||
|
type: FieldType.TEXT,
|
||||||
|
fieldMeta: {
|
||||||
|
textAlign: 'left',
|
||||||
|
type: 'text',
|
||||||
|
verticalAlign: 'bottom',
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(2, 0),
|
||||||
|
customText: 'SOME TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FieldType.TEXT,
|
||||||
|
fieldMeta: {
|
||||||
|
textAlign: 'center',
|
||||||
|
type: 'text',
|
||||||
|
verticalAlign: 'bottom',
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(2, 1),
|
||||||
|
customText: 'SOME TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FieldType.TEXT,
|
||||||
|
fieldMeta: {
|
||||||
|
textAlign: 'right',
|
||||||
|
type: 'text',
|
||||||
|
verticalAlign: 'bottom',
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(2, 2),
|
||||||
|
customText: 'SOME TEXT',
|
||||||
|
},
|
||||||
|
// NUMBER GRID ROW 1
|
||||||
|
{
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
fieldMeta: {
|
||||||
|
textAlign: 'left',
|
||||||
|
type: 'number',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(3, 0),
|
||||||
|
customText: '123456789123456789',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
fieldMeta: {
|
||||||
|
textAlign: 'center',
|
||||||
|
type: 'number',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(3, 1),
|
||||||
|
customText: '123456789123456789',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
fieldMeta: {
|
||||||
|
textAlign: 'right',
|
||||||
|
type: 'number',
|
||||||
|
verticalAlign: 'top',
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(3, 2),
|
||||||
|
customText: '123456789123456789',
|
||||||
|
},
|
||||||
|
// NUMBER GRID ROW 2
|
||||||
|
{
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
fieldMeta: {
|
||||||
|
textAlign: 'left',
|
||||||
|
type: 'number',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(4, 0),
|
||||||
|
customText: '123456789123456789',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
fieldMeta: {
|
||||||
|
textAlign: 'center',
|
||||||
|
type: 'number',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(4, 1),
|
||||||
|
customText: '123456789123456789',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
fieldMeta: {
|
||||||
|
textAlign: 'right',
|
||||||
|
type: 'number',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(4, 2),
|
||||||
|
customText: '123456789123456789',
|
||||||
|
},
|
||||||
|
// NUMBER GRID ROW 3
|
||||||
|
{
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
fieldMeta: {
|
||||||
|
textAlign: 'left',
|
||||||
|
type: 'number',
|
||||||
|
verticalAlign: 'bottom',
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(5, 0),
|
||||||
|
customText: '123456789123456789',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
fieldMeta: {
|
||||||
|
textAlign: 'center',
|
||||||
|
type: 'number',
|
||||||
|
verticalAlign: 'bottom',
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(5, 1),
|
||||||
|
customText: '123456789123456789',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
fieldMeta: {
|
||||||
|
textAlign: 'right',
|
||||||
|
type: 'number',
|
||||||
|
verticalAlign: 'bottom',
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(5, 2),
|
||||||
|
customText: '123456789123456789',
|
||||||
|
},
|
||||||
|
// Text combing
|
||||||
|
{
|
||||||
|
type: FieldType.TEXT,
|
||||||
|
fieldMeta: {
|
||||||
|
type: 'text',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
letterSpacing: 32,
|
||||||
|
characterLimit: 9,
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(6, 0, 'full'),
|
||||||
|
positionX: calculatePositionPageTwo(6, 0, 'full').positionX + 1.75,
|
||||||
|
width: calculatePositionPageTwo(6, 0, 'full').width + 1.75,
|
||||||
|
customText: 'HEY HEY 1',
|
||||||
|
},
|
||||||
|
// Number combing
|
||||||
|
{
|
||||||
|
type: FieldType.NUMBER,
|
||||||
|
fieldMeta: {
|
||||||
|
type: 'number',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
letterSpacing: 32,
|
||||||
|
},
|
||||||
|
page: 2,
|
||||||
|
...calculatePositionPageTwo(7, 0, 'full'),
|
||||||
|
positionX: calculatePositionPageTwo(7, 0, 'full').positionX + 1.75,
|
||||||
|
width: calculatePositionPageTwo(7, 0, 'full').width + 1.75,
|
||||||
|
|
||||||
|
customText: '123456789',
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||||
|
*
|
||||||
|
* PAGE 2 TEXT MULTILINE
|
||||||
|
*
|
||||||
|
* @@@@@@@@@@@@@@@@@@@@@@@
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
type: FieldType.TEXT,
|
||||||
|
fieldMeta: {
|
||||||
|
verticalAlign: 'top',
|
||||||
|
textAlign: 'left',
|
||||||
|
lineHeight: 2.24,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
page: 3,
|
||||||
|
...calculatePositionPageThree(0, 0, 'full', 3),
|
||||||
|
customText:
|
||||||
|
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FieldType.TEXT,
|
||||||
|
fieldMeta: {
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: 2.24,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
page: 3,
|
||||||
|
...calculatePositionPageThree(3, 0, 'full', 3),
|
||||||
|
customText:
|
||||||
|
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: FieldType.TEXT,
|
||||||
|
fieldMeta: {
|
||||||
|
verticalAlign: 'bottom',
|
||||||
|
textAlign: 'right',
|
||||||
|
lineHeight: 2.24,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
page: 3,
|
||||||
|
...calculatePositionPageThree(6, 0, 'full', 3),
|
||||||
|
customText:
|
||||||
|
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
|
||||||
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const formatAlignmentTestFields = ALIGNMENT_TEST_FIELDS.map((field, index) => {
|
|
||||||
const row = Math.floor(index / 3);
|
|
||||||
const column = index % 3;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...field,
|
|
||||||
positionX: alignmentGridStartX + column * columnWidth,
|
|
||||||
positionY: alignmentGridStartY + row * rowHeight,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
} from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
|
} from '@documenso/ui/primitives/document-flow/field-items-advanced-settings/constants';
|
||||||
|
|
||||||
import type { FieldTestData } from './field-alignment-pdf';
|
import type { FieldTestData } from './field-alignment-pdf';
|
||||||
|
import { signatureBase64Demo } from './field-alignment-pdf';
|
||||||
|
|
||||||
const columnWidth = 20.1;
|
const columnWidth = 20.1;
|
||||||
const fullColumnWidth = 75.8;
|
const fullColumnWidth = 75.8;
|
||||||
@ -37,7 +38,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
page: 2,
|
page: 2,
|
||||||
...calculatePosition(0, 0),
|
...calculatePosition(0, 0),
|
||||||
customText: '',
|
customText: '',
|
||||||
signature: 'My Signature',
|
signature: signatureBase64Demo,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.SIGNATURE,
|
type: FieldType.SIGNATURE,
|
||||||
@ -47,7 +48,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
page: 2,
|
page: 2,
|
||||||
...calculatePosition(1, 0),
|
...calculatePosition(1, 0),
|
||||||
customText: '',
|
customText: '',
|
||||||
signature: 'My Signature',
|
signature: signatureBase64Demo,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.SIGNATURE,
|
type: FieldType.SIGNATURE,
|
||||||
@ -67,7 +68,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
page: 2,
|
page: 2,
|
||||||
...calculatePosition(3, 0),
|
...calculatePosition(3, 0),
|
||||||
customText: '',
|
customText: '',
|
||||||
signature: 'My Signature',
|
signature: 'My Signature super overflow maybe',
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -80,7 +81,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
},
|
},
|
||||||
page: 3,
|
page: 3,
|
||||||
...calculatePosition(0, 0, 'full'),
|
...calculatePosition(0, 0, 'full'),
|
||||||
customText: '123456789',
|
customText: 'Hello world, this is some random text that I have written here',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.TEXT,
|
type: FieldType.TEXT,
|
||||||
@ -89,7 +90,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
},
|
},
|
||||||
page: 3,
|
page: 3,
|
||||||
...calculatePosition(1, 0),
|
...calculatePosition(1, 0),
|
||||||
customText: '123456789123456789123456789123456789',
|
customText: 'Some text that should overflow correctly',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.TEXT,
|
type: FieldType.TEXT,
|
||||||
@ -109,7 +110,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
},
|
},
|
||||||
page: 3,
|
page: 3,
|
||||||
...calculatePosition(3, 0),
|
...calculatePosition(3, 0),
|
||||||
customText: '123456789',
|
customText: 'Input should have a placeholder text when clicked',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.TEXT,
|
type: FieldType.TEXT,
|
||||||
@ -119,7 +120,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
},
|
},
|
||||||
page: 3,
|
page: 3,
|
||||||
...calculatePosition(3, 1),
|
...calculatePosition(3, 1),
|
||||||
customText: '123456789',
|
customText: 'Should have a label during editing and signing',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.TEXT,
|
type: FieldType.TEXT,
|
||||||
@ -129,7 +130,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
},
|
},
|
||||||
page: 3,
|
page: 3,
|
||||||
...calculatePosition(3, 2),
|
...calculatePosition(3, 2),
|
||||||
customText: '123456789',
|
customText: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.TEXT,
|
type: FieldType.TEXT,
|
||||||
@ -139,20 +140,19 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
},
|
},
|
||||||
page: 3,
|
page: 3,
|
||||||
...calculatePosition(4, 0),
|
...calculatePosition(4, 0),
|
||||||
customText: '123456789',
|
customText: 'This is a required field',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.TEXT,
|
type: FieldType.TEXT,
|
||||||
fieldMeta: {
|
fieldMeta: {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
text: 'Readonly Value',
|
text: 'Some Readonly Value',
|
||||||
},
|
},
|
||||||
page: 3,
|
page: 3,
|
||||||
...calculatePosition(4, 1),
|
...calculatePosition(4, 1),
|
||||||
customText: 'Readonly Value',
|
customText: '',
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PAGE 4 NUMBER
|
* PAGE 4 NUMBER
|
||||||
*/
|
*/
|
||||||
@ -220,7 +220,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: FieldType.NUMBER,
|
type: FieldType.NUMBER,
|
||||||
fieldMeta: {
|
fieldMeta: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
value: '123',
|
value: '123456789',
|
||||||
},
|
},
|
||||||
page: 4,
|
page: 4,
|
||||||
...calculatePosition(3, 2),
|
...calculatePosition(3, 2),
|
||||||
@ -241,10 +241,11 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
fieldMeta: {
|
fieldMeta: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
|
value: '123456789',
|
||||||
},
|
},
|
||||||
page: 4,
|
page: 4,
|
||||||
...calculatePosition(4, 1),
|
...calculatePosition(4, 1),
|
||||||
customText: '123456789',
|
customText: '',
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -272,8 +273,8 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
type: 'radio',
|
type: 'radio',
|
||||||
values: [
|
values: [
|
||||||
{ id: 1, checked: false, value: 'Option 1' },
|
{ id: 1, checked: false, value: 'Option 1' },
|
||||||
{ id: 2, checked: true, value: 'Option 2' },
|
{ id: 2, checked: false, value: 'Option 2' },
|
||||||
{ id: 3, checked: false, value: 'Option 3' },
|
{ id: 3, checked: true, value: 'Option 3' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 5,
|
page: 5,
|
||||||
@ -285,6 +286,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
fieldMeta: {
|
fieldMeta: {
|
||||||
direction: 'vertical',
|
direction: 'vertical',
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
|
required: true,
|
||||||
values: [
|
values: [
|
||||||
{ id: 1, checked: false, value: 'Option 1' },
|
{ id: 1, checked: false, value: 'Option 1' },
|
||||||
{ id: 2, checked: false, value: 'Option 2' },
|
{ id: 2, checked: false, value: 'Option 2' },
|
||||||
@ -293,17 +295,18 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
},
|
},
|
||||||
page: 5,
|
page: 5,
|
||||||
...calculatePosition(2, 0),
|
...calculatePosition(2, 0),
|
||||||
customText: '',
|
customText: '2',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.RADIO,
|
type: FieldType.RADIO,
|
||||||
fieldMeta: {
|
fieldMeta: {
|
||||||
direction: 'vertical',
|
direction: 'vertical',
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
|
readOnly: true,
|
||||||
values: [
|
values: [
|
||||||
{ id: 1, checked: false, value: 'Option 1' },
|
{ id: 1, checked: false, value: 'Option 1' },
|
||||||
{ id: 2, checked: false, value: 'Option 2' },
|
{ id: 2, checked: false, value: 'Option 2' },
|
||||||
{ id: 3, checked: false, value: 'Option 3' },
|
{ id: 3, checked: true, value: 'Option 3' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 5,
|
page: 5,
|
||||||
@ -338,7 +341,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
values: [
|
values: [
|
||||||
{ id: 1, checked: false, value: 'Option 1' },
|
{ id: 1, checked: false, value: 'Option 1' },
|
||||||
{ id: 2, checked: true, value: 'Option 2' },
|
{ id: 2, checked: true, value: 'Option 2' },
|
||||||
{ id: 2, checked: true, value: 'Option 3' },
|
{ id: 3, checked: false, value: 'Option 3' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 6,
|
page: 6,
|
||||||
@ -358,7 +361,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
},
|
},
|
||||||
page: 6,
|
page: 6,
|
||||||
...calculatePosition(2, 0),
|
...calculatePosition(2, 0),
|
||||||
customText: '',
|
customText: toCheckboxCustomText([2]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.CHECKBOX,
|
type: FieldType.CHECKBOX,
|
||||||
@ -368,7 +371,7 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
readOnly: true,
|
readOnly: true,
|
||||||
values: [
|
values: [
|
||||||
{ id: 1, checked: false, value: 'Option 1' },
|
{ id: 1, checked: false, value: 'Option 1' },
|
||||||
{ id: 2, checked: false, value: 'Option 2' },
|
{ id: 2, checked: true, value: 'Option 2' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
page: 6,
|
page: 6,
|
||||||
@ -445,11 +448,11 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
fieldMeta: {
|
fieldMeta: {
|
||||||
values: [{ value: 'Option 1' }, { value: 'Option 2' }],
|
values: [{ value: 'Option 1' }, { value: 'Option 2' }],
|
||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
defaultValue: 'Option 1',
|
defaultValue: 'Option 2',
|
||||||
},
|
},
|
||||||
page: 7,
|
page: 7,
|
||||||
...calculatePosition(1, 0),
|
...calculatePosition(1, 0),
|
||||||
customText: 'Option 1',
|
customText: 'Option 2',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.DROPDOWN,
|
type: FieldType.DROPDOWN,
|
||||||
@ -460,13 +463,14 @@ export const FIELD_META_TEST_FIELDS: FieldTestData[] = [
|
|||||||
},
|
},
|
||||||
page: 7,
|
page: 7,
|
||||||
...calculatePosition(2, 0),
|
...calculatePosition(2, 0),
|
||||||
customText: 'Option 1',
|
customText: 'Option 3',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: FieldType.DROPDOWN,
|
type: FieldType.DROPDOWN,
|
||||||
fieldMeta: {
|
fieldMeta: {
|
||||||
values: [{ value: 'Option 1' }, { value: 'Option 2' }, { value: 'Option 3' }],
|
values: [{ value: 'Option 1' }, { value: 'Option 2' }, { value: 'Option 3' }],
|
||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
|
defaultValue: 'Option 1',
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
},
|
},
|
||||||
page: 7,
|
page: 7,
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import {
|
|||||||
RecipientRole,
|
RecipientRole,
|
||||||
} from '@documenso/prisma/client';
|
} from '@documenso/prisma/client';
|
||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
import type { TCreateEnvelopeItemsRequest } from '@documenso/trpc/server/envelope-router/create-envelope-items.types';
|
import type { TCreateEnvelopeItemsPayload } from '@documenso/trpc/server/envelope-router/create-envelope-items.types';
|
||||||
import type {
|
import type {
|
||||||
TCreateEnvelopePayload,
|
TCreateEnvelopePayload,
|
||||||
TCreateEnvelopeResponse,
|
TCreateEnvelopeResponse,
|
||||||
@ -27,7 +27,7 @@ import type { TCreateEnvelopeRecipientsRequest } from '@documenso/trpc/server/en
|
|||||||
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
|
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
|
||||||
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
|
import type { TUpdateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/update-envelope.types';
|
||||||
|
|
||||||
import { formatAlignmentTestFields } from '../../../constants/field-alignment-pdf';
|
import { ALIGNMENT_TEST_FIELDS } from '../../../constants/field-alignment-pdf';
|
||||||
import { FIELD_META_TEST_FIELDS } from '../../../constants/field-meta-pdf';
|
import { FIELD_META_TEST_FIELDS } from '../../../constants/field-meta-pdf';
|
||||||
|
|
||||||
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
||||||
@ -403,28 +403,20 @@ test.describe('API V2 Envelopes', () => {
|
|||||||
expect(unauthRequest.status()).toBe(404);
|
expect(unauthRequest.status()).toBe(404);
|
||||||
|
|
||||||
// Step 2: Create second envelope item via API
|
// Step 2: Create second envelope item via API
|
||||||
// Todo: Envelopes - Use API Route
|
const createEnvelopeItemsPayload: TCreateEnvelopeItemsPayload = {
|
||||||
const fieldMetaDocumentData = await prisma.documentData.create({
|
|
||||||
data: {
|
|
||||||
type: 'BYTES_64',
|
|
||||||
data: fieldMetaPdf.toString('base64'),
|
|
||||||
initialData: fieldMetaPdf.toString('base64'),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const createEnvelopeItemsRequest: TCreateEnvelopeItemsRequest = {
|
|
||||||
envelopeId: createdEnvelope.id,
|
envelopeId: createdEnvelope.id,
|
||||||
data: [
|
|
||||||
{
|
|
||||||
title: 'Field Meta Test',
|
|
||||||
documentDataId: fieldMetaDocumentData.id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createEnvelopeItemFormData = new FormData();
|
||||||
|
createEnvelopeItemFormData.append('payload', JSON.stringify(createEnvelopeItemsPayload));
|
||||||
|
createEnvelopeItemFormData.append(
|
||||||
|
'files',
|
||||||
|
new File([fieldMetaPdf], 'field-meta.pdf', { type: 'application/pdf' }),
|
||||||
|
);
|
||||||
|
|
||||||
const createItemsRes = await request.post(`${baseUrl}/envelope/item/create-many`, {
|
const createItemsRes = await request.post(`${baseUrl}/envelope/item/create-many`, {
|
||||||
headers: { Authorization: `Bearer ${tokenA}` },
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
data: createEnvelopeItemsRequest,
|
multipart: createEnvelopeItemFormData,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(createItemsRes.ok()).toBeTruthy();
|
expect(createItemsRes.ok()).toBeTruthy();
|
||||||
@ -498,7 +490,7 @@ test.describe('API V2 Envelopes', () => {
|
|||||||
// Step 6: Create fields for first PDF (alignment fields)
|
// Step 6: Create fields for first PDF (alignment fields)
|
||||||
const alignmentFieldsRequest = {
|
const alignmentFieldsRequest = {
|
||||||
envelopeId: createdEnvelope.id,
|
envelopeId: createdEnvelope.id,
|
||||||
data: formatAlignmentTestFields.map((field) => ({
|
data: ALIGNMENT_TEST_FIELDS.map((field) => ({
|
||||||
recipientId,
|
recipientId,
|
||||||
envelopeItemId: alignmentItem.id,
|
envelopeItemId: alignmentItem.id,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
@ -555,9 +547,14 @@ test.describe('API V2 Envelopes', () => {
|
|||||||
expect(finalEnvelope.envelopeItems.length).toBe(2);
|
expect(finalEnvelope.envelopeItems.length).toBe(2);
|
||||||
expect(finalEnvelope.recipients.length).toBe(1);
|
expect(finalEnvelope.recipients.length).toBe(1);
|
||||||
expect(finalEnvelope.fields.length).toBe(
|
expect(finalEnvelope.fields.length).toBe(
|
||||||
formatAlignmentTestFields.length + FIELD_META_TEST_FIELDS.length,
|
ALIGNMENT_TEST_FIELDS.length + FIELD_META_TEST_FIELDS.length,
|
||||||
);
|
);
|
||||||
expect(finalEnvelope.title).toBe('Envelope Full Field Test');
|
expect(finalEnvelope.title).toBe('Envelope Full Field Test');
|
||||||
expect(finalEnvelope.type).toBe(EnvelopeType.DOCUMENT);
|
expect(finalEnvelope.type).toBe(EnvelopeType.DOCUMENT);
|
||||||
|
|
||||||
|
console.log({
|
||||||
|
createdEnvelopeId: finalEnvelope.id,
|
||||||
|
userEmail: userA.email,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import {
|
|||||||
import { seedBlankFolder } from '@documenso/prisma/seed/folders';
|
import { seedBlankFolder } from '@documenso/prisma/seed/folders';
|
||||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||||
import { seedUser } from '@documenso/prisma/seed/users';
|
import { seedUser } from '@documenso/prisma/seed/users';
|
||||||
|
import type { TCreateEnvelopeItemsPayload } from '@documenso/trpc/server/envelope-router/create-envelope-items.types';
|
||||||
|
|
||||||
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
||||||
|
|
||||||
@ -3855,25 +3856,24 @@ test.describe('Document API V2', () => {
|
|||||||
}) => {
|
}) => {
|
||||||
const doc = await seedBlankDocument(userA, teamA.id);
|
const doc = await seedBlankDocument(userA, teamA.id);
|
||||||
|
|
||||||
const documentData = await prisma.documentData.create({
|
const fieldMetaPdf = fs.readFileSync(
|
||||||
data: {
|
path.join(__dirname, '../../../../../assets/field-meta.pdf'),
|
||||||
type: 'BYTES_64',
|
);
|
||||||
data: Buffer.from('test pdf content').toString('base64'),
|
|
||||||
initialData: Buffer.from('test pdf content').toString('base64'),
|
const createEnvelopeItemsPayload: TCreateEnvelopeItemsPayload = {
|
||||||
},
|
envelopeId: doc.id,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(createEnvelopeItemsPayload));
|
||||||
|
formData.append(
|
||||||
|
'files',
|
||||||
|
new File([fieldMetaPdf], 'field-meta.pdf', { type: 'application/pdf' }),
|
||||||
|
);
|
||||||
|
|
||||||
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/item/create-many`, {
|
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/item/create-many`, {
|
||||||
headers: { Authorization: `Bearer ${tokenB}` },
|
headers: { Authorization: `Bearer ${tokenB}` },
|
||||||
data: {
|
multipart: formData,
|
||||||
envelopeId: doc.id,
|
|
||||||
data: [
|
|
||||||
{
|
|
||||||
title: 'New Item',
|
|
||||||
documentDataId: documentData.id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.ok()).toBeFalsy();
|
expect(res.ok()).toBeFalsy();
|
||||||
@ -3885,29 +3885,48 @@ test.describe('Document API V2', () => {
|
|||||||
}) => {
|
}) => {
|
||||||
const doc = await seedBlankDocument(userA, teamA.id);
|
const doc = await seedBlankDocument(userA, teamA.id);
|
||||||
|
|
||||||
const documentData = await prisma.documentData.create({
|
const fieldMetaPdf = fs.readFileSync(
|
||||||
data: {
|
path.join(__dirname, '../../../../../assets/field-meta.pdf'),
|
||||||
type: 'BYTES_64',
|
);
|
||||||
data: Buffer.from('test pdf content').toString('base64'),
|
|
||||||
initialData: Buffer.from('test pdf content').toString('base64'),
|
const createEnvelopeItemsPayload: TCreateEnvelopeItemsPayload = {
|
||||||
},
|
envelopeId: doc.id,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('payload', JSON.stringify(createEnvelopeItemsPayload));
|
||||||
|
formData.append(
|
||||||
|
'files',
|
||||||
|
new File([fieldMetaPdf], 'field-meta-1.pdf', { type: 'application/pdf' }),
|
||||||
|
);
|
||||||
|
formData.append(
|
||||||
|
'files',
|
||||||
|
new File([fieldMetaPdf], 'field-meta-2.pdf', { type: 'application/pdf' }),
|
||||||
|
);
|
||||||
|
|
||||||
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/item/create-many`, {
|
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/item/create-many`, {
|
||||||
headers: { Authorization: `Bearer ${tokenA}` },
|
headers: { Authorization: `Bearer ${tokenA}` },
|
||||||
data: {
|
multipart: formData,
|
||||||
envelopeId: doc.id,
|
});
|
||||||
data: [
|
|
||||||
{
|
const envelope = await prisma.envelope.findFirstOrThrow({
|
||||||
title: 'New Item',
|
where: {
|
||||||
documentDataId: documentData.id,
|
id: doc.id,
|
||||||
},
|
},
|
||||||
],
|
include: {
|
||||||
|
envelopeItems: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(res.ok()).toBeTruthy();
|
expect(res.ok()).toBeTruthy();
|
||||||
expect(res.status()).toBe(200);
|
expect(res.status()).toBe(200);
|
||||||
|
|
||||||
|
const envelopeItems = envelope.envelopeItems;
|
||||||
|
|
||||||
|
// 3 Files because seed creates one automatically.
|
||||||
|
expect(envelopeItems.length).toBe(3);
|
||||||
|
expect(envelopeItems[1].title).toBe('field-meta-1.pdf');
|
||||||
|
expect(envelopeItems[2].title).toBe('field-meta-2.pdf');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -21,35 +21,226 @@ import pixelMatch from 'pixelmatch';
|
|||||||
import { PNG } from 'pngjs';
|
import { PNG } from 'pngjs';
|
||||||
import type { TestInfo } from '@playwright/test';
|
import type { TestInfo } from '@playwright/test';
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
import { DocumentStatus } from '@prisma/client';
|
import { DocumentStatus, EnvelopeType } 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 { getEnvelopeItemPdfUrl } 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';
|
||||||
|
|
||||||
import { apiSignin } from '../fixtures/authentication';
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
import type {
|
||||||
|
TCreateEnvelopePayload,
|
||||||
|
TCreateEnvelopeResponse,
|
||||||
|
} from '../../../trpc/server/envelope-router/create-envelope.types';
|
||||||
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../../lib/constants/app';
|
||||||
|
import { createApiToken } from '../../../lib/server-only/public-api/create-api-token';
|
||||||
|
import { RecipientRole } from '../../../prisma/generated/types';
|
||||||
|
import { FIELD_META_TEST_FIELDS } from '../../constants/field-meta-pdf';
|
||||||
|
import { ALIGNMENT_TEST_FIELDS } from '../../constants/field-alignment-pdf';
|
||||||
|
import type { TDistributeEnvelopeRequest } from '../../../trpc/server/envelope-router/distribute-envelope.types';
|
||||||
|
import { isBase64Image } from '../../../lib/constants/signatures';
|
||||||
|
|
||||||
|
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
|
||||||
|
const baseUrl = `${WEBAPP_BASE_URL}/api/v2`;
|
||||||
|
|
||||||
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
test.describe.configure({ mode: 'parallel', timeout: 60000 });
|
||||||
|
|
||||||
test('field placement visual regression', async ({ page }, testInfo) => {
|
test.skip('seed alignment test document', async ({ page }) => {
|
||||||
const { user, team } = await seedUser();
|
const user = await prisma.user.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
const envelope = await seedAlignmentTestDocument({
|
email: 'example@documenso.com',
|
||||||
userId: user.id,
|
},
|
||||||
teamId: team.id,
|
include: {
|
||||||
recipientName: user.name || '',
|
ownedOrganisations: {
|
||||||
recipientEmail: user.email,
|
include: {
|
||||||
insertFields: true,
|
teams: true,
|
||||||
status: DocumentStatus.PENDING,
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const token = envelope.recipients[0].token;
|
const userId = user.id;
|
||||||
|
const teamId = user.ownedOrganisations[0].teams[0].id;
|
||||||
|
|
||||||
const signUrl = `/sign/${token}`;
|
await seedAlignmentTestDocument({
|
||||||
|
userId,
|
||||||
|
teamId,
|
||||||
|
recipientName: user.name || '',
|
||||||
|
recipientEmail: user.email,
|
||||||
|
insertFields: false,
|
||||||
|
status: DocumentStatus.DRAFT,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('field placement visual regression', async ({ page, request }, testInfo) => {
|
||||||
|
const { user, team } = await seedUser();
|
||||||
|
|
||||||
|
const { token } = await createApiToken({
|
||||||
|
userId: user.id,
|
||||||
|
teamId: team.id,
|
||||||
|
tokenName: 'test',
|
||||||
|
expiresIn: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 1: Create initial envelope with Prisma (with first envelope item)
|
||||||
|
const alignmentPdf = fs.readFileSync(
|
||||||
|
path.join(__dirname, '../../../../assets/field-font-alignment.pdf'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const fieldMetaPdf = fs.readFileSync(path.join(__dirname, '../../../../assets/field-meta.pdf'));
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
const fieldMetaFields = FIELD_META_TEST_FIELDS.map((field) => ({
|
||||||
|
identifier: 'field-meta',
|
||||||
|
type: field.type,
|
||||||
|
page: field.page,
|
||||||
|
positionX: field.positionX,
|
||||||
|
positionY: field.positionY,
|
||||||
|
width: field.width,
|
||||||
|
height: field.height,
|
||||||
|
fieldMeta: field.fieldMeta,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const alignmentFields = ALIGNMENT_TEST_FIELDS.map((field) => ({
|
||||||
|
identifier: 'alignment-pdf',
|
||||||
|
type: field.type,
|
||||||
|
page: field.page,
|
||||||
|
positionX: field.positionX,
|
||||||
|
positionY: field.positionY,
|
||||||
|
width: field.width,
|
||||||
|
height: field.height,
|
||||||
|
fieldMeta: field.fieldMeta,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const createEnvelopePayload: TCreateEnvelopePayload = {
|
||||||
|
type: EnvelopeType.DOCUMENT,
|
||||||
|
title: 'Envelope Full Field Test',
|
||||||
|
recipients: [
|
||||||
|
{
|
||||||
|
email: user.email,
|
||||||
|
name: user.name || '',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
fields: [...fieldMetaFields, ...alignmentFields],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
formData.append('payload', JSON.stringify(createEnvelopePayload));
|
||||||
|
|
||||||
|
formData.append('files', new File([alignmentPdf], 'alignment-pdf', { type: 'application/pdf' }));
|
||||||
|
formData.append('files', new File([fieldMetaPdf], 'field-meta', { type: 'application/pdf' }));
|
||||||
|
|
||||||
|
const createEnvelopeRequest = await request.post(`${baseUrl}/envelope/create`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
multipart: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createEnvelopeRequest.ok()).toBeTruthy();
|
||||||
|
expect(createEnvelopeRequest.status()).toBe(200);
|
||||||
|
|
||||||
|
const { id: createdEnvelopeId }: TCreateEnvelopeResponse = await createEnvelopeRequest.json();
|
||||||
|
|
||||||
|
const envelope = await prisma.envelope.findUniqueOrThrow({
|
||||||
|
where: {
|
||||||
|
id: createdEnvelopeId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
recipients: true,
|
||||||
|
envelopeItems: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const recipientId = envelope.recipients[0].id;
|
||||||
|
const alignmentItem = envelope.envelopeItems.find((item: { order: number }) => item.order === 1);
|
||||||
|
const fieldMetaItem = envelope.envelopeItems.find((item: { order: number }) => item.order === 2);
|
||||||
|
|
||||||
|
expect(recipientId).toBeDefined();
|
||||||
|
expect(alignmentItem).toBeDefined();
|
||||||
|
expect(fieldMetaItem).toBeDefined();
|
||||||
|
|
||||||
|
if (!alignmentItem || !fieldMetaItem) {
|
||||||
|
throw new Error('Envelope items not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const distributeEnvelopeRequest = await request.post(`${baseUrl}/envelope/distribute`, {
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
data: {
|
||||||
|
envelopeId: envelope.id,
|
||||||
|
} satisfies TDistributeEnvelopeRequest,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(distributeEnvelopeRequest.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
const uninsertedFields = await prisma.field.findMany({
|
||||||
|
where: {
|
||||||
|
envelopeId: envelope.id,
|
||||||
|
inserted: false,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
envelopeItem: {
|
||||||
|
select: {
|
||||||
|
title: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
uninsertedFields.map(async (field) => {
|
||||||
|
let foundField = ALIGNMENT_TEST_FIELDS.find(
|
||||||
|
(f) =>
|
||||||
|
field.page === f.page &&
|
||||||
|
field.envelopeItem.title === 'alignment-pdf' &&
|
||||||
|
Number(field.positionX).toFixed(2) === f.positionX.toFixed(2) &&
|
||||||
|
Number(field.positionY).toFixed(2) === f.positionY.toFixed(2) &&
|
||||||
|
Number(field.width).toFixed(2) === f.width.toFixed(2) &&
|
||||||
|
Number(field.height).toFixed(2) === f.height.toFixed(2),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!foundField) {
|
||||||
|
foundField = FIELD_META_TEST_FIELDS.find(
|
||||||
|
(f) =>
|
||||||
|
field.page === f.page &&
|
||||||
|
field.envelopeItem.title === 'field-meta' &&
|
||||||
|
Number(field.positionX).toFixed(2) === f.positionX.toFixed(2) &&
|
||||||
|
Number(field.positionY).toFixed(2) === f.positionY.toFixed(2) &&
|
||||||
|
Number(field.width).toFixed(2) === f.width.toFixed(2) &&
|
||||||
|
Number(field.height).toFixed(2) === f.height.toFixed(2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!foundField) {
|
||||||
|
throw new Error('Field not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.field.update({
|
||||||
|
where: {
|
||||||
|
id: field.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
inserted: true,
|
||||||
|
customText: foundField.customText,
|
||||||
|
signature: foundField.signature
|
||||||
|
? {
|
||||||
|
create: {
|
||||||
|
recipientId: envelope.recipients[0].id,
|
||||||
|
signatureImageAsBase64: isBase64Image(foundField.signature)
|
||||||
|
? foundField.signature
|
||||||
|
: null,
|
||||||
|
typedSignature: isBase64Image(foundField.signature) ? null : foundField.signature,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const recipientToken = envelope.recipients[0].token;
|
||||||
|
const signUrl = `/sign/${recipientToken}`;
|
||||||
|
|
||||||
await apiSignin({
|
await apiSignin({
|
||||||
page,
|
page,
|
||||||
@ -95,7 +286,14 @@ 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 = getEnvelopeItemPdfUrl({
|
||||||
|
type: 'download',
|
||||||
|
envelopeItem: item,
|
||||||
|
token: recipientToken,
|
||||||
|
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 +301,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 +372,16 @@ 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 = getEnvelopeItemPdfUrl({
|
||||||
|
type: 'download',
|
||||||
|
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(
|
||||||
@ -276,7 +481,7 @@ const compareSignedPdfWithImages = async ({
|
|||||||
// Expect the certificate to NOT be blank. Since the storedImage is blank.
|
// Expect the certificate to NOT be blank. Since the storedImage is blank.
|
||||||
expect.soft(comparison).toBeGreaterThan(20000);
|
expect.soft(comparison).toBeGreaterThan(20000);
|
||||||
} else {
|
} else {
|
||||||
expect.soft(comparison).toEqual(0);
|
expect.soft(comparison).toBeLessThan(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 { getEnvelopeItemPdfUrl } 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,26 @@ 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 = getEnvelopeItemPdfUrl({
|
||||||
|
type: 'download',
|
||||||
|
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 +84,18 @@ test.describe('Signing Certificate Tests', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const firstDocumentData = completedDocument.envelopeItems[0].documentData;
|
const firstDocumentData = completedDocument.envelopeItems[0];
|
||||||
|
|
||||||
const completedDocumentData = await getFile(firstDocumentData);
|
const documentUrl = getEnvelopeItemPdfUrl({
|
||||||
|
type: 'download',
|
||||||
|
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 +132,26 @@ 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 = getEnvelopeItemPdfUrl({
|
||||||
|
type: 'download',
|
||||||
|
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 +189,18 @@ test.describe('Signing Certificate Tests', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const firstDocumentData = completedDocument.envelopeItems[0].documentData;
|
const firstDocumentData = completedDocument.envelopeItems[0];
|
||||||
|
|
||||||
const completedDocumentData = await getFile(firstDocumentData);
|
const documentUrl = getEnvelopeItemPdfUrl({
|
||||||
|
type: 'download',
|
||||||
|
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 +237,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 = getEnvelopeItemPdfUrl({
|
||||||
|
type: 'download',
|
||||||
|
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 +294,16 @@ test.describe('Signing Certificate Tests', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const completedDocumentData = await getFile(completedDocument.envelopeItems[0].documentData);
|
const documentUrl = getEnvelopeItemPdfUrl({
|
||||||
|
type: 'download',
|
||||||
|
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);
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
import { DocumentDataType, TeamMemberRole } from '@prisma/client';
|
import { DocumentDataType, TeamMemberRole } from '@prisma/client';
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||||
@ -12,6 +11,10 @@ import { seedUser } from '@documenso/prisma/seed/users';
|
|||||||
import { apiSignin } from '../fixtures/authentication';
|
import { apiSignin } from '../fixtures/authentication';
|
||||||
|
|
||||||
const EXAMPLE_PDF_PATH = path.join(__dirname, '../../../../assets/example.pdf');
|
const EXAMPLE_PDF_PATH = path.join(__dirname, '../../../../assets/example.pdf');
|
||||||
|
const FIELD_ALIGNMENT_TEST_PDF_PATH = path.join(
|
||||||
|
__dirname,
|
||||||
|
'../../../../assets/field-font-alignment.pdf',
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. Create a template with all settings filled out
|
* 1. Create a template with all settings filled out
|
||||||
@ -233,10 +236,6 @@ test('[TEMPLATE]: should create a document from a template with custom document'
|
|||||||
const { user, team } = await seedUser();
|
const { user, team } = await seedUser();
|
||||||
const template = await seedBlankTemplate(user, team.id);
|
const template = await seedBlankTemplate(user, team.id);
|
||||||
|
|
||||||
// Create a temporary PDF file for upload
|
|
||||||
|
|
||||||
const pdfContent = fs.readFileSync(EXAMPLE_PDF_PATH).toString('base64');
|
|
||||||
|
|
||||||
await apiSignin({
|
await apiSignin({
|
||||||
page,
|
page,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
@ -277,7 +276,7 @@ test('[TEMPLATE]: should create a document from a template with custom document'
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await fileChooser.setFiles(EXAMPLE_PDF_PATH);
|
await fileChooser.setFiles(FIELD_ALIGNMENT_TEST_PDF_PATH);
|
||||||
|
|
||||||
// Wait for upload to complete
|
// Wait for upload to complete
|
||||||
await expect(page.getByText('Remove')).toBeVisible();
|
await expect(page.getByText('Remove')).toBeVisible();
|
||||||
@ -314,8 +313,12 @@ test('[TEMPLATE]: should create a document from a template with custom document'
|
|||||||
expect(firstDocumentData.type).toEqual(expectedDocumentDataType);
|
expect(firstDocumentData.type).toEqual(expectedDocumentDataType);
|
||||||
|
|
||||||
if (expectedDocumentDataType === DocumentDataType.BYTES_64) {
|
if (expectedDocumentDataType === DocumentDataType.BYTES_64) {
|
||||||
expect(firstDocumentData.data).toEqual(pdfContent);
|
// Todo: Doesn't really work due to normalization of the PDF which won't let us directly compare the data.
|
||||||
expect(firstDocumentData.initialData).toEqual(pdfContent);
|
// Probably need to do a pixel match
|
||||||
|
expect(firstDocumentData.data).not.toEqual(template.envelopeItems[0].documentData.data);
|
||||||
|
expect(firstDocumentData.initialData).not.toEqual(
|
||||||
|
template.envelopeItems[0].documentData.initialData,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// For S3, we expect the data/initialData to be the S3 path (non-empty string)
|
// For S3, we expect the data/initialData to be the S3 path (non-empty string)
|
||||||
expect(firstDocumentData.data).toBeTruthy();
|
expect(firstDocumentData.data).toBeTruthy();
|
||||||
@ -336,8 +339,6 @@ test('[TEMPLATE]: should create a team document from a template with custom docu
|
|||||||
|
|
||||||
const template = await seedBlankTemplate(owner, team.id);
|
const template = await seedBlankTemplate(owner, team.id);
|
||||||
|
|
||||||
const pdfContent = fs.readFileSync(EXAMPLE_PDF_PATH).toString('base64');
|
|
||||||
|
|
||||||
await apiSignin({
|
await apiSignin({
|
||||||
page,
|
page,
|
||||||
email: owner.email,
|
email: owner.email,
|
||||||
@ -378,7 +379,7 @@ test('[TEMPLATE]: should create a team document from a template with custom docu
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await fileChooser.setFiles(EXAMPLE_PDF_PATH);
|
await fileChooser.setFiles(FIELD_ALIGNMENT_TEST_PDF_PATH);
|
||||||
|
|
||||||
// Wait for upload to complete
|
// Wait for upload to complete
|
||||||
await expect(page.getByText('Remove')).toBeVisible();
|
await expect(page.getByText('Remove')).toBeVisible();
|
||||||
@ -416,8 +417,12 @@ test('[TEMPLATE]: should create a team document from a template with custom docu
|
|||||||
expect(firstDocumentData.type).toEqual(expectedDocumentDataType);
|
expect(firstDocumentData.type).toEqual(expectedDocumentDataType);
|
||||||
|
|
||||||
if (expectedDocumentDataType === DocumentDataType.BYTES_64) {
|
if (expectedDocumentDataType === DocumentDataType.BYTES_64) {
|
||||||
expect(firstDocumentData.data).toEqual(pdfContent);
|
// Todo: Doesn't really work due to normalization of the PDF which won't let us directly compare the data.
|
||||||
expect(firstDocumentData.initialData).toEqual(pdfContent);
|
// Probably need to do a pixel match
|
||||||
|
expect(firstDocumentData.data).not.toEqual(template.envelopeItems[0].documentData.data);
|
||||||
|
expect(firstDocumentData.initialData).not.toEqual(
|
||||||
|
template.envelopeItems[0].documentData.initialData,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// For S3, we expect the data/initialData to be the S3 path (non-empty string)
|
// For S3, we expect the data/initialData to be the S3 path (non-empty string)
|
||||||
expect(firstDocumentData.data).toBeTruthy();
|
expect(firstDocumentData.data).toBeTruthy();
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 102 KiB |
BIN
packages/app-tests/visual-regression/alignment-pdf-2.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
packages/app-tests/visual-regression/alignment-pdf-3.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |