mirror of
https://github.com/documenso/documenso.git
synced 2025-11-17 02:01:33 +10:00
Compare commits
6 Commits
feat/audit
...
exp/bg
| Author | SHA1 | Date | |
|---|---|---|---|
| 5de5b47e8b | |||
| f3e0fe6796 | |||
| c0c6a26ff2 | |||
| 1181167bf9 | |||
| cc400495d4 | |||
| 1b1071778d |
@ -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">
|
||||||
|
|||||||
@ -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 },
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -40,43 +40,6 @@ services:
|
|||||||
entrypoint: sh
|
entrypoint: sh
|
||||||
command: -c 'mkdir -p /data/documenso && minio server /data --console-address ":9001" --address ":9002"'
|
command: -c 'mkdir -p /data/documenso && minio server /data --console-address ":9001" --address ":9002"'
|
||||||
|
|
||||||
triggerdotdev:
|
|
||||||
image: ghcr.io/triggerdotdev/trigger.dev:latest
|
|
||||||
container_name: triggerdotdev
|
|
||||||
environment:
|
|
||||||
- LOGIN_ORIGIN=http://localhost:3030
|
|
||||||
- APP_ORIGIN=http://localhost:3030
|
|
||||||
- PORT=3030
|
|
||||||
- REMIX_APP_PORT=3030
|
|
||||||
- MAGIC_LINK_SECRET=secret
|
|
||||||
- SESSION_SECRET=secret
|
|
||||||
- ENCRYPTION_KEY=deadbeefcafefeed
|
|
||||||
- DATABASE_URL=postgresql://trigger:password@triggerdotdev_database:5432/trigger
|
|
||||||
- DIRECT_URL=postgresql://trigger:password@triggerdotdev_database:5432/trigger
|
|
||||||
- RUNTIME_PLATFORM=docker-compose
|
|
||||||
ports:
|
|
||||||
- 3030:3030
|
|
||||||
depends_on:
|
|
||||||
- triggerdotdev_database
|
|
||||||
|
|
||||||
triggerdotdev_database:
|
|
||||||
container_name: triggerdotdev_database
|
|
||||||
image: postgres:15
|
|
||||||
volumes:
|
|
||||||
- triggerdotdev_database:/var/lib/postgresql/data
|
|
||||||
healthcheck:
|
|
||||||
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER}']
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=trigger
|
|
||||||
- POSTGRES_PASSWORD=password
|
|
||||||
- POSTGRES_DB=trigger
|
|
||||||
ports:
|
|
||||||
- 54321:5432
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
minio:
|
minio:
|
||||||
documenso_database:
|
documenso_database:
|
||||||
triggerdotdev_database:
|
|
||||||
|
|||||||
4627
package-lock.json
generated
4627
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -28,7 +28,6 @@
|
|||||||
"with:env": "dotenv -e .env -e .env.local --",
|
"with:env": "dotenv -e .env -e .env.local --",
|
||||||
"reset:hard": "npm run clean && npm i && npm run prisma:generate",
|
"reset:hard": "npm run clean && npm i && npm run prisma:generate",
|
||||||
"precommit": "npm install && git add package.json package-lock.json",
|
"precommit": "npm install && git add package.json package-lock.json",
|
||||||
"trigger:dev": "npm run with:env -- npx trigger-cli dev --handler-path=\"/api/jobs\"",
|
|
||||||
"inngest:dev": "inngest dev -u http://localhost:3000/api/jobs",
|
"inngest:dev": "inngest dev -u http://localhost:3000/api/jobs",
|
||||||
"make:version": "npm version --workspace @documenso/remix --include-workspace-root --no-git-tag-version -m \"v%s\"",
|
"make:version": "npm version --workspace @documenso/remix --include-workspace-root --no-git-tag-version -m \"v%s\"",
|
||||||
"translate": "npm run translate:extract && npm run translate:compile",
|
"translate": "npm run translate:extract && npm run translate:compile",
|
||||||
@ -44,7 +43,6 @@
|
|||||||
"@commitlint/cli": "^17.7.1",
|
"@commitlint/cli": "^17.7.1",
|
||||||
"@commitlint/config-conventional": "^17.7.0",
|
"@commitlint/config-conventional": "^17.7.0",
|
||||||
"@lingui/cli": "^5.2.0",
|
"@lingui/cli": "^5.2.0",
|
||||||
"@trigger.dev/cli": "^2.3.18",
|
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.3.1",
|
||||||
"dotenv-cli": "^7.3.0",
|
"dotenv-cli": "^7.3.0",
|
||||||
"eslint": "^8.40.0",
|
"eslint": "^8.40.0",
|
||||||
@ -76,8 +74,5 @@
|
|||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"zod": "3.24.1"
|
"zod": "3.24.1"
|
||||||
},
|
|
||||||
"trigger.dev": {
|
|
||||||
"endpointId": "documenso-app"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,13 +1,19 @@
|
|||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { env } from '../../utils/env';
|
import { env } from '../../utils/env';
|
||||||
import type { JobDefinition, TriggerJobOptions } from './_internal/job';
|
import type {
|
||||||
|
JobDefinition,
|
||||||
|
JobRunIO,
|
||||||
|
SimpleTriggerJobOptions,
|
||||||
|
TriggerJobOptions,
|
||||||
|
} from './_internal/job';
|
||||||
import type { BaseJobProvider as JobClientProvider } from './base';
|
import type { BaseJobProvider as JobClientProvider } from './base';
|
||||||
import { InngestJobProvider } from './inngest';
|
import { InngestJobProvider } from './inngest';
|
||||||
import { LocalJobProvider } from './local';
|
import { LocalJobProvider } from './local';
|
||||||
|
|
||||||
export class JobClient<T extends ReadonlyArray<JobDefinition> = []> {
|
export class JobClient<T extends ReadonlyArray<JobDefinition> = []> {
|
||||||
private _provider: JobClientProvider;
|
private _provider: JobClientProvider;
|
||||||
|
private _jobDefinitions: Record<string, JobDefinition> = {};
|
||||||
|
|
||||||
public constructor(definitions: T) {
|
public constructor(definitions: T) {
|
||||||
this._provider = match(env('NEXT_PRIVATE_JOBS_PROVIDER'))
|
this._provider = match(env('NEXT_PRIVATE_JOBS_PROVIDER'))
|
||||||
@ -16,10 +22,67 @@ export class JobClient<T extends ReadonlyArray<JobDefinition> = []> {
|
|||||||
|
|
||||||
definitions.forEach((definition) => {
|
definitions.forEach((definition) => {
|
||||||
this._provider.defineJob(definition);
|
this._provider.defineJob(definition);
|
||||||
|
this._jobDefinitions[definition.id] = definition;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async triggerJob(options: TriggerJobOptions<T>) {
|
/**
|
||||||
|
* Check if Inngest background job processing is available.
|
||||||
|
*
|
||||||
|
* For Inngest to be considered available:
|
||||||
|
* 1. NEXT_PRIVATE_JOBS_PROVIDER must be set to 'inngest'
|
||||||
|
* 2. Either INNGEST_EVENT_KEY or NEXT_PRIVATE_INNGEST_EVENT_KEY must be provided
|
||||||
|
*
|
||||||
|
* If Inngest is not available, jobs will be executed synchronously without background scheduling.
|
||||||
|
*/
|
||||||
|
public isInngestAvailable(): boolean {
|
||||||
|
return (
|
||||||
|
env('NEXT_PRIVATE_JOBS_PROVIDER') === 'inngest' &&
|
||||||
|
Boolean(env('INNGEST_EVENT_KEY') || env('NEXT_PRIVATE_INNGEST_EVENT_KEY'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async triggerJob(options: TriggerJobOptions<T>): Promise<unknown> {
|
||||||
|
// When Inngest is not available, execute the job directly
|
||||||
|
if (!this.isInngestAvailable()) {
|
||||||
|
const eligibleJob = Object.values(this._jobDefinitions).find(
|
||||||
|
(job) => job.trigger.name === options.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (eligibleJob && eligibleJob.handler) {
|
||||||
|
// Execute the job directly without scheduling
|
||||||
|
const payload = options.payload;
|
||||||
|
const io: JobRunIO = {
|
||||||
|
wait: async (_cacheKey: string, ms: number): Promise<void> => {
|
||||||
|
// Create a Promise that resolves after ms milliseconds
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
setTimeout(resolve, ms);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
logger: console,
|
||||||
|
runTask: async <R>(cacheKey: string, callback: () => Promise<R>): Promise<R> => {
|
||||||
|
return await callback();
|
||||||
|
},
|
||||||
|
triggerJob: async (
|
||||||
|
_cacheKey: string,
|
||||||
|
jobOptions: SimpleTriggerJobOptions,
|
||||||
|
): Promise<unknown> => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
|
return await this.triggerJob(jobOptions as TriggerJobOptions<T>);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await eligibleJob.handler({ payload, io });
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Direct job execution failed for ${options.name}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use background processing with Inngest if available
|
||||||
return this._provider.triggerJob(options);
|
return this._provider.triggerJob(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,16 +32,49 @@ export const run = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { userId, documentId, recipientId, requestMetadata } = payload;
|
const { userId, documentId, recipientId, requestMetadata } = payload;
|
||||||
|
|
||||||
const [user, document, recipient] = await Promise.all([
|
try {
|
||||||
|
// First, check if the document exists directly before performing the multi-promise
|
||||||
|
const documentExists = await prisma.document.findFirst({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!documentExists) {
|
||||||
|
throw new Error(`No Document found with ID ${documentId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a small delay to allow any pending transactions to complete
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve();
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [user, recipient] = await Promise.all([
|
||||||
prisma.user.findFirstOrThrow({
|
prisma.user.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: userId,
|
id: userId,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
prisma.document.findFirstOrThrow({
|
|
||||||
|
prisma.recipient.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: recipientId,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Get the document without restricting to PENDING status
|
||||||
|
const document = await prisma.document.findFirstOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: documentId,
|
id: documentId,
|
||||||
status: DocumentStatus.PENDING,
|
// Don't restrict to PENDING status, as it might still be in DRAFT status
|
||||||
|
// if the transaction hasn't fully completed yet
|
||||||
|
status: {
|
||||||
|
in: [DocumentStatus.DRAFT, DocumentStatus.PENDING],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
documentMeta: true,
|
documentMeta: true,
|
||||||
@ -53,13 +86,7 @@ export const run = async ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
});
|
||||||
prisma.recipient.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: recipientId,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { documentMeta, team } = document;
|
const { documentMeta, team } = document;
|
||||||
|
|
||||||
@ -209,4 +236,8 @@ export const run = async ({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Job failed with error:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -28,7 +28,6 @@
|
|||||||
"@lingui/core": "^5.2.0",
|
"@lingui/core": "^5.2.0",
|
||||||
"@lingui/macro": "^5.2.0",
|
"@lingui/macro": "^5.2.0",
|
||||||
"@lingui/react": "^5.2.0",
|
"@lingui/react": "^5.2.0",
|
||||||
"jose": "^6.0.0",
|
|
||||||
"@noble/ciphers": "0.4.0",
|
"@noble/ciphers": "0.4.0",
|
||||||
"@noble/hashes": "1.3.2",
|
"@noble/hashes": "1.3.2",
|
||||||
"@node-rs/bcrypt": "^1.10.0",
|
"@node-rs/bcrypt": "^1.10.0",
|
||||||
@ -38,6 +37,7 @@
|
|||||||
"@vvo/tzdb": "^6.117.0",
|
"@vvo/tzdb": "^6.117.0",
|
||||||
"csv-parse": "^5.6.0",
|
"csv-parse": "^5.6.0",
|
||||||
"inngest": "^3.19.13",
|
"inngest": "^3.19.13",
|
||||||
|
"jose": "^6.0.0",
|
||||||
"kysely": "0.26.3",
|
"kysely": "0.26.3",
|
||||||
"luxon": "^3.4.0",
|
"luxon": "^3.4.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
@ -47,6 +47,7 @@
|
|||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"playwright": "1.43.0",
|
"playwright": "1.43.0",
|
||||||
"posthog-js": "^1.224.0",
|
"posthog-js": "^1.224.0",
|
||||||
|
"posthog-node": "^4.17.1",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"remeda": "^2.17.3",
|
"remeda": "^2.17.3",
|
||||||
"sharp": "0.32.6",
|
"sharp": "0.32.6",
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -133,31 +133,6 @@ export const sendDocument = async ({
|
|||||||
Object.assign(document, result);
|
Object.assign(document, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commented out server side checks for minimum 1 signature per signer now since we need to
|
|
||||||
// decide if we want to enforce this for API & templates.
|
|
||||||
// const fields = await getFieldsForDocument({
|
|
||||||
// documentId: documentId,
|
|
||||||
// userId: userId,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// const fieldsWithSignerEmail = fields.map((field) => ({
|
|
||||||
// ...field,
|
|
||||||
// signerEmail:
|
|
||||||
// document.Recipient.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
|
|
||||||
// }));
|
|
||||||
|
|
||||||
// const everySignerHasSignature = document?.Recipient.every(
|
|
||||||
// (recipient) =>
|
|
||||||
// recipient.role !== RecipientRole.SIGNER ||
|
|
||||||
// fieldsWithSignerEmail.some(
|
|
||||||
// (field) => field.type === 'SIGNATURE' && field.signerEmail === recipient.email,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// if (!everySignerHasSignature) {
|
|
||||||
// throw new Error('Some signers have not been assigned a signature field.');
|
|
||||||
// }
|
|
||||||
|
|
||||||
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
const isRecipientSigningRequestEmailEnabled = extractDerivedDocumentEmailSettings(
|
||||||
document.documentMeta,
|
document.documentMeta,
|
||||||
).recipientSigningRequest;
|
).recipientSigningRequest;
|
||||||
@ -165,52 +140,14 @@ export const sendDocument = async ({
|
|||||||
// Only send email if one of the following is true:
|
// Only send email if one of the following is true:
|
||||||
// - It is explicitly set
|
// - It is explicitly set
|
||||||
// - The email is enabled for signing requests AND sendEmail is undefined
|
// - The email is enabled for signing requests AND sendEmail is undefined
|
||||||
if (sendEmail || (isRecipientSigningRequestEmailEnabled && sendEmail === undefined)) {
|
const shouldSendEmail =
|
||||||
await Promise.all(
|
sendEmail || (isRecipientSigningRequestEmailEnabled && sendEmail === undefined);
|
||||||
recipientsToNotify.map(async (recipient) => {
|
|
||||||
if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await jobs.triggerJob({
|
|
||||||
name: 'send.signing.requested.email',
|
|
||||||
payload: {
|
|
||||||
userId,
|
|
||||||
documentId,
|
|
||||||
recipientId: recipient.id,
|
|
||||||
requestMetadata: requestMetadata?.requestMetadata,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const allRecipientsHaveNoActionToTake = document.recipients.every(
|
const allRecipientsHaveNoActionToTake = document.recipients.every(
|
||||||
(recipient) =>
|
(recipient) =>
|
||||||
recipient.role === RecipientRole.CC || recipient.signingStatus === SigningStatus.SIGNED,
|
recipient.role === RecipientRole.CC || recipient.signingStatus === SigningStatus.SIGNED,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (allRecipientsHaveNoActionToTake) {
|
|
||||||
await jobs.triggerJob({
|
|
||||||
name: 'internal.seal-document',
|
|
||||||
payload: {
|
|
||||||
documentId,
|
|
||||||
requestMetadata: requestMetadata?.requestMetadata,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Keep the return type the same for the `sendDocument` method
|
|
||||||
return await prisma.document.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: documentId,
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
documentMeta: true,
|
|
||||||
recipients: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedDocument = await prisma.$transaction(async (tx) => {
|
const updatedDocument = await prisma.$transaction(async (tx) => {
|
||||||
if (document.status === DocumentStatus.DRAFT) {
|
if (document.status === DocumentStatus.DRAFT) {
|
||||||
await tx.documentAuditLog.create({
|
await tx.documentAuditLog.create({
|
||||||
@ -244,5 +181,47 @@ export const sendDocument = async ({
|
|||||||
teamId,
|
teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Now that the transaction is complete and document status is updated, trigger email jobs
|
||||||
|
if (shouldSendEmail) {
|
||||||
|
await Promise.all(
|
||||||
|
recipientsToNotify.map(async (recipient) => {
|
||||||
|
if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await jobs.triggerJob({
|
||||||
|
name: 'send.signing.requested.email',
|
||||||
|
payload: {
|
||||||
|
userId,
|
||||||
|
documentId,
|
||||||
|
recipientId: recipient.id,
|
||||||
|
requestMetadata: requestMetadata?.requestMetadata,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allRecipientsHaveNoActionToTake) {
|
||||||
|
await jobs.triggerJob({
|
||||||
|
name: 'internal.seal-document',
|
||||||
|
payload: {
|
||||||
|
documentId,
|
||||||
|
requestMetadata: requestMetadata?.requestMetadata,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep the return type the same for the `sendDocument` method
|
||||||
|
return await prisma.document.findFirstOrThrow({
|
||||||
|
where: {
|
||||||
|
id: documentId,
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
documentMeta: true,
|
||||||
|
recipients: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return updatedDocument;
|
return updatedDocument;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { DocumentVisibility } from '@prisma/client';
|
import { DocumentStatus, DocumentVisibility, TeamMemberRole } from '@prisma/client';
|
||||||
import { DocumentStatus, TeamMemberRole } from '@prisma/client';
|
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
@ -119,7 +118,6 @@ export const updateDocument = async ({
|
|||||||
|
|
||||||
// If no data just return the document since this function is normally chained after a meta update.
|
// If no data just return the document since this function is normally chained after a meta update.
|
||||||
if (!data || Object.values(data).length === 0) {
|
if (!data || Object.values(data).length === 0) {
|
||||||
console.log('no data');
|
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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