Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ node_modules
.superpowers/
.trellis/
.worktrees/
.trae/
openspec/
.mcp.json
AGENTS.md
Expand Down
69 changes: 69 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 72 additions & 2 deletions apps/desktop/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useProductionSafetyStore } from "@/stores/productionSafetyStore";
import { enforceRightSidebarPanelExclusivity, RIGHT_SIDEBAR_PANEL_IDS, transitionRightSidebarPanels, useSettingsStore, type RightSidebarPanelId, type RightSidebarPanelState } from "@/stores/settingsStore";
import { useSavedSqlStore } from "@/stores/savedSqlStore";
import { usePromptTemplateStore } from "@/stores/promptTemplateStore";
import { useProjectStore } from "@/stores/projectStore";
import { useToast } from "@/composables/useToast";
import { useTheme } from "@/composables/useTheme";
import { useAppUpdater } from "@/composables/useAppUpdater";
Expand Down Expand Up @@ -145,6 +146,7 @@ const settingsStore = useSettingsStore();
const savedSqlStore = useSavedSqlStore();
const promptTemplateStore = usePromptTemplateStore();
const recentConnectionIds = ref<readonly string[]>(parseRecentConnectionIds(safeLocalStorageGet(RECENT_CONNECTION_IDS_STORAGE_KEY)));
const projectStore = useProjectStore();
connectionStore.setBeforeConnectHandler(async (config) => {
await ensureJdbcxRuntimeDrivers(config, api);
const jdbcProductRuntimeBefore = JSON.stringify({
Expand Down Expand Up @@ -493,6 +495,7 @@ const { setupTauriListeners, cleanupTauriListeners } = useTauriEvents({
openSqlFilePath,
openDbFilePath,
openConnectionDeepLink,
openSqlProjectPaths,
});
const { showCloseActionPrompt, chooseQuit, chooseMinimize, cancelCloseActionPrompt, performCloseAction, setupCloseActionPromptListener, cleanupCloseActionPromptListener } = useCloseActionPrompt({ requestClose: requestAppClose });
useVisibilityChange();
Expand Down Expand Up @@ -1024,9 +1027,19 @@ function handleCloseActionPromptOpenChange(open: boolean) {
async function writeExternalSqlTab(tab: QueryTab, options: { closeAfterSave?: boolean; expectedContentHash?: string; expectedMissing?: boolean } = {}): Promise<"saved" | "retry" | "failed"> {
if (!tab.externalSqlPath || !isTauriRuntime()) return "failed";
try {
// Local History 保底:写回前把磁盘当前内容记入项目快照(仅项目内文件)。
if (tab.projectId) {
try {
await api.snapshotSqlFileBeforeSave(tab.projectId, tab.externalSqlPath);
} catch {
// 快照失败不阻断保存(快照仅为保底,非关键路径)
}
}
const result = await api.writeExternalSqlFile(tab.externalSqlPath, tab.sql, {
expectedContentHash: options.expectedContentHash,
expectedMissing: options.expectedMissing,
encoding: tab.fileEncoding,
lineEnding: tab.fileLineEnding,
});
if (result.kind !== "written") return "retry";
rememberExternalSqlFileTarget(tab.externalSqlPath, { connectionId: tab.connectionId, database: tab.database, catalog: tab.catalog });
Expand Down Expand Up @@ -1419,7 +1432,19 @@ async function openSqlFilePath(path: string) {
await desktopOpenTabsRestorationBarrier?.settled;
const snapshot = await api.readExternalSqlFileSnapshot(path);
const target = resolveExternalSqlFileTarget(path, (savedConnectionId) => !!connectionStore.getConfig(savedConnectionId), unassociatedExternalSqlFileTarget());
queryStore.openExternalSqlFile(target.connectionId, target.database, path, snapshot.content, snapshot.version, target.catalog);
let projectId: string | undefined;
try {
await projectStore.ensureLoaded();
projectId = projectStore.projectForFilePath(path)?.id ?? undefined;
} catch {
projectId = undefined;
}
queryStore.openExternalSqlFile(target.connectionId, target.database, path, snapshot.content, snapshot.version, {
catalog: target.catalog,
projectId,
fileEncoding: snapshot.encoding,
fileLineEnding: snapshot.lineEnding,
});
} catch (e: any) {
toast(t("toolbar.sqlOpenFailed", { message: externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)) }), 5000);
}
Expand All @@ -1437,6 +1462,32 @@ async function openPendingSqlFiles() {
}
}

async function openSqlProjectPaths(paths: string[]) {
if (!isTauriRuntime() || paths.length === 0) return;
try {
const opened = await projectStore.openProjects(paths);
if (opened.length === 0) return;
openRightSidebarPanel("sqlFile");
if (opened.length === 1) {
toast(t("toolbar.sqlProjectOpened", { name: opened[0].name }));
} else {
toast(t("toolbar.sqlProjectsOpened", { name: opened[0].name, count: opened.length - 1 }));
}
} catch (e: any) {
toast(t("toolbar.sqlOpenFailed", { message: e?.message || String(e) }), 5000);
}
}

async function openPendingSqlProjects() {
if (!isTauriRuntime()) return;
try {
const paths = await api.pendingOpenSqlProjects();
await openSqlProjectPaths(paths);
} catch {
/* ignore startup project-open probing errors */
}
}

async function openDbFilePath(path: string) {
if (!isTauriRuntime()) return;
await connectionStore.initFromDisk();
Expand Down Expand Up @@ -2021,7 +2072,19 @@ async function handleQuickOpenSelect(item: any) {
try {
const snapshot = await api.readExternalSqlFileSnapshot(item.filePath);
const target = resolveExternalSqlFileTarget(item.filePath, (savedConnectionId) => !!connectionStore.getConfig(savedConnectionId), unassociatedExternalSqlFileTarget());
queryStore.openExternalSqlFile(target.connectionId, target.database, item.filePath, snapshot.content, snapshot.version, target.catalog);
let projectId: string | undefined;
try {
await projectStore.ensureLoaded();
projectId = projectStore.projectForFilePath(item.filePath)?.id ?? undefined;
} catch {
projectId = undefined;
}
queryStore.openExternalSqlFile(target.connectionId, target.database, item.filePath, snapshot.content, snapshot.version, {
catalog: target.catalog,
projectId,
fileEncoding: snapshot.encoding,
fileLineEnding: snapshot.lineEnding,
});
} catch (e: any) {
toast(
externalSqlFileOpenErrorMessage(e, (key, params) => t(key, params)),
Expand Down Expand Up @@ -2455,6 +2518,10 @@ function openDriverStoreFromEvent(event: Event) {
openDriverStorePage(((event as CustomEvent).detail as DriverStoreFocus | undefined) ?? null);
}

function showSqlFilePanelFromEvent() {
openRightSidebarPanel("sqlFile");
}

function runUpdateNotificationChecks() {
if (!updateNotificationsEnabled.value) return;
checkUpdates({ silent: true });
Expand Down Expand Up @@ -2489,6 +2556,7 @@ onMounted(async () => {
window.addEventListener("keydown", handleNativeSelectAll, true);
window.addEventListener("keydown", handleKeydown);
window.addEventListener("dbx-open-driver-store", openDriverStoreFromEvent);
window.addEventListener("dbx-show-sql-file-panel", showSqlFilePanelFromEvent);
window.addEventListener("dbx-mcp-status-changed", handleMcpStatusChanged);
if (isDesktop) {
document.addEventListener("contextmenu", handleContextMenu);
Expand Down Expand Up @@ -2545,6 +2613,7 @@ onMounted(async () => {
void openPendingSqlFiles();
void openPendingDbFiles();
void openPendingConnectionLinks();
void openPendingSqlProjects();
console.log(`[STARTUP] onMounted sync done: ${(performance.now() - mountStart).toFixed(0)}ms`);
});

Expand All @@ -2557,6 +2626,7 @@ onUnmounted(() => {
window.removeEventListener("keydown", handleNativeSelectAll, true);
window.removeEventListener("keydown", handleKeydown);
window.removeEventListener("dbx-open-driver-store", openDriverStoreFromEvent);
window.removeEventListener("dbx-show-sql-file-panel", showSqlFilePanelFromEvent);
window.removeEventListener("dbx-mcp-status-changed", handleMcpStatusChanged);
document.removeEventListener("contextmenu", handleContextMenu);
window.clearTimeout(sqlLibraryFlyAnimationTimer);
Expand Down
8 changes: 7 additions & 1 deletion apps/desktop/src/components/layout/AppDialogs.vue
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,13 @@ watch(
:prefill-schema="dialogs.dataComparePrefillSchema.value"
:prefill-table="dialogs.dataComparePrefillTable.value"
/>
<SqlFileExecutionDialog v-model:open="dialogs.showSqlFileDialog.value" :prefill-connection-id="dialogs.sqlFilePrefillConnectionId.value" :prefill-database="dialogs.sqlFilePrefillDatabase.value" :prefill-file-path="dialogs.sqlFilePrefillFilePath.value" />
<SqlFileExecutionDialog
v-model:open="dialogs.showSqlFileDialog.value"
:prefill-connection-id="dialogs.sqlFilePrefillConnectionId.value"
:prefill-database="dialogs.sqlFilePrefillDatabase.value"
:prefill-file-path="dialogs.sqlFilePrefillFilePath.value"
:prefill-file-paths="dialogs.sqlFilePrefillFilePaths.value"
/>
<SchemaDiagramDialog
v-if="dialogs.showDiagramDialog.value"
v-model:open="dialogs.showDiagramDialog.value"
Expand Down
96 changes: 96 additions & 0 deletions apps/desktop/src/components/layout/ProjectSettingsDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { useConnectionStore } from "@/stores/connectionStore";
import type { SqlProject } from "@/lib/backend/tauri";

const NONE_CONNECTION_VALUE = "__none__";

const props = defineProps<{
project: SqlProject | null;
}>();

const emit = defineEmits<{
save: [project: SqlProject];
}>();

const open = defineModel<boolean>("open", { default: false });

const { t } = useI18n();
const connectionStore = useConnectionStore();

const name = ref("");
const connectionId = ref<string>(NONE_CONNECTION_VALUE);
const defaultSchema = ref("");

const sqlConnections = computed(() => connectionStore.connections.filter((connection) => !["redis", "mongodb", "elasticsearch", "easysearch", "qdrant", "milvus", "weaviate", "chromadb", "etcd", "zookeeper", "consul", "mq", "nacos"].includes(connection.db_type)));

watch(
open,
(value) => {
if (!value || !props.project) return;
name.value = props.project.name;
connectionId.value = props.project.connectionId || NONE_CONNECTION_VALUE;
defaultSchema.value = props.project.defaultSchema || "";
},
{ immediate: true },
);

function connectionLabel(id: string): string {
return connectionStore.connections.find((connection) => connection.id === id)?.name || id;
}

function handleSave() {
if (!props.project) return;
emit("save", {
...props.project,
name: name.value.trim() || props.project.name,
connectionId: connectionId.value === NONE_CONNECTION_VALUE ? null : connectionId.value,
defaultSchema: defaultSchema.value.trim() ? defaultSchema.value.trim() : null,
});
open.value = false;
}
</script>

<template>
<Dialog v-model:open="open">
<DialogContent class="sm:max-w-[440px]">
<DialogHeader>
<DialogTitle>{{ t("sqlFileTree.projectSettings") }}</DialogTitle>
<DialogDescription class="truncate" :title="project?.rootPath">{{ project?.rootPath }}</DialogDescription>
</DialogHeader>
<div class="grid gap-3 py-2">
<div class="grid gap-1.5">
<Label class="text-[13px]">{{ t("sqlFileTree.projectName") }}</Label>
<Input v-model="name" class="h-8 text-[13px]" :placeholder="t('sqlFileTree.projectName')" @keydown.enter.prevent="handleSave" />
</div>
<div class="grid gap-1.5">
<Label class="text-[13px]">{{ t("sqlFileTree.boundConnection") }}</Label>
<Select v-model="connectionId">
<SelectTrigger class="h-8 w-full text-[13px]">
<SelectValue :placeholder="t('sqlFileTree.noBoundConnection')" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="NONE_CONNECTION_VALUE">{{ t("sqlFileTree.noBoundConnection") }}</SelectItem>
<SelectItem v-for="connection in sqlConnections" :key="connection.id" :value="connection.id">{{ connectionLabel(connection.id) }}</SelectItem>
</SelectContent>
</Select>
<p class="text-[11px] text-muted-foreground">{{ t("sqlFileTree.boundConnectionHint") }}</p>
</div>
<div class="grid gap-1.5">
<Label class="text-[13px]">{{ t("sqlFileTree.defaultSchema") }}</Label>
<Input v-model="defaultSchema" class="h-8 text-[13px]" :placeholder="t('sqlFileTree.defaultSchemaPlaceholder')" @keydown.enter.prevent="handleSave" />
</div>
</div>
<DialogFooter>
<Button variant="outline" size="sm" @click="open = false">{{ t("dangerDialog.cancel") }}</Button>
<Button size="sm" @click="handleSave">{{ t("dangerDialog.confirm") }}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>
Loading
Loading