decap-cms-core
Version:
Decap CMS core application, see decap-cms package for the main distribution.
689 lines (616 loc) • 18.6 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { translate } from 'react-polyglot';
import { Link } from 'react-router-dom';
import {
Icon,
Dropdown,
DropdownItem,
StyledDropdownButton,
colorsRaw,
colors,
components,
buttons,
zIndex,
} from 'decap-cms-ui-default';
import { status } from '../../constants/publishModes';
import { SettingsDropdown } from '../UI';
const styles = {
noOverflow: css`
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
`,
buttonMargin: css`
margin: 0 10px;
`,
toolbarSection: css`
height: 100%;
display: flex;
align-items: center;
border: 0 solid ${colors.textFieldBorder};
`,
publishedButton: css`
background-color: ${colorsRaw.tealLight};
color: ${colorsRaw.tealDark};
`,
};
const TooltipText = styled.div`
visibility: hidden;
width: 321px;
background-color: #555;
color: #fff;
text-align: unset;
border-radius: 6px;
padding: 5px;
/* Position the tooltip text */
position: absolute;
z-index: 1;
top: 145%;
left: 50%;
margin-left: -320px;
/* Fade in tooltip */
opacity: 0;
transition: opacity 0.3s;
`;
const Tooltip = styled.div`
position: relative;
display: inline-block;
&:hover + ${TooltipText} {
visibility: visible;
opacity: 0.9;
}
`;
const TooltipContainer = styled.div`
position: relative;
`;
const DropdownButton = styled(StyledDropdownButton)`
${styles.noOverflow}
@media (max-width: 1200px) {
padding-left: 10px;
}
`;
const ToolbarContainer = styled.div`
box-shadow: 0 2px 6px 0 rgba(68, 74, 87, 0.05), 0 1px 3px 0 rgba(68, 74, 87, 0.1),
0 2px 54px rgba(0, 0, 0, 0.1);
position: absolute;
top: 0;
left: 0;
width: 100%;
min-width: 800px;
z-index: ${zIndex.zIndex300};
background-color: #fff;
height: 66px;
display: flex;
justify-content: space-between;
`;
const ToolbarSectionMain = styled.div`
${styles.toolbarSection};
flex: 10;
display: flex;
justify-content: space-between;
padding: 0 10px;
`;
const ToolbarSubSectionFirst = styled.div`
display: flex;
align-items: center;
`;
const ToolbarSubSectionLast = styled(ToolbarSubSectionFirst)`
justify-content: flex-end;
`;
const ToolbarSectionBackLink = styled(Link)`
${styles.toolbarSection};
border-right-width: 1px;
font-weight: normal;
padding: 0 20px;
&:hover,
&:focus {
background-color: #f1f2f4;
}
`;
const ToolbarSectionMeta = styled.div`
${styles.toolbarSection};
border-left-width: 1px;
padding: 0 7px;
`;
const ToolbarDropdown = styled(Dropdown)`
${styles.buttonMargin};
${Icon} {
color: ${colorsRaw.teal};
}
`;
const BackArrow = styled.div`
color: ${colors.textLead};
font-size: 21px;
font-weight: 600;
margin-right: 16px;
`;
const BackCollection = styled.div`
color: ${colors.textLead};
font-size: 14px;
`;
const BackStatus = styled.div`
margin-top: 6px;
`;
const BackStatusUnchanged = styled(BackStatus)`
${components.textBadgeSuccess};
`;
const BackStatusChanged = styled(BackStatus)`
${components.textBadgeDanger};
`;
const ToolbarButton = styled.button`
${buttons.button};
${buttons.default};
${styles.buttonMargin};
${styles.noOverflow};
display: block;
@media (max-width: 1200px) {
padding: 0 10px;
}
`;
const DeleteButton = styled(ToolbarButton)`
${buttons.lightRed};
`;
const SaveButton = styled(ToolbarButton)`
${buttons.lightBlue};
&[disabled] {
${buttons.disabled};
}
`;
const PublishedToolbarButton = styled(DropdownButton)`
${styles.publishedButton}
`;
const PublishedButton = styled(ToolbarButton)`
${styles.publishedButton}
`;
const PublishButton = styled(DropdownButton)`
background-color: ${colorsRaw.teal};
`;
const StatusButton = styled(DropdownButton)`
background-color: ${colorsRaw.tealLight};
color: ${colorsRaw.teal};
`;
const PreviewButtonContainer = styled.div`
margin-right: 12px;
color: ${colorsRaw.blue};
display: flex;
align-items: center;
a,
${Icon} {
color: ${colorsRaw.blue};
}
${Icon} {
position: relative;
top: 1px;
}
`;
const RefreshPreviewButton = styled.button`
background: none;
border: 0;
cursor: pointer;
color: ${colorsRaw.blue};
span {
margin-right: 6px;
}
`;
const PreviewLink = RefreshPreviewButton.withComponent('a');
const PublishDropDownItem = styled(DropdownItem)`
min-width: initial;
`;
const StatusDropdownItem = styled(DropdownItem)`
${Icon} {
color: ${colors.infoText};
}
`;
export class EditorToolbar extends React.Component {
static propTypes = {
isPersisting: PropTypes.bool,
isPublishing: PropTypes.bool,
isUpdatingStatus: PropTypes.bool,
isDeleting: PropTypes.bool,
onPersist: PropTypes.func.isRequired,
onPersistAndNew: PropTypes.func.isRequired,
onPersistAndDuplicate: PropTypes.func.isRequired,
showDelete: PropTypes.bool.isRequired,
onDelete: PropTypes.func.isRequired,
onDeleteUnpublishedChanges: PropTypes.func.isRequired,
onChangeStatus: PropTypes.func.isRequired,
onPublish: PropTypes.func.isRequired,
unPublish: PropTypes.func.isRequired,
onDuplicate: PropTypes.func.isRequired,
onPublishAndNew: PropTypes.func.isRequired,
onPublishAndDuplicate: PropTypes.func.isRequired,
user: PropTypes.object,
hasChanged: PropTypes.bool,
displayUrl: PropTypes.string,
collection: ImmutablePropTypes.map.isRequired,
hasWorkflow: PropTypes.bool,
useOpenAuthoring: PropTypes.bool,
hasUnpublishedChanges: PropTypes.bool,
isNewEntry: PropTypes.bool,
isModification: PropTypes.bool,
currentStatus: PropTypes.string,
onLogoutClick: PropTypes.func.isRequired,
deployPreview: PropTypes.object,
loadDeployPreview: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
editorBackLink: PropTypes.string.isRequired,
};
componentDidMount() {
const { isNewEntry, loadDeployPreview } = this.props;
if (!isNewEntry) {
loadDeployPreview({ maxAttempts: 3 });
}
}
componentDidUpdate(prevProps) {
const { isNewEntry, isPersisting, loadDeployPreview } = this.props;
if (!isNewEntry && prevProps.isPersisting && !isPersisting) {
loadDeployPreview({ maxAttempts: 3 });
}
}
renderSimpleControls = () => {
const { collection, hasChanged, isNewEntry, showDelete, onDelete, t } = this.props;
const canCreate = collection.get('create');
return (
<>
{!isNewEntry && !hasChanged
? this.renderExistingEntrySimplePublishControls({ canCreate })
: this.renderNewEntrySimplePublishControls({ canCreate })}
<div>
{showDelete ? (
<DeleteButton onClick={onDelete}>{t('editor.editorToolbar.deleteEntry')}</DeleteButton>
) : null}
</div>
</>
);
};
renderDeployPreviewControls = label => {
const { deployPreview = {}, loadDeployPreview, t } = this.props;
const { url, status, isFetching } = deployPreview;
if (!status) {
return;
}
const deployPreviewReady = status === 'SUCCESS' && !isFetching;
return (
<PreviewButtonContainer>
{deployPreviewReady ? (
<PreviewLink rel="noopener noreferrer" target="_blank" href={url}>
<span>{label}</span>
<Icon type="new-tab" size="xsmall" />
</PreviewLink>
) : (
<RefreshPreviewButton onClick={loadDeployPreview}>
<span>{t('editor.editorToolbar.deployPreviewPendingButtonLabel')}</span>
<Icon type="refresh" size="xsmall" />
</RefreshPreviewButton>
)}
</PreviewButtonContainer>
);
};
renderStatusInfoTooltip = () => {
const { t, currentStatus } = this.props;
const statusToLocaleKey = {
[status.get('DRAFT')]: 'statusInfoTooltipDraft',
[status.get('PENDING_REVIEW')]: 'statusInfoTooltipInReview',
};
const statusKey = Object.keys(statusToLocaleKey).find(key => key === currentStatus);
return (
<TooltipContainer>
<Tooltip>
<Icon type="info-circle" size="small" className="tooltip" />
</Tooltip>
{statusKey && (
<TooltipText>{t(`editor.editorToolbar.${statusToLocaleKey[statusKey]}`)}</TooltipText>
)}
</TooltipContainer>
);
};
renderWorkflowStatusControls = () => {
const { isUpdatingStatus, onChangeStatus, currentStatus, t, useOpenAuthoring } = this.props;
const statusToTranslation = {
[status.get('DRAFT')]: t('editor.editorToolbar.draft'),
[status.get('PENDING_REVIEW')]: t('editor.editorToolbar.inReview'),
[status.get('PENDING_PUBLISH')]: t('editor.editorToolbar.ready'),
};
const buttonText = isUpdatingStatus
? t('editor.editorToolbar.updating')
: t('editor.editorToolbar.status', { status: statusToTranslation[currentStatus] });
return (
<>
<ToolbarDropdown
dropdownTopOverlap="40px"
dropdownWidth="120px"
renderButton={() => <StatusButton>{buttonText}</StatusButton>}
>
<StatusDropdownItem
label={t('editor.editorToolbar.draft')}
onClick={() => onChangeStatus('DRAFT')}
icon={currentStatus === status.get('DRAFT') ? 'check' : null}
/>
<StatusDropdownItem
label={t('editor.editorToolbar.inReview')}
onClick={() => onChangeStatus('PENDING_REVIEW')}
icon={currentStatus === status.get('PENDING_REVIEW') ? 'check' : null}
/>
{useOpenAuthoring ? (
''
) : (
<StatusDropdownItem
label={t('editor.editorToolbar.ready')}
onClick={() => onChangeStatus('PENDING_PUBLISH')}
icon={currentStatus === status.get('PENDING_PUBLISH') ? 'check' : null}
/>
)}
</ToolbarDropdown>
{useOpenAuthoring && this.renderStatusInfoTooltip()}
</>
);
};
renderNewEntryWorkflowPublishControls = ({ canCreate, canPublish }) => {
const { isPublishing, onPublish, onPublishAndNew, onPublishAndDuplicate, t } = this.props;
return canPublish ? (
<ToolbarDropdown
dropdownTopOverlap="40px"
dropdownWidth="200px"
renderButton={() => (
<PublishButton>
{isPublishing
? t('editor.editorToolbar.publishing')
: t('editor.editorToolbar.publish')}
</PublishButton>
)}
>
<PublishDropDownItem
label={t('editor.editorToolbar.publishNow')}
icon="arrow"
iconDirection="right"
onClick={onPublish}
/>
{canCreate ? (
<>
<PublishDropDownItem
label={t('editor.editorToolbar.publishAndCreateNew')}
icon="add"
onClick={onPublishAndNew}
/>
<PublishDropDownItem
label={t('editor.editorToolbar.publishAndDuplicate')}
icon="add"
onClick={onPublishAndDuplicate}
/>
</>
) : null}
</ToolbarDropdown>
) : (
''
);
};
renderExistingEntryWorkflowPublishControls = ({ canCreate, canPublish, canDelete }) => {
const { unPublish, onDuplicate, isPersisting, t } = this.props;
return canPublish || canCreate ? (
<ToolbarDropdown
dropdownTopOverlap="40px"
dropdownWidth="150px"
key="td-publish-create"
renderButton={() => (
<PublishedToolbarButton>
{isPersisting
? t('editor.editorToolbar.unpublishing')
: t('editor.editorToolbar.published')}
</PublishedToolbarButton>
)}
>
{canDelete && canPublish && (
<DropdownItem
label={t('editor.editorToolbar.unpublish')}
icon="arrow"
iconDirection="right"
onClick={unPublish}
/>
)}
{canCreate && (
<DropdownItem
label={t('editor.editorToolbar.duplicate')}
icon="add"
onClick={onDuplicate}
/>
)}
</ToolbarDropdown>
) : (
''
);
};
renderExistingEntrySimplePublishControls = ({ canCreate }) => {
const { onDuplicate, t } = this.props;
return canCreate ? (
<ToolbarDropdown
dropdownTopOverlap="40px"
dropdownWidth="150px"
renderButton={() => (
<PublishedToolbarButton>{t('editor.editorToolbar.published')}</PublishedToolbarButton>
)}
>
{
<DropdownItem
label={t('editor.editorToolbar.duplicate')}
icon="add"
onClick={onDuplicate}
/>
}
</ToolbarDropdown>
) : (
<PublishedButton>{t('editor.editorToolbar.published')}</PublishedButton>
);
};
renderNewEntrySimplePublishControls = ({ canCreate }) => {
const { onPersist, onPersistAndNew, onPersistAndDuplicate, isPersisting, t } = this.props;
return (
<div>
<ToolbarDropdown
dropdownTopOverlap="40px"
dropdownWidth="150px"
renderButton={() => (
<PublishButton>
{isPersisting
? t('editor.editorToolbar.publishing')
: t('editor.editorToolbar.publish')}
</PublishButton>
)}
>
<DropdownItem
label={t('editor.editorToolbar.publishNow')}
icon="arrow"
iconDirection="right"
onClick={onPersist}
/>
{canCreate ? (
<>
<DropdownItem
label={t('editor.editorToolbar.publishAndCreateNew')}
icon="add"
onClick={onPersistAndNew}
/>
<DropdownItem
label={t('editor.editorToolbar.publishAndDuplicate')}
icon="add"
onClick={onPersistAndDuplicate}
/>
</>
) : null}
</ToolbarDropdown>
</div>
);
};
renderSimpleDeployPreviewControls = () => {
const { hasChanged, isNewEntry, t } = this.props;
if (!isNewEntry && !hasChanged) {
return this.renderDeployPreviewControls(t('editor.editorToolbar.deployButtonLabel'));
}
};
renderWorkflowControls = () => {
const {
onPersist,
onDelete,
onDeleteUnpublishedChanges,
showDelete,
hasChanged,
hasUnpublishedChanges,
useOpenAuthoring,
isPersisting,
isDeleting,
isNewEntry,
isModification,
currentStatus,
collection,
t,
} = this.props;
const canCreate = collection.get('create');
const canPublish = collection.get('publish') && !useOpenAuthoring;
const canDelete = collection.get('delete', true);
const deleteLabel =
(hasUnpublishedChanges &&
isModification &&
t('editor.editorToolbar.deleteUnpublishedChanges')) ||
(hasUnpublishedChanges &&
(isNewEntry || !isModification) &&
t('editor.editorToolbar.deleteUnpublishedEntry')) ||
(!hasUnpublishedChanges && !isModification && t('editor.editorToolbar.deletePublishedEntry'));
return [
<SaveButton
disabled={!hasChanged}
key="save-button"
onClick={() => hasChanged && onPersist()}
>
{isPersisting ? t('editor.editorToolbar.saving') : t('editor.editorToolbar.save')}
</SaveButton>,
currentStatus
? [
<React.Fragment key="workflow-status-controls">
{this.renderWorkflowStatusControls()}
{!hasChanged && this.renderNewEntryWorkflowPublishControls({ canCreate, canPublish })}
</React.Fragment>,
]
: !isNewEntry && (
<React.Fragment key="existing-entry-workflow-publish-controls">
{this.renderExistingEntryWorkflowPublishControls({
canCreate,
canPublish,
canDelete,
})}
</React.Fragment>
),
(!showDelete || useOpenAuthoring) && !hasUnpublishedChanges && !isModification ? null : (
<DeleteButton
key="delete-button"
onClick={hasUnpublishedChanges ? onDeleteUnpublishedChanges : onDelete}
>
{isDeleting ? t('editor.editorToolbar.deleting') : deleteLabel}
</DeleteButton>
),
];
};
renderWorkflowDeployPreviewControls = () => {
const { currentStatus, isNewEntry, t } = this.props;
if (currentStatus) {
return this.renderDeployPreviewControls(t('editor.editorToolbar.deployPreviewButtonLabel'));
}
/**
* Publish control for published workflow entry.
*/
if (!isNewEntry) {
return this.renderDeployPreviewControls(t('editor.editorToolbar.deployButtonLabel'));
}
};
render() {
const {
user,
hasChanged,
displayUrl,
collection,
hasWorkflow,
onLogoutClick,
t,
editorBackLink,
} = this.props;
return (
<ToolbarContainer>
<ToolbarSectionBackLink to={editorBackLink}>
<BackArrow>←</BackArrow>
<div>
<BackCollection>
{t('editor.editorToolbar.backCollection', {
collectionLabel: collection.get('label'),
})}
</BackCollection>
{hasChanged ? (
<BackStatusChanged>{t('editor.editorToolbar.unsavedChanges')}</BackStatusChanged>
) : (
<BackStatusUnchanged>{t('editor.editorToolbar.changesSaved')}</BackStatusUnchanged>
)}
</div>
</ToolbarSectionBackLink>
<ToolbarSectionMain>
<ToolbarSubSectionFirst>
{hasWorkflow ? this.renderWorkflowControls() : this.renderSimpleControls()}
</ToolbarSubSectionFirst>
<ToolbarSubSectionLast>
{hasWorkflow
? this.renderWorkflowDeployPreviewControls()
: this.renderSimpleDeployPreviewControls()}
</ToolbarSubSectionLast>
</ToolbarSectionMain>
<ToolbarSectionMeta>
<SettingsDropdown
displayUrl={displayUrl}
imageUrl={user?.avatar_url}
onLogoutClick={onLogoutClick}
/>
</ToolbarSectionMeta>
</ToolbarContainer>
);
}
}
export default translate()(EditorToolbar);