feat: add roles to templates recipients

This commit is contained in:
Ephraim Atta-Duncan
2024-02-15 07:01:41 +00:00
parent f72b669f67
commit 769eaa0ed9
9 changed files with 135 additions and 66 deletions

View File

@ -1,4 +1,5 @@
import { prisma } from '@documenso/prisma'; import { prisma } from '@documenso/prisma';
import type { RecipientRole } from '@documenso/prisma/client';
import { nanoid } from '../../universal/id'; import { nanoid } from '../../universal/id';
@ -9,6 +10,7 @@ export type SetRecipientsForTemplateOptions = {
id?: number; id?: number;
email: string; email: string;
name: string; name: string;
role: RecipientRole;
}[]; }[];
}; };
@ -84,11 +86,13 @@ export const setRecipientsForTemplate = async ({
update: { update: {
name: recipient.name, name: recipient.name,
email: recipient.email, email: recipient.email,
role: recipient.role,
templateId, templateId,
}, },
create: { create: {
name: recipient.name, name: recipient.name,
email: recipient.email, email: recipient.email,
role: recipient.role,
token: nanoid(), token: nanoid(),
templateId, templateId,
}, },

View File

@ -57,6 +57,7 @@ export const createDocumentFromTemplate = async ({
create: template.Recipient.map((recipient) => ({ create: template.Recipient.map((recipient) => ({
email: recipient.email, email: recipient.email,
name: recipient.name, name: recipient.name,
role: recipient.role,
token: nanoid(), token: nanoid(),
})), })),
}, },

View File

@ -53,6 +53,7 @@ export const recipientRouter = router({
id: signer.nativeId, id: signer.nativeId,
email: signer.email, email: signer.email,
name: signer.name, name: signer.name,
role: signer.role,
})), })),
}); });
} catch (err) { } catch (err) {

View File

@ -34,6 +34,7 @@ export const ZAddTemplateSignersMutationSchema = z
nativeId: z.number().optional(), nativeId: z.number().optional(),
email: z.string().email().min(1), email: z.string().email().min(1),
name: z.string(), name: z.string(),
role: z.nativeEnum(RecipientRole),
}), }),
), ),
}) })

View File

@ -4,7 +4,7 @@ import React, { useId } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { BadgeCheck, Copy, Eye, PencilLine, Plus, Trash } from 'lucide-react'; import { Plus, Trash } from 'lucide-react';
import { Controller, useFieldArray, useForm } from 'react-hook-form'; import { Controller, useFieldArray, useForm } from 'react-hook-form';
import { useLimits } from '@documenso/ee/server-only/limits/provider/client'; import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
@ -17,6 +17,7 @@ import { Button } from '../button';
import { FormErrorMessage } from '../form/form-error-message'; import { FormErrorMessage } from '../form/form-error-message';
import { Input } from '../input'; import { Input } from '../input';
import { Label } from '../label'; import { Label } from '../label';
import { ROLE_ICONS } from '../recipient-role-icons';
import { Select, SelectContent, SelectItem, SelectTrigger } from '../select'; import { Select, SelectContent, SelectItem, SelectTrigger } from '../select';
import { useStep } from '../stepper'; import { useStep } from '../stepper';
import { useToast } from '../use-toast'; import { useToast } from '../use-toast';
@ -32,13 +33,6 @@ import {
import { ShowFieldItem } from './show-field-item'; import { ShowFieldItem } from './show-field-item';
import type { DocumentFlowStep } from './types'; 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 = { export type AddSignersFormProps = {
documentFlow: DocumentFlowStep; documentFlow: DocumentFlowStep;
recipients: Recipient[]; recipients: Recipient[];

View File

@ -0,0 +1,10 @@
import { BadgeCheck, Copy, Eye, PencilLine } from 'lucide-react';
import type { RecipientRole } from '.prisma/client';
export 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" />,
};

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Caveat } from 'next/font/google'; import { Caveat } from 'next/font/google';
@ -10,9 +10,10 @@ import { useFieldArray, useForm } from 'react-hook-form';
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect'; import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element'; import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; 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 { nanoid } from '@documenso/lib/universal/id';
import type { Field, Recipient } from '@documenso/prisma/client'; import type { Field, Recipient } from '@documenso/prisma/client';
import { FieldType } from '@documenso/prisma/client'; import { FieldType, RecipientRole } from '@documenso/prisma/client';
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';
import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Card, CardContent } from '@documenso/ui/primitives/card';
@ -291,6 +292,28 @@ export const AddTemplateFieldsFormPartial = ({
setSelectedSigner(recipients[0]); setSelectedSigner(recipients[0]);
}, [recipients]); }, [recipients]);
const recipientsByRole = useMemo(() => {
const recipientsByRole: Record<RecipientRole, Recipient[]> = {
CC: [],
VIEWER: [],
SIGNER: [],
APPROVER: [],
};
recipients.forEach((recipient) => {
recipientsByRole[recipient.role].push(recipient);
});
return recipientsByRole;
}, [recipients]);
const recipientsByRoleToDisplay = useMemo(() => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return (Object.entries(recipientsByRole) as [RecipientRole, Recipient[]][]).filter(
([role]) => role !== RecipientRole.CC && role !== RecipientRole.VIEWER,
);
}, [recipientsByRole]);
return ( return (
<> <>
<DocumentFlowFormContainerContent> <DocumentFlowFormContainerContent>
@ -363,55 +386,49 @@ export const AddTemplateFieldsFormPartial = ({
</span> </span>
</CommandEmpty> </CommandEmpty>
<CommandGroup> {recipientsByRoleToDisplay.map(([role, recipients], roleIndex) => (
{recipients.map((recipient, index) => ( <CommandGroup key={roleIndex}>
<CommandItem <div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
key={index} {`${RECIPIENT_ROLES_DESCRIPTION[role].roleName}s`}
className={cn({ </div>
// 'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
})}
onSelect={() => {
setSelectedSigner(recipient);
setShowRecipientsSelector(false);
}}
>
{/* {recipient.sendStatus !== SendStatus.SENT ? (
<Check
aria-hidden={recipient !== selectedSigner}
className={cn('mr-2 h-4 w-4 flex-shrink-0', {
'opacity-0': recipient !== selectedSigner,
'opacity-100': recipient === selectedSigner,
})}
/>
) : (
<Tooltip>
<TooltipTrigger>
<Info className="mr-2 h-4 w-4" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
This document has already been sent to this recipient. You can no
longer edit this recipient.
</TooltipContent>
</Tooltip>
)} */}
{recipient.name && ( {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={recipient.id}
className={cn('px-2 last:mb-1 [&:not(:first-child)]:mt-1')}
onSelect={() => {
setSelectedSigner(recipient);
setShowRecipientsSelector(false);
}}
>
<span <span
className="truncate" className={cn('text-foreground/70 truncate', {
title={`${recipient.name} (${recipient.email})`} 'text-foreground/80': recipient === selectedSigner,
})}
> >
{recipient.name} ({recipient.email}) {recipient.name && (
</span> <span title={`${recipient.name} (${recipient.email})`}>
)} {recipient.name} ({recipient.email})
</span>
)}
{!recipient.name && ( {!recipient.name && (
<span className="truncate" title={recipient.email}> <span title={recipient.email}>{recipient.email}</span>
{recipient.email} )}
</span> </span>
)} </CommandItem>
</CommandItem> ))}
))} </CommandGroup>
</CommandGroup> ))}
</Command> </Command>
</PopoverContent> </PopoverContent>
</Popover> </Popover>

View File

@ -5,10 +5,10 @@ import React, { useId, useState } from 'react';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { Plus, Trash } from 'lucide-react'; import { Plus, Trash } from 'lucide-react';
import { useFieldArray, useForm } from 'react-hook-form'; import { Controller, useFieldArray, useForm } from 'react-hook-form';
import { nanoid } from '@documenso/lib/universal/id'; import { nanoid } from '@documenso/lib/universal/id';
import type { Field, Recipient } from '@documenso/prisma/client'; import { type Field, type Recipient, RecipientRole } from '@documenso/prisma/client';
import { Button } from '@documenso/ui/primitives/button'; import { Button } from '@documenso/ui/primitives/button';
import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message'; import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
import { Input } from '@documenso/ui/primitives/input'; import { Input } from '@documenso/ui/primitives/input';
@ -21,6 +21,8 @@ import {
DocumentFlowFormContainerStep, DocumentFlowFormContainerStep,
} from '../document-flow/document-flow-root'; } from '../document-flow/document-flow-root';
import type { DocumentFlowStep } from '../document-flow/types'; import type { DocumentFlowStep } from '../document-flow/types';
import { ROLE_ICONS } from '../recipient-role-icons';
import { Select, SelectContent, SelectItem, SelectTrigger } from '../select';
import { useStep } from '../stepper'; import { useStep } from '../stepper';
import type { TAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types'; import type { TAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types';
import { ZAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types'; import { ZAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types';
@ -59,12 +61,14 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
formId: String(recipient.id), formId: String(recipient.id),
name: recipient.name, name: recipient.name,
email: recipient.email, email: recipient.email,
role: recipient.role,
})) }))
: [ : [
{ {
formId: initialId, formId: initialId,
name: `Recipient 1`, name: `Recipient 1`,
email: `recipient.1@documenso.com`, email: `recipient.1@documenso.com`,
role: RecipientRole.SIGNER,
}, },
], ],
}, },
@ -86,6 +90,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
formId: nanoid(12), formId: nanoid(12),
name: `Recipient ${placeholderRecipientCount}`, name: `Recipient ${placeholderRecipientCount}`,
email: `recipient.${placeholderRecipientCount}@documenso.com`, email: `recipient.${placeholderRecipientCount}@documenso.com`,
role: RecipientRole.SIGNER,
}); });
setPlaceholderRecipientCount((count) => count + 1); setPlaceholderRecipientCount((count) => count + 1);
@ -95,12 +100,6 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
removeSigner(index); removeSigner(index);
}; };
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter' && event.target instanceof HTMLInputElement) {
onAddPlaceholderRecipient();
}
};
return ( return (
<> <>
<DocumentFlowFormContainerContent> <DocumentFlowFormContainerContent>
@ -113,10 +112,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
className="flex flex-wrap items-end gap-x-4" className="flex flex-wrap items-end gap-x-4"
> >
<div className="flex-1"> <div className="flex-1">
<Label htmlFor={`signer-${signer.id}-email`}> <Label htmlFor={`signer-${signer.id}-email`}>Email</Label>
Email
<span className="text-destructive ml-1 inline-block font-medium">*</span>
</Label>
<Input <Input
id={`signer-${signer.id}-email`} id={`signer-${signer.id}-email`}
@ -139,6 +135,48 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
/> />
</div> </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> <div>
<button <button
type="button" type="button"

View File

@ -1,5 +1,7 @@
import { z } from 'zod'; import { z } from 'zod';
import { RecipientRole } from '.prisma/client';
export const ZAddTemplatePlacholderRecipientsFormSchema = z export const ZAddTemplatePlacholderRecipientsFormSchema = z
.object({ .object({
signers: z.array( signers: z.array(
@ -8,6 +10,7 @@ export const ZAddTemplatePlacholderRecipientsFormSchema = z
nativeId: z.number().optional(), nativeId: z.number().optional(),
email: z.string().min(1).email(), email: z.string().min(1).email(),
name: z.string(), name: z.string(),
role: z.nativeEnum(RecipientRole),
}), }),
), ),
}) })