Skip to content

Commit 0a3600f

Browse files
committed
♻️ Refactor ChangePassword to work as embedded dialog
1 parent 68e0d64 commit 0a3600f

File tree

1 file changed

+61
-27
lines changed

1 file changed

+61
-27
lines changed

frontend/src/components/UserSettings/ChangePassword.tsx

Lines changed: 61 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { type UpdatePassword, UsersService } from "@/client"
77
import {
88
Form,
99
FormControl,
10+
FormDescription,
1011
FormField,
1112
FormItem,
1213
FormLabel,
@@ -17,32 +18,50 @@ import { PasswordInput } from "@/components/ui/password-input"
1718
import useCustomToast from "@/hooks/useCustomToast"
1819
import { handleError } from "@/utils"
1920

20-
const formSchema = z
21+
const PASSWORD_MIN_LENGTH = 8
22+
23+
const passwordSchema = z
24+
.string()
25+
.min(1, { message: "Password is required" })
26+
.min(PASSWORD_MIN_LENGTH, {
27+
message: `Password must be at least ${PASSWORD_MIN_LENGTH} characters`,
28+
})
29+
30+
const changePasswordSchema = z
2131
.object({
22-
current_password: z
23-
.string()
24-
.min(1, { message: "Password is required" })
25-
.min(8, { message: "Password must be at least 8 characters" }),
26-
new_password: z
27-
.string()
28-
.min(1, { message: "Password is required" })
29-
.min(8, { message: "Password must be at least 8 characters" }),
32+
current_password: passwordSchema,
33+
new_password: passwordSchema,
3034
confirm_password: z
3135
.string()
32-
.min(1, { message: "Password confirmation is required" }),
36+
.min(1, { message: "Please confirm your new password" }),
3337
})
3438
.refine((data) => data.new_password === data.confirm_password, {
35-
message: "The passwords don't match",
39+
message: "Passwords do not match",
3640
path: ["confirm_password"],
3741
})
42+
.refine((data) => data.new_password !== data.current_password, {
43+
message: "New password cannot be the same as the current one",
44+
path: ["new_password"],
45+
})
46+
47+
type ChangePasswordFormData = z.infer<typeof changePasswordSchema>
3848

39-
type FormData = z.infer<typeof formSchema>
49+
export interface ChangePasswordProps {
50+
/** Called after password is updated successfully (e.g. to close a dialog) */
51+
onSuccess?: () => void
52+
/** When true, omits the heading and adjusts layout for use inside a dialog */
53+
embedded?: boolean
54+
}
4055

41-
const ChangePassword = () => {
56+
export default function ChangePassword({
57+
onSuccess,
58+
embedded,
59+
}: ChangePasswordProps) {
4260
const { showSuccessToast, showErrorToast } = useCustomToast()
43-
const form = useForm<FormData>({
44-
resolver: zodResolver(formSchema),
45-
mode: "onSubmit",
61+
62+
const form = useForm<ChangePasswordFormData>({
63+
resolver: zodResolver(changePasswordSchema),
64+
mode: "onBlur",
4665
criteriaMode: "all",
4766
defaultValues: {
4867
current_password: "",
@@ -57,33 +76,42 @@ const ChangePassword = () => {
5776
onSuccess: () => {
5877
showSuccessToast("Password updated successfully")
5978
form.reset()
79+
onSuccess?.()
6080
},
6181
onError: handleError.bind(showErrorToast),
6282
})
6383

64-
const onSubmit = async (data: FormData) => {
84+
const onSubmit = (data: ChangePasswordFormData) => {
85+
if (mutation.isPending) return
6586
mutation.mutate(data)
6687
}
6788

6889
return (
69-
<div className="max-w-md">
70-
<h3 className="text-lg font-semibold py-4">Change Password</h3>
90+
<div className={embedded ? "pt-1" : "max-w-md"}>
91+
{!embedded && (
92+
<h2 className="text-lg font-semibold tracking-tight pb-4">
93+
Change password
94+
</h2>
95+
)}
7196
<Form {...form}>
7297
<form
7398
onSubmit={form.handleSubmit(onSubmit)}
74-
className="flex flex-col gap-4"
99+
noValidate
100+
className="flex flex-col gap-6"
75101
>
76102
<FormField
77103
control={form.control}
78104
name="current_password"
79105
render={({ field, fieldState }) => (
80106
<FormItem>
81-
<FormLabel>Current Password</FormLabel>
107+
<FormLabel>Current password</FormLabel>
82108
<FormControl>
83109
<PasswordInput
84110
data-testid="current-password-input"
85111
placeholder="••••••••"
112+
autoComplete="current-password"
86113
aria-invalid={fieldState.invalid}
114+
disabled={mutation.isPending}
87115
{...field}
88116
/>
89117
</FormControl>
@@ -97,15 +125,20 @@ const ChangePassword = () => {
97125
name="new_password"
98126
render={({ field, fieldState }) => (
99127
<FormItem>
100-
<FormLabel>New Password</FormLabel>
128+
<FormLabel>New password</FormLabel>
101129
<FormControl>
102130
<PasswordInput
103131
data-testid="new-password-input"
104132
placeholder="••••••••"
133+
autoComplete="new-password"
105134
aria-invalid={fieldState.invalid}
135+
disabled={mutation.isPending}
106136
{...field}
107137
/>
108138
</FormControl>
139+
<FormDescription>
140+
At least {PASSWORD_MIN_LENGTH} characters
141+
</FormDescription>
109142
<FormMessage />
110143
</FormItem>
111144
)}
@@ -116,12 +149,14 @@ const ChangePassword = () => {
116149
name="confirm_password"
117150
render={({ field, fieldState }) => (
118151
<FormItem>
119-
<FormLabel>Confirm Password</FormLabel>
152+
<FormLabel>Confirm new password</FormLabel>
120153
<FormControl>
121154
<PasswordInput
122155
data-testid="confirm-password-input"
123156
placeholder="••••••••"
157+
autoComplete="new-password"
124158
aria-invalid={fieldState.invalid}
159+
disabled={mutation.isPending}
125160
{...field}
126161
/>
127162
</FormControl>
@@ -133,14 +168,13 @@ const ChangePassword = () => {
133168
<LoadingButton
134169
type="submit"
135170
loading={mutation.isPending}
136-
className="self-start"
171+
disabled={!form.formState.isDirty || mutation.isPending}
172+
className={embedded ? "w-full sm:w-auto" : "w-full sm:w-auto"}
137173
>
138-
Update Password
174+
Update password
139175
</LoadingButton>
140176
</form>
141177
</Form>
142178
</div>
143179
)
144180
}
145-
146-
export default ChangePassword

0 commit comments

Comments
 (0)