feat: add initials field type (#1279)

Adds a new field type that enables document recipients to add
their `initials` on the document.
This commit is contained in:
Catalin Pit
2024-08-12 15:29:32 +02:00
committed by GitHub
parent ef3ecc33f1
commit 29910ab2a7
16 changed files with 261 additions and 494 deletions

View File

@ -41,6 +41,7 @@ import { CheckboxField } from '~/app/(signing)/sign/[token]/checkbox-field';
import { DateField } from '~/app/(signing)/sign/[token]/date-field';
import { DropdownField } from '~/app/(signing)/sign/[token]/dropdown-field';
import { EmailField } from '~/app/(signing)/sign/[token]/email-field';
import { InitialsField } from '~/app/(signing)/sign/[token]/initials-field';
import { NameField } from '~/app/(signing)/sign/[token]/name-field';
import { NumberField } from '~/app/(signing)/sign/[token]/number-field';
import { useRequiredSigningContext } from '~/app/(signing)/sign/[token]/provider';
@ -182,6 +183,15 @@ export const SignDirectTemplateForm = ({
onUnsignField={onUnsignField}
/>
))
.with(FieldType.INITIALS, () => (
<InitialsField
key={field.id}
field={field}
recipient={directRecipient}
onSignField={onSignField}
onUnsignField={onUnsignField}
/>
))
.with(FieldType.NAME, () => (
<NameField
key={field.id}

View File

@ -0,0 +1,140 @@
'use client';
import { useTransition } from 'react';
import { useRouter } from 'next/navigation';
import { Loader } from 'lucide-react';
import { DO_NOT_INVALIDATE_QUERY_ON_MUTATION } from '@documenso/lib/constants/trpc';
import { AppError, AppErrorCode } from '@documenso/lib/errors/app-error';
import type { TRecipientActionAuth } from '@documenso/lib/types/document-auth';
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
import type { Recipient } from '@documenso/prisma/client';
import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
import { trpc } from '@documenso/trpc/react';
import type {
TRemovedSignedFieldWithTokenMutationSchema,
TSignFieldWithTokenMutationSchema,
} from '@documenso/trpc/server/field-router/schema';
import { useToast } from '@documenso/ui/primitives/use-toast';
import { useRequiredSigningContext } from './provider';
import { SigningFieldContainer } from './signing-field-container';
export type InitialsFieldProps = {
field: FieldWithSignature;
recipient: Recipient;
onSignField?: (value: TSignFieldWithTokenMutationSchema) => Promise<void> | void;
onUnsignField?: (value: TRemovedSignedFieldWithTokenMutationSchema) => Promise<void> | void;
};
export const InitialsField = ({
field,
recipient,
onSignField,
onUnsignField,
}: InitialsFieldProps) => {
const router = useRouter();
const { toast } = useToast();
const { fullName } = useRequiredSigningContext();
const initials = extractInitials(fullName);
const [isPending, startTransition] = useTransition();
const { mutateAsync: signFieldWithToken, isLoading: isSignFieldWithTokenLoading } =
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const {
mutateAsync: removeSignedFieldWithToken,
isLoading: isRemoveSignedFieldWithTokenLoading,
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
const onSign = async (authOptions?: TRecipientActionAuth) => {
try {
const value = initials ?? '';
const payload: TSignFieldWithTokenMutationSchema = {
token: recipient.token,
fieldId: field.id,
value,
isBase64: false,
authOptions,
};
if (onSignField) {
await onSignField(payload);
return;
}
await signFieldWithToken(payload);
startTransition(() => router.refresh());
} catch (err) {
const error = AppError.parseError(err);
if (error.code === AppErrorCode.UNAUTHORIZED) {
throw error;
}
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while signing the document.',
variant: 'destructive',
});
}
};
const onRemove = async () => {
try {
const payload: TRemovedSignedFieldWithTokenMutationSchema = {
token: recipient.token,
fieldId: field.id,
};
if (onUnsignField) {
await onUnsignField(payload);
return;
}
await removeSignedFieldWithToken(payload);
startTransition(() => router.refresh());
} catch (err) {
console.error(err);
toast({
title: 'Error',
description: 'An error occurred while removing the signature.',
variant: 'destructive',
});
}
};
return (
<SigningFieldContainer field={field} onSign={onSign} onRemove={onRemove} type="Initials">
{isLoading && (
<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 && (
<p className="group-hover:text-primary text-muted-foreground duration-200 group-hover:text-yellow-300">
Initials
</p>
)}
{field.inserted && (
<p className="text-muted-foreground dark:text-background/80 truncate duration-200">
{field.customText}
</p>
)}
</SigningFieldContainer>
);
};

View File

@ -39,7 +39,16 @@ export type SignatureFieldProps = {
*/
onSign?: (documentAuthValue?: TRecipientActionAuth) => Promise<void> | void;
onRemove?: (fieldType?: string) => Promise<void> | void;
type?: 'Date' | 'Email' | 'Name' | 'Signature' | 'Radio' | 'Dropdown' | 'Number' | 'Checkbox';
type?:
| 'Date'
| 'Initials'
| 'Email'
| 'Name'
| 'Signature'
| 'Radio'
| 'Dropdown'
| 'Number'
| 'Checkbox';
tooltipText?: string | null;
};

View File

@ -26,6 +26,7 @@ import { DateField } from './date-field';
import { DropdownField } from './dropdown-field';
import { EmailField } from './email-field';
import { SigningForm } from './form';
import { InitialsField } from './initials-field';
import { NameField } from './name-field';
import { NumberField } from './number-field';
import { RadioField } from './radio-field';
@ -101,6 +102,9 @@ export const SigningPageView = ({
.with(FieldType.SIGNATURE, () => (
<SignatureField key={field.id} field={field} recipient={recipient} />
))
.with(FieldType.INITIALS, () => (
<InitialsField key={field.id} field={field} recipient={recipient} />
))
.with(FieldType.NAME, () => (
<NameField key={field.id} field={field} recipient={recipient} />
))

View File

@ -98,6 +98,7 @@ export const DocumentReadOnlyFields = ({ documentMeta, fields }: DocumentReadOnl
{
type: P.union(
FieldType.NAME,
FieldType.INITIALS,
FieldType.EMAIL,
FieldType.NUMBER,
FieldType.RADIO,

View File

@ -231,10 +231,17 @@ export const signFieldWithToken = async ({
type,
data: signatureImageAsBase64 || typedSignature || '',
}))
.with(FieldType.DATE, FieldType.EMAIL, FieldType.NAME, FieldType.TEXT, (type) => ({
type,
data: updatedField.customText,
}))
.with(
FieldType.DATE,
FieldType.EMAIL,
FieldType.NAME,
FieldType.TEXT,
FieldType.INITIALS,
(type) => ({
type,
data: updatedField.customText,
}),
)
.with(
FieldType.NUMBER,
FieldType.RADIO,

View File

@ -468,6 +468,7 @@ export const createDocumentFromDirectTemplate = async ({
.with(
FieldType.DATE,
FieldType.EMAIL,
FieldType.INITIALS,
FieldType.NAME,
FieldType.TEXT,
FieldType.NUMBER,

View File

@ -233,6 +233,10 @@ export const ZDocumentAuditLogEventDocumentFieldInsertedSchema = z.object({
// Organised into union to allow us to extend each field if required.
field: z.union([
z.object({
type: z.literal(FieldType.INITIALS),
data: z.string(),
}),
z.object({
type: z.literal(FieldType.EMAIL),
data: z.string(),

View File

@ -1,481 +0,0 @@
import type { ColumnType } from 'kysely';
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
? ColumnType<S, I | undefined, U>
: ColumnType<T, T | undefined, T>;
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
export const IdentityProvider = {
DOCUMENSO: 'DOCUMENSO',
GOOGLE: 'GOOGLE',
OIDC: 'OIDC',
} as const;
export type IdentityProvider = (typeof IdentityProvider)[keyof typeof IdentityProvider];
export const Role = {
ADMIN: 'ADMIN',
USER: 'USER',
} as const;
export type Role = (typeof Role)[keyof typeof Role];
export const UserSecurityAuditLogType = {
ACCOUNT_PROFILE_UPDATE: 'ACCOUNT_PROFILE_UPDATE',
ACCOUNT_SSO_LINK: 'ACCOUNT_SSO_LINK',
AUTH_2FA_DISABLE: 'AUTH_2FA_DISABLE',
AUTH_2FA_ENABLE: 'AUTH_2FA_ENABLE',
PASSKEY_CREATED: 'PASSKEY_CREATED',
PASSKEY_DELETED: 'PASSKEY_DELETED',
PASSKEY_UPDATED: 'PASSKEY_UPDATED',
PASSWORD_RESET: 'PASSWORD_RESET',
PASSWORD_UPDATE: 'PASSWORD_UPDATE',
SIGN_OUT: 'SIGN_OUT',
SIGN_IN: 'SIGN_IN',
SIGN_IN_FAIL: 'SIGN_IN_FAIL',
SIGN_IN_2FA_FAIL: 'SIGN_IN_2FA_FAIL',
SIGN_IN_PASSKEY_FAIL: 'SIGN_IN_PASSKEY_FAIL',
} as const;
export type UserSecurityAuditLogType =
(typeof UserSecurityAuditLogType)[keyof typeof UserSecurityAuditLogType];
export const WebhookTriggerEvents = {
DOCUMENT_CREATED: 'DOCUMENT_CREATED',
DOCUMENT_SENT: 'DOCUMENT_SENT',
DOCUMENT_OPENED: 'DOCUMENT_OPENED',
DOCUMENT_SIGNED: 'DOCUMENT_SIGNED',
DOCUMENT_COMPLETED: 'DOCUMENT_COMPLETED',
} as const;
export type WebhookTriggerEvents = (typeof WebhookTriggerEvents)[keyof typeof WebhookTriggerEvents];
export const WebhookCallStatus = {
SUCCESS: 'SUCCESS',
FAILED: 'FAILED',
} as const;
export type WebhookCallStatus = (typeof WebhookCallStatus)[keyof typeof WebhookCallStatus];
export const ApiTokenAlgorithm = {
SHA512: 'SHA512',
} as const;
export type ApiTokenAlgorithm = (typeof ApiTokenAlgorithm)[keyof typeof ApiTokenAlgorithm];
export const SubscriptionStatus = {
ACTIVE: 'ACTIVE',
PAST_DUE: 'PAST_DUE',
INACTIVE: 'INACTIVE',
} as const;
export type SubscriptionStatus = (typeof SubscriptionStatus)[keyof typeof SubscriptionStatus];
export const DocumentStatus = {
DRAFT: 'DRAFT',
PENDING: 'PENDING',
COMPLETED: 'COMPLETED',
} as const;
export type DocumentStatus = (typeof DocumentStatus)[keyof typeof DocumentStatus];
export const DocumentSource = {
DOCUMENT: 'DOCUMENT',
TEMPLATE: 'TEMPLATE',
TEMPLATE_DIRECT_LINK: 'TEMPLATE_DIRECT_LINK',
} as const;
export type DocumentSource = (typeof DocumentSource)[keyof typeof DocumentSource];
export const DocumentDataType = {
S3_PATH: 'S3_PATH',
BYTES: 'BYTES',
BYTES_64: 'BYTES_64',
} as const;
export type DocumentDataType = (typeof DocumentDataType)[keyof typeof DocumentDataType];
export const ReadStatus = {
NOT_OPENED: 'NOT_OPENED',
OPENED: 'OPENED',
} as const;
export type ReadStatus = (typeof ReadStatus)[keyof typeof ReadStatus];
export const SendStatus = {
NOT_SENT: 'NOT_SENT',
SENT: 'SENT',
} as const;
export type SendStatus = (typeof SendStatus)[keyof typeof SendStatus];
export const SigningStatus = {
NOT_SIGNED: 'NOT_SIGNED',
SIGNED: 'SIGNED',
} as const;
export type SigningStatus = (typeof SigningStatus)[keyof typeof SigningStatus];
export const RecipientRole = {
CC: 'CC',
SIGNER: 'SIGNER',
VIEWER: 'VIEWER',
APPROVER: 'APPROVER',
} as const;
export type RecipientRole = (typeof RecipientRole)[keyof typeof RecipientRole];
export const FieldType = {
SIGNATURE: 'SIGNATURE',
FREE_SIGNATURE: 'FREE_SIGNATURE',
NAME: 'NAME',
EMAIL: 'EMAIL',
DATE: 'DATE',
TEXT: 'TEXT',
NUMBER: 'NUMBER',
RADIO: 'RADIO',
CHECKBOX: 'CHECKBOX',
DROPDOWN: 'DROPDOWN',
} as const;
export type FieldType = (typeof FieldType)[keyof typeof FieldType];
export const TeamMemberRole = {
ADMIN: 'ADMIN',
MANAGER: 'MANAGER',
MEMBER: 'MEMBER',
} as const;
export type TeamMemberRole = (typeof TeamMemberRole)[keyof typeof TeamMemberRole];
export const TeamMemberInviteStatus = {
ACCEPTED: 'ACCEPTED',
PENDING: 'PENDING',
} as const;
export type TeamMemberInviteStatus =
(typeof TeamMemberInviteStatus)[keyof typeof TeamMemberInviteStatus];
export const TemplateType = {
PUBLIC: 'PUBLIC',
PRIVATE: 'PRIVATE',
} as const;
export type TemplateType = (typeof TemplateType)[keyof typeof TemplateType];
export type Account = {
id: string;
userId: number;
type: string;
provider: string;
providerAccountId: string;
refresh_token: string | null;
access_token: string | null;
expires_at: number | null;
created_at: number | null;
ext_expires_in: number | null;
token_type: string | null;
scope: string | null;
id_token: string | null;
session_state: string | null;
};
export type AnonymousVerificationToken = {
id: string;
token: string;
expiresAt: Timestamp;
createdAt: Generated<Timestamp>;
};
export type ApiToken = {
id: Generated<number>;
name: string;
token: string;
algorithm: Generated<ApiTokenAlgorithm>;
expires: Timestamp | null;
createdAt: Generated<Timestamp>;
userId: number | null;
teamId: number | null;
};
export type Document = {
id: Generated<number>;
userId: number;
authOptions: unknown | null;
formValues: unknown | null;
title: string;
status: Generated<DocumentStatus>;
documentDataId: string;
createdAt: Generated<Timestamp>;
updatedAt: Generated<Timestamp>;
completedAt: Timestamp | null;
deletedAt: Timestamp | null;
teamId: number | null;
templateId: number | null;
source: DocumentSource;
};
export type DocumentAuditLog = {
id: string;
documentId: number;
createdAt: Generated<Timestamp>;
type: string;
data: unknown;
name: string | null;
email: string | null;
userId: number | null;
userAgent: string | null;
ipAddress: string | null;
};
export type DocumentData = {
id: string;
type: DocumentDataType;
data: string;
initialData: string;
};
export type DocumentMeta = {
id: string;
subject: string | null;
message: string | null;
timezone: Generated<string | null>;
password: string | null;
dateFormat: Generated<string | null>;
documentId: number;
redirectUrl: string | null;
};
export type DocumentShareLink = {
id: Generated<number>;
email: string;
slug: string;
documentId: number;
createdAt: Generated<Timestamp>;
updatedAt: Timestamp;
};
export type Field = {
id: Generated<number>;
secondaryId: string;
documentId: number | null;
templateId: number | null;
recipientId: number;
type: FieldType;
page: number;
positionX: Generated<string>;
positionY: Generated<string>;
width: Generated<string>;
height: Generated<string>;
customText: string;
inserted: boolean;
fieldMeta: unknown | null;
};
export type Passkey = {
id: string;
userId: number;
name: string;
createdAt: Generated<Timestamp>;
updatedAt: Generated<Timestamp>;
lastUsedAt: Timestamp | null;
credentialId: Buffer;
credentialPublicKey: Buffer;
counter: string;
credentialDeviceType: string;
credentialBackedUp: boolean;
transports: string[];
};
export type PasswordResetToken = {
id: Generated<number>;
token: string;
createdAt: Generated<Timestamp>;
expiry: Timestamp;
userId: number;
};
export type Recipient = {
id: Generated<number>;
documentId: number | null;
templateId: number | null;
email: string;
name: Generated<string>;
token: string;
documentDeletedAt: Timestamp | null;
expired: Timestamp | null;
signedAt: Timestamp | null;
authOptions: unknown | null;
role: Generated<RecipientRole>;
readStatus: Generated<ReadStatus>;
signingStatus: Generated<SigningStatus>;
sendStatus: Generated<SendStatus>;
};
export type Session = {
id: string;
sessionToken: string;
userId: number;
expires: Timestamp;
};
export type Signature = {
id: Generated<number>;
created: Generated<Timestamp>;
recipientId: number;
fieldId: number;
signatureImageAsBase64: string | null;
typedSignature: string | null;
};
export type SiteSettings = {
id: string;
enabled: Generated<boolean>;
data: unknown;
lastModifiedByUserId: number | null;
lastModifiedAt: Generated<Timestamp>;
};
export type Subscription = {
id: Generated<number>;
status: Generated<SubscriptionStatus>;
planId: string;
priceId: string;
periodEnd: Timestamp | null;
userId: number | null;
teamId: number | null;
createdAt: Generated<Timestamp>;
updatedAt: Timestamp;
cancelAtPeriodEnd: Generated<boolean>;
};
export type Team = {
id: Generated<number>;
name: string;
url: string;
createdAt: Generated<Timestamp>;
customerId: string | null;
ownerUserId: number;
};
export type TeamEmail = {
teamId: number;
createdAt: Generated<Timestamp>;
name: string;
email: string;
};
export type TeamEmailVerification = {
teamId: number;
name: string;
email: string;
token: string;
expiresAt: Timestamp;
createdAt: Generated<Timestamp>;
};
export type TeamMember = {
id: Generated<number>;
teamId: number;
createdAt: Generated<Timestamp>;
role: TeamMemberRole;
userId: number;
};
export type TeamMemberInvite = {
id: Generated<number>;
teamId: number;
createdAt: Generated<Timestamp>;
email: string;
status: Generated<TeamMemberInviteStatus>;
role: TeamMemberRole;
token: string;
};
export type TeamPending = {
id: Generated<number>;
name: string;
url: string;
createdAt: Generated<Timestamp>;
customerId: string;
ownerUserId: number;
};
export type TeamTransferVerification = {
teamId: number;
userId: number;
name: string;
email: string;
token: string;
expiresAt: Timestamp;
createdAt: Generated<Timestamp>;
clearPaymentMethods: Generated<boolean>;
};
export type Template = {
id: Generated<number>;
type: Generated<TemplateType>;
title: string;
userId: number;
teamId: number | null;
authOptions: unknown | null;
templateDocumentDataId: string;
createdAt: Generated<Timestamp>;
updatedAt: Generated<Timestamp>;
};
export type TemplateDirectLink = {
id: string;
templateId: number;
token: string;
createdAt: Generated<Timestamp>;
enabled: boolean;
directTemplateRecipientId: number;
};
export type TemplateMeta = {
id: string;
subject: string | null;
message: string | null;
timezone: Generated<string | null>;
password: string | null;
dateFormat: Generated<string | null>;
templateId: number;
redirectUrl: string | null;
};
export type User = {
id: Generated<number>;
name: string | null;
customerId: string | null;
email: string;
emailVerified: Timestamp | null;
password: string | null;
source: string | null;
signature: string | null;
createdAt: Generated<Timestamp>;
updatedAt: Generated<Timestamp>;
lastSignedIn: Generated<Timestamp>;
roles: Generated<Role[]>;
identityProvider: Generated<IdentityProvider>;
twoFactorSecret: string | null;
twoFactorEnabled: Generated<boolean>;
twoFactorBackupCodes: string | null;
url: string | null;
};
export type UserProfile = {
id: number;
bio: string | null;
};
export type UserSecurityAuditLog = {
id: Generated<number>;
userId: number;
createdAt: Generated<Timestamp>;
type: UserSecurityAuditLogType;
userAgent: string | null;
ipAddress: string | null;
};
export type VerificationToken = {
id: Generated<number>;
secondaryId: string;
identifier: string;
token: string;
expires: Timestamp;
createdAt: Generated<Timestamp>;
userId: number;
};
export type Webhook = {
id: string;
webhookUrl: string;
eventTriggers: WebhookTriggerEvents[];
secret: string | null;
enabled: Generated<boolean>;
createdAt: Generated<Timestamp>;
updatedAt: Generated<Timestamp>;
userId: number;
teamId: number | null;
};
export type WebhookCall = {
id: string;
status: WebhookCallStatus;
url: string;
event: WebhookTriggerEvents;
requestBody: unknown;
responseCode: number;
responseHeaders: unknown | null;
responseBody: unknown | null;
createdAt: Generated<Timestamp>;
webhookId: string;
};
export type DB = {
Account: Account;
AnonymousVerificationToken: AnonymousVerificationToken;
ApiToken: ApiToken;
Document: Document;
DocumentAuditLog: DocumentAuditLog;
DocumentData: DocumentData;
DocumentMeta: DocumentMeta;
DocumentShareLink: DocumentShareLink;
Field: Field;
Passkey: Passkey;
PasswordResetToken: PasswordResetToken;
Recipient: Recipient;
Session: Session;
Signature: Signature;
SiteSettings: SiteSettings;
Subscription: Subscription;
Team: Team;
TeamEmail: TeamEmail;
TeamEmailVerification: TeamEmailVerification;
TeamMember: TeamMember;
TeamMemberInvite: TeamMemberInvite;
TeamPending: TeamPending;
TeamTransferVerification: TeamTransferVerification;
Template: Template;
TemplateDirectLink: TemplateDirectLink;
TemplateMeta: TemplateMeta;
User: User;
UserProfile: UserProfile;
UserSecurityAuditLog: UserSecurityAuditLog;
VerificationToken: VerificationToken;
Webhook: Webhook;
WebhookCall: WebhookCall;
};

View File

@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "FieldType" ADD VALUE 'INITIALS';

View File

@ -409,6 +409,7 @@ model Recipient {
enum FieldType {
SIGNATURE
FREE_SIGNATURE
INITIALS
NAME
EMAIL
DATE

View File

@ -414,7 +414,7 @@ export const documentRouter = router({
teamId,
}).catch(() => null);
if (!document || document.teamId !== teamId) {
if (!document || (teamId && document.teamId !== teamId)) {
throw new TRPCError({
code: 'FORBIDDEN',
message: 'You do not have access to this document.',

View File

@ -10,6 +10,7 @@ import {
CheckSquare,
ChevronDown,
ChevronsUpDown,
Contact,
Disc,
Hash,
Info,
@ -650,6 +651,32 @@ export const AddFieldsFormPartial = ({
</Card>
</button>
<button
type="button"
className="group h-full w-full"
onClick={() => setSelectedField(FieldType.INITIALS)}
onMouseDown={() => setSelectedField(FieldType.INITIALS)}
data-selected={selectedField === FieldType.INITIALS ? true : undefined}
>
<Card
className={cn(
'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">
<p
className={cn(
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
)}
>
<Contact className="h-4 w-4" />
Initials
</p>
</CardContent>
</Card>
</button>
<button
type="button"
className="group h-full w-full"
@ -663,7 +690,7 @@ export const AddFieldsFormPartial = ({
// selectedSignerStyles.borderClass,
)}
>
<CardContent className="p-4">
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
<p
className={cn(
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',

View File

@ -1,4 +1,14 @@
import { CalendarDays, CheckSquare, ChevronDown, Disc, Hash, Mail, Type, User } from 'lucide-react';
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 { FieldType } from '@documenso/prisma/client';
@ -13,6 +23,7 @@ type FieldIconProps = {
};
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' },
@ -46,9 +57,11 @@ export const FieldIcon = ({
if (fieldMeta && (type === 'TEXT' || type === 'NUMBER')) {
if (type === 'TEXT' && 'text' in fieldMeta && fieldMeta.text && !fieldMeta.label) {
label = fieldMeta.text.length > 10 ? fieldMeta.text.substring(0, 10) + '...' : fieldMeta.text;
label =
fieldMeta.text.length > 10 ? fieldMeta.text.substring(0, 10) + '...' : fieldMeta.text;
} else if (fieldMeta.label) {
label = fieldMeta.label.length > 10 ? fieldMeta.label.substring(0, 10) + '...' : fieldMeta.label;
label =
fieldMeta.label.length > 10 ? fieldMeta.label.substring(0, 10) + '...' : fieldMeta.label;
} else {
label = fieldIcons[type]?.label;
}
@ -58,7 +71,7 @@ export const FieldIcon = ({
return (
<div className="text-field-card-foreground flex items-center justify-center gap-x-1.5 text-sm">
<Icon className='h-4 w-4' /> {label}
<Icon className="h-4 w-4" /> {label}
</div>
);
}

View File

@ -46,6 +46,7 @@ export type TDocumentFlowFormSchema = z.infer<typeof ZDocumentFlowFormSchema>;
export const FRIENDLY_FIELD_TYPE: Record<FieldType, string> = {
[FieldType.SIGNATURE]: 'Signature',
[FieldType.FREE_SIGNATURE]: 'Free Signature',
[FieldType.INITIALS]: 'Initials',
[FieldType.TEXT]: 'Text',
[FieldType.DATE]: 'Date',
[FieldType.EMAIL]: 'Email',

View File

@ -9,6 +9,7 @@ import {
CheckSquare,
ChevronDown,
ChevronsUpDown,
Contact,
Disc,
Hash,
Mail,
@ -383,10 +384,11 @@ export const AddTemplateFieldsFormPartial = ({
{selectedField && (
<div
className={cn(
'pointer-events-none fixed z-50 flex cursor-pointer flex-col items-center justify-center bg-white transition duration-200',
'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',
selectedSignerStyles.default.base,
{
'-rotate-6 scale-90 opacity-50': !isFieldWithinBounds,
'-rotate-6 scale-90 opacity-50 dark:bg-black/20': !isFieldWithinBounds,
'dark:text-black/60': isFieldWithinBounds,
},
)}
style={{
@ -546,6 +548,32 @@ export const AddTemplateFieldsFormPartial = ({
</Card>
</button>
<button
type="button"
className="group h-full w-full"
onClick={() => setSelectedField(FieldType.INITIALS)}
onMouseDown={() => setSelectedField(FieldType.INITIALS)}
data-selected={selectedField === FieldType.INITIALS ? true : undefined}
>
<Card
className={cn(
'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">
<p
className={cn(
'text-muted-foreground group-data-[selected]:text-foreground flex items-center justify-center gap-x-1.5 text-sm font-normal',
)}
>
<Contact className="h-4 w-4" />
Initials
</p>
</CardContent>
</Card>
</button>
<button
type="button"
className="group h-full w-full"