diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/INodeActions.ts b/webapp/packages/core-navigation-tree/src/NodesManager/INodeActions.ts index 14f509f186..6e1b8140b7 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/INodeActions.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/INodeActions.ts @@ -7,5 +7,5 @@ */ export interface INodeActions { - readonly rename?: () => void; + readonly rename?: (saveCallback: (name: string) => Promise) => void; } diff --git a/webapp/packages/core-resource-manager/src/ResourceManagerResource.ts b/webapp/packages/core-resource-manager/src/ResourceManagerResource.ts index 332b1a4170..f000c3decc 100644 --- a/webapp/packages/core-resource-manager/src/ResourceManagerResource.ts +++ b/webapp/packages/core-resource-manager/src/ResourceManagerResource.ts @@ -62,16 +62,12 @@ export class ResourceManagerResource extends CachedTreeResource { const fromResourceKey = getRmResourceKey(from); const toResourceKey = getRmResourceKey(to); + await this.performUpdate(from, undefined, async () => { await this.graphQLService.sdk.moveResource({ projectId: fromResourceKey.projectId, diff --git a/webapp/packages/core-sdk/src/Resource/CachedResource.ts b/webapp/packages/core-sdk/src/Resource/CachedResource.ts index 4e6d54ffb2..842ec783dc 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedResource.ts @@ -840,9 +840,10 @@ export abstract class CachedResource< } protected markOutdatedSync(key: ResourceKey): void { - if (!this.hasMetadata(key)) { - return; - } + // Commented because it can lead to skipping existing keys outdate if some of them doesn't exists + // if (!this.hasMetadata(key)) { + // return; + // } this.updateMetadata(key, metadata => { metadata.outdated = true; }); @@ -937,15 +938,19 @@ export abstract class CachedResource< } }, { - before: () => { - this.markOutdatedSync(key); - }, success: () => { if (loaded) { + this.onDataOutdated.execute(key); // TODO: probably need to remove, we need to notify any related resources that subscribed to .onOutdate, to recursively outdate them this.dataUpdate(key); } }, - error: exception => this.markError(exception, key, include), + error: exception => { + this.markOutdatedSync(key); + this.markError(exception, key, include); + }, + after: () => { + this.flushOutdatedWaitList(); + }, }, ); } @@ -1005,6 +1010,9 @@ export abstract class CachedResource< } }, error: exception => this.markError(exception, key, include), + after: () => { + this.flushOutdatedWaitList(); + }, }, ); } @@ -1025,17 +1033,15 @@ export abstract class CachedResource< const value = await promise(param, context, refresh); this.markUpdated(param); + return value; + } + private flushOutdatedWaitList(): void { for (let i = 0; i < this.outdateWaitList.length; i++) { const key = this.outdateWaitList[i]; - // if (this.includes(param, key)) { this.markOutdatedSync(key); - // this.outdateWaitList.splice(i, 1); - // break; - // } } this.outdateWaitList = []; - return value; } public getName(): string { diff --git a/webapp/packages/core-ui/src/InlineEditor/InlineEditor.tsx b/webapp/packages/core-ui/src/InlineEditor/InlineEditor.tsx index 38bf61e298..dfb1af5ce6 100644 --- a/webapp/packages/core-ui/src/InlineEditor/InlineEditor.tsx +++ b/webapp/packages/core-ui/src/InlineEditor/InlineEditor.tsx @@ -106,10 +106,10 @@ export const InlineEditor = observer( const inputRef = useRef(null); useEffect(() => { - if (autofocus) { + if (autofocus && !disabled) { setTimeout(() => inputRef.current?.focus(), 100); } - }, []); + }, [disabled]); useImperativeHandle(ref, () => inputRef.current!); diff --git a/webapp/packages/plugin-navigation-tree-rm/.gitignore b/webapp/packages/plugin-navigation-tree-rm/.gitignore new file mode 100644 index 0000000000..15bc16c7c3 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree-rm/.gitignore @@ -0,0 +1,17 @@ +# dependencies +/node_modules + +# testing +/coverage + +# production +/lib + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/plugin-navigation-tree-rm/package.json b/webapp/packages/plugin-navigation-tree-rm/package.json new file mode 100644 index 0000000000..0394d3f289 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree-rm/package.json @@ -0,0 +1,42 @@ +{ + "name": "@cloudbeaver/plugin-navigation-tree-rm", + "sideEffects": false, + "version": "0.1.0", + "description": "", + "license": "Apache-2.0", + "main": "lib/index.js", + "module": "lib/index.js", + "types": "src/index.ts", + "scripts": { + "prebuild": "rimraf --glob lib/*", + "build": "webpack --config ../../configs/webpack.plugin.config.js", + "postbuild": "tsc", + "lint": "eslint ./src/ --ext .ts,.tsx", + "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", + "test": "core-cli-test", + "validate-dependencies": "core-cli-validate-dependencies" + }, + "dependencies": { + "@cloudbeaver/plugin-resource-manager": "~0.1.0", + "@cloudbeaver/plugin-navigation-tree": "~0.1.0", + "@cloudbeaver/core-di": "~0.1.0", + "@cloudbeaver/core-dialogs": "~0.1.0", + "@cloudbeaver/core-events": "~0.1.0", + "@cloudbeaver/core-executor": "~0.1.0", + "@cloudbeaver/core-localization": "~0.1.0", + "@cloudbeaver/core-navigation-tree": "~0.1.0", + "@cloudbeaver/core-projects": "~0.1.0", + "@cloudbeaver/core-root": "~0.1.0", + "@cloudbeaver/core-sdk": "~0.1.0", + "@cloudbeaver/core-ui": "~0.1.0", + "@cloudbeaver/core-utils": "~0.1.0", + "@cloudbeaver/core-view": "~0.1.0" + }, + "peerDependencies": { + "react": "~18.x.x", + "mobx": "~6.x.x", + "mobx-react-lite": "~3.x.x", + "reshadow": "~0.x.x", + "@testing-library/jest-dom": "~5.x.x" + } +} diff --git a/webapp/packages/plugin-navigation-tree-rm/src/LocaleService.ts b/webapp/packages/plugin-navigation-tree-rm/src/LocaleService.ts new file mode 100644 index 0000000000..e3649a06b4 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree-rm/src/LocaleService.ts @@ -0,0 +1,35 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { LocalizationService } from '@cloudbeaver/core-localization'; + +@injectable() +export class LocaleService extends Bootstrap { + constructor(private readonly localizationService: LocalizationService) { + super(); + } + + register(): void | Promise { + this.localizationService.addProvider(this.provider.bind(this)); + } + + load(): void | Promise {} + + private async provider(locale: string) { + switch (locale) { + case 'ru': + return (await import('./locales/ru')).default; + case 'it': + return (await import('./locales/it')).default; + case 'zh': + return (await import('./locales/zh')).default; + default: + return (await import('./locales/en')).default; + } + } +} diff --git a/webapp/packages/plugin-navigation-tree-rm/src/NavTreeRMContextMenuService.ts b/webapp/packages/plugin-navigation-tree-rm/src/NavTreeRMContextMenuService.ts new file mode 100644 index 0000000000..2aa8e52e10 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree-rm/src/NavTreeRMContextMenuService.ts @@ -0,0 +1,108 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { CommonDialogService, DialogueStateResult, RenameDialog } from '@cloudbeaver/core-dialogs'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { LocalizationService } from '@cloudbeaver/core-localization'; +import { DATA_CONTEXT_NAV_NODE } from '@cloudbeaver/core-navigation-tree'; +import { getRmResourceKey, NAV_NODE_TYPE_RM_FOLDER, NAV_NODE_TYPE_RM_RESOURCE } from '@cloudbeaver/core-resource-manager'; +import { createPath, getPathParent } from '@cloudbeaver/core-utils'; +import { ACTION_DELETE, ACTION_RENAME, ActionService } from '@cloudbeaver/core-view'; +import { DATA_CONTEXT_NAV_NODE_ACTIONS } from '@cloudbeaver/plugin-navigation-tree'; +import { getResourceKeyFromNodeId, NavResourceNodeService } from '@cloudbeaver/plugin-resource-manager'; + +@injectable() +export class NavTreeRMContextMenuService extends Bootstrap { + constructor( + private readonly actionService: ActionService, + private readonly commonDialogService: CommonDialogService, + private readonly notificationService: NotificationService, + private readonly navResourceNodeService: NavResourceNodeService, + private readonly localizationService: LocalizationService, + ) { + super(); + } + + register(): void { + this.actionService.addHandler({ + id: 'nav-node-rm-handler', + isActionApplicable: (context, action): boolean => { + const node = context.tryGet(DATA_CONTEXT_NAV_NODE); + + if (!node) { + return false; + } + + if (![NAV_NODE_TYPE_RM_RESOURCE, NAV_NODE_TYPE_RM_FOLDER].includes(node.nodeType as string)) { + return false; + } + + return [ACTION_RENAME, ACTION_DELETE].includes(action); + }, + handler: async (context, action) => { + const node = context.get(DATA_CONTEXT_NAV_NODE); + const resourceKey = getResourceKeyFromNodeId(node.id); + + if (!resourceKey) { + return; + } + + const key = getRmResourceKey(resourceKey); + + switch (action) { + case ACTION_RENAME: { + const actions = context.tryGet(DATA_CONTEXT_NAV_NODE_ACTIONS); + + const save = async (newName: string) => { + if (key.name !== newName && newName.trim().length) { + try { + await this.navResourceNodeService.move(resourceKey, createPath(getPathParent(resourceKey), newName)); + node.name = newName; // fix name flickering in tree + } catch (exception: any) { + this.notificationService.logException(exception, 'app_navigationTree_node_rename_error'); + return false; + } + } + return true; + }; + + if (actions?.rename) { + actions.rename(save); + } else { + const result = await this.commonDialogService.open(RenameDialog, { + value: key.name ?? '', + subTitle: key.name, + objectName: node.nodeType || 'Object', + icon: node.icon, + validation: name => name.trim().length > 0, + }); + + if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) { + save(result); + } + } + break; + } + case ACTION_DELETE: { + try { + await this.navResourceNodeService.delete(resourceKey); + } catch (exception: any) { + this.notificationService.logException( + exception, + this.localizationService.translate('app_navigationTree_node_delete_error', undefined, { name: key.name }), + ); + } + break; + } + } + }, + }); + } + + load(): void {} +} diff --git a/webapp/packages/plugin-navigation-tree-rm/src/index.ts b/webapp/packages/plugin-navigation-tree-rm/src/index.ts new file mode 100644 index 0000000000..fe9636e353 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree-rm/src/index.ts @@ -0,0 +1,4 @@ +import { navigationTreeRMPlugin } from './manifest'; + +export { navigationTreeRMPlugin }; +export default navigationTreeRMPlugin; diff --git a/webapp/packages/plugin-navigation-tree-rm/src/locales/en.ts b/webapp/packages/plugin-navigation-tree-rm/src/locales/en.ts new file mode 100644 index 0000000000..d6d1738de6 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree-rm/src/locales/en.ts @@ -0,0 +1 @@ +export default []; diff --git a/webapp/packages/plugin-navigation-tree-rm/src/locales/it.ts b/webapp/packages/plugin-navigation-tree-rm/src/locales/it.ts new file mode 100644 index 0000000000..d6d1738de6 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree-rm/src/locales/it.ts @@ -0,0 +1 @@ +export default []; diff --git a/webapp/packages/plugin-navigation-tree-rm/src/locales/ru.ts b/webapp/packages/plugin-navigation-tree-rm/src/locales/ru.ts new file mode 100644 index 0000000000..d6d1738de6 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree-rm/src/locales/ru.ts @@ -0,0 +1 @@ +export default []; diff --git a/webapp/packages/plugin-navigation-tree-rm/src/locales/zh.ts b/webapp/packages/plugin-navigation-tree-rm/src/locales/zh.ts new file mode 100644 index 0000000000..d6d1738de6 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree-rm/src/locales/zh.ts @@ -0,0 +1 @@ +export default []; diff --git a/webapp/packages/plugin-navigation-tree-rm/src/manifest.ts b/webapp/packages/plugin-navigation-tree-rm/src/manifest.ts new file mode 100644 index 0000000000..5923b9fc30 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree-rm/src/manifest.ts @@ -0,0 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 type { PluginManifest } from '@cloudbeaver/core-di'; + +import { LocaleService } from './LocaleService'; +import { NavTreeRMContextMenuService } from './NavTreeRMContextMenuService'; + +export const navigationTreeRMPlugin: PluginManifest = { + info: { name: 'Navigation Tree RM plugin' }, + providers: [LocaleService, NavTreeRMContextMenuService], +}; diff --git a/webapp/packages/plugin-navigation-tree-rm/tsconfig.json b/webapp/packages/plugin-navigation-tree-rm/tsconfig.json new file mode 100644 index 0000000000..e3af542617 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree-rm/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib" + }, + "include": ["src"], + "references": [{ "path": "../*" }] +} diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx index 37327ef165..5664da5c1c 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx @@ -5,8 +5,9 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { action, observable } from 'mobx'; import { observer } from 'mobx-react-lite'; -import React, { forwardRef, useContext, useDeferredValue, useState } from 'react'; +import React, { forwardRef, useContext, useDeferredValue } from 'react'; import styled, { css, use } from 'reshadow'; import { @@ -21,6 +22,7 @@ import { TreeNodeName, useMouseContextMenu, useObjectRef, + useObservableRef, } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events'; @@ -71,6 +73,20 @@ const styles = css` } `; +interface IEditingState { + saving: boolean; + editing: boolean; + + resolve: (value: string) => Promise; + + startEditing(resolve: (value: string) => Promise): void; + + save(name: string): void; + setSaveStatus(saving: boolean): void; + finish(): void; + cancel(): void; +} + export const NavigationNodeControl: NavTreeControlComponent = observer( forwardRef(function NavigationNodeControl({ node, dndElement, dndPlaceholder }, ref) { const mouseContextMenu = useMouseContextMenu(); @@ -83,12 +99,56 @@ export const NavigationNodeControl: NavTreeControlComponent = observer node.objectFeatures.includes(EObjectFeature.dataSourceConnected)); const selected = treeNodeContext.selected; - const [editing, setEditing] = useState(false); + const editingState = useObservableRef( + () => ({ + saving: false, + editing: false, + resolve: () => Promise.resolve(false), + + startEditing(resolve) { + this.editing = true; + this.saving = false; + this.resolve = resolve; + }, + + async save(name: string) { + this.setSaveStatus(true); + try { + const saved = await this.resolve(name); + + if (saved) { + this.finish(); + } + } finally { + this.setSaveStatus(false); + } + }, + setSaveStatus(saving: boolean) { + this.saving = saving; + }, + finish() { + this.editing = false; + this.saving = false; + this.resolve = () => Promise.resolve(false); + }, + cancel() { + this.finish(); + }, + }), + { + saving: observable.ref, + editing: observable.ref, + startEditing: action.bound, + save: action.bound, + setSaveStatus: action.bound, + finish: action.bound, + cancel: action.bound, + }, + false, + ); const nodeActions = useObjectRef({ - rename: () => { - setEditing(true); - }, + rename: editingState.startEditing, }); let icon = node.icon; @@ -113,6 +173,7 @@ export const NavigationNodeControl: NavTreeControlComponent = observer treeContext?.tree.isNodeExpandable(node.id) ?? true)); const filterActive = useDeferredValue(getComputed(() => treeContext?.tree.filtering)); + const { editing, saving } = editingState; const attributes = { [DATA_ATTRIBUTE_NODE_EDITING]: editing }; @@ -133,7 +194,11 @@ export const NavigationNodeControl: NavTreeControlComponent = observer - {editing ? setEditing(false)} /> : {node.name}} + {editing ? ( + + ) : ( + {node.name} + )} {!editing && !dndPlaceholder && ( diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor.tsx index ae11996402..e6f1517850 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor.tsx @@ -9,9 +9,8 @@ import { observer } from 'mobx-react-lite'; import { useState } from 'react'; import styled, { css } from 'reshadow'; -import { useService } from '@cloudbeaver/core-di'; -import { EventContext, EventStopPropagationFlag, NotificationService } from '@cloudbeaver/core-events'; -import { type NavNode, NavTreeResource } from '@cloudbeaver/core-navigation-tree'; +import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events'; +import type { NavNode } from '@cloudbeaver/core-navigation-tree'; import { InlineEditor } from '@cloudbeaver/core-ui'; const styles = css` @@ -26,32 +25,16 @@ const styles = css` interface Props { node: NavNode; + disabled?: boolean; + onSave: (name: string) => void; onClose: () => void; } -export const NavigationNodeEditor = observer(function NavigationNodeEditor({ node, onClose }) { - const navTreeResource = useService(NavTreeResource); - const notificationService = useService(NotificationService); - - const [loading, setLoading] = useState(false); +export const NavigationNodeEditor = observer(function NavigationNodeEditor({ node, disabled, onSave, onClose }) { const [name, setName] = useState(node.name || ''); - async function save() { - if (loading) { - return; - } - - try { - if (node.name !== name && name.trim().length) { - setLoading(true); - await navTreeResource.changeName(node, name); - } - } catch (exception: any) { - notificationService.logException(exception, 'app_navigationTree_node_change_name_error'); - } finally { - setLoading(false); - onClose(); - } + function save() { + onSave(name); } function stopPropagation(event: React.MouseEvent) { @@ -61,7 +44,7 @@ export const NavigationNodeEditor = observer(function NavigationNodeEdito return styled(styles)( navNodeInfoResource.isOutdated(node.id) && !treeNodeContext.loading); const selected = treeNodeContext.selected; - const [editing, setEditing] = useState(false); - - const nodeActions = useObjectRef({ - rename: () => { - setEditing(true); - }, - }); - const isDragging = getComputed(() => { if (!node.projectId || !elementsTreeContext?.tree.activeDnDData) { return false; @@ -118,15 +101,15 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer - {editing ? setEditing(false)} /> : {node.name}} + {node.name} - {!editing && !dndPlaceholder && ( + {!dndPlaceholder && ( - + )} , diff --git a/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeContextMenuService.ts b/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeContextMenuService.ts index 1d799b09d8..9d6421b1c8 100644 --- a/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeContextMenuService.ts +++ b/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeContextMenuService.ts @@ -5,8 +5,6 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { untracked } from 'mobx'; - import { CoreSettingsService } from '@cloudbeaver/core-app'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult, RenameDialog } from '@cloudbeaver/core-dialogs'; @@ -18,12 +16,14 @@ import { ENodeFeature, getNodeDisplayName, type INodeActions, + NAV_NODE_TYPE_FOLDER, type NavNode, NavNodeInfoResource, NavNodeManagerService, NavTreeResource, NavTreeSettingsService, nodeDeleteContext, + NodeManagerUtils, } from '@cloudbeaver/core-navigation-tree'; import { ResourceKeyUtils } from '@cloudbeaver/core-sdk'; import { @@ -105,28 +105,14 @@ export class NavNodeContextMenuService extends Bootstrap { return false; } - if (action === ACTION_RENAME) { - const globalPermission = this.navTreeSettingsService.settings.isValueDefault('editing') - ? this.coreSettingsService.settings.getValue('app.metadata.editing') - : this.navTreeSettingsService.settings.getValue('editing'); - - if (!globalPermission || !node.features) { - return false; + if (NodeManagerUtils.isDatabaseObject(node.id) || node.nodeType === NAV_NODE_TYPE_FOLDER) { + if (action === ACTION_RENAME) { + return node.features?.includes(ENodeFeature.canRename) ?? false; } - return node.features.includes(ENodeFeature.canRename); - } - - if (action === ACTION_DELETE) { - const globalPermission = this.navTreeSettingsService.settings.isValueDefault('deleting') - ? this.coreSettingsService.settings.getValue('app.metadata.deleting') - : this.navTreeSettingsService.settings.getValue('deleting'); - - if (!globalPermission || !node.features) { - return false; + if (action === ACTION_DELETE) { + return node.features?.includes(ENodeFeature.canDelete) ?? false; } - - return node.features.includes(ENodeFeature.canDelete); } if (action === ACTION_OPEN) { @@ -155,8 +141,20 @@ export class NavNodeContextMenuService extends Bootstrap { case ACTION_RENAME: { const actions = context.tryGet(DATA_CONTEXT_NAV_NODE_ACTIONS); + const save = async (newName: string) => { + if (name !== newName && newName.trim().length) { + try { + await this.navTreeResource.changeName(node, newName); + } catch (exception: any) { + this.notificationService.logException(exception, 'app_navigationTree_node_rename_error'); + return false; + } + } + return true; + }; + if (actions?.rename) { - actions.rename(); + actions.rename(save); } else { const result = await this.commonDialogService.open(RenameDialog, { value: name, @@ -167,13 +165,7 @@ export class NavNodeContextMenuService extends Bootstrap { }); if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) { - if (name !== result && result.trim().length) { - try { - await this.navTreeResource.changeName(node, result); - } catch (exception: any) { - this.notificationService.logException(exception, 'app_navigationTree_node_rename_error'); - } - } + save(result); } } break; @@ -195,17 +187,36 @@ export class NavNodeContextMenuService extends Bootstrap { this.menuService.addCreator({ isApplicable: context => context.has(DATA_CONTEXT_NAV_NODE) && !context.has(DATA_CONTEXT_MENU_NESTED), - getItems: (context, items) => [...items, ACTION_OPEN, ACTION_RENAME, ACTION_DELETE, ACTION_REFRESH], + getItems: (context, items) => { + const editingGlobalPermission = this.navTreeSettingsService.settings.isValueDefault('editing') + ? this.coreSettingsService.settings.getValue('app.metadata.editing') + : this.navTreeSettingsService.settings.getValue('editing'); + + const deleteGlobalPermission = this.navTreeSettingsService.settings.isValueDefault('deleting') + ? this.coreSettingsService.settings.getValue('app.metadata.deleting') + : this.navTreeSettingsService.settings.getValue('deleting'); + + items = [...items]; + + if (editingGlobalPermission) { + items.push(ACTION_RENAME); + } + + if (deleteGlobalPermission) { + items.push(ACTION_DELETE); + } + + items.push(ACTION_REFRESH); + + return items; + }, orderItems: (context, items) => { const actionsOpen = menuExtractItems(items, [ACTION_OPEN]); - const actionsManage = menuExtractItems(items, [ACTION_RENAME, ACTION_DELETE]); - const actionsRefresh = menuExtractItems(items, [ACTION_REFRESH]); items.unshift(...actionsOpen); - items.push(...actionsManage); if (actionsRefresh.length > 0) { diff --git a/webapp/packages/plugin-navigation-tree/src/index.ts b/webapp/packages/plugin-navigation-tree/src/index.ts index 584565255d..c33fe0d751 100644 --- a/webapp/packages/plugin-navigation-tree/src/index.ts +++ b/webapp/packages/plugin-navigation-tree/src/index.ts @@ -6,6 +6,7 @@ export * from './NavigationTree/ElementsTree/ElementsTreeTools/NavigationTreeSet export * from './NavigationTree/ElementsTree/ElementsTreeTools/NavigationTreeSettings/ElementsTreeSettingsService'; export * from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeLoaders'; +export * from './NavigationTree/ElementsTree/NavigationTreeNode/TreeNodeMenu/DATA_CONTEXT_NAV_NODE_ACTIONS'; export * from './NavigationTree/ElementsTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenuLoader'; export * from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNodeRendererLoader'; export * from './NavigationTree/ElementsTree/NavigationTreeNode/isDraggingInsideProject'; diff --git a/webapp/packages/plugin-resource-manager/src/NavNodes/ResourceFoldersBootstrap.ts b/webapp/packages/plugin-resource-manager/src/NavNodes/ResourceFoldersBootstrap.ts index 38237a4d68..d9ddc87dc7 100644 --- a/webapp/packages/plugin-resource-manager/src/NavNodes/ResourceFoldersBootstrap.ts +++ b/webapp/packages/plugin-resource-manager/src/NavNodes/ResourceFoldersBootstrap.ts @@ -22,7 +22,7 @@ import { NavTreeResource, ProjectsNavNodeService, } from '@cloudbeaver/core-navigation-tree'; -import { ProjectInfo, ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; +import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; import { getRmResourceKey, getRmResourcePath, @@ -45,6 +45,7 @@ import { import { DATA_CONTEXT_ELEMENTS_TREE, MENU_ELEMENTS_TREE_TOOLS } from '@cloudbeaver/plugin-navigation-tree'; import { FolderDialog } from '@cloudbeaver/plugin-projects'; +import { NavResourceNodeService } from '../NavResourceNodeService'; import { ResourceManagerService } from '../ResourceManagerService'; import { DATA_CONTEXT_RESOURCE_MANAGER_TREE_RESOURCE_TYPE_ID } from '../Tree/DATA_CONTEXT_RESOURCE_MANAGER_TREE_RESOURCE_TYPE_ID'; import { getResourceKeyFromNodeId } from './getResourceKeyFromNodeId'; @@ -73,6 +74,7 @@ export class ResourceFoldersBootstrap extends Bootstrap { private readonly commonDialogService: CommonDialogService, private readonly actionService: ActionService, private readonly menuService: MenuService, + private readonly navResourceNodeService: NavResourceNodeService, private readonly navNodeInfoResource: NavNodeInfoResource, private readonly projectsNavNodeService: ProjectsNavNodeService, ) { @@ -114,7 +116,7 @@ export class ResourceFoldersBootstrap extends Bootstrap { }); } - load(): void | Promise { } + load(): void | Promise {} private async moveResourceToFolder({ type, targetNode, moveContexts }: INodeMoveData, contexts: IExecutionContextProvider) { const move = contexts.getContext(navNodeMoveContext); @@ -148,8 +150,19 @@ export class ResourceFoldersBootstrap extends Bootstrap { move.setCanMove(true); } else { try { - await this.navTreeResource.moveTo(resourceKeyList(nodeIdList), targetNode.id); - await this.navTreeResource.refreshTree(RESOURCES_NODE_PATH, true); + const targetRmFolderId = getResourceKeyFromNodeId(targetNode.id); + + if (targetRmFolderId) { + for (const nodeId of nodeIdList) { + const rmNodeId = getResourceKeyFromNodeId(nodeId); + if (rmNodeId) { + const key = getRmResourceKey(rmNodeId); + if (key.name) { + await this.navResourceNodeService.move(rmNodeId, createPath(targetRmFolderId, key.name)); + } + } + } + } } catch (exception: any) { this.notificationService.logException(exception, 'plugin_resource_manager_folder_move_failed'); } @@ -296,38 +309,6 @@ export class ResourceFoldersBootstrap extends Bootstrap { private syncNavTree() { this.navNodeManagerService.onMove.addHandler(this.moveResourceToFolder.bind(this)); - // this.navNodeInfoResource.onItemUpdate.addHandler(executorHandlerFilter( - // () => syncOutdate, - // key => { - // syncOutdate = false; - // try { - // const resources = [...new Set(ResourceKeyUtils - // .mapArray(key, getResourceKeyFromNodeId) - // .filter(isDefined) - // .map(getPathParent))]; - - // const keyList = resourceKeyList(resources); - - // if (!this.resourceManagerResource.isOutdated(keyList)) { - // this.resourceManagerResource.markOutdated(keyList); // because of this - // } - // } finally { - // syncOutdate = true; - // } - // } - // )); - - // this.navNodeInfoResource.onItemDelete.addHandler(executorHandlerFilter( - // () => true, - // key => { - // const resources = ResourceKeyUtils - // .mapArray(key, getResourceKeyFromNodeId) - // .filter(isDefined); - - // this.resourceManagerResource.delete(resourceKeyList(resources)); - // } - // )); - this.resourceManagerResource.onItemUpdate.addHandler(key => { const updated = resourceKeyList([...new Set(ResourceKeyUtils.mapArray(key, getResourceNodeId).map(getPathParent))]); if (!this.navTreeResource.isOutdated(updated)) { diff --git a/webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/NavigationNodeProjectControl.tsx b/webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/NavigationNodeProjectControl.tsx index c3dc4e6ef3..707fb66b13 100644 --- a/webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/NavigationNodeProjectControl.tsx +++ b/webapp/packages/plugin-resource-manager/src/Tree/ProjectsRenderer/NavigationNodeProjectControl.tsx @@ -6,28 +6,19 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import React, { forwardRef, useContext, useState } from 'react'; +import React, { forwardRef, useContext } from 'react'; import styled, { css, use } from 'reshadow'; -import { - getComputed, - TREE_NODE_STYLES, - TreeNodeContext, - TreeNodeControl, - TreeNodeName, - useMouseContextMenu, - useObjectRef, -} from '@cloudbeaver/core-blocks'; +import { getComputed, TREE_NODE_STYLES, TreeNodeContext, TreeNodeControl, TreeNodeName, useMouseContextMenu } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events'; -import { type INodeActions, NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; +import { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; import { ProjectInfoResource } from '@cloudbeaver/core-projects'; import { NAV_NODE_TYPE_RM_PROJECT } from '@cloudbeaver/core-resource-manager'; import { CaptureViewContext } from '@cloudbeaver/core-view'; import { ElementsTreeContext, isDraggingInsideProject, - NavigationNodeEditorLoader, NavTreeControlComponent, NavTreeControlProps, TreeNodeMenuLoader, @@ -94,15 +85,8 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer({ - rename: () => { - setEditing(true); - }, - }); - function handlePortalClick(event: React.MouseEvent) { EventContext.set(event, EventStopPropagationFlag); treeNodeContext.select(); @@ -149,15 +133,15 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer - {editing ? setEditing(false)} /> : {name}} + {name} - {!editing && !dndPlaceholder && ( + {!dndPlaceholder && ( - + )} , diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts index 830203862a..35e980637e 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts @@ -149,7 +149,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { return this.scheduler.executing; } - setResourceKey(resourceKey: string): void { + setResourceKey(resourceKey: string | undefined): void { if (this.state.resourceKey && this.resourceUseKeyId) { this.resourceManagerResource.free(toJS(this.state.resourceKey), this.resourceUseKeyId); this.resourceUseKeyId = null; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts index 036734add5..1a6a4448d1 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts @@ -223,6 +223,9 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { const tab = this.sqlEditorTabResourceService.getResourceTab(key); if (tab) { + const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId) as ResourceSqlDataSource; + + dataSource?.setResourceKey(undefined); // prevent deleted resource refresh tabs.push(tab.id); } }); diff --git a/webapp/packages/product-default/package.json b/webapp/packages/product-default/package.json index 5af4d1ba0a..4a20c7d04f 100644 --- a/webapp/packages/product-default/package.json +++ b/webapp/packages/product-default/package.json @@ -47,6 +47,7 @@ "@cloudbeaver/plugin-log-viewer": "~0.1.0", "@cloudbeaver/plugin-navigation-tabs": "~0.1.0", "@cloudbeaver/plugin-navigation-tree": "~0.1.0", + "@cloudbeaver/plugin-navigation-tree-rm": "~0.1.0", "@cloudbeaver/plugin-object-viewer": "~0.1.0", "@cloudbeaver/plugin-product": "~0.1.0", "@cloudbeaver/plugin-projects": "~0.1.0", diff --git a/webapp/packages/product-default/src/index.ts b/webapp/packages/product-default/src/index.ts index e80c053263..71a9a01cff 100644 --- a/webapp/packages/product-default/src/index.ts +++ b/webapp/packages/product-default/src/index.ts @@ -24,6 +24,7 @@ import localization from '@cloudbeaver/plugin-localization'; import logViewer from '@cloudbeaver/plugin-log-viewer'; import navigationTabs from '@cloudbeaver/plugin-navigation-tabs'; import navigationTree from '@cloudbeaver/plugin-navigation-tree'; +import { navigationTreeRMPlugin } from '@cloudbeaver/plugin-navigation-tree-rm'; import objectViewer from '@cloudbeaver/plugin-object-viewer'; import productPlugin from '@cloudbeaver/plugin-product'; import projects from '@cloudbeaver/plugin-projects'; @@ -87,6 +88,7 @@ const PLUGINS: PluginManifest[] = [ sqlEditorResource, logViewer, navigationTree, + navigationTreeRMPlugin, datasourceContextSwitch, topAppBar, version,