UNPKG

@craftercms/studio-ui

Version:

Services, components, models & utils to build CrafterCMS authoring extensions.

327 lines (325 loc) 11.3 kB
/* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import React, { useEffect, useRef, useState } from 'react'; import PathNavigatorTreeUI from './PathNavigatorTreeUI'; import { useDispatch } from 'react-redux'; import { pathNavigatorTreeBackgroundRefresh, pathNavigatorTreeCollapsePath, pathNavigatorTreeExpandPath, pathNavigatorTreeFetchPathChildren, pathNavigatorTreeFetchPathPage, pathNavigatorTreeInit, pathNavigatorTreeRefresh, pathNavigatorTreeSetKeyword, pathNavigatorTreeToggleCollapsed } from '../../state/actions/pathNavigatorTree'; import { getEditorMode, isEditableViaFormEditor, isImage, isNavigable, isPreviewable, isVideo, isPdfDocument } from '../PathNavigator/utils'; import ContextMenu from '../ContextMenu/ContextMenu'; import { getNumOfMenuOptionsForItem, lookupItemByPath } from '../../utils/content'; import { previewItem } from '../../state/actions/preview'; import { getOffsetLeft, getOffsetTop } from '@mui/material/Popover'; import { showEditDialog, showItemMegaMenu, showPreviewDialog } from '../../state/actions/dialogs'; import { getStoredPathNavigatorTree } from '../../utils/state'; import PathNavigatorSkeleton from '../PathNavigator/PathNavigatorSkeleton'; import { useSelection } from '../../hooks/useSelection'; import { useEnv } from '../../hooks/useEnv'; import { useActiveUser } from '../../hooks/useActiveUser'; import { useItemsByPath } from '../../hooks/useItemsByPath'; import { useSubject } from '../../hooks/useSubject'; import { debounceTime } from 'rxjs/operators'; import { useActiveSite } from '../../hooks/useActiveSite'; import { batchActions } from '../../state/actions/misc'; import { UNDEFINED } from '../../utils/constants'; // @see https://github.com/craftercms/craftercms/issues/5360 // const translations = defineMessages({ // refresh: { // id: 'words.refresh', // defaultMessage: 'Refresh' // } // }); // // const menuOptions: LookupTable<ContextMenuOptionDescriptor> = { // refresh: { // id: 'refresh', // label: translations.refresh // } // }; export function PathNavigatorTree(props) { // region const { ... } = props; const { label, id = props.label.replace(/\s/g, ''), excludes, limit = 10, icon, expandedIcon, collapsedIcon, container, rootPath, initialExpanded, initialCollapsed = true, collapsible = true, initialSystemTypes, onNodeClick, active, classes, showNavigableAsLinks, showPublishingTarget, showWorkflowState, showItemMenu } = props; // endregion const state = useSelection((state) => state.pathNavigatorTree[id]); const { id: siteId, uuid } = useActiveSite(); const user = useActiveUser(); const onSearch$ = useSubject(); const uiConfig = useSelection((state) => state.uiConfig); const [widgetMenu, setWidgetMenu] = useState({ anchorEl: null, sections: [] }); const { authoringBase } = useEnv(); const dispatch = useDispatch(); const itemsByPath = useItemsByPath(); const initialRefs = useRef({ initialCollapsed, initialSystemTypes, limit, excludes, initialExpanded }); const keywordByPath = state === null || state === void 0 ? void 0 : state.keywordByPath; const totalByPath = state === null || state === void 0 ? void 0 : state.totalByPath; const childrenByParentPath = state === null || state === void 0 ? void 0 : state.childrenByParentPath; const rootItem = lookupItemByPath(rootPath, itemsByPath); useEffect(() => { // Adding uiConfig as means to stop navigator from trying to // initialize with previous state information when switching sites if ( rootPath !== (state === null || state === void 0 ? void 0 : state.rootPath) && uiConfig.currentSite === siteId ) { const storedState = getStoredPathNavigatorTree(uuid, user.username, id); const { initialSystemTypes, initialCollapsed, limit, excludes, initialExpanded } = initialRefs.current; dispatch( pathNavigatorTreeInit( Object.assign( { id, rootPath, excludes, limit, collapsed: initialCollapsed, systemTypes: initialSystemTypes, expanded: initialExpanded }, storedState ) ) ); } }, [ dispatch, id, rootPath, siteId, state === null || state === void 0 ? void 0 : state.rootPath, uiConfig.currentSite, user.username, uuid ]); useEffect(() => { const subscription = onSearch$.pipe(debounceTime(400)).subscribe(({ keyword, path }) => { dispatch( batchActions([ pathNavigatorTreeSetKeyword({ id, path, keyword }), pathNavigatorTreeBackgroundRefresh({ id }) ]) ); }); return () => { subscription.unsubscribe(); }; }, [dispatch, id, onSearch$, rootPath]); if (!rootItem || !state) { const storedState = getStoredPathNavigatorTree(uuid, user.username, id); return React.createElement(PathNavigatorSkeleton, { renderBody: storedState ? !storedState.collapsed : !initialCollapsed }); } // region Handlers const onChangeCollapsed = (collapsed) => { collapsible && dispatch(pathNavigatorTreeToggleCollapsed({ id, collapsed })); }; const onNodeLabelClick = onNodeClick !== null && onNodeClick !== void 0 ? onNodeClick : (event, path) => { if (isNavigable(itemsByPath[path])) { dispatch( previewItem({ item: itemsByPath[path], newTab: event.ctrlKey || event.metaKey }) ); } else if (isPreviewable(itemsByPath[path])) { onPreview(itemsByPath[path]); } else { onToggleNodeClick(path); } }; const onToggleNodeClick = (path) => { // If the path is already expanded, should be collapsed if (state.expanded.includes(path)) { dispatch(pathNavigatorTreeCollapsePath({ id, path })); } else { // If the item's children have been loaded, should simply be expanded if (childrenByParentPath[path]) { dispatch(pathNavigatorTreeExpandPath({ id, path })); } else { // Children not fetched yet, should be fetched dispatch(pathNavigatorTreeFetchPathChildren({ id, path })); } } }; const onHeaderButtonClick = (element) => { // @see https://github.com/craftercms/craftercms/issues/5360 onWidgetOptionsClick('refresh'); // setWidgetMenu({ // sections: [[toContextMenuOptionsLookup(menuOptions, formatMessage).refresh]], // anchorEl: element // }); }; const onOpenItemMenu = (element, path) => { const anchorRect = element.getBoundingClientRect(); const top = anchorRect.top + getOffsetTop(anchorRect, 'top'); const left = anchorRect.left + getOffsetLeft(anchorRect, 'left'); dispatch( showItemMegaMenu({ path, anchorReference: 'anchorPosition', anchorPosition: { top, left }, loaderItems: getNumOfMenuOptionsForItem(itemsByPath[path]) }) ); }; const onCloseWidgetOptions = () => setWidgetMenu(Object.assign(Object.assign({}, widgetMenu), { anchorEl: null })); const onWidgetOptionsClick = (option) => { onCloseWidgetOptions(); if (option === 'refresh') { dispatch( pathNavigatorTreeRefresh({ id }) ); } }; const onFilterChange = (keyword, path) => { if (!state.expanded.includes(path)) { dispatch( pathNavigatorTreeExpandPath({ id, path }) ); } onSearch$.next({ keyword, path }); }; const onMoreClick = (path) => { dispatch(pathNavigatorTreeFetchPathPage({ id, path })); }; const onPreview = (item) => { if (isEditableViaFormEditor(item)) { dispatch(showEditDialog({ path: item.path, authoringBase, site: siteId, readonly: true })); } else if (isImage(item) || isVideo(item) || isPdfDocument(item.mimeType)) { dispatch( showPreviewDialog({ type: isImage(item) ? 'image' : isVideo(item) ? 'video' : 'pdf', title: item.label, url: item.path }) ); } else { const mode = getEditorMode(item); dispatch( showPreviewDialog({ type: 'editor', title: item.label, url: item.path, mode }) ); } }; // endregion return React.createElement( React.Fragment, null, React.createElement(PathNavigatorTreeUI, { classes: { header: classes === null || classes === void 0 ? void 0 : classes.header }, title: label, active: active, icon: expandedIcon && collapsedIcon ? (state.collapsed ? collapsedIcon : expandedIcon) : icon, container: container, isCollapsed: state.collapsed, rootPath: rootPath, isRootPathMissing: state.isRootPathMissing, itemsByPath: itemsByPath, keywordByPath: keywordByPath, totalByPath: totalByPath, childrenByParentPath: childrenByParentPath, expandedNodes: state === null || state === void 0 ? void 0 : state.expanded, onIconClick: onToggleNodeClick, onLabelClick: onNodeLabelClick, onChangeCollapsed: onChangeCollapsed, onOpenItemMenu: onOpenItemMenu, onHeaderButtonClick: state.collapsed ? UNDEFINED : onHeaderButtonClick, onFilterChange: onFilterChange, onMoreClick: onMoreClick, showNavigableAsLinks: showNavigableAsLinks, showPublishingTarget: showPublishingTarget, showWorkflowState: showWorkflowState, showItemMenu: showItemMenu }), React.createElement(ContextMenu, { anchorEl: widgetMenu.anchorEl, options: widgetMenu.sections, open: Boolean(widgetMenu.anchorEl), onClose: onCloseWidgetOptions, onMenuItemClicked: onWidgetOptionsClick }) ); } export default PathNavigatorTree;