diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebConnectionFolderUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebConnectionFolderUtils.java index c95ebd05d3..0ddb8dda88 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebConnectionFolderUtils.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebConnectionFolderUtils.java @@ -21,6 +21,10 @@ import io.cloudbeaver.model.WebConnectionFolderInfo; import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.model.DBPDataSourceFolder; +import org.jkiss.dbeaver.model.navigator.DBNLocalFolder; +import org.jkiss.dbeaver.model.navigator.DBNNode; +import org.jkiss.dbeaver.model.navigator.DBNProject; +import org.jkiss.dbeaver.model.navigator.DBNRoot; import org.jkiss.utils.CommonUtils; public class WebConnectionFolderUtils { @@ -44,4 +48,14 @@ public class WebConnectionFolderUtils { throw new DBWebException("Folder name '" + folderName + "' contains illegal characters: /"); } } + + public static DBPDataSourceFolder getParentFolder(DBNNode folderNode) throws DBWebException { + if (folderNode instanceof DBNRoot || folderNode instanceof DBNProject) { + return null; + } else if (folderNode instanceof DBNLocalFolder) { + return ((DBNLocalFolder) folderNode).getFolder(); + } else { + throw new DBWebException("Navigator node '" + folderNode.getNodeItemPath() + "' is not a folder node"); + } + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java index 38d127ed38..5b01f9b60d 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java @@ -466,17 +466,22 @@ public class WebServiceNavigator implements DBWServiceNavigator { } checkProjectEditAccess(node, session); if (node instanceof DBNDataSource) { - DBPDataSourceFolder folder; - if (folderNode instanceof DBNRoot || folderNode instanceof DBNProject) { - folder = null; - } else if (folderNode instanceof DBNLocalFolder) { - folder = ((DBNLocalFolder) folderNode).getFolder(); - } else { - throw new DBWebException("Navigator node '" + folderNodePath + "' is not a folder node"); - } + DBPDataSourceFolder folder = WebConnectionFolderUtils.getParentFolder(folderNode); ((DBNDataSource) node).moveToFolder(folderNode.getOwnerProject(), folder); node.getOwnerProject().getDataSourceRegistry().updateDataSource( ((DBNDataSource) node).getDataSourceContainer()); + } else if (node instanceof DBNLocalFolder) { + DBPDataSourceFolder folder = WebConnectionFolderUtils.getParentFolder(folderNode); + if (folder != null) { + List siblings = Arrays.stream(folder.getChildren()) + .map(DBPDataSourceFolder::getName) + .collect(Collectors.toList()); + if (siblings.contains(node.getName())) { + throw new DBWebException("Node " + folderNodePath + " contains folder with name '" + node.getName() + "'"); + } + } + ((DBNLocalFolder) node).getFolder().setParent(folder); + WebServiceUtils.updateConfigAndRefreshDatabases(session, node.getOwnerProject().getId()); } else if (node instanceof DBNResourceManagerResource) { boolean rmNewNode = folderNode instanceof DBNAbstractResourceManagerNode; DBNResourceManagerResource rmOldNode = (DBNResourceManagerResource) node; diff --git a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts index dca837972e..5f1b4513b3 100644 --- a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts +++ b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts @@ -155,22 +155,30 @@ export class ConnectionFoldersBootstrap extends Bootstrap { }: INodeMoveData, contexts: IExecutionContextProvider ) { + if (![NAV_NODE_TYPE_PROJECT, NAV_NODE_TYPE_FOLDER].includes(targetNode.nodeType!)) { + return; + } + await this.projectsResource.load(CachedMapAllKey); const move = contexts.getContext(navNodeMoveContext); const nodes = getNodesFromContext(moveContexts); const nodeIdList = nodes.map(node => node.id); - const children = this.navTreeResource.get(targetNode.id); + const children = this.navTreeResource.get(targetNode.id) ?? []; const targetProject = this.projectsNavNodeService.getProject(targetNode.id); - const supported = ( - [NAV_NODE_TYPE_PROJECT, NAV_NODE_TYPE_FOLDER].includes(targetNode.nodeType!) - && nodes.every(node => ( - node.nodeType === NAV_NODE_TYPE_CONNECTION - && targetProject === this.projectsNavNodeService.getProject(node.id) - && !children?.includes(node.id) - )) - ); + const supported = nodes.every(node => { + if ( + ![NAV_NODE_TYPE_CONNECTION, NAV_NODE_TYPE_FOLDER, NAV_NODE_TYPE_PROJECT].includes(node.nodeType!) + || targetProject !== this.projectsNavNodeService.getProject(node.id) + || children.includes(node.id) + || targetNode.id === node.id + ) { + return false; + } + + return true; + }); if (!supported) { return; @@ -181,6 +189,31 @@ export class ConnectionFoldersBootstrap extends Bootstrap { move.setCanMove(true); } } else { + const childrenNode = this.navNodeInfoResource.get(resourceKeyList(children)); + const folderDuplicates = nodes.filter(node => ( + node.nodeType === NAV_NODE_TYPE_FOLDER + && ( + childrenNode.some(child => child?.nodeType === NAV_NODE_TYPE_FOLDER && child.name === node.name) + || nodes.some(child => ( + child.nodeType === NAV_NODE_TYPE_FOLDER + && child.name === node.name + && child.id !== node.id + )) + ) + )); + + if (folderDuplicates.length > 0) { + this.notificationService.logError({ + title: 'connections_public_connection_folder_move_failed', + message: this.localizationService.translate( + 'connections_public_connection_folder_move_duplication', + undefined, + { name: folderDuplicates.map(node => `"${node.name}"`).join(', ') } + ), + }); + return; + } + try { await this.navTreeResource.moveTo(resourceKeyList(nodeIdList), targetNode.id); const connections = nodeIdList diff --git a/webapp/packages/plugin-connections/src/locales/en.ts b/webapp/packages/plugin-connections/src/locales/en.ts index 7586edd8c0..35e4d191d4 100644 --- a/webapp/packages/plugin-connections/src/locales/en.ts +++ b/webapp/packages/plugin-connections/src/locales/en.ts @@ -6,5 +6,6 @@ export default [ ['connections_public_connection_edit_reconnect_message', 'Connection has been updated. Do you want to reconnect?'], ['connections_public_connection_edit_reconnect_failed', 'Failed to reconnect'], ['connections_public_connection_folder_move_failed', 'Failed to move to folder'], + ['connections_public_connection_folder_move_duplication', 'Target folder or selected folders contains folder with the same name ({args.name})'], ['connections_public_connection_folder_delete_confirmation', 'You\'re going to delete "{args.name}". Connections won\'t be deleted. Are you sure?'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/it.ts b/webapp/packages/plugin-connections/src/locales/it.ts index fc4b70d326..a250f28a29 100644 --- a/webapp/packages/plugin-connections/src/locales/it.ts +++ b/webapp/packages/plugin-connections/src/locales/it.ts @@ -6,5 +6,6 @@ export default [ ['connections_public_connection_edit_reconnect_message', 'Connection has been updated. Do you want to reconnect?'], ['connections_public_connection_edit_reconnect_failed', 'Failed to reconnect'], ['connections_public_connection_folder_move_failed', 'Failed to move to folder'], + ['connections_public_connection_folder_move_duplication', 'Target folder or selected folders contains folder with the same name ({args.name})'], ['connections_public_connection_folder_delete_confirmation', 'You\'re going to delete "{args.name}". Connections won\'t be deleted. Are you sure?'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/ru.ts b/webapp/packages/plugin-connections/src/locales/ru.ts index 1976281521..7595609214 100644 --- a/webapp/packages/plugin-connections/src/locales/ru.ts +++ b/webapp/packages/plugin-connections/src/locales/ru.ts @@ -6,5 +6,6 @@ export default [ ['connections_public_connection_edit_reconnect_message', 'Подключение было обновлено. Вы хотите переподключиться?'], ['connections_public_connection_edit_reconnect_failed', 'Не удалось переподключиться'], ['connections_public_connection_folder_move_failed', 'Ошибка перемещения в папку'], + ['connections_public_connection_folder_move_duplication', 'Выбранные папки или папка назначения содержит папки с таким же названием ({args.name})'], ['connections_public_connection_folder_delete_confirmation', 'Вы удаляете "{args.name}". Подключения не будут удалены. Вы уверены?'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/zh.ts b/webapp/packages/plugin-connections/src/locales/zh.ts index 5576999d6b..50053ac465 100644 --- a/webapp/packages/plugin-connections/src/locales/zh.ts +++ b/webapp/packages/plugin-connections/src/locales/zh.ts @@ -6,5 +6,6 @@ export default [ ['connections_public_connection_edit_reconnect_message', '连接已更新。您想重新连接吗?'], ['connections_public_connection_edit_reconnect_failed', '重新连接失败'], ['connections_public_connection_folder_move_failed', 'Failed to move to folder'], + ['connections_public_connection_folder_move_duplication', 'Target folder or selected folders contains folder with the same name ({args.name})'], ['connections_public_connection_folder_delete_confirmation', 'You\'re going to delete "{args.name}". Connections won\'t be deleted. Are you sure?'], ];