@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
332 lines (330 loc) • 12.9 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, { useMemo, useState } from 'react';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import { getHostToGuestBus } from '../../utils/subjects';
import { makeStyles } from 'tss-react/mui';
import ListItemText from '@mui/material/ListItemText';
import List from '@mui/material/List';
import MenuItem from '@mui/material/MenuItem';
import Select, { selectClasses } from '@mui/material/Select';
import { useDispatch } from 'react-redux';
import {
clearDropTargets,
clearHighlightedDropTargets,
contentTypeDropTargetsRequest,
scrollToDropTarget,
setPreviewEditMode
} from '../../state/actions/preview';
import { SuspenseWithEmptyState } from '../Suspencified/Suspencified';
import { useSelection } from '../../hooks/useSelection';
import { useLogicResource } from '../../hooks/useLogicResource';
import { useMount } from '../../hooks/useMount';
import { getAvatarWithIconColors } from '../../utils/contentType';
import { darken, useTheme } from '@mui/material/styles';
import { ContentTypeField } from '../../icons';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemButton from '@mui/material/ListItemButton';
import Box from '@mui/material/Box';
import { nou } from '../../utils/object';
import InputLabel from '@mui/material/InputLabel';
import FormControl from '@mui/material/FormControl';
import ListSubheader from '@mui/material/ListSubheader';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import HourglassEmptyRounded from '@mui/icons-material/HourglassEmptyRounded';
import Alert from '@mui/material/Alert';
import { EmptyState } from '../EmptyState';
import FormHelperText from '@mui/material/FormHelperText';
const translations = defineMessages({
dropTargetsPanel: {
// Translation not used in code but powers i18n for `ui.xml`
id: 'previewDropTargetsPanelTitle',
defaultMessage: 'Drop Targets'
},
selectedContentType: {
defaultMessage: 'Selected content type'
},
noResults: {
id: 'previewDropTargetsPanel.noResults',
defaultMessage: 'No results found.'
},
chooseContentType: {
id: 'previewDropTargetsPanel.chooseContentType',
defaultMessage: 'Please choose a content type.'
}
});
const useStyles = makeStyles()(() => ({
select: {
width: '100%',
padding: '15px 15px 0',
'& > div': {
width: '100%'
}
}
}));
export function PreviewDropTargetsPanel() {
const { classes } = useStyles();
const hostToGuest$ = getHostToGuestBus();
const dropTargetsBranch = useSelection((state) => state.preview.dropTargets);
const contentTypesBranch = useSelection((state) => state.contentTypes);
const editMode = useSelection((state) => state.preview.editMode);
const contentTypesUpdated = useSelection((state) => state.preview.guest?.contentTypesUpdated);
const contentTypes = contentTypesBranch.byId
? Object.values(contentTypesBranch.byId).filter((contentType) => contentType.type === 'component')
: null;
const { formatMessage } = useIntl();
const dispatch = useDispatch();
const [listMode, setListMode] = useState(true);
const allowedTypesData = useSelection((state) => state.preview.guest?.allowedContentTypes);
const awaitingGuestCheckIn = nou(allowedTypesData);
const allowedContentTypes = useMemo(() => {
const allowedTypes = [];
if (!contentTypes || !allowedTypesData) return allowedTypes;
contentTypes.forEach((contentType) => {
allowedTypesData[contentType.id] && allowedTypes.push(contentType);
});
return allowedTypes;
}, [allowedTypesData, contentTypes]);
useMount(() => {
return () => {
dispatch(clearDropTargets());
hostToGuest$.next({
type: clearHighlightedDropTargets.type
});
};
});
const onSelectedDropZone = (dropTarget) => {
if (!editMode) {
dispatch(setPreviewEditMode({ editMode: true }));
}
hostToGuest$.next({
type: scrollToDropTarget.type,
payload: dropTarget
});
};
function handleSelectChange(contentTypeId) {
hostToGuest$.next(contentTypeDropTargetsRequest({ contentTypeId }));
}
const resetState = () => {
setListMode(true);
dispatch(clearDropTargets());
hostToGuest$.next(clearHighlightedDropTargets());
};
const dropTargetsResource = useLogicResource(dropTargetsBranch, {
shouldResolve: (source) => source.selectedContentType === null || Boolean(source.byId),
shouldReject: (source) => false,
shouldRenew: (source, resource) => resource.complete,
resultSelector: (source) =>
source.byId
? Object.values(source.byId).filter(
(dropTarget) => dropTarget.contentTypeId === dropTargetsBranch.selectedContentType
)
: [],
errorSelector: (source) => null
});
return awaitingGuestCheckIn
? React.createElement(
Alert,
{
severity: 'info',
variant: 'outlined',
icon: React.createElement(HourglassEmptyRounded, null),
sx: { border: 0 }
},
React.createElement(FormattedMessage, { defaultMessage: 'Waiting for the preview application to load.' })
)
: React.createElement(
React.Fragment,
null,
contentTypesUpdated &&
React.createElement(
Alert,
{ severity: 'warning', variant: 'outlined', sx: { border: 0 } },
React.createElement(FormattedMessage, {
defaultMessage: 'Content type definitions have changed. Please refresh the preview application.'
})
),
allowedContentTypes.length
? listMode
? React.createElement(
React.Fragment,
null,
React.createElement(
FormHelperText,
{ sx: { p: 2 } },
React.createElement(FormattedMessage, {
defaultMessage: 'Select content type to view the available drop targets for it'
})
),
React.createElement(
ListSubheader,
null,
React.createElement(FormattedMessage, { defaultMessage: 'Compatible types' })
),
allowedContentTypes?.map((contentType, i) =>
React.createElement(
ListItemButton,
{
key: i,
onClick: () => {
setListMode(false);
handleSelectChange(contentType.id);
}
},
React.createElement(ContentTypeItem, { contentType: contentType })
)
)
)
: React.createElement(
React.Fragment,
null,
React.createElement(
Box,
{ className: classes.select, display: 'flex', alignItems: 'center' },
React.createElement(
FormControl,
null,
React.createElement(InputLabel, null, formatMessage(translations.selectedContentType)),
React.createElement(
Select,
{
value: dropTargetsBranch.selectedContentType || '',
label: formatMessage(translations.selectedContentType),
sx: {
[`& .${selectClasses.select}`]: {
display: 'flex',
alignItems: 'center',
overflow: 'hidden'
}
},
onChange: (event) => {
event.stopPropagation();
handleSelectChange(event.target.value);
}
},
React.createElement(
ListSubheader,
null,
React.createElement(FormattedMessage, { defaultMessage: 'Compatible types' })
),
allowedContentTypes?.map((contentType, i) =>
React.createElement(
MenuItem,
{ value: contentType.id, key: i },
React.createElement(ContentTypeItem, { contentType: contentType })
)
)
)
),
dropTargetsBranch?.selectedContentType &&
React.createElement(
Tooltip,
{ title: React.createElement(FormattedMessage, { defaultMessage: 'Cancel selection' }) },
React.createElement(
IconButton,
{ edge: 'end', sx: { ml: 0.625 }, onClick: () => resetState() },
React.createElement(CloseRoundedIcon, null)
)
)
),
React.createElement(
List,
null,
React.createElement(
SuspenseWithEmptyState,
{
resource: dropTargetsResource,
withEmptyStateProps: {
emptyStateProps: {
title: dropTargetsBranch.selectedContentType
? formatMessage(translations.noResults)
: formatMessage(translations.chooseContentType)
}
}
},
React.createElement(DropTargetsList, {
resource: dropTargetsResource,
onSelectedDropZone: onSelectedDropZone
})
)
)
)
: React.createElement(EmptyState, {
title: 'No drop targets were found on the current view.',
sxs: { title: { textAlign: 'center' } }
})
);
}
function ContentTypeItem(props) {
const { contentType } = props;
const theme = useTheme();
const { backgroundColor, textColor } = getAvatarWithIconColors(contentType.name, theme, darken);
return React.createElement(
React.Fragment,
null,
React.createElement(
ListItemIcon,
{ sx: { minWidth: 'unset !important' } },
React.createElement(Box, {
sx: {
flexShrink: 0,
width: '24px',
height: '24px',
borderRadius: '20px',
overflow: 'hidden',
backgroundColor,
borderColor: textColor,
borderStyle: 'solid',
borderWidth: '1px'
}
})
),
React.createElement(
ListItemText,
{ primaryTypographyProps: { noWrap: true }, title: contentType.name },
contentType.name
)
);
}
function DropTargetsList(props) {
const dropTargets = props.resource.read();
return dropTargets?.map((dropTarget) =>
React.createElement(
ListItemButton,
{ key: dropTarget.id, onClick: () => props.onSelectedDropZone(dropTarget) },
React.createElement(ListItemIcon, null, React.createElement(ContentTypeField, null)),
React.createElement(ListItemText, { primary: dropTarget.label })
)
);
}
export default PreviewDropTargetsPanel;