diff --git a/apps/desktop/src/components/grid/__tests__/DataGridConditionEditor.spec.ts b/apps/desktop/src/components/grid/__tests__/DataGridConditionEditor.spec.ts index 8ee9a8e8e0..40045dfb42 100644 --- a/apps/desktop/src/components/grid/__tests__/DataGridConditionEditor.spec.ts +++ b/apps/desktop/src/components/grid/__tests__/DataGridConditionEditor.spec.ts @@ -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(); }); diff --git a/apps/desktop/src/composables/__tests__/useDataGridConditionEditor.spec.ts b/apps/desktop/src/composables/__tests__/useDataGridConditionEditor.spec.ts index 45826cefc9..f08057f888 100644 --- a/apps/desktop/src/composables/__tests__/useDataGridConditionEditor.spec.ts +++ b/apps/desktop/src/composables/__tests__/useDataGridConditionEditor.spec.ts @@ -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({ diff --git a/apps/desktop/src/composables/useDataGridConditionEditor.ts b/apps/desktop/src/composables/useDataGridConditionEditor.ts index 9cbb094688..c9dad01912 100644 --- a/apps/desktop/src/composables/useDataGridConditionEditor.ts +++ b/apps/desktop/src/composables/useDataGridConditionEditor.ts @@ -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 = [];