mirror of
https://github.com/documenso/documenso.git
synced 2026-10-05 11:14:00 +10:00
fix: remove contents from public envelope create route
This commit is contained in:
@@ -190,9 +190,10 @@ Controls envelope configuration options. Set to `null` to hide envelope settings
|
||||
|
||||
Controls available actions during editing:
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
| ------------------ | --------- | ------- | ------------------------ |
|
||||
| `allowAttachments` | `boolean` | `true` | Allow adding attachments |
|
||||
| Property | Type | Default | Description |
|
||||
| ------------------ | --------- | ------- | -------------------------------------------------------------------------------------------- |
|
||||
| `allowContents` | `boolean` | `true` | Allow adding contents (text, lines, shapes, highlights and images) alongside fields in the add fields step |
|
||||
| `allowAttachments` | `boolean` | `true` | Allow adding attachments |
|
||||
|
||||
### Envelope Items
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CONTENT_TEXT_MAX_LENGTH } from '@documenso/lib/constants/envelope-content';
|
||||
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from '@documenso/ui/primitives/form/form';
|
||||
import { Textarea } from '@documenso/ui/primitives/textarea';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
@@ -47,6 +48,7 @@ export const EditorContentTextForm = () => {
|
||||
className="h-auto"
|
||||
placeholder={t`Add text to the document`}
|
||||
rows={3}
|
||||
maxLength={CONTENT_TEXT_MAX_LENGTH}
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -270,7 +270,7 @@ export const EnvelopeEditorFieldsPage = () => {
|
||||
|
||||
const { allowContents } = editorConfig.actions;
|
||||
|
||||
if (!editorConfig.general?.allowAddFieldsStep) {
|
||||
if (!editorConfig.general.allowAddFieldsStep) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from '@documenso/lib/utils/embed-config';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TCreateEnvelopePayload } from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
||||
import type { TCreateEmbeddingEnvelopePayload } from '@documenso/trpc/server/embedding-router/create-embedding-envelope.types';
|
||||
import { Spinner } from '@documenso/ui/primitives/spinner';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
@@ -176,7 +176,7 @@ const EnvelopeCreatePage = ({ embedAuthoringOptions }: EnvelopeCreatePageProps)
|
||||
|
||||
const buildCreateEnvelopeRequest = (
|
||||
envelope: Omit<TEditorEnvelope, 'id'>,
|
||||
): { payload: TCreateEnvelopePayload; files: File[]; contentImages: File[] } => {
|
||||
): { payload: TCreateEmbeddingEnvelopePayload; files: File[]; contentImages: File[] } => {
|
||||
const sortedItems = [...envelope.envelopeItems].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
||||
|
||||
const itemIdToIndex = new Map<string, number>();
|
||||
@@ -248,7 +248,7 @@ const EnvelopeCreatePage = ({ embedAuthoringOptions }: EnvelopeCreatePageProps)
|
||||
|
||||
const contentImages = imagesToUpload.map((image) => image.file);
|
||||
|
||||
const payload: TCreateEnvelopePayload = {
|
||||
const payload: TCreateEmbeddingEnvelopePayload = {
|
||||
title: envelope.title,
|
||||
type: envelope.type,
|
||||
externalId: envelope.externalId ?? undefined,
|
||||
|
||||
+111
-44
@@ -10,13 +10,12 @@ import {
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
import type {
|
||||
TCreateEnvelopePayload,
|
||||
TCreateEnvelopeResponse,
|
||||
} from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
||||
import type { ZCreateEmbeddingEnvelopePayloadSchema } from '@documenso/trpc/server/embedding-router/create-embedding-envelope.types';
|
||||
import type { TCreateEnvelopeResponse } from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
||||
import type { TGetEnvelopeResponse } from '@documenso/trpc/server/envelope-router/get-envelope.types';
|
||||
import { type APIRequestContext, expect, test } from '@playwright/test';
|
||||
import { type APIRequestContext, type APIResponse, expect, test } from '@playwright/test';
|
||||
import { EnvelopeType, FieldType, RecipientRole, type Team } from '@prisma/client';
|
||||
import type { z } from 'zod';
|
||||
|
||||
import { createGifLabelledAsPng, createImageFile, type TestImageFile } from '../../fixtures/contents';
|
||||
|
||||
@@ -35,21 +34,32 @@ type TCreateContentInput = {
|
||||
imageIndex?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* The payload as sent, i.e. before any defaults are applied.
|
||||
*/
|
||||
type TCreateEmbeddingEnvelopePayloadInput = z.input<typeof ZCreateEmbeddingEnvelopePayloadSchema>;
|
||||
|
||||
type CreateEnvelopeWithContentsOptions = {
|
||||
request: APIRequestContext;
|
||||
token: string;
|
||||
payload?: Partial<Omit<TCreateEnvelopePayload, 'contents'>>;
|
||||
|
||||
/**
|
||||
* A presign token, since only the embedded editor may create an envelope
|
||||
* with contents.
|
||||
*/
|
||||
presignToken: string;
|
||||
payload?: Partial<Omit<TCreateEmbeddingEnvelopePayloadInput, 'contents'>>;
|
||||
contents: TCreateContentInput[];
|
||||
pdfNames?: string[];
|
||||
images?: TestImageFile[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an envelope with contents through the public create route.
|
||||
* Create an envelope with contents through the embedded create route, as the
|
||||
* embedded editor does.
|
||||
*/
|
||||
const createEnvelopeWithContents = async ({
|
||||
request,
|
||||
token,
|
||||
presignToken,
|
||||
payload = {},
|
||||
contents,
|
||||
pdfNames = ['example.pdf'],
|
||||
@@ -75,12 +85,38 @@ const createEnvelopeWithContents = async ({
|
||||
formData.append('contentImages', new File([image.buffer], image.name, { type: image.mimeType }));
|
||||
}
|
||||
|
||||
return await request.post(`${baseUrl}/envelope/create`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
return await request.post(`${WEBAPP_BASE_URL}/api/trpc/embeddingPresign.createEmbeddingEnvelope`, {
|
||||
headers: { authorization: `Bearer ${presignToken}` },
|
||||
multipart: formData,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* The ID of the envelope a successful embedded create returned. tRPC wraps
|
||||
* the result, unlike the OpenAPI routes.
|
||||
*/
|
||||
const getCreatedEnvelopeId = async (res: APIResponse) => {
|
||||
const body: { result: { data: { json: TCreateEnvelopeResponse } } } = await res.json();
|
||||
|
||||
return body.result.data.json.id;
|
||||
};
|
||||
|
||||
/**
|
||||
* A presign token for the team, as the embedded editor is given.
|
||||
*/
|
||||
const createPresignToken = async (request: APIRequestContext, apiToken: string) => {
|
||||
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2/embedding/create-presign-token`, {
|
||||
headers: { Authorization: `Bearer ${apiToken}`, 'Content-Type': 'application/json' },
|
||||
data: {},
|
||||
});
|
||||
|
||||
expect(res.ok(), await res.text()).toBeTruthy();
|
||||
|
||||
const { token }: { token: string } = await res.json();
|
||||
|
||||
return token;
|
||||
};
|
||||
|
||||
const getEnvelope = async (request: APIRequestContext, token: string, envelopeId: string) => {
|
||||
const res = await request.get(`${baseUrl}/envelope/${envelopeId}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
@@ -154,14 +190,13 @@ const imageMeta = (positionY: number): TEnvelopeContentMetaInput => ({
|
||||
|
||||
test.describe('Create envelope with contents', () => {
|
||||
let team: Team;
|
||||
let userId: number;
|
||||
let token: string;
|
||||
let presignToken: string;
|
||||
|
||||
test.beforeEach(async () => {
|
||||
test.beforeEach(async ({ request }) => {
|
||||
const seeded = await seedUser();
|
||||
|
||||
team = seeded.team;
|
||||
userId = seeded.user.id;
|
||||
|
||||
({ token } = await createApiToken({
|
||||
userId: seeded.user.id,
|
||||
@@ -169,6 +204,8 @@ test.describe('Create envelope with contents', () => {
|
||||
tokenName: 'test-envelope-create-contents',
|
||||
expiresIn: null,
|
||||
}));
|
||||
|
||||
presignToken = await createPresignToken(request, token);
|
||||
});
|
||||
|
||||
for (const envelopeType of [EnvelopeType.DOCUMENT, EnvelopeType.TEMPLATE]) {
|
||||
@@ -177,7 +214,7 @@ test.describe('Create envelope with contents', () => {
|
||||
|
||||
const res = await createEnvelopeWithContents({
|
||||
request,
|
||||
token,
|
||||
presignToken,
|
||||
payload: { type: envelopeType },
|
||||
contents: [
|
||||
{ contentMeta: textMeta('Hello') },
|
||||
@@ -193,7 +230,7 @@ test.describe('Create envelope with contents', () => {
|
||||
|
||||
expect(res.ok(), await res.text()).toBeTruthy();
|
||||
|
||||
const { id } = (await res.json()) as TCreateEnvelopeResponse;
|
||||
const id = await getCreatedEnvelopeId(res);
|
||||
|
||||
// Contents are returned by the public get route.
|
||||
const envelope = await getEnvelope(request, token, id);
|
||||
@@ -234,7 +271,7 @@ test.describe('Create envelope with contents', () => {
|
||||
test('places each content on the file it identifies', async ({ request }) => {
|
||||
const res = await createEnvelopeWithContents({
|
||||
request,
|
||||
token,
|
||||
presignToken,
|
||||
pdfNames: ['first.pdf', 'second.pdf'],
|
||||
contents: [
|
||||
{ contentMeta: textMeta('Default') },
|
||||
@@ -245,7 +282,7 @@ test.describe('Create envelope with contents', () => {
|
||||
|
||||
expect(res.ok(), await res.text()).toBeTruthy();
|
||||
|
||||
const { id } = (await res.json()) as TCreateEnvelopeResponse;
|
||||
const id = await getCreatedEnvelopeId(res);
|
||||
|
||||
const envelope = await getEnvelope(request, token, id);
|
||||
const [first, second] = [...envelope.envelopeItems].sort((a, b) => a.order - b.order);
|
||||
@@ -276,7 +313,7 @@ test.describe('Create envelope with contents', () => {
|
||||
|
||||
const res = await createEnvelopeWithContents({
|
||||
request,
|
||||
token,
|
||||
presignToken,
|
||||
payload: { externalId },
|
||||
contents: [
|
||||
{ contentMeta: imageMeta(40), imageIndex: 0 },
|
||||
@@ -302,7 +339,7 @@ test.describe('Create envelope with contents', () => {
|
||||
// The example PDF has a single page.
|
||||
const res = await createEnvelopeWithContents({
|
||||
request,
|
||||
token,
|
||||
presignToken,
|
||||
payload: { externalId },
|
||||
contents: [{ contentMeta: { ...imageMeta(40), page: 2 }, imageIndex: 0 }],
|
||||
images: [image],
|
||||
@@ -322,7 +359,7 @@ test.describe('Create envelope with contents', () => {
|
||||
|
||||
const res = await createEnvelopeWithContents({
|
||||
request,
|
||||
token,
|
||||
presignToken,
|
||||
payload: { externalId },
|
||||
contents: [{ contentMeta: imageMeta(40), imageIndex: 1 }],
|
||||
images: [await createImageFile(`index-${nanoid()}.png`, 50, 50)],
|
||||
@@ -340,7 +377,7 @@ test.describe('Create envelope with contents', () => {
|
||||
|
||||
const res = await createEnvelopeWithContents({
|
||||
request,
|
||||
token,
|
||||
presignToken,
|
||||
payload: { externalId },
|
||||
contents: [{ contentMeta: textMeta('Not an image'), imageIndex: 0 }],
|
||||
images: [image],
|
||||
@@ -364,7 +401,7 @@ test.describe('Create envelope with contents', () => {
|
||||
// Only "example.pdf" is uploaded, at index 0.
|
||||
const res = await createEnvelopeWithContents({
|
||||
request,
|
||||
token,
|
||||
presignToken,
|
||||
payload: { externalId },
|
||||
contents: [{ identifier, contentMeta: imageMeta(40), imageIndex: 0 }],
|
||||
images: [image],
|
||||
@@ -391,7 +428,7 @@ test.describe('Create envelope with contents', () => {
|
||||
|
||||
const res = await createEnvelopeWithContents({
|
||||
request,
|
||||
token,
|
||||
presignToken,
|
||||
payload: { externalId },
|
||||
contents: [{ identifier, contentMeta: textMeta('Bad index') }],
|
||||
});
|
||||
@@ -405,7 +442,7 @@ test.describe('Create envelope with contents', () => {
|
||||
|
||||
const res = await createEnvelopeWithContents({
|
||||
request,
|
||||
token,
|
||||
presignToken,
|
||||
payload: {
|
||||
externalId,
|
||||
recipients: [
|
||||
@@ -441,7 +478,7 @@ test.describe('Create envelope with contents', () => {
|
||||
|
||||
const res = await createEnvelopeWithContents({
|
||||
request,
|
||||
token,
|
||||
presignToken,
|
||||
payload: {
|
||||
recipients: [
|
||||
{
|
||||
@@ -460,7 +497,7 @@ test.describe('Create envelope with contents', () => {
|
||||
|
||||
expect(res.ok(), await res.text()).toBeTruthy();
|
||||
|
||||
const { id } = (await res.json()) as TCreateEnvelopeResponse;
|
||||
const id = await getCreatedEnvelopeId(res);
|
||||
|
||||
const envelope = await prisma.envelope.findUniqueOrThrow({
|
||||
where: { id },
|
||||
@@ -474,9 +511,13 @@ test.describe('Create envelope with contents', () => {
|
||||
const fieldLogs = auditLogs.filter((log) => log.type === 'FIELD_CREATED');
|
||||
const contentLogs = auditLogs.filter((log) => log.type === 'CONTENT_CREATED');
|
||||
|
||||
// Attributed to the user who made the request.
|
||||
// Attributed from the request metadata like the editor's own logs. The
|
||||
// embedded routes carry no audit user, so these are anonymous, the same
|
||||
// as contents saved through the embedded update route.
|
||||
for (const log of auditLogs) {
|
||||
expect(log.userId).toBe(userId);
|
||||
expect(log.userId).toBeNull();
|
||||
expect(log.email).toBeNull();
|
||||
expect(log.name).toBeNull();
|
||||
}
|
||||
|
||||
// One entry per field, carrying the same identifiers the editor's logs do.
|
||||
@@ -522,7 +563,7 @@ test.describe('Create envelope with contents', () => {
|
||||
test('does not log fields or contents on a template', async ({ request }) => {
|
||||
const res = await createEnvelopeWithContents({
|
||||
request,
|
||||
token,
|
||||
presignToken,
|
||||
payload: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
recipients: [
|
||||
@@ -539,7 +580,7 @@ test.describe('Create envelope with contents', () => {
|
||||
|
||||
expect(res.ok(), await res.text()).toBeTruthy();
|
||||
|
||||
const { id } = (await res.json()) as TCreateEnvelopeResponse;
|
||||
const id = await getCreatedEnvelopeId(res);
|
||||
|
||||
expect(await prisma.documentAuditLog.count({ where: { envelopeId: id } })).toBe(0);
|
||||
});
|
||||
@@ -550,7 +591,7 @@ test.describe('Create envelope with contents', () => {
|
||||
|
||||
const res = await createEnvelopeWithContents({
|
||||
request,
|
||||
token,
|
||||
presignToken,
|
||||
payload: { externalId },
|
||||
contents: [{ contentMeta: imageMeta(40), imageIndex: 0 }],
|
||||
images: [image],
|
||||
@@ -563,7 +604,41 @@ test.describe('Create envelope with contents', () => {
|
||||
expect(await countDataContentsNamed(image.name)).toBe(0);
|
||||
});
|
||||
|
||||
test('documents contents on the public create route', async ({ request }) => {
|
||||
test('the public create route ignores contents', async ({ request }) => {
|
||||
const externalId = `e2e-public-contents-${nanoid()}`;
|
||||
const image = await createImageFile(`public-${nanoid()}.png`, 50, 50);
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
// Sent in the embedded shape, which the public route does not accept.
|
||||
formData.append(
|
||||
'payload',
|
||||
JSON.stringify({
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
title: 'Public Envelope',
|
||||
externalId,
|
||||
contents: [{ contentMeta: textMeta('Ignored') }, { contentMeta: imageMeta(40), imageIndex: 0 }],
|
||||
}),
|
||||
);
|
||||
|
||||
formData.append('files', new File([examplePdfBuffer], 'example.pdf', { type: 'application/pdf' }));
|
||||
formData.append('contentImages', new File([image.buffer], image.name, { type: image.mimeType }));
|
||||
|
||||
const res = await request.post(`${baseUrl}/envelope/create`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
multipart: formData,
|
||||
});
|
||||
|
||||
expect(res.ok(), await res.text()).toBeTruthy();
|
||||
|
||||
const { id } = (await res.json()) as TCreateEnvelopeResponse;
|
||||
|
||||
// The envelope is created without them, and the image is never stored.
|
||||
expect(await prisma.envelopeContent.count({ where: { envelopeId: id } })).toBe(0);
|
||||
expect(await countDataContentsNamed(image.name)).toBe(0);
|
||||
});
|
||||
|
||||
test('does not document contents on the public create route', async ({ request }) => {
|
||||
const res = await request.get(`${WEBAPP_BASE_URL}/api/v2/openapi.json`);
|
||||
|
||||
expect(res.ok()).toBeTruthy();
|
||||
@@ -581,16 +656,8 @@ test.describe('Create envelope with contents', () => {
|
||||
'properties',
|
||||
];
|
||||
|
||||
expect(openApiDocument).toHaveProperty([...requestSchemaPath, 'contentImages']);
|
||||
|
||||
expect(openApiDocument).toHaveProperty([
|
||||
...requestSchemaPath,
|
||||
'payload',
|
||||
'properties',
|
||||
'contents',
|
||||
'items',
|
||||
'properties',
|
||||
'imageIndex',
|
||||
]);
|
||||
expect(openApiDocument).toHaveProperty([...requestSchemaPath, 'files']);
|
||||
expect(openApiDocument).not.toHaveProperty([...requestSchemaPath, 'contentImages']);
|
||||
expect(openApiDocument).not.toHaveProperty([...requestSchemaPath, 'payload', 'properties', 'contents']);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
import { CONTENT_TEXT_MAX_LENGTH } from '@documenso/lib/constants/envelope-content';
|
||||
import {
|
||||
CONTENT_MAX_Z_INDEX,
|
||||
EnvelopeContentType,
|
||||
@@ -26,7 +27,7 @@ test.describe.configure({
|
||||
* its organisation's allowance.
|
||||
*/
|
||||
|
||||
const textMeta = (index: number, overrides: { zIndex?: number } = {}): TEnvelopeContentMetaInput => ({
|
||||
const textMeta = (index: number, overrides: { zIndex?: number; text?: string } = {}): TEnvelopeContentMetaInput => ({
|
||||
type: EnvelopeContentType.TEXT,
|
||||
page: 1,
|
||||
positionX: 10,
|
||||
@@ -216,3 +217,21 @@ test('rejects a stacking order above the authored ceiling', async ({ page }) =>
|
||||
expect(res.ok()).toBeFalsy();
|
||||
expect(await getContentCount(envelope.id)).toBe(0);
|
||||
});
|
||||
|
||||
test('rejects a text longer than the cap', async ({ page }) => {
|
||||
const { envelope, envelopeItemId, teamId } = await setupEnvelope(page, {});
|
||||
|
||||
const atCap = await setContents(page, teamId, envelope.id, [
|
||||
{ envelopeItemId, contentMeta: textMeta(0, { text: 'a'.repeat(CONTENT_TEXT_MAX_LENGTH) }) },
|
||||
]);
|
||||
|
||||
expect(atCap.ok()).toBeTruthy();
|
||||
expect(await getContentCount(envelope.id)).toBe(1);
|
||||
|
||||
const overCap = await setContents(page, teamId, envelope.id, [
|
||||
{ envelopeItemId, contentMeta: textMeta(0, { text: 'a'.repeat(CONTENT_TEXT_MAX_LENGTH + 1) }) },
|
||||
]);
|
||||
|
||||
expect(overCap.ok()).toBeFalsy();
|
||||
expect(await getContentCount(envelope.id)).toBe(1);
|
||||
});
|
||||
|
||||
@@ -53,8 +53,12 @@ const IMAGE_META: TEnvelopeContentMetaInput = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an envelope with two files through the public create route, with a
|
||||
* text on each file and an image on the second.
|
||||
* Create an envelope with two files, with a text on each file and an image on
|
||||
* the second.
|
||||
*
|
||||
* Goes through the embedded create route, since only the embedded editor may
|
||||
* create an envelope with contents, using a presign token minted from the
|
||||
* API token.
|
||||
*/
|
||||
const createSourceEnvelope = async (
|
||||
request: APIRequestContext,
|
||||
@@ -62,6 +66,15 @@ const createSourceEnvelope = async (
|
||||
type: EnvelopeType,
|
||||
options: { withSignerFields?: boolean } = {},
|
||||
) => {
|
||||
const presignRes = await request.post(`${WEBAPP_BASE_URL}/api/v2/embedding/create-presign-token`, {
|
||||
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
||||
data: {},
|
||||
});
|
||||
|
||||
expect(presignRes.ok(), await presignRes.text()).toBeTruthy();
|
||||
|
||||
const { token: presignToken }: { token: string } = await presignRes.json();
|
||||
|
||||
const image = await createImageFile(`copy-${nanoid()}.png`, 120, 60);
|
||||
|
||||
const formData = new FormData();
|
||||
@@ -98,16 +111,17 @@ const createSourceEnvelope = async (
|
||||
|
||||
formData.append('contentImages', new File([image.buffer], image.name, { type: image.mimeType }));
|
||||
|
||||
const res = await request.post(`${baseUrl}/envelope/create`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
const res = await request.post(`${WEBAPP_BASE_URL}/api/trpc/embeddingPresign.createEmbeddingEnvelope`, {
|
||||
headers: { authorization: `Bearer ${presignToken}` },
|
||||
multipart: formData,
|
||||
});
|
||||
|
||||
expect(res.ok(), await res.text()).toBeTruthy();
|
||||
|
||||
const { id } = (await res.json()) as TCreateEnvelopeResponse;
|
||||
// tRPC wraps the result, unlike the OpenAPI routes.
|
||||
const body: { result: { data: { json: TCreateEnvelopeResponse } } } = await res.json();
|
||||
|
||||
return id;
|
||||
return body.result.data.json.id;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -226,10 +240,13 @@ const getCopyRoute = (name: string): CopyRoute => {
|
||||
|
||||
test.describe('Copy contents to new envelopes', () => {
|
||||
let token: string;
|
||||
let teamName: string;
|
||||
|
||||
test.beforeEach(async () => {
|
||||
const { user, team } = await seedUser();
|
||||
|
||||
teamName = team.name;
|
||||
|
||||
({ token } = await createApiToken({
|
||||
userId: user.id,
|
||||
teamId: team.id,
|
||||
@@ -284,6 +301,12 @@ test.describe('Copy contents to new envelopes', () => {
|
||||
expect(logsOfType('DOCUMENT_CREATED')).toHaveLength(1);
|
||||
expect(logsOfType('DOCUMENT_CREATED')[0].data).toMatchObject({ title: copy.title });
|
||||
|
||||
// Attributed from the request metadata, which for a team API token is the team.
|
||||
for (const log of auditLogs) {
|
||||
expect(log.userId).toBeNull();
|
||||
expect(log.name).toBe(teamName);
|
||||
}
|
||||
|
||||
// One entry per copied field, naming the copy's own field and recipient IDs.
|
||||
const [signer] = copy.recipients;
|
||||
|
||||
|
||||
@@ -29,6 +29,15 @@ export const CONTENT_IMAGE_MAX_EDGE = 2048;
|
||||
*/
|
||||
export const CONTENT_IMAGE_MAX_INPUT_PIXELS = 25_000_000;
|
||||
|
||||
/**
|
||||
* The longest text a text content may hold.
|
||||
*
|
||||
* Text is the only free-form value on a content, and it is recorded in full
|
||||
* in the audit log on every save, so it is capped the same as a document
|
||||
* message.
|
||||
*/
|
||||
export const CONTENT_TEXT_MAX_LENGTH = 5_000;
|
||||
|
||||
/**
|
||||
* The number of contents allowed on a single envelope.
|
||||
*
|
||||
|
||||
@@ -726,9 +726,6 @@ export const createEnvelope = async ({
|
||||
return createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
||||
envelopeId: envelope.id,
|
||||
user: {
|
||||
id: userId,
|
||||
},
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
fieldId: field.secondaryId,
|
||||
@@ -743,9 +740,6 @@ export const createEnvelope = async ({
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.CONTENT_CREATED,
|
||||
envelopeId: envelope.id,
|
||||
user: {
|
||||
id: userId,
|
||||
},
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
contentId: content.id,
|
||||
|
||||
@@ -209,9 +209,6 @@ export const duplicateEnvelope = async ({
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
|
||||
envelopeId: duplicatedEnvelope.id,
|
||||
user: {
|
||||
id: userId,
|
||||
},
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
title: duplicatedEnvelope.title,
|
||||
@@ -268,9 +265,6 @@ export const duplicateEnvelope = async ({
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_CREATED,
|
||||
envelopeId: duplicatedEnvelope.id,
|
||||
user: {
|
||||
id: userId,
|
||||
},
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
fieldId: field.secondaryId,
|
||||
@@ -304,9 +298,6 @@ export const duplicateEnvelope = async ({
|
||||
createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.CONTENT_CREATED,
|
||||
envelopeId: duplicatedEnvelope.id,
|
||||
user: {
|
||||
id: userId,
|
||||
},
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
contentId: content.id,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from 'zod';
|
||||
import { CONTENT_TEXT_MAX_LENGTH } from '../constants/envelope-content';
|
||||
import { DataContentType } from './data-content-meta';
|
||||
|
||||
// Note: The default is different to fields (12), the range is the same.
|
||||
@@ -237,7 +238,7 @@ export const ZBasePositionalContentMetaSchema = z.object({
|
||||
|
||||
export const ZContentTextMetaSchema = ZBasePositionalContentMetaSchema.extend({
|
||||
type: z.literal(EnvelopeContentType.TEXT),
|
||||
text: z.string().optional().default(''),
|
||||
text: z.string().max(CONTENT_TEXT_MAX_LENGTH).optional().default(''),
|
||||
textAlign: ZContentTextAlignSchema.optional().default(DEFAULT_CONTENT_TEXT_ALIGN),
|
||||
verticalAlign: ZContentVerticalAlignSchema.optional().default(DEFAULT_CONTENT_VERTICAL_ALIGN),
|
||||
lineHeight: ZContentLineHeightSchema.optional().default(DEFAULT_CONTENT_LINE_HEIGHT),
|
||||
|
||||
@@ -1,5 +1,41 @@
|
||||
import { ZCreateEnvelopeRequestSchema, ZCreateEnvelopeResponseSchema } from '../envelope-router/create-envelope.types';
|
||||
import { ZEnvelopeContentMetaSchema } from '@documenso/lib/types/envelope-content-meta';
|
||||
import { z } from 'zod';
|
||||
import { zfd } from 'zod-form-data';
|
||||
|
||||
export const ZCreateEmbeddingEnvelopeRequestSchema = ZCreateEnvelopeRequestSchema;
|
||||
import { zfdContentImageFile, zfdFile, zodFormData } from '../../utils/zod-form-data';
|
||||
import { ZCreateEnvelopePayloadSchema, ZCreateEnvelopeResponseSchema } from '../envelope-router/create-envelope.types';
|
||||
|
||||
/**
|
||||
* Extend the create envelope route with contents since embeds set all the data at once.
|
||||
*/
|
||||
export const ZCreateEmbeddingEnvelopePayloadSchema = ZCreateEnvelopePayloadSchema.extend({
|
||||
contents: z
|
||||
.object({
|
||||
identifier: z
|
||||
.union([z.string(), z.number().int().min(0)])
|
||||
.describe('Either the filename or the zero-based index of the file that was uploaded to attach the content to.')
|
||||
.optional(),
|
||||
contentMeta: ZEnvelopeContentMetaSchema.describe('All the properties of the content being placed.'),
|
||||
imageIndex: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.describe(
|
||||
'The index of the file in `contentImages` to show in the content. Only image contents can hold an image.',
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const ZCreateEmbeddingEnvelopeRequestSchema = zodFormData({
|
||||
payload: zfd.json(ZCreateEmbeddingEnvelopePayloadSchema),
|
||||
files: zfd.repeatableOfType(zfdFile()),
|
||||
contentImages: zfd.repeatableOfType(zfdContentImageFile()),
|
||||
});
|
||||
|
||||
export const ZCreateEmbeddingEnvelopeResponseSchema = ZCreateEnvelopeResponseSchema;
|
||||
|
||||
export type TCreateEmbeddingEnvelopePayload = z.infer<typeof ZCreateEmbeddingEnvelopePayloadSchema>;
|
||||
export type TCreateEmbeddingEnvelopeRequest = z.infer<typeof ZCreateEmbeddingEnvelopeRequestSchema>;
|
||||
|
||||
@@ -13,8 +13,8 @@ import type { Logger } from 'pino';
|
||||
import { match, P } from 'ts-pattern';
|
||||
|
||||
import { insertFormValuesInPdf } from '../../../lib/server-only/pdf/insert-form-values-in-pdf';
|
||||
import type { TCreateEmbeddingEnvelopeRequest } from '../embedding-router/create-embedding-envelope.types';
|
||||
import { authenticatedProcedure } from '../trpc';
|
||||
import type { TCreateEnvelopeRequest } from './create-envelope.types';
|
||||
import {
|
||||
createEnvelopeMeta,
|
||||
ZCreateEnvelopeRequestSchema,
|
||||
@@ -51,7 +51,16 @@ type CreateEnvelopeRouteOptions = {
|
||||
* Unverified team ID.
|
||||
*/
|
||||
teamId: number;
|
||||
input: TCreateEnvelopeRequest;
|
||||
|
||||
/**
|
||||
* This function is used by both embeds and normal flows.
|
||||
*
|
||||
* Since embeds also allow setting contents, we need to remap the type to allow
|
||||
* undefined contents for normal flows.
|
||||
*/
|
||||
input: Omit<TCreateEmbeddingEnvelopeRequest, 'contentImages'> & {
|
||||
contentImages?: File[];
|
||||
};
|
||||
apiRequestMetadata: ApiRequestMetadata;
|
||||
|
||||
/**
|
||||
@@ -73,7 +82,7 @@ export const createEnvelopeRouteCaller = async ({
|
||||
logger,
|
||||
options = {},
|
||||
}: CreateEnvelopeRouteOptions) => {
|
||||
const { payload, files, contentImages } = input;
|
||||
const { payload, files, contentImages = [] } = input;
|
||||
|
||||
const {
|
||||
title,
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ZDocumentAccessAuthTypesSchema, ZDocumentActionAuthTypesSchema } from '
|
||||
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
|
||||
import { ZDocumentMetaCreateSchema } from '@documenso/lib/types/document-meta';
|
||||
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
|
||||
import { ZEnvelopeContentMetaSchema } from '@documenso/lib/types/envelope-content-meta';
|
||||
import {
|
||||
ZClampedFieldHeightSchema,
|
||||
ZClampedFieldPositionXSchema,
|
||||
@@ -16,7 +15,7 @@ import { EnvelopeType } from '@prisma/client';
|
||||
import { z } from 'zod';
|
||||
import { zfd } from 'zod-form-data';
|
||||
|
||||
import { zfdContentImageFile, zfdFile, zodFormData } from '../../utils/zod-form-data';
|
||||
import { zfdFile, zodFormData } from '../../utils/zod-form-data';
|
||||
import { ZDocumentExternalIdSchema, ZDocumentTitleSchema, ZDocumentVisibilitySchema } from '../document-router/schema';
|
||||
import type { TrpcRouteMeta } from '../trpc';
|
||||
import { ZCreateEnvelopeRecipientSchema } from './envelope-recipients/create-envelope-recipients.types';
|
||||
@@ -32,6 +31,9 @@ export const createEnvelopeMeta: TrpcRouteMeta = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Note: This is reused in the embedding routes.
|
||||
*/
|
||||
export const ZCreateEnvelopePayloadSchema = z.object({
|
||||
title: ZDocumentTitleSchema,
|
||||
type: z.nativeEnum(EnvelopeType),
|
||||
@@ -70,24 +72,6 @@ export const ZCreateEnvelopePayloadSchema = z.object({
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
contents: z
|
||||
.object({
|
||||
identifier: z
|
||||
.union([z.string(), z.number().int().min(0)])
|
||||
.describe('Either the filename or the zero-based index of the file that was uploaded to attach the content to.')
|
||||
.optional(),
|
||||
contentMeta: ZEnvelopeContentMetaSchema.describe('All the properties of the content being placed.'),
|
||||
imageIndex: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.describe(
|
||||
'The index of the file in `contentImages` to show in the content. Only image contents can hold an image.',
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.array()
|
||||
.optional(),
|
||||
meta: ZDocumentMetaCreateSchema.optional(),
|
||||
attachments: z
|
||||
.array(
|
||||
@@ -103,7 +87,6 @@ export const ZCreateEnvelopePayloadSchema = z.object({
|
||||
export const ZCreateEnvelopeRequestSchema = zodFormData({
|
||||
payload: zfd.json(ZCreateEnvelopePayloadSchema),
|
||||
files: zfd.repeatableOfType(zfdFile()),
|
||||
contentImages: zfd.repeatableOfType(zfdContentImageFile()),
|
||||
});
|
||||
|
||||
export const ZCreateEnvelopeResponseSchema = z.object({
|
||||
|
||||
Reference in New Issue
Block a user