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,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { buildDataGridStructuredWhere, useDataGridFilterBuilder, type DataGridStructuredFilterRule } from "@/composables/useDataGridFilterBuilder";
import { describe, expect, it, vi } from "vitest";
import { buildDataGridStructuredWhere, createDataGridFilterConditionCache, moveDataGridStructuredFilterRule, useDataGridFilterBuilder, type DataGridStructuredFilterRule } from "@/composables/useDataGridFilterBuilder";
describe("useDataGridFilterBuilder", () => {
it("searches columns by camel-case initials and any-position text", () => {
@@ -52,4 +52,52 @@ describe("useDataGridFilterBuilder", () => {
]),
).toBe("((a) AND (b)) OR (c)");
});
it("moves complete and disabled rules while preserving their condition data", async () => {
let nextId = 0;
const builder = useDataGridFilterBuilder({
columns: ["a", "b", "c"],
createId: () => `rule-${++nextId}`,
isComplete: () => true,
buildCondition: async (rule) => `${rule.columnName} = ${rule.rawValue}`,
});
builder.ensureRule();
builder.updateRule("rule-1", { columnName: "a", rawValue: "1" });
builder.addRule();
builder.updateRule("rule-2", { columnName: "b", rawValue: "2", disabled: true });
builder.addRule();
builder.updateRule("rule-3", { columnName: "c", rawValue: "3" });
builder.moveRule("rule-3", 0);
expect(builder.rules.value.map((rule) => rule.id)).toEqual(["rule-3", "rule-1", "rule-2"]);
expect(builder.rules.value[2]).toMatchObject({ columnName: "b", rawValue: "2", disabled: true });
expect(await builder.buildWhere()).toBe("(c = 3) AND (a = 1)");
});
it("clamps rule moves and ignores unknown rule ids", () => {
const rule = (id: string): DataGridStructuredFilterRule => ({ id, columnName: id, mode: "equals", rawValue: id, rawEndValue: "", conjunction: "AND" });
const rules = [rule("a"), rule("b"), rule("c")];
expect(moveDataGridStructuredFilterRule(rules, "a", 99).map((item) => item.id)).toEqual(["b", "c", "a"]);
expect(moveDataGridStructuredFilterRule(rules, "c", -2).map((item) => item.id)).toEqual(["c", "a", "b"]);
expect(moveDataGridStructuredFilterRule(rules, "missing", 1)).toEqual(rules);
});
it("reuses unchanged rule conditions and drops removed rules", async () => {
const cache = createDataGridFilterConditionCache();
const buildFirst = vi.fn(async () => "id = 1");
const buildChanged = vi.fn(async () => "id = 2");
await expect(cache.resolve("rule-1", "id:1", buildFirst)).resolves.toBe("id = 1");
await expect(cache.resolve("rule-1", "id:1", buildFirst)).resolves.toBe("id = 1");
expect(buildFirst).toHaveBeenCalledOnce();
await expect(cache.resolve("rule-1", "id:2", buildChanged)).resolves.toBe("id = 2");
expect(buildChanged).toHaveBeenCalledOnce();
cache.retain([]);
await expect(cache.resolve("rule-1", "id:2", buildChanged)).resolves.toBe("id = 2");
expect(buildChanged).toHaveBeenCalledTimes(2);
});
});
@@ -29,6 +29,45 @@ export function buildDataGridStructuredWhere(items: Array<{ rule: DataGridStruct
return result;
}
export function moveDataGridStructuredFilterRule(rules: readonly DataGridStructuredFilterRule[], ruleId: string, targetIndex: number): DataGridStructuredFilterRule[] {
const sourceIndex = rules.findIndex((rule) => rule.id === ruleId);
if (sourceIndex < 0 || rules.length < 2) return [...rules];
const nextIndex = Math.min(rules.length - 1, Math.max(0, Math.trunc(targetIndex)));
if (sourceIndex === nextIndex) return [...rules];
const nextRules = [...rules];
const [rule] = nextRules.splice(sourceIndex, 1);
nextRules.splice(nextIndex, 0, rule);
return nextRules;
}
export function createDataGridFilterConditionCache() {
const entries = new Map<string, { signature: string; condition: Promise<string | null> }>();
function resolve(ruleId: string, signature: string, build: () => Promise<string | null>): Promise<string | null> {
const cached = entries.get(ruleId);
if (cached?.signature === signature) return cached.condition;
let condition: Promise<string | null>;
condition = Promise.resolve()
.then(build)
.catch((error) => {
if (entries.get(ruleId)?.condition === condition) entries.delete(ruleId);
throw error;
});
entries.set(ruleId, { signature, condition });
return condition;
}
function retain(ruleIds: readonly string[]) {
const retained = new Set(ruleIds);
for (const ruleId of entries.keys()) {
if (!retained.has(ruleId)) entries.delete(ruleId);
}
}
return { resolve, retain };
}
export function useDataGridFilterBuilder(options: UseDataGridFilterBuilderOptions) {
const rules = ref<DataGridStructuredFilterRule[]>([]);
const open = ref(false);
@@ -63,6 +102,9 @@ export function useDataGridFilterBuilder(options: UseDataGridFilterBuilderOption
return next;
});
}
function moveRule(id: string, targetIndex: number) {
rules.value = moveDataGridStructuredFilterRule(rules.value, id, targetIndex);
}
function reset() {
appliedWhereInput.value = "";
rules.value = toValue(options.columns).length ? [defaultRule()] : [];
@@ -90,5 +132,5 @@ export function useDataGridFilterBuilder(options: UseDataGridFilterBuilderOption
},
);
return { rules, open, columnSearch, appliedWhereInput, filteredColumns, activeCount, defaultRule, ensureRule, addRule, removeRule, updateRule, reset, buildWhere, apply };
return { rules, open, columnSearch, appliedWhereInput, filteredColumns, activeCount, defaultRule, ensureRule, addRule, removeRule, updateRule, moveRule, reset, buildWhere, apply };
}