mirror of
https://github.com/panatgithub/AnkiHeadingSync.git
synced 2026-07-22 06:51:43 +00:00
fix(settings): cache Anki note templates
中文: 优化设置页卡片类型布局,隐藏语义问答题与 AnkiConnect URL;允许多级列表问答题从 Anki 模板中选择笔记类型和字段。 中文: 将手动读取到的 Anki 笔记模板列表缓存到 data.json,设置页重新打开后下拉菜单可直接使用上次读取结果。 English: Refine the settings card-type layout, hide Semantic Q&A and the AnkiConnect URL, and let nested-list Q&A use selectable Anki note types and fields. English: Cache loaded Anki note templates in data.json so settings dropdowns can reuse the last loaded list after reopening.
This commit is contained in:
parent
0fdf9c8ef3
commit
06492948d9
8 changed files with 340 additions and 126 deletions
|
|
@ -1,7 +1,9 @@
|
|||
import type { CardType } from "@/domain/card/entities/RenderedFields";
|
||||
|
||||
export type NoteModelFieldMappingCardType = CardType | "qa-group";
|
||||
|
||||
export interface NoteModelFieldMapping {
|
||||
cardType: CardType;
|
||||
cardType: NoteModelFieldMappingCardType;
|
||||
modelName: string;
|
||||
loadedFieldNames: string[];
|
||||
titleField?: string;
|
||||
|
|
@ -10,6 +12,6 @@ export interface NoteModelFieldMapping {
|
|||
loadedAt: number;
|
||||
}
|
||||
|
||||
export function createNoteFieldMappingKey(cardType: CardType, modelName: string): string {
|
||||
export function createNoteFieldMappingKey(cardType: NoteModelFieldMappingCardType, modelName: string): string {
|
||||
return `${cardType}:${modelName}`;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ describe("PluginSettings", () => {
|
|||
expect(DEFAULT_SETTINGS.obsidianBacklinkPlacement).toBe("answer-last-line");
|
||||
expect(DEFAULT_SETTINGS.syncObsidianTagsToAnki).toBe(true);
|
||||
expect(DEFAULT_SETTINGS.keepPureTagLinesInCardBody).toBe(true);
|
||||
expect(DEFAULT_SETTINGS.ankiNoteTypeCache).toEqual([]);
|
||||
expect(DEFAULT_SETTINGS.cardTypeConfigs).toEqual({
|
||||
basic: {
|
||||
enabled: true,
|
||||
|
|
@ -108,6 +109,34 @@ describe("PluginSettings", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("normalizes cached Anki note types on load and save paths", () => {
|
||||
const settings = mergePluginSettings({
|
||||
ankiNoteTypeCache: [" Custom Basic ", "", "Cloze", "Custom Basic"],
|
||||
});
|
||||
|
||||
expect(settings.ankiNoteTypeCache).toEqual(["Cloze", "Custom Basic"]);
|
||||
expect(normalizePluginSettings({
|
||||
...DEFAULT_SETTINGS,
|
||||
ankiNoteTypeCache: ["Basic", " ", "Cloze", "Basic"],
|
||||
}).ankiNoteTypeCache).toEqual(["Basic", "Cloze"]);
|
||||
});
|
||||
|
||||
it("rejects invalid cached Anki note type lists", () => {
|
||||
expectPluginUserError(() => {
|
||||
validatePluginSettings({
|
||||
...DEFAULT_SETTINGS,
|
||||
ankiNoteTypeCache: "Basic" as never,
|
||||
});
|
||||
}, "errors.settings.ankiNoteTypeCacheArray");
|
||||
|
||||
expectPluginUserError(() => {
|
||||
validatePluginSettings({
|
||||
...DEFAULT_SETTINGS,
|
||||
ankiNoteTypeCache: ["Basic", 1] as never,
|
||||
});
|
||||
}, "errors.settings.ankiNoteTypeCacheStrings");
|
||||
});
|
||||
|
||||
it("fills new backlink defaults when loading legacy snapshots", () => {
|
||||
const settings = mergePluginSettings({
|
||||
qaNoteType: "Legacy Basic",
|
||||
|
|
@ -195,7 +224,7 @@ describe("PluginSettings", () => {
|
|||
})).not.toThrow();
|
||||
});
|
||||
|
||||
it("writes managed QA Group model back during normalization", () => {
|
||||
it("preserves user-selected QA Group model during normalization", () => {
|
||||
const settings = mergePluginSettings({
|
||||
cardTypeConfigs: {
|
||||
...DEFAULT_SETTINGS.cardTypeConfigs,
|
||||
|
|
@ -206,7 +235,7 @@ describe("PluginSettings", () => {
|
|||
},
|
||||
});
|
||||
|
||||
expect(settings.cardTypeConfigs["qa-group"].noteType).toBe(QA_GROUP_MODEL_NAME);
|
||||
expect(settings.cardTypeConfigs["qa-group"].noteType).toBe("Custom QA Group");
|
||||
});
|
||||
|
||||
it("rejects invalid module 5 enum values", () => {
|
||||
|
|
@ -231,4 +260,4 @@ describe("PluginSettings", () => {
|
|||
});
|
||||
}, "errors.settings.cardAnswerCutoffModeInvalid");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ export interface PluginSettings {
|
|||
semanticQaNoteType: string;
|
||||
cardTypeConfigs: CardTypeConfigs;
|
||||
noteFieldMappings: Record<string, NoteModelFieldMapping>;
|
||||
ankiNoteTypeCache: string[];
|
||||
defaultDeck: string;
|
||||
fileDeckEnabled: boolean;
|
||||
fileDeckMarker: string;
|
||||
|
|
@ -69,6 +70,7 @@ export const DEFAULT_SETTINGS: PluginSettings = {
|
|||
semanticQaNoteType: DEFAULT_SEMANTIC_QA_NOTE_TYPE,
|
||||
cardTypeConfigs: createDefaultCardTypeConfigs(),
|
||||
noteFieldMappings: {},
|
||||
ankiNoteTypeCache: [],
|
||||
defaultDeck: "Obsidian",
|
||||
fileDeckEnabled: false,
|
||||
fileDeckMarker: "TARGET DECK",
|
||||
|
|
@ -114,6 +116,7 @@ export function normalizePluginSettings(settings: PluginSettings): PluginSetting
|
|||
...settings,
|
||||
...legacySettings,
|
||||
cardTypeConfigs,
|
||||
ankiNoteTypeCache: normalizeAnkiNoteTypeCache(settings.ankiNoteTypeCache),
|
||||
obsidianBacklinkLabel: normalizeObsidianBacklinkLabel(settings.obsidianBacklinkLabel),
|
||||
};
|
||||
}
|
||||
|
|
@ -140,6 +143,7 @@ export function validatePluginSettings(settings: PluginSettings): void {
|
|||
}
|
||||
|
||||
validateNoteFieldMappings(settings.noteFieldMappings);
|
||||
validateAnkiNoteTypeCache(settings.ankiNoteTypeCache);
|
||||
|
||||
if (!settings.defaultDeck.trim()) {
|
||||
throw new PluginUserError("errors.settings.defaultDeckRequired");
|
||||
|
|
@ -193,6 +197,18 @@ export function validatePluginSettings(settings: PluginSettings): void {
|
|||
}
|
||||
}
|
||||
|
||||
function validateAnkiNoteTypeCache(ankiNoteTypeCache: string[]): void {
|
||||
if (!Array.isArray(ankiNoteTypeCache)) {
|
||||
throw new PluginUserError("errors.settings.ankiNoteTypeCacheArray");
|
||||
}
|
||||
|
||||
for (const noteType of ankiNoteTypeCache) {
|
||||
if (typeof noteType !== "string") {
|
||||
throw new PluginUserError("errors.settings.ankiNoteTypeCacheStrings");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateFolderList(folderList: string[], label: "include" | "exclude"): void {
|
||||
if (!Array.isArray(folderList)) {
|
||||
throw new PluginUserError(label === "include" ? "errors.settings.includeFoldersArray" : "errors.settings.excludeFoldersArray");
|
||||
|
|
@ -234,6 +250,10 @@ export function validateCardTypeConfigs(cardTypeConfigs: CardTypeConfigs): void
|
|||
throw new PluginUserError("errors.settings.qaNoteTypeRequired");
|
||||
}
|
||||
|
||||
if (configId === "qa-group" && !config.noteType.trim()) {
|
||||
throw new PluginUserError("errors.settings.qaNoteTypeRequired");
|
||||
}
|
||||
|
||||
if (configId === "cloze" && !config.noteType.trim()) {
|
||||
throw new PluginUserError("errors.settings.clozeNoteTypeRequired");
|
||||
}
|
||||
|
|
@ -264,7 +284,7 @@ function validateNoteFieldMappings(noteFieldMappings: Record<string, NoteModelFi
|
|||
}
|
||||
|
||||
for (const mapping of Object.values(noteFieldMappings)) {
|
||||
if (mapping.cardType !== "basic" && mapping.cardType !== "cloze" && mapping.cardType !== "semantic-qa") {
|
||||
if (mapping.cardType !== "basic" && mapping.cardType !== "qa-group" && mapping.cardType !== "cloze" && mapping.cardType !== "semantic-qa") {
|
||||
throw new PluginUserError("errors.settings.noteFieldMappingsCardType");
|
||||
}
|
||||
|
||||
|
|
@ -324,7 +344,7 @@ function createLegacyBackfilledCardTypeConfigs(settings: Partial<PluginSettings>
|
|||
...defaults["qa-group"],
|
||||
headingLevel: sanitizeHeadingLevel(settings.qaHeadingLevel, defaults["qa-group"].headingLevel),
|
||||
extraMarker: sanitizeMarker(settings.qaGroupMarker, defaults["qa-group"].extraMarker),
|
||||
noteType: QA_GROUP_MODEL_NAME,
|
||||
noteType: sanitizeNoteType(settings.cardTypeConfigs?.["qa-group"]?.noteType ?? QA_GROUP_MODEL_NAME, defaults["qa-group"].noteType),
|
||||
},
|
||||
cloze: {
|
||||
...defaults.cloze,
|
||||
|
|
@ -358,9 +378,7 @@ function mergeCardTypeConfigs(
|
|||
enabled: typeof mergedConfig.enabled === "boolean" ? mergedConfig.enabled : fallbackConfig.enabled,
|
||||
headingLevel: sanitizeHeadingLevel(mergedConfig.headingLevel, fallbackConfig.headingLevel),
|
||||
extraMarker: sanitizeMarker(mergedConfig.extraMarker, fallbackConfig.extraMarker),
|
||||
noteType: configId === "qa-group"
|
||||
? QA_GROUP_MODEL_NAME
|
||||
: sanitizeNoteType(mergedConfig.noteType, fallbackConfig.noteType),
|
||||
noteType: sanitizeNoteType(mergedConfig.noteType, fallbackConfig.noteType),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -398,4 +416,24 @@ function sanitizeNoteType(value: string | undefined, fallback: string): string {
|
|||
|
||||
const trimmedValue = value.trim();
|
||||
return trimmedValue.length > 0 ? trimmedValue : fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAnkiNoteTypeCache(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const noteTypeSet = new Set<string>();
|
||||
for (const noteType of value) {
|
||||
if (typeof noteType !== "string") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const trimmedNoteType = noteType.trim();
|
||||
if (trimmedNoteType.length > 0) {
|
||||
noteTypeSet.add(trimmedNoteType);
|
||||
}
|
||||
}
|
||||
|
||||
return [...noteTypeSet].sort((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createNoteFieldMappingKey, type NoteModelFieldMapping } from "@/application/config/NoteModelFieldMapping";
|
||||
import { createNoteFieldMappingKey, type NoteModelFieldMapping, type NoteModelFieldMappingCardType } from "@/application/config/NoteModelFieldMapping";
|
||||
import type { NoteModelDetails } from "@/application/dto/NoteModelDetails";
|
||||
import { PluginUserError } from "@/application/errors/PluginUserError";
|
||||
import { isBasicLikeCardType, type CardType, type RenderedFields } from "@/domain/card/entities/RenderedFields";
|
||||
|
|
@ -33,8 +33,8 @@ export class NoteFieldMappingService {
|
|||
return this.mapCloze(card, noteModelDetails, mapping);
|
||||
}
|
||||
|
||||
suggest(cardType: CardType, modelName: string, fieldNames: string[], loadedAt = Date.now()): NoteModelFieldMapping {
|
||||
return isBasicLikeCardType(cardType)
|
||||
suggest(cardType: NoteModelFieldMappingCardType, modelName: string, fieldNames: string[], loadedAt = Date.now()): NoteModelFieldMapping {
|
||||
return isBasicLikeMappingCardType(cardType)
|
||||
? {
|
||||
cardType,
|
||||
modelName,
|
||||
|
|
@ -57,7 +57,7 @@ export class NoteFieldMappingService {
|
|||
validateMapping(mapping: NoteModelFieldMapping, noteModelDetails: NoteModelDetails): void {
|
||||
const availableFields = new Set(noteModelDetails.fieldNames);
|
||||
|
||||
if (isBasicLikeCardType(mapping.cardType)) {
|
||||
if (isBasicLikeMappingCardType(mapping.cardType)) {
|
||||
if (!mapping.titleField || !mapping.bodyField) {
|
||||
throw new PluginUserError(getIncompleteSavedMappingKey(mapping.cardType), {
|
||||
modelName: mapping.modelName,
|
||||
|
|
@ -151,6 +151,10 @@ export class NoteFieldMappingService {
|
|||
}
|
||||
}
|
||||
|
||||
function isBasicLikeMappingCardType(cardType: NoteModelFieldMappingCardType): cardType is Extract<NoteModelFieldMappingCardType, "basic" | "qa-group" | "semantic-qa"> {
|
||||
return cardType === "qa-group" || cardType === "basic" || cardType === "semantic-qa";
|
||||
}
|
||||
|
||||
function getMissingSavedMappingKey(cardType: CardType): "errors.noteFieldMapping.missingSavedMapping.basic" | "errors.noteFieldMapping.missingSavedMapping.cloze" | "errors.noteFieldMapping.missingSavedMapping.semanticQa" {
|
||||
if (cardType === "cloze") {
|
||||
return "errors.noteFieldMapping.missingSavedMapping.cloze";
|
||||
|
|
@ -161,7 +165,7 @@ function getMissingSavedMappingKey(cardType: CardType): "errors.noteFieldMapping
|
|||
: "errors.noteFieldMapping.missingSavedMapping.basic";
|
||||
}
|
||||
|
||||
function getIncompleteSavedMappingKey(cardType: CardType): "errors.noteFieldMapping.incompleteSavedMapping.basic" | "errors.noteFieldMapping.incompleteSavedMapping.cloze" | "errors.noteFieldMapping.incompleteSavedMapping.semanticQa" {
|
||||
function getIncompleteSavedMappingKey(cardType: NoteModelFieldMappingCardType): "errors.noteFieldMapping.incompleteSavedMapping.basic" | "errors.noteFieldMapping.incompleteSavedMapping.cloze" | "errors.noteFieldMapping.incompleteSavedMapping.semanticQa" {
|
||||
if (cardType === "cloze") {
|
||||
return "errors.noteFieldMapping.incompleteSavedMapping.cloze";
|
||||
}
|
||||
|
|
@ -171,7 +175,7 @@ function getIncompleteSavedMappingKey(cardType: CardType): "errors.noteFieldMapp
|
|||
: "errors.noteFieldMapping.incompleteSavedMapping.basic";
|
||||
}
|
||||
|
||||
function getTitleBodyMustDifferKey(cardType: Extract<CardType, "basic" | "semantic-qa">): "errors.noteFieldMapping.titleBodyMustDiffer.basic" | "errors.noteFieldMapping.titleBodyMustDiffer.semanticQa" {
|
||||
function getTitleBodyMustDifferKey(cardType: Extract<NoteModelFieldMappingCardType, "basic" | "qa-group" | "semantic-qa">): "errors.noteFieldMapping.titleBodyMustDiffer.basic" | "errors.noteFieldMapping.titleBodyMustDiffer.semanticQa" {
|
||||
return cardType === "semantic-qa"
|
||||
? "errors.noteFieldMapping.titleBodyMustDiffer.semanticQa"
|
||||
: "errors.noteFieldMapping.titleBodyMustDiffer.basic";
|
||||
|
|
@ -179,4 +183,4 @@ function getTitleBodyMustDifferKey(cardType: Extract<CardType, "basic" | "semant
|
|||
|
||||
function findFieldName(fieldNames: string[], preferredNames: string[]): string | undefined {
|
||||
return fieldNames.find((fieldName) => preferredNames.some((preferredName) => preferredName.toLowerCase() === fieldName.toLowerCase()));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -215,13 +215,15 @@ export const en = {
|
|||
cards: {
|
||||
cardTypes: {
|
||||
title: "Card types and basics",
|
||||
desc: "Edit the recognition table directly. Changes save automatically and the table drives runtime recognition.",
|
||||
desc: "Edit recognition rules directly. Changes save automatically.",
|
||||
advancedHint: "Advanced: keep AnkiConnect URL here without promoting it to a separate status card.",
|
||||
markerPlaceholder: "/",
|
||||
fieldsUnavailable: "-- Read fields first --",
|
||||
autoManagedField: "Managed automatically",
|
||||
autoComposedAnswer: "Auto composed",
|
||||
qaGroupManagedNoteType: "{{modelName}} (managed automatically)",
|
||||
statusLabel: "Status: ",
|
||||
loadedSummary: "Loaded {{noteTypeCount}} note templates and configured {{configuredCount}} selected card modes.",
|
||||
loadedFieldsStatus: "Refreshed fields for {{count}} selected note types.",
|
||||
failedLoad: "Failed to load note types or fields from Anki.",
|
||||
failedSave: "Failed to save card type settings.",
|
||||
|
|
@ -238,6 +240,15 @@ export const en = {
|
|||
questionField: "Question field",
|
||||
answerField: "Answer field",
|
||||
},
|
||||
labels: {
|
||||
headingLevel: "Card marker:",
|
||||
extraMarker: "Extra marker:",
|
||||
noteType: "Anki note type:",
|
||||
questionField: "Question field:",
|
||||
answerField: "Answer field:",
|
||||
mainField: "Main field:",
|
||||
fields: "Fields:",
|
||||
},
|
||||
rows: {
|
||||
basic: "Q&A (regular paragraph)",
|
||||
qaGroup: "Q&A (nested list)",
|
||||
|
|
@ -354,6 +365,8 @@ export const en = {
|
|||
excludeFoldersArray: "Exclude folders must be an array.",
|
||||
excludeFoldersStrings: "Exclude folders must only contain strings.",
|
||||
ankiConnectUrlRequired: "AnkiConnect URL is required.",
|
||||
ankiNoteTypeCacheArray: "Anki note type cache must be an array.",
|
||||
ankiNoteTypeCacheStrings: "Anki note type cache can only contain strings.",
|
||||
noteFieldMappingsObject: "Note field mappings must be an object.",
|
||||
noteFieldMappingsCardType: "Note field mappings must use a supported card type.",
|
||||
noteFieldMappingsModelName: "Note field mappings must include a model name.",
|
||||
|
|
@ -398,4 +411,4 @@ export const en = {
|
|||
},
|
||||
} satisfies MessageTree;
|
||||
|
||||
export default en;
|
||||
export default en;
|
||||
|
|
|
|||
|
|
@ -213,13 +213,15 @@ export const zh = {
|
|||
cards: {
|
||||
cardTypes: {
|
||||
title: "卡片类型及基础设置",
|
||||
desc: "直接编辑识别表格。修改后自动保存,运行时识别也以这张表为准。",
|
||||
desc: "直接编辑识别规则,修改后自动保存。",
|
||||
advancedHint: "高级项:将 AnkiConnect URL 保留在这里,不单独做连接状态卡片。",
|
||||
markerPlaceholder: "/",
|
||||
fieldsUnavailable: "-- 请先读取字段 --",
|
||||
autoManagedField: "自动维护",
|
||||
autoComposedAnswer: "自动拼接",
|
||||
qaGroupManagedNoteType: "{{modelName}}(自动维护)",
|
||||
statusLabel: "状态:",
|
||||
loadedSummary: "已获取 {{noteTypeCount}} 个笔记模板,已选择并配置 {{configuredCount}} 个卡片模式。",
|
||||
loadedFieldsStatus: "已刷新 {{count}} 个当前所选模板的字段。",
|
||||
failedLoad: "从 Anki 读取模板或字段失败。",
|
||||
failedSave: "保存卡片类型设置失败。",
|
||||
|
|
@ -236,6 +238,15 @@ export const zh = {
|
|||
questionField: "问题字段",
|
||||
answerField: "答案字段",
|
||||
},
|
||||
labels: {
|
||||
headingLevel: "卡片标记:",
|
||||
extraMarker: "额外标记:",
|
||||
noteType: "Anki 笔记模板:",
|
||||
questionField: "问题字段:",
|
||||
answerField: "答案字段:",
|
||||
mainField: "主字段:",
|
||||
fields: "字段:",
|
||||
},
|
||||
rows: {
|
||||
basic: "问答题(常规段落形式)",
|
||||
qaGroup: "问答题(多级列表形式)",
|
||||
|
|
@ -352,6 +363,8 @@ export const zh = {
|
|||
excludeFoldersArray: "排除文件夹必须是数组。",
|
||||
excludeFoldersStrings: "排除文件夹列表中只能包含字符串。",
|
||||
ankiConnectUrlRequired: "AnkiConnect URL 不能为空。",
|
||||
ankiNoteTypeCacheArray: "Anki 笔记模板缓存必须是数组。",
|
||||
ankiNoteTypeCacheStrings: "Anki 笔记模板缓存中只能包含字符串。",
|
||||
noteFieldMappingsObject: "字段映射必须是对象。",
|
||||
noteFieldMappingsCardType: "字段映射必须使用受支持的卡片类型。",
|
||||
noteFieldMappingsModelName: "字段映射必须包含模型名称。",
|
||||
|
|
@ -396,4 +409,4 @@ export const zh = {
|
|||
},
|
||||
} satisfies typeof en;
|
||||
|
||||
export default zh;
|
||||
export default zh;
|
||||
|
|
|
|||
|
|
@ -276,7 +276,8 @@ import { AnkiHeadingSyncSettingTab } from "./PluginSettingTab";
|
|||
type FakeContainer = InstanceType<typeof FakePluginSettingTab>["containerEl"];
|
||||
type FakeSettingInstance = InstanceType<typeof FakeSetting>;
|
||||
type FakeToggleInstance = { triggerChange(value: boolean): Promise<void> };
|
||||
type QueryRoot = FakeContainer | HTMLElement;
|
||||
type FakeElementInstance = InstanceType<typeof FakeElement>;
|
||||
type QueryRoot = FakeContainer | FakeElementInstance | HTMLElement;
|
||||
|
||||
class FakePlugin {
|
||||
public readonly app = {};
|
||||
|
|
@ -441,6 +442,12 @@ function collectTexts(container: QueryRoot): string[] {
|
|||
.filter((text): text is string => Boolean(text));
|
||||
}
|
||||
|
||||
function collectOptionValues(selectEl: FakeElementInstance): string[] {
|
||||
return selectEl.children
|
||||
.filter((element) => element.tag === "option")
|
||||
.map((element) => element.value);
|
||||
}
|
||||
|
||||
function getEmptyCallCount(container: QueryRoot): number {
|
||||
return asFakeContainer(container).emptyCallCount;
|
||||
}
|
||||
|
|
@ -475,22 +482,24 @@ describe("PluginSettingTab", () => {
|
|||
expect(queryByDataset(tab.containerEl, "settingsCardBody", "deck").style.display).toBe("none");
|
||||
});
|
||||
|
||||
it("renders card 1 table with four rows and the required columns", () => {
|
||||
it("renders card 1 as three readable card type blocks", () => {
|
||||
const plugin = new FakePlugin();
|
||||
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
|
||||
|
||||
tab.display();
|
||||
|
||||
const headerTexts = findElements(tab.containerEl, (element) => element.tag === "th").map((element) => element.textContent);
|
||||
expect(headerTexts).toEqual(["启用", "卡片类型", "卡片标记", "额外标记", "Anki 笔记模板", "问题字段", "答案字段"]);
|
||||
expect(queryByDataset(tab.containerEl, "cardTypeList", "true")).toBeDefined();
|
||||
expect(findElements(tab.containerEl, (element) => element.tag === "th")).toHaveLength(0);
|
||||
expect(() => findSetting(tab.containerEl, "AnkiConnect URL")).toThrow("Setting not found");
|
||||
|
||||
const rowLabels = findElements(tab.containerEl, (element) => element.dataset.cardTypeConfig !== undefined).map((row) => row.children[1]?.textContent);
|
||||
const rowLabels = findElements(tab.containerEl, (element) => element.dataset.cardTypeConfig !== undefined)
|
||||
.map((row) => collectTexts(row).find((text) => text.includes("题")));
|
||||
expect(rowLabels).toEqual([
|
||||
"问答题(常规段落形式)",
|
||||
"问答题(多级列表形式)",
|
||||
"填空题",
|
||||
"语义问答题",
|
||||
]);
|
||||
expect(() => queryByDataset(tab.containerEl, "cardTypeMarker", "semantic-qa")).toThrow("Element not found");
|
||||
});
|
||||
|
||||
it("auto saves toggle, heading, note type and field mapping edits", async () => {
|
||||
|
|
@ -516,6 +525,11 @@ describe("PluginSettingTab", () => {
|
|||
await basicNoteType.trigger("change");
|
||||
expect(plugin.settings.cardTypeConfigs.basic.noteType).toBe("Basic");
|
||||
|
||||
const qaGroupNoteType = queryByDataset(tab.containerEl, "cardTypeNoteType", "qa-group");
|
||||
qaGroupNoteType.value = "Custom Basic";
|
||||
await qaGroupNoteType.trigger("change");
|
||||
expect(plugin.settings.cardTypeConfigs["qa-group"].noteType).toBe("Custom Basic");
|
||||
|
||||
await flushPromises();
|
||||
const basicQuestionField = queryByDataset(tab.containerEl, "cardTypeQuestionField", "basic");
|
||||
basicQuestionField.value = "Hint";
|
||||
|
|
@ -528,6 +542,18 @@ describe("PluginSettingTab", () => {
|
|||
titleField: "Hint",
|
||||
bodyField: "Body",
|
||||
}));
|
||||
|
||||
const qaGroupQuestionField = queryByDataset(tab.containerEl, "cardTypeQuestionField", "qa-group");
|
||||
qaGroupQuestionField.value = "Title";
|
||||
await qaGroupQuestionField.trigger("change");
|
||||
const qaGroupAnswerField = queryByDataset(tab.containerEl, "cardTypeAnswerField", "qa-group");
|
||||
qaGroupAnswerField.value = "Body";
|
||||
await qaGroupAnswerField.trigger("change");
|
||||
|
||||
expect(plugin.settings.noteFieldMappings[createNoteFieldMappingKey("qa-group", plugin.settings.cardTypeConfigs["qa-group"].noteType)]).toEqual(expect.objectContaining({
|
||||
titleField: "Title",
|
||||
bodyField: "Body",
|
||||
}));
|
||||
});
|
||||
|
||||
it("debounces text input saves instead of saving every keystroke", async () => {
|
||||
|
|
@ -537,10 +563,10 @@ describe("PluginSettingTab", () => {
|
|||
|
||||
tab.display();
|
||||
|
||||
const markerInput = queryByDataset(tab.containerEl, "cardTypeMarker", "semantic-qa");
|
||||
markerInput.value = "#semantic-a";
|
||||
const markerInput = queryByDataset(tab.containerEl, "cardTypeMarker", "cloze");
|
||||
markerInput.value = "#cloze-a";
|
||||
await markerInput.trigger("input");
|
||||
markerInput.value = "#semantic-ab";
|
||||
markerInput.value = "#cloze-ab";
|
||||
await markerInput.trigger("input");
|
||||
|
||||
expect(plugin.updateCalls).toHaveLength(0);
|
||||
|
|
@ -551,7 +577,7 @@ describe("PluginSettingTab", () => {
|
|||
|
||||
vi.advanceTimersByTime(1);
|
||||
await flushPromises();
|
||||
expect(plugin.settings.cardTypeConfigs["semantic-qa"].extraMarker).toBe("#semantic-ab");
|
||||
expect(plugin.settings.cardTypeConfigs.cloze.extraMarker).toBe("#cloze-ab");
|
||||
expect(plugin.updateCalls).toHaveLength(1);
|
||||
});
|
||||
|
||||
|
|
@ -597,6 +623,45 @@ describe("PluginSettingTab", () => {
|
|||
expect(getEmptyCallCount(tab.containerEl)).toBe(initialEmptyCount);
|
||||
});
|
||||
|
||||
it("persists loaded Anki note types and uses the cache before the next manual refresh", async () => {
|
||||
const plugin = new FakePlugin();
|
||||
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
|
||||
|
||||
tab.display();
|
||||
await queryByDataset(tab.containerEl, "cardTypesRefresh", "true").trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
expect(plugin.settings.ankiNoteTypeCache).toEqual([
|
||||
"Basic",
|
||||
"Cloze",
|
||||
"Custom Basic",
|
||||
"Custom Cloze",
|
||||
QA_GROUP_MODEL_NAME,
|
||||
"Semantic QA",
|
||||
]);
|
||||
expect(plugin.updateCalls.some((call) => Array.isArray(call.ankiNoteTypeCache))).toBe(true);
|
||||
|
||||
const cachedPlugin = new FakePlugin();
|
||||
cachedPlugin.settings = normalizePluginSettings({
|
||||
...cachedPlugin.settings,
|
||||
ankiNoteTypeCache: ["Basic", "Cached Basic", "Cached Cloze"],
|
||||
cardTypeConfigs: {
|
||||
...cachedPlugin.settings.cardTypeConfigs,
|
||||
basic: {
|
||||
...cachedPlugin.settings.cardTypeConfigs.basic,
|
||||
noteType: "Cached Basic",
|
||||
},
|
||||
},
|
||||
});
|
||||
const cachedTab = new AnkiHeadingSyncSettingTab(cachedPlugin as never);
|
||||
|
||||
cachedTab.display();
|
||||
|
||||
const basicNoteTypeSelect = queryByDataset(cachedTab.containerEl, "cardTypeNoteType", "basic");
|
||||
expect(collectOptionValues(basicNoteTypeSelect)).toEqual(["Basic", "Cached Basic", "Cached Cloze"]);
|
||||
expect(cachedPlugin.listNoteModelsCalls).toBe(0);
|
||||
});
|
||||
|
||||
it("folder tree expand and check refresh only the scope card", async () => {
|
||||
const plugin = new FakePlugin();
|
||||
plugin.settings = normalizePluginSettings({
|
||||
|
|
@ -637,4 +702,4 @@ describe("PluginSettingTab", () => {
|
|||
expect(plugin.settings.fileDeckEnabled).toBe(true);
|
||||
expect(getEmptyCallCount(tab.containerEl)).toBe(initialEmptyCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { PluginSettingTab, Setting } from "obsidian";
|
||||
|
||||
import { QA_GROUP_MODEL_NAME } from "@/application/config/ManagedNoteModels";
|
||||
import {
|
||||
CARD_TYPE_CONFIG_IDS,
|
||||
DEFAULT_OBSIDIAN_BACKLINK_LABEL,
|
||||
normalizePluginSettings,
|
||||
type CardAnswerCutoffMode,
|
||||
|
|
@ -13,12 +11,11 @@ import {
|
|||
type ScopeMode,
|
||||
validatePluginSettings,
|
||||
} from "@/application/config/PluginSettings";
|
||||
import { createNoteFieldMappingKey, type NoteModelFieldMapping } from "@/application/config/NoteModelFieldMapping";
|
||||
import { createNoteFieldMappingKey, type NoteModelFieldMapping, type NoteModelFieldMappingCardType } from "@/application/config/NoteModelFieldMapping";
|
||||
import type { FolderTreeNode } from "@/application/dto/FolderTreeNode";
|
||||
import type { NoteModelDetails } from "@/application/dto/NoteModelDetails";
|
||||
import { renderUserFacingMessage, toUserFacingMessage, type UserFacingMessage } from "@/application/errors/PluginUserError";
|
||||
import { NoteFieldMappingService } from "@/application/services/NoteFieldMappingService";
|
||||
import type { CardType } from "@/domain/card/entities/RenderedFields";
|
||||
import { t } from "@/presentation/i18n";
|
||||
import type AnkiHeadingSyncPlugin from "@/presentation/AnkiHeadingSyncPlugin";
|
||||
|
||||
|
|
@ -28,6 +25,7 @@ const NOTE_TYPE_STATUS_IDLE: UserFacingMessage = { key: "settings.mapping.status
|
|||
const FOLDER_TREE_STATUS_LOADING: UserFacingMessage = { key: "settings.scope.loading" };
|
||||
const TEXT_SAVE_DEBOUNCE_MS = 500;
|
||||
const SETTINGS_CARD_ORDER = ["card-types", "sync-content", "scope", "deck", "commands"] as const;
|
||||
const VISIBLE_CARD_TYPE_CONFIG_IDS = ["basic", "qa-group", "cloze"] as const;
|
||||
|
||||
type SettingsCardId = (typeof SETTINGS_CARD_ORDER)[number];
|
||||
|
||||
|
|
@ -166,6 +164,10 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
containerEl.createEl("p", { text: t("settings.cards.cardTypes.desc") });
|
||||
|
||||
const actionRow = containerEl.createDiv();
|
||||
actionRow.style.display = "flex";
|
||||
actionRow.style.flexDirection = "column";
|
||||
actionRow.style.alignItems = "flex-start";
|
||||
actionRow.style.gap = "8px";
|
||||
const loadButton = actionRow.createEl("button", {
|
||||
text: this.ankiConfigLoading
|
||||
? t("settings.cards.cardTypes.loadAnki.loading")
|
||||
|
|
@ -177,52 +179,38 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
loadButton.addEventListener("click", () => {
|
||||
void this.loadAnkiCardTypeConfig();
|
||||
});
|
||||
actionRow.createEl("span", { text: renderUserFacingMessage(this.cardTypeStatus) });
|
||||
actionRow.createEl("span", {
|
||||
text: `${t("settings.cards.cardTypes.statusLabel")}${renderUserFacingMessage(this.cardTypeStatus)}`,
|
||||
});
|
||||
|
||||
const table = containerEl.createEl("table");
|
||||
table.dataset.cardTypeTable = "true";
|
||||
const headerRow = table.createEl("tr");
|
||||
for (const columnLabel of [
|
||||
t("settings.cards.cardTypes.columns.enabled"),
|
||||
t("settings.cards.cardTypes.columns.type"),
|
||||
t("settings.cards.cardTypes.columns.headingLevel"),
|
||||
t("settings.cards.cardTypes.columns.extraMarker"),
|
||||
t("settings.cards.cardTypes.columns.noteType"),
|
||||
t("settings.cards.cardTypes.columns.questionField"),
|
||||
t("settings.cards.cardTypes.columns.answerField"),
|
||||
]) {
|
||||
headerRow.createEl("th", { text: columnLabel });
|
||||
const cardTypeList = containerEl.createDiv();
|
||||
cardTypeList.dataset.cardTypeList = "true";
|
||||
cardTypeList.style.display = "flex";
|
||||
cardTypeList.style.flexDirection = "column";
|
||||
cardTypeList.style.gap = "14px";
|
||||
|
||||
for (const configId of VISIBLE_CARD_TYPE_CONFIG_IDS) {
|
||||
this.renderCardTypeBlock(cardTypeList, configId);
|
||||
}
|
||||
|
||||
for (const configId of CARD_TYPE_CONFIG_IDS) {
|
||||
this.renderCardTypeRow(table, configId);
|
||||
}
|
||||
|
||||
containerEl.createEl("p", { text: t("settings.cards.cardTypes.advancedHint") });
|
||||
new Setting(containerEl)
|
||||
.setName(t("settings.ankiConnectUrl.name"))
|
||||
.setDesc(t("settings.ankiConnectUrl.desc"))
|
||||
.addText((text) => {
|
||||
text
|
||||
.setPlaceholder(t("settings.ankiConnectUrl.placeholder"))
|
||||
.setValue(this.getDraftValue("anki-connect-url", this.plugin.settings.ankiConnectUrl))
|
||||
.onChange((value) => {
|
||||
this.scheduleDebouncedTextSave("anki-connect-url", value, async (draftValue) => {
|
||||
const nextValue = draftValue.trim() || this.plugin.settings.ankiConnectUrl;
|
||||
await this.plugin.updateSettings({ ankiConnectUrl: nextValue });
|
||||
this.textDraftValues.delete("anki-connect-url");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private renderCardTypeRow(tableEl: HTMLElement, configId: CardTypeConfigId): void {
|
||||
private renderCardTypeBlock(containerEl: HTMLElement, configId: CardTypeConfigId): void {
|
||||
const config = this.plugin.settings.cardTypeConfigs[configId];
|
||||
const row = tableEl.createEl("tr");
|
||||
row.dataset.cardTypeConfig = configId;
|
||||
const blockEl = containerEl.createDiv();
|
||||
blockEl.dataset.cardTypeConfig = configId;
|
||||
blockEl.style.display = "flex";
|
||||
blockEl.style.flexDirection = "column";
|
||||
blockEl.style.gap = "8px";
|
||||
blockEl.style.padding = "12px";
|
||||
blockEl.style.border = "1px solid var(--background-modifier-border)";
|
||||
blockEl.style.borderRadius = "8px";
|
||||
|
||||
const enabledCell = row.createEl("td");
|
||||
const enabledCheckbox = enabledCell.createEl("input") as HTMLInputElement;
|
||||
const headerRow = blockEl.createDiv();
|
||||
headerRow.style.display = "flex";
|
||||
headerRow.style.alignItems = "center";
|
||||
headerRow.style.gap = "10px";
|
||||
|
||||
const enabledCheckbox = headerRow.createEl("input") as HTMLInputElement;
|
||||
enabledCheckbox.type = "checkbox";
|
||||
enabledCheckbox.checked = config.enabled;
|
||||
enabledCheckbox.dataset.cardTypeEnabled = configId;
|
||||
|
|
@ -230,11 +218,19 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
void this.saveCardTypeConfig(configId, { enabled: enabledCheckbox.checked });
|
||||
});
|
||||
|
||||
row.createEl("td", { text: this.getCardTypeLabel(configId) });
|
||||
const titleEl = headerRow.createEl("strong", { text: this.getCardTypeLabel(configId) });
|
||||
titleEl.style.fontSize = "var(--font-ui-medium)";
|
||||
|
||||
const headingCell = row.createEl("td");
|
||||
const headingSelect = this.createSelect(headingCell, `card-type-heading:${configId}`);
|
||||
const recognitionRow = blockEl.createDiv();
|
||||
recognitionRow.style.display = "flex";
|
||||
recognitionRow.style.flexWrap = "wrap";
|
||||
recognitionRow.style.alignItems = "center";
|
||||
recognitionRow.style.gap = "10px 14px";
|
||||
|
||||
const headingGroup = this.createInlineControlGroup(recognitionRow, t("settings.cards.cardTypes.labels.headingLevel"));
|
||||
const headingSelect = this.createSelect(headingGroup, `card-type-heading:${configId}`);
|
||||
headingSelect.dataset.cardTypeHeading = configId;
|
||||
headingSelect.style.width = "88px";
|
||||
for (let level = 1; level <= 6; level += 1) {
|
||||
this.appendOption(headingSelect, String(level), `H${level}`);
|
||||
}
|
||||
|
|
@ -243,11 +239,12 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
void this.saveCardTypeConfig(configId, { headingLevel: Number(headingSelect.value) });
|
||||
});
|
||||
|
||||
const markerCell = row.createEl("td");
|
||||
const markerInput = markerCell.createEl("input") as HTMLInputElement;
|
||||
const markerGroup = this.createInlineControlGroup(recognitionRow, t("settings.cards.cardTypes.labels.extraMarker"));
|
||||
const markerInput = markerGroup.createEl("input") as HTMLInputElement;
|
||||
markerInput.type = "text";
|
||||
markerInput.dataset.cardTypeMarker = configId;
|
||||
markerInput.placeholder = t("settings.cards.cardTypes.markerPlaceholder");
|
||||
markerInput.style.width = "220px";
|
||||
markerInput.value = this.getDraftValue(`card-type-marker:${configId}`, config.extraMarker);
|
||||
markerInput.addEventListener("input", () => {
|
||||
this.scheduleDebouncedTextSave(`card-type-marker:${configId}`, markerInput.value, async (draftValue) => {
|
||||
|
|
@ -258,59 +255,57 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
});
|
||||
});
|
||||
|
||||
const noteTypeCell = row.createEl("td");
|
||||
if (configId === "qa-group") {
|
||||
const noteTypeText = noteTypeCell.createEl("span", {
|
||||
text: t("settings.cards.cardTypes.qaGroupManagedNoteType", { modelName: QA_GROUP_MODEL_NAME }),
|
||||
});
|
||||
noteTypeText.dataset.cardTypeNoteType = configId;
|
||||
} else {
|
||||
const noteTypeSelect = this.createSelect(noteTypeCell, `card-type-note-type:${configId}`);
|
||||
noteTypeSelect.dataset.cardTypeNoteType = configId;
|
||||
for (const noteModel of this.getSelectableNoteModels(config.noteType)) {
|
||||
this.appendOption(noteTypeSelect, noteModel, noteModel);
|
||||
}
|
||||
noteTypeSelect.value = config.noteType;
|
||||
noteTypeSelect.disabled = this.ankiConfigLoading;
|
||||
noteTypeSelect.addEventListener("change", () => {
|
||||
void this.saveCardTypeConfig(configId, { noteType: noteTypeSelect.value });
|
||||
});
|
||||
}
|
||||
const ankiRow = blockEl.createDiv();
|
||||
ankiRow.style.display = "grid";
|
||||
ankiRow.style.gridTemplateColumns = "max-content 260px max-content 160px max-content 160px";
|
||||
ankiRow.style.alignItems = "center";
|
||||
ankiRow.style.columnGap = "12px";
|
||||
ankiRow.style.rowGap = "8px";
|
||||
ankiRow.style.overflow = "hidden";
|
||||
|
||||
this.renderCardTypeFieldCells(row, configId);
|
||||
const noteTypeGroup = this.createGridControlSlot(ankiRow, t("settings.cards.cardTypes.labels.noteType"));
|
||||
const noteTypeSelect = this.createSelect(noteTypeGroup, `card-type-note-type:${configId}`);
|
||||
noteTypeSelect.dataset.cardTypeNoteType = configId;
|
||||
this.applyEllipsisWidth(noteTypeSelect, "260px");
|
||||
for (const noteModel of this.getSelectableNoteModels(config.noteType)) {
|
||||
this.appendOption(noteTypeSelect, noteModel, noteModel);
|
||||
}
|
||||
noteTypeSelect.value = config.noteType;
|
||||
noteTypeSelect.disabled = this.ankiConfigLoading;
|
||||
noteTypeSelect.addEventListener("change", () => {
|
||||
void this.saveCardTypeConfig(configId, { noteType: noteTypeSelect.value });
|
||||
});
|
||||
|
||||
this.renderCardTypeFieldControls(ankiRow, configId);
|
||||
}
|
||||
|
||||
private renderCardTypeFieldCells(rowEl: HTMLElement, configId: CardTypeConfigId): void {
|
||||
const questionCell = rowEl.createEl("td");
|
||||
const answerCell = rowEl.createEl("td");
|
||||
|
||||
if (configId === "qa-group") {
|
||||
questionCell.createEl("span", { text: t("settings.cards.cardTypes.autoManagedField") });
|
||||
answerCell.createEl("span", { text: t("settings.cards.cardTypes.autoManagedField") });
|
||||
return;
|
||||
}
|
||||
|
||||
private renderCardTypeFieldControls(containerEl: HTMLElement, configId: CardTypeConfigId): void {
|
||||
const mapping = this.getCurrentMappingForConfig(configId);
|
||||
if (configId === "cloze") {
|
||||
const mainFieldSelect = this.createFieldSelect(questionCell, `card-type-question-field:${configId}`);
|
||||
const mainFieldGroup = this.createGridControlSlot(containerEl, t("settings.cards.cardTypes.labels.mainField"));
|
||||
const mainFieldSelect = this.createFieldSelect(mainFieldGroup, `card-type-question-field:${configId}`);
|
||||
mainFieldSelect.dataset.cardTypeQuestionField = configId;
|
||||
this.applyEllipsisWidth(mainFieldSelect, "180px");
|
||||
this.populateFieldSelect(mainFieldSelect, mapping?.loadedFieldNames ?? [], mapping?.mainField);
|
||||
mainFieldSelect.addEventListener("change", () => {
|
||||
void this.saveFieldMapping(configId, { mainField: mainFieldSelect.value || undefined });
|
||||
});
|
||||
answerCell.createEl("span", { text: t("settings.cards.cardTypes.autoComposedAnswer") });
|
||||
return;
|
||||
}
|
||||
|
||||
const titleFieldSelect = this.createFieldSelect(questionCell, `card-type-question-field:${configId}`);
|
||||
const titleFieldGroup = this.createGridControlSlot(containerEl, t("settings.cards.cardTypes.labels.questionField"));
|
||||
const titleFieldSelect = this.createFieldSelect(titleFieldGroup, `card-type-question-field:${configId}`);
|
||||
titleFieldSelect.dataset.cardTypeQuestionField = configId;
|
||||
this.applyEllipsisWidth(titleFieldSelect, "160px");
|
||||
this.populateFieldSelect(titleFieldSelect, mapping?.loadedFieldNames ?? [], mapping?.titleField);
|
||||
titleFieldSelect.addEventListener("change", () => {
|
||||
void this.saveFieldMapping(configId, { titleField: titleFieldSelect.value || undefined });
|
||||
});
|
||||
|
||||
const bodyFieldSelect = this.createFieldSelect(answerCell, `card-type-answer-field:${configId}`);
|
||||
const bodyFieldGroup = this.createGridControlSlot(containerEl, t("settings.cards.cardTypes.labels.answerField"));
|
||||
const bodyFieldSelect = this.createFieldSelect(bodyFieldGroup, `card-type-answer-field:${configId}`);
|
||||
bodyFieldSelect.dataset.cardTypeAnswerField = configId;
|
||||
this.applyEllipsisWidth(bodyFieldSelect, "160px");
|
||||
this.populateFieldSelect(bodyFieldSelect, mapping?.loadedFieldNames ?? [], mapping?.bodyField);
|
||||
bodyFieldSelect.addEventListener("change", () => {
|
||||
void this.saveFieldMapping(configId, { bodyField: bodyFieldSelect.value || undefined });
|
||||
|
|
@ -606,7 +601,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
return true;
|
||||
}
|
||||
|
||||
private async saveFieldMapping(configId: Exclude<CardTypeConfigId, "qa-group">, partialMapping: Partial<NoteModelFieldMapping>): Promise<void> {
|
||||
private async saveFieldMapping(configId: CardTypeConfigId, partialMapping: Partial<NoteModelFieldMapping>): Promise<void> {
|
||||
const mapping = this.getCurrentMappingForConfig(configId);
|
||||
const runtimeCardType = this.getRuntimeCardType(configId);
|
||||
const modelName = this.plugin.settings.cardTypeConfigs[configId].noteType;
|
||||
|
|
@ -637,20 +632,32 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
try {
|
||||
const noteModels = await this.plugin.listNoteModels();
|
||||
this.availableNoteModels.splice(0, this.availableNoteModels.length, ...[...noteModels].sort((left, right) => left.localeCompare(right)));
|
||||
const selectedConfigs = CARD_TYPE_CONFIG_IDS.filter((configId) => configId !== "qa-group") as Array<Exclude<CardTypeConfigId, "qa-group">>;
|
||||
await this.plugin.updateSettings({ ankiNoteTypeCache: this.getAvailableNoteModels() });
|
||||
const selectedConfigs = [...VISIBLE_CARD_TYPE_CONFIG_IDS];
|
||||
let configuredCount = 0;
|
||||
|
||||
for (const configId of selectedConfigs) {
|
||||
const runtimeCardType = this.getRuntimeCardType(configId);
|
||||
const modelName = this.plugin.settings.cardTypeConfigs[configId].noteType;
|
||||
const mappingKey = createNoteFieldMappingKey(runtimeCardType, modelName);
|
||||
const modelDetails = await this.plugin.getNoteModelDetails(modelName);
|
||||
let modelDetails: NoteModelDetails;
|
||||
try {
|
||||
modelDetails = await this.plugin.getNoteModelDetails(modelName);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.loadedModelDetails[mappingKey] = modelDetails;
|
||||
this.seedDraftMapping(runtimeCardType, modelName, modelDetails);
|
||||
configuredCount += 1;
|
||||
}
|
||||
|
||||
this.cardTypeStatus = {
|
||||
rawMessage: `${t("settings.mapping.status.loadedCount", { count: this.availableNoteModels.length })} ${t("settings.cards.cardTypes.loadedFieldsStatus", { count: selectedConfigs.length })}`,
|
||||
key: "settings.cards.cardTypes.loadedSummary",
|
||||
params: {
|
||||
noteTypeCount: this.availableNoteModels.length,
|
||||
configuredCount,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
this.cardTypeStatus = toUserFacingMessage(error, "settings.cards.cardTypes.failedLoad");
|
||||
|
|
@ -660,7 +667,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
}
|
||||
}
|
||||
|
||||
private seedDraftMapping(runtimeCardType: CardType, modelName: string, modelDetails: NoteModelDetails): void {
|
||||
private seedDraftMapping(runtimeCardType: NoteModelFieldMappingCardType, modelName: string, modelDetails: NoteModelDetails): void {
|
||||
const mappingKey = createNoteFieldMappingKey(runtimeCardType, modelName);
|
||||
const currentMapping = this.plugin.settings.noteFieldMappings[mappingKey] ?? this.draftMappings[mappingKey];
|
||||
|
||||
|
|
@ -677,7 +684,9 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
}
|
||||
|
||||
private getSelectableNoteModels(selectedModelName: string): string[] {
|
||||
const noteModels = this.availableNoteModels.length > 0 ? [...this.availableNoteModels] : [];
|
||||
const noteModels = this.availableNoteModels.length > 0
|
||||
? this.getAvailableNoteModels()
|
||||
: [...this.plugin.settings.ankiNoteTypeCache];
|
||||
if (!noteModels.includes(selectedModelName)) {
|
||||
noteModels.unshift(selectedModelName);
|
||||
}
|
||||
|
|
@ -685,7 +694,11 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
return noteModels;
|
||||
}
|
||||
|
||||
private getCurrentMappingForConfig(configId: Exclude<CardTypeConfigId, "qa-group">): NoteModelFieldMapping | undefined {
|
||||
private getAvailableNoteModels(): string[] {
|
||||
return [...this.availableNoteModels];
|
||||
}
|
||||
|
||||
private getCurrentMappingForConfig(configId: CardTypeConfigId): NoteModelFieldMapping | undefined {
|
||||
const runtimeCardType = this.getRuntimeCardType(configId);
|
||||
const modelName = this.plugin.settings.cardTypeConfigs[configId].noteType;
|
||||
const mappingKey = createNoteFieldMappingKey(runtimeCardType, modelName);
|
||||
|
|
@ -711,7 +724,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
};
|
||||
}
|
||||
|
||||
private createFallbackMapping(runtimeCardType: CardType, modelName: string): NoteModelFieldMapping {
|
||||
private createFallbackMapping(runtimeCardType: NoteModelFieldMappingCardType, modelName: string): NoteModelFieldMapping {
|
||||
const mappingKey = createNoteFieldMappingKey(runtimeCardType, modelName);
|
||||
const modelDetails = this.loadedModelDetails[mappingKey];
|
||||
|
||||
|
|
@ -727,7 +740,11 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
};
|
||||
}
|
||||
|
||||
private getRuntimeCardType(configId: Exclude<CardTypeConfigId, "qa-group">): CardType {
|
||||
private getRuntimeCardType(configId: CardTypeConfigId): NoteModelFieldMapping["cardType"] {
|
||||
if (configId === "qa-group") {
|
||||
return "qa-group";
|
||||
}
|
||||
|
||||
return configId === "cloze" ? "cloze" : configId === "semantic-qa" ? "semantic-qa" : "basic";
|
||||
}
|
||||
|
||||
|
|
@ -825,6 +842,39 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
}
|
||||
}
|
||||
|
||||
private createInlineControlGroup(containerEl: HTMLElement, label: string): HTMLElement {
|
||||
const groupEl = containerEl.createEl("label");
|
||||
groupEl.style.display = "inline-flex";
|
||||
groupEl.style.alignItems = "center";
|
||||
groupEl.style.gap = "6px";
|
||||
groupEl.style.flexWrap = "nowrap";
|
||||
|
||||
const labelEl = groupEl.createEl("span", { text: label });
|
||||
labelEl.style.whiteSpace = "nowrap";
|
||||
|
||||
return groupEl;
|
||||
}
|
||||
|
||||
private createGridControlSlot(containerEl: HTMLElement, label: string): HTMLElement {
|
||||
const labelEl = containerEl.createEl("span", { text: label });
|
||||
labelEl.style.whiteSpace = "nowrap";
|
||||
|
||||
const slotEl = containerEl.createDiv();
|
||||
slotEl.style.minWidth = "0";
|
||||
slotEl.style.overflow = "hidden";
|
||||
|
||||
return slotEl;
|
||||
}
|
||||
|
||||
private applyEllipsisWidth(element: HTMLElement, width: string): void {
|
||||
element.style.width = width;
|
||||
element.style.maxWidth = width;
|
||||
element.style.minWidth = width;
|
||||
element.style.overflow = "hidden";
|
||||
element.style.textOverflow = "ellipsis";
|
||||
element.style.whiteSpace = "nowrap";
|
||||
}
|
||||
|
||||
private scheduleDebouncedTextSave(key: string, value: string, saveAction: (draftValue: string) => Promise<void>): void {
|
||||
this.textDraftValues.set(key, value);
|
||||
|
||||
|
|
@ -958,4 +1008,4 @@ function getScopeModeTreeDescription(scopeMode: ScopeMode): string {
|
|||
}
|
||||
|
||||
return t("settings.scope.treeDescription.exclude");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue