diff --git a/apps/desktop/src/components/layout/SqlLibraryPanel.vue b/apps/desktop/src/components/layout/SqlLibraryPanel.vue index 010169ce8..57174ac25 100644 --- a/apps/desktop/src/components/layout/SqlLibraryPanel.vue +++ b/apps/desktop/src/components/layout/SqlLibraryPanel.vue @@ -421,13 +421,19 @@ function isFolderExpanded(folderId: string) { async function openNewFolderInput(parentFolderId?: string) { const parent = parentFolderId ? savedSqlStore.allFolders.find((folder) => folder.id === parentFolderId) : undefined; + if (parentFolderId && !parent) return; const connectionId = parent?.connectionId || connectionStore.connections[0]?.id; if (!connectionId) return; 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); + try { + const folder = await savedSqlStore.createFolder(connectionId, t("savedSql.newFolderDefault"), parent?.id); + searchText.value = ""; + startRenameFolder(folder); + } catch (e: any) { + toast(t("savedSql.createFolderFailed", { message: e?.message || String(e) }), 5000); + } } async function openNewQueryInFolder(folder?: SavedSqlFolder) { @@ -892,7 +898,7 @@ const contextMenuItems = computed(() => { ]; } return [ - { label: t("savedSql.newFolder"), action: () => openNewFolderInput(target.id), icon: FolderPlus }, + { label: t("savedSql.newSubfolder"), action: () => openNewFolderInput(target.id), icon: FolderPlus }, { label: t("savedSql.newQuery"), action: () => openNewQueryInFolder(target), icon: FilePlus }, { label: t("sqlLibrary.importIntoFolder"), action: () => importDirectoryIntoLibrary(target), icon: Download }, { label: t("sqlLibrary.exportFolder"), action: () => exportFolderContents(target), icon: Upload }, @@ -1150,7 +1156,7 @@ function showDropInside(targetId: string) { - @@ -1225,6 +1231,18 @@ function showDropInside(targetId: string) { {{ item.item.name }} ({{ folderFileCount(item.item.id) }}) + + +
({{ folderFileCount(row.folder.id) }}) + + +
{ + const root: SavedSqlFolder = { + id: "root", + connectionId: "conn-1", + name: "Root", + orderIndex: 0, + createdAt: "2026-07-19T00:00:00.000Z", + updatedAt: "2026-07-19T00:00:00.000Z", + }; + const sibling: SavedSqlFolder = { + id: "child-1", + connectionId: "conn-1", + parentFolderId: "root", + name: "Existing child", + orderIndex: 0, + createdAt: "2026-07-19T00:00:00.000Z", + updatedAt: "2026-07-19T00:00:00.000Z", + }; + apiMock.loadSavedSqlLibrary.mockResolvedValue({ folders: [root, sibling], files: [] }); + + const store = useSavedSqlStore(); + await store.initFromStorage(); + const child = await store.createFolder("conn-1", "Nested child", "root"); + + assert.equal(child.parentFolderId, "root"); + assert.equal(child.connectionId, "conn-1"); + assert.equal(child.orderIndex, 1); + assert.equal(apiMock.saveSavedSqlFolder.mock.calls.at(-1)?.[0].parentFolderId, "root"); + assert.deepEqual( + store.listChildFolders("conn-1", "root").map((folder) => folder.id), + ["child-1", child.id], + ); +}); + +test("does not move a SQL folder into its own descendant", async () => { + const root: SavedSqlFolder = { + id: "root", + connectionId: "conn-1", + name: "Root", + createdAt: "2026-07-19T00:00:00.000Z", + updatedAt: "2026-07-19T00:00:00.000Z", + }; + const child: SavedSqlFolder = { + id: "child", + connectionId: "conn-1", + parentFolderId: "root", + name: "Child", + createdAt: "2026-07-19T00:00:00.000Z", + updatedAt: "2026-07-19T00:00:00.000Z", + }; + apiMock.loadSavedSqlLibrary.mockResolvedValue({ folders: [root, child], files: [] }); + + const store = useSavedSqlStore(); + await store.initFromStorage(); + await store.moveFolderToFolder("root", "child"); + + assert.equal(store.allFolders.find((folder) => folder.id === "root")?.parentFolderId, undefined); + assert.equal(apiMock.saveSavedSqlFolder.mock.calls.length, 0); +}); + test("saved SQL summaries load file content on demand", async () => { const summaryFile: SavedSqlFile = { id: "sql-1",