This commit is contained in:
David Nguyen
2025-03-03 21:35:12 +11:00
parent 25bb6ffe77
commit 172a5be737
32 changed files with 742 additions and 843 deletions

View File

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

View File

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

View File

@ -254,19 +254,19 @@ export const DocumentSigningCheckboxField = ({
{validationSign?.label} {checkboxValidationLength}
</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) => {
const itemValue = item.value || `empty-value-${item.id}`;
return (
<div key={index} className="flex items-center gap-x-1.5">
<Checkbox
className="h-4 w-4"
className="h-3 w-3"
id={`checkbox-${index}`}
checked={checkedValues.includes(itemValue)}
onCheckedChange={() => handleCheckboxChange(item.value, item.id)}
/>
<Label htmlFor={`checkbox-${index}`}>
<Label htmlFor={`checkbox-${index}`} className="text-xs font-normal">
{item.value.includes('empty-value-') ? '' : item.value}
</Label>
</div>
@ -277,7 +277,7 @@ export const DocumentSigningCheckboxField = ({
)}
{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) => {
const itemValue = item.value || `empty-value-${item.id}`;
@ -290,7 +290,7 @@ export const DocumentSigningCheckboxField = ({
disabled={isLoading}
onCheckedChange={() => void handleCheckboxOptionClick(item)}
/>
<Label htmlFor={`checkbox-${index}`} className="text-xs">
<Label htmlFor={`checkbox-${index}`} className="text-xs font-normal">
{item.value.includes('empty-value-') ? '' : item.value}
</Label>
</div>

View File

@ -151,12 +151,10 @@ export const DocumentSigningDateField = ({
<div className="flex h-full w-full items-center">
<p
className={cn(
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
'text-muted-foreground dark:text-background/80 w-full text-left 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',
'!text-center': parsedFieldMeta?.textAlign === 'center',
'!text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>

View File

@ -136,12 +136,10 @@ export const DocumentSigningEmailField = ({
<div className="flex h-full w-full items-center">
<p
className={cn(
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
'text-muted-foreground dark:text-background/80 w-full text-left 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',
'!text-center': parsedFieldMeta?.textAlign === 'center',
'!text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>

View File

@ -182,12 +182,10 @@ export const DocumentSigningNameField = ({
<div className="flex h-full w-full items-center">
<p
className={cn(
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
'text-muted-foreground dark:text-background/80 w-full text-left 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',
'!text-center': parsedFieldMeta?.textAlign === 'center',
'!text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>

View File

@ -272,12 +272,10 @@ export const DocumentSigningNumberField = ({
<div className="flex h-full w-full items-center">
<p
className={cn(
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
'text-muted-foreground dark:text-background/80 w-full text-left 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',
'!text-center': parsedFieldMeta?.textAlign === 'center',
'!text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>

View File

@ -19,6 +19,7 @@ import {
import type { CompletedField } from '@documenso/lib/types/fields';
import type { FieldWithSignatureAndFieldMeta } from '@documenso/prisma/types/field-with-signature-and-fieldmeta';
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 { ElementVisible } from '@documenso/ui/primitives/element-visible';
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 { DocumentSigningSignatureField } from '~/components/general/document-signing/document-signing-signature-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';

View File

@ -157,17 +157,20 @@ export const DocumentSigningRadioField = ({
)}
{!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) => (
<div key={index} className="flex items-center gap-x-1.5">
<RadioGroupItem
className="h-4 w-4 shrink-0"
className="h-3 w-3 shrink-0"
value={item.value}
id={`option-${index}`}
checked={item.checked}
/>
<Label htmlFor={`option-${index}`}>
<Label htmlFor={`option-${index}`} className="text-xs font-normal">
{item.value.includes('empty-value-') ? '' : item.value}
</Label>
</div>
@ -176,7 +179,7 @@ export const DocumentSigningRadioField = ({
)}
{field.inserted && (
<RadioGroup className="gap-y-1">
<RadioGroup className="my-0.5 gap-y-1">
{values?.map((item, index) => (
<div key={index} className="flex items-center gap-x-1.5">
<RadioGroupItem
@ -185,7 +188,7 @@ export const DocumentSigningRadioField = ({
id={`option-${index}`}
checked={item.value === field.customText}
/>
<Label htmlFor={`option-${index}`} className="text-xs">
<Label htmlFor={`option-${index}`} className="text-xs font-normal">
{item.value.includes('empty-value-') ? '' : item.value}
</Label>
</div>

View File

@ -277,12 +277,11 @@ export const DocumentSigningTextField = ({
<div className="flex h-full w-full items-center">
<p
className={cn(
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
'text-muted-foreground dark:text-background/80 w-full text-left 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',
// Todo: Test
'!text-center': parsedFieldMeta?.textAlign === 'center',
'!text-right': parsedFieldMeta?.textAlign === 'right',
},
)}
>
@ -304,11 +303,9 @@ export const DocumentSigningTextField = ({
id="custom-text"
placeholder={parsedFieldMeta?.placeholder ?? _(msg`Enter your text here`)}
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,
'text-left': parsedFieldMeta?.textAlign === 'left',
'text-center':
!parsedFieldMeta?.textAlign || parsedFieldMeta?.textAlign === 'center',
'text-center': parsedFieldMeta?.textAlign === 'center',
'text-right': parsedFieldMeta?.textAlign === 'right',
})}
value={localText}

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>
);
};