mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 08:13:56 +10:00
feat: add roles to templates recipients
This commit is contained in:
@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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(),
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -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[];
|
||||||
|
|||||||
10
packages/ui/primitives/recipient-role-icons.tsx
Normal file
10
packages/ui/primitives/recipient-role-icons.tsx
Normal 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" />,
|
||||||
|
};
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user