Skip to content

Commit c9fa4de

Browse files
authored
refactor: solid presets (@Miodec) (#7825)
1 parent d77d15e commit c9fa4de

9 files changed

Lines changed: 288 additions & 159 deletions

File tree

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import * as Persistence from "../../src/ts/config/persistence";
1010
import * as Notifications from "../../src/ts/states/notifications";
1111
import * as TestLogic from "../../src/ts/test/test-logic";
1212
import * as Tags from "../../src/ts/collections/tags";
13+
import * as Presets from "../../src/ts/collections/presets";
1314

1415
describe("PresetController", () => {
1516
describe("apply", () => {
@@ -20,6 +21,7 @@ describe("PresetController", () => {
2021
//
2122
}));
2223
const dbGetSnapshotMock = vi.spyOn(DB, "getSnapshot");
24+
const getPresetMock = vi.spyOn(Presets.__nonReactive, "getPreset");
2325
const configApplyMock = vi.spyOn(Lifecycle, "applyConfig");
2426
const configSaveFullConfigMock = vi.spyOn(
2527
Persistence,
@@ -41,6 +43,7 @@ describe("PresetController", () => {
4143
beforeEach(() => {
4244
[
4345
dbGetSnapshotMock,
46+
getPresetMock,
4447
configApplyMock,
4548
configSaveFullConfigMock,
4649
configGetConfigChangesMock,
@@ -51,6 +54,7 @@ describe("PresetController", () => {
5154
tagsSaveActiveMock,
5255
].forEach((it) => it.mockClear());
5356

57+
dbGetSnapshotMock.mockReturnValue({} as any);
5458
configApplyMock.mockResolvedValue();
5559
});
5660

@@ -88,6 +92,9 @@ describe("PresetController", () => {
8892
});
8993

9094
it("should ignore unknown preset", async () => {
95+
//GIVEN
96+
getPresetMock.mockReturnValue(undefined);
97+
9198
//WHEN
9299
await PresetController.apply("unknown");
93100
//THEN
@@ -143,7 +150,8 @@ describe("PresetController", () => {
143150
_id: "1",
144151
...partialPreset,
145152
} as any;
146-
dbGetSnapshotMock.mockReturnValue({ presets: [preset] } as any);
153+
dbGetSnapshotMock.mockReturnValue({} as any);
154+
getPresetMock.mockReturnValue(preset as any);
147155
return preset;
148156
};
149157
});
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { Preset } from "@monkeytype/schemas/presets";
2+
import { queryCollectionOptions } from "@tanstack/query-db-collection";
3+
import {
4+
createCollection,
5+
createOptimisticAction,
6+
useLiveQuery,
7+
} from "@tanstack/solid-db";
8+
import Ape from "../ape";
9+
import { queryClient } from "../queries";
10+
import { baseKey } from "../queries/utils/keys";
11+
import { ConfigGroupName } from "@monkeytype/schemas/configs";
12+
import { tempId } from "./utils/misc";
13+
14+
export type PresetItem = Preset;
15+
16+
const queryKeys = {
17+
root: () => [...baseKey("presets", { isUserSpecific: true })],
18+
};
19+
20+
// oxlint-disable-next-line typescript/explicit-function-return-type
21+
export function usePresetsLiveQuery() {
22+
return useLiveQuery((q) => {
23+
return q
24+
.from({ preset: presetsCollection })
25+
.orderBy(({ preset }) => preset.name, "asc");
26+
});
27+
}
28+
29+
const presetsCollection = createCollection(
30+
queryCollectionOptions({
31+
staleTime: Infinity,
32+
startSync: true,
33+
queryKey: queryKeys.root(),
34+
35+
queryClient,
36+
getKey: (it) => it._id,
37+
queryFn: async () => {
38+
return [] as PresetItem[];
39+
},
40+
}),
41+
);
42+
43+
type ActionType = {
44+
addPreset: {
45+
name: string;
46+
config: Preset["config"];
47+
settingGroups: ConfigGroupName[] | undefined;
48+
};
49+
editPreset: {
50+
presetId: string;
51+
name: string;
52+
config?: Preset["config"];
53+
settingGroups?: ConfigGroupName[] | null;
54+
};
55+
deletePreset: {
56+
presetId: string;
57+
};
58+
};
59+
60+
const actions = {
61+
addPreset: createOptimisticAction<ActionType["addPreset"]>({
62+
onMutate: ({ name, config, settingGroups }) => {
63+
presetsCollection.insert({
64+
_id: tempId(),
65+
name: name.replace(/_/g, " "),
66+
config,
67+
settingGroups,
68+
});
69+
},
70+
mutationFn: async ({ name, config, settingGroups }) => {
71+
const response = await Ape.presets.add({
72+
body: {
73+
name: name.replace(/ /g, "_"),
74+
config,
75+
...(settingGroups !== undefined && { settingGroups }),
76+
},
77+
});
78+
if (response.status !== 200) {
79+
throw new Error(`Failed to add preset: ${response.body.message}`);
80+
}
81+
82+
const newPreset = {
83+
_id: response.body.data.presetId,
84+
name: name.replace(/_/g, " "),
85+
config,
86+
settingGroups,
87+
};
88+
89+
presetsCollection.utils.writeInsert(newPreset);
90+
},
91+
}),
92+
editPreset: createOptimisticAction<ActionType["editPreset"]>({
93+
onMutate: ({ presetId, name, config, settingGroups }) => {
94+
presetsCollection.update(presetId, (preset) => {
95+
preset.name = name.replace(/_/g, " ");
96+
97+
if (config !== undefined) {
98+
preset.config = config;
99+
}
100+
if (settingGroups !== undefined) {
101+
preset.settingGroups = settingGroups;
102+
}
103+
});
104+
},
105+
mutationFn: async ({ presetId, name, config, settingGroups }) => {
106+
const existing = presetsCollection.get(presetId);
107+
108+
if (existing === undefined) {
109+
throw new Error("Preset not found");
110+
}
111+
112+
const response = await Ape.presets.save({
113+
body: {
114+
_id: presetId,
115+
name: name.replace(/ /g, "_"),
116+
...(config !== undefined && {
117+
config: config,
118+
settingGroups: settingGroups,
119+
}),
120+
},
121+
});
122+
if (response.status !== 200) {
123+
throw new Error(`Failed to edit preset: ${response.body.message}`);
124+
}
125+
126+
// if this is missing getPreset is out of sync
127+
presetsCollection.utils.writeUpdate({
128+
_id: presetId,
129+
name: name.replace(/_/g, " "),
130+
...(config !== undefined && { config }),
131+
...(settingGroups !== undefined && { settingGroups }),
132+
});
133+
},
134+
}),
135+
deletePreset: createOptimisticAction<ActionType["deletePreset"]>({
136+
onMutate: ({ presetId }) => {
137+
presetsCollection.delete(presetId);
138+
},
139+
mutationFn: async ({ presetId }) => {
140+
const response = await Ape.presets.delete({
141+
params: { presetId },
142+
});
143+
if (response.status !== 200) {
144+
throw new Error(`Failed to delete preset: ${response.body.message}`);
145+
}
146+
presetsCollection.utils.writeDelete(presetId);
147+
},
148+
}),
149+
};
150+
151+
// --- Public API ---
152+
153+
function getPresets(): PresetItem[] {
154+
return [...presetsCollection.values()].sort((a, b) =>
155+
a.name.localeCompare(b.name),
156+
);
157+
}
158+
159+
function getPreset(id: string): PresetItem | undefined {
160+
return presetsCollection.get(id);
161+
}
162+
163+
export function fillPresetsCollection(presets: Preset[]): void {
164+
const presetItems = presets.map((preset) => ({
165+
_id: preset._id,
166+
name: preset.name.replace(/_/g, " "),
167+
config: preset.config,
168+
settingGroups: preset.settingGroups,
169+
}));
170+
171+
presetsCollection.utils.writeBatch(() => {
172+
presetItems.forEach((item) => {
173+
presetsCollection.utils.writeInsert(item);
174+
});
175+
});
176+
}
177+
178+
export async function addPreset(
179+
params: ActionType["addPreset"],
180+
): Promise<void> {
181+
const transaction = actions.addPreset(params);
182+
await transaction.isPersisted.promise;
183+
}
184+
185+
export async function editPreset(
186+
params: ActionType["editPreset"],
187+
): Promise<void> {
188+
const transaction = actions.editPreset(params);
189+
await transaction.isPersisted.promise;
190+
}
191+
192+
export async function deletePreset(
193+
params: ActionType["deletePreset"],
194+
): Promise<void> {
195+
const transaction = actions.deletePreset(params);
196+
await transaction.isPersisted.promise;
197+
}
198+
199+
/**
200+
* Used for non reactive access. Do not use in Solid components.
201+
*/
202+
export const __nonReactive = {
203+
getPresets,
204+
getPreset,
205+
};

frontend/src/ts/commandline/lists/presets.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import * as DB from "../../db";
21
import * as ModesNotice from "../../elements/modes-notice";
32
import * as Settings from "../../pages/settings";
43
import * as PresetController from "../../controllers/preset-controller";
54
import * as EditPresetPopup from "../../modals/edit-preset";
65
import { isAuthenticated } from "../../states/core";
76
import { Command, CommandsSubgroup } from "../types";
7+
import { __nonReactive } from "../../collections/presets";
88

99
const subgroup: CommandsSubgroup = {
1010
title: "Presets...",
@@ -28,15 +28,13 @@ const commands: Command[] = [
2828
];
2929

3030
function update(): void {
31-
const snapshot = DB.getSnapshot();
31+
const presets = __nonReactive.getPresets();
3232
subgroup.list = [];
33-
if (!snapshot?.presets || snapshot.presets.length === 0) return;
34-
snapshot.presets.forEach((preset) => {
35-
const dis = preset.display;
36-
33+
if (presets.length === 0) return;
34+
presets.forEach((preset) => {
3735
subgroup.list.push({
3836
id: "applyPreset" + preset._id,
39-
display: dis,
37+
display: preset.name,
4038
exec: async (): Promise<void> => {
4139
Settings.setEventDisabled(true);
4240
await PresetController.apply(preset._id);

frontend/src/ts/constants/default-snapshot.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
ModifiableTestActivityCalendar,
88
TestActivityCalendar,
99
} from "../elements/test-activity-calendar";
10-
import { Preset } from "@monkeytype/schemas/presets";
1110
import { Language } from "@monkeytype/schemas/languages";
1211
import { ConnectionStatus } from "@monkeytype/schemas/connections";
1312

@@ -69,17 +68,12 @@ export type Snapshot = Omit<
6968
maxStreak: number;
7069
isPremium: boolean;
7170
streakHourOffset?: number;
72-
presets: SnapshotPreset[];
7371
xp: number;
7472
testActivity?: ModifiableTestActivityCalendar;
7573
testActivityByYear?: { [key: string]: TestActivityCalendar };
7674
connections: Record<string, ConnectionStatus | "incoming">;
7775
};
7876

79-
export type SnapshotPreset = Preset & {
80-
display: string;
81-
};
82-
8377
const defaultSnap = {
8478
personalBests: {
8579
time: {},
@@ -94,7 +88,6 @@ const defaultSnap = {
9488
isPremium: false,
9589
config: getDefaultConfig(),
9690
customThemes: [],
97-
presets: [],
9891
banned: undefined,
9992
verified: undefined,
10093
lbMemory: { time: { 15: { english: 0 }, 60: { english: 0 } } },
Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,22 @@
1-
import { Preset } from "@monkeytype/schemas/presets";
2-
31
import { Config } from "../config/store";
42
import { applyConfig } from "../config/lifecycle";
53
import * as DB from "../db";
6-
import {
7-
showNoticeNotification,
8-
showSuccessNotification,
9-
} from "../states/notifications";
4+
import { showSuccessNotification } from "../states/notifications";
105
import * as TestLogic from "../test/test-logic";
116
import {
127
clearActiveTags,
138
setTagActive,
149
saveActiveToLocalStorage,
1510
} from "../collections/tags";
16-
import { SnapshotPreset } from "../constants/default-snapshot";
1711
import { saveFullConfigToLocalStorage } from "../config/persistence";
1812
import * as ModesNotice from "../elements/modes-notice";
13+
import { __nonReactive, type PresetItem } from "../collections/presets";
1914

2015
export async function apply(_id: string): Promise<void> {
2116
const snapshot = DB.getSnapshot();
2217
if (!snapshot) return;
2318

24-
const presetToApply = snapshot.presets?.find((preset) => preset._id === _id);
19+
const presetToApply = __nonReactive.getPreset(_id);
2520
if (presetToApply === undefined) {
2621
return;
2722
}
@@ -52,21 +47,7 @@ export async function apply(_id: string): Promise<void> {
5247
showSuccessNotification("Preset applied", { durationMs: 2000 });
5348
saveFullConfigToLocalStorage();
5449
}
55-
function isPartialPreset(preset: SnapshotPreset): boolean {
56-
return preset.settingGroups !== undefined && preset.settingGroups !== null;
57-
}
58-
59-
export async function getPreset(_id: string): Promise<Preset | undefined> {
60-
const snapshot = DB.getSnapshot();
61-
if (!snapshot) {
62-
return;
63-
}
64-
65-
const preset = snapshot.presets?.find((preset) => preset._id === _id);
6650

67-
if (preset === undefined) {
68-
showNoticeNotification("Preset not found");
69-
return;
70-
}
71-
return preset;
51+
function isPartialPreset(preset: PresetItem): boolean {
52+
return preset.settingGroups !== undefined && preset.settingGroups !== null;
7253
}

0 commit comments

Comments
 (0)