fix(grid): restore first WHERE suggestion default selection

This commit is contained in:
二丫讲梵
2026-10-09 21:12:51 +08:00
committed by GitHub
parent 235842e6a9
commit 884eb047f9
3 changed files with 103 additions and 10 deletions
@@ -313,7 +313,7 @@ describe("DataGridConditionEditor quote completion", () => {
expect(value.value).toBe("name");
});
it("keeps the first WHERE field suggestion unselected and only applies on Enter", async () => {
it("selects the first WHERE field suggestion with Enter", async () => {
const { value, input } = mountEditor("where", "", { columns: ["customer_id", "customer_name"] });
input.focus();
input.value = "cus";
@@ -321,12 +321,11 @@ describe("DataGridConditionEditor quote completion", () => {
input.dispatchEvent(new Event("input", { bubbles: true }));
await vi.waitFor(() => expect(document.querySelectorAll('[role="option"]')).toHaveLength(2));
// 不再默认高亮:回车只用于应用筛选,不会把列名写进输入框(issue #10595)
expect(document.querySelector('[role="option"][aria-selected="true"]')).toBeNull();
expect(document.querySelector('[role="option"][aria-selected="true"]')?.textContent).toContain("customer_id");
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true }));
await nextTick();
expect(value.value).toBe("cus");
expect(value.value).toBe("customer_id");
expect(document.querySelector('[role="listbox"]')).toBeNull();
});
@@ -34,9 +34,6 @@ describe("useDataGridConditionEditor", () => {
value.value = "status = 1 AND cust";
await nextTick();
await vi.waitFor(() => expect(editor.suggestions.value.map((item) => item.value)).toEqual(["customer_id", "customer_name"]));
// 不再默认高亮第一条建议:必须显式导航/接受,避免回车应用筛选时改写输入(issue #10595)
expect(editor.highlightedIndex.value).toBe(-1);
expect(editor.navigate(1)).toBe(true);
expect(editor.highlightedIndex.value).toBe(0);
expect(editor.navigate(1)).toBe(true);
expect(editor.highlightedIndex.value).toBe(1);
@@ -46,6 +43,65 @@ describe("useDataGridConditionEditor", () => {
expect(value.value).toBe("status = 1 AND customer_name");
});
it.each(["cus", "status = 1 AND cus", "NOT cus", "status = 1 AND NOT cus", "(cus", "status = 1 OR (cus", "id BETWEEN 0 AND 1 AND cus", "id NOT BETWEEN 0 AND 1 OR cus", "name = 'BETWEEN' AND cus", "`BETWEEN` = 1 AND cus", "id BETWEEN abs(0) AND 1 AND cus"])(
"accepts the first WHERE field suggestion directly with Enter: %s",
async (condition) => {
const value = ref("");
const editor = useDataGridConditionEditor({ kind: "where", value, columns: ["customer_id", "customer_name"], historyScope: {} });
value.value = condition;
await nextTick();
await vi.waitFor(() => expect(editor.suggestions.value).toHaveLength(2));
expect(editor.highlightedIndex.value).toBe(0);
expect(editor.handleKeydown(keyboardEvent("Enter"))).toBe("accept");
expect(value.value).toBe(condition.replace(/cus$/, "customer_id"));
},
);
it.each(["status = 1 ", "status is", "status IS nu", "status NOT betw"])("applies WHERE conditions without automatically accepting keyword suggestions: %s", async (condition) => {
const value = ref("");
const editor = useDataGridConditionEditor({ kind: "where", value, columns: ["status"], historyScope: {} });
value.value = condition;
await nextTick();
await vi.waitFor(() => expect(editor.suggestions.value.length).toBeGreaterThan(0));
expect(editor.suggestions.value[0].kind).toBe("keyword");
expect(editor.highlightedIndex.value).toBe(-1);
expect(editor.handleKeydown(keyboardEvent("Enter"))).toBe("apply");
expect(value.value).toBe(condition);
});
it.each([
"id = 1 + 1",
"id = - 1",
"id = + 1",
"id = 2 * 1",
"id = 2 / 1",
"id = (1",
"id = abs(1",
"id = coalesce(0, 1",
"id BETWEEN 0 AND 1",
"id NOT BETWEEN 0 AND 1",
"id between 0 and 1",
"id BETWEEN abs(0) AND 1",
"id BETWEEN 'AND' AND 1",
"(id BETWEEN 0 AND 1",
"id = 2 AND other_id BETWEEN 0 AND 1",
"id BETWEEN CASE WHEN enabled = 1 AND flag = 1 THEN 0 ELSE 0 END AND 1",
"id BETWEEN CASE WHEN enabled = 1 OR flag = 1 THEN 0 ELSE 0 END AND 1",
"id BETWEEN CASE WHEN a = 1 THEN CASE WHEN b = 1 AND c = 1 THEN 0 ELSE 0 END ELSE 0 END AND 1",
])("does not rewrite value expressions on Enter: %s", async (condition) => {
const value = ref("");
const editor = useDataGridConditionEditor({ kind: "where", value, columns: ["id", "version1"], historyScope: {} });
value.value = condition;
await nextTick();
await vi.waitFor(() => expect(editor.suggestions.value).toEqual([{ value: "version1", kind: "column" }]));
expect(editor.highlightedIndex.value).toBe(-1);
expect(editor.handleKeydown(keyboardEvent("Enter"))).toBe("apply");
expect(value.value).toBe(condition);
});
it("does not suggest column names while typing a value after a comparison operator", async () => {
const value = ref("");
const editor = useDataGridConditionEditor({
@@ -310,6 +310,44 @@ function whereSuggestionRole(target: DataGridConditionCompletionTarget, identifi
return "connector";
}
function isBetweenRangeSeparator(prefix: string, separatorOffset: number): boolean {
// Ignore quoted values/identifiers and comments; BETWEEN state belongs to its parenthesis scope.
const tokens = prefix.slice(0, separatorOffset).match(/--[^\r\n]*|\/\*[\s\S]*?(?:\*\/|$)|'(?:''|\\.|[^'\\])*'|"(?:""|\\.|[^"\\])*"|`(?:``|[^`])*`|\[(?:\]\]|[^\]])*\]|[A-Za-z_][\w$]*|[()]/g) ?? [];
const pendingBetween = [false];
const caseDepth = [0];
for (const token of tokens) {
if (token === "(") {
pendingBetween.push(false);
caseDepth.push(0);
} else if (token === ")") {
if (pendingBetween.length > 1) {
pendingBetween.pop();
caseDepth.pop();
}
} else if (token.toUpperCase() === "CASE") {
caseDepth[caseDepth.length - 1] += 1;
} else if (token.toUpperCase() === "END") {
caseDepth[caseDepth.length - 1] = Math.max(0, caseDepth[caseDepth.length - 1] - 1);
} else if (token.toUpperCase() === "BETWEEN") {
pendingBetween[pendingBetween.length - 1] = true;
} else if (caseDepth[caseDepth.length - 1] === 0 && (token.toUpperCase() === "AND" || token.toUpperCase() === "OR")) {
pendingBetween[pendingBetween.length - 1] = false;
}
}
return pendingBetween[pendingBetween.length - 1];
}
function shouldPreselectWhereColumn(target: DataGridConditionCompletionTarget): boolean {
if (!target.token) return false;
const prefix = target.value.slice(0, target.from).trimEnd();
// Only preselect at the start of a predicate, not inside arithmetic or function arguments.
if (/^(?:NOT\b\s*|\(\s*)*$/i.test(prefix)) return true;
const connector = /(?:^|\s)(AND|OR)\b\s*(?:NOT\b\s*|\(\s*)*$/i.exec(prefix);
if (!connector) return false;
const separatorOffset = connector.index + connector[0].indexOf(connector[1]);
return connector[1].toUpperCase() !== "AND" || !isBetweenRangeSeparator(prefix, separatorOffset);
}
/**
* ORDER BY 的角色判定:开头或逗号之后是排序列位置(提示列名),
* 已经写出排序列之后是排序方向位置(提示 ASC/DESC)。
@@ -440,9 +478,9 @@ export function useDataGridConditionEditor(options: UseDataGridConditionEditorOp
const providerValues = values ? [...new Set(values)] : undefined;
suggestions.value = providerValues ? (providerValues.some((value) => value.toLowerCase() === target.token.toLowerCase()) ? [] : providerValues.slice(0, limit).map((suggestion) => ({ value: suggestion, kind: "column" }))) : defaultSuggestions(target).slice(0, limit);
replacementRange.value = { from: target.from, to: target.to };
// 不再默认高亮第一条建议:否则用户按回车«应用筛选»时会先把高亮项写进输入框(issue #10595)。
// 接受补全需要显式操作(↓/↑ 后回车、Tab 或点击)。
highlightedIndex.value = -1;
// WHERE 条件开头默认选中首个列名,支持直接回车补全(issue #7155)。
// 算术表达式、函数参数和关键字建议不默认选中,避免回车应用时改写条件(issue #10595)。
highlightedIndex.value = options.kind === "where" && (role === "field" || role === "after_not") && shouldPreselectWhereColumn(target) && suggestions.value[0]?.kind === "column" ? 0 : -1;
} catch (error) {
if (!controller.signal.aborted && requestId === suggestionRequestId) {
suggestions.value = [];