mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
feat: add text align option to fields (#1610)
## Description Adds the ability to align text to the left, center or right for relevant fields. Previously text was always centered which can be less desirable. See attached debug document which has left, center and right text alignments set for fields. <img width="614" alt="image" src="https://github.com/user-attachments/assets/361a030e-813d-458b-9c7a-ff4c9fa5e33c" /> ## Related Issue N/A ## Changes Made - Added text align option - Update the insert in pdf method to support different alignments - Added a debug mode to field insertion ## Testing Performed - Ran manual tests using the debug mode
This commit is contained in:
@ -16,6 +16,7 @@ import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'
|
|||||||
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 { ZDateFieldMeta } from '@documenso/lib/types/field-meta';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
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';
|
||||||
@ -23,6 +24,7 @@ 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 { SigningFieldContainer } from './signing-field-container';
|
import { SigningFieldContainer } from './signing-field-container';
|
||||||
@ -59,6 +61,9 @@ export const DateField = ({
|
|||||||
isPending: isRemoveSignedFieldWithTokenLoading,
|
isPending: isRemoveSignedFieldWithTokenLoading,
|
||||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
|
const safeFieldMeta = ZDateFieldMeta.safeParse(field.fieldMeta);
|
||||||
|
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
|
||||||
|
|
||||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
||||||
|
|
||||||
const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone);
|
const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone);
|
||||||
@ -150,9 +155,21 @@ export const DateField = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{field.inserted && (
|
{field.inserted && (
|
||||||
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.425rem,25cqw,0.825rem)] duration-200">
|
<div className="flex h-full w-full items-center">
|
||||||
{localDateString}
|
<p
|
||||||
</p>
|
className={cn(
|
||||||
|
'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',
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{localDateString}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</SigningFieldContainer>
|
</SigningFieldContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { Loader } from 'lucide-react';
|
|||||||
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 { ZEmailFieldMeta } from '@documenso/lib/types/field-meta';
|
||||||
import type { Recipient } from '@documenso/prisma/client';
|
import type { Recipient } from '@documenso/prisma/client';
|
||||||
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';
|
||||||
@ -18,6 +19,7 @@ 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 { useRequiredSigningContext } from './provider';
|
import { useRequiredSigningContext } from './provider';
|
||||||
@ -48,6 +50,9 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
|
|||||||
isPending: isRemoveSignedFieldWithTokenLoading,
|
isPending: isRemoveSignedFieldWithTokenLoading,
|
||||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
|
const safeFieldMeta = ZEmailFieldMeta.safeParse(field.fieldMeta);
|
||||||
|
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
|
||||||
|
|
||||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
||||||
|
|
||||||
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
const onSign = async (authOptions?: TRecipientActionAuth) => {
|
||||||
@ -128,9 +133,21 @@ export const EmailField = ({ field, recipient, onSignField, onUnsignField }: Ema
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{field.inserted && (
|
{field.inserted && (
|
||||||
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.425rem,25cqw,0.825rem)] duration-200">
|
<div className="flex h-full w-full items-center">
|
||||||
{field.customText}
|
<p
|
||||||
</p>
|
className={cn(
|
||||||
|
'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>
|
||||||
)}
|
)}
|
||||||
</SigningFieldContainer>
|
</SigningFieldContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { Loader } from 'lucide-react';
|
|||||||
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 { ZNameFieldMeta } from '@documenso/lib/types/field-meta';
|
||||||
import { type Recipient } from '@documenso/prisma/client';
|
import { type Recipient } from '@documenso/prisma/client';
|
||||||
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';
|
||||||
@ -18,6 +19,7 @@ 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';
|
||||||
@ -56,6 +58,9 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
|
|||||||
isPending: isRemoveSignedFieldWithTokenLoading,
|
isPending: isRemoveSignedFieldWithTokenLoading,
|
||||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
|
const safeFieldMeta = ZNameFieldMeta.safeParse(field.fieldMeta);
|
||||||
|
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
|
||||||
|
|
||||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
||||||
|
|
||||||
const [showFullNameModal, setShowFullNameModal] = useState(false);
|
const [showFullNameModal, setShowFullNameModal] = useState(false);
|
||||||
@ -172,9 +177,21 @@ export const NameField = ({ field, recipient, onSignField, onUnsignField }: Name
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{field.inserted && (
|
{field.inserted && (
|
||||||
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.425rem,25cqw,0.825rem)] duration-200">
|
<div className="flex h-full w-full items-center">
|
||||||
{field.customText}
|
<p
|
||||||
</p>
|
className={cn(
|
||||||
|
'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}>
|
||||||
|
|||||||
@ -52,8 +52,19 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
|||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
const [showRadioModal, setShowRadioModal] = useState(false);
|
const [showRadioModal, setShowRadioModal] = useState(false);
|
||||||
|
|
||||||
const parsedFieldMeta = field.fieldMeta ? ZNumberFieldMeta.parse(field.fieldMeta) : null;
|
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
||||||
const isReadOnly = parsedFieldMeta?.readOnly;
|
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutateAsync: removeSignedFieldWithToken,
|
||||||
|
isPending: isRemoveSignedFieldWithTokenLoading,
|
||||||
|
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
|
const safeFieldMeta = ZNumberFieldMeta.safeParse(field.fieldMeta);
|
||||||
|
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
|
||||||
|
|
||||||
|
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
||||||
|
|
||||||
const defaultValue = parsedFieldMeta?.value;
|
const defaultValue = parsedFieldMeta?.value;
|
||||||
const [localNumber, setLocalNumber] = useState(
|
const [localNumber, setLocalNumber] = useState(
|
||||||
parsedFieldMeta?.value ? String(parsedFieldMeta.value) : '0',
|
parsedFieldMeta?.value ? String(parsedFieldMeta.value) : '0',
|
||||||
@ -71,16 +82,6 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
|||||||
|
|
||||||
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
|
const { executeActionAuthProcedure } = useRequiredDocumentAuthContext();
|
||||||
|
|
||||||
const { mutateAsync: signFieldWithToken, isPending: isSignFieldWithTokenLoading } =
|
|
||||||
trpc.field.signFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
|
||||||
|
|
||||||
const {
|
|
||||||
mutateAsync: removeSignedFieldWithToken,
|
|
||||||
isPending: isRemoveSignedFieldWithTokenLoading,
|
|
||||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
|
||||||
|
|
||||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
|
||||||
|
|
||||||
const handleNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const text = e.target.value;
|
const text = e.target.value;
|
||||||
setLocalNumber(text);
|
setLocalNumber(text);
|
||||||
@ -208,7 +209,7 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
(!field.inserted && defaultValue && localNumber) ||
|
(!field.inserted && defaultValue && localNumber) ||
|
||||||
(!field.inserted && isReadOnly && defaultValue)
|
(!field.inserted && parsedFieldMeta?.readOnly && defaultValue)
|
||||||
) {
|
) {
|
||||||
void executeActionAuthProcedure({
|
void executeActionAuthProcedure({
|
||||||
onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
|
onReauthFormSubmit: async (authOptions) => await onSign(authOptions),
|
||||||
@ -260,9 +261,21 @@ export const NumberField = ({ field, recipient, onSignField, onUnsignField }: Nu
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{field.inserted && (
|
{field.inserted && (
|
||||||
<p className="text-muted-foreground dark:text-background/80 text-[clamp(0.425rem,25cqw,0.825rem)] duration-200">
|
<div className="flex h-full w-full items-center">
|
||||||
{field.customText}
|
<p
|
||||||
</p>
|
className={cn(
|
||||||
|
'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={showRadioModal} onOpenChange={setShowRadioModal}>
|
<Dialog open={showRadioModal} onOpenChange={setShowRadioModal}>
|
||||||
|
|||||||
@ -62,7 +62,8 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
|
|||||||
isPending: isRemoveSignedFieldWithTokenLoading,
|
isPending: isRemoveSignedFieldWithTokenLoading,
|
||||||
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
} = trpc.field.removeSignedFieldWithToken.useMutation(DO_NOT_INVALIDATE_QUERY_ON_MUTATION);
|
||||||
|
|
||||||
const parsedFieldMeta = field.fieldMeta ? ZTextFieldMeta.parse(field.fieldMeta) : null;
|
const safeFieldMeta = ZTextFieldMeta.safeParse(field.fieldMeta);
|
||||||
|
const parsedFieldMeta = safeFieldMeta.success ? safeFieldMeta.data : null;
|
||||||
|
|
||||||
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending;
|
||||||
const shouldAutoSignField =
|
const shouldAutoSignField =
|
||||||
@ -261,11 +262,23 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{field.inserted && (
|
{field.inserted && (
|
||||||
<p className="text-muted-foreground dark:text-background/80 flex items-center justify-center gap-x-1 text-[clamp(0.425rem,25cqw,0.825rem)] duration-200">
|
<div className="flex h-full w-full items-center">
|
||||||
{field.customText.length < 20
|
<p
|
||||||
? field.customText
|
className={cn(
|
||||||
: field.customText.substring(0, 15) + '...'}
|
'text-muted-foreground dark:text-background/80 w-full text-[clamp(0.425rem,25cqw,0.825rem)] duration-200',
|
||||||
</p>
|
{
|
||||||
|
'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, 15) + '...'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Dialog open={showCustomTextModal} onOpenChange={setShowCustomTextModal}>
|
<Dialog open={showCustomTextModal} onOpenChange={setShowCustomTextModal}>
|
||||||
@ -281,6 +294,10 @@ export const TextField = ({ field, recipient, onSignField, onUnsignField }: Text
|
|||||||
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 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 || parsedFieldMeta?.textAlign === 'center',
|
||||||
|
'text-right': parsedFieldMeta?.textAlign === 'right',
|
||||||
})}
|
})}
|
||||||
value={localText}
|
value={localText}
|
||||||
onChange={handleTextChange}
|
onChange={handleTextChange}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
// 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 type { PDFDocument } from 'pdf-lib';
|
import type { PDFDocument } from 'pdf-lib';
|
||||||
import { RotationTypes, degrees, radiansToDegrees } from 'pdf-lib';
|
import { RotationTypes, degrees, radiansToDegrees, rgb } from 'pdf-lib';
|
||||||
import { P, match } from 'ts-pattern';
|
import { P, match } from 'ts-pattern';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -36,6 +36,9 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isSignatureField = isSignatureFieldType(field.type);
|
const isSignatureField = isSignatureFieldType(field.type);
|
||||||
|
const isDebugMode =
|
||||||
|
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
||||||
|
process.env.DEBUG_PDF_INSERT === '1' || process.env.DEBUG_PDF_INSERT === 'true';
|
||||||
|
|
||||||
pdf.registerFontkit(fontkit);
|
pdf.registerFontkit(fontkit);
|
||||||
|
|
||||||
@ -83,6 +86,35 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
|||||||
const fieldX = pageWidth * (Number(field.positionX) / 100);
|
const fieldX = pageWidth * (Number(field.positionX) / 100);
|
||||||
const fieldY = pageHeight * (Number(field.positionY) / 100);
|
const fieldY = pageHeight * (Number(field.positionY) / 100);
|
||||||
|
|
||||||
|
// Draw debug box if debug mode is enabled
|
||||||
|
if (isDebugMode) {
|
||||||
|
let debugX = fieldX;
|
||||||
|
let debugY = pageHeight - fieldY - fieldHeight; // Invert Y for PDF coordinates
|
||||||
|
|
||||||
|
if (pageRotationInDegrees !== 0) {
|
||||||
|
const adjustedPosition = adjustPositionForRotation(
|
||||||
|
pageWidth,
|
||||||
|
pageHeight,
|
||||||
|
debugX,
|
||||||
|
debugY,
|
||||||
|
pageRotationInDegrees,
|
||||||
|
);
|
||||||
|
|
||||||
|
debugX = adjustedPosition.xPos;
|
||||||
|
debugY = adjustedPosition.yPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
page.drawRectangle({
|
||||||
|
x: debugX,
|
||||||
|
y: debugY,
|
||||||
|
width: fieldWidth,
|
||||||
|
height: fieldHeight,
|
||||||
|
borderColor: rgb(1, 0, 0), // Red
|
||||||
|
borderWidth: 1,
|
||||||
|
rotate: degrees(pageRotationInDegrees),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const font = await pdf.embedFont(
|
const font = await pdf.embedFont(
|
||||||
isSignatureField ? fontCaveat : fontNoto,
|
isSignatureField ? fontCaveat : fontNoto,
|
||||||
isSignatureField ? { features: { calt: false } } : undefined,
|
isSignatureField ? { features: { calt: false } } : undefined,
|
||||||
@ -278,6 +310,7 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
|||||||
const meta = Parser ? Parser.safeParse(field.fieldMeta) : null;
|
const meta = Parser ? Parser.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 longestLineInTextForWidth = field.customText
|
const longestLineInTextForWidth = field.customText
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.sort((a, b) => b.length - a.length)[0];
|
.sort((a, b) => b.length - a.length)[0];
|
||||||
@ -293,7 +326,17 @@ export const insertFieldInPDF = async (pdf: PDFDocument, field: FieldWithSignatu
|
|||||||
|
|
||||||
textWidth = font.widthOfTextAtSize(longestLineInTextForWidth, fontSize);
|
textWidth = font.widthOfTextAtSize(longestLineInTextForWidth, fontSize);
|
||||||
|
|
||||||
let textX = fieldX + (fieldWidth - textWidth) / 2;
|
// Add padding similar to web display (roughly 0.5rem equivalent in PDF units)
|
||||||
|
const padding = 8; // PDF points, roughly equivalent to 0.5rem
|
||||||
|
|
||||||
|
// Calculate X position based on text alignment with 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;
|
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
|
||||||
|
|||||||
@ -11,9 +11,14 @@ export const ZBaseFieldMeta = z.object({
|
|||||||
|
|
||||||
export type TBaseFieldMeta = z.infer<typeof ZBaseFieldMeta>;
|
export type TBaseFieldMeta = z.infer<typeof ZBaseFieldMeta>;
|
||||||
|
|
||||||
|
export const ZFieldTextAlignSchema = z.enum(['left', 'center', 'right']);
|
||||||
|
|
||||||
|
export type TFieldTextAlignSchema = z.infer<typeof ZFieldTextAlignSchema>;
|
||||||
|
|
||||||
export const ZInitialsFieldMeta = ZBaseFieldMeta.extend({
|
export const ZInitialsFieldMeta = ZBaseFieldMeta.extend({
|
||||||
type: z.literal('initials'),
|
type: z.literal('initials'),
|
||||||
fontSize: z.number().min(8).max(96).optional(),
|
fontSize: z.number().min(8).max(96).optional(),
|
||||||
|
textAlign: ZFieldTextAlignSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TInitialsFieldMeta = z.infer<typeof ZInitialsFieldMeta>;
|
export type TInitialsFieldMeta = z.infer<typeof ZInitialsFieldMeta>;
|
||||||
@ -21,6 +26,7 @@ export type TInitialsFieldMeta = z.infer<typeof ZInitialsFieldMeta>;
|
|||||||
export const ZNameFieldMeta = ZBaseFieldMeta.extend({
|
export const ZNameFieldMeta = ZBaseFieldMeta.extend({
|
||||||
type: z.literal('name'),
|
type: z.literal('name'),
|
||||||
fontSize: z.number().min(8).max(96).optional(),
|
fontSize: z.number().min(8).max(96).optional(),
|
||||||
|
textAlign: ZFieldTextAlignSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TNameFieldMeta = z.infer<typeof ZNameFieldMeta>;
|
export type TNameFieldMeta = z.infer<typeof ZNameFieldMeta>;
|
||||||
@ -28,6 +34,7 @@ export type TNameFieldMeta = z.infer<typeof ZNameFieldMeta>;
|
|||||||
export const ZEmailFieldMeta = ZBaseFieldMeta.extend({
|
export const ZEmailFieldMeta = ZBaseFieldMeta.extend({
|
||||||
type: z.literal('email'),
|
type: z.literal('email'),
|
||||||
fontSize: z.number().min(8).max(96).optional(),
|
fontSize: z.number().min(8).max(96).optional(),
|
||||||
|
textAlign: ZFieldTextAlignSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TEmailFieldMeta = z.infer<typeof ZEmailFieldMeta>;
|
export type TEmailFieldMeta = z.infer<typeof ZEmailFieldMeta>;
|
||||||
@ -35,6 +42,7 @@ export type TEmailFieldMeta = z.infer<typeof ZEmailFieldMeta>;
|
|||||||
export const ZDateFieldMeta = ZBaseFieldMeta.extend({
|
export const ZDateFieldMeta = ZBaseFieldMeta.extend({
|
||||||
type: z.literal('date'),
|
type: z.literal('date'),
|
||||||
fontSize: z.number().min(8).max(96).optional(),
|
fontSize: z.number().min(8).max(96).optional(),
|
||||||
|
textAlign: ZFieldTextAlignSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TDateFieldMeta = z.infer<typeof ZDateFieldMeta>;
|
export type TDateFieldMeta = z.infer<typeof ZDateFieldMeta>;
|
||||||
@ -44,6 +52,7 @@ export const ZTextFieldMeta = ZBaseFieldMeta.extend({
|
|||||||
text: z.string().optional(),
|
text: z.string().optional(),
|
||||||
characterLimit: z.number().optional(),
|
characterLimit: z.number().optional(),
|
||||||
fontSize: z.number().min(8).max(96).optional(),
|
fontSize: z.number().min(8).max(96).optional(),
|
||||||
|
textAlign: ZFieldTextAlignSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TTextFieldMeta = z.infer<typeof ZTextFieldMeta>;
|
export type TTextFieldMeta = z.infer<typeof ZTextFieldMeta>;
|
||||||
@ -55,6 +64,7 @@ export const ZNumberFieldMeta = ZBaseFieldMeta.extend({
|
|||||||
minValue: z.number().optional(),
|
minValue: z.number().optional(),
|
||||||
maxValue: z.number().optional(),
|
maxValue: z.number().optional(),
|
||||||
fontSize: z.number().min(8).max(96).optional(),
|
fontSize: z.number().min(8).max(96).optional(),
|
||||||
|
textAlign: ZFieldTextAlignSchema.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TNumberFieldMeta = z.infer<typeof ZNumberFieldMeta>;
|
export type TNumberFieldMeta = z.infer<typeof ZNumberFieldMeta>;
|
||||||
|
|||||||
@ -71,21 +71,25 @@ const getDefaultState = (fieldType: FieldType): FieldMeta => {
|
|||||||
return {
|
return {
|
||||||
type: 'initials',
|
type: 'initials',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
textAlign: 'left',
|
||||||
};
|
};
|
||||||
case FieldType.NAME:
|
case FieldType.NAME:
|
||||||
return {
|
return {
|
||||||
type: 'name',
|
type: 'name',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
textAlign: 'left',
|
||||||
};
|
};
|
||||||
case FieldType.EMAIL:
|
case FieldType.EMAIL:
|
||||||
return {
|
return {
|
||||||
type: 'email',
|
type: 'email',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
textAlign: 'left',
|
||||||
};
|
};
|
||||||
case FieldType.DATE:
|
case FieldType.DATE:
|
||||||
return {
|
return {
|
||||||
type: 'date',
|
type: 'date',
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
textAlign: 'left',
|
||||||
};
|
};
|
||||||
case FieldType.TEXT:
|
case FieldType.TEXT:
|
||||||
return {
|
return {
|
||||||
@ -97,6 +101,7 @@ const getDefaultState = (fieldType: FieldType): FieldMeta => {
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
required: false,
|
required: false,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
|
textAlign: 'left',
|
||||||
};
|
};
|
||||||
case FieldType.NUMBER:
|
case FieldType.NUMBER:
|
||||||
return {
|
return {
|
||||||
@ -110,6 +115,7 @@ const getDefaultState = (fieldType: FieldType): FieldMeta => {
|
|||||||
required: false,
|
required: false,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
|
textAlign: 'left',
|
||||||
};
|
};
|
||||||
case FieldType.RADIO:
|
case FieldType.RADIO:
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -5,6 +5,13 @@ import { validateFields as validateDateFields } from '@documenso/lib/advanced-fi
|
|||||||
import { type TDateFieldMeta as DateFieldMeta } from '@documenso/lib/types/field-meta';
|
import { type TDateFieldMeta as DateFieldMeta } from '@documenso/lib/types/field-meta';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@documenso/ui/primitives/select';
|
||||||
|
|
||||||
type DateFieldAdvancedSettingsProps = {
|
type DateFieldAdvancedSettingsProps = {
|
||||||
fieldState: DateFieldMeta;
|
fieldState: DateFieldMeta;
|
||||||
@ -66,6 +73,27 @@ export const DateFieldAdvancedSettings = ({
|
|||||||
max={96}
|
max={96}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>
|
||||||
|
<Trans>Text Align</Trans>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={fieldState.textAlign}
|
||||||
|
onValueChange={(value) => handleInput('textAlign', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-background mt-2">
|
||||||
|
<SelectValue placeholder="Select text align" />
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="left">Left</SelectItem>
|
||||||
|
<SelectItem value="center">Center</SelectItem>
|
||||||
|
<SelectItem value="right">Right</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,6 +5,13 @@ import { validateFields as validateEmailFields } from '@documenso/lib/advanced-f
|
|||||||
import { type TEmailFieldMeta as EmailFieldMeta } from '@documenso/lib/types/field-meta';
|
import { type TEmailFieldMeta as EmailFieldMeta } from '@documenso/lib/types/field-meta';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@documenso/ui/primitives/select';
|
||||||
|
|
||||||
type EmailFieldAdvancedSettingsProps = {
|
type EmailFieldAdvancedSettingsProps = {
|
||||||
fieldState: EmailFieldMeta;
|
fieldState: EmailFieldMeta;
|
||||||
@ -48,6 +55,27 @@ export const EmailFieldAdvancedSettings = ({
|
|||||||
max={96}
|
max={96}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>
|
||||||
|
<Trans>Text Align</Trans>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={fieldState.textAlign}
|
||||||
|
onValueChange={(value) => handleInput('textAlign', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-background mt-2">
|
||||||
|
<SelectValue placeholder="Select text align" />
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="left">Left</SelectItem>
|
||||||
|
<SelectItem value="center">Center</SelectItem>
|
||||||
|
<SelectItem value="right">Right</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import { type TInitialsFieldMeta as InitialsFieldMeta } from '@documenso/lib/typ
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
|
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../select';
|
||||||
|
|
||||||
type InitialsFieldAdvancedSettingsProps = {
|
type InitialsFieldAdvancedSettingsProps = {
|
||||||
fieldState: InitialsFieldMeta;
|
fieldState: InitialsFieldMeta;
|
||||||
handleFieldChange: (key: keyof InitialsFieldMeta, value: string | boolean) => void;
|
handleFieldChange: (key: keyof InitialsFieldMeta, value: string | boolean) => void;
|
||||||
@ -48,6 +50,27 @@ export const InitialsFieldAdvancedSettings = ({
|
|||||||
max={96}
|
max={96}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>
|
||||||
|
<Trans>Text Align</Trans>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={fieldState.textAlign}
|
||||||
|
onValueChange={(value) => handleInput('textAlign', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-background mt-2">
|
||||||
|
<SelectValue placeholder="Select text align" />
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="left">Left</SelectItem>
|
||||||
|
<SelectItem value="center">Center</SelectItem>
|
||||||
|
<SelectItem value="right">Right</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -5,6 +5,13 @@ import { validateFields as validateNameFields } from '@documenso/lib/advanced-fi
|
|||||||
import { type TNameFieldMeta as NameFieldMeta } from '@documenso/lib/types/field-meta';
|
import { type TNameFieldMeta as NameFieldMeta } from '@documenso/lib/types/field-meta';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@documenso/ui/primitives/select';
|
||||||
|
|
||||||
type NameFieldAdvancedSettingsProps = {
|
type NameFieldAdvancedSettingsProps = {
|
||||||
fieldState: NameFieldMeta;
|
fieldState: NameFieldMeta;
|
||||||
@ -48,6 +55,27 @@ export const NameFieldAdvancedSettings = ({
|
|||||||
max={96}
|
max={96}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>
|
||||||
|
<Trans>Text Align</Trans>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={fieldState.textAlign}
|
||||||
|
onValueChange={(value) => handleInput('textAlign', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-background mt-2">
|
||||||
|
<SelectValue placeholder="Select text align" />
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="left">Left</SelectItem>
|
||||||
|
<SelectItem value="center">Center</SelectItem>
|
||||||
|
<SelectItem value="right">Right</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -38,12 +38,12 @@ export const NumberFieldAdvancedSettings = ({
|
|||||||
const [showValidation, setShowValidation] = useState(false);
|
const [showValidation, setShowValidation] = useState(false);
|
||||||
|
|
||||||
const handleInput = (field: keyof NumberFieldMeta, value: string | boolean) => {
|
const handleInput = (field: keyof NumberFieldMeta, value: string | boolean) => {
|
||||||
const userValue = field === 'value' ? value : fieldState.value ?? 0;
|
const userValue = field === 'value' ? value : (fieldState.value ?? 0);
|
||||||
const userMinValue = field === 'minValue' ? Number(value) : Number(fieldState.minValue ?? 0);
|
const userMinValue = field === 'minValue' ? Number(value) : Number(fieldState.minValue ?? 0);
|
||||||
const userMaxValue = field === 'maxValue' ? Number(value) : Number(fieldState.maxValue ?? 0);
|
const userMaxValue = field === 'maxValue' ? Number(value) : Number(fieldState.maxValue ?? 0);
|
||||||
const readOnly = field === 'readOnly' ? Boolean(value) : Boolean(fieldState.readOnly);
|
const readOnly = field === 'readOnly' ? Boolean(value) : Boolean(fieldState.readOnly);
|
||||||
const required = field === 'required' ? Boolean(value) : Boolean(fieldState.required);
|
const required = field === 'required' ? Boolean(value) : Boolean(fieldState.required);
|
||||||
const numberFormat = field === 'numberFormat' ? String(value) : fieldState.numberFormat ?? '';
|
const numberFormat = field === 'numberFormat' ? String(value) : (fieldState.numberFormat ?? '');
|
||||||
const fontSize = field === 'fontSize' ? Number(value) : Number(fieldState.fontSize ?? 14);
|
const fontSize = field === 'fontSize' ? Number(value) : Number(fieldState.fontSize ?? 14);
|
||||||
|
|
||||||
const valueErrors = validateNumberField(String(userValue), {
|
const valueErrors = validateNumberField(String(userValue), {
|
||||||
@ -135,6 +135,27 @@ export const NumberFieldAdvancedSettings = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>
|
||||||
|
<Trans>Text Align</Trans>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={fieldState.textAlign}
|
||||||
|
onValueChange={(value) => handleInput('textAlign', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-background mt-2">
|
||||||
|
<SelectValue placeholder="Select text align" />
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="left">Left</SelectItem>
|
||||||
|
<SelectItem value="center">Center</SelectItem>
|
||||||
|
<SelectItem value="right">Right</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-2 flex flex-col gap-4">
|
<div className="mt-2 flex flex-col gap-4">
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@ -5,6 +5,13 @@ import { validateTextField } from '@documenso/lib/advanced-fields-validation/val
|
|||||||
import { type TTextFieldMeta as TextFieldMeta } from '@documenso/lib/types/field-meta';
|
import { type TTextFieldMeta as TextFieldMeta } from '@documenso/lib/types/field-meta';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
import { Label } from '@documenso/ui/primitives/label';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@documenso/ui/primitives/select';
|
||||||
import { Switch } from '@documenso/ui/primitives/switch';
|
import { Switch } from '@documenso/ui/primitives/switch';
|
||||||
import { Textarea } from '@documenso/ui/primitives/textarea';
|
import { Textarea } from '@documenso/ui/primitives/textarea';
|
||||||
|
|
||||||
@ -22,7 +29,7 @@ export const TextFieldAdvancedSettings = ({
|
|||||||
const { _ } = useLingui();
|
const { _ } = useLingui();
|
||||||
|
|
||||||
const handleInput = (field: keyof TextFieldMeta, value: string | boolean) => {
|
const handleInput = (field: keyof TextFieldMeta, value: string | boolean) => {
|
||||||
const text = field === 'text' ? String(value) : fieldState.text ?? '';
|
const text = field === 'text' ? String(value) : (fieldState.text ?? '');
|
||||||
const limit =
|
const limit =
|
||||||
field === 'characterLimit' ? Number(value) : Number(fieldState.characterLimit ?? 0);
|
field === 'characterLimit' ? Number(value) : Number(fieldState.characterLimit ?? 0);
|
||||||
const fontSize = field === 'fontSize' ? Number(value) : Number(fieldState.fontSize ?? 14);
|
const fontSize = field === 'fontSize' ? Number(value) : Number(fieldState.fontSize ?? 14);
|
||||||
@ -112,6 +119,27 @@ export const TextFieldAdvancedSettings = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>
|
||||||
|
<Trans>Text Align</Trans>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={fieldState.textAlign}
|
||||||
|
onValueChange={(value) => handleInput('textAlign', value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="bg-background mt-2">
|
||||||
|
<SelectValue placeholder="Select text align" />
|
||||||
|
</SelectTrigger>
|
||||||
|
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="left">Left</SelectItem>
|
||||||
|
<SelectItem value="center">Center</SelectItem>
|
||||||
|
<SelectItem value="right">Right</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 flex flex-col gap-4">
|
<div className="mt-4 flex flex-col gap-4">
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
Reference in New Issue
Block a user