From 48233fee50c106334a949647ed3deef6d66dfa3c Mon Sep 17 00:00:00 2001 From: Dmitri Pisarev Date: Mon, 2 Dec 2019 12:18:27 +0300 Subject: [PATCH] FEATURE: batch operations on tree nodes (#2568) --- Classes/Controller/BackendController.php | 2 +- .../Controller/BackendServiceController.php | 27 ++- .../NeosUiDefaultNodesOperation.php | 4 +- Classes/Service/NodeClipboard.php | 28 ++- Configuration/Routes.Service.yaml | 12 +- Configuration/Settings.yaml | 4 +- Resources/Private/Fusion/Backend/Root.fusion | 8 +- Resources/Private/Translations/en/Main.xlf | 18 ++ packages/neos-ts-interfaces/src/index.ts | 6 + .../src/Endpoints/index.ts | 20 +- .../Operations/NeosUiDefaultNodes/index.ts | 4 +- .../NodeToolbar/Buttons/AddNode/index.js | 3 +- .../Buttons/PasteClipBoardNode/index.js | 12 +- .../neos-ui-guest-frame/src/InlineUI/index.js | 16 +- .../src/initializeGuestFrame.js | 7 +- .../src/CR/Nodes/helpers.ts | 45 +++- .../src/CR/Nodes/index.spec.js | 4 +- .../neos-ui-redux-store/src/CR/Nodes/index.ts | 206 +++++++++++----- .../src/CR/Nodes/selectors.ts | 32 ++- packages/neos-ui-redux-store/src/CR/index.ts | 2 +- .../src/UI/ContentCanvas/index.ts | 2 +- .../src/UI/InsertionModeModal/index.ts | 14 +- .../src/UI/Inspector/selectors.spec.js | 14 +- .../src/UI/PageTree/index.spec.js | 23 +- .../src/UI/PageTree/index.ts | 22 +- .../src/UI/PageTree/selectors.ts | 18 +- packages/neos-ui-redux-store/src/UI/index.ts | 2 +- .../neos-ui-redux-store/src/User/index.ts | 2 +- .../src/combineReducers.ts | 41 ++++ packages/neos-ui-redux-store/src/index.ts | 4 +- .../CR/NodeOperations/determineInsertMode.js | 4 +- .../src/CR/NodeOperations/hideNode.js | 15 ++ .../src/CR/NodeOperations/index.js | 2 + .../src/CR/NodeOperations/moveDroppedNodes.js | 21 ++ .../src/CR/NodeOperations/pasteNode.js | 20 +- .../src/CR/NodeOperations/reloadState.js | 6 +- .../NodeOperations/removeNodeIfConfirmed.js | 11 +- .../src/CR/NodeOperations/showNode.js | 15 ++ packages/neos-ui-sagas/src/manifest.js | 1 + .../LeftSideBar/NodeTree/Node/index.js | 132 +++++----- .../LeftSideBar/NodeTree/helpers.ts | 9 + .../Containers/LeftSideBar/NodeTree/index.js | 50 ++-- .../Buttons/HideSelectedNode/index.js | 20 +- .../LeftSideBar/NodeTreeToolBar/index.js | 229 +++++++++--------- .../src/Containers/Modals/DeleteNode/index.js | 53 ++-- .../src/Containers/Modals/InsertMode/index.js | 34 +-- .../RightSideBar/Inspector/index.js | 25 +- .../RightSideBar/Inspector/style.css | 2 +- packages/neos-ui/src/clipboardMiddleware.js | 9 +- packages/neos-ui/src/manifest.js | 3 +- packages/react-ui-components/src/Tree/node.js | 6 +- packages/tsconfig.json | 3 +- packages/utils-helpers/src/index.ts | 2 + packages/utils-helpers/src/isEqualSet.ts | 5 + tslint.json | 2 +- 55 files changed, 849 insertions(+), 432 deletions(-) create mode 100644 packages/neos-ui-redux-store/src/combineReducers.ts create mode 100644 packages/neos-ui-sagas/src/CR/NodeOperations/moveDroppedNodes.js create mode 100644 packages/neos-ui/src/Containers/LeftSideBar/NodeTree/helpers.ts create mode 100644 packages/utils-helpers/src/isEqualSet.ts diff --git a/Classes/Controller/BackendController.php b/Classes/Controller/BackendController.php index 03a43f52c2..b9073dedc9 100644 --- a/Classes/Controller/BackendController.php +++ b/Classes/Controller/BackendController.php @@ -153,7 +153,7 @@ public function indexAction(NodeInterface $node = null) $this->view->assign('user', $user); $this->view->assign('documentNode', $node); $this->view->assign('site', $siteNode); - $this->view->assign('clipboardNode', $this->clipboard->getNodeContextPath()); + $this->view->assign('clipboardNodes', $this->clipboard->getNodeContextPaths()); $this->view->assign('clipboardMode', $this->clipboard->getMode()); $this->view->assign('headScripts', $this->styleAndJavascriptInclusionService->getHeadScripts()); $this->view->assign('headStylesheets', $this->styleAndJavascriptInclusionService->getHeadStylesheets()); diff --git a/Classes/Controller/BackendServiceController.php b/Classes/Controller/BackendServiceController.php index 9b66417607..bd9fbcc5f3 100644 --- a/Classes/Controller/BackendServiceController.php +++ b/Classes/Controller/BackendServiceController.php @@ -22,6 +22,7 @@ use Neos\Flow\Mvc\ResponseInterface; use Neos\Flow\Mvc\View\JsonView; use Neos\Flow\Persistence\PersistenceManagerInterface; +use Neos\Flow\Property\PropertyMapper; use Neos\Neos\Domain\Service\ContentContextFactory; use Neos\Neos\Domain\Service\ContentDimensionPresetSourceInterface; use Neos\Neos\Service\PublishingService; @@ -116,6 +117,12 @@ class BackendServiceController extends ActionController */ protected $clipboard; + /** + * @Flow\Inject + * @var PropertyMapper + */ + protected $propertyMapper; + /** * @Flow\Inject * @var ContentDimensionPresetSourceInterface @@ -348,12 +355,16 @@ public function changeBaseWorkspaceAction(string $targetWorkspaceName, NodeInter /** * Persists the clipboard node on copy * - * @param NodeInterface $node + * @param array $nodes * @return void */ - public function copyNodeAction(NodeInterface $node) + public function copyNodesAction(array $nodes) { - $this->clipboard->copyNode($node); + // TODO @christianm want's to have a property mapper for this + $nodes = array_map(function ($node) { + return $this->propertyMapper->convert($node, NodeInterface::class); + }, $nodes); + $this->clipboard->copyNodes($nodes); } /** @@ -369,12 +380,16 @@ public function clearClipboardAction() /** * Persists the clipboard node on cut * - * @param NodeInterface $node + * @param array $nodes * @return void */ - public function cutNodeAction(NodeInterface $node) + public function cutNodesAction(array $nodes) { - $this->clipboard->cutNode($node); + // TODO @christianm want's to have a property mapper for this + $nodes = array_map(function ($node) { + return $this->propertyMapper->convert($node, NodeInterface::class); + }, $nodes); + $this->clipboard->cutNodes($nodes); } public function getWorkspaceInfoAction() diff --git a/Classes/FlowQueryOperations/NeosUiDefaultNodesOperation.php b/Classes/FlowQueryOperations/NeosUiDefaultNodesOperation.php index 6c4c944ab9..de452f40fe 100644 --- a/Classes/FlowQueryOperations/NeosUiDefaultNodesOperation.php +++ b/Classes/FlowQueryOperations/NeosUiDefaultNodesOperation.php @@ -63,7 +63,7 @@ public function canEvaluate($context) public function evaluate(FlowQuery $flowQuery, array $arguments) { list($siteNode, $documentNode) = $flowQuery->getContext(); - list($baseNodeType, $loadingDepth, $toggledNodes, $clipboardNodeContextPath) = $arguments; + list($baseNodeType, $loadingDepth, $toggledNodes, $clipboardNodesContextPaths) = $arguments; // Collect all parents of documentNode up to siteNode $parents = []; @@ -96,7 +96,7 @@ public function evaluate(FlowQuery $flowQuery, array $arguments) $nodes[] = $documentNode; } - if ($clipboardNodeContextPath) { + foreach ($clipboardNodesContextPaths as $clipboardNodeContextPath) { $clipboardNode = $this->propertyMapper->convert($clipboardNodeContextPath, NodeInterface::class); if ($clipboardNode && !in_array($clipboardNode, $nodes)) { $nodes[] = $clipboardNode; diff --git a/Classes/Service/NodeClipboard.php b/Classes/Service/NodeClipboard.php index d04bd917ed..2a3ddfd95b 100644 --- a/Classes/Service/NodeClipboard.php +++ b/Classes/Service/NodeClipboard.php @@ -27,7 +27,7 @@ class NodeClipboard /** * @var string */ - protected $nodeContextPath = ''; + protected $nodeContextPaths = []; /** * @var string one of the NodeClipboard::MODE_* constants @@ -37,26 +37,30 @@ class NodeClipboard /** * Save copied node to clipboard. * - * @param NodeInterface $node + * @param NodeInterface[] $nodes * @return void * @Flow\Session(autoStart=true) */ - public function copyNode(NodeInterface $node) + public function copyNodes(array $nodes) { - $this->nodeContextPath = $node->getContextPath(); + $this->nodeContextPaths = array_map(function ($node) { + return $node->getContextPath(); + }, $nodes); $this->mode = self::MODE_COPY; } /** - * Save cut node to clipboard. + * Save cut nodes to clipboard. * - * @param NodeInterface $node + * @param NodeInterface[] $nodes * @return void * @Flow\Session(autoStart=true) */ - public function cutNode(NodeInterface $node) + public function cutNodes(array $nodes) { - $this->nodeContextPath = $node->getContextPath(); + $this->nodeContextPaths = array_map(function ($node) { + return $node->getContextPath(); + }, $nodes); $this->mode = self::MODE_MOVE; } @@ -68,18 +72,18 @@ public function cutNode(NodeInterface $node) */ public function clear() { - $this->nodeContextPath = ''; + $this->nodeContextPaths = []; $this->mode = ''; } /** * Get clipboard node. * - * @return string $nodeContextPath + * @return array $nodeContextPath */ - public function getNodeContextPath() + public function getNodeContextPaths() { - return $this->nodeContextPath ? $this->nodeContextPath : ''; + return $this->nodeContextPaths ? $this->nodeContextPaths : []; } /** diff --git a/Configuration/Routes.Service.yaml b/Configuration/Routes.Service.yaml index aaa5fb13bc..5be7b1dde3 100644 --- a/Configuration/Routes.Service.yaml +++ b/Configuration/Routes.Service.yaml @@ -31,19 +31,19 @@ httpMethods: ['POST'] - - name: 'Copy node to clipboard' - uriPattern: 'copy-node' + name: 'Copy nodes to clipboard' + uriPattern: 'copy-nodes' defaults: '@controller': 'BackendService' - '@action': 'copyNode' + '@action': 'copyNodes' httpMethods: ['POST'] - - name: 'Cut node to clipboard' - uriPattern: 'cut-node' + name: 'Cut nodes to clipboard' + uriPattern: 'cut-nodes' defaults: '@controller': 'BackendService' - '@action': 'cutNode' + '@action': 'cutNodes' httpMethods: ['POST'] - diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index 01e5016821..fd0a0d38c7 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -126,7 +126,7 @@ Neos: byContextPath: '${Neos.Ui.NodeInfo.defaultNodesForBackend(site, documentNode, controllerContext)}' siteNode: '${q(site).property(''_contextPath'')}' documentNode: '${q(documentNode).property(''_contextPath'')}' - clipboard: '${clipboardNode || null}' + clipboard: '${clipboardNodes || []}' clipboardMode: '${clipboardMode || null}' contentDimensions: byName: '${Neos.Ui.ContentDimensions.contentDimensionsByName()}' @@ -156,7 +156,7 @@ Neos: pageTree: isLoading: false hasError: false - focused: '${q(documentNode).property(''_contextPath'')}' + focused: '${[q(documentNode).property(''_contextPath'')]}' active: '${q(documentNode).property(''_contextPath'')}' remote: isSaving: false diff --git a/Resources/Private/Fusion/Backend/Root.fusion b/Resources/Private/Fusion/Backend/Root.fusion index 7d628be006..5800b6ac57 100644 --- a/Resources/Private/Fusion/Backend/Root.fusion +++ b/Resources/Private/Fusion/Backend/Root.fusion @@ -117,11 +117,11 @@ backend = Neos.Fusion:Template { changeBaseWorkspace = Neos.Fusion:UriBuilder { action = 'changeBaseWorkspace' } - copyNode = Neos.Fusion:UriBuilder { - action = 'copyNode' + copyNodes = Neos.Fusion:UriBuilder { + action = 'copyNodes' } - cutNode = Neos.Fusion:UriBuilder { - action = 'cutNode' + cutNodes = Neos.Fusion:UriBuilder { + action = 'cutNodes' } clearClipboard = Neos.Fusion:UriBuilder { action = 'clearClipboard' diff --git a/Resources/Private/Translations/en/Main.xlf b/Resources/Private/Translations/en/Main.xlf index 573996031f..14dc500397 100644 --- a/Resources/Private/Translations/en/Main.xlf +++ b/Resources/Private/Translations/en/Main.xlf @@ -269,6 +269,24 @@ Syncronize with the title property + + nodes + + + content elements selected + + + documents selected + + + Select a single document in order to be able to edit its properties + + + Select a single content element in order to be able to edit its properties + + + Delete {amount} nodes + diff --git a/packages/neos-ts-interfaces/src/index.ts b/packages/neos-ts-interfaces/src/index.ts index c5db3bfc00..25f9fcea3f 100644 --- a/packages/neos-ts-interfaces/src/index.ts +++ b/packages/neos-ts-interfaces/src/index.ts @@ -95,6 +95,12 @@ export enum InsertPosition { AFTER = 'after' } +export enum SelectionModeTypes { + SINGLE_SELECT = 'SINGLE_SELECT', + MULTIPLE_SELECT = 'MULTIPLE_SELECT', + RANGE_SELECT = 'RANGE_SELECT' +} + export interface ValidatorConfiguration { [propName: string]: any; } diff --git a/packages/neos-ui-backend-connector/src/Endpoints/index.ts b/packages/neos-ui-backend-connector/src/Endpoints/index.ts index 26f83ec8cc..d6b9b67d88 100644 --- a/packages/neos-ui-backend-connector/src/Endpoints/index.ts +++ b/packages/neos-ui-backend-connector/src/Endpoints/index.ts @@ -10,8 +10,8 @@ export interface Routes { publish: string; discard: string; changeBaseWorkspace: string; - copyNode: string; - cutNode: string; + copyNodes: string; + cutNodes: string; clearClipboard: string; loadTree: string; flowQuery: string; @@ -107,8 +107,8 @@ export default (routes: Routes) => { })).then(response => fetchWithErrorHandling.parseJson(response)) .catch(reason => fetchWithErrorHandling.generalErrorHandler(reason)); - const copyNode = (node: NodeContextPath) => fetchWithErrorHandling.withCsrfToken(csrfToken => ({ - url: routes.ui.service.copyNode, + const copyNodes = (nodes: NodeContextPath[]) => fetchWithErrorHandling.withCsrfToken(csrfToken => ({ + url: routes.ui.service.copyNodes, method: 'POST', credentials: 'include', @@ -117,13 +117,13 @@ export default (routes: Routes) => { 'Content-Type': 'application/json' }, body: JSON.stringify({ - node + nodes }) })).then(response => fetchWithErrorHandling.parseJson(response)) .catch(reason => fetchWithErrorHandling.generalErrorHandler(reason)); - const cutNode = (node: NodeContextPath) => fetchWithErrorHandling.withCsrfToken(csrfToken => ({ - url: routes.ui.service.cutNode, + const cutNodes = (nodes: NodeContextPath[]) => fetchWithErrorHandling.withCsrfToken(csrfToken => ({ + url: routes.ui.service.cutNodes, method: 'POST', credentials: 'include', @@ -132,7 +132,7 @@ export default (routes: Routes) => { 'Content-Type': 'application/json' }, body: JSON.stringify({ - node + nodes }) })).then(response => fetchWithErrorHandling.parseJson(response)) .catch(reason => fetchWithErrorHandling.generalErrorHandler(reason)); @@ -610,8 +610,8 @@ export default (routes: Routes) => { publish, discard, changeBaseWorkspace, - copyNode, - cutNode, + copyNodes, + cutNodes, clearClipboard, createImageVariant, loadMasterPlugins, diff --git a/packages/neos-ui-backend-connector/src/FlowQuery/Operations/NeosUiDefaultNodes/index.ts b/packages/neos-ui-backend-connector/src/FlowQuery/Operations/NeosUiDefaultNodes/index.ts index e3f7c0d6df..d494be4f7b 100644 --- a/packages/neos-ui-backend-connector/src/FlowQuery/Operations/NeosUiDefaultNodes/index.ts +++ b/packages/neos-ui-backend-connector/src/FlowQuery/Operations/NeosUiDefaultNodes/index.ts @@ -1,6 +1,6 @@ import {NodeTypeName, NodeContextPath} from '@neos-project/neos-ts-interfaces'; -export default () => (baseNodeType: NodeTypeName, loadingDepth: number | undefined, toggledNodes: NodeContextPath[], clipboardNodeContextPath: NodeContextPath) => ({ +export default () => (baseNodeType: NodeTypeName, loadingDepth: number | undefined, toggledNodes: NodeContextPath[], clipboardNodesContextPaths: NodeContextPath[]) => ({ type: 'neosUiDefaultNodes', - payload: [baseNodeType, loadingDepth, toggledNodes, clipboardNodeContextPath] + payload: [baseNodeType, loadingDepth, toggledNodes, clipboardNodesContextPaths] }); diff --git a/packages/neos-ui-guest-frame/src/InlineUI/NodeToolbar/Buttons/AddNode/index.js b/packages/neos-ui-guest-frame/src/InlineUI/NodeToolbar/Buttons/AddNode/index.js index 2b4d35e011..f9635db0e0 100644 --- a/packages/neos-ui-guest-frame/src/InlineUI/NodeToolbar/Buttons/AddNode/index.js +++ b/packages/neos-ui-guest-frame/src/InlineUI/NodeToolbar/Buttons/AddNode/index.js @@ -1,7 +1,6 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; import {connect} from 'react-redux'; -import {$get} from 'plow-js'; import {selectors, actions} from '@neos-project/neos-ui-redux-store'; import IconButton from '@neos-project/react-ui-components/src/IconButton/'; @@ -15,7 +14,7 @@ import {neos} from '@neos-project/neos-ui-decorators'; const isAllowedToAddChildOrSiblingNodesSelector = selectors.CR.Nodes.makeIsAllowedToAddChildOrSiblingNodes(nodeTypesRegistry); return state => { - const focusedNodeContextPath = $get('cr.nodes.focused.contextPath', state); + const focusedNodeContextPath = selectors.CR.Nodes.focusedNodePathSelector(state); const isAllowedToAddChildOrSiblingNodes = isAllowedToAddChildOrSiblingNodesSelector(state, { reference: focusedNodeContextPath }); diff --git a/packages/neos-ui-guest-frame/src/InlineUI/NodeToolbar/Buttons/PasteClipBoardNode/index.js b/packages/neos-ui-guest-frame/src/InlineUI/NodeToolbar/Buttons/PasteClipBoardNode/index.js index 7f808b8490..79f255a60a 100644 --- a/packages/neos-ui-guest-frame/src/InlineUI/NodeToolbar/Buttons/PasteClipBoardNode/index.js +++ b/packages/neos-ui-guest-frame/src/InlineUI/NodeToolbar/Buttons/PasteClipBoardNode/index.js @@ -14,11 +14,13 @@ import {selectors, actions} from '@neos-project/neos-ui-redux-store'; const canBePastedSelector = selectors.CR.Nodes.makeCanBePastedSelector(nodeTypesRegistry); return (state, {contextPath}) => { - const clipboardNodeContextPath = selectors.CR.Nodes.clipboardNodeContextPathSelector(state); - const canBePasted = canBePastedSelector(state, { - subject: clipboardNodeContextPath, - reference: contextPath - }); + const clipboardNodesContextPaths = selectors.CR.Nodes.clipboardNodesContextPathsSelector(state); + const canBePasted = (clipboardNodesContextPaths.every(clipboardNodeContextPath => { + return canBePastedSelector(state, { + subject: clipboardNodeContextPath, + reference: contextPath + }); + })); return {canBePasted}; }; diff --git a/packages/neos-ui-guest-frame/src/InlineUI/index.js b/packages/neos-ui-guest-frame/src/InlineUI/index.js index 3d7f43bcb0..9869255854 100644 --- a/packages/neos-ui-guest-frame/src/InlineUI/index.js +++ b/packages/neos-ui-guest-frame/src/InlineUI/index.js @@ -9,6 +9,7 @@ import NodeToolbar from './NodeToolbar/index'; import style from './style.css'; import InlineValidationErrors from './InlineValidationErrors/index'; +import {isEqualSet} from '@neos-project/utils-helpers'; @neos(globalRegistry => ({ nodeTypesRegistry: globalRegistry.get('@neos-project/neos-ui-contentrepository') @@ -16,10 +17,11 @@ import InlineValidationErrors from './InlineValidationErrors/index'; @connect($transform({ focused: $get('cr.nodes.focused'), focusedNode: selectors.CR.Nodes.focusedSelector, + focusedNodesContextPaths: selectors.CR.Nodes.focusedNodePathsSelector, shouldScrollIntoView: selectors.UI.ContentCanvas.shouldScrollIntoView, destructiveOperationsAreDisabled: selectors.CR.Nodes.destructiveOperationsAreDisabledSelector, clipboardMode: $get('cr.nodes.clipboardMode'), - clipboardNodeContextPath: selectors.CR.Nodes.clipboardNodeContextPathSelector + clipboardNodesContextPaths: selectors.CR.Nodes.clipboardNodesContextPathsSelector }), { requestScrollIntoView: actions.UI.ContentCanvas.requestScrollIntoView }) @@ -32,16 +34,17 @@ export default class InlineUI extends PureComponent { requestScrollIntoView: PropTypes.func.isRequired, shouldScrollIntoView: PropTypes.bool.isRequired, clipboardMode: PropTypes.string, - clipboardNodeContextPath: PropTypes.string + clipboardNodesContextPaths: PropTypes.array }; render() { const {focused} = this.props; - const focusedNodeContextPath = focused.contextPath; - const {nodeTypesRegistry, focusedNode, shouldScrollIntoView, requestScrollIntoView, destructiveOperationsAreDisabled, clipboardMode, clipboardNodeContextPath} = this.props; + const {nodeTypesRegistry, focusedNode, focusedNodesContextPaths, clipboardNodesContextPaths, shouldScrollIntoView, requestScrollIntoView, destructiveOperationsAreDisabled, clipboardMode} = this.props; + const focusedNodeContextPath = focusedNode.contextPath; const isDocument = nodeTypesRegistry.hasRole($get('nodeType', focusedNode), 'document'); - const isCut = focusedNodeContextPath === clipboardNodeContextPath && clipboardMode === 'Move'; - const isCopied = focusedNodeContextPath === clipboardNodeContextPath && clipboardMode === 'Copy'; + const allFocusedNodesAreInClipboard = isEqualSet(focusedNodesContextPaths, clipboardNodesContextPaths); + const isCut = allFocusedNodesAreInClipboard && clipboardMode === 'Move'; + const isCopied = allFocusedNodesAreInClipboard && clipboardMode === 'Copy'; const canBeDeleted = $get('policy.canRemove', this.props.focusedNode) || false; const canBeEdited = $get('policy.canEdit', this.props.focusedNode) || false; const visibilityCanBeToggled = !$contains('_hidden', 'policy.disallowedProperties', this.props.focusedNode); @@ -57,6 +60,7 @@ export default class InlineUI extends PureComponent { canBeDeleted={canBeDeleted} canBeEdited={canBeEdited} visibilityCanBeToggled={visibilityCanBeToggled} + contextPath={focusedNodeContextPath} {...focused} />} diff --git a/packages/neos-ui-guest-frame/src/initializeGuestFrame.js b/packages/neos-ui-guest-frame/src/initializeGuestFrame.js index cd3a2837b4..178391fcbd 100644 --- a/packages/neos-ui-guest-frame/src/initializeGuestFrame.js +++ b/packages/neos-ui-guest-frame/src/initializeGuestFrame.js @@ -15,6 +15,7 @@ import { } from './dom'; import style from './style.css'; +import {SelectionModeTypes} from '@neos-project/neos-ts-interfaces'; // // Get all parent elements of the event target. @@ -101,7 +102,7 @@ export default ({globalRegistry, store}) => function * initializeGuestFrame() { const contextPath = selectedDomNode.getAttribute('data-__neos-node-contextpath'); const fusionPath = selectedDomNode.getAttribute('data-__neos-fusion-path'); const state = store.getState(); - const focusedNodeContextPath = $get('cr.nodes.focused.contextPath', state); + const focusedNodeContextPath = selectors.CR.Nodes.focusedNodePathSelector(state); if (!isInsideEditableProperty) { store.dispatch(actions.UI.ContentCanvas.setCurrentlyEditedPropertyName('')); } @@ -162,6 +163,10 @@ export default ({globalRegistry, store}) => function * initializeGuestFrame() { } yield takeEvery(actionTypes.CR.Nodes.FOCUS, function * (action) { + // Don't focus node in contentcanvas when multiselecting + if (action.payload.selectionMode !== SelectionModeTypes.SINGLE_SELECT) { + return; + } const oldNode = findInGuestFrame(`.${style['markActiveNodeAsFocused--focusedNode']}`); if (oldNode) { diff --git a/packages/neos-ui-redux-store/src/CR/Nodes/helpers.ts b/packages/neos-ui-redux-store/src/CR/Nodes/helpers.ts index 37fb3a9e8e..e748e592cb 100644 --- a/packages/neos-ui-redux-store/src/CR/Nodes/helpers.ts +++ b/packages/neos-ui-redux-store/src/CR/Nodes/helpers.ts @@ -1,4 +1,4 @@ -import {isNode, Node, NodeMap, NodeContextPath} from '@neos-project/neos-ts-interfaces'; +import {isNode, Node, NodeMap, NodeContextPath, SelectionModeTypes} from '@neos-project/neos-ts-interfaces'; // // Helper function to determine allowed node types @@ -54,3 +54,46 @@ export const getNodeOrThrow = (nodeMap: NodeMap, contextPath: NodeContextPath) = } return node; }; + +export const calculateNewFocusedNodes = (selectionMode: SelectionModeTypes, contextPath: NodeContextPath, focusedNodesContextPaths: NodeContextPath[], nodesByContextPath: NodeMap): NodeContextPath[] | null => { + if (selectionMode === SelectionModeTypes.SINGLE_SELECT) { + return [contextPath]; + } else if (selectionMode === SelectionModeTypes.RANGE_SELECT) { + const lastSelectedNodeContextPath = focusedNodesContextPaths[focusedNodesContextPaths.length - 1]; + const maybeParentNodeContextPath = parentNodeContextPath(lastSelectedNodeContextPath); + if (maybeParentNodeContextPath) { + const parentNode = getNodeOrThrow(nodesByContextPath, maybeParentNodeContextPath); + const tempSelection: string[] = []; + let startSelectionFlag = false; + // if both start and end nodes are within children, then we can do range select + const startAndEndOfSelectionAreOnOneLevel = parentNode.children.some(child => { + if (child.contextPath === lastSelectedNodeContextPath || child.contextPath === contextPath) { + if (startSelectionFlag) { // if matches for the second time it means that both start and end of selection were found + tempSelection.push(child.contextPath); // also push the last node + return true; + } else { + startSelectionFlag = true; + } + } + if (startSelectionFlag) { + tempSelection.push(child.contextPath); + } + return false; + }); + if (startAndEndOfSelectionAreOnOneLevel) { + const focusedNodesContextPathsSet = new Set(focusedNodesContextPaths); + tempSelection.forEach(contextPath => focusedNodesContextPathsSet.add(contextPath)); + return [...focusedNodesContextPathsSet] as string[]; + } + } + } else { + const focusedNodesContextPathsSet = new Set(focusedNodesContextPaths); + if (focusedNodesContextPathsSet.has(contextPath)) { + focusedNodesContextPathsSet.delete(contextPath); + } else { + focusedNodesContextPathsSet.add(contextPath); + } + return [...focusedNodesContextPathsSet]; + } + return null; +}; diff --git a/packages/neos-ui-redux-store/src/CR/Nodes/index.spec.js b/packages/neos-ui-redux-store/src/CR/Nodes/index.spec.js index 7ff02887a0..054aaaee03 100644 --- a/packages/neos-ui-redux-store/src/CR/Nodes/index.spec.js +++ b/packages/neos-ui-redux-store/src/CR/Nodes/index.spec.js @@ -61,10 +61,10 @@ test(`The reducer should create a valid initial state`, () => { siteNode: 'siteNode', documentNode: 'documentNode', focused: { - contextPath: null, + contextPaths: [], fusionPath: null }, - toBeRemoved: null, + toBeRemoved: [], clipboard: null, clipboardMode: null, inlineValidationErrors: {} diff --git a/packages/neos-ui-redux-store/src/CR/Nodes/index.ts b/packages/neos-ui-redux-store/src/CR/Nodes/index.ts index 978667359c..7387c96463 100644 --- a/packages/neos-ui-redux-store/src/CR/Nodes/index.ts +++ b/packages/neos-ui-redux-store/src/CR/Nodes/index.ts @@ -4,9 +4,9 @@ import {action as createAction, ActionType} from 'typesafe-actions'; import {actionTypes as system, InitAction} from '@neos-project/neos-ui-redux-store/src/System'; import * as selectors from './selectors'; -import {parentNodeContextPath, getNodeOrThrow} from './helpers'; +import {parentNodeContextPath, getNodeOrThrow, calculateNewFocusedNodes} from './helpers'; -import {FusionPath, NodeContextPath, InsertPosition, NodeMap, ClipboardMode, NodeTypeName} from '@neos-project/neos-ts-interfaces'; +import {FusionPath, NodeContextPath, InsertPosition, NodeMap, ClipboardMode, SelectionModeTypes, NodeTypeName} from '@neos-project/neos-ts-interfaces'; interface InlineValidationErrors { [itemProp: string]: any; @@ -24,11 +24,11 @@ export interface State extends Readonly<{ siteNode: NodeContextPath | null; documentNode: NodeContextPath | null; focused: { - contextPath: NodeContextPath | null; fusionPath: FusionPath | null; + contextPaths: NodeContextPath[]; }, - toBeRemoved: NodeContextPath | null; - clipboard: NodeContextPath | null; + toBeRemoved: NodeContextPath[]; + clipboard: NodeContextPath[]; clipboardMode: ClipboardMode | null; inlineValidationErrors: InlineValidationErrors }> {} @@ -38,11 +38,11 @@ export const defaultState: State = { siteNode: null, documentNode: null, focused: { - contextPath: null, + contextPaths: [], fusionPath: null }, - toBeRemoved: null, - clipboard: null, + toBeRemoved: [], + clipboard: [], clipboardMode: null, inlineValidationErrors: {} }; @@ -65,6 +65,7 @@ export enum actionTypes { UNFOCUS = '@neos/neos-ui/CR/Nodes/UNFOCUS', COMMENCE_CREATION = '@neos/neos-ui/CR/Nodes/COMMENCE_CREATION', COMMENCE_REMOVAL = '@neos/neos-ui/CR/Nodes/COMMENCE_REMOVAL', + COMMENCE_REMOVAL_MULTIPLE = '@neos/neos-ui/CR/Nodes/COMMENCE_REMOVAL_MULTIPLE', REMOVAL_ABORTED = '@neos/neos-ui/CR/Nodes/REMOVAL_ABORTED', REMOVAL_CONFIRMED = '@neos/neos-ui/CR/Nodes/REMOVAL_CONFIRMED', REMOVE = '@neos/neos-ui/CR/Nodes/REMOVE', @@ -72,12 +73,17 @@ export enum actionTypes { SET_STATE = '@neos/neos-ui/CR/Nodes/SET_STATE', RELOAD_STATE = '@neos/neos-ui/CR/Nodes/RELOAD_STATE', COPY = '@neos/neos-ui/CR/Nodes/COPY', + COPY_MULTIPLE = '@neos/neos-ui/CR/Nodes/COPY_MULTIPLE', CUT = '@neos/neos-ui/CR/Nodes/CUT', + CUT_MULTIPLE = '@neos/neos-ui/CR/Nodes/CUT_MULTIPLE', MOVE = '@neos/neos-ui/CR/Nodes/MOVE', + MOVE_MULTIPLE = '@neos/neos-ui/CR/Nodes/MOVE_MULTIPLE', PASTE = '@neos/neos-ui/CR/Nodes/PASTE', COMMIT_PASTE = '@neos/neos-ui/CR/Nodes/COMMIT_PASTE', HIDE = '@neos/neos-ui/CR/Nodes/HIDE', + HIDE_MULTIPLE = '@neos/neos-ui/CR/Nodes/HIDE_MULTIPLE', SHOW = '@neos/neos-ui/CR/Nodes/SHOW', + SHOW_MULTIPLE = '@neos/neos-ui/CR/Nodes/SHOW_MULTIPLE', UPDATE_URI = '@neos/neos-ui/CR/Nodes/UPDATE_URI', SET_INLINE_VALIDATION_ERRORS = '@neos/neos-ui/CR/Nodes/SET_INLINE_VALIDATION_ERRORS' } @@ -112,7 +118,7 @@ const changeProperty = (propertyChanges: ReadonlyArray) => creat * @param {String} fusionPath The fusion path of the focused node, needed for out-of-band-rendering, e.g. when * adding new nodes */ -const focus = (contextPath: NodeContextPath, fusionPath: FusionPath) => createAction(actionTypes.FOCUS, {contextPath, fusionPath}); +const focus = (contextPath: NodeContextPath, fusionPath: FusionPath, selectionMode: SelectionModeTypes = SelectionModeTypes.SINGLE_SELECT) => createAction(actionTypes.FOCUS, {contextPath, fusionPath, selectionMode}); /** * Un-marks all nodes as not focused. @@ -126,6 +132,8 @@ const unFocus = () => createAction(actionTypes.UNFOCUS); */ const commenceRemoval = (contextPath: NodeContextPath) => createAction(actionTypes.COMMENCE_REMOVAL, contextPath); +const commenceRemovalMultiple = (contextPaths: NodeContextPath[]) => createAction(actionTypes.COMMENCE_REMOVAL_MULTIPLE, contextPaths); + /** * Start node creation workflow * @@ -221,6 +229,8 @@ const adoptDataToHost = (object: T): T => JSON.parse(JSON.stringify(object)); */ const copy = (contextPath: NodeContextPath) => createAction(actionTypes.COPY, contextPath); +const copyMultiple = (contextPaths: NodeContextPath[]) => createAction(actionTypes.COPY_MULTIPLE, contextPaths); + /** * Mark a node for cut on paste * @@ -228,6 +238,8 @@ const copy = (contextPath: NodeContextPath) => createAction(actionTypes.COPY, co */ const cut = (contextPath: NodeContextPath) => createAction(actionTypes.CUT, contextPath); +const cutMultiple = (contextPaths: NodeContextPath[]) => createAction(actionTypes.CUT_MULTIPLE, contextPaths); + /** * Move a node * @@ -241,6 +253,12 @@ const move = ( position: InsertPosition ) => createAction(actionTypes.MOVE, {nodeToBeMoved, targetNode, position}); +const moveMultiple = ( + nodesToBeMoved: NodeContextPath[], + targetNode: NodeContextPath, + position: InsertPosition +) => createAction(actionTypes.MOVE_MULTIPLE, {nodesToBeMoved, targetNode, position}); + /** * Paste the contents of the node clipboard * @@ -262,6 +280,8 @@ const commitPaste = (clipboardMode: ClipboardMode) => createAction(actionTypes.C */ const hide = (contextPath: NodeContextPath) => createAction(actionTypes.HIDE, contextPath); +const hideMultiple = (contextPaths: NodeContextPath[]) => createAction(actionTypes.HIDE_MULTIPLE, contextPaths); + /** * Show the given node * @@ -269,6 +289,8 @@ const hide = (contextPath: NodeContextPath) => createAction(actionTypes.HIDE, co */ const show = (contextPath: NodeContextPath) => createAction(actionTypes.SHOW, contextPath); +const showMultiple = (contextPaths: NodeContextPath[]) => createAction(actionTypes.SHOW_MULTIPLE, contextPaths); + /** * Update uris of all affected nodes after uriPathSegment of a node has changed * Must update the node itself and all of its descendants @@ -289,6 +311,7 @@ export const actions = { unFocus, commenceCreation, commenceRemoval, + commenceRemovalMultiple, abortRemoval, confirmRemoval, remove, @@ -296,16 +319,70 @@ export const actions = { setState, reloadState, copy, + copyMultiple, cut, + cutMultiple, move, + moveMultiple, paste, commitPaste, hide, + hideMultiple, show, + showMultiple, updateUri, setInlineValidationErrors }; +const moveNodeInState = ( + sourceNodeContextPath: string, + targetNodeContextPath: string, + position: string, + draft: State +) => { + let baseNodeContextPath; + if (position === 'into') { + baseNodeContextPath = targetNodeContextPath; + } else { + baseNodeContextPath = parentNodeContextPath(targetNodeContextPath); + if (baseNodeContextPath === null) { + throw new Error(`Target node "{targetNodeContextPath}" doesn't have a parent, yet you are trying to move a node next to it`); + } + } + + const sourceNodeParentContextPath = parentNodeContextPath(sourceNodeContextPath); + if (sourceNodeParentContextPath === null) { + throw new Error(`The source node "{sourceNodeParentContextPath}" doesn't have a parent, you can't move it`); + } + const baseNode = getNodeOrThrow(draft.byContextPath, baseNodeContextPath); + const sourceNodeParent = getNodeOrThrow(draft.byContextPath, sourceNodeParentContextPath); + + const originalSourceChildren = sourceNodeParent.children; + const sourceIndex = originalSourceChildren.findIndex(child => child.contextPath === sourceNodeContextPath); + const childRepresentationOfSourceNode = originalSourceChildren[sourceIndex]; + + const processedChildren = baseNode.children; + + if (sourceNodeParentContextPath === baseNodeContextPath) { + // If moving into the same parent, delete source node from it + processedChildren.splice(sourceIndex, 1); + } else { + // Else delete the source node from its parent + originalSourceChildren.splice(sourceIndex, 1); + sourceNodeParent.children = originalSourceChildren; + } + + // Add source node to the children of the base node, at the right position + if (position === 'into') { + processedChildren.push(childRepresentationOfSourceNode); + } else { + const targetIndex = processedChildren.findIndex(child => child.contextPath === targetNodeContextPath); + const insertIndex = position === 'before' ? targetIndex : targetIndex + 1; + processedChildren.splice(insertIndex, 0, childRepresentationOfSourceNode); + } + baseNode.children = processedChildren; +}; + // // Export the reducer // @@ -336,48 +413,14 @@ export const reducer = (state: State = defaultState, action: InitAction | Action } case actionTypes.MOVE: { const {nodeToBeMoved: sourceNodeContextPath, targetNode: targetNodeContextPath, position} = action.payload; - - let baseNodeContextPath; - if (position === 'into') { - baseNodeContextPath = targetNodeContextPath; - } else { - baseNodeContextPath = parentNodeContextPath(targetNodeContextPath); - if (baseNodeContextPath === null) { - throw new Error(`Target node "{targetNodeContextPath}" doesn't have a parent, yet you are trying to move a node next to it`); - } - } - - const sourceNodeParentContextPath = parentNodeContextPath(sourceNodeContextPath); - if (sourceNodeParentContextPath === null) { - throw new Error(`The source node "{sourceNodeParentContextPath}" doesn't have a parent, you can't move it`); - } - const baseNode = getNodeOrThrow(draft.byContextPath, baseNodeContextPath); - const sourceNodeParent = getNodeOrThrow(draft.byContextPath, sourceNodeParentContextPath); - - const originalSourceChildren = sourceNodeParent.children; - const sourceIndex = originalSourceChildren.findIndex(child => child.contextPath === sourceNodeContextPath); - const childRepresentationOfSourceNode = originalSourceChildren[sourceIndex]; - - const processedChildren = baseNode.children; - - if (sourceNodeParentContextPath === baseNodeContextPath) { - // If moving into the same parent, delete source node from it - processedChildren.splice(sourceIndex, 1); - } else { - // Else delete the source node from its parent - originalSourceChildren.splice(sourceIndex, 1); - sourceNodeParent.children = originalSourceChildren; - } - - // Add source node to the children of the base node, at the right position - if (position === 'into') { - processedChildren.push(childRepresentationOfSourceNode); - } else { - const targetIndex = processedChildren.findIndex(child => child.contextPath === targetNodeContextPath); - const insertIndex = position === 'before' ? targetIndex : targetIndex + 1; - processedChildren.splice(insertIndex, 0, childRepresentationOfSourceNode); - } - baseNode.children = processedChildren; + moveNodeInState(sourceNodeContextPath, targetNodeContextPath, position, draft); + break; + } + case actionTypes.MOVE_MULTIPLE: { + const {nodesToBeMoved, targetNode: targetNodeContextPath, position} = action.payload; + nodesToBeMoved.forEach(sourceNodeContextPath => { + moveNodeInState(sourceNodeContextPath, targetNodeContextPath, position, draft); + }); break; } case actionTypes.MERGE: { @@ -404,26 +447,33 @@ export const reducer = (state: State = defaultState, action: InitAction | Action break; } case actionTypes.FOCUS: { - const {contextPath, fusionPath} = action.payload; - draft.focused.contextPath = contextPath; + const {contextPath, fusionPath, selectionMode} = action.payload; draft.focused.fusionPath = fusionPath; + const newFocusedNodes = calculateNewFocusedNodes(selectionMode, contextPath, draft.focused.contextPaths, draft.byContextPath); + if (newFocusedNodes) { + draft.focused.contextPaths = newFocusedNodes; + } break; } case actionTypes.UNFOCUS: { - draft.focused.contextPath = null; draft.focused.fusionPath = null; + draft.focused.contextPaths = []; break; } case actionTypes.COMMENCE_REMOVAL: { + draft.toBeRemoved = [action.payload]; + break; + } + case actionTypes.COMMENCE_REMOVAL_MULTIPLE: { draft.toBeRemoved = action.payload; break; } case actionTypes.REMOVAL_ABORTED: { - draft.toBeRemoved = null; + draft.toBeRemoved = []; break; } case actionTypes.REMOVAL_CONFIRMED: { - draft.toBeRemoved = null; + draft.toBeRemoved = []; break; } case actionTypes.REMOVE: { @@ -440,8 +490,8 @@ export const reducer = (state: State = defaultState, action: InitAction | Action // to different Document nodes and having a (content) node selected previously, the Inspector // does not properly refresh. We just need to ensure that everytime we switch pages, we // reset the focused (content) node of the page. - draft.focused.contextPath = null; draft.focused.fusionPath = null; + draft.focused.contextPaths = []; } break; } @@ -449,38 +499,64 @@ export const reducer = (state: State = defaultState, action: InitAction | Action const {siteNodeContextPath, documentNodeContextPath, nodes, merge} = action.payload; draft.siteNode = siteNodeContextPath; draft.documentNode = documentNodeContextPath; - draft.focused.contextPath = null; draft.focused.fusionPath = null; + draft.focused.contextPaths = []; if (nodes) { draft.byContextPath = merge ? defaultsDeep({}, draft.byContextPath, nodes) : nodes; } break; } case actionTypes.COPY: { + draft.clipboard = [action.payload]; + draft.clipboardMode = ClipboardMode.COPY; + break; + } + case actionTypes.COPY_MULTIPLE: { draft.clipboard = action.payload; draft.clipboardMode = ClipboardMode.COPY; break; } case actionTypes.CUT: { + draft.clipboard = [action.payload]; + draft.clipboardMode = ClipboardMode.MOVE; + break; + } + case actionTypes.CUT_MULTIPLE: { draft.clipboard = action.payload; draft.clipboardMode = ClipboardMode.MOVE; break; } case actionTypes.COMMIT_PASTE: { if (action.payload === ClipboardMode.MOVE) { - draft.clipboard = null; + draft.clipboard = []; draft.clipboardMode = null; } break; } case actionTypes.HIDE: { const node = getNodeOrThrow(draft.byContextPath, action.payload); - node.properties.hidden = true; + node.properties._hidden = true; + break; + } + case actionTypes.HIDE_MULTIPLE: { + const contextPaths = action.payload; + contextPaths.forEach(contextPath => { + const node = getNodeOrThrow(draft.byContextPath, contextPath); + node.properties._hidden = true; + }); break; } case actionTypes.SHOW: { const node = getNodeOrThrow(draft.byContextPath, action.payload); - node.properties.hidden = false; + node.properties._hidden = false; + break; + } + case actionTypes.SHOW_MULTIPLE: { + const contextPaths = action.payload; + contextPaths.forEach(contextPath => { + const node = getNodeOrThrow(draft.byContextPath, contextPath); + node.properties._hidden = false; + }); break; } case actionTypes.UPDATE_URI: { @@ -497,10 +573,10 @@ export const reducer = (state: State = defaultState, action: InitAction | Action (nodeUri.includes(oldUriFragment + '/') || nodeUri.includes(oldUriFragment + '@')) ) { const newNodeUri = nodeUri - // Node with changes uriPathSegment - .replace(oldUriFragment + '@', newUriFragment + '@') - // Descendant of a node with changed uriPathSegment - .replace(oldUriFragment + '/', newUriFragment + '/'); + // Node with changes uriPathSegment + .replace(oldUriFragment + '@', newUriFragment + '@') + // Descendant of a node with changed uriPathSegment + .replace(oldUriFragment + '/', newUriFragment + '/'); node.uri = newNodeUri; } }); diff --git a/packages/neos-ui-redux-store/src/CR/Nodes/selectors.ts b/packages/neos-ui-redux-store/src/CR/Nodes/selectors.ts index 092e16cb0c..75827d7020 100644 --- a/packages/neos-ui-redux-store/src/CR/Nodes/selectors.ts +++ b/packages/neos-ui-redux-store/src/CR/Nodes/selectors.ts @@ -8,9 +8,17 @@ export const inlineValidationErrorsSelector = (state: GlobalState) => $get(['cr' export const nodesByContextPathSelector = (state: GlobalState) => $get(['cr', 'nodes', 'byContextPath'], state); export const siteNodeContextPathSelector = (state: GlobalState) => $get(['cr', 'nodes', 'siteNode'], state); export const documentNodeContextPathSelector = (state: GlobalState) => $get(['cr', 'nodes', 'documentNode'], state); -// This is internal, as in most cases you want `focusedNodePathSelector`, which is able to fallback to documentNode, when no node is focused -const _focusedNodeContextPathSelector = (state: GlobalState) => $get(['cr', 'nodes', 'focused', 'contextPath'], state); +export const focusedNodePathsSelector = (state: GlobalState) => $get(['cr', 'nodes', 'focused', 'contextPaths'], state); +// This is internal, as in most cases you want `focusedNodePathSelector`, which is able to fallback to documentNode, when no node is focused +export const _focusedNodeContextPathSelector = createSelector( + [ + focusedNodePathsSelector, + ], + (focusedNodePaths) => { + return focusedNodePaths && focusedNodePaths[0] ? focusedNodePaths[0] : null; + } +); export const isDocumentNodeSelectedSelector = createSelector( [ _focusedNodeContextPathSelector, @@ -213,11 +221,13 @@ export const focusedGrandParentSelector = createSelector( } ); +export const clipboardNodesContextPathsSelector = (state: GlobalState) => $get(['cr', 'nodes', 'clipboard'], state); + export const clipboardNodeContextPathSelector = createSelector( [ - (state: GlobalState) => $get(['cr', 'nodes', 'clipboard'], state) + clipboardNodesContextPathsSelector ], - clipboardNodeContextPath => clipboardNodeContextPath + clipboardNodesContextPaths => Boolean(console.warn('This selector is deprecated, use "clipboardNodesContextPathsSelector" instead')) || (clipboardNodesContextPaths && clipboardNodesContextPaths[0]) ); export const clipboardIsEmptySelector = createSelector( @@ -372,6 +382,20 @@ export const destructiveOperationsAreDisabledSelector = createSelector( } ); +export const destructiveOperationsAreDisabledForContentTreeSelector = createSelector( + [ + siteNodeContextPathSelector, + focusedNodePathsSelector, + nodesByContextPathSelector + ], + (siteNodeContextPath, focusedNodesContextPaths, nodesByContextPath) => { + return focusedNodesContextPaths.map(contextPath => { + const node = nodesByContextPath[contextPath]; + return node && node.isAutoCreated || siteNodeContextPath === contextPath; + }).filter(Boolean).length > 0; + } +); + export const focusedNodeParentLineSelector = createSelector( [ focusedSelector, diff --git a/packages/neos-ui-redux-store/src/CR/index.ts b/packages/neos-ui-redux-store/src/CR/index.ts index 5328a4794f..2b535a5c28 100644 --- a/packages/neos-ui-redux-store/src/CR/index.ts +++ b/packages/neos-ui-redux-store/src/CR/index.ts @@ -1,4 +1,4 @@ -import {combineReducers} from 'redux'; +import {combineReducers} from '@neos-project/neos-ui-redux-store/src/combineReducers'; import * as ContentDimensions from '@neos-project/neos-ui-redux-store/src/CR/ContentDimensions'; diff --git a/packages/neos-ui-redux-store/src/UI/ContentCanvas/index.ts b/packages/neos-ui-redux-store/src/UI/ContentCanvas/index.ts index 18e3e5252d..9b54e28a20 100644 --- a/packages/neos-ui-redux-store/src/UI/ContentCanvas/index.ts +++ b/packages/neos-ui-redux-store/src/UI/ContentCanvas/index.ts @@ -51,7 +51,7 @@ export enum actionTypes { } const setPreviewUrl = (previewUrl: string) => createAction(actionTypes.SET_PREVIEW_URL, previewUrl); -const setSrc = (src: string, openInNewWindow: boolean = false) => createAction(actionTypes.SET_SRC, {src, openInNewWindow}); +const setSrc = (src: string, metaKeyPressed: boolean = false) => createAction(actionTypes.SET_SRC, {src, metaKeyPressed}); const setFormattingUnderCursor = (formatting: Formatting) => createAction(actionTypes.FORMATTING_UNDER_CURSOR, formatting); const setCurrentlyEditedPropertyName = (propertyName: string) => createAction(actionTypes.SET_CURRENTLY_EDITED_PROPERTY_NAME, propertyName); const startLoading = () => createAction(actionTypes.START_LOADING); diff --git a/packages/neos-ui-redux-store/src/UI/InsertionModeModal/index.ts b/packages/neos-ui-redux-store/src/UI/InsertionModeModal/index.ts index f8b1944800..cb45f656cd 100644 --- a/packages/neos-ui-redux-store/src/UI/InsertionModeModal/index.ts +++ b/packages/neos-ui-redux-store/src/UI/InsertionModeModal/index.ts @@ -10,7 +10,7 @@ import {NodeContextPath} from '@neos-project/neos-ts-interfaces'; export interface State extends Readonly<{ isOpen: boolean; - subjectContextPath: NodeContextPath | null; + subjectContextPaths: NodeContextPath[]; referenceContextPath: NodeContextPath | null; enableAlongsideModes: boolean; enableIntoMode: boolean; @@ -19,7 +19,7 @@ export interface State extends Readonly<{ export const defaultState: State = { isOpen: false, - subjectContextPath: null, + subjectContextPaths: [], referenceContextPath: null, enableAlongsideModes: false, enableIntoMode: false, @@ -36,13 +36,13 @@ export enum actionTypes { } const open = ( - subjectContextPath: NodeContextPath, + subjectContextPaths: NodeContextPath[], referenceContextPath: NodeContextPath, enableAlongsideModes: boolean, enableIntoMode: boolean, operationType: string ) => createAction(actionTypes.OPEN, { - subjectContextPath, + subjectContextPaths, referenceContextPath, enableAlongsideModes, enableIntoMode, @@ -69,7 +69,7 @@ export const reducer = (state: State = defaultState, action: InitAction | Action switch (action.type) { case actionTypes.OPEN: { draft.isOpen = true; - draft.subjectContextPath = action.payload.subjectContextPath; + draft.subjectContextPaths = action.payload.subjectContextPaths; draft.referenceContextPath = action.payload.referenceContextPath; draft.enableAlongsideModes = action.payload.enableAlongsideModes; draft.enableIntoMode = action.payload.enableIntoMode; @@ -78,7 +78,7 @@ export const reducer = (state: State = defaultState, action: InitAction | Action } case actionTypes.CANCEL: { draft.isOpen = false; - draft.subjectContextPath = null; + draft.subjectContextPaths = []; draft.referenceContextPath = null; draft.enableAlongsideModes = false; draft.enableIntoMode = false; @@ -86,7 +86,7 @@ export const reducer = (state: State = defaultState, action: InitAction | Action } case actionTypes.APPLY: { draft.isOpen = false; - draft.subjectContextPath = null; + draft.subjectContextPaths = []; draft.referenceContextPath = null; draft.enableAlongsideModes = false; draft.enableIntoMode = false; diff --git a/packages/neos-ui-redux-store/src/UI/Inspector/selectors.spec.js b/packages/neos-ui-redux-store/src/UI/Inspector/selectors.spec.js index af4c7c7503..388128413e 100644 --- a/packages/neos-ui-redux-store/src/UI/Inspector/selectors.spec.js +++ b/packages/neos-ui-redux-store/src/UI/Inspector/selectors.spec.js @@ -4,7 +4,7 @@ import * as selectors from './selectors'; test(`transientValues should return the transient inspector values for the currently focused node`, () => { const state = $all( - $set('cr.nodes.focused.contextPath', 'dummyContextPath'), + $set('cr.nodes.focused.contextPaths', ['dummyContextPath']), $set('ui.inspector.valuesByNodePath.dummyContextPath', {some: 'transientValue'}), {} ); @@ -16,7 +16,7 @@ test(`transientValues should return the transient inspector values for the curre test(`Inspector is dirty when transient values are set`, () => { const state = $all( - $set('cr.nodes.focused.contextPath', 'dummyContextPath'), + $set('cr.nodes.focused.contextPaths', ['dummyContextPath']), $set('ui.inspector.valuesByNodePath.dummyContextPath', {some: 'transientValue'}), {} ); @@ -26,7 +26,7 @@ test(`Inspector is dirty when transient values are set`, () => { test(`Inspector is not dirty when no transient values are set`, () => { const state = $all( - $set('cr.nodes.focused.contextPath', 'dummyContextPath'), + $set('cr.nodes.focused.contextPaths', ['dummyContextPath']), $set('ui.inspector.valuesByNodePath.dummyContextPath', {}), {} ); @@ -42,7 +42,7 @@ test(`Inspector is not dirty when no transient values are set`, () => { test(`Inspector is not dirty when no transient values have been dropped`, () => { const state = $all( - $set('cr.nodes.focused.contextPath', 'dummyContextPath'), + $set('cr.nodes.focused.contextPaths', ['dummyContextPath']), $set('ui.inspector.valuesByNodePath.dummyContextPath', {some: 'transientValue'}), {} ); @@ -64,7 +64,7 @@ test(`validationErrorsSelector should return null, when there's no validator con const validationErrorsSelector = selectors.makeValidationErrorsSelector(nodeTypesRegistry, validatorRegistry); const state = $all( $set('ui.inspector.valuesByNodePath', {}), - $set('cr.nodes.focused.contextPath', 'dummyContextPath'), + $set('cr.nodes.focused.contextPaths', ['dummyContextPath']), $set('cr.nodes.byContextPath.dummyContextPath.properties', { title: 'Foo' }), @@ -85,7 +85,7 @@ test(`validationErrorsSelector should read the nodeType configuration for the cu const validationErrorsSelector = selectors.makeValidationErrorsSelector(nodeTypesRegistry, validatorRegistry); const state = $all( $set('ui.inspector.valuesByNodePath', {}), - $set('cr.nodes.focused.contextPath', 'dummyContextPath'), + $set('cr.nodes.focused.contextPaths', ['dummyContextPath']), $set('cr.nodes.byContextPath.dummyContextPath.nodeType', 'DummyNodeType'), {} ); @@ -110,7 +110,7 @@ test(`validationErrorsSelector should return validationErrors, when there are in const validationErrorsSelector = selectors.makeValidationErrorsSelector(nodeTypesRegistry, validatorRegistry); const state = $all( $set('ui.inspector.valuesByNodePath', {}), - $set('cr.nodes.focused.contextPath', 'dummyContextPath'), + $set('cr.nodes.focused.contextPaths', ['dummyContextPath']), $set('cr.nodes.byContextPath.dummyContextPath.properties', { title: '', label: '' diff --git a/packages/neos-ui-redux-store/src/UI/PageTree/index.spec.js b/packages/neos-ui-redux-store/src/UI/PageTree/index.spec.js index 5931567e67..4fe92ba8d2 100644 --- a/packages/neos-ui-redux-store/src/UI/PageTree/index.spec.js +++ b/packages/neos-ui-redux-store/src/UI/PageTree/index.spec.js @@ -1,4 +1,5 @@ import {actionTypes, actions, reducer} from './index'; +import {SelectionModeTypes} from '@neos-project/neos-ts-interfaces'; import {actionTypes as system} from '../../System/index'; @@ -43,9 +44,25 @@ test(`The reducer should return a plain JS object as the initial state.`, () => }); test(`The "focus" action should set the focused node context path.`, () => { - const nextState = reducer(undefined, actions.focus('someOtherContextPath')); - - expect(nextState.isFocused).toBe('someOtherContextPath'); + const globalState = { + ui: { + focused: [], + toggled: [], + hidden: [], + intermediate: [], + loading: [], + errors: [] + }, + cr: { + nodes: { + siteNode: 'siteNode', + documentNode: 'documentNode', + byContextPath: [] + } + } + }; + const nextState = reducer(globalState.ui, actions.focus('someOtherContextPath', undefined, SelectionModeTypes.SINGLE_SELECT), globalState); + expect(nextState.focused).toEqual(['someOtherContextPath']); }); test(`The "invalidate" action should remove the given node from toggled state`, () => { diff --git a/packages/neos-ui-redux-store/src/UI/PageTree/index.ts b/packages/neos-ui-redux-store/src/UI/PageTree/index.ts index 9cc6086d25..0d7e95d354 100644 --- a/packages/neos-ui-redux-store/src/UI/PageTree/index.ts +++ b/packages/neos-ui-redux-store/src/UI/PageTree/index.ts @@ -1,13 +1,14 @@ import produce from 'immer'; import {action as createAction, ActionType} from 'typesafe-actions'; -import {actionTypes as system, InitAction} from '@neos-project/neos-ui-redux-store/src/System'; -import {NodeContextPath} from '@neos-project/neos-ts-interfaces'; +import {actionTypes as system, InitAction, GlobalState} from '@neos-project/neos-ui-redux-store/src/System'; +import {NodeContextPath, SelectionModeTypes} from '@neos-project/neos-ts-interfaces'; import * as selectors from './selectors'; +import {calculateNewFocusedNodes} from '../../CR/Nodes/helpers'; export interface State extends Readonly<{ - isFocused: NodeContextPath | null; + focused: NodeContextPath[]; toggled: NodeContextPath[]; hidden: NodeContextPath[]; intermediate: NodeContextPath[]; @@ -16,7 +17,7 @@ export interface State extends Readonly<{ }> {} export const defaultState: State = { - isFocused: null, + focused: [], toggled: [], hidden: [], intermediate: [], @@ -38,7 +39,7 @@ export enum actionTypes { SET_SEARCH_RESULT = '@neos/neos-ui/UI/PageTree/SET_SEARCH_RESULT' } -const focus = (contextPath: NodeContextPath) => createAction(actionTypes.FOCUS, {contextPath}); +const focus = (contextPath: NodeContextPath, _: undefined, selectionMode: SelectionModeTypes = SelectionModeTypes.SINGLE_SELECT) => createAction(actionTypes.FOCUS, {contextPath, selectionMode}); const toggle = (contextPath: NodeContextPath) => createAction(actionTypes.TOGGLE, {contextPath}); const invalidate = (contextPath: NodeContextPath) => createAction(actionTypes.INVALIDATE, {contextPath}); const requestChildren = (contextPath: NodeContextPath, {unCollapse = true, activate = false} = {}) => createAction(actionTypes.REQUEST_CHILDREN, {contextPath, opts: {unCollapse, activate}}); @@ -75,14 +76,19 @@ export type Action = ActionType; // // Export the reducer // -export const reducer = (state: State = defaultState, action: InitAction | Action) => produce(state, draft => { +export const reducer = (state: State = defaultState, action: InitAction | Action, globalState: GlobalState) => produce(state, draft => { switch (action.type) { case system.INIT: { - draft.isFocused = action.payload.cr.nodes.documentNode || action.payload.cr.nodes.siteNode || null; + const contextPath = action.payload.cr.nodes.documentNode || action.payload.cr.nodes.siteNode; + draft.focused = contextPath ? [contextPath] : []; break; } case actionTypes.FOCUS: { - draft.isFocused = action.payload.contextPath; + const {contextPath, selectionMode} = action.payload; + const newFocusedNodes = calculateNewFocusedNodes(selectionMode, contextPath, draft.focused, globalState.cr.nodes.byContextPath); + if (newFocusedNodes) { + draft.focused = newFocusedNodes; + } break; } case actionTypes.TOGGLE: { diff --git a/packages/neos-ui-redux-store/src/UI/PageTree/selectors.ts b/packages/neos-ui-redux-store/src/UI/PageTree/selectors.ts index 2b36b9b7a5..1c3bcc91e6 100644 --- a/packages/neos-ui-redux-store/src/UI/PageTree/selectors.ts +++ b/packages/neos-ui-redux-store/src/UI/PageTree/selectors.ts @@ -2,16 +2,30 @@ import {$get} from 'plow-js'; import {GlobalState} from '@neos-project/neos-ui-redux-store/src/System'; import {createSelector} from 'reselect'; -import {siteNodeSelector, nodesByContextPathSelector} from '@neos-project/neos-ui-redux-store/src/CR/Nodes/selectors'; +import {siteNodeContextPathSelector, siteNodeSelector, nodesByContextPathSelector} from '@neos-project/neos-ui-redux-store/src/CR/Nodes/selectors'; import {isNodeCollapsed} from '@neos-project/neos-ui-redux-store/src/CR/Nodes/helpers'; -export const getFocused = (state: GlobalState) => $get(['ui', 'pageTree', 'isFocused'], state); +export const getAllFocused = (state: GlobalState) => $get(['ui', 'pageTree', 'focused'], state); +export const getFocused = (state: GlobalState) => { + const focused = getAllFocused(state); + return focused && focused[0] ? focused[0] : null; +}; export const getToggled = (state: GlobalState) => $get(['ui', 'pageTree', 'toggled'], state); export const getLoading = (state: GlobalState) => $get(['ui', 'pageTree', 'loading'], state); export const getErrors = (state: GlobalState) => $get(['ui', 'pageTree', 'errors'], state); export const getHidden = (state: GlobalState) => $get(['ui', 'pageTree', 'hidden'], state); export const getIntermediate = (state: GlobalState) => $get(['ui', 'pageTree', 'intermediate'], state); +export const destructiveOperationsAreDisabledForPageTreeSelector = createSelector( + [ + siteNodeContextPathSelector, + getAllFocused + ], + (siteNodeContextPath, focusedNodesContextPaths) => { + return [...focusedNodesContextPaths].map(contextPath => siteNodeContextPath === contextPath).filter(Boolean).length > 0; + } +); + export const getIsLoading = createSelector( [ getLoading diff --git a/packages/neos-ui-redux-store/src/UI/index.ts b/packages/neos-ui-redux-store/src/UI/index.ts index d202ffee8a..0d0428f47f 100644 --- a/packages/neos-ui-redux-store/src/UI/index.ts +++ b/packages/neos-ui-redux-store/src/UI/index.ts @@ -1,4 +1,4 @@ -import {combineReducers} from 'redux'; +import {combineReducers} from '@neos-project/neos-ui-redux-store/src/combineReducers'; import * as FlashMessages from '@neos-project/neos-ui-redux-store/src/UI/FlashMessages'; import * as FullScreen from '@neos-project/neos-ui-redux-store/src/UI/FullScreen'; diff --git a/packages/neos-ui-redux-store/src/User/index.ts b/packages/neos-ui-redux-store/src/User/index.ts index cb29e87b91..708899f75e 100644 --- a/packages/neos-ui-redux-store/src/User/index.ts +++ b/packages/neos-ui-redux-store/src/User/index.ts @@ -1,4 +1,4 @@ -import {combineReducers} from 'redux'; +import {combineReducers} from '@neos-project/neos-ui-redux-store/src/combineReducers'; import * as Settings from '@neos-project/neos-ui-redux-store/src/User/Settings'; import * as Preferences from '@neos-project/neos-ui-redux-store/src/User/Preferences'; diff --git a/packages/neos-ui-redux-store/src/combineReducers.ts b/packages/neos-ui-redux-store/src/combineReducers.ts new file mode 100644 index 0000000000..7245abc27c --- /dev/null +++ b/packages/neos-ui-redux-store/src/combineReducers.ts @@ -0,0 +1,41 @@ +// This is a custom combineReducers implementation that also passes the original global state alongside + +type FunctionReturnType = T extends (...args: any[]) => infer R ? R : never; + +type CombinedState = { [K in keyof T]: FunctionReturnType }; + +type ActionFromReducer = T[keyof T] extends ( + state: any, + action: infer A, + ...args: any[] +) => any + ? A + : never; + +interface Reducers { + [ key: string]: any; +} + +type ReducerReturnValueRoot = ( + state: CombinedState, + action: ActionFromReducer, + globalState: any +) => CombinedState; + +export function combineReducers(reducers: T): ReducerReturnValueRoot { + type S = CombinedState; + const reducerProps = Object.keys(reducers); + + return (state: S = {} as S, action: any, globalState: any): S => { + let hasChanged = false; + const nextState: S = {} as S; + for (const prop of reducerProps) { + const reducer = reducers[prop]; + const previousStateForKey = state[prop]; + const nextStateForKey = reducer(previousStateForKey, action, globalState || state); + nextState[prop] = nextStateForKey; + hasChanged = hasChanged || nextStateForKey !== previousStateForKey; + } + return hasChanged ? nextState : state; + }; +} diff --git a/packages/neos-ui-redux-store/src/index.ts b/packages/neos-ui-redux-store/src/index.ts index ff93db3057..32d09af504 100644 --- a/packages/neos-ui-redux-store/src/index.ts +++ b/packages/neos-ui-redux-store/src/index.ts @@ -1,4 +1,4 @@ -import {combineReducers} from 'redux'; +import {combineReducers} from '@neos-project/neos-ui-redux-store/src/combineReducers'; import * as Changes from '@neos-project/neos-ui-redux-store/src/Changes'; import * as CR from '@neos-project/neos-ui-redux-store/src/CR'; @@ -32,7 +32,7 @@ export const reducer = combineReducers({ ui: UI.reducer, user: User.reducer, // NOTE: The plugins reducer is UNPLANNED EXTENSIBILITY, do not modify unless you know what you are doing! - plugins: (state) => state || {} + plugins: (state: System.GlobalState) => state || {} }); // diff --git a/packages/neos-ui-sagas/src/CR/NodeOperations/determineInsertMode.js b/packages/neos-ui-sagas/src/CR/NodeOperations/determineInsertMode.js index e91e06cc88..19ffefc538 100644 --- a/packages/neos-ui-sagas/src/CR/NodeOperations/determineInsertMode.js +++ b/packages/neos-ui-sagas/src/CR/NodeOperations/determineInsertMode.js @@ -2,13 +2,13 @@ import {take, put, race} from 'redux-saga/effects'; import {actions, actionTypes} from '@neos-project/neos-ui-redux-store'; -export default function * determineInsertMode(subjectContextPath, referenceContextPath, canBeInsertedAlongside, canBeInsertedInto, operation) { // eslint-disable-line max-params +export default function * determineInsertMode(subjectContextPaths, referenceContextPath, canBeInsertedAlongside, canBeInsertedInto, operation) { // eslint-disable-line max-params if (canBeInsertedInto && !canBeInsertedAlongside) { return 'into'; } yield put(actions.UI.InsertionModeModal.open( - subjectContextPath, + subjectContextPaths, referenceContextPath, canBeInsertedAlongside, canBeInsertedInto, diff --git a/packages/neos-ui-sagas/src/CR/NodeOperations/hideNode.js b/packages/neos-ui-sagas/src/CR/NodeOperations/hideNode.js index f3fbaf204c..314ad6c2cf 100644 --- a/packages/neos-ui-sagas/src/CR/NodeOperations/hideNode.js +++ b/packages/neos-ui-sagas/src/CR/NodeOperations/hideNode.js @@ -19,4 +19,19 @@ export default function * hideNode() { } }])); }); + yield takeLatest(actionTypes.CR.Nodes.HIDE_MULTIPLE, function * performPropertyChange(action) { + const contextPaths = action.payload; + const changes = [...contextPaths].map(contextPath => { + markNodeAsHidden(contextPath); + return { + type: 'Neos.Neos.Ui:Property', + subject: contextPath, + payload: { + propertyName: '_hidden', + value: true + } + }; + }); + yield put(actions.Changes.persistChanges(changes)); + }); } diff --git a/packages/neos-ui-sagas/src/CR/NodeOperations/index.js b/packages/neos-ui-sagas/src/CR/NodeOperations/index.js index 50688508f1..c3e5151dc7 100644 --- a/packages/neos-ui-sagas/src/CR/NodeOperations/index.js +++ b/packages/neos-ui-sagas/src/CR/NodeOperations/index.js @@ -2,6 +2,7 @@ import addNode from './addNode'; import removeNodeIfConfirmed from './removeNodeIfConfirmed'; import pasteNode from './pasteNode'; import moveDroppedNode from './moveDroppedNode'; +import moveDroppedNodes from './moveDroppedNodes'; import hideNode from './hideNode'; import showNode from './showNode'; import reloadState from './reloadState'; @@ -11,6 +12,7 @@ export { removeNodeIfConfirmed, pasteNode, moveDroppedNode, + moveDroppedNodes, hideNode, showNode, reloadState diff --git a/packages/neos-ui-sagas/src/CR/NodeOperations/moveDroppedNodes.js b/packages/neos-ui-sagas/src/CR/NodeOperations/moveDroppedNodes.js new file mode 100644 index 0000000000..c6531713ac --- /dev/null +++ b/packages/neos-ui-sagas/src/CR/NodeOperations/moveDroppedNodes.js @@ -0,0 +1,21 @@ +import {takeEvery, put} from 'redux-saga/effects'; + +import {actions, actionTypes} from '@neos-project/neos-ui-redux-store'; + +import {calculateChangeTypeFromMode, calculateDomAddressesFromMode} from './helpers'; + +export default function * moveDroppedNodes() { + yield takeEvery(actionTypes.CR.Nodes.MOVE_MULTIPLE, function * handleNodeMove({payload}) { + const {nodesToBeMoved, targetNode: reference, position} = payload; + const changes = nodesToBeMoved.map(subject => ({ + type: calculateChangeTypeFromMode(position, 'Move'), + subject, + payload: calculateDomAddressesFromMode( + position, + reference + ) + })); + + yield put(actions.Changes.persistChanges(changes)); + }); +} diff --git a/packages/neos-ui-sagas/src/CR/NodeOperations/pasteNode.js b/packages/neos-ui-sagas/src/CR/NodeOperations/pasteNode.js index 411e5a944b..9731e8e04d 100644 --- a/packages/neos-ui-sagas/src/CR/NodeOperations/pasteNode.js +++ b/packages/neos-ui-sagas/src/CR/NodeOperations/pasteNode.js @@ -12,12 +12,19 @@ export default function * pasteNode({globalRegistry}) { const canBeInsertedIntoSelector = selectors.CR.Nodes.makeCanBeCopiedIntoSelector(nodeTypesRegistry); yield takeEvery(actionTypes.CR.Nodes.PASTE, function * waitForPaste(action) { - const subject = yield select($get('cr.nodes.clipboard')); + const subject = yield select(selectors.CR.Nodes.clipboardNodesContextPathsSelector); const clipboardMode = yield select($get('cr.nodes.clipboardMode')); const {contextPath: reference, fusionPath} = action.payload; - const canBeInsertedAlongside = yield select(canBeInsertedAlongsideSelector, {subject, reference}); - const canBeInsertedInto = yield select(canBeInsertedIntoSelector, {subject, reference}); + const state = yield select(); + const canBeInsertedAlongside = subject.every(contextPath => { + const result = canBeInsertedAlongsideSelector(state, {subject: contextPath, reference}); + return result; + }); + const canBeInsertedInto = subject.every(contextPath => { + const result = canBeInsertedIntoSelector(state, {subject: contextPath, reference}); + return result; + }); const mode = yield call( determineInsertMode, @@ -30,11 +37,12 @@ export default function * pasteNode({globalRegistry}) { if (mode) { yield put(actions.CR.Nodes.commitPaste(clipboardMode)); - yield put(actions.Changes.persistChanges([{ + const changes = subject.map(contextPath => ({ type: calculateChangeTypeFromMode(mode, clipboardMode), - subject, + subject: contextPath, payload: calculateDomAddressesFromMode(mode, reference, fusionPath) - }])); + })); + yield put(actions.Changes.persistChanges(changes)); } }); } diff --git a/packages/neos-ui-sagas/src/CR/NodeOperations/reloadState.js b/packages/neos-ui-sagas/src/CR/NodeOperations/reloadState.js index b9328b1579..4824f078ef 100644 --- a/packages/neos-ui-sagas/src/CR/NodeOperations/reloadState.js +++ b/packages/neos-ui-sagas/src/CR/NodeOperations/reloadState.js @@ -2,13 +2,13 @@ import {takeLatest, put, select} from 'redux-saga/effects'; import {$get} from 'plow-js'; import backend from '@neos-project/neos-ui-backend-connector'; -import {actions, actionTypes} from '@neos-project/neos-ui-redux-store'; +import {selectors, actions, actionTypes} from '@neos-project/neos-ui-redux-store'; export default function * watchReloadState({configuration}) { yield takeLatest(actionTypes.CR.Nodes.RELOAD_STATE, function * reloadState(action) { const {q} = backend.get(); const currentSiteNodeContextPath = yield select($get('cr.nodes.siteNode')); - const clipboardNodeContextPath = yield select($get('cr.nodes.clipboard')); + const clipboardNodesContextPaths = yield select(selectors.CR.Nodes.clipboardNodesContextPathsSelector); const toggledNodes = yield select($get('ui.pageTree.toggled')); const siteNodeContextPath = $get('payload.siteNodeContextPath', action) || currentSiteNodeContextPath; const documentNodeContextPath = yield $get('payload.documentNodeContextPath', action) || select($get('cr.nodes.documentNode')); @@ -17,7 +17,7 @@ export default function * watchReloadState({configuration}) { configuration.nodeTree.presets.default.baseNodeType, configuration.nodeTree.loadingDepth, toggledNodes, - clipboardNodeContextPath + clipboardNodesContextPaths ).getForTree(); const nodeMap = nodes.reduce((nodeMap, node) => { nodeMap[$get('contextPath', node)] = node; diff --git a/packages/neos-ui-sagas/src/CR/NodeOperations/removeNodeIfConfirmed.js b/packages/neos-ui-sagas/src/CR/NodeOperations/removeNodeIfConfirmed.js index a297bca0e9..90751184a8 100644 --- a/packages/neos-ui-sagas/src/CR/NodeOperations/removeNodeIfConfirmed.js +++ b/packages/neos-ui-sagas/src/CR/NodeOperations/removeNodeIfConfirmed.js @@ -4,7 +4,7 @@ import {$get} from 'plow-js'; import {actions, actionTypes} from '@neos-project/neos-ui-redux-store'; export default function * removeNodeIfConfirmed() { - yield takeLatest(actionTypes.CR.Nodes.COMMENCE_REMOVAL, function * waitForConfirmation() { + yield takeLatest([actionTypes.CR.Nodes.COMMENCE_REMOVAL, actionTypes.CR.Nodes.COMMENCE_REMOVAL_MULTIPLE], function * waitForConfirmation() { const state = yield select(); const waitForNextAction = yield race([ take(actionTypes.CR.Nodes.REMOVAL_ABORTED), @@ -17,12 +17,13 @@ export default function * removeNodeIfConfirmed() { } if (nextAction.type === actionTypes.CR.Nodes.REMOVAL_CONFIRMED) { - const nodeToBeRemovedContextPath = $get('cr.nodes.toBeRemoved', state); - - yield put(actions.Changes.persistChanges([{ + const nodesToBeRemovedContextPath = $get('cr.nodes.toBeRemoved', state); + const changes = nodesToBeRemovedContextPath.map(nodeToBeRemovedContextPath => ({ type: 'Neos.Neos.Ui:RemoveNode', subject: nodeToBeRemovedContextPath - }])); + })); + + yield put(actions.Changes.persistChanges(changes)); } }); } diff --git a/packages/neos-ui-sagas/src/CR/NodeOperations/showNode.js b/packages/neos-ui-sagas/src/CR/NodeOperations/showNode.js index 88017e87d3..fae2f3ea14 100644 --- a/packages/neos-ui-sagas/src/CR/NodeOperations/showNode.js +++ b/packages/neos-ui-sagas/src/CR/NodeOperations/showNode.js @@ -19,4 +19,19 @@ export default function * showNode() { } }])); }); + yield takeLatest(actionTypes.CR.Nodes.SHOW_MULTIPLE, function * performPropertyChange(action) { + const contextPaths = action.payload; + const changes = [...contextPaths].map(contextPath => { + markNodeAsVisible(contextPath); + return { + type: 'Neos.Neos.Ui:Property', + subject: contextPath, + payload: { + propertyName: '_hidden', + value: false + } + }; + }); + yield put(actions.Changes.persistChanges(changes)); + }); } diff --git a/packages/neos-ui-sagas/src/manifest.js b/packages/neos-ui-sagas/src/manifest.js index de2dae311c..08bc14da7e 100644 --- a/packages/neos-ui-sagas/src/manifest.js +++ b/packages/neos-ui-sagas/src/manifest.js @@ -39,6 +39,7 @@ manifest('main.sagas', {}, globalRegistry => { sagasRegistry.set('neos-ui/CR/NodeOperations/addNode', {saga: crNodeOperations.addNode}); sagasRegistry.set('neos-ui/CR/NodeOperations/pasteNode', {saga: crNodeOperations.pasteNode}); sagasRegistry.set('neos-ui/CR/NodeOperations/moveDroppedNode', {saga: crNodeOperations.moveDroppedNode}); + sagasRegistry.set('neos-ui/CR/NodeOperations/moveDroppedNodes', {saga: crNodeOperations.moveDroppedNodes}); sagasRegistry.set('neos-ui/CR/NodeOperations/hideNode', {saga: crNodeOperations.hideNode}); sagasRegistry.set('neos-ui/CR/NodeOperations/showNode', {saga: crNodeOperations.showNode}); sagasRegistry.set('neos-ui/CR/NodeOperations/removeNodeIfConfirmed', {saga: crNodeOperations.removeNodeIfConfirmed}); diff --git a/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/Node/index.js b/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/Node/index.js index 4eaa74cc62..ff1f0db36b 100644 --- a/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/Node/index.js +++ b/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/Node/index.js @@ -12,6 +12,8 @@ import {selectors} from '@neos-project/neos-ui-redux-store'; import {isNodeCollapsed} from '@neos-project/neos-ui-redux-store/src/CR/Nodes/helpers'; import {neos} from '@neos-project/neos-ui-decorators'; +import {hasNestedNodes} from '@neos-project/neos-ui/src/Containers/LeftSideBar/NodeTree/helpers'; + import animate from 'amator'; import hashSum from 'hash-sum'; import moment from 'moment'; @@ -52,7 +54,7 @@ export default class Node extends PureComponent { node: PropTypes.object, nodeDndType: PropTypes.string.isRequired, nodeTypeRole: PropTypes.string, - currentlyDraggedNode: PropTypes.object, + currentlyDraggedNodes: PropTypes.array, hasChildren: PropTypes.bool, isLastChild: PropTypes.bool, childNodes: PropTypes.array, @@ -129,10 +131,10 @@ export default class Node extends PureComponent { } accepts = mode => { - const {node, currentlyDraggedNode, canBeInsertedAlongside, canBeInsertedInto} = this.props; + const {node, currentlyDraggedNodes, canBeInsertedAlongside, canBeInsertedInto} = this.props; const canBeInserted = mode === 'into' ? canBeInsertedInto : canBeInsertedAlongside; - return canBeInserted && (getContextPath(currentlyDraggedNode) !== getContextPath(node)); + return canBeInserted && !currentlyDraggedNodes.includes(getContextPath(node)); } handleNodeDrag = () => { @@ -217,30 +219,30 @@ export default class Node extends PureComponent { isCollapsed() { const {node, toggledNodeContextPaths, rootNode, loadingDepth} = this.props; - const isToggled = toggledNodeContextPaths.includes($get('contextPath', node)); + const isToggled = toggledNodeContextPaths.includes(node.contextPath); return isNodeCollapsed(node, isToggled, rootNode, loadingDepth); } isHidden() { const {node, hiddenContextPaths} = this.props; - return hiddenContextPaths && hiddenContextPaths.includes($get('contextPath', node)); + return hiddenContextPaths && hiddenContextPaths.includes(node.contextPath); } isIntermediate() { const {node, intermediateContextPaths} = this.props; - return intermediateContextPaths && intermediateContextPaths.includes($get('contextPath', node)); + return intermediateContextPaths && intermediateContextPaths.includes(node.contextPath); } isLoading() { const {node, loadingNodeContextPaths} = this.props; - return loadingNodeContextPaths ? loadingNodeContextPaths.includes($get('contextPath', node)) : false; + return loadingNodeContextPaths ? loadingNodeContextPaths.includes(node.contextPath) : false; } hasError() { const {node, errorNodeContextPaths} = this.props; - return errorNodeContextPaths ? errorNodeContextPaths.includes($get('contextPath', node)) : false; + return errorNodeContextPaths ? errorNodeContextPaths.includes(node.contextPath) : false; } getDragAndDropContext() { @@ -266,8 +268,9 @@ export default class Node extends PureComponent { onNodeFocus, onNodeDrag, onNodeDrop, - currentlyDraggedNode, - isContentTreeNode + currentlyDraggedNodes, + isContentTreeNode, + focusedNodesContextPaths } = this.props; if (this.isHidden()) { @@ -278,16 +281,19 @@ export default class Node extends PureComponent { }; const childNodesCount = childNodes.length; - const labelIdentifier = (isContentTreeNode ? 'content-' : '') + 'treeitem-' + hashSum($get('contextPath', node)) + '-label'; + const labelIdentifier = (isContentTreeNode ? 'content-' : '') + 'treeitem-' + hashSum(node.contextPath) + '-label'; const labelTitle = decodeLabel($get('label', node)) + ' (' + this.getNodeTypeLabel() + ')'; + // Autocreated or we have nested nodes and the node that we are dragging belongs to the selection + const dragForbidden = node.isAutoCreated || (hasNestedNodes(focusedNodesContextPaths) && focusedNodesContextPaths.includes(node.contextPath)); + return ( {this.isCollapsed() ? null : ( @@ -315,7 +321,7 @@ export default class Node extends PureComponent { {childNodes.filter(n => n).map((node, index) => @@ -337,14 +343,16 @@ export default class Node extends PureComponent { handleNodeToggle = () => { const {node, onNodeToggle} = this.props; - onNodeToggle($get('contextPath', node)); + onNodeToggle(node.contextPath); } handleNodeClick = e => { const {node, onNodeFocus, onNodeClick} = this.props; - const openInNewWindow = e.metaKey || e.shiftKey || e.ctrlKey; - onNodeFocus($get('contextPath', node), openInNewWindow); - onNodeClick($get('uri', node), $get('contextPath', node), openInNewWindow); + const metaKeyPressed = e.metaKey || e.ctrlKey; + const shiftKeyPressed = e.shiftKey; + const altKeyPressed = e.altKey; + onNodeFocus(node.contextPath, metaKeyPressed, altKeyPressed, shiftKeyPressed); + onNodeClick($get('uri', node), node.contextPath, metaKeyPressed, altKeyPressed, shiftKeyPressed); } } @@ -363,29 +371,34 @@ export const PageTreeNode = withNodeTypeRegistryAndI18nRegistry(connect( const canBeMovedIntoSelector = selectors.CR.Nodes.makeCanBeMovedIntoSelector(nodeTypesRegistry); const isDocumentNodeDirtySelector = selectors.CR.Workspaces.makeIsDocumentNodeDirtySelector(); - return (state, {node, currentlyDraggedNode}) => ({ - isContentTreeNode: false, - rootNode: selectors.CR.Nodes.siteNodeSelector(state), - loadingDepth: neos.configuration.nodeTree.loadingDepth, - childNodes: childrenOfSelector(state, getContextPath(node)), - hasChildren: hasChildrenSelector(state, getContextPath(node)), - isActive: selectors.CR.Nodes.documentNodeContextPathSelector(state) === $get('contextPath', node), - isFocused: selectors.UI.PageTree.getFocused(state) === $get('contextPath', node), - toggledNodeContextPaths: selectors.UI.PageTree.getToggled(state), - hiddenContextPaths: selectors.UI.PageTree.getHidden(state), - intermediateContextPaths: selectors.UI.PageTree.getIntermediate(state), - loadingNodeContextPaths: selectors.UI.PageTree.getLoading(state), - errorNodeContextPaths: selectors.UI.PageTree.getErrors(state), - isNodeDirty: isDocumentNodeDirtySelector(state, $get('contextPath', node)), - canBeInsertedAlongside: canBeMovedAlongsideSelector(state, { - subject: getContextPath(currentlyDraggedNode), + return (state, {node, currentlyDraggedNodes}) => { + const canBeInsertedAlongside = currentlyDraggedNodes.every(draggedNodeContextPath => canBeMovedAlongsideSelector(state, { + subject: draggedNodeContextPath, reference: getContextPath(node) - }), - canBeInsertedInto: canBeMovedIntoSelector(state, { - subject: getContextPath(currentlyDraggedNode), + })); + const canBeInsertedInto = currentlyDraggedNodes.every(draggedNodeContextPath => canBeMovedIntoSelector(state, { + subject: draggedNodeContextPath, reference: getContextPath(node) - }) - }); + })); + return ({ + isContentTreeNode: false, + focusedNodesContextPaths: selectors.UI.PageTree.getAllFocused(state), + rootNode: selectors.CR.Nodes.siteNodeSelector(state), + loadingDepth: neos.configuration.nodeTree.loadingDepth, + childNodes: childrenOfSelector(state, getContextPath(node)), + hasChildren: hasChildrenSelector(state, getContextPath(node)), + isActive: selectors.CR.Nodes.documentNodeContextPathSelector(state) === node.contextPath, + isFocused: selectors.UI.PageTree.getAllFocused(state).includes(node.contextPath), + toggledNodeContextPaths: selectors.UI.PageTree.getToggled(state), + hiddenContextPaths: selectors.UI.PageTree.getHidden(state), + intermediateContextPaths: selectors.UI.PageTree.getIntermediate(state), + loadingNodeContextPaths: selectors.UI.PageTree.getLoading(state), + errorNodeContextPaths: selectors.UI.PageTree.getErrors(state), + isNodeDirty: isDocumentNodeDirtySelector(state, node.contextPath), + canBeInsertedAlongside, + canBeInsertedInto + }); + }; } )(Node)); @@ -402,24 +415,29 @@ export const ContentTreeNode = withNodeTypeRegistryAndI18nRegistry(connect( const canBeMovedIntoSelector = selectors.CR.Nodes.makeCanBeMovedIntoSelector(nodeTypesRegistry); const isContentNodeDirtySelector = selectors.CR.Workspaces.makeIsContentNodeDirtySelector(); - return (state, {node, currentlyDraggedNode}) => ({ - isContentTreeNode: true, - rootNode: selectors.CR.Nodes.documentNodeSelector(state), - loadingDepth: neos.configuration.structureTree.loadingDepth, - childNodes: childrenOfSelector(state, getContextPath(node)), - hasChildren: hasChildrenSelector(state, getContextPath(node)), - isActive: selectors.CR.Nodes.documentNodeContextPathSelector(state) === $get('contextPath', node), - isFocused: $get('cr.nodes.focused.contextPath', state) === $get('contextPath', node), - toggledNodeContextPaths: selectors.UI.ContentTree.getToggled(state), - isNodeDirty: isContentNodeDirtySelector(state, $get('contextPath', node)), - canBeInsertedAlongside: canBeMovedAlongsideSelector(state, { - subject: getContextPath(currentlyDraggedNode), + return (state, {node, currentlyDraggedNodes}) => { + const canBeInsertedAlongside = currentlyDraggedNodes.every(draggedNodeContextPath => canBeMovedAlongsideSelector(state, { + subject: draggedNodeContextPath, reference: getContextPath(node) - }), - canBeInsertedInto: canBeMovedIntoSelector(state, { - subject: getContextPath(currentlyDraggedNode), + })); + const canBeInsertedInto = currentlyDraggedNodes.every(draggedNodeContextPath => canBeMovedIntoSelector(state, { + subject: draggedNodeContextPath, reference: getContextPath(node) - }) - }); + })); + return ({ + isContentTreeNode: true, + focusedNodesContextPaths: selectors.UI.PageTree.getAllFocused(state), + rootNode: selectors.CR.Nodes.documentNodeSelector(state), + loadingDepth: neos.configuration.structureTree.loadingDepth, + childNodes: childrenOfSelector(state, getContextPath(node)), + hasChildren: hasChildrenSelector(state, getContextPath(node)), + isActive: selectors.CR.Nodes.documentNodeContextPathSelector(state) === node.contextPath, + isFocused: selectors.CR.Nodes.focusedNodePathsSelector(state).includes(node.contextPath), + toggledNodeContextPaths: selectors.UI.ContentTree.getToggled(state), + isNodeDirty: isContentNodeDirtySelector(state, node.contextPath), + canBeInsertedAlongside, + canBeInsertedInto + }); + }; } )(Node)); diff --git a/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/helpers.ts b/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/helpers.ts new file mode 100644 index 0000000000..d26bdd3fb3 --- /dev/null +++ b/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/helpers.ts @@ -0,0 +1,9 @@ +import {NodeContextPath} from '@neos-project/neos-ts-interfaces'; + +export const hasNestedNodes = (focusedNodesContextPaths: NodeContextPath[]) => { + return !focusedNodesContextPaths.every((contextPathA: NodeContextPath) => { + const path = contextPathA.split('@')[0]; + // TODO: adjust this for the new CR when this is merged: https://github.com/neos/neos-ui/pull/2178 + return focusedNodesContextPaths.every((contextPathB: NodeContextPath) => !(contextPathB.indexOf(path) === 0 && contextPathA !== contextPathB)); + }); +}; diff --git a/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/index.js b/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/index.js index e32beab685..34c7acf5a5 100644 --- a/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/index.js +++ b/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/index.js @@ -7,6 +7,7 @@ import mergeClassNames from 'classnames'; import {Tree, Icon} from '@neos-project/react-ui-components'; import {actions, selectors} from '@neos-project/neos-ui-redux-store'; +import {SelectionModeTypes} from '@neos-project/neos-ts-interfaces'; import {dndTypes} from '@neos-project/neos-ui-constants'; import {PageTreeNode, ContentTreeNode} from './Node/index'; @@ -26,11 +27,11 @@ export default class NodeTree extends PureComponent { requestScrollIntoView: PropTypes.func, setActiveContentCanvasSrc: PropTypes.func, setActiveContentCanvasContextPath: PropTypes.func, - moveNode: PropTypes.func + moveNodes: PropTypes.func }; state = { - currentlyDraggedNode: null + currentlyDraggedNodes: [] }; handleToggle = contextPath => { @@ -39,23 +40,28 @@ export default class NodeTree extends PureComponent { toggle(contextPath); } - handleFocus = (contextPath, openInNewWindow) => { - const {focus, allowOpeningNodesInNewWindow} = this.props; - if (openInNewWindow && allowOpeningNodesInNewWindow) { - // We do not need to change focus if we open the clicked node in the new window. + handleFocus = (contextPath, metaKeyPressed, altKeyPressed, shiftKeyPressed) => { + const {focus} = this.props; + + if (altKeyPressed) { return; } + const selectionMode = shiftKeyPressed ? SelectionModeTypes.RANGE_SELECT : (metaKeyPressed ? SelectionModeTypes.MULTIPLE_SELECT : SelectionModeTypes.SINGLE_SELECT); - focus(contextPath); + focus(contextPath, undefined, selectionMode); } - handleClick = (src, contextPath, openInNewWindow) => { - const {setActiveContentCanvasSrc, setActiveContentCanvasContextPath, requestScrollIntoView, allowOpeningNodesInNewWindow, reload, contentCanvasSrc} = this.props; - if (openInNewWindow && allowOpeningNodesInNewWindow) { + handleClick = (src, contextPath, metaKeyPressed, altKeyPressed, shiftKeyPressed) => { + const {setActiveContentCanvasSrc, setActiveContentCanvasContextPath, requestScrollIntoView, reload, contentCanvasSrc} = this.props; + if (altKeyPressed) { window.open(window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : '') + window.location.pathname + '?node=' + contextPath); return; } + if (metaKeyPressed || shiftKeyPressed) { + return; + } + // Set a flag that will imperatively tell ContentCanvas to scroll to focused node if (requestScrollIntoView) { requestScrollIntoView(true); @@ -74,17 +80,23 @@ export default class NodeTree extends PureComponent { handleDrag = node => { this.setState({ - currentlyDraggedNode: node + currentlyDraggedNodes: + this.props.focusedNodesContextPaths.includes(node.contextPath) ? + this.props.focusedNodesContextPaths : + [node.contextPath] // moving a node outside of focused nodes }); } handleDrop = (targetNode, position) => { - const {currentlyDraggedNode} = this.state; - const {moveNode} = this.props; - moveNode($get('contextPath', currentlyDraggedNode), $get('contextPath', targetNode), position); + const {currentlyDraggedNodes} = this.state; + const {moveNodes, focus} = this.props; + moveNodes(currentlyDraggedNodes, $get('contextPath', targetNode), position); + // We need to refocus the tree, so all focus would be reset, because its context paths have changed while moving + // Could be removed with the new CR + focus($get('contextPath', targetNode)); this.setState({ - currentlyDraggedNode: null + currentlyDraggedNodes: [] }); } @@ -114,7 +126,7 @@ export default class NodeTree extends PureComponent { onNodeFocus={this.handleFocus} onNodeDrag={this.handleDrag} onNodeDrop={this.handleDrop} - currentlyDraggedNode={this.state.currentlyDraggedNode} + currentlyDraggedNodes={this.state.currentlyDraggedNodes} /> ); @@ -123,6 +135,7 @@ export default class NodeTree extends PureComponent { export const PageTree = connect(state => ({ rootNode: selectors.CR.Nodes.siteNodeSelector(state), + focusedNodesContextPaths: selectors.UI.PageTree.getAllFocused(state), ChildRenderer: PageTreeNode, allowOpeningNodesInNewWindow: true, contentCanvasSrc: $get('ui.contentCanvas.src', state) @@ -132,17 +145,18 @@ export const PageTree = connect(state => ({ reload: actions.UI.ContentCanvas.reload, setActiveContentCanvasSrc: actions.UI.ContentCanvas.setSrc, setActiveContentCanvasContextPath: actions.CR.Nodes.setDocumentNode, - moveNode: actions.CR.Nodes.move, + moveNodes: actions.CR.Nodes.moveMultiple, requestScrollIntoView: null })(NodeTree); export const ContentTree = connect(state => ({ rootNode: selectors.CR.Nodes.documentNodeSelector(state), + focusedNodesContextPaths: selectors.CR.Nodes.focusedNodePathsSelector(state), ChildRenderer: ContentTreeNode, allowOpeningNodesInNewWindow: false }), { toggle: actions.UI.ContentTree.toggle, focus: actions.CR.Nodes.focus, - moveNode: actions.CR.Nodes.move, + moveNodes: actions.CR.Nodes.moveMultiple, requestScrollIntoView: actions.UI.ContentCanvas.requestScrollIntoView })(NodeTree); diff --git a/packages/neos-ui/src/Containers/LeftSideBar/NodeTreeToolBar/Buttons/HideSelectedNode/index.js b/packages/neos-ui/src/Containers/LeftSideBar/NodeTreeToolBar/Buttons/HideSelectedNode/index.js index 8efc9c0460..f4e45c5f49 100644 --- a/packages/neos-ui/src/Containers/LeftSideBar/NodeTreeToolBar/Buttons/HideSelectedNode/index.js +++ b/packages/neos-ui/src/Containers/LeftSideBar/NodeTreeToolBar/Buttons/HideSelectedNode/index.js @@ -8,29 +8,15 @@ export default class HideSelectedNode extends PureComponent { className: PropTypes.string, id: PropTypes.string, - focusedNodeContextPath: PropTypes.string, disabled: PropTypes.bool.isRequired, isHidden: PropTypes.bool.isRequired, - onHide: PropTypes.func.isRequired, - onShow: PropTypes.func.isRequired, + onClick: PropTypes.func.isRequired, i18nRegistry: PropTypes.object.isRequired }; - handleHide = () => { - const {focusedNodeContextPath, onHide} = this.props; - - onHide(focusedNodeContextPath); - } - - handleShow = () => { - const {focusedNodeContextPath, onShow} = this.props; - - onShow(focusedNodeContextPath); - } - render() { - const {className, id, disabled, isHidden, i18nRegistry} = this.props; + const {className, id, disabled, isHidden, i18nRegistry, onClick} = this.props; return ( { - const {hideNode, canBeEdited, visibilityCanBeToggled} = this.props; - + handleHideNode = () => { + const {hideNodes, canBeEdited, visibilityCanBeToggled, focusedNodesContextPaths} = this.props; if (canBeEdited && visibilityCanBeToggled) { - hideNode(contextPath); + hideNodes(focusedNodesContextPaths); } } - handleShowNode = contextPath => { - const {showNode, canBeEdited, visibilityCanBeToggled} = this.props; + handleShowNode = () => { + const {showNodes, canBeEdited, visibilityCanBeToggled, focusedNodesContextPaths} = this.props; if (canBeEdited && visibilityCanBeToggled) { - showNode(contextPath); + showNodes(focusedNodesContextPaths); } } - handleCopyNode = contextPath => { - const {copyNode} = this.props; + handleCopyNodes = () => { + const {copyNodes, focusedNodesContextPaths} = this.props; - copyNode(contextPath); + copyNodes(focusedNodesContextPaths); } - handleCutNode = contextPath => { - const {cutNode, canBeEdited} = this.props; + handleCutNodes = () => { + const {cutNodes, canBeEdited, focusedNodesContextPaths} = this.props; if (canBeEdited) { - cutNode(contextPath); + cutNodes(focusedNodesContextPaths); } } @@ -97,11 +98,11 @@ export default class NodeTreeToolBar extends PureComponent { pasteNode(contextPath); } - handleDeleteNode = contextPath => { - const {deleteNode, canBeDeleted, canBeEdited} = this.props; + handleDeleteNodes = () => { + const {deleteNodes, canBeDeleted, canBeEdited, focusedNodesContextPaths} = this.props; if (canBeDeleted && canBeEdited) { - deleteNode(contextPath); + deleteNodes(focusedNodesContextPaths); } } @@ -146,7 +147,7 @@ export default class NodeTreeToolBar extends PureComponent { isPanelOpen={!isHiddenContentTree} onClick={this.handleToggleContentTree} id={`neos-${treeType}-ToggleContentTree`} - /> + /> )}
@@ -165,15 +166,14 @@ export default class NodeTreeToolBar extends PureComponent { focusedNodeContextPath={focusedNodeContextPath} disabled={destructiveOperationsAreDisabled || !canBeEdited || !visibilityCanBeToggled} isHidden={isHidden} - onHide={this.handleHideNode} - onShow={this.handleShowNode} + onClick={isHidden ? this.handleShowNode : this.handleHideNode} id={`neos-${treeType}-HideSelectedNode`} /> ({ nodeTypesRegistry: globalRegistry.get('@neos-project/neos-ui-contentrepository') })); -export const PageTreeToolbar = withNodeTypesRegistry(connect( - (state, {nodeTypesRegistry}) => { - const canBePastedSelector = selectors.CR.Nodes.makeCanBePastedSelector(nodeTypesRegistry); - const isAllowedToAddChildOrSiblingNodesSelector = selectors.CR.Nodes.makeIsAllowedToAddChildOrSiblingNodes(nodeTypesRegistry); - - return state => { - const siteNodeContextPath = $get('cr.nodes.siteNode', state); - const focusedNodeContextPath = selectors.UI.PageTree.getFocused(state); - const getNodeByContextPathSelector = selectors.CR.Nodes.makeGetNodeByContextPathSelector(focusedNodeContextPath); - const focusedNode = getNodeByContextPathSelector(state); - const clipboardNodeContextPath = selectors.CR.Nodes.clipboardNodeContextPathSelector(state); - const canBePasted = canBePastedSelector(state, { +const removeAllowed = (focusedNodesContextPaths, state) => focusedNodesContextPaths.every(contextPath => { + const getNodeByContextPathSelector = selectors.CR.Nodes.makeGetNodeByContextPathSelector(contextPath); + const focusedNode = getNodeByContextPathSelector(state); + return $get('policy.canRemove', focusedNode); +}); +const visibilityToggleAllowed = (focusedNodesContextPaths, state) => focusedNodesContextPaths.every(contextPath => { + const getNodeByContextPathSelector = selectors.CR.Nodes.makeGetNodeByContextPathSelector(contextPath); + const focusedNode = getNodeByContextPathSelector(state); + return !$contains('_hidden', 'policy.disallowedProperties', focusedNode); +}); +const editingAllowed = (focusedNodesContextPaths, state) => focusedNodesContextPaths.every(contextPath => { + const getNodeByContextPathSelector = selectors.CR.Nodes.makeGetNodeByContextPathSelector(contextPath); + const focusedNode = getNodeByContextPathSelector(state); + return !$contains('_hidden', 'policy.disallowedProperties', focusedNode); +}); + +const makeMapStateToProps = isDocument => (state, {nodeTypesRegistry}) => { + const canBePastedSelector = selectors.CR.Nodes.makeCanBePastedSelector(nodeTypesRegistry); + const isAllowedToAddChildOrSiblingNodesSelector = selectors.CR.Nodes.makeIsAllowedToAddChildOrSiblingNodes(nodeTypesRegistry); + + return state => { + const focusedNodeContextPath = isDocument ? selectors.UI.PageTree.getFocused(state) : selectors.CR.Nodes.focusedNodePathSelector(state); + const focusedNodesContextPaths = isDocument ? selectors.UI.PageTree.getAllFocused(state) : selectors.CR.Nodes.focusedNodePathsSelector(state); + + const getNodeByContextPathSelector = selectors.CR.Nodes.makeGetNodeByContextPathSelector(focusedNodeContextPath); + const focusedNode = getNodeByContextPathSelector(state); + const clipboardNodesContextPaths = selectors.CR.Nodes.clipboardNodesContextPathsSelector(state); + const canBePasted = clipboardNodesContextPaths.every(clipboardNodeContextPath => { + return canBePastedSelector(state, { subject: clipboardNodeContextPath, reference: focusedNodeContextPath }); - const canBeDeleted = $get('policy.canRemove', focusedNode) || false; - const canBeEdited = $get('policy.canEdit', focusedNode) || false; - const visibilityCanBeToggled = !$contains('_hidden', 'policy.disallowedProperties', focusedNode); - const clipboardMode = $get('cr.nodes.clipboardMode', state); - const isCut = focusedNodeContextPath === clipboardNodeContextPath && clipboardMode === 'Move'; - const isCopied = focusedNodeContextPath === clipboardNodeContextPath && clipboardMode === 'Copy'; - const isLoading = selectors.UI.PageTree.getIsLoading(state); - const isHidden = $get('properties._hidden', focusedNode); - const destructiveOperationsAreDisabled = ( - Boolean(focusedNode) === false || - $get('isAutoCreated', focusedNode) || - siteNodeContextPath === focusedNodeContextPath - ); - const isAllowedToAddChildOrSiblingNodes = isAllowedToAddChildOrSiblingNodesSelector(state, { - reference: focusedNodeContextPath - }); + }); + + const selectionHasNestedNodes = hasNestedNodes(focusedNodesContextPaths); + + const canBeDeleted = (removeAllowed(focusedNodesContextPaths, state) && !selectionHasNestedNodes) || false; + const visibilityCanBeToggled = visibilityToggleAllowed(focusedNodesContextPaths, state); + const canBeEdited = editingAllowed(focusedNodesContextPaths, state); + + const clipboardMode = $get('cr.nodes.clipboardMode', state); + const allFocusedNodesAreInClipboard = isEqualSet(focusedNodesContextPaths, clipboardNodesContextPaths); + const isCut = allFocusedNodesAreInClipboard && clipboardMode === 'Move'; + const isCopied = allFocusedNodesAreInClipboard && clipboardMode === 'Copy'; - return { - focusedNodeContextPath, - canBePasted, - canBeDeleted, - canBeEdited, - visibilityCanBeToggled, - isLoading, - isHidden, - destructiveOperationsAreDisabled, - isAllowedToAddChildOrSiblingNodes, - isCut, - isCopied, - treeType: 'PageTree' - }; + const isHidden = $get('properties._hidden', focusedNode); + + const isAllowedToAddChildOrSiblingNodes = isAllowedToAddChildOrSiblingNodesSelector(state, { + reference: focusedNodeContextPath + }); + + const destructiveOperationsAreDisabled = ( + isDocument ? + selectors.UI.PageTree.destructiveOperationsAreDisabledForPageTreeSelector(state) : + selectors.CR.Nodes.destructiveOperationsAreDisabledForContentTreeSelector(state) + ) || selectionHasNestedNodes; + const isLoading = isDocument ? selectors.UI.PageTree.getIsLoading(state) : selectors.UI.ContentTree.getIsLoading(state); + + return { + focusedNodeContextPath, + focusedNodesContextPaths, + canBePasted, + canBeDeleted, + canBeEdited, + visibilityCanBeToggled, + isLoading, + isHidden, + destructiveOperationsAreDisabled, + isAllowedToAddChildOrSiblingNodes, + isCut, + isCopied, + treeType: isDocument ? 'PageTree' : 'ContentTree', + displayToggleContentTreeButton: !isDocument }; - }, { + }; +}; + +export const PageTreeToolbar = withNodeTypesRegistry(connect( + makeMapStateToProps(true), { addNode: actions.CR.Nodes.commenceCreation, - copyNode: actions.CR.Nodes.copy, - cutNode: actions.CR.Nodes.cut, - deleteNode: actions.CR.Nodes.commenceRemoval, + copyNodes: actions.CR.Nodes.copyMultiple, + cutNodes: actions.CR.Nodes.cutMultiple, + deleteNodes: actions.CR.Nodes.commenceRemovalMultiple, hideNode: actions.CR.Nodes.hide, + hideNodes: actions.CR.Nodes.hideMultiple, showNode: actions.CR.Nodes.show, + showNodes: actions.CR.Nodes.showMultiple, pasteNode: actions.CR.Nodes.paste, reloadTree: actions.CR.Nodes.reloadState } )(NodeTreeToolBar)); export const ContentTreeToolbar = withNodeTypesRegistry(connect( - (state, {nodeTypesRegistry}) => { - const canBePastedSelector = selectors.CR.Nodes.makeCanBePastedSelector(nodeTypesRegistry); - const isAllowedToAddChildOrSiblingNodesSelector = selectors.CR.Nodes.makeIsAllowedToAddChildOrSiblingNodes(nodeTypesRegistry); - - return state => { - const focusedNodeContextPath = $get('cr.nodes.focused.contextPath', state); - const getNodeByContextPathSelector = selectors.CR.Nodes.makeGetNodeByContextPathSelector(focusedNodeContextPath); - const focusedNode = getNodeByContextPathSelector(state); - const clipboardNodeContextPath = selectors.CR.Nodes.clipboardNodeContextPathSelector(state); - const canBePasted = canBePastedSelector(state, { - subject: clipboardNodeContextPath, - reference: focusedNodeContextPath - }); - const canBeDeleted = $get('policy.canRemove', focusedNode) || false; - const canBeEdited = $get('policy.canEdit', focusedNode) || false; - const visibilityCanBeToggled = !$contains('_hidden', 'policy.disallowedProperties', focusedNode); - const clipboardMode = $get('cr.nodes.clipboardMode', state); - const isCut = focusedNodeContextPath === clipboardNodeContextPath && clipboardMode === 'Move'; - const isCopied = focusedNodeContextPath === clipboardNodeContextPath && clipboardMode === 'Copy'; - const isLoading = selectors.UI.ContentTree.getIsLoading(state); - const isHidden = $get('properties._hidden', focusedNode); - const destructiveOperationsAreDisabled = selectors.CR.Nodes.destructiveOperationsAreDisabledSelector(state); - const isAllowedToAddChildOrSiblingNodes = isAllowedToAddChildOrSiblingNodesSelector(state, { - reference: focusedNodeContextPath - }); - const isHiddenContentTree = $get('ui.leftSideBar.contentTree.isHidden', state); - - return { - focusedNodeContextPath, - displayToggleContentTreeButton: true, - canBePasted, - canBeDeleted, - canBeEdited, - visibilityCanBeToggled, - isLoading, - isHidden, - destructiveOperationsAreDisabled, - isAllowedToAddChildOrSiblingNodes, - isCut, - isCopied, - isHiddenContentTree, - treeType: 'ContentTree' - }; - }; - }, { + makeMapStateToProps(false), { addNode: actions.CR.Nodes.commenceCreation, - copyNode: actions.CR.Nodes.copy, - cutNode: actions.CR.Nodes.cut, - deleteNode: actions.CR.Nodes.commenceRemoval, + copyNodes: actions.CR.Nodes.copyMultiple, + cutNodes: actions.CR.Nodes.cutMultiple, + deleteNodes: actions.CR.Nodes.commenceRemovalMultiple, hideNode: actions.CR.Nodes.hide, + hideNodes: actions.CR.Nodes.hideMultiple, showNode: actions.CR.Nodes.show, + showNodes: actions.CR.Nodes.showMultiple, pasteNode: actions.CR.Nodes.paste, reloadTree: actions.UI.ContentTree.reloadTree, toggleContentTree: actions.UI.LeftSideBar.toggleContentTree diff --git a/packages/neos-ui/src/Containers/Modals/DeleteNode/index.js b/packages/neos-ui/src/Containers/Modals/DeleteNode/index.js index b945105838..1324db9290 100644 --- a/packages/neos-ui/src/Containers/Modals/DeleteNode/index.js +++ b/packages/neos-ui/src/Containers/Modals/DeleteNode/index.js @@ -14,20 +14,21 @@ import {neos} from '@neos-project/neos-ui-decorators'; import style from './style.css'; @connect($transform({ - nodeToBeDeletedContextPath: $get('cr.nodes.toBeRemoved'), + nodesToBeDeletedContextPaths: $get('cr.nodes.toBeRemoved'), getNodeByContextPath: selectors.CR.Nodes.nodeByContextPath }), { confirm: actions.CR.Nodes.confirmRemoval, abort: actions.CR.Nodes.abortRemoval }) @neos(globalRegistry => ({ + i18nRegistry: globalRegistry.get('i18n'), nodeTypesRegistry: globalRegistry.get('@neos-project/neos-ui-contentrepository') })) export default class DeleteNodeModal extends PureComponent { static propTypes = { nodeTypesRegistry: PropTypes.object.isRequired, - nodeToBeDeletedContextPath: PropTypes.string, + nodesToBeDeletedContextPaths: PropTypes.array, getNodeByContextPath: PropTypes.func.isRequired, confirm: PropTypes.func.isRequired, @@ -47,20 +48,36 @@ export default class DeleteNodeModal extends PureComponent { } renderTitle() { - const {nodeToBeDeletedContextPath, getNodeByContextPath, nodeTypesRegistry} = this.props; - const node = getNodeByContextPath(nodeToBeDeletedContextPath); - const nodeType = $get('nodeType', node); - const nodeTypeLabel = $get('ui.label', nodeTypesRegistry.get(nodeType)) || 'Neos.Neos:Main:node'; - + const {nodesToBeDeletedContextPaths, getNodeByContextPath, nodeTypesRegistry} = this.props; + if (nodesToBeDeletedContextPaths.length === 1) { + const singleNodeToBeDeletedContextPath = nodesToBeDeletedContextPaths[0]; + const node = getNodeByContextPath(singleNodeToBeDeletedContextPath); + const nodeType = $get('nodeType', node); + const nodeTypeLabel = $get('ui.label', nodeTypesRegistry.get(nodeType)) || 'Neos.Neos:Main:node'; + return ( +
+ + + +   + +   + "{$get('label', node)}" + +
+ ); + } return (
- -   - -   - "{$get('label', node)}" +
); @@ -96,10 +113,14 @@ export default class DeleteNodeModal extends PureComponent { } render() { - const {nodeToBeDeletedContextPath, getNodeByContextPath} = this.props; - const node = getNodeByContextPath(nodeToBeDeletedContextPath); + const {nodesToBeDeletedContextPaths, getNodeByContextPath, i18nRegistry} = this.props; + let node = null; + if (nodesToBeDeletedContextPaths.length === 1) { + const singleNodeToBeDeletedContextPath = nodesToBeDeletedContextPaths[0]; + node = getNodeByContextPath(singleNodeToBeDeletedContextPath); + } - if (!node) { + if (nodesToBeDeletedContextPaths.length === 0) { return null; } @@ -114,7 +135,7 @@ export default class DeleteNodeModal extends PureComponent { >
-   "{$get('label', node)}"? +   {nodesToBeDeletedContextPaths.length > 1 ? `${nodesToBeDeletedContextPaths.length} ${i18nRegistry.translate('nodes', 'nodes', {}, 'Neos.Neos.Ui', 'Main')}` : `"${$get('label', node)}"`}?
); diff --git a/packages/neos-ui/src/Containers/Modals/InsertMode/index.js b/packages/neos-ui/src/Containers/Modals/InsertMode/index.js index 2845356221..fea8e6b3b7 100644 --- a/packages/neos-ui/src/Containers/Modals/InsertMode/index.js +++ b/packages/neos-ui/src/Containers/Modals/InsertMode/index.js @@ -18,7 +18,7 @@ import style from './style.css'; @connect($transform({ isOpen: $get('ui.insertionModeModal.isOpen'), - subjectContextPath: $get('ui.insertionModeModal.subjectContextPath'), + subjectContextPaths: $get('ui.insertionModeModal.subjectContextPaths'), referenceContextPath: $get('ui.insertionModeModal.referenceContextPath'), enableAlongsideModes: $get('ui.insertionModeModal.enableAlongsideModes'), enableIntoMode: $get('ui.insertionModeModal.enableIntoMode'), @@ -30,6 +30,7 @@ import style from './style.css'; }) @neos(globalRegistry => ({ + i18nRegistry: globalRegistry.get('i18n'), nodeTypesRegistry: globalRegistry.get('@neos-project/neos-ui-contentrepository') })) export default class InsertModeModal extends PureComponent { @@ -41,8 +42,9 @@ export default class InsertModeModal extends PureComponent { cancel: PropTypes.func.isRequired, apply: PropTypes.func.isRequired, nodeTypesRegistry: PropTypes.object.isRequired, + i18nRegistry: PropTypes.object.isRequired, getNodeByContextPath: PropTypes.func.isRequired, - subjectContextPath: PropTypes.string, + subjectContextPaths: PropTypes.array, referenceContextPath: PropTypes.string }; @@ -65,8 +67,12 @@ export default class InsertModeModal extends PureComponent { apply(mode); } - renderNodeLabel(contextPath) { - const {getNodeByContextPath, nodeTypesRegistry} = this.props; + renderNodeLabel(contextPaths) { + const {getNodeByContextPath, nodeTypesRegistry, i18nRegistry} = this.props; + if (contextPaths.length > 1) { + return `${contextPaths.length} ${i18nRegistry.translate('nodes', 'nodes', {}, 'Neos.Neos.Ui', 'Main')}`; + } + const contextPath = contextPaths[0]; const node = getNodeByContextPath(contextPath); const getLabel = $get('label'); const getNodeType = $get('nodeType'); @@ -81,7 +87,7 @@ export default class InsertModeModal extends PureComponent { } renderTitle() { - const {subjectContextPath, referenceContextPath, operationType} = this.props; + const {subjectContextPaths, referenceContextPath, operationType} = this.props; return (
@@ -92,8 +98,8 @@ export default class InsertModeModal extends PureComponent { key="copy" id="Neos.Neos:Main:copy__from__to--title" params={{ - source: this.renderNodeLabel(subjectContextPath), - target: this.renderNodeLabel(referenceContextPath) + source: this.renderNodeLabel(subjectContextPaths), + target: this.renderNodeLabel([referenceContextPath]) }} /> } @@ -102,8 +108,8 @@ export default class InsertModeModal extends PureComponent { key="move" id="Neos.Neos:Main:move__from__to--title" params={{ - source: this.renderNodeLabel(subjectContextPath), - target: this.renderNodeLabel(referenceContextPath) + source: this.renderNodeLabel(subjectContextPaths), + target: this.renderNodeLabel([referenceContextPath]) }} /> } @@ -112,8 +118,8 @@ export default class InsertModeModal extends PureComponent { key="move" id="Neos.Neos:Main:move__from__to--title" params={{ - source: this.renderNodeLabel(subjectContextPath), - target: this.renderNodeLabel(referenceContextPath) + source: this.renderNodeLabel(subjectContextPaths), + target: this.renderNodeLabel([referenceContextPath]) }} /> } @@ -152,7 +158,7 @@ export default class InsertModeModal extends PureComponent { render() { const { isOpen, - subjectContextPath, + subjectContextPaths, referenceContextPath, enableAlongsideModes, enableIntoMode @@ -175,8 +181,8 @@ export default class InsertModeModal extends PureComponent {

diff --git a/packages/neos-ui/src/Containers/RightSideBar/Inspector/index.js b/packages/neos-ui/src/Containers/RightSideBar/Inspector/index.js index e3cdd9ae82..9c2f1ce565 100644 --- a/packages/neos-ui/src/Containers/RightSideBar/Inspector/index.js +++ b/packages/neos-ui/src/Containers/RightSideBar/Inspector/index.js @@ -38,6 +38,8 @@ import style from './style.css'; return { focusedNode: selectors.CR.Nodes.focusedSelector(state), + focusedContentNodesContextPaths: selectors.CR.Nodes.focusedNodePathsSelector(state), + focusedDocumentNodesContextPaths: selectors.UI.PageTree.getAllFocused(state), validationErrors: validationErrorsSelector(state), isApplyDisabled: isApplyDisabledSelector(state), transientValues: selectors.UI.Inspector.transientValues(state), @@ -213,7 +215,7 @@ export default class Inspector extends PureComponent { } renderFallback() { - return (
); + return (
); } handlePanelToggle = path => { @@ -238,6 +240,8 @@ export default class Inspector extends PureComponent { render() { const { focusedNode, + focusedContentNodesContextPaths, + focusedDocumentNodesContextPaths, commit, validationErrors, isApplyDisabled, @@ -246,6 +250,25 @@ export default class Inspector extends PureComponent { shouldShowSecondaryInspector, i18nRegistry } = this.props; + if (focusedContentNodesContextPaths.length > 1) { + return ( +
+
{focusedContentNodesContextPaths.length} {i18nRegistry.translate('contentElementsSelected', 'content elements selected', {}, 'Neos.Neos.Ui', 'Main')}
+
+ ); + } + if (focusedDocumentNodesContextPaths.length > 1) { + return ( +
+
{focusedDocumentNodesContextPaths.length} {i18nRegistry.translate('documentsSelected', 'documents selected', {}, 'Neos.Neos.Ui', 'Main')}
+
); + } const augmentedCommit = (propertyId, value, hooks) => { commit(propertyId, value, hooks, focusedNode); diff --git a/packages/neos-ui/src/Containers/RightSideBar/Inspector/style.css b/packages/neos-ui/src/Containers/RightSideBar/Inspector/style.css index ae6d3e0039..f983b6f9d1 100644 --- a/packages/neos-ui/src/Containers/RightSideBar/Inspector/style.css +++ b/packages/neos-ui/src/Containers/RightSideBar/Inspector/style.css @@ -60,7 +60,7 @@ width: var(--size-SidebarWidth); } } -.loader { +.centeredInspector { display: flex; align-items: center; justify-content: center; diff --git a/packages/neos-ui/src/clipboardMiddleware.js b/packages/neos-ui/src/clipboardMiddleware.js index 3c829515a0..4dca7cc1c2 100644 --- a/packages/neos-ui/src/clipboardMiddleware.js +++ b/packages/neos-ui/src/clipboardMiddleware.js @@ -13,6 +13,8 @@ const clipboardMiddleware = ({getState}) => { const clipboardActionsPatterns = [ '@neos/neos-ui/CR/Nodes/COPY', '@neos/neos-ui/CR/Nodes/CUT', + '@neos/neos-ui/CR/Nodes/COPY_MULTIPLE', + '@neos/neos-ui/CR/Nodes/CUT_MULTIPLE', '@neos/neos-ui/CR/Nodes/COMMIT_PASTE' ]; @@ -24,16 +26,17 @@ const clipboardMiddleware = ({getState}) => { if (serverActionMatched) { clearTimeout(timer); timer = setTimeout(() => { - const {copyNode, cutNode, clearClipboard} = backend.get().endpoints; + const {copyNodes, cutNodes, clearClipboard} = backend.get().endpoints; const state = getState(); + const contextPaths = $get('cr.nodes.clipboard', state); if (action.type === '@neos/neos-ui/CR/Nodes/COMMIT_PASTE') { if (action.payload === 'Move') { clearClipboard(); } } else if ($get('cr.nodes.clipboardMode', state) === 'Copy') { - copyNode($get('cr.nodes.clipboard', state)); + copyNodes(contextPaths); } else if ($get('cr.nodes.clipboardMode', state) === 'Move') { - cutNode($get('cr.nodes.clipboard', state)); + cutNodes(contextPaths); } timer = null; }, debounceLocalStorageTimeout); diff --git a/packages/neos-ui/src/manifest.js b/packages/neos-ui/src/manifest.js index 016299b131..0ef9d8e29c 100644 --- a/packages/neos-ui/src/manifest.js +++ b/packages/neos-ui/src/manifest.js @@ -254,7 +254,8 @@ manifest('main', {}, globalRegistry => { // serverFeedbackHandlers.set('Neos.Neos.Ui:RemoveNode/Main', ({contextPath, parentContextPath}, {store}) => { const state = store.getState(); - if ($get('cr.nodes.focused.contextPath', state) === contextPath) { + const focusedNodeContextPath = selectors.CR.Nodes.focusedNodePathSelector(state); + if (focusedNodeContextPath === contextPath) { store.dispatch(actions.CR.Nodes.unFocus()); } diff --git a/packages/react-ui-components/src/Tree/node.js b/packages/react-ui-components/src/Tree/node.js index 765b5c1558..078b00ab23 100644 --- a/packages/react-ui-components/src/Tree/node.js +++ b/packages/react-ui-components/src/Tree/node.js @@ -179,7 +179,7 @@ export class Header extends PureComponent { [theme['header__data--deniesDrop']]: isOver && !canDrop }); - return connectDragSource( + return (
- {connectDropTarget( + {connectDropTarget(connectDragSource(
- )} + ))} {isLastChild && ( { + const unionSize = new Set([...a, ...b]).size; + return unionSize !== 0 && unionSize === a.length && unionSize === b.length; +}; +export default isEqualSet; diff --git a/tslint.json b/tslint.json index 1c650a7e86..4817ca96cc 100644 --- a/tslint.json +++ b/tslint.json @@ -5,7 +5,7 @@ "member-ordering": false, "ordered-imports": false, "object-literal-sort-keys": false, - "no-console": true, + "no-console": [true, "log"], "no-debugger": true, "no-parameter-reassignment": true, "no-var-keyword": true,