feat(sql-library): support nested folders
This commit is contained in:
parent
84745bb349
commit
b43be528e1
|
|
@ -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<CtxMenuItem[]>(() => {
|
|||
];
|
||||
}
|
||||
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) {
|
|||
</Button>
|
||||
</LightTooltip>
|
||||
<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" />
|
||||
</Button>
|
||||
</LightTooltip>
|
||||
|
|
@ -1225,6 +1231,18 @@ function showDropInside(targetId: string) {
|
|||
{{ item.item.name }}
|
||||
<span class="ml-1 text-muted-foreground">({{ folderFileCount(item.item.id) }})</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
|
||||
|
|
@ -1298,6 +1316,18 @@ function showDropInside(targetId: string) {
|
|||
{{ row.folder.name }}
|
||||
<span class="ml-1 text-muted-foreground">({{ folderFileCount(row.folder.id) }})</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
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ export default {
|
|||
rootFolder: "SQL Library",
|
||||
newQuery: "New Query",
|
||||
newFolder: "New SQL Folder",
|
||||
newSubfolder: "New Subfolder",
|
||||
newFolderDefault: "New Folder",
|
||||
renameFolder: "Rename Folder",
|
||||
deleteFolder: "Delete Folder",
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ export default withEnglishFallback({
|
|||
rootFolder: "Biblioteca SQL",
|
||||
newQuery: "Nueva consulta",
|
||||
newFolder: "Nueva carpeta SQL",
|
||||
newSubfolder: "Nueva subcarpeta",
|
||||
newFolderDefault: "Nueva carpeta",
|
||||
renameFolder: "Renombrar carpeta",
|
||||
deleteFolder: "Eliminar carpeta",
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ export default withEnglishFallback({
|
|||
rootFolder: "Libreria SQL",
|
||||
newQuery: "Nuova query",
|
||||
newFolder: "Nuova Cartella SQL",
|
||||
newSubfolder: "Nuova Sottocartella",
|
||||
newFolderDefault: "Nuova Cartella",
|
||||
renameFolder: "Rinomina Cartella",
|
||||
deleteFolder: "Elimina Cartella",
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ export default withEnglishFallback({
|
|||
rootFolder: "SQLライブラリ",
|
||||
newQuery: "新規クエリ",
|
||||
newFolder: "新しいSQLフォルダ",
|
||||
newSubfolder: "新しいサブフォルダ",
|
||||
newFolderDefault: "新しいフォルダ",
|
||||
renameFolder: "フォルダ名を変更",
|
||||
deleteFolder: "フォルダを削除",
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ export default withEnglishFallback({
|
|||
rootFolder: "Biblioteca SQL",
|
||||
newQuery: "Nova Consulta",
|
||||
newFolder: "Nova Pasta SQL",
|
||||
newSubfolder: "Nova Subpasta",
|
||||
newFolderDefault: "Nova Pasta",
|
||||
renameFolder: "Renomear Pasta",
|
||||
deleteFolder: "Excluir Pasta",
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ export default withEnglishFallback({
|
|||
rootFolder: "SQL 库",
|
||||
newQuery: "新建查询",
|
||||
newFolder: "新建 SQL 文件夹",
|
||||
newSubfolder: "新建子文件夹",
|
||||
newFolderDefault: "新建文件夹",
|
||||
renameFolder: "重命名文件夹",
|
||||
deleteFolder: "删除文件夹",
|
||||
|
|
|
|||
|
|
@ -114,6 +114,7 @@ export default withEnglishFallback({
|
|||
rootFolder: "SQL 庫",
|
||||
newQuery: "新建查詢",
|
||||
newFolder: "建立 SQL 資料夾",
|
||||
newSubfolder: "建立子資料夾",
|
||||
newFolderDefault: "建立資料夾",
|
||||
renameFolder: "重新命名資料夾",
|
||||
deleteFolder: "刪除資料夾",
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
|
|
@ -48,6 +48,66 @@ test("concurrent saved SQL folder creates reuse the same pending folder", async
|
|||
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 () => {
|
||||
const summaryFile: SavedSqlFile = {
|
||||
id: "sql-1",
|
||||
|
|
|
|||
Loading…
Reference in New Issue