mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 08:42:12 +10:00
Merge branch 'main' into reattach-pdf
This commit is contained in:
@ -6,7 +6,7 @@ import { cva } from 'class-variance-authority';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center rounded-md px-2 py-1.5 text-xs font-medium ring-1 ring-inset w-fit',
|
||||
'inline-flex items-center rounded-md text-xs font-medium ring-1 ring-inset w-fit',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@ -21,9 +21,15 @@ const badgeVariants = cva(
|
||||
secondary:
|
||||
'bg-blue-50 text-blue-700 ring-blue-700/10 dark:bg-blue-400/10 dark:text-blue-400 dark:ring-blue-400/30',
|
||||
},
|
||||
size: {
|
||||
small: 'px-1.5 py-0.5 text-xs',
|
||||
default: 'px-2 py-1.5 text-xs',
|
||||
large: 'px-3 py-2 text-sm',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -32,8 +38,8 @@ export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
||||
function Badge({ className, variant, size, ...props }: BadgeProps) {
|
||||
return <div className={cn(badgeVariants({ variant, size }), className)} {...props} />;
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
|
||||
@ -13,7 +13,8 @@ const buttonVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
|
||||
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive',
|
||||
outline: 'border border-input hover:bg-accent hover:text-accent-foreground',
|
||||
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
|
||||
ghost: 'hover:bg-accent hover:text-accent-foreground',
|
||||
|
||||
82
packages/ui/primitives/color-picker.tsx
Normal file
82
packages/ui/primitives/color-picker.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { HexColorInput, HexColorPicker } from 'react-colorful';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from './popover';
|
||||
|
||||
export type ColorPickerProps = {
|
||||
disabled?: boolean;
|
||||
value: string;
|
||||
defaultValue?: string;
|
||||
onChange: (color: string) => void;
|
||||
} & HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const ColorPicker = ({
|
||||
className,
|
||||
disabled = false,
|
||||
value,
|
||||
defaultValue = '#000000',
|
||||
onChange,
|
||||
...props
|
||||
}: ColorPickerProps) => {
|
||||
const [color, setColor] = useState(value || defaultValue);
|
||||
const [inputColor, setInputColor] = useState(value || defaultValue);
|
||||
|
||||
const onColorChange = (newColor: string) => {
|
||||
setColor(newColor);
|
||||
setInputColor(newColor);
|
||||
onChange(newColor);
|
||||
};
|
||||
|
||||
const onInputChange = (newColor: string) => {
|
||||
setInputColor(newColor);
|
||||
};
|
||||
|
||||
const onInputBlur = () => {
|
||||
setColor(inputColor);
|
||||
onChange(inputColor);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<button
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
className="bg-background h-12 w-12 rounded-md border p-1 disabled:pointer-events-none disabled:opacity-50"
|
||||
>
|
||||
<div className="h-full w-full rounded-sm" style={{ backgroundColor: color }} />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent className="w-auto">
|
||||
<HexColorPicker
|
||||
className={cn(
|
||||
className,
|
||||
'w-full aria-disabled:pointer-events-none aria-disabled:opacity-50',
|
||||
)}
|
||||
color={color}
|
||||
onChange={onColorChange}
|
||||
aria-disabled={disabled}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<HexColorInput
|
||||
className="mt-4 h-10 rounded-md border bg-transparent px-3 py-2 text-sm disabled:pointer-events-none disabled:opacity-50"
|
||||
color={inputColor}
|
||||
onChange={onInputChange}
|
||||
onBlur={onInputBlur}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
onInputBlur();
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@ -326,9 +326,9 @@ export const AddFieldsFormPartial = ({
|
||||
{selectedField && (
|
||||
<Card
|
||||
className={cn(
|
||||
'bg-background pointer-events-none fixed z-50 cursor-pointer transition-opacity',
|
||||
'bg-field-card/80 pointer-events-none fixed z-50 cursor-pointer border-2 backdrop-blur-[1px]',
|
||||
{
|
||||
'border-primary': isFieldWithinBounds,
|
||||
'border-field-card-border': isFieldWithinBounds,
|
||||
'opacity-50': !isFieldWithinBounds,
|
||||
},
|
||||
)}
|
||||
@ -339,7 +339,7 @@ export const AddFieldsFormPartial = ({
|
||||
width: fieldBounds.current.width,
|
||||
}}
|
||||
>
|
||||
<CardContent className="text-foreground flex h-full w-full items-center justify-center p-2">
|
||||
<CardContent className="text-field-card-foreground flex h-full w-full items-center justify-center p-2">
|
||||
{FRIENDLY_FIELD_TYPE[selectedField]}
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -555,6 +555,28 @@ export const AddFieldsFormPartial = ({
|
||||
</CardContent>
|
||||
</Card>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="group h-full w-full"
|
||||
onClick={() => setSelectedField(FieldType.TEXT)}
|
||||
onMouseDown={() => setSelectedField(FieldType.TEXT)}
|
||||
data-selected={selectedField === FieldType.TEXT ? true : undefined}
|
||||
>
|
||||
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50">
|
||||
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground text-xl font-medium',
|
||||
)}
|
||||
>
|
||||
{'Text'}
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-xs">Custom Text</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -44,6 +44,7 @@ export type AddSignatureFormProps = {
|
||||
|
||||
onSubmit: (_data: TAddSignatureFormSchema) => Promise<void> | void;
|
||||
requireName?: boolean;
|
||||
requireCustomText?: boolean;
|
||||
requireSignature?: boolean;
|
||||
};
|
||||
|
||||
@ -54,6 +55,7 @@ export const AddSignatureFormPartial = ({
|
||||
|
||||
onSubmit,
|
||||
requireName = false,
|
||||
requireCustomText = false,
|
||||
requireSignature = true,
|
||||
}: AddSignatureFormProps) => {
|
||||
const { currentStep, totalSteps } = useStep();
|
||||
@ -70,6 +72,14 @@ export const AddSignatureFormPartial = ({
|
||||
});
|
||||
}
|
||||
|
||||
if (requireCustomText && val.customText.length === 0) {
|
||||
ctx.addIssue({
|
||||
path: ['customText'],
|
||||
code: 'custom',
|
||||
message: 'Text is required',
|
||||
});
|
||||
}
|
||||
|
||||
if (requireSignature && val.signature.length === 0) {
|
||||
ctx.addIssue({
|
||||
path: ['signature'],
|
||||
@ -85,6 +95,7 @@ export const AddSignatureFormPartial = ({
|
||||
name: '',
|
||||
email: '',
|
||||
signature: '',
|
||||
customText: '',
|
||||
},
|
||||
});
|
||||
|
||||
@ -131,6 +142,11 @@ export const AddSignatureFormPartial = ({
|
||||
return !form.formState.errors.email;
|
||||
}
|
||||
|
||||
if (fieldType === FieldType.TEXT) {
|
||||
await form.trigger('customText');
|
||||
return !form.formState.errors.customText;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@ -154,6 +170,11 @@ export const AddSignatureFormPartial = ({
|
||||
customText: form.getValues('name'),
|
||||
inserted: true,
|
||||
}))
|
||||
.with(FieldType.TEXT, () => ({
|
||||
...field,
|
||||
customText: form.getValues('customText'),
|
||||
inserted: true,
|
||||
}))
|
||||
.with(FieldType.SIGNATURE, () => {
|
||||
const value = form.getValues('signature');
|
||||
|
||||
@ -302,6 +323,29 @@ export const AddSignatureFormPartial = ({
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{requireCustomText && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="customText"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel required={requireCustomText}>Custom Text</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
className="bg-background"
|
||||
{...field}
|
||||
onChange={(value) => {
|
||||
onFormValueChange(FieldType.TEXT);
|
||||
field.onChange(value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DocumentFlowFormContainerContent>
|
||||
|
||||
@ -330,7 +374,7 @@ export const AddSignatureFormPartial = ({
|
||||
<ElementVisible target={PDF_VIEWER_PAGE_SELECTOR}>
|
||||
{localFields.map((field) =>
|
||||
match(field.type)
|
||||
.with(FieldType.DATE, FieldType.EMAIL, FieldType.NAME, () => {
|
||||
.with(FieldType.DATE, FieldType.TEXT, FieldType.EMAIL, FieldType.NAME, () => {
|
||||
return (
|
||||
<SinglePlayerModeCustomTextField
|
||||
onClick={insertField(field)}
|
||||
|
||||
@ -6,6 +6,7 @@ export const ZAddSignatureFormSchema = z.object({
|
||||
.min(1, { message: 'Email is required' })
|
||||
.email({ message: 'Invalid email address' }),
|
||||
name: z.string(),
|
||||
customText: z.string(),
|
||||
signature: z.string(),
|
||||
});
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import React, { useId } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
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 { 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 { Input } from '../input';
|
||||
import { Label } from '../label';
|
||||
import { ROLE_ICONS } from '../recipient-role-icons';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '../select';
|
||||
import { useStep } from '../stepper';
|
||||
import { useToast } from '../use-toast';
|
||||
@ -32,13 +33,6 @@ 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[];
|
||||
|
||||
@ -226,7 +226,7 @@ export const AddSubjectFormPartial = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="mt-2 flex flex-col">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div>
|
||||
<Label htmlFor="redirectUrl" className="flex items-center">
|
||||
|
||||
@ -128,24 +128,22 @@ export const FieldItem = ({
|
||||
)}
|
||||
|
||||
<Card
|
||||
className={cn('bg-background h-full w-full', {
|
||||
'border-primary': !disabled,
|
||||
'border-primary/80': active,
|
||||
className={cn('bg-field-card/80 h-full w-full backdrop-blur-[1px]', {
|
||||
'border-field-card-border': !disabled,
|
||||
'border-field-card-border/80': active,
|
||||
})}
|
||||
>
|
||||
<CardContent
|
||||
className={cn(
|
||||
'text-foreground flex h-full w-full flex-col items-center justify-center p-2',
|
||||
'text-field-card-foreground flex h-full w-full flex-col items-center justify-center p-2',
|
||||
{
|
||||
'text-muted-foreground/50': disabled,
|
||||
'text-field-card-foreground/50': disabled,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{FRIENDLY_FIELD_TYPE[field.type]}
|
||||
|
||||
<p className="text-muted-foreground/50 w-full truncate text-center text-xs">
|
||||
{field.signerEmail}
|
||||
</p>
|
||||
<p className="w-full truncate text-center text-xs">{field.signerEmail}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Rnd>,
|
||||
|
||||
@ -172,6 +172,7 @@ export function SinglePlayerModeCustomTextField({
|
||||
.with(FieldType.DATE, () => 'Date')
|
||||
.with(FieldType.NAME, () => 'Name')
|
||||
.with(FieldType.EMAIL, () => 'Email')
|
||||
.with(FieldType.TEXT, () => 'Text')
|
||||
.with(FieldType.SIGNATURE, FieldType.FREE_SIGNATURE, () => 'Signature')
|
||||
.otherwise(() => '')}
|
||||
</button>
|
||||
|
||||
@ -10,7 +10,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'border-input ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border bg-transparent px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'bg-background border-input ring-offset-background placeholder:text-muted-foreground focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
|
||||
className,
|
||||
{
|
||||
'ring-2 !ring-red-500 transition-all': props['aria-invalid'],
|
||||
|
||||
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';
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
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 { 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 { FieldType } from '@documenso/prisma/client';
|
||||
import { FieldType, RecipientRole } from '@documenso/prisma/client';
|
||||
import { cn } from '@documenso/ui/lib/utils';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||
@ -291,6 +292,28 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
setSelectedSigner(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]);
|
||||
|
||||
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 (
|
||||
<>
|
||||
<DocumentFlowFormContainerContent>
|
||||
@ -363,55 +386,49 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
</span>
|
||||
</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
{recipients.map((recipient, index) => (
|
||||
<CommandItem
|
||||
key={index}
|
||||
className={cn({
|
||||
// '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>
|
||||
)} */}
|
||||
{recipientsByRoleToDisplay.map(([role, recipients], roleIndex) => (
|
||||
<CommandGroup key={roleIndex}>
|
||||
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
|
||||
{`${RECIPIENT_ROLES_DESCRIPTION[role].roleName}s`}
|
||||
</div>
|
||||
|
||||
{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
|
||||
className="truncate"
|
||||
title={`${recipient.name} (${recipient.email})`}
|
||||
className={cn('text-foreground/70 truncate', {
|
||||
'text-foreground/80': recipient === selectedSigner,
|
||||
})}
|
||||
>
|
||||
{recipient.name} ({recipient.email})
|
||||
</span>
|
||||
)}
|
||||
{recipient.name && (
|
||||
<span title={`${recipient.name} (${recipient.email})`}>
|
||||
{recipient.name} ({recipient.email})
|
||||
</span>
|
||||
)}
|
||||
|
||||
{!recipient.name && (
|
||||
<span className="truncate" title={recipient.email}>
|
||||
{recipient.email}
|
||||
{!recipient.name && (
|
||||
<span title={recipient.email}>{recipient.email}</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
@ -511,6 +528,28 @@ export const AddTemplateFieldsFormPartial = ({
|
||||
</CardContent>
|
||||
</Card>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="group h-full w-full"
|
||||
onClick={() => setSelectedField(FieldType.TEXT)}
|
||||
onMouseDown={() => setSelectedField(FieldType.TEXT)}
|
||||
data-selected={selectedField === FieldType.TEXT ? true : undefined}
|
||||
>
|
||||
<Card className="group-data-[selected]:border-documenso h-full w-full cursor-pointer group-disabled:opacity-50">
|
||||
<CardContent className="flex flex-col items-center justify-center px-6 py-4">
|
||||
<p
|
||||
className={cn(
|
||||
'text-muted-foreground group-data-[selected]:text-foreground text-xl font-medium',
|
||||
)}
|
||||
>
|
||||
{'Text'}
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground mt-2 text-xs">Custom Text</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -5,10 +5,10 @@ import React, { useId, useState } from 'react';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
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 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 { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
@ -21,6 +21,8 @@ import {
|
||||
DocumentFlowFormContainerStep,
|
||||
} from '../document-flow/document-flow-root';
|
||||
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 type { TAddTemplatePlacholderRecipientsFormSchema } 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),
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
formId: initialId,
|
||||
name: `Recipient 1`,
|
||||
email: `recipient.1@documenso.com`,
|
||||
role: RecipientRole.SIGNER,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -86,6 +90,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
formId: nanoid(12),
|
||||
name: `Recipient ${placeholderRecipientCount}`,
|
||||
email: `recipient.${placeholderRecipientCount}@documenso.com`,
|
||||
role: RecipientRole.SIGNER,
|
||||
});
|
||||
|
||||
setPlaceholderRecipientCount((count) => count + 1);
|
||||
@ -95,12 +100,6 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
removeSigner(index);
|
||||
};
|
||||
|
||||
const onKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter' && event.target instanceof HTMLInputElement) {
|
||||
onAddPlaceholderRecipient();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentFlowFormContainerContent>
|
||||
@ -113,10 +112,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
className="flex flex-wrap items-end gap-x-4"
|
||||
>
|
||||
<div className="flex-1">
|
||||
<Label htmlFor={`signer-${signer.id}-email`}>
|
||||
Email
|
||||
<span className="text-destructive ml-1 inline-block font-medium">*</span>
|
||||
</Label>
|
||||
<Label htmlFor={`signer-${signer.id}-email`}>Email</Label>
|
||||
|
||||
<Input
|
||||
id={`signer-${signer.id}-email`}
|
||||
@ -139,6 +135,48 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
/>
|
||||
</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 ZAddTemplatePlacholderRecipientsFormSchema = z
|
||||
.object({
|
||||
signers: z.array(
|
||||
@ -8,6 +10,7 @@ export const ZAddTemplatePlacholderRecipientsFormSchema = z
|
||||
nativeId: z.number().optional(),
|
||||
email: z.string().min(1).email(),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
@ -3,7 +3,8 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import * as TogglePrimitive from '@radix-ui/react-toggle';
|
||||
import { VariantProps, cva } from 'class-variance-authority';
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user