diff --git a/frontend/__tests__/root/config.spec.ts b/frontend/__tests__/root/config.spec.ts
index 90eb7ca8fa44..0f89fb6aa4d6 100644
--- a/frontend/__tests__/root/config.spec.ts
+++ b/frontend/__tests__/root/config.spec.ts
@@ -10,7 +10,7 @@ import {
Config as ConfigType,
CaretStyleSchema,
} from "@monkeytype/schemas/configs";
-import * as FunboxValidation from "../../src/ts/test/funbox/funbox-validation";
+import * as FunboxValidation from "../../src/ts/config/funbox-validation";
import * as ConfigValidation from "../../src/ts/config/validation";
import * as ConfigEvent from "../../src/ts/observables/config-event";
import * as ApeConfig from "../../src/ts/ape/config";
diff --git a/frontend/__tests__/test/funbox/funbox-validation.spec.ts b/frontend/__tests__/test/funbox/funbox-validation.spec.ts
index 1f033d3d6d4e..00c1e0825028 100644
--- a/frontend/__tests__/test/funbox/funbox-validation.spec.ts
+++ b/frontend/__tests__/test/funbox/funbox-validation.spec.ts
@@ -1,27 +1,18 @@
-import { describe, it, expect, afterEach, vi } from "vitest";
-import { canSetConfigWithCurrentFunboxes } from "../../../src/ts/test/funbox/funbox-validation";
+import { describe, it, expect } from "vitest";
+import { canSetConfigWithCurrentFunboxes } from "../../../src/ts/config/funbox-validation";
-import * as Notifications from "../../../src/ts/states/notifications";
import { FunboxName } from "@monkeytype/schemas/configs";
describe("funbox-validation", () => {
describe("canSetConfigWithCurrentFunboxes", () => {
- const addNotificationMock = vi.spyOn(
- Notifications,
- "showNoticeNotification",
- );
- afterEach(() => {
- addNotificationMock.mockClear();
- });
-
const testCases = [
//checks for frontendForcedConfig
{
key: "mode",
value: "zen",
funbox: ["memory"],
- error: "You can't set mode to zen with currently active funboxes.",
+ expected: false,
},
- { key: "mode", value: "words", funbox: ["memory"] }, //ok
+ { key: "mode", value: "words", funbox: ["memory"], expected: true },
//checks for zen mode
...[
@@ -40,10 +31,15 @@ describe("funbox-validation", () => {
key: "mode",
value: "zen",
funbox: [funbox],
- error: "You can't set mode to zen with currently active funboxes.",
+ expected: false,
})),
- { key: "mode", value: "zen", funbox: ["mirror"] }, //ok
- { key: "mode", value: "zen", funbox: ["space_balls"] }, //no frontendFunctions
+ { key: "mode", value: "zen", funbox: ["mirror"], expected: true },
+ {
+ key: "mode",
+ value: "zen",
+ funbox: ["space_balls"],
+ expected: true,
+ },
//checks for words and custom
...["quote", "custom"].flatMap((value) =>
@@ -56,23 +52,22 @@ describe("funbox-validation", () => {
key: "mode",
value,
funbox: [funbox],
- error: `You can't set mode to ${value} with currently active funboxes.`,
+ expected: false,
})),
),
- { key: "mode", value: "quote", funbox: ["space_balls"] }, //no frontendFunctions
+ {
+ key: "mode",
+ value: "quote",
+ funbox: ["space_balls"],
+ expected: true,
+ },
];
it.for(testCases)(
`check $funbox with $key=$value`,
- ({ key, value, funbox, error }) => {
+ ({ key, value, funbox, expected }) => {
expect(
canSetConfigWithCurrentFunboxes(key, value, funbox as FunboxName[]),
- ).toBe(error === undefined);
-
- if (error !== undefined) {
- expect(addNotificationMock).toHaveBeenCalledWith(error, {
- durationMs: 5000,
- });
- }
+ ).toBe(expected);
},
);
});
diff --git a/frontend/src/terms-of-service.html b/frontend/src/terms-of-service.html
index 8430fedaa118..0ae96cb1e318 100644
--- a/frontend/src/terms-of-service.html
+++ b/frontend/src/terms-of-service.html
@@ -211,26 +211,28 @@
Limitations
Users as a whole.
Privacy Policy
- If you use our Services, you must abide by our Privacy Policy. You
- acknowledge that you have read our
-
- Privacy Policy
-
- and understand that it sets forth how we collect, use, and store
- your information. If you do not agree with our Privacy Statement, then
- you must stop using the Services immediately. Any person, entity, or
- service collecting data from the Services must comply with our Privacy
- Statement. Misuse of any User's Personal Information is prohibited. If
- you collect any Personal Information from a User, you agree that you
- will only use the Personal Information you gather for the purpose for
- which the User has authorized it. You agree that you will reasonably
- secure any Personal Information you have gathered from the Services, and
- you will respond promptly to complaints, removal requests, and 'do not
- contact' requests from us or Users.
+
+ If you use our Services, you must abide by our Privacy Policy. You
+ acknowledge that you have read our
+
+ Privacy Policy
+
+ and understand that it sets forth how we collect, use, and store
+ your information. If you do not agree with our Privacy Statement, then
+ you must stop using the Services immediately. Any person, entity, or
+ service collecting data from the Services must comply with our Privacy
+ Statement. Misuse of any User's Personal Information is prohibited. If
+ you collect any Personal Information from a User, you agree that you
+ will only use the Personal Information you gather for the purpose for
+ which the User has authorized it. You agree that you will reasonably
+ secure any Personal Information you have gathered from the Services,
+ and you will respond promptly to complaints, removal requests, and 'do
+ not contact' requests from us or Users.
+
Limitations on Automated Use
You shouldn't use bots or access our Services in malicious or prohibited
diff --git a/frontend/src/ts/config/funbox-validation.ts b/frontend/src/ts/config/funbox-validation.ts
new file mode 100644
index 000000000000..974a4876d3d2
--- /dev/null
+++ b/frontend/src/ts/config/funbox-validation.ts
@@ -0,0 +1,89 @@
+import { checkForcedConfig, getFunbox } from "@monkeytype/funbox";
+import { Config, ConfigValue, FunboxName } from "@monkeytype/schemas/configs";
+
+export function canSetConfigWithCurrentFunboxes(
+ key: string,
+ value: ConfigValue,
+ funbox: FunboxName[] = [],
+): boolean {
+ const funboxes = getFunbox(funbox);
+ if (key === "mode") {
+ let fb = getFunbox(funbox).filter(
+ (f) =>
+ f.frontendForcedConfig?.["mode"] !== undefined &&
+ !(f.frontendForcedConfig["mode"] as ConfigValue[]).includes(value),
+ );
+ if (value === "zen") {
+ fb = fb.concat(
+ funboxes.filter((f) => {
+ const funcs = f.frontendFunctions ?? [];
+ const props = f.properties ?? [];
+ return (
+ funcs.includes("getWord") ||
+ funcs.includes("pullSection") ||
+ funcs.includes("alterText") ||
+ funcs.includes("withWords") ||
+ props.includes("changesCapitalisation") ||
+ props.includes("nospace") ||
+ props.some((fp) => fp.startsWith("toPush:")) ||
+ props.includes("changesWordsVisibility") ||
+ props.includes("speaks") ||
+ props.includes("changesLayout") ||
+ props.includes("changesWordsFrequency")
+ );
+ }),
+ );
+ }
+ if (value === "quote" || value === "custom") {
+ fb = fb.concat(
+ funboxes.filter((f) => {
+ const funcs = f.frontendFunctions ?? [];
+ const props = f.properties ?? [];
+ return (
+ funcs.includes("getWord") ||
+ funcs.includes("pullSection") ||
+ funcs.includes("withWords") ||
+ props.includes("changesWordsFrequency")
+ );
+ }),
+ );
+ }
+
+ if (fb.length > 0) {
+ return false;
+ }
+ }
+ if (!checkForcedConfig(key, value, funboxes).result) {
+ return false;
+ }
+
+ return true;
+}
+
+export type FunboxConfigError = {
+ key: string;
+ value: ConfigValue;
+};
+
+export function canSetFunboxWithConfig(
+ funbox: FunboxName,
+ config: Config,
+): { ok: true } | { ok: false; errors: FunboxConfigError[] } {
+ const funboxToCheck = [...config.funbox, funbox];
+
+ const errors: FunboxConfigError[] = [];
+ for (const [configKey, configValue] of Object.entries(config)) {
+ if (
+ !canSetConfigWithCurrentFunboxes(configKey, configValue, funboxToCheck)
+ ) {
+ errors.push({
+ key: configKey,
+ value: configValue,
+ });
+ }
+ }
+ if (errors.length > 0) {
+ return { ok: false, errors };
+ }
+ return { ok: true };
+}
diff --git a/frontend/src/ts/config/metadata.ts b/frontend/src/ts/config/metadata.ts
index d3947ae9bec6..4a5f62f5c2a6 100644
--- a/frontend/src/ts/config/metadata.ts
+++ b/frontend/src/ts/config/metadata.ts
@@ -2,7 +2,7 @@ import { checkCompatibility } from "@monkeytype/funbox";
import * as DB from "../db";
import { showNoticeNotification } from "../states/notifications";
import { isAuthenticated } from "../firebase";
-import { canSetFunboxWithConfig } from "../test/funbox/funbox-validation";
+import { canSetFunboxWithConfig } from "./funbox-validation";
import { reloadAfter } from "../utils/misc";
import { isDevEnvironment } from "../utils/env";
import * as ConfigSchemas from "@monkeytype/schemas/configs";
@@ -307,9 +307,10 @@ export const configMetadata: ConfigMetadataObject = {
}
for (const funbox of value) {
- if (!canSetFunboxWithConfig(funbox, currentConfig)) {
+ const check = canSetFunboxWithConfig(funbox, currentConfig);
+ if (!check.ok) {
showNoticeNotification(
- `${value}" cannot be enabled with the current config`,
+ `"${funbox}" cannot be enabled with the current config`,
);
return true;
}
diff --git a/frontend/src/ts/config/setters.ts b/frontend/src/ts/config/setters.ts
index 84fb44e08dd2..e409c043eabb 100644
--- a/frontend/src/ts/config/setters.ts
+++ b/frontend/src/ts/config/setters.ts
@@ -8,9 +8,10 @@ import { showNoticeNotification } from "../states/notifications";
import {
canSetConfigWithCurrentFunboxes,
canSetFunboxWithConfig,
-} from "../test/funbox/funbox-validation";
+} from "./funbox-validation";
import * as TestState from "../test/test-state";
-import { typedKeys, triggerResize } from "../utils/misc";
+import { typedKeys, triggerResize, escapeHTML } from "../utils/misc";
+import { camelCaseToWords, capitalizeFirstLetter } from "../utils/strings";
import { Config, setConfigStore } from "./store";
import { FunboxName } from "@monkeytype/schemas/configs";
@@ -77,6 +78,16 @@ export function setConfig(
}
if (!canSetConfigWithCurrentFunboxes(key, value, Config.funbox)) {
+ if (key === "words" || key === "time") {
+ showNoticeNotification("Active funboxes do not support infinite tests");
+ } else {
+ showNoticeNotification(
+ `You can't set ${camelCaseToWords(
+ key,
+ )} to ${String(value)} with currently active funboxes.`,
+ { durationMs: 5000 },
+ );
+ }
console.warn(
`Could not set config key "${key}" with value "${JSON.stringify(
value,
@@ -152,7 +163,18 @@ export function toggleFunbox(funbox: FunboxName, nosave?: boolean): boolean {
return false;
}
- if (!canSetFunboxWithConfig(funbox, Config)) {
+ const funboxCheck = canSetFunboxWithConfig(funbox, Config);
+ if (!funboxCheck.ok) {
+ const errorStrings = funboxCheck.errors.map(
+ (e) =>
+ `${capitalizeFirstLetter(
+ camelCaseToWords(e.key),
+ )} cannot be set to ${String(e.value)}.`,
+ );
+ showNoticeNotification(
+ `You can't enable ${escapeHTML(funbox.replace(/_/g, " "))}:
${errorStrings.map((s) => escapeHTML(s)).join("
")}`,
+ { durationMs: 5000, useInnerHtml: true },
+ );
return false;
}
diff --git a/frontend/src/ts/test/funbox/funbox-validation.tsx b/frontend/src/ts/test/funbox/funbox-validation.tsx
deleted file mode 100644
index 4875b2874238..000000000000
--- a/frontend/src/ts/test/funbox/funbox-validation.tsx
+++ /dev/null
@@ -1,198 +0,0 @@
-import { FunboxMetadata, getFunbox } from "@monkeytype/funbox";
-import { Config, ConfigValue, FunboxName } from "@monkeytype/schemas/configs";
-import { intersect } from "@monkeytype/util/arrays";
-
-import { showNoticeNotification } from "../../states/notifications";
-import { escapeHTML } from "../../utils/misc";
-import * as Strings from "../../utils/strings";
-
-export function checkForcedConfig(
- key: string,
- value: ConfigValue,
- funboxes: FunboxMetadata[],
-): {
- result: boolean;
- forcedConfigs?: ConfigValue[];
-} {
- if (funboxes.length === 0) {
- return { result: true };
- }
-
- if (key === "words" || key === "time") {
- if (value === 0) {
- const fb = funboxes.filter((f) =>
- f.properties?.includes("noInfiniteDuration"),
- );
- if (fb.length > 0) {
- return {
- result: false,
- forcedConfigs: [key === "words" ? 10 : 15],
- };
- } else {
- return { result: true };
- }
- } else {
- return { result: true };
- }
- } else {
- const forcedConfigs: Record = {};
- // collect all forced configs
- for (const fb of funboxes) {
- if (fb.frontendForcedConfig) {
- //push keys to forcedConfigs, if they don't exist. if they do, intersect the values
- for (const key in fb.frontendForcedConfig) {
- if (forcedConfigs[key] === undefined) {
- forcedConfigs[key] = fb.frontendForcedConfig[key] as ConfigValue[];
- } else {
- forcedConfigs[key] = intersect(
- forcedConfigs[key],
- fb.frontendForcedConfig[key] as ConfigValue[],
- true,
- );
- }
- }
- }
- }
-
- //check if the key is in forcedConfigs, if it is check the value, if its not, return true
- if (forcedConfigs[key] === undefined) {
- return { result: true };
- } else {
- if (forcedConfigs[key]?.length === 0) {
- throw new Error("No intersection of forced configs");
- }
- return {
- result: (forcedConfigs[key] ?? []).includes(value),
- forcedConfigs: forcedConfigs[key],
- };
- }
- }
-}
-
-// function: canSetConfigWithCurrentFunboxes
-// checks using checkFunboxForcedConfigs. if it returns true, return true
-// if it returns false, show a notification and return false
-export function canSetConfigWithCurrentFunboxes(
- key: string,
- value: ConfigValue,
- funbox: FunboxName[] = [],
- noNotification = false,
-): boolean {
- let errorCount = 0;
- const funboxes = getFunbox(funbox);
- if (key === "mode") {
- let fb = getFunbox(funbox).filter(
- (f) =>
- f.frontendForcedConfig?.["mode"] !== undefined &&
- !(f.frontendForcedConfig["mode"] as ConfigValue[]).includes(value),
- );
- if (value === "zen") {
- fb = fb.concat(
- funboxes.filter((f) => {
- const funcs = f.frontendFunctions ?? [];
- const props = f.properties ?? [];
- return (
- funcs.includes("getWord") ||
- funcs.includes("pullSection") ||
- funcs.includes("alterText") ||
- funcs.includes("withWords") ||
- props.includes("changesCapitalisation") ||
- props.includes("nospace") ||
- props.some((fp) => fp.startsWith("toPush:")) ||
- props.includes("changesWordsVisibility") ||
- props.includes("speaks") ||
- props.includes("changesLayout") ||
- props.includes("changesWordsFrequency")
- );
- }),
- );
- }
- if (value === "quote" || value === "custom") {
- fb = fb.concat(
- funboxes.filter((f) => {
- const funcs = f.frontendFunctions ?? [];
- const props = f.properties ?? [];
- return (
- funcs.includes("getWord") ||
- funcs.includes("pullSection") ||
- funcs.includes("withWords") ||
- props.includes("changesWordsFrequency")
- );
- }),
- );
- }
-
- if (fb.length > 0) {
- errorCount += 1;
- }
- }
- if (key === "words" || key === "time") {
- if (!checkForcedConfig(key, value, funboxes).result) {
- if (!noNotification) {
- showNoticeNotification("Active funboxes do not support infinite tests");
- return false;
- } else {
- errorCount += 1;
- }
- }
- } else if (!checkForcedConfig(key, value, funboxes).result) {
- errorCount += 1;
- }
-
- if (errorCount > 0) {
- if (!noNotification) {
- showNoticeNotification(
- `You can't set ${Strings.camelCaseToWords(
- key,
- )} to ${value.toString()} with currently active funboxes.`,
- {
- durationMs: 5000,
- },
- );
- }
- return false;
- } else {
- return true;
- }
-}
-
-export function canSetFunboxWithConfig(
- funbox: FunboxName,
- config: Config,
-): boolean {
- let funboxToCheck = [...config.funbox, funbox];
-
- const errors = [];
- for (const [configKey, configValue] of Object.entries(config)) {
- if (
- !canSetConfigWithCurrentFunboxes(
- configKey,
- configValue,
- funboxToCheck,
- true,
- )
- ) {
- errors.push({
- key: configKey,
- value: configValue,
- });
- }
- }
- if (errors.length > 0) {
- const errorStrings = [];
- for (const error of errors) {
- errorStrings.push(
- `${Strings.capitalizeFirstLetter(
- Strings.camelCaseToWords(error.key),
- )} cannot be set to ${error.value.toString()}.`,
- );
- }
- showNoticeNotification(
- `You can't enable ${escapeHTML(funbox.replace(/_/g, " "))}:
${errorStrings.map((s) => escapeHTML(s)).join("
")}`,
- { durationMs: 5000, useInnerHtml: true },
- );
- return false;
- } else {
- return true;
- }
-}
diff --git a/frontend/src/ts/test/funbox/funbox.ts b/frontend/src/ts/test/funbox/funbox.ts
index 716723ea3449..7d4d846fbcdf 100644
--- a/frontend/src/ts/test/funbox/funbox.ts
+++ b/frontend/src/ts/test/funbox/funbox.ts
@@ -13,7 +13,7 @@ import * as MemoryTimer from "./memory-funbox-timer";
import * as FunboxMemory from "./funbox-memory";
import { HighlightMode, FunboxName } from "@monkeytype/schemas/configs";
import { Mode } from "@monkeytype/schemas/shared";
-import { checkCompatibility } from "@monkeytype/funbox";
+import { checkCompatibility, checkForcedConfig } from "@monkeytype/funbox";
import {
getAllFunboxes,
getActiveFunboxes,
@@ -22,7 +22,6 @@ import {
isFunboxActiveWithProperty,
getActiveFunboxesWithProperty,
} from "./list";
-import { checkForcedConfig } from "./funbox-validation";
import { tryCatch } from "@monkeytype/util/trycatch";
import { qs, qsa } from "../../utils/dom";
import * as ConfigEvent from "../../observables/config-event";
diff --git a/packages/funbox/src/index.ts b/packages/funbox/src/index.ts
index 2d937562b4a7..25b87ccd82da 100644
--- a/packages/funbox/src/index.ts
+++ b/packages/funbox/src/index.ts
@@ -1,10 +1,10 @@
import { FunboxName } from "@monkeytype/schemas/configs";
import { getList, getFunbox, getObject, getFunboxNames } from "./list";
import { FunboxMetadata, FunboxProperty } from "./types";
-import { checkCompatibility } from "./validation";
+import { checkCompatibility, checkForcedConfig } from "./validation";
export type { FunboxMetadata, FunboxProperty };
-export { checkCompatibility, getFunbox, getFunboxNames };
+export { checkCompatibility, checkForcedConfig, getFunbox, getFunboxNames };
export function getAllFunboxes(): FunboxMetadata[] {
return getList();
diff --git a/packages/funbox/src/validation.ts b/packages/funbox/src/validation.ts
index b7c7c98ea16a..0ad0335d372b 100644
--- a/packages/funbox/src/validation.ts
+++ b/packages/funbox/src/validation.ts
@@ -1,9 +1,74 @@
import { intersect } from "@monkeytype/util/arrays";
import { FunboxForcedConfig, FunboxMetadata } from "./types";
import { getFunbox } from "./list";
-import { FunboxName } from "@monkeytype/schemas/configs";
+import { ConfigValue, FunboxName } from "@monkeytype/schemas/configs";
import { safeNumber } from "@monkeytype/util/numbers";
+export function checkForcedConfig(
+ key: string,
+ value: ConfigValue,
+ funboxes: FunboxMetadata[],
+): {
+ result: boolean;
+ forcedConfigs?: ConfigValue[];
+} {
+ if (funboxes.length === 0) {
+ return { result: true };
+ }
+
+ if (key === "words" || key === "time") {
+ if (value === 0) {
+ const fb = funboxes.filter((f) =>
+ f.properties?.includes("noInfiniteDuration"),
+ );
+ if (fb.length > 0) {
+ return {
+ result: false,
+ forcedConfigs: [key === "words" ? 10 : 15],
+ };
+ } else {
+ return { result: true };
+ }
+ } else {
+ return { result: true };
+ }
+ } else {
+ const forcedConfigs: Record = {};
+ // collect all forced configs
+ for (const fb of funboxes) {
+ if (fb.frontendForcedConfig) {
+ //push keys to forcedConfigs, if they don't exist. if they do, intersect the values
+ for (const forcedKey in fb.frontendForcedConfig) {
+ if (forcedConfigs[forcedKey] === undefined) {
+ forcedConfigs[forcedKey] = fb.frontendForcedConfig[
+ forcedKey
+ ] as ConfigValue[];
+ } else {
+ forcedConfigs[forcedKey] = intersect(
+ forcedConfigs[forcedKey],
+ fb.frontendForcedConfig[forcedKey] as ConfigValue[],
+ true,
+ );
+ }
+ }
+ }
+ }
+
+ //check if the key is in forcedConfigs, if it is check the value, if its not, return true
+ if (forcedConfigs[key] === undefined) {
+ return { result: true };
+ } else {
+ if (forcedConfigs[key]?.length === 0) {
+ throw new Error("No intersection of forced configs");
+ }
+ return {
+ result: (forcedConfigs[key] ?? []).includes(value),
+ forcedConfigs: forcedConfigs[key],
+ };
+ }
+ }
+}
+
export function checkCompatibility(
funboxNames: FunboxName[],
withFunbox?: FunboxName,