Skip to content

Commit

Permalink
Merge pull request #3756 from Devclaim/feature/collapseAllButton
Browse files Browse the repository at this point in the history
FEATURE: Collapse All Button in Content and Page Tree
  • Loading branch information
Sebobo committed Apr 24, 2024
2 parents 83e9f22 + 8fcfe72 commit 95a5583
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 34 deletions.
4 changes: 4 additions & 0 deletions Resources/Private/Translations/de/Main.xlf
Expand Up @@ -622,6 +622,10 @@
<source>Inside</source>
<target state="translated">Innen</target>
</trans-unit>
<trans-unit id="collapseAll" xml:space="preserve">
<source>Collapse All</source>
<target state="translated">Alle Ordner zuklappen</target>
</trans-unit>
</body>
</file>
</xliff>
3 changes: 3 additions & 0 deletions Resources/Private/Translations/en/Main.xlf
Expand Up @@ -374,6 +374,9 @@
<trans-unit id="errorBoundary.footer" xml:space="preserve">
<source>For more information about the error please refer to the JavaScript console.</source>
</trans-unit>
<trans-unit id="collapseAll" xml:space="preserve">
<source>Collapse All</source>
</trans-unit>
</body>
</file>
</xliff>
61 changes: 61 additions & 0 deletions packages/neos-ui-redux-store/src/CR/Nodes/selectors.ts
Expand Up @@ -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)
Expand Down
24 changes: 23 additions & 1 deletion packages/neos-ui-redux-store/src/UI/ContentTree/index.ts
Expand Up @@ -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);
Expand All @@ -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
Expand All @@ -51,7 +56,8 @@ export const actions = {
reloadTree,
requestChildren,
setAsLoading,
setAsLoaded
setAsLoaded,
collapseAll
};

export type Action = ActionType<typeof actions>;
Expand Down Expand Up @@ -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;
}
}
});

Expand Down
27 changes: 25 additions & 2 deletions packages/neos-ui-redux-store/src/UI/PageTree/index.ts
Expand Up @@ -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});
Expand All @@ -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;
Expand All @@ -72,7 +78,8 @@ export const actions = {
setAsLoaded,
requestChildren,
commenceSearch,
setSearchResult
setSearchResult,
collapseAll
};

export type Action = ActionType<typeof actions>;
Expand Down Expand Up @@ -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;
}
}
});

Expand Down
117 changes: 86 additions & 31 deletions packages/neos-ui/src/Containers/LeftSideBar/NodeTree/index.js
Expand Up @@ -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);
Expand All @@ -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 = {
Expand All @@ -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;

Expand Down Expand Up @@ -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 (
<div className={style.loader}>
Expand All @@ -123,6 +147,13 @@ export default class NodeTree extends PureComponent {

return (
<Tree className={classNames}>
<button
onClick={this.handleCollapseAll}
className={style.collapseAll}
title={i18nRegistry.translate('Neos.Neos.Ui:Main:collapseAll')}
>
<Icon className={style.collapseAllIcon} icon="compress-alt"/>
</button>
<ConnectedDragLayer
nodeDndType={dndTypes.NODE}
ChildRenderer={ChildRenderer}
Expand All @@ -134,6 +165,7 @@ export default class NodeTree extends PureComponent {
node={rootNode}
level={1}
onNodeToggle={this.handleToggle}
onToggleChildren={this.handleToggleChildren}
onNodeClick={this.handleClick}
onNodeFocus={this.handleFocus}
onNodeDrag={this.handleDrag}
Expand All @@ -146,32 +178,55 @@ 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
}), {
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));
Expand Up @@ -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;
}
}

0 comments on commit 95a5583

Please sign in to comment.