From 7cf920a6b22fcb6f3e7f647c91dfcfe256c5e31c Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 19 May 2026 11:55:38 +0200 Subject: [PATCH 1/8] fix(account): newly created tags not appearing in filters (@fehmer) --- .../controllers/preset-controller.spec.ts | 20 +++- frontend/src/ts/collections/results.ts | 12 +- frontend/src/ts/collections/tags.ts | 111 +++++++++++++----- frontend/src/ts/commandline/lists/tags.ts | 4 +- .../pages/settings/custom-setting/Tags.tsx | 2 +- .../src/ts/controllers/preset-controller.ts | 4 +- 6 files changed, 109 insertions(+), 44 deletions(-) diff --git a/frontend/__tests__/controllers/preset-controller.spec.ts b/frontend/__tests__/controllers/preset-controller.spec.ts index ad250c5f494e..4b23d4cb3b4f 100644 --- a/frontend/__tests__/controllers/preset-controller.spec.ts +++ b/frontend/__tests__/controllers/preset-controller.spec.ts @@ -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(); }); @@ -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(); }); diff --git a/frontend/src/ts/collections/results.ts b/frontend/src/ts/collections/results.ts index a224008a34d4..a9146207224c 100644 --- a/frontend/src/ts/collections/results.ts +++ b/frontend/src/ts/collections/results.ts @@ -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, @@ -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), }) @@ -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) diff --git a/frontend/src/ts/collections/tags.ts b/frontend/src/ts/collections/tags.ts index 5142acebbae3..f4378afda50a 100644 --- a/frontend/src/ts/collections/tags.ts +++ b/frontend/src/ts/collections/tags.ts @@ -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"; @@ -60,6 +62,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; @@ -74,6 +86,18 @@ type ActionType = { deleteTag: { tagId: string; }; + toggleTagActive: { + tagId: string; + noSave?: boolean; + }; + setTagActive: { + tagId: string; + active: boolean; + noSave?: boolean; + }; + clearActiveTags: { + noSave?: boolean; + }; }; const actions = { @@ -168,6 +192,43 @@ const actions = { tagsCollection.utils.writeDelete(tagId); }, }), + toggleTagActive: createOptimisticAction({ + 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({ + 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({ + onMutate: ({ noSave }) => { + tagsCollection.utils.writeBatch(() => { + tagsCollection.forEach((tag) => { + if (tag.active) { + tagsCollection.utils.writeUpdate({ ...tag, active: false }); + } + }); + }); + if (!noSave) saveActiveToLocalStorage(); + }, + mutationFn: async () => { + return; + }, + }), }; // --- Public API --- @@ -200,6 +261,27 @@ export async function deleteTag( await transaction.isPersisted.promise; } +export async function toggleTagActive( + params: ActionType["toggleTagActive"], +): Promise { + const transaction = actions.toggleTagActive(params); + await transaction.isPersisted.promise; +} + +export async function setTagActive( + params: ActionType["setTagActive"], +): Promise { + const transaction = actions.setTagActive(params); + await transaction.isPersisted.promise; +} + +export async function clearActiveTags( + params: ActionType["clearActiveTags"], +): Promise { + const transaction = actions.clearActiveTags(params); + await transaction.isPersisted.promise; +} + function getTags(): TagItem[] { return [...tagsCollection.values()].sort((a, b) => a.name.localeCompare(b.name), @@ -230,35 +312,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( diff --git a/frontend/src/ts/commandline/lists/tags.ts b/frontend/src/ts/commandline/lists/tags.ts index 209ae2047a30..63848285bf91 100644 --- a/frontend/src/ts/commandline/lists/tags.ts +++ b/frontend/src/ts/commandline/lists/tags.ts @@ -41,7 +41,7 @@ function update(): void { icon: "fa-times", sticky: true, exec: async (): Promise => { - clearActiveTags(); + await clearActiveTags({}); if ( Config.paceCaret === "average" || Config.paceCaret === "tagPb" || @@ -62,7 +62,7 @@ function update(): void { return __nonReactive.getTag(tag._id)?.active ?? false; }, exec: async (): Promise => { - toggleTagActive(tag._id); + await toggleTagActive({ tagId: tag._id }); if ( Config.paceCaret === "average" || diff --git a/frontend/src/ts/components/pages/settings/custom-setting/Tags.tsx b/frontend/src/ts/components/pages/settings/custom-setting/Tags.tsx index 74fdf79658bb..ea7d15628d96 100644 --- a/frontend/src/ts/components/pages/settings/custom-setting/Tags.tsx +++ b/frontend/src/ts/components/pages/settings/custom-setting/Tags.tsx @@ -36,7 +36,7 @@ export function Tags(): JSXElement { text={tag.name} active={tag.active} onClick={() => { - toggleTagActive(tag._id); + void toggleTagActive({ tagId: tag._id }); }} />