feat(sql-library): support nested folders
This commit is contained in:
10 files changed
+101
-4
No files matched your search
@@ -421,13 +421,19 @@ function isFolderExpanded(folderId: string) {
|
|||||||
|
|
||||||
async function openNewFolderInput(parentFolderId?: string) {
|
async function openNewFolderInput(parentFolderId?: string) {
|
||||||
const parent = parentFolderId ? savedSqlStore.allFolders.find((folder) => folder.id === parentFolderId) : undefined;
|
const parent = parentFolderId ? savedSqlStore.allFolders.find((folder) => folder.id === parentFolderId) : undefined;
|
||||||
|
if (parentFolderId && !parent) return;
|
||||||
const connectionId = parent?.connectionId || connectionStore.connections[0]?.id;
|
const connectionId = parent?.connectionId || connectionStore.connections[0]?.id;
|
||||||
if (!connectionId) return;
|
if (!connectionId) return;
|
||||||
if (parent?.id) {
|
if (parent?.id) {
|
||||||
collapsedFolders.value = new Set([...collapsedFolders.value].filter((id) => id !== parent.id));
|
collapsedFolders.value = new Set([...collapsedFolders.value].filter((id) => id !== parent.id));
|
||||||
}
|
}
|
||||||
const folder = await savedSqlStore.createFolder(connectionId, t("savedSql.newFolderDefault"), parent?.id);
|
try {
|
||||||
startRenameFolder(folder);
|
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) {
|
async function openNewQueryInFolder(folder?: SavedSqlFolder) {
|
||||||
@@ -892,7 +898,7 @@ const contextMenuItems = computed<CtxMenuItem[]>(() => {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [
|
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("savedSql.newQuery"), action: () => openNewQueryInFolder(target), icon: FilePlus },
|
||||||
{ label: t("sqlLibrary.importIntoFolder"), action: () => importDirectoryIntoLibrary(target), icon: Download },
|
{ label: t("sqlLibrary.importIntoFolder"), action: () => importDirectoryIntoLibrary(target), icon: Download },
|
||||||
{ label: t("sqlLibrary.exportFolder"), action: () => exportFolderContents(target), icon: Upload },
|
{ label: t("sqlLibrary.exportFolder"), action: () => exportFolderContents(target), icon: Upload },
|
||||||
@@ -1150,7 +1156,7 @@ function showDropInside(targetId: string) {
|
|||||||
</Button>
|
</Button>
|
||||||
</LightTooltip>
|
</LightTooltip>
|
||||||
<LightTooltip :text="t('savedSql.newFolder')" side="bottom" :delay="0" :close-delay="0" nowrap>
|
<LightTooltip :text="t('savedSql.newFolder')" side="bottom" :delay="0" :close-delay="0" nowrap>
|
||||||
<Button variant="ghost" size="icon" class="h-5 w-5" @click="openNewFolderInput">
|
<Button variant="ghost" size="icon" class="h-5 w-5" @click="openNewFolderInput()">
|
||||||
<FolderPlus class="h-3 w-3" />
|
<FolderPlus class="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
</LightTooltip>
|
</LightTooltip>
|
||||||
@@ -1225,6 +1231,18 @@ function showDropInside(targetId: string) {
|
|||||||
{{ item.item.name }}
|
{{ item.item.name }}
|
||||||
<span class="ml-1 text-muted-foreground">({{ folderFileCount(item.item.id) }})</span>
|
<span class="ml-1 text-muted-foreground">({{ folderFileCount(item.item.id) }})</span>
|
||||||
</span>
|
</span>
|
||||||
|
<LightTooltip v-if="!isRenamingFolder(item.item.id)" :text="t('savedSql.newSubfolder')" side="left" :delay="0" :close-delay="0" nowrap>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-no-drag="true"
|
||||||
|
class="flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground/70 hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||||
|
:aria-label="t('savedSql.newSubfolder')"
|
||||||
|
@mousedown.stop
|
||||||
|
@click.stop="openNewFolderInput(item.item.id)"
|
||||||
|
>
|
||||||
|
<FolderPlus class="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</LightTooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -1298,6 +1316,18 @@ function showDropInside(targetId: string) {
|
|||||||
{{ row.folder.name }}
|
{{ row.folder.name }}
|
||||||
<span class="ml-1 text-muted-foreground">({{ folderFileCount(row.folder.id) }})</span>
|
<span class="ml-1 text-muted-foreground">({{ folderFileCount(row.folder.id) }})</span>
|
||||||
</span>
|
</span>
|
||||||
|
<LightTooltip v-if="!isRenamingFolder(row.folder.id)" :text="t('savedSql.newSubfolder')" side="left" :delay="0" :close-delay="0" nowrap>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-no-drag="true"
|
||||||
|
class="flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground/70 hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
|
||||||
|
:aria-label="t('savedSql.newSubfolder')"
|
||||||
|
@mousedown.stop
|
||||||
|
@click.stop="openNewFolderInput(row.folder.id)"
|
||||||
|
>
|
||||||
|
<FolderPlus class="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</LightTooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export default {
|
|||||||
rootFolder: "SQL Library",
|
rootFolder: "SQL Library",
|
||||||
newQuery: "New Query",
|
newQuery: "New Query",
|
||||||
newFolder: "New SQL Folder",
|
newFolder: "New SQL Folder",
|
||||||
|
newSubfolder: "New Subfolder",
|
||||||
newFolderDefault: "New Folder",
|
newFolderDefault: "New Folder",
|
||||||
renameFolder: "Rename Folder",
|
renameFolder: "Rename Folder",
|
||||||
deleteFolder: "Delete Folder",
|
deleteFolder: "Delete Folder",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export default withEnglishFallback({
|
|||||||
rootFolder: "Biblioteca SQL",
|
rootFolder: "Biblioteca SQL",
|
||||||
newQuery: "Nueva consulta",
|
newQuery: "Nueva consulta",
|
||||||
newFolder: "Nueva carpeta SQL",
|
newFolder: "Nueva carpeta SQL",
|
||||||
|
newSubfolder: "Nueva subcarpeta",
|
||||||
newFolderDefault: "Nueva carpeta",
|
newFolderDefault: "Nueva carpeta",
|
||||||
renameFolder: "Renombrar carpeta",
|
renameFolder: "Renombrar carpeta",
|
||||||
deleteFolder: "Eliminar carpeta",
|
deleteFolder: "Eliminar carpeta",
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ export default withEnglishFallback({
|
|||||||
rootFolder: "Libreria SQL",
|
rootFolder: "Libreria SQL",
|
||||||
newQuery: "Nuova query",
|
newQuery: "Nuova query",
|
||||||
newFolder: "Nuova Cartella SQL",
|
newFolder: "Nuova Cartella SQL",
|
||||||
|
newSubfolder: "Nuova Sottocartella",
|
||||||
newFolderDefault: "Nuova Cartella",
|
newFolderDefault: "Nuova Cartella",
|
||||||
renameFolder: "Rinomina Cartella",
|
renameFolder: "Rinomina Cartella",
|
||||||
deleteFolder: "Elimina Cartella",
|
deleteFolder: "Elimina Cartella",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export default withEnglishFallback({
|
|||||||
rootFolder: "SQLライブラリ",
|
rootFolder: "SQLライブラリ",
|
||||||
newQuery: "新規クエリ",
|
newQuery: "新規クエリ",
|
||||||
newFolder: "新しいSQLフォルダ",
|
newFolder: "新しいSQLフォルダ",
|
||||||
|
newSubfolder: "新しいサブフォルダ",
|
||||||
newFolderDefault: "新しいフォルダ",
|
newFolderDefault: "新しいフォルダ",
|
||||||
renameFolder: "フォルダ名を変更",
|
renameFolder: "フォルダ名を変更",
|
||||||
deleteFolder: "フォルダを削除",
|
deleteFolder: "フォルダを削除",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export default withEnglishFallback({
|
|||||||
rootFolder: "Biblioteca SQL",
|
rootFolder: "Biblioteca SQL",
|
||||||
newQuery: "Nova Consulta",
|
newQuery: "Nova Consulta",
|
||||||
newFolder: "Nova Pasta SQL",
|
newFolder: "Nova Pasta SQL",
|
||||||
|
newSubfolder: "Nova Subpasta",
|
||||||
newFolderDefault: "Nova Pasta",
|
newFolderDefault: "Nova Pasta",
|
||||||
renameFolder: "Renomear Pasta",
|
renameFolder: "Renomear Pasta",
|
||||||
deleteFolder: "Excluir Pasta",
|
deleteFolder: "Excluir Pasta",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export default withEnglishFallback({
|
|||||||
rootFolder: "SQL 库",
|
rootFolder: "SQL 库",
|
||||||
newQuery: "新建查询",
|
newQuery: "新建查询",
|
||||||
newFolder: "新建 SQL 文件夹",
|
newFolder: "新建 SQL 文件夹",
|
||||||
|
newSubfolder: "新建子文件夹",
|
||||||
newFolderDefault: "新建文件夹",
|
newFolderDefault: "新建文件夹",
|
||||||
renameFolder: "重命名文件夹",
|
renameFolder: "重命名文件夹",
|
||||||
deleteFolder: "删除文件夹",
|
deleteFolder: "删除文件夹",
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export default withEnglishFallback({
|
|||||||
rootFolder: "SQL 庫",
|
rootFolder: "SQL 庫",
|
||||||
newQuery: "新建查詢",
|
newQuery: "新建查詢",
|
||||||
newFolder: "建立 SQL 資料夾",
|
newFolder: "建立 SQL 資料夾",
|
||||||
|
newSubfolder: "建立子資料夾",
|
||||||
newFolderDefault: "建立資料夾",
|
newFolderDefault: "建立資料夾",
|
||||||
renameFolder: "重新命名資料夾",
|
renameFolder: "重新命名資料夾",
|
||||||
deleteFolder: "刪除資料夾",
|
deleteFolder: "刪除資料夾",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
+60
@@ -48,6 +48,66 @@ test("concurrent saved SQL folder creates reuse the same pending folder", async
|
|||||||
assert.equal(store.folders[0]?.id, firstFolder.id);
|
assert.equal(store.folders[0]?.id, firstFolder.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("creates a nested SQL folder under the requested parent", async () => {
|
||||||
|
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 () => {
|
test("saved SQL summaries load file content on demand", async () => {
|
||||||
const summaryFile: SavedSqlFile = {
|
const summaryFile: SavedSqlFile = {
|
||||||
id: "sql-1",
|
id: "sql-1",
|
||||||
|
|||||||
Reference in new issue
Block a user