feat: add signature configurations (#1710)

Add ability to enable or disable allowed signature types:
- Drawn
- Typed
- Uploaded

**Tabbed style signature dialog**

![image](https://github.com/user-attachments/assets/a816fab6-b071-42a5-bb5c-6d4a2572431e)

**Document settings**

![image](https://github.com/user-attachments/assets/f0c1bff1-6be1-4c87-b384-1666fa25d7a6)

**Team preferences**

![image](https://github.com/user-attachments/assets/8767b05e-1463-4087-8672-f3f43d8b0f2c)

- Add multiselect to select allowed signatures in document and templates
settings tab
- Add multiselect to select allowed signatures in teams preferences
- Removed "Enable typed signatures" from document/template edit page
- Refactored signature pad to use tabs instead of an all in one
signature pad

Added E2E tests to check settings are applied correctly for documents
and templates
This commit is contained in:
David Nguyen
2025-03-24 15:25:29 +11:00
parent 231f51bd1f
commit 063fd32f18
85 changed files with 3141 additions and 1316 deletions

View File

@ -3,8 +3,8 @@ import { useEffect, useLayoutEffect, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { type DocumentData, type Field, FieldType } from '@prisma/client';
import type { DocumentMeta, Recipient, Signature, TemplateMeta } from '@prisma/client';
import { type DocumentData, type Field, FieldType } from '@prisma/client';
import { LucideChevronDown, LucideChevronUp } from 'lucide-react';
import { DateTime } from 'luxon';
import { useSearchParams } from 'react-router';
@ -25,12 +25,11 @@ import type {
} from '@documenso/trpc/server/field-router/schema';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { BrandingLogo } from '~/components/general/branding-logo';
@ -69,16 +68,8 @@ export const EmbedDirectTemplateClientPage = ({
const [searchParams] = useSearchParams();
const {
fullName,
email,
signature,
signatureValid,
setFullName,
setEmail,
setSignature,
setSignatureValid,
} = useRequiredDocumentSigningContext();
const { fullName, email, signature, setFullName, setEmail, setSignature } =
useRequiredDocumentSigningContext();
const [hasFinishedInit, setHasFinishedInit] = useState(false);
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
@ -194,10 +185,6 @@ export const EmbedDirectTemplateClientPage = ({
const onCompleteClick = async () => {
try {
if (hasSignatureField && !signatureValid) {
return;
}
const valid = validateFieldsInserted(pendingFields);
if (!valid) {
@ -419,34 +406,16 @@ export const EmbedDirectTemplateClientPage = ({
<Trans>Signature</Trans>
</Label>
<Card className="mt-2" gradient degrees={-120}>
<CardContent className="p-0">
<SignaturePad
className="h-44 w-full"
disabled={isThrottled || isSubmitting}
defaultValue={signature ?? undefined}
onChange={(value) => {
setSignature(value);
}}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
allowTypedSignature={Boolean(
metadata &&
'typedSignatureEnabled' in metadata &&
metadata.typedSignatureEnabled,
)}
/>
</CardContent>
</Card>
{hasSignatureField && !signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>
Signature is too small. Please provide a more complete signature.
</Trans>
</div>
)}
<SignaturePadDialog
className="mt-2"
disabled={isThrottled || isSubmitting}
disableAnimation
value={signature ?? ''}
onChange={(v) => setSignature(v ?? '')}
typedSignatureEnabled={metadata?.typedSignatureEnabled}
uploadSignatureEnabled={metadata?.uploadSignatureEnabled}
drawSignatureEnabled={metadata?.drawSignatureEnabled}
/>
</div>
)}
</div>

View File

@ -54,6 +54,8 @@ export const EmbedDocumentFields = ({
onSignField={onSignField}
onUnsignField={onUnsignField}
typedSignatureEnabled={metadata?.typedSignatureEnabled}
uploadSignatureEnabled={metadata?.uploadSignatureEnabled}
drawSignatureEnabled={metadata?.drawSignatureEnabled}
/>
))
.with(FieldType.INITIALS, () => (

View File

@ -21,13 +21,12 @@ import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with
import { trpc } from '@documenso/trpc/react';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { BrandingLogo } from '~/components/general/branding-logo';
@ -70,15 +69,8 @@ export const EmbedSignDocumentClientPage = ({
const { _ } = useLingui();
const { toast } = useToast();
const {
fullName,
email,
signature,
signatureValid,
setFullName,
setSignature,
setSignatureValid,
} = useRequiredDocumentSigningContext();
const { fullName, email, signature, setFullName, setSignature } =
useRequiredDocumentSigningContext();
const [hasFinishedInit, setHasFinishedInit] = useState(false);
const [hasDocumentLoaded, setHasDocumentLoaded] = useState(false);
@ -129,10 +121,6 @@ export const EmbedSignDocumentClientPage = ({
const onCompleteClick = async () => {
try {
if (hasSignatureField && !signatureValid) {
return;
}
const valid = validateFieldsInserted(fieldsRequiringValidation);
if (!valid) {
@ -432,34 +420,16 @@ export const EmbedSignDocumentClientPage = ({
<Trans>Signature</Trans>
</Label>
<Card className="mt-2" gradient degrees={-120}>
<CardContent className="p-0">
<SignaturePad
className="h-44 w-full"
disabled={isThrottled || isSubmitting}
defaultValue={signature ?? undefined}
onChange={(value) => {
setSignature(value);
}}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
allowTypedSignature={Boolean(
metadata &&
'typedSignatureEnabled' in metadata &&
metadata.typedSignatureEnabled,
)}
/>
</CardContent>
</Card>
{hasSignatureField && !signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>
Signature is too small. Please provide a more complete signature.
</Trans>
</div>
)}
<SignaturePadDialog
className="mt-2"
disabled={isThrottled || isSubmitting}
disableAnimation
value={signature ?? ''}
onChange={(v) => setSignature(v ?? '')}
typedSignatureEnabled={metadata?.typedSignatureEnabled}
uploadSignatureEnabled={metadata?.uploadSignatureEnabled}
drawSignatureEnabled={metadata?.drawSignatureEnabled}
/>
</div>
)}
</>
@ -477,9 +447,7 @@ export const EmbedSignDocumentClientPage = ({
) : (
<Button
className={allowDocumentRejection ? 'col-start-2' : 'col-span-2'}
disabled={
isThrottled || (!isAssistantMode && hasSignatureField && !signatureValid)
}
disabled={isThrottled}
loading={isSubmitting}
onClick={() => throttledOnCompleteClick()}
>

View File

@ -19,12 +19,15 @@ import {
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
export const ZProfileFormSchema = z.object({
name: z.string().trim().min(1, { message: 'Please enter a valid name.' }),
signature: z.string().min(1, 'Signature Pad cannot be empty'),
name: z
.string()
.trim()
.min(1, { message: msg`Please enter a valid name.`.id }),
signature: z.string().min(1, { message: msg`Signature Pad cannot be empty.`.id }),
});
export const ZTwoFactorAuthTokenSchema = z.object({
@ -109,22 +112,20 @@ export const ProfileForm = ({ className }: ProfileFormProps) => {
</Label>
<Input id="email" type="email" className="bg-muted mt-2" value={user.email} disabled />
</div>
<FormField
control={form.control}
name="signature"
render={({ field: { onChange } }) => (
render={({ field: { onChange, value } }) => (
<FormItem>
<FormLabel>
<Trans>Signature</Trans>
</FormLabel>
<FormControl>
<SignaturePad
className="h-44 w-full"
<SignaturePadDialog
disabled={isSubmitting}
containerClassName={cn('rounded-lg border bg-background')}
defaultValue={user.signature ?? undefined}
value={value}
onChange={(v) => onChange(v ?? '')}
allowTypedSignature={true}
/>
</FormControl>
<FormMessage />
@ -134,7 +135,7 @@ export const ProfileForm = ({ className }: ProfileFormProps) => {
</fieldset>
<Button type="submit" loading={isSubmitting} className="self-end">
{isSubmitting ? <Trans>Updating profile...</Trans> : <Trans>Update profile</Trans>}
<Trans>Update profile</Trans>
</Button>
</form>
</Form>

View File

@ -30,7 +30,7 @@ import {
} from '@documenso/ui/primitives/form/form';
import { Input } from '@documenso/ui/primitives/input';
import { PasswordInput } from '@documenso/ui/primitives/password-input';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { UserProfileSkeleton } from '~/components/general/user-profile-skeleton';
@ -353,16 +353,15 @@ export const SignUpForm = ({
<FormField
control={form.control}
name="signature"
render={({ field: { onChange } }) => (
render={({ field: { onChange, value } }) => (
<FormItem>
<FormLabel>
<Trans>Sign Here</Trans>
</FormLabel>
<FormControl>
<SignaturePad
className="h-36 w-full"
<SignaturePadDialog
disabled={isSubmitting}
containerClassName="mt-2 rounded-lg border bg-background"
value={value}
onChange={(v) => onChange(v ?? '')}
/>
</FormControl>

View File

@ -308,7 +308,7 @@ export function TeamBrandingPreferencesForm({ team, settings }: TeamBrandingPref
<div className="flex flex-row justify-end space-x-4">
<Button type="submit" loading={form.formState.isSubmitting}>
<Trans>Save</Trans>
<Trans>Update</Trans>
</Button>
</div>
</fieldset>

View File

@ -8,12 +8,15 @@ import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { DOCUMENT_SIGNATURE_TYPES, DocumentSignatureType } from '@documenso/lib/constants/document';
import {
SUPPORTED_LANGUAGES,
SUPPORTED_LANGUAGE_CODES,
isValidLanguageCode,
} from '@documenso/lib/constants/i18n';
import { extractTeamSignatureSettings } from '@documenso/lib/utils/teams';
import { trpc } from '@documenso/trpc/react';
import { DocumentSignatureSettingsTooltip } from '@documenso/ui/components/document/document-signature-settings-tooltip';
import { Alert } from '@documenso/ui/primitives/alert';
import { Button } from '@documenso/ui/primitives/button';
import {
@ -23,7 +26,9 @@ import {
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { MultiSelectCombobox } from '@documenso/ui/primitives/multi-select-combobox';
import {
Select,
SelectContent,
@ -38,8 +43,10 @@ const ZTeamDocumentPreferencesFormSchema = z.object({
documentVisibility: z.nativeEnum(DocumentVisibility),
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES),
includeSenderDetails: z.boolean(),
typedSignatureEnabled: z.boolean(),
includeSigningCertificate: z.boolean(),
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(1, {
message: msg`At least one signature type must be enabled`.id,
}),
});
type TTeamDocumentPreferencesFormSchema = z.infer<typeof ZTeamDocumentPreferencesFormSchema>;
@ -69,8 +76,8 @@ export const TeamDocumentPreferencesForm = ({
? settings?.documentLanguage
: 'en',
includeSenderDetails: settings?.includeSenderDetails ?? false,
typedSignatureEnabled: settings?.typedSignatureEnabled ?? true,
includeSigningCertificate: settings?.includeSigningCertificate ?? true,
signatureTypes: extractTeamSignatureSettings(settings),
},
resolver: zodResolver(ZTeamDocumentPreferencesFormSchema),
});
@ -84,7 +91,7 @@ export const TeamDocumentPreferencesForm = ({
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
signatureTypes,
} = data;
await updateTeamDocumentPreferences({
@ -93,8 +100,10 @@ export const TeamDocumentPreferencesForm = ({
documentVisibility,
documentLanguage,
includeSenderDetails,
typedSignatureEnabled,
includeSigningCertificate,
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
},
});
@ -190,6 +199,44 @@ export const TeamDocumentPreferencesForm = ({
)}
/>
<FormField
control={form.control}
name="signatureTypes"
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel className="flex flex-row items-center">
<Trans>Default Signature Settings</Trans>
<DocumentSignatureSettingsTooltip />
</FormLabel>
<FormControl>
<MultiSelectCombobox
options={Object.values(DOCUMENT_SIGNATURE_TYPES).map((option) => ({
label: _(option.label),
value: option.value,
}))}
selectedValues={field.value}
onChange={field.onChange}
className="bg-background w-full"
enableSearch={false}
emptySelectionPlaceholder="Select signature types"
testId="signature-types-combobox"
/>
</FormControl>
{form.formState.errors.signatureTypes ? (
<FormMessage />
) : (
<FormDescription>
<Trans>
Controls which signatures are allowed to be used when signing a document.
</Trans>
</FormDescription>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="includeSenderDetails"
@ -238,36 +285,6 @@ 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"
@ -301,7 +318,7 @@ export const TeamDocumentPreferencesForm = ({
<div className="flex flex-row justify-end space-x-4">
<Button type="submit" loading={form.formState.isSubmitting}>
<Trans>Save</Trans>
<Trans>Update</Trans>
</Button>
</div>
</fieldset>

View File

@ -24,7 +24,6 @@ import type {
} from '@documenso/trpc/server/field-router/schema';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import {
DocumentFlowFormContainerContent,
DocumentFlowFormContainerFooter,
@ -35,7 +34,7 @@ import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/ty
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog';
import { useStep } from '@documenso/ui/primitives/stepper';
import { DocumentSigningCheckboxField } from '~/components/general/document-signing/document-signing-checkbox-field';
@ -73,8 +72,7 @@ export const DirectTemplateSigningForm = ({
template,
onSubmit,
}: DirectTemplateSigningFormProps) => {
const { fullName, signature, signatureValid, setFullName, setSignature } =
useRequiredDocumentSigningContext();
const { fullName, signature, setFullName, setSignature } = useRequiredDocumentSigningContext();
const [localFields, setLocalFields] = useState<DirectTemplateLocalField[]>(directRecipientFields);
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
@ -135,8 +133,6 @@ export const DirectTemplateSigningForm = ({
);
};
const hasSignatureField = localFields.some((field) => field.type === FieldType.SIGNATURE);
const uninsertedFields = useMemo(() => {
return sortFieldsByPosition(localFields.filter((field) => !field.inserted));
}, [localFields]);
@ -149,10 +145,6 @@ export const DirectTemplateSigningForm = ({
const handleSubmit = async () => {
setValidateUninsertedFields(true);
if (hasSignatureField && !signatureValid) {
return;
}
const isFieldsValid = validateFieldsInserted(localFields);
if (!isFieldsValid) {
@ -240,6 +232,8 @@ export const DirectTemplateSigningForm = ({
onSignField={onSignField}
onUnsignField={onUnsignField}
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
uploadSignatureEnabled={template.templateMeta?.uploadSignatureEnabled}
drawSignatureEnabled={template.templateMeta?.drawSignatureEnabled}
/>
))
.with(FieldType.INITIALS, () => (
@ -384,19 +378,15 @@ export const DirectTemplateSigningForm = ({
<Trans>Signature</Trans>
</Label>
<Card className="mt-2" gradient degrees={-120}>
<CardContent className="p-0">
<SignaturePad
className="h-44 w-full"
disabled={isSubmitting}
defaultValue={signature ?? undefined}
onChange={(value) => {
setSignature(value);
}}
allowTypedSignature={template.templateMeta?.typedSignatureEnabled}
/>
</CardContent>
</Card>
<SignaturePadDialog
className="mt-2"
disabled={isSubmitting}
value={signature ?? ''}
onChange={(value) => setSignature(value)}
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
uploadSignatureEnabled={template.templateMeta?.uploadSignatureEnabled}
drawSignatureEnabled={template.templateMeta?.drawSignatureEnabled}
/>
</div>
</div>
</div>

View File

@ -20,11 +20,10 @@ import { trpc } from '@documenso/trpc/react';
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card';
import { Input } from '@documenso/ui/primitives/input';
import { Label } from '@documenso/ui/primitives/label';
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { SignaturePadDialog } from '@documenso/ui/primitives/signature-pad/signature-pad-dialog';
import { useToast } from '@documenso/ui/primitives/use-toast';
import {
@ -71,8 +70,7 @@ export const DocumentSigningForm = ({
const assistantSignersId = useId();
const { fullName, signature, setFullName, setSignature, signatureValid, setSignatureValid } =
useRequiredDocumentSigningContext();
const { fullName, signature, setFullName, setSignature } = useRequiredDocumentSigningContext();
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
const [isConfirmationDialogOpen, setIsConfirmationDialogOpen] = useState(false);
@ -120,10 +118,6 @@ export const DocumentSigningForm = ({
const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation);
if (hasSignatureField && !signatureValid) {
return;
}
if (!isFieldsValid) {
return;
}
@ -423,32 +417,15 @@ export const DocumentSigningForm = ({
<Trans>Signature</Trans>
</Label>
<Card className="mt-2" gradient degrees={-120}>
<CardContent className="p-0">
<SignaturePad
className="h-44 w-full"
disabled={isSubmitting}
defaultValue={signature ?? undefined}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
onChange={(value) => {
if (signatureValid) {
setSignature(value);
}
}}
allowTypedSignature={document.documentMeta?.typedSignatureEnabled}
/>
</CardContent>
</Card>
{!signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>
Signature is too small. Please provide a more complete signature.
</Trans>
</div>
)}
<SignaturePadDialog
className="mt-2"
disabled={isSubmitting}
value={signature ?? ''}
onChange={(v) => setSignature(v ?? '')}
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
uploadSignatureEnabled={document.documentMeta?.uploadSignatureEnabled}
drawSignatureEnabled={document.documentMeta?.drawSignatureEnabled}
/>
</div>
)}
</div>

View File

@ -177,6 +177,8 @@ export const DocumentSigningPageView = ({
key={field.id}
field={field}
typedSignatureEnabled={documentMeta?.typedSignatureEnabled}
uploadSignatureEnabled={documentMeta?.uploadSignatureEnabled}
drawSignatureEnabled={documentMeta?.drawSignatureEnabled}
/>
))
.with(FieldType.INITIALS, () => (

View File

@ -1,4 +1,6 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { createContext, useContext, useState } from 'react';
import { isBase64Image } from '@documenso/lib/constants/signatures';
export type DocumentSigningContextValue = {
fullName: string;
@ -7,8 +9,6 @@ export type DocumentSigningContextValue = {
setEmail: (_value: string) => void;
signature: string | null;
setSignature: (_value: string | null) => void;
signatureValid: boolean;
setSignatureValid: (_valid: boolean) => void;
};
const DocumentSigningContext = createContext<DocumentSigningContextValue | null>(null);
@ -31,6 +31,9 @@ export interface DocumentSigningProviderProps {
fullName?: string | null;
email?: string | null;
signature?: string | null;
typedSignatureEnabled?: boolean;
uploadSignatureEnabled?: boolean;
drawSignatureEnabled?: boolean;
children: React.ReactNode;
}
@ -38,18 +41,31 @@ export const DocumentSigningProvider = ({
fullName: initialFullName,
email: initialEmail,
signature: initialSignature,
typedSignatureEnabled = true,
uploadSignatureEnabled = true,
drawSignatureEnabled = true,
children,
}: DocumentSigningProviderProps) => {
const [fullName, setFullName] = useState(initialFullName || '');
const [email, setEmail] = useState(initialEmail || '');
const [signature, setSignature] = useState(initialSignature || null);
const [signatureValid, setSignatureValid] = useState(true);
useEffect(() => {
if (initialSignature) {
setSignature(initialSignature);
}
}, [initialSignature]);
// Ensure the user signature doesn't show up if it's not allowed.
const [signature, setSignature] = useState(
(() => {
const sig = initialSignature || '';
const isBase64 = isBase64Image(sig);
if (isBase64 && (uploadSignatureEnabled || drawSignatureEnabled)) {
return sig;
}
if (!isBase64 && typedSignatureEnabled) {
return sig;
}
return null;
})(),
);
return (
<DocumentSigningContext.Provider
@ -60,8 +76,6 @@ export const DocumentSigningProvider = ({
setEmail,
signature,
setSignature,
signatureValid,
setSignatureValid,
}}
>
{children}

View File

@ -17,7 +17,6 @@ import type {
} from '@documenso/trpc/server/field-router/schema';
import { Button } from '@documenso/ui/primitives/button';
import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
import { Label } from '@documenso/ui/primitives/label';
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
import { useToast } from '@documenso/ui/primitives/use-toast';
@ -29,11 +28,14 @@ import { useRequiredDocumentSigningContext } from './document-signing-provider';
import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
type SignatureFieldState = 'empty' | 'signed-image' | 'signed-text';
export type DocumentSigningSignatureFieldProps = {
field: FieldWithSignature;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
typedSignatureEnabled?: boolean;
uploadSignatureEnabled?: boolean;
drawSignatureEnabled?: boolean;
};
export const DocumentSigningSignatureField = ({
@ -41,6 +43,8 @@ export const DocumentSigningSignatureField = ({
onSignField,
onUnsignField,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
}: DocumentSigningSignatureFieldProps) => {
const { _ } = useLingui();
const { toast } = useToast();
@ -52,12 +56,8 @@ export const DocumentSigningSignatureField = ({
const containerRef = useRef<HTMLDivElement>(null);
const [fontSize, setFontSize] = useState(2);
const {
signature: providedSignature,
setSignature: setProvidedSignature,
signatureValid,
setSignatureValid,
} = useRequiredDocumentSigningContext();
const { signature: providedSignature, setSignature: setProvidedSignature } =
useRequiredDocumentSigningContext();
const { executeActionAuthProcedure } = useRequiredDocumentSigningAuthContext();
@ -89,7 +89,7 @@ export const DocumentSigningSignatureField = ({
}, [field.inserted, signature?.signatureImageAsBase64]);
const onPreSign = () => {
if (!providedSignature || !signatureValid) {
if (!providedSignature) {
setShowSignatureModal(true);
return false;
}
@ -102,6 +102,7 @@ export const DocumentSigningSignatureField = ({
const onDialogSignClick = () => {
setShowSignatureModal(false);
setProvidedSignature(localSignature);
if (!localSignature) {
return;
}
@ -116,14 +117,14 @@ export const DocumentSigningSignatureField = ({
try {
const value = signature || providedSignature;
if (!value || (signature && !signatureValid)) {
if (!value) {
setShowSignatureModal(true);
return;
}
const isTypedSignature = !value.startsWith('data:image');
if (isTypedSignature && !typedSignatureEnabled) {
if (isTypedSignature && typedSignatureEnabled === false) {
toast({
title: _(msg`Error`),
description: _(msg`Typed signatures are not allowed. Please draw your signature.`),
@ -275,29 +276,14 @@ export const DocumentSigningSignatureField = ({
</Trans>
</DialogTitle>
<div className="">
<Label htmlFor="signature">
<Trans>Signature</Trans>
</Label>
<div className="border-border mt-2 rounded-md border">
<SignaturePad
id="signature"
className="h-44 w-full"
onChange={(value) => setLocalSignature(value)}
allowTypedSignature={typedSignatureEnabled}
onValidityChange={(isValid) => {
setSignatureValid(isValid);
}}
/>
</div>
{!signatureValid && (
<div className="text-destructive mt-2 text-sm">
<Trans>Signature is too small. Please provide a more complete signature.</Trans>
</div>
)}
</div>
<SignaturePad
className="mt-2"
value={localSignature ?? ''}
onChange={({ value }) => setLocalSignature(value)}
typedSignatureEnabled={typedSignatureEnabled}
uploadSignatureEnabled={uploadSignatureEnabled}
drawSignatureEnabled={drawSignatureEnabled}
/>
<DocumentSigningDisclosure />
@ -317,7 +303,7 @@ export const DocumentSigningSignatureField = ({
<Button
type="button"
className="flex-1"
disabled={!localSignature || !signatureValid}
disabled={!localSignature}
onClick={() => onDialogSignClick()}
>
<Trans>Sign</Trans>

View File

@ -5,6 +5,7 @@ import { useLingui } from '@lingui/react';
import { DocumentDistributionMethod, DocumentStatus } from '@prisma/client';
import { useNavigate, useSearchParams } from 'react-router';
import { DocumentSignatureType } from '@documenso/lib/constants/document';
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
import {
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
@ -71,7 +72,7 @@ export const DocumentEditForm = ({
const { recipients, fields } = document;
const { mutateAsync: updateDocumentSettings } = trpc.document.setSettingsForDocument.useMutation({
const { mutateAsync: updateDocument } = trpc.document.updateDocument.useMutation({
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
onSuccess: (newData) => {
utils.document.getDocumentWithDetailsById.setData(
@ -174,9 +175,9 @@ export const DocumentEditForm = ({
const onAddSettingsFormSubmit = async (data: TAddSettingsFormSchema) => {
try {
const { timezone, dateFormat, redirectUrl, language } = data.meta;
const { timezone, dateFormat, redirectUrl, language, signatureTypes } = data.meta;
await updateDocumentSettings({
await updateDocument({
documentId: document.id,
data: {
title: data.title,
@ -190,6 +191,9 @@ export const DocumentEditForm = ({
dateFormat,
redirectUrl,
language: isValidLanguageCode(language) ? language : undefined,
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
},
});
@ -213,7 +217,7 @@ export const DocumentEditForm = ({
signingOrder: data.signingOrder,
}),
updateDocumentSettings({
updateDocument({
documentId: document.id,
meta: {
allowDictateNextSigner: data.allowDictateNextSigner,
@ -249,14 +253,6 @@ export const DocumentEditForm = ({
fields: data.fields,
});
await updateDocumentSettings({
documentId: document.id,
meta: {
typedSignatureEnabled: data.typedSignatureEnabled,
},
});
// Clear all field data from localStorage
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
@ -386,7 +382,6 @@ export const DocumentEditForm = ({
fields={fields}
onSubmit={onAddFieldsFormSubmit}
isDocumentPdfLoaded={isDocumentPdfLoaded}
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
teamId={team?.id}
/>

View File

@ -4,6 +4,7 @@ import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { useNavigate } from 'react-router';
import { DocumentSignatureType } from '@documenso/lib/constants/document';
import { isValidLanguageCode } from '@documenso/lib/constants/i18n';
import {
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
@ -124,6 +125,8 @@ export const TemplateEditForm = ({
});
const onAddSettingsFormSubmit = async (data: TAddTemplateSettingsFormSchema) => {
const { signatureTypes } = data.meta;
try {
await updateTemplateSettings({
templateId: template.id,
@ -136,6 +139,9 @@ export const TemplateEditForm = ({
},
meta: {
...data.meta,
typedSignatureEnabled: signatureTypes.includes(DocumentSignatureType.TYPE),
uploadSignatureEnabled: signatureTypes.includes(DocumentSignatureType.UPLOAD),
drawSignatureEnabled: signatureTypes.includes(DocumentSignatureType.DRAW),
language: isValidLanguageCode(data.meta.language) ? data.meta.language : undefined,
},
});
@ -188,13 +194,6 @@ export const TemplateEditForm = ({
fields: data.fields,
});
await updateTemplateSettings({
templateId: template.id,
meta: {
typedSignatureEnabled: data.typedSignatureEnabled,
},
});
// Clear all field data from localStorage
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
@ -286,7 +285,6 @@ export const TemplateEditForm = ({
fields={fields}
onSubmit={onAddFieldsFormSubmit}
teamId={team?.id}
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
/>
</Stepper>
</DocumentFlowFormContainer>

View File

@ -79,7 +79,14 @@ export default function DirectTemplatePage() {
const { template, directTemplateRecipient } = data;
return (
<DocumentSigningProvider email={user?.email} fullName={user?.name} signature={user?.signature}>
<DocumentSigningProvider
email={user?.email}
fullName={user?.name}
signature={user?.signature}
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
uploadSignatureEnabled={template.templateMeta?.uploadSignatureEnabled}
drawSignatureEnabled={template.templateMeta?.drawSignatureEnabled}
>
<DocumentSigningAuthProvider
documentAuthOptions={template.authOptions}
recipient={directTemplateRecipient}

View File

@ -233,6 +233,9 @@ export default function SigningPage() {
email={recipient.email}
fullName={user?.email === recipient.email ? user?.name : recipient.name}
signature={user?.email === recipient.email ? user?.signature : undefined}
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
uploadSignatureEnabled={document.documentMeta?.uploadSignatureEnabled}
drawSignatureEnabled={document.documentMeta?.drawSignatureEnabled}
>
<DocumentSigningAuthProvider
documentAuthOptions={document.authOptions}

View File

@ -131,7 +131,14 @@ export default function EmbedDirectTemplatePage() {
} = useSuperLoaderData<typeof loader>();
return (
<DocumentSigningProvider email={user?.email} fullName={user?.name} signature={user?.signature}>
<DocumentSigningProvider
email={user?.email}
fullName={user?.name}
signature={user?.signature}
typedSignatureEnabled={template.templateMeta?.typedSignatureEnabled}
uploadSignatureEnabled={template.templateMeta?.uploadSignatureEnabled}
drawSignatureEnabled={template.templateMeta?.drawSignatureEnabled}
>
<DocumentSigningAuthProvider
documentAuthOptions={template.authOptions}
recipient={recipient}

View File

@ -156,6 +156,9 @@ export default function EmbedSignDocumentPage() {
email={recipient.email}
fullName={user?.email === recipient.email ? user?.name : recipient.name}
signature={user?.email === recipient.email ? user?.signature : undefined}
typedSignatureEnabled={document.documentMeta?.typedSignatureEnabled}
uploadSignatureEnabled={document.documentMeta?.uploadSignatureEnabled}
drawSignatureEnabled={document.documentMeta?.drawSignatureEnabled}
>
<DocumentSigningAuthProvider
documentAuthOptions={document.authOptions}

View File

@ -326,6 +326,8 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
allowDictateNextSigner: body.meta.allowDictateNextSigner,
language: body.meta.language,
typedSignatureEnabled: body.meta.typedSignatureEnabled,
uploadSignatureEnabled: body.meta.uploadSignatureEnabled,
drawSignatureEnabled: body.meta.drawSignatureEnabled,
distributionMethod: body.meta.distributionMethod,
emailSettings: body.meta.emailSettings,
requestMetadata: metadata,

View File

@ -158,6 +158,8 @@ export const ZCreateDocumentMutationSchema = z.object({
allowDictateNextSigner: z.boolean().optional(),
language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
typedSignatureEnabled: z.boolean().optional().default(true),
uploadSignatureEnabled: z.boolean().optional().default(true),
drawSignatureEnabled: z.boolean().optional().default(true),
distributionMethod: z.nativeEnum(DocumentDistributionMethod).optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
@ -291,6 +293,8 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
language: z.enum(SUPPORTED_LANGUAGE_CODES),
distributionMethod: z.nativeEnum(DocumentDistributionMethod),
typedSignatureEnabled: z.boolean(),
uploadSignatureEnabled: z.boolean(),
drawSignatureEnabled: z.boolean(),
emailSettings: ZDocumentEmailSettingsSchema,
})
.partial()

View File

@ -246,7 +246,9 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
});
}
await signSignaturePad(page);
if (fields.some((field) => field.type === FieldType.SIGNATURE)) {
await signSignaturePad(page);
}
for (const field of fields) {
await page.locator(`#field-${field.id}`).getByRole('button').click();
@ -349,7 +351,9 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient an
});
}
await signSignaturePad(page);
if (fields.some((field) => field.type === FieldType.SIGNATURE)) {
await signSignaturePad(page);
}
for (const field of fields) {
await page.locator(`#field-${field.id}`).getByRole('button').click();

View File

@ -11,7 +11,7 @@ import { prisma } from '@documenso/prisma';
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
import { seedUser } from '@documenso/prisma/seed/users';
import { signSignaturePad } from '../fixtures/signature';
import { signDirectSignaturePad, signSignaturePad } from '../fixtures/signature';
test('[NEXT_RECIPIENT_DICTATION]: should allow updating next recipient when dictation is enabled', async ({
page,
@ -322,7 +322,7 @@ test('[NEXT_RECIPIENT_DICTATION]: should allow assistant to dictate next signer'
await page.locator(`#field-${field.id}`).getByRole('button').click();
if (field.type === FieldType.SIGNATURE) {
await signSignaturePad(page);
await signDirectSignaturePad(page);
await page.getByRole('button', { name: 'Sign', exact: true }).click();
}

View File

@ -222,7 +222,10 @@ test.describe('Signing Certificate Tests', () => {
// 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
.getByRole('button', { name: /Update/ })
.first()
.click();
await page.waitForTimeout(1000);
@ -236,7 +239,10 @@ test.describe('Signing Certificate Tests', () => {
// 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
.getByRole('button', { name: /Update/ })
.first()
.click();
await page.waitForTimeout(1000);

View File

@ -1,40 +1,28 @@
import type { Page } from '@playwright/test';
/**
* Will open the signature pad dialog and sign it.
*/
export const signSignaturePad = async (page: Page) => {
await page.waitForTimeout(200);
const canvas = page.getByTestId('signature-pad');
await page.getByTestId('signature-pad-dialog-button').click();
const box = await canvas.boundingBox();
// Click type tab
await page.getByRole('tab', { name: 'Type' }).click();
await page.getByTestId('signature-pad-type-input').fill('Signature');
if (!box) {
throw new Error('Signature pad not found');
}
// Calculate center point
const centerX = box.x + box.width / 2;
const centerY = box.y + box.height / 2;
// Calculate square size (making it slightly smaller than the canvas)
const squareSize = Math.min(box.width, box.height) * 0.4; // 40% of the smallest dimension
// Move to center
await page.mouse.move(centerX, centerY);
await page.mouse.down();
// Draw square clockwise from center
// Move right
await page.mouse.move(centerX + squareSize, centerY, { steps: 10 });
// Move down
await page.mouse.move(centerX + squareSize, centerY + squareSize, { steps: 10 });
// Move left
await page.mouse.move(centerX - squareSize, centerY + squareSize, { steps: 10 });
// Move up
await page.mouse.move(centerX - squareSize, centerY - squareSize, { steps: 10 });
// Move right
await page.mouse.move(centerX + squareSize, centerY - squareSize, { steps: 10 });
// Move down to close the square
await page.mouse.move(centerX + squareSize, centerY, { steps: 10 });
await page.mouse.up();
// Click Next button
await page.getByRole('button', { name: 'Next' }).click();
};
/**
* For when the signature pad is already open.
*/
export const signDirectSignaturePad = async (page: Page) => {
await page.waitForTimeout(200);
// Click type tab
await page.getByRole('tab', { name: 'Type' }).click();
await page.getByTestId('signature-pad-type-input').fill('Signature');
};

View File

@ -23,7 +23,7 @@ test('[TEAMS]: update the default document visibility in the team global setting
// !: Brittle selector
await page.getByRole('combobox').first().click();
await page.getByRole('option', { name: 'Admin' }).click();
await page.getByRole('button', { name: 'Save' }).first().click();
await page.getByRole('button', { name: 'Update' }).first().click();
const toast = page.locator('li[role="status"][data-state="open"]').first();
await expect(toast).toBeVisible();
@ -47,7 +47,7 @@ test('[TEAMS]: update the sender details in the team global settings', async ({
await expect(checkbox).toBeChecked();
await page.getByRole('button', { name: 'Save' }).first().click();
await page.getByRole('button', { name: 'Update' }).first().click();
const toast = page.locator('li[role="status"][data-state="open"]').first();
await expect(toast).toBeVisible();

View File

@ -0,0 +1,182 @@
import { expect, test } from '@playwright/test';
import { prisma } from '@documenso/prisma';
import {
seedTeamDocumentWithMeta,
seedTeamDocuments,
seedTeamTemplateWithMeta,
} from '@documenso/prisma/seed/documents';
import { apiSignin } from '../fixtures/authentication';
test.describe.configure({ mode: 'parallel' });
test('[TEAMS]: check that default team signature settings are all enabled', async ({ page }) => {
const { team } = await seedTeamDocuments();
await apiSignin({
page,
email: team.owner.email,
password: 'password',
redirectPath: `/t/${team.url}/settings/preferences`,
});
// Verify that the default created team settings has all signatures enabled
await expect(page.getByRole('combobox').filter({ hasText: 'Type' })).toBeVisible();
await expect(page.getByRole('combobox').filter({ hasText: 'Upload' })).toBeVisible();
await expect(page.getByRole('combobox').filter({ hasText: 'Draw' })).toBeVisible();
const document = await seedTeamDocumentWithMeta(team);
// Create a document and check the settings
await page.goto(`/t/${team.url}/documents/${document.id}/edit`);
// Verify that the settings match
await page.getByRole('button', { name: 'Advanced Options' }).click();
await expect(page.getByRole('combobox').filter({ hasText: 'Type' })).toBeVisible();
await expect(page.getByRole('combobox').filter({ hasText: 'Upload' })).toBeVisible();
await expect(page.getByRole('combobox').filter({ hasText: 'Draw' })).toBeVisible();
// Go to document and check that the signatured tabs are correct.
await page.goto(`/sign/${document.recipients[0].token}`);
await page.getByTestId('signature-pad-dialog-button').click();
// Check the tab values
await expect(page.getByRole('tab', { name: 'Type' })).toBeVisible();
await expect(page.getByRole('tab', { name: 'Upload' })).toBeVisible();
await expect(page.getByRole('tab', { name: 'Draw' })).toBeVisible();
});
test('[TEAMS]: check signature modes can be disabled', async ({ page }) => {
const { team } = await seedTeamDocuments();
await apiSignin({
page,
email: team.owner.email,
password: 'password',
redirectPath: `/t/${team.url}/settings/preferences`,
});
const allTabs = ['Type', 'Upload', 'Draw'];
const tabTest = [['Type', 'Upload', 'Draw'], ['Type', 'Upload'], ['Type']];
for (const tabs of tabTest) {
await page.goto(`/t/${team.url}/settings/preferences`);
// Update combobox to have the correct tabs
await page.getByTestId('signature-types-combobox').click();
await expect(page.getByRole('option', { name: 'Type' })).toBeVisible();
await expect(page.getByRole('option', { name: 'Upload' })).toBeVisible();
await expect(page.getByRole('option', { name: 'Draw' })).toBeVisible();
// Clear all selected items.
for (const tab of allTabs) {
const item = page.getByRole('option', { name: tab });
const isSelected = (await item.innerHTML()).includes('opacity-100');
if (isSelected) {
await item.click();
}
}
// Selected wanted items.
for (const tab of tabs) {
const item = page.getByRole('option', { name: tab });
await item.click();
}
await page.getByRole('button', { name: 'Update' }).first().click();
const document = await seedTeamDocumentWithMeta(team);
// Go to document and check that the signatured tabs are correct.
await page.goto(`/sign/${document.recipients[0].token}`);
await page.getByTestId('signature-pad-dialog-button').click();
// Check the tab values
for (const tab of allTabs) {
if (tabs.includes(tab)) {
await expect(page.getByRole('tab', { name: tab })).toBeVisible();
} else {
await expect(page.getByRole('tab', { name: tab })).not.toBeVisible();
}
}
}
});
test('[TEAMS]: check signature modes work for templates', async ({ page }) => {
const { team } = await seedTeamDocuments();
await apiSignin({
page,
email: team.owner.email,
password: 'password',
redirectPath: `/t/${team.url}/settings/preferences`,
});
const allTabs = ['Type', 'Upload', 'Draw'];
const tabTest = [['Type', 'Upload', 'Draw'], ['Type', 'Upload'], ['Type']];
for (const tabs of tabTest) {
await page.goto(`/t/${team.url}/settings/preferences`);
// Update combobox to have the correct tabs
await page.getByTestId('signature-types-combobox').click();
await expect(page.getByRole('option', { name: 'Type' })).toBeVisible();
await expect(page.getByRole('option', { name: 'Upload' })).toBeVisible();
await expect(page.getByRole('option', { name: 'Draw' })).toBeVisible();
// Clear all selected items.
for (const tab of allTabs) {
const item = page.getByRole('option', { name: tab });
const isSelected = (await item.innerHTML()).includes('opacity-100');
if (isSelected) {
await item.click();
}
}
// Selected wanted items.
for (const tab of tabs) {
const item = page.getByRole('option', { name: tab });
await item.click();
}
await page.getByRole('button', { name: 'Update' }).first().click();
const template = await seedTeamTemplateWithMeta(team);
await page.goto(`/t/${team.url}/templates/${template.id}`);
await page.getByRole('button', { name: 'Use' }).click();
// Check the send document checkbox to true
await page.getByLabel('Send document').click();
await page.getByRole('button', { name: 'Create and send' }).click();
await page.waitForTimeout(1000);
const document = await prisma.document.findFirst({
where: {
templateId: template.id,
},
include: {
documentMeta: true,
},
});
// Test kinda flaky, debug here.
// console.log({
// tabs,
// typedSignatureEnabled: document?.documentMeta?.typedSignatureEnabled,
// uploadSignatureEnabled: document?.documentMeta?.uploadSignatureEnabled,
// drawSignatureEnabled: document?.documentMeta?.drawSignatureEnabled,
// });
expect(document?.documentMeta?.typedSignatureEnabled).toEqual(tabs.includes('Type'));
expect(document?.documentMeta?.uploadSignatureEnabled).toEqual(tabs.includes('Upload'));
expect(document?.documentMeta?.drawSignatureEnabled).toEqual(tabs.includes('Draw'));
}
});

View File

@ -298,6 +298,7 @@ test('[DIRECT_TEMPLATES]: use direct template link with 2 recipients', async ({
await page.goto(formatDirectTemplatePath(template.directLink?.token || ''));
await expect(page.getByRole('heading', { name: 'General' })).toBeVisible();
await page.waitForTimeout(1000);
await page.getByPlaceholder('recipient@documenso.com').fill(seedTestEmail());
await page.getByRole('button', { name: 'Continue' }).click();

View File

@ -4,6 +4,7 @@ import { getUserByEmail } from '@documenso/lib/server-only/user/get-user-by-emai
import { seedUser } from '@documenso/prisma/seed/users';
import { apiSignin } from '../fixtures/authentication';
import { signSignaturePad } from '../fixtures/signature';
test('[USER] update full name', async ({ page }) => {
const user = await seedUser();
@ -12,7 +13,7 @@ test('[USER] update full name', async ({ page }) => {
await page.getByLabel('Full Name').fill('John Doe');
await page.getByPlaceholder('Type your signature').fill('John Doe');
await signSignaturePad(page);
await page.getByRole('button', { name: 'Update profile' }).click();

View File

@ -34,3 +34,29 @@ export const DOCUMENT_DISTRIBUTION_METHODS: Record<string, DocumentDistributionM
description: msg`None`,
},
} satisfies Record<DocumentDistributionMethod, DocumentDistributionMethodTypeData>;
export enum DocumentSignatureType {
DRAW = 'draw',
TYPE = 'type',
UPLOAD = 'upload',
}
type DocumentSignatureTypeData = {
label: MessageDescriptor;
value: DocumentSignatureType;
};
export const DOCUMENT_SIGNATURE_TYPES = {
[DocumentSignatureType.DRAW]: {
label: msg`Draw`,
value: DocumentSignatureType.DRAW,
},
[DocumentSignatureType.TYPE]: {
label: msg`Type`,
value: DocumentSignatureType.TYPE,
},
[DocumentSignatureType.UPLOAD]: {
label: msg`Upload`,
value: DocumentSignatureType.UPLOAD,
},
} satisfies Record<DocumentSignatureType, DocumentSignatureTypeData>;

View File

@ -0,0 +1,4 @@
export const SIGNATURE_CANVAS_DPI = 2;
export const SIGNATURE_MIN_COVERAGE_THRESHOLD = 0.01;
export const isBase64Image = (value: string) => value.startsWith('data:image/png;base64,');

View File

@ -23,6 +23,8 @@ const SEND_TEAM_DELETED_EMAIL_JOB_DEFINITION_SCHEMA = z.object({
brandingHidePoweredBy: z.boolean(),
teamId: z.number(),
typedSignatureEnabled: z.boolean(),
uploadSignatureEnabled: z.boolean(),
drawSignatureEnabled: z.boolean(),
})
.nullish(),
}),

View File

@ -27,6 +27,8 @@ export type CreateDocumentMetaOptions = {
allowDictateNextSigner?: boolean;
distributionMethod?: DocumentDistributionMethod;
typedSignatureEnabled?: boolean;
uploadSignatureEnabled?: boolean;
drawSignatureEnabled?: boolean;
language?: SupportedLanguageCodes;
requestMetadata: ApiRequestMetadata;
};
@ -46,6 +48,8 @@ export const upsertDocumentMeta = async ({
emailSettings,
distributionMethod,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
language,
requestMetadata,
}: CreateDocumentMetaOptions) => {
@ -99,6 +103,8 @@ export const upsertDocumentMeta = async ({
emailSettings,
distributionMethod,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
language,
},
update: {
@ -113,6 +119,8 @@ export const upsertDocumentMeta = async ({
emailSettings,
distributionMethod,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
language,
},
});

View File

@ -158,6 +158,10 @@ export const createDocumentV2 = async ({
language: meta?.language || team?.teamGlobalSettings?.documentLanguage,
typedSignatureEnabled:
meta?.typedSignatureEnabled ?? team?.teamGlobalSettings?.typedSignatureEnabled,
uploadSignatureEnabled:
meta?.uploadSignatureEnabled ?? team?.teamGlobalSettings?.uploadSignatureEnabled,
drawSignatureEnabled:
meta?.drawSignatureEnabled ?? team?.teamGlobalSettings?.drawSignatureEnabled,
},
},
},

View File

@ -128,8 +128,10 @@ export const createDocument = async ({
documentMeta: {
create: {
language: team?.teamGlobalSettings?.documentLanguage,
typedSignatureEnabled: team?.teamGlobalSettings?.typedSignatureEnabled,
timezone: timezone,
typedSignatureEnabled: team?.teamGlobalSettings?.typedSignatureEnabled ?? true,
uploadSignatureEnabled: team?.teamGlobalSettings?.uploadSignatureEnabled ?? true,
drawSignatureEnabled: team?.teamGlobalSettings?.drawSignatureEnabled ?? true,
},
},
},

View File

@ -201,7 +201,7 @@ export const signFieldWithToken = async ({
throw new Error('Signature field must have a signature');
}
if (isSignatureField && !documentMeta?.typedSignatureEnabled && typedSignature) {
if (isSignatureField && documentMeta?.typedSignatureEnabled === false && typedSignature) {
throw new Error('Typed signatures are not allowed. Please draw your signature');
}

View File

@ -1,73 +0,0 @@
import type { DocumentVisibility } from '@prisma/client';
import { TeamMemberRole } from '@prisma/client';
import type { z } from 'zod';
import { prisma } from '@documenso/prisma';
import { TeamGlobalSettingsSchema } from '@documenso/prisma/generated/zod/modelSchema/TeamGlobalSettingsSchema';
import type { SupportedLanguageCodes } from '../../constants/i18n';
export type UpdateTeamDocumentSettingsOptions = {
userId: number;
teamId: number;
settings: {
documentVisibility: DocumentVisibility;
documentLanguage: SupportedLanguageCodes;
includeSenderDetails: boolean;
typedSignatureEnabled: boolean;
includeSigningCertificate: boolean;
};
};
export const ZUpdateTeamDocumentSettingsResponseSchema = TeamGlobalSettingsSchema;
export type TUpdateTeamDocumentSettingsResponse = z.infer<
typeof ZUpdateTeamDocumentSettingsResponseSchema
>;
export const updateTeamDocumentSettings = async ({
userId,
teamId,
settings,
}: UpdateTeamDocumentSettingsOptions): Promise<TUpdateTeamDocumentSettingsResponse> => {
const {
documentVisibility,
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
} = settings;
const member = await prisma.teamMember.findFirst({
where: {
userId,
teamId,
},
});
if (!member || member.role !== TeamMemberRole.ADMIN) {
throw new Error('You do not have permission to update this team.');
}
return await prisma.teamGlobalSettings.upsert({
where: {
teamId,
},
create: {
teamId,
documentVisibility,
documentLanguage,
includeSenderDetails,
typedSignatureEnabled,
includeSigningCertificate,
},
update: {
documentVisibility,
documentLanguage,
includeSenderDetails,
typedSignatureEnabled,
includeSigningCertificate,
},
});
};

View File

@ -324,6 +324,9 @@ export const createDocumentFromDirectTemplate = async ({
language: metaLanguage,
signingOrder: metaSigningOrder,
distributionMethod: template.templateMeta?.distributionMethod,
typedSignatureEnabled: template.templateMeta?.typedSignatureEnabled,
uploadSignatureEnabled: template.templateMeta?.uploadSignatureEnabled,
drawSignatureEnabled: template.templateMeta?.drawSignatureEnabled,
},
},
},

View File

@ -96,6 +96,9 @@ export const createDocumentFromTemplateLegacy = async ({
signingOrder: template.templateMeta?.signingOrder ?? undefined,
language:
template.templateMeta?.language || template.team?.teamGlobalSettings?.documentLanguage,
typedSignatureEnabled: template.templateMeta?.typedSignatureEnabled,
uploadSignatureEnabled: template.templateMeta?.uploadSignatureEnabled,
drawSignatureEnabled: template.templateMeta?.drawSignatureEnabled,
},
},
},

View File

@ -82,9 +82,11 @@ export type CreateDocumentFromTemplateOptions = {
signingOrder?: DocumentSigningOrder;
language?: SupportedLanguageCodes;
distributionMethod?: DocumentDistributionMethod;
typedSignatureEnabled?: boolean;
allowDictateNextSigner?: boolean;
emailSettings?: TDocumentEmailSettings;
typedSignatureEnabled?: boolean;
uploadSignatureEnabled?: boolean;
drawSignatureEnabled?: boolean;
};
requestMetadata: ApiRequestMetadata;
};
@ -405,6 +407,10 @@ export const createDocumentFromTemplate = async ({
template.team?.teamGlobalSettings?.documentLanguage,
typedSignatureEnabled:
override?.typedSignatureEnabled ?? template.templateMeta?.typedSignatureEnabled,
uploadSignatureEnabled:
override?.uploadSignatureEnabled ?? template.templateMeta?.uploadSignatureEnabled,
drawSignatureEnabled:
override?.drawSignatureEnabled ?? template.templateMeta?.drawSignatureEnabled,
allowDictateNextSigner:
override?.allowDictateNextSigner ??
template.templateMeta?.allowDictateNextSigner ??

View File

@ -4,6 +4,8 @@ import { prisma } from '@documenso/prisma';
import { TemplateSchema } from '@documenso/prisma/generated/zod/modelSchema//TemplateSchema';
import type { TCreateTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema';
import { AppError, AppErrorCode } from '../../errors/app-error';
export type CreateTemplateOptions = TCreateTemplateMutationSchema & {
userId: number;
teamId?: number;
@ -19,8 +21,10 @@ export const createTemplate = async ({
teamId,
templateDocumentDataId,
}: CreateTemplateOptions) => {
let team = null;
if (teamId) {
await prisma.team.findFirstOrThrow({
team = await prisma.team.findFirst({
where: {
id: teamId,
members: {
@ -29,7 +33,14 @@ export const createTemplate = async ({
},
},
},
include: {
teamGlobalSettings: true,
},
});
if (!team) {
throw new AppError(AppErrorCode.NOT_FOUND);
}
}
return await prisma.template.create({
@ -38,6 +49,14 @@ export const createTemplate = async ({
userId,
templateDocumentDataId,
teamId,
templateMeta: {
create: {
language: team?.teamGlobalSettings?.documentLanguage,
typedSignatureEnabled: team?.teamGlobalSettings?.typedSignatureEnabled ?? true,
uploadSignatureEnabled: team?.teamGlobalSettings?.uploadSignatureEnabled ?? true,
drawSignatureEnabled: team?.teamGlobalSettings?.drawSignatureEnabled ?? true,
},
},
},
});
};

View File

@ -419,6 +419,10 @@ msgstr "<0>{teamName}</0> hat angefragt, Ihre E-Mail-Adresse für ihr Team bei D
msgid "<0>Click to upload</0> or drag and drop"
msgstr "<0>Klicken Sie hier, um hochzuladen</0> oder ziehen Sie die Datei per Drag & Drop"
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "<0>Drawn</0> - A signature that is drawn using a mouse or stylus."
msgstr ""
#: packages/ui/primitives/template-flow/add-template-settings.tsx
msgid "<0>Email</0> - The recipient will be emailed the document to sign, approve, etc."
msgstr "<0>E-Mail</0> - Der Empfänger erhält das Dokument zur Unterschrift, Genehmigung usw."
@ -465,6 +469,14 @@ msgstr "<0>Passkey erforderlich</0> - Der Empfänger muss ein Konto haben und de
msgid "<0>Sender:</0> All"
msgstr "<0>Absender:</0> Alle"
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "<0>Typed</0> - A signature that is typed using a keyboard."
msgstr ""
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "<0>Uploaded</0> - A signature that is uploaded from a file."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "<0>You are about to complete approving <1>\"{documentTitle}\"</1>.</0><2/> Are you sure?"
msgstr "<0>Sie sind dabei, die Genehmigung von <1>\"{documentTitle}\"</1> abzuschließen.</0><2/> Sind Sie sicher?"
@ -898,6 +910,16 @@ msgstr "Alle Zeiten"
msgid "Allow document recipients to reply directly to this email address"
msgstr "Erlauben Sie den Dokumentempfängern, direkt an diese E-Mail-Adresse zu antworten"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "Allow signers to dictate next signer"
msgstr ""
#: packages/ui/primitives/template-flow/add-template-settings.tsx
#: packages/ui/primitives/document-flow/add-settings.tsx
msgid "Allowed Signature Types"
msgstr ""
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Allows authenticating using biometrics, password managers, hardware keys, etc."
msgstr "Erlaubt die Authentifizierung mit biometrischen Daten, Passwort-Managern, Hardware-Schlüsseln usw."
@ -1237,6 +1259,13 @@ msgstr ""
msgid "Assisting"
msgstr ""
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
#: packages/ui/primitives/template-flow/add-template-settings.types.tsx
#: packages/ui/primitives/document-flow/add-settings.types.ts
#: packages/lib/types/document-meta.ts
msgid "At least one signature type must be enabled"
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
msgid "Attempts sealing the document again, useful for after a code change has occurred to resolve an erroneous document."
msgstr "Versuche, das Dokument erneut zu versiegeln, nützlich nach einer Codeänderung, um ein fehlerhaftes Dokument zu beheben."
@ -1309,11 +1338,11 @@ msgstr "Bitte bestätige vor dem Start deine E-Mail-Adresse, indem du auf den Bu
msgid "Billing"
msgstr "Abrechnung"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Black"
msgstr "Schwarz"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Blue"
msgstr "Blau"
@ -1384,6 +1413,10 @@ msgstr "Indem Sie den elektronischen Unterzeichnungsdienst von Documenso verwend
msgid "By proceeding with your electronic signature, you acknowledge and consent that it will be used to sign the given document and holds the same legal validity as a handwritten signature. By completing the electronic signing process, you affirm your understanding and acceptance of these conditions."
msgstr "Indem Sie fortfahren, Ihre elektronische Unterschrift zu leisten, erkennen Sie an und stimmen zu, dass sie verwendet wird, um das gegebene Dokument zu unterzeichnen, und die gleiche rechtliche Gültigkeit wie eine handschriftliche Unterschrift hat. Durch den Abschluss des elektronischen Unterzeichnungsprozesses bestätigen Sie Ihr Verständnis und Ihre Akzeptanz dieser Bedingungen."
#: apps/remix/app/components/forms/signup.tsx
msgid "By proceeding, you agree to our <0>Terms of Service</0> and <1>Privacy Policy</1>."
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "By using the electronic signature feature, you are consenting to conduct transactions and receive disclosures electronically. You acknowledge that your electronic signature on documents is binding and that you accept the terms outlined in the documents you are signing."
msgstr "Durch die Verwendung der elektronischen Unterschriftsfunktion stimmen Sie zu, Transaktionen durchzuführen und Offenlegungen elektronisch zu erhalten. Sie erkennen an, dass Ihre elektronische Unterschrift auf Dokumenten bindend ist und dass Sie die Bedingungen akzeptieren, die in den Dokumenten dargelegt sind, die Sie unterzeichnen."
@ -1436,6 +1469,8 @@ msgstr ""
#: apps/remix/app/components/dialogs/document-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/signature-pad/signature-pad-dialog.tsx
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Cancel"
@ -1522,7 +1557,7 @@ msgstr "Datei löschen"
msgid "Clear filters"
msgstr "Filter löschen"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-draw.tsx
msgid "Clear Signature"
msgstr "Unterschrift löschen"
@ -1697,6 +1732,7 @@ msgstr "Inhalt"
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/document-flow/document-flow-root.tsx
msgid "Continue"
msgstr "Fortsetzen"
@ -1737,14 +1773,18 @@ msgstr "Steuert die Standard-sichtbarkeit eines hochgeladenen Dokuments."
msgid "Controls the formatting of the message that will be sent when inviting a recipient to sign a document. If a custom message has been provided while configuring the document, it will be used instead."
msgstr "Steuert das Format der Nachricht, die gesendet wird, wenn ein Empfänger eingeladen wird, ein Dokument zu unterschreiben. Wenn eine benutzerdefinierte Nachricht beim Konfigurieren des Dokuments bereitgestellt wurde, wird diese stattdessen verwendet."
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Controls whether the recipients can sign the documents using a typed signature. Enable or disable the typed signature globally."
msgstr "Legt fest, ob die Empfänger die Dokumente mit einer getippten Unterschrift unterschreiben können. Aktivieren oder deaktivieren Sie die getippte Unterschrift global."
#: packages/ui/primitives/document-flow/add-settings.tsx
msgid "Controls the language for the document, including the language to be used for email notifications, and the final certificate that is generated and attached to the document."
msgstr ""
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "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."
msgstr "Legt fest, ob das Signaturzertifikat in das Dokument aufgenommen wird, wenn es heruntergeladen wird. Das Signaturzertifikat kann weiterhin separat von der Protokollseite heruntergeladen werden."
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Controls which signatures are allowed to be used when signing a document."
msgstr ""
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Copied"
@ -1975,6 +2015,10 @@ msgstr "Standardsprache des Dokuments"
msgid "Default Document Visibility"
msgstr "Standard Sichtbarkeit des Dokuments"
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Default Signature Settings"
msgstr ""
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
msgid "delete"
msgstr "löschen"
@ -2201,6 +2245,11 @@ msgstr "Dokument \"{0}\" - Abgelehnt von {1}"
msgid "Document \"{0}\" - Rejection Confirmed"
msgstr "Dokument \"{0}\" - Ablehnung Bestätigt"
#. placeholder {0}: document.title
#: packages/lib/jobs/definitions/emails/send-document-cancelled-emails.handler.ts
msgid "Document \"{0}\" Cancelled"
msgstr ""
#: packages/ui/primitives/template-flow/add-template-settings.tsx
#: packages/ui/primitives/document-flow/add-settings.tsx
#: packages/ui/components/document/document-global-auth-access-select.tsx
@ -2349,6 +2398,10 @@ msgstr "Dokumentpräferenzen aktualisiert"
msgid "Document re-sent"
msgstr "Dokument erneut gesendet"
#: apps/remix/app/components/general/document/document-status.tsx
msgid "Document rejected"
msgstr ""
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/components/embed/embed-document-rejected.tsx
#: packages/email/template-components/template-document-rejected.tsx
@ -2486,6 +2539,10 @@ msgstr "Entwurfte Dokumente"
msgid "Drag & drop your PDF here."
msgstr "Ziehen Sie Ihr PDF hierher."
#: packages/lib/constants/document.ts
msgid "Draw"
msgstr ""
#: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/document-flow/add-fields.tsx
msgid "Dropdown"
@ -2543,6 +2600,7 @@ msgstr "Offenlegung der elektronischen Unterschrift"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/general/document-signing/document-signing-email-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/direct-template/direct-template-configure-form.tsx
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/profile.tsx
@ -2553,6 +2611,7 @@ msgstr "Offenlegung der elektronischen Unterschrift"
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-add-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-fields.tsx
@ -2646,15 +2705,6 @@ msgstr "Direktlink-Signierung aktivieren"
msgid "Enable signing order"
msgstr "Aktiviere die Signaturreihenfolge"
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Enable Typed Signature"
msgstr "Getippte Unterschrift aktivieren"
#: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/document-flow/add-fields.tsx
msgid "Enable Typed Signatures"
msgstr "Aktivieren Sie getippte Unterschriften"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/webhooks._index.tsx
@ -2929,7 +2979,7 @@ msgstr "Zum Eigentümer gehen"
msgid "Go to your <0>public profile settings</0> to add documents."
msgstr "Gehen Sie zu Ihren <0>öffentlichen Profileinstellungen</0>, um Dokumente hinzuzufügen."
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Green"
msgstr "Grün"
@ -3483,10 +3533,12 @@ msgstr "Meine Vorlagen"
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/general/claim-account.tsx
#: apps/remix/app/components/general/document-signing/document-signing-name-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-add-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-fields.tsx
@ -3537,6 +3589,7 @@ msgstr "Neue Vorlage"
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: packages/ui/primitives/signature-pad/signature-pad-dialog.tsx
msgid "Next"
msgstr "Nächster"
@ -3954,6 +4007,7 @@ msgstr "Bitte geben Sie einen aussagekräftigen Namen für Ihr Token ein. Dies w
#: apps/remix/app/components/general/claim-account.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/forms/profile.tsx
msgid "Please enter a valid name."
msgstr "Bitte geben Sie einen gültigen Namen ein."
@ -3989,10 +4043,6 @@ msgstr "Bitte beachten Sie, dass Sie den Zugriff auf alle Dokumente, die mit die
msgid "Please note that you will lose access to all documents associated with this team & all the members will be removed and notified"
msgstr "Bitte beachten Sie, dass Sie den Zugriff auf alle mit diesem Team verbundenen Dokumente verlieren werden und alle Mitglieder entfernt und benachrichtigt werden."
#: apps/remix/app/components/general/document-signing/document-signing-reject-dialog.tsx
msgid "Please provide a reason"
msgstr "Bitte geben Sie einen Grund an"
#: apps/remix/app/components/forms/2fa/disable-authenticator-app-dialog.tsx
msgid "Please provide a token from the authenticator, or a backup code. If you do not have a backup code available, please contact support."
msgstr "Bitte geben Sie ein Token von der Authentifizierungs-App oder einen Backup-Code an. Wenn Sie keinen Backup-Code haben, kontaktieren Sie bitte den Support."
@ -4063,6 +4113,10 @@ msgstr "Privat"
msgid "Private templates can only be modified and viewed by you."
msgstr "Private Vorlagen können nur von Ihnen bearbeitet und angezeigt werden."
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Proceed"
msgstr ""
#: apps/remix/app/routes/_authenticated+/settings+/profile.tsx
#: apps/remix/app/components/general/settings-nav-mobile.tsx
#: apps/remix/app/components/general/settings-nav-desktop.tsx
@ -4140,6 +4194,10 @@ msgstr "Bereit"
msgid "Reason"
msgstr "Grund"
#: packages/email/template-components/template-document-cancel.tsx
msgid "Reason for cancellation: {cancellationReason}"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Reason for rejection: "
msgstr ""
@ -4222,7 +4280,7 @@ msgstr "Wiederherstellungscode kopiert"
msgid "Recovery codes"
msgstr "Wiederherstellungscodes"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Red"
msgstr "Rot"
@ -4243,8 +4301,11 @@ msgstr "Registrierung erfolgreich"
msgid "Reject Document"
msgstr "Dokument Ablehnen"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/components/general/stack-avatars-with-tooltip.tsx
#: apps/remix/app/components/general/document/document-status.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: packages/lib/constants/document.ts
msgid "Rejected"
msgstr "Abgelehnt"
@ -4429,8 +4490,6 @@ msgstr "Zeilen pro Seite"
#: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-number-field.tsx
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
#: apps/remix/app/components/forms/team-branding-preferences-form.tsx
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Save"
@ -4740,7 +4799,6 @@ msgstr "Registrieren mit OIDC"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx
#: apps/remix/app/components/forms/profile.tsx
@ -4757,12 +4815,17 @@ msgstr "Unterschrift"
msgid "Signature ID"
msgstr "Signatur-ID"
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
msgid "Signature is too small. Please provide a more complete signature."
msgstr "Die Unterschrift ist zu klein. Bitte geben Sie eine vollständigere Unterschrift an."
#: packages/ui/primitives/signature-pad/signature-pad-draw.tsx
msgid "Signature is too small"
msgstr ""
#: apps/remix/app/components/forms/profile.tsx
msgid "Signature Pad cannot be empty."
msgstr ""
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "Signature types"
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Signatures Collected"
@ -4987,6 +5050,7 @@ msgid "Subject <0>(Optional)</0>"
msgstr "Betreff <0>(Optional)</0>"
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Submitting..."
msgstr ""
@ -5501,6 +5565,10 @@ msgstr "Der Token, den Sie zur Zurücksetzung Ihres Passworts verwendet haben, i
msgid "The two-factor authentication code provided is incorrect"
msgstr ""
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "The types of signatures that recipients are allowed to use when signing the document."
msgstr ""
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/webhooks.$id.tsx
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
@ -5582,6 +5650,10 @@ msgstr "Dieses Dokument wurde vom Eigentümer storniert und steht anderen nicht
msgid "This document has been cancelled by the owner."
msgstr "Dieses Dokument wurde vom Eigentümer storniert."
#: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx
msgid "This document has been rejected by a recipient"
msgstr ""
#: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx
msgid "This document has been signed by all recipients"
msgstr "Dieses Dokument wurde von allen Empfängern unterschrieben"
@ -5730,6 +5802,10 @@ msgstr "Zeitzone"
msgid "Title"
msgstr "Titel"
#: packages/ui/primitives/document-flow/add-settings.types.ts
msgid "Title cannot be empty"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx
msgid "To accept this invitation you must create an account."
msgstr "Um diese Einladung anzunehmen, müssen Sie ein Konto erstellen."
@ -5897,6 +5973,7 @@ msgstr "Zwei-Faktor-Wiederauthentifizierung"
#: apps/remix/app/components/tables/templates-table.tsx
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: packages/lib/constants/document.ts
msgid "Type"
msgstr "Typ"
@ -6004,6 +6081,7 @@ msgstr "Unvollendet"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.billing.tsx
msgid "Unknown"
msgstr "Unbekannt"
@ -6014,11 +6092,14 @@ msgstr "Unbezahlt"
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
#: apps/remix/app/components/forms/team-branding-preferences-form.tsx
#: apps/remix/app/components/forms/public-profile-form.tsx
#: apps/remix/app/components/dialogs/team-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Update"
msgstr "Aktualisieren"
@ -6039,6 +6120,8 @@ msgid "Update profile"
msgstr "Profil aktualisieren"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Update Recipient"
msgstr "Empfänger aktualisieren"
@ -6077,10 +6160,6 @@ msgstr "Webhook aktualisieren"
msgid "Updating password..."
msgstr "Passwort wird aktualisiert..."
#: apps/remix/app/components/forms/profile.tsx
msgid "Updating profile..."
msgstr "Profil wird aktualisiert..."
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Updating Your Information"
msgstr "Aktualisierung Ihrer Informationen"
@ -6089,6 +6168,10 @@ msgstr "Aktualisierung Ihrer Informationen"
msgid "Upgrade"
msgstr "Upgrade"
#: packages/lib/constants/document.ts
msgid "Upload"
msgstr ""
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
msgid "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
msgstr "Laden Sie eine CSV-Datei hoch, um mehrere Dokumente aus dieser Vorlage zu erstellen. Jede Zeile repräsentiert ein Dokument mit den Empfängerdaten."
@ -6113,7 +6196,7 @@ msgstr "CSV hochladen"
msgid "Upload custom document"
msgstr "Benutzerdefiniertes Dokument hochladen"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-upload.tsx
msgid "Upload Signature"
msgstr "Signatur hochladen"
@ -6287,6 +6370,7 @@ msgstr "Dokument anzeigen"
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/email/template-components/template-document-rejected.tsx
#: packages/email/template-components/template-document-invite.tsx
msgid "View Document"
@ -6653,6 +6737,11 @@ msgstr "Willkommen bei Documenso!"
msgid "Were you trying to edit this document instead?"
msgstr "Hast du stattdessen versucht, dieses Dokument zu bearbeiten?"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "When enabled, signers can choose who should sign next in the sequence instead of following the predefined order."
msgstr ""
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
msgid "When you click continue, you will be prompted to add the first available authenticator on your system."
msgstr "Wenn Sie auf Fortfahren klicken, werden Sie aufgefordert, den ersten verfügbaren Authenticator auf Ihrem System hinzuzufügen."

View File

@ -414,6 +414,10 @@ msgstr "<0>{teamName}</0> has requested to use your email address for their team
msgid "<0>Click to upload</0> or drag and drop"
msgstr "<0>Click to upload</0> or drag and drop"
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "<0>Drawn</0> - A signature that is drawn using a mouse or stylus."
msgstr "<0>Drawn</0> - A signature that is drawn using a mouse or stylus."
#: packages/ui/primitives/template-flow/add-template-settings.tsx
msgid "<0>Email</0> - The recipient will be emailed the document to sign, approve, etc."
msgstr "<0>Email</0> - The recipient will be emailed the document to sign, approve, etc."
@ -460,6 +464,14 @@ msgstr "<0>Require passkey</0> - The recipient must have an account and passkey
msgid "<0>Sender:</0> All"
msgstr "<0>Sender:</0> All"
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "<0>Typed</0> - A signature that is typed using a keyboard."
msgstr "<0>Typed</0> - A signature that is typed using a keyboard."
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "<0>Uploaded</0> - A signature that is uploaded from a file."
msgstr "<0>Uploaded</0> - A signature that is uploaded from a file."
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "<0>You are about to complete approving <1>\"{documentTitle}\"</1>.</0><2/> Are you sure?"
msgstr "<0>You are about to complete approving <1>\"{documentTitle}\"</1>.</0><2/> Are you sure?"
@ -893,6 +905,16 @@ msgstr "All Time"
msgid "Allow document recipients to reply directly to this email address"
msgstr "Allow document recipients to reply directly to this email address"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "Allow signers to dictate next signer"
msgstr "Allow signers to dictate next signer"
#: packages/ui/primitives/template-flow/add-template-settings.tsx
#: packages/ui/primitives/document-flow/add-settings.tsx
msgid "Allowed Signature Types"
msgstr "Allowed Signature Types"
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Allows authenticating using biometrics, password managers, hardware keys, etc."
msgstr "Allows authenticating using biometrics, password managers, hardware keys, etc."
@ -1232,6 +1254,13 @@ msgstr "Assisted"
msgid "Assisting"
msgstr "Assisting"
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
#: packages/ui/primitives/template-flow/add-template-settings.types.tsx
#: packages/ui/primitives/document-flow/add-settings.types.ts
#: packages/lib/types/document-meta.ts
msgid "At least one signature type must be enabled"
msgstr "At least one signature type must be enabled"
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
msgid "Attempts sealing the document again, useful for after a code change has occurred to resolve an erroneous document."
msgstr "Attempts sealing the document again, useful for after a code change has occurred to resolve an erroneous document."
@ -1304,11 +1333,11 @@ msgstr "Before you get started, please confirm your email address by clicking th
msgid "Billing"
msgstr "Billing"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Black"
msgstr "Black"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Blue"
msgstr "Blue"
@ -1379,6 +1408,10 @@ msgstr "By proceeding to use the electronic signature service provided by Docume
msgid "By proceeding with your electronic signature, you acknowledge and consent that it will be used to sign the given document and holds the same legal validity as a handwritten signature. By completing the electronic signing process, you affirm your understanding and acceptance of these conditions."
msgstr "By proceeding with your electronic signature, you acknowledge and consent that it will be used to sign the given document and holds the same legal validity as a handwritten signature. By completing the electronic signing process, you affirm your understanding and acceptance of these conditions."
#: apps/remix/app/components/forms/signup.tsx
msgid "By proceeding, you agree to our <0>Terms of Service</0> and <1>Privacy Policy</1>."
msgstr "By proceeding, you agree to our <0>Terms of Service</0> and <1>Privacy Policy</1>."
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "By using the electronic signature feature, you are consenting to conduct transactions and receive disclosures electronically. You acknowledge that your electronic signature on documents is binding and that you accept the terms outlined in the documents you are signing."
msgstr "By using the electronic signature feature, you are consenting to conduct transactions and receive disclosures electronically. You acknowledge that your electronic signature on documents is binding and that you accept the terms outlined in the documents you are signing."
@ -1431,6 +1464,8 @@ msgstr "Can prepare"
#: apps/remix/app/components/dialogs/document-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/signature-pad/signature-pad-dialog.tsx
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Cancel"
@ -1517,7 +1552,7 @@ msgstr "Clear file"
msgid "Clear filters"
msgstr "Clear filters"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-draw.tsx
msgid "Clear Signature"
msgstr "Clear Signature"
@ -1692,6 +1727,7 @@ msgstr "Content"
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/document-flow/document-flow-root.tsx
msgid "Continue"
msgstr "Continue"
@ -1732,14 +1768,18 @@ msgstr "Controls the default visibility of an uploaded document."
msgid "Controls the formatting of the message that will be sent when inviting a recipient to sign a document. If a custom message has been provided while configuring the document, it will be used instead."
msgstr "Controls the formatting of the message that will be sent when inviting a recipient to sign a document. If a custom message has been provided while configuring the document, it will be used instead."
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Controls whether the recipients can sign the documents using a typed signature. Enable or disable the typed signature globally."
msgstr "Controls whether the recipients can sign the documents using a typed signature. Enable or disable the typed signature globally."
#: packages/ui/primitives/document-flow/add-settings.tsx
msgid "Controls the language for the document, including the language to be used for email notifications, and the final certificate that is generated and attached to the document."
msgstr "Controls the language for the document, including the language to be used for email notifications, and the final certificate that is generated and attached to the document."
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "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."
msgstr "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."
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Controls which signatures are allowed to be used when signing a document."
msgstr "Controls which signatures are allowed to be used when signing a document."
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Copied"
@ -1970,6 +2010,10 @@ msgstr "Default Document Language"
msgid "Default Document Visibility"
msgstr "Default Document Visibility"
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Default Signature Settings"
msgstr "Default Signature Settings"
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
msgid "delete"
msgstr "delete"
@ -2196,6 +2240,11 @@ msgstr "Document \"{0}\" - Rejected by {1}"
msgid "Document \"{0}\" - Rejection Confirmed"
msgstr "Document \"{0}\" - Rejection Confirmed"
#. placeholder {0}: document.title
#: packages/lib/jobs/definitions/emails/send-document-cancelled-emails.handler.ts
msgid "Document \"{0}\" Cancelled"
msgstr "Document \"{0}\" Cancelled"
#: packages/ui/primitives/template-flow/add-template-settings.tsx
#: packages/ui/primitives/document-flow/add-settings.tsx
#: packages/ui/components/document/document-global-auth-access-select.tsx
@ -2344,6 +2393,10 @@ msgstr "Document preferences updated"
msgid "Document re-sent"
msgstr "Document re-sent"
#: apps/remix/app/components/general/document/document-status.tsx
msgid "Document rejected"
msgstr "Document rejected"
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/components/embed/embed-document-rejected.tsx
#: packages/email/template-components/template-document-rejected.tsx
@ -2481,6 +2534,10 @@ msgstr "Drafted Documents"
msgid "Drag & drop your PDF here."
msgstr "Drag & drop your PDF here."
#: packages/lib/constants/document.ts
msgid "Draw"
msgstr "Draw"
#: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/document-flow/add-fields.tsx
msgid "Dropdown"
@ -2538,6 +2595,7 @@ msgstr "Electronic Signature Disclosure"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/general/document-signing/document-signing-email-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/direct-template/direct-template-configure-form.tsx
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/profile.tsx
@ -2548,6 +2606,7 @@ msgstr "Electronic Signature Disclosure"
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-add-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-fields.tsx
@ -2641,15 +2700,6 @@ msgstr "Enable Direct Link Signing"
msgid "Enable signing order"
msgstr "Enable signing order"
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Enable Typed Signature"
msgstr "Enable Typed Signature"
#: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/document-flow/add-fields.tsx
msgid "Enable Typed Signatures"
msgstr "Enable Typed Signatures"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/webhooks._index.tsx
@ -2924,7 +2974,7 @@ msgstr "Go to owner"
msgid "Go to your <0>public profile settings</0> to add documents."
msgstr "Go to your <0>public profile settings</0> to add documents."
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Green"
msgstr "Green"
@ -3478,10 +3528,12 @@ msgstr "My templates"
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/general/claim-account.tsx
#: apps/remix/app/components/general/document-signing/document-signing-name-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-add-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-fields.tsx
@ -3532,6 +3584,7 @@ msgstr "New Template"
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: packages/ui/primitives/signature-pad/signature-pad-dialog.tsx
msgid "Next"
msgstr "Next"
@ -3949,6 +4002,7 @@ msgstr "Please enter a meaningful name for your token. This will help you identi
#: apps/remix/app/components/general/claim-account.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/forms/profile.tsx
msgid "Please enter a valid name."
msgstr "Please enter a valid name."
@ -3984,10 +4038,6 @@ msgstr "Please note that this action is irreversible. Once confirmed, your webho
msgid "Please note that you will lose access to all documents associated with this team & all the members will be removed and notified"
msgstr "Please note that you will lose access to all documents associated with this team & all the members will be removed and notified"
#: apps/remix/app/components/general/document-signing/document-signing-reject-dialog.tsx
msgid "Please provide a reason"
msgstr "Please provide a reason"
#: apps/remix/app/components/forms/2fa/disable-authenticator-app-dialog.tsx
msgid "Please provide a token from the authenticator, or a backup code. If you do not have a backup code available, please contact support."
msgstr "Please provide a token from the authenticator, or a backup code. If you do not have a backup code available, please contact support."
@ -4058,6 +4108,10 @@ msgstr "Private"
msgid "Private templates can only be modified and viewed by you."
msgstr "Private templates can only be modified and viewed by you."
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Proceed"
msgstr "Proceed"
#: apps/remix/app/routes/_authenticated+/settings+/profile.tsx
#: apps/remix/app/components/general/settings-nav-mobile.tsx
#: apps/remix/app/components/general/settings-nav-desktop.tsx
@ -4135,6 +4189,10 @@ msgstr "Ready"
msgid "Reason"
msgstr "Reason"
#: packages/email/template-components/template-document-cancel.tsx
msgid "Reason for cancellation: {cancellationReason}"
msgstr "Reason for cancellation: {cancellationReason}"
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Reason for rejection: "
msgstr "Reason for rejection: "
@ -4217,7 +4275,7 @@ msgstr "Recovery code copied"
msgid "Recovery codes"
msgstr "Recovery codes"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Red"
msgstr "Red"
@ -4238,8 +4296,11 @@ msgstr "Registration Successful"
msgid "Reject Document"
msgstr "Reject Document"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/components/general/stack-avatars-with-tooltip.tsx
#: apps/remix/app/components/general/document/document-status.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: packages/lib/constants/document.ts
msgid "Rejected"
msgstr "Rejected"
@ -4424,8 +4485,6 @@ msgstr "Rows per page"
#: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-number-field.tsx
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
#: apps/remix/app/components/forms/team-branding-preferences-form.tsx
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Save"
@ -4735,7 +4794,6 @@ msgstr "Sign Up with OIDC"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx
#: apps/remix/app/components/forms/profile.tsx
@ -4752,12 +4810,17 @@ msgstr "Signature"
msgid "Signature ID"
msgstr "Signature ID"
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
msgid "Signature is too small. Please provide a more complete signature."
msgstr "Signature is too small. Please provide a more complete signature."
#: packages/ui/primitives/signature-pad/signature-pad-draw.tsx
msgid "Signature is too small"
msgstr "Signature is too small"
#: apps/remix/app/components/forms/profile.tsx
msgid "Signature Pad cannot be empty."
msgstr "Signature Pad cannot be empty."
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "Signature types"
msgstr "Signature types"
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Signatures Collected"
@ -4982,6 +5045,7 @@ msgid "Subject <0>(Optional)</0>"
msgstr "Subject <0>(Optional)</0>"
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Submitting..."
msgstr "Submitting..."
@ -5498,6 +5562,10 @@ msgstr "The token you have used to reset your password is either expired or it n
msgid "The two-factor authentication code provided is incorrect"
msgstr "The two-factor authentication code provided is incorrect"
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "The types of signatures that recipients are allowed to use when signing the document."
msgstr "The types of signatures that recipients are allowed to use when signing the document."
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/webhooks.$id.tsx
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
@ -5579,6 +5647,10 @@ msgstr "This document has been cancelled by the owner and is no longer available
msgid "This document has been cancelled by the owner."
msgstr "This document has been cancelled by the owner."
#: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx
msgid "This document has been rejected by a recipient"
msgstr "This document has been rejected by a recipient"
#: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx
msgid "This document has been signed by all recipients"
msgstr "This document has been signed by all recipients"
@ -5727,6 +5799,10 @@ msgstr "Time Zone"
msgid "Title"
msgstr "Title"
#: packages/ui/primitives/document-flow/add-settings.types.ts
msgid "Title cannot be empty"
msgstr "Title cannot be empty"
#: apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx
msgid "To accept this invitation you must create an account."
msgstr "To accept this invitation you must create an account."
@ -5894,6 +5970,7 @@ msgstr "Two-Factor Re-Authentication"
#: apps/remix/app/components/tables/templates-table.tsx
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: packages/lib/constants/document.ts
msgid "Type"
msgstr "Type"
@ -6001,6 +6078,7 @@ msgstr "Uncompleted"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.billing.tsx
msgid "Unknown"
msgstr "Unknown"
@ -6011,11 +6089,14 @@ msgstr "Unpaid"
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
#: apps/remix/app/components/forms/team-branding-preferences-form.tsx
#: apps/remix/app/components/forms/public-profile-form.tsx
#: apps/remix/app/components/dialogs/team-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Update"
msgstr "Update"
@ -6036,6 +6117,8 @@ msgid "Update profile"
msgstr "Update profile"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Update Recipient"
msgstr "Update Recipient"
@ -6074,10 +6157,6 @@ msgstr "Update webhook"
msgid "Updating password..."
msgstr "Updating password..."
#: apps/remix/app/components/forms/profile.tsx
msgid "Updating profile..."
msgstr "Updating profile..."
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Updating Your Information"
msgstr "Updating Your Information"
@ -6086,6 +6165,10 @@ msgstr "Updating Your Information"
msgid "Upgrade"
msgstr "Upgrade"
#: packages/lib/constants/document.ts
msgid "Upload"
msgstr "Upload"
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
msgid "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
msgstr "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
@ -6110,7 +6193,7 @@ msgstr "Upload CSV"
msgid "Upload custom document"
msgstr "Upload custom document"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-upload.tsx
msgid "Upload Signature"
msgstr "Upload Signature"
@ -6284,6 +6367,7 @@ msgstr "View document"
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/email/template-components/template-document-rejected.tsx
#: packages/email/template-components/template-document-invite.tsx
msgid "View Document"
@ -6650,6 +6734,11 @@ msgstr "Welcome to Documenso!"
msgid "Were you trying to edit this document instead?"
msgstr "Were you trying to edit this document instead?"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "When enabled, signers can choose who should sign next in the sequence instead of following the predefined order."
msgstr "When enabled, signers can choose who should sign next in the sequence instead of following the predefined order."
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
msgid "When you click continue, you will be prompted to add the first available authenticator on your system."
msgstr "When you click continue, you will be prompted to add the first available authenticator on your system."

View File

@ -419,6 +419,10 @@ msgstr "<0>{teamName}</0> ha solicitado usar tu dirección de correo electrónic
msgid "<0>Click to upload</0> or drag and drop"
msgstr "<0>Haga clic para subir</0> o arrastre y suelte"
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "<0>Drawn</0> - A signature that is drawn using a mouse or stylus."
msgstr ""
#: packages/ui/primitives/template-flow/add-template-settings.tsx
msgid "<0>Email</0> - The recipient will be emailed the document to sign, approve, etc."
msgstr "<0>Correo electrónico</0> - Al destinatario se le enviará el documento para firmar, aprobar, etc."
@ -465,6 +469,14 @@ msgstr "<0>Requerir clave de acceso</0> - El destinatario debe tener una cuenta
msgid "<0>Sender:</0> All"
msgstr "<0>Remitente:</0> Todos"
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "<0>Typed</0> - A signature that is typed using a keyboard."
msgstr ""
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "<0>Uploaded</0> - A signature that is uploaded from a file."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "<0>You are about to complete approving <1>\"{documentTitle}\"</1>.</0><2/> Are you sure?"
msgstr "<0>Está a punto de completar la aprobación de <1>\"{documentTitle}\"</1>.</0><2/> ¿Está seguro?"
@ -898,6 +910,16 @@ msgstr "Todo el Tiempo"
msgid "Allow document recipients to reply directly to this email address"
msgstr "Permitir que los destinatarios del documento respondan directamente a esta dirección de correo electrónico"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "Allow signers to dictate next signer"
msgstr ""
#: packages/ui/primitives/template-flow/add-template-settings.tsx
#: packages/ui/primitives/document-flow/add-settings.tsx
msgid "Allowed Signature Types"
msgstr ""
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Allows authenticating using biometrics, password managers, hardware keys, etc."
msgstr "Permite autenticarse usando biometría, administradores de contraseñas, claves de hardware, etc."
@ -1237,6 +1259,13 @@ msgstr ""
msgid "Assisting"
msgstr ""
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
#: packages/ui/primitives/template-flow/add-template-settings.types.tsx
#: packages/ui/primitives/document-flow/add-settings.types.ts
#: packages/lib/types/document-meta.ts
msgid "At least one signature type must be enabled"
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
msgid "Attempts sealing the document again, useful for after a code change has occurred to resolve an erroneous document."
msgstr "Intenta sellar el documento de nuevo, útil después de que se haya producido un cambio de código para resolver un documento erróneo."
@ -1309,11 +1338,11 @@ msgstr "Antes de comenzar, por favor confirma tu dirección de correo electróni
msgid "Billing"
msgstr "Facturación"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Black"
msgstr "Negro"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Blue"
msgstr "Azul"
@ -1384,6 +1413,10 @@ msgstr "Al continuar utilizando el servicio de firma electrónica proporcionado
msgid "By proceeding with your electronic signature, you acknowledge and consent that it will be used to sign the given document and holds the same legal validity as a handwritten signature. By completing the electronic signing process, you affirm your understanding and acceptance of these conditions."
msgstr "Al continuar con su firma electrónica, usted reconoce y consiente que se utilizará para firmar el documento dado y tiene la misma validez legal que una firma manuscrita. Al completar el proceso de firma electrónica, usted afirma su comprensión y aceptación de estas condiciones."
#: apps/remix/app/components/forms/signup.tsx
msgid "By proceeding, you agree to our <0>Terms of Service</0> and <1>Privacy Policy</1>."
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "By using the electronic signature feature, you are consenting to conduct transactions and receive disclosures electronically. You acknowledge that your electronic signature on documents is binding and that you accept the terms outlined in the documents you are signing."
msgstr "Al utilizar la función de firma electrónica, usted está consintiendo realizar transacciones y recibir divulgaciones electrónicamente. Reconoce que su firma electrónica en los documentos es vinculante y que acepta los términos esbozados en los documentos que está firmando."
@ -1436,6 +1469,8 @@ msgstr ""
#: apps/remix/app/components/dialogs/document-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/signature-pad/signature-pad-dialog.tsx
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Cancel"
@ -1522,7 +1557,7 @@ msgstr "Limpiar archivo"
msgid "Clear filters"
msgstr "Limpiar filtros"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-draw.tsx
msgid "Clear Signature"
msgstr "Limpiar firma"
@ -1697,6 +1732,7 @@ msgstr "Contenido"
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/document-flow/document-flow-root.tsx
msgid "Continue"
msgstr "Continuar"
@ -1737,14 +1773,18 @@ msgstr "Controla la visibilidad predeterminada de un documento cargado."
msgid "Controls the formatting of the message that will be sent when inviting a recipient to sign a document. If a custom message has been provided while configuring the document, it will be used instead."
msgstr "Controla el formato del mensaje que se enviará al invitar a un destinatario a firmar un documento. Si se ha proporcionado un mensaje personalizado al configurar el documento, se usará en su lugar."
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Controls whether the recipients can sign the documents using a typed signature. Enable or disable the typed signature globally."
msgstr "Controla si los destinatarios pueden firmar los documentos utilizando una firma mecanografiada. Habilitar o deshabilitar la firma mecanografiada globalmente."
#: packages/ui/primitives/document-flow/add-settings.tsx
msgid "Controls the language for the document, including the language to be used for email notifications, and the final certificate that is generated and attached to the document."
msgstr ""
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "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."
msgstr "Controla si el certificado de firma se incluirá en el documento cuando se descargue. El certificado de firma aún puede descargarse por separado desde la página de registros."
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Controls which signatures are allowed to be used when signing a document."
msgstr ""
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Copied"
@ -1975,6 +2015,10 @@ msgstr "Idioma predeterminado del documento"
msgid "Default Document Visibility"
msgstr "Visibilidad predeterminada del documento"
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Default Signature Settings"
msgstr ""
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
msgid "delete"
msgstr "eliminar"
@ -2201,6 +2245,11 @@ msgstr "Documento \"{0}\" - Rechazado por {1}"
msgid "Document \"{0}\" - Rejection Confirmed"
msgstr "Documento \"{0}\" - Rechazo confirmado"
#. placeholder {0}: document.title
#: packages/lib/jobs/definitions/emails/send-document-cancelled-emails.handler.ts
msgid "Document \"{0}\" Cancelled"
msgstr ""
#: packages/ui/primitives/template-flow/add-template-settings.tsx
#: packages/ui/primitives/document-flow/add-settings.tsx
#: packages/ui/components/document/document-global-auth-access-select.tsx
@ -2349,6 +2398,10 @@ msgstr "Preferencias del documento actualizadas"
msgid "Document re-sent"
msgstr "Documento reenviado"
#: apps/remix/app/components/general/document/document-status.tsx
msgid "Document rejected"
msgstr ""
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/components/embed/embed-document-rejected.tsx
#: packages/email/template-components/template-document-rejected.tsx
@ -2486,6 +2539,10 @@ msgstr "Documentos redactados"
msgid "Drag & drop your PDF here."
msgstr "Arrastre y suelte su PDF aquí."
#: packages/lib/constants/document.ts
msgid "Draw"
msgstr ""
#: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/document-flow/add-fields.tsx
msgid "Dropdown"
@ -2543,6 +2600,7 @@ msgstr "Divulgación de Firma Electrónica"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/general/document-signing/document-signing-email-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/direct-template/direct-template-configure-form.tsx
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/profile.tsx
@ -2553,6 +2611,7 @@ msgstr "Divulgación de Firma Electrónica"
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-add-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-fields.tsx
@ -2646,15 +2705,6 @@ msgstr "Habilitar firma de enlace directo"
msgid "Enable signing order"
msgstr "Habilitar orden de firma"
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Enable Typed Signature"
msgstr "Habilitar firma mecanografiada"
#: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/document-flow/add-fields.tsx
msgid "Enable Typed Signatures"
msgstr "Habilitar firmas escritas"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/webhooks._index.tsx
@ -2929,7 +2979,7 @@ msgstr "Ir al propietario"
msgid "Go to your <0>public profile settings</0> to add documents."
msgstr "Ve a tu <0>configuración de perfil público</0> para agregar documentos."
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Green"
msgstr "Verde"
@ -3483,10 +3533,12 @@ msgstr "Mis plantillas"
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/general/claim-account.tsx
#: apps/remix/app/components/general/document-signing/document-signing-name-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-add-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-fields.tsx
@ -3537,6 +3589,7 @@ msgstr "Nueva plantilla"
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: packages/ui/primitives/signature-pad/signature-pad-dialog.tsx
msgid "Next"
msgstr "Siguiente"
@ -3954,6 +4007,7 @@ msgstr "Por favor, ingresa un nombre significativo para tu token. Esto te ayudar
#: apps/remix/app/components/general/claim-account.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/forms/profile.tsx
msgid "Please enter a valid name."
msgstr "Por favor, introduce un nombre válido."
@ -3989,10 +4043,6 @@ msgstr "Por favor, ten en cuenta que esta acción es irreversible. Una vez confi
msgid "Please note that you will lose access to all documents associated with this team & all the members will be removed and notified"
msgstr "Por favor, ten en cuenta que perderás acceso a todos los documentos asociados con este equipo y todos los miembros serán eliminados y notificados"
#: apps/remix/app/components/general/document-signing/document-signing-reject-dialog.tsx
msgid "Please provide a reason"
msgstr "Please provide a reason"
#: apps/remix/app/components/forms/2fa/disable-authenticator-app-dialog.tsx
msgid "Please provide a token from the authenticator, or a backup code. If you do not have a backup code available, please contact support."
msgstr "Por favor, proporciona un token del autenticador o un código de respaldo. Si no tienes un código de respaldo disponible, contacta al soporte."
@ -4063,6 +4113,10 @@ msgstr "Privado"
msgid "Private templates can only be modified and viewed by you."
msgstr "Las plantillas privadas solo pueden ser modificadas y vistas por ti."
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Proceed"
msgstr ""
#: apps/remix/app/routes/_authenticated+/settings+/profile.tsx
#: apps/remix/app/components/general/settings-nav-mobile.tsx
#: apps/remix/app/components/general/settings-nav-desktop.tsx
@ -4140,6 +4194,10 @@ msgstr "Listo"
msgid "Reason"
msgstr "Razón"
#: packages/email/template-components/template-document-cancel.tsx
msgid "Reason for cancellation: {cancellationReason}"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Reason for rejection: "
msgstr ""
@ -4222,7 +4280,7 @@ msgstr "Código de recuperación copiado"
msgid "Recovery codes"
msgstr "Códigos de recuperación"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Red"
msgstr "Rojo"
@ -4243,8 +4301,11 @@ msgstr "Registro exitoso"
msgid "Reject Document"
msgstr "Rechazar Documento"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/components/general/stack-avatars-with-tooltip.tsx
#: apps/remix/app/components/general/document/document-status.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: packages/lib/constants/document.ts
msgid "Rejected"
msgstr "Rejected"
@ -4429,8 +4490,6 @@ msgstr "Filas por página"
#: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-number-field.tsx
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
#: apps/remix/app/components/forms/team-branding-preferences-form.tsx
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Save"
@ -4740,7 +4799,6 @@ msgstr "Regístrate con OIDC"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx
#: apps/remix/app/components/forms/profile.tsx
@ -4757,12 +4815,17 @@ msgstr "Firma"
msgid "Signature ID"
msgstr "ID de Firma"
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
msgid "Signature is too small. Please provide a more complete signature."
msgstr "La firma es demasiado pequeña. Proporcione una firma más completa."
#: packages/ui/primitives/signature-pad/signature-pad-draw.tsx
msgid "Signature is too small"
msgstr ""
#: apps/remix/app/components/forms/profile.tsx
msgid "Signature Pad cannot be empty."
msgstr ""
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "Signature types"
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Signatures Collected"
@ -4987,6 +5050,7 @@ msgid "Subject <0>(Optional)</0>"
msgstr "Asunto <0>(Opcional)</0>"
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Submitting..."
msgstr ""
@ -5501,6 +5565,10 @@ msgstr "El token que has utilizado para restablecer tu contraseña ha expirado o
msgid "The two-factor authentication code provided is incorrect"
msgstr ""
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "The types of signatures that recipients are allowed to use when signing the document."
msgstr ""
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/webhooks.$id.tsx
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
@ -5582,6 +5650,10 @@ msgstr "Este documento ha sido cancelado por el propietario y ya no está dispon
msgid "This document has been cancelled by the owner."
msgstr "Este documento ha sido cancelado por el propietario."
#: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx
msgid "This document has been rejected by a recipient"
msgstr ""
#: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx
msgid "This document has been signed by all recipients"
msgstr "Este documento ha sido firmado por todos los destinatarios"
@ -5730,6 +5802,10 @@ msgstr "Zona horaria"
msgid "Title"
msgstr "Título"
#: packages/ui/primitives/document-flow/add-settings.types.ts
msgid "Title cannot be empty"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx
msgid "To accept this invitation you must create an account."
msgstr "Para aceptar esta invitación debes crear una cuenta."
@ -5897,6 +5973,7 @@ msgstr "Re-autenticación de Doble Factor"
#: apps/remix/app/components/tables/templates-table.tsx
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: packages/lib/constants/document.ts
msgid "Type"
msgstr "Tipo"
@ -6004,6 +6081,7 @@ msgstr "Incompleto"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.billing.tsx
msgid "Unknown"
msgstr "Desconocido"
@ -6014,11 +6092,14 @@ msgstr "No pagado"
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
#: apps/remix/app/components/forms/team-branding-preferences-form.tsx
#: apps/remix/app/components/forms/public-profile-form.tsx
#: apps/remix/app/components/dialogs/team-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Update"
msgstr "Actualizar"
@ -6039,6 +6120,8 @@ msgid "Update profile"
msgstr "Actualizar perfil"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Update Recipient"
msgstr "Actualizar destinatario"
@ -6077,10 +6160,6 @@ msgstr "Actualizar webhook"
msgid "Updating password..."
msgstr "Actualizando contraseña..."
#: apps/remix/app/components/forms/profile.tsx
msgid "Updating profile..."
msgstr "Actualizando perfil..."
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Updating Your Information"
msgstr "Actualizando Su Información"
@ -6089,6 +6168,10 @@ msgstr "Actualizando Su Información"
msgid "Upgrade"
msgstr "Actualizar"
#: packages/lib/constants/document.ts
msgid "Upload"
msgstr ""
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
msgid "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
msgstr "Sube un archivo CSV para crear múltiples documentos a partir de esta plantilla. Cada fila representa un documento con los detalles del destinatario."
@ -6113,7 +6196,7 @@ msgstr "Subir CSV"
msgid "Upload custom document"
msgstr "Subir documento personalizado"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-upload.tsx
msgid "Upload Signature"
msgstr "Subir firma"
@ -6287,6 +6370,7 @@ msgstr "Ver documento"
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/email/template-components/template-document-rejected.tsx
#: packages/email/template-components/template-document-invite.tsx
msgid "View Document"
@ -6653,6 +6737,11 @@ msgstr "¡Bienvenido a Documenso!"
msgid "Were you trying to edit this document instead?"
msgstr "¿Estabas intentando editar este documento en su lugar?"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "When enabled, signers can choose who should sign next in the sequence instead of following the predefined order."
msgstr ""
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
msgid "When you click continue, you will be prompted to add the first available authenticator on your system."
msgstr "Cuando haces clic en continuar, se te pedirá que añadas el primer autenticador disponible en tu sistema."

View File

@ -419,6 +419,10 @@ msgstr "<0>{teamName}</0> a demandé à utiliser votre adresse e-mail pour leur
msgid "<0>Click to upload</0> or drag and drop"
msgstr "<0>Cliquez pour importer</0> ou faites glisser et déposez"
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "<0>Drawn</0> - A signature that is drawn using a mouse or stylus."
msgstr ""
#: packages/ui/primitives/template-flow/add-template-settings.tsx
msgid "<0>Email</0> - The recipient will be emailed the document to sign, approve, etc."
msgstr "<0>Email</0> - Le destinataire recevra le document par e-mail pour signer, approuver, etc."
@ -465,6 +469,14 @@ msgstr "<0>Clé d'accès requise</0> - Le destinataire doit avoir un compte et u
msgid "<0>Sender:</0> All"
msgstr "<0>Expéditeur :</0> Tous"
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "<0>Typed</0> - A signature that is typed using a keyboard."
msgstr ""
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "<0>Uploaded</0> - A signature that is uploaded from a file."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "<0>You are about to complete approving <1>\"{documentTitle}\"</1>.</0><2/> Are you sure?"
msgstr "<0>Vous êtes sur le point de terminer l'approbation de <1>\"{documentTitle}\"</1>.</0><2/> Êtes-vous sûr ?"
@ -898,6 +910,16 @@ msgstr "Depuis toujours"
msgid "Allow document recipients to reply directly to this email address"
msgstr "Autoriser les destinataires du document à répondre directement à cette adresse e-mail"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "Allow signers to dictate next signer"
msgstr ""
#: packages/ui/primitives/template-flow/add-template-settings.tsx
#: packages/ui/primitives/document-flow/add-settings.tsx
msgid "Allowed Signature Types"
msgstr ""
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Allows authenticating using biometrics, password managers, hardware keys, etc."
msgstr "Permet d'authentifier en utilisant des biométries, des gestionnaires de mots de passe, des clés matérielles, etc."
@ -1237,6 +1259,13 @@ msgstr ""
msgid "Assisting"
msgstr ""
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
#: packages/ui/primitives/template-flow/add-template-settings.types.tsx
#: packages/ui/primitives/document-flow/add-settings.types.ts
#: packages/lib/types/document-meta.ts
msgid "At least one signature type must be enabled"
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
msgid "Attempts sealing the document again, useful for after a code change has occurred to resolve an erroneous document."
msgstr "Essaye de sceller le document à nouveau, utile après qu'un changement de code ait eu lieu pour résoudre un document erroné."
@ -1309,11 +1338,11 @@ msgstr "Avant de commencer, veuillez confirmer votre adresse e-mail en cliquant
msgid "Billing"
msgstr "Facturation"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Black"
msgstr "Noir"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Blue"
msgstr "Bleu"
@ -1384,6 +1413,10 @@ msgstr "En procédant à l'utilisation du service de signature électronique fou
msgid "By proceeding with your electronic signature, you acknowledge and consent that it will be used to sign the given document and holds the same legal validity as a handwritten signature. By completing the electronic signing process, you affirm your understanding and acceptance of these conditions."
msgstr "En procédant avec votre signature électronique, vous reconnaissez et consentez à ce qu'elle soit utilisée pour signer le document donné et a la même validité légale qu'une signature manuscrite. En complétant le processus de signature électronique, vous affirmez votre compréhension et votre acceptation de ces conditions."
#: apps/remix/app/components/forms/signup.tsx
msgid "By proceeding, you agree to our <0>Terms of Service</0> and <1>Privacy Policy</1>."
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "By using the electronic signature feature, you are consenting to conduct transactions and receive disclosures electronically. You acknowledge that your electronic signature on documents is binding and that you accept the terms outlined in the documents you are signing."
msgstr "En utilisant la fonctionnalité de signature électronique, vous consentez à effectuer des transactions et à recevoir des divulgations électroniquement. Vous reconnaissez que votre signature électronique sur les documents est contraignante et que vous acceptez les termes énoncés dans les documents que vous signez."
@ -1436,6 +1469,8 @@ msgstr ""
#: apps/remix/app/components/dialogs/document-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/signature-pad/signature-pad-dialog.tsx
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Cancel"
@ -1522,7 +1557,7 @@ msgstr "Effacer le fichier"
msgid "Clear filters"
msgstr "Effacer les filtres"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-draw.tsx
msgid "Clear Signature"
msgstr "Effacer la signature"
@ -1697,6 +1732,7 @@ msgstr "Contenu"
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/document-flow/document-flow-root.tsx
msgid "Continue"
msgstr "Continuer"
@ -1737,14 +1773,18 @@ msgstr "Contrôle la visibilité par défaut d'un document importé."
msgid "Controls the formatting of the message that will be sent when inviting a recipient to sign a document. If a custom message has been provided while configuring the document, it will be used instead."
msgstr "Contrôle le formatage du message qui sera envoyé lors de l'invitation d'un destinataire à signer un document. Si un message personnalisé a été fourni lors de la configuration du document, il sera utilisé à la place."
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Controls whether the recipients can sign the documents using a typed signature. Enable or disable the typed signature globally."
msgstr "Contrôle si les destinataires peuvent signer les documents à l'aide d'une signature dactylographiée. Active ou désactive globalement la signature dactylographiée."
#: packages/ui/primitives/document-flow/add-settings.tsx
msgid "Controls the language for the document, including the language to be used for email notifications, and the final certificate that is generated and attached to the document."
msgstr ""
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "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."
msgstr "Contrôle si le certificat de signature sera inclus dans le document lorsqu'il sera téléchargé. Le certificat de signature peut toujours être téléchargé séparément à partir de la page d'historique'."
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Controls which signatures are allowed to be used when signing a document."
msgstr ""
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Copied"
@ -1975,6 +2015,10 @@ msgstr "Langue par défaut du document"
msgid "Default Document Visibility"
msgstr "Visibilité par défaut du document"
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Default Signature Settings"
msgstr ""
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
msgid "delete"
msgstr "supprimer"
@ -2201,6 +2245,11 @@ msgstr "Document \"{0}\" - Rejeté par {1}"
msgid "Document \"{0}\" - Rejection Confirmed"
msgstr "Document \"{0}\" - Rejet Confirmé"
#. placeholder {0}: document.title
#: packages/lib/jobs/definitions/emails/send-document-cancelled-emails.handler.ts
msgid "Document \"{0}\" Cancelled"
msgstr ""
#: packages/ui/primitives/template-flow/add-template-settings.tsx
#: packages/ui/primitives/document-flow/add-settings.tsx
#: packages/ui/components/document/document-global-auth-access-select.tsx
@ -2349,6 +2398,10 @@ msgstr "Préférences de document mises à jour"
msgid "Document re-sent"
msgstr "Document renvoyé"
#: apps/remix/app/components/general/document/document-status.tsx
msgid "Document rejected"
msgstr ""
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/components/embed/embed-document-rejected.tsx
#: packages/email/template-components/template-document-rejected.tsx
@ -2486,6 +2539,10 @@ msgstr "Documents brouillon"
msgid "Drag & drop your PDF here."
msgstr "Faites glisser et déposez votre PDF ici."
#: packages/lib/constants/document.ts
msgid "Draw"
msgstr ""
#: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/document-flow/add-fields.tsx
msgid "Dropdown"
@ -2543,6 +2600,7 @@ msgstr "Divulgation de signature électronique"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/general/document-signing/document-signing-email-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/direct-template/direct-template-configure-form.tsx
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/profile.tsx
@ -2553,6 +2611,7 @@ msgstr "Divulgation de signature électronique"
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-add-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-fields.tsx
@ -2646,15 +2705,6 @@ msgstr "Activer la signature de lien direct"
msgid "Enable signing order"
msgstr "Activer l'ordre de signature"
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Enable Typed Signature"
msgstr "Activer la signature dactylographiée"
#: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/document-flow/add-fields.tsx
msgid "Enable Typed Signatures"
msgstr "Activer les signatures tapées"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/webhooks._index.tsx
@ -2929,7 +2979,7 @@ msgstr "Aller au propriétaire"
msgid "Go to your <0>public profile settings</0> to add documents."
msgstr "Allez à vos <0>paramètres de profil public</0> pour ajouter des documents."
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Green"
msgstr "Vert"
@ -3483,10 +3533,12 @@ msgstr "Mes modèles"
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/general/claim-account.tsx
#: apps/remix/app/components/general/document-signing/document-signing-name-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-add-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-fields.tsx
@ -3537,6 +3589,7 @@ msgstr "Nouveau modèle"
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: packages/ui/primitives/signature-pad/signature-pad-dialog.tsx
msgid "Next"
msgstr "Suivant"
@ -3954,6 +4007,7 @@ msgstr "Veuillez entrer un nom significatif pour votre token. Cela vous aidera
#: apps/remix/app/components/general/claim-account.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/forms/profile.tsx
msgid "Please enter a valid name."
msgstr "Veuiillez entrer un nom valide."
@ -3989,10 +4043,6 @@ msgstr "Veuillez noter que cette action est irréversible. Une fois confirmée,
msgid "Please note that you will lose access to all documents associated with this team & all the members will be removed and notified"
msgstr "Veuillez noter que vous perdrez l'accès à tous les documents associés à cette équipe et que tous les membres seront supprimés et notifiés"
#: apps/remix/app/components/general/document-signing/document-signing-reject-dialog.tsx
msgid "Please provide a reason"
msgstr "Veuillez fournir une raison"
#: apps/remix/app/components/forms/2fa/disable-authenticator-app-dialog.tsx
msgid "Please provide a token from the authenticator, or a backup code. If you do not have a backup code available, please contact support."
msgstr "Veuillez fournir un token de l'authentificateur, ou un code de secours. Si vous n'avez pas de code de secours disponible, veuillez contacter le support."
@ -4063,6 +4113,10 @@ msgstr "Privé"
msgid "Private templates can only be modified and viewed by you."
msgstr "Les modèles privés ne peuvent être modifiés et consultés que par vous."
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Proceed"
msgstr ""
#: apps/remix/app/routes/_authenticated+/settings+/profile.tsx
#: apps/remix/app/components/general/settings-nav-mobile.tsx
#: apps/remix/app/components/general/settings-nav-desktop.tsx
@ -4140,6 +4194,10 @@ msgstr "Prêt"
msgid "Reason"
msgstr "Raison"
#: packages/email/template-components/template-document-cancel.tsx
msgid "Reason for cancellation: {cancellationReason}"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Reason for rejection: "
msgstr ""
@ -4222,7 +4280,7 @@ msgstr "Code de récupération copié"
msgid "Recovery codes"
msgstr "Codes de récupération"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Red"
msgstr "Rouge"
@ -4243,8 +4301,11 @@ msgstr "Inscription réussie"
msgid "Reject Document"
msgstr "Rejeter le Document"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/components/general/stack-avatars-with-tooltip.tsx
#: apps/remix/app/components/general/document/document-status.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: packages/lib/constants/document.ts
msgid "Rejected"
msgstr "Rejeté"
@ -4429,8 +4490,6 @@ msgstr "Lignes par page"
#: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-number-field.tsx
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
#: apps/remix/app/components/forms/team-branding-preferences-form.tsx
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Save"
@ -4740,7 +4799,6 @@ msgstr "S'inscrire avec OIDC"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx
#: apps/remix/app/components/forms/profile.tsx
@ -4757,12 +4815,17 @@ msgstr "Signature"
msgid "Signature ID"
msgstr "ID de signature"
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
msgid "Signature is too small. Please provide a more complete signature."
msgstr "La signature est trop petite. Veuillez fournir une signature plus grande."
#: packages/ui/primitives/signature-pad/signature-pad-draw.tsx
msgid "Signature is too small"
msgstr ""
#: apps/remix/app/components/forms/profile.tsx
msgid "Signature Pad cannot be empty."
msgstr ""
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "Signature types"
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Signatures Collected"
@ -4987,6 +5050,7 @@ msgid "Subject <0>(Optional)</0>"
msgstr "Objet <0>(Optionnel)</0>"
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Submitting..."
msgstr ""
@ -5501,6 +5565,10 @@ msgstr "Le token que vous avez utilisé pour réinitialiser votre mot de passe a
msgid "The two-factor authentication code provided is incorrect"
msgstr ""
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "The types of signatures that recipients are allowed to use when signing the document."
msgstr ""
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/webhooks.$id.tsx
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
@ -5582,6 +5650,10 @@ msgstr "Ce document a été annulé par le propriétaire et n'est plus disponibl
msgid "This document has been cancelled by the owner."
msgstr "Ce document a été annulé par le propriétaire."
#: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx
msgid "This document has been rejected by a recipient"
msgstr ""
#: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx
msgid "This document has been signed by all recipients"
msgstr "Ce document a été signé par tous les destinataires"
@ -5730,6 +5802,10 @@ msgstr "Fuseau horaire"
msgid "Title"
msgstr "Titre"
#: packages/ui/primitives/document-flow/add-settings.types.ts
msgid "Title cannot be empty"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx
msgid "To accept this invitation you must create an account."
msgstr "Pour accepter cette invitation, vous devez créer un compte."
@ -5897,6 +5973,7 @@ msgstr "Ré-authentification à deux facteurs"
#: apps/remix/app/components/tables/templates-table.tsx
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: packages/lib/constants/document.ts
msgid "Type"
msgstr "Type"
@ -6004,6 +6081,7 @@ msgstr "Non complet"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.billing.tsx
msgid "Unknown"
msgstr "Inconnu"
@ -6014,11 +6092,14 @@ msgstr "Non payé"
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
#: apps/remix/app/components/forms/team-branding-preferences-form.tsx
#: apps/remix/app/components/forms/public-profile-form.tsx
#: apps/remix/app/components/dialogs/team-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Update"
msgstr "Mettre à jour"
@ -6039,6 +6120,8 @@ msgid "Update profile"
msgstr "Mettre à jour le profil"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Update Recipient"
msgstr "Mettre à jour le destinataire"
@ -6077,10 +6160,6 @@ msgstr "Mettre à jour le webhook"
msgid "Updating password..."
msgstr "Mise à jour du mot de passe..."
#: apps/remix/app/components/forms/profile.tsx
msgid "Updating profile..."
msgstr "Mise à jour du profil..."
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Updating Your Information"
msgstr "Mise à jour de vos informations"
@ -6089,6 +6168,10 @@ msgstr "Mise à jour de vos informations"
msgid "Upgrade"
msgstr "Améliorer"
#: packages/lib/constants/document.ts
msgid "Upload"
msgstr ""
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
msgid "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
msgstr "Importer un fichier CSV pour créer plusieurs documents à partir de ce modèle. Chaque ligne représente un document avec les coordonnées de son destinataire."
@ -6113,7 +6196,7 @@ msgstr "Importer le CSV"
msgid "Upload custom document"
msgstr "Importer un document personnalisé"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-upload.tsx
msgid "Upload Signature"
msgstr "Importer une signature"
@ -6287,6 +6370,7 @@ msgstr "Voir le document"
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/email/template-components/template-document-rejected.tsx
#: packages/email/template-components/template-document-invite.tsx
msgid "View Document"
@ -6653,6 +6737,11 @@ msgstr "Bienvenue sur Documenso !"
msgid "Were you trying to edit this document instead?"
msgstr "Essayiez-vous d'éditer ce document à la place ?"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "When enabled, signers can choose who should sign next in the sequence instead of following the predefined order."
msgstr ""
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
msgid "When you click continue, you will be prompted to add the first available authenticator on your system."
msgstr "Lorsque vous cliquez sur continuer, vous serez invité à ajouter le premier authentificateur disponible sur votre système."

View File

@ -419,6 +419,10 @@ msgstr "<0>{teamName}</0> ha richiesto di utilizzare il tuo indirizzo email per
msgid "<0>Click to upload</0> or drag and drop"
msgstr "<0>Fai clic per caricare</0> o trascina e rilascia"
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "<0>Drawn</0> - A signature that is drawn using a mouse or stylus."
msgstr ""
#: packages/ui/primitives/template-flow/add-template-settings.tsx
msgid "<0>Email</0> - The recipient will be emailed the document to sign, approve, etc."
msgstr "<0>Email</0> - Al destinatario verrà inviato il documento tramite email per firmare, approvare, ecc."
@ -465,6 +469,14 @@ msgstr "<0>Richiede passkey</0> - Il destinatario deve avere un account e una pa
msgid "<0>Sender:</0> All"
msgstr "<0>Mittente:</0> Tutti"
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "<0>Typed</0> - A signature that is typed using a keyboard."
msgstr ""
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "<0>Uploaded</0> - A signature that is uploaded from a file."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "<0>You are about to complete approving <1>\"{documentTitle}\"</1>.</0><2/> Are you sure?"
msgstr "<0>Stai per completare l'approvazione di <1>\"{documentTitle}\"</1>.</0><2/> Sei sicuro?"
@ -898,6 +910,16 @@ msgstr "Tutto il tempo"
msgid "Allow document recipients to reply directly to this email address"
msgstr "Consenti ai destinatari del documento di rispondere direttamente a questo indirizzo email"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "Allow signers to dictate next signer"
msgstr ""
#: packages/ui/primitives/template-flow/add-template-settings.tsx
#: packages/ui/primitives/document-flow/add-settings.tsx
msgid "Allowed Signature Types"
msgstr ""
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Allows authenticating using biometrics, password managers, hardware keys, etc."
msgstr "Consente di autenticare utilizzando biometria, gestori di password, chiavi hardware, ecc."
@ -1237,6 +1259,13 @@ msgstr ""
msgid "Assisting"
msgstr ""
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
#: packages/ui/primitives/template-flow/add-template-settings.types.tsx
#: packages/ui/primitives/document-flow/add-settings.types.ts
#: packages/lib/types/document-meta.ts
msgid "At least one signature type must be enabled"
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
msgid "Attempts sealing the document again, useful for after a code change has occurred to resolve an erroneous document."
msgstr "Tenta nuovamente di sigillare il documento, utile dopo una modifica al codice per risolvere un documento errato."
@ -1309,11 +1338,11 @@ msgstr "Prima di iniziare, conferma il tuo indirizzo email facendo clic sul puls
msgid "Billing"
msgstr "Fatturazione"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Black"
msgstr "Nero"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Blue"
msgstr "Blu"
@ -1384,6 +1413,10 @@ msgstr "Procedendo con l'utilizzo del servizio di firma elettronica fornito da D
msgid "By proceeding with your electronic signature, you acknowledge and consent that it will be used to sign the given document and holds the same legal validity as a handwritten signature. By completing the electronic signing process, you affirm your understanding and acceptance of these conditions."
msgstr "Procedendo con la tua firma elettronica, riconosci e acconsenti che sarà utilizzata per firmare il documento dato e ha la stessa validità legale di una firma autografa. Completando il processo di firma elettronica, affermi la tua comprensione e accettazione di queste condizioni."
#: apps/remix/app/components/forms/signup.tsx
msgid "By proceeding, you agree to our <0>Terms of Service</0> and <1>Privacy Policy</1>."
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "By using the electronic signature feature, you are consenting to conduct transactions and receive disclosures electronically. You acknowledge that your electronic signature on documents is binding and that you accept the terms outlined in the documents you are signing."
msgstr "Utilizzando la funzione di firma elettronica, acconsenti a effettuare transazioni e ricevere divulgazioni elettronicamente. Riconosci che la tua firma elettronica sui documenti è vincolante e accetti i termini delineati nei documenti che stai firmando."
@ -1436,6 +1469,8 @@ msgstr ""
#: apps/remix/app/components/dialogs/document-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/signature-pad/signature-pad-dialog.tsx
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Cancel"
@ -1522,7 +1557,7 @@ msgstr "Rimuovi file"
msgid "Clear filters"
msgstr "Cancella filtri"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-draw.tsx
msgid "Clear Signature"
msgstr "Cancella firma"
@ -1697,6 +1732,7 @@ msgstr "Contenuto"
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/document-flow/document-flow-root.tsx
msgid "Continue"
msgstr "Continua"
@ -1737,14 +1773,18 @@ msgstr "Controlla la visibilità predefinita di un documento caricato."
msgid "Controls the formatting of the message that will be sent when inviting a recipient to sign a document. If a custom message has been provided while configuring the document, it will be used instead."
msgstr "Controlla la formattazione del messaggio che verrà inviato quando si invita un destinatario a firmare un documento. Se è stato fornito un messaggio personalizzato durante la configurazione del documento, verrà utilizzato invece."
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Controls whether the recipients can sign the documents using a typed signature. Enable or disable the typed signature globally."
msgstr "Controlla se i destinatari possono firmare i documenti utilizzando una firma digitata. Abilita o disabilita la firma digitata a livello globale."
#: packages/ui/primitives/document-flow/add-settings.tsx
msgid "Controls the language for the document, including the language to be used for email notifications, and the final certificate that is generated and attached to the document."
msgstr ""
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "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."
msgstr "Controlla se il certificato di firma sarà incluso nel documento quando viene scaricato. Il certificato di firma può comunque essere scaricato separatamente dalla pagina dei log."
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Controls which signatures are allowed to be used when signing a document."
msgstr ""
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Copied"
@ -1975,6 +2015,10 @@ msgstr "Lingua predefinita del documento"
msgid "Default Document Visibility"
msgstr "Visibilità predefinita del documento"
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Default Signature Settings"
msgstr ""
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
msgid "delete"
msgstr "elimina"
@ -2201,6 +2245,11 @@ msgstr "Documento \"{0}\" - Rifiutato da {1}"
msgid "Document \"{0}\" - Rejection Confirmed"
msgstr "Documento \"{0}\" - Rifiuto Confermato"
#. placeholder {0}: document.title
#: packages/lib/jobs/definitions/emails/send-document-cancelled-emails.handler.ts
msgid "Document \"{0}\" Cancelled"
msgstr ""
#: packages/ui/primitives/template-flow/add-template-settings.tsx
#: packages/ui/primitives/document-flow/add-settings.tsx
#: packages/ui/components/document/document-global-auth-access-select.tsx
@ -2349,6 +2398,10 @@ msgstr "Preferenze del documento aggiornate"
msgid "Document re-sent"
msgstr "Documento rinviato"
#: apps/remix/app/components/general/document/document-status.tsx
msgid "Document rejected"
msgstr ""
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/components/embed/embed-document-rejected.tsx
#: packages/email/template-components/template-document-rejected.tsx
@ -2486,6 +2539,10 @@ msgstr "Documenti redatti"
msgid "Drag & drop your PDF here."
msgstr "Trascina e rilascia il tuo PDF qui."
#: packages/lib/constants/document.ts
msgid "Draw"
msgstr ""
#: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/document-flow/add-fields.tsx
msgid "Dropdown"
@ -2543,6 +2600,7 @@ msgstr "Divulgazione della firma elettronica"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/general/document-signing/document-signing-email-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/direct-template/direct-template-configure-form.tsx
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/profile.tsx
@ -2553,6 +2611,7 @@ msgstr "Divulgazione della firma elettronica"
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-add-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-fields.tsx
@ -2646,15 +2705,6 @@ msgstr "Abilita la firma di link diretto"
msgid "Enable signing order"
msgstr "Abilita ordine di firma"
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Enable Typed Signature"
msgstr "Abilita firma digitata"
#: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/document-flow/add-fields.tsx
msgid "Enable Typed Signatures"
msgstr "Abilita firme digitate"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/webhooks._index.tsx
@ -2929,7 +2979,7 @@ msgstr "Vai al proprietario"
msgid "Go to your <0>public profile settings</0> to add documents."
msgstr "Vai alle tue <0>impostazioni del profilo pubblico</0> per aggiungere documenti."
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Green"
msgstr "Verde"
@ -3483,10 +3533,12 @@ msgstr "I miei modelli"
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/general/claim-account.tsx
#: apps/remix/app/components/general/document-signing/document-signing-name-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-add-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-fields.tsx
@ -3537,6 +3589,7 @@ msgstr "Nuovo modello"
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: packages/ui/primitives/signature-pad/signature-pad-dialog.tsx
msgid "Next"
msgstr "Successivo"
@ -3954,6 +4007,7 @@ msgstr "Si prega di inserire un nome significativo per il proprio token. Questo
#: apps/remix/app/components/general/claim-account.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/forms/profile.tsx
msgid "Please enter a valid name."
msgstr "Per favore inserisci un nome valido."
@ -3989,10 +4043,6 @@ msgstr "Si prega di notare che questa azione è irreversibile. Una volta conferm
msgid "Please note that you will lose access to all documents associated with this team & all the members will be removed and notified"
msgstr "Si prega di notare che perderai l'accesso a tutti i documenti associati a questo team e tutti i membri saranno rimossi e notificati"
#: apps/remix/app/components/general/document-signing/document-signing-reject-dialog.tsx
msgid "Please provide a reason"
msgstr "Per favore, fornire una ragione"
#: apps/remix/app/components/forms/2fa/disable-authenticator-app-dialog.tsx
msgid "Please provide a token from the authenticator, or a backup code. If you do not have a backup code available, please contact support."
msgstr "Si prega di fornire un token dal tuo autenticatore, o un codice di backup. Se non hai un codice di backup disponibile, contatta il supporto."
@ -4063,6 +4113,10 @@ msgstr "Privato"
msgid "Private templates can only be modified and viewed by you."
msgstr "I modelli privati possono essere modificati e visualizzati solo da te."
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Proceed"
msgstr ""
#: apps/remix/app/routes/_authenticated+/settings+/profile.tsx
#: apps/remix/app/components/general/settings-nav-mobile.tsx
#: apps/remix/app/components/general/settings-nav-desktop.tsx
@ -4140,6 +4194,10 @@ msgstr "Pronto"
msgid "Reason"
msgstr "Motivo"
#: packages/email/template-components/template-document-cancel.tsx
msgid "Reason for cancellation: {cancellationReason}"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Reason for rejection: "
msgstr ""
@ -4222,7 +4280,7 @@ msgstr "Codice di recupero copiato"
msgid "Recovery codes"
msgstr "Codici di recupero"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Red"
msgstr "Rosso"
@ -4243,8 +4301,11 @@ msgstr "Registrazione avvenuta con successo"
msgid "Reject Document"
msgstr "Rifiuta Documento"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/components/general/stack-avatars-with-tooltip.tsx
#: apps/remix/app/components/general/document/document-status.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: packages/lib/constants/document.ts
msgid "Rejected"
msgstr "Rifiutato"
@ -4429,8 +4490,6 @@ msgstr "Righe per pagina"
#: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-number-field.tsx
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
#: apps/remix/app/components/forms/team-branding-preferences-form.tsx
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Save"
@ -4740,7 +4799,6 @@ msgstr "Iscriviti con OIDC"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx
#: apps/remix/app/components/forms/profile.tsx
@ -4757,12 +4815,17 @@ msgstr "Firma"
msgid "Signature ID"
msgstr "ID Firma"
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
msgid "Signature is too small. Please provide a more complete signature."
msgstr "La firma è troppo piccola. Si prega di fornire una firma più completa."
#: packages/ui/primitives/signature-pad/signature-pad-draw.tsx
msgid "Signature is too small"
msgstr ""
#: apps/remix/app/components/forms/profile.tsx
msgid "Signature Pad cannot be empty."
msgstr ""
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "Signature types"
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Signatures Collected"
@ -4987,6 +5050,7 @@ msgid "Subject <0>(Optional)</0>"
msgstr "Oggetto <0>(Opzionale)</0>"
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Submitting..."
msgstr ""
@ -5501,6 +5565,10 @@ msgstr "Il token che hai usato per reimpostare la tua password è scaduto o non
msgid "The two-factor authentication code provided is incorrect"
msgstr ""
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "The types of signatures that recipients are allowed to use when signing the document."
msgstr ""
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/webhooks.$id.tsx
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
@ -5582,6 +5650,10 @@ msgstr "Questo documento è stato annullato dal proprietario e non è più dispo
msgid "This document has been cancelled by the owner."
msgstr "Questo documento è stato annullato dal proprietario."
#: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx
msgid "This document has been rejected by a recipient"
msgstr ""
#: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx
msgid "This document has been signed by all recipients"
msgstr "Questo documento è stato firmato da tutti i destinatari"
@ -5730,6 +5802,10 @@ msgstr "Fuso orario"
msgid "Title"
msgstr "Titolo"
#: packages/ui/primitives/document-flow/add-settings.types.ts
msgid "Title cannot be empty"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx
msgid "To accept this invitation you must create an account."
msgstr "Per accettare questo invito devi creare un account."
@ -5897,6 +5973,7 @@ msgstr "Ri-autenticazione a due fattori"
#: apps/remix/app/components/tables/templates-table.tsx
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: packages/lib/constants/document.ts
msgid "Type"
msgstr "Tipo"
@ -6004,6 +6081,7 @@ msgstr "Incompleto"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.billing.tsx
msgid "Unknown"
msgstr "Sconosciuto"
@ -6014,11 +6092,14 @@ msgstr "Non pagato"
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
#: apps/remix/app/components/forms/team-branding-preferences-form.tsx
#: apps/remix/app/components/forms/public-profile-form.tsx
#: apps/remix/app/components/dialogs/team-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Update"
msgstr "Aggiorna"
@ -6039,6 +6120,8 @@ msgid "Update profile"
msgstr "Aggiorna profilo"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Update Recipient"
msgstr "Aggiorna destinatario"
@ -6077,10 +6160,6 @@ msgstr "Aggiorna webhook"
msgid "Updating password..."
msgstr "Aggiornamento della password..."
#: apps/remix/app/components/forms/profile.tsx
msgid "Updating profile..."
msgstr "Aggiornamento del profilo..."
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Updating Your Information"
msgstr "Aggiornamento delle tue informazioni"
@ -6089,6 +6168,10 @@ msgstr "Aggiornamento delle tue informazioni"
msgid "Upgrade"
msgstr "Aggiorna"
#: packages/lib/constants/document.ts
msgid "Upload"
msgstr ""
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
msgid "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
msgstr "Carica un file CSV per creare più documenti da questo modello. Ogni riga rappresenta un documento con i dettagli del destinatario."
@ -6113,7 +6196,7 @@ msgstr "Carica CSV"
msgid "Upload custom document"
msgstr "Carica documento personalizzato"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-upload.tsx
msgid "Upload Signature"
msgstr "Carica Firma"
@ -6287,6 +6370,7 @@ msgstr "Visualizza documento"
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/email/template-components/template-document-rejected.tsx
#: packages/email/template-components/template-document-invite.tsx
msgid "View Document"
@ -6653,6 +6737,11 @@ msgstr "Benvenuto su Documenso!"
msgid "Were you trying to edit this document instead?"
msgstr "Stavi provando a modificare questo documento invece?"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "When enabled, signers can choose who should sign next in the sequence instead of following the predefined order."
msgstr ""
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
msgid "When you click continue, you will be prompted to add the first available authenticator on your system."
msgstr "Quando fai clic su continua, ti verrà chiesto di aggiungere il primo autenticatore disponibile sul tuo sistema."

View File

@ -419,6 +419,10 @@ msgstr "<0>{teamName}</0> poprosił o używanie twojego adresu e-mail dla swojeg
msgid "<0>Click to upload</0> or drag and drop"
msgstr "<0>Kliknij, aby przesłać</0> lub przeciągnij i upuść"
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "<0>Drawn</0> - A signature that is drawn using a mouse or stylus."
msgstr ""
#: packages/ui/primitives/template-flow/add-template-settings.tsx
msgid "<0>Email</0> - The recipient will be emailed the document to sign, approve, etc."
msgstr "<0>E-mail</0> - Odbiorca otrzyma e-mail z dokumentem do podpisania, zatwierdzenia itp."
@ -465,6 +469,14 @@ msgstr "<0>Wymagana passkey</0> - Odbiorca musi mieć konto i skonfigurowaną pa
msgid "<0>Sender:</0> All"
msgstr "<0>Rządzący:</0> Wszyscy"
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "<0>Typed</0> - A signature that is typed using a keyboard."
msgstr ""
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "<0>Uploaded</0> - A signature that is uploaded from a file."
msgstr ""
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
msgid "<0>You are about to complete approving <1>\"{documentTitle}\"</1>.</0><2/> Are you sure?"
msgstr "<0>Jesteś na drodze do zatwierdzenia <1>\"{documentTitle}\"</1>.</0><2/> Czy jesteś pewien?"
@ -898,6 +910,16 @@ msgstr "Cały czas"
msgid "Allow document recipients to reply directly to this email address"
msgstr "Zezwól odbiorcom dokumentów na bezpośrednią odpowiedź na ten adres e-mail"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "Allow signers to dictate next signer"
msgstr ""
#: packages/ui/primitives/template-flow/add-template-settings.tsx
#: packages/ui/primitives/document-flow/add-settings.tsx
msgid "Allowed Signature Types"
msgstr ""
#: apps/remix/app/routes/_authenticated+/settings+/security._index.tsx
msgid "Allows authenticating using biometrics, password managers, hardware keys, etc."
msgstr "Pozwala na uwierzytelnianie za pomocą biometrii, menedżerów haseł, kluczy sprzętowych itp."
@ -1237,6 +1259,13 @@ msgstr ""
msgid "Assisting"
msgstr ""
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
#: packages/ui/primitives/template-flow/add-template-settings.types.tsx
#: packages/ui/primitives/document-flow/add-settings.types.ts
#: packages/lib/types/document-meta.ts
msgid "At least one signature type must be enabled"
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/documents.$id.tsx
msgid "Attempts sealing the document again, useful for after a code change has occurred to resolve an erroneous document."
msgstr "Ponowne próby zapieczętowania dokumentu, przydatne po zmianie kodu w celu rozwiązania błędnego dokumentu."
@ -1309,11 +1338,11 @@ msgstr "Zanim zaczniesz, proszę potwierdź swój adres e-mail, klikając przyci
msgid "Billing"
msgstr "Fakturowanie"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Black"
msgstr "Czarny"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Blue"
msgstr "Niebieski"
@ -1384,6 +1413,10 @@ msgstr "Kontynuując korzystanie z usługi podpisu elektronicznego oferowanej pr
msgid "By proceeding with your electronic signature, you acknowledge and consent that it will be used to sign the given document and holds the same legal validity as a handwritten signature. By completing the electronic signing process, you affirm your understanding and acceptance of these conditions."
msgstr "Kontynuując z Twoim podpisem elektronicznym, przyjmujesz i zgadzasz się, że będzie on użyty do podpisania danego dokumentu i ma tę samą ważność prawną jak odręczny podpis. Dokonując procesu podpisu elektronicznego, potwierdzasz swoje zrozumienie i akceptację tych warunków."
#: apps/remix/app/components/forms/signup.tsx
msgid "By proceeding, you agree to our <0>Terms of Service</0> and <1>Privacy Policy</1>."
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "By using the electronic signature feature, you are consenting to conduct transactions and receive disclosures electronically. You acknowledge that your electronic signature on documents is binding and that you accept the terms outlined in the documents you are signing."
msgstr "Korzystając z funkcji podpisu elektronicznego, wyrażasz zgodę na przeprowadzanie transakcji i otrzymywanie ujawnień elektronicznie. Przyjmujesz do wiadomości, że Twój podpis elektroniczny na dokumentach jest wiążący i akceptujesz warunki przedstawione w dokumentach, które podpisujesz."
@ -1436,6 +1469,8 @@ msgstr ""
#: apps/remix/app/components/dialogs/document-move-dialog.tsx
#: apps/remix/app/components/dialogs/document-duplicate-dialog.tsx
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/signature-pad/signature-pad-dialog.tsx
#: packages/ui/primitives/document-flow/send-document-action-dialog.tsx
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Cancel"
@ -1522,7 +1557,7 @@ msgstr "Wyczyść plik"
msgid "Clear filters"
msgstr "Wyczyść filtry"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-draw.tsx
msgid "Clear Signature"
msgstr "Wyczyść podpis"
@ -1697,6 +1732,7 @@ msgstr "Treść"
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/document-flow/document-flow-root.tsx
msgid "Continue"
msgstr "Kontynuuj"
@ -1737,14 +1773,18 @@ msgstr "Kontroluje domyślną widoczność przesłanego dokumentu."
msgid "Controls the formatting of the message that will be sent when inviting a recipient to sign a document. If a custom message has been provided while configuring the document, it will be used instead."
msgstr "Kontroluje formatowanie wiadomości, która zostanie wysłana podczas zapraszania odbiorcy do podpisania dokumentu. Jeśli w konfiguracji dokumentu podano niestandardową wiadomość, zostanie użyta zamiast tego."
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Controls whether the recipients can sign the documents using a typed signature. Enable or disable the typed signature globally."
msgstr "Kontroluje, czy odbiorcy mogą podpisywać dokumenty za pomocą pisanych podpisów. Włącz lub wyłącz podpis pisany globalnie."
#: packages/ui/primitives/document-flow/add-settings.tsx
msgid "Controls the language for the document, including the language to be used for email notifications, and the final certificate that is generated and attached to the document."
msgstr ""
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "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."
msgstr "Kontroluje, czy certyfikat podpisu zostanie dołączony do dokumentu podczas jego pobierania. Certyfikat podpisu można również pobrać osobno ze strony logów."
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Controls which signatures are allowed to be used when signing a document."
msgstr ""
#: apps/remix/app/components/general/document/document-recipient-link-copy-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Copied"
@ -1975,6 +2015,10 @@ msgstr "Domyślny język dokumentu"
msgid "Default Document Visibility"
msgstr "Domyślna widoczność dokumentu"
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Default Signature Settings"
msgstr ""
#: apps/remix/app/components/dialogs/document-delete-dialog.tsx
msgid "delete"
msgstr "usuń"
@ -2201,6 +2245,11 @@ msgstr "Dokument \"{0}\" - Odrzucony przez {1}"
msgid "Document \"{0}\" - Rejection Confirmed"
msgstr "Dokument \"{0}\" - Odrzucenie potwierdzone"
#. placeholder {0}: document.title
#: packages/lib/jobs/definitions/emails/send-document-cancelled-emails.handler.ts
msgid "Document \"{0}\" Cancelled"
msgstr ""
#: packages/ui/primitives/template-flow/add-template-settings.tsx
#: packages/ui/primitives/document-flow/add-settings.tsx
#: packages/ui/components/document/document-global-auth-access-select.tsx
@ -2349,6 +2398,10 @@ msgstr "Preferencje dokumentu zaktualizowane"
msgid "Document re-sent"
msgstr "Dokument ponownie wysłany"
#: apps/remix/app/components/general/document/document-status.tsx
msgid "Document rejected"
msgstr ""
#: apps/remix/app/routes/_recipient+/sign.$token+/rejected.tsx
#: apps/remix/app/components/embed/embed-document-rejected.tsx
#: packages/email/template-components/template-document-rejected.tsx
@ -2486,6 +2539,10 @@ msgstr "Szkice dokumentów"
msgid "Drag & drop your PDF here."
msgstr "Przeciągnij i upuść swój PDF tutaj."
#: packages/lib/constants/document.ts
msgid "Draw"
msgstr ""
#: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/document-flow/add-fields.tsx
msgid "Dropdown"
@ -2543,6 +2600,7 @@ msgstr "Ujawnienie podpisu elektronicznego"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/general/document-signing/document-signing-email-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/general/direct-template/direct-template-configure-form.tsx
#: apps/remix/app/components/forms/signin.tsx
#: apps/remix/app/components/forms/profile.tsx
@ -2553,6 +2611,7 @@ msgstr "Ujawnienie podpisu elektronicznego"
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-add-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-fields.tsx
@ -2646,15 +2705,6 @@ msgstr "Włącz podpisywanie linku bezpośredniego"
msgid "Enable signing order"
msgstr "Włącz kolejność podpisów"
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
msgid "Enable Typed Signature"
msgstr "Włącz podpis pisany"
#: packages/ui/primitives/template-flow/add-template-fields.tsx
#: packages/ui/primitives/document-flow/add-fields.tsx
msgid "Enable Typed Signatures"
msgstr "Włącz podpisy typu pisanego"
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks._index.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/webhooks._index.tsx
@ -2929,7 +2979,7 @@ msgstr "Przejdź do właściciela"
msgid "Go to your <0>public profile settings</0> to add documents."
msgstr "Przejdź do swojego <0>ustawienia profilu publicznego</0>, aby dodać dokumenty."
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Green"
msgstr "Zielony"
@ -3483,10 +3533,12 @@ msgstr "Moje szablony"
#: apps/remix/app/components/tables/admin-dashboard-users-table.tsx
#: apps/remix/app/components/general/claim-account.tsx
#: apps/remix/app/components/general/document-signing/document-signing-name-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: apps/remix/app/components/dialogs/template-use-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-add-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/template-flow/add-template-fields.tsx
@ -3537,6 +3589,7 @@ msgstr "Nowy szablon"
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
#: packages/ui/primitives/signature-pad/signature-pad-dialog.tsx
msgid "Next"
msgstr "Dalej"
@ -3954,6 +4007,7 @@ msgstr "Wpisz nazwę tokena. Pomoże to później w jego identyfikacji."
#: apps/remix/app/components/general/claim-account.tsx
#: apps/remix/app/components/forms/signup.tsx
#: apps/remix/app/components/forms/profile.tsx
msgid "Please enter a valid name."
msgstr "Proszę wpisać poprawną nazwę."
@ -3989,10 +4043,6 @@ msgstr "Proszę pamiętać, że ta czynność jest nieodwracalna. Po potwierdzen
msgid "Please note that you will lose access to all documents associated with this team & all the members will be removed and notified"
msgstr "Proszę pamiętać, że stracisz dostęp do wszystkich dokumentów powiązanych z tym zespołem i wszyscy członkowie zostaną usunięci oraz powiadomieni"
#: apps/remix/app/components/general/document-signing/document-signing-reject-dialog.tsx
msgid "Please provide a reason"
msgstr "Proszę podać powód"
#: apps/remix/app/components/forms/2fa/disable-authenticator-app-dialog.tsx
msgid "Please provide a token from the authenticator, or a backup code. If you do not have a backup code available, please contact support."
msgstr "Proszę podać token z aplikacji uwierzytelniającej lub kod zapasowy. Jeśli nie masz dostępnego kodu zapasowego, skontaktuj się z pomocą techniczną."
@ -4063,6 +4113,10 @@ msgstr "Prywatne"
msgid "Private templates can only be modified and viewed by you."
msgstr "Prywatne szablony mogą być modyfikowane i przeglądane tylko przez Ciebie."
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Proceed"
msgstr ""
#: apps/remix/app/routes/_authenticated+/settings+/profile.tsx
#: apps/remix/app/components/general/settings-nav-mobile.tsx
#: apps/remix/app/components/general/settings-nav-desktop.tsx
@ -4140,6 +4194,10 @@ msgstr "Gotowy"
msgid "Reason"
msgstr "Powód"
#: packages/email/template-components/template-document-cancel.tsx
msgid "Reason for cancellation: {cancellationReason}"
msgstr ""
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
msgid "Reason for rejection: "
msgstr ""
@ -4222,7 +4280,7 @@ msgstr "Kod odzyskiwania skopiowany"
msgid "Recovery codes"
msgstr "Kody odzyskiwania"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-color-picker.tsx
msgid "Red"
msgstr "Czerwony"
@ -4243,8 +4301,11 @@ msgstr "Rejestracja zakończona sukcesem"
msgid "Reject Document"
msgstr "Odrzuć dokument"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/components/general/stack-avatars-with-tooltip.tsx
#: apps/remix/app/components/general/document/document-status.tsx
#: apps/remix/app/components/general/document/document-page-view-recipients.tsx
#: packages/lib/constants/document.ts
msgid "Rejected"
msgstr "Odrzucony"
@ -4429,8 +4490,6 @@ msgstr "Wiersze na stronę"
#: apps/remix/app/components/general/document-signing/document-signing-text-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-number-field.tsx
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
#: apps/remix/app/components/forms/team-branding-preferences-form.tsx
#: apps/remix/app/components/dialogs/template-direct-link-dialog.tsx
#: packages/ui/primitives/document-flow/field-item-advanced-settings.tsx
msgid "Save"
@ -4740,7 +4799,6 @@ msgstr "Zarejestruj się za pomocą OIDC"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/general/direct-template/direct-template-signing-form.tsx
#: apps/remix/app/components/forms/profile.tsx
@ -4757,12 +4815,17 @@ msgstr "Podpis"
msgid "Signature ID"
msgstr "Identyfikator podpisu"
#: apps/remix/app/components/general/document-signing/document-signing-signature-field.tsx
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/embed/embed-document-signing-page.tsx
#: apps/remix/app/components/embed/embed-direct-template-client-page.tsx
msgid "Signature is too small. Please provide a more complete signature."
msgstr "Podpis jest zbyt mały. Proszę podać bardziej kompletny podpis."
#: packages/ui/primitives/signature-pad/signature-pad-draw.tsx
msgid "Signature is too small"
msgstr ""
#: apps/remix/app/components/forms/profile.tsx
msgid "Signature Pad cannot be empty."
msgstr ""
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "Signature types"
msgstr ""
#: apps/remix/app/routes/_authenticated+/admin+/stats.tsx
msgid "Signatures Collected"
@ -4987,6 +5050,7 @@ msgid "Subject <0>(Optional)</0>"
msgstr "Temat <0>(Opcjonalnie)</0>"
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Submitting..."
msgstr ""
@ -5501,6 +5565,10 @@ msgstr "Token, którego użyłeś do zresetowania hasła, jest albo wygasły, al
msgid "The two-factor authentication code provided is incorrect"
msgstr ""
#: packages/ui/components/document/document-signature-settings-tooltip.tsx
msgid "The types of signatures that recipients are allowed to use when signing the document."
msgstr ""
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.webhooks.$id.tsx
#: apps/remix/app/routes/_authenticated+/settings+/webhooks.$id.tsx
#: apps/remix/app/components/dialogs/webhook-create-dialog.tsx
@ -5582,6 +5650,10 @@ msgstr "Ten dokument został anulowany przez właściciela i nie jest już dost
msgid "This document has been cancelled by the owner."
msgstr "Ten dokument został anulowany przez właściciela."
#: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx
msgid "This document has been rejected by a recipient"
msgstr ""
#: apps/remix/app/routes/_authenticated+/documents.$id._index.tsx
msgid "This document has been signed by all recipients"
msgstr "Ten dokument został podpisany przez wszystkich odbiorców"
@ -5730,6 +5802,10 @@ msgstr "Strefa czasowa"
msgid "Title"
msgstr "Tytuł"
#: packages/ui/primitives/document-flow/add-settings.types.ts
msgid "Title cannot be empty"
msgstr ""
#: apps/remix/app/routes/_unauthenticated+/team.invite.$token.tsx
msgid "To accept this invitation you must create an account."
msgstr "Aby zaakceptować to zaproszenie, musisz założyć konto."
@ -5897,6 +5973,7 @@ msgstr "Ponowna autoryzacja za pomocą dwuetapowej weryfikacji"
#: apps/remix/app/components/tables/templates-table.tsx
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: packages/lib/constants/document.ts
msgid "Type"
msgstr "Typ"
@ -6004,6 +6081,7 @@ msgstr "Niezakończony"
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_internal+/[__htmltopdf]+/certificate.tsx
#: apps/remix/app/routes/_authenticated+/t.$teamUrl+/settings.billing.tsx
msgid "Unknown"
msgstr "Nieznany"
@ -6014,11 +6092,14 @@ msgstr "Nieopłacone"
#: apps/remix/app/components/tables/settings-security-passkey-table-actions.tsx
#: apps/remix/app/components/tables/settings-public-profile-templates-table.tsx
#: apps/remix/app/components/forms/team-document-preferences-form.tsx
#: apps/remix/app/components/forms/team-branding-preferences-form.tsx
#: apps/remix/app/components/forms/public-profile-form.tsx
#: apps/remix/app/components/dialogs/team-member-update-dialog.tsx
#: apps/remix/app/components/dialogs/team-email-update-dialog.tsx
#: apps/remix/app/components/dialogs/public-profile-template-manage-dialog.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
msgid "Update"
msgstr "Zaktualizuj"
@ -6039,6 +6120,8 @@ msgid "Update profile"
msgstr "Zaktualizuj profil"
#: apps/remix/app/components/tables/admin-document-recipient-item-table.tsx
#: apps/remix/app/components/general/document-signing/document-signing-complete-dialog.tsx
#: apps/remix/app/components/dialogs/assistant-confirmation-dialog.tsx
msgid "Update Recipient"
msgstr "Zaktualizuj odbiorcę"
@ -6077,10 +6160,6 @@ msgstr "Zaktualizuj webhook"
msgid "Updating password..."
msgstr "Aktualizowanie hasła..."
#: apps/remix/app/components/forms/profile.tsx
msgid "Updating profile..."
msgstr "Aktualizacja profilu..."
#: apps/remix/app/routes/_unauthenticated+/articles.signature-disclosure.tsx
msgid "Updating Your Information"
msgstr "Aktualizacja Twoich informacji"
@ -6089,6 +6168,10 @@ msgstr "Aktualizacja Twoich informacji"
msgid "Upgrade"
msgstr "Ulepsz"
#: packages/lib/constants/document.ts
msgid "Upload"
msgstr ""
#: apps/remix/app/components/dialogs/template-bulk-send-dialog.tsx
msgid "Upload a CSV file to create multiple documents from this template. Each row represents one document with its recipient details."
msgstr "Prześlij plik CSV, aby utworzyć wiele dokumentów z tego szablonu. Każda linia reprezentuje jeden dokument z jego szczegółami odbiorcy."
@ -6113,7 +6196,7 @@ msgstr "Prześlij CSV"
msgid "Upload custom document"
msgstr "Prześlij niestandardowy dokument"
#: packages/ui/primitives/signature-pad/signature-pad.tsx
#: packages/ui/primitives/signature-pad/signature-pad-upload.tsx
msgid "Upload Signature"
msgstr "Prześlij podpis"
@ -6287,6 +6370,7 @@ msgstr "Zobacz dokument"
#: apps/remix/app/components/general/document-signing/document-signing-form.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/ui/primitives/document-flow/add-subject.tsx
#: packages/email/template-components/template-document-rejected.tsx
#: packages/email/template-components/template-document-invite.tsx
msgid "View Document"
@ -6653,6 +6737,11 @@ msgstr "Witamy w Documenso!"
msgid "Were you trying to edit this document instead?"
msgstr "Czy próbowałeś raczej edytować ten dokument?"
#: packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx
#: packages/ui/primitives/document-flow/add-signers.tsx
msgid "When enabled, signers can choose who should sign next in the sequence instead of following the predefined order."
msgstr ""
#: apps/remix/app/components/dialogs/passkey-create-dialog.tsx
msgid "When you click continue, you will be prompted to add the first available authenticator on your system."
msgstr "Kiedy klikniesz kontynuuj, zostaniesz poproszony o dodanie pierwszego dostępnego autoryzatora w swoim systemie."

View File

@ -0,0 +1,53 @@
import { msg } from '@lingui/core/macro';
import { z } from 'zod';
import { DocumentMetaSchema } from '@documenso/prisma/generated/zod/modelSchema/DocumentMetaSchema';
/**
* The full document response schema.
*
* Mainly used for returning a single document from the API.
*/
export const ZDocumentMetaSchema = DocumentMetaSchema.pick({
signingOrder: true,
distributionMethod: true,
id: true,
subject: true,
message: true,
timezone: true,
password: true,
dateFormat: true,
documentId: true,
redirectUrl: true,
typedSignatureEnabled: true,
uploadSignatureEnabled: true,
drawSignatureEnabled: true,
language: true,
emailSettings: true,
});
export type TDocumentMeta = z.infer<typeof ZDocumentMetaSchema>;
/**
* If you update this, you must also update the schema.prisma @default value for
* - Template meta
* - Document meta
*/
export const ZDocumentSignatureSettingsSchema = z
.object({
typedSignatureEnabled: z.boolean(),
uploadSignatureEnabled: z.boolean(),
drawnSignatureEnabled: z.boolean(),
})
.refine(
(data) => {
return (
data.typedSignatureEnabled || data.uploadSignatureEnabled || data.drawnSignatureEnabled
);
},
{
message: msg`At least one signature type must be enabled`.id,
},
);
export type TDocumentSignatureSettings = z.infer<typeof ZDocumentSignatureSettingsSchema>;

View File

@ -51,6 +51,8 @@ export const ZDocumentSchema = DocumentSchema.pick({
documentId: true,
redirectUrl: true,
typedSignatureEnabled: true,
uploadSignatureEnabled: true,
drawSignatureEnabled: true,
allowDictateNextSigner: true,
language: true,
emailSettings: true,

View File

@ -45,6 +45,8 @@ export const ZTemplateSchema = TemplateSchema.pick({
dateFormat: true,
signingOrder: true,
typedSignatureEnabled: true,
uploadSignatureEnabled: true,
drawSignatureEnabled: true,
allowDictateNextSigner: true,
distributionMethod: true,
templateId: true,

View File

@ -48,6 +48,8 @@ export const ZWebhookDocumentMetaSchema = z.object({
signingOrder: z.nativeEnum(DocumentSigningOrder),
allowDictateNextSigner: z.boolean(),
typedSignatureEnabled: z.boolean(),
uploadSignatureEnabled: z.boolean(),
drawSignatureEnabled: z.boolean(),
language: z.string(),
distributionMethod: z.nativeEnum(DocumentDistributionMethod),
emailSettings: z.any().nullable(),

View File

@ -1,4 +1,5 @@
import { NEXT_PUBLIC_WEBAPP_URL } from '../constants/app';
import { DocumentSignatureType } from '../constants/document';
import type { TEAM_MEMBER_ROLE_MAP } from '../constants/teams';
import { TEAM_MEMBER_ROLE_HIERARCHY, TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '../constants/teams';
@ -44,3 +45,31 @@ export const isTeamRoleWithinUserHierarchy = (
) => {
return TEAM_MEMBER_ROLE_HIERARCHY[currentUserRole].some((i) => i === roleToCheck);
};
export const extractTeamSignatureSettings = (
settings?: {
typedSignatureEnabled: boolean;
drawSignatureEnabled: boolean;
uploadSignatureEnabled: boolean;
} | null,
) => {
if (!settings) {
return [DocumentSignatureType.TYPE, DocumentSignatureType.UPLOAD, DocumentSignatureType.DRAW];
}
const signatureTypes: DocumentSignatureType[] = [];
if (settings.typedSignatureEnabled) {
signatureTypes.push(DocumentSignatureType.TYPE);
}
if (settings.drawSignatureEnabled) {
signatureTypes.push(DocumentSignatureType.DRAW);
}
if (settings.uploadSignatureEnabled) {
signatureTypes.push(DocumentSignatureType.UPLOAD);
}
return signatureTypes;
};

View File

@ -0,0 +1,11 @@
-- AlterTable
ALTER TABLE "DocumentMeta" ADD COLUMN "drawSignatureEnabled" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "uploadSignatureEnabled" BOOLEAN NOT NULL DEFAULT true;
-- AlterTable
ALTER TABLE "TeamGlobalSettings" ADD COLUMN "drawSignatureEnabled" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "uploadSignatureEnabled" BOOLEAN NOT NULL DEFAULT true;
-- AlterTable
ALTER TABLE "TemplateMeta" ADD COLUMN "drawSignatureEnabled" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "uploadSignatureEnabled" BOOLEAN NOT NULL DEFAULT true;

View File

@ -0,0 +1,25 @@
import type { PrismaClient } from '@prisma/client';
export function addPrismaMiddleware(prisma: PrismaClient) {
prisma.$use(async (params, next) => {
// Check if we're creating a new team
if (params.model === 'Team' && params.action === 'create') {
// Execute the team creation
const result = await next(params);
// Create the TeamGlobalSettings
await prisma.teamGlobalSettings.create({
data: {
teamId: result.id,
},
});
return result;
}
// For all other operations, just pass through
return next(params);
});
return prisma;
}

View File

@ -390,21 +390,25 @@ enum DocumentDistributionMethod {
/// @zod.import(["import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';"])
model DocumentMeta {
id String @id @default(cuid())
id String @id @default(cuid())
subject String?
message String?
timezone String? @default("Etc/UTC") @db.Text
timezone String? @default("Etc/UTC") @db.Text
password String?
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
documentId Int @unique
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
documentId Int @unique
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
redirectUrl String?
signingOrder DocumentSigningOrder @default(PARALLEL)
allowDictateNextSigner Boolean @default(false)
typedSignatureEnabled Boolean @default(true)
language String @default("en")
distributionMethod DocumentDistributionMethod @default(EMAIL)
emailSettings Json? /// [DocumentEmailSettings] @zod.custom.use(ZDocumentEmailSettingsSchema)
signingOrder DocumentSigningOrder @default(PARALLEL)
allowDictateNextSigner Boolean @default(false)
typedSignatureEnabled Boolean @default(true)
uploadSignatureEnabled Boolean @default(true)
drawSignatureEnabled Boolean @default(true)
language String @default("en")
distributionMethod DocumentDistributionMethod @default(EMAIL)
emailSettings Json? /// [DocumentEmailSettings] @zod.custom.use(ZDocumentEmailSettingsSchema)
}
enum ReadStatus {
@ -545,9 +549,12 @@ model TeamGlobalSettings {
documentVisibility DocumentVisibility @default(EVERYONE)
documentLanguage String @default("en")
includeSenderDetails Boolean @default(true)
typedSignatureEnabled Boolean @default(true)
includeSigningCertificate Boolean @default(true)
typedSignatureEnabled Boolean @default(true)
uploadSignatureEnabled Boolean @default(true)
drawSignatureEnabled Boolean @default(true)
brandingEnabled Boolean @default(false)
brandingLogo String @default("")
brandingUrl String @default("")
@ -669,9 +676,12 @@ model TemplateMeta {
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
signingOrder DocumentSigningOrder? @default(PARALLEL)
allowDictateNextSigner Boolean @default(false)
typedSignatureEnabled Boolean @default(true)
distributionMethod DocumentDistributionMethod @default(EMAIL)
typedSignatureEnabled Boolean @default(true)
uploadSignatureEnabled Boolean @default(true)
drawSignatureEnabled Boolean @default(true)
templateId Int @unique
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
redirectUrl String?

View File

@ -1,9 +1,12 @@
import type { Document, User } from '@prisma/client';
import type { Document, Team, User } from '@prisma/client';
import { nanoid } from 'nanoid';
import fs from 'node:fs';
import path from 'node:path';
import { match } from 'ts-pattern';
import { createDocument } from '@documenso/lib/server-only/document/create-document';
import { createTemplate } from '@documenso/lib/server-only/template/create-template';
import { prisma } from '..';
import {
DocumentDataType,
@ -87,6 +90,145 @@ export const unseedDocument = async (documentId: number) => {
});
};
export const seedTeamDocumentWithMeta = async (team: Team) => {
const documentData = await prisma.documentData.create({
data: {
type: DocumentDataType.BYTES_64,
data: examplePdf,
initialData: examplePdf,
},
});
const document = await createDocument({
userId: team.ownerUserId,
teamId: team.id,
title: `[TEST] Document ${nanoid(8)} - Draft`,
documentDataId: documentData.id,
normalizePdf: true,
requestMetadata: {
auth: null,
requestMetadata: {},
source: 'app',
},
});
const owner = await prisma.user.findFirstOrThrow({
where: {
id: team.ownerUserId,
},
});
await prisma.document.update({
where: {
id: document.id,
},
data: {
status: DocumentStatus.PENDING,
},
});
await prisma.recipient.create({
data: {
email: owner.email,
name: owner.name ?? '',
token: nanoid(),
readStatus: ReadStatus.OPENED,
sendStatus: SendStatus.SENT,
signingStatus: SigningStatus.NOT_SIGNED,
signedAt: new Date(),
document: {
connect: {
id: document.id,
},
},
fields: {
create: {
page: 1,
type: FieldType.SIGNATURE,
inserted: false,
customText: '',
positionX: new Prisma.Decimal(1),
positionY: new Prisma.Decimal(1),
width: new Prisma.Decimal(5),
height: new Prisma.Decimal(5),
documentId: document.id,
},
},
},
});
return await prisma.document.findFirstOrThrow({
where: {
id: document.id,
},
include: {
recipients: true,
},
});
};
export const seedTeamTemplateWithMeta = async (team: Team) => {
const documentData = await prisma.documentData.create({
data: {
type: DocumentDataType.BYTES_64,
data: examplePdf,
initialData: examplePdf,
},
});
const template = await createTemplate({
title: `[TEST] Template ${nanoid(8)} - Draft`,
userId: team.ownerUserId,
teamId: team.id,
templateDocumentDataId: documentData.id,
});
const owner = await prisma.user.findFirstOrThrow({
where: {
id: team.ownerUserId,
},
});
await prisma.recipient.create({
data: {
email: owner.email,
name: owner.name ?? '',
token: nanoid(),
readStatus: ReadStatus.OPENED,
sendStatus: SendStatus.SENT,
signingStatus: SigningStatus.NOT_SIGNED,
signedAt: new Date(),
template: {
connect: {
id: template.id,
},
},
fields: {
create: {
page: 1,
type: FieldType.SIGNATURE,
inserted: false,
customText: '',
positionX: new Prisma.Decimal(1),
positionY: new Prisma.Decimal(1),
width: new Prisma.Decimal(5),
height: new Prisma.Decimal(5),
templateId: template.id,
},
},
},
});
return await prisma.document.findFirstOrThrow({
where: {
id: template.id,
},
include: {
recipients: true,
},
});
};
export const seedDraftDocument = async (
sender: User,
recipients: (User | string)[],

View File

@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { fontFamily } = require('tailwindcss/defaultTheme');
const { default: flattenColorPalette } = require('tailwindcss/lib/util/flattenColorPalette');
/** @type {import('tailwindcss').Config} */
module.exports = {
@ -14,6 +15,9 @@ module.exports = {
zIndex: {
9999: '9999',
},
aspectRatio: {
'signature-pad': '16 / 7',
},
colors: {
border: 'hsl(var(--border))',
'field-border': 'hsl(var(--field-border))',
@ -147,5 +151,17 @@ module.exports = {
require('tailwindcss-animate'),
require('@tailwindcss/typography'),
require('@tailwindcss/container-queries'),
addVariablesForColors,
],
};
function addVariablesForColors({ addBase, theme }) {
let allColors = flattenColorPalette(theme('colors'));
let newVars = Object.fromEntries(
Object.entries(allColors).map(([key, val]) => [`--${key}`, val]),
);
addBase({
':root': newVars,
});
}

View File

@ -56,9 +56,12 @@ import {
ZSearchDocumentsMutationSchema,
ZSetSigningOrderForDocumentMutationSchema,
ZSuccessResponseSchema,
} from './schema';
import { updateDocumentRoute } from './update-document';
import {
ZUpdateDocumentRequestSchema,
ZUpdateDocumentResponseSchema,
} from './schema';
} from './update-document.types';
export const documentRouter = router({
/**
@ -335,20 +338,12 @@ export const documentRouter = router({
});
}),
updateDocument: updateDocumentRoute,
/**
* @public
*
* Todo: Refactor to updateDocument.
* @deprecated Delete this after updateDocument endpoint is deployed
*/
setSettingsForDocument: authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/document/update',
summary: 'Update document',
tags: ['Document'],
},
})
.input(ZUpdateDocumentRequestSchema)
.output(ZUpdateDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
@ -368,6 +363,8 @@ export const documentRouter = router({
dateFormat: meta.dateFormat,
language: meta.language,
typedSignatureEnabled: meta.typedSignatureEnabled,
uploadSignatureEnabled: meta.uploadSignatureEnabled,
drawSignatureEnabled: meta.drawSignatureEnabled,
redirectUrl: meta.redirectUrl,
distributionMethod: meta.distributionMethod,
signingOrder: meta.signingOrder,

View File

@ -109,6 +109,14 @@ export const ZDocumentMetaTypedSignatureEnabledSchema = z
.boolean()
.describe('Whether to allow recipients to sign using a typed signature.');
export const ZDocumentMetaDrawSignatureEnabledSchema = z
.boolean()
.describe('Whether to allow recipients to sign using a draw signature.');
export const ZDocumentMetaUploadSignatureEnabledSchema = z
.boolean()
.describe('Whether to allow recipients to sign using an uploaded signature.');
export const ZFindDocumentsRequestSchema = ZFindSearchParamsSchema.extend({
templateId: z
.number()
@ -233,6 +241,8 @@ export const ZCreateDocumentV2RequestSchema = z.object({
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
@ -249,36 +259,6 @@ export const ZCreateDocumentV2ResponseSchema = z.object({
),
});
export const ZUpdateDocumentRequestSchema = z.object({
documentId: z.number(),
data: z
.object({
title: ZDocumentTitleSchema.optional(),
externalId: ZDocumentExternalIdSchema.nullish(),
visibility: ZDocumentVisibilitySchema.optional(),
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullish(),
globalActionAuth: ZDocumentActionAuthTypesSchema.nullish(),
})
.optional(),
meta: z
.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
allowDictateNextSigner: z.boolean().optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
});
export const ZUpdateDocumentResponseSchema = ZDocumentLiteSchema;
export const ZSetFieldsForDocumentMutationSchema = z.object({
documentId: z.number(),
fields: z.array(

View File

@ -0,0 +1,52 @@
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
import { updateDocument } from '@documenso/lib/server-only/document/update-document';
import { authenticatedProcedure } from '../trpc';
import {
ZUpdateDocumentRequestSchema,
ZUpdateDocumentResponseSchema,
} from './update-document.types';
import { updateDocumentMeta } from './update-document.types';
/**
* Public route.
*/
export const updateDocumentRoute = authenticatedProcedure
.meta(updateDocumentMeta)
.input(ZUpdateDocumentRequestSchema)
.output(ZUpdateDocumentResponseSchema)
.mutation(async ({ input, ctx }) => {
const { teamId } = ctx;
const { documentId, data, meta = {} } = input;
const userId = ctx.user.id;
if (Object.values(meta).length > 0) {
await upsertDocumentMeta({
userId: ctx.user.id,
teamId,
documentId,
subject: meta.subject,
message: meta.message,
timezone: meta.timezone,
dateFormat: meta.dateFormat,
language: meta.language,
typedSignatureEnabled: meta.typedSignatureEnabled,
uploadSignatureEnabled: meta.uploadSignatureEnabled,
drawSignatureEnabled: meta.drawSignatureEnabled,
redirectUrl: meta.redirectUrl,
distributionMethod: meta.distributionMethod,
signingOrder: meta.signingOrder,
emailSettings: meta.emailSettings,
requestMetadata: ctx.metadata,
});
}
return await updateDocument({
userId,
teamId,
documentId,
data,
requestMetadata: ctx.metadata,
});
});

View File

@ -0,0 +1,68 @@
import { DocumentSigningOrder } from '@prisma/client';
// import type { OpenApiMeta } from 'trpc-to-openapi';
import { z } from 'zod';
import { ZDocumentLiteSchema } from '@documenso/lib/types/document';
import {
ZDocumentAccessAuthTypesSchema,
ZDocumentActionAuthTypesSchema,
} from '@documenso/lib/types/document-auth';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import type { TrpcRouteMeta } from '../trpc';
import {
ZDocumentExternalIdSchema,
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
ZDocumentMetaDrawSignatureEnabledSchema,
ZDocumentMetaLanguageSchema,
ZDocumentMetaMessageSchema,
ZDocumentMetaRedirectUrlSchema,
ZDocumentMetaSubjectSchema,
ZDocumentMetaTimezoneSchema,
ZDocumentMetaTypedSignatureEnabledSchema,
ZDocumentMetaUploadSignatureEnabledSchema,
ZDocumentTitleSchema,
ZDocumentVisibilitySchema,
} from './schema';
export const updateDocumentMeta: TrpcRouteMeta = {
openapi: {
method: 'POST',
path: '/document/update',
summary: 'Update document',
tags: ['Document'],
},
};
export const ZUpdateDocumentRequestSchema = z.object({
documentId: z.number(),
data: z
.object({
title: ZDocumentTitleSchema.optional(),
externalId: ZDocumentExternalIdSchema.nullish(),
visibility: ZDocumentVisibilitySchema.optional(),
globalAccessAuth: ZDocumentAccessAuthTypesSchema.nullish(),
globalActionAuth: ZDocumentActionAuthTypesSchema.nullish(),
})
.optional(),
meta: z
.object({
subject: ZDocumentMetaSubjectSchema.optional(),
message: ZDocumentMetaMessageSchema.optional(),
timezone: ZDocumentMetaTimezoneSchema.optional(),
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
allowDictateNextSigner: z.boolean().optional(),
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
emailSettings: ZDocumentEmailSettingsSchema.optional(),
})
.optional(),
});
export const ZUpdateDocumentResponseSchema = ZDocumentLiteSchema;

View File

@ -33,7 +33,6 @@ import { resendTeamEmailVerification } from '@documenso/lib/server-only/team/res
import { resendTeamMemberInvitation } from '@documenso/lib/server-only/team/resend-team-member-invitation';
import { updateTeam } from '@documenso/lib/server-only/team/update-team';
import { updateTeamBrandingSettings } from '@documenso/lib/server-only/team/update-team-branding-settings';
import { updateTeamDocumentSettings } from '@documenso/lib/server-only/team/update-team-document-settings';
import { updateTeamEmail } from '@documenso/lib/server-only/team/update-team-email';
import { updateTeamMember } from '@documenso/lib/server-only/team/update-team-member';
import { updateTeamPublicProfile } from '@documenso/lib/server-only/team/update-team-public-profile';
@ -66,12 +65,12 @@ import {
ZResendTeamEmailVerificationMutationSchema,
ZResendTeamMemberInvitationMutationSchema,
ZUpdateTeamBrandingSettingsMutationSchema,
ZUpdateTeamDocumentSettingsMutationSchema,
ZUpdateTeamEmailMutationSchema,
ZUpdateTeamMemberMutationSchema,
ZUpdateTeamMutationSchema,
ZUpdateTeamPublicProfileMutationSchema,
} from './schema';
import { updateTeamDocumentSettingsRoute } from './update-team-document-settings';
export const teamRouter = router({
// Internal endpoint for now.
@ -571,18 +570,7 @@ export const teamRouter = router({
return await getTeamPrices();
}),
// Internal endpoint. Use updateTeam instead.
updateTeamDocumentSettings: authenticatedProcedure
.input(ZUpdateTeamDocumentSettingsMutationSchema)
.mutation(async ({ ctx, input }) => {
const { teamId, settings } = input;
return await updateTeamDocumentSettings({
userId: ctx.user.id,
teamId,
settings,
});
}),
updateTeamDocumentSettings: updateTeamDocumentSettingsRoute,
// Internal endpoint for now.
acceptTeamInvitation: authenticatedProcedure

View File

@ -1,7 +1,6 @@
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
import { TeamMemberRole } from '@prisma/client';
import { z } from 'zod';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { PROTECTED_TEAM_URLS } from '@documenso/lib/constants/teams';
import { ZFindSearchParamsSchema } from '@documenso/lib/types/search-params';
@ -195,20 +194,6 @@ export const ZUpdateTeamBrandingSettingsMutationSchema = z.object({
}),
});
export const ZUpdateTeamDocumentSettingsMutationSchema = z.object({
teamId: z.number(),
settings: z.object({
documentVisibility: z
.nativeEnum(DocumentVisibility)
.optional()
.default(DocumentVisibility.EVERYONE),
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).optional().default('en'),
includeSenderDetails: z.boolean().optional().default(false),
typedSignatureEnabled: z.boolean().optional().default(true),
includeSigningCertificate: z.boolean().optional().default(true),
}),
});
export type TCreateTeamMutationSchema = z.infer<typeof ZCreateTeamMutationSchema>;
export type TCreateTeamEmailVerificationMutationSchema = z.infer<
typeof ZCreateTeamEmailVerificationMutationSchema
@ -247,6 +232,3 @@ export type TResendTeamMemberInvitationMutationSchema = z.infer<
export type TUpdateTeamBrandingSettingsMutationSchema = z.infer<
typeof ZUpdateTeamBrandingSettingsMutationSchema
>;
export type TUpdateTeamDocumentSettingsMutationSchema = z.infer<
typeof ZUpdateTeamDocumentSettingsMutationSchema
>;

View File

@ -0,0 +1,71 @@
import { TEAM_MEMBER_ROLE_PERMISSIONS_MAP } from '@documenso/lib/constants/teams';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import { prisma } from '@documenso/prisma';
import { authenticatedProcedure } from '../trpc';
import {
ZUpdateTeamDocumentSettingsRequestSchema,
ZUpdateTeamDocumentSettingsResponseSchema,
} from './update-team-document-settings.types';
/**
* Private route.
*/
export const updateTeamDocumentSettingsRoute = authenticatedProcedure
.input(ZUpdateTeamDocumentSettingsRequestSchema)
.output(ZUpdateTeamDocumentSettingsResponseSchema)
.mutation(async ({ ctx, input }) => {
const { user } = ctx;
const { teamId, settings } = input;
const {
documentVisibility,
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
} = settings;
const member = await prisma.teamMember.findFirst({
where: {
userId: user.id,
teamId,
role: {
in: TEAM_MEMBER_ROLE_PERMISSIONS_MAP.MANAGE_TEAM,
},
},
});
if (!member) {
throw new AppError(AppErrorCode.UNAUTHORIZED, {
message: 'You do not have permission to update this team.',
});
}
return await prisma.teamGlobalSettings.upsert({
where: {
teamId,
},
create: {
teamId,
documentVisibility,
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
},
update: {
documentVisibility,
documentLanguage,
includeSenderDetails,
includeSigningCertificate,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
},
});
});

View File

@ -0,0 +1,23 @@
import { z } from 'zod';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import TeamGlobalSettingsSchema from '@documenso/prisma/generated/zod/modelSchema/TeamGlobalSettingsSchema';
export const ZUpdateTeamDocumentSettingsRequestSchema = z.object({
teamId: z.number(),
settings: z.object({
documentVisibility: z
.nativeEnum(DocumentVisibility)
.optional()
.default(DocumentVisibility.EVERYONE),
documentLanguage: z.enum(SUPPORTED_LANGUAGE_CODES).optional().default('en'),
includeSenderDetails: z.boolean().optional().default(false),
includeSigningCertificate: z.boolean().optional().default(true),
typedSignatureEnabled: z.boolean().optional().default(true),
uploadSignatureEnabled: z.boolean().optional().default(true),
drawSignatureEnabled: z.boolean().optional().default(true),
}),
});
export const ZUpdateTeamDocumentSettingsResponseSchema = TeamGlobalSettingsSchema;

View File

@ -19,12 +19,14 @@ import { TemplateDirectLinkSchema } from '@documenso/prisma/generated/zod/modelS
import {
ZDocumentMetaDateFormatSchema,
ZDocumentMetaDistributionMethodSchema,
ZDocumentMetaDrawSignatureEnabledSchema,
ZDocumentMetaLanguageSchema,
ZDocumentMetaMessageSchema,
ZDocumentMetaRedirectUrlSchema,
ZDocumentMetaSubjectSchema,
ZDocumentMetaTimezoneSchema,
ZDocumentMetaTypedSignatureEnabledSchema,
ZDocumentMetaUploadSignatureEnabledSchema,
} from '../document-router/schema';
import { ZSignFieldWithTokenMutationSchema } from '../field-router/schema';
@ -164,6 +166,8 @@ export const ZUpdateTemplateRequestSchema = z.object({
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
language: ZDocumentMetaLanguageSchema.optional(),
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
uploadSignatureEnabled: ZDocumentMetaUploadSignatureEnabledSchema.optional(),
drawSignatureEnabled: ZDocumentMetaDrawSignatureEnabledSchema.optional(),
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
allowDictateNextSigner: z.boolean().optional(),
})

View File

@ -9,8 +9,8 @@ import { isAdmin } from '@documenso/lib/utils/is-admin';
import type { TrpcContext } from './context';
// Can't import type from trpc-to-openapi because it breaks nextjs build, not sure why.
type OpenApiMeta = {
// Can't import type from trpc-to-openapi because it breaks build, not sure why.
export type TrpcRouteMeta = {
openapi?: {
enabled?: boolean;
method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
@ -30,7 +30,7 @@ type OpenApiMeta = {
} & Record<string, unknown>;
const t = initTRPC
.meta<OpenApiMeta>()
.meta<TrpcRouteMeta>()
.context<TrpcContext>()
.create({
transformer: SuperJSON,

View File

@ -0,0 +1,58 @@
import { Trans } from '@lingui/react/macro';
import { InfoIcon } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
export const DocumentSignatureSettingsTooltip = () => {
return (
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
<h2>
<strong>
<Trans>Signature types</Trans>
</strong>
</h2>
<p>
<Trans>
The types of signatures that recipients are allowed to use when signing the document.
</Trans>
</p>
<ul className="ml-3.5 list-outside list-disc space-y-0.5 py-2">
<li>
<Trans>
<strong>
<Trans>Drawn</Trans>
</strong>
{' - '}
<Trans>A signature that is drawn using a mouse or stylus.</Trans>
</Trans>
</li>
<li>
<Trans>
<strong>
<Trans>Typed</Trans>
</strong>
{' - '}
<Trans>A signature that is typed using a keyboard.</Trans>
</Trans>
</li>
<li>
<Trans>
<strong>
<Trans>Uploaded</Trans>
</strong>
{' - '}
<Trans>A signature that is uploaded from a file.</Trans>
</Trans>
</li>
</ul>
</TooltipContent>
</Tooltip>
);
};

View File

@ -47,9 +47,8 @@ import { cn } from '../../lib/utils';
import { Alert, AlertDescription } from '../alert';
import { Button } from '../button';
import { Card, CardContent } from '../card';
import { Checkbox } from '../checkbox';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '../command';
import { Form, FormControl, FormField, FormItem, FormLabel } from '../form/form';
import { Form } from '../form/form';
import { Popover, PopoverContent, PopoverTrigger } from '../popover';
import { useStep } from '../stepper';
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
@ -94,7 +93,6 @@ export type AddFieldsFormProps = {
onSubmit: (_data: TAddFieldsFormSchema) => void;
canGoBack?: boolean;
isDocumentPdfLoaded: boolean;
typedSignatureEnabled?: boolean;
teamId?: number;
};
@ -106,7 +104,6 @@ export const AddFieldsFormPartial = ({
onSubmit,
canGoBack = false,
isDocumentPdfLoaded,
typedSignatureEnabled,
teamId,
}: AddFieldsFormProps) => {
const { toast } = useToast();
@ -137,7 +134,6 @@ export const AddFieldsFormPartial = ({
recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
})),
typedSignatureEnabled: typedSignatureEnabled ?? false,
},
});
@ -787,31 +783,6 @@ export const AddFieldsFormPartial = ({
)}
<Form {...form}>
<FormField
control={form.control}
name="typedSignatureEnabled"
render={({ field: { value, ...field } }) => (
<FormItem className="mb-6 flex flex-row items-center space-x-2 space-y-0">
<FormControl>
<Checkbox
{...field}
id="typedSignatureEnabled"
checked={value}
onCheckedChange={(checked) => field.onChange(checked)}
disabled={form.formState.isSubmitting}
/>
</FormControl>
<FormLabel
htmlFor="typedSignatureEnabled"
className="text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
<Trans>Enable Typed Signatures</Trans>
</FormLabel>
</FormItem>
)}
/>
<div className="-mx-2 flex-1 overflow-y-auto px-2">
<fieldset disabled={isFieldsDisabled} className="my-2 grid grid-cols-3 gap-4">
<button

View File

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

View File

@ -2,6 +2,7 @@ import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans } from '@lingui/react/macro';
import { useLingui } from '@lingui/react/macro';
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
import { DocumentStatus, type Field, type Recipient, SendStatus } from '@prisma/client';
import { InfoIcon } from 'lucide-react';
@ -9,10 +10,12 @@ import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { DOCUMENT_SIGNATURE_TYPES } from '@documenso/lib/constants/document';
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import type { TDocument } from '@documenso/lib/types/document';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { extractTeamSignatureSettings } from '@documenso/lib/utils/teams';
import {
DocumentGlobalAuthAccessSelect,
DocumentGlobalAuthAccessTooltip,
@ -39,7 +42,9 @@ import {
FormLabel,
FormMessage,
} from '@documenso/ui/primitives/form/form';
import { MultiSelectCombobox } from '@documenso/ui/primitives/multi-select-combobox';
import { DocumentSignatureSettingsTooltip } from '../../components/document/document-signature-settings-tooltip';
import { Combobox } from '../combobox';
import { Input } from '../input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select';
@ -78,6 +83,8 @@ export const AddSettingsFormPartial = ({
currentTeamMemberRole,
onSubmit,
}: AddSettingsFormProps) => {
const { t } = useLingui();
const { documentAuthOption } = extractDocumentAuthMethods({
documentAuth: document.authOptions,
});
@ -90,6 +97,7 @@ export const AddSettingsFormPartial = ({
visibility: document.visibility || '',
globalAccessAuth: documentAuthOption?.globalAccessAuth || undefined,
globalActionAuth: documentAuthOption?.globalActionAuth || undefined,
meta: {
timezone:
TIME_ZONES.find((timezone) => timezone === document.documentMeta?.timezone) ??
@ -99,6 +107,7 @@ export const AddSettingsFormPartial = ({
?.value ?? DEFAULT_DOCUMENT_DATE_FORMAT,
redirectUrl: document.documentMeta?.redirectUrl ?? '',
language: document.documentMeta?.language ?? 'en',
signatureTypes: extractTeamSignatureSettings(document.documentMeta),
},
},
});
@ -189,9 +198,11 @@ export const AddSettingsFormPartial = ({
</TooltipTrigger>
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
Controls the language for the document, including the language to be used
for email notifications, and the final certificate that is generated and
attached to the document.
<Trans>
Controls the language for the document, including the language to be used
for email notifications, and the final certificate that is generated and
attached to the document.
</Trans>
</TooltipContent>
</Tooltip>
</FormLabel>
@ -314,6 +325,34 @@ export const AddSettingsFormPartial = ({
)}
/>
<FormField
control={form.control}
name="meta.signatureTypes"
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
<Trans>Allowed Signature Types</Trans>
<DocumentSignatureSettingsTooltip />
</FormLabel>
<FormControl>
<MultiSelectCombobox
options={Object.values(DOCUMENT_SIGNATURE_TYPES).map((option) => ({
label: t(option.label),
value: option.value,
}))}
selectedValues={field.value}
onChange={field.onChange}
className="bg-background w-full"
emptySelectionPlaceholder="Select signature types"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="meta.dateFormat"

View File

@ -1,7 +1,9 @@
import { msg } from '@lingui/core/macro';
import { DocumentVisibility } from '@prisma/client';
import { z } from 'zod';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { DocumentSignatureType } from '@documenso/lib/constants/document';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import {
@ -26,7 +28,10 @@ export const ZMapNegativeOneToUndefinedSchema = z
});
export const ZAddSettingsFormSchema = z.object({
title: z.string().trim().min(1, { message: "Title can't be empty" }),
title: z
.string()
.trim()
.min(1, { message: msg`Title cannot be empty`.id }),
externalId: z.string().optional(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: ZMapNegativeOneToUndefinedSchema.pipe(
@ -49,6 +54,9 @@ export const ZAddSettingsFormSchema = z.object({
.union([z.string(), z.enum(SUPPORTED_LANGUAGE_CODES)])
.optional()
.default('en'),
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(1, {
message: msg`At least one signature type must be enabled`.id,
}),
}),
});

View File

@ -24,11 +24,14 @@ type ComboBoxOption<T = OptionValue> = {
type MultiSelectComboboxProps<T = OptionValue> = {
emptySelectionPlaceholder?: React.ReactElement | string;
enableClearAllButton?: boolean;
enableSearch?: boolean;
className?: string;
loading?: boolean;
inputPlaceholder?: MessageDescriptor;
onChange: (_values: T[]) => void;
options: ComboBoxOption<T>[];
selectedValues: T[];
testId?: string;
};
/**
@ -41,11 +44,14 @@ type MultiSelectComboboxProps<T = OptionValue> = {
export function MultiSelectCombobox<T = OptionValue>({
emptySelectionPlaceholder = 'Select values...',
enableClearAllButton,
enableSearch = true,
className,
inputPlaceholder,
loading,
onChange,
options,
selectedValues,
testId,
}: MultiSelectComboboxProps<T>) {
const { _ } = useLingui();
@ -59,8 +65,6 @@ export function MultiSelectCombobox<T = OptionValue>({
}
onChange(newSelectedOptions);
setOpen(false);
};
const selectedOptions = React.useMemo(() => {
@ -107,7 +111,8 @@ export function MultiSelectCombobox<T = OptionValue>({
role="combobox"
disabled={loading}
aria-expanded={open}
className="w-[200px] px-3"
className={cn('w-[200px] px-3', className)}
data-testid={testId}
>
<AnimatePresence>
{loading ? (
@ -146,7 +151,7 @@ export function MultiSelectCombobox<T = OptionValue>({
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder={inputPlaceholder && _(inputPlaceholder)} />
{enableSearch && <CommandInput placeholder={inputPlaceholder && _(inputPlaceholder)} />}
<CommandEmpty>
<Trans>No value found.</Trans>
</CommandEmpty>

View File

@ -1,3 +1,5 @@
import { SIGNATURE_CANVAS_DPI } from '@documenso/lib/constants/signatures';
import { Point } from './point';
export class Canvas {
@ -14,7 +16,7 @@ export class Canvas {
private lastVelocity = 0;
private readonly VELOCITY_FILTER_WEIGHT = 0.5;
private readonly DPI = 2;
private readonly DPI = SIGNATURE_CANVAS_DPI;
constructor(canvas: HTMLCanvasElement) {
this.$canvas = canvas;

View File

@ -0,0 +1,63 @@
import { Trans } from '@lingui/react/macro';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { cn } from '../../lib/utils';
export type SignaturePadColorPickerProps = {
selectedColor: string;
setSelectedColor: (color: string) => void;
className?: string;
};
export const SignaturePadColorPicker = ({
selectedColor,
setSelectedColor,
className,
}: SignaturePadColorPickerProps) => {
return (
<div className={cn('text-foreground absolute right-2 top-2 filter', className)}>
<Select defaultValue={selectedColor} onValueChange={(value) => setSelectedColor(value)}>
<SelectTrigger className="h-auto w-auto border-none p-0.5">
<SelectValue placeholder="" />
</SelectTrigger>
<SelectContent className="w-[100px]" align="end">
<SelectItem value="black">
<div className="text-muted-foreground flex items-center text-[0.688rem]">
<div className="border-border mr-1 h-4 w-4 rounded-full border-2 bg-black shadow-sm" />
<Trans>Black</Trans>
</div>
</SelectItem>
<SelectItem value="red">
<div className="text-muted-foreground flex items-center text-[0.688rem]">
<div className="border-border mr-1 h-4 w-4 rounded-full border-2 bg-[red] shadow-sm" />
<Trans>Red</Trans>
</div>
</SelectItem>
<SelectItem value="blue">
<div className="text-muted-foreground flex items-center text-[0.688rem]">
<div className="border-border mr-1 h-4 w-4 rounded-full border-2 bg-[blue] shadow-sm" />
<Trans>Blue</Trans>
</div>
</SelectItem>
<SelectItem value="green">
<div className="text-muted-foreground flex items-center text-[0.688rem]">
<div className="border-border mr-1 h-4 w-4 rounded-full border-2 bg-[green] shadow-sm" />
<Trans>Green</Trans>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
);
};

View File

@ -0,0 +1,150 @@
import type { HTMLAttributes } from 'react';
import { useState } from 'react';
import type { MessageDescriptor } from '@lingui/core';
import { Trans, useLingui } from '@lingui/react/macro';
import { motion } from 'framer-motion';
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
import { Dialog, DialogClose, DialogContent, DialogFooter } from '@documenso/ui/primitives/dialog';
import { cn } from '../../lib/utils';
import { Button } from '../button';
import { SignaturePad } from './signature-pad';
import { SignatureRender } from './signature-render';
export type SignaturePadDialogProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'onChange'> & {
disabled?: boolean;
value?: string;
onChange: (_value: string) => void;
dialogConfirmText?: MessageDescriptor | string;
disableAnimation?: boolean;
typedSignatureEnabled?: boolean;
uploadSignatureEnabled?: boolean;
drawSignatureEnabled?: boolean;
};
export const SignaturePadDialog = ({
className,
value,
onChange,
disabled = false,
disableAnimation = false,
typedSignatureEnabled,
uploadSignatureEnabled,
drawSignatureEnabled,
dialogConfirmText,
}: SignaturePadDialogProps) => {
const { i18n } = useLingui();
const [showSignatureModal, setShowSignatureModal] = useState(false);
const [signature, setSignature] = useState<string>(value ?? '');
return (
<div
className={cn(
'aspect-signature-pad bg-background relative block w-full select-none rounded-lg border',
className,
{
'pointer-events-none opacity-50': disabled,
},
)}
>
{value && (
<div className="inset-0 h-full w-full">
<SignatureRender value={value} />
</div>
)}
<motion.button
data-testid="signature-pad-dialog-button"
type="button"
disabled={disabled}
className="absolute inset-0 flex items-center justify-center bg-transparent"
onClick={() => setShowSignatureModal(true)}
whileHover="onHover"
>
{!value && !disableAnimation && (
<motion.svg
width="120"
height="120"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="text-muted-foreground/60"
variants={{
onHover: {
scale: 1.1,
transition: {
type: 'spring',
stiffness: 300,
damping: 12,
mass: 0.8,
restDelta: 0.001,
},
},
}}
>
<motion.path
d="M1.5 11H14.5M1.5 14C1.5 14 8.72 2 4.86938 2H4.875C2.01 2 1.97437 14.0694 8 6.51188V6.5C8 6.5 9 11.3631 11.5 7.52375V7.5C11.5 7.5 11.5 9 14.5 9"
stroke="currentColor"
strokeWidth="1.1"
strokeLinecap="round"
strokeLinejoin="round"
initial={{ pathLength: 0, opacity: 0 }}
animate={{
pathLength: 1,
opacity: 1,
transition: {
pathLength: {
duration: 2,
ease: 'easeInOut',
},
opacity: { duration: 0.6 },
},
}}
/>
</motion.svg>
)}
</motion.button>
<Dialog open={showSignatureModal} onOpenChange={disabled ? undefined : setShowSignatureModal}>
<DialogContent hideClose={true} className="p-6 pt-4">
<SignaturePad
id="signature"
value={value}
className={className}
disabled={disabled}
onChange={({ value }) => setSignature(value)}
typedSignatureEnabled={typedSignatureEnabled}
uploadSignatureEnabled={uploadSignatureEnabled}
drawSignatureEnabled={drawSignatureEnabled}
/>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="ghost">
<Trans>Cancel</Trans>
</Button>
</DialogClose>
<Button
type="button"
disabled={!signature}
onClick={() => {
onChange(signature);
setShowSignatureModal(false);
}}
>
{dialogConfirmText ? (
parseMessageDescriptor(i18n._, dialogConfirmText)
) : (
<Trans>Next</Trans>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
};

View File

@ -0,0 +1,327 @@
import type { MouseEvent, PointerEvent, RefObject, TouchEvent } from 'react';
import { useMemo, useRef, useState } from 'react';
import { Trans } from '@lingui/react/macro';
import { Undo2 } from 'lucide-react';
import type { StrokeOptions } from 'perfect-freehand';
import { getStroke } from 'perfect-freehand';
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
import {
SIGNATURE_CANVAS_DPI,
SIGNATURE_MIN_COVERAGE_THRESHOLD,
} from '@documenso/lib/constants/signatures';
import { cn } from '../../lib/utils';
import { getSvgPathFromStroke } from './helper';
import { Point } from './point';
import { SignaturePadColorPicker } from './signature-pad-color-picker';
const checkSignatureValidity = (element: RefObject<HTMLCanvasElement>) => {
if (!element.current) {
return false;
}
const ctx = element.current.getContext('2d');
if (!ctx) {
return false;
}
const imageData = ctx.getImageData(0, 0, element.current.width, element.current.height);
const data = imageData.data;
let filledPixels = 0;
const totalPixels = data.length / 4;
for (let i = 0; i < data.length; i += 4) {
if (data[i + 3] > 0) filledPixels++;
}
const filledPercentage = filledPixels / totalPixels;
const isValid = filledPercentage > SIGNATURE_MIN_COVERAGE_THRESHOLD;
return isValid;
};
export type SignaturePadDrawProps = {
className?: string;
value: string;
onChange: (_signatureDataUrl: string) => void;
};
export const SignaturePadDraw = ({
className,
value,
onChange,
...props
}: SignaturePadDrawProps) => {
const $el = useRef<HTMLCanvasElement>(null);
const $imageData = useRef<ImageData | null>(null);
const $fileInput = useRef<HTMLInputElement>(null);
const [isPressed, setIsPressed] = useState(false);
const [lines, setLines] = useState<Point[][]>([]);
const [currentLine, setCurrentLine] = useState<Point[]>([]);
const [isSignatureValid, setIsSignatureValid] = useState<boolean | null>(null);
const [selectedColor, setSelectedColor] = useState('black');
const perfectFreehandOptions = useMemo(() => {
const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10;
return {
size,
thinning: 0.25,
streamline: 0.5,
smoothing: 0.5,
end: {
taper: size * 2,
},
} satisfies StrokeOptions;
}, []);
const onMouseDown = (event: MouseEvent | PointerEvent | TouchEvent) => {
if (event.cancelable) {
event.preventDefault();
}
setIsPressed(true);
const point = Point.fromEvent(event, SIGNATURE_CANVAS_DPI, $el.current);
setCurrentLine([point]);
};
const onMouseMove = (event: MouseEvent | PointerEvent | TouchEvent) => {
if (event.cancelable) {
event.preventDefault();
}
if (!isPressed) {
return;
}
const point = Point.fromEvent(event, SIGNATURE_CANVAS_DPI, $el.current);
const lastPoint = currentLine[currentLine.length - 1];
if (lastPoint && point.distanceTo(lastPoint) > 5) {
setCurrentLine([...currentLine, point]);
// Update the canvas here to draw the lines
if ($el.current) {
const ctx = $el.current.getContext('2d');
if (ctx) {
ctx.restore();
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.fillStyle = selectedColor;
lines.forEach((line) => {
const pathData = new Path2D(
getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)),
);
ctx.fill(pathData);
});
const pathData = new Path2D(
getSvgPathFromStroke(getStroke([...currentLine, point], perfectFreehandOptions)),
);
ctx.fill(pathData);
}
}
}
};
const onMouseUp = (event: MouseEvent | PointerEvent | TouchEvent, addLine = true) => {
if (event.cancelable) {
event.preventDefault();
}
setIsPressed(false);
const point = Point.fromEvent(event, SIGNATURE_CANVAS_DPI, $el.current);
const newLines = [...lines];
if (addLine && currentLine.length > 0) {
newLines.push([...currentLine, point]);
setCurrentLine([]);
}
setLines(newLines);
if ($el.current && newLines.length > 0) {
const ctx = $el.current.getContext('2d');
if (ctx) {
ctx.restore();
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.fillStyle = selectedColor;
newLines.forEach((line) => {
const pathData = new Path2D(
getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)),
);
ctx.fill(pathData);
});
const isValidSignature = checkSignatureValidity($el);
setIsSignatureValid(isValidSignature);
if (isValidSignature) {
onChange?.($el.current.toDataURL());
}
ctx.save();
}
}
};
const onMouseEnter = (event: MouseEvent | PointerEvent | TouchEvent) => {
if (event.cancelable) {
event.preventDefault();
}
if ('buttons' in event && event.buttons === 1) {
onMouseDown(event);
}
};
const onMouseLeave = (event: MouseEvent | PointerEvent | TouchEvent) => {
if (event.cancelable) {
event.preventDefault();
}
if (isPressed) {
onMouseUp(event, true);
} else {
onMouseUp(event, false);
}
};
const onClearClick = () => {
if ($el.current) {
const ctx = $el.current.getContext('2d');
ctx?.clearRect(0, 0, $el.current.width, $el.current.height);
$imageData.current = null;
}
if ($fileInput.current) {
$fileInput.current.value = '';
}
onChange('');
setLines([]);
setCurrentLine([]);
setIsPressed(false);
};
const onUndoClick = () => {
if (lines.length === 0 || !$el.current) {
return;
}
const newLines = lines.slice(0, -1);
setLines(newLines);
// Clear and redraw the canvas
const ctx = $el.current.getContext('2d');
const { width, height } = $el.current;
ctx?.clearRect(0, 0, width, height);
if ($imageData.current) {
ctx?.putImageData($imageData.current, 0, 0);
}
newLines.forEach((line) => {
const pathData = new Path2D(getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)));
ctx?.fill(pathData);
});
onChange?.($el.current.toDataURL());
};
unsafe_useEffectOnce(() => {
if ($el.current) {
$el.current.width = $el.current.clientWidth * SIGNATURE_CANVAS_DPI;
$el.current.height = $el.current.clientHeight * SIGNATURE_CANVAS_DPI;
}
if ($el.current && value) {
const ctx = $el.current.getContext('2d');
const { width, height } = $el.current;
const img = new Image();
img.onload = () => {
ctx?.drawImage(img, 0, 0, Math.min(width, img.width), Math.min(height, img.height));
const defaultImageData = ctx?.getImageData(0, 0, width, height) || null;
$imageData.current = defaultImageData;
};
img.src = value;
}
});
return (
<div className={cn('h-full w-full', className)}>
<canvas
data-testid="signature-pad-draw"
ref={$el}
className={cn('h-full w-full', {
'dark:hue-rotate-180 dark:invert': selectedColor === 'black',
})}
style={{ touchAction: 'none' }}
onPointerMove={(event) => onMouseMove(event)}
onPointerDown={(event) => onMouseDown(event)}
onPointerUp={(event) => onMouseUp(event)}
onPointerLeave={(event) => onMouseLeave(event)}
onPointerEnter={(event) => onMouseEnter(event)}
{...props}
/>
<SignaturePadColorPicker selectedColor={selectedColor} setSelectedColor={setSelectedColor} />
<div className="absolute bottom-3 right-3 flex gap-2">
<button
type="button"
className="focus-visible:ring-ring ring-offset-background text-muted-foreground/60 hover:text-muted-foreground rounded-full p-0 text-[0.688rem] focus-visible:outline-none focus-visible:ring-2"
onClick={() => onClearClick()}
>
<Trans>Clear Signature</Trans>
</button>
</div>
{isSignatureValid === false && (
<div className="absolute bottom-4 left-4 flex gap-2">
<span className="text-destructive text-xs">
<Trans>Signature is too small</Trans>
</span>
</div>
)}
{isSignatureValid && lines.length > 0 && (
<div className="absolute bottom-4 left-4 flex gap-2">
<button
type="button"
title="undo"
className="focus-visible:ring-ring ring-offset-background text-muted-foreground/60 hover:text-muted-foreground rounded-full p-0 text-[0.688rem] focus-visible:outline-none focus-visible:ring-2"
onClick={onUndoClick}
>
<Undo2 className="h-4 w-4" />
<span className="sr-only">Undo</span>
</button>
</div>
)}
</div>
);
};

View File

@ -0,0 +1,29 @@
import { useState } from 'react';
import { cn } from '../../lib/utils';
export type SignaturePadTypeProps = {
className?: string;
value?: string;
onChange: (_value: string) => void;
};
export const SignaturePadType = ({ className, value, onChange }: SignaturePadTypeProps) => {
// Colors don't actually work for text.
const [selectedColor, setSelectedColor] = useState('black');
return (
<div className={cn('flex h-full w-full items-center justify-center', className)}>
<input
data-testid="signature-pad-type-input"
placeholder="Type your signature"
className="font-signature w-full bg-transparent px-4 text-center text-7xl text-black placeholder:text-4xl focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 dark:text-white"
// style={{ color: selectedColor }}
value={value}
onChange={(event) => onChange(event.target.value.trimStart())}
/>
{/* <SignaturePadColorPicker selectedColor={selectedColor} setSelectedColor={setSelectedColor} /> */}
</div>
);
};

View File

@ -0,0 +1,166 @@
import { useRef } from 'react';
import { Trans } from '@lingui/react/macro';
import { motion } from 'framer-motion';
import { UploadCloudIcon } from 'lucide-react';
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
import { SIGNATURE_CANVAS_DPI } from '@documenso/lib/constants/signatures';
import { cn } from '../../lib/utils';
const loadImage = async (file: File | undefined): Promise<HTMLImageElement> => {
if (!file) {
throw new Error('No file selected');
}
if (!file.type.startsWith('image/')) {
throw new Error('Invalid file type');
}
if (file.size > 5 * 1024 * 1024) {
throw new Error('Image size should be less than 5MB');
}
return new Promise((resolve, reject) => {
const img = new Image();
const objectUrl = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(objectUrl);
resolve(img);
};
img.onerror = () => {
URL.revokeObjectURL(objectUrl);
reject(new Error('Failed to load image'));
};
img.src = objectUrl;
});
};
const loadImageOntoCanvas = (
image: HTMLImageElement,
canvas: HTMLCanvasElement,
ctx: CanvasRenderingContext2D,
): ImageData => {
const scale = Math.min((canvas.width * 0.8) / image.width, (canvas.height * 0.8) / image.height);
const x = (canvas.width - image.width * scale) / 2;
const y = (canvas.height - image.height * scale) / 2;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(image, x, y, image.width * scale, image.height * scale);
ctx.restore();
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
return imageData;
};
export type SignaturePadUploadProps = {
className?: string;
value: string;
onChange: (_signatureDataUrl: string) => void;
};
export const SignaturePadUpload = ({
className,
value,
onChange,
...props
}: SignaturePadUploadProps) => {
const $el = useRef<HTMLCanvasElement>(null);
const $imageData = useRef<ImageData | null>(null);
const $fileInput = useRef<HTMLInputElement>(null);
const handleImageUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
try {
const img = await loadImage(event.target.files?.[0]);
if (!$el.current) return;
const ctx = $el.current.getContext('2d');
if (!ctx) return;
$imageData.current = loadImageOntoCanvas(img, $el.current, ctx);
onChange?.($el.current.toDataURL());
} catch (error) {
console.error(error);
}
};
unsafe_useEffectOnce(() => {
// Todo: Not really sure if this is required for uploaded images.
if ($el.current) {
$el.current.width = $el.current.clientWidth * SIGNATURE_CANVAS_DPI;
$el.current.height = $el.current.clientHeight * SIGNATURE_CANVAS_DPI;
}
if ($el.current && value) {
const ctx = $el.current.getContext('2d');
const { width, height } = $el.current;
const img = new Image();
img.onload = () => {
ctx?.drawImage(img, 0, 0, Math.min(width, img.width), Math.min(height, img.height));
const defaultImageData = ctx?.getImageData(0, 0, width, height) || null;
$imageData.current = defaultImageData;
};
img.src = value;
}
});
return (
<div className={cn('relative h-full w-full', className)}>
<canvas
data-testid="signature-pad-upload"
ref={$el}
className="h-full w-full dark:hue-rotate-180 dark:invert"
style={{ touchAction: 'none' }}
{...props}
/>
<input
ref={$fileInput}
type="file"
accept="image/*"
className="hidden"
onChange={handleImageUpload}
/>
<motion.button
className="absolute inset-0 flex h-full w-full items-center justify-center"
initial="initial"
animate="animate"
whileHover="hover"
onClick={() => $fileInput.current?.click()}
>
{!value && (
<motion.div>
<div className="text-muted-foreground flex flex-col items-center justify-center">
<div className="flex flex-col items-center">
<UploadCloudIcon className="h-8 w-8" />
<span className="text-lg font-semibold">
<Trans>Upload Signature</Trans>
</span>
</div>
</div>
</motion.div>
)}
</motion.button>
</div>
);
};

View File

@ -1,591 +1,199 @@
import type { HTMLAttributes, MouseEvent, PointerEvent, TouchEvent } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import type { HTMLAttributes } from 'react';
import { useState } from 'react';
import { Trans } from '@lingui/react/macro';
import { Undo2, Upload } from 'lucide-react';
import type { StrokeOptions } from 'perfect-freehand';
import { getStroke } from 'perfect-freehand';
import { KeyboardIcon, UploadCloudIcon } from 'lucide-react';
import { match } from 'ts-pattern';
import { unsafe_useEffectOnce } from '@documenso/lib/client-only/hooks/use-effect-once';
import { Input } from '@documenso/ui/primitives/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { DocumentSignatureType } from '@documenso/lib/constants/document';
import { isBase64Image } from '@documenso/lib/constants/signatures';
import { SignatureIcon } from '../../icons/signature';
import { cn } from '../../lib/utils';
import { getSvgPathFromStroke } from './helper';
import { Point } from './point';
import { SignaturePadDraw } from './signature-pad-draw';
import { SignaturePadType } from './signature-pad-type';
import { SignaturePadUpload } from './signature-pad-upload';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './signature-tabs';
const DPI = 2;
const isBase64Image = (value: string) => value.startsWith('data:image/png;base64,');
const loadImage = async (file: File | undefined): Promise<HTMLImageElement> => {
if (!file) {
throw new Error('No file selected');
}
if (!file.type.startsWith('image/')) {
throw new Error('Invalid file type');
}
if (file.size > 5 * 1024 * 1024) {
throw new Error('Image size should be less than 5MB');
}
return new Promise((resolve, reject) => {
const img = new Image();
const objectUrl = URL.createObjectURL(file);
img.onload = () => {
URL.revokeObjectURL(objectUrl);
resolve(img);
};
img.onerror = () => {
URL.revokeObjectURL(objectUrl);
reject(new Error('Failed to load image'));
};
img.src = objectUrl;
});
};
const loadImageOntoCanvas = (
image: HTMLImageElement,
canvas: HTMLCanvasElement,
ctx: CanvasRenderingContext2D,
): ImageData => {
const scale = Math.min((canvas.width * 0.8) / image.width, (canvas.height * 0.8) / image.height);
const x = (canvas.width - image.width * scale) / 2;
const y = (canvas.height - image.height * scale) / 2;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(image, x, y, image.width * scale, image.height * scale);
ctx.restore();
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
return imageData;
export type SignaturePadValue = {
type: DocumentSignatureType;
value: string;
};
export type SignaturePadProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'onChange'> & {
onChange?: (_signatureDataUrl: string | null) => void;
containerClassName?: string;
value?: string;
onChange?: (_value: SignaturePadValue) => void;
disabled?: boolean;
allowTypedSignature?: boolean;
defaultValue?: string;
typedSignatureEnabled?: boolean;
uploadSignatureEnabled?: boolean;
drawSignatureEnabled?: boolean;
onValidityChange?: (isValid: boolean) => void;
minCoverageThreshold?: number;
};
export const SignaturePad = ({
className,
containerClassName,
defaultValue,
value = '',
onChange,
disabled = false,
allowTypedSignature,
onValidityChange,
minCoverageThreshold = 0.01,
...props
typedSignatureEnabled = true,
uploadSignatureEnabled = true,
drawSignatureEnabled = true,
}: SignaturePadProps) => {
const $el = useRef<HTMLCanvasElement>(null);
const $imageData = useRef<ImageData | null>(null);
const $fileInput = useRef<HTMLInputElement>(null);
const [imageSignature, setImageSignature] = useState(isBase64Image(value) ? value : '');
const [drawSignature, setDrawSignature] = useState(isBase64Image(value) ? value : '');
const [typedSignature, setTypedSignature] = useState(isBase64Image(value) ? '' : value);
const [isPressed, setIsPressed] = useState(false);
const [lines, setLines] = useState<Point[][]>([]);
const [currentLine, setCurrentLine] = useState<Point[]>([]);
const [selectedColor, setSelectedColor] = useState('black');
const [typedSignature, setTypedSignature] = useState(
defaultValue && !isBase64Image(defaultValue) ? defaultValue : '',
/**
* This is cooked.
*
* Get the first enabled tab that has a signature if possible, otherwise just get
* the first enabled tab.
*/
const [tab, setTab] = useState(
((): 'draw' | 'text' | 'image' => {
// First passthrough to check to see if there's a signature for a given tab.
if (drawSignatureEnabled && drawSignature) {
return 'draw';
}
if (typedSignatureEnabled && typedSignature) {
return 'text';
}
if (uploadSignatureEnabled && imageSignature) {
return 'image';
}
// Second passthrough to just select the first avaliable tab.
if (drawSignatureEnabled) {
return 'draw';
}
if (typedSignatureEnabled) {
return 'text';
}
if (uploadSignatureEnabled) {
return 'image';
}
throw new Error('No signature enabled');
})(),
);
const perfectFreehandOptions = useMemo(() => {
const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10;
const onImageSignatureChange = (value: string) => {
setImageSignature(value);
return {
size,
thinning: 0.25,
streamline: 0.5,
smoothing: 0.5,
end: {
taper: size * 2,
},
} satisfies StrokeOptions;
}, []);
const checkSignatureValidity = () => {
if ($el.current) {
const ctx = $el.current.getContext('2d');
if (ctx) {
const imageData = ctx.getImageData(0, 0, $el.current.width, $el.current.height);
const data = imageData.data;
let filledPixels = 0;
const totalPixels = data.length / 4;
for (let i = 0; i < data.length; i += 4) {
if (data[i + 3] > 0) filledPixels++;
}
const filledPercentage = filledPixels / totalPixels;
const isValid = filledPercentage > minCoverageThreshold;
onValidityChange?.(isValid);
return isValid;
}
}
onChange?.({
type: DocumentSignatureType.UPLOAD,
value,
});
};
const onMouseDown = (event: MouseEvent | PointerEvent | TouchEvent) => {
if (event.cancelable) {
event.preventDefault();
}
const onDrawSignatureChange = (value: string) => {
setDrawSignature(value);
setIsPressed(true);
if (typedSignature) {
setTypedSignature('');
if ($el.current) {
const ctx = $el.current.getContext('2d');
ctx?.clearRect(0, 0, $el.current.width, $el.current.height);
}
}
const point = Point.fromEvent(event, DPI, $el.current);
setCurrentLine([point]);
onChange?.({
type: DocumentSignatureType.DRAW,
value,
});
};
const onMouseMove = (event: MouseEvent | PointerEvent | TouchEvent) => {
if (event.cancelable) {
event.preventDefault();
}
const onTypedSignatureChange = (value: string) => {
setTypedSignature(value);
if (!isPressed) {
onChange?.({
type: DocumentSignatureType.TYPE,
value,
});
};
const onTabChange = (value: 'draw' | 'text' | 'image') => {
if (disabled) {
return;
}
const point = Point.fromEvent(event, DPI, $el.current);
const lastPoint = currentLine[currentLine.length - 1];
setTab(value);
if (lastPoint && point.distanceTo(lastPoint) > 5) {
setCurrentLine([...currentLine, point]);
// Update the canvas here to draw the lines
if ($el.current) {
const ctx = $el.current.getContext('2d');
if (ctx) {
ctx.restore();
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.fillStyle = selectedColor;
lines.forEach((line) => {
const pathData = new Path2D(
getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)),
);
ctx.fill(pathData);
});
const pathData = new Path2D(
getSvgPathFromStroke(getStroke([...currentLine, point], perfectFreehandOptions)),
);
ctx.fill(pathData);
}
}
}
match(value)
.with('draw', () => {
onDrawSignatureChange(drawSignature);
})
.with('text', () => {
onTypedSignatureChange(typedSignature);
})
.with('image', () => {
onImageSignatureChange(imageSignature);
})
.exhaustive();
};
const onMouseUp = (event: MouseEvent | PointerEvent | TouchEvent, addLine = true) => {
if (event.cancelable) {
event.preventDefault();
}
setIsPressed(false);
const point = Point.fromEvent(event, DPI, $el.current);
const newLines = [...lines];
if (addLine && currentLine.length > 0) {
newLines.push([...currentLine, point]);
setCurrentLine([]);
}
setLines(newLines);
if ($el.current && newLines.length > 0) {
const ctx = $el.current.getContext('2d');
if (ctx) {
ctx.restore();
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.fillStyle = selectedColor;
newLines.forEach((line) => {
const pathData = new Path2D(
getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)),
);
ctx.fill(pathData);
});
const isValidSignature = checkSignatureValidity();
if (isValidSignature) {
onChange?.($el.current.toDataURL());
}
ctx.save();
}
}
};
const onMouseEnter = (event: MouseEvent | PointerEvent | TouchEvent) => {
if (event.cancelable) {
event.preventDefault();
}
if ('buttons' in event && event.buttons === 1) {
onMouseDown(event);
}
};
const onMouseLeave = (event: MouseEvent | PointerEvent | TouchEvent) => {
if (event.cancelable) {
event.preventDefault();
}
if (isPressed) {
onMouseUp(event, true);
} else {
onMouseUp(event, false);
}
};
const onClearClick = () => {
if ($el.current) {
const ctx = $el.current.getContext('2d');
ctx?.clearRect(0, 0, $el.current.width, $el.current.height);
$imageData.current = null;
}
if ($fileInput.current) {
$fileInput.current.value = '';
}
onChange?.(null);
setTypedSignature('');
setLines([]);
setCurrentLine([]);
setIsPressed(false);
};
const renderTypedSignature = () => {
if ($el.current && typedSignature) {
const ctx = $el.current.getContext('2d');
if (ctx) {
const canvasWidth = $el.current.width;
const canvasHeight = $el.current.height;
const fontFamily = 'Caveat';
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = selectedColor;
// Calculate the desired width (25ch)
const desiredWidth = canvasWidth * 0.85; // 85% of canvas width
// Start with a base font size
let fontSize = 18;
ctx.font = `${fontSize}px ${fontFamily}`;
// Measure 10 characters and calculate scale factor
const characterWidth = ctx.measureText('m'.repeat(10)).width;
const scaleFactor = desiredWidth / characterWidth;
// Apply scale factor to font size
fontSize = fontSize * scaleFactor;
// Adjust font size if it exceeds canvas width
ctx.font = `${fontSize}px ${fontFamily}`;
const textWidth = ctx.measureText(typedSignature).width;
if (textWidth > desiredWidth) {
fontSize = fontSize * (desiredWidth / textWidth);
}
// Set final font and render text
ctx.font = `${fontSize}px ${fontFamily}`;
ctx.fillText(typedSignature, canvasWidth / 2, canvasHeight / 2);
}
}
};
const handleTypedSignatureChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
// Deny input while drawing.
if (isPressed) {
return;
}
if (lines.length > 0) {
setLines([]);
setCurrentLine([]);
}
setTypedSignature(newValue);
if ($el.current) {
const ctx = $el.current.getContext('2d');
ctx?.clearRect(0, 0, $el.current.width, $el.current.height);
}
if (newValue.trim() !== '') {
onChange?.(newValue);
onValidityChange?.(true);
} else {
onChange?.(null);
onValidityChange?.(false);
}
};
const handleImageUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
try {
const img = await loadImage(event.target.files?.[0]);
if (!$el.current) return;
const ctx = $el.current.getContext('2d');
if (!ctx) return;
$imageData.current = loadImageOntoCanvas(img, $el.current, ctx);
onChange?.($el.current.toDataURL());
setLines([]);
setCurrentLine([]);
setTypedSignature('');
} catch (error) {
console.error(error);
}
};
useEffect(() => {
if (typedSignature.trim() !== '' && !isBase64Image(typedSignature)) {
renderTypedSignature();
onChange?.(typedSignature);
}
}, [typedSignature, selectedColor]);
const onUndoClick = () => {
if (lines.length === 0 && typedSignature.length === 0) {
return;
}
if (typedSignature.length > 0) {
const newTypedSignature = typedSignature.slice(0, -1);
setTypedSignature(newTypedSignature);
// You might want to call onChange here as well
// onChange?.(newTypedSignature);
} else {
const newLines = lines.slice(0, -1);
setLines(newLines);
// Clear and redraw the canvas
if ($el.current) {
const ctx = $el.current.getContext('2d');
const { width, height } = $el.current;
ctx?.clearRect(0, 0, width, height);
if (typeof defaultValue === 'string' && $imageData.current) {
ctx?.putImageData($imageData.current, 0, 0);
}
newLines.forEach((line) => {
const pathData = new Path2D(
getSvgPathFromStroke(getStroke(line, perfectFreehandOptions)),
);
ctx?.fill(pathData);
});
onChange?.($el.current.toDataURL());
}
}
};
useEffect(() => {
if ($el.current) {
$el.current.width = $el.current.clientWidth * DPI;
$el.current.height = $el.current.clientHeight * DPI;
}
if (defaultValue && typedSignature) {
renderTypedSignature();
}
}, []);
unsafe_useEffectOnce(() => {
if ($el.current && typeof defaultValue === 'string') {
const ctx = $el.current.getContext('2d');
const { width, height } = $el.current;
const img = new Image();
img.onload = () => {
ctx?.drawImage(img, 0, 0, Math.min(width, img.width), Math.min(height, img.height));
const defaultImageData = ctx?.getImageData(0, 0, width, height) || null;
$imageData.current = defaultImageData;
};
img.src = defaultValue;
}
});
if (!drawSignatureEnabled && !typedSignatureEnabled && !uploadSignatureEnabled) {
return null;
}
return (
<div
className={cn('relative block select-none', containerClassName, {
'pointer-events-none opacity-50': disabled,
<Tabs
defaultValue={tab}
className={cn({
'pointer-events-none': disabled,
})}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
onValueChange={(value) => onTabChange(value as 'draw' | 'text' | 'image')}
>
<canvas
data-testid="signature-pad"
ref={$el}
className={cn(
'relative block',
{
'dark:hue-rotate-180 dark:invert': selectedColor === 'black',
},
className,
<TabsList>
{drawSignatureEnabled && (
<TabsTrigger value="draw">
<SignatureIcon className="mr-2 size-4" />
Draw
</TabsTrigger>
)}
style={{ touchAction: 'none' }}
onPointerMove={(event) => onMouseMove(event)}
onPointerDown={(event) => onMouseDown(event)}
onPointerUp={(event) => onMouseUp(event)}
onPointerLeave={(event) => onMouseLeave(event)}
onPointerEnter={(event) => onMouseEnter(event)}
{...props}
/>
{allowTypedSignature && (
<div
className={cn('ml-4 pb-1', {
'ml-10': lines.length > 0 || typedSignature.length > 0,
})}
>
<Input
placeholder="Type your signature"
className="w-1/2 border-none p-0 focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0"
value={typedSignature}
onChange={handleTypedSignatureChange}
/>
</div>
)}
{typedSignatureEnabled && (
<TabsTrigger value="text">
<KeyboardIcon className="mr-2 size-4" />
Type
</TabsTrigger>
)}
<div className="text-foreground absolute left-3 top-3 filter">
<div
className="focus-visible:ring-ring ring-offset-background text-muted-foreground/60 hover:text-muted-foreground flex cursor-pointer flex-row gap-2 rounded-full p-0 text-[0.688rem] focus-visible:outline-none focus-visible:ring-2"
onClick={() => $fileInput.current?.click()}
>
<Input
ref={$fileInput}
type="file"
accept="image/*"
className="hidden"
onChange={handleImageUpload}
disabled={disabled}
/>
<Upload className="h-4 w-4" />
<span>
<Trans>Upload Signature</Trans>
</span>
</div>
</div>
{uploadSignatureEnabled && (
<TabsTrigger value="image">
<UploadCloudIcon className="mr-2 size-4" />
Upload
</TabsTrigger>
)}
</TabsList>
<div className="text-foreground absolute right-2 top-2 filter">
<Select defaultValue={selectedColor} onValueChange={(value) => setSelectedColor(value)}>
<SelectTrigger className="h-auto w-auto border-none p-0.5">
<SelectValue placeholder="" />
</SelectTrigger>
<TabsContent
value="draw"
className="border-border aspect-signature-pad dark:bg-background relative flex items-center justify-center rounded-md border bg-neutral-50 text-center"
>
<SignaturePadDraw
className="h-full w-full"
onChange={onDrawSignatureChange}
value={drawSignature}
/>
</TabsContent>
<SelectContent className="w-[100px]" align="end">
<SelectItem value="black">
<div className="text-muted-foreground flex items-center text-[0.688rem]">
<div className="border-border mr-1 h-4 w-4 rounded-full border-2 bg-black shadow-sm" />
<Trans>Black</Trans>
</div>
</SelectItem>
<TabsContent
value="text"
className="border-border aspect-signature-pad dark:bg-background relative flex items-center justify-center rounded-md border bg-neutral-50 text-center"
>
<SignaturePadType value={typedSignature} onChange={onTypedSignatureChange} />
</TabsContent>
<SelectItem value="red">
<div className="text-muted-foreground flex items-center text-[0.688rem]">
<div className="border-border mr-1 h-4 w-4 rounded-full border-2 bg-[red] shadow-sm" />
<Trans>Red</Trans>
</div>
</SelectItem>
<SelectItem value="blue">
<div className="text-muted-foreground flex items-center text-[0.688rem]">
<div className="border-border mr-1 h-4 w-4 rounded-full border-2 bg-[blue] shadow-sm" />
<Trans>Blue</Trans>
</div>
</SelectItem>
<SelectItem value="green">
<div className="text-muted-foreground flex items-center text-[0.688rem]">
<div className="border-border mr-1 h-4 w-4 rounded-full border-2 bg-[green] shadow-sm" />
<Trans>Green</Trans>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="absolute bottom-3 right-3 flex gap-2">
<button
type="button"
className="focus-visible:ring-ring ring-offset-background text-muted-foreground/60 hover:text-muted-foreground rounded-full p-0 text-[0.688rem] focus-visible:outline-none focus-visible:ring-2"
onClick={() => onClearClick()}
>
<Trans>Clear Signature</Trans>
</button>
</div>
{(lines.length > 0 || typedSignature.length > 0) && (
<div className="absolute bottom-4 left-4 flex gap-2">
<button
type="button"
title="undo"
className="focus-visible:ring-ring ring-offset-background text-muted-foreground/60 hover:text-muted-foreground rounded-full p-0 text-[0.688rem] focus-visible:outline-none focus-visible:ring-2"
onClick={onUndoClick}
>
<Undo2 className="h-4 w-4" />
<span className="sr-only">Undo</span>
</button>
</div>
)}
</div>
<TabsContent
value="image"
className={cn(
'border-border aspect-signature-pad dark:bg-background relative rounded-md border bg-neutral-50',
{
'bg-white': imageSignature,
},
)}
>
<SignaturePadUpload value={imageSignature} onChange={onImageSignatureChange} />
</TabsContent>
</Tabs>
);
};

View File

@ -0,0 +1,128 @@
import { useEffect, useRef } from 'react';
import { SIGNATURE_CANVAS_DPI, isBase64Image } from '@documenso/lib/constants/signatures';
import { cn } from '../../lib/utils';
export type SignatureRenderProps = {
className?: string;
value: string;
};
/**
* Renders a typed, uploaded or drawn signature.
*/
export const SignatureRender = ({ className, value }: SignatureRenderProps) => {
const $el = useRef<HTMLCanvasElement>(null);
const $imageData = useRef<ImageData | null>(null);
const renderTypedSignature = () => {
if (!$el.current) {
return;
}
const ctx = $el.current.getContext('2d');
if (!ctx) {
return;
}
ctx.clearRect(0, 0, $el.current.width, $el.current.height);
const canvasWidth = $el.current.width;
const canvasHeight = $el.current.height;
const fontFamily = 'Caveat';
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// ctx.fillStyle = selectedColor; // Todo: Color not implemented...
// Calculate the desired width (25ch)
const desiredWidth = canvasWidth * 0.85; // 85% of canvas width
// Start with a base font size
let fontSize = 18;
ctx.font = `${fontSize}px ${fontFamily}`;
// Measure 10 characters and calculate scale factor
const characterWidth = ctx.measureText('m'.repeat(10)).width;
const scaleFactor = desiredWidth / characterWidth;
// Apply scale factor to font size
fontSize = fontSize * scaleFactor;
// Adjust font size if it exceeds canvas width
ctx.font = `${fontSize}px ${fontFamily}`;
const textWidth = ctx.measureText(value).width;
if (textWidth > desiredWidth) {
fontSize = fontSize * (desiredWidth / textWidth);
}
// Set final font and render text
ctx.font = `${fontSize}px ${fontFamily}`;
ctx.fillText(value, canvasWidth / 2, canvasHeight / 2);
};
const renderImageSignature = () => {
if (!$el.current || typeof value !== 'string') {
return;
}
const ctx = $el.current.getContext('2d');
if (!ctx) {
return;
}
ctx.clearRect(0, 0, $el.current.width, $el.current.height);
const { width, height } = $el.current;
const img = new Image();
img.onload = () => {
// Calculate the scaled dimensions while maintaining aspect ratio
const scale = Math.min(width / img.width, height / img.height);
const scaledWidth = img.width * scale;
const scaledHeight = img.height * scale;
// Calculate center position
const x = (width - scaledWidth) / 2;
const y = (height - scaledHeight) / 2;
ctx?.drawImage(img, x, y, scaledWidth, scaledHeight);
const defaultImageData = ctx?.getImageData(0, 0, width, height) || null;
$imageData.current = defaultImageData;
};
img.src = value;
};
useEffect(() => {
if ($el.current) {
$el.current.width = $el.current.clientWidth * SIGNATURE_CANVAS_DPI;
$el.current.height = $el.current.clientHeight * SIGNATURE_CANVAS_DPI;
}
}, []);
useEffect(() => {
if (isBase64Image(value)) {
renderImageSignature();
} else {
renderTypedSignature();
}
}, [value]);
return (
<canvas
ref={$el}
className={cn('h-full w-full dark:hue-rotate-180 dark:invert', className)}
style={{ touchAction: 'none' }}
/>
);
};

View File

@ -0,0 +1,147 @@
import * as React from 'react';
import { motion } from 'framer-motion';
import { cn } from '../../lib/utils';
interface TabsProps extends React.HTMLAttributes<HTMLDivElement> {
defaultValue?: string;
value?: string;
onValueChange?: (value: string) => void;
children: React.ReactNode;
}
interface TabsContextValue {
value: string;
onValueChange: (value: string) => void;
}
const TabsContext = React.createContext<TabsContextValue | undefined>(undefined);
function useTabs() {
const context = React.useContext(TabsContext);
if (!context) {
throw new Error('useTabs must be used within a Tabs provider');
}
return context;
}
export function Tabs({
defaultValue,
value,
onValueChange,
children,
className,
...props
}: TabsProps) {
const [tabValue, setTabValue] = React.useState(defaultValue || '');
const handleValueChange = React.useCallback(
(newValue: string) => {
setTabValue(newValue);
onValueChange?.(newValue);
},
[onValueChange],
);
const contextValue = React.useMemo(
() => ({
value: value !== undefined ? value : tabValue,
onValueChange: handleValueChange,
}),
[value, tabValue, handleValueChange],
);
return (
<TabsContext.Provider value={contextValue}>
<div className={cn('w-full', className)} {...props}>
{children}
</div>
</TabsContext.Provider>
);
}
interface TabsListProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
export function TabsList({ children, className, ...props }: TabsListProps) {
return (
<div
className={cn('border-border flex flex-wrap border-b', className)}
role="tabslist"
{...props}
>
{children}
</div>
);
}
interface TabsTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
value: string;
icon?: React.ReactNode;
children: React.ReactNode;
}
export function TabsTrigger({ value, icon, children, className, ...props }: TabsTriggerProps) {
const { value: selectedValue, onValueChange } = useTabs();
const isSelected = selectedValue === value;
return (
<button
role="tab"
type="button"
aria-selected={isSelected}
data-state={isSelected ? 'active' : 'inactive'}
onClick={() => onValueChange(value)}
className={cn(
'relative flex items-center px-4 py-3 text-sm font-medium transition-all',
'focus-visible:ring-ring focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
isSelected ? 'text-foreground' : 'text-muted-foreground hover:text-foreground',
className,
)}
{...props}
>
{icon && <span className="flex items-center">{icon}</span>}
{children}
{isSelected && (
<motion.div
layoutId="activeTabIndicator"
className="bg-foreground/40 absolute bottom-0 left-0 h-0.5 w-full rounded-full"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{
type: 'spring',
stiffness: 500,
damping: 50,
}}
/>
)}
</button>
);
}
interface TabsContentProps extends React.HTMLAttributes<HTMLDivElement> {
value: string;
children: React.ReactNode;
}
export function TabsContent({ value, children, className, ...props }: TabsContentProps) {
const { value: selectedValue } = useTabs();
const isSelected = selectedValue === value;
if (!isSelected) {
return null;
}
return (
<div
role="tabpanel"
data-state={isSelected ? 'active' : 'inactive'}
className={cn('mt-4', className)}
{...props}
>
{children}
</div>
);
}

View File

@ -54,10 +54,9 @@ import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitive
import { useToast } from '@documenso/ui/primitives/use-toast';
import { getSignerColorStyles, useSignerColors } from '../../lib/signer-colors';
import { Checkbox } from '../checkbox';
import type { FieldFormType } from '../document-flow/add-fields';
import { FieldAdvancedSettings } from '../document-flow/field-item-advanced-settings';
import { Form, FormControl, FormField, FormItem, FormLabel } from '../form/form';
import { Form } from '../form/form';
import { useStep } from '../stepper';
import type { TAddTemplateFieldsFormSchema } from './add-template-fields.types';
@ -74,7 +73,6 @@ export type AddTemplateFieldsFormProps = {
fields: Field[];
onSubmit: (_data: TAddTemplateFieldsFormSchema) => void;
teamId?: number;
typedSignatureEnabled?: boolean;
};
export const AddTemplateFieldsFormPartial = ({
@ -84,7 +82,6 @@ export const AddTemplateFieldsFormPartial = ({
fields,
onSubmit,
teamId,
typedSignatureEnabled,
}: AddTemplateFieldsFormProps) => {
const { _ } = useLingui();
const { toast } = useToast();
@ -119,7 +116,6 @@ export const AddTemplateFieldsFormPartial = ({
recipients.find((recipient) => recipient.id === field.recipientId)?.token ?? '',
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
})),
typedSignatureEnabled: typedSignatureEnabled ?? false,
},
});
@ -483,12 +479,6 @@ export const AddTemplateFieldsFormPartial = ({
form.setValue('fields', updatedFields);
};
const isTypedSignatureEnabled = form.watch('typedSignatureEnabled');
const handleTypedSignatureChange = (value: boolean) => {
form.setValue('typedSignatureEnabled', value, { shouldDirty: true });
};
return (
<>
{showAdvancedSettings && currentField ? (
@ -662,31 +652,6 @@ export const AddTemplateFieldsFormPartial = ({
)}
<Form {...form}>
<FormField
control={form.control}
name="typedSignatureEnabled"
render={({ field: { value, ...field } }) => (
<FormItem className="mb-6 flex flex-row items-center space-x-2 space-y-0">
<FormControl>
<Checkbox
{...field}
id="typedSignatureEnabled"
checked={value}
onCheckedChange={(checked) => field.onChange(checked)}
disabled={form.formState.isSubmitting}
/>
</FormControl>
<FormLabel
htmlFor="typedSignatureEnabled"
className="text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
<Trans>Enable Typed Signatures</Trans>
</FormLabel>
</FormItem>
)}
/>
<div className="-mx-2 flex-1 overflow-y-auto px-2">
<fieldset className="my-2 grid grid-cols-3 gap-4">
<button

View File

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

View File

@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { useLingui } from '@lingui/react';
import { useLingui } from '@lingui/react/macro';
import { Trans } from '@lingui/react/macro';
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
import { DocumentDistributionMethod, type Field, type Recipient } from '@prisma/client';
@ -10,12 +10,16 @@ import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { DOCUMENT_DISTRIBUTION_METHODS } from '@documenso/lib/constants/document';
import {
DOCUMENT_DISTRIBUTION_METHODS,
DOCUMENT_SIGNATURE_TYPES,
} from '@documenso/lib/constants/document';
import { SUPPORTED_LANGUAGES } from '@documenso/lib/constants/i18n';
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import type { TTemplate } from '@documenso/lib/types/template';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { extractTeamSignatureSettings } from '@documenso/lib/utils/teams';
import type { TDocumentMetaDateFormat } from '@documenso/trpc/server/document-router/schema';
import {
DocumentGlobalAuthAccessSelect,
@ -46,6 +50,7 @@ import {
} from '@documenso/ui/primitives/form/form';
import { DocumentEmailCheckboxes } from '../../components/document/document-email-checkboxes';
import { DocumentSignatureSettingsTooltip } from '../../components/document/document-signature-settings-tooltip';
import { Combobox } from '../combobox';
import {
DocumentFlowFormContainerActions,
@ -57,6 +62,7 @@ import {
import { ShowFieldItem } from '../document-flow/show-field-item';
import type { DocumentFlowStep } from '../document-flow/types';
import { Input } from '../input';
import { MultiSelectCombobox } from '../multi-select-combobox';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../select';
import { useStep } from '../stepper';
import { Textarea } from '../textarea';
@ -85,7 +91,7 @@ export const AddTemplateSettingsFormPartial = ({
currentTeamMemberRole,
onSubmit,
}: AddTemplateSettingsFormProps) => {
const { _ } = useLingui();
const { t, i18n } = useLingui();
const { documentAuthOption } = extractDocumentAuthMethods({
documentAuth: template.authOptions,
@ -111,6 +117,7 @@ export const AddTemplateSettingsFormPartial = ({
redirectUrl: template.templateMeta?.redirectUrl ?? '',
language: template.templateMeta?.language ?? 'en',
emailSettings: ZDocumentEmailSettingsSchema.parse(template?.templateMeta?.emailSettings),
signatureTypes: extractTeamSignatureSettings(template?.templateMeta),
},
},
});
@ -314,7 +321,7 @@ export const AddTemplateSettingsFormPartial = ({
{Object.values(DOCUMENT_DISTRIBUTION_METHODS).map(
({ value, description }) => (
<SelectItem key={value} value={value}>
{_(description)}
{i18n._(description)}
</SelectItem>
),
)}
@ -325,6 +332,34 @@ export const AddTemplateSettingsFormPartial = ({
)}
/>
<FormField
control={form.control}
name="meta.signatureTypes"
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
<Trans>Allowed Signature Types</Trans>
<DocumentSignatureSettingsTooltip />
</FormLabel>
<FormControl>
<MultiSelectCombobox
options={Object.values(DOCUMENT_SIGNATURE_TYPES).map((option) => ({
label: t(option.label),
value: option.value,
}))}
selectedValues={field.value}
onChange={field.onChange}
className="bg-background w-full"
emptySelectionPlaceholder="Select signature types"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{isEnterprise && (
<FormField
control={form.control}

View File

@ -1,8 +1,10 @@
import { msg } from '@lingui/core/macro';
import { DocumentDistributionMethod } from '@prisma/client';
import { DocumentVisibility } from '@prisma/client';
import { z } from 'zod';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
import { DocumentSignatureType } from '@documenso/lib/constants/document';
import { SUPPORTED_LANGUAGE_CODES } from '@documenso/lib/constants/i18n';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import {
@ -49,6 +51,9 @@ export const ZAddTemplateSettingsFormSchema = z.object({
.optional()
.default('en'),
emailSettings: ZDocumentEmailSettingsSchema,
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(1, {
message: msg`At least one signature type must be enabled`.id,
}),
}),
});