From 4ff8592e8fc156a51ec0d56b69dd6c78dc5f5e8e Mon Sep 17 00:00:00 2001 From: nafees nazik Date: Wed, 29 Nov 2023 22:11:55 +0530 Subject: [PATCH 01/83] 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/83] 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/83] 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/83] 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/83] 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/83] 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/83] 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/83] 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/83] 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/83] 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 323380d7574c55e0b6da91ced6d02d0c7f44540d Mon Sep 17 00:00:00 2001 From: Navindu Amarakoon Date: Sat, 9 Dec 2023 10:37:16 +0530 Subject: [PATCH 12/83] feat: env variable to disable signing up --- apps/web/src/app/(unauthenticated)/signin/page.tsx | 14 ++++++++------ apps/web/src/app/(unauthenticated)/signup/page.tsx | 5 +++++ packages/trpc/server/auth-router/router.ts | 7 +++++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/(unauthenticated)/signin/page.tsx b/apps/web/src/app/(unauthenticated)/signin/page.tsx index a4890d849..0b0333b65 100644 --- a/apps/web/src/app/(unauthenticated)/signin/page.tsx +++ b/apps/web/src/app/(unauthenticated)/signin/page.tsx @@ -13,12 +13,14 @@ export default function SignInPage() { -

- Don't have an account?{' '} - - Sign up - -

+ {process.env.NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && ( +

+ Don't have an account?{' '} + + Sign up + +

+ )}

Create a new account

diff --git a/packages/trpc/server/auth-router/router.ts b/packages/trpc/server/auth-router/router.ts index 59c51ade5..24dd272ee 100644 --- a/packages/trpc/server/auth-router/router.ts +++ b/packages/trpc/server/auth-router/router.ts @@ -11,6 +11,13 @@ import { ZSignUpMutationSchema, ZVerifyPasswordMutationSchema } from './schema'; export const authRouter = router({ signup: procedure.input(ZSignUpMutationSchema).mutation(async ({ input }) => { try { + if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === 'true') { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Signups are disabled.', + }); + } + const { name, email, password, signature } = input; const user = await createUser({ name, email, password, signature }); From dbdef79263f5ae3acae594d9df78ea2598987226 Mon Sep 17 00:00:00 2001 From: Navindu Amarakoon Date: Sat, 9 Dec 2023 10:38:48 +0530 Subject: [PATCH 13/83] chore: remove old env variable from docker compose --- docker/compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docker/compose.yml b/docker/compose.yml index b427f419c..9d4f0e951 100644 --- a/docker/compose.yml +++ b/docker/compose.yml @@ -33,7 +33,6 @@ services: - SMTP_MAIL_USER=username - SMTP_MAIL_PASSWORD=password - MAIL_FROM=admin@example.com - - NEXT_PUBLIC_ALLOW_SIGNUP=true ports: - 3000:3000 volumes: From 78a1ee2af0ddc3465648e9c21aea351d73101570 Mon Sep 17 00:00:00 2001 From: Navindu Amarakoon Date: Sat, 9 Dec 2023 11:35:45 +0530 Subject: [PATCH 14/83] feat: disable oauth signup when DISABLE_SIGNUP is true --- apps/web/src/components/forms/signin.tsx | 2 ++ packages/lib/next-auth/auth-options.ts | 10 ++++++++++ packages/lib/next-auth/error-codes.ts | 1 + packages/tsconfig/process-env.d.ts | 2 ++ 4 files changed, 15 insertions(+) diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx index 0d7dd723f..95dc6f9af 100644 --- a/apps/web/src/components/forms/signin.tsx +++ b/apps/web/src/components/forms/signin.tsx @@ -24,6 +24,7 @@ const ERROR_MESSAGES: Partial> = { 'This account appears to be using a social login method, please sign in using that method', [ErrorCode.INCORRECT_TWO_FACTOR_CODE]: 'The two-factor authentication code provided is incorrect', [ErrorCode.INCORRECT_TWO_FACTOR_BACKUP_CODE]: 'The backup code provided is incorrect', + [ErrorCode.SIGNUP_DISABLED]: 'Creating new accounts is currently disabled', }; const TwoFactorEnabledErrorCode = ErrorCode.TWO_FACTOR_MISSING_CREDENTIALS; @@ -146,6 +147,7 @@ export const SignInForm = ({ className }: SignInFormProps) => { try { await signIn('google', { callbackUrl: LOGIN_REDIRECT_PATH }); } catch (err) { + console.error(err); toast({ title: 'An unknown error occurred', description: diff --git a/packages/lib/next-auth/auth-options.ts b/packages/lib/next-auth/auth-options.ts index 6d59b0666..1d900c391 100644 --- a/packages/lib/next-auth/auth-options.ts +++ b/packages/lib/next-auth/auth-options.ts @@ -162,5 +162,15 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = { return session; }, + + async signIn({ user }) { + if (process.env.NEXT_PUBLIC_DISABLE_SIGNUP === 'true') { + const userData = await getUserByEmail({ email: user.email! }); + + return !!userData; + } else { + return true; + } + }, }, }; diff --git a/packages/lib/next-auth/error-codes.ts b/packages/lib/next-auth/error-codes.ts index c3dfafece..f69206456 100644 --- a/packages/lib/next-auth/error-codes.ts +++ b/packages/lib/next-auth/error-codes.ts @@ -19,4 +19,5 @@ export const ErrorCode = { INCORRECT_PASSWORD: 'INCORRECT_PASSWORD', MISSING_ENCRYPTION_KEY: 'MISSING_ENCRYPTION_KEY', MISSING_BACKUP_CODE: 'MISSING_BACKUP_CODE', + SIGNUP_DISABLED: 'SIGNUP_DISABLED', } as const; diff --git a/packages/tsconfig/process-env.d.ts b/packages/tsconfig/process-env.d.ts index 717f13ade..7169120c8 100644 --- a/packages/tsconfig/process-env.d.ts +++ b/packages/tsconfig/process-env.d.ts @@ -55,6 +55,8 @@ declare namespace NodeJS { NEXT_PRIVATE_SMTP_FROM_NAME?: string; NEXT_PRIVATE_SMTP_FROM_ADDRESS?: string; + NEXT_PUBLIC_DISABLE_SIGNUP?: string; + /** * Vercel environment variables */ From 3b3987dcf8d55095384a3e74c427fbb7caa147b3 Mon Sep 17 00:00:00 2001 From: Navindu Amarakoon Date: Sat, 9 Dec 2023 11:43:30 +0530 Subject: [PATCH 15/83] chore: add env to env.example --- .env.example | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.env.example b/.env.example index 45c26f6be..dff33d77c 100644 --- a/.env.example +++ b/.env.example @@ -87,6 +87,8 @@ NEXT_PUBLIC_POSTHOG_KEY="" NEXT_PUBLIC_POSTHOG_HOST="https://eu.posthog.com" # OPTIONAL: Leave blank to disable billing. NEXT_PUBLIC_FEATURE_BILLING_ENABLED= +# OPTIONAL: Leave blank to allow users to signup through /signup page. +NEXT_PUBLIC_DISABLE_SIGNUP= # This is only required for the marketing site # [[REDIS]] From ee5ce78c822f512117f530aa886dc254c602b7c0 Mon Sep 17 00:00:00 2001 From: Navindu Amarakoon Date: Sat, 9 Dec 2023 11:48:46 +0530 Subject: [PATCH 16/83] chore: remove unused code --- apps/web/src/components/forms/signin.tsx | 1 - packages/lib/next-auth/error-codes.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx index 95dc6f9af..9694bd581 100644 --- a/apps/web/src/components/forms/signin.tsx +++ b/apps/web/src/components/forms/signin.tsx @@ -24,7 +24,6 @@ const ERROR_MESSAGES: Partial> = { 'This account appears to be using a social login method, please sign in using that method', [ErrorCode.INCORRECT_TWO_FACTOR_CODE]: 'The two-factor authentication code provided is incorrect', [ErrorCode.INCORRECT_TWO_FACTOR_BACKUP_CODE]: 'The backup code provided is incorrect', - [ErrorCode.SIGNUP_DISABLED]: 'Creating new accounts is currently disabled', }; const TwoFactorEnabledErrorCode = ErrorCode.TWO_FACTOR_MISSING_CREDENTIALS; diff --git a/packages/lib/next-auth/error-codes.ts b/packages/lib/next-auth/error-codes.ts index f69206456..c3dfafece 100644 --- a/packages/lib/next-auth/error-codes.ts +++ b/packages/lib/next-auth/error-codes.ts @@ -19,5 +19,4 @@ export const ErrorCode = { INCORRECT_PASSWORD: 'INCORRECT_PASSWORD', MISSING_ENCRYPTION_KEY: 'MISSING_ENCRYPTION_KEY', MISSING_BACKUP_CODE: 'MISSING_BACKUP_CODE', - SIGNUP_DISABLED: 'SIGNUP_DISABLED', } as const; From 95041fa2e4d8fb58948e56420e79fe010c75d006 Mon Sep 17 00:00:00 2001 From: Navindu Amarakoon Date: Sat, 9 Dec 2023 12:05:36 +0530 Subject: [PATCH 17/83] fix: build error --- turbo.json | 1 + 1 file changed, 1 insertion(+) diff --git a/turbo.json b/turbo.json index 36b169a80..9ee878150 100644 --- a/turbo.json +++ b/turbo.json @@ -45,6 +45,7 @@ "NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID", "NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID", "NEXT_PUBLIC_STRIPE_FREE_PLAN_ID", + "NEXT_PUBLIC_DISABLE_SIGNUP", "NEXT_PRIVATE_DATABASE_URL", "NEXT_PRIVATE_DIRECT_DATABASE_URL", "NEXT_PRIVATE_GOOGLE_CLIENT_ID", From 5c1d30bfbb8e935459885ac871b71c0fcbe7fbdd Mon Sep 17 00:00:00 2001 From: Navindu Amarakoon Date: Sun, 10 Dec 2023 09:23:31 +0530 Subject: [PATCH 18/83] chore: remove console log --- apps/web/src/components/forms/signin.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx index 9694bd581..0d7dd723f 100644 --- a/apps/web/src/components/forms/signin.tsx +++ b/apps/web/src/components/forms/signin.tsx @@ -146,7 +146,6 @@ export const SignInForm = ({ className }: SignInFormProps) => { try { await signIn('google', { callbackUrl: LOGIN_REDIRECT_PATH }); } catch (err) { - console.error(err); toast({ title: 'An unknown error occurred', description: From 2d931b2c9ba683a6ae64f52b1f8b7b00bcc3706d Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Mon, 11 Dec 2023 23:36:54 +0530 Subject: [PATCH 19/83] chore: fix caching issue in workflows Signed-off-by: Adithya Krishna --- .github/workflows/issue-assignee-check.yml | 4 ++++ .github/workflows/pr-review-reminder.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/issue-assignee-check.yml b/.github/workflows/issue-assignee-check.yml index 1ce7a02be..dbd321509 100644 --- a/.github/workflows/issue-assignee-check.yml +++ b/.github/workflows/issue-assignee-check.yml @@ -12,6 +12,10 @@ jobs: if: ${{ github.event.issue.assignee }} && github.event.action == 'assigned' && github.event.sender.type == 'User' runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 - name: Set up Node.js uses: actions/setup-node@v4 with: diff --git a/.github/workflows/pr-review-reminder.yml b/.github/workflows/pr-review-reminder.yml index cc272fbfe..78f927e61 100644 --- a/.github/workflows/pr-review-reminder.yml +++ b/.github/workflows/pr-review-reminder.yml @@ -12,6 +12,10 @@ jobs: if: ${{ github.event.pull_request.user.login }} && github.event.action == ('opened' || 'reopened' || 'ready_for_review' || 'review_requested') runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 - name: Set up Node.js uses: actions/setup-node@v4 with: From f9139a54a53a12efc98aea39ddfad86aa40491af Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Mon, 11 Dec 2023 23:37:28 +0530 Subject: [PATCH 20/83] chore: prevent frequent commenting for semantic pr titles Signed-off-by: Adithya Krishna --- .github/workflows/semantic-pull-requests.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/semantic-pull-requests.yml b/.github/workflows/semantic-pull-requests.yml index 7685562b5..37b764652 100644 --- a/.github/workflows/semantic-pull-requests.yml +++ b/.github/workflows/semantic-pull-requests.yml @@ -16,6 +16,24 @@ jobs: name: Validate PR title runs-on: ubuntu-latest steps: + - name: Check PR creator's previous activity + id: check_activity + run: | + CREATOR=$(curl -s "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" | jq -r '.user.login') + ACTIVITY=$(curl -s "https://api.github.com/search/commits?q=author:${CREATOR}+repo:${{ github.repository }}" | jq -r '.total_count') + if [ "$ACTIVITY" -eq 0 ]; then + echo "::set-output name=is_new::true" + else + echo "::set-output name=is_new::false" + fi + + - name: Count PRs created by user + id: count_prs + run: | + CREATOR=$(curl -s "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}" | jq -r '.user.login') + PR_COUNT=$(curl -s "https://api.github.com/search/issues?q=type:pr+is:open+author:${CREATOR}+repo:${{ github.repository }}" | jq -r '.total_count') + echo "::set-output name=pr_count::$PR_COUNT" + - uses: amannn/action-semantic-pull-request@v5 id: lint_pr_title env: @@ -36,7 +54,7 @@ jobs: ${{ steps.lint_pr_title.outputs.error_message }} ``` - - if: ${{ steps.lint_pr_title.outputs.error_message == null }} + - if: ${{ steps.lint_pr_title.outputs.error_message == null && steps.check_activity.outputs.is_new == 'false' && steps.count_prs.outputs.pr_count < 2}} uses: marocchino/sticky-pull-request-comment@v2 with: header: pr-title-lint-error From f2d4c0721dc005b5b72f65afbdd975fd35c2dedc Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Mon, 11 Dec 2023 23:38:07 +0530 Subject: [PATCH 21/83] chore: removed packageManager as we have engines Signed-off-by: Adithya Krishna --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 780f76793..30076100f 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "turbo": "^1.9.3" }, "name": "@documenso/root", - "packageManager": "npm@8.19.2", "workspaces": [ "apps/*", "packages/*" From 6d34ebd91bca211c5853c6c5bc900684ecc0274f Mon Sep 17 00:00:00 2001 From: Mythie Date: Wed, 13 Dec 2023 22:49:58 +1100 Subject: [PATCH 22/83] fix: no longer available client component --- apps/web/src/app/(signing)/sign/[token]/no-longer-available.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/src/app/(signing)/sign/[token]/no-longer-available.tsx b/apps/web/src/app/(signing)/sign/[token]/no-longer-available.tsx index 8c7051caa..39bfba935 100644 --- a/apps/web/src/app/(signing)/sign/[token]/no-longer-available.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/no-longer-available.tsx @@ -1,3 +1,5 @@ +'use client'; + import React from 'react'; import Link from 'next/link'; From 31a9127c9e77295e65edb00858a4effa76a7e030 Mon Sep 17 00:00:00 2001 From: Ephraim Atta-Duncan Date: Fri, 6 Oct 2023 22:54:24 +0000 Subject: [PATCH 23/83] 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 88534fa1c665eb7544f697c6adedf9e640bce196 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Thu, 14 Dec 2023 15:22:54 +1100 Subject: [PATCH 24/83] feat: add multi subscription support (#734) ## Description Previously we assumed that there can only be 1 subscription per user. However, that will soon no longer the case with the introduction of the Teams subscription. This PR will apply the required migrations to support multiple subscriptions. ## Changes Made - Updated the Prisma schema to allow for multiple `Subscriptions` per `User` - Added a Stripe `customerId` field to the `User` model - Updated relevant billing sections to support multiple subscriptions ## Testing Performed - Tested running the Prisma migration on a demo database created on the main branch Will require a lot of additional testing. ## Checklist - [ ] I have tested these changes locally and they work as expected. - [ ] I have added/updated tests that prove the effectiveness of these changes. - [X] I have followed the project's coding style guidelines. ## Additional Notes Added the following custom SQL statement to the migration: > DELETE FROM "Subscription" WHERE "planId" IS NULL OR "priceId" IS NULL; Prior to deployment this will require changes to Stripe products: - Adding `type` meta attribute --------- Co-authored-by: Lucas Smith --- .env.example | 4 - apps/marketing/process-env.d.ts | 2 - .../(marketing)/claim-plan-dialog.tsx | 160 ------------------ .../components/(marketing)/pricing-table.tsx | 11 +- apps/web/process-env.d.ts | 2 - .../admin/users/data-table-users.tsx | 21 ++- .../src/app/(dashboard)/admin/users/page.tsx | 17 +- .../settings/billing/billing-plans.tsx | 2 +- .../billing/create-billing-portal.action.ts | 37 +--- .../billing/create-checkout.action.ts | 43 ++--- .../app/(dashboard)/settings/billing/page.tsx | 33 +++- packages/ee/server-only/limits/server.ts | 38 +++-- .../ee/server-only/stripe/create-customer.ts | 31 ---- .../ee/server-only/stripe/get-customer.ts | 75 ++++++++ .../stripe/get-prices-by-interval.ts | 15 +- .../server-only/stripe/get-prices-by-type.ts | 11 ++ .../ee/server-only/stripe/webhook/handler.ts | 32 ++-- .../stripe/webhook/on-subscription-deleted.ts | 7 +- .../stripe/webhook/on-subscription-updated.ts | 11 +- .../lib/server-only/admin/get-users-stats.ts | 4 +- .../get-subscription-by-user-id.ts | 15 -- .../get-subscriptions-by-user-id.ts | 15 ++ packages/lib/server-only/user/create-user.ts | 17 +- .../migration.sql | 31 ++++ packages/prisma/schema.prisma | 11 +- packages/tsconfig/process-env.d.ts | 2 - render.yaml | 4 - turbo.json | 3 - 28 files changed, 288 insertions(+), 366 deletions(-) delete mode 100644 apps/marketing/src/components/(marketing)/claim-plan-dialog.tsx delete mode 100644 packages/ee/server-only/stripe/create-customer.ts create mode 100644 packages/ee/server-only/stripe/get-prices-by-type.ts delete mode 100644 packages/lib/server-only/subscription/get-subscription-by-user-id.ts create mode 100644 packages/lib/server-only/subscription/get-subscriptions-by-user-id.ts create mode 100644 packages/prisma/migrations/20231206073509_add_multple_subscriptions/migration.sql diff --git a/.env.example b/.env.example index 968dd05e5..4c3c8f2e9 100644 --- a/.env.example +++ b/.env.example @@ -77,14 +77,10 @@ NEXT_PRIVATE_MAILCHANNELS_DKIM_PRIVATE_KEY= NEXT_PRIVATE_STRIPE_API_KEY= NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET= NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID= -NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID= -NEXT_PUBLIC_STRIPE_FREE_PLAN_ID= # [[FEATURES]] # OPTIONAL: Leave blank to disable PostHog and feature flags. NEXT_PUBLIC_POSTHOG_KEY="" -# OPTIONAL: Defines the host to use for PostHog. -NEXT_PUBLIC_POSTHOG_HOST="https://eu.posthog.com" # OPTIONAL: Leave blank to disable billing. NEXT_PUBLIC_FEATURE_BILLING_ENABLED= diff --git a/apps/marketing/process-env.d.ts b/apps/marketing/process-env.d.ts index 942007d17..207bacef5 100644 --- a/apps/marketing/process-env.d.ts +++ b/apps/marketing/process-env.d.ts @@ -6,8 +6,6 @@ declare namespace NodeJS { NEXT_PRIVATE_DATABASE_URL: string; NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string; - NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string; - NEXT_PUBLIC_STRIPE_FREE_PLAN_ID?: string; NEXT_PRIVATE_STRIPE_API_KEY: string; NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string; diff --git a/apps/marketing/src/components/(marketing)/claim-plan-dialog.tsx b/apps/marketing/src/components/(marketing)/claim-plan-dialog.tsx deleted file mode 100644 index 8f826b4de..000000000 --- a/apps/marketing/src/components/(marketing)/claim-plan-dialog.tsx +++ /dev/null @@ -1,160 +0,0 @@ -'use client'; - -import React, { useEffect, useState } from 'react'; - -import { useSearchParams } from 'next/navigation'; - -import { zodResolver } from '@hookform/resolvers/zod'; -import { Info } from 'lucide-react'; -import { usePlausible } from 'next-plausible'; -import { useForm } from 'react-hook-form'; -import { z } from 'zod'; - -import { useAnalytics } from '@documenso/lib/client-only/hooks/use-analytics'; -import { cn } from '@documenso/ui/lib/utils'; -import { Button } from '@documenso/ui/primitives/button'; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from '@documenso/ui/primitives/dialog'; -import { Input } from '@documenso/ui/primitives/input'; -import { Label } from '@documenso/ui/primitives/label'; -import { useToast } from '@documenso/ui/primitives/use-toast'; - -import { claimPlan } from '~/api/claim-plan/fetcher'; - -import { FormErrorMessage } from '../form/form-error-message'; - -export const ZClaimPlanDialogFormSchema = z.object({ - name: z.string().trim().min(3, { message: 'Please enter a valid name.' }), - email: z.string().email(), -}); - -export type TClaimPlanDialogFormSchema = z.infer; - -export type ClaimPlanDialogProps = { - className?: string; - planId: string; - children: React.ReactNode; -}; - -export const ClaimPlanDialog = ({ className, planId, children }: ClaimPlanDialogProps) => { - const params = useSearchParams(); - const analytics = useAnalytics(); - const event = usePlausible(); - - const { toast } = useToast(); - - const [open, setOpen] = useState(() => params?.get('cancelled') === 'true'); - - const { - register, - handleSubmit, - formState: { errors, isSubmitting }, - reset, - } = useForm({ - defaultValues: { - name: params?.get('name') ?? '', - email: params?.get('email') ?? '', - }, - resolver: zodResolver(ZClaimPlanDialogFormSchema), - }); - - const onFormSubmit = async ({ name, email }: TClaimPlanDialogFormSchema) => { - try { - const delay = new Promise((resolve) => { - setTimeout(resolve, 1000); - }); - - const [redirectUrl] = await Promise.all([ - claimPlan({ name, email, planId, signatureText: name, signatureDataUrl: null }), - delay, - ]); - - event('claim-plan-pricing'); - analytics.capture('Marketing: Claim plan', { planId, email }); - - window.location.href = redirectUrl; - } catch (error) { - event('claim-plan-failed'); - analytics.capture('Marketing: Claim plan failure', { planId, email }); - - toast({ - title: 'Something went wrong', - description: error instanceof Error ? error.message : 'Please try again later.', - variant: 'destructive', - }); - } - }; - - useEffect(() => { - if (!isSubmitting && !open) { - reset(); - } - }, [open]); - - return ( - !isSubmitting && setOpen(value)}> - {children} - - - - Claim your plan - - - We're almost there! Please enter your email address and name to claim your plan. - - - -
-
- {params?.get('cancelled') === 'true' && ( -
-
-
- -
-
-

- You have cancelled the payment process. If you didn't mean to do this, please - try again. -

-
-
-
- )} - -
- - - - - -
- -
- - - - - -
- - -
-
-
-
- ); -}; diff --git a/apps/marketing/src/components/(marketing)/pricing-table.tsx b/apps/marketing/src/components/(marketing)/pricing-table.tsx index 712435e68..b65411064 100644 --- a/apps/marketing/src/components/(marketing)/pricing-table.tsx +++ b/apps/marketing/src/components/(marketing)/pricing-table.tsx @@ -1,9 +1,9 @@ 'use client'; -import { HTMLAttributes, useState } from 'react'; +import type { HTMLAttributes } from 'react'; +import { useState } from 'react'; import Link from 'next/link'; -import { useSearchParams } from 'next/navigation'; import { AnimatePresence, motion } from 'framer-motion'; import { usePlausible } from 'next-plausible'; @@ -16,14 +16,9 @@ export type PricingTableProps = HTMLAttributes; const SELECTED_PLAN_BAR_LAYOUT_ID = 'selected-plan-bar'; export const PricingTable = ({ className, ...props }: PricingTableProps) => { - const params = useSearchParams(); const event = usePlausible(); - const [period, setPeriod] = useState<'MONTHLY' | 'YEARLY'>(() => - params?.get('planId') === process.env.NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID - ? 'YEARLY' - : 'MONTHLY', - ); + const [period, setPeriod] = useState<'MONTHLY' | 'YEARLY'>('MONTHLY'); return (
diff --git a/apps/web/process-env.d.ts b/apps/web/process-env.d.ts index f775cb7d8..0c00cb4c1 100644 --- a/apps/web/process-env.d.ts +++ b/apps/web/process-env.d.ts @@ -6,8 +6,6 @@ declare namespace NodeJS { NEXT_PRIVATE_DATABASE_URL: string; NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string; - NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string; - NEXT_PUBLIC_STRIPE_FREE_PLAN_ID?: string; NEXT_PRIVATE_STRIPE_API_KEY: string; NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string; diff --git a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx index f0c91615b..a8e02ca9f 100644 --- a/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/data-table-users.tsx @@ -8,7 +8,7 @@ import { Edit, Loader } from 'lucide-react'; import { useDebouncedValue } from '@documenso/lib/client-only/hooks/use-debounced-value'; import { useUpdateSearchParams } from '@documenso/lib/client-only/hooks/use-update-search-params'; -import { Document, Role, Subscription } from '@documenso/prisma/client'; +import type { Document, Role, Subscription } from '@documenso/prisma/client'; import { Button } from '@documenso/ui/primitives/button'; import { DataTable } from '@documenso/ui/primitives/data-table'; import { DataTablePagination } from '@documenso/ui/primitives/data-table-pagination'; @@ -19,7 +19,7 @@ type UserData = { name: string | null; email: string; roles: Role[]; - Subscription?: SubscriptionLite | null; + Subscription?: SubscriptionLite[] | null; Document: DocumentLite[]; }; @@ -35,9 +35,16 @@ type UsersDataTableProps = { totalPages: number; perPage: number; page: number; + individualPriceIds: string[]; }; -export const UsersDataTable = ({ users, totalPages, perPage, page }: UsersDataTableProps) => { +export const UsersDataTable = ({ + users, + totalPages, + perPage, + page, + individualPriceIds, +}: UsersDataTableProps) => { const [isPending, startTransition] = useTransition(); const updateSearchParams = useUpdateSearchParams(); const [searchString, setSearchString] = useState(''); @@ -100,7 +107,13 @@ export const UsersDataTable = ({ users, totalPages, perPage, page }: UsersDataTa { header: 'Subscription', accessorKey: 'subscription', - cell: ({ row }) => row.original.Subscription?.status ?? 'NONE', + cell: ({ row }) => { + const foundIndividualSubscription = (row.original.Subscription ?? []).find((sub) => + individualPriceIds.includes(sub.priceId), + ); + + return foundIndividualSubscription?.status ?? 'NONE'; + }, }, { header: 'Documents', diff --git a/apps/web/src/app/(dashboard)/admin/users/page.tsx b/apps/web/src/app/(dashboard)/admin/users/page.tsx index 686ce7669..069378274 100644 --- a/apps/web/src/app/(dashboard)/admin/users/page.tsx +++ b/apps/web/src/app/(dashboard)/admin/users/page.tsx @@ -1,3 +1,5 @@ +import { getPricesByType } from '@documenso/ee/server-only/stripe/get-prices-by-type'; + import { UsersDataTable } from './data-table-users'; import { search } from './fetch-users.actions'; @@ -14,12 +16,23 @@ export default async function AdminManageUsers({ searchParams = {} }: AdminManag const perPage = Number(searchParams.perPage) || 10; const searchString = searchParams.search || ''; - const { users, totalPages } = await search(searchString, page, perPage); + const [{ users, totalPages }, individualPrices] = await Promise.all([ + search(searchString, page, perPage), + getPricesByType('individual'), + ]); + + const individualPriceIds = individualPrices.map((price) => price.id); return (

Manage users

- +
); } diff --git a/apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx b/apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx index ba4c0f818..a931f489b 100644 --- a/apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx +++ b/apps/web/src/app/(dashboard)/settings/billing/billing-plans.tsx @@ -4,7 +4,7 @@ import { useState } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; -import { PriceIntervals } from '@documenso/ee/server-only/stripe/get-prices-by-interval'; +import type { PriceIntervals } from '@documenso/ee/server-only/stripe/get-prices-by-interval'; import { useIsMounted } from '@documenso/lib/client-only/hooks/use-is-mounted'; import { toHumanPrice } from '@documenso/lib/universal/stripe/to-human-price'; import { Button } from '@documenso/ui/primitives/button'; diff --git a/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts b/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts index ee5dbf175..885414515 100644 --- a/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts +++ b/apps/web/src/app/(dashboard)/settings/billing/create-billing-portal.action.ts @@ -1,46 +1,13 @@ 'use server'; -import { - getStripeCustomerByEmail, - getStripeCustomerById, -} from '@documenso/ee/server-only/stripe/get-customer'; +import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer'; import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; -import type { Stripe } from '@documenso/lib/server-only/stripe'; -import { stripe } from '@documenso/lib/server-only/stripe'; -import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id'; export const createBillingPortal = async () => { const { user } = await getRequiredServerComponentSession(); - const existingSubscription = await getSubscriptionByUserId({ userId: user.id }); - - let stripeCustomer: Stripe.Customer | null = null; - - // Find the Stripe customer for the current user subscription. - if (existingSubscription) { - stripeCustomer = await getStripeCustomerById(existingSubscription.customerId); - - if (!stripeCustomer) { - throw new Error('Missing Stripe customer for subscription'); - } - } - - // Fallback to check if a Stripe customer already exists for the current user email. - if (!stripeCustomer) { - stripeCustomer = await getStripeCustomerByEmail(user.email); - } - - // Create a Stripe customer if it does not exist for the current user. - if (!stripeCustomer) { - stripeCustomer = await stripe.customers.create({ - name: user.name ?? undefined, - email: user.email, - metadata: { - userId: user.id, - }, - }); - } + const { stripeCustomer } = await getStripeCustomerByUser(user); return getPortalSession({ customerId: stripeCustomer.id, diff --git a/apps/web/src/app/(dashboard)/settings/billing/create-checkout.action.ts b/apps/web/src/app/(dashboard)/settings/billing/create-checkout.action.ts index 465d662a1..f8f20030c 100644 --- a/apps/web/src/app/(dashboard)/settings/billing/create-checkout.action.ts +++ b/apps/web/src/app/(dashboard)/settings/billing/create-checkout.action.ts @@ -1,55 +1,36 @@ 'use server'; -import { createCustomer } from '@documenso/ee/server-only/stripe/create-customer'; import { getCheckoutSession } from '@documenso/ee/server-only/stripe/get-checkout-session'; -import { - getStripeCustomerByEmail, - getStripeCustomerById, -} from '@documenso/ee/server-only/stripe/get-customer'; +import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer'; import { getPortalSession } from '@documenso/ee/server-only/stripe/get-portal-session'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; -import type { Stripe } from '@documenso/lib/server-only/stripe'; -import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id'; +import { getSubscriptionsByUserId } from '@documenso/lib/server-only/subscription/get-subscriptions-by-user-id'; export type CreateCheckoutOptions = { priceId: string; }; export const createCheckout = async ({ priceId }: CreateCheckoutOptions) => { - const { user } = await getRequiredServerComponentSession(); + const session = await getRequiredServerComponentSession(); - const existingSubscription = await getSubscriptionByUserId({ userId: user.id }); + const { user, stripeCustomer } = await getStripeCustomerByUser(session.user); - let stripeCustomer: Stripe.Customer | null = null; + const existingSubscriptions = await getSubscriptionsByUserId({ userId: user.id }); - // Find the Stripe customer for the current user subscription. - if (existingSubscription?.periodEnd && existingSubscription.periodEnd >= new Date()) { - stripeCustomer = await getStripeCustomerById(existingSubscription.customerId); - - if (!stripeCustomer) { - throw new Error('Missing Stripe customer for subscription'); - } + const foundSubscription = existingSubscriptions.find( + (subscription) => + subscription.priceId === priceId && + subscription.periodEnd && + subscription.periodEnd >= new Date(), + ); + if (foundSubscription) { return getPortalSession({ customerId: stripeCustomer.id, returnUrl: `${process.env.NEXT_PUBLIC_WEBAPP_URL}/settings/billing`, }); } - // Fallback to check if a Stripe customer already exists for the current user email. - if (!stripeCustomer) { - stripeCustomer = await getStripeCustomerByEmail(user.email); - } - - // Create a Stripe customer if it does not exist for the current user. - if (!stripeCustomer) { - await createCustomer({ - user, - }); - - stripeCustomer = await getStripeCustomerByEmail(user.email); - } - return getCheckoutSession({ customerId: stripeCustomer.id, priceId, diff --git a/apps/web/src/app/(dashboard)/settings/billing/page.tsx b/apps/web/src/app/(dashboard)/settings/billing/page.tsx index 61dff3216..74e4bd685 100644 --- a/apps/web/src/app/(dashboard)/settings/billing/page.tsx +++ b/apps/web/src/app/(dashboard)/settings/billing/page.tsx @@ -2,12 +2,15 @@ import { redirect } from 'next/navigation'; import { match } from 'ts-pattern'; +import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer'; import { getPricesByInterval } from '@documenso/ee/server-only/stripe/get-prices-by-interval'; +import { getPricesByType } from '@documenso/ee/server-only/stripe/get-prices-by-type'; import { getProductByPriceId } from '@documenso/ee/server-only/stripe/get-product-by-price-id'; import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; import { getServerComponentFlag } from '@documenso/lib/server-only/feature-flags/get-server-component-feature-flag'; -import type { Stripe } from '@documenso/lib/server-only/stripe'; -import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id'; +import { type Stripe } from '@documenso/lib/server-only/stripe'; +import { getSubscriptionsByUserId } from '@documenso/lib/server-only/subscription/get-subscriptions-by-user-id'; +import { SubscriptionStatus } from '@documenso/prisma/client'; import { LocaleDate } from '~/components/formatter/locale-date'; @@ -15,7 +18,7 @@ import { BillingPlans } from './billing-plans'; import { BillingPortalButton } from './billing-portal-button'; export default async function BillingSettingsPage() { - const { user } = await getRequiredServerComponentSession(); + let { user } = await getRequiredServerComponentSession(); const isBillingEnabled = await getServerComponentFlag('app_billing'); @@ -24,20 +27,36 @@ export default async function BillingSettingsPage() { redirect('/settings/profile'); } - const [subscription, prices] = await Promise.all([ - getSubscriptionByUserId({ userId: user.id }), - getPricesByInterval(), + if (!user.customerId) { + user = await getStripeCustomerByUser(user).then((result) => result.user); + } + + const [subscriptions, prices, individualPrices] = await Promise.all([ + getSubscriptionsByUserId({ userId: user.id }), + getPricesByInterval({ type: 'individual' }), + getPricesByType('individual'), ]); + const individualPriceIds = individualPrices.map(({ id }) => id); + let subscriptionProduct: Stripe.Product | null = null; + const individualUserSubscriptions = subscriptions.filter(({ priceId }) => + individualPriceIds.includes(priceId), + ); + + const subscription = + individualUserSubscriptions.find(({ status }) => status === SubscriptionStatus.ACTIVE) ?? + individualUserSubscriptions[0]; + if (subscription?.priceId) { subscriptionProduct = await getProductByPriceId({ priceId: subscription.priceId }).catch( () => null, ); } - const isMissingOrInactiveOrFreePlan = !subscription || subscription.status === 'INACTIVE'; + const isMissingOrInactiveOrFreePlan = + !subscription || subscription.status === SubscriptionStatus.INACTIVE; return (
diff --git a/packages/ee/server-only/limits/server.ts b/packages/ee/server-only/limits/server.ts index 548ad108a..f256c6356 100644 --- a/packages/ee/server-only/limits/server.ts +++ b/packages/ee/server-only/limits/server.ts @@ -1,10 +1,10 @@ import { DateTime } from 'luxon'; -import { stripe } from '@documenso/lib/server-only/stripe'; import { getFlag } from '@documenso/lib/universal/get-feature-flag'; import { prisma } from '@documenso/prisma'; import { SubscriptionStatus } from '@documenso/prisma/client'; +import { getPricesByType } from '../stripe/get-prices-by-type'; import { FREE_PLAN_LIMITS, SELFHOSTED_PLAN_LIMITS } from './constants'; import { ERROR_CODES } from './errors'; import { ZLimitsSchema } from './schema'; @@ -43,23 +43,29 @@ export const getServerLimits = async ({ email }: GetServerLimitsOptions) => { let quota = structuredClone(FREE_PLAN_LIMITS); let remaining = structuredClone(FREE_PLAN_LIMITS); - // Since we store details and allow for past due plans we need to check if the subscription is active. - if (user.Subscription?.status !== SubscriptionStatus.INACTIVE && user.Subscription?.priceId) { - const { product } = await stripe.prices - .retrieve(user.Subscription.priceId, { - expand: ['product'], - }) - .catch((err) => { - console.error(err); - throw err; - }); + const activeSubscriptions = user.Subscription.filter( + ({ status }) => status === SubscriptionStatus.ACTIVE, + ); - if (typeof product === 'string') { - throw new Error(ERROR_CODES.SUBSCRIPTION_FETCH_FAILED); + if (activeSubscriptions.length > 0) { + const individualPrices = await getPricesByType('individual'); + + for (const subscription of activeSubscriptions) { + const price = individualPrices.find((price) => price.id === subscription.priceId); + if (!price || typeof price.product === 'string' || price.product.deleted) { + continue; + } + + const currentQuota = ZLimitsSchema.parse( + 'metadata' in price.product ? price.product.metadata : {}, + ); + + // Use the subscription with the highest quota. + if (currentQuota.documents > quota.documents && currentQuota.recipients > quota.recipients) { + quota = currentQuota; + remaining = structuredClone(quota); + } } - - quota = ZLimitsSchema.parse('metadata' in product ? product.metadata : {}); - remaining = structuredClone(quota); } const documents = await prisma.document.count({ diff --git a/packages/ee/server-only/stripe/create-customer.ts b/packages/ee/server-only/stripe/create-customer.ts deleted file mode 100644 index 175298d0b..000000000 --- a/packages/ee/server-only/stripe/create-customer.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { stripe } from '@documenso/lib/server-only/stripe'; -import { getSubscriptionByUserId } from '@documenso/lib/server-only/subscription/get-subscription-by-user-id'; -import { prisma } from '@documenso/prisma'; -import { User } from '@documenso/prisma/client'; - -export type CreateCustomerOptions = { - user: User; -}; - -export const createCustomer = async ({ user }: CreateCustomerOptions) => { - const existingSubscription = await getSubscriptionByUserId({ userId: user.id }); - - if (existingSubscription) { - throw new Error('User already has a subscription'); - } - - const customer = await stripe.customers.create({ - name: user.name ?? undefined, - email: user.email, - metadata: { - userId: user.id, - }, - }); - - return await prisma.subscription.create({ - data: { - userId: user.id, - customerId: customer.id, - }, - }); -}; diff --git a/packages/ee/server-only/stripe/get-customer.ts b/packages/ee/server-only/stripe/get-customer.ts index 11e782966..c85488e6f 100644 --- a/packages/ee/server-only/stripe/get-customer.ts +++ b/packages/ee/server-only/stripe/get-customer.ts @@ -1,4 +1,8 @@ import { stripe } from '@documenso/lib/server-only/stripe'; +import { prisma } from '@documenso/prisma'; +import type { User } from '@documenso/prisma/client'; + +import { onSubscriptionUpdated } from './webhook/on-subscription-updated'; export const getStripeCustomerByEmail = async (email: string) => { const foundStripeCustomers = await stripe.customers.list({ @@ -17,3 +21,74 @@ export const getStripeCustomerById = async (stripeCustomerId: string) => { return null; } }; + +/** + * Get a stripe customer by user. + * + * Will create a Stripe customer and update the relevant user if one does not exist. + */ +export const getStripeCustomerByUser = async (user: User) => { + if (user.customerId) { + const stripeCustomer = await getStripeCustomerById(user.customerId); + + if (!stripeCustomer) { + throw new Error('Missing Stripe customer'); + } + + return { + user, + stripeCustomer, + }; + } + + let stripeCustomer = await getStripeCustomerByEmail(user.email); + + const isSyncRequired = Boolean(stripeCustomer && !stripeCustomer.deleted); + + if (!stripeCustomer) { + stripeCustomer = await stripe.customers.create({ + name: user.name ?? undefined, + email: user.email, + metadata: { + userId: user.id, + }, + }); + } + + const updatedUser = await prisma.user.update({ + where: { + id: user.id, + }, + data: { + customerId: stripeCustomer.id, + }, + }); + + // Sync subscriptions if the customer already exists for back filling the DB + // and local development. + if (isSyncRequired) { + await syncStripeCustomerSubscriptions(user.id, stripeCustomer.id).catch((e) => { + console.error(e); + }); + } + + return { + user: updatedUser, + stripeCustomer, + }; +}; + +const syncStripeCustomerSubscriptions = async (userId: number, stripeCustomerId: string) => { + const stripeSubscriptions = await stripe.subscriptions.list({ + customer: stripeCustomerId, + }); + + await Promise.all( + stripeSubscriptions.data.map(async (subscription) => + onSubscriptionUpdated({ + userId, + subscription, + }), + ), + ); +}; diff --git a/packages/ee/server-only/stripe/get-prices-by-interval.ts b/packages/ee/server-only/stripe/get-prices-by-interval.ts index f621425cc..a5578a813 100644 --- a/packages/ee/server-only/stripe/get-prices-by-interval.ts +++ b/packages/ee/server-only/stripe/get-prices-by-interval.ts @@ -1,4 +1,4 @@ -import Stripe from 'stripe'; +import type Stripe from 'stripe'; import { stripe } from '@documenso/lib/server-only/stripe'; @@ -7,7 +7,14 @@ type PriceWithProduct = Stripe.Price & { product: Stripe.Product }; export type PriceIntervals = Record; -export const getPricesByInterval = async () => { +export type GetPricesByIntervalOptions = { + /** + * Filter products by their meta 'type' attribute. + */ + type?: 'individual'; +}; + +export const getPricesByInterval = async ({ type }: GetPricesByIntervalOptions = {}) => { let { data: prices } = await stripe.prices.search({ query: `active:'true' type:'recurring'`, expand: ['data.product'], @@ -19,8 +26,10 @@ export const getPricesByInterval = async () => { // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const product = price.product as Stripe.Product; + const filter = !type || product.metadata?.type === type; + // Filter out prices for products that are not active. - return product.active; + return product.active && filter; }); const intervals: PriceIntervals = { diff --git a/packages/ee/server-only/stripe/get-prices-by-type.ts b/packages/ee/server-only/stripe/get-prices-by-type.ts new file mode 100644 index 000000000..22124562c --- /dev/null +++ b/packages/ee/server-only/stripe/get-prices-by-type.ts @@ -0,0 +1,11 @@ +import { stripe } from '@documenso/lib/server-only/stripe'; + +export const getPricesByType = async (type: 'individual') => { + const { data: prices } = await stripe.prices.search({ + query: `metadata['type']:'${type}' type:'recurring'`, + expand: ['data.product'], + limit: 100, + }); + + return prices; +}; diff --git a/packages/ee/server-only/stripe/webhook/handler.ts b/packages/ee/server-only/stripe/webhook/handler.ts index 3058ed261..047de7962 100644 --- a/packages/ee/server-only/stripe/webhook/handler.ts +++ b/packages/ee/server-only/stripe/webhook/handler.ts @@ -75,23 +75,23 @@ export const stripeWebhookHandler = async ( // Finally, attempt to get the user ID from the subscription within the database. if (!userId && customerId) { - const result = await prisma.subscription.findFirst({ + const result = await prisma.user.findFirst({ select: { - userId: true, + id: true, }, where: { customerId, }, }); - if (!result?.userId) { + if (!result?.id) { return res.status(500).json({ success: false, message: 'User not found', }); } - userId = result.userId; + userId = result.id; } const subscriptionId = @@ -124,23 +124,23 @@ export const stripeWebhookHandler = async ( ? subscription.customer : subscription.customer.id; - const result = await prisma.subscription.findFirst({ + const result = await prisma.user.findFirst({ select: { - userId: true, + id: true, }, where: { customerId, }, }); - if (!result?.userId) { + if (!result?.id) { return res.status(500).json({ success: false, message: 'User not found', }); } - await onSubscriptionUpdated({ userId: result.userId, subscription }); + await onSubscriptionUpdated({ userId: result.id, subscription }); return res.status(200).json({ success: true, @@ -182,23 +182,23 @@ export const stripeWebhookHandler = async ( }); } - const result = await prisma.subscription.findFirst({ + const result = await prisma.user.findFirst({ select: { - userId: true, + id: true, }, where: { customerId, }, }); - if (!result?.userId) { + if (!result?.id) { return res.status(500).json({ success: false, message: 'User not found', }); } - await onSubscriptionUpdated({ userId: result.userId, subscription }); + await onSubscriptionUpdated({ userId: result.id, subscription }); return res.status(200).json({ success: true, @@ -233,23 +233,23 @@ export const stripeWebhookHandler = async ( }); } - const result = await prisma.subscription.findFirst({ + const result = await prisma.user.findFirst({ select: { - userId: true, + id: true, }, where: { customerId, }, }); - if (!result?.userId) { + if (!result?.id) { return res.status(500).json({ success: false, message: 'User not found', }); } - await onSubscriptionUpdated({ userId: result.userId, subscription }); + await onSubscriptionUpdated({ userId: result.id, subscription }); return res.status(200).json({ success: true, diff --git a/packages/ee/server-only/stripe/webhook/on-subscription-deleted.ts b/packages/ee/server-only/stripe/webhook/on-subscription-deleted.ts index 27ff0cf4d..df1b36f55 100644 --- a/packages/ee/server-only/stripe/webhook/on-subscription-deleted.ts +++ b/packages/ee/server-only/stripe/webhook/on-subscription-deleted.ts @@ -1,4 +1,4 @@ -import { Stripe } from '@documenso/lib/server-only/stripe'; +import type { Stripe } from '@documenso/lib/server-only/stripe'; import { prisma } from '@documenso/prisma'; import { SubscriptionStatus } from '@documenso/prisma/client'; @@ -7,12 +7,9 @@ export type OnSubscriptionDeletedOptions = { }; export const onSubscriptionDeleted = async ({ subscription }: OnSubscriptionDeletedOptions) => { - const customerId = - typeof subscription.customer === 'string' ? subscription.customer : subscription.customer?.id; - await prisma.subscription.update({ where: { - customerId, + planId: subscription.id, }, data: { status: SubscriptionStatus.INACTIVE, diff --git a/packages/ee/server-only/stripe/webhook/on-subscription-updated.ts b/packages/ee/server-only/stripe/webhook/on-subscription-updated.ts index dfa22d128..d7ce7b062 100644 --- a/packages/ee/server-only/stripe/webhook/on-subscription-updated.ts +++ b/packages/ee/server-only/stripe/webhook/on-subscription-updated.ts @@ -1,6 +1,6 @@ import { match } from 'ts-pattern'; -import { Stripe } from '@documenso/lib/server-only/stripe'; +import type { Stripe } from '@documenso/lib/server-only/stripe'; import { prisma } from '@documenso/prisma'; import { SubscriptionStatus } from '@documenso/prisma/client'; @@ -13,9 +13,6 @@ export const onSubscriptionUpdated = async ({ userId, subscription, }: OnSubscriptionUpdatedOptions) => { - const customerId = - typeof subscription.customer === 'string' ? subscription.customer : subscription.customer?.id; - const status = match(subscription.status) .with('active', () => SubscriptionStatus.ACTIVE) .with('past_due', () => SubscriptionStatus.PAST_DUE) @@ -23,22 +20,22 @@ export const onSubscriptionUpdated = async ({ await prisma.subscription.upsert({ where: { - customerId, + planId: subscription.id, }, create: { - customerId, status: status, planId: subscription.id, priceId: subscription.items.data[0].price.id, periodEnd: new Date(subscription.current_period_end * 1000), userId, + cancelAtPeriodEnd: subscription.cancel_at_period_end, }, update: { - customerId, status: status, planId: subscription.id, priceId: subscription.items.data[0].price.id, periodEnd: new Date(subscription.current_period_end * 1000), + cancelAtPeriodEnd: subscription.cancel_at_period_end, }, }); }; diff --git a/packages/lib/server-only/admin/get-users-stats.ts b/packages/lib/server-only/admin/get-users-stats.ts index 13db21d83..09892171a 100644 --- a/packages/lib/server-only/admin/get-users-stats.ts +++ b/packages/lib/server-only/admin/get-users-stats.ts @@ -9,7 +9,9 @@ export const getUsersWithSubscriptionsCount = async () => { return await prisma.user.count({ where: { Subscription: { - status: SubscriptionStatus.ACTIVE, + some: { + status: SubscriptionStatus.ACTIVE, + }, }, }, }); diff --git a/packages/lib/server-only/subscription/get-subscription-by-user-id.ts b/packages/lib/server-only/subscription/get-subscription-by-user-id.ts deleted file mode 100644 index 772134f7c..000000000 --- a/packages/lib/server-only/subscription/get-subscription-by-user-id.ts +++ /dev/null @@ -1,15 +0,0 @@ -'use server'; - -import { prisma } from '@documenso/prisma'; - -export type GetSubscriptionByUserIdOptions = { - userId: number; -}; - -export const getSubscriptionByUserId = async ({ userId }: GetSubscriptionByUserIdOptions) => { - return await prisma.subscription.findFirst({ - where: { - userId, - }, - }); -}; diff --git a/packages/lib/server-only/subscription/get-subscriptions-by-user-id.ts b/packages/lib/server-only/subscription/get-subscriptions-by-user-id.ts new file mode 100644 index 000000000..33f6255bd --- /dev/null +++ b/packages/lib/server-only/subscription/get-subscriptions-by-user-id.ts @@ -0,0 +1,15 @@ +'use server'; + +import { prisma } from '@documenso/prisma'; + +export type GetSubscriptionsByUserIdOptions = { + userId: number; +}; + +export const getSubscriptionsByUserId = async ({ userId }: GetSubscriptionsByUserIdOptions) => { + return await prisma.subscription.findMany({ + where: { + userId, + }, + }); +}; diff --git a/packages/lib/server-only/user/create-user.ts b/packages/lib/server-only/user/create-user.ts index 46c43b93b..f7db60c85 100644 --- a/packages/lib/server-only/user/create-user.ts +++ b/packages/lib/server-only/user/create-user.ts @@ -1,9 +1,11 @@ import { hash } from 'bcrypt'; +import { getStripeCustomerByUser } from '@documenso/ee/server-only/stripe/get-customer'; import { prisma } from '@documenso/prisma'; import { IdentityProvider } from '@documenso/prisma/client'; import { SALT_ROUNDS } from '../../constants/auth'; +import { getFlag } from '../../universal/get-feature-flag'; export interface CreateUserOptions { name: string; @@ -13,6 +15,8 @@ export interface CreateUserOptions { } export const createUser = async ({ name, email, password, signature }: CreateUserOptions) => { + const isBillingEnabled = await getFlag('app_billing'); + const hashedPassword = await hash(password, SALT_ROUNDS); const userExists = await prisma.user.findFirst({ @@ -25,7 +29,7 @@ export const createUser = async ({ name, email, password, signature }: CreateUse throw new Error('User already exists'); } - return await prisma.user.create({ + let user = await prisma.user.create({ data: { name, email: email.toLowerCase(), @@ -34,4 +38,15 @@ export const createUser = async ({ name, email, password, signature }: CreateUse identityProvider: IdentityProvider.DOCUMENSO, }, }); + + if (isBillingEnabled) { + try { + const stripeSession = await getStripeCustomerByUser(user); + user = stripeSession.user; + } catch (e) { + console.error(e); + } + } + + return user; }; diff --git a/packages/prisma/migrations/20231206073509_add_multple_subscriptions/migration.sql b/packages/prisma/migrations/20231206073509_add_multple_subscriptions/migration.sql new file mode 100644 index 000000000..931815bf0 --- /dev/null +++ b/packages/prisma/migrations/20231206073509_add_multple_subscriptions/migration.sql @@ -0,0 +1,31 @@ +/* + Warnings: + + - A unique constraint covering the columns `[planId]` on the table `Subscription` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[customerId]` on the table `User` will be added. If there are existing duplicate values, this will fail. + - Made the column `planId` on table `Subscription` required. This step will fail if there are existing NULL values in that column. + - Made the column `priceId` on table `Subscription` required. This step will fail if there are existing NULL values in that column. + +*/ +-- Custom migration statement +DELETE FROM "Subscription" WHERE "planId" IS NULL OR "priceId" IS NULL; + +-- DropIndex +DROP INDEX "Subscription_customerId_key"; + +-- DropIndex +DROP INDEX "Subscription_userId_key"; + +-- AlterTable +ALTER TABLE "Subscription" ALTER COLUMN "planId" SET NOT NULL, +ALTER COLUMN "priceId" SET NOT NULL; + +-- AlterTable +ALTER TABLE "User" ADD COLUMN "customerId" TEXT; +ALTER TABLE "Subscription" DROP COLUMN "customerId"; + +-- CreateIndex +CREATE UNIQUE INDEX "Subscription_planId_key" ON "Subscription"("planId"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_customerId_key" ON "User"("customerId"); diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 75c175adc..88b79517b 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -21,6 +21,7 @@ enum Role { model User { id Int @id @default(autoincrement()) name String? + customerId String? @unique email String @unique emailVerified DateTime? password String? @@ -34,7 +35,7 @@ model User { accounts Account[] sessions Session[] Document Document[] - Subscription Subscription? + Subscription Subscription[] PasswordResetToken PasswordResetToken[] twoFactorSecret String? twoFactorEnabled Boolean @default(false) @@ -72,18 +73,16 @@ enum SubscriptionStatus { model Subscription { id Int @id @default(autoincrement()) status SubscriptionStatus @default(INACTIVE) - planId String? - priceId String? - customerId String + planId String @unique + priceId String periodEnd DateTime? - userId Int @unique + userId Int createdAt DateTime @default(now()) updatedAt DateTime @updatedAt cancelAtPeriodEnd Boolean @default(false) User User @relation(fields: [userId], references: [id], onDelete: Cascade) - @@unique([customerId]) @@index([userId]) } diff --git a/packages/tsconfig/process-env.d.ts b/packages/tsconfig/process-env.d.ts index 717f13ade..dda8f771b 100644 --- a/packages/tsconfig/process-env.d.ts +++ b/packages/tsconfig/process-env.d.ts @@ -10,8 +10,6 @@ declare namespace NodeJS { NEXT_PRIVATE_ENCRYPTION_KEY: string; NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID: string; - NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID: string; - NEXT_PUBLIC_STRIPE_FREE_PLAN_ID?: string; NEXT_PRIVATE_STRIPE_API_KEY: string; NEXT_PRIVATE_STRIPE_WEBHOOK_SECRET: string; diff --git a/render.yaml b/render.yaml index eb213c32c..9fe1bd2e9 100644 --- a/render.yaml +++ b/render.yaml @@ -67,14 +67,10 @@ services: sync: false - key: NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID sync: false - - key: NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID - sync: false # Features - key: NEXT_PUBLIC_POSTHOG_KEY sync: false - - key: NEXT_PUBLIC_POSTHOG_HOST - value: 'https://eu.posthog.com' - key: NEXT_PUBLIC_FEATURE_BILLING_ENABLED sync: false diff --git a/turbo.json b/turbo.json index 36b169a80..d96f681d8 100644 --- a/turbo.json +++ b/turbo.json @@ -40,11 +40,8 @@ "NEXT_PUBLIC_WEBAPP_URL", "NEXT_PUBLIC_MARKETING_URL", "NEXT_PUBLIC_POSTHOG_KEY", - "NEXT_PUBLIC_POSTHOG_HOST", "NEXT_PUBLIC_FEATURE_BILLING_ENABLED", - "NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_YEARLY_PRICE_ID", "NEXT_PUBLIC_STRIPE_COMMUNITY_PLAN_MONTHLY_PRICE_ID", - "NEXT_PUBLIC_STRIPE_FREE_PLAN_ID", "NEXT_PRIVATE_DATABASE_URL", "NEXT_PRIVATE_DIRECT_DATABASE_URL", "NEXT_PRIVATE_GOOGLE_CLIENT_ID", From 1eeb5fb103f81be2866177cc9aa9b4004b918e1c Mon Sep 17 00:00:00 2001 From: Mythie Date: Thu, 14 Dec 2023 15:28:27 +1100 Subject: [PATCH 25/83] 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) => { -
+
{/* @@ -238,7 +242,7 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => { - {(step === 'NAME' || step === 'SIGN') && ( + {(step === STEP.NAME || step === STEP.SIGN) && ( Date: Sat, 16 Dec 2023 00:30:52 +0530 Subject: [PATCH 29/83] fixed z-index --- packages/ui/primitives/toast.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/primitives/toast.tsx b/packages/ui/primitives/toast.tsx index 8b8355323..89616c132 100644 --- a/packages/ui/primitives/toast.tsx +++ b/packages/ui/primitives/toast.tsx @@ -15,7 +15,7 @@ const ToastViewport = React.forwardRef< Date: Sat, 16 Dec 2023 14:41:18 +1100 Subject: [PATCH 30/83] fix: lint errors --- apps/marketing/src/components/constants.ts | 2 +- apps/web/src/components/forms/signup.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/marketing/src/components/constants.ts b/apps/marketing/src/components/constants.ts index 1f11df116..dcbb631a2 100644 --- a/apps/marketing/src/components/constants.ts +++ b/apps/marketing/src/components/constants.ts @@ -1,5 +1,5 @@ export const STEP = { EMAIL: 'EMAIL', NAME: 'NAME', - SIGN: "SIGN" + SIGN: 'SIGN', } as const; diff --git a/apps/web/src/components/forms/signup.tsx b/apps/web/src/components/forms/signup.tsx index d67449b17..3988314c5 100644 --- a/apps/web/src/components/forms/signup.tsx +++ b/apps/web/src/components/forms/signup.tsx @@ -41,6 +41,7 @@ export type SignUpFormProps = { export const SignUpForm = ({ className }: SignUpFormProps) => { const { toast } = useToast(); + const analytics = useAnalytics(); const form = useForm({ values: { From 2056de2e16b1c9f9f048c71d20fd4dd36ee14423 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Tue, 19 Dec 2023 13:34:29 +0530 Subject: [PATCH 31/83] feat: added koyeb as a deploy option Signed-off-by: Adithya Krishna --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 24d932858..77a5e4800 100644 --- a/README.md +++ b/README.md @@ -286,6 +286,10 @@ WantedBy=multi-user.target [![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/documenso/documenso) +### Koyeb + +[![Deploy to Koyeb](https://www.koyeb.com/static/images/deploy/button.svg)](https://app.koyeb.com/deploy?type=git&repository=github.com/documenso/documenso&branch=main&name=documenso-app&builder=dockerfile&dockerfile=/docker/Dockerfile) + ## Troubleshooting ### I'm not receiving any emails when using the developer quickstart. From eda635e2db9c87db7e03a2de2ca3cc6287bdbc1c Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Tue, 19 Dec 2023 13:35:18 +0530 Subject: [PATCH 32/83] feat: added custom railway config Signed-off-by: Adithya Krishna --- railway.toml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 railway.toml diff --git a/railway.toml b/railway.toml new file mode 100644 index 000000000..672eaa7e5 --- /dev/null +++ b/railway.toml @@ -0,0 +1,4 @@ +[build] + +builder = "DOCKERFILE" +dockerfilePath = "/docker/Dockerfile" From c949c4701b018269449ded97f26192eb8428f824 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Tue, 19 Dec 2023 13:51:17 +0530 Subject: [PATCH 33/83] chore: udpated railway template link Signed-off-by: Adithya Krishna --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 77a5e4800..ffd12d2ac 100644 --- a/README.md +++ b/README.md @@ -280,7 +280,7 @@ WantedBy=multi-user.target ### Railway -[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/DjrRRX) +[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/bG6D4p?referralCode=OEa9MT) ### Render From 775a1b774d9cabaa48a37cc18913296a742ab62c Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Tue, 19 Dec 2023 17:15:19 +0530 Subject: [PATCH 34/83] chore: fix vulnerability with sharp Signed-off-by: Adithya Krishna --- apps/marketing/package.json | 2 +- apps/web/package.json | 2 +- package-lock.json | 830 +++++++++++++++++++++++------------- 3 files changed, 524 insertions(+), 310 deletions(-) diff --git a/apps/marketing/package.json b/apps/marketing/package.json index 83d13d07c..1cfb7337f 100644 --- a/apps/marketing/package.json +++ b/apps/marketing/package.json @@ -36,7 +36,7 @@ "react-hook-form": "^7.43.9", "react-icons": "^4.11.0", "recharts": "^2.7.2", - "sharp": "0.32.5", + "sharp": "0.33.1", "typescript": "5.2.2", "zod": "^3.22.4" }, diff --git a/apps/web/package.json b/apps/web/package.json index 150982c2d..89675318f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -42,7 +42,7 @@ "react-hotkeys-hook": "^4.4.1", "react-icons": "^4.11.0", "react-rnd": "^10.4.1", - "sharp": "0.32.5", + "sharp": "0.33.1", "ts-pattern": "^5.0.5", "typescript": "5.2.2", "uqr": "^0.1.2", diff --git a/package-lock.json b/package-lock.json index 61c4749e6..d7dcdfa77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55,7 +55,7 @@ "react-hook-form": "^7.43.9", "react-icons": "^4.11.0", "recharts": "^2.7.2", - "sharp": "0.32.5", + "sharp": "0.33.1", "typescript": "5.2.2", "zod": "^3.22.4" }, @@ -71,6 +71,45 @@ "integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A==", "dev": true }, + "apps/marketing/node_modules/sharp": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.1.tgz", + "integrity": "sha512-iAYUnOdTqqZDb3QjMneBKINTllCJDZ3em6WaWy7NPECM4aHncvqHRm0v0bN9nqJxMiwamv5KIdauJ6lUzKDpTQ==", + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "semver": "^7.5.4" + }, + "engines": { + "libvips": ">=8.15.0", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.1", + "@img/sharp-darwin-x64": "0.33.1", + "@img/sharp-libvips-darwin-arm64": "1.0.0", + "@img/sharp-libvips-darwin-x64": "1.0.0", + "@img/sharp-libvips-linux-arm": "1.0.0", + "@img/sharp-libvips-linux-arm64": "1.0.0", + "@img/sharp-libvips-linux-s390x": "1.0.0", + "@img/sharp-libvips-linux-x64": "1.0.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.0", + "@img/sharp-libvips-linuxmusl-x64": "1.0.0", + "@img/sharp-linux-arm": "0.33.1", + "@img/sharp-linux-arm64": "0.33.1", + "@img/sharp-linux-s390x": "0.33.1", + "@img/sharp-linux-x64": "0.33.1", + "@img/sharp-linuxmusl-arm64": "0.33.1", + "@img/sharp-linuxmusl-x64": "0.33.1", + "@img/sharp-wasm32": "0.33.1", + "@img/sharp-win32-ia32": "0.33.1", + "@img/sharp-win32-x64": "0.33.1" + } + }, "apps/marketing/node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -116,7 +155,7 @@ "react-hotkeys-hook": "^4.4.1", "react-icons": "^4.11.0", "react-rnd": "^10.4.1", - "sharp": "0.32.5", + "sharp": "0.33.1", "ts-pattern": "^5.0.5", "typescript": "5.2.2", "uqr": "^0.1.2", @@ -136,6 +175,45 @@ "integrity": "sha512-O+z53uwx64xY7D6roOi4+jApDGFg0qn6WHcxe5QeqjMaTezBO/mxdfFXIVAVVyNWKx84OmPB3L8kbVYOTeN34A==", "dev": true }, + "apps/web/node_modules/sharp": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.1.tgz", + "integrity": "sha512-iAYUnOdTqqZDb3QjMneBKINTllCJDZ3em6WaWy7NPECM4aHncvqHRm0v0bN9nqJxMiwamv5KIdauJ6lUzKDpTQ==", + "hasInstallScript": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "semver": "^7.5.4" + }, + "engines": { + "libvips": ">=8.15.0", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.1", + "@img/sharp-darwin-x64": "0.33.1", + "@img/sharp-libvips-darwin-arm64": "1.0.0", + "@img/sharp-libvips-darwin-x64": "1.0.0", + "@img/sharp-libvips-linux-arm": "1.0.0", + "@img/sharp-libvips-linux-arm64": "1.0.0", + "@img/sharp-libvips-linux-s390x": "1.0.0", + "@img/sharp-libvips-linux-x64": "1.0.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.0", + "@img/sharp-libvips-linuxmusl-x64": "1.0.0", + "@img/sharp-linux-arm": "0.33.1", + "@img/sharp-linux-arm64": "0.33.1", + "@img/sharp-linux-s390x": "0.33.1", + "@img/sharp-linux-x64": "0.33.1", + "@img/sharp-linuxmusl-arm64": "0.33.1", + "@img/sharp-linuxmusl-x64": "0.33.1", + "@img/sharp-wasm32": "0.33.1", + "@img/sharp-win32-ia32": "0.33.1", + "@img/sharp-win32-x64": "0.33.1" + } + }, "apps/web/node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -1889,6 +1967,15 @@ "resolved": "https://registry.npmjs.org/@effect-ts/system/-/system-0.57.5.tgz", "integrity": "sha512-/crHGujo0xnuHIYNc1VgP0HGJGFSoSqq88JFXe6FmFyXPpWt8Xu39LyLg7rchsxfXFeEdA9CrIZvLV5eswXV5g==" }, + "node_modules/@emnapi/runtime": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.44.0.tgz", + "integrity": "sha512-ZX/etZEZw8DR7zAB1eVQT40lNo0jeqpb6dCgOvctB6FIQ5PoXfMuNY8+ayQfu8tNQbAB8gQWSSJupR8NxeiZXw==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emotion/is-prop-valid": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", @@ -2171,6 +2258,437 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==" }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.1.tgz", + "integrity": "sha512-esr2BZ1x0bo+wl7Gx2hjssYhjrhUsD88VQulI0FrG8/otRQUOxLWHMBd1Y1qo2Gfg2KUvXNpT0ASnV9BzJCexw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.1.tgz", + "integrity": "sha512-YrnuB3bXuWdG+hJlXtq7C73lF8ampkhU3tMxg5Hh+E7ikxbUVOU9nlNtVTloDXz6pRHt2y2oKJq7DY/yt+UXYw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.0.tgz", + "integrity": "sha512-VzYd6OwnUR81sInf3alj1wiokY50DjsHz5bvfnsFpxs5tqQxESoHtJO6xyksDs3RIkyhMWq2FufXo6GNSU9BMw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "macos": ">=11", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.0.tgz", + "integrity": "sha512-dD9OznTlHD6aovRswaPNEy8dKtSAmNo4++tO7uuR4o5VxbVAOoEQ1uSmN4iFAdQneTHws1lkTZeiXPrcCkh6IA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "macos": ">=10.13", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.0.tgz", + "integrity": "sha512-VwgD2eEikDJUk09Mn9Dzi1OW2OJFRQK+XlBTkUNmAWPrtj8Ly0yq05DFgu1VCMx2/DqCGQVi5A1dM9hTmxf3uw==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.0.tgz", + "integrity": "sha512-xTYThiqEZEZc0PRU90yVtM3KE7lw1bKdnDQ9kCTHWbqWyHOe4NpPOtMGy27YnN51q0J5dqRrvicfPbALIOeAZA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.0.tgz", + "integrity": "sha512-o9E46WWBC6JsBlwU4QyU9578G77HBDT1NInd+aERfxeOPbk0qBZHgoDsQmA2v9TbqJRWzoBPx1aLOhprBMgPjw==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.0.tgz", + "integrity": "sha512-naldaJy4hSVhWBgEjfdBY85CAa4UO+W1nx6a1sWStHZ7EUfNiuBTTN2KUYT5dH1+p/xij1t2QSXfCiFJoC5S/Q==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.0.tgz", + "integrity": "sha512-OdorplCyvmSAPsoJLldtLh3nLxRrkAAAOHsGWGDYfN0kh730gifK+UZb3dWORRa6EusNqCTjfXV4GxvgJ/nPDQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.0.tgz", + "integrity": "sha512-FW8iK6rJrg+X2jKD0Ajhjv6y74lToIBEvkZhl42nZt563FfxkCYacrXZtd+q/sRQDypQLzY5WdLkVTbJoPyqNg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.1.tgz", + "integrity": "sha512-Ii4X1vnzzI4j0+cucsrYA5ctrzU9ciXERfJR633S2r39CiD8npqH2GMj63uFZRCFt3E687IenAdbwIpQOJ5BNA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.1.tgz", + "integrity": "sha512-59B5GRO2d5N3tIfeGHAbJps7cLpuWEQv/8ySd9109ohQ3kzyCACENkFVAnGPX00HwPTQcaBNF7HQYEfZyZUFfw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.1.tgz", + "integrity": "sha512-tRGrb2pHnFUXpOAj84orYNxHADBDIr0J7rrjwQrTNMQMWA4zy3StKmMvwsI7u3dEZcgwuMMooIIGWEWOjnmG8A==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.28", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.1.tgz", + "integrity": "sha512-4y8osC0cAc1TRpy02yn5omBeloZZwS62fPZ0WUAYQiLhSFSpWJfY/gMrzKzLcHB9ulUV6ExFiu2elMaixKDbeg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "glibc": ">=2.26", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.1.tgz", + "integrity": "sha512-D3lV6clkqIKUizNS8K6pkuCKNGmWoKlBGh5p0sLO2jQERzbakhu4bVX1Gz+RS4vTZBprKlWaf+/Rdp3ni2jLfA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.1.tgz", + "integrity": "sha512-LOGKNu5w8uu1evVqUAUKTix2sQu1XDRIYbsi5Q0c/SrXhvJ4QyOx+GaajxmOg5PZSsSnCYPSmhjHHsRBx06/wQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "musl": ">=1.2.2", + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.1.tgz", + "integrity": "sha512-vWI/sA+0p+92DLkpAMb5T6I8dg4z2vzCUnp8yvxHlwBpzN8CIcO3xlSXrLltSvK6iMsVMNswAv+ub77rsf25lA==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^0.44.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.1.tgz", + "integrity": "sha512-/xhYkylsKL05R+NXGJc9xr2Tuw6WIVl2lubFJaFYfW4/MQ4J+dgjIo/T4qjNRizrqs/szF/lC9a5+updmY9jaQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.1", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.1.tgz", + "integrity": "sha512-XaM69X0n6kTEsp9tVYYLhXdg7Qj32vYJlAKRutxUsm1UlgQNx6BOhHwZPwukCGXBU2+tH87ip2eV1I/E8MQnZg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0", + "yarn": ">=3.2.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -6901,11 +7419,6 @@ "dequal": "^2.0.3" } }, - "node_modules/b4a": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", - "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==" - }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -8350,14 +8863,6 @@ "node": ">=8" } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -9997,14 +10502,6 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "engines": { - "node": ">=6" - } - }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -10039,11 +10536,6 @@ "node": ">=6.0.0" } }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==" - }, "node_modules/fast-folder-size": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/fast-folder-size/-/fast-folder-size-1.6.1.tgz", @@ -10373,11 +10865,6 @@ "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==" }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, "node_modules/fs-extra": { "version": "11.1.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", @@ -10647,11 +11134,6 @@ "node": ">=10" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" - }, "node_modules/glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -13739,11 +14221,6 @@ "node": ">=10" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" - }, "node_modules/moo": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", @@ -13868,11 +14345,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" - }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -14023,17 +14495,6 @@ "tslib": "^2.0.3" } }, - "node_modules/node-abi": { - "version": "3.51.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz", - "integrity": "sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-addon-api": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", @@ -14986,111 +15447,6 @@ "preact": ">=10" } }, - "node_modules/prebuild-install": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz", - "integrity": "sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/prebuild-install/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, - "node_modules/prebuild-install/node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prebuild-install/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prebuild-install/node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/prebuild-install/node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/prebuild-install/node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -15385,11 +15741,6 @@ } ] }, - "node_modules/queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==" - }, "node_modules/quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -15430,28 +15781,6 @@ "node": ">= 0.8" } }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/re-resizable": { "version": "6.9.6", "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.6.tgz", @@ -16758,82 +17087,6 @@ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, - "node_modules/sharp": { - "version": "0.32.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.5.tgz", - "integrity": "sha512-0dap3iysgDkNaPOaOL4X/0akdu0ma62GcdC2NBQ+93eqpePdDdr2/LM0sFdDSMmN7yS+odyZtPsb7tx/cYBKnQ==", - "hasInstallScript": true, - "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.2", - "node-addon-api": "^6.1.0", - "prebuild-install": "^7.1.1", - "semver": "^7.5.4", - "simple-get": "^4.0.1", - "tar-fs": "^3.0.4", - "tunnel-agent": "^0.6.0" - }, - "engines": { - "node": ">=14.15.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/sharp/node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sharp/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/sharp/node_modules/node-addon-api": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", - "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==" - }, - "node_modules/sharp/node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -16904,7 +17157,8 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "optional": true }, "node_modules/simple-get": { "version": "3.1.1", @@ -17130,15 +17384,6 @@ "node": ">=10.0.0" } }, - "node_modules/streamx": { - "version": "2.15.5", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.5.tgz", - "integrity": "sha512-9thPGMkKC2GctCzyCUjME3yR03x2xNo0GPKGkRw2UMYN+gqWa9uqpyNWhmsNCutU5zHmkUum0LsCRQTXUgUCAg==", - "dependencies": { - "fast-fifo": "^1.1.0", - "queue-tick": "^1.0.1" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -17626,26 +17871,6 @@ "node": ">=10" } }, - "node_modules/tar-fs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", - "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", - "dependencies": { - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - } - }, - "node_modules/tar-stream": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", - "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, "node_modules/text-extensions": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz", @@ -17990,17 +18215,6 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/turbo": { "version": "1.10.16", "resolved": "https://registry.npmjs.org/turbo/-/turbo-1.10.16.tgz", From 01caa949d9b7f9ba0118a84c1bc46895cbe72daa Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Tue, 19 Dec 2023 22:20:50 +0530 Subject: [PATCH 35/83] feat: show document title for delete dialog Signed-off-by: Adithya Krishna --- .../app/(dashboard)/documents/data-table-action-dropdown.tsx | 1 + .../src/app/(dashboard)/documents/delete-document-dialog.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) 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..ee5552f8c 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 @@ -164,6 +164,7 @@ export const DataTableActionDropdown = ({ row }: DataTableActionDropdownProps) = 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..8de13bffe 100644 --- a/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx +++ b/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx @@ -21,6 +21,7 @@ type DeleteDraftDocumentDialogProps = { open: boolean; onOpenChange: (_open: boolean) => void; status: DocumentStatus; + documentTitle: string; }; export const DeleteDocumentDialog = ({ @@ -28,6 +29,7 @@ export const DeleteDocumentDialog = ({ open, onOpenChange, status, + documentTitle, }: DeleteDraftDocumentDialogProps) => { const router = useRouter(); @@ -42,7 +44,7 @@ export const DeleteDocumentDialog = ({ toast({ title: 'Document deleted', - description: 'Your document has been successfully deleted.', + description: `Your document '${documentTitle} has been successfully deleted.`, duration: 5000, }); From 1af909835dc373e1405d03a939be91848a15e2b8 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Tue, 19 Dec 2023 22:25:23 +0530 Subject: [PATCH 36/83] chore: updated title to double quotes Signed-off-by: Adithya Krishna --- .../src/app/(dashboard)/documents/delete-document-dialog.tsx | 2 +- 1 file changed, 1 insertion(+), 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 8de13bffe..11560aa21 100644 --- a/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx +++ b/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx @@ -44,7 +44,7 @@ export const DeleteDocumentDialog = ({ toast({ title: 'Document deleted', - description: `Your document '${documentTitle} has been successfully deleted.`, + description: `Your document "${documentTitle}" has been successfully deleted.`, duration: 5000, }); From 84a0c3981008794d4f7791a7951c636436e62975 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Wed, 20 Dec 2023 10:36:06 +0530 Subject: [PATCH 37/83] chore: made requested changes Signed-off-by: Adithya Krishna --- .../documents/delete-document-dialog.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 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 11560aa21..7b82f93bc 100644 --- a/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx +++ b/apps/web/src/app/(dashboard)/documents/delete-document-dialog.tsx @@ -41,10 +41,16 @@ 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: `Your document "${documentTitle}" has been successfully deleted.`, + description: deletedFileToastDescription, duration: 5000, }); @@ -74,7 +80,10 @@ export const DeleteDocumentDialog = ({ !isLoading && onOpenChange(value)}> - Do you want to delete this document? + + Do you want to delete the {documentTitle}{' '} + document? + Please note that this action is irreversible. Once confirmed, your document will be @@ -83,7 +92,7 @@ export const DeleteDocumentDialog = ({ {status !== DocumentStatus.DRAFT && ( -
+
Date: Thu, 21 Dec 2023 17:01:12 +1100 Subject: [PATCH 38/83] fix: swap server-actions for trpc mutations --- apps/marketing/src/components/constants.ts | 2 +- .../templates/[id]/edit-template.tsx | 9 +++--- .../add-template-fields.action.ts | 32 ------------------- .../add-template-placeholders.action.ts | 28 ---------------- packages/trpc/server/field-router/router.ts | 23 +++++++++++++ packages/trpc/server/field-router/schema.ts | 19 +++++++++++ .../trpc/server/recipient-router/router.ts | 32 ++++++++++++++++++- .../trpc/server/recipient-router/schema.ts | 23 +++++++++++++ ...d-template-placeholder-recipients.types.ts | 2 +- 9 files changed, 103 insertions(+), 67 deletions(-) delete mode 100644 apps/web/src/components/forms/edit-template/add-template-fields.action.ts delete mode 100644 apps/web/src/components/forms/edit-template/add-template-placeholders.action.ts diff --git a/apps/marketing/src/components/constants.ts b/apps/marketing/src/components/constants.ts index 1f11df116..dcbb631a2 100644 --- a/apps/marketing/src/components/constants.ts +++ b/apps/marketing/src/components/constants.ts @@ -1,5 +1,5 @@ export const STEP = { EMAIL: 'EMAIL', NAME: 'NAME', - SIGN: "SIGN" + SIGN: 'SIGN', } as const; 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 920cac247..bdc769e79 100644 --- a/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx +++ b/apps/web/src/app/(dashboard)/templates/[id]/edit-template.tsx @@ -5,6 +5,7 @@ import { useState } from 'react'; import { useRouter } from 'next/navigation'; import type { DocumentData, Field, Recipient, Template, User } from '@documenso/prisma/client'; +import { trpc } from '@documenso/trpc/react'; import { cn } from '@documenso/ui/lib/utils'; import { Card, CardContent } from '@documenso/ui/primitives/card'; import { @@ -20,9 +21,6 @@ import { AddTemplatePlaceholderRecipientsFormPartial } from '@documenso/ui/primi 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'; -import { addTemplatePlaceholders } from '~/components/forms/edit-template/add-template-placeholders.action'; - export type EditTemplateFormProps = { className?: string; user: User; @@ -63,11 +61,14 @@ export const EditTemplateForm = ({ const currentDocumentFlow = documentFlow[step]; + const { mutateAsync: addTemplateFields } = trpc.field.addTemplateFields.useMutation(); + const { mutateAsync: addTemplateSigners } = trpc.recipient.addTemplateSigners.useMutation(); + const onAddTemplatePlaceholderFormSubmit = async ( data: TAddTemplatePlacholderRecipientsFormSchema, ) => { try { - await addTemplatePlaceholders({ + await addTemplateSigners({ templateId: template.id, signers: data.signers, }); 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 deleted file mode 100644 index 845ae5faf..000000000 --- a/apps/web/src/components/forms/edit-template/add-template-fields.action.ts +++ /dev/null @@ -1,32 +0,0 @@ -'use server'; - -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; -import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-fields-for-template'; -import type { 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 deleted file mode 100644 index ba5a3e004..000000000 --- a/apps/web/src/components/forms/edit-template/add-template-placeholders.action.ts +++ /dev/null @@ -1,28 +0,0 @@ -'use server'; - -import { getRequiredServerComponentSession } from '@documenso/lib/next-auth/get-server-component-session'; -import { setRecipientsForTemplate } from '@documenso/lib/server-only/recipient/set-recipients-for-template'; -import type { 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/trpc/server/field-router/router.ts b/packages/trpc/server/field-router/router.ts index 7d049df0d..07cdcd347 100644 --- a/packages/trpc/server/field-router/router.ts +++ b/packages/trpc/server/field-router/router.ts @@ -2,11 +2,13 @@ import { TRPCError } from '@trpc/server'; import { removeSignedFieldWithToken } from '@documenso/lib/server-only/field/remove-signed-field-with-token'; import { setFieldsForDocument } from '@documenso/lib/server-only/field/set-fields-for-document'; +import { setFieldsForTemplate } from '@documenso/lib/server-only/field/set-fields-for-template'; import { signFieldWithToken } from '@documenso/lib/server-only/field/sign-field-with-token'; import { authenticatedProcedure, procedure, router } from '../trpc'; import { ZAddFieldsMutationSchema, + ZAddTemplateFieldsMutationSchema, ZRemovedSignedFieldWithTokenMutationSchema, ZSignFieldWithTokenMutationSchema, } from './schema'; @@ -42,6 +44,27 @@ export const fieldRouter = router({ } }), + addTemplateFields: authenticatedProcedure + .input(ZAddTemplateFieldsMutationSchema) + .mutation(async ({ input, ctx }) => { + const { templateId, fields } = input; + + await setFieldsForTemplate({ + userId: ctx.user.id, + templateId, + fields: fields.map((field) => ({ + id: field.nativeId, + signerEmail: field.signerEmail, + type: field.type, + pageNumber: field.pageNumber, + pageX: field.pageX, + pageY: field.pageY, + pageWidth: field.pageWidth, + pageHeight: field.pageHeight, + })), + }); + }), + signFieldWithToken: procedure .input(ZSignFieldWithTokenMutationSchema) .mutation(async ({ input }) => { diff --git a/packages/trpc/server/field-router/schema.ts b/packages/trpc/server/field-router/schema.ts index d9f207adb..9bd576667 100644 --- a/packages/trpc/server/field-router/schema.ts +++ b/packages/trpc/server/field-router/schema.ts @@ -21,6 +21,25 @@ export const ZAddFieldsMutationSchema = z.object({ export type TAddFieldsMutationSchema = z.infer; +export const ZAddTemplateFieldsMutationSchema = z.object({ + templateId: z.number(), + fields: z.array( + z.object({ + formId: z.string().min(1), + nativeId: z.number().optional(), + type: z.nativeEnum(FieldType), + signerEmail: z.string().min(1), + 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 TAddTemplateFieldsMutationSchema = z.infer; + export const ZSignFieldWithTokenMutationSchema = z.object({ token: z.string(), fieldId: z.number(), diff --git a/packages/trpc/server/recipient-router/router.ts b/packages/trpc/server/recipient-router/router.ts index 913749dde..09097895c 100644 --- a/packages/trpc/server/recipient-router/router.ts +++ b/packages/trpc/server/recipient-router/router.ts @@ -2,9 +2,14 @@ import { TRPCError } from '@trpc/server'; import { completeDocumentWithToken } from '@documenso/lib/server-only/document/complete-document-with-token'; import { setRecipientsForDocument } from '@documenso/lib/server-only/recipient/set-recipients-for-document'; +import { setRecipientsForTemplate } from '@documenso/lib/server-only/recipient/set-recipients-for-template'; import { authenticatedProcedure, procedure, router } from '../trpc'; -import { ZAddSignersMutationSchema, ZCompleteDocumentWithTokenMutationSchema } from './schema'; +import { + ZAddSignersMutationSchema, + ZAddTemplateSignersMutationSchema, + ZCompleteDocumentWithTokenMutationSchema, +} from './schema'; export const recipientRouter = router({ addSigners: authenticatedProcedure @@ -32,6 +37,31 @@ export const recipientRouter = router({ } }), + addTemplateSigners: authenticatedProcedure + .input(ZAddTemplateSignersMutationSchema) + .mutation(async ({ input, ctx }) => { + try { + const { templateId, signers } = input; + + return await setRecipientsForTemplate({ + userId: ctx.user.id, + templateId, + recipients: signers.map((signer) => ({ + id: signer.nativeId, + email: signer.email, + name: signer.name, + })), + }); + } catch (err) { + console.error(err); + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'We were unable to sign this field. Please try again later.', + }); + } + }), + completeDocumentWithToken: procedure .input(ZCompleteDocumentWithTokenMutationSchema) .mutation(async ({ input }) => { diff --git a/packages/trpc/server/recipient-router/schema.ts b/packages/trpc/server/recipient-router/schema.ts index ca177a3d5..8920e7672 100644 --- a/packages/trpc/server/recipient-router/schema.ts +++ b/packages/trpc/server/recipient-router/schema.ts @@ -23,6 +23,29 @@ export const ZAddSignersMutationSchema = z export type TAddSignersMutationSchema = z.infer; +export const ZAddTemplateSignersMutationSchema = z + .object({ + templateId: z.number(), + signers: z.array( + z.object({ + nativeId: z.number().optional(), + email: z.string().email().min(1), + name: z.string(), + }), + ), + }) + .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 TAddTemplateSignersMutationSchema = z.infer; + export const ZCompleteDocumentWithTokenMutationSchema = z.object({ token: z.string(), documentId: z.number(), 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 index 89c197f5e..780405a0c 100644 --- a/packages/ui/primitives/template-flow/add-template-placeholder-recipients.types.ts +++ b/packages/ui/primitives/template-flow/add-template-placeholder-recipients.types.ts @@ -7,7 +7,7 @@ export const ZAddTemplatePlacholderRecipientsFormSchema = z formId: z.string().min(1), nativeId: z.number().optional(), email: z.string().min(1).email(), - name: z.string().optional(), + name: z.string(), }), ), }) From 298396c86c3f7c6f7a99fedfd3a4d2b927c9a48c Mon Sep 17 00:00:00 2001 From: Mythie Date: Thu, 21 Dec 2023 17:36:35 +1100 Subject: [PATCH 39/83] fix: awaiting in promise.all array --- packages/lib/server-only/template/get-templates.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/lib/server-only/template/get-templates.ts b/packages/lib/server-only/template/get-templates.ts index 60de7cd89..5f802d278 100644 --- a/packages/lib/server-only/template/get-templates.ts +++ b/packages/lib/server-only/template/get-templates.ts @@ -8,7 +8,7 @@ export type GetTemplatesOptions = { export const getTemplates = async ({ userId, page = 1, perPage = 10 }: GetTemplatesOptions) => { const [templates, count] = await Promise.all([ - await prisma.template.findMany({ + prisma.template.findMany({ where: { userId, }, @@ -21,11 +21,9 @@ export const getTemplates = async ({ userId, page = 1, perPage = 10 }: GetTempla createdAt: 'desc', }, }), - await prisma.template.count({ + prisma.template.count({ where: { - User: { - id: userId, - }, + userId, }, }), ]); From 7babd824709ddbf471b2bd075f85bfc0b54eb985 Mon Sep 17 00:00:00 2001 From: Mythie Date: Thu, 21 Dec 2023 20:42:45 +1100 Subject: [PATCH 40/83] fix: updates from review --- .../documents/duplicate-document-dialog.tsx | 1 + .../(dashboard)/templates/data-table-templates.tsx | 2 ++ .../(dashboard)/templates/delete-template-dialog.tsx | 10 +++++----- .../templates/duplicate-template-dialog.tsx | 2 -- .../(dashboard)/templates/new-template-dialog.tsx | 2 +- .../lib/server-only/admin/get-recipients-stats.ts | 9 +++++---- .../lib/server-only/field/set-fields-for-document.ts | 3 ++- .../lib/server-only/field/set-fields-for-template.ts | 10 ++++++---- .../migration.sql | 12 ++++++++++++ packages/prisma/schema.prisma | 6 +++--- 10 files changed, 37 insertions(+), 20 deletions(-) create mode 100644 packages/prisma/migrations/20231221070056_make_recipient_status_columns_non_null_again/migration.sql diff --git a/apps/web/src/app/(dashboard)/documents/duplicate-document-dialog.tsx b/apps/web/src/app/(dashboard)/documents/duplicate-document-dialog.tsx index 7a8ff2d64..56c112d75 100644 --- a/apps/web/src/app/(dashboard)/documents/duplicate-document-dialog.tsx +++ b/apps/web/src/app/(dashboard)/documents/duplicate-document-dialog.tsx @@ -41,6 +41,7 @@ export const DuplicateDocumentDialog = ({ trpcReact.document.duplicateDocument.useMutation({ onSuccess: (newId) => { router.push(`/documents/${newId}`); + toast({ title: 'Document Duplicated', description: 'Your document has been successfully duplicated.', 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 629204c2a..63d6888b1 100644 --- a/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx +++ b/apps/web/src/app/(dashboard)/templates/data-table-templates.tsx @@ -58,11 +58,13 @@ export const TemplatesDataTable = ({ 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({ diff --git a/apps/web/src/app/(dashboard)/templates/delete-template-dialog.tsx b/apps/web/src/app/(dashboard)/templates/delete-template-dialog.tsx index ed7db1e72..9075f4677 100644 --- a/apps/web/src/app/(dashboard)/templates/delete-template-dialog.tsx +++ b/apps/web/src/app/(dashboard)/templates/delete-template-dialog.tsx @@ -23,13 +23,13 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD const { toast } = useToast(); - const { mutateAsync: deleteDocument, isLoading } = trpcReact.template.deleteTemplate.useMutation({ + const { mutateAsync: deleteTemplate, isLoading } = trpcReact.template.deleteTemplate.useMutation({ onSuccess: () => { router.refresh(); toast({ title: 'Template deleted', - description: 'Your document has been successfully deleted.', + description: 'Your template has been successfully deleted.', duration: 5000, }); @@ -37,9 +37,9 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD }, }); - const onDraftDelete = async () => { + const onDeleteTemplate = async () => { try { - await deleteDocument({ id }); + await deleteTemplate({ id }); } catch { toast({ title: 'Something went wrong', @@ -73,7 +73,7 @@ export const DeleteTemplateDialog = ({ id, open, onOpenChange }: DeleteTemplateD Cancel -
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 41/83] 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 42/83] 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 43/83] 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 44/83] 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 45/83] 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 46/83] 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 dd568361210f6228a90efa7482d815d13881ae4a Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Fri, 22 Dec 2023 11:44:22 +0530 Subject: [PATCH 47/83] chore: update url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ffd12d2ac..eff988bd4 100644 --- a/README.md +++ b/README.md @@ -280,7 +280,7 @@ WantedBy=multi-user.target ### Railway -[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/bG6D4p?referralCode=OEa9MT) +[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/bG6D4p) ### Render From 6d58e60a65b5fc55dd2e8a0e5ea201fd75528ad8 Mon Sep 17 00:00:00 2001 From: sadam Date: Sat, 23 Dec 2023 23:18:32 -0500 Subject: [PATCH 48/83] 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 49/83] 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 50/83] 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 51/83] 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 57/83] 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 58/83] 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 59/83] 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 60/83] 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 61/83] 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 62/83] 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 63/83] 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 64/83] 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 65/83] 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 66/83] 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 67/83] 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 68/83] 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 69/83] 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 70/83] 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 71/83] 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 72/83] 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 73/83] 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) => ( Date: Wed, 3 Jan 2024 13:23:13 +0530 Subject: [PATCH 74/83] chore: fix package vulnerabilities Signed-off-by: Adithya Krishna --- package-lock.json | 149 ++++++++++++++++++++++---- packages/eslint-config/package.json | 2 +- packages/tailwind-config/package.json | 2 +- 3 files changed, 133 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 708b54363..e3c1139f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6619,6 +6619,15 @@ "@types/node": "*" } }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, "node_modules/@types/hast": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.8.tgz", @@ -6656,6 +6665,11 @@ "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.10.tgz", "integrity": "sha512-Rllzc5KHk0Al5/WANwgSPl1/CwjqCy+AZrGd78zuK+jO9aDM6ffblZ+zIjgPNAaEBmlO0RYDvLNh7wD0zKVgEg==" }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==" + }, "node_modules/@types/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", @@ -8963,6 +8977,14 @@ "node": ">=6" } }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-libc": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", @@ -8971,6 +8993,14 @@ "node": ">=8" } }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -10092,22 +10122,6 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, - "node_modules/eslint-plugin-package-json": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-package-json/-/eslint-plugin-package-json-0.1.5.tgz", - "integrity": "sha512-WEWQHMrKi3XHw5HKsykNO0ui1VQ+Au1H0WcgWU3Kgt/S7yTu9SW5dPUu/pliZ+tbHO0PNWV+tURNkDYL+fxEpA==", - "dependencies": { - "disparity": "^3.0.0", - "package-json-validator": "^0.6.3", - "requireindex": "^1.2.0" - }, - "engines": { - "node": ">=8.0.0" - }, - "peerDependencies": { - "eslint": ">=4.7.0" - } - }, "node_modules/eslint-plugin-prettier": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz", @@ -11120,6 +11134,14 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/git-hooks-list": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/git-hooks-list/-/git-hooks-list-1.0.3.tgz", + "integrity": "sha512-Y7wLWcrLUXwk2noSka166byGCvhMtDRpgHdzCno1UQv/n/Hegp++a2xBWJL1lJarnKD3SWaljD+0z1ztqxuKyQ==", + "funding": { + "url": "https://github.com/fisker/git-hooks-list?sponsor=1" + } + }, "node_modules/git-raw-commits": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz", @@ -17225,6 +17247,53 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/sort-object-keys": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sort-object-keys/-/sort-object-keys-1.1.3.tgz", + "integrity": "sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==" + }, + "node_modules/sort-package-json": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/sort-package-json/-/sort-package-json-1.57.0.tgz", + "integrity": "sha512-FYsjYn2dHTRb41wqnv+uEqCUvBpK3jZcTp9rbz2qDTmel7Pmdtf+i2rLaaPMRZeSVM60V3Se31GyWFpmKs4Q5Q==", + "dependencies": { + "detect-indent": "^6.0.0", + "detect-newline": "3.1.0", + "git-hooks-list": "1.0.3", + "globby": "10.0.0", + "is-plain-obj": "2.1.0", + "sort-object-keys": "^1.1.3" + }, + "bin": { + "sort-package-json": "cli.js" + } + }, + "node_modules/sort-package-json/node_modules/globby": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-10.0.0.tgz", + "integrity": "sha512-3LifW9M4joGZasyYPz2A1U74zbC/45fvpXUvO/9KbSa+VV0aGZarWkfdgKyR9sExNP0t0x0ss/UMJpNpcaTspw==", + "dependencies": { + "@types/glob": "^7.1.1", + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.0.3", + "glob": "^7.1.3", + "ignore": "^5.1.1", + "merge2": "^1.2.3", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sort-package-json/node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "engines": { + "node": ">=8" + } + }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -19539,13 +19608,30 @@ "eslint-config-next": "13.4.19", "eslint-config-prettier": "^8.8.0", "eslint-config-turbo": "^1.9.3", - "eslint-plugin-package-json": "^0.1.4", + "eslint-plugin-package-json": "^0.2.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.32.2", "eslint-plugin-unused-imports": "^3.0.0", "typescript": "5.2.2" } }, + "packages/eslint-config/node_modules/eslint-plugin-package-json": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-package-json/-/eslint-plugin-package-json-0.2.0.tgz", + "integrity": "sha512-JQulhbH8M3gnyEKekqt9+4MKQtK8GRLBQlTvTiqyNSkbF+cDpq6GojCdGN6ov11wE+8iHjZlFDeg8u+gXfjhGA==", + "dependencies": { + "disparity": "^3.2.0", + "package-json-validator": "^0.6.3", + "requireindex": "^1.2.0", + "sort-package-json": "^1.57.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "eslint": ">=4.7.0" + } + }, "packages/eslint-config/node_modules/typescript": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", @@ -19675,7 +19761,7 @@ "license": "MIT", "dependencies": { "autoprefixer": "^10.4.13", - "postcss": "^8.4.21", + "postcss": "^8.4.32", "tailwindcss": "3.3.2", "tailwindcss-animate": "^1.0.5" }, @@ -19683,6 +19769,33 @@ "@tailwindcss/typography": "^0.5.9" } }, + "packages/tailwind-config/node_modules/postcss": { + "version": "8.4.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", + "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "packages/trpc": { "name": "@documenso/trpc", "version": "1.0.0", diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index f80719aa1..d519a3362 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -13,7 +13,7 @@ "eslint-config-next": "13.4.19", "eslint-config-prettier": "^8.8.0", "eslint-config-turbo": "^1.9.3", - "eslint-plugin-package-json": "^0.1.4", + "eslint-plugin-package-json": "^0.2.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-react": "^7.32.2", "eslint-plugin-unused-imports": "^3.0.0", diff --git a/packages/tailwind-config/package.json b/packages/tailwind-config/package.json index af96dc595..d6827955f 100644 --- a/packages/tailwind-config/package.json +++ b/packages/tailwind-config/package.json @@ -8,7 +8,7 @@ }, "dependencies": { "autoprefixer": "^10.4.13", - "postcss": "^8.4.21", + "postcss": "^8.4.32", "tailwindcss": "3.3.2", "tailwindcss-animate": "^1.0.5" }, From 6be119ac95468ebfb88896ff730b670fda80a6c1 Mon Sep 17 00:00:00 2001 From: David Nguyen Date: Wed, 3 Jan 2024 20:10:50 +1100 Subject: [PATCH 75/83] fix: improve document meta logic --- .../server-only/document-meta/upsert-document-meta.ts | 9 +++++++++ packages/trpc/server/document-router/router.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/packages/lib/server-only/document-meta/upsert-document-meta.ts b/packages/lib/server-only/document-meta/upsert-document-meta.ts index c7221cce9..34c33e7cd 100644 --- a/packages/lib/server-only/document-meta/upsert-document-meta.ts +++ b/packages/lib/server-only/document-meta/upsert-document-meta.ts @@ -8,6 +8,7 @@ export type CreateDocumentMetaOptions = { message: string; timezone: string; dateFormat: string; + userId: number; }; export const upsertDocumentMeta = async ({ @@ -16,7 +17,15 @@ export const upsertDocumentMeta = async ({ timezone, dateFormat, documentId, + userId, }: CreateDocumentMetaOptions) => { + await prisma.document.findFirstOrThrow({ + where: { + id: documentId, + userId, + }, + }); + return await prisma.documentMeta.upsert({ where: { documentId, diff --git a/packages/trpc/server/document-router/router.ts b/packages/trpc/server/document-router/router.ts index 425f34857..b4a1b60e3 100644 --- a/packages/trpc/server/document-router/router.ts +++ b/packages/trpc/server/document-router/router.ts @@ -188,6 +188,7 @@ export const documentRouter = router({ message: meta.message, dateFormat: meta.dateFormat, timezone: meta.timezone, + userId: ctx.user.id, }); } From fface15a22002ebb676a36cb6670ae15fd152939 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Fri, 5 Jan 2024 12:56:07 +0200 Subject: [PATCH 76/83] feat: jump to next field --- apps/web/src/app/(signing)/sign/[token]/form.tsx | 6 ++++++ apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/web/src/app/(signing)/sign/[token]/form.tsx b/apps/web/src/app/(signing)/sign/[token]/form.tsx index 4f20a8199..f5c94e6ec 100644 --- a/apps/web/src/app/(signing)/sign/[token]/form.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/form.tsx @@ -49,6 +49,11 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) = return sortFieldsByPosition(fields.filter((field) => !field.inserted)); }, [fields]); + const fieldsValidated = () => { + setValidateUninsertedFields(true); + validateFieldsInserted(fields); + }; + const onFormSubmit = async () => { setValidateUninsertedFields(true); @@ -154,6 +159,7 @@ export const SigningForm = ({ document, recipient, fields }: SigningFormProps) = onSignatureComplete={handleSubmit(onFormSubmit)} document={document} fields={fields} + fieldsValidated={fieldsValidated} />
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 faecf5d7e..6e01aa3cf 100644 --- a/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx @@ -15,6 +15,7 @@ export type SignDialogProps = { isSubmitting: boolean; document: Document; fields: Field[]; + fieldsValidated: () => void | Promise; onSignatureComplete: () => void | Promise; }; @@ -22,6 +23,7 @@ export const SignDialog = ({ isSubmitting, document, fields, + fieldsValidated, onSignatureComplete, }: SignDialogProps) => { const [showDialog, setShowDialog] = useState(false); @@ -29,16 +31,17 @@ export const SignDialog = ({ const isComplete = fields.every((field) => field.inserted); return ( - + From 4fd6a0d5b6346ccd273d79175877434015112ef6 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Fri, 5 Jan 2024 13:06:16 +0200 Subject: [PATCH 77/83] chore: update onOpenChange --- apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 6e01aa3cf..a69a79b5d 100644 --- a/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx @@ -31,7 +31,7 @@ export const SignDialog = ({ const isComplete = fields.every((field) => field.inserted); return ( - + From 3054d84ba70eb737e27b85e33b82a8541a822b43 Mon Sep 17 00:00:00 2001 From: Catalin Pit <25515812+catalinpit@users.noreply.github.com> Date: Mon, 8 Jan 2024 09:58:34 +0200 Subject: [PATCH 79/83] chore: implemented feedback --- apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 a69a79b5d..e4d4571fc 100644 --- a/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx +++ b/apps/web/src/app/(signing)/sign/[token]/sign-dialog.tsx @@ -37,11 +37,10 @@ export const SignDialog = ({ className="w-full" type="button" size="lg" - variant={isComplete ? 'default' : 'outline'} onClick={fieldsValidated} loading={isSubmitting} > - {isComplete ? 'Complete' : 'Fill fields'} + {isComplete ? 'Complete' : 'Next field'} From f9d26e6b3f19ef83c746019c643a2371569b18e8 Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Mon, 8 Jan 2024 13:39:34 +0530 Subject: [PATCH 80/83] fix: stepsRemaining value of the early adopters plan's input section (#803) --- apps/marketing/src/components/(marketing)/widget.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/marketing/src/components/(marketing)/widget.tsx b/apps/marketing/src/components/(marketing)/widget.tsx index c1ceadafe..80c13b275 100644 --- a/apps/marketing/src/components/(marketing)/widget.tsx +++ b/apps/marketing/src/components/(marketing)/widget.tsx @@ -1,6 +1,7 @@ 'use client'; -import { HTMLAttributes, KeyboardEvent, useMemo, useState } from 'react'; +import type { HTMLAttributes, KeyboardEvent } from 'react'; +import { useMemo, useState } from 'react'; import { zodResolver } from '@hookform/resolvers/zod'; import { AnimatePresence, motion } from 'framer-motion'; @@ -90,10 +91,10 @@ export const Widget = ({ className, children, ...props }: WidgetProps) => { } if (step === STEP.EMAIL) { - return 1; + return 3; } - return 3; + return 1; }, [step]); const onNextStepClick = () => { From 66bb56047a3b8ae2c29d93c991ee1ae1cdcc63ea Mon Sep 17 00:00:00 2001 From: Timur Ercan Date: Tue, 9 Jan 2024 14:32:49 +0100 Subject: [PATCH 81/83] chore: update roadmap links --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 62cfeee72..39cbb4332 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,9 @@ · Issues · - Roadmap + Upcoming Releases · - Upcoming Launches + Roadmap

From 7e71e06e04bc991856beccec3b11648a853ee40a Mon Sep 17 00:00:00 2001 From: hiteshwadhwani Date: Sat, 13 Jan 2024 14:19:37 +0530 Subject: [PATCH 82/83] fix: keyboard shortcut ctrl+k default behaviour fixed --- apps/web/src/components/(dashboard)/common/command-menu.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/(dashboard)/common/command-menu.tsx b/apps/web/src/components/(dashboard)/common/command-menu.tsx index 39cd9df0d..ffbd213a4 100644 --- a/apps/web/src/components/(dashboard)/common/command-menu.tsx +++ b/apps/web/src/components/(dashboard)/common/command-menu.tsx @@ -95,8 +95,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { const currentPage = pages[pages.length - 1]; - const toggleOpen = (e: KeyboardEvent) => { - e.preventDefault(); + const toggleOpen = () => { setIsOpen((isOpen) => !isOpen); onOpenChange?.(!isOpen); @@ -136,7 +135,7 @@ export function CommandMenu({ open, onOpenChange }: CommandMenuProps) { 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(['ctrl+k', 'meta+k'], toggleOpen, { preventDefault: true }); useHotkeys(SETTINGS_PAGE_SHORTCUT, goToSettings); useHotkeys(DOCUMENTS_PAGE_SHORTCUT, goToDocuments); useHotkeys(TEMPLATES_PAGE_SHORTCUT, goToTemplates); From 58b3a127eaf7416fb92f135c54f36959a45138c9 Mon Sep 17 00:00:00 2001 From: Adithya Krishna Date: Mon, 15 Jan 2024 05:18:55 +0530 Subject: [PATCH 83/83] chore: fix color for light mode icon (#806) --- apps/marketing/content/blog/pre-seed.mdx | 2 +- apps/marketing/content/blog/shop.mdx | 2 +- packages/ui/primitives/theme-switcher.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/marketing/content/blog/pre-seed.mdx b/apps/marketing/content/blog/pre-seed.mdx index fae0a6c4a..215700355 100644 --- a/apps/marketing/content/blog/pre-seed.mdx +++ b/apps/marketing/content/blog/pre-seed.mdx @@ -1,6 +1,6 @@ --- title: Announcing Pre-Seed and Open Metrics -description: We are exicited to report the closing of our Pre-Seed round. You can find the juicy details on our new /open page. Yes, it was signed using Documenso. +description: We are excited to report the closing of our Pre-Seed round. You can find the juicy details on our new /open page. Yes, it was signed using Documenso. authorName: 'Timur Ercan' authorImage: '/blog/blog-author-timur.jpeg' authorRole: 'Co-Founder' diff --git a/apps/marketing/content/blog/shop.mdx b/apps/marketing/content/blog/shop.mdx index fafd98a40..cb5b65554 100644 --- a/apps/marketing/content/blog/shop.mdx +++ b/apps/marketing/content/blog/shop.mdx @@ -30,7 +30,7 @@ We kicked off [Malfunction Mania](https://documenso.com/blog/malfunction-mania) ## Documenso Merch Shop -The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors. All items can be earned by contrinuting to Documenso. +The shirt will be available in our [merch shop](https://documen.so/shop) via a unique discount code. While the shirt will be gone after Malfunction Mania, the shop is here to stay and provide a well-deserved reward for great community members and contributors. All items can be earned by contributing to Documenso.
{ > {isMounted && theme === THEMES_TYPE.LIGHT && ( )}