@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
506 lines (504 loc) • 16.1 kB
JavaScript
/*
* 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;