mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
Compare commits
21 Commits
chore/sing
...
v1.12.0-rc
| Author | SHA1 | Date | |
|---|---|---|---|
| 60a623fafd | |||
| 6059b79a8e | |||
| c73d61955b | |||
| 7c3ca72359 | |||
| 55c8632620 | |||
| ce66da0055 | |||
| 695ed418e2 | |||
| 93aece9644 | |||
| abd4fddf31 | |||
| 44bc769e60 | |||
| c8f80f7be0 | |||
| 8540f24de0 | |||
| 67203d4bd7 | |||
| 9d1e638f0f | |||
| bd64ad9fef | |||
| 99b0ad574e | |||
| 9594e1fee8 | |||
| 5e3a2b8f76 | |||
| f928503a33 | |||
| c389670785 | |||
| 99ad2eb645 |
2
.npmrc
2
.npmrc
@ -1 +1,3 @@
|
|||||||
auto-install-peers = true
|
auto-install-peers = true
|
||||||
|
legacy-peer-deps = true
|
||||||
|
prefer-dedupe = true
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import nextra from 'nextra';
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
transpilePackages: [
|
transpilePackages: [
|
||||||
@ -9,9 +11,10 @@ const nextConfig = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const withNextra = require('nextra')({
|
const withNextra = nextra({
|
||||||
theme: 'nextra-theme-docs',
|
theme: 'nextra-theme-docs',
|
||||||
themeConfig: './theme.config.tsx',
|
themeConfig: './theme.config.tsx',
|
||||||
|
codeHighlight: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = withNextra(nextConfig);
|
export default withNextra(nextConfig);
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
"@documenso/tailwind-config": "*",
|
"@documenso/tailwind-config": "*",
|
||||||
"@documenso/trpc": "*",
|
"@documenso/trpc": "*",
|
||||||
"@documenso/ui": "*",
|
"@documenso/ui": "*",
|
||||||
"next": "14.2.6",
|
"next": "14.2.28",
|
||||||
"next-plausible": "^3.12.0",
|
"next-plausible": "^3.12.0",
|
||||||
"nextra": "^2.13.4",
|
"nextra": "^2.13.4",
|
||||||
"nextra-theme-docs": "^2.13.4",
|
"nextra-theme-docs": "^2.13.4",
|
||||||
|
|||||||
@ -19,6 +19,22 @@ const themeConfig: DocsThemeConfig = {
|
|||||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||||
|
<script
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
!function(){
|
||||||
|
if (location.hostname === 'localhost') return;
|
||||||
|
var e="6c236490c9a68c1",
|
||||||
|
t=function(){Reo.init({ clientID: e })},
|
||||||
|
n=document.createElement("script");
|
||||||
|
n.src="https://static.reo.dev/"+e+"/reo.js";
|
||||||
|
n.defer=true;
|
||||||
|
n.onload=t;
|
||||||
|
document.head.appendChild(n);
|
||||||
|
}();
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -12,7 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"next": "14.2.6"
|
"next": "14.2.28"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
|||||||
@ -173,34 +173,59 @@ export const ConfigureFieldsView = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onFieldCopy = useCallback(
|
const onFieldCopy = useCallback(
|
||||||
(event?: KeyboardEvent | null, options?: { duplicate?: boolean }) => {
|
(event?: KeyboardEvent | null, options?: { duplicate?: boolean; duplicateAll?: boolean }) => {
|
||||||
const { duplicate = false } = options ?? {};
|
const { duplicate = false, duplicateAll = false } = options ?? {};
|
||||||
|
|
||||||
if (lastActiveField) {
|
if (lastActiveField) {
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
|
|
||||||
if (!duplicate) {
|
if (duplicate) {
|
||||||
setFieldClipboard(lastActiveField);
|
const newField: TConfigureFieldsFormSchema['fields'][0] = {
|
||||||
|
...structuredClone(lastActiveField),
|
||||||
|
nativeId: undefined,
|
||||||
|
formId: nanoid(12),
|
||||||
|
signerEmail: selectedRecipient?.email ?? lastActiveField.signerEmail,
|
||||||
|
recipientId: selectedRecipient?.id ?? lastActiveField.recipientId,
|
||||||
|
pageX: lastActiveField.pageX + 3,
|
||||||
|
pageY: lastActiveField.pageY + 3,
|
||||||
|
};
|
||||||
|
|
||||||
toast({
|
append(newField);
|
||||||
title: 'Copied field',
|
|
||||||
description: 'Copied field to clipboard',
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newField: TConfigureFieldsFormSchema['fields'][0] = {
|
setFieldClipboard(lastActiveField);
|
||||||
...structuredClone(lastActiveField),
|
|
||||||
nativeId: undefined,
|
|
||||||
formId: nanoid(12),
|
|
||||||
signerEmail: selectedRecipient?.email ?? lastActiveField.signerEmail,
|
|
||||||
recipientId: selectedRecipient?.id ?? lastActiveField.recipientId,
|
|
||||||
pageX: lastActiveField.pageX + 3,
|
|
||||||
pageY: lastActiveField.pageY + 3,
|
|
||||||
};
|
|
||||||
|
|
||||||
append(newField);
|
toast({
|
||||||
|
title: 'Copied field',
|
||||||
|
description: 'Copied field to clipboard',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[append, lastActiveField, selectedRecipient?.email, selectedRecipient?.id, toast],
|
[append, lastActiveField, selectedRecipient?.email, selectedRecipient?.id, toast],
|
||||||
@ -533,6 +558,7 @@ 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={() => {
|
||||||
|
|||||||
@ -332,7 +332,7 @@ export const EmbedDirectTemplateClientPage = ({
|
|||||||
{/* Widget */}
|
{/* Widget */}
|
||||||
<div
|
<div
|
||||||
key={isExpanded ? 'expanded' : 'collapsed'}
|
key={isExpanded ? 'expanded' : 'collapsed'}
|
||||||
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
|
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:bottom-[unset] md:top-4 md:z-auto md:w-[350px] md:px-0"
|
||||||
data-expanded={isExpanded || undefined}
|
data-expanded={isExpanded || undefined}
|
||||||
>
|
>
|
||||||
<div className="border-border bg-widget flex h-fit w-full flex-col rounded-xl border px-4 py-4 md:min-h-[min(calc(100dvh-2rem),48rem)] md:py-6">
|
<div className="border-border bg-widget flex h-fit w-full flex-col rounded-xl border px-4 py-4 md:min-h-[min(calc(100dvh-2rem),48rem)] md:py-6">
|
||||||
|
|||||||
@ -290,7 +290,7 @@ export const EmbedSignDocumentClientPage = ({
|
|||||||
{/* Widget */}
|
{/* Widget */}
|
||||||
<div
|
<div
|
||||||
key={isExpanded ? 'expanded' : 'collapsed'}
|
key={isExpanded ? 'expanded' : 'collapsed'}
|
||||||
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
|
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:bottom-[unset] md:top-4 md:z-auto md:w-[350px] md:px-0"
|
||||||
data-expanded={isExpanded || undefined}
|
data-expanded={isExpanded || undefined}
|
||||||
>
|
>
|
||||||
<div className="embed--DocumentWidget border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
|
<div className="embed--DocumentWidget border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
|
||||||
|
|||||||
@ -0,0 +1,182 @@
|
|||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { ReadStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||||
|
import { ArrowRight, EyeIcon, XCircle } from 'lucide-react';
|
||||||
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
|
import type { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
|
import { Badge } from '@documenso/ui/primitives/badge';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import { Progress } from '@documenso/ui/primitives/progress';
|
||||||
|
|
||||||
|
// Get the return type from getRecipientByToken
|
||||||
|
type RecipientWithFields = Awaited<ReturnType<typeof getRecipientByToken>>;
|
||||||
|
|
||||||
|
interface DocumentEnvelope {
|
||||||
|
document: DocumentAndSender;
|
||||||
|
recipient: RecipientWithFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MultiSignDocumentListProps {
|
||||||
|
envelopes: DocumentEnvelope[];
|
||||||
|
onDocumentSelect: (document: DocumentEnvelope['document']) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MultiSignDocumentList({ envelopes, onDocumentSelect }: MultiSignDocumentListProps) {
|
||||||
|
// Calculate progress
|
||||||
|
const completedDocuments = envelopes.filter(
|
||||||
|
(envelope) => envelope.recipient.signingStatus === SigningStatus.SIGNED,
|
||||||
|
);
|
||||||
|
const totalDocuments = envelopes.length;
|
||||||
|
const progressPercentage = (completedDocuments.length / totalDocuments) * 100;
|
||||||
|
|
||||||
|
// Find next document to sign (first one that's not signed and not rejected)
|
||||||
|
const nextDocumentToSign = envelopes.find(
|
||||||
|
(envelope) =>
|
||||||
|
envelope.recipient.signingStatus !== SigningStatus.SIGNED &&
|
||||||
|
envelope.recipient.signingStatus !== SigningStatus.REJECTED,
|
||||||
|
);
|
||||||
|
|
||||||
|
const allDocumentsCompleted = completedDocuments.length === totalDocuments;
|
||||||
|
|
||||||
|
const hasAssistantOrCcRecipient = envelopes.some(
|
||||||
|
(envelope) =>
|
||||||
|
envelope.recipient.role === RecipientRole.ASSISTANT ||
|
||||||
|
envelope.recipient.role === RecipientRole.CC,
|
||||||
|
);
|
||||||
|
|
||||||
|
function handleView(doc: DocumentEnvelope['document']) {
|
||||||
|
onDocumentSelect(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNextDocument() {
|
||||||
|
if (nextDocumentToSign) {
|
||||||
|
onDocumentSelect(nextDocumentToSign.document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAssistantOrCcRecipient) {
|
||||||
|
return (
|
||||||
|
<div className="mx-auto mt-16 flex w-full max-w-lg flex-col md:mt-16 md:rounded-2xl md:border md:px-8 md:py-16 md:shadow-lg">
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<XCircle className="text-destructive h-16 w-16 md:h-24 md:w-24" strokeWidth={1.2} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="mt-12 text-xl font-bold md:text-2xl">
|
||||||
|
<Trans>It looks like we ran into an issue!</Trans>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground mt-6">
|
||||||
|
<Trans>
|
||||||
|
One of the documents in the current bundle has a signing role that is not compatible
|
||||||
|
with the current signing experience.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground mt-2">
|
||||||
|
<Trans>
|
||||||
|
Assistants and Copy roles are currently not compatible with the multi-sign experience.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground mt-2">
|
||||||
|
<Trans>Please contact the site owner for further assistance.</Trans>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-background mx-auto w-full max-w-lg md:my-12 md:rounded-2xl md:border md:p-8 md:shadow-lg">
|
||||||
|
<h2 className="text-foreground mb-1 text-lg font-semibold">
|
||||||
|
<Trans>Sign Documents</Trans>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
<Trans>
|
||||||
|
You have been requested to sign the following documents. Review each document carefully
|
||||||
|
and complete the signing process.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Progress Section */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="flex items-center justify-between text-sm">
|
||||||
|
<span className="text-muted-foreground font-medium">
|
||||||
|
<Trans>Progress</Trans>
|
||||||
|
</span>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{completedDocuments.length} of {totalDocuments} completed
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<Progress value={progressPercentage} className="h-2" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6 flex flex-col gap-4">
|
||||||
|
{envelopes.map((envelope) => (
|
||||||
|
<div
|
||||||
|
key={envelope.document.id}
|
||||||
|
className="border-border flex items-center gap-4 rounded-lg border px-4 py-2"
|
||||||
|
>
|
||||||
|
<span className="text-foreground flex-1 truncate text-sm font-medium">
|
||||||
|
{envelope.document.title}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{match(envelope.recipient)
|
||||||
|
.with({ signingStatus: SigningStatus.SIGNED }, () => (
|
||||||
|
<Badge size="small" variant="default">
|
||||||
|
<Trans>Completed</Trans>
|
||||||
|
</Badge>
|
||||||
|
))
|
||||||
|
.with({ signingStatus: SigningStatus.REJECTED }, () => (
|
||||||
|
<Badge size="small" variant="destructive">
|
||||||
|
<Trans>Rejected</Trans>
|
||||||
|
</Badge>
|
||||||
|
))
|
||||||
|
.with({ readStatus: ReadStatus.OPENED }, () => (
|
||||||
|
<Badge size="small" variant="neutral">
|
||||||
|
<Trans>Viewed</Trans>
|
||||||
|
</Badge>
|
||||||
|
))
|
||||||
|
.otherwise(() => null)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="-mr-2"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleView(envelope.document)}
|
||||||
|
>
|
||||||
|
<EyeIcon className="mr-1 h-4 w-4" />
|
||||||
|
<Trans>View</Trans>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Next Document Button */}
|
||||||
|
{!allDocumentsCompleted && nextDocumentToSign && (
|
||||||
|
<div className="mt-6">
|
||||||
|
<Button onClick={handleNextDocument} className="w-full" size="lg">
|
||||||
|
<Trans>View next document</Trans>
|
||||||
|
<ArrowRight className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{allDocumentsCompleted && (
|
||||||
|
<Alert className="mt-6 text-center">
|
||||||
|
<AlertTitle>
|
||||||
|
<Trans>All documents have been completed!</Trans>
|
||||||
|
</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
<Trans>Thank you for completing the signing process.</Trans>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,396 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { DocumentStatus, FieldType, SigningStatus } from '@prisma/client';
|
||||||
|
import { Loader, LucideChevronDown, LucideChevronUp, X } from 'lucide-react';
|
||||||
|
import { P, match } from 'ts-pattern';
|
||||||
|
|
||||||
|
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
|
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
|
||||||
|
import { trpc } from '@documenso/trpc/react';
|
||||||
|
import type {
|
||||||
|
TRemovedSignedFieldWithTokenMutationSchema,
|
||||||
|
TSignFieldWithTokenMutationSchema,
|
||||||
|
} from '@documenso/trpc/server/field-router/schema';
|
||||||
|
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
|
||||||
|
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||||
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||||
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
|
import PDFViewer from '@documenso/ui/primitives/pdf-viewer';
|
||||||
|
import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog';
|
||||||
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
|
import { useRequiredDocumentSigningContext } from '../../general/document-signing/document-signing-provider';
|
||||||
|
import { DocumentSigningRejectDialog } from '../../general/document-signing/document-signing-reject-dialog';
|
||||||
|
import { EmbedDocumentFields } from '../embed-document-fields';
|
||||||
|
|
||||||
|
interface MultiSignDocumentSigningViewProps {
|
||||||
|
token: string;
|
||||||
|
recipientId: number;
|
||||||
|
onBack: () => void;
|
||||||
|
onDocumentCompleted?: (data: { token: string; documentId: number; recipientId: number }) => void;
|
||||||
|
onDocumentRejected?: (data: {
|
||||||
|
token: string;
|
||||||
|
documentId: number;
|
||||||
|
recipientId: number;
|
||||||
|
reason: string;
|
||||||
|
}) => void;
|
||||||
|
onDocumentError?: () => void;
|
||||||
|
onDocumentReady?: () => void;
|
||||||
|
isNameLocked?: boolean;
|
||||||
|
allowDocumentRejection?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MultiSignDocumentSigningView = ({
|
||||||
|
token,
|
||||||
|
recipientId,
|
||||||
|
onBack,
|
||||||
|
onDocumentCompleted,
|
||||||
|
onDocumentRejected,
|
||||||
|
onDocumentError,
|
||||||
|
onDocumentReady,
|
||||||
|
isNameLocked = false,
|
||||||
|
allowDocumentRejection = false,
|
||||||
|
}: MultiSignDocumentSigningViewProps) => {
|
||||||
|
const { _ } = useLingui();
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const { fullName, email, signature, setFullName, setSignature } =
|
||||||
|
useRequiredDocumentSigningContext();
|
||||||
|
|
||||||
|
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
|
||||||
|
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false);
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [showPendingFieldTooltip, setShowPendingFieldTooltip] = useState(false);
|
||||||
|
|
||||||
|
const { data: document, isLoading } = trpc.embeddingPresign.getMultiSignDocument.useQuery(
|
||||||
|
{ token },
|
||||||
|
{
|
||||||
|
staleTime: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { mutateAsync: signFieldWithToken } = trpc.field.signFieldWithToken.useMutation();
|
||||||
|
const { mutateAsync: removeSignedFieldWithToken } =
|
||||||
|
trpc.field.removeSignedFieldWithToken.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: completeDocumentWithToken } =
|
||||||
|
trpc.recipient.completeDocumentWithToken.useMutation();
|
||||||
|
|
||||||
|
const hasSignatureField = document?.fields.some((field) => field.type === FieldType.SIGNATURE);
|
||||||
|
|
||||||
|
const [pendingFields, completedFields] = [
|
||||||
|
document?.fields.filter((field) => field.recipient.signingStatus !== SigningStatus.SIGNED) ??
|
||||||
|
[],
|
||||||
|
document?.fields.filter((field) => field.recipient.signingStatus === SigningStatus.SIGNED) ??
|
||||||
|
[],
|
||||||
|
];
|
||||||
|
|
||||||
|
const uninsertedFields = document?.fields.filter((field) => !field.inserted) ?? [];
|
||||||
|
|
||||||
|
const onSignField = async (payload: TSignFieldWithTokenMutationSchema) => {
|
||||||
|
try {
|
||||||
|
await signFieldWithToken(payload);
|
||||||
|
} catch (err) {
|
||||||
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
|
if (error.code === AppErrorCode.UNAUTHORIZED) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: _(msg`Error`),
|
||||||
|
description: _(msg`An error occurred while signing the document.`),
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onUnsignField = async (payload: TRemovedSignedFieldWithTokenMutationSchema) => {
|
||||||
|
try {
|
||||||
|
await removeSignedFieldWithToken(payload);
|
||||||
|
} catch (err) {
|
||||||
|
const error = AppError.parseError(err);
|
||||||
|
|
||||||
|
if (error.code === AppErrorCode.UNAUTHORIZED) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDocumentComplete = async () => {
|
||||||
|
try {
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
await completeDocumentWithToken({
|
||||||
|
documentId: document!.id,
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
|
||||||
|
onBack();
|
||||||
|
|
||||||
|
onDocumentCompleted?.({
|
||||||
|
token,
|
||||||
|
documentId: document!.id,
|
||||||
|
recipientId,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
onDocumentError?.();
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'Error',
|
||||||
|
description: 'Failed to complete the document. Please try again.',
|
||||||
|
variant: 'destructive',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onNextFieldClick = () => {
|
||||||
|
setShowPendingFieldTooltip(true);
|
||||||
|
|
||||||
|
setIsExpanded(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRejected = (reason: string) => {
|
||||||
|
if (onDocumentRejected && document) {
|
||||||
|
onDocumentRejected({
|
||||||
|
token,
|
||||||
|
documentId: document.id,
|
||||||
|
recipientId,
|
||||||
|
reason,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-background min-h-screen overflow-hidden">
|
||||||
|
<div id="document-field-portal-root" className="relative h-full w-full overflow-y-auto p-8">
|
||||||
|
{match({ isLoading, document })
|
||||||
|
.with({ isLoading: true }, () => (
|
||||||
|
<div className="flex min-h-[400px] w-full items-center justify-center">
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<Loader className="text-primary h-8 w-8 animate-spin" />
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
<Trans>Loading document...</Trans>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
.with({ isLoading: false, document: undefined }, () => (
|
||||||
|
<div className="flex min-h-[400px] w-full items-center justify-center">
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
<Trans>Failed to load document</Trans>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
.with({ document: P.nonNullable }, ({ document }) => (
|
||||||
|
<>
|
||||||
|
<div className="mx-auto flex w-full max-w-screen-xl items-baseline justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<h1 className="text-2xl font-semibold">{document.title}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button variant="ghost" size="sm" onClick={onBack} className="p-2">
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{allowDocumentRejection && (
|
||||||
|
<div className="embed--Actions mb-4 mt-8 flex w-full flex-row-reverse items-baseline justify-between">
|
||||||
|
<DocumentSigningRejectDialog
|
||||||
|
document={document}
|
||||||
|
token={token}
|
||||||
|
onRejected={onRejected}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="embed--DocumentContainer relative mx-auto mt-8 flex w-full max-w-screen-xl flex-col gap-x-6 gap-y-12 md:flex-row">
|
||||||
|
<div
|
||||||
|
className={cn('embed--DocumentViewer flex-1', {
|
||||||
|
'md:mx-auto md:max-w-2xl': document.status === DocumentStatus.COMPLETED,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<PDFViewer
|
||||||
|
documentData={document.documentData}
|
||||||
|
onDocumentLoad={() => {
|
||||||
|
setHasDocumentLoaded(true);
|
||||||
|
onDocumentReady?.();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Widget */}
|
||||||
|
{document.status !== DocumentStatus.COMPLETED && (
|
||||||
|
<div
|
||||||
|
key={isExpanded ? 'expanded' : 'collapsed'}
|
||||||
|
className="embed--DocumentWidgetContainer group/document-widget fixed bottom-8 left-0 z-50 h-fit max-h-[calc(100dvh-2rem)] w-full flex-shrink-0 px-6 md:sticky md:bottom-[unset] md:top-0 md:z-auto md:w-[350px] md:px-0"
|
||||||
|
data-expanded={isExpanded || undefined}
|
||||||
|
>
|
||||||
|
<div className="embed--DocumentWidget border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="embed--DocumentWidgetHeader">
|
||||||
|
<div className="flex items-center justify-between gap-x-2">
|
||||||
|
<h3 className="text-foreground text-xl font-semibold md:text-2xl">
|
||||||
|
<Trans>Sign document</Trans>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<Button variant="outline" className="h-8 w-8 p-0 md:hidden">
|
||||||
|
{isExpanded ? (
|
||||||
|
<LucideChevronDown
|
||||||
|
className="text-muted-foreground h-5 w-5"
|
||||||
|
onClick={() => setIsExpanded(false)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LucideChevronUp
|
||||||
|
className="text-muted-foreground h-5 w-5"
|
||||||
|
onClick={() => setIsExpanded(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="embed--DocumentWidgetContent hidden group-data-[expanded]/document-widget:block md:block">
|
||||||
|
<p className="text-muted-foreground mt-2 text-sm">
|
||||||
|
<Trans>Sign the document to complete the process.</Trans>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr className="border-border mb-8 mt-4" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<div className="embed--DocumentWidgetForm -mx-2 hidden px-2 group-data-[expanded]/document-widget:block md:block">
|
||||||
|
<div className="flex flex-1 flex-col gap-y-4">
|
||||||
|
{
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="full-name">
|
||||||
|
<Trans>Full Name</Trans>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
id="full-name"
|
||||||
|
className="bg-background mt-2"
|
||||||
|
disabled={isNameLocked}
|
||||||
|
value={fullName}
|
||||||
|
onChange={(e) => !isNameLocked && setFullName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="email">
|
||||||
|
<Trans>Email</Trans>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
className="bg-background mt-2"
|
||||||
|
value={email}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasSignatureField && (
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="Signature">
|
||||||
|
<Trans>Signature</Trans>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<SignaturePadDialog
|
||||||
|
className="mt-2"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
disableAnimation
|
||||||
|
value={signature ?? ''}
|
||||||
|
onChange={(v) => setSignature(v ?? '')}
|
||||||
|
typedSignatureEnabled={
|
||||||
|
document.documentMeta?.typedSignatureEnabled
|
||||||
|
}
|
||||||
|
uploadSignatureEnabled={
|
||||||
|
document.documentMeta?.uploadSignatureEnabled
|
||||||
|
}
|
||||||
|
drawSignatureEnabled={
|
||||||
|
document.documentMeta?.drawSignatureEnabled
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden flex-1 group-data-[expanded]/document-widget:block md:block" />
|
||||||
|
|
||||||
|
<div className="embed--DocumentWidgetFooter mt-4 hidden w-full grid-cols-2 items-center group-data-[expanded]/document-widget:grid md:grid">
|
||||||
|
{uninsertedFields.length > 0 ? (
|
||||||
|
<Button className="col-start-2" onClick={onNextFieldClick}>
|
||||||
|
<Trans>Next</Trans>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
className="col-span-2"
|
||||||
|
loading={isSubmitting}
|
||||||
|
onClick={onDocumentComplete}
|
||||||
|
>
|
||||||
|
<Trans>Complete</Trans>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{hasDocumentLoaded && (
|
||||||
|
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||||
|
{showPendingFieldTooltip && pendingFields.length > 0 && (
|
||||||
|
<FieldToolTip
|
||||||
|
key={pendingFields[0].id}
|
||||||
|
field={pendingFields[0]}
|
||||||
|
color="warning"
|
||||||
|
>
|
||||||
|
<Trans>Click to insert field</Trans>
|
||||||
|
</FieldToolTip>
|
||||||
|
)}
|
||||||
|
</ElementVisible>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Fields */}
|
||||||
|
{hasDocumentLoaded && (
|
||||||
|
<EmbedDocumentFields
|
||||||
|
fields={pendingFields}
|
||||||
|
metadata={document.documentMeta}
|
||||||
|
onSignField={onSignField}
|
||||||
|
onUnsignField={onUnsignField}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Completed fields */}
|
||||||
|
{document.status !== DocumentStatus.COMPLETED && (
|
||||||
|
<DocumentReadOnlyFields
|
||||||
|
documentMeta={document.documentMeta ?? undefined}
|
||||||
|
fields={completedFields}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
.otherwise(() => null)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -130,7 +130,7 @@ export const DirectTemplateConfigureForm = ({
|
|||||||
{...field}
|
{...field}
|
||||||
disabled={
|
disabled={
|
||||||
field.disabled ||
|
field.disabled ||
|
||||||
derivedRecipientAccessAuth !== null ||
|
derivedRecipientAccessAuth.length > 0 ||
|
||||||
user?.email !== undefined
|
user?.email !== undefined
|
||||||
}
|
}
|
||||||
placeholder="recipient@documenso.com"
|
placeholder="recipient@documenso.com"
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { Trans } from '@lingui/react/macro';
|
import { Trans } from '@lingui/react/macro';
|
||||||
import type { FieldType } from '@prisma/client';
|
import type { FieldType } from '@prisma/client';
|
||||||
|
import { ChevronLeftIcon } from 'lucide-react';
|
||||||
import { P, match } from 'ts-pattern';
|
import { P, match } from 'ts-pattern';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -7,6 +10,7 @@ import {
|
|||||||
type TRecipientActionAuth,
|
type TRecipientActionAuth,
|
||||||
type TRecipientActionAuthTypes,
|
type TRecipientActionAuthTypes,
|
||||||
} from '@documenso/lib/types/document-auth';
|
} from '@documenso/lib/types/document-auth';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -18,11 +22,12 @@ import {
|
|||||||
import { DocumentSigningAuth2FA } from './document-signing-auth-2fa';
|
import { DocumentSigningAuth2FA } from './document-signing-auth-2fa';
|
||||||
import { DocumentSigningAuthAccount } from './document-signing-auth-account';
|
import { DocumentSigningAuthAccount } from './document-signing-auth-account';
|
||||||
import { DocumentSigningAuthPasskey } from './document-signing-auth-passkey';
|
import { DocumentSigningAuthPasskey } from './document-signing-auth-passkey';
|
||||||
|
import { DocumentSigningAuthPassword } from './document-signing-auth-password';
|
||||||
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
|
|
||||||
export type DocumentSigningAuthDialogProps = {
|
export type DocumentSigningAuthDialogProps = {
|
||||||
title?: string;
|
title?: string;
|
||||||
documentAuthType: TRecipientActionAuthTypes;
|
availableAuthTypes: TRecipientActionAuthTypes[];
|
||||||
description?: string;
|
description?: string;
|
||||||
actionTarget: FieldType | 'DOCUMENT';
|
actionTarget: FieldType | 'DOCUMENT';
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -37,54 +42,158 @@ export type DocumentSigningAuthDialogProps = {
|
|||||||
export const DocumentSigningAuthDialog = ({
|
export const DocumentSigningAuthDialog = ({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
documentAuthType,
|
availableAuthTypes,
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
onReauthFormSubmit,
|
onReauthFormSubmit,
|
||||||
}: DocumentSigningAuthDialogProps) => {
|
}: DocumentSigningAuthDialogProps) => {
|
||||||
const { recipient, user, isCurrentlyAuthenticating } = useRequiredDocumentSigningAuthContext();
|
const { recipient, user, isCurrentlyAuthenticating } = useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
|
// Filter out EXPLICIT_NONE from available auth types for the chooser
|
||||||
|
const validAuthTypes = availableAuthTypes.filter(
|
||||||
|
(authType) => authType !== DocumentAuth.EXPLICIT_NONE,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [selectedAuthType, setSelectedAuthType] = useState<TRecipientActionAuthTypes | null>(() => {
|
||||||
|
// Auto-select if there's only one valid option
|
||||||
|
if (validAuthTypes.length === 1) {
|
||||||
|
return validAuthTypes[0];
|
||||||
|
}
|
||||||
|
// Return null if multiple options - show chooser
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
const handleOnOpenChange = (value: boolean) => {
|
const handleOnOpenChange = (value: boolean) => {
|
||||||
if (isCurrentlyAuthenticating) {
|
if (isCurrentlyAuthenticating) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset selected auth type when dialog closes
|
||||||
|
if (!value) {
|
||||||
|
setSelectedAuthType(() => {
|
||||||
|
if (validAuthTypes.length === 1) {
|
||||||
|
return validAuthTypes[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onOpenChange(value);
|
onOpenChange(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleBackToChooser = () => {
|
||||||
|
setSelectedAuthType(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// If no valid auth types available, don't render anything
|
||||||
|
if (validAuthTypes.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOnOpenChange}>
|
<Dialog open={open} onOpenChange={handleOnOpenChange}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{title || <Trans>Sign field</Trans>}</DialogTitle>
|
<DialogTitle>
|
||||||
|
{selectedAuthType && validAuthTypes.length > 1 && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleBackToChooser}
|
||||||
|
className="h-6 w-6 p-0"
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<span>{title || <Trans>Sign field</Trans>}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(!selectedAuthType || validAuthTypes.length === 1) &&
|
||||||
|
(title || <Trans>Sign field</Trans>)}
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{description || <Trans>Reauthentication is required to sign this field</Trans>}
|
{description || <Trans>Reauthentication is required to sign this field</Trans>}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{match({ documentAuthType, user })
|
{/* Show chooser if no auth type is selected and there are multiple options */}
|
||||||
.with(
|
{!selectedAuthType && validAuthTypes.length > 1 && (
|
||||||
{ documentAuthType: DocumentAuth.ACCOUNT },
|
<div className="space-y-4">
|
||||||
{ user: P.when((user) => !user || user.email !== recipient.email) }, // Assume all current auth methods requires them to be logged in.
|
<p className="text-muted-foreground text-sm">
|
||||||
() => <DocumentSigningAuthAccount onOpenChange={onOpenChange} />,
|
<Trans>Choose your preferred authentication method:</Trans>
|
||||||
)
|
</p>
|
||||||
.with({ documentAuthType: DocumentAuth.PASSKEY }, () => (
|
<div className="grid gap-2">
|
||||||
<DocumentSigningAuthPasskey
|
{validAuthTypes.map((authType) => (
|
||||||
open={open}
|
<Button
|
||||||
onOpenChange={onOpenChange}
|
key={authType}
|
||||||
onReauthFormSubmit={onReauthFormSubmit}
|
type="button"
|
||||||
/>
|
variant="outline"
|
||||||
))
|
className="h-auto justify-start p-4"
|
||||||
.with({ documentAuthType: DocumentAuth.TWO_FACTOR_AUTH }, () => (
|
onClick={() => setSelectedAuthType(authType)}
|
||||||
<DocumentSigningAuth2FA
|
>
|
||||||
open={open}
|
<div className="text-left">
|
||||||
onOpenChange={onOpenChange}
|
<div className="font-medium">
|
||||||
onReauthFormSubmit={onReauthFormSubmit}
|
{match(authType)
|
||||||
/>
|
.with(DocumentAuth.ACCOUNT, () => <Trans>Account</Trans>)
|
||||||
))
|
.with(DocumentAuth.PASSKEY, () => <Trans>Passkey</Trans>)
|
||||||
.with({ documentAuthType: DocumentAuth.EXPLICIT_NONE }, () => null)
|
.with(DocumentAuth.TWO_FACTOR_AUTH, () => <Trans>2FA</Trans>)
|
||||||
.exhaustive()}
|
.with(DocumentAuth.PASSWORD, () => <Trans>Password</Trans>)
|
||||||
|
.exhaustive()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-muted-foreground text-sm">
|
||||||
|
{match(authType)
|
||||||
|
.with(DocumentAuth.ACCOUNT, () => <Trans>Sign in to your account</Trans>)
|
||||||
|
.with(DocumentAuth.PASSKEY, () => (
|
||||||
|
<Trans>Use your passkey for authentication</Trans>
|
||||||
|
))
|
||||||
|
.with(DocumentAuth.TWO_FACTOR_AUTH, () => (
|
||||||
|
<Trans>Enter your 2FA code</Trans>
|
||||||
|
))
|
||||||
|
.with(DocumentAuth.PASSWORD, () => <Trans>Enter your password</Trans>)
|
||||||
|
.exhaustive()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Show the selected auth component */}
|
||||||
|
{selectedAuthType &&
|
||||||
|
match({ documentAuthType: selectedAuthType, user })
|
||||||
|
.with(
|
||||||
|
{ documentAuthType: DocumentAuth.ACCOUNT },
|
||||||
|
{ user: P.when((user) => !user || user.email !== recipient.email) }, // Assume all current auth methods requires them to be logged in.
|
||||||
|
() => <DocumentSigningAuthAccount onOpenChange={onOpenChange} />,
|
||||||
|
)
|
||||||
|
.with({ documentAuthType: DocumentAuth.PASSKEY }, () => (
|
||||||
|
<DocumentSigningAuthPasskey
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
onReauthFormSubmit={onReauthFormSubmit}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
.with({ documentAuthType: DocumentAuth.TWO_FACTOR_AUTH }, () => (
|
||||||
|
<DocumentSigningAuth2FA
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
onReauthFormSubmit={onReauthFormSubmit}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
.with({ documentAuthType: DocumentAuth.PASSWORD }, () => (
|
||||||
|
<DocumentSigningAuthPassword
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
onReauthFormSubmit={onReauthFormSubmit}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
.with({ documentAuthType: DocumentAuth.EXPLICIT_NONE }, () => null)
|
||||||
|
.exhaustive()}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,148 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { Trans } from '@lingui/react/macro';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
|
import { DocumentAuth, type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@documenso/ui/primitives/alert';
|
||||||
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
import { DialogFooter } from '@documenso/ui/primitives/dialog';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@documenso/ui/primitives/form/form';
|
||||||
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
|
|
||||||
|
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
|
||||||
|
|
||||||
|
export type DocumentSigningAuthPasswordProps = {
|
||||||
|
actionTarget?: 'FIELD' | 'DOCUMENT';
|
||||||
|
actionVerb?: string;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (value: boolean) => void;
|
||||||
|
onReauthFormSubmit: (values?: TRecipientActionAuth) => Promise<void> | void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ZPasswordAuthFormSchema = z.object({
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(1, { message: 'Password is required' })
|
||||||
|
.max(72, { message: 'Password must be at most 72 characters long' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
type TPasswordAuthFormSchema = z.infer<typeof ZPasswordAuthFormSchema>;
|
||||||
|
|
||||||
|
export const DocumentSigningAuthPassword = ({
|
||||||
|
actionTarget = 'FIELD',
|
||||||
|
actionVerb = 'sign',
|
||||||
|
onReauthFormSubmit,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: DocumentSigningAuthPasswordProps) => {
|
||||||
|
const { recipient, isCurrentlyAuthenticating, setIsCurrentlyAuthenticating } =
|
||||||
|
useRequiredDocumentSigningAuthContext();
|
||||||
|
|
||||||
|
const form = useForm<TPasswordAuthFormSchema>({
|
||||||
|
resolver: zodResolver(ZPasswordAuthFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
password: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [formErrorCode, setFormErrorCode] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ password }: TPasswordAuthFormSchema) => {
|
||||||
|
try {
|
||||||
|
setIsCurrentlyAuthenticating(true);
|
||||||
|
|
||||||
|
await onReauthFormSubmit({
|
||||||
|
type: DocumentAuth.PASSWORD,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsCurrentlyAuthenticating(false);
|
||||||
|
|
||||||
|
onOpenChange(false);
|
||||||
|
} catch (err) {
|
||||||
|
setIsCurrentlyAuthenticating(false);
|
||||||
|
|
||||||
|
const error = AppError.parseError(err);
|
||||||
|
setFormErrorCode(error.code);
|
||||||
|
|
||||||
|
// Todo: Alert.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset({
|
||||||
|
password: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
setFormErrorCode(null);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||||
|
<fieldset disabled={isCurrentlyAuthenticating}>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{formErrorCode && (
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<AlertTitle>
|
||||||
|
<Trans>Unauthorized</Trans>
|
||||||
|
</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
<Trans>
|
||||||
|
We were unable to verify your details. Please try again or contact support
|
||||||
|
</Trans>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel required>
|
||||||
|
<Trans>Password</Trans>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
{...field}
|
||||||
|
autoComplete="current-password"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button type="button" variant="secondary" onClick={() => onOpenChange(false)}>
|
||||||
|
<Trans>Cancel</Trans>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button type="submit" loading={isCurrentlyAuthenticating}>
|
||||||
|
<Trans>Sign</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,7 +1,6 @@
|
|||||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { type Document, FieldType, type Passkey, type Recipient } from '@prisma/client';
|
import { type Document, FieldType, type Passkey, type Recipient } from '@prisma/client';
|
||||||
import { match } from 'ts-pattern';
|
|
||||||
|
|
||||||
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
import type { SessionUser } from '@documenso/auth/server/lib/session/session';
|
||||||
import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth';
|
import { MAXIMUM_PASSKEYS } from '@documenso/lib/constants/auth';
|
||||||
@ -33,8 +32,8 @@ export type DocumentSigningAuthContextValue = {
|
|||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
recipientAuthOption: TRecipientAuthOptions;
|
recipientAuthOption: TRecipientAuthOptions;
|
||||||
setRecipient: (_value: Recipient) => void;
|
setRecipient: (_value: Recipient) => void;
|
||||||
derivedRecipientAccessAuth: TRecipientAccessAuthTypes | null;
|
derivedRecipientAccessAuth: TRecipientAccessAuthTypes[];
|
||||||
derivedRecipientActionAuth: TRecipientActionAuthTypes | null;
|
derivedRecipientActionAuth: TRecipientActionAuthTypes[];
|
||||||
isAuthRedirectRequired: boolean;
|
isAuthRedirectRequired: boolean;
|
||||||
isCurrentlyAuthenticating: boolean;
|
isCurrentlyAuthenticating: boolean;
|
||||||
setIsCurrentlyAuthenticating: (_value: boolean) => void;
|
setIsCurrentlyAuthenticating: (_value: boolean) => void;
|
||||||
@ -100,7 +99,7 @@ export const DocumentSigningAuthProvider = ({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
placeholderData: (previousData) => previousData,
|
placeholderData: (previousData) => previousData,
|
||||||
enabled: derivedRecipientActionAuth === DocumentAuth.PASSKEY,
|
enabled: derivedRecipientActionAuth?.includes(DocumentAuth.PASSKEY) ?? false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -121,21 +120,28 @@ export const DocumentSigningAuthProvider = ({
|
|||||||
* Will be `null` if the user still requires authentication, or if they don't need
|
* Will be `null` if the user still requires authentication, or if they don't need
|
||||||
* authentication.
|
* authentication.
|
||||||
*/
|
*/
|
||||||
const preCalculatedActionAuthOptions = match(derivedRecipientActionAuth)
|
const preCalculatedActionAuthOptions = useMemo(() => {
|
||||||
.with(DocumentAuth.ACCOUNT, () => {
|
if (
|
||||||
if (recipient.email !== user?.email) {
|
!derivedRecipientActionAuth ||
|
||||||
return null;
|
derivedRecipientActionAuth.length === 0 ||
|
||||||
}
|
derivedRecipientActionAuth.includes(DocumentAuth.EXPLICIT_NONE)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
type: DocumentAuth.EXPLICIT_NONE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
derivedRecipientActionAuth.includes(DocumentAuth.ACCOUNT) &&
|
||||||
|
user?.email == recipient.email
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
type: DocumentAuth.ACCOUNT,
|
type: DocumentAuth.ACCOUNT,
|
||||||
};
|
};
|
||||||
})
|
}
|
||||||
.with(DocumentAuth.EXPLICIT_NONE, () => ({
|
|
||||||
type: DocumentAuth.EXPLICIT_NONE,
|
return null;
|
||||||
}))
|
}, [derivedRecipientActionAuth, user, recipient]);
|
||||||
.with(DocumentAuth.PASSKEY, DocumentAuth.TWO_FACTOR_AUTH, null, () => null)
|
|
||||||
.exhaustive();
|
|
||||||
|
|
||||||
const executeActionAuthProcedure = async (options: ExecuteActionAuthProcedureOptions) => {
|
const executeActionAuthProcedure = async (options: ExecuteActionAuthProcedureOptions) => {
|
||||||
// Directly run callback if no auth required.
|
// Directly run callback if no auth required.
|
||||||
@ -170,7 +176,8 @@ export const DocumentSigningAuthProvider = ({
|
|||||||
// Assume that a user must be logged in for any auth requirements.
|
// Assume that a user must be logged in for any auth requirements.
|
||||||
const isAuthRedirectRequired = Boolean(
|
const isAuthRedirectRequired = Boolean(
|
||||||
derivedRecipientActionAuth &&
|
derivedRecipientActionAuth &&
|
||||||
derivedRecipientActionAuth !== DocumentAuth.EXPLICIT_NONE &&
|
derivedRecipientActionAuth.length > 0 &&
|
||||||
|
!derivedRecipientActionAuth.includes(DocumentAuth.EXPLICIT_NONE) &&
|
||||||
user?.email !== recipient.email,
|
user?.email !== recipient.email,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -208,7 +215,7 @@ export const DocumentSigningAuthProvider = ({
|
|||||||
onOpenChange={() => setDocumentAuthDialogPayload(null)}
|
onOpenChange={() => setDocumentAuthDialogPayload(null)}
|
||||||
onReauthFormSubmit={documentAuthDialogPayload.onReauthFormSubmit}
|
onReauthFormSubmit={documentAuthDialogPayload.onReauthFormSubmit}
|
||||||
actionTarget={documentAuthDialogPayload.actionTarget}
|
actionTarget={documentAuthDialogPayload.actionTarget}
|
||||||
documentAuthType={derivedRecipientActionAuth}
|
availableAuthTypes={derivedRecipientActionAuth}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</DocumentSigningAuthContext.Provider>
|
</DocumentSigningAuthContext.Provider>
|
||||||
@ -217,7 +224,7 @@ export const DocumentSigningAuthProvider = ({
|
|||||||
|
|
||||||
type ExecuteActionAuthProcedureOptions = Omit<
|
type ExecuteActionAuthProcedureOptions = Omit<
|
||||||
DocumentSigningAuthDialogProps,
|
DocumentSigningAuthDialogProps,
|
||||||
'open' | 'onOpenChange' | 'documentAuthType' | 'recipientRole'
|
'open' | 'onOpenChange' | 'documentAuthType' | 'recipientRole' | 'availableAuthTypes'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
DocumentSigningAuthProvider.displayName = 'DocumentSigningAuthProvider';
|
DocumentSigningAuthProvider.displayName = 'DocumentSigningAuthProvider';
|
||||||
|
|||||||
@ -44,6 +44,7 @@ const AUTO_SIGNABLE_FIELD_TYPES: string[] = [
|
|||||||
// other field types.
|
// other field types.
|
||||||
const NON_AUTO_SIGNABLE_ACTION_AUTH_TYPES: string[] = [
|
const NON_AUTO_SIGNABLE_ACTION_AUTH_TYPES: string[] = [
|
||||||
DocumentAuth.PASSKEY,
|
DocumentAuth.PASSKEY,
|
||||||
|
DocumentAuth.PASSWORD,
|
||||||
DocumentAuth.TWO_FACTOR_AUTH,
|
DocumentAuth.TWO_FACTOR_AUTH,
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -96,8 +97,8 @@ export const DocumentSigningAutoSign = ({ recipient, fields }: DocumentSigningAu
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const actionAuthAllowsAutoSign = !NON_AUTO_SIGNABLE_ACTION_AUTH_TYPES.includes(
|
const actionAuthAllowsAutoSign = derivedRecipientActionAuth.every(
|
||||||
derivedRecipientActionAuth ?? '',
|
(actionAuth) => !NON_AUTO_SIGNABLE_ACTION_AUTH_TYPES.includes(actionAuth),
|
||||||
);
|
);
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async () => {
|
||||||
@ -110,16 +111,16 @@ export const DocumentSigningAutoSign = ({ recipient, fields }: DocumentSigningAu
|
|||||||
.with(FieldType.DATE, () => new Date().toISOString())
|
.with(FieldType.DATE, () => new Date().toISOString())
|
||||||
.otherwise(() => '');
|
.otherwise(() => '');
|
||||||
|
|
||||||
const authOptions = match(derivedRecipientActionAuth)
|
const authOptions = match(derivedRecipientActionAuth.at(0))
|
||||||
.with(DocumentAuth.ACCOUNT, () => ({
|
.with(DocumentAuth.ACCOUNT, () => ({
|
||||||
type: DocumentAuth.ACCOUNT,
|
type: DocumentAuth.ACCOUNT,
|
||||||
}))
|
}))
|
||||||
.with(DocumentAuth.EXPLICIT_NONE, () => ({
|
.with(DocumentAuth.EXPLICIT_NONE, () => ({
|
||||||
type: DocumentAuth.EXPLICIT_NONE,
|
type: DocumentAuth.EXPLICIT_NONE,
|
||||||
}))
|
}))
|
||||||
.with(null, () => undefined)
|
.with(undefined, () => undefined)
|
||||||
.with(
|
.with(
|
||||||
P.union(DocumentAuth.PASSKEY, DocumentAuth.TWO_FACTOR_AUTH),
|
P.union(DocumentAuth.PASSKEY, DocumentAuth.TWO_FACTOR_AUTH, DocumentAuth.PASSWORD),
|
||||||
// This is a bit dirty, but the sentinel value used here is incredibly short-lived.
|
// This is a bit dirty, but the sentinel value used here is incredibly short-lived.
|
||||||
() => 'NOT_SUPPORTED' as const,
|
() => 'NOT_SUPPORTED' as const,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -92,8 +92,6 @@ export const DocumentSigningCompleteDialog = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onFormSubmit = async (data: TNextSignerFormSchema) => {
|
const onFormSubmit = async (data: TNextSignerFormSchema) => {
|
||||||
console.log('data', data);
|
|
||||||
console.log('form.formState.errors', form.formState.errors);
|
|
||||||
try {
|
try {
|
||||||
if (allowDictateNextSigner && data.name && data.email) {
|
if (allowDictateNextSigner && data.name && data.email) {
|
||||||
await onSignatureComplete({ name: data.name, email: data.email });
|
await onSignatureComplete({ name: data.name, email: data.email });
|
||||||
|
|||||||
@ -166,7 +166,7 @@ export const DocumentSigningFieldContainer = ({
|
|||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
|
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
className="border-0 bg-orange-300 fill-orange-300 font-bold text-orange-900"
|
className="border-0 bg-orange-300 fill-orange-300 text-orange-900"
|
||||||
sideOffset={2}
|
sideOffset={2}
|
||||||
>
|
>
|
||||||
{tooltipText && <p>{tooltipText}</p>}
|
{tooltipText && <p>{tooltipText}</p>}
|
||||||
|
|||||||
@ -183,8 +183,8 @@ export const DocumentEditForm = ({
|
|||||||
title: data.title,
|
title: data.title,
|
||||||
externalId: data.externalId || null,
|
externalId: data.externalId || null,
|
||||||
visibility: data.visibility,
|
visibility: data.visibility,
|
||||||
globalAccessAuth: data.globalAccessAuth ?? null,
|
globalAccessAuth: data.globalAccessAuth ?? [],
|
||||||
globalActionAuth: data.globalActionAuth ?? null,
|
globalActionAuth: data.globalActionAuth ?? [],
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
timezone,
|
timezone,
|
||||||
@ -229,7 +229,7 @@ export const DocumentEditForm = ({
|
|||||||
recipients: data.signers.map((signer) => ({
|
recipients: data.signers.map((signer) => ({
|
||||||
...signer,
|
...signer,
|
||||||
// Explicitly set to null to indicate we want to remove auth if required.
|
// Explicitly set to null to indicate we want to remove auth if required.
|
||||||
actionAuth: signer.actionAuth || null,
|
actionAuth: signer.actionAuth ?? [],
|
||||||
})),
|
})),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -81,11 +81,15 @@ export const DocumentHistorySheet = ({
|
|||||||
* @param text The text to format
|
* @param text The text to format
|
||||||
* @returns The formatted text
|
* @returns The formatted text
|
||||||
*/
|
*/
|
||||||
const formatGenericText = (text?: string | null) => {
|
const formatGenericText = (text?: string | string[] | null): string => {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(text)) {
|
||||||
|
return text.map((t) => formatGenericText(t)).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
return (text.charAt(0).toUpperCase() + text.slice(1).toLowerCase()).replaceAll('_', ' ');
|
return (text.charAt(0).toUpperCase() + text.slice(1).toLowerCase()).replaceAll('_', ' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -245,11 +249,19 @@ export const DocumentHistorySheet = ({
|
|||||||
values={[
|
values={[
|
||||||
{
|
{
|
||||||
key: 'Old',
|
key: 'Old',
|
||||||
value: DOCUMENT_AUTH_TYPES[data.from || '']?.value || 'None',
|
value: Array.isArray(data.from)
|
||||||
|
? data.from
|
||||||
|
.map((f) => DOCUMENT_AUTH_TYPES[f]?.value || 'None')
|
||||||
|
.join(', ')
|
||||||
|
: DOCUMENT_AUTH_TYPES[data.from || '']?.value || 'None',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'New',
|
key: 'New',
|
||||||
value: DOCUMENT_AUTH_TYPES[data.to || '']?.value || 'None',
|
value: Array.isArray(data.to)
|
||||||
|
? data.to
|
||||||
|
.map((f) => DOCUMENT_AUTH_TYPES[f]?.value || 'None')
|
||||||
|
.join(', ')
|
||||||
|
: DOCUMENT_AUTH_TYPES[data.to || '']?.value || 'None',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -134,8 +134,8 @@ export const TemplateEditForm = ({
|
|||||||
title: data.title,
|
title: data.title,
|
||||||
externalId: data.externalId || null,
|
externalId: data.externalId || null,
|
||||||
visibility: data.visibility,
|
visibility: data.visibility,
|
||||||
globalAccessAuth: data.globalAccessAuth ?? null,
|
globalAccessAuth: data.globalAccessAuth ?? [],
|
||||||
globalActionAuth: data.globalActionAuth ?? null,
|
globalActionAuth: data.globalActionAuth ?? [],
|
||||||
},
|
},
|
||||||
meta: {
|
meta: {
|
||||||
...data.meta,
|
...data.meta,
|
||||||
|
|||||||
@ -1,96 +1,47 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { msg } from '@lingui/core/macro';
|
||||||
|
import { useLingui } from '@lingui/react';
|
||||||
import { Plural, Trans } from '@lingui/react/macro';
|
|
||||||
import { WebhookTriggerEvents } from '@prisma/client';
|
import { WebhookTriggerEvents } from '@prisma/client';
|
||||||
import { Check, ChevronsUpDown } from 'lucide-react';
|
|
||||||
|
|
||||||
import { toFriendlyWebhookEventName } from '@documenso/lib/universal/webhook/to-friendly-webhook-event-name';
|
import { toFriendlyWebhookEventName } from '@documenso/lib/universal/webhook/to-friendly-webhook-event-name';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { MultiSelect, type Option } from '@documenso/ui/primitives/multiselect';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
|
||||||
import {
|
|
||||||
Command,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
} from '@documenso/ui/primitives/command';
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
|
||||||
|
|
||||||
import { truncateTitle } from '~/utils/truncate-title';
|
|
||||||
|
|
||||||
type WebhookMultiSelectComboboxProps = {
|
type WebhookMultiSelectComboboxProps = {
|
||||||
listValues: string[];
|
listValues: string[];
|
||||||
onChange: (_values: string[]) => void;
|
onChange: (_values: string[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const triggerEvents = Object.values(WebhookTriggerEvents).map((event) => ({
|
||||||
|
value: event,
|
||||||
|
label: toFriendlyWebhookEventName(event),
|
||||||
|
}));
|
||||||
|
|
||||||
export const WebhookMultiSelectCombobox = ({
|
export const WebhookMultiSelectCombobox = ({
|
||||||
listValues,
|
listValues,
|
||||||
onChange,
|
onChange,
|
||||||
}: WebhookMultiSelectComboboxProps) => {
|
}: WebhookMultiSelectComboboxProps) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const { _ } = useLingui();
|
||||||
const [selectedValues, setSelectedValues] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const triggerEvents = Object.values(WebhookTriggerEvents);
|
const value = listValues.map((event) => ({
|
||||||
|
value: event,
|
||||||
|
label: toFriendlyWebhookEventName(event),
|
||||||
|
}));
|
||||||
|
|
||||||
useEffect(() => {
|
const onMutliSelectChange = (options: Option[]) => {
|
||||||
setSelectedValues(listValues);
|
onChange(options.map((option) => option.value));
|
||||||
}, [listValues]);
|
|
||||||
|
|
||||||
const allEvents = [...new Set([...triggerEvents, ...selectedValues])];
|
|
||||||
|
|
||||||
const handleSelect = (currentValue: string) => {
|
|
||||||
let newSelectedValues;
|
|
||||||
|
|
||||||
if (selectedValues.includes(currentValue)) {
|
|
||||||
newSelectedValues = selectedValues.filter((value) => value !== currentValue);
|
|
||||||
} else {
|
|
||||||
newSelectedValues = [...selectedValues, currentValue];
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedValues(newSelectedValues);
|
|
||||||
onChange(newSelectedValues);
|
|
||||||
setIsOpen(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
<MultiSelect
|
||||||
<PopoverTrigger asChild>
|
commandProps={{
|
||||||
<Button
|
label: _(msg`Select triggers`),
|
||||||
variant="outline"
|
}}
|
||||||
role="combobox"
|
defaultOptions={triggerEvents}
|
||||||
aria-expanded={isOpen}
|
value={value}
|
||||||
className="w-[200px] justify-between"
|
onChange={onMutliSelectChange}
|
||||||
>
|
placeholder={_(msg`Select triggers`)}
|
||||||
<Plural value={selectedValues.length} zero="Select values" other="# selected..." />
|
hideClearAllButton
|
||||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
hidePlaceholderWhenSelected
|
||||||
</Button>
|
emptyIndicator={<p className="text-center text-sm">No triggers available</p>}
|
||||||
</PopoverTrigger>
|
/>
|
||||||
<PopoverContent className="z-9999 w-full max-w-[280px] p-0">
|
|
||||||
<Command>
|
|
||||||
<CommandInput
|
|
||||||
placeholder={truncateTitle(
|
|
||||||
selectedValues.map((v) => toFriendlyWebhookEventName(v)).join(', '),
|
|
||||||
15,
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<CommandEmpty>
|
|
||||||
<Trans>No value found.</Trans>
|
|
||||||
</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{allEvents.map((value: string, i: number) => (
|
|
||||||
<CommandItem key={i} onSelect={() => handleSelect(value)}>
|
|
||||||
<Check
|
|
||||||
className={cn(
|
|
||||||
'mr-2 h-4 w-4',
|
|
||||||
selectedValues.includes(value) ? 'opacity-100' : 'opacity-0',
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{toFriendlyWebhookEventName(value)}
|
|
||||||
</CommandItem>
|
|
||||||
))}
|
|
||||||
</CommandGroup>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
Edit,
|
Edit,
|
||||||
EyeIcon,
|
EyeIcon,
|
||||||
|
FileDown,
|
||||||
|
FolderInput,
|
||||||
Loader,
|
Loader,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
MoveRight,
|
MoveRight,
|
||||||
@ -182,7 +184,7 @@ export const DocumentsTableActionDropdown = ({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<DropdownMenuItem onClick={onDownloadOriginalClick}>
|
<DropdownMenuItem onClick={onDownloadOriginalClick}>
|
||||||
<Download className="mr-2 h-4 w-4" />
|
<FileDown className="mr-2 h-4 w-4" />
|
||||||
<Trans>Download Original</Trans>
|
<Trans>Download Original</Trans>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
@ -201,7 +203,7 @@ export const DocumentsTableActionDropdown = ({
|
|||||||
|
|
||||||
{onMoveDocument && (
|
{onMoveDocument && (
|
||||||
<DropdownMenuItem onClick={onMoveDocument} onSelect={(e) => e.preventDefault()}>
|
<DropdownMenuItem onClick={onMoveDocument} onSelect={(e) => e.preventDefault()}>
|
||||||
<MoveRight className="mr-2 h-4 w-4" />
|
<FolderInput className="mr-2 h-4 w-4" />
|
||||||
<Trans>Move to Folder</Trans>
|
<Trans>Move to Folder</Trans>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -69,8 +69,6 @@ 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, SigningStatus, TeamMemberRole } from '@prisma/client';
|
import { DocumentStatus, 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,25 +111,10 @@ 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,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +124,7 @@ export default function DocumentPage() {
|
|||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
const { user } = useSession();
|
const { user } = useSession();
|
||||||
|
|
||||||
const { document, documentRootPath, fields, isSingleSignerDocument } = loaderData;
|
const { document, documentRootPath, fields } = loaderData;
|
||||||
|
|
||||||
const { recipients, documentData, documentMeta } = document;
|
const { recipients, documentData, documentMeta } = document;
|
||||||
|
|
||||||
@ -252,10 +237,6 @@ 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',
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useLingui } from '@lingui/react';
|
|||||||
import { FieldType, SigningStatus } from '@prisma/client';
|
import { FieldType, SigningStatus } from '@prisma/client';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { redirect } from 'react-router';
|
import { redirect } from 'react-router';
|
||||||
|
import { prop, sortBy } from 'remeda';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
import { UAParser } from 'ua-parser-js';
|
import { UAParser } from 'ua-parser-js';
|
||||||
import { renderSVG } from 'uqr';
|
import { renderSVG } from 'uqr';
|
||||||
@ -17,6 +18,7 @@ import {
|
|||||||
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
|
import { getEntireDocument } from '@documenso/lib/server-only/admin/get-entire-document';
|
||||||
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';
|
||||||
import { getDocumentCertificateAuditLogs } from '@documenso/lib/server-only/document/get-document-certificate-audit-logs';
|
import { getDocumentCertificateAuditLogs } from '@documenso/lib/server-only/document/get-document-certificate-audit-logs';
|
||||||
|
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||||
import { getTranslations } from '@documenso/lib/utils/i18n';
|
import { getTranslations } from '@documenso/lib/utils/i18n';
|
||||||
@ -62,6 +64,10 @@ export async function loader({ request }: Route.LoaderArgs) {
|
|||||||
throw redirect('/');
|
throw redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const team = document.teamId
|
||||||
|
? await getTeamById({ teamId: document.teamId, userId: document.userId })
|
||||||
|
: null;
|
||||||
|
|
||||||
const isPlatformDocument = await isDocumentPlatform(document);
|
const isPlatformDocument = await isDocumentPlatform(document);
|
||||||
|
|
||||||
const documentLanguage = ZSupportedLanguageCodeSchema.parse(document.documentMeta?.language);
|
const documentLanguage = ZSupportedLanguageCodeSchema.parse(document.documentMeta?.language);
|
||||||
@ -74,6 +80,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
document,
|
document,
|
||||||
|
team,
|
||||||
documentLanguage,
|
documentLanguage,
|
||||||
isPlatformDocument,
|
isPlatformDocument,
|
||||||
auditLogs,
|
auditLogs,
|
||||||
@ -91,7 +98,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
|||||||
* Update: Maybe <Trans> tags work now after RR7 migration.
|
* Update: Maybe <Trans> tags work now after RR7 migration.
|
||||||
*/
|
*/
|
||||||
export default function SigningCertificate({ loaderData }: Route.ComponentProps) {
|
export default function SigningCertificate({ loaderData }: Route.ComponentProps) {
|
||||||
const { document, documentLanguage, isPlatformDocument, auditLogs, messages } = loaderData;
|
const { document, team, documentLanguage, isPlatformDocument, auditLogs, messages } = loaderData;
|
||||||
|
|
||||||
const { i18n, _ } = useLingui();
|
const { i18n, _ } = useLingui();
|
||||||
|
|
||||||
@ -127,18 +134,30 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
recipientAuth: recipient.authOptions,
|
recipientAuth: recipient.authOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
let authLevel = match(extractedAuthMethods.derivedRecipientActionAuth)
|
const insertedAuditLogsWithFieldAuth = sortBy(
|
||||||
|
auditLogs.DOCUMENT_FIELD_INSERTED.filter(
|
||||||
|
(log) => log.data.recipientId === recipient.id && log.data.fieldSecurity,
|
||||||
|
),
|
||||||
|
[prop('createdAt'), 'desc'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const actionAuthMethod = insertedAuditLogsWithFieldAuth.at(0)?.data?.fieldSecurity?.type;
|
||||||
|
|
||||||
|
let authLevel = match(actionAuthMethod)
|
||||||
.with('ACCOUNT', () => _(msg`Account Re-Authentication`))
|
.with('ACCOUNT', () => _(msg`Account Re-Authentication`))
|
||||||
.with('TWO_FACTOR_AUTH', () => _(msg`Two-Factor Re-Authentication`))
|
.with('TWO_FACTOR_AUTH', () => _(msg`Two-Factor Re-Authentication`))
|
||||||
|
.with('PASSWORD', () => _(msg`Password Re-Authentication`))
|
||||||
.with('PASSKEY', () => _(msg`Passkey Re-Authentication`))
|
.with('PASSKEY', () => _(msg`Passkey Re-Authentication`))
|
||||||
.with('EXPLICIT_NONE', () => _(msg`Email`))
|
.with('EXPLICIT_NONE', () => _(msg`Email`))
|
||||||
.with(null, () => null)
|
.with(undefined, () => null)
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
|
|
||||||
if (!authLevel) {
|
if (!authLevel) {
|
||||||
authLevel = match(extractedAuthMethods.derivedRecipientAccessAuth)
|
const accessAuthMethod = extractedAuthMethods.derivedRecipientAccessAuth.at(0);
|
||||||
|
|
||||||
|
authLevel = match(accessAuthMethod)
|
||||||
.with('ACCOUNT', () => _(msg`Account Authentication`))
|
.with('ACCOUNT', () => _(msg`Account Authentication`))
|
||||||
.with(null, () => _(msg`Email`))
|
.with(undefined, () => _(msg`Email`))
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,7 +362,7 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{isPlatformDocument && (
|
{!isPlatformDocument && !team?.teamGlobalSettings?.brandingHidePoweredBy && (
|
||||||
<div className="my-8 flex-row-reverse space-y-4">
|
<div className="my-8 flex-row-reverse space-y-4">
|
||||||
<div className="flex items-end justify-end gap-x-4">
|
<div className="flex items-end justify-end gap-x-4">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -47,9 +47,9 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Ensure typesafety when we add more options.
|
// Ensure typesafety when we add more options.
|
||||||
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
const isAccessAuthValid = match(derivedRecipientAccessAuth.at(0))
|
||||||
.with(DocumentAccessAuth.ACCOUNT, () => Boolean(session.user))
|
.with(DocumentAccessAuth.ACCOUNT, () => Boolean(session.user))
|
||||||
.with(null, () => true)
|
.with(undefined, () => true)
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
|
|
||||||
if (!isAccessAuthValid) {
|
if (!isAccessAuthValid) {
|
||||||
|
|||||||
@ -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-x-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-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 },
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -68,9 +68,9 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
const isAccessAuthValid = match(derivedRecipientAccessAuth.at(0))
|
||||||
.with(DocumentAccessAuth.ACCOUNT, () => user !== null)
|
.with(DocumentAccessAuth.ACCOUNT, () => !!user)
|
||||||
.with(null, () => true)
|
.with(undefined, () => true)
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
|
|
||||||
if (!isAccessAuthValid) {
|
if (!isAccessAuthValid) {
|
||||||
|
|||||||
@ -81,9 +81,9 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
|||||||
documentAuth: document.authOptions,
|
documentAuth: document.authOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
const isAccessAuthValid = match(derivedRecipientAccessAuth.at(0))
|
||||||
.with(DocumentAccessAuth.ACCOUNT, () => user !== null)
|
.with(DocumentAccessAuth.ACCOUNT, () => user && user.email === recipient.email)
|
||||||
.with(null, () => true)
|
.with(undefined, () => true)
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
|
|
||||||
if (!isAccessAuthValid) {
|
if (!isAccessAuthValid) {
|
||||||
|
|||||||
325
apps/remix/app/routes/embed+/v1+/multisign+/_index.tsx
Normal file
325
apps/remix/app/routes/embed+/v1+/multisign+/_index.tsx
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
import { useEffect, useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { SigningStatus } from '@prisma/client';
|
||||||
|
import { useRevalidator } from 'react-router';
|
||||||
|
|
||||||
|
import { getOptionalSession } from '@documenso/auth/server/lib/utils/get-session';
|
||||||
|
import { isCommunityPlan as isUserCommunityPlan } from '@documenso/ee/server-only/util/is-community-plan';
|
||||||
|
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
|
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
||||||
|
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
|
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||||
|
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
|
||||||
|
|
||||||
|
import { BrandingLogo } from '~/components/general/branding-logo';
|
||||||
|
import { DocumentSigningAuthProvider } from '~/components/general/document-signing/document-signing-auth-provider';
|
||||||
|
import { DocumentSigningProvider } from '~/components/general/document-signing/document-signing-provider';
|
||||||
|
import { DocumentSigningRecipientProvider } from '~/components/general/document-signing/document-signing-recipient-provider';
|
||||||
|
import { ZSignDocumentEmbedDataSchema } from '~/types/embed-document-sign-schema';
|
||||||
|
import { injectCss } from '~/utils/css-vars';
|
||||||
|
import { superLoaderJson, useSuperLoaderData } from '~/utils/super-json-loader';
|
||||||
|
|
||||||
|
import { MultiSignDocumentList } from '../../../../components/embed/multisign/multi-sign-document-list';
|
||||||
|
import { MultiSignDocumentSigningView } from '../../../../components/embed/multisign/multi-sign-document-signing-view';
|
||||||
|
import type { Route } from './+types/_index';
|
||||||
|
|
||||||
|
export async function loader({ request }: Route.LoaderArgs) {
|
||||||
|
const { user } = await getOptionalSession(request);
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
const tokens = url.searchParams.getAll('token');
|
||||||
|
|
||||||
|
const envelopes = await Promise.all(
|
||||||
|
tokens.map(async (token) => {
|
||||||
|
const document = await getDocumentAndSenderByToken({
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
|
||||||
|
const recipient = await getRecipientByToken({ token });
|
||||||
|
|
||||||
|
return { document, recipient };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check the first envelope for whitelabelling settings (assuming all docs are from same team)
|
||||||
|
const firstDocument = envelopes[0]?.document;
|
||||||
|
|
||||||
|
if (!firstDocument) {
|
||||||
|
return superLoaderJson({
|
||||||
|
envelopes,
|
||||||
|
user,
|
||||||
|
hidePoweredBy: false,
|
||||||
|
allowWhitelabelling: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const team = firstDocument.teamId
|
||||||
|
? await getTeamById({ teamId: firstDocument.teamId, userId: firstDocument.userId }).catch(
|
||||||
|
() => null,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const [isPlatformDocument, isEnterpriseDocument, isCommunityPlan] = await Promise.all([
|
||||||
|
isDocumentPlatform(firstDocument),
|
||||||
|
isUserEnterprise({
|
||||||
|
userId: firstDocument.userId,
|
||||||
|
teamId: firstDocument.teamId ?? undefined,
|
||||||
|
}),
|
||||||
|
isUserCommunityPlan({
|
||||||
|
userId: firstDocument.userId,
|
||||||
|
teamId: firstDocument.teamId ?? undefined,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const hidePoweredBy = team?.teamGlobalSettings?.brandingHidePoweredBy ?? false;
|
||||||
|
const allowWhitelabelling = isCommunityPlan || isPlatformDocument || isEnterpriseDocument;
|
||||||
|
|
||||||
|
return superLoaderJson({
|
||||||
|
envelopes,
|
||||||
|
user,
|
||||||
|
hidePoweredBy,
|
||||||
|
allowWhitelabelling,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MultisignPage() {
|
||||||
|
const { envelopes, user, hidePoweredBy, allowWhitelabelling } =
|
||||||
|
useSuperLoaderData<typeof loader>();
|
||||||
|
const revalidator = useRevalidator();
|
||||||
|
|
||||||
|
const [selectedDocument, setSelectedDocument] = useState<
|
||||||
|
(typeof envelopes)[number]['document'] | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
// Additional state for embed functionality
|
||||||
|
const [hasFinishedInit, setHasFinishedInit] = useState(false);
|
||||||
|
const [isNameLocked, setIsNameLocked] = useState(false);
|
||||||
|
const [allowDocumentRejection, setAllowDocumentRejection] = useState(false);
|
||||||
|
const [showOtherRecipientsCompletedFields, setShowOtherRecipientsCompletedFields] =
|
||||||
|
useState(false);
|
||||||
|
const [embedFullName, setEmbedFullName] = useState('');
|
||||||
|
|
||||||
|
// Check if all documents are completed
|
||||||
|
const isCompleted = envelopes.every(
|
||||||
|
(envelope) => envelope.recipient.signingStatus === SigningStatus.SIGNED,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedRecipient = selectedDocument
|
||||||
|
? envelopes.find((e) => e.document.id === selectedDocument.id)?.recipient
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const onSelectDocument = (document: (typeof envelopes)[number]['document']) => {
|
||||||
|
setSelectedDocument(document);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBackToDocumentList = () => {
|
||||||
|
setSelectedDocument(null);
|
||||||
|
// Revalidate to fetch fresh data when returning to document list
|
||||||
|
void revalidator.revalidate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDocumentCompleted = (data: {
|
||||||
|
token: string;
|
||||||
|
documentId: number;
|
||||||
|
recipientId: number;
|
||||||
|
}) => {
|
||||||
|
// Send postMessage for individual document completion
|
||||||
|
if (window.parent) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: 'document-completed',
|
||||||
|
data: {
|
||||||
|
token: data.token,
|
||||||
|
documentId: data.documentId,
|
||||||
|
recipientId: data.recipientId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDocumentRejected = (data: {
|
||||||
|
token: string;
|
||||||
|
documentId: number;
|
||||||
|
recipientId: number;
|
||||||
|
reason: string;
|
||||||
|
}) => {
|
||||||
|
// Send postMessage for document rejection
|
||||||
|
if (window.parent) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: 'document-rejected',
|
||||||
|
data: {
|
||||||
|
token: data.token,
|
||||||
|
documentId: data.documentId,
|
||||||
|
recipientId: data.recipientId,
|
||||||
|
reason: data.reason,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDocumentError = () => {
|
||||||
|
// Send postMessage for document error
|
||||||
|
if (window.parent) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: 'document-error',
|
||||||
|
data: null,
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDocumentReady = () => {
|
||||||
|
// Send postMessage when document is ready
|
||||||
|
if (window.parent) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: 'document-ready',
|
||||||
|
data: null,
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAllDocumentsCompleted = () => {
|
||||||
|
// Send postMessage for all documents completion
|
||||||
|
if (window.parent) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
action: 'all-documents-completed',
|
||||||
|
data: {
|
||||||
|
documents: envelopes.map((envelope) => ({
|
||||||
|
token: envelope.recipient.token,
|
||||||
|
documentId: envelope.document.id,
|
||||||
|
recipientId: envelope.recipient.id,
|
||||||
|
action:
|
||||||
|
envelope.recipient.signingStatus === SigningStatus.SIGNED
|
||||||
|
? 'document-completed'
|
||||||
|
: 'document-rejected',
|
||||||
|
reason:
|
||||||
|
envelope.recipient.signingStatus === SigningStatus.REJECTED
|
||||||
|
? envelope.recipient.rejectionReason
|
||||||
|
: undefined,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'*',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
envelopes.every((envelope) => envelope.recipient.signingStatus !== SigningStatus.NOT_SIGNED)
|
||||||
|
) {
|
||||||
|
onAllDocumentsCompleted();
|
||||||
|
}
|
||||||
|
}, [envelopes]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const hash = window.location.hash.slice(1);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = ZSignDocumentEmbedDataSchema.parse(JSON.parse(decodeURIComponent(atob(hash))));
|
||||||
|
|
||||||
|
if (!isCompleted && data.name) {
|
||||||
|
setEmbedFullName(data.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since a recipient can be provided a name we can lock it without requiring
|
||||||
|
// a to be provided by the parent application, unlike direct templates.
|
||||||
|
setIsNameLocked(!!data.lockName);
|
||||||
|
setAllowDocumentRejection(!!data.allowDocumentRejection);
|
||||||
|
setShowOtherRecipientsCompletedFields(!!data.showOtherRecipientsCompletedFields);
|
||||||
|
|
||||||
|
if (data.darkModeDisabled) {
|
||||||
|
document.documentElement.classList.add('dark-mode-disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowWhitelabelling) {
|
||||||
|
injectCss({
|
||||||
|
css: data.css,
|
||||||
|
cssVars: data.cssVars,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
setHasFinishedInit(true);
|
||||||
|
|
||||||
|
// !: While the two setters are stable we still want to ensure we're avoiding
|
||||||
|
// !: re-renders.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// If a document is selected, show the signing view
|
||||||
|
if (selectedDocument && selectedRecipient) {
|
||||||
|
// Determine the full name to use - prioritize embed data, then user name, then recipient name
|
||||||
|
const fullNameToUse =
|
||||||
|
embedFullName ||
|
||||||
|
(user?.email === selectedRecipient.email ? user?.name : selectedRecipient.name) ||
|
||||||
|
'';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<DocumentSigningProvider
|
||||||
|
email={selectedRecipient.email}
|
||||||
|
fullName={fullNameToUse}
|
||||||
|
signature={user?.email === selectedRecipient.email ? user?.signature : undefined}
|
||||||
|
typedSignatureEnabled={selectedDocument.documentMeta?.typedSignatureEnabled}
|
||||||
|
uploadSignatureEnabled={selectedDocument.documentMeta?.uploadSignatureEnabled}
|
||||||
|
drawSignatureEnabled={selectedDocument.documentMeta?.drawSignatureEnabled}
|
||||||
|
>
|
||||||
|
<DocumentSigningAuthProvider
|
||||||
|
documentAuthOptions={selectedDocument.authOptions}
|
||||||
|
recipient={selectedRecipient}
|
||||||
|
user={user}
|
||||||
|
>
|
||||||
|
<DocumentSigningRecipientProvider recipient={selectedRecipient} targetSigner={null}>
|
||||||
|
<MultiSignDocumentSigningView
|
||||||
|
token={selectedRecipient.token}
|
||||||
|
recipientId={selectedRecipient.id}
|
||||||
|
onBack={onBackToDocumentList}
|
||||||
|
onDocumentCompleted={onDocumentCompleted}
|
||||||
|
onDocumentRejected={onDocumentRejected}
|
||||||
|
onDocumentError={onDocumentError}
|
||||||
|
onDocumentReady={onDocumentReady}
|
||||||
|
isNameLocked={isNameLocked}
|
||||||
|
/>
|
||||||
|
</DocumentSigningRecipientProvider>
|
||||||
|
</DocumentSigningAuthProvider>
|
||||||
|
</DocumentSigningProvider>
|
||||||
|
|
||||||
|
{!hidePoweredBy && (
|
||||||
|
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
|
||||||
|
<span>Powered by</span>
|
||||||
|
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, show the document list
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<MultiSignDocumentList envelopes={envelopes} onDocumentSelect={onSelectDocument} />
|
||||||
|
|
||||||
|
{!hidePoweredBy && (
|
||||||
|
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
|
||||||
|
<span>Powered by</span>
|
||||||
|
<BrandingLogo className="ml-2 inline-block h-[14px]" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
17
apps/remix/app/types/embed-multisign-document-schema.ts
Normal file
17
apps/remix/app/types/embed-multisign-document-schema.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { ZBaseEmbedDataSchema } from './embed-base-schemas';
|
||||||
|
|
||||||
|
export const ZEmbedMultiSignDocumentSchema = ZBaseEmbedDataSchema.extend({
|
||||||
|
email: z
|
||||||
|
.union([z.literal(''), z.string().email()])
|
||||||
|
.optional()
|
||||||
|
.transform((value) => value || undefined),
|
||||||
|
lockEmail: z.boolean().optional().default(false),
|
||||||
|
name: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform((value) => value || undefined),
|
||||||
|
lockName: z.boolean().optional().default(false),
|
||||||
|
allowDocumentRejection: z.boolean().optional(),
|
||||||
|
});
|
||||||
@ -33,8 +33,8 @@
|
|||||||
"@lingui/react": "^5.2.0",
|
"@lingui/react": "^5.2.0",
|
||||||
"@oslojs/crypto": "^1.0.1",
|
"@oslojs/crypto": "^1.0.1",
|
||||||
"@oslojs/encoding": "^1.1.0",
|
"@oslojs/encoding": "^1.1.0",
|
||||||
"@react-router/node": "^7.1.5",
|
"@react-router/node": "^7.6.0",
|
||||||
"@react-router/serve": "^7.1.5",
|
"@react-router/serve": "^7.6.0",
|
||||||
"@simplewebauthn/browser": "^9.0.1",
|
"@simplewebauthn/browser": "^9.0.1",
|
||||||
"@simplewebauthn/server": "^9.0.3",
|
"@simplewebauthn/server": "^9.0.3",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
@ -49,8 +49,8 @@
|
|||||||
"luxon": "^3.4.0",
|
"luxon": "^3.4.0",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"plausible-tracker": "^0.3.9",
|
"plausible-tracker": "^0.3.9",
|
||||||
"posthog-js": "^1.224.0",
|
"posthog-js": "^1.245.0",
|
||||||
"posthog-node": "^4.8.1",
|
"posthog-node": "^4.17.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-call": "^1.3.0",
|
"react-call": "^1.3.0",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
@ -59,7 +59,7 @@
|
|||||||
"react-hotkeys-hook": "^4.4.1",
|
"react-hotkeys-hook": "^4.4.1",
|
||||||
"react-icons": "^5.4.0",
|
"react-icons": "^5.4.0",
|
||||||
"react-rnd": "^10.4.1",
|
"react-rnd": "^10.4.1",
|
||||||
"react-router": "^7.1.5",
|
"react-router": "^7.6.0",
|
||||||
"recharts": "^2.7.2",
|
"recharts": "^2.7.2",
|
||||||
"remeda": "^2.17.3",
|
"remeda": "^2.17.3",
|
||||||
"remix-themes": "^2.0.4",
|
"remix-themes": "^2.0.4",
|
||||||
@ -75,9 +75,9 @@
|
|||||||
"@babel/preset-react": "^7.26.3",
|
"@babel/preset-react": "^7.26.3",
|
||||||
"@babel/preset-typescript": "^7.26.0",
|
"@babel/preset-typescript": "^7.26.0",
|
||||||
"@lingui/babel-plugin-lingui-macro": "^5.2.0",
|
"@lingui/babel-plugin-lingui-macro": "^5.2.0",
|
||||||
"@lingui/vite-plugin": "^5.2.0",
|
"@lingui/vite-plugin": "^5.3.1",
|
||||||
"@react-router/dev": "^7.1.5",
|
"@react-router/dev": "^7.6.0",
|
||||||
"@react-router/remix-routes-option-adapter": "^7.1.5",
|
"@react-router/remix-routes-option-adapter": "^7.6.0",
|
||||||
"@rollup/plugin-babel": "^6.0.4",
|
"@rollup/plugin-babel": "^6.0.4",
|
||||||
"@rollup/plugin-commonjs": "^28.0.2",
|
"@rollup/plugin-commonjs": "^28.0.2",
|
||||||
"@rollup/plugin-node-resolve": "^16.0.0",
|
"@rollup/plugin-node-resolve": "^16.0.0",
|
||||||
@ -91,14 +91,14 @@
|
|||||||
"@types/react-dom": "^18",
|
"@types/react-dom": "^18",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"esbuild": "0.24.2",
|
"esbuild": "^0.25.4",
|
||||||
"remix-flat-routes": "^0.8.4",
|
"remix-flat-routes": "^0.8.4",
|
||||||
"rollup": "^4.34.5",
|
"rollup": "^4.34.5",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
"typescript": "5.6.2",
|
"typescript": "5.6.2",
|
||||||
"vite": "^6.1.0",
|
"vite": "^6.3.5",
|
||||||
"vite-plugin-babel-macros": "^1.0.6",
|
"vite-plugin-babel-macros": "^1.0.6",
|
||||||
"vite-tsconfig-paths": "^5.1.4"
|
"vite-tsconfig-paths": "^5.1.4"
|
||||||
},
|
},
|
||||||
"version": "1.10.3"
|
"version": "1.12.0-rc.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,12 +35,27 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
ssr: {
|
ssr: {
|
||||||
noExternal: ['react-dropzone', 'plausible-tracker', 'pdfjs-dist'],
|
noExternal: ['react-dropzone', 'plausible-tracker', 'pdfjs-dist'],
|
||||||
external: ['@node-rs/bcrypt', '@prisma/client', '@documenso/tailwind-config'],
|
external: [
|
||||||
|
'@node-rs/bcrypt',
|
||||||
|
'@prisma/client',
|
||||||
|
'@documenso/tailwind-config',
|
||||||
|
'playwright',
|
||||||
|
'playwright-core',
|
||||||
|
'@playwright/browser-chromium',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
entries: ['./app/**/*', '../../packages/ui/**/*', '../../packages/lib/**/*'],
|
entries: ['./app/**/*', '../../packages/ui/**/*', '../../packages/lib/**/*'],
|
||||||
include: ['prop-types', 'file-selector', 'attr-accept'],
|
include: ['prop-types', 'file-selector', 'attr-accept'],
|
||||||
exclude: ['node_modules', '@node-rs/bcrypt', '@documenso/pdf-sign', 'sharp'],
|
exclude: [
|
||||||
|
'node_modules',
|
||||||
|
'@node-rs/bcrypt',
|
||||||
|
'@documenso/pdf-sign',
|
||||||
|
'sharp',
|
||||||
|
'playwright',
|
||||||
|
'playwright-core',
|
||||||
|
'@playwright/browser-chromium',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
@ -68,7 +83,8 @@ export default defineConfig({
|
|||||||
'@documenso/pdf-sign',
|
'@documenso/pdf-sign',
|
||||||
'@aws-sdk/cloudfront-signer',
|
'@aws-sdk/cloudfront-signer',
|
||||||
'nodemailer',
|
'nodemailer',
|
||||||
'playwright',
|
/playwright/,
|
||||||
|
'@playwright/browser-chromium',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -114,4 +114,4 @@ COPY --chown=nodejs:nodejs ./docker/start.sh /app/apps/remix/start.sh
|
|||||||
|
|
||||||
WORKDIR /app/apps/remix
|
WORKDIR /app/apps/remix
|
||||||
|
|
||||||
CMD ["sh", "start.sh"]
|
CMD ["sh", "start.sh"]
|
||||||
@ -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:
|
|
||||||
|
|||||||
20594
package-lock.json
generated
20594
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.10.3",
|
"version": "1.12.0-rc.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
"dev": "turbo run dev --filter=@documenso/remix",
|
"dev": "turbo run dev --filter=@documenso/remix",
|
||||||
@ -44,18 +44,22 @@
|
|||||||
"@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.5.0",
|
||||||
"dotenv": "^16.3.1",
|
"dotenv-cli": "^8.0.0",
|
||||||
"dotenv-cli": "^7.3.0",
|
|
||||||
"eslint": "^8.40.0",
|
"eslint": "^8.40.0",
|
||||||
"eslint-config-custom": "*",
|
"eslint-config-custom": "*",
|
||||||
"husky": "^9.0.11",
|
"husky": "^9.0.11",
|
||||||
"lint-staged": "^15.2.2",
|
"lint-staged": "^15.2.2",
|
||||||
"playwright": "1.43.0",
|
"playwright": "1.52.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"turbo": "^1.9.3",
|
"turbo": "^1.9.3",
|
||||||
"vite": "^6.1.0"
|
"vite": "^6.3.5",
|
||||||
|
"@prisma/client": "^6.8.2",
|
||||||
|
"prisma": "^6.8.2",
|
||||||
|
"prisma-extension-kysely": "^3.0.0",
|
||||||
|
"prisma-kysely": "^1.8.0",
|
||||||
|
"nodemailer": "^6.10.1"
|
||||||
},
|
},
|
||||||
"name": "@documenso/root",
|
"name": "@documenso/root",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
|||||||
@ -20,11 +20,11 @@
|
|||||||
"@ts-rest/core": "^3.30.5",
|
"@ts-rest/core": "^3.30.5",
|
||||||
"@ts-rest/open-api": "^3.33.0",
|
"@ts-rest/open-api": "^3.33.0",
|
||||||
"@ts-rest/serverless": "^3.30.5",
|
"@ts-rest/serverless": "^3.30.5",
|
||||||
"@types/swagger-ui-react": "^4.18.3",
|
"@types/swagger-ui-react": "^5.18.0",
|
||||||
"luxon": "^3.4.0",
|
"luxon": "^3.4.0",
|
||||||
"superjson": "^1.13.1",
|
"superjson": "^1.13.1",
|
||||||
"swagger-ui-react": "^5.11.0",
|
"swagger-ui-react": "^5.21.0",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"zod": "3.24.1"
|
"zod": "3.24.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -11,6 +11,7 @@ import {
|
|||||||
ZDeleteDocumentMutationSchema,
|
ZDeleteDocumentMutationSchema,
|
||||||
ZDeleteFieldMutationSchema,
|
ZDeleteFieldMutationSchema,
|
||||||
ZDeleteRecipientMutationSchema,
|
ZDeleteRecipientMutationSchema,
|
||||||
|
ZDownloadDocumentQuerySchema,
|
||||||
ZDownloadDocumentSuccessfulSchema,
|
ZDownloadDocumentSuccessfulSchema,
|
||||||
ZFindTeamMembersResponseSchema,
|
ZFindTeamMembersResponseSchema,
|
||||||
ZGenerateDocumentFromTemplateMutationResponseSchema,
|
ZGenerateDocumentFromTemplateMutationResponseSchema,
|
||||||
@ -71,6 +72,7 @@ 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,6 +142,7 @@ 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') {
|
||||||
@ -177,7 +178,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDocumentCompleted(document.status)) {
|
if (!downloadOriginalDocument && !isDocumentCompleted(document.status)) {
|
||||||
return {
|
return {
|
||||||
status: 400,
|
status: 400,
|
||||||
body: {
|
body: {
|
||||||
@ -186,7 +187,9 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { url } = await getPresignGetUrl(document.documentData.data);
|
const { url } = await getPresignGetUrl(
|
||||||
|
downloadOriginalDocument ? document.documentData.initialData : document.documentData.data,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: 200,
|
status: 200,
|
||||||
@ -818,7 +821,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
|||||||
name,
|
name,
|
||||||
role,
|
role,
|
||||||
signingOrder,
|
signingOrder,
|
||||||
actionAuth: authOptions?.actionAuth ?? null,
|
actionAuth: authOptions?.actionAuth ?? [],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
requestMetadata: metadata,
|
requestMetadata: metadata,
|
||||||
@ -885,7 +888,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
|||||||
name,
|
name,
|
||||||
role,
|
role,
|
||||||
signingOrder,
|
signingOrder,
|
||||||
actionAuth: authOptions?.actionAuth,
|
actionAuth: authOptions?.actionAuth ?? [],
|
||||||
requestMetadata: metadata.requestMetadata,
|
requestMetadata: metadata.requestMetadata,
|
||||||
}).catch(() => null);
|
}).catch(() => null);
|
||||||
|
|
||||||
|
|||||||
@ -119,6 +119,15 @@ 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(),
|
||||||
});
|
});
|
||||||
@ -168,8 +177,8 @@ export const ZCreateDocumentMutationSchema = z.object({
|
|||||||
.default({}),
|
.default({}),
|
||||||
authOptions: z
|
authOptions: z
|
||||||
.object({
|
.object({
|
||||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
|
||||||
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.openapi({
|
.openapi({
|
||||||
@ -228,8 +237,8 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
authOptions: z
|
authOptions: z
|
||||||
.object({
|
.object({
|
||||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
|
||||||
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
|
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
|
||||||
@ -301,8 +310,8 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
authOptions: z
|
authOptions: z
|
||||||
.object({
|
.object({
|
||||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional().default([]),
|
||||||
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional().default([]),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
|
formValues: z.record(z.string(), z.union([z.string(), z.boolean(), z.number()])).optional(),
|
||||||
@ -341,7 +350,7 @@ export const ZCreateRecipientMutationSchema = z.object({
|
|||||||
signingOrder: z.number().nullish(),
|
signingOrder: z.number().nullish(),
|
||||||
authOptions: z
|
authOptions: z
|
||||||
.object({
|
.object({
|
||||||
actionAuth: ZRecipientActionAuthTypesSchema.optional(),
|
actionAuth: z.array(ZRecipientActionAuthTypesSchema).optional().default([]),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.openapi({
|
.openapi({
|
||||||
|
|||||||
@ -42,8 +42,8 @@ test('[DOCUMENT_AUTH]: should allow or deny access when required', async ({ page
|
|||||||
{
|
{
|
||||||
createDocumentOptions: {
|
createDocumentOptions: {
|
||||||
authOptions: createDocumentAuthOptions({
|
authOptions: createDocumentAuthOptions({
|
||||||
globalAccessAuth: 'ACCOUNT',
|
globalAccessAuth: ['ACCOUNT'],
|
||||||
globalActionAuth: null,
|
globalActionAuth: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -65,8 +65,8 @@ test('[DOCUMENT_AUTH]: should allow signing with valid global auth', async ({ pa
|
|||||||
recipients: [recipientWithAccount],
|
recipients: [recipientWithAccount],
|
||||||
updateDocumentOptions: {
|
updateDocumentOptions: {
|
||||||
authOptions: createDocumentAuthOptions({
|
authOptions: createDocumentAuthOptions({
|
||||||
globalAccessAuth: null,
|
globalAccessAuth: [],
|
||||||
globalActionAuth: 'ACCOUNT',
|
globalActionAuth: ['ACCOUNT'],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -116,8 +116,8 @@ test.skip('[DOCUMENT_AUTH]: should deny signing document when required for globa
|
|||||||
recipients: [recipientWithAccount],
|
recipients: [recipientWithAccount],
|
||||||
updateDocumentOptions: {
|
updateDocumentOptions: {
|
||||||
authOptions: createDocumentAuthOptions({
|
authOptions: createDocumentAuthOptions({
|
||||||
globalAccessAuth: null,
|
globalAccessAuth: [],
|
||||||
globalActionAuth: 'ACCOUNT',
|
globalActionAuth: ['ACCOUNT'],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -147,8 +147,8 @@ test('[DOCUMENT_AUTH]: should deny signing fields when required for global auth'
|
|||||||
recipients: [recipientWithAccount, seedTestEmail()],
|
recipients: [recipientWithAccount, seedTestEmail()],
|
||||||
updateDocumentOptions: {
|
updateDocumentOptions: {
|
||||||
authOptions: createDocumentAuthOptions({
|
authOptions: createDocumentAuthOptions({
|
||||||
globalAccessAuth: null,
|
globalAccessAuth: [],
|
||||||
globalActionAuth: 'ACCOUNT',
|
globalActionAuth: ['ACCOUNT'],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -193,20 +193,20 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
|
|||||||
recipientsCreateOptions: [
|
recipientsCreateOptions: [
|
||||||
{
|
{
|
||||||
authOptions: createRecipientAuthOptions({
|
authOptions: createRecipientAuthOptions({
|
||||||
accessAuth: null,
|
accessAuth: [],
|
||||||
actionAuth: null,
|
actionAuth: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
authOptions: createRecipientAuthOptions({
|
authOptions: createRecipientAuthOptions({
|
||||||
accessAuth: null,
|
accessAuth: [],
|
||||||
actionAuth: 'EXPLICIT_NONE',
|
actionAuth: ['EXPLICIT_NONE'],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
authOptions: createRecipientAuthOptions({
|
authOptions: createRecipientAuthOptions({
|
||||||
accessAuth: null,
|
accessAuth: [],
|
||||||
actionAuth: 'ACCOUNT',
|
actionAuth: ['ACCOUNT'],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -218,7 +218,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
|
|||||||
const { actionAuth } = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
const { actionAuth } = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||||
|
|
||||||
// This document has no global action auth, so only account should require auth.
|
// This document has no global action auth, so only account should require auth.
|
||||||
const isAuthRequired = actionAuth === 'ACCOUNT';
|
const isAuthRequired = actionAuth.includes('ACCOUNT');
|
||||||
|
|
||||||
const signUrl = `/sign/${token}`;
|
const signUrl = `/sign/${token}`;
|
||||||
|
|
||||||
@ -292,28 +292,28 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient an
|
|||||||
recipientsCreateOptions: [
|
recipientsCreateOptions: [
|
||||||
{
|
{
|
||||||
authOptions: createRecipientAuthOptions({
|
authOptions: createRecipientAuthOptions({
|
||||||
accessAuth: null,
|
accessAuth: [],
|
||||||
actionAuth: null,
|
actionAuth: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
authOptions: createRecipientAuthOptions({
|
authOptions: createRecipientAuthOptions({
|
||||||
accessAuth: null,
|
accessAuth: [],
|
||||||
actionAuth: 'EXPLICIT_NONE',
|
actionAuth: ['EXPLICIT_NONE'],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
authOptions: createRecipientAuthOptions({
|
authOptions: createRecipientAuthOptions({
|
||||||
accessAuth: null,
|
accessAuth: [],
|
||||||
actionAuth: 'ACCOUNT',
|
actionAuth: ['ACCOUNT'],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
fields: [FieldType.DATE, FieldType.SIGNATURE],
|
fields: [FieldType.DATE, FieldType.SIGNATURE],
|
||||||
updateDocumentOptions: {
|
updateDocumentOptions: {
|
||||||
authOptions: createDocumentAuthOptions({
|
authOptions: createDocumentAuthOptions({
|
||||||
globalAccessAuth: null,
|
globalAccessAuth: [],
|
||||||
globalActionAuth: 'ACCOUNT',
|
globalActionAuth: ['ACCOUNT'],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -323,7 +323,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient an
|
|||||||
const { actionAuth } = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
const { actionAuth } = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||||
|
|
||||||
// This document HAS global action auth, so account and inherit should require auth.
|
// This document HAS global action auth, so account and inherit should require auth.
|
||||||
const isAuthRequired = actionAuth === 'ACCOUNT' || actionAuth === null;
|
const isAuthRequired = actionAuth.includes('ACCOUNT') || actionAuth.length === 0;
|
||||||
|
|
||||||
const signUrl = `/sign/${token}`;
|
const signUrl = `/sign/${token}`;
|
||||||
|
|
||||||
|
|||||||
@ -40,7 +40,7 @@ test.describe('[EE_ONLY]', () => {
|
|||||||
|
|
||||||
// Set EE action auth.
|
// Set EE action auth.
|
||||||
await page.getByTestId('documentActionSelectValue').click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
await page.getByRole('option').filter({ hasText: 'Require passkey' }).click();
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
||||||
|
|
||||||
// Save the settings by going to the next step.
|
// Save the settings by going to the next step.
|
||||||
@ -82,7 +82,7 @@ test.describe('[EE_ONLY]', () => {
|
|||||||
|
|
||||||
// Set EE action auth.
|
// Set EE action auth.
|
||||||
await page.getByTestId('documentActionSelectValue').click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
await page.getByRole('option').filter({ hasText: 'Require passkey' }).click();
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
||||||
|
|
||||||
// Save the settings by going to the next step.
|
// Save the settings by going to the next step.
|
||||||
@ -143,7 +143,7 @@ test('[DOCUMENT_FLOW]: add settings', async ({ page }) => {
|
|||||||
|
|
||||||
// Set access auth.
|
// Set access auth.
|
||||||
await page.getByTestId('documentAccessSelectValue').click();
|
await page.getByTestId('documentAccessSelectValue').click();
|
||||||
await page.getByLabel('Require account').getByText('Require account').click();
|
await page.getByRole('option').filter({ hasText: 'Require account' }).click();
|
||||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
||||||
|
|
||||||
// Action auth should NOT be visible.
|
// Action auth should NOT be visible.
|
||||||
|
|||||||
@ -256,10 +256,16 @@ test('[DOCUMENTS]: deleting documents as a recipient should only hide it for the
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Open document action menu.
|
// Open document action menu.
|
||||||
await page
|
await expect(async () => {
|
||||||
.locator('tr', { hasText: 'Document 1 - Completed' })
|
await page
|
||||||
.getByTestId('document-table-action-btn')
|
.locator('tr', { hasText: 'Document 1 - Completed' })
|
||||||
.click();
|
.getByTestId('document-table-action-btn')
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Hide' })).toBeVisible();
|
||||||
|
}).toPass();
|
||||||
|
|
||||||
// Delete document.
|
// Delete document.
|
||||||
await page.getByRole('menuitem', { name: 'Hide' }).click();
|
await page.getByRole('menuitem', { name: 'Hide' }).click();
|
||||||
@ -267,11 +273,16 @@ test('[DOCUMENTS]: deleting documents as a recipient should only hide it for the
|
|||||||
|
|
||||||
await page.waitForTimeout(1000);
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
// Open document action menu.
|
await expect(async () => {
|
||||||
await page
|
await page
|
||||||
.locator('tr', { hasText: 'Document 1 - Pending' })
|
.locator('tr', { hasText: 'Document 1 - Pending' })
|
||||||
.getByTestId('document-table-action-btn')
|
.getByTestId('document-table-action-btn')
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Hide' })).toBeVisible();
|
||||||
|
}).toPass();
|
||||||
|
|
||||||
// Delete document.
|
// Delete document.
|
||||||
await page.getByRole('menuitem', { name: 'Hide' }).click();
|
await page.getByRole('menuitem', { name: 'Hide' }).click();
|
||||||
|
|||||||
@ -342,7 +342,13 @@ test('user can move a document to a document folder', async ({ page }) => {
|
|||||||
redirectPath: '/documents',
|
redirectPath: '/documents',
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('document-table-action-btn').click();
|
await expect(async () => {
|
||||||
|
await page.getByTestId('document-table-action-btn').first().click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
|
}).toPass();
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Proposals' }).click();
|
await page.getByRole('button', { name: 'Proposals' }).click();
|
||||||
@ -379,7 +385,13 @@ test('user can move a document from folder to the root', async ({ page }) => {
|
|||||||
|
|
||||||
await page.getByText('Proposals').click();
|
await page.getByText('Proposals').click();
|
||||||
|
|
||||||
await page.getByTestId('document-table-action-btn').click();
|
await expect(async () => {
|
||||||
|
await page.getByTestId('document-table-action-btn').first().click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
|
}).toPass();
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Root' }).click();
|
await page.getByRole('button', { name: 'Root' }).click();
|
||||||
@ -791,7 +803,13 @@ test('user can move a template to a template folder', async ({ page }) => {
|
|||||||
redirectPath: '/templates',
|
redirectPath: '/templates',
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByTestId('template-table-action-btn').click();
|
await expect(async () => {
|
||||||
|
await page.getByTestId('template-table-action-btn').first().click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
|
}).toPass();
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Client Templates' }).click();
|
await page.getByRole('button', { name: 'Client Templates' }).click();
|
||||||
@ -828,7 +846,13 @@ test('user can move a template from a folder to the root', async ({ page }) => {
|
|||||||
|
|
||||||
await page.getByText('Client Templates').click();
|
await page.getByText('Client Templates').click();
|
||||||
|
|
||||||
await page.getByTestId('template-table-action-btn').click();
|
await expect(async () => {
|
||||||
|
await page.getByTestId('template-table-action-btn').first().click();
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Move to Folder' })).toBeVisible();
|
||||||
|
}).toPass();
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
await page.getByRole('menuitem', { name: 'Move to Folder' }).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Root' }).click();
|
await page.getByRole('button', { name: 'Root' }).click();
|
||||||
|
|||||||
@ -230,13 +230,21 @@ test('[TEAMS]: resend pending team document', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/documents?status=PENDING`,
|
redirectPath: `/t/${team.url}/documents?status=PENDING`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByRole('row').getByRole('button').nth(1).click();
|
await expect(async () => {
|
||||||
await page.getByRole('menuitem', { name: 'Resend' }).click();
|
await page.getByTestId('document-table-action-btn').first().click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Resend' })).toBeVisible();
|
||||||
|
}).toPass();
|
||||||
|
|
||||||
|
await page.getByRole('menuitem').filter({ hasText: 'Resend' }).click();
|
||||||
await page.getByLabel('test.documenso.com').first().click();
|
await page.getByLabel('test.documenso.com').first().click();
|
||||||
await page.getByRole('button', { name: 'Send reminder' }).click();
|
await page.getByRole('button', { name: 'Send reminder' }).click();
|
||||||
|
|
||||||
await expect(page.getByRole('status')).toContainText('Document re-sent');
|
await expect(
|
||||||
|
page.getByRole('status').filter({ hasText: 'Document re-sent' }).first(),
|
||||||
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('[TEAMS]: delete draft team document', async ({ page }) => {
|
test('[TEAMS]: delete draft team document', async ({ page }) => {
|
||||||
@ -248,7 +256,13 @@ test('[TEAMS]: delete draft team document', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/documents?status=DRAFT`,
|
redirectPath: `/t/${team.url}/documents?status=DRAFT`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByRole('row').getByRole('button').nth(1).click();
|
await expect(async () => {
|
||||||
|
await page.getByTestId('document-table-action-btn').first().click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
|
}).toPass();
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await page.getByRole('button', { name: 'Delete' }).click();
|
||||||
@ -286,7 +300,13 @@ test('[TEAMS]: delete pending team document', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/documents?status=PENDING`,
|
redirectPath: `/t/${team.url}/documents?status=PENDING`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByRole('row').getByRole('button').nth(1).click();
|
await expect(async () => {
|
||||||
|
await page.getByTestId('document-table-action-btn').first().click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
|
}).toPass();
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
||||||
@ -325,7 +345,13 @@ test('[TEAMS]: delete completed team document', async ({ page }) => {
|
|||||||
redirectPath: `/t/${team.url}/documents?status=COMPLETED`,
|
redirectPath: `/t/${team.url}/documents?status=COMPLETED`,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page.getByRole('row').getByRole('button').nth(2).click();
|
await expect(async () => {
|
||||||
|
await page.getByTestId('document-table-action-btn').first().click();
|
||||||
|
|
||||||
|
await page.waitForTimeout(1000);
|
||||||
|
|
||||||
|
await expect(page.getByRole('menuitem', { name: 'Delete' })).toBeVisible();
|
||||||
|
}).toPass();
|
||||||
|
|
||||||
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
await page.getByRole('menuitem', { name: 'Delete' }).click();
|
||||||
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
await page.getByPlaceholder("Type 'delete' to confirm").fill('delete');
|
||||||
|
|||||||
@ -37,7 +37,7 @@ test.describe('[EE_ONLY]', () => {
|
|||||||
|
|
||||||
// Set EE action auth.
|
// Set EE action auth.
|
||||||
await page.getByTestId('documentActionSelectValue').click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
await page.getByRole('option').filter({ hasText: 'Require passkey' }).click();
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
||||||
|
|
||||||
// Save the settings by going to the next step.
|
// Save the settings by going to the next step.
|
||||||
@ -79,7 +79,7 @@ test.describe('[EE_ONLY]', () => {
|
|||||||
|
|
||||||
// Set EE action auth.
|
// Set EE action auth.
|
||||||
await page.getByTestId('documentActionSelectValue').click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
await page.getByRole('option').filter({ hasText: 'Require passkey' }).click();
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
||||||
|
|
||||||
// Save the settings by going to the next step.
|
// Save the settings by going to the next step.
|
||||||
@ -140,7 +140,7 @@ test('[TEMPLATE_FLOW]: add settings', async ({ page }) => {
|
|||||||
|
|
||||||
// Set access auth.
|
// Set access auth.
|
||||||
await page.getByTestId('documentAccessSelectValue').click();
|
await page.getByTestId('documentAccessSelectValue').click();
|
||||||
await page.getByLabel('Require account').getByText('Require account').click();
|
await page.getByRole('option').filter({ hasText: 'Require account' }).click();
|
||||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
||||||
|
|
||||||
// Action auth should NOT be visible.
|
// Action auth should NOT be visible.
|
||||||
|
|||||||
@ -58,8 +58,8 @@ test.describe('[EE_ONLY]', () => {
|
|||||||
|
|
||||||
// Add advanced settings for a single recipient.
|
// Add advanced settings for a single recipient.
|
||||||
await page.getByLabel('Show advanced settings').check();
|
await page.getByLabel('Show advanced settings').check();
|
||||||
await page.getByRole('combobox').first().click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').click();
|
await page.getByRole('option').filter({ hasText: 'Require passkey' }).click();
|
||||||
|
|
||||||
// Navigate to the next step and back.
|
// Navigate to the next step and back.
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
|
|||||||
@ -48,13 +48,13 @@ test('[TEMPLATE]: should create a document from a template', async ({ page }) =>
|
|||||||
|
|
||||||
// Set template document access.
|
// Set template document access.
|
||||||
await page.getByTestId('documentAccessSelectValue').click();
|
await page.getByTestId('documentAccessSelectValue').click();
|
||||||
await page.getByLabel('Require account').getByText('Require account').click();
|
await page.getByRole('option').filter({ hasText: 'Require account' }).click();
|
||||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
||||||
|
|
||||||
// Set EE action auth.
|
// Set EE action auth.
|
||||||
if (isBillingEnabled) {
|
if (isBillingEnabled) {
|
||||||
await page.getByTestId('documentActionSelectValue').click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
await page.getByRole('option').filter({ hasText: 'Require passkey' }).click();
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,8 +85,8 @@ test('[TEMPLATE]: should create a document from a template', async ({ page }) =>
|
|||||||
// Apply require passkey for Recipient 1.
|
// Apply require passkey for Recipient 1.
|
||||||
if (isBillingEnabled) {
|
if (isBillingEnabled) {
|
||||||
await page.getByLabel('Show advanced settings').check();
|
await page.getByLabel('Show advanced settings').check();
|
||||||
await page.getByRole('combobox').first().click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').click();
|
await page.getByRole('option').filter({ hasText: 'Require passkey' }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
@ -119,10 +119,12 @@ test('[TEMPLATE]: should create a document from a template', async ({ page }) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(document.title).toEqual('TEMPLATE_TITLE');
|
expect(document.title).toEqual('TEMPLATE_TITLE');
|
||||||
expect(documentAuth.documentAuthOption.globalAccessAuth).toEqual('ACCOUNT');
|
expect(documentAuth.documentAuthOption.globalAccessAuth).toContain('ACCOUNT');
|
||||||
expect(documentAuth.documentAuthOption.globalActionAuth).toEqual(
|
|
||||||
isBillingEnabled ? 'PASSKEY' : null,
|
if (isBillingEnabled) {
|
||||||
);
|
expect(documentAuth.documentAuthOption.globalActionAuth).toContain('PASSKEY');
|
||||||
|
}
|
||||||
|
|
||||||
expect(document.documentMeta?.dateFormat).toEqual('dd/MM/yyyy hh:mm a');
|
expect(document.documentMeta?.dateFormat).toEqual('dd/MM/yyyy hh:mm a');
|
||||||
expect(document.documentMeta?.message).toEqual('MESSAGE');
|
expect(document.documentMeta?.message).toEqual('MESSAGE');
|
||||||
expect(document.documentMeta?.redirectUrl).toEqual('https://documenso.com');
|
expect(document.documentMeta?.redirectUrl).toEqual('https://documenso.com');
|
||||||
@ -143,11 +145,11 @@ test('[TEMPLATE]: should create a document from a template', async ({ page }) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isBillingEnabled) {
|
if (isBillingEnabled) {
|
||||||
expect(recipientOneAuth.derivedRecipientActionAuth).toEqual('PASSKEY');
|
expect(recipientOneAuth.derivedRecipientActionAuth).toContain('PASSKEY');
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(recipientOneAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
expect(recipientOneAuth.derivedRecipientAccessAuth).toContain('ACCOUNT');
|
||||||
expect(recipientTwoAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
expect(recipientTwoAuth.derivedRecipientAccessAuth).toContain('ACCOUNT');
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -183,13 +185,13 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
|
|||||||
|
|
||||||
// Set template document access.
|
// Set template document access.
|
||||||
await page.getByTestId('documentAccessSelectValue').click();
|
await page.getByTestId('documentAccessSelectValue').click();
|
||||||
await page.getByLabel('Require account').getByText('Require account').click();
|
await page.getByRole('option').filter({ hasText: 'Require account' }).click();
|
||||||
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
await expect(page.getByTestId('documentAccessSelectValue')).toContainText('Require account');
|
||||||
|
|
||||||
// Set EE action auth.
|
// Set EE action auth.
|
||||||
if (isBillingEnabled) {
|
if (isBillingEnabled) {
|
||||||
await page.getByTestId('documentActionSelectValue').click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').getByText('Require passkey').click();
|
await page.getByRole('option').filter({ hasText: 'Require passkey' }).click();
|
||||||
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
await expect(page.getByTestId('documentActionSelectValue')).toContainText('Require passkey');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,8 +222,8 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
|
|||||||
// Apply require passkey for Recipient 1.
|
// Apply require passkey for Recipient 1.
|
||||||
if (isBillingEnabled) {
|
if (isBillingEnabled) {
|
||||||
await page.getByLabel('Show advanced settings').check();
|
await page.getByLabel('Show advanced settings').check();
|
||||||
await page.getByRole('combobox').first().click();
|
await page.getByTestId('documentActionSelectValue').click();
|
||||||
await page.getByLabel('Require passkey').click();
|
await page.getByRole('option').filter({ hasText: 'Require passkey' }).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Continue' }).click();
|
await page.getByRole('button', { name: 'Continue' }).click();
|
||||||
@ -256,10 +258,12 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(document.title).toEqual('TEMPLATE_TITLE');
|
expect(document.title).toEqual('TEMPLATE_TITLE');
|
||||||
expect(documentAuth.documentAuthOption.globalAccessAuth).toEqual('ACCOUNT');
|
expect(documentAuth.documentAuthOption.globalAccessAuth).toContain('ACCOUNT');
|
||||||
expect(documentAuth.documentAuthOption.globalActionAuth).toEqual(
|
|
||||||
isBillingEnabled ? 'PASSKEY' : null,
|
if (isBillingEnabled) {
|
||||||
);
|
expect(documentAuth.documentAuthOption.globalActionAuth).toContain('PASSKEY');
|
||||||
|
}
|
||||||
|
|
||||||
expect(document.documentMeta?.dateFormat).toEqual('dd/MM/yyyy hh:mm a');
|
expect(document.documentMeta?.dateFormat).toEqual('dd/MM/yyyy hh:mm a');
|
||||||
expect(document.documentMeta?.message).toEqual('MESSAGE');
|
expect(document.documentMeta?.message).toEqual('MESSAGE');
|
||||||
expect(document.documentMeta?.redirectUrl).toEqual('https://documenso.com');
|
expect(document.documentMeta?.redirectUrl).toEqual('https://documenso.com');
|
||||||
@ -280,11 +284,11 @@ test('[TEMPLATE]: should create a team document from a team template', async ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (isBillingEnabled) {
|
if (isBillingEnabled) {
|
||||||
expect(recipientOneAuth.derivedRecipientActionAuth).toEqual('PASSKEY');
|
expect(recipientOneAuth.derivedRecipientActionAuth).toContain('PASSKEY');
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(recipientOneAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
expect(recipientOneAuth.derivedRecipientAccessAuth).toContain('ACCOUNT');
|
||||||
expect(recipientTwoAuth.derivedRecipientAccessAuth).toEqual('ACCOUNT');
|
expect(recipientTwoAuth.derivedRecipientAccessAuth).toContain('ACCOUNT');
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -172,8 +172,8 @@ test('[DIRECT_TEMPLATES]: direct template link auth access', async ({ page }) =>
|
|||||||
userId: user.id,
|
userId: user.id,
|
||||||
createTemplateOptions: {
|
createTemplateOptions: {
|
||||||
authOptions: createDocumentAuthOptions({
|
authOptions: createDocumentAuthOptions({
|
||||||
globalAccessAuth: 'ACCOUNT',
|
globalAccessAuth: ['ACCOUNT'],
|
||||||
globalActionAuth: null,
|
globalActionAuth: [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { type Page, expect, test } from '@playwright/test';
|
import { type Page, expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
import { alphaid } from '@documenso/lib/universal/id';
|
||||||
import {
|
import {
|
||||||
extractUserVerificationToken,
|
extractUserVerificationToken,
|
||||||
seedTestEmail,
|
seedTestEmail,
|
||||||
@ -23,9 +24,11 @@ test('[USER] can sign up with email and password', async ({ page }: { page: Page
|
|||||||
await signSignaturePad(page);
|
await signSignaturePad(page);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Next', exact: true }).click();
|
await page.getByRole('button', { name: 'Next', exact: true }).click();
|
||||||
await page.getByLabel('Public profile username').fill(Date.now().toString());
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Complete', exact: true }).click();
|
await page.getByLabel('Public profile username').fill(alphaid(10));
|
||||||
|
await page.getByLabel('Public profile username').blur();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Complete' }).click();
|
||||||
|
|
||||||
await page.waitForURL('/unverified-account');
|
await page.waitForURL('/unverified-account');
|
||||||
|
|
||||||
|
|||||||
@ -12,13 +12,13 @@
|
|||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.18.1",
|
"@playwright/test": "1.52.0",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@documenso/lib": "*",
|
"@documenso/lib": "*",
|
||||||
"@documenso/prisma": "*",
|
"@documenso/prisma": "*",
|
||||||
"pdf-lib": "^1.17.1"
|
"pdf-lib": "^1.17.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"start-server-and-test": "^2.0.1"
|
"start-server-and-test": "^2.0.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -17,7 +17,7 @@ export default defineConfig({
|
|||||||
testDir: './e2e',
|
testDir: './e2e',
|
||||||
/* Run tests in files in parallel */
|
/* Run tests in files in parallel */
|
||||||
fullyParallel: false,
|
fullyParallel: false,
|
||||||
workers: '50%',
|
workers: 1,
|
||||||
maxFailures: process.env.CI ? 1 : undefined,
|
maxFailures: process.env.CI ? 1 : undefined,
|
||||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
forbidOnly: !!process.env.CI,
|
forbidOnly: !!process.env.CI,
|
||||||
|
|||||||
@ -18,8 +18,8 @@
|
|||||||
"arctic": "^3.1.0",
|
"arctic": "^3.1.0",
|
||||||
"hono": "4.7.0",
|
"hono": "4.7.0",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^5.1.5",
|
||||||
"ts-pattern": "^5.0.5",
|
"ts-pattern": "^5.0.5",
|
||||||
"zod": "3.24.1"
|
"zod": "3.24.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -36,7 +36,7 @@
|
|||||||
"@react-email/section": "0.0.10",
|
"@react-email/section": "0.0.10",
|
||||||
"@react-email/tailwind": "0.0.9",
|
"@react-email/tailwind": "0.0.9",
|
||||||
"@react-email/text": "0.0.6",
|
"@react-email/text": "0.0.6",
|
||||||
"nodemailer": "6.9.9",
|
"nodemailer": "^6.10.1",
|
||||||
"react-email": "1.9.5",
|
"react-email": "1.9.5",
|
||||||
"resend": "2.0.0"
|
"resend": "2.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,13 +1,7 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
extends: [
|
extends: ['next', 'turbo', 'eslint:recommended', 'plugin:@typescript-eslint/recommended'],
|
||||||
'next',
|
|
||||||
'turbo',
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:package-json/recommended',
|
|
||||||
],
|
|
||||||
|
|
||||||
plugins: ['package-json', 'unused-imports'],
|
plugins: ['unused-imports'],
|
||||||
|
|
||||||
env: {
|
env: {
|
||||||
es2022: true,
|
es2022: true,
|
||||||
|
|||||||
@ -10,11 +10,11 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^7.1.1",
|
"@typescript-eslint/eslint-plugin": "^7.1.1",
|
||||||
"@typescript-eslint/parser": "^7.1.1",
|
"@typescript-eslint/parser": "^7.1.1",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-next": "^14.1.3",
|
"eslint-config-next": "^14.2.28",
|
||||||
"eslint-config-turbo": "^1.12.5",
|
"eslint-config-turbo": "^1.12.5",
|
||||||
"eslint-plugin-package-json": "^0.10.4",
|
"eslint-plugin-package-json": "^0.31.0",
|
||||||
"eslint-plugin-react": "^7.34.0",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-unused-imports": "^3.1.0",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"typescript": "5.6.2"
|
"typescript": "5.6.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
75
packages/lib/client-only/hooks/use-element-bounds.ts
Normal file
75
packages/lib/client-only/hooks/use-element-bounds.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||||
|
|
||||||
|
export const useElementBounds = (elementOrSelector: HTMLElement | string, withScroll = false) => {
|
||||||
|
const [bounds, setBounds] = useState({
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
height: 0,
|
||||||
|
width: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const calculateBounds = () => {
|
||||||
|
const $el =
|
||||||
|
typeof elementOrSelector === 'string'
|
||||||
|
? document.querySelector<HTMLElement>(elementOrSelector)
|
||||||
|
: elementOrSelector;
|
||||||
|
|
||||||
|
if (!$el) {
|
||||||
|
throw new Error('Element not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (withScroll) {
|
||||||
|
return getBoundingClientRect($el);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { top, left, width, height } = $el.getBoundingClientRect();
|
||||||
|
|
||||||
|
return {
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBounds(calculateBounds());
|
||||||
|
}, [calculateBounds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onResize = () => {
|
||||||
|
setBounds(calculateBounds());
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('resize', onResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', onResize);
|
||||||
|
};
|
||||||
|
}, [calculateBounds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const $el =
|
||||||
|
typeof elementOrSelector === 'string'
|
||||||
|
? document.querySelector<HTMLElement>(elementOrSelector)
|
||||||
|
: elementOrSelector;
|
||||||
|
|
||||||
|
if (!$el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
setBounds(calculateBounds());
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe($el);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [calculateBounds]);
|
||||||
|
|
||||||
|
return bounds;
|
||||||
|
};
|
||||||
@ -19,6 +19,10 @@ export const DOCUMENT_AUTH_TYPES: Record<string, DocumentAuthTypeData> = {
|
|||||||
key: DocumentAuth.TWO_FACTOR_AUTH,
|
key: DocumentAuth.TWO_FACTOR_AUTH,
|
||||||
value: 'Require 2FA',
|
value: 'Require 2FA',
|
||||||
},
|
},
|
||||||
|
[DocumentAuth.PASSWORD]: {
|
||||||
|
key: DocumentAuth.PASSWORD,
|
||||||
|
value: 'Require password',
|
||||||
|
},
|
||||||
[DocumentAuth.EXPLICIT_NONE]: {
|
[DocumentAuth.EXPLICIT_NONE]: {
|
||||||
key: DocumentAuth.EXPLICIT_NONE,
|
key: DocumentAuth.EXPLICIT_NONE,
|
||||||
value: 'None (Overrides global settings)',
|
value: 'None (Overrides global settings)',
|
||||||
|
|||||||
@ -15,7 +15,6 @@
|
|||||||
"clean": "rimraf node_modules"
|
"clean": "rimraf node_modules"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/kysely-adapter": "^0.6.0",
|
|
||||||
"@aws-sdk/client-s3": "^3.410.0",
|
"@aws-sdk/client-s3": "^3.410.0",
|
||||||
"@aws-sdk/cloudfront-signer": "^3.410.0",
|
"@aws-sdk/cloudfront-signer": "^3.410.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.410.0",
|
"@aws-sdk/s3-request-presigner": "^3.410.0",
|
||||||
@ -41,12 +40,13 @@
|
|||||||
"kysely": "0.26.3",
|
"kysely": "0.26.3",
|
||||||
"luxon": "^3.4.0",
|
"luxon": "^3.4.0",
|
||||||
"micro": "^10.0.1",
|
"micro": "^10.0.1",
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^5.1.5",
|
||||||
"oslo": "^0.17.0",
|
"oslo": "^0.17.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"playwright": "1.43.0",
|
"playwright": "1.52.0",
|
||||||
"posthog-js": "^1.224.0",
|
"posthog-js": "^1.245.0",
|
||||||
|
"posthog-node": "^4.17.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"remeda": "^2.17.3",
|
"remeda": "^2.17.3",
|
||||||
"sharp": "0.32.6",
|
"sharp": "0.32.6",
|
||||||
@ -55,7 +55,7 @@
|
|||||||
"zod": "3.24.1"
|
"zod": "3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/browser-chromium": "1.43.0",
|
"@playwright/browser-chromium": "1.52.0",
|
||||||
"@types/luxon": "^3.3.1",
|
"@types/luxon": "^3.3.1",
|
||||||
"@types/pg": "^8.11.4"
|
"@types/pg": "^8.11.4"
|
||||||
}
|
}
|
||||||
|
|||||||
20
packages/lib/server-only/2fa/verify-password.ts
Normal file
20
packages/lib/server-only/2fa/verify-password.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { compare } from '@node-rs/bcrypt';
|
||||||
|
|
||||||
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
|
type VerifyPasswordOptions = {
|
||||||
|
userId: number;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const verifyPassword = async ({ userId, password }: VerifyPasswordOptions) => {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user || !user.password) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await compare(password, user.password);
|
||||||
|
};
|
||||||
@ -23,6 +23,7 @@ import {
|
|||||||
ZWebhookDocumentSchema,
|
ZWebhookDocumentSchema,
|
||||||
mapDocumentToWebhookDocumentPayload,
|
mapDocumentToWebhookDocumentPayload,
|
||||||
} from '../../types/webhook-payload';
|
} from '../../types/webhook-payload';
|
||||||
|
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
||||||
import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn';
|
import { getIsRecipientsTurnToSign } from '../recipient/get-is-recipient-turn';
|
||||||
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
||||||
import { sendPendingEmail } from './send-pending-email';
|
import { sendPendingEmail } from './send-pending-email';
|
||||||
@ -140,6 +141,11 @@ export const completeDocumentWithToken = async ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const authOptions = extractDocumentAuthMethods({
|
||||||
|
documentAuth: document.authOptions,
|
||||||
|
recipientAuth: recipient.authOptions,
|
||||||
|
});
|
||||||
|
|
||||||
await tx.documentAuditLog.create({
|
await tx.documentAuditLog.create({
|
||||||
data: createDocumentAuditLogData({
|
data: createDocumentAuditLogData({
|
||||||
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||||
@ -154,6 +160,7 @@ export const completeDocumentWithToken = async ({
|
|||||||
recipientName: recipient.name,
|
recipientName: recipient.name,
|
||||||
recipientId: recipient.id,
|
recipientId: recipient.id,
|
||||||
recipientRole: recipient.role,
|
recipientRole: recipient.role,
|
||||||
|
actionAuth: authOptions.derivedRecipientActionAuth,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -39,8 +39,8 @@ export type CreateDocumentOptions = {
|
|||||||
title: string;
|
title: string;
|
||||||
externalId?: string;
|
externalId?: string;
|
||||||
visibility?: DocumentVisibility;
|
visibility?: DocumentVisibility;
|
||||||
globalAccessAuth?: TDocumentAccessAuthTypes;
|
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||||
globalActionAuth?: TDocumentActionAuthTypes;
|
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||||
formValues?: TDocumentFormValues;
|
formValues?: TDocumentFormValues;
|
||||||
recipients: TCreateDocumentV2Request['recipients'];
|
recipients: TCreateDocumentV2Request['recipients'];
|
||||||
};
|
};
|
||||||
@ -113,14 +113,16 @@ export const createDocumentV2 = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const authOptions = createDocumentAuthOptions({
|
const authOptions = createDocumentAuthOptions({
|
||||||
globalAccessAuth: data?.globalAccessAuth || null,
|
globalAccessAuth: data?.globalAccessAuth || [],
|
||||||
globalActionAuth: data?.globalActionAuth || null,
|
globalActionAuth: data?.globalActionAuth || [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const recipientsHaveActionAuth = data.recipients?.some((recipient) => recipient.actionAuth);
|
const recipientsHaveActionAuth = data.recipients?.some(
|
||||||
|
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
// Check if user has permission to set the global action auth.
|
||||||
if (authOptions.globalActionAuth || recipientsHaveActionAuth) {
|
if (authOptions.globalActionAuth.length > 0 || recipientsHaveActionAuth) {
|
||||||
const isDocumentEnterprise = await isUserEnterprise({
|
const isDocumentEnterprise = await isUserEnterprise({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
@ -171,8 +173,8 @@ export const createDocumentV2 = async ({
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
(data.recipients || []).map(async (recipient) => {
|
(data.recipients || []).map(async (recipient) => {
|
||||||
const recipientAuthOptions = createRecipientAuthOptions({
|
const recipientAuthOptions = createRecipientAuthOptions({
|
||||||
accessAuth: recipient.accessAuth || null,
|
accessAuth: recipient.accessAuth ?? [],
|
||||||
actionAuth: recipient.actionAuth || null,
|
actionAuth: recipient.actionAuth ?? [],
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.recipient.create({
|
await tx.recipient.create({
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
import { DocumentSource, type Prisma } from '@prisma/client';
|
import { DocumentSource, type Prisma, WebhookTriggerEvents } 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 {
|
||||||
@ -86,7 +91,24 @@ export const duplicateDocument = async ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const createdDocument = await prisma.document.create(createDocumentArguments);
|
const createdDocument = await prisma.document.create({
|
||||||
|
...createDocumentArguments,
|
||||||
|
include: {
|
||||||
|
recipients: true,
|
||||||
|
documentMeta: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await triggerWebhook({
|
||||||
|
event: WebhookTriggerEvents.DOCUMENT_CREATED,
|
||||||
|
data: ZWebhookDocumentSchema.parse({
|
||||||
|
...mapDocumentToWebhookDocumentPayload(createdDocument),
|
||||||
|
recipients: createdDocument.recipients,
|
||||||
|
documentMeta: createdDocument.documentMeta,
|
||||||
|
}),
|
||||||
|
userId: userId,
|
||||||
|
teamId: teamId,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
documentId: createdDocument.id,
|
documentId: createdDocument.id,
|
||||||
|
|||||||
@ -17,6 +17,7 @@ export const getDocumentCertificateAuditLogs = async ({
|
|||||||
in: [
|
in: [
|
||||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
|
||||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED,
|
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_REJECTED,
|
||||||
|
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
|
||||||
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
||||||
DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT,
|
||||||
],
|
],
|
||||||
@ -36,6 +37,9 @@ export const getDocumentCertificateAuditLogs = async ({
|
|||||||
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED]: auditLogs.filter(
|
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED]: auditLogs.filter(
|
||||||
(log) => log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
(log) => log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
|
||||||
),
|
),
|
||||||
|
[DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED]: auditLogs.filter(
|
||||||
|
(log) => log.type === DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
|
||||||
|
),
|
||||||
[DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT]: auditLogs.filter(
|
[DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT]: auditLogs.filter(
|
||||||
(log) =>
|
(log) =>
|
||||||
log.type === DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT &&
|
log.type === DOCUMENT_AUDIT_LOG_TYPE.EMAIL_SENT &&
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { match } from 'ts-pattern';
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
|
|
||||||
import { verifyTwoFactorAuthenticationToken } from '../2fa/verify-2fa-token';
|
import { verifyTwoFactorAuthenticationToken } from '../2fa/verify-2fa-token';
|
||||||
|
import { verifyPassword } from '../2fa/verify-password';
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import type { TDocumentAuth, TDocumentAuthMethods } from '../../types/document-auth';
|
import type { TDocumentAuth, TDocumentAuthMethods } from '../../types/document-auth';
|
||||||
import { DocumentAuth } from '../../types/document-auth';
|
import { DocumentAuth } from '../../types/document-auth';
|
||||||
@ -60,23 +61,26 @@ export const isRecipientAuthorized = async ({
|
|||||||
recipientAuth: recipient.authOptions,
|
recipientAuth: recipient.authOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
const authMethod: TDocumentAuth | null =
|
const authMethods: TDocumentAuth[] =
|
||||||
type === 'ACCESS' ? derivedRecipientAccessAuth : derivedRecipientActionAuth;
|
type === 'ACCESS' ? derivedRecipientAccessAuth : derivedRecipientActionAuth;
|
||||||
|
|
||||||
// Early true return when auth is not required.
|
// Early true return when auth is not required.
|
||||||
if (!authMethod || authMethod === DocumentAuth.EXPLICIT_NONE) {
|
if (
|
||||||
|
authMethods.length === 0 ||
|
||||||
|
authMethods.some((method) => method === DocumentAuth.EXPLICIT_NONE)
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create auth options when none are passed for account.
|
// Create auth options when none are passed for account.
|
||||||
if (!authOptions && authMethod === DocumentAuth.ACCOUNT) {
|
if (!authOptions && authMethods.some((method) => method === DocumentAuth.ACCOUNT)) {
|
||||||
authOptions = {
|
authOptions = {
|
||||||
type: DocumentAuth.ACCOUNT,
|
type: DocumentAuth.ACCOUNT,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authentication required does not match provided method.
|
// Authentication required does not match provided method.
|
||||||
if (!authOptions || authOptions.type !== authMethod || !userId) {
|
if (!authOptions || !authMethods.includes(authOptions.type) || !userId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,6 +121,15 @@ export const isRecipientAuthorized = async ({
|
|||||||
window: 10, // 5 minutes worth of tokens
|
window: 10, // 5 minutes worth of tokens
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
.with({ type: DocumentAuth.PASSWORD }, async ({ password }) => {
|
||||||
|
return await verifyPassword({
|
||||||
|
userId,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.with({ type: DocumentAuth.EXPLICIT_NONE }, () => {
|
||||||
|
return true;
|
||||||
|
})
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -160,7 +173,7 @@ const verifyPasskey = async ({
|
|||||||
}: VerifyPasskeyOptions): Promise<void> => {
|
}: VerifyPasskeyOptions): Promise<void> => {
|
||||||
const passkey = await prisma.passkey.findFirst({
|
const passkey = await prisma.passkey.findFirst({
|
||||||
where: {
|
where: {
|
||||||
credentialId: Buffer.from(authenticationResponse.id, 'base64'),
|
credentialId: new Uint8Array(Buffer.from(authenticationResponse.id, 'base64')),
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -128,7 +128,7 @@ export const sealDocument = async ({
|
|||||||
|
|
||||||
// Normalize and flatten layers that could cause issues with the signature
|
// Normalize and flatten layers that could cause issues with the signature
|
||||||
normalizeSignatureAppearances(doc);
|
normalizeSignatureAppearances(doc);
|
||||||
flattenForm(doc);
|
await flattenForm(doc);
|
||||||
flattenAnnotations(doc);
|
flattenAnnotations(doc);
|
||||||
|
|
||||||
// Add rejection stamp if the document is rejected
|
// Add rejection stamp if the document is rejected
|
||||||
@ -153,7 +153,7 @@ export const sealDocument = async ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Re-flatten post-insertion to handle fields that create arcoFields
|
// Re-flatten post-insertion to handle fields that create arcoFields
|
||||||
flattenForm(doc);
|
await flattenForm(doc);
|
||||||
|
|
||||||
const pdfBytes = await doc.save();
|
const pdfBytes = await doc.save();
|
||||||
|
|
||||||
|
|||||||
@ -21,8 +21,8 @@ export type UpdateDocumentOptions = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
externalId?: string | null;
|
externalId?: string | null;
|
||||||
visibility?: DocumentVisibility | null;
|
visibility?: DocumentVisibility | null;
|
||||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||||
useLegacyFieldInsertion?: boolean;
|
useLegacyFieldInsertion?: boolean;
|
||||||
};
|
};
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata: ApiRequestMetadata;
|
||||||
@ -119,7 +119,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,7 +136,7 @@ export const updateDocument = async ({
|
|||||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
// Check if user has permission to set the global action auth.
|
||||||
if (newGlobalActionAuth) {
|
if (newGlobalActionAuth && newGlobalActionAuth.length > 0) {
|
||||||
const isDocumentEnterprise = await isUserEnterprise({
|
const isDocumentEnterprise = await isUserEnterprise({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { FieldType } from '@prisma/client';
|
|||||||
|
|
||||||
import { AppError, AppErrorCode } from '../../errors/app-error';
|
import { AppError, AppErrorCode } from '../../errors/app-error';
|
||||||
import type { TRecipientActionAuth } from '../../types/document-auth';
|
import type { TRecipientActionAuth } from '../../types/document-auth';
|
||||||
import { extractDocumentAuthMethods } from '../../utils/document-auth';
|
|
||||||
import { isRecipientAuthorized } from './is-recipient-authorized';
|
import { isRecipientAuthorized } from './is-recipient-authorized';
|
||||||
|
|
||||||
export type ValidateFieldAuthOptions = {
|
export type ValidateFieldAuthOptions = {
|
||||||
@ -26,14 +25,9 @@ export const validateFieldAuth = async ({
|
|||||||
userId,
|
userId,
|
||||||
authOptions,
|
authOptions,
|
||||||
}: ValidateFieldAuthOptions) => {
|
}: ValidateFieldAuthOptions) => {
|
||||||
const { derivedRecipientActionAuth } = extractDocumentAuthMethods({
|
|
||||||
documentAuth: documentAuthOptions,
|
|
||||||
recipientAuth: recipient.authOptions,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Override all non-signature fields to not require any auth.
|
// Override all non-signature fields to not require any auth.
|
||||||
if (field.type !== FieldType.SIGNATURE) {
|
if (field.type !== FieldType.SIGNATURE) {
|
||||||
return null;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValid = await isRecipientAuthorized({
|
const isValid = await isRecipientAuthorized({
|
||||||
@ -50,5 +44,5 @@ export const validateFieldAuth = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return derivedRecipientActionAuth;
|
return authOptions?.type;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
|
|||||||
|
|
||||||
export type ViewedDocumentOptions = {
|
export type ViewedDocumentOptions = {
|
||||||
token: string;
|
token: string;
|
||||||
recipientAccessAuth?: TDocumentAccessAuthTypes | null;
|
recipientAccessAuth?: TDocumentAccessAuthTypes[];
|
||||||
requestMetadata?: RequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ export const viewedDocument = async ({
|
|||||||
recipientId: recipient.id,
|
recipientId: recipient.id,
|
||||||
recipientName: recipient.name,
|
recipientName: recipient.name,
|
||||||
recipientRole: recipient.role,
|
recipientRole: recipient.role,
|
||||||
accessAuth: recipientAccessAuth || undefined,
|
accessAuth: recipientAccessAuth ?? [],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -27,7 +27,6 @@ export const createEmbeddingPresignToken = async ({
|
|||||||
// In development mode, allow setting expiresIn to 0 for testing
|
// In development mode, allow setting expiresIn to 0 for testing
|
||||||
// In production, enforce a minimum expiration time
|
// In production, enforce a minimum expiration time
|
||||||
const isDevelopment = env('NODE_ENV') !== 'production';
|
const isDevelopment = env('NODE_ENV') !== 'production';
|
||||||
console.log('isDevelopment', isDevelopment);
|
|
||||||
const minExpirationMinutes = isDevelopment ? 0 : 5;
|
const minExpirationMinutes = isDevelopment ? 0 : 5;
|
||||||
|
|
||||||
// Ensure expiresIn is at least the minimum allowed value
|
// Ensure expiresIn is at least the minimum allowed value
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import fontkit from '@pdf-lib/fontkit';
|
||||||
import type { PDFField, PDFWidgetAnnotation } from 'pdf-lib';
|
import type { PDFField, PDFWidgetAnnotation } from 'pdf-lib';
|
||||||
import {
|
import {
|
||||||
PDFCheckBox,
|
PDFCheckBox,
|
||||||
@ -13,6 +14,8 @@ import {
|
|||||||
translate,
|
translate,
|
||||||
} from 'pdf-lib';
|
} from 'pdf-lib';
|
||||||
|
|
||||||
|
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
|
||||||
|
|
||||||
export const removeOptionalContentGroups = (document: PDFDocument) => {
|
export const removeOptionalContentGroups = (document: PDFDocument) => {
|
||||||
const context = document.context;
|
const context = document.context;
|
||||||
const catalog = context.lookup(context.trailerInfo.Root);
|
const catalog = context.lookup(context.trailerInfo.Root);
|
||||||
@ -21,12 +24,20 @@ export const removeOptionalContentGroups = (document: PDFDocument) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const flattenForm = (document: PDFDocument) => {
|
export const flattenForm = async (document: PDFDocument) => {
|
||||||
removeOptionalContentGroups(document);
|
removeOptionalContentGroups(document);
|
||||||
|
|
||||||
const form = document.getForm();
|
const form = document.getForm();
|
||||||
|
|
||||||
form.updateFieldAppearances();
|
const fontNoto = await fetch(`${NEXT_PUBLIC_WEBAPP_URL()}/fonts/noto-sans.ttf`).then(
|
||||||
|
async (res) => res.arrayBuffer(),
|
||||||
|
);
|
||||||
|
|
||||||
|
document.registerFontkit(fontkit);
|
||||||
|
|
||||||
|
const font = await document.embedFont(fontNoto);
|
||||||
|
|
||||||
|
form.updateFieldAppearances(font);
|
||||||
|
|
||||||
for (const field of form.getFields()) {
|
for (const field of form.getFields()) {
|
||||||
for (const widget of field.acroField.getWidgets()) {
|
for (const widget of field.acroField.getWidgets()) {
|
||||||
|
|||||||
@ -1,8 +1,15 @@
|
|||||||
// https://github.com/Hopding/pdf-lib/issues/20#issuecomment-412852821
|
// https://github.com/Hopding/pdf-lib/issues/20#issuecomment-412852821
|
||||||
import fontkit from '@pdf-lib/fontkit';
|
import fontkit from '@pdf-lib/fontkit';
|
||||||
import { FieldType } from '@prisma/client';
|
import { FieldType } from '@prisma/client';
|
||||||
import type { PDFDocument, PDFFont } from 'pdf-lib';
|
import type { PDFDocument, PDFFont, PDFTextField } from 'pdf-lib';
|
||||||
import { RotationTypes, TextAlignment, degrees, radiansToDegrees, rgb } from 'pdf-lib';
|
import {
|
||||||
|
RotationTypes,
|
||||||
|
TextAlignment,
|
||||||
|
degrees,
|
||||||
|
radiansToDegrees,
|
||||||
|
rgb,
|
||||||
|
setFontAndSize,
|
||||||
|
} from 'pdf-lib';
|
||||||
import { P, match } from 'ts-pattern';
|
import { P, match } from 'ts-pattern';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -442,6 +449,10 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
|||||||
adjustedFieldY = adjustedPosition.yPos;
|
adjustedFieldY = adjustedPosition.yPos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set properties for the text field
|
||||||
|
setTextFieldFontSize(textField, font, fontSize);
|
||||||
|
textField.setText(textToInsert);
|
||||||
|
|
||||||
// Set the position and size of the text field
|
// Set the position and size of the text field
|
||||||
textField.addToPage(page, {
|
textField.addToPage(page, {
|
||||||
x: adjustedFieldX,
|
x: adjustedFieldX,
|
||||||
@ -450,6 +461,8 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
|||||||
height: adjustedFieldHeight,
|
height: adjustedFieldHeight,
|
||||||
rotate: degrees(pageRotationInDegrees),
|
rotate: degrees(pageRotationInDegrees),
|
||||||
|
|
||||||
|
font,
|
||||||
|
|
||||||
// Hide borders.
|
// Hide borders.
|
||||||
borderWidth: 0,
|
borderWidth: 0,
|
||||||
borderColor: undefined,
|
borderColor: undefined,
|
||||||
@ -457,10 +470,6 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
|||||||
|
|
||||||
...(isDebugMode ? { borderWidth: 1, borderColor: rgb(0, 0, 1) } : {}),
|
...(isDebugMode ? { borderWidth: 1, borderColor: rgb(0, 0, 1) } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set properties for the text field
|
|
||||||
textField.setFontSize(fontSize);
|
|
||||||
textField.setText(textToInsert);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return pdf;
|
return pdf;
|
||||||
@ -629,3 +638,21 @@ function breakLongString(text: string, maxWidth: number, font: PDFFont, fontSize
|
|||||||
|
|
||||||
return lines.join('\n');
|
return lines.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setTextFieldFontSize = (textField: PDFTextField, font: PDFFont, fontSize: number) => {
|
||||||
|
textField.defaultUpdateAppearances(font);
|
||||||
|
textField.updateAppearances(font);
|
||||||
|
|
||||||
|
try {
|
||||||
|
textField.setFontSize(fontSize);
|
||||||
|
} catch (err) {
|
||||||
|
let da = textField.acroField.getDefaultAppearance() ?? '';
|
||||||
|
|
||||||
|
da += `\n ${setFontAndSize(font.name, fontSize)}`;
|
||||||
|
|
||||||
|
textField.acroField.setDefaultAppearance(da);
|
||||||
|
}
|
||||||
|
|
||||||
|
textField.defaultUpdateAppearances(font);
|
||||||
|
textField.updateAppearances(font);
|
||||||
|
};
|
||||||
|
|||||||
@ -22,8 +22,8 @@ export interface CreateDocumentRecipientsOptions {
|
|||||||
name: string;
|
name: string;
|
||||||
role: RecipientRole;
|
role: RecipientRole;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
accessAuth?: TRecipientAccessAuthTypes[];
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
actionAuth?: TRecipientActionAuthTypes[];
|
||||||
}[];
|
}[];
|
||||||
requestMetadata: ApiRequestMetadata;
|
requestMetadata: ApiRequestMetadata;
|
||||||
}
|
}
|
||||||
@ -71,7 +71,9 @@ export const createDocumentRecipients = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipientsToCreate.some((recipient) => recipient.actionAuth);
|
const recipientsHaveActionAuth = recipientsToCreate.some(
|
||||||
|
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
// Check if user has permission to set the global action auth.
|
||||||
if (recipientsHaveActionAuth) {
|
if (recipientsHaveActionAuth) {
|
||||||
@ -110,8 +112,8 @@ export const createDocumentRecipients = async ({
|
|||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
normalizedRecipients.map(async (recipient) => {
|
normalizedRecipients.map(async (recipient) => {
|
||||||
const authOptions = createRecipientAuthOptions({
|
const authOptions = createRecipientAuthOptions({
|
||||||
accessAuth: recipient.accessAuth || null,
|
accessAuth: recipient.accessAuth ?? [],
|
||||||
actionAuth: recipient.actionAuth || null,
|
actionAuth: recipient.actionAuth ?? [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const createdRecipient = await tx.recipient.create({
|
const createdRecipient = await tx.recipient.create({
|
||||||
@ -140,8 +142,8 @@ export const createDocumentRecipients = async ({
|
|||||||
recipientName: createdRecipient.name,
|
recipientName: createdRecipient.name,
|
||||||
recipientId: createdRecipient.id,
|
recipientId: createdRecipient.id,
|
||||||
recipientRole: createdRecipient.role,
|
recipientRole: createdRecipient.role,
|
||||||
accessAuth: recipient.accessAuth || undefined,
|
accessAuth: recipient.accessAuth ?? [],
|
||||||
actionAuth: recipient.actionAuth || undefined,
|
actionAuth: recipient.actionAuth ?? [],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -19,8 +19,8 @@ export interface CreateTemplateRecipientsOptions {
|
|||||||
name: string;
|
name: string;
|
||||||
role: RecipientRole;
|
role: RecipientRole;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
accessAuth?: TRecipientAccessAuthTypes[];
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
actionAuth?: TRecipientActionAuthTypes[];
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,7 +60,9 @@ export const createTemplateRecipients = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipientsToCreate.some((recipient) => recipient.actionAuth);
|
const recipientsHaveActionAuth = recipientsToCreate.some(
|
||||||
|
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
// Check if user has permission to set the global action auth.
|
||||||
if (recipientsHaveActionAuth) {
|
if (recipientsHaveActionAuth) {
|
||||||
@ -99,8 +101,8 @@ export const createTemplateRecipients = async ({
|
|||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
normalizedRecipients.map(async (recipient) => {
|
normalizedRecipients.map(async (recipient) => {
|
||||||
const authOptions = createRecipientAuthOptions({
|
const authOptions = createRecipientAuthOptions({
|
||||||
accessAuth: recipient.accessAuth || null,
|
accessAuth: recipient.accessAuth ?? [],
|
||||||
actionAuth: recipient.actionAuth || null,
|
actionAuth: recipient.actionAuth ?? [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const createdRecipient = await tx.recipient.create({
|
const createdRecipient = await tx.recipient.create({
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { msg } from '@lingui/core/macro';
|
|||||||
import type { Recipient } from '@prisma/client';
|
import type { Recipient } from '@prisma/client';
|
||||||
import { RecipientRole } from '@prisma/client';
|
import { RecipientRole } from '@prisma/client';
|
||||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||||
|
import { isDeepEqual } from 'remeda';
|
||||||
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
import { mailer } from '@documenso/email/mailer';
|
import { mailer } from '@documenso/email/mailer';
|
||||||
@ -96,7 +97,9 @@ export const setDocumentRecipients = async ({
|
|||||||
throw new Error('Document already complete');
|
throw new Error('Document already complete');
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth);
|
const recipientsHaveActionAuth = recipients.some(
|
||||||
|
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
// Check if user has permission to set the global action auth.
|
||||||
if (recipientsHaveActionAuth) {
|
if (recipientsHaveActionAuth) {
|
||||||
@ -245,8 +248,8 @@ export const setDocumentRecipients = async ({
|
|||||||
metadata: requestMetadata,
|
metadata: requestMetadata,
|
||||||
data: {
|
data: {
|
||||||
...baseAuditLog,
|
...baseAuditLog,
|
||||||
accessAuth: recipient.accessAuth || undefined,
|
accessAuth: recipient.accessAuth || [],
|
||||||
actionAuth: recipient.actionAuth || undefined,
|
actionAuth: recipient.actionAuth || [],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@ -361,8 +364,8 @@ type RecipientData = {
|
|||||||
name: string;
|
name: string;
|
||||||
role: RecipientRole;
|
role: RecipientRole;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
accessAuth?: TRecipientAccessAuthTypes[];
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
actionAuth?: TRecipientActionAuthTypes[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type RecipientDataWithClientId = Recipient & {
|
type RecipientDataWithClientId = Recipient & {
|
||||||
@ -372,15 +375,15 @@ type RecipientDataWithClientId = Recipient & {
|
|||||||
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
||||||
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
const authOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
|
||||||
|
|
||||||
const newRecipientAccessAuth = newRecipientData.accessAuth || null;
|
const newRecipientAccessAuth = newRecipientData.accessAuth || [];
|
||||||
const newRecipientActionAuth = newRecipientData.actionAuth || null;
|
const newRecipientActionAuth = newRecipientData.actionAuth || [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
recipient.email !== newRecipientData.email ||
|
recipient.email !== newRecipientData.email ||
|
||||||
recipient.name !== newRecipientData.name ||
|
recipient.name !== newRecipientData.name ||
|
||||||
recipient.role !== newRecipientData.role ||
|
recipient.role !== newRecipientData.role ||
|
||||||
recipient.signingOrder !== newRecipientData.signingOrder ||
|
recipient.signingOrder !== newRecipientData.signingOrder ||
|
||||||
authOptions.accessAuth !== newRecipientAccessAuth ||
|
!isDeepEqual(authOptions.accessAuth, newRecipientAccessAuth) ||
|
||||||
authOptions.actionAuth !== newRecipientActionAuth
|
!isDeepEqual(authOptions.actionAuth, newRecipientActionAuth)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export type SetTemplateRecipientsOptions = {
|
|||||||
name: string;
|
name: string;
|
||||||
role: RecipientRole;
|
role: RecipientRole;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
actionAuth?: TRecipientActionAuthTypes[];
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -64,7 +64,9 @@ export const setTemplateRecipients = async ({
|
|||||||
throw new Error('Template not found');
|
throw new Error('Template not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth);
|
const recipientsHaveActionAuth = recipients.some(
|
||||||
|
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
// Check if user has permission to set the global action auth.
|
||||||
if (recipientsHaveActionAuth) {
|
if (recipientsHaveActionAuth) {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { Recipient } from '@prisma/client';
|
import type { Recipient } from '@prisma/client';
|
||||||
import { RecipientRole } from '@prisma/client';
|
import { RecipientRole } from '@prisma/client';
|
||||||
import { SendStatus, SigningStatus } from '@prisma/client';
|
import { SendStatus, SigningStatus } from '@prisma/client';
|
||||||
|
import { isDeepEqual } from 'remeda';
|
||||||
|
|
||||||
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
|
||||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||||
@ -72,7 +73,9 @@ export const updateDocumentRecipients = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth);
|
const recipientsHaveActionAuth = recipients.some(
|
||||||
|
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
// Check if user has permission to set the global action auth.
|
||||||
if (recipientsHaveActionAuth) {
|
if (recipientsHaveActionAuth) {
|
||||||
@ -218,8 +221,8 @@ type RecipientData = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
role?: RecipientRole;
|
role?: RecipientRole;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
accessAuth?: TRecipientAccessAuthTypes[];
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
actionAuth?: TRecipientActionAuthTypes[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: RecipientData) => {
|
||||||
@ -233,7 +236,7 @@ const hasRecipientBeenChanged = (recipient: Recipient, newRecipientData: Recipie
|
|||||||
recipient.name !== newRecipientData.name ||
|
recipient.name !== newRecipientData.name ||
|
||||||
recipient.role !== newRecipientData.role ||
|
recipient.role !== newRecipientData.role ||
|
||||||
recipient.signingOrder !== newRecipientData.signingOrder ||
|
recipient.signingOrder !== newRecipientData.signingOrder ||
|
||||||
authOptions.accessAuth !== newRecipientAccessAuth ||
|
!isDeepEqual(authOptions.accessAuth, newRecipientAccessAuth) ||
|
||||||
authOptions.actionAuth !== newRecipientActionAuth
|
!isDeepEqual(authOptions.actionAuth, newRecipientActionAuth)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export type UpdateRecipientOptions = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
role?: RecipientRole;
|
role?: RecipientRole;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
actionAuth?: TRecipientActionAuthTypes[];
|
||||||
userId: number;
|
userId: number;
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
requestMetadata?: RequestMetadata;
|
requestMetadata?: RequestMetadata;
|
||||||
@ -90,7 +90,7 @@ export const updateRecipient = async ({
|
|||||||
throw new Error('Recipient not found');
|
throw new Error('Recipient not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actionAuth) {
|
if (actionAuth && actionAuth.length > 0) {
|
||||||
const isDocumentEnterprise = await isUserEnterprise({
|
const isDocumentEnterprise = await isUserEnterprise({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
@ -117,7 +117,7 @@ export const updateRecipient = async ({
|
|||||||
signingOrder,
|
signingOrder,
|
||||||
authOptions: createRecipientAuthOptions({
|
authOptions: createRecipientAuthOptions({
|
||||||
accessAuth: recipientAuthOptions.accessAuth,
|
accessAuth: recipientAuthOptions.accessAuth,
|
||||||
actionAuth: actionAuth ?? null,
|
actionAuth: actionAuth ?? [],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -22,8 +22,8 @@ export interface UpdateTemplateRecipientsOptions {
|
|||||||
name?: string;
|
name?: string;
|
||||||
role?: RecipientRole;
|
role?: RecipientRole;
|
||||||
signingOrder?: number | null;
|
signingOrder?: number | null;
|
||||||
accessAuth?: TRecipientAccessAuthTypes | null;
|
accessAuth?: TRecipientAccessAuthTypes[];
|
||||||
actionAuth?: TRecipientActionAuthTypes | null;
|
actionAuth?: TRecipientActionAuthTypes[];
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +63,9 @@ export const updateTemplateRecipients = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipientsHaveActionAuth = recipients.some((recipient) => recipient.actionAuth);
|
const recipientsHaveActionAuth = recipients.some(
|
||||||
|
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
|
||||||
|
);
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
// Check if user has permission to set the global action auth.
|
||||||
if (recipientsHaveActionAuth) {
|
if (recipientsHaveActionAuth) {
|
||||||
|
|||||||
@ -70,7 +70,7 @@ export type CreateDocumentFromDirectTemplateOptions = {
|
|||||||
|
|
||||||
type CreatedDirectRecipientField = {
|
type CreatedDirectRecipientField = {
|
||||||
field: Field & { signature?: Signature | null };
|
field: Field & { signature?: Signature | null };
|
||||||
derivedRecipientActionAuth: TRecipientActionAuthTypes | null;
|
derivedRecipientActionAuth?: TRecipientActionAuthTypes;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ZCreateDocumentFromDirectTemplateResponseSchema = z.object({
|
export const ZCreateDocumentFromDirectTemplateResponseSchema = z.object({
|
||||||
@ -151,9 +151,9 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
const directRecipientName = user?.name || initialDirectRecipientName;
|
const directRecipientName = user?.name || initialDirectRecipientName;
|
||||||
|
|
||||||
// Ensure typesafety when we add more options.
|
// Ensure typesafety when we add more options.
|
||||||
const isAccessAuthValid = match(derivedRecipientAccessAuth)
|
const isAccessAuthValid = match(derivedRecipientAccessAuth.at(0))
|
||||||
.with(DocumentAccessAuth.ACCOUNT, () => user && user?.email === directRecipientEmail)
|
.with(DocumentAccessAuth.ACCOUNT, () => user && user?.email === directRecipientEmail)
|
||||||
.with(null, () => true)
|
.with(undefined, () => true)
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
|
|
||||||
if (!isAccessAuthValid) {
|
if (!isAccessAuthValid) {
|
||||||
@ -460,7 +460,7 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
const createdDirectRecipientFields: CreatedDirectRecipientField[] = [
|
const createdDirectRecipientFields: CreatedDirectRecipientField[] = [
|
||||||
...createdDirectRecipient.fields.map((field) => ({
|
...createdDirectRecipient.fields.map((field) => ({
|
||||||
field,
|
field,
|
||||||
derivedRecipientActionAuth: null,
|
derivedRecipientActionAuth: undefined,
|
||||||
})),
|
})),
|
||||||
...createdDirectRecipientSignatureFields,
|
...createdDirectRecipientSignatureFields,
|
||||||
];
|
];
|
||||||
@ -567,6 +567,7 @@ export const createDocumentFromDirectTemplate = async ({
|
|||||||
recipientId: createdDirectRecipient.id,
|
recipientId: createdDirectRecipient.id,
|
||||||
recipientName: createdDirectRecipient.name,
|
recipientName: createdDirectRecipient.name,
|
||||||
recipientRole: createdDirectRecipient.role,
|
recipientRole: createdDirectRecipient.role,
|
||||||
|
actionAuth: createdDirectRecipient.authOptions?.actionAuth ?? [],
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|||||||
@ -9,11 +9,13 @@ 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';
|
||||||
@ -508,10 +510,8 @@ 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);
|
|
||||||
|
|
||||||
return {
|
const payload = {
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
recipientId: recipient.id,
|
recipientId: recipient.id,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
@ -522,8 +522,38 @@ export const createDocumentFromTemplate = async ({
|
|||||||
height: field.height,
|
height: field.height,
|
||||||
customText: '',
|
customText: '',
|
||||||
inserted: false,
|
inserted: false,
|
||||||
fieldMeta: updatedFieldMeta,
|
fieldMeta: field.fieldMeta,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -15,8 +15,8 @@ export type UpdateTemplateOptions = {
|
|||||||
title?: string;
|
title?: string;
|
||||||
externalId?: string | null;
|
externalId?: string | null;
|
||||||
visibility?: DocumentVisibility;
|
visibility?: DocumentVisibility;
|
||||||
globalAccessAuth?: TDocumentAccessAuthTypes | null;
|
globalAccessAuth?: TDocumentAccessAuthTypes[];
|
||||||
globalActionAuth?: TDocumentActionAuthTypes | null;
|
globalActionAuth?: TDocumentActionAuthTypes[];
|
||||||
publicTitle?: string;
|
publicTitle?: string;
|
||||||
publicDescription?: string;
|
publicDescription?: string;
|
||||||
type?: Template['type'];
|
type?: Template['type'];
|
||||||
@ -74,7 +74,7 @@ export const updateTemplate = async ({
|
|||||||
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
data?.globalActionAuth === undefined ? documentGlobalActionAuth : data.globalActionAuth;
|
||||||
|
|
||||||
// Check if user has permission to set the global action auth.
|
// Check if user has permission to set the global action auth.
|
||||||
if (newGlobalActionAuth) {
|
if (newGlobalActionAuth && newGlobalActionAuth.length > 0) {
|
||||||
const isDocumentEnterprise = await isUserEnterprise({
|
const isDocumentEnterprise = await isUserEnterprise({
|
||||||
userId,
|
userId,
|
||||||
teamId,
|
teamId,
|
||||||
|
|||||||
@ -111,11 +111,6 @@ msgstr "{0, plural, one {1 Empfänger} other {# Empfänger}}"
|
|||||||
msgid "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}"
|
msgid "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}"
|
||||||
msgstr "{0, plural, one {Warte auf 1 Empfänger} other {Warte auf # Empfänger}}"
|
msgstr "{0, plural, one {Warte auf 1 Empfänger} other {Warte auf # Empfänger}}"
|
||||||
|
|
||||||
#. placeholder {0}: selectedValues.length
|
|
||||||
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
|
||||||
msgid "{0, plural, zero {Select values} other {# selected...}}"
|
|
||||||
msgstr "{0, plural, zero {Werte auswählen} other {# ausgewählt...}}"
|
|
||||||
|
|
||||||
#. placeholder {0}: _(FRIENDLY_FIELD_TYPE[fieldType as FieldType])
|
#. placeholder {0}: _(FRIENDLY_FIELD_TYPE[fieldType as FieldType])
|
||||||
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
|
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
|
||||||
msgid "{0}"
|
msgid "{0}"
|
||||||
@ -873,6 +868,7 @@ msgstr "Erweiterte Optionen"
|
|||||||
|
|
||||||
#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx
|
#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx
|
||||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx
|
#: packages/ui/primitives/template-flow/add-template-fields.tsx
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
#: packages/ui/primitives/document-flow/add-fields.tsx
|
#: packages/ui/primitives/document-flow/add-fields.tsx
|
||||||
msgid "Advanced settings"
|
msgid "Advanced settings"
|
||||||
msgstr "Erweiterte Einstellungen"
|
msgstr "Erweiterte Einstellungen"
|
||||||
@ -2725,9 +2721,14 @@ msgstr "Aufgrund einer unbezahlten Rechnung wurde Ihrem Team der Zugriff eingesc
|
|||||||
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
|
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
msgid "Duplicate"
|
msgid "Duplicate"
|
||||||
msgstr "Duplizieren"
|
msgstr "Duplizieren"
|
||||||
|
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
|
msgid "Duplicate on all pages"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
|
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
|
||||||
#: apps/remix/app/routes/_authenticated+/settings+/webhooks._index.tsx
|
#: apps/remix/app/routes/_authenticated+/settings+/webhooks._index.tsx
|
||||||
#: apps/remix/app/components/tables/templates-table-action-dropdown.tsx
|
#: apps/remix/app/components/tables/templates-table-action-dropdown.tsx
|
||||||
@ -3915,7 +3916,6 @@ msgstr "Keine gültigen direkten Vorlagen gefunden"
|
|||||||
msgid "No valid recipients found"
|
msgid "No valid recipients found"
|
||||||
msgstr "Keine gültigen Empfänger gefunden"
|
msgstr "Keine gültigen Empfänger gefunden"
|
||||||
|
|
||||||
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
|
||||||
#: apps/remix/app/components/general/multiselect-role-combobox.tsx
|
#: apps/remix/app/components/general/multiselect-role-combobox.tsx
|
||||||
#: packages/ui/primitives/multi-select-combobox.tsx
|
#: packages/ui/primitives/multi-select-combobox.tsx
|
||||||
#: packages/ui/primitives/combobox.tsx
|
#: packages/ui/primitives/combobox.tsx
|
||||||
@ -4607,6 +4607,7 @@ msgstr "Erinnerung: Bitte {recipientActionVerb} dein Dokument"
|
|||||||
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
|
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/team-email-delete-dialog.tsx
|
#: apps/remix/app/components/dialogs/team-email-delete-dialog.tsx
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
#: packages/ui/primitives/document-flow/add-fields.tsx
|
#: packages/ui/primitives/document-flow/add-fields.tsx
|
||||||
msgid "Remove"
|
msgid "Remove"
|
||||||
msgstr "Entfernen"
|
msgstr "Entfernen"
|
||||||
@ -4849,6 +4850,11 @@ msgstr "Standardoption auswählen"
|
|||||||
msgid "Select passkey"
|
msgid "Select passkey"
|
||||||
msgstr "Passkey auswählen"
|
msgstr "Passkey auswählen"
|
||||||
|
|
||||||
|
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
||||||
|
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
||||||
|
msgid "Select triggers"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
||||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
||||||
#: packages/ui/primitives/document-flow/add-subject.tsx
|
#: packages/ui/primitives/document-flow/add-subject.tsx
|
||||||
|
|||||||
@ -106,11 +106,6 @@ msgstr "{0, plural, one {1 Recipient} other {# Recipients}}"
|
|||||||
msgid "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}"
|
msgid "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}"
|
||||||
msgstr "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}"
|
msgstr "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}"
|
||||||
|
|
||||||
#. placeholder {0}: selectedValues.length
|
|
||||||
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
|
||||||
msgid "{0, plural, zero {Select values} other {# selected...}}"
|
|
||||||
msgstr "{0, plural, zero {Select values} other {# selected...}}"
|
|
||||||
|
|
||||||
#. placeholder {0}: _(FRIENDLY_FIELD_TYPE[fieldType as FieldType])
|
#. placeholder {0}: _(FRIENDLY_FIELD_TYPE[fieldType as FieldType])
|
||||||
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
|
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
|
||||||
msgid "{0}"
|
msgid "{0}"
|
||||||
@ -868,6 +863,7 @@ msgstr "Advanced Options"
|
|||||||
|
|
||||||
#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx
|
#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx
|
||||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx
|
#: packages/ui/primitives/template-flow/add-template-fields.tsx
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
#: packages/ui/primitives/document-flow/add-fields.tsx
|
#: packages/ui/primitives/document-flow/add-fields.tsx
|
||||||
msgid "Advanced settings"
|
msgid "Advanced settings"
|
||||||
msgstr "Advanced settings"
|
msgstr "Advanced settings"
|
||||||
@ -2720,9 +2716,14 @@ msgstr "Due to an unpaid invoice, your team has been restricted. Please settle t
|
|||||||
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
|
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
msgid "Duplicate"
|
msgid "Duplicate"
|
||||||
msgstr "Duplicate"
|
msgstr "Duplicate"
|
||||||
|
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
|
msgid "Duplicate on all pages"
|
||||||
|
msgstr "Duplicate on all pages"
|
||||||
|
|
||||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
|
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
|
||||||
#: apps/remix/app/routes/_authenticated+/settings+/webhooks._index.tsx
|
#: apps/remix/app/routes/_authenticated+/settings+/webhooks._index.tsx
|
||||||
#: apps/remix/app/components/tables/templates-table-action-dropdown.tsx
|
#: apps/remix/app/components/tables/templates-table-action-dropdown.tsx
|
||||||
@ -3910,7 +3911,6 @@ msgstr "No valid direct templates found"
|
|||||||
msgid "No valid recipients found"
|
msgid "No valid recipients found"
|
||||||
msgstr "No valid recipients found"
|
msgstr "No valid recipients found"
|
||||||
|
|
||||||
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
|
||||||
#: apps/remix/app/components/general/multiselect-role-combobox.tsx
|
#: apps/remix/app/components/general/multiselect-role-combobox.tsx
|
||||||
#: packages/ui/primitives/multi-select-combobox.tsx
|
#: packages/ui/primitives/multi-select-combobox.tsx
|
||||||
#: packages/ui/primitives/combobox.tsx
|
#: packages/ui/primitives/combobox.tsx
|
||||||
@ -4602,6 +4602,7 @@ msgstr "Reminder: Please {recipientActionVerb} your document"
|
|||||||
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
|
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/team-email-delete-dialog.tsx
|
#: apps/remix/app/components/dialogs/team-email-delete-dialog.tsx
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
#: packages/ui/primitives/document-flow/add-fields.tsx
|
#: packages/ui/primitives/document-flow/add-fields.tsx
|
||||||
msgid "Remove"
|
msgid "Remove"
|
||||||
msgstr "Remove"
|
msgstr "Remove"
|
||||||
@ -4844,6 +4845,11 @@ msgstr "Select default option"
|
|||||||
msgid "Select passkey"
|
msgid "Select passkey"
|
||||||
msgstr "Select passkey"
|
msgstr "Select passkey"
|
||||||
|
|
||||||
|
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
||||||
|
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
||||||
|
msgid "Select triggers"
|
||||||
|
msgstr "Select triggers"
|
||||||
|
|
||||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
||||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
||||||
#: packages/ui/primitives/document-flow/add-subject.tsx
|
#: packages/ui/primitives/document-flow/add-subject.tsx
|
||||||
|
|||||||
@ -111,11 +111,6 @@ msgstr "{0, plural, one {1 Destinatario} other {# Destinatarios}}"
|
|||||||
msgid "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}"
|
msgid "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}"
|
||||||
msgstr "{0, plural, one {Esperando 1 destinatario} other {Esperando # destinatarios}}"
|
msgstr "{0, plural, one {Esperando 1 destinatario} other {Esperando # destinatarios}}"
|
||||||
|
|
||||||
#. placeholder {0}: selectedValues.length
|
|
||||||
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
|
||||||
msgid "{0, plural, zero {Select values} other {# selected...}}"
|
|
||||||
msgstr "{0, plural, zero {Selecciona valores} other {# seleccionados...}}"
|
|
||||||
|
|
||||||
#. placeholder {0}: _(FRIENDLY_FIELD_TYPE[fieldType as FieldType])
|
#. placeholder {0}: _(FRIENDLY_FIELD_TYPE[fieldType as FieldType])
|
||||||
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
|
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
|
||||||
msgid "{0}"
|
msgid "{0}"
|
||||||
@ -873,6 +868,7 @@ msgstr "Opciones avanzadas"
|
|||||||
|
|
||||||
#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx
|
#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx
|
||||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx
|
#: packages/ui/primitives/template-flow/add-template-fields.tsx
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
#: packages/ui/primitives/document-flow/add-fields.tsx
|
#: packages/ui/primitives/document-flow/add-fields.tsx
|
||||||
msgid "Advanced settings"
|
msgid "Advanced settings"
|
||||||
msgstr "Configuraciones avanzadas"
|
msgstr "Configuraciones avanzadas"
|
||||||
@ -2725,9 +2721,14 @@ msgstr "Debido a una factura impaga, tu equipo ha sido restringido. Realiza el p
|
|||||||
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
|
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
msgid "Duplicate"
|
msgid "Duplicate"
|
||||||
msgstr "Duplicar"
|
msgstr "Duplicar"
|
||||||
|
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
|
msgid "Duplicate on all pages"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
|
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
|
||||||
#: apps/remix/app/routes/_authenticated+/settings+/webhooks._index.tsx
|
#: apps/remix/app/routes/_authenticated+/settings+/webhooks._index.tsx
|
||||||
#: apps/remix/app/components/tables/templates-table-action-dropdown.tsx
|
#: apps/remix/app/components/tables/templates-table-action-dropdown.tsx
|
||||||
@ -3915,7 +3916,6 @@ msgstr "No se encontraron plantillas directas válidas"
|
|||||||
msgid "No valid recipients found"
|
msgid "No valid recipients found"
|
||||||
msgstr "No se encontraron destinatarios válidos"
|
msgstr "No se encontraron destinatarios válidos"
|
||||||
|
|
||||||
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
|
||||||
#: apps/remix/app/components/general/multiselect-role-combobox.tsx
|
#: apps/remix/app/components/general/multiselect-role-combobox.tsx
|
||||||
#: packages/ui/primitives/multi-select-combobox.tsx
|
#: packages/ui/primitives/multi-select-combobox.tsx
|
||||||
#: packages/ui/primitives/combobox.tsx
|
#: packages/ui/primitives/combobox.tsx
|
||||||
@ -4607,6 +4607,7 @@ msgstr "Recordatorio: Por favor {recipientActionVerb} tu documento"
|
|||||||
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
|
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/team-email-delete-dialog.tsx
|
#: apps/remix/app/components/dialogs/team-email-delete-dialog.tsx
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
#: packages/ui/primitives/document-flow/add-fields.tsx
|
#: packages/ui/primitives/document-flow/add-fields.tsx
|
||||||
msgid "Remove"
|
msgid "Remove"
|
||||||
msgstr "Eliminar"
|
msgstr "Eliminar"
|
||||||
@ -4849,6 +4850,11 @@ msgstr "Seleccionar opción predeterminada"
|
|||||||
msgid "Select passkey"
|
msgid "Select passkey"
|
||||||
msgstr "Seleccionar clave de acceso"
|
msgstr "Seleccionar clave de acceso"
|
||||||
|
|
||||||
|
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
||||||
|
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
||||||
|
msgid "Select triggers"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
||||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
||||||
#: packages/ui/primitives/document-flow/add-subject.tsx
|
#: packages/ui/primitives/document-flow/add-subject.tsx
|
||||||
|
|||||||
@ -111,11 +111,6 @@ msgstr "{0, plural, one {1 Destinataire} other {# Destinataires}}"
|
|||||||
msgid "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}"
|
msgid "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}"
|
||||||
msgstr "{0, plural, one {En attente d'1 destinataire} other {En attente de # destinataires}}"
|
msgstr "{0, plural, one {En attente d'1 destinataire} other {En attente de # destinataires}}"
|
||||||
|
|
||||||
#. placeholder {0}: selectedValues.length
|
|
||||||
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
|
||||||
msgid "{0, plural, zero {Select values} other {# selected...}}"
|
|
||||||
msgstr "{0, plural, zero {Sélectionner des valeurs} other {# sélectionnées...}}"
|
|
||||||
|
|
||||||
#. placeholder {0}: _(FRIENDLY_FIELD_TYPE[fieldType as FieldType])
|
#. placeholder {0}: _(FRIENDLY_FIELD_TYPE[fieldType as FieldType])
|
||||||
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
|
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
|
||||||
msgid "{0}"
|
msgid "{0}"
|
||||||
@ -873,6 +868,7 @@ msgstr "Options avancées"
|
|||||||
|
|
||||||
#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx
|
#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx
|
||||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx
|
#: packages/ui/primitives/template-flow/add-template-fields.tsx
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
#: packages/ui/primitives/document-flow/add-fields.tsx
|
#: packages/ui/primitives/document-flow/add-fields.tsx
|
||||||
msgid "Advanced settings"
|
msgid "Advanced settings"
|
||||||
msgstr "Paramètres avancés"
|
msgstr "Paramètres avancés"
|
||||||
@ -2725,9 +2721,14 @@ msgstr "En raison d'une facture impayée, votre équipe a été restreinte. Veui
|
|||||||
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
|
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
msgid "Duplicate"
|
msgid "Duplicate"
|
||||||
msgstr "Dupliquer"
|
msgstr "Dupliquer"
|
||||||
|
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
|
msgid "Duplicate on all pages"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
|
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
|
||||||
#: apps/remix/app/routes/_authenticated+/settings+/webhooks._index.tsx
|
#: apps/remix/app/routes/_authenticated+/settings+/webhooks._index.tsx
|
||||||
#: apps/remix/app/components/tables/templates-table-action-dropdown.tsx
|
#: apps/remix/app/components/tables/templates-table-action-dropdown.tsx
|
||||||
@ -3915,7 +3916,6 @@ msgstr "Aucun modèle direct valide trouvé"
|
|||||||
msgid "No valid recipients found"
|
msgid "No valid recipients found"
|
||||||
msgstr "Aucun destinataire valide trouvé"
|
msgstr "Aucun destinataire valide trouvé"
|
||||||
|
|
||||||
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
|
||||||
#: apps/remix/app/components/general/multiselect-role-combobox.tsx
|
#: apps/remix/app/components/general/multiselect-role-combobox.tsx
|
||||||
#: packages/ui/primitives/multi-select-combobox.tsx
|
#: packages/ui/primitives/multi-select-combobox.tsx
|
||||||
#: packages/ui/primitives/combobox.tsx
|
#: packages/ui/primitives/combobox.tsx
|
||||||
@ -4607,6 +4607,7 @@ msgstr "Rappel : Veuillez {recipientActionVerb} votre document"
|
|||||||
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
|
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/team-email-delete-dialog.tsx
|
#: apps/remix/app/components/dialogs/team-email-delete-dialog.tsx
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
#: packages/ui/primitives/document-flow/add-fields.tsx
|
#: packages/ui/primitives/document-flow/add-fields.tsx
|
||||||
msgid "Remove"
|
msgid "Remove"
|
||||||
msgstr "Retirer"
|
msgstr "Retirer"
|
||||||
@ -4849,6 +4850,11 @@ msgstr "Sélectionner l'option par défaut"
|
|||||||
msgid "Select passkey"
|
msgid "Select passkey"
|
||||||
msgstr "Sélectionner la clé d'authentification"
|
msgstr "Sélectionner la clé d'authentification"
|
||||||
|
|
||||||
|
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
||||||
|
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
||||||
|
msgid "Select triggers"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
||||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
||||||
#: packages/ui/primitives/document-flow/add-subject.tsx
|
#: packages/ui/primitives/document-flow/add-subject.tsx
|
||||||
|
|||||||
@ -111,11 +111,6 @@ msgstr "{0, plural, one {1 destinatario} other {# destinatari}}"
|
|||||||
msgid "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}"
|
msgid "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}"
|
||||||
msgstr "{0, plural, one {In attesa di 1 destinatario} other {In attesa di # destinatari}}"
|
msgstr "{0, plural, one {In attesa di 1 destinatario} other {In attesa di # destinatari}}"
|
||||||
|
|
||||||
#. placeholder {0}: selectedValues.length
|
|
||||||
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
|
||||||
msgid "{0, plural, zero {Select values} other {# selected...}}"
|
|
||||||
msgstr "{0, plural, zero {Seleziona valori} other {# selezionati...}}"
|
|
||||||
|
|
||||||
#. placeholder {0}: _(FRIENDLY_FIELD_TYPE[fieldType as FieldType])
|
#. placeholder {0}: _(FRIENDLY_FIELD_TYPE[fieldType as FieldType])
|
||||||
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
|
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
|
||||||
msgid "{0}"
|
msgid "{0}"
|
||||||
@ -873,6 +868,7 @@ msgstr "Opzioni avanzate"
|
|||||||
|
|
||||||
#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx
|
#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx
|
||||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx
|
#: packages/ui/primitives/template-flow/add-template-fields.tsx
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
#: packages/ui/primitives/document-flow/add-fields.tsx
|
#: packages/ui/primitives/document-flow/add-fields.tsx
|
||||||
msgid "Advanced settings"
|
msgid "Advanced settings"
|
||||||
msgstr "Impostazioni avanzate"
|
msgstr "Impostazioni avanzate"
|
||||||
@ -2725,9 +2721,14 @@ msgstr "A causa di una fattura non pagata, il vostro team è stato limitato. Si
|
|||||||
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
|
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
msgid "Duplicate"
|
msgid "Duplicate"
|
||||||
msgstr "Duplica"
|
msgstr "Duplica"
|
||||||
|
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
|
msgid "Duplicate on all pages"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
|
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
|
||||||
#: apps/remix/app/routes/_authenticated+/settings+/webhooks._index.tsx
|
#: apps/remix/app/routes/_authenticated+/settings+/webhooks._index.tsx
|
||||||
#: apps/remix/app/components/tables/templates-table-action-dropdown.tsx
|
#: apps/remix/app/components/tables/templates-table-action-dropdown.tsx
|
||||||
@ -3915,7 +3916,6 @@ msgstr "Nessun modello diretto valido trovato"
|
|||||||
msgid "No valid recipients found"
|
msgid "No valid recipients found"
|
||||||
msgstr "Nessun destinatario valido trovato"
|
msgstr "Nessun destinatario valido trovato"
|
||||||
|
|
||||||
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
|
||||||
#: apps/remix/app/components/general/multiselect-role-combobox.tsx
|
#: apps/remix/app/components/general/multiselect-role-combobox.tsx
|
||||||
#: packages/ui/primitives/multi-select-combobox.tsx
|
#: packages/ui/primitives/multi-select-combobox.tsx
|
||||||
#: packages/ui/primitives/combobox.tsx
|
#: packages/ui/primitives/combobox.tsx
|
||||||
@ -4607,6 +4607,7 @@ msgstr "Promemoria: per favore {recipientActionVerb} il tuo documento"
|
|||||||
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
|
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/team-email-delete-dialog.tsx
|
#: apps/remix/app/components/dialogs/team-email-delete-dialog.tsx
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
#: packages/ui/primitives/document-flow/add-fields.tsx
|
#: packages/ui/primitives/document-flow/add-fields.tsx
|
||||||
msgid "Remove"
|
msgid "Remove"
|
||||||
msgstr "Rimuovi"
|
msgstr "Rimuovi"
|
||||||
@ -4849,6 +4850,11 @@ msgstr "Seleziona opzione predefinita"
|
|||||||
msgid "Select passkey"
|
msgid "Select passkey"
|
||||||
msgstr "Seleziona una chiave di accesso"
|
msgstr "Seleziona una chiave di accesso"
|
||||||
|
|
||||||
|
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
||||||
|
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
||||||
|
msgid "Select triggers"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
||||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
||||||
#: packages/ui/primitives/document-flow/add-subject.tsx
|
#: packages/ui/primitives/document-flow/add-subject.tsx
|
||||||
|
|||||||
@ -111,11 +111,6 @@ msgstr "{0, plural, one {1 odbiorca} few {# odbiorców} many {# odbiorców} othe
|
|||||||
msgid "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}"
|
msgid "{0, plural, one {Waiting on 1 recipient} other {Waiting on # recipients}}"
|
||||||
msgstr "{0, plural, one {Czekam na 1 odbiorcę} few {Czekam na # odbiorców} many {Czekam na # odbiorców} other {Czekam na # odbiorców}}"
|
msgstr "{0, plural, one {Czekam na 1 odbiorcę} few {Czekam na # odbiorców} many {Czekam na # odbiorców} other {Czekam na # odbiorców}}"
|
||||||
|
|
||||||
#. placeholder {0}: selectedValues.length
|
|
||||||
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
|
||||||
msgid "{0, plural, zero {Select values} other {# selected...}}"
|
|
||||||
msgstr "{0, plural, zero {Wybierz wartości} one {# wybrana...} few {# wybrane...} many {# wybranych...} other {# wybranych...}}"
|
|
||||||
|
|
||||||
#. placeholder {0}: _(FRIENDLY_FIELD_TYPE[fieldType as FieldType])
|
#. placeholder {0}: _(FRIENDLY_FIELD_TYPE[fieldType as FieldType])
|
||||||
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
|
#: apps/remix/app/components/general/document-signing/document-signing-auto-sign.tsx
|
||||||
msgid "{0}"
|
msgid "{0}"
|
||||||
@ -873,6 +868,7 @@ msgstr "Opcje zaawansowane"
|
|||||||
|
|
||||||
#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx
|
#: apps/remix/app/components/embed/authoring/field-advanced-settings-drawer.tsx
|
||||||
#: packages/ui/primitives/template-flow/add-template-fields.tsx
|
#: packages/ui/primitives/template-flow/add-template-fields.tsx
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
#: packages/ui/primitives/document-flow/add-fields.tsx
|
#: packages/ui/primitives/document-flow/add-fields.tsx
|
||||||
msgid "Advanced settings"
|
msgid "Advanced settings"
|
||||||
msgstr "Ustawienia zaawansowane"
|
msgstr "Ustawienia zaawansowane"
|
||||||
@ -2725,9 +2721,14 @@ msgstr "Z powodu nieopłaconej faktury Twój zespół został ograniczony. Prosz
|
|||||||
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
|
#: apps/remix/app/components/dialogs/template-duplicate-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
msgid "Duplicate"
|
msgid "Duplicate"
|
||||||
msgstr "Zduplikuj"
|
msgstr "Zduplikuj"
|
||||||
|
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
|
msgid "Duplicate on all pages"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
|
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
|
||||||
#: apps/remix/app/routes/_authenticated+/settings+/webhooks._index.tsx
|
#: apps/remix/app/routes/_authenticated+/settings+/webhooks._index.tsx
|
||||||
#: apps/remix/app/components/tables/templates-table-action-dropdown.tsx
|
#: apps/remix/app/components/tables/templates-table-action-dropdown.tsx
|
||||||
@ -3915,7 +3916,6 @@ msgstr "Nie znaleziono ważnych szablonów bezpośrednich"
|
|||||||
msgid "No valid recipients found"
|
msgid "No valid recipients found"
|
||||||
msgstr "Nie znaleziono ważnych odbiorców"
|
msgstr "Nie znaleziono ważnych odbiorców"
|
||||||
|
|
||||||
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
|
||||||
#: apps/remix/app/components/general/multiselect-role-combobox.tsx
|
#: apps/remix/app/components/general/multiselect-role-combobox.tsx
|
||||||
#: packages/ui/primitives/multi-select-combobox.tsx
|
#: packages/ui/primitives/multi-select-combobox.tsx
|
||||||
#: packages/ui/primitives/combobox.tsx
|
#: packages/ui/primitives/combobox.tsx
|
||||||
@ -4607,6 +4607,7 @@ msgstr "Przypomnienie: Proszę {recipientActionVerb} Twój dokument"
|
|||||||
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
|
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
|
||||||
#: apps/remix/app/components/dialogs/team-email-delete-dialog.tsx
|
#: apps/remix/app/components/dialogs/team-email-delete-dialog.tsx
|
||||||
|
#: packages/ui/primitives/document-flow/field-item.tsx
|
||||||
#: packages/ui/primitives/document-flow/add-fields.tsx
|
#: packages/ui/primitives/document-flow/add-fields.tsx
|
||||||
msgid "Remove"
|
msgid "Remove"
|
||||||
msgstr "Usuń"
|
msgstr "Usuń"
|
||||||
@ -4849,6 +4850,11 @@ msgstr "Wybierz domyślną opcję"
|
|||||||
msgid "Select passkey"
|
msgid "Select passkey"
|
||||||
msgstr "Wybierz klucz uwierzytelniający"
|
msgstr "Wybierz klucz uwierzytelniający"
|
||||||
|
|
||||||
|
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
||||||
|
#: apps/remix/app/components/general/webhook-multiselect-combobox.tsx
|
||||||
|
msgid "Select triggers"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
||||||
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
|
||||||
#: packages/ui/primitives/document-flow/add-subject.tsx
|
#: packages/ui/primitives/document-flow/add-subject.tsx
|
||||||
|
|||||||
@ -123,8 +123,8 @@ export const ZDocumentAuditLogFieldDiffSchema = z.union([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
export const ZGenericFromToSchema = z.object({
|
export const ZGenericFromToSchema = z.object({
|
||||||
from: z.string().nullable(),
|
from: z.union([z.string(), z.array(z.string())]).nullable(),
|
||||||
to: z.string().nullable(),
|
to: z.union([z.string(), z.array(z.string())]).nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ZRecipientDiffActionAuthSchema = ZGenericFromToSchema.extend({
|
export const ZRecipientDiffActionAuthSchema = ZGenericFromToSchema.extend({
|
||||||
@ -296,7 +296,7 @@ export const ZDocumentAuditLogEventDocumentFieldInsertedSchema = z.object({
|
|||||||
},
|
},
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
type: ZRecipientActionAuthTypesSchema,
|
type: ZRecipientActionAuthTypesSchema.optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
),
|
),
|
||||||
@ -384,7 +384,7 @@ export const ZDocumentAuditLogEventDocumentFieldPrefilledSchema = z.object({
|
|||||||
},
|
},
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
type: ZRecipientActionAuthTypesSchema,
|
type: ZRecipientActionAuthTypesSchema.optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
),
|
),
|
||||||
@ -428,7 +428,13 @@ export const ZDocumentAuditLogEventDocumentMetaUpdatedSchema = z.object({
|
|||||||
export const ZDocumentAuditLogEventDocumentOpenedSchema = z.object({
|
export const ZDocumentAuditLogEventDocumentOpenedSchema = z.object({
|
||||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED),
|
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED),
|
||||||
data: ZBaseRecipientDataSchema.extend({
|
data: ZBaseRecipientDataSchema.extend({
|
||||||
accessAuth: z.string().optional(),
|
accessAuth: z.preprocess((unknownValue) => {
|
||||||
|
if (!unknownValue) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.isArray(unknownValue) ? unknownValue : [unknownValue];
|
||||||
|
}, z.array(ZRecipientAccessAuthTypesSchema)),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -438,7 +444,13 @@ export const ZDocumentAuditLogEventDocumentOpenedSchema = z.object({
|
|||||||
export const ZDocumentAuditLogEventDocumentRecipientCompleteSchema = z.object({
|
export const ZDocumentAuditLogEventDocumentRecipientCompleteSchema = z.object({
|
||||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED),
|
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED),
|
||||||
data: ZBaseRecipientDataSchema.extend({
|
data: ZBaseRecipientDataSchema.extend({
|
||||||
actionAuth: z.string().optional(),
|
actionAuth: z.preprocess((unknownValue) => {
|
||||||
|
if (!unknownValue) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.isArray(unknownValue) ? unknownValue : [unknownValue];
|
||||||
|
}, z.array(ZRecipientActionAuthTypesSchema)),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -516,8 +528,20 @@ export const ZDocumentAuditLogEventFieldUpdatedSchema = z.object({
|
|||||||
export const ZDocumentAuditLogEventRecipientAddedSchema = z.object({
|
export const ZDocumentAuditLogEventRecipientAddedSchema = z.object({
|
||||||
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED),
|
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED),
|
||||||
data: ZBaseRecipientDataSchema.extend({
|
data: ZBaseRecipientDataSchema.extend({
|
||||||
accessAuth: ZRecipientAccessAuthTypesSchema.optional(),
|
accessAuth: z.preprocess((unknownValue) => {
|
||||||
actionAuth: ZRecipientActionAuthTypesSchema.optional(),
|
if (!unknownValue) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.isArray(unknownValue) ? unknownValue : [unknownValue];
|
||||||
|
}, z.array(ZRecipientAccessAuthTypesSchema)),
|
||||||
|
actionAuth: z.preprocess((unknownValue) => {
|
||||||
|
if (!unknownValue) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.isArray(unknownValue) ? unknownValue : [unknownValue];
|
||||||
|
}, z.array(ZRecipientActionAuthTypesSchema)),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -9,8 +9,10 @@ export const ZDocumentAuthTypesSchema = z.enum([
|
|||||||
'ACCOUNT',
|
'ACCOUNT',
|
||||||
'PASSKEY',
|
'PASSKEY',
|
||||||
'TWO_FACTOR_AUTH',
|
'TWO_FACTOR_AUTH',
|
||||||
|
'PASSWORD',
|
||||||
'EXPLICIT_NONE',
|
'EXPLICIT_NONE',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const DocumentAuth = ZDocumentAuthTypesSchema.Enum;
|
export const DocumentAuth = ZDocumentAuthTypesSchema.Enum;
|
||||||
|
|
||||||
const ZDocumentAuthAccountSchema = z.object({
|
const ZDocumentAuthAccountSchema = z.object({
|
||||||
@ -27,6 +29,11 @@ const ZDocumentAuthPasskeySchema = z.object({
|
|||||||
tokenReference: z.string().min(1),
|
tokenReference: z.string().min(1),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ZDocumentAuthPasswordSchema = z.object({
|
||||||
|
type: z.literal(DocumentAuth.PASSWORD),
|
||||||
|
password: z.string().min(1),
|
||||||
|
});
|
||||||
|
|
||||||
const ZDocumentAuth2FASchema = z.object({
|
const ZDocumentAuth2FASchema = z.object({
|
||||||
type: z.literal(DocumentAuth.TWO_FACTOR_AUTH),
|
type: z.literal(DocumentAuth.TWO_FACTOR_AUTH),
|
||||||
token: z.string().min(4).max(10),
|
token: z.string().min(4).max(10),
|
||||||
@ -40,6 +47,7 @@ export const ZDocumentAuthMethodsSchema = z.discriminatedUnion('type', [
|
|||||||
ZDocumentAuthExplicitNoneSchema,
|
ZDocumentAuthExplicitNoneSchema,
|
||||||
ZDocumentAuthPasskeySchema,
|
ZDocumentAuthPasskeySchema,
|
||||||
ZDocumentAuth2FASchema,
|
ZDocumentAuth2FASchema,
|
||||||
|
ZDocumentAuthPasswordSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,9 +69,15 @@ export const ZDocumentActionAuthSchema = z.discriminatedUnion('type', [
|
|||||||
ZDocumentAuthAccountSchema,
|
ZDocumentAuthAccountSchema,
|
||||||
ZDocumentAuthPasskeySchema,
|
ZDocumentAuthPasskeySchema,
|
||||||
ZDocumentAuth2FASchema,
|
ZDocumentAuth2FASchema,
|
||||||
|
ZDocumentAuthPasswordSchema,
|
||||||
]);
|
]);
|
||||||
export const ZDocumentActionAuthTypesSchema = z
|
export const ZDocumentActionAuthTypesSchema = z
|
||||||
.enum([DocumentAuth.ACCOUNT, DocumentAuth.PASSKEY, DocumentAuth.TWO_FACTOR_AUTH])
|
.enum([
|
||||||
|
DocumentAuth.ACCOUNT,
|
||||||
|
DocumentAuth.PASSKEY,
|
||||||
|
DocumentAuth.TWO_FACTOR_AUTH,
|
||||||
|
DocumentAuth.PASSWORD,
|
||||||
|
])
|
||||||
.describe(
|
.describe(
|
||||||
'The type of authentication required for the recipient to sign the document. This field is restricted to Enterprise plan users only.',
|
'The type of authentication required for the recipient to sign the document. This field is restricted to Enterprise plan users only.',
|
||||||
);
|
);
|
||||||
@ -89,6 +103,7 @@ export const ZRecipientActionAuthSchema = z.discriminatedUnion('type', [
|
|||||||
ZDocumentAuthAccountSchema,
|
ZDocumentAuthAccountSchema,
|
||||||
ZDocumentAuthPasskeySchema,
|
ZDocumentAuthPasskeySchema,
|
||||||
ZDocumentAuth2FASchema,
|
ZDocumentAuth2FASchema,
|
||||||
|
ZDocumentAuthPasswordSchema,
|
||||||
ZDocumentAuthExplicitNoneSchema,
|
ZDocumentAuthExplicitNoneSchema,
|
||||||
]);
|
]);
|
||||||
export const ZRecipientActionAuthTypesSchema = z
|
export const ZRecipientActionAuthTypesSchema = z
|
||||||
@ -96,6 +111,7 @@ export const ZRecipientActionAuthTypesSchema = z
|
|||||||
DocumentAuth.ACCOUNT,
|
DocumentAuth.ACCOUNT,
|
||||||
DocumentAuth.PASSKEY,
|
DocumentAuth.PASSKEY,
|
||||||
DocumentAuth.TWO_FACTOR_AUTH,
|
DocumentAuth.TWO_FACTOR_AUTH,
|
||||||
|
DocumentAuth.PASSWORD,
|
||||||
DocumentAuth.EXPLICIT_NONE,
|
DocumentAuth.EXPLICIT_NONE,
|
||||||
])
|
])
|
||||||
.describe('The type of authentication required for the recipient to sign the document.');
|
.describe('The type of authentication required for the recipient to sign the document.');
|
||||||
@ -110,18 +126,26 @@ export const RecipientActionAuth = ZRecipientActionAuthTypesSchema.Enum;
|
|||||||
*/
|
*/
|
||||||
export const ZDocumentAuthOptionsSchema = z.preprocess(
|
export const ZDocumentAuthOptionsSchema = z.preprocess(
|
||||||
(unknownValue) => {
|
(unknownValue) => {
|
||||||
if (unknownValue) {
|
if (!unknownValue || typeof unknownValue !== 'object') {
|
||||||
return unknownValue;
|
return {
|
||||||
|
globalAccessAuth: [],
|
||||||
|
globalActionAuth: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const globalAccessAuth =
|
||||||
|
'globalAccessAuth' in unknownValue ? processAuthValue(unknownValue.globalAccessAuth) : [];
|
||||||
|
const globalActionAuth =
|
||||||
|
'globalActionAuth' in unknownValue ? processAuthValue(unknownValue.globalActionAuth) : [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
globalAccessAuth: null,
|
globalAccessAuth,
|
||||||
globalActionAuth: null,
|
globalActionAuth,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
z.object({
|
z.object({
|
||||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullable(),
|
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema),
|
||||||
globalActionAuth: ZDocumentActionAuthTypesSchema.nullable(),
|
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -130,21 +154,46 @@ export const ZDocumentAuthOptionsSchema = z.preprocess(
|
|||||||
*/
|
*/
|
||||||
export const ZRecipientAuthOptionsSchema = z.preprocess(
|
export const ZRecipientAuthOptionsSchema = z.preprocess(
|
||||||
(unknownValue) => {
|
(unknownValue) => {
|
||||||
if (unknownValue) {
|
if (!unknownValue || typeof unknownValue !== 'object') {
|
||||||
return unknownValue;
|
return {
|
||||||
|
accessAuth: [],
|
||||||
|
actionAuth: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const accessAuth =
|
||||||
|
'accessAuth' in unknownValue ? processAuthValue(unknownValue.accessAuth) : [];
|
||||||
|
const actionAuth =
|
||||||
|
'actionAuth' in unknownValue ? processAuthValue(unknownValue.actionAuth) : [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessAuth: null,
|
accessAuth,
|
||||||
actionAuth: null,
|
actionAuth,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
z.object({
|
z.object({
|
||||||
accessAuth: ZRecipientAccessAuthTypesSchema.nullable(),
|
accessAuth: z.array(ZRecipientAccessAuthTypesSchema),
|
||||||
actionAuth: ZRecipientActionAuthTypesSchema.nullable(),
|
actionAuth: z.array(ZRecipientActionAuthTypesSchema),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to process the auth value.
|
||||||
|
*
|
||||||
|
* Converts the old singular auth value to an array of auth values.
|
||||||
|
*/
|
||||||
|
const processAuthValue = (value: unknown) => {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [value];
|
||||||
|
};
|
||||||
|
|
||||||
export type TDocumentAuth = z.infer<typeof ZDocumentAuthTypesSchema>;
|
export type TDocumentAuth = z.infer<typeof ZDocumentAuthTypesSchema>;
|
||||||
export type TDocumentAuthMethods = z.infer<typeof ZDocumentAuthMethodsSchema>;
|
export type TDocumentAuthMethods = z.infer<typeof ZDocumentAuthMethodsSchema>;
|
||||||
export type TDocumentAuthOptions = z.infer<typeof ZDocumentAuthOptionsSchema>;
|
export type TDocumentAuthOptions = z.infer<typeof ZDocumentAuthOptionsSchema>;
|
||||||
|
|||||||
@ -155,6 +155,10 @@ 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(),
|
||||||
|
}),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import type { I18n } from '@lingui/core';
|
|||||||
import { msg } from '@lingui/core/macro';
|
import { msg } from '@lingui/core/macro';
|
||||||
import type { DocumentAuditLog, DocumentMeta, Field, Recipient } from '@prisma/client';
|
import type { DocumentAuditLog, DocumentMeta, Field, Recipient } from '@prisma/client';
|
||||||
import { RecipientRole } from '@prisma/client';
|
import { RecipientRole } from '@prisma/client';
|
||||||
|
import { isDeepEqual } from 'remeda';
|
||||||
import { match } from 'ts-pattern';
|
import { match } from 'ts-pattern';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
@ -106,7 +107,7 @@ export const diffRecipientChanges = (
|
|||||||
const newActionAuth =
|
const newActionAuth =
|
||||||
newAuthOptions?.actionAuth === undefined ? oldActionAuth : newAuthOptions.actionAuth;
|
newAuthOptions?.actionAuth === undefined ? oldActionAuth : newAuthOptions.actionAuth;
|
||||||
|
|
||||||
if (oldAccessAuth !== newAccessAuth) {
|
if (!isDeepEqual(oldAccessAuth, newAccessAuth)) {
|
||||||
diffs.push({
|
diffs.push({
|
||||||
type: RECIPIENT_DIFF_TYPE.ACCESS_AUTH,
|
type: RECIPIENT_DIFF_TYPE.ACCESS_AUTH,
|
||||||
from: oldAccessAuth ?? '',
|
from: oldAccessAuth ?? '',
|
||||||
@ -114,7 +115,7 @@ export const diffRecipientChanges = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldActionAuth !== newActionAuth) {
|
if (!isDeepEqual(oldActionAuth, newActionAuth)) {
|
||||||
diffs.push({
|
diffs.push({
|
||||||
type: RECIPIENT_DIFF_TYPE.ACTION_AUTH,
|
type: RECIPIENT_DIFF_TYPE.ACTION_AUTH,
|
||||||
from: oldActionAuth ?? '',
|
from: oldActionAuth ?? '',
|
||||||
|
|||||||
@ -27,17 +27,21 @@ export const extractDocumentAuthMethods = ({
|
|||||||
const documentAuthOption = ZDocumentAuthOptionsSchema.parse(documentAuth);
|
const documentAuthOption = ZDocumentAuthOptionsSchema.parse(documentAuth);
|
||||||
const recipientAuthOption = ZRecipientAuthOptionsSchema.parse(recipientAuth);
|
const recipientAuthOption = ZRecipientAuthOptionsSchema.parse(recipientAuth);
|
||||||
|
|
||||||
const derivedRecipientAccessAuth: TRecipientAccessAuthTypes | null =
|
const derivedRecipientAccessAuth: TRecipientAccessAuthTypes[] =
|
||||||
recipientAuthOption.accessAuth || documentAuthOption.globalAccessAuth;
|
recipientAuthOption.accessAuth.length > 0
|
||||||
|
? recipientAuthOption.accessAuth
|
||||||
|
: documentAuthOption.globalAccessAuth;
|
||||||
|
|
||||||
const derivedRecipientActionAuth: TRecipientActionAuthTypes | null =
|
const derivedRecipientActionAuth: TRecipientActionAuthTypes[] =
|
||||||
recipientAuthOption.actionAuth || documentAuthOption.globalActionAuth;
|
recipientAuthOption.actionAuth.length > 0
|
||||||
|
? recipientAuthOption.actionAuth
|
||||||
|
: documentAuthOption.globalActionAuth;
|
||||||
|
|
||||||
const recipientAccessAuthRequired = derivedRecipientAccessAuth !== null;
|
const recipientAccessAuthRequired = derivedRecipientAccessAuth.length > 0;
|
||||||
|
|
||||||
const recipientActionAuthRequired =
|
const recipientActionAuthRequired =
|
||||||
derivedRecipientActionAuth !== DocumentAuth.EXPLICIT_NONE &&
|
derivedRecipientActionAuth.length > 0 &&
|
||||||
derivedRecipientActionAuth !== null;
|
!derivedRecipientActionAuth.includes(DocumentAuth.EXPLICIT_NONE);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
derivedRecipientAccessAuth,
|
derivedRecipientAccessAuth,
|
||||||
@ -54,8 +58,8 @@ export const extractDocumentAuthMethods = ({
|
|||||||
*/
|
*/
|
||||||
export const createDocumentAuthOptions = (options: TDocumentAuthOptions): TDocumentAuthOptions => {
|
export const createDocumentAuthOptions = (options: TDocumentAuthOptions): TDocumentAuthOptions => {
|
||||||
return {
|
return {
|
||||||
globalAccessAuth: options?.globalAccessAuth ?? null,
|
globalAccessAuth: options?.globalAccessAuth ?? [],
|
||||||
globalActionAuth: options?.globalActionAuth ?? null,
|
globalActionAuth: options?.globalActionAuth ?? [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -66,7 +70,7 @@ export const createRecipientAuthOptions = (
|
|||||||
options: TRecipientAuthOptions,
|
options: TRecipientAuthOptions,
|
||||||
): TRecipientAuthOptions => {
|
): TRecipientAuthOptions => {
|
||||||
return {
|
return {
|
||||||
accessAuth: options?.accessAuth ?? null,
|
accessAuth: options?.accessAuth ?? [],
|
||||||
actionAuth: options?.actionAuth ?? null,
|
actionAuth: options?.actionAuth ?? [],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -21,18 +21,18 @@
|
|||||||
"seed": "tsx ./seed-database.ts"
|
"seed": "tsx ./seed-database.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^5.4.2",
|
"@prisma/client": "^6.8.2",
|
||||||
"kysely": "0.26.3",
|
"kysely": "0.26.3",
|
||||||
"prisma": "^5.4.2",
|
"prisma": "^6.8.2",
|
||||||
"prisma-extension-kysely": "^2.1.0",
|
"prisma-extension-kysely": "^3.0.0",
|
||||||
"prisma-kysely": "^1.8.0",
|
"prisma-kysely": "^1.8.0",
|
||||||
"prisma-json-types-generator": "^3.2.2",
|
"prisma-json-types-generator": "^3.2.2",
|
||||||
"ts-pattern": "^5.0.6",
|
"ts-pattern": "^5.0.6",
|
||||||
"zod-prisma-types": "3.1.9"
|
"zod-prisma-types": "3.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"dotenv": "^16.3.1",
|
"dotenv": "^16.5.0",
|
||||||
"dotenv-cli": "^7.3.0",
|
"dotenv-cli": "^8.0.0",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
"typescript": "5.6.2"
|
"typescript": "5.6.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,6 @@
|
|||||||
"ts-pattern": "^5.0.5"
|
"ts-pattern": "^5.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vitest": "^2.1.8"
|
"vitest": "^3.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -206,8 +206,8 @@ export const ZCreateDocumentV2RequestSchema = z.object({
|
|||||||
title: ZDocumentTitleSchema,
|
title: ZDocumentTitleSchema,
|
||||||
externalId: ZDocumentExternalIdSchema.optional(),
|
externalId: ZDocumentExternalIdSchema.optional(),
|
||||||
visibility: ZDocumentVisibilitySchema.optional(),
|
visibility: ZDocumentVisibilitySchema.optional(),
|
||||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.optional(),
|
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
||||||
globalActionAuth: ZDocumentActionAuthTypesSchema.optional(),
|
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
||||||
formValues: ZDocumentFormValuesSchema.optional(),
|
formValues: ZDocumentFormValuesSchema.optional(),
|
||||||
recipients: z
|
recipients: z
|
||||||
.array(
|
.array(
|
||||||
|
|||||||
@ -42,8 +42,8 @@ export const ZUpdateDocumentRequestSchema = z.object({
|
|||||||
title: ZDocumentTitleSchema.optional(),
|
title: ZDocumentTitleSchema.optional(),
|
||||||
externalId: ZDocumentExternalIdSchema.nullish(),
|
externalId: ZDocumentExternalIdSchema.nullish(),
|
||||||
visibility: ZDocumentVisibilitySchema.optional(),
|
visibility: ZDocumentVisibilitySchema.optional(),
|
||||||
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullish(),
|
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
|
||||||
globalActionAuth: ZDocumentActionAuthTypesSchema.nullish(),
|
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
|
||||||
useLegacyFieldInsertion: z.boolean().optional(),
|
useLegacyFieldInsertion: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.optional(),
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import { router } from '../trpc';
|
import { router } from '../trpc';
|
||||||
|
import { applyMultiSignSignatureRoute } from './apply-multi-sign-signature';
|
||||||
import { createEmbeddingDocumentRoute } from './create-embedding-document';
|
import { createEmbeddingDocumentRoute } from './create-embedding-document';
|
||||||
import { createEmbeddingPresignTokenRoute } from './create-embedding-presign-token';
|
import { createEmbeddingPresignTokenRoute } from './create-embedding-presign-token';
|
||||||
import { createEmbeddingTemplateRoute } from './create-embedding-template';
|
import { createEmbeddingTemplateRoute } from './create-embedding-template';
|
||||||
|
import { getMultiSignDocumentRoute } from './get-multi-sign-document';
|
||||||
import { updateEmbeddingDocumentRoute } from './update-embedding-document';
|
import { updateEmbeddingDocumentRoute } from './update-embedding-document';
|
||||||
import { updateEmbeddingTemplateRoute } from './update-embedding-template';
|
import { updateEmbeddingTemplateRoute } from './update-embedding-template';
|
||||||
import { verifyEmbeddingPresignTokenRoute } from './verify-embedding-presign-token';
|
import { verifyEmbeddingPresignTokenRoute } from './verify-embedding-presign-token';
|
||||||
@ -13,4 +15,6 @@ export const embeddingPresignRouter = router({
|
|||||||
createEmbeddingTemplate: createEmbeddingTemplateRoute,
|
createEmbeddingTemplate: createEmbeddingTemplateRoute,
|
||||||
updateEmbeddingDocument: updateEmbeddingDocumentRoute,
|
updateEmbeddingDocument: updateEmbeddingDocumentRoute,
|
||||||
updateEmbeddingTemplate: updateEmbeddingTemplateRoute,
|
updateEmbeddingTemplate: updateEmbeddingTemplateRoute,
|
||||||
|
applyMultiSignSignature: applyMultiSignSignatureRoute,
|
||||||
|
getMultiSignDocument: getMultiSignDocumentRoute,
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user