From d42c1b2a84c3039e50d4ca33cb58177016a2eb82 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Fri, 12 Dec 2025 09:55:23 +0100 Subject: [PATCH 1/2] fix(config): sanitize config failing on array with multiple problems (@fehmer) (#7221) Sanitize throws error if an object contains an array with 1) an invalid value and 2) too few items. The list of problems contains the path to the array twice and tries to remove the invalid element from the already deleted array. Config object: ```json "customPolyglot": [ "japanese" ] ``` --- frontend/__tests__/utils/sanitize.spec.ts | 12 ++++++++++++ frontend/src/ts/utils/sanitize.ts | 5 ++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/frontend/__tests__/utils/sanitize.spec.ts b/frontend/__tests__/utils/sanitize.spec.ts index b8de6b2e9c1e..1f74826675b4 100644 --- a/frontend/__tests__/utils/sanitize.spec.ts +++ b/frontend/__tests__/utils/sanitize.spec.ts @@ -151,6 +151,18 @@ describe("sanitize function", () => { optional: { name: "Alice", age: 23 }, }, }, + { + input: { + name: "Alice", + //results in two errors on the same path. array with invalid value and not enough items + enumArray: ["invalid" as any], + }, + expected: { + mandatory: false, + partial: { name: "Alice" }, //enumArray is removed + optional: false, + }, + }, ]; it.for(testCases)("object mandatory with $input", ({ input, expected }) => { diff --git a/frontend/src/ts/utils/sanitize.ts b/frontend/src/ts/utils/sanitize.ts index 9398db54d76f..62edc7728e28 100644 --- a/frontend/src/ts/utils/sanitize.ts +++ b/frontend/src/ts/utils/sanitize.ts @@ -1,9 +1,12 @@ import { z } from "zod"; function removeProblems( - obj: T, + obj: T | undefined, problems: (number | string)[], ): T | undefined { + //already removed + if (obj === undefined) return undefined; + if (Array.isArray(obj)) { if (problems.length === obj.length) return undefined; From 92533e2bdd9558466c036c8b59b693e0763c6d43 Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Fri, 12 Dec 2025 10:18:15 +0100 Subject: [PATCH 2/2] refactor: move configGroup to config-metadata (@fehmer) (#7222) --- frontend/src/ts/config-metadata.ts | 97 ++++++++++++++++++++- frontend/src/ts/modals/edit-preset.ts | 4 +- packages/schemas/src/configs.ts | 116 -------------------------- 3 files changed, 98 insertions(+), 119 deletions(-) diff --git a/frontend/src/ts/config-metadata.ts b/frontend/src/ts/config-metadata.ts index 2dfa38ae0597..a9794c851c81 100644 --- a/frontend/src/ts/config-metadata.ts +++ b/frontend/src/ts/config-metadata.ts @@ -37,6 +37,11 @@ export type ConfigMetadata = { // : never; // }; + /** + * Group that this config belongs to. Used for partial presets + */ + group: ConfigSchemas.ConfigGroupName; + /** * Is a test restart required after this config change? */ @@ -87,13 +92,13 @@ export type ConfigMetadataObject = { //todo: // maybe have generic set somehow handle test restarting -// maybe add config group to each metadata object? all though its already defined in ConfigGroupsLiteral export const configMetadata: ConfigMetadataObject = { // test punctuation: { icon: "fa-at", changeRequiresRestart: true, + group: "test", overrideValue: ({ value, currentConfig }) => { if (currentConfig.mode === "quote") { return false; @@ -104,6 +109,7 @@ export const configMetadata: ConfigMetadataObject = { numbers: { icon: "fa-hashtag", changeRequiresRestart: true, + group: "test", overrideValue: ({ value, currentConfig }) => { if (currentConfig.mode === "quote") { return false; @@ -115,6 +121,7 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-font", displayString: "word count", changeRequiresRestart: true, + group: "test", overrideConfig: ({ currentConfig }) => { if (currentConfig.mode !== "words") { return { @@ -128,6 +135,7 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-clock", changeRequiresRestart: true, displayString: "time", + group: "test", overrideConfig: ({ currentConfig }) => { if (currentConfig.mode !== "time") { return { @@ -140,6 +148,7 @@ export const configMetadata: ConfigMetadataObject = { mode: { icon: "fa-bars", changeRequiresRestart: true, + group: "test", overrideConfig: ({ value }) => { if (value === "custom" || value === "quote" || value === "zen") { return { @@ -159,6 +168,7 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-quote-right", displayString: "quote length", changeRequiresRestart: true, + group: "test", overrideConfig: ({ currentConfig }) => { if (currentConfig.mode !== "quote") { return { @@ -172,52 +182,62 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-language", displayString: "language", changeRequiresRestart: true, + group: "test", }, burstHeatmap: { icon: "fa-fire", displayString: "word burst heatmap", changeRequiresRestart: false, + group: "test", }, // behavior difficulty: { icon: "fa-star", changeRequiresRestart: true, + group: "behavior", }, quickRestart: { icon: "fa-redo-alt", displayString: "quick restart", changeRequiresRestart: false, + group: "behavior", }, repeatQuotes: { icon: "fa-sync-alt", displayString: "repeat quotes", changeRequiresRestart: false, + group: "behavior", }, blindMode: { icon: "fa-eye-slash", displayString: "blind mode", changeRequiresRestart: false, + group: "behavior", }, alwaysShowWordsHistory: { icon: "fa-align-left", displayString: "always show words history", changeRequiresRestart: false, + group: "behavior", }, singleListCommandLine: { icon: "fa-list", displayString: "single list command line", changeRequiresRestart: false, + group: "behavior", }, minWpm: { icon: "fa-bomb", displayString: "min speed", changeRequiresRestart: true, + group: "behavior", }, minWpmCustomSpeed: { icon: "fa-bomb", displayString: "min speed custom", changeRequiresRestart: true, + group: "behavior", overrideConfig: ({ currentConfig }) => { if (currentConfig.minWpm !== "custom") { return { @@ -231,11 +251,13 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-bomb", displayString: "min accuracy", changeRequiresRestart: true, + group: "behavior", }, minAccCustom: { icon: "fa-bomb", displayString: "min accuracy custom", changeRequiresRestart: true, + group: "behavior", overrideConfig: ({ currentConfig }) => { if (currentConfig.minAcc !== "custom") { return { @@ -249,20 +271,24 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-bomb", displayString: "min word burst", changeRequiresRestart: true, + group: "behavior", }, minBurstCustomSpeed: { icon: "fa-bomb", displayString: "min word burst custom speed", changeRequiresRestart: true, + group: "behavior", }, britishEnglish: { icon: "fa-language", displayString: "british english", changeRequiresRestart: true, + group: "behavior", }, funbox: { icon: "fa-gamepad", changeRequiresRestart: true, + group: "behavior", isBlocked: ({ value, currentConfig }) => { if (!checkCompatibility(value)) { Notifications.add( @@ -291,6 +317,7 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-tint", displayString: "custom layoutfluid", changeRequiresRestart: true, + group: "behavior", overrideValue: ({ value }) => { return Array.from(new Set(value)); }, @@ -299,6 +326,7 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-language", displayString: "custom polyglot", changeRequiresRestart: false, + group: "behavior", overrideValue: ({ value }) => { return Array.from(new Set(value)); }, @@ -309,6 +337,7 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-feather-alt", changeRequiresRestart: false, displayString: "freedom mode", + group: "input", overrideConfig: ({ value }) => { if (value) { return { @@ -322,16 +351,19 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-minus", displayString: "strict space", changeRequiresRestart: true, + group: "input", }, oppositeShiftMode: { icon: "fa-exchange-alt", displayString: "opposite shift mode", changeRequiresRestart: false, + group: "input", }, stopOnError: { icon: "fa-hand-paper", displayString: "stop on error", changeRequiresRestart: true, + group: "input", overrideConfig: ({ value }) => { if (value !== "off") { return { @@ -345,6 +377,7 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-backspace", displayString: "confidence mode", changeRequiresRestart: false, + group: "input", overrideConfig: ({ value }) => { if (value !== "off") { return { @@ -359,36 +392,43 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-step-forward", displayString: "quick end", changeRequiresRestart: false, + group: "input", }, indicateTypos: { icon: "fa-exclamation", displayString: "indicate typos", changeRequiresRestart: false, + group: "input", }, compositionDisplay: { icon: "fa-language", displayString: "composition display", changeRequiresRestart: false, + group: "input", }, hideExtraLetters: { icon: "fa-eye-slash", displayString: "hide extra letters", changeRequiresRestart: false, + group: "input", }, lazyMode: { icon: "fa-couch", displayString: "lazy mode", changeRequiresRestart: true, + group: "input", }, layout: { icon: "fa-keyboard", displayString: "layout", changeRequiresRestart: true, + group: "input", }, codeUnindentOnBackspace: { icon: "fa-code", displayString: "code unindent on backspace", changeRequiresRestart: true, + group: "input", }, // sound @@ -396,21 +436,25 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-volume-down", displayString: "sound volume", changeRequiresRestart: false, + group: "sound", }, playSoundOnClick: { icon: "fa-volume-up", displayString: "play sound on click", changeRequiresRestart: false, + group: "sound", }, playSoundOnError: { icon: "fa-volume-mute", displayString: "play sound on error", changeRequiresRestart: false, + group: "sound", }, playTimeWarning: { icon: "fa-exclamation-triangle", displayString: "play time warning", changeRequiresRestart: false, + group: "sound", }, // caret @@ -418,16 +462,19 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-i-cursor", displayString: "smooth caret", changeRequiresRestart: false, + group: "caret", }, caretStyle: { icon: "fa-i-cursor", displayString: "caret style", changeRequiresRestart: false, + group: "caret", }, paceCaret: { icon: "fa-i-cursor", displayString: "pace caret", changeRequiresRestart: false, + group: "caret", isBlocked: ({ value }) => { if (document.readyState === "complete") { if ((value === "pb" || value === "tagPb") && !isAuthenticated()) { @@ -445,6 +492,7 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-i-cursor", displayString: "pace caret custom speed", changeRequiresRestart: false, + group: "caret", overrideConfig: ({ currentConfig }) => { if (currentConfig.paceCaret !== "custom") { return { @@ -458,11 +506,13 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-i-cursor", displayString: "pace caret style", changeRequiresRestart: false, + group: "caret", }, repeatedPace: { icon: "fa-i-cursor", displayString: "repeated pace", changeRequiresRestart: false, + group: "caret", }, // appearance @@ -470,42 +520,50 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-chart-pie", displayString: "live progress style", changeRequiresRestart: false, + group: "appearance", }, liveSpeedStyle: { icon: "fa-tachometer-alt", displayString: "live speed style", changeRequiresRestart: false, + group: "appearance", }, liveAccStyle: { icon: "fa-tachometer-alt", displayString: "live accuracy style", changeRequiresRestart: false, + group: "appearance", }, liveBurstStyle: { icon: "fa-tachometer-alt", displayString: "live word burst style", changeRequiresRestart: false, + group: "appearance", }, timerColor: { icon: "fa-chart-pie", displayString: "timer color", changeRequiresRestart: false, + group: "appearance", }, timerOpacity: { icon: "fa-chart-pie", displayString: "timer opacity", changeRequiresRestart: false, + group: "appearance", }, highlightMode: { icon: "fa-highlighter", displayString: "highlight mode", changeRequiresRestart: false, + group: "appearance", }, tapeMode: { icon: "fa-tape", triggerResize: true, changeRequiresRestart: false, displayString: "tape mode", + group: "appearance", overrideConfig: ({ value }) => { if (value !== "off") { return { @@ -520,16 +578,19 @@ export const configMetadata: ConfigMetadataObject = { displayString: "tape margin", triggerResize: true, changeRequiresRestart: false, + group: "appearance", }, smoothLineScroll: { icon: "fa-align-left", displayString: "smooth line scroll", changeRequiresRestart: false, + group: "appearance", }, showAllLines: { icon: "fa-align-left", changeRequiresRestart: false, displayString: "show all lines", + group: "appearance", isBlocked: ({ value, currentConfig }) => { if (value && currentConfig.tapeMode !== "off") { Notifications.add("Show all lines doesn't support tape mode.", 0); @@ -542,43 +603,51 @@ export const configMetadata: ConfigMetadataObject = { icon: "00", displayString: "always show decimal places", changeRequiresRestart: false, + group: "appearance", }, typingSpeedUnit: { icon: "fa-tachometer-alt", displayString: "typing speed unit", changeRequiresRestart: false, + group: "appearance", }, startGraphsAtZero: { icon: "fa-chart-line", displayString: "start graphs at zero", changeRequiresRestart: false, + group: "appearance", }, maxLineWidth: { icon: "fa-text-width", changeRequiresRestart: false, triggerResize: true, displayString: "max line width", + group: "appearance", }, fontSize: { icon: "fa-font", changeRequiresRestart: false, triggerResize: true, displayString: "font size", + group: "appearance", }, fontFamily: { icon: "fa-font", displayString: "font family", changeRequiresRestart: false, + group: "appearance", }, keymapMode: { icon: "fa-keyboard", displayString: "keymap mode", changeRequiresRestart: false, + group: "appearance", }, keymapLayout: { icon: "fa-keyboard", displayString: "keymap layout", changeRequiresRestart: false, + group: "appearance", overrideConfig: ({ currentConfig }) => currentConfig.keymapMode === "off" ? { keymapMode: "static" } : {}, }, @@ -586,6 +655,7 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-keyboard", displayString: "keymap style", changeRequiresRestart: false, + group: "appearance", overrideConfig: ({ currentConfig }) => currentConfig.keymapMode === "off" ? { keymapMode: "static" } : {}, }, @@ -593,6 +663,7 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-keyboard", displayString: "keymap legend style", changeRequiresRestart: false, + group: "appearance", overrideConfig: ({ currentConfig }) => currentConfig.keymapMode === "off" ? { keymapMode: "static" } : {}, }, @@ -600,6 +671,7 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-keyboard", displayString: "keymap show top row", changeRequiresRestart: false, + group: "appearance", overrideConfig: ({ currentConfig }) => currentConfig.keymapMode === "off" ? { keymapMode: "static" } : {}, }, @@ -608,6 +680,7 @@ export const configMetadata: ConfigMetadataObject = { triggerResize: true, changeRequiresRestart: false, displayString: "keymap size", + group: "appearance", overrideValue: ({ value }) => { if (value < 0.5) value = 0.5; if (value > 3.5) value = 3.5; @@ -622,16 +695,19 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-adjust", displayString: "flip test colors", changeRequiresRestart: false, + group: "theme", }, colorfulMode: { icon: "fa-fill-drip", displayString: "colorful mode", changeRequiresRestart: false, + group: "theme", }, customBackground: { icon: "fa-link", displayString: "URL background", changeRequiresRestart: false, + group: "theme", overrideValue: ({ value }) => { return value.trim(); }, @@ -640,31 +716,37 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-image", displayString: "custom background size", changeRequiresRestart: false, + group: "theme", }, customBackgroundFilter: { icon: "fa-image", displayString: "custom background filter", changeRequiresRestart: false, + group: "theme", }, autoSwitchTheme: { icon: "fa-palette", displayString: "auto switch theme", changeRequiresRestart: false, + group: "theme", }, themeLight: { icon: "fa-palette", displayString: "theme light", changeRequiresRestart: false, + group: "theme", }, themeDark: { icon: "fa-palette", displayString: "theme dark", changeRequiresRestart: false, + group: "theme", }, randomTheme: { icon: "fa-palette", changeRequiresRestart: false, displayString: "random theme", + group: "theme", isBlocked: ({ value }) => { if (value === "custom") { const snapshot = DB.getSnapshot(); @@ -697,10 +779,12 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-palette", displayString: "favorite themes", changeRequiresRestart: false, + group: "theme", }, theme: { icon: "fa-palette", changeRequiresRestart: false, + group: "theme", overrideConfig: () => { return { customTheme: false, @@ -711,11 +795,13 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-palette", displayString: "custom theme", changeRequiresRestart: false, + group: "theme", }, customThemeColors: { icon: "fa-palette", displayString: "custom theme colors", changeRequiresRestart: false, + group: "theme", overrideValue: ({ value }) => { const allColorsThesame = value.every((color) => color === value[0]); if (allColorsThesame) { @@ -731,26 +817,31 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-question", displayString: "show key tips", changeRequiresRestart: false, + group: "hideElements", }, showOutOfFocusWarning: { icon: "fa-exclamation", displayString: "show out of focus warning", changeRequiresRestart: false, + group: "hideElements", }, capsLockWarning: { icon: "fa-exclamation-triangle", displayString: "caps lock warning", changeRequiresRestart: false, + group: "hideElements", }, showAverage: { icon: "fa-chart-bar", displayString: "show average", changeRequiresRestart: false, + group: "hideElements", }, showPb: { icon: "fa-crown", displayString: "show personal best", changeRequiresRestart: false, + group: "hideElements", }, // other (hidden) @@ -758,6 +849,7 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-chart-line", displayString: "account chart", changeRequiresRestart: false, + group: "hidden", overrideValue: ({ value, currentValue }) => { // if both speed and accuracy are off, set opposite to on // i dedicate this fix to AshesOfAFallen and our 2 collective brain cells @@ -772,17 +864,20 @@ export const configMetadata: ConfigMetadataObject = { icon: "fa-egg", displayString: "monkey", changeRequiresRestart: false, + group: "hidden", }, monkeyPowerLevel: { icon: "fa-egg", displayString: "monkey power level", changeRequiresRestart: false, + group: "hidden", }, // ads ads: { icon: "fa-ad", changeRequiresRestart: false, + group: "ads", overrideValue: ({ value }) => { if (isDevEnvironment()) { return "off"; diff --git a/frontend/src/ts/modals/edit-preset.ts b/frontend/src/ts/modals/edit-preset.ts index a45b976cb3f4..694e9b69ffc8 100644 --- a/frontend/src/ts/modals/edit-preset.ts +++ b/frontend/src/ts/modals/edit-preset.ts @@ -15,7 +15,6 @@ import { getPreset } from "../controllers/preset-controller"; import { ConfigGroupName, ConfigGroupNameSchema, - ConfigGroupsLiteral, ConfigKey, Config as ConfigType, } from "@monkeytype/schemas/configs"; @@ -23,6 +22,7 @@ import { getDefaultConfig } from "../constants/default-config"; import { SnapshotPreset } from "../constants/default-snapshot"; import { ValidatedHtmlInputElement } from "../elements/input-validation"; import { qsr } from "../utils/dom"; +import { configMetadata } from "../config-metadata"; const state = { presetType: "full" as PresetType, @@ -363,7 +363,7 @@ async function apply(): Promise { } function getSettingGroup(configFieldName: ConfigKey): ConfigGroupName { - return ConfigGroupsLiteral[configFieldName]; + return configMetadata[configFieldName].group; } function getPartialConfigChanges( diff --git a/packages/schemas/src/configs.ts b/packages/schemas/src/configs.ts index d8d1f54ab5cc..edfd6af29355 100644 --- a/packages/schemas/src/configs.ts +++ b/packages/schemas/src/configs.ts @@ -508,120 +508,4 @@ export const ConfigGroupNameSchema = z.enum([ "hidden", "ads", ]); - export type ConfigGroupName = z.infer; - -export const ConfigGroupsLiteral = { - //test - punctuation: "test", - numbers: "test", - words: "test", - time: "test", - mode: "test", - quoteLength: "test", - language: "test", - burstHeatmap: "test", - - //behavior - difficulty: "behavior", - quickRestart: "behavior", - repeatQuotes: "behavior", - blindMode: "behavior", - alwaysShowWordsHistory: "behavior", - singleListCommandLine: "behavior", - minWpm: "behavior", - minWpmCustomSpeed: "behavior", - minAcc: "behavior", - minAccCustom: "behavior", - minBurst: "behavior", - minBurstCustomSpeed: "behavior", - britishEnglish: "behavior", - funbox: "behavior", //todo: maybe move to test? - customLayoutfluid: "behavior", - customPolyglot: "behavior", - - //input - freedomMode: "input", - strictSpace: "input", - oppositeShiftMode: "input", - stopOnError: "input", - confidenceMode: "input", - quickEnd: "input", - indicateTypos: "input", - compositionDisplay: "input", - hideExtraLetters: "input", - lazyMode: "input", - layout: "input", - codeUnindentOnBackspace: "input", - - //sound - soundVolume: "sound", - playSoundOnClick: "sound", - playSoundOnError: "sound", - playTimeWarning: "sound", - - //caret - smoothCaret: "caret", - caretStyle: "caret", - paceCaret: "caret", - paceCaretCustomSpeed: "caret", - paceCaretStyle: "caret", - repeatedPace: "caret", - - //appearance - timerStyle: "appearance", - liveSpeedStyle: "appearance", - liveAccStyle: "appearance", - liveBurstStyle: "appearance", - timerColor: "appearance", - timerOpacity: "appearance", - highlightMode: "appearance", - tapeMode: "appearance", - tapeMargin: "appearance", - smoothLineScroll: "appearance", - showAllLines: "appearance", - alwaysShowDecimalPlaces: "appearance", - typingSpeedUnit: "appearance", - startGraphsAtZero: "appearance", - maxLineWidth: "appearance", - fontSize: "appearance", - fontFamily: "appearance", - keymapMode: "appearance", - keymapLayout: "appearance", - keymapStyle: "appearance", - keymapLegendStyle: "appearance", - keymapShowTopRow: "appearance", - keymapSize: "appearance", - - //theme - flipTestColors: "theme", - colorfulMode: "theme", - customBackground: "theme", - customBackgroundSize: "theme", - customBackgroundFilter: "theme", - autoSwitchTheme: "theme", - themeLight: "theme", - themeDark: "theme", - randomTheme: "theme", - favThemes: "theme", - theme: "theme", - customTheme: "theme", - customThemeColors: "theme", - - //hide elements - showKeyTips: "hideElements", - showOutOfFocusWarning: "hideElements", - capsLockWarning: "hideElements", - showAverage: "hideElements", - showPb: "hideElements", - - //other - accountChart: "hidden", - monkey: "hidden", - monkeyPowerLevel: "hidden", - - //ads - ads: "ads", -} as const satisfies Record; - -export type ConfigGroups = typeof ConfigGroupsLiteral;