Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEATURE: Collapse All Button in Content and Page Tree #3756

Merged
merged 8 commits into from Apr 24, 2024
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 makeGetCollapsableDocumentNodes = (nodeTypesRegistry: NodeTypesRegistry) => createSelector(
Sebobo marked this conversation as resolved.
Show resolved Hide resolved
[
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');
}
grebaldi marked this conversation as resolved.
Show resolved Hide resolved
const isCollapsable = node.children.some(
child => child ? documentSubNodeTypes.includes(child.nodeType) : false
)
if (documentSubNodeTypes.includes(node.nodeType) && isCollapsable) {
result[contextPath] = node;
}
});
return result;
}
);

export const makeGetCollapsableContentNodes = (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');
}
grebaldi marked this conversation as resolved.
Show resolved Hide resolved
const isCollapsable = node.children.some(
child => child ? contentSubNodeTypes.includes(child.nodeType) : false
)
if (contentSubNodeTypes.includes(node.nodeType) && isCollapsable) {
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
113 changes: 83 additions & 30 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,14 @@ 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,
allCollapsableNodes: PropTypes.object,
loadingDepth: PropTypes.number
};

state = {
Expand All @@ -45,6 +49,25 @@ export default class NodeTree extends PureComponent {
toggle(contextPath);
}

handleCollapseAll = () => {
const {collapseAll, allCollapsableNodes, rootNode, loadingDepth} = this.props
let nodeContextPaths = []
const collapsedByDefaultNodesContextPaths = []

Object.values(allCollapsableNodes).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 @@ -123,6 +146,13 @@ export default class NodeTree extends PureComponent {

return (
<Tree className={classNames}>
<a
role="button"
grebaldi marked this conversation as resolved.
Show resolved Hide resolved
onClick={this.handleCollapseAll}
className={style.collapseAll}
>
Collapse All
Sebobo marked this conversation as resolved.
Show resolved Hide resolved
</a>
<ConnectedDragLayer
nodeDndType={dndTypes.NODE}
ChildRenderer={ChildRenderer}
Expand All @@ -134,6 +164,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 +177,54 @@ 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 withNodeTypeRegistry = neos(globalRegistry => ({
nodeTypesRegistry: globalRegistry.get('@neos-project/neos-ui-contentrepository')
}));

export const PageTree = withNodeTypeRegistry(connect(
(state, {neos, nodeTypesRegistry}) => {
const documentNodesSelector = selectors.CR.Nodes.makeGetCollapsableDocumentNodes(nodeTypesRegistry);
return ({
rootNode: selectors.CR.Nodes.siteNodeSelector(state),
focusedNodesContextPaths: selectors.UI.PageTree.getAllFocused(state),
ChildRenderer: PageTreeNode,
allowOpeningNodesInNewWindow: true,
loadingDepth: neos.configuration.structureTree.loadingDepth,
allCollapsableNodes: 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 = withNodeTypeRegistry(connect(
(state, {neos, nodeTypesRegistry}) => {
const contentNodesSelector = selectors.CR.Nodes.makeGetCollapsableContentNodes(nodeTypesRegistry);
return ({
rootNode: selectors.CR.Nodes.documentNodeSelector(state),
focusedNodesContextPaths: selectors.CR.Nodes.focusedNodePathsSelector(state),
ChildRenderer: ContentTreeNode,
allowOpeningNodesInNewWindow: false,
loadingDepth: neos.configuration.structureTree.loadingDepth,
allCollapsableNodes: 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,22 @@
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;
text-align: center;
cursor: pointer;
z-index: 5;
padding: 10px 5px 0 5px;

&:hover {
color: var(--colors-PrimaryBlueHover);
}
}