diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 10cf8643ea..301832e9a1 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -211,6 +211,7 @@ import { filterObjectBrowserTableColumns } from "@/lib/table/objectBrowserTableI import { reserveDataGridHeaderLine } from "@/lib/dataGrid/dataGridHeaderLayout"; import { supportsTableStructureEditing } from "@/lib/database/databaseCapabilities"; import { rememberDataGridConditionHistory } from "@/lib/dataGrid/dataGridConditionHistory"; +import { restoreDataGridLocalColumnFilters, serializeDataGridLocalColumnFilters } from "@/lib/dataGrid/dataGridLocalColumnFilterState"; import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect"; import { dataGridConditionColumnOptions } from "@/lib/dataGrid/dataGridConditionCompletion"; import { isMacOS } from "@/lib/backend/platform"; @@ -344,6 +345,7 @@ const emit = defineEmits<{ sort: [column: string, columnIndex: number, direction: "asc" | "desc" | null, whereInput?: string, mode?: DataGridSortMode]; "update:whereInput": [value: string]; "update:orderByInput": [value: string]; + "local-column-filters-change": [value: Record]; }>(); const autoRefresh = useDataGridAutoRefresh({ canRefresh: computed(() => !isSaving.value && !props.loading), refresh: onToolbarRefresh }); @@ -624,7 +626,7 @@ type FilterMode = DataGridContextFilterMode; type StructuredFilterRule = DataGridStructuredFilterRule; -const localColumnFilters = ref>>({}); +const localColumnFilters = ref>>(restoreDataGridLocalColumnFilters(props.result.local_column_filters, props.result.columns.length)); const localFilterOpenColumn = ref(null); const headerActionMenuOpenColumn = ref(null); const headerSortMenuOpenColumn = ref(null); @@ -641,6 +643,8 @@ const serverFilterValueByKey = ref>(new Map()); const serverColumnFilters = ref>({}); let serverFilterRequestId = 0; let serverFilterSearchTimer: ReturnType | undefined; + +watch(localColumnFilters, (filters) => emit("local-column-filters-change", serializeDataGridLocalColumnFilters(filters)), { deep: true }); const allFilterModeOptions: Array<{ value: FilterMode; labelKey: string }> = [ { value: "equals", labelKey: "grid.filterBuilderEquals" }, { value: "not-equals", labelKey: "grid.filterBuilderNotEquals" }, diff --git a/apps/desktop/src/components/layout/ContentArea.vue b/apps/desktop/src/components/layout/ContentArea.vue index cb205a50bc..ae2349e3c2 100644 --- a/apps/desktop/src/components/layout/ContentArea.vue +++ b/apps/desktop/src/components/layout/ContentArea.vue @@ -1198,6 +1198,7 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand :all-export-results="allResultExportSheets" :export-file-base-name="activeTab.title" @update:order-by-input="(v: string) => (activeTab.orderByInput = v)" + @local-column-filters-change="(filters: Record) => queryStore.updateDataGridLocalColumnFilters(activeTab.id, filters)" @reload="(sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number, intent?: DataGridReloadIntent) => emit('reload', sql, searchText, whereInput, orderBy, limit, offset, intent)" @paginate="(offset: number, limit: number, whereInput?: string, orderBy?: string) => emit('paginate', offset, limit, whereInput, orderBy)" @sort="(column: string, columnIndex: number, direction: 'asc' | 'desc' | null, whereInput?: string, mode?: DataGridSortMode) => emit('sort', column, columnIndex, direction, whereInput, mode)" @@ -1525,6 +1526,7 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand :export-file-base-name="activeTab.title" @update:where-input="(v: string) => (activeTab.whereInput = v)" @update:order-by-input="(v: string) => (activeTab.orderByInput = v)" + @local-column-filters-change="(filters: Record) => queryStore.updateDataGridLocalColumnFilters(activeTab.id, filters)" @reload="(sql?: string, searchText?: string, whereInput?: string, orderBy?: string, limit?: number, offset?: number, intent?: DataGridReloadIntent) => emit('reload', sql, searchText, whereInput, orderBy, limit, offset, intent)" @paginate="(offset: number, limit: number, whereInput?: string, orderBy?: string) => emit('paginate', offset, limit, whereInput, orderBy)" @sort="(column: string, columnIndex: number, direction: 'asc' | 'desc' | null, whereInput?: string, mode?: DataGridSortMode) => emit('sort', column, columnIndex, direction, whereInput, mode)" diff --git a/apps/desktop/src/lib/__tests__/dataGrid/dataGridLocalColumnFilterState.spec.ts b/apps/desktop/src/lib/__tests__/dataGrid/dataGridLocalColumnFilterState.spec.ts new file mode 100644 index 0000000000..0af87929d1 --- /dev/null +++ b/apps/desktop/src/lib/__tests__/dataGrid/dataGridLocalColumnFilterState.spec.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { restoreDataGridLocalColumnFilters, serializeDataGridLocalColumnFilters } from "@/lib/dataGrid/dataGridLocalColumnFilterState"; + +describe("data grid local column filter state", () => { + it("round-trips selected values so a remounted grid restores its filters", () => { + const serialized = serializeDataGridLocalColumnFilters({ + 0: new Set(["str:active", "__dbx_null__"]), + 2: new Set(["num:42"]), + }); + + const restored = restoreDataGridLocalColumnFilters(serialized, 3); + + expect(restored[0]).toEqual(new Set(["str:active", "__dbx_null__"])); + expect(restored[2]).toEqual(new Set(["num:42"])); + }); + + it("drops invalid columns and empty filters before restoring state", () => { + const restored = restoreDataGridLocalColumnFilters( + { + "-1": ["str:invalid"], + "1": [], + "4": ["str:out-of-range"], + nope: ["str:invalid"], + }, + 3, + ); + + expect(restored).toEqual({}); + }); +}); diff --git a/apps/desktop/src/lib/dataGrid/dataGridLocalColumnFilterState.ts b/apps/desktop/src/lib/dataGrid/dataGridLocalColumnFilterState.ts new file mode 100644 index 0000000000..50f077f67d --- /dev/null +++ b/apps/desktop/src/lib/dataGrid/dataGridLocalColumnFilterState.ts @@ -0,0 +1,18 @@ +export type SerializedDataGridLocalColumnFilters = Record; + +export function restoreDataGridLocalColumnFilters(serialized: SerializedDataGridLocalColumnFilters | undefined, columnCount: number): Record> { + if (!serialized || typeof serialized !== "object") return {}; + + const restored: Record> = {}; + for (const [columnIndexText, values] of Object.entries(serialized)) { + const columnIndex = Number(columnIndexText); + if (!Number.isInteger(columnIndex) || columnIndex < 0 || columnIndex >= columnCount || !Array.isArray(values)) continue; + const filteredValues = values.filter((value): value is string => typeof value === "string"); + if (filteredValues.length > 0) restored[columnIndex] = new Set(filteredValues); + } + return restored; +} + +export function serializeDataGridLocalColumnFilters(filters: Record>): SerializedDataGridLocalColumnFilters { + return Object.fromEntries(Object.entries(filters).flatMap(([columnIndex, values]) => (values.size > 0 ? [[columnIndex, [...values]]] : []))); +} diff --git a/apps/desktop/src/stores/__tests__/queryStore.switchTab.spec.ts b/apps/desktop/src/stores/__tests__/queryStore.switchTab.spec.ts index 7728b89b87..e2c1fd1fbd 100644 --- a/apps/desktop/src/stores/__tests__/queryStore.switchTab.spec.ts +++ b/apps/desktop/src/stores/__tests__/queryStore.switchTab.spec.ts @@ -68,4 +68,23 @@ describe("queryStore switchTab", () => { expect(queryStore.activeTabId).toBe(tabId); expect(settingsStore.settingsPageActive).toBe(false); }); + + it("stores local data-grid column filters on the tab result", async () => { + const { useQueryStore } = await import("@/stores/queryStore"); + const queryStore = useQueryStore(); + const tabId = queryStore.createTab("pg-1", "app", "users", "data", "public"); + const tab = queryStore.tabs.find((item) => item.id === tabId)!; + tab.result = { + columns: ["id", "status"], + rows: [[1, "active"]], + affected_rows: 0, + execution_time_ms: 1, + }; + + queryStore.updateDataGridLocalColumnFilters(tabId, { "1": ["str:active"] }); + expect(tab.result.local_column_filters).toEqual({ "1": ["str:active"] }); + + queryStore.updateDataGridLocalColumnFilters(tabId, {}); + expect(tab.result.local_column_filters).toBeUndefined(); + }); }); diff --git a/apps/desktop/src/stores/queryStore.ts b/apps/desktop/src/stores/queryStore.ts index 6f050f7322..7d104ff2f7 100644 --- a/apps/desktop/src/stores/queryStore.ts +++ b/apps/desktop/src/stores/queryStore.ts @@ -2089,6 +2089,16 @@ export const useQueryStore = defineStore("query", () => { } } + function updateDataGridLocalColumnFilters(id: string, filters: Record) { + const tab = tabs.value.find((item) => item.id === id); + if (!tab?.result) return; + if (Object.keys(filters).length === 0) { + delete tab.result.local_column_filters; + } else { + tab.result.local_column_filters = Object.fromEntries(Object.entries(filters).map(([columnIndex, values]) => [columnIndex, [...values]])); + } + } + function setAutoCommit(id: string, autoCommit: boolean) { const tab = tabs.value.find((t) => t.id === id); if (tab) { @@ -4584,6 +4594,7 @@ export const useQueryStore = defineStore("query", () => { rollbackConnectionTransactions, rollbackDatabaseTransactions, updateSql, + updateDataGridLocalColumnFilters, updateEditorViewport, updateEditorSelection, updateObjectBrowserViewport, diff --git a/apps/desktop/src/types/database.ts b/apps/desktop/src/types/database.ts index 5da6f2c220..c3f1b3ec88 100644 --- a/apps/desktop/src/types/database.ts +++ b/apps/desktop/src/types/database.ts @@ -552,6 +552,8 @@ export interface QueryResult { statement_index?: number; /** Internal row identifiers appended to editable query results. */ hidden_column_indexes?: number[]; + /** Local value filters survive DataGrid component eviction when switching tabs. */ + local_column_filters?: Record; /** * Database type name for each column, parallel to `columns`. Optional and may * be shorter/empty when a driver cannot supply types (schemaless stores,