implement deck config saving on JS end

This commit is contained in:
Damien Elmes 2021-04-20 19:50:05 +10:00
parent 094c272294
commit 55277aa90a
8 changed files with 131 additions and 11 deletions

View file

@ -34,7 +34,7 @@ LABEL_REQUIRED = 2
LABEL_REPEATED = 3
# messages we don't want to unroll in codegen
SKIP_UNROLL_INPUT = {"TranslateString", "SetPreferences"}
SKIP_UNROLL_INPUT = {"TranslateString", "SetPreferences", "UpdateDeckConfigs"}
SKIP_UNROLL_OUTPUT = {"GetPreferences"}
SKIP_DECODE = {"Graphs", "GetGraphPreferences"}

View file

@ -23,6 +23,7 @@ DeckNameId = _pb.DeckNameId
FilteredDeckConfig = _pb.Deck.Filtered
DeckCollapseScope = _pb.SetDeckCollapsedIn.Scope
DeckConfigsForUpdate = _pb.DeckConfigsForUpdate
UpdateDeckConfigs = _pb.UpdateDeckConfigsIn
# legacy code may pass this in as the type argument to .id()
defaultDeck = 0
@ -328,6 +329,9 @@ class DeckManager:
def get_deck_configs_for_update(self, deck_id: DeckId) -> DeckConfigsForUpdate:
return self.col._backend.get_deck_configs_for_update(deck_id)
def update_deck_configs(self, input: UpdateDeckConfigs) -> OpChanges:
return self.col._backend.update_deck_configs(input=input)
def all_config(self) -> List[DeckConfigDict]:
"A list of all deck config."
return list(from_json_bytes(self.col._backend.all_deck_config_legacy()))

View file

@ -20,8 +20,10 @@ from waitress.server import create_server
import aqt
from anki import hooks
from anki.collection import GraphPreferences
from anki.collection import GraphPreferences, OpChanges
from anki.decks import UpdateDeckConfigs
from anki.utils import devMode, from_json_bytes
from aqt.operations.deck import update_deck_configs
from aqt.qt import *
from aqt.utils import aqt_data_folder
@ -283,11 +285,32 @@ def deck_configs_for_update() -> bytes:
).SerializeToString()
def update_deck_configs_request() -> bytes:
# the regular change tracking machinery expects to be started on the main
# thread and uses a callback on success, so we need to run this op on
# main, and return immediately from the web request
input = UpdateDeckConfigs()
input.ParseFromString(request.data)
def on_success(changes: OpChanges) -> None:
print("done", changes)
def handle_on_main() -> None:
update_deck_configs(parent=aqt.mw, input=input).success(
on_success
).run_in_background()
aqt.mw.taskman.run_on_main(handle_on_main)
return b""
post_handlers = {
"graphData": graph_data,
"graphPreferences": graph_preferences,
"setGraphPreferences": set_graph_preferences,
"deckConfigsForUpdate": deck_configs_for_update,
"updateDeckConfigs": update_deck_configs_request,
# pylint: disable=unnecessary-lambda
"i18nResources": i18n_resources,
"congratsInfo": congrats_info,

View file

@ -6,7 +6,7 @@ from __future__ import annotations
from typing import Optional, Sequence
from anki.collection import OpChanges, OpChangesWithCount, OpChangesWithId
from anki.decks import DeckCollapseScope, DeckId
from anki.decks import DeckCollapseScope, DeckId, UpdateDeckConfigs
from aqt import QWidget
from aqt.operations import CollectionOp
from aqt.utils import getOnlyText, tooltip, tr
@ -80,3 +80,9 @@ def set_deck_collapsed(
def set_current_deck(*, parent: QWidget, deck_id: DeckId) -> CollectionOp[OpChanges]:
return CollectionOp(parent, lambda col: col.decks.set_current(deck_id))
def update_deck_configs(
*, parent: QWidget, input: UpdateDeckConfigs
) -> CollectionOp[OpChanges]:
return CollectionOp(parent, lambda col: col.decks.update_deck_configs(input))

View file

@ -54,6 +54,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
}, 100);
}
function save(applyToChildDecks: boolean): void {
state.save(applyToChildDecks);
}
</script>
<style>
@ -63,7 +67,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</style>
<div class="btn-group">
<button type="button" class="btn btn-primary">Save</button>
<button
type="button"
class="btn btn-primary"
on:click={() => save(false)}>Save</button>
<button
type="button"
class="btn btn-primary dropdown-toggle dropdown-toggle-split"
@ -82,6 +89,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<li>
<hr class="dropdown-divider" />
</li>
<li><a class="dropdown-item" href={'#'}>Apply to Child Decks</a></li>
<li>
<a class="dropdown-item" href={'#'} on:click={() => save(true)}>Save to All
Children</a>
</li>
</ul>
</div>

View file

@ -15,7 +15,7 @@ export async function deckConfig(
modules: [ModuleName.SCHEDULING, ModuleName.ACTIONS, ModuleName.DECK_CONFIG],
});
const info = await getDeckConfigInfo(deckId);
const state = new DeckConfigState(info);
const state = new DeckConfigState(deckId, info);
new DeckConfigPage({
target,
props: { state },

View file

@ -92,6 +92,7 @@ const exampleData = {
function startingState(): DeckConfigState {
return new DeckConfigState(
123,
pb.BackendProto.DeckConfigsForUpdate.fromObject(exampleData)
);
}
@ -198,7 +199,8 @@ test("deck list", () => {
expect(get(state.currentConfig).newPerDay).toBe(10);
// only the pre-existing deck should be listed for removal
expect((state as any).removedConfigs).toStrictEqual([1618570764780]);
const out = state.dataForSaving(false);
expect(out.removedConfigIds).toStrictEqual([1618570764780]);
});
test("duplicate name", () => {
@ -233,3 +235,44 @@ test("parent counts", () => {
});
expect(get(state.parentLimits)).toStrictEqual({ newCards: 123, reviews: 200 });
});
test("saving", () => {
let state = startingState();
let out = state.dataForSaving(false);
expect(out.removedConfigIds).toStrictEqual([]);
expect(out.targetDeckId).toBe(123);
// in no-changes case, currently selected config should
// be returned
expect(out.configs.length).toBe(1);
expect(out.configs[0].name).toBe("another one");
expect(out.applyToChildren).toBe(false);
// rename, then change current deck
state.setCurrentName("zzz");
out = state.dataForSaving(true);
state.setCurrentIndex(0);
// renamed deck should be in changes, with current deck as last element
out = state.dataForSaving(true);
expect(out.configs.map((c) => c.name)).toStrictEqual(["zzz", "Default"]);
expect(out.applyToChildren).toBe(true);
// start again, adding new deck
state = startingState();
state.addConfig("hello");
// deleting it should not change removedConfigs
state.removeCurrentConfig();
out = state.dataForSaving(true);
expect(out.removedConfigIds).toStrictEqual([]);
// select the other non-default deck & remove
state.setCurrentIndex(1);
state.removeCurrentConfig();
// should be listed in removedConfigs, and modified should
// only contain Default, which is the new current deck
out = state.dataForSaving(true);
expect(out.removedConfigIds).toStrictEqual([1618570764780]);
expect(out.configs.map((c) => c.name)).toStrictEqual(["Default"]);
});

View file

@ -19,6 +19,14 @@ export async function getDeckConfigInfo(
);
}
export async function saveDeckConfig(
input: pb.BackendProto.UpdateDeckConfigsIn
): Promise<void> {
const data: Uint8Array = pb.BackendProto.UpdateDeckConfigsIn.encode(input).finish();
await postRequest("/_anki/updateDeckConfigs", data);
return;
}
export type DeckConfigId = number;
export interface ConfigWithCount {
@ -47,6 +55,7 @@ export class DeckConfigState {
readonly currentDeck: pb.BackendProto.DeckConfigsForUpdate.CurrentDeck;
readonly defaults: ConfigInner;
private targetDeckId: number;
private configs: ConfigWithCount[];
private selectedIdx: number;
private configListSetter!: (val: ConfigListEntry[]) => void;
@ -55,7 +64,8 @@ export class DeckConfigState {
private removedConfigs: DeckConfigId[] = [];
private schemaModified: boolean;
constructor(data: pb.BackendProto.DeckConfigsForUpdate) {
constructor(targetDeckId: number, data: pb.BackendProto.DeckConfigsForUpdate) {
this.targetDeckId = targetDeckId;
this.currentDeck = data.currentDeck as pb.BackendProto.DeckConfigsForUpdate.CurrentDeck;
this.defaults = data.defaults!.config! as ConfigInner;
this.configs = data.allConfig.map((config) => {
@ -117,7 +127,6 @@ export class DeckConfigState {
}
/// Adds a new config, making it current.
/// not already a new config.
addConfig(name: string): void {
const uniqueName = this.ensureNewNameUnique(name);
const config = pb.BackendProto.DeckConfig.create({
@ -144,9 +153,8 @@ export class DeckConfigState {
removeCurrentConfig(): void {
const currentId = this.configs[this.selectedIdx].config.id;
if (currentId === 1) {
throw "can't remove default config";
throw Error("can't remove default config");
}
if (currentId !== 0) {
this.removedConfigs.push(currentId);
this.schemaModified = true;
@ -157,6 +165,32 @@ export class DeckConfigState {
this.updateConfigList();
}
dataForSaving(applyToChildren: boolean): pb.BackendProto.UpdateDeckConfigsIn {
const modifiedConfigsExcludingCurrent = this.configs
.map((c) => c.config)
.filter((c, idx) => {
return (
idx !== this.selectedIdx &&
(c.id === 0 || this.modifiedConfigs.has(c.id))
);
});
const configs = [
...modifiedConfigsExcludingCurrent,
// current must come last, even if unmodified
this.configs[this.selectedIdx].config,
];
return pb.BackendProto.UpdateDeckConfigsIn.create({
targetDeckId: this.targetDeckId,
removedConfigIds: this.removedConfigs,
configs,
applyToChildren,
});
}
async save(applyToChildren: boolean): Promise<void> {
await saveDeckConfig(this.dataForSaving(applyToChildren));
}
private onCurrentConfigChanged(config: ConfigInner): void {
if (!isEqual(config, this.configs[this.selectedIdx].config.config)) {
this.configs[this.selectedIdx].config.config = config;