mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 00:32:43 +10:00
Merge branch 'main' into fix/layout-shift-on-table
This commit is contained in:
31
packages/ui/icons/verified.tsx
Normal file
31
packages/ui/icons/verified.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import type { LucideIcon } from 'lucide-react/dist/lucide-react';
|
||||
|
||||
export const VerifiedIcon: LucideIcon = forwardRef(
|
||||
({ size = 24, color = 'currentColor', ...props }, ref) => {
|
||||
return (
|
||||
<svg
|
||||
ref={ref}
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 25 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<g id="badge, verified, award">
|
||||
<path
|
||||
id="Icon"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M10.5457 2.89094C11.5779 1.70302 13.4223 1.70302 14.4545 2.89094L15.2585 3.81628C15.3917 3.96967 15.5947 4.04354 15.7954 4.0117L17.0061 3.81965C18.5603 3.57309 19.9732 4.75869 20.0003 6.33214L20.0214 7.55778C20.0249 7.76096 20.1329 7.94799 20.3071 8.05261L21.358 8.6837C22.7071 9.49389 23.0274 11.3103 22.0368 12.5331L21.2651 13.4855C21.1372 13.6434 21.0997 13.8561 21.1659 14.0482L21.5652 15.2072C22.0779 16.695 21.1557 18.2923 19.6109 18.5922L18.4075 18.8258C18.208 18.8646 18.0426 19.0034 17.9698 19.1931L17.5308 20.3376C16.9672 21.8069 15.234 22.4378 13.8578 21.6745L12.7858 21.08C12.6081 20.9814 12.3921 20.9814 12.2144 21.08L11.1424 21.6745C9.76623 22.4378 8.033 21.8069 7.4694 20.3376L7.03038 19.1931C6.9576 19.0034 6.79216 18.8646 6.59268 18.8258L5.38932 18.5922C3.84448 18.2923 2.92224 16.695 3.43495 15.2072L3.83431 14.0482C3.90052 13.8561 3.86302 13.6434 3.7351 13.4855L2.96343 12.5331C1.97279 11.3103 2.29307 9.49389 3.64218 8.6837L4.69306 8.05261C4.86728 7.94799 4.97526 7.76096 4.97875 7.55778L4.99985 6.33214C5.02694 4.75869 6.43987 3.57309 7.99413 3.81965L9.20481 4.0117C9.40551 4.04354 9.60845 3.96967 9.74173 3.81628L10.5457 2.89094ZM15.7072 11.2071C16.0977 10.8166 16.0977 10.1834 15.7072 9.79289C15.3167 9.40237 14.6835 9.40237 14.293 9.79289L11.5001 12.5858L10.7072 11.7929C10.3167 11.4024 9.68351 11.4024 9.29298 11.7929C8.90246 12.1834 8.90246 12.8166 9.29298 13.2071L10.4394 14.3536C11.0252 14.9393 11.975 14.9393 12.5608 14.3536L15.7072 11.2071Z"
|
||||
fill={color}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
VerifiedIcon.displayName = 'VerifiedIcon';
|
||||
120
packages/ui/lib/document-dropzone-constants.ts
Normal file
120
packages/ui/lib/document-dropzone-constants.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import type { Variants } from 'framer-motion';
|
||||
|
||||
export const DocumentDropzoneContainerVariants: Variants = {
|
||||
initial: {
|
||||
scale: 1,
|
||||
},
|
||||
animate: {
|
||||
scale: 1,
|
||||
},
|
||||
hover: {
|
||||
transition: {
|
||||
staggerChildren: 0.05,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const DocumentDropzoneCardLeftVariants: Variants = {
|
||||
initial: {
|
||||
x: 40,
|
||||
y: -10,
|
||||
rotate: -14,
|
||||
},
|
||||
animate: {
|
||||
x: 40,
|
||||
y: -10,
|
||||
rotate: -14,
|
||||
},
|
||||
hover: {
|
||||
x: -25,
|
||||
y: -25,
|
||||
rotate: -22,
|
||||
},
|
||||
};
|
||||
|
||||
export const DocumentDropzoneCardRightVariants: Variants = {
|
||||
initial: {
|
||||
x: -40,
|
||||
y: -10,
|
||||
rotate: 14,
|
||||
},
|
||||
animate: {
|
||||
x: -40,
|
||||
y: -10,
|
||||
rotate: 14,
|
||||
},
|
||||
hover: {
|
||||
x: 25,
|
||||
y: -25,
|
||||
rotate: 22,
|
||||
},
|
||||
};
|
||||
|
||||
export const DocumentDropzoneCardCenterVariants: Variants = {
|
||||
initial: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
animate: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
hover: {
|
||||
x: 0,
|
||||
y: -25,
|
||||
},
|
||||
};
|
||||
|
||||
export const DocumentDropzoneDisabledCardLeftVariants: Variants = {
|
||||
initial: {
|
||||
x: 50,
|
||||
y: 0,
|
||||
rotate: -14,
|
||||
},
|
||||
animate: {
|
||||
x: 50,
|
||||
y: 0,
|
||||
rotate: -14,
|
||||
},
|
||||
hover: {
|
||||
x: 30,
|
||||
y: 0,
|
||||
rotate: -17,
|
||||
transition: { type: 'spring', duration: 0.3, stiffness: 500 },
|
||||
},
|
||||
};
|
||||
|
||||
export const DocumentDropzoneDisabledCardRightVariants: Variants = {
|
||||
initial: {
|
||||
x: -50,
|
||||
y: 0,
|
||||
rotate: 14,
|
||||
},
|
||||
animate: {
|
||||
x: -50,
|
||||
y: 0,
|
||||
rotate: 14,
|
||||
},
|
||||
hover: {
|
||||
x: -30,
|
||||
y: 0,
|
||||
rotate: 17,
|
||||
transition: { type: 'spring', duration: 0.3, stiffness: 500 },
|
||||
},
|
||||
};
|
||||
|
||||
export const DocumentDropzoneDisabledCardCenterVariants: Variants = {
|
||||
initial: {
|
||||
x: -10,
|
||||
y: 0,
|
||||
},
|
||||
animate: {
|
||||
x: -10,
|
||||
y: 0,
|
||||
},
|
||||
hover: {
|
||||
x: [-15, -10, -5, -10],
|
||||
y: 0,
|
||||
transition: { type: 'spring', duration: 0.3, stiffness: 1000 },
|
||||
},
|
||||
};
|
||||
@ -64,6 +64,7 @@
|
||||
"luxon": "^3.4.2",
|
||||
"next": "14.0.3",
|
||||
"pdfjs-dist": "3.6.172",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-day-picker": "^8.7.1",
|
||||
"react-hook-form": "^7.45.4",
|
||||
"react-pdf": "7.3.3",
|
||||
|
||||
@ -6,20 +6,30 @@ import { cva } from 'class-variance-authority';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
const badgeVariants = cva(
|
||||
'inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
|
||||
'inline-flex items-center rounded-md text-xs font-medium ring-1 ring-inset w-fit',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-primary hover:bg-primary/80 border-transparent text-primary-foreground',
|
||||
secondary:
|
||||
'bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground',
|
||||
neutral:
|
||||
'bg-gray-50 text-gray-600 ring-gray-500/10 dark:bg-gray-400/10 dark:text-gray-400 dark:ring-gray-400/20',
|
||||
destructive:
|
||||
'bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground',
|
||||
outline: 'text-foreground',
|
||||
'bg-red-50 text-red-700 ring-red-600/10 dark:bg-red-400/10 dark:text-red-400 dark:ring-red-400/20',
|
||||
warning:
|
||||
'bg-yellow-50 text-yellow-800 ring-yellow-600/20 dark:bg-yellow-400/10 dark:text-yellow-500 dark:ring-yellow-400/20',
|
||||
default:
|
||||
'bg-green-50 text-green-700 ring-green-600/20 dark:bg-green-500/10 dark:text-green-400 dark:ring-green-500/20',
|
||||
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',
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -28,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>
|
||||
);
|
||||
};
|
||||
@ -1,90 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import type { Variants } from 'framer-motion';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { AlertTriangle, Plus } from 'lucide-react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT, IS_BILLING_ENABLED } from '@documenso/lib/constants/app';
|
||||
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
||||
|
||||
import {
|
||||
DocumentDropzoneCardCenterVariants,
|
||||
DocumentDropzoneCardLeftVariants,
|
||||
DocumentDropzoneCardRightVariants,
|
||||
DocumentDropzoneContainerVariants,
|
||||
DocumentDropzoneDisabledCardCenterVariants,
|
||||
DocumentDropzoneDisabledCardLeftVariants,
|
||||
DocumentDropzoneDisabledCardRightVariants,
|
||||
} from '../lib/document-dropzone-constants';
|
||||
import { cn } from '../lib/utils';
|
||||
import { Button } from './button';
|
||||
import { Card, CardContent } from './card';
|
||||
|
||||
const DocumentDropzoneContainerVariants: Variants = {
|
||||
initial: {
|
||||
scale: 1,
|
||||
},
|
||||
animate: {
|
||||
scale: 1,
|
||||
},
|
||||
hover: {
|
||||
transition: {
|
||||
staggerChildren: 0.05,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const DocumentDropzoneCardLeftVariants: Variants = {
|
||||
initial: {
|
||||
x: 40,
|
||||
y: -10,
|
||||
rotate: -14,
|
||||
},
|
||||
animate: {
|
||||
x: 40,
|
||||
y: -10,
|
||||
rotate: -14,
|
||||
},
|
||||
hover: {
|
||||
x: -25,
|
||||
y: -25,
|
||||
rotate: -22,
|
||||
},
|
||||
};
|
||||
|
||||
const DocumentDropzoneCardRightVariants: Variants = {
|
||||
initial: {
|
||||
x: -40,
|
||||
y: -10,
|
||||
rotate: 14,
|
||||
},
|
||||
animate: {
|
||||
x: -40,
|
||||
y: -10,
|
||||
rotate: 14,
|
||||
},
|
||||
hover: {
|
||||
x: 25,
|
||||
y: -25,
|
||||
rotate: 22,
|
||||
},
|
||||
};
|
||||
|
||||
const DocumentDropzoneCardCenterVariants: Variants = {
|
||||
initial: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
animate: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
hover: {
|
||||
x: 0,
|
||||
y: -25,
|
||||
},
|
||||
};
|
||||
|
||||
const DocumentDescription = {
|
||||
document: {
|
||||
headline: 'Add a document',
|
||||
},
|
||||
template: {
|
||||
headline: 'Upload Template Document',
|
||||
},
|
||||
};
|
||||
|
||||
export type DocumentDropzoneProps = {
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
@ -123,68 +60,108 @@ export const DocumentDropzone = ({
|
||||
maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT),
|
||||
});
|
||||
|
||||
const heading = {
|
||||
document: disabled ? 'You have reached your document limit.' : 'Add a document',
|
||||
template: 'Upload Template Document',
|
||||
};
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
className={cn('flex aria-disabled:cursor-not-allowed', className)}
|
||||
variants={DocumentDropzoneContainerVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
whileHover="hover"
|
||||
aria-disabled={disabled}
|
||||
>
|
||||
<Card
|
||||
role="button"
|
||||
className={cn(
|
||||
'focus-visible:ring-ring ring-offset-background flex flex-1 cursor-pointer flex-col items-center justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 aria-disabled:pointer-events-none aria-disabled:opacity-60',
|
||||
'focus-visible:ring-ring ring-offset-background group flex flex-1 cursor-pointer flex-col items-center justify-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
|
||||
className,
|
||||
)}
|
||||
gradient={true}
|
||||
gradient={!disabled}
|
||||
degrees={120}
|
||||
aria-disabled={disabled}
|
||||
{...getRootProps()}
|
||||
{...props}
|
||||
>
|
||||
<CardContent className="text-muted-foreground/40 flex flex-col items-center justify-center p-6">
|
||||
{/* <FilePlus strokeWidth="1px" className="h-16 w-16"/> */}
|
||||
<div className="flex">
|
||||
<motion.div
|
||||
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 a z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
||||
variants={!disabled ? DocumentDropzoneCardLeftVariants : undefined}
|
||||
>
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
||||
</motion.div>
|
||||
{disabled ? (
|
||||
// Disabled State
|
||||
<div className="flex">
|
||||
<motion.div
|
||||
className="group-hover:bg-destructive/2 border-muted-foreground/20 group-hover:border-destructive/10 dark:bg-muted/80 a z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
||||
variants={DocumentDropzoneDisabledCardLeftVariants}
|
||||
>
|
||||
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-full rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-5/6 rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-full rounded-[2px]" />
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
||||
variants={!disabled ? DocumentDropzoneCardCenterVariants : undefined}
|
||||
>
|
||||
<Plus
|
||||
strokeWidth="2px"
|
||||
className="text-muted-foreground/20 group-hover:text-documenso h-12 w-12"
|
||||
/>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
className="group-hover:bg-destructive/5 border-muted-foreground/20 group-hover:border-destructive/50 dark:bg-muted/80 z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
||||
variants={DocumentDropzoneDisabledCardCenterVariants}
|
||||
>
|
||||
<AlertTriangle
|
||||
strokeWidth="2px"
|
||||
className="text-muted-foreground/20 group-hover:text-destructive h-12 w-12"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
||||
variants={!disabled ? DocumentDropzoneCardRightVariants : undefined}
|
||||
>
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
||||
</motion.div>
|
||||
</div>
|
||||
<motion.div
|
||||
className="group-hover:bg-destructive/2 border-muted-foreground/20 group-hover:border-destructive/10 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
||||
variants={DocumentDropzoneDisabledCardRightVariants}
|
||||
>
|
||||
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-full rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-5/6 rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/10 group-hover:bg-destructive/10 h-2 w-full rounded-[2px]" />
|
||||
</motion.div>
|
||||
</div>
|
||||
) : (
|
||||
// Non Disabled State
|
||||
<div className="flex">
|
||||
<motion.div
|
||||
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 a z-10 flex aspect-[3/4] w-24 origin-top-right -rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
||||
variants={DocumentDropzoneCardLeftVariants}
|
||||
>
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-20 flex aspect-[3/4] w-24 flex-col items-center justify-center gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
||||
variants={DocumentDropzoneCardCenterVariants}
|
||||
>
|
||||
<Plus
|
||||
strokeWidth="2px"
|
||||
className="text-muted-foreground/20 group-hover:text-documenso h-12 w-12"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 origin-top-left rotate-[22deg] flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm"
|
||||
variants={DocumentDropzoneCardRightVariants}
|
||||
>
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
|
||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input {...getInputProps()} />
|
||||
|
||||
<p className="group-hover:text-foreground text-muted-foreground mt-8 font-medium">
|
||||
{DocumentDescription[type].headline}
|
||||
</p>
|
||||
<p className="text-foreground mt-8 font-medium">{heading[type]}</p>
|
||||
|
||||
<p className="text-muted-foreground/80 mt-1 text-sm">
|
||||
<p className="text-muted-foreground/80 mt-1 text-center text-sm">
|
||||
{disabled ? disabledMessage : 'Drag & drop your PDF here.'}
|
||||
</p>
|
||||
|
||||
{disabled && IS_BILLING_ENABLED() && (
|
||||
<Button className="hover:bg-warning/80 bg-warning mt-4 w-32" asChild>
|
||||
<Link href="/settings/billing">Upgrade</Link>
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
@ -323,9 +323,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,
|
||||
},
|
||||
)}
|
||||
@ -336,7 +336,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>
|
||||
@ -552,6 +552,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'],
|
||||
|
||||
@ -233,18 +233,20 @@ export const PDFViewer = ({
|
||||
{Array(numPages)
|
||||
.fill(null)
|
||||
.map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="border-border my-8 overflow-hidden rounded border will-change-transform first:mt-0 last:mb-0"
|
||||
>
|
||||
<PDFPage
|
||||
pageNumber={i + 1}
|
||||
width={width}
|
||||
renderAnnotationLayer={false}
|
||||
renderTextLayer={false}
|
||||
loading={() => ''}
|
||||
onClick={(e) => onDocumentPageClick(e, i + 1)}
|
||||
/>
|
||||
<div key={i} className="last:-mb-2">
|
||||
<div className="border-border overflow-hidden rounded border will-change-transform">
|
||||
<PDFPage
|
||||
pageNumber={i + 1}
|
||||
width={width}
|
||||
renderAnnotationLayer={false}
|
||||
renderTextLayer={false}
|
||||
loading={() => ''}
|
||||
onClick={(e) => onDocumentPageClick(e, i + 1)}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-muted-foreground/80 my-2 text-center text-[11px]">
|
||||
Page {i + 1} of {numPages}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</PDFDocument>
|
||||
|
||||
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" />,
|
||||
};
|
||||
@ -143,14 +143,17 @@ const sheetVariants = cva(
|
||||
|
||||
export interface DialogContentProps
|
||||
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
|
||||
VariantProps<typeof sheetVariants> {}
|
||||
VariantProps<typeof sheetVariants> {
|
||||
showOverlay?: boolean;
|
||||
sheetClass?: string;
|
||||
}
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
DialogContentProps
|
||||
>(({ position, size, className, children, ...props }, ref) => (
|
||||
>(({ position, size, className, sheetClass, showOverlay = true, children, ...props }, ref) => (
|
||||
<SheetPortal position={position}>
|
||||
<SheetOverlay />
|
||||
{showOverlay && <SheetOverlay className={sheetClass} />}
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ position, size }), className)}
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -18,6 +18,10 @@
|
||||
--card-border-tint: 112 205 159;
|
||||
--card-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--field-card: 95 74% 90%;
|
||||
--field-card-border: 95.08 71.08% 67.45%;
|
||||
--field-card-foreground: 222.2 47.4% 11.2%;
|
||||
|
||||
--widget: 0 0% 97%;
|
||||
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
@ -38,6 +42,8 @@
|
||||
--ring: 95.08 71.08% 67.45%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
--warning: 54 96% 45%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
@ -55,6 +61,8 @@
|
||||
--card-border-tint: 112 205 159;
|
||||
--card-foreground: 0 0% 95%;
|
||||
|
||||
--widget: 0 0% 14.9%;
|
||||
|
||||
--border: 0 0% 27.9%;
|
||||
--input: 0 0% 27.9%;
|
||||
|
||||
@ -73,6 +81,8 @@
|
||||
--ring: 95.08 71.08% 67.45%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
|
||||
--warning: 54 96% 45%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user