Skip to content

Commit e32155e

Browse files
fehmerMiodec
andauthored
impr: add validations to settings input (@fehmer) (monkeytypegame#6751)
Co-authored-by: Miodec <[email protected]>
1 parent da720ac commit e32155e

File tree

10 files changed

+668
-608
lines changed

10 files changed

+668
-608
lines changed

frontend/src/html/pages/settings.html

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -256,9 +256,6 @@
256256
step="1"
257257
value=""
258258
/>
259-
<button class="save no-auto-handle">
260-
<i class="fas fa-save fa-fw"></i>
261-
</button>
262259
</div>
263260
<div class="buttons">
264261
<button data-config-value="off">off</button>
@@ -288,9 +285,6 @@
288285
step="1"
289286
value=""
290287
/>
291-
<button class="save no-auto-handle">
292-
<i class="fas fa-save fa-fw"></i>
293-
</button>
294288
</div>
295289
<div class="buttons">
296290
<button data-config-value="off">off</button>
@@ -322,9 +316,6 @@
322316
step="1"
323317
value=""
324318
/>
325-
<button class="save no-auto-handle">
326-
<i class="fas fa-save fa-fw"></i>
327-
</button>
328319
</div>
329320
<div class="buttons">
330321
<button data-config-value="off">off</button>
@@ -792,9 +783,6 @@
792783
step="1"
793784
value=""
794785
/>
795-
<button class="save no-auto-handle">
796-
<i class="fas fa-save fa-fw"></i>
797-
</button>
798786
</div>
799787
<div class="buttons">
800788
<button data-config-value="off">off</button>
@@ -1020,9 +1008,6 @@
10201008
min="10"
10211009
max="90"
10221010
/>
1023-
<button class="save no-auto-handle">
1024-
<i class="fas fa-save fa-fw"></i>
1025-
</button>
10261011
</div>
10271012
</div>
10281013
</div>
@@ -1131,9 +1116,6 @@
11311116
class="input"
11321117
min="0"
11331118
/>
1134-
<button class="save no-auto-handle">
1135-
<i class="fas fa-save fa-fw"></i>
1136-
</button>
11371119
</div>
11381120
</div>
11391121
</div>
@@ -1154,9 +1136,6 @@
11541136
class="input"
11551137
tabindex="0"
11561138
/>
1157-
<button class="save no-auto-handle">
1158-
<i class="fas fa-save fa-fw"></i>
1159-
</button>
11601139
</div>
11611140
</div>
11621141
</div>

frontend/src/styles/settings.scss

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
.inputAndButton {
9191
display: grid;
9292
grid-template-columns: auto min-content;
93-
gap: 0.5rem;
93+
// gap: 0.5rem;
9494
margin-bottom: 0.5rem;
9595

9696
span {
@@ -104,6 +104,20 @@
104104
margin-right: 0rem;
105105
}
106106
}
107+
108+
.hasError {
109+
animation: shake 0.1s ease-in-out infinite;
110+
}
111+
112+
.statusIndicator {
113+
opacity: 0;
114+
}
115+
&:has(input:focus),
116+
&:has([data-indicator-status="failed"]) {
117+
.statusIndicator {
118+
opacity: 1;
119+
}
120+
}
107121
}
108122

109123
.rangeGroup {

frontend/src/ts/commandline/commandline.ts

Lines changed: 33 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Command, CommandsSubgroup, CommandWithValidation } from "./types";
1414
import { areSortedArraysEqual } from "../utils/arrays";
1515
import { parseIntOptional } from "../utils/numbers";
1616
import { debounce } from "throttle-debounce";
17+
import { createInputEventHandler } from "../elements/input-validation";
1718

1819
type CommandlineMode = "search" | "input";
1920
type InputModeParams = {
@@ -618,6 +619,18 @@ async function runActiveCommand(): Promise<void> {
618619
value: command.defaultValue?.() ?? "",
619620
icon: command.icon ?? "fa-chevron-right",
620621
};
622+
if ("validation" in command && !handlersCache.has(command.id)) {
623+
const commandWithValidation = command as CommandWithValidation<unknown>;
624+
const handler = createInputEventHandler(
625+
updateValidationResult,
626+
commandWithValidation.validation,
627+
"inputValueConvert" in commandWithValidation
628+
? commandWithValidation.inputValueConvert
629+
: undefined
630+
);
631+
handlersCache.set(command.id, handler);
632+
}
633+
621634
await updateInput(inputModeParams.value as string);
622635
hideCommands();
623636
} else if (command.subgroup) {
@@ -788,48 +801,10 @@ function updateValidationResult(
788801
}
789802
}
790803

791-
async function isValid(
792-
checkValue: unknown,
793-
originalValue: string,
794-
originalInput: HTMLInputElement,
795-
validation: CommandWithValidation<unknown>["validation"]
796-
): Promise<void> {
797-
updateValidationResult({ status: "checking" });
798-
799-
if (validation.schema !== undefined) {
800-
const schemaResult = validation.schema.safeParse(checkValue);
801-
802-
if (!schemaResult.success) {
803-
updateValidationResult({
804-
status: "failed",
805-
errorMessage: schemaResult.error.errors
806-
.map((err) => err.message)
807-
.join(", "),
808-
});
809-
return;
810-
}
811-
}
812-
813-
if (validation.isValid === undefined) {
814-
updateValidationResult({ status: "success" });
815-
return;
816-
}
817-
818-
const result = await validation.isValid(checkValue);
819-
if (originalInput.value !== originalValue) {
820-
//value has change in the meantime, discard result
821-
return;
822-
}
823-
824-
if (result === true) {
825-
updateValidationResult({ status: "success" });
826-
} else {
827-
updateValidationResult({
828-
status: "failed",
829-
errorMessage: result,
830-
});
831-
}
832-
}
804+
/*
805+
* Handlers needs to be created only once per command to ensure they debounce with the given delay
806+
*/
807+
const handlersCache = new Map<string, (e: Event) => Promise<void>>();
833808

834809
const modal = new AnimatedModal({
835810
dialogId: "commandLine",
@@ -921,34 +896,24 @@ const modal = new AnimatedModal({
921896
}
922897
});
923898

924-
input.addEventListener(
925-
"input",
926-
debounce(100, async (e) => {
927-
if (
928-
inputModeParams === null ||
929-
inputModeParams.command === null ||
930-
!("validation" in inputModeParams.command)
931-
) {
932-
return;
933-
}
934-
935-
const originalInput = (e as InputEvent).target as HTMLInputElement;
936-
const currentValue = originalInput.value;
937-
let checkValue: unknown = currentValue;
938-
const command =
939-
inputModeParams.command as CommandWithValidation<unknown>;
899+
input.addEventListener("input", async (e) => {
900+
if (
901+
inputModeParams === null ||
902+
inputModeParams.command === null ||
903+
!("validation" in inputModeParams.command)
904+
) {
905+
return;
906+
}
940907

941-
if ("inputValueConvert" in command) {
942-
checkValue = command.inputValueConvert(currentValue);
943-
}
944-
await isValid(
945-
checkValue,
946-
currentValue,
947-
originalInput,
948-
command.validation
908+
const handler = handlersCache.get(inputModeParams.command.id);
909+
if (handler === undefined) {
910+
throw new Error(
911+
`Expected handler for command ${inputModeParams.command.id} is missing`
949912
);
950-
})
951-
);
913+
}
914+
915+
await handler(e);
916+
});
952917

953918
modalEl.addEventListener("mousemove", (_e) => {
954919
mouseMode = true;

frontend/src/ts/commandline/types.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Config } from "@monkeytype/schemas/configs";
22
import AnimatedModal from "../utils/animated-modal";
3-
import { z } from "zod";
3+
import { Validation } from "../elements/input-validation";
44

55
// this file is needed becauase otherwise it would produce a circular dependency
66

@@ -47,21 +47,7 @@ export type CommandWithValidation<T> = (T extends string
4747
* If the schema is defined it is always checked first.
4848
* Only if the schema validaton is passed or missing the `isValid` method is called.
4949
*/
50-
validation: {
51-
/**
52-
* Zod schema to validate the input value against.
53-
* The indicator will show the error messages from the schema.
54-
*/
55-
schema?: z.Schema<T>;
56-
/**
57-
* Custom async validation method.
58-
* This is intended to be used for validations that cannot be handled with a Zod schema like server-side validations.
59-
* @param value current input value
60-
* @param thisPopup the current modal
61-
* @returns true if the `value` is valid, an errorMessage as string if it is invalid.
62-
*/
63-
isValid?: (value: T) => Promise<true | string>;
64-
};
50+
validation: Validation<T>;
6551
};
6652

6753
export type CommandsSubgroup = {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ export class InputIndicator {
7474
}
7575

7676
$(this.inputElement).css("padding-right", "2.1em");
77+
this.parentElement.attr("data-indicator-status", optionId);
7778
}
7879

7980
get(): keyof typeof this.options | null {

0 commit comments

Comments
 (0)