diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index 01b88b4b11..f4ef53520b 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -2,7 +2,7 @@ import { computed, ref, defineAsyncComponent, watch, nextTick, onMounted, onUnmounted } from "vue"; import { safeLocalStorageGet, safeLocalStorageSet } from "@/lib/backend/safeStorage"; import { appendDebugLog, isDebugLoggingEnabled } from "@/lib/backend/debugLog"; -import { canReloadUnavailableDataTab } from "@/lib/table/tableDataRefresh"; +import { canReloadUnavailableDataTab, restoredDataTabReloadFilters } from "@/lib/table/tableDataRefresh"; import { defaultViewForResult } from "@/lib/query/queryResultDefaultView"; import { isQueryExecutionErrorResult } from "@/lib/query/queryResultError"; import { batchSqlRecoveryState, type BatchSqlRecoveryAction } from "@/lib/query/batchSqlRecovery"; @@ -916,6 +916,11 @@ function refreshQueryEditorCompletionCache(): boolean { return true; } +function reloadUnavailableDataTab() { + const { whereInput, orderBy } = restoredDataTabReloadFilters(props.activeTab); + emit("reload", undefined, undefined, whereInput, orderBy); +} + function refreshData(): boolean { // Reuse ObjectBrowser's reload path so schema reloads and stale object-response guards stay intact. if (props.activeTab.mode === "objects") return objectBrowserRef.value?.refresh?.() ?? false; @@ -927,7 +932,7 @@ function refreshData(): boolean { if (props.activeTab.mode === "databases") return databaseBrowserRef.value?.refresh?.() ?? false; // Restored data tabs intentionally omit row data, so refresh must work before DataGrid mounts. if (canReloadUnavailableDataTab(props.activeTab)) { - emit("reload"); + reloadUnavailableDataTab(); return true; } if (activeElasticsearchJsonResponse.value) { @@ -2220,7 +2225,7 @@ defineExpose({ {{ key }} {{ t("grid.dataUnavailableHintSuffix") }} - diff --git a/apps/desktop/src/components/layout/__tests__/ContentArea.unavailableDataReload.spec.ts b/apps/desktop/src/components/layout/__tests__/ContentArea.unavailableDataReload.spec.ts new file mode 100644 index 0000000000..35a8317c02 --- /dev/null +++ b/apps/desktop/src/components/layout/__tests__/ContentArea.unavailableDataReload.spec.ts @@ -0,0 +1,20 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const contentAreaSource = readFileSync(new URL("../ContentArea.vue", import.meta.url), "utf8"); + +describe("ContentArea restored data-tab refresh", () => { + it("routes both no-data refresh entries through the filter-preserving helper (#7963)", () => { + // 裸 emit("reload") 会让 onReloadData 把缺省的 whereInput/orderBy 当成 + // “用户已清空”,刷新后退回整张表的数据。 + expect(contentAreaSource).not.toContain(`@click="emit('reload')"`); + expect(contentAreaSource).not.toMatch(/\bemit\("reload"\)\s*;/); + expect(contentAreaSource).toContain(`@click="reloadUnavailableDataTab()"`); + expect(contentAreaSource).toContain("reloadUnavailableDataTab();"); + }); + + it("passes the restored tab's own WHERE/ORDER BY into the reload event", () => { + expect(contentAreaSource).toContain("const { whereInput, orderBy } = restoredDataTabReloadFilters(props.activeTab);"); + expect(contentAreaSource).toContain(`emit("reload", undefined, undefined, whereInput, orderBy);`); + }); +}); diff --git a/apps/desktop/src/composables/__tests__/useDataGridActions.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridActions.spec.ts index 076334f96b..e3a9f79d1b 100644 --- a/apps/desktop/src/composables/__tests__/useDataGridActions.spec.ts +++ b/apps/desktop/src/composables/__tests__/useDataGridActions.spec.ts @@ -2,6 +2,7 @@ import { computed, reactive } from "vue"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { useDataGridActions } from "@/composables/useDataGridActions"; import { clearTableMetadataCache } from "@/lib/metadata/tableMetadataCache"; +import { restoredDataTabReloadFilters } from "@/lib/table/tableDataRefresh"; import type { QueryTab } from "@/types/database"; const mocks = vi.hoisted(() => ({ @@ -166,6 +167,50 @@ describe("useDataGridActions", () => { expect(mocks.executeTabSql.mock.calls[0]?.[2]).not.toHaveProperty("preserveTotalRowCountDuringExecution"); }); + it("keeps the restored filter and sort when the no-data placeholder refresh runs (#7963)", async () => { + // 重启后恢复的数据标签页没有 result,刷新走的是 ContentArea 空态占位符那条 + // 不带参数的入口;它必须回带标签页自身的 whereInput/orderByInput, + // 否则刷新会重新加载整张表。 + const tab = tableDataTab({ + result: undefined, + whereInput: "status = 'active'", + orderByInput: '"name" DESC', + tableMeta: { + schema: "public", + tableName: "users", + tableType: "TABLE", + columns: [ + { name: "id", data_type: "integer", is_nullable: false, column_default: null, is_primary_key: true, extra: null }, + { name: "name", data_type: "text", is_nullable: true, column_default: null, is_primary_key: false, extra: null }, + { name: "status", data_type: "text", is_nullable: true, column_default: null, is_primary_key: false, extra: null }, + ], + primaryKeys: ["id"], + }, + }); + mocks.tabs.push(tab); + const actions = useDataGridActions(computed(() => tab)); + + const { whereInput, orderBy } = restoredDataTabReloadFilters(tab); + await actions.onReloadData(undefined, undefined, whereInput, orderBy); + + expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(expect.objectContaining({ whereInput: "status = 'active'", orderBy: '"name" DESC' })); + expect(tab.whereInput).toBe("status = 'active'"); + expect(tab.orderByInput).toBe('"name" DESC'); + }); + + it("still clears the stored filter when a mounted grid refreshes with an emptied WHERE input", async () => { + // 对照:DataGrid 的 currentWhereInput() 在用户清空筛选时返回 undefined, + // 所以 onReloadData 里不能无条件回退到 tab.whereInput。 + const tab = tableDataTab({ whereInput: "status = 'active'" }); + mocks.tabs.push(tab); + const actions = useDataGridActions(computed(() => tab)); + + await actions.onReloadData(tab.sql, "", undefined, undefined, undefined, undefined, "refresh"); + + expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(expect.objectContaining({ whereInput: undefined, orderBy: undefined })); + expect(tab.whereInput).toBe(""); + }); + it("preserves the toolbar page segment and offset for table-data refresh", async () => { const tab = tableDataTab({ resultPageLimit: 25, diff --git a/apps/desktop/src/lib/table/__tests__/tableDataRefresh.spec.ts b/apps/desktop/src/lib/table/__tests__/tableDataRefresh.spec.ts index 76c3c9681a..9939ad18e2 100644 --- a/apps/desktop/src/lib/table/__tests__/tableDataRefresh.spec.ts +++ b/apps/desktop/src/lib/table/__tests__/tableDataRefresh.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { canReloadUnavailableDataTab } from "@/lib/table/tableDataRefresh"; +import { canReloadUnavailableDataTab, restoredDataTabReloadFilters } from "@/lib/table/tableDataRefresh"; describe("canReloadUnavailableDataTab", () => { it("allows restored data tabs to reload before the grid mounts", () => { @@ -21,3 +21,17 @@ describe("canReloadUnavailableDataTab", () => { ).toBe(false); }); }); + +describe("restoredDataTabReloadFilters", () => { + it("carries the restored tab's own filter and sort back into the reload (#7963)", () => { + expect(restoredDataTabReloadFilters({ whereInput: "status = 'active'", orderByInput: "name DESC" })).toEqual({ + whereInput: "status = 'active'", + orderBy: "name DESC", + }); + }); + + it("reports blank filters as absent so the reload does not emit an empty WHERE/ORDER BY", () => { + expect(restoredDataTabReloadFilters({ whereInput: "", orderByInput: undefined })).toEqual({ whereInput: undefined, orderBy: undefined }); + expect(restoredDataTabReloadFilters({ whereInput: " ", orderByInput: " " })).toEqual({ whereInput: undefined, orderBy: undefined }); + }); +}); diff --git a/apps/desktop/src/lib/table/tableDataRefresh.ts b/apps/desktop/src/lib/table/tableDataRefresh.ts index 67ee7d7737..d627eb01f2 100644 --- a/apps/desktop/src/lib/table/tableDataRefresh.ts +++ b/apps/desktop/src/lib/table/tableDataRefresh.ts @@ -5,3 +5,16 @@ type RefreshableDataTab = Pick; export function canReloadUnavailableDataTab(tab: RefreshableDataTab): boolean { return tab.mode === "data" && !tab.result && !tab.isExecuting; } + +type RestoredDataTabFilters = Pick; + +// 恢复的数据标签页没有 result,刷新走的是 DataGrid 之外那条不带参数的 reload 入口。 +// onReloadData 把缺省的 whereInput/orderBy 当成“用户已清空”(DataGrid 清空筛选时 +// 正是传 undefined),所以这里必须显式回带标签页自己持久化的过滤/排序, +// 否则刷新会退回整张表的数据(#7963)。 +export function restoredDataTabReloadFilters(tab: RestoredDataTabFilters): { whereInput?: string; orderBy?: string } { + return { + whereInput: tab.whereInput?.trim() || undefined, + orderBy: tab.orderByInput?.trim() || undefined, + }; +}