feat: add visible completed fields
This commit is contained in:
@ -8,6 +8,7 @@ import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
|
||||
import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
||||
import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag';
|
||||
import { getCompletedFieldsForDocument } from '@documenso/lib/server-only/field/get-completed-fields-for-document';
|
||||
import { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||
@ -19,6 +20,7 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
|
||||
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
||||
import { DocumentHistorySheet } from '~/components/document/document-history-sheet';
|
||||
import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields';
|
||||
import {
|
||||
DocumentStatus as DocumentStatusComponent,
|
||||
FRIENDLY_STATUS_MAP,
|
||||
@ -83,11 +85,16 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
||||
documentMeta.password = securePassword;
|
||||
}
|
||||
|
||||
const recipients = await getRecipientsForDocument({
|
||||
documentId,
|
||||
teamId: team?.id,
|
||||
userId: user.id,
|
||||
});
|
||||
const [recipients, completedFields] = await Promise.all([
|
||||
getRecipientsForDocument({
|
||||
documentId,
|
||||
teamId: team?.id,
|
||||
userId: user.id,
|
||||
}),
|
||||
getCompletedFieldsForDocument({
|
||||
documentId,
|
||||
}),
|
||||
]);
|
||||
|
||||
const documentWithRecipients = {
|
||||
...document,
|
||||
@ -148,6 +155,13 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{document.status === DocumentStatus.PENDING && (
|
||||
<DocumentReadOnlyFields
|
||||
fields={completedFields}
|
||||
documentMeta={document.documentMeta || undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="col-span-12 lg:col-span-6 xl:col-span-5">
|
||||
<div className="space-y-6">
|
||||
<section className="border-border bg-widget flex flex-col rounded-xl border pb-4 pt-6">
|
||||
|
@ -6,6 +6,7 @@ import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-c
|
||||
import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||
import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
|
||||
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
||||
import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get-completed-fields-for-token';
|
||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||
@ -37,7 +38,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
||||
|
||||
const requestMetadata = extractNextHeaderRequestMetadata(requestHeaders);
|
||||
|
||||
const [document, fields, recipient] = await Promise.all([
|
||||
const [document, fields, recipient, completedFields] = await Promise.all([
|
||||
getDocumentAndSenderByToken({
|
||||
token,
|
||||
userId: user?.id,
|
||||
@ -45,6 +46,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
||||
}).catch(() => null),
|
||||
getFieldsForToken({ token }),
|
||||
getRecipientByToken({ token }).catch(() => null),
|
||||
getCompletedFieldsForToken({ token }),
|
||||
]);
|
||||
|
||||
if (!document || !document.documentData || !recipient) {
|
||||
@ -120,7 +122,12 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
||||
signature={user?.email === recipient.email ? user.signature : undefined}
|
||||
>
|
||||
<DocumentAuthProvider document={document} recipient={recipient} user={user}>
|
||||
<SigningPageView recipient={recipient} document={document} fields={fields} />
|
||||
<SigningPageView
|
||||
recipient={recipient}
|
||||
document={document}
|
||||
fields={fields}
|
||||
completedFields={completedFields}
|
||||
/>
|
||||
</DocumentAuthProvider>
|
||||
</SigningProvider>
|
||||
);
|
||||
|
@ -4,12 +4,14 @@ import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-form
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||
import type { DocumentAndSender } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||
import type { CompletedField } from '@documenso/lib/types/fields';
|
||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||
import { FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
||||
|
||||
import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields';
|
||||
import { truncateTitle } from '~/helpers/truncate-title';
|
||||
|
||||
import { DateField } from './date-field';
|
||||
@ -23,9 +25,15 @@ export type SigningPageViewProps = {
|
||||
document: DocumentAndSender;
|
||||
recipient: Recipient;
|
||||
fields: Field[];
|
||||
completedFields: CompletedField[];
|
||||
};
|
||||
|
||||
export const SigningPageView = ({ document, recipient, fields }: SigningPageViewProps) => {
|
||||
export const SigningPageView = ({
|
||||
document,
|
||||
recipient,
|
||||
fields,
|
||||
completedFields,
|
||||
}: SigningPageViewProps) => {
|
||||
const truncatedTitle = truncateTitle(document.title);
|
||||
|
||||
const { documentData, documentMeta } = document;
|
||||
@ -70,6 +78,8 @@ export const SigningPageView = ({ document, recipient, fields }: SigningPageView
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DocumentReadOnlyFields fields={completedFields} />
|
||||
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{fields.map((field) =>
|
||||
match(field.type)
|
||||
|
@ -1,12 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useState } from 'react';
|
||||
|
||||
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover';
|
||||
import { PopoverHover } from '@documenso/ui/primitives/popover';
|
||||
|
||||
import { AvatarWithRecipient } from './avatar-with-recipient';
|
||||
import { StackAvatar } from './stack-avatar';
|
||||
@ -23,11 +21,6 @@ export const StackAvatarsWithTooltip = ({
|
||||
position,
|
||||
children,
|
||||
}: StackAvatarsWithTooltipProps) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const isControlled = useRef(false);
|
||||
const isMouseOverTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const waitingRecipients = recipients.filter(
|
||||
(recipient) => getRecipientType(recipient) === 'waiting',
|
||||
);
|
||||
@ -44,105 +37,62 @@ export const StackAvatarsWithTooltip = ({
|
||||
(recipient) => getRecipientType(recipient) === 'unsigned',
|
||||
);
|
||||
|
||||
const onMouseEnter = () => {
|
||||
if (isMouseOverTimeout.current) {
|
||||
clearTimeout(isMouseOverTimeout.current);
|
||||
}
|
||||
|
||||
if (isControlled.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
isMouseOverTimeout.current = setTimeout(() => {
|
||||
setOpen((o) => (!o ? true : o));
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
if (isMouseOverTimeout.current) {
|
||||
clearTimeout(isMouseOverTimeout.current);
|
||||
}
|
||||
|
||||
if (isControlled.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setOpen((o) => (o ? false : o));
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const onOpenChange = (newOpen: boolean) => {
|
||||
isControlled.current = newOpen;
|
||||
|
||||
setOpen(newOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={onOpenChange}>
|
||||
<PopoverTrigger
|
||||
className="flex cursor-pointer"
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{children || <StackAvatars recipients={recipients} />}
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
side={position}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
className="flex flex-col gap-y-5 py-2"
|
||||
>
|
||||
{completedRecipients.length > 0 && (
|
||||
<div>
|
||||
<h1 className="text-base font-medium">Completed</h1>
|
||||
{completedRecipients.map((recipient: Recipient) => (
|
||||
<div key={recipient.id} className="my-1 flex items-center gap-2">
|
||||
<StackAvatar
|
||||
first={true}
|
||||
key={recipient.id}
|
||||
type={getRecipientType(recipient)}
|
||||
fallbackText={recipientAbbreviation(recipient)}
|
||||
/>
|
||||
<div className="">
|
||||
<p className="text-muted-foreground text-sm">{recipient.email}</p>
|
||||
<p className="text-muted-foreground/70 text-xs">
|
||||
{RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName}
|
||||
</p>
|
||||
</div>
|
||||
<PopoverHover
|
||||
trigger={children || <StackAvatars recipients={recipients} />}
|
||||
contentProps={{
|
||||
className: 'flex flex-col gap-y-5 py-2',
|
||||
side: position,
|
||||
}}
|
||||
>
|
||||
{completedRecipients.length > 0 && (
|
||||
<div>
|
||||
<h1 className="text-base font-medium">Completed</h1>
|
||||
{completedRecipients.map((recipient: Recipient) => (
|
||||
<div key={recipient.id} className="my-1 flex items-center gap-2">
|
||||
<StackAvatar
|
||||
first={true}
|
||||
key={recipient.id}
|
||||
type={getRecipientType(recipient)}
|
||||
fallbackText={recipientAbbreviation(recipient)}
|
||||
/>
|
||||
<div className="">
|
||||
<p className="text-muted-foreground text-sm">{recipient.email}</p>
|
||||
<p className="text-muted-foreground/70 text-xs">
|
||||
{RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{waitingRecipients.length > 0 && (
|
||||
<div>
|
||||
<h1 className="text-base font-medium">Waiting</h1>
|
||||
{waitingRecipients.map((recipient: Recipient) => (
|
||||
<AvatarWithRecipient key={recipient.id} recipient={recipient} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{waitingRecipients.length > 0 && (
|
||||
<div>
|
||||
<h1 className="text-base font-medium">Waiting</h1>
|
||||
{waitingRecipients.map((recipient: Recipient) => (
|
||||
<AvatarWithRecipient key={recipient.id} recipient={recipient} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{openedRecipients.length > 0 && (
|
||||
<div>
|
||||
<h1 className="text-base font-medium">Opened</h1>
|
||||
{openedRecipients.map((recipient: Recipient) => (
|
||||
<AvatarWithRecipient key={recipient.id} recipient={recipient} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{openedRecipients.length > 0 && (
|
||||
<div>
|
||||
<h1 className="text-base font-medium">Opened</h1>
|
||||
{openedRecipients.map((recipient: Recipient) => (
|
||||
<AvatarWithRecipient key={recipient.id} recipient={recipient} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{uncompletedRecipients.length > 0 && (
|
||||
<div>
|
||||
<h1 className="text-base font-medium">Uncompleted</h1>
|
||||
{uncompletedRecipients.map((recipient: Recipient) => (
|
||||
<AvatarWithRecipient key={recipient.id} recipient={recipient} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{uncompletedRecipients.length > 0 && (
|
||||
<div>
|
||||
<h1 className="text-base font-medium">Uncompleted</h1>
|
||||
{uncompletedRecipients.map((recipient: Recipient) => (
|
||||
<AvatarWithRecipient key={recipient.id} recipient={recipient} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</PopoverHover>
|
||||
);
|
||||
};
|
||||
|
113
apps/web/src/components/document/document-read-only-fields.tsx
Normal file
113
apps/web/src/components/document/document-read-only-fields.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { P, match } from 'ts-pattern';
|
||||
|
||||
import {
|
||||
DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||
convertToLocalSystemFormat,
|
||||
} from '@documenso/lib/constants/date-formats';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
||||
import type { CompletedField } from '@documenso/lib/types/fields';
|
||||
import { extractInitials } from '@documenso/lib/utils/recipient-formatter';
|
||||
import type { DocumentMeta } from '@documenso/prisma/client';
|
||||
import { FieldType } from '@documenso/prisma/client';
|
||||
import { FieldRootContainer } from '@documenso/ui/components/field/field';
|
||||
import { Avatar, AvatarFallback } from '@documenso/ui/primitives/avatar';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { FRIENDLY_FIELD_TYPE } from '@documenso/ui/primitives/document-flow/types';
|
||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||
import { PopoverHover } from '@documenso/ui/primitives/popover';
|
||||
|
||||
export type DocumentReadOnlyFieldsProps = {
|
||||
fields: CompletedField[];
|
||||
documentMeta?: DocumentMeta;
|
||||
};
|
||||
|
||||
export const DocumentReadOnlyFields = ({ documentMeta, fields }: DocumentReadOnlyFieldsProps) => {
|
||||
const [hiddenFieldIds, setHiddenFieldIds] = useState<Record<string, boolean>>({});
|
||||
|
||||
const handleHideField = (fieldId: string) => {
|
||||
setHiddenFieldIds((prev) => ({ ...prev, [fieldId]: true }));
|
||||
};
|
||||
|
||||
return (
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{fields.map(
|
||||
(field) =>
|
||||
!hiddenFieldIds[field.secondaryId] && (
|
||||
<FieldRootContainer
|
||||
field={field}
|
||||
key={field.id}
|
||||
cardClassName="border-gray-100/50 !shadow-none backdrop-blur-[1px] bg-background/90"
|
||||
>
|
||||
<div className="absolute -right-3 -top-3">
|
||||
<PopoverHover
|
||||
trigger={
|
||||
<Avatar className="dark:border-border h-8 w-8 border-2 border-solid border-gray-200/50 transition-colors hover:border-gray-200">
|
||||
<AvatarFallback className="bg-neutral-50 text-xs text-gray-400">
|
||||
{extractInitials(field.Recipient.name || field.Recipient.email)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
}
|
||||
contentProps={{
|
||||
className: 'flex w-fit flex-col py-2.5 text-sm',
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
<span className="font-semibold">
|
||||
{field.Recipient.name
|
||||
? `${field.Recipient.name} (${field.Recipient.email})`
|
||||
: field.Recipient.email}{' '}
|
||||
</span>
|
||||
inserted a {FRIENDLY_FIELD_TYPE[field.type].toLowerCase()}
|
||||
</p>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
className="mt-2.5 h-6 text-xs focus:outline-none focus-visible:ring-0"
|
||||
onClick={() => handleHideField(field.secondaryId)}
|
||||
>
|
||||
Hide field
|
||||
</Button>
|
||||
</PopoverHover>
|
||||
</div>
|
||||
|
||||
<div className="text-muted-foreground">
|
||||
{match(field)
|
||||
.with({ type: FieldType.SIGNATURE }, (field) =>
|
||||
field.Signature?.signatureImageAsBase64 ? (
|
||||
<img
|
||||
src={field.Signature.signatureImageAsBase64}
|
||||
alt="Signature"
|
||||
className="h-full w-full object-contain dark:invert"
|
||||
/>
|
||||
) : (
|
||||
<p className="font-signature text-muted-foreground text-lg duration-200 sm:text-xl md:text-2xl lg:text-3xl">
|
||||
{field.Signature?.typedSignature}
|
||||
</p>
|
||||
),
|
||||
)
|
||||
.with({ type: P.union(FieldType.NAME, FieldType.TEXT, FieldType.EMAIL) }, () => (
|
||||
<p className="text-muted-foreground text-lg">{field.customText}</p>
|
||||
))
|
||||
.with({ type: FieldType.DATE }, () => (
|
||||
<p>
|
||||
{convertToLocalSystemFormat(
|
||||
field.customText,
|
||||
documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||
documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
|
||||
)}
|
||||
</p>
|
||||
))
|
||||
.with({ type: FieldType.FREE_SIGNATURE }, () => null)
|
||||
.exhaustive()}
|
||||
</div>
|
||||
</FieldRootContainer>
|
||||
),
|
||||
)}
|
||||
</ElementVisible>
|
||||
);
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
export type GetCompletedFieldsForDocumentOptions = {
|
||||
documentId: number;
|
||||
};
|
||||
|
||||
export const getCompletedFieldsForDocument = async ({
|
||||
documentId,
|
||||
}: GetCompletedFieldsForDocumentOptions) => {
|
||||
return await prisma.field.findMany({
|
||||
where: {
|
||||
documentId,
|
||||
Recipient: {
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
},
|
||||
inserted: true,
|
||||
},
|
||||
include: {
|
||||
Signature: true,
|
||||
Recipient: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
@ -0,0 +1,33 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
export type GetCompletedFieldsForTokenOptions = {
|
||||
token: string;
|
||||
};
|
||||
|
||||
export const getCompletedFieldsForToken = async ({ token }: GetCompletedFieldsForTokenOptions) => {
|
||||
return await prisma.field.findMany({
|
||||
where: {
|
||||
Document: {
|
||||
Recipient: {
|
||||
some: {
|
||||
token,
|
||||
},
|
||||
},
|
||||
},
|
||||
Recipient: {
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
},
|
||||
inserted: true,
|
||||
},
|
||||
include: {
|
||||
Signature: true,
|
||||
Recipient: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
@ -89,6 +89,10 @@ export const createDocumentFromTemplate = async ({
|
||||
|
||||
const documentRecipient = document.Recipient.find((doc) => doc.email === recipient?.email);
|
||||
|
||||
if (!documentRecipient) {
|
||||
throw new Error('Recipient not found.');
|
||||
}
|
||||
|
||||
return {
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
@ -99,7 +103,7 @@ export const createDocumentFromTemplate = async ({
|
||||
customText: field.customText,
|
||||
inserted: field.inserted,
|
||||
documentId: document.id,
|
||||
recipientId: documentRecipient?.id || null,
|
||||
recipientId: documentRecipient.id,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
@ -81,6 +81,10 @@ export const duplicateTemplate = async ({
|
||||
(doc) => doc.email === recipient?.email,
|
||||
);
|
||||
|
||||
if (!duplicatedTemplateRecipient) {
|
||||
throw new Error('Recipient not found.');
|
||||
}
|
||||
|
||||
return {
|
||||
type: field.type,
|
||||
page: field.page,
|
||||
@ -91,7 +95,7 @@ export const duplicateTemplate = async ({
|
||||
customText: field.customText,
|
||||
inserted: field.inserted,
|
||||
templateId: duplicatedTemplate.id,
|
||||
recipientId: duplicatedTemplateRecipient?.id || null,
|
||||
recipientId: duplicatedTemplateRecipient.id,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
3
packages/lib/types/fields.ts
Normal file
3
packages/lib/types/fields.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import type { getCompletedFieldsForToken } from '../server-only/field/get-completed-fields-for-token';
|
||||
|
||||
export type CompletedField = Awaited<ReturnType<typeof getCompletedFieldsForToken>>[number];
|
@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Made the column `recipientId` on table `Field` required. This step will fail if there are existing NULL values in that column.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "Field" ALTER COLUMN "recipientId" SET NOT NULL;
|
@ -386,7 +386,7 @@ model Field {
|
||||
secondaryId String @unique @default(cuid())
|
||||
documentId Int?
|
||||
templateId Int?
|
||||
recipientId Int?
|
||||
recipientId Int
|
||||
type FieldType
|
||||
page Int
|
||||
positionX Decimal @default(0)
|
||||
@ -397,7 +397,7 @@ model Field {
|
||||
inserted Boolean
|
||||
Document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
Template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
||||
Recipient Recipient? @relation(fields: [recipientId], references: [id], onDelete: Cascade)
|
||||
Recipient Recipient @relation(fields: [recipientId], references: [id], onDelete: Cascade)
|
||||
Signature Signature?
|
||||
|
||||
@@index([documentId])
|
||||
|
@ -19,6 +19,7 @@ export type FieldContainerPortalProps = {
|
||||
field: Field;
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
cardClassName?: string;
|
||||
};
|
||||
|
||||
export function FieldContainerPortal({
|
||||
@ -44,7 +45,7 @@ export function FieldContainerPortal({
|
||||
);
|
||||
}
|
||||
|
||||
export function FieldRootContainer({ field, children }: FieldContainerPortalProps) {
|
||||
export function FieldRootContainer({ field, children, cardClassName }: FieldContainerPortalProps) {
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
@ -78,6 +79,7 @@ export function FieldRootContainer({ field, children }: FieldContainerPortalProp
|
||||
{
|
||||
'border-orange-300 ring-1 ring-orange-300': !field.inserted && isValidating,
|
||||
},
|
||||
cardClassName,
|
||||
)}
|
||||
ref={ref}
|
||||
data-inserted={field.inserted ? 'true' : 'false'}
|
||||
|
@ -30,4 +30,66 @@ const PopoverContent = React.forwardRef<
|
||||
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent };
|
||||
type PopoverHoverProps = {
|
||||
trigger: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
contentProps?: React.ComponentPropsWithoutRef<typeof PopoverContent>;
|
||||
};
|
||||
|
||||
const PopoverHover = ({ trigger, children, contentProps }: PopoverHoverProps) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const isControlled = React.useRef(false);
|
||||
const isMouseOver = React.useRef<boolean>(false);
|
||||
|
||||
const onMouseEnter = () => {
|
||||
isMouseOver.current = true;
|
||||
|
||||
if (isControlled.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const onMouseLeave = () => {
|
||||
isMouseOver.current = false;
|
||||
|
||||
if (isControlled.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setOpen(isMouseOver.current);
|
||||
}, 200);
|
||||
};
|
||||
|
||||
const onOpenChange = (newOpen: boolean) => {
|
||||
isControlled.current = newOpen;
|
||||
|
||||
setOpen(newOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={onOpenChange}>
|
||||
<PopoverTrigger
|
||||
className="flex cursor-pointer outline-none"
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{trigger}
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
side="top"
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
{...contentProps}
|
||||
>
|
||||
{children}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverHover };
|
||||
|
Reference in New Issue
Block a user