refactor: use validation on email update modal (@fehmer) (#6272)

This commit is contained in:
Christian Fehmer 2025-02-19 16:27:20 +01:00 committed by GitHub
parent 14d423e088
commit 09eb1a2b0e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 36 additions and 34 deletions

View file

@ -35,7 +35,7 @@ import {
} from "../utils/simple-modal";
import { ShowOptions } from "../utils/animated-modal";
import { GenerateDataRequest } from "@monkeytype/contracts/dev";
import { UserNameSchema } from "@monkeytype/contracts/users";
import { UserEmailSchema, UserNameSchema } from "@monkeytype/contracts/users";
import { goToPage } from "../pages/leaderboards";
type PopupKey =
@ -230,11 +230,20 @@ list.updateEmail = new SimpleModal({
type: "text",
placeholder: "New email",
initVal: "",
validation: {
schema: UserEmailSchema,
},
},
{
type: "text",
placeholder: "Confirm new email",
initVal: "",
validation: {
schema: UserEmailSchema,
isValid: async (currentValue, thisPopup) =>
currentValue === thisPopup.inputs?.[1]?.currentValue() ||
"Emails don't match",
},
},
],
buttonText: "update",
@ -262,7 +271,7 @@ list.updateEmail = new SimpleModal({
const response = await Ape.users.updateEmail({
body: {
newEmail: email.trim(),
newEmail: email,
previousEmail: reauth.user.email as string,
},
});

View file

@ -31,9 +31,10 @@ type CommonInput<TType, TValue> = {
* Custom async validation method.
* This is intended to be used for validations that cannot be handled with a Zod schema like server-side validations.
* @param value current input value
* @param thisPopup the current modal
* @returns true if the `value` is valid, an errorMessage as string if it is invalid.
*/
isValid?: (value: string) => Promise<true | string>;
isValid?: (value: string, thisPopup: SimpleModal) => Promise<true | string>;
};
};
@ -88,8 +89,9 @@ export type ExecReturn = {
afterHide?: () => void;
};
type CommonInputTypeWithIndicator = CommonInputType & {
type FormInput = CommonInputType & {
indicator?: InputIndicator;
currentValue: () => string;
};
type SimpleModalOptions = {
id: string;
@ -114,7 +116,7 @@ export class SimpleModal {
modal: AnimatedModal;
id: string;
title: string;
inputs: CommonInputTypeWithIndicator[];
inputs: FormInput[];
text?: string;
textAllowHtml: boolean;
buttonText: string;
@ -130,7 +132,7 @@ export class SimpleModal {
this.id = options.id;
this.execFn = options.execFn;
this.title = options.title;
this.inputs = options.inputs ?? [];
this.inputs = (options.inputs as FormInput[]) ?? [];
this.text = options.text;
this.textAllowHtml = options.textAllowHtml ?? false;
this.wrapper = modal.getWrapper();
@ -313,9 +315,17 @@ export class SimpleModal {
const element = document.querySelector(
"#" + attributes["id"]
) as HTMLInputElement;
if (input.oninput !== undefined) {
element.oninput = input.oninput;
}
input.currentValue = () => {
if (element.type === "checkbox")
return element.checked ? "true" : "false";
return element.value;
};
if (input.validation !== undefined) {
const indicator = new InputIndicator(element, {
valid: {
@ -335,7 +345,7 @@ export class SimpleModal {
input.indicator = indicator;
const debouceIsValid = debounce(1000, async (value: string) => {
const result = await input.validation?.isValid?.(value);
const result = await input.validation?.isValid?.(value, this);
if (element.value !== value) {
//value of the input has changed in the meantime. discard
@ -389,43 +399,24 @@ export class SimpleModal {
exec(): void {
if (!this.canClose) return;
const vals: string[] = [];
for (const el of $("#simpleModal input, #simpleModal textarea")) {
if ($(el).is(":checkbox")) {
vals.push($(el).is(":checked") ? "true" : "false");
} else {
vals.push($(el).val() as string);
}
}
type CommonInputWithCurrentValue = CommonInputTypeWithIndicator & {
currentValue: string | undefined;
};
const inputsWithCurrentValue: CommonInputWithCurrentValue[] = [];
for (let i = 0; i < this.inputs.length; i++) {
inputsWithCurrentValue.push({
...(this.inputs[i] as CommonInputTypeWithIndicator),
currentValue: vals[i],
});
}
if (
inputsWithCurrentValue
this.inputs
.filter((i) => i.hidden !== true && i.optional !== true)
.some((v) => v.currentValue === undefined || v.currentValue === "")
.some((v) => v.currentValue() === undefined || v.currentValue() === "")
) {
Notifications.add("Please fill in all fields", 0);
return;
}
if (inputsWithCurrentValue.some((i) => i.indicator?.get() === "invalid")) {
if (this.inputs.some((i) => i.indicator?.get() === "invalid")) {
Notifications.add("Please solve all validation errors", 0);
return;
}
this.disableInputs();
Loader.show();
const vals: string[] = this.inputs.map((it) => it.currentValue());
void this.execFn(this, ...vals).then((res) => {
Loader.hide();
if (res.showNotification ?? true) {

View file

@ -30,6 +30,8 @@ import { IdSchema, LanguageSchema, StringNumberSchema } from "./schemas/util";
import { CustomThemeColorsSchema } from "./schemas/configs";
import { doesNotContainProfanity } from "./validation/validation";
export const UserEmailSchema = z.string().email();
export const GetUserResponseSchema = responseWithData(
UserSchema.extend({
inboxUnreadSize: z.number().int().nonnegative(),
@ -50,7 +52,7 @@ export const UserNameSchema = doesNotContainProfanity(
);
export const CreateUserRequestSchema = z.object({
email: z.string().email().optional(),
email: UserEmailSchema.optional(),
name: UserNameSchema,
uid: z.string().optional(), //defined by firebase, no validation should be applied
captcha: z.string(), //defined by google recaptcha, no validation should be applied
@ -80,8 +82,8 @@ export type UpdateLeaderboardMemoryRequest = z.infer<
>;
export const UpdateEmailRequestSchema = z.object({
newEmail: z.string().email(),
previousEmail: z.string().email(),
newEmail: UserEmailSchema,
previousEmail: UserEmailSchema,
});
export type UpdateEmailRequestSchema = z.infer<typeof UpdateEmailRequestSchema>;
@ -303,7 +305,7 @@ export type ReportUserRequest = z.infer<typeof ReportUserRequestSchema>;
export const ForgotPasswordEmailRequestSchema = z.object({
captcha: z.string(),
email: z.string().email(),
email: UserEmailSchema,
});
export type ForgotPasswordEmailRequest = z.infer<
typeof ForgotPasswordEmailRequestSchema