From 760d2c6409a8df790be76aaabd4ceaf0ed8cf382 Mon Sep 17 00:00:00 2001 From: Abeautifulsnow Date: Mon, 14 Sep 2026 19:59:21 +0800 Subject: [PATCH] fix(source): open pending tab while loading object source --- apps/desktop/src/App.vue | 48 +--- .../src/components/layout/ContentArea.vue | 48 +++- .../components/layout/TabExecutionStatus.vue | 22 +- .../__tests__/TabExecutionStatus.spec.ts | 29 +- .../components/objects/ObjectSourceDialog.vue | 4 + .../src/components/sidebar/ConnectionTree.vue | 17 +- .../sidebar/SidebarTreeRuntimeHost.vue | 81 ++---- .../__tests__/app/openTabsPersistence.spec.ts | 14 + .../src/lib/app/openTabsPersistence.ts | 12 +- .../queryStore.objectSourcePending.spec.ts | 272 ++++++++++++++++++ apps/desktop/src/stores/queryStore.ts | 254 +++++++++++++++- apps/desktop/src/types/database.ts | 23 ++ .../objectSourceOpenFeedback.test.ts | 89 ++++++ .../app-tests/sidebarContextMenuHost.test.ts | 60 +++- 14 files changed, 824 insertions(+), 149 deletions(-) create mode 100644 apps/desktop/src/stores/__tests__/queryStore.objectSourcePending.spec.ts create mode 100644 packages/app-tests/objectSourceOpenFeedback.test.ts diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 3ab5aab13a..35910ed76d 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -2618,7 +2618,7 @@ function onEditTableStructure(table: SqlObjectNavigationTarget) { queryStore.openTableStructure(target.connectionId, target.database, target.schema, target.tableName, undefined, undefined, target.catalog); } -async function onOpenObjectSource(table: SqlObjectNavigationTarget, initialEditing: boolean) { +function onOpenObjectSource(table: SqlObjectNavigationTarget, initialEditing: boolean) { const provisionalTarget = tableTargetFromActiveTab(table); if (!provisionalTarget) return; const databaseType = effectiveDatabaseTypeForConnection(connectionStore.getConfig(provisionalTarget.connectionId)); @@ -2630,42 +2630,22 @@ async function onOpenObjectSource(table: SqlObjectNavigationTarget, initialEditi const sourceName = sqlObjectNavigationSourceName(navigation); const sourceSchema = sqlObjectNavigationSourceSchema(navigation, target.schema || target.database); try { - await connectionStore.ensureConnected(target.connectionId); - connectionStore.activeConnectionId = target.connectionId; - // Align Ctrl/Cmd+click with sidebar "view source" (dialog vs data tab). + // Keep the editor navigation path aligned with the sidebar: create a visible + // pending tab first, then let the store own connection/source loading. if (settingsStore.editorSettings.routineSourceOpenMode === "query-tab") { - try { - const resolvedDatabaseType = effectiveDatabaseTypeForConnection(connectionStore.getConfig(target.connectionId)); - if (!resolvedDatabaseType) throw new Error("Connection type is unavailable."); - // Wrap Oracle bare ALL_SOURCE (`procedure name is ...`) as CREATE OR REPLACE for the editor. - const { - raw, - editableSource, - objectType: resolvedType, - } = await loadEditableObjectSourceForEditor(api.getObjectSource, buildEditableObjectSource, { - connectionId: target.connectionId, - database: target.database, - schema: sourceSchema || target.database, - name: sourceName, - objectType, - databaseType: resolvedDatabaseType, - signature: navigation.signature, - }); - const tabId = queryStore.createTab(target.connectionId, target.database, `Source - ${sourceName}`, "query", sourceSchema, editableSource, target.catalog, { forceNew: true, sourceView: true }); - const sourceIsEditable = raw.editable !== false && !["SEQUENCE", "TRIGGER", "TYPE", "TYPE_BODY"].includes(resolvedType); - if (sourceIsEditable) { - queryStore.setObjectSource(tabId, { - schema: sourceSchema || target.database, - name: sourceName, - objectType: resolvedType, - signature: navigation.signature, - }); - } - } catch (e: any) { - toast(e?.message || String(e), 5000); - } + queryStore.openObjectSourceTabPending({ + connectionId: target.connectionId, + database: target.database, + title: `Source - ${sourceName}`, + schema: sourceSchema || target.database, + catalog: target.catalog, + request: { name: sourceName, objectType, signature: navigation.signature }, + }); return; } + + // The dialog owns ensureConnected so its existing loading/error/retry UI is + // mounted before a slow connection health check or Oracle metadata query. queryEditorObjectSourceTarget.value = { connectionId: target.connectionId, database: target.database, diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 58332f78cd..5dbef2f9b9 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -672,6 +672,7 @@ const resultsPaneSize = ref(Number(safeLocalStorageGet("dbx-results-pane-size")) // re-normalize a single pane, and shrinking it would leave a blank dead zone. const editorPaneSize = computed(() => (props.editorOnly || !resultsPaneOpen.value ? 100 : 100 - resultsPaneSize.value)); const queryRunningElapsed = ref(0); +const sourceLoadElapsed = ref(0); function toggleResultsPane(): boolean { if (props.activeTab.mode !== "query" || !hasQueryOutput.value) return false; @@ -703,25 +704,36 @@ function onResultsResized(payload: { panes: { size: number }[] }) { } let queryRunningElapsedFrame: number | undefined; -function stopQueryRunningElapsedTimer() { +function stopRunningElapsedTimer() { if (queryRunningElapsedFrame !== undefined) { window.cancelAnimationFrame(queryRunningElapsedFrame); queryRunningElapsedFrame = undefined; } } -function updateQueryRunningElapsed() { - const startedAt = props.activeTab.queryExecutionStartedAt; - queryRunningElapsed.value = props.activeTab.isExecuting && startedAt ? Math.max(0, Date.now() - startedAt) : 0; +/** 源码仍在加载(未失败)时的开始时间;用于耗时显示与 rAF 循环的续期判断。 */ +function pendingSourceLoadStartedAt(): number | undefined { + const load = props.activeTab.sourceLoad; + return load && !load.error ? load.startedAt : undefined; } -function startQueryRunningElapsedTimer() { - stopQueryRunningElapsedTimer(); - updateQueryRunningElapsed(); - if (!props.activeTab.isExecuting || !props.activeTab.queryExecutionStartedAt) return; +// 一个 rAF 循环同时驱动「查询执行中」与「对象源码加载中」两个耗时显示: +// issue #9035 的核心体感就是「不知道要等多久」,所以源码加载也要显示已耗时。 +function updateRunningElapsed() { + const startedAt = props.activeTab.queryExecutionStartedAt; + queryRunningElapsed.value = props.activeTab.isExecuting && startedAt ? Math.max(0, Date.now() - startedAt) : 0; + const sourceStartedAt = pendingSourceLoadStartedAt(); + sourceLoadElapsed.value = sourceStartedAt ? Math.max(0, Date.now() - sourceStartedAt) : 0; +} + +function startRunningElapsedTimer() { + stopRunningElapsedTimer(); + updateRunningElapsed(); + const isTicking = () => (props.activeTab.isExecuting && !!props.activeTab.queryExecutionStartedAt) || pendingSourceLoadStartedAt() !== undefined; + if (!isTicking()) return; const updateOnNextFrame = () => { - updateQueryRunningElapsed(); - if (props.activeTab.isExecuting && props.activeTab.queryExecutionStartedAt) { + updateRunningElapsed(); + if (isTicking()) { queryRunningElapsedFrame = window.requestAnimationFrame(updateOnNextFrame); } }; @@ -729,11 +741,12 @@ function startQueryRunningElapsedTimer() { } const queryRunningElapsedSeconds = computed(() => formatElapsedSeconds(queryRunningElapsed.value)); +const sourceLoadElapsedSeconds = computed(() => formatElapsedSeconds(sourceLoadElapsed.value)); -watch(() => [props.activeTab.id, props.activeTab.isExecuting, props.activeTab.queryExecutionStartedAt] as const, startQueryRunningElapsedTimer, { immediate: true }); +watch(() => [props.activeTab.id, props.activeTab.isExecuting, props.activeTab.queryExecutionStartedAt, props.activeTab.sourceLoad?.startedAt, props.activeTab.sourceLoad?.error] as const, startRunningElapsedTimer, { immediate: true }); onUnmounted(() => { - stopQueryRunningElapsedTimer(); + stopRunningElapsedTimer(); standaloneResultToolbarResizeObserver?.disconnect(); window.removeEventListener("dbx-refresh-active-kv-browser", onRefreshActiveKvBrowser); window.removeEventListener("resize", updateStandaloneResultToolbarDimensions); @@ -1320,7 +1333,18 @@ defineExpose({ + + +
+

{{ activeTab.sourceLoad.error }}

+ +
; + tab: Pick; }>(); const { t } = useI18n(); -const status = computed(() => { - if (props.tab.mode !== "query" || !props.tab.isExecuting) return undefined; +// 源码加载中(issue #9035)与查询执行中共用这一个 tab 栏转圈:两者都是 +// 「这个 tab 正在等后端」,用户需要的反馈是同一种。 +const status = computed<"running" | "cancelling" | "source-loading" | undefined>(() => { + if (props.tab.mode !== "query") return undefined; + if (props.tab.sourceLoad && !props.tab.sourceLoad.error) return "source-loading"; + if (!props.tab.isExecuting) return undefined; return props.tab.isCancelling ? "cancelling" : "running"; }); + +const labelKey = computed(() => (status.value === "source-loading" ? "common.loading" : queryExecutionLabelKey(props.tab)));