feat(sql-library): add move to folder action

This commit is contained in:
t8y2 committed 2026-07-04 00:37:23 +08:00
1 parent 69f880fdf1
commit 614daa5b19
11 files changed
+234 -6

No files matched your search

@@ -51,6 +51,19 @@ function getConnectionLabel(connectionId: string) {
return conn?.name || connectionId;
}
function folderPath(folder: SavedSqlFolder) {
const folderById = new Map(savedSqlStore.allFolders.map((item) => [item.id, item]));
const parts: string[] = [];
const seen = new Set<string>();
let current: SavedSqlFolder | undefined = folder;
while (current && !seen.has(current.id)) {
seen.add(current.id);
parts.unshift(current.name);
current = current.parentFolderId ? folderById.get(current.parentFolderId) : undefined;
}
return parts.join(" / ");
}
function activeImportConnectionId() {
return connectionStore.activeConnectionId || connectionStore.connections[0]?.id || "";
}
@@ -586,6 +599,14 @@ async function executeBatchDelete() {
toast(t("sqlLibrary.batchDeleteSuccess", { count: fileIds.length + folderIds.length }), 2000);
}
async function moveFilesToFolder(fileIds: string[], folderId?: string) {
const movableIds = [...new Set(fileIds)].filter((id) => savedSqlStore.getFile(id));
if (movableIds.length === 0) return;
await savedSqlStore.moveFilesToFolder(movableIds, folderId);
clearSelection();
toast(t("sqlLibrary.moveSuccess", { count: movableIds.length }), 2000);
}
async function openFile(file: SavedSqlFile) {
if (suppressNextRowClick.value) return;
const loadedFile = await savedSqlStore.ensureFileContent(file.id);
@@ -717,13 +738,52 @@ function handleFolderClick(folder: SavedSqlFolder, event: MouseEvent) {
const contextTarget = ref<SavedSqlFolder | SavedSqlFile | "panel" | null>(null);
function folderMoveMenuItems(fileIds: string[]): CtxMenuItem[] {
const files = [...new Set(fileIds)].map((id) => savedSqlStore.getFile(id)).filter((file): file is SavedSqlFile => Boolean(file));
const allInUnfiled = files.length > 0 && files.every((file) => !file.folderId);
const folderItems = savedSqlStore.allFoldersTreeOrder
.filter((folder) => isConnectionVisible(folder.connectionId))
.map((folder) => ({
label: folderPath(folder),
action: () =>
moveFilesToFolder(
files.map((file) => file.id),
folder.id,
),
disabled: files.every((file) => file.folderId === folder.id),
icon: FolderClosed,
}));
return [
{
label: t("sqlLibrary.unfiled"),
action: () =>
moveFilesToFolder(
files.map((file) => file.id),
undefined,
),
disabled: files.length === 0 || allInUnfiled,
icon: FolderOpen,
},
...(folderItems.length > 0 ? [{ label: "", separator: true }, ...folderItems] : []),
];
}
const contextMenuItems = computed<CtxMenuItem[]>(() => {
const target = contextTarget.value;
if (!target) return [];
// If there's selection, show batch delete option
if (hasSelection.value) {
const selectedFiles = Array.from(selectedFileIds.value);
return [
{
label: t("sqlLibrary.moveSelectedToFolder", { count: selectedFiles.length }),
icon: FolderClosed,
children: folderMoveMenuItems(selectedFiles),
visible: selectedFiles.length > 0,
},
{ label: "", separator: true, visible: selectedFiles.length > 0 },
{
label: t("sqlLibrary.batchDelete", { count: selectedCount.value }),
action: confirmBatchDelete,
@@ -756,6 +816,7 @@ const contextMenuItems = computed<CtxMenuItem[]>(() => {
return [
{ label: t("savedSql.open"), action: () => openFile(target), icon: FileText },
{ label: t("sqlLibrary.exportFile"), action: () => exportSingleFile(target), icon: FileInput },
{ label: t("sqlLibrary.moveToFolder"), icon: FolderClosed, children: folderMoveMenuItems([target.id]) },
{ label: "", separator: true },
{ label: t("savedSql.renameFile"), action: () => startRenameFile(target), icon: Pencil },
{ label: "", separator: true },
@@ -52,9 +52,11 @@ const subRef = ref<HTMLElement>();
const subX = ref(0);
const subY = ref(0);
let subCloseTimer: ReturnType<typeof setTimeout> | null = null;
let subAnchorRect: { left: number; right: number; top: number; bottom: number } | null = null;
function close() {
activeSubIndex.value = null;
subAnchorRect = null;
show.value = false;
}
@@ -147,6 +149,7 @@ function onItemMouseEnter(index: number, event: MouseEvent) {
}
const trigger = event.currentTarget as HTMLElement;
const rect = trigger.getBoundingClientRect();
subAnchorRect = { left: rect.left, right: rect.right, top: rect.top, bottom: rect.bottom };
subX.value = rect.right + 4;
subY.value = rect.top;
activeSubIndex.value = index;
@@ -194,11 +197,25 @@ function adjustSubPosition() {
const rect = subRef.value.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
if (rect.right > vw) {
subX.value = Math.max(0, vw - rect.width - 8);
const margin = 8;
const gap = 4;
if (subAnchorRect) {
const rightX = subAnchorRect.right + gap;
const leftX = subAnchorRect.left - rect.width - gap;
if (rightX + rect.width <= vw - margin) {
subX.value = rightX;
} else if (leftX >= margin) {
subX.value = leftX;
} else {
subX.value = Math.max(margin, Math.min(rightX, vw - rect.width - margin));
}
} else if (rect.right > vw - margin) {
subX.value = Math.max(margin, vw - rect.width - margin);
}
if (rect.bottom > vh) {
subY.value = Math.max(0, vh - rect.height - 8);
if (rect.bottom > vh - margin) {
subY.value = Math.max(margin, vh - rect.height - margin);
} else if (rect.top < margin) {
subY.value = margin;
}
// When the submenu flips left due to right-edge overflow, it may land
// under the mouse cursor. Since the mouse didn't move, mouseenter won't
@@ -272,8 +289,8 @@ onBeforeUnmount(() => {
<div
v-if="show && activeSubIndex !== null && items[activeSubIndex]?.children?.length"
ref="subRef"
:style="{ position: 'fixed', left: subX + 'px', top: subY + 'px', zIndex: 10000 }"
class="bg-popover text-popover-foreground min-w-40 rounded-[6px] p-1 overflow-x-hidden overflow-y-auto ring-1 ring-foreground/10 shadow-lg"
:style="{ position: 'fixed', left: subX + 'px', top: subY + 'px', zIndex: 10000, maxHeight: 'min(420px, calc(100vh - 16px))' }"
class="bg-popover text-popover-foreground w-56 max-w-[calc(100vw-16px)] rounded-[6px] p-1 overflow-x-hidden overflow-y-auto ring-1 ring-foreground/10 shadow-lg"
@mouseenter="onSubMouseEnter"
@mouseleave="onSubMouseLeave"
>