CB-5832 Add SQL editor shortcut to save script (#3167)

* CB-5832 feat: add shortcut for save as script action

* CB-5832 feat: add info about a shortcut to Shortcuts modal

* CB-5832 fix: ru localization

* CB-5832 refactor: use SqlEditorView for action registration

* CB-5832 refactor: use SqlEditorView for action registration

* CB-5832 refactor: check action in handler

* CB-5832 refactor: bind function in constructor

---------

Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
This commit is contained in:
Sychev Andrey
2025-01-07 12:47:52 +03:00
committed by GitHub
co-authored by Daria Marutkina
parent 60d90b9e73
commit de6f9b7dae
20 changed files with 182 additions and 102 deletions
@@ -12,6 +12,9 @@
{
"path": "../core-connections/tsconfig.json"
},
{
"path": "../core-data-context/tsconfig.json"
},
{
"path": "../core-di/tsconfig.json"
},
+1
View File
@@ -32,6 +32,7 @@
"@cloudbeaver/plugin-data-spreadsheet-new": "^0",
"@cloudbeaver/plugin-navigation-tree": "^0",
"@cloudbeaver/plugin-sql-editor": "^0",
"@cloudbeaver/plugin-sql-editor-navigation-tab-script": "^0",
"@cloudbeaver/plugin-top-app-bar": "^0",
"react": "^18"
},
@@ -21,6 +21,7 @@ import {
KEY_BINDING_SQL_EDITOR_FORMAT,
KEY_BINDING_SQL_EDITOR_SHOW_EXECUTION_PLAN,
} from '@cloudbeaver/plugin-sql-editor';
import { KEY_BINDING_SQL_EDITOR_SAVE_AS_SCRIPT } from '@cloudbeaver/plugin-sql-editor-navigation-tab-script';
import type { IShortcut } from './IShortcut.js';
@@ -64,6 +65,10 @@ export const SQL_EDITOR_SHORTCUTS: IShortcut[] = [
label: 'sql_editor_shortcut_format',
code: transformKeys(KEY_BINDING_SQL_EDITOR_FORMAT),
},
{
label: 'sql_editor_shortcut_save_as_script',
code: transformKeys(KEY_BINDING_SQL_EDITOR_SAVE_AS_SCRIPT),
},
{
label: 'sql_editor_shortcut_undo',
code: transformKeys(KEY_BINDING_UNDO),
@@ -1,3 +1,10 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
export default [
['shortcuts_title', 'Shortcuts'],
@@ -14,6 +21,7 @@ export default [
['sql_editor_shortcut_execute_script', 'Execute script'],
['sql_editor_shortcut_show_execution_plan', 'Show Execution plan'],
['sql_editor_shortcut_format', 'Format script'],
['sql_editor_shortcut_save_as_script', 'Save as script'],
['sql_editor_shortcut_open_editor_in_new_tab', 'Open SQL Editor in the separate browser Tab'],
['sql_editor_shortcut_undo', 'Undo'],
['sql_editor_shortcut_redo', 'Redo'],
@@ -19,6 +19,7 @@ export default [
['sql_editor_shortcut_execute_script', 'Exécuter le script'],
['sql_editor_shortcut_show_execution_plan', "Afficher le plan d'exécution"],
['sql_editor_shortcut_format', 'Formater le script'],
['sql_editor_shortcut_save_as_script', 'Save as script'],
['sql_editor_shortcut_open_editor_in_new_tab', "Ouvrir l'éditeur SQL dans un nouvel onglet"],
['sql_editor_shortcut_undo', 'Annuler'],
['sql_editor_shortcut_redo', 'Rétablir'],
@@ -1,3 +1,10 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
export default [
['shortcuts_title', 'Shortcuts'],
@@ -14,6 +21,7 @@ export default [
['sql_editor_shortcut_execute_script', 'Execute script'],
['sql_editor_shortcut_show_execution_plan', 'Show Execution plan'],
['sql_editor_shortcut_format', 'Format script'],
['sql_editor_shortcut_save_as_script', 'Save as script'],
['sql_editor_shortcut_open_editor_in_new_tab', 'Open SQL Editor in the separate browser Tab'],
['sql_editor_shortcut_undo', 'Undo'],
['sql_editor_shortcut_redo', 'Redo'],
@@ -1,3 +1,10 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
export default [
['shortcuts_title', 'Горячие клавиши'],
@@ -14,6 +21,7 @@ export default [
['sql_editor_shortcut_execute_script', 'Выполнить скрипт'],
['sql_editor_shortcut_show_execution_plan', 'Показать план выполнения'],
['sql_editor_shortcut_format', 'Форматировать скрипт'],
['sql_editor_shortcut_save_as_script', 'Сохранить как скрипт'],
['sql_editor_shortcut_open_editor_in_new_tab', 'Открыть SQL редактор в новой бразуерной вкладке'],
['sql_editor_shortcut_undo', 'Отменить'],
['sql_editor_shortcut_redo', 'Повторить'],
@@ -21,6 +21,7 @@ export default [
['sql_editor_shortcut_execute_script', '执行脚本'],
['sql_editor_shortcut_show_execution_plan', '显示执行计划'],
['sql_editor_shortcut_format', '格式化脚本'],
['sql_editor_shortcut_save_as_script', 'Save as script'],
['sql_editor_shortcut_open_editor_in_new_tab', '在单独的浏览器标签下打开SQL编辑器'],
['sql_editor_shortcut_undo', '撤销'],
['sql_editor_shortcut_redo', '重做'],
@@ -45,6 +45,9 @@
{
"path": "../plugin-navigation-tree/tsconfig.json"
},
{
"path": "../plugin-sql-editor-navigation-tab-script/tsconfig.json"
},
{
"path": "../plugin-sql-editor/tsconfig.json"
},
@@ -20,6 +20,7 @@
"dependencies": {
"@cloudbeaver/core-blocks": "^0",
"@cloudbeaver/core-connections": "^0",
"@cloudbeaver/core-data-context": "^0",
"@cloudbeaver/core-di": "^0",
"@cloudbeaver/core-dialogs": "^0",
"@cloudbeaver/core-events": "^0",
@@ -0,0 +1,14 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { createKeyBinding } from '@cloudbeaver/core-view';
export const KEY_BINDING_SQL_EDITOR_SAVE_AS_SCRIPT = createKeyBinding({
id: 'save-as-script',
keys: 'shift+mod+s',
preventDefault: true,
});
@@ -5,6 +5,7 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import type { IDataContextProvider } from '@cloudbeaver/core-data-context';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
@@ -14,7 +15,7 @@ import { isResourceOfType, ProjectInfoResource, ProjectsService } from '@cloudbe
import { CachedMapAllKey, CachedTreeChildrenKey } from '@cloudbeaver/core-resource';
import { getRmResourcePath, NAV_NODE_TYPE_RM_RESOURCE, ResourceManagerResource, RESOURCES_NODE_PATH } from '@cloudbeaver/core-resource-manager';
import { createPath, getPathName } from '@cloudbeaver/core-utils';
import { ActionService, MenuService } from '@cloudbeaver/core-view';
import { ActionService, KeyBindingService, MenuService } from '@cloudbeaver/core-view';
import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs';
import { getResourceKeyFromNodeId } from '@cloudbeaver/plugin-navigation-tree-rm';
import { RESOURCE_NAME_REGEX, ResourceManagerService } from '@cloudbeaver/plugin-resource-manager';
@@ -29,10 +30,12 @@ import {
SQL_EDITOR_TOOLS_MENU,
SqlDataSourceService,
SqlEditorSettingsService,
SqlEditorView,
} from '@cloudbeaver/plugin-sql-editor';
import { isSQLEditorTab, SqlEditorNavigatorService } from '@cloudbeaver/plugin-sql-editor-navigation-tab';
import { ACTION_SAVE_AS_SCRIPT } from './ACTION_SAVE_AS_SCRIPT.js';
import { KEY_BINDING_SQL_EDITOR_SAVE_AS_SCRIPT } from './KEY_BINDING_SQL_EDITOR_SAVE_AS_SCRIPT.js';
import { ResourceSqlDataSource } from './ResourceSqlDataSource.js';
import { SqlEditorTabResourceService } from './SqlEditorTabResourceService.js';
@@ -55,8 +58,11 @@ export class PluginBootstrap extends Bootstrap {
private readonly sqlEditorSettingsService: SqlEditorSettingsService,
private readonly resourceManagerResource: ResourceManagerResource,
private readonly resourceManagerScriptsService: ResourceManagerScriptsService,
private readonly keyBindingService: KeyBindingService,
private readonly sqlEditorView: SqlEditorView,
) {
super();
this.saveAsScriptHandler = this.saveAsScriptHandler.bind(this);
}
override register(): void {
@@ -78,103 +84,9 @@ export class PluginBootstrap extends Bootstrap {
return dataSource instanceof MemorySqlDataSource || dataSource instanceof LocalStorageSqlDataSource;
},
handler: async (context, action) => {
const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!;
let dataSource: ISqlDataSource | ResourceSqlDataSource | undefined = this.sqlDataSourceService.get(state.editorId);
if (!dataSource) {
return;
}
handler: (context, action) => {
if (action === ACTION_SAVE_AS_SCRIPT) {
let projectId = dataSource.executionContext?.projectId ?? null;
await this.projectInfoResource.load(CachedMapAllKey);
const name = getSqlEditorName(state, dataSource);
if (projectId) {
const project = this.projectInfoResource.get(projectId);
if (!project?.canEditResources) {
projectId = null;
}
}
const result = await this.commonDialogService.open(SaveScriptDialog, {
defaultScriptName: name,
projectId,
validation: async ({ name, projectId }, setMessage) => {
const trimmedName = name.trim();
if (!projectId || !trimmedName.length) {
return false;
}
if (!RESOURCE_NAME_REGEX.test(trimmedName)) {
setMessage('plugin_resource_manager_scripts_script_name_invalid_characters_message');
return false;
}
const project = this.projectInfoResource.get(projectId);
const nameWithExtension = this.projectInfoResource.getNameWithExtension(projectId, SCRIPTS_TYPE_ID, trimmedName);
const rootFolder = project ? this.resourceManagerScriptsService.getRootFolder(project) : undefined;
const key = getRmResourcePath(projectId, rootFolder);
try {
await this.resourceManagerResource.load(CachedTreeChildrenKey(key));
return !this.resourceManagerResource.has(createPath(key, nameWithExtension));
} catch (exception: any) {
return false;
}
},
});
if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) {
try {
projectId = result.projectId;
if (!projectId) {
throw new Error('Project not selected');
}
const project = this.projectInfoResource.get(projectId);
if (!project) {
throw new Error('Project not found');
}
const nameWithoutExtension = result.name.trim();
const scriptName = this.projectInfoResource.getNameWithExtension(projectId, SCRIPTS_TYPE_ID, nameWithoutExtension);
const scriptsRootFolder = this.resourceManagerScriptsService.getRootFolder(project);
const folderResourceKey = getResourceKeyFromNodeId(createPath(RESOURCES_NODE_PATH, projectId, scriptsRootFolder));
if (!folderResourceKey) {
this.notificationService.logError({ title: 'ui_error', message: 'plugin_sql_editor_navigation_tab_resource_save_script_error' });
return;
}
const resourceKey = createPath(folderResourceKey, scriptName);
await this.resourceManagerScriptsService.createScript(resourceKey, dataSource.executionContext, dataSource.script);
dataSource = this.sqlDataSourceService.create(state, ResourceSqlDataSource.key, {
script: dataSource.script,
executionContext: dataSource.executionContext,
});
(dataSource as ResourceSqlDataSource).setResourceKey(resourceKey);
this.notificationService.logSuccess({
title: 'plugin_sql_editor_navigation_tab_resource_save_script_success',
message: nameWithoutExtension,
});
if (!this.resourceManagerScriptsService.active) {
this.resourceManagerScriptsService.togglePanel();
}
} catch (exception) {
this.notificationService.logException(exception as any, 'plugin_sql_editor_navigation_tab_resource_save_script_error');
}
}
this.saveAsScriptHandler(context);
}
},
getActionInfo: (context, action) => {
@@ -189,6 +101,8 @@ export class PluginBootstrap extends Bootstrap {
},
});
this.sqlEditorView.registerAction(ACTION_SAVE_AS_SCRIPT);
this.menuService.addCreator({
menus: [SQL_EDITOR_TOOLS_MENU],
contexts: [DATA_CONTEXT_SQL_EDITOR_STATE],
@@ -201,6 +115,113 @@ export class PluginBootstrap extends Bootstrap {
},
getItems: (context, items) => [...items, ACTION_SAVE_AS_SCRIPT],
});
this.keyBindingService.addKeyBindingHandler({
id: 'save-as-script',
binding: KEY_BINDING_SQL_EDITOR_SAVE_AS_SCRIPT,
actions: [ACTION_SAVE_AS_SCRIPT],
contexts: [DATA_CONTEXT_SQL_EDITOR_STATE],
isBindingApplicable: (_, action) => action === ACTION_SAVE_AS_SCRIPT,
handler: this.saveAsScriptHandler.bind(this),
});
}
private async saveAsScriptHandler(context: IDataContextProvider) {
const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!;
let dataSource: ISqlDataSource | ResourceSqlDataSource | undefined = this.sqlDataSourceService.get(state.editorId);
if (!dataSource) {
return;
}
let projectId = dataSource.executionContext?.projectId ?? null;
await this.projectInfoResource.load(CachedMapAllKey);
const name = getSqlEditorName(state, dataSource);
if (projectId) {
const project = this.projectInfoResource.get(projectId);
if (!project?.canEditResources) {
projectId = null;
}
}
const result = await this.commonDialogService.open(SaveScriptDialog, {
defaultScriptName: name,
projectId,
validation: async ({ name, projectId }, setMessage) => {
const trimmedName = name.trim();
if (!projectId || !trimmedName.length) {
return false;
}
if (!RESOURCE_NAME_REGEX.test(trimmedName)) {
setMessage('plugin_resource_manager_scripts_script_name_invalid_characters_message');
return false;
}
const project = this.projectInfoResource.get(projectId);
const nameWithExtension = this.projectInfoResource.getNameWithExtension(projectId, SCRIPTS_TYPE_ID, trimmedName);
const rootFolder = project ? this.resourceManagerScriptsService.getRootFolder(project) : undefined;
const key = getRmResourcePath(projectId, rootFolder);
try {
await this.resourceManagerResource.load(CachedTreeChildrenKey(key));
return !this.resourceManagerResource.has(createPath(key, nameWithExtension));
} catch (exception: any) {
return false;
}
},
});
if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) {
try {
projectId = result.projectId;
if (!projectId) {
throw new Error('Project not selected');
}
const project = this.projectInfoResource.get(projectId);
if (!project) {
throw new Error('Project not found');
}
const nameWithoutExtension = result.name.trim();
const scriptName = this.projectInfoResource.getNameWithExtension(projectId, SCRIPTS_TYPE_ID, nameWithoutExtension);
const scriptsRootFolder = this.resourceManagerScriptsService.getRootFolder(project);
const folderResourceKey = getResourceKeyFromNodeId(createPath(RESOURCES_NODE_PATH, projectId, scriptsRootFolder));
if (!folderResourceKey) {
this.notificationService.logError({ title: 'ui_error', message: 'plugin_sql_editor_navigation_tab_resource_save_script_error' });
return;
}
const resourceKey = createPath(folderResourceKey, scriptName);
await this.resourceManagerScriptsService.createScript(resourceKey, dataSource.executionContext, dataSource.script);
dataSource = this.sqlDataSourceService.create(state, ResourceSqlDataSource.key, {
script: dataSource.script,
executionContext: dataSource.executionContext,
});
(dataSource as ResourceSqlDataSource).setResourceKey(resourceKey);
this.notificationService.logSuccess({
title: 'plugin_sql_editor_navigation_tab_resource_save_script_success',
message: nameWithoutExtension,
});
if (!this.resourceManagerScriptsService.active) {
this.resourceManagerScriptsService.togglePanel();
}
} catch (exception) {
this.notificationService.logException(exception as any, 'plugin_sql_editor_navigation_tab_resource_save_script_error');
}
}
}
private canOpenHandler(data: INodeNavigationData, contexts: IExecutionContextProvider<INodeNavigationData>): void {
@@ -7,4 +7,6 @@
*/
import { manifest } from './manifest.js';
export * from './KEY_BINDING_SQL_EDITOR_SAVE_AS_SCRIPT.js';
export default manifest;
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
export default [
['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Save as script'],
['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Save as script (Shift + Ctrl + S)'],
['plugin_sql_editor_navigation_tab_script_state_renaming', 'Renaming script...'],
['plugin_sql_editor_navigation_tab_script_state_reading', 'Reading script...'],
['plugin_sql_editor_navigation_tab_script_state_saving', 'Saving script...'],
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
export default [
['plugin_sql_editor_navigation_tab_resource_save_title', 'Enregistrer comme script'],
['plugin_sql_editor_navigation_tab_resource_save_title', 'Enregistrer comme script (Shift + Ctrl + S)'],
['plugin_sql_editor_navigation_tab_script_state_renaming', 'Renommer le script...'],
['plugin_sql_editor_navigation_tab_script_state_reading', 'Lecture du script...'],
['plugin_sql_editor_navigation_tab_script_state_saving', 'Enregistrement du script...'],
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
export default [
['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Save as script'],
['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Save as script (Shift + Ctrl + S)'],
['plugin_sql_editor_navigation_tab_script_state_renaming', 'Renaming script...'],
['plugin_sql_editor_navigation_tab_script_state_reading', 'Reading script...'],
['plugin_sql_editor_navigation_tab_script_state_saving', 'Saving script...'],
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
export default [
['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Сохранить как скрипт'],
['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Сохранить как скрипт (Shift + Ctrl + S)'],
['plugin_sql_editor_navigation_tab_script_state_renaming', 'Переименовывание скрипта...'],
['plugin_sql_editor_navigation_tab_script_state_reading', 'Чтение скрипта...'],
['plugin_sql_editor_navigation_tab_script_state_saving', 'Сохранение скрипта...'],
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
export default [
['plugin_sql_editor_navigation_tab_resource_save_script_title', '保存为脚本'],
['plugin_sql_editor_navigation_tab_resource_save_script_title', '保存为脚本 (Shift + Ctrl + S)'],
['plugin_sql_editor_navigation_tab_script_state_renaming', '重命名脚本中...'],
['plugin_sql_editor_navigation_tab_script_state_reading', '读取脚本中...'],
['plugin_sql_editor_navigation_tab_script_state_saving', '保存脚本中...'],
@@ -12,6 +12,9 @@
{
"path": "../core-connections/tsconfig.json"
},
{
"path": "../core-data-context/tsconfig.json"
},
{
"path": "../core-di/tsconfig.json"
},
@@ -47,5 +47,6 @@ export * from './SQLEditorLoader.js';
export * from './SqlEditorModeService.js';
export * from './SqlEditorService.js';
export * from './SqlEditorSettingsService.js';
export * from './SqlEditorView.js';
export default sqlEditorPluginManifest;