fix(grid): 复制页签时保留独立筛选与排序规则 (#11628)
This commit is contained in:
9 files changed
+150
-11
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { nextTick, ref } from "vue";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { buildDataGridStructuredOrderBy, clearDataGridSortBuilderMemoryCache, combineDataGridOrderByInputs, moveDataGridStructuredSortRule, useDataGridSortBuilder, type DataGridStructuredSortRule } from "@/composables/useDataGridSortBuilder";
|
||||
import { buildDataGridStructuredOrderBy, clearDataGridSortBuilderMemoryCache, combineDataGridOrderByInputs, copyDataGridSortBuilderStatesForTab, moveDataGridStructuredSortRule, useDataGridSortBuilder, type DataGridStructuredSortRule } from "@/composables/useDataGridSortBuilder";
|
||||
|
||||
function idFactory() {
|
||||
let index = 0;
|
||||
@@ -60,6 +60,32 @@ describe("structured data-grid ORDER BY", () => {
|
||||
});
|
||||
|
||||
describe("useDataGridSortBuilder", () => {
|
||||
it("copies each scope owned by a tab without copying another tab's rules", async () => {
|
||||
const scopeKey = ref("scope-1");
|
||||
const source = useDataGridSortBuilder({ columns: ["id", "name"], cacheKey: "tab-1", scopeKey, createId: idFactory() });
|
||||
source.setOpen(true);
|
||||
source.updateRule(source.rules.value[0].id, { columnName: "id", direction: "desc" });
|
||||
source.markApplied('"id" DESC');
|
||||
await nextTick();
|
||||
scopeKey.value = "scope-2";
|
||||
await nextTick();
|
||||
source.updateRule(source.rules.value[0].id, { columnName: "name" });
|
||||
source.markApplied('"name" ASC');
|
||||
const other = useDataGridSortBuilder({ columns: ["id"], cacheKey: "tab-10", scopeKey: "scope-1" });
|
||||
other.setOpen(true);
|
||||
other.updateRule(other.rules.value[0].id, { columnName: "id" });
|
||||
other.markApplied('"id" ASC');
|
||||
await nextTick();
|
||||
|
||||
copyDataGridSortBuilderStatesForTab("tab-1", "copy");
|
||||
|
||||
const copy = useDataGridSortBuilder({ columns: ["id", "name"], cacheKey: "copy", scopeKey: "scope-1" });
|
||||
const copyOtherScope = useDataGridSortBuilder({ columns: ["id", "name"], cacheKey: "copy", scopeKey: "scope-2" });
|
||||
expect(copy.appliedOrderByInput.value).toBe('"id" DESC');
|
||||
expect(copyOtherScope.appliedOrderByInput.value).toBe('"name" ASC');
|
||||
expect(useDataGridSortBuilder({ columns: ["id"], cacheKey: "copy0", scopeKey: "scope-1" }).rules.value).toEqual([]);
|
||||
});
|
||||
|
||||
it("keeps disabled draft rules when the same tab is remounted", async () => {
|
||||
const columns = ref(["id", "name"]);
|
||||
const first = useDataGridSortBuilder({ columns, cacheKey: "tab-1", scopeKey: "scope-1", createId: idFactory() });
|
||||
|
||||
@@ -24,6 +24,26 @@ type UseDataGridSortBuilderOptions = {
|
||||
const SORT_BUILDER_CACHE_MAX_ENTRIES = 128;
|
||||
const sortBuilderCache = new Map<string, DataGridSortBuilderCacheEntry>();
|
||||
|
||||
function saveCacheEntry(key: string, entry: DataGridSortBuilderCacheEntry) {
|
||||
sortBuilderCache.delete(key);
|
||||
sortBuilderCache.set(key, { appliedOrderBy: entry.appliedOrderBy, rules: cloneRules(entry.rules) });
|
||||
while (sortBuilderCache.size > SORT_BUILDER_CACHE_MAX_ENTRIES) {
|
||||
const oldest = sortBuilderCache.keys().next().value;
|
||||
if (oldest === undefined) break;
|
||||
sortBuilderCache.delete(oldest);
|
||||
}
|
||||
}
|
||||
|
||||
export function copyDataGridSortBuilderStatesForTab(sourceTabId: string, targetTabId: string) {
|
||||
if (sourceTabId === targetTabId) return;
|
||||
for (const [key, entry] of [...sortBuilderCache.entries()]) {
|
||||
const { cacheKey, scopeKey } = JSON.parse(key) as { cacheKey: string; scopeKey: string };
|
||||
if (cacheKey === sourceTabId || cacheKey.startsWith(`${sourceTabId}-`)) {
|
||||
saveCacheEntry(JSON.stringify({ cacheKey: `${targetTabId}${cacheKey.slice(sourceTabId.length)}`, scopeKey }), entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cloneRules(rules: readonly DataGridStructuredSortRule[]): DataGridStructuredSortRule[] {
|
||||
return rules.map((rule) => ({ ...rule }));
|
||||
}
|
||||
@@ -72,13 +92,7 @@ export function useDataGridSortBuilder(options: UseDataGridSortBuilderOptions) {
|
||||
function persist() {
|
||||
const key = entryKey();
|
||||
if (!key) return;
|
||||
sortBuilderCache.delete(key);
|
||||
sortBuilderCache.set(key, { appliedOrderBy: appliedOrderByInput.value, rules: cloneRules(rules.value) });
|
||||
while (sortBuilderCache.size > SORT_BUILDER_CACHE_MAX_ENTRIES) {
|
||||
const oldest = sortBuilderCache.keys().next().value;
|
||||
if (oldest === undefined) break;
|
||||
sortBuilderCache.delete(oldest);
|
||||
}
|
||||
saveCacheEntry(key, { appliedOrderBy: appliedOrderByInput.value, rules: rules.value });
|
||||
}
|
||||
|
||||
function restore() {
|
||||
|
||||
+20
-1
@@ -1,7 +1,7 @@
|
||||
/** @vitest-environment happy-dom */
|
||||
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { clearDataGridStructuredFilterStates, clearDataGridStructuredFilterStatesForTab, dropDataGridStructuredFilterMemoryCache, loadDataGridStructuredFilterState, saveDataGridStructuredFilterState } from "@/lib/dataGrid/dataGridFilterBuilderPersistence";
|
||||
import { clearDataGridStructuredFilterStates, clearDataGridStructuredFilterStatesForTab, copyDataGridStructuredFilterStatesForTab, dropDataGridStructuredFilterMemoryCache, loadDataGridStructuredFilterState, saveDataGridStructuredFilterState } from "@/lib/dataGrid/dataGridFilterBuilderPersistence";
|
||||
|
||||
describe("data grid structured filter persistence", () => {
|
||||
const cacheKey = "issue-436-filter-view";
|
||||
@@ -25,6 +25,25 @@ describe("data grid structured filter persistence", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("copies only the source tab's grids and persists separate manual and quick conditions across a restart", () => {
|
||||
const state = loadDataGridStructuredFilterState(cacheKey, scopeKey)!;
|
||||
saveDataGridStructuredFilterState("tab-1", state);
|
||||
saveDataGridStructuredFilterState("tab-1-run-1-0", { ...state, manualWhereInput: "tenant_id = 8" });
|
||||
saveDataGridStructuredFilterState("tab-10", { ...state, manualWhereInput: "tenant_id = 9" });
|
||||
dropDataGridStructuredFilterMemoryCache();
|
||||
|
||||
copyDataGridStructuredFilterStatesForTab("tab-1", "copy");
|
||||
dropDataGridStructuredFilterMemoryCache();
|
||||
|
||||
expect(loadDataGridStructuredFilterState("copy", scopeKey)).toEqual(state);
|
||||
expect(loadDataGridStructuredFilterState("copy-run-1-0", scopeKey)?.manualWhereInput).toBe("tenant_id = 8");
|
||||
expect(loadDataGridStructuredFilterState("copy0", scopeKey)).toBeUndefined();
|
||||
expect(loadDataGridStructuredFilterState("copy", "other-scope")).toBeUndefined();
|
||||
clearDataGridStructuredFilterStatesForTab("copy");
|
||||
expect(loadDataGridStructuredFilterState("tab-1", scopeKey)).toEqual(state);
|
||||
expect(loadDataGridStructuredFilterState("tab-10", scopeKey)?.manualWhereInput).toBe("tenant_id = 9");
|
||||
});
|
||||
|
||||
it("does not leak filters into another table scope", () => {
|
||||
expect(loadDataGridStructuredFilterState(cacheKey, `${scopeKey}\0archive`)).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -171,6 +171,15 @@ export function saveDataGridStructuredFilterState(cacheKey: string, state: DataG
|
||||
persistToStorage();
|
||||
}
|
||||
|
||||
export function copyDataGridStructuredFilterStatesForTab(sourceTabId: string, targetTabId: string) {
|
||||
if (sourceTabId === targetTabId) return;
|
||||
ensureHydrated();
|
||||
const entries = [...structuredFilterStateCache.entries()].filter(([key]) => key === sourceTabId || key.startsWith(`${sourceTabId}-`));
|
||||
for (const [key, state] of entries) {
|
||||
saveDataGridStructuredFilterState(`${targetTabId}${key.slice(sourceTabId.length)}`, state);
|
||||
}
|
||||
}
|
||||
|
||||
export function clearDataGridStructuredFilterStatesForTab(tabId: string) {
|
||||
ensureHydrated();
|
||||
structuredFilterStateCache.delete(tabId);
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
import { effectScope, nextTick } from "vue";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { clearDataGridStructuredFilterStates, loadDataGridStructuredFilterState, saveDataGridStructuredFilterState } from "@/lib/dataGrid/dataGridFilterBuilderPersistence";
|
||||
import { clearDataGridSortBuilderMemoryCache, useDataGridSortBuilder } from "@/composables/useDataGridSortBuilder";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
buildTableSelectSql: vi.fn(),
|
||||
@@ -57,6 +60,8 @@ describe("queryStore table data refresh", () => {
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
installLocalStorage();
|
||||
clearDataGridStructuredFilterStates();
|
||||
clearDataGridSortBuilderMemoryCache();
|
||||
setActivePinia(createPinia());
|
||||
mocks.metadataGeneration = 0;
|
||||
mocks.getConnectionConfig.mockReturnValue({
|
||||
@@ -124,6 +129,66 @@ describe("queryStore table data refresh", () => {
|
||||
expect(copy.result).not.toBe(original.result);
|
||||
});
|
||||
|
||||
it.each(["filter", "sort"] as const)("duplicates the %s builder state separately from manual SQL inputs (#11609)", async (kind) => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
const id = store.createTab("pg-1", "app", "users", "data", "public");
|
||||
store.setTableMeta(id, {
|
||||
database: "app",
|
||||
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: "status", data_type: "text", is_nullable: true, column_default: null, is_primary_key: false, extra: null },
|
||||
],
|
||||
primaryKeys: ["id"],
|
||||
});
|
||||
const original = store.tabs.find((tab) => tab.id === id)!;
|
||||
original.whereInput = "(status = 'ACTIVE') AND (id > 10) AND (status <> 'DELETED')";
|
||||
original.orderByInput = "status ASC";
|
||||
original.structuredOrderByInput = "id DESC";
|
||||
const filterState = {
|
||||
scopeKey: "filter-scope",
|
||||
manualWhereInput: "status = 'ACTIVE'",
|
||||
appliedWhereInput: "id > 10",
|
||||
rules: [{ id: "filter-1", columnName: "id", mode: "greater-than" as const, rawValue: "10", rawEndValue: "", conjunction: "AND" as const }],
|
||||
serverColumnFilters: { 1: { condition: "status <> 'DELETED'", keys: ["active"], labels: ["ACTIVE"] } },
|
||||
};
|
||||
saveDataGridStructuredFilterState(id, filterState);
|
||||
const scope = effectScope();
|
||||
try {
|
||||
const source = scope.run(() => useDataGridSortBuilder({ columns: ["id", "status"], cacheKey: id, scopeKey: "sort-scope" }))!;
|
||||
source.setOpen(true);
|
||||
source.updateRule(source.rules.value[0].id, { columnName: "id", direction: "desc" });
|
||||
source.addRule();
|
||||
source.updateRule(source.rules.value[1].id, { columnName: "status", direction: "asc", disabled: true });
|
||||
source.markApplied("id DESC");
|
||||
await nextTick();
|
||||
store.duplicateTab(id);
|
||||
const copy = store.tabs[1];
|
||||
await vi.waitFor(() => expect(copy.result).toBeDefined());
|
||||
expect(mocks.buildTableSelectSql).toHaveBeenCalledWith(expect.objectContaining({ whereInput: original.whereInput, orderBy: "status ASC, id DESC" }));
|
||||
if (kind === "filter") {
|
||||
const copied = loadDataGridStructuredFilterState(copy.id, "filter-scope");
|
||||
expect(copied).toEqual(filterState);
|
||||
copied!.rules[0].rawValue = "100";
|
||||
copied!.serverColumnFilters[1].labels.push("CHANGED");
|
||||
saveDataGridStructuredFilterState(copy.id, copied!);
|
||||
expect(loadDataGridStructuredFilterState(id, "filter-scope")).toEqual(filterState);
|
||||
} else {
|
||||
const target = scope.run(() => useDataGridSortBuilder({ columns: ["id", "status"], cacheKey: copy.id, scopeKey: "sort-scope" }))!;
|
||||
expect(target.rules.value).toEqual(source.rules.value);
|
||||
expect(target.appliedOrderByInput.value).toBe("id DESC");
|
||||
target.updateRule(target.rules.value[0].id, { direction: "asc" });
|
||||
await nextTick();
|
||||
expect(source.rules.value[0].direction).toBe("desc");
|
||||
}
|
||||
} finally {
|
||||
scope.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it("refreshes only matching data tabs after a table mutation", async () => {
|
||||
const { useQueryStore } = await import("@/stores/queryStore");
|
||||
const store = useQueryStore();
|
||||
|
||||
@@ -108,10 +108,10 @@ import { replaceSqlServerLeadingUseQuery, sqlServerLeadingUseScript, switchesDat
|
||||
import { classifySqlRisk } from "@/lib/sql/sqlRisk";
|
||||
import { externalSqlFileDisplayTitles, normalizeExternalSqlPath } from "@/lib/sql/sqlFileOpen";
|
||||
import { clearDataGridPendingSnapshot, clearDataGridPendingSnapshotsForTab } from "@/composables/useDataGridEditor";
|
||||
import { combineDataGridOrderByInputs } from "@/composables/useDataGridSortBuilder";
|
||||
import { combineDataGridOrderByInputs, copyDataGridSortBuilderStatesForTab } from "@/composables/useDataGridSortBuilder";
|
||||
import { beginClosingDataGridViewSnapshotsForTab, clearDataGridViewSnapshot, clearDataGridViewSnapshotsForTab } from "@/lib/dataGrid/dataGridViewStateCache";
|
||||
import { beginClosingBrowserState } from "@/lib/tabs/documentBrowserStateCache";
|
||||
import { clearDataGridStructuredFilterStatesForTab } from "@/lib/dataGrid/dataGridFilterBuilderPersistence";
|
||||
import { clearDataGridStructuredFilterStatesForTab, copyDataGridStructuredFilterStatesForTab } from "@/lib/dataGrid/dataGridFilterBuilderPersistence";
|
||||
import { clearDataGridSearchStatesForTab } from "@/lib/dataGrid/dataGridSearchStatePersistence";
|
||||
import { buildTabResultSnapshot, deleteTabResultSnapshot, pruneTabResultSnapshots, readTabResultSnapshot, tabResultCacheKey, writeTabResultSnapshot } from "@/lib/tabs/tabResultCache";
|
||||
import { estimateQueryResultsBytes, selectInactiveResultEvictions } from "@/lib/tabs/queryResultSize";
|
||||
@@ -4824,6 +4824,12 @@ export const useQueryStore = defineStore("query", () => {
|
||||
previewSql: original.previewSql,
|
||||
};
|
||||
initializeResultAutoSave(newTab);
|
||||
if (newTab.mode === "data") {
|
||||
// Seed builder state before the new grid mounts; the combined WHERE alone
|
||||
// cannot recover which conditions belong to the manual input or quick UI.
|
||||
copyDataGridStructuredFilterStatesForTab(id, newId);
|
||||
copyDataGridSortBuilderStatesForTab(id, newId);
|
||||
}
|
||||
tabs.value.splice(idx + 1, 0, newTab);
|
||||
|
||||
const owner = groupForTab(id);
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
Reference in new issue
Block a user