diff --git a/.github/scripts/check-jdbc-plugin-version.mjs b/.github/scripts/check-jdbc-plugin-version.mjs index 266930d5ee..538b2015b7 100644 --- a/.github/scripts/check-jdbc-plugin-version.mjs +++ b/.github/scripts/check-jdbc-plugin-version.mjs @@ -13,10 +13,7 @@ function manifestVersion(manifestJson) { return JSON.parse(manifestJson).version ?? ""; } -export function evaluateJdbcPluginVersionChange({ - headPomVersion, - headManifestVersion, -}) { +export function evaluateJdbcPluginVersionChange({ headPomVersion, headManifestVersion }) { const errors = []; if (headPomVersion !== headManifestVersion) { errors.push(`JDBC plugin version mismatch: pom.xml is ${headPomVersion} but manifest.json is ${headManifestVersion}.`); diff --git a/.github/scripts/evaluate-issue-content.sh b/.github/scripts/evaluate-issue-content.sh deleted file mode 100644 index 488eba4184..0000000000 --- a/.github/scripts/evaluate-issue-content.sh +++ /dev/null @@ -1,28 +0,0 @@ -set -euo pipefail - -issue_text="$( - jq -r ' - [ - .issue.title // "", - .issue.body // "" - ] | join("\n") - ' "$GITHUB_EVENT_PATH" -)" - -issue_number="$(jq -r '.issue.number' "$GITHUB_EVENT_PATH")" - -prefix="$(printf '\345\210\267')" -cn_target="$(printf '\346\230\237')" -ascii_target="$(printf '\163\164\141\162')" -pattern="${prefix}[[:space:]]*(${cn_target}|${ascii_target})" - -if grep -Eiq "$pattern" <<<"$issue_text"; then - matched=true -else - matched=false -fi - -{ - printf 'matched=%s\n' "$matched" - printf 'issue_number=%s\n' "$issue_number" -} >>"$GITHUB_OUTPUT" diff --git a/.github/scripts/sync-changelog.mjs b/.github/scripts/sync-changelog.mjs index b42bbea43e..8011641729 100644 --- a/.github/scripts/sync-changelog.mjs +++ b/.github/scripts/sync-changelog.mjs @@ -1,38 +1,35 @@ #!/usr/bin/env node -import { createHash } from 'node:crypto'; -import { writeFileSync } from 'node:fs'; -import { resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { createHash } from "node:crypto"; +import { writeFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; -const REPO = 't8y2/dbx'; -const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ''; -const DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY || ''; -const OUT_CN = 'releases-cn.json'; -const OUT_EN = 'releases-en.json'; -const EN_CACHE_URL = process.env.CHANGELOG_EN_CACHE_URL || 'https://dl.dbxio.com/changelog/releases-en.json'; +const REPO = "t8y2/dbx"; +const GITHUB_TOKEN = process.env.GITHUB_TOKEN || ""; +const DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY || ""; +const OUT_CN = "releases-cn.json"; +const OUT_EN = "releases-en.json"; +const EN_CACHE_URL = process.env.CHANGELOG_EN_CACHE_URL || "https://dl.dbxio.com/changelog/releases-en.json"; const SECTION_MAP = { - '新功能': 'added', - 'Added': 'added', - '改进': 'improved', - 'Improved': 'improved', - '修复': 'fixed', - 'Fixed': 'fixed', - '变更': 'changed', - 'Changed': 'changed', - '移除': 'removed', - 'Removed': 'removed', + 新功能: "added", + Added: "added", + 改进: "improved", + Improved: "improved", + 修复: "fixed", + Fixed: "fixed", + 变更: "changed", + Changed: "changed", + 移除: "removed", + Removed: "removed", }; export async function fetchAllReleases() { const releases = []; let page = 1; while (true) { - const res = await fetch( - `https://api.github.com/repos/${REPO}/releases?per_page=100&page=${page}`, - { headers: { Authorization: `token ${GITHUB_TOKEN}`, Accept: 'application/vnd.github+json' } }, - ); + const res = await fetch(`https://api.github.com/repos/${REPO}/releases?per_page=100&page=${page}`, { headers: { Authorization: `token ${GITHUB_TOKEN}`, Accept: "application/vnd.github+json" } }); if (!res.ok) throw new Error(`GitHub API ${res.status}: ${await res.text()}`); const data = await res.json(); if (data.length === 0) break; @@ -43,7 +40,7 @@ export async function fetchAllReleases() { } export function stripDownloadSection(body) { - const markers = ['### 下载安装', '### Download', '### 系统要求', '### System Requirements']; + const markers = ["### 下载安装", "### Download", "### 系统要求", "### System Requirements"]; let idx = body.length; for (const m of markers) { const i = body.indexOf(m); @@ -57,11 +54,11 @@ export function parseBody(body) { const sections = []; let current = null; - for (const line of cleaned.split('\n')) { + for (const line of cleaned.split("\n")) { const headerMatch = line.match(/^###\s+(.+)/); if (headerMatch) { const title = headerMatch[1].trim(); - const type = SECTION_MAP[title] || 'other'; + const type = SECTION_MAP[title] || "other"; current = { type, title, items: [] }; sections.push(current); continue; @@ -77,7 +74,7 @@ export function parseBody(body) { const plainMatch = line.match(/^-\s+(.+)/); if (plainMatch) { - current.items.push({ title: plainMatch[1].trim(), desc: '' }); + current.items.push({ title: plainMatch[1].trim(), desc: "" }); } } @@ -85,16 +82,16 @@ export function parseBody(body) { } export function buildReleaseSourceHash(release) { - return createHash('sha256') + return createHash("sha256") .update( JSON.stringify({ tag: release.tag_name, name: release.name || release.tag_name, - publishedAt: release.published_at || '', - body: release.body || '', + publishedAt: release.published_at || "", + body: release.body || "", }), ) - .digest('hex'); + .digest("hex"); } export function buildReleasesJson(releases, now = new Date()) { @@ -108,7 +105,7 @@ export function buildReleasesJson(releases, now = new Date()) { name: r.name || r.tag_name, date: r.published_at.slice(0, 10), _sourceHash: buildReleaseSourceHash(r), - sections: parseBody(r.body || ''), + sections: parseBody(r.body || ""), })), }; } @@ -116,18 +113,15 @@ export function buildReleasesJson(releases, now = new Date()) { function releaseToMarkdown(release) { return release.sections .map((s) => { - const items = s.items.map((i) => (i.desc ? `- **${i.title}** — ${i.desc}` : `- ${i.title}`)).join('\n'); + const items = s.items.map((i) => (i.desc ? `- **${i.title}** — ${i.desc}` : `- ${i.title}`)).join("\n"); return `### ${s.title}\n${items}`; }) - .join('\n\n'); + .join("\n\n"); } -export async function fetchCachedEnglish({ - cacheUrl = EN_CACHE_URL, - fetchImpl = fetch, -} = {}) { +export async function fetchCachedEnglish({ cacheUrl = EN_CACHE_URL, fetchImpl = fetch } = {}) { try { - const res = await fetchImpl(cacheUrl, { headers: { Accept: 'application/json' } }); + const res = await fetchImpl(cacheUrl, { headers: { Accept: "application/json" } }); if (!res.ok) { console.warn(`English changelog cache unavailable: ${res.status}`); return null; @@ -139,17 +133,9 @@ export async function fetchCachedEnglish({ } } -export async function translateToEnglish( - cnJson, - { - cachedEnJson = null, - deepseekApiKey = DEEPSEEK_API_KEY, - fetchImpl = fetch, - sleep = (ms) => new Promise((r) => setTimeout(r, ms)), - } = {}, -) { +export async function translateToEnglish(cnJson, { cachedEnJson = null, deepseekApiKey = DEEPSEEK_API_KEY, fetchImpl = fetch, sleep = (ms) => new Promise((r) => setTimeout(r, ms)) } = {}) { if (!deepseekApiKey) { - console.warn('DEEPSEEK_API_KEY not set, skipping translation'); + console.warn("DEEPSEEK_API_KEY not set, skipping translation"); return null; } @@ -178,18 +164,18 @@ export async function translateToEnglish( continue; } - const res = await fetchImpl('https://api.deepseek.com/chat/completions', { - method: 'POST', - headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${deepseekApiKey}` }, + const res = await fetchImpl("https://api.deepseek.com/chat/completions", { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: `Bearer ${deepseekApiKey}` }, body: JSON.stringify({ - model: 'deepseek-chat', + model: "deepseek-chat", messages: [ { - role: 'system', + role: "system", content: - 'You are a technical translator. Translate the following Chinese software changelog to English. Keep the exact markdown format (### headers, - bullet points, **bold** titles, — dashes). Only translate, do not add or remove content. Keep technical terms, product names, and contributor names unchanged.', + "You are a technical translator. Translate the following Chinese software changelog to English. Keep the exact markdown format (### headers, - bullet points, **bold** titles, — dashes). Only translate, do not add or remove content. Keep technical terms, product names, and contributor names unchanged.", }, - { role: 'user', content: sectionsText }, + { role: "user", content: sectionsText }, ], temperature: 0.1, }), @@ -202,7 +188,7 @@ export async function translateToEnglish( } const data = await res.json(); - const translated = data.choices?.[0]?.message?.content || ''; + const translated = data.choices?.[0]?.message?.content || ""; const enSections = parseBody(translated); enReleases.push({ ...release, sections: enSections.length > 0 ? enSections : release.sections }); translatedCount++; @@ -215,7 +201,7 @@ export async function translateToEnglish( } async function main() { - console.log('Fetching releases from GitHub...'); + console.log("Fetching releases from GitHub..."); const releases = await fetchAllReleases(); console.log(`Found ${releases.length} releases`); @@ -225,17 +211,17 @@ async function main() { writeFileSync(OUT_CN, JSON.stringify(cnJson, null, 2)); console.log(`Wrote ${OUT_CN}`); - console.log('Fetching cached English changelog...'); + console.log("Fetching cached English changelog..."); const cachedEnJson = await fetchCachedEnglish(); - console.log('Translating to English...'); + console.log("Translating to English..."); const enJson = await translateToEnglish(cnJson, { cachedEnJson }); if (enJson) { writeFileSync(OUT_EN, JSON.stringify(enJson, null, 2)); console.log(`Wrote ${OUT_EN}`); } - console.log('Done!'); + console.log("Done!"); } if (process.argv[1] && fileURLToPath(import.meta.url) === resolve(process.argv[1])) { diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 969a06083a..74eb283b46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,7 +66,7 @@ jobs: - name: Rust cache uses: swatinem/rust-cache@v2 with: - workspaces: './ -> target' + workspaces: "./ -> target" shared-key: ci-x86_64-unknown-linux-gnu - name: Cargo fmt check @@ -89,7 +89,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: temurin - java-version: '17' + java-version: "17" cache: maven - name: JDBC plugin version guard diff --git a/.github/workflows/detect-abusive-content.yml b/.github/workflows/detect-abusive-content.yml deleted file mode 100644 index e1b44b4621..0000000000 --- a/.github/workflows/detect-abusive-content.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: Issue moderation - -# 这段 workflow 只用于检测并处理极少数恶意诽谤型 Issue。 -# 我们不希望仓库的 issue 区变成争吵、造谣或情绪宣泄的地方,也不希望真正来反馈问题、 -# 提交建议、讨论兼容性的用户被这些内容打扰。 -# -# 关于“刷星”“刷 Star”这类指控,这里也留一段诚恳的说明,给维护者和以后回看配置的人看: -# DBX 的 Star 和关注度不是一天突然冒出来的,也不是靠任何灰色手段堆出来的。这个项目是一点点写、 -# 一点点修、一个问题一个问题回复、一个版本一个版本发布走到现在的。过程中确实有一些外部来源带来了 -# 可见增长,比如用户在社群里的推荐、技术博主的介绍、开源资讯站点的收录、不同平台上的自然传播, -# 以及使用者觉得有帮助后顺手点的 Star。这些增长并不总是平均发生的,有时某个平台推荐一次, -# 某篇文章或视频传播一次,短时间内就会带来一波新增关注;这在开源项目里是很正常的。 -# 我们也真心感谢每一位认真使用、反馈问题、提出建议、写文章、录视频、在社群里推荐或只是默默点 Star -# 的朋友。正是这些具体而真实的支持,让项目有动力继续往前走。 -# -# 我们理解外界可以有疑问,也欢迎基于事实的讨论。但把没有证据的“刷星”“刷 Star”反复发到 issue 里, -# 对排查问题、改进软件、维护社区氛围都没有帮助。对于这种明显不是技术反馈、也不提供事实依据的内容, -# 这里选择自动删除,而不是公开争辩。这样做不是为了回避监督,而是为了保护正常用户的讨论空间, -# 也避免让恶意内容通过 issue 区获得更多曝光。 -# -# 如果将来确实有人提出具体、可核验、善意的问题,维护者仍然可以正常回应;这个 workflow 只匹配非常窄的 -# 关键词范围:“刷星”以及大小写不敏感的“刷star”。 -# -# This workflow exists only for a small number of abusive or defamatory issues. -# We do not want the issue tracker to become a place for rumors, arguments, or repeated bad-faith accusations. -# DBX did not appear out of nowhere, and its stars were not manufactured through any gray-market shortcut. -# The project has grown step by step: by writing code, fixing bugs, answering questions, shipping releases, -# and listening to people who actually use it. Some growth also came from natural external exposure: -# community recommendations, blog posts, open-source newsletters, social platforms, videos, and users who found -# the project useful enough to star it. Open-source attention is rarely linear; a single recommendation or post -# can bring a visible wave of new users, and that is normal. -# We are genuinely grateful to everyone who uses DBX seriously, reports bugs, suggests improvements, writes posts, -# records videos, recommends it in communities, or simply leaves a star because the project helped them. That real, -# concrete support is what keeps the project moving forward. -# -# Honest questions and evidence-based discussion are welcome. Repeated claims such as "刷星" or "刷star" without -# evidence are not useful technical feedback, and they make the issue tracker worse for everyone who is here to -# report bugs or improve the software. For those narrow cases, we choose to delete the issue quietly instead -# of amplifying it through public arguments. - -on: - issues: - types: [opened] - -permissions: - issues: write - -jobs: - issue-policy: - name: Review issue content - runs-on: ubuntu-latest - if: github.event_name == 'issues' - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - persist-credentials: false - - - name: Evaluate issue content - id: check - run: bash .github/scripts/evaluate-issue-content.sh - - - name: Apply issue policy - if: steps.check.outputs.matched == 'true' - env: - GH_TOKEN: ${{ secrets.ISSUE_DELETE_TOKEN || github.token }} - ISSUE_NUMBER: ${{ steps.check.outputs.issue_number }} - # Deleting issues may require an admin-scoped token. If github.token is - # not enough in this repository, set ISSUE_DELETE_TOKEN in repo secrets. - run: gh issue delete "$ISSUE_NUMBER" --yes --repo "$GITHUB_REPOSITORY" diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 45dfa9df9b..c23e88cef9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,9 +3,9 @@ name: Deploy Docs on: push: branches: [main] - paths: ['docs/**'] + paths: ["docs/**"] workflow_run: - workflows: ['Publish Packages'] + workflows: ["Publish Packages"] types: [completed] workflow_dispatch: diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml index 3dcb328931..522fc34232 100644 --- a/.github/workflows/publish-packages.yml +++ b/.github/workflows/publish-packages.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: inputs: tag: - description: 'Release tag (e.g. v0.3.10)' + description: "Release tag (e.g. v0.3.10)" required: true permissions: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d3c98f87d8..f89ce602f2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: Release on: push: tags: - - 'v*' + - "v*" permissions: contents: write @@ -107,7 +107,7 @@ jobs: - name: Rust cache uses: swatinem/rust-cache@v2 with: - workspaces: './ -> target' + workspaces: "./ -> target" shared-key: release-${{ matrix.target }}-${{ steps.deps-hash.outputs.hash }} add-rust-environment-hash-key: false cache-on-failure: true @@ -173,7 +173,7 @@ jobs: APPLE_TEAM_ID: ${{ startsWith(matrix.platform, 'macos') && secrets.APPLE_TEAM_ID || '' }} with: tagName: ${{ github.ref_name }} - releaseName: 'DBX ${{ github.ref_name }}' + releaseName: "DBX ${{ github.ref_name }}" releaseBody: ${{ steps.release-notes.outputs.body }} releaseDraft: true prerelease: false @@ -246,7 +246,7 @@ jobs: uses: actions/setup-java@v4 with: distribution: temurin - java-version: '17' + java-version: "17" cache: maven - name: Apply automatic JDBC plugin version bump @@ -302,7 +302,6 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh release edit ${{ github.ref_name }} --repo ${{ github.repository }} --draft=false --prerelease - docker: runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/sync-changelog.yml b/.github/workflows/sync-changelog.yml index 833e554f2c..8f7f173340 100644 --- a/.github/workflows/sync-changelog.yml +++ b/.github/workflows/sync-changelog.yml @@ -2,7 +2,7 @@ name: Sync Changelog to R2 on: workflow_run: - workflows: ['Publish Packages'] + workflows: ["Publish Packages"] types: [completed] workflow_dispatch: diff --git a/.oxfmtrc.json b/.oxfmtrc.json index 98e1cc0438..660b096c30 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -1,6 +1,6 @@ { "$schema": "./node_modules/oxfmt/configuration_schema.json", - "printWidth": 120, + "printWidth": 300, "tabWidth": 2, "singleQuote": false, "trailingComma": "all", diff --git a/README.zh-CN.md b/README.zh-CN.md index 04d03284c1..2018a9a4ac 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -258,13 +258,13 @@ pnpm tauri build ## 技术栈 -| 层级 | 技术 | -|------|------| -| 框架 | [Tauri 2](https://tauri.app/) | -| 前端 | [Vue 3](https://vuejs.org/) + TypeScript | -| UI | [shadcn-vue](https://www.shadcn-vue.com/) + Tailwind CSS | -| 编辑器 | [CodeMirror 6](https://codemirror.net/) | -| 后端 | Rust + [sqlx](https://github.com/launchbadge/sqlx) / [tiberius](https://github.com/prisma/tiberius) / [redis-rs](https://github.com/redis-rs/redis-rs) / [mongodb](https://github.com/mongodb/mongo-rust-driver) | +| 层级 | 技术 | +| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 框架 | [Tauri 2](https://tauri.app/) | +| 前端 | [Vue 3](https://vuejs.org/) + TypeScript | +| UI | [shadcn-vue](https://www.shadcn-vue.com/) + Tailwind CSS | +| 编辑器 | [CodeMirror 6](https://codemirror.net/) | +| 后端 | Rust + [sqlx](https://github.com/launchbadge/sqlx) / [tiberius](https://github.com/prisma/tiberius) / [redis-rs](https://github.com/redis-rs/redis-rs) / [mongodb](https://github.com/mongodb/mongo-rust-driver) | ## 社区 diff --git a/apps/desktop/public/icons/database/turso.png b/apps/desktop/public/icons/database/turso.png new file mode 100644 index 0000000000..d78843d483 Binary files /dev/null and b/apps/desktop/public/icons/database/turso.png differ diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 87c68655d8..328a922884 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -70,6 +70,7 @@ import type { AiAction } from "@/lib/ai"; const AiAssistant = defineAsyncComponent(() => import("@/components/editor/AiAssistant.vue")); const QueryHistory = defineAsyncComponent(() => import("@/components/editor/QueryHistory.vue")); +const SqlLibraryPanel = defineAsyncComponent(() => import("@/components/layout/SqlLibraryPanel.vue")); const DriverStorePage = defineAsyncComponent(() => import("@/components/config/DriverStoreDialog.vue")); const UpdateDialog = defineAsyncComponent(() => import("@/components/layout/UpdateDialog.vue")); const LoginPage = defineAsyncComponent(() => import("@/components/auth/LoginPage.vue")); @@ -85,21 +86,7 @@ const settingsStore = useSettingsStore(); const savedSqlStore = useSavedSqlStore(); const { message: toastMessage, visible: toastVisible, toast } = useToast(); const { isDark, themeMode, applyTheme, setThemeMode } = useTheme(); -const { - checkingUpdates, - updateInfo, - updateCheckMessage, - showUpdateDialog, - isDownloadingUpdate, - downloadProgress, - updateReady, - hasUpdateAvailable, - openUrl, - checkUpdates, - openLatestRelease, - downloadAndInstallUpdate, - restartApp, -} = useAppUpdater(); +const { checkingUpdates, updateInfo, updateCheckMessage, showUpdateDialog, isDownloadingUpdate, downloadProgress, updateReady, hasUpdateAvailable, openUrl, checkUpdates, openLatestRelease, downloadAndInstallUpdate, restartApp } = useAppUpdater(); const { setupFileDrop } = useFileDrop(); const isDesktop = isTauriRuntime(); @@ -116,10 +103,10 @@ const showDriverStore = ref(false); const agentDriverUpdateCount = ref(0); const showHistory = ref(false); const showAiPanel = ref(safeLocalStorageGet("dbx-ai-panel-open") === "true"); +const showSqlLibraryPanel = ref(safeLocalStorageGet("dbx-sql-library-open") === "true"); const sidebarOpen = ref(safeLocalStorageGet("dbx-sidebar-open") !== "false"); const aiPanelReady = ref(false); -const { sidebarWidth, aiPanelWidth, historyWidth, startSidebarResize, startAiPanelResize, startHistoryResize } = - usePanelResize(); +const { sidebarWidth, aiPanelWidth, historyWidth, sqlLibraryWidth, startSidebarResize, startAiPanelResize, startHistoryResize, startSqlLibraryResize } = usePanelResize(); const aiAssistantRef = ref(null); const appSidebarRef = ref | null>(null); const contentAreaRef = ref | null>(null); @@ -196,18 +183,7 @@ async function resolveActiveExecutableSql() { : ""; } -const { - dangerSql, - pendingDangerSql, - showDangerDialog, - suppressDangerConfirm, - tryExecute, - doExecute, - cancelActiveExecution, - tryExplain, - onDangerConfirm, - explainMode, -} = useSqlExecution({ +const { dangerSql, pendingDangerSql, showDangerDialog, suppressDangerConfirm, tryExecute, doExecute, cancelActiveExecution, tryExplain, onDangerConfirm, explainMode } = useSqlExecution({ activeTab, activeConnection, executableSql, @@ -217,8 +193,7 @@ const { const dialogs = useDialogSources(); const { getDatabaseOptions } = useDatabaseOptions(); -const { openLineageTarget, openDatabaseSearchTarget, onStructureEditorSaved, openTableTarget } = - useNavigationTargets(dialogs); +const { openLineageTarget, openDatabaseSearchTarget, onStructureEditorSaved, openTableTarget } = useNavigationTargets(dialogs); const { onExecuteSql, onReloadData, onPaginate, onSort } = useDataGridActions(activeTab); const { setupTauriListeners, cleanupTauriListeners } = useTauriEvents({ openTableTarget, @@ -231,13 +206,9 @@ useVisibilityChange(); const appVersion = ref(""); const isClassicLayout = computed(() => settingsStore.editorSettings.appLayout === "classic"); const updateNotificationsEnabled = computed(() => settingsStore.editorSettings.updateNotificationsEnabled); -const toolbarAgentDriverUpdateCount = computed(() => - updateNotificationsEnabled.value ? agentDriverUpdateCount.value : 0, -); +const toolbarAgentDriverUpdateCount = computed(() => (updateNotificationsEnabled.value ? agentDriverUpdateCount.value : 0)); const toolbarHasUpdateAvailable = computed(() => updateNotificationsEnabled.value && hasUpdateAvailable.value); -const hasSqlFileConnections = computed(() => - connectionStore.connections.some((c) => supportsSqlFileExecution(c.db_type)), -); +const hasSqlFileConnections = computed(() => connectionStore.connections.some((c) => supportsSqlFileExecution(c.db_type))); const connectionStats = computed(() => ({ total: connectionStore.connections.length, connected: connectionStore.connectedIds.size, @@ -245,8 +216,7 @@ const connectionStats = computed(() => ({ })); const recentConnections = computed(() => connectionStore.connections.slice(0, 5)); const saveSqlFolders = computed(() => { - const tab = activeTab.value; - return tab ? savedSqlStore.listFolders(tab.connectionId) : []; + return savedSqlStore.allFolders; }); async function applyUiScale(scale: number) { @@ -281,11 +251,7 @@ function isGlobalUiZoomTarget(target: EventTarget | null): target is Element { if (target.closest("[data-query-editor-root], [data-cell-detail-editor-root], [data-object-source-editor]")) { return true; } - if ( - target instanceof HTMLInputElement || - target instanceof HTMLTextAreaElement || - (target instanceof HTMLElement && target.isContentEditable) - ) { + if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || (target instanceof HTMLElement && target.isContentEditable)) { return false; } return !target.closest("[contenteditable='true']"); @@ -325,6 +291,11 @@ function toggleAiPanel() { safeLocalStorageSet("dbx-ai-panel-open", String(showAiPanel.value)); } +function toggleSqlLibrary() { + showSqlLibraryPanel.value = !showSqlLibraryPanel.value; + safeLocalStorageSet("dbx-sql-library-open", String(showSqlLibraryPanel.value)); +} + function fixWithAi(errorMessage: string) { if (!showAiPanel.value) { showAiPanel.value = true; @@ -371,8 +342,9 @@ function formatActiveSql() { } function defaultSavedSqlName(title: string) { - const trimmed = title.trim() || "Query"; - return trimmed.endsWith(".sql") ? trimmed : `${trimmed}.sql`; + const trimmed = title.trim() || "query"; + const normalized = trimmed.replace(/\s+/g, "_"); + return normalized.endsWith(".sql") ? normalized : `${normalized}.sql`; } async function openSaveSqlDialog() { @@ -394,7 +366,6 @@ async function openSaveSqlDialog() { sql: tab.sql, }); queryStore.linkSavedSql(tab.id, updated.id, updated.name); - connectionStore.refreshSavedSqlTree(tab.connectionId); toast(t("savedSql.saved"), 2000); return; } @@ -445,7 +416,6 @@ async function confirmSaveSqlToLibrary() { 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) { @@ -490,8 +460,7 @@ async function openSqlFilePath(path: string) { if (!isTauriRuntime()) return; try { const content = await api.readExternalSqlFile(path); - const connectionId = - connectionStore.activeConnectionId || activeTab.value?.connectionId || connectionStore.connections[0]?.id || ""; + const connectionId = connectionStore.activeConnectionId || activeTab.value?.connectionId || connectionStore.connections[0]?.id || ""; const connection = connectionId ? connectionStore.getConfig(connectionId) : undefined; const database = activeTab.value?.database || (connection ? resolveDefaultDatabase(connection, []) : ""); const tabId = queryStore.createTab(connectionId, database, sqlFileTitleFromPath(path), "query"); @@ -524,6 +493,7 @@ function getDbTypeFromPath(path: string): "sqlite" | "duckdb" | null { async function openDbFilePath(path: string) { if (!isTauriRuntime()) return; + await connectionStore.initFromDisk(); try { const name = path.split("/").pop()?.split("\\").pop() || path; const dbType = getDbTypeFromPath(path); @@ -581,6 +551,7 @@ async function openPendingDbFiles() { } async function openConnectionDeepLink(url: string) { + await connectionStore.initFromDisk(); try { const draft = parseConnectionDeepLink(url); if (!draft) return; @@ -815,12 +786,7 @@ function handleKeydown(e: KeyboardEvent) { void openSaveSqlDialog(); return; } - if ( - activeTab.value?.mode === "query" && - isExecuteSqlShortcut(e, shortcuts) && - e.target instanceof Element && - e.target.closest("[data-query-editor-root]") - ) { + if (activeTab.value?.mode === "query" && isExecuteSqlShortcut(e, shortcuts) && e.target instanceof Element && e.target.closest("[data-query-editor-root]")) { e.preventDefault(); e.stopPropagation(); tryExecute(); @@ -868,9 +834,10 @@ function onLoginSuccess() { function initApp() { const t0 = performance.now(); console.log("[STARTUP] initApp begin"); - settingsStore.initDesktopSettings().catch(() => {}); - savedSqlStore - .initFromStorage() + settingsStore + .initDesktopSettings() + .catch(() => {}) + .then(() => savedSqlStore.initFromStorage()) .then(() => { console.log(`[STARTUP] savedSqlStore.initFromStorage: ${(performance.now() - t0).toFixed(0)}ms`); return connectionStore.initFromDisk(); @@ -995,21 +962,16 @@ onUnmounted(() => {