ProfileInformation.tsx215 lines · main
| 1 | import { zodResolver } from '@hookform/resolvers/zod' |
| 2 | import { groupBy } from 'lodash' |
| 3 | import { SubmitHandler, useForm } from 'react-hook-form' |
| 4 | import { toast } from 'sonner' |
| 5 | import { |
| 6 | Button, |
| 7 | Card, |
| 8 | CardContent, |
| 9 | CardFooter, |
| 10 | Form, |
| 11 | FormControl, |
| 12 | FormField, |
| 13 | Input, |
| 14 | Select, |
| 15 | SelectContent, |
| 16 | SelectItem, |
| 17 | SelectTrigger, |
| 18 | SelectValue, |
| 19 | } from 'ui' |
| 20 | import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' |
| 21 | import { |
| 22 | PageSection, |
| 23 | PageSectionContent, |
| 24 | PageSectionMeta, |
| 25 | PageSectionSummary, |
| 26 | PageSectionTitle, |
| 27 | } from 'ui-patterns/PageSection' |
| 28 | import z from 'zod' |
| 29 | |
| 30 | import { useProfileIdentitiesQuery } from '@/data/profile/profile-identities-query' |
| 31 | import { useProfileUpdateMutation } from '@/data/profile/profile-update-mutation' |
| 32 | import { useProfile } from '@/lib/profile' |
| 33 | |
| 34 | const FormSchema = z.object({ |
| 35 | first_name: z.string().optional(), |
| 36 | last_name: z.string().optional(), |
| 37 | username: z.string().optional(), |
| 38 | primary_email: z.string().email().optional(), |
| 39 | }) |
| 40 | |
| 41 | const formId = 'profile-information-form' |
| 42 | |
| 43 | export const ProfileInformation = () => { |
| 44 | const { profile } = useProfile() |
| 45 | |
| 46 | const { |
| 47 | data: identityData, |
| 48 | isPending: isIdentitiesLoading, |
| 49 | isSuccess: isIdentitiesSuccess, |
| 50 | } = useProfileIdentitiesQuery() |
| 51 | const identities = (identityData?.identities ?? []).filter((x) => x.identity_data?.email !== null) |
| 52 | const dedupedIdentityEmails = Object.keys(groupBy(identities, 'identity_data.email')) |
| 53 | |
| 54 | const defaultValues = { |
| 55 | first_name: profile?.first_name ?? '', |
| 56 | last_name: profile?.last_name ?? '', |
| 57 | username: profile?.username ?? '', |
| 58 | primary_email: profile?.primary_email ?? '', |
| 59 | } |
| 60 | |
| 61 | const form = useForm({ |
| 62 | resolver: zodResolver(FormSchema as any), |
| 63 | defaultValues, |
| 64 | values: defaultValues, |
| 65 | }) |
| 66 | |
| 67 | const { mutate: updateProfile, isPending: isUpdatingProfile } = useProfileUpdateMutation({ |
| 68 | onSuccess: (data) => { |
| 69 | toast.success('Successfully saved profile') |
| 70 | const { first_name, last_name, username, primary_email } = data |
| 71 | form.reset({ |
| 72 | first_name: first_name ?? undefined, |
| 73 | last_name: last_name ?? undefined, |
| 74 | username, |
| 75 | primary_email, |
| 76 | }) |
| 77 | }, |
| 78 | onError: (error) => toast.error(`Failed to update profile: ${error.message}`), |
| 79 | }) |
| 80 | |
| 81 | const onSubmit: SubmitHandler<z.infer<typeof FormSchema>> = async (data) => { |
| 82 | updateProfile({ |
| 83 | firstName: data.first_name || '', |
| 84 | lastName: data.last_name || '', |
| 85 | username: data.username || '', |
| 86 | primaryEmail: data.primary_email || '', |
| 87 | }) |
| 88 | } |
| 89 | |
| 90 | return ( |
| 91 | <PageSection> |
| 92 | <PageSectionMeta> |
| 93 | <PageSectionSummary> |
| 94 | <PageSectionTitle>Profile information</PageSectionTitle> |
| 95 | </PageSectionSummary> |
| 96 | </PageSectionMeta> |
| 97 | <PageSectionContent> |
| 98 | <Form {...form}> |
| 99 | <form id={formId} className="space-y-6 w-full" onSubmit={form.handleSubmit(onSubmit)}> |
| 100 | <Card> |
| 101 | <CardContent> |
| 102 | <FormField |
| 103 | control={form.control} |
| 104 | name="first_name" |
| 105 | render={({ field }) => ( |
| 106 | <FormItemLayout label="First name" layout="flex-row-reverse"> |
| 107 | <FormControl className="col-span-8"> |
| 108 | <Input {...field} placeholder="First name" className="w-full" /> |
| 109 | </FormControl> |
| 110 | </FormItemLayout> |
| 111 | )} |
| 112 | /> |
| 113 | </CardContent> |
| 114 | <CardContent> |
| 115 | <FormField |
| 116 | control={form.control} |
| 117 | name="last_name" |
| 118 | render={({ field }) => ( |
| 119 | <FormItemLayout label="Last name" layout="flex-row-reverse"> |
| 120 | <FormControl className="col-span-8"> |
| 121 | <Input {...field} placeholder="Last name" className="w-full" /> |
| 122 | </FormControl> |
| 123 | </FormItemLayout> |
| 124 | )} |
| 125 | /> |
| 126 | </CardContent> |
| 127 | <CardContent> |
| 128 | <FormField |
| 129 | control={form.control} |
| 130 | name="primary_email" |
| 131 | render={({ field }) => ( |
| 132 | <FormItemLayout |
| 133 | label="Primary email" |
| 134 | description={ |
| 135 | profile?.is_sso_user |
| 136 | ? 'Managed by your SSO provider and cannot be changed here' |
| 137 | : 'Used for account notifications' |
| 138 | } |
| 139 | layout="flex-row-reverse" |
| 140 | > |
| 141 | <FormControl className="col-span-8"> |
| 142 | <div className="flex flex-col gap-1"> |
| 143 | <Select |
| 144 | value={field.value} |
| 145 | onValueChange={field.onChange} |
| 146 | disabled={profile?.is_sso_user} |
| 147 | > |
| 148 | <SelectTrigger className="col-span-8 w-full"> |
| 149 | <SelectValue placeholder="Select primary email" /> |
| 150 | </SelectTrigger> |
| 151 | <SelectContent className="col-span-8"> |
| 152 | {isIdentitiesSuccess && |
| 153 | dedupedIdentityEmails.map((email) => ( |
| 154 | <SelectItem key={email} value={email}> |
| 155 | {email} |
| 156 | </SelectItem> |
| 157 | ))} |
| 158 | </SelectContent> |
| 159 | </Select> |
| 160 | </div> |
| 161 | </FormControl> |
| 162 | </FormItemLayout> |
| 163 | )} |
| 164 | /> |
| 165 | </CardContent> |
| 166 | <CardContent> |
| 167 | <FormField |
| 168 | control={form.control} |
| 169 | name="username" |
| 170 | render={({ field }) => ( |
| 171 | <FormItemLayout |
| 172 | label="Username" |
| 173 | description={ |
| 174 | profile?.is_sso_user |
| 175 | ? 'Managed by your SSO provider and cannot be changed here' |
| 176 | : 'Display name used across dashboard' |
| 177 | } |
| 178 | layout="flex-row-reverse" |
| 179 | > |
| 180 | <FormControl className="col-span-8"> |
| 181 | <div className="flex flex-col gap-1"> |
| 182 | <Input |
| 183 | {...field} |
| 184 | className="w-full" |
| 185 | placeholder="Username" |
| 186 | disabled={profile?.is_sso_user} |
| 187 | /> |
| 188 | </div> |
| 189 | </FormControl> |
| 190 | </FormItemLayout> |
| 191 | )} |
| 192 | /> |
| 193 | </CardContent> |
| 194 | <CardFooter className="justify-end space-x-2"> |
| 195 | {form.formState.isDirty && ( |
| 196 | <Button type="default" onClick={() => form.reset()}> |
| 197 | Cancel |
| 198 | </Button> |
| 199 | )} |
| 200 | <Button |
| 201 | type="primary" |
| 202 | htmlType="submit" |
| 203 | loading={isUpdatingProfile || isIdentitiesLoading} |
| 204 | disabled={!form.formState.isDirty} |
| 205 | > |
| 206 | Save |
| 207 | </Button> |
| 208 | </CardFooter> |
| 209 | </Card> |
| 210 | </form> |
| 211 | </Form> |
| 212 | </PageSectionContent> |
| 213 | </PageSection> |
| 214 | ) |
| 215 | } |