Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
208 changes: 208 additions & 0 deletions frontend/src/ts/collections/presets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
import { Preset } from "@monkeytype/schemas/presets";
import { queryCollectionOptions } from "@tanstack/query-db-collection";
import {
createCollection,
createOptimisticAction,
useLiveQuery,
} from "@tanstack/solid-db";
import Ape from "../ape";
import { queryClient } from "../queries";
import { baseKey } from "../queries/utils/keys";
import { ConfigGroupName } from "@monkeytype/schemas/configs";

export type PresetItem = Preset & { display: string };

const queryKeys = {
root: () => [...baseKey("presets", { isUserSpecific: true })],
};

function toPresetItem(preset: Preset): PresetItem {
return {
...preset,
display: preset.name.replaceAll("_", " "),
Comment thread
Miodec marked this conversation as resolved.
Outdated
};
}

// oxlint-disable-next-line typescript/explicit-function-return-type
export function usePresetsLiveQuery() {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we rename to ConfigPresets for more clarity? We have FilterPresets as well

return useLiveQuery((q) => {
return q
.from({ preset: presetsCollection })
.orderBy(({ preset }) => preset.name, "asc");
});
}

const presetsCollection = createCollection(
queryCollectionOptions({
staleTime: Infinity,
startSync: true,
queryKey: queryKeys.root(),

queryClient,
getKey: (it) => it._id,
queryFn: async () => {
return [] as PresetItem[];
},
}),
);

type ActionType = {
addPreset: {
name: string;
config: Preset["config"];
settingGroups: ConfigGroupName[] | undefined;
};
editPreset: {
presetId: string;
name: string;
config?: Preset["config"];
settingGroups?: ConfigGroupName[] | null;
};
deletePreset: {
presetId: string;
};
};

const actions = {
addPreset: createOptimisticAction<ActionType["addPreset"]>({
onMutate: ({ name, config, settingGroups }) => {
presetsCollection.insert({
_id: "temp-" + Date.now(),
Comment thread
Miodec marked this conversation as resolved.
Outdated
name,
display: name.replaceAll("_", " "),
config,
settingGroups,
});
},
mutationFn: async ({ name, config, settingGroups }) => {
const response = await Ape.presets.add({
body: {
name,
config,
...(settingGroups !== undefined && { settingGroups }),
},
});
if (response.status !== 200) {
throw new Error(`Failed to add preset: ${response.body.message}`);
}
presetsCollection.utils.writeInsert(
toPresetItem({
_id: response.body.data.presetId,
name: name,
settingGroups: settingGroups,
config: config,
}),
);
},
}),
editPreset: createOptimisticAction<ActionType["editPreset"]>({
onMutate: ({ presetId, name, config, settingGroups }) => {
presetsCollection.update(presetId, (preset) => {
preset.name = name;
preset.display = name.replaceAll("_", " ");

if (config !== undefined) {
preset.config = config;
}
if (settingGroups !== undefined) {
preset.settingGroups = settingGroups;
}
});
},
mutationFn: async ({ presetId, name, config, settingGroups }) => {
const existing = presetsCollection.get(presetId);

if (existing === undefined) {
throw new Error("Preset not found");
}

const response = await Ape.presets.save({
body: {
_id: presetId,
name: name,
...(config !== undefined && {
config: config,
settingGroups: settingGroups,
}),
},
});
if (response.status !== 200) {
throw new Error(`Failed to edit preset: ${response.body.message}`);
}

// if this is missing getPreset is out of sync
presetsCollection.utils.writeUpdate({
_id: presetId,
name,
Comment thread
Miodec marked this conversation as resolved.
Outdated
display: name.replaceAll("_", " "),
...(config !== undefined && { config }),
...(settingGroups !== undefined && { settingGroups }),
});
},
}),
deletePreset: createOptimisticAction<ActionType["deletePreset"]>({
onMutate: ({ presetId }) => {
presetsCollection.delete(presetId);
},
mutationFn: async ({ presetId }) => {
const response = await Ape.presets.delete({
params: { presetId },
});
if (response.status !== 200) {
throw new Error(`Failed to delete preset: ${response.body.message}`);
}
presetsCollection.utils.writeDelete(presetId);
},
}),
};

// --- Public API ---

function getPresets(): PresetItem[] {
return [...presetsCollection.values()].sort((a, b) =>
a.name.localeCompare(b.name),
);
}

function getPreset(id: string): PresetItem | undefined {
return presetsCollection.get(id);
}

export function fillPresetsCollection(presets: Preset[]): void {
const presetItems = presets.map(toPresetItem);

presetsCollection.utils.writeBatch(() => {
Comment thread
Miodec marked this conversation as resolved.
Outdated
presetItems.forEach((item) => {
presetsCollection.utils.writeInsert(item);
});
});
}

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

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

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

/**
* Used for non reactive access. Do not use in Solid components.
*/
export const __nonReactive = {
getPresets,
getPreset,
};
8 changes: 4 additions & 4 deletions frontend/src/ts/commandline/lists/presets.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as DB from "../../db";
import * as ModesNotice from "../../elements/modes-notice";
import * as Settings from "../../pages/settings";
import * as PresetController from "../../controllers/preset-controller";
import * as EditPresetPopup from "../../modals/edit-preset";
import { isAuthenticated } from "../../states/core";
import { Command, CommandsSubgroup } from "../types";
import { __nonReactive } from "../../collections/presets";

const subgroup: CommandsSubgroup = {
title: "Presets...",
Expand All @@ -28,10 +28,10 @@ const commands: Command[] = [
];

function update(): void {
const snapshot = DB.getSnapshot();
const presets = __nonReactive.getPresets();
subgroup.list = [];
if (!snapshot?.presets || snapshot.presets.length === 0) return;
snapshot.presets.forEach((preset) => {
if (presets.length === 0) return;
presets.forEach((preset) => {
const dis = preset.display;
Comment thread
Miodec marked this conversation as resolved.
Outdated

subgroup.list.push({
Expand Down
7 changes: 0 additions & 7 deletions frontend/src/ts/constants/default-snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
ModifiableTestActivityCalendar,
TestActivityCalendar,
} from "../elements/test-activity-calendar";
import { Preset } from "@monkeytype/schemas/presets";
import { Language } from "@monkeytype/schemas/languages";
import { ConnectionStatus } from "@monkeytype/schemas/connections";

Expand Down Expand Up @@ -69,17 +68,12 @@ export type Snapshot = Omit<
maxStreak: number;
isPremium: boolean;
streakHourOffset?: number;
presets: SnapshotPreset[];
xp: number;
testActivity?: ModifiableTestActivityCalendar;
testActivityByYear?: { [key: string]: TestActivityCalendar };
connections: Record<string, ConnectionStatus | "incoming">;
};

export type SnapshotPreset = Preset & {
display: string;
};

const defaultSnap = {
personalBests: {
time: {},
Expand All @@ -94,7 +88,6 @@ const defaultSnap = {
isPremium: false,
config: getDefaultConfig(),
customThemes: [],
presets: [],
banned: undefined,
verified: undefined,
lbMemory: { time: { 15: { english: 0 }, 60: { english: 0 } } },
Expand Down
29 changes: 5 additions & 24 deletions frontend/src/ts/controllers/preset-controller.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
import { Preset } from "@monkeytype/schemas/presets";

import { Config } from "../config/store";
import { applyConfig } from "../config/lifecycle";
import * as DB from "../db";
import {
showNoticeNotification,
showSuccessNotification,
} from "../states/notifications";
import { showSuccessNotification } from "../states/notifications";
import * as TestLogic from "../test/test-logic";
import {
clearActiveTags,
setTagActive,
saveActiveToLocalStorage,
} from "../collections/tags";
import { SnapshotPreset } from "../constants/default-snapshot";
import { saveFullConfigToLocalStorage } from "../config/persistence";
import * as ModesNotice from "../elements/modes-notice";
import { __nonReactive, type PresetItem } from "../collections/presets";

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

const presetToApply = snapshot.presets?.find((preset) => preset._id === _id);
const presetToApply = __nonReactive.getPreset(_id);
if (presetToApply === undefined) {
Comment thread
Miodec marked this conversation as resolved.
return;
}
Expand Down Expand Up @@ -52,21 +47,7 @@ export async function apply(_id: string): Promise<void> {
showSuccessNotification("Preset applied", { durationMs: 2000 });
saveFullConfigToLocalStorage();
}
function isPartialPreset(preset: SnapshotPreset): boolean {
return preset.settingGroups !== undefined && preset.settingGroups !== null;
}

export async function getPreset(_id: string): Promise<Preset | undefined> {
const snapshot = DB.getSnapshot();
if (!snapshot) {
return;
}

const preset = snapshot.presets?.find((preset) => preset._id === _id);

if (preset === undefined) {
showNoticeNotification("Preset not found");
return;
}
return preset;
function isPartialPreset(preset: PresetItem): boolean {
return preset.settingGroups !== undefined && preset.settingGroups !== null;
}
29 changes: 4 additions & 25 deletions frontend/src/ts/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
import {
getDefaultSnapshot,
Snapshot,
SnapshotPreset,
SnapshotResult,
} from "./constants/default-snapshot";
import { getFirstDayOfTheWeek } from "./utils/date-and-time";
Expand All @@ -44,6 +43,7 @@ import { setXpBarData } from "./states/header";
import { FunboxMetadata } from "@monkeytype/funbox";
import { fillTagsCollection, __nonReactive } from "./collections/tags";
import { updateTagsInFilterStorage } from "./states/result-filters";
import { fillPresetsCollection } from "./collections/presets";

let dbSnapshot: Snapshot | undefined;
const firstDayOfTheWeek = getFirstDayOfTheWeek();
Expand Down Expand Up @@ -198,34 +198,13 @@ export async function initSnapshot(): Promise<Snapshot | false> {
snap.customThemes = userData.customThemes ?? [];

fillTagsCollection(userData.tags ?? []);
updateTagsInFilterStorage(userData.tags?.map((it) => it._id) ?? []);
fillPresetsCollection(presetsData ?? []);

if (presetsData !== undefined && presetsData !== null) {
const presetsWithDisplay = presetsData.map((preset) => {
return {
...preset,
display: preset.name.replace(/_/g, " "),
};
}) as SnapshotPreset[];
snap.presets = presetsWithDisplay;

snap.presets = snap.presets?.sort(
(a: SnapshotPreset, b: SnapshotPreset) => {
if (a.name > b.name) {
return 1;
} else if (a.name < b.name) {
return -1;
} else {
return 0;
}
},
);
}
fillResultFilterPresetsCollection(userData.resultFilterPresets ?? []);
updateTagsInFilterStorage(userData.tags?.map((it) => it._id) ?? []);

snap.connections = convertConnections(connectionsData);

fillResultFilterPresetsCollection(userData.resultFilterPresets ?? []);

dbSnapshot = snap;

return dbSnapshot;
Expand Down
Loading
Loading