fix(sql-library): preserve folders during directory import
Closes #5877
This commit is contained in:
4 files changed
+101
-18
No files matched your search
@@ -17,6 +17,7 @@ import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { focusSidebarRenameInput } from "@/lib/sidebar/sidebarRenameFocus";
|
||||
import { savedSqlFolderBranchFileCount } from "@/lib/savedSql/savedSqlFolderCounts";
|
||||
import { collectSavedSqlDirectoryImportFiles } from "@/lib/savedSql/savedSqlDirectoryImport";
|
||||
import { ensureSqlExtension, stripSqlExtension } from "@/lib/savedSql/savedSqlFileName";
|
||||
import { savedSqlExecutionTargetFromTab, type SavedSqlOpenTargetMode } from "@/lib/savedSql/savedSqlExecutionTarget";
|
||||
import type { SavedSqlFile, SavedSqlFolder } from "@/types/database";
|
||||
@@ -76,14 +77,6 @@ function sanitizeFileSystemSegment(name: string) {
|
||||
return name.replace(/[<>:"/\\|?*\p{Cc}]/gu, "_").trim() || "untitled";
|
||||
}
|
||||
|
||||
function relativeImportName(baseDir: string, filePath: string) {
|
||||
const normalizedBase = baseDir.replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
const normalizedFile = filePath.replace(/\\/g, "/");
|
||||
const relative = normalizedFile.startsWith(`${normalizedBase}/`) ? normalizedFile.slice(normalizedBase.length + 1) : normalizedFile.split("/").pop() || "import.sql";
|
||||
const pretty = relative.replace(/\//g, " - ");
|
||||
return ensureSqlExtension(pretty);
|
||||
}
|
||||
|
||||
function uniqueImportedName(name: string, takenNames: Set<string>) {
|
||||
const normalized = ensureSqlExtension(name);
|
||||
if (!takenNames.has(normalized)) {
|
||||
@@ -201,10 +194,27 @@ async function exportFolderContents(folder?: SavedSqlFolder) {
|
||||
}
|
||||
}
|
||||
|
||||
async function collectSqlFilesRecursively(dir: string): Promise<string[]> {
|
||||
const collectPaths = (entries: Awaited<ReturnType<typeof api.listSqlFilesInFolder>>): string[] => entries.flatMap((entry) => (entry.is_dir ? collectPaths(entry.children) : [entry.path]));
|
||||
async function collectSqlFilesRecursively(dir: string) {
|
||||
return collectSavedSqlDirectoryImportFiles(await api.listSqlFilesInFolder(dir));
|
||||
}
|
||||
|
||||
return collectPaths(await api.listSqlFilesInFolder(dir));
|
||||
function importedFolderCacheKey(parentFolderId: string | undefined, name: string) {
|
||||
return JSON.stringify([parentFolderId || "", name]);
|
||||
}
|
||||
|
||||
async function resolveImportedFolder(connectionId: string, rootFolderId: string | undefined, folderNames: string[], folderCache: Map<string, SavedSqlFolder>) {
|
||||
let parentFolderId = rootFolderId;
|
||||
for (const name of folderNames) {
|
||||
const cacheKey = importedFolderCacheKey(parentFolderId, name);
|
||||
let folder = folderCache.get(cacheKey);
|
||||
if (!folder) {
|
||||
folder = savedSqlStore.listChildFolders(connectionId, parentFolderId).find((candidate) => candidate.name === name);
|
||||
if (!folder) folder = await savedSqlStore.createFolder(connectionId, name, parentFolderId);
|
||||
folderCache.set(cacheKey, folder);
|
||||
}
|
||||
parentFolderId = folder.id;
|
||||
}
|
||||
return parentFolderId;
|
||||
}
|
||||
|
||||
async function importDirectoryIntoLibrary(targetFolder?: SavedSqlFolder) {
|
||||
@@ -229,27 +239,36 @@ async function importDirectoryIntoLibrary(targetFolder?: SavedSqlFolder) {
|
||||
});
|
||||
if (!selected || Array.isArray(selected)) return;
|
||||
|
||||
const sqlPaths = await collectSqlFilesRecursively(selected);
|
||||
if (sqlPaths.length === 0) {
|
||||
const importFiles = await collectSqlFilesRecursively(selected);
|
||||
if (importFiles.length === 0) {
|
||||
toast(t("sqlLibrary.importNone"), 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
const takenNames = new Set((targetFolder ? savedSqlStore.filesInFolder(targetFolder.id) : savedSqlStore.filesWithoutFolder()).map((file) => file.name));
|
||||
const folderCache = new Map<string, SavedSqlFolder>();
|
||||
const takenNamesByFolder = new Map<string, Set<string>>();
|
||||
|
||||
for (const path of sqlPaths) {
|
||||
for (const file of importFiles) {
|
||||
const folderId = await resolveImportedFolder(connectionId, targetFolder?.id, file.folderNames, folderCache);
|
||||
const folderKey = folderId || "";
|
||||
let takenNames = takenNamesByFolder.get(folderKey);
|
||||
if (!takenNames) {
|
||||
takenNames = new Set(savedSqlStore.listFiles(connectionId, folderId).map((savedFile) => savedFile.name));
|
||||
takenNamesByFolder.set(folderKey, takenNames);
|
||||
}
|
||||
const path = file.path;
|
||||
const content = await api.readExternalSqlFile(path);
|
||||
const displayName = uniqueImportedName(relativeImportName(selected, path), takenNames);
|
||||
const displayName = uniqueImportedName(file.name, takenNames);
|
||||
await savedSqlStore.saveFile({
|
||||
connectionId,
|
||||
folderId: targetFolder?.id,
|
||||
folderId,
|
||||
name: displayName,
|
||||
database: "",
|
||||
sql: content,
|
||||
});
|
||||
}
|
||||
|
||||
toast(t("sqlLibrary.imported", { count: sqlPaths.length }), 2500);
|
||||
toast(t("sqlLibrary.imported", { count: importFiles.length }), 2500);
|
||||
} catch (e: any) {
|
||||
toast(t("sqlLibrary.importFailed", { message: externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)) }), 5000);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { collectSavedSqlDirectoryImportFiles } from "@/lib/savedSql/savedSqlDirectoryImport";
|
||||
|
||||
describe("saved SQL directory import", () => {
|
||||
it("preserves nested directory paths without flattening file names", () => {
|
||||
const files = collectSavedSqlDirectoryImportFiles([
|
||||
{ name: "database", path: "/scripts/database", is_dir: true, children: [{ name: "daily.sql", path: "/scripts/database/daily.sql", is_dir: false, children: [] }] },
|
||||
{
|
||||
name: "incidents",
|
||||
path: "/scripts/incidents",
|
||||
is_dir: true,
|
||||
children: [
|
||||
{
|
||||
name: "postgres",
|
||||
path: "/scripts/incidents/postgres",
|
||||
is_dir: true,
|
||||
children: [{ name: "repair.sql", path: "/scripts/incidents/postgres/repair.sql", is_dir: false, children: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ name: "root.sql", path: "/scripts/root.sql", is_dir: false, children: [] },
|
||||
]);
|
||||
|
||||
expect(files).toEqual([
|
||||
{ name: "daily.sql", path: "/scripts/database/daily.sql", folderNames: ["database"] },
|
||||
{ name: "repair.sql", path: "/scripts/incidents/postgres/repair.sql", folderNames: ["incidents", "postgres"] },
|
||||
{ name: "root.sql", path: "/scripts/root.sql", folderNames: [] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
export interface SavedSqlDirectoryImportEntry {
|
||||
name: string;
|
||||
path: string;
|
||||
is_dir: boolean;
|
||||
children: SavedSqlDirectoryImportEntry[];
|
||||
}
|
||||
|
||||
export interface SavedSqlDirectoryImportFile {
|
||||
name: string;
|
||||
path: string;
|
||||
folderNames: string[];
|
||||
}
|
||||
|
||||
export function collectSavedSqlDirectoryImportFiles(entries: SavedSqlDirectoryImportEntry[], folderNames: string[] = []): SavedSqlDirectoryImportFile[] {
|
||||
const files: SavedSqlDirectoryImportFile[] = [];
|
||||
for (const entry of entries) {
|
||||
if (entry.is_dir) {
|
||||
files.push(...collectSavedSqlDirectoryImportFiles(entry.children, [...folderNames, entry.name]));
|
||||
} else {
|
||||
files.push({ name: entry.name, path: entry.path, folderNames });
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
Reference in new issue
Block a user