mirror of
https://github.com/documenso/documenso.git
synced 2025-11-18 10:42:01 +10:00
Merge branch 'feat/auto-placing-fields' of github.com:documenso/documenso into feat/auto-placing-fields
This commit is contained in:
@ -1,12 +1,14 @@
|
||||
import type { DocumentData } from '@prisma/client';
|
||||
import type { EnvelopeItem } from '@prisma/client';
|
||||
|
||||
import { getFile } from '../universal/upload/get-file';
|
||||
import { getEnvelopeItemPdfUrl } from '../utils/envelope-download';
|
||||
import { downloadFile } from './download-file';
|
||||
|
||||
type DocumentVersion = 'original' | 'signed';
|
||||
|
||||
type DownloadPDFProps = {
|
||||
documentData: DocumentData;
|
||||
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
|
||||
token: string | undefined;
|
||||
|
||||
fileName?: string;
|
||||
/**
|
||||
* Specifies which version of the document to download.
|
||||
@ -17,18 +19,19 @@ type DownloadPDFProps = {
|
||||
};
|
||||
|
||||
export const downloadPDF = async ({
|
||||
documentData,
|
||||
envelopeItem,
|
||||
token,
|
||||
fileName,
|
||||
version = 'signed',
|
||||
}: DownloadPDFProps) => {
|
||||
const bytes = await getFile({
|
||||
type: documentData.type,
|
||||
data: version === 'signed' ? documentData.data : documentData.initialData,
|
||||
const downloadUrl = getEnvelopeItemPdfUrl({
|
||||
type: 'download',
|
||||
envelopeItem: envelopeItem,
|
||||
token,
|
||||
version,
|
||||
});
|
||||
|
||||
const blob = new Blob([bytes], {
|
||||
type: 'application/pdf',
|
||||
});
|
||||
const blob = await fetch(downloadUrl).then(async (res) => await res.blob());
|
||||
|
||||
const baseTitle = (fileName ?? 'document').replace(/\.pdf$/, '');
|
||||
const suffix = version === 'signed' ? '_signed.pdf' : '.pdf';
|
||||
|
||||
@ -165,10 +165,7 @@ export const useEditorFields = ({
|
||||
const index = localFields.findIndex((field) => field.formId === formId);
|
||||
|
||||
if (index !== -1) {
|
||||
update(index, {
|
||||
...localFields[index],
|
||||
id,
|
||||
});
|
||||
form.setValue(`fields.${index}.id`, id);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import Konva from 'konva';
|
||||
import type { RenderParameters } from 'pdfjs-dist/types/src/display/api';
|
||||
@ -25,6 +25,8 @@ export function usePageRenderer(renderFunction: RenderFunction) {
|
||||
const stage = useRef<Konva.Stage | null>(null);
|
||||
const pageLayer = useRef<Konva.Layer | null>(null);
|
||||
|
||||
const [renderError, setRenderError] = useState<boolean>(false);
|
||||
|
||||
/**
|
||||
* The raw viewport with no scaling. Basically the actual PDF size.
|
||||
*/
|
||||
@ -122,5 +124,7 @@ export function usePageRenderer(renderFunction: RenderFunction) {
|
||||
unscaledViewport,
|
||||
scaledViewport,
|
||||
pageContext,
|
||||
renderError,
|
||||
setRenderError,
|
||||
};
|
||||
}
|
||||
|
||||
@ -46,6 +46,7 @@ type EnvelopeEditorProviderValue = {
|
||||
setLocalEnvelope: (localEnvelope: Partial<TEnvelope>) => void;
|
||||
|
||||
updateEnvelope: (envelopeUpdates: UpdateEnvelopePayload) => void;
|
||||
updateEnvelopeAsync: (envelopeUpdates: UpdateEnvelopePayload) => Promise<void>;
|
||||
setRecipientsDebounced: (recipients: TSetEnvelopeRecipientsRequest['recipients']) => void;
|
||||
setRecipientsAsync: (recipients: TSetEnvelopeRecipientsRequest['recipients']) => Promise<void>;
|
||||
|
||||
@ -66,8 +67,6 @@ type EnvelopeEditorProviderValue = {
|
||||
};
|
||||
|
||||
syncEnvelope: () => Promise<void>;
|
||||
// refetchEnvelope: () => Promise<void>;
|
||||
// updateEnvelope: (envelope: TEnvelope) => Promise<void>;
|
||||
};
|
||||
|
||||
interface EnvelopeEditorProviderProps {
|
||||
@ -151,7 +150,7 @@ export const EnvelopeEditorProvider = ({
|
||||
});
|
||||
|
||||
const envelopeRecipientSetMutationQuery = trpc.envelope.recipient.set.useMutation({
|
||||
onSuccess: ({ recipients }) => {
|
||||
onSuccess: ({ data: recipients }) => {
|
||||
setEnvelope((prev) => ({
|
||||
...prev,
|
||||
recipients,
|
||||
@ -197,7 +196,7 @@ export const EnvelopeEditorProvider = ({
|
||||
});
|
||||
|
||||
// Insert the IDs into the local fields.
|
||||
envelopeFields.fields.forEach((field) => {
|
||||
envelopeFields.data.forEach((field) => {
|
||||
const localField = localFields.find((localField) => localField.formId === field.formId);
|
||||
|
||||
if (localField && !localField.id) {
|
||||
@ -215,7 +214,6 @@ export const EnvelopeEditorProvider = ({
|
||||
} = useEnvelopeAutosave(async (envelopeUpdates: UpdateEnvelopePayload) => {
|
||||
await envelopeUpdateMutationQuery.mutateAsync({
|
||||
envelopeId: envelope.id,
|
||||
envelopeType: envelope.type,
|
||||
data: envelopeUpdates.data,
|
||||
meta: envelopeUpdates.meta,
|
||||
});
|
||||
@ -237,6 +235,13 @@ export const EnvelopeEditorProvider = ({
|
||||
setEnvelopeDebounced(envelopeUpdates);
|
||||
};
|
||||
|
||||
const updateEnvelopeAsync = async (envelopeUpdates: UpdateEnvelopePayload) => {
|
||||
await envelopeUpdateMutationQuery.mutateAsync({
|
||||
envelopeId: envelope.id,
|
||||
...envelopeUpdates,
|
||||
});
|
||||
};
|
||||
|
||||
const getRecipientColorKey = useCallback(
|
||||
(recipientId: number) => {
|
||||
const recipientIndex = envelope.recipients.findIndex(
|
||||
@ -324,6 +329,7 @@ export const EnvelopeEditorProvider = ({
|
||||
setLocalEnvelope,
|
||||
getRecipientColorKey,
|
||||
updateEnvelope,
|
||||
updateEnvelopeAsync,
|
||||
setRecipientsDebounced,
|
||||
setRecipientsAsync,
|
||||
editorFields,
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import type { DocumentData } from '@prisma/client';
|
||||
import type { Field, Recipient } from '@prisma/client';
|
||||
|
||||
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
|
||||
import { AVAILABLE_RECIPIENT_COLORS } from '@documenso/ui/lib/recipient-colors';
|
||||
|
||||
import type { TEnvelope } from '../../types/envelope';
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
import type { FieldRenderMode } from '../../universal/field-renderer/render-field';
|
||||
import { getEnvelopeItemPdfUrl } from '../../utils/envelope-download';
|
||||
|
||||
type FileData =
|
||||
| {
|
||||
@ -18,19 +19,31 @@ type FileData =
|
||||
status: 'loaded';
|
||||
};
|
||||
|
||||
type EnvelopeRenderOverrideSettings = {
|
||||
mode?: FieldRenderMode;
|
||||
showRecipientTooltip?: boolean;
|
||||
showRecipientSigningStatus?: boolean;
|
||||
};
|
||||
|
||||
type EnvelopeRenderItem = TEnvelope['envelopeItems'][number];
|
||||
|
||||
type EnvelopeRenderProviderValue = {
|
||||
getPdfBuffer: (documentDataId: string) => FileData | null;
|
||||
getPdfBuffer: (envelopeItemId: string) => FileData | null;
|
||||
envelopeItems: EnvelopeRenderItem[];
|
||||
currentEnvelopeItem: EnvelopeRenderItem | null;
|
||||
setCurrentEnvelopeItem: (envelopeItemId: string) => void;
|
||||
fields: TEnvelope['fields'];
|
||||
fields: Field[];
|
||||
recipients: Pick<Recipient, 'id' | 'name' | 'email' | 'signingStatus'>[];
|
||||
getRecipientColorKey: (recipientId: number) => TRecipientColor;
|
||||
|
||||
renderError: boolean;
|
||||
setRenderError: (renderError: boolean) => void;
|
||||
overrideSettings?: EnvelopeRenderOverrideSettings;
|
||||
};
|
||||
|
||||
interface EnvelopeRenderProviderProps {
|
||||
children: React.ReactNode;
|
||||
|
||||
envelope: Pick<TEnvelope, 'envelopeItems'>;
|
||||
|
||||
/**
|
||||
@ -38,14 +51,27 @@ interface EnvelopeRenderProviderProps {
|
||||
*
|
||||
* Only pass if the CustomRenderer you are passing in wants fields.
|
||||
*/
|
||||
fields?: TEnvelope['fields'];
|
||||
fields?: Field[];
|
||||
|
||||
/**
|
||||
* Optional recipient IDs used to determine the color of the fields.
|
||||
* Optional recipient used to determine the color of the fields and hover
|
||||
* previews.
|
||||
*
|
||||
* Only required for generic page renderers.
|
||||
*/
|
||||
recipientIds?: number[];
|
||||
recipients?: Pick<Recipient, 'id' | 'name' | 'email' | 'signingStatus'>[];
|
||||
|
||||
/**
|
||||
* The token to access the envelope.
|
||||
*
|
||||
* If not provided, it will be assumed that the current user can access the document.
|
||||
*/
|
||||
token: string | undefined;
|
||||
|
||||
/**
|
||||
* Custom override settings for generic page renderers.
|
||||
*/
|
||||
overrideSettings?: EnvelopeRenderOverrideSettings;
|
||||
}
|
||||
|
||||
const EnvelopeRenderContext = createContext<EnvelopeRenderProviderValue | null>(null);
|
||||
@ -67,39 +93,51 @@ export const EnvelopeRenderProvider = ({
|
||||
children,
|
||||
envelope,
|
||||
fields,
|
||||
recipientIds = [],
|
||||
token,
|
||||
recipients = [],
|
||||
overrideSettings,
|
||||
}: EnvelopeRenderProviderProps) => {
|
||||
// Indexed by documentDataId.
|
||||
const [files, setFiles] = useState<Record<string, FileData>>({});
|
||||
|
||||
const [currentItem, setItem] = useState<EnvelopeRenderItem | null>(null);
|
||||
|
||||
const [renderError, setRenderError] = useState<boolean>(false);
|
||||
|
||||
const envelopeItems = useMemo(
|
||||
() => envelope.envelopeItems.sort((a, b) => a.order - b.order),
|
||||
[envelope.envelopeItems],
|
||||
);
|
||||
|
||||
const loadEnvelopeItemPdfFile = async (documentData: DocumentData) => {
|
||||
if (files[documentData.id]?.status === 'loading') {
|
||||
const loadEnvelopeItemPdfFile = async (envelopeItem: EnvelopeRenderItem) => {
|
||||
if (files[envelopeItem.id]?.status === 'loading') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!files[documentData.id]) {
|
||||
if (!files[envelopeItem.id]) {
|
||||
setFiles((prev) => ({
|
||||
...prev,
|
||||
[documentData.id]: {
|
||||
[envelopeItem.id]: {
|
||||
status: 'loading',
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
try {
|
||||
const file = await getFile(documentData);
|
||||
const downloadUrl = getEnvelopeItemPdfUrl({
|
||||
type: 'view',
|
||||
envelopeItem: envelopeItem,
|
||||
token,
|
||||
});
|
||||
|
||||
const blob = await fetch(downloadUrl).then(async (res) => await res.blob());
|
||||
|
||||
const file = await blob.arrayBuffer();
|
||||
|
||||
setFiles((prev) => ({
|
||||
...prev,
|
||||
[documentData.id]: {
|
||||
file,
|
||||
[envelopeItem.id]: {
|
||||
file: new Uint8Array(file),
|
||||
status: 'loaded',
|
||||
},
|
||||
}));
|
||||
@ -108,7 +146,7 @@ export const EnvelopeRenderProvider = ({
|
||||
|
||||
setFiles((prev) => ({
|
||||
...prev,
|
||||
[documentData.id]: {
|
||||
[envelopeItem.id]: {
|
||||
status: 'error',
|
||||
},
|
||||
}));
|
||||
@ -116,8 +154,8 @@ export const EnvelopeRenderProvider = ({
|
||||
};
|
||||
|
||||
const getPdfBuffer = useCallback(
|
||||
(documentDataId: string) => {
|
||||
return files[documentDataId] || null;
|
||||
(envelopeItemId: string) => {
|
||||
return files[envelopeItemId] || null;
|
||||
},
|
||||
[files],
|
||||
);
|
||||
@ -137,13 +175,18 @@ export const EnvelopeRenderProvider = ({
|
||||
|
||||
// Look for any missing pdf files and load them.
|
||||
useEffect(() => {
|
||||
const missingFiles = envelope.envelopeItems.filter((item) => !files[item.documentDataId]);
|
||||
const missingFiles = envelope.envelopeItems.filter((item) => !files[item.id]);
|
||||
|
||||
for (const item of missingFiles) {
|
||||
void loadEnvelopeItemPdfFile(item.documentData);
|
||||
void loadEnvelopeItemPdfFile(item);
|
||||
}
|
||||
}, [envelope.envelopeItems]);
|
||||
|
||||
const recipientIds = useMemo(
|
||||
() => recipients.map((recipient) => recipient.id).sort(),
|
||||
[recipients],
|
||||
);
|
||||
|
||||
const getRecipientColorKey = useCallback(
|
||||
(recipientId: number) => {
|
||||
const recipientIndex = recipientIds.findIndex((id) => id === recipientId);
|
||||
@ -163,7 +206,11 @@ export const EnvelopeRenderProvider = ({
|
||||
currentEnvelopeItem: currentItem,
|
||||
setCurrentEnvelopeItem,
|
||||
fields: fields ?? [],
|
||||
recipients,
|
||||
getRecipientColorKey,
|
||||
renderError,
|
||||
setRenderError,
|
||||
overrideSettings,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -12,6 +12,7 @@ export const NEXT_PRIVATE_INTERNAL_WEBAPP_URL =
|
||||
export const IS_BILLING_ENABLED = () => env('NEXT_PUBLIC_FEATURE_BILLING_ENABLED') === 'true';
|
||||
|
||||
export const API_V2_BETA_URL = '/api/v2-beta';
|
||||
export const API_V2_URL = '/api/v2';
|
||||
|
||||
export const SUPPORT_EMAIL = env('NEXT_PUBLIC_SUPPORT_EMAIL') ?? 'support@documenso.com';
|
||||
|
||||
|
||||
@ -55,11 +55,11 @@
|
||||
"skia-canvas": "^3.0.8",
|
||||
"stripe": "^12.7.0",
|
||||
"ts-pattern": "^5.0.5",
|
||||
"zod": "3.24.1"
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/browser-chromium": "1.52.0",
|
||||
"@types/luxon": "^3.3.1",
|
||||
"@types/pg": "^8.11.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -78,6 +78,14 @@ export const adminFindDocuments = async ({
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
envelopeId: true,
|
||||
title: true,
|
||||
order: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.envelope.count({
|
||||
|
||||
@ -248,6 +248,14 @@ export const findDocuments = async ({
|
||||
url: true,
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
envelopeId: true,
|
||||
title: true,
|
||||
order: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
prisma.envelope.count({
|
||||
|
||||
@ -92,6 +92,10 @@ export const getDocumentAndSenderByToken = async ({
|
||||
},
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
order: true,
|
||||
envelopeId: true,
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
|
||||
@ -63,5 +63,8 @@ export const getDocumentWithDetailsById = async ({
|
||||
documentId: legacyDocumentId,
|
||||
password: null,
|
||||
},
|
||||
envelopeItems: envelope.envelopeItems.map((envelopeItem) => ({
|
||||
...envelopeItem,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
@ -20,7 +20,12 @@ import { validateCheckboxLength } from '../../advanced-fields-validation/validat
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { jobs } from '../../jobs/client';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { ZCheckboxFieldMeta, ZDropdownFieldMeta, ZRadioFieldMeta } from '../../types/field-meta';
|
||||
import {
|
||||
ZCheckboxFieldMeta,
|
||||
ZDropdownFieldMeta,
|
||||
ZFieldAndMetaSchema,
|
||||
ZRadioFieldMeta,
|
||||
} from '../../types/field-meta';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
@ -174,9 +179,20 @@ export const sendDocument = async ({
|
||||
|
||||
const fieldsToAutoInsert: { fieldId: number; customText: string }[] = [];
|
||||
|
||||
// Auto insert radio and checkboxes that have default values.
|
||||
// Validate and autoinsert fields for V2 envelopes.
|
||||
if (envelope.internalVersion === 2) {
|
||||
for (const field of envelope.fields) {
|
||||
for (const unknownField of envelope.fields) {
|
||||
const parsedField = ZFieldAndMetaSchema.safeParse(unknownField);
|
||||
|
||||
if (parsedField.error) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'One or more fields have invalid metadata. Error: ' + parsedField.error.message,
|
||||
});
|
||||
}
|
||||
|
||||
const field = parsedField.data;
|
||||
const fieldId = unknownField.id;
|
||||
|
||||
if (field.type === FieldType.RADIO) {
|
||||
const { values = [] } = ZRadioFieldMeta.parse(field.fieldMeta);
|
||||
|
||||
@ -184,7 +200,7 @@ export const sendDocument = async ({
|
||||
|
||||
if (checkedItemIndex !== -1) {
|
||||
fieldsToAutoInsert.push({
|
||||
fieldId: field.id,
|
||||
fieldId,
|
||||
customText: toRadioCustomText(checkedItemIndex),
|
||||
});
|
||||
}
|
||||
@ -195,7 +211,7 @@ export const sendDocument = async ({
|
||||
|
||||
if (defaultValue && values.some((value) => value.value === defaultValue)) {
|
||||
fieldsToAutoInsert.push({
|
||||
fieldId: field.id,
|
||||
fieldId,
|
||||
customText: defaultValue,
|
||||
});
|
||||
}
|
||||
@ -234,9 +250,9 @@ export const sendDocument = async ({
|
||||
);
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
if (isValid && checkedIndices.length > 0) {
|
||||
fieldsToAutoInsert.push({
|
||||
fieldId: field.id,
|
||||
fieldId,
|
||||
customText: toCheckboxCustomText(checkedIndices),
|
||||
});
|
||||
}
|
||||
|
||||
@ -17,11 +17,16 @@ import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-reques
|
||||
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { TCreateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/create-envelope.types';
|
||||
|
||||
import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
|
||||
import type {
|
||||
TDocumentAccessAuthTypes,
|
||||
TDocumentActionAuthTypes,
|
||||
TRecipientAccessAuthTypes,
|
||||
TRecipientActionAuthTypes,
|
||||
} from '../../types/document-auth';
|
||||
import type { TDocumentFormValues } from '../../types/document-form-values';
|
||||
import type { TEnvelopeAttachmentType } from '../../types/envelope-attachment';
|
||||
import type { TFieldAndMeta } from '../../types/field-meta';
|
||||
import {
|
||||
ZWebhookDocumentSchema,
|
||||
mapEnvelopeToWebhookDocumentPayload,
|
||||
@ -35,6 +40,25 @@ import { incrementDocumentId, incrementTemplateId } from '../envelope/increment-
|
||||
import { getTeamSettings } from '../team/get-team-settings';
|
||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||
|
||||
type CreateEnvelopeRecipientFieldOptions = TFieldAndMeta & {
|
||||
documentDataId: string;
|
||||
page: number;
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
|
||||
type CreateEnvelopeRecipientOptions = {
|
||||
email: string;
|
||||
name: string;
|
||||
role: RecipientRole;
|
||||
signingOrder?: number;
|
||||
accessAuth?: TRecipientAccessAuthTypes[];
|
||||
actionAuth?: TRecipientActionAuthTypes[];
|
||||
fields?: CreateEnvelopeRecipientFieldOptions[];
|
||||
};
|
||||
|
||||
export type CreateEnvelopeOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
@ -47,7 +71,6 @@ export type CreateEnvelopeOptions = {
|
||||
envelopeItems: { title?: string; documentDataId: string; order?: number }[];
|
||||
formValues?: TDocumentFormValues;
|
||||
|
||||
timezone?: string;
|
||||
userTimezone?: string;
|
||||
|
||||
templateType?: TemplateType;
|
||||
@ -57,7 +80,7 @@ export type CreateEnvelopeOptions = {
|
||||
visibility?: DocumentVisibility;
|
||||
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||
recipients?: TCreateEnvelopeRequest['recipients'];
|
||||
recipients?: CreateEnvelopeRecipientOptions[];
|
||||
folderId?: string;
|
||||
};
|
||||
attachments?: Array<{
|
||||
@ -84,7 +107,6 @@ export const createEnvelope = async ({
|
||||
title,
|
||||
externalId,
|
||||
formValues,
|
||||
timezone,
|
||||
userTimezone,
|
||||
folderId,
|
||||
templateType,
|
||||
@ -143,6 +165,7 @@ export const createEnvelope = async ({
|
||||
let envelopeItems: { title?: string; documentDataId: string; order?: number }[] =
|
||||
data.envelopeItems;
|
||||
|
||||
// Todo: Envelopes - Remove
|
||||
if (normalizePdf) {
|
||||
envelopeItems = await Promise.all(
|
||||
data.envelopeItems.map(async (item) => {
|
||||
@ -220,7 +243,7 @@ export const createEnvelope = async ({
|
||||
|
||||
// userTimezone is last because it's always passed in regardless of the organisation/team settings
|
||||
// for uploads from the frontend
|
||||
const timezoneToUse = timezone || settings.documentTimezone || userTimezone;
|
||||
const timezoneToUse = meta?.timezone || settings.documentTimezone || userTimezone;
|
||||
|
||||
const documentMeta = await prisma.documentMeta.create({
|
||||
data: extractDerivedDocumentMeta(settings, {
|
||||
|
||||
@ -143,7 +143,7 @@ export const getEnvelopeForDirectTemplateSigning = async ({
|
||||
envelope,
|
||||
recipient: {
|
||||
...recipient,
|
||||
token: envelope.directLink?.token || '',
|
||||
directToken: envelope.directLink?.token || '',
|
||||
},
|
||||
recipientSignature: null,
|
||||
isRecipientsTurn: true,
|
||||
|
||||
@ -2,7 +2,6 @@ import { DocumentSigningOrder, DocumentStatus, EnvelopeType, SigningStatus } fro
|
||||
import { z } from 'zod';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
|
||||
import DocumentMetaSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
|
||||
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
|
||||
import EnvelopeSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeSchema';
|
||||
@ -72,20 +71,11 @@ export const ZEnvelopeForSigningResponse = z.object({
|
||||
.array(),
|
||||
|
||||
envelopeItems: EnvelopeItemSchema.pick({
|
||||
envelopeId: true,
|
||||
id: true,
|
||||
title: true,
|
||||
documentDataId: true,
|
||||
order: true,
|
||||
})
|
||||
.extend({
|
||||
documentData: DocumentDataSchema.pick({
|
||||
type: true,
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
}),
|
||||
})
|
||||
.array(),
|
||||
}).array(),
|
||||
|
||||
team: TeamSchema.pick({
|
||||
id: true,
|
||||
@ -117,6 +107,7 @@ export const ZEnvelopeForSigningResponse = z.object({
|
||||
signingOrder: true,
|
||||
rejectionReason: true,
|
||||
}).extend({
|
||||
directToken: z.string().nullish(),
|
||||
fields: ZFieldSchema.omit({
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
@ -199,11 +190,7 @@ export const getEnvelopeForRecipientSigning = async ({
|
||||
signingOrder: 'asc',
|
||||
},
|
||||
},
|
||||
envelopeItems: {
|
||||
include: {
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
envelopeItems: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@ -26,9 +26,9 @@ export interface CreateEnvelopeFieldsOptions {
|
||||
envelopeItemId?: string;
|
||||
|
||||
recipientId: number;
|
||||
pageNumber: number;
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
page: number;
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
})[];
|
||||
@ -122,9 +122,9 @@ export const createEnvelopeFields = async ({
|
||||
const newlyCreatedFields = await tx.field.createManyAndReturn({
|
||||
data: validatedFields.map((field) => ({
|
||||
type: field.type,
|
||||
page: field.pageNumber,
|
||||
positionX: field.pageX,
|
||||
positionY: field.pageY,
|
||||
page: field.page,
|
||||
positionX: field.positionX,
|
||||
positionY: field.positionY,
|
||||
width: field.width,
|
||||
height: field.height,
|
||||
customText: '',
|
||||
|
||||
@ -11,7 +11,7 @@ export type GetFieldByIdOptions = {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
fieldId: number;
|
||||
envelopeType: EnvelopeType;
|
||||
envelopeType?: EnvelopeType;
|
||||
};
|
||||
|
||||
export const getFieldById = async ({
|
||||
@ -41,7 +41,7 @@ export const getFieldById = async ({
|
||||
type: 'envelopeId',
|
||||
id: field.envelopeId,
|
||||
},
|
||||
type: envelopeType,
|
||||
type: envelopeType ?? null,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
@ -158,7 +158,7 @@ export const setFieldsForDocument = async ({
|
||||
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
|
||||
|
||||
const errors = validateNumberField(
|
||||
String(numberFieldParsedMeta.value),
|
||||
String(numberFieldParsedMeta.value || ''),
|
||||
numberFieldParsedMeta,
|
||||
false,
|
||||
);
|
||||
|
||||
@ -10,18 +10,21 @@ import {
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { type EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { mapFieldToLegacyField } from '../../utils/fields';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface UpdateDocumentFieldsOptions {
|
||||
export interface UpdateEnvelopeFieldsOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
documentId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
type?: EnvelopeType | null; // Only used to enforce the type.
|
||||
fields: {
|
||||
id: number;
|
||||
type?: FieldType;
|
||||
pageNumber?: number;
|
||||
envelopeItemId?: string;
|
||||
pageX?: number;
|
||||
pageY?: number;
|
||||
width?: number;
|
||||
@ -31,19 +34,17 @@ export interface UpdateDocumentFieldsOptions {
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
}
|
||||
|
||||
export const updateDocumentFields = async ({
|
||||
export const updateEnvelopeFields = async ({
|
||||
userId,
|
||||
teamId,
|
||||
documentId,
|
||||
id,
|
||||
type = null,
|
||||
fields,
|
||||
requestMetadata,
|
||||
}: UpdateDocumentFieldsOptions) => {
|
||||
}: UpdateEnvelopeFieldsOptions) => {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'documentId',
|
||||
id: documentId,
|
||||
},
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
id,
|
||||
type,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
@ -53,18 +54,19 @@ export const updateDocumentFields = async ({
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
envelopeItems: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
message: 'Envelope not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (envelope.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
message: 'Envelope already complete',
|
||||
});
|
||||
}
|
||||
|
||||
@ -96,6 +98,29 @@ export const updateDocumentFields = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const fieldType = field.type || originalField.type;
|
||||
const fieldMetaType = field.fieldMeta?.type || originalField.fieldMeta?.type;
|
||||
|
||||
// Not going to mess with V1 envelopes.
|
||||
if (
|
||||
envelope.internalVersion === 2 &&
|
||||
fieldMetaType &&
|
||||
fieldMetaType.toLowerCase() !== fieldType.toLowerCase()
|
||||
) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Field meta type does not match the field type',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
field.envelopeItemId &&
|
||||
!envelope.envelopeItems.some((item) => item.id === field.envelopeItemId)
|
||||
) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Envelope item not found',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
originalField,
|
||||
updateData: field,
|
||||
@ -118,27 +143,30 @@ export const updateDocumentFields = async ({
|
||||
width: updateData.width,
|
||||
height: updateData.height,
|
||||
fieldMeta: updateData.fieldMeta,
|
||||
envelopeItemId: updateData.envelopeItemId,
|
||||
},
|
||||
});
|
||||
|
||||
const changes = diffFieldChanges(originalField, updatedField);
|
||||
|
||||
// Handle field updated audit log.
|
||||
if (changes.length > 0) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
fieldId: updatedField.secondaryId,
|
||||
fieldRecipientEmail: recipientEmail,
|
||||
fieldRecipientId: updatedField.recipientId,
|
||||
fieldType: updatedField.type,
|
||||
changes,
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (envelope.type === EnvelopeType.DOCUMENT) {
|
||||
const changes = diffFieldChanges(originalField, updatedField);
|
||||
|
||||
if (changes.length > 0) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
fieldId: updatedField.secondaryId,
|
||||
fieldRecipientEmail: recipientEmail,
|
||||
fieldRecipientId: updatedField.recipientId,
|
||||
fieldType: updatedField.type,
|
||||
changes,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return updatedField;
|
||||
@ -1,116 +0,0 @@
|
||||
import { EnvelopeType, type FieldType } from '@prisma/client';
|
||||
|
||||
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { mapFieldToLegacyField } from '../../utils/fields';
|
||||
import { canRecipientFieldsBeModified } from '../../utils/recipients';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface UpdateTemplateFieldsOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
templateId: number;
|
||||
fields: {
|
||||
id: number;
|
||||
type?: FieldType;
|
||||
pageNumber?: number;
|
||||
pageX?: number;
|
||||
pageY?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
fieldMeta?: TFieldMetaSchema;
|
||||
}[];
|
||||
}
|
||||
|
||||
export const updateTemplateFields = async ({
|
||||
userId,
|
||||
teamId,
|
||||
templateId,
|
||||
fields,
|
||||
}: UpdateTemplateFieldsOptions) => {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'templateId',
|
||||
id: templateId,
|
||||
},
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
});
|
||||
}
|
||||
|
||||
const fieldsToUpdate = fields.map((field) => {
|
||||
const originalField = envelope.fields.find((existingField) => existingField.id === field.id);
|
||||
|
||||
if (!originalField) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: `Field with id ${field.id} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
const recipient = envelope.recipients.find(
|
||||
(recipient) => recipient.id === originalField.recipientId,
|
||||
);
|
||||
|
||||
// Each field MUST have a recipient associated with it.
|
||||
if (!recipient) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: `Recipient attached to field ${field.id} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
// Check whether the recipient associated with the field can be modified.
|
||||
if (!canRecipientFieldsBeModified(recipient, envelope.fields)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message:
|
||||
'Cannot modify a field where the recipient has already interacted with the document',
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
updateData: field,
|
||||
};
|
||||
});
|
||||
|
||||
const updatedFields = await prisma.$transaction(async (tx) => {
|
||||
return await Promise.all(
|
||||
fieldsToUpdate.map(async ({ updateData }) => {
|
||||
const updatedField = await tx.field.update({
|
||||
where: {
|
||||
id: updateData.id,
|
||||
},
|
||||
data: {
|
||||
type: updateData.type,
|
||||
page: updateData.pageNumber,
|
||||
positionX: updateData.pageX,
|
||||
positionY: updateData.pageY,
|
||||
width: updateData.width,
|
||||
height: updateData.height,
|
||||
fieldMeta: updateData.fieldMeta,
|
||||
},
|
||||
});
|
||||
|
||||
return updatedField;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
fields: updatedFields.map((field) => mapFieldToLegacyField(field, envelope)),
|
||||
};
|
||||
};
|
||||
@ -1,14 +1,23 @@
|
||||
import { PDFDocument } from '@cantoo/pdf-lib';
|
||||
|
||||
import { removePlaceholdersFromPDF } from './auto-place-fields';
|
||||
import { AppError } from '../../errors/app-error';
|
||||
import { flattenAnnotations } from './flatten-annotations';
|
||||
import { flattenForm, removeOptionalContentGroups } from './flatten-form';
|
||||
|
||||
export const normalizePdf = async (pdf: Buffer) => {
|
||||
const pdfDoc = await PDFDocument.load(pdf).catch(() => null);
|
||||
const pdfDoc = await PDFDocument.load(pdf).catch((e) => {
|
||||
console.error(`PDF normalization error: ${e.message}`);
|
||||
|
||||
if (!pdfDoc) {
|
||||
return pdf;
|
||||
throw new AppError('INVALID_DOCUMENT_FILE', {
|
||||
message: 'The document is not a valid PDF',
|
||||
});
|
||||
});
|
||||
|
||||
if (pdfDoc.isEncrypted) {
|
||||
throw new AppError('INVALID_DOCUMENT_FILE', {
|
||||
message: 'The document is encrypted',
|
||||
});
|
||||
}
|
||||
|
||||
removeOptionalContentGroups(pdfDoc);
|
||||
|
||||
@ -15,7 +15,7 @@ import type { EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { mapRecipientToLegacyRecipient } from '../../utils/recipients';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface CreateDocumentRecipientsOptions {
|
||||
export interface CreateEnvelopeRecipientsOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
@ -30,16 +30,16 @@ export interface CreateDocumentRecipientsOptions {
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
}
|
||||
|
||||
export const createDocumentRecipients = async ({
|
||||
export const createEnvelopeRecipients = async ({
|
||||
userId,
|
||||
teamId,
|
||||
id,
|
||||
recipients: recipientsToCreate,
|
||||
requestMetadata,
|
||||
}: CreateDocumentRecipientsOptions) => {
|
||||
}: CreateEnvelopeRecipientsOptions) => {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
type: null,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
@ -62,13 +62,13 @@ export const createDocumentRecipients = async ({
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
message: 'Envelope not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (envelope.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
message: 'Envelope already complete',
|
||||
});
|
||||
}
|
||||
|
||||
@ -112,21 +112,23 @@ export const createDocumentRecipients = async ({
|
||||
});
|
||||
|
||||
// Handle recipient created audit log.
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
recipientEmail: createdRecipient.email,
|
||||
recipientName: createdRecipient.name,
|
||||
recipientId: createdRecipient.id,
|
||||
recipientRole: createdRecipient.role,
|
||||
accessAuth: recipient.accessAuth ?? [],
|
||||
actionAuth: recipient.actionAuth ?? [],
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (envelope.type === EnvelopeType.DOCUMENT) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
recipientEmail: createdRecipient.email,
|
||||
recipientName: createdRecipient.name,
|
||||
recipientId: createdRecipient.id,
|
||||
recipientRole: createdRecipient.role,
|
||||
accessAuth: recipient.accessAuth ?? [],
|
||||
actionAuth: recipient.actionAuth ?? [],
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return createdRecipient;
|
||||
}),
|
||||
@ -1,115 +0,0 @@
|
||||
import { EnvelopeType, RecipientRole } from '@prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
||||
import { type TRecipientActionAuthTypes } from '@documenso/lib/types/document-auth';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { mapRecipientToLegacyRecipient } from '../../utils/recipients';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface CreateTemplateRecipientsOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
templateId: number;
|
||||
recipients: {
|
||||
email: string;
|
||||
name: string;
|
||||
role: RecipientRole;
|
||||
signingOrder?: number | null;
|
||||
accessAuth?: TRecipientAccessAuthTypes[];
|
||||
actionAuth?: TRecipientActionAuthTypes[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export const createTemplateRecipients = async ({
|
||||
userId,
|
||||
teamId,
|
||||
templateId,
|
||||
recipients: recipientsToCreate,
|
||||
}: CreateTemplateRecipientsOptions) => {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'templateId',
|
||||
id: templateId,
|
||||
},
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const template = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
team: {
|
||||
select: {
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!template) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template not found',
|
||||
});
|
||||
}
|
||||
|
||||
const recipientsHaveActionAuth = recipientsToCreate.some(
|
||||
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||
);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (recipientsHaveActionAuth && !template.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
|
||||
const normalizedRecipients = recipientsToCreate.map((recipient) => ({
|
||||
...recipient,
|
||||
email: recipient.email.toLowerCase(),
|
||||
}));
|
||||
|
||||
const createdRecipients = await prisma.$transaction(async (tx) => {
|
||||
return await Promise.all(
|
||||
normalizedRecipients.map(async (recipient) => {
|
||||
const authOptions = createRecipientAuthOptions({
|
||||
accessAuth: recipient.accessAuth ?? [],
|
||||
actionAuth: recipient.actionAuth ?? [],
|
||||
});
|
||||
|
||||
const createdRecipient = await tx.recipient.create({
|
||||
data: {
|
||||
envelopeId: template.id,
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
signingOrder: recipient.signingOrder,
|
||||
token: nanoid(),
|
||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus:
|
||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
authOptions,
|
||||
},
|
||||
});
|
||||
|
||||
return createdRecipient;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
recipients: createdRecipients.map((recipient) =>
|
||||
mapRecipientToLegacyRecipient(recipient, template),
|
||||
),
|
||||
};
|
||||
};
|
||||
@ -14,26 +14,27 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
|
||||
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
|
||||
import { canRecipientBeModified } from '../../utils/recipients';
|
||||
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getEmailContext } from '../email/get-email-context';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface DeleteDocumentRecipientOptions {
|
||||
export interface DeleteEnvelopeRecipientOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
recipientId: number;
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
}
|
||||
|
||||
export const deleteDocumentRecipient = async ({
|
||||
export const deleteEnvelopeRecipient = async ({
|
||||
userId,
|
||||
teamId,
|
||||
recipientId,
|
||||
requestMetadata,
|
||||
}: DeleteDocumentRecipientOptions) => {
|
||||
}: DeleteEnvelopeRecipientOptions) => {
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: {
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
recipients: {
|
||||
some: {
|
||||
id: recipientId,
|
||||
@ -48,6 +49,9 @@ export const deleteDocumentRecipient = async ({
|
||||
where: {
|
||||
id: recipientId,
|
||||
},
|
||||
include: {
|
||||
fields: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -89,24 +93,43 @@ export const deleteDocumentRecipient = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const deletedRecipient = await prisma.$transaction(async (tx) => {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
recipientEmail: recipientToDelete.email,
|
||||
recipientName: recipientToDelete.name,
|
||||
recipientId: recipientToDelete.id,
|
||||
recipientRole: recipientToDelete.role,
|
||||
},
|
||||
}),
|
||||
if (!canRecipientBeModified(recipientToDelete, recipientToDelete.fields)) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Recipient has already interacted with the document.',
|
||||
});
|
||||
}
|
||||
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: envelope.id,
|
||||
},
|
||||
type: null,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const deletedRecipient = await prisma.$transaction(async (tx) => {
|
||||
if (envelope.type === EnvelopeType.DOCUMENT) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
recipientEmail: recipientToDelete.email,
|
||||
recipientName: recipientToDelete.name,
|
||||
recipientId: recipientToDelete.id,
|
||||
recipientRole: recipientToDelete.role,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
return await tx.recipient.delete({
|
||||
where: {
|
||||
id: recipientId,
|
||||
envelope: envelopeWhereInput,
|
||||
},
|
||||
});
|
||||
});
|
||||
@ -116,7 +139,11 @@ export const deleteDocumentRecipient = async ({
|
||||
).recipientRemoved;
|
||||
|
||||
// Send email to deleted recipient.
|
||||
if (recipientToDelete.sendStatus === SendStatus.SENT && isRecipientRemovedEmailEnabled) {
|
||||
if (
|
||||
recipientToDelete.sendStatus === SendStatus.SENT &&
|
||||
isRecipientRemovedEmailEnabled &&
|
||||
envelope.type === EnvelopeType.DOCUMENT
|
||||
) {
|
||||
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
|
||||
|
||||
const template = createElement(RecipientRemovedFromDocumentTemplate, {
|
||||
@ -1,58 +0,0 @@
|
||||
import { EnvelopeType } from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { buildTeamWhereQuery } from '../../utils/teams';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface DeleteTemplateRecipientOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
recipientId: number;
|
||||
}
|
||||
|
||||
export const deleteTemplateRecipient = async ({
|
||||
userId,
|
||||
teamId,
|
||||
recipientId,
|
||||
}: DeleteTemplateRecipientOptions): Promise<void> => {
|
||||
const recipientToDelete = await prisma.recipient.findFirst({
|
||||
where: {
|
||||
id: recipientId,
|
||||
envelope: {
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
team: buildTeamWhereQuery({ teamId, userId }),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!recipientToDelete) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Recipient not found',
|
||||
});
|
||||
}
|
||||
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'envelopeId',
|
||||
id: recipientToDelete.envelopeId,
|
||||
},
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
if (!recipientToDelete || recipientToDelete.id !== recipientId) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Recipient not found',
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.recipient.delete({
|
||||
where: {
|
||||
id: recipientId,
|
||||
envelope: envelopeWhereInput,
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -1,5 +1,4 @@
|
||||
import { EnvelopeType, RecipientRole } from '@prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||
import { EnvelopeType, RecipientRole, SendStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
||||
@ -16,29 +15,38 @@ import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope';
|
||||
import { extractLegacyIds } from '../../universal/id';
|
||||
import { type EnvelopeIdOptions } from '../../utils/envelope';
|
||||
import { mapFieldToLegacyField } from '../../utils/fields';
|
||||
import { canRecipientBeModified } from '../../utils/recipients';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface UpdateDocumentRecipientsOptions {
|
||||
export interface UpdateEnvelopeRecipientsOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
id: EnvelopeIdOptions;
|
||||
recipients: RecipientData[];
|
||||
recipients: {
|
||||
id: number;
|
||||
email?: string;
|
||||
name?: string;
|
||||
role?: RecipientRole;
|
||||
signingOrder?: number | null;
|
||||
accessAuth?: TRecipientAccessAuthTypes[];
|
||||
actionAuth?: TRecipientActionAuthTypes[];
|
||||
}[];
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
}
|
||||
|
||||
export const updateDocumentRecipients = async ({
|
||||
export const updateEnvelopeRecipients = async ({
|
||||
userId,
|
||||
teamId,
|
||||
id,
|
||||
recipients,
|
||||
requestMetadata,
|
||||
}: UpdateDocumentRecipientsOptions) => {
|
||||
}: UpdateEnvelopeRecipientsOptions) => {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id,
|
||||
type: EnvelopeType.DOCUMENT,
|
||||
type: null,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
@ -62,13 +70,13 @@ export const updateDocumentRecipients = async ({
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Document not found',
|
||||
message: 'Envelope not found',
|
||||
});
|
||||
}
|
||||
|
||||
if (envelope.completedAt) {
|
||||
throw new AppError(AppErrorCode.INVALID_REQUEST, {
|
||||
message: 'Document already complete',
|
||||
message: 'Envelope already complete',
|
||||
});
|
||||
}
|
||||
|
||||
@ -160,24 +168,26 @@ export const updateDocumentRecipients = async ({
|
||||
});
|
||||
}
|
||||
|
||||
const changes = diffRecipientChanges(originalRecipient, updatedRecipient);
|
||||
|
||||
// Handle recipient updated audit log.
|
||||
if (changes.length > 0) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
recipientEmail: updatedRecipient.email,
|
||||
recipientName: updatedRecipient.name,
|
||||
recipientId: updatedRecipient.id,
|
||||
recipientRole: updatedRecipient.role,
|
||||
changes,
|
||||
},
|
||||
}),
|
||||
});
|
||||
if (envelope.type === EnvelopeType.DOCUMENT) {
|
||||
const changes = diffRecipientChanges(originalRecipient, updatedRecipient);
|
||||
|
||||
if (changes.length > 0) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
||||
envelopeId: envelope.id,
|
||||
metadata: requestMetadata,
|
||||
data: {
|
||||
recipientEmail: updatedRecipient.email,
|
||||
recipientName: updatedRecipient.name,
|
||||
recipientId: updatedRecipient.id,
|
||||
recipientRole: updatedRecipient.role,
|
||||
changes,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return updatedRecipient;
|
||||
@ -188,19 +198,8 @@ export const updateDocumentRecipients = async ({
|
||||
return {
|
||||
recipients: updatedRecipients.map((recipient) => ({
|
||||
...recipient,
|
||||
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId),
|
||||
templateId: null,
|
||||
...extractLegacyIds(envelope),
|
||||
fields: recipient.fields.map((field) => mapFieldToLegacyField(field, envelope)),
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
type RecipientData = {
|
||||
id: number;
|
||||
email?: string;
|
||||
name?: string;
|
||||
role?: RecipientRole;
|
||||
signingOrder?: number | null;
|
||||
accessAuth?: TRecipientAccessAuthTypes[];
|
||||
actionAuth?: TRecipientActionAuthTypes[];
|
||||
};
|
||||
@ -1,168 +0,0 @@
|
||||
import { EnvelopeType, RecipientRole } from '@prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||
|
||||
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
|
||||
import {
|
||||
type TRecipientActionAuthTypes,
|
||||
ZRecipientAuthOptionsSchema,
|
||||
} from '@documenso/lib/types/document-auth';
|
||||
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
|
||||
import { mapFieldToLegacyField } from '../../utils/fields';
|
||||
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
|
||||
|
||||
export interface UpdateTemplateRecipientsOptions {
|
||||
userId: number;
|
||||
teamId: number;
|
||||
templateId: number;
|
||||
recipients: {
|
||||
id: number;
|
||||
email?: string;
|
||||
name?: string;
|
||||
role?: RecipientRole;
|
||||
signingOrder?: number | null;
|
||||
accessAuth?: TRecipientAccessAuthTypes[];
|
||||
actionAuth?: TRecipientActionAuthTypes[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export const updateTemplateRecipients = async ({
|
||||
userId,
|
||||
teamId,
|
||||
templateId,
|
||||
recipients,
|
||||
}: UpdateTemplateRecipientsOptions) => {
|
||||
const { envelopeWhereInput } = await getEnvelopeWhereInput({
|
||||
id: {
|
||||
type: 'templateId',
|
||||
id: templateId,
|
||||
},
|
||||
type: EnvelopeType.TEMPLATE,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
|
||||
const envelope = await prisma.envelope.findFirst({
|
||||
where: envelopeWhereInput,
|
||||
include: {
|
||||
recipients: true,
|
||||
team: {
|
||||
select: {
|
||||
organisation: {
|
||||
select: {
|
||||
organisationClaim: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!envelope) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: 'Template not found',
|
||||
});
|
||||
}
|
||||
|
||||
const recipientsHaveActionAuth = recipients.some(
|
||||
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||
);
|
||||
|
||||
// Check if user has permission to set the global action auth.
|
||||
if (recipientsHaveActionAuth && !envelope.team.organisation.organisationClaim.flags.cfr21) {
|
||||
throw new AppError(AppErrorCode.UNAUTHORIZED, {
|
||||
message: 'You do not have permission to set the action auth',
|
||||
});
|
||||
}
|
||||
|
||||
const recipientsToUpdate = recipients.map((recipient) => {
|
||||
const originalRecipient = envelope.recipients.find(
|
||||
(existingRecipient) => existingRecipient.id === recipient.id,
|
||||
);
|
||||
|
||||
if (!originalRecipient) {
|
||||
throw new AppError(AppErrorCode.NOT_FOUND, {
|
||||
message: `Recipient with id ${recipient.id} not found`,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
originalRecipient,
|
||||
recipientUpdateData: recipient,
|
||||
};
|
||||
});
|
||||
|
||||
const updatedRecipients = await prisma.$transaction(async (tx) => {
|
||||
return await Promise.all(
|
||||
recipientsToUpdate.map(async ({ originalRecipient, recipientUpdateData }) => {
|
||||
let authOptions = ZRecipientAuthOptionsSchema.parse(originalRecipient.authOptions);
|
||||
|
||||
if (
|
||||
recipientUpdateData.actionAuth !== undefined ||
|
||||
recipientUpdateData.accessAuth !== undefined
|
||||
) {
|
||||
authOptions = createRecipientAuthOptions({
|
||||
accessAuth: recipientUpdateData.accessAuth || authOptions.accessAuth,
|
||||
actionAuth: recipientUpdateData.actionAuth || authOptions.actionAuth,
|
||||
});
|
||||
}
|
||||
|
||||
const mergedRecipient = {
|
||||
...originalRecipient,
|
||||
...recipientUpdateData,
|
||||
};
|
||||
|
||||
const updatedRecipient = await tx.recipient.update({
|
||||
where: {
|
||||
id: originalRecipient.id,
|
||||
envelopeId: envelope.id,
|
||||
},
|
||||
data: {
|
||||
name: mergedRecipient.name,
|
||||
email: mergedRecipient.email,
|
||||
role: mergedRecipient.role,
|
||||
signingOrder: mergedRecipient.signingOrder,
|
||||
envelopeId: envelope.id,
|
||||
sendStatus:
|
||||
mergedRecipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus:
|
||||
mergedRecipient.role === RecipientRole.CC
|
||||
? SigningStatus.SIGNED
|
||||
: SigningStatus.NOT_SIGNED,
|
||||
authOptions,
|
||||
},
|
||||
include: {
|
||||
fields: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Clear all fields if the recipient role is changed to a type that cannot have fields.
|
||||
if (
|
||||
originalRecipient.role !== updatedRecipient.role &&
|
||||
(updatedRecipient.role === RecipientRole.CC ||
|
||||
updatedRecipient.role === RecipientRole.VIEWER)
|
||||
) {
|
||||
await tx.field.deleteMany({
|
||||
where: {
|
||||
recipientId: updatedRecipient.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return updatedRecipient;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
recipients: updatedRecipients.map((recipient) => ({
|
||||
...recipient,
|
||||
documentId: null,
|
||||
templateId: mapSecondaryIdToTemplateId(envelope.secondaryId),
|
||||
fields: recipient.fields.map((field) => mapFieldToLegacyField(field, envelope)),
|
||||
})),
|
||||
};
|
||||
};
|
||||
@ -82,6 +82,7 @@ type CreatedDirectRecipientField = {
|
||||
|
||||
export const ZCreateDocumentFromDirectTemplateResponseSchema = z.object({
|
||||
token: z.string(),
|
||||
envelopeId: z.string(),
|
||||
documentId: z.number(),
|
||||
recipientId: z.number(),
|
||||
});
|
||||
@ -815,6 +816,7 @@ export const createDocumentFromDirectTemplate = async ({
|
||||
|
||||
return {
|
||||
token,
|
||||
envelopeId: createdEnvelope.id,
|
||||
documentId: incrementedDocumentId.documentId,
|
||||
recipientId,
|
||||
};
|
||||
|
||||
@ -87,5 +87,9 @@ export const getTemplateByDirectLinkToken = async ({
|
||||
},
|
||||
recipients: recipientsWithMappedFields,
|
||||
fields: recipientsWithMappedFields.flatMap((recipient) => recipient.fields),
|
||||
envelopeItems: envelope.envelopeItems.map((item) => ({
|
||||
id: item.id,
|
||||
envelopeId: item.envelopeId,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
@ -29,6 +29,7 @@ export const getTemplateById = async ({ id, userId, teamId }: GetTemplateByIdOpt
|
||||
envelopeItems: {
|
||||
select: {
|
||||
id: true,
|
||||
envelopeId: true,
|
||||
documentData: true,
|
||||
},
|
||||
},
|
||||
@ -94,5 +95,9 @@ export const getTemplateById = async ({ id, userId, teamId }: GetTemplateByIdOpt
|
||||
}
|
||||
: null,
|
||||
id: mapSecondaryIdToTemplateId(envelope.secondaryId),
|
||||
envelopeItems: envelope.envelopeItems.map((envelopeItem) => ({
|
||||
id: envelopeItem.id,
|
||||
envelopeId: envelopeItem.envelopeId,
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -53,7 +53,7 @@ msgstr "\"Team Name\" has invited you to sign \"example document\"."
|
||||
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signer-form.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
|
||||
msgid "(You)"
|
||||
msgstr "(You)"
|
||||
|
||||
@ -1076,6 +1076,10 @@ msgstr "Add Placeholder Recipient"
|
||||
msgid "Add Placeholders"
|
||||
msgstr "Add Placeholders"
|
||||
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
|
||||
msgid "Add Recipients"
|
||||
msgstr "Add Recipients"
|
||||
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-recipient-form.tsx
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx
|
||||
#: packages/ui/primitives/document-flow/add-signers.tsx
|
||||
@ -1124,6 +1128,8 @@ msgstr "Add this URL to your provider's allowed redirect URIs"
|
||||
msgid "Additional brand information to display at the bottom of emails"
|
||||
msgstr "Additional brand information to display at the bottom of emails"
|
||||
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
|
||||
#: packages/lib/constants/teams-translations.ts
|
||||
#: packages/lib/constants/organisations-translations.ts
|
||||
msgid "Admin"
|
||||
@ -1363,17 +1369,6 @@ msgstr "An error occurred while disabling direct link signing."
|
||||
msgid "An error occurred while disabling the user."
|
||||
msgstr "An error occurred while disabling the user."
|
||||
|
||||
#: apps/remix/app/components/tables/inbox-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-button.tsx
|
||||
#: apps/remix/app/components/general/share-document-download-button.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-button.tsx
|
||||
msgid "An error occurred while downloading your document."
|
||||
msgstr "An error occurred while downloading your document."
|
||||
|
||||
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
|
||||
msgid "An error occurred while duplicating template."
|
||||
msgstr "An error occurred while duplicating template."
|
||||
@ -1455,6 +1450,10 @@ msgstr "An error occurred while signing as assistant."
|
||||
msgid "An error occurred while signing the document."
|
||||
msgstr "An error occurred while signing the document."
|
||||
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx
|
||||
msgid "An error occurred while signing the field."
|
||||
msgstr "An error occurred while signing the field."
|
||||
|
||||
#: apps/remix/app/components/general/billing-plans.tsx
|
||||
#: apps/remix/app/components/general/billing-plans.tsx
|
||||
msgid "An error occurred while trying to create a checkout session."
|
||||
@ -1538,6 +1537,7 @@ msgstr "An unexpected error occurred."
|
||||
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/admin-document-delete-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/account-delete-dialog.tsx
|
||||
@ -1694,7 +1694,7 @@ msgstr "Assist"
|
||||
msgid "Assist Document"
|
||||
msgstr "Assist Document"
|
||||
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
|
||||
msgid "Assist with signing"
|
||||
msgstr "Assist with signing"
|
||||
|
||||
@ -2038,6 +2038,7 @@ msgstr "Can prepare"
|
||||
#: apps/remix/app/components/dialogs/claim-delete-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/claim-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
|
||||
#: packages/ui/primitives/signature-pad/signature-pad-dialog.tsx
|
||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
||||
@ -2165,10 +2166,6 @@ msgstr "Clear filters"
|
||||
msgid "Clear Signature"
|
||||
msgstr "Clear Signature"
|
||||
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
|
||||
msgid "Click here to add a recipient"
|
||||
msgstr "Click here to add a recipient"
|
||||
|
||||
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
|
||||
msgid "Click here to get started"
|
||||
msgstr "Click here to get started"
|
||||
@ -2191,7 +2188,7 @@ msgstr "Click to copy signing link for sending to recipient"
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
|
||||
#: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
|
||||
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
|
||||
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
|
||||
msgid "Click to insert field"
|
||||
@ -2234,10 +2231,6 @@ msgstr "Client secret is required"
|
||||
msgid "Close"
|
||||
msgstr "Close"
|
||||
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-preview-page.tsx
|
||||
msgid "Coming soon"
|
||||
msgstr "Coming soon"
|
||||
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-advanced-settings.tsx
|
||||
msgid "Communication"
|
||||
msgstr "Communication"
|
||||
@ -2249,8 +2242,8 @@ msgstr "Compare all plans and features in detail"
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
|
||||
#: apps/remix/app/components/forms/signup.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
|
||||
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
|
||||
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
|
||||
@ -2296,6 +2289,12 @@ msgstr "Completed Documents"
|
||||
msgid "Completed on {formattedDate}"
|
||||
msgstr "Completed on {formattedDate}"
|
||||
|
||||
#: packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx
|
||||
#: packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx
|
||||
#: packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx
|
||||
msgid "Configuration Error"
|
||||
msgstr "Configuration Error"
|
||||
|
||||
#. placeholder {0}: parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[currentField.type])
|
||||
#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx
|
||||
msgid "Configure {0} Field"
|
||||
@ -3567,20 +3566,17 @@ msgstr "Domain Name"
|
||||
msgid "Don't have an account? <0>Sign up</0>"
|
||||
msgstr "Don't have an account? <0>Sign up</0>"
|
||||
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
|
||||
#: apps/remix/app/components/tables/organisation-billing-invoices-table.tsx
|
||||
#: apps/remix/app/components/tables/inbox-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-button.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-button.tsx
|
||||
#: apps/remix/app/components/general/share-document-download-button.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-button.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-button.tsx
|
||||
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
|
||||
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
|
||||
#: apps/remix/app/components/forms/2fa/view-recovery-codes-dialog.tsx
|
||||
#: apps/remix/app/components/forms/2fa/enable-authenticator-app-dialog.tsx
|
||||
#: packages/ui/components/document/document-download-button.tsx
|
||||
#: packages/email/template-components/template-document-completed.tsx
|
||||
msgid "Download"
|
||||
msgstr "Download"
|
||||
@ -3597,11 +3593,6 @@ msgstr "Download Certificate"
|
||||
msgid "Download Files"
|
||||
msgstr "Download Files"
|
||||
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
msgid "Download Original"
|
||||
msgstr "Download Original"
|
||||
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signer-header.tsx
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
|
||||
@ -3752,7 +3743,7 @@ msgstr "Electronic Signature Disclosure"
|
||||
#: apps/remix/app/components/forms/signin.tsx
|
||||
#: apps/remix/app/components/forms/profile.tsx
|
||||
#: apps/remix/app/components/forms/forgot-password.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
|
||||
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
|
||||
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
|
||||
#: apps/remix/app/components/embed/authoring/configure-document-recipients.tsx
|
||||
@ -4023,10 +4014,6 @@ msgstr "Enter your text here"
|
||||
msgid "Enterprise"
|
||||
msgstr "Enterprise"
|
||||
|
||||
#: packages/ui/primitives/document-upload.tsx
|
||||
msgid "Envelope (beta)"
|
||||
msgstr "Envelope (beta)"
|
||||
|
||||
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
|
||||
msgid "Envelope distributed"
|
||||
msgstr "Envelope distributed"
|
||||
@ -4072,7 +4059,6 @@ msgstr "Envelope updated"
|
||||
#: apps/remix/app/routes/_authenticated+/settings+/security.linked-accounts.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/users.$id.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
|
||||
#: apps/remix/app/components/general/verify-email-banner.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
@ -4081,6 +4067,7 @@ msgstr "Envelope updated"
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
#: apps/remix/app/components/general/template/template-edit-form.tsx
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signer-page-renderer.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
|
||||
@ -4429,7 +4416,7 @@ msgstr "Free Signature Settings"
|
||||
#: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx
|
||||
#: apps/remix/app/components/forms/signup.tsx
|
||||
#: apps/remix/app/components/forms/profile.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
|
||||
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
|
||||
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
|
||||
msgid "Full Name"
|
||||
@ -4574,7 +4561,7 @@ msgstr "has invited you to view this document"
|
||||
msgid "Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist."
|
||||
msgstr "Having an assistant as the last signer means they will be unable to take any action as there are no subsequent signers to assist."
|
||||
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
|
||||
msgid "Help complete the document for other signers."
|
||||
msgstr "Help complete the document for other signers."
|
||||
|
||||
@ -5252,6 +5239,8 @@ msgstr "Manage your passkeys."
|
||||
msgid "Manage your site settings here"
|
||||
msgstr "Manage your site settings here"
|
||||
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
|
||||
#: packages/lib/constants/teams-translations.ts
|
||||
#: packages/lib/constants/organisations-translations.ts
|
||||
msgid "Manager"
|
||||
@ -5300,6 +5289,8 @@ msgstr "Maximum number of uploaded files per envelope allowed"
|
||||
#: apps/remix/app/components/tables/team-members-table.tsx
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
|
||||
#: packages/lib/constants/teams-translations.ts
|
||||
#: packages/lib/constants/organisations-translations.ts
|
||||
msgid "Member"
|
||||
@ -5310,10 +5301,6 @@ msgstr "Member"
|
||||
msgid "Member Count"
|
||||
msgstr "Member Count"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
|
||||
msgid "Member promoted to owner successfully"
|
||||
msgstr "Member promoted to owner successfully"
|
||||
|
||||
#: apps/remix/app/components/tables/organisation-members-table.tsx
|
||||
msgid "Member Since"
|
||||
msgstr "Member Since"
|
||||
@ -5346,6 +5333,10 @@ msgstr "Message <0>(Optional)</0>"
|
||||
msgid "Min"
|
||||
msgstr "Min"
|
||||
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
|
||||
msgid "Missing Recipients"
|
||||
msgstr "Missing Recipients"
|
||||
|
||||
#: apps/remix/app/components/general/template/template-page-view-recipients.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
|
||||
msgid "Modify recipients"
|
||||
@ -5484,7 +5475,7 @@ msgstr "New Password"
|
||||
msgid "New Template"
|
||||
msgstr "New Template"
|
||||
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
|
||||
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
|
||||
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
|
||||
#: apps/remix/app/components/dialogs/team-member-create-dialog.tsx
|
||||
@ -5932,9 +5923,15 @@ msgstr "Override organisation settings"
|
||||
#: apps/remix/app/components/tables/organisation-members-table.tsx
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
|
||||
msgid "Owner"
|
||||
msgstr "Owner"
|
||||
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
|
||||
msgid "Ownership transferred to {organisationMemberName}."
|
||||
msgstr "Ownership transferred to {organisationMemberName}."
|
||||
|
||||
#. placeholder {0}: table.getState().pagination.pageIndex + 1
|
||||
#. placeholder {1}: table.getPageCount() || 1
|
||||
#: packages/ui/primitives/data-table-pagination.tsx
|
||||
@ -6039,6 +6036,7 @@ msgstr "PDF Document"
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-header.tsx
|
||||
#: apps/remix/app/components/general/document/document-status.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
|
||||
#: packages/ui/components/document/envelope-recipient-field-tooltip.tsx
|
||||
#: packages/ui/components/document/document-read-only-fields.tsx
|
||||
#: packages/lib/constants/document.ts
|
||||
msgid "Pending"
|
||||
@ -6376,10 +6374,6 @@ msgstr "Profile updated"
|
||||
msgid "Progress"
|
||||
msgstr "Progress"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
|
||||
msgid "Promote to owner"
|
||||
msgstr "Promote to owner"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/settings+/security.linked-accounts.tsx
|
||||
msgid "Provider"
|
||||
msgstr "Provider"
|
||||
@ -6880,6 +6874,7 @@ msgstr "Right"
|
||||
#: apps/remix/app/components/dialogs/team-member-update-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/team-group-update-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
|
||||
msgid "Role"
|
||||
msgstr "Role"
|
||||
|
||||
@ -7339,7 +7334,7 @@ msgstr "Sign as<0>{0} <1>({1})</1></0>"
|
||||
msgid "Sign Checkbox Field"
|
||||
msgstr "Sign Checkbox Field"
|
||||
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
|
||||
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
|
||||
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
|
||||
msgid "Sign document"
|
||||
@ -7407,7 +7402,7 @@ msgstr "Sign Signature Field"
|
||||
msgid "Sign Text Field"
|
||||
msgstr "Sign Text Field"
|
||||
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
|
||||
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
|
||||
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
|
||||
msgid "Sign the document to complete the process."
|
||||
@ -7449,7 +7444,7 @@ msgstr "Sign your initials into the field"
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
|
||||
#: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx
|
||||
#: apps/remix/app/components/forms/profile.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
|
||||
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
|
||||
#: apps/remix/app/components/embed/multisign/multi-sign-document-signing-view.tsx
|
||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx
|
||||
@ -7483,13 +7478,10 @@ msgstr "Signature types"
|
||||
msgid "Signatures Collected"
|
||||
msgstr "Signatures Collected"
|
||||
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
|
||||
msgid "Signatures will appear once the document has been completed"
|
||||
msgstr "Signatures will appear once the document has been completed"
|
||||
|
||||
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
|
||||
#: apps/remix/app/components/dialogs/envelope-download-dialog.tsx
|
||||
#: packages/ui/components/document/envelope-recipient-field-tooltip.tsx
|
||||
#: packages/ui/components/document/document-read-only-fields.tsx
|
||||
msgid "Signed"
|
||||
msgstr "Signed"
|
||||
@ -7535,7 +7527,7 @@ msgstr "Signing certificate provided by"
|
||||
msgid "Signing Complete!"
|
||||
msgstr "Signing Complete!"
|
||||
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
|
||||
msgid "Signing for"
|
||||
msgstr "Signing for"
|
||||
|
||||
@ -7595,11 +7587,6 @@ msgstr "Some signers have not been assigned a signature field. Please assign at
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
#: apps/remix/app/components/tables/organisation-member-invites-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-member-invites-table.tsx
|
||||
#: apps/remix/app/components/tables/inbox-table.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-dropdown.tsx
|
||||
#: apps/remix/app/components/tables/documents-table-action-button.tsx
|
||||
#: apps/remix/app/components/general/share-document-download-button.tsx
|
||||
#: apps/remix/app/components/general/billing-plans.tsx
|
||||
#: apps/remix/app/components/general/billing-plans.tsx
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
@ -7610,16 +7597,14 @@ msgstr "Some signers have not been assigned a signature field. Please assign at
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-portal-button.tsx
|
||||
#: apps/remix/app/components/general/organisations/organisation-billing-banner.tsx
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signing-complete-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signing-complete-dialog.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-account.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-dropdown.tsx
|
||||
#: apps/remix/app/components/general/document/document-page-view-button.tsx
|
||||
#: apps/remix/app/components/general/document/document-certificate-download-button.tsx
|
||||
#: apps/remix/app/components/general/document/document-audit-log-download-button.tsx
|
||||
#: apps/remix/app/components/general/direct-template/direct-template-signing-auth-page.tsx
|
||||
#: apps/remix/app/components/general/direct-template/direct-template-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
|
||||
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
|
||||
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
|
||||
@ -7658,6 +7643,11 @@ msgstr "Something went wrong while loading the document."
|
||||
msgid "Something went wrong while loading your passkeys."
|
||||
msgstr "Something went wrong while loading your passkeys."
|
||||
|
||||
#: packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx
|
||||
#: packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx
|
||||
msgid "Something went wrong while rendering the document, some fields may be missing or corrupted."
|
||||
msgstr "Something went wrong while rendering the document, some fields may be missing or corrupted."
|
||||
|
||||
#: apps/remix/app/components/general/verify-email-banner.tsx
|
||||
msgid "Something went wrong while sending the confirmation email."
|
||||
msgstr "Something went wrong while sending the confirmation email."
|
||||
@ -7808,7 +7798,6 @@ msgstr "Subscription invalid"
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
|
||||
#: apps/remix/app/components/tables/organisation-member-invites-table.tsx
|
||||
@ -7853,6 +7842,7 @@ msgstr "Subscription invalid"
|
||||
#: apps/remix/app/components/dialogs/organisation-delete-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/organisation-create-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/envelope-item-delete-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-create-dialog.tsx
|
||||
msgid "Success"
|
||||
msgstr "Success"
|
||||
@ -8523,6 +8513,10 @@ msgstr "The token you have used to reset your password is either expired or it n
|
||||
msgid "The two-factor authentication code provided is incorrect"
|
||||
msgstr "The two-factor authentication code provided is incorrect"
|
||||
|
||||
#: apps/remix/app/components/forms/editor/editor-field-signature-form.tsx
|
||||
msgid "The typed signature font size"
|
||||
msgstr "The typed signature font size"
|
||||
|
||||
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
|
||||
msgid "The types of signatures that recipients are allowed to use when signing the document."
|
||||
msgstr "The types of signatures that recipients are allowed to use when signing the document."
|
||||
@ -8576,6 +8570,10 @@ msgstr "There are no completed documents yet. Documents that you have created or
|
||||
msgid "There was an error uploading your file. Please try again."
|
||||
msgstr "There was an error uploading your file. Please try again."
|
||||
|
||||
#: packages/ui/components/pdf-viewer/pdf-viewer-konva.tsx
|
||||
msgid "There was an issue rendering some fields, please review the fields and try again."
|
||||
msgstr "There was an issue rendering some fields, please review the fields and try again."
|
||||
|
||||
#: packages/ui/components/document/document-global-auth-action-select.tsx
|
||||
msgid "These can be overriden by setting the authentication requirements directly on each recipient in the next step. Multiple methods can be selected."
|
||||
msgstr "These can be overriden by setting the authentication requirements directly on each recipient in the next step. Multiple methods can be selected."
|
||||
@ -8724,10 +8722,6 @@ msgstr "This envelope could not be distributed at this time. Please try again."
|
||||
msgid "This envelope could not be resent at this time. Please try again."
|
||||
msgstr "This envelope could not be resent at this time. Please try again."
|
||||
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-preview-page.tsx
|
||||
msgid "This feature is coming soon"
|
||||
msgstr "This feature is coming soon"
|
||||
|
||||
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
|
||||
msgid "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
|
||||
msgstr "This field cannot be modified or deleted. When you share this template's direct link or add it to your public profile, anyone who accesses it can input their name and email, and fill in the fields assigned to them."
|
||||
@ -9199,6 +9193,7 @@ msgstr "Untitled Group"
|
||||
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/organisation-email-update-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/folder-update-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx
|
||||
#: packages/ui/primitives/document-flow/add-subject.tsx
|
||||
msgid "Update"
|
||||
@ -9235,6 +9230,7 @@ msgstr "Update organisation"
|
||||
|
||||
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
|
||||
msgid "Update organisation member"
|
||||
msgstr "Update organisation member"
|
||||
|
||||
@ -9255,9 +9251,11 @@ msgstr "Update profile"
|
||||
msgid "Update Recipient"
|
||||
msgstr "Update Recipient"
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
|
||||
#: apps/remix/app/components/tables/team-members-table.tsx
|
||||
#: apps/remix/app/components/tables/team-groups-table.tsx
|
||||
#: apps/remix/app/components/tables/organisation-members-table.tsx
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
|
||||
msgid "Update role"
|
||||
msgstr "Update role"
|
||||
|
||||
@ -9296,6 +9294,10 @@ msgstr "Update user"
|
||||
msgid "Update webhook"
|
||||
msgstr "Update webhook"
|
||||
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
|
||||
msgid "Updated {organisationMemberName} to {roleLabel}."
|
||||
msgstr "Updated {organisationMemberName} to {roleLabel}."
|
||||
|
||||
#: apps/remix/app/components/forms/password.tsx
|
||||
msgid "Updating password..."
|
||||
msgstr "Updating password..."
|
||||
@ -9366,6 +9368,10 @@ msgstr "Upload Document"
|
||||
msgid "Upload documents and add recipients"
|
||||
msgstr "Upload documents and add recipients"
|
||||
|
||||
#: packages/ui/primitives/document-upload.tsx
|
||||
msgid "Upload Envelope"
|
||||
msgstr "Upload Envelope"
|
||||
|
||||
#: apps/remix/app/components/general/template/template-drop-zone-wrapper.tsx
|
||||
#: apps/remix/app/components/general/envelope-editor/envelope-editor-upload-page.tsx
|
||||
#: apps/remix/app/components/general/document/envelope-upload-button.tsx
|
||||
@ -9662,10 +9668,6 @@ msgstr "View more"
|
||||
msgid "View next document"
|
||||
msgstr "View next document"
|
||||
|
||||
#: apps/remix/app/routes/_recipient+/sign.$token+/complete.tsx
|
||||
msgid "View Original Document"
|
||||
msgstr "View Original Document"
|
||||
|
||||
#: apps/remix/app/components/tables/admin-organisations-table.tsx
|
||||
msgid "View owner"
|
||||
msgstr "View owner"
|
||||
@ -9780,10 +9782,6 @@ msgstr "We are unable to update this passkey at the moment. Please try again lat
|
||||
msgid "We couldn't create a Stripe customer. Please try again."
|
||||
msgstr "We couldn't create a Stripe customer. Please try again."
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/admin+/organisations.$id.tsx
|
||||
msgid "We couldn't promote the member to owner. Please try again."
|
||||
msgstr "We couldn't promote the member to owner. Please try again."
|
||||
|
||||
#: apps/remix/app/routes/_authenticated+/o.$orgUrl.settings.groups.$id.tsx
|
||||
msgid "We couldn't update the group. Please try again."
|
||||
msgstr "We couldn't update the group. Please try again."
|
||||
@ -9946,6 +9944,7 @@ msgid "We encountered an unknown error while attempting to update the template.
|
||||
msgstr "We encountered an unknown error while attempting to update the template. Please try again later."
|
||||
|
||||
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
|
||||
msgid "We encountered an unknown error while attempting to update this organisation member. Please try again later."
|
||||
msgstr "We encountered an unknown error while attempting to update this organisation member. Please try again later."
|
||||
|
||||
@ -10018,9 +10017,10 @@ msgstr "We were unable to set your public profile to public. Please try again."
|
||||
msgid "We were unable to setup two-factor authentication for your account. Please ensure that you have entered your code correctly and try again."
|
||||
msgstr "We were unable to setup two-factor authentication for your account. Please ensure that you have entered your code correctly and try again."
|
||||
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signing-complete-dialog.tsx
|
||||
#: apps/remix/app/components/general/envelope-signing/envelope-signing-complete-dialog.tsx
|
||||
#: apps/remix/app/components/general/direct-template/direct-template-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
|
||||
#: apps/remix/app/components/embed/embed-document-signing-page-v1.tsx
|
||||
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
|
||||
msgid "We were unable to submit this document at this time. Please try again later."
|
||||
msgstr "We were unable to submit this document at this time. Please try again later."
|
||||
@ -10322,6 +10322,7 @@ msgid "You are currently updating <0>{memberName}.</0>"
|
||||
msgstr "You are currently updating <0>{memberName}.</0>"
|
||||
|
||||
#: apps/remix/app/components/dialogs/organisation-member-update-dialog.tsx
|
||||
#: apps/remix/app/components/dialogs/admin-organisation-member-update-dialog.tsx
|
||||
msgid "You are currently updating <0>{organisationMemberName}.</0>"
|
||||
msgstr "You are currently updating <0>{organisationMemberName}.</0>"
|
||||
|
||||
@ -10495,7 +10496,7 @@ msgid "You have been invited to join the following organisation"
|
||||
msgstr "You have been invited to join the following organisation"
|
||||
|
||||
#: packages/lib/server-only/recipient/set-document-recipients.ts
|
||||
#: packages/lib/server-only/recipient/delete-document-recipient.ts
|
||||
#: packages/lib/server-only/recipient/delete-envelope-recipient.ts
|
||||
msgid "You have been removed from a document"
|
||||
msgstr "You have been removed from a document"
|
||||
|
||||
@ -10664,6 +10665,7 @@ msgstr "You need to be an admin to manage API tokens."
|
||||
msgid "You need to be logged in as <0>{email}</0> to view this page."
|
||||
msgstr "You need to be logged in as <0>{email}</0> to view this page."
|
||||
|
||||
#: apps/remix/app/components/general/document-signing/document-signing-auth-page.tsx
|
||||
#: apps/remix/app/components/general/direct-template/direct-template-signing-auth-page.tsx
|
||||
msgid "You need to be logged in to view this page."
|
||||
msgstr "You need to be logged in to view this page."
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -2,6 +2,7 @@ import { z } from 'zod';
|
||||
|
||||
import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
|
||||
import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
|
||||
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
|
||||
import { FolderSchema } from '@documenso/prisma/generated/zod/modelSchema/FolderSchema';
|
||||
import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
|
||||
@ -74,6 +75,10 @@ export const ZDocumentSchema = LegacyDocumentSchema.pick({
|
||||
password: z.string().nullable().default(null),
|
||||
documentId: z.number().default(-1).optional(),
|
||||
}),
|
||||
envelopeItems: EnvelopeItemSchema.pick({
|
||||
id: true,
|
||||
envelopeId: true,
|
||||
}).array(),
|
||||
|
||||
folder: FolderSchema.pick({
|
||||
id: true,
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import DocumentDataSchema from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
|
||||
import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
|
||||
import { EnvelopeItemSchema } from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
|
||||
import { EnvelopeSchema } from '@documenso/prisma/generated/zod/modelSchema/EnvelopeSchema';
|
||||
import { TeamSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
import TemplateDirectLinkSchema from '@documenso/prisma/generated/zod/modelSchema/TemplateDirectLinkSchema';
|
||||
|
||||
import { ZFieldSchema } from './field';
|
||||
import { ZRecipientLiteSchema } from './recipient';
|
||||
import { ZEnvelopeFieldSchema } from './field';
|
||||
import { ZEnvelopeRecipientLiteSchema } from './recipient';
|
||||
|
||||
/**
|
||||
* The full envelope response schema.
|
||||
@ -37,11 +36,8 @@ export const ZEnvelopeSchema = EnvelopeSchema.pick({
|
||||
userId: true,
|
||||
teamId: true,
|
||||
folderId: true,
|
||||
templateId: true,
|
||||
}).extend({
|
||||
templateId: z
|
||||
.number()
|
||||
.nullish()
|
||||
.describe('The ID of the template that the document was created from, if any.'),
|
||||
documentMeta: DocumentMetaSchema.pick({
|
||||
signingOrder: true,
|
||||
distributionMethod: true,
|
||||
@ -60,29 +56,14 @@ export const ZEnvelopeSchema = EnvelopeSchema.pick({
|
||||
emailId: true,
|
||||
emailReplyTo: true,
|
||||
}),
|
||||
recipients: ZRecipientLiteSchema.omit({
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
}).array(),
|
||||
fields: ZFieldSchema.omit({
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
}).array(),
|
||||
recipients: ZEnvelopeRecipientLiteSchema.array(),
|
||||
fields: ZEnvelopeFieldSchema.array(),
|
||||
envelopeItems: EnvelopeItemSchema.pick({
|
||||
envelopeId: true,
|
||||
id: true,
|
||||
title: true,
|
||||
documentDataId: true,
|
||||
order: true,
|
||||
})
|
||||
.extend({
|
||||
documentData: DocumentDataSchema.pick({
|
||||
type: true,
|
||||
id: true,
|
||||
data: true,
|
||||
initialData: true,
|
||||
}),
|
||||
})
|
||||
.array(),
|
||||
}).array(),
|
||||
directLink: TemplateDirectLinkSchema.pick({
|
||||
directTemplateRecipientId: true,
|
||||
enabled: true,
|
||||
|
||||
@ -188,7 +188,7 @@ export type TFieldMetaSchema = z.infer<typeof ZFieldMetaSchema>;
|
||||
export const ZFieldAndMetaSchema = z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal(FieldType.SIGNATURE),
|
||||
fieldMeta: z.undefined(),
|
||||
fieldMeta: ZSignatureFieldMeta.optional(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal(FieldType.FREE_SIGNATURE),
|
||||
|
||||
@ -50,6 +50,11 @@ export const ZFieldSchema = FieldSchema.pick({
|
||||
templateId: z.number().nullish(),
|
||||
});
|
||||
|
||||
export const ZEnvelopeFieldSchema = ZFieldSchema.omit({
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
});
|
||||
|
||||
export const ZFieldPageNumberSchema = z
|
||||
.number()
|
||||
.min(1)
|
||||
@ -69,6 +74,30 @@ export const ZFieldWidthSchema = z.number().min(1).describe('The width of the fi
|
||||
|
||||
export const ZFieldHeightSchema = z.number().min(1).describe('The height of the field.');
|
||||
|
||||
export const ZClampedFieldPositionXSchema = z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(100)
|
||||
.describe('The percentage based X coordinate where the field will be placed.');
|
||||
|
||||
export const ZClampedFieldPositionYSchema = z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(100)
|
||||
.describe('The percentage based Y coordinate where the field will be placed.');
|
||||
|
||||
export const ZClampedFieldWidthSchema = z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(100)
|
||||
.describe('The percentage based width of the field on the page.');
|
||||
|
||||
export const ZClampedFieldHeightSchema = z
|
||||
.number()
|
||||
.min(0)
|
||||
.max(100)
|
||||
.describe('The percentage based height of the field on the page.');
|
||||
|
||||
// ---------------------------------------------
|
||||
|
||||
const PrismaDecimalSchema = z.preprocess(
|
||||
|
||||
@ -95,3 +95,18 @@ export const ZRecipientManySchema = RecipientSchema.pick({
|
||||
documentId: z.number().nullish(),
|
||||
templateId: z.number().nullish(),
|
||||
});
|
||||
|
||||
export const ZEnvelopeRecipientSchema = ZRecipientSchema.omit({
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
});
|
||||
|
||||
export const ZEnvelopeRecipientLiteSchema = ZRecipientLiteSchema.omit({
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
});
|
||||
|
||||
export const ZEnvelopeRecipientManySchema = ZRecipientManySchema.omit({
|
||||
documentId: true,
|
||||
templateId: true,
|
||||
});
|
||||
|
||||
@ -2,6 +2,7 @@ import { z } from 'zod';
|
||||
|
||||
import { DocumentDataSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentDataSchema';
|
||||
import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
|
||||
import EnvelopeItemSchema from '@documenso/prisma/generated/zod/modelSchema/EnvelopeItemSchema';
|
||||
import { FolderSchema } from '@documenso/prisma/generated/zod/modelSchema/FolderSchema';
|
||||
import TeamSchema from '@documenso/prisma/generated/zod/modelSchema/TeamSchema';
|
||||
import { UserSchema } from '@documenso/prisma/generated/zod/modelSchema/UserSchema';
|
||||
@ -87,6 +88,10 @@ export const ZTemplateSchema = TemplateSchema.pick({
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
}).nullable(),
|
||||
envelopeItems: EnvelopeItemSchema.pick({
|
||||
id: true,
|
||||
envelopeId: true,
|
||||
}).array(),
|
||||
});
|
||||
|
||||
export type TTemplate = z.infer<typeof ZTemplateSchema>;
|
||||
|
||||
@ -30,3 +30,5 @@ export const symmetricDecrypt = ({ key, data }: SymmetricDecryptOptions) => {
|
||||
|
||||
return chacha.decrypt(dataAsBytes);
|
||||
};
|
||||
|
||||
export { sha256 };
|
||||
|
||||
@ -153,6 +153,11 @@ export const createFieldHoverInteraction = ({
|
||||
const hoverColor = RECIPIENT_COLOR_STYLES[options.color].baseRingHover;
|
||||
|
||||
fieldGroup.on('mouseover', () => {
|
||||
const layer = fieldRect.getLayer();
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
|
||||
new Konva.Tween({
|
||||
node: fieldRect,
|
||||
duration: 0.3,
|
||||
@ -161,6 +166,11 @@ export const createFieldHoverInteraction = ({
|
||||
});
|
||||
|
||||
fieldGroup.on('mouseout', () => {
|
||||
const layer = fieldRect.getLayer();
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
|
||||
new Konva.Tween({
|
||||
node: fieldRect,
|
||||
duration: 0.3,
|
||||
@ -169,6 +179,11 @@ export const createFieldHoverInteraction = ({
|
||||
});
|
||||
|
||||
fieldGroup.on('transformstart', () => {
|
||||
const layer = fieldRect.getLayer();
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
|
||||
new Konva.Tween({
|
||||
node: fieldRect,
|
||||
duration: 0.3,
|
||||
@ -177,6 +192,11 @@ export const createFieldHoverInteraction = ({
|
||||
});
|
||||
|
||||
fieldGroup.on('transformend', () => {
|
||||
const layer = fieldRect.getLayer();
|
||||
if (!layer) {
|
||||
return;
|
||||
}
|
||||
|
||||
new Konva.Tween({
|
||||
node: fieldRect,
|
||||
duration: 0.3,
|
||||
|
||||
@ -19,7 +19,7 @@ export type FieldToRender = Pick<
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
fieldMeta?: TFieldMetaSchema | null;
|
||||
signature?: Signature | null;
|
||||
signature?: Pick<Signature, 'signatureImageAsBase64' | 'typedSignature'> | null;
|
||||
};
|
||||
|
||||
export type RenderFieldElementOptions = {
|
||||
|
||||
@ -3,6 +3,7 @@ import { match } from 'ts-pattern';
|
||||
|
||||
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
|
||||
import type { TCheckboxFieldMeta } from '../../types/field-meta';
|
||||
import { parseCheckboxCustomText } from '../../utils/fields';
|
||||
import {
|
||||
createFieldHoverInteraction,
|
||||
konvaTextFill,
|
||||
@ -62,16 +63,15 @@ export const renderCheckboxFieldElement = (
|
||||
const rectWidth = fieldRect.width() * groupScaleX;
|
||||
const rectHeight = fieldRect.height() * groupScaleY;
|
||||
|
||||
// Todo: Envelopes - check sorting more than 10
|
||||
// arr.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
||||
|
||||
const squares = fieldGroup
|
||||
.find('.checkbox-square')
|
||||
.sort((a, b) => a.id().localeCompare(b.id()));
|
||||
.sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true }));
|
||||
const checkmarks = fieldGroup
|
||||
.find('.checkbox-checkmark')
|
||||
.sort((a, b) => a.id().localeCompare(b.id()));
|
||||
const text = fieldGroup.find('.checkbox-text').sort((a, b) => a.id().localeCompare(b.id()));
|
||||
.sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true }));
|
||||
const text = fieldGroup
|
||||
.find('.checkbox-text')
|
||||
.sort((a, b) => a.id().localeCompare(b.id(), undefined, { numeric: true }));
|
||||
|
||||
const groupedItems = squares.map((square, i) => ({
|
||||
squareElement: square,
|
||||
@ -130,7 +130,7 @@ export const renderCheckboxFieldElement = (
|
||||
pageLayer.batchDraw();
|
||||
});
|
||||
|
||||
const checkedValues: number[] = field.customText ? JSON.parse(field.customText) : [];
|
||||
const checkedValues: number[] = field.customText ? parseCheckboxCustomText(field.customText) : [];
|
||||
|
||||
checkboxValues.forEach(({ value, checked }, index) => {
|
||||
const isCheckboxChecked = match(mode)
|
||||
@ -170,7 +170,7 @@ export const renderCheckboxFieldElement = (
|
||||
width: itemSize,
|
||||
height: itemSize,
|
||||
stroke: '#374151',
|
||||
strokeWidth: 2,
|
||||
strokeWidth: 1.5,
|
||||
cornerRadius: 2,
|
||||
fill: 'white',
|
||||
});
|
||||
|
||||
@ -8,13 +8,24 @@ import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
|
||||
import type { TFieldMetaSchema } from '../../types/field-meta';
|
||||
import { renderCheckboxFieldElement } from './render-checkbox-field';
|
||||
import { renderDropdownFieldElement } from './render-dropdown-field';
|
||||
import { renderGenericTextFieldElement } from './render-generic-text-field';
|
||||
import { renderRadioFieldElement } from './render-radio-field';
|
||||
import { renderSignatureFieldElement } from './render-signature-field';
|
||||
import { renderTextFieldElement } from './render-text-field';
|
||||
|
||||
export const MIN_FIELD_HEIGHT_PX = 12;
|
||||
export const MIN_FIELD_WIDTH_PX = 36;
|
||||
|
||||
/**
|
||||
* The render type.
|
||||
*
|
||||
* @default 'edit'
|
||||
*
|
||||
* - `edit` - The field is rendered in editor page.
|
||||
* - `sign` - The field is rendered for the signing page.
|
||||
* - `export` - The field is rendered for exporting and sealing into the PDF. No backgrounds, interactive elements, etc.
|
||||
*/
|
||||
export type FieldRenderMode = 'edit' | 'sign' | 'export';
|
||||
|
||||
export type FieldToRender = Pick<
|
||||
Field,
|
||||
'envelopeItemId' | 'recipientId' | 'type' | 'page' | 'customText' | 'inserted' | 'recipientId'
|
||||
@ -25,7 +36,7 @@ export type FieldToRender = Pick<
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
fieldMeta?: TFieldMetaSchema | null;
|
||||
signature?: Signature | null;
|
||||
signature?: Pick<Signature, 'signatureImageAsBase64' | 'typedSignature'> | null;
|
||||
};
|
||||
|
||||
type RenderFieldOptions = {
|
||||
@ -38,16 +49,7 @@ type RenderFieldOptions = {
|
||||
|
||||
translations: Record<FieldType, string> | null;
|
||||
|
||||
/**
|
||||
* The render type.
|
||||
*
|
||||
* @default 'edit'
|
||||
*
|
||||
* - `edit` - The field is rendered in edit mode.
|
||||
* - `sign` - The field is rendered in sign mode. No interactive elements.
|
||||
* - `export` - The field is rendered in export mode. No backgrounds, interactive elements, etc.
|
||||
*/
|
||||
mode: 'edit' | 'sign' | 'export';
|
||||
mode: FieldRenderMode;
|
||||
|
||||
scale: number;
|
||||
editable?: boolean;
|
||||
@ -76,10 +78,21 @@ export const renderField = ({
|
||||
};
|
||||
|
||||
return match(field.type)
|
||||
.with(FieldType.TEXT, () => renderTextFieldElement(field, options))
|
||||
.with(
|
||||
FieldType.INITIALS,
|
||||
FieldType.NAME,
|
||||
FieldType.EMAIL,
|
||||
FieldType.DATE,
|
||||
FieldType.TEXT,
|
||||
FieldType.NUMBER,
|
||||
() => renderGenericTextFieldElement(field, options),
|
||||
)
|
||||
.with(FieldType.CHECKBOX, () => renderCheckboxFieldElement(field, options))
|
||||
.with(FieldType.RADIO, () => renderRadioFieldElement(field, options))
|
||||
.with(FieldType.DROPDOWN, () => renderDropdownFieldElement(field, options))
|
||||
.with(FieldType.SIGNATURE, () => renderSignatureFieldElement(field, options))
|
||||
.otherwise(() => renderTextFieldElement(field, options)); // Todo: Envelopes
|
||||
.with(FieldType.FREE_SIGNATURE, () => {
|
||||
throw new Error('Free signature fields are not supported');
|
||||
})
|
||||
.exhaustive();
|
||||
};
|
||||
|
||||
@ -12,6 +12,8 @@ import {
|
||||
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
|
||||
import { calculateFieldPosition } from './field-renderer';
|
||||
|
||||
const DEFAULT_TEXT_ALIGN = 'left';
|
||||
|
||||
const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Text => {
|
||||
const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options;
|
||||
|
||||
@ -31,8 +33,8 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
|
||||
// Calculate text positioning based on alignment
|
||||
const textX = 0;
|
||||
const textY = 0;
|
||||
let textAlign: 'left' | 'center' | 'right' = textMeta?.textAlign || 'left';
|
||||
let textVerticalAlign: 'top' | 'middle' | 'bottom' = 'top';
|
||||
let textAlign: 'left' | 'center' | 'right' = textMeta?.textAlign || DEFAULT_TEXT_ALIGN;
|
||||
const textVerticalAlign: 'top' | 'middle' | 'bottom' = 'middle';
|
||||
const textFontSize = textMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
|
||||
const textPadding = 10;
|
||||
|
||||
@ -40,51 +42,29 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
|
||||
|
||||
// Handle edit mode.
|
||||
if (mode === 'edit') {
|
||||
textToRender = fieldTypeName;
|
||||
textAlign = 'center';
|
||||
textVerticalAlign = 'middle';
|
||||
|
||||
if (textMeta?.label) {
|
||||
textToRender = textMeta.label;
|
||||
} else if (textMeta?.text) {
|
||||
if (textMeta?.text) {
|
||||
textToRender = textMeta.text;
|
||||
textAlign = textMeta.textAlign || 'center'; // Todo: Envelopes - What is the default
|
||||
|
||||
// Todo: Envelopes - Handle this on signatures
|
||||
if (textMeta.characterLimit) {
|
||||
textToRender = textToRender.slice(0, textMeta.characterLimit);
|
||||
}
|
||||
} else {
|
||||
// Show field name which is centered for the edit mode if no label/text is avaliable.
|
||||
textToRender = textMeta?.label || fieldTypeName;
|
||||
textAlign = 'center';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle sign mode.
|
||||
if (mode === 'sign' || mode === 'export') {
|
||||
textToRender = fieldTypeName;
|
||||
textAlign = 'center';
|
||||
textVerticalAlign = 'middle';
|
||||
|
||||
if (textMeta?.label) {
|
||||
textToRender = textMeta.label;
|
||||
}
|
||||
|
||||
if (textMeta?.text) {
|
||||
textToRender = textMeta.text;
|
||||
textAlign = textMeta.textAlign || 'center'; // Todo: Envelopes - What is the default
|
||||
|
||||
// Todo: Envelopes - Handle this on signatures
|
||||
if (textMeta.characterLimit) {
|
||||
textToRender = textToRender.slice(0, textMeta.characterLimit);
|
||||
if (!field.inserted) {
|
||||
if (textMeta?.text) {
|
||||
textToRender = textMeta.text;
|
||||
} else if (mode === 'sign') {
|
||||
// Only show the field name in sign mode if no text/label is avaliable.
|
||||
textToRender = textMeta?.label || fieldTypeName;
|
||||
textAlign = 'center';
|
||||
}
|
||||
}
|
||||
|
||||
if (field.inserted) {
|
||||
textToRender = field.customText;
|
||||
textAlign = textMeta?.textAlign || 'center'; // Todo: Envelopes - What is the default
|
||||
|
||||
// Todo: Envelopes - Handle this on signatures
|
||||
if (textMeta?.characterLimit) {
|
||||
textToRender = textToRender.slice(0, textMeta.characterLimit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,7 +86,7 @@ const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOption
|
||||
return fieldText;
|
||||
};
|
||||
|
||||
export const renderTextFieldElement = (
|
||||
export const renderGenericTextFieldElement = (
|
||||
field: FieldToRender,
|
||||
options: RenderFieldElementOptions,
|
||||
) => {
|
||||
@ -159,7 +159,7 @@ export const renderRadioFieldElement = (
|
||||
y: itemInputY,
|
||||
radius: calculateRadioSize(fontSize) / 2,
|
||||
stroke: '#374151',
|
||||
strokeWidth: 2,
|
||||
strokeWidth: 1.5,
|
||||
fill: 'white',
|
||||
});
|
||||
|
||||
|
||||
@ -7,7 +7,13 @@ export type GetFileOptions = {
|
||||
data: string;
|
||||
};
|
||||
|
||||
export const getFile = async ({ type, data }: GetFileOptions) => {
|
||||
/**
|
||||
* KEPT FOR POSTERITY, SHOULD BE REMOVED IN THE FUTURE
|
||||
* DO NOT USE OR I WILL FIRE YOU
|
||||
*
|
||||
* - Lucas, 2025-11-04
|
||||
*/
|
||||
const getFile = async ({ type, data }: GetFileOptions) => {
|
||||
return await match(type)
|
||||
.with(DocumentDataType.BYTES, () => getFileFromBytes(data))
|
||||
.with(DocumentDataType.BYTES_64, () => getFileFromBytes64(data))
|
||||
|
||||
@ -7,6 +7,7 @@ import { env } from '@documenso/lib/utils/env';
|
||||
|
||||
import { AppError } from '../../errors/app-error';
|
||||
import { createDocumentData } from '../../server-only/document-data/create-document-data';
|
||||
import { normalizePdf } from '../../server-only/pdf/normalize-pdf';
|
||||
import { uploadS3File } from './server-actions';
|
||||
|
||||
type File = {
|
||||
@ -43,6 +44,28 @@ export const putPdfFileServerSide = async (file: File) => {
|
||||
return await createDocumentData({ type, data });
|
||||
};
|
||||
|
||||
/**
|
||||
* Uploads a pdf file and normalizes it.
|
||||
*/
|
||||
export const putNormalizedPdfFileServerSide = async (file: File) => {
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
const normalized = await normalizePdf(buffer);
|
||||
|
||||
const fileName = file.name.endsWith('.pdf') ? file.name : `${file.name}.pdf`;
|
||||
|
||||
const documentData = await putFileServerSide({
|
||||
name: fileName,
|
||||
type: 'application/pdf',
|
||||
arrayBuffer: async () => Promise.resolve(normalized),
|
||||
});
|
||||
|
||||
return await createDocumentData({
|
||||
type: documentData.type,
|
||||
data: documentData.data,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Uploads a file to the appropriate storage location.
|
||||
*/
|
||||
|
||||
@ -6,7 +6,7 @@ import { env } from '@documenso/lib/utils/env';
|
||||
import type {
|
||||
TGetPresignedPostUrlResponse,
|
||||
TUploadPdfResponse,
|
||||
} from '@documenso/remix/server/api/files.types';
|
||||
} from '@documenso/remix/server/api/files/files.types';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||
import { AppError } from '../../errors/app-error';
|
||||
|
||||
34
packages/lib/utils/envelope-download.ts
Normal file
34
packages/lib/utils/envelope-download.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import type { EnvelopeItem } from '@prisma/client';
|
||||
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
|
||||
|
||||
export type EnvelopeItemPdfUrlOptions =
|
||||
| {
|
||||
type: 'download';
|
||||
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
|
||||
token: string | undefined;
|
||||
version: 'original' | 'signed';
|
||||
}
|
||||
| {
|
||||
type: 'view';
|
||||
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
|
||||
token: string | undefined;
|
||||
};
|
||||
|
||||
export const getEnvelopeItemPdfUrl = (options: EnvelopeItemPdfUrlOptions) => {
|
||||
const { envelopeItem, token, type } = options;
|
||||
|
||||
const { id, envelopeId } = envelopeItem;
|
||||
|
||||
if (type === 'download') {
|
||||
const version = options.version;
|
||||
|
||||
return token
|
||||
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}/download/${version}`
|
||||
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}/download/${version}`;
|
||||
}
|
||||
|
||||
return token
|
||||
? `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/token/${token}/envelopeItem/${id}`
|
||||
: `${NEXT_PUBLIC_WEBAPP_URL()}/api/files/envelope/${envelopeId}/envelopeItem/${id}`;
|
||||
};
|
||||
@ -104,7 +104,6 @@ export const extractFieldInsertionValues = ({
|
||||
const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateNumberField(fieldValue.value.toString(), numberFieldParsedMeta, true);
|
||||
|
||||
// Todo
|
||||
if (errors.length > 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid number',
|
||||
@ -127,7 +126,6 @@ export const extractFieldInsertionValues = ({
|
||||
const parsedTextFieldMeta = ZTextFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateTextField(fieldValue.value, parsedTextFieldMeta, true);
|
||||
|
||||
// Todo
|
||||
if (errors.length > 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid email',
|
||||
@ -189,7 +187,6 @@ export const extractFieldInsertionValues = ({
|
||||
(sign) => sign.label === validationRule,
|
||||
);
|
||||
|
||||
// Todo: Envelopes - Test this.
|
||||
if (checkboxValidationRule) {
|
||||
const isValid = validateCheckboxLength(
|
||||
selectedValues.length,
|
||||
@ -224,7 +221,6 @@ export const extractFieldInsertionValues = ({
|
||||
const parsedDropdownFieldMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
|
||||
const errors = validateDropdownField(fieldValue.value, parsedDropdownFieldMeta, true);
|
||||
|
||||
// Todo: Envelopes
|
||||
if (errors.length > 0) {
|
||||
throw new AppError(AppErrorCode.INVALID_BODY, {
|
||||
message: 'Invalid dropdown value',
|
||||
|
||||
@ -81,6 +81,10 @@ export const mapFieldToLegacyField = (
|
||||
};
|
||||
|
||||
export const parseCheckboxCustomText = (customText: string): number[] => {
|
||||
if (!customText) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return JSON.parse(customText);
|
||||
};
|
||||
|
||||
|
||||
37
packages/lib/utils/is-valid-return-to.ts
Normal file
37
packages/lib/utils/is-valid-return-to.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||
|
||||
export const isValidReturnTo = (returnTo?: string) => {
|
||||
if (!returnTo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Decode if it's URL encoded
|
||||
const decodedReturnTo = decodeURIComponent(returnTo);
|
||||
const returnToUrl = new URL(decodedReturnTo, NEXT_PUBLIC_WEBAPP_URL());
|
||||
|
||||
if (returnToUrl.origin !== NEXT_PUBLIC_WEBAPP_URL()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const normalizeReturnTo = (returnTo?: string) => {
|
||||
if (!returnTo) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
// Decode if it's URL encoded
|
||||
const decodedReturnTo = decodeURIComponent(returnTo);
|
||||
const returnToUrl = new URL(decodedReturnTo, NEXT_PUBLIC_WEBAPP_URL());
|
||||
|
||||
return `${returnToUrl.pathname}${returnToUrl.search}${returnToUrl.hash}`;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user