diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue index d0af3bdf82..e50b01b02e 100644 --- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue +++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue @@ -154,8 +154,9 @@ import { isMcpPolicyMutationBlocked, MCP_CAPABILITY_ROWS, MCP_EXECUTION_MODE_COL import { isMacOS, isWindows } from "@/lib/backend/platform"; import { combineDataTypeForDatabase, dataTypeLengthInputValue, getDataTypeOptions, getDefaultLengthForType, isDataTypeLengthDisabled, splitDataType } from "@/lib/table/tableStructureEditorState"; import { useToast } from "@/composables/useToast"; -import type { DatabaseType, SqlSnippet } from "@/types/database"; +import type { DatabaseType, SqlShortcutAction, SqlSnippet } from "@/types/database"; import { uuid } from "@/lib/common/utils"; +import { findSqlShortcutConflicts, hasSqlShortcutConflicts as sqlShortcutsHaveConflicts, SQL_SHORTCUT_TABLE_TOKEN } from "@/lib/sql/sqlShortcutActions"; import { DEFAULT_SQL_SNIPPETS } from "@/lib/sql/sqlCompletion"; import AiProviderLogo from "@/components/icons/AiProviderLogo.vue"; import AppLogo from "@/components/icons/AppLogo.vue"; @@ -563,6 +564,12 @@ function editableSnippet(snippet: SqlSnippet): SqlSnippet { const editSnippets = ref(settingsStore.editorSettings.snippets.map(editableSnippet)); +function editableSqlShortcut(action: SqlShortcutAction): SqlShortcutAction { + return { ...action, enabled: action.enabled !== false }; +} + +const editSqlShortcuts = ref(settingsStore.editorSettings.sqlShortcuts.map(editableSqlShortcut)); + function currentEditorSettingsDraft(): EditorSettingsDraft { return { fontFamily: editFontFamily.value, @@ -652,6 +659,7 @@ function currentEditorSettingsDraft(): EditorSettingsDraft { updateDownloadSource: editUpdateDownloadSource.value, toolbarItems: { ...editToolbarItems.value }, snippets: editSnippets.value, + sqlShortcuts: editSqlShortcuts.value, sqlVariableSubstitutionEnabled: editSqlVariableSubstitutionEnabled.value, sqlVariableSyntaxOverrides: editSqlVariableSyntaxOverrides.value, clickTableNavigationTarget: editClickTableNavigationTarget.value, @@ -665,6 +673,12 @@ const snippetDialogOpen = ref(false); const snippetEditingId = ref(null); const snippetForm = ref({ label: "", prefix: "", body: "" }); const snippetFormPrefixError = ref(""); + +const sqlShortcutDialogOpen = ref(false); +const sqlShortcutEditingId = ref(null); +const sqlShortcutForm = ref({ label: "", shortcut: "", sql: "" }); +const sqlShortcutFormLabelError = ref(""); +const editingSqlShortcutInputId = ref(null); const iconThemeBlackDescriptionText = computed(() => (isMacOS() ? t("settings.iconThemeBlackDescriptionMac") : t("settings.iconThemeBlackDescription"))); const layoutDescTruncated = { separated: ref(false), @@ -792,6 +806,127 @@ function confirmDeleteSnippet(snippet: SqlSnippet) { } } +function openAddSqlShortcutDialog() { + sqlShortcutEditingId.value = null; + sqlShortcutForm.value = { label: "", shortcut: "", sql: `SELECT * FROM ${SQL_SHORTCUT_TABLE_TOKEN}` }; + sqlShortcutFormLabelError.value = ""; + editingSqlShortcutInputId.value = null; + sqlShortcutDialogOpen.value = true; +} + +function openEditSqlShortcutDialog(action: SqlShortcutAction) { + sqlShortcutEditingId.value = action.id; + sqlShortcutForm.value = { + label: action.label, + shortcut: action.shortcut, + sql: action.sql, + }; + sqlShortcutFormLabelError.value = ""; + editingSqlShortcutInputId.value = null; + sqlShortcutDialogOpen.value = true; +} + +function saveSqlShortcut() { + const label = sqlShortcutForm.value.label.trim(); + if (!label) { + sqlShortcutFormLabelError.value = t("settings.sqlShortcutsLabelRequired"); + return; + } + const payload: SqlShortcutAction = { + id: sqlShortcutEditingId.value ?? uuid(), + label, + shortcut: sqlShortcutForm.value.shortcut.trim(), + sql: sqlShortcutForm.value.sql, + enabled: true, + }; + const nextShortcuts = sqlShortcutEditingId.value + ? editSqlShortcuts.value.map((item) => + item.id === sqlShortcutEditingId.value + ? { + ...payload, + enabled: item.enabled !== false, + } + : item, + ) + : [...editSqlShortcuts.value, payload]; + if (sqlShortcutsHaveConflicts(nextShortcuts, editShortcuts.value)) { + toast(t("settings.shortcutConflict"), 3000); + return; + } + editSqlShortcuts.value = nextShortcuts; + sqlShortcutDialogOpen.value = false; + void commitSqlShortcuts(); +} + +function setSqlShortcutEnabled(id: string, enabled: boolean) { + const idx = editSqlShortcuts.value.findIndex((item) => item.id === id); + if (idx === -1) return; + if (enabled) { + const next = editSqlShortcuts.value.map((item) => (item.id === id ? { ...item, enabled } : item)); + if (sqlShortcutsHaveConflicts(next, editShortcuts.value)) { + toast(t("settings.shortcutConflict"), 3000); + return; + } + } + editSqlShortcuts.value[idx] = { ...editSqlShortcuts.value[idx], enabled }; + void commitSqlShortcuts(); +} + +function deleteSqlShortcut(id: string) { + editSqlShortcuts.value = editSqlShortcuts.value.filter((item) => item.id !== id); + void commitSqlShortcuts(); +} + +function confirmDeleteSqlShortcut(action: SqlShortcutAction) { + if (window.confirm(t("settings.sqlShortcutsDeleteConfirm", { name: action.label }))) { + deleteSqlShortcut(action.id); + } +} + +function onSqlShortcutBindingKeydown(id: string, event: KeyboardEvent) { + event.preventDefault(); + event.stopPropagation(); + if (editingSqlShortcutInputId.value !== id) return; + if (event.key === "Escape") { + editingSqlShortcutInputId.value = null; + return; + } + const shortcut = eventToShortcut(event); + if (!shortcut) return; + sqlShortcutForm.value = { ...sqlShortcutForm.value, shortcut }; + editingSqlShortcutInputId.value = null; +} + +function focusSqlShortcutInput(id: string) { + editingSqlShortcutInputId.value = id; + const input = document.querySelector(`[data-sql-shortcut-input="${id}"]`); + requestAnimationFrame(() => { + input?.focus(); + input?.select(); + }); +} + +function cancelSqlShortcutInputEdit() { + editingSqlShortcutInputId.value = null; +} + +function clearSqlShortcutBinding() { + sqlShortcutForm.value = { ...sqlShortcutForm.value, shortcut: "" }; +} + +async function commitSqlShortcuts() { + const sqlShortcuts = editSqlShortcuts.value.map(editableSqlShortcut); + try { + await settingsStore.updateEditorSettingsAndPersist({ sqlShortcuts }); + editEditorSettingsBase.value = { + ...editEditorSettingsBase.value, + sqlShortcuts: JSON.parse(JSON.stringify(sqlShortcuts)) as SqlShortcutAction[], + }; + } catch (error) { + applySettingsErrorToast(error); + } +} + const uiFontPreviewValues = new Set([DEFAULT_UI_FONT_FAMILY, SYSTEM_UI_FONT_FAMILY]); const systemFontOptions = computed(() => { @@ -920,6 +1055,7 @@ function syncEditorSettingsDraftFromStore() { editUpdateDownloadSource.value = settingsStore.editorSettings.updateDownloadSource; editToolbarItems.value = { ...settingsStore.editorSettings.toolbarItems }; editSnippets.value = settingsStore.editorSettings.snippets.map(editableSnippet); + editSqlShortcuts.value = settingsStore.editorSettings.sqlShortcuts.map(editableSqlShortcut); editSqlVariableSubstitutionEnabled.value = settingsStore.editorSettings.sqlVariableSubstitutionEnabled; editSqlVariableSyntaxOverrides.value = normalizeSqlVariableSyntaxOverrides(settingsStore.editorSettings.sqlVariableSyntaxOverrides); editClickTableNavigationTarget.value = settingsStore.editorSettings.clickTableNavigationTarget; @@ -960,6 +1096,8 @@ const shortcutConflicts = computed(() => return conflict ? [definition.id] : []; }), ); +const sqlShortcutConflicts = computed(() => findSqlShortcutConflicts(editSqlShortcuts.value, editShortcuts.value)); +const hasSqlShortcutConflicts = computed(() => sqlShortcutConflicts.value.length > 0); const shortcutSearchQuery = ref(""); const formatterEditorShortcutIds: ShortcutActionId[] = [ "formatSql", @@ -997,8 +1135,13 @@ const filteredShortcutDefinitions = computed(() => { }); const hasShortcutConflicts = computed(() => shortcutConflicts.value.length > 0); const shortcutsChanged = computed(() => JSON.stringify(editShortcuts.value) !== JSON.stringify(editEditorSettingsBase.value.shortcuts)); +const sqlShortcutsChanged = computed(() => JSON.stringify(editSqlShortcuts.value) !== JSON.stringify(editEditorSettingsBase.value.sqlShortcuts)); const duckDbWorkerSettingsRequireRestart = computed(() => editDuckDbWorkerProcessIsolation.value !== startupDuckDbWorkerProcessIsolation.value || normalizeDuckDbWorkerMaxProcesses(editDuckDbWorkerMaxProcesses.value) !== startupDuckDbWorkerMaxProcesses.value); -const hasBlockingShortcutConflicts = computed(() => shortcutsChanged.value && hasShortcutConflicts.value); +const hasBlockingShortcutConflicts = computed(() => { + const shortcutDraftTouched = shortcutsChanged.value || sqlShortcutsChanged.value; + if (!shortcutDraftTouched) return false; + return hasShortcutConflicts.value || hasSqlShortcutConflicts.value; +}); const hasBlockingFormatterConfig = computed(() => activeSettingsTab.value === "formatter" && !sqlFormatterConfigValid.value); const hasBlockingQueryResultRowLimit = computed(() => editQueryResultMaxRowsEnabled.value && editQueryResultMaxRows.value < editPageSize.value); const hasApplyBlocker = computed(() => hasBlockingShortcutConflicts.value || hasBlockingFormatterConfig.value || hasBlockingQueryResultRowLimit.value); @@ -1304,6 +1447,7 @@ function resetAllDefaults() { editUpdateDownloadSource.value = DEFAULT_EDITOR_SETTINGS.updateDownloadSource; editToolbarItems.value = { ...DEFAULT_EDITOR_SETTINGS.toolbarItems }; editSnippets.value = DEFAULT_SQL_SNIPPETS.map((s) => ({ ...s })); + editSqlShortcuts.value = []; } function addTableColumnTemplateRow() { @@ -5957,6 +6101,70 @@ onUnmounted(() => { + +
+
+
+

{{ t("settings.sqlShortcutsTitle") }}

+

+ {{ t("settings.sqlShortcutsDescription") }} +

+
+ +
+ +
+ {{ t("settings.sqlShortcutsEmpty") }} +
+
+ + + + + + + + + + + + + + + + + + + +
{{ t("settings.sqlShortcutsLabel") }}{{ t("settings.shortcutPressShortcut") }}{{ t("settings.snippetsStatus") }}{{ t("settings.sqlShortcutsSql") }}
{{ action.label }} + + {{ action.shortcut ? formatShortcutPill(action.shortcut) : t("settings.sqlShortcutsUnbound") }} + + +
+ + +
+
{{ action.sql }} +
+ + +
+
+
+

+ {{ t("settings.shortcutConflict") }} +

+
@@ -7527,6 +7735,68 @@ onUnmounted(() => { + + + + + + {{ sqlShortcutEditingId ? t("settings.sqlShortcutsEditTitle") : t("settings.sqlShortcutsAddTitle") }} + + +
+
+ + +

+ {{ sqlShortcutFormLabelError }} +

+
+
+ +
+ + + + +
+
+
+ +