mirror of
https://github.com/documenso/documenso.git
synced 2025-11-14 00:32:43 +10:00
Merge branch 'main' into feat/account-deletion
This commit is contained in:
@ -5,7 +5,7 @@ import { useState } from 'react';
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
import { DocumentData } from '@documenso/prisma/client';
|
||||
import type { DocumentData } from '@documenso/prisma/client';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Dialog, DialogOverlay, DialogPortal } from '../../primitives/dialog';
|
||||
|
||||
@ -1,22 +1,33 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cva } from 'class-variance-authority';
|
||||
import type { VariantProps } from 'class-variance-authority';
|
||||
import { cva } from 'class-variance-authority';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
const alertVariants = cva(
|
||||
'relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11',
|
||||
'relative w-full rounded-lg p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&>svg~*]:pl-8',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: 'bg-background text-foreground',
|
||||
destructive:
|
||||
'text-destructive border-destructive/50 dark:border-destructive [&>svg]:text-destructive text-destructive',
|
||||
default:
|
||||
'bg-green-50 text-green-700 [&_.alert-title]:text-green-800 [&>svg]:text-green-400',
|
||||
neutral:
|
||||
'bg-gray-50 dark:bg-neutral-900/20 text-muted-foreground [&_.alert-title]:text-foreground',
|
||||
secondary: 'bg-blue-50 text-blue-700 [&_.alert-title]:text-blue-800 [&>svg]:text-blue-400',
|
||||
destructive: 'bg-red-50 text-red-700 [&_.alert-title]:text-red-800 [&>svg]:text-red-400',
|
||||
warning:
|
||||
'bg-yellow-50 text-yellow-700 [&_.alert-title]:text-yellow-800 [&>svg]:text-yellow-400',
|
||||
},
|
||||
padding: {
|
||||
tighter: 'p-2',
|
||||
tight: 'px-4 py-2',
|
||||
default: 'p-4',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'default',
|
||||
padding: 'default',
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -24,19 +35,20 @@ const alertVariants = cva(
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
|
||||
>(({ className, variant, padding, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant, padding }), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
Alert.displayName = 'Alert';
|
||||
|
||||
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
|
||||
{...props}
|
||||
/>
|
||||
<h5 ref={ref} className={cn('alert-title text-base font-medium', className)} {...props} />
|
||||
),
|
||||
);
|
||||
|
||||
@ -46,7 +58,7 @@ const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn('text-sm [&_p]:leading-relaxed', className)} {...props} />
|
||||
<div ref={ref} className={cn('text-sm', className)} {...props} />
|
||||
));
|
||||
|
||||
AlertDescription.displayName = 'AlertDescription';
|
||||
|
||||
@ -35,7 +35,7 @@ const CommandDialog = ({ children, commandProps, ...props }: CommandDialogProps)
|
||||
<DialogContent className="overflow-hidden p-0 shadow-2xl">
|
||||
<Command
|
||||
{...commandProps}
|
||||
className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4"
|
||||
className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-0 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4"
|
||||
>
|
||||
{children}
|
||||
</Command>
|
||||
@ -92,7 +92,7 @@ const CommandGroup = React.forwardRef<
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium',
|
||||
'text-foreground [&_[cmdk-group-heading]]:text-muted-foreground mx-2 overflow-hidden border-b pb-2 last:border-0 [&_[cmdk-group-heading]]:mt-2 [&_[cmdk-group-heading]]:px-0 [&_[cmdk-group-heading]]:py-2 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-normal [&_[cmdk-group-heading]]:opacity-50 ',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@ -121,7 +121,7 @@ const CommandItem = React.forwardRef<
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
'aria-selected:bg-accent aria-selected:text-accent-foreground relative -mx-2 -my-1 flex cursor-default select-none items-center rounded-lg px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Table } from '@tanstack/react-table';
|
||||
import type { Table } from '@tanstack/react-table';
|
||||
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
|
||||
import { match } from 'ts-pattern';
|
||||
|
||||
|
||||
@ -2,36 +2,53 @@
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import {
|
||||
import type {
|
||||
ColumnDef,
|
||||
PaginationState,
|
||||
Table as TTable,
|
||||
Updater,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
VisibilityState,
|
||||
} from '@tanstack/react-table';
|
||||
import { flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';
|
||||
|
||||
import { Skeleton } from './skeleton';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from './table';
|
||||
|
||||
export type DataTableChildren<TData> = (_table: TTable<TData>) => React.ReactNode;
|
||||
|
||||
export interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
columnVisibility?: VisibilityState;
|
||||
data: TData[];
|
||||
perPage?: number;
|
||||
currentPage?: number;
|
||||
totalPages?: number;
|
||||
onPaginationChange?: (_page: number, _perPage: number) => void;
|
||||
onClearFilters?: () => void;
|
||||
hasFilters?: boolean;
|
||||
children?: DataTableChildren<TData>;
|
||||
skeleton?: {
|
||||
enable: boolean;
|
||||
rows: number;
|
||||
component?: React.ReactNode;
|
||||
};
|
||||
error?: {
|
||||
enable: boolean;
|
||||
component?: React.ReactNode;
|
||||
};
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
columnVisibility,
|
||||
data,
|
||||
error,
|
||||
perPage,
|
||||
currentPage,
|
||||
totalPages,
|
||||
skeleton,
|
||||
hasFilters,
|
||||
onClearFilters,
|
||||
onPaginationChange,
|
||||
children,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
@ -67,6 +84,7 @@ export function DataTable<TData, TValue>({
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
state: {
|
||||
pagination: manualPagination ? pagination : undefined,
|
||||
columnVisibility,
|
||||
},
|
||||
manualPagination,
|
||||
pageCount: totalPages,
|
||||
@ -103,10 +121,31 @@ export function DataTable<TData, TValue>({
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : error?.enable ? (
|
||||
<TableRow>
|
||||
{error.component ?? (
|
||||
<TableCell colSpan={columns.length} className="h-32 text-center">
|
||||
Something went wrong.
|
||||
</TableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
) : skeleton?.enable ? (
|
||||
Array.from({ length: skeleton.rows }).map((_, i) => (
|
||||
<TableRow key={`skeleton-row-${i}`}>{skeleton.component ?? <Skeleton />}</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
<TableCell colSpan={columns.length} className="h-32 text-center">
|
||||
<p>No results found</p>
|
||||
|
||||
{hasFilters && onClearFilters !== undefined && (
|
||||
<button
|
||||
onClick={() => onClearFilters()}
|
||||
className="text-foreground mt-1 text-sm"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
|
||||
@ -5,6 +5,7 @@ import { motion } from 'framer-motion';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
|
||||
import { APP_DOCUMENT_UPLOAD_SIZE_LIMIT } from '@documenso/lib/constants/app';
|
||||
import { megabytesToBytes } from '@documenso/lib/universal/unit-convertions';
|
||||
|
||||
import { cn } from '../lib/utils';
|
||||
@ -89,6 +90,7 @@ export type DocumentDropzoneProps = {
|
||||
disabled?: boolean;
|
||||
disabledMessage?: string;
|
||||
onDrop?: (_file: File) => void | Promise<void>;
|
||||
onDropRejected?: () => void | Promise<void>;
|
||||
type?: 'document' | 'template';
|
||||
[key: string]: unknown;
|
||||
};
|
||||
@ -96,6 +98,7 @@ export type DocumentDropzoneProps = {
|
||||
export const DocumentDropzone = ({
|
||||
className,
|
||||
onDrop,
|
||||
onDropRejected,
|
||||
disabled,
|
||||
disabledMessage = 'You cannot upload documents at this time.',
|
||||
type = 'document',
|
||||
@ -112,7 +115,12 @@ export const DocumentDropzone = ({
|
||||
void onDrop(acceptedFile);
|
||||
}
|
||||
},
|
||||
maxSize: megabytesToBytes(50),
|
||||
onDropRejected: () => {
|
||||
if (onDropRejected) {
|
||||
void onDropRejected();
|
||||
}
|
||||
},
|
||||
maxSize: megabytesToBytes(APP_DOCUMENT_UPLOAD_SIZE_LIMIT),
|
||||
});
|
||||
|
||||
return (
|
||||
@ -175,7 +183,7 @@ export const DocumentDropzone = ({
|
||||
</p>
|
||||
|
||||
<p className="text-muted-foreground/80 mt-1 text-sm">
|
||||
{disabled ? disabledMessage : 'Drag & drop your document here.'}
|
||||
{disabled ? disabledMessage : 'Drag & drop your PDF here.'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { Caveat } from 'next/font/google';
|
||||
|
||||
@ -10,8 +10,10 @@ import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect';
|
||||
import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element';
|
||||
import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer';
|
||||
import { RECIPIENT_ROLES_DESCRIPTION } from '@documenso/lib/constants/recipient-roles';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||
import { RecipientRole } from '@documenso/prisma/client';
|
||||
import { FieldType, SendStatus } from '@documenso/prisma/client';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
@ -30,8 +32,7 @@ import {
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
import { FieldItem } from './field-item';
|
||||
import type { DocumentFlowStep } from './types';
|
||||
import { FRIENDLY_FIELD_TYPE } from './types';
|
||||
import { type DocumentFlowStep, FRIENDLY_FIELD_TYPE } from './types';
|
||||
|
||||
const fontCaveat = Caveat({
|
||||
weight: ['500'],
|
||||
@ -103,6 +104,12 @@ export const AddFieldsFormPartial = ({
|
||||
|
||||
const hasSelectedSignerBeenSent = selectedSigner?.sendStatus === SendStatus.SENT;
|
||||
|
||||
const isFieldsDisabled =
|
||||
!selectedSigner ||
|
||||
hasSelectedSignerBeenSent ||
|
||||
selectedSigner?.role === RecipientRole.VIEWER ||
|
||||
selectedSigner?.role === RecipientRole.CC;
|
||||
|
||||
const [isFieldWithinBounds, setIsFieldWithinBounds] = useState(false);
|
||||
const [coords, setCoords] = useState({
|
||||
x: 0,
|
||||
@ -282,12 +289,28 @@ export const AddFieldsFormPartial = ({
|
||||
setSelectedSigner(recipients.find((r) => r.sendStatus !== SendStatus.SENT) ?? recipients[0]);
|
||||
}, [recipients]);
|
||||
|
||||
const recipientsByRole = useMemo(() => {
|
||||
const recipientsByRole: Record<RecipientRole, Recipient[]> = {
|
||||
CC: [],
|
||||
VIEWER: [],
|
||||
SIGNER: [],
|
||||
APPROVER: [],
|
||||
};
|
||||
|
||||
recipients.forEach((recipient) => {
|
||||
recipientsByRole[recipient.role].push(recipient);
|
||||
});
|
||||
|
||||
return recipientsByRole;
|
||||
}, [recipients]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DocumentFlowFormContainerHeader
|
||||
title={documentFlow.title}
|
||||
description={documentFlow.description}
|
||||
/>
|
||||
|
||||
<DocumentFlowFormContainerContent>
|
||||
<div className="flex flex-col">
|
||||
{selectedField && (
|
||||
@ -352,72 +375,94 @@ export const AddFieldsFormPartial = ({
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput />
|
||||
|
||||
<CommandEmpty>
|
||||
<span className="text-muted-foreground inline-block px-4">
|
||||
No recipient matching this description was found.
|
||||
</span>
|
||||
</CommandEmpty>
|
||||
|
||||
<CommandGroup>
|
||||
{recipients.map((recipient, index) => (
|
||||
<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>
|
||||
)}
|
||||
{Object.entries(recipientsByRole).map(([role, recipients], roleIndex) => (
|
||||
<CommandGroup key={roleIndex}>
|
||||
<div className="text-muted-foreground mb-1 ml-2 mt-2 text-xs font-medium">
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
RECIPIENT_ROLES_DESCRIPTION[role as RecipientRole].roleName
|
||||
}
|
||||
</div>
|
||||
|
||||
{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('!rounded-2xl px-4 last:mb-1 [&:not(:first-child)]:mt-1', {
|
||||
'text-muted-foreground': recipient.sendStatus === SendStatus.SENT,
|
||||
})}
|
||||
onSelect={() => {
|
||||
setSelectedSigner(recipient);
|
||||
setShowRecipientsSelector(false);
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className="truncate"
|
||||
title={`${recipient.name} (${recipient.email})`}
|
||||
className={cn('text-foreground/70 truncate', {
|
||||
'text-foreground': 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>
|
||||
|
||||
<div className="ml-auto flex items-center justify-center">
|
||||
{recipient.sendStatus !== SendStatus.SENT ? (
|
||||
<Check
|
||||
aria-hidden={recipient !== selectedSigner}
|
||||
className={cn('h-4 w-4 flex-shrink-0', {
|
||||
'opacity-0': recipient !== selectedSigner,
|
||||
'opacity-100': recipient === selectedSigner,
|
||||
})}
|
||||
/>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Info className="mx-2 h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="text-muted-foreground max-w-xs">
|
||||
This document has already been sent to this recipient. You can no
|
||||
longer edit this recipient.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
|
||||
<div className="-mx-2 flex-1 overflow-y-auto px-2">
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-8">
|
||||
<fieldset disabled={isFieldsDisabled} className="grid grid-cols-2 gap-x-4 gap-y-8">
|
||||
<button
|
||||
type="button"
|
||||
className="group h-full w-full"
|
||||
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
|
||||
onClick={() => setSelectedField(FieldType.SIGNATURE)}
|
||||
onMouseDown={() => setSelectedField(FieldType.SIGNATURE)}
|
||||
data-selected={selectedField === FieldType.SIGNATURE ? true : undefined}
|
||||
@ -441,7 +486,6 @@ export const AddFieldsFormPartial = ({
|
||||
<button
|
||||
type="button"
|
||||
className="group h-full w-full"
|
||||
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
|
||||
onClick={() => setSelectedField(FieldType.EMAIL)}
|
||||
onMouseDown={() => setSelectedField(FieldType.EMAIL)}
|
||||
data-selected={selectedField === FieldType.EMAIL ? true : undefined}
|
||||
@ -464,7 +508,6 @@ export const AddFieldsFormPartial = ({
|
||||
<button
|
||||
type="button"
|
||||
className="group h-full w-full"
|
||||
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
|
||||
onClick={() => setSelectedField(FieldType.NAME)}
|
||||
onMouseDown={() => setSelectedField(FieldType.NAME)}
|
||||
data-selected={selectedField === FieldType.NAME ? true : undefined}
|
||||
@ -487,7 +530,6 @@ export const AddFieldsFormPartial = ({
|
||||
<button
|
||||
type="button"
|
||||
className="group h-full w-full"
|
||||
disabled={!selectedSigner || selectedSigner?.sendStatus === SendStatus.SENT}
|
||||
onClick={() => setSelectedField(FieldType.DATE)}
|
||||
onMouseDown={() => setSelectedField(FieldType.DATE)}
|
||||
data-selected={selectedField === FieldType.DATE ? true : undefined}
|
||||
@ -506,7 +548,7 @@ export const AddFieldsFormPartial = ({
|
||||
</CardContent>
|
||||
</Card>
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFlowFormContainerContent>
|
||||
|
||||
@ -4,19 +4,20 @@ import React, { useId } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { Plus, Trash } from 'lucide-react';
|
||||
import { BadgeCheck, Copy, Eye, PencilLine, Plus, Trash } from 'lucide-react';
|
||||
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
||||
|
||||
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||
import { nanoid } from '@documenso/lib/universal/id';
|
||||
import type { Field, Recipient } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, SendStatus } from '@documenso/prisma/client';
|
||||
import { DocumentStatus, RecipientRole, SendStatus } from '@documenso/prisma/client';
|
||||
import type { DocumentWithData } from '@documenso/prisma/types/document-with-data';
|
||||
|
||||
import { Button } from '../button';
|
||||
import { FormErrorMessage } from '../form/form-error-message';
|
||||
import { Input } from '../input';
|
||||
import { Label } from '../label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '../select';
|
||||
import { useStep } from '../stepper';
|
||||
import { useToast } from '../use-toast';
|
||||
import type { TAddSignersFormSchema } from './add-signers.types';
|
||||
@ -28,8 +29,16 @@ import {
|
||||
DocumentFlowFormContainerHeader,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
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[];
|
||||
@ -42,7 +51,7 @@ export const AddSignersFormPartial = ({
|
||||
documentFlow,
|
||||
recipients,
|
||||
document,
|
||||
fields: _fields,
|
||||
fields,
|
||||
onSubmit,
|
||||
}: AddSignersFormProps) => {
|
||||
const { toast } = useToast();
|
||||
@ -66,12 +75,14 @@ export const AddSignersFormPartial = ({
|
||||
formId: String(recipient.id),
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
role: recipient.role,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
formId: initialId,
|
||||
name: '',
|
||||
email: '',
|
||||
role: RecipientRole.SIGNER,
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -103,6 +114,7 @@ export const AddSignersFormPartial = ({
|
||||
formId: nanoid(12),
|
||||
name: '',
|
||||
email: '',
|
||||
role: RecipientRole.SIGNER,
|
||||
});
|
||||
};
|
||||
|
||||
@ -136,6 +148,10 @@ export const AddSignersFormPartial = ({
|
||||
/>
|
||||
<DocumentFlowFormContainerContent>
|
||||
<div className="flex w-full flex-col gap-y-4">
|
||||
{fields.map((field, index) => (
|
||||
<ShowFieldItem key={index} field={field} recipients={recipients} />
|
||||
))}
|
||||
|
||||
<AnimatePresence>
|
||||
{signers.map((signer, index) => (
|
||||
<motion.div
|
||||
@ -184,6 +200,48 @@ export const AddSignersFormPartial = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="w-[60px]">
|
||||
<Controller
|
||||
control={control}
|
||||
name={`signers.${index}.role`}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Select value={value} onValueChange={(x) => onChange(x)}>
|
||||
<SelectTrigger className="bg-background">{ROLE_ICONS[value]}</SelectTrigger>
|
||||
|
||||
<SelectContent className="" align="end">
|
||||
<SelectItem value={RecipientRole.SIGNER}>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.SIGNER]}</span>
|
||||
Signer
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value={RecipientRole.CC}>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.CC]}</span>
|
||||
Receives copy
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value={RecipientRole.APPROVER}>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.APPROVER]}</span>
|
||||
Approver
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
<SelectItem value={RecipientRole.VIEWER}>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.VIEWER]}</span>
|
||||
Viewer
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { RecipientRole } from '.prisma/client';
|
||||
|
||||
export const ZAddSignersFormSchema = z
|
||||
.object({
|
||||
signers: z.array(
|
||||
@ -8,6 +10,7 @@ export const ZAddSignersFormSchema = z
|
||||
nativeId: z.number().optional(),
|
||||
email: z.string().email().min(1),
|
||||
name: z.string(),
|
||||
role: z.nativeEnum(RecipientRole),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
@ -38,6 +38,7 @@ import {
|
||||
DocumentFlowFormContainerHeader,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
import { ShowFieldItem } from './show-field-item';
|
||||
import type { DocumentFlowStep } from './types';
|
||||
|
||||
export type AddSubjectFormProps = {
|
||||
@ -60,7 +61,6 @@ export const AddSubjectFormPartial = ({
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors, isSubmitting, touchedFields },
|
||||
getValues,
|
||||
setValue,
|
||||
} = useForm<TAddSubjectFormSchema>({
|
||||
defaultValues: {
|
||||
@ -98,6 +98,10 @@ export const AddSubjectFormPartial = ({
|
||||
/>
|
||||
<DocumentFlowFormContainerContent>
|
||||
<div className="flex flex-col">
|
||||
{fields.map((field, index) => (
|
||||
<ShowFieldItem key={index} field={field} recipients={recipients} />
|
||||
))}
|
||||
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div>
|
||||
<Label htmlFor="subject">
|
||||
@ -106,7 +110,6 @@ export const AddSubjectFormPartial = ({
|
||||
|
||||
<Input
|
||||
id="subject"
|
||||
// placeholder="Subject"
|
||||
className="bg-background mt-2"
|
||||
disabled={isSubmitting}
|
||||
{...register('meta.subject')}
|
||||
|
||||
@ -17,6 +17,7 @@ import {
|
||||
DocumentFlowFormContainerHeader,
|
||||
DocumentFlowFormContainerStep,
|
||||
} from './document-flow-root';
|
||||
import { ShowFieldItem } from './show-field-item';
|
||||
import type { DocumentFlowStep } from './types';
|
||||
|
||||
export type AddTitleFormProps = {
|
||||
@ -29,8 +30,8 @@ export type AddTitleFormProps = {
|
||||
|
||||
export const AddTitleFormPartial = ({
|
||||
documentFlow,
|
||||
recipients: _recipients,
|
||||
fields: _fields,
|
||||
recipients,
|
||||
fields,
|
||||
document,
|
||||
onSubmit,
|
||||
}: AddTitleFormProps) => {
|
||||
@ -55,6 +56,10 @@ export const AddTitleFormPartial = ({
|
||||
description={documentFlow.description}
|
||||
/>
|
||||
<DocumentFlowFormContainerContent>
|
||||
{fields.map((field, index) => (
|
||||
<ShowFieldItem key={index} field={field} recipients={recipients} />
|
||||
))}
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div>
|
||||
|
||||
49
packages/ui/primitives/document-flow/show-field-item.tsx
Normal file
49
packages/ui/primitives/document-flow/show-field-item.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
'use client';
|
||||
|
||||
import type { Prisma } from '@prisma/client';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { useFieldPageCoords } from '@documenso/lib/client-only/hooks/use-field-page-coords';
|
||||
|
||||
import { cn } from '../../lib/utils';
|
||||
import { Card, CardContent } from '../card';
|
||||
import { FRIENDLY_FIELD_TYPE } from './types';
|
||||
|
||||
export type ShowFieldItemProps = {
|
||||
field: Prisma.FieldGetPayload<null>;
|
||||
recipients: Prisma.RecipientGetPayload<null>[];
|
||||
};
|
||||
|
||||
export const ShowFieldItem = ({ field, recipients }: ShowFieldItemProps) => {
|
||||
const coords = useFieldPageCoords(field);
|
||||
|
||||
const signerEmail =
|
||||
recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '';
|
||||
|
||||
return createPortal(
|
||||
<div
|
||||
className={cn('pointer-events-none absolute z-10 opacity-75')}
|
||||
style={{
|
||||
top: `${coords.y}px`,
|
||||
left: `${coords.x}px`,
|
||||
height: `${coords.height}px`,
|
||||
width: `${coords.width}px`,
|
||||
}}
|
||||
>
|
||||
<Card className={cn('bg-background h-full w-full')}>
|
||||
<CardContent
|
||||
className={cn(
|
||||
'text-muted-foreground/50 flex h-full w-full flex-col items-center justify-center p-2',
|
||||
)}
|
||||
>
|
||||
{FRIENDLY_FIELD_TYPE[field.type]}
|
||||
|
||||
<p className="text-muted-foreground/50 w-full truncate text-center text-xs">
|
||||
{signerEmail}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
};
|
||||
@ -16,6 +16,7 @@ const DPI = 2;
|
||||
export type SignaturePadProps = Omit<HTMLAttributes<HTMLCanvasElement>, 'onChange'> & {
|
||||
onChange?: (_signatureDataUrl: string | null) => void;
|
||||
containerClassName?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const SignaturePad = ({
|
||||
@ -23,6 +24,7 @@ export const SignaturePad = ({
|
||||
containerClassName,
|
||||
defaultValue,
|
||||
onChange,
|
||||
disabled = false,
|
||||
...props
|
||||
}: SignaturePadProps) => {
|
||||
const $el = useRef<HTMLCanvasElement>(null);
|
||||
@ -214,7 +216,11 @@ export const SignaturePad = ({
|
||||
}, [defaultValue]);
|
||||
|
||||
return (
|
||||
<div className={cn('relative block', containerClassName)}>
|
||||
<div
|
||||
className={cn('relative block', containerClassName, {
|
||||
'pointer-events-none opacity-50': disabled,
|
||||
})}
|
||||
>
|
||||
<canvas
|
||||
ref={$el}
|
||||
className={cn('relative block dark:invert', className)}
|
||||
|
||||
Reference in New Issue
Block a user