Merge branch 'main' into feat/document-file-conversion

Resolved conflicts:
- create-document-data.ts: merged initialData + originalData/originalMimeType
- files.helpers.ts: kept both imports
- envelope-drop-zone-wrapper.tsx: adopted buildDropzoneRejectionDescription
- create-envelope-items.ts: adopted UNSAFE_createEnvelopeItems
- create-envelope.ts: integrated convertToPdfIfNeeded into createEnvelopeRouteCaller

Extended putPdfFileServerSide to accept { initialData, originalData,
originalMimeType } options; updated seal-document.handler call site.
This commit is contained in:
ephraimduncan
2026-04-20 10:11:35 +00:00
1127 changed files with 107456 additions and 22991 deletions
+2
View File
@@ -20,6 +20,8 @@ const badgeVariants = cva(
'bg-green-50 text-green-700 ring-green-600/20 dark:bg-green-500/10 dark:text-green-400 dark:ring-green-500/20',
secondary:
'bg-blue-50 text-blue-700 ring-blue-700/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/30',
orange:
'bg-orange-50 text-orange-700 ring-orange-700/10 dark:bg-orange-400/10 dark:text-orange-400 dark:ring-orange-400/30',
},
size: {
small: 'px-1.5 py-0.5 text-xs',
+22 -2
View File
@@ -4,6 +4,7 @@ import { Trans } from '@lingui/react/macro';
import type {
ColumnDef,
PaginationState,
RowSelectionState,
Table as TTable,
Updater,
VisibilityState,
@@ -15,7 +16,7 @@ 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 type { ColumnDef as DataTableColumnDef, RowSelectionState } from '@tanstack/react-table';
export interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
@@ -40,6 +41,10 @@ export interface DataTableProps<TData, TValue> {
enable: boolean;
component?: React.ReactNode;
};
enableRowSelection?: boolean;
rowSelection?: RowSelectionState;
onRowSelectionChange?: (selection: RowSelectionState) => void;
getRowId?: (row: TData) => string;
}
export function DataTable<TData, TValue>({
@@ -58,6 +63,10 @@ export function DataTable<TData, TValue>({
rowClassName,
children,
emptyState,
enableRowSelection,
rowSelection,
onRowSelectionChange,
getRowId,
}: DataTableProps<TData, TValue>) {
const pagination = useMemo<PaginationState>(() => {
if (currentPage !== undefined && perPage !== undefined) {
@@ -85,6 +94,13 @@ export function DataTable<TData, TValue>({
}
};
const onTableRowSelectionChange = (updater: Updater<RowSelectionState>) => {
if (onRowSelectionChange) {
const newSelection = typeof updater === 'function' ? updater(rowSelection ?? {}) : updater;
onRowSelectionChange(newSelection);
}
};
const table = useReactTable({
data,
columns,
@@ -92,10 +108,14 @@ export function DataTable<TData, TValue>({
state: {
pagination: manualPagination ? pagination : undefined,
columnVisibility,
rowSelection: rowSelection ?? {},
},
manualPagination,
pageCount: totalPages,
onPaginationChange: onTablePaginationChange,
enableRowSelection,
onRowSelectionChange: onTableRowSelectionChange,
getRowId,
});
return (
@@ -162,7 +182,7 @@ export function DataTable<TData, TValue>({
{hasFilters && onClearFilters !== undefined && (
<button
onClick={() => onClearFilters()}
className="text-foreground mt-1 text-sm"
className="mt-1 text-sm text-foreground"
>
<Trans>Clear filters</Trans>
</button>
@@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Field, Recipient } from '@prisma/client';
import type { Field } from '@prisma/client';
import { FieldType, Prisma, RecipientRole, SendStatus } from '@prisma/client';
import {
CalendarDays,
@@ -23,11 +23,12 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { PDF_VIEWER_PAGE_SELECTOR, getPdfPagesCount } from '@documenso/lib/constants/pdf-viewer';
import {
type TFieldMetaSchema as FieldMeta,
ZFieldMetaSchema,
} from '@documenso/lib/types/field-meta';
import type { TRecipientLite } from '@documenso/lib/types/recipient';
import { nanoid } from '@documenso/lib/universal/id';
import { ADVANCED_FIELD_TYPES_WITH_OPTIONAL_SETTING } from '@documenso/lib/utils/advanced-fields-helpers';
import { validateFieldsUninserted } from '@documenso/lib/utils/fields';
@@ -35,10 +36,11 @@ import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
import {
canRecipientBeModified,
canRecipientFieldsBeModified,
getRecipientsWithMissingFields,
} from '@documenso/lib/utils/recipients';
import { FieldToolTip } from '../../components/field/field-tooltip';
import { useRecipientColors } from '../../lib/recipient-colors';
import { getRecipientColorStyles } from '../../lib/recipient-colors';
import { cn } from '../../lib/utils';
import { Alert, AlertDescription } from '../alert';
import { Card, CardContent } from '../card';
@@ -82,7 +84,7 @@ export type FieldFormType = {
export type AddFieldsFormProps = {
documentFlow: DocumentFlowStep;
hideRecipients?: boolean;
recipients: Recipient[];
recipients: TRecipientLite[];
fields: Field[];
onSubmit: (_data: TAddFieldsFormSchema) => void;
onAutoSave: (_data: TAddFieldsFormSchema) => Promise<void>;
@@ -171,7 +173,7 @@ export const AddFieldsFormPartial = ({
});
const [selectedField, setSelectedField] = useState<FieldType | null>(null);
const [selectedSigner, setSelectedSigner] = useState<Recipient | null>(null);
const [selectedSigner, setSelectedSigner] = useState<TRecipientLite | null>(null);
const [lastActiveField, setLastActiveField] = useState<TAddFieldsFormSchema['fields'][0] | null>(
null,
);
@@ -179,9 +181,7 @@ export const AddFieldsFormPartial = ({
null,
);
const selectedSignerIndex = recipients.findIndex((r) => r.id === selectedSigner?.id);
const selectedSignerStyles = useRecipientColors(
selectedSignerIndex === -1 ? 0 : selectedSignerIndex,
);
const selectedSignerStyles = getRecipientColorStyles(selectedSignerIndex);
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
@@ -430,13 +430,15 @@ export const AddFieldsFormPartial = ({
}
if (duplicateAll) {
const pages = Array.from(document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR));
const totalPages = getPdfPagesCount();
pages.forEach((_, index) => {
const pageNumber = index + 1;
if (totalPages < 1) {
return;
}
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber += 1) {
if (pageNumber === lastActiveField.pageNumber) {
return;
continue;
}
const newField: TAddFieldsFormSchema['fields'][0] = {
@@ -449,7 +451,7 @@ export const AddFieldsFormPartial = ({
};
append(newField);
});
}
return;
}
@@ -535,7 +537,7 @@ export const AddFieldsFormPartial = ({
}, [recipients]);
const recipientsByRole = useMemo(() => {
const recipientsByRole: Record<RecipientRole, Recipient[]> = {
const recipientsByRole: Record<RecipientRole, TRecipientLite[]> = {
CC: [],
VIEWER: [],
SIGNER: [],
@@ -555,15 +557,11 @@ export const AddFieldsFormPartial = ({
};
const handleGoNextClick = () => {
const everySignerHasSignature = recipientsByRole.SIGNER.every((signer) =>
localFields.some(
(field) =>
(field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE) &&
field.signerEmail === signer.email,
),
);
// localFields already have recipientId set correctly (see field creation at line 338)
// Using the existing recipientId is important for handling duplicate email recipients
const recipientsMissingFields = getRecipientsWithMissingFields(recipients, localFields);
if (!everySignerHasSignature) {
if (recipientsMissingFields.length > 0) {
setIsMissingSignatureDialogVisible(true);
return;
}
@@ -756,7 +754,7 @@ export const AddFieldsFormPartial = ({
)}
>
<Contact className="h-4 w-4" />
Initials
<Trans>Initials</Trans>
</p>
</CardContent>
</Card>
@@ -906,7 +904,7 @@ export const AddFieldsFormPartial = ({
)}
>
<Disc className="h-4 w-4" />
Radio
<Trans>Radio</Trans>
</p>
</CardContent>
</Card>
@@ -931,8 +929,7 @@ export const AddFieldsFormPartial = ({
)}
>
<CheckSquare className="h-4 w-4" />
{/* Not translated on purpose. */}
Checkbox
<Trans>Checkbox</Trans>
</p>
</CardContent>
</Card>
@@ -6,7 +6,6 @@ import {
DocumentStatus,
DocumentVisibility,
type Field,
type Recipient,
SendStatus,
TeamMemberRole,
} from '@prisma/client';
@@ -21,6 +20,7 @@ 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 type { TRecipientLite } from '@documenso/lib/types/recipient';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { extractTeamSignatureSettings } from '@documenso/lib/utils/teams';
import {
@@ -74,7 +74,7 @@ import type { DocumentFlowStep } from './types';
export type AddSettingsFormProps = {
documentFlow: DocumentFlowStep;
recipients: Recipient[];
recipients: TRecipientLite[];
fields: Field[];
isDocumentPdfLoaded: boolean;
document: TDocument;
@@ -242,7 +242,7 @@ export const AddSettingsFormPartial = ({
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-foreground max-w-md space-y-2 p-4">
<TooltipContent className="max-w-md space-y-2 p-4 text-foreground">
<Trans>
Controls the language for the document, including the language to be used
for email notifications, and the final certificate that is generated and
@@ -269,7 +269,7 @@ export const AddSettingsFormPartial = ({
<SelectContent>
{Object.entries(SUPPORTED_LANGUAGES).map(([code, language]) => (
<SelectItem key={code} value={code}>
{language.full}
{t(language.full)}
</SelectItem>
))}
</SelectContent>
@@ -361,11 +361,11 @@ 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">
<AccordionTrigger className="mb-2 rounded border px-3 py-2 text-left text-foreground hover:bg-neutral-200/30 hover:no-underline">
<Trans>Advanced Options</Trans>
</AccordionTrigger>
<AccordionContent className="text-muted-foreground -mx-1 px-1 pt-2 text-sm leading-relaxed">
<AccordionContent className="-mx-1 px-1 pt-2 text-sm leading-relaxed text-muted-foreground">
<div className="flex flex-col space-y-6">
<FormField
control={form.control}
@@ -379,7 +379,7 @@ export const AddSettingsFormPartial = ({
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
<TooltipContent className="max-w-xs text-muted-foreground">
<Trans>
Add an external ID to the document. This can be used to identify
the document in external systems.
@@ -418,7 +418,7 @@ export const AddSettingsFormPartial = ({
field.onChange(value);
void handleAutoSave();
}}
className="bg-background w-full"
className="w-full bg-background"
emptySelectionPlaceholder={t`Select signature types`}
/>
</FormControl>
@@ -506,7 +506,7 @@ export const AddSettingsFormPartial = ({
<InfoIcon className="mx-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
<TooltipContent className="max-w-xs text-muted-foreground">
<Trans>
Add a URL to redirect the user to once the document is signed
</Trans>
@@ -6,7 +6,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Field, Recipient } from '@prisma/client';
import type { Field } from '@prisma/client';
import { DocumentSigningOrder, RecipientRole, SendStatus } from '@prisma/client';
import { motion } from 'framer-motion';
import { GripVerticalIcon, HelpCircle, Plus, Trash } from 'lucide-react';
@@ -19,6 +19,7 @@ import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounce
import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/organisation';
import { useSession } from '@documenso/lib/client-only/providers/session';
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
import type { TRecipientLite } from '@documenso/lib/types/recipient';
import { nanoid } from '@documenso/lib/universal/id';
import { canRecipientBeModified as utilCanRecipientBeModified } from '@documenso/lib/utils/recipients';
import { trpc } from '@documenso/trpc/react';
@@ -54,12 +55,12 @@ import { SigningOrderConfirmation } from './signing-order-confirmation';
import type { DocumentFlowStep } from './types';
type AutoSaveResponse = {
recipients: Recipient[];
recipients: TRecipientLite[];
};
export type AddSignersFormProps = {
documentFlow: DocumentFlowStep;
recipients: Recipient[];
recipients: TRecipientLite[];
fields: Field[];
signingOrder?: DocumentSigningOrder | null;
allowDictateNextSigner?: boolean;
@@ -3,16 +3,14 @@ import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { z } from 'zod';
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
import { zEmail } from '@documenso/lib/utils/zod';
export const ZAddSignersFormSchema = z.object({
signers: z.array(
z.object({
formId: z.string().min(1),
nativeId: z.number().optional(),
email: z
.string()
.email({ message: msg`Invalid email`.id })
.min(1),
email: zEmail(msg`Invalid email`.id).min(1),
name: z.string(),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
@@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Field, Recipient } from '@prisma/client';
import type { Field } from '@prisma/client';
import { DocumentDistributionMethod, DocumentStatus, RecipientRole } from '@prisma/client';
import { AnimatePresence, motion } from 'framer-motion';
import { InfoIcon } from 'lucide-react';
@@ -15,6 +15,7 @@ import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/org
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import type { TDocument } from '@documenso/lib/types/document';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import type { TRecipientLite } from '@documenso/lib/types/recipient';
import { formatSigningLink } from '@documenso/lib/utils/recipients';
import { trpc } from '@documenso/trpc/react';
import { DocumentSendEmailMessageHelper } from '@documenso/ui/components/document/document-send-email-message-helper';
@@ -59,7 +60,7 @@ import type { DocumentFlowStep } from './types';
export type AddSubjectFormProps = {
documentFlow: DocumentFlowStep;
recipients: Recipient[];
recipients: TRecipientLite[];
fields: Field[];
document: TDocument;
onSubmit: (_data: TAddSubjectFormSchema) => void;
@@ -191,10 +192,10 @@ export const AddSubjectFormPartial = ({
>
<TabsList className="w-full">
<TabsTrigger className="w-full" value={DocumentDistributionMethod.EMAIL}>
Email
<Trans>Email</Trans>
</TabsTrigger>
<TabsTrigger className="w-full" value={DocumentDistributionMethod.NONE}>
None
<Trans>None</Trans>
</TabsTrigger>
</TabsList>
</Tabs>
@@ -2,14 +2,12 @@ import { DocumentDistributionMethod } from '@prisma/client';
import { z } from 'zod';
import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';
import { zEmail } from '@documenso/lib/utils/zod';
export const ZAddSubjectFormSchema = z.object({
meta: z.object({
emailId: z.string().nullable(),
emailReplyTo: z.preprocess(
(val) => (val === '' ? undefined : val),
z.string().email().optional(),
),
emailReplyTo: z.preprocess((val) => (val === '' ? undefined : val), zEmail().optional()),
// emailReplyName: z.string().optional(),
subject: z.string(),
message: z.string(),
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import { FieldType } from '@prisma/client';
import { CopyPlus, Settings2, SquareStack, Trash } from 'lucide-react';
import { createPortal } from 'react-dom';
@@ -9,11 +10,12 @@ import { Rnd } from 'react-rnd';
import { useSearchParams } from 'react-router';
import { useElementBounds } from '@documenso/lib/client-only/hooks/use-element-bounds';
import { useIsPageInDom } from '@documenso/lib/client-only/hooks/use-is-page-in-dom';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
import { useRecipientColors } from '../../lib/recipient-colors';
import { getRecipientColorStyles } from '../../lib/recipient-colors';
import { cn } from '../../lib/utils';
import { FieldContent } from './field-content';
import type { TDocumentFlowFormSchema } from './types';
@@ -49,7 +51,17 @@ export type FieldItemProps = {
/**
* The item when editing fields??
*/
export const FieldItem = ({
export const FieldItem = (props: FieldItemProps) => {
const isPageInDom = useIsPageInDom(props.field.pageNumber);
if (!isPageInDom) {
return null;
}
return <FieldItemInner {...props} />;
};
const FieldItemInner = ({
fieldClassName,
field,
passive,
@@ -88,7 +100,7 @@ export const FieldItem = ({
`${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`,
);
const signerStyles = useRecipientColors(recipientIndex);
const signerStyles = getRecipientColorStyles(recipientIndex);
const isDevMode = searchParams.get('devmode') === 'true';
@@ -321,22 +333,44 @@ export const FieldItem = ({
</div>
{isDevMode && (
<div className="absolute -top-20 left-1/2 z-50 -translate-x-1/2 rounded-md border border-border bg-background/95 px-2 py-1 shadow-sm backdrop-blur-sm">
<div className="absolute bottom-full left-1/2 z-50 mb-1 -translate-x-1/2 rounded-md border border-border bg-background/95 px-2 py-1 shadow-sm backdrop-blur-sm">
<div className="flex flex-col gap-0.5 text-[9px]">
{field.nativeId && (
<span>
<span className="text-muted-foreground">
<Trans>Field ID:</Trans>
</span>{' '}
<span className="font-mono text-foreground">{field.nativeId}</span>
</span>
)}
<span>
<span className="text-muted-foreground">Pos X:&nbsp;</span>
<span className="text-muted-foreground">
<Trans>Recipient ID:</Trans>
</span>{' '}
<span className="font-mono text-foreground">{field.recipientId}</span>
</span>
<span>
<span className="text-muted-foreground">
<Trans>Pos X:</Trans>
</span>{' '}
<span className="font-mono text-foreground">{field.pageX.toFixed(2)}</span>
</span>
<span>
<span className="text-muted-foreground">Pos Y:&nbsp;</span>
<span className="text-muted-foreground">
<Trans>Pos Y:</Trans>
</span>{' '}
<span className="font-mono text-foreground">{field.pageY.toFixed(2)}</span>
</span>
<span>
<span className="text-muted-foreground">Width:&nbsp;</span>
<span className="text-muted-foreground">
<Trans>Width:</Trans>
</span>{' '}
<span className="font-mono text-foreground">{field.pageWidth.toFixed(2)}</span>
</span>
<span>
<span className="text-muted-foreground">Height:&nbsp;</span>
<span className="text-muted-foreground">
<Trans>Height:</Trans>
</span>{' '}
<span className="font-mono text-foreground">{field.pageHeight.toFixed(2)}</span>
</span>
</div>
@@ -36,14 +36,14 @@ export const DropdownFieldAdvancedSettings = ({
const { _ } = useLingui();
const [showValidation, setShowValidation] = useState(false);
const [values, setValues] = useState(fieldState.values ?? [{ value: 'Option 1' }]);
const [values, setValues] = useState(fieldState.values ?? [{ value: _(msg`Option 1`) }]);
const [readOnly, setReadOnly] = useState(fieldState.readOnly ?? false);
const [required, setRequired] = useState(fieldState.required ?? false);
const [defaultValue, setDefaultValue] = useState(fieldState.defaultValue ?? 'Option 1');
const [defaultValue, setDefaultValue] = useState(fieldState.defaultValue ?? _(msg`Option 1`));
const addValue = () => {
setValues([...values, { value: 'New option' }]);
handleFieldChange('values', [...values, { value: 'New option' }]);
setValues([...values, { value: _(msg`New option`) }]);
handleFieldChange('values', [...values, { value: _(msg`New option`) }]);
};
const removeValue = (index: number) => {
@@ -90,11 +90,11 @@ export const DropdownFieldAdvancedSettings = ({
}, [values]);
useEffect(() => {
setValues(fieldState.values ?? [{ value: 'Option 1' }]);
setValues(fieldState.values ?? [{ value: _(msg`Option 1`) }]);
}, [fieldState.values]);
useEffect(() => {
setDefaultValue(fieldState.defaultValue ?? 'Option 1');
setDefaultValue(fieldState.defaultValue ?? _(msg`Option 1`));
}, [fieldState.defaultValue]);
return (
@@ -114,7 +114,7 @@ export const DropdownFieldAdvancedSettings = ({
handleFieldChange('defaultValue', val);
}}
>
<SelectTrigger className="text-muted-foreground bg-background mt-2 w-full">
<SelectTrigger className="mt-2 w-full bg-background text-muted-foreground">
<SelectValue defaultValue={defaultValue} placeholder={`-- ${_(msg`Select`)} --`} />
</SelectTrigger>
<SelectContent position="popper">
@@ -152,7 +152,7 @@ export const DropdownFieldAdvancedSettings = ({
</div>
</div>
<Button
className="bg-foreground/10 hover:bg-foreground/5 border-foreground/10 mt-2 border"
className="mt-2 border border-foreground/10 bg-foreground/10 hover:bg-foreground/5"
variant="outline"
onClick={() => setShowValidation((prev) => !prev)}
>
@@ -183,7 +183,7 @@ export const DropdownFieldAdvancedSettings = ({
</div>
))}
<Button
className="bg-foreground/10 hover:bg-foreground/5 border-foreground/10 ml-9 mt-4 border"
className="ml-9 mt-4 border border-foreground/10 bg-foreground/10 hover:bg-foreground/5"
variant="outline"
onClick={addValue}
>
@@ -31,7 +31,7 @@ export const RadioFieldAdvancedSettings = ({
const [showValidation, setShowValidation] = useState(false);
const [values, setValues] = useState(
fieldState.values ?? [{ id: 1, checked: false, value: 'Default value' }],
fieldState.values ?? [{ id: 1, checked: false, value: _(msg`Default value`) }],
);
const [readOnly, setReadOnly] = useState(fieldState.readOnly ?? false);
const [required, setRequired] = useState(fieldState.required ?? false);
@@ -99,7 +99,7 @@ export const RadioFieldAdvancedSettings = ({
};
useEffect(() => {
setValues(fieldState.values ?? [{ id: 1, checked: false, value: 'Default value' }]);
setValues(fieldState.values ?? [{ id: 1, checked: false, value: _(msg`Default value`) }]);
}, [fieldState.values]);
useEffect(() => {
@@ -122,7 +122,7 @@ export const RadioFieldAdvancedSettings = ({
</Label>
<Input
id="label"
className="bg-background mt-2"
className="mt-2 bg-background"
placeholder={_(msg`Field label`)}
value={fieldState.label}
onChange={(e) => handleFieldChange('label', e.target.value)}
@@ -150,7 +150,7 @@ export const RadioFieldAdvancedSettings = ({
</div>
</div>
<Button
className="bg-foreground/10 hover:bg-foreground/5 border-foreground/10 mt-2 border"
className="mt-2 border border-foreground/10 bg-foreground/10 hover:bg-foreground/5"
variant="outline"
onClick={() => setShowValidation((prev) => !prev)}
>
@@ -167,7 +167,7 @@ export const RadioFieldAdvancedSettings = ({
{values.map((value) => (
<div key={value.id} className="mt-2 flex items-center gap-4">
<Checkbox
className="data-[state=checked]:bg-documenso border-foreground/30 data-[state=checked]:ring-primary dark:data-[state=checked]:ring-offset-background h-5 w-5 rounded-full data-[state=checked]:ring-1 data-[state=checked]:ring-offset-2 data-[state=checked]:ring-offset-white"
className="h-5 w-5 rounded-full border-foreground/30 data-[state=checked]:bg-documenso data-[state=checked]:ring-1 data-[state=checked]:ring-primary data-[state=checked]:ring-offset-2 data-[state=checked]:ring-offset-white dark:data-[state=checked]:ring-offset-background"
checked={value.checked}
onCheckedChange={(checked) => handleCheckedChange(Boolean(checked), value.id)}
/>
@@ -186,7 +186,7 @@ export const RadioFieldAdvancedSettings = ({
</div>
))}
<Button
className="bg-foreground/10 hover:bg-foreground/5 border-foreground/10 ml-9 mt-4 border"
className="ml-9 mt-4 border border-foreground/10 bg-foreground/10 hover:bg-foreground/5"
variant="outline"
onClick={addValue}
>
@@ -4,6 +4,7 @@ import { FieldType } from '@prisma/client';
import { z } from 'zod';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { zEmail } from '@documenso/lib/utils/zod';
export const ZDocumentFlowFormSchema = z.object({
title: z.string().min(1),
@@ -12,7 +13,7 @@ export const ZDocumentFlowFormSchema = z.object({
z.object({
formId: z.string().min(1),
nativeId: z.number().optional(),
email: z.string().min(1).email(),
email: zEmail().min(1),
name: z.string(),
}),
),
+5 -4
View File
@@ -1,3 +1,4 @@
import { Trans } from '@lingui/react/macro';
import { FieldType } from '@prisma/client';
import {
CalendarDays,
@@ -103,10 +104,10 @@ export const FieldSelector = ({
)}
>
<CardContent className="relative flex items-center justify-center gap-x-2 px-6 py-4">
{Icon && <Icon className="text-muted-foreground h-4 w-4" />}
{Icon && <Icon className="h-4 w-4 text-muted-foreground" />}
<span
className={cn(
'text-muted-foreground group-data-[selected]:text-foreground text-sm',
'text-sm text-muted-foreground group-data-[selected]:text-foreground',
field.type === FieldType.SIGNATURE && 'invisible',
)}
>
@@ -114,8 +115,8 @@ export const FieldSelector = ({
</span>
{field.type === FieldType.SIGNATURE && (
<div className="text-muted-foreground font-signature absolute inset-0 flex items-center justify-center text-lg">
Signature
<div className="absolute inset-0 flex items-center justify-center font-signature text-lg text-muted-foreground">
<Trans>Signature</Trans>
</div>
)}
</CardContent>
@@ -1,10 +0,0 @@
import {
type LoadedPDFDocument,
type OnPDFViewerPageClick,
PDFViewer,
type PDFViewerProps,
} from './base';
export { PDFViewer, type LoadedPDFDocument, type OnPDFViewerPageClick, type PDFViewerProps };
export default PDFViewer;
-290
View File
@@ -1,290 +0,0 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { EnvelopeItem } from '@prisma/client';
import { base64 } from '@scure/base';
import { Loader } from 'lucide-react';
import { type PDFDocumentProxy } from 'pdfjs-dist';
import { Document as PDFDocument, Page as PDFPage, pdfjs } from 'react-pdf';
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { getEnvelopeItemPdfUrl } from '@documenso/lib/utils/envelope-download';
import { cn } from '../../lib/utils';
import { useToast } from '../use-toast';
export type LoadedPDFDocument = PDFDocumentProxy;
/**
* This imports the worker from the `pdfjs-dist` package.
* Wrapped in typeof window check to prevent SSR evaluation.
*/
if (typeof window !== 'undefined') {
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/legacy/build/pdf.worker.min.mjs',
import.meta.url,
).toString();
}
const pdfViewerOptions = {
cMapUrl: `${NEXT_PUBLIC_WEBAPP_URL()}/static/cmaps/`,
};
export type OnPDFViewerPageClick = (_event: {
pageNumber: number;
numPages: number;
originalEvent: React.MouseEvent<HTMLDivElement, MouseEvent>;
pageHeight: number;
pageWidth: number;
pageX: number;
pageY: number;
}) => void | Promise<void>;
const PDFLoader = () => (
<>
<Loader className="h-12 w-12 animate-spin text-documenso" />
<p className="mt-4 text-muted-foreground">
<Trans>Loading document...</Trans>
</p>
</>
);
export type PDFViewerProps = {
className?: string;
envelopeItem: Pick<EnvelopeItem, 'id' | 'envelopeId'>;
token: string | undefined;
presignToken?: string | undefined;
version: 'original' | 'signed';
onDocumentLoad?: (_doc: LoadedPDFDocument) => void;
onPageClick?: OnPDFViewerPageClick;
overrideData?: string;
customPageRenderer?: React.FunctionComponent;
[key: string]: unknown;
} & Omit<React.HTMLAttributes<HTMLDivElement>, 'onPageClick'>;
export const PDFViewer = ({
className,
envelopeItem,
token,
presignToken,
version,
onDocumentLoad,
onPageClick,
overrideData,
customPageRenderer,
...props
}: PDFViewerProps) => {
const { _ } = useLingui();
const { toast } = useToast();
const $el = useRef<HTMLDivElement>(null);
const [isDocumentBytesLoading, setIsDocumentBytesLoading] = useState(false);
const [documentBytes, setDocumentBytes] = useState<Uint8Array | null>(
overrideData ? base64.decode(overrideData) : null,
);
const [width, setWidth] = useState(0);
const [numPages, setNumPages] = useState(0);
const [pdfError, setPdfError] = useState(false);
const isLoading = isDocumentBytesLoading || !documentBytes;
const envelopeItemFile = useMemo(() => {
if (!documentBytes) {
return null;
}
return {
data: documentBytes,
};
}, [documentBytes]);
const onDocumentLoaded = (doc: LoadedPDFDocument) => {
setNumPages(doc.numPages);
onDocumentLoad?.(doc);
};
const onDocumentPageClick = (
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
pageNumber: number,
) => {
const $el = event.target instanceof HTMLElement ? event.target : null;
if (!$el) {
return;
}
const $page = $el.closest(PDF_VIEWER_PAGE_SELECTOR);
if (!$page) {
return;
}
const { height, width, top, left } = $page.getBoundingClientRect();
const pageX = event.clientX - left;
const pageY = event.clientY - top;
if (onPageClick) {
void onPageClick({
pageNumber,
numPages,
originalEvent: event,
pageHeight: height,
pageWidth: width,
pageX,
pageY,
});
}
};
useEffect(() => {
if ($el.current) {
const $current = $el.current;
const { width } = $current.getBoundingClientRect();
setWidth(width);
const onResize = () => {
const { width } = $current.getBoundingClientRect();
setWidth(width);
};
window.addEventListener('resize', onResize);
return () => {
window.removeEventListener('resize', onResize);
};
}
}, []);
useEffect(() => {
if (overrideData) {
const bytes = base64.decode(overrideData);
setDocumentBytes(bytes);
return;
}
const fetchDocumentBytes = async () => {
try {
setIsDocumentBytesLoading(true);
const documentUrl = getEnvelopeItemPdfUrl({
type: 'view',
envelopeItem: envelopeItem,
token,
presignToken,
});
const bytes = await fetch(documentUrl).then(async (res) => await res.arrayBuffer());
setDocumentBytes(new Uint8Array(bytes));
setIsDocumentBytesLoading(false);
} catch (err) {
console.error(err);
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while loading the document.`),
variant: 'destructive',
});
}
};
void fetchDocumentBytes();
}, [envelopeItem.envelopeId, envelopeItem.id, token, version, toast, overrideData]);
return (
<div ref={$el} className={cn('overflow-hidden', className)} {...props}>
{isLoading ? (
<div
className={cn(
'flex h-[80vh] max-h-[60rem] w-full flex-col items-center justify-center overflow-hidden rounded',
)}
>
<PDFLoader />
</div>
) : (
<>
<PDFDocument
file={envelopeItemFile}
className={cn('w-full overflow-hidden rounded', {
'h-[80vh] max-h-[60rem]': numPages === 0,
})}
onLoadSuccess={(d) => onDocumentLoaded(d)}
// Uploading a invalid document causes an error which doesn't appear to be handled by the `error` prop.
// Therefore we add some additional custom error handling.
onSourceError={() => {
setPdfError(true);
}}
externalLinkTarget="_blank"
loading={
<div className="flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50 dark:bg-background">
{pdfError ? (
<div className="text-center text-muted-foreground">
<p>
<Trans>Something went wrong while loading the document.</Trans>
</p>
<p className="mt-1 text-sm">
<Trans>Please try again or contact our support.</Trans>
</p>
</div>
) : (
<PDFLoader />
)}
</div>
}
error={
<div className="flex h-[80vh] max-h-[60rem] flex-col items-center justify-center bg-white/50 dark:bg-background">
<div className="text-center text-muted-foreground">
<p>
<Trans>Something went wrong while loading the document.</Trans>
</p>
<p className="mt-1 text-sm">
<Trans>Please try again or contact our support.</Trans>
</p>
</div>
</div>
}
options={pdfViewerOptions}
>
{Array(numPages)
.fill(null)
.map((_, i) => (
<div key={i} className="last:-mb-2">
<div className="overflow-hidden rounded border border-border will-change-transform">
<PDFPage
pageNumber={i + 1}
width={width}
renderAnnotationLayer={false}
renderTextLayer={false}
loading={() => ''}
renderMode={customPageRenderer ? 'custom' : 'canvas'}
customRenderer={customPageRenderer}
onClick={(e) => onDocumentPageClick(e, i + 1)}
/>
</div>
<p className="my-2 text-center text-[11px] text-muted-foreground/80">
<Trans>
Page {i + 1} of {numPages}
</Trans>
</p>
</div>
))}
</PDFDocument>
</>
)}
</div>
);
};
export default PDFViewer;
@@ -1 +0,0 @@
export * from './base';
@@ -1,7 +0,0 @@
import { ClientOnly } from '../../components/client-only';
import { PDFViewer, type PDFViewerProps } from './base.client';
export const PDFViewerLazy = (props: PDFViewerProps) => {
return <ClientOnly fallback={<div>Loading...</div>}>{() => <PDFViewer {...props} />}</ClientOnly>;
};
+17 -25
View File
@@ -2,12 +2,12 @@ import { useCallback, useMemo, useState } from 'react';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Recipient } from '@prisma/client';
import { RecipientRole, SendStatus, SigningStatus } from '@prisma/client';
import { Check, ChevronsUpDown, Info } from 'lucide-react';
import { sortBy } from 'remeda';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import type { TRecipientLite } from '@documenso/lib/types/recipient';
import { getRecipientColorStyles } from '../lib/recipient-colors';
import { cn } from '../lib/utils';
@@ -18,9 +18,9 @@ import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip';
export interface RecipientSelectorProps {
className?: string;
selectedRecipient: Recipient | null;
onSelectedRecipientChange: (recipient: Recipient) => void;
recipients: Recipient[];
selectedRecipient: TRecipientLite | null;
onSelectedRecipientChange: (recipient: TRecipientLite) => void;
recipients: TRecipientLite[];
align?: 'center' | 'end' | 'start';
}
@@ -35,7 +35,7 @@ export const RecipientSelector = ({
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
const recipientsByRole = useMemo(() => {
const recipientsWithRole: Record<RecipientRole, Recipient[]> = {
const recipientsWithRole: Record<RecipientRole, TRecipientLite[]> = {
CC: [],
VIEWER: [],
SIGNER: [],
@@ -67,12 +67,12 @@ export const RecipientSelector = ({
[(r) => r.signingOrder || Number.MAX_SAFE_INTEGER, 'asc'],
[(r) => r.id, 'asc'],
),
] as [RecipientRole, Recipient[]],
] as [RecipientRole, TRecipientLite[]],
);
}, [recipientsByRole]);
const getRecipientLabel = useCallback(
(recipient: Recipient) => {
(recipient: TRecipientLite) => {
if (recipient.name && recipient.email) {
return `${recipient.name} (${recipient.email})`;
}
@@ -101,13 +101,9 @@ export const RecipientSelector = ({
variant="outline"
role="combobox"
className={cn(
'bg-background text-muted-foreground hover:text-foreground justify-between font-normal',
getRecipientColorStyles(
Math.max(
recipients.findIndex((r) => r.id === selectedRecipient?.id),
0,
),
).comboxBoxTrigger,
'justify-between bg-background font-normal text-muted-foreground hover:text-foreground',
getRecipientColorStyles(recipients.findIndex((r) => r.id === selectedRecipient?.id))
.comboBoxTrigger,
className,
)}
>
@@ -126,21 +122,21 @@ export const RecipientSelector = ({
<CommandInput />
<CommandEmpty>
<span className="text-muted-foreground inline-block px-4">
<span className="inline-block px-4 text-muted-foreground">
<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">
<div className="mb-1 ml-2 mt-2 text-xs font-medium text-muted-foreground">
{_(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)}
</div>
{roleRecipients.length === 0 && (
<div
key={`${role}-empty`}
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs"
className="px-4 pb-4 pt-2.5 text-center text-xs text-muted-foreground/80"
>
<Trans>No recipients with this role</Trans>
</div>
@@ -151,12 +147,8 @@ export const RecipientSelector = ({
key={recipient.id}
className={cn(
'px-2 last:mb-1 [&:not(:first-child)]:mt-1',
getRecipientColorStyles(
Math.max(
recipients.findIndex((r) => r.id === recipient.id),
0,
),
).comboxBoxItem,
getRecipientColorStyles(recipients.findIndex((r) => r.id === recipient.id))
.comboBoxItem,
{
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
},
@@ -168,7 +160,7 @@ export const RecipientSelector = ({
disabled={recipient.signingStatus !== SigningStatus.NOT_SIGNED}
>
<span
className={cn('text-foreground/70 truncate', {
className={cn('truncate text-foreground/70', {
'text-foreground/80': recipient.id === selectedRecipient?.id,
})}
>
@@ -190,7 +182,7 @@ export const RecipientSelector = ({
<Info className="ml-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs">
<TooltipContent className="max-w-xs text-muted-foreground">
<Trans>
This document has already been sent to this recipient. You can no longer
edit this recipient.
+5 -5
View File
@@ -23,7 +23,7 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'border-input ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-10 w-full items-center justify-between rounded-md border bg-transparent px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
disabled={loading || props.disabled}
@@ -56,7 +56,7 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content
ref={ref}
className={cn(
'bg-popover text-popover-foreground animate-in fade-in-80 relative z-[1001] min-w-[8rem] overflow-hidden rounded-md border shadow-md',
'relative z-[1001] min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-80',
position === 'popper' && 'translate-y-1',
className,
)}
@@ -65,7 +65,7 @@ const SelectContent = React.forwardRef<
>
<SelectPrimitive.Viewport
className={cn(
'p-1',
'max-h-60 overflow-y-auto p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
)}
@@ -98,7 +98,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
'focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
{...props}
@@ -121,7 +121,7 @@ const SelectSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn('bg-muted -mx-1 my-1 h-px', className)}
className={cn('-mx-1 my-1 h-px bg-muted', className)}
{...props}
/>
));
@@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { Field, Recipient } from '@prisma/client';
import type { Field } from '@prisma/client';
import { FieldType, RecipientRole, SendStatus } from '@prisma/client';
import {
CalendarDays,
@@ -24,13 +24,14 @@ import { useHotkeys } from 'react-hotkeys-hook';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
import { useAutoSave } from '@documenso/lib/client-only/hooks/use-autosave';
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { PDF_VIEWER_PAGE_SELECTOR, getPdfPagesCount } from '@documenso/lib/constants/pdf-viewer';
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
import {
type TFieldMetaSchema as FieldMeta,
ZFieldMetaSchema,
} from '@documenso/lib/types/field-meta';
import type { TRecipientLite } from '@documenso/lib/types/recipient';
import { nanoid } from '@documenso/lib/universal/id';
import { ADVANCED_FIELD_TYPES_WITH_OPTIONAL_SETTING } from '@documenso/lib/utils/advanced-fields-helpers';
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
@@ -57,7 +58,7 @@ import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/type
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { getRecipientColorStyles, useRecipientColors } from '../../lib/recipient-colors';
import { getRecipientColorStyles } from '../../lib/recipient-colors';
import type { FieldFormType } from '../document-flow/add-fields';
import { FieldAdvancedSettings } from '../document-flow/field-item-advanced-settings';
import { Form } from '../form/form';
@@ -75,7 +76,7 @@ const DEFAULT_WIDTH_PX = MIN_WIDTH_PX * 2.5;
export type AddTemplateFieldsFormProps = {
documentFlow: DocumentFlowStep;
recipients: Recipient[];
recipients: TRecipientLite[];
fields: Field[];
onSubmit: (_data: TAddTemplateFieldsFormSchema) => void;
onAutoSave: (_data: TAddTemplateFieldsFormSchema) => Promise<void>;
@@ -154,13 +155,11 @@ export const AddTemplateFieldsFormPartial = ({
});
const [selectedField, setSelectedField] = useState<FieldType | null>(null);
const [selectedSigner, setSelectedSigner] = useState<Recipient | null>(null);
const [selectedSigner, setSelectedSigner] = useState<TRecipientLite | null>(null);
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
const selectedSignerIndex = recipients.findIndex((r) => r.id === selectedSigner?.id);
const selectedSignerStyles = useRecipientColors(
selectedSignerIndex === -1 ? 0 : selectedSignerIndex,
);
const selectedSignerStyles = getRecipientColorStyles(selectedSignerIndex);
const onFieldCopy = useCallback(
(event?: KeyboardEvent | null, options?: { duplicate?: boolean; duplicateAll?: boolean }) => {
@@ -188,13 +187,15 @@ export const AddTemplateFieldsFormPartial = ({
}
if (duplicateAll) {
const pages = Array.from(document.querySelectorAll(PDF_VIEWER_PAGE_SELECTOR));
const totalPages = getPdfPagesCount();
pages.forEach((_, index) => {
const pageNumber = index + 1;
if (totalPages < 1) {
return;
}
for (let pageNumber = 1; pageNumber <= totalPages; pageNumber += 1) {
if (pageNumber === lastActiveField.pageNumber) {
return;
continue;
}
const newField: TAddTemplateFieldsFormSchema['fields'][0] = {
@@ -208,7 +209,7 @@ export const AddTemplateFieldsFormPartial = ({
};
append(newField);
});
}
void handleAutoSave();
return;
@@ -491,7 +492,7 @@ export const AddTemplateFieldsFormPartial = ({
}, [recipients]);
const recipientsByRole = useMemo(() => {
const recipientsByRole: Record<RecipientRole, Recipient[]> = {
const recipientsByRole: Record<RecipientRole, TRecipientLite[]> = {
CC: [],
VIEWER: [],
SIGNER: [],
@@ -520,7 +521,7 @@ export const AddTemplateFieldsFormPartial = ({
const recipientsByRoleToDisplay = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][]).filter(
return (Object.entries(recipientsByRole) as [RecipientRole, TRecipientLite[]][]).filter(
([role]) =>
role !== RecipientRole.CC &&
role !== RecipientRole.VIEWER &&
@@ -651,7 +652,7 @@ export const AddTemplateFieldsFormPartial = ({
role="combobox"
className={cn(
'mb-12 mt-2 justify-between bg-background font-normal text-muted-foreground hover:text-foreground',
selectedSignerStyles?.comboxBoxTrigger,
selectedSignerStyles?.comboBoxTrigger,
)}
>
{selectedSigner?.email &&
@@ -708,11 +709,8 @@ export const AddTemplateFieldsFormPartial = ({
className={cn(
'px-2 last:mb-1 [&:not(:first-child)]:mt-1',
getRecipientColorStyles(
Math.max(
recipients.findIndex((r) => r.id === recipient.id),
0,
),
)?.comboxBoxItem,
recipients.findIndex((r) => r.id === recipient.id),
)?.comboBoxItem,
)}
onSelect={() => {
setSelectedSigner(recipient);
@@ -797,7 +795,7 @@ export const AddTemplateFieldsFormPartial = ({
)}
>
<Contact className="h-4 w-4" />
Initials
<Trans>Initials</Trans>
</p>
</CardContent>
</Card>
@@ -953,7 +951,7 @@ export const AddTemplateFieldsFormPartial = ({
)}
>
<Disc className="h-4 w-4" />
Radio
<Trans>Radio</Trans>
</p>
</CardContent>
</Card>
@@ -979,8 +977,7 @@ export const AddTemplateFieldsFormPartial = ({
)}
>
<CheckSquare className="h-4 w-4" />
{/* Not translated on purpose. */}
Checkbox
<Trans>Checkbox</Trans>
</p>
</CardContent>
</Card>
@@ -7,7 +7,7 @@ import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { TemplateDirectLink } from '@prisma/client';
import { DocumentSigningOrder, type Field, type Recipient, RecipientRole } from '@prisma/client';
import { DocumentSigningOrder, type Field, RecipientRole } from '@prisma/client';
import { motion } from 'framer-motion';
import { GripVerticalIcon, HelpCircle, Link2Icon, Plus, Trash } from 'lucide-react';
import { useFieldArray, useForm } from 'react-hook-form';
@@ -17,6 +17,7 @@ import { useCurrentOrganisation } from '@documenso/lib/client-only/providers/org
import { useSession } from '@documenso/lib/client-only/providers/session';
import { isTemplateRecipientEmailPlaceholder } from '@documenso/lib/constants/template';
import { ZRecipientAuthOptionsSchema } from '@documenso/lib/types/document-auth';
import type { TRecipientLite } from '@documenso/lib/types/recipient';
import { nanoid } from '@documenso/lib/universal/id';
import { generateRecipientPlaceholder } from '@documenso/lib/utils/templates';
import { AnimateGenericFadeInOut } from '@documenso/ui/components/animate/animate-generic-fade-in-out';
@@ -49,12 +50,12 @@ import type { TAddTemplatePlacholderRecipientsFormSchema } from './add-template-
import { ZAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types';
type AutoSaveResponse = {
recipients: Recipient[];
recipients: TRecipientLite[];
};
export type AddTemplatePlaceholderRecipientsFormProps = {
documentFlow: DocumentFlowStep;
recipients: Recipient[];
recipients: TRecipientLite[];
fields: Field[];
signingOrder?: DocumentSigningOrder | null;
allowDictateNextSigner?: boolean;
@@ -533,7 +534,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
<Tooltip>
<TooltipTrigger asChild>
<span className="text-muted-foreground ml-1 cursor-help">
<span className="ml-1 cursor-help text-muted-foreground">
<HelpCircle className="h-3.5 w-3.5" />
</span>
</TooltipTrigger>
@@ -586,7 +587,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
{...provided.draggableProps}
{...provided.dragHandleProps}
className={cn('py-1', {
'bg-widget-foreground pointer-events-none rounded-md pt-2':
'pointer-events-none rounded-md bg-widget-foreground pt-2':
snapshot.isDragging,
})}
>
@@ -754,7 +755,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
handleRoleChange(index, value as RecipientRole);
}}
disabled={isSubmitting}
hideCCRecipients={isSignerDirectRecipient(signer)}
hideCCerRole={isSignerDirectRecipient(signer)}
/>
</FormControl>
@@ -768,11 +769,11 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
<TooltipTrigger className="col-span-1 mt-auto inline-flex h-10 w-10 items-center justify-center text-slate-500 hover:opacity-80">
<Link2Icon className="h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
<h3 className="text-foreground text-lg font-semibold">
<TooltipContent className="z-9999 max-w-md p-4 text-foreground">
<h3 className="text-lg font-semibold text-foreground">
<Trans>Direct link receiver</Trans>
</h3>
<p className="text-muted-foreground mt-1">
<p className="mt-1 text-muted-foreground">
<Trans>
This field cannot be modified or deleted. When you share
this template's direct link or add it to your public
@@ -829,7 +830,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
<Button
type="button"
className="dark:bg-muted dark:hover:bg-muted/80 bg-black/5 hover:bg-black/10"
className="bg-black/5 hover:bg-black/10 dark:bg-muted dark:hover:bg-muted/80"
variant="secondary"
disabled={
isSubmitting ||
@@ -852,7 +853,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
/>
<label
className="text-muted-foreground ml-2 text-sm"
className="ml-2 text-sm text-muted-foreground"
htmlFor="showAdvancedRecipientSettings"
>
<Trans>Show advanced settings</Trans>
@@ -2,6 +2,7 @@ import { DocumentSigningOrder, RecipientRole } from '@prisma/client';
import { z } from 'zod';
import { ZRecipientActionAuthTypesSchema } from '@documenso/lib/types/document-auth';
import { zEmail } from '@documenso/lib/utils/zod';
export const ZAddTemplatePlacholderRecipientsFormSchema = z
.object({
@@ -9,7 +10,7 @@ export const ZAddTemplatePlacholderRecipientsFormSchema = z
z.object({
formId: z.string().min(1),
nativeId: z.number().optional(),
email: z.string().min(1).email(),
email: zEmail().min(1),
name: z.string().min(1, { message: 'Name is required' }),
role: z.nativeEnum(RecipientRole),
signingOrder: z.number().optional(),
@@ -2,8 +2,8 @@ import { useEffect } from 'react';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans, useLingui } from '@lingui/react/macro';
import { DocumentVisibility, TeamMemberRole } from '@prisma/client';
import { DocumentDistributionMethod, type Field, type Recipient } from '@prisma/client';
import { DocumentVisibility, TeamMemberRole, TemplateType } from '@prisma/client';
import { DocumentDistributionMethod, type Field } from '@prisma/client';
import { InfoIcon } from 'lucide-react';
import { useForm } from 'react-hook-form';
import { match } from 'ts-pattern';
@@ -19,6 +19,7 @@ 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 { TDocumentMetaDateFormat } from '@documenso/lib/types/document-meta';
import type { TRecipientLite } from '@documenso/lib/types/recipient';
import type { TTemplate } from '@documenso/lib/types/template';
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
import { extractTeamSignatureSettings } from '@documenso/lib/utils/teams';
@@ -36,6 +37,10 @@ import {
DocumentVisibilitySelect,
DocumentVisibilityTooltip,
} from '@documenso/ui/components/document/document-visibility-select';
import {
TemplateTypeSelect,
TemplateTypeTooltip,
} from '@documenso/ui/components/template/template-type-select';
import {
Accordion,
AccordionContent,
@@ -77,7 +82,7 @@ import { ZAddTemplateSettingsFormSchema } from './add-template-settings.types';
export type AddTemplateSettingsFormProps = {
documentFlow: DocumentFlowStep;
recipients: Recipient[];
recipients: TRecipientLite[];
fields: Field[];
isDocumentPdfLoaded: boolean;
template: TTemplate;
@@ -96,7 +101,7 @@ export const AddTemplateSettingsFormPartial = ({
onSubmit,
onAutoSave,
}: AddTemplateSettingsFormProps) => {
const { t, i18n } = useLingui();
const { t } = useLingui();
const organisation = useCurrentOrganisation();
@@ -109,6 +114,7 @@ export const AddTemplateSettingsFormPartial = ({
defaultValues: {
title: template.title,
externalId: template.externalId || undefined,
templateType: template.type || TemplateType.PRIVATE,
visibility: template.visibility || '',
globalAccessAuth: documentAuthOption?.globalAccessAuth || [],
globalActionAuth: documentAuthOption?.globalActionAuth || [],
@@ -262,7 +268,7 @@ export const AddTemplateSettingsFormPartial = ({
<SelectContent>
{Object.entries(SUPPORTED_LANGUAGES).map(([code, language]) => (
<SelectItem key={code} value={code}>
{language.full}
{t(language.full)}
</SelectItem>
))}
</SelectContent>
@@ -325,6 +331,29 @@ export const AddTemplateSettingsFormPartial = ({
/>
)}
<FormField
control={form.control}
name="templateType"
render={({ field }) => (
<FormItem>
<FormLabel className="flex flex-row items-center">
<Trans>Template type</Trans>
<TemplateTypeTooltip organisationTeamCount={organisation.teams.length} />
</FormLabel>
<FormControl>
<TemplateTypeSelect
{...field}
onValueChange={(value) => {
field.onChange(value);
void handleAutoSave();
}}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="meta.distributionMethod"
@@ -391,7 +420,7 @@ export const AddTemplateSettingsFormPartial = ({
{Object.values(DOCUMENT_DISTRIBUTION_METHODS).map(
({ value, description }) => (
<SelectItem key={value} value={value}>
{i18n._(description)}
{t(description)}
</SelectItem>
),
)}
@@ -1,6 +1,6 @@
import { msg } from '@lingui/core/macro';
import { DocumentDistributionMethod } from '@prisma/client';
import { DocumentVisibility } from '@prisma/client';
import { DocumentVisibility, TemplateType } from '@prisma/client';
import { z } from 'zod';
import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
@@ -17,10 +17,12 @@ import {
ZDocumentMetaTimezoneSchema,
} from '@documenso/lib/types/document-meta';
import { isValidRedirectUrl } from '@documenso/lib/utils/is-valid-redirect-url';
import { zEmail } from '@documenso/lib/utils/zod';
export const ZAddTemplateSettingsFormSchema = z.object({
title: z.string().trim().min(1, { message: "Title can't be empty" }),
externalId: z.string().optional(),
templateType: z.nativeEnum(TemplateType).optional(),
visibility: z.nativeEnum(DocumentVisibility).optional(),
globalAccessAuth: z
.array(z.union([ZDocumentAccessAuthTypesSchema, z.literal('-1')]))
@@ -49,10 +51,7 @@ export const ZAddTemplateSettingsFormSchema = z.object({
.optional()
.default('en'),
emailId: z.string().nullable(),
emailReplyTo: z.preprocess(
(val) => (val === '' ? undefined : val),
z.string().email().optional(),
),
emailReplyTo: z.preprocess((val) => (val === '' ? undefined : val), zEmail().optional()),
emailSettings: ZDocumentEmailSettingsSchema,
signatureTypes: z.array(z.nativeEnum(DocumentSignatureType)).min(1, {
message: msg`At least one signature type must be enabled`.id,
+8 -1
View File
@@ -8,7 +8,14 @@ const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipTrigger = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Trigger>
>(({ type = 'button', ...props }, ref) => (
<TooltipPrimitive.Trigger ref={ref} type={type} {...props} />
));
TooltipTrigger.displayName = TooltipPrimitive.Trigger.displayName;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,