ProfileInformation.tsx215 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { groupBy } from 'lodash'
3import { SubmitHandler, useForm } from 'react-hook-form'
4import { toast } from 'sonner'
5import {
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'
20import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
21import {
22 PageSection,
23 PageSectionContent,
24 PageSectionMeta,
25 PageSectionSummary,
26 PageSectionTitle,
27} from 'ui-patterns/PageSection'
28import z from 'zod'
29
30import { useProfileIdentitiesQuery } from '@/data/profile/profile-identities-query'
31import { useProfileUpdateMutation } from '@/data/profile/profile-update-mutation'
32import { useProfile } from '@/lib/profile'
33
34const 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
41const formId = 'profile-information-form'
42
43export 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}