mirror of
https://github.com/panatgithub/AnkiHeadingSync.git
synced 2026-07-22 06:51:43 +00:00
fix(settings): refine plugin run scope tree UI
中文: 将范围设置统一为插件运行范围,并把文件夹选择区调整为对齐稳定的 Finder 式树形 UI。 English: Renames the scope settings to plugin run scope and refines the folder selector into a stable Finder-style tree UI.
This commit is contained in:
parent
354e25b4ae
commit
23b9b83527
7 changed files with 327 additions and 24 deletions
89
docs/plugin-run-scope-ui-decisions.md
Normal file
89
docs/plugin-run-scope-ui-decisions.md
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# Plugin Run Scope UI Decisions
|
||||
|
||||
## 1. 最终文案决策
|
||||
|
||||
范围卡最终统一为:
|
||||
|
||||
1. 中文卡片标题:`插件运行范围`
|
||||
2. 英文卡片标题:`Plugin run scope`
|
||||
3. 中文范围下拉名称:`插件运行范围`
|
||||
4. 英文范围下拉名称:`Plugin run scope`
|
||||
|
||||
范围卡顶部旧说明不再渲染。
|
||||
|
||||
## 2. 文件夹树行布局决策
|
||||
|
||||
每个树行固定为三列:
|
||||
|
||||
1. 展开槽
|
||||
2. checkbox
|
||||
3. 文件夹名
|
||||
|
||||
布局固定为:
|
||||
|
||||
1. `display: grid`
|
||||
2. `grid-template-columns: 24px 24px minmax(0, 1fr)`
|
||||
3. `align-items: center`
|
||||
4. 固定最小高度
|
||||
|
||||
缩进只由 `depth * 18px` 控制,使用行容器的左边距实现。
|
||||
|
||||
## 3. 展开槽与文件夹名决策
|
||||
|
||||
展开槽规则固定为:
|
||||
|
||||
1. 有子节点时用轻量按钮
|
||||
2. 无子节点时仍渲染占位元素
|
||||
3. 两种情况下都保留 `folderToggle` dataset
|
||||
4. 展开符号继续使用 `▸ / ▾`
|
||||
|
||||
文件夹名规则固定为:
|
||||
|
||||
1. 单行显示
|
||||
2. `overflow: hidden`
|
||||
3. `text-overflow: ellipsis`
|
||||
4. `white-space: nowrap`
|
||||
|
||||
## 4. 已选祖先自动展开策略
|
||||
|
||||
补充展开已选祖先的时机固定为:
|
||||
|
||||
1. 文件夹树首次成功加载后
|
||||
2. 作用范围切换到 `include` / `exclude` 后
|
||||
3. 手动刷新文件夹树后
|
||||
4. 设置页关闭后再次进入时
|
||||
|
||||
不在以下场景强制重置:
|
||||
|
||||
1. 用户手动点击展开/折叠后的普通重渲染
|
||||
2. 勾选/取消勾选文件夹后的普通重渲染
|
||||
|
||||
实现方式是一次性“待补充展开”标志位,而不是每次 render 都强行根据已选项重算展开状态。
|
||||
|
||||
## 5. refresh 行为决策
|
||||
|
||||
`refreshFolderTree()` 保持现有语义:
|
||||
|
||||
1. 清空旧展开状态
|
||||
2. 重新加载树
|
||||
3. 加载完成后再次补充展开已选祖先
|
||||
|
||||
## 6. 保持不变的内容
|
||||
|
||||
本次明确保持不变:
|
||||
|
||||
1. `all / include / exclude` 的业务含义
|
||||
2. 设置字段名和枚举值
|
||||
3. `FolderScopeTree` 的选择压缩算法
|
||||
4. 作用范围对同步逻辑的影响
|
||||
5. 设置页局部刷新机制
|
||||
6. 不新增 CSS 构建链路
|
||||
|
||||
## 7. 与计划的显式偏差
|
||||
|
||||
没有行为偏差。
|
||||
|
||||
仅有一个实现级说明:
|
||||
|
||||
1. 当前仓库已经有安全的 built-plugin sync 脚本
|
||||
2. 因此最终同步继续复用 `build:obsidian` 和现有脚本,而不是再引入新的同步实现
|
||||
103
docs/plugin-run-scope-ui-gap-report.md
Normal file
103
docs/plugin-run-scope-ui-gap-report.md
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
# Plugin Run Scope UI Gap Report
|
||||
|
||||
## 审查范围
|
||||
|
||||
本次按真实仓库实现审查了以下位置:
|
||||
|
||||
1. [src/presentation/settings/PluginSettingTab.ts](src/presentation/settings/PluginSettingTab.ts)
|
||||
2. [src/presentation/i18n/messages/zh.ts](src/presentation/i18n/messages/zh.ts)
|
||||
3. [src/presentation/i18n/messages/en.ts](src/presentation/i18n/messages/en.ts)
|
||||
4. [src/presentation/settings/PluginSettingTab.test.ts](src/presentation/settings/PluginSettingTab.test.ts)
|
||||
5. [src/presentation/settings/FolderScopeTree.test.ts](src/presentation/settings/FolderScopeTree.test.ts)
|
||||
6. [scripts/sync-plugin-dist.mjs](scripts/sync-plugin-dist.mjs)
|
||||
7. [scripts/obsidian-plugin-path.mjs](scripts/obsidian-plugin-path.mjs)
|
||||
|
||||
## 当前实现确认
|
||||
|
||||
### 1. 范围卡文案仍是旧版本
|
||||
|
||||
当前范围卡使用:
|
||||
|
||||
1. 卡片标题 `卡片同步范围`
|
||||
2. 范围下拉名称 `运行范围`
|
||||
3. 顶部说明 `保留现有作用范围和文件夹树逻辑;文件夹树按需加载,并支持局部刷新。`
|
||||
|
||||
与本轮目标的 `插件运行范围` 不一致。
|
||||
|
||||
### 2. 范围卡顶部说明目前仍会渲染
|
||||
|
||||
`renderScopeCard()` 当前第一行就是:
|
||||
|
||||
1. `containerEl.createEl("p", { text: t("settings.cards.scope.desc") })`
|
||||
|
||||
这会直接把旧说明文案渲染到页面上。
|
||||
|
||||
### 3. 当前文件夹树行结构还不是固定三列
|
||||
|
||||
`renderFolderNode()` 当前实现是:
|
||||
|
||||
1. 行容器只用 `paddingLeft = depth * 18px`
|
||||
2. 行内依次直接放 toggle、checkbox、label
|
||||
3. 没有固定宽度的展开槽
|
||||
4. 没有稳定的三列 grid
|
||||
5. label 也没有单行省略样式
|
||||
|
||||
结果是:
|
||||
|
||||
1. 无子节点时展开槽不具备明确占位样式
|
||||
2. checkbox 对齐依赖默认 inline 布局
|
||||
3. 长文件夹名可能撑乱排版
|
||||
|
||||
### 4. 当前没有“已选祖先自动展开”逻辑
|
||||
|
||||
当前展开状态只来自:
|
||||
|
||||
1. `expandedFolderPaths`
|
||||
2. 用户点击 `folderToggle`
|
||||
|
||||
当前没有:
|
||||
|
||||
1. 初次加载树后自动展开已选项祖先
|
||||
2. 切换到 include / exclude 后补充展开已选祖先
|
||||
3. refresh 后重新展开已选祖先
|
||||
|
||||
因此如果 `includeFolders = ["notes/sub"]`,打开范围卡后 `notes` 不会自动展开。
|
||||
|
||||
### 5. 现有局部刷新行为已经满足复用条件
|
||||
|
||||
真实代码已经具备以下可复用行为:
|
||||
|
||||
1. `refreshFolderTree()` 只重渲染 scope card
|
||||
2. `updateScopeMode()` 只重渲染 scope card
|
||||
3. `updateFolderSelection()` 只重渲染 scope card
|
||||
4. `FolderScopeTree.test.ts` 已经覆盖选择压缩算法
|
||||
|
||||
这意味着本次只需要改 UI 结构与补充展开策略,不需要动选择压缩逻辑。
|
||||
|
||||
### 6. build/sync 脚本当前已满足目标目录安全要求
|
||||
|
||||
当前 [scripts/sync-plugin-dist.mjs](scripts/sync-plugin-dist.mjs) 只会复制:
|
||||
|
||||
1. `dist/plugin/main.js`
|
||||
2. `dist/plugin/manifest.json`
|
||||
3. `dist/plugin/styles.css`(如果存在)
|
||||
|
||||
不会删除目标目录文件,也不会触碰 `data.json`。
|
||||
|
||||
## 建议的最小落点
|
||||
|
||||
1. 只改 `PluginSettingTab.ts` 的范围卡渲染与树行样式
|
||||
2. 更新 `settings.cards.scope.title` 与 `settings.scope.name`
|
||||
3. 删除范围卡顶部说明的渲染,但不改业务逻辑
|
||||
4. 给树行改为固定三列 grid,并保留现有 dataset
|
||||
5. 增加“按需补充展开已选祖先”的一次性标志位
|
||||
6. 在 `PluginSettingTab.test.ts` 补标题、旧说明消失、树行对齐、已选祖先展开测试
|
||||
|
||||
## 与计划的显式偏差
|
||||
|
||||
没有产品级偏差。
|
||||
|
||||
唯一需要记录的是实现层面的仓库现实:
|
||||
|
||||
1. 当前 sync 脚本已经是安全复制 built plugin 文件的版本
|
||||
2. 因此本轮不需要再改 build/sync 脚本,只需要继续按它的产物约束验证和同步
|
||||
26
main.js
26
main.js
File diff suppressed because one or more lines are too long
|
|
@ -191,7 +191,7 @@ export const en = {
|
|||
priorityDesc: "Final deck priority: file-level custom deck > folder-mapped deck > default deck. Existing-card deck behavior stays aligned with the current sync implementation.",
|
||||
},
|
||||
scope: {
|
||||
name: "Run scope",
|
||||
name: "Plugin run scope",
|
||||
summary: {
|
||||
all: "Process Markdown files across the whole vault",
|
||||
include: "Only process Markdown files in the checked folders below",
|
||||
|
|
@ -284,7 +284,7 @@ export const en = {
|
|||
},
|
||||
},
|
||||
scope: {
|
||||
title: "Card sync scope",
|
||||
title: "Plugin run scope",
|
||||
desc: "Keep the existing scope modes and folder tree. The tree is loaded lazily and can be refreshed locally.",
|
||||
refreshFolders: "Refresh folder list",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ export const zh = {
|
|||
priorityDesc: "最终 deck 优先级:文件级自定义牌组 > 文件夹映射 deck > 默认牌组。旧卡的 deck 行为保持与当前同步实现一致。",
|
||||
},
|
||||
scope: {
|
||||
name: "运行范围",
|
||||
name: "插件运行范围",
|
||||
summary: {
|
||||
all: "处理整个 vault 中的 Markdown 文件",
|
||||
include: "仅处理下方勾选文件夹中的 Markdown 文件",
|
||||
|
|
@ -282,7 +282,7 @@ export const zh = {
|
|||
},
|
||||
},
|
||||
scope: {
|
||||
title: "卡片同步范围",
|
||||
title: "插件运行范围",
|
||||
desc: "保留现有作用范围和文件夹树逻辑;文件夹树按需加载,并支持局部刷新。",
|
||||
refreshFolders: "刷新文件夹列表",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1292,6 +1292,45 @@ describe("PluginSettingTab", () => {
|
|||
expect(queryByDataset(tab.containerEl, "qaGroupWarning", "qa-group").textContent).toContain("第 03 组缺少答案字段");
|
||||
});
|
||||
|
||||
it("renders the scope card with renamed copy and aligned folder tree rows", async () => {
|
||||
const plugin = new FakePlugin();
|
||||
plugin.settings = normalizePluginSettings({
|
||||
...plugin.settings,
|
||||
scopeMode: "include",
|
||||
includeFolders: ["notes/sub"],
|
||||
});
|
||||
const tab = new AnkiHeadingSyncSettingTab(plugin as never);
|
||||
|
||||
tab.display();
|
||||
expect(queryByDataset(tab.containerEl, "settingsCardToggle", "scope").textContent).toContain("插件运行范围");
|
||||
|
||||
await queryByDataset(tab.containerEl, "settingsCardToggle", "scope").trigger("click");
|
||||
await flushPromises();
|
||||
|
||||
expect(findSetting(tab.containerEl, "插件运行范围")).toBeDefined();
|
||||
expect(() => findSetting(tab.containerEl, "运行范围")).toThrow("Setting not found");
|
||||
expect(collectTexts(tab.containerEl)).not.toContain("保留现有作用范围和文件夹树逻辑;文件夹树按需加载,并支持局部刷新。");
|
||||
|
||||
const parentRow = queryByDataset(tab.containerEl, "folderRow", "notes");
|
||||
const parentToggle = queryByDataset(tab.containerEl, "folderToggle", "notes");
|
||||
const childRow = queryByDataset(tab.containerEl, "folderRow", "notes/sub");
|
||||
const childToggle = queryByDataset(tab.containerEl, "folderToggle", "notes/sub");
|
||||
const childCheckbox = queryByDataset(tab.containerEl, "folderPath", "notes/sub");
|
||||
const childLabel = queryByDataset(tab.containerEl, "folderPathLabel", "notes/sub");
|
||||
|
||||
expect(parentToggle.textContent).toBe("▾");
|
||||
expect(parentRow.style.display).toBe("grid");
|
||||
expect(parentRow.style.gridTemplateColumns).toBe("24px 24px minmax(0, 1fr)");
|
||||
expect(parentRow.style.alignItems).toBe("center");
|
||||
expect(childRow.style.marginLeft).toBe("18px");
|
||||
expect(childToggle.style.width).toBe("24px");
|
||||
expect(childToggle.style.height).toBe("24px");
|
||||
expect(childCheckbox).toBeDefined();
|
||||
expect(childLabel.style.whiteSpace).toBe("nowrap");
|
||||
expect(childLabel.style.overflow).toBe("hidden");
|
||||
expect(childLabel.style.textOverflow).toBe("ellipsis");
|
||||
});
|
||||
|
||||
it("folder tree expand and check refresh only the scope card", async () => {
|
||||
const plugin = new FakePlugin();
|
||||
plugin.settings = normalizePluginSettings({
|
||||
|
|
@ -1307,6 +1346,9 @@ describe("PluginSettingTab", () => {
|
|||
await flushPromises();
|
||||
expect(plugin.listFolderTreeCalls).toBe(1);
|
||||
expect(getEmptyCallCount(tab.containerEl)).toBe(initialEmptyCount);
|
||||
expect(queryByDataset(tab.containerEl, "folderRow", "notes")).toBeDefined();
|
||||
expect(queryByDataset(tab.containerEl, "folderToggle", "notes")).toBeDefined();
|
||||
expect(queryByDataset(tab.containerEl, "folderPathLabel", "notes")).toBeDefined();
|
||||
|
||||
await queryByDataset(tab.containerEl, "folderToggle", "notes").trigger("click");
|
||||
expect(getEmptyCallCount(tab.containerEl)).toBe(initialEmptyCount);
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
private detectedAnkiNoteTypeCache: string[] | null = null;
|
||||
private noteTypeCacheCheckToken = 0;
|
||||
private pageHeaderResizeObserver: ResizeObserver | null = null;
|
||||
private scopeNeedsSelectedAncestorExpansion = true;
|
||||
|
||||
constructor(plugin: AnkiHeadingSyncPlugin) {
|
||||
super(plugin.app, plugin);
|
||||
|
|
@ -101,6 +102,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
this.noteTypeCacheCheckPromise = null;
|
||||
this.detectedAnkiNoteTypeCache = null;
|
||||
this.noteTypeCacheCheckToken += 1;
|
||||
this.scopeNeedsSelectedAncestorExpansion = true;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
|
|
@ -593,8 +595,6 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
}
|
||||
|
||||
private renderScopeCard(containerEl: HTMLElement): void {
|
||||
containerEl.createEl("p", { text: t("settings.cards.scope.desc") });
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("settings.scope.name"))
|
||||
.setDesc(getScopeModeSummary(this.plugin.settings.scopeMode))
|
||||
|
|
@ -634,12 +634,15 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
return;
|
||||
}
|
||||
|
||||
const selectedFolders = this.plugin.settings.scopeMode === "include"
|
||||
? this.plugin.settings.includeFolders
|
||||
: this.plugin.settings.excludeFolders;
|
||||
this.expandSelectedFolderAncestorsIfNeeded(this.plugin.settings.scopeMode);
|
||||
|
||||
const selectedFolders = this.getSelectedFoldersForScopeMode(this.plugin.settings.scopeMode);
|
||||
const selectionTree = buildFolderTreeSelection(this.folderTree, selectedFolders);
|
||||
const treeContainer = containerEl.createDiv();
|
||||
treeContainer.dataset.scopeTree = "true";
|
||||
treeContainer.style.display = "flex";
|
||||
treeContainer.style.flexDirection = "column";
|
||||
treeContainer.style.gap = "2px";
|
||||
|
||||
for (const node of selectionTree) {
|
||||
this.renderFolderNode(treeContainer, node, this.plugin.settings.scopeMode, 0);
|
||||
|
|
@ -1329,6 +1332,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
.then((folderTree) => {
|
||||
this.folderTree = folderTree;
|
||||
this.folderTreeStatus = folderTree.length > 0 ? { rawMessage: "" } : { key: "settings.scope.empty" };
|
||||
this.requestSelectedFolderAncestorExpansion();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.folderTree = [];
|
||||
|
|
@ -1350,6 +1354,7 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
private async updateScopeMode(scopeMode: ScopeMode): Promise<void> {
|
||||
await this.plugin.updateSettings({ scopeMode });
|
||||
if (scopeMode !== "all") {
|
||||
this.requestSelectedFolderAncestorExpansion();
|
||||
this.ensureFolderTreeLoaded();
|
||||
}
|
||||
this.renderCard("scope");
|
||||
|
|
@ -1366,14 +1371,36 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
private renderFolderNode(containerEl: HTMLElement, node: FolderTreeSelectionNode, scopeMode: ScopeMode, depth: number): void {
|
||||
const row = containerEl.createDiv();
|
||||
row.dataset.folderRow = node.path;
|
||||
row.style.paddingLeft = `${depth * 18}px`;
|
||||
row.style.display = "grid";
|
||||
row.style.gridTemplateColumns = "24px 24px minmax(0, 1fr)";
|
||||
row.style.alignItems = "center";
|
||||
row.style.columnGap = "0px";
|
||||
row.style.minHeight = "28px";
|
||||
row.style.marginLeft = `${depth * 18}px`;
|
||||
row.style.width = "100%";
|
||||
row.style.boxSizing = "border-box";
|
||||
|
||||
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 = "24px";
|
||||
toggleControl.style.height = "24px";
|
||||
toggleControl.style.display = "inline-flex";
|
||||
toggleControl.style.alignItems = "center";
|
||||
toggleControl.style.justifyContent = "center";
|
||||
toggleControl.style.padding = "0";
|
||||
toggleControl.style.margin = "0";
|
||||
toggleControl.style.border = "0";
|
||||
toggleControl.style.background = "transparent";
|
||||
toggleControl.style.boxShadow = "none";
|
||||
toggleControl.style.lineHeight = "1";
|
||||
toggleControl.style.color = "var(--text-muted)";
|
||||
if (hasChildren) {
|
||||
toggleControl.setAttr("aria-label", expanded ? t("settings.scope.collapseFolder", { name: node.name }) : t("settings.scope.expandFolder", { name: node.name }));
|
||||
toggleControl.setAttr("title", expanded ? t("settings.scope.collapseFolder", { name: node.name }) : t("settings.scope.expandFolder", { name: node.name }));
|
||||
toggleControl.style.cursor = "pointer";
|
||||
toggleControl.addEventListener("click", () => {
|
||||
if (this.expandedFolderPaths.has(node.path)) {
|
||||
this.expandedFolderPaths.delete(node.path);
|
||||
|
|
@ -1390,11 +1417,18 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
checkbox.checked = node.checked;
|
||||
checkbox.indeterminate = node.indeterminate;
|
||||
checkbox.dataset.folderPath = node.path;
|
||||
checkbox.style.margin = "0";
|
||||
checkbox.style.justifySelf = "center";
|
||||
checkbox.addEventListener("change", () => {
|
||||
void this.updateFolderSelection(scopeMode, node.path, checkbox.checked);
|
||||
});
|
||||
|
||||
row.createEl("span", { text: node.name }).dataset.folderPathLabel = node.path;
|
||||
const labelEl = row.createEl("span", { text: node.name });
|
||||
labelEl.dataset.folderPathLabel = node.path;
|
||||
labelEl.style.minWidth = "0";
|
||||
labelEl.style.overflow = "hidden";
|
||||
labelEl.style.textOverflow = "ellipsis";
|
||||
labelEl.style.whiteSpace = "nowrap";
|
||||
|
||||
if (!hasChildren || !expanded) {
|
||||
return;
|
||||
|
|
@ -1402,11 +1436,35 @@ export class AnkiHeadingSyncSettingTab extends PluginSettingTab {
|
|||
|
||||
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, depth + 1);
|
||||
}
|
||||
}
|
||||
|
||||
private requestSelectedFolderAncestorExpansion(): void {
|
||||
this.scopeNeedsSelectedAncestorExpansion = true;
|
||||
}
|
||||
|
||||
private expandSelectedFolderAncestorsIfNeeded(scopeMode: ScopeMode): void {
|
||||
if (!this.scopeNeedsSelectedAncestorExpansion) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scopeNeedsSelectedAncestorExpansion = false;
|
||||
for (const folderPath of this.getSelectedFoldersForScopeMode(scopeMode)) {
|
||||
for (const ancestorPath of getAncestorFolderPaths(folderPath)) {
|
||||
this.expandedFolderPaths.add(ancestorPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getSelectedFoldersForScopeMode(scopeMode: ScopeMode): string[] {
|
||||
return scopeMode === "include" ? this.plugin.settings.includeFolders : this.plugin.settings.excludeFolders;
|
||||
}
|
||||
|
||||
private createInlineControlGroup(containerEl: HTMLElement, label: string): HTMLElement {
|
||||
const groupEl = containerEl.createEl("label");
|
||||
groupEl.style.display = "inline-flex";
|
||||
|
|
@ -1616,3 +1674,14 @@ function getScopeModeTreeDescription(scopeMode: ScopeMode): string {
|
|||
|
||||
return t("settings.scope.treeDescription.exclude");
|
||||
}
|
||||
|
||||
function getAncestorFolderPaths(folderPath: string): string[] {
|
||||
const segments = folderPath.split("/").filter((segment) => segment.length > 0);
|
||||
const ancestorPaths: string[] = [];
|
||||
|
||||
for (let index = 1; index < segments.length; index += 1) {
|
||||
ancestorPaths.push(segments.slice(0, index).join("/"));
|
||||
}
|
||||
|
||||
return ancestorPaths;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue