Compare commits

...

4 Commits

Author SHA1 Message Date
David Nguyen
085ed0741c Merge branch 'main' into fix/field-rework 2025-03-24 19:51:22 +11:00
David Nguyen
fb66c353da fix: checkbox error 2025-03-12 15:37:19 +11:00
David Nguyen
c992a36b4b fix: adjust field cropping 2025-03-12 00:22:09 +11:00
David Nguyen
bacb581825 fix: rework fields 2025-03-11 21:46:12 +11:00
42 changed files with 1318 additions and 1140 deletions

View File

@@ -114,7 +114,7 @@ export const TemplateBulkSendDialog = ({
<Dialog> <Dialog>
<DialogTrigger asChild> <DialogTrigger asChild>
{trigger ?? ( {trigger ?? (
<Button> <Button variant="outline">
<Upload className="mr-2 h-4 w-4" /> <Upload className="mr-2 h-4 w-4" />
<Trans>Bulk Send via CSV</Trans> <Trans>Bulk Send via CSV</Trans>
</Button> </Button>

View File

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

View File

@@ -9,6 +9,10 @@ import { z } from 'zod';
import { useOptionalSession } from '@documenso/lib/client-only/providers/session'; import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
import type { TTemplate } from '@documenso/lib/types/template'; import type { TTemplate } from '@documenso/lib/types/template';
import {
DocumentReadOnlyFields,
mapFieldsWithRecipients,
} from '@documenso/ui/components/document/document-read-only-fields';
import { import {
DocumentFlowFormContainerActions, DocumentFlowFormContainerActions,
DocumentFlowFormContainerContent, DocumentFlowFormContainerContent,
@@ -16,7 +20,6 @@ import {
DocumentFlowFormContainerHeader, DocumentFlowFormContainerHeader,
DocumentFlowFormContainerStep, DocumentFlowFormContainerStep,
} from '@documenso/ui/primitives/document-flow/document-flow-root'; } from '@documenso/ui/primitives/document-flow/document-flow-root';
import { ShowFieldItem } from '@documenso/ui/primitives/document-flow/show-field-item';
import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types'; import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types';
import { import {
Form, Form,
@@ -97,14 +100,16 @@ export const DirectTemplateConfigureForm = ({
<DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} /> <DocumentFlowFormContainerHeader title={flowStep.title} description={flowStep.description} />
<DocumentFlowFormContainerContent> <DocumentFlowFormContainerContent>
{isDocumentPdfLoaded && {isDocumentPdfLoaded && (
directTemplateRecipient.fields.map((field, index) => ( <DocumentReadOnlyFields
<ShowFieldItem fields={mapFieldsWithRecipients(
key={index} directTemplateRecipient.fields,
field={field} recipientsWithBlankDirectRecipientEmail,
recipients={recipientsWithBlankDirectRecipientEmail} )}
/> recipientIds={recipients.map((recipient) => recipient.id)}
))} showRecipientColors={true}
/>
)}
<Form {...form}> <Form {...form}>
<fieldset <fieldset

View File

@@ -97,6 +97,12 @@ export const DocumentSigningCheckboxField = ({
const onSign = async (authOptions?: TRecipientActionAuth) => { const onSign = async (authOptions?: TRecipientActionAuth) => {
try { try {
// Do nothing, this should only happen when the user clicks the field, but
// misses the checkbox which triggers this callback.
if (checkedValues.length === 0) {
return;
}
if (!isLengthConditionMet) { if (!isLengthConditionMet) {
return; return;
} }
@@ -270,21 +276,26 @@ export const DocumentSigningCheckboxField = ({
{validationSign?.label} {checkboxValidationLength} {validationSign?.label} {checkboxValidationLength}
</FieldToolTip> </FieldToolTip>
)} )}
<div className="z-50 flex flex-col gap-y-2"> <div className="z-50 my-0.5 flex flex-col gap-y-1">
{values?.map((item: { id: number; value: string; checked: boolean }, index: number) => { {values?.map((item: { id: number; value: string; checked: boolean }, index: number) => {
const itemValue = item.value || `empty-value-${item.id}`; const itemValue = item.value || `empty-value-${item.id}`;
return ( return (
<div key={index} className="flex items-center gap-x-1.5"> <div key={index} className="flex items-center">
<Checkbox <Checkbox
className="h-4 w-4" className="h-3 w-3"
id={`checkbox-${index}`} id={`checkbox-${field.id}-${item.id}`}
checked={checkedValues.includes(itemValue)} checked={checkedValues.includes(itemValue)}
onCheckedChange={() => handleCheckboxChange(item.value, item.id)} onCheckedChange={() => handleCheckboxChange(item.value, item.id)}
/> />
<Label htmlFor={`checkbox-${index}`}> {!item.value.includes('empty-value-') && item.value && (
{item.value.includes('empty-value-') ? '' : item.value} <Label
</Label> htmlFor={`checkbox-${field.id}-${item.id}`}
className="text-foreground ml-1.5 text-xs font-normal"
>
{item.value}
</Label>
)}
</div> </div>
); );
})} })}
@@ -293,22 +304,27 @@ export const DocumentSigningCheckboxField = ({
)} )}
{field.inserted && ( {field.inserted && (
<div className="flex flex-col gap-y-1"> <div className="my-0.5 flex flex-col gap-y-1">
{values?.map((item: { id: number; value: string; checked: boolean }, index: number) => { {values?.map((item: { id: number; value: string; checked: boolean }, index: number) => {
const itemValue = item.value || `empty-value-${item.id}`; const itemValue = item.value || `empty-value-${item.id}`;
return ( return (
<div key={index} className="flex items-center gap-x-1.5"> <div key={index} className="flex items-center">
<Checkbox <Checkbox
className="h-3 w-3" className="h-3 w-3"
id={`checkbox-${index}`} id={`checkbox-${field.id}-${item.id}`}
checked={parsedCheckedValues.includes(itemValue)} checked={parsedCheckedValues.includes(itemValue)}
disabled={isLoading} disabled={isLoading}
onCheckedChange={() => void handleCheckboxOptionClick(item)} onCheckedChange={() => void handleCheckboxOptionClick(item)}
/> />
<Label htmlFor={`checkbox-${index}`} className="text-xs"> {!item.value.includes('empty-value-') && item.value && (
{item.value.includes('empty-value-') ? '' : item.value} <Label
</Label> htmlFor={`checkbox-${field.id}-${item.id}`}
className="text-foreground ml-1.5 text-xs font-normal"
>
{item.value}
</Label>
)}
</div> </div>
); );
})} })}

View File

@@ -281,7 +281,7 @@ export const DocumentSigningCompleteDialog = ({
<div className="flex w-full flex-1 flex-nowrap gap-4"> <div className="flex w-full flex-1 flex-nowrap gap-4">
<Button <Button
type="button" type="button"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10" className="flex-1"
variant="secondary" variant="secondary"
onClick={() => setShowDialog(false)} onClick={() => setShowDialog(false)}
disabled={form.formState.isSubmitting} disabled={form.formState.isSubmitting}

View File

@@ -142,7 +142,7 @@ export const DocumentSigningDateField = ({
)} )}
{!field.inserted && ( {!field.inserted && (
<p className="group-hover:text-primary text-muted-foreground text-[clamp(0.425rem,25cqw,0.825rem)] duration-200 group-hover:text-yellow-300"> <p className="group-hover:text-primary text-foreground group-hover:text-recipient-green text-[clamp(0.425rem,25cqw,0.825rem)] duration-200">
<Trans>Date</Trans> <Trans>Date</Trans>
</p> </p>
)} )}
@@ -151,12 +151,10 @@ export const DocumentSigningDateField = ({
<div className="flex h-full w-full items-center"> <div className="flex h-full w-full items-center">
<p <p
className={cn( className={cn(
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200', 'text-foreground w-full whitespace-nowrap text-left text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
{ {
'text-left': parsedFieldMeta?.textAlign === 'left', '!text-center': parsedFieldMeta?.textAlign === 'center',
'text-center': '!text-right': parsedFieldMeta?.textAlign === 'right',
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
}, },
)} )}
> >

View File

@@ -177,15 +177,11 @@ export const DocumentSigningDropdownField = ({
)} )}
{!field.inserted && ( {!field.inserted && (
<p className="group-hover:text-primary text-muted-foreground flex flex-col items-center justify-center duration-200"> <p className="group-hover:text-primary text-foreground flex flex-col items-center justify-center duration-200">
<Select value={localChoice} onValueChange={handleSelectItem}> <Select value={localChoice} onValueChange={handleSelectItem}>
<SelectTrigger <SelectTrigger
className={cn( className={cn(
'text-muted-foreground z-10 h-full w-full border-none ring-0 focus:ring-0', 'text-foreground z-10 h-full w-full border-none ring-0 focus:border-none focus:ring-0',
{
'hover:text-red-300': parsedFieldMeta.required,
'hover:text-yellow-300': !parsedFieldMeta.required,
},
)} )}
> >
<SelectValue <SelectValue
@@ -205,7 +201,7 @@ export const DocumentSigningDropdownField = ({
)} )}
{field.inserted && ( {field.inserted && (
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.425rem,25cqw,0.825rem)] duration-200"> <p className="text-foreground text-[clamp(0.425rem,25cqw,0.825rem)] duration-200">
{field.customText} {field.customText}
</p> </p>
)} )}

View File

@@ -1,7 +1,6 @@
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
import { useRevalidator } from 'react-router'; import { useRevalidator } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
@@ -14,10 +13,14 @@ import type {
TRemovedSignedFieldWithTokenMutationSchema, TRemovedSignedFieldWithTokenMutationSchema,
TSignFieldWithTokenMutationSchema, TSignFieldWithTokenMutationSchema,
} from '@documenso/trpc/server/field-router/schema'; } from '@documenso/trpc/server/field-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { DocumentSigningFieldContainer } from './document-signing-field-container'; import { DocumentSigningFieldContainer } from './document-signing-field-container';
import {
DocumentSigningFieldsInserted,
DocumentSigningFieldsLoader,
DocumentSigningFieldsUninserted,
} from './document-signing-fields';
import { useRequiredDocumentSigningContext } from './document-signing-provider'; import { useRequiredDocumentSigningContext } from './document-signing-provider';
import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider'; import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
@@ -120,34 +123,18 @@ export const DocumentSigningEmailField = ({
return ( return (
<DocumentSigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Email"> <DocumentSigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Email">
{isLoading && ( {isLoading && <DocumentSigningFieldsLoader />}
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
</div>
)}
{!field.inserted && ( {!field.inserted && (
<p className="group-hover:text-primary text-muted-foreground text-[clamp(0.425rem,25cqw,0.825rem)] duration-200 group-hover:text-yellow-300"> <DocumentSigningFieldsUninserted>
<Trans>Email</Trans> <Trans>Email</Trans>
</p> </DocumentSigningFieldsUninserted>
)} )}
{field.inserted && ( {field.inserted && (
<div className="flex h-full w-full items-center"> <DocumentSigningFieldsInserted textAlign={parsedFieldMeta?.textAlign}>
<p {field.customText}
className={cn( </DocumentSigningFieldsInserted>
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
{
'text-left': parsedFieldMeta?.textAlign === 'left',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>
{field.customText}
</p>
</div>
)} )}
</DocumentSigningFieldContainer> </DocumentSigningFieldContainer>
); );

View File

@@ -2,12 +2,14 @@ import React from 'react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { FieldType } from '@prisma/client'; import { FieldType } from '@prisma/client';
import { TooltipArrow } from '@radix-ui/react-tooltip';
import { X } from 'lucide-react'; import { X } from 'lucide-react';
import { type TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import { type TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta'; import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { FieldRootContainer } from '@documenso/ui/components/field/field'; import { FieldRootContainer } from '@documenso/ui/components/field/field';
import { RECIPIENT_COLOR_STYLES } from '@documenso/ui/lib/recipient-colors';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
@@ -128,57 +130,51 @@ export const DocumentSigningFieldContainer = ({
}; };
return ( return (
<div className={cn('[container-type:size]', { group: type === 'Checkbox' })}> <div className={cn('[container-type:size]')}>
<FieldRootContainer field={field}> <FieldRootContainer color={RECIPIENT_COLOR_STYLES.green} field={field}>
{!field.inserted && !loading && !readOnlyField && ( {!field.inserted && !loading && !readOnlyField && (
<button <button
type="submit" type="submit"
className="absolute inset-0 z-10 h-full w-full rounded-md border" className="absolute inset-0 z-10 h-full w-full rounded-[2px]"
onClick={async () => handleInsertField()} onClick={async () => handleInsertField()}
/> />
)} )}
{readOnlyField && ( {readOnlyField && (
<button className="bg-background/40 absolute inset-0 z-10 flex h-full w-full items-center justify-center rounded-md text-sm opacity-0 duration-200 group-hover:opacity-100"> <button className="bg-background/40 absolute inset-0 z-10 flex h-full w-full items-center justify-center rounded-md text-sm opacity-0 duration-200 group-hover:opacity-100">
<span className="bg-foreground/50 dark:bg-background/50 text-background dark:text-foreground rounded-xl p-2"> <span className="bg-foreground/50 text-background rounded-xl p-2">
<Trans>Read only field</Trans> <Trans>Read only field</Trans>
</span> </span>
</button> </button>
)} )}
{type === 'Date' && field.inserted && !loading && !readOnlyField && (
<Tooltip delayDuration={0}>
<TooltipTrigger asChild>
<button
className="text-destructive bg-background/40 absolute inset-0 z-10 flex h-full w-full items-center justify-center rounded-md text-sm opacity-0 duration-200 group-hover:opacity-100"
onClick={onRemoveSignedFieldClick}
>
<Trans>Remove</Trans>
</button>
</TooltipTrigger>
{tooltipText && <TooltipContent className="max-w-xs">{tooltipText}</TooltipContent>}
</Tooltip>
)}
{type === 'Checkbox' && field.inserted && !loading && !readOnlyField && ( {type === 'Checkbox' && field.inserted && !loading && !readOnlyField && (
<button <button
className="dark:bg-background absolute -bottom-10 flex items-center justify-evenly rounded-md border bg-gray-900 opacity-0 group-hover:opacity-100" className="absolute -bottom-10 flex items-center justify-evenly rounded-md border bg-gray-900 opacity-0 group-hover:opacity-100"
onClick={() => void onClearCheckBoxValues(type)} onClick={() => void onClearCheckBoxValues(type)}
> >
<span className="dark:text-muted-foreground/50 dark:hover:text-muted-foreground dark:hover:bg-foreground/10 rounded-md p-1 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"> <span className="rounded-md p-1 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</span> </span>
</button> </button>
)} )}
{type !== 'Date' && type !== 'Checkbox' && field.inserted && !loading && !readOnlyField && ( {type !== 'Checkbox' && field.inserted && !loading && !readOnlyField && (
<button <Tooltip delayDuration={0}>
className="text-destructive bg-background/50 absolute inset-0 z-10 flex h-full w-full items-center justify-center rounded-md text-sm opacity-0 duration-200 group-hover:opacity-100" <TooltipTrigger asChild>
onClick={onRemoveSignedFieldClick} <button className="absolute inset-0 z-10" onClick={onRemoveSignedFieldClick}></button>
> </TooltipTrigger>
<Trans>Remove</Trans>
</button> <TooltipContent
className="border-0 bg-orange-300 fill-orange-300 font-bold text-orange-900"
sideOffset={2}
>
{tooltipText && <p>{tooltipText}</p>}
<Trans>Remove</Trans>
<TooltipArrow />
</TooltipContent>
</Tooltip>
)} )}
{(field.type === FieldType.RADIO || field.type === FieldType.CHECKBOX) && {(field.type === FieldType.RADIO || field.type === FieldType.CHECKBOX) &&

View File

@@ -0,0 +1,51 @@
import { Loader } from 'lucide-react';
import { cn } from '@documenso/ui/lib/utils';
export const DocumentSigningFieldsLoader = () => {
return (
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
</div>
);
};
export const DocumentSigningFieldsUninserted = ({ children }: { children: React.ReactNode }) => {
return (
<p className="group-hover:text-primary text-foreground group-hover:text-recipient-green text-[clamp(0.425rem,25cqw,0.825rem)] duration-200">
{children}
</p>
);
};
type DocumentSigningFieldsInsertedProps = {
children: React.ReactNode;
/**
* The text alignment of the field.
*
* Defaults to left.
*/
textAlign?: 'left' | 'center' | 'right';
};
export const DocumentSigningFieldsInserted = ({
children,
textAlign = 'left',
}: DocumentSigningFieldsInsertedProps) => {
return (
<div className="flex h-full w-full items-center">
<p
className={cn(
'text-foreground w-full text-left text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
{
'!text-center': textAlign === 'center',
'!text-right': textAlign === 'right',
},
)}
>
{children}
</p>
</div>
);
};

View File

@@ -1,12 +1,12 @@
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
import { useRevalidator } from 'react-router'; import { useRevalidator } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error'; import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth'; import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { ZInitialsFieldMeta } from '@documenso/lib/types/field-meta';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter'; import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react'; import { trpc } from '@documenso/trpc/react';
@@ -17,6 +17,11 @@ import type {
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { DocumentSigningFieldContainer } from './document-signing-field-container'; import { DocumentSigningFieldContainer } from './document-signing-field-container';
import {
DocumentSigningFieldsInserted,
DocumentSigningFieldsLoader,
DocumentSigningFieldsUninserted,
} from './document-signing-fields';
import { useRequiredDocumentSigningContext } from './document-signing-provider'; import { useRequiredDocumentSigningContext } from './document-signing-provider';
import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider'; import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
@@ -50,6 +55,9 @@ export const DocumentSigningInitialsField = ({
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading; const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading;
const safeFieldMeta = ZInitialsFieldMeta.safeParse(field.fieldMeta);
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
const onSign = async (authOptions?: TRecipientActionAuth) => { const onSign = async (authOptions?: TRecipientActionAuth) => {
try { try {
const value = initials ?? ''; const value = initials ?? '';
@@ -122,22 +130,18 @@ export const DocumentSigningInitialsField = ({
onRemove={onRemove} onRemove={onRemove}
type="Initials" type="Initials"
> >
{isLoading && ( {isLoading && <DocumentSigningFieldsLoader />}
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
</div>
)}
{!field.inserted && ( {!field.inserted && (
<p className="group-hover:text-primary text-muted-foreground text-[clamp(0.425rem,25cqw,0.825rem)] duration-200 group-hover:text-yellow-300"> <DocumentSigningFieldsUninserted>
<Trans>Initials</Trans> <Trans>Initials</Trans>
</p> </DocumentSigningFieldsUninserted>
)} )}
{field.inserted && ( {field.inserted && (
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.425rem,25cqw,0.825rem)] duration-200"> <DocumentSigningFieldsInserted textAlign={parsedFieldMeta?.textAlign}>
{field.customText} {field.customText}
</p> </DocumentSigningFieldsInserted>
)} )}
</DocumentSigningFieldContainer> </DocumentSigningFieldContainer>
); );

View File

@@ -3,7 +3,6 @@ import { useState } from 'react';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { Loader } from 'lucide-react';
import { useRevalidator } from 'react-router'; import { useRevalidator } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
@@ -16,7 +15,6 @@ import type {
TRemovedSignedFieldWithTokenMutationSchema, TRemovedSignedFieldWithTokenMutationSchema,
TSignFieldWithTokenMutationSchema, TSignFieldWithTokenMutationSchema,
} from '@documenso/trpc/server/field-router/schema'; } from '@documenso/trpc/server/field-router/schema';
import { cn } from '@documenso/ui/lib/utils';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog'; import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
@@ -25,6 +23,11 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
import { DocumentSigningFieldContainer } from './document-signing-field-container'; import { DocumentSigningFieldContainer } from './document-signing-field-container';
import {
DocumentSigningFieldsInserted,
DocumentSigningFieldsLoader,
DocumentSigningFieldsUninserted,
} from './document-signing-fields';
import { useRequiredDocumentSigningContext } from './document-signing-provider'; import { useRequiredDocumentSigningContext } from './document-signing-provider';
import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider'; import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
@@ -166,34 +169,18 @@ export const DocumentSigningNameField = ({
onRemove={onRemove} onRemove={onRemove}
type="Name" type="Name"
> >
{isLoading && ( {isLoading && <DocumentSigningFieldsLoader />}
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
</div>
)}
{!field.inserted && ( {!field.inserted && (
<p className="group-hover:text-primary text-muted-foreground duration-200 group-hover:text-yellow-300"> <DocumentSigningFieldsUninserted>
<Trans>Name</Trans> <Trans>Name</Trans>
</p> </DocumentSigningFieldsUninserted>
)} )}
{field.inserted && ( {field.inserted && (
<div className="flex h-full w-full items-center"> <DocumentSigningFieldsInserted textAlign={parsedFieldMeta?.textAlign}>
<p {field.customText}
className={cn( </DocumentSigningFieldsInserted>
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
{
'text-left': parsedFieldMeta?.textAlign === 'left',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>
{field.customText}
</p>
</div>
)} )}
<Dialog open={showFullNameModal} onOpenChange={setShowFullNameModal}> <Dialog open={showFullNameModal} onOpenChange={setShowFullNameModal}>
@@ -202,7 +189,7 @@ export const DocumentSigningNameField = ({
<Trans> <Trans>
Sign as Sign as
<div> <div>
{recipient.name} <div className="text-muted-foreground">({recipient.email})</div> {recipient.name} <div className="text-foreground">({recipient.email})</div>
</div> </div>
</Trans> </Trans>
</DialogTitle> </DialogTitle>
@@ -224,7 +211,7 @@ export const DocumentSigningNameField = ({
<div className="flex w-full flex-1 flex-nowrap gap-4"> <div className="flex w-full flex-1 flex-nowrap gap-4">
<Button <Button
type="button" type="button"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10" className="flex-1"
variant="secondary" variant="secondary"
onClick={() => { onClick={() => {
setShowFullNameModal(false); setShowFullNameModal(false);

View File

@@ -3,7 +3,6 @@ import { useEffect, useState } from 'react';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro'; import { Trans } from '@lingui/react/macro';
import { Hash, Loader } from 'lucide-react';
import { useRevalidator } from 'react-router'; import { useRevalidator } from 'react-router';
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number'; import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
@@ -25,6 +24,11 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
import { DocumentSigningFieldContainer } from './document-signing-field-container'; import { DocumentSigningFieldContainer } from './document-signing-field-container';
import {
DocumentSigningFieldsInserted,
DocumentSigningFieldsLoader,
DocumentSigningFieldsUninserted,
} from './document-signing-fields';
import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider'; import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
type ValidationErrors = { type ValidationErrors = {
@@ -245,45 +249,16 @@ export const DocumentSigningNumberField = ({
onRemove={onRemove} onRemove={onRemove}
type="Number" type="Number"
> >
{isLoading && ( {isLoading && <DocumentSigningFieldsLoader />}
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
</div>
)}
{!field.inserted && ( {!field.inserted && (
<p <DocumentSigningFieldsUninserted>{fieldDisplayName}</DocumentSigningFieldsUninserted>
className={cn(
'group-hover:text-primary text-muted-foreground flex flex-col items-center justify-center duration-200',
{
'group-hover:text-yellow-300': !field.inserted && !parsedFieldMeta?.required,
'group-hover:text-red-300': !field.inserted && parsedFieldMeta?.required,
},
)}
>
<span className="flex items-center justify-center gap-x-1">
<Hash className="h-[clamp(0.625rem,20cqw,0.925rem)] w-[clamp(0.625rem,20cqw,0.925rem)]" />{' '}
<span className="text-[clamp(0.425rem,25cqw,0.825rem)]">{fieldDisplayName}</span>
</span>
</p>
)} )}
{field.inserted && ( {field.inserted && (
<div className="flex h-full w-full items-center"> <DocumentSigningFieldsInserted textAlign={parsedFieldMeta?.textAlign}>
<p {field.customText}
className={cn( </DocumentSigningFieldsInserted>
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
{
'text-left': parsedFieldMeta?.textAlign === 'left',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>
{field.customText}
</p>
</div>
)} )}
<Dialog open={showNumberModal} onOpenChange={setShowNumberModal}> <Dialog open={showNumberModal} onOpenChange={setShowNumberModal}>
@@ -339,7 +314,7 @@ export const DocumentSigningNumberField = ({
<div className="flex w-full flex-1 flex-nowrap gap-4"> <div className="flex w-full flex-1 flex-nowrap gap-4">
<Button <Button
type="button" type="button"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10" className="flex-1"
variant="secondary" variant="secondary"
onClick={() => { onClick={() => {
setShowNumberModal(false); setShowNumberModal(false);

View File

@@ -19,6 +19,7 @@ import {
import type { CompletedField } from '@documenso/lib/types/fields'; import type { CompletedField } from '@documenso/lib/types/fields';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta'; import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields'; import type { RecipientWithFields } from '@documenso/prisma/types/recipient-with-fields';
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Card, CardContent } from '@documenso/ui/primitives/card';
import { ElementVisible } from '@documenso/ui/primitives/element-visible'; import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer'; import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
@@ -36,7 +37,6 @@ import { DocumentSigningRadioField } from '~/components/general/document-signing
import { DocumentSigningRejectDialog } from '~/components/general/document-signing/document-signing-reject-dialog'; import { DocumentSigningRejectDialog } from '~/components/general/document-signing/document-signing-reject-dialog';
import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field'; import { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-field';
import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field'; import { DocumentSigningTextField } from '~/components/general/document-signing/document-signing-text-field';
import { DocumentReadOnlyFields } from '~/components/general/document/document-read-only-fields';
import { DocumentSigningRecipientProvider } from './document-signing-recipient-provider'; import { DocumentSigningRecipientProvider } from './document-signing-recipient-provider';
@@ -157,7 +157,7 @@ export const DocumentSigningPageView = ({
</div> </div>
</div> </div>
<DocumentReadOnlyFields fields={completedFields} /> <DocumentReadOnlyFields fields={completedFields} showRecipientTooltip={true} />
{recipient.role !== RecipientRole.ASSISTANT && ( {recipient.role !== RecipientRole.ASSISTANT && (
<DocumentSigningAutoSign recipient={recipient} fields={fields} /> <DocumentSigningAutoSign recipient={recipient} fields={fields} />

View File

@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Loader } from 'lucide-react';
import { useRevalidator } from 'react-router'; import { useRevalidator } from 'react-router';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc'; import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
@@ -21,6 +20,7 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
import { DocumentSigningFieldContainer } from './document-signing-field-container'; import { DocumentSigningFieldContainer } from './document-signing-field-container';
import { DocumentSigningFieldsLoader } from './document-signing-fields';
import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider'; import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
export type DocumentSigningRadioFieldProps = { export type DocumentSigningRadioFieldProps = {
@@ -150,44 +150,52 @@ export const DocumentSigningRadioField = ({
return ( return (
<DocumentSigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Radio"> <DocumentSigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Radio">
{isLoading && ( {isLoading && <DocumentSigningFieldsLoader />}
<div className="bg-background absolute inset-0 z-20 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
</div>
)}
{!field.inserted && ( {!field.inserted && (
<RadioGroup onValueChange={(value) => handleSelectItem(value)} className="z-10"> <RadioGroup
onValueChange={(value) => handleSelectItem(value)}
className="z-10 my-0.5 gap-y-1"
>
{values?.map((item, index) => ( {values?.map((item, index) => (
<div key={index} className="flex items-center gap-x-1.5"> <div key={index} className="flex items-center">
<RadioGroupItem <RadioGroupItem
className="h-4 w-4 shrink-0" className="h-3 w-3 shrink-0"
value={item.value} value={item.value}
id={`option-${index}`} id={`option-${field.id}-${item.id}`}
checked={item.checked} checked={item.checked}
/> />
{!item.value.includes('empty-value-') && item.value && (
<Label htmlFor={`option-${index}`}> <Label
{item.value.includes('empty-value-') ? '' : item.value} htmlFor={`option-${field.id}-${item.id}`}
</Label> className="text-foreground ml-1.5 text-xs font-normal"
>
{item.value}
</Label>
)}
</div> </div>
))} ))}
</RadioGroup> </RadioGroup>
)} )}
{field.inserted && ( {field.inserted && (
<RadioGroup className="gap-y-1"> <RadioGroup className="my-0.5 gap-y-1">
{values?.map((item, index) => ( {values?.map((item, index) => (
<div key={index} className="flex items-center gap-x-1.5"> <div key={index} className="flex items-center">
<RadioGroupItem <RadioGroupItem
className="h-3 w-3" className="h-3 w-3"
value={item.value} value={item.value}
id={`option-${index}`} id={`option-${field.id}-${item.id}`}
checked={item.value === field.customText} checked={item.value === field.customText}
/> />
<Label htmlFor={`option-${index}`} className="text-xs"> {!item.value.includes('empty-value-') && item.value && (
{item.value.includes('empty-value-') ? '' : item.value} <Label
</Label> htmlFor={`option-${field.id}-${item.id}`}
className="text-foreground ml-1.5 text-xs font-normal"
>
{item.value}
</Label>
)}
</div> </div>
))} ))}
</RadioGroup> </RadioGroup>

View File

@@ -242,7 +242,7 @@ export const DocumentSigningSignatureField = ({
)} )}
{state === 'empty' && ( {state === 'empty' && (
<p className="group-hover:text-primary font-signature text-muted-foreground text-[clamp(0.575rem,25cqw,1.2rem)] text-xl duration-200 group-hover:text-yellow-300"> <p className="group-hover:text-primary font-signature text-muted-foreground group-hover:text-recipient-green text-[clamp(0.575rem,25cqw,1.2rem)] text-xl duration-200">
<Trans>Signature</Trans> <Trans>Signature</Trans>
</p> </p>
)} )}
@@ -259,7 +259,7 @@ export const DocumentSigningSignatureField = ({
<div ref={containerRef} className="flex h-full w-full items-center justify-center p-2"> <div ref={containerRef} className="flex h-full w-full items-center justify-center p-2">
<p <p
ref={signatureRef} ref={signatureRef}
className="font-signature text-muted-foreground dark:text-background w-full overflow-hidden break-all text-center leading-tight duration-200" className="font-signature text-muted-foreground w-full overflow-hidden break-all text-center leading-tight duration-200"
style={{ fontSize: `${fontSize}rem` }} style={{ fontSize: `${fontSize}rem` }}
> >
{signature?.typedSignature} {signature?.typedSignature}
@@ -291,7 +291,7 @@ export const DocumentSigningSignatureField = ({
<div className="flex w-full flex-1 flex-nowrap gap-4"> <div className="flex w-full flex-1 flex-nowrap gap-4">
<Button <Button
type="button" type="button"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10" className="flex-1"
variant="secondary" variant="secondary"
onClick={() => { onClick={() => {
setShowSignatureModal(false); setShowSignatureModal(false);

View File

@@ -3,7 +3,6 @@ import { useEffect, useState } from 'react';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react'; import { useLingui } from '@lingui/react';
import { Plural, Trans } from '@lingui/react/macro'; import { Plural, Trans } from '@lingui/react/macro';
import { Loader, Type } from 'lucide-react';
import { useRevalidator } from 'react-router'; import { useRevalidator } from 'react-router';
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text'; import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
@@ -25,6 +24,11 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider'; import { useRequiredDocumentSigningAuthContext } from './document-signing-auth-provider';
import { DocumentSigningFieldContainer } from './document-signing-field-container'; import { DocumentSigningFieldContainer } from './document-signing-field-container';
import {
DocumentSigningFieldsInserted,
DocumentSigningFieldsLoader,
DocumentSigningFieldsUninserted,
} from './document-signing-fields';
import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider'; import { useDocumentSigningRecipientContext } from './document-signing-recipient-provider';
export type DocumentSigningTextFieldProps = { export type DocumentSigningTextFieldProps = {
@@ -248,49 +252,20 @@ export const DocumentSigningTextField = ({
onRemove={onRemove} onRemove={onRemove}
type="Text" type="Text"
> >
{isLoading && ( {isLoading && <DocumentSigningFieldsLoader />}
<div className="bg-background absolute inset-0 flex items-center justify-center rounded-md">
<Loader className="text-primary h-5 w-5 animate-spin md:h-8 md:w-8" />
</div>
)}
{!field.inserted && ( {!field.inserted && (
<p <DocumentSigningFieldsUninserted>
className={cn( {fieldDisplayName || <Trans>Text</Trans>}
'group-hover:text-primary text-muted-foreground flex flex-col items-center justify-center duration-200', </DocumentSigningFieldsUninserted>
{
'group-hover:text-yellow-300': !field.inserted && !parsedFieldMeta?.required,
'group-hover:text-red-300': !field.inserted && parsedFieldMeta?.required,
},
)}
>
<span className="flex items-center justify-center gap-x-1">
<Type className="h-[clamp(0.625rem,20cqw,0.925rem)] w-[clamp(0.625rem,20cqw,0.925rem)]" />
<span className="text-[clamp(0.425rem,25cqw,0.825rem)]">
{fieldDisplayName || <Trans>Text</Trans>}
</span>
</span>
</p>
)} )}
{field.inserted && ( {field.inserted && (
<div className="flex h-full w-full items-center"> <DocumentSigningFieldsInserted textAlign={parsedFieldMeta?.textAlign}>
<p {field.customText.length < 20
className={cn( ? field.customText
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200', : field.customText.substring(0, 20) + '...'}
{ </DocumentSigningFieldsInserted>
'text-left': parsedFieldMeta?.textAlign === 'left',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>
{field.customText.length < 20
? field.customText
: field.customText.substring(0, 20) + '...'}
</p>
</div>
)} )}
<Dialog open={showCustomTextModal} onOpenChange={setShowCustomTextModal}> <Dialog open={showCustomTextModal} onOpenChange={setShowCustomTextModal}>
@@ -304,11 +279,9 @@ export const DocumentSigningTextField = ({
id="custom-text" id="custom-text"
placeholder={parsedFieldMeta?.placeholder ?? _(msg`Enter your text here`)} placeholder={parsedFieldMeta?.placeholder ?? _(msg`Enter your text here`)}
className={cn('mt-2 w-full rounded-md', { className={cn('mt-2 w-full rounded-md', {
'border-2 border-red-300 ring-2 ring-red-200 ring-offset-2 ring-offset-red-200 focus-visible:border-red-400 focus-visible:ring-4 focus-visible:ring-red-200 focus-visible:ring-offset-2 focus-visible:ring-offset-red-200': 'border-2 border-red-300 text-left ring-2 ring-red-200 ring-offset-2 ring-offset-red-200 focus-visible:border-red-400 focus-visible:ring-4 focus-visible:ring-red-200 focus-visible:ring-offset-2 focus-visible:ring-offset-red-200':
userInputHasErrors, userInputHasErrors,
'text-left': parsedFieldMeta?.textAlign === 'left', 'text-center': parsedFieldMeta?.textAlign === 'center',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right', 'text-right': parsedFieldMeta?.textAlign === 'right',
})} })}
value={localText} value={localText}
@@ -354,8 +327,8 @@ export const DocumentSigningTextField = ({
<div className="mt-4 flex w-full flex-1 flex-nowrap gap-4"> <div className="mt-4 flex w-full flex-1 flex-nowrap gap-4">
<Button <Button
type="button" type="button"
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
variant="secondary" variant="secondary"
className="flex-1"
onClick={() => { onClick={() => {
setShowCustomTextModal(false); setShowCustomTextModal(false);
setLocalCustomText(''); setLocalCustomText('');

View File

@@ -1,171 +0,0 @@
import { useState } from 'react';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { DocumentMeta } from '@prisma/client';
import { FieldType, SigningStatus } from '@prisma/client';
import { Clock, EyeOffIcon } from 'lucide-react';
import { P, match } from 'ts-pattern';
import {
DEFAULT_DOCUMENT_DATE_FORMAT,
convertToLocalSystemFormat,
} from '@documenso/lib/constants/date-formats';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
import type { DocumentField } from '@documenso/lib/server-only/field/get-fields-for-document';
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { FieldRootContainer } from '@documenso/ui/components/field/field';
import { SignatureIcon } from '@documenso/ui/icons/signature';
import { cn } from '@documenso/ui/lib/utils';
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
import { Badge } from '@documenso/ui/primitives/badge';
import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { PopoverHover } from '@documenso/ui/primitives/popover';
export type DocumentReadOnlyFieldsProps = {
fields: DocumentField[];
documentMeta?: DocumentMeta;
showFieldStatus?: boolean;
};
export const DocumentReadOnlyFields = ({
documentMeta,
fields,
showFieldStatus = true,
}: DocumentReadOnlyFieldsProps) => {
const { _ } = useLingui();
const [hiddenFieldIds, setHiddenFieldIds] = useState<Record<string, boolean>>({});
const handleHideField = (fieldId: string) => {
setHiddenFieldIds((prev) => ({ ...prev, [fieldId]: true }));
};
return (
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
{fields.map(
(field) =>
!hiddenFieldIds[field.secondaryId] && (
<FieldRootContainer
field={field}
key={field.id}
cardClassName="border-gray-300/50 !shadow-none backdrop-blur-[1px] bg-gray-50 ring-0 ring-offset-0"
>
<div className="absolute -right-3 -top-3">
<PopoverHover
trigger={
<Avatar className="dark:border-foreground h-8 w-8 border-2 border-solid border-gray-200/50 transition-colors hover:border-gray-200">
<AvatarFallback className="bg-neutral-50 text-xs text-gray-400">
{extractInitials(field.recipient.name || field.recipient.email)}
</AvatarFallback>
</Avatar>
}
contentProps={{
className: 'relative flex w-fit flex-col p-4 text-sm',
}}
>
{showFieldStatus && (
<Badge
className="mx-auto mb-1 py-0.5"
variant={
field.recipient.signingStatus === SigningStatus.SIGNED
? 'default'
: 'secondary'
}
>
{field.recipient.signingStatus === SigningStatus.SIGNED ? (
<>
<SignatureIcon className="mr-1 h-3 w-3" />
<Trans>Signed</Trans>
</>
) : (
<>
<Clock className="mr-1 h-3 w-3" />
<Trans>Pending</Trans>
</>
)}
</Badge>
)}
<p className="text-center font-semibold">
<span>{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])} field</span>
</p>
<p className="text-muted-foreground mt-1 text-center text-xs">
{field.recipient.name
? `${field.recipient.name} (${field.recipient.email})`
: field.recipient.email}{' '}
</p>
<button
className="absolute right-0 top-0 my-1 p-2 focus:outline-none focus-visible:ring-0"
onClick={() => handleHideField(field.secondaryId)}
title="Hide field"
>
<EyeOffIcon className="h-3 w-3" />
</button>
</PopoverHover>
</div>
<div className="text-muted-foreground dark:text-background/70 break-all text-sm">
{field.recipient.signingStatus === SigningStatus.SIGNED &&
match(field)
.with({ type: FieldType.SIGNATURE }, (field) =>
field.signature?.signatureImageAsBase64 ? (
<img
src={field.signature.signatureImageAsBase64}
alt="Signature"
className="h-full w-full object-contain dark:invert"
/>
) : (
<p className="font-signature text-muted-foreground text-lg duration-200 sm:text-xl md:text-2xl">
{field.signature?.typedSignature}
</p>
),
)
.with(
{
type: P.union(
FieldType.NAME,
FieldType.INITIALS,
FieldType.EMAIL,
FieldType.NUMBER,
FieldType.RADIO,
FieldType.CHECKBOX,
FieldType.DROPDOWN,
),
},
() => field.customText,
)
.with({ type: FieldType.TEXT }, () => field.customText.substring(0, 20) + '...')
.with({ type: FieldType.DATE }, () =>
convertToLocalSystemFormat(
field.customText,
documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
),
)
.with({ type: FieldType.FREE_SIGNATURE }, () => null)
.exhaustive()}
{field.recipient.signingStatus === SigningStatus.NOT_SIGNED && (
<p
className={cn('text-muted-foreground text-lg duration-200', {
'font-signature sm:text-xl md:text-2xl':
field.type === FieldType.SIGNATURE ||
field.type === FieldType.FREE_SIGNATURE,
})}
>
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])}
</p>
)}
</div>
</FieldRootContainer>
),
)}
</ElementVisible>
);
};

View File

@@ -13,6 +13,7 @@ import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/g
import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { DocumentVisibility } from '@documenso/lib/types/document-visibility'; import { DocumentVisibility } from '@documenso/lib/types/document-visibility';
import { formatDocumentsPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath } from '@documenso/lib/utils/teams';
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
import { Badge } from '@documenso/ui/primitives/badge'; import { Badge } from '@documenso/ui/primitives/badge';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Card, CardContent } from '@documenso/ui/primitives/card';
@@ -24,7 +25,6 @@ import { DocumentPageViewDropdown } from '~/components/general/document/document
import { DocumentPageViewInformation } from '~/components/general/document/document-page-view-information'; import { DocumentPageViewInformation } from '~/components/general/document/document-page-view-information';
import { DocumentPageViewRecentActivity } from '~/components/general/document/document-page-view-recent-activity'; import { DocumentPageViewRecentActivity } from '~/components/general/document/document-page-view-recent-activity';
import { DocumentPageViewRecipients } from '~/components/general/document/document-page-view-recipients'; import { DocumentPageViewRecipients } from '~/components/general/document/document-page-view-recipients';
import { DocumentReadOnlyFields } from '~/components/general/document/document-read-only-fields';
import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/document-recipient-link-copy-dialog'; import { DocumentRecipientLinkCopyDialog } from '~/components/general/document/document-recipient-link-copy-dialog';
import { import {
DocumentStatus as DocumentStatusComponent, DocumentStatus as DocumentStatusComponent,
@@ -200,8 +200,14 @@ export default function DocumentPage() {
</CardContent> </CardContent>
</Card> </Card>
{document.status === DocumentStatus.PENDING && ( {document.status !== DocumentStatus.COMPLETED && (
<DocumentReadOnlyFields fields={fields} documentMeta={documentMeta || undefined} /> <DocumentReadOnlyFields
fields={fields}
documentMeta={documentMeta || undefined}
showRecipientTooltip={true}
showRecipientColors={true}
recipientIds={recipients.map((recipient) => recipient.id)}
/>
)} )}
<div className="col-span-12 lg:col-span-6 xl:col-span-5"> <div className="col-span-12 lg:col-span-6 xl:col-span-5">

View File

@@ -7,6 +7,7 @@ import { getSession } from '@documenso/auth/server/lib/utils/get-session';
import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team'; import { type TGetTeamByUrlResponse, getTeamByUrl } from '@documenso/lib/server-only/team/get-team';
import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id'; import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id';
import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams'; import { formatDocumentsPath, formatTemplatesPath } from '@documenso/lib/utils/teams';
import { DocumentReadOnlyFields } from '@documenso/ui/components/document/document-read-only-fields';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Card, CardContent } from '@documenso/ui/primitives/card';
import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer'; import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
@@ -14,7 +15,6 @@ import { PDFViewer } from '@documenso/ui/primitives/pdf-viewer';
import { TemplateBulkSendDialog } from '~/components/dialogs/template-bulk-send-dialog'; import { TemplateBulkSendDialog } from '~/components/dialogs/template-bulk-send-dialog';
import { TemplateDirectLinkDialogWrapper } from '~/components/dialogs/template-direct-link-dialog-wrapper'; import { TemplateDirectLinkDialogWrapper } from '~/components/dialogs/template-direct-link-dialog-wrapper';
import { TemplateUseDialog } from '~/components/dialogs/template-use-dialog'; import { TemplateUseDialog } from '~/components/dialogs/template-use-dialog';
import { DocumentReadOnlyFields } from '~/components/general/document/document-read-only-fields';
import { TemplateDirectLinkBadge } from '~/components/general/template/template-direct-link-badge'; import { TemplateDirectLinkBadge } from '~/components/general/template/template-direct-link-badge';
import { TemplatePageViewDocumentsTable } from '~/components/general/template/template-page-view-documents-table'; import { TemplatePageViewDocumentsTable } from '~/components/general/template/template-page-view-documents-table';
import { TemplatePageViewInformation } from '~/components/general/template/template-page-view-information'; import { TemplatePageViewInformation } from '~/components/general/template/template-page-view-information';
@@ -151,6 +151,9 @@ export default function TemplatePage() {
<DocumentReadOnlyFields <DocumentReadOnlyFields
fields={readOnlyFields} fields={readOnlyFields}
showFieldStatus={false} showFieldStatus={false}
showRecipientTooltip={true}
showRecipientColors={true}
recipientIds={recipients.map((recipient) => recipient.id)}
documentMeta={mockedDocumentMeta} documentMeta={mockedDocumentMeta}
/> />

View File

@@ -1,8 +1,8 @@
// https://github.com/Hopding/pdf-lib/issues/20#issuecomment-412852821 // https://github.com/Hopding/pdf-lib/issues/20#issuecomment-412852821
import fontkit from '@pdf-lib/fontkit'; import fontkit from '@pdf-lib/fontkit';
import { FieldType } from '@prisma/client'; import { FieldType } from '@prisma/client';
import type { PDFDocument } from 'pdf-lib'; import type { PDFDocument, PDFFont } from 'pdf-lib';
import { RotationTypes, degrees, radiansToDegrees, rgb } from 'pdf-lib'; import { RotationTypes, TextAlignment, degrees, radiansToDegrees, rgb } from 'pdf-lib';
import { P, match } from 'ts-pattern'; import { P, match } from 'ts-pattern';
import { import {
@@ -34,6 +34,13 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
]); ]);
const isSignatureField = isSignatureFieldType(field.type); const isSignatureField = isSignatureFieldType(field.type);
/**
* Red box is the original field width, height and position.
*
* Blue box is the adjusted field width, height and position. It will represent
* where the text will overflow into.
*/
const isDebugMode = const isDebugMode =
// eslint-disable-next-line turbo/no-undeclared-env-vars // eslint-disable-next-line turbo/no-undeclared-env-vars
process.env.DEBUG_PDF_INSERT === '1' || process.env.DEBUG_PDF_INSERT === 'true'; process.env.DEBUG_PDF_INSERT === '1' || process.env.DEBUG_PDF_INSERT === 'true';
@@ -227,8 +234,13 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
const selected: string[] = fromCheckboxValue(field.customText); const selected: string[] = fromCheckboxValue(field.customText);
const topPadding = 12;
const leftCheckboxPadding = 8;
const leftCheckboxLabelPadding = 12;
const checkboxSpaceY = 13;
for (const [index, item] of (values ?? []).entries()) { for (const [index, item] of (values ?? []).entries()) {
const offsetY = index * 16; const offsetY = index * checkboxSpaceY + topPadding;
const checkbox = pdf.getForm().createCheckBox(`checkbox.${field.secondaryId}.${index}`); const checkbox = pdf.getForm().createCheckBox(`checkbox.${field.secondaryId}.${index}`);
@@ -237,7 +249,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
} }
page.drawText(item.value.includes('empty-value-') ? '' : item.value, { page.drawText(item.value.includes('empty-value-') ? '' : item.value, {
x: fieldX + 16, x: fieldX + leftCheckboxPadding + leftCheckboxLabelPadding,
y: pageHeight - (fieldY + offsetY), y: pageHeight - (fieldY + offsetY),
size: 12, size: 12,
font, font,
@@ -245,7 +257,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
}); });
checkbox.addToPage(page, { checkbox.addToPage(page, {
x: fieldX, x: fieldX + leftCheckboxPadding,
y: pageHeight - (fieldY + offsetY), y: pageHeight - (fieldY + offsetY),
height: 8, height: 8,
width: 8, width: 8,
@@ -268,21 +280,28 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
const selected = field.customText.split(','); const selected = field.customText.split(',');
const topPadding = 12;
const leftRadioPadding = 8;
const leftRadioLabelPadding = 12;
const radioSpaceY = 13;
for (const [index, item] of (values ?? []).entries()) { for (const [index, item] of (values ?? []).entries()) {
const offsetY = index * 16; const offsetY = index * radioSpaceY + topPadding;
const radio = pdf.getForm().createRadioGroup(`radio.${field.secondaryId}.${index}`); const radio = pdf.getForm().createRadioGroup(`radio.${field.secondaryId}.${index}`);
// Draw label.
page.drawText(item.value.includes('empty-value-') ? '' : item.value, { page.drawText(item.value.includes('empty-value-') ? '' : item.value, {
x: fieldX + 16, x: fieldX + leftRadioPadding + leftRadioLabelPadding,
y: pageHeight - (fieldY + offsetY), y: pageHeight - (fieldY + offsetY),
size: 12, size: 12,
font, font,
rotate: degrees(pageRotationInDegrees), rotate: degrees(pageRotationInDegrees),
}); });
// Draw radio button.
radio.addOptionToPage(item.value, page, { radio.addOptionToPage(item.value, page, {
x: fieldX, x: fieldX + leftRadioPadding,
y: pageHeight - (fieldY + offsetY), y: pageHeight - (fieldY + offsetY),
height: 8, height: 8,
width: 8, width: 8,
@@ -304,62 +323,144 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
} as const; } as const;
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const Parser = fieldMetaParsers[field.type as keyof typeof fieldMetaParsers]; const fieldMetaParser = fieldMetaParsers[field.type as keyof typeof fieldMetaParsers];
const meta = Parser ? Parser.safeParse(field.fieldMeta) : null; const meta = fieldMetaParser ? fieldMetaParser.safeParse(field.fieldMeta) : null;
const customFontSize = meta?.success && meta.data.fontSize ? meta.data.fontSize : null; const customFontSize = meta?.success && meta.data.fontSize ? meta.data.fontSize : null;
const textAlign = meta?.success && meta.data.textAlign ? meta.data.textAlign : 'center'; const textAlign = meta?.success && meta.data.textAlign ? meta.data.textAlign : 'left';
const longestLineInTextForWidth = field.customText
.split('\n')
.sort((a, b) => b.length - a.length)[0];
let fontSize = customFontSize || maxFontSize; let fontSize = customFontSize || maxFontSize;
let textWidth = font.widthOfTextAtSize(longestLineInTextForWidth, fontSize); const textWidth = font.widthOfTextAtSize(field.customText, fontSize);
const textHeight = font.heightAtSize(fontSize); const textHeight = font.heightAtSize(fontSize);
// Scale font only if no custom font and height exceeds field height.
if (!customFontSize) { if (!customFontSize) {
const scalingFactor = Math.min(fieldWidth / textWidth, fieldHeight / textHeight, 1); const scalingFactor = Math.min(fieldHeight / textHeight, 1);
fontSize = Math.max(Math.min(fontSize * scalingFactor, maxFontSize), minFontSize); fontSize = Math.max(Math.min(fontSize * scalingFactor, maxFontSize), minFontSize);
} }
textWidth = font.widthOfTextAtSize(longestLineInTextForWidth, fontSize); /**
* Calculate whether the field should be multiline.
*
* - True = text will overflow downwards.
* - False = text will overflow sideways.
*/
const isMultiline =
field.type === FieldType.TEXT &&
(textWidth > fieldWidth || field.customText.includes('\n'));
// Add padding similar to web display (roughly 0.5rem equivalent in PDF units) // Add padding similar to web display (roughly 0.5rem equivalent in PDF units)
const padding = 8; // PDF points, roughly equivalent to 0.5rem const padding = 8;
// Calculate X position based on text alignment with padding const textAlignmentOptions = getTextAlignmentOptions(textAlign, fieldX, isMultiline, padding);
let textX = fieldX + padding; // Left alignment starts after padding
if (textAlign === 'center') {
textX = fieldX + (fieldWidth - textWidth) / 2; // Center alignment ignores padding
} else if (textAlign === 'right') {
textX = fieldX + fieldWidth - textWidth - padding; // Right alignment respects right padding
}
let textY = fieldY + (fieldHeight - textHeight) / 2;
// Invert the Y axis since PDFs use a bottom-left coordinate system // Invert the Y axis since PDFs use a bottom-left coordinate system
textY = pageHeight - textY - textHeight; let textFieldBoxY = pageHeight - fieldY - fieldHeight;
const textFieldBoxX = textAlignmentOptions.xPos;
const textField = pdf.getForm().createTextField(`text.${field.secondaryId}`);
textField.setAlignment(textAlignmentOptions.textAlignment);
/**
* From now on we will adjust the field size and position so the text
* overflows correctly in the X or Y axis depending on the field type.
*/
let adjustedFieldWidth = fieldWidth - padding * 2; //
let adjustedFieldHeight = fieldHeight;
let adjustedFieldX = textFieldBoxX;
let adjustedFieldY = textFieldBoxY;
let textToInsert = field.customText;
// The padding to use when fields go off the page.
const pagePadding = 4;
// Handle multiline text, which will overflow on the Y axis.
if (isMultiline) {
textToInsert = breakLongString(textToInsert, adjustedFieldWidth, font, fontSize);
textField.enableMultiline();
textField.disableCombing();
textField.disableScrolling();
// Adjust the textFieldBox so it extends to the bottom of the page so text can wrap.
textFieldBoxY = pageHeight - fieldY - fieldHeight;
// Calculate how much PX from the current field to bottom of the page.
const fieldYOffset = pageHeight - (fieldY + fieldHeight) - pagePadding;
// Field height will be from current to bottom of page.
adjustedFieldHeight = fieldHeight + fieldYOffset;
// Need to move the field Y so it offsets the new field height.
adjustedFieldY = adjustedFieldY - fieldYOffset;
}
// Handle non-multiline text, which will overflow on the X axis.
if (!isMultiline) {
// Left align will extend all the way to the right of the page
if (textAlignmentOptions.textAlignment === TextAlignment.Left) {
adjustedFieldWidth = pageWidth - textFieldBoxX - pagePadding;
}
// Right align will extend all the way to the left of the page.
if (textAlignmentOptions.textAlignment === TextAlignment.Right) {
adjustedFieldWidth = textFieldBoxX + fieldWidth - pagePadding;
adjustedFieldX = adjustedFieldX - adjustedFieldWidth + fieldWidth;
}
// Center align will extend to the closest page edge, then use that * 2 as the width.
if (textAlignmentOptions.textAlignment === TextAlignment.Center) {
const fieldMidpoint = textFieldBoxX + fieldWidth / 2;
const isCloserToLeftEdge = fieldMidpoint < pageWidth / 2;
// If field is closer to left edge, the width must be based of the left.
if (isCloserToLeftEdge) {
adjustedFieldWidth = (textFieldBoxX - pagePadding) * 2 + fieldWidth;
adjustedFieldX = pagePadding;
}
// If field is closer to right edge, the width must be based of the right
if (!isCloserToLeftEdge) {
adjustedFieldWidth = (pageWidth - textFieldBoxX - pagePadding - fieldWidth / 2) * 2;
adjustedFieldX = pageWidth - adjustedFieldWidth - pagePadding;
}
}
}
if (pageRotationInDegrees !== 0) { if (pageRotationInDegrees !== 0) {
const adjustedPosition = adjustPositionForRotation( const adjustedPosition = adjustPositionForRotation(
pageWidth, pageWidth,
pageHeight, pageHeight,
textX, adjustedFieldX,
textY, adjustedFieldY,
pageRotationInDegrees, pageRotationInDegrees,
); );
textX = adjustedPosition.xPos; adjustedFieldX = adjustedPosition.xPos;
textY = adjustedPosition.yPos; adjustedFieldY = adjustedPosition.yPos;
} }
page.drawText(field.customText, { // Set the position and size of the text field
x: textX, textField.addToPage(page, {
y: textY, x: adjustedFieldX,
size: fontSize, y: adjustedFieldY,
font, width: adjustedFieldWidth,
height: adjustedFieldHeight,
rotate: degrees(pageRotationInDegrees), rotate: degrees(pageRotationInDegrees),
// Hide borders.
borderWidth: 0,
borderColor: undefined,
backgroundColor: undefined,
...(isDebugMode ? { borderWidth: 1, borderColor: rgb(0, 0, 1) } : {}),
}); });
// Set properties for the text field
textField.setFontSize(fontSize);
textField.setText(textToInsert);
}); });
return pdf; return pdf;
@@ -393,3 +494,138 @@ const adjustPositionForRotation = (
yPos, yPos,
}; };
}; };
const textAlignmentMap = {
left: TextAlignment.Left,
center: TextAlignment.Center,
right: TextAlignment.Right,
} as const;
/**
* Get the PDF-lib alignment position, and the X position of the field with padding included.
*
* @param textAlign - The text alignment of the field.
* @param fieldX - The X position of the field.
* @param isMultiline - Whether the field is multiline.
* @param padding - The padding of the field. Defaults to 8.
*
* @returns The X position and text alignment for the field.
*/
const getTextAlignmentOptions = (
textAlign: 'left' | 'center' | 'right',
fieldX: number,
isMultiline: boolean,
padding: number = 8,
) => {
const textAlignment = textAlignmentMap[textAlign];
// For multiline, it needs to be centered so we just basic left padding.
if (isMultiline) {
return {
xPos: fieldX + padding,
textAlignment,
};
}
return match(textAlign)
.with('left', () => ({
xPos: fieldX + padding,
textAlignment,
}))
.with('center', () => ({
xPos: fieldX,
textAlignment,
}))
.with('right', () => ({
xPos: fieldX - padding,
textAlignment,
}))
.exhaustive();
};
/**
* Break a long string into multiple lines so it fits within a given width,
* using natural word breaking similar to word processors.
*
* - Keeps words together when possible
* - Only breaks words when they're too long to fit on a line
* - Handles whitespace intelligently
*
* @param text - The text to break into lines
* @param maxWidth - The maximum width of each line in PX
* @param font - The PDF font object
* @param fontSize - The font size in points
* @returns Object containing the result string and line count
*/
function breakLongString(text: string, maxWidth: number, font: PDFFont, fontSize: number): string {
// Handle empty text
if (!text) {
return '';
}
const lines: string[] = [];
// Process each original line separately to preserve newlines
for (const paragraph of text.split('\n')) {
// If paragraph fits on one line or is empty, add it as-is
if (paragraph === '' || font.widthOfTextAtSize(paragraph, fontSize) <= maxWidth) {
lines.push(paragraph);
continue;
}
// Split paragraph into words
const words = paragraph.split(' ');
let currentLine = '';
for (const word of words) {
// Check if adding word to current line would exceed max width
const lineWithWord = currentLine.length === 0 ? word : `${currentLine} ${word}`;
if (font.widthOfTextAtSize(lineWithWord, fontSize) <= maxWidth) {
// Word fits, add it to current line
currentLine = lineWithWord;
} else {
// Word doesn't fit on current line
// First, save current line if it's not empty
if (currentLine.length > 0) {
lines.push(currentLine);
currentLine = '';
}
// Check if word fits on a line by itself
if (font.widthOfTextAtSize(word, fontSize) <= maxWidth) {
// Word fits on its own line
currentLine = word;
} else {
// Word is too long, need to break it character by character
let charLine = '';
// Process each character in the word
for (const char of word) {
const nextCharLine = charLine + char;
if (font.widthOfTextAtSize(nextCharLine, fontSize) <= maxWidth) {
// Character fits, add it
charLine = nextCharLine;
} else {
// Character doesn't fit, push current charLine and start a new one
lines.push(charLine);
charLine = char;
}
}
// Add any remaining characters as the current line
currentLine = charLine;
}
}
}
// Add the last line if not empty
if (currentLine.length > 0) {
lines.push(currentLine);
}
}
return lines.join('\n');
}

View File

@@ -107,6 +107,14 @@ module.exports = {
900: '#364772', 900: '#364772',
950: '#252d46', 950: '#252d46',
}, },
recipient: {
green: 'hsl(var(--recipient-green))',
blue: 'hsl(var(--recipient-blue))',
purple: 'hsl(var(--recipient-purple))',
orange: 'hsl(var(--recipient-orange))',
yellow: 'hsl(var(--recipient-yellow))',
pink: 'hsl(var(--recipient-pink))',
},
}, },
backgroundImage: { backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',

View File

@@ -0,0 +1,170 @@
import { useState } from 'react';
import { useLingui } from '@lingui/react';
import { Trans } from '@lingui/react/macro';
import type { DocumentMeta, Field, Recipient } from '@prisma/client';
import { SigningStatus } from '@prisma/client';
import { Clock, EyeOffIcon } from 'lucide-react';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import type { DocumentField } from '@documenso/lib/server-only/field/get-fields-for-document';
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import { FieldRootContainer } from '@documenso/ui/components/field/field';
import { SignatureIcon } from '@documenso/ui/icons/signature';
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
import { Badge } from '@documenso/ui/primitives/badge';
import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types';
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
import { PopoverHover } from '@documenso/ui/primitives/popover';
import { getRecipientColorStyles } from '../../lib/recipient-colors';
import { FieldContent } from '../../primitives/document-flow/field-content';
export type DocumentReadOnlyFieldsProps = {
fields: DocumentField[];
documentMeta?: DocumentMeta;
showFieldStatus?: boolean;
/**
* Required if you want to show colors.
*
* Can't derive this from the fields because sometimes recipients don't have fields
* yet.
*/
recipientIds?: number[];
/**
* Whether to show the recipient tooltip.
*
* @default false
*/
showRecipientTooltip?: boolean;
/**
* Whether to color code the recipient fields.
*
* @default false
*/
showRecipientColors?: boolean;
};
export const mapFieldsWithRecipients = (
fields: Field[],
recipients: Recipient[],
): DocumentField[] => {
return fields.map((field) => {
const recipient = recipients.find((recipient) => recipient.id === field.recipientId) || {
id: field.recipientId,
name: 'Unknown',
email: 'Unknown',
signingStatus: SigningStatus.NOT_SIGNED,
};
return { ...field, recipient, signature: null };
});
};
export const DocumentReadOnlyFields = ({
documentMeta,
fields,
recipientIds = [],
showFieldStatus = true,
showRecipientTooltip = false,
showRecipientColors = false,
}: DocumentReadOnlyFieldsProps) => {
const { _ } = useLingui();
const [hiddenFieldIds, setHiddenFieldIds] = useState<Record<string, boolean>>({});
const handleHideField = (fieldId: string) => {
setHiddenFieldIds((prev) => ({ ...prev, [fieldId]: true }));
};
return (
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
{fields.map(
(field) =>
!hiddenFieldIds[field.secondaryId] && (
<FieldRootContainer
field={field}
key={field.id}
color={
showRecipientColors
? getRecipientColorStyles(
Math.max(
recipientIds.findIndex((id) => id === field.recipientId),
0,
),
)
: undefined
}
>
{showRecipientTooltip && (
<div className="absolute -right-3 -top-3">
<PopoverHover
trigger={
<Avatar className="h-6 w-6 border-2 border-solid border-gray-200/50 transition-colors hover:border-gray-200">
<AvatarFallback className="bg-neutral-50 text-xs text-gray-400">
{extractInitials(field.recipient.name || field.recipient.email)}
</AvatarFallback>
</Avatar>
}
contentProps={{
className: 'relative flex w-fit flex-col p-4 text-sm',
}}
>
{showFieldStatus && (
<Badge
className="mx-auto mb-1 py-0.5"
variant={
field.recipient.signingStatus === SigningStatus.SIGNED
? 'default'
: 'secondary'
}
>
{field.recipient.signingStatus === SigningStatus.SIGNED ? (
<>
<SignatureIcon className="mr-1 h-3 w-3" />
<Trans>Signed</Trans>
</>
) : (
<>
<Clock className="mr-1 h-3 w-3" />
<Trans>Pending</Trans>
</>
)}
</Badge>
)}
<p className="text-center font-semibold">
<span>
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])} field
</span>
</p>
<p className="text-muted-foreground mt-1 text-center text-xs">
{field.recipient.name
? `${field.recipient.name} (${field.recipient.email})`
: field.recipient.email}{' '}
</p>
<button
className="absolute right-0 top-0 my-1 p-2 focus:outline-none focus-visible:ring-0"
onClick={() => handleHideField(field.secondaryId)}
title="Hide field"
>
<EyeOffIcon className="h-3 w-3" />
</button>
</PopoverHover>
</div>
)}
<FieldContent field={field} documentMeta={documentMeta} />
</FieldRootContainer>
),
)}
</ElementVisible>
);
};

View File

@@ -1,71 +1,18 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useState } from 'react';
import type { Field } from '@prisma/client'; import { type Field, FieldType } from '@prisma/client';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords'; import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta'; import { isFieldUnsignedAndRequired } from '@documenso/lib/utils/advanced-fields-helpers';
import { ZFieldMetaSchema } from '@documenso/lib/types/field-meta';
import type { RecipientColorStyles } from '../../lib/recipient-colors';
import { cn } from '../../lib/utils'; import { cn } from '../../lib/utils';
import { Card, CardContent } from '../../primitives/card';
export type FieldRootContainerProps = {
field: Field;
children: React.ReactNode;
};
export type FieldContainerPortalProps = { export type FieldContainerPortalProps = {
field: Field; field: Field;
className?: string; className?: string;
children: React.ReactNode; children: React.ReactNode;
cardClassName?: string;
};
const getCardClassNames = (
field: Field,
parsedField: TFieldMetaSchema | null,
isValidating: boolean,
checkBoxOrRadio: boolean,
cardClassName?: string,
) => {
const baseClasses =
'field--FieldRootContainer field-card-container relative z-20 h-full w-full transition-all';
const insertedClasses =
'bg-primary/20 border-primary ring-primary/20 ring-offset-primary/20 ring-2 ring-offset-2 dark:shadow-none';
const nonRequiredClasses =
'border-yellow-300 shadow-none ring-2 ring-yellow-100 ring-offset-2 ring-offset-yellow-100 dark:border-2';
const validatingClasses = 'border-orange-300 ring-1 ring-orange-300';
const requiredClasses =
'border-red-500 shadow-none ring-2 ring-red-200 ring-offset-2 ring-offset-red-200 hover:text-red-500';
const requiredCheckboxRadioClasses = 'border-dashed border-red-500';
if (checkBoxOrRadio) {
return cn(
{
[insertedClasses]: field.inserted,
'ring-offset-yellow-200 border-dashed border-yellow-300 ring-2 ring-yellow-200 ring-offset-2 dark:shadow-none':
!field.inserted && !parsedField?.required,
'shadow-none': !field.inserted,
[validatingClasses]: !field.inserted && isValidating,
[requiredCheckboxRadioClasses]: !field.inserted && parsedField?.required,
},
cardClassName,
);
}
return cn(
baseClasses,
{
[insertedClasses]: field.inserted,
[nonRequiredClasses]: !field.inserted && !parsedField?.required,
'shadow-none': !field.inserted && checkBoxOrRadio,
[validatingClasses]: !field.inserted && isValidating,
[requiredClasses]: !field.inserted && parsedField?.required && !checkBoxOrRadio,
},
cardClassName,
);
}; };
export function FieldContainerPortal({ export function FieldContainerPortal({
@@ -76,13 +23,10 @@ export function FieldContainerPortal({
const coords = useFieldPageCoords(field); const coords = useFieldPageCoords(field);
const isCheckboxOrRadioField = field.type === 'CHECKBOX' || field.type === 'RADIO'; const isCheckboxOrRadioField = field.type === 'CHECKBOX' || field.type === 'RADIO';
const isFieldSigned = field.inserted;
const style = { const style = {
top: `${coords.y}px`, top: `${coords.y}px`,
left: `${coords.x}px`, left: `${coords.x}px`,
// height: `${coords.height}px`,
// width: `${coords.width}px`,
...(!isCheckboxOrRadioField && { ...(!isCheckboxOrRadioField && {
height: `${coords.height}px`, height: `${coords.height}px`,
width: `${coords.width}px`, width: `${coords.width}px`,
@@ -97,7 +41,14 @@ export function FieldContainerPortal({
); );
} }
export function FieldRootContainer({ field, children, cardClassName }: FieldContainerPortalProps) { export type FieldRootContainerProps = {
field: Field;
color?: RecipientColorStyles;
children: React.ReactNode;
className?: string;
};
export function FieldRootContainer({ field, children, color, className }: FieldRootContainerProps) {
const [isValidating, setIsValidating] = useState(false); const [isValidating, setIsValidating] = useState(false);
const ref = React.useRef<HTMLDivElement>(null); const ref = React.useRef<HTMLDivElement>(null);
@@ -121,33 +72,26 @@ export function FieldRootContainer({ field, children, cardClassName }: FieldCont
}; };
}, []); }, []);
const parsedField = useMemo(
() => (field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : null),
[field.fieldMeta],
);
const isCheckboxOrRadio = useMemo(
() => parsedField?.type === 'checkbox' || parsedField?.type === 'radio',
[parsedField],
);
const cardClassNames = useMemo(
() => getCardClassNames(field, parsedField, isValidating, isCheckboxOrRadio, cardClassName),
[field, parsedField, isValidating, isCheckboxOrRadio, cardClassName],
);
return ( return (
<FieldContainerPortal field={field}> <FieldContainerPortal field={field}>
<Card <div
id={`field-${field.id}`} id={`field-${field.id}`}
ref={ref} ref={ref}
data-field-type={field.type} data-field-type={field.type}
data-inserted={field.inserted ? 'true' : 'false'} data-inserted={field.inserted ? 'true' : 'false'}
className={cardClassNames} className={cn(
'field--FieldRootContainer field-card-container dark-mode-disabled group relative z-20 flex h-full w-full items-center rounded-[2px] bg-white/90 ring-2 ring-gray-200 transition-all',
color?.base,
{
'px-2': field.type !== FieldType.SIGNATURE && field.type !== FieldType.FREE_SIGNATURE,
'justify-center': !field.inserted,
'ring-orange-300': isValidating && isFieldUnsignedAndRequired(field),
},
className,
)}
> >
<CardContent className="text-foreground hover:shadow-primary-foreground group flex h-full w-full flex-col items-center justify-center p-2"> {children}
{children} </div>
</CardContent>
</Card>
</FieldContainerPortal> </FieldContainerPortal>
); );
} }

View File

@@ -0,0 +1,95 @@
// !: We declare all of our classes here since TailwindCSS will remove any unused CSS classes,
// !: therefore doing this at runtime is not possible without whitelisting a set of classnames.
// !:
// !: This will later be improved as we move to a CSS variable approach and rotate the lightness
export type RecipientColorMap = Record<number, RecipientColorStyles>;
export type RecipientColorStyles = {
base: string;
fieldItem: string;
fieldItemInitials: string;
comboxBoxTrigger: string;
comboxBoxItem: string;
};
// !: values of the declared variable to do all the background, border and shadow styles.
export const RECIPIENT_COLOR_STYLES = {
green: {
base: 'ring-recipient-green hover:bg-recipient-green/30',
fieldItem: 'group/field-item rounded-[2px]',
fieldItemInitials: 'group-hover/field-item:bg-recipient-green',
comboxBoxTrigger:
'ring-2 ring-recipient-green hover:bg-recipient-green/15 active:bg-recipient-green/15 shadow-[0_0_0_5px_hsl(var(--recipient-green)/10%),0_0_0_2px_hsl(var(--recipient-green)/60%),0_0_0_0.5px_hsl(var(--recipient-green))]',
comboxBoxItem: 'hover:bg-recipient-green/15 active:bg-recipient-green/15',
},
blue: {
base: 'ring-recipient-blue hover:bg-recipient-blue/30',
fieldItem: 'group/field-item rounded-[2px]',
fieldItemInitials: 'group-hover/field-item:bg-recipient-blue',
comboxBoxTrigger:
'ring-2 ring-recipient-blue hover:bg-recipient-blue/15 active:bg-recipient-blue/15 shadow-[0_0_0_5px_hsl(var(--recipient-blue)/10%),0_0_0_2px_hsl(var(--recipient-blue)/60%),0_0_0_0.5px_hsl(var(--recipient-blue))]',
comboxBoxItem: 'ring-recipient-blue hover:bg-recipient-blue/15 active:bg-recipient-blue/15',
},
purple: {
base: 'ring-recipient-purple hover:bg-recipient-purple/30',
fieldItem: 'group/field-item rounded-[2px]',
fieldItemInitials: 'group-hover/field-item:bg-recipient-purple',
comboxBoxTrigger:
'ring-2 ring-recipient-purple hover:bg-recipient-purple/15 active:bg-recipient-purple/15 shadow-[0_0_0_5px_hsl(var(--recipient-purple)/10%),0_0_0_2px_hsl(var(--recipient-purple)/60%),0_0_0_0.5px_hsl(var(--recipient-purple))]',
comboxBoxItem: 'hover:bg-recipient-purple/15 active:bg-recipient-purple/15',
},
orange: {
base: 'ring-recipient-orange hover:bg-recipient-orange/30',
fieldItem: 'group/field-item rounded-[2px]',
fieldItemInitials: 'group-hover/field-item:bg-recipient-orange',
comboxBoxTrigger:
'ring-2 ring-recipient-orange hover:bg-recipient-orange/15 active:bg-recipient-orange/15 shadow-[0_0_0_5px_hsl(var(--recipient-orange)/10%),0_0_0_2px_hsl(var(--recipient-orange)/60%),0_0_0_0.5px_hsl(var(--recipient-orange))]',
comboxBoxItem: 'hover:bg-recipient-orange/15 active:bg-recipient-orange/15',
},
yellow: {
base: 'ring-recipient-yellow hover:bg-recipient-yellow/30',
fieldItem: 'group/field-item rounded-[2px]',
fieldItemInitials: 'group-hover/field-item:bg-recipient-yellow',
comboxBoxTrigger:
'ring-2 ring-recipient-yellow hover:bg-recipient-yellow/15 active:bg-recipient-yellow/15 shadow-[0_0_0_5px_hsl(var(--recipient-yellow)/10%),0_0_0_2px_hsl(var(--recipient-yellow)/60%),0_0_0_0.5px_hsl(var(--recipient-yellow))]',
comboxBoxItem: 'hover:bg-recipient-yellow/15 active:bg-recipient-yellow/15',
},
pink: {
base: 'ring-recipient-pink hover:bg-recipient-pink/30',
fieldItem: 'group/field-item rounded-[2px]',
fieldItemInitials: 'group-hover/field-item:bg-recipient-pink',
comboxBoxTrigger:
'ring-2 ring-recipient-pink hover:bg-recipient-pink/15 active:bg-recipient-pink/15 shadow-[0_0_0_5px_hsl(var(--recipient-pink)/10%),0_0_0_2px_hsl(var(--recipient-pink)/60%),0_0_0_0.5px_hsl(var(--recipient-pink',
comboxBoxItem: 'hover:bg-recipient-pink/15 active:bg-recipient-pink/15',
},
} satisfies Record<string, RecipientColorStyles>;
export type CombinedStylesKey = keyof typeof RECIPIENT_COLOR_STYLES;
export const AVAILABLE_RECIPIENT_COLORS = [
'green',
'blue',
'purple',
'orange',
'yellow',
'pink',
] satisfies CombinedStylesKey[];
export const useRecipientColors = (index: number) => {
const key = AVAILABLE_RECIPIENT_COLORS[index % AVAILABLE_RECIPIENT_COLORS.length];
return RECIPIENT_COLOR_STYLES[key];
};
export const getRecipientColorStyles = (index: number) => {
// Disabling the rule since the hook doesn't do anything special and can
// be used universally.
// eslint-disable-next-line react-hooks/rules-of-hooks
return useRecipientColors(index);
};

View File

@@ -1,108 +0,0 @@
// !: We declare all of our classes here since TailwindCSS will remove any unused CSS classes,
// !: therefore doing this at runtime is not possible without whitelisting a set of classnames.
// !:
// !: This will later be improved as we move to a CSS variable approach and rotate the lightness
// !: values of the declared variable to do all the background, border and shadow styles.
export const SIGNER_COLOR_STYLES = {
green: {
default: {
background: 'bg-[hsl(var(--signer-green))]',
base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-green)/10%),0_0_0_2px_hsl(var(--signer-green)/60%),0_0_0_0.5px_hsl(var(--signer-green))]',
fieldItem:
'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-green))]/10 hover:to-[hsl(var(--signer-green))]/10',
fieldItemInitials:
'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-green))]',
comboxBoxItem:
'hover:bg-[hsl(var(--signer-green)/15%)] active:bg-[hsl(var(--signer-green)/15%)]',
},
},
blue: {
default: {
background: 'bg-[hsl(var(--signer-blue))]',
base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-blue)/10%),0_0_0_2px_hsl(var(--signer-blue)/60%),0_0_0_0.5px_hsl(var(--signer-blue))]',
fieldItem:
'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-blue))]/10 hover:to-[hsl(var(--signer-blue))]/10',
fieldItemInitials:
'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-blue))]',
comboxBoxItem:
'hover:bg-[hsl(var(--signer-blue)/15%)] active:bg-[hsl(var(--signer-blue)/15%)]',
},
},
purple: {
default: {
background: 'bg-[hsl(var(--signer-purple))]',
base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-purple)/10%),0_0_0_2px_hsl(var(--signer-purple)/60%),0_0_0_0.5px_hsl(var(--signer-purple))]',
fieldItem:
'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-purple))]/10 hover:to-[hsl(var(--signer-purple))]/10',
fieldItemInitials:
'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-purple))]',
comboxBoxItem:
'hover:bg-[hsl(var(--signer-purple)/15%)] active:bg-[hsl(var(--signer-purple)/15%)]',
},
},
orange: {
default: {
background: 'bg-[hsl(var(--signer-orange))]',
base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-orange)/10%),0_0_0_2px_hsl(var(--signer-orange)/60%),0_0_0_0.5px_hsl(var(--signer-orange))]',
fieldItem:
'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-orange))]/10 hover:to-[hsl(var(--signer-orange))]/10',
fieldItemInitials:
'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-orange))]',
comboxBoxItem:
'hover:bg-[hsl(var(--signer-orange)/15%)] active:bg-[hsl(var(--signer-orange)/15%)]',
},
},
yellow: {
default: {
background: 'bg-[hsl(var(--signer-yellow))]',
base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-yellow)/10%),0_0_0_2px_hsl(var(--signer-yellow)/60%),0_0_0_0.5px_hsl(var(--signer-yellow))]',
fieldItem:
'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-yellow))]/10 hover:to-[hsl(var(--signer-yellow))]/10',
fieldItemInitials:
'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-yellow))]',
comboxBoxItem:
'hover:bg-[hsl(var(--signer-yellow)/15%)] active:bg-[hsl(var(--signer-yellow)/15%)]',
},
},
pink: {
default: {
background: 'bg-[hsl(var(--signer-pink))]',
base: 'rounded-lg shadow-[0_0_0_5px_hsl(var(--signer-pink)/10%),0_0_0_2px_hsl(var(--signer-pink)/60%),0_0_0_0.5px_hsl(var(--signer-pink))]',
fieldItem:
'group/field-item p-2 border-none ring-none hover:bg-gradient-to-r hover:from-[hsl(var(--signer-pink))]/10 hover:to-[hsl(var(--signer-pink))]/10',
fieldItemInitials:
'opacity-0 transition duration-200 group-hover/field-item:opacity-100 group-hover/field-item:bg-[hsl(var(--signer-pink))]',
comboxBoxItem:
'hover:bg-[hsl(var(--signer-pink)/15%)] active:bg-[hsl(var(--signer-pink)/15%)]',
},
},
};
export type CombinedStylesKey = keyof typeof SIGNER_COLOR_STYLES;
export const AVAILABLE_SIGNER_COLORS = [
'green',
'blue',
'purple',
'orange',
'yellow',
'pink',
] satisfies CombinedStylesKey[];
export const useSignerColors = (index: number) => {
const key = AVAILABLE_SIGNER_COLORS[index % AVAILABLE_SIGNER_COLORS.length];
return SIGNER_COLOR_STYLES[key];
};
export const getSignerColorStyles = (index: number) => {
// Disabling the rule since the hook doesn't do anything special and can
// be used universally.
// eslint-disable-next-line react-hooks/rules-of-hooks
return useSignerColors(index);
};

View File

@@ -42,7 +42,7 @@ import {
} from '@documenso/lib/utils/recipients'; } from '@documenso/lib/utils/recipients';
import { FieldToolTip } from '../../components/field/field-tooltip'; import { FieldToolTip } from '../../components/field/field-tooltip';
import { getSignerColorStyles, useSignerColors } from '../../lib/signer-colors'; import { getRecipientColorStyles, useRecipientColors } from '../../lib/recipient-colors';
import { cn } from '../../lib/utils'; import { cn } from '../../lib/utils';
import { Alert, AlertDescription } from '../alert'; import { Alert, AlertDescription } from '../alert';
import { Button } from '../button'; import { Button } from '../button';
@@ -87,7 +87,6 @@ export type FieldFormType = {
export type AddFieldsFormProps = { export type AddFieldsFormProps = {
documentFlow: DocumentFlowStep; documentFlow: DocumentFlowStep;
hideRecipients?: boolean;
recipients: Recipient[]; recipients: Recipient[];
fields: Field[]; fields: Field[];
onSubmit: (_data: TAddFieldsFormSchema) => void; onSubmit: (_data: TAddFieldsFormSchema) => void;
@@ -98,7 +97,6 @@ export type AddFieldsFormProps = {
export const AddFieldsFormPartial = ({ export const AddFieldsFormPartial = ({
documentFlow, documentFlow,
hideRecipients = false,
recipients, recipients,
fields, fields,
onSubmit, onSubmit,
@@ -182,9 +180,10 @@ export const AddFieldsFormPartial = ({
null, null,
); );
const selectedSignerIndex = recipients.findIndex((r) => r.id === selectedSigner?.id); const selectedSignerIndex = recipients.findIndex((r) => r.id === selectedSigner?.id);
const selectedSignerStyles = useSignerColors( const selectedSignerStyles = useRecipientColors(
selectedSignerIndex === -1 ? 0 : selectedSignerIndex, selectedSignerIndex === -1 ? 0 : selectedSignerIndex,
); );
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false); const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
const filterFieldsWithEmptyValues = (fields: typeof localFields, fieldType: string) => const filterFieldsWithEmptyValues = (fields: typeof localFields, fieldType: string) =>
@@ -600,13 +599,12 @@ export const AddFieldsFormPartial = ({
{selectedField && ( {selectedField && (
<div <div
className={cn( className={cn(
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white transition duration-200 [container-type:size]', 'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center rounded-[2px] bg-white ring-2 transition duration-200 [container-type:size]',
selectedSignerStyles.default.base, selectedSignerStyles?.base,
{ {
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds, '-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
'dark:text-black/60': isFieldWithinBounds, 'dark:text-black/60': isFieldWithinBounds,
}, },
// selectedField === FieldType.SIGNATURE && fontCaveat.className,
)} )}
style={{ style={{
top: coords.y, top: coords.y,
@@ -653,7 +651,6 @@ export const AddFieldsFormPartial = ({
setCurrentField(field); setCurrentField(field);
handleAdvancedSettings(); handleAdvancedSettings();
}} }}
hideRecipients={hideRecipients}
hasErrors={!!hasFieldError} hasErrors={!!hasFieldError}
active={activeFieldId === field.formId} active={activeFieldId === field.formId}
onFieldActivate={() => setActiveFieldId(field.formId)} onFieldActivate={() => setActiveFieldId(field.formId)}
@@ -662,125 +659,124 @@ export const AddFieldsFormPartial = ({
); );
})} })}
{!hideRecipients && ( <Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}> <PopoverTrigger asChild>
<PopoverTrigger asChild> <Button
<Button type="button"
type="button" variant="outline"
variant="outline" role="combobox"
role="combobox" className={cn(
className={cn( 'bg-background text-muted-foreground hover:text-foreground mb-12 mt-2 justify-between font-normal',
'bg-background text-muted-foreground hover:text-foreground mb-12 mt-2 justify-between font-normal', selectedSignerStyles?.comboxBoxTrigger,
selectedSignerStyles.default.base, )}
)} >
> {selectedSigner?.email && (
{selectedSigner?.email && ( <span className="flex-1 truncate text-left">
<span className="flex-1 truncate text-left"> {selectedSigner?.name} ({selectedSigner?.email})
{selectedSigner?.name} ({selectedSigner?.email}) </span>
</span> )}
)}
{!selectedSigner?.email && ( {!selectedSigner?.email && (
<span className="flex-1 truncate text-left">{selectedSigner?.email}</span> <span className="flex-1 truncate text-left">{selectedSigner?.email}</span>
)} )}
<ChevronsUpDown className="ml-2 h-4 w-4" /> <ChevronsUpDown className="ml-2 h-4 w-4" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent className="p-0" align="start">
<Command value={selectedSigner?.email}> <Command value={selectedSigner?.email}>
<CommandInput /> <CommandInput />
<CommandEmpty> <CommandEmpty>
<span className="text-muted-foreground inline-block px-4"> <span className="text-muted-foreground inline-block px-4">
<Trans>No recipient matching this description was found.</Trans> <Trans>No recipient matching this description was found.</Trans>
</span> </span>
</CommandEmpty> </CommandEmpty>
{recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => ( {/* Note: This is duplicated in `add-template-fields.tsx` */}
<CommandGroup key={roleIndex}> {recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => (
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium"> <CommandGroup key={roleIndex}>
{_(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)} <div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
{_(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"
>
<Trans>No recipients with this role</Trans>
</div> </div>
)}
{roleRecipients.length === 0 && ( {roleRecipients.map((recipient) => (
<div <CommandItem
key={`${role}-empty`} key={recipient.id}
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs" className={cn(
'px-2 last:mb-1 [&:not(:first-child)]:mt-1',
getRecipientColorStyles(
Math.max(
recipients.findIndex((r) => r.id === recipient.id),
0,
),
)?.comboxBoxItem,
{
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
},
)}
onSelect={() => {
setSelectedSigner(recipient);
setShowRecipientsSelector(false);
}}
>
<span
className={cn('text-foreground/70 truncate', {
'text-foreground/80': recipient === selectedSigner,
})}
> >
<Trans>No recipients with this role</Trans> {recipient.name && (
</div> <span title={`${recipient.name} (${recipient.email})`}>
)} {recipient.name} ({recipient.email})
</span>
{roleRecipients.map((recipient) => (
<CommandItem
key={recipient.id}
className={cn(
'px-2 last:mb-1 [&:not(:first-child)]:mt-1',
getSignerColorStyles(
Math.max(
recipients.findIndex((r) => r.id === recipient.id),
0,
),
).default.comboxBoxItem,
{
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
},
)} )}
onSelect={() => {
setSelectedSigner(recipient);
setShowRecipientsSelector(false);
}}
>
<span
className={cn('text-foreground/70 truncate', {
'text-foreground/80': recipient === selectedSigner,
})}
>
{recipient.name && (
<span title={`${recipient.name} (${recipient.email})`}>
{recipient.name} ({recipient.email})
</span>
)}
{!recipient.name && ( {!recipient.name && (
<span title={recipient.email}>{recipient.email}</span> <span title={recipient.email}>{recipient.email}</span>
)} )}
</span> </span>
<div className="ml-auto flex items-center justify-center"> <div className="ml-auto flex items-center justify-center">
{recipient.sendStatus !== SendStatus.SENT ? ( {recipient.sendStatus !== SendStatus.SENT ? (
<Check <Check
aria-hidden={recipient !== selectedSigner} aria-hidden={recipient !== selectedSigner}
className={cn('h-4 w-4 flex-shrink-0', { className={cn('h-4 w-4 flex-shrink-0', {
'opacity-0': recipient !== selectedSigner, 'opacity-0': recipient !== selectedSigner,
'opacity-100': recipient === selectedSigner, 'opacity-100': recipient === selectedSigner,
})} })}
/> />
) : ( ) : (
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
<Info className="ml-2 h-4 w-4" /> <Info className="ml-2 h-4 w-4" />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="text-muted-foreground max-w-xs"> <TooltipContent className="text-muted-foreground max-w-xs">
<Trans> <Trans>
This document has already been sent to this recipient. You This document has already been sent to this recipient. You can
can no longer edit this recipient. no longer edit this recipient.
</Trans> </Trans>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
)} )}
</div> </div>
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>
))} ))}
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
)}
<Form {...form}> <Form {...form}>
<div className="-mx-2 flex-1 overflow-y-auto px-2"> <div className="-mx-2 flex-1 overflow-y-auto px-2">
@@ -795,7 +791,6 @@ export const AddFieldsFormPartial = ({
<Card <Card
className={cn( className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50', 'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)} )}
> >
<CardContent className="flex flex-col items-center justify-center px-6 py-4"> <CardContent className="flex flex-col items-center justify-center px-6 py-4">
@@ -820,7 +815,6 @@ export const AddFieldsFormPartial = ({
<Card <Card
className={cn( className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50', 'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)} )}
> >
<CardContent className="flex flex-col items-center justify-center px-6 py-4"> <CardContent className="flex flex-col items-center justify-center px-6 py-4">
@@ -846,7 +840,6 @@ export const AddFieldsFormPartial = ({
<Card <Card
className={cn( className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50', 'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)} )}
> >
<CardContent className="flex flex-col items-center justify-center px-6 py-4"> <CardContent className="flex flex-col items-center justify-center px-6 py-4">
@@ -872,7 +865,6 @@ export const AddFieldsFormPartial = ({
<Card <Card
className={cn( className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50', 'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)} )}
> >
<CardContent className="p-4"> <CardContent className="p-4">
@@ -898,7 +890,6 @@ export const AddFieldsFormPartial = ({
<Card <Card
className={cn( className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50', 'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)} )}
> >
<CardContent className="p-4"> <CardContent className="p-4">
@@ -924,7 +915,6 @@ export const AddFieldsFormPartial = ({
<Card <Card
className={cn( className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50', 'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)} )}
> >
<CardContent className="p-4"> <CardContent className="p-4">
@@ -950,7 +940,6 @@ export const AddFieldsFormPartial = ({
<Card <Card
className={cn( className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50', 'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)} )}
> >
<CardContent className="p-4"> <CardContent className="p-4">
@@ -976,7 +965,6 @@ export const AddFieldsFormPartial = ({
<Card <Card
className={cn( className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50', 'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)} )}
> >
<CardContent className="p-4"> <CardContent className="p-4">
@@ -1002,7 +990,6 @@ export const AddFieldsFormPartial = ({
<Card <Card
className={cn( className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50', 'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)} )}
> >
<CardContent className="p-4"> <CardContent className="p-4">
@@ -1029,7 +1016,6 @@ export const AddFieldsFormPartial = ({
<Card <Card
className={cn( className={cn(
'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50', 'flex h-full w-full cursor-pointer items-center justify-center group-disabled:opacity-50',
// selectedSignerStyles.borderClass,
)} )}
> >
<CardContent className="p-4"> <CardContent className="p-4">

View File

@@ -24,6 +24,10 @@ import {
DocumentGlobalAuthActionSelect, DocumentGlobalAuthActionSelect,
DocumentGlobalAuthActionTooltip, DocumentGlobalAuthActionTooltip,
} from '@documenso/ui/components/document/document-global-auth-action-select'; } from '@documenso/ui/components/document/document-global-auth-action-select';
import {
DocumentReadOnlyFields,
mapFieldsWithRecipients,
} from '@documenso/ui/components/document/document-read-only-fields';
import { import {
DocumentVisibilitySelect, DocumentVisibilitySelect,
DocumentVisibilityTooltip, DocumentVisibilityTooltip,
@@ -59,7 +63,6 @@ import {
DocumentFlowFormContainerHeader, DocumentFlowFormContainerHeader,
DocumentFlowFormContainerStep, DocumentFlowFormContainerStep,
} from './document-flow-root'; } from './document-flow-root';
import { ShowFieldItem } from './show-field-item';
import type { DocumentFlowStep } from './types'; import type { DocumentFlowStep } from './types';
export type AddSettingsFormProps = { export type AddSettingsFormProps = {
@@ -154,10 +157,13 @@ export const AddSettingsFormPartial = ({
/> />
<DocumentFlowFormContainerContent> <DocumentFlowFormContainerContent>
{isDocumentPdfLoaded && {isDocumentPdfLoaded && (
fields.map((field, index) => ( <DocumentReadOnlyFields
<ShowFieldItem key={index} field={field} recipients={recipients} /> showRecipientColors={true}
))} recipientIds={recipients.map((recipient) => recipient.id)}
fields={mapFieldsWithRecipients(fields, recipients)}
/>
)}
<Form {...form}> <Form {...form}>
<fieldset <fieldset

View File

@@ -23,6 +23,10 @@ import { RecipientActionAuthSelect } from '@documenso/ui/components/recipient/re
import { RecipientRoleSelect } from '@documenso/ui/components/recipient/recipient-role-select'; import { RecipientRoleSelect } from '@documenso/ui/components/recipient/recipient-role-select';
import { cn } from '@documenso/ui/lib/utils'; import { cn } from '@documenso/ui/lib/utils';
import {
DocumentReadOnlyFields,
mapFieldsWithRecipients,
} from '../../components/document/document-read-only-fields';
import { Button } from '../button'; import { Button } from '../button';
import { Checkbox } from '../checkbox'; import { Checkbox } from '../checkbox';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
@@ -40,7 +44,6 @@ import {
DocumentFlowFormContainerHeader, DocumentFlowFormContainerHeader,
DocumentFlowFormContainerStep, DocumentFlowFormContainerStep,
} from './document-flow-root'; } from './document-flow-root';
import { ShowFieldItem } from './show-field-item';
import { SigningOrderConfirmation } from './signing-order-confirmation'; import { SigningOrderConfirmation } from './signing-order-confirmation';
import type { DocumentFlowStep } from './types'; import type { DocumentFlowStep } from './types';
@@ -368,10 +371,13 @@ export const AddSignersFormPartial = ({
description={documentFlow.description} description={documentFlow.description}
/> />
<DocumentFlowFormContainerContent> <DocumentFlowFormContainerContent>
{isDocumentPdfLoaded && {isDocumentPdfLoaded && (
fields.map((field, index) => ( <DocumentReadOnlyFields
<ShowFieldItem key={index} field={field} recipients={recipients} /> showRecipientColors={true}
))} recipientIds={recipients.map((recipient) => recipient.id)}
fields={mapFieldsWithRecipients(fields, recipients)}
/>
)}
<AnimateGenericFadeInOut motionKey={showAdvancedSettings ? 'Show' : 'Hide'}> <AnimateGenericFadeInOut motionKey={showAdvancedSettings ? 'Show' : 'Hide'}>
<Form {...form}> <Form {...form}>

View File

@@ -16,6 +16,10 @@ import { Tabs, TabsList, TabsTrigger } from '@documenso/ui/primitives/tabs';
import { CopyTextButton } from '../../components/common/copy-text-button'; import { CopyTextButton } from '../../components/common/copy-text-button';
import { DocumentEmailCheckboxes } from '../../components/document/document-email-checkboxes'; import { DocumentEmailCheckboxes } from '../../components/document/document-email-checkboxes';
import {
DocumentReadOnlyFields,
mapFieldsWithRecipients,
} from '../../components/document/document-read-only-fields';
import { AvatarWithText } from '../avatar'; import { AvatarWithText } from '../avatar';
import { FormErrorMessage } from '../form/form-error-message'; import { FormErrorMessage } from '../form/form-error-message';
import { Input } from '../input'; import { Input } from '../input';
@@ -31,7 +35,6 @@ import {
DocumentFlowFormContainerHeader, DocumentFlowFormContainerHeader,
DocumentFlowFormContainerStep, DocumentFlowFormContainerStep,
} from './document-flow-root'; } from './document-flow-root';
import { ShowFieldItem } from './show-field-item';
import type { DocumentFlowStep } from './types'; import type { DocumentFlowStep } from './types';
export type AddSubjectFormProps = { export type AddSubjectFormProps = {
@@ -103,10 +106,13 @@ export const AddSubjectFormPartial = ({
/> />
<DocumentFlowFormContainerContent> <DocumentFlowFormContainerContent>
<div className="flex flex-col"> <div className="flex flex-col">
{isDocumentPdfLoaded && {isDocumentPdfLoaded && (
fields.map((field, index) => ( <DocumentReadOnlyFields
<ShowFieldItem key={index} field={field} recipients={recipients} /> showRecipientColors={true}
))} recipientIds={recipients.map((recipient) => recipient.id)}
fields={mapFieldsWithRecipients(fields, recipients)}
/>
)}
<Tabs <Tabs
onValueChange={(value) => onValueChange={(value) =>

View File

@@ -1,46 +0,0 @@
import { ZCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
import type { TCheckboxFieldMeta } from '@documenso/lib/types/field-meta';
import { Checkbox } from '@documenso/ui/primitives/checkbox';
import { Label } from '@documenso/ui/primitives/label';
import { FieldIcon } from '../field-icon';
import type { TDocumentFlowFormSchema } from '../types';
type Field = TDocumentFlowFormSchema['fields'][0];
export type CheckboxFieldProps = {
field: Field;
};
export const CheckboxField = ({ field }: CheckboxFieldProps) => {
let parsedFieldMeta: TCheckboxFieldMeta | undefined = undefined;
if (field.fieldMeta) {
parsedFieldMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
}
if (parsedFieldMeta && (!parsedFieldMeta.values || parsedFieldMeta.values.length === 0)) {
return <FieldIcon fieldMeta={field.fieldMeta} type={field.type} />;
}
return (
<div className="flex flex-col gap-y-1">
{!parsedFieldMeta?.values ? (
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} />
) : (
parsedFieldMeta.values.map((item: { value: string; checked: boolean }, index: number) => (
<div key={index} className="flex items-center gap-x-1.5">
<Checkbox
className="dark:border-field-border h-3 w-3 bg-white"
id={`checkbox-${index}`}
checked={item.checked}
/>
<Label htmlFor={`checkbox-${index}`} className="text-xs font-normal text-black">
{item.value}
</Label>
</div>
))
)}
</div>
);
};

View File

@@ -1,49 +0,0 @@
import { ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
import type { TRadioFieldMeta } from '@documenso/lib/types/field-meta';
import { Label } from '@documenso/ui/primitives/label';
import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group';
import { FieldIcon } from '../field-icon';
import type { TDocumentFlowFormSchema } from '../types';
type Field = TDocumentFlowFormSchema['fields'][0];
export type RadioFieldProps = {
field: Field;
};
export const RadioField = ({ field }: RadioFieldProps) => {
let parsedFieldMeta: TRadioFieldMeta | undefined = undefined;
if (field.fieldMeta) {
parsedFieldMeta = ZRadioFieldMeta.parse(field.fieldMeta);
}
if (parsedFieldMeta && (!parsedFieldMeta.values || parsedFieldMeta.values.length === 0)) {
return <FieldIcon fieldMeta={field.fieldMeta} type={field.type} />;
}
return (
<div className="flex flex-col gap-y-2">
{!parsedFieldMeta?.values ? (
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} />
) : (
<RadioGroup className="gap-y-1">
{parsedFieldMeta.values?.map((item, index) => (
<div key={index} className="flex items-center gap-x-1.5">
<RadioGroupItem
className="dark:border-field-border pointer-events-none h-3 w-3"
value={item.value}
id={`option-${index}`}
checked={item.checked}
/>
<Label htmlFor={`option-${index}`} className="text-xs font-normal text-black">
{item.value}
</Label>
</div>
))}
</RadioGroup>
)}
</div>
);
};

View File

@@ -0,0 +1,188 @@
import { useLingui } from '@lingui/react';
import type { DocumentMeta, Signature } from '@prisma/client';
import { FieldType } from '@prisma/client';
import { ChevronDown } from 'lucide-react';
import {
DEFAULT_DOCUMENT_DATE_FORMAT,
convertToLocalSystemFormat,
} from '@documenso/lib/constants/date-formats';
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { fromCheckboxValue } from '@documenso/lib/universal/field-checkbox';
import { cn } from '../../lib/utils';
import { Checkbox } from '../checkbox';
import { Label } from '../label';
import { RadioGroup, RadioGroupItem } from '../radio-group';
import { FRIENDLY_FIELD_TYPE } from './types';
type FieldIconProps = {
/**
* Loose field type since this is used for partial fields.
*/
field: {
inserted?: boolean;
customText?: string;
type: FieldType;
fieldMeta?: TFieldMetaSchema | null;
signature?: Signature | null;
};
documentMeta?: DocumentMeta;
};
/**
* Renders the content inside field containers prior to sealing.
*/
export const FieldContent = ({ field, documentMeta }: FieldIconProps) => {
const { _ } = useLingui();
const { type, fieldMeta } = field;
// Only render checkbox if values exist, otherwise render the empty checkbox field content.
if (
field.type === FieldType.CHECKBOX &&
field.fieldMeta?.type === 'checkbox' &&
field.fieldMeta.values &&
field.fieldMeta.values.length > 0
) {
let checkedValues: string[] = [];
try {
checkedValues = fromCheckboxValue(field.customText ?? '');
} catch (err) {
// Do nothing.
console.error(err);
}
return (
<div className="flex flex-col gap-y-1 py-0.5">
{field.fieldMeta.values.map((item, index) => (
<div key={index} className="flex items-center">
<Checkbox
className="h-3 w-3"
id={`checkbox-${index}`}
checked={checkedValues.includes(
item.value === '' ? `empty-value-${index + 1}` : item.value, // I got no idea...
)}
/>
{item.value && (
<Label
htmlFor={`checkbox-${index}`}
className="text-foreground ml-1.5 text-xs font-normal"
>
{item.value}
</Label>
)}
</div>
))}
</div>
);
}
// Only render radio if values exist, otherwise render the empty radio field content.
if (
field.type === FieldType.RADIO &&
field.fieldMeta?.type === 'radio' &&
field.fieldMeta.values &&
field.fieldMeta.values.length > 0
) {
return (
<div className="flex flex-col gap-y-2 py-0.5">
<RadioGroup className="gap-y-1">
{field.fieldMeta.values.map((item, index) => (
<div key={index} className="flex items-center">
<RadioGroupItem
className="pointer-events-none h-3 w-3"
value={item.value}
id={`option-${index}`}
checked={item.value === field.customText}
/>
{item.value && (
<Label
htmlFor={`option-${index}`}
className="text-foreground ml-1.5 text-xs font-normal"
>
{item.value}
</Label>
)}
</div>
))}
</RadioGroup>
</div>
);
}
if (
field.type === FieldType.DROPDOWN &&
field.fieldMeta?.type === 'dropdown' &&
!field.inserted
) {
return (
<div className="text-field-card-foreground flex flex-row items-center py-0.5 text-[clamp(0.07rem,25cqw,0.825rem)] text-sm">
<p>Select</p>
<ChevronDown className="h-4 w-4" />
</div>
);
}
if (
field.type === FieldType.SIGNATURE &&
field.signature?.signatureImageAsBase64 &&
field.inserted
) {
return (
<img
src={field.signature.signatureImageAsBase64}
alt="Signature"
className="h-full w-full object-contain"
/>
);
}
let textToDisplay = fieldMeta?.label || _(FRIENDLY_FIELD_TYPE[type]) || '';
const isSignatureField =
field.type === FieldType.SIGNATURE || field.type === FieldType.FREE_SIGNATURE;
// Trim default labels.
if (textToDisplay.length > 20) {
textToDisplay = textToDisplay.substring(0, 20) + '...';
}
if (field.inserted) {
if (field.customText) {
textToDisplay = field.customText;
}
if (field.type === FieldType.DATE) {
textToDisplay = convertToLocalSystemFormat(
field.customText ?? '',
documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
);
}
if (isSignatureField && field.signature?.typedSignature) {
textToDisplay = field.signature.typedSignature;
}
}
const textAlign = fieldMeta && 'textAlign' in fieldMeta ? fieldMeta.textAlign : 'left';
return (
<div
className={cn(
'text-field-card-foreground flex h-full w-full items-center justify-center gap-x-1.5 overflow-clip whitespace-nowrap text-center text-[clamp(0.07rem,25cqw,0.825rem)]',
{
// Using justify instead of align because we also vertically center the text.
'justify-start': field.inserted && !isSignatureField && textAlign === 'left',
'justify-end': field.inserted && !isSignatureField && textAlign === 'right',
'font-signature text-[clamp(0.07rem,25cqw,1.125rem)]': isSignatureField,
},
)}
>
{textToDisplay}
</div>
);
};

View File

@@ -1,72 +0,0 @@
import { Trans } from '@lingui/react/macro';
import { FieldType } from '@prisma/client';
import {
CalendarDays,
CheckSquare,
ChevronDown,
Contact,
Disc,
Hash,
Mail,
Type,
User,
} from 'lucide-react';
import type { TFieldMetaSchema as FieldMetaType } from '@documenso/lib/types/field-meta';
import { cn } from '../../lib/utils';
type FieldIconProps = {
fieldMeta: FieldMetaType;
type: FieldType;
};
const fieldIcons = {
[FieldType.INITIALS]: { icon: Contact, label: 'Initials' },
[FieldType.EMAIL]: { icon: Mail, label: 'Email' },
[FieldType.NAME]: { icon: User, label: 'Name' },
[FieldType.DATE]: { icon: CalendarDays, label: 'Date' },
[FieldType.TEXT]: { icon: Type, label: 'Text' },
[FieldType.NUMBER]: { icon: Hash, label: 'Number' },
[FieldType.RADIO]: { icon: Disc, label: 'Radio' },
[FieldType.CHECKBOX]: { icon: CheckSquare, label: 'Checkbox' },
[FieldType.DROPDOWN]: { icon: ChevronDown, label: 'Select' },
};
export const FieldIcon = ({ fieldMeta, type }: FieldIconProps) => {
if (type === 'SIGNATURE' || type === 'FREE_SIGNATURE') {
return (
<div
className={cn(
'text-field-card-foreground font-signature flex items-center justify-center gap-x-1 text-[clamp(0.575rem,25cqw,1.2rem)]',
)}
>
<Trans>Signature</Trans>
</div>
);
} else {
const Icon = fieldIcons[type]?.icon;
let label;
if (fieldMeta && (type === 'TEXT' || type === 'NUMBER')) {
if (type === 'TEXT' && 'text' in fieldMeta && fieldMeta.text && !fieldMeta.label) {
label =
fieldMeta.text.length > 20 ? fieldMeta.text.substring(0, 20) + '...' : fieldMeta.text;
} else if (fieldMeta.label) {
label =
fieldMeta.label.length > 20 ? fieldMeta.label.substring(0, 20) + '...' : fieldMeta.label;
} else {
label = fieldIcons[type]?.label;
}
} else {
label = fieldIcons[type]?.label;
}
return (
<div className="text-field-card-foreground flex items-center justify-center gap-x-1.5 text-[clamp(0.425rem,25cqw,0.825rem)]">
<Icon className="h-[clamp(0.625rem,20cqw,0.925rem)] w-[clamp(0.625rem,20cqw,0.925rem)]" />{' '}
{label}
</div>
);
}
};

View File

@@ -229,11 +229,19 @@ export const FieldAdvancedSettings = forwardRef<HTMLDivElement, FieldAdvancedSet
return ( return (
<div ref={ref} className="flex h-full flex-col"> <div ref={ref} className="flex h-full flex-col">
<DocumentFlowFormContainerHeader title={title} description={description} /> <DocumentFlowFormContainerHeader title={title} description={description} />
<DocumentFlowFormContainerContent> <DocumentFlowFormContainerContent>
{isDocumentPdfLoaded && {isDocumentPdfLoaded &&
fields.map((field, index) => ( fields.map((localField, index) => (
<span key={index} className="opacity-75 active:pointer-events-none"> <span key={index} className="opacity-75 active:pointer-events-none">
<FieldItem key={index} field={field} disabled={true} /> <FieldItem
key={index}
field={localField}
disabled={true}
fieldClassName={
localField.formId === field.formId ? 'ring-red-400' : 'ring-neutral-200'
}
/>
</span> </span>
))} ))}

View File

@@ -4,23 +4,21 @@ import { FieldType } from '@prisma/client';
import { CopyPlus, Settings2, Trash } from 'lucide-react'; import { CopyPlus, Settings2, Trash } from 'lucide-react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import { Rnd } from 'react-rnd'; import { Rnd } from 'react-rnd';
import { match } from 'ts-pattern';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta'; import type { TFieldMetaSchema } from '@documenso/lib/types/field-meta';
import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field-meta'; import { ZCheckboxFieldMeta, ZRadioFieldMeta } from '@documenso/lib/types/field-meta';
import { useSignerColors } from '../../lib/signer-colors'; import { useRecipientColors } from '../../lib/recipient-colors';
import { cn } from '../../lib/utils'; import { cn } from '../../lib/utils';
import { CheckboxField } from './advanced-fields/checkbox'; import { FieldContent } from './field-content';
import { RadioField } from './advanced-fields/radio';
import { FieldIcon } from './field-icon';
import type { TDocumentFlowFormSchema } from './types'; import type { TDocumentFlowFormSchema } from './types';
type Field = TDocumentFlowFormSchema['fields'][0]; type Field = TDocumentFlowFormSchema['fields'][0];
export type FieldItemProps = { export type FieldItemProps = {
field: Field; field: Field;
fieldClassName?: string;
passive?: boolean; passive?: boolean;
disabled?: boolean; disabled?: boolean;
minHeight?: number; minHeight?: number;
@@ -35,14 +33,17 @@ export type FieldItemProps = {
onFocus?: () => void; onFocus?: () => void;
onBlur?: () => void; onBlur?: () => void;
recipientIndex?: number; recipientIndex?: number;
hideRecipients?: boolean;
hasErrors?: boolean; hasErrors?: boolean;
active?: boolean; active?: boolean;
onFieldActivate?: () => void; onFieldActivate?: () => void;
onFieldDeactivate?: () => void; onFieldDeactivate?: () => void;
}; };
/**
* The item when editing fields??
*/
export const FieldItem = ({ export const FieldItem = ({
fieldClassName,
field, field,
passive, passive,
disabled, disabled,
@@ -58,7 +59,6 @@ export const FieldItem = ({
onBlur, onBlur,
onAdvancedSettings, onAdvancedSettings,
recipientIndex = 0, recipientIndex = 0,
hideRecipients = false,
hasErrors, hasErrors,
active, active,
onFieldActivate, onFieldActivate,
@@ -73,7 +73,7 @@ export const FieldItem = ({
const [settingsActive, setSettingsActive] = useState(false); const [settingsActive, setSettingsActive] = useState(false);
const $el = useRef(null); const $el = useRef(null);
const signerStyles = useSignerColors(recipientIndex); const signerStyles = useRecipientColors(recipientIndex);
const advancedField = [ const advancedField = [
'NUMBER', 'NUMBER',
@@ -209,7 +209,7 @@ export const FieldItem = ({
return createPortal( return createPortal(
<Rnd <Rnd
key={coords.pageX + coords.pageY + coords.pageHeight + coords.pageWidth} key={coords.pageX + coords.pageY + coords.pageHeight + coords.pageWidth}
className={cn('group', { className={cn('dark-mode-disabled group', {
'pointer-events-none': passive, 'pointer-events-none': passive,
'pointer-events-none cursor-not-allowed opacity-75': disabled, 'pointer-events-none cursor-not-allowed opacity-75': disabled,
'z-50': active && !disabled, 'z-50': active && !disabled,
@@ -260,11 +260,12 @@ export const FieldItem = ({
<div <div
className={cn( className={cn(
'relative flex h-full w-full items-center justify-center bg-white', 'group/field-item relative flex h-full w-full items-center justify-center rounded-[2px] bg-white/90 px-2 ring-2 transition-colors',
!hasErrors && signerStyles.default.base, !hasErrors && signerStyles.base,
!hasErrors && signerStyles.default.fieldItem, !hasErrors && signerStyles.fieldItem,
fieldClassName,
{ {
'rounded-lg border border-red-400 bg-red-400/20 shadow-[0_0_0_5px_theme(colors.red.500/10%),0_0_0_2px_theme(colors.red.500/40%),0_0_0_0.5px_theme(colors.red.500)]': 'rounded-[2px] border bg-red-400/20 shadow-[0_0_0_5px_theme(colors.red.500/10%),0_0_0_2px_theme(colors.red.500/40%),0_0_0_0.5px_theme(colors.red.500)] ring-red-400':
hasErrors, hasErrors,
}, },
!fixedSize && '[container-type:size]', !fixedSize && '[container-type:size]',
@@ -279,37 +280,31 @@ export const FieldItem = ({
ref={$el} ref={$el}
data-field-id={field.nativeId} data-field-id={field.nativeId}
> >
{match(field.type) <FieldContent field={field} />
.with('CHECKBOX', () => <CheckboxField field={field} />)
.with('RADIO', () => <RadioField field={field} />)
.otherwise(() => (
<FieldIcon fieldMeta={field.fieldMeta} type={field.type} />
))}
{!hideRecipients && ( {/* On hover, display recipient initials on side of field. */}
<div className="absolute -right-5 top-0 z-20 hidden h-full w-5 items-center justify-center group-hover:flex"> <div className="absolute -right-5 top-0 z-20 hidden h-full w-5 items-center justify-center group-hover:flex">
<div <div
className={cn( className={cn(
'flex h-5 w-5 flex-col items-center justify-center rounded-r-md text-[0.5rem] font-bold text-white', 'flex h-5 w-5 flex-col items-center justify-center rounded-r-md text-[0.5rem] font-bold text-white opacity-0 transition duration-200 group-hover/field-item:opacity-100',
signerStyles.default.fieldItemInitials, signerStyles.fieldItemInitials,
{ {
'!opacity-50': disabled || passive, '!opacity-50': disabled || passive,
}, },
)} )}
> >
{(field.signerEmail?.charAt(0)?.toUpperCase() ?? '') + {(field.signerEmail?.charAt(0)?.toUpperCase() ?? '') +
(field.signerEmail?.charAt(1)?.toUpperCase() ?? '')} (field.signerEmail?.charAt(1)?.toUpperCase() ?? '')}
</div>
</div> </div>
)} </div>
</div> </div>
{!disabled && settingsActive && ( {!disabled && settingsActive && (
<div className="z-[60] mt-1 flex justify-center"> <div className="absolute z-[60] mt-1 flex w-full items-center justify-center">
<div className="dark:bg-background group flex items-center justify-evenly gap-x-1 rounded-md border bg-gray-900 p-0.5"> <div className="group flex items-center justify-evenly gap-x-1 rounded-md border bg-gray-900 p-0.5">
{advancedField && ( {advancedField && (
<button <button
className="dark:text-muted-foreground/50 dark:hover:text-muted-foreground dark:hover:bg-foreground/10 rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100" className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
onClick={onAdvancedSettings} onClick={onAdvancedSettings}
onTouchEnd={onAdvancedSettings} onTouchEnd={onAdvancedSettings}
> >
@@ -318,7 +313,7 @@ export const FieldItem = ({
)} )}
<button <button
className="dark:text-muted-foreground/50 dark:hover:text-muted-foreground dark:hover:bg-foreground/10 rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100" className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
onClick={onDuplicate} onClick={onDuplicate}
onTouchEnd={onDuplicate} onTouchEnd={onDuplicate}
> >
@@ -326,7 +321,7 @@ export const FieldItem = ({
</button> </button>
<button <button
className="dark:text-muted-foreground/50 dark:hover:text-muted-foreground dark:hover:bg-foreground/10 rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100" className="rounded-sm p-1.5 text-gray-400 transition-colors hover:bg-white/10 hover:text-gray-100"
onClick={onRemove} onClick={onRemove}
onTouchEnd={onRemove} onTouchEnd={onRemove}
> >

View File

@@ -1,49 +0,0 @@
import { useLingui } from '@lingui/react';
import { FieldType, type Prisma } from '@prisma/client';
import { createPortal } from 'react-dom';
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
import { parseMessageDescriptor } from '@documenso/lib/utils/i18n';
import { cn } from '../../lib/utils';
import { Card, CardContent } from '../card';
import { FRIENDLY_FIELD_TYPE } from './types';
export type ShowFieldItemProps = {
field: Prisma.FieldGetPayload<null>;
recipients: Prisma.RecipientGetPayload<null>[];
};
export const ShowFieldItem = ({ field }: ShowFieldItemProps) => {
const { _ } = useLingui();
const coords = useFieldPageCoords(field);
return createPortal(
<div
className={cn('pointer-events-none absolute z-10 opacity-75')}
style={{
top: `${coords.y}px`,
left: `${coords.x}px`,
height: `${coords.height}px`,
width: `${coords.width}px`,
}}
>
<Card className={cn('bg-background h-full w-full [container-type:size]')}>
<CardContent
className={cn(
'text-muted-foreground/50 flex h-full w-full flex-col items-center justify-center p-0 text-[clamp(0.575rem,1.8cqw,1.2rem)] leading-none',
field.type === FieldType.SIGNATURE && 'font-signature',
)}
>
{parseMessageDescriptor(_, FRIENDLY_FIELD_TYPE[field.type])}
{/* <p className="text-muted-foreground/50 w-full truncate text-center text-xs">
{signerEmail}
</p> */}
</CardContent>
</Card>
</div>,
document.body,
);
};

View File

@@ -12,7 +12,12 @@ export const ElementVisible = ({ target, children }: ElementVisibleProps) => {
const observer = new MutationObserver((_mutations) => { const observer = new MutationObserver((_mutations) => {
const $el = document.querySelector(target); const $el = document.querySelector(target);
setVisible(!!$el); // Wait a fraction of a second to allow the scrollbar to load if it exists.
// If we don't wait, then the elements on the first page will be
// shifted across.
setTimeout(() => {
setVisible(!!$el);
}, 100);
}); });
observer.observe(document.body, { observer.observe(document.body, {

View File

@@ -53,7 +53,7 @@ import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/type
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover'; import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
import { useToast } from '@documenso/ui/primitives/use-toast'; import { useToast } from '@documenso/ui/primitives/use-toast';
import { getSignerColorStyles, useSignerColors } from '../../lib/signer-colors'; import { getRecipientColorStyles, useRecipientColors } from '../../lib/recipient-colors';
import type { FieldFormType } from '../document-flow/add-fields'; import type { FieldFormType } from '../document-flow/add-fields';
import { FieldAdvancedSettings } from '../document-flow/field-item-advanced-settings'; import { FieldAdvancedSettings } from '../document-flow/field-item-advanced-settings';
import { Form } from '../form/form'; import { Form } from '../form/form';
@@ -68,7 +68,6 @@ const DEFAULT_WIDTH_PX = MIN_WIDTH_PX * 2.5;
export type AddTemplateFieldsFormProps = { export type AddTemplateFieldsFormProps = {
documentFlow: DocumentFlowStep; documentFlow: DocumentFlowStep;
hideRecipients?: boolean;
recipients: Recipient[]; recipients: Recipient[];
fields: Field[]; fields: Field[];
onSubmit: (_data: TAddTemplateFieldsFormSchema) => void; onSubmit: (_data: TAddTemplateFieldsFormSchema) => void;
@@ -77,7 +76,6 @@ export type AddTemplateFieldsFormProps = {
export const AddTemplateFieldsFormPartial = ({ export const AddTemplateFieldsFormPartial = ({
documentFlow, documentFlow,
hideRecipients = false,
recipients, recipients,
fields, fields,
onSubmit, onSubmit,
@@ -136,7 +134,7 @@ export const AddTemplateFieldsFormPartial = ({
const [showRecipientsSelector, setShowRecipientsSelector] = useState(false); const [showRecipientsSelector, setShowRecipientsSelector] = useState(false);
const selectedSignerIndex = recipients.findIndex((r) => r.id === selectedSigner?.id); const selectedSignerIndex = recipients.findIndex((r) => r.id === selectedSigner?.id);
const selectedSignerStyles = useSignerColors( const selectedSignerStyles = useRecipientColors(
selectedSignerIndex === -1 ? 0 : selectedSignerIndex, selectedSignerIndex === -1 ? 0 : selectedSignerIndex,
); );
@@ -505,8 +503,8 @@ export const AddTemplateFieldsFormPartial = ({
{selectedField && ( {selectedField && (
<div <div
className={cn( className={cn(
'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white transition duration-200 [container-type:size]', 'text-muted-foreground dark:text-muted-background pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center rounded-[2px] bg-white ring-2 transition duration-200 [container-type:size]',
selectedSignerStyles.default.base, selectedSignerStyles?.base,
{ {
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds, '-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
'dark:text-black/60': isFieldWithinBounds, 'dark:text-black/60': isFieldWithinBounds,
@@ -549,7 +547,6 @@ export const AddTemplateFieldsFormPartial = ({
setCurrentField(field); setCurrentField(field);
handleAdvancedSettings(); handleAdvancedSettings();
}} }}
hideRecipients={hideRecipients}
active={activeFieldId === field.formId} active={activeFieldId === field.formId}
onFieldActivate={() => setActiveFieldId(field.formId)} onFieldActivate={() => setActiveFieldId(field.formId)}
onFieldDeactivate={() => setActiveFieldId(null)} onFieldDeactivate={() => setActiveFieldId(null)}
@@ -557,99 +554,98 @@ export const AddTemplateFieldsFormPartial = ({
); );
})} })}
{!hideRecipients && ( <Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}>
<Popover open={showRecipientsSelector} onOpenChange={setShowRecipientsSelector}> <PopoverTrigger asChild>
<PopoverTrigger asChild> <Button
<Button type="button"
type="button" variant="outline"
variant="outline" role="combobox"
role="combobox" className={cn(
className={cn( 'bg-background text-muted-foreground hover:text-foreground mb-12 mt-2 justify-between font-normal',
'bg-background text-muted-foreground hover:text-foreground mb-12 mt-2 justify-between font-normal', selectedSignerStyles?.comboxBoxTrigger,
selectedSignerStyles.default.base, )}
)} >
> {selectedSigner?.email && (
{selectedSigner?.email && ( <span className="flex-1 truncate text-left">
<span className="flex-1 truncate text-left"> {selectedSigner?.name} ({selectedSigner?.email})
{selectedSigner?.name} ({selectedSigner?.email}) </span>
</span> )}
)}
{!selectedSigner?.email && ( {!selectedSigner?.email && (
<span className="gradie flex-1 truncate text-left"> <span className="gradie flex-1 truncate text-left">
{selectedSigner?.email} {selectedSigner?.email}
</span> </span>
)} )}
<ChevronsUpDown className="ml-2 h-4 w-4" /> <ChevronsUpDown className="ml-2 h-4 w-4" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="p-0" align="start"> <PopoverContent className="p-0" align="start">
<Command value={selectedSigner?.email}> <Command value={selectedSigner?.email}>
<CommandInput /> <CommandInput />
<CommandEmpty> <CommandEmpty>
<span className="text-muted-foreground inline-block px-4"> <span className="text-muted-foreground inline-block px-4">
<Trans>No recipient matching this description was found.</Trans> <Trans>No recipient matching this description was found.</Trans>
</span> </span>
</CommandEmpty> </CommandEmpty>
{recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => ( {/* Note: This is duplicated in `add-fields.tsx` */}
<CommandGroup key={roleIndex}> {recipientsByRoleToDisplay.map(([role, roleRecipients], roleIndex) => (
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium"> <CommandGroup key={roleIndex}>
{_(RECIPIENT_ROLES_DESCRIPTION[role].roleNamePlural)} <div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
{_(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"
>
<Trans>No recipients with this role</Trans>
</div> </div>
)}
{roleRecipients.length === 0 && ( {roleRecipients.map((recipient) => (
<div <CommandItem
key={`${role}-empty`} key={recipient.id}
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs" className={cn(
'px-2 last:mb-1 [&:not(:first-child)]:mt-1',
getRecipientColorStyles(
Math.max(
recipients.findIndex((r) => r.id === recipient.id),
0,
),
)?.comboxBoxItem,
)}
onSelect={() => {
setSelectedSigner(recipient);
setShowRecipientsSelector(false);
}}
>
<span
className={cn('text-foreground/70 truncate', {
'text-foreground/80': recipient === selectedSigner,
})}
> >
<Trans>No recipients with this role</Trans> {recipient.name && (
</div> <span title={`${recipient.name} (${recipient.email})`}>
)} {recipient.name} ({recipient.email})
</span>
{roleRecipients.map((recipient) => (
<CommandItem
key={recipient.id}
className={cn(
'px-2 last:mb-1 [&:not(:first-child)]:mt-1',
getSignerColorStyles(
Math.max(
recipients.findIndex((r) => r.id === recipient.id),
0,
),
).default.comboxBoxItem,
)} )}
onSelect={() => {
setSelectedSigner(recipient);
setShowRecipientsSelector(false);
}}
>
<span
className={cn('text-foreground/70 truncate', {
'text-foreground/80': recipient === selectedSigner,
})}
>
{recipient.name && (
<span title={`${recipient.name} (${recipient.email})`}>
{recipient.name} ({recipient.email})
</span>
)}
{!recipient.name && ( {!recipient.name && (
<span title={recipient.email}>{recipient.email}</span> <span title={recipient.email}>{recipient.email}</span>
)} )}
</span> </span>
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>
))} ))}
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>
)}
<Form {...form}> <Form {...form}>
<div className="-mx-2 flex-1 overflow-y-auto px-2"> <div className="-mx-2 flex-1 overflow-y-auto px-2">

View File

@@ -25,6 +25,10 @@ import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-messa
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
import { toast } from '@documenso/ui/primitives/use-toast'; import { toast } from '@documenso/ui/primitives/use-toast';
import {
DocumentReadOnlyFields,
mapFieldsWithRecipients,
} from '../../components/document/document-read-only-fields';
import { Checkbox } from '../checkbox'; import { Checkbox } from '../checkbox';
import { import {
DocumentFlowFormContainerActions, DocumentFlowFormContainerActions,
@@ -33,7 +37,6 @@ import {
DocumentFlowFormContainerHeader, DocumentFlowFormContainerHeader,
DocumentFlowFormContainerStep, DocumentFlowFormContainerStep,
} from '../document-flow/document-flow-root'; } from '../document-flow/document-flow-root';
import { ShowFieldItem } from '../document-flow/show-field-item';
import { SigningOrderConfirmation } from '../document-flow/signing-order-confirmation'; import { SigningOrderConfirmation } from '../document-flow/signing-order-confirmation';
import type { DocumentFlowStep } from '../document-flow/types'; import type { DocumentFlowStep } from '../document-flow/types';
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
@@ -391,10 +394,13 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
description={documentFlow.description} description={documentFlow.description}
/> />
<DocumentFlowFormContainerContent> <DocumentFlowFormContainerContent>
{isDocumentPdfLoaded && {isDocumentPdfLoaded && (
fields.map((field, index) => ( <DocumentReadOnlyFields
<ShowFieldItem key={index} field={field} recipients={recipients} /> showRecipientColors={true}
))} recipientIds={recipients.map((recipient) => recipient.id)}
fields={mapFieldsWithRecipients(fields, recipients)}
/>
)}
<AnimateGenericFadeInOut motionKey={showAdvancedSettings ? 'Show' : 'Hide'}> <AnimateGenericFadeInOut motionKey={showAdvancedSettings ? 'Show' : 'Hide'}>
<Form {...form}> <Form {...form}>
@@ -500,6 +506,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
ref={provided.innerRef} ref={provided.innerRef}
className="flex w-full flex-col gap-y-2" className="flex w-full flex-col gap-y-2"
> >
{/* todo */}
{signers.map((signer, index) => ( {signers.map((signer, index) => (
<Draggable <Draggable
key={`${signer.id}-${signer.signingOrder}`} key={`${signer.id}-${signer.signingOrder}`}

View File

@@ -50,6 +50,10 @@ import {
} from '@documenso/ui/primitives/form/form'; } from '@documenso/ui/primitives/form/form';
import { DocumentEmailCheckboxes } from '../../components/document/document-email-checkboxes'; import { DocumentEmailCheckboxes } from '../../components/document/document-email-checkboxes';
import {
DocumentReadOnlyFields,
mapFieldsWithRecipients,
} from '../../components/document/document-read-only-fields';
import { DocumentSignatureSettingsTooltip } from '../../components/document/document-signature-settings-tooltip'; import { DocumentSignatureSettingsTooltip } from '../../components/document/document-signature-settings-tooltip';
import { Combobox } from '../combobox'; import { Combobox } from '../combobox';
import { import {
@@ -59,7 +63,6 @@ import {
DocumentFlowFormContainerHeader, DocumentFlowFormContainerHeader,
DocumentFlowFormContainerStep, DocumentFlowFormContainerStep,
} from '../document-flow/document-flow-root'; } from '../document-flow/document-flow-root';
import { ShowFieldItem } from '../document-flow/show-field-item';
import type { DocumentFlowStep } from '../document-flow/types'; import type { DocumentFlowStep } from '../document-flow/types';
import { Input } from '../input'; import { Input } from '../input';
import { MultiSelectCombobox } from '../multi-select-combobox'; import { MultiSelectCombobox } from '../multi-select-combobox';
@@ -153,10 +156,13 @@ export const AddTemplateSettingsFormPartial = ({
/> />
<DocumentFlowFormContainerContent> <DocumentFlowFormContainerContent>
{isDocumentPdfLoaded && {isDocumentPdfLoaded && (
fields.map((field, index) => ( <DocumentReadOnlyFields
<ShowFieldItem key={index} field={field} recipients={recipients} /> showRecipientColors={true}
))} recipientIds={recipients.map((recipient) => recipient.id)}
fields={mapFieldsWithRecipients(fields, recipients)}
/>
)}
<Form {...form}> <Form {...form}>
<fieldset <fieldset

View File

@@ -3,7 +3,8 @@
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
:root { :root,
.dark-mode-disabled {
--background: 0 0% 100%; --background: 0 0% 100%;
--foreground: 222.2 47.4% 11.2%; --foreground: 222.2 47.4% 11.2%;
@@ -48,12 +49,12 @@
--gold: 47.9 95.8% 53.1%; --gold: 47.9 95.8% 53.1%;
--signer-green: 100 48% 55%; --recipient-green: 100 48% 55%;
--signer-blue: 212 56% 50%; --recipient-blue: 212 56% 50%;
--signer-purple: 266 100% 64%; --recipient-purple: 266 100% 64%;
--signer-orange: 36 92% 54%; --recipient-orange: 36 92% 54%;
--signer-yellow: 51 100% 43%; --recipient-yellow: 51 100% 43%;
--signer-pink: 313 65% 57%; --recipient-pink: 313 65% 57%;
/* Base - Neutral */ /* Base - Neutral */
--new-neutral-50: 0, 0%, 96%; --new-neutral-50: 0, 0%, 96%;
@@ -190,7 +191,9 @@
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
font-feature-settings: 'rlig' 1, 'calt' 1; font-feature-settings:
'rlig' 1,
'calt' 1;
} }
} }
@@ -210,8 +213,12 @@
} }
.gradient-border-mask::before { .gradient-border-mask::before {
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); mask:
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: exclude; mask-composite: exclude;
-webkit-mask-composite: xor; -webkit-mask-composite: xor;
} }