Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions frontend/__tests__/controllers/preset-controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,14 @@ describe("PresetController", () => {

//THEN
expect(tagsClearMock).toHaveBeenCalled();
expect(tagsSetMock).toHaveBeenNthCalledWith(1, "tagOne", true, false);
expect(tagsSetMock).toHaveBeenNthCalledWith(2, "tagTwo", true, false);
expect(tagsSetMock).toHaveBeenNthCalledWith(1, {
tagId: "tagOne",
active: true,
});
expect(tagsSetMock).toHaveBeenNthCalledWith(2, {
tagId: "tagTwo",
active: true,
});
expect(tagsSaveActiveMock).toHaveBeenCalled();
});

Expand Down Expand Up @@ -141,8 +147,14 @@ describe("PresetController", () => {

//THEN
expect(tagsClearMock).toHaveBeenCalled();
expect(tagsSetMock).toHaveBeenNthCalledWith(1, "tagOne", true, false);
expect(tagsSetMock).toHaveBeenNthCalledWith(2, "tagTwo", true, false);
expect(tagsSetMock).toHaveBeenNthCalledWith(1, {
tagId: "tagOne",
active: true,
});
expect(tagsSetMock).toHaveBeenNthCalledWith(2, {
tagId: "tagTwo",
active: true,
});
expect(tagsSaveActiveMock).toHaveBeenCalled();
});

Expand Down
12 changes: 6 additions & 6 deletions frontend/src/ts/collections/results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { baseKey } from "../queries/utils/keys";
import { isAuthenticated } from "../states/core";
import { getLastResult, setLastResult } from "../states/snapshot";
import {
getActiveTagsOnce,
reconcileLocalTagPB,
saveLocalTagPB,
__nonReactive as tagsNonReactive,
Expand Down Expand Up @@ -586,13 +587,13 @@ export async function getUserAverage10(
//exit early if there is no user. Don't init the result collection
if (!isAuthenticated()) return { wpm: 0, acc: 0 };

const tagIds = (await getActiveTagsOnce()).map((it) => it._id);

const result = await queryOnce((q) =>
q
.from({
//we use sub-query to filter first and then aggregate
last10: buildSettingsResultsQuery(options, {
tagIds: tagsNonReactive.getActiveTags().map((it) => it._id),
})
last10: buildSettingsResultsQuery(options, { tagIds })
.orderBy(({ r }) => r.timestamp, "desc")
.limit(10),
})
Expand All @@ -609,11 +610,10 @@ export async function getUserDailyBest(
): Promise<{ wpm: number; acc: number }> {
//exit early if there is no user. Don't init the result collection
if (!isAuthenticated()) return { wpm: 0, acc: 0 };
const tagIds = (await getActiveTagsOnce()).map((it) => it._id);

const result = await queryOnce(() =>
buildSettingsResultsQuery(options, {
tagIds: tagsNonReactive.getActiveTags().map((it) => it._id),
})
buildSettingsResultsQuery(options, { tagIds })
.where(({ r }) => gte(r.timestamp, Date.now() - 24 * 60 * 60 * 1000))
.orderBy(({ r }) => r.wpm, "desc")
.limit(1)
Expand Down
122 changes: 92 additions & 30 deletions frontend/src/ts/collections/tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { queryCollectionOptions } from "@tanstack/query-db-collection";
import {
createCollection,
createOptimisticAction,
eq,
queryOnce,
useLiveQuery,
} from "@tanstack/solid-db";
import { z } from "zod";
Expand All @@ -20,8 +22,9 @@ import {
} from "@monkeytype/schemas/shared";
import { Difficulty } from "@monkeytype/schemas/configs";
import { Language } from "@monkeytype/schemas/languages";
import { applyIdWorkaround, tempId } from "./utils/misc";
import { applyIdWorkaround, isTempId, tempId } from "./utils/misc";
import { fetchUserFromApi } from "../ape/user";
import { updateTagsInFilterStorage } from "../states/result-filters";

export type TagItem = UserTag & { active: boolean };

Expand Down Expand Up @@ -60,6 +63,16 @@ export function useTagsLiveQuery() {
});
}

// oxlint-disable-next-line typescript/explicit-function-return-type
export async function getActiveTagsOnce() {
return queryOnce((q) => {
return q
.from({ tag: tagsCollection })
.where(({ tag }) => eq(tag.active, true))
.orderBy(({ tag }) => tag.name, "asc");
});
}

type ActionType = {
insertTag: {
name: string;
Expand All @@ -74,6 +87,18 @@ type ActionType = {
deleteTag: {
tagId: string;
};
toggleTagActive: {
tagId: string;
noSave?: boolean;
};
setTagActive: {
tagId: string;
active: boolean;
noSave?: boolean;
};
clearActiveTags: {
noSave?: boolean;
};
};

const actions = {
Expand All @@ -100,6 +125,11 @@ const actions = {
};

tagsCollection.utils.writeInsert(newTag);
updateTagsInFilterStorage(
[...tagsCollection.values()]
.filter((it) => !isTempId(it._id))
.map((it) => it._id),
);
},
}),
updateTagName: createOptimisticAction<ActionType["updateTagName"]>({
Expand Down Expand Up @@ -166,6 +196,46 @@ const actions = {
throw new Error(`Failed to delete tag: ${response.body.message}`);
}
tagsCollection.utils.writeDelete(tagId);
updateTagsInFilterStorage(
[...tagsCollection.values()].map((it) => it._id),
);
},
}),
toggleTagActive: createOptimisticAction<ActionType["toggleTagActive"]>({
onMutate: ({ tagId, noSave }) => {
const tag = tagsCollection.get(tagId);
if (tag === undefined) return;
tagsCollection.utils.writeUpdate({ ...tag, active: !tag.active });
if (!noSave) saveActiveToLocalStorage();
},
mutationFn: async () => {
return;
},
}),
setTagActive: createOptimisticAction<ActionType["setTagActive"]>({
onMutate: ({ tagId, active, noSave }) => {
const tag = tagsCollection.get(tagId);
if (tag === undefined) return;
tagsCollection.utils.writeUpdate({ ...tag, active });
if (!noSave) saveActiveToLocalStorage();
},
mutationFn: async () => {
return;
},
}),
clearActiveTags: createOptimisticAction<ActionType["clearActiveTags"]>({
onMutate: ({ noSave }) => {
tagsCollection.utils.writeBatch(() => {
tagsCollection.forEach((tag) => {
if (tag.active) {
tagsCollection.utils.writeUpdate({ ...tag, active: false });
}
});
});
if (!noSave) saveActiveToLocalStorage();
},
mutationFn: async () => {
return;
},
}),
};
Expand Down Expand Up @@ -200,6 +270,27 @@ export async function deleteTag(
await transaction.isPersisted.promise;
}

export async function toggleTagActive(
params: ActionType["toggleTagActive"],
): Promise<void> {
const transaction = actions.toggleTagActive(params);
await transaction.isPersisted.promise;
}

export async function setTagActive(
params: ActionType["setTagActive"],
): Promise<void> {
const transaction = actions.setTagActive(params);
await transaction.isPersisted.promise;
}

export async function clearActiveTags(
params: ActionType["clearActiveTags"] = {},
): Promise<void> {
const transaction = actions.clearActiveTags(params);
await transaction.isPersisted.promise;
}

function getTags(): TagItem[] {
return [...tagsCollection.values()].sort((a, b) =>
a.name.localeCompare(b.name),
Expand Down Expand Up @@ -230,35 +321,6 @@ export function saveActiveToLocalStorage(): void {
activeTagsLS.set(activeIds);
}

export function toggleTagActive(tagId: string, nosave = false): void {
const tag = tagsCollection.get(tagId);
if (tag === undefined) return;
tagsCollection.utils.writeUpdate({ ...tag, active: !tag.active });
if (!nosave) saveActiveToLocalStorage();
}

export function setTagActive(
tagId: string,
state: boolean,
nosave = false,
): void {
const tag = tagsCollection.get(tagId);
if (tag === undefined) return;
tagsCollection.utils.writeUpdate({ ...tag, active: state });
if (!nosave) saveActiveToLocalStorage();
}

export function clearActiveTags(nosave = false): void {
tagsCollection.utils.writeBatch(() => {
tagsCollection.forEach((tag) => {
if (tag.active) {
tagsCollection.utils.writeUpdate({ ...tag, active: false });
}
});
});
if (!nosave) saveActiveToLocalStorage();
}

// --- Personal bests ---

export function getLocalTagPB<M extends Mode>(
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/ts/collections/utils/misc.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
const tempIdPrefix = "temp_";

export function tempId(): string {
return `temp_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
return `${tempIdPrefix}${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
}

export function isTempId(id: string): boolean {
return id.startsWith(tempIdPrefix);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/ts/commandline/lists/tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function update(): void {
icon: "fa-times",
sticky: true,
exec: async (): Promise<void> => {
clearActiveTags();
await clearActiveTags();
if (
Config.paceCaret === "average" ||
Config.paceCaret === "tagPb" ||
Expand All @@ -62,7 +62,7 @@ function update(): void {
return __nonReactive.getTag(tag._id)?.active ?? false;
},
exec: async (): Promise<void> => {
toggleTagActive(tag._id);
await toggleTagActive({ tagId: tag._id });

if (
Config.paceCaret === "average" ||
Expand Down
32 changes: 20 additions & 12 deletions frontend/src/ts/components/pages/account/Filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import defaultResultFilters from "../../../constants/default-result-filters";
import { SimpleModal } from "../../../elements/simple-modal";
import { FaSolidIcon } from "../../../types/font-awesome";
import { IsValidResponse } from "../../../types/validation";
import { cn } from "../../../utils/cn";
import { createErrorMessage } from "../../../utils/error";
import {
getLanguageDisplayString,
Expand Down Expand Up @@ -141,6 +142,7 @@ export function Filters(props: {
text: string;
group: T;
format?: (value: K) => string;
class?: string;
}): JSXElement => {
// Isolate this group's data to prevent unnecessary updates
const groupData = createMemo(() => props.filters[options.group]);
Expand All @@ -159,7 +161,7 @@ export function Filters(props: {
);

return (
<div>
<div class={cn(`w-full`, options.class)}>
<H3 fa={{ icon: options.icon, fixedWidth: true }} text={options.text} />
<SlimSelect
multiple
Expand Down Expand Up @@ -320,7 +322,7 @@ export function Filters(props: {
onClick={() => props.onChangeFilters(noFilters())}
class="mb-4 w-full"
/>
<div class="gap-4 md:grid md:grid-cols-2 [&>div]:last:col-span-2">
<div class="gap-4 md:grid md:grid-cols-2">
<ButtonGroup text="difficulty" icon="fa-star" group="difficulty" />
<ButtonGroup text="personal best" icon="fa-crown" group="pb" />
<ButtonGroup text="mode" icon="fa-bars" group="mode" />
Expand All @@ -334,20 +336,25 @@ export function Filters(props: {
<ButtonGroup text="punctuation" icon="fa-at" group="punctuation" />
<ButtonGroup text="numbers" icon="fa-hashtag" group="numbers" />

<Dropdown
icon="fa-tag"
text="tags"
group="tags"
format={(tag) =>
tag === "none"
? "no tag"
: (tags().find((it) => it._id === tag)?.name ?? tag)
}
/>
<Show when={tags().length > 0}>
<Dropdown
icon="fa-tag"
text="tags"
group="tags"
format={(tag) =>
tag === "none"
? "no tag"
: (tags().find((it) => it._id === tag)?.name ?? tag)
}
/>
</Show>
<Dropdown
icon="fa-gamepad"
text="funbox"
group="funbox"
class={cn("", {
"col-span-2": tags().length === 0,
})}
format={(val) =>
val === "none" ? "no funbox" : replaceUnderscoresWithSpaces(val)
}
Expand All @@ -356,6 +363,7 @@ export function Filters(props: {
icon="fa-globe-americas"
text="language"
group="language"
class="col-span-2"
format={getLanguageDisplayString}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function Tags(): JSXElement {
text={tag.name}
active={tag.active}
onClick={() => {
toggleTagActive(tag._id);
void toggleTagActive({ tagId: tag._id });
}}
/>
<Button
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/ts/controllers/preset-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ export async function apply(_id: string): Promise<void> {
!isPartialPreset(presetToApply) ||
presetToApply.settingGroups?.includes("behavior")
) {
clearActiveTags(true);
await clearActiveTags({ noSave: true });
if (presetToApply.config.tags) {
for (const tagId of presetToApply.config.tags) {
setTagActive(tagId, true, false);
await setTagActive({ tagId, active: true });
}
saveActiveToLocalStorage();
}
Expand Down
Loading