feat(grid): add persistent SQL filter views

This commit is contained in:
zipg authored and GitHub committed 2026-08-15 14:45:52 +08:00
1 parent 8bf4485a75
commit 9e7d80f467
27 files changed
+1428 -107

No files matched your search

@@ -1,24 +1,39 @@
import { describe, expect, it } from "vitest";
import { beforeEach, describe, expect, it } from "vitest";
import { loadDataGridStructuredFilterState, saveDataGridStructuredFilterState } from "@/lib/dataGrid/dataGridFilterBuilderPersistence";
describe("data grid filter builder persistence", () => {
it("isolates scopes and returns cloned state", () => {
saveDataGridStructuredFilterState("cache", {
scopeKey: "scope-a",
manualWhereInput: "status = 1",
rules: [{ id: "rule-1", columnName: "status", mode: "equals", rawValue: "1", rawEndValue: "", conjunction: "AND" }],
appliedWhereInput: '"status" = 1',
serverColumnFilters: { 0: { condition: '"status" = 1', keys: ["1"], labels: ["1"] } },
describe("data grid structured filter persistence", () => {
const cacheKey = "issue-436-filter-view";
const scopeKey = "mysql\0demo\0users";
beforeEach(() => {
saveDataGridStructuredFilterState(cacheKey, {
scopeKey,
manualWhereInput: "tenant_id = 7",
rules: [{ id: "r1", columnName: "status", mode: "equals", rawValue: "open", rawEndValue: "", conjunction: "AND" }],
appliedWhereInput: "status = 'open'",
serverColumnFilters: {},
});
});
expect(loadDataGridStructuredFilterState("cache", "scope-b")).toBeUndefined();
const restored = loadDataGridStructuredFilterState("cache", "scope-a")!;
restored.rules[0].rawValue = "2";
restored.serverColumnFilters[0].keys.push("2");
it("restores the filter rules and manual condition", () => {
expect(loadDataGridStructuredFilterState(cacheKey, scopeKey)).toMatchObject({
manualWhereInput: "tenant_id = 7",
rules: [{ columnName: "status", rawValue: "open" }],
});
});
expect(loadDataGridStructuredFilterState("cache", "scope-a")).toMatchObject({
rules: [{ rawValue: "1" }],
serverColumnFilters: { 0: { keys: ["1"] } },
it("does not leak filters into another table scope", () => {
expect(loadDataGridStructuredFilterState(cacheKey, `${scopeKey}\0archive`)).toBeUndefined();
});
it("returns cloned state without mutating the cache", () => {
const restored = loadDataGridStructuredFilterState(cacheKey, scopeKey)!;
restored.rules[0].rawValue = "closed";
restored.serverColumnFilters[0] = { condition: "\"status\" = 'closed'", keys: ["closed"], labels: ["closed"] };
expect(loadDataGridStructuredFilterState(cacheKey, scopeKey)).toMatchObject({
rules: [{ rawValue: "open" }],
serverColumnFilters: {},
});
});
});
@@ -0,0 +1,21 @@
import { describe, expect, it } from "vitest";
import { resolveDataGridFilterRuleDropPlacement } from "@/lib/dataGrid/dataGridFilterRuleDrag";
const bounds = [
{ id: "r1", top: 100, bottom: 130 },
{ id: "r2", top: 130, bottom: 160 },
{ id: "r3", top: 160, bottom: 190 },
{ id: "r4", top: 190, bottom: 220 },
];
describe("resolveDataGridFilterRuleDropPlacement", () => {
it("keeps the first and last insertion targets active beyond the outer rows", () => {
expect(resolveDataGridFilterRuleDropPlacement(["r1", "r2", "r3", "r4"], "r3", bounds, 40)).toEqual({ ruleId: "r1", position: "before", targetIndex: 0 });
expect(resolveDataGridFilterRuleDropPlacement(["r1", "r2", "r3", "r4"], "r2", bounds, 280)).toEqual({ ruleId: "r4", position: "after", targetIndex: 3 });
});
it("calculates the final index after removing the dragged rule", () => {
expect(resolveDataGridFilterRuleDropPlacement(["r1", "r2", "r3", "r4"], "r1", bounds, 165)).toEqual({ ruleId: "r3", position: "before", targetIndex: 1 });
expect(resolveDataGridFilterRuleDropPlacement(["r1", "r2", "r3", "r4"], "r4", bounds, 140)).toEqual({ ruleId: "r2", position: "before", targetIndex: 1 });
});
});
@@ -0,0 +1,27 @@
export type DataGridFilterRuleDropPlacement = {
ruleId: string;
position: "before" | "after";
targetIndex: number;
};
export type DataGridFilterRuleBounds = {
id: string;
top: number;
bottom: number;
};
export function resolveDataGridFilterRuleDropPlacement(ruleIds: readonly string[], draggingRuleId: string, rowBounds: readonly DataGridFilterRuleBounds[], clientY: number): DataGridFilterRuleDropPlacement | undefined {
const remainingRuleIds = ruleIds.filter((id) => id !== draggingRuleId);
const boundsById = new Map(rowBounds.map((bounds) => [bounds.id, bounds]));
const boundedRules = remainingRuleIds.flatMap((id, targetIndex) => {
const bounds = boundsById.get(id);
return bounds ? [{ id, targetIndex, bounds }] : [];
});
if (!boundedRules.length) return undefined;
const nextRule = boundedRules.find(({ bounds }) => clientY < bounds.top + (bounds.bottom - bounds.top) / 2);
if (nextRule) return { ruleId: nextRule.id, position: "before", targetIndex: nextRule.targetIndex };
const lastRule = boundedRules[boundedRules.length - 1];
return { ruleId: lastRule.id, position: "after", targetIndex: remainingRuleIds.length };
}
@@ -0,0 +1,8 @@
export const DATA_GRID_TEXT_FILTER_PANEL_HEIGHT_MIN = 96;
export const DATA_GRID_TEXT_FILTER_PANEL_HEIGHT_MAX = 420;
export const DATA_GRID_TEXT_FILTER_PANEL_HEIGHT_DEFAULT = 168;
export function normalizeDataGridTextFilterPanelHeight(value: unknown): number {
if (typeof value !== "number" || !Number.isFinite(value)) return DATA_GRID_TEXT_FILTER_PANEL_HEIGHT_DEFAULT;
return Math.min(DATA_GRID_TEXT_FILTER_PANEL_HEIGHT_MAX, Math.max(DATA_GRID_TEXT_FILTER_PANEL_HEIGHT_MIN, Math.round(value)));
}
@@ -69,6 +69,11 @@ describe("EDITOR_SETTINGS_DRAFT_KEYS", () => {
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("colorizeDataGridCellTypes");
});
it("includes the data grid filter view", () => {
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("dataGridFilterEditorView");
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("dataGridTextFilterPanelHeight");
});
it("includes completionTriggerMode", () => {
expect(EDITOR_SETTINGS_DRAFT_KEYS).toContain("completionTriggerMode");
});
@@ -124,6 +129,12 @@ describe("editorSettingsDraftFromSettings", () => {
expect(editorSettingsDraftFromSettings(makeSettings({ colorizeDataGridCellTypes: false })).colorizeDataGridCellTypes).toBe(false);
});
it("maps the data grid filter view from settings", () => {
const draft = editorSettingsDraftFromSettings(makeSettings({ dataGridFilterEditorView: "text", dataGridTextFilterPanelHeight: 224 }));
expect(draft.dataGridFilterEditorView).toBe("text");
expect(draft.dataGridTextFilterPanelHeight).toBe(224);
});
it("preserves the table-open default for legacy settings", () => {
const settings = makeSettings();
delete (settings as Partial<EditorSettings>).tableOpenPageSize;
@@ -137,6 +137,7 @@ describe("settings search", () => {
{ titleKey: "settings.confirmDangerousSqlExecution", category: "editor", targetId: "editor" },
{ titleKey: "settings.continueOnErrorOnBatch", category: "editor", targetId: "editor" },
{ titleKey: "settings.dataGridQuickEntry", category: "data", targetId: "data" },
{ titleKey: "settings.dataGridFilterView", category: "data", targetId: "data" },
{ titleKey: "settings.colorizeDataGridCellTypes", category: "data", targetId: "data" },
{ titleKey: "transfer.dataTransfer", category: "appearance", targetId: "appearance" },
{ titleKey: "toolbar.driverManager", category: "appearance", targetId: "appearance" },
@@ -37,6 +37,8 @@ export const EDITOR_SETTINGS_DRAFT_KEYS = [
"showIndexIndicatorsInHeader",
"compactColumnHeaderActions",
"dataGridQuickEntry",
"dataGridFilterEditorView",
"dataGridTextFilterPanelHeight",
"dataGridAutoTransposeSingleRow",
"pageSize",
"tableOpenPageSize",
@@ -185,6 +185,7 @@ export const SETTINGS_SEARCH_DEFINITIONS: readonly SettingsSearchDefinition[] =
{ id: "data-grid-infinite-scroll", category: "data", titleKey: "settings.infiniteScroll", descriptionKey: "settings.infiniteScrollDescription", targetId: "data" },
{ id: "data-grid-auto-transpose", category: "data", titleKey: "settings.dataGridAutoTransposeSingleRow", descriptionKey: "settings.dataGridAutoTransposeSingleRowDescription", targetId: "data" },
{ id: "data-grid-quick-entry", category: "data", titleKey: "settings.dataGridQuickEntry", descriptionKey: "settings.dataGridQuickEntryDescription", targetId: "data" },
{ id: "data-grid-filter-view", category: "data", titleKey: "settings.dataGridFilterView", descriptionKey: "settings.dataGridFilterViewDescription", targetId: "data" },
{ id: "data-grid-flattening-multi-line", category: "data", titleKey: "settings.flatteningMultiLineText", descriptionKey: "settings.flatteningMultiLineTextDescription", targetId: "data" },
{ id: "appearance-toolbar", category: "appearance", titleKey: "settings.toolbarTitle", descriptionKey: "settings.toolbarHiddenHint", targetId: "appearance" },
{ id: "appearance-exclusive-sidebar-panels", category: "appearance", titleKey: "settings.exclusiveRightSidebarPanels", descriptionKey: "settings.exclusiveRightSidebarPanelsDescription", targetId: "appearance" },