From 27d2f18302f8fe58273d6ff7b7b67d0b0d5707ac Mon Sep 17 00:00:00 2001 From: Meng Zhang Date: Thu, 7 Dec 2023 13:39:39 +0800 Subject: [PATCH] feat: add authentication page example (#955) * feat: add authentication page autogen * feat: pass invitation code to UserAuthForm in SignUp * [autofix.ci] apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- ee/tabby-ui/app/auth/components/signup.tsx | 34 ++++ .../app/auth/components/user-auth-form.tsx | 146 +++++++++++++++ ee/tabby-ui/app/auth/page.tsx | 16 ++ ee/tabby-ui/components/ui/form.tsx | 177 ++++++++++++++++++ ee/tabby-ui/lib/gql/generates/gql.ts | 16 ++ ee/tabby-ui/lib/gql/generates/graphql.ts | 147 +++++++++++++++ ee/tabby-ui/lib/tabby-gql-client.ts | 44 ++++- ee/tabby-ui/package.json | 3 + ee/tabby-ui/yarn.lock | 15 ++ 9 files changed, 597 insertions(+), 1 deletion(-) create mode 100644 ee/tabby-ui/app/auth/components/signup.tsx create mode 100644 ee/tabby-ui/app/auth/components/user-auth-form.tsx create mode 100644 ee/tabby-ui/app/auth/page.tsx create mode 100644 ee/tabby-ui/components/ui/form.tsx diff --git a/ee/tabby-ui/app/auth/components/signup.tsx b/ee/tabby-ui/app/auth/components/signup.tsx new file mode 100644 index 0000000..b8eeffa --- /dev/null +++ b/ee/tabby-ui/app/auth/components/signup.tsx @@ -0,0 +1,34 @@ +'use client' + +import { graphql } from '@/lib/gql/generates' +import { UserAuthForm } from './user-auth-form' +import { useGraphQL } from '@/lib/hooks/use-graphql' +import { useSearchParams } from 'next/navigation' + +export const getIsAdminInitialized = graphql(/* GraphQL */ ` + query GetIsAdminInitialized { + isAdminInitialized + } +`) + +export default function Signup() { + const { data } = useGraphQL(getIsAdminInitialized) + const title = data?.isAdminInitialized + ? 'Create an account' + : 'Create an admin account' + + const searchParams = useSearchParams() + const invitationCode = searchParams.get('invitationCode') || undefined + + return ( +
+
+

{title}

+

+ Fill form below to create your account +

+
+ +
+ ) +} diff --git a/ee/tabby-ui/app/auth/components/user-auth-form.tsx b/ee/tabby-ui/app/auth/components/user-auth-form.tsx new file mode 100644 index 0000000..8f5c228 --- /dev/null +++ b/ee/tabby-ui/app/auth/components/user-auth-form.tsx @@ -0,0 +1,146 @@ +'use client' + +import * as React from 'react' + +import { zodResolver } from '@hookform/resolvers/zod' +import { useForm } from 'react-hook-form' +import * as z from 'zod' + +import { cn } from '@/lib/utils' +import { IconSpinner } from '@/components/ui/icons' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form' +import { graphql } from '@/lib/gql/generates' +import { useGraphQLForm } from '@/lib/tabby-gql-client' + +export const registerUser = graphql(/* GraphQL */ ` + mutation register( + $email: String! + $password1: String! + $password2: String! + $invitationCode: String + ) { + register( + email: $email + password1: $password1 + password2: $password2 + invitationCode: $invitationCode + ) { + accessToken + refreshToken + } + } +`) + +const formSchema = z.object({ + email: z.string().email('Invalid email address'), + password1: z.string(), + password2: z.string(), + invitationCode: z.string().optional() +}) + +interface UserAuthFormProps extends React.HTMLAttributes { + invitationCode?: string +} + +export function UserAuthForm({ + className, + invitationCode, + ...props +}: UserAuthFormProps) { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + password1: '', + password2: '', + invitationCode + } + }) + + const { isSubmitting } = form.formState + const { onSubmit } = useGraphQLForm(registerUser, { + onError: (path, message) => form.setError(path as any, { message }) + }) + + return ( +
+
+ + ( + + Email + + + + + + )} + /> + ( + + Password + + + + + + )} + /> + ( + + Confirm Password + + + + + + )} + /> + ( + + + + + + )} + /> + + + + +
+ ) +} diff --git a/ee/tabby-ui/app/auth/page.tsx b/ee/tabby-ui/app/auth/page.tsx new file mode 100644 index 0000000..f6a9db4 --- /dev/null +++ b/ee/tabby-ui/app/auth/page.tsx @@ -0,0 +1,16 @@ +import { Metadata } from 'next' + +import Signup from './components/signup' + +export const metadata: Metadata = { + title: 'Authentication', + description: 'Authentication forms built using the components.' +} + +export default function AuthenticationPage() { + return ( +
+ +
+ ) +} diff --git a/ee/tabby-ui/components/ui/form.tsx b/ee/tabby-ui/components/ui/form.tsx new file mode 100644 index 0000000..1fd11f4 --- /dev/null +++ b/ee/tabby-ui/components/ui/form.tsx @@ -0,0 +1,177 @@ +import * as React from 'react' +import * as LabelPrimitive from '@radix-ui/react-label' +import { Slot } from '@radix-ui/react-slot' +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext +} from 'react-hook-form' + +import { cn } from '@/lib/utils' +import { Label } from '@/components/ui/label' + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + const name = fieldContext.name || 'root' + + const fieldState = getFieldState(name, formState) + + if (!formState) { + throw new Error('useFormField should be used within
') + } + + const { id } = itemContext + + return { + id, + name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = 'FormItem' + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +