From 60bfb41f13996468399e465c9dda206ca5d3b1bb Mon Sep 17 00:00:00 2001
From: jearhe <0verme7753@gmail.com>
Date: Fri, 28 Aug 2026 14:25:42 +0800
Subject: [PATCH] feat(editor): add clear-all action for sql parameter values
---
.../components/editor/SqlParameterDialog.vue | 13 +++
.../__tests__/SqlParameterDialog.spec.ts | 102 ++++++++++++++++++
apps/desktop/src/i18n/locales/en.ts | 1 +
apps/desktop/src/i18n/locales/es.ts | 1 +
apps/desktop/src/i18n/locales/it.ts | 1 +
apps/desktop/src/i18n/locales/ja.ts | 1 +
apps/desktop/src/i18n/locales/ko.ts | 1 +
apps/desktop/src/i18n/locales/pt-BR.ts | 1 +
apps/desktop/src/i18n/locales/zh-CN.ts | 1 +
apps/desktop/src/i18n/locales/zh-TW.ts | 1 +
10 files changed, 123 insertions(+)
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: "字串",