feat(sql-library): add collapse all button and collapsed default
This commit is contained in:
1 parent
23f0b1255b
commit
d79d357924
10 files changed
+68
-5
No files matched your search
@@ -2,7 +2,7 @@
|
||||
import { computed, nextTick, onBeforeUnmount, reactive, ref, watch } from "vue";
|
||||
import type { CSSProperties } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ArrowDownWideNarrow, Download, FilePlus, FileText, FolderCog, FolderClosed, FolderOpen, FolderPlus, Library, LocateFixed, Pencil, Play, Search, Trash2, Upload, X } from "@lucide/vue";
|
||||
import { ArrowDownWideNarrow, ChevronsDownUp, Download, FilePlus, FileText, FolderCog, FolderClosed, FolderOpen, FolderPlus, Library, LocateFixed, Pencil, Play, Search, Trash2, Upload, X } from "@lucide/vue";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import CustomContextMenu, { type ContextMenuItem as CtxMenuItem } from "@/components/ui/CustomContextMenu.vue";
|
||||
@@ -346,7 +346,7 @@ const visibleFolderRows = computed<SqlLibraryRow[]>(() => {
|
||||
const appendFolder = (folder: SavedSqlFolder, depth: number) => {
|
||||
if (!folderBranchMatchesQuery(folder)) return;
|
||||
rows.push({ type: "folder", folder, depth, folderIndex: folderIndex++ });
|
||||
if (!isFolderExpanded(folder.id)) return;
|
||||
if (!isFolderExpanded(folder)) return;
|
||||
for (const child of childFolders(folder.id)) {
|
||||
appendFolder(child, depth + 1);
|
||||
}
|
||||
@@ -375,16 +375,52 @@ const hasAnyVisibleItem = computed(() => visibleFolderRows.value.length > 0 || v
|
||||
|
||||
const collapsedFolders = ref<Set<string>>(new Set());
|
||||
|
||||
// Seed a default-collapsed state when the library first becomes non-empty so
|
||||
// opening the panel shows every directory collapsed instead of fully expanded.
|
||||
// Guarded so a later in-session folder add/delete does not re-collapse folders
|
||||
// the user has already expanded.
|
||||
const collapseDefaultsSeeded = ref(false);
|
||||
watch(
|
||||
() => savedSqlStore.allFolders.map((folder) => folder.id),
|
||||
(folderIds) => {
|
||||
if (collapseDefaultsSeeded.value || folderIds.length === 0) return;
|
||||
collapseDefaultsSeeded.value = true;
|
||||
collapsedFolders.value = new Set(folderIds);
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
function collapseAllFolders() {
|
||||
// Collapse the whole tree: every folder currently known, including nested
|
||||
// children, is marked collapsed in one shot.
|
||||
const folderIds = savedSqlStore.allFoldersTreeOrder.map((folder) => folder.id);
|
||||
if (folderIds.length === 0) return;
|
||||
collapsedFolders.value = new Set(folderIds);
|
||||
}
|
||||
|
||||
function hasAnyFolder(): boolean {
|
||||
return savedSqlStore.allFolders.length > 0;
|
||||
}
|
||||
|
||||
function toggleFolder(folderId: string) {
|
||||
if (suppressNextRowClick.value) return;
|
||||
// While a search is active, matched branches are force-expanded for
|
||||
// visibility; toggling them would only mutate hidden state that surprises
|
||||
// after the search is cleared, so treat the click as a no-op.
|
||||
if (searchQuery.value) {
|
||||
const folder = savedSqlStore.allFolders.find((candidate) => candidate.id === folderId);
|
||||
if (folder && folderBranchMatchesQuery(folder)) return;
|
||||
}
|
||||
const next = new Set(collapsedFolders.value);
|
||||
if (next.has(folderId)) next.delete(folderId);
|
||||
else next.add(folderId);
|
||||
collapsedFolders.value = next;
|
||||
}
|
||||
|
||||
function isFolderExpanded(folderId: string) {
|
||||
return !collapsedFolders.value.has(folderId);
|
||||
function isFolderExpanded(folder: SavedSqlFolder) {
|
||||
// 搜索激活时,命中的分支自动展开以便直接看到匹配文件;否则遵循折叠状态。
|
||||
if (searchQuery.value && folderBranchMatchesQuery(folder)) return true;
|
||||
return !collapsedFolders.value.has(folder.id);
|
||||
}
|
||||
|
||||
async function openNewFolderInput(parentFolderId?: string) {
|
||||
@@ -1127,6 +1163,11 @@ function showDropInside(targetId: string) {
|
||||
</HelpTooltip>
|
||||
<span v-if="hasSelection" class="text-[12px] text-muted-foreground ml-1">({{ selectedCount }})</span>
|
||||
<span class="flex-1" />
|
||||
<LightTooltip :text="t('sqlLibrary.collapseAll')" side="bottom" :delay="0" :close-delay="0" nowrap :disabled="!hasAnyFolder()">
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5" :disabled="!hasAnyFolder() || sortMode === 'date'" @click="collapseAllFolders">
|
||||
<ChevronsDownUp class="h-3 w-3" />
|
||||
</Button>
|
||||
</LightTooltip>
|
||||
<LightTooltip :text="sortMode === 'folder' ? t('sqlLibrary.sortByDate') : t('sqlLibrary.sortByFolder')" side="bottom" :delay="0" :close-delay="0" nowrap>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5" @click="sortMode = sortMode === 'folder' ? 'date' : 'folder'">
|
||||
<ArrowDownWideNarrow :class="['h-3 w-3', sortMode === 'date' ? 'text-primary' : '']" />
|
||||
@@ -1276,7 +1317,7 @@ function showDropInside(targetId: string) {
|
||||
>
|
||||
<div v-if="showDropBefore(row.folder.id)" class="absolute left-2 right-2 top-0 border-t-2 border-primary" />
|
||||
<div v-if="showDropAfter(row.folder.id)" class="absolute left-2 right-2 bottom-0 border-b-2 border-primary" />
|
||||
<component :is="isFolderExpanded(row.folder.id) ? FolderOpen : FolderClosed" class="h-4 w-4 text-amber-500 shrink-0" />
|
||||
<component :is="isFolderExpanded(row.folder) ? FolderOpen : FolderClosed" class="h-4 w-4 text-amber-500 shrink-0" />
|
||||
<template v-if="isRenamingFolder(row.folder.id)">
|
||||
<input
|
||||
:ref="setRenameInputRef"
|
||||
|
||||
@@ -32,4 +32,18 @@ describe("SqlLibraryPanel selection contrast", () => {
|
||||
expect(panelSource).toMatch(/if \(anchorIndex === null\)[\s\S]*lastClickedItemIndex\.value = currentIndex;[\s\S]*return;/);
|
||||
expect(panelSource).not.toContain("lastClickedItemIndex.value ?? 0");
|
||||
});
|
||||
|
||||
it("shows a single collapse-all button and seeds a default-collapsed library", () => {
|
||||
expect(panelSource).toContain("ChevronsDownUp,");
|
||||
expect(panelSource).toContain("t('sqlLibrary.collapseAll')");
|
||||
expect(panelSource).toMatch(/function collapseAllFolders\(\)[\s\S]*allFoldersTreeOrder[\s\S]*collapsedFolders\.value = new Set\(folderIds\);/);
|
||||
expect(panelSource).toMatch(/const collapseDefaultsSeeded = ref\(false\);[\s\S]*collapsedFolders\.value = new Set\(folderIds\);/);
|
||||
expect(panelSource).toMatch(/@click="collapseAllFolders"/);
|
||||
expect(panelSource).toMatch(/:disabled="!hasAnyFolder\(\)"/);
|
||||
});
|
||||
|
||||
it("auto-expands a search-matched branch so matched files remain visible when the library collapses by default", () => {
|
||||
expect(panelSource).toMatch(/function isFolderExpanded\(folder: SavedSqlFolder\) \{[\s\S]*if \(searchQuery\.value && folderBranchMatchesQuery\(folder\)\) return true;[\s\S]*return !collapsedFolders\.value\.has\(folder\.id\);/);
|
||||
expect(panelSource).toMatch(/if \(!isFolderExpanded\(folder\)\) return;/);
|
||||
});
|
||||
});
|
||||
@@ -418,6 +418,7 @@ export default {
|
||||
clearSelection: "Clear Selection",
|
||||
sortByDate: "Sort by Date Modified",
|
||||
sortByFolder: "Sort by Folder Structure",
|
||||
collapseAll: "Collapse all",
|
||||
},
|
||||
|
||||
connection: {
|
||||
|
||||
@@ -420,6 +420,7 @@ export default withEnglishFallback({
|
||||
clearSelection: "Limpiar selección",
|
||||
sortByDate: "Ordenar por fecha de modificación",
|
||||
sortByFolder: "Ordenar por estructura de carpetas",
|
||||
collapseAll: "Contraer todo",
|
||||
},
|
||||
|
||||
connection: {
|
||||
|
||||
@@ -419,6 +419,7 @@ export default withEnglishFallback({
|
||||
clearSelection: "Cancella selezione",
|
||||
sortByDate: "Ordina per data di modifica",
|
||||
sortByFolder: "Ordina per struttura cartelle",
|
||||
collapseAll: "Comprimi tutto",
|
||||
},
|
||||
|
||||
connection: {
|
||||
|
||||
@@ -420,6 +420,7 @@ export default withEnglishFallback({
|
||||
clearSelection: "選択を解除",
|
||||
sortByDate: "更新日時で並べ替え",
|
||||
sortByFolder: "フォルダ構造で並べ替え",
|
||||
collapseAll: "すべて折りたたむ",
|
||||
},
|
||||
connection: {
|
||||
title: "新しい接続",
|
||||
|
||||
@@ -413,6 +413,7 @@ export default withEnglishFallback({
|
||||
clearSelection: "선택 해제",
|
||||
sortByDate: "수정 날짜순 정렬",
|
||||
sortByFolder: "폴더 구조순 정렬",
|
||||
collapseAll: "모두 접기",
|
||||
},
|
||||
connection: {
|
||||
title: "새 연결",
|
||||
|
||||
@@ -420,6 +420,7 @@ export default withEnglishFallback({
|
||||
clearSelection: "Limpar seleção",
|
||||
sortByDate: "Ordenar por data de modificação",
|
||||
sortByFolder: "Ordenar por estrutura de pastas",
|
||||
collapseAll: "Recolher todos",
|
||||
},
|
||||
|
||||
connection: {
|
||||
|
||||
@@ -343,6 +343,7 @@ export default withEnglishFallback({
|
||||
clearSelection: "清除选择",
|
||||
sortByDate: "按修改日期排序",
|
||||
sortByFolder: "按文件夹结构排序",
|
||||
collapseAll: "全部折叠",
|
||||
},
|
||||
|
||||
connection: {
|
||||
|
||||
@@ -420,6 +420,7 @@ export default withEnglishFallback({
|
||||
clearSelection: "清除選取",
|
||||
sortByDate: "依修改日期排序",
|
||||
sortByFolder: "依資料夾結構排序",
|
||||
collapseAll: "全部摺疊",
|
||||
},
|
||||
|
||||
connection: {
|
||||
|
||||
Reference in new issue
Block a user