feat(grid): add persistent SQL filter views
This commit is contained in:
27 files changed
+1428
-107
No files matched your search
+31
-16
@@ -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" },
|
||||
|
||||
Reference in new issue
Block a user