mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-31 00:22:30 +00:00
feat(sql): add saved SQL library
This commit is contained in:
+106
-23
@@ -16,6 +16,7 @@ import LoginPage from "@/components/auth/LoginPage.vue";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { useSavedSqlStore } from "@/stores/savedSqlStore";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import { useAppUpdater } from "@/composables/useAppUpdater";
|
||||
@@ -35,11 +36,16 @@ import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import { isCloseTabShortcut, isExecuteSqlShortcut } from "@/lib/keyboardShortcuts";
|
||||
import { isPreviewTab } from "@/lib/tabPresentation";
|
||||
import { SQL_FILE_UNSUPPORTED_TYPES } from "@/lib/databaseCapabilities";
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
|
||||
const { t } = useI18n();
|
||||
const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const savedSqlStore = useSavedSqlStore();
|
||||
const { message: toastMessage, visible: toastVisible, toast } = useToast();
|
||||
const { isDark, applyTheme, toggleTheme } = useTheme();
|
||||
const {
|
||||
@@ -75,6 +81,10 @@ const selectedSql = ref("");
|
||||
const cursorPos = ref(0);
|
||||
const formatSqlRequestId = ref(0);
|
||||
const activeOutputView = ref<"result" | "explain" | "chart">("result");
|
||||
const showSaveSqlDialog = ref(false);
|
||||
const saveSqlName = ref("");
|
||||
const saveSqlFolderId = ref("");
|
||||
const ROOT_SAVED_SQL_FOLDER = "__root__";
|
||||
|
||||
const activeTab = computed(() => queryStore.tabs.find((t) => t.id === queryStore.activeTabId));
|
||||
|
||||
@@ -114,6 +124,10 @@ const connectionStats = computed(() => ({
|
||||
types: new Set(connectionStore.connections.map((c) => c.driver_profile || c.db_type)).size,
|
||||
}));
|
||||
const recentConnections = computed(() => connectionStore.connections.slice(0, 5));
|
||||
const saveSqlFolders = computed(() => {
|
||||
const tab = activeTab.value;
|
||||
return tab ? savedSqlStore.listFolders(tab.connectionId) : [];
|
||||
});
|
||||
|
||||
watch(
|
||||
() => queryStore.activeTabId,
|
||||
@@ -142,32 +156,56 @@ function formatActiveSql() {
|
||||
formatSqlRequestId.value++;
|
||||
}
|
||||
|
||||
async function saveSqlToFile() {
|
||||
function defaultSavedSqlName(title: string) {
|
||||
const trimmed = title.trim() || "Query";
|
||||
return trimmed.endsWith(".sql") ? trimmed : `${trimmed}.sql`;
|
||||
}
|
||||
|
||||
function openSaveSqlDialog() {
|
||||
const tab = activeTab.value;
|
||||
if (!tab || !tab.sql.trim()) return;
|
||||
const existing = tab.savedSqlId ? savedSqlStore.getFile(tab.savedSqlId) : undefined;
|
||||
if (existing) {
|
||||
const updated = savedSqlStore.saveFile({
|
||||
id: existing.id,
|
||||
connectionId: tab.connectionId,
|
||||
folderId: existing.folderId,
|
||||
name: existing.name,
|
||||
database: tab.database,
|
||||
schema: tab.schema,
|
||||
sql: tab.sql,
|
||||
});
|
||||
queryStore.linkSavedSql(tab.id, updated.id, updated.name);
|
||||
connectionStore.refreshSavedSqlTree(tab.connectionId);
|
||||
toast(t("savedSql.saved"), 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
saveSqlName.value = defaultSavedSqlName(tab.title);
|
||||
saveSqlFolderId.value = ROOT_SAVED_SQL_FOLDER;
|
||||
showSaveSqlDialog.value = true;
|
||||
}
|
||||
|
||||
function confirmSaveSqlToLibrary() {
|
||||
const tab = activeTab.value;
|
||||
const name = saveSqlName.value.trim();
|
||||
if (!tab || !tab.sql.trim() || !name) return;
|
||||
try {
|
||||
if (isTauriRuntime()) {
|
||||
const { save } = await import("@tauri-apps/plugin-dialog");
|
||||
const { writeTextFile } = await import("@tauri-apps/plugin-fs");
|
||||
const path = await save({
|
||||
defaultPath: `${tab.title}.sql`,
|
||||
filters: [{ name: "SQL", extensions: ["sql"] }],
|
||||
});
|
||||
if (path) {
|
||||
await writeTextFile(path, tab.sql);
|
||||
toast(t("toolbar.sqlSaved"), 2000);
|
||||
}
|
||||
} else {
|
||||
const blob = new Blob([tab.sql], { type: "text/plain" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `${tab.title}.sql`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
const saved = savedSqlStore.saveFile({
|
||||
id: tab.savedSqlId,
|
||||
connectionId: tab.connectionId,
|
||||
folderId: saveSqlFolderId.value === ROOT_SAVED_SQL_FOLDER ? undefined : saveSqlFolderId.value,
|
||||
name: defaultSavedSqlName(name),
|
||||
database: tab.database,
|
||||
schema: tab.schema,
|
||||
sql: tab.sql,
|
||||
});
|
||||
queryStore.linkSavedSql(tab.id, saved.id, saved.name);
|
||||
connectionStore.refreshSavedSqlTree(tab.connectionId);
|
||||
showSaveSqlDialog.value = false;
|
||||
toast(t("savedSql.saved"), 2000);
|
||||
} catch (e: any) {
|
||||
toast(t("toolbar.sqlSaveFailed", { message: e?.message || String(e) }), 5000);
|
||||
toast(t("savedSql.saveFailed", { message: e?.message || String(e) }), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,6 +368,19 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
if (queryStore.activeTabId) queryStore.closeTab(queryStore.activeTabId);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
activeTab.value?.mode === "query" &&
|
||||
!showSaveSqlDialog.value &&
|
||||
!e.isComposing &&
|
||||
!e.altKey &&
|
||||
(e.metaKey || e.ctrlKey) &&
|
||||
e.key.toLowerCase() === "s"
|
||||
) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openSaveSqlDialog();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
activeTab.value?.mode === "query" &&
|
||||
isExecuteSqlShortcut(e) &&
|
||||
@@ -480,7 +531,7 @@ onUnmounted(() => {
|
||||
@cancel="cancelActiveExecution()"
|
||||
@explain="tryExplain()"
|
||||
@format-sql="formatActiveSql"
|
||||
@save-sql="saveSqlToFile"
|
||||
@save-sql="openSaveSqlDialog"
|
||||
@open-sql="openSqlFile"
|
||||
@change-connection="changeActiveConnection"
|
||||
@change-database="changeActiveDatabase"
|
||||
@@ -608,6 +659,38 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<Dialog v-model:open="showSaveSqlDialog">
|
||||
<DialogContent class="sm:max-w-[420px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t("savedSql.saveToLibrary") }}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="space-y-3">
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ t("savedSql.fileName") }}</label>
|
||||
<Input v-model="saveSqlName" @keydown.enter.prevent="confirmSaveSqlToLibrary" />
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<label class="text-xs font-medium text-muted-foreground">{{ t("savedSql.folder") }}</label>
|
||||
<Select v-model="saveSqlFolderId">
|
||||
<SelectTrigger class="h-8 w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent position="popper">
|
||||
<SelectItem :value="ROOT_SAVED_SQL_FOLDER">{{ t("savedSql.rootFolder") }}</SelectItem>
|
||||
<SelectItem v-for="folder in saveSqlFolders" :key="folder.id" :value="folder.id">
|
||||
{{ folder.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showSaveSqlDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button :disabled="!saveSqlName.trim()" @click="confirmSaveSqlToLibrary">{{ t("savedSql.save") }}</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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')"
|
||||
|
||||
+22
-2
@@ -32,7 +32,7 @@ export default {
|
||||
stopExplain: "Stop explain",
|
||||
formatSql: "Format SQL",
|
||||
formatSqlFailed: "Failed to format SQL",
|
||||
saveSql: "Save SQL to file",
|
||||
saveSql: "Save to SQL Library",
|
||||
openSql: "Open SQL file",
|
||||
sqlSaved: "SQL saved",
|
||||
sqlOpenFailed: "Failed to open file: {message}",
|
||||
@@ -65,6 +65,24 @@ export default {
|
||||
filterByType: "Filter by type",
|
||||
clearFilter: "Clear filter",
|
||||
},
|
||||
savedSql: {
|
||||
saveToLibrary: "Save to SQL Library",
|
||||
save: "Save",
|
||||
saved: "SQL saved",
|
||||
saveFailed: "Failed to save SQL: {message}",
|
||||
fileName: "File name",
|
||||
folder: "Folder",
|
||||
rootFolder: "SQL Library",
|
||||
newFolder: "New SQL Folder",
|
||||
newFolderDefault: "New Folder",
|
||||
renameFolder: "Rename Folder",
|
||||
deleteFolder: "Delete Folder",
|
||||
deleteFolderConfirm: "Delete “{name}” and the SQL files inside it?",
|
||||
open: "Open SQL",
|
||||
renameFile: "Rename SQL",
|
||||
deleteFile: "Delete SQL",
|
||||
deleteFileConfirm: "Delete “{name}”?",
|
||||
},
|
||||
connection: {
|
||||
title: "New Connection",
|
||||
name: "Name",
|
||||
@@ -137,7 +155,8 @@ export default {
|
||||
colorPurple: "Purple",
|
||||
},
|
||||
editor: {
|
||||
pressToExecute: "Press Cmd+Enter to execute",
|
||||
pressToExecute: "Press {mod}+Enter to execute",
|
||||
pressToSaveSql: "Press {mod}+S to save SQL",
|
||||
noDatabase: "No database selected",
|
||||
selectConnection: "Select connection",
|
||||
selectDatabase: "Select database",
|
||||
@@ -504,6 +523,7 @@ export default {
|
||||
createSchemaNamePlaceholder: "Schema name",
|
||||
},
|
||||
tree: {
|
||||
savedSql: "SQL Library",
|
||||
columns: "Columns",
|
||||
indexes: "Indexes",
|
||||
foreignKeys: "Foreign Keys",
|
||||
|
||||
@@ -32,7 +32,7 @@ export default {
|
||||
stopExplain: "停止执行计划",
|
||||
formatSql: "格式化 SQL",
|
||||
formatSqlFailed: "SQL 格式化失败",
|
||||
saveSql: "保存 SQL 到文件",
|
||||
saveSql: "保存到 SQL 库",
|
||||
openSql: "打开 SQL 文件",
|
||||
sqlSaved: "SQL 已保存",
|
||||
sqlOpenFailed: "打开文件失败:{message}",
|
||||
@@ -64,6 +64,24 @@ export default {
|
||||
filterByType: "按类型筛选",
|
||||
clearFilter: "清除筛选",
|
||||
},
|
||||
savedSql: {
|
||||
saveToLibrary: "保存到 SQL 库",
|
||||
save: "保存",
|
||||
saved: "SQL 已保存",
|
||||
saveFailed: "保存 SQL 失败:{message}",
|
||||
fileName: "文件名",
|
||||
folder: "文件夹",
|
||||
rootFolder: "SQL 库",
|
||||
newFolder: "新建 SQL 文件夹",
|
||||
newFolderDefault: "新建文件夹",
|
||||
renameFolder: "重命名文件夹",
|
||||
deleteFolder: "删除文件夹",
|
||||
deleteFolderConfirm: "确定删除“{name}”以及其中的 SQL 文件吗?",
|
||||
open: "打开 SQL",
|
||||
renameFile: "重命名 SQL",
|
||||
deleteFile: "删除 SQL",
|
||||
deleteFileConfirm: "确定删除“{name}”吗?",
|
||||
},
|
||||
connection: {
|
||||
title: "新建连接",
|
||||
name: "名称",
|
||||
@@ -136,7 +154,8 @@ export default {
|
||||
colorPurple: "紫色",
|
||||
},
|
||||
editor: {
|
||||
pressToExecute: "按 Cmd+Enter 执行查询",
|
||||
pressToExecute: "按 {mod}+Enter 执行查询",
|
||||
pressToSaveSql: "按 {mod}+S 保存 SQL",
|
||||
noDatabase: "未选择数据库",
|
||||
selectConnection: "选择连接",
|
||||
selectDatabase: "选择数据库",
|
||||
@@ -493,6 +512,7 @@ export default {
|
||||
createSchemaNamePlaceholder: "Schema 名称",
|
||||
},
|
||||
tree: {
|
||||
savedSql: "SQL 库",
|
||||
columns: "字段",
|
||||
indexes: "索引",
|
||||
foreignKeys: "外键",
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface SavedOpenTab {
|
||||
database: string;
|
||||
schema?: string;
|
||||
sql: string;
|
||||
savedSqlId?: string;
|
||||
pinned?: boolean;
|
||||
mode?: QueryTab["mode"];
|
||||
tableMeta?: QueryTab["tableMeta"];
|
||||
@@ -25,6 +26,7 @@ export function serializeOpenTabs(tabs: QueryTab[]): SavedOpenTab[] {
|
||||
database: tab.database,
|
||||
schema: tab.schema,
|
||||
sql: tab.sql,
|
||||
savedSqlId: tab.savedSqlId,
|
||||
pinned: tab.pinned,
|
||||
mode: tab.mode,
|
||||
tableMeta: tab.tableMeta,
|
||||
|
||||
+107
-19
@@ -23,6 +23,7 @@ import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import { isSchemaAware, TREE_SCHEMA_TYPES } from "@/lib/databaseCapabilities";
|
||||
import { buildDatabaseTreeNodes } from "@/lib/databaseTree";
|
||||
import { buildSqlServerDatabaseTreeNodes, SQLSERVER_DEFAULT_SCHEMA } from "@/lib/sqlServerTree";
|
||||
import { useSavedSqlStore } from "@/stores/savedSqlStore";
|
||||
|
||||
const PINNED_TREE_NODES_STORAGE_KEY = "dbx-pinned-tree-nodes";
|
||||
|
||||
@@ -204,6 +205,73 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
loadedTreeNodeChildrenIds.value.add(parent.id);
|
||||
}
|
||||
|
||||
function buildSavedSqlRootNode(connectionId: string, existingRoot?: TreeNode): TreeNode {
|
||||
const savedSqlStore = useSavedSqlStore();
|
||||
const existingById = new Map<string, TreeNode>();
|
||||
const collectExisting = (node?: TreeNode) => {
|
||||
if (!node) return;
|
||||
existingById.set(node.id, node);
|
||||
node.children?.forEach(collectExisting);
|
||||
};
|
||||
collectExisting(existingRoot);
|
||||
|
||||
const fileNode = (file: ReturnType<typeof savedSqlStore.listFiles>[number]): TreeNode => ({
|
||||
id: `${connectionId}:__saved_sql:file:${file.id}`,
|
||||
label: file.name,
|
||||
type: "saved-sql-file",
|
||||
connectionId,
|
||||
database: file.database,
|
||||
schema: file.schema,
|
||||
savedSqlId: file.id,
|
||||
});
|
||||
|
||||
const folderNodes = savedSqlStore.listFolders(connectionId).map((folder) => {
|
||||
const id = `${connectionId}:__saved_sql:folder:${folder.id}`;
|
||||
const existing = existingById.get(id);
|
||||
return {
|
||||
id,
|
||||
label: folder.name,
|
||||
type: "saved-sql-folder" as const,
|
||||
connectionId,
|
||||
savedSqlFolderId: folder.id,
|
||||
isExpanded: existing?.isExpanded ?? true,
|
||||
children: savedSqlStore.listFiles(connectionId, folder.id).map(fileNode),
|
||||
};
|
||||
});
|
||||
|
||||
const rootId = `${connectionId}:__saved_sql`;
|
||||
return {
|
||||
id: rootId,
|
||||
label: "tree.savedSql",
|
||||
type: "saved-sql-root",
|
||||
connectionId,
|
||||
isExpanded: existingRoot?.isExpanded ?? true,
|
||||
children: [...folderNodes, ...savedSqlStore.listFiles(connectionId).map(fileNode)],
|
||||
};
|
||||
}
|
||||
|
||||
function withSavedSqlRoot(connectionId: string, children: TreeNode[], existingConnectionNode?: TreeNode): TreeNode[] {
|
||||
const existingRoot = existingConnectionNode?.children?.find((child) => child.type === "saved-sql-root");
|
||||
const nonSavedChildren = children.filter((child) => child.type !== "saved-sql-root");
|
||||
return [buildSavedSqlRootNode(connectionId, existingRoot), ...nonSavedChildren];
|
||||
}
|
||||
|
||||
function refreshSavedSqlTree(connectionId?: string) {
|
||||
const refresh = (nodes: TreeNode[]) => {
|
||||
for (const node of nodes) {
|
||||
if (node.type === "connection" && node.connectionId && (!connectionId || node.connectionId === connectionId)) {
|
||||
node.children = withSavedSqlRoot(
|
||||
node.connectionId,
|
||||
(node.children || []).filter((child) => child.type !== "saved-sql-root"),
|
||||
node,
|
||||
);
|
||||
}
|
||||
if (node.children) refresh(node.children);
|
||||
}
|
||||
};
|
||||
refresh(treeNodes.value);
|
||||
}
|
||||
|
||||
function schemaCacheKey(...parts: string[]): string {
|
||||
return parts.map((part) => encodeURIComponent(part)).join(":");
|
||||
}
|
||||
@@ -211,7 +279,10 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
async function loadPersistedTreeChildren(node: TreeNode, cacheKey: string): Promise<boolean> {
|
||||
const children = await api.loadSchemaCache<TreeNode[]>(cacheKey).catch(() => null);
|
||||
if (!children) return false;
|
||||
setChildren(node, children);
|
||||
setChildren(
|
||||
node,
|
||||
node.type === "connection" && node.connectionId ? withSavedSqlRoot(node.connectionId, children, node) : children,
|
||||
);
|
||||
node.isExpanded = true;
|
||||
return true;
|
||||
}
|
||||
@@ -427,7 +498,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
try {
|
||||
await ensureConnected(connectionId);
|
||||
const databases = await api.listDatabases(connectionId);
|
||||
const children = buildDatabaseTreeNodes(connectionId, databases);
|
||||
const children = withSavedSqlRoot(connectionId, buildDatabaseTreeNodes(connectionId, databases), node);
|
||||
setChildren(node, children);
|
||||
await savePersistedTreeChildren(cacheKey, children);
|
||||
node.isExpanded = true;
|
||||
@@ -446,15 +517,19 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const dbs = await api.redisListDatabases(connectionId);
|
||||
setChildren(
|
||||
node,
|
||||
dbs.map((db) => ({
|
||||
id: `${connectionId}:db${db}`,
|
||||
label: `db${db}`,
|
||||
type: "redis-db" as const,
|
||||
withSavedSqlRoot(
|
||||
connectionId,
|
||||
database: String(db),
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
})),
|
||||
dbs.map((db) => ({
|
||||
id: `${connectionId}:db${db}`,
|
||||
label: `db${db}`,
|
||||
type: "redis-db" as const,
|
||||
connectionId,
|
||||
database: String(db),
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
})),
|
||||
node,
|
||||
),
|
||||
);
|
||||
node.isExpanded = true;
|
||||
} finally {
|
||||
@@ -472,15 +547,19 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
const dbs = await api.mongoListDatabases(connectionId);
|
||||
setChildren(
|
||||
node,
|
||||
dbs.map((db) => ({
|
||||
id: `${connectionId}:${db}`,
|
||||
label: db,
|
||||
type: "mongo-db" as const,
|
||||
withSavedSqlRoot(
|
||||
connectionId,
|
||||
database: db,
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
})),
|
||||
dbs.map((db) => ({
|
||||
id: `${connectionId}:${db}`,
|
||||
label: db,
|
||||
type: "mongo-db" as const,
|
||||
connectionId,
|
||||
database: db,
|
||||
isExpanded: false,
|
||||
children: [],
|
||||
})),
|
||||
node,
|
||||
),
|
||||
);
|
||||
node.isExpanded = true;
|
||||
} finally {
|
||||
@@ -936,7 +1015,15 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
return { ...node, children: mergeState(node.children || []) };
|
||||
}
|
||||
if (existing && node.type === "connection") {
|
||||
return { ...existing, label: node.label, pinned: node.pinned };
|
||||
return {
|
||||
...existing,
|
||||
label: node.label,
|
||||
pinned: node.pinned,
|
||||
children: withSavedSqlRoot(node.connectionId!, existing.children || [], existing),
|
||||
};
|
||||
}
|
||||
if (node.type === "connection" && node.connectionId) {
|
||||
return { ...node, children: withSavedSqlRoot(node.connectionId, node.children || []) };
|
||||
}
|
||||
return node;
|
||||
});
|
||||
@@ -1113,6 +1200,7 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
activeConnectionId,
|
||||
treeNodes,
|
||||
refreshAllTree,
|
||||
refreshSavedSqlTree,
|
||||
refreshTreeNode,
|
||||
connectedIds,
|
||||
connectionErrors,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { restoreOpenTabsState, serializeOpenTabs } from "@/lib/openTabsPersisten
|
||||
import * as api from "@/lib/api";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { isTauriRuntime } from "@/lib/tauriRuntime";
|
||||
import type { SavedSqlFile } from "@/stores/savedSqlStore";
|
||||
|
||||
const STORAGE_KEY = "dbx-open-tabs";
|
||||
const ACTIVE_TAB_KEY = "dbx-active-tab";
|
||||
@@ -111,6 +112,39 @@ export const useQueryStore = defineStore("query", () => {
|
||||
}
|
||||
}
|
||||
|
||||
function linkSavedSql(id: string, savedSqlId: string, title?: string) {
|
||||
const tab = tabs.value.find((t) => t.id === id);
|
||||
if (!tab) return;
|
||||
tab.savedSqlId = savedSqlId;
|
||||
if (title) tab.title = title;
|
||||
}
|
||||
|
||||
function openSavedSql(file: SavedSqlFile) {
|
||||
const existing = tabs.value.find((tab) => tab.savedSqlId === file.id);
|
||||
if (existing) {
|
||||
activeTabId.value = existing.id;
|
||||
return existing.id;
|
||||
}
|
||||
|
||||
const id = uuid();
|
||||
const tab: QueryTab = {
|
||||
id,
|
||||
title: file.name,
|
||||
connectionId: file.connectionId,
|
||||
database: file.database,
|
||||
schema: file.schema,
|
||||
sql: file.sql,
|
||||
savedSqlId: file.id,
|
||||
isExecuting: false,
|
||||
isCancelling: false,
|
||||
isExplaining: false,
|
||||
mode: "query",
|
||||
};
|
||||
tabs.value.push(tab);
|
||||
activeTabId.value = id;
|
||||
return id;
|
||||
}
|
||||
|
||||
function togglePinnedTab(id: string) {
|
||||
const tab = tabs.value.find((t) => t.id === id);
|
||||
if (!tab) return;
|
||||
@@ -426,6 +460,8 @@ export const useQueryStore = defineStore("query", () => {
|
||||
closeOtherTabs,
|
||||
closeAllTabs,
|
||||
updateSql,
|
||||
linkSavedSql,
|
||||
openSavedSql,
|
||||
togglePinnedTab,
|
||||
updateDatabase,
|
||||
updateSchema,
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { computed, ref } from "vue";
|
||||
import { uuid } from "@/lib/utils";
|
||||
|
||||
const STORAGE_KEY = "dbx-saved-sql-library";
|
||||
|
||||
export interface SavedSqlFolder {
|
||||
id: string;
|
||||
connectionId: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface SavedSqlFile {
|
||||
id: string;
|
||||
connectionId: string;
|
||||
folderId?: string;
|
||||
name: string;
|
||||
database: string;
|
||||
schema?: string;
|
||||
sql: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface SavedSqlState {
|
||||
folders: SavedSqlFolder[];
|
||||
files: SavedSqlFile[];
|
||||
}
|
||||
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function loadState(): SavedSqlState {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) return { folders: [], files: [] };
|
||||
const parsed = JSON.parse(raw) as Partial<SavedSqlState>;
|
||||
return {
|
||||
folders: Array.isArray(parsed.folders) ? parsed.folders.filter((item) => item?.id && item?.connectionId) : [],
|
||||
files: Array.isArray(parsed.files) ? parsed.files.filter((item) => item?.id && item?.connectionId) : [],
|
||||
};
|
||||
} catch {
|
||||
return { folders: [], files: [] };
|
||||
}
|
||||
}
|
||||
|
||||
export const useSavedSqlStore = defineStore("savedSql", () => {
|
||||
const initial = loadState();
|
||||
const folders = ref<SavedSqlFolder[]>(initial.folders);
|
||||
const files = ref<SavedSqlFile[]>(initial.files);
|
||||
|
||||
const version = computed(
|
||||
() => `${folders.value.length}:${files.value.length}:${files.value.map((f) => f.updatedAt).join("|")}`,
|
||||
);
|
||||
|
||||
function persist() {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify({ folders: folders.value, files: files.value }));
|
||||
}
|
||||
|
||||
function listFolders(connectionId: string) {
|
||||
return folders.value
|
||||
.filter((folder) => folder.connectionId === connectionId)
|
||||
.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: "base" }));
|
||||
}
|
||||
|
||||
function listFiles(connectionId: string, folderId?: string) {
|
||||
return files.value
|
||||
.filter((file) => file.connectionId === connectionId && (file.folderId || "") === (folderId || ""))
|
||||
.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: "base" }));
|
||||
}
|
||||
|
||||
function getFile(id: string) {
|
||||
return files.value.find((file) => file.id === id);
|
||||
}
|
||||
|
||||
function createFolder(connectionId: string, name: string) {
|
||||
const timestamp = nowIso();
|
||||
const folder: SavedSqlFolder = {
|
||||
id: uuid(),
|
||||
connectionId,
|
||||
name,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
folders.value = [...folders.value, folder];
|
||||
persist();
|
||||
return folder;
|
||||
}
|
||||
|
||||
function renameFolder(id: string, name: string) {
|
||||
const timestamp = nowIso();
|
||||
folders.value = folders.value.map((folder) =>
|
||||
folder.id === id ? { ...folder, name, updatedAt: timestamp } : folder,
|
||||
);
|
||||
persist();
|
||||
}
|
||||
|
||||
function deleteFolder(id: string) {
|
||||
folders.value = folders.value.filter((folder) => folder.id !== id);
|
||||
files.value = files.value.filter((file) => file.folderId !== id);
|
||||
persist();
|
||||
}
|
||||
|
||||
function saveFile(input: {
|
||||
id?: string;
|
||||
connectionId: string;
|
||||
folderId?: string;
|
||||
name: string;
|
||||
database: string;
|
||||
schema?: string;
|
||||
sql: string;
|
||||
}) {
|
||||
const timestamp = nowIso();
|
||||
if (input.id) {
|
||||
const existing = getFile(input.id);
|
||||
if (existing) {
|
||||
const updated: SavedSqlFile = {
|
||||
...existing,
|
||||
folderId: input.folderId || undefined,
|
||||
name: input.name,
|
||||
database: input.database,
|
||||
schema: input.schema,
|
||||
sql: input.sql,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
files.value = files.value.map((file) => (file.id === input.id ? updated : file));
|
||||
persist();
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
|
||||
const file: SavedSqlFile = {
|
||||
id: uuid(),
|
||||
connectionId: input.connectionId,
|
||||
folderId: input.folderId || undefined,
|
||||
name: input.name,
|
||||
database: input.database,
|
||||
schema: input.schema,
|
||||
sql: input.sql,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
};
|
||||
files.value = [...files.value, file];
|
||||
persist();
|
||||
return file;
|
||||
}
|
||||
|
||||
function renameFile(id: string, name: string) {
|
||||
const timestamp = nowIso();
|
||||
files.value = files.value.map((file) => (file.id === id ? { ...file, name, updatedAt: timestamp } : file));
|
||||
persist();
|
||||
}
|
||||
|
||||
function deleteFile(id: string) {
|
||||
files.value = files.value.filter((file) => file.id !== id);
|
||||
persist();
|
||||
}
|
||||
|
||||
return {
|
||||
folders,
|
||||
files,
|
||||
version,
|
||||
listFolders,
|
||||
listFiles,
|
||||
getFile,
|
||||
createFolder,
|
||||
renameFolder,
|
||||
deleteFolder,
|
||||
saveFile,
|
||||
renameFile,
|
||||
deleteFile,
|
||||
};
|
||||
});
|
||||
@@ -107,6 +107,9 @@ export type TreeNodeType =
|
||||
| "group-indexes"
|
||||
| "group-fkeys"
|
||||
| "group-triggers"
|
||||
| "saved-sql-root"
|
||||
| "saved-sql-folder"
|
||||
| "saved-sql-file"
|
||||
| "column"
|
||||
| "index"
|
||||
| "fkey"
|
||||
@@ -142,6 +145,8 @@ export interface TreeNode {
|
||||
database?: string;
|
||||
schema?: string;
|
||||
tableName?: string;
|
||||
savedSqlId?: string;
|
||||
savedSqlFolderId?: string;
|
||||
meta?: ColumnInfo | IndexInfo | ForeignKeyInfo | TriggerInfo;
|
||||
}
|
||||
|
||||
@@ -152,6 +157,7 @@ export interface QueryTab {
|
||||
database: string;
|
||||
schema?: string;
|
||||
sql: string;
|
||||
savedSqlId?: string;
|
||||
lastExecutedSql?: string;
|
||||
resultBaseSql?: string;
|
||||
resultSortedSql?: string;
|
||||
|
||||
Reference in New Issue
Block a user