impr: handle backend unavailable in remote validations (@fehmer) (#7105)

This commit is contained in:
Christian Fehmer 2025-11-13 17:12:37 +01:00 committed by GitHub
parent 05afcc51f9
commit 0e4b9c4687
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 62 additions and 47 deletions

View file

@ -14,6 +14,8 @@ export type ValidationResult = {
errorMessage?: string;
};
export type IsValidResponse = true | string | { warning: string };
export type Validation<T> = {
/**
* Zod schema to validate the input value against.
@ -28,7 +30,7 @@ export type Validation<T> = {
* @param thisPopup the current modal
* @returns true if the `value` is valid, an errorMessage as string if it is invalid.
*/
isValid?: (value: T) => Promise<true | string | { warning: string }>;
isValid?: (value: T) => Promise<IsValidResponse>;
/** custom debounce delay for `isValid` call. defaults to 100 */
debounceDelay?: number;

View file

@ -1,12 +1,13 @@
import Ape from "../ape";
import * as DB from "../db";
import { IsValidResponse } from "../elements/input-validation";
import * as Settings from "../pages/settings";
import AnimatedModal, { ShowOptions } from "../utils/animated-modal";
import { SimpleModal, TextInput } from "../utils/simple-modal";
import { TagNameSchema } from "@monkeytype/schemas/users";
const cleanTagName = (tagName: string): string => tagName.replaceAll(" ", "_");
const tagNameValidation = async (tagName: string): Promise<true | string> => {
const tagNameValidation = async (tagName: string): Promise<IsValidResponse> => {
const validationResult = TagNameSchema.safeParse(cleanTagName(tagName));
if (validationResult.success) return true;
return validationResult.error.errors.map((err) => err.message).join(", ");

View file

@ -16,6 +16,7 @@ import AnimatedModal from "../utils/animated-modal";
import { resetIgnoreAuthCallback } from "../firebase";
import { validateWithIndicator } from "../elements/input-validation";
import { UserNameSchema } from "@monkeytype/schemas/users";
import { remoteValidation } from "../utils/remote-validation";
let signedInUser: UserCredential | undefined = undefined;
@ -154,17 +155,10 @@ function disableInput(): void {
validateWithIndicator(nameInputEl, {
schema: UserNameSchema,
isValid: async (name: string) => {
const checkNameResponse = await Ape.users.getNameAvailability({
params: { name: name },
});
return (
(checkNameResponse.status === 200 &&
checkNameResponse.body.data.available) ||
"Name not available"
);
},
isValid: remoteValidation(
async (name) => Ape.users.getNameAvailability({ params: { name } }),
{ check: (data) => data.available || "Name not available" }
),
debounceDelay: 1000,
callback: (result) => {
if (result.status === "success") {

View file

@ -45,6 +45,7 @@ import {
import { goToPage } from "../pages/leaderboards";
import FileStorage from "../utils/file-storage";
import { z } from "zod";
import { remoteValidation } from "../utils/remote-validation";
type PopupKey =
| "updateEmail"
@ -479,17 +480,10 @@ list.updateName = new SimpleModal({
initVal: "",
validation: {
schema: UserNameSchema,
isValid: async (newName: string) => {
const checkNameResponse = await Ape.users.getNameAvailability({
params: { name: newName },
});
return (
(checkNameResponse.status === 200 &&
checkNameResponse.body.data.available) ||
"Name not available"
);
},
isValid: remoteValidation(
async (name) => Ape.users.getNameAvailability({ params: { name } }),
{ check: (data) => data.available || "Name not available" }
),
debounceDelay: 1000,
},
},

View file

@ -29,6 +29,7 @@ import { Connection } from "@monkeytype/schemas/connections";
import { Friend, UserNameSchema } from "@monkeytype/schemas/users";
import * as Loader from "../elements/loader";
import { LocalStorageWithSchema } from "../utils/local-storage-with-schema";
import { remoteValidation } from "../utils/remote-validation";
const pageElement = $(".page.pageFriends");
@ -75,17 +76,10 @@ const addFriendModal = new SimpleModal({
initVal: "",
validation: {
schema: UserNameSchema,
isValid: async (name: string) => {
const checkNameResponse = await Ape.users.getNameAvailability({
params: { name: name },
});
return (
(checkNameResponse.status === 200 &&
!checkNameResponse.body.data.available) ||
"Unknown user"
);
},
isValid: remoteValidation(
async (name) => Ape.users.getNameAvailability({ params: { name } }),
{ check: (data) => !data.available || "Unknown user" }
),
debounceDelay: 1000,
},
},

View file

@ -10,6 +10,7 @@ import {
import { validateWithIndicator } from "../elements/input-validation";
import { isDevEnvironment } from "../utils/misc";
import { z } from "zod";
import { remoteValidation } from "../utils/remote-validation";
let registerForm: {
name?: string;
@ -73,17 +74,10 @@ const nameInputEl = document.querySelector(
) as HTMLInputElement;
validateWithIndicator(nameInputEl, {
schema: UserNameSchema,
isValid: async (name: string) => {
const checkNameResponse = await Ape.users.getNameAvailability({
params: { name: name },
});
return (
(checkNameResponse.status === 200 &&
checkNameResponse.body.data.available) ||
"Name not available"
);
},
isValid: remoteValidation(
async (name) => Ape.users.getNameAvailability({ params: { name } }),
{ check: (data) => data.available || "Name not available" }
),
debounceDelay: 1000,
callback: (result) => {
registerForm.name =

View file

@ -0,0 +1,32 @@
import { IsValidResponse } from "../elements/input-validation";
type IsValidResonseOrFunction =
| ((message: string) => IsValidResponse)
| IsValidResponse;
export function remoteValidation<V, T>(
call: (
val: V
) => Promise<{ status: number; body: { data?: T; message: string } }>,
options?: {
check?: (data: T) => IsValidResponse;
on4xx?: IsValidResonseOrFunction;
on5xx?: IsValidResonseOrFunction;
}
): (val: V) => Promise<IsValidResponse> {
return async (val) => {
const result = await call(val);
if (result.status <= 299) {
return options?.check?.(result.body.data as T) ?? true;
}
let handler: IsValidResonseOrFunction | undefined;
if (result.status <= 499) {
handler = options?.on4xx ?? ((message) => message);
} else {
handler = options?.on5xx ?? "Server unavailable. Please try again later.";
}
if (typeof handler === "function") return handler(result.body.message);
return handler;
};
}

View file

@ -5,6 +5,7 @@ import * as Loader from "../elements/loader";
import * as Notifications from "../elements/notifications";
import * as ConnectionState from "../states/connection";
import {
IsValidResponse,
Validation,
ValidationOptions,
ValidationResult,
@ -33,7 +34,10 @@ type CommonInput<TType, TValue> = {
* @param thisPopup the current modal
* @returns true if the `value` is valid, an errorMessage as string if it is invalid.
*/
isValid?: (value: string, thisPopup: SimpleModal) => Promise<true | string>;
isValid?: (
value: string,
thisPopup: SimpleModal
) => Promise<IsValidResponse>;
};
};