mirror of
https://github.com/panatgithub/AnkiHeadingSync.git
synced 2026-07-22 06:51:43 +00:00
fix(settings): preserve explicit folder selections
中文: 修复设置页文件夹树半选逻辑,子文件夹选择不再自动提升为父文件夹;补充 Anki Heading Sync 插件同步路径说明。 English: Keep child folder selections explicit in the settings tree, render parent folders as indeterminate, and document the Anki Heading Sync plugin sync target.
This commit is contained in:
parent
17e22fbaa5
commit
c367a772c7
5 changed files with 131 additions and 21 deletions
9
AGENTS.md
Normal file
9
AGENTS.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Repository Notes
|
||||
|
||||
## Obsidian Plugin Sync Target
|
||||
|
||||
When syncing this plugin into the user's Obsidian vault, use this exact plugin directory:
|
||||
|
||||
`/Users/panxiaorong/Library/Mobile Documents/iCloud~md~obsidian/Documents/obsidian/.obsidian/plugins/Anki Heading Sync`
|
||||
|
||||
Copy only the built plugin files (`main.js`, `manifest.json`, `styles.css`) into that directory. Do not delete or overwrite `data.json`.
|
||||
|
|
@ -28,6 +28,20 @@ const TREE: FolderTreeNode[] = [
|
|||
},
|
||||
];
|
||||
|
||||
const SINGLE_CHILD_TREE: FolderTreeNode[] = [
|
||||
{
|
||||
path: "9Anki背诵",
|
||||
name: "9Anki背诵",
|
||||
children: [
|
||||
{
|
||||
path: "9Anki背诵/随感",
|
||||
name: "随感",
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
describe("FolderScopeTree", () => {
|
||||
it("selecting a parent folder saves the minimal parent-only selection", () => {
|
||||
expect(toggleFolderTreeSelection(TREE, [], "notes", true)).toEqual(["notes"]);
|
||||
|
|
@ -52,11 +66,56 @@ describe("FolderScopeTree", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("keeps a single selected child explicit and marks its parent indeterminate", () => {
|
||||
expect(toggleFolderTreeSelection(SINGLE_CHILD_TREE, [], "9Anki背诵/随感", true)).toEqual(["9Anki背诵/随感"]);
|
||||
|
||||
const selection = buildFolderTreeSelection(SINGLE_CHILD_TREE, ["9Anki背诵/随感"]);
|
||||
|
||||
expect(selection[0]).toMatchObject({
|
||||
path: "9Anki背诵",
|
||||
checked: false,
|
||||
indeterminate: true,
|
||||
});
|
||||
expect(selection[0]?.children[0]).toMatchObject({
|
||||
path: "9Anki背诵/随感",
|
||||
checked: true,
|
||||
indeterminate: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps fully selected sibling folders explicit instead of promoting them to the parent", () => {
|
||||
expect(compressFolderSelections(TREE, ["notes/daily", "notes/projects"])).toEqual(["notes/daily", "notes/projects"]);
|
||||
|
||||
const selection = buildFolderTreeSelection(TREE, ["notes/daily", "notes/projects"]);
|
||||
|
||||
expect(selection[0]).toMatchObject({
|
||||
path: "notes",
|
||||
checked: false,
|
||||
indeterminate: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("shows descendants as selected when their parent is explicitly selected", () => {
|
||||
const selection = buildFolderTreeSelection(TREE, ["notes"]);
|
||||
|
||||
expect(selection[0]).toMatchObject({
|
||||
path: "notes",
|
||||
checked: true,
|
||||
indeterminate: false,
|
||||
});
|
||||
expect(selection[0]?.children[0]).toMatchObject({
|
||||
path: "notes/daily",
|
||||
checked: true,
|
||||
indeterminate: false,
|
||||
});
|
||||
expect(selection[0]?.children[1]).toMatchObject({
|
||||
path: "notes/projects",
|
||||
checked: true,
|
||||
indeterminate: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("expands a selected parent when one child subtree is unchecked", () => {
|
||||
expect(toggleFolderTreeSelection(TREE, ["notes"], "notes/daily", false)).toEqual(["notes/projects"]);
|
||||
});
|
||||
|
||||
it("compresses fully selected sibling folders back to their parent", () => {
|
||||
expect(compressFolderSelections(TREE, ["notes/daily", "notes/projects"])).toEqual(["notes"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -74,13 +74,12 @@ function buildSelectionNode(
|
|||
};
|
||||
}
|
||||
|
||||
const allChildrenChecked = children.length > 0 && children.every((child) => child.checked && !child.indeterminate);
|
||||
const someChildrenChecked = children.some((child) => child.checked || child.indeterminate);
|
||||
|
||||
return {
|
||||
...node,
|
||||
checked: allChildrenChecked,
|
||||
indeterminate: someChildrenChecked && !allChildrenChecked,
|
||||
checked: false,
|
||||
indeterminate: someChildrenChecked,
|
||||
children,
|
||||
};
|
||||
}
|
||||
|
|
@ -88,33 +87,22 @@ function buildSelectionNode(
|
|||
function compressSelectionNode(
|
||||
node: FolderTreeNode,
|
||||
explicitSelections: Set<string>,
|
||||
): { fullySelected: boolean; paths: string[] } {
|
||||
): { paths: string[] } {
|
||||
if (explicitSelections.has(node.path)) {
|
||||
return {
|
||||
fullySelected: true,
|
||||
paths: [node.path],
|
||||
};
|
||||
}
|
||||
|
||||
if (node.children.length === 0) {
|
||||
return {
|
||||
fullySelected: false,
|
||||
paths: [],
|
||||
};
|
||||
}
|
||||
|
||||
const childResults = node.children.map((child) => compressSelectionNode(child, explicitSelections));
|
||||
const allChildrenSelected = childResults.length > 0 && childResults.every((child) => child.fullySelected);
|
||||
|
||||
if (allChildrenSelected) {
|
||||
return {
|
||||
fullySelected: true,
|
||||
paths: [node.path],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
fullySelected: false,
|
||||
paths: childResults.flatMap((child) => child.paths),
|
||||
};
|
||||
}
|
||||
|
|
@ -204,4 +192,4 @@ function normalizePath(path: string): string {
|
|||
|
||||
function isPathInsideFolder(filePath: string, folderPath: string): boolean {
|
||||
return filePath === folderPath || filePath.startsWith(`${folderPath}/`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -817,6 +817,7 @@ describe("AnkiHeadingSyncSettingTab", () => {
|
|||
|
||||
expect(parentCheckbox.checked).toBe(false);
|
||||
expect(parentCheckbox.indeterminate).toBe(true);
|
||||
expect((parentCheckbox as unknown as { [key: string]: string })["aria-checked"]).toBe("mixed");
|
||||
expect(queryCheckboxByPath(container, "notes/sub")).toBeUndefined();
|
||||
|
||||
await getFolderToggle(container, "notes").trigger("click");
|
||||
|
|
@ -833,6 +834,58 @@ describe("AnkiHeadingSyncSettingTab", () => {
|
|||
expect(container.textNodes).toContain("empty");
|
||||
});
|
||||
|
||||
it("keeps a single selected child folder from being promoted to its parent", async () => {
|
||||
const plugin = new FakePlugin();
|
||||
plugin.settings = {
|
||||
...plugin.settings,
|
||||
scopeMode: "include",
|
||||
};
|
||||
plugin.folderTree = [
|
||||
{
|
||||
path: "9Anki背诵",
|
||||
name: "9Anki背诵",
|
||||
children: [
|
||||
{
|
||||
path: "9Anki背诵/随感",
|
||||
name: "随感",
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
|
||||
const container = tab.containerEl as unknown as FakeContainerElInstance;
|
||||
|
||||
tab.display();
|
||||
await flushAsync();
|
||||
tab.display();
|
||||
await getFolderToggle(container, "9Anki背诵").trigger("click");
|
||||
|
||||
const childCheckbox = getCheckboxByPath(container, "9Anki背诵/随感");
|
||||
childCheckbox.checked = true;
|
||||
await childCheckbox.trigger("change");
|
||||
await flushAsync();
|
||||
|
||||
expect(plugin.settings.includeFolders).toEqual(["9Anki背诵/随感"]);
|
||||
expect(getCheckboxByPath(container, "9Anki背诵/随感").checked).toBe(true);
|
||||
expect(getCheckboxByPath(container, "9Anki背诵").checked).toBe(false);
|
||||
expect(getCheckboxByPath(container, "9Anki背诵").indeterminate).toBe(true);
|
||||
expect((getCheckboxByPath(container, "9Anki背诵") as unknown as { [key: string]: string })["aria-checked"]).toBe("mixed");
|
||||
|
||||
tab.hide();
|
||||
tab.display();
|
||||
await flushAsync();
|
||||
tab.display();
|
||||
|
||||
expect(plugin.settings.includeFolders).toEqual(["9Anki背诵/随感"]);
|
||||
expect(getCheckboxByPath(container, "9Anki背诵").checked).toBe(false);
|
||||
expect(getCheckboxByPath(container, "9Anki背诵").indeterminate).toBe(true);
|
||||
|
||||
await getFolderToggle(container, "9Anki背诵").trigger("click");
|
||||
|
||||
expect(getCheckboxByPath(container, "9Anki背诵/随感").checked).toBe(true);
|
||||
});
|
||||
|
||||
it("reloads folder tree after the settings tab is reopened and shows newly created folders", async () => {
|
||||
const plugin = new FakePlugin();
|
||||
plugin.settings = {
|
||||
|
|
|
|||
|
|
@ -751,6 +751,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
checkbox.type = "checkbox";
|
||||
checkbox.checked = node.checked;
|
||||
checkbox.indeterminate = node.indeterminate;
|
||||
checkbox.setAttr("aria-checked", node.indeterminate ? "mixed" : String(node.checked));
|
||||
checkbox.dataset.folderPath = node.path;
|
||||
checkbox.style.margin = "0";
|
||||
checkbox.addEventListener("change", () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue