mirror of
https://github.com/documenso/documenso.git
synced 2025-11-12 15:53:02 +10:00
feat: add recipient roles (#716)
Fixes #705 --------- Co-authored-by: Lucas Smith <me@lucasjamessmith.me> Co-authored-by: David Nguyen <davidngu28@gmail.com>
This commit is contained in:
@ -158,6 +158,7 @@ export const SinglePlayerClient = () => {
|
||||
readStatus: 'OPENED',
|
||||
signingStatus: 'NOT_SIGNED',
|
||||
sendStatus: 'NOT_SENT',
|
||||
role: 'SIGNER',
|
||||
};
|
||||
|
||||
const onFileDrop = async (file: File) => {
|
||||
|
||||
@ -2,13 +2,13 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Download, Edit, Pencil } from 'lucide-react';
|
||||
import { CheckCircle, Download, Edit, EyeIcon, Pencil } from 'lucide-react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||
import type { Document, Recipient, User } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@ -37,6 +37,7 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
|
||||
const isPending = row.status === DocumentStatus.PENDING;
|
||||
const isComplete = row.status === DocumentStatus.COMPLETED;
|
||||
const isSigned = recipient?.signingStatus === SigningStatus.SIGNED;
|
||||
const role = recipient?.role;
|
||||
|
||||
const onDownloadClick = async () => {
|
||||
try {
|
||||
@ -68,6 +69,11 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Consider if want to keep this logic for hiding viewing for CC'ers
|
||||
if (recipient?.role === RecipientRole.CC && isComplete === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return match({
|
||||
isOwner,
|
||||
isRecipient,
|
||||
@ -87,15 +93,32 @@ export const DataTableActionButton = ({ row }: DataTableActionButtonProps) => {
|
||||
.with({ isRecipient: true, isPending: true, isSigned: false }, () => (
|
||||
<Button className="w-32" asChild>
|
||||
<Link href={`/sign/${recipient?.token}`}>
|
||||
{match(role)
|
||||
.with(RecipientRole.SIGNER, () => (
|
||||
<>
|
||||
<Pencil className="-ml-1 mr-2 h-4 w-4" />
|
||||
Sign
|
||||
</>
|
||||
))
|
||||
.with(RecipientRole.APPROVER, () => (
|
||||
<>
|
||||
<CheckCircle className="-ml-1 mr-2 h-4 w-4" />
|
||||
Approve
|
||||
</>
|
||||
))
|
||||
.otherwise(() => (
|
||||
<>
|
||||
<EyeIcon className="-ml-1 mr-2 h-4 w-4" />
|
||||
View
|
||||
</>
|
||||
))}
|
||||
</Link>
|
||||
</Button>
|
||||
))
|
||||
.with({ isPending: true, isSigned: true }, () => (
|
||||
<Button className="w-32" disabled={true}>
|
||||
<Pencil className="-ml-1 mr-2 inline h-4 w-4" />
|
||||
Sign
|
||||
<EyeIcon className="-ml-1 mr-2 h-4 w-4" />
|
||||
View
|
||||
</Button>
|
||||
))
|
||||
.with({ isComplete: true }, () => (
|
||||
|
||||
@ -5,9 +5,11 @@ import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import {
|
||||
CheckCircle,
|
||||
Copy,
|
||||
Download,
|
||||
Edit,
|
||||
EyeIcon,
|
||||
Loader,
|
||||
MoreHorizontal,
|
||||
Pencil,
|
||||
@ -19,7 +21,7 @@ import { useSession } from 'next-auth/react';
|
||||
|
||||
import { downloadPDF } from '@documenso/lib/client-only/download-pdf';
|
||||
import type { Document, Recipient, User } from '@documenso/prisma/client';
|
||||
import { DocumentStatus } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, RecipientRole } from '@documenso/prisma/client';
|
||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||
import { trpc as trpcClient } from '@documenso/trpc/client';
|
||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||
@ -105,12 +107,32 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) =
|
||||
<DropdownMenuContent className="w-52" align="start" forceMount>
|
||||
<DropdownMenuLabel>Action</DropdownMenuLabel>
|
||||
|
||||
{recipient?.role !== RecipientRole.CC && (
|
||||
<DropdownMenuItem disabled={!recipient || isComplete} asChild>
|
||||
<Link href={`/sign/${recipient?.token}`}>
|
||||
{recipient?.role === RecipientRole.VIEWER && (
|
||||
<>
|
||||
<EyeIcon className="mr-2 h-4 w-4" />
|
||||
View
|
||||
</>
|
||||
)}
|
||||
|
||||
{recipient?.role === RecipientRole.SIGNER && (
|
||||
<>
|
||||
<Pencil className="mr-2 h-4 w-4" />
|
||||
Sign
|
||||
</>
|
||||
)}
|
||||
|
||||
{recipient?.role === RecipientRole.APPROVER && (
|
||||
<>
|
||||
<CheckCircle className="mr-2 h-4 w-4" />
|
||||
Approve
|
||||
</>
|
||||
)}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
<DropdownMenuItem disabled={!isOwner || isComplete} asChild>
|
||||
<Link href={`/documents/${row.id}`}>
|
||||
|
||||
@ -10,7 +10,7 @@ import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document
|
||||
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';
|
||||
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button';
|
||||
import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button';
|
||||
import { SigningCard3D } from '@documenso/ui/components/signing-card';
|
||||
@ -94,7 +94,10 @@ export default async function CompletedSigningPage({
|
||||
))}
|
||||
|
||||
<h2 className="mt-6 max-w-[35ch] text-center text-2xl font-semibold leading-normal md:text-3xl lg:text-4xl">
|
||||
You have signed
|
||||
You have
|
||||
{recipient.role === RecipientRole.SIGNER && ' signed '}
|
||||
{recipient.role === RecipientRole.VIEWER && ' viewed '}
|
||||
{recipient.role === RecipientRole.APPROVER && ' approved '}
|
||||
<span className="mt-1.5 block">"{truncatedTitle}"</span>
|
||||
</h2>
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import { useForm } from 'react-hook-form';
|
||||
|
||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||
import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields';
|
||||
import type { Document, Field, Recipient } from '@documenso/prisma/client';
|
||||
import { type Document, type Field, type Recipient, RecipientRole } from '@documenso/prisma/client';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { FieldToolTip } from '@documenso/ui/components/field/field-tooltip';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
@ -96,15 +96,52 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
||||
|
||||
<fieldset
|
||||
disabled={isSubmitting}
|
||||
className={cn('-mx-2 flex flex-1 flex-col overflow-hidden px-2')}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'custom-scrollbar -mx-2 flex flex-1 flex-col overflow-y-auto overflow-x-hidden px-2',
|
||||
)}
|
||||
>
|
||||
<h3 className="text-foreground text-2xl font-semibold">Sign Document</h3>
|
||||
<div className={cn('flex flex-1 flex-col')}>
|
||||
<h3 className="text-foreground text-2xl font-semibold">
|
||||
{recipient.role === RecipientRole.VIEWER && 'View Document'}
|
||||
{recipient.role === RecipientRole.SIGNER && 'Sign Document'}
|
||||
{recipient.role === RecipientRole.APPROVER && 'Approve Document'}
|
||||
</h3>
|
||||
|
||||
{recipient.role === RecipientRole.VIEWER ? (
|
||||
<>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
Please mark as viewed to complete
|
||||
</p>
|
||||
|
||||
<hr className="border-border mb-8 mt-4" />
|
||||
|
||||
<div className="-mx-2 flex flex-1 flex-col gap-4 overflow-y-auto px-2">
|
||||
<div className="flex flex-1 flex-col gap-y-4" />
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<Button
|
||||
type="button"
|
||||
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<SignDialog
|
||||
isSubmitting={isSubmitting}
|
||||
onSignatureComplete={handleSubmit(onFormSubmit)}
|
||||
document={document}
|
||||
fields={fields}
|
||||
fieldsValidated={fieldsValidated}
|
||||
role={recipient.role}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-muted-foreground mt-2 text-sm">
|
||||
Please review the document before signing.
|
||||
</p>
|
||||
@ -161,9 +198,12 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) =
|
||||
document={document}
|
||||
fields={fields}
|
||||
fieldsValidated={fieldsValidated}
|
||||
role={recipient.role}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
@ -14,7 +14,7 @@ import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-f
|
||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||
import { symmetricDecrypt } from '@documenso/lib/universal/crypto';
|
||||
import { DocumentStatus, FieldType, SigningStatus } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, FieldType, RecipientRole, SigningStatus } 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';
|
||||
@ -110,7 +110,10 @@ export default async function SigningPage({ params: { token } }: SigningPageProp
|
||||
|
||||
<div className="mt-2.5 flex items-center gap-x-6">
|
||||
<p className="text-muted-foreground">
|
||||
{document.User.name} ({document.User.email}) has invited you to sign this document.
|
||||
{document.User.name} ({document.User.email}) has invited you to{' '}
|
||||
{recipient.role === RecipientRole.VIEWER && 'view'}
|
||||
{recipient.role === RecipientRole.SIGNER && 'sign'}
|
||||
{recipient.role === RecipientRole.APPROVER && 'approve'} this document.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { Document, Field } from '@documenso/prisma/client';
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
@ -17,6 +18,7 @@ export type SignDialogProps = {
|
||||
fields: Field[];
|
||||
fieldsValidated: () => void | Promise<void>;
|
||||
onSignatureComplete: () => void | Promise<void>;
|
||||
role: RecipientRole;
|
||||
};
|
||||
|
||||
export const SignDialog = ({
|
||||
@ -25,6 +27,7 @@ export const SignDialog = ({
|
||||
fields,
|
||||
fieldsValidated,
|
||||
onSignatureComplete,
|
||||
role,
|
||||
}: SignDialogProps) => {
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const truncatedTitle = truncateTitle(document.title);
|
||||
@ -45,9 +48,18 @@ export const SignDialog = ({
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<div className="text-center">
|
||||
<div className="text-foreground text-xl font-semibold">Sign Document</div>
|
||||
<div className="text-foreground text-xl font-semibold">
|
||||
{role === RecipientRole.VIEWER && 'Mark Document as Viewed'}
|
||||
{role === RecipientRole.SIGNER && 'Sign Document'}
|
||||
{role === RecipientRole.APPROVER && 'Approve Document'}
|
||||
</div>
|
||||
<div className="text-muted-foreground mx-auto w-4/5 py-2 text-center">
|
||||
You are about to finish signing "{truncatedTitle}". Are you sure?
|
||||
{role === RecipientRole.VIEWER &&
|
||||
`You are about to finish viewing "${truncatedTitle}". Are you sure?`}
|
||||
{role === RecipientRole.SIGNER &&
|
||||
`You are about to finish signing "${truncatedTitle}". Are you sure?`}
|
||||
{role === RecipientRole.APPROVER &&
|
||||
`You are about to finish approving "${truncatedTitle}". Are you sure?`}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -71,7 +83,9 @@ export const SignDialog = ({
|
||||
loading={isSubmitting}
|
||||
onClick={onSignatureComplete}
|
||||
>
|
||||
Sign
|
||||
{role === RecipientRole.VIEWER && 'Mark as Viewed'}
|
||||
{role === RecipientRole.SIGNER && 'Sign'}
|
||||
{role === RecipientRole.APPROVER && 'Approve'}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
|
||||
@ -4,6 +4,7 @@ import React from 'react';
|
||||
|
||||
import { useCopyToClipboard } from '@documenso/lib/client-only/hooks/use-copy-to-clipboard';
|
||||
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 { cn } from '@documenso/ui/lib/utils';
|
||||
@ -47,8 +48,17 @@ export function AvatarWithRecipient({ recipient }: AvatarWithRecipientProps) {
|
||||
type={getRecipientType(recipient)}
|
||||
fallbackText={recipientAbbreviation(recipient)}
|
||||
/>
|
||||
|
||||
<span className="text-muted-foreground text-sm">{recipient.email}</span>
|
||||
<div>
|
||||
<div
|
||||
className="text-muted-foreground text-sm"
|
||||
title="Click to copy signing link for sending to recipient"
|
||||
>
|
||||
<p>{recipient.email} </p>
|
||||
<p className="text-muted-foreground/70 text-xs">
|
||||
{RECIPIENT_ROLES_DESCRIPTION[recipient.role].roleName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
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 {
|
||||
@ -59,7 +60,12 @@ export const StackAvatarsWithTooltip = ({
|
||||
type={getRecipientType(recipient)}
|
||||
fallbackText={recipientAbbreviation(recipient)}
|
||||
/>
|
||||
<span className="text-muted-foreground text-sm">{recipient.email}</span>
|
||||
<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>
|
||||
|
||||
@ -1,3 +1,6 @@
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import type { RecipientRole } from '@documenso/prisma/client';
|
||||
|
||||
import { Button, Section, Text } from '../components';
|
||||
import { TemplateDocumentImage } from './template-document-image';
|
||||
|
||||
@ -7,6 +10,7 @@ export interface TemplateDocumentInviteProps {
|
||||
documentName: string;
|
||||
signDocumentLink: string;
|
||||
assetBaseUrl: string;
|
||||
role: RecipientRole;
|
||||
}
|
||||
|
||||
export const TemplateDocumentInvite = ({
|
||||
@ -14,19 +18,22 @@ export const TemplateDocumentInvite = ({
|
||||
documentName,
|
||||
signDocumentLink,
|
||||
assetBaseUrl,
|
||||
role,
|
||||
}: TemplateDocumentInviteProps) => {
|
||||
const { actionVerb, progressiveVerb } = RECIPIENT_ROLES_DESCRIPTION[role];
|
||||
|
||||
return (
|
||||
<>
|
||||
<TemplateDocumentImage className="mt-6" assetBaseUrl={assetBaseUrl} />
|
||||
|
||||
<Section>
|
||||
<Text className="text-primary mx-auto mb-0 max-w-[80%] text-center text-lg font-semibold">
|
||||
{inviterName} has invited you to sign
|
||||
{inviterName} has invited you to {actionVerb.toLowerCase()}
|
||||
<br />"{documentName}"
|
||||
</Text>
|
||||
|
||||
<Text className="my-1 text-center text-base text-slate-400">
|
||||
Continue by signing the document.
|
||||
Continue by {progressiveVerb.toLowerCase()} the document.
|
||||
</Text>
|
||||
|
||||
<Section className="mb-6 mt-8 text-center">
|
||||
@ -34,7 +41,7 @@ export const TemplateDocumentInvite = ({
|
||||
className="bg-documenso-500 inline-flex items-center justify-center rounded-lg px-6 py-3 text-center text-sm font-medium text-black no-underline"
|
||||
href={signDocumentLink}
|
||||
>
|
||||
Sign Document
|
||||
{actionVerb} Document
|
||||
</Button>
|
||||
</Section>
|
||||
</Section>
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import type { RecipientRole } from '@documenso/prisma/client';
|
||||
import config from '@documenso/tailwind-config';
|
||||
|
||||
import {
|
||||
@ -19,6 +21,7 @@ import { TemplateFooter } from '../template-components/template-footer';
|
||||
|
||||
export type DocumentInviteEmailTemplateProps = Partial<TemplateDocumentInviteProps> & {
|
||||
customBody?: string;
|
||||
role: RecipientRole;
|
||||
};
|
||||
|
||||
export const DocumentInviteEmailTemplate = ({
|
||||
@ -28,8 +31,11 @@ export const DocumentInviteEmailTemplate = ({
|
||||
signDocumentLink = 'https://documenso.com',
|
||||
assetBaseUrl = 'http://localhost:3002',
|
||||
customBody,
|
||||
role,
|
||||
}: DocumentInviteEmailTemplateProps) => {
|
||||
const previewText = `${inviterName} has invited you to sign ${documentName}`;
|
||||
const action = RECIPIENT_ROLES_DESCRIPTION[role].actionVerb.toLowerCase();
|
||||
|
||||
const previewText = `${inviterName} has invited you to ${action} ${documentName}`;
|
||||
|
||||
const getAssetUrl = (path: string) => {
|
||||
return new URL(path, assetBaseUrl).toString();
|
||||
@ -64,6 +70,7 @@ export const DocumentInviteEmailTemplate = ({
|
||||
documentName={documentName}
|
||||
signDocumentLink={signDocumentLink}
|
||||
assetBaseUrl={assetBaseUrl}
|
||||
role={role}
|
||||
/>
|
||||
</Section>
|
||||
</Container>
|
||||
@ -81,7 +88,7 @@ export const DocumentInviteEmailTemplate = ({
|
||||
{customBody ? (
|
||||
<pre className="font-sans text-base text-slate-400">{customBody}</pre>
|
||||
) : (
|
||||
`${inviterName} has invited you to sign the document "${documentName}".`
|
||||
`${inviterName} has invited you to ${action} the document "${documentName}".`
|
||||
)}
|
||||
</Text>
|
||||
</Section>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import type { Recipient } from '@documenso/prisma/client';
|
||||
import { ReadStatus, SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
import { ReadStatus, RecipientRole, SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
export const getRecipientType = (recipient: Recipient) => {
|
||||
if (
|
||||
recipient.sendStatus === SendStatus.SENT &&
|
||||
recipient.signingStatus === SigningStatus.SIGNED
|
||||
recipient.role === RecipientRole.CC ||
|
||||
(recipient.sendStatus === SendStatus.SENT && recipient.signingStatus === SigningStatus.SIGNED)
|
||||
) {
|
||||
return 'completed';
|
||||
}
|
||||
|
||||
26
packages/lib/constants/recipient-roles.ts
Normal file
26
packages/lib/constants/recipient-roles.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
|
||||
export const RECIPIENT_ROLES_DESCRIPTION: {
|
||||
[key in RecipientRole]: { actionVerb: string; progressiveVerb: string; roleName: string };
|
||||
} = {
|
||||
[RecipientRole.APPROVER]: {
|
||||
actionVerb: 'Approve',
|
||||
progressiveVerb: 'Approving',
|
||||
roleName: 'Approver',
|
||||
},
|
||||
[RecipientRole.CC]: {
|
||||
actionVerb: 'CC',
|
||||
progressiveVerb: 'CC',
|
||||
roleName: 'CC',
|
||||
},
|
||||
[RecipientRole.SIGNER]: {
|
||||
actionVerb: 'Sign',
|
||||
progressiveVerb: 'Signing',
|
||||
roleName: 'Signer',
|
||||
},
|
||||
[RecipientRole.VIEWER]: {
|
||||
actionVerb: 'View',
|
||||
progressiveVerb: 'Viewing',
|
||||
roleName: 'Viewer',
|
||||
},
|
||||
};
|
||||
@ -3,7 +3,7 @@ import { P, match } from 'ts-pattern';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import type { Document, Prisma } from '@documenso/prisma/client';
|
||||
import { SigningStatus } from '@documenso/prisma/client';
|
||||
import { RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-document-status';
|
||||
|
||||
import type { FindResultSet } from '../../types/find-result-set';
|
||||
@ -87,6 +87,9 @@ export const findDocuments = async ({
|
||||
some: {
|
||||
email: user.email,
|
||||
signingStatus: SigningStatus.NOT_SIGNED,
|
||||
role: {
|
||||
not: RecipientRole.CC,
|
||||
},
|
||||
},
|
||||
},
|
||||
deletedAt: null,
|
||||
@ -109,6 +112,9 @@ export const findDocuments = async ({
|
||||
some: {
|
||||
email: user.email,
|
||||
signingStatus: SigningStatus.SIGNED,
|
||||
role: {
|
||||
not: RecipientRole.CC,
|
||||
},
|
||||
},
|
||||
},
|
||||
deletedAt: null,
|
||||
|
||||
@ -6,7 +6,9 @@ import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document
|
||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '../../constants/recipient-roles';
|
||||
|
||||
export type ResendDocumentOptions = {
|
||||
documentId: number;
|
||||
@ -59,6 +61,10 @@ export const resendDocument = async ({ documentId, userId, recipients }: ResendD
|
||||
|
||||
await Promise.all(
|
||||
document.Recipient.map(async (recipient) => {
|
||||
if (recipient.role === RecipientRole.CC) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { email, name } = recipient;
|
||||
|
||||
const customEmailTemplate = {
|
||||
@ -77,8 +83,11 @@ export const resendDocument = async ({ documentId, userId, recipients }: ResendD
|
||||
assetBaseUrl,
|
||||
signDocumentLink,
|
||||
customBody: renderCustomEmailTemplate(customEmail?.message || '', customEmailTemplate),
|
||||
role: recipient.role,
|
||||
});
|
||||
|
||||
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: email,
|
||||
@ -90,7 +99,7 @@ export const resendDocument = async ({ documentId, userId, recipients }: ResendD
|
||||
},
|
||||
subject: customEmail?.subject
|
||||
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
||||
: 'Please sign this document',
|
||||
: `Please ${actionVerb.toLowerCase()} this document`,
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
});
|
||||
|
||||
@ -6,7 +6,7 @@ import { PDFDocument } from 'pdf-lib';
|
||||
|
||||
import PostHogServerClient from '@documenso/lib/server-only/feature-flags/get-post-hog-server-client';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@documenso/prisma/client';
|
||||
import { signPdf } from '@documenso/signing';
|
||||
|
||||
import { getFile } from '../../universal/upload/get-file';
|
||||
@ -44,6 +44,9 @@ export const sealDocument = async ({ documentId, sendEmail = true }: SealDocumen
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
documentId: document.id,
|
||||
role: {
|
||||
not: RecipientRole.CC,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -6,7 +6,9 @@ import { DocumentInviteEmailTemplate } from '@documenso/email/templates/document
|
||||
import { FROM_ADDRESS, FROM_NAME } from '@documenso/lib/constants/email';
|
||||
import { renderCustomEmailTemplate } from '@documenso/lib/utils/render-custom-email-template';
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, RecipientRole, SendStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '../../constants/recipient-roles';
|
||||
|
||||
export type SendDocumentOptions = {
|
||||
documentId: number;
|
||||
@ -47,6 +49,10 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
||||
|
||||
await Promise.all(
|
||||
document.Recipient.map(async (recipient) => {
|
||||
if (recipient.sendStatus === SendStatus.SENT || recipient.role === RecipientRole.CC) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { email, name } = recipient;
|
||||
|
||||
const customEmailTemplate = {
|
||||
@ -55,10 +61,6 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
||||
'document.name': document.title,
|
||||
};
|
||||
|
||||
if (recipient.sendStatus === SendStatus.SENT) {
|
||||
return;
|
||||
}
|
||||
|
||||
const assetBaseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL || 'http://localhost:3000';
|
||||
const signDocumentLink = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/sign/${recipient.token}`;
|
||||
|
||||
@ -69,8 +71,11 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
||||
assetBaseUrl,
|
||||
signDocumentLink,
|
||||
customBody: renderCustomEmailTemplate(customEmail?.message || '', customEmailTemplate),
|
||||
role: recipient.role,
|
||||
});
|
||||
|
||||
const { actionVerb } = RECIPIENT_ROLES_DESCRIPTION[recipient.role];
|
||||
|
||||
await mailer.sendMail({
|
||||
to: {
|
||||
address: email,
|
||||
@ -82,7 +87,7 @@ export const sendDocument = async ({ documentId, userId }: SendDocumentOptions)
|
||||
},
|
||||
subject: customEmail?.subject
|
||||
? renderCustomEmailTemplate(customEmail.subject, customEmailTemplate)
|
||||
: 'Please sign this document',
|
||||
: `Please ${actionVerb.toLowerCase()} this document`,
|
||||
html: render(template),
|
||||
text: render(template, { plainText: true }),
|
||||
});
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
import { SendStatus, SigningStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { nanoid } from '../../universal/id';
|
||||
@ -10,6 +11,7 @@ export interface SetRecipientsForDocumentOptions {
|
||||
id?: number | null;
|
||||
email: string;
|
||||
name: string;
|
||||
role: RecipientRole;
|
||||
}[];
|
||||
}
|
||||
|
||||
@ -79,13 +81,20 @@ export const setRecipientsForDocument = async ({
|
||||
update: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
documentId,
|
||||
signingStatus:
|
||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
},
|
||||
create: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
token: nanoid(),
|
||||
documentId,
|
||||
sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
|
||||
signingStatus:
|
||||
recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
|
||||
},
|
||||
}),
|
||||
),
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "RecipientRole" AS ENUM ('CC', 'SIGNER', 'VIEWER', 'APPROVER');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Recipient" ADD COLUMN "role" "RecipientRole" NOT NULL DEFAULT 'SIGNER';
|
||||
@ -209,6 +209,13 @@ enum SigningStatus {
|
||||
SIGNED
|
||||
}
|
||||
|
||||
enum RecipientRole {
|
||||
CC
|
||||
SIGNER
|
||||
VIEWER
|
||||
APPROVER
|
||||
}
|
||||
|
||||
model Recipient {
|
||||
id Int @id @default(autoincrement())
|
||||
documentId Int?
|
||||
@ -218,6 +225,7 @@ model Recipient {
|
||||
token String
|
||||
expired DateTime?
|
||||
signedAt DateTime?
|
||||
role RecipientRole @default(SIGNER)
|
||||
readStatus ReadStatus @default(NOT_OPENED)
|
||||
signingStatus SigningStatus @default(NOT_SIGNED)
|
||||
sendStatus SendStatus @default(NOT_SENT)
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DocumentStatus, FieldType } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
|
||||
export const ZGetDocumentByIdQuerySchema = z.object({
|
||||
id: z.number().min(1),
|
||||
@ -35,6 +35,7 @@ export const ZSetRecipientsForDocumentMutationSchema = z.object({
|
||||
id: z.number().nullish(),
|
||||
email: z.string().min(1).email(),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
@ -25,6 +25,7 @@ export const recipientRouter = router({
|
||||
id: signer.nativeId,
|
||||
email: signer.email,
|
||||
name: signer.name,
|
||||
role: signer.role,
|
||||
})),
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
|
||||
export const ZAddSignersMutationSchema = z
|
||||
.object({
|
||||
documentId: z.number(),
|
||||
@ -8,6 +10,7 @@ export const ZAddSignersMutationSchema = z
|
||||
nativeId: z.number().optional(),
|
||||
email: z.string().email().min(1),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { Caveat } from 'next/font/google';
|
||||
|
||||
@ -10,8 +10,10 @@ import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
import { FieldType, SendStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
@ -102,6 +104,12 @@ export const AddFieldsFormPartial = ({
|
||||
|
||||
const hasSelectedSignerBeenSent = selectedSigner?.sendStatus === SendStatus.SENT;
|
||||
|
||||
const isFieldsDisabled =
|
||||
!selectedSigner ||
|
||||
hasSelectedSignerBeenSent ||
|
||||
selectedSigner?.role === RecipientRole.VIEWER ||
|
||||
selectedSigner?.role === RecipientRole.CC;
|
||||
|
||||
const [isFieldWithinBounds, setIsFieldWithinBounds] = useState(false);
|
||||
const [coords, setCoords] = useState({
|
||||
x: 0,
|
||||
@ -281,12 +289,28 @@ export const AddFieldsFormPartial = ({
|
||||
setSelectedSigner(recipients.find((r) => r.sendStatus !== SendStatus.SENT) ?? recipients[0]);
|
||||
}, [recipients]);
|
||||
|
||||
const recipientsByRole = useMemo(() => {
|
||||
const recipientsByRole: Record<RecipientRole, Recipient[]> = {
|
||||
CC: [],
|
||||
VIEWER: [],
|
||||
SIGNER: [],
|
||||
APPROVER: [],
|
||||
};
|
||||
|
||||
recipients.forEach((recipient) => {
|
||||
recipientsByRole[recipient.role].push(recipient);
|
||||
});
|
||||
|
||||
return recipientsByRole;
|
||||
}, [recipients]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentFlowFormContainerHeader
|
||||
title={documentFlow.title}
|
||||
description={documentFlow.description}
|
||||
/>
|
||||
|
||||
<DocumentFlowFormContainerContent>
|
||||
<div className="flex flex-col">
|
||||
{selectedField && (
|
||||
@ -351,17 +375,35 @@ export const AddFieldsFormPartial = ({
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput />
|
||||
|
||||
<CommandEmpty>
|
||||
<span className="text-muted-foreground inline-block px-4">
|
||||
No recipient matching this description was found.
|
||||
</span>
|
||||
</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
{recipients.map((recipient, index) => (
|
||||
{Object.entries(recipientsByRole).map(([role, recipients], roleIndex) => (
|
||||
<CommandGroup key={roleIndex}>
|
||||
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
RECIPIENT_ROLES_DESCRIPTION[role as RecipientRole].roleName
|
||||
}
|
||||
</div>
|
||||
|
||||
{recipients.length === 0 && (
|
||||
<div
|
||||
key={`${role}-empty`}
|
||||
className="text-muted-foreground/80 px-4 pb-4 pt-2.5 text-center text-xs"
|
||||
>
|
||||
No recipients with this role
|
||||
</div>
|
||||
)}
|
||||
|
||||
{recipients.map((recipient) => (
|
||||
<CommandItem
|
||||
key={index}
|
||||
className={cn({
|
||||
key={recipient.id}
|
||||
className={cn('!rounded-2xl px-4 last:mb-1 [&:not(:first-child)]:mt-1', {
|
||||
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
|
||||
})}
|
||||
onSelect={() => {
|
||||
@ -369,10 +411,27 @@ export const AddFieldsFormPartial = ({
|
||||
setShowRecipientsSelector(false);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={cn('text-foreground/70 truncate', {
|
||||
'text-foreground': recipient === selectedSigner,
|
||||
})}
|
||||
>
|
||||
{recipient.name && (
|
||||
<span title={`${recipient.name} (${recipient.email})`}>
|
||||
{recipient.name} ({recipient.email})
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!recipient.name && (
|
||||
<span title={recipient.email}>{recipient.email}</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
<div className="ml-auto flex items-center justify-center">
|
||||
{recipient.sendStatus !== SendStatus.SENT ? (
|
||||
<Check
|
||||
aria-hidden={recipient !== selectedSigner}
|
||||
className={cn('mr-2 h-4 w-4 flex-shrink-0', {
|
||||
className={cn('h-4 w-4 flex-shrink-0', {
|
||||
'opacity-0': recipient !== selectedSigner,
|
||||
'opacity-100': recipient === selectedSigner,
|
||||
})}
|
||||
@ -380,43 +439,30 @@ export const AddFieldsFormPartial = ({
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="mr-2 h-4 w-4" />
|
||||
<Info className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
|
||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||
This document has already been sent to this recipient. You can no
|
||||
longer edit this recipient.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{recipient.name && (
|
||||
<span
|
||||
className="truncate"
|
||||
title={`${recipient.name} (${recipient.email})`}
|
||||
>
|
||||
{recipient.name} ({recipient.email})
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!recipient.name && (
|
||||
<span className="truncate" title={recipient.email}>
|
||||
{recipient.email}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
<div className="-mx-2 flex-1 overflow-y-auto px-2">
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-8">
|
||||
<fieldset disabled={isFieldsDisabled} className="grid grid-cols-2 gap-x-4 gap-y-8">
|
||||
<button
|
||||
type="button"
|
||||
className="group h-full w-full"
|
||||
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
|
||||
onClick={() => setSelectedField(FieldType.SIGNATURE)}
|
||||
onMouseDown={() => setSelectedField(FieldType.SIGNATURE)}
|
||||
data-selected={selectedField === FieldType.SIGNATURE ? true : undefined}
|
||||
@ -440,7 +486,6 @@ export const AddFieldsFormPartial = ({
|
||||
<button
|
||||
type="button"
|
||||
className="group h-full w-full"
|
||||
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
|
||||
onClick={() => setSelectedField(FieldType.EMAIL)}
|
||||
onMouseDown={() => setSelectedField(FieldType.EMAIL)}
|
||||
data-selected={selectedField === FieldType.EMAIL ? true : undefined}
|
||||
@ -463,7 +508,6 @@ export const AddFieldsFormPartial = ({
|
||||
<button
|
||||
type="button"
|
||||
className="group h-full w-full"
|
||||
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
|
||||
onClick={() => setSelectedField(FieldType.NAME)}
|
||||
onMouseDown={() => setSelectedField(FieldType.NAME)}
|
||||
data-selected={selectedField === FieldType.NAME ? true : undefined}
|
||||
@ -486,7 +530,6 @@ export const AddFieldsFormPartial = ({
|
||||
<button
|
||||
type="button"
|
||||
className="group h-full w-full"
|
||||
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
|
||||
onClick={() => setSelectedField(FieldType.DATE)}
|
||||
onMouseDown={() => setSelectedField(FieldType.DATE)}
|
||||
data-selected={selectedField === FieldType.DATE ? true : undefined}
|
||||
@ -505,7 +548,7 @@ export const AddFieldsFormPartial = ({
|
||||
</CardContent>
|
||||
</Card>
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFlowFormContainerContent>
|
||||
|
||||
@ -4,19 +4,20 @@ import React, { useId } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Plus, Trash } from 'lucide-react';
|
||||
import { BadgeCheck, Copy, Eye, PencilLine, Plus, Trash } from 'lucide-react';
|
||||
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
||||
|
||||
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, RecipientRole, SendStatus } from '@documenso/prisma/client';
|
||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||
|
||||
import { Button } from '../button';
|
||||
import { FormErrorMessage } from '../form/form-error-message';
|
||||
import { Input } from '../input';
|
||||
import { Label } from '../label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '../select';
|
||||
import { useStep } from '../stepper';
|
||||
import { useToast } from '../use-toast';
|
||||
import type { TAddSignersFormSchema } from './add-signers.types';
|
||||
@ -31,6 +32,13 @@ import {
|
||||
import { ShowFieldItem } from './show-field-item';
|
||||
import type { DocumentFlowStep } from './types';
|
||||
|
||||
const ROLE_ICONS: Record<RecipientRole, JSX.Element> = {
|
||||
SIGNER: <PencilLine className="h-4 w-4" />,
|
||||
APPROVER: <BadgeCheck className="h-4 w-4" />,
|
||||
CC: <Copy className="h-4 w-4" />,
|
||||
VIEWER: <Eye className="h-4 w-4" />,
|
||||
};
|
||||
|
||||
export type AddSignersFormProps = {
|
||||
documentFlow: DocumentFlowStep;
|
||||
recipients: Recipient[];
|
||||
@ -67,12 +75,14 @@ export const AddSignersFormPartial = ({
|
||||
formId: String(recipient.id),
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
formId: initialId,
|
||||
name: '',
|
||||
email: '',
|
||||
role: RecipientRole.SIGNER,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -104,6 +114,7 @@ export const AddSignersFormPartial = ({
|
||||
formId: nanoid(12),
|
||||
name: '',
|
||||
email: '',
|
||||
role: RecipientRole.SIGNER,
|
||||
});
|
||||
};
|
||||
|
||||
@ -189,6 +200,48 @@ export const AddSignersFormPartial = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-[60px]">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`signers.${index}.role`}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Select value={value} onValueChange={(x) => onChange(x)}>
|
||||
<SelectTrigger className="bg-background">{ROLE_ICONS[value]}</SelectTrigger>
|
||||
|
||||
<SelectContent className="" align="end">
|
||||
<SelectItem value={RecipientRole.SIGNER}>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.SIGNER]}</span>
|
||||
Signer
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value={RecipientRole.CC}>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.CC]}</span>
|
||||
Receives copy
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value={RecipientRole.APPROVER}>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.APPROVER]}</span>
|
||||
Approver
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value={RecipientRole.VIEWER}>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.VIEWER]}</span>
|
||||
Viewer
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { RecipientRole } from '.prisma/client';
|
||||
|
||||
export const ZAddSignersFormSchema = z
|
||||
.object({
|
||||
signers: z.array(
|
||||
@ -8,6 +10,7 @@ export const ZAddSignersFormSchema = z
|
||||
nativeId: z.number().optional(),
|
||||
email: z.string().email().min(1),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
@ -110,7 +110,6 @@ export const AddSubjectFormPartial = ({
|
||||
|
||||
<Input
|
||||
id="subject"
|
||||
// placeholder="Subject"
|
||||
className="bg-background mt-2"
|
||||
disabled={isSubmitting}
|
||||
{...register('meta.subject')}
|
||||
|
||||
Reference in New Issue
Block a user