Skip to content
4 changes: 2 additions & 2 deletions frontend/src/ts/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ export async function initSnapshot(): Promise<Snapshot | false> {
snap.tags =
userData.tags?.map((tag) => ({
...tag,
display: tag.name.replaceAll("_", " "),
display: tag.name.replace(/_/g, " "),
})) ?? [];

snap.tags = snap.tags?.sort((a, b) => {
Expand All @@ -234,7 +234,7 @@ export async function initSnapshot(): Promise<Snapshot | false> {
const presetsWithDisplay = presetsData.map((preset) => {
return {
...preset,
display: preset.name.replace(/_/gi, " "),
display: preset.name.replace(/_/g, " "),
};
}) as SnapshotPreset[];
snap.presets = presetsWithDisplay;
Expand Down
1 change: 0 additions & 1 deletion frontend/src/ts/elements/account/result-filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,6 @@ function addFilterPresetToSnapshot(filter: ResultFilters): void {
export async function createFilterPreset(
name: string,
): Promise<number | undefined> {
name = name.replace(/ /g, "_");
showLoaderBar();
const result = await Ape.users.addResultFilterPreset({
body: { ...filters, name },
Expand Down
42 changes: 25 additions & 17 deletions frontend/src/ts/modals/edit-preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { ValidatedHtmlInputElement } from "../elements/input-validation";
import { ElementWithUtils } from "../utils/dom";
import { configMetadata } from "../config/metadata";
import { getConfigChanges as getConfigChangesFromConfig } from "../config/utils";
import { normalizeName } from "../utils/strings";

const state = {
presetType: "full" as PresetType,
Expand All @@ -46,7 +47,12 @@ export function show(action: string, id?: string, name?: string): void {
presetNameEl ??= new ValidatedHtmlInputElement(
modalEl.qsr("input[type=text]"),
{
schema: PresetNameSchema,
isValid: async (name) => {
const parsed = PresetNameSchema.safeParse(normalizeName(name));
if (parsed.success) return true;
return parsed.error.errors.map((err) => err.message).join(", ");
},
debounceDelay: 0,
},
);
if (action === "add") {
Expand All @@ -66,7 +72,7 @@ export function show(action: string, id?: string, name?: string): void {
modalEl.setAttribute("data-preset-id", id);
modalEl.qsr(".popupTitle").setHtml("Edit preset");
modalEl.qsr(".submit").setHtml(`save`);
presetNameEl?.setValue(name.replaceAll(" ", "_"));
presetNameEl?.setValue(name);
presetNameEl?.getParent()?.show();

modalEl.qsa("input").show();
Expand Down Expand Up @@ -221,7 +227,7 @@ function hide(): void {
async function apply(): Promise<void> {
const modalEl = modal.getModal();
const action = modalEl.getAttribute("data-action");
const presetName = modalEl
const propPresetName = modalEl
.qsr<HTMLInputElement>(".group input[title='presets']")
.getValue() as string;
const presetId = modalEl.getAttribute("data-preset-id") as string;
Expand All @@ -248,22 +254,24 @@ async function apply(): Promise<void> {
}

const addOrEditAction = action === "add" || action === "edit";
if (addOrEditAction) {
//validate the preset name only in add or edit mode

const noPresetName: boolean =
presetName.replace(/^_+|_+$/g, "").length === 0; //all whitespace names are rejected
if (noPresetName) {
showNoticeNotification("Preset name cannot be empty");
return;
}
if (addOrEditAction && propPresetName.trim().length === 0) {
showNoticeNotification("Preset name cannot be empty");
return;
}

if (presetNameEl?.getValidationResult().status === "failed") {
showNoticeNotification("Preset name is not valid");
return;
}
const cleanedPresetName = normalizeName(propPresetName);
const parsedPresetName = addOrEditAction
? PresetNameSchema.safeParse(cleanedPresetName)
: null;

if (parsedPresetName && !parsedPresetName.success) {
showNoticeNotification("Preset name is not valid");
return;
}

const presetName = parsedPresetName?.data ?? "";

hide();

showLoaderBar();
Expand Down Expand Up @@ -291,7 +299,7 @@ async function apply(): Promise<void> {
...(state.presetType === "partial" && {
settingGroups: activeSettingGroups,
}),
display: presetName.replaceAll("_", " "),
display: presetName.replace(/_/g, " "),
_id: response.body.data.presetId,
} as SnapshotPreset);
}
Expand Down Expand Up @@ -323,7 +331,7 @@ async function apply(): Promise<void> {
showSuccessNotification("Preset updated");

preset.name = presetName;
preset.display = presetName.replaceAll("_", " ");
preset.display = presetName.replace(/_/g, " ");
if (updateConfig) {
preset.config = configChanges;
if (state.presetType === "partial") {
Expand Down
14 changes: 7 additions & 7 deletions frontend/src/ts/modals/edit-tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import { SimpleModal, TextInput } from "../elements/simple-modal";
import { TagNameSchema } from "@monkeytype/schemas/users";
import { SnapshotUserTag } from "../constants/default-snapshot";
import { IsValidResponse } from "../types/validation";
import { normalizeName } from "../utils/strings";

function getTagFromSnapshot(tagId: string): SnapshotUserTag | undefined {
return DB.getSnapshot()?.tags.find((tag) => tag._id === tagId);
}

const cleanTagName = (tagName: string): string => tagName.replaceAll(" ", "_");
const tagNameValidation = async (tagName: string): Promise<IsValidResponse> => {
const validationResult = TagNameSchema.safeParse(cleanTagName(tagName));
const validationResult = TagNameSchema.safeParse(normalizeName(tagName));
if (validationResult.success) return true;
return validationResult.error.errors.map((err) => err.message).join(", ");
};
Expand All @@ -32,7 +32,7 @@ const actionModals: Record<Action, SimpleModal> = {
],
buttonText: "add",
execFn: async (_thisPopup, propTagName) => {
const tagName = cleanTagName(propTagName);
const tagName = TagNameSchema.parse(normalizeName(propTagName));
const response = await Ape.users.createTag({ body: { tagName } });

if (response.status !== 200) {
Expand All @@ -46,8 +46,8 @@ const actionModals: Record<Action, SimpleModal> = {
}

DB.getSnapshot()?.tags?.push({
display: propTagName,
name: response.body.data.name,
display: tagName.replace(/_/g, " "),
name: tagName,
_id: response.body.data._id,
personalBests: {
time: {},
Expand Down Expand Up @@ -77,7 +77,7 @@ const actionModals: Record<Action, SimpleModal> = {
(_thisPopup.inputs[0] as TextInput).initVal = _thisPopup.parameters[0];
},
execFn: async (_thisPopup, propTagName) => {
const tagName = cleanTagName(propTagName);
const tagName = TagNameSchema.parse(normalizeName(propTagName));
const tagId = _thisPopup.parameters[1] as string;

const response = await Ape.users.editTag({
Expand All @@ -96,7 +96,7 @@ const actionModals: Record<Action, SimpleModal> = {

if (matchingTag !== undefined) {
matchingTag.name = tagName;
matchingTag.display = propTagName;
matchingTag.display = tagName.replace(/_/g, " ");
}

void Settings.update();
Expand Down
15 changes: 13 additions & 2 deletions frontend/src/ts/modals/new-filter-preset.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ResultFiltersSchema } from "@monkeytype/schemas/users";
import { createFilterPreset } from "../elements/account/result-filters";
import { SimpleModal } from "../elements/simple-modal";
import { normalizeName } from "../utils/strings";

export function show(): void {
newFilterPresetModal.show(undefined, {});
Expand All @@ -15,13 +16,23 @@ const newFilterPresetModal = new SimpleModal({
type: "text",
initVal: "",
validation: {
schema: ResultFiltersSchema.shape.name,
isValid: async (name) => {
const parsed = ResultFiltersSchema.shape.name.safeParse(
normalizeName(name),
);
if (parsed.success) return true;
return parsed.error.errors.map((err) => err.message).join(", ");
},
debounceDelay: 0,
},
},
],
buttonText: "add",
execFn: async (_thisPopup, name) => {
const status = await createFilterPreset(name);
const cleanedName = ResultFiltersSchema.shape.name.parse(
normalizeName(name),
);
const status = await createFilterPreset(cleanedName);

if (status === 1) {
return { status: "success", message: "Filter preset created" };
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/ts/utils/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ export function capitalizeFirstLetter(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}

/**
* Normalizes free-form names to canonical storage format.
* Trims edge whitespace and collapses all inner whitespace runs to underscores.
*/
export function normalizeName(name: string): string {
return name.trim().replace(/\s+/g, "_");
}

/**
* @param text String to split
* @param delimiters Single character delimiters.
Expand Down
Loading