fix(grid): keep filters when refreshing restored table tabs

This commit is contained in:
Melody Ruch authored and GitHub committed 2026-09-03 17:52:07 +08:00
1 parent cd8300c3f5
commit aa637b2d90
5 files changed
+101 -4

No files matched your search

@@ -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({
<kbd v-for="key in modRKeys" :key="key" class="min-w-5 rounded border border-border/60 bg-muted/50 px-1.5 py-0.5 text-center font-mono text-[12px] leading-none text-muted-foreground shadow-xs">{{ key }}</kbd>
<span>{{ t("grid.dataUnavailableHintSuffix") }}</span>
</div>
<Button variant="outline" size="sm" class="h-7 gap-1.5" @click="emit('reload')">
<Button variant="outline" size="sm" class="h-7 gap-1.5" @click="reloadUnavailableDataTab()">
<RefreshCcw class="h-3.5 w-3.5" />
{{ t("grid.refresh") }}
</Button>
@@ -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);`);
});
});
@@ -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,
@@ -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 });
});
});
@@ -5,3 +5,16 @@ type RefreshableDataTab = Pick<QueryTab, "mode" | "result" | "isExecuting">;
export function canReloadUnavailableDataTab(tab: RefreshableDataTab): boolean {
return tab.mode === "data" && !tab.result && !tab.isExecuting;
}
type RestoredDataTabFilters = Pick<QueryTab, "whereInput" | "orderByInput">;
// 恢复的数据标签页没有 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,
};
}