← full-stack-fastapi-template  /  frontend/src/components/Admin/EditUser.tsx

1
import { zodResolver } from "@hookform/resolvers/zod"
2
import { useMutation, useQueryClient } from "@tanstack/react-query"
3
import { Pencil } from "lucide-react"
4
import { useState } from "react"
5
import { useForm } from "react-hook-form"
6
import { z } from "zod"
7
8
import { type UserPublic, UsersService } from "@/client"
9
import { Button } from "@/components/ui/button"
10
import { Checkbox } from "@/components/ui/checkbox"
11
import {
12
  Dialog,
13
  DialogClose,
14
  DialogContent,
15
  DialogDescription,
16
  DialogFooter,
17
  DialogHeader,
18
  DialogTitle,
19
} from "@/components/ui/dialog"
20
import { DropdownMenuItem } from "@/components/ui/dropdown-menu"
21
import {
22
  Form,
23
  FormControl,
24
  FormField,
25
  FormItem,
26
  FormLabel,
27
  FormMessage,
28
} from "@/components/ui/form"
29
import { Input } from "@/components/ui/input"
30
import { LoadingButton } from "@/components/ui/loading-button"
31
import useCustomToast from "@/hooks/useCustomToast"
32
import { handleError } from "@/utils"
33
34
const formSchema = z
35
  .object({
36
    email: z.email({ message: "Invalid email address" }),
37
    full_name: z.string().optional(),
38
    password: z
39
      .string()
40
      .min(8, { message: "Password must be at least 8 characters" })
41
      .optional()
42
      .or(z.literal("")),
43
    confirm_password: z.string().optional(),
44
    is_superuser: z.boolean().optional(),
45
    is_active: z.boolean().optional(),
46
  })
47
  .refine((data) => !data.password || data.password === data.confirm_password, {
48
    message: "The passwords don't match",
49
    path: ["confirm_password"],
50
  })
51
52
type FormData = z.infer<typeof formSchema>
53
54
interface EditUserProps {
55
  user: UserPublic
56
  onSuccess: () => void
57
}
58
59
const EditUser = ({ user, onSuccess }: EditUserProps) => {
60
  const [isOpen, setIsOpen] = useState(false)
61
  const queryClient = useQueryClient()
62
  const { showSuccessToast, showErrorToast } = useCustomToast()
63
64
  const form = useForm<FormData>({
65
    resolver: zodResolver(formSchema),
66
    mode: "onBlur",
67
    criteriaMode: "all",
68
    defaultValues: {
69
      email: user.email,
70
      full_name: user.full_name ?? undefined,
71
      is_superuser: user.is_superuser,
72
      is_active: user.is_active,
73
    },
74
  })
75
76
  const mutation = useMutation({
77
    mutationFn: (data: FormData) =>
78
      UsersService.updateUser({ userId: user.id, requestBody: data }),
79
    onSuccess: () => {
80
      showSuccessToast("User updated successfully")
81
      setIsOpen(false)
82
      onSuccess()
83
    },
84
    onError: handleError.bind(showErrorToast),
85
    onSettled: () => {
86
      queryClient.invalidateQueries({ queryKey: ["users"] })
87
    },
88
  })
89
90
  const onSubmit = (data: FormData) => {
91
    // exclude confirm_password from submission data and remove password if empty
92
    const { confirm_password: _, ...submitData } = data
93
    if (!submitData.password) {
94
      delete submitData.password
95
    }
96
    mutation.mutate(submitData)
97
  }
98
99
  return (
100
    <Dialog open={isOpen} onOpenChange={setIsOpen}>
101
      <DropdownMenuItem
102
        onSelect={(e) => e.preventDefault()}
103
        onClick={() => setIsOpen(true)}
104
      >
105
        <Pencil />
106
        Edit User
107
      </DropdownMenuItem>
108
      <DialogContent className="sm:max-w-md">
109
        <Form {...form}>
110
          <form onSubmit={form.handleSubmit(onSubmit)}>
111
            <DialogHeader>
112
              <DialogTitle>Edit User</DialogTitle>
113
              <DialogDescription>
114
                Update the user details below.
115
              </DialogDescription>
116
            </DialogHeader>
117
            <div className="grid gap-4 py-4">
118
              <FormField
119
                control={form.control}
120
                name="email"
121
                render={({ field }) => (
122
                  <FormItem>
123
                    <FormLabel>
124
                      Email <span className="text-destructive">*</span>
125
                    </FormLabel>
126
                    <FormControl>
127
                      <Input
128
                        placeholder="Email"
129
                        type="email"
130
                        {...field}
131
                        required
132
                      />
133
                    </FormControl>
134
                    <FormMessage />
135
                  </FormItem>
136
                )}
137
              />
138
139
              <FormField
140
                control={form.control}
141
                name="full_name"
142
                render={({ field }) => (
143
                  <FormItem>
144
                    <FormLabel>Full Name</FormLabel>
145
                    <FormControl>
146
                      <Input placeholder="Full name" type="text" {...field} />
147
                    </FormControl>
148
                    <FormMessage />
149
                  </FormItem>
150
                )}
151
              />
152
153
              <FormField
154
                control={form.control}
155
                name="password"
156
                render={({ field }) => (
157
                  <FormItem>
158
                    <FormLabel>Set Password</FormLabel>
159
                    <FormControl>
160
                      <Input
161
                        placeholder="Password"
162
                        type="password"
163
                        {...field}
164
                      />
165
                    </FormControl>
166
                    <FormMessage />
167
                  </FormItem>
168
                )}
169
              />
170
171
              <FormField
172
                control={form.control}
173
                name="confirm_password"
174
                render={({ field }) => (
175
                  <FormItem>
176
                    <FormLabel>Confirm Password</FormLabel>
177
                    <FormControl>
178
                      <Input
179
                        placeholder="Password"
180
                        type="password"
181
                        {...field}
182
                      />
183
                    </FormControl>
184
                    <FormMessage />
185
                  </FormItem>
186
                )}
187
              />
188
189
              <FormField
190
                control={form.control}
191
                name="is_superuser"
192
                render={({ field }) => (
193
                  <FormItem className="flex items-center gap-3 space-y-0">
194
                    <FormControl>
195
                      <Checkbox
196
                        checked={field.value}
197
                        onCheckedChange={field.onChange}
198
                      />
199
                    </FormControl>
200
                    <FormLabel className="font-normal">Is superuser?</FormLabel>
201
                  </FormItem>
202
                )}
203
              />
204
205
              <FormField
206
                control={form.control}
207
                name="is_active"
208
                render={({ field }) => (
209
                  <FormItem className="flex items-center gap-3 space-y-0">
210
                    <FormControl>
211
                      <Checkbox
212
                        checked={field.value}
213
                        onCheckedChange={field.onChange}
214
                      />
215
                    </FormControl>
216
                    <FormLabel className="font-normal">Is active?</FormLabel>
217
                  </FormItem>
218
                )}
219
              />
220
            </div>
221
222
            <DialogFooter>
223
              <DialogClose asChild>
224
                <Button variant="outline" disabled={mutation.isPending}>
225
                  Cancel
226
                </Button>
227
              </DialogClose>
228
              <LoadingButton type="submit" loading={mutation.isPending}>
229
                Save
230
              </LoadingButton>
231
            </DialogFooter>
232
          </form>
233
        </Form>
234
      </DialogContent>
235
    </Dialog>
236
  )
237
}
238
239
export default EditUser
240