diff --git a/Resources/Private/Translations/de/Main.xlf b/Resources/Private/Translations/de/Main.xlf index 63f94eb18b..7494ff951b 100644 --- a/Resources/Private/Translations/de/Main.xlf +++ b/Resources/Private/Translations/de/Main.xlf @@ -622,6 +622,10 @@ Inside Innen + + Collapse All + Alle Ordner zuklappen + diff --git a/Resources/Private/Translations/en/Main.xlf b/Resources/Private/Translations/en/Main.xlf index 0db6995fa5..4c41c70e7c 100644 --- a/Resources/Private/Translations/en/Main.xlf +++ b/Resources/Private/Translations/en/Main.xlf @@ -374,6 +374,9 @@ For more information about the error please refer to the JavaScript console. + + Collapse All + 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 eed7cf09ea..e45df536af 100644 --- a/packages/neos-ui-redux-store/src/CR/Nodes/selectors.ts +++ b/packages/neos-ui-redux-store/src/CR/Nodes/selectors.ts @@ -66,6 +66,67 @@ export const makeGetDocumentNodes = (nodeTypesRegistry: NodeTypesRegistry) => cr } ); +export const makeGetCollapsibleDocumentNodes = (nodeTypesRegistry: NodeTypesRegistry) => createSelector( + [ + nodesByContextPathSelector + ], + nodesMap => { + const documentRole = nodeTypesRegistry.getRole('document'); + if (!documentRole) { + throw new Error('Document role is not loaded!'); + } + const documentSubNodeTypes = nodeTypesRegistry.getSubTypesOf(documentRole); + + const result: NodeMap = {}; + Object.keys(nodesMap).forEach(contextPath => { + const node = nodesMap[contextPath]; + if (!node) { + throw new Error('This error should never be thrown, it\'s a way to fool TypeScript'); + } + const isCollapsible = node.children.some( + child => child ? documentSubNodeTypes.includes(child.nodeType) : false + ) + if (documentSubNodeTypes.includes(node.nodeType) && isCollapsible) { + result[contextPath] = node; + } + }); + return result; + } +); + +export const makeGetCollapsibleContentNodes = (nodeTypesRegistry: NodeTypesRegistry) => createSelector( + [ + nodesByContextPathSelector + ], + nodesMap => { + const contentRole = nodeTypesRegistry.getRole('content'); + const collectionRole = nodeTypesRegistry.getRole('contentCollection'); + if (!contentRole) { + throw new Error('Content role is not loaded!'); + } + if (!collectionRole) { + throw new Error('ContentCollection role is not loaded!'); + } + const contentSubNodeTypes = nodeTypesRegistry.getSubTypesOf(contentRole); + contentSubNodeTypes.push(...nodeTypesRegistry.getSubTypesOf(collectionRole)) + + const result: NodeMap = {}; + Object.keys(nodesMap).forEach(contextPath => { + const node = nodesMap[contextPath]; + if (!node) { + throw new Error('This error should never be thrown, it\'s a way to fool TypeScript'); + } + const isCollapsible = node.children.some( + child => child ? contentSubNodeTypes.includes(child.nodeType) : false + ) + if (contentSubNodeTypes.includes(node.nodeType) && isCollapsible) { + result[contextPath] = node; + } + }); + return result; + } +); + export const makeGetNodeByContextPathSelector = (contextPath: NodeContextPath) => createSelector( [ (state: GlobalState) => $get(['cr', 'nodes', 'byContextPath', contextPath], state) diff --git a/packages/neos-ui-redux-store/src/UI/ContentTree/index.ts b/packages/neos-ui-redux-store/src/UI/ContentTree/index.ts index aa7c933115..f541403315 100644 --- a/packages/neos-ui-redux-store/src/UI/ContentTree/index.ts +++ b/packages/neos-ui-redux-store/src/UI/ContentTree/index.ts @@ -31,6 +31,7 @@ export enum actionTypes { REQUEST_CHILDREN = '@neos/neos-ui/UI/ContentTree/REQUEST_CHILDREN', SET_AS_LOADING = '@neos/neos-ui/UI/ContentTree/SET_AS_LOADING', SET_AS_LOADED = '@neos/neos-ui/UI/ContentTree/SET_AS_LOADED', + COLLAPSE_ALL = '@neos/neos-ui/UI/ContentTree/COLLAPSE_ALL' } const toggle = (contextPath: NodeContextPath) => createAction(actionTypes.TOGGLE, contextPath); @@ -40,6 +41,10 @@ const reloadTree = () => createAction(actionTypes.RELOAD_TREE); const requestChildren = (contextPath: NodeContextPath, {unCollapse = true, activate = false} = {}) => createAction(actionTypes.REQUEST_CHILDREN, {contextPath, opts: {unCollapse, activate}}); const setAsLoading = (contextPath: NodeContextPath) => createAction(actionTypes.SET_AS_LOADING, {contextPath}); const setAsLoaded = (contextPath: NodeContextPath) => createAction(actionTypes.SET_AS_LOADED, {contextPath}); +const collapseAll = ( + nodeContextPaths: NodeContextPath[], + collapsedByDefaultNodeContextPaths: NodeContextPath[] +) => createAction(actionTypes.COLLAPSE_ALL, {nodeContextPaths, collapsedByDefaultNodeContextPaths}); // // Export the actions @@ -51,7 +56,8 @@ export const actions = { reloadTree, requestChildren, setAsLoading, - setAsLoaded + setAsLoaded, + collapseAll }; export type Action = ActionType; @@ -89,6 +95,22 @@ export const reducer = (state: State = defaultState, action: InitAction | Action draft.loading = draft.loading.filter(i => i !== contextPath); break; } + case actionTypes.COLLAPSE_ALL: { + const {nodeContextPaths, collapsedByDefaultNodeContextPaths} = action.payload; + + nodeContextPaths.forEach(path => { + if (!draft.toggled.includes(path)) { + draft.toggled.push(path); + } + }); + + collapsedByDefaultNodeContextPaths.forEach(path => { + if (draft.toggled.includes(path)) { + draft.toggled = draft.toggled.filter(i => i !== path); + } + }); + break; + } } }); 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 8b55561600..a321f7a5e3 100644 --- a/packages/neos-ui-redux-store/src/UI/PageTree/index.ts +++ b/packages/neos-ui-redux-store/src/UI/PageTree/index.ts @@ -40,7 +40,8 @@ export enum actionTypes { SET_AS_LOADED = '@neos/neos-ui/UI/PageTree/SET_AS_LOADED', REQUEST_CHILDREN = '@neos/neos-ui/UI/PageTree/REQUEST_CHILDREN', COMMENCE_SEARCH = '@neos/neos-ui/UI/PageTree/COMMENCE_SEARCH', - SET_SEARCH_RESULT = '@neos/neos-ui/UI/PageTree/SET_SEARCH_RESULT' + SET_SEARCH_RESULT = '@neos/neos-ui/UI/PageTree/SET_SEARCH_RESULT', + COLLAPSE_ALL = '@neos/neos-ui/UI/PageTree/COLLAPSE_ALL' } const focus = (contextPath: NodeContextPath, _: undefined, selectionMode: SelectionModeTypes = SelectionModeTypes.SINGLE_SELECT) => createAction(actionTypes.FOCUS, {contextPath, selectionMode}); @@ -49,6 +50,11 @@ const invalidate = (contextPath: NodeContextPath) => createAction(actionTypes.IN const requestChildren = (contextPath: NodeContextPath, {unCollapse = true, activate = false} = {}) => createAction(actionTypes.REQUEST_CHILDREN, {contextPath, opts: {unCollapse, activate}}); const setAsLoading = (contextPath: NodeContextPath) => createAction(actionTypes.SET_AS_LOADING, {contextPath}); const setAsLoaded = (contextPath: NodeContextPath) => createAction(actionTypes.SET_AS_LOADED, {contextPath}); +const collapseAll = ( + nodeContextPaths: NodeContextPath[], + collapsedByDefaultNodeContextPaths: NodeContextPath[] +) => createAction(actionTypes.COLLAPSE_ALL, {nodeContextPaths, collapsedByDefaultNodeContextPaths}); + interface CommenceSearchOptions extends Readonly<{ query: string; filterNodeType: string; @@ -72,7 +78,8 @@ export const actions = { setAsLoaded, requestChildren, commenceSearch, - setSearchResult + setSearchResult, + collapseAll }; export type Action = ActionType; @@ -134,6 +141,22 @@ export const reducer = (state: State = defaultState, action: InitAction | Action draft.filterNodeType = action.payload.filterNodeType; break; } + case actionTypes.COLLAPSE_ALL: { + const {nodeContextPaths, collapsedByDefaultNodeContextPaths} = action.payload; + + nodeContextPaths.forEach(path => { + if (!draft.toggled.includes(path)) { + draft.toggled.push(path); + } + }); + + collapsedByDefaultNodeContextPaths.forEach(path => { + if (draft.toggled.includes(path)) { + draft.toggled = draft.toggled.filter(i => i !== path); + } + }); + break; + } } }); diff --git a/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/index.js b/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/index.js index 5efdd420b5..813eaf2a52 100644 --- a/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/index.js +++ b/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/index.js @@ -13,6 +13,7 @@ import {dndTypes} from '@neos-project/neos-ui-constants'; import {PageTreeNode, ContentTreeNode} from './Node/index'; import style from './style.module.css'; +import {neos} from '@neos-project/neos-ui-decorators'; const ConnectedDragLayer = connect((state, {currentlyDraggedNodes}) => { const getNodeByContextPath = selectors.CR.Nodes.nodeByContextPath(state); @@ -28,11 +29,15 @@ export default class NodeTree extends PureComponent { allowOpeningNodesInNewWindow: PropTypes.bool, nodeTypeRole: PropTypes.string, toggle: PropTypes.func, + collapseAll: PropTypes.func, focus: PropTypes.func, requestScrollIntoView: PropTypes.func, setActiveContentCanvasSrc: PropTypes.func, setActiveContentCanvasContextPath: PropTypes.func, - moveNodes: PropTypes.func + moveNodes: PropTypes.func, + allCollapsibleNodes: PropTypes.object, + loadingDepth: PropTypes.number, + i18nRegistry: PropTypes.object.isRequired }; state = { @@ -45,6 +50,25 @@ export default class NodeTree extends PureComponent { toggle(contextPath); } + handleCollapseAll = () => { + const {collapseAll, allCollapsibleNodes, rootNode, loadingDepth} = this.props + let nodeContextPaths = [] + const collapsedByDefaultNodesContextPaths = [] + + Object.values(allCollapsibleNodes).forEach(node => { + const collapsedByDefault = loadingDepth === 0 ? false : node.depth - rootNode.depth >= loadingDepth + if (collapsedByDefault) { + collapsedByDefaultNodesContextPaths.push(node.contextPath) + } else { + nodeContextPaths.push(node.contextPath) + } + }); + + // Do not Collapse RootNode + nodeContextPaths = nodeContextPaths.filter(i => i !== rootNode.contextPath); + collapseAll(nodeContextPaths, collapsedByDefaultNodesContextPaths); + } + handleFocus = (contextPath, metaKeyPressed, altKeyPressed, shiftKeyPressed) => { const {focus} = this.props; @@ -108,7 +132,7 @@ export default class NodeTree extends PureComponent { } render() { - const {rootNode, ChildRenderer} = this.props; + const {rootNode, ChildRenderer, i18nRegistry} = this.props; if (!rootNode) { return (
@@ -123,6 +147,13 @@ export default class NodeTree extends PureComponent { return ( + ({ - rootNode: selectors.CR.Nodes.siteNodeSelector(state), - focusedNodesContextPaths: selectors.UI.PageTree.getAllFocused(state), - ChildRenderer: PageTreeNode, - allowOpeningNodesInNewWindow: true -}), { - toggle: actions.UI.PageTree.toggle, - focus: actions.UI.PageTree.focus, - setActiveContentCanvasSrc: actions.UI.ContentCanvas.setSrc, - setActiveContentCanvasContextPath: actions.CR.Nodes.setDocumentNode, - moveNodes: actions.CR.Nodes.moveMultiple, - requestScrollIntoView: null -}, (stateProps, dispatchProps, ownProps) => { - return Object.assign({}, stateProps, dispatchProps, ownProps); -})(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, - moveNodes: actions.CR.Nodes.moveMultiple, - requestScrollIntoView: actions.UI.ContentCanvas.requestScrollIntoView -}, (stateProps, dispatchProps, ownProps) => { - return Object.assign({}, stateProps, dispatchProps, ownProps); -})(NodeTree); +const withNodeTypeRegistryAndI18nRegistry = neos(globalRegistry => ({ + nodeTypesRegistry: globalRegistry.get('@neos-project/neos-ui-contentrepository'), + i18nRegistry: globalRegistry.get('i18n') +})); + +export const PageTree = withNodeTypeRegistryAndI18nRegistry(connect( + (state, {neos, nodeTypesRegistry}) => { + const documentNodesSelector = selectors.CR.Nodes.makeGetCollapsibleDocumentNodes(nodeTypesRegistry); + return ({ + rootNode: selectors.CR.Nodes.siteNodeSelector(state), + focusedNodesContextPaths: selectors.UI.PageTree.getAllFocused(state), + ChildRenderer: PageTreeNode, + allowOpeningNodesInNewWindow: true, + loadingDepth: neos.configuration.structureTree.loadingDepth, + allCollapsibleNodes: documentNodesSelector(state) + }) + }, { + toggle: actions.UI.PageTree.toggle, + collapseAll: actions.UI.PageTree.collapseAll, + focus: actions.UI.PageTree.focus, + setActiveContentCanvasSrc: actions.UI.ContentCanvas.setSrc, + setActiveContentCanvasContextPath: actions.CR.Nodes.setDocumentNode, + moveNodes: actions.CR.Nodes.moveMultiple, + requestScrollIntoView: null, + isContentTree: false + }, (stateProps, dispatchProps, ownProps) => { + return Object.assign({}, stateProps, dispatchProps, ownProps); + } +)(NodeTree)); + +export const ContentTree = withNodeTypeRegistryAndI18nRegistry(connect( + (state, {neos, nodeTypesRegistry}) => { + const contentNodesSelector = selectors.CR.Nodes.makeGetCollapsibleContentNodes(nodeTypesRegistry); + return ({ + rootNode: selectors.CR.Nodes.documentNodeSelector(state), + focusedNodesContextPaths: selectors.CR.Nodes.focusedNodePathsSelector(state), + ChildRenderer: ContentTreeNode, + allowOpeningNodesInNewWindow: false, + loadingDepth: neos.configuration.structureTree.loadingDepth, + allCollapsibleNodes: contentNodesSelector(state) + }) + }, { + toggle: actions.UI.ContentTree.toggle, + collapseAll: actions.UI.ContentTree.collapseAll, + focus: actions.CR.Nodes.focus, + moveNodes: actions.CR.Nodes.moveMultiple, + requestScrollIntoView: actions.UI.ContentCanvas.requestScrollIntoView, + isContentTree: true + }, (stateProps, dispatchProps, ownProps) => { + return Object.assign({}, stateProps, dispatchProps, ownProps); + } +)(NodeTree)); diff --git a/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/style.module.css b/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/style.module.css index 96f33a7a7f..89daf292db 100644 --- a/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/style.module.css +++ b/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/style.module.css @@ -6,7 +6,31 @@ background: var(--colors-ContrastDarker); border-bottom: 1px solid var(--colors-ContrastDark); border-right: 1px solid var(--colors-ContrastDark); + position: relative; } .loader { margin: var(--spacing-Quarter); } +.collapseAll { + position: absolute; + right: 0; + top: 0; + opacity: .5; + cursor: pointer; + z-index: 5; + padding: var(--spacing-Half); + background-color: transparent; + border: 0; + + .collapseAllIcon { + font-size: 1.2em; + } + + &:hover > .collapseAllIcon { + color: var(--colors-PrimaryBlueHover); + } + + &:hover { + opacity: 1; + } +}