mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 00:32:43 +10:00
Compare commits
12 Commits
chore/subj
...
v1.5.5-rc.
| Author | SHA1 | Date | |
|---|---|---|---|
| e949fb14ae | |||
| e1573465f6 | |||
| 0062359977 | |||
| 03727bfad2 | |||
| c4a680caf7 | |||
| 4de122f814 | |||
| e4cf9c8251 | |||
| 41ed6c9ad7 | |||
| afaeba9739 | |||
| f6e6dac46c | |||
| a97ffa97a4 | |||
| 6526377f1b |
@ -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 { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session';
|
||||||
import { getDocumentById } from '@documenso/lib/server-only/document/get-document-by-id';
|
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 { 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 { getRecipientsForDocument } from '@documenso/lib/server-only/recipient/get-recipients-for-document';
|
||||||
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
||||||
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
import { formatDocumentsPath } from '@documenso/lib/utils/teams';
|
||||||
@ -20,6 +21,7 @@ import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
|||||||
|
|
||||||
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
import { StackAvatarsWithTooltip } from '~/components/(dashboard)/avatar/stack-avatars-with-tooltip';
|
||||||
import { DocumentHistorySheet } from '~/components/document/document-history-sheet';
|
import { DocumentHistorySheet } from '~/components/document/document-history-sheet';
|
||||||
|
import { DocumentReadOnlyFields } from '~/components/document/document-read-only-fields';
|
||||||
import {
|
import {
|
||||||
DocumentStatus as DocumentStatusComponent,
|
DocumentStatus as DocumentStatusComponent,
|
||||||
FRIENDLY_STATUS_MAP,
|
FRIENDLY_STATUS_MAP,
|
||||||
@ -84,11 +86,16 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
|||||||
documentMeta.password = securePassword;
|
documentMeta.password = securePassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recipients = await getRecipientsForDocument({
|
const [recipients, completedFields] = await Promise.all([
|
||||||
documentId,
|
getRecipientsForDocument({
|
||||||
teamId: team?.id,
|
documentId,
|
||||||
userId: user.id,
|
teamId: team?.id,
|
||||||
});
|
userId: user.id,
|
||||||
|
}),
|
||||||
|
getCompletedFieldsForDocument({
|
||||||
|
documentId,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
const documentWithRecipients = {
|
const documentWithRecipients = {
|
||||||
...document,
|
...document,
|
||||||
@ -155,6 +162,13 @@ export const DocumentPageView = async ({ params, team }: DocumentPageViewProps)
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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="col-span-12 lg:col-span-6 xl:col-span-5">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<section className="border-border bg-widget flex flex-col rounded-xl border pb-4 pt-6">
|
<section className="border-border bg-widget flex flex-col rounded-xl border pb-4 pt-6">
|
||||||
|
|||||||
@ -224,10 +224,6 @@ export const EditDocumentForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const setSubjectFormFields = (subject?: string, message?: string) => {
|
|
||||||
// Add functionality here
|
|
||||||
};
|
|
||||||
|
|
||||||
const onAddFieldsFormSubmit = async (data: TAddFieldsFormSchema) => {
|
const onAddFieldsFormSubmit = async (data: TAddFieldsFormSchema) => {
|
||||||
try {
|
try {
|
||||||
await addFields({
|
await addFields({
|
||||||
@ -363,7 +359,6 @@ export const EditDocumentForm = ({
|
|||||||
fields={fields}
|
fields={fields}
|
||||||
onSubmit={onAddSubjectFormSubmit}
|
onSubmit={onAddSubjectFormSubmit}
|
||||||
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
isDocumentPdfLoaded={isDocumentPdfLoaded}
|
||||||
setSubjectFormFields={setSubjectFormFields}
|
|
||||||
/>
|
/>
|
||||||
</Stepper>
|
</Stepper>
|
||||||
</DocumentFlowFormContainer>
|
</DocumentFlowFormContainer>
|
||||||
|
|||||||
@ -133,7 +133,11 @@ export const DocumentLogsPageView = async ({ params, team }: DocumentLogsPageVie
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end">
|
<div className="mt-4 flex w-full flex-row sm:mt-0 sm:w-auto sm:self-end">
|
||||||
<DownloadCertificateButton className="mr-2" documentId={document.id} />
|
<DownloadCertificateButton
|
||||||
|
className="mr-2"
|
||||||
|
documentId={document.id}
|
||||||
|
documentStatus={document.status}
|
||||||
|
/>
|
||||||
|
|
||||||
<DownloadAuditLogButton documentId={document.id} />
|
<DownloadAuditLogButton documentId={document.id} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { DownloadIcon } from 'lucide-react';
|
import { DownloadIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { DocumentStatus } from '@documenso/prisma/client';
|
||||||
import { trpc } from '@documenso/trpc/react';
|
import { trpc } from '@documenso/trpc/react';
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
@ -10,11 +11,13 @@ import { useToast } from '@documenso/ui/primitives/use-toast';
|
|||||||
export type DownloadCertificateButtonProps = {
|
export type DownloadCertificateButtonProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
documentId: number;
|
documentId: number;
|
||||||
|
documentStatus: DocumentStatus;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DownloadCertificateButton = ({
|
export const DownloadCertificateButton = ({
|
||||||
className,
|
className,
|
||||||
documentId,
|
documentId,
|
||||||
|
documentStatus,
|
||||||
}: DownloadCertificateButtonProps) => {
|
}: DownloadCertificateButtonProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
@ -69,6 +72,7 @@ export const DownloadCertificateButton = ({
|
|||||||
className={cn('w-full sm:w-auto', className)}
|
className={cn('w-full sm:w-auto', className)}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
disabled={documentStatus !== DocumentStatus.COMPLETED}
|
||||||
onClick={() => void onDownloadCertificatesClick()}
|
onClick={() => void onDownloadCertificatesClick()}
|
||||||
>
|
>
|
||||||
{!isLoading && <DownloadIcon className="mr-1.5 h-4 w-4" />}
|
{!isLoading && <DownloadIcon className="mr-1.5 h-4 w-4" />}
|
||||||
|
|||||||
@ -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 { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token';
|
||||||
import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
|
import { isRecipientAuthorized } from '@documenso/lib/server-only/document/is-recipient-authorized';
|
||||||
import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document';
|
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 { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
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 requestMetadata = extractNextHeaderRequestMetadata(requestHeaders);
|
||||||
|
|
||||||
const [document, fields, recipient] = await Promise.all([
|
const [document, fields, recipient, completedFields] = await Promise.all([
|
||||||
getDocumentAndSenderByToken({
|
getDocumentAndSenderByToken({
|
||||||
token,
|
token,
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
@ -45,6 +46,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
}).catch(() => null),
|
}).catch(() => null),
|
||||||
getFieldsForToken({ token }),
|
getFieldsForToken({ token }),
|
||||||
getRecipientByToken({ token }).catch(() => null),
|
getRecipientByToken({ token }).catch(() => null),
|
||||||
|
getCompletedFieldsForToken({ token }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -125,7 +127,12 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
|||||||
signature={user?.email === recipient.email ? user.signature : undefined}
|
signature={user?.email === recipient.email ? user.signature : undefined}
|
||||||
>
|
>
|
||||||
<DocumentAuthProvider document={document} recipient={recipient} user={user}>
|
<DocumentAuthProvider document={document} recipient={recipient} user={user}>
|
||||||
<SigningPageView recipient={recipient} document={document} fields={fields} />
|
<SigningPageView
|
||||||
|
recipient={recipient}
|
||||||
|
document={document}
|
||||||
|
fields={fields}
|
||||||
|
completedFields={completedFields}
|
||||||
|
/>
|
||||||
</DocumentAuthProvider>
|
</DocumentAuthProvider>
|
||||||
</SigningProvider>
|
</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 { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||||
import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones';
|
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 { 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 type { Field, Recipient } from '@documenso/prisma/client';
|
||||||
import { FieldType, RecipientRole } from '@documenso/prisma/client';
|
import { FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
import { ElementVisible } from '@documenso/ui/primitives/element-visible';
|
||||||
import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer';
|
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 { truncateTitle } from '~/helpers/truncate-title';
|
||||||
|
|
||||||
import { DateField } from './date-field';
|
import { DateField } from './date-field';
|
||||||
@ -23,9 +25,15 @@ export type SigningPageViewProps = {
|
|||||||
document: DocumentAndSender;
|
document: DocumentAndSender;
|
||||||
recipient: Recipient;
|
recipient: Recipient;
|
||||||
fields: Field[];
|
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 truncatedTitle = truncateTitle(document.title);
|
||||||
|
|
||||||
const { documentData, documentMeta } = document;
|
const { documentData, documentMeta } = document;
|
||||||
@ -70,6 +78,8 @@ export const SigningPageView = ({ document, recipient, fields }: SigningPageView
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<DocumentReadOnlyFields fields={completedFields} />
|
||||||
|
|
||||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||||
{fields.map((field) =>
|
{fields.map((field) =>
|
||||||
match(field.type)
|
match(field.type)
|
||||||
|
|||||||
@ -1,12 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRef, useState } from 'react';
|
|
||||||
|
|
||||||
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
import { getRecipientType } from '@documenso/lib/client-only/recipient-type';
|
||||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||||
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
import { recipientAbbreviation } from '@documenso/lib/utils/recipient-formatter';
|
||||||
import type { DocumentStatus, Recipient } from '@documenso/prisma/client';
|
import type { DocumentStatus, 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 { AvatarWithRecipient } from './avatar-with-recipient';
|
||||||
import { StackAvatar } from './stack-avatar';
|
import { StackAvatar } from './stack-avatar';
|
||||||
@ -25,11 +23,6 @@ export const StackAvatarsWithTooltip = ({
|
|||||||
position,
|
position,
|
||||||
children,
|
children,
|
||||||
}: StackAvatarsWithTooltipProps) => {
|
}: StackAvatarsWithTooltipProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const isControlled = useRef(false);
|
|
||||||
const isMouseOverTimeout = useRef<NodeJS.Timeout | null>(null);
|
|
||||||
|
|
||||||
const waitingRecipients = recipients.filter(
|
const waitingRecipients = recipients.filter(
|
||||||
(recipient) => getRecipientType(recipient) === 'waiting',
|
(recipient) => getRecipientType(recipient) === 'waiting',
|
||||||
);
|
);
|
||||||
@ -46,117 +39,74 @@ export const StackAvatarsWithTooltip = ({
|
|||||||
(recipient) => getRecipientType(recipient) === 'unsigned',
|
(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 (
|
return (
|
||||||
<Popover open={open} onOpenChange={onOpenChange}>
|
<PopoverHover
|
||||||
<PopoverTrigger
|
trigger={children || <StackAvatars recipients={recipients} />}
|
||||||
className="flex cursor-pointer"
|
contentProps={{
|
||||||
onMouseEnter={onMouseEnter}
|
className: 'flex flex-col gap-y-5 py-2',
|
||||||
onMouseLeave={onMouseLeave}
|
side: position,
|
||||||
>
|
}}
|
||||||
{children || <StackAvatars recipients={recipients} />}
|
>
|
||||||
</PopoverTrigger>
|
{completedRecipients.length > 0 && (
|
||||||
|
<div>
|
||||||
<PopoverContent
|
<h1 className="text-base font-medium">Completed</h1>
|
||||||
side={position}
|
{completedRecipients.map((recipient: Recipient) => (
|
||||||
onMouseEnter={onMouseEnter}
|
<div key={recipient.id} className="my-1 flex items-center gap-2">
|
||||||
onMouseLeave={onMouseLeave}
|
<StackAvatar
|
||||||
className="flex flex-col gap-y-5 py-2"
|
first={true}
|
||||||
>
|
key={recipient.id}
|
||||||
{completedRecipients.length > 0 && (
|
type={getRecipientType(recipient)}
|
||||||
<div>
|
fallbackText={recipientAbbreviation(recipient)}
|
||||||
<h1 className="text-base font-medium">Completed</h1>
|
/>
|
||||||
{completedRecipients.map((recipient: Recipient) => (
|
<div className="">
|
||||||
<div key={recipient.id} className="my-1 flex items-center gap-2">
|
<p className="text-muted-foreground text-sm">{recipient.email}</p>
|
||||||
<StackAvatar
|
<p className="text-muted-foreground/70 text-xs">
|
||||||
first={true}
|
{RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName}
|
||||||
key={recipient.id}
|
</p>
|
||||||
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>
|
||||||
</div>
|
))}
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{waitingRecipients.length > 0 && (
|
{waitingRecipients.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-base font-medium">Waiting</h1>
|
<h1 className="text-base font-medium">Waiting</h1>
|
||||||
{waitingRecipients.map((recipient: Recipient) => (
|
{waitingRecipients.map((recipient: Recipient) => (
|
||||||
<AvatarWithRecipient
|
<AvatarWithRecipient
|
||||||
key={recipient.id}
|
key={recipient.id}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
documentStatus={documentStatus}
|
documentStatus={documentStatus}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{openedRecipients.length > 0 && (
|
{openedRecipients.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-base font-medium">Opened</h1>
|
<h1 className="text-base font-medium">Opened</h1>
|
||||||
{openedRecipients.map((recipient: Recipient) => (
|
{openedRecipients.map((recipient: Recipient) => (
|
||||||
<AvatarWithRecipient
|
<AvatarWithRecipient
|
||||||
key={recipient.id}
|
key={recipient.id}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
documentStatus={documentStatus}
|
documentStatus={documentStatus}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{uncompletedRecipients.length > 0 && (
|
{uncompletedRecipients.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-base font-medium">Uncompleted</h1>
|
<h1 className="text-base font-medium">Uncompleted</h1>
|
||||||
{uncompletedRecipients.map((recipient: Recipient) => (
|
{uncompletedRecipients.map((recipient: Recipient) => (
|
||||||
<AvatarWithRecipient
|
<AvatarWithRecipient
|
||||||
key={recipient.id}
|
key={recipient.id}
|
||||||
recipient={recipient}
|
recipient={recipient}
|
||||||
documentStatus={documentStatus}
|
documentStatus={documentStatus}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</PopoverContent>
|
</PopoverHover>
|
||||||
</Popover>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
112
apps/web/src/components/document/document-read-only-fields.tsx
Normal file
112
apps/web/src/components/document/document-read-only-fields.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
'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 break-all text-sm">
|
||||||
|
{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) },
|
||||||
|
() => field.customText,
|
||||||
|
)
|
||||||
|
.with({ type: FieldType.DATE }, () =>
|
||||||
|
convertToLocalSystemFormat(
|
||||||
|
field.customText,
|
||||||
|
documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT,
|
||||||
|
documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with({ type: FieldType.FREE_SIGNATURE }, () => null)
|
||||||
|
.exhaustive()}
|
||||||
|
</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);
|
const documentRecipient = document.Recipient.find((doc) => doc.email === recipient?.email);
|
||||||
|
|
||||||
|
if (!documentRecipient) {
|
||||||
|
throw new Error('Recipient not found.');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: field.type,
|
type: field.type,
|
||||||
page: field.page,
|
page: field.page,
|
||||||
@ -99,7 +103,7 @@ export const createDocumentFromTemplate = async ({
|
|||||||
customText: field.customText,
|
customText: field.customText,
|
||||||
inserted: field.inserted,
|
inserted: field.inserted,
|
||||||
documentId: document.id,
|
documentId: document.id,
|
||||||
recipientId: documentRecipient?.id || null,
|
recipientId: documentRecipient.id,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -81,6 +81,10 @@ export const duplicateTemplate = async ({
|
|||||||
(doc) => doc.email === recipient?.email,
|
(doc) => doc.email === recipient?.email,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!duplicatedTemplateRecipient) {
|
||||||
|
throw new Error('Recipient not found.');
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: field.type,
|
type: field.type,
|
||||||
page: field.page,
|
page: field.page,
|
||||||
@ -91,7 +95,7 @@ export const duplicateTemplate = async ({
|
|||||||
customText: field.customText,
|
customText: field.customText,
|
||||||
inserted: field.inserted,
|
inserted: field.inserted,
|
||||||
templateId: duplicatedTemplate.id,
|
templateId: duplicatedTemplate.id,
|
||||||
recipientId: duplicatedTemplateRecipient?.id || null,
|
recipientId: duplicatedTemplateRecipient.id,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,6 +4,7 @@ export const getWebhooksByUserId = async (userId: number) => {
|
|||||||
return await prisma.webhook.findMany({
|
return await prisma.webhook.findMany({
|
||||||
where: {
|
where: {
|
||||||
userId,
|
userId,
|
||||||
|
teamId: null,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'desc',
|
createdAt: 'desc',
|
||||||
|
|||||||
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,11 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Made the column `recipientId` on table `Field` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- Drop all Fields where the recipientId is null
|
||||||
|
DELETE FROM "Field" WHERE "recipientId" IS NULL;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Field" ALTER COLUMN "recipientId" SET NOT NULL;
|
||||||
@ -387,7 +387,7 @@ model Field {
|
|||||||
secondaryId String @unique @default(cuid())
|
secondaryId String @unique @default(cuid())
|
||||||
documentId Int?
|
documentId Int?
|
||||||
templateId Int?
|
templateId Int?
|
||||||
recipientId Int?
|
recipientId Int
|
||||||
type FieldType
|
type FieldType
|
||||||
page Int
|
page Int
|
||||||
positionX Decimal @default(0)
|
positionX Decimal @default(0)
|
||||||
@ -398,7 +398,7 @@ model Field {
|
|||||||
inserted Boolean
|
inserted Boolean
|
||||||
Document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
Document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||||
Template Template? @relation(fields: [templateId], 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?
|
Signature Signature?
|
||||||
|
|
||||||
@@index([documentId])
|
@@index([documentId])
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { DateTime } from 'luxon';
|
|||||||
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
import { getServerLimits } from '@documenso/ee/server-only/limits/server';
|
||||||
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
import { NEXT_PUBLIC_WEBAPP_URL } from '@documenso/lib/constants/app';
|
||||||
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
|
import { DOCUMENSO_ENCRYPTION_KEY } from '@documenso/lib/constants/crypto';
|
||||||
|
import { AppError } from '@documenso/lib/errors/app-error';
|
||||||
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
|
import { encryptSecondaryData } from '@documenso/lib/server-only/crypto/encrypt';
|
||||||
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
|
import { upsertDocumentMeta } from '@documenso/lib/server-only/document-meta/upsert-document-meta';
|
||||||
import { createDocument } from '@documenso/lib/server-only/document/create-document';
|
import { createDocument } from '@documenso/lib/server-only/document/create-document';
|
||||||
@ -20,6 +21,7 @@ import { updateDocumentSettings } from '@documenso/lib/server-only/document/upda
|
|||||||
import { updateTitle } from '@documenso/lib/server-only/document/update-title';
|
import { updateTitle } from '@documenso/lib/server-only/document/update-title';
|
||||||
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
|
import { symmetricEncrypt } from '@documenso/lib/universal/crypto';
|
||||||
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
import { extractNextApiRequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||||
|
import { DocumentStatus } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { authenticatedProcedure, procedure, router } from '../trpc';
|
import { authenticatedProcedure, procedure, router } from '../trpc';
|
||||||
import {
|
import {
|
||||||
@ -221,10 +223,6 @@ export const documentRouter = router({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
getDocumentMetaById: authenticatedProcedure
|
|
||||||
.input(ZSetSettingsForDocumentMutationSchema)
|
|
||||||
.mutation(async ({ input, ctx }) => {}),
|
|
||||||
|
|
||||||
setTitleForDocument: authenticatedProcedure
|
setTitleForDocument: authenticatedProcedure
|
||||||
.input(ZSetTitleForDocumentMutationSchema)
|
.input(ZSetTitleForDocumentMutationSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
@ -417,6 +415,10 @@ export const documentRouter = router({
|
|||||||
teamId,
|
teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (document.status !== DocumentStatus.COMPLETED) {
|
||||||
|
throw new AppError('DOCUMENT_NOT_COMPLETE');
|
||||||
|
}
|
||||||
|
|
||||||
const encrypted = encryptSecondaryData({
|
const encrypted = encryptSecondaryData({
|
||||||
data: document.id.toString(),
|
data: document.id.toString(),
|
||||||
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
|
expiresAt: DateTime.now().plus({ minutes: 5 }).toJSDate().valueOf(),
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export type FieldContainerPortalProps = {
|
|||||||
field: Field;
|
field: Field;
|
||||||
className?: string;
|
className?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
|
cardClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FieldContainerPortal({
|
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 [isValidating, setIsValidating] = useState(false);
|
||||||
|
|
||||||
const ref = React.useRef<HTMLDivElement>(null);
|
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,
|
'border-orange-300 ring-1 ring-orange-300': !field.inserted && isValidating,
|
||||||
},
|
},
|
||||||
|
cardClassName,
|
||||||
)}
|
)}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
data-inserted={field.inserted ? 'true' : 'false'}
|
data-inserted={field.inserted ? 'true' : 'false'}
|
||||||
|
|||||||
@ -9,7 +9,11 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats';
|
||||||
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
|
import { DOCUMENT_AUTH_TYPES } from '@documenso/lib/constants/document-auth';
|
||||||
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones';
|
||||||
import { DocumentAccessAuth, DocumentActionAuth } from '@documenso/lib/types/document-auth';
|
import {
|
||||||
|
DocumentAccessAuth,
|
||||||
|
DocumentActionAuth,
|
||||||
|
DocumentAuth,
|
||||||
|
} from '@documenso/lib/types/document-auth';
|
||||||
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
import { extractDocumentAuthMethods } from '@documenso/lib/utils/document-auth';
|
||||||
import { DocumentStatus, type Field, type Recipient, SendStatus } from '@documenso/prisma/client';
|
import { DocumentStatus, type Field, type Recipient, SendStatus } from '@documenso/prisma/client';
|
||||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||||
@ -216,9 +220,9 @@ export const AddSettingsFormPartial = ({
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul className="ml-3.5 list-outside list-disc space-y-0.5 py-2">
|
<ul className="ml-3.5 list-outside list-disc space-y-0.5 py-2">
|
||||||
<li>
|
{/* <li>
|
||||||
<strong>Require account</strong> - The recipient must be signed in
|
<strong>Require account</strong> - The recipient must be signed in
|
||||||
</li>
|
</li> */}
|
||||||
<li>
|
<li>
|
||||||
<strong>Require passkey</strong> - The recipient must have an account
|
<strong>Require passkey</strong> - The recipient must have an account
|
||||||
and passkey configured via their settings
|
and passkey configured via their settings
|
||||||
@ -242,11 +246,13 @@ export const AddSettingsFormPartial = ({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
||||||
<SelectContent position="popper">
|
<SelectContent position="popper">
|
||||||
{Object.values(DocumentActionAuth).map((authType) => (
|
{Object.values(DocumentActionAuth)
|
||||||
<SelectItem key={authType} value={authType}>
|
.filter((auth) => auth !== DocumentAuth.ACCOUNT)
|
||||||
{DOCUMENT_AUTH_TYPES[authType].value}
|
.map((authType) => (
|
||||||
</SelectItem>
|
<SelectItem key={authType} value={authType}>
|
||||||
))}
|
{DOCUMENT_AUTH_TYPES[authType].value}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
|
||||||
{/* Note: -1 is remapped in the Zod schema to the required value. */}
|
{/* Note: -1 is remapped in the Zod schema to the required value. */}
|
||||||
<SelectItem value={'-1'}>None</SelectItem>
|
<SelectItem value={'-1'}>None</SelectItem>
|
||||||
|
|||||||
@ -28,8 +28,6 @@ export const ZAddSettingsFormSchema = z.object({
|
|||||||
ZDocumentActionAuthTypesSchema.optional(),
|
ZDocumentActionAuthTypesSchema.optional(),
|
||||||
),
|
),
|
||||||
meta: z.object({
|
meta: z.object({
|
||||||
subject: z.string().optional(),
|
|
||||||
message: z.string().optional(),
|
|
||||||
timezone: z.string().optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
timezone: z.string().optional().default(DEFAULT_DOCUMENT_TIME_ZONE),
|
||||||
dateFormat: z.string().optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
dateFormat: z.string().optional().default(DEFAULT_DOCUMENT_DATE_FORMAT),
|
||||||
redirectUrl: z
|
redirectUrl: z
|
||||||
|
|||||||
@ -302,10 +302,10 @@ export const AddSignersFormPartial = ({
|
|||||||
global action signing authentication method configured in
|
global action signing authentication method configured in
|
||||||
the "General Settings" step
|
the "General Settings" step
|
||||||
</li>
|
</li>
|
||||||
<li>
|
{/* <li>
|
||||||
<strong>Require account</strong> - The recipient must be
|
<strong>Require account</strong> - The recipient must be
|
||||||
signed in
|
signed in
|
||||||
</li>
|
</li> */}
|
||||||
<li>
|
<li>
|
||||||
<strong>Require passkey</strong> - The recipient must have
|
<strong>Require passkey</strong> - The recipient must have
|
||||||
an account and passkey configured via their settings
|
an account and passkey configured via their settings
|
||||||
@ -326,11 +326,13 @@ export const AddSignersFormPartial = ({
|
|||||||
{/* Note: -1 is remapped in the Zod schema to the required value. */}
|
{/* Note: -1 is remapped in the Zod schema to the required value. */}
|
||||||
<SelectItem value="-1">Inherit authentication method</SelectItem>
|
<SelectItem value="-1">Inherit authentication method</SelectItem>
|
||||||
|
|
||||||
{Object.values(RecipientActionAuth).map((authType) => (
|
{Object.values(RecipientActionAuth)
|
||||||
<SelectItem key={authType} value={authType}>
|
.filter((auth) => auth !== RecipientActionAuth.ACCOUNT)
|
||||||
{DOCUMENT_AUTH_TYPES[authType].value}
|
.map((authType) => (
|
||||||
</SelectItem>
|
<SelectItem key={authType} value={authType}>
|
||||||
))}
|
{DOCUMENT_AUTH_TYPES[authType].value}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
|
|
||||||
@ -32,8 +30,6 @@ export type AddSubjectFormProps = {
|
|||||||
document: DocumentWithData;
|
document: DocumentWithData;
|
||||||
onSubmit: (_data: TAddSubjectFormSchema) => void;
|
onSubmit: (_data: TAddSubjectFormSchema) => void;
|
||||||
isDocumentPdfLoaded: boolean;
|
isDocumentPdfLoaded: boolean;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
setSubjectFormFields: (subject?: string, message?: string) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AddSubjectFormPartial = ({
|
export const AddSubjectFormPartial = ({
|
||||||
@ -43,12 +39,10 @@ export const AddSubjectFormPartial = ({
|
|||||||
document,
|
document,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
isDocumentPdfLoaded,
|
isDocumentPdfLoaded,
|
||||||
setSubjectFormFields,
|
|
||||||
}: AddSubjectFormProps) => {
|
}: AddSubjectFormProps) => {
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
getValues,
|
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm<TAddSubjectFormSchema>({
|
} = useForm<TAddSubjectFormSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@ -63,13 +57,6 @@ export const AddSubjectFormPartial = ({
|
|||||||
const onFormSubmit = handleSubmit(onSubmit);
|
const onFormSubmit = handleSubmit(onSubmit);
|
||||||
const { currentStep, totalSteps, previousStep } = useStep();
|
const { currentStep, totalSteps, previousStep } = useStep();
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
const { meta } = getValues();
|
|
||||||
setSubjectFormFields(meta.subject, meta.message);
|
|
||||||
};
|
|
||||||
}, [getValues, setSubjectFormFields]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DocumentFlowFormContainerHeader
|
<DocumentFlowFormContainerHeader
|
||||||
|
|||||||
@ -30,4 +30,66 @@ const PopoverContent = React.forwardRef<
|
|||||||
|
|
||||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
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