diff --git a/package.json b/package.json index 0ac77f63..c4432c59 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ }, "dependencies": { "bricks.js": "^1.7.0", + "dateformat": "^1.0.12", "fuzzy": "^0.1.1", "js-base64": "^2.1.9", "json-loader": "^0.5.4", diff --git a/src/backends/github/API.js b/src/backends/github/API.js index 9fe61b71..210a229f 100644 --- a/src/backends/github/API.js +++ b/src/backends/github/API.js @@ -103,24 +103,12 @@ export default class API { } retrieveMetadata(key) { - const cache = LocalForage.getItem(`gh.meta.${key}`); - return cache.then((cached) => { - if (cached && cached.expires > Date.now()) { return cached.data; } - - return this.request(`${this.repoURL}/contents/${key}.json`, { - params: { ref: 'refs/meta/_netlify_cms' }, - headers: { Accept: 'application/vnd.github.VERSION.raw' }, - cache: 'no-store', - }) - .then(response => JSON.parse(response)) - .then((result) => { - LocalForage.setItem(`gh.meta.${key}`, { - expires: Date.now() + 300000, // In 5 minutes - data: result, - }); - return result; - }); - }).catch(error => null); + return this.request(`${this.repoURL}/contents/${key}.json`, { + params: { ref: 'refs/meta/_netlify_cms' }, + headers: { Accept: 'application/vnd.github.VERSION.raw' }, + cache: 'no-store', + }) + .then(response => JSON.parse(response)); } readFile(path, sha, branch = this.branch) { @@ -148,14 +136,26 @@ export default class API { } readUnpublishedBranchFile(contentKey) { - let metaData; - return this.retrieveMetadata(contentKey) - .then(data => { - metaData = data; - return this.readFile(data.objects.entry, null, data.branch); - }) - .then(file => { - return { metaData, file }; + const cache = LocalForage.getItem(`gh.unpublished.${contentKey}`); + return cache.then((cached) => { + if (cached && cached.expires > Date.now()) { return cached.data; } + + let metaData; + return this.retrieveMetadata(contentKey) + .then(data => { + metaData = data; + return this.readFile(data.objects.entry, null, data.branch); + }) + .then(file => { + return { metaData, file }; + }) + .then((result) => { + LocalForage.setItem(`gh.unpublished.${contentKey}`, { + expires: Date.now() + 300000, // In 5 minutes + data: result, + }); + return result; + }); }); } @@ -191,19 +191,24 @@ export default class API { if (options.mode && options.mode === EDITORIAL_WORKFLOW) { const contentKey = options.collectionName ? `${options.collectionName}-${entry.slug}` : entry.slug; const branchName = `cms/${contentKey}`; - return this.createBranch(branchName, response.sha) - .then(this.storeMetadata(contentKey, { + return this.user().then(user => { + return user.name ? user.name : user.login; + }) + .then(username => this.storeMetadata(contentKey, { type: 'PR', - status: status.DRAFT, + user: username, + status: status.first(), branch: branchName, collection: options.collectionName, - title: options.parsedData.title, - description: options.parsedData.description, + title: options.parsedData && options.parsedData.title, + description: options.parsedData && options.parsedData.description, objects: { entry: entry.path, files: mediaFiles.map(file => file.path) - } + }, + timeStamp: new Date().toISOString() })) + .then(this.createBranch(branchName, response.sha)) .then(this.createPR(options.commitMessage, `cms/${contentKey}`)); } else { return this.patchBranch(this.branch, response.sha); diff --git a/src/components/UnpublishedListing.css b/src/components/UnpublishedListing.css new file mode 100644 index 00000000..a4b97b67 --- /dev/null +++ b/src/components/UnpublishedListing.css @@ -0,0 +1,29 @@ +.column { + position: relative; + display: inline-block; + vertical-align: top; + text-align: center; + width: 28%; +} + +.column:not(:last-child) { + margin-right: 8%; +} + +.card { + width: 100% !important; + margin: 7px 0; + + & h1 { + font-size: 17px; + & small { + font-weight: normal; + } + } + + & p { + color: #555; + font-size: 12px; + margin-top: 5px; + } +} diff --git a/src/components/UnpublishedListing.js b/src/components/UnpublishedListing.js index 0ef0cdfa..e43c8b07 100644 --- a/src/components/UnpublishedListing.js +++ b/src/components/UnpublishedListing.js @@ -1,29 +1,41 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import dateFormat from 'dateFormat'; import { Card } from './UI'; import { statusDescriptions } from '../constants/publishModes'; +import styles from './UnpublishedListing.css'; export default class UnpublishedListing extends React.Component { - renderColumn(entries) { + renderColumns(entries, column) { if (!entries) return; - return ( -
+ + if (!column) { + return entries.entrySeq().map(([currColumn, currEntries]) => ( +
+

{statusDescriptions.get(currColumn)}

+ {this.renderColumns(currEntries, currColumn)} +
+ )); + } else { + return
{entries.map(entry => { - return

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

; + // Look for an "author" field. Fallback to username on backend implementation; + const author = entry.getIn(['data', 'author'], entry.getIn(['metaData', 'user'])); + const timeStamp = dateFormat(Date.parse(entry.getIn(['metaData', 'timeStamp'])), 'longDate'); + return ( + +

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

+

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

+
+ ); } )} -
- ); +
; + } } render() { - const { entries } = this.props; - const columns = entries.entrySeq().map(([key, currEntries]) => ( -
-

{statusDescriptions.get(key)}

- {this.renderColumn(currEntries)} -
- )); + const columns = this.renderColumns(this.props.entries); return (
@@ -34,12 +46,5 @@ export default class UnpublishedListing extends React.Component { } UnpublishedListing.propTypes = { - entries: ImmutablePropTypes.map, + entries: ImmutablePropTypes.orderedMap, }; - - - -
-

Drafts

- Cool Recipe -
diff --git a/src/constants/publishModes.js b/src/constants/publishModes.js index 9e7256d9..2cf4e443 100644 --- a/src/constants/publishModes.js +++ b/src/constants/publishModes.js @@ -1,18 +1,18 @@ -import { Map } from 'immutable'; +import { Map, OrderedMap } from 'immutable'; // Create/edit workflow modes export const SIMPLE = 'simple'; export const EDITORIAL_WORKFLOW = 'editorial_workflow'; // Available status -export const status = { +export const status = OrderedMap({ DRAFT: 'draft', PENDING_REVIEW: 'pending_review', PENDING_PUBLISH: 'pending_publish', -}; +}); export const statusDescriptions = Map({ - [status.DRAFT]: 'Draft', - [status.PENDING_REVIEW]: 'Waiting for Review', - [status.PENDING_PUBLISH]: 'Waiting to go live', + [status.get('DRAFT')]: 'Draft', + [status.get('PENDING_REVIEW')]: 'Waiting for Review', + [status.get('PENDING_PUBLISH')]: 'Waiting to go live', }); diff --git a/src/containers/EditorialWorkflowHoC.js b/src/containers/EditorialWorkflowHoC.js index a7ffcc74..603f7819 100644 --- a/src/containers/EditorialWorkflowHoC.js +++ b/src/containers/EditorialWorkflowHoC.js @@ -1,12 +1,11 @@ import React, { PropTypes } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import { Map } from 'immutable'; +import { OrderedMap } from 'immutable'; import { init, loadUnpublishedEntries } from '../actions/editorialWorkflow'; import { selectUnpublishedEntries } from '../reducers'; import { EDITORIAL_WORKFLOW, status } from '../constants/publishModes'; import UnpublishedListing from '../components/UnpublishedListing'; import { connect } from 'react-redux'; -import _ from 'lodash'; export default function EditorialWorkflow(WrappedComponent) { class EditorialWorkflow extends WrappedComponent { @@ -45,11 +44,15 @@ export default function EditorialWorkflow(WrappedComponent) { const returnObj = { isEditorialWorkflow }; if (isEditorialWorkflow) { - returnObj.unpublishedEntries = _.reduce(status, (acc, currStatus) => { + /* + * 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()} + */ + returnObj.unpublishedEntries = status.reduce((acc, currStatus) => { return acc.set(currStatus, selectUnpublishedEntries(state, currStatus)); - }, Map()); + }, OrderedMap()); } - return returnObj; }