mirror of
https://github.com/panatgithub/AnkiHeadingSync.git
synced 2026-07-22 06:51:43 +00:00
fix(settings): always refresh folder tree on display to show newly-created folders
This commit is contained in:
parent
cb968b1d17
commit
f0b8063270
2 changed files with 99 additions and 9 deletions
|
|
@ -13,11 +13,13 @@ const {
|
|||
class HoistedFakeElement {
|
||||
public readonly children: HoistedFakeElement[] = [];
|
||||
public readonly dataset: Record<string, string> = {};
|
||||
public readonly style: Record<string, string> = {};
|
||||
public checked = false;
|
||||
public indeterminate = false;
|
||||
public type = "";
|
||||
public value = "";
|
||||
public text = "";
|
||||
public textContent = "";
|
||||
|
||||
private readonly listeners = new Map<string, Array<() => void | Promise<void>>>();
|
||||
|
||||
|
|
@ -349,6 +351,28 @@ function getCheckboxByPath(containerEl: FakeContainerElInstance, folderPath: str
|
|||
return checkbox;
|
||||
}
|
||||
|
||||
function queryFolderToggle(containerEl: FakeContainerElInstance, folderPath: string): FakeElementInstance | undefined {
|
||||
return findElement(containerEl, (element) => element.dataset.folderToggle === folderPath);
|
||||
}
|
||||
|
||||
function getFolderToggle(containerEl: FakeContainerElInstance, folderPath: string): FakeElementInstance {
|
||||
const toggle = queryFolderToggle(containerEl, folderPath);
|
||||
if (!toggle) {
|
||||
throw new Error(`Toggle not found for folder path: ${folderPath}`);
|
||||
}
|
||||
|
||||
return toggle;
|
||||
}
|
||||
|
||||
function getFolderRow(containerEl: FakeContainerElInstance, folderPath: string): FakeElementInstance {
|
||||
const row = findElement(containerEl, (element) => element.dataset.folderRow === folderPath);
|
||||
if (!row) {
|
||||
throw new Error(`Row not found for folder path: ${folderPath}`);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
function findElement(
|
||||
root: FakeElementInstance,
|
||||
predicate: (element: FakeElementInstance) => boolean,
|
||||
|
|
@ -452,7 +476,7 @@ describe("AnkiHeadingSyncSettingTab", () => {
|
|||
expect(queryCheckboxByPath(container, "notes")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("shows the folder tree in include mode and rehydrates saved selections after redisplay", async () => {
|
||||
it("shows the folder tree as a collapsed hierarchy and reveals children after expanding a parent", async () => {
|
||||
const plugin = new FakePlugin();
|
||||
plugin.settings = {
|
||||
...plugin.settings,
|
||||
|
|
@ -467,11 +491,22 @@ describe("AnkiHeadingSyncSettingTab", () => {
|
|||
tab.display();
|
||||
|
||||
const parentCheckbox = getCheckboxByPath(container, "notes");
|
||||
const childCheckbox = getCheckboxByPath(container, "notes/sub");
|
||||
|
||||
expect(parentCheckbox.checked).toBe(false);
|
||||
expect(parentCheckbox.indeterminate).toBe(true);
|
||||
expect(queryCheckboxByPath(container, "notes/sub")).toBeUndefined();
|
||||
|
||||
await getFolderToggle(container, "notes").trigger("click");
|
||||
|
||||
const childCheckbox = getCheckboxByPath(container, "notes/sub");
|
||||
const parentRow = getFolderRow(container, "notes");
|
||||
const childRow = getFolderRow(container, "notes/sub");
|
||||
|
||||
expect(childCheckbox.checked).toBe(true);
|
||||
expect(parentRow.dataset.folderDepth).toBe("0");
|
||||
expect(childRow.dataset.folderDepth).toBe("1");
|
||||
expect(parentRow.style.paddingLeft).toBe("0px");
|
||||
expect(childRow.style.paddingLeft).toBe("18px");
|
||||
expect(container.textNodes).toContain("empty");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,9 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
private folderTree: FolderTreeNode[] = [];
|
||||
private folderTreeStatus = FOLDER_TREE_STATUS_LOADING;
|
||||
private folderTreeLoadPromise: Promise<void> | null = null;
|
||||
private hasLoadedFolderTree = false;
|
||||
// removed cached 'hasLoadedFolderTree' so the settings UI can refresh
|
||||
// the folder list when displayed (ensures newly-created Obsidian folders appear).
|
||||
private readonly expandedFolderPaths = new Set<string>();
|
||||
|
||||
constructor(plugin: AnkiHeadingSyncPlugin) {
|
||||
super(plugin.app, plugin);
|
||||
|
|
@ -423,42 +425,86 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
const selectedFolders = settings.scopeMode === "include" ? settings.includeFolders : settings.excludeFolders;
|
||||
const selectionTree = buildFolderTreeSelection(this.folderTree, selectedFolders);
|
||||
const treeContainer = scopeContainer.createDiv();
|
||||
treeContainer.style.marginTop = "8px";
|
||||
treeContainer.style.display = "flex";
|
||||
treeContainer.style.flexDirection = "column";
|
||||
treeContainer.style.gap = "2px";
|
||||
|
||||
for (const node of selectionTree) {
|
||||
this.renderFolderNode(treeContainer, node, settings.scopeMode);
|
||||
this.renderFolderNode(treeContainer, node, settings.scopeMode, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private renderFolderNode(containerEl: HTMLElement, node: FolderTreeSelectionNode, scopeMode: ScopeMode): void {
|
||||
private renderFolderNode(containerEl: HTMLElement, node: FolderTreeSelectionNode, scopeMode: ScopeMode, depth: number): void {
|
||||
const row = containerEl.createDiv();
|
||||
row.dataset.folderRow = node.path;
|
||||
row.dataset.folderDepth = String(depth);
|
||||
row.style.display = "flex";
|
||||
row.style.alignItems = "center";
|
||||
row.style.gap = "6px";
|
||||
row.style.minHeight = "24px";
|
||||
row.style.paddingLeft = `${depth * 18}px`;
|
||||
|
||||
const hasChildren = node.children.length > 0;
|
||||
const expanded = hasChildren && this.expandedFolderPaths.has(node.path);
|
||||
const toggleControl = row.createEl(hasChildren ? "button" : "span");
|
||||
toggleControl.dataset.folderToggle = node.path;
|
||||
toggleControl.textContent = hasChildren ? (expanded ? "▾" : "▸") : "";
|
||||
toggleControl.style.width = "18px";
|
||||
toggleControl.style.display = "inline-flex";
|
||||
toggleControl.style.alignItems = "center";
|
||||
toggleControl.style.justifyContent = "center";
|
||||
toggleControl.style.flexShrink = "0";
|
||||
toggleControl.style.padding = "0";
|
||||
toggleControl.style.border = "0";
|
||||
toggleControl.style.background = "transparent";
|
||||
toggleControl.style.color = "var(--text-muted)";
|
||||
toggleControl.style.cursor = hasChildren ? "pointer" : "default";
|
||||
|
||||
if (hasChildren) {
|
||||
toggleControl.setAttr("aria-label", expanded ? `收起 ${node.name}` : `展开 ${node.name}`);
|
||||
toggleControl.setAttr("aria-expanded", String(expanded));
|
||||
toggleControl.addEventListener("click", () => {
|
||||
this.toggleFolderExpanded(node.path);
|
||||
});
|
||||
}
|
||||
|
||||
const checkbox = row.createEl("input") as HTMLInputElement;
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = node.checked;
|
||||
checkbox.indeterminate = node.indeterminate;
|
||||
checkbox.dataset.folderPath = node.path;
|
||||
checkbox.style.margin = "0";
|
||||
checkbox.addEventListener("change", () => {
|
||||
void this.updateFolderSelection(scopeMode, node.path, checkbox.checked);
|
||||
});
|
||||
|
||||
const label = row.createEl("span", { text: node.name });
|
||||
label.dataset.folderPathLabel = node.path;
|
||||
label.style.userSelect = "none";
|
||||
|
||||
if (node.children.length === 0) {
|
||||
if (!hasChildren || !expanded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const childrenContainer = containerEl.createDiv();
|
||||
childrenContainer.dataset.folderChildren = node.path;
|
||||
childrenContainer.style.display = "flex";
|
||||
childrenContainer.style.flexDirection = "column";
|
||||
childrenContainer.style.gap = "2px";
|
||||
for (const child of node.children) {
|
||||
this.renderFolderNode(childrenContainer, child, scopeMode);
|
||||
this.renderFolderNode(childrenContainer, child, scopeMode, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private ensureFolderTreeLoaded(): void {
|
||||
if (this.hasLoadedFolderTree || this.folderTreeLoadPromise) {
|
||||
if (this.folderTreeLoadPromise) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.folderTreeStatus = FOLDER_TREE_STATUS_LOADING;
|
||||
// Always re-fetch the folder tree when requested (no permanent cache).
|
||||
// Avoid concurrent loads using folderTreeLoadPromise.
|
||||
this.folderTreeLoadPromise = this.plugin
|
||||
.listFolderTree()
|
||||
.then((folderTree) => {
|
||||
|
|
@ -470,7 +516,6 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
this.folderTreeStatus = error instanceof Error ? error.message : "读取 vault 文件夹失败。";
|
||||
})
|
||||
.finally(() => {
|
||||
this.hasLoadedFolderTree = true;
|
||||
this.folderTreeLoadPromise = null;
|
||||
this.display();
|
||||
});
|
||||
|
|
@ -488,6 +533,16 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
await this.plugin.updateSettings(scopeMode === "include" ? { includeFolders: nextSelection } : { excludeFolders: nextSelection });
|
||||
this.display();
|
||||
}
|
||||
|
||||
private toggleFolderExpanded(folderPath: string): void {
|
||||
if (this.expandedFolderPaths.has(folderPath)) {
|
||||
this.expandedFolderPaths.delete(folderPath);
|
||||
} else {
|
||||
this.expandedFolderPaths.add(folderPath);
|
||||
}
|
||||
|
||||
this.display();
|
||||
}
|
||||
}
|
||||
|
||||
function getScopeModeSummary(scopeMode: ScopeMode): string {
|
||||
|
|
|
|||
Loading…
Reference in a new issue