diff --git a/.babelrc b/.babelrc index 990f92a8..9f49dc7a 100644 --- a/.babelrc +++ b/.babelrc @@ -1,4 +1,6 @@ { "presets": ["es2015", "stage-1", "react"], - "plugins": ["lodash"] + "plugins": ["lodash", ["babel-plugin-transform-builtin-extend", { + globals: ["Error"] + }]] } diff --git a/package.json b/package.json index 73dd450c..51846947 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "dependencies": { "@kadira/storybook": "^1.36.0", "autoprefixer": "^6.3.3", + "babel-plugin-transform-builtin-extend": "^1.1.0", "dateformat": "^1.0.12", "deep-equal": "^1.0.1", "fuzzy": "^0.1.1", diff --git a/src/actions/editor.js b/src/actions/editor.js index 37fb0f90..c7a98c5f 100644 --- a/src/actions/editor.js +++ b/src/actions/editor.js @@ -1,6 +1,7 @@ import history from '../routing/history'; export const SWITCH_VISUAL_MODE = 'SWITCH_VISUAL_MODE'; +export const CLOSED_ENTRY = 'CLOSED_ENTRY'; export function switchVisualMode(useVisualMode) { return { @@ -9,8 +10,13 @@ export function switchVisualMode(useVisualMode) { }; } -export function cancelEdit() { - return () => { - history.goBack(); +export function closeEntry(collection) { + return (dispatch) => { + if (collection && collection.get('name', false)) { + history.push(`collections/${ collection.get('name') }`); + } else { + history.goBack(); + } + dispatch({ type: CLOSED_ENTRY }); }; } diff --git a/src/actions/editorialWorkflow.js b/src/actions/editorialWorkflow.js index c8cd575d..dd4b88b5 100644 --- a/src/actions/editorialWorkflow.js +++ b/src/actions/editorialWorkflow.js @@ -1,9 +1,12 @@ import uuid from 'uuid'; import { actions as notifActions } from 'redux-notifications'; +import { closeEntry } from './editor'; import { BEGIN, COMMIT, REVERT } from 'redux-optimist'; import { currentBackend } from '../backends/backend'; import { getAsset } from '../reducers'; +import { loadEntry } from './entries'; import { status, EDITORIAL_WORKFLOW } from '../constants/publishModes'; +import { EditorialWorkflowError } from "../valueObjects/errors"; const { notifSend } = notifActions; @@ -12,6 +15,7 @@ const { notifSend } = notifActions; */ export const UNPUBLISHED_ENTRY_REQUEST = 'UNPUBLISHED_ENTRY_REQUEST'; export const UNPUBLISHED_ENTRY_SUCCESS = 'UNPUBLISHED_ENTRY_SUCCESS'; +export const UNPUBLISHED_ENTRY_REDIRECT = 'UNPUBLISHED_ENTRY_REDIRECT'; export const UNPUBLISHED_ENTRIES_REQUEST = 'UNPUBLISHED_ENTRIES_REQUEST'; export const UNPUBLISHED_ENTRIES_SUCCESS = 'UNPUBLISHED_ENTRIES_SUCCESS'; @@ -33,17 +37,33 @@ export const UNPUBLISHED_ENTRY_PUBLISH_FAILURE = 'UNPUBLISHED_ENTRY_PUBLISH_FAIL * Simple Action Creators (Internal) */ -function unpublishedEntryLoading(status, slug) { +function unpublishedEntryLoading(collection, slug) { return { type: UNPUBLISHED_ENTRY_REQUEST, - payload: { status, slug }, + payload: { + collection: collection.get('name'), + slug, + }, }; } -function unpublishedEntryLoaded(status, entry) { +function unpublishedEntryLoaded(collection, entry) { return { type: UNPUBLISHED_ENTRY_SUCCESS, - payload: { status, entry }, + payload: { + collection: collection.get('name'), + entry, + }, + }; +} + +function unpublishedEntryRedirected(collection, slug) { + return { + type: UNPUBLISHED_ENTRY_REDIRECT, + payload: { + collection: collection.get('name'), + slug, + }, }; } @@ -75,7 +95,10 @@ function unpublishedEntriesFailed(error) { function unpublishedEntryPersisting(collection, entry, transactionID) { return { type: UNPUBLISHED_ENTRY_PERSIST_REQUEST, - payload: { collection, entry }, + payload: { + collection: collection.get('name'), + entry, + }, optimist: { type: BEGIN, id: transactionID }, }; } @@ -83,7 +106,10 @@ function unpublishedEntryPersisting(collection, entry, transactionID) { function unpublishedEntryPersisted(collection, entry, transactionID) { return { type: UNPUBLISHED_ENTRY_PERSIST_SUCCESS, - payload: { collection, entry }, + payload: { + collection: collection.get('name'), + entry, + }, optimist: { type: COMMIT, id: transactionID }, }; } @@ -99,7 +125,12 @@ function unpublishedEntryPersistedFail(error, transactionID) { function unpublishedEntryStatusChangeRequest(collection, slug, oldStatus, newStatus, transactionID) { return { type: UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST, - payload: { collection, slug, oldStatus, newStatus }, + payload: { + collection, + slug, + oldStatus, + newStatus, + }, optimist: { type: BEGIN, id: transactionID }, }; } @@ -107,7 +138,12 @@ function unpublishedEntryStatusChangeRequest(collection, slug, oldStatus, newSta function unpublishedEntryStatusChangePersisted(collection, slug, oldStatus, newStatus, transactionID) { return { type: UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS, - payload: { collection, slug, oldStatus, newStatus }, + payload: { + collection, + slug, + oldStatus, + newStatus, + }, optimist: { type: COMMIT, id: transactionID }, }; } @@ -120,18 +156,18 @@ function unpublishedEntryStatusChangeError(collection, slug, transactionID) { }; } -function unpublishedEntryPublishRequest(collection, slug, status, transactionID) { +function unpublishedEntryPublishRequest(collection, slug, transactionID) { return { type: UNPUBLISHED_ENTRY_PUBLISH_REQUEST, - payload: { collection, slug, status }, + payload: { collection, slug }, optimist: { type: BEGIN, id: transactionID }, }; } -function unpublishedEntryPublished(collection, slug, status, transactionID) { +function unpublishedEntryPublished(collection, slug, transactionID) { return { type: UNPUBLISHED_ENTRY_PUBLISH_SUCCESS, - payload: { collection, slug, status }, + payload: { collection, slug }, optimist: { type: COMMIT, id: transactionID }, }; } @@ -148,13 +184,25 @@ function unpublishedEntryPublishError(collection, slug, transactionID) { * Exported Thunk Action Creators */ -export function loadUnpublishedEntry(collection, status, slug) { +export function loadUnpublishedEntry(collection, slug) { return (dispatch, getState) => { const state = getState(); const backend = currentBackend(state.config); - dispatch(unpublishedEntryLoading(status, slug)); + dispatch(unpublishedEntryLoading(collection, slug)); backend.unpublishedEntry(collection, slug) - .then(entry => dispatch(unpublishedEntryLoaded(status, entry))); + .then(entry => dispatch(unpublishedEntryLoaded(collection, entry))) + .catch((error) => { + if (error instanceof EditorialWorkflowError && error.notUnderEditorialWorkflow) { + dispatch(unpublishedEntryRedirected(collection, slug)); + dispatch(loadEntry(collection, slug)); + } else { + dispatch(notifSend({ + message: `Error loading entry: ${ error }`, + kind: 'danger', + dismissAfter: 8000, + })); + } + }); }; } @@ -188,13 +236,14 @@ export function persistUnpublishedEntry(collection, entryDraft, existingUnpublis kind: 'success', dismissAfter: 4000, })); + dispatch(closeEntry()); dispatch(unpublishedEntryPersisted(collection, entry, transactionID)); }) .catch((error) => { dispatch(notifSend({ - message: 'Failed to persist entry', + message: `Failed to persist entry: ${ error }`, kind: 'danger', - dismissAfter: 4000, + dismissAfter: 8000, })); dispatch(unpublishedEntryPersistedFail(error, transactionID)); }); @@ -217,17 +266,22 @@ export function updateUnpublishedEntryStatus(collection, slug, oldStatus, newSta }; } -export function publishUnpublishedEntry(collection, slug, status) { +export function publishUnpublishedEntry(collection, slug) { return (dispatch, getState) => { const state = getState(); const backend = currentBackend(state.config); const transactionID = uuid.v4(); - dispatch(unpublishedEntryPublishRequest(collection, slug, status, transactionID)); - backend.publishUnpublishedEntry(collection, slug, status) + dispatch(unpublishedEntryPublishRequest(collection, slug, transactionID)); + backend.publishUnpublishedEntry(collection, slug) .then(() => { - dispatch(unpublishedEntryPublished(collection, slug, status, transactionID)); + dispatch(unpublishedEntryPublished(collection, slug, transactionID)); }) - .catch(() => { + .catch((error) => { + dispatch(notifSend({ + message: `Failed to merge: ${ error }`, + kind: 'danger', + dismissAfter: 8000, + })); dispatch(unpublishedEntryPublishError(collection, slug, transactionID)); }); }; diff --git a/src/actions/entries.js b/src/actions/entries.js index f995c319..7c530a4e 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -1,5 +1,6 @@ import { List } from 'immutable'; import { actions as notifActions } from 'redux-notifications'; +import { closeEntry } from './editor'; import { currentBackend } from '../backends/backend'; import { getIntegrationProvider } from '../integrations'; import { getAsset, selectIntegration } from '../reducers'; @@ -164,7 +165,7 @@ export function changeDraftField(field, value, metadata) { * Exported Thunk Action Creators */ -export function loadEntry(entry, collection, slug) { +export function loadEntry(collection, slug) { return (dispatch, getState) => { const state = getState(); const backend = currentBackend(state.config); @@ -177,7 +178,7 @@ export function loadEntry(entry, collection, slug) { dispatch(notifSend({ message: `Failed to load entry: ${ error.message }`, kind: 'danger', - dismissAfter: 4000, + dismissAfter: 8000, })); dispatch(entryLoadError(error, collection, slug)); }); @@ -227,13 +228,14 @@ export function persistEntry(collection, entryDraft) { kind: 'success', dismissAfter: 4000, })); + dispatch(closeEntry(collection)); dispatch(entryPersisted(collection, entry)); }) .catch((error) => { dispatch(notifSend({ - message: 'Failed to persist entry', + message: `Failed to persist entry: ${ error }`, kind: 'danger', - dismissAfter: 4000, + dismissAfter: 8000, })); dispatch(entryPersistFail(collection, entry, error)); }); diff --git a/src/backends/backend.js b/src/backends/backend.js index 07d480d2..f7ed1674 100644 --- a/src/backends/backend.js +++ b/src/backends/backend.js @@ -124,7 +124,7 @@ class Backend { .then(loadedEntries => loadedEntries.filter(entry => entry !== null)) .then(entries => ( entries.map((loadedEntry) => { - const entry = createEntry("draft", loadedEntry.slug, loadedEntry.file.path, { raw: loadedEntry.data }); + const entry = createEntry(loadedEntry.metaData.collection, loadedEntry.slug, loadedEntry.file.path, { raw: loadedEntry.data }); entry.metaData = loadedEntry.metaData; return entry; }) @@ -195,8 +195,8 @@ class Backend { return this.implementation.updateUnpublishedEntryStatus(collection, slug, newStatus); } - publishUnpublishedEntry(collection, slug, status) { - return this.implementation.publishUnpublishedEntry(collection, slug, status); + publishUnpublishedEntry(collection, slug) { + return this.implementation.publishUnpublishedEntry(collection, slug); } diff --git a/src/backends/github/API.js b/src/backends/github/API.js index 200e4875..d398a706 100644 --- a/src/backends/github/API.js +++ b/src/backends/github/API.js @@ -3,6 +3,7 @@ import { Base64 } from "js-base64"; import _ from "lodash"; import AssetProxy from "../../valueObjects/AssetProxy"; import { SIMPLE, EDITORIAL_WORKFLOW, status } from "../../constants/publishModes"; +import { APIError, EditorialWorkflowError } from "../../valueObjects/errors"; export default class API { constructor(config) { @@ -57,13 +58,17 @@ export default class API { request(path, options = {}) { const headers = this.requestHeaders(options.headers || {}); const url = this.urlFor(path, options); + let responseStatus; return fetch(url, { ...options, headers }).then((response) => { + responseStatus = response.status; const contentType = response.headers.get("Content-Type"); if (contentType && contentType.match(/json/)) { return this.parseJsonResponse(response); } - return response.text(); + }) + .catch((error) => { + throw new APIError(error.message, responseStatus, 'GitHub'); }); } @@ -90,7 +95,6 @@ export default class API { } storeMetadata(key, data) { - console.log('Trying to store Metadata'); return this.checkMetadataRef() .then((branchData) => { const fileTree = { @@ -158,10 +162,12 @@ export default class API { const unpublishedPromise = this.retrieveMetadata(contentKey) .then((data) => { metaData = data; - return this.readFile(data.objects.entry, null, data.branch); + return this.readFile(data.objects.entry.path, null, data.branch); }) .then(fileData => ({ metaData, fileData })) - .catch(error => null); + .catch(() => { + throw new EditorialWorkflowError('content is not under editorial workflow', true); + }); return unpublishedPromise; } @@ -174,19 +180,15 @@ export default class API { }); } - persistFiles(entry, mediaFiles, options) { - let filename, - part, - parts, - subtree; + composeFileTree(files) { + let filename; + let part; + let parts; + let subtree; const fileTree = {}; - const uploadPromises = []; - - const files = mediaFiles.concat(entry); files.forEach((file) => { if (file.uploaded) { return; } - uploadPromises.push(this.uploadBlob(file)); parts = file.path.split("/").filter(part => part); filename = parts.pop(); subtree = fileTree; @@ -197,6 +199,22 @@ export default class API { subtree[filename] = file; file.file = true; }); + + return fileTree; + } + + persistFiles(entry, mediaFiles, options) { + const uploadPromises = []; + const files = mediaFiles.concat(entry); + + + files.forEach((file) => { + if (file.uploaded) { return; } + uploadPromises.push(this.uploadBlob(file)); + }); + + const fileTree = this.composeFileTree(files); + return Promise.all(uploadPromises).then(() => { if (!options.mode || (options.mode && options.mode === SIMPLE)) { return this.getBranch() @@ -204,7 +222,7 @@ export default class API { .then(changeTree => this.commit(options.commitMessage, changeTree)) .then(response => this.patchBranch(this.branch, response.sha)); } else if (options.mode && options.mode === EDITORIAL_WORKFLOW) { - const mediaFilesList = mediaFiles.map(file => file.path); + const mediaFilesList = mediaFiles.map(file => ({ path: file.path, sha: file.sha })); return this.editorialWorkflowGit(fileTree, entry, mediaFilesList, options); } }); @@ -214,9 +232,8 @@ export default class API { const contentKey = entry.slug; const branchName = `cms/${ contentKey }`; const unpublished = options.unpublished || false; - if (!unpublished) { - // Open new editorial review workflow for this entry - Create new metadata and commit to new branch + // Open new editorial review workflow for this entry - Create new metadata and commit to new branch` const contentKey = entry.slug; const branchName = `cms/${ contentKey }`; @@ -239,11 +256,15 @@ export default class API { title: options.parsedData && options.parsedData.title, description: options.parsedData && options.parsedData.description, objects: { - entry: entry.path, + entry: { + path: entry.path, + sha: entry.sha, + }, files: filesList, }, timeStamp: new Date().toISOString(), - }))); + } + ))); } else { // Entry is already on editorial review workflow - just update metadata and commit to existing branch return this.getBranch(branchName) @@ -257,13 +278,18 @@ export default class API { .then((metadata) => { let files = metadata.objects && metadata.objects.files || []; files = files.concat(filesList); - + const updatedPR = metadata.pr; + updatedPR.head = response.sha; return { ...metadata, + pr: updatedPR, title: options.parsedData && options.parsedData.title, description: options.parsedData && options.parsedData.description, objects: { - entry: entry.path, + entry: { + path: entry.path, + sha: entry.sha, + }, files: _.uniq(files), }, timeStamp: new Date().toISOString(), @@ -285,17 +311,15 @@ export default class API { .then(updatedMetadata => this.storeMetadata(contentKey, updatedMetadata)); } - publishUnpublishedEntry(collection, slug, status) { + publishUnpublishedEntry(collection, slug) { const contentKey = slug; + let prNumber; return this.retrieveMetadata(contentKey) - .then((metadata) => { - const headSha = metadata.pr && metadata.pr.head; - const number = metadata.pr && metadata.pr.number; - return this.mergePR(headSha, number); - }) + .then(metadata => this.mergePR(metadata.pr, metadata.objects)) .then(() => this.deleteBranch(`cms/${ contentKey }`)); } + createRef(type, name, sha) { return this.request(`${ this.repoURL }/git/refs`, { method: "POST", @@ -340,16 +364,40 @@ export default class API { }); } - mergePR(headSha, number) { - return this.request(`${ this.repoURL }/pulls/${ number }/merge`, { + mergePR(pullrequest, objects) { + const headSha = pullrequest.head; + const prNumber = pullrequest.number; + console.log("%c Merging PR", "line-height: 30px;text-align: center;font-weight: bold"); // eslint-disable-line + return this.request(`${ this.repoURL }/pulls/${ prNumber }/merge`, { method: "PUT", body: JSON.stringify({ commit_message: "Automatically generated. Merged on Netlify CMS.", sha: headSha, }), + }) + .catch((error) => { + if (error instanceof APIError && error.status === 405) { + this.forceMergePR(pullrequest, objects); + } else { + throw error; + } }); } + forceMergePR(pullrequest, objects) { + const files = objects.files.concat(objects.entry); + const fileTree = this.composeFileTree(files); + let commitMessage = "Automatically generated. Merged on Netlify CMS\n\nForce merge of:"; + files.forEach((file) => { + commitMessage += `\n* "${ file.path }"`; + }); + console.log("%c Automatic merge not possible - Forcing merge.", "line-height: 30px;text-align: center;font-weight: bold"); // eslint-disable-line + return this.getBranch() + .then(branchData => this.updateTree(branchData.commit.sha, "/", fileTree)) + .then(changeTree => this.commit(commitMessage, changeTree)) + .then(response => this.patchBranch(this.branch, response.sha)); + } + getTree(sha) { return sha ? this.request(`${ this.repoURL }/git/trees/${ sha }`) : Promise.resolve({ tree: [] }); } @@ -379,9 +427,9 @@ export default class API { updateTree(sha, path, fileTree) { return this.getTree(sha) .then((tree) => { - let obj, - filename, - fileOrDir; + let obj; + let filename; + let fileOrDir; const updates = []; const added = {}; diff --git a/src/backends/github/implementation.js b/src/backends/github/implementation.js index 2bd95afc..74340dc3 100644 --- a/src/backends/github/implementation.js +++ b/src/backends/github/implementation.js @@ -93,7 +93,7 @@ export default class GitHub { resolve(null); sem.leave(); } else { - const path = data.metaData.objects.entry; + const path = data.metaData.objects.entry.path; resolve({ slug, file: { path }, @@ -104,7 +104,7 @@ export default class GitHub { } }).catch((err) => { sem.leave(); - reject(err); + resolve(null); })); })); }); @@ -120,19 +120,22 @@ export default class GitHub { unpublishedEntry(collection, slug) { return this.api.readUnpublishedBranchFile(slug) - .then(data => ({ - slug, - file: { path: data.metaData.objects.entry }, - data: data.fileData, - metaData: data.metaData, - })); + .then((data) => { + if (!data) return null; + return { + slug, + file: { path: data.metaData.objects.entry.path }, + data: data.fileData, + metaData: data.metaData, + }; + }); } updateUnpublishedEntryStatus(collection, slug, newStatus) { return this.api.updateUnpublishedEntryStatus(collection, slug, newStatus); } - publishUnpublishedEntry(collection, slug, status) { - return this.api.publishUnpublishedEntry(collection, slug, status); + publishUnpublishedEntry(collection, slug) { + return this.api.publishUnpublishedEntry(collection, slug); } } diff --git a/src/components/UnpublishedListing/UnpublishedListing.js b/src/components/UnpublishedListing/UnpublishedListing.js index a5ddac64..32673da5 100644 --- a/src/components/UnpublishedListing/UnpublishedListing.js +++ b/src/components/UnpublishedListing/UnpublishedListing.js @@ -58,7 +58,7 @@ class UnpublishedListing extends React.Component { // Look for an "author" field. Fallback to username on backend implementation; const author = entry.getIn(['data', 'author'], entry.getIn(['metaData', 'user'])); const timeStamp = moment(entry.getIn(['metaData', 'timeStamp'])).format('llll'); - const link = `editorialworkflow/${ entry.getIn(['metaData', 'collection']) }/${ entry.getIn(['metaData', 'status']) }/${ entry.get('slug') }`; + const link = `collections/${ entry.getIn(['metaData', 'collection']) }/entries/${ entry.get('slug') }`; const slug = entry.get('slug'); const ownStatus = entry.getIn(['metaData', 'status']); const collection = entry.getIn(['metaData', 'collection']); diff --git a/src/containers/EntryPage.js b/src/containers/EntryPage.js index f3780281..67c727bd 100644 --- a/src/containers/EntryPage.js +++ b/src/containers/EntryPage.js @@ -9,7 +9,7 @@ import { changeDraftField, persistEntry, } from '../actions/entries'; -import { cancelEdit } from '../actions/editor'; +import { closeEntry } from '../actions/editor'; import { addAsset, removeAsset } from '../actions/media'; import { openSidebar } from '../actions/globalUI'; import { selectEntry, getAsset } from '../reducers'; @@ -32,7 +32,7 @@ class EntryPage extends React.Component { loadEntry: PropTypes.func.isRequired, persistEntry: PropTypes.func.isRequired, removeAsset: PropTypes.func.isRequired, - cancelEdit: PropTypes.func.isRequired, + closeEntry: PropTypes.func.isRequired, openSidebar: PropTypes.func.isRequired, fields: ImmutablePropTypes.list.isRequired, slug: PropTypes.string, @@ -45,7 +45,7 @@ class EntryPage extends React.Component { if (newEntry) { createEmptyDraft(collection); } else { - loadEntry(entry, collection, slug); + loadEntry(collection, slug); } } @@ -67,6 +67,10 @@ class EntryPage extends React.Component { if (entry) this.props.createDraftFromEntry(entry); }; + handleCloseEntry = () => { + this.props.closeEntry(); + }; + handlePersistEntry = () => { const { persistEntry, collection, entryDraft } = this.props; persistEntry(collection, entryDraft); @@ -82,7 +86,7 @@ class EntryPage extends React.Component { changeDraftField, addAsset, removeAsset, - cancelEdit, + closeEntry, } = this.props; if (entry && entry.get('error')) { @@ -104,7 +108,7 @@ class EntryPage extends React.Component { onAddAsset={addAsset} onRemoveAsset={removeAsset} onPersist={this.handlePersistEntry} - onCancelEdit={cancelEdit} + onCancelEdit={this.handleCloseEntry} /> ); } @@ -141,7 +145,7 @@ export default connect( createEmptyDraft, discardDraft, persistEntry, - cancelEdit, + closeEntry, openSidebar, } )(entryPageHOC(EntryPage)); diff --git a/src/containers/editorialWorkflow/EntryPageHOC.js b/src/containers/editorialWorkflow/EntryPageHOC.js index b3010ab4..194a139f 100644 --- a/src/containers/editorialWorkflow/EntryPageHOC.js +++ b/src/containers/editorialWorkflow/EntryPageHOC.js @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { EDITORIAL_WORKFLOW } from '../../constants/publishModes'; -import { selectUnpublishedEntry } from '../../reducers'; +import { selectUnpublishedEntry, selectEntry } from '../../reducers'; import { loadUnpublishedEntry, persistUnpublishedEntry } from '../../actions/editorialWorkflow'; @@ -13,36 +13,32 @@ export default function EntryPageHOC(EntryPage) { } function mapStateToProps(state, ownProps) { + const { collections } = state; const isEditorialWorkflow = (state.config.get('publish_mode') === EDITORIAL_WORKFLOW); - const unpublishedEntry = ownProps.route && ownProps.route.unpublishedEntry === true; - const returnObj = { isEditorialWorkflow }; - if (isEditorialWorkflow && unpublishedEntry) { - const status = ownProps.params.status; + if (isEditorialWorkflow) { const slug = ownProps.params.slug; - const entry = selectUnpublishedEntry(state, status, slug); - returnObj.entry = entry; + const collection = collections.get(ownProps.params.name); + const unpublishedEntry = selectUnpublishedEntry(state, collection.get('name'), slug); + if (unpublishedEntry) { + returnObj.unpublishedEntry = true; + returnObj.entry = unpublishedEntry; + } } return returnObj; } function mergeProps(stateProps, dispatchProps, ownProps) { - const { isEditorialWorkflow } = stateProps; + const { isEditorialWorkflow, unpublishedEntry } = stateProps; const { dispatch } = dispatchProps; - - const unpublishedEntry = ownProps.route && ownProps.route.unpublishedEntry === true; - const status = ownProps.params.status; - const returnObj = {}; - if (unpublishedEntry) { - // Overwrite loadEntry to loadUnpublishedEntry - returnObj.loadEntry = (entry, collection, slug) => { - dispatch(loadUnpublishedEntry(collection, status, slug)); - }; - } - if (isEditorialWorkflow) { + // Overwrite loadEntry to loadUnpublishedEntry + returnObj.loadEntry = (collection, slug) => { + dispatch(loadUnpublishedEntry(collection, slug)); + }; + // Overwrite persistEntry to persistUnpublishedEntry returnObj.persistEntry = (collection, entryDraft) => { dispatch(persistUnpublishedEntry(collection, entryDraft, unpublishedEntry)); diff --git a/src/containers/editorialWorkflow/UnpublishedEntriesPanel.js b/src/containers/editorialWorkflow/UnpublishedEntriesPanel.js index 7abca9f5..8d0220e6 100644 --- a/src/containers/editorialWorkflow/UnpublishedEntriesPanel.js +++ b/src/containers/editorialWorkflow/UnpublishedEntriesPanel.js @@ -3,7 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { OrderedMap } from 'immutable'; import { connect } from 'react-redux'; import { loadUnpublishedEntries, updateUnpublishedEntryStatus, publishUnpublishedEntry } from '../../actions/editorialWorkflow'; -import { selectUnpublishedEntries } from '../../reducers'; +import { selectUnpublishedEntriesByStatus } from '../../reducers'; import { EDITORIAL_WORKFLOW, status } from '../../constants/publishModes'; import UnpublishedListing from '../../components/UnpublishedListing/UnpublishedListing'; import { Loader } from '../../components/UI'; @@ -48,11 +48,11 @@ function mapStateToProps(state) { /* * Generates an ordered Map of the available status as keys. - * Each key containing a List of available unpubhlished entries - * Eg.: OrderedMap{'draft':List(), 'pending_review':List(), 'pending_publish':List()} + * Each key containing a Sequence of available unpubhlished entries + * Eg.: OrderedMap{'draft':Seq(), 'pending_review':Seq(), 'pending_publish':Seq()} */ returnObj.unpublishedEntries = status.reduce((acc, currStatus) => ( - acc.set(currStatus, selectUnpublishedEntries(state, currStatus)) + acc.set(currStatus, selectUnpublishedEntriesByStatus(state, currStatus)) ), OrderedMap()); } return returnObj; diff --git a/src/reducers/editorialWorkflow.js b/src/reducers/editorialWorkflow.js index b426d1c0..628382a1 100644 --- a/src/reducers/editorialWorkflow.js +++ b/src/reducers/editorialWorkflow.js @@ -1,7 +1,8 @@ import { Map, List, fromJS } from 'immutable'; -import { status, EDITORIAL_WORKFLOW } from '../constants/publishModes'; +import { EDITORIAL_WORKFLOW } from '../constants/publishModes'; import { UNPUBLISHED_ENTRY_REQUEST, + UNPUBLISHED_ENTRY_REDIRECT, UNPUBLISHED_ENTRY_SUCCESS, UNPUBLISHED_ENTRIES_REQUEST, UNPUBLISHED_ENTRIES_SUCCESS, @@ -21,78 +22,59 @@ const unpublishedEntries = (state = null, action) => { } return state; case UNPUBLISHED_ENTRY_REQUEST: - return state.setIn(['entities', `${ action.payload.status }.${ action.payload.slug }`, 'isFetching'], true); + return state.setIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`, 'isFetching'], true); + + case UNPUBLISHED_ENTRY_REDIRECT: + return state.deleteIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`]); case UNPUBLISHED_ENTRY_SUCCESS: return state.setIn( - ['entities', `${ action.payload.status }.${ action.payload.entry.slug }`], + ['entities', `${ action.payload.collection }.${ action.payload.entry.slug }`], fromJS(action.payload.entry) ); - case UNPUBLISHED_ENTRIES_REQUEST: return state.setIn(['pages', 'isFetching'], true); case UNPUBLISHED_ENTRIES_SUCCESS: - const { entries, pages } = action.payload; return state.withMutations((map) => { - entries.forEach(entry => ( - map.setIn(['entities', `${ entry.metaData.status }.${ entry.slug }`], fromJS(entry).set('isFetching', false)) + action.payload.entries.forEach(entry => ( + map.setIn(['entities', `${ entry.collection }.${ entry.slug }`], fromJS(entry).set('isFetching', false)) )); map.set('pages', Map({ - ...pages, - ids: List(entries.map(entry => entry.slug)), + ...action.payload.pages, + ids: List(action.payload.entries.map(entry => entry.slug)), })); }); case UNPUBLISHED_ENTRY_PERSIST_REQUEST: // Update Optimistically - const { collection, entry } = action.payload; - const ownStatus = entry.getIn(['metaData', 'status'], status.first()); return state.withMutations((map) => { - map.setIn(['entities', `${ ownStatus }.${ entry.get('slug') }`], fromJS(entry)); - map.updateIn(['pages', 'ids'], List(), list => list.push(entry.get('slug'))); + map.setIn(['entities', `${ action.payload.collection }.${ action.payload.entry.get('slug') }`], fromJS(action.payload.entry)); + map.updateIn(['pages', 'ids'], List(), list => list.push(action.payload.entry.get('slug'))); }); case UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST: // Update Optimistically - return state.withMutations((map) => { - let entry = map.getIn(['entities', `${ action.payload.oldStatus }.${ action.payload.slug }`]); - entry = entry.setIn(['metaData', 'status'], action.payload.newStatus); - - let entities = map.get('entities').filter((val, key) => ( - key !== `${ action.payload.oldStatus }.${ action.payload.slug }` - )); - entities = entities.set(`${ action.payload.newStatus }.${ action.payload.slug }`, entry); - - map.set('entities', entities); - }); + return state.setIn( + ['entities', `${ action.payload.collection }.${ action.payload.slug }`, 'metaData', 'status'], + action.payload.newStatus + ); case UNPUBLISHED_ENTRY_PUBLISH_REQUEST: // Update Optimistically - return state.deleteIn(['entities', `${ action.payload.status }.${ action.payload.slug }`]); + return state.deleteIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`]); default: return state; } }; -export const selectUnpublishedEntry = (state, status, slug) => { - return state && state.getIn(['entities', `${ status }.${ slug }`]); -}; +export const selectUnpublishedEntry = (state, collection, slug) => state && state.getIn(['entities', `${ collection }.${ slug }`]); -export const selectUnpublishedEntries = (state, status) => { - if (!state) return; - const slugs = state.getIn(['pages', 'ids']); - - return slugs && slugs.reduce((acc, slug) => { - const entry = selectUnpublishedEntry(state, status, slug); - if (entry) { - return acc.push(entry); - } else { - return acc; - } - }, List()); +export const selectUnpublishedEntriesByStatus = (state, status) => { + if (!state) return null; + return state.get('entities').filter(entry => entry.getIn(['metaData', 'status']) === status).valueSeq(); }; diff --git a/src/reducers/index.js b/src/reducers/index.js index 2b534751..78dfc39e 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -40,11 +40,11 @@ export const selectSearchedEntries = (state) => { return searchItems && searchItems.map(({ collection, slug }) => fromEntries.selectEntry(state.entries, collection, slug)); }; -export const selectUnpublishedEntry = (state, status, slug) => - fromEditorialWorkflow.selectUnpublishedEntry(state.editorialWorkflow, status, slug); +export const selectUnpublishedEntry = (state, collection, slug) => + fromEditorialWorkflow.selectUnpublishedEntry(state.editorialWorkflow, collection, slug); -export const selectUnpublishedEntries = (state, status) => - fromEditorialWorkflow.selectUnpublishedEntries(state.editorialWorkflow, status); +export const selectUnpublishedEntriesByStatus = (state, status) => + fromEditorialWorkflow.selectUnpublishedEntriesByStatus(state.editorialWorkflow, status); export const selectIntegration = (state, collection, hook) => fromIntegrations.selectIntegration(state.integrations, collection, hook); diff --git a/src/routing/routes.js b/src/routing/routes.js index cc6b9460..4795ee0b 100644 --- a/src/routing/routes.js +++ b/src/routing/routes.js @@ -23,11 +23,6 @@ export default ( path="/collections/:name/entries/:slug" component={EntryPage} /> -