mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 16:23:06 +10:00
feat: add uninserted field validation
This commit is contained in:
@ -9,7 +9,7 @@ export type MarketingLayoutProps = {
|
|||||||
|
|
||||||
export default function MarketingLayout({ children }: MarketingLayoutProps) {
|
export default function MarketingLayout({ children }: MarketingLayoutProps) {
|
||||||
return (
|
return (
|
||||||
<div className="relative max-w-[100vw] overflow-y-auto overflow-x-hidden pt-20 md:pt-28">
|
<div className="relative max-w-[100vw] pt-20 md:pt-28">
|
||||||
<div className="fixed left-0 top-0 z-50 w-full bg-white/50 backdrop-blur-md">
|
<div className="fixed left-0 top-0 z-50 w-full bg-white/50 backdrop-blur-md">
|
||||||
<Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
<Header className="mx-auto h-16 max-w-screen-xl px-4 md:h-20 lg:px-8" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -204,7 +204,7 @@ export default function SinglePlayerModePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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">
|
||||||
<DocumentFlowFormContainer onSubmit={(e) => e.preventDefault()}>
|
<DocumentFlowFormContainer className="top-24" onSubmit={(e) => e.preventDefault()}>
|
||||||
<DocumentFlowFormContainerHeader
|
<DocumentFlowFormContainerHeader
|
||||||
title={currentDocumentFlow.title}
|
title={currentDocumentFlow.title}
|
||||||
description={currentDocumentFlow.description}
|
description={currentDocumentFlow.description}
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@ -19593,7 +19593,7 @@
|
|||||||
"@radix-ui/react-tabs": "^1.0.3",
|
"@radix-ui/react-tabs": "^1.0.3",
|
||||||
"@radix-ui/react-toast": "^1.1.3",
|
"@radix-ui/react-toast": "^1.1.3",
|
||||||
"@radix-ui/react-toggle": "^1.0.2",
|
"@radix-ui/react-toggle": "^1.0.2",
|
||||||
"@radix-ui/react-tooltip": "^1.0.5",
|
"@radix-ui/react-tooltip": "^1.0.6",
|
||||||
"@tanstack/react-table": "^8.9.1",
|
"@tanstack/react-table": "^8.9.1",
|
||||||
"class-variance-authority": "^0.6.0",
|
"class-variance-authority": "^0.6.0",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
|
|||||||
@ -24,8 +24,8 @@
|
|||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.3.0",
|
|
||||||
"@documenso/lib": "*",
|
"@documenso/lib": "*",
|
||||||
|
"@hookform/resolvers": "^3.3.0",
|
||||||
"@radix-ui/react-accordion": "^1.1.1",
|
"@radix-ui/react-accordion": "^1.1.1",
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.3",
|
"@radix-ui/react-alert-dialog": "^1.0.3",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.0.2",
|
"@radix-ui/react-aspect-ratio": "^1.0.2",
|
||||||
@ -51,7 +51,7 @@
|
|||||||
"@radix-ui/react-tabs": "^1.0.3",
|
"@radix-ui/react-tabs": "^1.0.3",
|
||||||
"@radix-ui/react-toast": "^1.1.3",
|
"@radix-ui/react-toast": "^1.1.3",
|
||||||
"@radix-ui/react-toggle": "^1.0.2",
|
"@radix-ui/react-toggle": "^1.0.2",
|
||||||
"@radix-ui/react-tooltip": "^1.0.5",
|
"@radix-ui/react-tooltip": "^1.0.6",
|
||||||
"@tanstack/react-table": "^8.9.1",
|
"@tanstack/react-table": "^8.9.1",
|
||||||
"class-variance-authority": "^0.6.0",
|
"class-variance-authority": "^0.6.0",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
@ -8,7 +8,7 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { match } from 'ts-pattern';
|
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 { FieldType } from '@documenso/prisma/client';
|
import { Field, FieldType } from '@documenso/prisma/client';
|
||||||
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
@ -24,6 +24,7 @@ import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
|||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||||
|
|
||||||
|
import { FieldToolTip } from '../field/field-tooltip';
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
|
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../form/form';
|
||||||
import { ZAddSignatureFormSchema } from './add-signature.types';
|
import { ZAddSignatureFormSchema } from './add-signature.types';
|
||||||
import {
|
import {
|
||||||
@ -50,6 +51,8 @@ export const AddSignatureFormPartial = ({
|
|||||||
requireName = false,
|
requireName = false,
|
||||||
requireSignature = true,
|
requireSignature = true,
|
||||||
}: AddSignatureFormProps) => {
|
}: AddSignatureFormProps) => {
|
||||||
|
const [validateUninsertedFields, setValidateUninsertedFields] = useState(false);
|
||||||
|
|
||||||
// Refined schema which takes into account whether to allow an empty name or signature.
|
// Refined schema which takes into account whether to allow an empty name or signature.
|
||||||
const refinedSchema = ZAddSignatureFormSchema.superRefine((val, ctx) => {
|
const refinedSchema = ZAddSignatureFormSchema.superRefine((val, ctx) => {
|
||||||
if (requireName && val.name.length === 0) {
|
if (requireName && val.name.length === 0) {
|
||||||
@ -81,72 +84,101 @@ export const AddSignatureFormPartial = ({
|
|||||||
/**
|
/**
|
||||||
* A local copy of the provided fields to modify.
|
* A local copy of the provided fields to modify.
|
||||||
*/
|
*/
|
||||||
const [localFields, setLocalFields] = useState(
|
const [localFields, setLocalFields] = useState<Field[]>(JSON.parse(JSON.stringify(fields)));
|
||||||
fields.map((field) => {
|
|
||||||
let customText = field.customText;
|
|
||||||
|
|
||||||
if (field.type === FieldType.DATE) {
|
const uninsertedFields = useMemo(() => {
|
||||||
customText = DateTime.now().toFormat('yyyy-MM-dd hh:mm a');
|
const fields = localFields.filter((field) => !field.inserted);
|
||||||
|
|
||||||
|
return fields.sort((a, b) => {
|
||||||
|
if (a.page < b.page) {
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inserted = match(field.type)
|
if (a.page > b.page) {
|
||||||
.with(FieldType.DATE, () => true)
|
return 1;
|
||||||
.with(FieldType.NAME, () => form.getValues('name').length > 0)
|
}
|
||||||
.with(FieldType.EMAIL, () => form.getValues('email').length > 0)
|
|
||||||
.with(FieldType.SIGNATURE, () => form.getValues('signature').length > 0)
|
|
||||||
.otherwise(() => true);
|
|
||||||
|
|
||||||
return { ...field, inserted, customText };
|
const aTop = a.positionY;
|
||||||
}),
|
const bTop = b.positionY;
|
||||||
);
|
|
||||||
|
|
||||||
const onEmailInputBlur = () => {
|
if (aTop < bTop) {
|
||||||
setLocalFields((prev) =>
|
return -1;
|
||||||
prev.map((field) => {
|
}
|
||||||
if (field.type !== FieldType.EMAIL) {
|
|
||||||
return field;
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = form.getValues('email');
|
if (aTop > bTop) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return 0;
|
||||||
...field,
|
});
|
||||||
customText: value,
|
}, [localFields]);
|
||||||
inserted: value.length > 0,
|
|
||||||
};
|
const onValidateFields = async (values: TAddSignatureFormSchema) => {
|
||||||
}),
|
setValidateUninsertedFields(true);
|
||||||
);
|
|
||||||
|
const firstUninsertedField = uninsertedFields[0];
|
||||||
|
|
||||||
|
const firstUninsertedFieldElement =
|
||||||
|
firstUninsertedField && document.getElementById(`field-${firstUninsertedField.id}`);
|
||||||
|
|
||||||
|
if (firstUninsertedFieldElement) {
|
||||||
|
firstUninsertedFieldElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await onSubmit(values);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onNameInputBlur = () => {
|
/**
|
||||||
setLocalFields((prev) =>
|
* Validates whether the corresponding form for a given field type is valid.
|
||||||
prev.map((field) => {
|
*
|
||||||
if (field.type !== FieldType.NAME) {
|
* @returns `true` if the form associated with the provided field is valid, `false` otherwise.
|
||||||
return field;
|
*/
|
||||||
}
|
const validateFieldForm = async (fieldType: Field['type']): Promise<boolean> => {
|
||||||
|
if (fieldType === FieldType.SIGNATURE) {
|
||||||
|
await form.trigger('signature');
|
||||||
|
return !form.formState.errors.signature;
|
||||||
|
}
|
||||||
|
|
||||||
const value = form.getValues('name');
|
if (fieldType === FieldType.NAME) {
|
||||||
|
await form.trigger('name');
|
||||||
|
return !form.formState.errors.name;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
if (fieldType === FieldType.EMAIL) {
|
||||||
...field,
|
await form.trigger('email');
|
||||||
customText: value,
|
return !form.formState.errors.email;
|
||||||
inserted: value.length > 0,
|
}
|
||||||
};
|
|
||||||
}),
|
return true;
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSignatureInputChange = (value: string) => {
|
/**
|
||||||
setLocalFields((prev) =>
|
* Insert the corresponding form value into a given field.
|
||||||
prev.map((field) => {
|
*/
|
||||||
if (field.type !== FieldType.SIGNATURE) {
|
const insertFormValueIntoField = (field: Field) => {
|
||||||
return field;
|
return match(field.type)
|
||||||
}
|
.with(FieldType.DATE, () => ({
|
||||||
|
...field,
|
||||||
|
customText: DateTime.now().toFormat('yyyy-MM-dd hh:mm a'),
|
||||||
|
inserted: true,
|
||||||
|
}))
|
||||||
|
.with(FieldType.EMAIL, () => ({
|
||||||
|
...field,
|
||||||
|
customText: form.getValues('email'),
|
||||||
|
inserted: true,
|
||||||
|
}))
|
||||||
|
.with(FieldType.NAME, () => ({
|
||||||
|
...field,
|
||||||
|
customText: form.getValues('name'),
|
||||||
|
inserted: true,
|
||||||
|
}))
|
||||||
|
.with(FieldType.SIGNATURE, () => {
|
||||||
|
const value = form.getValues('signature');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...field,
|
...field,
|
||||||
value: value ?? '',
|
value,
|
||||||
inserted: true,
|
|
||||||
Signature: {
|
Signature: {
|
||||||
id: -1,
|
id: -1,
|
||||||
recipientId: -1,
|
recipientId: -1,
|
||||||
@ -155,7 +187,27 @@ export const AddSignatureFormPartial = ({
|
|||||||
signatureImageAsBase64: value,
|
signatureImageAsBase64: value,
|
||||||
typedSignature: null,
|
typedSignature: null,
|
||||||
},
|
},
|
||||||
|
inserted: true,
|
||||||
};
|
};
|
||||||
|
})
|
||||||
|
.otherwise(() => {
|
||||||
|
throw new Error('Unsupported field');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const insertField = (field: Field) => async () => {
|
||||||
|
const isFieldFormValid = await validateFieldForm(field.type);
|
||||||
|
if (!isFieldFormValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocalFields((prev) =>
|
||||||
|
prev.map((prevField) => {
|
||||||
|
if (prevField.id !== field.id) {
|
||||||
|
return prevField;
|
||||||
|
}
|
||||||
|
|
||||||
|
return insertFormValueIntoField(field);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -172,16 +224,7 @@ export const AddSignatureFormPartial = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel required>Email</FormLabel>
|
<FormLabel required>Email</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input className="bg-background" type="email" autoComplete="email" {...field} />
|
||||||
className="bg-background"
|
|
||||||
type="email"
|
|
||||||
autoComplete="email"
|
|
||||||
{...field}
|
|
||||||
onBlur={() => {
|
|
||||||
field.onBlur();
|
|
||||||
onEmailInputBlur();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -196,14 +239,7 @@ export const AddSignatureFormPartial = ({
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel required={requireName}>Name</FormLabel>
|
<FormLabel required={requireName}>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input className="bg-background" {...field} />
|
||||||
className="bg-background"
|
|
||||||
{...field}
|
|
||||||
onBlur={() => {
|
|
||||||
field.onBlur();
|
|
||||||
onNameInputBlur();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -231,10 +267,7 @@ export const AddSignatureFormPartial = ({
|
|||||||
<SignaturePad
|
<SignaturePad
|
||||||
className="h-44 w-full"
|
className="h-44 w-full"
|
||||||
defaultValue={field.value}
|
defaultValue={field.value}
|
||||||
onChange={(value) => {
|
{...field}
|
||||||
field.onChange(value ?? '');
|
|
||||||
onSignatureInputChange(value ?? '');
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@ -258,19 +291,37 @@ export const AddSignatureFormPartial = ({
|
|||||||
loading={form.formState.isSubmitting}
|
loading={form.formState.isSubmitting}
|
||||||
disabled={form.formState.isSubmitting}
|
disabled={form.formState.isSubmitting}
|
||||||
onGoBackClick={documentFlow.onBackStep}
|
onGoBackClick={documentFlow.onBackStep}
|
||||||
onGoNextClick={async () => await form.handleSubmit(onSubmit)()}
|
onGoNextClick={form.handleSubmit(onValidateFields)}
|
||||||
/>
|
/>
|
||||||
</DocumentFlowFormContainerFooter>
|
</DocumentFlowFormContainerFooter>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
{validateUninsertedFields && uninsertedFields[0] && (
|
||||||
|
<FieldToolTip key={uninsertedFields[0].id} field={uninsertedFields[0]} color="warning">
|
||||||
|
Click to insert field
|
||||||
|
</FieldToolTip>
|
||||||
|
)}
|
||||||
|
|
||||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||||
{localFields.map((field) =>
|
{localFields.map((field) =>
|
||||||
match(field.type)
|
match(field.type)
|
||||||
.with(FieldType.DATE, FieldType.EMAIL, FieldType.NAME, () => {
|
.with(FieldType.DATE, FieldType.EMAIL, FieldType.NAME, () => {
|
||||||
return <SinglePlayerModeCustomTextField key={field.id} field={field} />;
|
return (
|
||||||
|
<SinglePlayerModeCustomTextField
|
||||||
|
onClick={insertField(field)}
|
||||||
|
validateUninsertedField={validateUninsertedFields}
|
||||||
|
key={field.id}
|
||||||
|
field={field}
|
||||||
|
/>
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.with(FieldType.SIGNATURE, () => (
|
.with(FieldType.SIGNATURE, () => (
|
||||||
<SinglePlayerModeSignatureField key={field.id} field={field} />
|
<SinglePlayerModeSignatureField
|
||||||
|
onClick={insertField(field)}
|
||||||
|
validateUninsertedField={validateUninsertedFields}
|
||||||
|
key={field.id}
|
||||||
|
field={field}
|
||||||
|
/>
|
||||||
))
|
))
|
||||||
.otherwise(() => {
|
.otherwise(() => {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export const DocumentFlowFormContainer = ({
|
|||||||
<form
|
<form
|
||||||
id={id}
|
id={id}
|
||||||
className={cn(
|
className={cn(
|
||||||
'dark:bg-background border-border bg-widget sticky top-20 flex h-full max-h-[80rem] flex-col rounded-xl border px-4 py-6',
|
'dark:bg-background border-border bg-widget sticky top-20 flex h-full max-h-[64rem] flex-col rounded-xl border px-4 py-6',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -28,6 +28,13 @@ export type FieldContainerPortalProps = {
|
|||||||
export type SinglePlayerModeFieldContainerProps = {
|
export type SinglePlayerModeFieldContainerProps = {
|
||||||
field: FieldWithSignature;
|
field: FieldWithSignature;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
validateUninsertedField?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SinglePlayerModeFieldProps<T> = {
|
||||||
|
field: T;
|
||||||
|
onClick?: () => void;
|
||||||
|
validateUninsertedField?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FieldContainerPortal({
|
export function FieldContainerPortal({
|
||||||
@ -56,20 +63,35 @@ export function FieldContainerPortal({
|
|||||||
export function SinglePlayerModeFieldCardContainer({
|
export function SinglePlayerModeFieldCardContainer({
|
||||||
field,
|
field,
|
||||||
children,
|
children,
|
||||||
|
validateUninsertedField = false,
|
||||||
}: SinglePlayerModeFieldContainerProps) {
|
}: SinglePlayerModeFieldContainerProps) {
|
||||||
return (
|
return (
|
||||||
<FieldContainerPortal field={field}>
|
<FieldContainerPortal field={field}>
|
||||||
<motion.div className="h-full w-full" animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
|
<motion.div className="h-full w-full" animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
|
||||||
<Card
|
<Card
|
||||||
className="bg-background relative z-20 h-full w-full"
|
id={`field-${field.id}`}
|
||||||
|
className={cn('bg-background relative z-20 h-full w-full transition-all', {
|
||||||
|
'border-orange-300 ring-1 ring-orange-300': !field.inserted && validateUninsertedField,
|
||||||
|
})}
|
||||||
data-inserted={field.inserted ? 'true' : 'false'}
|
data-inserted={field.inserted ? 'true' : 'false'}
|
||||||
>
|
>
|
||||||
<CardContent
|
<CardContent className="text-foreground hover:shadow-primary-foreground group flex h-full w-full flex-col items-center justify-center p-2">
|
||||||
className={cn(
|
<AnimatePresence mode="wait" initial={false}>
|
||||||
'text-foreground hover:shadow-primary-foreground group flex h-full w-full flex-col items-center justify-center p-2',
|
<motion.div
|
||||||
)}
|
key={field.inserted ? 'inserted' : 'not-inserted'}
|
||||||
>
|
initial={{ opacity: 0 }}
|
||||||
{children}
|
animate={{
|
||||||
|
opacity: 1,
|
||||||
|
}}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.2,
|
||||||
|
ease: 'easeIn',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@ -77,7 +99,11 @@ export function SinglePlayerModeFieldCardContainer({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SinglePlayerModeSignatureField({ field }: { field: FieldWithSignature }) {
|
export function SinglePlayerModeSignatureField({
|
||||||
|
field,
|
||||||
|
validateUninsertedField,
|
||||||
|
onClick,
|
||||||
|
}: SinglePlayerModeFieldProps<FieldWithSignature>) {
|
||||||
const fontVariable = '--font-signature';
|
const fontVariable = '--font-signature';
|
||||||
const fontVariableValue = getComputedStyle(document.documentElement).getPropertyValue(
|
const fontVariableValue = getComputedStyle(document.documentElement).getPropertyValue(
|
||||||
fontVariable,
|
fontVariable,
|
||||||
@ -110,50 +136,44 @@ export function SinglePlayerModeSignatureField({ field }: { field: FieldWithSign
|
|||||||
const insertedTypeSignature = field.inserted && field.Signature?.typedSignature;
|
const insertedTypeSignature = field.inserted && field.Signature?.typedSignature;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SinglePlayerModeFieldCardContainer field={field}>
|
<SinglePlayerModeFieldCardContainer
|
||||||
<AnimatePresence mode="wait" initial={false}>
|
validateUninsertedField={validateUninsertedField}
|
||||||
<motion.div
|
field={field}
|
||||||
key={
|
>
|
||||||
(insertedBase64Signature && 'base64Signature') ||
|
{insertedBase64Signature ? (
|
||||||
(insertedTypeSignature && 'typedSignature') ||
|
<img
|
||||||
'not-inserted'
|
src={insertedBase64Signature}
|
||||||
}
|
alt="Your signature"
|
||||||
initial={{ opacity: 0 }}
|
className="h-full w-full object-contain"
|
||||||
animate={{
|
/>
|
||||||
opacity: 1,
|
) : insertedTypeSignature ? (
|
||||||
transition: {
|
<p
|
||||||
duration: 0.3,
|
ref={$paragraphEl}
|
||||||
},
|
style={{
|
||||||
|
fontSize: `clamp(${minFontSize}px, ${fontSize}px, ${maxFontSize}px)`,
|
||||||
|
fontFamily: `var(${fontVariable})`,
|
||||||
}}
|
}}
|
||||||
exit={{ opacity: 0 }}
|
className="font-signature"
|
||||||
>
|
>
|
||||||
{insertedBase64Signature ? (
|
{insertedTypeSignature}
|
||||||
<img
|
</p>
|
||||||
src={insertedBase64Signature}
|
) : (
|
||||||
alt="Your signature"
|
<button
|
||||||
className="h-full w-full object-contain"
|
onClick={() => onClick?.()}
|
||||||
/>
|
className="group-hover:text-primary text-muted-foreground absolute inset-0 duration-200"
|
||||||
) : insertedTypeSignature ? (
|
>
|
||||||
<p
|
Signature
|
||||||
ref={$paragraphEl}
|
</button>
|
||||||
style={{
|
)}
|
||||||
fontSize: `clamp(${minFontSize}px, ${fontSize}px, ${maxFontSize}px)`,
|
|
||||||
fontFamily: `var(${fontVariable})`,
|
|
||||||
}}
|
|
||||||
className="font-signature"
|
|
||||||
>
|
|
||||||
{insertedTypeSignature}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<p className="group-hover:text-primary text-muted-foreground duration-200">Signature</p>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
</AnimatePresence>
|
|
||||||
</SinglePlayerModeFieldCardContainer>
|
</SinglePlayerModeFieldCardContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SinglePlayerModeCustomTextField({ field }: { field: Field }) {
|
export function SinglePlayerModeCustomTextField({
|
||||||
|
field,
|
||||||
|
validateUninsertedField,
|
||||||
|
onClick,
|
||||||
|
}: SinglePlayerModeFieldProps<Field>) {
|
||||||
const fontVariable = '--font-sans';
|
const fontVariable = '--font-sans';
|
||||||
const fontVariableValue = getComputedStyle(document.documentElement).getPropertyValue(
|
const fontVariableValue = getComputedStyle(document.documentElement).getPropertyValue(
|
||||||
fontVariable,
|
fontVariable,
|
||||||
@ -183,7 +203,10 @@ export function SinglePlayerModeCustomTextField({ field }: { field: Field }) {
|
|||||||
const fontSize = maxFontSize * scalingFactor;
|
const fontSize = maxFontSize * scalingFactor;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SinglePlayerModeFieldCardContainer key="not-inserted" field={field}>
|
<SinglePlayerModeFieldCardContainer
|
||||||
|
validateUninsertedField={validateUninsertedField}
|
||||||
|
field={field}
|
||||||
|
>
|
||||||
{field.inserted ? (
|
{field.inserted ? (
|
||||||
<p
|
<p
|
||||||
ref={$paragraphEl}
|
ref={$paragraphEl}
|
||||||
@ -195,14 +218,17 @@ export function SinglePlayerModeCustomTextField({ field }: { field: Field }) {
|
|||||||
{field.customText}
|
{field.customText}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="group-hover:text-primary text-muted-foreground text-lg duration-200">
|
<button
|
||||||
|
onClick={() => onClick?.()}
|
||||||
|
className="group-hover:text-primary text-muted-foreground absolute inset-0 text-lg duration-200"
|
||||||
|
>
|
||||||
{match(field.type)
|
{match(field.type)
|
||||||
.with(FieldType.DATE, () => 'Date')
|
.with(FieldType.DATE, () => 'Date')
|
||||||
.with(FieldType.NAME, () => 'Name')
|
.with(FieldType.NAME, () => 'Name')
|
||||||
.with(FieldType.EMAIL, () => 'Email')
|
.with(FieldType.EMAIL, () => 'Email')
|
||||||
.with(FieldType.SIGNATURE, FieldType.FREE_SIGNATURE, () => 'Signature')
|
.with(FieldType.SIGNATURE, FieldType.FREE_SIGNATURE, () => 'Signature')
|
||||||
.otherwise(() => '')}
|
.otherwise(() => '')}
|
||||||
</p>
|
</button>
|
||||||
)}
|
)}
|
||||||
</SinglePlayerModeFieldCardContainer>
|
</SinglePlayerModeFieldCardContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
63
packages/ui/primitives/field/field-tooltip.tsx
Normal file
63
packages/ui/primitives/field/field-tooltip.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { TooltipArrow } from '@radix-ui/react-tooltip';
|
||||||
|
import { VariantProps, cva } from 'class-variance-authority';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
|
||||||
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@documenso/ui/primitives/tooltip';
|
||||||
|
|
||||||
|
import { Field } from '.prisma/client';
|
||||||
|
|
||||||
|
const tooltipVariants = cva('font-semibold', {
|
||||||
|
variants: {
|
||||||
|
color: {
|
||||||
|
default: 'border-2 fill-white',
|
||||||
|
warning: 'border-0 bg-orange-300 fill-orange-300 text-orange-900',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
color: 'default',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface FieldToolTipProps extends VariantProps<typeof tooltipVariants> {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
field: Field;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders a tooltip for a given field.
|
||||||
|
*/
|
||||||
|
export function FieldToolTip({ children, color, className = '', field }: FieldToolTipProps) {
|
||||||
|
const coords = useFieldPageCoords(field);
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<div
|
||||||
|
className={cn('absolute', className)}
|
||||||
|
style={{
|
||||||
|
top: `${coords.y}px`,
|
||||||
|
left: `${coords.x}px`,
|
||||||
|
height: `${coords.height}px`,
|
||||||
|
width: `${coords.width}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip delayDuration={0} open={!field.inserted}>
|
||||||
|
<TooltipTrigger className="absolute top-0 w-full"></TooltipTrigger>
|
||||||
|
|
||||||
|
<TooltipContent className={tooltipVariants({ color, className })} sideOffset={2}>
|
||||||
|
{children}
|
||||||
|
<TooltipArrow />
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user