Compare commits

..

1 Commits

Author SHA1 Message Date
5a238d99d8 fix: single signer wording 2025-05-10 21:34:15 +00:00
27 changed files with 108 additions and 636 deletions

View File

@ -173,59 +173,34 @@ export const ConfigureFieldsView = ({
});
const onFieldCopy = useCallback(
(event?: KeyboardEvent | null, options?: { duplicate?: boolean; duplicateAll?: boolean }) => {
const { duplicate = false, duplicateAll = false } = options ?? {};
(event?: KeyboardEvent | null, options?: { duplicate?: boolean }) => {
const { duplicate = false } = options ?? {};
if (lastActiveField) {
event?.preventDefault();
if (duplicate) {
const newField: TConfigureFieldsFormSchema['fields'][0] = {
...structuredClone(lastActiveField),
nativeId: undefined,
formId: nanoid(12),
signerEmail: selectedRecipient?.email ?? lastActiveField.signerEmail,
recipientId: selectedRecipient?.id ?? lastActiveField.recipientId,
pageX: lastActiveField.pageX + 3,
pageY: lastActiveField.pageY + 3,
};
if (!duplicate) {
setFieldClipboard(lastActiveField);
append(newField);
return;
}
if (duplicateAll) {
const pages = Array.from(document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR));
pages.forEach((_, index) => {
const pageNumber = index + 1;
if (pageNumber === lastActiveField.pageNumber) {
return;
}
const newField: TConfigureFieldsFormSchema['fields'][0] = {
...structuredClone(lastActiveField),
nativeId: undefined,
formId: nanoid(12),
signerEmail: selectedRecipient?.email ?? lastActiveField.signerEmail,
recipientId: selectedRecipient?.id ?? lastActiveField.recipientId,
pageNumber,
};
append(newField);
toast({
title: 'Copied field',
description: 'Copied field to clipboard',
});
return;
}
setFieldClipboard(lastActiveField);
const newField: TConfigureFieldsFormSchema['fields'][0] = {
...structuredClone(lastActiveField),
nativeId: undefined,
formId: nanoid(12),
signerEmail: selectedRecipient?.email ?? lastActiveField.signerEmail,
recipientId: selectedRecipient?.id ?? lastActiveField.recipientId,
pageX: lastActiveField.pageX + 3,
pageY: lastActiveField.pageY + 3,
};
toast({
title: 'Copied field',
description: 'Copied field to clipboard',
});
append(newField);
}
},
[append, lastActiveField, selectedRecipient?.email, selectedRecipient?.id, toast],
@ -558,7 +533,6 @@ export const ConfigureFieldsView = ({
onMove={(node) => onFieldMove(node, index)}
onRemove={() => remove(index)}
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
onDuplicateAllPages={() => onFieldCopy(null, { duplicateAll: true })}
onFocus={() => setLastActiveField(field)}
onBlur={() => setLastActiveField(null)}
onAdvancedSettings={() => {

View File

@ -44,7 +44,6 @@ const ZTeamDocumentPreferencesFormSchema = z.object({
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES),
includeSenderDetails: z.boolean(),
includeSigningCertificate: z.boolean(),
includeAuditLog: z.boolean(),
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(1, {
message: msg`At least one signature type must be enabled`.id,
}),
@ -78,7 +77,6 @@ export const TeamDocumentPreferencesForm = ({
: 'en',
includeSenderDetails: settings?.includeSenderDetails ?? false,
includeSigningCertificate: settings?.includeSigningCertificate ?? true,
includeAuditLog: settings?.includeAuditLog ?? false,
signatureTypes: extractTeamSignatureSettings(settings),
},
resolver: zodResolver(ZTeamDocumentPreferencesFormSchema),
@ -93,7 +91,6 @@ export const TeamDocumentPreferencesForm = ({
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
includeAuditLog,
signatureTypes,
} = data;
@ -104,7 +101,6 @@ export const TeamDocumentPreferencesForm = ({
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
includeAuditLog,
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
@ -311,7 +307,7 @@ export const TeamDocumentPreferencesForm = ({
<FormDescription>
<Trans>
Controls whether the signing certificate will be included with the document when
Controls whether the signing certificate will be included in the document when
it is downloaded. The signing certificate can still be downloaded from the logs
page separately.
</Trans>
@ -320,36 +316,6 @@ export const TeamDocumentPreferencesForm = ({
)}
/>
<FormField
control={form.control}
name="includeAuditLog"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>
<Trans>Include the Audit Log in the Document</Trans>
</FormLabel>
<div>
<FormControl className="block">
<Switch
ref={field.ref}
name={field.name}
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</div>
<FormDescription>
<Trans>
Controls whether the audit log will be included with the document when it is
downloaded. The audit log can still be downloaded from the logs page separately.
</Trans>
</FormDescription>
</FormItem>
)}
/>
<div className="flex flex-row justify-end space-x-4">
<Button type="submit" loading={form.formState.isSubmitting}>
<Trans>Update</Trans>

View File

@ -12,14 +12,7 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
import { isDocumentCompleted } from '@documenso/lib/utils/document';
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc as trpcClient } from '@documenso/trpc/client';
import { trpc } from '@documenso/trpc/react';
import { Button } from '@documenso/ui/primitives/button';
import {
SplitButton,
SplitButtonAction,
SplitButtonDropdown,
SplitButtonDropdownItem,
} from '@documenso/ui/primitives/split-button';
import { useToast } from '@documenso/ui/primitives/use-toast';
export type DocumentPageViewButtonProps = {
@ -49,12 +42,6 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
? `${documentsPath}/f/${document.folderId}/${document.id}/edit`
: `${documentsPath}/${document.id}/edit`;
const { mutateAsync: downloadCertificate, isPending: isDownloadingCertificate } =
trpc.document.downloadCertificate.useMutation();
const { mutateAsync: downloadAuditLogs, isPending: isDownloadingAuditLogs } =
trpc.document.downloadAuditLogs.useMutation();
const onDownloadClick = async () => {
try {
const documentWithData = await trpcClient.document.getDocumentById.query(
@ -84,125 +71,6 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
}
};
const onDownloadOriginalClick = async () => {
try {
const documentWithData = await trpcClient.document.getDocumentById.query(
{
documentId: document.id,
},
{
context: {
teamId: document.team?.id?.toString(),
},
},
);
const documentData = documentWithData?.documentData;
if (!documentData) {
throw new Error('No document available');
}
await downloadPDF({ documentData, fileName: documentWithData.title, version: 'original' });
} catch (err) {
toast({
title: _(msg`Something went wrong`),
description: _(msg`An error occurred while downloading your document.`),
variant: 'destructive',
});
}
};
const onDownloadCertificateClick = async () => {
try {
const { url } = await downloadCertificate({ documentId: document.id });
const iframe = Object.assign(window.document.createElement('iframe'), {
src: url,
});
Object.assign(iframe.style, {
position: 'fixed',
top: '0',
left: '0',
width: '0',
height: '0',
});
const onLoaded = () => {
if (iframe.contentDocument?.readyState === 'complete') {
iframe.contentWindow?.print();
iframe.contentWindow?.addEventListener('afterprint', () => {
window.document.body.removeChild(iframe);
});
}
};
// When the iframe has loaded, print the iframe and remove it from the dom
iframe.addEventListener('load', onLoaded);
window.document.body.appendChild(iframe);
onLoaded();
} catch (error) {
console.error(error);
toast({
title: _(msg`Something went wrong`),
description: _(
msg`Sorry, we were unable to download the certificate. Please try again later.`,
),
variant: 'destructive',
});
}
};
const onDownloadAuditLogClick = async () => {
try {
const { url } = await downloadAuditLogs({ documentId: document.id });
const iframe = Object.assign(window.document.createElement('iframe'), {
src: url,
});
Object.assign(iframe.style, {
position: 'fixed',
top: '0',
left: '0',
width: '0',
height: '0',
});
const onLoaded = () => {
if (iframe.contentDocument?.readyState === 'complete') {
iframe.contentWindow?.print();
iframe.contentWindow?.addEventListener('afterprint', () => {
window.document.body.removeChild(iframe);
});
}
};
// When the iframe has loaded, print the iframe and remove it from the dom
iframe.addEventListener('load', onLoaded);
window.document.body.appendChild(iframe);
onLoaded();
} catch (error) {
console.error(error);
toast({
title: _(msg`Something went wrong`),
description: _(
msg`Sorry, we were unable to download the audit logs. Please try again later.`,
),
variant: 'destructive',
});
}
};
return match({
isRecipient,
isPending,
@ -242,26 +110,10 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
</Button>
))
.with({ isComplete: true }, () => (
<SplitButton className="w-full">
<SplitButtonAction onClick={onDownloadClick}>
<Download className="-ml-1 mr-2 inline h-4 w-4" />
<Trans>Download</Trans>
</SplitButtonAction>
<SplitButtonDropdown>
<SplitButtonDropdownItem onClick={onDownloadOriginalClick}>
<Trans>Download Original Document</Trans>
</SplitButtonDropdownItem>
<SplitButtonDropdownItem onClick={onDownloadCertificateClick}>
<Trans>Download Document Certificate</Trans>
</SplitButtonDropdownItem>
<SplitButtonDropdownItem onClick={onDownloadAuditLogClick}>
<Trans>Download Audit Log</Trans>
</SplitButtonDropdownItem>
</SplitButtonDropdown>
</SplitButton>
<Button className="w-full" onClick={onDownloadClick}>
<Download className="-ml-1 mr-2 inline h-4 w-4" />
<Trans>Download</Trans>
</Button>
))
.otherwise(() => null);
};

View File

@ -10,8 +10,6 @@ import {
Download,
Edit,
EyeIcon,
FileDown,
FolderInput,
Loader,
MoreHorizontal,
MoveRight,
@ -184,7 +182,7 @@ export const DocumentsTableActionDropdown = ({
</DropdownMenuItem>
<DropdownMenuItem onClick={onDownloadOriginalClick}>
<FileDown className="mr-2 h-4 w-4" />
<Download className="mr-2 h-4 w-4" />
<Trans>Download Original</Trans>
</DropdownMenuItem>
@ -203,7 +201,7 @@ export const DocumentsTableActionDropdown = ({
{onMoveDocument && (
<DropdownMenuItem onClick={onMoveDocument} onSelect={(e) => e.preventDefault()}>
<FolderInput className="mr-2 h-4 w-4" />
<MoveRight className="mr-2 h-4 w-4" />
<Trans>Move to Folder</Trans>
</DropdownMenuItem>
)}

View File

@ -69,6 +69,8 @@ export const TemplatesTableActionDropdown = ({
? `${templateRootPath}/f/${row.folderId}/${row.id}/edit`
: `${templateRootPath}/${row.id}/edit`;
return (
<DropdownMenu>
<DropdownMenuTrigger data-testid="template-table-action-btn">

View File

@ -1,6 +1,6 @@
import { useLingui } from '@lingui/react';
import { Plural, Trans } from '@lingui/react/macro';
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
import { DocumentStatus, SigningStatus, TeamMemberRole } from '@prisma/client';
import { ChevronLeft, Clock9, Users2 } from 'lucide-react';
import { Link, redirect } from 'react-router';
import { match } from 'ts-pattern';
@ -111,10 +111,25 @@ export async function loader({ params, request }: Route.LoaderArgs) {
recipients,
};
let isSingleSignerDocument = false;
if (
documentWithRecipients.status === DocumentStatus.PENDING &&
documentWithRecipients.recipients.length === 1
) {
const singleRecipient = documentWithRecipients.recipients[0];
if (
singleRecipient.email === user.email &&
singleRecipient.signingStatus === SigningStatus.SIGNED
) {
isSingleSignerDocument = true;
}
}
return superLoaderJson({
document: documentWithRecipients,
documentRootPath,
fields,
isSingleSignerDocument,
});
}
@ -124,7 +139,7 @@ export default function DocumentPage() {
const { _ } = useLingui();
const { user } = useSession();
const { document, documentRootPath, fields } = loaderData;
const { document, documentRootPath, fields, isSingleSignerDocument } = loaderData;
const { recipients, documentData, documentMeta } = document;
@ -237,6 +252,10 @@ export default function DocumentPage() {
<Trans>This document is currently a draft and has not been sent</Trans>
))
.with(DocumentStatus.PENDING, () => {
if (isSingleSignerDocument) {
return <Trans>This document has been signed and is being finalized.</Trans>;
}
const pendingRecipients = recipients.filter(
(recipient) => recipient.signingStatus === 'NOT_SIGNED',
);

View File

@ -119,7 +119,7 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
return (
<div
className={cn(
'-mx-4 flex flex-col items-center overflow-hidden px-4 pt-24 md:-mx-8 md:px-8 lg:pt-36 xl:pt-44',
'-mx-4 flex flex-col items-center overflow-x-hidden px-4 pt-24 md:-mx-8 md:px-8 lg:pt-36 xl:pt-44',
{ 'pt-0 lg:pt-0 xl:pt-0': canSignUp },
)}
>

View File

@ -11,7 +11,6 @@ import {
ZDeleteDocumentMutationSchema,
ZDeleteFieldMutationSchema,
ZDeleteRecipientMutationSchema,
ZDownloadDocumentQuerySchema,
ZDownloadDocumentSuccessfulSchema,
ZFindTeamMembersResponseSchema,
ZGenerateDocumentFromTemplateMutationResponseSchema,
@ -72,7 +71,6 @@ export const ApiContractV1 = c.router(
downloadSignedDocument: {
method: 'GET',
path: '/api/v1/documents/:id/download',
query: ZDownloadDocumentQuerySchema,
responses: {
200: ZDownloadDocumentSuccessfulSchema,
401: ZUnsuccessfulResponseSchema,

View File

@ -142,7 +142,6 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
downloadSignedDocument: authenticatedMiddleware(async (args, user, team) => {
const { id: documentId } = args.params;
const { downloadOriginalDocument } = args.query;
try {
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
@ -178,7 +177,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
};
}
if (!downloadOriginalDocument && !isDocumentCompleted(document.status)) {
if (!isDocumentCompleted(document.status)) {
return {
status: 400,
body: {
@ -187,9 +186,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
};
}
const { url } = await getPresignGetUrl(
downloadOriginalDocument ? document.documentData.data : document.documentData.initialData,
);
const { url } = await getPresignGetUrl(document.documentData.data);
return {
status: 200,

View File

@ -119,15 +119,6 @@ export const ZUploadDocumentSuccessfulSchema = z.object({
key: z.string(),
});
export const ZDownloadDocumentQuerySchema = z.object({
downloadOriginalDocument: z
.preprocess((val) => String(val) === 'true' || String(val) === '1', z.boolean())
.optional()
.default(false),
});
export type TDownloadDocumentQuerySchema = z.infer<typeof ZDownloadDocumentQuerySchema>;
export const ZDownloadDocumentSuccessfulSchema = z.object({
downloadUrl: z.string(),
});

View File

@ -16,7 +16,6 @@ const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
documentLanguage: z.string(),
includeSenderDetails: z.boolean(),
includeSigningCertificate: z.boolean(),
includeAuditLog: z.boolean(),
brandingEnabled: z.boolean(),
brandingLogo: z.string(),
brandingUrl: z.string(),

View File

@ -9,7 +9,6 @@ import { signPdf } from '@documenso/signing';
import { AppError, AppErrorCode } from '../../../errors/app-error';
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client';
import { getAuditLogPdf } from '../../../server-only/htmltopdf/get-audit-log-pdf';
import { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
import { addRejectionStampToPdf } from '../../../server-only/pdf/add-rejection-stamp-to-pdf';
import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
@ -53,7 +52,6 @@ export const run = async ({
teamGlobalSettings: {
select: {
includeSigningCertificate: true,
includeAuditLog: true,
},
},
},
@ -154,13 +152,6 @@ export const run = async ({
}).catch(() => null)
: null;
const auditLogData =
(document.team?.teamGlobalSettings?.includeAuditLog ?? false)
? await getAuditLogPdf({
documentId,
}).catch(() => null)
: null;
const newDataId = await io.runTask('decorate-and-sign-pdf', async () => {
const pdfDoc = await PDFDocument.load(pdfData);
@ -187,16 +178,6 @@ export const run = async ({
});
}
if (auditLogData) {
const auditLogDoc = await PDFDocument.load(auditLogData);
const auditLogPages = await pdfDoc.copyPages(auditLogDoc, auditLogDoc.getPageIndices());
auditLogPages.forEach((page) => {
pdfDoc.addPage(page);
});
}
for (const field of fields) {
if (field.inserted) {
document.useLegacyFieldInsertion

View File

@ -1,14 +1,9 @@
import { DocumentSource, type Prisma, WebhookTriggerEvents } from '@prisma/client';
import { DocumentSource, type Prisma } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import {
ZWebhookDocumentSchema,
mapDocumentToWebhookDocumentPayload,
} from '../../types/webhook-payload';
import { prefixedId } from '../../universal/id';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
import { getDocumentWhereInput } from './get-document-by-id';
export interface DuplicateDocumentOptions {
@ -91,24 +86,7 @@ export const duplicateDocument = async ({
};
}
const createdDocument = await prisma.document.create({
...createDocumentArguments,
include: {
recipients: true,
documentMeta: true,
},
});
await triggerWebhook({
event: WebhookTriggerEvents.DOCUMENT_CREATED,
data: ZWebhookDocumentSchema.parse({
...mapDocumentToWebhookDocumentPayload(createdDocument),
recipients: createdDocument.recipients,
documentMeta: createdDocument.documentMeta,
}),
userId: userId,
teamId: teamId,
});
const createdDocument = await prisma.document.create(createDocumentArguments);
return {
documentId: createdDocument.id,

View File

@ -17,7 +17,6 @@ import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getFileServerSide } from '../../universal/upload/get-file.server';
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers';
import { getAuditLogPdf } from '../htmltopdf/get-audit-log-pdf';
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
import { addRejectionStampToPdf } from '../pdf/add-rejection-stamp-to-pdf';
import { flattenAnnotations } from '../pdf/flatten-annotations';
@ -54,7 +53,6 @@ export const sealDocument = async ({
teamGlobalSettings: {
select: {
includeSigningCertificate: true,
includeAuditLog: true,
},
},
},
@ -126,13 +124,6 @@ export const sealDocument = async ({
}).catch(() => null)
: null;
const auditLogData =
(document.team?.teamGlobalSettings?.includeAuditLog ?? false)
? await getAuditLogPdf({
documentId,
}).catch(() => null)
: null;
const doc = await PDFDocument.load(pdfData);
// Normalize and flatten layers that could cause issues with the signature
@ -155,16 +146,6 @@ export const sealDocument = async ({
});
}
if (auditLogData) {
const auditLog = await PDFDocument.load(auditLogData);
const auditLogPages = await doc.copyPages(auditLog, auditLog.getPageIndices());
auditLogPages.forEach((page) => {
doc.addPage(page);
});
}
for (const field of fields) {
document.useLegacyFieldInsertion
? await legacy_insertFieldInPDF(doc, field)

View File

@ -1,69 +0,0 @@
import { DateTime } from 'luxon';
import type { Browser } from 'playwright';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { type SupportedLanguageCodes, isValidLanguageCode } from '../../constants/i18n';
import { env } from '../../utils/env';
import { encryptSecondaryData } from '../crypto/encrypt';
export type GetAuditLogPdfOptions = {
documentId: number;
// eslint-disable-next-line @typescript-eslint/ban-types
language?: SupportedLanguageCodes | (string & {});
};
export const getAuditLogPdf = async ({ documentId, language }: GetAuditLogPdfOptions) => {
const { chromium } = await import('playwright');
const encryptedId = encryptSecondaryData({
data: documentId.toString(),
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
});
let browser: Browser;
const browserlessUrl = env('NEXT_PRIVATE_BROWSERLESS_URL');
if (browserlessUrl) {
// !: Use CDP rather than the default `connect` method to avoid coupling to the playwright version.
// !: Previously we would have to keep the playwright version in sync with the browserless version to avoid errors.
browser = await chromium.connectOverCDP(browserlessUrl);
} else {
browser = await chromium.launch();
}
if (!browser) {
throw new Error(
'Failed to establish a browser, please ensure you have either a Browserless.io url or chromium browser installed',
);
}
const browserContext = await browser.newContext();
const page = await browserContext.newPage();
const lang = isValidLanguageCode(language) ? language : 'en';
await page.context().addCookies([
{
name: 'language',
value: lang,
url: NEXT_PUBLIC_WEBAPP_URL(),
},
]);
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/audit-log?d=${encryptedId}`, {
waitUntil: 'networkidle',
timeout: 10_000,
});
const result = await page.pdf({
format: 'A4',
});
await browserContext.close();
void browser.close();
return result;
};

View File

@ -9,13 +9,11 @@ import {
SigningStatus,
WebhookTriggerEvents,
} from '@prisma/client';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../../constants/date-formats';
import type { SupportedLanguageCodes } from '../../constants/i18n';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
@ -510,8 +508,10 @@ export const createDocumentFromTemplate = async ({
fieldsToCreate = fieldsToCreate.concat(
fields.map((field) => {
const prefillField = prefillFields?.find((value) => value.id === field.id);
// Use type assertion to help TypeScript understand the structure
const updatedFieldMeta = getUpdatedFieldMeta(field, prefillField);
const payload = {
return {
documentId: document.id,
recipientId: recipient.id,
type: field.type,
@ -522,38 +522,8 @@ export const createDocumentFromTemplate = async ({
height: field.height,
customText: '',
inserted: false,
fieldMeta: field.fieldMeta,
fieldMeta: updatedFieldMeta,
};
if (prefillField) {
match(prefillField)
.with({ type: 'date' }, (selector) => {
if (!selector.value) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Date value is required for field ${field.id}`,
});
}
const date = new Date(selector.value);
if (isNaN(date.getTime())) {
throw new AppError(AppErrorCode.INVALID_BODY, {
message: `Invalid date value for field ${field.id}: ${selector.value}`,
});
}
payload.customText = DateTime.fromJSDate(date).toFormat(
template.templateMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
);
payload.inserted = true;
})
.otherwise((selector) => {
payload.fieldMeta = getUpdatedFieldMeta(field, selector);
});
}
return payload;
}),
);
});

View File

@ -155,10 +155,6 @@ export const ZFieldMetaPrefillFieldsSchema = z
label: z.string().optional(),
value: z.string().optional(),
}),
z.object({
type: z.literal('date'),
value: z.string().optional(),
}),
]),
);

View File

@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "TeamGlobalSettings" ADD COLUMN "includeAuditLog" BOOLEAN NOT NULL DEFAULT false;

View File

@ -582,13 +582,11 @@ enum TeamMemberInviteStatus {
}
model TeamGlobalSettings {
teamId Int @unique
documentVisibility DocumentVisibility @default(EVERYONE)
documentLanguage String @default("en")
includeSenderDetails Boolean @default(true)
includeSigningCertificate Boolean @default(true)
includeAuditLog Boolean @default(false)
teamId Int @unique
documentVisibility DocumentVisibility @default(EVERYONE)
documentLanguage String @default("en")
includeSenderDetails Boolean @default(true)
includeSigningCertificate Boolean @default(true)
typedSignatureEnabled Boolean @default(true)
uploadSignatureEnabled Boolean @default(true)

View File

@ -23,7 +23,6 @@ export const updateTeamDocumentSettingsRoute = authenticatedProcedure
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
includeAuditLog,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
@ -55,7 +54,6 @@ export const updateTeamDocumentSettingsRoute = authenticatedProcedure
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
includeAuditLog,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
@ -65,7 +63,6 @@ export const updateTeamDocumentSettingsRoute = authenticatedProcedure
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
includeAuditLog,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,

View File

@ -14,7 +14,6 @@ export const ZUpdateTeamDocumentSettingsRequestSchema = z.object({
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).optional().default('en'),
includeSenderDetails: z.boolean().optional().default(false),
includeSigningCertificate: z.boolean().optional().default(true),
includeAuditLog: z.boolean().optional().default(false),
typedSignatureEnabled: z.boolean().optional().default(true),
uploadSignatureEnabled: z.boolean().optional().default(true),
drawSignatureEnabled: z.boolean().optional().default(true),

View File

@ -24,7 +24,7 @@ export const SigningCard = ({
signingCelebrationImage,
}: SigningCardProps) => {
return (
<div className={cn('relative w-full max-w-sm md:max-w-md', className)}>
<div className={cn('relative w-full max-w-xs md:max-w-sm', className)}>
<SigningCardContent name={name} signature={signature} />
{signingCelebrationImage && (
@ -48,7 +48,7 @@ export const SigningCard3D = ({
const [trackMouse, setTrackMouse] = useState(false);
const timeoutRef = useRef<number | undefined>();
const timeoutRef = useRef<NodeJS.Timeout>();
const cardX = useMotionValue(0);
const cardY = useMotionValue(0);
@ -103,7 +103,7 @@ export const SigningCard3D = ({
clearTimeout(timeoutRef.current);
// Revert the card back to the center position after the mouse stops moving.
timeoutRef.current = window.setTimeout(() => {
timeoutRef.current = setTimeout(() => {
void animate(cardX, 0, { duration: 2, ease: 'backInOut' });
void animate(cardY, 0, { duration: 2, ease: 'backInOut' });
@ -120,15 +120,12 @@ export const SigningCard3D = ({
return () => {
window.removeEventListener('mousemove', onMouseMove);
if (timeoutRef.current) {
window.clearTimeout(timeoutRef.current);
}
};
}, [onMouseMove]);
return (
<div
className={cn('relative w-full max-w-sm md:max-w-md', className)}
className={cn('relative w-full max-w-xs md:max-w-sm', className)}
style={{ perspective: 800 }}
>
<motion.div

View File

@ -400,60 +400,35 @@ export const AddFieldsFormPartial = ({
);
const onFieldCopy = useCallback(
(event?: KeyboardEvent | null, options?: { duplicate?: boolean; duplicateAll?: boolean }) => {
const { duplicate = false, duplicateAll = false } = options ?? {};
(event?: KeyboardEvent | null, options?: { duplicate?: boolean }) => {
const { duplicate = false } = options ?? {};
if (lastActiveField) {
event?.preventDefault();
if (duplicate) {
const newField: TAddFieldsFormSchema['fields'][0] = {
...structuredClone(lastActiveField),
nativeId: undefined,
formId: nanoid(12),
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
pageX: lastActiveField.pageX + 3,
pageY: lastActiveField.pageY + 3,
};
if (!duplicate) {
setFieldClipboard(lastActiveField);
append(newField);
return;
}
if (duplicateAll) {
const pages = Array.from(document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR));
pages.forEach((_, index) => {
const pageNumber = index + 1;
if (pageNumber === lastActiveField.pageNumber) {
return;
}
const newField: TAddFieldsFormSchema['fields'][0] = {
...structuredClone(lastActiveField),
nativeId: undefined,
formId: nanoid(12),
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
pageNumber,
};
append(newField);
toast({
title: 'Copied field',
description: 'Copied field to clipboard',
});
return;
}
setFieldClipboard(lastActiveField);
const newField: TAddFieldsFormSchema['fields'][0] = {
...structuredClone(lastActiveField),
formId: nanoid(12),
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
pageX: lastActiveField.pageX + 3,
pageY: lastActiveField.pageY + 3,
};
toast({
title: 'Copied field',
description: 'Copied field to clipboard',
});
append(newField);
}
},
[append, lastActiveField, selectedSigner?.email, selectedSigner?.id, toast],
[append, lastActiveField, selectedSigner?.email, toast],
);
const onFieldPaste = useCallback(
@ -666,7 +641,6 @@ export const AddFieldsFormPartial = ({
onMove={(options) => onFieldMove(options, index)}
onRemove={() => remove(index)}
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
onDuplicateAllPages={() => onFieldCopy(null, { duplicateAll: true })}
onAdvancedSettings={() => {
setCurrentField(field);
handleAdvancedSettings();

View File

@ -311,7 +311,6 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
/>
))
.otherwise(() => null)}
{errors.length > 0 && (
<div className="mt-4">
<ul>
@ -324,7 +323,6 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
</div>
)}
</DocumentFlowFormContainerContent>
<DocumentFlowFormContainerFooter className="mt-auto">
<DocumentFlowFormContainerActions
goNextLabel={msg`Save`}

View File

@ -1,9 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { FieldType } from '@prisma/client';
import { CopyPlus, Settings2, SquareStack, Trash } from 'lucide-react';
import { CopyPlus, Settings2, Trash } from 'lucide-react';
import { createPortal } from 'react-dom';
import { Rnd } from 'react-rnd';
@ -31,7 +29,6 @@ export type FieldItemProps = {
onMove?: (_node: HTMLElement) => void;
onRemove?: () => void;
onDuplicate?: () => void;
onDuplicateAllPages?: () => void;
onAdvancedSettings?: () => void;
onFocus?: () => void;
onBlur?: () => void;
@ -58,18 +55,15 @@ export const FieldItem = ({
onMove,
onRemove,
onDuplicate,
onDuplicateAllPages,
onAdvancedSettings,
onFocus,
onBlur,
onAdvancedSettings,
recipientIndex = 0,
hasErrors,
active,
onFieldActivate,
onFieldDeactivate,
}: FieldItemProps) => {
const { _ } = useLingui();
const [coords, setCoords] = useState({
pageX: 0,
pageY: 0,
@ -310,7 +304,6 @@ export const FieldItem = ({
<div className="group flex items-center justify-evenly gap-x-1 rounded-md border bg-gray-900 p-0.5">
{advancedField && (
<button
title={_(msg`Advanced settings`)}
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
onClick={onAdvancedSettings}
onTouchEnd={onAdvancedSettings}
@ -320,7 +313,6 @@ export const FieldItem = ({
)}
<button
title={_(msg`Duplicate`)}
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
onClick={onDuplicate}
onTouchEnd={onDuplicate}
@ -329,16 +321,6 @@ export const FieldItem = ({
</button>
<button
title={_(msg`Duplicate on all pages`)}
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
onClick={onDuplicateAllPages}
onTouchEnd={onDuplicateAllPages}
>
<SquareStack className="h-3 w-3" />
</button>
<button
title={_(msg`Remove`)}
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
onClick={onRemove}
onTouchEnd={onRemove}

View File

@ -1,83 +0,0 @@
import * as React from 'react';
import { ChevronDown } from 'lucide-react';
import { cn } from '../lib/utils';
import { Button } from './button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from './dropdown-menu';
const SplitButtonContext = React.createContext<{
variant?: React.ComponentProps<typeof Button>['variant'];
size?: React.ComponentProps<typeof Button>['size'];
}>({});
const SplitButton = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & {
variant?: React.ComponentProps<typeof Button>['variant'];
size?: React.ComponentProps<typeof Button>['size'];
}
>(({ className, children, variant = 'default', size = 'default', ...props }, ref) => {
return (
<SplitButtonContext.Provider value={{ variant, size }}>
<div ref={ref} className={cn('inline-flex', className)} {...props}>
{children}
</div>
</SplitButtonContext.Provider>
);
});
SplitButton.displayName = 'SplitButton';
const SplitButtonAction = React.forwardRef<
HTMLButtonElement,
React.ButtonHTMLAttributes<HTMLButtonElement>
>(({ className, children, ...props }, ref) => {
const { variant, size } = React.useContext(SplitButtonContext);
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn('flex-1 rounded-r-none border-r-0', className)}
{...props}
>
{children}
</Button>
);
});
SplitButtonAction.displayName = 'SplitButtonAction';
const SplitButtonDropdown = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ children, ...props }, ref) => {
const { variant, size } = React.useContext(SplitButtonContext);
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant={variant}
size={size}
className="rounded-l-none px-2 focus-visible:ring-offset-0"
>
<ChevronDown className="h-4 w-4" />
<span className="sr-only">More options</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" {...props} ref={ref}>
{children}
</DropdownMenuContent>
</DropdownMenu>
);
},
);
SplitButtonDropdown.displayName = 'SplitButtonDropdown';
const SplitButtonDropdownItem = DropdownMenuItem;
export { SplitButton, SplitButtonAction, SplitButtonDropdown, SplitButtonDropdownItem };

View File

@ -139,64 +139,44 @@ export const AddTemplateFieldsFormPartial = ({
);
const onFieldCopy = useCallback(
(event?: KeyboardEvent | null, options?: { duplicate?: boolean; duplicateAll?: boolean }) => {
const { duplicate = false, duplicateAll = false } = options ?? {};
(event?: KeyboardEvent | null, options?: { duplicate?: boolean }) => {
const { duplicate = false } = options ?? {};
if (lastActiveField) {
event?.preventDefault();
if (duplicate) {
const newField: TAddTemplateFieldsFormSchema['fields'][0] = {
...structuredClone(lastActiveField),
nativeId: undefined,
formId: nanoid(12),
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
signerId: selectedSigner?.id ?? lastActiveField.signerId,
signerToken: selectedSigner?.token ?? lastActiveField.signerToken,
pageX: lastActiveField.pageX + 3,
pageY: lastActiveField.pageY + 3,
};
if (!duplicate) {
setFieldClipboard(lastActiveField);
append(newField);
return;
}
if (duplicateAll) {
const pages = Array.from(document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR));
pages.forEach((_, index) => {
const pageNumber = index + 1;
if (pageNumber === lastActiveField.pageNumber) {
return;
}
const newField: TAddTemplateFieldsFormSchema['fields'][0] = {
...structuredClone(lastActiveField),
nativeId: undefined,
formId: nanoid(12),
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
signerId: selectedSigner?.id ?? lastActiveField.signerId,
signerToken: selectedSigner?.token ?? lastActiveField.signerToken,
pageNumber,
};
append(newField);
toast({
title: 'Copied field',
description: 'Copied field to clipboard',
});
return;
}
setFieldClipboard(lastActiveField);
const newField: TAddTemplateFieldsFormSchema['fields'][0] = {
...structuredClone(lastActiveField),
formId: nanoid(12),
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
signerId: selectedSigner?.id ?? lastActiveField.signerId,
signerToken: selectedSigner?.token ?? lastActiveField.signerToken,
pageX: lastActiveField.pageX + 3,
pageY: lastActiveField.pageY + 3,
};
toast({
title: 'Copied field',
description: 'Copied field to clipboard',
});
append(newField);
}
},
[append, lastActiveField, selectedSigner?.email, selectedSigner?.id, toast],
[
append,
lastActiveField,
selectedSigner?.email,
selectedSigner?.id,
selectedSigner?.token,
toast,
],
);
const onFieldPaste = useCallback(
@ -563,7 +543,6 @@ export const AddTemplateFieldsFormPartial = ({
onMove={(options) => onFieldMove(options, index)}
onRemove={() => remove(index)}
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
onDuplicateAllPages={() => onFieldCopy(null, { duplicateAll: true })}
onAdvancedSettings={() => {
setCurrentField(field);
handleAdvancedSettings();