Skip to content
This repository was archived by the owner on Aug 29, 2021. It is now read-only.

Commit 9e858af

Browse files
barthcNathan Kitchen
authored and
Nathan Kitchen
committed
feat: workflow unpublished entry (decaporg#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
1 parent 964534b commit 9e858af

File tree

10 files changed

+121
-5
lines changed

10 files changed

+121
-5
lines changed

cypress/integration/editorial_workflow_spec_test_backend.js

+12
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
validateObjectFieldsAndExit,
1919
validateNestedObjectFieldsAndExit,
2020
validateListFieldsAndExit,
21+
unpublishEntry,
2122
} from '../utils/steps';
2223
import { setting1, setting2, workflowStatus, editorStatus } from '../utils/constants';
2324

@@ -123,4 +124,15 @@ describe('Test Backend Editorial Workflow', () => {
123124
goToWorkflow();
124125
assertWorkflowStatus(entry1, workflowStatus.ready);
125126
});
127+
128+
it('can unpublish an existing entry', () => {
129+
// first publish an entry
130+
login();
131+
createPostAndExit(entry1);
132+
goToWorkflow();
133+
updateWorkflowStatus(entry1, workflowStatus.draft, workflowStatus.ready);
134+
publishWorkflowEntry(entry1);
135+
// then unpublish it
136+
unpublishEntry(entry1);
137+
});
126138
});

cypress/utils/constants.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const setting2 = { name: 'Andrew Wommack', description: 'A Gospel Teacher' };
55
const notifications = {
66
saved: 'Entry saved',
77
published: 'Entry published',
8+
unpublished: 'Entry unpublished',
89
updated: 'Entry status updated',
910
deletedUnpublished: 'Unpublished changes deleted',
1011
error: {

cypress/utils/steps.js

+18
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,23 @@ function updateExistingPostAndExit(fromEntry, toEntry) {
212212
cy.contains('h2', toEntry.title);
213213
}
214214

215+
function unpublishEntry(entry) {
216+
goToCollections();
217+
cy.contains('h2', entry.title)
218+
.parent()
219+
.click({ force: true });
220+
cy.contains('[role="button"]', 'Published').as('publishedButton');
221+
cy.get('@publishedButton')
222+
.parent()
223+
.within(() => {
224+
cy.get('@publishedButton').click();
225+
cy.contains('[role="menuitem"] span', 'Unpublish').click();
226+
});
227+
assertNotification(notifications.unpublished);
228+
goToWorkflow();
229+
assertWorkflowStatus(entry, workflowStatus.ready);
230+
}
231+
215232
function validateObjectFields({ limit, author }) {
216233
cy.get('a[href^="#/collections/settings"]').click();
217234
cy.get('a[href^="#/collections/settings/entries/general"]').click();
@@ -303,4 +320,5 @@ module.exports = {
303320
validateObjectFieldsAndExit,
304321
validateNestedObjectFieldsAndExit,
305322
validateListFieldsAndExit,
323+
unpublishEntry,
306324
};

packages/netlify-cms-backend-github/src/API.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,7 @@ export default class API {
667667
}
668668
: undefined,
669669
user: user.name || user.login,
670-
status: this.initialWorkflowStatus,
670+
status: options.status || this.initialWorkflowStatus,
671671
branch: branchName,
672672
collection: options.collectionName,
673673
commitMessage: options.commitMessage,

packages/netlify-cms-backend-test/src/implementation.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ export default class TestBackend {
175175
},
176176
metaData: {
177177
collection: options.collectionName,
178-
status: this.options.initialWorkflowStatus,
178+
status: options.status || this.options.initialWorkflowStatus,
179179
title: options.parsedData && options.parsedData.title,
180180
description: options.parsedData && options.parsedData.description,
181181
},

packages/netlify-cms-core/src/actions/editorialWorkflow.js

+42-2
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ import uuid from 'uuid/v4';
22
import { get } from 'lodash';
33
import { actions as notifActions } from 'redux-notifications';
44
import { BEGIN, COMMIT, REVERT } from 'redux-optimist';
5+
import { Map } from 'immutable';
56
import { serializeValues } from 'Lib/serializeEntryValues';
67
import { currentBackend } from 'coreSrc/backend';
7-
import { selectPublishedSlugs, selectUnpublishedSlugs } from 'Reducers';
8+
import { selectPublishedSlugs, selectUnpublishedSlugs, selectEntry } from 'Reducers';
89
import { selectFields } from 'Reducers/collections';
9-
import { EDITORIAL_WORKFLOW } from 'Constants/publishModes';
10+
import { EDITORIAL_WORKFLOW, status } from 'Constants/publishModes';
1011
import { EDITORIAL_WORKFLOW_ERROR } from 'netlify-cms-lib-util';
1112
import {
1213
loadEntry,
14+
entryDeleted,
1315
getMediaAssets,
1416
setDraftEntryMediaFiles,
1517
clearDraftEntryMediaFiles,
@@ -525,3 +527,41 @@ export function publishUnpublishedEntry(collection, slug) {
525527
});
526528
};
527529
}
530+
531+
export function unpublishPublishedEntry(collection, slug) {
532+
return (dispatch, getState) => {
533+
const state = getState();
534+
const backend = currentBackend(state.config);
535+
const transactionID = uuid();
536+
const entry = selectEntry(state, collection.get('name'), slug);
537+
const entryDraft = Map().set('entry', entry);
538+
dispatch(unpublishedEntryPersisting(collection, entry, transactionID));
539+
return backend
540+
.persistEntry(state.config, collection, entryDraft, [], state.integrations, [], {
541+
status: status.get('PENDING_PUBLISH'),
542+
})
543+
.then(() => backend.deleteEntry(state.config, collection, slug))
544+
.then(() => {
545+
dispatch(unpublishedEntryPersisted(collection, entryDraft, transactionID, slug));
546+
dispatch(entryDeleted(collection, slug));
547+
dispatch(loadUnpublishedEntry(collection, slug));
548+
dispatch(
549+
notifSend({
550+
message: { key: 'ui.toast.entryUnpublished' },
551+
kind: 'success',
552+
dismissAfter: 4000,
553+
}),
554+
);
555+
})
556+
.catch(error => {
557+
dispatch(
558+
notifSend({
559+
message: { key: 'ui.toast.onFailToUnpublishEntry', details: error },
560+
kind: 'danger',
561+
dismissAfter: 8000,
562+
}),
563+
);
564+
dispatch(unpublishedEntryPersistedFail(error, transactionID));
565+
});
566+
};
567+
}

packages/netlify-cms-core/src/components/Editor/Editor.js

+12
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import {
2626
updateUnpublishedEntryStatus,
2727
publishUnpublishedEntry,
28+
unpublishPublishedEntry,
2829
deleteUnpublishedEntry,
2930
} from 'Actions/editorialWorkflow';
3031
import { loadDeployPreview } from 'Actions/deploys';
@@ -299,6 +300,15 @@ export class Editor extends React.Component {
299300
}
300301
};
301302

303+
handleUnpublishEntry = async () => {
304+
const { unpublishPublishedEntry, collection, slug, t } = this.props;
305+
if (!window.confirm(t('editor.editor.onUnpublishing'))) return;
306+
307+
await unpublishPublishedEntry(collection, slug);
308+
309+
return navigateToCollection(collection.get('name'));
310+
};
311+
302312
handleDeleteEntry = () => {
303313
const { entryDraft, newEntry, collection, deleteEntry, slug, t } = this.props;
304314
if (entryDraft.get('hasChanged')) {
@@ -404,6 +414,7 @@ export class Editor extends React.Component {
404414
onDeleteUnpublishedChanges={this.handleDeleteUnpublishedChanges}
405415
onChangeStatus={this.handleChangeStatus}
406416
onPublish={this.handlePublishEntry}
417+
unPublish={this.handleUnpublishEntry}
407418
showDelete={this.props.showDelete}
408419
user={user}
409420
hasChanged={hasChanged}
@@ -481,6 +492,7 @@ export default connect(mapStateToProps, {
481492
deleteEntry,
482493
updateUnpublishedEntryStatus,
483494
publishUnpublishedEntry,
495+
unpublishPublishedEntry,
484496
deleteUnpublishedEntry,
485497
logoutUser,
486498
})(withWorkflow(translate()(Editor)));

packages/netlify-cms-core/src/components/Editor/EditorInterface.js

+3
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ class EditorInterface extends Component {
160160
onDeleteUnpublishedChanges,
161161
onChangeStatus,
162162
onPublish,
163+
unPublish,
163164
onValidate,
164165
user,
165166
hasChanged,
@@ -234,6 +235,7 @@ class EditorInterface extends Component {
234235
onChangeStatus={onChangeStatus}
235236
showDelete={showDelete}
236237
onPublish={onPublish}
238+
unPublish={unPublish}
237239
onPublishAndNew={() => this.handleOnPublish({ createNew: true })}
238240
user={user}
239241
hasChanged={hasChanged}
@@ -291,6 +293,7 @@ EditorInterface.propTypes = {
291293
onDelete: PropTypes.func.isRequired,
292294
onDeleteUnpublishedChanges: PropTypes.func.isRequired,
293295
onPublish: PropTypes.func.isRequired,
296+
unPublish: PropTypes.func.isRequired,
294297
onChangeStatus: PropTypes.func.isRequired,
295298
user: ImmutablePropTypes.map.isRequired,
296299
hasChanged: PropTypes.bool,

packages/netlify-cms-core/src/components/Editor/EditorToolbar.js

+26-1
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ const SaveButton = styled(ToolbarButton)`
141141
${buttons.lightBlue};
142142
`;
143143

144+
const UnpublishButton = styled(StyledDropdownButton)`
145+
background-color: ${colorsRaw.tealLight};
146+
color: ${colorsRaw.teal};
147+
`;
148+
144149
const StatusPublished = styled.div`
145150
${styles.buttonMargin};
146151
border: 1px solid ${colors.textFieldBorder};
@@ -212,6 +217,7 @@ class EditorToolbar extends React.Component {
212217
onDeleteUnpublishedChanges: PropTypes.func.isRequired,
213218
onChangeStatus: PropTypes.func.isRequired,
214219
onPublish: PropTypes.func.isRequired,
220+
unPublish: PropTypes.func.isRequired,
215221
onPublishAndNew: PropTypes.func.isRequired,
216222
user: ImmutablePropTypes.map.isRequired,
217223
hasChanged: PropTypes.bool,
@@ -377,10 +383,12 @@ class EditorToolbar extends React.Component {
377383
isPublishing,
378384
onChangeStatus,
379385
onPublish,
386+
unPublish,
380387
onPublishAndNew,
381388
currentStatus,
382389
isNewEntry,
383390
useOpenAuthoring,
391+
isPersisting,
384392
t,
385393
} = this.props;
386394
if (currentStatus) {
@@ -458,7 +466,24 @@ class EditorToolbar extends React.Component {
458466
return (
459467
<>
460468
{this.renderDeployPreviewControls(t('editor.editorToolbar.deployButtonLabel'))}
461-
<StatusPublished>{t('editor.editorToolbar.published')}</StatusPublished>
469+
<ToolbarDropdown
470+
dropdownTopOverlap="40px"
471+
dropdownWidth="150px"
472+
renderButton={() => (
473+
<UnpublishButton>
474+
{isPersisting
475+
? t('editor.editorToolbar.unpublishing')
476+
: t('editor.editorToolbar.published')}
477+
</UnpublishButton>
478+
)}
479+
>
480+
<DropdownItem
481+
label={t('editor.editorToolbar.unpublish')}
482+
icon="arrow"
483+
iconDirection="right"
484+
onClick={unPublish}
485+
/>
486+
</ToolbarDropdown>
462487
</>
463488
);
464489
}

packages/netlify-cms-locales/src/en/index.js

+5
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const en = {
4949
onPublishingNotReady: 'Please update status to "Ready" before publishing.',
5050
onPublishingWithUnsavedChanges: 'You have unsaved changes, please save before publishing.',
5151
onPublishing: 'Are you sure you want to publish this entry?',
52+
onUnpublishing: 'Are you sure you want to unpublish this entry?',
5253
onDeleteWithUnsavedChanges:
5354
'Are you sure you want to delete this published entry, as well as your unsaved changes from the current session?',
5455
onDeletePublishedEntry: 'Are you sure you want to delete this published entry?',
@@ -63,6 +64,8 @@ const en = {
6364
publishing: 'Publishing...',
6465
publish: 'Publish',
6566
published: 'Published',
67+
unpublish: 'Unpublish',
68+
unpublishing: 'Unpublishing...',
6669
publishAndCreateNew: 'Publish and create new',
6770
deleteUnpublishedChanges: 'Delete unpublished changes',
6871
deleteUnpublishedEntry: 'Delete unpublished entry',
@@ -140,7 +143,9 @@ const en = {
140143
missingRequiredField: "Oops, you've missed a required field. Please complete before saving.",
141144
entrySaved: 'Entry saved',
142145
entryPublished: 'Entry published',
146+
entryUnpublished: 'Entry unpublished',
143147
onFailToPublishEntry: 'Failed to publish: %{details}',
148+
onFailToUnpublishEntry: 'Failed to unpublish entry: %{details}',
144149
entryUpdated: 'Entry status updated',
145150
onDeleteUnpublishedChanges: 'Unpublished changes deleted',
146151
onFailToAuth: '%{details}',

0 commit comments

Comments
 (0)