Merge branch 'main' into feat/delete-archive

This commit is contained in:
Mythie
2024-11-27 10:57:13 +11:00
82 changed files with 2450 additions and 1820 deletions

View File

@ -27,9 +27,6 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"autoprefixer": "^10.0.1",
"postcss": "^8",
"tailwindcss": "^3.3.0",
"typescript": "^5" "typescript": "^5"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@documenso/marketing", "name": "@documenso/marketing",
"version": "1.8.1-rc.0", "version": "1.8.1-rc.1",
"private": true, "private": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {

View File

@ -1,6 +1,6 @@
{ {
"name": "@documenso/web", "name": "@documenso/web",
"version": "1.8.1-rc.0", "version": "1.8.1-rc.1",
"private": true, "private": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
@ -28,6 +28,7 @@
"@simplewebauthn/browser": "^9.0.1", "@simplewebauthn/browser": "^9.0.1",
"@simplewebauthn/server": "^9.0.3", "@simplewebauthn/server": "^9.0.3",
"@tanstack/react-query": "^4.29.5", "@tanstack/react-query": "^4.29.5",
"colord": "^2.9.3",
"cookie-es": "^1.0.0", "cookie-es": "^1.0.0",
"formidable": "^2.1.1", "formidable": "^2.1.1",
"framer-motion": "^10.12.8", "framer-motion": "^10.12.8",
@ -53,7 +54,7 @@
"react-icons": "^4.11.0", "react-icons": "^4.11.0",
"react-rnd": "^10.4.1", "react-rnd": "^10.4.1",
"recharts": "^2.7.2", "recharts": "^2.7.2",
"remeda": "^2.12.1", "remeda": "^2.17.3",
"sharp": "0.32.6", "sharp": "0.32.6",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",
"ua-parser-js": "^1.0.37", "ua-parser-js": "^1.0.37",

View File

@ -146,7 +146,10 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
<div className="flex flex-row justify-between truncate"> <div className="flex flex-row justify-between truncate">
<div> <div>
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}> <h1
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
title={document.title}
>
{document.title} {document.title}
</h1> </h1>
@ -218,7 +221,7 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
<DocumentPageViewDropdown document={documentWithRecipients} team={team} /> <DocumentPageViewDropdown document={documentWithRecipients} team={team} />
</div> </div>
<p className="text-muted-foreground mt-2 px-4 text-sm "> <p className="text-muted-foreground mt-2 px-4 text-sm">
{match(document.status) {match(document.status)
.with(DocumentStatus.COMPLETED, () => ( .with(DocumentStatus.COMPLETED, () => (
<Trans>This document has been signed by all recipients</Trans> <Trans>This document has been signed by all recipients</Trans>

View File

@ -109,7 +109,10 @@ export const DocumentEditPageView = async ({ params, team }: DocumentEditPageVie
<Trans>Documents</Trans> <Trans>Documents</Trans>
</Link> </Link>
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}> <h1
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
title={document.title}
>
{document.title} {document.title}
</h1> </h1>

View File

@ -121,7 +121,10 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
<div className="flex flex-col justify-between truncate sm:flex-row"> <div className="flex flex-col justify-between truncate sm:flex-row">
<div> <div>
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}> <h1
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
title={document.title}
>
{document.title} {document.title}
</h1> </h1>

View File

@ -141,6 +141,23 @@ export const EditTemplateForm = ({
}, },
}); });
const { mutateAsync: updateTypedSignature } =
trpc.template.updateTemplateTypedSignatureSettings.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.template.getTemplateWithDetailsById.setData(
{
id: initialTemplate.id,
},
(oldData) => ({
...(oldData || initialTemplate),
...newData,
id: Number(newData.id),
}),
);
},
});
const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => { const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => {
try { try {
await updateTemplateSettings({ await updateTemplateSettings({
@ -211,6 +228,12 @@ export const EditTemplateForm = ({
fields: data.fields, fields: data.fields,
}); });
await updateTypedSignature({
templateId: template.id,
teamId: team?.id,
typedSignatureEnabled: data.typedSignatureEnabled,
});
// Clear all field data from localStorage // Clear all field data from localStorage
for (let i = 0; i < localStorage.length; i++) { for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i); const key = localStorage.key(i);
@ -225,14 +248,13 @@ export const EditTemplateForm = ({
duration: 5000, duration: 5000,
}); });
// Router refresh is here to clear the router cache for when navigating to /documents.
router.refresh();
router.push(templateRootPath); router.push(templateRootPath);
} catch (err) { } catch (err) {
console.error(err);
toast({ toast({
title: _(msg`Error`), title: _(msg`Error`),
description: _(msg`An error occurred while adding signers.`), description: _(msg`An error occurred while adding fields.`),
variant: 'destructive', variant: 'destructive',
}); });
} }
@ -301,6 +323,7 @@ export const EditTemplateForm = ({
fields={fields} fields={fields}
onSubmit={onAddFieldsFormSubmit} onSubmit={onAddFieldsFormSubmit}
teamId={team?.id} teamId={team?.id}
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
/> />
</Stepper> </Stepper>
</DocumentFlowFormContainer> </DocumentFlowFormContainer>

View File

@ -63,7 +63,10 @@ export const TemplateEditPageView = async ({ params, team }: TemplateEditPageVie
<Trans>Template</Trans> <Trans>Template</Trans>
</Link> </Link>
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={template.title}> <h1
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
title={template.title}
>
{template.title} {template.title}
</h1> </h1>

View File

@ -73,7 +73,6 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
const mockedDocumentMeta = templateMeta const mockedDocumentMeta = templateMeta
? { ? {
typedSignatureEnabled: false,
...templateMeta, ...templateMeta,
signingOrder: templateMeta.signingOrder || DocumentSigningOrder.SEQUENTIAL, signingOrder: templateMeta.signingOrder || DocumentSigningOrder.SEQUENTIAL,
documentId: 0, documentId: 0,
@ -89,7 +88,10 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
<div className="flex flex-row justify-between truncate"> <div className="flex flex-row justify-between truncate">
<div> <div>
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={template.title}> <h1
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
title={template.title}
>
{template.title} {template.title}
</h1> </h1>
@ -155,7 +157,7 @@ export const TemplatePageView = async ({ params, team }: TemplatePageViewProps)
</div> </div>
</div> </div>
<p className="text-muted-foreground mt-2 px-4 text-sm "> <p className="text-muted-foreground mt-2 px-4 text-sm">
<Trans>Manage and view template</Trans> <Trans>Manage and view template</Trans>
</p> </p>

View File

@ -209,11 +209,19 @@ export default async function SigningCertificate({ searchParams }: SigningCertif
boxShadow: `0px 0px 0px 4.88px rgba(122, 196, 85, 0.1), 0px 0px 0px 1.22px rgba(122, 196, 85, 0.6), 0px 0px 0px 0.61px rgba(122, 196, 85, 1)`, boxShadow: `0px 0px 0px 4.88px rgba(122, 196, 85, 0.1), 0px 0px 0px 1.22px rgba(122, 196, 85, 0.6), 0px 0px 0px 0.61px rgba(122, 196, 85, 1)`,
}} }}
> >
<img {signature.Signature?.signatureImageAsBase64 && (
src={`${signature.Signature?.signatureImageAsBase64}`} <img
alt="Signature" src={`${signature.Signature?.signatureImageAsBase64}`}
className="max-h-12 max-w-full" alt="Signature"
/> className="max-h-12 max-w-full"
/>
)}
{signature.Signature?.typedSignature && (
<p className="font-signature text-center text-sm">
{signature.Signature?.typedSignature}
</p>
)}
</div> </div>
<p className="text-muted-foreground mt-2 text-sm print:text-xs"> <p className="text-muted-foreground mt-2 text-sm print:text-xs">

View File

@ -12,7 +12,6 @@ import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider'; import { DocumentAuthProvider } from '~/app/(signing)/sign/[token]/document-auth-provider';
import { SigningProvider } from '~/app/(signing)/sign/[token]/provider'; import { SigningProvider } from '~/app/(signing)/sign/[token]/provider';
import { truncateTitle } from '~/helpers/truncate-title';
import { DirectTemplatePageView } from './direct-template'; import { DirectTemplatePageView } from './direct-template';
import { DirectTemplateAuthPageView } from './signing-auth-page'; import { DirectTemplateAuthPageView } from './signing-auth-page';
@ -72,8 +71,11 @@ export default async function TemplatesDirectPage({ params }: TemplatesDirectPag
user={user} user={user}
> >
<div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8"> <div className="mx-auto -mt-4 w-full max-w-screen-xl px-4 md:px-8">
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={template.title}> <h1
{truncateTitle(template.title)} className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
title={template.title}
>
{template.title}
</h1> </h1>
<div className="text-muted-foreground mb-8 mt-2.5 flex items-center gap-x-2"> <div className="text-muted-foreground mb-8 mt-2.5 flex items-center gap-x-2">

View File

@ -102,9 +102,9 @@ export const SignDirectTemplateForm = ({
created: new Date(), created: new Date(),
recipientId: 1, recipientId: 1,
fieldId: 1, fieldId: 1,
signatureImageAsBase64: value.value, signatureImageAsBase64: value.value.startsWith('data:') ? value.value : null,
typedSignature: null, typedSignature: value.value.startsWith('data:') ? null : value.value,
}; } satisfies Signature;
} }
if (field.type === FieldType.DATE) { if (field.type === FieldType.DATE) {

View File

@ -24,8 +24,6 @@ import { SigningCard3D } from '@documenso/ui/components/signing-card';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Badge } from '@documenso/ui/primitives/badge'; import { Badge } from '@documenso/ui/primitives/badge';
import { truncateTitle } from '~/helpers/truncate-title';
import { SigningAuthPageView } from '../signing-auth-page'; import { SigningAuthPageView } from '../signing-auth-page';
import { ClaimAccount } from './claim-account'; import { ClaimAccount } from './claim-account';
import { DocumentPreviewButton } from './document-preview-button'; import { DocumentPreviewButton } from './document-preview-button';
@ -61,8 +59,6 @@ export default async function CompletedSigningPage({
return notFound(); return notFound();
} }
const truncatedTitle = truncateTitle(document.title);
const { documentData } = document; const { documentData } = document;
const [fields, recipient] = await Promise.all([ const [fields, recipient] = await Promise.all([
@ -118,7 +114,9 @@ export default async function CompletedSigningPage({
})} })}
> >
<Badge variant="neutral" size="default" className="mb-6 rounded-xl border bg-transparent"> <Badge variant="neutral" size="default" className="mb-6 rounded-xl border bg-transparent">
{truncatedTitle} <span className="block max-w-[10rem] truncate font-medium hover:underline md:max-w-[20rem]">
{document.title}
</span>
</Badge> </Badge>
{/* Card with recipient */} {/* Card with recipient */}

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { createContext, useContext, useState } from 'react'; import { createContext, useContext, useEffect, useState } from 'react';
export type SigningContextValue = { export type SigningContextValue = {
fullName: string; fullName: string;
@ -44,6 +44,12 @@ export const SigningProvider = ({
const [email, setEmail] = useState(initialEmail || ''); const [email, setEmail] = useState(initialEmail || '');
const [signature, setSignature] = useState(initialSignature || null); const [signature, setSignature] = useState(initialSignature || null);
useEffect(() => {
if (initialSignature) {
setSignature(initialSignature);
}
}, [initialSignature]);
return ( return (
<SigningContext.Provider <SigningContext.Provider
value={{ value={{

View File

@ -14,7 +14,6 @@ import {
} from '@documenso/ui/primitives/dialog'; } from '@documenso/ui/primitives/dialog';
import { SigningDisclosure } from '~/components/general/signing-disclosure'; import { SigningDisclosure } from '~/components/general/signing-disclosure';
import { truncateTitle } from '~/helpers/truncate-title';
export type SignDialogProps = { export type SignDialogProps = {
isSubmitting: boolean; isSubmitting: boolean;
@ -36,7 +35,7 @@ export const SignDialog = ({
disabled = false, disabled = false,
}: SignDialogProps) => { }: SignDialogProps) => {
const [showDialog, setShowDialog] = useState(false); const [showDialog, setShowDialog] = useState(false);
const truncatedTitle = truncateTitle(documentTitle);
const isComplete = fields.every((field) => field.inserted); const isComplete = fields.every((field) => field.inserted);
const handleOpenChange = (open: boolean) => { const handleOpenChange = (open: boolean) => {
@ -75,7 +74,13 @@ export const SignDialog = ({
{role === RecipientRole.VIEWER && ( {role === RecipientRole.VIEWER && (
<span> <span>
<Trans> <Trans>
You are about to complete viewing "{truncatedTitle}". <span className="inline-flex flex-wrap">
You are about to complete viewing "
<span className="inline-block max-w-[11rem] truncate align-baseline">
{documentTitle}
</span>
".
</span>
<br /> Are you sure? <br /> Are you sure?
</Trans> </Trans>
</span> </span>
@ -83,7 +88,13 @@ export const SignDialog = ({
{role === RecipientRole.SIGNER && ( {role === RecipientRole.SIGNER && (
<span> <span>
<Trans> <Trans>
You are about to complete signing "{truncatedTitle}". <span className="inline-flex flex-wrap">
You are about to complete signing "
<span className="inline-block max-w-[11rem] truncate align-baseline">
{documentTitle}
</span>
".
</span>
<br /> Are you sure? <br /> Are you sure?
</Trans> </Trans>
</span> </span>
@ -91,7 +102,13 @@ export const SignDialog = ({
{role === RecipientRole.APPROVER && ( {role === RecipientRole.APPROVER && (
<span> <span>
<Trans> <Trans>
You are about to complete approving "{truncatedTitle}". <span className="inline-flex flex-wrap">
You are about to complete approving{' '}
<span className="inline-block max-w-[11rem] truncate align-baseline">
"{documentTitle}"
</span>
.
</span>
<br /> Are you sure? <br /> Are you sure?
</Trans> </Trans>
</span> </span>

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useMemo, useState, useTransition } from 'react'; import { useLayoutEffect, useMemo, useRef, useState, useTransition } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
@ -51,6 +51,10 @@ export const SignatureField = ({
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
const signatureRef = useRef<HTMLParagraphElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [fontSize, setFontSize] = useState(2);
const { signature: providedSignature, setSignature: setProvidedSignature } = const { signature: providedSignature, setSignature: setProvidedSignature } =
useRequiredSigningContext(); useRequiredSigningContext();
@ -108,6 +112,7 @@ export const SignatureField = ({
actionTarget: field.type, actionTarget: field.type,
}); });
}; };
const onSign = async (authOptions?: TRecipientActionAuth, signature?: string) => { const onSign = async (authOptions?: TRecipientActionAuth, signature?: string) => {
try { try {
const value = signature || providedSignature; const value = signature || providedSignature;
@ -117,11 +122,23 @@ export const SignatureField = ({
return; return;
} }
const isTypedSignature = !value.startsWith('data:image');
if (isTypedSignature && !typedSignatureEnabled) {
toast({
title: _(msg`Error`),
description: _(msg`Typed signatures are not allowed. Please draw your signature.`),
variant: 'destructive',
});
return;
}
const payload: TSignFieldWithTokenMutationSchema = { const payload: TSignFieldWithTokenMutationSchema = {
token: recipient.token, token: recipient.token,
fieldId: field.id, fieldId: field.id,
value, value,
isBase64: true, isBase64: !isTypedSignature,
authOptions, authOptions,
}; };
@ -176,6 +193,41 @@ export const SignatureField = ({
} }
}; };
useLayoutEffect(() => {
if (!signatureRef.current || !containerRef.current || !signature?.typedSignature) {
return;
}
const adjustTextSize = () => {
const container = containerRef.current;
const text = signatureRef.current;
if (!container || !text) {
return;
}
let size = 2;
text.style.fontSize = `${size}rem`;
while (
(text.scrollWidth > container.clientWidth || text.scrollHeight > container.clientHeight) &&
size > 0.8
) {
size -= 0.1;
text.style.fontSize = `${size}rem`;
}
setFontSize(size);
};
const resizeObserver = new ResizeObserver(adjustTextSize);
resizeObserver.observe(containerRef.current);
adjustTextSize();
return () => resizeObserver.disconnect();
}, [signature?.typedSignature]);
return ( return (
<SigningFieldContainer <SigningFieldContainer
field={field} field={field}
@ -205,10 +257,15 @@ export const SignatureField = ({
)} )}
{state === 'signed-text' && ( {state === 'signed-text' && (
<p className="font-signature text-muted-foreground dark:text-background text-lg duration-200 sm:text-xl md:text-2xl lg:text-3xl"> <div ref={containerRef} className="flex h-full w-full items-center justify-center p-2">
{/* This optional chaining is intentional, we don't want to move the check into the condition above */} <p
{signature?.typedSignature} ref={signatureRef}
</p> className="font-signature text-muted-foreground dark:text-background w-full overflow-hidden break-all text-center leading-tight duration-200"
style={{ fontSize: `${fontSize}rem` }}
>
{signature?.typedSignature}
</p>
</div>
)} )}
<Dialog open={showSignatureModal} onOpenChange={setShowSignatureModal}> <Dialog open={showSignatureModal} onOpenChange={setShowSignatureModal}>

View File

@ -55,7 +55,10 @@ export const SigningPageView = ({
return ( return (
<div className="mx-auto w-full max-w-screen-xl"> <div className="mx-auto w-full max-w-screen-xl">
<h1 className="mt-4 truncate text-2xl font-semibold md:text-3xl" title={document.title}> <h1
className="mt-4 block max-w-[20rem] truncate text-2xl font-semibold md:max-w-[30rem] md:text-3xl"
title={document.title}
>
{document.title} {document.title}
</h1> </h1>

View File

@ -52,13 +52,7 @@ export default async function TeamsSettingsPage({ params }: TeamsSettingsPagePro
<AvatarImageForm className="mb-8" team={team} user={session.user} /> <AvatarImageForm className="mb-8" team={team} user={session.user} />
<UpdateTeamForm <UpdateTeamForm teamId={team.id} teamName={team.name} teamUrl={team.url} />
teamId={team.id}
teamName={team.name}
teamUrl={team.url}
documentVisibility={team.teamGlobalSettings?.documentVisibility}
includeSenderDetails={team.teamGlobalSettings?.includeSenderDetails}
/>
<section className="mt-6 space-y-6"> <section className="mt-6 space-y-6">
{(team.teamEmail || team.emailVerification) && ( {(team.teamEmail || team.emailVerification) && (

View File

@ -39,6 +39,8 @@ const ZTeamDocumentPreferencesFormSchema = z.object({
documentVisibility: z.nativeEnum(DocumentVisibility), documentVisibility: z.nativeEnum(DocumentVisibility),
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES), documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES),
includeSenderDetails: z.boolean(), includeSenderDetails: z.boolean(),
typedSignatureEnabled: z.boolean(),
includeSigningCertificate: z.boolean(),
}); });
type TTeamDocumentPreferencesFormSchema = z.infer<typeof ZTeamDocumentPreferencesFormSchema>; type TTeamDocumentPreferencesFormSchema = z.infer<typeof ZTeamDocumentPreferencesFormSchema>;
@ -68,6 +70,8 @@ export const TeamDocumentPreferencesForm = ({
? settings?.documentLanguage ? settings?.documentLanguage
: 'en', : 'en',
includeSenderDetails: settings?.includeSenderDetails ?? false, includeSenderDetails: settings?.includeSenderDetails ?? false,
typedSignatureEnabled: settings?.typedSignatureEnabled ?? true,
includeSigningCertificate: settings?.includeSigningCertificate ?? true,
}, },
resolver: zodResolver(ZTeamDocumentPreferencesFormSchema), resolver: zodResolver(ZTeamDocumentPreferencesFormSchema),
}); });
@ -76,7 +80,13 @@ export const TeamDocumentPreferencesForm = ({
const onSubmit = async (data: TTeamDocumentPreferencesFormSchema) => { const onSubmit = async (data: TTeamDocumentPreferencesFormSchema) => {
try { try {
const { documentVisibility, documentLanguage, includeSenderDetails } = data; const {
documentVisibility,
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
} = data;
await updateTeamDocumentPreferences({ await updateTeamDocumentPreferences({
teamId: team.id, teamId: team.id,
@ -84,6 +94,8 @@ export const TeamDocumentPreferencesForm = ({
documentVisibility, documentVisibility,
documentLanguage, documentLanguage,
includeSenderDetails, includeSenderDetails,
typedSignatureEnabled,
includeSigningCertificate,
}, },
}); });
@ -105,7 +117,7 @@ export const TeamDocumentPreferencesForm = ({
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}> <form onSubmit={form.handleSubmit(onSubmit)}>
<fieldset <fieldset
className="flex h-full max-w-xl flex-col gap-y-4" className="flex h-full max-w-xl flex-col gap-y-6"
disabled={form.formState.isSubmitting} disabled={form.formState.isSubmitting}
> >
<FormField <FormField
@ -227,6 +239,67 @@ export const TeamDocumentPreferencesForm = ({
)} )}
/> />
<FormField
control={form.control}
name="typedSignatureEnabled"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>
<Trans>Enable Typed Signature</Trans>
</FormLabel>
<div>
<FormControl className="block">
<Switch
ref={field.ref}
name={field.name}
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</div>
<FormDescription>
<Trans>
Controls whether the recipients can sign the documents using a typed signature.
Enable or disable the typed signature globally.
</Trans>
</FormDescription>
</FormItem>
)}
/>
<FormField
control={form.control}
name="includeSigningCertificate"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>
<Trans>Include the Signing Certificate in the Document</Trans>
</FormLabel>
<div>
<FormControl className="block">
<Switch
ref={field.ref}
name={field.name}
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</div>
<FormDescription>
<Trans>
Controls whether the signing certificate will be included in the document when
it is downloaded. The signing certificate can still be downloaded from the logs
page separately.
</Trans>
</FormDescription>
</FormItem>
)}
/>
<div className="flex flex-row justify-end space-x-4"> <div className="flex flex-row justify-end space-x-4">
<Button type="submit" loading={form.formState.isSubmitting}> <Button type="submit" loading={form.formState.isSubmitting}>
<Trans>Save</Trans> <Trans>Save</Trans>

View File

@ -1,8 +1,12 @@
import { z } from 'zod'; import { z } from 'zod';
import { ZCssVarsSchema } from './css-vars';
export const ZBaseEmbedDataSchema = z.object({ export const ZBaseEmbedDataSchema = z.object({
darkModeDisabled: z.boolean().optional().default(false),
css: z css: z
.string() .string()
.optional() .optional()
.transform((value) => value || undefined), .transform((value) => value || undefined),
cssVars: ZCssVarsSchema.optional().default({}),
}); });

View File

@ -10,6 +10,7 @@ export type EmbedDocumentCompletedPageProps = {
}; };
export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentCompletedPageProps) => { export const EmbedDocumentCompleted = ({ name, signature }: EmbedDocumentCompletedPageProps) => {
console.log({ signature });
return ( return (
<div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6"> <div className="relative mx-auto flex min-h-[100dvh] max-w-screen-lg flex-col items-center justify-center p-6">
<h3 className="text-foreground text-2xl font-semibold"> <h3 className="text-foreground text-2xl font-semibold">

View File

@ -0,0 +1,59 @@
import { colord } from 'colord';
import { toSnakeCase } from 'remeda';
import { z } from 'zod';
export const ZCssVarsSchema = z
.object({
background: z.string().optional().describe('Base background color'),
foreground: z.string().optional().describe('Base text color'),
muted: z.string().optional().describe('Muted/subtle background color'),
mutedForeground: z.string().optional().describe('Muted/subtle text color'),
popover: z.string().optional().describe('Popover/dropdown background color'),
popoverForeground: z.string().optional().describe('Popover/dropdown text color'),
card: z.string().optional().describe('Card background color'),
cardBorder: z.string().optional().describe('Card border color'),
cardBorderTint: z.string().optional().describe('Card border tint/highlight color'),
cardForeground: z.string().optional().describe('Card text color'),
fieldCard: z.string().optional().describe('Field card background color'),
fieldCardBorder: z.string().optional().describe('Field card border color'),
fieldCardForeground: z.string().optional().describe('Field card text color'),
widget: z.string().optional().describe('Widget background color'),
widgetForeground: z.string().optional().describe('Widget text color'),
border: z.string().optional().describe('Default border color'),
input: z.string().optional().describe('Input field border color'),
primary: z.string().optional().describe('Primary action/button color'),
primaryForeground: z.string().optional().describe('Primary action/button text color'),
secondary: z.string().optional().describe('Secondary action/button color'),
secondaryForeground: z.string().optional().describe('Secondary action/button text color'),
accent: z.string().optional().describe('Accent/highlight color'),
accentForeground: z.string().optional().describe('Accent/highlight text color'),
destructive: z.string().optional().describe('Destructive/danger action color'),
destructiveForeground: z.string().optional().describe('Destructive/danger text color'),
ring: z.string().optional().describe('Focus ring color'),
radius: z.string().optional().describe('Border radius size in REM units'),
warning: z.string().optional().describe('Warning/alert color'),
})
.describe('Custom CSS variables for theming');
export type TCssVarsSchema = z.infer<typeof ZCssVarsSchema>;
export const toNativeCssVars = (vars: TCssVarsSchema) => {
const cssVars: Record<string, string> = {};
const { radius, ...colorVars } = vars;
for (const [key, value] of Object.entries(colorVars)) {
if (value) {
const color = colord(value);
const { h, s, l } = color.toHsl();
cssVars[`--${toSnakeCase(key)}`] = `${h} ${s} ${l}`;
}
}
if (radius) {
cssVars[`--radius`] = `${radius}`;
}
return cssVars;
};

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useEffect, useLayoutEffect, useState } from 'react';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
@ -14,7 +14,7 @@ import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-form
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import { validateFieldsInserted } from '@documenso/lib/utils/fields'; import { validateFieldsInserted } from '@documenso/lib/utils/fields';
import type { DocumentMeta, Recipient, TemplateMeta } from '@documenso/prisma/client'; import type { DocumentMeta, Recipient, Signature, TemplateMeta } from '@documenso/prisma/client';
import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client'; import { type DocumentData, type Field, FieldType } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import type { import type {
@ -38,6 +38,7 @@ import { Logo } from '~/components/branding/logo';
import { EmbedClientLoading } from '../../client-loading'; import { EmbedClientLoading } from '../../client-loading';
import { EmbedDocumentCompleted } from '../../completed'; import { EmbedDocumentCompleted } from '../../completed';
import { EmbedDocumentFields } from '../../document-fields'; import { EmbedDocumentFields } from '../../document-fields';
import { injectCss } from '../../util';
import { ZDirectTemplateEmbedDataSchema } from './schema'; import { ZDirectTemplateEmbedDataSchema } from './schema';
export type EmbedDirectTemplateClientPageProps = { export type EmbedDirectTemplateClientPageProps = {
@ -47,6 +48,8 @@ export type EmbedDirectTemplateClientPageProps = {
recipient: Recipient; recipient: Recipient;
fields: Field[]; fields: Field[];
metadata?: DocumentMeta | TemplateMeta | null; metadata?: DocumentMeta | TemplateMeta | null;
hidePoweredBy?: boolean;
isPlatformOrEnterprise?: boolean;
}; };
export const EmbedDirectTemplateClientPage = ({ export const EmbedDirectTemplateClientPage = ({
@ -56,6 +59,8 @@ export const EmbedDirectTemplateClientPage = ({
recipient, recipient,
fields, fields,
metadata, metadata,
hidePoweredBy = false,
isPlatformOrEnterprise = false,
}: EmbedDirectTemplateClientPageProps) => { }: EmbedDirectTemplateClientPageProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
@ -108,9 +113,9 @@ export const EmbedDirectTemplateClientPage = ({
created: new Date(), created: new Date(),
recipientId: 1, recipientId: 1,
fieldId: 1, fieldId: 1,
signatureImageAsBase64: payload.value, signatureImageAsBase64: payload.value.startsWith('data:') ? payload.value : null,
typedSignature: null, typedSignature: payload.value.startsWith('data:') ? null : payload.value,
}; } satisfies Signature;
} }
if (field.type === FieldType.DATE) { if (field.type === FieldType.DATE) {
@ -249,7 +254,7 @@ export const EmbedDirectTemplateClientPage = ({
} }
}; };
useEffect(() => { useLayoutEffect(() => {
const hash = window.location.hash.slice(1); const hash = window.location.hash.slice(1);
try { try {
@ -264,6 +269,17 @@ export const EmbedDirectTemplateClientPage = ({
setFullName(data.name); setFullName(data.name);
setIsNameLocked(!!data.lockName); setIsNameLocked(!!data.lockName);
} }
if (data.darkModeDisabled) {
document.documentElement.classList.add('dark-mode-disabled');
}
if (isPlatformOrEnterprise) {
injectCss({
css: data.css,
cssVars: data.cssVars,
});
}
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
@ -296,8 +312,8 @@ export const EmbedDirectTemplateClientPage = ({
fieldId: 1, fieldId: 1,
recipientId: 1, recipientId: 1,
created: new Date(), created: new Date(),
typedSignature: null, signatureImageAsBase64: signature?.startsWith('data:') ? signature : null,
signatureImageAsBase64: signature, typedSignature: signature?.startsWith('data:') ? null : signature,
}} }}
/> />
); );
@ -452,10 +468,12 @@ export const EmbedDirectTemplateClientPage = ({
/> />
</div> </div>
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100"> {!hidePoweredBy && (
<span>Powered by</span> <div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
<Logo className="ml-2 inline-block h-[14px]" /> <span>Powered by</span>
</div> <Logo className="ml-2 inline-block h-[14px]" />
</div>
)}
</div> </div>
); );
}; };

View File

@ -2,8 +2,11 @@ import { notFound } from 'next/navigation';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token'; import { getTemplateByDirectLinkToken } from '@documenso/lib/server-only/template/get-template-by-direct-link-token';
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth'; import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth'; import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
@ -51,6 +54,14 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem
documentAuth: template.authOptions, documentAuth: template.authOptions,
}); });
const [isPlatformDocument, isEnterpriseDocument] = await Promise.all([
isDocumentPlatform(template),
isUserEnterprise({
userId: template.userId,
teamId: template.teamId ?? undefined,
}),
]);
const isAccessAuthValid = match(derivedRecipientAccessAuth) const isAccessAuthValid = match(derivedRecipientAccessAuth)
.with(DocumentAccessAuth.ACCOUNT, () => user !== null) .with(DocumentAccessAuth.ACCOUNT, () => user !== null)
.with(null, () => true) .with(null, () => true)
@ -72,6 +83,12 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem
const fields = template.Field.filter((field) => field.recipientId === directTemplateRecipientId); const fields = template.Field.filter((field) => field.recipientId === directTemplateRecipientId);
const team = template.teamId
? await getTeamById({ teamId: template.teamId, userId: template.userId }).catch(() => null)
: null;
const hidePoweredBy = team?.teamGlobalSettings?.brandingHidePoweredBy ?? false;
return ( return (
<SigningProvider email={user?.email} fullName={user?.name} signature={user?.signature}> <SigningProvider email={user?.email} fullName={user?.name} signature={user?.signature}>
<DocumentAuthProvider <DocumentAuthProvider
@ -86,6 +103,8 @@ export default async function EmbedDirectTemplatePage({ params }: EmbedDirectTem
recipient={recipient} recipient={recipient}
fields={fields} fields={fields}
metadata={template.templateMeta} metadata={template.templateMeta}
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument}
/> />
</DocumentAuthProvider> </DocumentAuthProvider>
</SigningProvider> </SigningProvider>

View File

@ -58,6 +58,7 @@ export const EmbedDocumentFields = ({
recipient={recipient} recipient={recipient}
onSignField={onSignField} onSignField={onSignField}
onUnsignField={onUnsignField} onUnsignField={onUnsignField}
typedSignatureEnabled={metadata?.typedSignatureEnabled}
/> />
)) ))
.with(FieldType.INITIALS, () => ( .with(FieldType.INITIALS, () => (

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useEffect, useLayoutEffect, useState } from 'react';
import { Trans, msg } from '@lingui/macro'; import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
@ -28,6 +28,7 @@ import { Logo } from '~/components/branding/logo';
import { EmbedClientLoading } from '../../client-loading'; import { EmbedClientLoading } from '../../client-loading';
import { EmbedDocumentCompleted } from '../../completed'; import { EmbedDocumentCompleted } from '../../completed';
import { EmbedDocumentFields } from '../../document-fields'; import { EmbedDocumentFields } from '../../document-fields';
import { injectCss } from '../../util';
import { ZSignDocumentEmbedDataSchema } from './schema'; import { ZSignDocumentEmbedDataSchema } from './schema';
export type EmbedSignDocumentClientPageProps = { export type EmbedSignDocumentClientPageProps = {
@ -38,6 +39,8 @@ export type EmbedSignDocumentClientPageProps = {
fields: Field[]; fields: Field[];
metadata?: DocumentMeta | TemplateMeta | null; metadata?: DocumentMeta | TemplateMeta | null;
isCompleted?: boolean; isCompleted?: boolean;
hidePoweredBy?: boolean;
isPlatformOrEnterprise?: boolean;
}; };
export const EmbedSignDocumentClientPage = ({ export const EmbedSignDocumentClientPage = ({
@ -48,6 +51,8 @@ export const EmbedSignDocumentClientPage = ({
fields, fields,
metadata, metadata,
isCompleted, isCompleted,
hidePoweredBy = false,
isPlatformOrEnterprise = false,
}: EmbedSignDocumentClientPageProps) => { }: EmbedSignDocumentClientPageProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
@ -131,7 +136,7 @@ export const EmbedSignDocumentClientPage = ({
} }
}; };
useEffect(() => { useLayoutEffect(() => {
const hash = window.location.hash.slice(1); const hash = window.location.hash.slice(1);
try { try {
@ -144,6 +149,17 @@ export const EmbedSignDocumentClientPage = ({
// Since a recipient can be provided a name we can lock it without requiring // Since a recipient can be provided a name we can lock it without requiring
// a to be provided by the parent application, unlike direct templates. // a to be provided by the parent application, unlike direct templates.
setIsNameLocked(!!data.lockName); setIsNameLocked(!!data.lockName);
if (data.darkModeDisabled) {
document.documentElement.classList.add('dark-mode-disabled');
}
if (isPlatformOrEnterprise) {
injectCss({
css: data.css,
cssVars: data.cssVars,
});
}
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
@ -176,8 +192,8 @@ export const EmbedSignDocumentClientPage = ({
fieldId: 1, fieldId: 1,
recipientId: 1, recipientId: 1,
created: new Date(), created: new Date(),
typedSignature: null, signatureImageAsBase64: signature?.startsWith('data:') ? signature : null,
signatureImageAsBase64: signature, typedSignature: signature?.startsWith('data:') ? null : signature,
}} }}
/> />
); );
@ -202,7 +218,7 @@ export const EmbedSignDocumentClientPage = ({
className="group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0" className="group/document-widget fixed bottom-8 left-0 z-50 h-fit w-full flex-shrink-0 px-6 md:sticky md:top-4 md:z-auto md:w-[350px] md:px-0"
data-expanded={isExpanded || undefined} data-expanded={isExpanded || undefined}
> >
<div className="border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6"> <div className="border-border bg-widget flex w-full flex-col rounded-xl border px-4 py-4 md:py-6">
{/* Header */} {/* Header */}
<div> <div>
<div className="flex items-center justify-between gap-x-2"> <div className="flex items-center justify-between gap-x-2">
@ -325,10 +341,12 @@ export const EmbedSignDocumentClientPage = ({
<EmbedDocumentFields recipient={recipient} fields={fields} metadata={metadata} /> <EmbedDocumentFields recipient={recipient} fields={fields} metadata={metadata} />
</div> </div>
<div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100"> {!hidePoweredBy && (
<span>Powered by</span> <div className="bg-primary text-primary-foreground fixed bottom-0 left-0 z-40 rounded-tr px-2 py-1 text-xs font-medium opacity-60 hover:opacity-100">
<Logo className="ml-2 inline-block h-[14px]" /> <span>Powered by</span>
</div> <Logo className="ml-2 inline-block h-[14px]" />
</div>
)}
</div> </div>
); );
}; };

View File

@ -2,11 +2,14 @@ import { notFound } from 'next/navigation';
import { match } from 'ts-pattern'; import { match } from 'ts-pattern';
import { isUserEnterprise } from '@documenso/ee/server-only/util/is-document-enterprise';
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app'; import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token'; import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token'; import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
import { getTeamById } from '@documenso/lib/server-only/team/get-team';
import { DocumentAccessAuth } from '@documenso/lib/types/document-auth'; import { DocumentAccessAuth } from '@documenso/lib/types/document-auth';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth'; import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { DocumentStatus } from '@documenso/prisma/client'; import { DocumentStatus } from '@documenso/prisma/client';
@ -56,6 +59,14 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
return <EmbedPaywall />; return <EmbedPaywall />;
} }
const [isPlatformDocument, isEnterpriseDocument] = await Promise.all([
isDocumentPlatform(document),
isUserEnterprise({
userId: document.userId,
teamId: document.teamId ?? undefined,
}),
]);
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({ const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
documentAuth: document.authOptions, documentAuth: document.authOptions,
}); });
@ -74,6 +85,12 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
); );
} }
const team = document.teamId
? await getTeamById({ teamId: document.teamId, userId: document.userId }).catch(() => null)
: null;
const hidePoweredBy = team?.teamGlobalSettings?.brandingHidePoweredBy ?? false;
return ( return (
<SigningProvider <SigningProvider
email={recipient.email} email={recipient.email}
@ -93,6 +110,8 @@ export default async function EmbedSignDocumentPage({ params }: EmbedSignDocumen
fields={fields} fields={fields}
metadata={document.documentMeta} metadata={document.documentMeta}
isCompleted={document.status === DocumentStatus.COMPLETED} isCompleted={document.status === DocumentStatus.COMPLETED}
hidePoweredBy={isPlatformDocument || isEnterpriseDocument || hidePoweredBy}
isPlatformOrEnterprise={isPlatformDocument || isEnterpriseDocument}
/> />
</DocumentAuthProvider> </DocumentAuthProvider>
</SigningProvider> </SigningProvider>

View File

@ -0,0 +1,20 @@
import { type TCssVarsSchema, toNativeCssVars } from './css-vars';
export const injectCss = (options: { css?: string; cssVars?: TCssVarsSchema }) => {
const { css, cssVars } = options;
if (css) {
const style = document.createElement('style');
style.innerHTML = css;
document.head.appendChild(style);
}
if (cssVars) {
const nativeVars = toNativeCssVars(cssVars);
for (const [key, value] of Object.entries(nativeVars)) {
document.documentElement.style.setProperty(key, value);
}
}
};

View File

@ -6,22 +6,14 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro'; import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { useSession } from 'next-auth/react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
import type { z } from 'zod'; import type { z } from 'zod';
import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app'; import { WEBAPP_BASE_URL } from '@documenso/lib/constants/app';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { DocumentVisibility } from '@documenso/prisma/client';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
import { ZUpdateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema'; import { ZUpdateTeamMutationSchema } from '@documenso/trpc/server/team-router/schema';
import {
DocumentVisibilitySelect,
DocumentVisibilityTooltip,
} from '@documenso/ui/components/document/document-visibility-select';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { Checkbox } from '@documenso/ui/primitives/checkbox';
import { import {
Form, Form,
FormControl, FormControl,
@ -37,29 +29,17 @@ export type UpdateTeamDialogProps = {
teamId: number; teamId: number;
teamName: string; teamName: string;
teamUrl: string; teamUrl: string;
documentVisibility?: DocumentVisibility;
includeSenderDetails?: boolean;
}; };
const ZUpdateTeamFormSchema = ZUpdateTeamMutationSchema.shape.data.pick({ const ZUpdateTeamFormSchema = ZUpdateTeamMutationSchema.shape.data.pick({
name: true, name: true,
url: true, url: true,
documentVisibility: true,
includeSenderDetails: true,
}); });
type TUpdateTeamFormSchema = z.infer<typeof ZUpdateTeamFormSchema>; type TUpdateTeamFormSchema = z.infer<typeof ZUpdateTeamFormSchema>;
export const UpdateTeamForm = ({ export const UpdateTeamForm = ({ teamId, teamName, teamUrl }: UpdateTeamDialogProps) => {
teamId,
teamName,
teamUrl,
documentVisibility,
includeSenderDetails,
}: UpdateTeamDialogProps) => {
const router = useRouter(); const router = useRouter();
const { data: session } = useSession();
const email = session?.user?.email;
const { _ } = useLingui(); const { _ } = useLingui();
const { toast } = useToast(); const { toast } = useToast();
@ -68,36 +48,17 @@ export const UpdateTeamForm = ({
defaultValues: { defaultValues: {
name: teamName, name: teamName,
url: teamUrl, url: teamUrl,
documentVisibility,
includeSenderDetails,
}, },
}); });
const { mutateAsync: updateTeam } = trpc.team.updateTeam.useMutation(); const { mutateAsync: updateTeam } = trpc.team.updateTeam.useMutation();
const includeSenderDetailsCheck = form.watch('includeSenderDetails');
const mapVisibilityToRole = (visibility: DocumentVisibility): DocumentVisibility => const onFormSubmit = async ({ name, url }: TUpdateTeamFormSchema) => {
match(visibility)
.with(DocumentVisibility.ADMIN, () => DocumentVisibility.ADMIN)
.with(DocumentVisibility.MANAGER_AND_ABOVE, () => DocumentVisibility.MANAGER_AND_ABOVE)
.otherwise(() => DocumentVisibility.EVERYONE);
const currentVisibilityRole = mapVisibilityToRole(
documentVisibility ?? DocumentVisibility.EVERYONE,
);
const onFormSubmit = async ({
name,
url,
documentVisibility,
includeSenderDetails,
}: TUpdateTeamFormSchema) => {
try { try {
await updateTeam({ await updateTeam({
data: { data: {
name, name,
url, url,
documentVisibility,
includeSenderDetails,
}, },
teamId, teamId,
}); });
@ -111,8 +72,6 @@ export const UpdateTeamForm = ({
form.reset({ form.reset({
name, name,
url, url,
documentVisibility,
includeSenderDetails,
}); });
if (url !== teamUrl) { if (url !== teamUrl) {
@ -186,68 +145,6 @@ export const UpdateTeamForm = ({
)} )}
/> />
<FormField
control={form.control}
name="documentVisibility"
render={({ field }) => (
<FormItem>
<FormLabel className="mt-4 flex flex-row items-center">
<Trans>Default Document Visibility</Trans>
<DocumentVisibilityTooltip />
</FormLabel>
<FormControl>
<DocumentVisibilitySelect
currentMemberRole={currentVisibilityRole}
isTeamSettings={true}
{...field}
onValueChange={field.onChange}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="mb-4">
<FormField
control={form.control}
name="includeSenderDetails"
render={({ field }) => (
<FormItem>
<div className="mt-6 flex flex-row items-center gap-4">
<FormLabel>
<Trans>Send on Behalf of Team</Trans>
</FormLabel>
<FormControl>
<Checkbox
className="h-5 w-5"
checkClassName="text-white"
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</div>
{includeSenderDetailsCheck ? (
<blockquote className="text-foreground/50 text-xs italic">
<Trans>
"{email}" on behalf of "{teamName}" has invited you to sign "example
document".
</Trans>
</blockquote>
) : (
<blockquote className="text-foreground/50 text-xs italic">
<Trans>"{teamUrl}" has invited you to sign "example document".</Trans>
</blockquote>
)}
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex flex-row justify-end space-x-4"> <div className="flex flex-row justify-end space-x-4">
<AnimatePresence> <AnimatePresence>
{form.formState.isDirty && ( {form.formState.isDirty && (

View File

@ -138,6 +138,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => {
containerClassName={cn('rounded-lg border bg-background')} containerClassName={cn('rounded-lg border bg-background')}
defaultValue={user.signature ?? undefined} defaultValue={user.signature ?? undefined}
onChange={(v) => onChange(v ?? '')} onChange={(v) => onChange(v ?? '')}
allowTypedSignature={true}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

773
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"private": true, "private": true,
"version": "1.8.1-rc.0", "version": "1.8.1-rc.1",
"scripts": { "scripts": {
"build": "turbo run build", "build": "turbo run build",
"build:web": "turbo run build --filter=@documenso/web", "build:web": "turbo run build --filter=@documenso/web",
@ -52,7 +52,7 @@
"husky": "^9.0.11", "husky": "^9.0.11",
"lint-staged": "^15.2.2", "lint-staged": "^15.2.2",
"playwright": "1.43.0", "playwright": "1.43.0",
"prettier": "^2.5.1", "prettier": "^3.3.3",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",
"turbo": "^1.9.3" "turbo": "^1.9.3"
}, },

View File

@ -302,6 +302,7 @@ export const ApiContractV1Implementation = createNextRoute(ApiContractV1, {
redirectUrl: body.meta.redirectUrl, redirectUrl: body.meta.redirectUrl,
signingOrder: body.meta.signingOrder, signingOrder: body.meta.signingOrder,
language: body.meta.language, language: body.meta.language,
typedSignatureEnabled: body.meta.typedSignatureEnabled,
requestMetadata: extractNextApiRequestMetadata(args.req), requestMetadata: extractNextApiRequestMetadata(args.req),
}); });

View File

@ -3,7 +3,6 @@ import { z } from 'zod';
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n'; import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import '@documenso/lib/constants/time-zones';
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 { ZUrlSchema } from '@documenso/lib/schemas/common'; import { ZUrlSchema } from '@documenso/lib/schemas/common';
import { import {
@ -14,6 +13,7 @@ import {
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta'; import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { import {
DocumentDataType, DocumentDataType,
DocumentDistributionMethod,
DocumentSigningOrder, DocumentSigningOrder,
FieldType, FieldType,
ReadStatus, ReadStatus,
@ -132,6 +132,7 @@ export const ZCreateDocumentMutationSchema = z.object({
redirectUrl: z.string(), redirectUrl: z.string(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(), signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(), language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
typedSignatureEnabled: z.boolean().optional().default(true),
}) })
.partial(), .partial(),
authOptions: z authOptions: z
@ -226,14 +227,14 @@ export type TCreateDocumentFromTemplateMutationResponseSchema = z.infer<
export const ZGenerateDocumentFromTemplateMutationSchema = z.object({ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
title: z.string().optional(), title: z.string().optional(),
externalId: z.string().nullish(), externalId: z.string().optional(),
recipients: z recipients: z
.array( .array(
z.object({ z.object({
id: z.number(), id: z.number(),
email: z.string().email(),
name: z.string().optional(), name: z.string().optional(),
email: z.string().email().min(1), signingOrder: z.number().optional(),
signingOrder: z.number().nullish(),
}), }),
) )
.refine( .refine(
@ -252,8 +253,10 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
timezone: z.string(), timezone: z.string(),
dateFormat: z.string(), dateFormat: z.string(),
redirectUrl: ZUrlSchema, redirectUrl: ZUrlSchema,
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(), signingOrder: z.nativeEnum(DocumentSigningOrder),
language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(), language: z.enum(SUPPORTED_LANGUAGE_CODES),
distributionMethod: z.nativeEnum(DocumentDistributionMethod),
typedSignatureEnabled: z.boolean(),
}) })
.partial() .partial()
.optional(), .optional(),

View File

@ -0,0 +1,271 @@
import { expect, test } from '@playwright/test';
import { PDFDocument } from 'pdf-lib';
import { getDocumentByToken } from '@documenso/lib/server-only/document/get-document-by-token';
import { getFile } from '@documenso/lib/universal/upload/get-file';
import { prisma } from '@documenso/prisma';
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
import { seedTeam } from '@documenso/prisma/seed/teams';
import { seedUser } from '@documenso/prisma/seed/users';
import { apiSignin } from '../fixtures/authentication';
test.describe('Signing Certificate Tests', () => {
test('individual document should always include signing certificate', async ({ page }) => {
const user = await seedUser();
const { document, recipients } = await seedPendingDocumentWithFullFields({
owner: user,
recipients: ['signer@example.com'],
fields: [FieldType.SIGNATURE],
});
const documentData = await prisma.documentData
.findFirstOrThrow({
where: {
id: document.documentDataId,
},
})
.then(async (data) => getFile(data));
const originalPdf = await PDFDocument.load(documentData);
const recipient = recipients[0];
// Sign the document
await page.goto(`/sign/${recipient.token}`);
const canvas = page.locator('canvas');
const box = await canvas.boundingBox();
if (box) {
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + box.width / 4, box.y + box.height / 4);
await page.mouse.up();
}
for (const field of recipient.Field) {
await page.locator(`#field-${field.id}`).getByRole('button').click();
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
}
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(`/sign/${recipient.token}/complete`);
await expect(async () => {
const { status } = await getDocumentByToken({
token: recipient.token,
});
expect(status).toBe(DocumentStatus.COMPLETED);
}).toPass();
// Get the completed document
const completedDocument = await prisma.document.findFirstOrThrow({
where: { id: document.id },
include: { documentData: true },
});
const completedDocumentData = await getFile(completedDocument.documentData);
// Load the PDF and check number of pages
const pdfDoc = await PDFDocument.load(completedDocumentData);
expect(pdfDoc.getPageCount()).toBe(originalPdf.getPageCount() + 1); // Original + Certificate
});
test('team document with signing certificate enabled should include certificate', async ({
page,
}) => {
const team = await seedTeam();
const { document, recipients } = await seedPendingDocumentWithFullFields({
owner: team.owner,
recipients: ['signer@example.com'],
fields: [FieldType.SIGNATURE],
updateDocumentOptions: {
teamId: team.id,
},
});
await prisma.teamGlobalSettings.create({
data: {
teamId: team.id,
includeSigningCertificate: true,
},
});
const documentData = await prisma.documentData
.findFirstOrThrow({
where: {
id: document.documentDataId,
},
})
.then(async (data) => getFile(data));
const originalPdf = await PDFDocument.load(documentData);
const recipient = recipients[0];
// Sign the document
await page.goto(`/sign/${recipient.token}`);
const canvas = page.locator('canvas');
const box = await canvas.boundingBox();
if (box) {
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + box.width / 4, box.y + box.height / 4);
await page.mouse.up();
}
for (const field of recipient.Field) {
await page.locator(`#field-${field.id}`).getByRole('button').click();
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
}
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(`/sign/${recipient.token}/complete`);
await expect(async () => {
const { status } = await getDocumentByToken({
token: recipient.token,
});
expect(status).toBe(DocumentStatus.COMPLETED);
}).toPass();
// Get the completed document
const completedDocument = await prisma.document.findFirstOrThrow({
where: { id: document.id },
include: { documentData: true },
});
const completedDocumentData = await getFile(completedDocument.documentData);
// Load the PDF and check number of pages
const completedPdf = await PDFDocument.load(completedDocumentData);
expect(completedPdf.getPageCount()).toBe(originalPdf.getPageCount() + 1); // Original + Certificate
});
test('team document with signing certificate disabled should not include certificate', async ({
page,
}) => {
const team = await seedTeam();
const { document, recipients } = await seedPendingDocumentWithFullFields({
owner: team.owner,
recipients: ['signer@example.com'],
fields: [FieldType.SIGNATURE],
updateDocumentOptions: {
teamId: team.id,
},
});
await prisma.teamGlobalSettings.create({
data: {
teamId: team.id,
includeSigningCertificate: false,
},
});
const documentData = await prisma.documentData
.findFirstOrThrow({
where: {
id: document.documentDataId,
},
})
.then(async (data) => getFile(data));
const originalPdf = await PDFDocument.load(documentData);
const recipient = recipients[0];
// Sign the document
await page.goto(`/sign/${recipient.token}`);
const canvas = page.locator('canvas');
const box = await canvas.boundingBox();
if (box) {
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
await page.mouse.down();
await page.mouse.move(box.x + box.width / 4, box.y + box.height / 4);
await page.mouse.up();
}
for (const field of recipient.Field) {
await page.locator(`#field-${field.id}`).getByRole('button').click();
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
}
await page.getByRole('button', { name: 'Complete' }).click();
await page.getByRole('button', { name: 'Sign' }).click();
await page.waitForURL(`/sign/${recipient.token}/complete`);
await expect(async () => {
const { status } = await getDocumentByToken({
token: recipient.token,
});
expect(status).toBe(DocumentStatus.COMPLETED);
}).toPass();
// Get the completed document
const completedDocument = await prisma.document.findFirstOrThrow({
where: { id: document.id },
include: { documentData: true },
});
const completedDocumentData = await getFile(completedDocument.documentData);
// Load the PDF and check number of pages
const completedPdf = await PDFDocument.load(completedDocumentData);
expect(completedPdf.getPageCount()).toBe(originalPdf.getPageCount());
});
test('team can toggle signing certificate setting', async ({ page }) => {
const team = await seedTeam();
await apiSignin({
page,
email: team.owner.email,
redirectPath: `/t/${team.url}/settings/preferences`,
});
// Toggle signing certificate setting
await page.getByLabel('Include the Signing Certificate in the Document').click();
await page.getByRole('button', { name: /Save/ }).first().click();
await page.waitForTimeout(1000);
// Verify the setting was saved
const updatedTeam = await prisma.team.findFirstOrThrow({
where: { id: team.id },
include: { teamGlobalSettings: true },
});
expect(updatedTeam.teamGlobalSettings?.includeSigningCertificate).toBe(false);
// Toggle the setting back to true
await page.getByLabel('Include the Signing Certificate in the Document').click();
await page.getByRole('button', { name: /Save/ }).first().click();
await page.waitForTimeout(1000);
// Verify the setting was saved
const updatedTeam2 = await prisma.team.findFirstOrThrow({
where: { id: team.id },
include: { teamGlobalSettings: true },
});
expect(updatedTeam2.teamGlobalSettings?.includeSigningCertificate).toBe(true);
});
});

View File

@ -17,19 +17,17 @@ test('[TEAMS]: update the default document visibility in the team global setting
page, page,
email: team.owner.email, email: team.owner.email,
password: 'password', password: 'password',
redirectPath: `/t/${team.url}/settings`, redirectPath: `/t/${team.url}/settings/preferences`,
}); });
await page.getByRole('combobox').click(); // !: Brittle selector
await page.getByRole('combobox').first().click();
await page.getByRole('option', { name: 'Admin' }).click(); await page.getByRole('option', { name: 'Admin' }).click();
await page.getByRole('button', { name: 'Update team' }).click(); await page.getByRole('button', { name: 'Save' }).first().click();
const toast = page.locator('li[role="status"][data-state="open"]').first(); const toast = page.locator('li[role="status"][data-state="open"]').first();
await expect(toast).toBeVisible(); await expect(toast).toBeVisible();
await expect(toast.getByText('Success', { exact: true })).toBeVisible(); await expect(toast.getByText('Document preferences updated', { exact: true })).toBeVisible();
await expect(
toast.getByText('Your team has been successfully updated.', { exact: true }),
).toBeVisible();
}); });
test('[TEAMS]: update the sender details in the team global settings', async ({ page }) => { test('[TEAMS]: update the sender details in the team global settings', async ({ page }) => {
@ -41,7 +39,7 @@ test('[TEAMS]: update the sender details in the team global settings', async ({
page, page,
email: team.owner.email, email: team.owner.email,
password: 'password', password: 'password',
redirectPath: `/t/${team.url}/settings`, redirectPath: `/t/${team.url}/settings/preferences`,
}); });
const checkbox = page.getByLabel('Send on Behalf of Team'); const checkbox = page.getByLabel('Send on Behalf of Team');
@ -49,14 +47,11 @@ test('[TEAMS]: update the sender details in the team global settings', async ({
await expect(checkbox).toBeChecked(); await expect(checkbox).toBeChecked();
await page.getByRole('button', { name: 'Update team' }).click(); await page.getByRole('button', { name: 'Save' }).first().click();
const toast = page.locator('li[role="status"][data-state="open"]').first(); const toast = page.locator('li[role="status"][data-state="open"]').first();
await expect(toast).toBeVisible(); await expect(toast).toBeVisible();
await expect(toast.getByText('Success', { exact: true })).toBeVisible(); await expect(toast.getByText('Document preferences updated', { exact: true })).toBeVisible();
await expect(
toast.getByText('Your team has been successfully updated.', { exact: true }),
).toBeVisible();
await expect(checkbox).toBeChecked(); await expect(checkbox).toBeChecked();
}); });

View File

@ -7,15 +7,17 @@
"scripts": { "scripts": {
"test:dev": "NODE_OPTIONS=--experimental-require-module playwright test", "test:dev": "NODE_OPTIONS=--experimental-require-module playwright test",
"test-ui:dev": "NODE_OPTIONS=--experimental-require-module playwright test --ui", "test-ui:dev": "NODE_OPTIONS=--experimental-require-module playwright test --ui",
"test:e2e": "NODE_OPTIONS=--experimental-require-module start-server-and-test \"npm run start -w @documenso/web\" http://localhost:3000 \"playwright test\"" "test:e2e": "NODE_OPTIONS=--experimental-require-module start-server-and-test \"npm run start -w @documenso/web\" http://localhost:3000 \"playwright test $E2E_TEST_PATH\""
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.18.1", "@playwright/test": "^1.18.1",
"@types/node": "^20.8.2", "@types/node": "^20.8.2",
"@documenso/lib": "*",
"@documenso/prisma": "*", "@documenso/prisma": "*",
"@documenso/web": "*" "@documenso/web": "*",
"pdf-lib": "^1.17.1"
}, },
"dependencies": { "dependencies": {
"start-server-and-test": "^2.0.1" "start-server-and-test": "^2.0.1"

View File

@ -9,6 +9,7 @@ export const getDocumentRelatedPrices = async () => {
return await getPricesByPlan([ return await getPricesByPlan([
STRIPE_PLAN_TYPE.REGULAR, STRIPE_PLAN_TYPE.REGULAR,
STRIPE_PLAN_TYPE.COMMUNITY, STRIPE_PLAN_TYPE.COMMUNITY,
STRIPE_PLAN_TYPE.PLATFORM,
STRIPE_PLAN_TYPE.ENTERPRISE, STRIPE_PLAN_TYPE.ENTERPRISE,
]); ]);
}; };

View File

@ -0,0 +1,13 @@
import { STRIPE_PLAN_TYPE } from '@documenso/lib/constants/billing';
import { getPricesByPlan } from './get-prices-by-plan';
export const getPlatformPlanPrices = async () => {
return await getPricesByPlan(STRIPE_PLAN_TYPE.PLATFORM);
};
export const getPlatformPlanPriceIds = async () => {
const prices = await getPlatformPlanPrices();
return prices.map((price) => price.id);
};

View File

@ -9,6 +9,7 @@ export const getPrimaryAccountPlanPrices = async () => {
return await getPricesByPlan([ return await getPricesByPlan([
STRIPE_PLAN_TYPE.REGULAR, STRIPE_PLAN_TYPE.REGULAR,
STRIPE_PLAN_TYPE.COMMUNITY, STRIPE_PLAN_TYPE.COMMUNITY,
STRIPE_PLAN_TYPE.PLATFORM,
STRIPE_PLAN_TYPE.ENTERPRISE, STRIPE_PLAN_TYPE.ENTERPRISE,
]); ]);
}; };

View File

@ -6,7 +6,11 @@ import { getPricesByPlan } from './get-prices-by-plan';
* Returns the Stripe prices of items that affect the amount of teams a user can create. * Returns the Stripe prices of items that affect the amount of teams a user can create.
*/ */
export const getTeamRelatedPrices = async () => { export const getTeamRelatedPrices = async () => {
return await getPricesByPlan([STRIPE_PLAN_TYPE.COMMUNITY, STRIPE_PLAN_TYPE.ENTERPRISE]); return await getPricesByPlan([
STRIPE_PLAN_TYPE.COMMUNITY,
STRIPE_PLAN_TYPE.PLATFORM,
STRIPE_PLAN_TYPE.ENTERPRISE,
]);
}; };
/** /**

View File

@ -0,0 +1,61 @@
import { IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
import { subscriptionsContainsActivePlan } from '@documenso/lib/utils/billing';
import { prisma } from '@documenso/prisma';
import type { Document, Subscription } from '@documenso/prisma/client';
import { getPlatformPlanPriceIds } from '../stripe/get-platform-plan-prices';
export type IsDocumentPlatformOptions = Pick<Document, 'id' | 'userId' | 'teamId'>;
/**
* Whether the user is platform, or has permission to use platform features on
* behalf of their team.
*
* It is assumed that the provided user is part of the provided team.
*/
export const isDocumentPlatform = async ({
userId,
teamId,
}: IsDocumentPlatformOptions): Promise<boolean> => {
let subscriptions: Subscription[] = [];
if (!IS_BILLING_ENABLED()) {
return true;
}
if (teamId) {
subscriptions = await prisma.team
.findFirstOrThrow({
where: {
id: teamId,
},
select: {
owner: {
include: {
Subscription: true,
},
},
},
})
.then((team) => team.owner.Subscription);
} else {
subscriptions = await prisma.user
.findFirstOrThrow({
where: {
id: userId,
},
select: {
Subscription: true,
},
})
.then((user) => user.Subscription);
}
if (subscriptions.length === 0) {
return false;
}
const platformPlanPriceIds = await getPlatformPlanPriceIds();
return subscriptionsContainsActivePlan(subscriptions, platformPlanPriceIds);
};

View File

@ -7,5 +7,6 @@ export enum STRIPE_PLAN_TYPE {
REGULAR = 'regular', REGULAR = 'regular',
TEAM = 'team', TEAM = 'team',
COMMUNITY = 'community', COMMUNITY = 'community',
PLATFORM = 'platform',
ENTERPRISE = 'enterprise', ENTERPRISE = 'enterprise',
} }

View File

@ -17,12 +17,14 @@ const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
documentVisibility: z.nativeEnum(DocumentVisibility), documentVisibility: z.nativeEnum(DocumentVisibility),
documentLanguage: z.string(), documentLanguage: z.string(),
includeSenderDetails: z.boolean(), includeSenderDetails: z.boolean(),
includeSigningCertificate: z.boolean(),
brandingEnabled: z.boolean(), brandingEnabled: z.boolean(),
brandingLogo: z.string(), brandingLogo: z.string(),
brandingUrl: z.string(), brandingUrl: z.string(),
brandingCompanyDetails: z.string(), brandingCompanyDetails: z.string(),
brandingHidePoweredBy: z.boolean(), brandingHidePoweredBy: z.boolean(),
teamId: z.number(), teamId: z.number(),
typedSignatureEnabled: z.boolean(),
}) })
.nullish(), .nullish(),
}), }),

View File

@ -57,7 +57,17 @@ export const SEAL_DOCUMENT_JOB_DEFINITION = {
}, },
}, },
include: { include: {
documentMeta: true,
Recipient: true, Recipient: true,
team: {
select: {
teamGlobalSettings: {
select: {
includeSigningCertificate: true,
},
},
},
},
}, },
}); });
@ -117,7 +127,13 @@ export const SEAL_DOCUMENT_JOB_DEFINITION = {
} }
const pdfData = await getFile(documentData); const pdfData = await getFile(documentData);
const certificateData = await getCertificatePdf({ documentId }).catch(() => null); const certificateData =
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
? await getCertificatePdf({
documentId,
language: document.documentMeta?.language,
}).catch(() => null)
: null;
const newDataId = await io.runTask('decorate-and-sign-pdf', async () => { const newDataId = await io.runTask('decorate-and-sign-pdf', async () => {
const pdfDoc = await PDFDocument.load(pdfData); const pdfDoc = await PDFDocument.load(pdfData);

View File

@ -51,7 +51,7 @@
"pg": "^8.11.3", "pg": "^8.11.3",
"playwright": "1.43.0", "playwright": "1.43.0",
"react": "^18", "react": "^18",
"remeda": "^2.12.1", "remeda": "^2.17.3",
"sharp": "0.32.6", "sharp": "0.32.6",
"stripe": "^12.7.0", "stripe": "^12.7.0",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",

View File

@ -112,6 +112,7 @@ export const createDocument = async ({
documentMeta: { documentMeta: {
create: { create: {
language: team?.teamGlobalSettings?.documentLanguage, language: team?.teamGlobalSettings?.documentLanguage,
typedSignatureEnabled: team?.teamGlobalSettings?.typedSignatureEnabled,
}, },
}, },
}, },

View File

@ -10,7 +10,6 @@ import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/
import { WebhookTriggerEvents } from '@documenso/prisma/client'; import { WebhookTriggerEvents } from '@documenso/prisma/client';
import { signPdf } from '@documenso/signing'; import { signPdf } from '@documenso/signing';
import { ZSupportedLanguageCodeSchema } from '../../constants/i18n';
import type { RequestMetadata } from '../../universal/extract-request-metadata'; import type { RequestMetadata } from '../../universal/extract-request-metadata';
import { getFile } from '../../universal/upload/get-file'; import { getFile } from '../../universal/upload/get-file';
import { putPdfFile } from '../../universal/upload/put-file'; import { putPdfFile } from '../../universal/upload/put-file';
@ -48,6 +47,15 @@ export const sealDocument = async ({
documentData: true, documentData: true,
documentMeta: true, documentMeta: true,
Recipient: true, Recipient: true,
team: {
select: {
teamGlobalSettings: {
select: {
includeSigningCertificate: true,
},
},
},
},
}, },
}); });
@ -92,11 +100,13 @@ export const sealDocument = async ({
// !: Need to write the fields onto the document as a hard copy // !: Need to write the fields onto the document as a hard copy
const pdfData = await getFile(documentData); const pdfData = await getFile(documentData);
const documentLanguage = ZSupportedLanguageCodeSchema.parse(document.documentMeta?.language); const certificateData =
(document.team?.teamGlobalSettings?.includeSigningCertificate ?? true)
const certificate = await getCertificatePdf({ documentId, language: documentLanguage }) ? await getCertificatePdf({
.then(async (doc) => PDFDocument.load(doc)) documentId,
.catch(() => null); language: document.documentMeta?.language,
}).catch(() => null)
: null;
const doc = await PDFDocument.load(pdfData); const doc = await PDFDocument.load(pdfData);
@ -105,7 +115,9 @@ export const sealDocument = async ({
flattenForm(doc); flattenForm(doc);
flattenAnnotations(doc); flattenAnnotations(doc);
if (certificate) { if (certificateData) {
const certificate = await PDFDocument.load(certificateData);
const certificatePages = await doc.copyPages(certificate, certificate.getPageIndices()); const certificatePages = await doc.copyPages(certificate, certificate.getPageIndices());
certificatePages.forEach((page) => { certificatePages.forEach((page) => {

View File

@ -177,6 +177,10 @@ export const signFieldWithToken = async ({
throw new Error('Signature field must have a signature'); throw new Error('Signature field must have a signature');
} }
if (isSignatureField && !documentMeta?.typedSignatureEnabled && typedSignature) {
throw new Error('Typed signatures are not allowed. Please draw your signature');
}
return await prisma.$transaction(async (tx) => { return await prisma.$transaction(async (tx) => {
const updatedField = await tx.field.update({ const updatedField = await tx.field.update({
where: { where: {

View File

@ -2,12 +2,13 @@ import { DateTime } from 'luxon';
import type { Browser } from 'playwright'; import type { Browser } from 'playwright';
import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app'; import { NEXT_PUBLIC_WEBAPP_URL } from '../../constants/app';
import type { SupportedLanguageCodes } from '../../constants/i18n'; import { type SupportedLanguageCodes, isValidLanguageCode } from '../../constants/i18n';
import { encryptSecondaryData } from '../crypto/encrypt'; import { encryptSecondaryData } from '../crypto/encrypt';
export type GetCertificatePdfOptions = { export type GetCertificatePdfOptions = {
documentId: number; documentId: number;
language?: SupportedLanguageCodes; // eslint-disable-next-line @typescript-eslint/ban-types
language?: SupportedLanguageCodes | (string & {});
}; };
export const getCertificatePdf = async ({ documentId, language }: GetCertificatePdfOptions) => { export const getCertificatePdf = async ({ documentId, language }: GetCertificatePdfOptions) => {
@ -38,15 +39,15 @@ export const getCertificatePdf = async ({ documentId, language }: GetCertificate
const page = await browserContext.newPage(); const page = await browserContext.newPage();
if (language) { const lang = isValidLanguageCode(language) ? language : 'en';
await page.context().addCookies([
{ await page.context().addCookies([
name: 'language', {
value: language, name: 'language',
url: NEXT_PUBLIC_WEBAPP_URL(), value: lang,
}, url: NEXT_PUBLIC_WEBAPP_URL(),
]); },
} ]);
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encryptedId}`, { await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/__htmltopdf/certificate?d=${encryptedId}`, {
waitUntil: 'networkidle', waitUntil: 'networkidle',

View File

@ -82,7 +82,10 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
const fieldX = pageWidth * (Number(field.positionX) / 100); const fieldX = pageWidth * (Number(field.positionX) / 100);
const fieldY = pageHeight * (Number(field.positionY) / 100); const fieldY = pageHeight * (Number(field.positionY) / 100);
const font = await pdf.embedFont(isSignatureField ? fontCaveat : fontNoto); const font = await pdf.embedFont(
isSignatureField ? fontCaveat : fontNoto,
isSignatureField ? { features: { calt: false } } : undefined,
);
if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) { if (field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) {
await pdf.embedFont(fontCaveat); await pdf.embedFont(fontCaveat);
@ -92,45 +95,89 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
.with( .with(
{ {
type: P.union(FieldType.SIGNATURE, FieldType.FREE_SIGNATURE), type: P.union(FieldType.SIGNATURE, FieldType.FREE_SIGNATURE),
Signature: { signatureImageAsBase64: P.string },
}, },
async (field) => { async (field) => {
const image = await pdf.embedPng(field.Signature?.signatureImageAsBase64 ?? ''); if (field.Signature?.signatureImageAsBase64) {
const image = await pdf.embedPng(field.Signature?.signatureImageAsBase64 ?? '');
let imageWidth = image.width; let imageWidth = image.width;
let imageHeight = image.height; let imageHeight = image.height;
const scalingFactor = Math.min(fieldWidth / imageWidth, fieldHeight / imageHeight, 1); const scalingFactor = Math.min(fieldWidth / imageWidth, fieldHeight / imageHeight, 1);
imageWidth = imageWidth * scalingFactor; imageWidth = imageWidth * scalingFactor;
imageHeight = imageHeight * scalingFactor; imageHeight = imageHeight * scalingFactor;
let imageX = fieldX + (fieldWidth - imageWidth) / 2; let imageX = fieldX + (fieldWidth - imageWidth) / 2;
let imageY = fieldY + (fieldHeight - imageHeight) / 2; let imageY = fieldY + (fieldHeight - imageHeight) / 2;
// Invert the Y axis since PDFs use a bottom-left coordinate system // Invert the Y axis since PDFs use a bottom-left coordinate system
imageY = pageHeight - imageY - imageHeight; imageY = pageHeight - imageY - imageHeight;
if (pageRotationInDegrees !== 0) { if (pageRotationInDegrees !== 0) {
const adjustedPosition = adjustPositionForRotation( const adjustedPosition = adjustPositionForRotation(
pageWidth, pageWidth,
pageHeight, pageHeight,
imageX, imageX,
imageY, imageY,
pageRotationInDegrees, pageRotationInDegrees,
); );
imageX = adjustedPosition.xPos; imageX = adjustedPosition.xPos;
imageY = adjustedPosition.yPos; imageY = adjustedPosition.yPos;
}
page.drawImage(image, {
x: imageX,
y: imageY,
width: imageWidth,
height: imageHeight,
rotate: degrees(pageRotationInDegrees),
});
} else {
const signatureText = field.Signature?.typedSignature ?? '';
const longestLineInTextForWidth = signatureText
.split('\n')
.sort((a, b) => b.length - a.length)[0];
let fontSize = maxFontSize;
let textWidth = font.widthOfTextAtSize(longestLineInTextForWidth, fontSize);
let textHeight = font.heightAtSize(fontSize);
const scalingFactor = Math.min(fieldWidth / textWidth, fieldHeight / textHeight, 1);
fontSize = Math.max(Math.min(fontSize * scalingFactor, maxFontSize), minFontSize);
textWidth = font.widthOfTextAtSize(longestLineInTextForWidth, fontSize);
textHeight = font.heightAtSize(fontSize);
let textX = fieldX + (fieldWidth - textWidth) / 2;
let textY = fieldY + (fieldHeight - textHeight) / 2;
// Invert the Y axis since PDFs use a bottom-left coordinate system
textY = pageHeight - textY - textHeight;
if (pageRotationInDegrees !== 0) {
const adjustedPosition = adjustPositionForRotation(
pageWidth,
pageHeight,
textX,
textY,
pageRotationInDegrees,
);
textX = adjustedPosition.xPos;
textY = adjustedPosition.yPos;
}
page.drawText(signatureText, {
x: textX,
y: textY,
size: fontSize,
font,
rotate: degrees(pageRotationInDegrees),
});
} }
page.drawImage(image, {
x: imageX,
y: imageY,
width: imageWidth,
height: imageHeight,
rotate: degrees(pageRotationInDegrees),
});
}, },
) )
.with({ type: FieldType.CHECKBOX }, (field) => { .with({ type: FieldType.CHECKBOX }, (field) => {

View File

@ -12,6 +12,8 @@ export type UpdateTeamDocumentSettingsOptions = {
documentVisibility: DocumentVisibility; documentVisibility: DocumentVisibility;
documentLanguage: SupportedLanguageCodes; documentLanguage: SupportedLanguageCodes;
includeSenderDetails: boolean; includeSenderDetails: boolean;
typedSignatureEnabled: boolean;
includeSigningCertificate: boolean;
}; };
}; };
@ -20,7 +22,13 @@ export const updateTeamDocumentSettings = async ({
teamId, teamId,
settings, settings,
}: UpdateTeamDocumentSettingsOptions) => { }: UpdateTeamDocumentSettingsOptions) => {
const { documentVisibility, documentLanguage, includeSenderDetails } = settings; const {
documentVisibility,
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
} = settings;
const member = await prisma.teamMember.findFirst({ const member = await prisma.teamMember.findFirst({
where: { where: {
@ -42,11 +50,15 @@ export const updateTeamDocumentSettings = async ({
documentVisibility, documentVisibility,
documentLanguage, documentLanguage,
includeSenderDetails, includeSenderDetails,
typedSignatureEnabled,
includeSigningCertificate,
}, },
update: { update: {
documentVisibility, documentVisibility,
documentLanguage, documentLanguage,
includeSenderDetails, includeSenderDetails,
typedSignatureEnabled,
includeSigningCertificate,
}, },
}); });
}; };

View File

@ -4,7 +4,6 @@ import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import { Prisma } from '@documenso/prisma/client'; import { Prisma } from '@documenso/prisma/client';
import type { DocumentVisibility } from '@documenso/prisma/client';
export type UpdateTeamOptions = { export type UpdateTeamOptions = {
userId: number; userId: number;
@ -12,8 +11,6 @@ export type UpdateTeamOptions = {
data: { data: {
name?: string; name?: string;
url?: string; url?: string;
documentVisibility?: DocumentVisibility;
includeSenderDetails?: boolean;
}; };
}; };
@ -45,18 +42,6 @@ export const updateTeam = async ({ userId, teamId, data }: UpdateTeamOptions) =>
data: { data: {
url: data.url, url: data.url,
name: data.name, name: data.name,
teamGlobalSettings: {
upsert: {
create: {
documentVisibility: data.documentVisibility,
includeSenderDetails: data.includeSenderDetails,
},
update: {
documentVisibility: data.documentVisibility,
includeSenderDetails: data.includeSenderDetails,
},
},
},
}, },
}); });

View File

@ -64,6 +64,7 @@ export type CreateDocumentFromTemplateOptions = {
signingOrder?: DocumentSigningOrder; signingOrder?: DocumentSigningOrder;
language?: SupportedLanguageCodes; language?: SupportedLanguageCodes;
distributionMethod?: DocumentDistributionMethod; distributionMethod?: DocumentDistributionMethod;
typedSignatureEnabled?: boolean;
}; };
requestMetadata?: RequestMetadata; requestMetadata?: RequestMetadata;
}; };
@ -146,7 +147,7 @@ export const createDocumentFromTemplate = async ({
return { return {
templateRecipientId: templateRecipient.id, templateRecipientId: templateRecipient.id,
fields: templateRecipient.Field, fields: templateRecipient.Field,
name: foundRecipient ? foundRecipient.name ?? '' : templateRecipient.name, name: foundRecipient ? (foundRecipient.name ?? '') : templateRecipient.name,
email: foundRecipient ? foundRecipient.email : templateRecipient.email, email: foundRecipient ? foundRecipient.email : templateRecipient.email,
role: templateRecipient.role, role: templateRecipient.role,
signingOrder: foundRecipient?.signingOrder ?? templateRecipient.signingOrder, signingOrder: foundRecipient?.signingOrder ?? templateRecipient.signingOrder,
@ -196,6 +197,8 @@ export const createDocumentFromTemplate = async ({
override?.language || override?.language ||
template.templateMeta?.language || template.templateMeta?.language ||
template.team?.teamGlobalSettings?.documentLanguage, template.team?.teamGlobalSettings?.documentLanguage,
typedSignatureEnabled:
override?.typedSignatureEnabled ?? template.templateMeta?.typedSignatureEnabled,
}, },
}, },
Recipient: { Recipient: {

View File

@ -448,7 +448,7 @@ msgid "Advanced Options"
msgstr "Erweiterte Optionen" msgstr "Erweiterte Optionen"
#: packages/ui/primitives/document-flow/add-fields.tsx:576 #: packages/ui/primitives/document-flow/add-fields.tsx:576
#: packages/ui/primitives/template-flow/add-template-fields.tsx:409 #: packages/ui/primitives/template-flow/add-template-fields.tsx:414
msgid "Advanced settings" msgid "Advanced settings"
msgstr "Erweiterte Einstellungen" msgstr "Erweiterte Einstellungen"
@ -504,11 +504,11 @@ msgstr "Genehmigung"
msgid "Before you get started, please confirm your email address by clicking the button below:" msgid "Before you get started, please confirm your email address by clicking the button below:"
msgstr "Bitte bestätige vor dem Start deine E-Mail-Adresse, indem du auf den Button unten klickst:" msgstr "Bitte bestätige vor dem Start deine E-Mail-Adresse, indem du auf den Button unten klickst:"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:377 #: packages/ui/primitives/signature-pad/signature-pad.tsx:383
msgid "Black" msgid "Black"
msgstr "Schwarz" msgstr "Schwarz"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:391 #: packages/ui/primitives/signature-pad/signature-pad.tsx:397
msgid "Blue" msgid "Blue"
msgstr "Blau" msgstr "Blau"
@ -566,7 +566,7 @@ msgstr "Checkbox-Werte"
msgid "Clear filters" msgid "Clear filters"
msgstr "Filter löschen" msgstr "Filter löschen"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:411 #: packages/ui/primitives/signature-pad/signature-pad.tsx:417
msgid "Clear Signature" msgid "Clear Signature"
msgstr "Unterschrift löschen" msgstr "Unterschrift löschen"
@ -594,7 +594,7 @@ msgid "Configure Direct Recipient"
msgstr "Direkten Empfänger konfigurieren" msgstr "Direkten Empfänger konfigurieren"
#: packages/ui/primitives/document-flow/add-fields.tsx:577 #: packages/ui/primitives/document-flow/add-fields.tsx:577
#: packages/ui/primitives/template-flow/add-template-fields.tsx:410 #: packages/ui/primitives/template-flow/add-template-fields.tsx:415
msgid "Configure the {0} field" msgid "Configure the {0} field"
msgstr "Konfigurieren Sie das Feld {0}" msgstr "Konfigurieren Sie das Feld {0}"
@ -657,7 +657,7 @@ msgstr "Benutzerdefinierter Text"
#: packages/ui/primitives/document-flow/add-fields.tsx:934 #: packages/ui/primitives/document-flow/add-fields.tsx:934
#: packages/ui/primitives/document-flow/types.ts:53 #: packages/ui/primitives/document-flow/types.ts:53
#: packages/ui/primitives/template-flow/add-template-fields.tsx:697 #: packages/ui/primitives/template-flow/add-template-fields.tsx:729
msgid "Date" msgid "Date"
msgstr "Datum" msgstr "Datum"
@ -801,7 +801,7 @@ msgid "Drag & drop your PDF here."
msgstr "Ziehen Sie Ihr PDF hierher." msgstr "Ziehen Sie Ihr PDF hierher."
#: packages/ui/primitives/document-flow/add-fields.tsx:1065 #: packages/ui/primitives/document-flow/add-fields.tsx:1065
#: packages/ui/primitives/template-flow/add-template-fields.tsx:827 #: packages/ui/primitives/template-flow/add-template-fields.tsx:860
msgid "Dropdown" msgid "Dropdown"
msgstr "Dropdown" msgstr "Dropdown"
@ -815,7 +815,7 @@ msgstr "Dropdown-Optionen"
#: packages/ui/primitives/document-flow/add-signers.tsx:512 #: packages/ui/primitives/document-flow/add-signers.tsx:512
#: packages/ui/primitives/document-flow/add-signers.tsx:519 #: packages/ui/primitives/document-flow/add-signers.tsx:519
#: packages/ui/primitives/document-flow/types.ts:54 #: packages/ui/primitives/document-flow/types.ts:54
#: packages/ui/primitives/template-flow/add-template-fields.tsx:645 #: packages/ui/primitives/template-flow/add-template-fields.tsx:677
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:471 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:471
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:478 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:478
msgid "Email" msgid "Email"
@ -851,6 +851,7 @@ msgid "Enable signing order"
msgstr "Aktiviere die Signaturreihenfolge" msgstr "Aktiviere die Signaturreihenfolge"
#: packages/ui/primitives/document-flow/add-fields.tsx:802 #: packages/ui/primitives/document-flow/add-fields.tsx:802
#: packages/ui/primitives/template-flow/add-template-fields.tsx:597
msgid "Enable Typed Signatures" msgid "Enable Typed Signatures"
msgstr "Aktivieren Sie getippte Unterschriften" msgstr "Aktivieren Sie getippte Unterschriften"
@ -938,7 +939,7 @@ msgstr "Globale Empfängerauthentifizierung"
msgid "Go Back" msgid "Go Back"
msgstr "Zurück" msgstr "Zurück"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:398 #: packages/ui/primitives/signature-pad/signature-pad.tsx:404
msgid "Green" msgid "Green"
msgstr "Grün" msgstr "Grün"
@ -1033,7 +1034,7 @@ msgstr "Min"
#: packages/ui/primitives/document-flow/add-signers.tsx:550 #: packages/ui/primitives/document-flow/add-signers.tsx:550
#: packages/ui/primitives/document-flow/add-signers.tsx:556 #: packages/ui/primitives/document-flow/add-signers.tsx:556
#: packages/ui/primitives/document-flow/types.ts:55 #: packages/ui/primitives/document-flow/types.ts:55
#: packages/ui/primitives/template-flow/add-template-fields.tsx:671 #: packages/ui/primitives/template-flow/add-template-fields.tsx:703
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:506 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:506
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:512 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:512
msgid "Name" msgid "Name"
@ -1052,7 +1053,7 @@ msgid "Needs to view"
msgstr "Muss sehen" msgstr "Muss sehen"
#: packages/ui/primitives/document-flow/add-fields.tsx:693 #: packages/ui/primitives/document-flow/add-fields.tsx:693
#: packages/ui/primitives/template-flow/add-template-fields.tsx:511 #: packages/ui/primitives/template-flow/add-template-fields.tsx:516
msgid "No recipient matching this description was found." msgid "No recipient matching this description was found."
msgstr "Kein passender Empfänger mit dieser Beschreibung gefunden." msgstr "Kein passender Empfänger mit dieser Beschreibung gefunden."
@ -1061,7 +1062,7 @@ msgid "No recipients"
msgstr "Keine Empfänger" msgstr "Keine Empfänger"
#: packages/ui/primitives/document-flow/add-fields.tsx:708 #: packages/ui/primitives/document-flow/add-fields.tsx:708
#: packages/ui/primitives/template-flow/add-template-fields.tsx:526 #: packages/ui/primitives/template-flow/add-template-fields.tsx:531
msgid "No recipients with this role" msgid "No recipients with this role"
msgstr "Keine Empfänger mit dieser Rolle" msgstr "Keine Empfänger mit dieser Rolle"
@ -1091,7 +1092,7 @@ msgstr "Keine"
#: packages/ui/primitives/document-flow/add-fields.tsx:986 #: packages/ui/primitives/document-flow/add-fields.tsx:986
#: packages/ui/primitives/document-flow/types.ts:56 #: packages/ui/primitives/document-flow/types.ts:56
#: packages/ui/primitives/template-flow/add-template-fields.tsx:749 #: packages/ui/primitives/template-flow/add-template-fields.tsx:781
msgid "Number" msgid "Number"
msgstr "Nummer" msgstr "Nummer"
@ -1183,7 +1184,6 @@ msgid "Please try again or contact our support."
msgstr "Bitte versuchen Sie es erneut oder kontaktieren Sie unseren Support." msgstr "Bitte versuchen Sie es erneut oder kontaktieren Sie unseren Support."
#: packages/ui/primitives/document-flow/types.ts:57 #: packages/ui/primitives/document-flow/types.ts:57
#: packages/ui/primitives/template-flow/add-template-fields.tsx:775
msgid "Radio" msgid "Radio"
msgstr "Radio" msgstr "Radio"
@ -1226,7 +1226,7 @@ msgstr "E-Mail des entfernten Empfängers"
msgid "Recipient signing request email" msgid "Recipient signing request email"
msgstr "E-Mail zur Unterzeichnungsanfrage des Empfängers" msgstr "E-Mail zur Unterzeichnungsanfrage des Empfängers"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:384 #: packages/ui/primitives/signature-pad/signature-pad.tsx:390
msgid "Red" msgid "Red"
msgstr "Rot" msgstr "Rot"
@ -1295,7 +1295,7 @@ msgstr "Zeilen pro Seite"
msgid "Save" msgid "Save"
msgstr "Speichern" msgstr "Speichern"
#: packages/ui/primitives/template-flow/add-template-fields.tsx:861 #: packages/ui/primitives/template-flow/add-template-fields.tsx:893
msgid "Save Template" msgid "Save Template"
msgstr "Vorlage speichern" msgstr "Vorlage speichern"
@ -1388,7 +1388,7 @@ msgstr "Anmelden"
#: packages/ui/primitives/document-flow/add-signature.tsx:323 #: packages/ui/primitives/document-flow/add-signature.tsx:323
#: packages/ui/primitives/document-flow/field-icon.tsx:52 #: packages/ui/primitives/document-flow/field-icon.tsx:52
#: packages/ui/primitives/document-flow/types.ts:49 #: packages/ui/primitives/document-flow/types.ts:49
#: packages/ui/primitives/template-flow/add-template-fields.tsx:593 #: packages/ui/primitives/template-flow/add-template-fields.tsx:625
msgid "Signature" msgid "Signature"
msgstr "Unterschrift" msgstr "Unterschrift"
@ -1473,7 +1473,7 @@ msgstr "Vorlagentitel"
#: packages/ui/primitives/document-flow/add-fields.tsx:960 #: packages/ui/primitives/document-flow/add-fields.tsx:960
#: packages/ui/primitives/document-flow/types.ts:52 #: packages/ui/primitives/document-flow/types.ts:52
#: packages/ui/primitives/template-flow/add-template-fields.tsx:723 #: packages/ui/primitives/template-flow/add-template-fields.tsx:755
msgid "Text" msgid "Text"
msgstr "Text" msgstr "Text"
@ -1637,7 +1637,7 @@ msgid "Title"
msgstr "Titel" msgstr "Titel"
#: packages/ui/primitives/document-flow/add-fields.tsx:1080 #: packages/ui/primitives/document-flow/add-fields.tsx:1080
#: packages/ui/primitives/template-flow/add-template-fields.tsx:841 #: packages/ui/primitives/template-flow/add-template-fields.tsx:873
msgid "To proceed further, please set at least one value for the {0} field." msgid "To proceed further, please set at least one value for the {0} field."
msgstr "Um fortzufahren, legen Sie bitte mindestens einen Wert für das Feld {0} fest." msgstr "Um fortzufahren, legen Sie bitte mindestens einen Wert für das Feld {0} fest."

File diff suppressed because it is too large Load Diff

View File

@ -443,7 +443,7 @@ msgid "Advanced Options"
msgstr "Advanced Options" msgstr "Advanced Options"
#: packages/ui/primitives/document-flow/add-fields.tsx:576 #: packages/ui/primitives/document-flow/add-fields.tsx:576
#: packages/ui/primitives/template-flow/add-template-fields.tsx:409 #: packages/ui/primitives/template-flow/add-template-fields.tsx:414
msgid "Advanced settings" msgid "Advanced settings"
msgstr "Advanced settings" msgstr "Advanced settings"
@ -499,11 +499,11 @@ msgstr "Approving"
msgid "Before you get started, please confirm your email address by clicking the button below:" msgid "Before you get started, please confirm your email address by clicking the button below:"
msgstr "Before you get started, please confirm your email address by clicking the button below:" msgstr "Before you get started, please confirm your email address by clicking the button below:"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:377 #: packages/ui/primitives/signature-pad/signature-pad.tsx:383
msgid "Black" msgid "Black"
msgstr "Black" msgstr "Black"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:391 #: packages/ui/primitives/signature-pad/signature-pad.tsx:397
msgid "Blue" msgid "Blue"
msgstr "Blue" msgstr "Blue"
@ -561,7 +561,7 @@ msgstr "Checkbox values"
msgid "Clear filters" msgid "Clear filters"
msgstr "Clear filters" msgstr "Clear filters"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:411 #: packages/ui/primitives/signature-pad/signature-pad.tsx:417
msgid "Clear Signature" msgid "Clear Signature"
msgstr "Clear Signature" msgstr "Clear Signature"
@ -589,7 +589,7 @@ msgid "Configure Direct Recipient"
msgstr "Configure Direct Recipient" msgstr "Configure Direct Recipient"
#: packages/ui/primitives/document-flow/add-fields.tsx:577 #: packages/ui/primitives/document-flow/add-fields.tsx:577
#: packages/ui/primitives/template-flow/add-template-fields.tsx:410 #: packages/ui/primitives/template-flow/add-template-fields.tsx:415
msgid "Configure the {0} field" msgid "Configure the {0} field"
msgstr "Configure the {0} field" msgstr "Configure the {0} field"
@ -652,7 +652,7 @@ msgstr "Custom Text"
#: packages/ui/primitives/document-flow/add-fields.tsx:934 #: packages/ui/primitives/document-flow/add-fields.tsx:934
#: packages/ui/primitives/document-flow/types.ts:53 #: packages/ui/primitives/document-flow/types.ts:53
#: packages/ui/primitives/template-flow/add-template-fields.tsx:697 #: packages/ui/primitives/template-flow/add-template-fields.tsx:729
msgid "Date" msgid "Date"
msgstr "Date" msgstr "Date"
@ -796,7 +796,7 @@ msgid "Drag & drop your PDF here."
msgstr "Drag & drop your PDF here." msgstr "Drag & drop your PDF here."
#: packages/ui/primitives/document-flow/add-fields.tsx:1065 #: packages/ui/primitives/document-flow/add-fields.tsx:1065
#: packages/ui/primitives/template-flow/add-template-fields.tsx:827 #: packages/ui/primitives/template-flow/add-template-fields.tsx:860
msgid "Dropdown" msgid "Dropdown"
msgstr "Dropdown" msgstr "Dropdown"
@ -810,7 +810,7 @@ msgstr "Dropdown options"
#: packages/ui/primitives/document-flow/add-signers.tsx:512 #: packages/ui/primitives/document-flow/add-signers.tsx:512
#: packages/ui/primitives/document-flow/add-signers.tsx:519 #: packages/ui/primitives/document-flow/add-signers.tsx:519
#: packages/ui/primitives/document-flow/types.ts:54 #: packages/ui/primitives/document-flow/types.ts:54
#: packages/ui/primitives/template-flow/add-template-fields.tsx:645 #: packages/ui/primitives/template-flow/add-template-fields.tsx:677
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:471 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:471
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:478 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:478
msgid "Email" msgid "Email"
@ -846,6 +846,7 @@ msgid "Enable signing order"
msgstr "Enable signing order" msgstr "Enable signing order"
#: packages/ui/primitives/document-flow/add-fields.tsx:802 #: packages/ui/primitives/document-flow/add-fields.tsx:802
#: packages/ui/primitives/template-flow/add-template-fields.tsx:597
msgid "Enable Typed Signatures" msgid "Enable Typed Signatures"
msgstr "Enable Typed Signatures" msgstr "Enable Typed Signatures"
@ -933,7 +934,7 @@ msgstr "Global recipient action authentication"
msgid "Go Back" msgid "Go Back"
msgstr "Go Back" msgstr "Go Back"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:398 #: packages/ui/primitives/signature-pad/signature-pad.tsx:404
msgid "Green" msgid "Green"
msgstr "Green" msgstr "Green"
@ -1028,7 +1029,7 @@ msgstr "Min"
#: packages/ui/primitives/document-flow/add-signers.tsx:550 #: packages/ui/primitives/document-flow/add-signers.tsx:550
#: packages/ui/primitives/document-flow/add-signers.tsx:556 #: packages/ui/primitives/document-flow/add-signers.tsx:556
#: packages/ui/primitives/document-flow/types.ts:55 #: packages/ui/primitives/document-flow/types.ts:55
#: packages/ui/primitives/template-flow/add-template-fields.tsx:671 #: packages/ui/primitives/template-flow/add-template-fields.tsx:703
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:506 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:506
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:512 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:512
msgid "Name" msgid "Name"
@ -1047,7 +1048,7 @@ msgid "Needs to view"
msgstr "Needs to view" msgstr "Needs to view"
#: packages/ui/primitives/document-flow/add-fields.tsx:693 #: packages/ui/primitives/document-flow/add-fields.tsx:693
#: packages/ui/primitives/template-flow/add-template-fields.tsx:511 #: packages/ui/primitives/template-flow/add-template-fields.tsx:516
msgid "No recipient matching this description was found." msgid "No recipient matching this description was found."
msgstr "No recipient matching this description was found." msgstr "No recipient matching this description was found."
@ -1056,7 +1057,7 @@ msgid "No recipients"
msgstr "No recipients" msgstr "No recipients"
#: packages/ui/primitives/document-flow/add-fields.tsx:708 #: packages/ui/primitives/document-flow/add-fields.tsx:708
#: packages/ui/primitives/template-flow/add-template-fields.tsx:526 #: packages/ui/primitives/template-flow/add-template-fields.tsx:531
msgid "No recipients with this role" msgid "No recipients with this role"
msgstr "No recipients with this role" msgstr "No recipients with this role"
@ -1086,7 +1087,7 @@ msgstr "None"
#: packages/ui/primitives/document-flow/add-fields.tsx:986 #: packages/ui/primitives/document-flow/add-fields.tsx:986
#: packages/ui/primitives/document-flow/types.ts:56 #: packages/ui/primitives/document-flow/types.ts:56
#: packages/ui/primitives/template-flow/add-template-fields.tsx:749 #: packages/ui/primitives/template-flow/add-template-fields.tsx:781
msgid "Number" msgid "Number"
msgstr "Number" msgstr "Number"
@ -1178,7 +1179,6 @@ msgid "Please try again or contact our support."
msgstr "Please try again or contact our support." msgstr "Please try again or contact our support."
#: packages/ui/primitives/document-flow/types.ts:57 #: packages/ui/primitives/document-flow/types.ts:57
#: packages/ui/primitives/template-flow/add-template-fields.tsx:775
msgid "Radio" msgid "Radio"
msgstr "Radio" msgstr "Radio"
@ -1221,7 +1221,7 @@ msgstr "Recipient removed email"
msgid "Recipient signing request email" msgid "Recipient signing request email"
msgstr "Recipient signing request email" msgstr "Recipient signing request email"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:384 #: packages/ui/primitives/signature-pad/signature-pad.tsx:390
msgid "Red" msgid "Red"
msgstr "Red" msgstr "Red"
@ -1290,7 +1290,7 @@ msgstr "Rows per page"
msgid "Save" msgid "Save"
msgstr "Save" msgstr "Save"
#: packages/ui/primitives/template-flow/add-template-fields.tsx:861 #: packages/ui/primitives/template-flow/add-template-fields.tsx:893
msgid "Save Template" msgid "Save Template"
msgstr "Save Template" msgstr "Save Template"
@ -1383,7 +1383,7 @@ msgstr "Sign In"
#: packages/ui/primitives/document-flow/add-signature.tsx:323 #: packages/ui/primitives/document-flow/add-signature.tsx:323
#: packages/ui/primitives/document-flow/field-icon.tsx:52 #: packages/ui/primitives/document-flow/field-icon.tsx:52
#: packages/ui/primitives/document-flow/types.ts:49 #: packages/ui/primitives/document-flow/types.ts:49
#: packages/ui/primitives/template-flow/add-template-fields.tsx:593 #: packages/ui/primitives/template-flow/add-template-fields.tsx:625
msgid "Signature" msgid "Signature"
msgstr "Signature" msgstr "Signature"
@ -1468,7 +1468,7 @@ msgstr "Template title"
#: packages/ui/primitives/document-flow/add-fields.tsx:960 #: packages/ui/primitives/document-flow/add-fields.tsx:960
#: packages/ui/primitives/document-flow/types.ts:52 #: packages/ui/primitives/document-flow/types.ts:52
#: packages/ui/primitives/template-flow/add-template-fields.tsx:723 #: packages/ui/primitives/template-flow/add-template-fields.tsx:755
msgid "Text" msgid "Text"
msgstr "Text" msgstr "Text"
@ -1632,7 +1632,7 @@ msgid "Title"
msgstr "Title" msgstr "Title"
#: packages/ui/primitives/document-flow/add-fields.tsx:1080 #: packages/ui/primitives/document-flow/add-fields.tsx:1080
#: packages/ui/primitives/template-flow/add-template-fields.tsx:841 #: packages/ui/primitives/template-flow/add-template-fields.tsx:873
msgid "To proceed further, please set at least one value for the {0} field." msgid "To proceed further, please set at least one value for the {0} field."
msgstr "To proceed further, please set at least one value for the {0} field." msgstr "To proceed further, please set at least one value for the {0} field."

File diff suppressed because it is too large Load Diff

View File

@ -448,7 +448,7 @@ msgid "Advanced Options"
msgstr "Opciones avanzadas" msgstr "Opciones avanzadas"
#: packages/ui/primitives/document-flow/add-fields.tsx:576 #: packages/ui/primitives/document-flow/add-fields.tsx:576
#: packages/ui/primitives/template-flow/add-template-fields.tsx:409 #: packages/ui/primitives/template-flow/add-template-fields.tsx:414
msgid "Advanced settings" msgid "Advanced settings"
msgstr "Configuraciones avanzadas" msgstr "Configuraciones avanzadas"
@ -504,11 +504,11 @@ msgstr "Aprobando"
msgid "Before you get started, please confirm your email address by clicking the button below:" msgid "Before you get started, please confirm your email address by clicking the button below:"
msgstr "Antes de comenzar, por favor confirma tu dirección de correo electrónico haciendo clic en el botón de abajo:" msgstr "Antes de comenzar, por favor confirma tu dirección de correo electrónico haciendo clic en el botón de abajo:"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:377 #: packages/ui/primitives/signature-pad/signature-pad.tsx:383
msgid "Black" msgid "Black"
msgstr "Negro" msgstr "Negro"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:391 #: packages/ui/primitives/signature-pad/signature-pad.tsx:397
msgid "Blue" msgid "Blue"
msgstr "Azul" msgstr "Azul"
@ -566,7 +566,7 @@ msgstr "Valores de Checkbox"
msgid "Clear filters" msgid "Clear filters"
msgstr "Limpiar filtros" msgstr "Limpiar filtros"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:411 #: packages/ui/primitives/signature-pad/signature-pad.tsx:417
msgid "Clear Signature" msgid "Clear Signature"
msgstr "Limpiar firma" msgstr "Limpiar firma"
@ -594,7 +594,7 @@ msgid "Configure Direct Recipient"
msgstr "Configurar destinatario directo" msgstr "Configurar destinatario directo"
#: packages/ui/primitives/document-flow/add-fields.tsx:577 #: packages/ui/primitives/document-flow/add-fields.tsx:577
#: packages/ui/primitives/template-flow/add-template-fields.tsx:410 #: packages/ui/primitives/template-flow/add-template-fields.tsx:415
msgid "Configure the {0} field" msgid "Configure the {0} field"
msgstr "Configurar el campo {0}" msgstr "Configurar el campo {0}"
@ -657,7 +657,7 @@ msgstr "Texto personalizado"
#: packages/ui/primitives/document-flow/add-fields.tsx:934 #: packages/ui/primitives/document-flow/add-fields.tsx:934
#: packages/ui/primitives/document-flow/types.ts:53 #: packages/ui/primitives/document-flow/types.ts:53
#: packages/ui/primitives/template-flow/add-template-fields.tsx:697 #: packages/ui/primitives/template-flow/add-template-fields.tsx:729
msgid "Date" msgid "Date"
msgstr "Fecha" msgstr "Fecha"
@ -801,7 +801,7 @@ msgid "Drag & drop your PDF here."
msgstr "Arrastre y suelte su PDF aquí." msgstr "Arrastre y suelte su PDF aquí."
#: packages/ui/primitives/document-flow/add-fields.tsx:1065 #: packages/ui/primitives/document-flow/add-fields.tsx:1065
#: packages/ui/primitives/template-flow/add-template-fields.tsx:827 #: packages/ui/primitives/template-flow/add-template-fields.tsx:860
msgid "Dropdown" msgid "Dropdown"
msgstr "Menú desplegable" msgstr "Menú desplegable"
@ -815,7 +815,7 @@ msgstr "Opciones de menú desplegable"
#: packages/ui/primitives/document-flow/add-signers.tsx:512 #: packages/ui/primitives/document-flow/add-signers.tsx:512
#: packages/ui/primitives/document-flow/add-signers.tsx:519 #: packages/ui/primitives/document-flow/add-signers.tsx:519
#: packages/ui/primitives/document-flow/types.ts:54 #: packages/ui/primitives/document-flow/types.ts:54
#: packages/ui/primitives/template-flow/add-template-fields.tsx:645 #: packages/ui/primitives/template-flow/add-template-fields.tsx:677
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:471 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:471
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:478 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:478
msgid "Email" msgid "Email"
@ -851,6 +851,7 @@ msgid "Enable signing order"
msgstr "Habilitar orden de firma" msgstr "Habilitar orden de firma"
#: packages/ui/primitives/document-flow/add-fields.tsx:802 #: packages/ui/primitives/document-flow/add-fields.tsx:802
#: packages/ui/primitives/template-flow/add-template-fields.tsx:597
msgid "Enable Typed Signatures" msgid "Enable Typed Signatures"
msgstr "Habilitar firmas escritas" msgstr "Habilitar firmas escritas"
@ -938,7 +939,7 @@ msgstr "Autenticación de acción de destinatario global"
msgid "Go Back" msgid "Go Back"
msgstr "Regresar" msgstr "Regresar"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:398 #: packages/ui/primitives/signature-pad/signature-pad.tsx:404
msgid "Green" msgid "Green"
msgstr "Verde" msgstr "Verde"
@ -1033,7 +1034,7 @@ msgstr "Mín"
#: packages/ui/primitives/document-flow/add-signers.tsx:550 #: packages/ui/primitives/document-flow/add-signers.tsx:550
#: packages/ui/primitives/document-flow/add-signers.tsx:556 #: packages/ui/primitives/document-flow/add-signers.tsx:556
#: packages/ui/primitives/document-flow/types.ts:55 #: packages/ui/primitives/document-flow/types.ts:55
#: packages/ui/primitives/template-flow/add-template-fields.tsx:671 #: packages/ui/primitives/template-flow/add-template-fields.tsx:703
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:506 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:506
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:512 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:512
msgid "Name" msgid "Name"
@ -1052,7 +1053,7 @@ msgid "Needs to view"
msgstr "Necesita ver" msgstr "Necesita ver"
#: packages/ui/primitives/document-flow/add-fields.tsx:693 #: packages/ui/primitives/document-flow/add-fields.tsx:693
#: packages/ui/primitives/template-flow/add-template-fields.tsx:511 #: packages/ui/primitives/template-flow/add-template-fields.tsx:516
msgid "No recipient matching this description was found." msgid "No recipient matching this description was found."
msgstr "No se encontró ningún destinatario que coincidiera con esta descripción." msgstr "No se encontró ningún destinatario que coincidiera con esta descripción."
@ -1061,7 +1062,7 @@ msgid "No recipients"
msgstr "Sin destinatarios" msgstr "Sin destinatarios"
#: packages/ui/primitives/document-flow/add-fields.tsx:708 #: packages/ui/primitives/document-flow/add-fields.tsx:708
#: packages/ui/primitives/template-flow/add-template-fields.tsx:526 #: packages/ui/primitives/template-flow/add-template-fields.tsx:531
msgid "No recipients with this role" msgid "No recipients with this role"
msgstr "No hay destinatarios con este rol" msgstr "No hay destinatarios con este rol"
@ -1091,7 +1092,7 @@ msgstr "Ninguno"
#: packages/ui/primitives/document-flow/add-fields.tsx:986 #: packages/ui/primitives/document-flow/add-fields.tsx:986
#: packages/ui/primitives/document-flow/types.ts:56 #: packages/ui/primitives/document-flow/types.ts:56
#: packages/ui/primitives/template-flow/add-template-fields.tsx:749 #: packages/ui/primitives/template-flow/add-template-fields.tsx:781
msgid "Number" msgid "Number"
msgstr "Número" msgstr "Número"
@ -1183,7 +1184,6 @@ msgid "Please try again or contact our support."
msgstr "Por favor, inténtalo de nuevo o contacta a nuestro soporte." msgstr "Por favor, inténtalo de nuevo o contacta a nuestro soporte."
#: packages/ui/primitives/document-flow/types.ts:57 #: packages/ui/primitives/document-flow/types.ts:57
#: packages/ui/primitives/template-flow/add-template-fields.tsx:775
msgid "Radio" msgid "Radio"
msgstr "Radio" msgstr "Radio"
@ -1226,7 +1226,7 @@ msgstr "Correo electrónico de destinatario eliminado"
msgid "Recipient signing request email" msgid "Recipient signing request email"
msgstr "Correo electrónico de solicitud de firma de destinatario" msgstr "Correo electrónico de solicitud de firma de destinatario"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:384 #: packages/ui/primitives/signature-pad/signature-pad.tsx:390
msgid "Red" msgid "Red"
msgstr "Rojo" msgstr "Rojo"
@ -1295,7 +1295,7 @@ msgstr "Filas por página"
msgid "Save" msgid "Save"
msgstr "Guardar" msgstr "Guardar"
#: packages/ui/primitives/template-flow/add-template-fields.tsx:861 #: packages/ui/primitives/template-flow/add-template-fields.tsx:893
msgid "Save Template" msgid "Save Template"
msgstr "Guardar plantilla" msgstr "Guardar plantilla"
@ -1388,7 +1388,7 @@ msgstr "Iniciar sesión"
#: packages/ui/primitives/document-flow/add-signature.tsx:323 #: packages/ui/primitives/document-flow/add-signature.tsx:323
#: packages/ui/primitives/document-flow/field-icon.tsx:52 #: packages/ui/primitives/document-flow/field-icon.tsx:52
#: packages/ui/primitives/document-flow/types.ts:49 #: packages/ui/primitives/document-flow/types.ts:49
#: packages/ui/primitives/template-flow/add-template-fields.tsx:593 #: packages/ui/primitives/template-flow/add-template-fields.tsx:625
msgid "Signature" msgid "Signature"
msgstr "Firma" msgstr "Firma"
@ -1473,7 +1473,7 @@ msgstr "Título de plantilla"
#: packages/ui/primitives/document-flow/add-fields.tsx:960 #: packages/ui/primitives/document-flow/add-fields.tsx:960
#: packages/ui/primitives/document-flow/types.ts:52 #: packages/ui/primitives/document-flow/types.ts:52
#: packages/ui/primitives/template-flow/add-template-fields.tsx:723 #: packages/ui/primitives/template-flow/add-template-fields.tsx:755
msgid "Text" msgid "Text"
msgstr "Texto" msgstr "Texto"
@ -1637,7 +1637,7 @@ msgid "Title"
msgstr "Título" msgstr "Título"
#: packages/ui/primitives/document-flow/add-fields.tsx:1080 #: packages/ui/primitives/document-flow/add-fields.tsx:1080
#: packages/ui/primitives/template-flow/add-template-fields.tsx:841 #: packages/ui/primitives/template-flow/add-template-fields.tsx:873
msgid "To proceed further, please set at least one value for the {0} field." msgid "To proceed further, please set at least one value for the {0} field."
msgstr "Para continuar, por favor establezca al menos un valor para el campo {0}." msgstr "Para continuar, por favor establezca al menos un valor para el campo {0}."

File diff suppressed because it is too large Load Diff

View File

@ -448,7 +448,7 @@ msgid "Advanced Options"
msgstr "Options avancées" msgstr "Options avancées"
#: packages/ui/primitives/document-flow/add-fields.tsx:576 #: packages/ui/primitives/document-flow/add-fields.tsx:576
#: packages/ui/primitives/template-flow/add-template-fields.tsx:409 #: packages/ui/primitives/template-flow/add-template-fields.tsx:414
msgid "Advanced settings" msgid "Advanced settings"
msgstr "Paramètres avancés" msgstr "Paramètres avancés"
@ -504,11 +504,11 @@ msgstr "En attente d'approbation"
msgid "Before you get started, please confirm your email address by clicking the button below:" msgid "Before you get started, please confirm your email address by clicking the button below:"
msgstr "Avant de commencer, veuillez confirmer votre adresse email en cliquant sur le bouton ci-dessous :" msgstr "Avant de commencer, veuillez confirmer votre adresse email en cliquant sur le bouton ci-dessous :"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:377 #: packages/ui/primitives/signature-pad/signature-pad.tsx:383
msgid "Black" msgid "Black"
msgstr "Noir" msgstr "Noir"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:391 #: packages/ui/primitives/signature-pad/signature-pad.tsx:397
msgid "Blue" msgid "Blue"
msgstr "Bleu" msgstr "Bleu"
@ -566,7 +566,7 @@ msgstr "Valeurs de case à cocher"
msgid "Clear filters" msgid "Clear filters"
msgstr "Effacer les filtres" msgstr "Effacer les filtres"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:411 #: packages/ui/primitives/signature-pad/signature-pad.tsx:417
msgid "Clear Signature" msgid "Clear Signature"
msgstr "Effacer la signature" msgstr "Effacer la signature"
@ -594,7 +594,7 @@ msgid "Configure Direct Recipient"
msgstr "Configurer le destinataire direct" msgstr "Configurer le destinataire direct"
#: packages/ui/primitives/document-flow/add-fields.tsx:577 #: packages/ui/primitives/document-flow/add-fields.tsx:577
#: packages/ui/primitives/template-flow/add-template-fields.tsx:410 #: packages/ui/primitives/template-flow/add-template-fields.tsx:415
msgid "Configure the {0} field" msgid "Configure the {0} field"
msgstr "Configurer le champ {0}" msgstr "Configurer le champ {0}"
@ -657,7 +657,7 @@ msgstr "Texte personnalisé"
#: packages/ui/primitives/document-flow/add-fields.tsx:934 #: packages/ui/primitives/document-flow/add-fields.tsx:934
#: packages/ui/primitives/document-flow/types.ts:53 #: packages/ui/primitives/document-flow/types.ts:53
#: packages/ui/primitives/template-flow/add-template-fields.tsx:697 #: packages/ui/primitives/template-flow/add-template-fields.tsx:729
msgid "Date" msgid "Date"
msgstr "Date" msgstr "Date"
@ -801,7 +801,7 @@ msgid "Drag & drop your PDF here."
msgstr "Faites glisser et déposez votre PDF ici." msgstr "Faites glisser et déposez votre PDF ici."
#: packages/ui/primitives/document-flow/add-fields.tsx:1065 #: packages/ui/primitives/document-flow/add-fields.tsx:1065
#: packages/ui/primitives/template-flow/add-template-fields.tsx:827 #: packages/ui/primitives/template-flow/add-template-fields.tsx:860
msgid "Dropdown" msgid "Dropdown"
msgstr "Liste déroulante" msgstr "Liste déroulante"
@ -815,7 +815,7 @@ msgstr "Options de liste déroulante"
#: packages/ui/primitives/document-flow/add-signers.tsx:512 #: packages/ui/primitives/document-flow/add-signers.tsx:512
#: packages/ui/primitives/document-flow/add-signers.tsx:519 #: packages/ui/primitives/document-flow/add-signers.tsx:519
#: packages/ui/primitives/document-flow/types.ts:54 #: packages/ui/primitives/document-flow/types.ts:54
#: packages/ui/primitives/template-flow/add-template-fields.tsx:645 #: packages/ui/primitives/template-flow/add-template-fields.tsx:677
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:471 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:471
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:478 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:478
msgid "Email" msgid "Email"
@ -851,6 +851,7 @@ msgid "Enable signing order"
msgstr "Activer l'ordre de signature" msgstr "Activer l'ordre de signature"
#: packages/ui/primitives/document-flow/add-fields.tsx:802 #: packages/ui/primitives/document-flow/add-fields.tsx:802
#: packages/ui/primitives/template-flow/add-template-fields.tsx:597
msgid "Enable Typed Signatures" msgid "Enable Typed Signatures"
msgstr "Activer les signatures tapées" msgstr "Activer les signatures tapées"
@ -938,7 +939,7 @@ msgstr "Authentification d'action de destinataire globale"
msgid "Go Back" msgid "Go Back"
msgstr "Retourner" msgstr "Retourner"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:398 #: packages/ui/primitives/signature-pad/signature-pad.tsx:404
msgid "Green" msgid "Green"
msgstr "Vert" msgstr "Vert"
@ -1033,7 +1034,7 @@ msgstr "Min"
#: packages/ui/primitives/document-flow/add-signers.tsx:550 #: packages/ui/primitives/document-flow/add-signers.tsx:550
#: packages/ui/primitives/document-flow/add-signers.tsx:556 #: packages/ui/primitives/document-flow/add-signers.tsx:556
#: packages/ui/primitives/document-flow/types.ts:55 #: packages/ui/primitives/document-flow/types.ts:55
#: packages/ui/primitives/template-flow/add-template-fields.tsx:671 #: packages/ui/primitives/template-flow/add-template-fields.tsx:703
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:506 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:506
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:512 #: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx:512
msgid "Name" msgid "Name"
@ -1052,7 +1053,7 @@ msgid "Needs to view"
msgstr "Nécessite une visualisation" msgstr "Nécessite une visualisation"
#: packages/ui/primitives/document-flow/add-fields.tsx:693 #: packages/ui/primitives/document-flow/add-fields.tsx:693
#: packages/ui/primitives/template-flow/add-template-fields.tsx:511 #: packages/ui/primitives/template-flow/add-template-fields.tsx:516
msgid "No recipient matching this description was found." msgid "No recipient matching this description was found."
msgstr "Aucun destinataire correspondant à cette description n'a été trouvé." msgstr "Aucun destinataire correspondant à cette description n'a été trouvé."
@ -1061,7 +1062,7 @@ msgid "No recipients"
msgstr "Aucun destinataire" msgstr "Aucun destinataire"
#: packages/ui/primitives/document-flow/add-fields.tsx:708 #: packages/ui/primitives/document-flow/add-fields.tsx:708
#: packages/ui/primitives/template-flow/add-template-fields.tsx:526 #: packages/ui/primitives/template-flow/add-template-fields.tsx:531
msgid "No recipients with this role" msgid "No recipients with this role"
msgstr "Aucun destinataire avec ce rôle" msgstr "Aucun destinataire avec ce rôle"
@ -1091,7 +1092,7 @@ msgstr "Aucun"
#: packages/ui/primitives/document-flow/add-fields.tsx:986 #: packages/ui/primitives/document-flow/add-fields.tsx:986
#: packages/ui/primitives/document-flow/types.ts:56 #: packages/ui/primitives/document-flow/types.ts:56
#: packages/ui/primitives/template-flow/add-template-fields.tsx:749 #: packages/ui/primitives/template-flow/add-template-fields.tsx:781
msgid "Number" msgid "Number"
msgstr "Numéro" msgstr "Numéro"
@ -1183,7 +1184,6 @@ msgid "Please try again or contact our support."
msgstr "Veuillez réessayer ou contacter notre support." msgstr "Veuillez réessayer ou contacter notre support."
#: packages/ui/primitives/document-flow/types.ts:57 #: packages/ui/primitives/document-flow/types.ts:57
#: packages/ui/primitives/template-flow/add-template-fields.tsx:775
msgid "Radio" msgid "Radio"
msgstr "Radio" msgstr "Radio"
@ -1226,7 +1226,7 @@ msgstr "E-mail de destinataire supprimé"
msgid "Recipient signing request email" msgid "Recipient signing request email"
msgstr "E-mail de demande de signature de destinataire" msgstr "E-mail de demande de signature de destinataire"
#: packages/ui/primitives/signature-pad/signature-pad.tsx:384 #: packages/ui/primitives/signature-pad/signature-pad.tsx:390
msgid "Red" msgid "Red"
msgstr "Rouge" msgstr "Rouge"
@ -1295,7 +1295,7 @@ msgstr "Lignes par page"
msgid "Save" msgid "Save"
msgstr "Sauvegarder" msgstr "Sauvegarder"
#: packages/ui/primitives/template-flow/add-template-fields.tsx:861 #: packages/ui/primitives/template-flow/add-template-fields.tsx:893
msgid "Save Template" msgid "Save Template"
msgstr "Sauvegarder le modèle" msgstr "Sauvegarder le modèle"
@ -1388,7 +1388,7 @@ msgstr "Se connecter"
#: packages/ui/primitives/document-flow/add-signature.tsx:323 #: packages/ui/primitives/document-flow/add-signature.tsx:323
#: packages/ui/primitives/document-flow/field-icon.tsx:52 #: packages/ui/primitives/document-flow/field-icon.tsx:52
#: packages/ui/primitives/document-flow/types.ts:49 #: packages/ui/primitives/document-flow/types.ts:49
#: packages/ui/primitives/template-flow/add-template-fields.tsx:593 #: packages/ui/primitives/template-flow/add-template-fields.tsx:625
msgid "Signature" msgid "Signature"
msgstr "Signature" msgstr "Signature"
@ -1473,7 +1473,7 @@ msgstr "Titre du modèle"
#: packages/ui/primitives/document-flow/add-fields.tsx:960 #: packages/ui/primitives/document-flow/add-fields.tsx:960
#: packages/ui/primitives/document-flow/types.ts:52 #: packages/ui/primitives/document-flow/types.ts:52
#: packages/ui/primitives/template-flow/add-template-fields.tsx:723 #: packages/ui/primitives/template-flow/add-template-fields.tsx:755
msgid "Text" msgid "Text"
msgstr "Texte" msgstr "Texte"
@ -1637,7 +1637,7 @@ msgid "Title"
msgstr "Titre" msgstr "Titre"
#: packages/ui/primitives/document-flow/add-fields.tsx:1080 #: packages/ui/primitives/document-flow/add-fields.tsx:1080
#: packages/ui/primitives/template-flow/add-template-fields.tsx:841 #: packages/ui/primitives/template-flow/add-template-fields.tsx:873
msgid "To proceed further, please set at least one value for the {0} field." msgid "To proceed further, please set at least one value for the {0} field."
msgstr "Pour continuer, veuillez définir au moins une valeur pour le champ {0}." msgstr "Pour continuer, veuillez définir au moins une valeur pour le champ {0}."

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,8 @@ module.exports = {
plugins: [ plugins: [
'@trivago/prettier-plugin-sort-imports', '@trivago/prettier-plugin-sort-imports',
'prettier-plugin-sql', // !: Disabled until Prettier 3.x is supported.
// 'prettier-plugin-sql',
'prettier-plugin-tailwindcss', 'prettier-plugin-tailwindcss',
], ],

View File

@ -7,10 +7,9 @@
"clean": "rimraf node_modules" "clean": "rimraf node_modules"
}, },
"dependencies": { "dependencies": {
"@trivago/prettier-plugin-sort-imports": "^4.1.1", "@trivago/prettier-plugin-sort-imports": "^4.3.0",
"prettier": "^2.8.8", "prettier": "^3.3.3",
"prettier-plugin-sql": "^0.14.0", "prettier-plugin-tailwindcss": "^0.6.9"
"prettier-plugin-tailwindcss": "^0.2.8"
}, },
"devDependencies": {} "devDependencies": {}
} }

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "DocumentMeta" ALTER COLUMN "typedSignatureEnabled" SET DEFAULT true;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "TeamGlobalSettings" ADD COLUMN "typedSignatureEnabled" BOOLEAN NOT NULL DEFAULT true;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "TeamGlobalSettings" ADD COLUMN "includeSigningCertificate" BOOLEAN NOT NULL DEFAULT true;

View File

@ -0,0 +1,7 @@
-- Existing templates should not have this enabled by default.
-- AlterTable
ALTER TABLE "TemplateMeta" ADD COLUMN "typedSignatureEnabled" BOOLEAN NOT NULL DEFAULT false;
-- New templates should have this enabled by default.
-- AlterTable
ALTER TABLE "TemplateMeta" ALTER COLUMN "typedSignatureEnabled" SET DEFAULT true;

View File

@ -374,7 +374,7 @@ model DocumentMeta {
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
redirectUrl String? redirectUrl String?
signingOrder DocumentSigningOrder @default(PARALLEL) signingOrder DocumentSigningOrder @default(PARALLEL)
typedSignatureEnabled Boolean @default(false) typedSignatureEnabled Boolean @default(true)
language String @default("en") language String @default("en")
distributionMethod DocumentDistributionMethod @default(EMAIL) distributionMethod DocumentDistributionMethod @default(EMAIL)
emailSettings Json? emailSettings Json?
@ -511,10 +511,12 @@ enum TeamMemberInviteStatus {
} }
model TeamGlobalSettings { model TeamGlobalSettings {
teamId Int @unique teamId Int @unique
documentVisibility DocumentVisibility @default(EVERYONE) documentVisibility DocumentVisibility @default(EVERYONE)
documentLanguage String @default("en") documentLanguage String @default("en")
includeSenderDetails Boolean @default(true) includeSenderDetails Boolean @default(true)
typedSignatureEnabled Boolean @default(true)
includeSigningCertificate Boolean @default(true)
brandingEnabled Boolean @default(false) brandingEnabled Boolean @default(false)
brandingLogo String @default("") brandingLogo String @default("")
@ -628,19 +630,21 @@ enum TemplateType {
} }
model TemplateMeta { model TemplateMeta {
id String @id @default(cuid()) id String @id @default(cuid())
subject String? subject String?
message String? message String?
timezone String? @default("Etc/UTC") @db.Text timezone String? @default("Etc/UTC") @db.Text
password String? password String?
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
signingOrder DocumentSigningOrder? @default(PARALLEL) signingOrder DocumentSigningOrder? @default(PARALLEL)
templateId Int @unique typedSignatureEnabled Boolean @default(true)
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade) distributionMethod DocumentDistributionMethod @default(EMAIL)
redirectUrl String?
language String @default("en") templateId Int @unique
distributionMethod DocumentDistributionMethod @default(EMAIL) template Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
emailSettings Json? redirectUrl String?
language String @default("en")
emailSettings Json?
} }
model Template { model Template {

View File

@ -3,7 +3,7 @@ const { fontFamily } = require('tailwindcss/defaultTheme');
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
darkMode: ['class'], darkMode: ['variant', '&:is(.dark:not(.dark-mode-disabled) *)'],
content: ['src/**/*.{ts,tsx}'], content: ['src/**/*.{ts,tsx}'],
theme: { theme: {
extend: { extend: {
@ -108,6 +108,9 @@ module.exports = {
'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 'gradient-conic': 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
}, },
borderRadius: { borderRadius: {
DEFAULT: 'calc(var(--radius) - 3px)',
'2xl': 'calc(var(--radius) + 4px)',
xl: 'calc(var(--radius) + 2px)',
lg: 'var(--radius)', lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)', md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)', sm: 'calc(var(--radius) - 4px)',

View File

@ -11,7 +11,7 @@
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"postcss": "^8.4.32", "postcss": "^8.4.32",
"tailwindcss": "3.3.2", "tailwindcss": "3.4.15",
"tailwindcss-animate": "^1.0.5" "tailwindcss-animate": "^1.0.5"
}, },
"devDependencies": {}, "devDependencies": {},

View File

@ -151,8 +151,6 @@ export const ZUpdateTeamMutationSchema = z.object({
data: z.object({ data: z.object({
name: ZTeamNameSchema, name: ZTeamNameSchema,
url: ZTeamUrlSchema, url: ZTeamUrlSchema,
documentVisibility: z.nativeEnum(DocumentVisibility).optional(),
includeSenderDetails: z.boolean().optional(),
}), }),
}); });
@ -212,6 +210,8 @@ export const ZUpdateTeamDocumentSettingsMutationSchema = z.object({
.default(DocumentVisibility.EVERYONE), .default(DocumentVisibility.EVERYONE),
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).optional().default('en'), documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).optional().default('en'),
includeSenderDetails: z.boolean().optional().default(false), includeSenderDetails: z.boolean().optional().default(false),
typedSignatureEnabled: z.boolean().optional().default(true),
includeSigningCertificate: z.boolean().optional().default(true),
}), }),
}); });

View File

@ -35,6 +35,7 @@ import {
ZSetSigningOrderForTemplateMutationSchema, ZSetSigningOrderForTemplateMutationSchema,
ZToggleTemplateDirectLinkMutationSchema, ZToggleTemplateDirectLinkMutationSchema,
ZUpdateTemplateSettingsMutationSchema, ZUpdateTemplateSettingsMutationSchema,
ZUpdateTemplateTypedSignatureSettingsMutationSchema,
} from './schema'; } from './schema';
export const templateRouter = router({ export const templateRouter = router({
@ -359,4 +360,48 @@ export const templateRouter = router({
}); });
} }
}), }),
updateTemplateTypedSignatureSettings: authenticatedProcedure
.input(ZUpdateTemplateTypedSignatureSettingsMutationSchema)
.mutation(async ({ input, ctx }) => {
try {
const { templateId, teamId, typedSignatureEnabled } = input;
const template = await getTemplateById({
id: templateId,
teamId,
userId: ctx.user.id,
}).catch(() => null);
if (!template) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Template not found',
});
}
return await updateTemplateSettings({
templateId,
teamId,
userId: ctx.user.id,
data: {},
meta: {
typedSignatureEnabled,
},
requestMetadata: extractNextApiRequestMetadata(ctx.req),
});
} catch (err) {
console.error(err);
if (err instanceof TRPCError) {
throw err;
}
throw new TRPCError({
code: 'BAD_REQUEST',
message:
'We were unable to update the settings for this template. Please try again later.',
});
}
}),
}); });

View File

@ -114,6 +114,7 @@ export const ZUpdateTemplateSettingsMutationSchema = z.object({
'Please enter a valid URL, make sure you include http:// or https:// part of the url.', 'Please enter a valid URL, make sure you include http:// or https:// part of the url.',
}), }),
language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(), language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
typedSignatureEnabled: z.boolean().optional(),
}) })
.optional(), .optional(),
}); });
@ -138,6 +139,12 @@ export const ZMoveTemplatesToTeamSchema = z.object({
teamId: z.number(), teamId: z.number(),
}); });
export const ZUpdateTemplateTypedSignatureSettingsMutationSchema = z.object({
templateId: z.number(),
teamId: z.number().optional(),
typedSignatureEnabled: z.boolean(),
});
export type TCreateTemplateMutationSchema = z.infer<typeof ZCreateTemplateMutationSchema>; export type TCreateTemplateMutationSchema = z.infer<typeof ZCreateTemplateMutationSchema>;
export type TCreateDocumentFromTemplateMutationSchema = z.infer< export type TCreateDocumentFromTemplateMutationSchema = z.infer<
typeof ZCreateDocumentFromTemplateMutationSchema typeof ZCreateDocumentFromTemplateMutationSchema

View File

@ -34,7 +34,7 @@ const getCardClassNames = (
const baseClasses = 'field-card-container relative z-20 h-full w-full transition-all'; const baseClasses = 'field-card-container relative z-20 h-full w-full transition-all';
const insertedClasses = const insertedClasses =
'bg-documenso/20 border-documenso ring-documenso-200 ring-offset-documenso-200 ring-2 ring-offset-2 dark:shadow-none'; 'bg-primary/20 border-primary ring-primary/20 ring-offset-primary/20 ring-2 ring-offset-2 dark:shadow-none';
const nonRequiredClasses = const nonRequiredClasses =
'border-yellow-300 shadow-none ring-2 ring-yellow-100 ring-offset-2 ring-offset-yellow-100 dark:border-2'; 'border-yellow-300 shadow-none ring-2 ring-yellow-100 ring-offset-2 ring-offset-yellow-100 dark:border-2';
const validatingClasses = 'border-orange-300 ring-1 ring-orange-300'; const validatingClasses = 'border-orange-300 ring-1 ring-orange-300';

View File

@ -74,7 +74,7 @@
"react-hook-form": "^7.45.4", "react-hook-form": "^7.45.4",
"react-pdf": "7.7.3", "react-pdf": "7.7.3",
"react-rnd": "^10.4.1", "react-rnd": "^10.4.1",
"remeda": "^1.27.1", "remeda": "^2.17.3",
"tailwind-merge": "^1.12.0", "tailwind-merge": "^1.12.0",
"tailwindcss-animate": "^1.0.5", "tailwindcss-animate": "^1.0.5",
"ts-pattern": "^5.0.5", "ts-pattern": "^5.0.5",

View File

@ -36,11 +36,11 @@ const Card = React.forwardRef<HTMLDivElement, CardProps>(
className={cn( className={cn(
'bg-background text-foreground group relative rounded-lg border-2 backdrop-blur-[2px]', 'bg-background text-foreground group relative rounded-lg border-2 backdrop-blur-[2px]',
{ {
'gradient-border-mask before:pointer-events-none before:absolute before:-inset-[2px] before:rounded-lg before:p-[2px] before:[background:linear-gradient(var(--card-gradient-degrees),theme(colors.documenso.DEFAULT/50%)_5%,theme(colors.border/80%)_30%)]': 'gradient-border-mask before:pointer-events-none before:absolute before:-inset-[2px] before:rounded-lg before:p-[2px] before:[background:linear-gradient(var(--card-gradient-degrees),theme(colors.primary.DEFAULT/50%)_5%,theme(colors.border/80%)_30%)]':
gradient, gradient,
'dark:gradient-border-mask before:pointer-events-none before:absolute before:-inset-[2px] before:rounded-lg before:p-[2px] before:[background:linear-gradient(var(--card-gradient-degrees),theme(colors.documenso.DEFAULT/70%)_5%,theme(colors.border/80%)_30%)]': 'dark:gradient-border-mask before:pointer-events-none before:absolute before:-inset-[2px] before:rounded-lg before:p-[2px] before:[background:linear-gradient(var(--card-gradient-degrees),theme(colors.primary.DEFAULT/70%)_5%,theme(colors.border/80%)_30%)]':
gradient, gradient,
'shadow-[0_0_0_4px_theme(colors.gray.100/70%),0_0_0_1px_theme(colors.gray.100/70%),0_0_0_0.5px_theme(colors.primary.DEFAULT/70%)]': 'shadow-[0_0_0_4px_theme(colors.gray.100/70%),0_0_0_1px_theme(colors.gray.100/70%),0_0_0_0.5px_var(colors.primary.DEFAULT/70%)]':
true, true,
'dark:shadow-[0]': true, 'dark:shadow-[0]': true,
}, },

View File

@ -141,7 +141,7 @@ export const RadioFieldAdvancedSettings = ({
{values.map((value) => ( {values.map((value) => (
<div key={value.id} className="mt-2 flex items-center gap-4"> <div key={value.id} className="mt-2 flex items-center gap-4">
<Checkbox <Checkbox
className="data-[state=checked]:bg-documenso border-foreground/30 data-[state=checked]:ring-documenso dark:data-[state=checked]:ring-offset-background h-5 w-5 rounded-full data-[state=checked]:ring-1 data-[state=checked]:ring-offset-2 data-[state=checked]:ring-offset-white" className="data-[state=checked]:bg-documenso border-foreground/30 data-[state=checked]:ring-primary dark:data-[state=checked]:ring-offset-background h-5 w-5 rounded-full data-[state=checked]:ring-1 data-[state=checked]:ring-offset-2 data-[state=checked]:ring-offset-white"
checked={value.checked} checked={value.checked}
onCheckedChange={(checked) => handleCheckedChange(Boolean(checked), value.id)} onCheckedChange={(checked) => handleCheckedChange(Boolean(checked), value.id)}
/> />

View File

@ -38,6 +38,7 @@ export type SignaturePadProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'onChang
containerClassName?: string; containerClassName?: string;
disabled?: boolean; disabled?: boolean;
allowTypedSignature?: boolean; allowTypedSignature?: boolean;
defaultValue?: string;
}; };
export const SignaturePad = ({ export const SignaturePad = ({
@ -56,7 +57,7 @@ export const SignaturePad = ({
const [lines, setLines] = useState<Point[][]>([]); const [lines, setLines] = useState<Point[][]>([]);
const [currentLine, setCurrentLine] = useState<Point[]>([]); const [currentLine, setCurrentLine] = useState<Point[]>([]);
const [selectedColor, setSelectedColor] = useState('black'); const [selectedColor, setSelectedColor] = useState('black');
const [typedSignature, setTypedSignature] = useState(''); const [typedSignature, setTypedSignature] = useState(defaultValue ?? '');
const perfectFreehandOptions = useMemo(() => { const perfectFreehandOptions = useMemo(() => {
const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10; const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10;
@ -206,6 +207,7 @@ export const SignaturePad = ({
if (ctx) { if (ctx) {
const canvasWidth = $el.current.width; const canvasWidth = $el.current.width;
const canvasHeight = $el.current.height; const canvasHeight = $el.current.height;
const fontFamily = String(fontCaveat.style.fontFamily);
ctx.clearRect(0, 0, canvasWidth, canvasHeight); ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.textAlign = 'center'; ctx.textAlign = 'center';
@ -217,7 +219,7 @@ export const SignaturePad = ({
// Start with a base font size // Start with a base font size
let fontSize = 18; let fontSize = 18;
ctx.font = `${fontSize}px ${fontCaveat.style.fontFamily}`; ctx.font = `${fontSize}px ${fontFamily}`;
// Measure 10 characters and calculate scale factor // Measure 10 characters and calculate scale factor
const characterWidth = ctx.measureText('m'.repeat(10)).width; const characterWidth = ctx.measureText('m'.repeat(10)).width;
@ -227,7 +229,7 @@ export const SignaturePad = ({
fontSize = fontSize * scaleFactor; fontSize = fontSize * scaleFactor;
// Adjust font size if it exceeds canvas width // Adjust font size if it exceeds canvas width
ctx.font = `${fontSize}px ${fontCaveat.style.fontFamily}`; ctx.font = `${fontSize}px ${fontFamily}`;
const textWidth = ctx.measureText(typedSignature).width; const textWidth = ctx.measureText(typedSignature).width;
@ -236,7 +238,7 @@ export const SignaturePad = ({
} }
// Set final font and render text // Set final font and render text
ctx.font = `${fontSize}px ${fontCaveat.style.fontFamily}`; ctx.font = `${fontSize}px ${fontFamily}`;
ctx.fillText(typedSignature, canvasWidth / 2, canvasHeight / 2); ctx.fillText(typedSignature, canvasWidth / 2, canvasHeight / 2);
} }
} }
@ -247,7 +249,7 @@ export const SignaturePad = ({
setTypedSignature(newValue); setTypedSignature(newValue);
if (newValue.trim() !== '') { if (newValue.trim() !== '') {
onChange?.($el.current?.toDataURL() || null); onChange?.(newValue);
} else { } else {
onChange?.(null); onChange?.(null);
} }
@ -256,7 +258,7 @@ export const SignaturePad = ({
useEffect(() => { useEffect(() => {
if (typedSignature.trim() !== '') { if (typedSignature.trim() !== '') {
renderTypedSignature(); renderTypedSignature();
onChange?.($el.current?.toDataURL() || null); onChange?.(typedSignature);
} else { } else {
onClearClick(); onClearClick();
} }
@ -303,6 +305,10 @@ export const SignaturePad = ({
$el.current.width = $el.current.clientWidth * DPI; $el.current.width = $el.current.clientWidth * DPI;
$el.current.height = $el.current.clientHeight * DPI; $el.current.height = $el.current.clientHeight * DPI;
} }
if (defaultValue && typedSignature) {
renderTypedSignature();
}
}, []); }, []);
unsafe_useEffectOnce(() => { unsafe_useEffectOnce(() => {

View File

@ -55,8 +55,10 @@ import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/type
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover'; import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
import { getSignerColorStyles, useSignerColors } from '../../lib/signer-colors'; import { getSignerColorStyles, useSignerColors } from '../../lib/signer-colors';
import { Checkbox } from '../checkbox';
import type { FieldFormType } from '../document-flow/add-fields'; import type { FieldFormType } from '../document-flow/add-fields';
import { FieldAdvancedSettings } from '../document-flow/field-item-advanced-settings'; import { FieldAdvancedSettings } from '../document-flow/field-item-advanced-settings';
import { Form, FormControl, FormField, FormItem, FormLabel } from '../form/form';
import { useStep } from '../stepper'; import { useStep } from '../stepper';
import type { TAddTemplateFieldsFormSchema } from './add-template-fields.types'; import type { TAddTemplateFieldsFormSchema } from './add-template-fields.types';
@ -80,6 +82,7 @@ export type AddTemplateFieldsFormProps = {
fields: Field[]; fields: Field[];
onSubmit: (_data: TAddTemplateFieldsFormSchema) => void; onSubmit: (_data: TAddTemplateFieldsFormSchema) => void;
teamId?: number; teamId?: number;
typedSignatureEnabled?: boolean;
}; };
export const AddTemplateFieldsFormPartial = ({ export const AddTemplateFieldsFormPartial = ({
@ -89,6 +92,7 @@ export const AddTemplateFieldsFormPartial = ({
fields, fields,
onSubmit, onSubmit,
teamId, teamId,
typedSignatureEnabled,
}: AddTemplateFieldsFormProps) => { }: AddTemplateFieldsFormProps) => {
const { _ } = useLingui(); const { _ } = useLingui();
@ -97,13 +101,7 @@ export const AddTemplateFieldsFormPartial = ({
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
const [currentField, setCurrentField] = useState<FieldFormType>(); const [currentField, setCurrentField] = useState<FieldFormType>();
const { const form = useForm<TAddTemplateFieldsFormSchema>({
control,
handleSubmit,
formState: { isSubmitting },
setValue,
getValues,
} = useForm<TAddTemplateFieldsFormSchema>({
defaultValues: { defaultValues: {
fields: fields.map((field) => ({ fields: fields.map((field) => ({
nativeId: field.id, nativeId: field.id,
@ -121,13 +119,14 @@ export const AddTemplateFieldsFormPartial = ({
recipients.find((recipient) => recipient.id === field.recipientId)?.token ?? '', recipients.find((recipient) => recipient.id === field.recipientId)?.token ?? '',
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined, fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
})), })),
typedSignatureEnabled: typedSignatureEnabled ?? false,
}, },
}); });
const onFormSubmit = handleSubmit(onSubmit); const onFormSubmit = form.handleSubmit(onSubmit);
const handleSavedFieldSettings = (fieldState: FieldMeta) => { const handleSavedFieldSettings = (fieldState: FieldMeta) => {
const initialValues = getValues(); const initialValues = form.getValues();
const updatedFields = initialValues.fields.map((field) => { const updatedFields = initialValues.fields.map((field) => {
if (field.formId === currentField?.formId) { if (field.formId === currentField?.formId) {
@ -142,7 +141,7 @@ export const AddTemplateFieldsFormPartial = ({
return field; return field;
}); });
setValue('fields', updatedFields); form.setValue('fields', updatedFields);
}; };
const { const {
@ -151,7 +150,7 @@ export const AddTemplateFieldsFormPartial = ({
update, update,
fields: localFields, fields: localFields,
} = useFieldArray({ } = useFieldArray({
control, control: form.control,
name: 'fields', name: 'fields',
}); });
@ -402,6 +401,12 @@ export const AddTemplateFieldsFormPartial = ({
setShowAdvancedSettings((prev) => !prev); setShowAdvancedSettings((prev) => !prev);
}; };
const isTypedSignatureEnabled = form.watch('typedSignatureEnabled');
const handleTypedSignatureChange = (value: boolean) => {
form.setValue('typedSignatureEnabled', value, { shouldDirty: true });
};
return ( return (
<> <>
{showAdvancedSettings && currentField ? ( {showAdvancedSettings && currentField ? (
@ -568,305 +573,334 @@ export const AddTemplateFieldsFormPartial = ({
</Popover> </Popover>
)} )}
<div className="-mx-2 flex-1 overflow-y-auto px-2"> <Form {...form}>
<fieldset className="my-2 grid grid-cols-3 gap-4"> <FormField
<button control={form.control}
type="button" name="typedSignatureEnabled"
className="group h-full w-full" render={({ field: { value, ...field } }) => (
onClick={() => setSelectedField(FieldType.SIGNATURE)} <FormItem className="mb-6 flex flex-row items-center space-x-2 space-y-0">
onMouseDown={() => setSelectedField(FieldType.SIGNATURE)} <FormControl>
data-selected={selectedField === FieldType.SIGNATURE ? true : undefined} <Checkbox
> {...field}
<Card id="typedSignatureEnabled"
className={cn( checkClassName="text-white"
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50', checked={value}
// selectedSignerStyles.borderClass, onCheckedChange={(checked) => field.onChange(checked)}
)} disabled={form.formState.isSubmitting}
> />
<CardContent className="flex flex-col items-center justify-center px-6 py-4"> </FormControl>
<p
className={cn(
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-lg font-normal',
fontCaveat.className,
)}
>
<Trans>Signature</Trans>
</p>
</CardContent>
</Card>
</button>
<button <FormLabel
type="button" htmlFor="typedSignatureEnabled"
className="group h-full w-full" className="text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
onClick={() => setSelectedField(FieldType.INITIALS)} >
onMouseDown={() => setSelectedField(FieldType.INITIALS)} <Trans>Enable Typed Signatures</Trans>
data-selected={selectedField === FieldType.INITIALS ? true : undefined} </FormLabel>
> </FormItem>
<Card )}
className={cn( />
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)}
>
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
<p
className={cn(
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
)}
>
<Contact className="h-4 w-4" />
Initials
</p>
</CardContent>
</Card>
</button>
<button <div className="-mx-2 flex-1 overflow-y-auto px-2">
type="button" <fieldset className="my-2 grid grid-cols-3 gap-4">
className="group h-full w-full" <button
onClick={() => setSelectedField(FieldType.EMAIL)} type="button"
onMouseDown={() => setSelectedField(FieldType.EMAIL)} className="group h-full w-full"
data-selected={selectedField === FieldType.EMAIL ? true : undefined} onClick={() => setSelectedField(FieldType.SIGNATURE)}
> onMouseDown={() => setSelectedField(FieldType.SIGNATURE)}
<Card data-selected={selectedField === FieldType.SIGNATURE ? true : undefined}
className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)}
> >
<CardContent className="p-4"> <Card
<p className={cn(
className={cn( 'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal', // selectedSignerStyles.borderClass,
)} )}
> >
<Mail className="h-4 w-4" /> <CardContent className="flex flex-col items-center justify-center px-6 py-4">
<Trans>Email</Trans> <p
</p> className={cn(
</CardContent> 'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-lg font-normal',
</Card> fontCaveat.className,
</button> )}
>
<Trans>Signature</Trans>
</p>
</CardContent>
</Card>
</button>
<button <button
type="button" type="button"
className="group h-full w-full" className="group h-full w-full"
onClick={() => setSelectedField(FieldType.NAME)} onClick={() => setSelectedField(FieldType.INITIALS)}
onMouseDown={() => setSelectedField(FieldType.NAME)} onMouseDown={() => setSelectedField(FieldType.INITIALS)}
data-selected={selectedField === FieldType.NAME ? true : undefined} data-selected={selectedField === FieldType.INITIALS ? true : undefined}
>
<Card
className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)}
> >
<CardContent className="p-4"> <Card
<p className={cn(
className={cn( 'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal', // selectedSignerStyles.borderClass,
)} )}
> >
<User className="h-4 w-4" /> <CardContent className="flex flex-col items-center justify-center px-6 py-4">
<Trans>Name</Trans> <p
</p> className={cn(
</CardContent> 'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
</Card> )}
</button> >
<Contact className="h-4 w-4" />
Initials
</p>
</CardContent>
</Card>
</button>
<button <button
type="button" type="button"
className="group h-full w-full" className="group h-full w-full"
onClick={() => setSelectedField(FieldType.DATE)} onClick={() => setSelectedField(FieldType.EMAIL)}
onMouseDown={() => setSelectedField(FieldType.DATE)} onMouseDown={() => setSelectedField(FieldType.EMAIL)}
data-selected={selectedField === FieldType.DATE ? true : undefined} data-selected={selectedField === FieldType.EMAIL ? true : undefined}
>
<Card
className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)}
> >
<CardContent className="p-4"> <Card
<p className={cn(
className={cn( 'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal', // selectedSignerStyles.borderClass,
)} )}
> >
<CalendarDays className="h-4 w-4" /> <CardContent className="p-4">
<Trans>Date</Trans> <p
</p> className={cn(
</CardContent> 'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
</Card> )}
</button> >
<Mail className="h-4 w-4" />
<Trans>Email</Trans>
</p>
</CardContent>
</Card>
</button>
<button <button
type="button" type="button"
className="group h-full w-full" className="group h-full w-full"
onClick={() => setSelectedField(FieldType.TEXT)} onClick={() => setSelectedField(FieldType.NAME)}
onMouseDown={() => setSelectedField(FieldType.TEXT)} onMouseDown={() => setSelectedField(FieldType.NAME)}
data-selected={selectedField === FieldType.TEXT ? true : undefined} data-selected={selectedField === FieldType.NAME ? true : undefined}
>
<Card
className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)}
> >
<CardContent className="p-4"> <Card
<p className={cn(
className={cn( 'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal', // selectedSignerStyles.borderClass,
)} )}
> >
<Type className="h-4 w-4" /> <CardContent className="p-4">
<Trans>Text</Trans> <p
</p> className={cn(
</CardContent> 'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
</Card> )}
</button> >
<User className="h-4 w-4" />
<Trans>Name</Trans>
</p>
</CardContent>
</Card>
</button>
<button <button
type="button" type="button"
className="group h-full w-full" className="group h-full w-full"
onClick={() => setSelectedField(FieldType.NUMBER)} onClick={() => setSelectedField(FieldType.DATE)}
onMouseDown={() => setSelectedField(FieldType.NUMBER)} onMouseDown={() => setSelectedField(FieldType.DATE)}
data-selected={selectedField === FieldType.NUMBER ? true : undefined} data-selected={selectedField === FieldType.DATE ? true : undefined}
>
<Card
className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)}
> >
<CardContent className="p-4"> <Card
<p className={cn(
className={cn( 'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal', // selectedSignerStyles.borderClass,
)} )}
> >
<Hash className="h-4 w-4" /> <CardContent className="p-4">
<Trans>Number</Trans> <p
</p> className={cn(
</CardContent> 'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
</Card> )}
</button> >
<CalendarDays className="h-4 w-4" />
<Trans>Date</Trans>
</p>
</CardContent>
</Card>
</button>
<button <button
type="button" type="button"
className="group h-full w-full" className="group h-full w-full"
onClick={() => setSelectedField(FieldType.RADIO)} onClick={() => setSelectedField(FieldType.TEXT)}
onMouseDown={() => setSelectedField(FieldType.RADIO)} onMouseDown={() => setSelectedField(FieldType.TEXT)}
data-selected={selectedField === FieldType.RADIO ? true : undefined} data-selected={selectedField === FieldType.TEXT ? true : undefined}
>
<Card
className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)}
> >
<CardContent className="p-4"> <Card
<p className={cn(
className={cn( 'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal', // selectedSignerStyles.borderClass,
)} )}
> >
<Disc className="h-4 w-4" /> <CardContent className="p-4">
<Trans>Radio</Trans> <p
</p> className={cn(
</CardContent> 'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
</Card> )}
</button> >
<Type className="h-4 w-4" />
<Trans>Text</Trans>
</p>
</CardContent>
</Card>
</button>
<button <button
type="button" type="button"
className="group h-full w-full" className="group h-full w-full"
onClick={() => setSelectedField(FieldType.CHECKBOX)} onClick={() => setSelectedField(FieldType.NUMBER)}
onMouseDown={() => setSelectedField(FieldType.CHECKBOX)} onMouseDown={() => setSelectedField(FieldType.NUMBER)}
data-selected={selectedField === FieldType.CHECKBOX ? true : undefined} data-selected={selectedField === FieldType.NUMBER ? true : undefined}
>
<Card
className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)}
> >
<CardContent className="p-4"> <Card
<p className={cn(
className={cn( 'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal', // selectedSignerStyles.borderClass,
)} )}
> >
<CheckSquare className="h-4 w-4" /> <CardContent className="p-4">
Checkbox <p
</p> className={cn(
</CardContent> 'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
</Card> )}
</button> >
<Hash className="h-4 w-4" />
<Trans>Number</Trans>
</p>
</CardContent>
</Card>
</button>
<button <button
type="button" type="button"
className="group h-full w-full" className="group h-full w-full"
onClick={() => setSelectedField(FieldType.DROPDOWN)} onClick={() => setSelectedField(FieldType.RADIO)}
onMouseDown={() => setSelectedField(FieldType.DROPDOWN)} onMouseDown={() => setSelectedField(FieldType.RADIO)}
data-selected={selectedField === FieldType.DROPDOWN ? true : undefined} data-selected={selectedField === FieldType.RADIO ? true : undefined}
>
<Card
className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)}
> >
<CardContent className="p-4"> <Card
<p className={cn(
className={cn( 'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal', // selectedSignerStyles.borderClass,
)} )}
> >
<ChevronDown className="h-4 w-4" /> <CardContent className="p-4">
<Trans>Dropdown</Trans> <p
</p> className={cn(
</CardContent> 'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
</Card> )}
</button> >
</fieldset> <Disc className="h-4 w-4" />
</div> Radio
</p>
</CardContent>
</Card>
</button>
<button
type="button"
className="group h-full w-full"
onClick={() => setSelectedField(FieldType.CHECKBOX)}
onMouseDown={() => setSelectedField(FieldType.CHECKBOX)}
data-selected={selectedField === FieldType.CHECKBOX ? true : undefined}
>
<Card
className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)}
>
<CardContent className="p-4">
<p
className={cn(
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
)}
>
<CheckSquare className="h-4 w-4" />
{/* Not translated on purpose. */}
Checkbox
</p>
</CardContent>
</Card>
</button>
<button
type="button"
className="group h-full w-full"
onClick={() => setSelectedField(FieldType.DROPDOWN)}
onMouseDown={() => setSelectedField(FieldType.DROPDOWN)}
data-selected={selectedField === FieldType.DROPDOWN ? true : undefined}
>
<Card
className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)}
>
<CardContent className="p-4">
<p
className={cn(
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
)}
>
<ChevronDown className="h-4 w-4" />
<Trans>Dropdown</Trans>
</p>
</CardContent>
</Card>
</button>
</fieldset>
</div>
</Form>
{hasErrors && (
<div className="mt-4">
<ul>
<li className="text-sm text-red-500">
<Trans>
To proceed further, please set at least one value for the{' '}
{emptyCheckboxFields.length > 0
? 'Checkbox'
: emptyRadioFields.length > 0
? 'Radio'
: 'Select'}{' '}
field.
</Trans>
</li>
</ul>
</div>
)}
<DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep step={currentStep} maxStep={totalSteps} />
<DocumentFlowFormContainerActions
loading={form.formState.isSubmitting}
disabled={form.formState.isSubmitting}
goNextLabel={msg`Save Template`}
disableNextStep={hasErrors}
onGoBackClick={() => {
previousStep();
remove();
}}
onGoNextClick={() => void onFormSubmit()}
/>
</DocumentFlowFormContainerFooter>
</div> </div>
</DocumentFlowFormContainerContent> </DocumentFlowFormContainerContent>
{hasErrors && (
<div className="mt-4">
<ul>
<li className="text-sm text-red-500">
<Trans>
To proceed further, please set at least one value for the{' '}
{emptyCheckboxFields.length > 0
? 'Checkbox'
: emptyRadioFields.length > 0
? 'Radio'
: 'Select'}{' '}
field.
</Trans>
</li>
</ul>
</div>
)}
<DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep step={currentStep} maxStep={totalSteps} />
<DocumentFlowFormContainerActions
loading={isSubmitting}
disabled={isSubmitting}
goNextLabel={msg`Save Template`}
disableNextStep={hasErrors}
onGoBackClick={() => {
previousStep();
remove();
}}
onGoNextClick={() => void onFormSubmit()}
/>
</DocumentFlowFormContainerFooter>
</> </>
)} )}
</> </>

View File

@ -20,6 +20,7 @@ export const ZAddTemplateFieldsFormSchema = z.object({
fieldMeta: ZFieldMetaSchema, fieldMeta: ZFieldMetaSchema,
}), }),
), ),
typedSignatureEnabled: z.boolean(),
}); });
export type TAddTemplateFieldsFormSchema = z.infer<typeof ZAddTemplateFieldsFormSchema>; export type TAddTemplateFieldsFormSchema = z.infer<typeof ZAddTemplateFieldsFormSchema>;

View File

@ -138,7 +138,7 @@
--new-surface-white: 0, 0%, 91%; --new-surface-white: 0, 0%, 91%;
} }
.dark { .dark:not(.dark-mode-disabled) {
--background: 0 0% 14.9%; --background: 0 0% 14.9%;
--foreground: 0 0% 97%; --foreground: 0 0% 97%;