UNPKG

@craftercms/studio-ui

Version:

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

446 lines (444 loc) 14.7 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 TreeItem from '@mui/lab/TreeItem'; import React, { useState } from 'react'; import CircularProgress from '@mui/material/CircularProgress'; import { Typography } from '@mui/material'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { makeStyles } from 'tss-react/mui'; import ItemDisplay from '../ItemDisplay'; import Tooltip from '@mui/material/Tooltip'; import IconButton from '@mui/material/IconButton'; import MoreVertRoundedIcon from '@mui/icons-material/MoreVertRounded'; import SearchRoundedIcon from '@mui/icons-material/SearchRounded'; import SearchBar from '../SearchBar/SearchBar'; import CloseIconRounded from '@mui/icons-material/CloseRounded'; import Button from '@mui/material/Button'; import ArrowRightRoundedIcon from '@mui/icons-material/ArrowRightRounded'; import ArrowDropDownRoundedIcon from '@mui/icons-material/ArrowDropDownRounded'; import { isBlank } from '../../utils/string'; import ErrorOutlineRounded from '@mui/icons-material/ErrorOutlineRounded'; const translations = defineMessages({ filter: { id: 'pathNavigatorTreeItemFilter.placeholder', defaultMessage: 'Filter children...' }, expand: { id: 'words.expand', defaultMessage: 'Expand' }, collapse: { id: 'words.collapse', defaultMessage: 'Collapse' } }); const useStyles = makeStyles()((theme, _params, classes) => ({ root: { [`&:focus > .${classes.content} .${classes.labelContainer}`]: { background: 'none' } }, content: { alignItems: 'flex-start', paddingRight: 0, '&:hover': { background: 'none' } }, labelContainer: { display: 'flex', paddingLeft: 0, flexWrap: 'wrap', overflow: 'hidden', '&:hover': { background: 'none' }, '& .MuiSvgIcon-root': { fontSize: '1.1rem' } }, itemDisplaySection: { width: '100%', display: 'flex', alignItems: 'center', minHeight: '23.5px', '&:hover': { backgroundColor: theme.palette.mode === 'dark' ? theme.palette.action.hover : theme.palette.grey['A200'] } }, filterSection: { width: '100%', display: 'flex', alignItems: 'center' }, empty: { color: theme.palette.text.secondary, display: 'flex', alignItems: 'center', minHeight: '23.5px', marginLeft: '10px', '& svg': { marginRight: '5px', fontSize: '1.1rem' } }, more: { color: theme.palette.text.primary, display: 'flex', alignItems: 'center', minHeight: '23.5px', marginLeft: '10px' }, iconContainer: { width: '26px', marginRight: 0, '& svg': { fontSize: '23.5px !important', color: theme.palette.text.secondary } }, focused: { background: 'none !important' }, loading: { display: 'flex', alignItems: 'center', minHeight: '23.5px', marginLeft: '10px', '& span': { marginLeft: '10px' } }, searchRoot: { margin: '5px 10px 5px 0', height: '25px', width: '100%' }, searchInput: { fontSize: '12px', padding: '5px !important' }, searchCloseButton: { marginRight: '10px' }, searchCloseIcon: { fontSize: '12px !important' }, iconButton: { padding: '2px 3px' }, active: { backgroundColor: theme.palette.action.selected } })); export function PathNavigatorTreeItem(props) { var _a, _b, _c, _d, _e, _f; const { path, itemsByPath, keywordByPath, totalByPath, childrenByParentPath, active = {}, showNavigableAsLinks = true, showPublishingTarget = true, showWorkflowState = true, showItemMenu = true, onLabelClick, onIconClick, onOpenItemMenu, onFilterChange, onMoreClick } = props; const { classes, cx } = useStyles(); const [over, setOver] = useState(false); const [showFilter, setShowFilter] = useState(Boolean(keywordByPath[path])); const [keyword, setKeyword] = useState((_a = keywordByPath[path]) !== null && _a !== void 0 ? _a : ''); const { formatMessage } = useIntl(); const children = (_b = childrenByParentPath[path]) !== null && _b !== void 0 ? _b : []; const onMouseOver = (e) => { e.stopPropagation(); setOver(true); }; const onMouseLeave = (e) => { e.stopPropagation(); setOver(false); }; const onFilterButtonClick = () => { setShowFilter(!showFilter); }; const onClearKeywords = () => { if (keyword) { setKeyword(''); onFilterChange('', path); } }; const onContextMenu = (e) => { if (onOpenItemMenu) { e.preventDefault(); onOpenItemMenu(e.currentTarget.querySelector('[data-item-menu]'), path); } }; // Children for TreeItem set here this way instead of as JSX children below since, because there // are multiple blocks, that would cause all nodes to have an "open" icon even if they have no children. const propsForTreeItem = { children: [] }; if (children.length) { propsForTreeItem.children = children.map((path) => React.createElement(PathNavigatorTreeItem, { key: path, path: path, itemsByPath: itemsByPath, keywordByPath: keywordByPath, totalByPath: totalByPath, childrenByParentPath: childrenByParentPath, active: active, onLabelClick: onLabelClick, onIconClick: onIconClick, onOpenItemMenu: onOpenItemMenu, onFilterChange: onFilterChange, onMoreClick: onMoreClick, showNavigableAsLinks: showNavigableAsLinks, showPublishingTarget: showPublishingTarget, showWorkflowState: showWorkflowState, showItemMenu: showItemMenu }) ); children.length < totalByPath[path] && propsForTreeItem.children.push( React.createElement( 'section', { key: 'more', className: classes.more }, React.createElement( Button, { color: 'primary', size: 'small', onClick: () => { onMoreClick(path); } }, React.createElement(FormattedMessage, { id: 'pathNavigatorTree.moreLinkLabel', defaultMessage: '{count, plural, one {...{count} more item} other {...{count} more items}}', values: { count: totalByPath[path] - children.length } }) ) ) ); } else if (totalByPath[path] > 0 && !childrenByParentPath.length) { propsForTreeItem.children.push( React.createElement( 'div', { key: 'loading', className: classes.loading }, React.createElement(CircularProgress, { size: 14 }), React.createElement( Typography, { variant: 'caption', color: 'textSecondary' }, React.createElement(FormattedMessage, { id: 'words.loading', defaultMessage: 'Loading' }) ) ) ); } else if (!isBlank(keywordByPath[path]) && totalByPath[path] === 0) { propsForTreeItem.children.push( React.createElement( 'section', { key: 'noResults', className: classes.empty }, React.createElement(ErrorOutlineRounded, null), React.createElement( Typography, { variant: 'caption' }, React.createElement(FormattedMessage, { id: 'filter.noResults', defaultMessage: 'No results match your query' }) ) ) ); } return ( // region <TreeItem ... /> React.createElement( TreeItem, Object.assign( { key: path, nodeId: path, expandIcon: // region React.createElement(ArrowRightRoundedIcon, { role: 'button', 'aria-label': formatMessage(translations.expand), 'aria-hidden': 'false', onClick: () => onIconClick(path) }), collapseIcon: // region React.createElement(ArrowDropDownRoundedIcon, { role: 'button', 'aria-label': formatMessage(translations.collapse), 'aria-hidden': 'false', onClick: () => onIconClick(path) }), label: React.createElement( React.Fragment, null, React.createElement( 'section', { role: 'button', onClick: (event) => onLabelClick(event, path), className: classes.itemDisplaySection, onMouseOver: onMouseOver, onMouseLeave: onMouseLeave, onContextMenu: onContextMenu }, React.createElement(ItemDisplay, { styles: { root: { flex: 1, minWidth: 0, minHeight: '23.5px' } }, item: itemsByPath[path], labelTypographyProps: { variant: 'body2' }, showNavigableAsLinks: showNavigableAsLinks, showPublishingTarget: showPublishingTarget, showWorkflowState: showWorkflowState }), over && showItemMenu && onOpenItemMenu && React.createElement( Tooltip, { title: React.createElement(FormattedMessage, { id: 'words.options', defaultMessage: 'Options' }) }, React.createElement( IconButton, { size: 'small', className: classes.iconButton, 'data-item-menu': true, onClick: (e) => { e.preventDefault(); e.stopPropagation(); onOpenItemMenu(e.currentTarget, path); } }, React.createElement(MoreVertRoundedIcon, null) ) ), (showFilter || Boolean(children.length)) && React.createElement( Tooltip, { title: React.createElement(FormattedMessage, { id: 'words.filter', defaultMessage: 'Filter' }) }, React.createElement( IconButton, { size: 'small', className: classes.iconButton, onClick: (e) => { e.preventDefault(); e.stopPropagation(); onClearKeywords(); onFilterButtonClick(); } }, React.createElement(SearchRoundedIcon, { color: showFilter ? 'primary' : 'action' }) ) ) ), showFilter && React.createElement( 'section', { className: classes.filterSection }, React.createElement(SearchBar, { autoFocus: true, onClick: (e) => e.stopPropagation(), onChange: (keyword) => { setKeyword(keyword); onFilterChange(keyword, path); }, keyword: keyword, placeholder: formatMessage(translations.filter), onActionButtonClick: (e, input) => { e.stopPropagation(); onClearKeywords(); input.focus(); }, showActionButton: keyword && true, classes: { root: cx( classes.searchRoot, (_c = props.classes) === null || _c === void 0 ? void 0 : _c.searchRoot ), inputInput: cx( classes.searchInput, (_d = props.classes) === null || _d === void 0 ? void 0 : _d.searchInput ), actionIcon: cx( classes.searchCloseIcon, (_e = props.classes) === null || _e === void 0 ? void 0 : _e.searchCleanButton ) } }), React.createElement( IconButton, { size: 'small', onClick: (e) => { e.stopPropagation(); onClearKeywords(); setShowFilter(false); }, className: cx( classes.searchCloseButton, (_f = props.classes) === null || _f === void 0 ? void 0 : _f.searchCloseButton ) }, React.createElement(CloseIconRounded, null) ) ) ), classes: { root: classes.root, content: classes.content, label: cx(classes.labelContainer, active[path] && classes.active), iconContainer: classes.iconContainer, focused: classes.focused } }, propsForTreeItem ) ) // endregion ); } export default PathNavigatorTreeItem;