mirror of
https://github.com/t8y2/dbx.git
synced 2026-11-01 08:26:35 +00:00
feat(sql): add saved SQL library
This commit is contained in:
@@ -77,6 +77,8 @@ const editorDialect = computed<"mysql" | "postgres" | "sqlserver">(() => {
|
||||
return "mysql";
|
||||
});
|
||||
|
||||
const shortcutModifier = computed(() => (navigator.platform.toLowerCase().includes("mac") ? "Cmd" : "Ctrl"));
|
||||
|
||||
const hasNumericData = computed(() => {
|
||||
const r = props.activeTab.result;
|
||||
if (!r || r.rows.length === 0) return false;
|
||||
@@ -313,9 +315,10 @@ function onHandleCloseColumnPanel() {
|
||||
</div>
|
||||
<div
|
||||
v-else-if="!activeTab.result"
|
||||
class="flex-1 min-h-0 flex items-center justify-center text-muted-foreground text-sm"
|
||||
class="flex-1 min-h-0 flex flex-col items-center justify-center gap-1 text-muted-foreground text-sm"
|
||||
>
|
||||
{{ t("editor.pressToExecute") }}
|
||||
<div>{{ t("editor.pressToExecute", { mod: shortcutModifier }) }}</div>
|
||||
<div>{{ t("editor.pressToSaveSql", { mod: shortcutModifier }) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -91,6 +91,11 @@ function databaseDisplayName(database: string): string {
|
||||
:variant="activeTab.isExecuting ? 'destructive' : 'ghost'"
|
||||
size="icon"
|
||||
class="h-6 w-6"
|
||||
:class="
|
||||
activeTab.isExecuting
|
||||
? ''
|
||||
: 'bg-emerald-500/10 text-emerald-700 hover:bg-emerald-500/20 hover:text-emerald-800 dark:text-emerald-300 dark:hover:text-emerald-200'
|
||||
"
|
||||
:disabled="
|
||||
activeTab.isCancelling || activeTab.isExplaining || (!activeTab.isExecuting && !executableSql.trim())
|
||||
"
|
||||
@@ -111,6 +116,11 @@ function databaseDisplayName(database: string): string {
|
||||
:variant="activeTab.isExplaining ? 'destructive' : 'ghost'"
|
||||
size="icon"
|
||||
class="h-6 w-6"
|
||||
:class="
|
||||
activeTab.isExplaining
|
||||
? ''
|
||||
: 'text-violet-600 hover:bg-violet-500/10 hover:text-violet-700 dark:text-violet-300 dark:hover:text-violet-200'
|
||||
"
|
||||
:disabled="activeTab.isExecuting || (!activeTab.isExplaining && !executableSql.trim())"
|
||||
@click="activeTab.isExplaining ? emit('cancel') : emit('explain')"
|
||||
>
|
||||
@@ -127,7 +137,7 @@ function databaseDisplayName(database: string): string {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-6"
|
||||
class="h-6 w-6 text-amber-600 hover:bg-amber-500/10 hover:text-amber-700 dark:text-amber-300 dark:hover:text-amber-200"
|
||||
:disabled="activeTab.isExecuting || activeTab.isExplaining || !activeTab.sql.trim()"
|
||||
@click="emit('formatSql')"
|
||||
>
|
||||
@@ -141,7 +151,7 @@ function databaseDisplayName(database: string): string {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-6"
|
||||
class="h-6 w-6 text-blue-600 hover:bg-blue-500/10 hover:text-blue-700 dark:text-blue-300 dark:hover:text-blue-200"
|
||||
:disabled="!activeTab.sql.trim()"
|
||||
@click="emit('saveSql')"
|
||||
>
|
||||
@@ -152,7 +162,12 @@ function databaseDisplayName(database: string): string {
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6" @click="emit('openSql')">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-6 text-sky-600 hover:bg-sky-500/10 hover:text-sky-700 dark:text-sky-300 dark:hover:text-sky-200"
|
||||
@click="emit('openSql')"
|
||||
>
|
||||
<FolderOpen class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
Scissors,
|
||||
CopyPlus,
|
||||
Plus,
|
||||
FileText,
|
||||
} from "lucide-vue-next";
|
||||
import {
|
||||
ContextMenu,
|
||||
@@ -50,6 +51,7 @@ import {
|
||||
} from "@/components/ui/context-menu";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSavedSqlStore } from "@/stores/savedSqlStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { useDatabaseOptions } from "@/composables/useDatabaseOptions";
|
||||
import type { DatabaseType, QueryResult, TreeNode, TreeNodeType } from "@/types/database";
|
||||
@@ -90,6 +92,7 @@ import { Input } from "@/components/ui/input";
|
||||
const { t } = useI18n();
|
||||
const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
const savedSqlStore = useSavedSqlStore();
|
||||
const { toast } = useToast();
|
||||
const { getDatabaseOptions } = useDatabaseOptions();
|
||||
|
||||
@@ -150,6 +153,12 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
|
||||
return { icon: Link, colorClass: "text-blue-400" };
|
||||
case "group-triggers":
|
||||
return { icon: Zap, colorClass: "text-orange-400" };
|
||||
case "saved-sql-root":
|
||||
return { icon: FolderOpen, colorClass: "text-blue-500" };
|
||||
case "saved-sql-folder":
|
||||
return { icon: node.isExpanded ? FolderOpen : FolderClosed, colorClass: "text-blue-400" };
|
||||
case "saved-sql-file":
|
||||
return { icon: FileText, colorClass: "text-blue-300" };
|
||||
case "index":
|
||||
return { icon: Key, colorClass: "text-amber-400" };
|
||||
case "fkey":
|
||||
@@ -167,8 +176,23 @@ function getIconInfo(node: TreeNode): { icon: any; colorClass: string } | null {
|
||||
}
|
||||
}
|
||||
|
||||
const leafTypes: Set<TreeNodeType> = new Set(["column", "index", "fkey", "trigger", "redis-db", "mongo-collection"]);
|
||||
const groupTypes: Set<TreeNodeType> = new Set(["group-columns", "group-indexes", "group-fkeys", "group-triggers"]);
|
||||
const leafTypes: Set<TreeNodeType> = new Set([
|
||||
"column",
|
||||
"index",
|
||||
"fkey",
|
||||
"trigger",
|
||||
"redis-db",
|
||||
"mongo-collection",
|
||||
"saved-sql-file",
|
||||
]);
|
||||
const groupTypes: Set<TreeNodeType> = new Set([
|
||||
"group-columns",
|
||||
"group-indexes",
|
||||
"group-fkeys",
|
||||
"group-triggers",
|
||||
"saved-sql-root",
|
||||
"saved-sql-folder",
|
||||
]);
|
||||
const pinnableTypes: Set<TreeNodeType> = new Set([
|
||||
"connection-group",
|
||||
"database",
|
||||
@@ -194,10 +218,19 @@ async function toggle() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === "saved-sql-root" || node.type === "saved-sql-folder") {
|
||||
node.isExpanded = !node.isExpanded;
|
||||
return;
|
||||
}
|
||||
|
||||
const showSavedSqlWhileLoading =
|
||||
node.type === "connection" && !node.isExpanded && node.children?.some((child) => child.type === "saved-sql-root");
|
||||
|
||||
if (node.isExpanded) {
|
||||
node.isExpanded = false;
|
||||
return;
|
||||
}
|
||||
if (showSavedSqlWhileLoading) node.isExpanded = true;
|
||||
|
||||
try {
|
||||
if (node.type === "connection" && node.connectionId) {
|
||||
@@ -250,11 +283,22 @@ function onClick() {
|
||||
const action = treeNodeRowAction(node.type, canExpand);
|
||||
if (action === "open-data") {
|
||||
openData();
|
||||
} else if (node.type === "saved-sql-file") {
|
||||
openSavedSqlFile();
|
||||
} else if (action === "toggle") {
|
||||
toggle();
|
||||
}
|
||||
}
|
||||
|
||||
function openSavedSqlFile() {
|
||||
const id = props.node.savedSqlId;
|
||||
if (!id) return;
|
||||
const file = savedSqlStore.getFile(id);
|
||||
if (!file) return;
|
||||
queryStore.openSavedSql(file);
|
||||
connectionStore.activeConnectionId = file.connectionId;
|
||||
}
|
||||
|
||||
async function openData() {
|
||||
const node = props.node;
|
||||
if (!(node.type === "table" || node.type === "view") || !node.connectionId || !node.database) return;
|
||||
@@ -1019,6 +1063,9 @@ const hasTypeMenu = computed(() => {
|
||||
t === "table" ||
|
||||
t === "view" ||
|
||||
t === "column" ||
|
||||
t === "saved-sql-root" ||
|
||||
t === "saved-sql-folder" ||
|
||||
t === "saved-sql-file" ||
|
||||
isGroupLabel(props.node)
|
||||
);
|
||||
});
|
||||
@@ -1149,6 +1196,70 @@ const currentGroupId = computed(() => {
|
||||
return null;
|
||||
});
|
||||
|
||||
// --- Saved SQL Library ---
|
||||
const showSavedSqlNameDialog = ref(false);
|
||||
const savedSqlNameMode = ref<"folder-create" | "folder-rename" | "file-rename" | null>(null);
|
||||
const savedSqlNameInput = ref("");
|
||||
const showDeleteSavedSqlFileConfirm = ref(false);
|
||||
const showDeleteSavedSqlFolderConfirm = ref(false);
|
||||
|
||||
function openCreateSavedSqlFolder() {
|
||||
savedSqlNameMode.value = "folder-create";
|
||||
savedSqlNameInput.value = t("savedSql.newFolderDefault");
|
||||
showSavedSqlNameDialog.value = true;
|
||||
}
|
||||
|
||||
function openRenameSavedSqlFolder() {
|
||||
savedSqlNameMode.value = "folder-rename";
|
||||
savedSqlNameInput.value = props.node.label;
|
||||
showSavedSqlNameDialog.value = true;
|
||||
}
|
||||
|
||||
function openRenameSavedSqlFile() {
|
||||
savedSqlNameMode.value = "file-rename";
|
||||
savedSqlNameInput.value = props.node.label.replace(/\.sql$/i, "");
|
||||
showSavedSqlNameDialog.value = true;
|
||||
}
|
||||
|
||||
function confirmSavedSqlName() {
|
||||
const name = savedSqlNameInput.value.trim();
|
||||
if (!name || !props.node.connectionId || !savedSqlNameMode.value) return;
|
||||
|
||||
if (savedSqlNameMode.value === "folder-create") {
|
||||
savedSqlStore.createFolder(props.node.connectionId, name);
|
||||
} else if (savedSqlNameMode.value === "folder-rename" && props.node.savedSqlFolderId) {
|
||||
savedSqlStore.renameFolder(props.node.savedSqlFolderId, name);
|
||||
} else if (savedSqlNameMode.value === "file-rename" && props.node.savedSqlId) {
|
||||
savedSqlStore.renameFile(props.node.savedSqlId, name.endsWith(".sql") ? name : `${name}.sql`);
|
||||
}
|
||||
|
||||
connectionStore.refreshSavedSqlTree(props.node.connectionId);
|
||||
showSavedSqlNameDialog.value = false;
|
||||
savedSqlNameMode.value = null;
|
||||
}
|
||||
|
||||
function deleteSavedSqlFile() {
|
||||
showDeleteSavedSqlFileConfirm.value = true;
|
||||
}
|
||||
|
||||
function confirmDeleteSavedSqlFile() {
|
||||
if (!props.node.savedSqlId) return;
|
||||
savedSqlStore.deleteFile(props.node.savedSqlId);
|
||||
connectionStore.refreshSavedSqlTree(props.node.connectionId);
|
||||
showDeleteSavedSqlFileConfirm.value = false;
|
||||
}
|
||||
|
||||
function deleteSavedSqlFolder() {
|
||||
showDeleteSavedSqlFolderConfirm.value = true;
|
||||
}
|
||||
|
||||
function confirmDeleteSavedSqlFolder() {
|
||||
if (!props.node.savedSqlFolderId) return;
|
||||
savedSqlStore.deleteFolder(props.node.savedSqlFolderId);
|
||||
connectionStore.refreshSavedSqlTree(props.node.connectionId);
|
||||
showDeleteSavedSqlFolderConfirm.value = false;
|
||||
}
|
||||
|
||||
// --- Drag and Drop ---
|
||||
import { useDragSort } from "@/composables/useDragSort";
|
||||
|
||||
@@ -1498,11 +1609,36 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
|
||||
</template>
|
||||
|
||||
<template v-if="isGroupLabel(node)">
|
||||
<ContextMenuItem @click="refresh">
|
||||
<ContextMenuItem v-if="node.type === 'saved-sql-root'" @click="openCreateSavedSqlFolder">
|
||||
<FolderPlus class="w-4 h-4" /> {{ t("savedSql.newFolder") }}
|
||||
</ContextMenuItem>
|
||||
<template v-if="node.type === 'saved-sql-folder'">
|
||||
<ContextMenuItem @click="openRenameSavedSqlFolder">
|
||||
<Pencil class="w-4 h-4" /> {{ t("savedSql.renameFolder") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem class="text-destructive" @click="deleteSavedSqlFolder">
|
||||
<Trash2 class="w-4 h-4" /> {{ t("savedSql.deleteFolder") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
</template>
|
||||
<ContextMenuItem v-if="node.type !== 'saved-sql-root' && node.type !== 'saved-sql-folder'" @click="refresh">
|
||||
<RefreshCw class="w-4 h-4" /> {{ t("contextMenu.refreshChildren") }}
|
||||
</ContextMenuItem>
|
||||
</template>
|
||||
|
||||
<template v-if="node.type === 'saved-sql-file'">
|
||||
<ContextMenuItem @click="openSavedSqlFile">
|
||||
<FileText class="w-4 h-4" /> {{ t("savedSql.open") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @click="openRenameSavedSqlFile">
|
||||
<Pencil class="w-4 h-4" /> {{ t("savedSql.renameFile") }}
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem class="text-destructive" @click="deleteSavedSqlFile">
|
||||
<Trash2 class="w-4 h-4" /> {{ t("savedSql.deleteFile") }}
|
||||
</ContextMenuItem>
|
||||
</template>
|
||||
|
||||
<template v-if="node.type !== 'connection'">
|
||||
<ContextMenuSeparator v-if="hasTypeMenu" />
|
||||
<ContextMenuItem @click="copyName"> <Copy class="w-4 h-4" /> {{ t("contextMenu.copyName") }} </ContextMenuItem>
|
||||
@@ -1566,6 +1702,61 @@ const isDragging = computed(() => dragState.active && dragState.draggedId === pr
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showSavedSqlNameDialog">
|
||||
<DialogContent class="sm:max-w-[380px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{{
|
||||
savedSqlNameMode === "folder-create"
|
||||
? t("savedSql.newFolder")
|
||||
: savedSqlNameMode === "folder-rename"
|
||||
? t("savedSql.renameFolder")
|
||||
: t("savedSql.renameFile")
|
||||
}}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Input v-model="savedSqlNameInput" @keydown.enter.prevent="confirmSavedSqlName" />
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showSavedSqlNameDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="!savedSqlNameInput.trim()" @click="confirmSavedSqlName">{{
|
||||
t("dangerDialog.confirm")
|
||||
}}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showDeleteSavedSqlFileConfirm">
|
||||
<DialogContent class="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("savedSql.deleteFile") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t("savedSql.deleteFileConfirm", { name: node.label }) }}
|
||||
</p>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showDeleteSavedSqlFileConfirm = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button variant="destructive" @click="confirmDeleteSavedSqlFile">{{ t("savedSql.deleteFile") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:open="showDeleteSavedSqlFolderConfirm">
|
||||
<DialogContent class="sm:max-w-[400px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("savedSql.deleteFolder") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{{ t("savedSql.deleteFolderConfirm", { name: node.label }) }}
|
||||
</p>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showDeleteSavedSqlFolderConfirm = false">{{
|
||||
t("dangerDialog.cancel")
|
||||
}}</Button>
|
||||
<Button variant="destructive" @click="confirmDeleteSavedSqlFolder">{{ t("savedSql.deleteFolder") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<DangerConfirmDialog
|
||||
v-model:open="showDropTableConfirm"
|
||||
:title="t('contextMenu.confirmDropTableTitle')"
|
||||
|
||||
Reference in New Issue
Block a user