feat(web): 集中项目管理与文件导航
This commit is contained in:
parent
f748052775
commit
c754afcb7b
|
|
@ -6,6 +6,10 @@
|
||||||
> 开发中的用户文案可先写入 `## Unreleased`;该区不会被前端解析,正式发布时再替换为数字版本和日期。
|
> 开发中的用户文案可先写入 `## Unreleased`;该区不会被前端解析,正式发布时再替换为数字版本和日期。
|
||||||
> 工程口径的完整记录见 `PROGRESS.md` / git log。
|
> 工程口径的完整记录见 `PROGRESS.md` / git log。
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
- 项目管理集中到左侧:支持直接创建、重命名、查看文件和安全移除空项目;右侧入口改为“当前项目 / 全部文件”,专注项目内及跨项目文件浏览。
|
||||||
|
|
||||||
## 0.72.1 — 2026-09-04
|
## 0.72.1 — 2026-09-04
|
||||||
|
|
||||||
- 左侧导航改为“项目→对话”分组,并保留最近对话和筛选结果;右侧文件范围改用“当前项目 / 全部项目”表达,更容易看清同一项目下的多个对话和共享资料。
|
- 左侧导航改为“项目→对话”分组,并保留最近对话和筛选结果;右侧文件范围改用“当前项目 / 全部项目”表达,更容易看清同一项目下的多个对话和共享资料。
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ const filesJs = fs.readFileSync("web/static/js/files.js", "utf8");
|
||||||
const layoutJs = fs.readFileSync("web/static/js/layout.js", "utf8");
|
const layoutJs = fs.readFileSync("web/static/js/layout.js", "utf8");
|
||||||
const jobsJs = fs.readFileSync("web/static/js/software_jobs.js", "utf8");
|
const jobsJs = fs.readFileSync("web/static/js/software_jobs.js", "utf8");
|
||||||
const previewJs = fs.readFileSync("web/static/js/preview.js", "utf8");
|
const previewJs = fs.readFileSync("web/static/js/preview.js", "utf8");
|
||||||
|
const newTaskJs = fs.readFileSync("web/static/js/newtask.js", "utf8");
|
||||||
const pageHtml = fs.readFileSync("web/static/dev.html", "utf8");
|
const pageHtml = fs.readFileSync("web/static/dev.html", "utf8");
|
||||||
|
|
||||||
test("file menu exposes an explicit confirmed replacement action", () => {
|
test("file menu exposes an explicit confirmed replacement action", () => {
|
||||||
|
|
@ -24,7 +25,7 @@ test("ordinary uploads default to rename conflict policy", () => {
|
||||||
|
|
||||||
test("file workspace exposes explicit task and all-files contexts", () => {
|
test("file workspace exposes explicit task and all-files contexts", () => {
|
||||||
assert.match(pageHtml, /id="files-scope-task"[^>]*>当前项目</);
|
assert.match(pageHtml, /id="files-scope-task"[^>]*>当前项目</);
|
||||||
assert.match(pageHtml, /id="files-scope-all"[^>]*>全部项目</);
|
assert.match(pageHtml, /id="files-scope-all"[^>]*>全部文件</);
|
||||||
assert.match(filesJs, /state\.filesScope = "task"/);
|
assert.match(filesJs, /state\.filesScope = "task"/);
|
||||||
assert.match(filesJs, /state\.filesScope = "all"/);
|
assert.match(filesJs, /state\.filesScope = "all"/);
|
||||||
});
|
});
|
||||||
|
|
@ -39,6 +40,17 @@ test("task navigation groups chats by project with restrained icon sizes", () =>
|
||||||
assert.match(chatJs, /working_dir: projectName/);
|
assert.match(chatJs, /working_dir: projectName/);
|
||||||
assert.match(chatJs, /state\.taskHasMore = filtered &&/);
|
assert.match(chatJs, /state\.taskHasMore = filtered &&/);
|
||||||
assert.match(chatJs, /task-project-more/);
|
assert.match(chatJs, /task-project-more/);
|
||||||
|
assert.match(chatJs, /task-project-create/);
|
||||||
|
assert.doesNotMatch(chatJs, /if \(!folder\.n_tasks\) continue/);
|
||||||
|
assert.match(chatJs, /renameProjectFolder/);
|
||||||
|
assert.match(chatJs, /removeProjectFolder/);
|
||||||
|
assert.match(chatJs, /openProjectFiles/);
|
||||||
|
assert.match(filesJs, /function createProjectFolder/);
|
||||||
|
assert.match(filesJs, /function removeProjectFolder/);
|
||||||
|
assert.match(chatJs, /folders-refresh-requested/);
|
||||||
|
assert.match(newTaskJs, /folders-refresh-requested/);
|
||||||
|
assert.match(filesJs, /setRightPanelTab\("files"\)/);
|
||||||
|
assert.match(filesJs, /recursive: entries\.length > 0/);
|
||||||
assert.match(pageHtml, /\.task-nav-icon \{ width: 15px; height: 15px;/);
|
assert.match(pageHtml, /\.task-nav-icon \{ width: 15px; height: 15px;/);
|
||||||
assert.match(pageHtml, /\.task-project-head \.task-nav-icon \{ width: 16px; height: 16px;/);
|
assert.match(pageHtml, /\.task-project-head \.task-nav-icon \{ width: 16px; height: 16px;/);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -685,11 +685,20 @@
|
||||||
.task-nav-recent { padding: 8px 0 6px; border-bottom: 1px solid var(--border-soft); }
|
.task-nav-recent { padding: 8px 0 6px; border-bottom: 1px solid var(--border-soft); }
|
||||||
.task-nav-section-title { min-height: 28px; padding: 4px 12px; display: flex; align-items: center; gap: 7px;
|
.task-nav-section-title { min-height: 28px; padding: 4px 12px; display: flex; align-items: center; gap: 7px;
|
||||||
color: var(--muted); font-size: 11px; font-weight: 600; letter-spacing: .02em; }
|
color: var(--muted); font-size: 11px; font-weight: 600; letter-spacing: .02em; }
|
||||||
|
.task-nav-section-title .spacer { flex: 1; }
|
||||||
|
.task-project-create { width: 24px; height: 24px; padding: 0; border: 0; border-radius: 5px;
|
||||||
|
background: transparent; color: var(--muted); font-size: 17px; line-height: 1; cursor: pointer; }
|
||||||
|
.task-project-create:hover { background: var(--hover); color: var(--text); }
|
||||||
.task-projects { padding: 6px 0 10px; }
|
.task-projects { padding: 6px 0 10px; }
|
||||||
.task-project { margin: 1px 0; }
|
.task-project { margin: 1px 0; }
|
||||||
.task-project-head { width: 100%; min-height: 34px; padding: 4px 12px 4px 8px; border: 0; border-radius: 0;
|
.task-project-row { display: flex; align-items: center; }
|
||||||
|
.task-project-head { flex: 1; min-width: 0; min-height: 34px; padding: 4px 4px 4px 8px; border: 0; border-radius: 0;
|
||||||
background: transparent; color: var(--text); display: flex; align-items: center; gap: 6px; cursor: pointer; text-align: left; }
|
background: transparent; color: var(--text); display: flex; align-items: center; gap: 6px; cursor: pointer; text-align: left; }
|
||||||
.task-project-head:hover { background: var(--hover); }
|
.task-project-row:hover { background: var(--hover); }
|
||||||
|
.task-project-menu { width: 28px; height: 28px; margin-right: 6px; padding: 0; flex: none; border: 0;
|
||||||
|
background: transparent; color: var(--muted); opacity: 0; cursor: pointer; }
|
||||||
|
.task-project-row:hover .task-project-menu, .task-project-menu:focus-visible { opacity: 1; }
|
||||||
|
@media (hover: none) { .task-project-menu { opacity: .65; } }
|
||||||
.task-project-chevron { width: 12px; height: 18px; display: inline-flex; align-items: center; justify-content: center;
|
.task-project-chevron { width: 12px; height: 18px; display: inline-flex; align-items: center; justify-content: center;
|
||||||
flex: none; color: var(--muted); font-size: 15px; line-height: 1; transition: transform .14s ease; }
|
flex: none; color: var(--muted); font-size: 15px; line-height: 1; transition: transform .14s ease; }
|
||||||
.task-project.open .task-project-chevron { transform: rotate(90deg); }
|
.task-project.open .task-project-chevron { transform: rotate(90deg); }
|
||||||
|
|
@ -2379,7 +2388,7 @@
|
||||||
<div id="right-files-panel" class="right-workspace-panel" role="tabpanel" aria-labelledby="right-tab-files">
|
<div id="right-files-panel" class="right-workspace-panel" role="tabpanel" aria-labelledby="right-tab-files">
|
||||||
<div id="file-context" class="file-context">
|
<div id="file-context" class="file-context">
|
||||||
<button id="files-scope-task" type="button" class="active">当前项目</button>
|
<button id="files-scope-task" type="button" class="active">当前项目</button>
|
||||||
<button id="files-scope-all" type="button">全部项目</button>
|
<button id="files-scope-all" type="button" title="跨项目浏览、复制和移动文件">全部文件</button>
|
||||||
<span id="files-proj" class="muted small" title=""></span>
|
<span id="files-proj" class="muted small" title=""></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="file-tools">
|
<div class="file-tools">
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,16 @@ import {
|
||||||
renderWebPreviewCardHtml,
|
renderWebPreviewCardHtml,
|
||||||
stopWebPreview,
|
stopWebPreview,
|
||||||
} from "./preview.js";
|
} from "./preview.js";
|
||||||
import { loadFiles, scheduleFilesRefresh, uploadFiles, formatUploadProgress } from "./files.js";
|
import {
|
||||||
|
createProjectFolder,
|
||||||
|
formatUploadProgress,
|
||||||
|
loadFiles,
|
||||||
|
openProjectFiles,
|
||||||
|
removeProjectFolder,
|
||||||
|
renameProjectFolder,
|
||||||
|
scheduleFilesRefresh,
|
||||||
|
uploadFiles,
|
||||||
|
} from "./files.js";
|
||||||
import { toolActivityLabel, _workingDirName, extractMediaBanner, extractArtifactRels, renderArtifactBarHtml, renderArtifactChipContent, upgradeMediaArtifacts, ARTIFACT_PRODUCING_TOOLS, _flushMediaArtifactCache } from "./media.js";
|
import { toolActivityLabel, _workingDirName, extractMediaBanner, extractArtifactRels, renderArtifactBarHtml, renderArtifactChipContent, upgradeMediaArtifacts, ARTIFACT_PRODUCING_TOOLS, _flushMediaArtifactCache } from "./media.js";
|
||||||
import { parseUserAttachments } from "./attachments.js";
|
import { parseUserAttachments } from "./attachments.js";
|
||||||
import {
|
import {
|
||||||
|
|
@ -290,7 +299,6 @@ function taskProjectGroups(tasks) {
|
||||||
const groups = [];
|
const groups = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
for (const folder of state.folders || []) {
|
for (const folder of state.folders || []) {
|
||||||
if (!folder.n_tasks) continue; // 空目录仍在右栏「全部项目」中管理
|
|
||||||
groups.push({
|
groups.push({
|
||||||
key: folder.name,
|
key: folder.name,
|
||||||
name: folder.name,
|
name: folder.name,
|
||||||
|
|
@ -341,16 +349,26 @@ function groupedTaskListHtml(tasks) {
|
||||||
const cachedPage = _projectTaskPages.get(group.key);
|
const cachedPage = _projectTaskPages.get(group.key);
|
||||||
const exactCount = cachedPage && cachedPage.page > 0 ? cachedPage.count : group.count;
|
const exactCount = cachedPage && cachedPage.page > 0 ? cachedPage.count : group.count;
|
||||||
return `<section class="task-project${open ? " open" : ""}" data-project-key="${escapeHtml(group.key)}">
|
return `<section class="task-project${open ? " open" : ""}" data-project-key="${escapeHtml(group.key)}">
|
||||||
|
<div class="task-project-row">
|
||||||
<button class="task-project-head" type="button" aria-expanded="${open}">
|
<button class="task-project-head" type="button" aria-expanded="${open}">
|
||||||
<span class="task-project-chevron" aria-hidden="true">›</span>
|
<span class="task-project-chevron" aria-hidden="true">›</span>
|
||||||
${taskNavIcon("folder")}
|
${taskNavIcon("folder")}
|
||||||
<span class="task-project-name" title="${escapeHtml(group.name)}">${escapeHtml(group.name)}</span>
|
<span class="task-project-name" title="${escapeHtml(group.name)}">${escapeHtml(group.name)}</span>
|
||||||
<span class="task-project-count">${exactCount}</span>
|
<span class="task-project-count">${exactCount}</span>
|
||||||
</button>
|
</button>
|
||||||
|
${group.key === "__ungrouped__" ? "" : `<button class="task-project-menu dd-toggle" type="button"
|
||||||
|
data-project-name="${escapeHtml(group.name)}" data-project-count="${exactCount}"
|
||||||
|
aria-label="管理项目 ${escapeHtml(group.name)}" title="项目操作">⋯</button>`}
|
||||||
|
</div>
|
||||||
<div class="task-project-items">${projectItemsHtml(group)}</div>
|
<div class="task-project-items">${projectItemsHtml(group)}</div>
|
||||||
</section>`;
|
</section>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
return `${recentHtml}<div class="task-projects">${projectsHtml}</div>`;
|
const projectTitle = `<div class="task-nav-section-title">
|
||||||
|
<span>项目</span><span class="spacer"></span>
|
||||||
|
<button id="task-project-create" class="task-project-create" type="button" title="创建项目" aria-label="创建项目">+</button>
|
||||||
|
</div>`;
|
||||||
|
const projectBody = projectsHtml || `<div class="task-project-state">暂无项目</div>`;
|
||||||
|
return `${recentHtml}${projectTitle}<div class="task-projects">${projectBody}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadProjectTasks(projectName, { append = false } = {}) {
|
async function loadProjectTasks(projectName, { append = false } = {}) {
|
||||||
|
|
@ -408,12 +426,11 @@ async function loadProjectTasks(projectName, { append = false } = {}) {
|
||||||
|
|
||||||
function renderTaskNavigation() {
|
function renderTaskNavigation() {
|
||||||
const listEl = $("task-list");
|
const listEl = $("task-list");
|
||||||
if (!_globalTaskResults.length) {
|
|
||||||
listEl.innerHTML = `<div class="empty">(暂无对话)</div>`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isTaskListFiltered()) {
|
if (isTaskListFiltered()) {
|
||||||
listEl.innerHTML = `<div class="task-filter-results"><div class="task-nav-section-title">筛选结果</div>${_globalTaskResults.map((t) => taskRowHtml(t)).join("")}</div>`;
|
const results = _globalTaskResults.length
|
||||||
|
? _globalTaskResults.map((t) => taskRowHtml(t)).join("")
|
||||||
|
: `<div class="empty">(无匹配对话)</div>`;
|
||||||
|
listEl.innerHTML = `<div class="task-filter-results"><div class="task-nav-section-title">筛选结果</div>${results}</div>`;
|
||||||
} else {
|
} else {
|
||||||
listEl.innerHTML = groupedTaskListHtml(_globalTaskResults);
|
listEl.innerHTML = groupedTaskListHtml(_globalTaskResults);
|
||||||
}
|
}
|
||||||
|
|
@ -421,10 +438,30 @@ function renderTaskNavigation() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function refreshTaskProjectIndex() {
|
export function refreshTaskProjectIndex() {
|
||||||
if (!isTaskListFiltered() && _globalTaskResults.length) renderTaskNavigation();
|
if (!isTaskListFiltered()) renderTaskNavigation();
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindTaskListInteractions(root) {
|
function bindTaskListInteractions(root) {
|
||||||
|
const createButton = root.querySelector("#task-project-create");
|
||||||
|
if (createButton) createButton.onclick = () => createProjectFolder();
|
||||||
|
root.querySelectorAll(".task-project-menu").forEach((button) => {
|
||||||
|
button.onclick = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
const name = button.dataset.projectName || "";
|
||||||
|
const count = Number(button.dataset.projectCount) || 0;
|
||||||
|
showMenu(button, [
|
||||||
|
{ act: "open-files", label: "查看项目文件", onclick: () => openProjectFiles(name) },
|
||||||
|
{ act: "rename", label: "重命名项目", cls: "act-rename", onclick: () => renameProjectFolder(name) },
|
||||||
|
{
|
||||||
|
act: "delete",
|
||||||
|
label: count ? `移除项目(尚有 ${count} 个对话)` : "移除项目",
|
||||||
|
cls: "act-delete",
|
||||||
|
disabled: count > 0,
|
||||||
|
onclick: () => removeProjectFolder(name),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
});
|
||||||
root.querySelectorAll(".task-row").forEach((el) => {
|
root.querySelectorAll(".task-row").forEach((el) => {
|
||||||
el.onclick = (e) => {
|
el.onclick = (e) => {
|
||||||
if (e.target.closest(".dd-toggle")) return;
|
if (e.target.closest(".dd-toggle")) return;
|
||||||
|
|
@ -3281,6 +3318,7 @@ async function createTaskFromDraft() {
|
||||||
applyChannelComposerLock(t);
|
applyChannelComposerLock(t);
|
||||||
$("chat-attach-btn").disabled = false;
|
$("chat-attach-btn").disabled = false;
|
||||||
loadTaskList(); // 新 task 立即进左栏;不等待列表 RTT 再发首条消息
|
loadTaskList(); // 新 task 立即进左栏;不等待列表 RTT 再发首条消息
|
||||||
|
document.dispatchEvent(new CustomEvent("folders-refresh-requested"));
|
||||||
loadFiles();
|
loadFiles();
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
@ -4045,6 +4083,7 @@ async function deleteTask(tid, name, nMsg) {
|
||||||
await showNewConversationDraft();
|
await showNewConversationDraft();
|
||||||
}
|
}
|
||||||
loadTaskList();
|
loadTaskList();
|
||||||
|
document.dispatchEvent(new CustomEvent("folders-refresh-requested"));
|
||||||
loadFiles();
|
loadFiles();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 401) { logout(); return; }
|
if (e.status === 401) { logout(); return; }
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,72 @@ async function createFolder() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function createProjectFolder() {
|
||||||
|
const rawName = await dialogPrompt({
|
||||||
|
title: "创建项目", label: "项目名称", placeholder: "输入项目名称", maxLength: 255,
|
||||||
|
okText: "创建",
|
||||||
|
});
|
||||||
|
if (rawName == null) return;
|
||||||
|
const name = rawName.trim();
|
||||||
|
if (!name) return;
|
||||||
|
try {
|
||||||
|
await api("POST", "/v1/files/mkdir", { path: "", name });
|
||||||
|
await loadFolderSuggestions();
|
||||||
|
if (state.filesScope === "all" && !state.filesPath) await loadFiles();
|
||||||
|
message(`已创建项目「${name}」`, "success");
|
||||||
|
} catch (e) {
|
||||||
|
if (e.status === 401) { logout(); return; }
|
||||||
|
message("创建项目失败:" + e.message, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renameProjectFolder(name) {
|
||||||
|
if (!name) return;
|
||||||
|
await renameFile(name, name, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openProjectFiles(name) {
|
||||||
|
if (!name) return;
|
||||||
|
state.filesScope = "all";
|
||||||
|
state.filesPath = name;
|
||||||
|
resetFileSearch();
|
||||||
|
setRightPanelTab("files");
|
||||||
|
loadFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeProjectFolder(name) {
|
||||||
|
if (!name) return;
|
||||||
|
let entries;
|
||||||
|
try {
|
||||||
|
const data = await api("GET", "/v1/files?path=" + encodeURIComponent(name));
|
||||||
|
entries = data.entries || [];
|
||||||
|
} catch (e) {
|
||||||
|
if (e.status === 401) { logout(); return; }
|
||||||
|
message("读取项目失败:" + e.message, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const detail = entries.length
|
||||||
|
? `项目「${name}」包含 ${entries.length} 项文件或目录,移除后不可恢复。`
|
||||||
|
: `确认移除空项目「${name}」?`;
|
||||||
|
if (!await dialogConfirm({
|
||||||
|
title: "移除项目", message: detail, okText: "确认移除", danger: true,
|
||||||
|
})) return;
|
||||||
|
try {
|
||||||
|
await api("POST", "/v1/files/delete", { path: name, recursive: entries.length > 0 });
|
||||||
|
if (state.filesPath === name || state.filesPath.startsWith(name + "/")) {
|
||||||
|
state.filesScope = "all";
|
||||||
|
state.filesPath = "";
|
||||||
|
resetFileSearch();
|
||||||
|
}
|
||||||
|
await loadFolderSuggestions();
|
||||||
|
await loadFiles();
|
||||||
|
message(`已移除项目「${name}」`, "success");
|
||||||
|
} catch (e) {
|
||||||
|
if (e.status === 401) { logout(); return; }
|
||||||
|
message("移除项目失败:" + e.message, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ───── 选入 modal(勾源 → 复制 / 移动到主区当前目录)─────
|
// ───── 选入 modal(勾源 → 复制 / 移动到主区当前目录)─────
|
||||||
// 设计:目的地永远是主区 state.filesPath。弹框内浏览的 path 跟主区独立 — 用户从 A 翻到 B
|
// 设计:目的地永远是主区 state.filesPath。弹框内浏览的 path 跟主区独立 — 用户从 A 翻到 B
|
||||||
// 勾几个,再翻到 C 接着勾,跨目录 selection 用 Set<rel> 全程保留;切换浏览路径不清空。
|
// 勾几个,再翻到 C 接着勾,跨目录 selection 用 Set<rel> 全程保留;切换浏览路径不清空。
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ $("nt-go").onclick = async () => {
|
||||||
const t = await api("POST", "/v1/tasks", body);
|
const t = await api("POST", "/v1/tasks", body);
|
||||||
localStorage.setItem(LS_RECENT_WORKING_DIR, working_dir);
|
localStorage.setItem(LS_RECENT_WORKING_DIR, working_dir);
|
||||||
$("new-task-modal").classList.remove("show");
|
$("new-task-modal").classList.remove("show");
|
||||||
|
await loadFolderSuggestions();
|
||||||
await loadTaskList();
|
await loadTaskList();
|
||||||
selectTask(t.task_id);
|
selectTask(t.task_id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -81,6 +82,10 @@ export async function loadFolderSuggestions() {
|
||||||
refreshTaskProjectIndex();
|
refreshTaskProjectIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// task 的创建/软删除发生在 chat.js;用事件保持目录下拉和左侧项目计数同步,
|
||||||
|
// 避免在 chat.js ↔ newtask.js 的既有依赖环上再增加反向 import。
|
||||||
|
document.addEventListener("folders-refresh-requested", () => { void loadFolderSuggestions(); });
|
||||||
|
|
||||||
// 灌顶部 filter-wd;自定义创建弹框使用与快速新对话一致的组合框。
|
// 灌顶部 filter-wd;自定义创建弹框使用与快速新对话一致的组合框。
|
||||||
function populateFolderSelects() {
|
function populateFolderSelects() {
|
||||||
const folders = state.folders || [];
|
const folders = state.folders || [];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue