Compare commits
4 Commits
release
...
feat/add-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
813d02d36b | ||
|
|
bad71d92c5 | ||
|
|
3fbf6c5842 | ||
|
|
4156f2afce |
@@ -1,9 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
@@ -14,208 +9,64 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
|
||||
import { DocumentSigningDisclosure } from '../general/document-signing/document-signing-disclosure';
|
||||
|
||||
export type NextSigner = {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
type ConfirmationDialogProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: (nextSigner?: NextSigner) => void;
|
||||
onConfirm: () => void;
|
||||
hasUninsertedFields: boolean;
|
||||
isSubmitting: boolean;
|
||||
allowDictateNextSigner?: boolean;
|
||||
defaultNextSigner?: NextSigner;
|
||||
};
|
||||
|
||||
const ZNextSignerFormSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
email: z.string().email('Invalid email address'),
|
||||
});
|
||||
|
||||
type TNextSignerFormSchema = z.infer<typeof ZNextSignerFormSchema>;
|
||||
|
||||
export function AssistantConfirmationDialog({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
hasUninsertedFields,
|
||||
isSubmitting,
|
||||
allowDictateNextSigner = false,
|
||||
defaultNextSigner,
|
||||
}: ConfirmationDialogProps) {
|
||||
const [isEditingNextSigner, setIsEditingNextSigner] = useState(false);
|
||||
|
||||
const form = useForm<TNextSignerFormSchema>({
|
||||
resolver: zodResolver(ZNextSignerFormSchema),
|
||||
defaultValues: {
|
||||
name: defaultNextSigner?.name ?? '',
|
||||
email: defaultNextSigner?.email ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
const onOpenChange = () => {
|
||||
if (form.formState.isSubmitting) {
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
form.reset({
|
||||
name: defaultNextSigner?.name ?? '',
|
||||
email: defaultNextSigner?.email ?? '',
|
||||
});
|
||||
|
||||
setIsEditingNextSigner(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onFormSubmit = async (data: TNextSignerFormSchema) => {
|
||||
if (allowDictateNextSigner && data.name && data.email) {
|
||||
await onConfirm({
|
||||
name: data.name,
|
||||
email: data.email,
|
||||
});
|
||||
} else {
|
||||
await onConfirm();
|
||||
}
|
||||
};
|
||||
|
||||
const isNextSignerValid = !allowDictateNextSigner || (form.watch('name') && form.watch('email'));
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||
<fieldset
|
||||
disabled={form.formState.isSubmitting || isSubmitting}
|
||||
className="border-none p-0"
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Complete Document</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
Are you sure you want to complete the document? This action cannot be undone.
|
||||
Please ensure that you have completed prefilling all relevant fields before
|
||||
proceeding.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans>Complete Document</Trans>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<Trans>
|
||||
Are you sure you want to complete the document? This action cannot be undone. Please
|
||||
ensure that you have completed prefilling all relevant fields before proceeding.
|
||||
</Trans>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-4">
|
||||
{allowDictateNextSigner && (
|
||||
<div className="space-y-4">
|
||||
{!isEditingNextSigner && (
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
The next recipient to sign this document will be{' '}
|
||||
<span className="font-semibold">{form.watch('name')}</span> (
|
||||
<span className="font-semibold">{form.watch('email')}</span>).
|
||||
</p>
|
||||
<div className="flex flex-col gap-4">
|
||||
<DocumentSigningDisclosure />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
className="mt-2"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsEditingNextSigner((prev) => !prev)}
|
||||
>
|
||||
<Trans>Update Recipient</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isEditingNextSigner && (
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Name</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
className="mt-2"
|
||||
placeholder="Enter the next signer's name"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Email</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="email"
|
||||
className="mt-2"
|
||||
placeholder="Enter the next signer's email"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DocumentSigningDisclosure className="mt-4" />
|
||||
</div>
|
||||
|
||||
<DialogFooter className="mt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={onClose}
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant={hasUninsertedFields ? 'destructive' : 'default'}
|
||||
disabled={form.formState.isSubmitting || !isNextSignerValid}
|
||||
loading={form.formState.isSubmitting}
|
||||
>
|
||||
{form.formState.isSubmitting ? (
|
||||
<Trans>Submitting...</Trans>
|
||||
) : hasUninsertedFields ? (
|
||||
<Trans>Proceed</Trans>
|
||||
) : (
|
||||
<Trans>Continue</Trans>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
<DialogFooter className="mt-4">
|
||||
<Button variant="secondary" onClick={onClose} disabled={isSubmitting}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant={hasUninsertedFields ? 'destructive' : 'default'}
|
||||
onClick={onConfirm}
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : hasUninsertedFields ? 'Proceed' : 'Continue'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
128
apps/remix/app/components/dialogs/folder-create-dialog.tsx
Normal file
128
apps/remix/app/components/dialogs/folder-create-dialog.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { FolderPlusIcon } from 'lucide-react';
|
||||
import { useSearchParams } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { parseToIntegerArray } from '@documenso/lib/utils/params';
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import { ZFindDocumentsInternalRequestSchema } from '@documenso/trpc/server/document-router/schema';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
const ZSearchParamsSchema = ZFindDocumentsInternalRequestSchema.pick({
|
||||
status: true,
|
||||
period: true,
|
||||
page: true,
|
||||
perPage: true,
|
||||
query: true,
|
||||
}).extend({
|
||||
senderIds: z.string().transform(parseToIntegerArray).optional().catch([]),
|
||||
folderId: z
|
||||
.string()
|
||||
.transform((val) => parseInt(val, 10))
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type CreateFolderDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||
|
||||
export const CreateFolderDialog = ({ trigger, ...props }: CreateFolderDialogProps) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false);
|
||||
const [newFolderName, setNewFolderName] = useState('');
|
||||
|
||||
const findDocumentSearchParams = useMemo(
|
||||
() => ZSearchParamsSchema.safeParse(Object.fromEntries(searchParams.entries())).data || {},
|
||||
[searchParams],
|
||||
);
|
||||
|
||||
const currentFolderId = findDocumentSearchParams.folderId;
|
||||
|
||||
const { mutateAsync: createFolder } = trpc.folder.createFolder.useMutation();
|
||||
|
||||
const handleCreateFolder = async () => {
|
||||
if (!newFolderName.trim()) {
|
||||
toast({
|
||||
title: 'Folder name is required',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await createFolder({
|
||||
name: newFolderName,
|
||||
parentId: currentFolderId,
|
||||
});
|
||||
|
||||
setNewFolderName('');
|
||||
setIsCreateFolderOpen(false);
|
||||
|
||||
toast({
|
||||
title: 'Folder created successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating folder:', error);
|
||||
toast({
|
||||
title: 'Failed to create folder',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Dialog {...props} open={isCreateFolderOpen} onOpenChange={setIsCreateFolderOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{trigger ?? (
|
||||
<Button variant="outline" className="flex items-center space-x-2">
|
||||
<FolderPlusIcon className="h-4 w-4" />
|
||||
<span>Create Folder</span>
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New Folder</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter a name for your new folder. Folders help you organize your documents.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<Label htmlFor="folder-name">Folder Name</Label>
|
||||
<Input
|
||||
id="folder-name"
|
||||
value={newFolderName}
|
||||
onChange={(e) => setNewFolderName(e.target.value)}
|
||||
placeholder="My Folder"
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" onClick={() => setIsCreateFolderOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCreateFolder}>Create</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
80
apps/remix/app/components/dialogs/folder-delete-dialog.tsx
Normal file
80
apps/remix/app/components/dialogs/folder-delete-dialog.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type FolderDeleteDialogProps = {
|
||||
folder: TFolderWithSubfolders | null;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||
|
||||
export const FolderDeleteDialog = ({ folder, isOpen, onOpenChange }: FolderDeleteDialogProps) => {
|
||||
const { toast } = useToast();
|
||||
const { mutateAsync: deleteFolder } = trpc.folder.deleteFolder.useMutation();
|
||||
|
||||
const handleDeleteFolder = async () => {
|
||||
if (!folder) return;
|
||||
|
||||
try {
|
||||
await deleteFolder({
|
||||
id: folder.id,
|
||||
});
|
||||
|
||||
onOpenChange(false);
|
||||
|
||||
toast({
|
||||
title: 'Folder deleted successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting folder:', error);
|
||||
toast({
|
||||
title: 'Failed to delete folder',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Delete Folder</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to delete this folder?
|
||||
{folder && folder._count.documents > 0 && (
|
||||
<span className="text-destructive mt-2 block">
|
||||
Warning: This folder contains {folder._count.documents} document(s). Deleting it
|
||||
will move all documents to the root.
|
||||
</span>
|
||||
)}
|
||||
{folder && folder._count.subfolders > 0 && (
|
||||
<span className="text-destructive mt-2 block">
|
||||
Warning: This folder contains {folder._count.subfolders} subfolder(s). Deleting it
|
||||
will delete all subfolders and their contents.
|
||||
</span>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleDeleteFolder}>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
97
apps/remix/app/components/dialogs/folder-move-dialog.tsx
Normal file
97
apps/remix/app/components/dialogs/folder-move-dialog.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { FolderIcon, HomeIcon } from 'lucide-react';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type FolderMoveDialogProps = {
|
||||
foldersData: TFolderWithSubfolders[] | undefined;
|
||||
folder: TFolderWithSubfolders | null;
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||
|
||||
export const FolderMoveDialog = ({
|
||||
foldersData,
|
||||
folder,
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
}: FolderMoveDialogProps) => {
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutateAsync: moveFolder } = trpc.folder.moveFolder.useMutation();
|
||||
|
||||
const handleMoveFolder = async (targetFolderId: number | null) => {
|
||||
if (!folder) return;
|
||||
|
||||
try {
|
||||
await moveFolder({
|
||||
id: folder.id,
|
||||
parentId: targetFolderId,
|
||||
});
|
||||
|
||||
onOpenChange(false);
|
||||
|
||||
toast({
|
||||
title: 'Folder moved successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error moving folder:', error);
|
||||
toast({
|
||||
title: 'Failed to move folder',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Move Folder</DialogTitle>
|
||||
<DialogDescription>Select a destination for this folder.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
onClick={async () => await handleMoveFolder(null)}
|
||||
>
|
||||
<HomeIcon className="mr-2 h-4 w-4" />
|
||||
Root
|
||||
</Button>
|
||||
|
||||
{foldersData &&
|
||||
foldersData
|
||||
.filter((f) => f.id !== folder?.id)
|
||||
.map((f) => (
|
||||
<Button
|
||||
key={f.id}
|
||||
variant="outline"
|
||||
className="w-full justify-start"
|
||||
onClick={async () => await handleMoveFolder(f.id)}
|
||||
>
|
||||
<FolderIcon className="mr-2 h-4 w-4" />
|
||||
{f.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
121
apps/remix/app/components/dialogs/folder-rename-dialog.tsx
Normal file
121
apps/remix/app/components/dialogs/folder-rename-dialog.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import type * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
|
||||
import { trpc } from '@documenso/trpc/react';
|
||||
import type { TFolderWithSubfolders } from '@documenso/trpc/server/folder-router/schema';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
import { Label } from '@documenso/ui/primitives/label';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
export type FolderRenameDialogProps = {
|
||||
trigger?: React.ReactNode;
|
||||
folder?: TFolderWithSubfolders | null;
|
||||
isOpen?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
} & Omit<DialogPrimitive.DialogProps, 'children'>;
|
||||
|
||||
export const FolderRenameDialog = ({
|
||||
trigger,
|
||||
folder,
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
...props
|
||||
}: FolderRenameDialogProps) => {
|
||||
const { toast } = useToast();
|
||||
const [isRenameFolderOpen, setIsRenameFolderOpen] = useState(isOpen || false);
|
||||
const [renameFolderName, setRenameFolderName] = useState('');
|
||||
const [folderToRename, setFolderToRename] = useState<TFolderWithSubfolders | null>(
|
||||
folder || null,
|
||||
);
|
||||
|
||||
const { mutateAsync: updateFolder } = trpc.folder.updateFolder.useMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen !== undefined) {
|
||||
setIsRenameFolderOpen(isOpen);
|
||||
}
|
||||
|
||||
if (folder && isOpen) {
|
||||
setFolderToRename(folder);
|
||||
setRenameFolderName(folder.name);
|
||||
}
|
||||
}, [isOpen, folder]);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
setIsRenameFolderOpen(open);
|
||||
if (onOpenChange) {
|
||||
onOpenChange(open);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRenameFolder = async () => {
|
||||
if (!folderToRename) return;
|
||||
|
||||
if (!renameFolderName.trim()) {
|
||||
toast({
|
||||
title: 'Folder name is required',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateFolder({
|
||||
id: folderToRename.id,
|
||||
name: renameFolderName,
|
||||
});
|
||||
|
||||
toast({
|
||||
title: 'Folder renamed successfully',
|
||||
});
|
||||
|
||||
setFolderToRename(null);
|
||||
setRenameFolderName('');
|
||||
handleOpenChange(false);
|
||||
} catch (error) {
|
||||
console.error('Error renaming folder:', error);
|
||||
toast({
|
||||
title: 'Failed to rename folder',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog {...props} open={isRenameFolderOpen} onOpenChange={handleOpenChange}>
|
||||
{trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Rename Folder</DialogTitle>
|
||||
<DialogDescription>Enter a new name for your folder.</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<Label htmlFor="rename-folder-name">Folder Name</Label>
|
||||
<Input
|
||||
id="rename-folder-name"
|
||||
value={renameFolderName}
|
||||
onChange={(e) => setRenameFolderName(e.target.value)}
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="secondary" onClick={() => handleOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleRenameFolder}>Rename</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -531,27 +531,6 @@ export const SignUpForm = ({
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
<p className="text-muted-foreground mt-6 text-xs">
|
||||
<Trans>
|
||||
By proceeding, you agree to our{' '}
|
||||
<Link
|
||||
to="https://documen.so/terms"
|
||||
target="_blank"
|
||||
className="text-documenso-700 duration-200 hover:opacity-70"
|
||||
>
|
||||
Terms of Service
|
||||
</Link>{' '}
|
||||
and{' '}
|
||||
<Link
|
||||
to="https://documen.so/privacy"
|
||||
target="_blank"
|
||||
className="text-documenso-700 duration-200 hover:opacity-70"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { Field } from '@prisma/client';
|
||||
import { RecipientRole } from '@prisma/client';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { match } from 'ts-pattern';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
import { Button } from '@documenso/ui/primitives/button';
|
||||
@@ -17,15 +14,6 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@documenso/ui/primitives/dialog';
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@documenso/ui/primitives/form/form';
|
||||
import { Input } from '@documenso/ui/primitives/input';
|
||||
|
||||
import { DocumentSigningDisclosure } from '~/components/general/document-signing/document-signing-disclosure';
|
||||
|
||||
@@ -34,23 +22,11 @@ export type DocumentSigningCompleteDialogProps = {
|
||||
documentTitle: string;
|
||||
fields: Field[];
|
||||
fieldsValidated: () => void | Promise<void>;
|
||||
onSignatureComplete: (nextSigner?: { name: string; email: string }) => void | Promise<void>;
|
||||
onSignatureComplete: () => void | Promise<void>;
|
||||
role: RecipientRole;
|
||||
disabled?: boolean;
|
||||
allowDictateNextSigner?: boolean;
|
||||
defaultNextSigner?: {
|
||||
name: string;
|
||||
email: string;
|
||||
};
|
||||
};
|
||||
|
||||
const ZNextSignerFormSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
email: z.string().email('Invalid email address'),
|
||||
});
|
||||
|
||||
type TNextSignerFormSchema = z.infer<typeof ZNextSignerFormSchema>;
|
||||
|
||||
export const DocumentSigningCompleteDialog = ({
|
||||
isSubmitting,
|
||||
documentTitle,
|
||||
@@ -59,54 +35,19 @@ export const DocumentSigningCompleteDialog = ({
|
||||
onSignatureComplete,
|
||||
role,
|
||||
disabled = false,
|
||||
allowDictateNextSigner = false,
|
||||
defaultNextSigner,
|
||||
}: DocumentSigningCompleteDialogProps) => {
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [isEditingNextSigner, setIsEditingNextSigner] = useState(false);
|
||||
|
||||
const form = useForm<TNextSignerFormSchema>({
|
||||
resolver: allowDictateNextSigner ? zodResolver(ZNextSignerFormSchema) : undefined,
|
||||
defaultValues: {
|
||||
name: defaultNextSigner?.name ?? '',
|
||||
email: defaultNextSigner?.email ?? '',
|
||||
},
|
||||
});
|
||||
|
||||
const isComplete = useMemo(() => !fieldsContainUnsignedRequiredField(fields), [fields]);
|
||||
|
||||
const handleOpenChange = (open: boolean) => {
|
||||
if (form.formState.isSubmitting || !isComplete) {
|
||||
if (isSubmitting || !isComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (open) {
|
||||
form.reset({
|
||||
name: defaultNextSigner?.name ?? '',
|
||||
email: defaultNextSigner?.email ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
setIsEditingNextSigner(false);
|
||||
setShowDialog(open);
|
||||
};
|
||||
|
||||
const onFormSubmit = async (data: TNextSignerFormSchema) => {
|
||||
console.log('data', data);
|
||||
console.log('form.formState.errors', form.formState.errors);
|
||||
try {
|
||||
if (allowDictateNextSigner && data.name && data.email) {
|
||||
await onSignatureComplete({ name: data.name, email: data.email });
|
||||
} else {
|
||||
await onSignatureComplete();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error completing signature:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const isNextSignerValid = !allowDictateNextSigner || (form.watch('name') && form.watch('email'));
|
||||
|
||||
return (
|
||||
<Dialog open={showDialog} onOpenChange={handleOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
@@ -130,184 +71,110 @@ export const DocumentSigningCompleteDialog = ({
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onFormSubmit)}>
|
||||
<fieldset disabled={form.formState.isSubmitting} className="border-none p-0">
|
||||
<DialogTitle>
|
||||
<div className="text-foreground text-xl font-semibold">
|
||||
{match(role)
|
||||
.with(RecipientRole.VIEWER, () => <Trans>Complete Viewing</Trans>)
|
||||
.with(RecipientRole.SIGNER, () => <Trans>Complete Signing</Trans>)
|
||||
.with(RecipientRole.APPROVER, () => <Trans>Complete Approval</Trans>)
|
||||
.with(RecipientRole.CC, () => <Trans>Complete Viewing</Trans>)
|
||||
.with(RecipientRole.ASSISTANT, () => <Trans>Complete Assisting</Trans>)
|
||||
.exhaustive()}
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogTitle>
|
||||
<div className="text-foreground text-xl font-semibold">
|
||||
{match(role)
|
||||
.with(RecipientRole.VIEWER, () => <Trans>Complete Viewing</Trans>)
|
||||
.with(RecipientRole.SIGNER, () => <Trans>Complete Signing</Trans>)
|
||||
.with(RecipientRole.APPROVER, () => <Trans>Complete Approval</Trans>)
|
||||
.with(RecipientRole.CC, () => <Trans>Complete Viewing</Trans>)
|
||||
.with(RecipientRole.ASSISTANT, () => <Trans>Complete Assisting</Trans>)
|
||||
.exhaustive()}
|
||||
</div>
|
||||
</DialogTitle>
|
||||
|
||||
<div className="text-muted-foreground max-w-[50ch]">
|
||||
{match(role)
|
||||
.with(RecipientRole.VIEWER, () => (
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="inline-flex flex-wrap">
|
||||
You are about to complete viewing "
|
||||
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
||||
{documentTitle}
|
||||
</span>
|
||||
".
|
||||
</span>
|
||||
<br /> Are you sure?
|
||||
</Trans>
|
||||
<div className="text-muted-foreground max-w-[50ch]">
|
||||
{match(role)
|
||||
.with(RecipientRole.VIEWER, () => (
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="inline-flex flex-wrap">
|
||||
You are about to complete viewing "
|
||||
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
||||
{documentTitle}
|
||||
</span>
|
||||
))
|
||||
.with(RecipientRole.SIGNER, () => (
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="inline-flex flex-wrap">
|
||||
You are about to complete signing "
|
||||
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
||||
{documentTitle}
|
||||
</span>
|
||||
".
|
||||
</span>
|
||||
<br /> Are you sure?
|
||||
</Trans>
|
||||
".
|
||||
</span>
|
||||
<br /> Are you sure?
|
||||
</Trans>
|
||||
</span>
|
||||
))
|
||||
.with(RecipientRole.SIGNER, () => (
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="inline-flex flex-wrap">
|
||||
You are about to complete signing "
|
||||
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
||||
{documentTitle}
|
||||
</span>
|
||||
))
|
||||
.with(RecipientRole.APPROVER, () => (
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="inline-flex flex-wrap">
|
||||
You are about to complete approving{' '}
|
||||
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
||||
"{documentTitle}"
|
||||
</span>
|
||||
.
|
||||
</span>
|
||||
<br /> Are you sure?
|
||||
</Trans>
|
||||
".
|
||||
</span>
|
||||
<br /> Are you sure?
|
||||
</Trans>
|
||||
</span>
|
||||
))
|
||||
.with(RecipientRole.APPROVER, () => (
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="inline-flex flex-wrap">
|
||||
You are about to complete approving{' '}
|
||||
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
||||
"{documentTitle}"
|
||||
</span>
|
||||
))
|
||||
.otherwise(() => (
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="inline-flex flex-wrap">
|
||||
You are about to complete viewing "
|
||||
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
||||
{documentTitle}
|
||||
</span>
|
||||
".
|
||||
</span>
|
||||
<br /> Are you sure?
|
||||
</Trans>
|
||||
.
|
||||
</span>
|
||||
<br /> Are you sure?
|
||||
</Trans>
|
||||
</span>
|
||||
))
|
||||
.otherwise(() => (
|
||||
<span>
|
||||
<Trans>
|
||||
<span className="inline-flex flex-wrap">
|
||||
You are about to complete viewing "
|
||||
<span className="inline-block max-w-[11rem] truncate align-baseline">
|
||||
{documentTitle}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
".
|
||||
</span>
|
||||
<br /> Are you sure?
|
||||
</Trans>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{allowDictateNextSigner && (
|
||||
<div className="mt-4 flex flex-col gap-4">
|
||||
{!isEditingNextSigner && (
|
||||
<div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
The next recipient to sign this document will be{' '}
|
||||
<span className="font-semibold">{form.watch('name')}</span> (
|
||||
<span className="font-semibold">{form.watch('email')}</span>).
|
||||
</p>
|
||||
<DocumentSigningDisclosure className="mt-4" />
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
className="mt-2"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setIsEditingNextSigner((prev) => !prev)}
|
||||
>
|
||||
<Trans>Update Recipient</Trans>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setShowDialog(false);
|
||||
}}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
{isEditingNextSigner && (
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Name</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
className="mt-2"
|
||||
placeholder="Enter the next signer's name"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
<Trans>Email</Trans>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="email"
|
||||
className="mt-2"
|
||||
placeholder="Enter the next signer's email"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DocumentSigningDisclosure className="mt-4" />
|
||||
|
||||
<DialogFooter className="mt-4">
|
||||
<div className="flex w-full flex-1 flex-nowrap gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
className="dark:bg-muted dark:hover:bg-muted/80 flex-1 bg-black/5 hover:bg-black/10"
|
||||
variant="secondary"
|
||||
onClick={() => setShowDialog(false)}
|
||||
disabled={form.formState.isSubmitting}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="flex-1"
|
||||
disabled={!isComplete || !isNextSignerValid}
|
||||
loading={form.formState.isSubmitting}
|
||||
>
|
||||
{match(role)
|
||||
.with(RecipientRole.VIEWER, () => <Trans>Mark as Viewed</Trans>)
|
||||
.with(RecipientRole.SIGNER, () => <Trans>Sign</Trans>)
|
||||
.with(RecipientRole.APPROVER, () => <Trans>Approve</Trans>)
|
||||
.with(RecipientRole.CC, () => <Trans>Mark as Viewed</Trans>)
|
||||
.with(RecipientRole.ASSISTANT, () => <Trans>Complete</Trans>)
|
||||
.exhaustive()}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</fieldset>
|
||||
</form>
|
||||
</Form>
|
||||
<Button
|
||||
type="button"
|
||||
className="flex-1"
|
||||
disabled={!isComplete}
|
||||
loading={isSubmitting}
|
||||
onClick={onSignatureComplete}
|
||||
>
|
||||
{match(role)
|
||||
.with(RecipientRole.VIEWER, () => <Trans>Mark as Viewed</Trans>)
|
||||
.with(RecipientRole.SIGNER, () => <Trans>Sign</Trans>)
|
||||
.with(RecipientRole.APPROVER, () => <Trans>Approve</Trans>)
|
||||
.with(RecipientRole.CC, () => <Trans>Mark as Viewed</Trans>)
|
||||
.with(RecipientRole.ASSISTANT, () => <Trans>Complete</Trans>)
|
||||
.exhaustive()}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { useId, useMemo, useState } from 'react';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { type Field, FieldType, type Recipient, RecipientRole } from '@prisma/client';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics';
|
||||
import { useOptionalSession } from '@documenso/lib/client-only/providers/session';
|
||||
@@ -27,20 +25,10 @@ import { RadioGroup, RadioGroupItem } from '@documenso/ui/primitives/radio-group
|
||||
import { SignaturePad } from '@documenso/ui/primitives/signature-pad';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
import {
|
||||
AssistantConfirmationDialog,
|
||||
type NextSigner,
|
||||
} from '../../dialogs/assistant-confirmation-dialog';
|
||||
import { AssistantConfirmationDialog } from '../../dialogs/assistant-confirmation-dialog';
|
||||
import { DocumentSigningCompleteDialog } from './document-signing-complete-dialog';
|
||||
import { useRequiredDocumentSigningContext } from './document-signing-provider';
|
||||
|
||||
export const ZSigningFormSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required').optional(),
|
||||
email: z.string().email('Invalid email address').optional(),
|
||||
});
|
||||
|
||||
export type TSigningFormSchema = z.infer<typeof ZSigningFormSchema>;
|
||||
|
||||
export type DocumentSigningFormProps = {
|
||||
document: DocumentAndSender;
|
||||
recipient: Recipient;
|
||||
@@ -87,9 +75,7 @@ export const DocumentSigningForm = ({
|
||||
},
|
||||
});
|
||||
|
||||
const { handleSubmit, formState } = useForm<TSigningFormSchema>({
|
||||
resolver: zodResolver(ZSigningFormSchema),
|
||||
});
|
||||
const { handleSubmit, formState } = useForm();
|
||||
|
||||
// Keep the loading state going if successful since the redirect may take some time.
|
||||
const isSubmitting = formState.isSubmitting || formState.isSubmitSuccessful;
|
||||
@@ -114,36 +100,20 @@ export const DocumentSigningForm = ({
|
||||
validateFieldsInserted(fieldsRequiringValidation);
|
||||
};
|
||||
|
||||
const onFormSubmit = async (data: TSigningFormSchema) => {
|
||||
try {
|
||||
setValidateUninsertedFields(true);
|
||||
const onFormSubmit = async () => {
|
||||
setValidateUninsertedFields(true);
|
||||
|
||||
const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation);
|
||||
const isFieldsValid = validateFieldsInserted(fieldsRequiringValidation);
|
||||
|
||||
if (hasSignatureField && !signatureValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isFieldsValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextSigner =
|
||||
data.email && data.name
|
||||
? {
|
||||
email: data.email,
|
||||
name: data.name,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
await completeDocument(undefined, nextSigner);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error instanceof Error ? error.message : 'An error occurred while signing',
|
||||
variant: 'destructive',
|
||||
});
|
||||
if (hasSignatureField && !signatureValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isFieldsValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
await completeDocument();
|
||||
};
|
||||
|
||||
const onAssistantFormSubmit = () => {
|
||||
@@ -154,11 +124,11 @@ export const DocumentSigningForm = ({
|
||||
setIsConfirmationDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleAssistantConfirmDialogSubmit = async (nextSigner?: NextSigner) => {
|
||||
const handleAssistantConfirmDialogSubmit = async () => {
|
||||
setIsAssistantSubmitting(true);
|
||||
|
||||
try {
|
||||
await completeDocument(undefined, nextSigner);
|
||||
await completeDocument();
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
@@ -171,18 +141,12 @@ export const DocumentSigningForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const completeDocument = async (
|
||||
authOptions?: TRecipientActionAuth,
|
||||
nextSigner?: { email: string; name: string },
|
||||
) => {
|
||||
const payload = {
|
||||
const completeDocument = async (authOptions?: TRecipientActionAuth) => {
|
||||
await completeDocumentWithToken({
|
||||
token: recipient.token,
|
||||
documentId: document.id,
|
||||
authOptions,
|
||||
...(nextSigner?.email && nextSigner?.name ? { nextSigner } : {}),
|
||||
};
|
||||
|
||||
await completeDocumentWithToken(payload);
|
||||
});
|
||||
|
||||
analytics.capture('App: Recipient has completed signing', {
|
||||
signerId: recipient.id,
|
||||
@@ -197,31 +161,6 @@ export const DocumentSigningForm = ({
|
||||
}
|
||||
};
|
||||
|
||||
const nextRecipient = useMemo(() => {
|
||||
if (
|
||||
!document.documentMeta?.signingOrder ||
|
||||
document.documentMeta.signingOrder !== 'SEQUENTIAL'
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sortedRecipients = allRecipients.sort((a, b) => {
|
||||
// Sort by signingOrder first (nulls last), then by id
|
||||
if (a.signingOrder === null && b.signingOrder === null) return a.id - b.id;
|
||||
if (a.signingOrder === null) return 1;
|
||||
if (b.signingOrder === null) return -1;
|
||||
if (a.signingOrder === b.signingOrder) return a.id - b.id;
|
||||
return a.signingOrder - b.signingOrder;
|
||||
});
|
||||
|
||||
const currentIndex = sortedRecipients.findIndex((r) => r.id === recipient.id);
|
||||
return currentIndex !== -1 && currentIndex < sortedRecipients.length - 1
|
||||
? sortedRecipients[currentIndex + 1]
|
||||
: undefined;
|
||||
}, [document.documentMeta?.signingOrder, allRecipients, recipient.id]);
|
||||
|
||||
console.log('nextRecipient', nextRecipient);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
@@ -271,19 +210,12 @@ export const DocumentSigningForm = ({
|
||||
|
||||
<DocumentSigningCompleteDialog
|
||||
isSubmitting={isSubmitting}
|
||||
onSignatureComplete={handleSubmit(onFormSubmit)}
|
||||
documentTitle={document.title}
|
||||
fields={fields}
|
||||
fieldsValidated={fieldsValidated}
|
||||
onSignatureComplete={async (nextSigner) => {
|
||||
await completeDocument(undefined, nextSigner);
|
||||
}}
|
||||
role={recipient.role}
|
||||
allowDictateNextSigner={document.documentMeta?.allowDictateNextSigner}
|
||||
defaultNextSigner={
|
||||
nextRecipient
|
||||
? { name: nextRecipient.name, email: nextRecipient.email }
|
||||
: undefined
|
||||
}
|
||||
disabled={!isRecipientsTurn}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -374,14 +306,6 @@ export const DocumentSigningForm = ({
|
||||
onClose={() => !isAssistantSubmitting && setIsConfirmationDialogOpen(false)}
|
||||
onConfirm={handleAssistantConfirmDialogSubmit}
|
||||
isSubmitting={isAssistantSubmitting}
|
||||
allowDictateNextSigner={
|
||||
nextRecipient && document.documentMeta?.allowDictateNextSigner
|
||||
}
|
||||
defaultNextSigner={
|
||||
nextRecipient
|
||||
? { name: nextRecipient.name, email: nextRecipient.email }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
</>
|
||||
@@ -452,40 +376,30 @@ export const DocumentSigningForm = ({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 md:flex-row">
|
||||
<Button
|
||||
type="button"
|
||||
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||
onClick={async () => navigate(-1)}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<DocumentSigningCompleteDialog
|
||||
isSubmitting={isSubmitting}
|
||||
onSignatureComplete={handleSubmit(onFormSubmit)}
|
||||
documentTitle={document.title}
|
||||
fields={fields}
|
||||
fieldsValidated={fieldsValidated}
|
||||
role={recipient.role}
|
||||
disabled={!isRecipientsTurn}
|
||||
/>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-4 md:flex-row">
|
||||
<Button
|
||||
type="button"
|
||||
className="dark:bg-muted dark:hover:bg-muted/80 w-full bg-black/5 hover:bg-black/10"
|
||||
variant="secondary"
|
||||
size="lg"
|
||||
disabled={typeof window !== 'undefined' && window.history.length <= 1}
|
||||
onClick={async () => navigate(-1)}
|
||||
>
|
||||
<Trans>Cancel</Trans>
|
||||
</Button>
|
||||
|
||||
<DocumentSigningCompleteDialog
|
||||
isSubmitting={isSubmitting || isAssistantSubmitting}
|
||||
documentTitle={document.title}
|
||||
fields={fields}
|
||||
fieldsValidated={fieldsValidated}
|
||||
disabled={!isRecipientsTurn}
|
||||
onSignatureComplete={async (nextSigner) => {
|
||||
await completeDocument(undefined, nextSigner);
|
||||
}}
|
||||
role={recipient.role}
|
||||
allowDictateNextSigner={
|
||||
nextRecipient && document.documentMeta?.allowDictateNextSigner
|
||||
}
|
||||
defaultNextSigner={
|
||||
nextRecipient
|
||||
? { name: nextRecipient.name, email: nextRecipient.email }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -40,9 +40,9 @@ import { DocumentReadOnlyFields } from '~/components/general/document/document-r
|
||||
|
||||
import { DocumentSigningRecipientProvider } from './document-signing-recipient-provider';
|
||||
|
||||
export type DocumentSigningPageViewProps = {
|
||||
recipient: RecipientWithFields;
|
||||
export type SigningPageViewProps = {
|
||||
document: DocumentAndSender;
|
||||
recipient: RecipientWithFields;
|
||||
fields: Field[];
|
||||
completedFields: CompletedField[];
|
||||
isRecipientsTurn: boolean;
|
||||
@@ -50,13 +50,13 @@ export type DocumentSigningPageViewProps = {
|
||||
};
|
||||
|
||||
export const DocumentSigningPageView = ({
|
||||
recipient,
|
||||
document,
|
||||
recipient,
|
||||
fields,
|
||||
completedFields,
|
||||
isRecipientsTurn,
|
||||
allRecipients = [],
|
||||
}: DocumentSigningPageViewProps) => {
|
||||
}: SigningPageViewProps) => {
|
||||
const { documentData, documentMeta } = document;
|
||||
|
||||
const [selectedSignerId, setSelectedSignerId] = useState<number | null>(allRecipients?.[0]?.id);
|
||||
|
||||
@@ -31,7 +31,10 @@ import { Textarea } from '@documenso/ui/primitives/textarea';
|
||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||
|
||||
const ZRejectDocumentFormSchema = z.object({
|
||||
reason: z.string().max(500, msg`Reason must be less than 500 characters`),
|
||||
reason: z
|
||||
.string()
|
||||
.min(5, msg`Please provide a reason`)
|
||||
.max(500, msg`Reason must be less than 500 characters`),
|
||||
});
|
||||
|
||||
type TRejectDocumentFormSchema = z.infer<typeof ZRejectDocumentFormSchema>;
|
||||
|
||||
@@ -71,7 +71,7 @@ export const DocumentEditForm = ({
|
||||
|
||||
const { recipients, fields } = document;
|
||||
|
||||
const { mutateAsync: updateDocumentSettings } = trpc.document.setSettingsForDocument.useMutation({
|
||||
const { mutateAsync: updateDocument } = trpc.document.setSettingsForDocument.useMutation({
|
||||
...DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||
onSuccess: (newData) => {
|
||||
utils.document.getDocumentWithDetailsById.setData(
|
||||
@@ -176,7 +176,7 @@ export const DocumentEditForm = ({
|
||||
try {
|
||||
const { timezone, dateFormat, redirectUrl, language } = data.meta;
|
||||
|
||||
await updateDocumentSettings({
|
||||
await updateDocument({
|
||||
documentId: document.id,
|
||||
data: {
|
||||
title: data.title,
|
||||
@@ -213,13 +213,6 @@ export const DocumentEditForm = ({
|
||||
signingOrder: data.signingOrder,
|
||||
}),
|
||||
|
||||
updateDocumentSettings({
|
||||
documentId: document.id,
|
||||
meta: {
|
||||
allowDictateNextSigner: data.allowDictateNextSigner,
|
||||
},
|
||||
}),
|
||||
|
||||
setRecipients({
|
||||
documentId: document.id,
|
||||
recipients: data.signers.map((signer) => ({
|
||||
@@ -249,7 +242,7 @@ export const DocumentEditForm = ({
|
||||
fields: data.fields,
|
||||
});
|
||||
|
||||
await updateDocumentSettings({
|
||||
await updateDocument({
|
||||
documentId: document.id,
|
||||
|
||||
meta: {
|
||||
@@ -372,7 +365,6 @@ export const DocumentEditForm = ({
|
||||
documentFlow={documentFlow.signers}
|
||||
recipients={recipients}
|
||||
signingOrder={document.documentMeta?.signingOrder}
|
||||
allowDictateNextSigner={document.documentMeta?.allowDictateNextSigner}
|
||||
fields={fields}
|
||||
isDocumentEnterprise={isDocumentEnterprise}
|
||||
onSubmit={onAddSignersFormSubmit}
|
||||
|
||||
@@ -161,7 +161,6 @@ export const TemplateEditForm = ({
|
||||
templateId: template.id,
|
||||
meta: {
|
||||
signingOrder: data.signingOrder,
|
||||
allowDictateNextSigner: data.allowDictateNextSigner,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -272,7 +271,6 @@ export const TemplateEditForm = ({
|
||||
recipients={recipients}
|
||||
fields={fields}
|
||||
signingOrder={template.templateMeta?.signingOrder}
|
||||
allowDictateNextSigner={template.templateMeta?.allowDictateNextSigner}
|
||||
templateDirectLink={template.directLink}
|
||||
onSubmit={onAddTemplatePlaceholderFormSubmit}
|
||||
isEnterprise={isEnterprise}
|
||||
|
||||
@@ -48,9 +48,13 @@ export type DocumentsTableActionDropdownProps = {
|
||||
recipients: Recipient[];
|
||||
team: Pick<Team, 'id' | 'url'> | null;
|
||||
};
|
||||
onMoveDocument?: () => void;
|
||||
};
|
||||
|
||||
export const DocumentsTableActionDropdown = ({ row }: DocumentsTableActionDropdownProps) => {
|
||||
export const DocumentsTableActionDropdown = ({
|
||||
row,
|
||||
onMoveDocument,
|
||||
}: DocumentsTableActionDropdownProps) => {
|
||||
const { user } = useSession();
|
||||
const team = useOptionalCurrentTeam();
|
||||
|
||||
@@ -165,6 +169,13 @@ export const DocumentsTableActionDropdown = ({ row }: DocumentsTableActionDropdo
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{onMoveDocument && (
|
||||
<DropdownMenuItem onClick={onMoveDocument} onSelect={(e) => e.preventDefault()}>
|
||||
<MoveRight className="mr-2 h-4 w-4" />
|
||||
<Trans>Move to Folder</Trans>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{/* No point displaying this if there's no functionality. */}
|
||||
{/* <DropdownMenuItem disabled>
|
||||
<XCircle className="mr-2 h-4 w-4" />
|
||||
|
||||
@@ -29,11 +29,17 @@ export type DocumentsTableProps = {
|
||||
data?: TFindDocumentsResponse;
|
||||
isLoading?: boolean;
|
||||
isLoadingError?: boolean;
|
||||
onMoveDocument?: (documentId: number) => void;
|
||||
};
|
||||
|
||||
type DocumentsTableRow = TFindDocumentsResponse['data'][number];
|
||||
|
||||
export const DocumentsTable = ({ data, isLoading, isLoadingError }: DocumentsTableProps) => {
|
||||
export const DocumentsTable = ({
|
||||
data,
|
||||
isLoading,
|
||||
isLoadingError,
|
||||
onMoveDocument,
|
||||
}: DocumentsTableProps) => {
|
||||
const { _, i18n } = useLingui();
|
||||
|
||||
const team = useOptionalCurrentTeam();
|
||||
@@ -80,12 +86,15 @@ export const DocumentsTable = ({ data, isLoading, isLoadingError }: DocumentsTab
|
||||
(!row.original.deletedAt || isDocumentCompleted(row.original.status)) && (
|
||||
<div className="flex items-center gap-x-4">
|
||||
<DocumentsTableActionButton row={row.original} />
|
||||
<DocumentsTableActionDropdown row={row.original} />
|
||||
<DocumentsTableActionDropdown
|
||||
row={row.original}
|
||||
onMoveDocument={onMoveDocument ? () => onMoveDocument(row.original.id) : undefined}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
] satisfies DataTableColumnDef<DocumentsTableRow>[];
|
||||
}, [team]);
|
||||
}, [team, onMoveDocument, _, i18n]);
|
||||
|
||||
const onPaginationChange = (page: number, perPage: number) => {
|
||||
startTransition(() => {
|
||||
|
||||
@@ -6,7 +6,6 @@ import { redirect } from 'react-router';
|
||||
import { match } from 'ts-pattern';
|
||||
import { UAParser } from 'ua-parser-js';
|
||||
|
||||
import { isDocumentPlatform } from '@documenso/ee/server-only/util/is-document-platform';
|
||||
import { APP_I18N_OPTIONS, ZSupportedLanguageCodeSchema } from '@documenso/lib/constants/i18n';
|
||||
import {
|
||||
RECIPIENT_ROLES_DESCRIPTION,
|
||||
@@ -60,8 +59,6 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
throw redirect('/');
|
||||
}
|
||||
|
||||
const isPlatformDocument = await isDocumentPlatform(document);
|
||||
|
||||
const documentLanguage = ZSupportedLanguageCodeSchema.parse(document.documentMeta?.language);
|
||||
|
||||
const auditLogs = await getDocumentCertificateAuditLogs({
|
||||
@@ -73,7 +70,6 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
return {
|
||||
document,
|
||||
documentLanguage,
|
||||
isPlatformDocument,
|
||||
auditLogs,
|
||||
messages,
|
||||
};
|
||||
@@ -89,7 +85,7 @@ export async function loader({ request }: Route.LoaderArgs) {
|
||||
* Update: Maybe <Trans> tags work now after RR7 migration.
|
||||
*/
|
||||
export default function SigningCertificate({ loaderData }: Route.ComponentProps) {
|
||||
const { document, documentLanguage, isPlatformDocument, auditLogs, messages } = loaderData;
|
||||
const { document, documentLanguage, auditLogs, messages } = loaderData;
|
||||
|
||||
const { i18n, _ } = useLingui();
|
||||
|
||||
@@ -341,17 +337,15 @@ export default function SigningCertificate({ loaderData }: Route.ComponentProps)
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{isPlatformDocument && (
|
||||
<div className="my-8 flex-row-reverse">
|
||||
<div className="flex items-end justify-end gap-x-4">
|
||||
<p className="flex-shrink-0 text-sm font-medium print:text-xs">
|
||||
{_(msg`Signing certificate provided by`)}:
|
||||
</p>
|
||||
<div className="my-8 flex-row-reverse">
|
||||
<div className="flex items-end justify-end gap-x-4">
|
||||
<p className="flex-shrink-0 text-sm font-medium print:text-xs">
|
||||
{_(msg`Signing certificate provided by`)}:
|
||||
</p>
|
||||
|
||||
<BrandingLogo className="max-h-6 print:max-h-4" />
|
||||
</div>
|
||||
<BrandingLogo className="max-h-6 print:max-h-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { DocumentSigningOrder, DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import { DocumentStatus, RecipientRole, SigningStatus } from '@prisma/client';
|
||||
import { Clock8 } from 'lucide-react';
|
||||
import { Link, redirect } from 'react-router';
|
||||
import { getOptionalLoaderContext } from 'server/utils/get-loader-session';
|
||||
@@ -13,7 +13,6 @@ import { viewedDocument } from '@documenso/lib/server-only/document/viewed-docum
|
||||
import { getCompletedFieldsForToken } from '@documenso/lib/server-only/field/get-completed-fields-for-token';
|
||||
import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token';
|
||||
import { getIsRecipientsTurnToSign } from '@documenso/lib/server-only/recipient/get-is-recipient-turn';
|
||||
import { getNextPendingRecipient } from '@documenso/lib/server-only/recipient/get-next-pending-recipient';
|
||||
import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token';
|
||||
import { getRecipientSignatures } from '@documenso/lib/server-only/recipient/get-recipient-signatures';
|
||||
import { getRecipientsForAssistant } from '@documenso/lib/server-only/recipient/get-recipients-for-assistant';
|
||||
@@ -73,24 +72,7 @@ export async function loader({ params, request }: Route.LoaderArgs) {
|
||||
? await getRecipientsForAssistant({
|
||||
token,
|
||||
})
|
||||
: [recipient];
|
||||
|
||||
if (
|
||||
document.documentMeta?.signingOrder === DocumentSigningOrder.SEQUENTIAL &&
|
||||
recipient.role !== RecipientRole.ASSISTANT
|
||||
) {
|
||||
const nextPendingRecipient = await getNextPendingRecipient({
|
||||
documentId: document.id,
|
||||
currentRecipientId: recipient.id,
|
||||
});
|
||||
|
||||
if (nextPendingRecipient) {
|
||||
allRecipients.push({
|
||||
...nextPendingRecipient,
|
||||
fields: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
: [];
|
||||
|
||||
const { derivedRecipientAccessAuth } = extractDocumentAuthMethods({
|
||||
documentAuth: document.authOptions,
|
||||
|
||||
@@ -99,6 +99,5 @@
|
||||
"vite": "^6.1.0",
|
||||
"vite-plugin-babel-macros": "^1.0.6",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
},
|
||||
"version": "1.10.0-rc.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ command -v docker >/dev/null 2>&1 || {
|
||||
SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
|
||||
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
|
||||
|
||||
# Get Git information
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
@@ -16,39 +15,12 @@ echo "Building docker image for monorepo at $MONOREPO_ROOT"
|
||||
echo "App version: $APP_VERSION"
|
||||
echo "Git SHA: $GIT_SHA"
|
||||
|
||||
# Build with temporary base tag
|
||||
docker build -f "$SCRIPT_DIR/Dockerfile" \
|
||||
--progress=plain \
|
||||
-t "documenso-base" \
|
||||
-t "documenso/documenso:latest" \
|
||||
-t "documenso/documenso:$GIT_SHA" \
|
||||
-t "documenso/documenso:$APP_VERSION" \
|
||||
-t "ghcr.io/documenso/documenso:latest" \
|
||||
-t "ghcr.io/documenso/documenso:$GIT_SHA" \
|
||||
-t "ghcr.io/documenso/documenso:$APP_VERSION" \
|
||||
"$MONOREPO_ROOT"
|
||||
|
||||
# Handle repository tagging
|
||||
if [ ! -z "$DOCKER_REPOSITORY" ]; then
|
||||
echo "Using custom repository: $DOCKER_REPOSITORY"
|
||||
|
||||
# Add tags for custom repository
|
||||
docker tag "documenso-base" "$DOCKER_REPOSITORY:latest"
|
||||
docker tag "documenso-base" "$DOCKER_REPOSITORY:$GIT_SHA"
|
||||
|
||||
# Add version tag if available
|
||||
if [ ! -z "$APP_VERSION" ] && [ "$APP_VERSION" != "undefined" ]; then
|
||||
docker tag "documenso-base" "$DOCKER_REPOSITORY:$APP_VERSION"
|
||||
fi
|
||||
else
|
||||
echo "Using default repositories: dockerhub and ghcr.io"
|
||||
|
||||
# Add tags for both default repositories
|
||||
docker tag "documenso-base" "documenso/documenso:latest"
|
||||
docker tag "documenso-base" "documenso/documenso:$GIT_SHA"
|
||||
docker tag "documenso-base" "ghcr.io/documenso/documenso:latest"
|
||||
docker tag "documenso-base" "ghcr.io/documenso/documenso:$GIT_SHA"
|
||||
|
||||
# Add version tags if available
|
||||
if [ ! -z "$APP_VERSION" ] && [ "$APP_VERSION" != "undefined" ]; then
|
||||
docker tag "documenso-base" "documenso/documenso:$APP_VERSION"
|
||||
docker tag "documenso-base" "ghcr.io/documenso/documenso:$APP_VERSION"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove the temporary base tag
|
||||
docker rmi "documenso-base"
|
||||
|
||||
@@ -9,11 +9,11 @@ SCRIPT_DIR="$(readlink -f "$(dirname "$0")")"
|
||||
MONOREPO_ROOT="$(readlink -f "$SCRIPT_DIR/../")"
|
||||
|
||||
# Get the platform from environment variable or set to linux/amd64 if not set
|
||||
# quote the string to prevent word splitting
|
||||
if [ -z "$PLATFORM" ]; then
|
||||
PLATFORM="linux/amd64"
|
||||
fi
|
||||
|
||||
# Get Git information
|
||||
APP_VERSION="$(git name-rev --tags --name-only $(git rev-parse HEAD) | head -n 1 | sed 's/\^0//')"
|
||||
GIT_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
@@ -21,41 +21,14 @@ echo "Building docker image for monorepo at $MONOREPO_ROOT"
|
||||
echo "App version: $APP_VERSION"
|
||||
echo "Git SHA: $GIT_SHA"
|
||||
|
||||
# Build with temporary base tag
|
||||
docker buildx build \
|
||||
-f "$SCRIPT_DIR/Dockerfile" \
|
||||
--platform=$PLATFORM \
|
||||
--progress=plain \
|
||||
-t "documenso-base" \
|
||||
-t "documenso/documenso:latest" \
|
||||
-t "documenso/documenso:$GIT_SHA" \
|
||||
-t "documenso/documenso:$APP_VERSION" \
|
||||
-t "ghcr.io/documenso/documenso:latest" \
|
||||
-t "ghcr.io/documenso/documenso:$GIT_SHA" \
|
||||
-t "ghcr.io/documenso/documenso:$APP_VERSION" \
|
||||
"$MONOREPO_ROOT"
|
||||
|
||||
# Handle repository tagging
|
||||
if [ ! -z "$DOCKER_REPOSITORY" ]; then
|
||||
echo "Using custom repository: $DOCKER_REPOSITORY"
|
||||
|
||||
# Add tags for custom repository
|
||||
docker tag "documenso-base" "$DOCKER_REPOSITORY:latest"
|
||||
docker tag "documenso-base" "$DOCKER_REPOSITORY:$GIT_SHA"
|
||||
|
||||
# Add version tag if available
|
||||
if [ ! -z "$APP_VERSION" ] && [ "$APP_VERSION" != "undefined" ]; then
|
||||
docker tag "documenso-base" "$DOCKER_REPOSITORY:$APP_VERSION"
|
||||
fi
|
||||
else
|
||||
echo "Using default repositories: dockerhub and ghcr.io"
|
||||
|
||||
# Add tags for both default repositories
|
||||
docker tag "documenso-base" "documenso/documenso:latest"
|
||||
docker tag "documenso-base" "documenso/documenso:$GIT_SHA"
|
||||
docker tag "documenso-base" "ghcr.io/documenso/documenso:latest"
|
||||
docker tag "documenso-base" "ghcr.io/documenso/documenso:$GIT_SHA"
|
||||
|
||||
# Add version tags if available
|
||||
if [ ! -z "$APP_VERSION" ] && [ "$APP_VERSION" != "undefined" ]; then
|
||||
docker tag "documenso-base" "documenso/documenso:$APP_VERSION"
|
||||
docker tag "documenso-base" "ghcr.io/documenso/documenso:$APP_VERSION"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove the temporary base tag
|
||||
docker rmi "documenso-base"
|
||||
|
||||
5
package-lock.json
generated
5
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@documenso/root",
|
||||
"version": "1.10.0-rc.1",
|
||||
"version": "1.9.0-rc.11",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@documenso/root",
|
||||
"version": "1.10.0-rc.1",
|
||||
"version": "1.9.0-rc.11",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
@@ -95,7 +95,6 @@
|
||||
},
|
||||
"apps/remix": {
|
||||
"name": "@documenso/remix",
|
||||
"version": "1.10.0-rc.1",
|
||||
"dependencies": {
|
||||
"@documenso/api": "*",
|
||||
"@documenso/assets": "*",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "1.10.0-rc.1",
|
||||
"version": "1.9.0-rc.11",
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev --filter=@documenso/remix",
|
||||
|
||||
@@ -323,7 +323,6 @@ export const ApiContractV1Implementation = tsr.router(ApiContractV1, {
|
||||
dateFormat: dateFormat?.value,
|
||||
redirectUrl: body.meta.redirectUrl,
|
||||
signingOrder: body.meta.signingOrder,
|
||||
allowDictateNextSigner: body.meta.allowDictateNextSigner,
|
||||
language: body.meta.language,
|
||||
typedSignatureEnabled: body.meta.typedSignatureEnabled,
|
||||
distributionMethod: body.meta.distributionMethod,
|
||||
|
||||
@@ -155,7 +155,6 @@ export const ZCreateDocumentMutationSchema = z.object({
|
||||
}),
|
||||
redirectUrl: z.string(),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||
allowDictateNextSigner: z.boolean().optional(),
|
||||
language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
|
||||
typedSignatureEnabled: z.boolean().optional().default(true),
|
||||
distributionMethod: z.nativeEnum(DocumentDistributionMethod).optional(),
|
||||
@@ -219,7 +218,6 @@ export const ZCreateDocumentFromTemplateMutationSchema = z.object({
|
||||
dateFormat: z.string(),
|
||||
redirectUrl: z.string(),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||
allowDictateNextSigner: z.boolean().optional(),
|
||||
language: z.enum(SUPPORTED_LANGUAGE_CODES).optional(),
|
||||
})
|
||||
.partial()
|
||||
@@ -287,7 +285,6 @@ export const ZGenerateDocumentFromTemplateMutationSchema = z.object({
|
||||
dateFormat: z.string(),
|
||||
redirectUrl: ZUrlSchema,
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
allowDictateNextSigner: z.boolean(),
|
||||
language: z.enum(SUPPORTED_LANGUAGE_CODES),
|
||||
distributionMethod: z.nativeEnum(DocumentDistributionMethod),
|
||||
typedSignatureEnabled: z.boolean(),
|
||||
|
||||
@@ -210,7 +210,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient au
|
||||
}),
|
||||
},
|
||||
],
|
||||
fields: [FieldType.DATE, FieldType.SIGNATURE],
|
||||
fields: [FieldType.DATE],
|
||||
});
|
||||
|
||||
for (const recipient of recipients) {
|
||||
@@ -307,7 +307,7 @@ test('[DOCUMENT_AUTH]: should allow field signing when required for recipient an
|
||||
}),
|
||||
},
|
||||
],
|
||||
fields: [FieldType.DATE, FieldType.SIGNATURE],
|
||||
fields: [FieldType.DATE],
|
||||
updateDocumentOptions: {
|
||||
authOptions: createDocumentAuthOptions({
|
||||
globalAccessAuth: null,
|
||||
|
||||
@@ -1,390 +0,0 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import {
|
||||
DocumentSigningOrder,
|
||||
DocumentStatus,
|
||||
FieldType,
|
||||
RecipientRole,
|
||||
SigningStatus,
|
||||
} from '@prisma/client';
|
||||
|
||||
import { prisma } from '@documenso/prisma';
|
||||
import { seedPendingDocumentWithFullFields } from '@documenso/prisma/seed/documents';
|
||||
import { seedUser } from '@documenso/prisma/seed/users';
|
||||
|
||||
import { signSignaturePad } from '../fixtures/signature';
|
||||
|
||||
test('[NEXT_RECIPIENT_DICTATION]: should allow updating next recipient when dictation is enabled', async ({
|
||||
page,
|
||||
}) => {
|
||||
const user = await seedUser();
|
||||
const firstSigner = await seedUser();
|
||||
const secondSigner = await seedUser();
|
||||
const thirdSigner = await seedUser();
|
||||
|
||||
const { recipients, document } = await seedPendingDocumentWithFullFields({
|
||||
owner: user,
|
||||
recipients: [firstSigner, secondSigner, thirdSigner],
|
||||
recipientsCreateOptions: [{ signingOrder: 1 }, { signingOrder: 2 }, { signingOrder: 3 }],
|
||||
updateDocumentOptions: {
|
||||
documentMeta: {
|
||||
upsert: {
|
||||
create: {
|
||||
allowDictateNextSigner: true,
|
||||
signingOrder: DocumentSigningOrder.SEQUENTIAL,
|
||||
},
|
||||
update: {
|
||||
allowDictateNextSigner: true,
|
||||
signingOrder: DocumentSigningOrder.SEQUENTIAL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const firstRecipient = recipients[0];
|
||||
const { token, fields } = firstRecipient;
|
||||
|
||||
const signUrl = `/sign/${token}`;
|
||||
|
||||
await page.goto(signUrl);
|
||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||
|
||||
await signSignaturePad(page);
|
||||
|
||||
// Fill in all fields
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
if (field.type === FieldType.TEXT) {
|
||||
await page.locator('#custom-text').fill('TEXT');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
}
|
||||
|
||||
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||
}
|
||||
|
||||
// Complete signing and update next recipient
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
|
||||
// Verify next recipient info is shown
|
||||
await expect(page.getByRole('dialog')).toBeVisible();
|
||||
await expect(page.getByText('The next recipient to sign this document will be')).toBeVisible();
|
||||
|
||||
// Update next recipient
|
||||
await page.locator('button').filter({ hasText: 'Update Recipient' }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Use dialog context to ensure we're targeting the correct form fields
|
||||
const dialog = page.getByRole('dialog');
|
||||
await dialog.getByLabel('Name').fill('New Recipient');
|
||||
await dialog.getByLabel('Email').fill('new.recipient@example.com');
|
||||
|
||||
// Submit and verify completion
|
||||
await page.getByRole('button', { name: 'Sign' }).click();
|
||||
await page.waitForURL(`${signUrl}/complete`);
|
||||
|
||||
// Verify document and recipient states
|
||||
const updatedDocument = await prisma.document.findUniqueOrThrow({
|
||||
where: { id: document.id },
|
||||
include: {
|
||||
recipients: {
|
||||
orderBy: { signingOrder: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Document should still be pending as there are more recipients
|
||||
expect(updatedDocument.status).toBe(DocumentStatus.PENDING);
|
||||
|
||||
// First recipient should be completed
|
||||
const updatedFirstRecipient = updatedDocument.recipients[0];
|
||||
expect(updatedFirstRecipient.signingStatus).toBe(SigningStatus.SIGNED);
|
||||
|
||||
// Second recipient should be the new recipient
|
||||
const updatedSecondRecipient = updatedDocument.recipients[1];
|
||||
expect(updatedSecondRecipient.name).toBe('New Recipient');
|
||||
expect(updatedSecondRecipient.email).toBe('new.recipient@example.com');
|
||||
expect(updatedSecondRecipient.signingOrder).toBe(2);
|
||||
expect(updatedSecondRecipient.signingStatus).toBe(SigningStatus.NOT_SIGNED);
|
||||
});
|
||||
|
||||
test('[NEXT_RECIPIENT_DICTATION]: should not show dictation UI when disabled', async ({ page }) => {
|
||||
const user = await seedUser();
|
||||
const firstSigner = await seedUser();
|
||||
const secondSigner = await seedUser();
|
||||
|
||||
const { recipients, document } = await seedPendingDocumentWithFullFields({
|
||||
owner: user,
|
||||
recipients: [firstSigner, secondSigner],
|
||||
recipientsCreateOptions: [{ signingOrder: 1 }, { signingOrder: 2 }],
|
||||
updateDocumentOptions: {
|
||||
documentMeta: {
|
||||
upsert: {
|
||||
create: {
|
||||
allowDictateNextSigner: false,
|
||||
signingOrder: DocumentSigningOrder.SEQUENTIAL,
|
||||
},
|
||||
update: {
|
||||
allowDictateNextSigner: false,
|
||||
signingOrder: DocumentSigningOrder.SEQUENTIAL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const firstRecipient = recipients[0];
|
||||
const { token, fields } = firstRecipient;
|
||||
|
||||
const signUrl = `/sign/${token}`;
|
||||
|
||||
await page.goto(signUrl);
|
||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||
|
||||
await signSignaturePad(page);
|
||||
|
||||
// Fill in all fields
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
if (field.type === FieldType.TEXT) {
|
||||
await page.locator('#custom-text').fill('TEXT');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
}
|
||||
|
||||
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||
}
|
||||
|
||||
// Complete signing
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
|
||||
// Verify next recipient UI is not shown
|
||||
await expect(
|
||||
page.getByText('The next recipient to sign this document will be'),
|
||||
).not.toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Update Recipient' })).not.toBeVisible();
|
||||
|
||||
// Submit and verify completion
|
||||
await page.getByRole('button', { name: 'Sign' }).click();
|
||||
await page.waitForURL(`${signUrl}/complete`);
|
||||
|
||||
// Verify document and recipient states
|
||||
|
||||
const updatedDocument = await prisma.document.findUniqueOrThrow({
|
||||
where: { id: document.id },
|
||||
include: {
|
||||
recipients: {
|
||||
orderBy: { signingOrder: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Document should still be pending as there are more recipients
|
||||
expect(updatedDocument.status).toBe(DocumentStatus.PENDING);
|
||||
|
||||
// First recipient should be completed
|
||||
const updatedFirstRecipient = updatedDocument.recipients[0];
|
||||
expect(updatedFirstRecipient.signingStatus).toBe(SigningStatus.SIGNED);
|
||||
|
||||
// Second recipient should remain unchanged
|
||||
const updatedSecondRecipient = updatedDocument.recipients[1];
|
||||
expect(updatedSecondRecipient.email).toBe(secondSigner.email);
|
||||
expect(updatedSecondRecipient.signingOrder).toBe(2);
|
||||
expect(updatedSecondRecipient.signingStatus).toBe(SigningStatus.NOT_SIGNED);
|
||||
});
|
||||
|
||||
test('[NEXT_RECIPIENT_DICTATION]: should work with parallel signing flow', async ({ page }) => {
|
||||
const user = await seedUser();
|
||||
const firstSigner = await seedUser();
|
||||
const secondSigner = await seedUser();
|
||||
|
||||
const { recipients, document } = await seedPendingDocumentWithFullFields({
|
||||
owner: user,
|
||||
recipients: [firstSigner, secondSigner],
|
||||
recipientsCreateOptions: [{ signingOrder: 1 }, { signingOrder: 2 }],
|
||||
updateDocumentOptions: {
|
||||
documentMeta: {
|
||||
upsert: {
|
||||
create: {
|
||||
allowDictateNextSigner: false,
|
||||
signingOrder: DocumentSigningOrder.PARALLEL,
|
||||
},
|
||||
update: {
|
||||
allowDictateNextSigner: false,
|
||||
signingOrder: DocumentSigningOrder.PARALLEL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Test both recipients can sign in parallel
|
||||
for (const recipient of recipients) {
|
||||
const { token, fields } = recipient;
|
||||
const signUrl = `/sign/${token}`;
|
||||
|
||||
await page.goto(signUrl);
|
||||
await expect(page.getByRole('heading', { name: 'Sign Document' })).toBeVisible();
|
||||
|
||||
await signSignaturePad(page);
|
||||
|
||||
// Fill in all fields
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
if (field.type === FieldType.TEXT) {
|
||||
await page.locator('#custom-text').fill('TEXT');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
}
|
||||
|
||||
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||
}
|
||||
|
||||
// Complete signing
|
||||
await page.getByRole('button', { name: 'Complete' }).click();
|
||||
|
||||
// Verify next recipient UI is not shown in parallel flow
|
||||
await expect(
|
||||
page.getByText('The next recipient to sign this document will be'),
|
||||
).not.toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Update Recipient' })).not.toBeVisible();
|
||||
|
||||
// Submit and verify completion
|
||||
await page.getByRole('button', { name: 'Sign' }).click();
|
||||
await page.waitForURL(`${signUrl}/complete`);
|
||||
}
|
||||
|
||||
// Verify final document and recipient states
|
||||
await expect(async () => {
|
||||
const updatedDocument = await prisma.document.findUniqueOrThrow({
|
||||
where: { id: document.id },
|
||||
include: {
|
||||
recipients: {
|
||||
orderBy: { signingOrder: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Document should be completed since all recipients have signed
|
||||
expect(updatedDocument.status).toBe(DocumentStatus.COMPLETED);
|
||||
|
||||
// All recipients should be completed
|
||||
for (const recipient of updatedDocument.recipients) {
|
||||
expect(recipient.signingStatus).toBe(SigningStatus.SIGNED);
|
||||
}
|
||||
}).toPass();
|
||||
});
|
||||
|
||||
test('[NEXT_RECIPIENT_DICTATION]: should allow assistant to dictate next signer', async ({
|
||||
page,
|
||||
}) => {
|
||||
const user = await seedUser();
|
||||
const assistant = await seedUser();
|
||||
const signer = await seedUser();
|
||||
const thirdSigner = await seedUser();
|
||||
|
||||
const { recipients, document } = await seedPendingDocumentWithFullFields({
|
||||
owner: user,
|
||||
recipients: [assistant, signer, thirdSigner],
|
||||
recipientsCreateOptions: [
|
||||
{ signingOrder: 1, role: RecipientRole.ASSISTANT },
|
||||
{ signingOrder: 2, role: RecipientRole.SIGNER },
|
||||
{ signingOrder: 3, role: RecipientRole.SIGNER },
|
||||
],
|
||||
updateDocumentOptions: {
|
||||
documentMeta: {
|
||||
upsert: {
|
||||
create: {
|
||||
allowDictateNextSigner: true,
|
||||
signingOrder: DocumentSigningOrder.SEQUENTIAL,
|
||||
},
|
||||
update: {
|
||||
allowDictateNextSigner: true,
|
||||
signingOrder: DocumentSigningOrder.SEQUENTIAL,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const assistantRecipient = recipients[0];
|
||||
const { token, fields } = assistantRecipient;
|
||||
|
||||
const signUrl = `/sign/${token}`;
|
||||
|
||||
await page.goto(signUrl);
|
||||
await expect(page.getByRole('heading', { name: 'Assist Document' })).toBeVisible();
|
||||
|
||||
await page.getByRole('radio', { name: assistantRecipient.name }).click();
|
||||
|
||||
// Fill in all fields
|
||||
for (const field of fields) {
|
||||
await page.locator(`#field-${field.id}`).getByRole('button').click();
|
||||
|
||||
if (field.type === FieldType.SIGNATURE) {
|
||||
await signSignaturePad(page);
|
||||
await page.getByRole('button', { name: 'Sign', exact: true }).click();
|
||||
}
|
||||
|
||||
if (field.type === FieldType.TEXT) {
|
||||
await page.locator('#custom-text').fill('TEXT');
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
}
|
||||
|
||||
await expect(page.locator(`#field-${field.id}`)).toHaveAttribute('data-inserted', 'true');
|
||||
}
|
||||
|
||||
// Complete assisting and update next recipient
|
||||
await page.getByRole('button', { name: 'Continue' }).click();
|
||||
|
||||
// Verify next recipient info is shown
|
||||
await expect(page.getByRole('dialog')).toBeVisible();
|
||||
await expect(page.getByText('The next recipient to sign this document will be')).toBeVisible();
|
||||
|
||||
// Update next recipient
|
||||
await page.locator('button').filter({ hasText: 'Update Recipient' }).click();
|
||||
|
||||
// Use dialog context to ensure we're targeting the correct form fields
|
||||
const dialog = page.getByRole('dialog');
|
||||
await dialog.getByLabel('Name').fill('New Signer');
|
||||
await dialog.getByLabel('Email').fill('new.signer@example.com');
|
||||
|
||||
// Submit and verify completion
|
||||
await page.getByRole('button', { name: /Continue|Proceed/i }).click();
|
||||
await page.waitForURL(`${signUrl}/complete`);
|
||||
|
||||
// Verify document and recipient states
|
||||
await expect(async () => {
|
||||
const updatedDocument = await prisma.document.findUniqueOrThrow({
|
||||
where: { id: document.id },
|
||||
include: {
|
||||
recipients: {
|
||||
orderBy: { signingOrder: 'asc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Document should still be pending as there are more recipients
|
||||
expect(updatedDocument.status).toBe(DocumentStatus.PENDING);
|
||||
|
||||
// Assistant should be completed
|
||||
const updatedAssistant = updatedDocument.recipients[0];
|
||||
expect(updatedAssistant.signingStatus).toBe(SigningStatus.SIGNED);
|
||||
expect(updatedAssistant.role).toBe(RecipientRole.ASSISTANT);
|
||||
|
||||
// Second recipient should be the new signer
|
||||
const updatedSigner = updatedDocument.recipients[1];
|
||||
expect(updatedSigner.name).toBe('New Signer');
|
||||
expect(updatedSigner.email).toBe('new.signer@example.com');
|
||||
expect(updatedSigner.signingOrder).toBe(2);
|
||||
expect(updatedSigner.signingStatus).toBe(SigningStatus.NOT_SIGNED);
|
||||
expect(updatedSigner.role).toBe(RecipientRole.SIGNER);
|
||||
|
||||
// Third recipient should remain unchanged
|
||||
const thirdRecipient = updatedDocument.recipients[2];
|
||||
expect(thirdRecipient.email).toBe(thirdSigner.email);
|
||||
expect(thirdRecipient.signingOrder).toBe(3);
|
||||
expect(thirdRecipient.signingStatus).toBe(SigningStatus.NOT_SIGNED);
|
||||
expect(thirdRecipient.role).toBe(RecipientRole.SIGNER);
|
||||
}).toPass();
|
||||
});
|
||||
@@ -56,7 +56,6 @@ test('[PUBLIC_PROFILE]: create profile', async ({ page }) => {
|
||||
// Go back to public profile page.
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/settings/public-profile`);
|
||||
await page.getByRole('switch').click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Assert values.
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/p/${publicProfileUrl}`);
|
||||
@@ -128,7 +127,6 @@ test('[PUBLIC_PROFILE]: create team profile', async ({ page }) => {
|
||||
// Go back to public profile page.
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/t/${team.url}/settings/public-profile`);
|
||||
await page.getByRole('switch').click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Assert values.
|
||||
await page.goto(`${NEXT_PUBLIC_WEBAPP_URL()}/p/${publicProfileUrl}`);
|
||||
|
||||
@@ -24,7 +24,6 @@ export type CreateDocumentMetaOptions = {
|
||||
redirectUrl?: string;
|
||||
emailSettings?: TDocumentEmailSettings;
|
||||
signingOrder?: DocumentSigningOrder;
|
||||
allowDictateNextSigner?: boolean;
|
||||
distributionMethod?: DocumentDistributionMethod;
|
||||
typedSignatureEnabled?: boolean;
|
||||
language?: SupportedLanguageCodes;
|
||||
@@ -42,7 +41,6 @@ export const upsertDocumentMeta = async ({
|
||||
password,
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
allowDictateNextSigner,
|
||||
emailSettings,
|
||||
distributionMethod,
|
||||
typedSignatureEnabled,
|
||||
@@ -95,7 +93,6 @@ export const upsertDocumentMeta = async ({
|
||||
documentId,
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
allowDictateNextSigner,
|
||||
emailSettings,
|
||||
distributionMethod,
|
||||
typedSignatureEnabled,
|
||||
@@ -109,7 +106,6 @@ export const upsertDocumentMeta = async ({
|
||||
timezone,
|
||||
redirectUrl,
|
||||
signingOrder,
|
||||
allowDictateNextSigner,
|
||||
emailSettings,
|
||||
distributionMethod,
|
||||
typedSignatureEnabled,
|
||||
|
||||
@@ -7,10 +7,7 @@ import {
|
||||
WebhookTriggerEvents,
|
||||
} from '@prisma/client';
|
||||
|
||||
import {
|
||||
DOCUMENT_AUDIT_LOG_TYPE,
|
||||
RECIPIENT_DIFF_TYPE,
|
||||
} from '@documenso/lib/types/document-audit-logs';
|
||||
import { DOCUMENT_AUDIT_LOG_TYPE } from '@documenso/lib/types/document-audit-logs';
|
||||
import type { RequestMetadata } from '@documenso/lib/universal/extract-request-metadata';
|
||||
import { fieldsContainUnsignedRequiredField } from '@documenso/lib/utils/advanced-fields-helpers';
|
||||
import { createDocumentAuditLogData } from '@documenso/lib/utils/document-audit-logs';
|
||||
@@ -33,10 +30,6 @@ export type CompleteDocumentWithTokenOptions = {
|
||||
userId?: number;
|
||||
authOptions?: TRecipientActionAuth;
|
||||
requestMetadata?: RequestMetadata;
|
||||
nextSigner?: {
|
||||
email: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
||||
const getDocument = async ({ token, documentId }: CompleteDocumentWithTokenOptions) => {
|
||||
@@ -64,7 +57,6 @@ export const completeDocumentWithToken = async ({
|
||||
token,
|
||||
documentId,
|
||||
requestMetadata,
|
||||
nextSigner,
|
||||
}: CompleteDocumentWithTokenOptions) => {
|
||||
const document = await getDocument({ token, documentId });
|
||||
|
||||
@@ -154,6 +146,7 @@ export const completeDocumentWithToken = async ({
|
||||
recipientName: recipient.name,
|
||||
recipientId: recipient.id,
|
||||
recipientRole: recipient.role,
|
||||
// actionAuth: derivedRecipientActionAuth || undefined,
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -171,9 +164,6 @@ export const completeDocumentWithToken = async ({
|
||||
select: {
|
||||
id: true,
|
||||
signingOrder: true,
|
||||
name: true,
|
||||
email: true,
|
||||
role: true,
|
||||
},
|
||||
where: {
|
||||
documentId: document.id,
|
||||
@@ -196,49 +186,9 @@ export const completeDocumentWithToken = async ({
|
||||
const [nextRecipient] = pendingRecipients;
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
if (nextSigner && document.documentMeta?.allowDictateNextSigner) {
|
||||
await tx.documentAuditLog.create({
|
||||
data: createDocumentAuditLogData({
|
||||
type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
|
||||
documentId: document.id,
|
||||
user: {
|
||||
name: recipient.name,
|
||||
email: recipient.email,
|
||||
},
|
||||
requestMetadata,
|
||||
data: {
|
||||
recipientEmail: nextRecipient.email,
|
||||
recipientName: nextRecipient.name,
|
||||
recipientId: nextRecipient.id,
|
||||
recipientRole: nextRecipient.role,
|
||||
changes: [
|
||||
{
|
||||
type: RECIPIENT_DIFF_TYPE.NAME,
|
||||
from: nextRecipient.name,
|
||||
to: nextSigner.name,
|
||||
},
|
||||
{
|
||||
type: RECIPIENT_DIFF_TYPE.EMAIL,
|
||||
from: nextRecipient.email,
|
||||
to: nextSigner.email,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
await tx.recipient.update({
|
||||
where: { id: nextRecipient.id },
|
||||
data: {
|
||||
sendStatus: SendStatus.SENT,
|
||||
...(nextSigner && document.documentMeta?.allowDictateNextSigner
|
||||
? {
|
||||
name: nextSigner.name,
|
||||
email: nextSigner.email,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
data: { sendStatus: SendStatus.SENT },
|
||||
});
|
||||
|
||||
await jobs.triggerJob({
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { prisma } from '@documenso/prisma';
|
||||
|
||||
export const getNextPendingRecipient = async ({
|
||||
documentId,
|
||||
currentRecipientId,
|
||||
}: {
|
||||
documentId: number;
|
||||
currentRecipientId: number;
|
||||
}) => {
|
||||
const recipients = await prisma.recipient.findMany({
|
||||
where: {
|
||||
documentId,
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
signingOrder: {
|
||||
sort: 'asc',
|
||||
nulls: 'last',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'asc',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const currentIndex = recipients.findIndex((r) => r.id === currentRecipientId);
|
||||
|
||||
if (currentIndex === -1 || currentIndex === recipients.length - 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...recipients[currentIndex + 1],
|
||||
token: '',
|
||||
};
|
||||
};
|
||||
@@ -83,7 +83,6 @@ export type CreateDocumentFromTemplateOptions = {
|
||||
language?: SupportedLanguageCodes;
|
||||
distributionMethod?: DocumentDistributionMethod;
|
||||
typedSignatureEnabled?: boolean;
|
||||
allowDictateNextSigner?: boolean;
|
||||
emailSettings?: TDocumentEmailSettings;
|
||||
};
|
||||
requestMetadata: ApiRequestMetadata;
|
||||
@@ -405,10 +404,6 @@ export const createDocumentFromTemplate = async ({
|
||||
template.team?.teamGlobalSettings?.documentLanguage,
|
||||
typedSignatureEnabled:
|
||||
override?.typedSignatureEnabled ?? template.templateMeta?.typedSignatureEnabled,
|
||||
allowDictateNextSigner:
|
||||
override?.allowDictateNextSigner ??
|
||||
template.templateMeta?.allowDictateNextSigner ??
|
||||
false,
|
||||
},
|
||||
},
|
||||
recipients: {
|
||||
|
||||
@@ -51,7 +51,6 @@ export const ZDocumentSchema = DocumentSchema.pick({
|
||||
documentId: true,
|
||||
redirectUrl: true,
|
||||
typedSignatureEnabled: true,
|
||||
allowDictateNextSigner: true,
|
||||
language: true,
|
||||
emailSettings: true,
|
||||
}).nullable(),
|
||||
|
||||
@@ -45,7 +45,6 @@ export const ZTemplateSchema = TemplateSchema.pick({
|
||||
dateFormat: true,
|
||||
signingOrder: true,
|
||||
typedSignatureEnabled: true,
|
||||
allowDictateNextSigner: true,
|
||||
distributionMethod: true,
|
||||
templateId: true,
|
||||
redirectUrl: true,
|
||||
|
||||
@@ -46,7 +46,6 @@ export const ZWebhookDocumentMetaSchema = z.object({
|
||||
dateFormat: z.string(),
|
||||
redirectUrl: z.string().nullable(),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
allowDictateNextSigner: z.boolean(),
|
||||
typedSignatureEnabled: z.boolean(),
|
||||
language: z.string(),
|
||||
distributionMethod: z.nativeEnum(DocumentDistributionMethod),
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Document" ADD COLUMN "folderId" INTEGER;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Folder" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"teamId" INTEGER,
|
||||
"parentId" INTEGER,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Folder_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Folder_userId_idx" ON "Folder"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Folder_teamId_idx" ON "Folder"("teamId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Folder_parentId_idx" ON "Folder"("parentId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Document_folderId_idx" ON "Document"("folderId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Folder" ADD CONSTRAINT "Folder_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Folder" ADD CONSTRAINT "Folder_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Folder" ADD CONSTRAINT "Folder_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Folder"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Document" ADD CONSTRAINT "Document_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "DocumentMeta" ADD COLUMN "allowDictateNextSigner" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "TemplateMeta" ADD COLUMN "allowDictateNextSigner" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -56,6 +56,7 @@ model User {
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
documents Document[]
|
||||
folders Folder[]
|
||||
subscriptions Subscription[]
|
||||
passwordResetTokens PasswordResetToken[]
|
||||
ownedTeams Team[]
|
||||
@@ -312,6 +313,25 @@ enum DocumentVisibility {
|
||||
ADMIN
|
||||
}
|
||||
|
||||
model Folder {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
teamId Int?
|
||||
team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade)
|
||||
parentId Int?
|
||||
parent Folder? @relation("FolderToFolder", fields: [parentId], references: [id], onDelete: Cascade)
|
||||
subfolders Folder[] @relation("FolderToFolder")
|
||||
documents Document[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
@@index([teamId])
|
||||
@@index([parentId])
|
||||
}
|
||||
|
||||
/// @zod.import(["import { ZDocumentAuthOptionsSchema } from '@documenso/lib/types/document-auth';", "import { ZDocumentFormValuesSchema } from '@documenso/lib/types/document-form-values';"])
|
||||
model Document {
|
||||
id Int @id @default(autoincrement())
|
||||
@@ -340,10 +360,13 @@ model Document {
|
||||
source DocumentSource
|
||||
|
||||
auditLogs DocumentAuditLog[]
|
||||
Folder Folder? @relation(fields: [folderId], references: [id])
|
||||
folderId Int?
|
||||
|
||||
@@unique([documentDataId])
|
||||
@@index([userId])
|
||||
@@index([status])
|
||||
@@index([folderId])
|
||||
}
|
||||
|
||||
model DocumentAuditLog {
|
||||
@@ -390,21 +413,20 @@ enum DocumentDistributionMethod {
|
||||
|
||||
/// @zod.import(["import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';"])
|
||||
model DocumentMeta {
|
||||
id String @id @default(cuid())
|
||||
subject String?
|
||||
message String?
|
||||
timezone String? @default("Etc/UTC") @db.Text
|
||||
password String?
|
||||
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
|
||||
documentId Int @unique
|
||||
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
redirectUrl String?
|
||||
signingOrder DocumentSigningOrder @default(PARALLEL)
|
||||
allowDictateNextSigner Boolean @default(false)
|
||||
typedSignatureEnabled Boolean @default(true)
|
||||
language String @default("en")
|
||||
distributionMethod DocumentDistributionMethod @default(EMAIL)
|
||||
emailSettings Json? /// [DocumentEmailSettings] @zod.custom.use(ZDocumentEmailSettingsSchema)
|
||||
id String @id @default(cuid())
|
||||
subject String?
|
||||
message String?
|
||||
timezone String? @default("Etc/UTC") @db.Text
|
||||
password String?
|
||||
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
|
||||
documentId Int @unique
|
||||
document Document @relation(fields: [documentId], references: [id], onDelete: Cascade)
|
||||
redirectUrl String?
|
||||
signingOrder DocumentSigningOrder @default(PARALLEL)
|
||||
typedSignatureEnabled Boolean @default(true)
|
||||
language String @default("en")
|
||||
distributionMethod DocumentDistributionMethod @default(EMAIL)
|
||||
emailSettings Json? /// [DocumentEmailSettings] @zod.custom.use(ZDocumentEmailSettingsSchema)
|
||||
}
|
||||
|
||||
enum ReadStatus {
|
||||
@@ -580,6 +602,7 @@ model Team {
|
||||
|
||||
documents Document[]
|
||||
templates Template[]
|
||||
folders Folder[]
|
||||
apiTokens ApiToken[]
|
||||
webhooks Webhook[]
|
||||
}
|
||||
@@ -661,16 +684,15 @@ enum TemplateType {
|
||||
|
||||
/// @zod.import(["import { ZDocumentEmailSettingsSchema } from '@documenso/lib/types/document-email';"])
|
||||
model TemplateMeta {
|
||||
id String @id @default(cuid())
|
||||
subject String?
|
||||
message String?
|
||||
timezone String? @default("Etc/UTC") @db.Text
|
||||
password String?
|
||||
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
|
||||
signingOrder DocumentSigningOrder? @default(PARALLEL)
|
||||
allowDictateNextSigner Boolean @default(false)
|
||||
typedSignatureEnabled Boolean @default(true)
|
||||
distributionMethod DocumentDistributionMethod @default(EMAIL)
|
||||
id String @id @default(cuid())
|
||||
subject String?
|
||||
message String?
|
||||
timezone String? @default("Etc/UTC") @db.Text
|
||||
password String?
|
||||
dateFormat String? @default("yyyy-MM-dd hh:mm a") @db.Text
|
||||
signingOrder DocumentSigningOrder? @default(PARALLEL)
|
||||
typedSignatureEnabled Boolean @default(true)
|
||||
distributionMethod DocumentDistributionMethod @default(EMAIL)
|
||||
|
||||
templateId Int @unique
|
||||
template Template @relation(fields: [templateId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@ -371,7 +371,6 @@ export const documentRouter = router({
|
||||
redirectUrl: meta.redirectUrl,
|
||||
distributionMethod: meta.distributionMethod,
|
||||
signingOrder: meta.signingOrder,
|
||||
allowDictateNextSigner: meta.allowDictateNextSigner,
|
||||
emailSettings: meta.emailSettings,
|
||||
requestMetadata: ctx.metadata,
|
||||
});
|
||||
|
||||
@@ -268,7 +268,6 @@ export const ZUpdateDocumentRequestSchema = z.object({
|
||||
dateFormat: ZDocumentMetaDateFormatSchema.optional(),
|
||||
distributionMethod: ZDocumentMetaDistributionMethodSchema.optional(),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||
allowDictateNextSigner: z.boolean().optional(),
|
||||
redirectUrl: ZDocumentMetaRedirectUrlSchema.optional(),
|
||||
language: ZDocumentMetaLanguageSchema.optional(),
|
||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||
|
||||
@@ -436,13 +436,12 @@ export const recipientRouter = router({
|
||||
completeDocumentWithToken: procedure
|
||||
.input(ZCompleteDocumentWithTokenMutationSchema)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { token, documentId, authOptions, nextSigner } = input;
|
||||
const { token, documentId, authOptions } = input;
|
||||
|
||||
return await completeDocumentWithToken({
|
||||
token,
|
||||
documentId,
|
||||
authOptions,
|
||||
nextSigner,
|
||||
userId: ctx.user?.id,
|
||||
requestMetadata: ctx.metadata.requestMetadata,
|
||||
});
|
||||
|
||||
@@ -212,12 +212,6 @@ export const ZCompleteDocumentWithTokenMutationSchema = z.object({
|
||||
token: z.string(),
|
||||
documentId: z.number(),
|
||||
authOptions: ZRecipientActionAuthSchema.optional(),
|
||||
nextSigner: z
|
||||
.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export type TCompleteDocumentWithTokenMutationSchema = z.infer<
|
||||
|
||||
@@ -3,6 +3,7 @@ import { apiTokenRouter } from './api-token-router/router';
|
||||
import { authRouter } from './auth-router/router';
|
||||
import { documentRouter } from './document-router/router';
|
||||
import { fieldRouter } from './field-router/router';
|
||||
import { folderRouter } from './folder-router/router';
|
||||
import { profileRouter } from './profile-router/router';
|
||||
import { recipientRouter } from './recipient-router/router';
|
||||
import { shareLinkRouter } from './share-link-router/router';
|
||||
@@ -16,6 +17,7 @@ export const appRouter = router({
|
||||
profile: profileRouter,
|
||||
document: documentRouter,
|
||||
field: fieldRouter,
|
||||
folder: folderRouter,
|
||||
recipient: recipientRouter,
|
||||
admin: adminRouter,
|
||||
shareLink: shareLinkRouter,
|
||||
|
||||
@@ -165,7 +165,6 @@ export const ZUpdateTemplateRequestSchema = z.object({
|
||||
language: ZDocumentMetaLanguageSchema.optional(),
|
||||
typedSignatureEnabled: ZDocumentMetaTypedSignatureEnabledSchema.optional(),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder).optional(),
|
||||
allowDictateNextSigner: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Trans } from '@lingui/react/macro';
|
||||
import type { Field, Recipient } from '@prisma/client';
|
||||
import { DocumentSigningOrder, RecipientRole, SendStatus } from '@prisma/client';
|
||||
import { motion } from 'framer-motion';
|
||||
import { GripVerticalIcon, HelpCircle, Plus, Trash } from 'lucide-react';
|
||||
import { GripVerticalIcon, Plus, Trash } from 'lucide-react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
import { prop, sortBy } from 'remeda';
|
||||
|
||||
@@ -29,7 +29,6 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '
|
||||
import { FormErrorMessage } from '../form/form-error-message';
|
||||
import { Input } from '../input';
|
||||
import { useStep } from '../stepper';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
|
||||
import { useToast } from '../use-toast';
|
||||
import type { TAddSignersFormSchema } from './add-signers.types';
|
||||
import { ZAddSignersFormSchema } from './add-signers.types';
|
||||
@@ -49,7 +48,6 @@ export type AddSignersFormProps = {
|
||||
recipients: Recipient[];
|
||||
fields: Field[];
|
||||
signingOrder?: DocumentSigningOrder | null;
|
||||
allowDictateNextSigner?: boolean;
|
||||
isDocumentEnterprise: boolean;
|
||||
onSubmit: (_data: TAddSignersFormSchema) => void;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
@@ -60,7 +58,6 @@ export const AddSignersFormPartial = ({
|
||||
recipients,
|
||||
fields,
|
||||
signingOrder,
|
||||
allowDictateNextSigner,
|
||||
isDocumentEnterprise,
|
||||
onSubmit,
|
||||
isDocumentPdfLoaded,
|
||||
@@ -107,7 +104,6 @@ export const AddSignersFormPartial = ({
|
||||
)
|
||||
: defaultRecipients,
|
||||
signingOrder: signingOrder || DocumentSigningOrder.PARALLEL,
|
||||
allowDictateNextSigner: allowDictateNextSigner ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -358,7 +354,6 @@ export const AddSignersFormPartial = ({
|
||||
|
||||
form.setValue('signers', updatedSigners);
|
||||
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL);
|
||||
form.setValue('allowDictateNextSigner', false);
|
||||
}, [form]);
|
||||
|
||||
return (
|
||||
@@ -394,11 +389,6 @@ export const AddSignersFormPartial = ({
|
||||
field.onChange(
|
||||
checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL,
|
||||
);
|
||||
|
||||
// If sequential signing is turned off, disable dictate next signer
|
||||
if (!checked) {
|
||||
form.setValue('allowDictateNextSigner', false);
|
||||
}
|
||||
}}
|
||||
disabled={isSubmitting || hasDocumentBeenSent}
|
||||
/>
|
||||
@@ -413,50 +403,6 @@ export const AddSignersFormPartial = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="allowDictateNextSigner"
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<FormItem className="mb-6 flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
{...field}
|
||||
id="allowDictateNextSigner"
|
||||
checked={value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={isSubmitting || hasDocumentBeenSent || !isSigningOrderSequential}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<div className="flex items-center">
|
||||
<FormLabel
|
||||
htmlFor="allowDictateNextSigner"
|
||||
className="text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
<Trans>Allow signers to dictate next signer</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-muted-foreground ml-1 cursor-help">
|
||||
<HelpCircle className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-80 p-4">
|
||||
<p>
|
||||
<Trans>
|
||||
When enabled, signers can choose who should sign next in the sequence
|
||||
instead of following the predefined order.
|
||||
</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<DragDropContext
|
||||
onDragEnd={onDragEnd}
|
||||
sensors={[
|
||||
|
||||
@@ -25,7 +25,6 @@ export const ZAddSignersFormSchema = z
|
||||
}),
|
||||
),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
allowDictateNextSigner: z.boolean().default(false),
|
||||
})
|
||||
.refine(
|
||||
(schema) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Trans } from '@lingui/react/macro';
|
||||
import type { TemplateDirectLink } from '@prisma/client';
|
||||
import { DocumentSigningOrder, type Field, type Recipient, RecipientRole } from '@prisma/client';
|
||||
import { motion } from 'framer-motion';
|
||||
import { GripVerticalIcon, HelpCircle, Link2Icon, Plus, Trash } from 'lucide-react';
|
||||
import { GripVerticalIcon, Link2Icon, Plus, Trash } from 'lucide-react';
|
||||
import { useFieldArray, useForm } from 'react-hook-form';
|
||||
|
||||
import { useSession } from '@documenso/lib/client-only/providers/session';
|
||||
@@ -47,11 +47,10 @@ export type AddTemplatePlaceholderRecipientsFormProps = {
|
||||
recipients: Recipient[];
|
||||
fields: Field[];
|
||||
signingOrder?: DocumentSigningOrder | null;
|
||||
allowDictateNextSigner?: boolean;
|
||||
templateDirectLink?: TemplateDirectLink | null;
|
||||
templateDirectLink: TemplateDirectLink | null;
|
||||
isEnterprise: boolean;
|
||||
onSubmit: (_data: TAddTemplatePlacholderRecipientsFormSchema) => void;
|
||||
isDocumentPdfLoaded: boolean;
|
||||
onSubmit: (_data: TAddTemplatePlacholderRecipientsFormSchema) => void;
|
||||
};
|
||||
|
||||
export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
@@ -61,7 +60,6 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
templateDirectLink,
|
||||
fields,
|
||||
signingOrder,
|
||||
allowDictateNextSigner,
|
||||
isDocumentPdfLoaded,
|
||||
onSubmit,
|
||||
}: AddTemplatePlaceholderRecipientsFormProps) => {
|
||||
@@ -114,7 +112,6 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
defaultValues: {
|
||||
signers: generateDefaultFormSigners(),
|
||||
signingOrder: signingOrder || DocumentSigningOrder.PARALLEL,
|
||||
allowDictateNextSigner: allowDictateNextSigner ?? false,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -122,7 +119,6 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
form.reset({
|
||||
signers: generateDefaultFormSigners(),
|
||||
signingOrder: signingOrder || DocumentSigningOrder.PARALLEL,
|
||||
allowDictateNextSigner: allowDictateNextSigner ?? false,
|
||||
});
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -381,7 +377,6 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
|
||||
form.setValue('signers', updatedSigners);
|
||||
form.setValue('signingOrder', DocumentSigningOrder.PARALLEL);
|
||||
form.setValue('allowDictateNextSigner', false);
|
||||
}, [form]);
|
||||
|
||||
return (
|
||||
@@ -421,11 +416,6 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
field.onChange(
|
||||
checked ? DocumentSigningOrder.SEQUENTIAL : DocumentSigningOrder.PARALLEL,
|
||||
);
|
||||
|
||||
// If sequential signing is turned off, disable dictate next signer
|
||||
if (!checked) {
|
||||
form.setValue('allowDictateNextSigner', false);
|
||||
}
|
||||
}}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
@@ -441,49 +431,6 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="allowDictateNextSigner"
|
||||
render={({ field: { value, ...field } }) => (
|
||||
<FormItem className="mb-6 flex flex-row items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
{...field}
|
||||
id="allowDictateNextSigner"
|
||||
checked={value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled={isSubmitting || !isSigningOrderSequential}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<div className="flex items-center">
|
||||
<FormLabel
|
||||
htmlFor="allowDictateNextSigner"
|
||||
className="text-sm leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
<Trans>Allow signers to dictate next signer</Trans>
|
||||
</FormLabel>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="text-muted-foreground ml-1 cursor-help">
|
||||
<HelpCircle className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-80 p-4">
|
||||
<p>
|
||||
<Trans>
|
||||
When enabled, signers can choose who should sign next in the sequence
|
||||
instead of following the predefined order.
|
||||
</Trans>
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Drag and drop context */}
|
||||
<DragDropContext
|
||||
onDragEnd={onDragEnd}
|
||||
|
||||
@@ -21,7 +21,6 @@ export const ZAddTemplatePlacholderRecipientsFormSchema = z
|
||||
}),
|
||||
),
|
||||
signingOrder: z.nativeEnum(DocumentSigningOrder),
|
||||
allowDictateNextSigner: z.boolean().default(false),
|
||||
})
|
||||
.refine(
|
||||
(schema) => {
|
||||
|
||||
Reference in New Issue
Block a user