Skip to content

Commit 0e4b9c4

Browse files
authored
impr: handle backend unavailable in remote validations (@fehmer) (#7105)
1 parent 05afcc5 commit 0e4b9c4

File tree

8 files changed

+62
-47
lines changed

8 files changed

+62
-47
lines changed

frontend/src/ts/elements/input-validation.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export type ValidationResult = {
1414
errorMessage?: string;
1515
};
1616

17+
export type IsValidResponse = true | string | { warning: string };
18+
1719
export type Validation<T> = {
1820
/**
1921
* Zod schema to validate the input value against.
@@ -28,7 +30,7 @@ export type Validation<T> = {
2830
* @param thisPopup the current modal
2931
* @returns true if the `value` is valid, an errorMessage as string if it is invalid.
3032
*/
31-
isValid?: (value: T) => Promise<true | string | { warning: string }>;
33+
isValid?: (value: T) => Promise<IsValidResponse>;
3234

3335
/** custom debounce delay for `isValid` call. defaults to 100 */
3436
debounceDelay?: number;

frontend/src/ts/modals/edit-tag.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import Ape from "../ape";
22
import * as DB from "../db";
3+
import { IsValidResponse } from "../elements/input-validation";
34
import * as Settings from "../pages/settings";
45
import AnimatedModal, { ShowOptions } from "../utils/animated-modal";
56
import { SimpleModal, TextInput } from "../utils/simple-modal";
67
import { TagNameSchema } from "@monkeytype/schemas/users";
78

89
const cleanTagName = (tagName: string): string => tagName.replaceAll(" ", "_");
9-
const tagNameValidation = async (tagName: string): Promise<true | string> => {
10+
const tagNameValidation = async (tagName: string): Promise<IsValidResponse> => {
1011
const validationResult = TagNameSchema.safeParse(cleanTagName(tagName));
1112
if (validationResult.success) return true;
1213
return validationResult.error.errors.map((err) => err.message).join(", ");

frontend/src/ts/modals/google-sign-up.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import AnimatedModal from "../utils/animated-modal";
1616
import { resetIgnoreAuthCallback } from "../firebase";
1717
import { validateWithIndicator } from "../elements/input-validation";
1818
import { UserNameSchema } from "@monkeytype/schemas/users";
19+
import { remoteValidation } from "../utils/remote-validation";
1920

2021
let signedInUser: UserCredential | undefined = undefined;
2122

@@ -154,17 +155,10 @@ function disableInput(): void {
154155

155156
validateWithIndicator(nameInputEl, {
156157
schema: UserNameSchema,
157-
isValid: async (name: string) => {
158-
const checkNameResponse = await Ape.users.getNameAvailability({
159-
params: { name: name },
160-
});
161-
162-
return (
163-
(checkNameResponse.status === 200 &&
164-
checkNameResponse.body.data.available) ||
165-
"Name not available"
166-
);
167-
},
158+
isValid: remoteValidation(
159+
async (name) => Ape.users.getNameAvailability({ params: { name } }),
160+
{ check: (data) => data.available || "Name not available" }
161+
),
168162
debounceDelay: 1000,
169163
callback: (result) => {
170164
if (result.status === "success") {

frontend/src/ts/modals/simple-modals.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import {
4545
import { goToPage } from "../pages/leaderboards";
4646
import FileStorage from "../utils/file-storage";
4747
import { z } from "zod";
48+
import { remoteValidation } from "../utils/remote-validation";
4849

4950
type PopupKey =
5051
| "updateEmail"
@@ -479,17 +480,10 @@ list.updateName = new SimpleModal({
479480
initVal: "",
480481
validation: {
481482
schema: UserNameSchema,
482-
isValid: async (newName: string) => {
483-
const checkNameResponse = await Ape.users.getNameAvailability({
484-
params: { name: newName },
485-
});
486-
487-
return (
488-
(checkNameResponse.status === 200 &&
489-
checkNameResponse.body.data.available) ||
490-
"Name not available"
491-
);
492-
},
483+
isValid: remoteValidation(
484+
async (name) => Ape.users.getNameAvailability({ params: { name } }),
485+
{ check: (data) => data.available || "Name not available" }
486+
),
493487
debounceDelay: 1000,
494488
},
495489
},

frontend/src/ts/pages/friends.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { Connection } from "@monkeytype/schemas/connections";
2929
import { Friend, UserNameSchema } from "@monkeytype/schemas/users";
3030
import * as Loader from "../elements/loader";
3131
import { LocalStorageWithSchema } from "../utils/local-storage-with-schema";
32+
import { remoteValidation } from "../utils/remote-validation";
3233

3334
const pageElement = $(".page.pageFriends");
3435

@@ -75,17 +76,10 @@ const addFriendModal = new SimpleModal({
7576
initVal: "",
7677
validation: {
7778
schema: UserNameSchema,
78-
isValid: async (name: string) => {
79-
const checkNameResponse = await Ape.users.getNameAvailability({
80-
params: { name: name },
81-
});
82-
83-
return (
84-
(checkNameResponse.status === 200 &&
85-
!checkNameResponse.body.data.available) ||
86-
"Unknown user"
87-
);
88-
},
79+
isValid: remoteValidation(
80+
async (name) => Ape.users.getNameAvailability({ params: { name } }),
81+
{ check: (data) => !data.available || "Unknown user" }
82+
),
8983
debounceDelay: 1000,
9084
},
9185
},

frontend/src/ts/pages/login.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { validateWithIndicator } from "../elements/input-validation";
1111
import { isDevEnvironment } from "../utils/misc";
1212
import { z } from "zod";
13+
import { remoteValidation } from "../utils/remote-validation";
1314

1415
let registerForm: {
1516
name?: string;
@@ -73,17 +74,10 @@ const nameInputEl = document.querySelector(
7374
) as HTMLInputElement;
7475
validateWithIndicator(nameInputEl, {
7576
schema: UserNameSchema,
76-
isValid: async (name: string) => {
77-
const checkNameResponse = await Ape.users.getNameAvailability({
78-
params: { name: name },
79-
});
80-
81-
return (
82-
(checkNameResponse.status === 200 &&
83-
checkNameResponse.body.data.available) ||
84-
"Name not available"
85-
);
86-
},
77+
isValid: remoteValidation(
78+
async (name) => Ape.users.getNameAvailability({ params: { name } }),
79+
{ check: (data) => data.available || "Name not available" }
80+
),
8781
debounceDelay: 1000,
8882
callback: (result) => {
8983
registerForm.name =
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { IsValidResponse } from "../elements/input-validation";
2+
3+
type IsValidResonseOrFunction =
4+
| ((message: string) => IsValidResponse)
5+
| IsValidResponse;
6+
export function remoteValidation<V, T>(
7+
call: (
8+
val: V
9+
) => Promise<{ status: number; body: { data?: T; message: string } }>,
10+
options?: {
11+
check?: (data: T) => IsValidResponse;
12+
on4xx?: IsValidResonseOrFunction;
13+
on5xx?: IsValidResonseOrFunction;
14+
}
15+
): (val: V) => Promise<IsValidResponse> {
16+
return async (val) => {
17+
const result = await call(val);
18+
if (result.status <= 299) {
19+
return options?.check?.(result.body.data as T) ?? true;
20+
}
21+
22+
let handler: IsValidResonseOrFunction | undefined;
23+
if (result.status <= 499) {
24+
handler = options?.on4xx ?? ((message) => message);
25+
} else {
26+
handler = options?.on5xx ?? "Server unavailable. Please try again later.";
27+
}
28+
29+
if (typeof handler === "function") return handler(result.body.message);
30+
return handler;
31+
};
32+
}

frontend/src/ts/utils/simple-modal.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as Loader from "../elements/loader";
55
import * as Notifications from "../elements/notifications";
66
import * as ConnectionState from "../states/connection";
77
import {
8+
IsValidResponse,
89
Validation,
910
ValidationOptions,
1011
ValidationResult,
@@ -33,7 +34,10 @@ type CommonInput<TType, TValue> = {
3334
* @param thisPopup the current modal
3435
* @returns true if the `value` is valid, an errorMessage as string if it is invalid.
3536
*/
36-
isValid?: (value: string, thisPopup: SimpleModal) => Promise<true | string>;
37+
isValid?: (
38+
value: string,
39+
thisPopup: SimpleModal
40+
) => Promise<IsValidResponse>;
3741
};
3842
};
3943

0 commit comments

Comments
 (0)