mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 07:43:16 +10:00
Compare commits
1 Commits
feat/audit
...
chore/sing
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a238d99d8 |
@ -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={() => {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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',
|
||||
);
|
||||
|
||||
@ -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 },
|
||||
)}
|
||||
>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(),
|
||||
});
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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;
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@ -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(),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "TeamGlobalSettings" ADD COLUMN "includeAuditLog" BOOLEAN NOT NULL DEFAULT false;
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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`}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 };
|
||||
@ -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();
|
||||
|
||||
Reference in New Issue
Block a user