Skip to content
Draft
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
203 changes: 203 additions & 0 deletions frontend/src/ts/collections/presets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
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("_", " "),
};
}

// oxlint-disable-next-line typescript/explicit-function-return-type
export function usePresetsLiveQuery() {
return useLiveQuery((q) => {
return q.from({ preset: presetsCollection }).select((p) => ({ ...p }));
});
}

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(),
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,
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 ---

// todo: this might not be reactive
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A new Todo was discovered. If it is not a priority right now,consider marking it for later attention.
todo: this might not be reactive

export function getPresets(): PresetItem[] {
return [...presetsCollection.values()];
}

// todo: this might not be reactive
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A new Todo was discovered. If it is not a priority right now,consider marking it for later attention.
todo: this might not be reactive

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

export function fillPresetsCollection(presets: Preset[]): void {
const presetItems = presets
.map(toPresetItem)
.sort((a, b) => a.name.localeCompare(b.name));

presetsCollection.utils.writeBatch(() => {
presetsCollection.forEach((preset) => {
presetsCollection.utils.writeDelete(preset._id);
});
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;
}
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 { getPresets } 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 = 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;

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 @@ -12,7 +12,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 @@ -79,18 +78,13 @@ export type Snapshot = Omit<
isPremium: boolean;
streakHourOffset?: number;
tags: SnapshotUserTag[];
presets: SnapshotPreset[];
results?: SnapshotResult<Mode>[];
xp: number;
testActivity?: ModifiableTestActivityCalendar;
testActivityByYear?: { [key: string]: TestActivityCalendar };
connections: Record<string, ConnectionStatus | "incoming">;
};

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

const defaultSnap = {
results: undefined,
personalBests: {
Expand All @@ -106,7 +100,6 @@ const defaultSnap = {
isPremium: false,
config: getDefaultConfig(),
customThemes: [],
presets: [],
tags: [],
banned: undefined,
verified: undefined,
Expand Down
32 changes: 8 additions & 24 deletions frontend/src/ts/controllers/preset-controller.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
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 * as TagController from "./tag-controller";
import { SnapshotPreset } from "../constants/default-snapshot";
import { saveFullConfigToLocalStorage } from "../config/persistence";
import {
getPreset as getPresetFromCollection,
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 = getPresetFromCollection(_id);
if (presetToApply === undefined) {
return;
}
Expand Down Expand Up @@ -46,21 +44,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;
}
24 changes: 2 additions & 22 deletions frontend/src/ts/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
import {
getDefaultSnapshot,
Snapshot,
SnapshotPreset,
SnapshotResult,
SnapshotUserTag,
} from "./constants/default-snapshot";
Expand All @@ -42,6 +41,7 @@ import {
import { XpBreakdown } from "@monkeytype/schemas/results";
import { setXpBarData } from "./states/header";
import { FunboxMetadata } from "@monkeytype/funbox";
import { fillPresetsCollection } from "./collections/presets";

let dbSnapshot: Snapshot | undefined;
const firstDayOfTheWeek = getFirstDayOfTheWeek();
Expand Down Expand Up @@ -231,27 +231,7 @@ export async function initSnapshot(): Promise<Snapshot | false> {
}
});

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;
}
},
);
}
fillPresetsCollection(presetsData ?? []);

snap.connections = convertConnections(connectionsData);

Expand Down
Loading
Loading