feat: workflow unpublished entry (#2914)
* feat: workflow unpublished entry * fix: post rebase fix - load unpublished entry after unpublish * feat: change unpublish button to dropdown * test(cypress): add unpublish entry cypress test
This commit is contained in:
parent
465f463959
commit
41bb9aac0d
@ -18,6 +18,7 @@ import {
|
||||
validateObjectFieldsAndExit,
|
||||
validateNestedObjectFieldsAndExit,
|
||||
validateListFieldsAndExit,
|
||||
unpublishEntry,
|
||||
} from '../utils/steps';
|
||||
import { setting1, setting2, workflowStatus, editorStatus } from '../utils/constants';
|
||||
|
||||
@ -123,4 +124,15 @@ describe('Test Backend Editorial Workflow', () => {
|
||||
goToWorkflow();
|
||||
assertWorkflowStatus(entry1, workflowStatus.ready);
|
||||
});
|
||||
|
||||
it('can unpublish an existing entry', () => {
|
||||
// first publish an entry
|
||||
login();
|
||||
createPostAndExit(entry1);
|
||||
goToWorkflow();
|
||||
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
|
||||
publishWorkflowEntry(entry1);
|
||||
// then unpublish it
|
||||
unpublishEntry(entry1);
|
||||
});
|
||||
});
|
||||
|
@ -5,6 +5,7 @@ const setting2 = { name: 'Andrew Wommack', description: 'A Gospel Teacher' };
|
||||
const notifications = {
|
||||
saved: 'Entry saved',
|
||||
published: 'Entry published',
|
||||
unpublished: 'Entry unpublished',
|
||||
updated: 'Entry status updated',
|
||||
deletedUnpublished: 'Unpublished changes deleted',
|
||||
error: {
|
||||
|
@ -212,6 +212,23 @@ function updateExistingPostAndExit(fromEntry, toEntry) {
|
||||
cy.contains('h2', toEntry.title);
|
||||
}
|
||||
|
||||
function unpublishEntry(entry) {
|
||||
goToCollections();
|
||||
cy.contains('h2', entry.title)
|
||||
.parent()
|
||||
.click({ force: true });
|
||||
cy.contains('[role="button"]', 'Published').as('publishedButton');
|
||||
cy.get('@publishedButton')
|
||||
.parent()
|
||||
.within(() => {
|
||||
cy.get('@publishedButton').click();
|
||||
cy.contains('[role="menuitem"] span', 'Unpublish').click();
|
||||
});
|
||||
assertNotification(notifications.unpublished);
|
||||
goToWorkflow();
|
||||
assertWorkflowStatus(entry, workflowStatus.ready);
|
||||
}
|
||||
|
||||
function validateObjectFields({ limit, author }) {
|
||||
cy.get('a[href^="#/collections/settings"]').click();
|
||||
cy.get('a[href^="#/collections/settings/entries/general"]').click();
|
||||
@ -303,4 +320,5 @@ module.exports = {
|
||||
validateObjectFieldsAndExit,
|
||||
validateNestedObjectFieldsAndExit,
|
||||
validateListFieldsAndExit,
|
||||
unpublishEntry,
|
||||
};
|
||||
|
@ -667,7 +667,7 @@ export default class API {
|
||||
}
|
||||
: undefined,
|
||||
user: user.name || user.login,
|
||||
status: this.initialWorkflowStatus,
|
||||
status: options.status || this.initialWorkflowStatus,
|
||||
branch: branchName,
|
||||
collection: options.collectionName,
|
||||
commitMessage: options.commitMessage,
|
||||
|
@ -175,7 +175,7 @@ export default class TestBackend {
|
||||
},
|
||||
metaData: {
|
||||
collection: options.collectionName,
|
||||
status: this.options.initialWorkflowStatus,
|
||||
status: options.status || this.options.initialWorkflowStatus,
|
||||
title: options.parsedData && options.parsedData.title,
|
||||
description: options.parsedData && options.parsedData.description,
|
||||
},
|
||||
|
@ -2,14 +2,16 @@ import uuid from 'uuid/v4';
|
||||
import { get } from 'lodash';
|
||||
import { actions as notifActions } from 'redux-notifications';
|
||||
import { BEGIN, COMMIT, REVERT } from 'redux-optimist';
|
||||
import { Map } from 'immutable';
|
||||
import { serializeValues } from 'Lib/serializeEntryValues';
|
||||
import { currentBackend } from 'coreSrc/backend';
|
||||
import { selectPublishedSlugs, selectUnpublishedSlugs } from 'Reducers';
|
||||
import { selectPublishedSlugs, selectUnpublishedSlugs, selectEntry } from 'Reducers';
|
||||
import { selectFields } from 'Reducers/collections';
|
||||
import { EDITORIAL_WORKFLOW } from 'Constants/publishModes';
|
||||
import { EDITORIAL_WORKFLOW, status } from 'Constants/publishModes';
|
||||
import { EDITORIAL_WORKFLOW_ERROR } from 'netlify-cms-lib-util';
|
||||
import {
|
||||
loadEntry,
|
||||
entryDeleted,
|
||||
getMediaAssets,
|
||||
setDraftEntryMediaFiles,
|
||||
clearDraftEntryMediaFiles,
|
||||
@ -525,3 +527,41 @@ export function publishUnpublishedEntry(collection, slug) {
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function unpublishPublishedEntry(collection, slug) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
const transactionID = uuid();
|
||||
const entry = selectEntry(state, collection.get('name'), slug);
|
||||
const entryDraft = Map().set('entry', entry);
|
||||
dispatch(unpublishedEntryPersisting(collection, entry, transactionID));
|
||||
return backend
|
||||
.persistEntry(state.config, collection, entryDraft, [], state.integrations, [], {
|
||||
status: status.get('PENDING_PUBLISH'),
|
||||
})
|
||||
.then(() => backend.deleteEntry(state.config, collection, slug))
|
||||
.then(() => {
|
||||
dispatch(unpublishedEntryPersisted(collection, entryDraft, transactionID, slug));
|
||||
dispatch(entryDeleted(collection, slug));
|
||||
dispatch(loadUnpublishedEntry(collection, slug));
|
||||
dispatch(
|
||||
notifSend({
|
||||
message: { key: 'ui.toast.entryUnpublished' },
|
||||
kind: 'success',
|
||||
dismissAfter: 4000,
|
||||
}),
|
||||
);
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(
|
||||
notifSend({
|
||||
message: { key: 'ui.toast.onFailToUnpublishEntry', details: error },
|
||||
kind: 'danger',
|
||||
dismissAfter: 8000,
|
||||
}),
|
||||
);
|
||||
dispatch(unpublishedEntryPersistedFail(error, transactionID));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import {
|
||||
import {
|
||||
updateUnpublishedEntryStatus,
|
||||
publishUnpublishedEntry,
|
||||
unpublishPublishedEntry,
|
||||
deleteUnpublishedEntry,
|
||||
} from 'Actions/editorialWorkflow';
|
||||
import { loadDeployPreview } from 'Actions/deploys';
|
||||
@ -299,6 +300,15 @@ export class Editor extends React.Component {
|
||||
}
|
||||
};
|
||||
|
||||
handleUnpublishEntry = async () => {
|
||||
const { unpublishPublishedEntry, collection, slug, t } = this.props;
|
||||
if (!window.confirm(t('editor.editor.onUnpublishing'))) return;
|
||||
|
||||
await unpublishPublishedEntry(collection, slug);
|
||||
|
||||
return navigateToCollection(collection.get('name'));
|
||||
};
|
||||
|
||||
handleDeleteEntry = () => {
|
||||
const { entryDraft, newEntry, collection, deleteEntry, slug, t } = this.props;
|
||||
if (entryDraft.get('hasChanged')) {
|
||||
@ -404,6 +414,7 @@ export class Editor extends React.Component {
|
||||
onDeleteUnpublishedChanges={this.handleDeleteUnpublishedChanges}
|
||||
onChangeStatus={this.handleChangeStatus}
|
||||
onPublish={this.handlePublishEntry}
|
||||
unPublish={this.handleUnpublishEntry}
|
||||
showDelete={this.props.showDelete}
|
||||
user={user}
|
||||
hasChanged={hasChanged}
|
||||
@ -481,6 +492,7 @@ export default connect(mapStateToProps, {
|
||||
deleteEntry,
|
||||
updateUnpublishedEntryStatus,
|
||||
publishUnpublishedEntry,
|
||||
unpublishPublishedEntry,
|
||||
deleteUnpublishedEntry,
|
||||
logoutUser,
|
||||
})(withWorkflow(translate()(Editor)));
|
||||
|
@ -160,6 +160,7 @@ class EditorInterface extends Component {
|
||||
onDeleteUnpublishedChanges,
|
||||
onChangeStatus,
|
||||
onPublish,
|
||||
unPublish,
|
||||
onValidate,
|
||||
user,
|
||||
hasChanged,
|
||||
@ -234,6 +235,7 @@ class EditorInterface extends Component {
|
||||
onChangeStatus={onChangeStatus}
|
||||
showDelete={showDelete}
|
||||
onPublish={onPublish}
|
||||
unPublish={unPublish}
|
||||
onPublishAndNew={() => this.handleOnPublish({ createNew: true })}
|
||||
user={user}
|
||||
hasChanged={hasChanged}
|
||||
@ -291,6 +293,7 @@ EditorInterface.propTypes = {
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onDeleteUnpublishedChanges: PropTypes.func.isRequired,
|
||||
onPublish: PropTypes.func.isRequired,
|
||||
unPublish: PropTypes.func.isRequired,
|
||||
onChangeStatus: PropTypes.func.isRequired,
|
||||
user: ImmutablePropTypes.map.isRequired,
|
||||
hasChanged: PropTypes.bool,
|
||||
|
@ -141,6 +141,11 @@ const SaveButton = styled(ToolbarButton)`
|
||||
${buttons.lightBlue};
|
||||
`;
|
||||
|
||||
const UnpublishButton = styled(StyledDropdownButton)`
|
||||
background-color: ${colorsRaw.tealLight};
|
||||
color: ${colorsRaw.teal};
|
||||
`;
|
||||
|
||||
const StatusPublished = styled.div`
|
||||
${styles.buttonMargin};
|
||||
border: 1px solid ${colors.textFieldBorder};
|
||||
@ -212,6 +217,7 @@ class EditorToolbar extends React.Component {
|
||||
onDeleteUnpublishedChanges: PropTypes.func.isRequired,
|
||||
onChangeStatus: PropTypes.func.isRequired,
|
||||
onPublish: PropTypes.func.isRequired,
|
||||
unPublish: PropTypes.func.isRequired,
|
||||
onPublishAndNew: PropTypes.func.isRequired,
|
||||
user: ImmutablePropTypes.map.isRequired,
|
||||
hasChanged: PropTypes.bool,
|
||||
@ -377,10 +383,12 @@ class EditorToolbar extends React.Component {
|
||||
isPublishing,
|
||||
onChangeStatus,
|
||||
onPublish,
|
||||
unPublish,
|
||||
onPublishAndNew,
|
||||
currentStatus,
|
||||
isNewEntry,
|
||||
useOpenAuthoring,
|
||||
isPersisting,
|
||||
t,
|
||||
} = this.props;
|
||||
if (currentStatus) {
|
||||
@ -458,7 +466,24 @@ class EditorToolbar extends React.Component {
|
||||
return (
|
||||
<>
|
||||
{this.renderDeployPreviewControls(t('editor.editorToolbar.deployButtonLabel'))}
|
||||
<StatusPublished>{t('editor.editorToolbar.published')}</StatusPublished>
|
||||
<ToolbarDropdown
|
||||
dropdownTopOverlap="40px"
|
||||
dropdownWidth="150px"
|
||||
renderButton={() => (
|
||||
<UnpublishButton>
|
||||
{isPersisting
|
||||
? t('editor.editorToolbar.unpublishing')
|
||||
: t('editor.editorToolbar.published')}
|
||||
</UnpublishButton>
|
||||
)}
|
||||
>
|
||||
<DropdownItem
|
||||
label={t('editor.editorToolbar.unpublish')}
|
||||
icon="arrow"
|
||||
iconDirection="right"
|
||||
onClick={unPublish}
|
||||
/>
|
||||
</ToolbarDropdown>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ const en = {
|
||||
onPublishingNotReady: 'Please update status to "Ready" before publishing.',
|
||||
onPublishingWithUnsavedChanges: 'You have unsaved changes, please save before publishing.',
|
||||
onPublishing: 'Are you sure you want to publish this entry?',
|
||||
onUnpublishing: 'Are you sure you want to unpublish this entry?',
|
||||
onDeleteWithUnsavedChanges:
|
||||
'Are you sure you want to delete this published entry, as well as your unsaved changes from the current session?',
|
||||
onDeletePublishedEntry: 'Are you sure you want to delete this published entry?',
|
||||
@ -63,6 +64,8 @@ const en = {
|
||||
publishing: 'Publishing...',
|
||||
publish: 'Publish',
|
||||
published: 'Published',
|
||||
unpublish: 'Unpublish',
|
||||
unpublishing: 'Unpublishing...',
|
||||
publishAndCreateNew: 'Publish and create new',
|
||||
deleteUnpublishedChanges: 'Delete unpublished changes',
|
||||
deleteUnpublishedEntry: 'Delete unpublished entry',
|
||||
@ -140,7 +143,9 @@ const en = {
|
||||
missingRequiredField: "Oops, you've missed a required field. Please complete before saving.",
|
||||
entrySaved: 'Entry saved',
|
||||
entryPublished: 'Entry published',
|
||||
entryUnpublished: 'Entry unpublished',
|
||||
onFailToPublishEntry: 'Failed to publish: %{details}',
|
||||
onFailToUnpublishEntry: 'Failed to unpublish entry: %{details}',
|
||||
entryUpdated: 'Entry status updated',
|
||||
onDeleteUnpublishedChanges: 'Unpublished changes deleted',
|
||||
onFailToAuth: '%{details}',
|
||||
|
Loading…
x
Reference in New Issue
Block a user