mirror of
https://github.com/documenso/documenso.git
synced 2025-11-16 09:41:35 +10:00
Merge branch 'main' of https://github.com/documenso/documenso into feat/redirect-templates
This commit is contained in:
@ -17,7 +17,8 @@ For the digital signature of your documents you need a signing certificate in .p
|
|||||||
`openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt`
|
`openssl pkcs12 -export -out certificate.p12 -inkey private.key -in certificate.crt`
|
||||||
|
|
||||||
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
|
4. You will be prompted to enter a password for the p12 file. Choose a strong password and remember it, as you will need it to use the certificate (**can be empty for dev certificates**)
|
||||||
5. Place the certificate `/apps/web/resources/certificate.p12`
|
|
||||||
|
5. Place the certificate `/apps/web/resources/certificate.p12` (If the path does not exist, it needs to be created)
|
||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { TClaimPlanRequestSchema, ZClaimPlanResponseSchema } from './types';
|
import type { TClaimPlanRequestSchema } from './types';
|
||||||
|
import { ZClaimPlanResponseSchema } from './types';
|
||||||
|
|
||||||
export const claimPlan = async ({
|
export const claimPlan = async ({
|
||||||
name,
|
name,
|
||||||
|
|||||||
@ -55,6 +55,7 @@ export const BarMetric = <T extends Record<string, Record<keyof T[string], unkno
|
|||||||
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
cursor={{ fill: 'hsl(var(--primary) / 10%)' }}
|
||||||
/>
|
/>
|
||||||
<Bar
|
<Bar
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
dataKey={metricKey as string}
|
dataKey={metricKey as string}
|
||||||
maxBarSize={60}
|
maxBarSize={60}
|
||||||
fill="hsl(var(--primary))"
|
fill="hsl(var(--primary))"
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export type FundingRaisedProps = HTMLAttributes<HTMLDivElement> & {
|
|||||||
export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps) => {
|
export const FundingRaised = ({ className, data, ...props }: FundingRaisedProps) => {
|
||||||
const formattedData = data.map((item) => ({
|
const formattedData = data.map((item) => ({
|
||||||
amount: Number(item.amount),
|
amount: Number(item.amount),
|
||||||
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
date: formatMonth(item.date as string),
|
date: formatMonth(item.date as string),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -2,13 +2,14 @@
|
|||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { Variants, motion } from 'framer-motion';
|
import type { Variants } from 'framer-motion';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card';
|
import { Card, CardContent, CardTitle } from '@documenso/ui/primitives/card';
|
||||||
|
|
||||||
import { TOSSFriendsSchema } from './schema';
|
import type { TOSSFriendsSchema } from './schema';
|
||||||
|
|
||||||
const ContainerVariants: Variants = {
|
const ContainerVariants: Variants = {
|
||||||
initial: {
|
initial: {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { MetadataRoute } from 'next';
|
import type { MetadataRoute } from 'next';
|
||||||
|
|
||||||
import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
|
import { getBaseUrl } from '@documenso/lib/universal/get-base-url';
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { MetadataRoute } from 'next';
|
import type { MetadataRoute } from 'next';
|
||||||
|
|
||||||
import { allBlogPosts, allGenericPages } from 'contentlayer/generated';
|
import { allBlogPosts, allGenericPages } from 'contentlayer/generated';
|
||||||
|
|
||||||
|
|||||||
@ -96,7 +96,7 @@ export const Hero = ({ className, ...props }: HeroProps) => {
|
|||||||
variants={HeroTitleVariants}
|
variants={HeroTitleVariants}
|
||||||
initial="initial"
|
initial="initial"
|
||||||
animate="animate"
|
animate="animate"
|
||||||
className="text-center text-4xl font-bold leading-tight tracking-tight lg:text-[64px]"
|
className="text-center text-4xl font-bold leading-tight tracking-tight md:text-[48px] lg:text-[64px]"
|
||||||
>
|
>
|
||||||
Document signing,
|
Document signing,
|
||||||
<span className="block" /> finally open source.
|
<span className="block" /> finally open source.
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
|
|||||||
@ -346,7 +346,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
|||||||
{signatureText && (
|
{signatureText && (
|
||||||
<p
|
<p
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-foreground text-4xl font-semibold [font-family:var(--font-caveat)]',
|
'text-foreground truncate text-4xl font-semibold [font-family:var(--font-caveat)]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{signatureText}
|
{signatureText}
|
||||||
@ -360,7 +360,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => {
|
|||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
id="signatureText"
|
id="signatureText"
|
||||||
className="text-foreground placeholder:text-muted-foreground border-none p-0 text-sm focus-visible:ring-0"
|
className="text-foreground placeholder:text-muted-foreground truncate border-none p-0 text-sm focus-visible:ring-0"
|
||||||
placeholder="Draw or type name here"
|
placeholder="Draw or type name here"
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
{...register('signatureText', {
|
{...register('signatureText', {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { FieldError } from 'react-hook-form';
|
import type { FieldError } from 'react-hook-form';
|
||||||
|
|
||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { SVGAttributes } from 'react';
|
import type { SVGAttributes } from 'react';
|
||||||
|
|
||||||
export type BackgroundProps = Omit<SVGAttributes<SVGElement>, 'viewBox'>;
|
export type BackgroundProps = Omit<SVGAttributes<SVGElement>, 'viewBox'>;
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||||
import { ThemeProviderProps } from 'next-themes/dist/types';
|
import type { ThemeProviderProps } from 'next-themes/dist/types';
|
||||||
|
|
||||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
|
|||||||
@ -118,7 +118,7 @@ export const DocumentPageViewDropdown = ({ document, team }: DocumentPageViewDro
|
|||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href={`${documentsPath}/${document.id}/logs`}>
|
<Link href={`${documentsPath}/${document.id}/logs`}>
|
||||||
<ScrollTextIcon className="mr-2 h-4 w-4" />
|
<ScrollTextIcon className="mr-2 h-4 w-4" />
|
||||||
Logs
|
Audit Log
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,10 @@ import { Button } from '@documenso/ui/primitives/button';
|
|||||||
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
import { Card, CardContent } from '@documenso/ui/primitives/card';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
@ -34,7 +37,6 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@documenso/ui/primitives/form/form';
|
} from '@documenso/ui/primitives/form/form';
|
||||||
import { Input } from '@documenso/ui/primitives/input';
|
import { Input } from '@documenso/ui/primitives/input';
|
||||||
import { Label } from '@documenso/ui/primitives/label';
|
|
||||||
import { useToast } from '@documenso/ui/primitives/use-toast';
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
||||||
|
|
||||||
const ZCreateTemplateFormSchema = z.object({
|
const ZCreateTemplateFormSchema = z.object({
|
||||||
@ -61,8 +63,7 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo
|
|||||||
resolver: zodResolver(ZCreateTemplateFormSchema),
|
resolver: zodResolver(ZCreateTemplateFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutateAsync: createTemplate, isLoading: isCreatingTemplate } =
|
const { mutateAsync: createTemplate } = trpc.template.createTemplate.useMutation();
|
||||||
trpc.template.createTemplate.useMutation();
|
|
||||||
|
|
||||||
const [showNewTemplateDialog, setShowNewTemplateDialog] = useState(false);
|
const [showNewTemplateDialog, setShowNewTemplateDialog] = useState(false);
|
||||||
const [uploadedFile, setUploadedFile] = useState<{ file: File; fileBase64: string } | null>();
|
const [uploadedFile, setUploadedFile] = useState<{ file: File; fileBase64: string } | null>();
|
||||||
@ -140,6 +141,7 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showNewTemplateDialog) {
|
if (!showNewTemplateDialog) {
|
||||||
form.reset();
|
form.reset();
|
||||||
|
setUploadedFile(null);
|
||||||
}
|
}
|
||||||
}, [form, showNewTemplateDialog]);
|
}, [form, showNewTemplateDialog]);
|
||||||
|
|
||||||
@ -154,20 +156,23 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo
|
|||||||
|
|
||||||
<DialogContent className="w-full max-w-xl">
|
<DialogContent className="w-full max-w-xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="mb-4">New Template</DialogTitle>
|
<DialogTitle>New Template</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Templates allow you to quickly generate documents with pre-filled recipients and fields.
|
||||||
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div>
|
<Form {...form}>
|
||||||
<Form {...form}>
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-y-4">
|
<fieldset disabled={form.formState.isSubmitting} className="flex flex-col gap-y-4">
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Name your template</FormLabel>
|
<FormLabel>Template name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input id="email" type="text" className="bg-background mt-1.5" {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
<span className="text-muted-foreground text-xs">
|
<span className="text-muted-foreground text-xs">
|
||||||
@ -180,55 +185,57 @@ export const NewTemplateDialog = ({ teamId, templateRootPath }: NewTemplateDialo
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div className="mt-1.5">
|
||||||
<Label htmlFor="template">Upload a Document</Label>
|
{uploadedFile ? (
|
||||||
|
<Card gradient className="h-[40vh]">
|
||||||
|
<CardContent className="flex h-full flex-col items-center justify-center p-2">
|
||||||
|
<button
|
||||||
|
onClick={() => resetForm()}
|
||||||
|
title="Remove Template"
|
||||||
|
className="text-muted-foreground absolute right-2.5 top-2.5 rounded-sm opacity-60 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
|
||||||
|
>
|
||||||
|
<X className="h-6 w-6" />
|
||||||
|
<span className="sr-only">Remove Template</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<div className="my-3">
|
<div className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm">
|
||||||
{uploadedFile ? (
|
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
||||||
<Card gradient className="h-[40vh]">
|
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
|
||||||
<CardContent className="flex h-full flex-col items-center justify-center p-2">
|
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
||||||
<button
|
</div>
|
||||||
onClick={() => resetForm()}
|
|
||||||
title="Remove Template"
|
|
||||||
className="text-muted-foreground absolute right-2.5 top-2.5 rounded-sm opacity-60 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none"
|
|
||||||
>
|
|
||||||
<X className="h-6 w-6" />
|
|
||||||
<span className="sr-only">Remove Template</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="border-muted-foreground/20 group-hover:border-documenso/80 dark:bg-muted/80 z-10 flex aspect-[3/4] w-24 flex-col gap-y-1 rounded-lg border bg-white/80 px-2 py-4 backdrop-blur-sm">
|
<p className="group-hover:text-foreground text-muted-foreground mt-4 font-medium">
|
||||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
Uploaded Document
|
||||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-5/6 rounded-[2px]" />
|
</p>
|
||||||
<div className="bg-muted-foreground/20 group-hover:bg-documenso h-2 w-full rounded-[2px]" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="group-hover:text-foreground text-muted-foreground mt-4 font-medium">
|
<span className="text-muted-foreground/80 mt-1 text-sm">
|
||||||
Uploaded Document
|
{uploadedFile.file.name}
|
||||||
</p>
|
</span>
|
||||||
|
</CardContent>
|
||||||
<span className="text-muted-foreground/80 mt-1 text-sm">
|
</Card>
|
||||||
{uploadedFile.file.name}
|
) : (
|
||||||
</span>
|
<DocumentDropzone className="h-[40vh]" onDrop={onFileDrop} type="template" />
|
||||||
</CardContent>
|
)}
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
<DocumentDropzone
|
|
||||||
className="mt-1.5 h-[40vh]"
|
|
||||||
onDrop={onFileDrop}
|
|
||||||
type="template"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex w-full justify-end">
|
<DialogFooter>
|
||||||
<Button loading={isCreatingTemplate} type="submit">
|
<DialogClose asChild>
|
||||||
Create Template
|
<Button type="button" variant="secondary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
loading={form.formState.isSubmitting}
|
||||||
|
disabled={!uploadedFile}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Create template
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</DialogFooter>
|
||||||
</form>
|
</fieldset>
|
||||||
</Form>
|
</form>
|
||||||
</div>
|
</Form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -131,7 +131,7 @@ export default async function CompletedSigningPage({
|
|||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
.with({ deletedAt: null }, () => (
|
.with({ deletedAt: null }, () => (
|
||||||
<div className="flex items-center text-center text-blue-600">
|
<div className="mt-4 flex items-center text-center text-blue-600">
|
||||||
<Clock8 className="mr-2 h-5 w-5" />
|
<Clock8 className="mr-2 h-5 w-5" />
|
||||||
<span className="text-sm">Waiting for others to sign</span>
|
<span className="text-sm">Waiting for others to sign</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { useCallback, useMemo, useState } from 'react';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { Loader, Monitor, Moon, Sun } from 'lucide-react';
|
import { Loader, Monitor, Moon, Sun } from 'lucide-react';
|
||||||
import { useSession } from 'next-auth/react';
|
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
|
||||||
@ -18,7 +17,6 @@ import {
|
|||||||
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
DO_NOT_INVALIDATE_QUERY_ON_MUTATION,
|
||||||
SKIP_QUERY_BATCH_META,
|
SKIP_QUERY_BATCH_META,
|
||||||
} from '@documenso/lib/constants/trpc';
|
} from '@documenso/lib/constants/trpc';
|
||||||
import type { Document, Recipient } from '@documenso/prisma/client';
|
|
||||||
import { trpc as trpcReact } from '@documenso/trpc/react';
|
import { trpc as trpcReact } from '@documenso/trpc/react';
|
||||||
import {
|
import {
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
@ -71,7 +69,6 @@ export type CommandMenuProps = {
|
|||||||
|
|
||||||
export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
const { data: session } = useSession();
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@ -93,17 +90,6 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const isOwner = useCallback(
|
|
||||||
(document: Document) => document.userId === session?.user.id,
|
|
||||||
[session?.user.id],
|
|
||||||
);
|
|
||||||
|
|
||||||
const getSigningLink = useCallback(
|
|
||||||
(recipients: Recipient[]) =>
|
|
||||||
`/sign/${recipients.find((r) => r.email === session?.user.email)?.token}`,
|
|
||||||
[session?.user.email],
|
|
||||||
);
|
|
||||||
|
|
||||||
const searchResults = useMemo(() => {
|
const searchResults = useMemo(() => {
|
||||||
if (!searchDocumentsData) {
|
if (!searchDocumentsData) {
|
||||||
return [];
|
return [];
|
||||||
@ -111,10 +97,10 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) {
|
|||||||
|
|
||||||
return searchDocumentsData.map((document) => ({
|
return searchDocumentsData.map((document) => ({
|
||||||
label: document.title,
|
label: document.title,
|
||||||
path: isOwner(document) ? `/documents/${document.id}` : getSigningLink(document.Recipient),
|
path: document.path,
|
||||||
value: [document.id, document.title, ...document.Recipient.map((r) => r.email)].join(' '),
|
value: document.value,
|
||||||
}));
|
}));
|
||||||
}, [searchDocumentsData, isOwner, getSigningLink]);
|
}, [searchDocumentsData]);
|
||||||
|
|
||||||
const currentPage = pages[pages.length - 1];
|
const currentPage = pages[pages.length - 1];
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import type { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
@ -12,8 +10,6 @@ import { getRootHref } from '@documenso/lib/utils/params';
|
|||||||
import { cn } from '@documenso/ui/lib/utils';
|
import { cn } from '@documenso/ui/lib/utils';
|
||||||
import { Button } from '@documenso/ui/primitives/button';
|
import { Button } from '@documenso/ui/primitives/button';
|
||||||
|
|
||||||
import { CommandMenu } from '../common/command-menu';
|
|
||||||
|
|
||||||
const navigationLinks = [
|
const navigationLinks = [
|
||||||
{
|
{
|
||||||
href: '/documents',
|
href: '/documents',
|
||||||
@ -25,13 +21,14 @@ const navigationLinks = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export type DesktopNavProps = HTMLAttributes<HTMLDivElement>;
|
export type DesktopNavProps = HTMLAttributes<HTMLDivElement> & {
|
||||||
|
setIsCommandMenuOpen: (value: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
export const DesktopNav = ({ className, setIsCommandMenuOpen, ...props }: DesktopNavProps) => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const [modifierKey, setModifierKey] = useState(() => 'Ctrl');
|
const [modifierKey, setModifierKey] = useState(() => 'Ctrl');
|
||||||
|
|
||||||
const rootHref = getRootHref(params, { returnEmptyRootString: true });
|
const rootHref = getRootHref(params, { returnEmptyRootString: true });
|
||||||
@ -70,12 +67,10 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CommandMenu open={open} onOpenChange={setOpen} />
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-muted-foreground flex w-96 items-center justify-between rounded-lg"
|
className="text-muted-foreground flex w-96 items-center justify-between rounded-lg"
|
||||||
onClick={() => setOpen((open) => !open)}
|
onClick={() => setIsCommandMenuOpen(true)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Search className="mr-2 h-5 w-5" />
|
<Search className="mr-2 h-5 w-5" />
|
||||||
|
|||||||
@ -58,7 +58,7 @@ export const Header = ({ className, user, teams, ...props }: HeaderProps) => {
|
|||||||
<Logo className="h-6 w-auto" />
|
<Logo className="h-6 w-auto" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<DesktopNav />
|
<DesktopNav setIsCommandMenuOpen={setIsCommandMenuOpen} />
|
||||||
|
|
||||||
<div className="flex gap-x-4 md:ml-8">
|
<div className="flex gap-x-4 md:ml-8">
|
||||||
<MenuSwitcher user={user} teams={teams} />
|
<MenuSwitcher user={user} teams={teams} />
|
||||||
|
|||||||
@ -93,7 +93,7 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
|
|||||||
<Button
|
<Button
|
||||||
data-testid="menu-switcher"
|
data-testid="menu-switcher"
|
||||||
variant="none"
|
variant="none"
|
||||||
className="relative flex h-12 flex-row items-center px-2 py-2 ring-0 focus:outline-none focus-visible:border-0 focus-visible:ring-0 focus-visible:ring-transparent"
|
className="relative flex h-12 flex-row items-center px-0 py-2 ring-0 focus:outline-none focus-visible:border-0 focus-visible:ring-0 focus-visible:ring-transparent md:px-2"
|
||||||
>
|
>
|
||||||
<AvatarWithText
|
<AvatarWithText
|
||||||
avatarFallback={formatAvatarFallback(selectedTeam?.name)}
|
avatarFallback={formatAvatarFallback(selectedTeam?.name)}
|
||||||
@ -102,12 +102,13 @@ export const MenuSwitcher = ({ user, teams: initialTeamsData }: MenuSwitcherProp
|
|||||||
rightSideComponent={
|
rightSideComponent={
|
||||||
<ChevronsUpDown className="text-muted-foreground ml-auto h-4 w-4" />
|
<ChevronsUpDown className="text-muted-foreground ml-auto h-4 w-4" />
|
||||||
}
|
}
|
||||||
|
textSectionClassName="hidden lg:flex"
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
className={cn('z-[60] ml-2 w-full md:ml-0', teams ? 'min-w-[20rem]' : 'min-w-[12rem]')}
|
className={cn('z-[60] ml-6 w-full md:ml-0', teams ? 'min-w-[20rem]' : 'min-w-[12rem]')}
|
||||||
align="end"
|
align="end"
|
||||||
forceMount
|
forceMount
|
||||||
>
|
>
|
||||||
|
|||||||
@ -46,7 +46,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
|
<Sheet open={isMenuOpen} onOpenChange={onMenuOpenChange}>
|
||||||
<SheetContent className="flex w-full max-w-[400px] flex-col">
|
<SheetContent className="flex w-full max-w-[350px] flex-col">
|
||||||
<Link href="/" onClick={handleMenuItemClick}>
|
<Link href="/" onClick={handleMenuItemClick}>
|
||||||
<Image
|
<Image
|
||||||
src={LogoImage}
|
src={LogoImage}
|
||||||
@ -87,7 +87,7 @@ export const MobileNavigation = ({ isMenuOpen, onMenuOpenChange }: MobileNavigat
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
© {new Date().getFullYear()} Documenso, Inc. All rights reserved.
|
© {new Date().getFullYear()} Documenso, Inc. <br /> All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
|
|||||||
@ -2,16 +2,34 @@ import { generateOpenApi } from '@ts-rest/open-api';
|
|||||||
|
|
||||||
import { ApiContractV1 } from './contract';
|
import { ApiContractV1 } from './contract';
|
||||||
|
|
||||||
export const OpenAPIV1 = generateOpenApi(
|
export const OpenAPIV1 = Object.assign(
|
||||||
ApiContractV1,
|
generateOpenApi(
|
||||||
{
|
ApiContractV1,
|
||||||
info: {
|
{
|
||||||
title: 'Documenso API',
|
info: {
|
||||||
version: '1.0.0',
|
title: 'Documenso API',
|
||||||
description: 'The Documenso API for retrieving, creating, updating and deleting documents.',
|
version: '1.0.0',
|
||||||
|
description: 'The Documenso API for retrieving, creating, updating and deleting documents.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
{
|
||||||
|
setOperationId: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
{
|
{
|
||||||
setOperationId: true,
|
components: {
|
||||||
|
securitySchemes: {
|
||||||
|
authorization: {
|
||||||
|
type: 'apiKey',
|
||||||
|
in: 'header',
|
||||||
|
name: 'Authorization',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
authorization: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { prisma } from '@documenso/prisma';
|
import { prisma } from '@documenso/prisma';
|
||||||
import { DocumentStatus } from '@documenso/prisma/client';
|
import { DocumentStatus } from '@documenso/prisma/client';
|
||||||
|
import type { Document, Recipient, User } from '@documenso/prisma/client';
|
||||||
import { maskRecipientTokensForDocument } from '../../utils/mask-recipient-tokens-for-document';
|
|
||||||
|
|
||||||
export type SearchDocumentsWithKeywordOptions = {
|
export type SearchDocumentsWithKeywordOptions = {
|
||||||
query: string;
|
query: string;
|
||||||
@ -79,12 +78,19 @@ export const searchDocumentsWithKeyword = async ({
|
|||||||
take: limit,
|
take: limit,
|
||||||
});
|
});
|
||||||
|
|
||||||
const maskedDocuments = documents.map((document) =>
|
const isOwner = (document: Document, user: User) => document.userId === user.id;
|
||||||
maskRecipientTokensForDocument({
|
const getSigningLink = (recipients: Recipient[], user: User) =>
|
||||||
document,
|
`/sign/${recipients.find((r) => r.email === user.email)?.token}`;
|
||||||
user,
|
|
||||||
}),
|
const maskedDocuments = documents.map((document) => {
|
||||||
);
|
const { Recipient, ...documentWithoutRecipient } = document;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...documentWithoutRecipient,
|
||||||
|
path: isOwner(document, user) ? `/documents/${document.id}` : getSigningLink(Recipient, user),
|
||||||
|
value: [document.id, document.title, ...document.Recipient.map((r) => r.email)].join(' '),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return maskedDocuments;
|
return maskedDocuments;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -358,6 +358,7 @@ export const documentRouter = router({
|
|||||||
query,
|
query,
|
||||||
userId: ctx.user.id,
|
userId: ctx.user.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
return documents;
|
return documents;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|||||||
@ -55,6 +55,8 @@ type AvatarWithTextProps = {
|
|||||||
primaryText: React.ReactNode;
|
primaryText: React.ReactNode;
|
||||||
secondaryText?: React.ReactNode;
|
secondaryText?: React.ReactNode;
|
||||||
rightSideComponent?: React.ReactNode;
|
rightSideComponent?: React.ReactNode;
|
||||||
|
// Optional class to hide/show the text beside avatar
|
||||||
|
textSectionClassName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AvatarWithText = ({
|
const AvatarWithText = ({
|
||||||
@ -64,6 +66,7 @@ const AvatarWithText = ({
|
|||||||
primaryText,
|
primaryText,
|
||||||
secondaryText,
|
secondaryText,
|
||||||
rightSideComponent,
|
rightSideComponent,
|
||||||
|
textSectionClassName,
|
||||||
}: AvatarWithTextProps) => (
|
}: AvatarWithTextProps) => (
|
||||||
<div className={cn('flex w-full max-w-xs items-center gap-2', className)}>
|
<div className={cn('flex w-full max-w-xs items-center gap-2', className)}>
|
||||||
<Avatar
|
<Avatar
|
||||||
@ -72,7 +75,7 @@ const AvatarWithText = ({
|
|||||||
<AvatarFallback className="text-xs text-gray-400">{avatarFallback}</AvatarFallback>
|
<AvatarFallback className="text-xs text-gray-400">{avatarFallback}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|
||||||
<div className="flex flex-col text-left text-sm font-normal">
|
<div className={cn('flex flex-col text-left text-sm font-normal', textSectionClassName)}>
|
||||||
<span className="text-foreground truncate">{primaryText}</span>
|
<span className="text-foreground truncate">{primaryText}</span>
|
||||||
<span className="text-muted-foreground truncate text-xs">{secondaryText}</span>
|
<span className="text-muted-foreground truncate text-xs">{secondaryText}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -32,7 +32,11 @@ type CommandDialogProps = DialogProps & {
|
|||||||
const CommandDialog = ({ children, commandProps, ...props }: CommandDialogProps) => {
|
const CommandDialog = ({ children, commandProps, ...props }: CommandDialogProps) => {
|
||||||
return (
|
return (
|
||||||
<Dialog {...props}>
|
<Dialog {...props}>
|
||||||
<DialogContent className="overflow-hidden p-0 shadow-2xl">
|
<DialogContent
|
||||||
|
className="w-11/12 items-center overflow-hidden rounded-lg p-0 shadow-2xl lg:mt-0"
|
||||||
|
position="center"
|
||||||
|
overlayClassName="bg-background/60"
|
||||||
|
>
|
||||||
<Command
|
<Command
|
||||||
{...commandProps}
|
{...commandProps}
|
||||||
className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-0 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4"
|
className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-0 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4"
|
||||||
|
|||||||
@ -54,28 +54,35 @@ const DialogContent = React.forwardRef<
|
|||||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||||
position?: 'start' | 'end' | 'center';
|
position?: 'start' | 'end' | 'center';
|
||||||
hideClose?: boolean;
|
hideClose?: boolean;
|
||||||
|
/* Below prop is to add additional classes to the overlay */
|
||||||
|
overlayClassName?: string;
|
||||||
}
|
}
|
||||||
>(({ className, children, position = 'start', hideClose = false, ...props }, ref) => (
|
>(
|
||||||
<DialogPortal position={position}>
|
(
|
||||||
<DialogOverlay />
|
{ className, children, overlayClassName, position = 'start', hideClose = false, ...props },
|
||||||
<DialogPrimitive.Content
|
ref,
|
||||||
ref={ref}
|
) => (
|
||||||
className={cn(
|
<DialogPortal position={position}>
|
||||||
'bg-background animate-in data-[state=open]:fade-in-90 sm:zoom-in-90 data-[state=open]:slide-in-from-bottom-10 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-50 grid w-full gap-4 rounded-b-lg border p-6 shadow-lg sm:max-w-lg sm:rounded-lg',
|
<DialogOverlay className={cn(overlayClassName)} />
|
||||||
className,
|
<DialogPrimitive.Content
|
||||||
)}
|
ref={ref}
|
||||||
{...props}
|
className={cn(
|
||||||
>
|
'bg-background animate-in data-[state=open]:fade-in-90 sm:zoom-in-90 data-[state=open]:slide-in-from-bottom-10 data-[state=open]:sm:slide-in-from-bottom-0 fixed z-50 grid w-full gap-4 rounded-b-lg border p-6 shadow-lg sm:max-w-lg sm:rounded-lg',
|
||||||
{children}
|
className,
|
||||||
{!hideClose && (
|
)}
|
||||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
|
{...props}
|
||||||
<X className="h-4 w-4" />
|
>
|
||||||
<span className="sr-only">Close</span>
|
{children}
|
||||||
</DialogPrimitive.Close>
|
{!hideClose && (
|
||||||
)}
|
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:pointer-events-none">
|
||||||
</DialogPrimitive.Content>
|
<X className="h-4 w-4" />
|
||||||
</DialogPortal>
|
<span className="sr-only">Close</span>
|
||||||
));
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import React, { useId, useMemo, useState } from 'react';
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { InfoIcon, Plus, Trash } from 'lucide-react';
|
import { InfoIcon, Plus, Trash } from 'lucide-react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
import { useFieldArray, useForm } from 'react-hook-form';
|
import { useFieldArray, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
import { useLimits } from '@documenso/ee/server-only/limits/provider/client';
|
||||||
@ -60,6 +61,8 @@ export const AddSignersFormPartial = ({
|
|||||||
}: AddSignersFormProps) => {
|
}: AddSignersFormProps) => {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { remaining } = useLimits();
|
const { remaining } = useLimits();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const user = session?.user;
|
||||||
|
|
||||||
const initialId = useId();
|
const initialId = useId();
|
||||||
|
|
||||||
@ -135,6 +138,16 @@ export const AddSignersFormPartial = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onAddSelfSigner = () => {
|
||||||
|
appendSigner({
|
||||||
|
formId: nanoid(12),
|
||||||
|
name: user?.name ?? '',
|
||||||
|
email: user?.email ?? '',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
actionAuth: undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const onAddSigner = () => {
|
const onAddSigner = () => {
|
||||||
appendSigner({
|
appendSigner({
|
||||||
formId: nanoid(12),
|
formId: nanoid(12),
|
||||||
@ -209,8 +222,12 @@ export const AddSignersFormPartial = ({
|
|||||||
<Input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
|
|
||||||
{...field}
|
{...field}
|
||||||
|
disabled={
|
||||||
|
isSubmitting ||
|
||||||
|
hasBeenSentToRecipientId(signer.nativeId) ||
|
||||||
|
signers[index].email === user?.email
|
||||||
|
}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -237,8 +254,12 @@ export const AddSignersFormPartial = ({
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
disabled={isSubmitting || hasBeenSentToRecipientId(signer.nativeId)}
|
|
||||||
{...field}
|
{...field}
|
||||||
|
disabled={
|
||||||
|
isSubmitting ||
|
||||||
|
hasBeenSentToRecipientId(signer.nativeId) ||
|
||||||
|
signers[index].email === user?.email
|
||||||
|
}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
@ -340,29 +361,83 @@ export const AddSignersFormPartial = ({
|
|||||||
<SelectContent align="end">
|
<SelectContent align="end">
|
||||||
<SelectItem value={RecipientRole.SIGNER}>
|
<SelectItem value={RecipientRole.SIGNER}>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.SIGNER]}</span>
|
<div className="flex w-[150px] items-center">
|
||||||
Signer
|
<span className="mr-2">{ROLE_ICONS[RecipientRole.SIGNER]}</span>
|
||||||
</div>
|
Needs to sign
|
||||||
</SelectItem>
|
</div>
|
||||||
|
<Tooltip>
|
||||||
<SelectItem value={RecipientRole.CC}>
|
<TooltipTrigger>
|
||||||
<div className="flex items-center">
|
<InfoIcon className="h-4 w-4" />
|
||||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.CC]}</span>
|
</TooltipTrigger>
|
||||||
Receives copy
|
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
|
||||||
|
<p>
|
||||||
|
The recipient is required to sign the document for it to be
|
||||||
|
completed.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
||||||
<SelectItem value={RecipientRole.APPROVER}>
|
<SelectItem value={RecipientRole.APPROVER}>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.APPROVER]}</span>
|
<div className="flex w-[150px] items-center">
|
||||||
Approver
|
<span className="mr-2">
|
||||||
|
{ROLE_ICONS[RecipientRole.APPROVER]}
|
||||||
|
</span>
|
||||||
|
Needs to approve
|
||||||
|
</div>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
|
||||||
|
<p>
|
||||||
|
The recipient is required to approve the document for it to
|
||||||
|
be completed.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
||||||
<SelectItem value={RecipientRole.VIEWER}>
|
<SelectItem value={RecipientRole.VIEWER}>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.VIEWER]}</span>
|
<div className="flex w-[150px] items-center">
|
||||||
Viewer
|
<span className="mr-2">{ROLE_ICONS[RecipientRole.VIEWER]}</span>
|
||||||
|
Needs to view
|
||||||
|
</div>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
|
||||||
|
<p>
|
||||||
|
The recipient is required to view the document for it to be
|
||||||
|
completed.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
|
||||||
|
<SelectItem value={RecipientRole.CC}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex w-[150px] items-center">
|
||||||
|
<span className="mr-2">{ROLE_ICONS[RecipientRole.CC]}</span>
|
||||||
|
Receives copy
|
||||||
|
</div>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
|
||||||
|
<p>
|
||||||
|
The recipient is not required to take any action and
|
||||||
|
receives a copy of the document after it is completed.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@ -403,32 +478,46 @@ export const AddSignersFormPartial = ({
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
className="flex-1"
|
||||||
disabled={isSubmitting || signers.length >= remaining.recipients}
|
disabled={isSubmitting || signers.length >= remaining.recipients}
|
||||||
onClick={() => onAddSigner()}
|
onClick={() => onAddSigner()}
|
||||||
>
|
>
|
||||||
<Plus className="-ml-1 mr-2 h-5 w-5" />
|
<Plus className="-ml-1 mr-2 h-5 w-5" />
|
||||||
Add Signer
|
Add Signer
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
{!alwaysShowAdvancedSettings && isDocumentEnterprise && (
|
type="button"
|
||||||
<div className="flex flex-row items-center">
|
variant="secondary"
|
||||||
<Checkbox
|
className="dark:bg-muted dark:hover:bg-muted/80 bg-black/5 hover:bg-black/10"
|
||||||
id="showAdvancedRecipientSettings"
|
disabled={
|
||||||
className="h-5 w-5"
|
isSubmitting ||
|
||||||
checkClassName="dark:text-white text-primary"
|
form.getValues('signers').some((signer) => signer.email === user?.email)
|
||||||
checked={showAdvancedSettings}
|
}
|
||||||
onCheckedChange={(value) => setShowAdvancedSettings(Boolean(value))}
|
onClick={() => onAddSelfSigner()}
|
||||||
/>
|
>
|
||||||
|
<Plus className="-ml-1 mr-2 h-5 w-5" />
|
||||||
<label
|
Add myself
|
||||||
className="text-muted-foreground ml-2 text-sm"
|
</Button>
|
||||||
htmlFor="showAdvancedRecipientSettings"
|
|
||||||
>
|
|
||||||
Show advanced settings
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{!alwaysShowAdvancedSettings && isDocumentEnterprise && (
|
||||||
|
<div className="mt-4 flex flex-row items-center">
|
||||||
|
<Checkbox
|
||||||
|
id="showAdvancedRecipientSettings"
|
||||||
|
className="h-5 w-5"
|
||||||
|
checkClassName="dark:text-white text-primary"
|
||||||
|
checked={showAdvancedSettings}
|
||||||
|
onCheckedChange={(value) => setShowAdvancedSettings(Boolean(value))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label
|
||||||
|
className="text-muted-foreground ml-2 text-sm"
|
||||||
|
htmlFor="showAdvancedRecipientSettings"
|
||||||
|
>
|
||||||
|
Show advanced settings
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Form>
|
</Form>
|
||||||
</AnimateGenericFadeInOut>
|
</AnimateGenericFadeInOut>
|
||||||
</DocumentFlowFormContainerContent>
|
</DocumentFlowFormContainerContent>
|
||||||
|
|||||||
@ -4,7 +4,8 @@ import React, { useId, useState } from 'react';
|
|||||||
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { Plus, Trash } from 'lucide-react';
|
import { InfoIcon, Plus, Trash } from 'lucide-react';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
import { Controller, useFieldArray, useForm } from 'react-hook-form';
|
||||||
|
|
||||||
import { nanoid } from '@documenso/lib/universal/id';
|
import { nanoid } from '@documenso/lib/universal/id';
|
||||||
@ -24,6 +25,7 @@ import type { DocumentFlowStep } from '../document-flow/types';
|
|||||||
import { ROLE_ICONS } from '../recipient-role-icons';
|
import { ROLE_ICONS } from '../recipient-role-icons';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger } from '../select';
|
import { Select, SelectContent, SelectItem, SelectTrigger } from '../select';
|
||||||
import { useStep } from '../stepper';
|
import { useStep } from '../stepper';
|
||||||
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
|
||||||
import type { TAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types';
|
import type { TAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types';
|
||||||
import { ZAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types';
|
import { ZAddTemplatePlacholderRecipientsFormSchema } from './add-template-placeholder-recipients.types';
|
||||||
|
|
||||||
@ -41,6 +43,8 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
}: AddTemplatePlaceholderRecipientsFormProps) => {
|
}: AddTemplatePlaceholderRecipientsFormProps) => {
|
||||||
const initialId = useId();
|
const initialId = useId();
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const user = session?.user;
|
||||||
const [placeholderRecipientCount, setPlaceholderRecipientCount] = useState(() =>
|
const [placeholderRecipientCount, setPlaceholderRecipientCount] = useState(() =>
|
||||||
recipients.length > 1 ? recipients.length + 1 : 2,
|
recipients.length > 1 ? recipients.length + 1 : 2,
|
||||||
);
|
);
|
||||||
@ -50,6 +54,7 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
|||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
getValues,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm<TAddTemplatePlacholderRecipientsFormSchema>({
|
} = useForm<TAddTemplatePlacholderRecipientsFormSchema>({
|
||||||
resolver: zodResolver(ZAddTemplatePlacholderRecipientsFormSchema),
|
resolver: zodResolver(ZAddTemplatePlacholderRecipientsFormSchema),
|
||||||
@ -86,6 +91,15 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
|||||||
name: 'signers',
|
name: 'signers',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onAddPlaceholderSelfRecipient = () => {
|
||||||
|
appendSigner({
|
||||||
|
formId: nanoid(12),
|
||||||
|
name: user?.name ?? '',
|
||||||
|
email: user?.email ?? '',
|
||||||
|
role: RecipientRole.SIGNER,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const onAddPlaceholderRecipient = () => {
|
const onAddPlaceholderRecipient = () => {
|
||||||
appendSigner({
|
appendSigner({
|
||||||
formId: nanoid(12),
|
formId: nanoid(12),
|
||||||
@ -155,29 +169,81 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
|||||||
<SelectContent className="" align="end">
|
<SelectContent className="" align="end">
|
||||||
<SelectItem value={RecipientRole.SIGNER}>
|
<SelectItem value={RecipientRole.SIGNER}>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.SIGNER]}</span>
|
<div className="flex w-[150px] items-center">
|
||||||
Signer
|
<span className="mr-2">{ROLE_ICONS[RecipientRole.SIGNER]}</span>
|
||||||
</div>
|
Needs to sign
|
||||||
</SelectItem>
|
</div>
|
||||||
|
<Tooltip>
|
||||||
<SelectItem value={RecipientRole.CC}>
|
<TooltipTrigger>
|
||||||
<div className="flex items-center">
|
<InfoIcon className="h-4 w-4" />
|
||||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.CC]}</span>
|
</TooltipTrigger>
|
||||||
Receives copy
|
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
|
||||||
|
<p>
|
||||||
|
The recipient is required to sign the document for it to be
|
||||||
|
completed.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
||||||
<SelectItem value={RecipientRole.APPROVER}>
|
<SelectItem value={RecipientRole.APPROVER}>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.APPROVER]}</span>
|
<div className="flex w-[150px] items-center">
|
||||||
Approver
|
<span className="mr-2">{ROLE_ICONS[RecipientRole.APPROVER]}</span>
|
||||||
|
Needs to approve
|
||||||
|
</div>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
|
||||||
|
<p>
|
||||||
|
The recipient is required to approve the document for it to be
|
||||||
|
completed.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
||||||
<SelectItem value={RecipientRole.VIEWER}>
|
<SelectItem value={RecipientRole.VIEWER}>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="mr-2">{ROLE_ICONS[RecipientRole.VIEWER]}</span>
|
<div className="flex w-[150px] items-center">
|
||||||
Viewer
|
<span className="mr-2">{ROLE_ICONS[RecipientRole.VIEWER]}</span>
|
||||||
|
Needs to view
|
||||||
|
</div>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
|
||||||
|
<p>
|
||||||
|
The recipient is required to view the document for it to be
|
||||||
|
completed.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
|
||||||
|
<SelectItem value={RecipientRole.CC}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="flex w-[150px] items-center">
|
||||||
|
<span className="mr-2">{ROLE_ICONS[RecipientRole.CC]}</span>
|
||||||
|
Receives copy
|
||||||
|
</div>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<InfoIcon className="h-4 w-4" />
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="text-foreground z-9999 max-w-md p-4">
|
||||||
|
<p>
|
||||||
|
The recipient is not required to take any action and receives a
|
||||||
|
copy of the document after it is completed.
|
||||||
|
</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@ -212,11 +278,27 @@ export const AddTemplatePlaceholderRecipientsFormPartial = ({
|
|||||||
error={'signers__root' in errors && errors['signers__root']}
|
error={'signers__root' in errors && errors['signers__root']}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4 flex flex-row items-center space-x-4">
|
||||||
<Button type="button" disabled={isSubmitting} onClick={() => onAddPlaceholderRecipient()}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="flex-1"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
onClick={() => onAddPlaceholderRecipient()}
|
||||||
|
>
|
||||||
<Plus className="-ml-1 mr-2 h-5 w-5" />
|
<Plus className="-ml-1 mr-2 h-5 w-5" />
|
||||||
Add Placeholder Recipient
|
Add Placeholder Recipient
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="dark:bg-muted dark:hover:bg-muted/80 bg-black/5 hover:bg-black/10"
|
||||||
|
disabled={
|
||||||
|
isSubmitting || getValues('signers').some((signer) => signer.email === user?.email)
|
||||||
|
}
|
||||||
|
onClick={() => onAddPlaceholderSelfRecipient()}
|
||||||
|
>
|
||||||
|
<Plus className="-ml-1 mr-2 h-5 w-5" />
|
||||||
|
Add Myself
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DocumentFlowFormContainerContent>
|
</DocumentFlowFormContainerContent>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user