ProfileInformation.tsx 7.4 KB

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