From f0e608a209114d96f1caf902103d5b43e11ae05b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Tue, 6 Sep 2016 13:04:17 -0300 Subject: [PATCH] Editorial Workflow skeleton --- src/actions/config.js | 6 ++- src/actions/editorialWorkflow.js | 53 +++++++++++++++++++ src/actions/entries.js | 1 - src/backends/backend.js | 26 +++++---- src/backends/github/API.js | 5 +- src/backends/github/implementation.js | 9 +++- src/backends/netlify-git/API.js | 4 +- src/backends/test-repo/implementation.js | 1 + .../publishModes.js} | 2 +- src/containers/CollectionPage.js | 3 +- src/formats/formats.js | 2 +- src/reducers/editorialWorkflow.js | 37 +++++++++++++ src/reducers/entries.js | 4 ++ src/reducers/index.js | 12 ++++- 14 files changed, 139 insertions(+), 26 deletions(-) create mode 100644 src/actions/editorialWorkflow.js rename src/{backends/constants.js => constants/publishModes.js} (50%) create mode 100644 src/reducers/editorialWorkflow.js diff --git a/src/actions/config.js b/src/actions/config.js index ff6581e9..8b286142 100644 --- a/src/actions/config.js +++ b/src/actions/config.js @@ -1,6 +1,8 @@ import yaml from 'js-yaml'; +import _ from 'lodash'; import { currentBackend } from '../backends/backend'; import { authenticate } from '../actions/auth'; +import * as publishModes from '../constants/publishModes'; import * as MediaProxy from '../valueObjects/MediaProxy'; export const CONFIG_REQUEST = 'CONFIG_REQUEST'; @@ -70,9 +72,9 @@ function parseConfig(data) { } } - if (!('publish_workflow' in config)) { + if (!('publish_mode' in config) || _.values(publishModes).indexOf(config.publish_mode) === -1) { // Make sure there is a publish workflow mode set - config['publish_workflow'] = 'simple'; + config.publish_mode = publishModes.SIMPLE; } if (!('public_folder' in config)) { diff --git a/src/actions/editorialWorkflow.js b/src/actions/editorialWorkflow.js new file mode 100644 index 00000000..6f478892 --- /dev/null +++ b/src/actions/editorialWorkflow.js @@ -0,0 +1,53 @@ +import { currentBackend } from '../backends/backend'; +import { EDITORIAL_WORKFLOW } from '../constants/publishModes'; +/* + * Contant Declarations + */ +export const UNPUBLISHED_ENTRIES_REQUEST = 'UNPUBLISHED_ENTRIES_REQUEST'; +export const UNPUBLISHED_ENTRIES_SUCCESS = 'UNPUBLISHED_ENTRIES_SUCCESS'; +export const UNPUBLISHED_ENTRIES_FAILURE = 'UNPUBLISHED_ENTRIES_FAILURE'; + + +/* + * Simple Action Creators (Internal) + */ +function unpublishedEntriesLoading() { + return { + type: UNPUBLISHED_ENTRIES_REQUEST + }; +} + +function unpublishedEntriesLoaded(entries, pagination) { + return { + type: UNPUBLISHED_ENTRIES_SUCCESS, + payload: { + entries: entries, + pages: pagination + } + }; +} + +function unpublishedEntriesFailed(error) { + return { + type: UNPUBLISHED_ENTRIES_FAILURE, + error: 'Failed to load entries', + payload: error.toString(), + }; +} + +/* + * Exported Thunk Action Creators + */ +export function loadUnpublishedEntries() { + return (dispatch, getState) => { + const state = getState(); + if (state.publish_mode !== EDITORIAL_WORKFLOW) return; + + const backend = currentBackend(state.config); + dispatch(unpublishedEntriesLoading()); + backend.unpublishedEntries().then( + (response) => dispatch(unpublishedEntriesLoaded(response.entries, response.pagination)), + (error) => dispatch(unpublishedEntriesFailed(error)) + ); + }; +} diff --git a/src/actions/entries.js b/src/actions/entries.js index 4f239d2f..9c9cffd6 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -17,7 +17,6 @@ export const DRAFT_CREATE_EMPTY = 'DRAFT_CREATE_EMPTY'; export const DRAFT_DISCARD = 'DRAFT_DISCARD'; export const DRAFT_CHANGE = 'DRAFT_CHANGE'; - export const ENTRY_PERSIST_REQUEST = 'ENTRY_PERSIST_REQUEST'; export const ENTRY_PERSIST_SUCCESS = 'ENTRY_PERSIST_SUCCESS'; export const ENTRY_PERSIST_FAILURE = 'ENTRY_PERSIST_FAILURE'; diff --git a/src/backends/backend.js b/src/backends/backend.js index b734108e..66fbd33c 100644 --- a/src/backends/backend.js +++ b/src/backends/backend.js @@ -3,7 +3,6 @@ import GitHubBackend from './github/implementation'; import NetlifyGitBackend from './netlify-git/implementation'; import { resolveFormat } from '../formats/formats'; import { createEntry } from '../valueObjects/Entry'; -import { SIMPLE, EDITORIAL } from './constants'; class LocalStorageAuthStore { storageKey = 'nf-cms-user'; @@ -65,9 +64,9 @@ class Backend { return this.entryWithFormat(collection)(newEntry); } - entryWithFormat(collection) { + entryWithFormat(collectionOrEntity) { return (entry) => { - const format = resolveFormat(collection, entry); + const format = resolveFormat(collectionOrEntity, entry); if (entry && entry.raw) { entry.data = format && format.fromFile(entry.raw); } @@ -75,6 +74,15 @@ class Backend { }; } + unpublishedEntries(page, perPage) { + return this.implementation.unpublishedEntries(page, perPage).then((response) => { + return { + pagination: response.pagination, + entries: response.entries.map(this.entryWithFormat('editorialWorkflow')) + }; + }); + } + slugFormatter(template, entry) { var date = new Date(); return template.replace(/\{\{([^\}]+)\}\}/g, function(_, name) { @@ -93,16 +101,6 @@ class Backend { }); } - getPublishMode(config) { - const publish_workflows = [SIMPLE, EDITORIAL]; - const mode = config.get('publish_workflow'); - if (publish_workflows.indexOf(mode) !== -1) { - return mode; - } else { - return SIMPLE; - } - } - persistEntry(config, collection, entryDraft, MediaFiles) { const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false; @@ -132,7 +130,7 @@ class Backend { collection.get('label') + ' “' + entryDraft.getIn(['entry', 'data', 'title']) + '”'; - const mode = this.getPublishMode(config); + const mode = config.get('publish_mode'); const collectionName = collection.get('name'); diff --git a/src/backends/github/API.js b/src/backends/github/API.js index 2cd4ac3d..3fe28ea5 100644 --- a/src/backends/github/API.js +++ b/src/backends/github/API.js @@ -1,7 +1,7 @@ import LocalForage from 'localforage'; import MediaProxy from '../../valueObjects/MediaProxy'; import { Base64 } from 'js-base64'; -import { EDITORIAL } from '../constants'; +import { EDITORIAL_WORKFLOW } from '../../constants/publishModes'; const API_ROOT = 'https://api.github.com'; @@ -169,7 +169,7 @@ export default class API { .then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree)) .then(changeTree => this.commit(options.commitMessage, changeTree)) .then((response) => { - if (options.mode && options.mode === EDITORIAL) { + 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) @@ -177,6 +177,7 @@ export default class API { type: 'PR', status: 'draft', branch: branchName, + collection: options.collectionName, title: options.parsedData.title, description: options.parsedData.description, objects: files.map(file => file.path) diff --git a/src/backends/github/implementation.js b/src/backends/github/implementation.js index 9cb11f45..d2dd26d1 100644 --- a/src/backends/github/implementation.js +++ b/src/backends/github/implementation.js @@ -1,5 +1,5 @@ import semaphore from 'semaphore'; -import {createEntry} from '../../valueObjects/Entry'; +import { createEntry } from '../../valueObjects/Entry'; import AuthenticationPage from './AuthenticationPage'; import API from './API'; @@ -62,4 +62,11 @@ export default class GitHub { persistEntry(entry, mediaFiles = [], options = {}) { return this.api.persistFiles(entry, mediaFiles, options); } + + unpublishedEntries() { + return Promise.resolve({ + pagination: {}, + entries: [] + }); + } } diff --git a/src/backends/netlify-git/API.js b/src/backends/netlify-git/API.js index e5e8d28c..25e288c8 100644 --- a/src/backends/netlify-git/API.js +++ b/src/backends/netlify-git/API.js @@ -1,7 +1,7 @@ import LocalForage from 'localforage'; import MediaProxy from '../../valueObjects/MediaProxy'; import { Base64 } from 'js-base64'; -import { EDITORIAL } from '../constants'; +import { EDITORIAL_WORKFLOW } from '../../constants/publishModes'; export default class API { constructor(token, url, branch) { @@ -161,7 +161,7 @@ export default class API { .then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree)) .then(changeTree => this.commit(options.commitMessage, changeTree)) .then((response) => { - if (options.mode && options.mode === EDITORIAL) { + if (options.mode && options.mode === EDITORIAL_WORKFLOW) { const contentKey = options.collectionName ? `${options.collectionName}-${entry.slug}` : entry.slug; return this.createBranch(`cms/${contentKey}`, response.sha) .then(this.storeMetadata(contentKey, { status: 'draft' })) diff --git a/src/backends/test-repo/implementation.js b/src/backends/test-repo/implementation.js index fce8d3d8..b8cf56cc 100644 --- a/src/backends/test-repo/implementation.js +++ b/src/backends/test-repo/implementation.js @@ -57,4 +57,5 @@ export default class TestRepo { mediaFiles.forEach(media => media.uploaded = true); return Promise.resolve(); } + } diff --git a/src/backends/constants.js b/src/constants/publishModes.js similarity index 50% rename from src/backends/constants.js rename to src/constants/publishModes.js index e4203a35..308f5696 100644 --- a/src/backends/constants.js +++ b/src/constants/publishModes.js @@ -1,3 +1,3 @@ // Create/edit workflows export const SIMPLE = 'simple'; -export const EDITORIAL = 'editorial'; +export const EDITORIAL_WORKFLOW = 'editorial_workflow'; diff --git a/src/containers/CollectionPage.js b/src/containers/CollectionPage.js index a04a1a21..9fe0a622 100644 --- a/src/containers/CollectionPage.js +++ b/src/containers/CollectionPage.js @@ -2,6 +2,7 @@ import React, { PropTypes } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; import { loadEntries } from '../actions/entries'; +import { loadUnpublishedEntries } from '../actions/editorialWorkflow'; import { selectEntries } from '../reducers'; import { Loader } from '../components/UI'; import EntryListing from '../components/EntryListing'; @@ -9,7 +10,7 @@ import EntryListing from '../components/EntryListing'; class DashboardPage extends React.Component { componentDidMount() { const { collection, dispatch } = this.props; - + dispatch(loadUnpublishedEntries); if (collection) { dispatch(loadEntries(collection)); } diff --git a/src/formats/formats.js b/src/formats/formats.js index 9d6f72ad..854a3505 100644 --- a/src/formats/formats.js +++ b/src/formats/formats.js @@ -1,5 +1,5 @@ import YAMLFrontmatter from './yaml-frontmatter'; -export function resolveFormat(collection, entry) { +export function resolveFormat(collectionOrEntity, entry) { return new YAMLFrontmatter(); } diff --git a/src/reducers/editorialWorkflow.js b/src/reducers/editorialWorkflow.js new file mode 100644 index 00000000..23f071f1 --- /dev/null +++ b/src/reducers/editorialWorkflow.js @@ -0,0 +1,37 @@ +import { Map, List, fromJS } from 'immutable'; +import { + UNPUBLISHED_ENTRIES_REQUEST, UNPUBLISHED_ENTRIES_SUCCESS +} from '../actions/editorialWorkflow'; + +const unpublishedEntries = (state = Map({ entities: Map(), pages: Map() }), action) => { + switch (action.type) { + 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)) + )); + map.set('pages', Map({ + ...pages, + ids: List(entries.map((entry) => entry.slug)) + })); + }); + default: + return state; + } +}; + +export const selectUnpublishedEntry = (state, status, slug) => ( + state.getIn(['entities', `${status}.${slug}`], null) +); + +export const selectUnpublishedEntries = (state, status) => { + const slugs = state.getIn(['pages', 'ids']); + return slugs && slugs.map((slug) => selectUnpublishedEntry(state, status, slug)); +}; + + +export default unpublishedEntries; diff --git a/src/reducers/entries.js b/src/reducers/entries.js index 70e73cd2..6ae5e554 100644 --- a/src/reducers/entries.js +++ b/src/reducers/entries.js @@ -7,13 +7,16 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { switch (action.type) { case ENTRY_REQUEST: return state.setIn(['entities', `${action.payload.collection}.${action.payload.slug}`, 'isFetching'], true); + case ENTRY_SUCCESS: return state.setIn( ['entities', `${action.payload.collection}.${action.payload.entry.slug}`], fromJS(action.payload.entry) ); + case ENTRIES_REQUEST: return state.setIn(['pages', action.payload.collection, 'isFetching'], true); + case ENTRIES_SUCCESS: const { collection, entries, pages } = action.payload; return state.withMutations((map) => { @@ -25,6 +28,7 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { ids: List(entries.map((entry) => entry.slug)) })); }); + default: return state; } diff --git a/src/reducers/index.js b/src/reducers/index.js index f7199111..62615bdc 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -2,6 +2,7 @@ import auth from './auth'; import config from './config'; import editor from './editor'; import entries, * as fromEntries from './entries'; +import editorialWorkflow, * as fromEditorialWorkflow from './editorialWorkflow'; import entryDraft from './entryDraft'; import collections from './collections'; import medias, * as fromMedias from './medias'; @@ -12,18 +13,27 @@ const reducers = { collections, editor, entries, + editorialWorkflow, entryDraft, medias }; export default reducers; +/* + * Selectors + */ export const selectEntry = (state, collection, slug) => fromEntries.selectEntry(state.entries, collection, slug); - export const selectEntries = (state, collection) => fromEntries.selectEntries(state.entries, collection); +export const selectUnpublishedEntry = (state, status, slug) => + fromEditorialWorkflow.selectUnpublishedEntry(state.editorialWorkflow, status, slug); + +export const selectUnpublishedEntries = (state, status) => + fromEditorialWorkflow.selectUnpublishedEntries(state.editorialWorkflow, status); + export const getMedia = (state, path) => fromMedias.getMedia(state.medias, path);