diff --git a/src/actions/editorialWorkflow.js b/src/actions/editorialWorkflow.js index c7e73082..9b1cb70b 100644 --- a/src/actions/editorialWorkflow.js +++ b/src/actions/editorialWorkflow.js @@ -17,6 +17,9 @@ export const UNPUBLISHED_ENTRY_PERSIST_SUCCESS = 'UNPUBLISHED_ENTRY_PERSIST_SUCC export const UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST = 'UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST'; export const UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS = 'UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS'; +export const UNPUBLISHED_ENTRY_PUBLISH_REQUEST = 'UNPUBLISHED_ENTRY_PUBLISH_REQUEST'; +export const UNPUBLISHED_ENTRY_PUBLISH_SUCCESS = 'UNPUBLISHED_ENTRY_PUBLISH_SUCCESS'; + /* * Simple Action Creators (Internal) */ @@ -81,7 +84,6 @@ function unpublishedEntryPersistedFail(error) { }; } - function unpublishedEntryStatusChangeRequest(collection, slug, oldStatus, newStatus) { return { type: UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST, @@ -96,6 +98,20 @@ function unpublishedEntryStatusChangePersisted(collection, slug, oldStatus, newS }; } +function unpublishedEntryPublishRequest(collection, slug, status) { + return { + type: UNPUBLISHED_ENTRY_PUBLISH_REQUEST, + payload: { collection, slug, status } + }; +} + +function unpublishedEntryPublished(collection, slug, status) { + return { + type: UNPUBLISHED_ENTRY_PUBLISH_SUCCESS, + payload: { collection, slug, status } + }; +} + /* * Exported Thunk Action Creators */ @@ -149,3 +165,15 @@ export function updateUnpublishedEntryStatus(collection, slug, oldStatus, newSta }); }; } + +export function publishUnpublishedEntry(collection, slug, status) { + return (dispatch, getState) => { + const state = getState(); + const backend = currentBackend(state.config); + dispatch(unpublishedEntryPublishRequest(collection, slug, status)); + backend.publishUnpublishedEntry(collection, slug, status) + .then(() => { + dispatch(unpublishedEntryPublished(collection, slug, status)); + }); + }; +} diff --git a/src/backends/backend.js b/src/backends/backend.js index db163b74..1cb9bc42 100644 --- a/src/backends/backend.js +++ b/src/backends/backend.js @@ -151,6 +151,10 @@ class Backend { return this.implementation.updateUnpublishedEntryStatus(collection, slug, newStatus); } + publishUnpublishedEntry(collection, slug, status) { + return this.implementation.publishUnpublishedEntry(collection, slug, status); + } + entryToRaw(collection, entry) { const format = resolveFormat(collection, entry); diff --git a/src/backends/github/API.js b/src/backends/github/API.js index 196150e8..fe2dc273 100644 --- a/src/backends/github/API.js +++ b/src/backends/github/API.js @@ -201,17 +201,24 @@ export default class API { if (!unpublished) { // Open new editorial review workflow for this entry - Create new metadata and commit to new branch + const contentKey = options.collectionName ? `${options.collectionName}-${entry.slug}` : entry.slug; + const branchName = `cms/${contentKey}`; + return this.getBranch() .then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree)) .then(changeTree => this.commit(options.commitMessage, changeTree)) - .then((response) => { - const contentKey = options.collectionName ? `${options.collectionName}-${entry.slug}` : entry.slug; - const branchName = `cms/${contentKey}`; + .then(commitResponse => this.createBranch(branchName, commitResponse.sha)) + .then(branchResponse => this.createPR(options.commitMessage, branchName)) + .then((prResponse) => { return this.user().then(user => { return user.name ? user.name : user.login; }) .then(username => this.storeMetadata(contentKey, { type: 'PR', + pr: { + number: prResponse.number, + head: prResponse.head && prResponse.head.sha + }, user: username, status: status.first(), branch: branchName, @@ -223,9 +230,7 @@ export default class API { files: filesList }, timeStamp: new Date().toISOString() - })) - .then(this.createBranch(branchName, response.sha)) - .then(this.createPR(options.commitMessage, `cms/${contentKey}`)); + })); }); } else { // Entry is already on editorial review workflow - just update metadata and commit to existing branch @@ -272,6 +277,16 @@ export default class API { .then(updatedMetadata => this.storeMetadata(contentKey, updatedMetadata)); } + publishUnpublishedEntry(collection, slug, status) { + const contentKey = collection ? `${collection}-${slug}` : slug; + 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); + }); + } + createRef(type, name, sha) { return this.request(`${this.repoURL}/git/refs`, { method: 'POST', @@ -306,6 +321,16 @@ export default class API { }); } + mergePR(headSha, number) { + return this.request(`${this.repoURL}/pulls/${number}/merge`, { + method: 'PUT', + body: JSON.stringify({ + commit_message: 'Automatically generated. Merged on Netlify CMS.', + sha: headSha + }), + }); + } + getTree(sha) { return sha ? this.request(`${this.repoURL}/git/trees/${sha}`) : Promise.resolve({ tree: [] }); } diff --git a/src/backends/github/implementation.js b/src/backends/github/implementation.js index c744bc46..2d270261 100644 --- a/src/backends/github/implementation.js +++ b/src/backends/github/implementation.js @@ -102,4 +102,8 @@ export default class GitHub { updateUnpublishedEntryStatus(collection, slug, newStatus) { return this.api.updateUnpublishedEntryStatus(collection, slug, newStatus); } + + publishUnpublishedEntry(collection, slug, status) { + return this.api.publishUnpublishedEntry(collection, slug, status); + } } diff --git a/src/components/UnpublishedListing.css b/src/components/UnpublishedListing.css index db202e36..ebd7bcd6 100644 --- a/src/components/UnpublishedListing.css +++ b/src/components/UnpublishedListing.css @@ -26,7 +26,7 @@ width: 100% !important; margin: 7px 0; - & h1 { + & h2 { font-size: 17px; & small { font-weight: normal; @@ -38,6 +38,11 @@ font-size: 12px; margin-top: 5px; } + + & button { + margin: 10px 10px 0 0; + float: right; + } } diff --git a/src/components/UnpublishedListing.js b/src/components/UnpublishedListing.js index 8fa90bf2..9d09b6a0 100644 --- a/src/components/UnpublishedListing.js +++ b/src/components/UnpublishedListing.js @@ -5,7 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import moment from 'moment'; import { Card } from './UI'; import { Link } from 'react-router'; -import { statusDescriptions } from '../constants/publishModes'; +import { status, statusDescriptions } from '../constants/publishModes'; import styles from './UnpublishedListing.css'; const CARD = 'card'; @@ -22,56 +22,52 @@ function Column({ connectDropTarget, status, isOver, children }) { ); } - const columnTargetSpec = { drop(props, monitor) { const slug = monitor.getItem().slug; const collection = monitor.getItem().collection; - const oldStatus = monitor.getItem().currentStatus; + const oldStatus = monitor.getItem().ownStatus; props.onChangeStatus(collection, slug, oldStatus, props.status); } }; - function columnCollect(connect, monitor) { return { connectDropTarget: connect.dropTarget(), isOver: monitor.isOver() }; } - - Column = DropTarget(CARD, columnTargetSpec, columnCollect)(Column); /* * Card DropTarget Component */ -function EntryCard({ connectDragSource, children }) { +function EntryCard({ slug, collection, ownStatus, onRequestPublish, connectDragSource, children }) { return connectDragSource(
{children} + {(ownStatus === status.last()) && + + }
); } - const cardDragSpec = { beginDrag(props) { return { slug: props.slug, collection: props.collection, - currentStatus: props.status + ownStatus: props.ownStatus }; } }; - function cardCollect(connect, monitor) { return { connectDragSource: connect.dragSource() }; } - EntryCard = DragSource(CARD, cardDragSpec, cardCollect)(EntryCard); /* @@ -81,6 +77,14 @@ class UnpublishedListing extends React.Component { constructor(props) { super(props); this.renderColumns = this.renderColumns.bind(this); + this.requestPublish = this.requestPublish.bind(this); + } + + requestPublish(collection, slug, ownStatus) { + if (ownStatus !== status.last()) return; + if (window.confirm('Are you sure you want to publish this entry?')) { + this.props.handlePublish(collection, slug, ownStatus); + } } renderColumns(entries, column) { @@ -103,14 +107,18 @@ class UnpublishedListing extends React.Component { 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 slug = entry.get('slug'); + const status = entry.getIn(['metaData', 'status']); + const collection = entry.getIn(['metaData', 'collection']); return ( -

{entry.getIn(['data', 'title'])} by {author}

+

{entry.getIn(['data', 'title'])} by {author}

Last updated: {timeStamp} by {entry.getIn(['metaData', 'user'])}

); @@ -122,7 +130,6 @@ class UnpublishedListing extends React.Component { render() { const columns = this.renderColumns(this.props.entries); - return (

Editorial Workflow

@@ -137,6 +144,7 @@ class UnpublishedListing extends React.Component { UnpublishedListing.propTypes = { entries: ImmutablePropTypes.orderedMap, handleChangeStatus: PropTypes.func.isRequired, + handlePublish: PropTypes.func.isRequired, }; export default DragDropContext(HTML5Backend)(UnpublishedListing); diff --git a/src/containers/App.js b/src/containers/App.js index 052af2c1..0cfec9fa 100644 --- a/src/containers/App.js +++ b/src/containers/App.js @@ -3,6 +3,7 @@ import { connect } from 'react-redux'; import { loadConfig } from '../actions/config'; import { loginUser } from '../actions/auth'; import { currentBackend } from '../backends/backend'; +import { Loader } from '../components/UI'; import { SHOW_COLLECTION, CREATE_COLLECTION, HELP } from '../actions/findbar'; import FindBar from './FindBar'; import styles from './App.css'; @@ -26,7 +27,7 @@ class App extends React.Component { configLoading() { return
-

Loading configuration...

+ Loading configuration...
; } diff --git a/src/containers/editorialWorkflow/CollectionPageHOC.js b/src/containers/editorialWorkflow/CollectionPageHOC.js index b5e14d08..6aeacb71 100644 --- a/src/containers/editorialWorkflow/CollectionPageHOC.js +++ b/src/containers/editorialWorkflow/CollectionPageHOC.js @@ -1,7 +1,7 @@ import React, { PropTypes } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { OrderedMap } from 'immutable'; -import { loadUnpublishedEntries, updateUnpublishedEntryStatus } from '../../actions/editorialWorkflow'; +import { loadUnpublishedEntries, updateUnpublishedEntryStatus, publishUnpublishedEntry } from '../../actions/editorialWorkflow'; import { selectUnpublishedEntries } from '../../reducers'; import { EDITORIAL_WORKFLOW, status } from '../../constants/publishModes'; import UnpublishedListing from '../../components/UnpublishedListing'; @@ -19,17 +19,17 @@ export default function CollectionPageHOC(CollectionPage) { super.componentDidMount(); } - handleChangeStatus(collection, slug, oldStatus, newStatus) { - this.props.updateUnpublishedEntryStatus(collection, slug, oldStatus, newStatus); - } - render() { - const { isEditorialWorkflow, unpublishedEntries } = this.props; + const { isEditorialWorkflow, unpublishedEntries, updateUnpublishedEntryStatus, publishUnpublishedEntry } = this.props; if (!isEditorialWorkflow) return super.render(); return (
- + {super.render()}
); @@ -60,5 +60,8 @@ export default function CollectionPageHOC(CollectionPage) { return returnObj; } - return connect(mapStateToProps, { updateUnpublishedEntryStatus })(CollectionPageHOC); + return connect(mapStateToProps, { + updateUnpublishedEntryStatus, + publishUnpublishedEntry + })(CollectionPageHOC); } diff --git a/src/reducers/editorialWorkflow.js b/src/reducers/editorialWorkflow.js index e5dcfd2c..246656f0 100644 --- a/src/reducers/editorialWorkflow.js +++ b/src/reducers/editorialWorkflow.js @@ -5,7 +5,8 @@ import { UNPUBLISHED_ENTRY_SUCCESS, UNPUBLISHED_ENTRIES_REQUEST, UNPUBLISHED_ENTRIES_SUCCESS, - UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS + UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS, + UNPUBLISHED_ENTRY_PUBLISH_SUCCESS } from '../actions/editorialWorkflow'; import { CONFIG_SUCCESS } from '../actions/config'; @@ -45,19 +46,21 @@ const unpublishedEntries = (state = null, action) => { }); case UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS: - const { slug, oldStatus, newStatus } = action.payload; return state.withMutations((map) => { - const entry = map.getIn(['entities', `${oldStatus}.${slug}`]); + 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 !== `${oldStatus}.${slug}` + key !== `${action.payload.oldStatus}.${action.payload.slug}` )); - - entities = entities.set(`${newStatus}.${slug}`, entry); + entities = entities.set(`${action.payload.newStatus}.${action.payload.slug}`, entry); map.set('entities', entities); }); + case UNPUBLISHED_ENTRY_PUBLISH_SUCCESS: + return state.deleteIn(['entities', `${action.payload.status}.${action.payload.slug}`]); + default: return state; }