UNPKG

@craftercms/studio-ui

Version:

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

506 lines (504 loc) 16.1 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, { Suspense, useEffect, useMemo } from 'react'; import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; import { makeStyles } from 'tss-react/mui'; import Popover from '@mui/material/Popover'; import Grid from '@mui/material/Grid'; import Typography from '@mui/material/Typography'; import LauncherSiteCard from '../LauncherSiteCard/LauncherSiteCard'; import CloseIcon from '@mui/icons-material/Close'; import IconButton from '@mui/material/IconButton'; import Hidden from '@mui/material/Hidden'; import { useDispatch } from 'react-redux'; import { getInitials } from '../../utils/string'; import { changeSite } from '../../state/actions/sites'; import palette from '../../styles/palette'; import Avatar from '@mui/material/Avatar'; import ExitToAppRoundedIcon from '@mui/icons-material/ExitToAppRounded'; import Card from '@mui/material/Card'; import CardHeader from '@mui/material/CardHeader'; import EmptyState from '../EmptyState/EmptyState'; import { setSiteCookie } from '../../utils/auth'; import List from '@mui/material/List'; import CrafterCMSLogo from '../../icons/CrafterCMSLogo'; import { renderWidgets } from '../Widget'; import { logout } from '../../state/actions/auth'; import { ListItem, Tooltip } from '@mui/material'; import { closeLauncher } from '../../state/actions/dialogs'; import { batchActions } from '../../state/actions/misc'; import LauncherGlobalNav from '../LauncherGlobalNav'; import Skeleton from '@mui/material/Skeleton'; import { useActiveSiteId } from '../../hooks/useActiveSiteId'; import { useEnv } from '../../hooks/useEnv'; import { useSystemVersion } from '../../hooks/useSystemVersion'; import { useActiveUser } from '../../hooks/useActiveUser'; import { useSiteList } from '../../hooks/useSiteList'; import { useSiteUIConfig } from '../../hooks/useSiteUIConfig'; import { initLauncherConfig } from '../../state/actions/launcher'; import { getSystemLink } from '../../utils/system'; import { PREVIEW_URL_PATH } from '../../utils/constants'; import useMinimizedDialogWarning from '../../hooks/useMinimizedDialogWarning'; const messages = defineMessages({ mySites: { id: 'globalMenu.mySites', defaultMessage: 'My Projects' }, preview: { id: 'words.preview', defaultMessage: 'Preview' }, search: { id: 'words.search', defaultMessage: 'Search' }, signOut: { id: 'launcherOpenerButton.signOut', defaultMessage: 'Sign Out' }, closeMenu: { id: 'globalMenu.closeMenu', defaultMessage: 'Close menu' }, logout: { id: 'words.logout', defaultMessage: 'Logout' } }); const useLauncherStyles = makeStyles()((theme) => ({ popover: { maxWidth: 1065, borderRadius: '10px', overflowY: 'hidden' }, appsSkeletonTile: { margin: 5, width: 120, height: 100, display: 'inline-flex' }, sitesRail: { backgroundColor: theme.palette.background.default }, appsRail: {}, railTop: { padding: '30px', overflow: 'auto', height: 'calc(100% - 65px)', maxHeight: 'calc(100vh - 95px)' }, railTopExtraPadded: { paddingTop: 70 }, railBottom: { height: 65, display: 'flex', alignItems: 'center', padding: '0 20px', placeContent: 'center space-between' }, gridContainer: { height: '100%', maxHeight: '100%', '@media(min-width: 1097px)': { width: 1065 } }, versionText: {}, titleCard: { marginBottom: '20px' }, closeButton: { position: 'absolute', top: '10px', right: '10px', '&.left': { right: 'auto', left: '10px' } }, simpleGear: { margin: 'auto' }, userCardRoot: { width: '100%', boxShadow: 'none' }, userCardHeader: { padding: 0 }, userCardActions: { marginTop: 0, marginRight: 0 }, userCardAvatar: { color: palette.white, textTransform: 'uppercase', backgroundColor: palette.red.main }, username: { maxWidth: '300px', overflow: 'hidden', textOverflow: 'ellipsis' }, mySitesTitle: { marginBottom: '24px', textTransform: 'uppercase', fontWeight: 600 } })); const UserDisplaySection = ({ classes, formatMessage, user, onLogout }) => React.createElement( 'div', { className: classes.railBottom }, React.createElement( Card, { className: classes.userCardRoot }, React.createElement(CardHeader, { classes: { action: classes.userCardActions }, className: classes.userCardHeader, avatar: React.createElement(Avatar, { 'aria-hidden': 'true', className: classes.userCardAvatar, children: getInitials(`${user.firstName} ${user.lastName}`) }), action: React.createElement( Tooltip, { title: formatMessage(messages.logout) }, React.createElement( IconButton, { 'aria-label': formatMessage(messages.signOut), onClick: onLogout, size: 'large' }, React.createElement(ExitToAppRoundedIcon, null) ) ), title: `${user.firstName} ${user.lastName}`, subheader: user.username || user.email, subheaderTypographyProps: { className: classes.username } }) ) ); const AppsRail = ({ classes, widgets, formatMessage, user, onLogout, closeButtonPosition, userRoles, globalNavigationPosition, clsx, lonely }) => React.createElement( Grid, { item: true, xs: 12, md: lonely ? 12 : 8, className: classes.appsRail }, React.createElement( 'div', { className: clsx(classes.railTop, closeButtonPosition === 'left' && classes.railTopExtraPadded) }, globalNavigationPosition === 'before' && React.createElement(LauncherGlobalNav, null), renderWidgets(widgets, { userRoles }), globalNavigationPosition !== 'before' && React.createElement(LauncherGlobalNav, null) ), React.createElement(UserDisplaySection, { classes: classes, formatMessage: formatMessage, onLogout: onLogout, user: user }) ); const AppsRailSkeleton = ({ classes, closeButtonPosition, formatMessage, onLogout, user, clsx }) => React.createElement( Grid, { item: true, xs: 12, md: 8, className: classes.appsRail }, React.createElement( 'div', { className: clsx(classes.railTop, closeButtonPosition === 'left' && classes.railTopExtraPadded) }, React.createElement(Skeleton, { variant: 'text', width: '150px', style: { marginBottom: 20 } }), new Array(9) .fill(null) .map((_, i) => React.createElement(Skeleton, { key: i, variant: 'rectangular', className: classes.appsSkeletonTile }) ) ), React.createElement(UserDisplaySection, { classes: classes, formatMessage: formatMessage, onLogout: onLogout, user: user }) ); const SitesRail = ({ classes, formatMessage, sites, site, onSiteCardClick, options, version }) => React.createElement( Hidden, { only: ['xs', 'sm'] }, React.createElement( Grid, { item: true, md: 4, className: classes.sitesRail }, React.createElement( 'div', { className: classes.railTop }, React.createElement( Typography, { variant: 'subtitle1', component: 'h2', className: classes.mySitesTitle }, formatMessage(messages.mySites) ), sites.length ? React.createElement( List, null, sites.map((item, i) => React.createElement(LauncherSiteCard, { key: i, selected: item.id === site, title: item.name, value: item.id, classes: { root: classes.titleCard }, state: item.state, onCardClick: () => onSiteCardClick(item.id), options: options }) ) ) : React.createElement(EmptyState, { title: React.createElement(FormattedMessage, { id: 'globalMenu.noSitesMessage', defaultMessage: 'No projects to display.' }) }) ), React.createElement( 'div', { className: classes.railBottom }, React.createElement(CrafterCMSLogo, { width: 115 }), React.createElement( Typography, { className: classes.versionText, color: 'textSecondary', variant: 'caption' }, version ) ) ) ); const SiteRailSkeleton = ({ classes, formatMessage, version }) => React.createElement( Grid, { item: true, md: 4, className: classes.sitesRail }, React.createElement( 'div', { className: classes.railTop }, React.createElement( Typography, { variant: 'subtitle1', component: 'h2', className: classes.mySitesTitle }, formatMessage(messages.mySites) ), React.createElement( List, null, new Array(3) .fill(null) .map((_, i) => React.createElement( ListItem, { key: i }, React.createElement(Skeleton, { variant: 'rectangular', width: '100%', height: '72px' }) ) ) ) ), React.createElement( 'div', { className: classes.railBottom }, React.createElement(CrafterCMSLogo, { width: 115 }), React.createElement( Typography, { className: classes.versionText, color: 'textSecondary', variant: 'caption' }, version ) ) ); // endregion export function Launcher(props) { const { classes, cx } = useLauncherStyles(); const siteId = useActiveSiteId(); const sites = useSiteList(); const user = useActiveUser(); const dispatch = useDispatch(); const version = useSystemVersion(); const { formatMessage } = useIntl(); const { authoringBase, useBaseDomain } = useEnv(); const { open, anchor: anchorSelector, sitesRailPosition = 'left', closeButtonPosition = 'right', globalNavigationPosition = 'after', siteCardMenuLinks, widgets } = props; const uiConfig = useSiteUIConfig(); const userRoles = user.rolesBySite[siteId]; const anchor = useMemo(() => (anchorSelector ? document.querySelector(anchorSelector) : null), [anchorSelector]); const cardActions = useMemo( () => siteCardMenuLinks ? siteCardMenuLinks .filter( (widget) => (widget.permittedRoles ?? []).length === 0 || (userRoles ?? []).some((role) => widget.permittedRoles.includes(role)) ) .map((descriptor) => ({ name: typeof descriptor.title === 'string' ? descriptor.title : formatMessage(descriptor.title), href: (site) => getSystemLink({ systemLinkId: descriptor.systemLinkId, authoringBase, site }), onClick(site) { setSiteCookie(site, useBaseDomain); } })) : null, [siteCardMenuLinks, userRoles, formatMessage, authoringBase, useBaseDomain] ); const checkMinimized = useMinimizedDialogWarning(); useEffect(() => { if (uiConfig.xml) { dispatch(initLauncherConfig({ configXml: uiConfig.xml })); } }, [uiConfig.xml, dispatch]); const onSiteCardClick = (site) => { if (!checkMinimized()) { setSiteCookie(site, useBaseDomain); if (window.location.href.includes(PREVIEW_URL_PATH)) { // If user is in UI next and switching to a site that's viewed in 4. dispatch(batchActions([changeSite(site), closeLauncher()])); } else { window.location.href = getSystemLink({ systemLinkId: 'preview', authoringBase, site }); } } }; const onMenuClose = () => dispatch(closeLauncher()); const onLogout = () => dispatch(logout()); const sitesRail = () => React.createElement(SitesRail, { classes: classes, formatMessage: formatMessage, sites: sites, site: siteId, version: version, options: cardActions, onSiteCardClick: onSiteCardClick }); const appsRail = () => React.createElement(AppsRail, { classes: classes, widgets: widgets, formatMessage: formatMessage, user: user, onLogout: onLogout, closeButtonPosition: closeButtonPosition, userRoles: userRoles, globalNavigationPosition: globalNavigationPosition, clsx: cx, lonely: sitesRailPosition === 'hidden' }); const sitesRailSkeleton = () => React.createElement(SiteRailSkeleton, { classes: classes, formatMessage: formatMessage, version: version }); const appsRailSkeleton = () => React.createElement(AppsRailSkeleton, { classes: classes, formatMessage: formatMessage, closeButtonPosition: closeButtonPosition, onLogout: onLogout, user: user, clsx: cx }); return React.createElement( Popover, { open: open && Boolean(anchor), anchorEl: anchor, onClose: onMenuClose, classes: { paper: classes.popover }, anchorOrigin: { vertical: 'top', horizontal: 'right' }, transformOrigin: { vertical: 'top', horizontal: 'right' } }, React.createElement( Tooltip, { title: formatMessage(messages.closeMenu) }, React.createElement( IconButton, { 'aria-label': formatMessage(messages.closeMenu), className: cx(classes.closeButton, closeButtonPosition), onClick: onMenuClose, size: 'large' }, React.createElement(CloseIcon, null) ) ), React.createElement( Suspense, { fallback: React.createElement( Grid, { container: true, spacing: 0, className: classes.gridContainer }, sitesRailPosition === 'left' ? React.createElement(React.Fragment, null, sitesRailSkeleton(), appsRailSkeleton()) : sitesRailPosition === 'right' ? React.createElement(React.Fragment, null, appsRailSkeleton(), sitesRailSkeleton()) : appsRailSkeleton() ) }, React.createElement( Grid, { container: true, spacing: 0, className: classes.gridContainer }, sitesRailPosition === 'left' ? React.createElement(React.Fragment, null, sitesRail(), appsRail()) : sitesRailPosition === 'right' ? React.createElement(React.Fragment, null, appsRail(), sitesRail()) : appsRail() ) ) ); } export default Launcher;