61e86cc29d
- changes can now be undone - the same field can now be mapped to multiple target fields, allowing fields to be cloned - the old Qt dialog has been removed - the old col.models.change() API calls the new code, to avoid breaking existing consumers. It requires the field map to always be passed in, but that appears to have been the common case. - closes #1175
221 lines
7.1 KiB
TypeScript
221 lines
7.1 KiB
TypeScript
// Copyright: Ankitects Pty Ltd and contributors
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
/* eslint
|
|
@typescript-eslint/no-non-null-assertion: "off",
|
|
*/
|
|
|
|
import pb from "lib/backend_proto";
|
|
import { postRequest } from "lib/postrequest";
|
|
import { readable, Readable } from "svelte/store";
|
|
import { isEqual } from "lodash-es";
|
|
|
|
export async function getNotetypeNames(): Promise<pb.BackendProto.NotetypeNames> {
|
|
return pb.BackendProto.NotetypeNames.decode(
|
|
await postRequest("/_anki/notetypeNames", "")
|
|
);
|
|
}
|
|
|
|
export async function getChangeNotetypeInfo(
|
|
oldNotetypeId: number,
|
|
newNotetypeId: number
|
|
): Promise<pb.BackendProto.ChangeNotetypeInfo> {
|
|
return pb.BackendProto.ChangeNotetypeInfo.decode(
|
|
await postRequest(
|
|
"/_anki/changeNotetypeInfo",
|
|
JSON.stringify({ oldNotetypeId, newNotetypeId })
|
|
)
|
|
);
|
|
}
|
|
|
|
export async function changeNotetype(
|
|
input: pb.BackendProto.ChangeNotetypeIn
|
|
): Promise<void> {
|
|
const data: Uint8Array = pb.BackendProto.ChangeNotetypeIn.encode(input).finish();
|
|
await postRequest("/_anki/changeNotetype", data);
|
|
return;
|
|
}
|
|
|
|
function nullToNegativeOne(list: (number | null)[]): number[] {
|
|
return list.map((val) => val ?? -1);
|
|
}
|
|
|
|
/// Public only for tests.
|
|
export function negativeOneToNull(list: number[]): (number | null)[] {
|
|
return list.map((val) => (val === -1 ? null : val));
|
|
}
|
|
|
|
/// Wrapper for the protobuf message to make it more ergonomic.
|
|
export class ChangeNotetypeInfoWrapper {
|
|
fields: (number | null)[];
|
|
templates?: (number | null)[];
|
|
readonly info: pb.BackendProto.ChangeNotetypeInfo;
|
|
|
|
constructor(info: pb.BackendProto.ChangeNotetypeInfo) {
|
|
this.info = info;
|
|
const templates = info.input!.newTemplates!;
|
|
if (templates.length > 0) {
|
|
this.templates = negativeOneToNull(templates);
|
|
}
|
|
this.fields = negativeOneToNull(info.input!.newFields!);
|
|
}
|
|
|
|
/// A list with an entry for each field/template in the new notetype, with
|
|
/// the values pointing back to indexes in the original notetype.
|
|
mapForContext(ctx: MapContext): (number | null)[] {
|
|
return ctx == MapContext.Template ? this.templates ?? [] : this.fields;
|
|
}
|
|
|
|
/// Return index of old fields/templates, with null values mapped to "Nothing"
|
|
/// at the end.
|
|
getOldIndex(ctx: MapContext, newIdx: number): number {
|
|
const map = this.mapForContext(ctx);
|
|
const val = map[newIdx];
|
|
return val ?? this.getOldNamesIncludingNothing(ctx).length - 1;
|
|
}
|
|
|
|
/// Return all the old names, with "Nothing" at the end.
|
|
getOldNamesIncludingNothing(ctx: MapContext): string[] {
|
|
return [...this.getOldNames(ctx), "(Nothing)"];
|
|
}
|
|
|
|
/// Old names without "Nothing" at the end.
|
|
getOldNames(ctx: MapContext): string[] {
|
|
return ctx == MapContext.Template
|
|
? this.info.oldTemplateNames
|
|
: this.info.oldFieldNames;
|
|
}
|
|
|
|
getNewName(ctx: MapContext, idx: number): string {
|
|
return (
|
|
ctx == MapContext.Template
|
|
? this.info.newTemplateNames
|
|
: this.info.newFieldNames
|
|
)[idx];
|
|
}
|
|
|
|
unusedItems(ctx: MapContext): string[] {
|
|
const usedEntries = new Set(this.mapForContext(ctx).filter((v) => v !== null));
|
|
const oldNames = this.getOldNames(ctx);
|
|
const unusedIdxs = [...Array(oldNames.length).keys()].filter(
|
|
(idx) => !usedEntries.has(idx)
|
|
);
|
|
const unusedNames = unusedIdxs.map((idx) => oldNames[idx]);
|
|
unusedNames.sort();
|
|
return unusedNames;
|
|
}
|
|
|
|
unchanged(): boolean {
|
|
return (
|
|
this.input().newNotetypeId === this.input().oldNotetypeId &&
|
|
isEqual(this.fields, [...Array(this.fields.length).keys()]) &&
|
|
isEqual(this.templates, [...Array(this.templates?.length ?? 0).keys()])
|
|
);
|
|
}
|
|
|
|
input(): pb.BackendProto.ChangeNotetypeIn {
|
|
return this.info.input as pb.BackendProto.ChangeNotetypeIn;
|
|
}
|
|
|
|
/// Pack changes back into input message for saving.
|
|
intoInput(): pb.BackendProto.ChangeNotetypeIn {
|
|
const input = this.info.input as pb.BackendProto.ChangeNotetypeIn;
|
|
input.newFields = nullToNegativeOne(this.fields);
|
|
if (this.templates) {
|
|
input.newTemplates = nullToNegativeOne(this.templates);
|
|
}
|
|
|
|
return input;
|
|
}
|
|
}
|
|
|
|
export interface NotetypeListEntry {
|
|
idx: number;
|
|
name: string;
|
|
current: boolean;
|
|
}
|
|
|
|
export enum MapContext {
|
|
Field,
|
|
Template,
|
|
}
|
|
export class ChangeNotetypeState {
|
|
readonly info: Readable<ChangeNotetypeInfoWrapper>;
|
|
readonly notetypes: Readable<NotetypeListEntry[]>;
|
|
|
|
private info_: ChangeNotetypeInfoWrapper;
|
|
private infoSetter!: (val: ChangeNotetypeInfoWrapper) => void;
|
|
private notetypeNames: pb.BackendProto.NotetypeNames;
|
|
private notetypesSetter!: (val: NotetypeListEntry[]) => void;
|
|
|
|
constructor(
|
|
notetypes: pb.BackendProto.NotetypeNames,
|
|
info: pb.BackendProto.ChangeNotetypeInfo
|
|
) {
|
|
this.info_ = new ChangeNotetypeInfoWrapper(info);
|
|
this.info = readable(this.info_, (set) => {
|
|
this.infoSetter = set;
|
|
});
|
|
this.notetypeNames = notetypes;
|
|
this.notetypes = readable(this.buildNotetypeList(), (set) => {
|
|
this.notetypesSetter = set;
|
|
return;
|
|
});
|
|
}
|
|
|
|
async setTargetNotetypeIndex(idx: number): Promise<void> {
|
|
this.info_.input().newNotetypeId = this.notetypeNames.entries[idx].id!;
|
|
this.notetypesSetter(this.buildNotetypeList());
|
|
const newInfo = await getChangeNotetypeInfo(
|
|
this.info_.input().oldNotetypeId,
|
|
this.info_.input().newNotetypeId
|
|
);
|
|
|
|
this.info_ = new ChangeNotetypeInfoWrapper(newInfo);
|
|
this.info_.unusedItems(MapContext.Field);
|
|
this.infoSetter(this.info_);
|
|
}
|
|
|
|
setOldIndex(ctx: MapContext, newIdx: number, oldIdx: number): void {
|
|
const list = this.info_.mapForContext(ctx);
|
|
const realOldIdx = oldIdx < list.length ? oldIdx : null;
|
|
const allowDupes = ctx == MapContext.Field;
|
|
|
|
// remove any existing references?
|
|
if (!allowDupes && realOldIdx !== null) {
|
|
for (let i = 0; i < list.length; i++) {
|
|
if (list[i] === realOldIdx) {
|
|
list[i] = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
list[newIdx] = realOldIdx;
|
|
this.infoSetter(this.info_);
|
|
}
|
|
|
|
async save(): Promise<void> {
|
|
if (this.info_.unchanged()) {
|
|
alert("No changes to save");
|
|
return;
|
|
}
|
|
await changeNotetype(this.dataForSaving());
|
|
}
|
|
|
|
dataForSaving(): pb.BackendProto.ChangeNotetypeIn {
|
|
return this.info_.intoInput();
|
|
}
|
|
|
|
private buildNotetypeList(): NotetypeListEntry[] {
|
|
const currentId = this.info_.input().newNotetypeId;
|
|
return this.notetypeNames.entries.map(
|
|
(entry, idx) =>
|
|
({
|
|
idx,
|
|
name: entry.name,
|
|
current: entry.id === currentId,
|
|
} as NotetypeListEntry)
|
|
);
|
|
}
|
|
}
|