mirror of
https://github.com/documenso/documenso.git
synced 2025-11-15 01:01:49 +10:00
Compare commits
1 Commits
feat/audit
...
chore/sing
| Author | SHA1 | Date | |
|---|---|---|---|
| 5a238d99d8 |
@ -173,13 +173,23 @@ export const ConfigureFieldsView = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onFieldCopy = useCallback(
|
const onFieldCopy = useCallback(
|
||||||
(event?: KeyboardEvent | null, options?: { duplicate?: boolean; duplicateAll?: boolean }) => {
|
(event?: KeyboardEvent | null, options?: { duplicate?: boolean }) => {
|
||||||
const { duplicate = false, duplicateAll = false } = options ?? {};
|
const { duplicate = false } = options ?? {};
|
||||||
|
|
||||||
if (lastActiveField) {
|
if (lastActiveField) {
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
|
|
||||||
if (duplicate) {
|
if (!duplicate) {
|
||||||
|
setFieldClipboard(lastActiveField);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Copied field',
|
||||||
|
description: 'Copied field to clipboard',
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newField: TConfigureFieldsFormSchema['fields'][0] = {
|
const newField: TConfigureFieldsFormSchema['fields'][0] = {
|
||||||
...structuredClone(lastActiveField),
|
...structuredClone(lastActiveField),
|
||||||
nativeId: undefined,
|
nativeId: undefined,
|
||||||
@ -191,41 +201,6 @@ export const ConfigureFieldsView = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
append(newField);
|
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFieldClipboard(lastActiveField);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Copied field',
|
|
||||||
description: 'Copied field to clipboard',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[append, lastActiveField, selectedRecipient?.email, selectedRecipient?.id, toast],
|
[append, lastActiveField, selectedRecipient?.email, selectedRecipient?.id, toast],
|
||||||
@ -558,7 +533,6 @@ export const ConfigureFieldsView = ({
|
|||||||
onMove={(node) => onFieldMove(node, index)}
|
onMove={(node) => onFieldMove(node, index)}
|
||||||
onRemove={() => remove(index)}
|
onRemove={() => remove(index)}
|
||||||
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
|
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
|
||||||
onDuplicateAllPages={() => onFieldCopy(null, { duplicateAll: true })}
|
|
||||||
onFocus={() => setLastActiveField(field)}
|
onFocus={() => setLastActiveField(field)}
|
||||||
onBlur={() => setLastActiveField(null)}
|
onBlur={() => setLastActiveField(null)}
|
||||||
onAdvancedSettings={() => {
|
onAdvancedSettings={() => {
|
||||||
|
|||||||
@ -44,7 +44,6 @@ const ZTeamDocumentPreferencesFormSchema = z.object({
|
|||||||
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES),
|
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES),
|
||||||
includeSenderDetails: z.boolean(),
|
includeSenderDetails: z.boolean(),
|
||||||
includeSigningCertificate: z.boolean(),
|
includeSigningCertificate: z.boolean(),
|
||||||
includeAuditLog: z.boolean(),
|
|
||||||
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(1, {
|
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(1, {
|
||||||
message: msg`At least one signature type must be enabled`.id,
|
message: msg`At least one signature type must be enabled`.id,
|
||||||
}),
|
}),
|
||||||
@ -78,7 +77,6 @@ export const TeamDocumentPreferencesForm = ({
|
|||||||
: 'en',
|
: 'en',
|
||||||
includeSenderDetails: settings?.includeSenderDetails ?? false,
|
includeSenderDetails: settings?.includeSenderDetails ?? false,
|
||||||
includeSigningCertificate: settings?.includeSigningCertificate ?? true,
|
includeSigningCertificate: settings?.includeSigningCertificate ?? true,
|
||||||
includeAuditLog: settings?.includeAuditLog ?? false,
|
|
||||||
signatureTypes: extractTeamSignatureSettings(settings),
|
signatureTypes: extractTeamSignatureSettings(settings),
|
||||||
},
|
},
|
||||||
resolver: zodResolver(ZTeamDocumentPreferencesFormSchema),
|
resolver: zodResolver(ZTeamDocumentPreferencesFormSchema),
|
||||||
@ -93,7 +91,6 @@ export const TeamDocumentPreferencesForm = ({
|
|||||||
documentLanguage,
|
documentLanguage,
|
||||||
includeSenderDetails,
|
includeSenderDetails,
|
||||||
includeSigningCertificate,
|
includeSigningCertificate,
|
||||||
includeAuditLog,
|
|
||||||
signatureTypes,
|
signatureTypes,
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
@ -104,7 +101,6 @@ export const TeamDocumentPreferencesForm = ({
|
|||||||
documentLanguage,
|
documentLanguage,
|
||||||
includeSenderDetails,
|
includeSenderDetails,
|
||||||
includeSigningCertificate,
|
includeSigningCertificate,
|
||||||
includeAuditLog,
|
|
||||||
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
|
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
|
||||||
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
|
||||||
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
|
||||||
@ -311,7 +307,7 @@ export const TeamDocumentPreferencesForm = ({
|
|||||||
|
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
<Trans>
|
<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
|
it is downloaded. The signing certificate can still be downloaded from the logs
|
||||||
page separately.
|
page separately.
|
||||||
</Trans>
|
</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">
|
<div className="flex flex-row justify-end space-x-4">
|
||||||
<Button type="submit" loading={form.formState.isSubmitting}>
|
<Button type="submit" loading={form.formState.isSubmitting}>
|
||||||
<Trans>Update</Trans>
|
<Trans>Update</Trans>
|
||||||
|
|||||||
@ -12,14 +12,7 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
|
|||||||
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
import { isDocumentCompleted } from '@documenso/lib/utils/document';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
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';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
export type DocumentPageViewButtonProps = {
|
export type DocumentPageViewButtonProps = {
|
||||||
@ -49,12 +42,6 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
|
|||||||
? `${documentsPath}/f/${document.folderId}/${document.id}/edit`
|
? `${documentsPath}/f/${document.folderId}/${document.id}/edit`
|
||||||
: `${documentsPath}/${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 () => {
|
const onDownloadClick = async () => {
|
||||||
try {
|
try {
|
||||||
const documentWithData = await trpcClient.document.getDocumentById.query(
|
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({
|
return match({
|
||||||
isRecipient,
|
isRecipient,
|
||||||
isPending,
|
isPending,
|
||||||
@ -242,26 +110,10 @@ export const DocumentPageViewButton = ({ document }: DocumentPageViewButtonProps
|
|||||||
</Button>
|
</Button>
|
||||||
))
|
))
|
||||||
.with({ isComplete: true }, () => (
|
.with({ isComplete: true }, () => (
|
||||||
<SplitButton className="w-full">
|
<Button className="w-full" onClick={onDownloadClick}>
|
||||||
<SplitButtonAction onClick={onDownloadClick}>
|
|
||||||
<Download className="-ml-1 mr-2 inline h-4 w-4" />
|
<Download className="-ml-1 mr-2 inline h-4 w-4" />
|
||||||
<Trans>Download</Trans>
|
<Trans>Download</Trans>
|
||||||
</SplitButtonAction>
|
</Button>
|
||||||
|
|
||||||
<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>
|
|
||||||
))
|
))
|
||||||
.otherwise(() => null);
|
.otherwise(() => null);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,8 +10,6 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
Edit,
|
Edit,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
FileDown,
|
|
||||||
FolderInput,
|
|
||||||
Loader,
|
Loader,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
MoveRight,
|
MoveRight,
|
||||||
@ -184,7 +182,7 @@ export const DocumentsTableActionDropdown = ({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem onClick={onDownloadOriginalClick}>
|
<DropdownMenuItem onClick={onDownloadOriginalClick}>
|
||||||
<FileDown className="mr-2 h-4 w-4" />
|
<Download className="mr-2 h-4 w-4" />
|
||||||
<Trans>Download Original</Trans>
|
<Trans>Download Original</Trans>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
@ -203,7 +201,7 @@ export const DocumentsTableActionDropdown = ({
|
|||||||
|
|
||||||
{onMoveDocument && (
|
{onMoveDocument && (
|
||||||
<DropdownMenuItem onClick={onMoveDocument} onSelect={(e) => e.preventDefault()}>
|
<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>
|
<Trans>Move to Folder</Trans>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -69,6 +69,8 @@ export const TemplatesTableActionDropdown = ({
|
|||||||
? `${templateRootPath}/f/${row.folderId}/${row.id}/edit`
|
? `${templateRootPath}/f/${row.folderId}/${row.id}/edit`
|
||||||
: `${templateRootPath}/${row.id}/edit`;
|
: `${templateRootPath}/${row.id}/edit`;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger data-testid="template-table-action-btn">
|
<DropdownMenuTrigger data-testid="template-table-action-btn">
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useLingui } from '@lingui/react';
|
import { useLingui } from '@lingui/react';
|
||||||
import { Plural, Trans } from '@lingui/react/macro';
|
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 { ChevronLeft, Clock9, Users2 } from 'lucide-react';
|
||||||
import { Link, redirect } from 'react-router';
|
import { Link, redirect } from 'react-router';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
@ -111,10 +111,25 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
|||||||
recipients,
|
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({
|
return superLoaderJson({
|
||||||
document: documentWithRecipients,
|
document: documentWithRecipients,
|
||||||
documentRootPath,
|
documentRootPath,
|
||||||
fields,
|
fields,
|
||||||
|
isSingleSignerDocument,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +139,7 @@ export default function DocumentPage() {
|
|||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { user } = useSession();
|
const { user } = useSession();
|
||||||
|
|
||||||
const { document, documentRootPath, fields } = loaderData;
|
const { document, documentRootPath, fields, isSingleSignerDocument } = loaderData;
|
||||||
|
|
||||||
const { recipients, documentData, documentMeta } = document;
|
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>
|
<Trans>This document is currently a draft and has not been sent</Trans>
|
||||||
))
|
))
|
||||||
.with(DocumentStatus.PENDING, () => {
|
.with(DocumentStatus.PENDING, () => {
|
||||||
|
if (isSingleSignerDocument) {
|
||||||
|
return <Trans>This document has been signed and is being finalized.</Trans>;
|
||||||
|
}
|
||||||
|
|
||||||
const pendingRecipients = recipients.filter(
|
const pendingRecipients = recipients.filter(
|
||||||
(recipient) => recipient.signingStatus === 'NOT_SIGNED',
|
(recipient) => recipient.signingStatus === 'NOT_SIGNED',
|
||||||
);
|
);
|
||||||
|
|||||||
@ -119,7 +119,7 @@ export default function CompletedSigningPage({ loaderData }: Route.ComponentProp
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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 },
|
{ 'pt-0 lg:pt-0 xl:pt-0': canSignUp },
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import {
|
|||||||
ZDeleteDocumentMutationSchema,
|
ZDeleteDocumentMutationSchema,
|
||||||
ZDeleteFieldMutationSchema,
|
ZDeleteFieldMutationSchema,
|
||||||
ZDeleteRecipientMutationSchema,
|
ZDeleteRecipientMutationSchema,
|
||||||
ZDownloadDocumentQuerySchema,
|
|
||||||
ZDownloadDocumentSuccessfulSchema,
|
ZDownloadDocumentSuccessfulSchema,
|
||||||
ZFindTeamMembersResponseSchema,
|
ZFindTeamMembersResponseSchema,
|
||||||
ZGenerateDocumentFromTemplateMutationResponseSchema,
|
ZGenerateDocumentFromTemplateMutationResponseSchema,
|
||||||
@ -72,7 +71,6 @@ export const ApiContractV1 = c.router(
|
|||||||
downloadSignedDocument: {
|
downloadSignedDocument: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/api/v1/documents/:id/download',
|
path: '/api/v1/documents/:id/download',
|
||||||
query: ZDownloadDocumentQuerySchema,
|
|
||||||
responses: {
|
responses: {
|
||||||
200: ZDownloadDocumentSuccessfulSchema,
|
200: ZDownloadDocumentSuccessfulSchema,
|
||||||
401: ZUnsuccessfulResponseSchema,
|
401: ZUnsuccessfulResponseSchema,
|
||||||
|
|||||||
@ -142,7 +142,6 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
|||||||
|
|
||||||
downloadSignedDocument: authenticatedMiddleware(async (args, user, team) => {
|
downloadSignedDocument: authenticatedMiddleware(async (args, user, team) => {
|
||||||
const { id: documentId } = args.params;
|
const { id: documentId } = args.params;
|
||||||
const { downloadOriginalDocument } = args.query;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (process.env.NEXT_PUBLIC_UPLOAD_TRANSPORT !== 's3') {
|
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 {
|
return {
|
||||||
status: 400,
|
status: 400,
|
||||||
body: {
|
body: {
|
||||||
@ -187,9 +186,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { url } = await getPresignGetUrl(
|
const { url } = await getPresignGetUrl(document.documentData.data);
|
||||||
downloadOriginalDocument ? document.documentData.data : document.documentData.initialData,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@ -119,15 +119,6 @@ export const ZUploadDocumentSuccessfulSchema = z.object({
|
|||||||
key: z.string(),
|
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({
|
export const ZDownloadDocumentSuccessfulSchema = z.object({
|
||||||
downloadUrl: z.string(),
|
downloadUrl: z.string(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -16,7 +16,6 @@ const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
|
|||||||
documentLanguage: z.string(),
|
documentLanguage: z.string(),
|
||||||
includeSenderDetails: z.boolean(),
|
includeSenderDetails: z.boolean(),
|
||||||
includeSigningCertificate: z.boolean(),
|
includeSigningCertificate: z.boolean(),
|
||||||
includeAuditLog: z.boolean(),
|
|
||||||
brandingEnabled: z.boolean(),
|
brandingEnabled: z.boolean(),
|
||||||
brandingLogo: z.string(),
|
brandingLogo: z.string(),
|
||||||
brandingUrl: z.string(),
|
brandingUrl: z.string(),
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import { signPdf } from '@documenso/signing';
|
|||||||
import { AppError, AppErrorCode } from '../../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../../errors/app-error';
|
||||||
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
import { sendCompletedEmail } from '../../../server-only/document/send-completed-email';
|
||||||
import PostHogServerClient from '../../../server-only/feature-flags/get-post-hog-server-client';
|
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 { getCertificatePdf } from '../../../server-only/htmltopdf/get-certificate-pdf';
|
||||||
import { addRejectionStampToPdf } from '../../../server-only/pdf/add-rejection-stamp-to-pdf';
|
import { addRejectionStampToPdf } from '../../../server-only/pdf/add-rejection-stamp-to-pdf';
|
||||||
import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
|
import { flattenAnnotations } from '../../../server-only/pdf/flatten-annotations';
|
||||||
@ -53,7 +52,6 @@ export const run = async ({
|
|||||||
teamGlobalSettings: {
|
teamGlobalSettings: {
|
||||||
select: {
|
select: {
|
||||||
includeSigningCertificate: true,
|
includeSigningCertificate: true,
|
||||||
includeAuditLog: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -154,13 +152,6 @@ export const run = async ({
|
|||||||
}).catch(() => null)
|
}).catch(() => null)
|
||||||
: 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 newDataId = await io.runTask('decorate-and-sign-pdf', async () => {
|
||||||
const pdfDoc = await PDFDocument.load(pdfData);
|
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) {
|
for (const field of fields) {
|
||||||
if (field.inserted) {
|
if (field.inserted) {
|
||||||
document.useLegacyFieldInsertion
|
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 { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import {
|
|
||||||
ZWebhookDocumentSchema,
|
|
||||||
mapDocumentToWebhookDocumentPayload,
|
|
||||||
} from '../../types/webhook-payload';
|
|
||||||
import { prefixedId } from '../../universal/id';
|
import { prefixedId } from '../../universal/id';
|
||||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
|
||||||
import { getDocumentWhereInput } from './get-document-by-id';
|
import { getDocumentWhereInput } from './get-document-by-id';
|
||||||
|
|
||||||
export interface DuplicateDocumentOptions {
|
export interface DuplicateDocumentOptions {
|
||||||
@ -91,24 +86,7 @@ export const duplicateDocument = async ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdDocument = await prisma.document.create({
|
const createdDocument = await prisma.document.create(createDocumentArguments);
|
||||||
...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,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
documentId: createdDocument.id,
|
documentId: createdDocument.id,
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import type { RequestMetadata } from '../../universal/extract-request-metadata';
|
|||||||
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
import { getFileServerSide } from '../../universal/upload/get-file.server';
|
||||||
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
import { putPdfFileServerSide } from '../../universal/upload/put-file.server';
|
||||||
import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers';
|
import { fieldsContainUnsignedRequiredField } from '../../utils/advanced-fields-helpers';
|
||||||
import { getAuditLogPdf } from '../htmltopdf/get-audit-log-pdf';
|
|
||||||
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
|
import { getCertificatePdf } from '../htmltopdf/get-certificate-pdf';
|
||||||
import { addRejectionStampToPdf } from '../pdf/add-rejection-stamp-to-pdf';
|
import { addRejectionStampToPdf } from '../pdf/add-rejection-stamp-to-pdf';
|
||||||
import { flattenAnnotations } from '../pdf/flatten-annotations';
|
import { flattenAnnotations } from '../pdf/flatten-annotations';
|
||||||
@ -54,7 +53,6 @@ export const sealDocument = async ({
|
|||||||
teamGlobalSettings: {
|
teamGlobalSettings: {
|
||||||
select: {
|
select: {
|
||||||
includeSigningCertificate: true,
|
includeSigningCertificate: true,
|
||||||
includeAuditLog: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -126,13 +124,6 @@ export const sealDocument = async ({
|
|||||||
}).catch(() => null)
|
}).catch(() => null)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const auditLogData =
|
|
||||||
(document.team?.teamGlobalSettings?.includeAuditLog ?? false)
|
|
||||||
? await getAuditLogPdf({
|
|
||||||
documentId,
|
|
||||||
}).catch(() => null)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const doc = await PDFDocument.load(pdfData);
|
const doc = await PDFDocument.load(pdfData);
|
||||||
|
|
||||||
// Normalize and flatten layers that could cause issues with the signature
|
// 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) {
|
for (const field of fields) {
|
||||||
document.useLegacyFieldInsertion
|
document.useLegacyFieldInsertion
|
||||||
? await legacy_insertFieldInPDF(doc, field)
|
? 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,
|
SigningStatus,
|
||||||
WebhookTriggerEvents,
|
WebhookTriggerEvents,
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
import { nanoid, prefixedId } from '@documenso/lib/universal/id';
|
||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '../../constants/date-formats';
|
|
||||||
import type { SupportedLanguageCodes } from '../../constants/i18n';
|
import type { SupportedLanguageCodes } from '../../constants/i18n';
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
|
||||||
@ -510,8 +508,10 @@ export const createDocumentFromTemplate = async ({
|
|||||||
fieldsToCreate = fieldsToCreate.concat(
|
fieldsToCreate = fieldsToCreate.concat(
|
||||||
fields.map((field) => {
|
fields.map((field) => {
|
||||||
const prefillField = prefillFields?.find((value) => value.id === field.id);
|
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,
|
documentId: document.id,
|
||||||
recipientId: recipient.id,
|
recipientId: recipient.id,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
@ -522,38 +522,8 @@ export const createDocumentFromTemplate = async ({
|
|||||||
height: field.height,
|
height: field.height,
|
||||||
customText: '',
|
customText: '',
|
||||||
inserted: false,
|
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(),
|
label: z.string().optional(),
|
||||||
value: 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;
|
|
||||||
@ -586,9 +586,7 @@ model TeamGlobalSettings {
|
|||||||
documentVisibility DocumentVisibility @default(EVERYONE)
|
documentVisibility DocumentVisibility @default(EVERYONE)
|
||||||
documentLanguage String @default("en")
|
documentLanguage String @default("en")
|
||||||
includeSenderDetails Boolean @default(true)
|
includeSenderDetails Boolean @default(true)
|
||||||
|
|
||||||
includeSigningCertificate Boolean @default(true)
|
includeSigningCertificate Boolean @default(true)
|
||||||
includeAuditLog Boolean @default(false)
|
|
||||||
|
|
||||||
typedSignatureEnabled Boolean @default(true)
|
typedSignatureEnabled Boolean @default(true)
|
||||||
uploadSignatureEnabled Boolean @default(true)
|
uploadSignatureEnabled Boolean @default(true)
|
||||||
|
|||||||
@ -23,7 +23,6 @@ export const updateTeamDocumentSettingsRoute = authenticatedProcedure
|
|||||||
documentLanguage,
|
documentLanguage,
|
||||||
includeSenderDetails,
|
includeSenderDetails,
|
||||||
includeSigningCertificate,
|
includeSigningCertificate,
|
||||||
includeAuditLog,
|
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
uploadSignatureEnabled,
|
uploadSignatureEnabled,
|
||||||
drawSignatureEnabled,
|
drawSignatureEnabled,
|
||||||
@ -55,7 +54,6 @@ export const updateTeamDocumentSettingsRoute = authenticatedProcedure
|
|||||||
documentLanguage,
|
documentLanguage,
|
||||||
includeSenderDetails,
|
includeSenderDetails,
|
||||||
includeSigningCertificate,
|
includeSigningCertificate,
|
||||||
includeAuditLog,
|
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
uploadSignatureEnabled,
|
uploadSignatureEnabled,
|
||||||
drawSignatureEnabled,
|
drawSignatureEnabled,
|
||||||
@ -65,7 +63,6 @@ export const updateTeamDocumentSettingsRoute = authenticatedProcedure
|
|||||||
documentLanguage,
|
documentLanguage,
|
||||||
includeSenderDetails,
|
includeSenderDetails,
|
||||||
includeSigningCertificate,
|
includeSigningCertificate,
|
||||||
includeAuditLog,
|
|
||||||
typedSignatureEnabled,
|
typedSignatureEnabled,
|
||||||
uploadSignatureEnabled,
|
uploadSignatureEnabled,
|
||||||
drawSignatureEnabled,
|
drawSignatureEnabled,
|
||||||
|
|||||||
@ -14,7 +14,6 @@ export const ZUpdateTeamDocumentSettingsRequestSchema = z.object({
|
|||||||
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).optional().default('en'),
|
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).optional().default('en'),
|
||||||
includeSenderDetails: z.boolean().optional().default(false),
|
includeSenderDetails: z.boolean().optional().default(false),
|
||||||
includeSigningCertificate: z.boolean().optional().default(true),
|
includeSigningCertificate: z.boolean().optional().default(true),
|
||||||
includeAuditLog: z.boolean().optional().default(false),
|
|
||||||
typedSignatureEnabled: z.boolean().optional().default(true),
|
typedSignatureEnabled: z.boolean().optional().default(true),
|
||||||
uploadSignatureEnabled: z.boolean().optional().default(true),
|
uploadSignatureEnabled: z.boolean().optional().default(true),
|
||||||
drawSignatureEnabled: z.boolean().optional().default(true),
|
drawSignatureEnabled: z.boolean().optional().default(true),
|
||||||
|
|||||||
@ -24,7 +24,7 @@ export const SigningCard = ({
|
|||||||
signingCelebrationImage,
|
signingCelebrationImage,
|
||||||
}: SigningCardProps) => {
|
}: SigningCardProps) => {
|
||||||
return (
|
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} />
|
<SigningCardContent name={name} signature={signature} />
|
||||||
|
|
||||||
{signingCelebrationImage && (
|
{signingCelebrationImage && (
|
||||||
@ -48,7 +48,7 @@ export const SigningCard3D = ({
|
|||||||
|
|
||||||
const [trackMouse, setTrackMouse] = useState(false);
|
const [trackMouse, setTrackMouse] = useState(false);
|
||||||
|
|
||||||
const timeoutRef = useRef<number | undefined>();
|
const timeoutRef = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
const cardX = useMotionValue(0);
|
const cardX = useMotionValue(0);
|
||||||
const cardY = useMotionValue(0);
|
const cardY = useMotionValue(0);
|
||||||
@ -103,7 +103,7 @@ export const SigningCard3D = ({
|
|||||||
clearTimeout(timeoutRef.current);
|
clearTimeout(timeoutRef.current);
|
||||||
|
|
||||||
// Revert the card back to the center position after the mouse stops moving.
|
// 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(cardX, 0, { duration: 2, ease: 'backInOut' });
|
||||||
void animate(cardY, 0, { duration: 2, ease: 'backInOut' });
|
void animate(cardY, 0, { duration: 2, ease: 'backInOut' });
|
||||||
|
|
||||||
@ -120,15 +120,12 @@ export const SigningCard3D = ({
|
|||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('mousemove', onMouseMove);
|
window.removeEventListener('mousemove', onMouseMove);
|
||||||
if (timeoutRef.current) {
|
|
||||||
window.clearTimeout(timeoutRef.current);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}, [onMouseMove]);
|
}, [onMouseMove]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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 }}
|
style={{ perspective: 800 }}
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@ -400,16 +400,25 @@ export const AddFieldsFormPartial = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const onFieldCopy = useCallback(
|
const onFieldCopy = useCallback(
|
||||||
(event?: KeyboardEvent | null, options?: { duplicate?: boolean; duplicateAll?: boolean }) => {
|
(event?: KeyboardEvent | null, options?: { duplicate?: boolean }) => {
|
||||||
const { duplicate = false, duplicateAll = false } = options ?? {};
|
const { duplicate = false } = options ?? {};
|
||||||
|
|
||||||
if (lastActiveField) {
|
if (lastActiveField) {
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
|
|
||||||
if (duplicate) {
|
if (!duplicate) {
|
||||||
|
setFieldClipboard(lastActiveField);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Copied field',
|
||||||
|
description: 'Copied field to clipboard',
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newField: TAddFieldsFormSchema['fields'][0] = {
|
const newField: TAddFieldsFormSchema['fields'][0] = {
|
||||||
...structuredClone(lastActiveField),
|
...structuredClone(lastActiveField),
|
||||||
nativeId: undefined,
|
|
||||||
formId: nanoid(12),
|
formId: nanoid(12),
|
||||||
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
||||||
pageX: lastActiveField.pageX + 3,
|
pageX: lastActiveField.pageX + 3,
|
||||||
@ -417,43 +426,9 @@ export const AddFieldsFormPartial = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
append(newField);
|
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFieldClipboard(lastActiveField);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Copied field',
|
|
||||||
description: 'Copied field to clipboard',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[append, lastActiveField, selectedSigner?.email, selectedSigner?.id, toast],
|
[append, lastActiveField, selectedSigner?.email, toast],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onFieldPaste = useCallback(
|
const onFieldPaste = useCallback(
|
||||||
@ -666,7 +641,6 @@ export const AddFieldsFormPartial = ({
|
|||||||
onMove={(options) => onFieldMove(options, index)}
|
onMove={(options) => onFieldMove(options, index)}
|
||||||
onRemove={() => remove(index)}
|
onRemove={() => remove(index)}
|
||||||
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
|
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
|
||||||
onDuplicateAllPages={() => onFieldCopy(null, { duplicateAll: true })}
|
|
||||||
onAdvancedSettings={() => {
|
onAdvancedSettings={() => {
|
||||||
setCurrentField(field);
|
setCurrentField(field);
|
||||||
handleAdvancedSettings();
|
handleAdvancedSettings();
|
||||||
|
|||||||
@ -311,7 +311,6 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
.otherwise(() => null)}
|
.otherwise(() => null)}
|
||||||
|
|
||||||
{errors.length > 0 && (
|
{errors.length > 0 && (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<ul>
|
<ul>
|
||||||
@ -324,7 +323,6 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DocumentFlowFormContainerContent>
|
</DocumentFlowFormContainerContent>
|
||||||
|
|
||||||
<DocumentFlowFormContainerFooter className="mt-auto">
|
<DocumentFlowFormContainerFooter className="mt-auto">
|
||||||
<DocumentFlowFormContainerActions
|
<DocumentFlowFormContainerActions
|
||||||
goNextLabel={msg`Save`}
|
goNextLabel={msg`Save`}
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
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 { 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 { createPortal } from 'react-dom';
|
||||||
import { Rnd } from 'react-rnd';
|
import { Rnd } from 'react-rnd';
|
||||||
|
|
||||||
@ -31,7 +29,6 @@ export type FieldItemProps = {
|
|||||||
onMove?: (_node: HTMLElement) => void;
|
onMove?: (_node: HTMLElement) => void;
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
onDuplicate?: () => void;
|
onDuplicate?: () => void;
|
||||||
onDuplicateAllPages?: () => void;
|
|
||||||
onAdvancedSettings?: () => void;
|
onAdvancedSettings?: () => void;
|
||||||
onFocus?: () => void;
|
onFocus?: () => void;
|
||||||
onBlur?: () => void;
|
onBlur?: () => void;
|
||||||
@ -58,18 +55,15 @@ export const FieldItem = ({
|
|||||||
onMove,
|
onMove,
|
||||||
onRemove,
|
onRemove,
|
||||||
onDuplicate,
|
onDuplicate,
|
||||||
onDuplicateAllPages,
|
|
||||||
onAdvancedSettings,
|
|
||||||
onFocus,
|
onFocus,
|
||||||
onBlur,
|
onBlur,
|
||||||
|
onAdvancedSettings,
|
||||||
recipientIndex = 0,
|
recipientIndex = 0,
|
||||||
hasErrors,
|
hasErrors,
|
||||||
active,
|
active,
|
||||||
onFieldActivate,
|
onFieldActivate,
|
||||||
onFieldDeactivate,
|
onFieldDeactivate,
|
||||||
}: FieldItemProps) => {
|
}: FieldItemProps) => {
|
||||||
const { _ } = useLingui();
|
|
||||||
|
|
||||||
const [coords, setCoords] = useState({
|
const [coords, setCoords] = useState({
|
||||||
pageX: 0,
|
pageX: 0,
|
||||||
pageY: 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">
|
<div className="group flex items-center justify-evenly gap-x-1 rounded-md border bg-gray-900 p-0.5">
|
||||||
{advancedField && (
|
{advancedField && (
|
||||||
<button
|
<button
|
||||||
title={_(msg`Advanced settings`)}
|
|
||||||
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||||
onClick={onAdvancedSettings}
|
onClick={onAdvancedSettings}
|
||||||
onTouchEnd={onAdvancedSettings}
|
onTouchEnd={onAdvancedSettings}
|
||||||
@ -320,7 +313,6 @@ export const FieldItem = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
title={_(msg`Duplicate`)}
|
|
||||||
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||||
onClick={onDuplicate}
|
onClick={onDuplicate}
|
||||||
onTouchEnd={onDuplicate}
|
onTouchEnd={onDuplicate}
|
||||||
@ -329,16 +321,6 @@ export const FieldItem = ({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<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"
|
className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
onTouchEnd={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,16 +139,25 @@ export const AddTemplateFieldsFormPartial = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const onFieldCopy = useCallback(
|
const onFieldCopy = useCallback(
|
||||||
(event?: KeyboardEvent | null, options?: { duplicate?: boolean; duplicateAll?: boolean }) => {
|
(event?: KeyboardEvent | null, options?: { duplicate?: boolean }) => {
|
||||||
const { duplicate = false, duplicateAll = false } = options ?? {};
|
const { duplicate = false } = options ?? {};
|
||||||
|
|
||||||
if (lastActiveField) {
|
if (lastActiveField) {
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
|
|
||||||
if (duplicate) {
|
if (!duplicate) {
|
||||||
|
setFieldClipboard(lastActiveField);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Copied field',
|
||||||
|
description: 'Copied field to clipboard',
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const newField: TAddTemplateFieldsFormSchema['fields'][0] = {
|
const newField: TAddTemplateFieldsFormSchema['fields'][0] = {
|
||||||
...structuredClone(lastActiveField),
|
...structuredClone(lastActiveField),
|
||||||
nativeId: undefined,
|
|
||||||
formId: nanoid(12),
|
formId: nanoid(12),
|
||||||
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
signerEmail: selectedSigner?.email ?? lastActiveField.signerEmail,
|
||||||
signerId: selectedSigner?.id ?? lastActiveField.signerId,
|
signerId: selectedSigner?.id ?? lastActiveField.signerId,
|
||||||
@ -158,45 +167,16 @@ export const AddTemplateFieldsFormPartial = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
append(newField);
|
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setFieldClipboard(lastActiveField);
|
|
||||||
|
|
||||||
toast({
|
|
||||||
title: 'Copied field',
|
|
||||||
description: 'Copied field to clipboard',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[append, lastActiveField, selectedSigner?.email, selectedSigner?.id, toast],
|
[
|
||||||
|
append,
|
||||||
|
lastActiveField,
|
||||||
|
selectedSigner?.email,
|
||||||
|
selectedSigner?.id,
|
||||||
|
selectedSigner?.token,
|
||||||
|
toast,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onFieldPaste = useCallback(
|
const onFieldPaste = useCallback(
|
||||||
@ -563,7 +543,6 @@ export const AddTemplateFieldsFormPartial = ({
|
|||||||
onMove={(options) => onFieldMove(options, index)}
|
onMove={(options) => onFieldMove(options, index)}
|
||||||
onRemove={() => remove(index)}
|
onRemove={() => remove(index)}
|
||||||
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
|
onDuplicate={() => onFieldCopy(null, { duplicate: true })}
|
||||||
onDuplicateAllPages={() => onFieldCopy(null, { duplicateAll: true })}
|
|
||||||
onAdvancedSettings={() => {
|
onAdvancedSettings={() => {
|
||||||
setCurrentField(field);
|
setCurrentField(field);
|
||||||
handleAdvancedSettings();
|
handleAdvancedSettings();
|
||||||
|
|||||||
Reference in New Issue
Block a user