Compare commits

..

4 Commits

134 changed files with 2505 additions and 2729 deletions

View File

@ -29,6 +29,10 @@ NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
# URL used by the web app to request itself (e.g. local background jobs) # URL used by the web app to request itself (e.g. local background jobs)
NEXT_PRIVATE_INTERNAL_WEBAPP_URL="http://localhost:3000" NEXT_PRIVATE_INTERNAL_WEBAPP_URL="http://localhost:3000"
# [[SERVER]]
# OPTIONAL: The port the server will listen on. Defaults to 3000.
PORT=3000
# [[DATABASE]] # [[DATABASE]]
NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso" NEXT_PRIVATE_DATABASE_URL="postgres://documenso:password@127.0.0.1:54320/documenso"
# Defines the URL to use for the database when running migrations and other commands that won't work with a connection pool. # Defines the URL to use for the database when running migrations and other commands that won't work with a connection pool.

View File

@ -5,7 +5,7 @@
"scripts": { "scripts": {
"dev": "next dev -p 3003", "dev": "next dev -p 3003",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start -p 3003",
"lint:fix": "next lint --fix", "lint:fix": "next lint --fix",
"clean": "rimraf .next && rimraf node_modules" "clean": "rimraf .next && rimraf node_modules"
}, },

View File

@ -15,18 +15,16 @@ import {
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { InfoIcon } from 'lucide-react'; import { InfoIcon } from 'lucide-react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useNavigate } from 'react-router';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import * as z from 'zod'; import * as z from 'zod';
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation'; import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import type { TEnvelope } from '@documenso/lib/types/envelope'; import type { TEnvelope } from '@documenso/lib/types/envelope';
import { formatSigningLink } from '@documenso/lib/utils/recipients';
import { trpc, trpc as trpcReact } from '@documenso/trpc/react'; import { trpc, trpc as trpcReact } from '@documenso/trpc/react';
import { CopyTextButton } from '@documenso/ui/components/common/copy-text-button';
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper'; import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
import { cn } from '@documenso/ui/lib/utils';
import { Alert, AlertDescription } from '@documenso/ui/primitives/alert'; import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
import { AvatarWithText } from '@documenso/ui/primitives/avatar';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
Dialog, Dialog,
@ -65,6 +63,7 @@ export type EnvelopeDistributeDialogProps = {
fields: Pick<Field, 'type' | 'recipientId'>[]; fields: Pick<Field, 'type' | 'recipientId'>[];
}; };
onDistribute?: () => Promise<void>; onDistribute?: () => Promise<void>;
documentRootPath: string;
trigger?: React.ReactNode; trigger?: React.ReactNode;
}; };
@ -89,6 +88,7 @@ export type TEnvelopeDistributeFormSchema = z.infer<typeof ZEnvelopeDistributeFo
export const EnvelopeDistributeDialog = ({ export const EnvelopeDistributeDialog = ({
envelope, envelope,
trigger, trigger,
documentRootPath,
onDistribute, onDistribute,
}: EnvelopeDistributeDialogProps) => { }: EnvelopeDistributeDialogProps) => {
const organisation = useCurrentOrganisation(); const organisation = useCurrentOrganisation();
@ -97,6 +97,7 @@ export const EnvelopeDistributeDialog = ({
const { toast } = useToast(); const { toast } = useToast();
const { t } = useLingui(); const { t } = useLingui();
const navigate = useNavigate();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -163,6 +164,14 @@ export const EnvelopeDistributeDialog = ({
await onDistribute?.(); await onDistribute?.();
let redirectPath = `${documentRootPath}/${envelope.id}`;
if (meta.distributionMethod === DocumentDistributionMethod.NONE) {
redirectPath += '?action=copy-links';
}
await navigate(redirectPath);
toast({ toast({
title: t`Envelope distributed`, title: t`Envelope distributed`,
description: t`Your envelope has been distributed successfully.`, description: t`Your envelope has been distributed successfully.`,
@ -198,6 +207,7 @@ export const EnvelopeDistributeDialog = ({
<Trans>Recipients will be able to sign the document once sent</Trans> <Trans>Recipients will be able to sign the document once sent</Trans>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
{!invalidEnvelopeCode ? ( {!invalidEnvelopeCode ? (
<Form {...form}> <Form {...form}>
<form onSubmit={handleSubmit(onFormSubmit)}> <form onSubmit={handleSubmit(onFormSubmit)}>
@ -220,7 +230,11 @@ export const EnvelopeDistributeDialog = ({
</TabsList> </TabsList>
</Tabs> </Tabs>
<div className="min-h-72"> <div
className={cn('min-h-72', {
'min-h-[23rem]': organisation.organisationClaim.flags.emailDomains,
})}
>
<AnimatePresence initial={false} mode="wait"> <AnimatePresence initial={false} mode="wait">
{distributionMethod === DocumentDistributionMethod.EMAIL && ( {distributionMethod === DocumentDistributionMethod.EMAIL && (
<motion.div <motion.div
@ -355,73 +369,18 @@ export const EnvelopeDistributeDialog = ({
exit={{ opacity: 0, transition: { duration: 0.15 } }} exit={{ opacity: 0, transition: { duration: 0.15 } }}
className="min-h-60 rounded-lg border" className="min-h-60 rounded-lg border"
> >
{envelope.status === DocumentStatus.DRAFT ? ( <div className="text-muted-foreground py-24 text-center text-sm">
<div className="text-muted-foreground py-24 text-center text-sm"> <p>
<p> <Trans>We won't send anything to notify recipients.</Trans>
<Trans>We won't send anything to notify recipients.</Trans> </p>
</p>
<p className="mt-2"> <p className="mt-2">
<Trans> <Trans>
We will generate signing links for you, which you can send to the We will generate signing links for you, which you can send to the
recipients through your method of choice. recipients through your method of choice.
</Trans> </Trans>
</p> </p>
</div> </div>
) : (
<ul className="text-muted-foreground divide-y">
{/* Todo: Envelopes - I don't think this section shows up */}
{recipients.length === 0 && (
<li className="flex flex-col items-center justify-center py-6 text-sm">
<Trans>No recipients</Trans>
</li>
)}
{recipients.map((recipient) => (
<li
key={recipient.id}
className="flex items-center justify-between px-4 py-3 text-sm"
>
<AvatarWithText
avatarFallback={recipient.email.slice(0, 1).toUpperCase()}
primaryText={
<p className="text-muted-foreground text-sm">
{recipient.email}
</p>
}
secondaryText={
<p className="text-muted-foreground/70 text-xs">
{t(RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName)}
</p>
}
/>
{recipient.role !== RecipientRole.CC && (
<CopyTextButton
value={formatSigningLink(recipient.token)}
onCopySuccess={() => {
toast({
title: t`Copied to clipboard`,
description: t`The signing link has been copied to your clipboard.`,
});
}}
badgeContentUncopied={
<p className="ml-1 text-xs">
<Trans>Copy</Trans>
</p>
}
badgeContentCopied={
<p className="ml-1 text-xs">
<Trans>Copied</Trans>
</p>
}
/>
)}
</li>
))}
</ul>
)}
</motion.div> </motion.div>
)} )}
</AnimatePresence> </AnimatePresence>

View File

@ -213,8 +213,6 @@ export const EnvelopeDownloadDialog = ({
</div> </div>
)) ))
)} )}
{/* Todo: Envelopes - Download all button */}
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -7,9 +7,9 @@ import { FilePlus, Loader } from 'lucide-react';
import { useNavigate } from 'react-router'; import { useNavigate } from 'react-router';
import { useSession } from '@documenso/lib/client-only/providers/session'; import { useSession } from '@documenso/lib/client-only/providers/session';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { formatTemplatesPath } from '@documenso/lib/utils/teams'; import { formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TCreateTemplatePayloadSchema } from '@documenso/trpc/server/template-router/schema';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { import {
Dialog, Dialog,
@ -54,17 +54,13 @@ export const TemplateCreateDialog = ({ folderId }: TemplateCreateDialogProps) =>
setIsUploadingFile(true); setIsUploadingFile(true);
try { try {
const payload = { const response = await putPdfFile(file);
const { legacyTemplateId: id } = await createTemplate({
title: file.name, title: file.name,
templateDocumentDataId: response.id,
folderId: folderId, folderId: folderId,
} satisfies TCreateTemplatePayloadSchema; });
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append('file', file);
const { envelopeId: id } = await createTemplate(formData);
toast({ toast({
title: _(msg`Template document uploaded`), title: _(msg`Template document uploaded`),

View File

@ -1,6 +1,7 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Trans } from '@lingui/react/macro';
import { useForm, useWatch } from 'react-hook-form'; import { useForm, useWatch } from 'react-hook-form';
import type { z } from 'zod'; import type { z } from 'zod';
@ -60,7 +61,12 @@ export const EditorFieldSignatureForm = ({
<Form {...form}> <Form {...form}>
<form> <form>
<fieldset className="flex flex-col gap-2"> <fieldset className="flex flex-col gap-2">
<EditorGenericFontSizeField formControl={form.control} /> <div>
<EditorGenericFontSizeField formControl={form.control} />
<p className="text-muted-foreground mt-0.5 text-xs">
<Trans>The typed signature font size</Trans>
</p>
</div>
</fieldset> </fieldset>
</form> </form>
</Form> </Form>

View File

@ -8,11 +8,13 @@ import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitive
export type DocumentSigningAttachmentsPopoverProps = { export type DocumentSigningAttachmentsPopoverProps = {
envelopeId: string; envelopeId: string;
token: string; token: string;
trigger?: React.ReactNode;
}; };
export const DocumentSigningAttachmentsPopover = ({ export const DocumentSigningAttachmentsPopover = ({
envelopeId, envelopeId,
token, token,
trigger,
}: DocumentSigningAttachmentsPopoverProps) => { }: DocumentSigningAttachmentsPopoverProps) => {
const { data: attachments } = trpc.envelope.attachment.find.useQuery({ const { data: attachments } = trpc.envelope.attachment.find.useQuery({
envelopeId, envelopeId,
@ -26,15 +28,17 @@ export const DocumentSigningAttachmentsPopover = ({
return ( return (
<Popover> <Popover>
<PopoverTrigger asChild> <PopoverTrigger asChild>
<Button variant="outline" className="gap-2"> {trigger ?? (
<PaperclipIcon className="h-4 w-4" /> <Button variant="outline" className="gap-2">
<span> <PaperclipIcon className="h-4 w-4" />
<Trans>Attachments</Trans>{' '} <span>
{attachments && attachments.data.length > 0 && ( <Trans>Attachments</Trans>{' '}
<span className="ml-1">({attachments.data.length})</span> {attachments && attachments.data.length > 0 && (
)} <span className="ml-1">({attachments.data.length})</span>
</span> )}
</Button> </span>
</Button>
)}
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-96" align="start"> <PopoverContent className="w-96" align="start">

View File

@ -3,7 +3,7 @@ import { lazy, useMemo } from 'react';
import { Plural, Trans } from '@lingui/react/macro'; import { Plural, Trans } from '@lingui/react/macro';
import { EnvelopeType, RecipientRole } from '@prisma/client'; import { EnvelopeType, RecipientRole } from '@prisma/client';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { ArrowLeftIcon, BanIcon, DownloadCloudIcon } from 'lucide-react'; import { ArrowLeftIcon, BanIcon, DownloadCloudIcon, PaperclipIcon } from 'lucide-react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
@ -75,7 +75,7 @@ export const DocumentSigningPageViewV2 = () => {
<EnvelopeSignerHeader /> <EnvelopeSignerHeader />
{/* Main Content Area */} {/* Main Content Area */}
<div className="flex h-[calc(100vh-73px)] w-screen"> <div className="flex h-[calc(100vh-4rem)] w-screen">
{/* Left Section - Step Navigation */} {/* Left Section - Step Navigation */}
<div className="bg-background border-border hidden w-80 flex-shrink-0 flex-col overflow-y-auto border-r py-4 lg:flex"> <div className="bg-background border-border hidden w-80 flex-shrink-0 flex-col overflow-y-auto border-r py-4 lg:flex">
<div className="px-4"> <div className="px-4">
@ -121,12 +121,16 @@ export const DocumentSigningPageViewV2 = () => {
<Trans>Actions</Trans> <Trans>Actions</Trans>
</h4> </h4>
<div className="w-full"> <DocumentSigningAttachmentsPopover
<DocumentSigningAttachmentsPopover envelopeId={envelope.id}
envelopeId={envelope.id} token={recipient.token}
token={recipient.token} trigger={
/> <Button variant="ghost" size="sm" className="w-full justify-start">
</div> <PaperclipIcon className="mr-2 h-4 w-4" />
<Trans>Attachments</Trans>
</Button>
}
/>
<EnvelopeDownloadDialog <EnvelopeDownloadDialog
envelopeId={envelope.id} envelopeId={envelope.id}

View File

@ -8,6 +8,7 @@ import {
RecipientRole, RecipientRole,
SigningStatus, SigningStatus,
} from '@prisma/client'; } from '@prisma/client';
import { prop, sortBy } from 'remeda';
import { isBase64Image } from '@documenso/lib/constants/signatures'; import { isBase64Image } from '@documenso/lib/constants/signatures';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
@ -165,7 +166,29 @@ export const EnvelopeSigningProvider = ({
* The fields that are still required to be signed by the actual recipient. * The fields that are still required to be signed by the actual recipient.
*/ */
const recipientFieldsRemaining = useMemo(() => { const recipientFieldsRemaining = useMemo(() => {
return envelopeData.recipient.fields.filter((field) => isFieldUnsignedAndRequired(field)); const requiredFields = envelopeData.recipient.fields
.filter((field) => isFieldUnsignedAndRequired(field))
.map((field) => {
const envelopeItem = envelope.envelopeItems.find(
(item) => item.id === field.envelopeItemId,
);
if (!envelopeItem) {
throw new Error('Missing envelope item');
}
return {
...field,
envelopeItemOrder: envelopeItem.order,
};
});
return sortBy(
requiredFields,
[prop('envelopeItemOrder'), 'asc'],
[prop('page'), 'asc'],
[prop('positionY'), 'asc'],
);
}, [envelopeData.recipient.fields]); }, [envelopeData.recipient.fields]);
/** /**

View File

@ -4,7 +4,10 @@ import { Trans } from '@lingui/react/macro';
import type { DocumentData, EnvelopeItem } from '@prisma/client'; import type { DocumentData, EnvelopeItem } from '@prisma/client';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { EnvelopeRenderProvider } from '@documenso/lib/client-only/providers/envelope-render-provider'; import {
EnvelopeRenderProvider,
useCurrentEnvelopeRender,
} from '@documenso/lib/client-only/providers/envelope-render-provider';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy'; import PDFViewerKonvaLazy from '@documenso/ui/components/pdf-viewer/pdf-viewer-konva-lazy';
@ -92,6 +95,60 @@ export const DocumentCertificateQRView = ({
</Dialog> </Dialog>
)} )}
{internalVersion === 2 ? (
<EnvelopeRenderProvider envelope={{ envelopeItems }}>
<DocumentCertificateQrV2
title={title}
recipientCount={recipientCount}
formattedDate={formattedDate}
/>
</EnvelopeRenderProvider>
) : (
<>
<div className="flex w-full flex-col justify-between gap-4 md:flex-row md:items-end">
<div className="space-y-1">
<h1 className="text-xl font-medium">{title}</h1>
<div className="text-muted-foreground flex flex-col gap-0.5 text-sm">
<p>
<Trans>{recipientCount} recipients</Trans>
</p>
<p>
<Trans>Completed on {formattedDate}</Trans>
</p>
</div>
</div>
<ShareDocumentDownloadButton
title={title}
documentData={envelopeItems[0].documentData}
/>
</div>
<div className="mt-12 w-full">
<PDFViewer key={envelopeItems[0].id} documentData={envelopeItems[0].documentData} />
</div>
</>
)}
</div>
);
};
type DocumentCertificateQrV2Props = {
title: string;
recipientCount: number;
formattedDate: string;
};
const DocumentCertificateQrV2 = ({
title,
recipientCount,
formattedDate,
}: DocumentCertificateQrV2Props) => {
const { currentEnvelopeItem } = useCurrentEnvelopeRender();
return (
<div className="flex min-h-screen flex-col items-start">
<div className="flex w-full flex-col justify-between gap-4 md:flex-row md:items-end"> <div className="flex w-full flex-col justify-between gap-4 md:flex-row md:items-end">
<div className="space-y-1"> <div className="space-y-1">
<h1 className="text-xl font-medium">{title}</h1> <h1 className="text-xl font-medium">{title}</h1>
@ -106,21 +163,18 @@ export const DocumentCertificateQRView = ({
</div> </div>
</div> </div>
<ShareDocumentDownloadButton title={title} documentData={envelopeItems[0].documentData} /> {currentEnvelopeItem && (
<ShareDocumentDownloadButton
title={title}
documentData={currentEnvelopeItem.documentData}
/>
)}
</div> </div>
<div className="mt-12 w-full"> <div className="mt-12 w-full">
{internalVersion === 2 ? ( <EnvelopeRendererFileSelector className="mb-4 p-0" fields={[]} secondaryOverride={''} />
<EnvelopeRenderProvider envelope={{ envelopeItems }}>
<EnvelopeRendererFileSelector className="mb-4 p-0" fields={[]} secondaryOverride={''} />
<PDFViewerKonvaLazy customPageRenderer={EnvelopeGenericPageRenderer} /> <PDFViewerKonvaLazy customPageRenderer={EnvelopeGenericPageRenderer} />
</EnvelopeRenderProvider>
) : (
<>
<PDFViewer key={envelopeItems[0].id} documentData={envelopeItems[0].documentData} />
</>
)}
</div> </div>
</div> </div>
); );

View File

@ -16,9 +16,9 @@ import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/l
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions'; import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
@ -62,18 +62,14 @@ export const DocumentDropZoneWrapper = ({ children, className }: DocumentDropZon
try { try {
setIsLoading(true); setIsLoading(true);
const payload = { const response = await putPdfFile(file);
const { legacyDocumentId: id } = await createDocument({
title: file.name, title: file.name,
timezone: userTimezone, documentDataId: response.id,
timezone: userTimezone, // Note: When migrating to v2 document upload remember to pass this through as a 'userTimezone' field.
folderId: folderId ?? undefined, folderId: folderId ?? undefined,
} satisfies TCreateDocumentPayloadSchema; });
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append('file', file);
const { envelopeId: id } = await createDocument(formData);
void refreshLimits(); void refreshLimits();

View File

@ -1,7 +1,10 @@
import { useEffect, useState } from 'react';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client'; import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
import { TooltipArrow } from '@radix-ui/react-tooltip';
import { import {
AlertTriangle, AlertTriangle,
CheckIcon, CheckIcon,
@ -12,7 +15,7 @@ import {
PlusIcon, PlusIcon,
UserIcon, UserIcon,
} from 'lucide-react'; } from 'lucide-react';
import { Link } from 'react-router'; import { Link, useSearchParams } from 'react-router';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles'; import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
@ -24,6 +27,12 @@ import { SignatureIcon } from '@documenso/ui/icons/signature';
import { AvatarWithText } from '@documenso/ui/primitives/avatar'; import { AvatarWithText } from '@documenso/ui/primitives/avatar';
import { Badge } from '@documenso/ui/primitives/badge'; import { Badge } from '@documenso/ui/primitives/badge';
import { PopoverHover } from '@documenso/ui/primitives/popover'; import { PopoverHover } from '@documenso/ui/primitives/popover';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@documenso/ui/primitives/tooltip';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
export type DocumentPageViewRecipientsProps = { export type DocumentPageViewRecipientsProps = {
@ -37,8 +46,24 @@ export const DocumentPageViewRecipients = ({
}: DocumentPageViewRecipientsProps) => { }: DocumentPageViewRecipientsProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const [searchParams, setSearchParams] = useSearchParams();
const recipients = envelope.recipients; const recipients = envelope.recipients;
const [shouldHighlightCopyButtons, setShouldHighlightCopyButtons] = useState(false);
// Check for action=view-tokens query parameter and set highlighting state
useEffect(() => {
const hasViewTokensAction = searchParams.get('action') === 'copy-links';
if (hasViewTokensAction) {
setShouldHighlightCopyButtons(true);
// Remove the query parameter immediately
const params = new URLSearchParams(searchParams);
params.delete('action');
setSearchParams(params);
}
}, [searchParams, setSearchParams]);
return ( return (
<section className="dark:bg-background border-border bg-widget flex flex-col rounded-xl border"> <section className="dark:bg-background border-border bg-widget flex flex-col rounded-xl border">
@ -69,7 +94,7 @@ export const DocumentPageViewRecipients = ({
</li> </li>
)} )}
{recipients.map((recipient) => ( {recipients.map((recipient, i) => (
<li key={recipient.id} className="flex items-center justify-between px-4 py-2.5 text-sm"> <li key={recipient.id} className="flex items-center justify-between px-4 py-2.5 text-sm">
<AvatarWithText <AvatarWithText
avatarFallback={recipient.email.slice(0, 1).toUpperCase()} avatarFallback={recipient.email.slice(0, 1).toUpperCase()}
@ -159,15 +184,33 @@ export const DocumentPageViewRecipients = ({
{envelope.status === DocumentStatus.PENDING && {envelope.status === DocumentStatus.PENDING &&
recipient.signingStatus === SigningStatus.NOT_SIGNED && recipient.signingStatus === SigningStatus.NOT_SIGNED &&
recipient.role !== RecipientRole.CC && ( recipient.role !== RecipientRole.CC && (
<CopyTextButton <TooltipProvider>
value={formatSigningLink(recipient.token)} <Tooltip open={shouldHighlightCopyButtons && i === 0}>
onCopySuccess={() => { <TooltipTrigger asChild>
toast({ <div
title: _(msg`Copied to clipboard`), className={shouldHighlightCopyButtons ? 'animate-pulse' : ''}
description: _(msg`The signing link has been copied to your clipboard.`), onClick={() => setShouldHighlightCopyButtons(false)}
}); >
}} <CopyTextButton
/> value={formatSigningLink(recipient.token)}
onCopySuccess={() => {
toast({
title: _(msg`Copied to clipboard`),
description: _(
msg`The signing link has been copied to your clipboard.`,
),
});
setShouldHighlightCopyButtons(false);
}}
/>
</div>
</TooltipTrigger>
<TooltipContent sideOffset={2}>
<Trans>Copy Signing Links</Trans>
<TooltipArrow className="fill-background" />
</TooltipContent>
</Tooltip>
</TooltipProvider>
)} )}
</div> </div>
</li> </li>

View File

@ -13,9 +13,9 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TCreateDocumentPayloadSchema } from '@documenso/trpc/server/document-router/create-document.types';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { DocumentDropzone } from '@documenso/ui/primitives/document-upload'; import { DocumentDropzone } from '@documenso/ui/primitives/document-upload';
import { import {
@ -73,18 +73,14 @@ export const DocumentUploadButton = ({ className }: DocumentUploadButtonProps) =
try { try {
setIsLoading(true); setIsLoading(true);
const payload = { const response = await putPdfFile(file);
const { legacyDocumentId: id } = await createDocument({
title: file.name, title: file.name,
documentDataId: response.id,
timezone: userTimezone, timezone: userTimezone,
folderId: folderId ?? undefined, folderId: folderId ?? undefined,
} satisfies TCreateDocumentPayloadSchema; });
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append('file', file);
const { envelopeId: id } = await createDocument(formData);
void refreshLimits(); void refreshLimits();

View File

@ -14,9 +14,9 @@ import { useSession } from '@documenso/lib/client-only/providers/session';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { TIME_ZONES } from '@documenso/lib/constants/time-zones'; import { TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TCreateEnvelopePayload } from '@documenso/trpc/server/envelope-router/create-envelope.types';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { DocumentDropzone } from '@documenso/ui/primitives/document-upload'; import { DocumentDropzone } from '@documenso/ui/primitives/document-upload';
import { import {
@ -78,24 +78,35 @@ export const EnvelopeUploadButton = ({ className, type, folderId }: EnvelopeUplo
try { try {
setIsLoading(true); setIsLoading(true);
const payload = { const result = await Promise.all(
files.map(async (file) => {
try {
const response = await putPdfFile(file);
return {
title: file.name,
documentDataId: response.id,
};
} catch (err) {
console.error(err);
throw new Error('Failed to upload document');
}
}),
);
const envelopeItemsToCreate = result.filter(
(item): item is { title: string; documentDataId: string } => item !== undefined,
);
const { id } = await createEnvelope({
folderId, folderId,
type, type,
title: files[0].name, title: files[0].name,
items: envelopeItemsToCreate,
meta: { meta: {
timezone: userTimezone, timezone: userTimezone,
}, },
} satisfies TCreateEnvelopePayload; }).catch((error) => {
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
for (const file of files) {
formData.append('files', file);
}
const { id } = await createEnvelope(formData).catch((error) => {
console.error(error); console.error(error);
throw error; throw error;

View File

@ -57,8 +57,6 @@ export default function EnvelopeEditorFieldsPageRenderer() {
); );
const handleResizeOrMove = (event: KonvaEventObject<Event>) => { const handleResizeOrMove = (event: KonvaEventObject<Event>) => {
console.log('Field resized or moved');
const { current: container } = canvasElement; const { current: container } = canvasElement;
if (!container) { if (!container) {
@ -273,9 +271,6 @@ export default function EnvelopeEditorFieldsPageRenderer() {
return; return;
} }
console.log(`pointerPosition.x: ${pointerPosition.x}`);
console.log(`pointerPosition.y: ${pointerPosition.y}`);
x1 = pointerPosition.x / scale; x1 = pointerPosition.x / scale;
y1 = pointerPosition.y / scale; y1 = pointerPosition.y / scale;
x2 = pointerPosition.x / scale; x2 = pointerPosition.x / scale;

View File

@ -5,6 +5,7 @@ import { msg } from '@lingui/core/macro';
import { Trans, useLingui } from '@lingui/react/macro'; import { Trans, useLingui } from '@lingui/react/macro';
import { FieldType, RecipientRole } from '@prisma/client'; import { FieldType, RecipientRole } from '@prisma/client';
import { FileTextIcon } from 'lucide-react'; import { FileTextIcon } from 'lucide-react';
import { Link } from 'react-router';
import { isDeepEqual } from 'remeda'; import { isDeepEqual } from 'remeda';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
@ -20,6 +21,7 @@ import type {
TNameFieldMeta, TNameFieldMeta,
TNumberFieldMeta, TNumberFieldMeta,
TRadioFieldMeta, TRadioFieldMeta,
TSignatureFieldMeta,
TTextFieldMeta, TTextFieldMeta,
} from '@documenso/lib/types/field-meta'; } from '@documenso/lib/types/field-meta';
import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients'; import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
@ -37,6 +39,7 @@ import { EditorFieldInitialsForm } from '~/components/forms/editor/editor-field-
import { EditorFieldNameForm } from '~/components/forms/editor/editor-field-name-form'; import { EditorFieldNameForm } from '~/components/forms/editor/editor-field-name-form';
import { EditorFieldNumberForm } from '~/components/forms/editor/editor-field-number-form'; import { EditorFieldNumberForm } from '~/components/forms/editor/editor-field-number-form';
import { EditorFieldRadioForm } from '~/components/forms/editor/editor-field-radio-form'; import { EditorFieldRadioForm } from '~/components/forms/editor/editor-field-radio-form';
import { EditorFieldSignatureForm } from '~/components/forms/editor/editor-field-signature-form';
import { EditorFieldTextForm } from '~/components/forms/editor/editor-field-text-form'; import { EditorFieldTextForm } from '~/components/forms/editor/editor-field-text-form';
import { EnvelopeEditorFieldDragDrop } from './envelope-editor-fields-drag-drop'; import { EnvelopeEditorFieldDragDrop } from './envelope-editor-fields-drag-drop';
@ -61,7 +64,7 @@ const FieldSettingsTypeTranslations: Record<FieldType, MessageDescriptor> = {
}; };
export const EnvelopeEditorFieldsPage = () => { export const EnvelopeEditorFieldsPage = () => {
const { envelope, editorFields } = useCurrentEnvelopeEditor(); const { envelope, editorFields, relativePath } = useCurrentEnvelopeEditor();
const { currentEnvelopeItem } = useCurrentEnvelopeRender(); const { currentEnvelopeItem } = useCurrentEnvelopeRender();
@ -104,12 +107,12 @@ export const EnvelopeEditorFieldsPage = () => {
return ( return (
<div className="relative flex h-full"> <div className="relative flex h-full">
<div className="flex w-full flex-col"> <div className="flex w-full flex-col overflow-y-auto">
{/* Horizontal envelope item selector */} {/* Horizontal envelope item selector */}
<EnvelopeRendererFileSelector fields={editorFields.localFields} /> <EnvelopeRendererFileSelector fields={editorFields.localFields} />
{/* Document View */} {/* Document View */}
<div className="mt-4 flex justify-center p-4"> <div className="mt-4 flex h-full justify-center p-4">
{currentEnvelopeItem !== null ? ( {currentEnvelopeItem !== null ? (
<PDFViewerKonvaLazy customPageRenderer={EnvelopeEditorFieldsPageRenderer} /> <PDFViewerKonvaLazy customPageRenderer={EnvelopeEditorFieldsPageRenderer} />
) : ( ) : (
@ -128,7 +131,7 @@ export const EnvelopeEditorFieldsPage = () => {
{/* Right Section - Form Fields Panel */} {/* Right Section - Form Fields Panel */}
{currentEnvelopeItem && ( {currentEnvelopeItem && (
<div className="bg-background border-border sticky top-0 h-[calc(100vh-73px)] w-80 flex-shrink-0 overflow-y-auto border-l py-4"> <div className="bg-background border-border sticky top-0 h-full w-80 flex-shrink-0 overflow-y-auto border-l py-4">
{/* Recipient selector section. */} {/* Recipient selector section. */}
<section className="px-4"> <section className="px-4">
<h3 className="text-foreground mb-2 text-sm font-semibold"> <h3 className="text-foreground mb-2 text-sm font-semibold">
@ -137,8 +140,14 @@ export const EnvelopeEditorFieldsPage = () => {
{envelope.recipients.length === 0 ? ( {envelope.recipients.length === 0 ? (
<Alert variant="warning"> <Alert variant="warning">
<AlertDescription> <AlertDescription className="flex flex-col gap-2">
<Trans>You need at least one recipient to add fields</Trans> <Trans>You need at least one recipient to add fields</Trans>
<Link to={`${relativePath.editorPath}`} className="text-sm">
<p>
<Trans>Click here to add a recipient</Trans>
</p>
</Link>
</AlertDescription> </AlertDescription>
</Alert> </Alert>
) : ( ) : (
@ -182,7 +191,7 @@ export const EnvelopeEditorFieldsPage = () => {
{/* Field details section. */} {/* Field details section. */}
<AnimateGenericFadeInOut key={editorFields.selectedField?.formId}> <AnimateGenericFadeInOut key={editorFields.selectedField?.formId}>
{selectedField && selectedField.type !== FieldType.SIGNATURE && ( {selectedField && (
<section> <section>
<Separator className="my-4" /> <Separator className="my-4" />
@ -192,6 +201,12 @@ export const EnvelopeEditorFieldsPage = () => {
</h3> </h3>
{match(selectedField.type) {match(selectedField.type)
.with(FieldType.SIGNATURE, () => (
<EditorFieldSignatureForm
value={selectedField?.fieldMeta as TSignatureFieldMeta | undefined}
onValueChange={(value) => updateSelectedFieldMeta(value)}
/>
))
.with(FieldType.CHECKBOX, () => ( .with(FieldType.CHECKBOX, () => (
<EditorFieldCheckboxForm <EditorFieldCheckboxForm
value={selectedField?.fieldMeta as TCheckboxFieldMeta | undefined} value={selectedField?.fieldMeta as TCheckboxFieldMeta | undefined}

View File

@ -37,7 +37,6 @@ export default function EnvelopeEditorHeader() {
updateEnvelope, updateEnvelope,
autosaveError, autosaveError,
relativePath, relativePath,
syncEnvelope,
editorFields, editorFields,
} = useCurrentEnvelopeEditor(); } = useCurrentEnvelopeEditor();
@ -152,7 +151,7 @@ export default function EnvelopeEditorHeader() {
...envelope, ...envelope,
fields: editorFields.localFields, fields: editorFields.localFields,
}} }}
onDistribute={syncEnvelope} documentRootPath={relativePath.documentRootPath}
trigger={ trigger={
<Button size="sm"> <Button size="sm">
<SendIcon className="mr-2 h-4 w-4" /> <SendIcon className="mr-2 h-4 w-4" />

View File

@ -33,7 +33,7 @@ export const EnvelopeEditorPreviewPage = () => {
return ( return (
<div className="relative flex h-full"> <div className="relative flex h-full">
<div className="flex w-full flex-col"> <div className="flex w-full flex-col overflow-y-auto">
{/* Horizontal envelope item selector */} {/* Horizontal envelope item selector */}
<EnvelopeRendererFileSelector fields={editorFields.localFields} /> <EnvelopeRendererFileSelector fields={editorFields.localFields} />
@ -82,7 +82,7 @@ export const EnvelopeEditorPreviewPage = () => {
{/* Right Section - Form Fields Panel */} {/* Right Section - Form Fields Panel */}
{currentEnvelopeItem && false && ( {currentEnvelopeItem && false && (
<div className="sticky top-0 h-[calc(100vh-73px)] w-80 flex-shrink-0 overflow-y-auto border-l border-gray-200 bg-white py-4"> <div className="sticky top-0 h-full w-80 flex-shrink-0 overflow-y-auto border-l border-gray-200 bg-white py-4">
{/* Add fields section. */} {/* Add fields section. */}
<section className="px-4"> <section className="px-4">
{/* <h3 className="mb-2 text-sm font-semibold text-gray-900"> {/* <h3 className="mb-2 text-sm font-semibold text-gray-900">

View File

@ -14,7 +14,7 @@ import { DocumentSigningOrder, EnvelopeType, RecipientRole, SendStatus } from '@
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { GripVerticalIcon, HelpCircleIcon, PlusIcon, TrashIcon } from 'lucide-react'; import { GripVerticalIcon, HelpCircleIcon, PlusIcon, TrashIcon } from 'lucide-react';
import { useFieldArray, useForm, useWatch } from 'react-hook-form'; import { useFieldArray, useForm, useWatch } from 'react-hook-form';
import { prop, sortBy } from 'remeda'; import { isDeepEqual, prop, sortBy } from 'remeda';
import { z } from 'zod'; import { z } from 'zod';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client'; import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
@ -148,8 +148,7 @@ export const EnvelopeEditorRecipientForm = () => {
}, },
}); });
// Always show advanced settings if any recipient has auth options. const recipientHasAuthSettings = useMemo(() => {
const alwaysShowAdvancedSettings = useMemo(() => {
const recipientHasAuthOptions = recipients.find((recipient) => { const recipientHasAuthOptions = recipients.find((recipient) => {
const recipientAuthOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions); const recipientAuthOptions = ZRecipientAuthOptionsSchema.parse(recipient.authOptions);
@ -165,7 +164,7 @@ export const EnvelopeEditorRecipientForm = () => {
return recipientHasAuthOptions !== undefined || formHasActionAuth !== undefined; return recipientHasAuthOptions !== undefined || formHasActionAuth !== undefined;
}, [recipients, form]); }, [recipients, form]);
const [showAdvancedSettings, setShowAdvancedSettings] = useState(alwaysShowAdvancedSettings); const [showAdvancedSettings, setShowAdvancedSettings] = useState(recipientHasAuthSettings);
const [showSigningOrderConfirmation, setShowSigningOrderConfirmation] = useState(false); const [showSigningOrderConfirmation, setShowSigningOrderConfirmation] = useState(false);
const { const {
@ -464,7 +463,7 @@ export const EnvelopeEditorRecipientForm = () => {
const formValueSigners = formValues.signers || []; const formValueSigners = formValues.signers || [];
// Remove the last signer if it's empty. // Remove the last signer if it's empty.
const recipients = formValueSigners.filter((signer, i) => { const nonEmptyRecipients = formValueSigners.filter((signer, i) => {
if (i === formValueSigners.length - 1 && signer.email === '') { if (i === formValueSigners.length - 1 && signer.email === '') {
return false; return false;
} }
@ -474,26 +473,48 @@ export const EnvelopeEditorRecipientForm = () => {
const validatedFormValues = ZEnvelopeRecipientsForm.safeParse({ const validatedFormValues = ZEnvelopeRecipientsForm.safeParse({
...formValues, ...formValues,
signers: recipients, signers: nonEmptyRecipients,
}); });
if (validatedFormValues.success) { if (!validatedFormValues.success) {
console.log('validatedFormValues', validatedFormValues); return;
}
const { data } = validatedFormValues;
const hasSigningOrderChanged = envelope.documentMeta.signingOrder !== data.signingOrder;
const hasAllowDictateNextSignerChanged =
envelope.documentMeta.allowDictateNextSigner !== data.allowDictateNextSigner;
const hasSignersChanged =
data.signers.length !== recipients.length ||
data.signers.some((signer) => {
const recipient = recipients.find((recipient) => recipient.id === signer.id);
if (!recipient) {
return true;
}
return (
signer.email !== recipient.email ||
signer.name !== recipient.name ||
signer.role !== recipient.role ||
signer.signingOrder !== recipient.signingOrder ||
!isDeepEqual(signer.actionAuth, recipient.authOptions?.actionAuth)
);
});
if (hasSignersChanged) {
setRecipientsDebounced(validatedFormValues.data.signers); setRecipientsDebounced(validatedFormValues.data.signers);
}
if ( if (hasSigningOrderChanged || hasAllowDictateNextSignerChanged) {
validatedFormValues.data.signingOrder !== envelope.documentMeta.signingOrder || updateEnvelope({
validatedFormValues.data.allowDictateNextSigner !== meta: {
envelope.documentMeta.allowDictateNextSigner signingOrder: validatedFormValues.data.signingOrder,
) { allowDictateNextSigner: validatedFormValues.data.allowDictateNextSigner,
updateEnvelope({ },
meta: { });
signingOrder: validatedFormValues.data.signingOrder,
allowDictateNextSigner: validatedFormValues.data.allowDictateNextSigner,
},
});
}
} }
}, [formValues]); }, [formValues]);
@ -534,17 +555,16 @@ export const EnvelopeEditorRecipientForm = () => {
<AnimateGenericFadeInOut motionKey={showAdvancedSettings ? 'Show' : 'Hide'}> <AnimateGenericFadeInOut motionKey={showAdvancedSettings ? 'Show' : 'Hide'}>
<Form {...form}> <Form {...form}>
<div className="bg-accent/50 -mt-2 mb-2 space-y-4 rounded-md p-4"> <div className="bg-accent/50 -mt-2 mb-2 space-y-4 rounded-md p-4">
{!alwaysShowAdvancedSettings && organisation.organisationClaim.flags.cfr21 && ( {organisation.organisationClaim.flags.cfr21 && (
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
<Checkbox <Checkbox
id="showAdvancedRecipientSettings" id="showAdvancedRecipientSettings"
className="h-5 w-5"
checked={showAdvancedSettings} checked={showAdvancedSettings}
onCheckedChange={(value) => setShowAdvancedSettings(Boolean(value))} onCheckedChange={(value) => setShowAdvancedSettings(Boolean(value))}
/> />
<label <label
className="text-muted-foreground ml-2 text-sm" className="ml-2 text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
htmlFor="showAdvancedRecipientSettings" htmlFor="showAdvancedRecipientSettings"
> >
<Trans>Show advanced settings</Trans> <Trans>Show advanced settings</Trans>
@ -703,171 +723,48 @@ export const EnvelopeEditorRecipientForm = () => {
<motion.fieldset <motion.fieldset
data-native-id={signer.id} data-native-id={signer.id}
disabled={isSubmitting || !canRecipientBeModified(signer.id)} disabled={isSubmitting || !canRecipientBeModified(signer.id)}
className={cn('grid grid-cols-10 items-end gap-2 pb-2', { className={cn('pb-2', {
'border-b pt-2': showAdvancedSettings, 'border-b pb-4':
'grid-cols-12 pr-3': isSigningOrderSequential, showAdvancedSettings && index !== signers.length - 1,
'pt-2': showAdvancedSettings && index === 0,
'pr-3': isSigningOrderSequential,
})} })}
> >
{isSigningOrderSequential && ( <div className="flex flex-row items-center gap-x-2">
<FormField {isSigningOrderSequential && (
control={form.control}
name={`signers.${index}.signingOrder`}
render={({ field }) => (
<FormItem
className={cn(
'col-span-1 mt-auto flex items-center gap-x-1 space-y-0',
{
'mb-6':
form.formState.errors.signers?.[index] &&
!form.formState.errors.signers[index]?.signingOrder,
},
)}
>
<GripVerticalIcon className="h-5 w-5 flex-shrink-0 opacity-40" />
<FormControl>
<Input
type="number"
max={signers.length}
data-testid="signing-order-input"
className={cn(
'w-full text-center',
'[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none',
)}
{...field}
onChange={(e) => {
field.onChange(e);
handleSigningOrderChange(index, e.target.value);
}}
onBlur={(e) => {
field.onBlur();
handleSigningOrderChange(index, e.target.value);
}}
disabled={
snapshot.isDragging ||
isSubmitting ||
!canRecipientBeModified(signer.id)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name={`signers.${index}.email`}
render={({ field }) => (
<FormItem
className={cn('relative', {
'mb-6':
form.formState.errors.signers?.[index] &&
!form.formState.errors.signers[index]?.email,
'col-span-4': !showAdvancedSettings,
'col-span-5': showAdvancedSettings,
})}
>
{!showAdvancedSettings && index === 0 && (
<FormLabel required>
<Trans>Email</Trans>
</FormLabel>
)}
<FormControl>
<RecipientAutoCompleteInput
type="email"
placeholder={t`Email`}
value={field.value}
disabled={
snapshot.isDragging ||
isSubmitting ||
!canRecipientBeModified(signer.id)
}
options={recipientSuggestions}
onSelect={(suggestion) =>
handleRecipientAutoCompleteSelect(index, suggestion)
}
onSearchQueryChange={(query) => {
field.onChange(query);
setRecipientSearchQuery(query);
}}
loading={isLoading}
data-testid="signer-email-input"
maxLength={254}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`signers.${index}.name`}
render={({ field }) => (
<FormItem
className={cn({
'mb-6':
form.formState.errors.signers?.[index] &&
!form.formState.errors.signers[index]?.name,
'col-span-4': !showAdvancedSettings,
'col-span-5': showAdvancedSettings,
})}
>
{!showAdvancedSettings && index === 0 && (
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
)}
<FormControl>
<RecipientAutoCompleteInput
type="text"
placeholder={t`Name`}
{...field}
disabled={
snapshot.isDragging ||
isSubmitting ||
!canRecipientBeModified(signer.id)
}
options={recipientSuggestions}
onSelect={(suggestion) =>
handleRecipientAutoCompleteSelect(index, suggestion)
}
onSearchQueryChange={(query) => {
field.onChange(query);
setRecipientSearchQuery(query);
}}
loading={isLoading}
maxLength={255}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{showAdvancedSettings &&
organisation.organisationClaim.flags.cfr21 && (
<FormField <FormField
control={form.control} control={form.control}
name={`signers.${index}.actionAuth`} name={`signers.${index}.signingOrder`}
render={({ field }) => ( render={({ field }) => (
<FormItem <FormItem
className={cn('col-span-8', { className={cn(
'mb-6': 'mt-auto flex items-center gap-x-1 space-y-0',
form.formState.errors.signers?.[index] && {
!form.formState.errors.signers[index]?.actionAuth, 'mb-6':
'col-span-10': isSigningOrderSequential, form.formState.errors.signers?.[index] &&
})} !form.formState.errors.signers[index]?.signingOrder,
},
)}
> >
<GripVerticalIcon className="h-5 w-5 flex-shrink-0 opacity-40" />
<FormControl> <FormControl>
<RecipientActionAuthSelect <Input
type="number"
max={signers.length}
data-testid="signing-order-input"
className={cn(
'w-10 text-center',
'[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none',
)}
{...field} {...field}
onValueChange={field.onChange} onChange={(e) => {
field.onChange(e);
handleSigningOrderChange(index, e.target.value);
}}
onBlur={(e) => {
field.onBlur();
handleSigningOrderChange(index, e.target.value);
}}
disabled={ disabled={
snapshot.isDragging || snapshot.isDragging ||
isSubmitting || isSubmitting ||
@ -875,20 +772,109 @@ export const EnvelopeEditorRecipientForm = () => {
} }
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
)} )}
<div className="col-span-2 flex gap-x-2"> <FormField
control={form.control}
name={`signers.${index}.email`}
render={({ field }) => (
<FormItem
className={cn('relative w-full', {
'mb-6':
form.formState.errors.signers?.[index] &&
!form.formState.errors.signers[index]?.email,
})}
>
{!showAdvancedSettings && index === 0 && (
<FormLabel required>
<Trans>Email</Trans>
</FormLabel>
)}
<FormControl>
<RecipientAutoCompleteInput
type="email"
placeholder={t`Email`}
value={field.value}
disabled={
snapshot.isDragging ||
isSubmitting ||
!canRecipientBeModified(signer.id)
}
options={recipientSuggestions}
onSelect={(suggestion) =>
handleRecipientAutoCompleteSelect(index, suggestion)
}
onSearchQueryChange={(query) => {
field.onChange(query);
setRecipientSearchQuery(query);
}}
loading={isLoading}
data-testid="signer-email-input"
maxLength={254}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`signers.${index}.name`}
render={({ field }) => (
<FormItem
className={cn('w-full', {
'mb-6':
form.formState.errors.signers?.[index] &&
!form.formState.errors.signers[index]?.name,
})}
>
{!showAdvancedSettings && index === 0 && (
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
)}
<FormControl>
<RecipientAutoCompleteInput
type="text"
placeholder={t`Name`}
{...field}
disabled={
snapshot.isDragging ||
isSubmitting ||
!canRecipientBeModified(signer.id)
}
options={recipientSuggestions}
onSelect={(suggestion) =>
handleRecipientAutoCompleteSelect(index, suggestion)
}
onSearchQueryChange={(query) => {
field.onChange(query);
setRecipientSearchQuery(query);
}}
loading={isLoading}
maxLength={255}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField <FormField
control={form.control} control={form.control}
name={`signers.${index}.role`} name={`signers.${index}.role`}
render={({ field }) => ( render={({ field }) => (
<FormItem <FormItem
className={cn('mt-auto', { className={cn('mt-auto w-fit', {
'mb-6': 'mb-6':
form.formState.errors.signers?.[index] && form.formState.errors.signers?.[index] &&
!form.formState.errors.signers[index]?.role, !form.formState.errors.signers[index]?.role,
@ -916,14 +902,11 @@ export const EnvelopeEditorRecipientForm = () => {
)} )}
/> />
<button <Button
type="button" variant="ghost"
className={cn( className={cn('mt-auto px-2', {
'mt-auto inline-flex h-10 w-10 items-center justify-center hover:opacity-80 disabled:cursor-not-allowed disabled:opacity-50', 'mb-6': form.formState.errors.signers?.[index],
{ })}
'mb-6': form.formState.errors.signers?.[index],
},
)}
data-testid="remove-signer-button" data-testid="remove-signer-button"
disabled={ disabled={
snapshot.isDragging || snapshot.isDragging ||
@ -934,8 +917,40 @@ export const EnvelopeEditorRecipientForm = () => {
onClick={() => onRemoveSigner(index)} onClick={() => onRemoveSigner(index)}
> >
<TrashIcon className="h-4 w-4" /> <TrashIcon className="h-4 w-4" />
</button> </Button>
</div> </div>
{showAdvancedSettings &&
organisation.organisationClaim.flags.cfr21 && (
<FormField
control={form.control}
name={`signers.${index}.actionAuth`}
render={({ field }) => (
<FormItem
className={cn('mt-2 w-full', {
'mb-6':
form.formState.errors.signers?.[index] &&
!form.formState.errors.signers[index]?.actionAuth,
'pl-6': isSigningOrderSequential,
})}
>
<FormControl>
<RecipientActionAuthSelect
{...field}
onValueChange={field.onChange}
disabled={
snapshot.isDragging ||
isSubmitting ||
!canRecipientBeModified(signer.id)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
</motion.fieldset> </motion.fieldset>
</div> </div>
)} )}

View File

@ -355,7 +355,7 @@ export const EnvelopeEditorSettingsDialog = ({
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onFormSubmit)}> <form onSubmit={form.handleSubmit(onFormSubmit)}>
<fieldset <fieldset
className="flex min-h-[45rem] w-full flex-col space-y-6 px-6 pt-6" className="flex h-[45rem] max-h-[calc(100vh-14rem)] w-full flex-col space-y-6 overflow-y-auto px-6 pt-6"
disabled={form.formState.isSubmitting} disabled={form.formState.isSubmitting}
key={activeTab} key={activeTab}
> >

View File

@ -81,7 +81,6 @@ export default function EnvelopeEditor() {
isAutosaving, isAutosaving,
flushAutosave, flushAutosave,
relativePath, relativePath,
syncEnvelope,
editorFields, editorFields,
} = useCurrentEnvelopeEditor(); } = useCurrentEnvelopeEditor();
@ -157,7 +156,7 @@ export default function EnvelopeEditor() {
<EnvelopeEditorHeader /> <EnvelopeEditorHeader />
{/* Main Content Area */} {/* Main Content Area */}
<div className="flex h-[calc(100vh-73px)] w-screen"> <div className="flex h-[calc(100vh-4rem)] w-screen">
{/* Left Section - Step Navigation */} {/* Left Section - Step Navigation */}
<div className="bg-background border-border flex w-80 flex-shrink-0 flex-col overflow-y-auto border-r py-4"> <div className="bg-background border-border flex w-80 flex-shrink-0 flex-col overflow-y-auto border-r py-4">
{/* Left section step selector. */} {/* Left section step selector. */}
@ -251,7 +250,7 @@ export default function EnvelopeEditor() {
...envelope, ...envelope,
fields: editorFields.localFields, fields: editorFields.localFields,
}} }}
onDistribute={syncEnvelope} documentRootPath={relativePath.documentRootPath}
trigger={ trigger={
<Button variant="ghost" size="sm" className="w-full justify-start"> <Button variant="ghost" size="sm" className="w-full justify-start">
<SendIcon className="mr-2 h-4 w-4" /> <SendIcon className="mr-2 h-4 w-4" />
@ -369,16 +368,14 @@ export default function EnvelopeEditor() {
</div> </div>
{/* Main Content - Changes based on current step */} {/* Main Content - Changes based on current step */}
<div className="flex-1 overflow-y-auto"> <AnimateGenericFadeInOut className="flex-1 overflow-y-auto" key={currentStep}>
<AnimateGenericFadeInOut key={currentStep}> {match({ currentStep, isStepLoading })
{match({ currentStep, isStepLoading }) .with({ isStepLoading: true }, () => <SpinnerBox className="py-32" />)
.with({ isStepLoading: true }, () => <SpinnerBox className="py-32" />) .with({ currentStep: 'upload' }, () => <EnvelopeEditorUploadPage />)
.with({ currentStep: 'upload' }, () => <EnvelopeEditorUploadPage />) .with({ currentStep: 'addFields' }, () => <EnvelopeEditorFieldsPage />)
.with({ currentStep: 'addFields' }, () => <EnvelopeEditorFieldsPage />) .with({ currentStep: 'preview' }, () => <EnvelopeEditorPreviewPage />)
.with({ currentStep: 'preview' }, () => <EnvelopeEditorPreviewPage />) .exhaustive()}
.exhaustive()} </AnimateGenericFadeInOut>
</AnimateGenericFadeInOut>
</div>
</div> </div>
</div> </div>
); );

View File

@ -20,7 +20,8 @@ export const EnvelopeItemSelector = ({
}: EnvelopeItemSelectorProps) => { }: EnvelopeItemSelectorProps) => {
return ( return (
<button <button
className={`flex min-w-0 flex-shrink-0 cursor-pointer items-center space-x-3 rounded-lg border px-4 py-3 transition-colors ${ title={typeof primaryText === 'string' ? primaryText : undefined}
className={`flex h-fit max-w-72 flex-shrink-0 cursor-pointer items-center space-x-3 rounded-lg border px-4 py-3 transition-colors ${
isSelected isSelected
? 'border-green-200 bg-green-50 text-green-900 dark:border-green-400/30 dark:bg-green-400/10 dark:text-green-400' ? 'border-green-200 bg-green-50 text-green-900 dark:border-green-400/30 dark:bg-green-400/10 dark:text-green-400'
: 'border-border bg-muted/50 hover:bg-muted/70' : 'border-border bg-muted/50 hover:bg-muted/70'
@ -39,7 +40,7 @@ export const EnvelopeItemSelector = ({
<div className="text-xs text-gray-500">{secondaryText}</div> <div className="text-xs text-gray-500">{secondaryText}</div>
</div> </div>
<div <div
className={cn('h-2 w-2 rounded-full', { className={cn('h-2 w-2 flex-shrink-0 rounded-full', {
'bg-green-500': isSelected, 'bg-green-500': isSelected,
})} })}
></div> ></div>
@ -61,7 +62,7 @@ export const EnvelopeRendererFileSelector = ({
const { envelopeItems, currentEnvelopeItem, setCurrentEnvelopeItem } = useCurrentEnvelopeRender(); const { envelopeItems, currentEnvelopeItem, setCurrentEnvelopeItem } = useCurrentEnvelopeRender();
return ( return (
<div className={cn('flex h-fit space-x-2 overflow-x-auto p-4', className)}> <div className={cn('flex h-fit flex-shrink-0 space-x-2 overflow-x-auto p-4', className)}>
{envelopeItems.map((doc, i) => ( {envelopeItems.map((doc, i) => (
<EnvelopeItemSelector <EnvelopeItemSelector
key={doc.id} key={doc.id}

View File

@ -12,7 +12,7 @@ import { getClientSideFieldTranslations } from '@documenso/lib/utils/fields';
export default function EnvelopeGenericPageRenderer() { export default function EnvelopeGenericPageRenderer() {
const { i18n } = useLingui(); const { i18n } = useLingui();
const { currentEnvelopeItem, fields } = useCurrentEnvelopeRender(); const { currentEnvelopeItem, fields, getRecipientColorKey } = useCurrentEnvelopeRender();
const { const {
stage, stage,
@ -60,8 +60,7 @@ export default function EnvelopeGenericPageRenderer() {
translations: getClientSideFieldTranslations(i18n), translations: getClientSideFieldTranslations(i18n),
pageWidth: unscaledViewport.width, pageWidth: unscaledViewport.width,
pageHeight: unscaledViewport.height, pageHeight: unscaledViewport.height,
// color: getRecipientColorKey(field.recipientId), color: getRecipientColorKey(field.recipientId),
color: 'purple', // Todo
editable: false, editable: false,
mode: 'sign', mode: 'sign',
}); });
@ -80,7 +79,7 @@ export default function EnvelopeGenericPageRenderer() {
}; };
/** /**
* Render fields when they are added or removed from the localFields. * Render fields when they are added or removed
*/ */
useEffect(() => { useEffect(() => {
if (!pageLayer.current || !stage.current) { if (!pageLayer.current || !stage.current) {
@ -93,14 +92,12 @@ export default function EnvelopeGenericPageRenderer() {
group.name() === 'field-group' && group.name() === 'field-group' &&
!localPageFields.some((field) => field.id.toString() === group.id()) !localPageFields.some((field) => field.id.toString() === group.id())
) { ) {
console.log('Field removed, removing from canvas');
group.destroy(); group.destroy();
} }
}); });
// If it exists, rerender. // If it exists, rerender.
localPageFields.forEach((field) => { localPageFields.forEach((field) => {
console.log('Field created/updated, rendering on canvas');
renderFieldOnLayer(field); renderFieldOnLayer(field);
}); });

View File

@ -15,7 +15,6 @@ export type ShareDocumentDownloadButtonProps = {
documentData: DocumentData; documentData: DocumentData;
}; };
// Todo: Envelopes - Support multiple item downloads.
export const ShareDocumentDownloadButton = ({ export const ShareDocumentDownloadButton = ({
title, title,
documentData, documentData,

View File

@ -10,9 +10,9 @@ import { match } from 'ts-pattern';
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app'; import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions'; import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
import { putPdfFile } from '@documenso/lib/universal/upload/put-file';
import { formatTemplatesPath } from '@documenso/lib/utils/teams'; import { formatTemplatesPath } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { TCreateTemplatePayloadSchema } from '@documenso/trpc/server/template-router/schema';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
@ -40,17 +40,13 @@ export const TemplateDropZoneWrapper = ({ children, className }: TemplateDropZon
try { try {
setIsLoading(true); setIsLoading(true);
const payload = { const documentData = await putPdfFile(file);
const { legacyTemplateId: id } = await createTemplate({
title: file.name, title: file.name,
templateDocumentDataId: documentData.id,
folderId: folderId ?? undefined, folderId: folderId ?? undefined,
} satisfies TCreateTemplatePayloadSchema; });
const formData = new FormData();
formData.append('payload', JSON.stringify(payload));
formData.append('file', file);
const { envelopeId: id } = await createTemplate(formData);
toast({ toast({
title: _(msg`Template uploaded`), title: _(msg`Template uploaded`),

View File

@ -148,6 +148,7 @@ export default function DocumentPage({ params }: Route.ComponentProps) {
<EnvelopeRenderProvider <EnvelopeRenderProvider
envelope={envelope} envelope={envelope}
fields={envelope.status == DocumentStatus.COMPLETED ? [] : envelope.fields} fields={envelope.status == DocumentStatus.COMPLETED ? [] : envelope.fields}
recipientIds={envelope.recipients.map((recipient) => recipient.id)}
> >
{isMultiEnvelopeItem && ( {isMultiEnvelopeItem && (
<EnvelopeRendererFileSelector fields={envelope.fields} className="mb-4 p-0" /> <EnvelopeRendererFileSelector fields={envelope.fields} className="mb-4 p-0" />

View File

@ -99,7 +99,11 @@ export default function EnvelopeEditorPage({ params }: Route.ComponentProps) {
return ( return (
<EnvelopeEditorProvider initialEnvelope={envelope}> <EnvelopeEditorProvider initialEnvelope={envelope}>
<EnvelopeRenderProvider envelope={envelope}> <EnvelopeRenderProvider
envelope={envelope}
fields={envelope.fields}
recipientIds={envelope.recipients.map((recipient) => recipient.id)}
>
<EnvelopeEditor /> <EnvelopeEditor />
</EnvelopeRenderProvider> </EnvelopeRenderProvider>
</EnvelopeEditorProvider> </EnvelopeEditorProvider>

View File

@ -168,7 +168,11 @@ export default function TemplatePage({ params }: Route.ComponentProps) {
<div className="mt-6 grid w-full grid-cols-12 gap-8"> <div className="mt-6 grid w-full grid-cols-12 gap-8">
{envelope.internalVersion === 2 ? ( {envelope.internalVersion === 2 ? (
<div className="relative col-span-12 lg:col-span-6 xl:col-span-7"> <div className="relative col-span-12 lg:col-span-6 xl:col-span-7">
<EnvelopeRenderProvider envelope={envelope} fields={envelope.fields}> <EnvelopeRenderProvider
envelope={envelope}
fields={envelope.fields}
recipientIds={envelope.recipients.map((recipient) => recipient.id)}
>
{isMultiEnvelopeItem && ( {isMultiEnvelopeItem && (
<EnvelopeRendererFileSelector fields={envelope.fields} className="mb-4 p-0" /> <EnvelopeRendererFileSelector fields={envelope.fields} className="mb-4 p-0" />
)} )}

View File

@ -30,4 +30,6 @@ server.use(
const handler = handle(build, server); const handler = handle(build, server);
serve({ fetch: handler.fetch, port: 3000 }); const port = parseInt(process.env.PORT || '3000', 10);
serve({ fetch: handler.fetch, port });

View File

@ -1,10 +1,10 @@
import type { Context } from 'hono'; import type { Context } from 'hono';
import { createOpenApiFetchHandler } from 'trpc-to-openapi';
import { API_V2_BETA_URL } from '@documenso/lib/constants/app'; import { API_V2_BETA_URL } from '@documenso/lib/constants/app';
import { AppError, genericErrorCodeToTrpcErrorCodeMap } from '@documenso/lib/errors/app-error'; import { AppError, genericErrorCodeToTrpcErrorCodeMap } from '@documenso/lib/errors/app-error';
import { createTrpcContext } from '@documenso/trpc/server/context'; import { createTrpcContext } from '@documenso/trpc/server/context';
import { appRouter } from '@documenso/trpc/server/router'; import { appRouter } from '@documenso/trpc/server/router';
import { createOpenApiFetchHandler } from '@documenso/trpc/utils/openapi-fetch-handler';
import { handleTrpcRouterError } from '@documenso/trpc/utils/trpc-error-handler'; import { handleTrpcRouterError } from '@documenso/trpc/utils/trpc-error-handler';
export const openApiTrpcServerHandler = async (c: Context) => { export const openApiTrpcServerHandler = async (c: Context) => {

View File

@ -21,7 +21,7 @@ export default defineConfig({
}, },
}, },
server: { server: {
port: 3000, port: parseInt(process.env.PORT || '3000', 10),
strictPort: true, strictPort: true,
}, },
plugins: [ plugins: [

820
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -44,7 +44,7 @@
"@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",
"@prisma/client": "^6.18.0", "@prisma/client": "^6.8.2",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"dotenv-cli": "^8.0.0", "dotenv-cli": "^8.0.0",
"eslint": "^8.40.0", "eslint": "^8.40.0",
@ -54,21 +54,11 @@
"nodemailer": "^6.10.1", "nodemailer": "^6.10.1",
"playwright": "1.52.0", "playwright": "1.52.0",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"prisma": "^6.18.0", "prisma": "^6.8.2",
"prisma-extension-kysely": "^3.0.0", "prisma-extension-kysely": "^3.0.0",
"prisma-kysely": "^1.8.0", "prisma-kysely": "^1.8.0",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",
"turbo": "^1.9.3", "turbo": "^1.9.3",
"@trpc/client": "11.7.0",
"@trpc/react-query": "11.7.0",
"@trpc/server": "11.7.0",
"superjson": "^2.2.5",
"trpc-to-openapi": "2.4.0",
"zod-openapi": "^4.2.4",
"@ts-rest/core": "^3.52.1",
"@ts-rest/open-api": "^3.52.1",
"@ts-rest/serverless": "^3.52.1",
"zod-prisma-types": "3.3.5",
"vite": "^6.3.5" "vite": "^6.3.5"
}, },
"name": "@documenso/root", "name": "@documenso/root",
@ -86,12 +76,12 @@
"mupdf": "^1.0.0", "mupdf": "^1.0.0",
"react": "^18", "react": "^18",
"typescript": "5.6.2", "typescript": "5.6.2",
"zod": "^3.25.76" "zod": "3.24.1"
}, },
"overrides": { "overrides": {
"zod": "^3.25.76" "zod": "3.24.1"
}, },
"trigger.dev": { "trigger.dev": {
"endpointId": "documenso-app" "endpointId": "documenso-app"
} }
} }

View File

@ -17,14 +17,14 @@
"dependencies": { "dependencies": {
"@documenso/lib": "*", "@documenso/lib": "*",
"@documenso/prisma": "*", "@documenso/prisma": "*",
"@ts-rest/core": "^3.52.0", "@ts-rest/core": "^3.30.5",
"@ts-rest/open-api": "^3.52.0", "@ts-rest/open-api": "^3.33.0",
"@ts-rest/serverless": "^3.52.0", "@ts-rest/serverless": "^3.30.5",
"@types/swagger-ui-react": "^5.18.0", "@types/swagger-ui-react": "^5.18.0",
"luxon": "^3.4.0", "luxon": "^3.4.0",
"superjson": "^2.2.5", "superjson": "^1.13.1",
"swagger-ui-react": "^5.21.0", "swagger-ui-react": "^5.21.0",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",
"zod": "^3.25.76" "zod": "3.24.1"
} }
} }

View File

@ -20,12 +20,12 @@ import {
getEnvelopeWhereInput, getEnvelopeWhereInput,
} from '@documenso/lib/server-only/envelope/get-envelope-by-id'; } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { deleteDocumentField } from '@documenso/lib/server-only/field/delete-document-field'; import { deleteDocumentField } from '@documenso/lib/server-only/field/delete-document-field';
import { updateDocumentFields } from '@documenso/lib/server-only/field/update-document-fields'; import { updateEnvelopeFields } from '@documenso/lib/server-only/field/update-envelope-fields';
import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-form-values-in-pdf'; import { insertFormValuesInPdf } from '@documenso/lib/server-only/pdf/insert-form-values-in-pdf';
import { deleteDocumentRecipient } from '@documenso/lib/server-only/recipient/delete-document-recipient'; import { deleteEnvelopeRecipient } from '@documenso/lib/server-only/recipient/delete-envelope-recipient';
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document'; import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients'; import { setDocumentRecipients } from '@documenso/lib/server-only/recipient/set-document-recipients';
import { updateDocumentRecipients } from '@documenso/lib/server-only/recipient/update-document-recipients'; import { updateEnvelopeRecipients } from '@documenso/lib/server-only/recipient/update-envelope-recipients';
import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template'; import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template';
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template'; import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
import { findTemplates } from '@documenso/lib/server-only/template/find-templates'; import { findTemplates } from '@documenso/lib/server-only/template/find-templates';
@ -1285,7 +1285,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}; };
} }
const updatedRecipient = await updateDocumentRecipients({ const updatedRecipient = await updateEnvelopeRecipients({
userId: user.id, userId: user.id,
teamId: team.id, teamId: team.id,
id: { id: {
@ -1336,7 +1336,7 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}, },
}); });
const deletedRecipient = await deleteDocumentRecipient({ const deletedRecipient = await deleteEnvelopeRecipient({
userId: user.id, userId: user.id,
teamId: team.id, teamId: team.id,
recipientId: Number(recipientId), recipientId: Number(recipientId),
@ -1634,10 +1634,13 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
}; };
} }
const { fields } = await updateDocumentFields({ const { fields } = await updateEnvelopeFields({
userId: user.id, userId: user.id,
teamId: team.id, teamId: team.id,
documentId: legacyDocumentId, id: {
type: 'documentId',
id: legacyDocumentId,
},
fields: [ fields: [
{ {
id: Number(fieldId), id: Number(fieldId),

View File

@ -20,6 +20,6 @@
"luxon": "^3.5.0", "luxon": "^3.5.0",
"nanoid": "^5.1.5", "nanoid": "^5.1.5",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",
"zod": "^3.25.76" "zod": "3.24.1"
} }
} }

View File

@ -19,6 +19,6 @@
"micro": "^10.0.1", "micro": "^10.0.1",
"react": "^18", "react": "^18",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",
"zod": "^3.25.76" "zod": "3.24.1"
} }
} }

View File

@ -50,6 +50,7 @@ type UseEditorFieldsResponse = {
// Field operations // Field operations
addField: (field: Omit<TLocalField, 'formId'>) => TLocalField; addField: (field: Omit<TLocalField, 'formId'>) => TLocalField;
setFieldId: (formId: string, id: number) => void;
removeFieldsByFormId: (formIds: string[]) => void; removeFieldsByFormId: (formIds: string[]) => void;
updateFieldByFormId: (formId: string, updates: Partial<TLocalField>) => void; updateFieldByFormId: (formId: string, updates: Partial<TLocalField>) => void;
duplicateField: (field: TLocalField, recipientId?: number) => TLocalField; duplicateField: (field: TLocalField, recipientId?: number) => TLocalField;
@ -160,6 +161,17 @@ export const useEditorFields = ({
[localFields, remove, triggerFieldsUpdate], [localFields, remove, triggerFieldsUpdate],
); );
const setFieldId = (formId: string, id: number) => {
const index = localFields.findIndex((field) => field.formId === formId);
if (index !== -1) {
update(index, {
...localFields[index],
id,
});
}
};
const updateFieldByFormId = useCallback( const updateFieldByFormId = useCallback(
(formId: string, updates: Partial<TLocalField>) => { (formId: string, updates: Partial<TLocalField>) => {
const index = localFields.findIndex((field) => field.formId === formId); const index = localFields.findIndex((field) => field.formId === formId);
@ -269,6 +281,7 @@ export const useEditorFields = ({
// Field operations // Field operations
addField, addField,
setFieldId,
removeFieldsByFormId, removeFieldsByFormId,
updateFieldByFormId, updateFieldByFormId,
duplicateField, duplicateField,

View File

@ -97,6 +97,11 @@ export const EnvelopeEditorProvider = ({
const [envelope, setEnvelope] = useState(initialEnvelope); const [envelope, setEnvelope] = useState(initialEnvelope);
const [autosaveError, setAutosaveError] = useState<boolean>(false); const [autosaveError, setAutosaveError] = useState<boolean>(false);
const editorFields = useEditorFields({
envelope,
handleFieldsUpdate: (fields) => setFieldsDebounced(fields),
});
const envelopeUpdateMutationQuery = trpc.envelope.update.useMutation({ const envelopeUpdateMutationQuery = trpc.envelope.update.useMutation({
onSuccess: (response, input) => { onSuccess: (response, input) => {
setEnvelope({ setEnvelope({
@ -184,13 +189,24 @@ export const EnvelopeEditorProvider = ({
triggerSave: setFieldsDebounced, triggerSave: setFieldsDebounced,
flush: setFieldsAsync, flush: setFieldsAsync,
isPending: isFieldsMutationPending, isPending: isFieldsMutationPending,
} = useEnvelopeAutosave(async (fields: TLocalField[]) => { } = useEnvelopeAutosave(async (localFields: TLocalField[]) => {
await envelopeFieldSetMutationQuery.mutateAsync({ const envelopeFields = await envelopeFieldSetMutationQuery.mutateAsync({
envelopeId: envelope.id, envelopeId: envelope.id,
envelopeType: envelope.type, envelopeType: envelope.type,
fields, fields: localFields,
}); });
}, 1000);
// Insert the IDs into the local fields.
envelopeFields.fields.forEach((field) => {
const localField = localFields.find((localField) => localField.formId === field.formId);
if (localField && !localField.id) {
localField.id = field.id;
editorFields.setFieldId(localField.formId, field.id);
}
});
}, 2000);
const { const {
triggerSave: setEnvelopeDebounced, triggerSave: setEnvelopeDebounced,
@ -221,11 +237,6 @@ export const EnvelopeEditorProvider = ({
setEnvelopeDebounced(envelopeUpdates); setEnvelopeDebounced(envelopeUpdates);
}; };
const editorFields = useEditorFields({
envelope,
handleFieldsUpdate: (fields) => setFieldsDebounced(fields),
});
const getRecipientColorKey = useCallback( const getRecipientColorKey = useCallback(
(recipientId: number) => { (recipientId: number) => {
const recipientIndex = envelope.recipients.findIndex( const recipientIndex = envelope.recipients.findIndex(

View File

@ -3,6 +3,9 @@ import React from 'react';
import type { DocumentData } from '@prisma/client'; import type { DocumentData } from '@prisma/client';
import type { TRecipientColor } from '@documenso/ui/lib/recipient-colors';
import { AVAILABLE_RECIPIENT_COLORS } from '@documenso/ui/lib/recipient-colors';
import type { TEnvelope } from '../../types/envelope'; import type { TEnvelope } from '../../types/envelope';
import { getFile } from '../../universal/upload/get-file'; import { getFile } from '../../universal/upload/get-file';
@ -23,6 +26,7 @@ type EnvelopeRenderProviderValue = {
currentEnvelopeItem: EnvelopeRenderItem | null; currentEnvelopeItem: EnvelopeRenderItem | null;
setCurrentEnvelopeItem: (envelopeItemId: string) => void; setCurrentEnvelopeItem: (envelopeItemId: string) => void;
fields: TEnvelope['fields']; fields: TEnvelope['fields'];
getRecipientColorKey: (recipientId: number) => TRecipientColor;
}; };
interface EnvelopeRenderProviderProps { interface EnvelopeRenderProviderProps {
@ -35,6 +39,13 @@ interface EnvelopeRenderProviderProps {
* Only pass if the CustomRenderer you are passing in wants fields. * Only pass if the CustomRenderer you are passing in wants fields.
*/ */
fields?: TEnvelope['fields']; fields?: TEnvelope['fields'];
/**
* Optional recipient IDs used to determine the color of the fields.
*
* Only required for generic page renderers.
*/
recipientIds?: number[];
} }
const EnvelopeRenderContext = createContext<EnvelopeRenderProviderValue | null>(null); const EnvelopeRenderContext = createContext<EnvelopeRenderProviderValue | null>(null);
@ -56,6 +67,7 @@ export const EnvelopeRenderProvider = ({
children, children,
envelope, envelope,
fields, fields,
recipientIds = [],
}: EnvelopeRenderProviderProps) => { }: EnvelopeRenderProviderProps) => {
// Indexed by documentDataId. // Indexed by documentDataId.
const [files, setFiles] = useState<Record<string, FileData>>({}); const [files, setFiles] = useState<Record<string, FileData>>({});
@ -132,6 +144,17 @@ export const EnvelopeRenderProvider = ({
} }
}, [envelope.envelopeItems]); }, [envelope.envelopeItems]);
const getRecipientColorKey = useCallback(
(recipientId: number) => {
const recipientIndex = recipientIds.findIndex((id) => id === recipientId);
return AVAILABLE_RECIPIENT_COLORS[
Math.max(recipientIndex, 0) % AVAILABLE_RECIPIENT_COLORS.length
];
},
[recipientIds],
);
return ( return (
<EnvelopeRenderContext.Provider <EnvelopeRenderContext.Provider
value={{ value={{
@ -140,6 +163,7 @@ export const EnvelopeRenderProvider = ({
currentEnvelopeItem: currentItem, currentEnvelopeItem: currentItem,
setCurrentEnvelopeItem, setCurrentEnvelopeItem,
fields: fields ?? [], fields: fields ?? [],
getRecipientColorKey,
}} }}
> >
{children} {children}

View File

@ -189,7 +189,6 @@ export const run = async ({
settings, settings,
}); });
// Todo: Envelopes - Is it okay to have dynamic IDs?
const newDocumentData = await Promise.all( const newDocumentData = await Promise.all(
envelopeItems.map(async (envelopeItem) => envelopeItems.map(async (envelopeItem) =>
io.runTask(`decorate-and-sign-envelope-item-${envelopeItem.id}`, async () => { io.runTask(`decorate-and-sign-envelope-item-${envelopeItem.id}`, async () => {

View File

@ -55,11 +55,11 @@
"skia-canvas": "^3.0.8", "skia-canvas": "^3.0.8",
"stripe": "^12.7.0", "stripe": "^12.7.0",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",
"zod": "^3.25.76" "zod": "3.24.1"
}, },
"devDependencies": { "devDependencies": {
"@playwright/browser-chromium": "1.52.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"
} }
} }

View File

@ -256,11 +256,10 @@ export const sendDocument = async ({
}); });
} }
// Todo: Envelopes - [AUDIT_LOGS]
if (envelope.internalVersion === 2) { if (envelope.internalVersion === 2) {
await Promise.all( const autoInsertedFields = await Promise.all(
fieldsToAutoInsert.map(async (field) => { fieldsToAutoInsert.map(async (field) => {
await tx.field.update({ return await tx.field.update({
where: { where: {
id: field.fieldId, id: field.fieldId,
}, },
@ -271,6 +270,21 @@ export const sendDocument = async ({
}); });
}), }),
); );
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELDS_AUTO_INSERTED,
envelopeId: envelope.id,
data: {
fields: autoInsertedFields.map((field) => ({
fieldId: field.id,
fieldType: field.type,
recipientId: field.recipientId,
})),
},
// Don't put metadata or user here since it's a system event.
}),
});
} }
return await tx.envelope.update({ return await tx.envelope.update({

View File

@ -16,16 +16,11 @@ import type { ApiRequestMetadata } from '@documenso/lib/universal/extract-reques
import { nanoid, prefixedId } from '@documenso/lib/universal/id'; import { nanoid, prefixedId } from '@documenso/lib/universal/id';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs'; import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import type { TCreateEnvelopeRequest } from '@documenso/trpc/server/envelope-router/create-envelope.types';
import type { import type { TDocumentAccessAuthTypes, TDocumentActionAuthTypes } from '../../types/document-auth';
TDocumentAccessAuthTypes,
TDocumentActionAuthTypes,
TRecipientAccessAuthTypes,
TRecipientActionAuthTypes,
} from '../../types/document-auth';
import type { TDocumentFormValues } from '../../types/document-form-values'; import type { TDocumentFormValues } from '../../types/document-form-values';
import type { TEnvelopeAttachmentType } from '../../types/envelope-attachment'; import type { TEnvelopeAttachmentType } from '../../types/envelope-attachment';
import type { TFieldAndMeta } from '../../types/field-meta';
import { import {
ZWebhookDocumentSchema, ZWebhookDocumentSchema,
mapEnvelopeToWebhookDocumentPayload, mapEnvelopeToWebhookDocumentPayload,
@ -39,25 +34,6 @@ import { incrementDocumentId, incrementTemplateId } from '../envelope/increment-
import { getTeamSettings } from '../team/get-team-settings'; import { getTeamSettings } from '../team/get-team-settings';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook'; import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';
type CreateEnvelopeRecipientFieldOptions = TFieldAndMeta & {
documentDataId: string;
page: number;
positionX: number;
positionY: number;
width: number;
height: number;
};
type CreateEnvelopeRecipientOptions = {
email: string;
name: string;
role: RecipientRole;
signingOrder?: number;
accessAuth?: TRecipientAccessAuthTypes[];
actionAuth?: TRecipientActionAuthTypes[];
fields?: CreateEnvelopeRecipientFieldOptions[];
};
export type CreateEnvelopeOptions = { export type CreateEnvelopeOptions = {
userId: number; userId: number;
teamId: number; teamId: number;
@ -80,7 +56,7 @@ export type CreateEnvelopeOptions = {
visibility?: DocumentVisibility; visibility?: DocumentVisibility;
globalAccessAuth?: TDocumentAccessAuthTypes[]; globalAccessAuth?: TDocumentAccessAuthTypes[];
globalActionAuth?: TDocumentActionAuthTypes[]; globalActionAuth?: TDocumentActionAuthTypes[];
recipients?: CreateEnvelopeRecipientOptions[]; recipients?: TCreateEnvelopeRequest['recipients'];
folderId?: string; folderId?: string;
}; };
attachments?: Array<{ attachments?: Array<{

View File

@ -11,7 +11,7 @@ export type GetFieldByIdOptions = {
userId: number; userId: number;
teamId: number; teamId: number;
fieldId: number; fieldId: number;
envelopeType: EnvelopeType; envelopeType?: EnvelopeType;
}; };
export const getFieldById = async ({ export const getFieldById = async ({
@ -41,7 +41,7 @@ export const getFieldById = async ({
type: 'envelopeId', type: 'envelopeId',
id: field.envelopeId, id: field.envelopeId,
}, },
type: envelopeType, type: envelopeType ?? null,
userId, userId,
teamId, teamId,
}); });

View File

@ -306,7 +306,10 @@ export const setFieldsForDocument = async ({
}); });
} }
return upsertedField; return {
...upsertedField,
formId: field.formId,
};
}), }),
); );
}); });
@ -340,17 +343,25 @@ export const setFieldsForDocument = async ({
} }
// Filter out fields that have been removed or have been updated. // Filter out fields that have been removed or have been updated.
const filteredFields = existingFields.filter((field) => { const mappedFilteredFields = existingFields
const isRemoved = removedFields.find((removedField) => removedField.id === field.id); .filter((field) => {
const isUpdated = persistedFields.find((persistedField) => persistedField.id === field.id); const isRemoved = removedFields.find((removedField) => removedField.id === field.id);
const isUpdated = persistedFields.find((persistedField) => persistedField.id === field.id);
return !isRemoved && !isUpdated; return !isRemoved && !isUpdated;
}); })
.map((field) => ({
...mapFieldToLegacyField(field, envelope),
formId: undefined,
}));
const mappedPersistentFields = persistedFields.map((field) => ({
...mapFieldToLegacyField(field, envelope),
formId: field?.formId,
}));
return { return {
fields: [...filteredFields, ...persistedFields].map((field) => fields: [...mappedFilteredFields, ...mappedPersistentFields],
mapFieldToLegacyField(field, envelope),
),
}; };
}; };
@ -359,6 +370,7 @@ export const setFieldsForDocument = async ({
*/ */
type FieldData = { type FieldData = {
id?: number | null; id?: number | null;
formId?: string;
envelopeItemId: string; envelopeItemId: string;
type: FieldType; type: FieldType;
recipientId: number; recipientId: number;

View File

@ -27,6 +27,7 @@ export type SetFieldsForTemplateOptions = {
id: EnvelopeIdOptions; id: EnvelopeIdOptions;
fields: { fields: {
id?: number | null; id?: number | null;
formId?: string;
envelopeItemId: string; envelopeItemId: string;
type: FieldType; type: FieldType;
recipientId: number; recipientId: number;
@ -111,10 +112,10 @@ export const setFieldsForTemplate = async ({
}; };
}); });
const persistedFields = await prisma.$transaction( const persistedFields = await Promise.all(
// Disabling as wrapping promises here causes type issues // Disabling as wrapping promises here causes type issues
// eslint-disable-next-line @typescript-eslint/promise-function-async // eslint-disable-next-line @typescript-eslint/promise-function-async
linkedFields.map((field) => { linkedFields.map(async (field) => {
const parsedFieldMeta = field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined; const parsedFieldMeta = field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined;
if (field.type === FieldType.TEXT && field.fieldMeta) { if (field.type === FieldType.TEXT && field.fieldMeta) {
@ -176,7 +177,7 @@ export const setFieldsForTemplate = async ({
} }
// Proceed with upsert operation // Proceed with upsert operation
return prisma.field.upsert({ const upsertedField = await prisma.field.upsert({
where: { where: {
id: field._persisted?.id ?? -1, id: field._persisted?.id ?? -1,
envelopeId: envelope.id, envelopeId: envelope.id,
@ -219,6 +220,11 @@ export const setFieldsForTemplate = async ({
}, },
}, },
}); });
return {
...upsertedField,
formId: field.formId,
};
}), }),
); );
@ -240,9 +246,17 @@ export const setFieldsForTemplate = async ({
return !isRemoved && !isUpdated; return !isRemoved && !isUpdated;
}); });
const mappedFilteredFields = filteredFields.map((field) => ({
...mapFieldToLegacyField(field, envelope),
formId: undefined,
}));
const mappedPersistentFields = persistedFields.map((field) => ({
...mapFieldToLegacyField(field, envelope),
formId: field?.formId,
}));
return { return {
fields: [...filteredFields, ...persistedFields].map((field) => fields: [...mappedFilteredFields, ...mappedPersistentFields],
mapFieldToLegacyField(field, envelope),
),
}; };
}; };

View File

@ -10,18 +10,21 @@ import {
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 { type EnvelopeIdOptions } from '../../utils/envelope';
import { mapFieldToLegacyField } from '../../utils/fields'; import { mapFieldToLegacyField } from '../../utils/fields';
import { canRecipientFieldsBeModified } from '../../utils/recipients'; import { canRecipientFieldsBeModified } from '../../utils/recipients';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface UpdateDocumentFieldsOptions { export interface UpdateEnvelopeFieldsOptions {
userId: number; userId: number;
teamId: number; teamId: number;
documentId: number; id: EnvelopeIdOptions;
type?: EnvelopeType | null; // Only used to enforce the type.
fields: { fields: {
id: number; id: number;
type?: FieldType; type?: FieldType;
pageNumber?: number; pageNumber?: number;
envelopeItemId?: string;
pageX?: number; pageX?: number;
pageY?: number; pageY?: number;
width?: number; width?: number;
@ -31,19 +34,17 @@ export interface UpdateDocumentFieldsOptions {
requestMetadata: ApiRequestMetadata; requestMetadata: ApiRequestMetadata;
} }
export const updateDocumentFields = async ({ export const updateEnvelopeFields = async ({
userId, userId,
teamId, teamId,
documentId, id,
type = null,
fields, fields,
requestMetadata, requestMetadata,
}: UpdateDocumentFieldsOptions) => { }: UpdateEnvelopeFieldsOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({ const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: { id,
type: 'documentId', type,
id: documentId,
},
type: EnvelopeType.DOCUMENT,
userId, userId,
teamId, teamId,
}); });
@ -53,18 +54,19 @@ export const updateDocumentFields = async ({
include: { include: {
recipients: true, recipients: true,
fields: true, fields: true,
envelopeItems: true,
}, },
}); });
if (!envelope) { if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, { throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found', message: 'Envelope not found',
}); });
} }
if (envelope.completedAt) { if (envelope.completedAt) {
throw new AppError(AppErrorCode.INVALID_REQUEST, { throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Document already complete', message: 'Envelope already complete',
}); });
} }
@ -96,6 +98,29 @@ export const updateDocumentFields = async ({
}); });
} }
const fieldType = field.type || originalField.type;
const fieldMetaType = field.fieldMeta?.type || originalField.fieldMeta?.type;
// Not going to mess with V1 envelopes.
if (
envelope.internalVersion === 2 &&
fieldMetaType &&
fieldMetaType.toLowerCase() !== fieldType.toLowerCase()
) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Field meta type does not match the field type',
});
}
if (
field.envelopeItemId &&
!envelope.envelopeItems.some((item) => item.id === field.envelopeItemId)
) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope item not found',
});
}
return { return {
originalField, originalField,
updateData: field, updateData: field,
@ -118,27 +143,30 @@ export const updateDocumentFields = async ({
width: updateData.width, width: updateData.width,
height: updateData.height, height: updateData.height,
fieldMeta: updateData.fieldMeta, fieldMeta: updateData.fieldMeta,
envelopeItemId: updateData.envelopeItemId,
}, },
}); });
const changes = diffFieldChanges(originalField, updatedField);
// Handle field updated audit log. // Handle field updated audit log.
if (changes.length > 0) { if (envelope.type === EnvelopeType.DOCUMENT) {
await tx.documentAuditLog.create({ const changes = diffFieldChanges(originalField, updatedField);
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED, if (changes.length > 0) {
envelopeId: envelope.id, await tx.documentAuditLog.create({
metadata: requestMetadata, data: createDocumentAuditLogData({
data: { type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_UPDATED,
fieldId: updatedField.secondaryId, envelopeId: envelope.id,
fieldRecipientEmail: recipientEmail, metadata: requestMetadata,
fieldRecipientId: updatedField.recipientId, data: {
fieldType: updatedField.type, fieldId: updatedField.secondaryId,
changes, fieldRecipientEmail: recipientEmail,
}, fieldRecipientId: updatedField.recipientId,
}), fieldType: updatedField.type,
}); changes,
},
}),
});
}
} }
return updatedField; return updatedField;

View File

@ -1,116 +0,0 @@
import { EnvelopeType, type FieldType } from '@prisma/client';
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { mapFieldToLegacyField } from '../../utils/fields';
import { canRecipientFieldsBeModified } from '../../utils/recipients';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface UpdateTemplateFieldsOptions {
userId: number;
teamId: number;
templateId: number;
fields: {
id: number;
type?: FieldType;
pageNumber?: number;
pageX?: number;
pageY?: number;
width?: number;
height?: number;
fieldMeta?: TFieldMetaSchema;
}[];
}
export const updateTemplateFields = async ({
userId,
teamId,
templateId,
fields,
}: UpdateTemplateFieldsOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'templateId',
id: templateId,
},
type: EnvelopeType.TEMPLATE,
userId,
teamId,
});
const envelope = await prisma.envelope.findFirst({
where: envelopeWhereInput,
include: {
recipients: true,
fields: true,
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found',
});
}
const fieldsToUpdate = fields.map((field) => {
const originalField = envelope.fields.find((existingField) => existingField.id === field.id);
if (!originalField) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: `Field with id ${field.id} not found`,
});
}
const recipient = envelope.recipients.find(
(recipient) => recipient.id === originalField.recipientId,
);
// Each field MUST have a recipient associated with it.
if (!recipient) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: `Recipient attached to field ${field.id} not found`,
});
}
// Check whether the recipient associated with the field can be modified.
if (!canRecipientFieldsBeModified(recipient, envelope.fields)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message:
'Cannot modify a field where the recipient has already interacted with the document',
});
}
return {
updateData: field,
};
});
const updatedFields = await prisma.$transaction(async (tx) => {
return await Promise.all(
fieldsToUpdate.map(async ({ updateData }) => {
const updatedField = await tx.field.update({
where: {
id: updateData.id,
},
data: {
type: updateData.type,
page: updateData.pageNumber,
positionX: updateData.pageX,
positionY: updateData.pageY,
width: updateData.width,
height: updateData.height,
fieldMeta: updateData.fieldMeta,
},
});
return updatedField;
}),
);
});
return {
fields: updatedFields.map((field) => mapFieldToLegacyField(field, envelope)),
};
};

View File

@ -1,22 +1,13 @@
import { PDFDocument } from '@cantoo/pdf-lib'; import { PDFDocument } from '@cantoo/pdf-lib';
import { AppError } from '../../errors/app-error';
import { flattenAnnotations } from './flatten-annotations'; import { flattenAnnotations } from './flatten-annotations';
import { flattenForm, removeOptionalContentGroups } from './flatten-form'; import { flattenForm, removeOptionalContentGroups } from './flatten-form';
export const normalizePdf = async (pdf: Buffer) => { export const normalizePdf = async (pdf: Buffer) => {
const pdfDoc = await PDFDocument.load(pdf).catch((e) => { const pdfDoc = await PDFDocument.load(pdf).catch(() => null);
console.error(`PDF normalization error: ${e.message}`);
throw new AppError('INVALID_DOCUMENT_FILE', { if (!pdfDoc) {
message: 'The document is not a valid PDF', return pdf;
});
});
if (pdfDoc.isEncrypted) {
throw new AppError('INVALID_DOCUMENT_FILE', {
message: 'The document is encrypted',
});
} }
removeOptionalContentGroups(pdfDoc); removeOptionalContentGroups(pdfDoc);

View File

@ -15,7 +15,7 @@ import type { EnvelopeIdOptions } from '../../utils/envelope';
import { mapRecipientToLegacyRecipient } from '../../utils/recipients'; import { mapRecipientToLegacyRecipient } from '../../utils/recipients';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface CreateDocumentRecipientsOptions { export interface CreateEnvelopeRecipientsOptions {
userId: number; userId: number;
teamId: number; teamId: number;
id: EnvelopeIdOptions; id: EnvelopeIdOptions;
@ -30,16 +30,16 @@ export interface CreateDocumentRecipientsOptions {
requestMetadata: ApiRequestMetadata; requestMetadata: ApiRequestMetadata;
} }
export const createDocumentRecipients = async ({ export const createEnvelopeRecipients = async ({
userId, userId,
teamId, teamId,
id, id,
recipients: recipientsToCreate, recipients: recipientsToCreate,
requestMetadata, requestMetadata,
}: CreateDocumentRecipientsOptions) => { }: CreateEnvelopeRecipientsOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({ const { envelopeWhereInput } = await getEnvelopeWhereInput({
id, id,
type: EnvelopeType.DOCUMENT, type: null,
userId, userId,
teamId, teamId,
}); });
@ -62,13 +62,13 @@ export const createDocumentRecipients = async ({
if (!envelope) { if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, { throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found', message: 'Envelope not found',
}); });
} }
if (envelope.completedAt) { if (envelope.completedAt) {
throw new AppError(AppErrorCode.INVALID_REQUEST, { throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Document already complete', message: 'Envelope already complete',
}); });
} }
@ -112,21 +112,23 @@ export const createDocumentRecipients = async ({
}); });
// Handle recipient created audit log. // Handle recipient created audit log.
await tx.documentAuditLog.create({ if (envelope.type === EnvelopeType.DOCUMENT) {
data: createDocumentAuditLogData({ await tx.documentAuditLog.create({
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED, data: createDocumentAuditLogData({
envelopeId: envelope.id, type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_CREATED,
metadata: requestMetadata, envelopeId: envelope.id,
data: { metadata: requestMetadata,
recipientEmail: createdRecipient.email, data: {
recipientName: createdRecipient.name, recipientEmail: createdRecipient.email,
recipientId: createdRecipient.id, recipientName: createdRecipient.name,
recipientRole: createdRecipient.role, recipientId: createdRecipient.id,
accessAuth: recipient.accessAuth ?? [], recipientRole: createdRecipient.role,
actionAuth: recipient.actionAuth ?? [], accessAuth: recipient.accessAuth ?? [],
}, actionAuth: recipient.actionAuth ?? [],
}), },
}); }),
});
}
return createdRecipient; return createdRecipient;
}), }),

View File

@ -1,115 +0,0 @@
import { EnvelopeType, RecipientRole } from '@prisma/client';
import { SendStatus, SigningStatus } from '@prisma/client';
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
import { type TRecipientActionAuthTypes } from '@documenso/lib/types/document-auth';
import { nanoid } from '@documenso/lib/universal/id';
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { mapRecipientToLegacyRecipient } from '../../utils/recipients';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface CreateTemplateRecipientsOptions {
userId: number;
teamId: number;
templateId: number;
recipients: {
email: string;
name: string;
role: RecipientRole;
signingOrder?: number | null;
accessAuth?: TRecipientAccessAuthTypes[];
actionAuth?: TRecipientActionAuthTypes[];
}[];
}
export const createTemplateRecipients = async ({
userId,
teamId,
templateId,
recipients: recipientsToCreate,
}: CreateTemplateRecipientsOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'templateId',
id: templateId,
},
type: EnvelopeType.TEMPLATE,
userId,
teamId,
});
const template = await prisma.envelope.findFirst({
where: envelopeWhereInput,
include: {
recipients: true,
team: {
select: {
organisation: {
select: {
organisationClaim: true,
},
},
},
},
},
});
if (!template) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
const recipientsHaveActionAuth = recipientsToCreate.some(
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
);
// Check if user has permission to set the global action auth.
if (recipientsHaveActionAuth && !template.team.organisation.organisationClaim.flags.cfr21) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to set the action auth',
});
}
const normalizedRecipients = recipientsToCreate.map((recipient) => ({
...recipient,
email: recipient.email.toLowerCase(),
}));
const createdRecipients = await prisma.$transaction(async (tx) => {
return await Promise.all(
normalizedRecipients.map(async (recipient) => {
const authOptions = createRecipientAuthOptions({
accessAuth: recipient.accessAuth ?? [],
actionAuth: recipient.actionAuth ?? [],
});
const createdRecipient = await tx.recipient.create({
data: {
envelopeId: template.id,
name: recipient.name,
email: recipient.email,
role: recipient.role,
signingOrder: recipient.signingOrder,
token: nanoid(),
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
signingStatus:
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
authOptions,
},
});
return createdRecipient;
}),
);
});
return {
recipients: createdRecipients.map((recipient) =>
mapRecipientToLegacyRecipient(recipient, template),
),
};
};

View File

@ -14,26 +14,27 @@ import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import { AppError, AppErrorCode } from '../../errors/app-error'; import { AppError, AppErrorCode } from '../../errors/app-error';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email'; import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs'; import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { canRecipientBeModified } from '../../utils/recipients';
import { renderEmailWithI18N } from '../../utils/render-email-with-i18n'; import { renderEmailWithI18N } from '../../utils/render-email-with-i18n';
import { buildTeamWhereQuery } from '../../utils/teams'; import { buildTeamWhereQuery } from '../../utils/teams';
import { getEmailContext } from '../email/get-email-context'; import { getEmailContext } from '../email/get-email-context';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface DeleteDocumentRecipientOptions { export interface DeleteEnvelopeRecipientOptions {
userId: number; userId: number;
teamId: number; teamId: number;
recipientId: number; recipientId: number;
requestMetadata: ApiRequestMetadata; requestMetadata: ApiRequestMetadata;
} }
export const deleteDocumentRecipient = async ({ export const deleteEnvelopeRecipient = async ({
userId, userId,
teamId, teamId,
recipientId, recipientId,
requestMetadata, requestMetadata,
}: DeleteDocumentRecipientOptions) => { }: DeleteEnvelopeRecipientOptions) => {
const envelope = await prisma.envelope.findFirst({ const envelope = await prisma.envelope.findFirst({
where: { where: {
type: EnvelopeType.DOCUMENT,
recipients: { recipients: {
some: { some: {
id: recipientId, id: recipientId,
@ -48,6 +49,9 @@ export const deleteDocumentRecipient = async ({
where: { where: {
id: recipientId, id: recipientId,
}, },
include: {
fields: true,
},
}, },
}, },
}); });
@ -89,24 +93,43 @@ export const deleteDocumentRecipient = async ({
}); });
} }
const deletedRecipient = await prisma.$transaction(async (tx) => { if (!canRecipientBeModified(recipientToDelete, recipientToDelete.fields)) {
await tx.documentAuditLog.create({ throw new AppError(AppErrorCode.INVALID_REQUEST, {
data: createDocumentAuditLogData({ message: 'Recipient has already interacted with the document.',
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED,
envelopeId: envelope.id,
metadata: requestMetadata,
data: {
recipientEmail: recipientToDelete.email,
recipientName: recipientToDelete.name,
recipientId: recipientToDelete.id,
recipientRole: recipientToDelete.role,
},
}),
}); });
}
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'envelopeId',
id: envelope.id,
},
type: null,
userId,
teamId,
});
const deletedRecipient = await prisma.$transaction(async (tx) => {
if (envelope.type === EnvelopeType.DOCUMENT) {
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_DELETED,
envelopeId: envelope.id,
metadata: requestMetadata,
data: {
recipientEmail: recipientToDelete.email,
recipientName: recipientToDelete.name,
recipientId: recipientToDelete.id,
recipientRole: recipientToDelete.role,
},
}),
});
}
return await tx.recipient.delete({ return await tx.recipient.delete({
where: { where: {
id: recipientId, id: recipientId,
envelope: envelopeWhereInput,
}, },
}); });
}); });
@ -116,7 +139,11 @@ export const deleteDocumentRecipient = async ({
).recipientRemoved; ).recipientRemoved;
// Send email to deleted recipient. // Send email to deleted recipient.
if (recipientToDelete.sendStatus === SendStatus.SENT && isRecipientRemovedEmailEnabled) { if (
recipientToDelete.sendStatus === SendStatus.SENT &&
isRecipientRemovedEmailEnabled &&
envelope.type === EnvelopeType.DOCUMENT
) {
const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000'; const assetBaseUrl = NEXT_PUBLIC_WEBAPP_URL() || 'http://localhost:3000';
const template = createElement(RecipientRemovedFromDocumentTemplate, { const template = createElement(RecipientRemovedFromDocumentTemplate, {

View File

@ -1,58 +0,0 @@
import { EnvelopeType } from '@prisma/client';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { buildTeamWhereQuery } from '../../utils/teams';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface DeleteTemplateRecipientOptions {
userId: number;
teamId: number;
recipientId: number;
}
export const deleteTemplateRecipient = async ({
userId,
teamId,
recipientId,
}: DeleteTemplateRecipientOptions): Promise<void> => {
const recipientToDelete = await prisma.recipient.findFirst({
where: {
id: recipientId,
envelope: {
type: EnvelopeType.TEMPLATE,
team: buildTeamWhereQuery({ teamId, userId }),
},
},
});
if (!recipientToDelete) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Recipient not found',
});
}
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'envelopeId',
id: recipientToDelete.envelopeId,
},
type: EnvelopeType.TEMPLATE,
userId,
teamId,
});
if (!recipientToDelete || recipientToDelete.id !== recipientId) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Recipient not found',
});
}
await prisma.recipient.delete({
where: {
id: recipientId,
envelope: envelopeWhereInput,
},
});
};

View File

@ -1,5 +1,4 @@
import { EnvelopeType, RecipientRole } from '@prisma/client'; import { EnvelopeType, RecipientRole, SendStatus, SigningStatus } from '@prisma/client';
import { SendStatus, SigningStatus } from '@prisma/client';
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 type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth'; import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
@ -16,29 +15,38 @@ import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
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 { type EnvelopeIdOptions, mapSecondaryIdToDocumentId } from '../../utils/envelope'; import { extractLegacyIds } from '../../universal/id';
import { type EnvelopeIdOptions } from '../../utils/envelope';
import { mapFieldToLegacyField } from '../../utils/fields'; import { mapFieldToLegacyField } from '../../utils/fields';
import { canRecipientBeModified } from '../../utils/recipients'; import { canRecipientBeModified } from '../../utils/recipients';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id'; import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface UpdateDocumentRecipientsOptions { export interface UpdateEnvelopeRecipientsOptions {
userId: number; userId: number;
teamId: number; teamId: number;
id: EnvelopeIdOptions; id: EnvelopeIdOptions;
recipients: RecipientData[]; recipients: {
id: number;
email?: string;
name?: string;
role?: RecipientRole;
signingOrder?: number | null;
accessAuth?: TRecipientAccessAuthTypes[];
actionAuth?: TRecipientActionAuthTypes[];
}[];
requestMetadata: ApiRequestMetadata; requestMetadata: ApiRequestMetadata;
} }
export const updateDocumentRecipients = async ({ export const updateEnvelopeRecipients = async ({
userId, userId,
teamId, teamId,
id, id,
recipients, recipients,
requestMetadata, requestMetadata,
}: UpdateDocumentRecipientsOptions) => { }: UpdateEnvelopeRecipientsOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({ const { envelopeWhereInput } = await getEnvelopeWhereInput({
id, id,
type: EnvelopeType.DOCUMENT, type: null,
userId, userId,
teamId, teamId,
}); });
@ -62,13 +70,13 @@ export const updateDocumentRecipients = async ({
if (!envelope) { if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, { throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document not found', message: 'Envelope not found',
}); });
} }
if (envelope.completedAt) { if (envelope.completedAt) {
throw new AppError(AppErrorCode.INVALID_REQUEST, { throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Document already complete', message: 'Envelope already complete',
}); });
} }
@ -160,24 +168,26 @@ export const updateDocumentRecipients = async ({
}); });
} }
const changes = diffRecipientChanges(originalRecipient, updatedRecipient);
// Handle recipient updated audit log. // Handle recipient updated audit log.
if (changes.length > 0) { if (envelope.type === EnvelopeType.DOCUMENT) {
await tx.documentAuditLog.create({ const changes = diffRecipientChanges(originalRecipient, updatedRecipient);
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED, if (changes.length > 0) {
envelopeId: envelope.id, await tx.documentAuditLog.create({
metadata: requestMetadata, data: createDocumentAuditLogData({
data: { type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
recipientEmail: updatedRecipient.email, envelopeId: envelope.id,
recipientName: updatedRecipient.name, metadata: requestMetadata,
recipientId: updatedRecipient.id, data: {
recipientRole: updatedRecipient.role, recipientEmail: updatedRecipient.email,
changes, recipientName: updatedRecipient.name,
}, recipientId: updatedRecipient.id,
}), recipientRole: updatedRecipient.role,
}); changes,
},
}),
});
}
} }
return updatedRecipient; return updatedRecipient;
@ -188,19 +198,8 @@ export const updateDocumentRecipients = async ({
return { return {
recipients: updatedRecipients.map((recipient) => ({ recipients: updatedRecipients.map((recipient) => ({
...recipient, ...recipient,
documentId: mapSecondaryIdToDocumentId(envelope.secondaryId), ...extractLegacyIds(envelope),
templateId: null,
fields: recipient.fields.map((field) => mapFieldToLegacyField(field, envelope)), fields: recipient.fields.map((field) => mapFieldToLegacyField(field, envelope)),
})), })),
}; };
}; };
type RecipientData = {
id: number;
email?: string;
name?: string;
role?: RecipientRole;
signingOrder?: number | null;
accessAuth?: TRecipientAccessAuthTypes[];
actionAuth?: TRecipientActionAuthTypes[];
};

View File

@ -1,168 +0,0 @@
import { EnvelopeType, RecipientRole } from '@prisma/client';
import { SendStatus, SigningStatus } from '@prisma/client';
import type { TRecipientAccessAuthTypes } from '@documenso/lib/types/document-auth';
import {
type TRecipientActionAuthTypes,
ZRecipientAuthOptionsSchema,
} from '@documenso/lib/types/document-auth';
import { createRecipientAuthOptions } from '@documenso/lib/utils/document-auth';
import { prisma } from '@documenso/prisma';
import { AppError, AppErrorCode } from '../../errors/app-error';
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
import { mapFieldToLegacyField } from '../../utils/fields';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
export interface UpdateTemplateRecipientsOptions {
userId: number;
teamId: number;
templateId: number;
recipients: {
id: number;
email?: string;
name?: string;
role?: RecipientRole;
signingOrder?: number | null;
accessAuth?: TRecipientAccessAuthTypes[];
actionAuth?: TRecipientActionAuthTypes[];
}[];
}
export const updateTemplateRecipients = async ({
userId,
teamId,
templateId,
recipients,
}: UpdateTemplateRecipientsOptions) => {
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'templateId',
id: templateId,
},
type: EnvelopeType.TEMPLATE,
userId,
teamId,
});
const envelope = await prisma.envelope.findFirst({
where: envelopeWhereInput,
include: {
recipients: true,
team: {
select: {
organisation: {
select: {
organisationClaim: true,
},
},
},
},
},
});
if (!envelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Template not found',
});
}
const recipientsHaveActionAuth = recipients.some(
(recipient) => recipient.actionAuth && recipient.actionAuth.length > 0,
);
// Check if user has permission to set the global action auth.
if (recipientsHaveActionAuth && !envelope.team.organisation.organisationClaim.flags.cfr21) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to set the action auth',
});
}
const recipientsToUpdate = recipients.map((recipient) => {
const originalRecipient = envelope.recipients.find(
(existingRecipient) => existingRecipient.id === recipient.id,
);
if (!originalRecipient) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: `Recipient with id ${recipient.id} not found`,
});
}
return {
originalRecipient,
recipientUpdateData: recipient,
};
});
const updatedRecipients = await prisma.$transaction(async (tx) => {
return await Promise.all(
recipientsToUpdate.map(async ({ originalRecipient, recipientUpdateData }) => {
let authOptions = ZRecipientAuthOptionsSchema.parse(originalRecipient.authOptions);
if (
recipientUpdateData.actionAuth !== undefined ||
recipientUpdateData.accessAuth !== undefined
) {
authOptions = createRecipientAuthOptions({
accessAuth: recipientUpdateData.accessAuth || authOptions.accessAuth,
actionAuth: recipientUpdateData.actionAuth || authOptions.actionAuth,
});
}
const mergedRecipient = {
...originalRecipient,
...recipientUpdateData,
};
const updatedRecipient = await tx.recipient.update({
where: {
id: originalRecipient.id,
envelopeId: envelope.id,
},
data: {
name: mergedRecipient.name,
email: mergedRecipient.email,
role: mergedRecipient.role,
signingOrder: mergedRecipient.signingOrder,
envelopeId: envelope.id,
sendStatus:
mergedRecipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
signingStatus:
mergedRecipient.role === RecipientRole.CC
? SigningStatus.SIGNED
: SigningStatus.NOT_SIGNED,
authOptions,
},
include: {
fields: true,
},
});
// Clear all fields if the recipient role is changed to a type that cannot have fields.
if (
originalRecipient.role !== updatedRecipient.role &&
(updatedRecipient.role === RecipientRole.CC ||
updatedRecipient.role === RecipientRole.VIEWER)
) {
await tx.field.deleteMany({
where: {
recipientId: updatedRecipient.id,
},
});
}
return updatedRecipient;
}),
);
});
return {
recipients: updatedRecipients.map((recipient) => ({
...recipient,
documentId: null,
templateId: mapSecondaryIdToTemplateId(envelope.secondaryId),
fields: recipient.fields.map((field) => mapFieldToLegacyField(field, envelope)),
})),
};
};

View File

@ -262,10 +262,20 @@ msgstr "{prefix} hat ein Feld hinzugefügt"
msgid "{prefix} added a recipient" msgid "{prefix} added a recipient"
msgstr "{prefix} hat einen Empfänger hinzugefügt" msgstr "{prefix} hat einen Empfänger hinzugefügt"
#. placeholder {0}: data.envelopeItemTitle
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} created an envelope item with title {0}"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts #: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} created the document" msgid "{prefix} created the document"
msgstr "{prefix} hat das Dokument erstellt" msgstr "{prefix} hat das Dokument erstellt"
#. placeholder {0}: data.envelopeItemTitle
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} deleted an envelope item with title {0}"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts #: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} deleted the document" msgid "{prefix} deleted the document"
msgstr "{prefix} hat das Dokument gelöscht" msgstr "{prefix} hat das Dokument gelöscht"
@ -356,6 +366,7 @@ msgstr "{recipientActionVerb} Dokument"
msgid "{recipientActionVerb} the document to complete the process." msgid "{recipientActionVerb} the document to complete the process."
msgstr "{recipientActionVerb} das Dokument, um den Prozess abzuschließen." msgstr "{recipientActionVerb} das Dokument, um den Prozess abzuschließen."
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx #: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
msgid "{recipientCount} recipients" msgid "{recipientCount} recipients"
msgstr "{recipientCount} Empfänger" msgstr "{recipientCount} Empfänger"
@ -1742,8 +1753,9 @@ msgstr ""
#: apps/remix/app/components/general/document/document-attachments-popover.tsx #: apps/remix/app/components/general/document/document-attachments-popover.tsx
msgid "Attachment removed successfully." msgid "Attachment removed successfully."
msgstr "" msgstr "<<<<<<< Updated upstream======="
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
#: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx #: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx
#: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx #: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx
#: apps/remix/app/components/general/document/document-attachments-popover.tsx #: apps/remix/app/components/general/document/document-attachments-popover.tsx
@ -2158,6 +2170,10 @@ msgstr "Filter löschen"
msgid "Clear Signature" msgid "Clear Signature"
msgstr "Unterschrift löschen" msgstr "Unterschrift löschen"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
msgid "Click here to add a recipient"
msgstr ""
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx #: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
msgid "Click here to get started" msgid "Click here to get started"
msgstr "Klicken Sie hier, um zu beginnen" msgstr "Klicken Sie hier, um zu beginnen"
@ -2280,6 +2296,7 @@ msgstr "Abgeschlossene Dokumente"
msgid "Completed Documents" msgid "Completed Documents"
msgstr "Abgeschlossene Dokumente" msgstr "Abgeschlossene Dokumente"
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx #: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
msgid "Completed on {formattedDate}" msgid "Completed on {formattedDate}"
msgstr "Abgeschlossen am {formattedDate}" msgstr "Abgeschlossen am {formattedDate}"
@ -2479,7 +2496,6 @@ msgid "Controls which signatures are allowed to be used when signing a document.
msgstr "Bestimmt, welche Signaturen beim Unterschreiben eines Dokuments verwendet werden dürfen." msgstr "Bestimmt, welche Signaturen beim Unterschreiben eines Dokuments verwendet werden dürfen."
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Copied" msgid "Copied"
msgstr "Kopiert" msgstr "Kopiert"
@ -2497,14 +2513,12 @@ msgstr "Kopiert"
#: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx #: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx #: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx #: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/components/document/document-share-button.tsx #: packages/ui/components/document/document-share-button.tsx
msgid "Copied to clipboard" msgid "Copied to clipboard"
msgstr "In die Zwischenablage kopiert" msgstr "In die Zwischenablage kopiert"
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Copy" msgid "Copy"
msgstr "Kopieren" msgstr "Kopieren"
@ -2522,6 +2536,7 @@ msgid "Copy Shareable Link"
msgstr "Kopiere den teilbaren Link" msgstr "Kopiere den teilbaren Link"
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Copy Signing Links" msgid "Copy Signing Links"
msgstr "Signierlinks kopieren" msgstr "Signierlinks kopieren"
@ -3646,7 +3661,6 @@ msgstr "Legen Sie Ihr Dokument hier ab"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx
#: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/document-flow/add-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx
#: packages/lib/utils/fields.ts
msgid "Dropdown" msgid "Dropdown"
msgstr "Dropdown" msgstr "Dropdown"
@ -4035,6 +4049,14 @@ msgstr ""
msgid "Envelope Item Count" msgid "Envelope Item Count"
msgstr "" msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Envelope item created"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Envelope item deleted"
msgstr ""
#: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx #: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx
msgid "Envelope resent" msgid "Envelope resent"
msgstr "" msgstr ""
@ -5544,7 +5566,6 @@ msgstr "Kein passender Empfänger mit dieser Beschreibung gefunden."
#: apps/remix/app/components/general/template/template-page-view-recipients.tsx #: apps/remix/app/components/general/template/template-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "No recipients" msgid "No recipients"
msgstr "Keine Empfänger" msgstr "Keine Empfänger"
@ -7066,6 +7087,10 @@ msgstr "Wählen Sie Mitglieder oder Gruppen von Mitgliedern, die dem Team hinzug
msgid "Select members to add to this team" msgid "Select members to add to this team"
msgstr "Wählen Sie Mitglieder aus, die diesem Team hinzugefügt werden sollen" msgstr "Wählen Sie Mitglieder aus, die diesem Team hinzugefügt werden sollen"
#: packages/lib/utils/fields.ts
msgid "Select Option"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx #: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Select passkey" msgid "Select passkey"
msgstr "Passkey auswählen" msgstr "Passkey auswählen"
@ -7870,6 +7895,15 @@ msgstr "E-Mail-Domains synchronisieren"
msgid "Sync failed, changes not saved" msgid "Sync failed, changes not saved"
msgstr "" msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
msgid "System auto inserted fields"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "System auto inserted fields"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx #: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "System Requirements" msgid "System Requirements"
msgstr "Systemanforderungen" msgstr "Systemanforderungen"
@ -8412,7 +8446,6 @@ msgstr "Der Name des Unterzeichners"
#: apps/remix/app/components/general/avatar-with-recipient.tsx #: apps/remix/app/components/general/avatar-with-recipient.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "The signing link has been copied to your clipboard." msgid "The signing link has been copied to your clipboard."
msgstr "Der Signierlink wurde in die Zwischenablage kopiert." msgstr "Der Signierlink wurde in die Zwischenablage kopiert."

View File

@ -257,10 +257,20 @@ msgstr "{prefix} added a field"
msgid "{prefix} added a recipient" msgid "{prefix} added a recipient"
msgstr "{prefix} added a recipient" msgstr "{prefix} added a recipient"
#. placeholder {0}: data.envelopeItemTitle
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} created an envelope item with title {0}"
msgstr "{prefix} created an envelope item with title {0}"
#: packages/lib/utils/document-audit-logs.ts #: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} created the document" msgid "{prefix} created the document"
msgstr "{prefix} created the document" msgstr "{prefix} created the document"
#. placeholder {0}: data.envelopeItemTitle
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} deleted an envelope item with title {0}"
msgstr "{prefix} deleted an envelope item with title {0}"
#: packages/lib/utils/document-audit-logs.ts #: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} deleted the document" msgid "{prefix} deleted the document"
msgstr "{prefix} deleted the document" msgstr "{prefix} deleted the document"
@ -351,6 +361,7 @@ msgstr "{recipientActionVerb} document"
msgid "{recipientActionVerb} the document to complete the process." msgid "{recipientActionVerb} the document to complete the process."
msgstr "{recipientActionVerb} the document to complete the process." msgstr "{recipientActionVerb} the document to complete the process."
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx #: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
msgid "{recipientCount} recipients" msgid "{recipientCount} recipients"
msgstr "{recipientCount} recipients" msgstr "{recipientCount} recipients"
@ -1737,8 +1748,9 @@ msgstr "Attachment added successfully."
#: apps/remix/app/components/general/document/document-attachments-popover.tsx #: apps/remix/app/components/general/document/document-attachments-popover.tsx
msgid "Attachment removed successfully." msgid "Attachment removed successfully."
msgstr "Attachment removed successfully." msgstr "Attachment removed successfully.<<<<<<< Updated upstream======="
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
#: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx #: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx
#: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx #: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx
#: apps/remix/app/components/general/document/document-attachments-popover.tsx #: apps/remix/app/components/general/document/document-attachments-popover.tsx
@ -2153,6 +2165,10 @@ msgstr "Clear filters"
msgid "Clear Signature" msgid "Clear Signature"
msgstr "Clear Signature" msgstr "Clear Signature"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
msgid "Click here to add a recipient"
msgstr "Click here to add a recipient"
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx #: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
msgid "Click here to get started" msgid "Click here to get started"
msgstr "Click here to get started" msgstr "Click here to get started"
@ -2275,6 +2291,7 @@ msgstr "Completed documents"
msgid "Completed Documents" msgid "Completed Documents"
msgstr "Completed Documents" msgstr "Completed Documents"
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx #: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
msgid "Completed on {formattedDate}" msgid "Completed on {formattedDate}"
msgstr "Completed on {formattedDate}" msgstr "Completed on {formattedDate}"
@ -2474,7 +2491,6 @@ msgid "Controls which signatures are allowed to be used when signing a document.
msgstr "Controls which signatures are allowed to be used when signing a document." msgstr "Controls which signatures are allowed to be used when signing a document."
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Copied" msgid "Copied"
msgstr "Copied" msgstr "Copied"
@ -2492,14 +2508,12 @@ msgstr "Copied"
#: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx #: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx #: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx #: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/components/document/document-share-button.tsx #: packages/ui/components/document/document-share-button.tsx
msgid "Copied to clipboard" msgid "Copied to clipboard"
msgstr "Copied to clipboard" msgstr "Copied to clipboard"
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Copy" msgid "Copy"
msgstr "Copy" msgstr "Copy"
@ -2517,6 +2531,7 @@ msgid "Copy Shareable Link"
msgstr "Copy Shareable Link" msgstr "Copy Shareable Link"
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Copy Signing Links" msgid "Copy Signing Links"
msgstr "Copy Signing Links" msgstr "Copy Signing Links"
@ -3641,7 +3656,6 @@ msgstr "Drop your document here"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx
#: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/document-flow/add-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx
#: packages/lib/utils/fields.ts
msgid "Dropdown" msgid "Dropdown"
msgstr "Dropdown" msgstr "Dropdown"
@ -4030,6 +4044,14 @@ msgstr "Envelope ID"
msgid "Envelope Item Count" msgid "Envelope Item Count"
msgstr "Envelope Item Count" msgstr "Envelope Item Count"
#: packages/lib/utils/document-audit-logs.ts
msgid "Envelope item created"
msgstr "Envelope item created"
#: packages/lib/utils/document-audit-logs.ts
msgid "Envelope item deleted"
msgstr "Envelope item deleted"
#: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx #: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx
msgid "Envelope resent" msgid "Envelope resent"
msgstr "Envelope resent" msgstr "Envelope resent"
@ -5539,7 +5561,6 @@ msgstr "No recipient matching this description was found."
#: apps/remix/app/components/general/template/template-page-view-recipients.tsx #: apps/remix/app/components/general/template/template-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "No recipients" msgid "No recipients"
msgstr "No recipients" msgstr "No recipients"
@ -7061,6 +7082,10 @@ msgstr "Select members or groups of members to add to the team."
msgid "Select members to add to this team" msgid "Select members to add to this team"
msgstr "Select members to add to this team" msgstr "Select members to add to this team"
#: packages/lib/utils/fields.ts
msgid "Select Option"
msgstr "Select Option"
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx #: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Select passkey" msgid "Select passkey"
msgstr "Select passkey" msgstr "Select passkey"
@ -7865,6 +7890,15 @@ msgstr "Sync Email Domains"
msgid "Sync failed, changes not saved" msgid "Sync failed, changes not saved"
msgstr "Sync failed, changes not saved" msgstr "Sync failed, changes not saved"
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
msgid "System auto inserted fields"
msgstr "System auto inserted fields"
#: packages/lib/utils/document-audit-logs.ts
msgid "System auto inserted fields"
msgstr "System auto inserted fields"
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx #: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "System Requirements" msgid "System Requirements"
msgstr "System Requirements" msgstr "System Requirements"
@ -8417,7 +8451,6 @@ msgstr "The signer's name"
#: apps/remix/app/components/general/avatar-with-recipient.tsx #: apps/remix/app/components/general/avatar-with-recipient.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "The signing link has been copied to your clipboard." msgid "The signing link has been copied to your clipboard."
msgstr "The signing link has been copied to your clipboard." msgstr "The signing link has been copied to your clipboard."

View File

@ -262,10 +262,20 @@ msgstr "{prefix} agregó un campo"
msgid "{prefix} added a recipient" msgid "{prefix} added a recipient"
msgstr "{prefix} agregó un destinatario" msgstr "{prefix} agregó un destinatario"
#. placeholder {0}: data.envelopeItemTitle
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} created an envelope item with title {0}"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts #: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} created the document" msgid "{prefix} created the document"
msgstr "{prefix} creó el documento" msgstr "{prefix} creó el documento"
#. placeholder {0}: data.envelopeItemTitle
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} deleted an envelope item with title {0}"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts #: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} deleted the document" msgid "{prefix} deleted the document"
msgstr "{prefix} eliminó el documento" msgstr "{prefix} eliminó el documento"
@ -356,6 +366,7 @@ msgstr "{recipientActionVerb} documento"
msgid "{recipientActionVerb} the document to complete the process." msgid "{recipientActionVerb} the document to complete the process."
msgstr "{recipientActionVerb} el documento para completar el proceso." msgstr "{recipientActionVerb} el documento para completar el proceso."
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx #: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
msgid "{recipientCount} recipients" msgid "{recipientCount} recipients"
msgstr "{recipientCount} destinatarios" msgstr "{recipientCount} destinatarios"
@ -1742,8 +1753,9 @@ msgstr ""
#: apps/remix/app/components/general/document/document-attachments-popover.tsx #: apps/remix/app/components/general/document/document-attachments-popover.tsx
msgid "Attachment removed successfully." msgid "Attachment removed successfully."
msgstr "" msgstr "<<<<<<< Updated upstream======="
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
#: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx #: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx
#: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx #: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx
#: apps/remix/app/components/general/document/document-attachments-popover.tsx #: apps/remix/app/components/general/document/document-attachments-popover.tsx
@ -2158,6 +2170,10 @@ msgstr "Limpiar filtros"
msgid "Clear Signature" msgid "Clear Signature"
msgstr "Limpiar firma" msgstr "Limpiar firma"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
msgid "Click here to add a recipient"
msgstr ""
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx #: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
msgid "Click here to get started" msgid "Click here to get started"
msgstr "Haga clic aquí para comenzar" msgstr "Haga clic aquí para comenzar"
@ -2280,6 +2296,7 @@ msgstr "Documentos completados"
msgid "Completed Documents" msgid "Completed Documents"
msgstr "Documentos Completados" msgstr "Documentos Completados"
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx #: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
msgid "Completed on {formattedDate}" msgid "Completed on {formattedDate}"
msgstr "Completado el {formattedDate}" msgstr "Completado el {formattedDate}"
@ -2479,7 +2496,6 @@ msgid "Controls which signatures are allowed to be used when signing a document.
msgstr "Controla qué firmas están permitidas al firmar un documento." msgstr "Controla qué firmas están permitidas al firmar un documento."
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Copied" msgid "Copied"
msgstr "Copiado" msgstr "Copiado"
@ -2497,14 +2513,12 @@ msgstr "Copiado"
#: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx #: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx #: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx #: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/components/document/document-share-button.tsx #: packages/ui/components/document/document-share-button.tsx
msgid "Copied to clipboard" msgid "Copied to clipboard"
msgstr "Copiado al portapapeles" msgstr "Copiado al portapapeles"
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Copy" msgid "Copy"
msgstr "Copiar" msgstr "Copiar"
@ -2522,6 +2536,7 @@ msgid "Copy Shareable Link"
msgstr "Copiar enlace compartible" msgstr "Copiar enlace compartible"
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Copy Signing Links" msgid "Copy Signing Links"
msgstr "Copiar enlaces de firma" msgstr "Copiar enlaces de firma"
@ -3646,7 +3661,6 @@ msgstr "Suelta tu documento aquí"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx
#: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/document-flow/add-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx
#: packages/lib/utils/fields.ts
msgid "Dropdown" msgid "Dropdown"
msgstr "Menú desplegable" msgstr "Menú desplegable"
@ -4035,6 +4049,14 @@ msgstr ""
msgid "Envelope Item Count" msgid "Envelope Item Count"
msgstr "" msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Envelope item created"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Envelope item deleted"
msgstr ""
#: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx #: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx
msgid "Envelope resent" msgid "Envelope resent"
msgstr "" msgstr ""
@ -5544,7 +5566,6 @@ msgstr "No se encontró ningún destinatario que coincidiera con esta descripci
#: apps/remix/app/components/general/template/template-page-view-recipients.tsx #: apps/remix/app/components/general/template/template-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "No recipients" msgid "No recipients"
msgstr "Sin destinatarios" msgstr "Sin destinatarios"
@ -7066,6 +7087,10 @@ msgstr "Seleccione miembros o grupos de miembros para agregar al equipo."
msgid "Select members to add to this team" msgid "Select members to add to this team"
msgstr "Seleccione los miembros para añadir a este equipo" msgstr "Seleccione los miembros para añadir a este equipo"
#: packages/lib/utils/fields.ts
msgid "Select Option"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx #: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Select passkey" msgid "Select passkey"
msgstr "Seleccionar clave de acceso" msgstr "Seleccionar clave de acceso"
@ -7870,6 +7895,15 @@ msgstr "Sincronizar dominios de correo electrónico"
msgid "Sync failed, changes not saved" msgid "Sync failed, changes not saved"
msgstr "" msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
msgid "System auto inserted fields"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "System auto inserted fields"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx #: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "System Requirements" msgid "System Requirements"
msgstr "Requisitos del Sistema" msgstr "Requisitos del Sistema"
@ -8412,7 +8446,6 @@ msgstr "El nombre del firmante"
#: apps/remix/app/components/general/avatar-with-recipient.tsx #: apps/remix/app/components/general/avatar-with-recipient.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "The signing link has been copied to your clipboard." msgid "The signing link has been copied to your clipboard."
msgstr "El enlace de firma ha sido copiado a tu portapapeles." msgstr "El enlace de firma ha sido copiado a tu portapapeles."

View File

@ -262,10 +262,20 @@ msgstr "{prefix} a ajouté un champ"
msgid "{prefix} added a recipient" msgid "{prefix} added a recipient"
msgstr "{prefix} a ajouté un destinataire" msgstr "{prefix} a ajouté un destinataire"
#. placeholder {0}: data.envelopeItemTitle
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} created an envelope item with title {0}"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts #: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} created the document" msgid "{prefix} created the document"
msgstr "{prefix} a créé le document" msgstr "{prefix} a créé le document"
#. placeholder {0}: data.envelopeItemTitle
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} deleted an envelope item with title {0}"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts #: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} deleted the document" msgid "{prefix} deleted the document"
msgstr "{prefix} a supprimé le document" msgstr "{prefix} a supprimé le document"
@ -356,6 +366,7 @@ msgstr "{recipientActionVerb} document"
msgid "{recipientActionVerb} the document to complete the process." msgid "{recipientActionVerb} the document to complete the process."
msgstr "{recipientActionVerb} the document to complete the process." msgstr "{recipientActionVerb} the document to complete the process."
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx #: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
msgid "{recipientCount} recipients" msgid "{recipientCount} recipients"
msgstr "{recipientCount} destinataires" msgstr "{recipientCount} destinataires"
@ -1742,8 +1753,9 @@ msgstr ""
#: apps/remix/app/components/general/document/document-attachments-popover.tsx #: apps/remix/app/components/general/document/document-attachments-popover.tsx
msgid "Attachment removed successfully." msgid "Attachment removed successfully."
msgstr "" msgstr "<<<<<<< Updated upstream======="
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
#: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx #: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx
#: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx #: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx
#: apps/remix/app/components/general/document/document-attachments-popover.tsx #: apps/remix/app/components/general/document/document-attachments-popover.tsx
@ -2158,6 +2170,10 @@ msgstr "Effacer les filtres"
msgid "Clear Signature" msgid "Clear Signature"
msgstr "Effacer la signature" msgstr "Effacer la signature"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
msgid "Click here to add a recipient"
msgstr ""
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx #: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
msgid "Click here to get started" msgid "Click here to get started"
msgstr "Cliquez ici pour commencer" msgstr "Cliquez ici pour commencer"
@ -2280,6 +2296,7 @@ msgstr "Documents complétés"
msgid "Completed Documents" msgid "Completed Documents"
msgstr "Documents Complétés" msgstr "Documents Complétés"
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx #: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
msgid "Completed on {formattedDate}" msgid "Completed on {formattedDate}"
msgstr "Terminé le {formattedDate}" msgstr "Terminé le {formattedDate}"
@ -2479,7 +2496,6 @@ msgid "Controls which signatures are allowed to be used when signing a document.
msgstr "Contrôle quelles signatures sont autorisées lors de la signature d'un document." msgstr "Contrôle quelles signatures sont autorisées lors de la signature d'un document."
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Copied" msgid "Copied"
msgstr "Copié" msgstr "Copié"
@ -2497,14 +2513,12 @@ msgstr "Copié"
#: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx #: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx #: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx #: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/components/document/document-share-button.tsx #: packages/ui/components/document/document-share-button.tsx
msgid "Copied to clipboard" msgid "Copied to clipboard"
msgstr "Copié dans le presse-papiers" msgstr "Copié dans le presse-papiers"
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Copy" msgid "Copy"
msgstr "Copier" msgstr "Copier"
@ -2522,6 +2536,7 @@ msgid "Copy Shareable Link"
msgstr "Copier le lien partageable" msgstr "Copier le lien partageable"
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Copy Signing Links" msgid "Copy Signing Links"
msgstr "Copier les liens de signature" msgstr "Copier les liens de signature"
@ -3646,7 +3661,6 @@ msgstr "Déposez votre document ici"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx
#: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/document-flow/add-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx
#: packages/lib/utils/fields.ts
msgid "Dropdown" msgid "Dropdown"
msgstr "Liste déroulante" msgstr "Liste déroulante"
@ -4035,6 +4049,14 @@ msgstr ""
msgid "Envelope Item Count" msgid "Envelope Item Count"
msgstr "" msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Envelope item created"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Envelope item deleted"
msgstr ""
#: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx #: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx
msgid "Envelope resent" msgid "Envelope resent"
msgstr "" msgstr ""
@ -5544,7 +5566,6 @@ msgstr "Aucun destinataire correspondant à cette description n'a été trouvé.
#: apps/remix/app/components/general/template/template-page-view-recipients.tsx #: apps/remix/app/components/general/template/template-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "No recipients" msgid "No recipients"
msgstr "Aucun destinataire" msgstr "Aucun destinataire"
@ -7066,6 +7087,10 @@ msgstr "Sélectionnez des membres ou groupes de membres à ajouter à l'équipe.
msgid "Select members to add to this team" msgid "Select members to add to this team"
msgstr "Sélectionnez des membres à ajouter à cette équipe" msgstr "Sélectionnez des membres à ajouter à cette équipe"
#: packages/lib/utils/fields.ts
msgid "Select Option"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx #: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Select passkey" msgid "Select passkey"
msgstr "Sélectionner la clé d'authentification" msgstr "Sélectionner la clé d'authentification"
@ -7870,6 +7895,15 @@ msgstr "Synchroniser les domaines de messagerie"
msgid "Sync failed, changes not saved" msgid "Sync failed, changes not saved"
msgstr "" msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
msgid "System auto inserted fields"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "System auto inserted fields"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx #: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "System Requirements" msgid "System Requirements"
msgstr "Exigences du système" msgstr "Exigences du système"
@ -8412,7 +8446,6 @@ msgstr "Le nom du signataire"
#: apps/remix/app/components/general/avatar-with-recipient.tsx #: apps/remix/app/components/general/avatar-with-recipient.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "The signing link has been copied to your clipboard." msgid "The signing link has been copied to your clipboard."
msgstr "Le lien de signature a été copié dans votre presse-papiers." msgstr "Le lien de signature a été copié dans votre presse-papiers."

View File

@ -262,10 +262,20 @@ msgstr "{prefix} ha aggiunto un campo"
msgid "{prefix} added a recipient" msgid "{prefix} added a recipient"
msgstr "{prefix} ha aggiunto un destinatario" msgstr "{prefix} ha aggiunto un destinatario"
#. placeholder {0}: data.envelopeItemTitle
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} created an envelope item with title {0}"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts #: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} created the document" msgid "{prefix} created the document"
msgstr "{prefix} ha creato il documento" msgstr "{prefix} ha creato il documento"
#. placeholder {0}: data.envelopeItemTitle
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} deleted an envelope item with title {0}"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts #: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} deleted the document" msgid "{prefix} deleted the document"
msgstr "{prefix} ha eliminato il documento" msgstr "{prefix} ha eliminato il documento"
@ -356,6 +366,7 @@ msgstr "{recipientActionVerb} documento"
msgid "{recipientActionVerb} the document to complete the process." msgid "{recipientActionVerb} the document to complete the process."
msgstr "{recipientActionVerb} il documento per completare il processo." msgstr "{recipientActionVerb} il documento per completare il processo."
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx #: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
msgid "{recipientCount} recipients" msgid "{recipientCount} recipients"
msgstr "{recipientCount} destinatari" msgstr "{recipientCount} destinatari"
@ -1742,8 +1753,9 @@ msgstr ""
#: apps/remix/app/components/general/document/document-attachments-popover.tsx #: apps/remix/app/components/general/document/document-attachments-popover.tsx
msgid "Attachment removed successfully." msgid "Attachment removed successfully."
msgstr "" msgstr "<<<<<<< Updated upstream======="
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
#: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx #: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx
#: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx #: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx
#: apps/remix/app/components/general/document/document-attachments-popover.tsx #: apps/remix/app/components/general/document/document-attachments-popover.tsx
@ -2158,6 +2170,10 @@ msgstr "Cancella filtri"
msgid "Clear Signature" msgid "Clear Signature"
msgstr "Cancella firma" msgstr "Cancella firma"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
msgid "Click here to add a recipient"
msgstr ""
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx #: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
msgid "Click here to get started" msgid "Click here to get started"
msgstr "Clicca qui per iniziare" msgstr "Clicca qui per iniziare"
@ -2280,6 +2296,7 @@ msgstr "Documenti Completati"
msgid "Completed Documents" msgid "Completed Documents"
msgstr "Documenti Completati" msgstr "Documenti Completati"
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx #: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
msgid "Completed on {formattedDate}" msgid "Completed on {formattedDate}"
msgstr "Completato il {formattedDate}" msgstr "Completato il {formattedDate}"
@ -2479,7 +2496,6 @@ msgid "Controls which signatures are allowed to be used when signing a document.
msgstr "Controlla quali firme sono consentite per firmare un documento." msgstr "Controlla quali firme sono consentite per firmare un documento."
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Copied" msgid "Copied"
msgstr "Copiato" msgstr "Copiato"
@ -2497,14 +2513,12 @@ msgstr "Copiato"
#: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx #: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx #: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx #: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/components/document/document-share-button.tsx #: packages/ui/components/document/document-share-button.tsx
msgid "Copied to clipboard" msgid "Copied to clipboard"
msgstr "Copiato negli appunti" msgstr "Copiato negli appunti"
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Copy" msgid "Copy"
msgstr "Copia" msgstr "Copia"
@ -2522,6 +2536,7 @@ msgid "Copy Shareable Link"
msgstr "Copia il Link Condivisibile" msgstr "Copia il Link Condivisibile"
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Copy Signing Links" msgid "Copy Signing Links"
msgstr "Copia link di firma" msgstr "Copia link di firma"
@ -3646,7 +3661,6 @@ msgstr "Rilascia qui il tuo documento"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx
#: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/document-flow/add-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx
#: packages/lib/utils/fields.ts
msgid "Dropdown" msgid "Dropdown"
msgstr "Menu a tendina" msgstr "Menu a tendina"
@ -4035,6 +4049,14 @@ msgstr ""
msgid "Envelope Item Count" msgid "Envelope Item Count"
msgstr "" msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Envelope item created"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Envelope item deleted"
msgstr ""
#: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx #: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx
msgid "Envelope resent" msgid "Envelope resent"
msgstr "" msgstr ""
@ -5544,7 +5566,6 @@ msgstr "Nessun destinatario corrispondente a questa descrizione è stato trovato
#: apps/remix/app/components/general/template/template-page-view-recipients.tsx #: apps/remix/app/components/general/template/template-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "No recipients" msgid "No recipients"
msgstr "Nessun destinatario" msgstr "Nessun destinatario"
@ -7066,6 +7087,10 @@ msgstr "Seleziona membri o gruppi di membri da aggiungere al team."
msgid "Select members to add to this team" msgid "Select members to add to this team"
msgstr "Seleziona membri da aggiungere a questo team" msgstr "Seleziona membri da aggiungere a questo team"
#: packages/lib/utils/fields.ts
msgid "Select Option"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx #: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Select passkey" msgid "Select passkey"
msgstr "Seleziona una chiave di accesso" msgstr "Seleziona una chiave di accesso"
@ -7870,6 +7895,15 @@ msgstr "Sincronizza Domini Email"
msgid "Sync failed, changes not saved" msgid "Sync failed, changes not saved"
msgstr "" msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
msgid "System auto inserted fields"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "System auto inserted fields"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx #: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "System Requirements" msgid "System Requirements"
msgstr "Requisiti di sistema" msgstr "Requisiti di sistema"
@ -8420,7 +8454,6 @@ msgstr "Il nome del firmatario"
#: apps/remix/app/components/general/avatar-with-recipient.tsx #: apps/remix/app/components/general/avatar-with-recipient.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "The signing link has been copied to your clipboard." msgid "The signing link has been copied to your clipboard."
msgstr "Il link di firma è stato copiato negli appunti." msgstr "Il link di firma è stato copiato negli appunti."

View File

@ -262,10 +262,20 @@ msgstr "Użytkownik {prefix} dodał pole"
msgid "{prefix} added a recipient" msgid "{prefix} added a recipient"
msgstr "Użytkownik {prefix} dodał odbiorcę" msgstr "Użytkownik {prefix} dodał odbiorcę"
#. placeholder {0}: data.envelopeItemTitle
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} created an envelope item with title {0}"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts #: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} created the document" msgid "{prefix} created the document"
msgstr "Użytkownik {prefix} utworzył dokument" msgstr "Użytkownik {prefix} utworzył dokument"
#. placeholder {0}: data.envelopeItemTitle
#: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} deleted an envelope item with title {0}"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts #: packages/lib/utils/document-audit-logs.ts
msgid "{prefix} deleted the document" msgid "{prefix} deleted the document"
msgstr "Użytkownik {prefix} usunął dokument" msgstr "Użytkownik {prefix} usunął dokument"
@ -356,6 +366,7 @@ msgstr "{recipientActionVerb} dokument"
msgid "{recipientActionVerb} the document to complete the process." msgid "{recipientActionVerb} the document to complete the process."
msgstr "Sprawdź i {recipientActionVerb} dokument, aby zakończyć proces." msgstr "Sprawdź i {recipientActionVerb} dokument, aby zakończyć proces."
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx #: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
msgid "{recipientCount} recipients" msgid "{recipientCount} recipients"
msgstr "{recipientCount} odbiorców" msgstr "{recipientCount} odbiorców"
@ -1742,8 +1753,9 @@ msgstr ""
#: apps/remix/app/components/general/document/document-attachments-popover.tsx #: apps/remix/app/components/general/document/document-attachments-popover.tsx
msgid "Attachment removed successfully." msgid "Attachment removed successfully."
msgstr "" msgstr "<<<<<<< Updated upstream======="
#: apps/remix/app/components/general/document-signing/document-signing-page-view-v2.tsx
#: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx #: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx
#: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx #: apps/remix/app/components/general/document-signing/document-signing-attachments-popover.tsx
#: apps/remix/app/components/general/document/document-attachments-popover.tsx #: apps/remix/app/components/general/document/document-attachments-popover.tsx
@ -2158,6 +2170,10 @@ msgstr "Wyczyść filtry"
msgid "Clear Signature" msgid "Clear Signature"
msgstr "Wyczyść podpis" msgstr "Wyczyść podpis"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-page.tsx
msgid "Click here to add a recipient"
msgstr ""
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx #: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
msgid "Click here to get started" msgid "Click here to get started"
msgstr "Kliknij, aby rozpocząć" msgstr "Kliknij, aby rozpocząć"
@ -2280,6 +2296,7 @@ msgstr "Dokumenty zakończone"
msgid "Completed Documents" msgid "Completed Documents"
msgstr "Zakończone dokumenty" msgstr "Zakończone dokumenty"
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
#: apps/remix/app/components/general/document/document-certificate-qr-view.tsx #: apps/remix/app/components/general/document/document-certificate-qr-view.tsx
msgid "Completed on {formattedDate}" msgid "Completed on {formattedDate}"
msgstr "Zakończono {formattedDate}" msgstr "Zakończono {formattedDate}"
@ -2479,7 +2496,6 @@ msgid "Controls which signatures are allowed to be used when signing a document.
msgstr "Kontroluje, które podpisy są dozwolone do użycia podczas podpisywania dokumentu." msgstr "Kontroluje, które podpisy są dozwolone do użycia podczas podpisywania dokumentu."
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Copied" msgid "Copied"
msgstr "Skopiowano" msgstr "Skopiowano"
@ -2497,14 +2513,12 @@ msgstr "Skopiowano"
#: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx #: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx #: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx
#: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx #: apps/remix/app/components/dialogs/organisation-email-domain-records-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/components/document/document-share-button.tsx #: packages/ui/components/document/document-share-button.tsx
msgid "Copied to clipboard" msgid "Copied to clipboard"
msgstr "Skopiowano do schowka" msgstr "Skopiowano do schowka"
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Copy" msgid "Copy"
msgstr "Kopiuj" msgstr "Kopiuj"
@ -2522,6 +2536,7 @@ msgid "Copy Shareable Link"
msgstr "Kopiuj udostępniany link" msgstr "Kopiuj udostępniany link"
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Copy Signing Links" msgid "Copy Signing Links"
msgstr "Kopiuj linki do podpisania" msgstr "Kopiuj linki do podpisania"
@ -3646,7 +3661,6 @@ msgstr "Upuść swój dokument tutaj"
#: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx #: apps/remix/app/components/general/envelope-editor/envelope-editor-fields-drag-drop.tsx
#: packages/ui/primitives/template-flow/add-template-fields.tsx #: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/document-flow/add-fields.tsx #: packages/ui/primitives/document-flow/add-fields.tsx
#: packages/lib/utils/fields.ts
msgid "Dropdown" msgid "Dropdown"
msgstr "Lista rozwijana" msgstr "Lista rozwijana"
@ -4035,6 +4049,14 @@ msgstr ""
msgid "Envelope Item Count" msgid "Envelope Item Count"
msgstr "" msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Envelope item created"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "Envelope item deleted"
msgstr ""
#: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx #: apps/remix/app/components/dialogs/envelope-redistribute-dialog.tsx
msgid "Envelope resent" msgid "Envelope resent"
msgstr "" msgstr ""
@ -5544,7 +5566,6 @@ msgstr "Nie znaleziono odbiorcy pasującego do tego opisu."
#: apps/remix/app/components/general/template/template-page-view-recipients.tsx #: apps/remix/app/components/general/template/template-page-view-recipients.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "No recipients" msgid "No recipients"
msgstr "Brak odbiorców" msgstr "Brak odbiorców"
@ -7066,6 +7087,10 @@ msgstr "Wybierz członków lub grupy członków, aby dodać do zespołu."
msgid "Select members to add to this team" msgid "Select members to add to this team"
msgstr "Wybierz członków, aby dodać do tego zespołu" msgstr "Wybierz członków, aby dodać do tego zespołu"
#: packages/lib/utils/fields.ts
msgid "Select Option"
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx #: apps/remix/app/components/general/document-signing/document-signing-auth-passkey.tsx
msgid "Select passkey" msgid "Select passkey"
msgstr "Wybierz klucz uwierzytelniający" msgstr "Wybierz klucz uwierzytelniający"
@ -7870,6 +7895,15 @@ msgstr "Synchronizuj domeny e-mail"
msgid "Sync failed, changes not saved" msgid "Sync failed, changes not saved"
msgstr "" msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgctxt "Audit log format"
msgid "System auto inserted fields"
msgstr ""
#: packages/lib/utils/document-audit-logs.ts
msgid "System auto inserted fields"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx #: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "System Requirements" msgid "System Requirements"
msgstr "Wymagania systemowe" msgstr "Wymagania systemowe"
@ -8412,7 +8446,6 @@ msgstr "Nazwa podpisującego"
#: apps/remix/app/components/general/avatar-with-recipient.tsx #: apps/remix/app/components/general/avatar-with-recipient.tsx
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx #: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx #: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: apps/remix/app/components/dialogs/envelope-distribute-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx #: packages/ui/primitives/document-flow/add-subject.tsx
msgid "The signing link has been copied to your clipboard." msgid "The signing link has been copied to your clipboard."
msgstr "Link do podpisu został skopiowany do schowka." msgstr "Link do podpisu został skopiowany do schowka."

View File

@ -21,10 +21,14 @@ export const ZDocumentAuditLogTypeSchema = z.enum([
'RECIPIENT_DELETED', 'RECIPIENT_DELETED',
'RECIPIENT_UPDATED', 'RECIPIENT_UPDATED',
'ENVELOPE_ITEM_CREATED',
'ENVELOPE_ITEM_DELETED',
// Document events. // Document events.
'DOCUMENT_COMPLETED', // When the document is sealed and fully completed. 'DOCUMENT_COMPLETED', // When the document is sealed and fully completed.
'DOCUMENT_CREATED', // When the document is created. 'DOCUMENT_CREATED', // When the document is created.
'DOCUMENT_DELETED', // When the document is soft deleted. 'DOCUMENT_DELETED', // When the document is soft deleted.
'DOCUMENT_FIELDS_AUTO_INSERTED', // When a field is auto inserted during send due to default values (radio/dropdown/checkbox).
'DOCUMENT_FIELD_INSERTED', // When a field is inserted (signed/approved/etc) by a recipient. 'DOCUMENT_FIELD_INSERTED', // When a field is inserted (signed/approved/etc) by a recipient.
'DOCUMENT_FIELD_UNINSERTED', // When a field is uninserted by a recipient. 'DOCUMENT_FIELD_UNINSERTED', // When a field is uninserted by a recipient.
'DOCUMENT_FIELD_PREFILLED', // When a field is prefilled by an assistant. 'DOCUMENT_FIELD_PREFILLED', // When a field is prefilled by an assistant.
@ -181,6 +185,28 @@ const ZBaseRecipientDataSchema = z.object({
recipientRole: z.string(), recipientRole: z.string(),
}); });
/**
* Event: Envelope item created.
*/
export const ZDocumentAuditLogEventEnvelopeItemCreatedSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_CREATED),
data: z.object({
envelopeItemId: z.string(),
envelopeItemTitle: z.string(),
}),
});
/**
* Event: Envelope item deleted.
*/
export const ZDocumentAuditLogEventEnvelopeItemDeletedSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_DELETED),
data: z.object({
envelopeItemId: z.string(),
envelopeItemTitle: z.string(),
}),
});
/** /**
* Event: Email sent. * Event: Email sent.
*/ */
@ -315,6 +341,22 @@ export const ZDocumentAuditLogEventDocumentFieldInsertedSchema = z.object({
}), }),
}); });
/**
* Event: Document field auto inserted.
*/
export const ZDocumentAuditLogEventDocumentFieldsAutoInsertedSchema = z.object({
type: z.literal(DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELDS_AUTO_INSERTED),
data: z.object({
fields: z.array(
z.object({
fieldId: z.number(),
fieldType: z.nativeEnum(FieldType),
recipientId: z.number(),
}),
),
}),
});
/** /**
* Event: Document field uninserted. * Event: Document field uninserted.
*/ */
@ -652,11 +694,14 @@ export const ZDocumentAuditLogBaseSchema = z.object({
export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and( export const ZDocumentAuditLogSchema = ZDocumentAuditLogBaseSchema.and(
z.union([ z.union([
ZDocumentAuditLogEventEnvelopeItemCreatedSchema,
ZDocumentAuditLogEventEnvelopeItemDeletedSchema,
ZDocumentAuditLogEventEmailSentSchema, ZDocumentAuditLogEventEmailSentSchema,
ZDocumentAuditLogEventDocumentCompletedSchema, ZDocumentAuditLogEventDocumentCompletedSchema,
ZDocumentAuditLogEventDocumentCreatedSchema, ZDocumentAuditLogEventDocumentCreatedSchema,
ZDocumentAuditLogEventDocumentDeletedSchema, ZDocumentAuditLogEventDocumentDeletedSchema,
ZDocumentAuditLogEventDocumentMovedToTeamSchema, ZDocumentAuditLogEventDocumentMovedToTeamSchema,
ZDocumentAuditLogEventDocumentFieldsAutoInsertedSchema,
ZDocumentAuditLogEventDocumentFieldInsertedSchema, ZDocumentAuditLogEventDocumentFieldInsertedSchema,
ZDocumentAuditLogEventDocumentFieldUninsertedSchema, ZDocumentAuditLogEventDocumentFieldUninsertedSchema,
ZDocumentAuditLogEventDocumentFieldPrefilledSchema, ZDocumentAuditLogEventDocumentFieldPrefilledSchema,

View File

@ -37,11 +37,8 @@ export const ZEnvelopeSchema = EnvelopeSchema.pick({
userId: true, userId: true,
teamId: true, teamId: true,
folderId: true, folderId: true,
templateId: true,
}).extend({ }).extend({
templateId: z
.number()
.nullish()
.describe('The ID of the template that the document was created from, if any.'),
documentMeta: DocumentMetaSchema.pick({ documentMeta: DocumentMetaSchema.pick({
signingOrder: true, signingOrder: true,
distributionMethod: true, distributionMethod: true,

View File

@ -50,6 +50,11 @@ export const ZFieldSchema = FieldSchema.pick({
templateId: z.number().nullish(), templateId: z.number().nullish(),
}); });
export const ZEnvelopeFieldSchema = ZFieldSchema.omit({
documentId: true,
templateId: true,
});
export const ZFieldPageNumberSchema = z export const ZFieldPageNumberSchema = z
.number() .number()
.min(1) .min(1)
@ -69,9 +74,32 @@ export const ZFieldWidthSchema = z.number().min(1).describe('The width of the fi
export const ZFieldHeightSchema = z.number().min(1).describe('The height of the field.'); export const ZFieldHeightSchema = z.number().min(1).describe('The height of the field.');
export const ZClampedFieldPageXSchema = z
.number()
.min(0)
.max(100)
.describe('The percentage based X coordinate where the field will be placed.');
export const ZClampedFieldPageYSchema = z
.number()
.min(0)
.max(100)
.describe('The percentage based Y coordinate where the field will be placed.');
export const ZClampedFieldWidthSchema = z
.number()
.min(0)
.max(100)
.describe('The percentage based width of the field on the page.');
export const ZClampedFieldHeightSchema = z
.number()
.min(0)
.max(100)
.describe('The percentage based height of the field on the page.');
// --------------------------------------------- // ---------------------------------------------
// Todo: Envelopes - dunno man
const PrismaDecimalSchema = z.preprocess( const PrismaDecimalSchema = z.preprocess(
(val) => (typeof val === 'string' ? new Prisma.Decimal(val) : val), (val) => (typeof val === 'string' ? new Prisma.Decimal(val) : val),
z.instanceof(Prisma.Decimal, { message: 'Must be a Decimal' }), z.instanceof(Prisma.Decimal, { message: 'Must be a Decimal' }),

View File

@ -95,3 +95,18 @@ export const ZRecipientManySchema = RecipientSchema.pick({
documentId: z.number().nullish(), documentId: z.number().nullish(),
templateId: z.number().nullish(), templateId: z.number().nullish(),
}); });
export const ZEnvelopeRecipientSchema = ZRecipientSchema.omit({
documentId: true,
templateId: true,
});
export const ZEnvelopeRecipientLiteSchema = ZRecipientLiteSchema.omit({
documentId: true,
templateId: true,
});
export const ZEnvelopeRecipientManySchema = ZRecipientManySchema.omit({
documentId: true,
templateId: true,
});

View File

@ -129,3 +129,58 @@ export const createSpinner = ({
return loadingGroup; return loadingGroup;
}; };
type CreateFieldHoverInteractionOptions = {
options: RenderFieldElementOptions;
fieldGroup: Konva.Group;
fieldRect: Konva.Rect;
};
/**
* Adds smooth transition-like behavior for hover effects to the field group and rectangle.
*/
export const createFieldHoverInteraction = ({
options,
fieldGroup,
fieldRect,
}: CreateFieldHoverInteractionOptions) => {
const { mode } = options;
if (mode === 'export' || !options.color) {
return;
}
const hoverColor = RECIPIENT_COLOR_STYLES[options.color].baseRingHover;
fieldGroup.on('mouseover', () => {
new Konva.Tween({
node: fieldRect,
duration: 0.3,
fill: hoverColor,
}).play();
});
fieldGroup.on('mouseout', () => {
new Konva.Tween({
node: fieldRect,
duration: 0.3,
fill: DEFAULT_RECT_BACKGROUND,
}).play();
});
fieldGroup.on('transformstart', () => {
new Konva.Tween({
node: fieldRect,
duration: 0.3,
fill: hoverColor,
}).play();
});
fieldGroup.on('transformend', () => {
new Konva.Tween({
node: fieldRect,
duration: 0.3,
fill: DEFAULT_RECT_BACKGROUND,
}).play();
});
};

View File

@ -4,6 +4,7 @@ import { match } from 'ts-pattern';
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf'; import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
import type { TCheckboxFieldMeta } from '../../types/field-meta'; import type { TCheckboxFieldMeta } from '../../types/field-meta';
import { import {
createFieldHoverInteraction,
konvaTextFill, konvaTextFill,
konvaTextFontFamily, konvaTextFontFamily,
upsertFieldGroup, upsertFieldGroup,
@ -26,25 +27,27 @@ export const renderCheckboxFieldElement = (
) => { ) => {
const { pageWidth, pageHeight, pageLayer, mode } = options; const { pageWidth, pageHeight, pageLayer, mode } = options;
const isFirstRender = !pageLayer.findOne(`#${field.renderId}`); const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
const fieldGroup = upsertFieldGroup(field, options);
// Clear previous children and listeners to re-render fresh.
fieldGroup.removeChildren();
fieldGroup.off('transform');
fieldGroup.add(upsertFieldRect(field, options));
const checkboxMeta: TCheckboxFieldMeta | null = (field.fieldMeta as TCheckboxFieldMeta) || null; const checkboxMeta: TCheckboxFieldMeta | null = (field.fieldMeta as TCheckboxFieldMeta) || null;
const checkboxValues = checkboxMeta?.values || []; const checkboxValues = checkboxMeta?.values || [];
const fontSize = checkboxMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE; const isFirstRender = !pageLayer.findOne(`#${field.renderId}`);
// Clear previous children and listeners to re-render fresh.
const fieldGroup = upsertFieldGroup(field, options);
fieldGroup.removeChildren();
fieldGroup.off('transform');
if (isFirstRender) { if (isFirstRender) {
pageLayer.add(fieldGroup); pageLayer.add(fieldGroup);
} }
const fieldRect = upsertFieldRect(field, options);
fieldGroup.add(fieldRect);
const fontSize = checkboxMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
// Handle rescaling items during transforms. // Handle rescaling items during transforms.
fieldGroup.on('transform', () => { fieldGroup.on('transform', () => {
const groupScaleX = fieldGroup.scaleX(); const groupScaleX = fieldGroup.scaleX();
@ -127,11 +130,9 @@ export const renderCheckboxFieldElement = (
pageLayer.batchDraw(); pageLayer.batchDraw();
}); });
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
const checkedValues: number[] = field.customText ? JSON.parse(field.customText) : []; const checkedValues: number[] = field.customText ? JSON.parse(field.customText) : [];
checkboxValues.forEach(({ id, value, checked }, index) => { checkboxValues.forEach(({ value, checked }, index) => {
const isCheckboxChecked = match(mode) const isCheckboxChecked = match(mode)
.with('edit', () => checked) .with('edit', () => checked)
.with('sign', () => checkedValues.includes(index)) .with('sign', () => checkedValues.includes(index))
@ -145,8 +146,6 @@ export const renderCheckboxFieldElement = (
}) })
.exhaustive(); .exhaustive();
console.log('wtf?');
const itemSize = calculateCheckboxSize(fontSize); const itemSize = calculateCheckboxSize(fontSize);
const { itemInputX, itemInputY, textX, textY, textWidth, textHeight } = const { itemInputX, itemInputY, textX, textY, textWidth, textHeight } =
@ -211,6 +210,8 @@ export const renderCheckboxFieldElement = (
fieldGroup.add(text); fieldGroup.add(text);
}); });
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
return { return {
fieldGroup, fieldGroup,
isFirstRender, isFirstRender,

View File

@ -1,8 +1,10 @@
import { FieldType } from '@prisma/client';
import Konva from 'konva'; import Konva from 'konva';
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf'; import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
import type { TDropdownFieldMeta } from '../../types/field-meta'; import type { TDropdownFieldMeta } from '../../types/field-meta';
import { import {
createFieldHoverInteraction,
konvaTextFill, konvaTextFill,
konvaTextFontFamily, konvaTextFontFamily,
upsertFieldGroup, upsertFieldGroup,
@ -48,79 +50,30 @@ export const renderDropdownFieldElement = (
field: FieldToRender, field: FieldToRender,
options: RenderFieldElementOptions, options: RenderFieldElementOptions,
) => { ) => {
const { pageWidth, pageHeight, pageLayer, mode } = options; const { pageWidth, pageHeight, pageLayer, mode, translations } = options;
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
const dropdownMeta: TDropdownFieldMeta | null = (field.fieldMeta as TDropdownFieldMeta) || null;
let selectedValue = translations?.[FieldType.DROPDOWN] || 'Select Option';
const isFirstRender = !pageLayer.findOne(`#${field.renderId}`); const isFirstRender = !pageLayer.findOne(`#${field.renderId}`);
const fieldGroup = upsertFieldGroup(field, options);
// Clear previous children to re-render fresh. // Clear previous children to re-render fresh.
const fieldGroup = upsertFieldGroup(field, options);
fieldGroup.removeChildren(); fieldGroup.removeChildren();
fieldGroup.off('transform');
fieldGroup.add(upsertFieldRect(field, options)); const fieldRect = upsertFieldRect(field, options);
fieldGroup.add(fieldRect);
if (isFirstRender) { if (isFirstRender) {
pageLayer.add(fieldGroup); pageLayer.add(fieldGroup);
fieldGroup.on('transform', () => {
const groupScaleX = fieldGroup.scaleX();
const groupScaleY = fieldGroup.scaleY();
const fieldRect = fieldGroup.findOne('.field-rect');
const text = fieldGroup.findOne('.dropdown-selected-text');
const arrow = fieldGroup.findOne('.dropdown-arrow');
if (!fieldRect || !text || !arrow) {
console.log('fieldRect or text or arrow not found');
return;
}
const rectWidth = fieldRect.width() * groupScaleX;
const rectHeight = fieldRect.height() * groupScaleY;
const { arrowX, arrowY, textX, textY, textWidth, textHeight } = calculateDropdownPosition({
fieldWidth: rectWidth,
fieldHeight: rectHeight,
});
arrow.setAttrs({
x: arrowX,
y: arrowY,
scaleX: 1,
scaleY: 1,
});
text.setAttrs({
scaleX: 1,
scaleY: 1,
x: textX,
y: textY,
width: textWidth,
height: textHeight,
});
fieldRect.setAttrs({
width: rectWidth,
height: rectHeight,
});
fieldGroup.scale({
x: 1,
y: 1,
});
pageLayer.batchDraw();
});
} }
const dropdownMeta: TDropdownFieldMeta | null = (field.fieldMeta as TDropdownFieldMeta) || null;
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
const fontSize = dropdownMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE; const fontSize = dropdownMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
// Todo: Envelopes - Translations
let selectedValue = 'Select Option';
if (field.inserted) { if (field.inserted) {
selectedValue = field.customText; selectedValue = field.customText;
} }
@ -158,27 +111,63 @@ export const renderDropdownFieldElement = (
visible: mode !== 'export', visible: mode !== 'export',
}); });
// Add hover state for dropdown
fieldGroup.on('mouseenter', () => {
// dropdownContainer.stroke('#2563EB');
// dropdownContainer.strokeWidth(2);
document.body.style.cursor = 'pointer';
pageLayer.batchDraw();
});
fieldGroup.on('mouseleave', () => {
// dropdownContainer.stroke('#374151');
// dropdownContainer.strokeWidth(2);
document.body.style.cursor = 'default';
pageLayer.batchDraw();
});
fieldGroup.add(selectedText); fieldGroup.add(selectedText);
if (!field.inserted || mode === 'export') { if (!field.inserted || mode === 'export') {
fieldGroup.add(arrow); fieldGroup.add(arrow);
} }
fieldGroup.on('transform', () => {
const groupScaleX = fieldGroup.scaleX();
const groupScaleY = fieldGroup.scaleY();
const fieldRect = fieldGroup.findOne('.field-rect');
const text = fieldGroup.findOne('.dropdown-selected-text');
const arrow = fieldGroup.findOne('.dropdown-arrow');
if (!fieldRect || !text || !arrow) {
return;
}
const rectWidth = fieldRect.width() * groupScaleX;
const rectHeight = fieldRect.height() * groupScaleY;
const { arrowX, arrowY, textX, textY, textWidth, textHeight } = calculateDropdownPosition({
fieldWidth: rectWidth,
fieldHeight: rectHeight,
});
arrow.setAttrs({
x: arrowX,
y: arrowY,
scaleX: 1,
scaleY: 1,
});
text.setAttrs({
scaleX: 1,
scaleY: 1,
x: textX,
y: textY,
width: textWidth,
height: textHeight,
});
fieldRect.setAttrs({
width: rectWidth,
height: rectHeight,
});
fieldGroup.scale({
x: 1,
y: 1,
});
pageLayer.batchDraw();
});
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
return { return {
fieldGroup, fieldGroup,
isFirstRender, isFirstRender,

View File

@ -4,6 +4,7 @@ import { match } from 'ts-pattern';
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf'; import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
import type { TRadioFieldMeta } from '../../types/field-meta'; import type { TRadioFieldMeta } from '../../types/field-meta';
import { import {
createFieldHoverInteraction,
konvaTextFill, konvaTextFill,
konvaTextFontFamily, konvaTextFontFamily,
upsertFieldGroup, upsertFieldGroup,
@ -26,25 +27,24 @@ export const renderRadioFieldElement = (
) => { ) => {
const { pageWidth, pageHeight, pageLayer, mode } = options; const { pageWidth, pageHeight, pageLayer, mode } = options;
const isFirstRender = !pageLayer.findOne(`#${field.renderId}`);
const fieldGroup = upsertFieldGroup(field, options);
// Clear previous children to re-render fresh
fieldGroup.removeChildren();
fieldGroup.add(upsertFieldRect(field, options));
const radioMeta: TRadioFieldMeta | null = (field.fieldMeta as TRadioFieldMeta) || null; const radioMeta: TRadioFieldMeta | null = (field.fieldMeta as TRadioFieldMeta) || null;
const radioValues = radioMeta?.values || []; const radioValues = radioMeta?.values || [];
const fontSize = radioMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE; const isFirstRender = !pageLayer.findOne(`#${field.renderId}`);
// Clear previous children and listeners to re-render fresh
const fieldGroup = upsertFieldGroup(field, options);
fieldGroup.removeChildren();
fieldGroup.off('transform');
if (isFirstRender) { if (isFirstRender) {
pageLayer.add(fieldGroup); pageLayer.add(fieldGroup);
} }
fieldGroup.off('transform'); const fieldRect = upsertFieldRect(field, options);
fieldGroup.add(fieldRect);
const fontSize = radioMeta?.fontSize || DEFAULT_STANDARD_FONT_SIZE;
// Handle rescaling items during transforms. // Handle rescaling items during transforms.
fieldGroup.on('transform', () => { fieldGroup.on('transform', () => {
@ -195,6 +195,8 @@ export const renderRadioFieldElement = (
fieldGroup.add(text); fieldGroup.add(text);
}); });
createFieldHoverInteraction({ fieldGroup, fieldRect, options });
return { return {
fieldGroup, fieldGroup,
isFirstRender, isFirstRender,

View File

@ -1,13 +1,12 @@
import Konva from 'konva'; import Konva from 'konva';
import {
DEFAULT_RECT_BACKGROUND,
RECIPIENT_COLOR_STYLES,
} from '@documenso/ui/lib/recipient-colors';
import { DEFAULT_SIGNATURE_TEXT_FONT_SIZE } from '../../constants/pdf'; import { DEFAULT_SIGNATURE_TEXT_FONT_SIZE } from '../../constants/pdf';
import { AppError } from '../../errors/app-error'; import { AppError } from '../../errors/app-error';
import { upsertFieldGroup, upsertFieldRect } from './field-generic-items'; import {
createFieldHoverInteraction,
upsertFieldGroup,
upsertFieldRect,
} from './field-generic-items';
import { calculateFieldPosition } from './field-renderer'; import { calculateFieldPosition } from './field-renderer';
import type { FieldToRender, RenderFieldElementOptions } from './field-renderer'; import type { FieldToRender, RenderFieldElementOptions } from './field-renderer';
@ -212,33 +211,7 @@ export const renderSignatureFieldElement = (
fieldRect.opacity(0); fieldRect.opacity(0);
} }
// Todo: Doesn't work. createFieldHoverInteraction({ fieldGroup, fieldRect, options });
if (mode !== 'export') {
const hoverColor = options.color
? RECIPIENT_COLOR_STYLES[options.color].baseRingHover
: '#e5e7eb';
// Todo: Envelopes - On hover add text color
// Add smooth transition-like behavior for hover effects
fieldGroup.on('mouseover', () => {
new Konva.Tween({
node: fieldRect,
duration: 0.3,
fill: hoverColor,
}).play();
});
fieldGroup.on('mouseout', () => {
new Konva.Tween({
node: fieldRect,
duration: 0.3,
fill: DEFAULT_RECT_BACKGROUND,
}).play();
});
fieldGroup.add(fieldRect);
}
return { return {
fieldGroup, fieldGroup,

View File

@ -1,13 +1,9 @@
import Konva from 'konva'; import Konva from 'konva';
import {
DEFAULT_RECT_BACKGROUND,
RECIPIENT_COLOR_STYLES,
} from '@documenso/ui/lib/recipient-colors';
import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf'; import { DEFAULT_STANDARD_FONT_SIZE } from '../../constants/pdf';
import type { TTextFieldMeta } from '../../types/field-meta'; import type { TTextFieldMeta } from '../../types/field-meta';
import { import {
createFieldHoverInteraction,
konvaTextFill, konvaTextFill,
konvaTextFontFamily, konvaTextFontFamily,
upsertFieldGroup, upsertFieldGroup,
@ -19,12 +15,12 @@ import { calculateFieldPosition } from './field-renderer';
const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Text => { const upsertFieldText = (field: FieldToRender, options: RenderFieldElementOptions): Konva.Text => {
const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options; const { pageWidth, pageHeight, mode = 'edit', pageLayer, translations } = options;
const fieldTypeName = translations?.[field.type] || field.type;
const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight); const { fieldWidth, fieldHeight } = calculateFieldPosition(field, pageWidth, pageHeight);
const textMeta = field.fieldMeta as TTextFieldMeta | undefined; const textMeta = field.fieldMeta as TTextFieldMeta | undefined;
const fieldTypeName = translations?.[field.type] || field.type;
const fieldText: Konva.Text = const fieldText: Konva.Text =
pageLayer.findOne(`#${field.renderId}-text`) || pageLayer.findOne(`#${field.renderId}-text`) ||
new Konva.Text({ new Konva.Text({
@ -118,9 +114,8 @@ export const renderTextFieldElement = (
const isFirstRender = !pageLayer.findOne(`#${field.renderId}`); const isFirstRender = !pageLayer.findOne(`#${field.renderId}`);
const fieldGroup = upsertFieldGroup(field, options);
// Clear previous children and listeners to re-render fresh. // Clear previous children and listeners to re-render fresh.
const fieldGroup = upsertFieldGroup(field, options);
fieldGroup.removeChildren(); fieldGroup.removeChildren();
fieldGroup.off('transform'); fieldGroup.off('transform');
@ -183,33 +178,7 @@ export const renderTextFieldElement = (
fieldRect.opacity(0); fieldRect.opacity(0);
} }
// Todo: Doesn't work. createFieldHoverInteraction({ fieldGroup, fieldRect, options });
if (mode !== 'export') {
const hoverColor = options.color
? RECIPIENT_COLOR_STYLES[options.color].baseRingHover
: '#e5e7eb';
// Todo: Envelopes - On hover add text color
// Add smooth transition-like behavior for hover effects
fieldGroup.on('mouseover', () => {
new Konva.Tween({
node: fieldRect,
duration: 0.3,
fill: hoverColor,
}).play();
});
fieldGroup.on('mouseout', () => {
new Konva.Tween({
node: fieldRect,
duration: 0.3,
fill: DEFAULT_RECT_BACKGROUND,
}).play();
});
fieldGroup.add(fieldRect);
}
return { return {
fieldGroup, fieldGroup,

View File

@ -7,7 +7,6 @@ import { env } from '@documenso/lib/utils/env';
import { AppError } from '../../errors/app-error'; import { AppError } from '../../errors/app-error';
import { createDocumentData } from '../../server-only/document-data/create-document-data'; import { createDocumentData } from '../../server-only/document-data/create-document-data';
import { normalizePdf } from '../../server-only/pdf/normalize-pdf';
import { uploadS3File } from './server-actions'; import { uploadS3File } from './server-actions';
type File = { type File = {
@ -44,28 +43,6 @@ export const putPdfFileServerSide = async (file: File) => {
return await createDocumentData({ type, data }); return await createDocumentData({ type, data });
}; };
/**
* Uploads a pdf file and normalizes it.
*/
export const putNormalizedPdfFileServerSide = async (file: File) => {
const buffer = Buffer.from(await file.arrayBuffer());
const normalized = await normalizePdf(buffer);
const fileName = file.name.endsWith('.pdf') ? file.name : `${file.name}.pdf`;
const documentData = await putFileServerSide({
name: fileName,
type: 'application/pdf',
arrayBuffer: async () => Promise.resolve(normalized),
});
return await createDocumentData({
type: documentData.type,
data: documentData.data,
});
};
/** /**
* Uploads a file to the appropriate storage location. * Uploads a file to the appropriate storage location.
*/ */

View File

@ -353,6 +353,13 @@ export const formatDocumentAuditLogAction = (
}), }),
identified: msg`${prefix} deleted the document`, identified: msg`${prefix} deleted the document`,
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELDS_AUTO_INSERTED }, () => ({
anonymous: msg({
message: `System auto inserted fields`,
context: `Audit log format`,
}),
identified: msg`System auto inserted fields`,
}))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED }, () => ({ .with({ type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED }, () => ({
anonymous: msg({ anonymous: msg({
message: `Field signed`, message: `Field signed`,
@ -515,6 +522,14 @@ export const formatDocumentAuditLogAction = (
context: `Audit log format`, context: `Audit log format`,
}), }),
})) }))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_CREATED }, ({ data }) => ({
anonymous: msg`Envelope item created`,
identified: msg`${prefix} created an envelope item with title ${data.envelopeItemTitle}`,
}))
.with({ type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_DELETED }, ({ data }) => ({
anonymous: msg`Envelope item deleted`,
identified: msg`${prefix} deleted an envelope item with title ${data.envelopeItemTitle}`,
}))
.exhaustive(); .exhaustive();
return { return {

View File

@ -101,7 +101,7 @@ export const getClientSideFieldTranslations = ({ t }: I18n): Record<FieldType, s
[FieldType.TEXT]: t(msg`Text`), [FieldType.TEXT]: t(msg`Text`),
[FieldType.CHECKBOX]: t(msg`Checkbox`), [FieldType.CHECKBOX]: t(msg`Checkbox`),
[FieldType.RADIO]: t(msg`Radio`), [FieldType.RADIO]: t(msg`Radio`),
[FieldType.DROPDOWN]: t(msg`Dropdown`), [FieldType.DROPDOWN]: t(msg`Select Option`),
[FieldType.SIGNATURE]: t(msg`Signature`), [FieldType.SIGNATURE]: t(msg`Signature`),
[FieldType.FREE_SIGNATURE]: t(msg`Free Signature`), [FieldType.FREE_SIGNATURE]: t(msg`Free Signature`),
[FieldType.INITIALS]: t(msg`Initials`), [FieldType.INITIALS]: t(msg`Initials`),

View File

@ -21,14 +21,14 @@
"seed": "tsx ./seed-database.ts" "seed": "tsx ./seed-database.ts"
}, },
"dependencies": { "dependencies": {
"@prisma/client": "^6.18.0", "@prisma/client": "^6.8.2",
"kysely": "0.26.3", "kysely": "0.26.3",
"prisma": "^6.18.0", "prisma": "^6.8.2",
"prisma-extension-kysely": "^3.0.0", "prisma-extension-kysely": "^3.0.0",
"prisma-kysely": "^1.8.0", "prisma-kysely": "^1.8.0",
"prisma-json-types-generator": "^3.6.2", "prisma-json-types-generator": "^3.2.2",
"ts-pattern": "^5.0.6", "ts-pattern": "^5.0.6",
"zod-prisma-types": "3.3.5" "zod-prisma-types": "3.2.4"
}, },
"devDependencies": { "devDependencies": {
"dotenv": "^16.5.0", "dotenv": "^16.5.0",

View File

@ -134,8 +134,8 @@ model Passkey {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) updatedAt DateTime @default(now())
lastUsedAt DateTime? lastUsedAt DateTime?
credentialId Bytes /// @zod.custom.use(z.instanceof(Uint8Array)) credentialId Bytes
credentialPublicKey Bytes /// @zod.custom.use(z.instanceof(Uint8Array)) credentialPublicKey Bytes
counter BigInt counter BigInt
credentialDeviceType String credentialDeviceType String
credentialBackedUp Boolean credentialBackedUp Boolean

View File

@ -1,23 +1,17 @@
import { import { createTRPCClient, httpBatchLink, httpLink, splitLink } from '@trpc/client';
createTRPCClient, import SuperJSON from 'superjson';
httpBatchLink,
httpLink,
isNonJsonSerializable,
splitLink,
} from '@trpc/client';
import { getBaseUrl } from '@documenso/lib/universal/get-base-url'; import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
import type { AppRouter } from '../server/router'; import type { AppRouter } from '../server/router';
import { dataTransformer } from '../utils/data-transformer';
export const trpc = createTRPCClient<AppRouter>({ export const trpc = createTRPCClient<AppRouter>({
links: [ links: [
splitLink({ splitLink({
condition: (op) => op.context.skipBatch === true || isNonJsonSerializable(op.input), condition: (op) => op.context.skipBatch === true,
true: httpLink({ true: httpLink({
url: `${getBaseUrl()}/api/trpc`, url: `${getBaseUrl()}/api/trpc`,
transformer: dataTransformer, transformer: SuperJSON,
headers: (opts) => { headers: (opts) => {
if (typeof opts.op.context.teamId === 'string') { if (typeof opts.op.context.teamId === 'string') {
return { return {
@ -30,7 +24,7 @@ export const trpc = createTRPCClient<AppRouter>({
}), }),
false: httpBatchLink({ false: httpBatchLink({
url: `${getBaseUrl()}/api/trpc`, url: `${getBaseUrl()}/api/trpc`,
transformer: dataTransformer, transformer: SuperJSON,
headers: (opts) => { headers: (opts) => {
const operationWithTeamId = opts.opList.find( const operationWithTeamId = opts.opList.find(
(op) => op.context.teamId && typeof op.context.teamId === 'string', (op) => op.context.teamId && typeof op.context.teamId === 'string',

View File

@ -12,21 +12,15 @@
"dependencies": { "dependencies": {
"@documenso/lib": "*", "@documenso/lib": "*",
"@documenso/prisma": "*", "@documenso/prisma": "*",
"@tanstack/react-query": "5.90.5", "@tanstack/react-query": "5.59.15",
"@trpc/client": "11.7.0", "@trpc/client": "11.0.0-rc.648",
"@trpc/react-query": "11.7.0", "@trpc/react-query": "11.0.0-rc.648",
"@trpc/server": "11.7.0", "@trpc/server": "11.0.0-rc.648",
"@ts-rest/core": "^3.52.0", "@ts-rest/core": "^3.30.5",
"formidable": "^3.5.4",
"luxon": "^3.4.0", "luxon": "^3.4.0",
"superjson": "^2.2.5", "superjson": "^1.13.1",
"trpc-to-openapi": "2.4.0", "trpc-to-openapi": "2.0.4",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",
"zod": "^3.25.76", "zod": "3.24.1"
"zod-form-data": "^2.0.8",
"zod-openapi": "^4.2.4"
},
"devDependencies": {
"@types/formidable": "^3.4.6"
} }
} }

View File

@ -1,13 +1,13 @@
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink, httpLink, isNonJsonSerializable, splitLink } from '@trpc/client'; import { httpBatchLink, httpLink, splitLink } from '@trpc/client';
import { createTRPCReact } from '@trpc/react-query'; import { createTRPCReact } from '@trpc/react-query';
import SuperJSON from 'superjson';
import { getBaseUrl } from '@documenso/lib/universal/get-base-url'; import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
import type { AppRouter } from '../server/router'; import type { AppRouter } from '../server/router';
import { dataTransformer } from '../utils/data-transformer';
export { getQueryKey } from '@trpc/react-query'; export { getQueryKey } from '@trpc/react-query';
@ -44,16 +44,16 @@ export function TrpcProvider({ children, headers }: TrpcProviderProps) {
trpc.createClient({ trpc.createClient({
links: [ links: [
splitLink({ splitLink({
condition: (op) => op.context.skipBatch === true || isNonJsonSerializable(op.input), condition: (op) => op.context.skipBatch === true,
true: httpLink({ true: httpLink({
url: `${getBaseUrl()}/api/trpc`, url: `${getBaseUrl()}/api/trpc`,
headers, headers,
transformer: dataTransformer, transformer: SuperJSON,
}), }),
false: httpBatchLink({ false: httpBatchLink({
url: `${getBaseUrl()}/api/trpc`, url: `${getBaseUrl()}/api/trpc`,
headers, headers,
transformer: dataTransformer, transformer: SuperJSON,
}), }),
}), }),
], ],

View File

@ -1,136 +0,0 @@
import { EnvelopeType } from '@prisma/client';
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
import { putPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZCreateDocumentFormDataRequestSchema,
ZCreateDocumentFormDataResponseSchema,
createDocumentFormDataMeta,
} from './create-document-formdata.types';
/**
* Temporary endpoint for V2 Beta until we allow passthrough documents on create.
*
* @public
*/
export const createDocumentFormDataRoute = authenticatedProcedure
.meta(createDocumentFormDataMeta)
.input(ZCreateDocumentFormDataRequestSchema)
.output(ZCreateDocumentFormDataResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { payload, file } = input;
const {
title,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
recipients,
meta,
folderId,
attachments,
} = payload;
const { remaining } = await getServerLimits({ userId: user.id, teamId });
if (remaining.documents <= 0) {
throw new AppError(AppErrorCode.LIMIT_EXCEEDED, {
message: 'You have reached your document limit for this month. Please upgrade your plan.',
statusCode: 400,
});
}
const documentData = await putPdfFileServerSide(file);
const createdEnvelope = await createEnvelope({
userId: ctx.user.id,
teamId,
normalizePdf: false, // Not normalizing because of presigned URL.
internalVersion: 1,
data: {
type: EnvelopeType.DOCUMENT,
title,
externalId,
visibility,
globalAccessAuth,
globalActionAuth,
recipients: (recipients || []).map((recipient) => ({
...recipient,
fields: (recipient.fields || []).map((field) => ({
...field,
page: field.pageNumber,
positionX: field.pageX,
positionY: field.pageY,
documentDataId: documentData.id,
})),
})),
folderId,
envelopeItems: [
{
// If you ever allow more than 1 in this endpoint, make sure to use `maximumEnvelopeItemCount` to limit it.
documentDataId: documentData.id,
},
],
},
attachments,
meta: {
...meta,
emailSettings: meta?.emailSettings ?? undefined,
},
requestMetadata: ctx.metadata,
});
const envelopeItems = await prisma.envelopeItem.findMany({
where: {
envelopeId: createdEnvelope.id,
},
include: {
documentData: true,
},
});
const legacyDocumentId = mapSecondaryIdToDocumentId(createdEnvelope.secondaryId);
const firstDocumentData = envelopeItems[0].documentData;
if (!firstDocumentData) {
throw new Error('Document data not found');
}
return {
document: {
...createdEnvelope,
envelopeId: createdEnvelope.id,
documentDataId: firstDocumentData.id,
documentData: {
...firstDocumentData,
envelopeItemId: envelopeItems[0].id,
},
documentMeta: {
...createdEnvelope.documentMeta,
documentId: legacyDocumentId,
},
id: legacyDocumentId,
fields: createdEnvelope.fields.map((field) => ({
...field,
documentId: legacyDocumentId,
templateId: null,
})),
recipients: createdEnvelope.recipients.map((recipient) => ({
...recipient,
documentId: legacyDocumentId,
templateId: null,
})),
},
folder: createdEnvelope.folder, // Todo: Remove this prior to api-v2 release.
};
});

View File

@ -1,97 +0,0 @@
import { z } from 'zod';
import { zfd } from 'zod-form-data';
import { ZDocumentSchema } from '@documenso/lib/types/document';
import {
ZDocumentAccessAuthTypesSchema,
ZDocumentActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';
import { ZDocumentMetaCreateSchema } from '@documenso/lib/types/document-meta';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
import {
ZFieldHeightSchema,
ZFieldPageNumberSchema,
ZFieldPageXSchema,
ZFieldPageYSchema,
ZFieldWidthSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { zodFormData } from '../../utils/zod-form-data';
import { ZCreateRecipientSchema } from '../recipient-router/schema';
import type { TrpcRouteMeta } from '../trpc';
import {
ZDocumentExternalIdSchema,
ZDocumentTitleSchema,
ZDocumentVisibilitySchema,
} from './schema';
export const createDocumentFormDataMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/document/create/formdata',
contentTypes: ['multipart/form-data'],
summary: 'Create document',
description: 'Create a document using form data.',
tags: ['Document'],
},
};
const ZCreateDocumentFormDataPayloadRequestSchema = z.object({
title: ZDocumentTitleSchema,
externalId: ZDocumentExternalIdSchema.optional(),
visibility: ZDocumentVisibilitySchema.optional(),
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
formValues: ZDocumentFormValuesSchema.optional(),
folderId: z
.string()
.describe(
'The ID of the folder to create the document in. If not provided, the document will be created in the root folder.',
)
.optional(),
recipients: z
.array(
ZCreateRecipientSchema.extend({
fields: ZFieldAndMetaSchema.and(
z.object({
pageNumber: ZFieldPageNumberSchema,
pageX: ZFieldPageXSchema,
pageY: ZFieldPageYSchema,
width: ZFieldWidthSchema,
height: ZFieldHeightSchema,
}),
)
.array()
.optional(),
}),
)
.optional(),
attachments: z
.array(
z.object({
label: z.string().min(1, 'Label is required'),
data: z.string().url('Must be a valid URL'),
type: ZEnvelopeAttachmentTypeSchema.optional().default('link'),
}),
)
.optional(),
meta: ZDocumentMetaCreateSchema.optional(),
});
// !: Can't use zfd.formData() here because it receives `undefined`
// !: somewhere in the pipeline of our openapi schema generation and throws
// !: an error.
export const ZCreateDocumentFormDataRequestSchema = zodFormData({
payload: zfd.json(ZCreateDocumentFormDataPayloadRequestSchema),
file: zfd.file(),
});
export const ZCreateDocumentFormDataResponseSchema = z.object({
document: ZDocumentSchema,
});
export type TCreateDocumentFormDataRequest = z.infer<typeof ZCreateDocumentFormDataRequestSchema>;
export type TCreateDocumentFormDataResponse = z.infer<typeof ZCreateDocumentFormDataResponseSchema>;

View File

@ -3,7 +3,6 @@ import { EnvelopeType } from '@prisma/client';
import { getServerLimits } from '@documenso/ee/server-only/limits/server'; import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope'; import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope'; import { mapSecondaryIdToDocumentId } from '@documenso/lib/utils/envelope';
import { authenticatedProcedure } from '../trpc'; import { authenticatedProcedure } from '../trpc';
@ -17,12 +16,7 @@ export const createDocumentRoute = authenticatedProcedure
.output(ZCreateDocumentResponseSchema) .output(ZCreateDocumentResponseSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { user, teamId } = ctx; const { user, teamId } = ctx;
const { title, documentDataId, timezone, folderId, attachments } = input;
const { payload, file } = input;
const { title, timezone, folderId, attachments } = payload;
const { id: documentDataId } = await putNormalizedPdfFileServerSide(file);
ctx.logger.info({ ctx.logger.info({
input: { input: {
@ -61,7 +55,6 @@ export const createDocumentRoute = authenticatedProcedure
}); });
return { return {
envelopeId: document.id,
legacyDocumentId: mapSecondaryIdToDocumentId(document.secondaryId), legacyDocumentId: mapSecondaryIdToDocumentId(document.secondaryId),
}; };
}); });

View File

@ -1,27 +1,23 @@
import { z } from 'zod'; import { z } from 'zod';
import { zfd } from 'zod-form-data';
import { ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta'; import { ZDocumentMetaTimezoneSchema } from '@documenso/lib/types/document-meta';
import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment'; import { ZEnvelopeAttachmentTypeSchema } from '@documenso/lib/types/envelope-attachment';
import { zodFormData } from '../../utils/zod-form-data';
import type { TrpcRouteMeta } from '../trpc';
import { ZDocumentTitleSchema } from './schema'; import { ZDocumentTitleSchema } from './schema';
// Currently not in use until we allow passthrough documents on create. // Currently not in use until we allow passthrough documents on create.
export const createDocumentMeta: TrpcRouteMeta = { // export const createDocumentMeta: TrpcRouteMeta = {
openapi: { // openapi: {
method: 'POST', // method: 'POST',
path: '/document/create', // path: '/document/create',
contentTypes: ['multipart/form-data'], // summary: 'Create document',
summary: 'Create document', // tags: ['Document'],
description: 'Create a document using form data.', // },
tags: ['Document'], // };
},
};
export const ZCreateDocumentPayloadSchema = z.object({ export const ZCreateDocumentRequestSchema = z.object({
title: ZDocumentTitleSchema, title: ZDocumentTitleSchema,
documentDataId: z.string().min(1),
timezone: ZDocumentMetaTimezoneSchema.optional(), timezone: ZDocumentMetaTimezoneSchema.optional(),
folderId: z.string().describe('The ID of the folder to create the document in').optional(), folderId: z.string().describe('The ID of the folder to create the document in').optional(),
attachments: z attachments: z
@ -35,16 +31,9 @@ export const ZCreateDocumentPayloadSchema = z.object({
.optional(), .optional(),
}); });
export const ZCreateDocumentRequestSchema = zodFormData({
payload: zfd.json(ZCreateDocumentPayloadSchema),
file: zfd.file(),
});
export const ZCreateDocumentResponseSchema = z.object({ export const ZCreateDocumentResponseSchema = z.object({
envelopeId: z.string(),
legacyDocumentId: z.number(), legacyDocumentId: z.number(),
}); });
export type TCreateDocumentPayloadSchema = z.infer<typeof ZCreateDocumentPayloadSchema>;
export type TCreateDocumentRequest = z.infer<typeof ZCreateDocumentRequestSchema>; export type TCreateDocumentRequest = z.infer<typeof ZCreateDocumentRequestSchema>;
export type TCreateDocumentResponse = z.infer<typeof ZCreateDocumentResponseSchema>; export type TCreateDocumentResponse = z.infer<typeof ZCreateDocumentResponseSchema>;

View File

@ -5,7 +5,6 @@ import { deleteAttachmentRoute } from './attachment/delete-attachment';
import { findAttachmentsRoute } from './attachment/find-attachments'; import { findAttachmentsRoute } from './attachment/find-attachments';
import { updateAttachmentRoute } from './attachment/update-attachment'; import { updateAttachmentRoute } from './attachment/update-attachment';
import { createDocumentRoute } from './create-document'; import { createDocumentRoute } from './create-document';
import { createDocumentFormDataRoute } from './create-document-formdata';
import { createDocumentTemporaryRoute } from './create-document-temporary'; import { createDocumentTemporaryRoute } from './create-document-temporary';
import { deleteDocumentRoute } from './delete-document'; import { deleteDocumentRoute } from './delete-document';
import { distributeDocumentRoute } from './distribute-document'; import { distributeDocumentRoute } from './distribute-document';
@ -41,7 +40,6 @@ export const documentRouter = router({
// Temporary v2 beta routes to be removed once V2 is fully released. // Temporary v2 beta routes to be removed once V2 is fully released.
download: downloadDocumentRoute, download: downloadDocumentRoute,
createDocumentTemporary: createDocumentTemporaryRoute, createDocumentTemporary: createDocumentTemporaryRoute,
createDocumentFormData: createDocumentFormDataRoute,
// Internal document routes for custom frontend requests. // Internal document routes for custom frontend requests.
getDocumentByToken: getDocumentByTokenRoute, getDocumentByToken: getDocumentByTokenRoute,

View File

@ -1,6 +1,8 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id'; import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import { prefixedId } from '@documenso/lib/universal/id'; import { prefixedId } from '@documenso/lib/universal/id';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope'; import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
@ -11,11 +13,21 @@ import {
} from './create-envelope-items.types'; } from './create-envelope-items.types';
export const createEnvelopeItemsRoute = authenticatedProcedure export const createEnvelopeItemsRoute = authenticatedProcedure
// Todo: Envelopes - Pending direct uploads
// .meta({
// openapi: {
// method: 'POST',
// path: '/envelope/item/create-many',
// summary: 'Create envelope items',
// description: 'Create multiple envelope items for an envelope',
// tags: ['Envelope Item'],
// },
// })
.input(ZCreateEnvelopeItemsRequestSchema) .input(ZCreateEnvelopeItemsRequestSchema)
.output(ZCreateEnvelopeItemsResponseSchema) .output(ZCreateEnvelopeItemsResponseSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { user, teamId } = ctx; const { user, teamId, metadata } = ctx;
const { envelopeId, items } = input; const { envelopeId, data: items } = input;
ctx.logger.info({ ctx.logger.info({
input: { input: {
@ -110,17 +122,39 @@ export const createEnvelopeItemsRoute = authenticatedProcedure
const currentHighestOrderValue = const currentHighestOrderValue =
envelope.envelopeItems[envelope.envelopeItems.length - 1]?.order ?? 1; envelope.envelopeItems[envelope.envelopeItems.length - 1]?.order ?? 1;
const result = await prisma.envelopeItem.createManyAndReturn({ const result = await prisma.$transaction(async (tx) => {
data: items.map((item) => ({ const createdItems = await tx.envelopeItem.createManyAndReturn({
id: prefixedId('envelope_item'), data: items.map((item) => ({
envelopeId, id: prefixedId('envelope_item'),
title: item.title, envelopeId,
documentDataId: item.documentDataId, title: item.title,
order: currentHighestOrderValue + 1, documentDataId: item.documentDataId,
})), order: currentHighestOrderValue + 1,
include: { })),
documentData: true, include: {
}, documentData: true,
},
});
await tx.documentAuditLog.createMany({
data: createdItems.map((item) =>
createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_CREATED,
envelopeId: envelope.id,
data: {
envelopeItemId: item.id,
envelopeItemTitle: item.title,
},
user: {
name: user.name,
email: user.email,
},
requestMetadata: metadata.requestMetadata,
}),
),
});
return createdItems;
}); });
return { return {

View File

@ -7,7 +7,7 @@ import { ZDocumentTitleSchema } from '../document-router/schema';
export const ZCreateEnvelopeItemsRequestSchema = z.object({ export const ZCreateEnvelopeItemsRequestSchema = z.object({
envelopeId: z.string(), envelopeId: z.string(),
items: z data: z
.object({ .object({
title: ZDocumentTitleSchema, title: ZDocumentTitleSchema,
documentDataId: z.string(), documentDataId: z.string(),

View File

@ -1,7 +1,6 @@
import { getServerLimits } from '@documenso/ee/server-only/limits/server'; import { getServerLimits } from '@documenso/ee/server-only/limits/server';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope'; import { createEnvelope } from '@documenso/lib/server-only/envelope/create-envelope';
import { putNormalizedPdfFileServerSide } from '@documenso/lib/universal/upload/put-file.server';
import { authenticatedProcedure } from '../trpc'; import { authenticatedProcedure } from '../trpc';
import { import {
@ -10,13 +9,19 @@ import {
} from './create-envelope.types'; } from './create-envelope.types';
export const createEnvelopeRoute = authenticatedProcedure export const createEnvelopeRoute = authenticatedProcedure
// Todo: Envelopes - Pending direct uploads
// .meta({
// openapi: {
// method: 'POST',
// path: '/envelope/create',
// summary: 'Create envelope',
// tags: ['Envelope'],
// },
// })
.input(ZCreateEnvelopeRequestSchema) .input(ZCreateEnvelopeRequestSchema)
.output(ZCreateEnvelopeResponseSchema) .output(ZCreateEnvelopeResponseSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { user, teamId } = ctx; const { user, teamId } = ctx;
const { payload, files } = input;
const { const {
title, title,
type, type,
@ -26,9 +31,10 @@ export const createEnvelopeRoute = authenticatedProcedure
globalActionAuth, globalActionAuth,
recipients, recipients,
folderId, folderId,
items,
meta, meta,
attachments, attachments,
} = payload; } = input;
ctx.logger.info({ ctx.logger.info({
input: { input: {
@ -48,62 +54,13 @@ export const createEnvelopeRoute = authenticatedProcedure
}); });
} }
if (files.length > maximumEnvelopeItemCount) { if (items.length > maximumEnvelopeItemCount) {
throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', { throw new AppError('ENVELOPE_ITEM_LIMIT_EXCEEDED', {
message: `You cannot upload more than ${maximumEnvelopeItemCount} envelope items per envelope`, message: `You cannot upload more than ${maximumEnvelopeItemCount} envelope items per envelope`,
statusCode: 400, statusCode: 400,
}); });
} }
// For each file, stream to s3 and create the document data.
const envelopeItems = await Promise.all(
files.map(async (file) => {
const { id: documentDataId } = await putNormalizedPdfFileServerSide(file);
return {
title: file.name,
documentDataId,
};
}),
);
const recipientsToCreate = recipients?.map((recipient) => ({
email: recipient.email,
name: recipient.name,
role: recipient.role,
signingOrder: recipient.signingOrder,
accessAuth: recipient.accessAuth,
actionAuth: recipient.actionAuth,
fields: recipient.fields?.map((field) => {
let documentDataId: string | undefined = undefined;
if (typeof field.identifier === 'string') {
documentDataId = envelopeItems.find(
(item) => item.title === field.identifier,
)?.documentDataId;
}
if (typeof field.identifier === 'number') {
documentDataId = envelopeItems.at(field.identifier)?.documentDataId;
}
if (field.identifier === undefined) {
documentDataId = envelopeItems[0]?.documentDataId;
}
if (!documentDataId) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Document data not found',
});
}
return {
...field,
documentDataId,
};
}),
}));
const envelope = await createEnvelope({ const envelope = await createEnvelope({
userId: user.id, userId: user.id,
teamId, teamId,
@ -115,9 +72,9 @@ export const createEnvelopeRoute = authenticatedProcedure
visibility, visibility,
globalAccessAuth, globalAccessAuth,
globalActionAuth, globalActionAuth,
recipients: recipientsToCreate, recipients,
folderId, folderId,
envelopeItems, envelopeItems: items,
}, },
attachments, attachments,
meta, meta,

View File

@ -1,6 +1,5 @@
import { EnvelopeType } from '@prisma/client'; import { EnvelopeType } from '@prisma/client';
import { z } from 'zod'; import { z } from 'zod';
import { zfd } from 'zod-form-data';
import { import {
ZDocumentAccessAuthTypesSchema, ZDocumentAccessAuthTypesSchema,
@ -18,28 +17,14 @@ import {
} from '@documenso/lib/types/field'; } from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta'; import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
import { zodFormData } from '../../utils/zod-form-data';
import { import {
ZDocumentExternalIdSchema, ZDocumentExternalIdSchema,
ZDocumentTitleSchema, ZDocumentTitleSchema,
ZDocumentVisibilitySchema, ZDocumentVisibilitySchema,
} from '../document-router/schema'; } from '../document-router/schema';
import { ZCreateRecipientSchema } from '../recipient-router/schema'; import { ZCreateRecipientSchema } from '../recipient-router/schema';
import type { TrpcRouteMeta } from '../trpc';
// Currently not in use until we allow passthrough documents on create. export const ZCreateEnvelopeRequestSchema = z.object({
export const createEnvelopeMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/envelope/create',
contentTypes: ['multipart/form-data'],
summary: 'Create envelope',
description: 'Create a envelope using form data.',
tags: ['Envelope'],
},
};
export const ZCreateEnvelopePayloadSchema = z.object({
title: ZDocumentTitleSchema, title: ZDocumentTitleSchema,
type: z.nativeEnum(EnvelopeType), type: z.nativeEnum(EnvelopeType),
externalId: ZDocumentExternalIdSchema.optional(), externalId: ZDocumentExternalIdSchema.optional(),
@ -47,6 +32,12 @@ export const ZCreateEnvelopePayloadSchema = z.object({
globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(), globalAccessAuth: z.array(ZDocumentAccessAuthTypesSchema).optional(),
globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(), globalActionAuth: z.array(ZDocumentActionAuthTypesSchema).optional(),
formValues: ZDocumentFormValuesSchema.optional(), formValues: ZDocumentFormValuesSchema.optional(),
items: z
.object({
title: ZDocumentTitleSchema.optional(),
documentDataId: z.string(),
})
.array(),
folderId: z folderId: z
.string() .string()
.describe( .describe(
@ -58,12 +49,11 @@ export const ZCreateEnvelopePayloadSchema = z.object({
ZCreateRecipientSchema.extend({ ZCreateRecipientSchema.extend({
fields: ZFieldAndMetaSchema.and( fields: ZFieldAndMetaSchema.and(
z.object({ z.object({
identifier: z documentDataId: z
.union([z.string(), z.number()]) .string()
.describe( .describe(
'Either the filename or the index of the file that was uploaded to attach the field to.', 'The ID of the document data to create the field on. If empty, the first document data will be used.',
) ),
.optional(),
page: ZFieldPageNumberSchema, page: ZFieldPageNumberSchema,
positionX: ZFieldPageXSchema, positionX: ZFieldPageXSchema,
positionY: ZFieldPageYSchema, positionY: ZFieldPageYSchema,
@ -88,15 +78,9 @@ export const ZCreateEnvelopePayloadSchema = z.object({
.optional(), .optional(),
}); });
export const ZCreateEnvelopeRequestSchema = zodFormData({
payload: zfd.json(ZCreateEnvelopePayloadSchema),
files: zfd.repeatableOfType(zfd.file()),
});
export const ZCreateEnvelopeResponseSchema = z.object({ export const ZCreateEnvelopeResponseSchema = z.object({
id: z.string(), id: z.string(),
}); });
export type TCreateEnvelopePayload = z.infer<typeof ZCreateEnvelopePayloadSchema>;
export type TCreateEnvelopeRequest = z.infer<typeof ZCreateEnvelopeRequestSchema>; export type TCreateEnvelopeRequest = z.infer<typeof ZCreateEnvelopeRequestSchema>;
export type TCreateEnvelopeResponse = z.infer<typeof ZCreateEnvelopeResponseSchema>; export type TCreateEnvelopeResponse = z.infer<typeof ZCreateEnvelopeResponseSchema>;

View File

@ -1,5 +1,7 @@
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id'; import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope'; import { canEnvelopeItemsBeModified } from '@documenso/lib/utils/envelope';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
@ -10,10 +12,19 @@ import {
} from './delete-envelope-item.types'; } from './delete-envelope-item.types';
export const deleteEnvelopeItemRoute = authenticatedProcedure export const deleteEnvelopeItemRoute = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/envelope/item/delete',
summary: 'Delete envelope item',
description: 'Delete an envelope item from an envelope',
tags: ['Envelope Item'],
},
})
.input(ZDeleteEnvelopeItemRequestSchema) .input(ZDeleteEnvelopeItemRequestSchema)
.output(ZDeleteEnvelopeItemResponseSchema) .output(ZDeleteEnvelopeItemResponseSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { user, teamId } = ctx; const { user, teamId, metadata } = ctx;
const { envelopeId, envelopeItemId } = input; const { envelopeId, envelopeItemId } = input;
ctx.logger.info({ ctx.logger.info({
@ -52,29 +63,48 @@ export const deleteEnvelopeItemRoute = authenticatedProcedure
}); });
} }
const deletedEnvelopeItem = await prisma.envelopeItem.delete({ const result = await prisma.$transaction(async (tx) => {
where: { const deletedEnvelopeItem = await tx.envelopeItem.delete({
id: envelopeItemId, where: {
envelopeId: envelope.id, id: envelopeItemId,
}, envelopeId: envelope.id,
select: { },
documentData: { select: {
select: { id: true,
id: true, title: true,
documentData: {
select: {
id: true,
},
}, },
}, },
}, });
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.ENVELOPE_ITEM_DELETED,
envelopeId: envelope.id,
data: {
envelopeItemId: deletedEnvelopeItem.id,
envelopeItemTitle: deletedEnvelopeItem.title,
},
user: {
name: user.name,
email: user.email,
},
requestMetadata: metadata.requestMetadata,
}),
});
return deletedEnvelopeItem;
}); });
// Todo: Envelopes [ASK] - Should we delete the document data?
await prisma.documentData.delete({ await prisma.documentData.delete({
where: { where: {
id: deletedEnvelopeItem.documentData.id, id: result.documentData.id,
envelopeItem: { envelopeItem: {
is: null, is: null,
}, },
}, },
}); });
// Todo: Envelope [AUDIT_LOGS]
}); });

View File

@ -1,8 +1,10 @@
import { EnvelopeType } from '@prisma/client'; import { EnvelopeType } from '@prisma/client';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { deleteDocument } from '@documenso/lib/server-only/document/delete-document'; import { deleteDocument } from '@documenso/lib/server-only/document/delete-document';
import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template'; import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc'; import { authenticatedProcedure } from '../trpc';
import { import {
@ -11,12 +13,19 @@ import {
} from './delete-envelope.types'; } from './delete-envelope.types';
export const deleteEnvelopeRoute = authenticatedProcedure export const deleteEnvelopeRoute = authenticatedProcedure
// .meta(deleteEnvelopeMeta) .meta({
openapi: {
method: 'POST',
path: '/envelope/delete',
summary: 'Delete envelope',
tags: ['Envelope'],
},
})
.input(ZDeleteEnvelopeRequestSchema) .input(ZDeleteEnvelopeRequestSchema)
.output(ZDeleteEnvelopeResponseSchema) .output(ZDeleteEnvelopeResponseSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const { teamId } = ctx; const { teamId } = ctx;
const { envelopeId, envelopeType } = input; const { envelopeId } = input;
ctx.logger.info({ ctx.logger.info({
input: { input: {
@ -24,7 +33,22 @@ export const deleteEnvelopeRoute = authenticatedProcedure
}, },
}); });
await match(envelopeType) const unsafeEnvelope = await prisma.envelope.findUnique({
where: {
id: envelopeId,
},
select: {
type: true,
},
});
if (!unsafeEnvelope) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Envelope not found',
});
}
await match(unsafeEnvelope.type)
.with(EnvelopeType.DOCUMENT, async () => .with(EnvelopeType.DOCUMENT, async () =>
deleteDocument({ deleteDocument({
userId: ctx.user.id, userId: ctx.user.id,

View File

@ -1,18 +1,7 @@
import { EnvelopeType } from '@prisma/client';
import { z } from 'zod'; import { z } from 'zod';
// export const deleteEnvelopeMeta: TrpcRouteMeta = {
// openapi: {
// method: 'POST',
// path: '/envelope/delete',
// summary: 'Delete envelope',
// tags: ['Envelope'],
// },
// };
export const ZDeleteEnvelopeRequestSchema = z.object({ export const ZDeleteEnvelopeRequestSchema = z.object({
envelopeId: z.string(), envelopeId: z.string(),
envelopeType: z.nativeEnum(EnvelopeType),
}); });
export const ZDeleteEnvelopeResponseSchema = z.void(); export const ZDeleteEnvelopeResponseSchema = z.void();

View File

@ -8,7 +8,15 @@ import {
} from './distribute-envelope.types'; } from './distribute-envelope.types';
export const distributeEnvelopeRoute = authenticatedProcedure export const distributeEnvelopeRoute = authenticatedProcedure
// .meta(distributeEnvelopeMeta) .meta({
openapi: {
method: 'POST',
path: '/envelope/distribute',
summary: 'Distribute envelope',
description: 'Send the envelope to recipients based on your distribution method',
tags: ['Envelope'],
},
})
.input(ZDistributeEnvelopeRequestSchema) .input(ZDistributeEnvelopeRequestSchema)
.output(ZDistributeEnvelopeResponseSchema) .output(ZDistributeEnvelopeResponseSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {

View File

@ -2,16 +2,6 @@ import { z } from 'zod';
import { ZDocumentMetaUpdateSchema } from '@documenso/lib/types/document-meta'; import { ZDocumentMetaUpdateSchema } from '@documenso/lib/types/document-meta';
// export const distributeEnvelopeMeta: TrpcRouteMeta = {
// openapi: {
// method: 'POST',
// path: '/envelope/distribute',
// summary: 'Distribute envelope',
// description: 'Send the document out to recipients based on your distribution method',
// tags: ['Envelope'],
// },
// };
export const ZDistributeEnvelopeRequestSchema = z.object({ export const ZDistributeEnvelopeRequestSchema = z.object({
envelopeId: z.string().describe('The ID of the envelope to send.'), envelopeId: z.string().describe('The ID of the envelope to send.'),
meta: ZDocumentMetaUpdateSchema.pick({ meta: ZDocumentMetaUpdateSchema.pick({

View File

@ -7,6 +7,15 @@ import {
} from './duplicate-envelope.types'; } from './duplicate-envelope.types';
export const duplicateEnvelopeRoute = authenticatedProcedure export const duplicateEnvelopeRoute = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/envelope/duplicate',
summary: 'Duplicate envelope',
description: 'Duplicate an envelope with all its settings',
tags: ['Envelope'],
},
})
.input(ZDuplicateEnvelopeRequestSchema) .input(ZDuplicateEnvelopeRequestSchema)
.output(ZDuplicateEnvelopeResponseSchema) .output(ZDuplicateEnvelopeResponseSchema)
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {

View File

@ -0,0 +1,41 @@
import { createEnvelopeFields } from '@documenso/lib/server-only/field/create-envelope-fields';
import { authenticatedProcedure } from '../../trpc';
import {
ZCreateEnvelopeFieldsRequestSchema,
ZCreateEnvelopeFieldsResponseSchema,
} from './create-envelope-fields.types';
export const createEnvelopeFieldsRoute = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/envelope/field/create-many',
summary: 'Create envelope fields',
description: 'Create multiple fields for an envelope',
tags: ['Envelope Fields'],
},
})
.input(ZCreateEnvelopeFieldsRequestSchema)
.output(ZCreateEnvelopeFieldsResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId, metadata } = ctx;
const { envelopeId, data: fields } = input;
ctx.logger.info({
input: {
envelopeId,
},
});
return await createEnvelopeFields({
userId: user.id,
teamId,
id: {
type: 'envelopeId',
id: envelopeId,
},
fields,
requestMetadata: metadata,
});
});

View File

@ -0,0 +1,40 @@
import { z } from 'zod';
import {
ZClampedFieldHeightSchema,
ZClampedFieldPageXSchema,
ZClampedFieldPageYSchema,
ZClampedFieldWidthSchema,
ZFieldPageNumberSchema,
ZFieldSchema,
} from '@documenso/lib/types/field';
import { ZFieldAndMetaSchema } from '@documenso/lib/types/field-meta';
const ZCreateFieldSchema = ZFieldAndMetaSchema.and(
z.object({
recipientId: z.number().describe('The ID of the recipient to create the field for'),
envelopeItemId: z
.string()
.optional()
.describe(
'The ID of the envelope item to put the field on. If not provided, field will be placed on the first item.',
),
pageNumber: ZFieldPageNumberSchema,
pageX: ZClampedFieldPageXSchema,
pageY: ZClampedFieldPageYSchema,
width: ZClampedFieldWidthSchema,
height: ZClampedFieldHeightSchema,
}),
);
export const ZCreateEnvelopeFieldsRequestSchema = z.object({
envelopeId: z.string(),
data: ZCreateFieldSchema.array(),
});
export const ZCreateEnvelopeFieldsResponseSchema = z.object({
fields: z.array(ZFieldSchema),
});
export type TCreateEnvelopeFieldsRequest = z.infer<typeof ZCreateEnvelopeFieldsRequestSchema>;
export type TCreateEnvelopeFieldsResponse = z.infer<typeof ZCreateEnvelopeFieldsResponseSchema>;

View File

@ -0,0 +1,125 @@
import { EnvelopeType } from '@prisma/client';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { getEnvelopeWhereInput } from '@documenso/lib/server-only/envelope/get-envelope-by-id';
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
import { canRecipientFieldsBeModified } from '@documenso/lib/utils/recipients';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../../trpc';
import {
ZDeleteEnvelopeFieldRequestSchema,
ZDeleteEnvelopeFieldResponseSchema,
} from './delete-envelope-field.types';
export const deleteEnvelopeFieldRoute = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/envelope/field/delete',
summary: 'Delete envelope field',
description: 'Delete an envelope field',
tags: ['Envelope Field'],
},
})
.input(ZDeleteEnvelopeFieldRequestSchema)
.output(ZDeleteEnvelopeFieldResponseSchema)
.mutation(async ({ input, ctx }) => {
const { user, teamId, metadata } = ctx;
const { fieldId } = input;
ctx.logger.info({
input: {
fieldId,
},
});
const unsafeField = await prisma.field.findUnique({
where: {
id: fieldId,
},
select: {
envelopeId: true,
},
});
if (!unsafeField) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Field not found',
});
}
const { envelopeWhereInput } = await getEnvelopeWhereInput({
id: {
type: 'envelopeId',
id: unsafeField.envelopeId,
},
type: null,
userId: user.id,
teamId,
});
const envelope = await prisma.envelope.findUnique({
where: envelopeWhereInput,
include: {
recipients: {
include: {
fields: true,
},
},
},
});
const recipientWithFields = envelope?.recipients.find((recipient) =>
recipient.fields.some((field) => field.id === fieldId),
);
const fieldToDelete = recipientWithFields?.fields.find((field) => field.id === fieldId);
if (!envelope || !recipientWithFields || !fieldToDelete) {
throw new AppError(AppErrorCode.NOT_FOUND, {
message: 'Field not found',
});
}
if (envelope.completedAt) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Envelope already complete',
});
}
// Check whether the recipient associated with the field can have new fields created.
if (!canRecipientFieldsBeModified(recipientWithFields, recipientWithFields.fields)) {
throw new AppError(AppErrorCode.INVALID_REQUEST, {
message: 'Recipient has already interacted with the document.',
});
}
await prisma.$transaction(async (tx) => {
const deletedField = await tx.field.delete({
where: {
id: fieldToDelete.id,
envelopeId: envelope.id,
},
});
// Handle field deleted audit log.
if (envelope.type === EnvelopeType.DOCUMENT) {
await tx.documentAuditLog.create({
data: createDocumentAuditLogData({
type: DOCUMENT_AUDIT_LOG_TYPE.FIELD_DELETED,
envelopeId: envelope.id,
metadata,
data: {
fieldId: deletedField.secondaryId,
fieldRecipientEmail: recipientWithFields.email,
fieldRecipientId: deletedField.recipientId,
fieldType: deletedField.type,
},
}),
});
}
return deletedField;
});
});

View File

@ -0,0 +1,10 @@
import { z } from 'zod';
export const ZDeleteEnvelopeFieldRequestSchema = z.object({
fieldId: z.number(),
});
export const ZDeleteEnvelopeFieldResponseSchema = z.void();
export type TDeleteEnvelopeFieldRequest = z.infer<typeof ZDeleteEnvelopeFieldRequestSchema>;
export type TDeleteEnvelopeFieldResponse = z.infer<typeof ZDeleteEnvelopeFieldResponseSchema>;

View File

@ -0,0 +1,36 @@
import { getFieldById } from '@documenso/lib/server-only/field/get-field-by-id';
import { authenticatedProcedure } from '../../trpc';
import {
ZGetEnvelopeFieldRequestSchema,
ZGetEnvelopeFieldResponseSchema,
} from './get-envelope-field.types';
export const getEnvelopeFieldRoute = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/envelope/field/{fieldId}',
summary: 'Get envelope field',
description: 'Returns an envelope field given an ID',
tags: ['Envelope Field'],
},
})
.input(ZGetEnvelopeFieldRequestSchema)
.output(ZGetEnvelopeFieldResponseSchema)
.query(async ({ input, ctx }) => {
const { teamId, user } = ctx;
const { fieldId } = input;
ctx.logger.info({
input: {
fieldId,
},
});
return await getFieldById({
userId: user.id,
teamId,
fieldId,
});
});

Some files were not shown because too many files have changed in this diff Show More