diff --git a/apps/desktop/src/components/editor/SqlParameterDialog.vue b/apps/desktop/src/components/editor/SqlParameterDialog.vue index 0528042d36..8b57c3fe32 100644 --- a/apps/desktop/src/components/editor/SqlParameterDialog.vue +++ b/apps/desktop/src/components/editor/SqlParameterDialog.vue @@ -86,6 +86,16 @@ function setAllParametersToRaw() { values.value = next; } +function clearParameterValues() { + const next = { ...values.value }; + for (const parameter of props.parameters) { + const current = next[parameter.key] ?? { kind: "string", value: "" }; + next[parameter.key] = { ...current, value: "" }; + } + activeHistoryName.value = ""; + values.value = next; +} + function updateValue(name: string, value: string) { const matchedHistory = histories.value[name]?.find((entry) => entry.value === value); values.value[name] = { ...(values.value[name] ?? { kind: "string" }), ...(matchedHistory ? { kind: matchedHistory.kind } : {}), value }; @@ -146,6 +156,9 @@ async function copyResolvedSql() {

{{ t("sqlParameters.description") }}

+ diff --git a/apps/desktop/src/components/editor/__tests__/SqlParameterDialog.spec.ts b/apps/desktop/src/components/editor/__tests__/SqlParameterDialog.spec.ts index 5cbb46e9a3..0c469a0f9b 100644 --- a/apps/desktop/src/components/editor/__tests__/SqlParameterDialog.spec.ts +++ b/apps/desktop/src/components/editor/__tests__/SqlParameterDialog.spec.ts @@ -37,6 +37,14 @@ const MIXED_VALUES: Record = { }; const RAW_VALUES = Object.fromEntries(Object.entries(MIXED_VALUES).map(([key, input]) => [key, { ...input, kind: "raw" as const }])); const RAW_SQL = "SELECT alpha beta, 42, NULL, current_date, ${empty_value}, alpha beta"; +const CLEARED_VALUES: Record = { + text_value: { kind: "string", value: "" }, + numeric_value: { kind: "number", value: "" }, + null_value: { kind: "null", value: "" }, + raw_value: { kind: "raw", value: "" }, + empty_value: { kind: "boolean", value: "" }, +}; +const CLEARED_SQL = "SELECT '', NULL, NULL, ${raw_value}, '', ''"; const mountedApps: App[] = []; @@ -74,12 +82,22 @@ function actionButton(): HTMLButtonElement | null { return document.body.querySelector('[data-testid="sql-parameters-use-raw-all"]'); } +function clearActionButton(): HTMLButtonElement | null { + return document.body.querySelector('[data-testid="sql-parameters-clear-values"]'); +} + function clickRawAction() { const button = actionButton(); expect(button).not.toBeNull(); button!.click(); } +function clickClearAction() { + const button = clearActionButton(); + expect(button).not.toBeNull(); + button!.click(); +} + function buttonWithText(text: string): HTMLButtonElement | undefined { return Array.from(document.body.querySelectorAll("button")).find((button) => button.textContent?.trim() === text); } @@ -164,3 +182,87 @@ describe("SqlParameterDialog raw parameter action", () => { expect(state.open).toBe(false); }); }); + +describe("SqlParameterDialog clear parameter values action", () => { + it("clears every displayed value while preserving kinds and updating the preview", async () => { + const { state, onExecute } = await mountDialog(); + + clickClearAction(); + await nextTick(); + + expect(parameterInputs().map((input) => input.value)).toEqual(Array(PARAMETERS.length).fill("")); + expect(Array.from(document.body.querySelectorAll('[role="combobox"]')).map((trigger) => trigger.textContent?.trim())).toEqual(["String", "Number", "NULL", "Raw SQL", "Boolean"]); + expect(parameterInputs()[2]?.disabled).toBe(true); + expect(document.body.querySelector("pre")?.textContent).toBe(CLEARED_SQL); + expect(state.open).toBe(true); + expect(onExecute).not.toHaveBeenCalled(); + expect(historyMocks.remember).not.toHaveBeenCalled(); + }); + + it("preserves parameter history after clearing current values", async () => { + const { onExecute } = await mountDialog(); + + clickClearAction(); + await nextTick(); + + const input = parameterInputs()[0]; + expect(input).not.toBeUndefined(); + input?.focus(); + await nextTick(); + await new Promise((resolve) => setTimeout(resolve, 0)); + + const historyEntry = Array.from(document.body.querySelectorAll("button")).find((button) => button.textContent?.includes("alpha beta")); + expect(historyEntry).toBeDefined(); + historyEntry?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true })); + await nextTick(); + + expect(parameterInputs()[0]?.value).toBe("alpha beta"); + expect(onExecute).not.toHaveBeenCalled(); + expect(historyMocks.remember).not.toHaveBeenCalled(); + }); + + it("keeps cancel side-effect free after clearing values", async () => { + const { state, onExecute } = await mountDialog(); + + clickClearAction(); + await nextTick(); + buttonWithText("Cancel")?.click(); + await nextTick(); + + expect(state.open).toBe(false); + expect(onExecute).not.toHaveBeenCalled(); + expect(historyMocks.remember).not.toHaveBeenCalled(); + }); + + it("remembers and emits cleared values only when Execute is clicked", async () => { + const { state, onExecute } = await mountDialog(); + + clickClearAction(); + await nextTick(); + expect(historyMocks.remember).not.toHaveBeenCalled(); + + buttonWithText("Execute")?.click(); + await nextTick(); + + expect(historyMocks.remember).toHaveBeenCalledOnce(); + expect(historyMocks.remember).toHaveBeenCalledWith(CLEARED_VALUES); + expect(onExecute).toHaveBeenCalledOnce(); + expect(onExecute).toHaveBeenCalledWith(CLEARED_SQL); + expect(state.open).toBe(false); + }); + + it("is idempotent when the current parameter values are already empty", async () => { + const { state, onExecute } = await mountDialog(CLEARED_VALUES); + const beforePreview = document.body.querySelector("pre")?.textContent; + + clickClearAction(); + await nextTick(); + + expect(parameterInputs().map((input) => input.value)).toEqual(Array(PARAMETERS.length).fill("")); + expect(Array.from(document.body.querySelectorAll('[role="combobox"]')).map((trigger) => trigger.textContent?.trim())).toEqual(["String", "Number", "NULL", "Raw SQL", "Boolean"]); + expect(document.body.querySelector("pre")?.textContent).toBe(beforePreview); + expect(state.open).toBe(true); + expect(onExecute).not.toHaveBeenCalled(); + expect(historyMocks.remember).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts index 4e6327d9c6..6f0a9c463d 100644 --- a/apps/desktop/src/i18n/locales/en.ts +++ b/apps/desktop/src/i18n/locales/en.ts @@ -5205,6 +5205,7 @@ export default { collectionValuePlaceholder: "Enter a JSON array or comma-separated values", preview: "SQL Preview", execute: "Execute", + clearValues: "Clear Values", useRawForAll: "Use Raw SQL for All", kind: { string: "String", diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts index 905d91f728..eba91a949f 100644 --- a/apps/desktop/src/i18n/locales/es.ts +++ b/apps/desktop/src/i18n/locales/es.ts @@ -4951,6 +4951,7 @@ export default withEnglishFallback({ collectionValuePlaceholder: "Introduce un array JSON o valores separados por comas", preview: "Vista previa SQL", execute: "Ejecutar", + clearValues: "Borrar valores", useRawForAll: "Usar SQL sin procesar para todos", kind: { string: "Cadena", diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts index f391430dcc..b8736fb373 100644 --- a/apps/desktop/src/i18n/locales/it.ts +++ b/apps/desktop/src/i18n/locales/it.ts @@ -4949,6 +4949,7 @@ export default withEnglishFallback({ collectionValuePlaceholder: "Inserisci un array JSON o valori separati da virgole", preview: "Anteprima SQL", execute: "Esegui", + clearValues: "Cancella valori", useRawForAll: "Usa SQL non elaborato per tutti", kind: { string: "Stringa", diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts index 53a13100bd..d8cffd0521 100644 --- a/apps/desktop/src/i18n/locales/ja.ts +++ b/apps/desktop/src/i18n/locales/ja.ts @@ -4978,6 +4978,7 @@ export default withEnglishFallback({ collectionValuePlaceholder: "JSON 配列またはカンマ区切りの値を入力", preview: "SQLプレビュー", execute: "実行", + clearValues: "値をクリア", useRawForAll: "すべてを生の SQL に設定", kind: { string: "文字列", diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts index 1dab66391d..c42ef6d554 100644 --- a/apps/desktop/src/i18n/locales/ko.ts +++ b/apps/desktop/src/i18n/locales/ko.ts @@ -4634,6 +4634,7 @@ export default withEnglishFallback({ collectionValuePlaceholder: "JSON 배열 또는 쉼표로 구분된 값 입력", preview: "SQL 미리보기", execute: "실행", + clearValues: "값 지우기", useRawForAll: "모두 원시 SQL로 설정", kind: { string: "문자열", diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts index 4e221b1bed..302bd50ec2 100644 --- a/apps/desktop/src/i18n/locales/pt-BR.ts +++ b/apps/desktop/src/i18n/locales/pt-BR.ts @@ -4951,6 +4951,7 @@ export default withEnglishFallback({ collectionValuePlaceholder: "Insira um array JSON ou valores separados por vírgula", preview: "Prévia SQL", execute: "Executar", + clearValues: "Limpar valores", useRawForAll: "Usar SQL bruto para todos", kind: { string: "Texto", diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts index 3d66fabc17..77e9ce60da 100644 --- a/apps/desktop/src/i18n/locales/zh-CN.ts +++ b/apps/desktop/src/i18n/locales/zh-CN.ts @@ -5190,6 +5190,7 @@ export default withEnglishFallback({ collectionValuePlaceholder: "输入 JSON 数组或逗号分隔值", preview: "SQL 预览", execute: "执行", + clearValues: "清空参数值", useRawForAll: "全部使用原始 SQL", kind: { string: "字符串", diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts index 440808d429..b3793dbafc 100644 --- a/apps/desktop/src/i18n/locales/zh-TW.ts +++ b/apps/desktop/src/i18n/locales/zh-TW.ts @@ -4275,6 +4275,7 @@ export default withEnglishFallback({ collectionValuePlaceholder: "輸入 JSON 陣列或逗號分隔值", preview: "SQL 預覽", execute: "執行", + clearValues: "清空參數值", useRawForAll: "全部使用原始 SQL", kind: { string: "字串",