chore: merged main

This commit is contained in:
Catalin Pit
2024-09-11 08:40:23 +03:00
389 changed files with 19630 additions and 4241 deletions

View File

@ -1,5 +1,7 @@
import * as React from 'react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { Check, ChevronDown } from 'lucide-react';
import { cn } from '../lib/utils';
@ -24,6 +26,8 @@ const Combobox = ({
disabled = false,
placeholder,
}: ComboboxProps) => {
const { _ } = useLingui();
const [open, setOpen] = React.useState(false);
const onOptionSelected = (newValue: string) => {
@ -31,7 +35,7 @@ const Combobox = ({
setOpen(false);
};
const placeholderValue = placeholder ?? 'Select an option';
const placeholderValue = placeholder ?? _(msg`Select an option`);
return (
<Popover open={open} onOpenChange={setOpen}>
@ -52,7 +56,9 @@ const Combobox = ({
<Command>
<CommandInput placeholder={value || placeholderValue} />
<CommandEmpty>No value found.</CommandEmpty>
<CommandEmpty>
<Trans>No value found.</Trans>
</CommandEmpty>
<CommandGroup className="max-h-[250px] overflow-y-auto">
{options.map((option, index) => (

View File

@ -1,3 +1,4 @@
import { Plural, Trans } from '@lingui/macro';
import type { Table } from '@tanstack/react-table';
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
import { match } from 'ts-pattern';
@ -26,8 +27,10 @@ export function DataTablePagination<TData>({
{match(additionalInformation)
.with('SelectedCount', () => (
<span>
{table.getFilteredSelectedRowModel().rows.length} of{' '}
{table.getFilteredRowModel().rows.length} row(s) selected.
<Trans>
{table.getFilteredSelectedRowModel().rows.length} of{' '}
{table.getFilteredRowModel().rows.length} row(s) selected.
</Trans>
</span>
))
.with('VisibleCount', () => {
@ -35,7 +38,11 @@ export function DataTablePagination<TData>({
return (
<span data-testid="data-table-count">
Showing {visibleRows} result{visibleRows > 1 && 's'}.
<Plural
value={visibleRows}
one={`Showing # result.`}
other={`Showing # results.`}
/>
</span>
);
})
@ -44,7 +51,9 @@ export function DataTablePagination<TData>({
</div>
<div className="flex items-center gap-x-2">
<p className="whitespace-nowrap text-sm font-medium">Rows per page</p>
<p className="whitespace-nowrap text-sm font-medium">
<Trans>Rows per page</Trans>
</p>
<Select
value={`${table.getState().pagination.pageSize}`}
onValueChange={(value) => {
@ -65,7 +74,9 @@ export function DataTablePagination<TData>({
</div>
<div className="flex flex-wrap items-center gap-x-6 gap-y-4 lg:gap-x-8">
<div className="flex items-center text-sm font-medium md:justify-center">
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount() || 1}
<Trans>
Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount() || 1}
</Trans>
</div>
<div className="flex items-center gap-x-2">

View File

@ -2,6 +2,7 @@
import React, { useMemo } from 'react';
import { Trans } from '@lingui/macro';
import type {
ColumnDef,
PaginationState,
@ -16,6 +17,8 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '.
export type DataTableChildren<TData> = (_table: TTable<TData>) => React.ReactNode;
export type { ColumnDef as DataTableColumnDef } from '@tanstack/react-table';
export interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
columnVisibility?: VisibilityState;
@ -130,7 +133,7 @@ export function DataTable<TData, TValue>({
<TableRow>
{error.component ?? (
<TableCell colSpan={columns.length} className="h-32 text-center">
Something went wrong.
<Trans>Something went wrong.</Trans>
</TableCell>
)}
</TableRow>
@ -141,14 +144,16 @@ export function DataTable<TData, TValue>({
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-32 text-center">
<p>No results found</p>
<p>
<Trans>No results found</Trans>
</p>
{hasFilters && onClearFilters !== undefined && (
<button
onClick={() => onClearFilters()}
className="text-foreground mt-1 text-sm"
>
Clear filters
<Trans>Clear filters</Trans>
</button>
)}
</TableCell>

View File

@ -2,6 +2,9 @@
import Link from 'next/link';
import type { MessageDescriptor } from '@lingui/core';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { motion } from 'framer-motion';
import { AlertTriangle, Plus } from 'lucide-react';
import { useDropzone } from 'react-dropzone';
@ -25,7 +28,7 @@ import { Card, CardContent } from './card';
export type DocumentDropzoneProps = {
className?: string;
disabled?: boolean;
disabledMessage?: string;
disabledMessage?: MessageDescriptor;
onDrop?: (_file: File) => void | Promise<void>;
onDropRejected?: () => void | Promise<void>;
type?: 'document' | 'template';
@ -37,10 +40,12 @@ export const DocumentDropzone = ({
onDrop,
onDropRejected,
disabled,
disabledMessage = 'You cannot upload documents at this time.',
disabledMessage = msg`You cannot upload documents at this time.`,
type = 'document',
...props
}: DocumentDropzoneProps) => {
const { _ } = useLingui();
const { getRootProps, getInputProps } = useDropzone({
accept: {
'application/pdf': ['.pdf'],
@ -61,8 +66,8 @@ export const DocumentDropzone = ({
});
const heading = {
document: disabled ? 'You have reached your document limit.' : 'Add a document',
template: 'Upload Template Document',
document: disabled ? msg`You have reached your document limit.` : msg`Add a document`,
template: msg`Upload Template Document`,
};
return (
@ -151,15 +156,17 @@ export const DocumentDropzone = ({
<input {...getInputProps()} />
<p className="text-foreground mt-8 font-medium">{heading[type]}</p>
<p className="text-foreground mt-8 font-medium">{_(heading[type])}</p>
<p className="text-muted-foreground/80 mt-1 text-center text-sm">
{disabled ? disabledMessage : 'Drag & drop your PDF here.'}
{_(disabled ? disabledMessage : msg`Drag & drop your PDF here.`)}
</p>
{disabled && IS_BILLING_ENABLED() && (
<Button className="hover:bg-warning/80 bg-warning mt-4 w-32" asChild>
<Link href="/settings/billing">Upgrade</Link>
<Link href="/settings/billing">
<Trans>Upgrade</Trans>
</Link>
</Button>
)}
</CardContent>

View File

@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Caveat } from 'next/font/google';
import { Trans, msg } from '@lingui/macro';
import {
CalendarDays,
Check,
@ -24,7 +25,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { RECIPIENT_ROLES_DESCRIPTION_ENG } from '@documenso/lib/constants/recipient-roles';
import {
type TFieldMetaSchema as FieldMeta,
ZFieldMetaSchema,
@ -506,8 +507,8 @@ export const AddFieldsFormPartial = ({
<>
{showAdvancedSettings && currentField ? (
<FieldAdvancedSettings
title="Advanced settings"
description={`Configure the ${FRIENDLY_FIELD_TYPE[currentField.type]} field`}
title={msg`Advanced settings`}
description={msg`Configure the ${FRIENDLY_FIELD_TYPE[currentField.type]} field`}
field={currentField}
fields={localFields}
onAdvancedSettings={handleAdvancedSettings}
@ -609,14 +610,15 @@ export const AddFieldsFormPartial = ({
<CommandEmpty>
<span className="text-muted-foreground inline-block px-4">
No recipient matching this description was found.
<Trans>No recipient matching this description was found.</Trans>
</span>
</CommandEmpty>
{recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => (
<CommandGroup key={roleIndex}>
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
{`${RECIPIENT_ROLES_DESCRIPTION[role].roleName}s`}
{/* Todo: Translations - Add plural translations. */}
{`${RECIPIENT_ROLES_DESCRIPTION_ENG[role].roleName}s`}
</div>
{roleRecipients.length === 0 && (
@ -624,7 +626,7 @@ export const AddFieldsFormPartial = ({
key={`${role}-empty`}
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs"
>
No recipients with this role
<Trans>No recipients with this role</Trans>
</div>
)}
@ -680,8 +682,10 @@ export const AddFieldsFormPartial = ({
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
This document has already been sent to this recipient. You can
no longer edit this recipient.
<Trans>
This document has already been sent to this recipient. You
can no longer edit this recipient.
</Trans>
</TooltipContent>
</Tooltip>
)}
@ -717,7 +721,7 @@ export const AddFieldsFormPartial = ({
fontCaveat.className,
)}
>
Signature
<Trans>Signature</Trans>
</p>
</CardContent>
</Card>
@ -769,7 +773,7 @@ export const AddFieldsFormPartial = ({
)}
>
<Mail className="h-4 w-4" />
Email
<Trans>Email</Trans>
</p>
</CardContent>
</Card>
@ -795,7 +799,7 @@ export const AddFieldsFormPartial = ({
)}
>
<User className="h-4 w-4" />
Name
<Trans>Name</Trans>
</p>
</CardContent>
</Card>
@ -821,7 +825,7 @@ export const AddFieldsFormPartial = ({
)}
>
<CalendarDays className="h-4 w-4" />
Date
<Trans>Date</Trans>
</p>
</CardContent>
</Card>
@ -847,7 +851,7 @@ export const AddFieldsFormPartial = ({
)}
>
<Type className="h-4 w-4" />
Text
<Trans>Text</Trans>
</p>
</CardContent>
</Card>
@ -873,7 +877,7 @@ export const AddFieldsFormPartial = ({
)}
>
<Hash className="h-4 w-4" />
Number
<Trans>Number</Trans>
</p>
</CardContent>
</Card>
@ -899,7 +903,7 @@ export const AddFieldsFormPartial = ({
)}
>
<Disc className="h-4 w-4" />
Radio
<Trans>Radio</Trans>
</p>
</CardContent>
</Card>
@ -925,7 +929,7 @@ export const AddFieldsFormPartial = ({
)}
>
<CheckSquare className="h-4 w-4" />
Checkbox
<Trans>Checkbox</Trans>
</p>
</CardContent>
</Card>
@ -951,7 +955,7 @@ export const AddFieldsFormPartial = ({
)}
>
<ChevronDown className="h-4 w-4" />
Dropdown
<Trans>Dropdown</Trans>
</p>
</CardContent>
</Card>
@ -964,23 +968,21 @@ export const AddFieldsFormPartial = ({
<div className="mt-4">
<ul>
<li className="text-sm text-red-500">
To proceed further, please set at least one value for the{' '}
{emptyCheckboxFields.length > 0
? 'Checkbox'
: emptyRadioFields.length > 0
? 'Radio'
: 'Select'}{' '}
field.
<Trans>
To proceed further, please set at least one value for the{' '}
{emptyCheckboxFields.length > 0
? 'Checkbox'
: emptyRadioFields.length > 0
? 'Radio'
: 'Select'}{' '}
field.
</Trans>
</li>
</ul>
</div>
)}
<DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep
title={documentFlow.title}
step={currentStep}
maxStep={totalSteps}
/>
<DocumentFlowFormContainerStep step={currentStep} maxStep={totalSteps} />
<DocumentFlowFormContainerActions
loading={isSubmitting}
@ -991,7 +993,7 @@ export const AddFieldsFormPartial = ({
remove();
documentFlow.onBackStep?.();
}}
goBackLabel={canRenderBackButtonAsRemove ? 'Remove' : undefined}
goBackLabel={canRenderBackButtonAsRemove ? msg`Remove` : undefined}
onGoNextClick={handleGoNextClick}
/>
</DocumentFlowFormContainerFooter>

View File

@ -3,6 +3,7 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans } from '@lingui/macro';
import { InfoIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
@ -140,7 +141,9 @@ export const AddSettingsFormPartial = ({
name="title"
render={({ field }) => (
<FormItem>
<FormLabel required>Title</FormLabel>
<FormLabel required>
<Trans>Title</Trans>
</FormLabel>
<FormControl>
<Input
@ -160,7 +163,7 @@ export const AddSettingsFormPartial = ({
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
Document access
<Trans>Document access</Trans>
<DocumentGlobalAuthAccessTooltip />
</FormLabel>
@ -178,7 +181,7 @@ export const AddSettingsFormPartial = ({
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
Recipient action authentication
<Trans>Recipient action authentication</Trans>
<DocumentGlobalAuthActionTooltip />
</FormLabel>
@ -193,7 +196,7 @@ export const AddSettingsFormPartial = ({
<Accordion type="multiple" className="mt-6">
<AccordionItem value="advanced-options" className="border-none">
<AccordionTrigger className="text-foreground mb-2 rounded border px-3 py-2 text-left hover:bg-neutral-200/30 hover:no-underline">
Advanced Options
<Trans>Advanced Options</Trans>
</AccordionTrigger>
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-2 text-sm leading-relaxed">
@ -204,15 +207,17 @@ export const AddSettingsFormPartial = ({
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
External ID{' '}
<Trans>External ID</Trans>{' '}
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
Add an external ID to the document. This can be used to identify the
document in external systems.
<Trans>
Add an external ID to the document. This can be used to identify
the document in external systems.
</Trans>
</TooltipContent>
</Tooltip>
</FormLabel>
@ -231,7 +236,9 @@ export const AddSettingsFormPartial = ({
name="meta.dateFormat"
render={({ field }) => (
<FormItem>
<FormLabel>Date Format</FormLabel>
<FormLabel>
<Trans>Date Format</Trans>
</FormLabel>
<FormControl>
<Select
@ -263,7 +270,9 @@ export const AddSettingsFormPartial = ({
name="meta.timezone"
render={({ field }) => (
<FormItem>
<FormLabel>Time Zone</FormLabel>
<FormLabel>
<Trans>Time Zone</Trans>
</FormLabel>
<FormControl>
<Combobox
@ -286,14 +295,16 @@ export const AddSettingsFormPartial = ({
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
Redirect URL{' '}
<Trans>Redirect URL</Trans>{' '}
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
Add a URL to redirect the user to once the document is signed
<Trans>
Add a URL to redirect the user to once the document is signed
</Trans>
</TooltipContent>
</Tooltip>
</FormLabel>
@ -315,11 +326,7 @@ export const AddSettingsFormPartial = ({
</DocumentFlowFormContainerContent>
<DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep
title={documentFlow.title}
step={currentStep}
maxStep={totalSteps}
/>
<DocumentFlowFormContainerStep step={currentStep} maxStep={totalSteps} />
<DocumentFlowFormContainerActions
loading={form.formState.isSubmitting}

View File

@ -3,6 +3,7 @@
import { useMemo, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans } from '@lingui/macro';
import { DateTime } from 'luxon';
import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
@ -267,7 +268,9 @@ export const AddSignatureFormPartial = ({
name="email"
render={({ field }) => (
<FormItem>
<FormLabel required>Email</FormLabel>
<FormLabel required>
<Trans>Email</Trans>
</FormLabel>
<FormControl>
<Input
className="bg-background"
@ -291,7 +294,9 @@ export const AddSignatureFormPartial = ({
name="name"
render={({ field }) => (
<FormItem>
<FormLabel required={requireName}>Name</FormLabel>
<FormLabel required={requireName}>
<Trans>Name</Trans>
</FormLabel>
<FormControl>
<Input
className="bg-background"
@ -314,7 +319,9 @@ export const AddSignatureFormPartial = ({
name="signature"
render={({ field }) => (
<FormItem>
<FormLabel required={requireSignature}>Signature</FormLabel>
<FormLabel required={requireSignature}>
<Trans>Signature</Trans>
</FormLabel>
<FormControl>
<Card
className={cn('mt-2', {
@ -349,7 +356,9 @@ export const AddSignatureFormPartial = ({
name="customText"
render={({ field }) => (
<FormItem>
<FormLabel required={requireCustomText}>Custom Text</FormLabel>
<FormLabel required={requireCustomText}>
<Trans>Custom Text</Trans>
</FormLabel>
<FormControl>
<Input
className="bg-background"
@ -369,11 +378,7 @@ export const AddSignatureFormPartial = ({
</DocumentFlowFormContainerContent>
<DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep
title={documentFlow.title}
step={currentStep}
maxStep={totalSteps}
/>
<DocumentFlowFormContainerStep step={currentStep} maxStep={totalSteps} />
<DocumentFlowFormContainerActions
loading={form.formState.isSubmitting}
@ -386,7 +391,7 @@ export const AddSignatureFormPartial = ({
{validateUninsertedFields && uninsertedFields[0] && (
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
Click to insert field
<Trans>Click to insert field</Trans>
</FieldToolTip>
)}

View File

@ -5,6 +5,8 @@ import React, { useId, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { motion } from 'framer-motion';
import { Plus, Trash } from 'lucide-react';
import { useSession } from 'next-auth/react';
@ -63,10 +65,12 @@ export const AddSignersFormPartial = ({
isDocumentPdfLoaded,
teamId,
}: AddSignersFormProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const { remaining } = useLimits();
const { data: session } = useSession();
const router = useRouter();
const user = session?.user;
const initialId = useId();
@ -318,13 +322,15 @@ export const AddSignersFormPartial = ({
})}
>
{!showAdvancedSettings && index === 0 && (
<FormLabel required>Email</FormLabel>
<FormLabel required>
<Trans>Email</Trans>
</FormLabel>
)}
<FormControl>
<Input
type="email"
placeholder="Email"
placeholder={_(msg`Email`)}
{...field}
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
onKeyDown={onKeyDown}
@ -351,7 +357,7 @@ export const AddSignersFormPartial = ({
<FormControl>
<Input
placeholder="Name"
placeholder={_(msg`Name`)}
{...field}
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
onKeyDown={onKeyDown}
@ -435,7 +441,7 @@ export const AddSignersFormPartial = ({
onClick={() => onAddSigner()}
>
<Plus className="-ml-1 mr-2 h-5 w-5" />
Add Signer
<Trans>Add Signer</Trans>
</Button>
<Button
@ -446,7 +452,7 @@ export const AddSignersFormPartial = ({
onClick={() => onAddSelfSigner()}
>
<Plus className="-ml-1 mr-2 h-5 w-5" />
Add myself
<Trans>Add myself</Trans>
</Button>
</div>
@ -464,7 +470,7 @@ export const AddSignersFormPartial = ({
className="text-muted-foreground ml-2 text-sm"
htmlFor="showAdvancedRecipientSettings"
>
Show advanced settings
<Trans>Show advanced settings</Trans>
</label>
</div>
)}
@ -473,11 +479,7 @@ export const AddSignersFormPartial = ({
</DocumentFlowFormContainerContent>
<DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep
title={documentFlow.title}
step={currentStep}
maxStep={totalSteps}
/>
<DocumentFlowFormContainerStep step={currentStep} maxStep={totalSteps} />
<DocumentFlowFormContainerActions
loading={isSubmitting}

View File

@ -1,6 +1,7 @@
'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro';
import { useForm } from 'react-hook-form';
import type { Field, Recipient } from '@documenso/prisma/client';
@ -74,7 +75,9 @@ export const AddSubjectFormPartial = ({
<div className="flex flex-col gap-y-4">
<div>
<Label htmlFor="subject">
Subject <span className="text-muted-foreground">(Optional)</span>
<Trans>
Subject <span className="text-muted-foreground">(Optional)</span>
</Trans>
</Label>
<Input
@ -89,7 +92,9 @@ export const AddSubjectFormPartial = ({
<div>
<Label htmlFor="message">
Message <span className="text-muted-foreground">(Optional)</span>
<Trans>
Message <span className="text-muted-foreground">(Optional)</span>
</Trans>
</Label>
<Textarea
@ -111,16 +116,12 @@ export const AddSubjectFormPartial = ({
</DocumentFlowFormContainerContent>
<DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep
title={documentFlow.title}
step={currentStep}
maxStep={totalSteps}
/>
<DocumentFlowFormContainerStep step={currentStep} maxStep={totalSteps} />
<DocumentFlowFormContainerActions
loading={isSubmitting}
disabled={isSubmitting}
goNextLabel={document.status === DocumentStatus.DRAFT ? 'Send' : 'Update'}
goNextLabel={document.status === DocumentStatus.DRAFT ? msg`Send` : msg`Update`}
onGoBackClick={previousStep}
onGoNextClick={() => void onFormSubmit()}
/>

View File

@ -3,6 +3,9 @@
import type { HTMLAttributes } from 'react';
import React from 'react';
import type { MessageDescriptor } from '@lingui/core';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { motion } from 'framer-motion';
import { cn } from '../../lib/utils';
@ -33,19 +36,21 @@ export const DocumentFlowFormContainer = ({
};
export type DocumentFlowFormContainerHeaderProps = {
title: string;
description: string;
title: MessageDescriptor;
description: MessageDescriptor;
};
export const DocumentFlowFormContainerHeader = ({
title,
description,
}: DocumentFlowFormContainerHeaderProps) => {
const { _ } = useLingui();
return (
<>
<h3 className="text-foreground text-2xl font-semibold">{title}</h3>
<h3 className="text-foreground text-2xl font-semibold">{_(title)}</h3>
<p className="text-muted-foreground mt-2 text-sm">{description}</p>
<p className="text-muted-foreground mt-2 text-sm">{_(description)}</p>
<hr className="border-border mb-8 mt-4" />
</>
@ -88,7 +93,6 @@ export const DocumentFlowFormContainerFooter = ({
};
export type DocumentFlowFormContainerStepProps = {
title: string;
step: number;
maxStep: number;
};
@ -100,7 +104,9 @@ export const DocumentFlowFormContainerStep = ({
return (
<div>
<p className="text-muted-foreground text-sm">
Step <span>{`${step} of ${maxStep}`}</span>
<Trans>
Step <span>{`${step} of ${maxStep}`}</span>
</Trans>
</p>
<div className="bg-muted relative mt-4 h-[2px] rounded-md">
@ -120,8 +126,8 @@ export const DocumentFlowFormContainerStep = ({
export type DocumentFlowFormContainerActionsProps = {
canGoBack?: boolean;
canGoNext?: boolean;
goNextLabel?: string;
goBackLabel?: string;
goNextLabel?: MessageDescriptor;
goBackLabel?: MessageDescriptor;
onGoBackClick?: () => void;
onGoNextClick?: () => void;
loading?: boolean;
@ -132,14 +138,15 @@ export type DocumentFlowFormContainerActionsProps = {
export const DocumentFlowFormContainerActions = ({
canGoBack = true,
canGoNext = true,
goNextLabel = 'Continue',
goBackLabel = 'Go Back',
goNextLabel = msg`Continue`,
goBackLabel = msg`Go Back`,
onGoBackClick,
onGoNextClick,
loading,
disabled,
disableNextStep = false,
}: DocumentFlowFormContainerActionsProps) => {
const { _ } = useLingui();
return (
<div className="mt-4 flex gap-x-4">
<Button
@ -150,7 +157,7 @@ export const DocumentFlowFormContainerActions = ({
disabled={disabled || loading || !canGoBack || !onGoBackClick}
onClick={onGoBackClick}
>
{goBackLabel}
{_(goBackLabel)}
</Button>
<Button
@ -161,7 +168,7 @@ export const DocumentFlowFormContainerActions = ({
loading={loading}
onClick={onGoNextClick}
>
{goNextLabel}
{_(goNextLabel)}
</Button>
</div>
);

View File

@ -1,3 +1,4 @@
import { Trans } from '@lingui/macro';
import {
CalendarDays,
CheckSquare,
@ -48,7 +49,7 @@ export const FieldIcon = ({
fontCaveatClassName,
)}
>
Signature
<Trans>Signature</Trans>
</div>
);
} else {

View File

@ -5,6 +5,9 @@ import { forwardRef, useEffect, useState } from 'react';
import { useParams } from 'next/navigation';
import { usePathname } from 'next/navigation';
import type { MessageDescriptor } from '@lingui/core';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { match } from 'ts-pattern';
import {
@ -37,8 +40,8 @@ import { TextFieldAdvancedSettings } from './field-items-advanced-settings/text-
export type FieldAdvancedSettingsProps = {
teamId?: number;
title: string;
description: string;
title: MessageDescriptor;
description: MessageDescriptor;
field: FieldFormType;
fields: FieldFormType[];
onAdvancedSettings?: () => void;
@ -121,7 +124,9 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
},
ref,
) => {
const { _ } = useLingui();
const { toast } = useToast();
const params = useParams();
const pathname = usePathname();
const id = params?.id;
@ -150,17 +155,7 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
const doesFieldExist = (!!document || !!template) && field.nativeId !== undefined;
const { data: fieldData } = trpc.field.getField.useQuery(
{
fieldId: Number(field.nativeId),
teamId,
},
{
enabled: doesFieldExist,
},
);
const fieldMeta = fieldData?.fieldMeta;
const fieldMeta = field?.fieldMeta;
const localStorageKey = `field_${field.formId}_${field.type}`;
@ -218,8 +213,8 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
console.error('Failed to save to localStorage:', error);
toast({
title: 'Error',
description: 'Failed to save settings.',
title: _(msg`Error`),
description: _(msg`Failed to save settings.`),
variant: 'destructive',
});
}
@ -288,8 +283,8 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
</DocumentFlowFormContainerContent>
<DocumentFlowFormContainerFooter className="mt-auto">
<DocumentFlowFormContainerActions
goNextLabel="Save"
goBackLabel="Cancel"
goNextLabel={msg`Save`}
goBackLabel={msg`Cancel`}
onGoBackClick={onAdvancedSettings}
onGoNextClick={handleOnGoNextClick}
disableNextStep={errors.length > 0}

View File

@ -2,6 +2,8 @@
import { useEffect, useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { ChevronDown, ChevronUp, Trash } from 'lucide-react';
import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
@ -35,6 +37,8 @@ export const CheckboxFieldAdvancedSettings = ({
handleFieldChange,
handleErrors,
}: CheckboxFieldAdvancedSettingsProps) => {
const { _ } = useLingui();
const [showValidation, setShowValidation] = useState(false);
const [values, setValues] = useState(fieldState.values ?? [{ id: 1, checked: false, value: '' }]);
const [readOnly, setReadOnly] = useState(fieldState.readOnly ?? false);
@ -122,13 +126,15 @@ export const CheckboxFieldAdvancedSettings = ({
<div className="flex flex-col gap-4">
<div className="flex flex-row items-center gap-x-4">
<div className="flex w-2/3 flex-col">
<Label>Validation</Label>
<Label>
<Trans>Validation</Trans>
</Label>
<Select
value={fieldState.validationRule}
onValueChange={(val) => handleToggleChange('validationRule', val)}
>
<SelectTrigger className="text-muted-foreground bg-background mt-2 w-full">
<SelectValue placeholder="Select at least" />
<SelectValue placeholder={_(msg`Select at least`)} />
</SelectTrigger>
<SelectContent position="popper">
{checkboxValidationRules.map((item, index) => (
@ -145,7 +151,7 @@ export const CheckboxFieldAdvancedSettings = ({
onValueChange={(val) => handleToggleChange('validationLength', val)}
>
<SelectTrigger className="text-muted-foreground bg-background mt-2 w-full">
<SelectValue placeholder="Pick a number" />
<SelectValue placeholder={_(msg`Pick a number`)} />
</SelectTrigger>
<SelectContent position="popper">
{checkboxValidationLength.map((item, index) => (
@ -164,7 +170,9 @@ export const CheckboxFieldAdvancedSettings = ({
checked={fieldState.required}
onCheckedChange={(checked) => handleToggleChange('required', checked)}
/>
<Label>Required field</Label>
<Label>
<Trans>Required field</Trans>
</Label>
</div>
<div className="flex flex-row items-center gap-2">
<Switch
@ -172,7 +180,9 @@ export const CheckboxFieldAdvancedSettings = ({
checked={fieldState.readOnly}
onCheckedChange={(checked) => handleToggleChange('readOnly', checked)}
/>
<Label>Read only</Label>
<Label>
<Trans>Read only</Trans>
</Label>
</div>
</div>
<Button
@ -181,7 +191,9 @@ export const CheckboxFieldAdvancedSettings = ({
onClick={() => setShowValidation((prev) => !prev)}
>
<span className="flex w-full flex-row justify-between">
<span className="flex items-center">Checkbox values</span>
<span className="flex items-center">
<Trans>Checkbox values</Trans>
</span>
{showValidation ? <ChevronUp /> : <ChevronDown />}
</span>
</Button>
@ -215,7 +227,7 @@ export const CheckboxFieldAdvancedSettings = ({
variant="outline"
onClick={addValue}
>
Add another value
<Trans>Add another value</Trans>
</Button>
</div>
)}

View File

@ -2,6 +2,8 @@
import { useEffect, useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { ChevronDown, ChevronUp, Trash } from 'lucide-react';
import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown';
@ -32,6 +34,8 @@ export const DropdownFieldAdvancedSettings = ({
handleFieldChange,
handleErrors,
}: DropdownFieldAdvancedSettingsProps) => {
const { _ } = useLingui();
const [showValidation, setShowValidation] = useState(false);
const [values, setValues] = useState(fieldState.values ?? [{ value: 'Option 1' }]);
const [readOnly, setReadOnly] = useState(fieldState.readOnly ?? false);
@ -87,7 +91,9 @@ export const DropdownFieldAdvancedSettings = ({
return (
<div className="text-dark flex flex-col gap-4">
<div>
<Label>Select default option</Label>
<Label>
<Trans>Select default option</Trans>
</Label>
<Select
defaultValue={defaultValue}
onValueChange={(val) => {
@ -96,7 +102,7 @@ export const DropdownFieldAdvancedSettings = ({
}}
>
<SelectTrigger className="text-muted-foreground bg-background mt-2 w-full">
<SelectValue defaultValue={defaultValue} placeholder="-- Select --" />
<SelectValue defaultValue={defaultValue} placeholder={`-- ${_(msg`Select`)} --`} />
</SelectTrigger>
<SelectContent position="popper">
{values.map((item, index) => (
@ -117,7 +123,9 @@ export const DropdownFieldAdvancedSettings = ({
checked={fieldState.required}
onCheckedChange={(checked) => handleToggleChange('required', checked)}
/>
<Label>Required field</Label>
<Label>
<Trans>Required field</Trans>
</Label>
</div>
<div className="flex flex-row items-center gap-2">
<Switch
@ -125,7 +133,9 @@ export const DropdownFieldAdvancedSettings = ({
checked={fieldState.readOnly}
onCheckedChange={(checked) => handleToggleChange('readOnly', checked)}
/>
<Label>Read only</Label>
<Label>
<Trans>Read only</Trans>
</Label>
</div>
</div>
<Button
@ -134,7 +144,9 @@ export const DropdownFieldAdvancedSettings = ({
onClick={() => setShowValidation((prev) => !prev)}
>
<span className="flex w-full flex-row justify-between">
<span className="flex items-center">Dropdown options</span>
<span className="flex items-center">
<Trans>Dropdown options</Trans>
</span>
{showValidation ? <ChevronUp /> : <ChevronDown />}
</span>
</Button>
@ -162,7 +174,7 @@ export const DropdownFieldAdvancedSettings = ({
variant="outline"
onClick={addValue}
>
Add another option
<Trans>Add another option</Trans>
</Button>
</div>
)}

View File

@ -2,6 +2,8 @@
import { useState } from 'react';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { ChevronDown, ChevronUp } from 'lucide-react';
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
@ -31,6 +33,8 @@ export const NumberFieldAdvancedSettings = ({
handleFieldChange,
handleErrors,
}: NumberFieldAdvancedSettingsProps) => {
const { _ } = useLingui();
const [showValidation, setShowValidation] = useState(false);
const handleInput = (field: keyof NumberFieldMeta, value: string | boolean) => {
@ -56,43 +60,51 @@ export const NumberFieldAdvancedSettings = ({
return (
<div className="flex flex-col gap-4">
<div>
<Label>Label</Label>
<Label>
<Trans>Label</Trans>
</Label>
<Input
id="label"
className="bg-background mt-2"
placeholder="Label"
placeholder={_(msg`Label`)}
value={fieldState.label}
onChange={(e) => handleFieldChange('label', e.target.value)}
/>
</div>
<div>
<Label className="mt-4">Placeholder</Label>
<Label className="mt-4">
<Trans>Placeholder</Trans>
</Label>
<Input
id="placeholder"
className="bg-background mt-2"
placeholder="Placeholder"
placeholder={_(msg`Placeholder`)}
value={fieldState.placeholder}
onChange={(e) => handleFieldChange('placeholder', e.target.value)}
/>
</div>
<div>
<Label className="mt-4">Value</Label>
<Label className="mt-4">
<Trans>Value</Trans>
</Label>
<Input
id="value"
className="bg-background mt-2"
placeholder="Value"
placeholder={_(msg`Value`)}
value={fieldState.value}
onChange={(e) => handleInput('value', e.target.value)}
/>
</div>
<div>
<Label>Number format</Label>
<Label>
<Trans>Number format</Trans>
</Label>
<Select
value={fieldState.numberFormat}
onValueChange={(val) => handleInput('numberFormat', val)}
>
<SelectTrigger className="text-muted-foreground bg-background mt-2 w-full">
<SelectValue placeholder="Field format" />
<SelectValue placeholder={_(msg`Field format`)} />
</SelectTrigger>
<SelectContent position="popper">
{numberFormatValues.map((item, index) => (
@ -110,7 +122,9 @@ export const NumberFieldAdvancedSettings = ({
checked={fieldState.required}
onCheckedChange={(checked) => handleInput('required', checked)}
/>
<Label>Required field</Label>
<Label>
<Trans>Required field</Trans>
</Label>
</div>
<div className="flex flex-row items-center gap-2">
<Switch
@ -118,7 +132,9 @@ export const NumberFieldAdvancedSettings = ({
checked={fieldState.readOnly}
onCheckedChange={(checked) => handleInput('readOnly', checked)}
/>
<Label>Read only</Label>
<Label>
<Trans>Read only</Trans>
</Label>
</div>
</div>
<Button
@ -127,14 +143,18 @@ export const NumberFieldAdvancedSettings = ({
onClick={() => setShowValidation((prev) => !prev)}
>
<span className="flex w-full flex-row justify-between">
<span className="flex items-center">Validation</span>
<span className="flex items-center">
<Trans>Validation</Trans>
</span>
{showValidation ? <ChevronUp /> : <ChevronDown />}
</span>
</Button>
{showValidation && (
<div className="mb-4 flex flex-row gap-x-4">
<div className="flex flex-col">
<Label className="mt-4">Min</Label>
<Label className="mt-4">
<Trans>Min</Trans>
</Label>
<Input
id="minValue"
className="bg-background mt-2"
@ -144,7 +164,9 @@ export const NumberFieldAdvancedSettings = ({
/>
</div>
<div className="flex flex-col">
<Label className="mt-4">Max</Label>
<Label className="mt-4">
<Trans>Max</Trans>
</Label>
<Input
id="maxValue"
className="bg-background mt-2"

View File

@ -2,6 +2,7 @@
import { useEffect, useState } from 'react';
import { Trans } from '@lingui/macro';
import { ChevronDown, ChevronUp, Trash } from 'lucide-react';
import { validateRadioField } from '@documenso/lib/advanced-fields-validation/validate-radio';
@ -107,7 +108,9 @@ export const RadioFieldAdvancedSettings = ({
checked={fieldState.required}
onCheckedChange={(checked) => handleToggleChange('required', checked)}
/>
<Label>Required field</Label>
<Label>
<Trans>Required field</Trans>
</Label>
</div>
<div className="flex flex-row items-center gap-2">
<Switch
@ -115,7 +118,9 @@ export const RadioFieldAdvancedSettings = ({
checked={fieldState.readOnly}
onCheckedChange={(checked) => handleToggleChange('readOnly', checked)}
/>
<Label>Read only</Label>
<Label>
<Trans>Read only</Trans>
</Label>
</div>
</div>
<Button
@ -124,7 +129,9 @@ export const RadioFieldAdvancedSettings = ({
onClick={() => setShowValidation((prev) => !prev)}
>
<span className="flex w-full flex-row justify-between">
<span className="flex items-center">Radio values</span>
<span className="flex items-center">
<Trans>Radio values</Trans>
</span>
{showValidation ? <ChevronUp /> : <ChevronDown />}
</span>
</Button>
@ -157,7 +164,7 @@ export const RadioFieldAdvancedSettings = ({
variant="outline"
onClick={addValue}
>
Add another value
<Trans>Add another value</Trans>
</Button>
</div>
)}

View File

@ -1,3 +1,6 @@
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
import { type TTextFieldMeta as TextFieldMeta } from '@documenso/lib/types/field-meta';
import { Input } from '@documenso/ui/primitives/input';
@ -16,6 +19,8 @@ export const TextFieldAdvancedSettings = ({
handleFieldChange,
handleErrors,
}: TextFieldAdvancedSettingsProps) => {
const { _ } = useLingui();
const handleInput = (field: keyof TextFieldMeta, value: string | boolean) => {
const text = field === 'text' ? String(value) : fieldState.text || '';
const limit =
@ -36,45 +41,53 @@ export const TextFieldAdvancedSettings = ({
return (
<div className="flex flex-col gap-4">
<div>
<Label>Label</Label>
<Label>
<Trans>Label</Trans>
</Label>
<Input
id="label"
className="bg-background mt-2"
placeholder="Field label"
placeholder={_(msg`Field label`)}
value={fieldState.label}
onChange={(e) => handleFieldChange('label', e.target.value)}
/>
</div>
<div>
<Label className="mt-4">Placeholder</Label>
<Label className="mt-4">
<Trans>Placeholder</Trans>
</Label>
<Input
id="placeholder"
className="bg-background mt-2"
placeholder="Field placeholder"
placeholder={_(msg`Field placeholder`)}
value={fieldState.placeholder}
onChange={(e) => handleFieldChange('placeholder', e.target.value)}
/>
</div>
<div>
<Label className="mt-4">Add text</Label>
<Label className="mt-4">
<Trans>Add text</Trans>
</Label>
<Textarea
id="text"
className="bg-background mt-2"
placeholder="Add text to the field"
placeholder={_(msg`Add text to the field`)}
value={fieldState.text}
onChange={(e) => handleInput('text', e.target.value)}
/>
</div>
<div>
<Label>Character Limit</Label>
<Label>
<Trans>Character Limit</Trans>
</Label>
<Input
id="characterLimit"
type="number"
min={0}
className="bg-background mt-2"
placeholder="Field character limit"
placeholder={_(msg`Field character limit`)}
value={fieldState.characterLimit}
onChange={(e) => handleInput('characterLimit', e.target.value)}
/>
@ -87,7 +100,9 @@ export const TextFieldAdvancedSettings = ({
checked={fieldState.required}
onCheckedChange={(checked) => handleInput('required', checked)}
/>
<Label>Required field</Label>
<Label>
<Trans>Required field</Trans>
</Label>
</div>
<div className="flex flex-row items-center gap-2">
<Switch
@ -95,7 +110,9 @@ export const TextFieldAdvancedSettings = ({
checked={fieldState.readOnly}
onCheckedChange={(checked) => handleInput('readOnly', checked)}
/>
<Label>Read only</Label>
<Label>
<Trans>Read only</Trans>
</Label>
</div>
</div>
</div>

View File

@ -1,5 +1,6 @@
'use client';
import { Trans } from '@lingui/macro';
import { DialogClose } from '@radix-ui/react-dialog';
import { Button } from '@documenso/ui/primitives/button';
@ -25,18 +26,22 @@ export const MissingSignatureFieldDialog = ({
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg" position="center">
<DialogHeader>
<DialogTitle>No signature field found</DialogTitle>
<DialogTitle>
<Trans>No signature field found</Trans>
</DialogTitle>
<DialogDescription>
<p className="mt-2">
Some signers have not been assigned a signature field. Please assign at least 1
signature field to each signer before proceeding.
<Trans>
Some signers have not been assigned a signature field. Please assign at least 1
signature field to each signer before proceeding.
</Trans>
</p>
</DialogDescription>
</DialogHeader>
<DialogFooter>
<DialogClose asChild>
<Button type="button" variant="secondary">
Close
<Trans>Close</Trans>
</Button>
</DialogClose>
</DialogFooter>

View File

@ -1,5 +1,6 @@
import { useState } from 'react';
import { Trans } from '@lingui/macro';
import { Loader } from 'lucide-react';
import type { ButtonProps } from '../button';
@ -30,16 +31,20 @@ export const SendDocumentActionDialog = ({
<DialogTrigger asChild>
<Button type="button" className={className}>
{loading && <Loader className="text-documenso mr-2 h-5 w-5 animate-spin" />}
Send
<Trans>Send</Trans>
</Button>
</DialogTrigger>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="text-center text-lg font-semibold">Send Document</DialogTitle>
<DialogTitle className="text-center text-lg font-semibold">
<Trans>Send Document</Trans>
</DialogTitle>
<DialogDescription className="text-center text-base">
You are about to send this document to the recipients. Are you sure you want to
continue?
<Trans>
You are about to send this document to the recipients. Are you sure you want to
continue?
</Trans>
</DialogDescription>
</DialogHeader>
@ -50,13 +55,13 @@ export const SendDocumentActionDialog = ({
variant="secondary"
onClick={() => setOpen(false)}
>
Cancel
<Trans>Cancel</Trans>
</Button>
{/* We would use DialogAction here but it interrupts the submit action */}
<Button className={className} {...props}>
{loading && <Loader className="mr-2 h-5 w-5 animate-spin" />}
Send
<Trans>Send</Trans>
</Button>
</DialogFooter>
</DialogContent>

View File

@ -1,3 +1,4 @@
import type { MessageDescriptor } from '@lingui/core';
import { z } from 'zod';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
@ -58,8 +59,8 @@ export const FRIENDLY_FIELD_TYPE: Record<FieldType, string> = {
};
export interface DocumentFlowStep {
title: string;
description: string;
title: MessageDescriptor;
description: MessageDescriptor;
stepIndex?: number;
onBackStep?: () => unknown;
onNextStep?: () => unknown;

View File

@ -1,6 +1,8 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
@ -30,6 +32,8 @@ export const PasswordDialog = ({
onPasswordSubmit,
isError,
}: PasswordDialogProps) => {
const { _ } = useLingui();
const form = useForm<TPasswordDialogFormSchema>({
defaultValues: {
password: defaultPassword ?? '',
@ -45,7 +49,7 @@ export const PasswordDialog = ({
if (isError) {
form.setError('password', {
type: 'manual',
message: 'The password you have entered is incorrect. Please try again.',
message: _(msg`The password you have entered is incorrect. Please try again.`),
});
}
}, [form, isError]);
@ -54,10 +58,14 @@ export const PasswordDialog = ({
<Dialog open={open}>
<DialogContent className="w-full max-w-md">
<DialogHeader>
<DialogTitle>Password Required</DialogTitle>
<DialogTitle>
<Trans>Password Required</Trans>
</DialogTitle>
<DialogDescription className="text-muted-foreground">
This document is password protected. Please enter the password to view the document.
<Trans>
This document is password protected. Please enter the password to view the document.
</Trans>
</DialogDescription>
</DialogHeader>
@ -73,7 +81,7 @@ export const PasswordDialog = ({
<Input
type="password"
className="bg-background"
placeholder="Enter password"
placeholder={_(msg`Enter password`)}
autoComplete="off"
{...field}
/>
@ -85,7 +93,9 @@ export const PasswordDialog = ({
/>
<div>
<Button>Submit</Button>
<Button>
<Trans>Submit</Trans>
</Button>
</div>
</fieldset>
</form>

View File

@ -2,6 +2,9 @@
import * as React from 'react';
import type { MessageDescriptor } from '@lingui/core';
import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { AnimatePresence } from 'framer-motion';
import { Check, ChevronsUpDown, Loader, XIcon } from 'lucide-react';
@ -24,7 +27,7 @@ type MultiSelectComboboxProps<T = OptionValue> = {
emptySelectionPlaceholder?: React.ReactNode | string;
enableClearAllButton?: boolean;
loading?: boolean;
inputPlaceholder?: string;
inputPlaceholder?: MessageDescriptor;
onChange: (_values: T[]) => void;
options: ComboBoxOption<T>[];
selectedValues: T[];
@ -46,6 +49,8 @@ export function MultiSelectCombobox<T = OptionValue>({
options,
selectedValues,
}: MultiSelectComboboxProps<T>) {
const { _ } = useLingui();
const [open, setOpen] = React.useState(false);
const handleSelect = (selectedOption: T) => {
@ -143,8 +148,10 @@ export function MultiSelectCombobox<T = OptionValue>({
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder={inputPlaceholder} />
<CommandEmpty>No value found.</CommandEmpty>
<CommandInput placeholder={inputPlaceholder && _(inputPlaceholder)} />
<CommandEmpty>
<Trans>No value found.</Trans>
</CommandEmpty>
<CommandGroup>
{options.map((option, i) => (
<CommandItem key={i} onSelect={() => handleSelect(option.value)}>

View File

@ -3,11 +3,19 @@
import type { HTMLAttributes, MouseEvent, PointerEvent, TouchEvent } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Trans } from '@lingui/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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@documenso/ui/primitives/select';
import { cn } from '../../lib/utils';
import { getSvgPathFromStroke } from './helper';
@ -35,6 +43,7 @@ export const SignaturePad = ({
const [isPressed, setIsPressed] = useState(false);
const [lines, setLines] = useState<Point[][]>([]);
const [currentLine, setCurrentLine] = useState<Point[]>([]);
const [selectedColor, setSelectedColor] = useState('black');
const perfectFreehandOptions = useMemo(() => {
const size = $el.current ? Math.min($el.current.height, $el.current.width) * 0.03 : 10;
@ -84,6 +93,7 @@ export const SignaturePad = ({
ctx.restore();
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.fillStyle = selectedColor;
lines.forEach((line) => {
const pathData = new Path2D(
@ -128,6 +138,7 @@ export const SignaturePad = ({
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.fillStyle = selectedColor;
newLines.forEach((line) => {
const pathData = new Path2D(
@ -236,7 +247,13 @@ export const SignaturePad = ({
>
<canvas
ref={$el}
className={cn('relative block dark:invert', className)}
className={cn(
'relative block',
{
'dark:hue-rotate-180 dark:invert': selectedColor === 'black',
},
className,
)}
style={{ touchAction: 'none' }}
onPointerMove={(event) => onMouseMove(event)}
onPointerDown={(event) => onMouseDown(event)}
@ -246,13 +263,51 @@ export const SignaturePad = ({
{...props}
/>
<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-1">
<SelectValue placeholder="" />
</SelectTrigger>
<SelectContent className="w-[150px]" align="end">
<SelectItem value="black">
<div className="text-muted-foreground flex items-center px-1 text-sm">
<div className="border-border mr-2 h-5 w-5 rounded-full border-2 bg-black shadow-sm" />
<Trans>Black</Trans>
</div>
</SelectItem>
<SelectItem value="red">
<div className="text-muted-foreground flex items-center px-1 text-sm">
<div className="border-border mr-2 h-5 w-5 rounded-full border-2 bg-[red] shadow-sm" />
<Trans>Red</Trans>
</div>
</SelectItem>
<SelectItem value="blue">
<div className="text-muted-foreground flex items-center px-1 text-sm">
<div className="border-border mr-2 h-5 w-5 rounded-full border-2 bg-[blue] shadow-sm" />
<Trans>Blue</Trans>
</div>
</SelectItem>
<SelectItem value="green">
<div className="text-muted-foreground flex items-center px-1 text-sm">
<div className="border-border mr-2 h-5 w-5 rounded-full border-2 bg-[green] shadow-sm" />
<Trans>Green</Trans>
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="absolute bottom-4 right-4 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-xs focus-visible:outline-none focus-visible:ring-2"
onClick={() => onClearClick()}
>
Clear Signature
<Trans>Clear Signature</Trans>
</button>
</div>

View File

@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Caveat } from 'next/font/google';
import { Trans, msg } from '@lingui/macro';
import {
CalendarDays,
CheckSquare,
@ -21,7 +22,7 @@ import { useFieldArray, useForm } from 'react-hook-form';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { RECIPIENT_ROLES_DESCRIPTION_ENG } from '@documenso/lib/constants/recipient-roles';
import {
type TFieldMetaSchema as FieldMeta,
ZFieldMetaSchema,
@ -111,6 +112,7 @@ export const AddTemplateFieldsFormPartial = ({
recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '',
signerToken:
recipients.find((recipient) => recipient.id === field.recipientId)?.token ?? '',
fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
})),
},
});
@ -155,6 +157,38 @@ export const AddTemplateFieldsFormPartial = ({
selectedSignerIndex === -1 ? 0 : selectedSignerIndex,
);
const filterFieldsWithEmptyValues = (fields: typeof localFields, fieldType: string) =>
fields
.filter((field) => field.type === fieldType)
.filter((field) => {
if (field.fieldMeta && 'values' in field.fieldMeta) {
return field.fieldMeta.values?.length === 0;
}
return true;
});
const emptyCheckboxFields = useMemo(
() => filterFieldsWithEmptyValues(localFields, FieldType.CHECKBOX),
// eslint-disable-next-line react-hooks/exhaustive-deps
[localFields],
);
const emptyRadioFields = useMemo(
() => filterFieldsWithEmptyValues(localFields, FieldType.RADIO),
// eslint-disable-next-line react-hooks/exhaustive-deps
[localFields],
);
const emptySelectFields = useMemo(
() => filterFieldsWithEmptyValues(localFields, FieldType.DROPDOWN),
// eslint-disable-next-line react-hooks/exhaustive-deps
[localFields],
);
const hasErrors =
emptyCheckboxFields.length > 0 || emptyRadioFields.length > 0 || emptySelectFields.length > 0;
const [isFieldWithinBounds, setIsFieldWithinBounds] = useState(false);
const [coords, setCoords] = useState({
x: 0,
@ -365,8 +399,8 @@ export const AddTemplateFieldsFormPartial = ({
<>
{showAdvancedSettings && currentField ? (
<FieldAdvancedSettings
title="Advanced settings"
description={`Configure the ${FRIENDLY_FIELD_TYPE[currentField.type]} field`}
title={msg`Advanced settings`}
description={msg`Configure the ${FRIENDLY_FIELD_TYPE[currentField.type]} field`}
field={currentField}
fields={localFields}
onAdvancedSettings={handleAdvancedSettings}
@ -460,14 +494,15 @@ export const AddTemplateFieldsFormPartial = ({
<CommandEmpty>
<span className="text-muted-foreground inline-block px-4">
No recipient matching this description was found.
<Trans>No recipient matching this description was found.</Trans>
</span>
</CommandEmpty>
{recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => (
<CommandGroup key={roleIndex}>
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
{`${RECIPIENT_ROLES_DESCRIPTION[role].roleName}s`}
{/* Todo: Translations - Add plural translations. */}
{`${RECIPIENT_ROLES_DESCRIPTION_ENG[role].roleName}s`}
</div>
{roleRecipients.length === 0 && (
@ -475,7 +510,7 @@ export const AddTemplateFieldsFormPartial = ({
key={`${role}-empty`}
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs"
>
No recipients with this role
<Trans>No recipients with this role</Trans>
</div>
)}
@ -542,7 +577,7 @@ export const AddTemplateFieldsFormPartial = ({
fontCaveat.className,
)}
>
Signature
<Trans>Signature</Trans>
</p>
</CardContent>
</Card>
@ -594,7 +629,7 @@ export const AddTemplateFieldsFormPartial = ({
)}
>
<Mail className="h-4 w-4" />
Email
<Trans>Email</Trans>
</p>
</CardContent>
</Card>
@ -620,7 +655,7 @@ export const AddTemplateFieldsFormPartial = ({
)}
>
<User className="h-4 w-4" />
Name
<Trans>Name</Trans>
</p>
</CardContent>
</Card>
@ -646,7 +681,7 @@ export const AddTemplateFieldsFormPartial = ({
)}
>
<CalendarDays className="h-4 w-4" />
Date
<Trans>Date</Trans>
</p>
</CardContent>
</Card>
@ -672,7 +707,7 @@ export const AddTemplateFieldsFormPartial = ({
)}
>
<Type className="h-4 w-4" />
Text
<Trans>Text</Trans>
</p>
</CardContent>
</Card>
@ -698,7 +733,7 @@ export const AddTemplateFieldsFormPartial = ({
)}
>
<Hash className="h-4 w-4" />
Number
<Trans>Number</Trans>
</p>
</CardContent>
</Card>
@ -724,7 +759,7 @@ export const AddTemplateFieldsFormPartial = ({
)}
>
<Disc className="h-4 w-4" />
Radio
<Trans>Radio</Trans>
</p>
</CardContent>
</Card>
@ -750,7 +785,7 @@ export const AddTemplateFieldsFormPartial = ({
)}
>
<CheckSquare className="h-4 w-4" />
Checkbox
<Trans>Checkbox</Trans>
</p>
</CardContent>
</Card>
@ -776,7 +811,7 @@ export const AddTemplateFieldsFormPartial = ({
)}
>
<ChevronDown className="h-4 w-4" />
Dropdown
<Trans>Dropdown</Trans>
</p>
</CardContent>
</Card>
@ -786,17 +821,32 @@ export const AddTemplateFieldsFormPartial = ({
</div>
</DocumentFlowFormContainerContent>
{hasErrors && (
<div className="mt-4">
<ul>
<li className="text-sm text-red-500">
<Trans>
To proceed further, please set at least one value for the{' '}
{emptyCheckboxFields.length > 0
? 'Checkbox'
: emptyRadioFields.length > 0
? 'Radio'
: 'Select'}{' '}
field.
</Trans>
</li>
</ul>
</div>
)}
<DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep
title={documentFlow.title}
step={currentStep}
maxStep={totalSteps}
/>
<DocumentFlowFormContainerStep step={currentStep} maxStep={totalSteps} />
<DocumentFlowFormContainerActions
loading={isSubmitting}
disabled={isSubmitting}
goNextLabel="Save Template"
goNextLabel={msg`Save Template`}
disableNextStep={hasErrors}
onGoBackClick={() => {
previousStep();
remove();

View File

@ -5,6 +5,8 @@ import React, { useEffect, useId, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { motion } from 'framer-motion';
import { Link2Icon, Plus, Trash } from 'lucide-react';
import { useSession } from 'next-auth/react';
@ -67,6 +69,8 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
const { toast } = useToast();
const router = useRouter();
const initialId = useId();
const { _ } = useLingui();
const { data: session } = useSession();
const user = session?.user;
@ -290,13 +294,15 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
})}
>
{!showAdvancedSettings && index === 0 && (
<FormLabel required>Email</FormLabel>
<FormLabel required>
<Trans>Email</Trans>
</FormLabel>
)}
<FormControl>
<Input
type="email"
placeholder="Email"
placeholder={_(msg`Email`)}
{...field}
disabled={field.disabled || isSubmitting}
onBlur={() => void handleOnBlur(index)}
@ -318,11 +324,15 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
'col-span-4': showAdvancedSettings,
})}
>
{!showAdvancedSettings && index === 0 && <FormLabel>Name</FormLabel>}
{!showAdvancedSettings && index === 0 && (
<FormLabel>
<Trans>Name</Trans>
</FormLabel>
)}
<FormControl>
<Input
placeholder="Name"
placeholder={_(msg`Name`)}
{...field}
disabled={field.disabled || isSubmitting}
onBlur={() => void handleOnBlur(index)}
@ -382,12 +392,14 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
</TooltipTrigger>
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
<h3 className="text-foreground text-lg font-semibold">
Direct link receiver
<Trans>Direct link receiver</Trans>
</h3>
<p className="text-muted-foreground mt-1">
This field cannot be modified or deleted. When you share this template's
direct link or add it to your public profile, anyone who accesses it can
input their name and email, and fill in the fields assigned to them.
<Trans>
This field cannot be modified or deleted. When you share this template's
direct link or add it to your public profile, anyone who accesses it can
input their name and email, and fill in the fields assigned to them.
</Trans>
</p>
</TooltipContent>
</Tooltip>
@ -423,7 +435,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
onClick={() => onAddPlaceholderRecipient()}
>
<Plus className="-ml-1 mr-2 h-5 w-5" />
Add Placeholder Recipient
<Trans>Add Placeholder Recipient</Trans>
</Button>
<Button
@ -437,7 +449,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
onClick={() => onAddPlaceholderSelfRecipient()}
>
<Plus className="-ml-1 mr-2 h-5 w-5" />
Add Myself
<Trans>Add Myself</Trans>
</Button>
</div>
@ -455,7 +467,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
className="text-muted-foreground ml-2 text-sm"
htmlFor="showAdvancedRecipientSettings"
>
Show advanced settings
<Trans>Show advanced settings</Trans>
</label>
</div>
)}
@ -464,11 +476,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
</DocumentFlowFormContainerContent>
<DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep
title={documentFlow.title}
step={currentStep}
maxStep={totalSteps}
/>
<DocumentFlowFormContainerStep step={currentStep} maxStep={totalSteps} />
<DocumentFlowFormContainerActions
loading={isSubmitting}

View File

@ -3,6 +3,8 @@
import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
import { InfoIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
@ -72,6 +74,8 @@ export const AddTemplateSettingsFormPartial = ({
template,
onSubmit,
}: AddTemplateSettingsFormProps) => {
const { _ } = useLingui();
const { documentAuthOption } = extractDocumentAuthMethods({
documentAuth: template.authOptions,
});
@ -126,7 +130,9 @@ export const AddTemplateSettingsFormPartial = ({
name="title"
render={({ field }) => (
<FormItem>
<FormLabel required>Template title</FormLabel>
<FormLabel required>
<Trans>Template title</Trans>
</FormLabel>
<FormControl>
<Input className="bg-background" {...field} />
@ -142,7 +148,7 @@ export const AddTemplateSettingsFormPartial = ({
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
Document access
<Trans>Document access</Trans>
<DocumentGlobalAuthAccessTooltip />
</FormLabel>
@ -160,7 +166,7 @@ export const AddTemplateSettingsFormPartial = ({
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
Recipient action authentication
<Trans>Recipient action authentication</Trans>
<DocumentGlobalAuthActionTooltip />
</FormLabel>
@ -175,7 +181,7 @@ export const AddTemplateSettingsFormPartial = ({
<Accordion type="multiple">
<AccordionItem value="email-options" className="border-none">
<AccordionTrigger className="text-foreground rounded border px-3 py-2 text-left hover:bg-neutral-200/30 hover:no-underline">
Email Options
<Trans>Email Options</Trans>
</AccordionTrigger>
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-4 text-sm leading-relaxed [&>div]:pb-0">
@ -186,7 +192,9 @@ export const AddTemplateSettingsFormPartial = ({
render={({ field }) => (
<FormItem>
<FormLabel>
Subject <span className="text-muted-foreground">(Optional)</span>
<Trans>
Subject <span className="text-muted-foreground">(Optional)</span>
</Trans>
</FormLabel>
<FormControl>
@ -204,7 +212,9 @@ export const AddTemplateSettingsFormPartial = ({
render={({ field }) => (
<FormItem>
<FormLabel>
Message <span className="text-muted-foreground">(Optional)</span>
<Trans>
Message <span className="text-muted-foreground">(Optional)</span>
</Trans>
</FormLabel>
<FormControl>
@ -225,7 +235,7 @@ export const AddTemplateSettingsFormPartial = ({
<Accordion type="multiple">
<AccordionItem value="advanced-options" className="border-none">
<AccordionTrigger className="text-foreground rounded border px-3 py-2 text-left hover:bg-neutral-200/30 hover:no-underline">
Advanced Options
<Trans>Advanced Options</Trans>
</AccordionTrigger>
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-4 text-sm leading-relaxed">
@ -236,15 +246,17 @@ export const AddTemplateSettingsFormPartial = ({
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
External ID{' '}
<Trans>External ID</Trans>{' '}
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
Add an external ID to the template. This can be used to identify in
external systems.
<Trans>
Add an external ID to the template. This can be used to identify
in external systems.
</Trans>
</TooltipContent>
</Tooltip>
</FormLabel>
@ -263,7 +275,9 @@ export const AddTemplateSettingsFormPartial = ({
name="meta.dateFormat"
render={({ field }) => (
<FormItem>
<FormLabel>Date Format</FormLabel>
<FormLabel>
<Trans>Date Format</Trans>
</FormLabel>
<FormControl>
<Select {...field} onValueChange={field.onChange}>
@ -291,7 +305,9 @@ export const AddTemplateSettingsFormPartial = ({
name="meta.timezone"
render={({ field }) => (
<FormItem>
<FormLabel>Time Zone</FormLabel>
<FormLabel>
<Trans>Time Zone</Trans>
</FormLabel>
<FormControl>
<Combobox
@ -313,14 +329,16 @@ export const AddTemplateSettingsFormPartial = ({
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
Redirect URL{' '}
<Trans>Redirect URL</Trans>{' '}
<Tooltip>
<TooltipTrigger>
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
Add a URL to redirect the user to once the document is signed
<Trans>
Add a URL to redirect the user to once the document is signed
</Trans>
</TooltipContent>
</Tooltip>
</FormLabel>
@ -342,11 +360,7 @@ export const AddTemplateSettingsFormPartial = ({
</DocumentFlowFormContainerContent>
<DocumentFlowFormContainerFooter>
<DocumentFlowFormContainerStep
title={documentFlow.title}
step={currentStep}
maxStep={totalSteps}
/>
<DocumentFlowFormContainerStep step={currentStep} maxStep={totalSteps} />
<DocumentFlowFormContainerActions
loading={form.formState.isSubmitting}