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,
|
validateObjectFieldsAndExit,
|
||||||
validateNestedObjectFieldsAndExit,
|
validateNestedObjectFieldsAndExit,
|
||||||
validateListFieldsAndExit,
|
validateListFieldsAndExit,
|
||||||
|
unpublishEntry,
|
||||||
} from '../utils/steps';
|
} from '../utils/steps';
|
||||||
import { setting1, setting2, workflowStatus, editorStatus } from '../utils/constants';
|
import { setting1, setting2, workflowStatus, editorStatus } from '../utils/constants';
|
||||||
|
|
||||||
@ -123,4 +124,15 @@ describe('Test Backend Editorial Workflow', () => {
|
|||||||
goToWorkflow();
|
goToWorkflow();
|
||||||
assertWorkflowStatus(entry1, workflowStatus.ready);
|
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 = {
|
const notifications = {
|
||||||
saved: 'Entry saved',
|
saved: 'Entry saved',
|
||||||
published: 'Entry published',
|
published: 'Entry published',
|
||||||
|
unpublished: 'Entry unpublished',
|
||||||
updated: 'Entry status updated',
|
updated: 'Entry status updated',
|
||||||
deletedUnpublished: 'Unpublished changes deleted',
|
deletedUnpublished: 'Unpublished changes deleted',
|
||||||
error: {
|
error: {
|
||||||
|
@ -212,6 +212,23 @@ function updateExistingPostAndExit(fromEntry, toEntry) {
|
|||||||
cy.contains('h2', toEntry.title);
|
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 }) {
|
function validateObjectFields({ limit, author }) {
|
||||||
cy.get('a[href^="#/collections/settings"]').click();
|
cy.get('a[href^="#/collections/settings"]').click();
|
||||||
cy.get('a[href^="#/collections/settings/entries/general"]').click();
|
cy.get('a[href^="#/collections/settings/entries/general"]').click();
|
||||||
@ -303,4 +320,5 @@ module.exports = {
|
|||||||
validateObjectFieldsAndExit,
|
validateObjectFieldsAndExit,
|
||||||
validateNestedObjectFieldsAndExit,
|
validateNestedObjectFieldsAndExit,
|
||||||
validateListFieldsAndExit,
|
validateListFieldsAndExit,
|
||||||
|
unpublishEntry,
|
||||||
};
|
};
|
||||||
|
@ -667,7 +667,7 @@ export default class API {
|
|||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
user: user.name || user.login,
|
user: user.name || user.login,
|
||||||
status: this.initialWorkflowStatus,
|
status: options.status || this.initialWorkflowStatus,
|
||||||
branch: branchName,
|
branch: branchName,
|
||||||
collection: options.collectionName,
|
collection: options.collectionName,
|
||||||
commitMessage: options.commitMessage,
|
commitMessage: options.commitMessage,
|
||||||
|
@ -175,7 +175,7 @@ export default class TestBackend {
|
|||||||
},
|
},
|
||||||
metaData: {
|
metaData: {
|
||||||
collection: options.collectionName,
|
collection: options.collectionName,
|
||||||
status: this.options.initialWorkflowStatus,
|
status: options.status || this.options.initialWorkflowStatus,
|
||||||
title: options.parsedData && options.parsedData.title,
|
title: options.parsedData && options.parsedData.title,
|
||||||
description: options.parsedData && options.parsedData.description,
|
description: options.parsedData && options.parsedData.description,
|
||||||
},
|
},
|
||||||
|
@ -2,14 +2,16 @@ import uuid from 'uuid/v4';
|
|||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import { actions as notifActions } from 'redux-notifications';
|
import { actions as notifActions } from 'redux-notifications';
|
||||||
import { BEGIN, COMMIT, REVERT } from 'redux-optimist';
|
import { BEGIN, COMMIT, REVERT } from 'redux-optimist';
|
||||||
|
import { Map } from 'immutable';
|
||||||
import { serializeValues } from 'Lib/serializeEntryValues';
|
import { serializeValues } from 'Lib/serializeEntryValues';
|
||||||
import { currentBackend } from 'coreSrc/backend';
|
import { currentBackend } from 'coreSrc/backend';
|
||||||
import { selectPublishedSlugs, selectUnpublishedSlugs } from 'Reducers';
|
import { selectPublishedSlugs, selectUnpublishedSlugs, selectEntry } from 'Reducers';
|
||||||
import { selectFields } from 'Reducers/collections';
|
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 { EDITORIAL_WORKFLOW_ERROR } from 'netlify-cms-lib-util';
|
||||||
import {
|
import {
|
||||||
loadEntry,
|
loadEntry,
|
||||||
|
entryDeleted,
|
||||||
getMediaAssets,
|
getMediaAssets,
|
||||||
setDraftEntryMediaFiles,
|
setDraftEntryMediaFiles,
|
||||||
clearDraftEntryMediaFiles,
|
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 {
|
import {
|
||||||
updateUnpublishedEntryStatus,
|
updateUnpublishedEntryStatus,
|
||||||
publishUnpublishedEntry,
|
publishUnpublishedEntry,
|
||||||
|
unpublishPublishedEntry,
|
||||||
deleteUnpublishedEntry,
|
deleteUnpublishedEntry,
|
||||||
} from 'Actions/editorialWorkflow';
|
} from 'Actions/editorialWorkflow';
|
||||||
import { loadDeployPreview } from 'Actions/deploys';
|
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 = () => {
|
handleDeleteEntry = () => {
|
||||||
const { entryDraft, newEntry, collection, deleteEntry, slug, t } = this.props;
|
const { entryDraft, newEntry, collection, deleteEntry, slug, t } = this.props;
|
||||||
if (entryDraft.get('hasChanged')) {
|
if (entryDraft.get('hasChanged')) {
|
||||||
@ -404,6 +414,7 @@ export class Editor extends React.Component {
|
|||||||
onDeleteUnpublishedChanges={this.handleDeleteUnpublishedChanges}
|
onDeleteUnpublishedChanges={this.handleDeleteUnpublishedChanges}
|
||||||
onChangeStatus={this.handleChangeStatus}
|
onChangeStatus={this.handleChangeStatus}
|
||||||
onPublish={this.handlePublishEntry}
|
onPublish={this.handlePublishEntry}
|
||||||
|
unPublish={this.handleUnpublishEntry}
|
||||||
showDelete={this.props.showDelete}
|
showDelete={this.props.showDelete}
|
||||||
user={user}
|
user={user}
|
||||||
hasChanged={hasChanged}
|
hasChanged={hasChanged}
|
||||||
@ -481,6 +492,7 @@ export default connect(mapStateToProps, {
|
|||||||
deleteEntry,
|
deleteEntry,
|
||||||
updateUnpublishedEntryStatus,
|
updateUnpublishedEntryStatus,
|
||||||
publishUnpublishedEntry,
|
publishUnpublishedEntry,
|
||||||
|
unpublishPublishedEntry,
|
||||||
deleteUnpublishedEntry,
|
deleteUnpublishedEntry,
|
||||||
logoutUser,
|
logoutUser,
|
||||||
})(withWorkflow(translate()(Editor)));
|
})(withWorkflow(translate()(Editor)));
|
||||||
|
@ -160,6 +160,7 @@ class EditorInterface extends Component {
|
|||||||
onDeleteUnpublishedChanges,
|
onDeleteUnpublishedChanges,
|
||||||
onChangeStatus,
|
onChangeStatus,
|
||||||
onPublish,
|
onPublish,
|
||||||
|
unPublish,
|
||||||
onValidate,
|
onValidate,
|
||||||
user,
|
user,
|
||||||
hasChanged,
|
hasChanged,
|
||||||
@ -234,6 +235,7 @@ class EditorInterface extends Component {
|
|||||||
onChangeStatus={onChangeStatus}
|
onChangeStatus={onChangeStatus}
|
||||||
showDelete={showDelete}
|
showDelete={showDelete}
|
||||||
onPublish={onPublish}
|
onPublish={onPublish}
|
||||||
|
unPublish={unPublish}
|
||||||
onPublishAndNew={() => this.handleOnPublish({ createNew: true })}
|
onPublishAndNew={() => this.handleOnPublish({ createNew: true })}
|
||||||
user={user}
|
user={user}
|
||||||
hasChanged={hasChanged}
|
hasChanged={hasChanged}
|
||||||
@ -291,6 +293,7 @@ EditorInterface.propTypes = {
|
|||||||
onDelete: PropTypes.func.isRequired,
|
onDelete: PropTypes.func.isRequired,
|
||||||
onDeleteUnpublishedChanges: PropTypes.func.isRequired,
|
onDeleteUnpublishedChanges: PropTypes.func.isRequired,
|
||||||
onPublish: PropTypes.func.isRequired,
|
onPublish: PropTypes.func.isRequired,
|
||||||
|
unPublish: PropTypes.func.isRequired,
|
||||||
onChangeStatus: PropTypes.func.isRequired,
|
onChangeStatus: PropTypes.func.isRequired,
|
||||||
user: ImmutablePropTypes.map.isRequired,
|
user: ImmutablePropTypes.map.isRequired,
|
||||||
hasChanged: PropTypes.bool,
|
hasChanged: PropTypes.bool,
|
||||||
|
@ -141,6 +141,11 @@ const SaveButton = styled(ToolbarButton)`
|
|||||||
${buttons.lightBlue};
|
${buttons.lightBlue};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const UnpublishButton = styled(StyledDropdownButton)`
|
||||||
|
background-color: ${colorsRaw.tealLight};
|
||||||
|
color: ${colorsRaw.teal};
|
||||||
|
`;
|
||||||
|
|
||||||
const StatusPublished = styled.div`
|
const StatusPublished = styled.div`
|
||||||
${styles.buttonMargin};
|
${styles.buttonMargin};
|
||||||
border: 1px solid ${colors.textFieldBorder};
|
border: 1px solid ${colors.textFieldBorder};
|
||||||
@ -212,6 +217,7 @@ class EditorToolbar extends React.Component {
|
|||||||
onDeleteUnpublishedChanges: PropTypes.func.isRequired,
|
onDeleteUnpublishedChanges: PropTypes.func.isRequired,
|
||||||
onChangeStatus: PropTypes.func.isRequired,
|
onChangeStatus: PropTypes.func.isRequired,
|
||||||
onPublish: PropTypes.func.isRequired,
|
onPublish: PropTypes.func.isRequired,
|
||||||
|
unPublish: PropTypes.func.isRequired,
|
||||||
onPublishAndNew: PropTypes.func.isRequired,
|
onPublishAndNew: PropTypes.func.isRequired,
|
||||||
user: ImmutablePropTypes.map.isRequired,
|
user: ImmutablePropTypes.map.isRequired,
|
||||||
hasChanged: PropTypes.bool,
|
hasChanged: PropTypes.bool,
|
||||||
@ -377,10 +383,12 @@ class EditorToolbar extends React.Component {
|
|||||||
isPublishing,
|
isPublishing,
|
||||||
onChangeStatus,
|
onChangeStatus,
|
||||||
onPublish,
|
onPublish,
|
||||||
|
unPublish,
|
||||||
onPublishAndNew,
|
onPublishAndNew,
|
||||||
currentStatus,
|
currentStatus,
|
||||||
isNewEntry,
|
isNewEntry,
|
||||||
useOpenAuthoring,
|
useOpenAuthoring,
|
||||||
|
isPersisting,
|
||||||
t,
|
t,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
if (currentStatus) {
|
if (currentStatus) {
|
||||||
@ -458,7 +466,24 @@ class EditorToolbar extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{this.renderDeployPreviewControls(t('editor.editorToolbar.deployButtonLabel'))}
|
{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.',
|
onPublishingNotReady: 'Please update status to "Ready" before publishing.',
|
||||||
onPublishingWithUnsavedChanges: 'You have unsaved changes, please save before publishing.',
|
onPublishingWithUnsavedChanges: 'You have unsaved changes, please save before publishing.',
|
||||||
onPublishing: 'Are you sure you want to publish this entry?',
|
onPublishing: 'Are you sure you want to publish this entry?',
|
||||||
|
onUnpublishing: 'Are you sure you want to unpublish this entry?',
|
||||||
onDeleteWithUnsavedChanges:
|
onDeleteWithUnsavedChanges:
|
||||||
'Are you sure you want to delete this published entry, as well as your unsaved changes from the current session?',
|
'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?',
|
onDeletePublishedEntry: 'Are you sure you want to delete this published entry?',
|
||||||
@ -63,6 +64,8 @@ const en = {
|
|||||||
publishing: 'Publishing...',
|
publishing: 'Publishing...',
|
||||||
publish: 'Publish',
|
publish: 'Publish',
|
||||||
published: 'Published',
|
published: 'Published',
|
||||||
|
unpublish: 'Unpublish',
|
||||||
|
unpublishing: 'Unpublishing...',
|
||||||
publishAndCreateNew: 'Publish and create new',
|
publishAndCreateNew: 'Publish and create new',
|
||||||
deleteUnpublishedChanges: 'Delete unpublished changes',
|
deleteUnpublishedChanges: 'Delete unpublished changes',
|
||||||
deleteUnpublishedEntry: 'Delete unpublished entry',
|
deleteUnpublishedEntry: 'Delete unpublished entry',
|
||||||
@ -140,7 +143,9 @@ const en = {
|
|||||||
missingRequiredField: "Oops, you've missed a required field. Please complete before saving.",
|
missingRequiredField: "Oops, you've missed a required field. Please complete before saving.",
|
||||||
entrySaved: 'Entry saved',
|
entrySaved: 'Entry saved',
|
||||||
entryPublished: 'Entry published',
|
entryPublished: 'Entry published',
|
||||||
|
entryUnpublished: 'Entry unpublished',
|
||||||
onFailToPublishEntry: 'Failed to publish: %{details}',
|
onFailToPublishEntry: 'Failed to publish: %{details}',
|
||||||
|
onFailToUnpublishEntry: 'Failed to unpublish entry: %{details}',
|
||||||
entryUpdated: 'Entry status updated',
|
entryUpdated: 'Entry status updated',
|
||||||
onDeleteUnpublishedChanges: 'Unpublished changes deleted',
|
onDeleteUnpublishedChanges: 'Unpublished changes deleted',
|
||||||
onFailToAuth: '%{details}',
|
onFailToAuth: '%{details}',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user