diff --git a/Classes/Fusion/Helper/NodeInfoHelper.php b/Classes/Fusion/Helper/NodeInfoHelper.php index 9b8d3db1ec..f7982c57c4 100644 --- a/Classes/Fusion/Helper/NodeInfoHelper.php +++ b/Classes/Fusion/Helper/NodeInfoHelper.php @@ -105,6 +105,7 @@ public function renderNodeWithMinimalPropertiesAndChildrenInformation( // TODO: we should export this correctly named, but that needs changes throughout the JS code as well. '_hidden' => $node->tags->withoutInherited()->contain(NeosSubtreeTag::disabled()), 'hiddenInMenu' => $node->getProperty('hiddenInMenu'), + '_hiddenByAncestors' => $node->tags->onlyInherited()->contain(NeosSubtreeTag::disabled()), '_hiddenInIndex' => $node->getProperty('hiddenInMenu'), '_hasTimeableNodeVisibility' => $node->getProperty('enableAfterDateTime') instanceof \DateTimeInterface @@ -147,6 +148,14 @@ public function renderNodeWithPropertiesAndChildrenInformation( $nodeInfo['properties'] = $this->nodePropertyConverterService->getPropertiesArray($node); $nodeInfo['tags'] = $node->tags; $nodeInfo['isFullyLoaded'] = true; + $nodeInfo['properties'] = array_merge($nodeInfo['properties'], [ + // TODO: we should export this correctly named, but that needs changes throughout the JS code as well. + '_hidden' => $node->tags->withoutInherited()->contain(NeosSubtreeTag::disabled()), + '_hiddenByAncestors' => $node->tags->onlyInherited()->contain(NeosSubtreeTag::disabled()), + '_hasTimeableNodeVisibility' => + $node->getProperty('enableAfterDateTime') instanceof \DateTimeInterface + || $node->getProperty('disableAfterDateTime') instanceof \DateTimeInterface, + ]); if ($actionRequest !== null) { $nodeInfo = array_merge($nodeInfo, $this->getUriInformation($node, $actionRequest)); 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 de3867777a..03ff589b01 100644 --- a/packages/neos-ui-redux-store/src/UI/PageTree/index.ts +++ b/packages/neos-ui-redux-store/src/UI/PageTree/index.ts @@ -43,7 +43,8 @@ export enum actionTypes { 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', - COLLAPSE_ALL = '@neos/neos-ui/UI/PageTree/COLLAPSE_ALL' + COLLAPSE_ALL = '@neos/neos-ui/UI/PageTree/COLLAPSE_ALL', + RELOAD_TREE = '@neos/neos-ui/UI/PageTree/RELOAD_TREE' } const focus = (contextPath: NodeContextPath, _: undefined, selectionMode: SelectionModeTypes = SelectionModeTypes.SINGLE_SELECT) => createAction(actionTypes.FOCUS, {contextPath, selectionMode}); @@ -56,6 +57,7 @@ const collapseAll = ( nodeContextPaths: NodeContextPath[], collapsedByDefaultNodeContextPaths: NodeContextPath[] ) => createAction(actionTypes.COLLAPSE_ALL, {nodeContextPaths, collapsedByDefaultNodeContextPaths}); +const reloadTree = (contextPath: NodeContextPath) => createAction(actionTypes.RELOAD_TREE, {contextPath}); interface CommenceSearchOptions extends Readonly<{ query: string; @@ -81,7 +83,8 @@ export const actions = { requestChildren, commenceSearch, setSearchResult, - collapseAll + collapseAll, + reloadTree }; export type Action = ActionType; diff --git a/packages/neos-ui-sagas/src/CR/NodeOperations/index.js b/packages/neos-ui-sagas/src/CR/NodeOperations/index.js index bbda395b09..fcbb93c41e 100644 --- a/packages/neos-ui-sagas/src/CR/NodeOperations/index.js +++ b/packages/neos-ui-sagas/src/CR/NodeOperations/index.js @@ -7,6 +7,7 @@ import moveDroppedNodes from './moveDroppedNodes'; import hideNode from './hideNode'; import showNode from './showNode'; import reloadState from './reloadState'; +import reloadTreesAfterVisibilityChange from './reloadTreesAfterVisibilityChange'; import {makeReloadNodes} from './reloadNodes'; export { @@ -19,5 +20,6 @@ export { hideNode, showNode, reloadState, + reloadTreesAfterVisibilityChange, makeReloadNodes }; diff --git a/packages/neos-ui-sagas/src/CR/NodeOperations/reloadTreesAfterVisibilityChange.js b/packages/neos-ui-sagas/src/CR/NodeOperations/reloadTreesAfterVisibilityChange.js new file mode 100644 index 0000000000..3bebe7116e --- /dev/null +++ b/packages/neos-ui-sagas/src/CR/NodeOperations/reloadTreesAfterVisibilityChange.js @@ -0,0 +1,37 @@ +import {takeEvery, take, put, select} from 'redux-saga/effects'; + +import {actionTypes, actions} from '@neos-project/neos-ui-redux-store'; + +export default function * reloadTreesAfterVisibilityChange({globalRegistry}) { + const nodeTypesRegistry = globalRegistry.get('@neos-project/neos-ui-contentrepository'); + yield takeEvery(actionTypes.Changes.PERSIST, function * (action) { + const affectedContextPaths = action.payload.changes + .filter(change => change.type === 'Neos.Neos.Ui:Property' + && change.payload.propertyName === '_hidden') + .map(change => change.subject); + if (affectedContextPaths.length === 0) { + return; + } + + // Wait until the persist round-trip is fully finished (survives request batching in + // watchPersist, which re-triggers on FINISH_SAVING while more changes are queued). + do { + yield take(actionTypes.UI.Remote.FINISH_SAVING); + } while (yield select(state => state?.ui?.remote?.isSaving)); + + // A hide/show also changes `_hiddenByAncestors` for every descendant. For each affected + // document node, reload its page-tree subtree so descendant document icons update immediately. + for (const contextPath of new Set(affectedContextPaths)) { + const nodeType = yield select(state => state?.cr?.nodes?.byContextPath?.[contextPath]?.nodeType); + if (nodeType && nodeTypesRegistry.hasRole(nodeType, 'document')) { + yield put(actions.UI.PageTree.reloadTree(contextPath)); + } + } + + // Always refresh the current document's content tree: hiding/showing a content node changes + // its descendants' `_hiddenByAncestors`, and hiding/showing the current document (or one of its + // document ancestors) makes all of its content inherited-disabled. A single lightweight reload + // of the current document's content covers both cases. + yield put(actions.UI.ContentTree.reloadTree()); + }); +} diff --git a/packages/neos-ui-sagas/src/UI/PageTree/index.js b/packages/neos-ui-sagas/src/UI/PageTree/index.js index 3ac91d555c..e2ce1a5a88 100644 --- a/packages/neos-ui-sagas/src/UI/PageTree/index.js +++ b/packages/neos-ui-sagas/src/UI/PageTree/index.js @@ -240,3 +240,33 @@ export function * watchSearch() { yield put(actions.UI.PageTree.setAsLoaded(contextPath)); }); } + +export function * watchReloadTree() { + yield takeEvery(actionTypes.UI.PageTree.RELOAD_TREE, function * reloadTree(action) { + const {contextPath} = action.payload; + const {q} = backend.get(); + const clipboardNodesContextPaths = yield select(selectors.CR.Nodes.clipboardNodesContextPathsSelector); + const toggledNodes = yield select(state => state?.ui?.pageTree?.toggled); + + yield put(actions.UI.PageTree.setAsLoading(contextPath)); + let nodes = []; + try { + nodes = yield q(contextPath).neosUiDefaultNodes( + getConfiguration(configuration => configuration.nodeTree.presets.default.baseNodeType), + getConfiguration(configuration => configuration.nodeTree.loadingDepth), + toggledNodes, + clipboardNodesContextPaths + ).getForTree('PAGE_TREE'); + } catch (err) { + yield put(actions.UI.PageTree.invalidate(contextPath)); + showFlashMessage({id: 'reloadTreeError', severity: 'error', message: err.message}); + return; + } + const nodeMap = nodes.reduce((map, node) => { + map[node?.contextPath] = node; + return map; + }, {}); + yield put(actions.CR.Nodes.merge(nodeMap)); + yield put(actions.UI.PageTree.setAsLoaded(contextPath)); + }); +} diff --git a/packages/neos-ui-sagas/src/manifest.js b/packages/neos-ui-sagas/src/manifest.js index d212af6370..817ece020f 100644 --- a/packages/neos-ui-sagas/src/manifest.js +++ b/packages/neos-ui-sagas/src/manifest.js @@ -48,6 +48,7 @@ manifest('main.sagas', {}, globalRegistry => { sagasRegistry.set('neos-ui/CR/NodeOperations/showNode', {saga: crNodeOperations.showNode}); sagasRegistry.set('neos-ui/CR/NodeOperations/removeNodeIfConfirmed', {saga: crNodeOperations.removeNodeIfConfirmed}); sagasRegistry.set('neos-ui/CR/NodeOperations/reloadState', {saga: crNodeOperations.reloadState}); + sagasRegistry.set('neos-ui/CR/NodeOperations/reloadTreesAfterVisibilityChange', {saga: crNodeOperations.reloadTreesAfterVisibilityChange}); sagasRegistry.set('neos-ui/CR/Policies/watchNodeInformationChanges', {saga: crPolicies.watchNodeInformationChanges}); @@ -79,6 +80,7 @@ manifest('main.sagas', {}, globalRegistry => { sagasRegistry.set('neos-ui/UI/PageTree/watchRequestChildrenForContextPath', {saga: uiPageTree.watchRequestChildrenForContextPath}); sagasRegistry.set('neos-ui/UI/PageTree/watchSearch', {saga: uiPageTree.watchSearch}); sagasRegistry.set('neos-ui/UI/PageTree/watchToggle', {saga: uiPageTree.watchToggle}); + sagasRegistry.set('neos-ui/UI/PageTree/watchReloadTree', {saga: uiPageTree.watchReloadTree}); sagasRegistry.set('neos-ui/UI/Hotkeys/handleHotkeys', {saga: uiHotkeys.handleHotkeys}); 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 5a35ae3037..a84a8d8a48 100644 --- a/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/Node/index.js +++ b/packages/neos-ui/src/Containers/LeftSideBar/NodeTree/Node/index.js @@ -170,6 +170,7 @@ export default class Node extends PureComponent { const {node} = this.props; const isDisabled = node?.properties?._hidden; + const isDisabledByAncestors = node?.properties?._hiddenByAncestors; const hasTimeableNodeVisibility = node?.properties?._hasTimeableNodeVisibility; if (hasTimeableNodeVisibility) { @@ -193,6 +194,16 @@ export default class Node extends PureComponent { ); } + if (isDisabledByAncestors) { + return ( + + + + + + ); + } + return null; } diff --git a/packages/react-ui-components/src/Icon/__snapshots__/icon.spec.tsx.snap b/packages/react-ui-components/src/Icon/__snapshots__/icon.spec.tsx.snap index 45e8c1f90c..c3d969fae4 100644 --- a/packages/react-ui-components/src/Icon/__snapshots__/icon.spec.tsx.snap +++ b/packages/react-ui-components/src/Icon/__snapshots__/icon.spec.tsx.snap @@ -10,6 +10,7 @@ exports[` should render correctly. 1`] = ` Object { "icon": "iconClassName", "icon--big": "bigClassName", + "icon--color-contrastDark": "color-contrastDarkClassName", "icon--color-error": "color-errorClassName", "icon--color-primaryBlue": "color-primaryBlueClassName", "icon--color-warn": "color-warnClassName", diff --git a/packages/react-ui-components/src/Icon/fontAwesomeIcon.spec.tsx b/packages/react-ui-components/src/Icon/fontAwesomeIcon.spec.tsx index 66ead9fc61..4aaa5d961f 100644 --- a/packages/react-ui-components/src/Icon/fontAwesomeIcon.spec.tsx +++ b/packages/react-ui-components/src/Icon/fontAwesomeIcon.spec.tsx @@ -21,6 +21,7 @@ describe('', () => { 'icon--big': 'bigClassName', 'icon--color-error': 'color-errorClassName', 'icon--color-primaryBlue': 'color-primaryBlueClassName', + 'icon--color-contrastDark': 'color-contrastDarkClassName', 'icon--color-warn': 'color-warnClassName', 'icon--paddedLeft': 'paddedLeftClassName', 'icon--paddedRight': 'paddedRightClassName', diff --git a/packages/react-ui-components/src/Icon/fontAwesomeIcon.tsx b/packages/react-ui-components/src/Icon/fontAwesomeIcon.tsx index e8bd492136..dee87fde9a 100644 --- a/packages/react-ui-components/src/Icon/fontAwesomeIcon.tsx +++ b/packages/react-ui-components/src/Icon/fontAwesomeIcon.tsx @@ -22,7 +22,8 @@ class FontAwesomeIcon extends PureComponent { [theme!['icon--paddedRight']]: padded === 'right', [theme!['icon--color-warn']]: color === 'warn', [theme!['icon--color-error']]: color === 'error', - [theme!['icon--color-primaryBlue']]: color === 'primaryBlue' + [theme!['icon--color-primaryBlue']]: color === 'primaryBlue', + [theme!['icon--color-contrastDark']]: color === 'contrastDark' } ); diff --git a/packages/react-ui-components/src/Icon/icon.spec.tsx b/packages/react-ui-components/src/Icon/icon.spec.tsx index 72a2eb5703..162609398b 100644 --- a/packages/react-ui-components/src/Icon/icon.spec.tsx +++ b/packages/react-ui-components/src/Icon/icon.spec.tsx @@ -13,6 +13,7 @@ describe('', () => { 'icon--big': 'bigClassName', 'icon--color-error': 'color-errorClassName', 'icon--color-primaryBlue': 'color-primaryBlueClassName', + 'icon--color-contrastDark': 'color-contrastDarkClassName', 'icon--color-warn': 'color-warnClassName', 'icon--paddedLeft': 'paddedLeftClassName', 'icon--paddedRight': 'paddedRightClassName', diff --git a/packages/react-ui-components/src/Icon/icon.tsx b/packages/react-ui-components/src/Icon/icon.tsx index b95c6d5bf7..88ddd75164 100644 --- a/packages/react-ui-components/src/Icon/icon.tsx +++ b/packages/react-ui-components/src/Icon/icon.tsx @@ -7,7 +7,7 @@ import {defaultProps} from './iconDefaultProps'; type IconSize = 'xs' | 'sm' | 'lg' | '2x' | '3x'; type IconPadding = 'none' | 'left' | 'right'; -type IconColor = 'default' | 'warn' | 'error' | 'primaryBlue'; +type IconColor = 'default' | 'warn' | 'error' | 'primaryBlue' | 'contrastDark'; export interface IconTheme { readonly icon: string; @@ -22,6 +22,7 @@ export interface IconTheme { readonly 'icon--color-warn': string; readonly 'icon--color-error': string; readonly 'icon--color-primaryBlue': string; + readonly 'icon--color-contrastDark': string; } export interface IconProps extends Omit { diff --git a/packages/react-ui-components/src/Icon/resourceIcon.tsx b/packages/react-ui-components/src/Icon/resourceIcon.tsx index 0970c0c74e..b1077a0c51 100644 --- a/packages/react-ui-components/src/Icon/resourceIcon.tsx +++ b/packages/react-ui-components/src/Icon/resourceIcon.tsx @@ -39,6 +39,7 @@ class ResourceIcon extends PureComponent { [theme!['icon--color-warn']]: color === 'warn', [theme!['icon--color-error']]: color === 'error', [theme!['icon--color-primaryBlue']]: color === 'primaryBlue', + [theme!['icon--color-contrastDark']]: color === 'contrastDark', [theme!['icon--huge']]: size === '3x', [theme!['icon--large']]: size === '2x', [theme!['icon--big']]: size === 'lg', diff --git a/packages/react-ui-components/src/Icon/style.module.css b/packages/react-ui-components/src/Icon/style.module.css index fc8e0e4907..7a1baee662 100644 --- a/packages/react-ui-components/src/Icon/style.module.css +++ b/packages/react-ui-components/src/Icon/style.module.css @@ -26,6 +26,10 @@ color: var(--colors-PrimaryBlue); } +.icon--color-contrastDark { + color: var(--colors-ContrastDark); +} + .icon--huge { & svg { height: 3em;