Merge branch 'main' into exp/autoplace-fields

This commit is contained in:
Ephraim Duncan
2025-11-19 00:44:51 +00:00
committed by GitHub
213 changed files with 1920 additions and 1439 deletions

View File

@ -28,9 +28,13 @@ import {
seedPendingDocument,
} from '@documenso/prisma/seed/documents';
import { seedBlankFolder } from '@documenso/prisma/seed/folders';
import { seedBlankTemplate } from '@documenso/prisma/seed/templates';
import { seedBlankTemplate, seedTemplate } from '@documenso/prisma/seed/templates';
import { seedUser } from '@documenso/prisma/seed/users';
import type { TCreateEnvelopeItemsPayload } from '@documenso/trpc/server/envelope-router/create-envelope-items.types';
import type {
TUseEnvelopePayload,
TUseEnvelopeResponse,
} from '@documenso/trpc/server/envelope-router/use-envelope.types';
const WEBAPP_BASE_URL = NEXT_PUBLIC_WEBAPP_URL();
@ -3074,6 +3078,82 @@ test.describe('Document API V2', () => {
});
});
test.describe('Envelope use endpoint', () => {
test('should block unauthorized access to envelope use endpoint', async ({ request }) => {
const doc = await seedTemplate({
title: 'Team template 1',
userId: userA.id,
teamId: teamA.id,
internalVersion: 2,
});
const payload: TUseEnvelopePayload = {
envelopeId: doc.id,
};
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/use`, {
headers: { Authorization: `Bearer ${tokenB}` },
multipart: formData,
});
expect(res.ok()).toBeFalsy();
expect(res.status()).toBe(404);
});
test('should allow authorized access to envelope use endpoint', async ({ page, request }) => {
const doc = await seedTemplate({
title: 'Team template 1',
userId: userA.id,
teamId: teamA.id,
internalVersion: 2,
});
const payload: TUseEnvelopePayload = {
envelopeId: doc.id,
distributeDocument: true,
recipients: [
{
id: doc.recipients[0].id,
email: doc.recipients[0].email,
name: 'New Name',
},
],
};
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
const res = await request.post(`${WEBAPP_BASE_URL}/api/v2-beta/envelope/use`, {
headers: { Authorization: `Bearer ${tokenA}` },
multipart: formData,
});
expect(res.ok()).toBeTruthy();
expect(res.status()).toBe(200);
const data: TUseEnvelopeResponse = await res.json();
const createdEnvelope = await prisma.envelope.findFirst({
where: {
id: data.id,
},
include: {
recipients: true,
},
});
expect(createdEnvelope).toBeDefined();
expect(createdEnvelope?.recipients.length).toBe(1);
expect(createdEnvelope?.recipients[0].email).toBe(doc.recipients[0].email);
expect(createdEnvelope?.recipients[0].name).toBe('New Name');
expect(createdEnvelope?.recipients[0].token).toBe(data.recipients[0].token);
expect(createdEnvelope?.recipients[0].token).not.toBe(doc.recipients[0].token);
});
});
test.describe('Envelope distribute endpoint', () => {
test('should block unauthorized access to envelope distribute endpoint', async ({
request,
@ -3925,8 +4005,12 @@ test.describe('Document API V2', () => {
// 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');
const isFieldMeta1 = envelopeItems.find((item) => item.title === 'field-meta-1.pdf');
const isFieldMeta2 = envelopeItems.find((item) => item.title === 'field-meta-2.pdf');
expect(isFieldMeta1).toBeDefined();
expect(isFieldMeta2).toBeDefined();
});
});

View File

@ -0,0 +1,35 @@
import React from 'react';
export type TemplateCustomMessageBodyProps = {
text?: string;
};
export const TemplateCustomMessageBody = ({ text }: TemplateCustomMessageBodyProps) => {
if (!text) {
return null;
}
const normalized = text
.trim()
.replace(/\r\n?/g, '\n')
.replace(/\n\s*\n+/g, '\n\n')
.replace(/\n{2,}/g, '\n\n');
const paragraphs = normalized.split('\n\n');
return paragraphs.map((paragraph, i) => (
<p
key={`p-${i}`}
className="whitespace-pre-line break-words font-sans text-base text-slate-400"
>
{paragraph.split('\n').map((line, j) => (
<React.Fragment key={`line-${i}-${j}`}>
{j > 0 && <br />}
{line}
</React.Fragment>
))}
</p>
));
};
export default TemplateCustomMessageBody;

View File

@ -8,6 +8,7 @@ import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-
import { Body, Container, Head, Hr, Html, Img, Link, Preview, Section, Text } from '../components';
import { useBranding } from '../providers/branding';
import { TemplateCustomMessageBody } from '../template-components/template-custom-message-body';
import type { TemplateDocumentInviteProps } from '../template-components/template-document-invite';
import { TemplateDocumentInvite } from '../template-components/template-document-invite';
import { TemplateFooter } from '../template-components/template-footer';
@ -105,7 +106,7 @@ export const DocumentInviteEmailTemplate = ({
<Text className="mt-2 text-base text-slate-400">
{customBody ? (
<pre className="font-sans text-base text-slate-400">{customBody}</pre>
<TemplateCustomMessageBody text={customBody} />
) : (
<Trans>
{inviterName} has invited you to {action} the document "{documentName}".

View File

@ -170,10 +170,15 @@ export const useEditorFields = ({
);
const setFieldId = (formId: string, id: number) => {
const index = localFields.findIndex((field) => field.formId === formId);
const { fields } = form.getValues();
const index = fields.findIndex((field) => field.formId === formId);
if (index !== -1) {
form.setValue(`fields.${index}.id`, id);
update(index, {
...fields[index],
id,
});
}
};

View File

@ -51,21 +51,21 @@ export const DOCUMENT_SIGNATURE_TYPES = {
[DocumentSignatureType.DRAW]: {
label: msg({
message: `Draw`,
context: `Draw signatute type`,
context: `Draw signature`,
}),
value: DocumentSignatureType.DRAW,
},
[DocumentSignatureType.TYPE]: {
label: msg({
message: `Type`,
context: `Type signatute type`,
context: `Type signature`,
}),
value: DocumentSignatureType.TYPE,
},
[DocumentSignatureType.UPLOAD]: {
label: msg({
message: `Upload`,
context: `Upload signatute type`,
context: `Upload signature`,
}),
value: DocumentSignatureType.UPLOAD,
},

View File

@ -1,6 +1,16 @@
import { z } from 'zod';
export const SUPPORTED_LANGUAGE_CODES = ['de', 'en', 'fr', 'es', 'it', 'pl'] as const;
export const SUPPORTED_LANGUAGE_CODES = [
'de',
'en',
'fr',
'es',
'it',
'pl',
'ja',
'ko',
'zh',
] as const;
export const ZSupportedLanguageCodeSchema = z.enum(SUPPORTED_LANGUAGE_CODES).catch('en');
@ -54,6 +64,18 @@ export const SUPPORTED_LANGUAGES: Record<string, SupportedLanguage> = {
short: 'pl',
full: 'Polish',
},
ja: {
short: 'ja',
full: 'Japanese',
},
ko: {
short: 'ko',
full: 'Korean',
},
zh: {
short: 'zh',
full: 'Chinese',
},
} satisfies Record<SupportedLanguageCodes, SupportedLanguage>;
export const isValidLanguageCode = (code: unknown): code is SupportedLanguageCodes =>

View File

@ -44,7 +44,7 @@ export const resendDocument = async ({
recipients,
teamId,
requestMetadata,
}: ResendDocumentOptions): Promise<void> => {
}: ResendDocumentOptions) => {
const user = await prisma.user.findFirstOrThrow({
where: {
id: userId,
@ -103,7 +103,7 @@ export const resendDocument = async ({
).recipientSigningRequest;
if (!isRecipientSigningRequestEmailEnabled) {
return;
return envelope;
}
const { branding, emailLanguage, organisationType, senderEmail, replyToEmail } =
@ -230,4 +230,6 @@ export const resendDocument = async ({
);
}),
);
return envelope;
};

View File

@ -31,26 +31,16 @@ export const viewedDocument = async ({
type: EnvelopeType.DOCUMENT,
},
},
include: {
envelope: {
include: {
documentMeta: true,
recipients: true,
},
},
},
});
if (!recipient) {
return;
}
const { envelope } = recipient;
await prisma.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_VIEWED,
envelopeId: envelope.id,
envelopeId: recipient.envelopeId,
user: {
name: recipient.name,
email: recipient.email,
@ -86,7 +76,7 @@ export const viewedDocument = async ({
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
envelopeId: envelope.id,
envelopeId: recipient.envelopeId,
user: {
name: recipient.name,
email: recipient.email,
@ -103,6 +93,16 @@ export const viewedDocument = async ({
});
});
const envelope = await prisma.envelope.findUniqueOrThrow({
where: {
id: recipient.envelopeId,
},
include: {
documentMeta: true,
recipients: true,
},
});
await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_OPENED,
data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelope)),

View File

@ -373,3 +373,52 @@ export const FIELD_META_DEFAULT_VALUES: Record<FieldType, TFieldMetaSchema> = {
[FieldType.CHECKBOX]: FIELD_CHECKBOX_META_DEFAULT_VALUES,
[FieldType.DROPDOWN]: FIELD_DROPDOWN_META_DEFAULT_VALUES,
} as const;
export const ZEnvelopeFieldAndMetaSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal(FieldType.SIGNATURE),
fieldMeta: ZSignatureFieldMeta.optional().default(FIELD_SIGNATURE_META_DEFAULT_VALUES),
}),
z.object({
type: z.literal(FieldType.FREE_SIGNATURE),
fieldMeta: z.undefined(),
}),
z.object({
type: z.literal(FieldType.INITIALS),
fieldMeta: ZInitialsFieldMeta.optional().default(FIELD_INITIALS_META_DEFAULT_VALUES),
}),
z.object({
type: z.literal(FieldType.NAME),
fieldMeta: ZNameFieldMeta.optional().default(FIELD_NAME_META_DEFAULT_VALUES),
}),
z.object({
type: z.literal(FieldType.EMAIL),
fieldMeta: ZEmailFieldMeta.optional().default(FIELD_EMAIL_META_DEFAULT_VALUES),
}),
z.object({
type: z.literal(FieldType.DATE),
fieldMeta: ZDateFieldMeta.optional().default(FIELD_DATE_META_DEFAULT_VALUES),
}),
z.object({
type: z.literal(FieldType.TEXT),
fieldMeta: ZTextFieldMeta.optional().default(FIELD_TEXT_META_DEFAULT_VALUES),
}),
z.object({
type: z.literal(FieldType.NUMBER),
fieldMeta: ZNumberFieldMeta.optional().default(FIELD_NUMBER_META_DEFAULT_VALUES),
}),
z.object({
type: z.literal(FieldType.RADIO),
fieldMeta: ZRadioFieldMeta.optional().default(FIELD_RADIO_META_DEFAULT_VALUES),
}),
z.object({
type: z.literal(FieldType.CHECKBOX),
fieldMeta: ZCheckboxFieldMeta.optional().default(FIELD_CHECKBOX_META_DEFAULT_VALUES),
}),
z.object({
type: z.literal(FieldType.DROPDOWN),
fieldMeta: ZDropdownFieldMeta.optional().default(FIELD_DROPDOWN_META_DEFAULT_VALUES),
}),
]);
type TEnvelopeFieldAndMeta = z.infer<typeof ZEnvelopeFieldAndMetaSchema>;

View File

@ -0,0 +1,41 @@
-- CreateIndex
CREATE INDEX "Envelope_folderId_idx" ON "Envelope"("folderId");
-- CreateIndex
CREATE INDEX "Envelope_teamId_idx" ON "Envelope"("teamId");
-- CreateIndex
CREATE INDEX "Envelope_userId_idx" ON "Envelope"("userId");
-- CreateIndex
CREATE INDEX "EnvelopeAttachment_envelopeId_idx" ON "EnvelopeAttachment"("envelopeId");
-- CreateIndex
CREATE INDEX "EnvelopeItem_envelopeId_idx" ON "EnvelopeItem"("envelopeId");
-- CreateIndex
CREATE INDEX "Field_envelopeItemId_idx" ON "Field"("envelopeItemId");
-- CreateIndex
CREATE INDEX "OrganisationGroup_organisationId_idx" ON "OrganisationGroup"("organisationId");
-- CreateIndex
CREATE INDEX "OrganisationGroupMember_groupId_idx" ON "OrganisationGroupMember"("groupId");
-- CreateIndex
CREATE INDEX "OrganisationGroupMember_organisationMemberId_idx" ON "OrganisationGroupMember"("organisationMemberId");
-- CreateIndex
CREATE INDEX "Session_userId_idx" ON "Session"("userId");
-- CreateIndex
CREATE INDEX "Session_sessionToken_idx" ON "Session"("sessionToken");
-- CreateIndex
CREATE INDEX "Team_organisationId_idx" ON "Team"("organisationId");
-- CreateIndex
CREATE INDEX "TeamGroup_teamId_idx" ON "TeamGroup"("teamId");
-- CreateIndex
CREATE INDEX "TeamGroup_organisationGroupId_idx" ON "TeamGroup"("organisationGroupId");

View File

@ -319,6 +319,9 @@ model Session {
updatedAt DateTime @updatedAt
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([sessionToken])
}
enum DocumentStatus {
@ -426,6 +429,10 @@ model Envelope {
documentMeta DocumentMeta @relation(fields: [documentMetaId], references: [id])
envelopeAttachments EnvelopeAttachment[]
@@index([folderId])
@@index([teamId])
@@index([userId])
}
model EnvelopeItem {
@ -444,6 +451,7 @@ model EnvelopeItem {
field Field[]
@@unique([documentDataId])
@@index([envelopeId])
}
model DocumentAuditLog {
@ -526,6 +534,8 @@ model EnvelopeAttachment {
envelopeId String
envelope Envelope @relation(fields: [envelopeId], references: [id], onDelete: Cascade)
@@index([envelopeId])
}
enum ReadStatus {
@ -613,6 +623,7 @@ model Field {
fieldMeta Json? /// [FieldMeta] @zod.custom.use(ZFieldMetaNotOptionalSchema)
@@index([envelopeId])
@@index([envelopeItemId])
@@index([recipientId])
}
@ -728,6 +739,8 @@ model OrganisationGroup {
organisationGroupMembers OrganisationGroupMember[]
teamGroups TeamGroup[]
@@index([organisationId])
}
model OrganisationGroupMember {
@ -740,6 +753,8 @@ model OrganisationGroupMember {
organisationMemberId String
@@unique([organisationMemberId, groupId])
@@index([groupId])
@@index([organisationMemberId])
}
model TeamGroup {
@ -754,6 +769,8 @@ model TeamGroup {
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
@@unique([teamId, organisationGroupId])
@@index([teamId])
@@index([organisationGroupId])
}
enum OrganisationGroupType {
@ -865,6 +882,8 @@ model Team {
teamGlobalSettingsId String @unique
teamGlobalSettings TeamGlobalSettings @relation(fields: [teamGlobalSettingsId], references: [id], onDelete: Cascade)
@@index([organisationId])
}
model TeamEmail {

View File

@ -110,7 +110,7 @@ export const seedTemplate = async (options: SeedTemplateOptions) => {
data: {
id: prefixedId('envelope'),
secondaryId: templateId.formattedTemplateId,
internalVersion: 1,
internalVersion: options.internalVersion ?? 1,
type: EnvelopeType.TEMPLATE,
title,
envelopeItems: {
@ -143,6 +143,7 @@ export const seedTemplate = async (options: SeedTemplateOptions) => {
documentData: true,
},
},
recipients: true,
},
});
};

View File

@ -41,10 +41,14 @@ export const createAttachmentRoute = authenticatedProcedure
type: EnvelopeType.DOCUMENT,
});
await createAttachment({
const attachment = await createAttachment({
envelopeId: envelope.id,
teamId,
userId,
data,
});
return {
id: attachment.id,
};
});

View File

@ -8,7 +8,9 @@ export const ZCreateAttachmentRequestSchema = z.object({
}),
});
export const ZCreateAttachmentResponseSchema = z.void();
export const ZCreateAttachmentResponseSchema = z.object({
id: z.string(),
});
export type TCreateAttachmentRequest = z.infer<typeof ZCreateAttachmentRequestSchema>;
export type TCreateAttachmentResponse = z.infer<typeof ZCreateAttachmentResponseSchema>;

View File

@ -1,5 +1,6 @@
import { deleteAttachment } from '@documenso/lib/server-only/envelope-attachment/delete-attachment';
import { ZGenericSuccessResponse } from '../../schema';
import { authenticatedProcedure } from '../../trpc';
import {
ZDeleteAttachmentRequestSchema,
@ -33,4 +34,6 @@ export const deleteAttachmentRoute = authenticatedProcedure
userId,
teamId,
});
return ZGenericSuccessResponse;
});

View File

@ -1,10 +1,12 @@
import { z } from 'zod';
import { ZSuccessResponseSchema } from '../../schema';
export const ZDeleteAttachmentRequestSchema = z.object({
id: z.string(),
});
export const ZDeleteAttachmentResponseSchema = z.void();
export const ZDeleteAttachmentResponseSchema = ZSuccessResponseSchema;
export type TDeleteAttachmentRequest = z.infer<typeof ZDeleteAttachmentRequestSchema>;
export type TDeleteAttachmentResponse = z.infer<typeof ZDeleteAttachmentResponseSchema>;

View File

@ -1,5 +1,6 @@
import { updateAttachment } from '@documenso/lib/server-only/envelope-attachment/update-attachment';
import { ZGenericSuccessResponse } from '../../schema';
import { authenticatedProcedure } from '../../trpc';
import {
ZUpdateAttachmentRequestSchema,
@ -34,4 +35,6 @@ export const updateAttachmentRoute = authenticatedProcedure
teamId,
data,
});
return ZGenericSuccessResponse;
});

View File

@ -1,5 +1,7 @@
import { z } from 'zod';
import { ZSuccessResponseSchema } from '../../schema';
export const ZUpdateAttachmentRequestSchema = z.object({
id: z.string(),
data: z.object({
@ -8,7 +10,7 @@ export const ZUpdateAttachmentRequestSchema = z.object({
}),
});
export const ZUpdateAttachmentResponseSchema = z.void();
export const ZUpdateAttachmentResponseSchema = ZSuccessResponseSchema;
export type TUpdateAttachmentRequest = z.infer<typeof ZUpdateAttachmentRequestSchema>;
export type TUpdateAttachmentResponse = z.infer<typeof ZUpdateAttachmentResponseSchema>;

View File

@ -1,12 +1,12 @@
import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
import { ZGenericSuccessResponse } from '../schema';
import { authenticatedProcedure } from '../trpc';
import {
ZDeleteDocumentRequestSchema,
ZDeleteDocumentResponseSchema,
deleteDocumentMeta,
} from './delete-document.types';
import { ZGenericSuccessResponse } from './schema';
export const deleteDocumentRoute = authenticatedProcedure
.meta(deleteDocumentMeta)

View File

@ -1,7 +1,7 @@
import { z } from 'zod';
import { ZSuccessResponseSchema } from '../schema';
import type { TrpcRouteMeta } from '../trpc';
import { ZSuccessResponseSchema } from './schema';
export const deleteDocumentMeta: TrpcRouteMeta = {
openapi: {

View File

@ -19,5 +19,6 @@ export const downloadDocumentRoute = authenticatedProcedure
},
});
// This endpoint is purely for V2 API, which is implemented in the Hono remix server.
throw new Error('NOT_IMPLEMENTED');
});

View File

@ -1,12 +1,12 @@
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
import { ZGenericSuccessResponse } from '../schema';
import { authenticatedProcedure } from '../trpc';
import {
ZRedistributeDocumentRequestSchema,
ZRedistributeDocumentResponseSchema,
redistributeDocumentMeta,
} from './redistribute-document.types';
import { ZGenericSuccessResponse } from './schema';
export const redistributeDocumentRoute = authenticatedProcedure
.meta(redistributeDocumentMeta)

View File

@ -1,7 +1,7 @@
import { z } from 'zod';
import { ZSuccessResponseSchema } from '../schema';
import type { TrpcRouteMeta } from '../trpc';
import { ZSuccessResponseSchema } from './schema';
export const redistributeDocumentMeta: TrpcRouteMeta = {
openapi: {

View File

@ -1,19 +1,6 @@
import { DocumentVisibility } from '@prisma/client';
import { z } from 'zod';
/**
* Required for empty responses since we currently can't 201 requests for our openapi setup.
*
* Without this it will throw an error in Speakeasy SDK when it tries to parse an empty response.
*/
export const ZSuccessResponseSchema = z.object({
success: z.literal(true),
});
export const ZGenericSuccessResponse = {
success: true,
} satisfies z.infer<typeof ZSuccessResponseSchema>;
export const ZDocumentTitleSchema = z
.string()
.trim()

View File

@ -21,33 +21,38 @@ import {
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { RecipientRole } from '@documenso/prisma/client';
import { DocumentSigningOrder } from '@documenso/prisma/generated/types';
import { ZDocumentExternalIdSchema, ZDocumentTitleSchema } from '../document-router/schema';
import { ZCreateRecipientSchema } from '../recipient-router/schema';
export const ZCreateEmbeddingDocumentRequestSchema = z.object({
title: ZDocumentTitleSchema,
documentDataId: z.string(),
externalId: ZDocumentExternalIdSchema.optional(),
recipients: z
.array(
ZCreateRecipientSchema.extend({
fields: ZFieldAndMetaSchema.and(
z.object({
pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
}),
)
.array()
.optional(),
}),
)
.optional(),
recipients: z.array(
z.object({
id: z.number().optional(),
email: z.string().email(),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
// We have an any cast so any changes here you need to update it in the embeding document edit page
// Search: "map<any>" to find it
fields: ZFieldAndMetaSchema.and(
z.object({
id: z.number().optional(),
pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
}),
)
.array()
.optional(),
}),
),
meta: z
.object({
subject: ZDocumentMetaSubjectSchema.optional(),

View File

@ -1,4 +1,4 @@
import { DocumentSigningOrder, FieldType, RecipientRole } from '@prisma/client';
import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { z } from 'zod';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
@ -21,30 +21,33 @@ import {
ZFieldPageYSchema,
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { ZDocumentTitleSchema } from '../document-router/schema';
const ZFieldSchema = z.object({
type: z.nativeEnum(FieldType),
pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
fieldMeta: ZFieldMetaSchema.optional(),
});
export const ZCreateEmbeddingTemplateRequestSchema = z.object({
title: ZDocumentTitleSchema,
documentDataId: z.string(),
recipients: z.array(
z.object({
email: z.string().email(),
name: z.string().optional(),
role: z.nativeEnum(RecipientRole).optional(),
email: z.union([z.string().length(0), z.string().email()]),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
fields: z.array(ZFieldSchema).optional(),
// We have an any cast so any changes here you need to update it in the embeding document edit page
// Search: "map<any>" to find it
fields: ZFieldAndMetaSchema.and(
z.object({
id: z.number().optional(),
pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
}),
)
.array()
.optional(),
}),
),
meta: z

View File

@ -32,7 +32,7 @@ export const ZUpdateEmbeddingDocumentRequestSchema = z.object({
recipients: z.array(
z.object({
id: z.number().optional(),
email: z.string().toLowerCase().email().min(1),
email: z.string().email(),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),

View File

@ -3,7 +3,6 @@ import { verifyEmbeddingPresignToken } from '@documenso/lib/server-only/embeddin
import { updateEnvelope } from '@documenso/lib/server-only/envelope/update-envelope';
import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-fields-for-template';
import { setTemplateRecipients } from '@documenso/lib/server-only/recipient/set-template-recipients';
import { nanoid } from '@documenso/lib/universal/id';
import { procedure } from '../trpc';
import {
@ -53,11 +52,6 @@ export const updateEmbeddingTemplateRoute = procedure
requestMetadata: ctx.metadata,
});
const recipientsWithClientId = recipients.map((recipient) => ({
...recipient,
clientId: nanoid(),
}));
const { recipients: updatedRecipients } = await setTemplateRecipients({
userId: apiToken.userId,
teamId: apiToken.teamId ?? undefined,
@ -65,7 +59,7 @@ export const updateEmbeddingTemplateRoute = procedure
type: 'templateId',
id: templateId,
},
recipients: recipientsWithClientId.map((recipient) => ({
recipients: recipients.map((recipient) => ({
id: recipient.id,
email: recipient.email,
name: recipient.name ?? '',
@ -74,8 +68,8 @@ export const updateEmbeddingTemplateRoute = procedure
})),
});
const fields = recipientsWithClientId.flatMap((recipient) => {
const recipientId = updatedRecipients.find((r) => r.email === recipient.email)?.id;
const fields = recipients.flatMap((recipient) => {
const recipientId = updatedRecipients.find((r) => r.id === recipient.id)?.id;
if (!recipientId) {
throw new AppError(AppErrorCode.UNKNOWN_ERROR, {
@ -86,8 +80,6 @@ export const updateEmbeddingTemplateRoute = procedure
return (recipient.fields ?? []).map((field) => ({
...field,
recipientId,
// !: Temp property to be removed once we don't link based on signer email
signerEmail: recipient.email,
}));
});

View File

@ -21,7 +21,7 @@ import {
ZFieldPageYSchema,
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { ZFieldAndMetaSchema, ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { ZDocumentTitleSchema } from '../document-router/schema';
@ -44,11 +44,25 @@ export const ZUpdateEmbeddingTemplateRequestSchema = z.object({
recipients: z.array(
z.object({
id: z.number().optional(),
email: z.string().email(),
name: z.string().optional(),
role: z.nativeEnum(RecipientRole).optional(),
email: z.union([z.string().length(0), z.string().email()]),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
fields: z.array(ZFieldSchema).optional(),
// We have an any cast so any changes here you need to update it in the embeding document edit page
// Search: "map<any>" to find it
fields: ZFieldAndMetaSchema.and(
z.object({
id: z.number().optional(),
pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
envelopeItemId: z.string(),
}),
)
.array()
.optional(),
}),
),
meta: z

View File

@ -71,7 +71,7 @@ export const createSubscriptionRoute = authenticatedProcedure
}
const returnUrl = isPersonalLayoutMode
? `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing`
? `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing-personal`
: `${NEXT_PUBLIC_WEBAPP_URL()}/o/${organisation.url}/settings/billing`;
const redirectUrl = await createCheckoutSession({

View File

@ -12,7 +12,7 @@ import { ZManageSubscriptionRequestSchema } from './manage-subscription.types';
export const manageSubscriptionRoute = authenticatedProcedure
.input(ZManageSubscriptionRequestSchema)
.mutation(async ({ ctx, input }) => {
const { organisationId } = input;
const { organisationId, isPersonalLayoutMode } = input;
ctx.logger.info({
input: {
@ -93,9 +93,13 @@ export const manageSubscriptionRoute = authenticatedProcedure
});
}
const returnUrl = isPersonalLayoutMode
? `${NEXT_PUBLIC_WEBAPP_URL()}/settings/billing-personal`
: `${NEXT_PUBLIC_WEBAPP_URL()}/o/${organisation.url}/settings/billing`;
const redirectUrl = await getPortalSession({
customerId,
returnUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/o/${organisation.url}/settings/billing`,
returnUrl,
});
return {

View File

@ -2,4 +2,5 @@ import { z } from 'zod';
export const ZManageSubscriptionRequestSchema = z.object({
organisationId: z.string().describe('The organisation to manage the subscription for'),
isPersonalLayoutMode: z.boolean().optional(),
});

View File

@ -21,10 +21,14 @@ export const createAttachmentRoute = authenticatedProcedure
input: { envelopeId, label: data.label },
});
await createAttachment({
const attachment = await createAttachment({
envelopeId,
teamId,
userId,
data,
});
return {
id: attachment.id,
};
});

View File

@ -20,7 +20,9 @@ export const ZCreateAttachmentRequestSchema = z.object({
}),
});
export const ZCreateAttachmentResponseSchema = z.void();
export const ZCreateAttachmentResponseSchema = z.object({
id: z.string(),
});
export type TCreateAttachmentRequest = z.infer<typeof ZCreateAttachmentRequestSchema>;
export type TCreateAttachmentResponse = z.infer<typeof ZCreateAttachmentResponseSchema>;

View File

@ -1,5 +1,6 @@
import { deleteAttachment } from '@documenso/lib/server-only/envelope-attachment/delete-attachment';
import { ZGenericSuccessResponse } from '../../schema';
import { authenticatedProcedure } from '../../trpc';
import {
ZDeleteAttachmentRequestSchema,
@ -26,4 +27,6 @@ export const deleteAttachmentRoute = authenticatedProcedure
userId,
teamId,
});
return ZGenericSuccessResponse;
});

View File

@ -1,5 +1,6 @@
import { z } from 'zod';
import { ZSuccessResponseSchema } from '../../schema';
import type { TrpcRouteMeta } from '../../trpc';
export const deleteAttachmentMeta: TrpcRouteMeta = {
@ -16,7 +17,7 @@ export const ZDeleteAttachmentRequestSchema = z.object({
id: z.string(),
});
export const ZDeleteAttachmentResponseSchema = z.void();
export const ZDeleteAttachmentResponseSchema = ZSuccessResponseSchema;
export type TDeleteAttachmentRequest = z.infer<typeof ZDeleteAttachmentRequestSchema>;
export type TDeleteAttachmentResponse = z.infer<typeof ZDeleteAttachmentResponseSchema>;

View File

@ -1,5 +1,6 @@
import { updateAttachment } from '@documenso/lib/server-only/envelope-attachment/update-attachment';
import { ZGenericSuccessResponse } from '../../schema';
import { authenticatedProcedure } from '../../trpc';
import {
ZUpdateAttachmentRequestSchema,
@ -27,4 +28,6 @@ export const updateAttachmentRoute = authenticatedProcedure
teamId,
data,
});
return ZGenericSuccessResponse;
});

View File

@ -1,5 +1,6 @@
import { z } from 'zod';
import { ZSuccessResponseSchema } from '../../schema';
import type { TrpcRouteMeta } from '../../trpc';
export const updateAttachmentMeta: TrpcRouteMeta = {
@ -20,7 +21,7 @@ export const ZUpdateAttachmentRequestSchema = z.object({
}),
});
export const ZUpdateAttachmentResponseSchema = z.void();
export const ZUpdateAttachmentResponseSchema = ZSuccessResponseSchema;
export type TUpdateAttachmentRequest = z.infer<typeof ZUpdateAttachmentRequestSchema>;
export type TUpdateAttachmentResponse = z.infer<typeof ZUpdateAttachmentResponseSchema>;

View File

@ -11,6 +11,7 @@ export const createEnvelopeItemsMeta: TrpcRouteMeta = {
method: 'POST',
path: '/envelope/item/create-many',
summary: 'Create envelope items',
contentTypes: ['multipart/form-data'],
description: 'Create multiple envelope items for an envelope',
tags: ['Envelope Items'],
},

View File

@ -16,7 +16,7 @@ import {
ZClampedFieldWidthSchema,
ZFieldPageNumberSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { ZEnvelopeFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { zodFormData } from '../../utils/zod-form-data';
import {
@ -55,7 +55,7 @@ export const ZCreateEnvelopePayloadSchema = z.object({
recipients: z
.array(
ZCreateRecipientSchema.extend({
fields: ZFieldAndMetaSchema.and(
fields: ZEnvelopeFieldAndMetaSchema.and(
z.object({
identifier: z
.union([z.string(), z.number()])

View File

@ -5,6 +5,7 @@ import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { ZGenericSuccessResponse } from '../schema';
import { authenticatedProcedure } from '../trpc';
import {
ZDeleteEnvelopeItemRequestSchema,
@ -100,4 +101,6 @@ export const deleteEnvelopeItemRoute = authenticatedProcedure
},
},
});
return ZGenericSuccessResponse;
});

View File

@ -1,5 +1,6 @@
import { z } from 'zod';
import { ZSuccessResponseSchema } from '../schema';
import type { TrpcRouteMeta } from '../trpc';
export const deleteEnvelopeItemMeta: TrpcRouteMeta = {
@ -17,7 +18,7 @@ export const ZDeleteEnvelopeItemRequestSchema = z.object({
envelopeItemId: z.string(),
});
export const ZDeleteEnvelopeItemResponseSchema = z.void();
export const ZDeleteEnvelopeItemResponseSchema = ZSuccessResponseSchema;
export type TDeleteEnvelopeItemRequest = z.infer<typeof ZDeleteEnvelopeItemRequestSchema>;
export type TDeleteEnvelopeItemResponse = z.infer<typeof ZDeleteEnvelopeItemResponseSchema>;

View File

@ -6,6 +6,7 @@ import { deleteDocument } from '@documenso/lib/server-only/document/delete-docum
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
import { prisma } from '@documenso/prisma';
import { ZGenericSuccessResponse } from '../schema';
import { authenticatedProcedure } from '../trpc';
import {
ZDeleteEnvelopeRequestSchema,
@ -65,4 +66,6 @@ export const deleteEnvelopeRoute = authenticatedProcedure
}),
)
.exhaustive();
return ZGenericSuccessResponse;
});

View File

@ -1,5 +1,6 @@
import { z } from 'zod';
import { ZSuccessResponseSchema } from '../schema';
import type { TrpcRouteMeta } from '../trpc';
export const deleteEnvelopeMeta: TrpcRouteMeta = {
@ -15,7 +16,7 @@ export const ZDeleteEnvelopeRequestSchema = z.object({
envelopeId: z.string(),
});
export const ZDeleteEnvelopeResponseSchema = z.void();
export const ZDeleteEnvelopeResponseSchema = ZSuccessResponseSchema;
export type TDeleteEnvelopeRequest = z.infer<typeof ZDeleteEnvelopeRequestSchema>;
export type TDeleteEnvelopeResponse = z.infer<typeof ZDeleteEnvelopeResponseSchema>;

View File

@ -1,5 +1,6 @@
import { updateDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import { sendDocument } from '@documenso/lib/server-only/document/send-document';
import { formatSigningLink } from '@documenso/lib/utils/recipients';
import { authenticatedProcedure } from '../trpc';
import {
@ -44,7 +45,7 @@ export const distributeEnvelopeRoute = authenticatedProcedure
});
}
await sendDocument({
const envelope = await sendDocument({
userId: ctx.user.id,
id: {
type: 'envelopeId',
@ -53,4 +54,18 @@ export const distributeEnvelopeRoute = authenticatedProcedure
teamId,
requestMetadata: ctx.metadata,
});
return {
success: true,
id: envelope.id,
recipients: envelope.recipients.map((recipient) => ({
id: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: formatSigningLink(recipient.token),
})),
};
});

View File

@ -2,7 +2,9 @@ import { z } from 'zod';
import { ZDocumentMetaUpdateSchema } from '@documenso/lib/types/document-meta';
import { ZSuccessResponseSchema } from '../schema';
import type { TrpcRouteMeta } from '../trpc';
import { ZRecipientWithSigningUrlSchema } from './schema';
export const distributeEnvelopeMeta: TrpcRouteMeta = {
openapi: {
@ -30,7 +32,10 @@ export const ZDistributeEnvelopeRequestSchema = z.object({
}).optional(),
});
export const ZDistributeEnvelopeResponseSchema = z.void();
export const ZDistributeEnvelopeResponseSchema = ZSuccessResponseSchema.extend({
id: z.string().describe('The ID of the envelope that was sent.'),
recipients: ZRecipientWithSigningUrlSchema.array(),
});
export type TDistributeEnvelopeRequest = z.infer<typeof ZDistributeEnvelopeRequestSchema>;
export type TDistributeEnvelopeResponse = z.infer<typeof ZDistributeEnvelopeResponseSchema>;

View File

@ -19,5 +19,6 @@ export const downloadEnvelopeItemRoute = authenticatedProcedure
},
});
// This endpoint is purely for V2 API, which is implemented in the Hono remix server.
throw new Error('NOT_IMPLEMENTED');
});

View File

@ -5,7 +5,7 @@ import type { TrpcRouteMeta } from '../trpc';
export const downloadEnvelopeItemMeta: TrpcRouteMeta = {
openapi: {
method: 'GET',
path: '/envelopeItem/{envelopeItemId}/download',
path: '/envelope/item/{envelopeItemId}/download',
summary: 'Download an envelope item',
description: 'Download an envelope item by its ID',
tags: ['Envelope Items'],

View File

@ -8,7 +8,7 @@ import {
ZFieldPageNumberSchema,
ZFieldSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { ZEnvelopeFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import type { TrpcRouteMeta } from '../../trpc';
@ -16,14 +16,13 @@ export const createEnvelopeFieldsMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/field/create-many',
contentTypes: ['multipart/form-data'],
summary: 'Create envelope fields',
description: 'Create multiple fields for an envelope',
tags: ['Envelope Fields'],
},
};
const ZCreateFieldSchema = ZFieldAndMetaSchema.and(
const ZCreateFieldSchema = ZEnvelopeFieldAndMetaSchema.and(
z.object({
recipientId: z.number().describe('The ID of the recipient to create the field for'),
envelopeItemId: z

View File

@ -7,6 +7,7 @@ import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-
import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
import { prisma } from '@documenso/prisma';
import { ZGenericSuccessResponse } from '../../schema';
import { authenticatedProcedure } from '../../trpc';
import {
ZDeleteEnvelopeFieldRequestSchema,
@ -115,4 +116,6 @@ export const deleteEnvelopeFieldRoute = authenticatedProcedure
return deletedField;
});
return ZGenericSuccessResponse;
});

View File

@ -1,5 +1,6 @@
import { z } from 'zod';
import { ZSuccessResponseSchema } from '../../schema';
import type { TrpcRouteMeta } from '../../trpc';
export const deleteEnvelopeFieldMeta: TrpcRouteMeta = {
@ -16,7 +17,7 @@ export const ZDeleteEnvelopeFieldRequestSchema = z.object({
fieldId: z.number(),
});
export const ZDeleteEnvelopeFieldResponseSchema = z.void();
export const ZDeleteEnvelopeFieldResponseSchema = ZSuccessResponseSchema;
export type TDeleteEnvelopeFieldRequest = z.infer<typeof ZDeleteEnvelopeFieldRequestSchema>;
export type TDeleteEnvelopeFieldResponse = z.infer<typeof ZDeleteEnvelopeFieldResponseSchema>;

View File

@ -8,7 +8,7 @@ import {
ZFieldPageNumberSchema,
ZFieldSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { ZEnvelopeFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import type { TrpcRouteMeta } from '../../trpc';
@ -22,7 +22,7 @@ export const updateEnvelopeFieldsMeta: TrpcRouteMeta = {
},
};
const ZUpdateFieldSchema = ZFieldAndMetaSchema.and(
const ZUpdateFieldSchema = ZEnvelopeFieldAndMetaSchema.and(
z.object({
id: z.number().describe('The ID of the field to update.'),
envelopeItemId: z

View File

@ -1,5 +1,6 @@
import { deleteEnvelopeRecipient } from '@documenso/lib/server-only/recipient/delete-envelope-recipient';
import { ZGenericSuccessResponse } from '../../schema';
import { authenticatedProcedure } from '../../trpc';
import {
ZDeleteEnvelopeRecipientRequestSchema,
@ -27,4 +28,6 @@ export const deleteEnvelopeRecipientRoute = authenticatedProcedure
recipientId,
requestMetadata: metadata,
});
return ZGenericSuccessResponse;
});

View File

@ -1,5 +1,6 @@
import { z } from 'zod';
import { ZSuccessResponseSchema } from '../../schema';
import type { TrpcRouteMeta } from '../../trpc';
export const deleteEnvelopeRecipientMeta: TrpcRouteMeta = {
@ -16,7 +17,7 @@ export const ZDeleteEnvelopeRecipientRequestSchema = z.object({
recipientId: z.number(),
});
export const ZDeleteEnvelopeRecipientResponseSchema = z.void();
export const ZDeleteEnvelopeRecipientResponseSchema = ZSuccessResponseSchema;
export type TDeleteEnvelopeRecipientRequest = z.infer<typeof ZDeleteEnvelopeRecipientRequestSchema>;
export type TDeleteEnvelopeRecipientResponse = z.infer<

View File

@ -1,4 +1,5 @@
import { resendDocument } from '@documenso/lib/server-only/document/resend-document';
import { formatSigningLink } from '@documenso/lib/utils/recipients';
import { authenticatedProcedure } from '../trpc';
import {
@ -22,7 +23,7 @@ export const redistributeEnvelopeRoute = authenticatedProcedure
},
});
await resendDocument({
const envelope = await resendDocument({
userId: ctx.user.id,
teamId,
id: {
@ -32,4 +33,18 @@ export const redistributeEnvelopeRoute = authenticatedProcedure
recipients,
requestMetadata: ctx.metadata,
});
return {
success: true,
id: envelope.id,
recipients: envelope.recipients.map((recipient) => ({
id: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: formatSigningLink(recipient.token),
})),
};
});

View File

@ -1,6 +1,8 @@
import { z } from 'zod';
import { ZSuccessResponseSchema } from '../schema';
import type { TrpcRouteMeta } from '../trpc';
import { ZRecipientWithSigningUrlSchema } from './schema';
export const redistributeEnvelopeMeta: TrpcRouteMeta = {
openapi: {
@ -21,7 +23,10 @@ export const ZRedistributeEnvelopeRequestSchema = z.object({
.describe('The IDs of the recipients to redistribute the envelope to.'),
});
export const ZRedistributeEnvelopeResponseSchema = z.void();
export const ZRedistributeEnvelopeResponseSchema = ZSuccessResponseSchema.extend({
id: z.string().describe('The ID of the envelope that was redistributed.'),
recipients: ZRecipientWithSigningUrlSchema.array(),
});
export type TRedistributeEnvelopeRequest = z.infer<typeof ZRedistributeEnvelopeRequestSchema>;
export type TRedistributeEnvelopeResponse = z.infer<typeof ZRedistributeEnvelopeResponseSchema>;

View File

@ -8,6 +8,7 @@ import { createEnvelopeItemsRoute } from './create-envelope-items';
import { deleteEnvelopeRoute } from './delete-envelope';
import { deleteEnvelopeItemRoute } from './delete-envelope-item';
import { distributeEnvelopeRoute } from './distribute-envelope';
import { downloadEnvelopeItemRoute } from './download-envelope-item';
import { duplicateEnvelopeRoute } from './duplicate-envelope';
import { createEnvelopeFieldsRoute } from './envelope-fields/create-envelope-fields';
import { deleteEnvelopeFieldRoute } from './envelope-fields/delete-envelope-field';
@ -46,6 +47,7 @@ export const envelopeRouter = router({
createMany: createEnvelopeItemsRoute,
updateMany: updateEnvelopeItemsRoute,
delete: deleteEnvelopeItemRoute,
download: downloadEnvelopeItemRoute,
},
recipient: {
get: getEnvelopeRecipientRoute,

View File

@ -0,0 +1,16 @@
import { z } from 'zod';
import RecipientSchema from '@documenso/prisma/generated/zod/modelSchema/RecipientSchema';
// Common schemas between envelope routes.
export const ZRecipientWithSigningUrlSchema = RecipientSchema.pick({
id: true,
name: true,
email: true,
token: true,
role: true,
signingOrder: true,
}).extend({
signingUrl: z.string().describe('The URL which the recipient uses to sign the document.'),
});

View File

@ -6,6 +6,7 @@ import { sendDocument } from '@documenso/lib/server-only/document/send-document'
import { getEnvelopeById } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { formatSigningLink } from '@documenso/lib/utils/recipients';
import { authenticatedProcedure } from '../trpc';
import {
@ -26,7 +27,7 @@ export const useEnvelopeRoute = authenticatedProcedure
const {
envelopeId,
externalId,
recipients,
recipients = [],
distributeDocument,
customDocumentData = [],
folderId,
@ -166,5 +167,14 @@ export const useEnvelopeRoute = authenticatedProcedure
return {
id: createdEnvelope.id,
recipients: createdEnvelope.recipients.map((recipient) => ({
id: recipient.id,
name: recipient.name,
email: recipient.email,
token: recipient.token,
role: recipient.role,
signingOrder: recipient.signingOrder,
signingUrl: formatSigningLink(recipient.token),
})),
};
});

View File

@ -19,6 +19,7 @@ import { ZFieldMetaPrefillFieldsSchema } from '@documenso/lib/types/field-meta';
import { zodFormData } from '../../utils/zod-form-data';
import type { TrpcRouteMeta } from '../trpc';
import { ZRecipientWithSigningUrlSchema } from './schema';
export const useEnvelopeMeta: TrpcRouteMeta = {
openapi: {
@ -44,7 +45,8 @@ export const ZUseEnvelopePayloadSchema = z.object({
signingOrder: z.number().optional(),
}),
)
.describe('The information of the recipients to create the document with.'),
.describe('The information of the recipients to create the document with.')
.optional(),
distributeDocument: z
.boolean()
.describe('Whether to create the document as pending and distribute it to recipients.')
@ -114,6 +116,7 @@ export const ZUseEnvelopeRequestSchema = zodFormData({
export const ZUseEnvelopeResponseSchema = z.object({
id: z.string().describe('The ID of the created envelope.'),
recipients: ZRecipientWithSigningUrlSchema.array(),
});
export type TUseEnvelopePayload = z.infer<typeof ZUseEnvelopePayloadSchema>;

View File

@ -10,7 +10,7 @@ import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-field
import { signFieldWithToken } from '@documenso/lib/server-only/field/sign-field-with-token';
import { updateEnvelopeFields } from '@documenso/lib/server-only/field/update-envelope-fields';
import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../document-router/schema';
import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../schema';
import { authenticatedProcedure, procedure, router } from '../trpc';
import {
ZCreateDocumentFieldRequestSchema,

View File

@ -7,6 +7,7 @@ import { getFolderBreadcrumbs } from '@documenso/lib/server-only/folder/get-fold
import { getFolderById } from '@documenso/lib/server-only/folder/get-folder-by-id';
import { updateFolder } from '@documenso/lib/server-only/folder/update-folder';
import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../schema';
import { authenticatedProcedure, router } from '../trpc';
import {
ZCreateFolderRequestSchema,
@ -16,10 +17,8 @@ import {
ZFindFoldersInternalResponseSchema,
ZFindFoldersRequestSchema,
ZFindFoldersResponseSchema,
ZGenericSuccessResponse,
ZGetFoldersResponseSchema,
ZGetFoldersSchema,
ZSuccessResponseSchema,
ZUpdateFolderRequestSchema,
ZUpdateFolderResponseSchema,
} from './schema';

View File

@ -5,19 +5,6 @@ import { ZFindResultResponse, ZFindSearchParamsSchema } from '@documenso/lib/typ
import { DocumentVisibility } from '@documenso/prisma/generated/types';
import FolderSchema from '@documenso/prisma/generated/zod/modelSchema/FolderSchema';
/**
* Required for empty responses since we currently can't 201 requests for our openapi setup.
*
* Without this it will throw an error in Speakeasy SDK when it tries to parse an empty response.
*/
export const ZSuccessResponseSchema = z.object({
success: z.boolean(),
});
export const ZGenericSuccessResponse = {
success: true,
} satisfies z.infer<typeof ZSuccessResponseSchema>;
export const ZFolderSchema = FolderSchema.pick({
id: true,
name: true,

View File

@ -9,7 +9,7 @@ import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-
import { setTemplateRecipients } from '@documenso/lib/server-only/recipient/set-template-recipients';
import { updateEnvelopeRecipients } from '@documenso/lib/server-only/recipient/update-envelope-recipients';
import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../document-router/schema';
import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../schema';
import { authenticatedProcedure, procedure, router } from '../trpc';
import { findRecipientSuggestionsRoute } from './find-recipient-suggestions';
import {

View File

@ -0,0 +1,14 @@
import { z } from 'zod';
/**
* Required for empty responses since we currently can't 201 requests for our openapi setup.
*
* Without this it will throw an error in Speakeasy SDK when it tries to parse an empty response.
*/
export const ZSuccessResponseSchema = z.object({
success: z.boolean(),
});
export const ZGenericSuccessResponse = {
success: true,
} satisfies z.infer<typeof ZSuccessResponseSchema>;

View File

@ -28,7 +28,7 @@ import { mapFieldToLegacyField } from '@documenso/lib/utils/fields';
import { mapRecipientToLegacyRecipient } from '@documenso/lib/utils/recipients';
import { mapEnvelopeToTemplateLite } from '@documenso/lib/utils/templates';
import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../document-router/schema';
import { ZGenericSuccessResponse, ZSuccessResponseSchema } from '../schema';
import { authenticatedProcedure, maybeAuthenticatedProcedure, router } from '../trpc';
import {
ZBulkSendTemplateMutationSchema,
@ -177,7 +177,19 @@ export const templateRouter = router({
const { payload, file } = input;
const { title, folderId } = payload;
const {
title,
folderId,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
publicTitle,
publicDescription,
type,
meta,
attachments,
} = payload;
const { id: templateDocumentDataId } = await putNormalizedPdfFileServerSide(file);
@ -194,13 +206,22 @@ export const templateRouter = router({
data: {
type: EnvelopeType.TEMPLATE,
title,
folderId,
envelopeItems: [
{
documentDataId: templateDocumentDataId,
},
],
folderId,
externalId: externalId ?? undefined,
visibility,
globalAccessAuth,
globalActionAuth,
templateType: type,
publicTitle,
publicDescription,
},
meta,
attachments,
requestMetadata: ctx.metadata,
});

View File

@ -79,16 +79,6 @@ export const ZTemplateMetaUpsertSchema = z.object({
allowDictateNextSigner: z.boolean().optional(),
});
export const ZCreateTemplatePayloadSchema = z.object({
title: z.string().min(1).trim(),
folderId: z.string().optional(),
});
export const ZCreateTemplateMutationSchema = zodFormData({
payload: zfd.json(ZCreateTemplatePayloadSchema),
file: zfd.file(),
});
export const ZCreateDocumentFromDirectTemplateRequestSchema = z.object({
directRecipientName: z.string().max(255).optional(),
directRecipientEmail: z.string().email().max(254),
@ -234,6 +224,13 @@ export const ZCreateTemplateResponseSchema = z.object({
id: z.number(),
});
export const ZCreateTemplatePayloadSchema = ZCreateTemplateV2RequestSchema;
export const ZCreateTemplateMutationSchema = zodFormData({
payload: zfd.json(ZCreateTemplatePayloadSchema),
file: zfd.file(),
});
export const ZUpdateTemplateRequestSchema = z.object({
templateId: z.number(),
data: z
@ -280,7 +277,7 @@ export const ZBulkSendTemplateMutationSchema = z.object({
sendImmediately: z.boolean(),
});
export type TCreateTemplatePayloadSchema = z.infer<typeof ZCreateTemplatePayloadSchema>;
export type TCreateTemplatePayloadSchema = z.input<typeof ZCreateTemplatePayloadSchema>;
export type TCreateTemplateMutationSchema = z.infer<typeof ZCreateTemplateMutationSchema>;
export type TDuplicateTemplateMutationSchema = z.infer<typeof ZDuplicateTemplateMutationSchema>;
export type TDeleteTemplateMutationSchema = z.infer<typeof ZDeleteTemplateMutationSchema>;

View File

@ -24,11 +24,17 @@ const getMultipartBody = async (req: Request) => {
const data: Record<string, unknown> = {};
for (const key of formData.keys()) {
const values = formData.getAll(key);
for (const [key, value] of formData.entries()) {
// !: Handles cases where our generated SDKs send key[] syntax for arrays.
const normalizedKey = key.endsWith('[]') ? key.slice(0, -2) : key;
// Return array for multiple values, single value otherwise (matches URL-encoded behavior)
data[key] = values.length > 1 ? values : values[0];
if (data[normalizedKey] === undefined) {
data[normalizedKey] = value;
} else if (Array.isArray(data[normalizedKey])) {
data[normalizedKey].push(value);
} else {
data[normalizedKey] = [data[normalizedKey], value];
}
}
return data;

View File

@ -1,5 +1,6 @@
import React, { forwardRef } from 'react';
import { useLingui } from '@lingui/react/macro';
import { TeamMemberRole } from '@prisma/client';
import type { SelectProps } from '@radix-ui/react-select';
import { InfoIcon } from 'lucide-react';
@ -27,6 +28,8 @@ export const DocumentVisibilitySelect = forwardRef<HTMLButtonElement, DocumentVi
{ currentTeamMemberRole, isTeamSettings = false, disabled, canUpdateVisibility, ...props },
ref,
) => {
const { t } = useLingui();
const isAdmin = currentTeamMemberRole === TeamMemberRole.ADMIN;
const isManager = currentTeamMemberRole === TeamMemberRole.MANAGER;
const canEdit = isTeamSettings || canUpdateVisibility;
@ -34,7 +37,7 @@ export const DocumentVisibilitySelect = forwardRef<HTMLButtonElement, DocumentVi
return (
<Select {...props} disabled={!canEdit || disabled}>
<SelectTrigger ref={ref} className="bg-background text-muted-foreground">
<SelectValue data-testid="documentVisibilitySelectValue" placeholder="Everyone" />
<SelectValue data-testid="documentVisibilitySelectValue" placeholder={t`Everyone`} />
</SelectTrigger>
<SelectContent position="popper">

View File

@ -419,7 +419,7 @@ export const AddSettingsFormPartial = ({
void handleAutoSave();
}}
className="bg-background w-full"
emptySelectionPlaceholder="Select signature types"
emptySelectionPlaceholder={t`Select signature types`}
/>
</FormControl>

View File

@ -1,6 +1,4 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Trans, useLingui } from '@lingui/react/macro';
import { validateFields as validateDateFields } from '@documenso/lib/advanced-fields-validation/validate-fields';
import { type TDateFieldMeta as DateFieldMeta } from '@documenso/lib/types/field-meta';
@ -25,7 +23,7 @@ export const DateFieldAdvancedSettings = ({
handleFieldChange,
handleErrors,
}: DateFieldAdvancedSettingsProps) => {
const { _ } = useLingui();
const { t } = useLingui();
// const handleInput = (field: keyof DateFieldMeta, value: string | boolean) => {
// if (field === 'fontSize') {
@ -67,7 +65,7 @@ export const DateFieldAdvancedSettings = ({
id="fontSize"
type="number"
className="bg-background mt-2"
placeholder={_(msg`Field font size`)}
placeholder={t`Field font size`}
value={fieldState.fontSize}
onChange={(e) => handleInput('fontSize', e.target.value)}
min={8}
@ -85,7 +83,7 @@ export const DateFieldAdvancedSettings = ({
onValueChange={(value) => handleInput('textAlign', value)}
>
<SelectTrigger className="bg-background mt-2">
<SelectValue placeholder="Select text align" />
<SelectValue placeholder={t`Select text align`} />
</SelectTrigger>
<SelectContent>

View File

@ -1,6 +1,4 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Trans, useLingui } from '@lingui/react/macro';
import { validateFields as validateEmailFields } from '@documenso/lib/advanced-fields-validation/validate-fields';
import { type TEmailFieldMeta as EmailFieldMeta } from '@documenso/lib/types/field-meta';
@ -25,7 +23,7 @@ export const EmailFieldAdvancedSettings = ({
handleFieldChange,
handleErrors,
}: EmailFieldAdvancedSettingsProps) => {
const { _ } = useLingui();
const { t } = useLingui();
const handleInput = (field: keyof EmailFieldMeta, value: string | boolean) => {
const fontSize = field === 'fontSize' ? Number(value) : Number(fieldState.fontSize ?? 14);
@ -49,7 +47,7 @@ export const EmailFieldAdvancedSettings = ({
id="fontSize"
type="number"
className="bg-background mt-2"
placeholder={_(msg`Field font size`)}
placeholder={t`Field font size`}
value={fieldState.fontSize}
onChange={(e) => handleInput('fontSize', e.target.value)}
min={8}
@ -67,7 +65,7 @@ export const EmailFieldAdvancedSettings = ({
onValueChange={(value) => handleInput('textAlign', value)}
>
<SelectTrigger className="bg-background mt-2">
<SelectValue placeholder="Select text align" />
<SelectValue placeholder={t`Select text align`} />
</SelectTrigger>
<SelectContent>

View File

@ -1,6 +1,4 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Trans, useLingui } from '@lingui/react/macro';
import { validateFields as validateInitialsFields } from '@documenso/lib/advanced-fields-validation/validate-fields';
import { type TInitialsFieldMeta as InitialsFieldMeta } from '@documenso/lib/types/field-meta';
@ -20,7 +18,7 @@ export const InitialsFieldAdvancedSettings = ({
handleFieldChange,
handleErrors,
}: InitialsFieldAdvancedSettingsProps) => {
const { _ } = useLingui();
const { t } = useLingui();
const handleInput = (field: keyof InitialsFieldMeta, value: string | boolean) => {
const fontSize = field === 'fontSize' ? Number(value) : Number(fieldState.fontSize ?? 14);
@ -44,7 +42,7 @@ export const InitialsFieldAdvancedSettings = ({
id="fontSize"
type="number"
className="bg-background mt-2"
placeholder={_(msg`Field font size`)}
placeholder={t`Field font size`}
value={fieldState.fontSize}
onChange={(e) => handleInput('fontSize', e.target.value)}
min={8}
@ -62,7 +60,7 @@ export const InitialsFieldAdvancedSettings = ({
onValueChange={(value) => handleInput('textAlign', value)}
>
<SelectTrigger className="bg-background mt-2">
<SelectValue placeholder="Select text align" />
<SelectValue placeholder={t`Select text align`} />
</SelectTrigger>
<SelectContent>

View File

@ -1,6 +1,4 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Trans, useLingui } from '@lingui/react/macro';
import { validateFields as validateNameFields } from '@documenso/lib/advanced-fields-validation/validate-fields';
import { type TNameFieldMeta as NameFieldMeta } from '@documenso/lib/types/field-meta';
@ -25,7 +23,7 @@ export const NameFieldAdvancedSettings = ({
handleFieldChange,
handleErrors,
}: NameFieldAdvancedSettingsProps) => {
const { _ } = useLingui();
const { t } = useLingui();
const handleInput = (field: keyof NameFieldMeta, value: string | boolean) => {
const fontSize = field === 'fontSize' ? Number(value) : Number(fieldState.fontSize ?? 14);
@ -49,7 +47,7 @@ export const NameFieldAdvancedSettings = ({
id="fontSize"
type="number"
className="bg-background mt-2"
placeholder={_(msg`Field font size`)}
placeholder={t`Field font size`}
value={fieldState.fontSize}
onChange={(e) => handleInput('fontSize', e.target.value)}
min={8}
@ -67,7 +65,7 @@ export const NameFieldAdvancedSettings = ({
onValueChange={(value) => handleInput('textAlign', value)}
>
<SelectTrigger className="bg-background mt-2">
<SelectValue placeholder="Select text align" />
<SelectValue placeholder={t`Select text align`} />
</SelectTrigger>
<SelectContent>

View File

@ -1,8 +1,6 @@
import { useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Trans, useLingui } from '@lingui/react/macro';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
@ -32,7 +30,7 @@ export const NumberFieldAdvancedSettings = ({
handleFieldChange,
handleErrors,
}: NumberFieldAdvancedSettingsProps) => {
const { _ } = useLingui();
const { t } = useLingui();
const [showValidation, setShowValidation] = useState(false);
@ -68,7 +66,7 @@ export const NumberFieldAdvancedSettings = ({
<Input
id="label"
className="bg-background mt-2"
placeholder={_(msg`Label`)}
placeholder={t`Label`}
value={fieldState.label}
onChange={(e) => handleFieldChange('label', e.target.value)}
/>
@ -80,7 +78,7 @@ export const NumberFieldAdvancedSettings = ({
<Input
id="placeholder"
className="bg-background mt-2"
placeholder={_(msg`Placeholder`)}
placeholder={t`Placeholder`}
value={fieldState.placeholder}
onChange={(e) => handleFieldChange('placeholder', e.target.value)}
/>
@ -92,7 +90,7 @@ export const NumberFieldAdvancedSettings = ({
<Input
id="value"
className="bg-background mt-2"
placeholder={_(msg`Value`)}
placeholder={t`Value`}
value={fieldState.value}
onChange={(e) => handleInput('value', e.target.value)}
/>
@ -106,7 +104,7 @@ export const NumberFieldAdvancedSettings = ({
onValueChange={(val) => handleInput('numberFormat', val)}
>
<SelectTrigger className="text-muted-foreground bg-background mt-2 w-full">
<SelectValue placeholder={_(msg`Field format`)} />
<SelectValue placeholder={t`Field format`} />
</SelectTrigger>
<SelectContent position="popper">
{numberFormatValues.map((item, index) => (
@ -126,7 +124,7 @@ export const NumberFieldAdvancedSettings = ({
id="fontSize"
type="number"
className="bg-background mt-2"
placeholder={_(msg`Field font size`)}
placeholder={t`Field font size`}
value={fieldState.fontSize}
onChange={(e) => handleInput('fontSize', e.target.value)}
min={8}
@ -144,7 +142,7 @@ export const NumberFieldAdvancedSettings = ({
onValueChange={(value) => handleInput('textAlign', value)}
>
<SelectTrigger className="bg-background mt-2">
<SelectValue placeholder="Select text align" />
<SelectValue placeholder={t`Select text align`} />
</SelectTrigger>
<SelectContent>
@ -198,7 +196,7 @@ export const NumberFieldAdvancedSettings = ({
<Input
id="minValue"
className="bg-background mt-2"
placeholder="E.g. 0"
placeholder={t`E.g. 0`}
value={fieldState.minValue ?? ''}
onChange={(e) => handleInput('minValue', e.target.value)}
/>
@ -210,7 +208,7 @@ export const NumberFieldAdvancedSettings = ({
<Input
id="maxValue"
className="bg-background mt-2"
placeholder="E.g. 100"
placeholder={t`E.g. 100`}
value={fieldState.maxValue ?? ''}
onChange={(e) => handleInput('maxValue', e.target.value)}
/>

View File

@ -1,6 +1,4 @@
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { Trans, useLingui } from '@lingui/react/macro';
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
import { type TTextFieldMeta as TextFieldMeta } from '@documenso/lib/types/field-meta';
@ -27,7 +25,7 @@ export const TextFieldAdvancedSettings = ({
handleFieldChange,
handleErrors,
}: TextFieldAdvancedSettingsProps) => {
const { _ } = useLingui();
const { t } = useLingui();
const handleInput = (field: keyof TextFieldMeta, value: string | boolean) => {
const text = field === 'text' ? String(value) : (fieldState.text ?? '');
@ -58,7 +56,7 @@ export const TextFieldAdvancedSettings = ({
<Input
id="label"
className="bg-background mt-2"
placeholder={_(msg`Field label`)}
placeholder={t`Field label`}
value={fieldState.label}
onChange={(e) => handleFieldChange('label', e.target.value)}
/>
@ -70,7 +68,7 @@ export const TextFieldAdvancedSettings = ({
<Input
id="placeholder"
className="bg-background mt-2"
placeholder={_(msg`Field placeholder`)}
placeholder={t`Field placeholder`}
value={fieldState.placeholder}
onChange={(e) => handleFieldChange('placeholder', e.target.value)}
/>
@ -83,7 +81,7 @@ export const TextFieldAdvancedSettings = ({
<Textarea
id="text"
className="bg-background mt-2"
placeholder={_(msg`Add text to the field`)}
placeholder={t`Add text to the field`}
value={fieldState.text}
onChange={(e) => handleInput('text', e.target.value)}
/>
@ -98,7 +96,7 @@ export const TextFieldAdvancedSettings = ({
type="number"
min={0}
className="bg-background mt-2"
placeholder={_(msg`Field character limit`)}
placeholder={t`Field character limit`}
value={fieldState.characterLimit}
onChange={(e) => handleInput('characterLimit', e.target.value)}
/>
@ -112,7 +110,7 @@ export const TextFieldAdvancedSettings = ({
id="fontSize"
type="number"
className="bg-background mt-2"
placeholder={_(msg`Field font size`)}
placeholder={t`Field font size`}
value={fieldState.fontSize}
onChange={(e) => handleInput('fontSize', e.target.value)}
min={8}
@ -136,7 +134,7 @@ export const TextFieldAdvancedSettings = ({
}}
>
<SelectTrigger className="bg-background mt-2">
<SelectValue placeholder="Select text align" />
<SelectValue placeholder={t`Select text align`} />
</SelectTrigger>
<SelectContent>

View File

@ -1,6 +1,7 @@
import * as React from 'react';
import type { MessageDescriptor } from '@lingui/core';
import { t } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { AnimatePresence } from 'framer-motion';
@ -43,7 +44,7 @@ type MultiSelectComboboxProps<T = OptionValue> = {
* - Clear all button
*/
export function MultiSelectCombobox<T = OptionValue>({
emptySelectionPlaceholder = 'Select values...',
emptySelectionPlaceholder = t`Select values...`,
enableClearAllButton,
enableSearch = true,
className,

View File

@ -1,4 +1,4 @@
import { useCallback, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
@ -34,8 +34,8 @@ export const RecipientSelector = ({
const { _ } = useLingui();
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
const recipientsByRole = useCallback(() => {
const recipientsByRole: Record<RecipientRole, Recipient[]> = {
const recipientsByRole = useMemo(() => {
const recipientsWithRole: Record<RecipientRole, Recipient[]> = {
CC: [],
VIEWER: [],
SIGNER: [],
@ -44,14 +44,14 @@ export const RecipientSelector = ({
};
recipients.forEach((recipient) => {
recipientsByRole[recipient.role].push(recipient);
recipientsWithRole[recipient.role].push(recipient);
});
return recipientsByRole;
return recipientsWithRole;
}, [recipients]);
const recipientsByRoleToDisplay = useCallback(() => {
return Object.entries(recipientsByRole())
const recipientsByRoleToDisplay = useMemo(() => {
return Object.entries(recipientsByRole)
.filter(
([role]) =>
role !== RecipientRole.CC &&
@ -71,6 +71,28 @@ export const RecipientSelector = ({
);
}, [recipientsByRole]);
const getRecipientLabel = useCallback(
(recipient: Recipient) => {
if (recipient.name && recipient.email) {
return `${recipient.name} (${recipient.email})`;
}
if (recipient.name) {
return recipient.name;
}
if (recipient.email) {
return recipient.email;
}
// Since objects are basically pointers we can use `indexOf` rather than `findIndex`
const index = recipients.indexOf(recipient);
return `Recipient ${index + 1}`;
},
[recipients, selectedRecipient],
);
return (
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
<PopoverTrigger asChild>
@ -89,16 +111,12 @@ export const RecipientSelector = ({
className,
)}
>
{selectedRecipient?.email && (
{selectedRecipient && (
<span className="flex-1 truncate text-left">
{selectedRecipient?.name} ({selectedRecipient?.email})
{getRecipientLabel(selectedRecipient)}
</span>
)}
{!selectedRecipient?.email && (
<span className="flex-1 truncate text-left">{selectedRecipient?.email}</span>
)}
<ChevronsUpDown className="ml-2 h-4 w-4" />
</Button>
</PopoverTrigger>
@ -113,7 +131,7 @@ export const RecipientSelector = ({
</span>
</CommandEmpty>
{recipientsByRoleToDisplay().map(([role, roleRecipients], roleIndex) => (
{recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => (
<CommandGroup key={roleIndex}>
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
{_(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)}
@ -154,13 +172,7 @@ export const RecipientSelector = ({
'text-foreground/80': recipient.id === selectedRecipient?.id,
})}
>
{recipient.name && (
<span title={`${recipient.name} (${recipient.email})`}>
{recipient.name} ({recipient.email})
</span>
)}
{!recipient.name && <span title={recipient.email}>{recipient.email}</span>}
{getRecipientLabel(recipient)}
</span>
<div className="ml-auto flex items-center justify-center">

View File

@ -1,5 +1,7 @@
import { useState } from 'react';
import { useLingui } from '@lingui/react/macro';
import { cn } from '../../lib/utils';
export type SignaturePadTypeProps = {
@ -9,6 +11,7 @@ export type SignaturePadTypeProps = {
};
export const SignaturePadType = ({ className, value, onChange }: SignaturePadTypeProps) => {
const { t } = useLingui();
// Colors don't actually work for text.
const [selectedColor, setSelectedColor] = useState('black');
@ -16,7 +19,7 @@ export const SignaturePadType = ({ className, value, onChange }: SignaturePadTyp
<div className={cn('flex h-full w-full items-center justify-center', className)}>
<input
data-testid="signature-pad-type-input"
placeholder="Type your signature"
placeholder={t`Type your signature`}
className="font-signature w-full bg-transparent px-4 text-center text-7xl text-black placeholder:text-4xl focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 dark:text-white"
// style={{ color: selectedColor }}
value={value}

View File

@ -147,21 +147,21 @@ export const SignaturePad = ({
{drawSignatureEnabled && (
<TabsTrigger value="draw">
<SignatureIcon className="mr-2 size-4" />
<Trans>Draw</Trans>
<Trans context="Draw signature">Draw</Trans>
</TabsTrigger>
)}
{typedSignatureEnabled && (
<TabsTrigger value="text">
<KeyboardIcon className="mr-2 size-4" />
<Trans>Type</Trans>
<Trans context="Type signature">Type</Trans>
</TabsTrigger>
)}
{uploadSignatureEnabled && (
<TabsTrigger value="image">
<UploadCloudIcon className="mr-2 size-4" />
<Trans>Upload</Trans>
<Trans context="Upload signature">Upload</Trans>
</TabsTrigger>
)}
</TabsList>

View File

@ -1,8 +1,7 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { Trans, useLingui } from '@lingui/react/macro';
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
import { DocumentDistributionMethod, type Field, type Recipient } from '@prisma/client';
import { InfoIcon } from 'lucide-react';
@ -425,7 +424,7 @@ export const AddTemplateSettingsFormPartial = ({
void handleAutoSave();
}}
className="bg-background w-full"
emptySelectionPlaceholder="Select signature types"
emptySelectionPlaceholder={t`Select signature types`}
/>
</FormControl>