feat(editor): allow disabling SQL snippets

This commit is contained in:
t8y2 2026-07-07 12:36:19 +08:00
parent 705bacba04
commit 65af804099
13 changed files with 93 additions and 6 deletions

View File

@ -326,7 +326,11 @@ const visibleTableColumnTemplateRows = computed(() =>
);
// --- Snippet state ---
const editSnippets = ref<SqlSnippet[]>(settingsStore.editorSettings.snippets.map((s) => ({ ...s })));
function editableSnippet(snippet: SqlSnippet): SqlSnippet {
return { ...snippet, enabled: snippet.enabled !== false };
}
const editSnippets = ref<SqlSnippet[]>(settingsStore.editorSettings.snippets.map(editableSnippet));
const snippetDialogOpen = ref(false);
const snippetEditingId = ref<string | null>(null);
@ -445,6 +449,7 @@ function saveSnippet() {
label: snippetForm.value.label.trim() || prefix,
prefix,
body: snippetForm.value.body,
enabled: editSnippets.value[idx].enabled !== false,
};
}
} else {
@ -453,11 +458,18 @@ function saveSnippet() {
label: snippetForm.value.label.trim() || prefix,
prefix,
body: snippetForm.value.body,
enabled: true,
});
}
snippetDialogOpen.value = false;
}
function setSnippetEnabled(id: string, enabled: boolean) {
const idx = editSnippets.value.findIndex((s) => s.id === id);
if (idx === -1) return;
editSnippets.value[idx] = { ...editSnippets.value[idx], enabled };
}
function deleteSnippet(id: string) {
editSnippets.value = editSnippets.value.filter((s) => s.id !== id);
}
@ -593,7 +605,7 @@ watch(
editQueryExportKeysetOptimizationEnabled.value = settingsStore.editorSettings.queryExportKeysetOptimizationEnabled;
editUpdateDownloadSource.value = settingsStore.editorSettings.updateDownloadSource;
editToolbarItems.value = { ...settingsStore.editorSettings.toolbarItems };
editSnippets.value = settingsStore.editorSettings.snippets.map((s) => ({ ...s }));
editSnippets.value = settingsStore.editorSettings.snippets.map(editableSnippet);
}
},
{ immediate: true },
@ -3470,12 +3482,13 @@ onUnmounted(cleanupPreviewEditor);
<div class="flex items-center justify-between">
<p class="text-sm text-muted-foreground">{{ t("settings.snippetsDescription") }}</p>
<Button variant="outline" size="sm" @click="openAddSnippetDialog">
<Plus class="mr-2 h-4 w-4" />
{{ t("settings.snippetsAdd") }}
</Button>
</div>
<div class="rounded-md border">
<table class="w-full text-sm">
<div class="overflow-x-auto rounded-md border">
<table class="w-full min-w-[720px] text-sm">
<thead>
<tr class="border-b bg-muted/50">
<th class="px-3 py-2 text-left font-medium whitespace-nowrap">
@ -3484,6 +3497,9 @@ onUnmounted(cleanupPreviewEditor);
<th class="px-3 py-2 text-left font-medium whitespace-nowrap">
{{ t("settings.snippetsPrefix") }}
</th>
<th class="px-3 py-2 text-left font-medium whitespace-nowrap">
{{ t("settings.snippetsStatus") }}
</th>
<th class="px-3 py-2 text-left font-medium whitespace-nowrap">
{{ t("settings.snippetsBody") }}
</th>
@ -3491,13 +3507,21 @@ onUnmounted(cleanupPreviewEditor);
</tr>
</thead>
<tbody>
<tr v-for="snippet in editSnippets" :key="snippet.id" class="border-b last:border-b-0 hover:bg-muted/30">
<tr v-for="snippet in editSnippets" :key="snippet.id" class="border-b last:border-b-0 hover:bg-muted/30" :class="snippet.enabled === false ? 'text-muted-foreground' : ''">
<td class="px-3 py-2">{{ snippet.label }}</td>
<td class="px-3 py-2">
<Badge variant="outline" class="h-5 rounded-md px-1.5 text-[11px] font-mono text-muted-foreground">
{{ snippet.prefix }}
</Badge>
</td>
<td class="px-3 py-2">
<div class="flex items-center gap-2">
<Switch :id="`snippet-enabled-${snippet.id}`" :model-value="snippet.enabled !== false" size="sm" :aria-label="t('settings.snippetsToggle')" @update:model-value="(value: boolean) => setSnippetEnabled(snippet.id, value)" />
<Label :for="`snippet-enabled-${snippet.id}`" class="text-xs font-normal text-muted-foreground">
{{ snippet.enabled === false ? t("settings.snippetsDisabled") : t("settings.snippetsEnabled") }}
</Label>
</div>
</td>
<td class="px-3 py-2 font-mono text-xs text-muted-foreground max-w-[300px] truncate">
{{ snippet.body }}
</td>

View File

@ -2942,6 +2942,10 @@ export default {
snippetsAdd: "Add Snippet",
snippetsLabel: "Label",
snippetsPrefix: "Prefix",
snippetsStatus: "Status",
snippetsEnabled: "Enabled",
snippetsDisabled: "Disabled",
snippetsToggle: "Enable or disable snippet",
snippetsBody: "SQL Body",
snippetsLabelPlaceholder: "e.g. select *",
snippetsPrefixPlaceholder: "e.g. sel",

View File

@ -2855,6 +2855,10 @@ export default withEnglishFallback({
snippetsAdd: "Agregar fragmento",
snippetsLabel: "Etiqueta",
snippetsPrefix: "Prefijo",
snippetsStatus: "Estado",
snippetsEnabled: "Activado",
snippetsDisabled: "Desactivado",
snippetsToggle: "Activar o desactivar fragmento",
snippetsBody: "SQL",
snippetsLabelPlaceholder: "p. ej. select *",
snippetsPrefixPlaceholder: "p. ej. sel",

View File

@ -2853,6 +2853,10 @@ export default withEnglishFallback({
snippetsAdd: "Aggiungi Snippet",
snippetsLabel: "Etichetta",
snippetsPrefix: "Prefisso",
snippetsStatus: "Stato",
snippetsEnabled: "Attivo",
snippetsDisabled: "Disattivo",
snippetsToggle: "Attiva o disattiva snippet",
snippetsBody: "Corpo SQL",
snippetsLabelPlaceholder: "es. select *",
snippetsPrefixPlaceholder: "es. sel",

View File

@ -2844,6 +2844,10 @@ export default withEnglishFallback({
snippetsAdd: "スニペットを追加",
snippetsLabel: "ラベル",
snippetsPrefix: "プレフィックス",
snippetsStatus: "ステータス",
snippetsEnabled: "有効",
snippetsDisabled: "無効",
snippetsToggle: "スニペットを有効または無効にする",
snippetsBody: "SQL本文",
snippetsLabelPlaceholder: "例: select *",
snippetsPrefixPlaceholder: "例: sel",

View File

@ -2854,6 +2854,10 @@ export default withEnglishFallback({
snippetsAdd: "Adicionar snippet",
snippetsLabel: "Rótulo",
snippetsPrefix: "Prefixo",
snippetsStatus: "Status",
snippetsEnabled: "Ativo",
snippetsDisabled: "Desativado",
snippetsToggle: "Ativar ou desativar snippet",
snippetsBody: "Corpo SQL",
snippetsLabelPlaceholder: "ex.: select *",
snippetsPrefixPlaceholder: "ex.: sel",

View File

@ -2942,6 +2942,10 @@ export default withEnglishFallback({
snippetsAdd: "添加片段",
snippetsLabel: "显示名",
snippetsPrefix: "触发键",
snippetsStatus: "状态",
snippetsEnabled: "已启用",
snippetsDisabled: "已关闭",
snippetsToggle: "启用或关闭片段",
snippetsBody: "SQL 内容",
snippetsLabelPlaceholder: "如select *",
snippetsPrefixPlaceholder: "如sel",

View File

@ -2751,6 +2751,10 @@ export default withEnglishFallback({
snippetsAdd: "新增片段",
snippetsLabel: "顯示名",
snippetsPrefix: "觸發鍵",
snippetsStatus: "狀態",
snippetsEnabled: "已啟用",
snippetsDisabled: "已停用",
snippetsToggle: "啟用或停用片段",
snippetsBody: "SQL 內容",
snippetsLabelPlaceholder: "如select *",
snippetsPrefixPlaceholder: "如sel",

View File

@ -81,6 +81,11 @@ describe("buildSnippetItems", () => {
expect(items[0].label).toBe("select all");
});
it("does not return disabled snippets", () => {
const items = buildSnippetItemsForTest("sel", [{ ...TEST_SNIPPETS[0], enabled: false }, TEST_SNIPPETS[1]]);
expect(items).toEqual([]);
});
it("does not keep matching a renamed snippet by its old short label prefix", () => {
const items = buildSnippetItemsForTest("sel", [{ id: "1", label: "select all", prefix: "fff", body: "SELECT *\nFROM my_table;" }]);
expect(items).toEqual([]);

View File

@ -662,66 +662,77 @@ export const DEFAULT_SQL_SNIPPETS: SqlSnippet[] = [
label: "select *",
prefix: "sel",
body: "SELECT *\nFROM table\nLIMIT 100;",
enabled: true,
},
{
id: "builtin-ins",
label: "insert into",
prefix: "ins",
body: "INSERT INTO table (columns)\nVALUES (values);",
enabled: true,
},
{
id: "builtin-upd",
label: "update set",
prefix: "upd",
body: "UPDATE table\nSET column = value\nWHERE condition;",
enabled: true,
},
{
id: "builtin-cte",
label: "common table expression",
prefix: "cte",
body: "WITH name AS (\n SELECT columns\n FROM table\n)\nSELECT *\nFROM name;",
enabled: true,
},
{
id: "builtin-join",
label: "join",
prefix: "join",
body: "JOIN table ON left_column = right_column",
enabled: true,
},
{
id: "builtin-case",
label: "case when",
prefix: "case",
body: "CASE\n WHEN condition THEN value\n ELSE default\nEND",
enabled: true,
},
{
id: "builtin-ct",
label: "create table",
prefix: "ct",
body: "CREATE TABLE table (\n column type\n);",
enabled: true,
},
{
id: "builtin-ex",
label: "exists",
prefix: "ex",
body: "EXISTS (\n SELECT 1\n FROM table\n WHERE condition\n)",
enabled: true,
},
{
id: "builtin-nex",
label: "not exists",
prefix: "nex",
body: "NOT EXISTS (\n SELECT 1\n FROM table\n WHERE condition\n)",
enabled: true,
},
{
id: "builtin-at",
label: "alter table add column",
prefix: "at",
body: "ALTER TABLE table\nADD COLUMN column type;",
enabled: true,
},
{
id: "builtin-ci",
label: "create index",
prefix: "ci",
body: "CREATE INDEX idx_name\nON table (column);",
enabled: true,
},
];
@ -3587,6 +3598,7 @@ function buildSnippetItems(prefix: string, snippets: SqlSnippet[], keywordCase?:
if (!prefix) return [];
return snippets
.filter((snippet) => {
if (snippet.enabled === false) return false;
const matchesSnippetPrefix = matchesPrefix(snippet.prefix, prefix);
const matchesSnippetLabel = prefix.length > snippet.prefix.length && matchesPrefix(snippet.label, prefix);
return matchesSnippetPrefix || matchesSnippetLabel;

View File

@ -658,7 +658,8 @@ function normalizeSqlSnippets(value: unknown, existing?: SqlSnippet[]): SqlSnipp
}
if (seenPrefixes.has(item.prefix)) continue;
seenPrefixes.add(item.prefix);
valid.push({ id: item.id, label: item.label, prefix: item.prefix, body: item.body });
// Older settings do not have this field; only an explicit false disables a snippet.
valid.push({ id: item.id, label: item.label, prefix: item.prefix, body: item.body, enabled: item.enabled !== false });
}
if (valid.length === 0) return existing ?? DEFAULT_SQL_SNIPPETS;
return valid;

View File

@ -68,6 +68,7 @@ export interface SqlSnippet {
label: string;
prefix: string;
body: string;
enabled?: boolean;
}
export type CompletionAssistantObjectKind = "database" | "schema" | "table" | "view" | "routine" | "procedure" | "function" | "column";

View File

@ -136,6 +136,22 @@ test("defaults statement run buttons to enabled and preserves saved booleans", (
assert.equal(normalizeEditorSettings({ showStatementRunButtons: "nope" as any }).showStatementRunButtons, true);
});
test("normalizes SQL snippet enabled state", () => {
const settings = normalizeEditorSettings({
snippets: [
{ id: "legacy", label: "legacy", prefix: "leg", body: "SELECT 1;" },
{ id: "disabled", label: "disabled", prefix: "dis", body: "SELECT 2;", enabled: false },
{ id: "invalid", label: "invalid", prefix: "inv", body: "SELECT 3;", enabled: "nope" },
],
} as any);
assert.deepEqual(settings.snippets, [
{ id: "legacy", label: "legacy", prefix: "leg", body: "SELECT 1;", enabled: true },
{ id: "disabled", label: "disabled", prefix: "dis", body: "SELECT 2;", enabled: false },
{ id: "invalid", label: "invalid", prefix: "inv", body: "SELECT 3;", enabled: true },
]);
});
test("defaults unsaved SQL close confirmation to enabled", () => {
assert.equal(DEFAULT_EDITOR_SETTINGS.confirmUnsavedSqlClose, true);
assert.equal(normalizeEditorSettings({}).confirmUnsavedSqlClose, true);