mirror of
https://github.com/panatgithub/AnkiHeadingSync.git
synced 2026-07-22 06:51:43 +00:00
chore(qa-group): remove managed model remnants
中文: 删除托管 QA Group 12 死代码,统一保留用户自选笔记模板与字段映射路径。 English: Removes dead managed QA Group 12 code and keeps user-selected note templates and field mappings as the active path.
This commit is contained in:
parent
98d64dac21
commit
b77d4b6332
14 changed files with 170 additions and 880 deletions
68
docs/remove-managed-qa-group-12-decisions.md
Normal file
68
docs/remove-managed-qa-group-12-decisions.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# Remove Managed QA Group 12 Decisions
|
||||
|
||||
## Final files to delete
|
||||
|
||||
- `src/application/config/ManagedNoteModels.ts`
|
||||
- `src/application/services/QaGroupModelDefinition.ts`
|
||||
- `src/application/services/QaGroupModelService.ts`
|
||||
- `src/application/services/QaGroupModelService.test.ts`
|
||||
|
||||
## Final gateway methods and types to remove
|
||||
|
||||
- Remove from `AnkiGroupGateway` and corresponding implementations/tests:
|
||||
- `getModelTemplates`
|
||||
- `getModelStyling`
|
||||
- `createModel`
|
||||
- `addModelField`
|
||||
- `addModelTemplate`
|
||||
- `updateModelTemplate`
|
||||
- `updateModelStyling`
|
||||
- Remove managed-model-only types:
|
||||
- `AnkiModelTemplate`
|
||||
- `CreateAnkiModelInput`
|
||||
|
||||
## Final AnkiConnectGateway cleanup
|
||||
|
||||
- Remove the AnkiConnect action wrappers that only serve the deleted managed model path:
|
||||
- `modelTemplates`
|
||||
- `modelStyling`
|
||||
- `createModel`
|
||||
- `modelFieldAdd`
|
||||
- `modelTemplateAdd`
|
||||
- `updateModelTemplates`
|
||||
- `updateModelStyling`
|
||||
|
||||
## Current QA Group path preserved
|
||||
|
||||
- Preserve `QaGroupSyncService` as the active runtime path.
|
||||
- Preserve `QaGroupFieldMappingService` as the active mapping/slot authority.
|
||||
- Preserve user-selected QA Group note types via `settings.cardTypeConfigs["qa-group"].noteType`.
|
||||
- Preserve saved mappings keyed by `qa-group:<modelName>`.
|
||||
- Preserve GI marker, slot, deck, tag, backlink, and clear-current-file behavior unchanged.
|
||||
|
||||
## Test update strategy
|
||||
|
||||
- Delete managed model service tests.
|
||||
- Remove AnkiConnectGateway tests that only cover deleted model-management methods.
|
||||
- Update remaining tests to use neutral example model names such as `QA Group List`.
|
||||
- Keep QA Group sync and QA Group field mapping tests as the regression guard for the real active path.
|
||||
|
||||
## Active wording cleanup strategy
|
||||
|
||||
- Ensure active source/tests/README/manifest do not keep the old managed QA Group 12 route.
|
||||
- Historical docs are intentionally excluded from bulk rewrite.
|
||||
|
||||
## Intentionally retained references
|
||||
|
||||
- Old QA Group 12 references in historical docs remain for audit history.
|
||||
- Generic model-reading methods still used by active settings and sync paths remain untouched:
|
||||
- `listNoteModels`
|
||||
- `getModelDetails`
|
||||
- `getModelFieldNames`
|
||||
- `getModelFieldNamesByModelNames`
|
||||
- `updateNoteModel`
|
||||
|
||||
## Intentional deviations from the plan
|
||||
|
||||
- `manifest.json` and `README.md` do not need changes in this round because current repository reality already removed the old managed QA Group 12 wording from those active files.
|
||||
- Internal QA Group sync semantics are intentionally unchanged; this round is dead-code removal only.
|
||||
79
docs/remove-managed-qa-group-12-gap-report.md
Normal file
79
docs/remove-managed-qa-group-12-gap-report.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# Remove Managed QA Group 12 Gap Report
|
||||
|
||||
## Scope
|
||||
|
||||
This report records the real repository state on 2026-04-27 before deleting the remaining dead managed QA Group model path.
|
||||
|
||||
## Active QA Group runtime path
|
||||
|
||||
- Active QA Group sync runs through `src/application/services/QaGroupSyncService.ts`.
|
||||
- The current model name is loaded from `settings.cardTypeConfigs["qa-group"].noteType`.
|
||||
- The current field mapping is loaded from `settings.noteFieldMappings[createNoteFieldMappingKey("qa-group", modelName)]`.
|
||||
- `src/application/services/QaGroupFieldMappingService.ts` remains the authority for:
|
||||
- suggested QA Group field mappings
|
||||
- saved slot derivation
|
||||
- mapping validation
|
||||
- user-selected QA Group model support
|
||||
- `src/presentation/settings/PluginSettingTab.ts` already builds and saves QA Group mappings against the selected note type.
|
||||
- There is no current runtime call from QA Group sync or settings into the managed-model service path.
|
||||
|
||||
## Files confirmed as dead production code
|
||||
|
||||
- `src/application/config/ManagedNoteModels.ts`
|
||||
- only exports the old managed model constant
|
||||
- no active production path requires it
|
||||
- `src/application/services/QaGroupModelDefinition.ts`
|
||||
- only builds the old managed model definition and templates
|
||||
- only referenced by the dead managed-model service and its tests
|
||||
- `src/application/services/QaGroupModelService.ts`
|
||||
- no active production caller found
|
||||
- only references the old model management gateway methods
|
||||
|
||||
## Gateway methods confirmed as managed-model-only
|
||||
|
||||
- In `src/application/ports/AnkiGateway.ts`, the following `AnkiGroupGateway` methods are only used by the dead managed model path:
|
||||
- `getModelTemplates`
|
||||
- `getModelStyling`
|
||||
- `createModel`
|
||||
- `addModelField`
|
||||
- `addModelTemplate`
|
||||
- `updateModelTemplate`
|
||||
- `updateModelStyling`
|
||||
- The following types are only used by those methods and related dead tests/fakes:
|
||||
- `AnkiModelTemplate`
|
||||
- `CreateAnkiModelInput`
|
||||
- Search found no other active production call site for these methods outside:
|
||||
- `QaGroupModelService.ts`
|
||||
- `AnkiConnectGateway.ts`
|
||||
- tests and test fakes
|
||||
|
||||
## Tests to delete or update
|
||||
|
||||
- Delete:
|
||||
- `src/application/services/QaGroupModelService.test.ts`
|
||||
- Update:
|
||||
- `src/infrastructure/anki/AnkiConnectGateway.test.ts`
|
||||
- remove model creation/template/styling wrapper tests
|
||||
- `src/application/services/QaGroupFieldMappingService.test.ts`
|
||||
- replace old example model name `ObsiAnki QA Group 12` with a neutral name
|
||||
- `src/test-support/manualSyncFakes.ts`
|
||||
- remove now-unused managed model gateway methods/types from the fake implementation
|
||||
|
||||
## References that may remain only in historical docs
|
||||
|
||||
- Historical docs under `docs/` still contain old QA Group 12 and managed model history.
|
||||
- These are not active product surfaces and do not need bulk rewrite in this round.
|
||||
|
||||
## Risk points
|
||||
|
||||
- `AnkiConnectGateway.ts` and `AnkiGateway.ts` must be cleaned consistently so interface and implementation stay aligned.
|
||||
- `src/test-support/manualSyncFakes.ts` must be updated with the same interface pruning or the test suite will break.
|
||||
- The active QA Group path must keep:
|
||||
- user-selected note type resolution
|
||||
- saved field mappings
|
||||
- GI marker behavior
|
||||
- slot handling
|
||||
- deck sync
|
||||
- tag sync
|
||||
- backlink behavior
|
||||
- current-file cleanup behavior
|
||||
40
main.js
40
main.js
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
export const QA_GROUP_MODEL_NAME = "ObsiAnki QA Group 12";
|
||||
|
|
@ -44,20 +44,6 @@ export interface DeckStat {
|
|||
noteCount?: number;
|
||||
}
|
||||
|
||||
export interface AnkiModelTemplate {
|
||||
name: string;
|
||||
front: string;
|
||||
back: string;
|
||||
}
|
||||
|
||||
export interface CreateAnkiModelInput {
|
||||
modelName: string;
|
||||
fieldNames: string[];
|
||||
templates: AnkiModelTemplate[];
|
||||
css: string;
|
||||
isCloze?: boolean;
|
||||
}
|
||||
|
||||
export interface AnkiNoteDetails extends AnkiNoteSummary {
|
||||
fields: Record<string, string>;
|
||||
}
|
||||
|
|
@ -86,13 +72,6 @@ export interface AnkiGateway {
|
|||
export interface AnkiGroupGateway extends AnkiGateway {
|
||||
getModelFieldNames(modelName: string): Promise<string[]>;
|
||||
getModelFieldNamesByModelNames(modelNames: string[]): Promise<Record<string, string[]>>;
|
||||
getModelTemplates(modelName: string): Promise<Record<string, AnkiModelTemplate>>;
|
||||
getModelStyling(modelName: string): Promise<string>;
|
||||
createModel(input: CreateAnkiModelInput): Promise<void>;
|
||||
addModelField(modelName: string, fieldName: string): Promise<void>;
|
||||
addModelTemplate(modelName: string, template: AnkiModelTemplate): Promise<void>;
|
||||
updateModelTemplate(modelName: string, template: AnkiModelTemplate): Promise<void>;
|
||||
updateModelStyling(modelName: string, css: string): Promise<void>;
|
||||
findNoteIds(query: string): Promise<number[]>;
|
||||
getNoteDetails(noteIds: number[]): Promise<AnkiNoteDetails[]>;
|
||||
}
|
||||
|
|
@ -68,7 +68,6 @@ describe("ManualSyncService", () => {
|
|||
|
||||
expect(result.created).toBe(1);
|
||||
expect(result.rewrittenMarkers).toBe(1);
|
||||
expect(ankiGateway.createdModels).toEqual([]);
|
||||
expect(ankiGateway.addedNotes[0]?.modelName).toBe(QA_GROUP_USER_NOTE_TYPE);
|
||||
expect(ankiGateway.addedNotes[0]?.fields).toMatchObject({
|
||||
题目: "Concepts",
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ describe("QaGroupFieldMappingService", () => {
|
|||
it("supports the legacy Stem plus Sxx_Q/Sxx_A shape as a normal user template", () => {
|
||||
const service = new QaGroupFieldMappingService();
|
||||
|
||||
const mapping = service.suggest("ObsiAnki QA Group 12", ["Stem", "S01_Q", "S01_A", "S02_Q", "S02_A"]);
|
||||
const mapping = service.suggest("QA Group List", ["Stem", "S01_Q", "S01_A", "S02_Q", "S02_A"]);
|
||||
|
||||
expect(mapping.titleField).toBe("Stem");
|
||||
expect(mapping.slots).toEqual([
|
||||
|
|
|
|||
|
|
@ -1,259 +0,0 @@
|
|||
import type { AnkiModelTemplate, CreateAnkiModelInput } from "@/application/ports/AnkiGateway";
|
||||
import {
|
||||
DEFAULT_OBSIDIAN_BACKLINK_LABEL,
|
||||
type ObsidianBacklinkPlacement,
|
||||
normalizeObsidianBacklinkLabel,
|
||||
} from "@/application/config/PluginSettings";
|
||||
import { QA_GROUP_MODEL_NAME } from "@/application/config/ManagedNoteModels";
|
||||
import type { GroupItem } from "@/domain/manual-sync/entities/IndexedGroupCardBlock";
|
||||
import { renderObsidianBacklinkAnchor } from "@/domain/shared/renderObsidianBacklink";
|
||||
|
||||
export const QA_GROUP_SLOT_COUNT = 12;
|
||||
|
||||
interface QaGroupModelDefinitionOptions {
|
||||
obsidianBacklinkLabel?: string;
|
||||
obsidianBacklinkPlacement?: ObsidianBacklinkPlacement;
|
||||
}
|
||||
|
||||
export { QA_GROUP_MODEL_NAME } from "@/application/config/ManagedNoteModels";
|
||||
|
||||
export function buildQaGroupModelDefinition(options: QaGroupModelDefinitionOptions = {}): CreateAnkiModelInput {
|
||||
const backlinkLabel = normalizeObsidianBacklinkLabel(options.obsidianBacklinkLabel ?? DEFAULT_OBSIDIAN_BACKLINK_LABEL);
|
||||
const backlinkPlacement = options.obsidianBacklinkPlacement ?? "answer-last-line";
|
||||
|
||||
return {
|
||||
modelName: QA_GROUP_MODEL_NAME,
|
||||
fieldNames: buildQaGroupFieldNames(),
|
||||
templates: buildQaGroupTemplates(backlinkLabel, backlinkPlacement),
|
||||
css: QA_GROUP_MODEL_CSS,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildQaGroupFieldNames(): string[] {
|
||||
const fieldNames = ["Stem", "GroupId", "Src"];
|
||||
|
||||
for (let slot = 1; slot <= QA_GROUP_SLOT_COUNT; slot += 1) {
|
||||
const slotId = formatQaGroupSlot(slot);
|
||||
fieldNames.push(`${slotId}_Id`, `${slotId}_Q`, `${slotId}_A`);
|
||||
}
|
||||
|
||||
return fieldNames;
|
||||
}
|
||||
|
||||
export function buildQaGroupTemplates(
|
||||
backlinkLabel = DEFAULT_OBSIDIAN_BACKLINK_LABEL,
|
||||
backlinkPlacement: ObsidianBacklinkPlacement = "answer-last-line",
|
||||
): AnkiModelTemplate[] {
|
||||
const templates: AnkiModelTemplate[] = [];
|
||||
const backlinkAnchor = renderObsidianBacklinkAnchor({
|
||||
href: "{{Src}}",
|
||||
label: backlinkLabel,
|
||||
escapeHref: false,
|
||||
});
|
||||
const backlinkLine = `{{#Src}}<p>${backlinkAnchor}</p>{{/Src}}`;
|
||||
|
||||
for (let slot = 1; slot <= QA_GROUP_SLOT_COUNT; slot += 1) {
|
||||
const slotId = formatQaGroupSlot(slot);
|
||||
templates.push({
|
||||
name: `Q${slotId.slice(1)}`,
|
||||
front: `{{#${slotId}_Q}}{{#${slotId}_A}}<div class="stem">{{Stem}}</div>\n<div class="q">{{${slotId}_Q}}</div>{{/${slotId}_A}}{{/${slotId}_Q}}`,
|
||||
back: buildQaGroupBackTemplate(slotId, backlinkLine, backlinkPlacement),
|
||||
});
|
||||
}
|
||||
|
||||
return templates;
|
||||
}
|
||||
|
||||
export function buildQaGroupNoteFields(stem: string, groupId: string, src: string, items: GroupItem[]): Record<string, string> {
|
||||
const fields = Object.fromEntries(buildQaGroupFieldNames().map((fieldName) => [fieldName, ""]));
|
||||
fields.Stem = stem;
|
||||
fields.GroupId = groupId;
|
||||
fields.Src = src;
|
||||
|
||||
for (const item of items) {
|
||||
if (!item.itemId || !item.slot) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const slotId = formatQaGroupSlot(item.slot);
|
||||
fields[`${slotId}_Id`] = item.itemId;
|
||||
fields[`${slotId}_Q`] = item.title;
|
||||
fields[`${slotId}_A`] = item.answer;
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
export function formatQaGroupSlot(slot: number): string {
|
||||
return `S${String(slot).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function buildQaGroupBackTemplate(
|
||||
slotId: string,
|
||||
backlinkLine: string,
|
||||
backlinkPlacement: ObsidianBacklinkPlacement,
|
||||
): string {
|
||||
if (backlinkPlacement === "question-last-line") {
|
||||
return `{{FrontSide}}\n${backlinkLine}\n\n<hr id="answer">\n\n<div class="a">{{${slotId}_A}}</div>`;
|
||||
}
|
||||
|
||||
if (backlinkPlacement === "answer-first-line") {
|
||||
return `{{FrontSide}}\n\n<hr id="answer">\n\n${backlinkLine}\n<div class="a">{{${slotId}_A}}</div>`;
|
||||
}
|
||||
|
||||
return `{{FrontSide}}\n\n<hr id="answer">\n\n<div class="a">{{${slotId}_A}}</div>\n${backlinkLine}`;
|
||||
}
|
||||
|
||||
export const QA_GROUP_MODEL_CSS = [
|
||||
".card {",
|
||||
" font-family: arial;",
|
||||
" font-size: 16px;",
|
||||
" text-align: left;",
|
||||
" color: black;",
|
||||
" background-color: white;",
|
||||
"}",
|
||||
"",
|
||||
".stem {",
|
||||
" margin-bottom: 0.85em;",
|
||||
"}",
|
||||
"",
|
||||
".q {",
|
||||
" margin-bottom: 0.85em;",
|
||||
"}",
|
||||
"",
|
||||
".a {",
|
||||
" margin-top: 0.85em;",
|
||||
"}",
|
||||
"",
|
||||
"em {",
|
||||
" color: #111;",
|
||||
" background-color: #69E147;",
|
||||
" border-radius: 5px;",
|
||||
" padding: 2px 5px;",
|
||||
" font-style: normal;",
|
||||
" -webkit-box-decoration-break: clone;",
|
||||
" box-decoration-break: clone;",
|
||||
"}",
|
||||
"",
|
||||
"strong {",
|
||||
" color: #111;",
|
||||
" background-color: #FFB347;",
|
||||
" border-radius: 5px;",
|
||||
" padding: 2px 5px;",
|
||||
" font-weight: 450;",
|
||||
" text-shadow: none;",
|
||||
" -webkit-box-decoration-break: clone;",
|
||||
" box-decoration-break: clone;",
|
||||
"}",
|
||||
"",
|
||||
"mark {",
|
||||
" border-radius: 5px;",
|
||||
" padding: 2px 5px;",
|
||||
"}",
|
||||
"",
|
||||
"blockquote {",
|
||||
" background-color: #ECECEC;",
|
||||
" padding: 2px 5px;",
|
||||
" border: 2px solid #7F7F7F;",
|
||||
" border-radius: 5px;",
|
||||
"}",
|
||||
"",
|
||||
"pre,",
|
||||
"code {",
|
||||
" font-family: \"JetBrains Mono\", \"Courier New\", monospace;",
|
||||
" color: #24292e;",
|
||||
" text-shadow: none;",
|
||||
" font-weight: normal;",
|
||||
" -webkit-font-smoothing: antialiased;",
|
||||
" -moz-osx-font-smoothing: grayscale;",
|
||||
"}",
|
||||
"",
|
||||
"pre em,",
|
||||
"code em,",
|
||||
"pre strong,",
|
||||
"code strong {",
|
||||
" background: none;",
|
||||
" color: inherit;",
|
||||
" padding: 0;",
|
||||
" font-style: inherit;",
|
||||
" font-weight: inherit;",
|
||||
" text-shadow: none;",
|
||||
"}",
|
||||
"",
|
||||
".hljs-comment,",
|
||||
".hljs-quote {",
|
||||
" color: #6a737d;",
|
||||
" font-style: italic;",
|
||||
"}",
|
||||
"",
|
||||
".hljs-keyword,",
|
||||
".hljs-selector-tag,",
|
||||
".hljs-name,",
|
||||
".hljs-tag {",
|
||||
" color: #d73a49;",
|
||||
"}",
|
||||
"",
|
||||
".hljs-attr {",
|
||||
" color: #6f42c1;",
|
||||
"}",
|
||||
"",
|
||||
".hljs-string {",
|
||||
" color: #032f62;",
|
||||
"}",
|
||||
"",
|
||||
".hljs-built_in,",
|
||||
".hljs-title {",
|
||||
" color: #005cc5;",
|
||||
"}",
|
||||
"",
|
||||
".hljs-literal {",
|
||||
" color: #e36209;",
|
||||
"}",
|
||||
"",
|
||||
".hljs-number {",
|
||||
" color: #005cc5;",
|
||||
"}",
|
||||
"",
|
||||
".hljs-section,",
|
||||
".hljs-selector-id {",
|
||||
" color: #22863a;",
|
||||
"}",
|
||||
"",
|
||||
".hljs-emphasis {",
|
||||
" font-style: italic;",
|
||||
"}",
|
||||
"",
|
||||
".hljs-strong {",
|
||||
" font-weight: bold;",
|
||||
"}",
|
||||
"",
|
||||
"a > code {",
|
||||
" color: inherit;",
|
||||
" text-decoration: none;",
|
||||
"}",
|
||||
"",
|
||||
".ahs-ob-tag {",
|
||||
" display: inline-block;",
|
||||
" box-sizing: border-box;",
|
||||
" max-width: 100%;",
|
||||
" margin: 0 4px 3px 0;",
|
||||
" padding: 1px 8px;",
|
||||
" border: 1px solid rgba(124, 101, 255, 0.16);",
|
||||
" border-radius: 999px;",
|
||||
" background: rgba(124, 101, 255, 0.12);",
|
||||
" color: #5b4fd6;",
|
||||
" font-size: 0.88em;",
|
||||
" font-weight: 500;",
|
||||
" line-height: 1.55;",
|
||||
" vertical-align: baseline;",
|
||||
" white-space: nowrap;",
|
||||
" text-decoration: none;",
|
||||
"}",
|
||||
"",
|
||||
".nightMode .ahs-ob-tag,",
|
||||
".card.nightMode .ahs-ob-tag {",
|
||||
" background: rgba(150, 135, 255, 0.18);",
|
||||
" color: #c9c1ff;",
|
||||
" border-color: rgba(150, 135, 255, 0.28);",
|
||||
"}",
|
||||
].join("\n");
|
||||
|
|
@ -1,189 +0,0 @@
|
|||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { FakeManualSyncAnkiGateway } from "@/test-support/manualSyncFakes";
|
||||
|
||||
import { buildQaGroupModelDefinition } from "./QaGroupModelDefinition";
|
||||
import { QaGroupModelService } from "./QaGroupModelService";
|
||||
|
||||
describe("QaGroupModelService", () => {
|
||||
it("builds the default QA Group template with a clickable backlink at the answer end", () => {
|
||||
const definition = buildQaGroupModelDefinition();
|
||||
|
||||
expect(definition.templates[0]?.back).toContain('<a class="anki-heading-sync-backlink" href="{{Src}}">Open in Obsidian</a>');
|
||||
expect(definition.templates[0]?.back).toContain('<div class="a">{{S01_A}}</div>');
|
||||
});
|
||||
|
||||
it("aligns the managed QA Group CSS with the configured QA note styling", () => {
|
||||
const definition = buildQaGroupModelDefinition();
|
||||
|
||||
expect(definition.css).toContain("font-family: arial;");
|
||||
expect(definition.css).toContain("font-size: 16px;");
|
||||
expect(definition.css).toContain("em {\n color: #111;\n background-color: #69E147;");
|
||||
expect(definition.css).toContain("strong {\n color: #111;\n background-color: #FFB347;");
|
||||
expect(definition.css).not.toContain("font-size: 22px;");
|
||||
expect(definition.css).not.toContain("font-weight: 700;");
|
||||
});
|
||||
|
||||
it("escapes a custom QA Group backlink label", () => {
|
||||
const definition = buildQaGroupModelDefinition({
|
||||
obsidianBacklinkLabel: 'Open <Obsidian> & "Now"',
|
||||
});
|
||||
|
||||
expect(definition.templates[0]?.back).toContain('Open <Obsidian> & "Now"');
|
||||
expect(definition.templates[0]?.back).not.toContain('Open <Obsidian> & "Now"');
|
||||
});
|
||||
|
||||
it("builds three different QA Group backlink placements", () => {
|
||||
const questionPlacement = buildQaGroupModelDefinition({
|
||||
obsidianBacklinkPlacement: "question-last-line",
|
||||
});
|
||||
const answerFirstPlacement = buildQaGroupModelDefinition({
|
||||
obsidianBacklinkPlacement: "answer-first-line",
|
||||
});
|
||||
const answerLastPlacement = buildQaGroupModelDefinition({
|
||||
obsidianBacklinkPlacement: "answer-last-line",
|
||||
});
|
||||
|
||||
expect(questionPlacement.templates[0]?.back).toContain('{{FrontSide}}\n{{#Src}}<p><a class="anki-heading-sync-backlink" href="{{Src}}">Open in Obsidian</a></p>{{/Src}}\n\n<hr id="answer">');
|
||||
expect(answerFirstPlacement.templates[0]?.back).toContain('<hr id="answer">\n\n{{#Src}}<p><a class="anki-heading-sync-backlink" href="{{Src}}">Open in Obsidian</a></p>{{/Src}}\n<div class="a">{{S01_A}}</div>');
|
||||
expect(answerLastPlacement.templates[0]?.back).toContain('<div class="a">{{S01_A}}</div>\n{{#Src}}<p><a class="anki-heading-sync-backlink" href="{{Src}}">Open in Obsidian</a></p>{{/Src}}');
|
||||
});
|
||||
|
||||
it("creates the QA Group model when it is missing", async () => {
|
||||
const ankiGateway = new FakeManualSyncAnkiGateway();
|
||||
const service = new QaGroupModelService(ankiGateway);
|
||||
const definition = buildQaGroupModelDefinition();
|
||||
|
||||
await service.ensureModel();
|
||||
|
||||
expect(ankiGateway.createdModels).toEqual([definition]);
|
||||
});
|
||||
|
||||
it("adds missing fields when the QA Group model already exists", async () => {
|
||||
const definition = buildQaGroupModelDefinition();
|
||||
const ankiGateway = createExistingQaGroupGateway({
|
||||
fieldNames: definition.fieldNames.filter((fieldName) => fieldName !== "S12_A"),
|
||||
});
|
||||
const service = new QaGroupModelService(ankiGateway);
|
||||
|
||||
await service.ensureModel();
|
||||
|
||||
expect(ankiGateway.addedModelFields).toEqual([
|
||||
{
|
||||
modelName: definition.modelName,
|
||||
fieldName: "S12_A",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("adds missing templates when the QA Group model already exists", async () => {
|
||||
const definition = buildQaGroupModelDefinition();
|
||||
const missingTemplate = definition.templates[definition.templates.length - 1];
|
||||
const ankiGateway = createExistingQaGroupGateway({
|
||||
templates: definition.templates.slice(0, -1),
|
||||
});
|
||||
const service = new QaGroupModelService(ankiGateway);
|
||||
|
||||
await service.ensureModel();
|
||||
|
||||
expect(ankiGateway.addedModelTemplates).toEqual([
|
||||
{
|
||||
modelName: definition.modelName,
|
||||
template: missingTemplate,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("updates drifted templates when the QA Group model already exists", async () => {
|
||||
const definition = buildQaGroupModelDefinition();
|
||||
const driftedTemplate = {
|
||||
...definition.templates[0],
|
||||
front: "drifted-front",
|
||||
};
|
||||
const ankiGateway = createExistingQaGroupGateway({
|
||||
templates: [driftedTemplate, ...definition.templates.slice(1)],
|
||||
});
|
||||
const service = new QaGroupModelService(ankiGateway);
|
||||
|
||||
await service.ensureModel();
|
||||
|
||||
expect(ankiGateway.updatedModelTemplates).toEqual([
|
||||
{
|
||||
modelName: definition.modelName,
|
||||
template: definition.templates[0],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("updates templates when the configured backlink label or placement drifts", async () => {
|
||||
const ankiGateway = createExistingQaGroupGateway();
|
||||
const service = new QaGroupModelService(ankiGateway);
|
||||
const definition = buildQaGroupModelDefinition({
|
||||
obsidianBacklinkLabel: "Open note",
|
||||
obsidianBacklinkPlacement: "question-last-line",
|
||||
});
|
||||
|
||||
await service.ensureModel({
|
||||
obsidianBacklinkLabel: "Open note",
|
||||
obsidianBacklinkPlacement: "question-last-line",
|
||||
});
|
||||
|
||||
expect(ankiGateway.updatedModelTemplates).toEqual(
|
||||
definition.templates.map((template) => ({
|
||||
modelName: definition.modelName,
|
||||
template,
|
||||
})),
|
||||
);
|
||||
});
|
||||
|
||||
it("updates drifted CSS when the QA Group model already exists", async () => {
|
||||
const definition = buildQaGroupModelDefinition();
|
||||
const ankiGateway = createExistingQaGroupGateway({
|
||||
css: ".card { color: red; }",
|
||||
});
|
||||
const service = new QaGroupModelService(ankiGateway);
|
||||
|
||||
await service.ensureModel();
|
||||
|
||||
expect(ankiGateway.updatedModelStyling).toEqual([
|
||||
{
|
||||
modelName: definition.modelName,
|
||||
css: definition.css,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("includes the action name and model name when an update step fails", async () => {
|
||||
const definition = buildQaGroupModelDefinition();
|
||||
const driftedTemplate = {
|
||||
...definition.templates[0],
|
||||
back: "drifted-back",
|
||||
};
|
||||
const ankiGateway = createExistingQaGroupGateway({
|
||||
templates: [driftedTemplate, ...definition.templates.slice(1)],
|
||||
});
|
||||
vi.spyOn(ankiGateway, "updateModelTemplate").mockRejectedValue(new Error("boom"));
|
||||
const service = new QaGroupModelService(ankiGateway);
|
||||
|
||||
await expect(service.ensureModel()).rejects.toThrow(
|
||||
`QA Group model sync failed during updateModelTemplate for "${definition.modelName}" (template=${definition.templates[0].name}): boom`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function createExistingQaGroupGateway(overrides: {
|
||||
fieldNames?: string[];
|
||||
templates?: ReturnType<typeof buildQaGroupModelDefinition>["templates"];
|
||||
css?: string;
|
||||
} = {}): FakeManualSyncAnkiGateway {
|
||||
const definition = buildQaGroupModelDefinition();
|
||||
const ankiGateway = new FakeManualSyncAnkiGateway();
|
||||
ankiGateway.modelDetailsByName[definition.modelName] = {
|
||||
fieldNames: [...(overrides.fieldNames ?? definition.fieldNames)],
|
||||
};
|
||||
ankiGateway.modelTemplatesByName[definition.modelName] = Object.fromEntries(
|
||||
(overrides.templates ?? definition.templates).map((template) => [template.name, { ...template }]),
|
||||
);
|
||||
ankiGateway.modelStylingByName[definition.modelName] = overrides.css ?? definition.css;
|
||||
return ankiGateway;
|
||||
}
|
||||
|
|
@ -1,108 +0,0 @@
|
|||
import type { PluginSettings } from "@/application/config/PluginSettings";
|
||||
import type { AnkiGroupGateway } from "@/application/ports/AnkiGateway";
|
||||
|
||||
import { buildQaGroupModelDefinition } from "./QaGroupModelDefinition";
|
||||
|
||||
export class QaGroupModelService {
|
||||
constructor(private readonly ankiGateway: AnkiGroupGateway) {}
|
||||
|
||||
async ensureModel(settings?: Pick<PluginSettings, "obsidianBacklinkLabel" | "obsidianBacklinkPlacement">): Promise<void> {
|
||||
const definition = buildQaGroupModelDefinition(settings);
|
||||
const modelNames = await this.runModelAction(
|
||||
"listNoteModels",
|
||||
definition.modelName,
|
||||
undefined,
|
||||
() => this.ankiGateway.listNoteModels(),
|
||||
);
|
||||
|
||||
if (!modelNames.includes(definition.modelName)) {
|
||||
await this.runModelAction(
|
||||
"createModel",
|
||||
definition.modelName,
|
||||
`fields=${definition.fieldNames.length}, templates=${definition.templates.length}`,
|
||||
() => this.ankiGateway.createModel(definition),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const existingFieldNames = await this.runModelAction(
|
||||
"getModelFieldNames",
|
||||
definition.modelName,
|
||||
undefined,
|
||||
() => this.ankiGateway.getModelFieldNames(definition.modelName),
|
||||
);
|
||||
|
||||
for (const fieldName of definition.fieldNames) {
|
||||
if (existingFieldNames.includes(fieldName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.runModelAction(
|
||||
"addModelField",
|
||||
definition.modelName,
|
||||
`field=${fieldName}`,
|
||||
() => this.ankiGateway.addModelField(definition.modelName, fieldName),
|
||||
);
|
||||
}
|
||||
|
||||
const existingTemplates = await this.runModelAction(
|
||||
"getModelTemplates",
|
||||
definition.modelName,
|
||||
undefined,
|
||||
() => this.ankiGateway.getModelTemplates(definition.modelName),
|
||||
);
|
||||
|
||||
for (const template of definition.templates) {
|
||||
const existingTemplate = existingTemplates[template.name];
|
||||
if (!existingTemplate) {
|
||||
await this.runModelAction(
|
||||
"addModelTemplate",
|
||||
definition.modelName,
|
||||
`template=${template.name}`,
|
||||
() => this.ankiGateway.addModelTemplate(definition.modelName, template),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existingTemplate.front !== template.front || existingTemplate.back !== template.back) {
|
||||
await this.runModelAction(
|
||||
"updateModelTemplate",
|
||||
definition.modelName,
|
||||
`template=${template.name}`,
|
||||
() => this.ankiGateway.updateModelTemplate(definition.modelName, template),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const existingCss = await this.runModelAction(
|
||||
"getModelStyling",
|
||||
definition.modelName,
|
||||
undefined,
|
||||
() => this.ankiGateway.getModelStyling(definition.modelName),
|
||||
);
|
||||
|
||||
if (existingCss !== definition.css) {
|
||||
await this.runModelAction(
|
||||
"updateModelStyling",
|
||||
definition.modelName,
|
||||
`cssLength=${definition.css.length}`,
|
||||
() => this.ankiGateway.updateModelStyling(definition.modelName, definition.css),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async runModelAction<T>(
|
||||
action: string,
|
||||
modelName: string,
|
||||
summary: string | undefined,
|
||||
operation: () => Promise<T>,
|
||||
): Promise<T> {
|
||||
try {
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
const details = summary ? ` (${summary})` : "";
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`QA Group model sync failed during ${action} for "${modelName}"${details}: ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,6 @@ describe("QaGroupSyncService", () => {
|
|||
const result = await service.sync([createIndexedGroupBlock()], createEmptyPluginState(), createModule3Settings());
|
||||
|
||||
expect(result.created).toBe(1);
|
||||
expect(ankiGateway.createdModels).toEqual([]);
|
||||
expect(ankiGateway.addedNotes[0]?.modelName).toBe(QA_GROUP_USER_NOTE_TYPE);
|
||||
expect(ankiGateway.addedNotes[0]?.fields.题目).toBe("Concepts");
|
||||
expect(ankiGateway.addedNotes[0]?.fields.问题01).toBe("Alpha");
|
||||
|
|
@ -210,7 +209,6 @@ describe("QaGroupSyncService", () => {
|
|||
obsidianBacklinkPlacement: "answer-first-line",
|
||||
}));
|
||||
|
||||
expect(ankiGateway.createdModels).toEqual([]);
|
||||
expect(ankiGateway.addedNotes[0]?.fields.答案01).toContain('Open <Vault>');
|
||||
expect(ankiGateway.addedNotes[0]?.fields.答案01).toMatch(/^<p><a class="anki-heading-sync-backlink"/);
|
||||
expect(ankiGateway.addedNotes[0]?.fields).not.toHaveProperty("Src");
|
||||
|
|
|
|||
|
|
@ -141,150 +141,6 @@ describe("AnkiConnectGateway", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("creates models with the repository's model-management contract", async () => {
|
||||
requestUrlMock.mockResolvedValue({
|
||||
json: {
|
||||
error: null,
|
||||
result: null,
|
||||
},
|
||||
});
|
||||
|
||||
const gateway = new AnkiConnectGateway(() => "http://127.0.0.1:8765");
|
||||
await gateway.createModel({
|
||||
modelName: "ObsiAnki QA Group 12",
|
||||
fieldNames: ["Stem", "GroupId"],
|
||||
css: ".card {}",
|
||||
templates: [
|
||||
{
|
||||
name: "Card 1",
|
||||
front: "{{Front}}",
|
||||
back: "{{Back}}",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(JSON.parse(requestUrlMock.mock.calls[0][0].body)).toEqual({
|
||||
action: "createModel",
|
||||
version: 6,
|
||||
params: {
|
||||
modelName: "ObsiAnki QA Group 12",
|
||||
inOrderFields: ["Stem", "GroupId"],
|
||||
css: ".card {}",
|
||||
isCloze: false,
|
||||
cardTemplates: [
|
||||
{
|
||||
Name: "Card 1",
|
||||
Front: "{{Front}}",
|
||||
Back: "{{Back}}",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("adds model fields with modelName and fieldName params", async () => {
|
||||
requestUrlMock.mockResolvedValue({
|
||||
json: {
|
||||
error: null,
|
||||
result: null,
|
||||
},
|
||||
});
|
||||
|
||||
const gateway = new AnkiConnectGateway(() => "http://127.0.0.1:8765");
|
||||
await gateway.addModelField("ObsiAnki QA Group 12", "Stem");
|
||||
|
||||
expect(JSON.parse(requestUrlMock.mock.calls[0][0].body)).toEqual({
|
||||
action: "modelFieldAdd",
|
||||
version: 6,
|
||||
params: {
|
||||
modelName: "ObsiAnki QA Group 12",
|
||||
fieldName: "Stem",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("adds model templates with templateName plus Front and Back", async () => {
|
||||
requestUrlMock.mockResolvedValue({
|
||||
json: {
|
||||
error: null,
|
||||
result: null,
|
||||
},
|
||||
});
|
||||
|
||||
const gateway = new AnkiConnectGateway(() => "http://127.0.0.1:8765");
|
||||
await gateway.addModelTemplate("ObsiAnki QA Group 12", {
|
||||
name: "Card 1",
|
||||
front: "{{Front}}",
|
||||
back: "{{Back}}",
|
||||
});
|
||||
|
||||
expect(JSON.parse(requestUrlMock.mock.calls[0][0].body)).toEqual({
|
||||
action: "modelTemplateAdd",
|
||||
version: 6,
|
||||
params: {
|
||||
modelName: "ObsiAnki QA Group 12",
|
||||
templateName: "Card 1",
|
||||
Front: "{{Front}}",
|
||||
Back: "{{Back}}",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("updates model templates with nested model params", async () => {
|
||||
requestUrlMock.mockResolvedValue({
|
||||
json: {
|
||||
error: null,
|
||||
result: null,
|
||||
},
|
||||
});
|
||||
|
||||
const gateway = new AnkiConnectGateway(() => "http://127.0.0.1:8765");
|
||||
await gateway.updateModelTemplate("ObsiAnki QA Group 12", {
|
||||
name: "Card 1",
|
||||
front: "{{Front}}",
|
||||
back: "{{Back}}",
|
||||
});
|
||||
|
||||
expect(JSON.parse(requestUrlMock.mock.calls[0][0].body)).toEqual({
|
||||
action: "updateModelTemplates",
|
||||
version: 6,
|
||||
params: {
|
||||
model: {
|
||||
name: "ObsiAnki QA Group 12",
|
||||
templates: {
|
||||
"Card 1": {
|
||||
Front: "{{Front}}",
|
||||
Back: "{{Back}}",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("updates model styling with nested model params", async () => {
|
||||
requestUrlMock.mockResolvedValue({
|
||||
json: {
|
||||
error: null,
|
||||
result: null,
|
||||
},
|
||||
});
|
||||
|
||||
const gateway = new AnkiConnectGateway(() => "http://127.0.0.1:8765");
|
||||
await gateway.updateModelStyling("ObsiAnki QA Group 12", ".card { color: red; }");
|
||||
|
||||
expect(JSON.parse(requestUrlMock.mock.calls[0][0].body)).toEqual({
|
||||
action: "updateModelStyling",
|
||||
version: 6,
|
||||
params: {
|
||||
model: {
|
||||
name: "ObsiAnki QA Group 12",
|
||||
css: ".card { color: red; }",
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("returns note existence and model summaries", async () => {
|
||||
requestUrlMock
|
||||
.mockResolvedValueOnce({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { requestUrl } from "obsidian";
|
||||
|
||||
import type { NoteModelDetails } from "@/application/dto/NoteModelDetails";
|
||||
import type { AddAnkiNoteInput, AnkiGroupGateway, AnkiModelTemplate, AnkiNoteDetails, AnkiNoteSummary, ChangeDeckInput, CreateAnkiModelInput, DeckStat, SyncAnkiNoteTagsInput, UpdateAnkiNoteInput, UpdateAnkiNoteModelInput } from "@/application/ports/AnkiGateway";
|
||||
import type { AddAnkiNoteInput, AnkiGroupGateway, AnkiNoteDetails, AnkiNoteSummary, ChangeDeckInput, DeckStat, SyncAnkiNoteTagsInput, UpdateAnkiNoteInput, UpdateAnkiNoteModelInput } from "@/application/ports/AnkiGateway";
|
||||
import type { MediaAsset } from "@/domain/card/entities/RenderedFields";
|
||||
|
||||
interface AnkiResponse<T> {
|
||||
|
|
@ -33,21 +33,6 @@ interface RawDeckStats {
|
|||
total_in_deck?: number;
|
||||
}
|
||||
|
||||
type ModelTemplates = Record<string, { Front?: string; Back?: string }>;
|
||||
|
||||
interface ModelStylingResponse {
|
||||
css?: string;
|
||||
}
|
||||
|
||||
function buildModelUpdateParams(modelName: string, updates: Record<string, unknown>): Record<string, unknown> {
|
||||
return {
|
||||
model: {
|
||||
name: modelName,
|
||||
...updates,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export class AnkiConnectGateway implements AnkiGroupGateway {
|
||||
constructor(private readonly getBaseUrl: () => string) {}
|
||||
|
||||
|
|
@ -102,69 +87,6 @@ export class AnkiConnectGateway implements AnkiGroupGateway {
|
|||
}, {});
|
||||
}
|
||||
|
||||
async getModelTemplates(modelName: string): Promise<Record<string, AnkiModelTemplate>> {
|
||||
const templates = await this.invoke<ModelTemplates>("modelTemplates", { modelName });
|
||||
return Object.fromEntries(Object.entries(templates).map(([templateName, template]) => [templateName, {
|
||||
name: templateName,
|
||||
front: template.Front ?? "",
|
||||
back: template.Back ?? "",
|
||||
}]));
|
||||
}
|
||||
|
||||
async getModelStyling(modelName: string): Promise<string> {
|
||||
const styling = await this.invoke<ModelStylingResponse | string>("modelStyling", { modelName });
|
||||
if (typeof styling === "string") {
|
||||
return styling;
|
||||
}
|
||||
|
||||
return styling.css ?? "";
|
||||
}
|
||||
|
||||
async createModel(input: CreateAnkiModelInput): Promise<void> {
|
||||
await this.invoke("createModel", {
|
||||
modelName: input.modelName,
|
||||
inOrderFields: input.fieldNames,
|
||||
css: input.css,
|
||||
isCloze: Boolean(input.isCloze),
|
||||
cardTemplates: input.templates.map((template) => ({
|
||||
Name: template.name,
|
||||
Front: template.front,
|
||||
Back: template.back,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
async addModelField(modelName: string, fieldName: string): Promise<void> {
|
||||
await this.invoke("modelFieldAdd", {
|
||||
modelName,
|
||||
fieldName,
|
||||
});
|
||||
}
|
||||
|
||||
async addModelTemplate(modelName: string, template: AnkiModelTemplate): Promise<void> {
|
||||
await this.invoke("modelTemplateAdd", {
|
||||
modelName,
|
||||
templateName: template.name,
|
||||
Front: template.front,
|
||||
Back: template.back,
|
||||
});
|
||||
}
|
||||
|
||||
async updateModelTemplate(modelName: string, template: AnkiModelTemplate): Promise<void> {
|
||||
await this.invoke("updateModelTemplates", buildModelUpdateParams(modelName, {
|
||||
templates: {
|
||||
[template.name]: {
|
||||
Front: template.front,
|
||||
Back: template.back,
|
||||
},
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async updateModelStyling(modelName: string, css: string): Promise<void> {
|
||||
await this.invoke("updateModelStyling", buildModelUpdateParams(modelName, { css }));
|
||||
}
|
||||
|
||||
async findNoteIds(query: string): Promise<number[]> {
|
||||
return this.invoke<number[]>("findNotes", { query });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { PluginSettings } from "@/application/config/PluginSettings";
|
||||
import { DEFAULT_SETTINGS } from "@/application/config/PluginSettings";
|
||||
import type { FolderTreeNode } from "@/application/dto/FolderTreeNode";
|
||||
import type { AddAnkiNoteInput, AnkiGroupGateway, AnkiModelTemplate, AnkiNoteDetails, AnkiNoteSummary, ChangeDeckInput, CreateAnkiModelInput, DeckStat, SyncAnkiNoteTagsInput, UpdateAnkiNoteInput, UpdateAnkiNoteModelInput } from "@/application/ports/AnkiGateway";
|
||||
import type { AddAnkiNoteInput, AnkiGroupGateway, AnkiNoteDetails, AnkiNoteSummary, ChangeDeckInput, DeckStat, SyncAnkiNoteTagsInput, UpdateAnkiNoteInput, UpdateAnkiNoteModelInput } from "@/application/ports/AnkiGateway";
|
||||
import type { ManualSyncVaultGateway } from "@/application/ports/ManualSyncVaultGateway";
|
||||
import type { PluginStateRepository } from "@/application/ports/PluginStateRepository";
|
||||
import { MarkdownWriteConflictError } from "@/application/ports/VaultGateway";
|
||||
|
|
@ -151,11 +151,6 @@ export class FakeManualSyncAnkiGateway implements AnkiGroupGateway {
|
|||
public noteDetailsById = new Map<number, AnkiNoteDetails>();
|
||||
public deckStatsByName = new Map<string, DeckStat>();
|
||||
public listedDeckNames: string[] | null = null;
|
||||
public createdModels: CreateAnkiModelInput[] = [];
|
||||
public addedModelFields: Array<{ modelName: string; fieldName: string }> = [];
|
||||
public addedModelTemplates: Array<{ modelName: string; template: AnkiModelTemplate }> = [];
|
||||
public updatedModelTemplates: Array<{ modelName: string; template: AnkiModelTemplate }> = [];
|
||||
public updatedModelStyling: Array<{ modelName: string; css: string }> = [];
|
||||
public foundNoteIds = new Map<string, number[]>();
|
||||
public modelDetailsByName: Record<string, NoteModelDetails> = {
|
||||
Basic: { fieldNames: ["Front", "Back"] },
|
||||
|
|
@ -164,8 +159,6 @@ export class FakeManualSyncAnkiGateway implements AnkiGroupGateway {
|
|||
fieldNames: ["题目", "问题01", "答案01", "问题02", "答案02", "问题03", "答案03"],
|
||||
},
|
||||
};
|
||||
public modelTemplatesByName: Record<string, Record<string, AnkiModelTemplate>> = {};
|
||||
public modelStylingByName: Record<string, string> = {};
|
||||
public updateNoteModelError: Error | null = null;
|
||||
|
||||
private nextNoteId = 9000;
|
||||
|
|
@ -201,53 +194,6 @@ export class FakeManualSyncAnkiGateway implements AnkiGroupGateway {
|
|||
])));
|
||||
}
|
||||
|
||||
async getModelTemplates(modelName: string): Promise<Record<string, AnkiModelTemplate>> {
|
||||
return this.modelTemplatesByName[modelName] ?? {};
|
||||
}
|
||||
|
||||
async getModelStyling(modelName: string): Promise<string> {
|
||||
return this.modelStylingByName[modelName] ?? "";
|
||||
}
|
||||
|
||||
async createModel(input: CreateAnkiModelInput): Promise<void> {
|
||||
this.createdModels.push(input);
|
||||
this.modelDetailsByName[input.modelName] = {
|
||||
fieldNames: [...input.fieldNames],
|
||||
};
|
||||
this.modelTemplatesByName[input.modelName] = Object.fromEntries(input.templates.map((template) => [template.name, { ...template }]));
|
||||
this.modelStylingByName[input.modelName] = input.css;
|
||||
}
|
||||
|
||||
async addModelField(modelName: string, fieldName: string): Promise<void> {
|
||||
this.addedModelFields.push({ modelName, fieldName });
|
||||
const existing = this.modelDetailsByName[modelName] ?? { fieldNames: [] };
|
||||
if (!existing.fieldNames.includes(fieldName)) {
|
||||
existing.fieldNames.push(fieldName);
|
||||
}
|
||||
this.modelDetailsByName[modelName] = existing;
|
||||
}
|
||||
|
||||
async addModelTemplate(modelName: string, template: AnkiModelTemplate): Promise<void> {
|
||||
this.addedModelTemplates.push({ modelName, template });
|
||||
this.modelTemplatesByName[modelName] = {
|
||||
...(this.modelTemplatesByName[modelName] ?? {}),
|
||||
[template.name]: { ...template },
|
||||
};
|
||||
}
|
||||
|
||||
async updateModelTemplate(modelName: string, template: AnkiModelTemplate): Promise<void> {
|
||||
this.updatedModelTemplates.push({ modelName, template });
|
||||
this.modelTemplatesByName[modelName] = {
|
||||
...(this.modelTemplatesByName[modelName] ?? {}),
|
||||
[template.name]: { ...template },
|
||||
};
|
||||
}
|
||||
|
||||
async updateModelStyling(modelName: string, css: string): Promise<void> {
|
||||
this.updatedModelStyling.push({ modelName, css });
|
||||
this.modelStylingByName[modelName] = css;
|
||||
}
|
||||
|
||||
async findNoteIds(query: string): Promise<number[]> {
|
||||
return [...(this.foundNoteIds.get(query) ?? [])];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue