diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 36c0c086a4..7589553ca9 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -218,7 +218,19 @@ const connectionStats = computed(() => ({ })); const recentConnections = computed(() => connectionStore.connections.slice(0, 5)); const savedSqlHistoryItems = computed(() => { - const folderById = new Map(savedSqlStore.allFolders.map((folder) => [folder.id, folder.name])); + const folderById = new Map(savedSqlStore.allFolders.map((folder) => [folder.id, folder])); + const folderPath = (folderId?: string): string | undefined => { + if (!folderId) return undefined; + const parts: string[] = []; + const seen = new Set(); + let folder = folderById.get(folderId); + while (folder && !seen.has(folder.id)) { + seen.add(folder.id); + parts.unshift(folder.name); + folder = folder.parentFolderId ? folderById.get(folder.parentFolderId) : undefined; + } + return parts.join("/"); + }; return rankSavedSqlHistory(savedSqlStore.allFiles, { limit: 6 }).map((file) => { const connection = connectionStore.getConfig(file.connectionId); return { @@ -226,13 +238,25 @@ const savedSqlHistoryItems = computed(() => { name: file.name, connectionName: connection ? connectionRedactedNameLabel(connection) : t("welcome.unknownConnection"), database: file.database, - folderName: file.folderId ? folderById.get(file.folderId) : undefined, + folderName: folderPath(file.folderId), openCount: file.openCount ?? 0, }; }); }); const saveSqlFolders = computed(() => { - return savedSqlStore.allFolders; + const folderById = new Map(savedSqlStore.allFolders.map((folder) => [folder.id, folder])); + const pathForFolder = (folderId: string) => { + const parts: string[] = []; + const seen = new Set(); + let folder = folderById.get(folderId); + while (folder && !seen.has(folder.id)) { + seen.add(folder.id); + parts.unshift(folder.name); + folder = folder.parentFolderId ? folderById.get(folder.parentFolderId) : undefined; + } + return parts.join(" / "); + }; + return savedSqlStore.allFoldersTreeOrder.map((folder) => ({ ...folder, displayName: pathForFolder(folder.id) || folder.name })); }); async function applyUiScale(scale: number) { @@ -1209,7 +1233,7 @@ onUnmounted(() => { {{ t("savedSql.rootFolder") }} - {{ folder.name }} + {{ folder.displayName }} diff --git a/apps/desktop/src/components/layout/SqlLibraryPanel.vue b/apps/desktop/src/components/layout/SqlLibraryPanel.vue index 8392ef5599..3055dd35b3 100644 --- a/apps/desktop/src/components/layout/SqlLibraryPanel.vue +++ b/apps/desktop/src/components/layout/SqlLibraryPanel.vue @@ -147,19 +147,25 @@ async function exportFolderContents(folder?: SavedSqlFolder) { const rootDir = await join(targetDir, rootDirName); await mkdir(rootDir, { recursive: true }); - if (folder) { - for (const file of savedSqlStore.filesInFolder(folder.id)) { - const filePath = await join(rootDir, sanitizeFileSystemSegment(ensureSqlExtension(file.name))); + const writeFolder = async (libraryFolder: SavedSqlFolder, dir: string): Promise => { + for (const child of savedSqlStore.allFolders.filter((item) => item.parentFolderId === libraryFolder.id)) { + const childDir = await join(dir, sanitizeFileSystemSegment(child.name)); + await mkdir(childDir, { recursive: true }); + await writeFolder(child, childDir); + } + for (const file of savedSqlStore.filesInFolder(libraryFolder.id)) { + const filePath = await join(dir, sanitizeFileSystemSegment(ensureSqlExtension(file.name))); await writeTextFile(filePath, file.sql); } + }; + + if (folder) { + await writeFolder(folder, rootDir); } else { - for (const libraryFolder of savedSqlStore.allFolders.filter((item) => isConnectionVisible(item.connectionId))) { + for (const libraryFolder of savedSqlStore.allFolders.filter((item) => isConnectionVisible(item.connectionId) && !item.parentFolderId)) { const folderDir = await join(rootDir, sanitizeFileSystemSegment(libraryFolder.name)); await mkdir(folderDir, { recursive: true }); - for (const file of savedSqlStore.filesInFolder(libraryFolder.id)) { - const filePath = await join(folderDir, sanitizeFileSystemSegment(ensureSqlExtension(file.name))); - await writeTextFile(filePath, file.sql); - } + await writeFolder(libraryFolder, folderDir); } const unfiled = savedSqlStore.filesWithoutFolder().filter((file) => !orphanedIds.value.has(file.id)); @@ -310,6 +316,20 @@ function folderMatchesQuery(folder: SavedSqlFolder) { return savedSqlStore.filesInFolder(folder.id).some((file) => !orphanedIds.value.has(file.id) && fileMatchesQuery(file)); } +function childFolders(parentFolderId?: string) { + return savedSqlStore.allFolders.filter((folder) => isConnectionVisible(folder.connectionId) && (folder.parentFolderId || "") === (parentFolderId || "")); +} + +function descendantFolders(parentFolderId: string): SavedSqlFolder[] { + const direct = childFolders(parentFolderId); + return direct.flatMap((folder) => [folder, ...descendantFolders(folder.id)]); +} + +function folderBranchMatchesQuery(folder: SavedSqlFolder) { + if (folderMatchesQuery(folder)) return true; + return descendantFolders(folder.id).some((child) => folderMatchesQuery(child)); +} + function filesInFolder(folderId: string) { const folder = savedSqlStore.allFolders.find((item) => item.id === folderId); const includeAllFilesForMatchedFolder = !!folder && !!searchQuery.value && folder.name.toLowerCase().includes(searchQuery.value); @@ -319,7 +339,26 @@ function filesInFolder(folderId: string) { .filter((file) => includeAllFilesForMatchedFolder || fileMatchesQuery(file)); } -const visibleFolders = computed(() => savedSqlStore.allFolders.filter((folder) => isConnectionVisible(folder.connectionId) && folderMatchesQuery(folder))); +type SqlLibraryRow = { type: "folder"; folder: SavedSqlFolder; depth: number } | { type: "file"; file: SavedSqlFile; depth: number }; + +const visibleFolderRows = computed(() => { + const rows: SqlLibraryRow[] = []; + const appendFolder = (folder: SavedSqlFolder, depth: number) => { + if (!folderBranchMatchesQuery(folder)) return; + rows.push({ type: "folder", folder, depth }); + if (!isFolderExpanded(folder.id)) return; + for (const child of childFolders(folder.id)) { + appendFolder(child, depth + 1); + } + for (const file of filesInFolder(folder.id)) { + rows.push({ type: "file", file, depth: depth + 1 }); + } + }; + for (const folder of childFolders()) { + appendFolder(folder, 0); + } + return rows; +}); const visibleFiles = computed(() => savedSqlStore @@ -328,7 +367,7 @@ const visibleFiles = computed(() => .filter((file) => fileMatchesQuery(file)), ); -const hasAnyVisibleItem = computed(() => visibleFolders.value.length > 0 || visibleFiles.value.length > 0); +const hasAnyVisibleItem = computed(() => visibleFolderRows.value.length > 0 || visibleFiles.value.length > 0); const collapsedFolders = ref>(new Set()); @@ -344,31 +383,15 @@ function isFolderExpanded(folderId: string) { return !collapsedFolders.value.has(folderId); } -const showNewFolderInput = ref(false); -const newFolderName = ref(""); -const newFolderInputRef = ref(null); - -function openNewFolderInput() { - newFolderName.value = ""; - showNewFolderInput.value = true; - nextTick(() => newFolderInputRef.value?.focus()); -} - -async function confirmNewFolder() { - const name = newFolderName.value.trim(); - if (!name) { - cancelNewFolder(); - return; - } - showNewFolderInput.value = false; - const connectionId = connectionStore.connections[0]?.id; +async function openNewFolderInput(parentFolderId?: string) { + const parent = parentFolderId ? savedSqlStore.allFolders.find((folder) => folder.id === parentFolderId) : undefined; + const connectionId = parent?.connectionId || connectionStore.connections[0]?.id; if (!connectionId) return; - await savedSqlStore.createFolder(connectionId, name); -} - -function cancelNewFolder() { - showNewFolderInput.value = false; - newFolderName.value = ""; + if (parent?.id) { + collapsedFolders.value = new Set([...collapsedFolders.value].filter((id) => id !== parent.id)); + } + const folder = await savedSqlStore.createFolder(connectionId, t("savedSql.newFolderDefault"), parent?.id); + startRenameFolder(folder); } const renamingTarget = ref<{ type: "folder" | "file"; id: string } | null>(null); @@ -479,6 +502,7 @@ const contextMenuItems = computed(() => { ]; } return [ + { label: t("savedSql.newFolder"), action: () => openNewFolderInput(target.id), icon: FolderPlus }, { label: t("sqlLibrary.importIntoFolder"), action: () => importDirectoryIntoLibrary(target), icon: Upload }, { label: t("sqlLibrary.exportFolder"), action: () => exportFolderContents(target), icon: Download }, { label: "", separator: true }, @@ -619,6 +643,11 @@ async function performDrop() { return; } + if (draggedType === "folder" && targetType === "folder" && dropPosition === "inside") { + await savedSqlStore.moveFolderToFolder(draggedId, targetId); + return; + } + if (draggedType !== "file") return; if (targetType === "folder") { @@ -677,7 +706,9 @@ function updateDropTarget(event: MouseEvent, targetId: string, targetType: DragI let nextPosition: DropPosition | null = null; if (dragState.draggedType === "folder" && targetType === "folder") { const rect = (event.currentTarget as HTMLElement).getBoundingClientRect(); - nextPosition = event.clientY - rect.top < rect.height / 2 ? "before" : "after"; + const y = event.clientY - rect.top; + const third = rect.height / 3; + nextPosition = y > third && y < rect.height - third ? "inside" : y < rect.height / 2 ? "before" : "after"; } else if (dragState.draggedType === "file" && targetType === "file") { const rect = (event.currentTarget as HTMLElement).getBoundingClientRect(); nextPosition = event.clientY - rect.top < rect.height / 2 ? "before" : "after"; @@ -744,19 +775,6 @@ function showDropInside(targetId: string) {
-
- - -
-