From 4ff8592e8fc156a51ec0d56b69dd6c78dc5f5e8e Mon Sep 17 00:00:00 2001 From: nafees nazik Date: Wed, 29 Nov 2023 22:11:55 +0530 Subject: [PATCH 01/53] feat: add password input component --- packages/ui/primitives/password-input.tsx | 43 +++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 packages/ui/primitives/password-input.tsx diff --git a/packages/ui/primitives/password-input.tsx b/packages/ui/primitives/password-input.tsx new file mode 100644 index 000000000..85249056d --- /dev/null +++ b/packages/ui/primitives/password-input.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; + +import { Eye, EyeOff } from 'lucide-react'; + +import { cn } from '../lib/utils'; +import { Button } from './button'; +import { Input, InputProps } from './input'; + +const PasswordInput = React.forwardRef( + ({ className, ...props }, ref) => { + const [showPassword, setShowPassword] = React.useState(false); + + return ( +
+ + + +
+ ); + }, +); + +PasswordInput.displayName = 'PasswordInput'; + +export { PasswordInput }; From 318dfcafc3a9fa1b8a0db923ea8c403813df55d6 Mon Sep 17 00:00:00 2001 From: nafees nazik Date: Wed, 29 Nov 2023 22:31:24 +0530 Subject: [PATCH 02/53] refactor: signup page --- apps/web/src/components/forms/signup.tsx | 189 +++++++++++------------ 1 file changed, 89 insertions(+), 100 deletions(-) diff --git a/apps/web/src/components/forms/signup.tsx b/apps/web/src/components/forms/signup.tsx index 11068ac68..fb70e4c2f 100644 --- a/apps/web/src/components/forms/signup.tsx +++ b/apps/web/src/components/forms/signup.tsx @@ -1,20 +1,24 @@ 'use client'; -import { useState } from 'react'; - import { zodResolver } from '@hookform/resolvers/zod'; -import { Eye, EyeOff } from 'lucide-react'; import { signIn } from 'next-auth/react'; -import { Controller, useForm } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { TRPCClientError } from '@documenso/trpc/client'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; -import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; -import { Label } from '@documenso/ui/primitives/label'; +import { PasswordInput } from '@documenso/ui/primitives/password-input'; import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; import { useToast } from '@documenso/ui/primitives/use-toast'; @@ -36,14 +40,8 @@ export type SignUpFormProps = { export const SignUpForm = ({ className }: SignUpFormProps) => { const { toast } = useToast(); - const [showPassword, setShowPassword] = useState(false); - const { - control, - register, - handleSubmit, - formState: { errors, isSubmitting }, - } = useForm({ + const form = useForm({ values: { name: '', email: '', @@ -53,6 +51,8 @@ export const SignUpForm = ({ className }: SignUpFormProps) => { resolver: zodResolver(ZSignUpFormSchema), }); + const isSubmitting = form.formState.isSubmitting; + const { mutateAsync: signup } = trpc.auth.signup.useMutation(); const onFormSubmit = async ({ name, email, password, signature }: TSignUpFormSchema) => { @@ -83,93 +83,82 @@ export const SignUpForm = ({ className }: SignUpFormProps) => { }; return ( -
-
- - - - - {errors.name && {errors.name.message}} -
- -
- - - - - {errors.email && {errors.email.message}} -
- -
- - -
- - - -
- -
- -
- - -
- ( - onChange(v ?? '')} - /> - )} - /> -
- - -
- - -
+ ( + + Name + + + + + + )} + /> + + ( + + Email + + + + + + )} + /> + + ( + + Password + + + + + + )} + /> + + ( + + Sign Here + + onChange(v ?? '')} + /> + + + + + )} + /> + + + + ); }; From 62809e9506f0046fffe713968ebdb2f426396341 Mon Sep 17 00:00:00 2001 From: nafees nazik Date: Wed, 29 Nov 2023 22:31:42 +0530 Subject: [PATCH 03/53] refactor: signin page --- apps/web/src/components/forms/signin.tsx | 150 +++++++++++------------ 1 file changed, 69 insertions(+), 81 deletions(-) diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx index abdc1efe6..b93a2cd46 100644 --- a/apps/web/src/components/forms/signin.tsx +++ b/apps/web/src/components/forms/signin.tsx @@ -1,9 +1,6 @@ 'use client'; -import { useState } from 'react'; - import { zodResolver } from '@hookform/resolvers/zod'; -import { Eye, EyeOff } from 'lucide-react'; import { signIn } from 'next-auth/react'; import { useForm } from 'react-hook-form'; import { FcGoogle } from 'react-icons/fc'; @@ -12,9 +9,16 @@ import { z } from 'zod'; import { ErrorCode, isErrorCode } from '@documenso/lib/next-auth/error-codes'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; -import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; -import { Label } from '@documenso/ui/primitives/label'; +import { PasswordInput } from '@documenso/ui/primitives/password-input'; import { useToast } from '@documenso/ui/primitives/use-toast'; const ERROR_MESSAGES = { @@ -39,13 +43,8 @@ export type SignInFormProps = { export const SignInForm = ({ className }: SignInFormProps) => { const { toast } = useToast(); - const [showPassword, setShowPassword] = useState(false); - const { - register, - handleSubmit, - formState: { errors, isSubmitting }, - } = useForm({ + const form = useForm({ values: { email: '', password: '', @@ -53,6 +52,8 @@ export const SignInForm = ({ className }: SignInFormProps) => { resolver: zodResolver(ZSignInFormSchema), }); + const isSubmitting = form.formState.isSubmitting; + const onFormSubmit = async ({ email, password }: TSignInFormSchema) => { try { const result = await signIn('credentials', { @@ -99,80 +100,67 @@ export const SignInForm = ({ className }: SignInFormProps) => { }; return ( -
-
- + + + ( + + Email + + + + + + )} + /> - + ( + + Password + + + + + + )} + /> - -
+ -
- - -
- - - +
+
+ Or continue with +
- -
- - - -
-
- Or continue with -
-
- - - + + + ); }; From dc56c2abf2dba0a85b9dd43b2aaa22e159c0546c Mon Sep 17 00:00:00 2001 From: nafees nazik Date: Wed, 29 Nov 2023 22:32:42 +0530 Subject: [PATCH 04/53] refactor: password form --- apps/web/src/components/forms/password.tsx | 191 +++++++-------------- 1 file changed, 65 insertions(+), 126 deletions(-) diff --git a/apps/web/src/components/forms/password.tsx b/apps/web/src/components/forms/password.tsx index 47cba1e88..c262719e3 100644 --- a/apps/web/src/components/forms/password.tsx +++ b/apps/web/src/components/forms/password.tsx @@ -1,9 +1,7 @@ 'use client'; -import { useState } from 'react'; - import { zodResolver } from '@hookform/resolvers/zod'; -import { Eye, EyeOff, Loader } from 'lucide-react'; +import { Loader } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -12,12 +10,17 @@ import { TRPCClientError } from '@documenso/trpc/client'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; -import { Input } from '@documenso/ui/primitives/input'; -import { Label } from '@documenso/ui/primitives/label'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { PasswordInput } from '@documenso/ui/primitives/password-input'; import { useToast } from '@documenso/ui/primitives/use-toast'; -import { FormErrorMessage } from '../form/form-error-message'; - export const ZPasswordFormSchema = z .object({ currentPassword: z @@ -48,16 +51,7 @@ export type PasswordFormProps = { export const PasswordForm = ({ className }: PasswordFormProps) => { const { toast } = useToast(); - const [showPassword, setShowPassword] = useState(false); - const [showConfirmPassword, setShowConfirmPassword] = useState(false); - const [showCurrentPassword, setShowCurrentPassword] = useState(false); - - const { - register, - handleSubmit, - reset, - formState: { errors, isSubmitting }, - } = useForm({ + const form = useForm({ values: { currentPassword: '', password: '', @@ -66,6 +60,8 @@ export const PasswordForm = ({ className }: PasswordFormProps) => { resolver: zodResolver(ZPasswordFormSchema), }); + const isSubmitting = form.formState.isSubmitting; + const { mutateAsync: updatePassword } = trpc.profile.updatePassword.useMutation(); const onFormSubmit = async ({ currentPassword, password }: TPasswordFormSchema) => { @@ -75,7 +71,7 @@ export const PasswordForm = ({ className }: PasswordFormProps) => { password, }); - reset(); + form.reset(); toast({ title: 'Password updated', @@ -101,117 +97,60 @@ export const PasswordForm = ({ className }: PasswordFormProps) => { }; return ( -
-
- + + + ( + + Current Password + + + + + + )} + /> -
- + ( + + Password + + + + + + )} + /> -
- - -
-
- - -
- - - -
- - -
- -
- - -
- - - -
- - -
- -
- -
-
+ + ); }; From 1e29dfd823755ecb241057d61be5e1836e3386ec Mon Sep 17 00:00:00 2001 From: nafees nazik Date: Wed, 29 Nov 2023 22:33:04 +0530 Subject: [PATCH 05/53] refactor: reset password form --- .../src/components/forms/reset-password.tsx | 140 ++++++------------ 1 file changed, 49 insertions(+), 91 deletions(-) diff --git a/apps/web/src/components/forms/reset-password.tsx b/apps/web/src/components/forms/reset-password.tsx index 47f423d76..e29686999 100644 --- a/apps/web/src/components/forms/reset-password.tsx +++ b/apps/web/src/components/forms/reset-password.tsx @@ -1,11 +1,8 @@ 'use client'; -import { useState } from 'react'; - import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; -import { Eye, EyeOff } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -13,9 +10,15 @@ import { TRPCClientError } from '@documenso/trpc/client'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; -import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message'; -import { Input } from '@documenso/ui/primitives/input'; -import { Label } from '@documenso/ui/primitives/label'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { PasswordInput } from '@documenso/ui/primitives/password-input'; import { useToast } from '@documenso/ui/primitives/use-toast'; export const ZResetPasswordFormSchema = z @@ -40,15 +43,7 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps) const { toast } = useToast(); - const [showPassword, setShowPassword] = useState(false); - const [showConfirmPassword, setShowConfirmPassword] = useState(false); - - const { - register, - reset, - handleSubmit, - formState: { errors, isSubmitting }, - } = useForm({ + const form = useForm({ values: { password: '', repeatedPassword: '', @@ -56,6 +51,8 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps) resolver: zodResolver(ZResetPasswordFormSchema), }); + const isSubmitting = form.formState.isSubmitting; + const { mutateAsync: resetPassword } = trpc.profile.resetPassword.useMutation(); const onFormSubmit = async ({ password }: Omit) => { @@ -65,7 +62,7 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps) token, }); - reset(); + form.reset(); toast({ title: 'Password updated', @@ -93,81 +90,42 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps) }; return ( -
-
- + + + ( + + Password + + + + + + )} + /> + ( + + Repeat Password + + + + + + )} + /> -
- - - -
- - -
- -
- - -
- - - -
- - -
- - -
+ + + ); }; From 0b2dce22389b796d7ce9155c04a9edab34946e39 Mon Sep 17 00:00:00 2001 From: nafees nazik Date: Wed, 29 Nov 2023 22:37:33 +0530 Subject: [PATCH 06/53] fix: type --- packages/ui/primitives/password-input.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ui/primitives/password-input.tsx b/packages/ui/primitives/password-input.tsx index 85249056d..502344a02 100644 --- a/packages/ui/primitives/password-input.tsx +++ b/packages/ui/primitives/password-input.tsx @@ -6,14 +6,13 @@ import { cn } from '../lib/utils'; import { Button } from './button'; import { Input, InputProps } from './input'; -const PasswordInput = React.forwardRef( +const PasswordInput = React.forwardRef>( ({ className, ...props }, ref) => { const [showPassword, setShowPassword] = React.useState(false); return (
Date: Thu, 30 Nov 2023 15:20:06 +0530 Subject: [PATCH 07/53] feat: add loading text prop --- packages/ui/primitives/button.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/ui/primitives/button.tsx b/packages/ui/primitives/button.tsx index 31df69dee..9ee3324c6 100644 --- a/packages/ui/primitives/button.tsx +++ b/packages/ui/primitives/button.tsx @@ -53,18 +53,23 @@ export interface ButtonProps * Will display the loading spinner and disable the button. */ loading?: boolean; + + /** + * The label to show in the button when `isLoading` is true + */ + loadingText?: string; } const Button = React.forwardRef( - ({ className, variant, size, asChild = false, loading, ...props }, ref) => { + ({ className, variant, size, asChild = false, loadingText, loading, ...props }, ref) => { if (asChild) { return ( ); } - const showLoader = loading === true; - const isDisabled = props.disabled || showLoader; + const isLoading = loading === true; + const isDisabled = props.disabled || isLoading; return ( ); }, From 6bbeaa084ce464b64174e47d940c843724a9af99 Mon Sep 17 00:00:00 2001 From: nafees nazik Date: Thu, 30 Nov 2023 15:55:29 +0530 Subject: [PATCH 08/53] refactor: forms --- .../src/components/forms/forgot-password.tsx | 63 ++++++---- apps/web/src/components/forms/password.tsx | 84 ++++++------- apps/web/src/components/forms/profile.tsx | 115 ++++++++--------- .../src/components/forms/reset-password.tsx | 59 ++++----- apps/web/src/components/forms/signin.tsx | 58 ++++----- apps/web/src/components/forms/signup.tsx | 118 +++++++++--------- 6 files changed, 259 insertions(+), 238 deletions(-) diff --git a/apps/web/src/components/forms/forgot-password.tsx b/apps/web/src/components/forms/forgot-password.tsx index 141f3780f..55e313c33 100644 --- a/apps/web/src/components/forms/forgot-password.tsx +++ b/apps/web/src/components/forms/forgot-password.tsx @@ -9,9 +9,15 @@ import { z } from 'zod'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; -import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; -import { Label } from '@documenso/ui/primitives/label'; import { useToast } from '@documenso/ui/primitives/use-toast'; export const ZForgotPasswordFormSchema = z.object({ @@ -28,18 +34,15 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => { const router = useRouter(); const { toast } = useToast(); - const { - register, - handleSubmit, - reset, - formState: { errors, isSubmitting }, - } = useForm({ + const form = useForm({ values: { email: '', }, resolver: zodResolver(ZForgotPasswordFormSchema), }); + const isSubmitting = form.formState.isSubmitting; + const { mutateAsync: forgotPassword } = trpc.profile.forgotPassword.useMutation(); const onFormSubmit = async ({ email }: TForgotPasswordFormSchema) => { @@ -52,29 +55,37 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => { duration: 5000, }); - reset(); + form.reset(); router.push('/check-email'); }; return ( -
-
- + + +
+ ( + + Email + + + + + + )} + /> +
- - - -
- - -
+ + + ); }; diff --git a/apps/web/src/components/forms/password.tsx b/apps/web/src/components/forms/password.tsx index c262719e3..b6b41264c 100644 --- a/apps/web/src/components/forms/password.tsx +++ b/apps/web/src/components/forms/password.tsx @@ -1,7 +1,6 @@ 'use client'; import { zodResolver } from '@hookform/resolvers/zod'; -import { Loader } from 'lucide-react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; @@ -102,51 +101,52 @@ export const PasswordForm = ({ className }: PasswordFormProps) => { className={cn('flex w-full flex-col gap-y-4', className)} onSubmit={form.handleSubmit(onFormSubmit)} > - ( - - Current Password - - - - - - )} - /> +
+ ( + + Current Password + + + + + + )} + /> - ( - - Password - - - - - - )} - /> + ( + + Password + + + + + + )} + /> - ( - - Repeat Password - - - - - - )} - /> + ( + + Repeat Password + + + + + + )} + /> +
-
diff --git a/apps/web/src/components/forms/profile.tsx b/apps/web/src/components/forms/profile.tsx index 6f611bed9..dc6e3c4d5 100644 --- a/apps/web/src/components/forms/profile.tsx +++ b/apps/web/src/components/forms/profile.tsx @@ -3,8 +3,7 @@ import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; -import { Loader } from 'lucide-react'; -import { Controller, useForm } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { User } from '@documenso/prisma/client'; @@ -12,13 +11,19 @@ import { TRPCClientError } from '@documenso/trpc/client'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; import { Button } from '@documenso/ui/primitives/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; import { Label } from '@documenso/ui/primitives/label'; import { SignaturePad } from '@documenso/ui/primitives/signature-pad'; import { useToast } from '@documenso/ui/primitives/use-toast'; -import { FormErrorMessage } from '../form/form-error-message'; - export const ZProfileFormSchema = z.object({ name: z.string().trim().min(1, { message: 'Please enter a valid name.' }), signature: z.string().min(1, 'Signature Pad cannot be empty'), @@ -36,12 +41,7 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => { const { toast } = useToast(); - const { - register, - control, - handleSubmit, - formState: { errors, isSubmitting }, - } = useForm({ + const form = useForm({ values: { name: user.name ?? '', signature: user.signature || '', @@ -49,6 +49,8 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => { resolver: zodResolver(ZProfileFormSchema), }); + const isSubmitting = form.formState.isSubmitting; + const { mutateAsync: updateProfile } = trpc.profile.updateProfile.useMutation(); const onFormSubmit = async ({ name, signature }: TProfileFormSchema) => { @@ -84,56 +86,57 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => { }; return ( -
-
- - - - - -
- -
- - - -
- -
- - -
- ( - onChange(v ?? '')} - /> + + +
+ ( + + Full Name + + + + + )} /> - -
-
-
- -
-
+ + ); }; diff --git a/apps/web/src/components/forms/reset-password.tsx b/apps/web/src/components/forms/reset-password.tsx index e29686999..e7c701667 100644 --- a/apps/web/src/components/forms/reset-password.tsx +++ b/apps/web/src/components/forms/reset-password.tsx @@ -95,35 +95,38 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps) className={cn('flex w-full flex-col gap-y-4', className)} onSubmit={form.handleSubmit(onFormSubmit)} > - ( - - Password - - - - - - )} - /> - ( - - Repeat Password - - - - - - )} - /> +
+ ( + + Password + + + + + + )} + /> -
+ + diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx index b93a2cd46..bc58e68e1 100644 --- a/apps/web/src/components/forms/signin.tsx +++ b/apps/web/src/components/forms/signin.tsx @@ -105,42 +105,44 @@ export const SignInForm = ({ className }: SignInFormProps) => { className={cn('flex w-full flex-col gap-y-4', className)} onSubmit={form.handleSubmit(onFormSubmit)} > - ( - - Email - - - - - - )} - /> +
+ ( + + Email + + + + + + )} + /> - ( - - Password - - - - - - )} - /> + ( + + Password + + + + + + )} + /> +
diff --git a/apps/web/src/components/forms/signup.tsx b/apps/web/src/components/forms/signup.tsx index fb70e4c2f..ad803d9c1 100644 --- a/apps/web/src/components/forms/signup.tsx +++ b/apps/web/src/components/forms/signup.tsx @@ -88,75 +88,77 @@ export const SignUpForm = ({ className }: SignUpFormProps) => { className={cn('flex w-full flex-col gap-y-4', className)} onSubmit={form.handleSubmit(onFormSubmit)} > - ( - - Name - - - - - - )} - /> +
+ ( + + Name + + + + + + )} + /> - ( - - Email - - - - - - )} - /> + ( + + Email + + + + + + )} + /> - ( - - Password - - - - - - )} - /> + ( + + Password + + + + + + )} + /> - ( - - Sign Here - - onChange(v ?? '')} - /> - + ( + + Sign Here + + onChange(v ?? '')} + /> + - - - )} - /> + + + )} + /> +
From 4733f1e84bdb5e625a91627c7d72312e668a0a2a Mon Sep 17 00:00:00 2001 From: nafees nazik Date: Sat, 2 Dec 2023 17:46:16 +0530 Subject: [PATCH 09/53] refactor: password input component --- packages/ui/primitives/input.tsx | 39 +------------------------------- 1 file changed, 1 insertion(+), 38 deletions(-) diff --git a/packages/ui/primitives/input.tsx b/packages/ui/primitives/input.tsx index ac739c984..1a5fba1bb 100644 --- a/packages/ui/primitives/input.tsx +++ b/packages/ui/primitives/input.tsx @@ -1,9 +1,6 @@ import * as React from 'react'; -import { Eye, EyeOff } from 'lucide-react'; - import { cn } from '../lib/utils'; -import { Button } from './button'; export type InputProps = React.InputHTMLAttributes; @@ -28,38 +25,4 @@ const Input = React.forwardRef( Input.displayName = 'Input'; -const PasswordInput = React.forwardRef( - ({ className, ...props }, ref) => { - const [showPassword, setShowPassword] = React.useState(false); - - return ( -
- - - -
- ); - }, -); - -PasswordInput.displayName = 'Input'; - -export { Input, PasswordInput }; +export { Input }; From a9068336571c8ba6cdb22e3802ab2692e96c2ad6 Mon Sep 17 00:00:00 2001 From: nafees nazik Date: Sat, 2 Dec 2023 17:54:19 +0530 Subject: [PATCH 10/53] feat: use password input component --- .../2fa/disable-authenticator-app-dialog.tsx | 67 ++++++++++--------- .../2fa/enable-authenticator-app-dialog.tsx | 4 +- .../forms/2fa/view-recovery-codes-dialog.tsx | 5 +- 3 files changed, 40 insertions(+), 36 deletions(-) diff --git a/apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx b/apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx index eac574181..eafed5500 100644 --- a/apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx +++ b/apps/web/src/components/forms/2fa/disable-authenticator-app-dialog.tsx @@ -23,6 +23,7 @@ import { FormMessage, } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; +import { PasswordInput } from '@documenso/ui/primitives/password-input'; import { useToast } from '@documenso/ui/primitives/use-toast'; export const ZDisableTwoFactorAuthenticationForm = z.object({ @@ -107,38 +108,42 @@ export const DisableAuthenticatorAppDialog = ({ )} className="flex flex-col gap-y-4" > - ( - - Password - - - - - - )} - /> +
+ ( + + Password + + + + + + )} + /> - ( - - Backup Code - - - - - - )} - /> + ( + + Backup Code + + + + + + )} + /> +
+
+ ( + + Email + + + + + + )} + /> -
-
- Or continue with -
-
+ ( + + Password + + + + + + )} + /> +
- + +
+
+ Or continue with +
+
+ + + { Two-Factor Authentication -
- {twoFactorAuthenticationMethod === 'totp' && ( -
- - - +
+ {twoFactorAuthenticationMethod === 'totp' && ( + ( + + Authentication Token + + + + + + )} /> + )} - -
- )} - - {twoFactorAuthenticationMethod === 'backup' && ( -
- - - ( + + Backup Code + + + + + + )} /> - - -
- )} + )} +
-
- + ); }; From 31a9127c9e77295e65edb00858a4effa76a7e030 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Fri, 6 Oct 2023 22:54:24 +0000 Subject: [PATCH 12/53] feat: templates --- .../app/(marketing)/singleplayer/client.tsx | 1 + .../single-player-mode-success.tsx | 5 +- .../documents/data-table-action-dropdown.tsx | 3 +- .../templates/[id]/edit-template.tsx | 152 +++++ .../app/(dashboard)/templates/[id]/page.tsx | 81 +++ .../templates/data-table-action-dropdown.tsx | 79 +++ .../templates/data-table-templates.tsx | 136 +++++ .../templates/data-table-title.tsx | 26 + .../templates/delete-template-dialog.tsx | 84 +++ .../templates/duplicate-template-dialog.tsx | 89 +++ .../app/(dashboard)/templates/empty-state.tsx | 17 + .../templates/new-template-dialog.tsx | 217 +++++++ .../src/app/(dashboard)/templates/page.tsx | 49 ++ .../(dashboard)/layout/desktop-nav.tsx | 43 +- .../(dashboard)/layout/profile-dropdown.tsx | 8 + .../components/formatter/template-type.tsx | 50 ++ .../add-template-fields.action.ts | 32 ++ .../add-template-placeholders.action.ts | 28 + .../server-only/admin/get-recipients-stats.ts | 7 +- .../server-only/document/find-documents.ts | 4 +- .../field/get-fields-for-template.ts | 22 + .../field/remove-signed-field-with-token.ts | 4 + .../field/set-fields-for-template.ts | 116 ++++ .../field/sign-field-with-token.ts | 4 + .../recipient/get-recipients-for-template.ts | 25 + .../recipient/set-recipients-for-template.ts | 98 ++++ .../template/create-document-from-template.ts | 78 +++ .../server-only/template/create-template.ts | 20 + .../server-only/template/delete-template.ts | 12 + .../template/duplicate-template.ts | 75 +++ .../template/get-template-by-id.ts | 18 + .../lib/server-only/template/get-templates.ts | 37 ++ .../20231007013737_templates/migration.sql | 52 ++ .../migration.sql | 15 + .../migration.sql | 14 + .../migration.sql | 8 + .../migration.sql | 8 + .../migration.sql | 9 + .../migration.sql | 51 ++ .../20231017042227_fix_typo/migration.sql | 23 + .../migration.sql | 8 + .../migration.sql | 8 + .../migration.sql | 5 + .../migration.sql | 10 + .../migration.sql | 54 ++ packages/prisma/schema.prisma | 53 +- packages/trpc/server/router.ts | 2 + .../trpc/server/template-router/router.ts | 94 +++ .../trpc/server/template-router/schema.ts | 26 + packages/ui/primitives/document-dropzone.tsx | 13 +- packages/ui/primitives/document-flow/types.ts | 2 +- .../template-flow/add-template-fields.tsx | 539 ++++++++++++++++++ .../add-template-fields.types.ts | 23 + .../add-template-placeholder-recipients.tsx | 205 +++++++ ...d-template-placeholder-recipients.types.ts | 26 + 55 files changed, 2834 insertions(+), 34 deletions(-) create mode 100644 apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx create mode 100644 apps/web/src/app/(dashboard)/templates/[id]/page.tsx create mode 100644 apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx create mode 100644 apps/web/src/app/(dashboard)/templates/data-table-templates.tsx create mode 100644 apps/web/src/app/(dashboard)/templates/data-table-title.tsx create mode 100644 apps/web/src/app/(dashboard)/templates/delete-template-dialog.tsx create mode 100644 apps/web/src/app/(dashboard)/templates/duplicate-template-dialog.tsx create mode 100644 apps/web/src/app/(dashboard)/templates/empty-state.tsx create mode 100644 apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx create mode 100644 apps/web/src/app/(dashboard)/templates/page.tsx create mode 100644 apps/web/src/components/formatter/template-type.tsx create mode 100644 apps/web/src/components/forms/edit-template/add-template-fields.action.ts create mode 100644 apps/web/src/components/forms/edit-template/add-template-placeholders.action.ts create mode 100644 packages/lib/server-only/field/get-fields-for-template.ts create mode 100644 packages/lib/server-only/field/set-fields-for-template.ts create mode 100644 packages/lib/server-only/recipient/get-recipients-for-template.ts create mode 100644 packages/lib/server-only/recipient/set-recipients-for-template.ts create mode 100644 packages/lib/server-only/template/create-document-from-template.ts create mode 100644 packages/lib/server-only/template/create-template.ts create mode 100644 packages/lib/server-only/template/delete-template.ts create mode 100644 packages/lib/server-only/template/duplicate-template.ts create mode 100644 packages/lib/server-only/template/get-template-by-id.ts create mode 100644 packages/lib/server-only/template/get-templates.ts create mode 100644 packages/prisma/migrations/20231007013737_templates/migration.sql create mode 100644 packages/prisma/migrations/20231007014431_templates_type/migration.sql create mode 100644 packages/prisma/migrations/20231007021427_reuse_document_data/migration.sql create mode 100644 packages/prisma/migrations/20231007033447_remove_inserted_on_template_field/migration.sql create mode 100644 packages/prisma/migrations/20231007080315_document_name_to_template/migration.sql create mode 100644 packages/prisma/migrations/20231007211915_template_created_date/migration.sql create mode 100644 packages/prisma/migrations/20231017041643_placeholder_recipients/migration.sql create mode 100644 packages/prisma/migrations/20231017042227_fix_typo/migration.sql create mode 100644 packages/prisma/migrations/20231019043226_template_recipient_email/migration.sql create mode 100644 packages/prisma/migrations/20231020032507_template_recipient_token/migration.sql create mode 100644 packages/prisma/migrations/20231021193915_template_token_for_recipient/migration.sql create mode 100644 packages/prisma/migrations/20231030061522_recipient_name_not_placeholder/migration.sql create mode 100644 packages/prisma/migrations/20231208090322_remove_template_specific_models/migration.sql create mode 100644 packages/trpc/server/template-router/router.ts create mode 100644 packages/trpc/server/template-router/schema.ts create mode 100644 packages/ui/primitives/template-flow/add-template-fields.tsx create mode 100644 packages/ui/primitives/template-flow/add-template-fields.types.ts create mode 100644 packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx create mode 100644 packages/ui/primitives/template-flow/add-template-placeholder-recipients.types.ts diff --git a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx index b7654c7cf..71f6963a2 100644 --- a/apps/marketing/src/app/(marketing)/singleplayer/client.tsx +++ b/apps/marketing/src/app/(marketing)/singleplayer/client.tsx @@ -151,6 +151,7 @@ export const SinglePlayerClient = () => { email: '', name: '', token: '', + templateToken: '', expired: null, signedAt: null, readStatus: 'OPENED', diff --git a/apps/marketing/src/components/(marketing)/single-player-mode/single-player-mode-success.tsx b/apps/marketing/src/components/(marketing)/single-player-mode/single-player-mode-success.tsx index aa423e522..1af71c775 100644 --- a/apps/marketing/src/components/(marketing)/single-player-mode/single-player-mode-success.tsx +++ b/apps/marketing/src/components/(marketing)/single-player-mode/single-player-mode-success.tsx @@ -6,8 +6,9 @@ import Link from 'next/link'; import signingCelebration from '@documenso/assets/images/signing-celebration.png'; import { useFeatureFlags } from '@documenso/lib/client-only/providers/feature-flag'; -import { DocumentStatus, Signature } from '@documenso/prisma/client'; -import { DocumentWithRecipient } from '@documenso/prisma/types/document-with-recipient'; +import type { Signature } from '@documenso/prisma/client'; +import { DocumentStatus } from '@documenso/prisma/client'; +import type { DocumentWithRecipient } from '@documenso/prisma/types/document-with-recipient'; import DocumentDialog from '@documenso/ui/components/document/document-dialog'; import { DocumentDownloadButton } from '@documenso/ui/components/document/document-download-button'; import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button'; diff --git a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx index 9c3532f88..f1cbcc147 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx @@ -99,6 +99,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = }; const nonSignedRecipients = row.Recipient.filter((item) => item.signingStatus !== 'SIGNED'); + return ( @@ -127,7 +128,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = Download - setDuplicateDialogOpen(true)}> + Duplicate diff --git a/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx b/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx new file mode 100644 index 000000000..b4d20b60d --- /dev/null +++ b/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx @@ -0,0 +1,152 @@ +'use client'; + +import { useState } from 'react'; + +import { useRouter } from 'next/navigation'; + +import { DocumentData, Field, Recipient, Template, User } from '@documenso/prisma/client'; +import { cn } from '@documenso/ui/lib/utils'; +import { Card, CardContent } from '@documenso/ui/primitives/card'; +import { + DocumentFlowFormContainer, + DocumentFlowFormContainerHeader, +} from '@documenso/ui/primitives/document-flow/document-flow-root'; +import { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types'; +import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; +import { AddTemplateFieldsFormPartial } from '@documenso/ui/primitives/template-flow/add-template-fields'; +import { TAddTemplateFieldsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-fields.types'; +import { AddTemplatePlaceholderRecipientsFormPartial } from '@documenso/ui/primitives/template-flow/add-template-placeholder-recipients'; +import { TAddTemplatePlacholderRecipientsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-placeholder-recipients.types'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { addTemplateFields } from '~/components/forms/edit-template/add-template-fields.action'; +import { addTemplatePlaceholders } from '~/components/forms/edit-template/add-template-placeholders.action'; + +export type EditTemplateFormProps = { + className?: string; + user: User; + template: Template; + recipients: Recipient[]; + fields: Field[]; + documentData: DocumentData; +}; + +type EditTemplateStep = 'signers' | 'fields'; + +export const EditTemplateForm = ({ + className, + template, + recipients, + fields, + user: _user, + documentData, +}: EditTemplateFormProps) => { + const { toast } = useToast(); + const router = useRouter(); + + const [step, setStep] = useState('signers'); + + const documentFlow: Record = { + signers: { + title: 'Add Placeholders', + description: 'Add all relevant placeholders for each recipient.', + stepIndex: 1, + }, + fields: { + title: 'Add Fields', + description: 'Add all relevant fields for each recipient.', + stepIndex: 2, + onBackStep: () => setStep('signers'), + }, + }; + + const currentDocumentFlow = documentFlow[step]; + + const onAddTemplatePlaceholderFormSubmit = async ( + data: TAddTemplatePlacholderRecipientsFormSchema, + ) => { + try { + await addTemplatePlaceholders({ + templateId: template.id, + signers: data.signers, + }); + + router.refresh(); + + setStep('fields'); + } catch (err) { + toast({ + title: 'Error', + description: 'An error occurred while adding signers.', + variant: 'destructive', + }); + } + }; + + const onAddFieldsFormSubmit = async (data: TAddTemplateFieldsFormSchema) => { + try { + await addTemplateFields({ + templateId: template.id, + fields: data.fields, + }); + + toast({ + title: 'Template saved', + description: 'Your templates has been saved successfully.', + duration: 5000, + }); + + router.push('/templates'); + } catch (err) { + toast({ + title: 'Error', + description: 'An error occurred while adding signers.', + variant: 'destructive', + }); + } + }; + + return ( +
+ + + + + + +
+ e.preventDefault()}> + + + {step === 'signers' && ( + + )} + + {step === 'fields' && ( + + )} + +
+
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/templates/[id]/page.tsx b/apps/web/src/app/(dashboard)/templates/[id]/page.tsx new file mode 100644 index 000000000..b8c645c80 --- /dev/null +++ b/apps/web/src/app/(dashboard)/templates/[id]/page.tsx @@ -0,0 +1,81 @@ +import React from 'react'; + +import Link from 'next/link'; +import { redirect } from 'next/navigation'; + +import { ChevronLeft } from 'lucide-react'; + +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { getFieldsForTemplate } from '@documenso/lib/server-only/field/get-fields-for-template'; +import { getRecipientsForTemplate } from '@documenso/lib/server-only/recipient/get-recipients-for-template'; +import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id'; + +import { TemplateType } from '~/components/formatter/template-type'; + +import { EditTemplateForm } from './edit-template'; + +export type TemplatePageProps = { + params: { + id: string; + }; +}; + +export default async function TemplatePage({ params }: TemplatePageProps) { + const { id } = params; + + const templateId = Number(id); + + if (!templateId || Number.isNaN(templateId)) { + redirect('/documents'); + } + + const { user } = await getRequiredServerComponentSession(); + + const template = await getTemplateById({ + id: templateId, + userId: user.id, + }).catch(() => null); + + if (!template || !template.templateDocumentData) { + redirect('/documents'); + } + + const { templateDocumentData } = template; + + const [templateRecipients, templateFields] = await Promise.all([ + await getRecipientsForTemplate({ + templateId, + userId: user.id, + }), + await getFieldsForTemplate({ + templateId, + userId: user.id, + }), + ]); + + return ( +
+ + + Templates + + +

+ {template.title} +

+ +
+ +
+ + +
+ ); +} diff --git a/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx new file mode 100644 index 000000000..15ad9b58b --- /dev/null +++ b/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx @@ -0,0 +1,79 @@ +'use client'; + +import { useState } from 'react'; + +import Link from 'next/link'; + +import { Copy, Edit, MoreHorizontal, Trash2 } from 'lucide-react'; +import { useSession } from 'next-auth/react'; + +import { Template } from '@documenso/prisma/client'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuTrigger, +} from '@documenso/ui/primitives/dropdown-menu'; + +import { DeleteTemplateDialog } from './delete-template-dialog'; +import { DuplicateTemplateDialog } from './duplicate-template-dialog'; + +export type DataTableActionDropdownProps = { + row: Template; +}; + +export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) => { + const { data: session } = useSession(); + + const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [isDuplicateDialogOpen, setDuplicateDialogOpen] = useState(false); + + if (!session) { + return null; + } + + const isOwner = row.userId === session.user.id; + + return ( + + + + + + + Action + + + + + Edit + + + + {/* onDuplicateButtonClick(row.id)}> */} + setDuplicateDialogOpen(true)}> + + Duplicate + + + setDeleteDialogOpen(true)}> + + Delete + + + + + + + + ); +}; diff --git a/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx b/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx new file mode 100644 index 000000000..3cc8102e7 --- /dev/null +++ b/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx @@ -0,0 +1,136 @@ +'use client'; + +import { useState, useTransition } from 'react'; + +import { useRouter } from 'next/navigation'; + +import { Loader, Plus } from 'lucide-react'; + +import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; +import { Template } from '@documenso/prisma/client'; +import { trpc } from '@documenso/trpc/react'; +import { Button } from '@documenso/ui/primitives/button'; +import { DataTable } from '@documenso/ui/primitives/data-table'; +import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +import { LocaleDate } from '~/components/formatter/locale-date'; +import { TemplateType } from '~/components/formatter/template-type'; + +import { DataTableActionDropdown } from './data-table-action-dropdown'; +import { DataTableTitle } from './data-table-title'; + +type TemplatesDataTableProps = { + templates: Template[]; + perPage: number; + page: number; + totalPages: number; +}; + +export const TemplatesDataTable = ({ + templates, + perPage, + page, + totalPages, +}: TemplatesDataTableProps) => { + const [isPending, startTransition] = useTransition(); + const updateSearchParams = useUpdateSearchParams(); + + const router = useRouter(); + + const { toast } = useToast(); + const [loadingStates, setLoadingStates] = useState<{ [key: string]: boolean }>({}); + + const { mutateAsync: createDocumentFromTemplate } = + trpc.template.createDocumentFromTemplate.useMutation(); + + const onPaginationChange = (page: number, perPage: number) => { + startTransition(() => { + updateSearchParams({ + page, + perPage, + }); + }); + }; + + const onUseButtonClick = async (templateId: number) => { + try { + const { id } = await createDocumentFromTemplate({ + templateId, + }); + toast({ + title: 'Document created', + description: 'Your document has been created from the template successfully.', + duration: 5000, + }); + router.push(`/documents/${id}`); + } catch (err) { + toast({ + title: 'Error', + description: 'An error occurred while creating document from template.', + variant: 'destructive', + }); + } + }; + + return ( +
+ , + }, + { + header: 'Title', + cell: ({ row }) => , + }, + { + header: 'Type', + accessorKey: 'type', + cell: ({ row }) => , + }, + { + header: 'Actions', + accessorKey: 'actions', + cell: ({ row }) => { + const isRowLoading = loadingStates[row.original.id]; + + return ( +
+ + +
+ ); + }, + }, + ]} + data={templates} + perPage={perPage} + currentPage={page} + totalPages={totalPages} + onPaginationChange={onPaginationChange} + > + {(table) => } +
+ + {isPending && ( +
+ +
+ )} +
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/templates/data-table-title.tsx b/apps/web/src/app/(dashboard)/templates/data-table-title.tsx new file mode 100644 index 000000000..31e1011be --- /dev/null +++ b/apps/web/src/app/(dashboard)/templates/data-table-title.tsx @@ -0,0 +1,26 @@ +import Link from 'next/link'; + +import { useSession } from 'next-auth/react'; + +import { Template } from '@documenso/prisma/client'; + +export type DataTableTitleProps = { + row: Template; +}; + +export const DataTableTitle = ({ row }: DataTableTitleProps) => { + const { data: session } = useSession(); + + if (!session) { + return null; + } + + return ( + + {row.title} + + ); +}; diff --git a/apps/web/src/app/(dashboard)/templates/delete-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/delete-template-dialog.tsx new file mode 100644 index 000000000..ed7db1e72 --- /dev/null +++ b/apps/web/src/app/(dashboard)/templates/delete-template-dialog.tsx @@ -0,0 +1,84 @@ +import { useRouter } from 'next/navigation'; + +import { trpc as trpcReact } from '@documenso/trpc/react'; +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'; + +type DeleteTemplateDialogProps = { + id: number; + open: boolean; + onOpenChange: (_open: boolean) => void; +}; + +export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateDialogProps) => { + const router = useRouter(); + + const { toast } = useToast(); + + const { mutateAsync: deleteDocument, isLoading } = trpcReact.template.deleteTemplate.useMutation({ + onSuccess: () => { + router.refresh(); + + toast({ + title: 'Template deleted', + description: 'Your document has been successfully deleted.', + duration: 5000, + }); + + onOpenChange(false); + }, + }); + + const onDraftDelete = async () => { + try { + await deleteDocument({ id }); + } catch { + toast({ + title: 'Something went wrong', + description: 'This template could not be deleted at this time. Please try again.', + variant: 'destructive', + duration: 7500, + }); + } + }; + + return ( + !isLoading && onOpenChange(value)}> + + + Do you want to delete this template? + + + Please note that this action is irreversible. Once confirmed, your template will be + permanently deleted. + + + + +
+ + + +
+
+
+
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/templates/duplicate-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/duplicate-template-dialog.tsx new file mode 100644 index 000000000..5c3118035 --- /dev/null +++ b/apps/web/src/app/(dashboard)/templates/duplicate-template-dialog.tsx @@ -0,0 +1,89 @@ +import { useRouter } from 'next/navigation'; + +import { trpc as trpcReact } from '@documenso/trpc/react'; +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'; + +type DuplicateTemplateDialogProps = { + id: number; + open: boolean; + onOpenChange: (_open: boolean) => void; +}; + +export const DuplicateTemplateDialog = ({ + id, + open, + onOpenChange, +}: DuplicateTemplateDialogProps) => { + const router = useRouter(); + + const { toast } = useToast(); + + const { mutateAsync: duplicateTemplate, isLoading } = + trpcReact.template.duplicateTemplate.useMutation({ + onSuccess: () => { + router.refresh(); + + toast({ + title: 'Template duplicated', + description: 'Your template has been duplicated successfully.', + duration: 5000, + }); + + onOpenChange(false); + }, + }); + + const onDuplicate = async () => { + try { + await duplicateTemplate({ + templateId: id, + }); + + router.refresh(); + } catch (err) { + toast({ + title: 'Error', + description: 'An error occurred while duplicating template.', + variant: 'destructive', + }); + } + }; + + return ( + !isLoading && onOpenChange(value)}> + + + Do you want to duplicate this template? + + Your template will be duplicated. + + + +
+ + + +
+
+
+
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/templates/empty-state.tsx b/apps/web/src/app/(dashboard)/templates/empty-state.tsx new file mode 100644 index 000000000..b928d8a83 --- /dev/null +++ b/apps/web/src/app/(dashboard)/templates/empty-state.tsx @@ -0,0 +1,17 @@ +import { Bird } from 'lucide-react'; + +export const EmptyTemplateState = () => { + return ( +
+ + +
+

We're all empty

+ +

+ You have not yet created any templates. To create a template please upload one. +

+
+
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx new file mode 100644 index 000000000..7de1355a7 --- /dev/null +++ b/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx @@ -0,0 +1,217 @@ +'use client'; + +import React, { useState } from 'react'; + +import { useRouter } from 'next/navigation'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { FilePlus, X } from 'lucide-react'; +import { useForm } from 'react-hook-form'; +import * as z from 'zod'; + +import { createDocumentData } from '@documenso/lib/server-only/document-data/create-document-data'; +import { base64 } from '@documenso/lib/universal/base64'; +import { putFile } from '@documenso/lib/universal/upload/put-file'; +import { trpc } from '@documenso/trpc/react'; +import { Button } from '@documenso/ui/primitives/button'; +import { Card, CardContent } from '@documenso/ui/primitives/card'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@documenso/ui/primitives/dialog'; +import { DocumentDropzone } from '@documenso/ui/primitives/document-dropzone'; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@documenso/ui/primitives/form/form'; +import { Input } from '@documenso/ui/primitives/input'; +import { Label } from '@documenso/ui/primitives/label'; +import { useToast } from '@documenso/ui/primitives/use-toast'; + +const ZCreateTemplateFormSchema = z.object({ + name: z.string(), +}); + +type TCreateTemplateFormSchema = z.infer; + +export const NewTemplateDialog = () => { + const router = useRouter(); + const { toast } = useToast(); + const form = useForm({ + resolver: zodResolver(ZCreateTemplateFormSchema), + defaultValues: { + name: '', + }, + }); + + const { mutateAsync: createTemplate, isLoading: isCreatingTemplate } = + trpc.template.createTemplate.useMutation(); + + const [showNewTemplateDialog, setShowNewTemplateDialog] = useState(false); + const [uploadedFile, setUploadedFile] = useState<{ file: File; fileBase64: string } | null>(); + + const onFileDrop = async (file: File) => { + try { + const arrayBuffer = await file.arrayBuffer(); + const base64String = base64.encode(new Uint8Array(arrayBuffer)); + + setUploadedFile({ + file, + fileBase64: `data:application/pdf;base64,${base64String}`, + }); + + if (!form.getValues('name')) { + form.setValue('name', file.name); + } + } catch { + toast({ + title: 'Something went wrong', + description: 'Please try again later.', + variant: 'destructive', + }); + } + }; + + const onSubmit = async (values: TCreateTemplateFormSchema) => { + if (!uploadedFile) { + return; + } + + const file: File = uploadedFile.file; + + try { + const { type, data } = await putFile(file); + + const { id: templateDocumentDataId } = await createDocumentData({ + type, + data, + }); + + const { id } = await createTemplate({ + title: values.name ? values.name : file.name, + templateDocumentDataId, + }); + + toast({ + title: 'Template document uploaded', + description: + 'Your document has been uploaded successfully. You will be redirected to the template page.', + duration: 5000, + }); + + setShowNewTemplateDialog(false); + + void router.push(`/templates/${id}`); + } catch { + toast({ + title: 'Something went wrong', + description: 'Please try again later.', + variant: 'destructive', + }); + } + }; + + const resetForm = () => { + if (form.getValues('name') === uploadedFile?.file.name) { + form.reset(); + } + + setUploadedFile(null); + }; + + return ( + + + + + + + New Template + + +
+ + ( + + Name your template + + + + + + Leave this empty if you would like to use your document's name for the + template + + + + + )} + /> + +
+ +
+ {uploadedFile ? ( + + +
resetForm()} + className="absolute right-2 top-2 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none" + > + + Remove Template +
+ +
+
+
+
+
+

+ Uploaded Document +

+ + + {uploadedFile.file.name} + + + + ) : ( + + )} +
+
+ +
+ +
+ + + + +
+ ); +}; diff --git a/apps/web/src/app/(dashboard)/templates/page.tsx b/apps/web/src/app/(dashboard)/templates/page.tsx new file mode 100644 index 000000000..bc6a90b12 --- /dev/null +++ b/apps/web/src/app/(dashboard)/templates/page.tsx @@ -0,0 +1,49 @@ +import React from 'react'; + +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { getTemplates } from '@documenso/lib/server-only/template/get-templates'; + +import { TemplatesDataTable } from './data-table-templates'; +import { EmptyTemplateState } from './empty-state'; +import { NewTemplateDialog } from './new-template-dialog'; + +type TemplatesPageProps = { + searchParams?: { + page?: number; + perPage?: number; + }; +}; + +export default async function TemplatesPage({ searchParams = {} }: TemplatesPageProps) { + const { user } = await getRequiredServerComponentSession(); + const page = Number(searchParams.page) || 1; + const perPage = Number(searchParams.perPage) || 10; + + const { templates, totalPages } = await getTemplates({ + userId: user.id, + page: page, + perPage: perPage, + }); + + return ( +
+
+

Templates

+ +
+ +
+ {templates.length > 0 ? ( + + ) : ( + + )} +
+
+ ); +} diff --git a/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx b/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx index 76077cb04..bb3384d0a 100644 --- a/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx +++ b/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx @@ -3,6 +3,9 @@ import type { HTMLAttributes } from 'react'; import { useEffect, useState } from 'react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + import { Search } from 'lucide-react'; import { cn } from '@documenso/ui/lib/utils'; @@ -10,10 +13,22 @@ import { Button } from '@documenso/ui/primitives/button'; import { CommandMenu } from '../common/command-menu'; +const navigationLinks = [ + { + href: '/documents', + label: 'Documents', + }, + { + href: '/templates', + label: 'Templates', + }, +]; + export type DesktopNavProps = HTMLAttributes; export const DesktopNav = ({ className, ...props }: DesktopNavProps) => { - // const pathname = usePathname(); + const pathname = usePathname(); + const [open, setOpen] = useState(false); const [modifierKey, setModifierKey] = useState(() => 'Ctrl'); @@ -48,18 +63,20 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => {
- {/* We have no other subpaths rn */} - {/* - Documents - */} + {navigationLinks.map(({ href, label }) => ( + + {label} + + ))}
); }; diff --git a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx index e488ba6e9..2dcbb9864 100644 --- a/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx +++ b/apps/web/src/components/(dashboard)/layout/profile-dropdown.tsx @@ -4,6 +4,7 @@ import Link from 'next/link'; import { CreditCard, + FileSpreadsheet, Lock, LogOut, User as LucideUser, @@ -106,6 +107,13 @@ export const ProfileDropdown = ({ user }: ProfileDropdownProps) => { )} + + + + + Templates + + diff --git a/apps/web/src/components/formatter/template-type.tsx b/apps/web/src/components/formatter/template-type.tsx new file mode 100644 index 000000000..a7f10105e --- /dev/null +++ b/apps/web/src/components/formatter/template-type.tsx @@ -0,0 +1,50 @@ +import { HTMLAttributes } from 'react'; + +import { Globe, Lock } from 'lucide-react'; +import type { LucideIcon } from 'lucide-react/dist/lucide-react'; + +import { TemplateType as TemplateTypePrisma } from '@documenso/prisma/client'; +import { cn } from '@documenso/ui/lib/utils'; + +type TemplateTypeIcon = { + label: string; + icon?: LucideIcon; + color: string; +}; + +type TemplateTypes = (typeof TemplateTypePrisma)[keyof typeof TemplateTypePrisma]; + +const TEMPLATE_TYPES: Record = { + PRIVATE: { + label: 'Private', + icon: Lock, + color: 'text-blue-600 dark:text-blue-300', + }, + PUBLIC: { + label: 'Public', + icon: Globe, + color: 'text-green-500 dark:text-green-300', + }, +}; + +export type TemplateTypeProps = HTMLAttributes & { + type: TemplateTypes; + inheritColor?: boolean; +}; + +export const TemplateType = ({ className, type, inheritColor, ...props }: TemplateTypeProps) => { + const { label, icon: Icon, color } = TEMPLATE_TYPES[type]; + + return ( + + {Icon && ( + + )} + {label} + + ); +}; diff --git a/apps/web/src/components/forms/edit-template/add-template-fields.action.ts b/apps/web/src/components/forms/edit-template/add-template-fields.action.ts new file mode 100644 index 000000000..2ee7ee825 --- /dev/null +++ b/apps/web/src/components/forms/edit-template/add-template-fields.action.ts @@ -0,0 +1,32 @@ +'use server'; + +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-fields-for-template'; +import { TAddTemplateFieldsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-fields.types'; + +export type AddTemplateFieldsActionInput = TAddTemplateFieldsFormSchema & { + templateId: number; +}; + +export const addTemplateFields = async ({ templateId, fields }: AddTemplateFieldsActionInput) => { + 'use server'; + + const { user } = await getRequiredServerComponentSession(); + + await setFieldsForTemplate({ + userId: user.id, + templateId, + fields: fields.map((field) => ({ + id: field.nativeId, + signerEmail: field.signerEmail, + signerId: field.signerId, + signerToken: field.signerToken, + type: field.type, + pageNumber: field.pageNumber, + pageX: field.pageX, + pageY: field.pageY, + pageWidth: field.pageWidth, + pageHeight: field.pageHeight, + })), + }); +}; diff --git a/apps/web/src/components/forms/edit-template/add-template-placeholders.action.ts b/apps/web/src/components/forms/edit-template/add-template-placeholders.action.ts new file mode 100644 index 000000000..b2183eed1 --- /dev/null +++ b/apps/web/src/components/forms/edit-template/add-template-placeholders.action.ts @@ -0,0 +1,28 @@ +'use server'; + +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { setRecipientsForTemplate } from '@documenso/lib/server-only/recipient/set-recipients-for-template'; +import { TAddTemplatePlacholderRecipientsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-placeholder-recipients.types'; + +export type AddTemplatePlaceholdersActionInput = TAddTemplatePlacholderRecipientsFormSchema & { + templateId: number; +}; + +export const addTemplatePlaceholders = async ({ + templateId, + signers, +}: AddTemplatePlaceholdersActionInput) => { + 'use server'; + + const { user } = await getRequiredServerComponentSession(); + + await setRecipientsForTemplate({ + userId: user.id, + templateId, + recipients: signers.map((signer) => ({ + id: signer.nativeId!, + email: signer.email, + name: signer.name!, + })), + }); +}; diff --git a/packages/lib/server-only/admin/get-recipients-stats.ts b/packages/lib/server-only/admin/get-recipients-stats.ts index f24d0b5a2..b6663e988 100644 --- a/packages/lib/server-only/admin/get-recipients-stats.ts +++ b/packages/lib/server-only/admin/get-recipients-stats.ts @@ -19,9 +19,10 @@ export const getRecipientsStats = async () => { results.forEach((result) => { const { readStatus, signingStatus, sendStatus, _count } = result; - stats[readStatus] += _count; - stats[signingStatus] += _count; - stats[sendStatus] += _count; + stats[readStatus as keyof typeof stats] += _count; + stats.TOTAL_RECIPIENTS += _count; + stats[signingStatus as keyof typeof stats] += _count; + stats[sendStatus as keyof typeof stats] += _count; stats.TOTAL_RECIPIENTS += _count; }); diff --git a/packages/lib/server-only/document/find-documents.ts b/packages/lib/server-only/document/find-documents.ts index a27458a55..18600ebe6 100644 --- a/packages/lib/server-only/document/find-documents.ts +++ b/packages/lib/server-only/document/find-documents.ts @@ -8,7 +8,7 @@ import { ExtendedDocumentStatus } from '@documenso/prisma/types/extended-documen import type { FindResultSet } from '../../types/find-result-set'; -export interface FindDocumentsOptions { +export type FindDocumentsOptions = { userId: number; term?: string; status?: ExtendedDocumentStatus; @@ -19,7 +19,7 @@ export interface FindDocumentsOptions { direction: 'asc' | 'desc'; }; period?: '' | '7d' | '14d' | '30d'; -} +}; export const findDocuments = async ({ userId, diff --git a/packages/lib/server-only/field/get-fields-for-template.ts b/packages/lib/server-only/field/get-fields-for-template.ts new file mode 100644 index 000000000..c174d7eff --- /dev/null +++ b/packages/lib/server-only/field/get-fields-for-template.ts @@ -0,0 +1,22 @@ +import { prisma } from '@documenso/prisma'; + +export interface GetFieldsForTemplateOptions { + templateId: number; + userId: number; +} + +export const getFieldsForTemplate = async ({ templateId, userId }: GetFieldsForTemplateOptions) => { + const fields = await prisma.field.findMany({ + where: { + templateId, + Template: { + userId, + }, + }, + orderBy: { + id: 'asc', + }, + }); + + return fields; +}; diff --git a/packages/lib/server-only/field/remove-signed-field-with-token.ts b/packages/lib/server-only/field/remove-signed-field-with-token.ts index 4a28e7627..ee472ec9f 100644 --- a/packages/lib/server-only/field/remove-signed-field-with-token.ts +++ b/packages/lib/server-only/field/remove-signed-field-with-token.ts @@ -27,6 +27,10 @@ export const removeSignedFieldWithToken = async ({ const { Document: document, Recipient: recipient } = field; + if (!document) { + throw new Error(`Document not found for field ${field.id}`); + } + if (document.status === DocumentStatus.COMPLETED) { throw new Error(`Document ${document.id} has already been completed`); } diff --git a/packages/lib/server-only/field/set-fields-for-template.ts b/packages/lib/server-only/field/set-fields-for-template.ts new file mode 100644 index 000000000..6e2e39afc --- /dev/null +++ b/packages/lib/server-only/field/set-fields-for-template.ts @@ -0,0 +1,116 @@ +import { prisma } from '@documenso/prisma'; +import { FieldType } from '@documenso/prisma/client'; + +export type Field = { + id?: number | null; + type: FieldType; + signerEmail: string; + signerId?: number; + pageNumber: number; + pageX: number; + pageY: number; + pageWidth: number; + pageHeight: number; +}; + +export type SetFieldsForTemplateOptions = { + userId: number; + templateId: number; + fields: Field[]; +}; + +export const setFieldsForTemplate = async ({ + userId, + templateId, + fields, +}: SetFieldsForTemplateOptions) => { + const template = await prisma.template.findFirst({ + where: { + id: templateId, + userId, + }, + }); + + if (!template) { + throw new Error('Document not found'); + } + + const existingFields = await prisma.field.findMany({ + where: { + templateId, + }, + include: { + Recipient: true, + }, + }); + + const removedFields = existingFields.filter( + (existingField) => + !fields.find( + (field) => + field.id === existingField.id || field.signerEmail === existingField.Recipient?.email, + ), + ); + + const linkedFields = fields.map((field) => { + const existing = existingFields.find((existingField) => existingField.id === field.id); + + return { + ...field, + _persisted: existing, + }; + }); + + const persistedFields = await prisma.$transaction( + // Disabling as wrapping promises here causes type issues + // eslint-disable-next-line @typescript-eslint/promise-function-async + linkedFields.map((field) => + prisma.field.upsert({ + where: { + id: field._persisted?.id ?? -1, + templateId, + }, + update: { + page: field.pageNumber, + positionX: field.pageX, + positionY: field.pageY, + width: field.pageWidth, + height: field.pageHeight, + }, + create: { + type: field.type, + page: field.pageNumber, + positionX: field.pageX, + positionY: field.pageY, + width: field.pageWidth, + height: field.pageHeight, + customText: '', + inserted: false, + Template: { + connect: { + id: templateId, + }, + }, + Recipient: { + connect: { + id: field.signerId, + email: field.signerEmail, + }, + }, + }, + }), + ), + ); + + if (removedFields.length > 0) { + await prisma.field.deleteMany({ + where: { + id: { + in: removedFields.map((field) => field.id), + }, + }, + }); + } + + return persistedFields; +}; diff --git a/packages/lib/server-only/field/sign-field-with-token.ts b/packages/lib/server-only/field/sign-field-with-token.ts index 6640a6a07..59fab71e5 100644 --- a/packages/lib/server-only/field/sign-field-with-token.ts +++ b/packages/lib/server-only/field/sign-field-with-token.ts @@ -33,6 +33,10 @@ export const signFieldWithToken = async ({ const { Document: document, Recipient: recipient } = field; + if (!document) { + throw new Error(`Document not found for field ${field.id}`); + } + if (document.status === DocumentStatus.COMPLETED) { throw new Error(`Document ${document.id} has already been completed`); } diff --git a/packages/lib/server-only/recipient/get-recipients-for-template.ts b/packages/lib/server-only/recipient/get-recipients-for-template.ts new file mode 100644 index 000000000..ab6f860eb --- /dev/null +++ b/packages/lib/server-only/recipient/get-recipients-for-template.ts @@ -0,0 +1,25 @@ +import { prisma } from '@documenso/prisma'; + +export interface GetRecipientsForTemplateOptions { + templateId: number; + userId: number; +} + +export const getRecipientsForTemplate = async ({ + templateId, + userId, +}: GetRecipientsForTemplateOptions) => { + const recipients = await prisma.recipient.findMany({ + where: { + templateId, + Template: { + userId, + }, + }, + orderBy: { + id: 'asc', + }, + }); + + return recipients; +}; diff --git a/packages/lib/server-only/recipient/set-recipients-for-template.ts b/packages/lib/server-only/recipient/set-recipients-for-template.ts new file mode 100644 index 000000000..2145d47b4 --- /dev/null +++ b/packages/lib/server-only/recipient/set-recipients-for-template.ts @@ -0,0 +1,98 @@ +import { prisma } from '@documenso/prisma'; + +import { nanoid } from '../../universal/id'; + +export type SetRecipientsForTemplateOptions = { + userId: number; + templateId: number; + recipients: { + id?: number; + email: string; + name: string; + }[]; +}; + +export const setRecipientsForTemplate = async ({ + userId, + templateId, + recipients, +}: SetRecipientsForTemplateOptions) => { + const template = await prisma.template.findFirst({ + where: { + id: templateId, + userId, + }, + }); + + if (!template) { + throw new Error('Template not found'); + } + + const normalizedRecipients = recipients.map((recipient) => ({ + ...recipient, + email: recipient.email.toLowerCase(), + })); + + const existingRecipients = await prisma.recipient.findMany({ + where: { + templateId, + }, + }); + + const removedRecipients = existingRecipients.filter( + (existingRecipient) => + !normalizedRecipients.find( + (recipient) => + recipient.id === existingRecipient.id || recipient.email === existingRecipient.email, + ), + ); + + const linkedRecipients = normalizedRecipients.map((recipient) => { + const existing = existingRecipients.find( + (existingRecipient) => + existingRecipient.id === recipient.id || existingRecipient.email === recipient.email, + ); + + return { + ...recipient, + _persisted: existing, + }; + }); + + const persistedRecipients = await prisma.$transaction( + // Disabling as wrapping promises here causes type issues + // eslint-disable-next-line @typescript-eslint/promise-function-async + linkedRecipients.map((recipient) => + prisma.recipient.upsert({ + where: { + id: recipient._persisted?.id ?? -1, + templateId, + }, + update: { + name: recipient.name, + email: recipient.email, + templateId, + }, + create: { + name: recipient.name, + email: recipient.email, + token: nanoid(), + templateToken: nanoid(), + templateId, + }, + }), + ), + ); + + if (removedRecipients.length > 0) { + await prisma.recipient.deleteMany({ + where: { + id: { + in: removedRecipients.map((recipient) => recipient.id), + }, + }, + }); + } + + return persistedRecipients; +}; diff --git a/packages/lib/server-only/template/create-document-from-template.ts b/packages/lib/server-only/template/create-document-from-template.ts new file mode 100644 index 000000000..b0589821f --- /dev/null +++ b/packages/lib/server-only/template/create-document-from-template.ts @@ -0,0 +1,78 @@ +import { nanoid } from '@documenso/lib/universal/id'; +import { prisma } from '@documenso/prisma'; +import { TCreateDocumentFromTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema'; + +export type CreateDocumentFromTemplateOptions = TCreateDocumentFromTemplateMutationSchema & { + userId: number; +}; + +export const createDocumentFromTemplate = async ({ + templateId, + userId, +}: CreateDocumentFromTemplateOptions) => { + const template = await prisma.template.findUnique({ + where: { id: templateId, userId }, + include: { + Recipient: true, + Field: true, + templateDocumentData: true, + }, + }); + + if (!template) { + throw new Error('Template not found.'); + } + + const documentData = await prisma.documentData.create({ + data: { + type: template.templateDocumentData.type, + data: template.templateDocumentData.data, + initialData: template.templateDocumentData.initialData, + }, + }); + + const document = await prisma.document.create({ + data: { + userId, + title: template.title, + documentDataId: documentData.id, + Recipient: { + create: template.Recipient.map((recipient) => ({ + email: recipient.email, + name: recipient.name, + token: nanoid(), + templateToken: recipient.templateToken, + })), + }, + }, + + include: { + Recipient: true, + }, + }); + + await prisma.field.createMany({ + data: template.Field.map((field) => { + const recipient = template.Recipient.find((recipient) => recipient.id === field.recipientId); + + const documentRecipient = document.Recipient.find( + (doc) => doc.templateToken === recipient?.templateToken, + ); + + return { + type: field.type, + page: field.page, + positionX: field.positionX, + positionY: field.positionY, + width: field.width, + height: field.height, + customText: field.customText, + inserted: field.inserted, + documentId: document.id, + recipientId: documentRecipient?.id || null, + }; + }), + }); + + return document; +}; diff --git a/packages/lib/server-only/template/create-template.ts b/packages/lib/server-only/template/create-template.ts new file mode 100644 index 000000000..d00526a64 --- /dev/null +++ b/packages/lib/server-only/template/create-template.ts @@ -0,0 +1,20 @@ +import { prisma } from '@documenso/prisma'; +import { TCreateTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema'; + +export type CreateTemplateOptions = TCreateTemplateMutationSchema & { + userId: number; +}; + +export const createTemplate = async ({ + title, + userId, + templateDocumentDataId, +}: CreateTemplateOptions) => { + return await prisma.template.create({ + data: { + title, + userId, + templateDocumentDataId, + }, + }); +}; diff --git a/packages/lib/server-only/template/delete-template.ts b/packages/lib/server-only/template/delete-template.ts new file mode 100644 index 000000000..f693bcec0 --- /dev/null +++ b/packages/lib/server-only/template/delete-template.ts @@ -0,0 +1,12 @@ +'use server'; + +import { prisma } from '@documenso/prisma'; + +export type DeleteTemplateOptions = { + id: number; + userId: number; +}; + +export const deleteTemplate = async ({ id, userId }: DeleteTemplateOptions) => { + return await prisma.template.delete({ where: { id, userId } }); +}; diff --git a/packages/lib/server-only/template/duplicate-template.ts b/packages/lib/server-only/template/duplicate-template.ts new file mode 100644 index 000000000..14806707b --- /dev/null +++ b/packages/lib/server-only/template/duplicate-template.ts @@ -0,0 +1,75 @@ +import { nanoid } from '@documenso/lib/universal/id'; +import { prisma } from '@documenso/prisma'; +import { TDuplicateTemplateMutationSchema } from '@documenso/trpc/server/template-router/schema'; + +export type DuplicateTemplateOptions = TDuplicateTemplateMutationSchema & { + userId: number; +}; + +export const duplicateTemplate = async ({ templateId, userId }: DuplicateTemplateOptions) => { + const template = await prisma.template.findUnique({ + where: { id: templateId, userId }, + include: { + Recipient: true, + Field: true, + templateDocumentData: true, + }, + }); + + if (!template) { + throw new Error('Template not found.'); + } + + const documentData = await prisma.documentData.create({ + data: { + type: template.templateDocumentData.type, + data: template.templateDocumentData.data, + initialData: template.templateDocumentData.initialData, + }, + }); + + const duplicatedTemplate = await prisma.template.create({ + data: { + userId, + title: template.title + ' (copy)', + templateDocumentDataId: documentData.id, + Recipient: { + create: template.Recipient.map((recipient) => ({ + email: recipient.email, + name: recipient.name, + token: nanoid(), + templateToken: recipient.templateToken, + })), + }, + }, + + include: { + Recipient: true, + }, + }); + + await prisma.field.createMany({ + data: template.Field.map((field) => { + const recipient = template.Recipient.find((recipient) => recipient.id === field.recipientId); + + const duplicatedTemplateRecipient = duplicatedTemplate.Recipient.find( + (doc) => doc.templateToken === recipient?.templateToken, + ); + + return { + type: field.type, + page: field.page, + positionX: field.positionX, + positionY: field.positionY, + width: field.width, + height: field.height, + customText: field.customText, + inserted: field.inserted, + templateId: duplicatedTemplate.id, + recipientId: duplicatedTemplateRecipient?.id || null, + }; + }), + }); + + return duplicatedTemplate; +}; diff --git a/packages/lib/server-only/template/get-template-by-id.ts b/packages/lib/server-only/template/get-template-by-id.ts new file mode 100644 index 000000000..56f959a9b --- /dev/null +++ b/packages/lib/server-only/template/get-template-by-id.ts @@ -0,0 +1,18 @@ +import { prisma } from '@documenso/prisma'; + +export interface GetTemplateByIdOptions { + id: number; + userId: number; +} + +export const getTemplateById = async ({ id, userId }: GetTemplateByIdOptions) => { + return await prisma.template.findFirstOrThrow({ + where: { + id, + userId, + }, + include: { + templateDocumentData: true, + }, + }); +}; diff --git a/packages/lib/server-only/template/get-templates.ts b/packages/lib/server-only/template/get-templates.ts new file mode 100644 index 000000000..60de7cd89 --- /dev/null +++ b/packages/lib/server-only/template/get-templates.ts @@ -0,0 +1,37 @@ +import { prisma } from '@documenso/prisma'; + +export type GetTemplatesOptions = { + userId: number; + page: number; + perPage: number; +}; + +export const getTemplates = async ({ userId, page = 1, perPage = 10 }: GetTemplatesOptions) => { + const [templates, count] = await Promise.all([ + await prisma.template.findMany({ + where: { + userId, + }, + include: { + templateDocumentData: true, + Field: true, + }, + skip: Math.max(page - 1, 0) * perPage, + orderBy: { + createdAt: 'desc', + }, + }), + await prisma.template.count({ + where: { + User: { + id: userId, + }, + }, + }), + ]); + + return { + templates, + totalPages: Math.ceil(count / perPage), + }; +}; diff --git a/packages/prisma/migrations/20231007013737_templates/migration.sql b/packages/prisma/migrations/20231007013737_templates/migration.sql new file mode 100644 index 000000000..e0c1bf4ec --- /dev/null +++ b/packages/prisma/migrations/20231007013737_templates/migration.sql @@ -0,0 +1,52 @@ +-- CreateEnum +CREATE TYPE "TemplateStatus" AS ENUM ('PUBLIC', 'PRIVATE'); + +-- CreateTable +CREATE TABLE "Template" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "status" "TemplateStatus" NOT NULL DEFAULT 'PRIVATE', + "templateDataId" TEXT NOT NULL, + + CONSTRAINT "Template_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TemplateData" ( + "id" TEXT NOT NULL, + "type" "DocumentDataType" NOT NULL, + "data" TEXT NOT NULL, + "initialData" TEXT NOT NULL, + + CONSTRAINT "TemplateData_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TemplateField" ( + "id" SERIAL NOT NULL, + "templateId" INTEGER NOT NULL, + "type" "FieldType" NOT NULL, + "page" INTEGER NOT NULL, + "positionX" DECIMAL(65,30) NOT NULL DEFAULT 0, + "positionY" DECIMAL(65,30) NOT NULL DEFAULT 0, + "width" DECIMAL(65,30) NOT NULL DEFAULT -1, + "height" DECIMAL(65,30) NOT NULL DEFAULT -1, + "customText" TEXT NOT NULL, + "inserted" BOOLEAN NOT NULL, + + CONSTRAINT "TemplateField_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Template_templateDataId_key" ON "Template"("templateDataId"); + +-- AddForeignKey +ALTER TABLE "Template" ADD CONSTRAINT "Template_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Template" ADD CONSTRAINT "Template_templateDataId_fkey" FOREIGN KEY ("templateDataId") REFERENCES "TemplateData"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TemplateField" ADD CONSTRAINT "TemplateField_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20231007014431_templates_type/migration.sql b/packages/prisma/migrations/20231007014431_templates_type/migration.sql new file mode 100644 index 000000000..c89e09a61 --- /dev/null +++ b/packages/prisma/migrations/20231007014431_templates_type/migration.sql @@ -0,0 +1,15 @@ +/* + Warnings: + + - The `status` column on the `Template` table would be dropped and recreated. This will lead to data loss if there is data in the column. + +*/ +-- CreateEnum +CREATE TYPE "TemplateType" AS ENUM ('PUBLIC', 'PRIVATE'); + +-- AlterTable +ALTER TABLE "Template" DROP COLUMN "status", +ADD COLUMN "status" "TemplateType" NOT NULL DEFAULT 'PRIVATE'; + +-- DropEnum +DROP TYPE "TemplateStatus"; diff --git a/packages/prisma/migrations/20231007021427_reuse_document_data/migration.sql b/packages/prisma/migrations/20231007021427_reuse_document_data/migration.sql new file mode 100644 index 000000000..629f292fc --- /dev/null +++ b/packages/prisma/migrations/20231007021427_reuse_document_data/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - You are about to drop the `TemplateData` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "Template" DROP CONSTRAINT "Template_templateDataId_fkey"; + +-- DropTable +DROP TABLE "TemplateData"; + +-- AddForeignKey +ALTER TABLE "Template" ADD CONSTRAINT "Template_templateDataId_fkey" FOREIGN KEY ("templateDataId") REFERENCES "DocumentData"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20231007033447_remove_inserted_on_template_field/migration.sql b/packages/prisma/migrations/20231007033447_remove_inserted_on_template_field/migration.sql new file mode 100644 index 000000000..25ace4f72 --- /dev/null +++ b/packages/prisma/migrations/20231007033447_remove_inserted_on_template_field/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `inserted` on the `TemplateField` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "TemplateField" DROP COLUMN "inserted"; diff --git a/packages/prisma/migrations/20231007080315_document_name_to_template/migration.sql b/packages/prisma/migrations/20231007080315_document_name_to_template/migration.sql new file mode 100644 index 000000000..45a52de3d --- /dev/null +++ b/packages/prisma/migrations/20231007080315_document_name_to_template/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `documentName` to the `Template` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Template" ADD COLUMN "documentName" TEXT NOT NULL; diff --git a/packages/prisma/migrations/20231007211915_template_created_date/migration.sql b/packages/prisma/migrations/20231007211915_template_created_date/migration.sql new file mode 100644 index 000000000..816da092e --- /dev/null +++ b/packages/prisma/migrations/20231007211915_template_created_date/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - Added the required column `updatedAt` to the `Template` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Template" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; diff --git a/packages/prisma/migrations/20231017041643_placeholder_recipients/migration.sql b/packages/prisma/migrations/20231017041643_placeholder_recipients/migration.sql new file mode 100644 index 000000000..b84a567c8 --- /dev/null +++ b/packages/prisma/migrations/20231017041643_placeholder_recipients/migration.sql @@ -0,0 +1,51 @@ +/* + Warnings: + + - You are about to drop the column `description` on the `Template` table. All the data in the column will be lost. + - You are about to drop the column `documentName` on the `Template` table. All the data in the column will be lost. + - You are about to drop the column `status` on the `Template` table. All the data in the column will be lost. + - You are about to drop the column `templateDataId` on the `Template` table. All the data in the column will be lost. + - A unique constraint covering the columns `[tempateDocumentDataId]` on the table `Template` will be added. If there are existing duplicate values, this will fail. + - Added the required column `tempateDocumentDataId` to the `Template` table without a default value. This is not possible if the table is not empty. + - Added the required column `inserted` to the `TemplateField` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "Template" DROP CONSTRAINT "Template_templateDataId_fkey"; + +-- DropIndex +DROP INDEX "Template_templateDataId_key"; + +-- AlterTable +ALTER TABLE "Template" DROP COLUMN "description", +DROP COLUMN "documentName", +DROP COLUMN "status", +DROP COLUMN "templateDataId", +ADD COLUMN "tempateDocumentDataId" TEXT NOT NULL, +ADD COLUMN "type" "TemplateType" NOT NULL DEFAULT 'PRIVATE', +ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP; + +-- AlterTable +ALTER TABLE "TemplateField" ADD COLUMN "inserted" BOOLEAN NOT NULL, +ADD COLUMN "recipientId" INTEGER; + +-- CreateTable +CREATE TABLE "TemplateRecipient" ( + "id" SERIAL NOT NULL, + "templateId" INTEGER NOT NULL, + "placeholder" VARCHAR(255) NOT NULL, + + CONSTRAINT "TemplateRecipient_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Template_tempateDocumentDataId_key" ON "Template"("tempateDocumentDataId"); + +-- AddForeignKey +ALTER TABLE "Template" ADD CONSTRAINT "Template_tempateDocumentDataId_fkey" FOREIGN KEY ("tempateDocumentDataId") REFERENCES "DocumentData"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TemplateRecipient" ADD CONSTRAINT "TemplateRecipient_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TemplateField" ADD CONSTRAINT "TemplateField_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "TemplateRecipient"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20231017042227_fix_typo/migration.sql b/packages/prisma/migrations/20231017042227_fix_typo/migration.sql new file mode 100644 index 000000000..ac9eaf10e --- /dev/null +++ b/packages/prisma/migrations/20231017042227_fix_typo/migration.sql @@ -0,0 +1,23 @@ +/* + Warnings: + + - You are about to drop the column `tempateDocumentDataId` on the `Template` table. All the data in the column will be lost. + - A unique constraint covering the columns `[templateDocumentDataId]` on the table `Template` will be added. If there are existing duplicate values, this will fail. + - Added the required column `templateDocumentDataId` to the `Template` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "Template" DROP CONSTRAINT "Template_tempateDocumentDataId_fkey"; + +-- DropIndex +DROP INDEX "Template_tempateDocumentDataId_key"; + +-- AlterTable +ALTER TABLE "Template" DROP COLUMN "tempateDocumentDataId", +ADD COLUMN "templateDocumentDataId" TEXT NOT NULL; + +-- CreateIndex +CREATE UNIQUE INDEX "Template_templateDocumentDataId_key" ON "Template"("templateDocumentDataId"); + +-- AddForeignKey +ALTER TABLE "Template" ADD CONSTRAINT "Template_templateDocumentDataId_fkey" FOREIGN KEY ("templateDocumentDataId") REFERENCES "DocumentData"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20231019043226_template_recipient_email/migration.sql b/packages/prisma/migrations/20231019043226_template_recipient_email/migration.sql new file mode 100644 index 000000000..266333794 --- /dev/null +++ b/packages/prisma/migrations/20231019043226_template_recipient_email/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `email` to the `TemplateRecipient` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "TemplateRecipient" ADD COLUMN "email" VARCHAR(255) NOT NULL; diff --git a/packages/prisma/migrations/20231020032507_template_recipient_token/migration.sql b/packages/prisma/migrations/20231020032507_template_recipient_token/migration.sql new file mode 100644 index 000000000..9ce1fa70a --- /dev/null +++ b/packages/prisma/migrations/20231020032507_template_recipient_token/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `token` to the `TemplateRecipient` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "TemplateRecipient" ADD COLUMN "token" TEXT NOT NULL; diff --git a/packages/prisma/migrations/20231021193915_template_token_for_recipient/migration.sql b/packages/prisma/migrations/20231021193915_template_token_for_recipient/migration.sql new file mode 100644 index 000000000..a6b3e7199 --- /dev/null +++ b/packages/prisma/migrations/20231021193915_template_token_for_recipient/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Recipient" ADD COLUMN "templateToken" TEXT; + +-- AlterTable +ALTER TABLE "TemplateRecipient" ADD COLUMN "templateToken" TEXT; diff --git a/packages/prisma/migrations/20231030061522_recipient_name_not_placeholder/migration.sql b/packages/prisma/migrations/20231030061522_recipient_name_not_placeholder/migration.sql new file mode 100644 index 000000000..8b9275c68 --- /dev/null +++ b/packages/prisma/migrations/20231030061522_recipient_name_not_placeholder/migration.sql @@ -0,0 +1,10 @@ +/* + Warnings: + + - You are about to drop the column `placeholder` on the `TemplateRecipient` table. All the data in the column will be lost. + - Added the required column `name` to the `TemplateRecipient` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "TemplateRecipient" DROP COLUMN "placeholder", +ADD COLUMN "name" VARCHAR(255) NOT NULL; diff --git a/packages/prisma/migrations/20231208090322_remove_template_specific_models/migration.sql b/packages/prisma/migrations/20231208090322_remove_template_specific_models/migration.sql new file mode 100644 index 000000000..7e2af3ef8 --- /dev/null +++ b/packages/prisma/migrations/20231208090322_remove_template_specific_models/migration.sql @@ -0,0 +1,54 @@ +/* + Warnings: + + - You are about to drop the `TemplateField` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `TemplateRecipient` table. If the table is not empty, all the data it contains will be lost. + - A unique constraint covering the columns `[templateId,email]` on the table `Recipient` will be added. If there are existing duplicate values, this will fail. + +*/ +-- DropForeignKey +ALTER TABLE "Field" DROP CONSTRAINT "Field_recipientId_fkey"; + +-- DropForeignKey +ALTER TABLE "TemplateField" DROP CONSTRAINT "TemplateField_recipientId_fkey"; + +-- DropForeignKey +ALTER TABLE "TemplateField" DROP CONSTRAINT "TemplateField_templateId_fkey"; + +-- DropForeignKey +ALTER TABLE "TemplateRecipient" DROP CONSTRAINT "TemplateRecipient_templateId_fkey"; + +-- AlterTable +ALTER TABLE "Field" ADD COLUMN "templateId" INTEGER, +ALTER COLUMN "documentId" DROP NOT NULL; + +-- AlterTable +ALTER TABLE "Recipient" ADD COLUMN "templateId" INTEGER, +ALTER COLUMN "documentId" DROP NOT NULL, +ALTER COLUMN "readStatus" DROP NOT NULL, +ALTER COLUMN "signingStatus" DROP NOT NULL, +ALTER COLUMN "sendStatus" DROP NOT NULL; + +-- DropTable +DROP TABLE "TemplateField"; + +-- DropTable +DROP TABLE "TemplateRecipient"; + +-- CreateIndex +CREATE INDEX "Field_templateId_idx" ON "Field"("templateId"); + +-- CreateIndex +CREATE INDEX "Recipient_templateId_idx" ON "Recipient"("templateId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Recipient_templateId_email_key" ON "Recipient"("templateId", "email"); + +-- AddForeignKey +ALTER TABLE "Recipient" ADD CONSTRAINT "Recipient_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Field" ADD CONSTRAINT "Field_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Field" ADD CONSTRAINT "Field_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Recipient"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 75c175adc..d21c4c637 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -40,6 +40,7 @@ model User { twoFactorEnabled Boolean @default(false) twoFactorBackupCodes String? VerificationToken VerificationToken[] + Template Template[] @@index([email]) } @@ -154,6 +155,7 @@ model DocumentData { data String initialData String Document Document? + Template Template? } model DocumentMeta { @@ -180,22 +182,27 @@ enum SigningStatus { } model Recipient { - id Int @id @default(autoincrement()) - documentId Int - email String @db.VarChar(255) - name String @default("") @db.VarChar(255) + id Int @id @default(autoincrement()) + documentId Int? + templateId Int? + email String @db.VarChar(255) + name String @default("") @db.VarChar(255) token String + templateToken String? expired DateTime? signedAt DateTime? - readStatus ReadStatus @default(NOT_OPENED) - signingStatus SigningStatus @default(NOT_SIGNED) - sendStatus SendStatus @default(NOT_SENT) - Document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) + readStatus ReadStatus? @default(NOT_OPENED) + signingStatus SigningStatus? @default(NOT_SIGNED) + sendStatus SendStatus? @default(NOT_SENT) + Document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade) + Template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade) Field Field[] Signature Signature[] @@unique([documentId, email]) + @@unique([templateId, email]) @@index([documentId]) + @@index([templateId]) @@index([token]) } @@ -210,7 +217,8 @@ enum FieldType { model Field { id Int @id @default(autoincrement()) - documentId Int + documentId Int? + templateId Int? recipientId Int? type FieldType page Int @@ -220,11 +228,13 @@ model Field { height Decimal @default(-1) customText String inserted Boolean - Document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) - Recipient Recipient? @relation(fields: [recipientId], references: [id], onDelete: Cascade) + Document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade) + Template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade) + Recipient Recipient? @relation(fields: [recipientId], references: [id]) Signature Signature? @@index([documentId]) + @@index([templateId]) @@index([recipientId]) } @@ -254,3 +264,24 @@ model DocumentShareLink { @@unique([documentId, email]) } + +enum TemplateType { + PUBLIC + PRIVATE +} + +model Template { + id Int @id @default(autoincrement()) + userId Int + User User @relation(fields: [userId], references: [id], onDelete: Cascade) + title String + type TemplateType @default(PRIVATE) + templateDocumentDataId String + templateDocumentData DocumentData @relation(fields: [templateDocumentDataId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + Recipient Recipient[] + Field Field[] + + @@unique([templateDocumentDataId]) +} diff --git a/packages/trpc/server/router.ts b/packages/trpc/server/router.ts index bf8a03ce1..77d18e06d 100644 --- a/packages/trpc/server/router.ts +++ b/packages/trpc/server/router.ts @@ -6,6 +6,7 @@ import { profileRouter } from './profile-router/router'; import { recipientRouter } from './recipient-router/router'; import { shareLinkRouter } from './share-link-router/router'; import { singleplayerRouter } from './singleplayer-router/router'; +import { templateRouter } from './template-router/router'; import { router } from './trpc'; import { twoFactorAuthenticationRouter } from './two-factor-authentication-router/router'; @@ -19,6 +20,7 @@ export const appRouter = router({ shareLink: shareLinkRouter, singleplayer: singleplayerRouter, twoFactorAuthentication: twoFactorAuthenticationRouter, + template: templateRouter, }); export type AppRouter = typeof appRouter; diff --git a/packages/trpc/server/template-router/router.ts b/packages/trpc/server/template-router/router.ts new file mode 100644 index 000000000..e18f4cb4a --- /dev/null +++ b/packages/trpc/server/template-router/router.ts @@ -0,0 +1,94 @@ +import { TRPCError } from '@trpc/server'; + +import { createDocumentFromTemplate } from '@documenso/lib/server-only/template/create-document-from-template'; +import { createTemplate } from '@documenso/lib/server-only/template/create-template'; +import { deleteTemplate } from '@documenso/lib/server-only/template/delete-template'; +import { duplicateTemplate } from '@documenso/lib/server-only/template/duplicate-template'; + +import { authenticatedProcedure, router } from '../trpc'; +import { + ZCreateDocumentFromTemplateMutationSchema, + ZCreateTemplateMutationSchema, + ZDeleteTemplateMutationSchema, + ZDuplicateTemplateMutationSchema, +} from './schema'; + +export const templateRouter = router({ + createTemplate: authenticatedProcedure + .input(ZCreateTemplateMutationSchema) + .mutation(async ({ input, ctx }) => { + try { + const { title, templateDocumentDataId } = input; + + return await createTemplate({ + title, + userId: ctx.user.id, + templateDocumentDataId, + }); + } catch (err) { + console.error(err); + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We were unable to create this template. Please try again later.', + }); + } + }), + + createDocumentFromTemplate: authenticatedProcedure + .input(ZCreateDocumentFromTemplateMutationSchema) + .mutation(async ({ input, ctx }) => { + try { + const { templateId } = input; + + return await createDocumentFromTemplate({ + templateId, + userId: ctx.user.id, + }); + } catch (err) { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We were unable to create this document. Please try again later.', + }); + } + }), + + duplicateTemplate: authenticatedProcedure + .input(ZDuplicateTemplateMutationSchema) + .mutation(async ({ input, ctx }) => { + try { + const { templateId } = input; + + return await duplicateTemplate({ + templateId, + userId: ctx.user.id, + }); + } catch (err) { + console.error(err); + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We were unable to duplicate the template. Please try again later.', + }); + } + }), + + deleteTemplate: authenticatedProcedure + .input(ZDeleteTemplateMutationSchema) + .mutation(async ({ input, ctx }) => { + try { + const { id } = input; + + const userId = ctx.user.id; + + return await deleteTemplate({ id, userId }); + } catch (err) { + console.error(err); + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We were unable to delete this template. Please try again later.', + }); + } + }), +}); diff --git a/packages/trpc/server/template-router/schema.ts b/packages/trpc/server/template-router/schema.ts new file mode 100644 index 000000000..bc7161f74 --- /dev/null +++ b/packages/trpc/server/template-router/schema.ts @@ -0,0 +1,26 @@ +import { z } from 'zod'; + +export const ZCreateTemplateMutationSchema = z.object({ + title: z.string().min(1), + templateDocumentDataId: z.string().min(1), +}); + +export const ZCreateDocumentFromTemplateMutationSchema = z.object({ + templateId: z.number(), +}); + +export const ZDuplicateTemplateMutationSchema = z.object({ + templateId: z.number(), +}); + +export const ZDeleteTemplateMutationSchema = z.object({ + id: z.number().min(1), +}); + +export type TCreateTemplateMutationSchema = z.infer; +export type TCreateDocumentFromTemplateMutationSchema = z.infer< + typeof ZCreateDocumentFromTemplateMutationSchema +>; + +export type TDuplicateTemplateMutationSchema = z.infer; +export type TDeleteTemplateMutationSchema = z.infer; diff --git a/packages/ui/primitives/document-dropzone.tsx b/packages/ui/primitives/document-dropzone.tsx index d81a3a7de..9ae4c2adb 100644 --- a/packages/ui/primitives/document-dropzone.tsx +++ b/packages/ui/primitives/document-dropzone.tsx @@ -75,10 +75,20 @@ const DocumentDropzoneCardCenterVariants: Variants = { }, }; +const DocumentDescription = { + document: { + headline: 'Add a document', + }, + template: { + headline: 'Upload Template Document', + }, +}; + export type DocumentDropzoneProps = { className?: string; disabled?: boolean; onDrop?: (_file: File) => void | Promise; + type?: 'document' | 'template'; [key: string]: unknown; }; @@ -86,6 +96,7 @@ export const DocumentDropzone = ({ className, onDrop, disabled, + type = 'document', ...props }: DocumentDropzoneProps) => { const { getRootProps, getInputProps } = useDropzone({ @@ -157,7 +168,7 @@ export const DocumentDropzone = ({

- Add a document + {DocumentDescription[type].headline}

Drag & drop your document here.

diff --git a/packages/ui/primitives/document-flow/types.ts b/packages/ui/primitives/document-flow/types.ts index 677dc931b..82f5706e6 100644 --- a/packages/ui/primitives/document-flow/types.ts +++ b/packages/ui/primitives/document-flow/types.ts @@ -24,7 +24,7 @@ export const ZDocumentFlowFormSchema = z.object({ formId: z.string().min(1), nativeId: z.number().optional(), type: z.nativeEnum(FieldType), - signerEmail: z.string().min(1), + signerEmail: z.string().min(1).optional(), pageNumber: z.number().min(1), pageX: z.number().min(0), pageY: z.number().min(0), diff --git a/packages/ui/primitives/template-flow/add-template-fields.tsx b/packages/ui/primitives/template-flow/add-template-fields.tsx new file mode 100644 index 000000000..138c73ea7 --- /dev/null +++ b/packages/ui/primitives/template-flow/add-template-fields.tsx @@ -0,0 +1,539 @@ +'use client'; + +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { Caveat } from 'next/font/google'; + +import { ChevronsUpDown } from 'lucide-react'; +import { useFieldArray, useForm } from 'react-hook-form'; + +import { getBoundingClientRect } from '@documenso/lib/client-only/get-bounding-client-rect'; +import { useDocumentElement } from '@documenso/lib/client-only/hooks/use-document-element'; +import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; +import { nanoid } from '@documenso/lib/universal/id'; +import { Field, FieldType, Recipient } from '@documenso/prisma/client'; +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; +import { Card, CardContent } from '@documenso/ui/primitives/card'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from '@documenso/ui/primitives/command'; +import { + DocumentFlowFormContainerActions, + DocumentFlowFormContainerContent, + DocumentFlowFormContainerFooter, + DocumentFlowFormContainerStep, +} from '@documenso/ui/primitives/document-flow/document-flow-root'; +import { FieldItem } from '@documenso/ui/primitives/document-flow/field-item'; +import { + DocumentFlowStep, + FRIENDLY_FIELD_TYPE, +} from '@documenso/ui/primitives/document-flow/types'; +import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover'; + +// import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; +import { TAddTemplateFieldsFormSchema } from './add-template-fields.types'; + +const fontCaveat = Caveat({ + weight: ['500'], + subsets: ['latin'], + display: 'swap', + variable: '--font-caveat', +}); + +const DEFAULT_HEIGHT_PERCENT = 5; +const DEFAULT_WIDTH_PERCENT = 15; + +const MIN_HEIGHT_PX = 60; +const MIN_WIDTH_PX = 200; + +export type AddTemplateFieldsFormProps = { + documentFlow: DocumentFlowStep; + hideRecipients?: boolean; + recipients: Recipient[]; + fields: Field[]; + numberOfSteps: number; + onSubmit: (_data: TAddTemplateFieldsFormSchema) => void; +}; + +export const AddTemplateFieldsFormPartial = ({ + documentFlow, + hideRecipients = false, + recipients, + fields, + numberOfSteps, + onSubmit, +}: AddTemplateFieldsFormProps) => { + const { isWithinPageBounds, getFieldPosition, getPage } = useDocumentElement(); + + const { + control, + handleSubmit, + formState: { isSubmitting }, + } = useForm({ + defaultValues: { + fields: fields.map((field) => ({ + nativeId: field.id, + formId: `${field.id}-${field.templateId}`, + pageNumber: field.page, + type: field.type, + pageX: Number(field.positionX), + pageY: Number(field.positionY), + pageWidth: Number(field.width), + pageHeight: Number(field.height), + signerId: field.recipientId ?? -1, + signerEmail: + recipients.find((recipient) => recipient.id === field.recipientId)?.email ?? '', + signerToken: + recipients.find((recipient) => recipient.id === field.recipientId)?.templateToken ?? '', + })), + }, + }); + + const onFormSubmit = handleSubmit(onSubmit); + + const { + append, + remove, + update, + fields: localFields, + } = useFieldArray({ + control, + name: 'fields', + }); + + const [selectedField, setSelectedField] = useState(null); + const [selectedSigner, setSelectedSigner] = useState(null); + const [showRecipientsSelector, setShowRecipientsSelector] = useState(false); + + const [isFieldWithinBounds, setIsFieldWithinBounds] = useState(false); + const [coords, setCoords] = useState({ + x: 0, + y: 0, + }); + + const fieldBounds = useRef({ + height: 0, + width: 0, + }); + + const onMouseMove = useCallback( + (event: MouseEvent) => { + setIsFieldWithinBounds( + isWithinPageBounds( + event, + PDF_VIEWER_PAGE_SELECTOR, + fieldBounds.current.width, + fieldBounds.current.height, + ), + ); + + setCoords({ + x: event.clientX - fieldBounds.current.width / 2, + y: event.clientY - fieldBounds.current.height / 2, + }); + }, + [isWithinPageBounds], + ); + + const onMouseClick = useCallback( + (event: MouseEvent) => { + if (!selectedField || !selectedSigner) { + return; + } + + const $page = getPage(event, PDF_VIEWER_PAGE_SELECTOR); + + if ( + !$page || + !isWithinPageBounds( + event, + PDF_VIEWER_PAGE_SELECTOR, + fieldBounds.current.width, + fieldBounds.current.height, + ) + ) { + setSelectedField(null); + return; + } + + const { top, left, height, width } = getBoundingClientRect($page); + + const pageNumber = parseInt($page.getAttribute('data-page-number') ?? '1', 10); + + // Calculate x and y as a percentage of the page width and height + let pageX = ((event.pageX - left) / width) * 100; + let pageY = ((event.pageY - top) / height) * 100; + + // Get the bounds as a percentage of the page width and height + const fieldPageWidth = (fieldBounds.current.width / width) * 100; + const fieldPageHeight = (fieldBounds.current.height / height) * 100; + + // And center it based on the bounds + pageX -= fieldPageWidth / 2; + pageY -= fieldPageHeight / 2; + + append({ + formId: nanoid(12), + type: selectedField, + pageNumber, + pageX, + pageY, + pageWidth: fieldPageWidth, + pageHeight: fieldPageHeight, + signerEmail: selectedSigner.email, + signerId: selectedSigner.id, + signerToken: selectedSigner.templateToken ?? '', + }); + + setIsFieldWithinBounds(false); + setSelectedField(null); + }, + [append, isWithinPageBounds, selectedField, selectedSigner, getPage], + ); + + const onFieldResize = useCallback( + (node: HTMLElement, index: number) => { + const field = localFields[index]; + + const $page = window.document.querySelector( + `${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`, + ); + + if (!$page) { + return; + } + + const { + x: pageX, + y: pageY, + width: pageWidth, + height: pageHeight, + } = getFieldPosition($page, node); + + update(index, { + ...field, + pageX, + pageY, + pageWidth, + pageHeight, + }); + }, + [getFieldPosition, localFields, update], + ); + + const onFieldMove = useCallback( + (node: HTMLElement, index: number) => { + const field = localFields[index]; + + const $page = window.document.querySelector( + `${PDF_VIEWER_PAGE_SELECTOR}[data-page-number="${field.pageNumber}"]`, + ); + + if (!$page) { + return; + } + + const { x: pageX, y: pageY } = getFieldPosition($page, node); + + update(index, { + ...field, + pageX, + pageY, + }); + }, + [getFieldPosition, localFields, update], + ); + + useEffect(() => { + if (selectedField) { + window.addEventListener('mousemove', onMouseMove); + window.addEventListener('mouseup', onMouseClick); + } + + return () => { + window.removeEventListener('mousemove', onMouseMove); + window.removeEventListener('mouseup', onMouseClick); + }; + }, [onMouseClick, onMouseMove, selectedField]); + + useEffect(() => { + const observer = new MutationObserver((_mutations) => { + const $page = document.querySelector(PDF_VIEWER_PAGE_SELECTOR); + + if (!$page) { + return; + } + + const { height, width } = $page.getBoundingClientRect(); + + fieldBounds.current = { + height: Math.max(height * (DEFAULT_HEIGHT_PERCENT / 100), MIN_HEIGHT_PX), + width: Math.max(width * (DEFAULT_WIDTH_PERCENT / 100), MIN_WIDTH_PX), + }; + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + }); + + return () => { + observer.disconnect(); + }; + }, []); + + useEffect(() => { + setSelectedSigner(recipients[0]); + }, [recipients]); + + return ( + <> + +
+ {selectedField && ( + + + {FRIENDLY_FIELD_TYPE[selectedField]} + + + )} + + {localFields.map((field, index) => ( + onFieldResize(options, index)} + onMove={(options) => onFieldMove(options, index)} + onRemove={() => remove(index)} + /> + ))} + + {!hideRecipients && ( + + + + + + + + + + + No recipient matching this description was found. + + + + + {recipients.map((recipient, index) => ( + { + setSelectedSigner(recipient); + setShowRecipientsSelector(false); + }} + > + {/* {recipient.sendStatus !== SendStatus.SENT ? ( + + ) : ( + + + + + + This document has already been sent to this recipient. You can no + longer edit this recipient. + + + )} */} + + {recipient.name && ( + + {recipient.name} ({recipient.email}) + + )} + + {!recipient.name && ( + + {recipient.email} + + )} + + ))} + + + + + )} + +
+
+ + + + + + + +
+
+
+
+ + + + + { + documentFlow.onBackStep?.(); + remove(); + }} + onGoNextClick={() => void onFormSubmit()} + /> + + + ); +}; diff --git a/packages/ui/primitives/template-flow/add-template-fields.types.ts b/packages/ui/primitives/template-flow/add-template-fields.types.ts new file mode 100644 index 000000000..4406f82a0 --- /dev/null +++ b/packages/ui/primitives/template-flow/add-template-fields.types.ts @@ -0,0 +1,23 @@ +import { z } from 'zod'; + +import { FieldType } from '@documenso/prisma/client'; + +export const ZAddTemplateFieldsFormSchema = z.object({ + fields: z.array( + z.object({ + formId: z.string().min(1), + nativeId: z.number().optional(), + type: z.nativeEnum(FieldType), + signerEmail: z.string().min(1), + signerToken: z.string(), + signerId: z.number().optional(), + pageNumber: z.number().min(1), + pageX: z.number().min(0), + pageY: z.number().min(0), + pageWidth: z.number().min(0), + pageHeight: z.number().min(0), + }), + ), +}); + +export type TAddTemplateFieldsFormSchema = z.infer; diff --git a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx new file mode 100644 index 000000000..498314133 --- /dev/null +++ b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.tsx @@ -0,0 +1,205 @@ +'use client'; + +import React, { useId, useState } from 'react'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { AnimatePresence, motion } from 'framer-motion'; +import { Plus, Trash } from 'lucide-react'; +import { Controller, useFieldArray, useForm } from 'react-hook-form'; + +import { nanoid } from '@documenso/lib/universal/id'; +import { Field, Recipient } from '@documenso/prisma/client'; +import { Button } from '@documenso/ui/primitives/button'; +import { FormErrorMessage } from '@documenso/ui/primitives/form/form-error-message'; +import { Input } from '@documenso/ui/primitives/input'; +import { Label } from '@documenso/ui/primitives/label'; + +import { + DocumentFlowFormContainerActions, + DocumentFlowFormContainerContent, + DocumentFlowFormContainerFooter, + DocumentFlowFormContainerStep, +} from '../document-flow/document-flow-root'; +import { DocumentFlowStep } from '../document-flow/types'; +import { + TAddTemplatePlacholderRecipientsFormSchema, + ZAddTemplatePlacholderRecipientsFormSchema, +} from './add-template-placeholder-recipients.types'; + +export type AddTemplatePlaceholderRecipientsFormProps = { + documentFlow: DocumentFlowStep; + recipients: Recipient[]; + fields: Field[]; + numberOfSteps: number; + onSubmit: (_data: TAddTemplatePlacholderRecipientsFormSchema) => void; +}; + +export const AddTemplatePlaceholderRecipientsFormPartial = ({ + documentFlow, + numberOfSteps, + recipients, + fields: _fields, + onSubmit, +}: AddTemplatePlaceholderRecipientsFormProps) => { + const initialId = useId(); + const [placeholderRecipientCount, setPlaceholderRecipientCount] = useState(1); + + const { + control, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + resolver: zodResolver(ZAddTemplatePlacholderRecipientsFormSchema), + defaultValues: { + signers: + recipients.length > 0 + ? recipients.map((recipient) => ({ + nativeId: recipient.id, + formId: String(recipient.id), + name: recipient.name, + email: recipient.email, + })) + : [ + { + formId: initialId, + name: `John Doe`, + email: `johndoe@documenso.com`, + }, + ], + }, + }); + + const onFormSubmit = handleSubmit(onSubmit); + + const { + append: appendSigner, + fields: signers, + remove: removeSigner, + } = useFieldArray({ + control, + name: 'signers', + }); + + const onAddPlaceholderRecipient = () => { + setPlaceholderRecipientCount(placeholderRecipientCount + 1); + + appendSigner({ + formId: nanoid(12), + name: `John Doe ${placeholderRecipientCount}`, + email: `johndoe${placeholderRecipientCount}@documenso.com`, + }); + }; + + const onRemoveSigner = (index: number) => { + removeSigner(index); + }; + + const onKeyDown = (event: React.KeyboardEvent) => { + if (event.key === 'Enter' && event.target instanceof HTMLInputElement) { + onAddPlaceholderRecipient(); + } + }; + + return ( + <> + +
+ + {signers.map((signer, index) => ( + +
+ + + ( + + )} + /> +
+ +
+ + + ( + + )} + /> +
+ +
+ +
+ +
+ + +
+
+ ))} +
+
+ + + +
+ +
+
+ + + + + void onFormSubmit()} + /> + + + ); +}; diff --git a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.types.ts b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.types.ts new file mode 100644 index 000000000..89c197f5e --- /dev/null +++ b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.types.ts @@ -0,0 +1,26 @@ +import { z } from 'zod'; + +export const ZAddTemplatePlacholderRecipientsFormSchema = z + .object({ + signers: z.array( + z.object({ + formId: z.string().min(1), + nativeId: z.number().optional(), + email: z.string().min(1).email(), + name: z.string().optional(), + }), + ), + }) + .refine( + (schema) => { + const emails = schema.signers.map((signer) => signer.email.toLowerCase()); + + return new Set(emails).size === emails.length; + }, + // Dirty hack to handle errors when .root is populated for an array type + { message: 'Signers must have unique emails', path: ['signers__root'] }, + ); + +export type TAddTemplatePlacholderRecipientsFormSchema = z.infer< + typeof ZAddTemplatePlacholderRecipientsFormSchema +>; From 1eeb5fb103f81be2866177cc9aa9b4004b918e1c Mon Sep 17 00:00:00 2001 From: Mythie Date: Thu, 14 Dec 2023 15:28:27 +1100 Subject: [PATCH 13/53] fix: tidy code and base on main --- .../templates/[id]/edit-template.tsx | 27 ++++--- .../app/(dashboard)/templates/[id]/page.tsx | 2 +- .../templates/data-table-action-dropdown.tsx | 2 +- .../templates/data-table-templates.tsx | 4 +- .../templates/new-template-dialog.tsx | 39 ++++++---- .../src/app/(dashboard)/templates/page.tsx | 9 ++- .../(dashboard)/layout/desktop-nav.tsx | 37 +++++---- .../components/(dashboard)/layout/header.tsx | 2 +- .../add-template-fields.action.ts | 4 +- .../add-template-placeholders.action.ts | 4 +- packages/ui/primitives/dialog.tsx | 2 +- .../template-flow/add-template-fields.tsx | 22 +++--- .../add-template-placeholder-recipients.tsx | 78 ++++++++----------- 13 files changed, 121 insertions(+), 111 deletions(-) diff --git a/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx b/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx index b4d20b60d..920cac247 100644 --- a/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx +++ b/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx @@ -4,19 +4,20 @@ import { useState } from 'react'; import { useRouter } from 'next/navigation'; -import { DocumentData, Field, Recipient, Template, User } from '@documenso/prisma/client'; +import type { DocumentData, Field, Recipient, Template, User } from '@documenso/prisma/client'; import { cn } from '@documenso/ui/lib/utils'; import { Card, CardContent } from '@documenso/ui/primitives/card'; import { DocumentFlowFormContainer, DocumentFlowFormContainerHeader, } from '@documenso/ui/primitives/document-flow/document-flow-root'; -import { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types'; +import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types'; import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; +import { Stepper } from '@documenso/ui/primitives/stepper'; import { AddTemplateFieldsFormPartial } from '@documenso/ui/primitives/template-flow/add-template-fields'; -import { TAddTemplateFieldsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-fields.types'; +import type { TAddTemplateFieldsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-fields.types'; import { AddTemplatePlaceholderRecipientsFormPartial } from '@documenso/ui/primitives/template-flow/add-template-placeholder-recipients'; -import { TAddTemplatePlacholderRecipientsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-placeholder-recipients.types'; +import type { TAddTemplatePlacholderRecipientsFormSchema } from '@documenso/ui/primitives/template-flow/add-template-placeholder-recipients.types'; import { useToast } from '@documenso/ui/primitives/use-toast'; import { addTemplateFields } from '~/components/forms/edit-template/add-template-fields.action'; @@ -32,6 +33,7 @@ export type EditTemplateFormProps = { }; type EditTemplateStep = 'signers' | 'fields'; +const EditTemplateSteps: EditTemplateStep[] = ['signers', 'fields']; export const EditTemplateForm = ({ className, @@ -56,7 +58,6 @@ export const EditTemplateForm = ({ title: 'Add Fields', description: 'Add all relevant fields for each recipient.', stepIndex: 2, - onBackStep: () => setStep('signers'), }, }; @@ -118,33 +119,35 @@ export const EditTemplateForm = ({
- e.preventDefault()}> + e.preventDefault()} + > - {step === 'signers' && ( + setStep(EditTemplateSteps[step - 1])} + > - )} - {step === 'fields' && ( - )} +
diff --git a/apps/web/src/app/(dashboard)/templates/[id]/page.tsx b/apps/web/src/app/(dashboard)/templates/[id]/page.tsx index b8c645c80..15eaa6f3c 100644 --- a/apps/web/src/app/(dashboard)/templates/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/templates/[id]/page.tsx @@ -5,7 +5,7 @@ import { redirect } from 'next/navigation'; import { ChevronLeft } from 'lucide-react'; -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getFieldsForTemplate } from '@documenso/lib/server-only/field/get-fields-for-template'; import { getRecipientsForTemplate } from '@documenso/lib/server-only/recipient/get-recipients-for-template'; import { getTemplateById } from '@documenso/lib/server-only/template/get-template-by-id'; diff --git a/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx index 15ad9b58b..9f26d632c 100644 --- a/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx +++ b/apps/web/src/app/(dashboard)/templates/data-table-action-dropdown.tsx @@ -7,7 +7,7 @@ import Link from 'next/link'; import { Copy, Edit, MoreHorizontal, Trash2 } from 'lucide-react'; import { useSession } from 'next-auth/react'; -import { Template } from '@documenso/prisma/client'; +import type { Template } from '@documenso/prisma/client'; import { DropdownMenu, DropdownMenuContent, diff --git a/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx b/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx index 3cc8102e7..629204c2a 100644 --- a/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx +++ b/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx @@ -7,7 +7,7 @@ import { useRouter } from 'next/navigation'; import { Loader, Plus } from 'lucide-react'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; -import { Template } from '@documenso/prisma/client'; +import type { Template } from '@documenso/prisma/client'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { DataTable } from '@documenso/ui/primitives/data-table'; @@ -109,7 +109,7 @@ export const TemplatesDataTable = ({ }} > {!isRowLoading && } - Use + Use Template
diff --git a/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx index 7de1355a7..19a465001 100644 --- a/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx +++ b/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx @@ -1,11 +1,12 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { zodResolver } from '@hookform/resolvers/zod'; import { FilePlus, X } from 'lucide-react'; +import { useSession } from 'next-auth/react'; import { useForm } from 'react-hook-form'; import * as z from 'zod'; @@ -18,7 +19,6 @@ import { Card, CardContent } from '@documenso/ui/primitives/card'; import { Dialog, DialogContent, - DialogDescription, DialogHeader, DialogTitle, DialogTrigger, @@ -45,7 +45,9 @@ type TCreateTemplateFormSchema = z.infer; export const NewTemplateDialog = () => { const router = useRouter(); + const { data: session } = useSession(); const { toast } = useToast(); + const form = useForm({ resolver: zodResolver(ZCreateTemplateFormSchema), defaultValues: { @@ -128,23 +130,29 @@ export const NewTemplateDialog = () => { setUploadedFile(null); }; + useEffect(() => { + if (!showNewTemplateDialog) { + form.reset(); + } + }, [form, showNewTemplateDialog]); + return ( - + New Template - + +
- + {
+
{uploadedFile ? ( -
resetForm()} - className="absolute right-2 top-2 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none" + 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" > - + Remove Template -
+
+

Uploaded Document

@@ -210,7 +221,7 @@ export const NewTemplateDialog = () => {
- +
); diff --git a/apps/web/src/app/(dashboard)/templates/page.tsx b/apps/web/src/app/(dashboard)/templates/page.tsx index bc6a90b12..f4167e42a 100644 --- a/apps/web/src/app/(dashboard)/templates/page.tsx +++ b/apps/web/src/app/(dashboard)/templates/page.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-session'; +import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getTemplates } from '@documenso/lib/server-only/template/get-templates'; import { TemplatesDataTable } from './data-table-templates'; @@ -27,9 +27,12 @@ export default async function TemplatesPage({ searchParams = {} }: TemplatesPage return (
-
+

Templates

- + +
+ +
diff --git a/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx b/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx index bb3384d0a..e04bc2818 100644 --- a/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx +++ b/apps/web/src/components/(dashboard)/layout/desktop-nav.tsx @@ -41,9 +41,29 @@ export const DesktopNav = ({ className, ...props }: DesktopNavProps) => { return (
- - {navigationLinks.map(({ href, label }) => ( - - {label} - - ))}
); }; diff --git a/apps/web/src/components/(dashboard)/layout/header.tsx b/apps/web/src/components/(dashboard)/layout/header.tsx index 25f260575..cf8873a1a 100644 --- a/apps/web/src/components/(dashboard)/layout/header.tsx +++ b/apps/web/src/components/(dashboard)/layout/header.tsx @@ -49,7 +49,7 @@ export const Header = ({ className, user, ...props }: HeaderProps) => { -
+
{/* -
diff --git a/apps/web/src/app/(dashboard)/templates/duplicate-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/duplicate-template-dialog.tsx index 5c3118035..be743ff48 100644 --- a/apps/web/src/app/(dashboard)/templates/duplicate-template-dialog.tsx +++ b/apps/web/src/app/(dashboard)/templates/duplicate-template-dialog.tsx @@ -47,8 +47,6 @@ export const DuplicateTemplateDialog = ({ await duplicateTemplate({ templateId: id, }); - - router.refresh(); } catch (err) { toast({ title: 'Error', diff --git a/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx index 19a465001..a4aa9bce2 100644 --- a/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx +++ b/apps/web/src/app/(dashboard)/templates/new-template-dialog.tsx @@ -49,10 +49,10 @@ export const NewTemplateDialog = () => { const { toast } = useToast(); const form = useForm({ - resolver: zodResolver(ZCreateTemplateFormSchema), defaultValues: { name: '', }, + resolver: zodResolver(ZCreateTemplateFormSchema), }); const { mutateAsync: createTemplate, isLoading: isCreatingTemplate } = diff --git a/packages/lib/server-only/admin/get-recipients-stats.ts b/packages/lib/server-only/admin/get-recipients-stats.ts index b6663e988..07368b5a1 100644 --- a/packages/lib/server-only/admin/get-recipients-stats.ts +++ b/packages/lib/server-only/admin/get-recipients-stats.ts @@ -19,10 +19,11 @@ export const getRecipientsStats = async () => { results.forEach((result) => { const { readStatus, signingStatus, sendStatus, _count } = result; - stats[readStatus as keyof typeof stats] += _count; - stats.TOTAL_RECIPIENTS += _count; - stats[signingStatus as keyof typeof stats] += _count; - stats[sendStatus as keyof typeof stats] += _count; + + stats[readStatus] += _count; + stats[signingStatus] += _count; + stats[sendStatus] += _count; + stats.TOTAL_RECIPIENTS += _count; }); diff --git a/packages/lib/server-only/field/set-fields-for-document.ts b/packages/lib/server-only/field/set-fields-for-document.ts index 664be3b91..bd14d49b2 100644 --- a/packages/lib/server-only/field/set-fields-for-document.ts +++ b/packages/lib/server-only/field/set-fields-for-document.ts @@ -1,5 +1,6 @@ import { prisma } from '@documenso/prisma'; -import { FieldType, SendStatus, SigningStatus } from '@documenso/prisma/client'; +import type { FieldType } from '@documenso/prisma/client'; +import { SendStatus, SigningStatus } from '@documenso/prisma/client'; export interface SetFieldsForDocumentOptions { userId: number; diff --git a/packages/lib/server-only/field/set-fields-for-template.ts b/packages/lib/server-only/field/set-fields-for-template.ts index 6e2e39afc..9431666bf 100644 --- a/packages/lib/server-only/field/set-fields-for-template.ts +++ b/packages/lib/server-only/field/set-fields-for-template.ts @@ -1,5 +1,5 @@ import { prisma } from '@documenso/prisma'; -import { FieldType } from '@documenso/prisma/client'; +import type { FieldType } from '@documenso/prisma/client'; export type Field = { id?: number | null; @@ -32,7 +32,7 @@ export const setFieldsForTemplate = async ({ }); if (!template) { - throw new Error('Document not found'); + throw new Error('Template not found'); } const existingFields = await prisma.field.findMany({ @@ -93,8 +93,10 @@ export const setFieldsForTemplate = async ({ }, Recipient: { connect: { - id: field.signerId, - email: field.signerEmail, + templateId_email: { + templateId, + email: field.signerEmail.toLowerCase(), + }, }, }, }, diff --git a/packages/prisma/migrations/20231221070056_make_recipient_status_columns_non_null_again/migration.sql b/packages/prisma/migrations/20231221070056_make_recipient_status_columns_non_null_again/migration.sql new file mode 100644 index 000000000..d2ebc6405 --- /dev/null +++ b/packages/prisma/migrations/20231221070056_make_recipient_status_columns_non_null_again/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Made the column `readStatus` on table `Recipient` required. This step will fail if there are existing NULL values in that column. + - Made the column `signingStatus` on table `Recipient` required. This step will fail if there are existing NULL values in that column. + - Made the column `sendStatus` on table `Recipient` required. This step will fail if there are existing NULL values in that column. + +*/ +-- AlterTable +ALTER TABLE "Recipient" ALTER COLUMN "readStatus" SET NOT NULL, +ALTER COLUMN "signingStatus" SET NOT NULL, +ALTER COLUMN "sendStatus" SET NOT NULL; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index eb34ae903..67fb182a7 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -189,9 +189,9 @@ model Recipient { token String expired DateTime? signedAt DateTime? - readStatus ReadStatus? @default(NOT_OPENED) - signingStatus SigningStatus? @default(NOT_SIGNED) - sendStatus SendStatus? @default(NOT_SENT) + readStatus ReadStatus @default(NOT_OPENED) + signingStatus SigningStatus @default(NOT_SIGNED) + sendStatus SendStatus @default(NOT_SENT) Document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade) Template Template? @relation(fields: [templateId], references: [id], onDelete: Cascade) Field Field[] From 519c645d0670f9ad75d1f3c1ea9a2ece41df3ca5 Mon Sep 17 00:00:00 2001 From: harkiratsm Date: Thu, 21 Dec 2023 15:36:24 +0530 Subject: [PATCH 22/53] fix: hotkeys was overlapping with the browser hotkeys Signed-off-by: harkiratsm --- apps/web/src/components/(dashboard)/common/command-menu.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/src/components/(dashboard)/common/command-menu.tsx b/apps/web/src/components/(dashboard)/common/command-menu.tsx index 2e352b45a..19a35874e 100644 --- a/apps/web/src/components/(dashboard)/common/command-menu.tsx +++ b/apps/web/src/components/(dashboard)/common/command-menu.tsx @@ -85,7 +85,8 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { const currentPage = pages[pages.length - 1]; - const toggleOpen = () => { + const toggleOpen = (e: KeyboardEvent) => { + e.preventDefault(); setIsOpen((isOpen) => !isOpen); onOpenChange?.(!isOpen); From 972c20f906c1c1b56265e8428a6bd1188a0db4ff Mon Sep 17 00:00:00 2001 From: Mythie Date: Thu, 21 Dec 2023 21:20:37 +1100 Subject: [PATCH 23/53] chore: tidy migrations --- .../20231007013737_templates/migration.sql | 52 ------------- .../migration.sql | 15 ---- .../migration.sql | 14 ---- .../migration.sql | 8 -- .../migration.sql | 8 -- .../migration.sql | 9 --- .../migration.sql | 51 ------------- .../20231017042227_fix_typo/migration.sql | 23 ------ .../migration.sql | 8 -- .../migration.sql | 8 -- .../migration.sql | 5 -- .../migration.sql | 10 --- .../migration.sql | 54 -------------- .../migration.sql | 8 -- .../migration.sql | 12 --- .../migration.sql | 73 +++++++++++++++++++ 16 files changed, 73 insertions(+), 285 deletions(-) delete mode 100644 packages/prisma/migrations/20231007013737_templates/migration.sql delete mode 100644 packages/prisma/migrations/20231007014431_templates_type/migration.sql delete mode 100644 packages/prisma/migrations/20231007021427_reuse_document_data/migration.sql delete mode 100644 packages/prisma/migrations/20231007033447_remove_inserted_on_template_field/migration.sql delete mode 100644 packages/prisma/migrations/20231007080315_document_name_to_template/migration.sql delete mode 100644 packages/prisma/migrations/20231007211915_template_created_date/migration.sql delete mode 100644 packages/prisma/migrations/20231017041643_placeholder_recipients/migration.sql delete mode 100644 packages/prisma/migrations/20231017042227_fix_typo/migration.sql delete mode 100644 packages/prisma/migrations/20231019043226_template_recipient_email/migration.sql delete mode 100644 packages/prisma/migrations/20231020032507_template_recipient_token/migration.sql delete mode 100644 packages/prisma/migrations/20231021193915_template_token_for_recipient/migration.sql delete mode 100644 packages/prisma/migrations/20231030061522_recipient_name_not_placeholder/migration.sql delete mode 100644 packages/prisma/migrations/20231208090322_remove_template_specific_models/migration.sql delete mode 100644 packages/prisma/migrations/20231214081915_remove_template_token_column/migration.sql delete mode 100644 packages/prisma/migrations/20231221070056_make_recipient_status_columns_non_null_again/migration.sql create mode 100644 packages/prisma/migrations/20231221101005_add_templates/migration.sql diff --git a/packages/prisma/migrations/20231007013737_templates/migration.sql b/packages/prisma/migrations/20231007013737_templates/migration.sql deleted file mode 100644 index e0c1bf4ec..000000000 --- a/packages/prisma/migrations/20231007013737_templates/migration.sql +++ /dev/null @@ -1,52 +0,0 @@ --- CreateEnum -CREATE TYPE "TemplateStatus" AS ENUM ('PUBLIC', 'PRIVATE'); - --- CreateTable -CREATE TABLE "Template" ( - "id" SERIAL NOT NULL, - "userId" INTEGER NOT NULL, - "title" TEXT NOT NULL, - "description" TEXT, - "status" "TemplateStatus" NOT NULL DEFAULT 'PRIVATE', - "templateDataId" TEXT NOT NULL, - - CONSTRAINT "Template_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "TemplateData" ( - "id" TEXT NOT NULL, - "type" "DocumentDataType" NOT NULL, - "data" TEXT NOT NULL, - "initialData" TEXT NOT NULL, - - CONSTRAINT "TemplateData_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "TemplateField" ( - "id" SERIAL NOT NULL, - "templateId" INTEGER NOT NULL, - "type" "FieldType" NOT NULL, - "page" INTEGER NOT NULL, - "positionX" DECIMAL(65,30) NOT NULL DEFAULT 0, - "positionY" DECIMAL(65,30) NOT NULL DEFAULT 0, - "width" DECIMAL(65,30) NOT NULL DEFAULT -1, - "height" DECIMAL(65,30) NOT NULL DEFAULT -1, - "customText" TEXT NOT NULL, - "inserted" BOOLEAN NOT NULL, - - CONSTRAINT "TemplateField_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "Template_templateDataId_key" ON "Template"("templateDataId"); - --- AddForeignKey -ALTER TABLE "Template" ADD CONSTRAINT "Template_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Template" ADD CONSTRAINT "Template_templateDataId_fkey" FOREIGN KEY ("templateDataId") REFERENCES "TemplateData"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TemplateField" ADD CONSTRAINT "TemplateField_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20231007014431_templates_type/migration.sql b/packages/prisma/migrations/20231007014431_templates_type/migration.sql deleted file mode 100644 index c89e09a61..000000000 --- a/packages/prisma/migrations/20231007014431_templates_type/migration.sql +++ /dev/null @@ -1,15 +0,0 @@ -/* - Warnings: - - - The `status` column on the `Template` table would be dropped and recreated. This will lead to data loss if there is data in the column. - -*/ --- CreateEnum -CREATE TYPE "TemplateType" AS ENUM ('PUBLIC', 'PRIVATE'); - --- AlterTable -ALTER TABLE "Template" DROP COLUMN "status", -ADD COLUMN "status" "TemplateType" NOT NULL DEFAULT 'PRIVATE'; - --- DropEnum -DROP TYPE "TemplateStatus"; diff --git a/packages/prisma/migrations/20231007021427_reuse_document_data/migration.sql b/packages/prisma/migrations/20231007021427_reuse_document_data/migration.sql deleted file mode 100644 index 629f292fc..000000000 --- a/packages/prisma/migrations/20231007021427_reuse_document_data/migration.sql +++ /dev/null @@ -1,14 +0,0 @@ -/* - Warnings: - - - You are about to drop the `TemplateData` table. If the table is not empty, all the data it contains will be lost. - -*/ --- DropForeignKey -ALTER TABLE "Template" DROP CONSTRAINT "Template_templateDataId_fkey"; - --- DropTable -DROP TABLE "TemplateData"; - --- AddForeignKey -ALTER TABLE "Template" ADD CONSTRAINT "Template_templateDataId_fkey" FOREIGN KEY ("templateDataId") REFERENCES "DocumentData"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20231007033447_remove_inserted_on_template_field/migration.sql b/packages/prisma/migrations/20231007033447_remove_inserted_on_template_field/migration.sql deleted file mode 100644 index 25ace4f72..000000000 --- a/packages/prisma/migrations/20231007033447_remove_inserted_on_template_field/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `inserted` on the `TemplateField` table. All the data in the column will be lost. - -*/ --- AlterTable -ALTER TABLE "TemplateField" DROP COLUMN "inserted"; diff --git a/packages/prisma/migrations/20231007080315_document_name_to_template/migration.sql b/packages/prisma/migrations/20231007080315_document_name_to_template/migration.sql deleted file mode 100644 index 45a52de3d..000000000 --- a/packages/prisma/migrations/20231007080315_document_name_to_template/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - Added the required column `documentName` to the `Template` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "Template" ADD COLUMN "documentName" TEXT NOT NULL; diff --git a/packages/prisma/migrations/20231007211915_template_created_date/migration.sql b/packages/prisma/migrations/20231007211915_template_created_date/migration.sql deleted file mode 100644 index 816da092e..000000000 --- a/packages/prisma/migrations/20231007211915_template_created_date/migration.sql +++ /dev/null @@ -1,9 +0,0 @@ -/* - Warnings: - - - Added the required column `updatedAt` to the `Template` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "Template" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, -ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; diff --git a/packages/prisma/migrations/20231017041643_placeholder_recipients/migration.sql b/packages/prisma/migrations/20231017041643_placeholder_recipients/migration.sql deleted file mode 100644 index b84a567c8..000000000 --- a/packages/prisma/migrations/20231017041643_placeholder_recipients/migration.sql +++ /dev/null @@ -1,51 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `description` on the `Template` table. All the data in the column will be lost. - - You are about to drop the column `documentName` on the `Template` table. All the data in the column will be lost. - - You are about to drop the column `status` on the `Template` table. All the data in the column will be lost. - - You are about to drop the column `templateDataId` on the `Template` table. All the data in the column will be lost. - - A unique constraint covering the columns `[tempateDocumentDataId]` on the table `Template` will be added. If there are existing duplicate values, this will fail. - - Added the required column `tempateDocumentDataId` to the `Template` table without a default value. This is not possible if the table is not empty. - - Added the required column `inserted` to the `TemplateField` table without a default value. This is not possible if the table is not empty. - -*/ --- DropForeignKey -ALTER TABLE "Template" DROP CONSTRAINT "Template_templateDataId_fkey"; - --- DropIndex -DROP INDEX "Template_templateDataId_key"; - --- AlterTable -ALTER TABLE "Template" DROP COLUMN "description", -DROP COLUMN "documentName", -DROP COLUMN "status", -DROP COLUMN "templateDataId", -ADD COLUMN "tempateDocumentDataId" TEXT NOT NULL, -ADD COLUMN "type" "TemplateType" NOT NULL DEFAULT 'PRIVATE', -ALTER COLUMN "updatedAt" SET DEFAULT CURRENT_TIMESTAMP; - --- AlterTable -ALTER TABLE "TemplateField" ADD COLUMN "inserted" BOOLEAN NOT NULL, -ADD COLUMN "recipientId" INTEGER; - --- CreateTable -CREATE TABLE "TemplateRecipient" ( - "id" SERIAL NOT NULL, - "templateId" INTEGER NOT NULL, - "placeholder" VARCHAR(255) NOT NULL, - - CONSTRAINT "TemplateRecipient_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "Template_tempateDocumentDataId_key" ON "Template"("tempateDocumentDataId"); - --- AddForeignKey -ALTER TABLE "Template" ADD CONSTRAINT "Template_tempateDocumentDataId_fkey" FOREIGN KEY ("tempateDocumentDataId") REFERENCES "DocumentData"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TemplateRecipient" ADD CONSTRAINT "TemplateRecipient_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "TemplateField" ADD CONSTRAINT "TemplateField_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "TemplateRecipient"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20231017042227_fix_typo/migration.sql b/packages/prisma/migrations/20231017042227_fix_typo/migration.sql deleted file mode 100644 index ac9eaf10e..000000000 --- a/packages/prisma/migrations/20231017042227_fix_typo/migration.sql +++ /dev/null @@ -1,23 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `tempateDocumentDataId` on the `Template` table. All the data in the column will be lost. - - A unique constraint covering the columns `[templateDocumentDataId]` on the table `Template` will be added. If there are existing duplicate values, this will fail. - - Added the required column `templateDocumentDataId` to the `Template` table without a default value. This is not possible if the table is not empty. - -*/ --- DropForeignKey -ALTER TABLE "Template" DROP CONSTRAINT "Template_tempateDocumentDataId_fkey"; - --- DropIndex -DROP INDEX "Template_tempateDocumentDataId_key"; - --- AlterTable -ALTER TABLE "Template" DROP COLUMN "tempateDocumentDataId", -ADD COLUMN "templateDocumentDataId" TEXT NOT NULL; - --- CreateIndex -CREATE UNIQUE INDEX "Template_templateDocumentDataId_key" ON "Template"("templateDocumentDataId"); - --- AddForeignKey -ALTER TABLE "Template" ADD CONSTRAINT "Template_templateDocumentDataId_fkey" FOREIGN KEY ("templateDocumentDataId") REFERENCES "DocumentData"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20231019043226_template_recipient_email/migration.sql b/packages/prisma/migrations/20231019043226_template_recipient_email/migration.sql deleted file mode 100644 index 266333794..000000000 --- a/packages/prisma/migrations/20231019043226_template_recipient_email/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - Added the required column `email` to the `TemplateRecipient` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "TemplateRecipient" ADD COLUMN "email" VARCHAR(255) NOT NULL; diff --git a/packages/prisma/migrations/20231020032507_template_recipient_token/migration.sql b/packages/prisma/migrations/20231020032507_template_recipient_token/migration.sql deleted file mode 100644 index 9ce1fa70a..000000000 --- a/packages/prisma/migrations/20231020032507_template_recipient_token/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - Added the required column `token` to the `TemplateRecipient` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "TemplateRecipient" ADD COLUMN "token" TEXT NOT NULL; diff --git a/packages/prisma/migrations/20231021193915_template_token_for_recipient/migration.sql b/packages/prisma/migrations/20231021193915_template_token_for_recipient/migration.sql deleted file mode 100644 index a6b3e7199..000000000 --- a/packages/prisma/migrations/20231021193915_template_token_for_recipient/migration.sql +++ /dev/null @@ -1,5 +0,0 @@ --- AlterTable -ALTER TABLE "Recipient" ADD COLUMN "templateToken" TEXT; - --- AlterTable -ALTER TABLE "TemplateRecipient" ADD COLUMN "templateToken" TEXT; diff --git a/packages/prisma/migrations/20231030061522_recipient_name_not_placeholder/migration.sql b/packages/prisma/migrations/20231030061522_recipient_name_not_placeholder/migration.sql deleted file mode 100644 index 8b9275c68..000000000 --- a/packages/prisma/migrations/20231030061522_recipient_name_not_placeholder/migration.sql +++ /dev/null @@ -1,10 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `placeholder` on the `TemplateRecipient` table. All the data in the column will be lost. - - Added the required column `name` to the `TemplateRecipient` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "TemplateRecipient" DROP COLUMN "placeholder", -ADD COLUMN "name" VARCHAR(255) NOT NULL; diff --git a/packages/prisma/migrations/20231208090322_remove_template_specific_models/migration.sql b/packages/prisma/migrations/20231208090322_remove_template_specific_models/migration.sql deleted file mode 100644 index 7e2af3ef8..000000000 --- a/packages/prisma/migrations/20231208090322_remove_template_specific_models/migration.sql +++ /dev/null @@ -1,54 +0,0 @@ -/* - Warnings: - - - You are about to drop the `TemplateField` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `TemplateRecipient` table. If the table is not empty, all the data it contains will be lost. - - A unique constraint covering the columns `[templateId,email]` on the table `Recipient` will be added. If there are existing duplicate values, this will fail. - -*/ --- DropForeignKey -ALTER TABLE "Field" DROP CONSTRAINT "Field_recipientId_fkey"; - --- DropForeignKey -ALTER TABLE "TemplateField" DROP CONSTRAINT "TemplateField_recipientId_fkey"; - --- DropForeignKey -ALTER TABLE "TemplateField" DROP CONSTRAINT "TemplateField_templateId_fkey"; - --- DropForeignKey -ALTER TABLE "TemplateRecipient" DROP CONSTRAINT "TemplateRecipient_templateId_fkey"; - --- AlterTable -ALTER TABLE "Field" ADD COLUMN "templateId" INTEGER, -ALTER COLUMN "documentId" DROP NOT NULL; - --- AlterTable -ALTER TABLE "Recipient" ADD COLUMN "templateId" INTEGER, -ALTER COLUMN "documentId" DROP NOT NULL, -ALTER COLUMN "readStatus" DROP NOT NULL, -ALTER COLUMN "signingStatus" DROP NOT NULL, -ALTER COLUMN "sendStatus" DROP NOT NULL; - --- DropTable -DROP TABLE "TemplateField"; - --- DropTable -DROP TABLE "TemplateRecipient"; - --- CreateIndex -CREATE INDEX "Field_templateId_idx" ON "Field"("templateId"); - --- CreateIndex -CREATE INDEX "Recipient_templateId_idx" ON "Recipient"("templateId"); - --- CreateIndex -CREATE UNIQUE INDEX "Recipient_templateId_email_key" ON "Recipient"("templateId", "email"); - --- AddForeignKey -ALTER TABLE "Recipient" ADD CONSTRAINT "Recipient_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Field" ADD CONSTRAINT "Field_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Field" ADD CONSTRAINT "Field_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Recipient"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/packages/prisma/migrations/20231214081915_remove_template_token_column/migration.sql b/packages/prisma/migrations/20231214081915_remove_template_token_column/migration.sql deleted file mode 100644 index 8514a14b7..000000000 --- a/packages/prisma/migrations/20231214081915_remove_template_token_column/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `templateToken` on the `Recipient` table. All the data in the column will be lost. - -*/ --- AlterTable -ALTER TABLE "Recipient" DROP COLUMN "templateToken"; diff --git a/packages/prisma/migrations/20231221070056_make_recipient_status_columns_non_null_again/migration.sql b/packages/prisma/migrations/20231221070056_make_recipient_status_columns_non_null_again/migration.sql deleted file mode 100644 index d2ebc6405..000000000 --- a/packages/prisma/migrations/20231221070056_make_recipient_status_columns_non_null_again/migration.sql +++ /dev/null @@ -1,12 +0,0 @@ -/* - Warnings: - - - Made the column `readStatus` on table `Recipient` required. This step will fail if there are existing NULL values in that column. - - Made the column `signingStatus` on table `Recipient` required. This step will fail if there are existing NULL values in that column. - - Made the column `sendStatus` on table `Recipient` required. This step will fail if there are existing NULL values in that column. - -*/ --- AlterTable -ALTER TABLE "Recipient" ALTER COLUMN "readStatus" SET NOT NULL, -ALTER COLUMN "signingStatus" SET NOT NULL, -ALTER COLUMN "sendStatus" SET NOT NULL; diff --git a/packages/prisma/migrations/20231221101005_add_templates/migration.sql b/packages/prisma/migrations/20231221101005_add_templates/migration.sql new file mode 100644 index 000000000..21b0a2918 --- /dev/null +++ b/packages/prisma/migrations/20231221101005_add_templates/migration.sql @@ -0,0 +1,73 @@ +/* + Warnings: + + - A unique constraint covering the columns `[templateId,email]` on the table `Recipient` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateEnum +CREATE TYPE "TemplateType" AS ENUM ('PUBLIC', 'PRIVATE'); + +-- DropForeignKey +ALTER TABLE "Field" DROP CONSTRAINT "Field_recipientId_fkey"; + +-- AlterTable +ALTER TABLE "Field" ADD COLUMN "templateId" INTEGER, +ALTER COLUMN "documentId" DROP NOT NULL; + +-- AlterTable +-- Add CHECK constraint to ensure that only one of the two columns is set +ALTER TABLE "Field" ADD CONSTRAINT "Field_templateId_documentId_check" CHECK ( + ("templateId" IS NULL AND "documentId" IS NOT NULL) OR + ("templateId" IS NOT NULL AND "documentId" IS NULL) +); + +-- AlterTable +ALTER TABLE "Recipient" ADD COLUMN "templateId" INTEGER, +ALTER COLUMN "documentId" DROP NOT NULL; + +-- AlterTable +-- Add CHECK constraint to ensure that only one of the two columns is set +ALTER TABLE "Recipient" ADD CONSTRAINT "Recipient_templateId_documentId_check" CHECK ( + ("templateId" IS NULL AND "documentId" IS NOT NULL) OR + ("templateId" IS NOT NULL AND "documentId" IS NULL) +); + +-- CreateTable +CREATE TABLE "Template" ( + "id" SERIAL NOT NULL, + "type" "TemplateType" NOT NULL DEFAULT 'PRIVATE', + "title" TEXT NOT NULL, + "userId" INTEGER NOT NULL, + "templateDocumentDataId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Template_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Template_templateDocumentDataId_key" ON "Template"("templateDocumentDataId"); + +-- CreateIndex +CREATE INDEX "Field_templateId_idx" ON "Field"("templateId"); + +-- CreateIndex +CREATE INDEX "Recipient_templateId_idx" ON "Recipient"("templateId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Recipient_templateId_email_key" ON "Recipient"("templateId", "email"); + +-- AddForeignKey +ALTER TABLE "Recipient" ADD CONSTRAINT "Recipient_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Field" ADD CONSTRAINT "Field_templateId_fkey" FOREIGN KEY ("templateId") REFERENCES "Template"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Field" ADD CONSTRAINT "Field_recipientId_fkey" FOREIGN KEY ("recipientId") REFERENCES "Recipient"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Template" ADD CONSTRAINT "Template_templateDocumentDataId_fkey" FOREIGN KEY ("templateDocumentDataId") REFERENCES "DocumentData"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Template" ADD CONSTRAINT "Template_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; From 9ad94f986269d1fa85eb44dd6ae1c6e761e4d048 Mon Sep 17 00:00:00 2001 From: Mythie Date: Thu, 21 Dec 2023 21:37:33 +1100 Subject: [PATCH 24/53] fix: updates from review --- .../app/(dashboard)/documents/data-table-action-dropdown.tsx | 2 +- apps/web/src/app/(dashboard)/templates/[id]/page.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx b/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx index f1cbcc147..b8031b088 100644 --- a/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx +++ b/apps/web/src/app/(dashboard)/documents/data-table-action-dropdown.tsx @@ -128,7 +128,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = Download - + setDuplicateDialogOpen(true)}> Duplicate diff --git a/apps/web/src/app/(dashboard)/templates/[id]/page.tsx b/apps/web/src/app/(dashboard)/templates/[id]/page.tsx index 15eaa6f3c..6d234eff2 100644 --- a/apps/web/src/app/(dashboard)/templates/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/templates/[id]/page.tsx @@ -43,11 +43,11 @@ export default async function TemplatePage({ params }: TemplatePageProps) { const { templateDocumentData } = template; const [templateRecipients, templateFields] = await Promise.all([ - await getRecipientsForTemplate({ + getRecipientsForTemplate({ templateId, userId: user.id, }), - await getFieldsForTemplate({ + getFieldsForTemplate({ templateId, userId: user.id, }), From 1aa0fc3101e2967e225b738326f364e3d98ecc54 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Fri, 22 Dec 2023 01:46:41 +0000 Subject: [PATCH 25/53] fix: remove loadingText prop --- .vscode/settings.json | 2 +- apps/web/src/components/forms/forgot-password.tsx | 4 ++-- apps/web/src/components/forms/password.tsx | 6 +++--- apps/web/src/components/forms/profile.tsx | 6 +++--- apps/web/src/components/forms/reset-password.tsx | 4 ++-- apps/web/src/components/forms/signin.tsx | 7 +++---- apps/web/src/components/forms/signup.tsx | 3 +-- packages/ui/primitives/button.tsx | 12 ++++-------- 8 files changed, 19 insertions(+), 25 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 97d5d1948..82aa3c1a3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { "typescript.tsdk": "node_modules/typescript/lib", "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "eslint.validate": ["typescript", "typescriptreact", "javascript", "javascriptreact"], "javascript.preferences.importModuleSpecifier": "non-relative", diff --git a/apps/web/src/components/forms/forgot-password.tsx b/apps/web/src/components/forms/forgot-password.tsx index 55e313c33..3d9efee42 100644 --- a/apps/web/src/components/forms/forgot-password.tsx +++ b/apps/web/src/components/forms/forgot-password.tsx @@ -82,8 +82,8 @@ export const ForgotPasswordForm = ({ className }: ForgotPasswordFormProps) => { /> - diff --git a/apps/web/src/components/forms/password.tsx b/apps/web/src/components/forms/password.tsx index b6b41264c..0eb491537 100644 --- a/apps/web/src/components/forms/password.tsx +++ b/apps/web/src/components/forms/password.tsx @@ -4,7 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; -import { User } from '@documenso/prisma/client'; +import type { User } from '@documenso/prisma/client'; import { TRPCClientError } from '@documenso/trpc/client'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; @@ -146,8 +146,8 @@ export const PasswordForm = ({ className }: PasswordFormProps) => {
-
diff --git a/apps/web/src/components/forms/profile.tsx b/apps/web/src/components/forms/profile.tsx index dc6e3c4d5..0ce5c7f3d 100644 --- a/apps/web/src/components/forms/profile.tsx +++ b/apps/web/src/components/forms/profile.tsx @@ -6,7 +6,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; -import { User } from '@documenso/prisma/client'; +import type { User } from '@documenso/prisma/client'; import { TRPCClientError } from '@documenso/trpc/client'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; @@ -133,8 +133,8 @@ export const ProfileForm = ({ className, user }: ProfileFormProps) => { /> - diff --git a/apps/web/src/components/forms/reset-password.tsx b/apps/web/src/components/forms/reset-password.tsx index e7c701667..354584f6e 100644 --- a/apps/web/src/components/forms/reset-password.tsx +++ b/apps/web/src/components/forms/reset-password.tsx @@ -125,8 +125,8 @@ export const ResetPasswordForm = ({ className, token }: ResetPasswordFormProps) /> - diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx index 46173a97c..4e671a569 100644 --- a/apps/web/src/components/forms/signin.tsx +++ b/apps/web/src/components/forms/signin.tsx @@ -199,9 +199,8 @@ export const SignInForm = ({ className }: SignInFormProps) => { size="lg" loading={isSubmitting} className="dark:bg-documenso dark:hover:opacity-90" - loadingText="Signing in..." > - Sign In + {isSubmitting ? 'Signing in...' : 'Sign In'}
@@ -275,8 +274,8 @@ export const SignInForm = ({ className }: SignInFormProps) => { {twoFactorAuthenticationMethod === 'totp' ? 'Use Backup Code' : 'Use Authenticator'} -
diff --git a/apps/web/src/components/forms/signup.tsx b/apps/web/src/components/forms/signup.tsx index 3988314c5..b91b4a9fd 100644 --- a/apps/web/src/components/forms/signup.tsx +++ b/apps/web/src/components/forms/signup.tsx @@ -162,10 +162,9 @@ export const SignUpForm = ({ className }: SignUpFormProps) => { type="submit" size="lg" loading={isSubmitting} - loadingText="Signing up..." className="dark:bg-documenso dark:hover:opacity-90" > - Sign Up + {isSubmitting ? 'Signing up...' : 'Sign Up'} diff --git a/packages/ui/primitives/button.tsx b/packages/ui/primitives/button.tsx index 9ee3324c6..5754b35a5 100644 --- a/packages/ui/primitives/button.tsx +++ b/packages/ui/primitives/button.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; import { Slot } from '@radix-ui/react-slot'; -import { VariantProps, cva } from 'class-variance-authority'; +import type { VariantProps } from 'class-variance-authority'; +import { cva } from 'class-variance-authority'; import { Loader } from 'lucide-react'; import { cn } from '../lib/utils'; @@ -53,15 +54,10 @@ export interface ButtonProps * Will display the loading spinner and disable the button. */ loading?: boolean; - - /** - * The label to show in the button when `isLoading` is true - */ - loadingText?: string; } const Button = React.forwardRef( - ({ className, variant, size, asChild = false, loadingText, loading, ...props }, ref) => { + ({ className, variant, size, asChild = false, loading, ...props }, ref) => { if (asChild) { return ( @@ -79,7 +75,7 @@ const Button = React.forwardRef( disabled={isDisabled} > {isLoading && } - {isLoading && loadingText ? loadingText : props.children} + {props.children} ); }, From 5a5d00fb2e6fe6eba1720ead71f54ac9c4ad2548 Mon Sep 17 00:00:00 2001 From: JA <51177379+ubinatus@users.noreply.github.com> Date: Thu, 21 Dec 2023 22:14:33 -0500 Subject: [PATCH 26/53] fix(webapp): reset delete document dialog (#762) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR makes a small but useful tweak to the `DeleteDocumentDialog`. Now, the input field gets cleared whenever the dialog is opened. Here’s what’s changed: 1. **Clear Field After Deleting**: After you delete something and open the dialog again, it won’t show the old, deleted text anymore. It’s clean and ready for the next delete. 2. **Type Again to Confirm**: If you type something but close the dialog without deleting, you’ll have to type it again next time. This way, it makes sure the user really mean to delete something and didn't do it by mistake. Demo Link: See the old vs. new in action here: https://www.loom.com/share/80eca0d3b1994f7cbcab6f222db2dbfe?sid=ebc6135c-345e-4640-b395-daff190a96e7 It’s a small change, but it makes the delete process safer and smoother. --- .../app/(dashboard)/documents/delete-document-dialog.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx b/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx index 5b4a84286..0dd238b4e 100644 --- a/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx +++ b/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; @@ -50,6 +50,13 @@ export const DeleteDocumentDialog = ({ }, }); + useEffect(() => { + if (open) { + setInputValue(''); + setIsDeleteEnabled(status === DocumentStatus.DRAFT); + } + }, [open, status]); + const onDelete = async () => { try { await deleteDocument({ id, status }); From 1c52c7ebcdb98416bc01dbb3bbb7ce37cf4c6946 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Fri, 22 Dec 2023 03:43:12 +0000 Subject: [PATCH 27/53] chore: update copy --- .../documents/delete-document-dialog.tsx | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx b/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx index 7b82f93bc..3914d65de 100644 --- a/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx +++ b/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx @@ -41,16 +41,10 @@ export const DeleteDocumentDialog = ({ const { mutateAsync: deleteDocument, isLoading } = trpcReact.document.deleteDocument.useMutation({ onSuccess: () => { router.refresh(); - const deletedFileToastDescription = ( -

- Your document {documentTitle} has been - successfully deleted. -

- ); toast({ title: 'Document deleted', - description: deletedFileToastDescription, + description: `"${documentTitle}" has been successfully deleted`, duration: 5000, }); @@ -80,10 +74,7 @@ export const DeleteDocumentDialog = ({ !isLoading && onOpenChange(value)}> - - Do you want to delete the {documentTitle}{' '} - document? - + Are you sure you want to delete "{documentTitle}"? Please note that this action is irreversible. Once confirmed, your document will be From 6d58e60a65b5fc55dd2e8a0e5ea201fd75528ad8 Mon Sep 17 00:00:00 2001 From: sadam Date: Sat, 23 Dec 2023 23:18:32 -0500 Subject: [PATCH 28/53] fix(url): change URL for cloning --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 24d932858..733bb1dde 100644 --- a/README.md +++ b/README.md @@ -115,10 +115,12 @@ To run Documenso locally, you will need Want to get up and running quickly? Follow these steps: -1. [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device. +1. [Fork this repository ](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks) to your github account. + +After forking clone it using the below command ```sh -git clone https://github.com/documenso/documenso +git clone https://github.com//documenso ``` 2. Set up your `.env` file using the recommendations in the `.env.example` file. Alternatively, just run `cp .env.example .env` to get started with our handpicked defaults. @@ -152,10 +154,12 @@ npm run d Follow these steps to setup Documenso on your local machine: -1. [Clone the repository](https://help.github.com/articles/cloning-a-repository/) it to your local device. +1. [Fork this repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks) it to your github account. + +After forking clone it using the below command ```sh -git clone https://github.com/documenso/documenso +git clone https://github.com//documenso ``` 2. Run `npm i` in the root directory @@ -227,7 +231,7 @@ cp .env.example .env The following environment variables must be set: -* `NEXTAUTH_URL` +* `NEXTAUTH_URL` * `NEXTAUTH_SECRET` * `NEXT_PUBLIC_WEBAPP_URL` * `NEXT_PUBLIC_MARKETING_URL` From 2b25806c335e38c18d258df4504fef98b29ed0bf Mon Sep 17 00:00:00 2001 From: sadam Date: Sat, 23 Dec 2023 23:26:53 -0500 Subject: [PATCH 29/53] fix(url): change URL for cloning --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 733bb1dde..d4ce339c1 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ Want to get up and running quickly? Follow these steps: 1. [Fork this repository ](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks) to your github account. -After forking clone it using the below command +After forking clone it using the below command: ```sh git clone https://github.com//documenso @@ -156,7 +156,7 @@ Follow these steps to setup Documenso on your local machine: 1. [Fork this repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks) it to your github account. -After forking clone it using the below command +After forking clone it using the below command: ```sh git clone https://github.com//documenso From 8d1b960aa87e0e0358489b2b4facfc11515f546d Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Mon, 25 Dec 2023 23:16:56 +0000 Subject: [PATCH 30/53] feat: add templates to command menu --- .../components/(dashboard)/common/command-menu.tsx | 14 ++++++++++++++ packages/lib/constants/keyboard-shortcuts.ts | 1 + 2 files changed, 15 insertions(+) diff --git a/apps/web/src/components/(dashboard)/common/command-menu.tsx b/apps/web/src/components/(dashboard)/common/command-menu.tsx index 19a35874e..5be89343b 100644 --- a/apps/web/src/components/(dashboard)/common/command-menu.tsx +++ b/apps/web/src/components/(dashboard)/common/command-menu.tsx @@ -11,6 +11,7 @@ import { useHotkeys } from 'react-hotkeys-hook'; import { DOCUMENTS_PAGE_SHORTCUT, SETTINGS_PAGE_SHORTCUT, + TEMPLATES_PAGE_SHORTCUT, } from '@documenso/lib/constants/keyboard-shortcuts'; import { trpc as trpcReact } from '@documenso/trpc/react'; import { @@ -38,6 +39,14 @@ const DOCUMENTS_PAGES = [ { label: 'Inbox documents', path: '/documents?status=INBOX' }, ]; +const TEMPLATES_PAGES = [ + { + label: 'All templates', + path: '/templates', + shortcut: TEMPLATES_PAGE_SHORTCUT.replace('+', ''), + }, +]; + const SETTINGS_PAGES = [ { label: 'Settings', @@ -124,10 +133,12 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { const goToSettings = useCallback(() => push(SETTINGS_PAGES[0].path), [push]); const goToDocuments = useCallback(() => push(DOCUMENTS_PAGES[0].path), [push]); + const goToTemplates = useCallback(() => push(TEMPLATES_PAGES[0].path), [push]); useHotkeys(['ctrl+k', 'meta+k'], toggleOpen); useHotkeys(SETTINGS_PAGE_SHORTCUT, goToSettings); useHotkeys(DOCUMENTS_PAGE_SHORTCUT, goToDocuments); + useHotkeys(TEMPLATES_PAGE_SHORTCUT, goToTemplates); const handleKeyDown = (e: React.KeyboardEvent) => { // Escape goes to previous page @@ -174,6 +185,9 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { + + + diff --git a/packages/lib/constants/keyboard-shortcuts.ts b/packages/lib/constants/keyboard-shortcuts.ts index 896b4abf5..34d3a02e6 100644 --- a/packages/lib/constants/keyboard-shortcuts.ts +++ b/packages/lib/constants/keyboard-shortcuts.ts @@ -1,2 +1,3 @@ export const SETTINGS_PAGE_SHORTCUT = 'N+S'; export const DOCUMENTS_PAGE_SHORTCUT = 'N+D'; +export const TEMPLATES_PAGE_SHORTCUT = 'N+T'; From 5a32b5cafd3b5ebe6f2a4fdff0ce85c9a4849dae Mon Sep 17 00:00:00 2001 From: Apoorv Taneja Date: Tue, 26 Dec 2023 05:19:27 +0530 Subject: [PATCH 31/53] fix: added constants for theme variables (#777) fixes: #776 --- .../components/(dashboard)/common/command-menu.tsx | 7 ++++--- packages/ui/primitives/constants.ts | 5 +++++ packages/ui/primitives/theme-switcher.tsx | 14 ++++++++------ 3 files changed, 17 insertions(+), 9 deletions(-) create mode 100644 packages/ui/primitives/constants.ts diff --git a/apps/web/src/components/(dashboard)/common/command-menu.tsx b/apps/web/src/components/(dashboard)/common/command-menu.tsx index 19a35874e..fe690329b 100644 --- a/apps/web/src/components/(dashboard)/common/command-menu.tsx +++ b/apps/web/src/components/(dashboard)/common/command-menu.tsx @@ -22,6 +22,7 @@ import { CommandList, CommandShortcut, } from '@documenso/ui/primitives/command'; +import { THEMES_TYPE } from '@documenso/ui/primitives/constants'; const DOCUMENTS_PAGES = [ { @@ -215,9 +216,9 @@ const Commands = ({ const ThemeCommands = ({ setTheme }: { setTheme: (_theme: string) => void }) => { const THEMES = useMemo( () => [ - { label: 'Light Mode', theme: 'light', icon: Sun }, - { label: 'Dark Mode', theme: 'dark', icon: Moon }, - { label: 'System Theme', theme: 'system', icon: Monitor }, + { label: 'Light Mode', theme: THEMES_TYPE.LIGHT, icon: Sun }, + { label: 'Dark Mode', theme: THEMES_TYPE.DARK, icon: Moon }, + { label: 'System Theme', theme: THEMES_TYPE.SYSTEM, icon: Monitor }, ], [], ); diff --git a/packages/ui/primitives/constants.ts b/packages/ui/primitives/constants.ts new file mode 100644 index 000000000..9771eb35a --- /dev/null +++ b/packages/ui/primitives/constants.ts @@ -0,0 +1,5 @@ +export const THEMES_TYPE = { + DARK: 'dark', + LIGHT: 'light', + SYSTEM: 'system' +}; \ No newline at end of file diff --git a/packages/ui/primitives/theme-switcher.tsx b/packages/ui/primitives/theme-switcher.tsx index 7aa570749..fcc789404 100644 --- a/packages/ui/primitives/theme-switcher.tsx +++ b/packages/ui/primitives/theme-switcher.tsx @@ -4,6 +4,8 @@ import { useTheme } from 'next-themes'; import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted'; +import { THEMES_TYPE } from './constants'; + export const ThemeSwitcher = () => { const { theme, setTheme } = useTheme(); const isMounted = useIsMounted(); @@ -12,9 +14,9 @@ export const ThemeSwitcher = () => {
+ + {lines.length > 0 && ( +
+ +
+ )}
); }; From 32633f96d292056a22e855986ba795bfba3e4b5c Mon Sep 17 00:00:00 2001 From: hallidayo <22655069+Hallidayo@users.noreply.github.com> Date: Tue, 26 Dec 2023 23:50:40 +0000 Subject: [PATCH 37/53] feat: dateformat and timezone customization (#506) --- .../src/pages/api/stripe/webhook/index.ts | 5 +- .../app/(dashboard)/admin/users/[id]/page.tsx | 4 +- .../documents/[id]/edit-document.tsx | 8 +- .../(dashboard)/documents/upload-document.tsx | 1 + .../app/(signing)/sign/[token]/date-field.tsx | 36 +++++- .../(signing)/sign/[token]/email-field.tsx | 6 +- .../src/app/(signing)/sign/[token]/form.tsx | 7 +- .../app/(signing)/sign/[token]/name-field.tsx | 6 +- .../src/app/(signing)/sign/[token]/page.tsx | 13 +- .../sign/[token]/signature-field.tsx | 2 +- .../sign/[token]/signing-field-container.tsx | 24 +++- .../components/formatter/document-status.tsx | 4 +- .../src/components/formatter/locale-date.tsx | 6 +- package-lock.json | 6 + packages/lib/client-only/recipient-type.ts | 3 +- packages/lib/constants/date-formats.ts | 71 +++++++++++ packages/lib/constants/time-zones.ts | 44 +++++++ packages/lib/package.json | 1 + .../document-meta/upsert-document-meta.ts | 8 ++ .../document/duplicate-document-by-id.ts | 2 + .../get-document-meta-by-document-id.ts | 13 ++ .../server-only/document/update-document.ts | 2 +- .../field/sign-field-with-token.ts | 13 +- packages/lib/utils/recipient-formatter.ts | 2 +- .../migration.sql | 3 + .../migration.sql | 8 ++ packages/prisma/schema.prisma | 2 + packages/prisma/types/document-with-data.ts | 2 +- .../prisma/types/document-with-recipient.ts | 2 +- packages/prisma/types/field-with-signature.ts | 2 +- .../trpc/server/document-router/router.ts | 10 +- .../trpc/server/document-router/schema.ts | 4 +- packages/trpc/server/trpc.ts | 2 +- packages/ui/lib/utils.ts | 3 +- packages/ui/primitives/combobox.tsx | 71 +++++------ packages/ui/primitives/command.tsx | 2 +- .../document-flow/add-signature.tsx | 5 +- .../primitives/document-flow/add-subject.tsx | 119 ++++++++++++++++-- .../document-flow/add-subject.types.ts | 7 +- .../ui/primitives/multiselect-combobox.tsx | 82 ++++++++++++ 40 files changed, 517 insertions(+), 94 deletions(-) create mode 100644 packages/lib/constants/date-formats.ts create mode 100644 packages/lib/constants/time-zones.ts create mode 100644 packages/lib/server-only/document/get-document-meta-by-document-id.ts create mode 100644 packages/prisma/migrations/20231207134820_add_document_meta_dateformat_timezone/migration.sql create mode 100644 packages/prisma/migrations/20231214081915_remove_template_token_column/migration.sql create mode 100644 packages/ui/primitives/multiselect-combobox.tsx diff --git a/apps/marketing/src/pages/api/stripe/webhook/index.ts b/apps/marketing/src/pages/api/stripe/webhook/index.ts index 2bdcdeb50..a19cffda9 100644 --- a/apps/marketing/src/pages/api/stripe/webhook/index.ts +++ b/apps/marketing/src/pages/api/stripe/webhook/index.ts @@ -1,4 +1,4 @@ -import { NextApiRequest, NextApiResponse } from 'next'; +import type { NextApiRequest, NextApiResponse } from 'next'; import { randomBytes } from 'crypto'; import { buffer } from 'micro'; @@ -6,7 +6,8 @@ import { buffer } from 'micro'; import { insertImageInPDF } from '@documenso/lib/server-only/pdf/insert-image-in-pdf'; import { insertTextInPDF } from '@documenso/lib/server-only/pdf/insert-text-in-pdf'; import { redis } from '@documenso/lib/server-only/redis'; -import { Stripe, stripe } from '@documenso/lib/server-only/stripe'; +import type { Stripe } from '@documenso/lib/server-only/stripe'; +import { stripe } from '@documenso/lib/server-only/stripe'; import { getFile } from '@documenso/lib/universal/upload/get-file'; import { updateFile } from '@documenso/lib/universal/upload/update-file'; import { prisma } from '@documenso/prisma'; diff --git a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx index 3baf5d63b..9ae270d28 100644 --- a/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/[id]/page.tsx @@ -9,7 +9,6 @@ import type { z } from 'zod'; import { trpc } from '@documenso/trpc/react'; import { ZUpdateProfileMutationByAdminSchema } from '@documenso/trpc/server/admin-router/schema'; import { Button } from '@documenso/ui/primitives/button'; -import { Combobox } from '@documenso/ui/primitives/combobox'; import { Form, FormControl, @@ -19,6 +18,7 @@ import { FormMessage, } from '@documenso/ui/primitives/form/form'; import { Input } from '@documenso/ui/primitives/input'; +import { MultiSelectCombobox } from '@documenso/ui/primitives/multiselect-combobox'; import { useToast } from '@documenso/ui/primitives/use-toast'; const ZUserFormSchema = ZUpdateProfileMutationByAdminSchema.omit({ id: true }); @@ -117,7 +117,7 @@ export default function UserPage({ params }: { params: { id: number } }) {
Roles - onChange(values)} /> diff --git a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx index ffce3bd6c..a5dc9e23e 100644 --- a/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/[id]/edit-document.tsx @@ -4,8 +4,8 @@ import { useState } from 'react'; import { useRouter } from 'next/navigation'; -import { DocumentStatus } from '@documenso/prisma/client'; import type { DocumentData, Field, Recipient, User } from '@documenso/prisma/client'; +import { DocumentStatus } from '@documenso/prisma/client'; import type { DocumentWithData } from '@documenso/prisma/types/document-with-data'; import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; @@ -145,14 +145,16 @@ export const EditDocumentForm = ({ }; const onAddSubjectFormSubmit = async (data: TAddSubjectFormSchema) => { - const { subject, message } = data.email; + const { subject, message, timezone, dateFormat } = data.meta; try { await sendDocument({ documentId: document.id, - email: { + meta: { subject, message, + timezone, + dateFormat, }, }); diff --git a/apps/web/src/app/(dashboard)/documents/upload-document.tsx b/apps/web/src/app/(dashboard)/documents/upload-document.tsx index 5e93495e3..04ba990d5 100644 --- a/apps/web/src/app/(dashboard)/documents/upload-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/upload-document.tsx @@ -25,6 +25,7 @@ export type UploadDocumentProps = { export const UploadDocument = ({ className }: UploadDocumentProps) => { const router = useRouter(); const analytics = useAnalytics(); + const { data: session } = useSession(); const { toast } = useToast(); diff --git a/apps/web/src/app/(signing)/sign/[token]/date-field.tsx b/apps/web/src/app/(signing)/sign/[token]/date-field.tsx index 9cff29c64..40d0f945a 100644 --- a/apps/web/src/app/(signing)/sign/[token]/date-field.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/date-field.tsx @@ -6,8 +6,13 @@ import { useRouter } from 'next/navigation'; import { Loader } from 'lucide-react'; -import { Recipient } from '@documenso/prisma/client'; -import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; +import { + DEFAULT_DOCUMENT_DATE_FORMAT, + convertToLocalSystemFormat, +} from '@documenso/lib/constants/date-formats'; +import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; +import type { Recipient } from '@documenso/prisma/client'; +import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { trpc } from '@documenso/trpc/react'; import { useToast } from '@documenso/ui/primitives/use-toast'; @@ -16,9 +21,16 @@ import { SigningFieldContainer } from './signing-field-container'; export type DateFieldProps = { field: FieldWithSignature; recipient: Recipient; + dateFormat?: string | null; + timezone?: string | null; }; -export const DateField = ({ field, recipient }: DateFieldProps) => { +export const DateField = ({ + field, + recipient, + dateFormat = DEFAULT_DOCUMENT_DATE_FORMAT, + timezone = DEFAULT_DOCUMENT_TIME_ZONE, +}: DateFieldProps) => { const router = useRouter(); const { toast } = useToast(); @@ -35,12 +47,18 @@ export const DateField = ({ field, recipient }: DateFieldProps) => { const isLoading = isSignFieldWithTokenLoading || isRemoveSignedFieldWithTokenLoading || isPending; + const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone); + + const isDifferentTimeZone = field.inserted && localDateString !== field.customText; + + const tooltipText = `"${field.customText}" will appear on the document as it has a timezone of "${timezone}".`; + const onSign = async () => { try { await signFieldWithToken({ token: recipient.token, fieldId: field.id, - value: '', + value: dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT, }); startTransition(() => router.refresh()); @@ -75,7 +93,13 @@ export const DateField = ({ field, recipient }: DateFieldProps) => { }; return ( - + {isLoading && (
@@ -87,7 +111,7 @@ export const DateField = ({ field, recipient }: DateFieldProps) => { )} {field.inserted && ( -

{field.customText}

+

{localDateString}

)} ); diff --git a/apps/web/src/app/(signing)/sign/[token]/email-field.tsx b/apps/web/src/app/(signing)/sign/[token]/email-field.tsx index f6f790799..4d52ca50a 100644 --- a/apps/web/src/app/(signing)/sign/[token]/email-field.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/email-field.tsx @@ -6,8 +6,8 @@ import { useRouter } from 'next/navigation'; import { Loader } from 'lucide-react'; -import { Recipient } from '@documenso/prisma/client'; -import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; +import type { Recipient } from '@documenso/prisma/client'; +import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { trpc } from '@documenso/trpc/react'; import { useToast } from '@documenso/ui/primitives/use-toast'; @@ -79,7 +79,7 @@ export const EmailField = ({ field, recipient }: EmailFieldProps) => { }; return ( - + {isLoading && (
diff --git a/apps/web/src/app/(signing)/sign/[token]/form.tsx b/apps/web/src/app/(signing)/sign/[token]/form.tsx index 29cd77995..4f20a8199 100644 --- a/apps/web/src/app/(signing)/sign/[token]/form.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/form.tsx @@ -34,6 +34,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) = const { data: session } = useSession(); const { fullName, signature, setFullName, setSignature } = useRequiredSigningContext(); + const [validateUninsertedFields, setValidateUninsertedFields] = useState(false); const { mutateAsync: completeDocumentWithToken } = @@ -92,7 +93,11 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) = disabled={isSubmitting} className={cn('-mx-2 flex flex-1 flex-col overflow-hidden px-2')} > -
+

Sign Document

diff --git a/apps/web/src/app/(signing)/sign/[token]/name-field.tsx b/apps/web/src/app/(signing)/sign/[token]/name-field.tsx index bbe18fb8a..6e661e77a 100644 --- a/apps/web/src/app/(signing)/sign/[token]/name-field.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/name-field.tsx @@ -6,8 +6,8 @@ import { useRouter } from 'next/navigation'; import { Loader } from 'lucide-react'; -import { Recipient } from '@documenso/prisma/client'; -import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; +import type { Recipient } from '@documenso/prisma/client'; +import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { trpc } from '@documenso/trpc/react'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, DialogContent, DialogFooter, DialogTitle } from '@documenso/ui/primitives/dialog'; @@ -98,7 +98,7 @@ export const NameField = ({ field, recipient }: NameFieldProps) => { }; return ( - + {isLoading && (

diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index 97babb82f..18b81696e 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -2,9 +2,12 @@ import { notFound, redirect } from 'next/navigation'; import { match } from 'ts-pattern'; +import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; +import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; import { getServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getDocumentAndSenderByToken } from '@documenso/lib/server-only/document/get-document-by-token'; +import { getDocumentMetaByDocumentId } from '@documenso/lib/server-only/document/get-document-meta-by-document-id'; import { viewedDocument } from '@documenso/lib/server-only/document/viewed-document'; import { getFieldsForToken } from '@documenso/lib/server-only/field/get-fields-for-token'; import { getRecipientByToken } from '@documenso/lib/server-only/recipient/get-recipient-by-token'; @@ -42,6 +45,8 @@ export default async function SigningPage({ params: { token } }: SigningPageProp viewedDocument({ token }).catch(() => null), ]); + const documentMeta = await getDocumentMetaByDocumentId({ id: document!.id }).catch(() => null); + if (!document || !document.documentData || !recipient) { return notFound(); } @@ -111,7 +116,13 @@ export default async function SigningPage({ params: { token } }: SigningPageProp )) .with(FieldType.DATE, () => ( - + )) .with(FieldType.EMAIL, () => ( diff --git a/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx b/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx index ec3e45fe5..220d3084a 100644 --- a/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/signature-field.tsx @@ -127,7 +127,7 @@ export const SignatureField = ({ field, recipient }: SignatureFieldProps) => { }; return ( - + {isLoading && (
diff --git a/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx b/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx index 046e5b3df..b4805fa6b 100644 --- a/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/signing-field-container.tsx @@ -2,8 +2,9 @@ import React from 'react'; -import { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; +import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; import { FieldRootContainer } from '@documenso/ui/components/field/field'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@documenso/ui/primitives/tooltip'; export type SignatureFieldProps = { field: FieldWithSignature; @@ -11,6 +12,8 @@ export type SignatureFieldProps = { children: React.ReactNode; onSign?: () => Promise | void; onRemove?: () => Promise | void; + type?: 'Date' | 'Email' | 'Name' | 'Signature'; + tooltipText?: string | null; }; export const SigningFieldContainer = ({ @@ -19,6 +22,8 @@ export const SigningFieldContainer = ({ onSign, onRemove, children, + type, + tooltipText, }: SignatureFieldProps) => { const onSignFieldClick = async () => { if (field.inserted) { @@ -46,7 +51,22 @@ export const SigningFieldContainer = ({ /> )} - {field.inserted && !loading && ( + {type === 'Date' && field.inserted && !loading && ( + + + + + + {tooltipText && {tooltipText}} + + )} + + {type !== 'Date' && field.inserted && !loading && ( - + + - + + No value found. - - {allRoles.map((value: string, i: number) => ( - handleSelect(value)}> + + + {options.map((option, index) => ( + onOptionSelected(option)}> - {value} + + {option} ))} diff --git a/packages/ui/primitives/command.tsx b/packages/ui/primitives/command.tsx index 67cd3f487..cbc306c66 100644 --- a/packages/ui/primitives/command.tsx +++ b/packages/ui/primitives/command.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; -import { DialogProps } from '@radix-ui/react-dialog'; +import type { DialogProps } from '@radix-ui/react-dialog'; import { Command as CommandPrimitive } from 'cmdk'; import { Search } from 'lucide-react'; diff --git a/packages/ui/primitives/document-flow/add-signature.tsx b/packages/ui/primitives/document-flow/add-signature.tsx index e4e5d9253..5accdca16 100644 --- a/packages/ui/primitives/document-flow/add-signature.tsx +++ b/packages/ui/primitives/document-flow/add-signature.tsx @@ -7,11 +7,13 @@ import { DateTime } from 'luxon'; import { useForm } from 'react-hook-form'; import { match } from 'ts-pattern'; +import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; import { PDF_VIEWER_PAGE_SELECTOR } from '@documenso/lib/constants/pdf-viewer'; import { sortFieldsByPosition, validateFieldsInserted } from '@documenso/lib/utils/fields'; import type { Field } from '@documenso/prisma/client'; import { FieldType } from '@documenso/prisma/client'; import type { FieldWithSignature } from '@documenso/prisma/types/field-with-signature'; +import type { DocumentFlowStep } from '@documenso/ui/primitives/document-flow/types'; import { FieldToolTip } from '../../components/field/field-tooltip'; import { cn } from '../../lib/utils'; @@ -34,7 +36,6 @@ import { SinglePlayerModeCustomTextField, SinglePlayerModeSignatureField, } from './single-player-mode-fields'; -import type { DocumentFlowStep } from './types'; export type AddSignatureFormProps = { defaultValues?: TAddSignatureFormSchema; @@ -140,7 +141,7 @@ export const AddSignatureFormPartial = ({ return match(field.type) .with(FieldType.DATE, () => ({ ...field, - customText: DateTime.now().toFormat('yyyy-MM-dd hh:mm a'), + customText: DateTime.now().toFormat(DEFAULT_DOCUMENT_DATE_FORMAT), inserted: true, })) .with(FieldType.EMAIL, () => ({ diff --git a/packages/ui/primitives/document-flow/add-subject.tsx b/packages/ui/primitives/document-flow/add-subject.tsx index 881d59c74..d73019732 100644 --- a/packages/ui/primitives/document-flow/add-subject.tsx +++ b/packages/ui/primitives/document-flow/add-subject.tsx @@ -1,11 +1,30 @@ 'use client'; -import { useForm } from 'react-hook-form'; +import { useEffect } from 'react'; +import { Controller, useForm } from 'react-hook-form'; + +import { DATE_FORMATS, DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; +import { DEFAULT_DOCUMENT_TIME_ZONE, TIME_ZONES } from '@documenso/lib/constants/time-zones'; import type { Field, Recipient } from '@documenso/prisma/client'; import { DocumentStatus } from '@documenso/prisma/client'; +import { SendStatus } from '@documenso/prisma/client'; import type { DocumentWithData } from '@documenso/prisma/types/document-with-data'; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from '@documenso/ui/primitives/accordion'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@documenso/ui/primitives/select'; +import { Combobox } from '../combobox'; import { FormErrorMessage } from '../form/form-error-message'; import { Input } from '../input'; import { Label } from '../label'; @@ -31,20 +50,25 @@ export type AddSubjectFormProps = { export const AddSubjectFormPartial = ({ documentFlow, - recipients: _recipients, - fields: _fields, + recipients: recipients, + fields: fields, document, onSubmit, }: AddSubjectFormProps) => { const { + control, register, handleSubmit, - formState: { errors, isSubmitting }, + formState: { errors, isSubmitting, touchedFields }, + getValues, + setValue, } = useForm({ defaultValues: { - email: { + meta: { subject: document.documentMeta?.subject ?? '', message: document.documentMeta?.message ?? '', + timezone: document.documentMeta?.timezone ?? DEFAULT_DOCUMENT_TIME_ZONE, + dateFormat: document.documentMeta?.dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT, }, }, }); @@ -52,6 +76,20 @@ export const AddSubjectFormPartial = ({ const onFormSubmit = handleSubmit(onSubmit); const { currentStep, totalSteps, previousStep } = useStep(); + const hasDateField = fields.find((field) => field.type === 'DATE'); + + const documentHasBeenSent = recipients.some( + (recipient) => recipient.sendStatus === SendStatus.SENT, + ); + + // We almost always want to set the timezone to the user's local timezone to avoid confusion + // when the document is signed. + useEffect(() => { + if (!touchedFields.meta?.timezone && !documentHasBeenSent) { + setValue('meta.timezone', Intl.DateTimeFormat().resolvedOptions().timeZone); + } + }, [documentHasBeenSent, setValue, touchedFields.meta?.timezone]); + return ( <> - +
@@ -86,14 +124,12 @@ export const AddSubjectFormPartial = ({ id="message" className="bg-background mt-2 h-32 resize-none" disabled={isSubmitting} - {...register('email.message')} + {...register('meta.message')} />
@@ -123,6 +159,67 @@ export const AddSubjectFormPartial = ({
+ + + + + Advanced Options + + + + {hasDateField && ( +
+ + + ( + + )} + /> +
+ )} + + {hasDateField && ( +
+ + + ( + value && onChange(value)} + disabled={documentHasBeenSent} + /> + )} + /> +
+ )} +
+
+
diff --git a/packages/ui/primitives/document-flow/add-subject.types.ts b/packages/ui/primitives/document-flow/add-subject.types.ts index 33e2dedfb..ea14f4c0f 100644 --- a/packages/ui/primitives/document-flow/add-subject.types.ts +++ b/packages/ui/primitives/document-flow/add-subject.types.ts @@ -1,9 +1,14 @@ import { z } from 'zod'; +import { DEFAULT_DOCUMENT_DATE_FORMAT } from '@documenso/lib/constants/date-formats'; +import { DEFAULT_DOCUMENT_TIME_ZONE } from '@documenso/lib/constants/time-zones'; + export const ZAddSubjectFormSchema = z.object({ - email: z.object({ + meta: z.object({ subject: z.string(), message: z.string(), + timezone: z.string().optional().default(DEFAULT_DOCUMENT_TIME_ZONE), + dateFormat: z.string().optional().default(DEFAULT_DOCUMENT_DATE_FORMAT), }), }); diff --git a/packages/ui/primitives/multiselect-combobox.tsx b/packages/ui/primitives/multiselect-combobox.tsx new file mode 100644 index 000000000..bac87ce0b --- /dev/null +++ b/packages/ui/primitives/multiselect-combobox.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; + +import { Check, ChevronsUpDown } from 'lucide-react'; + +import { Role } from '@documenso/prisma/client'; +import { cn } from '@documenso/ui/lib/utils'; +import { Button } from '@documenso/ui/primitives/button'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, +} from '@documenso/ui/primitives/command'; +import { Popover, PopoverContent, PopoverTrigger } from '@documenso/ui/primitives/popover'; + +type ComboboxProps = { + listValues: string[]; + onChange: (_values: string[]) => void; +}; + +const MultiSelectCombobox = ({ listValues, onChange }: ComboboxProps) => { + const [open, setOpen] = React.useState(false); + const [selectedValues, setSelectedValues] = React.useState([]); + const dbRoles = Object.values(Role); + + React.useEffect(() => { + setSelectedValues(listValues); + }, [listValues]); + + const allRoles = [...new Set([...dbRoles, ...selectedValues])]; + + const handleSelect = (currentValue: string) => { + let newSelectedValues; + if (selectedValues.includes(currentValue)) { + newSelectedValues = selectedValues.filter((value) => value !== currentValue); + } else { + newSelectedValues = [...selectedValues, currentValue]; + } + + setSelectedValues(newSelectedValues); + onChange(newSelectedValues); + setOpen(false); + }; + + return ( + + + + + + + + No value found. + + {allRoles.map((value: string, i: number) => ( + handleSelect(value)}> + + {value} + + ))} + + + + + ); +}; + +export { MultiSelectCombobox }; From eb84d7ff3cb32269b383f471e485538bab28bccd Mon Sep 17 00:00:00 2001 From: Mythie Date: Wed, 27 Dec 2023 11:58:02 +1100 Subject: [PATCH 38/53] fix: remove invalid migration --- .../migration.sql | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 packages/prisma/migrations/20231214081915_remove_template_token_column/migration.sql diff --git a/packages/prisma/migrations/20231214081915_remove_template_token_column/migration.sql b/packages/prisma/migrations/20231214081915_remove_template_token_column/migration.sql deleted file mode 100644 index 8514a14b7..000000000 --- a/packages/prisma/migrations/20231214081915_remove_template_token_column/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `templateToken` on the `Recipient` table. All the data in the column will be lost. - -*/ --- AlterTable -ALTER TABLE "Recipient" DROP COLUMN "templateToken"; From d8eff192febf8358e2dbb3069236368ccecdd1b8 Mon Sep 17 00:00:00 2001 From: Mythie Date: Wed, 27 Dec 2023 14:04:22 +1100 Subject: [PATCH 39/53] fix: update date format and add missing default --- .../app/(signing)/sign/[token]/date-field.tsx | 4 ++-- packages/lib/constants/date-formats.ts | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/(signing)/sign/[token]/date-field.tsx b/apps/web/src/app/(signing)/sign/[token]/date-field.tsx index 40d0f945a..ce34a55fd 100644 --- a/apps/web/src/app/(signing)/sign/[token]/date-field.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/date-field.tsx @@ -49,7 +49,7 @@ export const DateField = ({ const localDateString = convertToLocalSystemFormat(field.customText, dateFormat, timezone); - const isDifferentTimeZone = field.inserted && localDateString !== field.customText; + const isDifferentTime = field.inserted && localDateString !== field.customText; const tooltipText = `"${field.customText}" will appear on the document as it has a timezone of "${timezone}".`; @@ -98,7 +98,7 @@ export const DateField = ({ onSign={onSign} onRemove={onRemove} type="Date" - tooltipText={isDifferentTimeZone ? tooltipText : undefined} + tooltipText={isDifferentTime ? tooltipText : undefined} > {isLoading && (
diff --git a/packages/lib/constants/date-formats.ts b/packages/lib/constants/date-formats.ts index 8c9ebe9e6..5b36cefdf 100644 --- a/packages/lib/constants/date-formats.ts +++ b/packages/lib/constants/date-formats.ts @@ -5,10 +5,15 @@ import { DEFAULT_DOCUMENT_TIME_ZONE } from './time-zones'; export const DEFAULT_DOCUMENT_DATE_FORMAT = 'yyyy-MM-dd hh:mm a'; export const DATE_FORMATS = [ + { + key: 'yyyy-MM-dd_hh:mm_a', + label: 'YYYY-MM-DD HH:mm a', + value: DEFAULT_DOCUMENT_DATE_FORMAT, + }, { key: 'YYYYMMDD', label: 'YYYY-MM-DD', - value: DEFAULT_DOCUMENT_DATE_FORMAT, + value: 'YYYY-MM-DD', }, { key: 'DDMMYYYY', @@ -57,15 +62,18 @@ export const convertToLocalSystemFormat = ( dateFormat: string | null = DEFAULT_DOCUMENT_DATE_FORMAT, timeZone: string | null = DEFAULT_DOCUMENT_TIME_ZONE, ): string => { - const parsedDate = DateTime.fromFormat(customText, dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT, { - zone: timeZone ?? DEFAULT_DOCUMENT_TIME_ZONE, + const coalescedDateFormat = dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT; + const coalescedTimeZone = timeZone ?? DEFAULT_DOCUMENT_TIME_ZONE; + + const parsedDate = DateTime.fromFormat(customText, coalescedDateFormat, { + zone: coalescedTimeZone, }); if (!parsedDate.isValid) { return 'Invalid date'; } - const formattedDate = parsedDate.toLocal().toFormat(dateFormat ?? DEFAULT_DOCUMENT_DATE_FORMAT); + const formattedDate = parsedDate.toLocal().toFormat(coalescedDateFormat); return formattedDate; }; From c4800f74b90687f9f0d10e96bc2510f4cbbb5d44 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Thu, 28 Dec 2023 20:07:29 +1100 Subject: [PATCH 40/53] feat: update disabled dropzone text (#787) Update the dropzone so it will display the relevant disabled text based on the reason it is disabled. --- .../app/(dashboard)/documents/upload-document.tsx | 13 ++++++++++++- packages/ui/primitives/document-dropzone.tsx | 6 +++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/(dashboard)/documents/upload-document.tsx b/apps/web/src/app/(dashboard)/documents/upload-document.tsx index 04ba990d5..65b95f9ec 100644 --- a/apps/web/src/app/(dashboard)/documents/upload-document.tsx +++ b/apps/web/src/app/(dashboard)/documents/upload-document.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; @@ -36,6 +36,16 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => { const { mutateAsync: createDocument } = trpc.document.createDocument.useMutation(); + const disabledMessage = useMemo(() => { + if (remaining.documents === 0) { + return 'You have reached your document limit.'; + } + + if (!session?.user.emailVerified) { + return 'Verify your email to upload documents.'; + } + }, [remaining.documents, session?.user.emailVerified]); + const onFileDrop = async (file: File) => { try { setIsLoading(true); @@ -91,6 +101,7 @@ export const UploadDocument = ({ className }: UploadDocumentProps) => { diff --git a/packages/ui/primitives/document-dropzone.tsx b/packages/ui/primitives/document-dropzone.tsx index 8ba22109a..21337956d 100644 --- a/packages/ui/primitives/document-dropzone.tsx +++ b/packages/ui/primitives/document-dropzone.tsx @@ -87,6 +87,7 @@ const DocumentDescription = { export type DocumentDropzoneProps = { className?: string; disabled?: boolean; + disabledMessage?: string; onDrop?: (_file: File) => void | Promise; type?: 'document' | 'template'; [key: string]: unknown; @@ -96,6 +97,7 @@ export const DocumentDropzone = ({ className, onDrop, disabled, + disabledMessage = 'You cannot upload documents at this time.', type = 'document', ...props }: DocumentDropzoneProps) => { @@ -172,7 +174,9 @@ export const DocumentDropzone = ({ {DocumentDescription[type].headline}

-

Drag & drop your document here.

+

+ {disabled ? disabledMessage : 'Drag & drop your document here.'} +

From 3f89f8725bc407248275e81a363ace551111b486 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Thu, 28 Dec 2023 20:08:19 +1100 Subject: [PATCH 41/53] fix: resolve conflicting z-index values (#788) ## Description Currently there are various z-index values that are causing: - Toasts to be placed behind dialog blur background - Menu being cropped off by header ## Changes Made - Revert `z-[1000]` back to `z-50` for the header (not exactly sure why it was bumped) - Refactor z-indexes over 9000 to start from 1000 - Ensure z-index of toast is higher than dialog --- apps/web/src/components/(dashboard)/layout/header.tsx | 2 +- packages/ui/primitives/dialog.tsx | 2 +- packages/ui/primitives/select.tsx | 2 +- packages/ui/primitives/toast.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/(dashboard)/layout/header.tsx b/apps/web/src/components/(dashboard)/layout/header.tsx index cf8873a1a..bdae6c511 100644 --- a/apps/web/src/components/(dashboard)/layout/header.tsx +++ b/apps/web/src/components/(dashboard)/layout/header.tsx @@ -33,7 +33,7 @@ export const Header = ({ className, user, ...props }: HeaderProps) => { return (
5 && 'border-b-border', className, )} diff --git a/packages/ui/primitives/dialog.tsx b/packages/ui/primitives/dialog.tsx index 8e5ed20e5..47982ab09 100644 --- a/packages/ui/primitives/dialog.tsx +++ b/packages/ui/primitives/dialog.tsx @@ -20,7 +20,7 @@ const DialogPortal = ({ }: DialogPrimitive.DialogPortalProps & { position?: 'start' | 'end' | 'center' }) => (
Date: Thu, 28 Dec 2023 15:06:46 +0530 Subject: [PATCH 42/53] fix: fixed the title box overlapping issue (#785) The issue is fixed. Now the box is no more overlapping Screenshot 2023-12-26 at 10 20 32 AM --- packages/ui/primitives/document-flow/add-title.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/primitives/document-flow/add-title.tsx b/packages/ui/primitives/document-flow/add-title.tsx index 8c2a9dc7a..afce0d9e0 100644 --- a/packages/ui/primitives/document-flow/add-title.tsx +++ b/packages/ui/primitives/document-flow/add-title.tsx @@ -64,7 +64,7 @@ export const AddTitleFormPartial = ({ From fb0d9b8ef98b7ee03e0310cb7ea0728816beb663 Mon Sep 17 00:00:00 2001 From: apoorv taneja Date: Thu, 28 Dec 2023 23:14:46 +0530 Subject: [PATCH 43/53] fixed padding in footer --- .vscode/settings.json | 2 +- apps/web/src/app/layout.tsx | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 97d5d1948..82aa3c1a3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { "typescript.tsdk": "node_modules/typescript/lib", "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, "eslint.validate": ["typescript", "typescriptreact", "javascript", "javascriptreact"], "javascript.preferences.importModuleSpecifier": "non-relative", diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index ac88469b0..0afcde320 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -16,6 +16,8 @@ import { PostHogPageview } from '~/providers/posthog'; import './globals.css'; +dasdas; + const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' }); const fontCaveat = Caveat({ subsets: ['latin'], variable: '--font-signature' }); From 0bb86963a93bfda8dc003fedeb66e15d84f7a868 Mon Sep 17 00:00:00 2001 From: apoorv taneja Date: Thu, 28 Dec 2023 23:27:45 +0530 Subject: [PATCH 44/53] rolled back to original file --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 82aa3c1a3..97d5d1948 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { "typescript.tsdk": "node_modules/typescript/lib", "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit" + "source.fixAll.eslint": true }, "eslint.validate": ["typescript", "typescriptreact", "javascript", "javascriptreact"], "javascript.preferences.importModuleSpecifier": "non-relative", From 5307fa645337cc18772d7dab9b80ad79a7da2714 Mon Sep 17 00:00:00 2001 From: apoorv taneja Date: Thu, 28 Dec 2023 23:29:44 +0530 Subject: [PATCH 45/53] fixed padding issue in footer --- apps/marketing/src/components/(marketing)/footer.tsx | 2 +- apps/web/src/app/layout.tsx | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/marketing/src/components/(marketing)/footer.tsx b/apps/marketing/src/components/(marketing)/footer.tsx index 1399297c7..30a0cb373 100644 --- a/apps/marketing/src/components/(marketing)/footer.tsx +++ b/apps/marketing/src/components/(marketing)/footer.tsx @@ -70,7 +70,7 @@ export const Footer = ({ className, ...props }: FooterProps) => { key={index} href={link.href} target={link.target} - className="text-muted-foreground hover:text-muted-foreground/80 flex-shrink-0 text-sm" + className="text-muted-foreground hover:text-muted-foreground/80 flex-shrink-0 text-sm break-words" > {link.text} diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 0afcde320..ac88469b0 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -16,8 +16,6 @@ import { PostHogPageview } from '~/providers/posthog'; import './globals.css'; -dasdas; - const fontInter = Inter({ subsets: ['latin'], variable: '--font-sans' }); const fontCaveat = Caveat({ subsets: ['latin'], variable: '--font-signature' }); From 5d6f69dc195d62f7cebfade153c59c4207ed0956 Mon Sep 17 00:00:00 2001 From: apoorv taneja Date: Thu, 28 Dec 2023 23:30:20 +0530 Subject: [PATCH 46/53] fixed padding issue in footer --- apps/marketing/src/components/(marketing)/footer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/marketing/src/components/(marketing)/footer.tsx b/apps/marketing/src/components/(marketing)/footer.tsx index 30a0cb373..1694a5e48 100644 --- a/apps/marketing/src/components/(marketing)/footer.tsx +++ b/apps/marketing/src/components/(marketing)/footer.tsx @@ -70,7 +70,7 @@ export const Footer = ({ className, ...props }: FooterProps) => { key={index} href={link.href} target={link.target} - className="text-muted-foreground hover:text-muted-foreground/80 flex-shrink-0 text-sm break-words" + className="text-muted-foreground hover:text-muted-foreground/80 flex-shrink-0 break-words text-sm" > {link.text} From 341481d6dba8f91782e8e6787b7ddf3521064e19 Mon Sep 17 00:00:00 2001 From: Mohith Gadireddy <88539464+Mohith234@users.noreply.github.com> Date: Fri, 29 Dec 2023 15:48:19 +0530 Subject: [PATCH 47/53] fix: trimmed long file names for better UX (#760) Fixes #755 ### Notes for Reviewers - The max length of the title is set to be `16` - If the length of the title is <16 it returns the original one. - Or else the title will be the first 8 characters (start) and last 8 characters (end) - The truncated file name will look like `start...end` ### Screenshot for reference ![image](https://github.com/documenso/documenso/assets/88539464/565e4868-7bb1-4b46-9cb0-886d542b8a01) --------- Co-authored-by: Catalin Pit <25515812+catalinpit@users.noreply.github.com> --- .../src/app/(signing)/sign/[token]/complete/page.tsx | 6 +++++- apps/web/src/app/(signing)/sign/[token]/page.tsx | 6 +++++- .../web/src/app/(signing)/sign/[token]/sign-dialog.tsx | 8 +++++--- apps/web/src/helpers/truncate-title.ts | 10 ++++++++++ 4 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 apps/web/src/helpers/truncate-title.ts diff --git a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx index 54757667a..4b1aed265 100644 --- a/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/complete/page.tsx @@ -15,6 +15,8 @@ import { DocumentDownloadButton } from '@documenso/ui/components/document/docume import { DocumentShareButton } from '@documenso/ui/components/document/document-share-button'; import { SigningCard3D } from '@documenso/ui/components/signing-card'; +import { truncateTitle } from '~/helpers/truncate-title'; + export type CompletedSigningPageProps = { params: { token?: string; @@ -36,6 +38,8 @@ export default async function CompletedSigningPage({ return notFound(); } + const truncatedTitle = truncateTitle(document.title); + const { documentData } = document; const [fields, recipient] = await Promise.all([ @@ -89,7 +93,7 @@ export default async function CompletedSigningPage({

You have signed - "{document.title}" + "{truncatedTitle}"

{match({ status: document.status, deletedAt: document.deletedAt }) diff --git a/apps/web/src/app/(signing)/sign/[token]/page.tsx b/apps/web/src/app/(signing)/sign/[token]/page.tsx index 18b81696e..efd0b266c 100644 --- a/apps/web/src/app/(signing)/sign/[token]/page.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/page.tsx @@ -17,6 +17,8 @@ import { Card, CardContent } from '@documenso/ui/primitives/card'; import { ElementVisible } from '@documenso/ui/primitives/element-visible'; import { LazyPDFViewer } from '@documenso/ui/primitives/lazy-pdf-viewer'; +import { truncateTitle } from '~/helpers/truncate-title'; + import { DateField } from './date-field'; import { EmailField } from './email-field'; import { SigningForm } from './form'; @@ -51,6 +53,8 @@ export default async function SigningPage({ params: { token } }: SigningPageProp return notFound(); } + const truncatedTitle = truncateTitle(document.title); + const { documentData } = document; const { user } = await getServerComponentSession(); @@ -82,7 +86,7 @@ export default async function SigningPage({ params: { token } }: SigningPageProp >

- {document.title} + {truncatedTitle}

diff --git a/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx b/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx index 0ce750a39..faecf5d7e 100644 --- a/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; -import { Document, Field } from '@documenso/prisma/client'; +import type { Document, Field } from '@documenso/prisma/client'; import { Button } from '@documenso/ui/primitives/button'; import { Dialog, @@ -9,6 +9,8 @@ import { DialogTrigger, } from '@documenso/ui/primitives/dialog'; +import { truncateTitle } from '~/helpers/truncate-title'; + export type SignDialogProps = { isSubmitting: boolean; document: Document; @@ -23,7 +25,7 @@ export const SignDialog = ({ onSignatureComplete, }: SignDialogProps) => { const [showDialog, setShowDialog] = useState(false); - + const truncatedTitle = truncateTitle(document.title); const isComplete = fields.every((field) => field.inserted); return ( @@ -43,7 +45,7 @@ export const SignDialog = ({
Sign Document
- You are about to finish signing "{document.title}". Are you sure? + You are about to finish signing "{truncatedTitle}". Are you sure?
diff --git a/apps/web/src/helpers/truncate-title.ts b/apps/web/src/helpers/truncate-title.ts new file mode 100644 index 000000000..2ad25c39a --- /dev/null +++ b/apps/web/src/helpers/truncate-title.ts @@ -0,0 +1,10 @@ +export const truncateTitle = (title: string, maxLength: number = 16) => { + if (title.length <= maxLength) { + return title; + } + + const start = title.slice(0, maxLength / 2); + const end = title.slice(-maxLength / 2); + + return `${start}.....${end}`; +}; From bed788f78f8d35c8c97a2359cbc7db850cf8b2d6 Mon Sep 17 00:00:00 2001 From: sadam Date: Fri, 29 Dec 2023 08:48:26 -0500 Subject: [PATCH 48/53] docs(url): change URL for cloning --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d4ce339c1..711a1f6b3 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ To run Documenso locally, you will need Want to get up and running quickly? Follow these steps: -1. [Fork this repository ](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks) to your github account. +1. [Fork this repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks) to your GitHub account. After forking clone it using the below command: @@ -154,7 +154,7 @@ npm run d Follow these steps to setup Documenso on your local machine: -1. [Fork this repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks) it to your github account. +1. [Fork this repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks) to your GitHub account. After forking clone it using the below command: @@ -231,7 +231,7 @@ cp .env.example .env The following environment variables must be set: -* `NEXTAUTH_URL` +* `NEXTAUTH_URL` * `NEXTAUTH_SECRET` * `NEXT_PUBLIC_WEBAPP_URL` * `NEXT_PUBLIC_MARKETING_URL` From 77facba8b44b99425d96a267c86a30ae15cd80aa Mon Sep 17 00:00:00 2001 From: sadam Date: Sat, 30 Dec 2023 18:27:24 -0500 Subject: [PATCH 49/53] docs(url): change URL for cloning --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 711a1f6b3..26f91e8d2 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ Want to get up and running quickly? Follow these steps: 1. [Fork this repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks) to your GitHub account. -After forking clone it using the below command: +After forking the repository, clone it to your local device by using the following command: ```sh git clone https://github.com//documenso @@ -156,7 +156,7 @@ Follow these steps to setup Documenso on your local machine: 1. [Fork this repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/about-forks) to your GitHub account. -After forking clone it using the below command: +After forking the repository, clone it to your local device by using the following command: ```sh git clone https://github.com//documenso From d02f6774b21bf0dcdc67722824962b0ff810ead5 Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu Date: Mon, 1 Jan 2024 17:41:29 +0000 Subject: [PATCH 50/53] chore: update the stale to prevent automatic closure of issues --- .github/workflows/stale.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index efd681a71..ab852de4c 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -17,9 +17,8 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-pr-stale: 30 days-before-issue-stale: 30 - stale-issue-message: 'This issue has not seen activity for a while. It will be closed in 30 days unless further activity is detected' + days-before-issue-close: -1 stale-pr-message: 'This PR has not seen activitiy for a while. It will be closed in 30 days unless further activity is detected.' - close-issue-message: 'This issue has been closed because of inactivity.' close-pr-message: 'This PR has been closed because of inactivity.' exempt-pr-labels: 'WIP,on-hold,needs review' exempt-issue-labels: 'WIP,on-hold,needs review,roadmap,assigned' From d731532fbf57d5556919b3218e3d9d8ecd053887 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Tue, 2 Jan 2024 04:58:35 +0000 Subject: [PATCH 51/53] chore: hide empty accordion for documents without date field --- .../primitives/document-flow/add-subject.tsx | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/ui/primitives/document-flow/add-subject.tsx b/packages/ui/primitives/document-flow/add-subject.tsx index d73019732..8fef8af7b 100644 --- a/packages/ui/primitives/document-flow/add-subject.tsx +++ b/packages/ui/primitives/document-flow/add-subject.tsx @@ -160,14 +160,14 @@ export const AddSubjectFormPartial = ({
- - - - Advanced Options - + {hasDateField && ( + + + + Advanced Options + - - {hasDateField && ( +
- )} - {hasDateField && (
- )} -
-
-
+ +
+
+ )}
From c1a6a327af62e1e23cc369d353116a993d37a145 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Wed, 3 Jan 2024 12:54:32 +1100 Subject: [PATCH 52/53] chore: update stale workflows --- .github/workflows/stale.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index ab852de4c..3e829d24b 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -15,10 +15,10 @@ jobs: - uses: actions/stale@v4 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - days-before-pr-stale: 30 - days-before-issue-stale: 30 - days-before-issue-close: -1 + days-before-pr-stale: 90 + days-before-issue-stale: 90 + days-before-issue-close: 180 stale-pr-message: 'This PR has not seen activitiy for a while. It will be closed in 30 days unless further activity is detected.' close-pr-message: 'This PR has been closed because of inactivity.' exempt-pr-labels: 'WIP,on-hold,needs review' - exempt-issue-labels: 'WIP,on-hold,needs review,roadmap,assigned' + exempt-issue-labels: 'WIP,on-hold,needs review,roadmap,assigned,needs triage' From 5c16b10dc272e37fef6f7a4a16b54ea482014c35 Mon Sep 17 00:00:00 2001 From: Lucas Smith Date: Wed, 3 Jan 2024 13:16:27 +1100 Subject: [PATCH 53/53] fix: update footer to be responsive --- apps/marketing/src/components/(marketing)/footer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/marketing/src/components/(marketing)/footer.tsx b/apps/marketing/src/components/(marketing)/footer.tsx index 1399297c7..bbb2aba7b 100644 --- a/apps/marketing/src/components/(marketing)/footer.tsx +++ b/apps/marketing/src/components/(marketing)/footer.tsx @@ -39,7 +39,7 @@ export const Footer = ({ className, ...props }: FooterProps) => { return (
-
+
{
-
+
{FOOTER_LINKS.map((link, index) => (