mirror of
https://github.com/documenso/documenso.git
synced 2025-11-11 13:02:31 +10:00
Compare commits
2 Commits
experiment
...
v1.9.0-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
| 07c852744b | |||
| 4fab98c633 |
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/web",
|
"name": "@documenso/web",
|
||||||
"version": "1.9.0-rc.5",
|
"version": "1.9.0-rc.6",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -7,15 +7,17 @@ import { useRouter } from 'next/navigation';
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Trans, msg } from '@lingui/macro';
|
import { Trans, msg } from '@lingui/macro';
|
||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { InfoIcon, Plus } from 'lucide-react';
|
import { InfoIcon, Plus, Upload, X } from 'lucide-react';
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
import * as z from 'zod';
|
import * as z from 'zod';
|
||||||
|
|
||||||
|
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||||
import {
|
import {
|
||||||
TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX,
|
TEMPLATE_RECIPIENT_EMAIL_PLACEHOLDER_REGEX,
|
||||||
TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX,
|
TEMPLATE_RECIPIENT_NAME_PLACEHOLDER_REGEX,
|
||||||
} from '@documenso/lib/constants/template';
|
} from '@documenso/lib/constants/template';
|
||||||
import { AppError } from '@documenso/lib/errors/app-error';
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
|
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
import { DocumentDistributionMethod, DocumentSigningOrder } from '@documenso/prisma/client';
|
import { DocumentDistributionMethod, DocumentSigningOrder } from '@documenso/prisma/client';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
@ -50,6 +52,11 @@ import { useOptionalCurrentTeam } from '~/providers/team';
|
|||||||
const ZAddRecipientsForNewDocumentSchema = z
|
const ZAddRecipientsForNewDocumentSchema = z
|
||||||
.object({
|
.object({
|
||||||
distributeDocument: z.boolean(),
|
distributeDocument: z.boolean(),
|
||||||
|
useCustomDocument: z.boolean().default(false),
|
||||||
|
customDocumentData: z
|
||||||
|
.any()
|
||||||
|
.refine((data) => data instanceof File || data === undefined)
|
||||||
|
.optional(),
|
||||||
recipients: z.array(
|
recipients: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
@ -119,6 +126,8 @@ export function UseTemplateDialog({
|
|||||||
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
|
resolver: zodResolver(ZAddRecipientsForNewDocumentSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
distributeDocument: false,
|
distributeDocument: false,
|
||||||
|
useCustomDocument: false,
|
||||||
|
customDocumentData: undefined,
|
||||||
recipients: recipients
|
recipients: recipients
|
||||||
.sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0))
|
.sort((a, b) => (a.signingOrder || 0) - (b.signingOrder || 0))
|
||||||
.map((recipient) => {
|
.map((recipient) => {
|
||||||
@ -145,11 +154,19 @@ export function UseTemplateDialog({
|
|||||||
|
|
||||||
const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => {
|
const onSubmit = async (data: TAddRecipientsForNewDocumentSchema) => {
|
||||||
try {
|
try {
|
||||||
|
let customDocumentDataId: string | undefined = undefined;
|
||||||
|
|
||||||
|
if (data.useCustomDocument && data.customDocumentData) {
|
||||||
|
const customDocumentData = await putPdfFile(data.customDocumentData);
|
||||||
|
customDocumentDataId = customDocumentData.id;
|
||||||
|
}
|
||||||
|
|
||||||
const { id } = await createDocumentFromTemplate({
|
const { id } = await createDocumentFromTemplate({
|
||||||
templateId,
|
templateId,
|
||||||
teamId: team?.id,
|
teamId: team?.id,
|
||||||
recipients: data.recipients,
|
recipients: data.recipients,
|
||||||
distributeDocument: data.distributeDocument,
|
distributeDocument: data.distributeDocument,
|
||||||
|
customDocumentDataId,
|
||||||
});
|
});
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
@ -300,89 +317,245 @@ export function UseTemplateDialog({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{recipients.length > 0 && (
|
||||||
|
<div className="mt-4 flex flex-row items-center">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="distributeDocument"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<Checkbox
|
||||||
|
id="distributeDocument"
|
||||||
|
className="h-5 w-5"
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{documentDistributionMethod === DocumentDistributionMethod.EMAIL && (
|
||||||
|
<label
|
||||||
|
className="text-muted-foreground ml-2 flex items-center text-sm"
|
||||||
|
htmlFor="distributeDocument"
|
||||||
|
>
|
||||||
|
<Trans>Send document</Trans>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger type="button">
|
||||||
|
<InfoIcon className="mx-1 h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
|
||||||
|
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
||||||
|
<p>
|
||||||
|
<Trans>
|
||||||
|
The document will be immediately sent to recipients if this
|
||||||
|
is checked.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<Trans>
|
||||||
|
Otherwise, the document will be created as a draft.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{documentDistributionMethod === DocumentDistributionMethod.NONE && (
|
||||||
|
<label
|
||||||
|
className="text-muted-foreground ml-2 flex items-center text-sm"
|
||||||
|
htmlFor="distributeDocument"
|
||||||
|
>
|
||||||
|
<Trans>Create as pending</Trans>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger type="button">
|
||||||
|
<InfoIcon className="mx-1 h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
||||||
|
<p>
|
||||||
|
<Trans>
|
||||||
|
Create the document as pending and ready to sign.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<Trans>We won't send anything to notify recipients.</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mt-2">
|
||||||
|
<Trans>
|
||||||
|
We will generate signing links for you, which you can send
|
||||||
|
to the recipients through your method of choice.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="useCustomDocument"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<div className="flex flex-row items-center">
|
||||||
|
<Checkbox
|
||||||
|
id="useCustomDocument"
|
||||||
|
className="h-5 w-5"
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
field.onChange(checked);
|
||||||
|
if (!checked) {
|
||||||
|
form.setValue('customDocumentData', undefined);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="text-muted-foreground ml-2 flex items-center text-sm"
|
||||||
|
htmlFor="useCustomDocument"
|
||||||
|
>
|
||||||
|
<Trans>Upload custom document</Trans>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger type="button">
|
||||||
|
<InfoIcon className="mx-1 h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
||||||
|
<p>
|
||||||
|
<Trans>
|
||||||
|
Upload a custom document to use instead of the template's default
|
||||||
|
document
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{form.watch('useCustomDocument') && (
|
||||||
|
<div className="my-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="customDocumentData"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<div className="w-full space-y-4">
|
||||||
|
<label
|
||||||
|
className={cn(
|
||||||
|
'text-muted-foreground hover:border-muted-foreground/50 group relative flex min-h-[150px] cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed border-gray-300 px-6 py-10 transition-colors',
|
||||||
|
{
|
||||||
|
'border-destructive hover:border-destructive':
|
||||||
|
form.formState.errors.customDocumentData,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
{!field.value && (
|
||||||
|
<>
|
||||||
|
<Upload className="text-muted-foreground/50 mx-auto h-10 w-10" />
|
||||||
|
<div className="mt-4 flex text-sm leading-6">
|
||||||
|
<span className="text-muted-foreground relative">
|
||||||
|
<Trans>
|
||||||
|
<span className="text-primary font-semibold">
|
||||||
|
Click to upload
|
||||||
|
</span>{' '}
|
||||||
|
or drag and drop
|
||||||
|
</Trans>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground/80 text-xs">
|
||||||
|
PDF files only
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{field.value && (
|
||||||
|
<div className="text-muted-foreground space-y-1">
|
||||||
|
<p className="text-sm font-medium">{field.value.name}</p>
|
||||||
|
<p className="text-muted-foreground/60 text-xs">
|
||||||
|
{(field.value.size / (1024 * 1024)).toFixed(2)} MB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
className="absolute h-full w-full opacity-0"
|
||||||
|
accept=".pdf,application/pdf"
|
||||||
|
onChange={(e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
field.onChange(undefined);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.type !== 'application/pdf') {
|
||||||
|
form.setError('customDocumentData', {
|
||||||
|
type: 'manual',
|
||||||
|
message: _(msg`Please select a PDF file`),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > APP_DOCUMENT_UPLOAD_SIZE_LIMIT * 1024 * 1024) {
|
||||||
|
form.setError('customDocumentData', {
|
||||||
|
type: 'manual',
|
||||||
|
message: _(
|
||||||
|
msg`File size exceeds the limit of ${APP_DOCUMENT_UPLOAD_SIZE_LIMIT} MB`,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
field.onChange(file);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{field.value && (
|
||||||
|
<div className="absolute right-2 top-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
field.onChange(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<div className="sr-only">
|
||||||
|
<Trans>Clear file</Trans>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{recipients.length > 0 && (
|
<DialogFooter className="mt-4">
|
||||||
<div className="mt-4 flex flex-row items-center">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="distributeDocument"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex flex-row items-center">
|
|
||||||
<Checkbox
|
|
||||||
id="distributeDocument"
|
|
||||||
className="h-5 w-5"
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={field.onChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{documentDistributionMethod === DocumentDistributionMethod.EMAIL && (
|
|
||||||
<label
|
|
||||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
|
||||||
htmlFor="distributeDocument"
|
|
||||||
>
|
|
||||||
<Trans>Send document</Trans>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger type="button">
|
|
||||||
<InfoIcon className="mx-1 h-4 w-4" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
|
|
||||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
|
||||||
<p>
|
|
||||||
<Trans>
|
|
||||||
The document will be immediately sent to recipients if this is
|
|
||||||
checked.
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<Trans>
|
|
||||||
Otherwise, the document will be created as a draft.
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{documentDistributionMethod === DocumentDistributionMethod.NONE && (
|
|
||||||
<label
|
|
||||||
className="text-muted-foreground ml-2 flex items-center text-sm"
|
|
||||||
htmlFor="distributeDocument"
|
|
||||||
>
|
|
||||||
<Trans>Create as pending</Trans>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger type="button">
|
|
||||||
<InfoIcon className="mx-1 h-4 w-4" />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent className="text-muted-foreground z-[99999] max-w-md space-y-2 p-4">
|
|
||||||
<p>
|
|
||||||
<Trans>Create the document as pending and ready to sign.</Trans>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<Trans>We won't send anything to notify recipients.</Trans>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p className="mt-2">
|
|
||||||
<Trans>
|
|
||||||
We will generate signing links for you, which you can send to
|
|
||||||
the recipients through your method of choice.
|
|
||||||
</Trans>
|
|
||||||
</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button type="button" variant="secondary">
|
<Button type="button" variant="secondary">
|
||||||
<Trans>Close</Trans>
|
<Trans>Close</Trans>
|
||||||
|
|||||||
6
package-lock.json
generated
6
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.9.0-rc.5",
|
"version": "1.9.0-rc.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"version": "1.9.0-rc.5",
|
"version": "1.9.0-rc.6",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
"packages/*"
|
"packages/*"
|
||||||
@ -133,7 +133,7 @@
|
|||||||
},
|
},
|
||||||
"apps/web": {
|
"apps/web": {
|
||||||
"name": "@documenso/web",
|
"name": "@documenso/web",
|
||||||
"version": "1.9.0-rc.5",
|
"version": "1.9.0-rc.6",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/api": "*",
|
"@documenso/api": "*",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.9.0-rc.5",
|
"version": "1.9.0-rc.6",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"build:web": "turbo run build --filter=@documenso/web",
|
"build:web": "turbo run build --filter=@documenso/web",
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
import fs from 'fs';
|
||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
import { DocumentDataType } from '@documenso/prisma/client';
|
||||||
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
|
import { seedUserSubscription } from '@documenso/prisma/seed/subscriptions';
|
||||||
import { seedTeam } from '@documenso/prisma/seed/teams';
|
import { seedTeam } from '@documenso/prisma/seed/teams';
|
||||||
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
|
||||||
@ -13,6 +17,20 @@ test.describe.configure({ mode: 'parallel' });
|
|||||||
|
|
||||||
const enterprisePriceId = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || '';
|
const enterprisePriceId = process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PLAN_MONTHLY_PRICE_ID || '';
|
||||||
|
|
||||||
|
// Create a temporary PDF file for testing
|
||||||
|
function createTempPdfFile() {
|
||||||
|
const tempDir = os.tmpdir();
|
||||||
|
const tempFilePath = path.join(tempDir, 'test.pdf');
|
||||||
|
|
||||||
|
// Create a simple PDF file with some content
|
||||||
|
const pdfContent = Buffer.from(
|
||||||
|
'%PDF-1.4\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 obj<</Type/Page/MediaBox[0 0 612 792]/Parent 2 0 R>>endobj\nxref\n0 4\n0000000000 65535 f\n0000000009 00000 n\n0000000052 00000 n\n0000000101 00000 n\ntrailer<</Size 4/Root 1 0 R>>\nstartxref\n178\n%%EOF',
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(tempFilePath, pdfContent);
|
||||||
|
return tempFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. Create a template with all settings filled out
|
* 1. Create a template with all settings filled out
|
||||||
* 2. Create a document from the template
|
* 2. Create a document from the template
|
||||||
@ -283,3 +301,231 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
|
|||||||
expect(recipientOneAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
expect(recipientOneAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
||||||
expect(recipientTwoAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
expect(recipientTwoAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This test verifies that we can create a document from a template using a custom document
|
||||||
|
* instead of the template's default document.
|
||||||
|
*/
|
||||||
|
test('[TEMPLATE]: should create a document from a template with custom document', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const user = await seedUser();
|
||||||
|
const template = await seedBlankTemplate(user);
|
||||||
|
|
||||||
|
// Create a temporary PDF file for upload
|
||||||
|
const testPdfPath = createTempPdfFile();
|
||||||
|
const pdfContent = fs.readFileSync(testPdfPath).toString('base64');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/templates/${template.id}/edit`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set template title
|
||||||
|
await page.getByLabel('Title').fill('TEMPLATE_WITH_CUSTOM_DOC');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
||||||
|
|
||||||
|
// Add a signer
|
||||||
|
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
|
||||||
|
await page.getByPlaceholder('Name').fill('Recipient');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Save template' }).click();
|
||||||
|
|
||||||
|
// Use template with custom document
|
||||||
|
await page.waitForURL('/templates');
|
||||||
|
await page.getByRole('button', { name: 'Use Template' }).click();
|
||||||
|
|
||||||
|
// Enable custom document upload and upload file
|
||||||
|
await page.getByLabel('Upload custom document').check();
|
||||||
|
await page.locator('input[type="file"]').setInputFiles(testPdfPath);
|
||||||
|
|
||||||
|
// Wait for upload to complete
|
||||||
|
await expect(page.getByText(path.basename(testPdfPath))).toBeVisible();
|
||||||
|
|
||||||
|
// Create document with custom document data
|
||||||
|
await page.getByRole('button', { name: 'Create as draft' }).click();
|
||||||
|
|
||||||
|
// Review that the document was created with the custom document data
|
||||||
|
await page.waitForURL(/documents/);
|
||||||
|
|
||||||
|
const documentId = Number(page.url().split('/').pop());
|
||||||
|
|
||||||
|
const document = await prisma.document.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
documentData: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.title).toEqual('TEMPLATE_WITH_CUSTOM_DOC');
|
||||||
|
expect(document.documentData.type).toEqual(DocumentDataType.BYTES_64);
|
||||||
|
expect(document.documentData.data).toEqual(pdfContent);
|
||||||
|
expect(document.documentData.initialData).toEqual(pdfContent);
|
||||||
|
} finally {
|
||||||
|
// Clean up the temporary file
|
||||||
|
fs.unlinkSync(testPdfPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This test verifies that we can create a team document from a template using a custom document
|
||||||
|
* instead of the template's default document.
|
||||||
|
*/
|
||||||
|
test('[TEMPLATE]: should create a team document from a template with custom document', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const { owner, ...team } = await seedTeam({
|
||||||
|
createTeamMembers: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const template = await seedBlankTemplate(owner, {
|
||||||
|
createTemplateOptions: {
|
||||||
|
teamId: team.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a temporary PDF file for upload
|
||||||
|
const testPdfPath = createTempPdfFile();
|
||||||
|
const pdfContent = fs.readFileSync(testPdfPath).toString('base64');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: owner.email,
|
||||||
|
redirectPath: `/t/${team.url}/templates/${template.id}/edit`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set template title
|
||||||
|
await page.getByLabel('Title').fill('TEAM_TEMPLATE_WITH_CUSTOM_DOC');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
||||||
|
|
||||||
|
// Add a signer
|
||||||
|
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
|
||||||
|
await page.getByPlaceholder('Name').fill('Recipient');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Save template' }).click();
|
||||||
|
|
||||||
|
// Use template with custom document
|
||||||
|
await page.waitForURL(`/t/${team.url}/templates`);
|
||||||
|
await page.getByRole('button', { name: 'Use Template' }).click();
|
||||||
|
|
||||||
|
// Enable custom document upload and upload file
|
||||||
|
await page.getByLabel('Upload custom document').check();
|
||||||
|
await page.locator('input[type="file"]').setInputFiles(testPdfPath);
|
||||||
|
|
||||||
|
// Wait for upload to complete
|
||||||
|
await expect(page.getByText(path.basename(testPdfPath))).toBeVisible();
|
||||||
|
|
||||||
|
// Create document with custom document data
|
||||||
|
await page.getByRole('button', { name: 'Create as draft' }).click();
|
||||||
|
|
||||||
|
// Review that the document was created with the custom document data
|
||||||
|
await page.waitForURL(/documents/);
|
||||||
|
|
||||||
|
const documentId = Number(page.url().split('/').pop());
|
||||||
|
|
||||||
|
const document = await prisma.document.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
documentData: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.teamId).toEqual(team.id);
|
||||||
|
expect(document.title).toEqual('TEAM_TEMPLATE_WITH_CUSTOM_DOC');
|
||||||
|
expect(document.documentData.type).toEqual(DocumentDataType.BYTES_64);
|
||||||
|
expect(document.documentData.data).toEqual(pdfContent);
|
||||||
|
expect(document.documentData.initialData).toEqual(pdfContent);
|
||||||
|
} finally {
|
||||||
|
// Clean up the temporary file
|
||||||
|
fs.unlinkSync(testPdfPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This test verifies that when custom document upload is not enabled,
|
||||||
|
* the document uses the template's original document data.
|
||||||
|
*/
|
||||||
|
test('[TEMPLATE]: should create a document from a template using template document when custom document is not enabled', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const user = await seedUser();
|
||||||
|
const template = await seedBlankTemplate(user);
|
||||||
|
|
||||||
|
await apiSignin({
|
||||||
|
page,
|
||||||
|
email: user.email,
|
||||||
|
redirectPath: `/templates/${template.id}/edit`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set template title
|
||||||
|
await page.getByLabel('Title').fill('TEMPLATE_WITH_ORIGINAL_DOC');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Placeholder' })).toBeVisible();
|
||||||
|
|
||||||
|
// Add a signer
|
||||||
|
await page.getByPlaceholder('Email').fill('recipient@documenso.com');
|
||||||
|
await page.getByPlaceholder('Name').fill('Recipient');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
await expect(page.getByRole('heading', { name: 'Add Fields' })).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Save template' }).click();
|
||||||
|
|
||||||
|
// Use template without custom document
|
||||||
|
await page.waitForURL('/templates');
|
||||||
|
await page.getByRole('button', { name: 'Use Template' }).click();
|
||||||
|
|
||||||
|
// Verify custom document upload is not checked by default
|
||||||
|
await expect(page.getByLabel('Upload custom document')).not.toBeChecked();
|
||||||
|
|
||||||
|
// Create document without custom document data
|
||||||
|
await page.getByRole('button', { name: 'Create as draft' }).click();
|
||||||
|
|
||||||
|
// Review that the document was created with the template's document data
|
||||||
|
await page.waitForURL(/documents/);
|
||||||
|
|
||||||
|
const documentId = Number(page.url().split('/').pop());
|
||||||
|
|
||||||
|
const document = await prisma.document.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
documentData: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const templateWithData = await prisma.template.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: template.id,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
templateDocumentData: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(document.title).toEqual('TEMPLATE_WITH_ORIGINAL_DOC');
|
||||||
|
expect(document.documentData.data).toEqual(templateWithData.templateDocumentData.data);
|
||||||
|
expect(document.documentData.initialData).toEqual(
|
||||||
|
templateWithData.templateDocumentData.initialData,
|
||||||
|
);
|
||||||
|
expect(document.documentData.type).toEqual(templateWithData.templateDocumentData.type);
|
||||||
|
});
|
||||||
|
|||||||
@ -54,6 +54,7 @@ export type CreateDocumentFromTemplateOptions = {
|
|||||||
email: string;
|
email: string;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
}[];
|
}[];
|
||||||
|
customDocumentDataId?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Values that will override the predefined values in the template.
|
* Values that will override the predefined values in the template.
|
||||||
@ -90,6 +91,7 @@ export const createDocumentFromTemplate = async ({
|
|||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
recipients,
|
recipients,
|
||||||
|
customDocumentDataId,
|
||||||
override,
|
override,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
}: CreateDocumentFromTemplateOptions): Promise<TCreateDocumentFromTemplateResponse> => {
|
}: CreateDocumentFromTemplateOptions): Promise<TCreateDocumentFromTemplateResponse> => {
|
||||||
@ -171,11 +173,29 @@ export const createDocumentFromTemplate = async ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let parentDocumentData = template.templateDocumentData;
|
||||||
|
|
||||||
|
if (customDocumentDataId) {
|
||||||
|
const customDocumentData = await prisma.documentData.findFirst({
|
||||||
|
where: {
|
||||||
|
id: customDocumentDataId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!customDocumentData) {
|
||||||
|
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||||
|
message: 'Custom document data not found',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
parentDocumentData = customDocumentData;
|
||||||
|
}
|
||||||
|
|
||||||
const documentData = await prisma.documentData.create({
|
const documentData = await prisma.documentData.create({
|
||||||
data: {
|
data: {
|
||||||
type: template.templateDocumentData.type,
|
type: parentDocumentData.type,
|
||||||
data: template.templateDocumentData.data,
|
data: parentDocumentData.data,
|
||||||
initialData: template.templateDocumentData.initialData,
|
initialData: parentDocumentData.initialData,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -237,8 +237,8 @@ export const templateRouter = router({
|
|||||||
})
|
})
|
||||||
.input(ZCreateDocumentFromTemplateMutationSchema)
|
.input(ZCreateDocumentFromTemplateMutationSchema)
|
||||||
.output(ZGetDocumentWithDetailsByIdResponseSchema)
|
.output(ZGetDocumentWithDetailsByIdResponseSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const { templateId, teamId, recipients, distributeDocument } = input;
|
const { templateId, teamId, recipients, distributeDocument, customDocumentDataId } = input;
|
||||||
|
|
||||||
const limits = await getServerLimits({ email: ctx.user.email, teamId });
|
const limits = await getServerLimits({ email: ctx.user.email, teamId });
|
||||||
|
|
||||||
@ -253,6 +253,7 @@ export const templateRouter = router({
|
|||||||
teamId,
|
teamId,
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
recipients,
|
recipients,
|
||||||
|
customDocumentDataId,
|
||||||
requestMetadata,
|
requestMetadata,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -47,6 +47,7 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
|
|||||||
return new Set(emails).size === emails.length;
|
return new Set(emails).size === emails.length;
|
||||||
}, 'Recipients must have unique emails'),
|
}, 'Recipients must have unique emails'),
|
||||||
distributeDocument: z.boolean().optional(),
|
distributeDocument: z.boolean().optional(),
|
||||||
|
customDocumentDataId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZDuplicateTemplateMutationSchema = z.object({
|
export const ZDuplicateTemplateMutationSchema = z.object({
|
||||||
|
|||||||
Reference in New Issue
Block a user