Skip to content

Commit d64147e

Browse files
fehmerMiodec
andauthored
fix(account): newly created tags not appearing in filters (@fehmer) (#7968)
Co-authored-by: Miodec <jack@monkeytype.com>
1 parent 83b30b5 commit d64147e

8 files changed

Lines changed: 146 additions & 58 deletions

File tree

frontend/__tests__/controllers/preset-controller.spec.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,14 @@ describe("PresetController", () => {
8787

8888
//THEN
8989
expect(tagsClearMock).toHaveBeenCalled();
90-
expect(tagsSetMock).toHaveBeenNthCalledWith(1, "tagOne", true, false);
91-
expect(tagsSetMock).toHaveBeenNthCalledWith(2, "tagTwo", true, false);
90+
expect(tagsSetMock).toHaveBeenNthCalledWith(1, {
91+
tagId: "tagOne",
92+
active: true,
93+
});
94+
expect(tagsSetMock).toHaveBeenNthCalledWith(2, {
95+
tagId: "tagTwo",
96+
active: true,
97+
});
9298
expect(tagsSaveActiveMock).toHaveBeenCalled();
9399
});
94100

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

142148
//THEN
143149
expect(tagsClearMock).toHaveBeenCalled();
144-
expect(tagsSetMock).toHaveBeenNthCalledWith(1, "tagOne", true, false);
145-
expect(tagsSetMock).toHaveBeenNthCalledWith(2, "tagTwo", true, false);
150+
expect(tagsSetMock).toHaveBeenNthCalledWith(1, {
151+
tagId: "tagOne",
152+
active: true,
153+
});
154+
expect(tagsSetMock).toHaveBeenNthCalledWith(2, {
155+
tagId: "tagTwo",
156+
active: true,
157+
});
146158
expect(tagsSaveActiveMock).toHaveBeenCalled();
147159
});
148160

frontend/src/ts/collections/results.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { baseKey } from "../queries/utils/keys";
3030
import { isAuthenticated } from "../states/core";
3131
import { getLastResult, setLastResult } from "../states/snapshot";
3232
import {
33+
getActiveTagsOnce,
3334
reconcileLocalTagPB,
3435
saveLocalTagPB,
3536
__nonReactive as tagsNonReactive,
@@ -586,13 +587,13 @@ export async function getUserAverage10(
586587
//exit early if there is no user. Don't init the result collection
587588
if (!isAuthenticated()) return { wpm: 0, acc: 0 };
588589

590+
const tagIds = (await getActiveTagsOnce()).map((it) => it._id);
591+
589592
const result = await queryOnce((q) =>
590593
q
591594
.from({
592595
//we use sub-query to filter first and then aggregate
593-
last10: buildSettingsResultsQuery(options, {
594-
tagIds: tagsNonReactive.getActiveTags().map((it) => it._id),
595-
})
596+
last10: buildSettingsResultsQuery(options, { tagIds })
596597
.orderBy(({ r }) => r.timestamp, "desc")
597598
.limit(10),
598599
})
@@ -609,11 +610,10 @@ export async function getUserDailyBest(
609610
): Promise<{ wpm: number; acc: number }> {
610611
//exit early if there is no user. Don't init the result collection
611612
if (!isAuthenticated()) return { wpm: 0, acc: 0 };
613+
const tagIds = (await getActiveTagsOnce()).map((it) => it._id);
612614

613615
const result = await queryOnce(() =>
614-
buildSettingsResultsQuery(options, {
615-
tagIds: tagsNonReactive.getActiveTags().map((it) => it._id),
616-
})
616+
buildSettingsResultsQuery(options, { tagIds })
617617
.where(({ r }) => gte(r.timestamp, Date.now() - 24 * 60 * 60 * 1000))
618618
.orderBy(({ r }) => r.wpm, "desc")
619619
.limit(1)

frontend/src/ts/collections/tags.ts

Lines changed: 92 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { queryCollectionOptions } from "@tanstack/query-db-collection";
33
import {
44
createCollection,
55
createOptimisticAction,
6+
eq,
7+
queryOnce,
68
useLiveQuery,
79
} from "@tanstack/solid-db";
810
import { z } from "zod";
@@ -20,8 +22,9 @@ import {
2022
} from "@monkeytype/schemas/shared";
2123
import { Difficulty } from "@monkeytype/schemas/configs";
2224
import { Language } from "@monkeytype/schemas/languages";
23-
import { applyIdWorkaround, tempId } from "./utils/misc";
25+
import { applyIdWorkaround, isTempId, tempId } from "./utils/misc";
2426
import { fetchUserFromApi } from "../ape/user";
27+
import { updateTagsInFilterStorage } from "../states/result-filters";
2528

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

@@ -60,6 +63,16 @@ export function useTagsLiveQuery() {
6063
});
6164
}
6265

66+
// oxlint-disable-next-line typescript/explicit-function-return-type
67+
export async function getActiveTagsOnce() {
68+
return queryOnce((q) => {
69+
return q
70+
.from({ tag: tagsCollection })
71+
.where(({ tag }) => eq(tag.active, true))
72+
.orderBy(({ tag }) => tag.name, "asc");
73+
});
74+
}
75+
6376
type ActionType = {
6477
insertTag: {
6578
name: string;
@@ -74,6 +87,18 @@ type ActionType = {
7487
deleteTag: {
7588
tagId: string;
7689
};
90+
toggleTagActive: {
91+
tagId: string;
92+
noSave?: boolean;
93+
};
94+
setTagActive: {
95+
tagId: string;
96+
active: boolean;
97+
noSave?: boolean;
98+
};
99+
clearActiveTags: {
100+
noSave?: boolean;
101+
};
77102
};
78103

79104
const actions = {
@@ -100,6 +125,11 @@ const actions = {
100125
};
101126

102127
tagsCollection.utils.writeInsert(newTag);
128+
updateTagsInFilterStorage(
129+
[...tagsCollection.values()]
130+
.filter((it) => !isTempId(it._id))
131+
.map((it) => it._id),
132+
);
103133
},
104134
}),
105135
updateTagName: createOptimisticAction<ActionType["updateTagName"]>({
@@ -166,6 +196,46 @@ const actions = {
166196
throw new Error(`Failed to delete tag: ${response.body.message}`);
167197
}
168198
tagsCollection.utils.writeDelete(tagId);
199+
updateTagsInFilterStorage(
200+
[...tagsCollection.values()].map((it) => it._id),
201+
);
202+
},
203+
}),
204+
toggleTagActive: createOptimisticAction<ActionType["toggleTagActive"]>({
205+
onMutate: ({ tagId, noSave }) => {
206+
const tag = tagsCollection.get(tagId);
207+
if (tag === undefined) return;
208+
tagsCollection.utils.writeUpdate({ ...tag, active: !tag.active });
209+
if (!noSave) saveActiveToLocalStorage();
210+
},
211+
mutationFn: async () => {
212+
return;
213+
},
214+
}),
215+
setTagActive: createOptimisticAction<ActionType["setTagActive"]>({
216+
onMutate: ({ tagId, active, noSave }) => {
217+
const tag = tagsCollection.get(tagId);
218+
if (tag === undefined) return;
219+
tagsCollection.utils.writeUpdate({ ...tag, active });
220+
if (!noSave) saveActiveToLocalStorage();
221+
},
222+
mutationFn: async () => {
223+
return;
224+
},
225+
}),
226+
clearActiveTags: createOptimisticAction<ActionType["clearActiveTags"]>({
227+
onMutate: ({ noSave }) => {
228+
tagsCollection.utils.writeBatch(() => {
229+
tagsCollection.forEach((tag) => {
230+
if (tag.active) {
231+
tagsCollection.utils.writeUpdate({ ...tag, active: false });
232+
}
233+
});
234+
});
235+
if (!noSave) saveActiveToLocalStorage();
236+
},
237+
mutationFn: async () => {
238+
return;
169239
},
170240
}),
171241
};
@@ -200,6 +270,27 @@ export async function deleteTag(
200270
await transaction.isPersisted.promise;
201271
}
202272

273+
export async function toggleTagActive(
274+
params: ActionType["toggleTagActive"],
275+
): Promise<void> {
276+
const transaction = actions.toggleTagActive(params);
277+
await transaction.isPersisted.promise;
278+
}
279+
280+
export async function setTagActive(
281+
params: ActionType["setTagActive"],
282+
): Promise<void> {
283+
const transaction = actions.setTagActive(params);
284+
await transaction.isPersisted.promise;
285+
}
286+
287+
export async function clearActiveTags(
288+
params: ActionType["clearActiveTags"] = {},
289+
): Promise<void> {
290+
const transaction = actions.clearActiveTags(params);
291+
await transaction.isPersisted.promise;
292+
}
293+
203294
function getTags(): TagItem[] {
204295
return [...tagsCollection.values()].sort((a, b) =>
205296
a.name.localeCompare(b.name),
@@ -230,35 +321,6 @@ export function saveActiveToLocalStorage(): void {
230321
activeTagsLS.set(activeIds);
231322
}
232323

233-
export function toggleTagActive(tagId: string, nosave = false): void {
234-
const tag = tagsCollection.get(tagId);
235-
if (tag === undefined) return;
236-
tagsCollection.utils.writeUpdate({ ...tag, active: !tag.active });
237-
if (!nosave) saveActiveToLocalStorage();
238-
}
239-
240-
export function setTagActive(
241-
tagId: string,
242-
state: boolean,
243-
nosave = false,
244-
): void {
245-
const tag = tagsCollection.get(tagId);
246-
if (tag === undefined) return;
247-
tagsCollection.utils.writeUpdate({ ...tag, active: state });
248-
if (!nosave) saveActiveToLocalStorage();
249-
}
250-
251-
export function clearActiveTags(nosave = false): void {
252-
tagsCollection.utils.writeBatch(() => {
253-
tagsCollection.forEach((tag) => {
254-
if (tag.active) {
255-
tagsCollection.utils.writeUpdate({ ...tag, active: false });
256-
}
257-
});
258-
});
259-
if (!nosave) saveActiveToLocalStorage();
260-
}
261-
262324
// --- Personal bests ---
263325

264326
export function getLocalTagPB<M extends Mode>(

frontend/src/ts/collections/utils/misc.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
const tempIdPrefix = "temp_";
2+
13
export function tempId(): string {
2-
return `temp_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
4+
return `${tempIdPrefix}${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`;
5+
}
6+
7+
export function isTempId(id: string): boolean {
8+
return id.startsWith(tempIdPrefix);
39
}
410

511
/**

frontend/src/ts/commandline/lists/tags.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function update(): void {
4141
icon: "fa-times",
4242
sticky: true,
4343
exec: async (): Promise<void> => {
44-
clearActiveTags();
44+
await clearActiveTags();
4545
if (
4646
Config.paceCaret === "average" ||
4747
Config.paceCaret === "tagPb" ||
@@ -62,7 +62,7 @@ function update(): void {
6262
return __nonReactive.getTag(tag._id)?.active ?? false;
6363
},
6464
exec: async (): Promise<void> => {
65-
toggleTagActive(tag._id);
65+
await toggleTagActive({ tagId: tag._id });
6666

6767
if (
6868
Config.paceCaret === "average" ||

frontend/src/ts/components/pages/account/Filters.tsx

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import defaultResultFilters from "../../../constants/default-result-filters";
1919
import { SimpleModal } from "../../../elements/simple-modal";
2020
import { FaSolidIcon } from "../../../types/font-awesome";
2121
import { IsValidResponse } from "../../../types/validation";
22+
import { cn } from "../../../utils/cn";
2223
import { createErrorMessage } from "../../../utils/error";
2324
import {
2425
getLanguageDisplayString,
@@ -141,6 +142,7 @@ export function Filters(props: {
141142
text: string;
142143
group: T;
143144
format?: (value: K) => string;
145+
class?: string;
144146
}): JSXElement => {
145147
// Isolate this group's data to prevent unnecessary updates
146148
const groupData = createMemo(() => props.filters[options.group]);
@@ -159,7 +161,7 @@ export function Filters(props: {
159161
);
160162

161163
return (
162-
<div>
164+
<div class={cn(`w-full`, options.class)}>
163165
<H3 fa={{ icon: options.icon, fixedWidth: true }} text={options.text} />
164166
<SlimSelect
165167
multiple
@@ -320,7 +322,7 @@ export function Filters(props: {
320322
onClick={() => props.onChangeFilters(noFilters())}
321323
class="mb-4 w-full"
322324
/>
323-
<div class="gap-4 md:grid md:grid-cols-2 [&>div]:last:col-span-2">
325+
<div class="gap-4 md:grid md:grid-cols-2">
324326
<ButtonGroup text="difficulty" icon="fa-star" group="difficulty" />
325327
<ButtonGroup text="personal best" icon="fa-crown" group="pb" />
326328
<ButtonGroup text="mode" icon="fa-bars" group="mode" />
@@ -334,20 +336,25 @@ export function Filters(props: {
334336
<ButtonGroup text="punctuation" icon="fa-at" group="punctuation" />
335337
<ButtonGroup text="numbers" icon="fa-hashtag" group="numbers" />
336338

337-
<Dropdown
338-
icon="fa-tag"
339-
text="tags"
340-
group="tags"
341-
format={(tag) =>
342-
tag === "none"
343-
? "no tag"
344-
: (tags().find((it) => it._id === tag)?.name ?? tag)
345-
}
346-
/>
339+
<Show when={tags().length > 0}>
340+
<Dropdown
341+
icon="fa-tag"
342+
text="tags"
343+
group="tags"
344+
format={(tag) =>
345+
tag === "none"
346+
? "no tag"
347+
: (tags().find((it) => it._id === tag)?.name ?? tag)
348+
}
349+
/>
350+
</Show>
347351
<Dropdown
348352
icon="fa-gamepad"
349353
text="funbox"
350354
group="funbox"
355+
class={cn("", {
356+
"col-span-2": tags().length === 0,
357+
})}
351358
format={(val) =>
352359
val === "none" ? "no funbox" : replaceUnderscoresWithSpaces(val)
353360
}
@@ -356,6 +363,7 @@ export function Filters(props: {
356363
icon="fa-globe-americas"
357364
text="language"
358365
group="language"
366+
class="col-span-2"
359367
format={getLanguageDisplayString}
360368
/>
361369
</div>

frontend/src/ts/components/pages/settings/custom-setting/Tags.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function Tags(): JSXElement {
3636
text={tag.name}
3737
active={tag.active}
3838
onClick={() => {
39-
toggleTagActive(tag._id);
39+
void toggleTagActive({ tagId: tag._id });
4040
}}
4141
/>
4242
<Button

frontend/src/ts/controllers/preset-controller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ export async function apply(_id: string): Promise<void> {
3434
!isPartialPreset(presetToApply) ||
3535
presetToApply.settingGroups?.includes("behavior")
3636
) {
37-
clearActiveTags(true);
37+
await clearActiveTags({ noSave: true });
3838
if (presetToApply.config.tags) {
3939
for (const tagId of presetToApply.config.tags) {
40-
setTagActive(tagId, true, false);
40+
await setTagActive({ tagId, active: true });
4141
}
4242
saveActiveToLocalStorage();
4343
}

0 commit comments

Comments
 (0)