Editorial Workflow skeleton

This commit is contained in:
Cássio Zen 2016-09-06 13:04:17 -03:00
parent b0e62d1ca9
commit f0e608a209
14 changed files with 139 additions and 26 deletions

View File

@ -1,6 +1,8 @@
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import _ from 'lodash';
import { currentBackend } from '../backends/backend'; import { currentBackend } from '../backends/backend';
import { authenticate } from '../actions/auth'; import { authenticate } from '../actions/auth';
import * as publishModes from '../constants/publishModes';
import * as MediaProxy from '../valueObjects/MediaProxy'; import * as MediaProxy from '../valueObjects/MediaProxy';
export const CONFIG_REQUEST = 'CONFIG_REQUEST'; 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 // Make sure there is a publish workflow mode set
config['publish_workflow'] = 'simple'; config.publish_mode = publishModes.SIMPLE;
} }
if (!('public_folder' in config)) { if (!('public_folder' in config)) {

View File

@ -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))
);
};
}

View File

@ -17,7 +17,6 @@ export const DRAFT_CREATE_EMPTY = 'DRAFT_CREATE_EMPTY';
export const DRAFT_DISCARD = 'DRAFT_DISCARD'; export const DRAFT_DISCARD = 'DRAFT_DISCARD';
export const DRAFT_CHANGE = 'DRAFT_CHANGE'; export const DRAFT_CHANGE = 'DRAFT_CHANGE';
export const ENTRY_PERSIST_REQUEST = 'ENTRY_PERSIST_REQUEST'; export const ENTRY_PERSIST_REQUEST = 'ENTRY_PERSIST_REQUEST';
export const ENTRY_PERSIST_SUCCESS = 'ENTRY_PERSIST_SUCCESS'; export const ENTRY_PERSIST_SUCCESS = 'ENTRY_PERSIST_SUCCESS';
export const ENTRY_PERSIST_FAILURE = 'ENTRY_PERSIST_FAILURE'; export const ENTRY_PERSIST_FAILURE = 'ENTRY_PERSIST_FAILURE';

View File

@ -3,7 +3,6 @@ import GitHubBackend from './github/implementation';
import NetlifyGitBackend from './netlify-git/implementation'; import NetlifyGitBackend from './netlify-git/implementation';
import { resolveFormat } from '../formats/formats'; import { resolveFormat } from '../formats/formats';
import { createEntry } from '../valueObjects/Entry'; import { createEntry } from '../valueObjects/Entry';
import { SIMPLE, EDITORIAL } from './constants';
class LocalStorageAuthStore { class LocalStorageAuthStore {
storageKey = 'nf-cms-user'; storageKey = 'nf-cms-user';
@ -65,9 +64,9 @@ class Backend {
return this.entryWithFormat(collection)(newEntry); return this.entryWithFormat(collection)(newEntry);
} }
entryWithFormat(collection) { entryWithFormat(collectionOrEntity) {
return (entry) => { return (entry) => {
const format = resolveFormat(collection, entry); const format = resolveFormat(collectionOrEntity, entry);
if (entry && entry.raw) { if (entry && entry.raw) {
entry.data = format && format.fromFile(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) { slugFormatter(template, entry) {
var date = new Date(); var date = new Date();
return template.replace(/\{\{([^\}]+)\}\}/g, function(_, name) { 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) { persistEntry(config, collection, entryDraft, MediaFiles) {
const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false; const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false;
@ -132,7 +130,7 @@ class Backend {
collection.get('label') + ' “' + collection.get('label') + ' “' +
entryDraft.getIn(['entry', 'data', 'title']) + '”'; entryDraft.getIn(['entry', 'data', 'title']) + '”';
const mode = this.getPublishMode(config); const mode = config.get('publish_mode');
const collectionName = collection.get('name'); const collectionName = collection.get('name');

View File

@ -1,7 +1,7 @@
import LocalForage from 'localforage'; import LocalForage from 'localforage';
import MediaProxy from '../../valueObjects/MediaProxy'; import MediaProxy from '../../valueObjects/MediaProxy';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import { EDITORIAL } from '../constants'; import { EDITORIAL_WORKFLOW } from '../../constants/publishModes';
const API_ROOT = 'https://api.github.com'; const API_ROOT = 'https://api.github.com';
@ -169,7 +169,7 @@ export default class API {
.then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree)) .then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree))
.then(changeTree => this.commit(options.commitMessage, changeTree)) .then(changeTree => this.commit(options.commitMessage, changeTree))
.then((response) => { .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 contentKey = options.collectionName ? `${options.collectionName}-${entry.slug}` : entry.slug;
const branchName = `cms/${contentKey}`; const branchName = `cms/${contentKey}`;
return this.createBranch(branchName, response.sha) return this.createBranch(branchName, response.sha)
@ -177,6 +177,7 @@ export default class API {
type: 'PR', type: 'PR',
status: 'draft', status: 'draft',
branch: branchName, branch: branchName,
collection: options.collectionName,
title: options.parsedData.title, title: options.parsedData.title,
description: options.parsedData.description, description: options.parsedData.description,
objects: files.map(file => file.path) objects: files.map(file => file.path)

View File

@ -1,5 +1,5 @@
import semaphore from 'semaphore'; import semaphore from 'semaphore';
import {createEntry} from '../../valueObjects/Entry'; import { createEntry } from '../../valueObjects/Entry';
import AuthenticationPage from './AuthenticationPage'; import AuthenticationPage from './AuthenticationPage';
import API from './API'; import API from './API';
@ -62,4 +62,11 @@ export default class GitHub {
persistEntry(entry, mediaFiles = [], options = {}) { persistEntry(entry, mediaFiles = [], options = {}) {
return this.api.persistFiles(entry, mediaFiles, options); return this.api.persistFiles(entry, mediaFiles, options);
} }
unpublishedEntries() {
return Promise.resolve({
pagination: {},
entries: []
});
}
} }

View File

@ -1,7 +1,7 @@
import LocalForage from 'localforage'; import LocalForage from 'localforage';
import MediaProxy from '../../valueObjects/MediaProxy'; import MediaProxy from '../../valueObjects/MediaProxy';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import { EDITORIAL } from '../constants'; import { EDITORIAL_WORKFLOW } from '../../constants/publishModes';
export default class API { export default class API {
constructor(token, url, branch) { constructor(token, url, branch) {
@ -161,7 +161,7 @@ export default class API {
.then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree)) .then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree))
.then(changeTree => this.commit(options.commitMessage, changeTree)) .then(changeTree => this.commit(options.commitMessage, changeTree))
.then((response) => { .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 contentKey = options.collectionName ? `${options.collectionName}-${entry.slug}` : entry.slug;
return this.createBranch(`cms/${contentKey}`, response.sha) return this.createBranch(`cms/${contentKey}`, response.sha)
.then(this.storeMetadata(contentKey, { status: 'draft' })) .then(this.storeMetadata(contentKey, { status: 'draft' }))

View File

@ -57,4 +57,5 @@ export default class TestRepo {
mediaFiles.forEach(media => media.uploaded = true); mediaFiles.forEach(media => media.uploaded = true);
return Promise.resolve(); return Promise.resolve();
} }
} }

View File

@ -1,3 +1,3 @@
// Create/edit workflows // Create/edit workflows
export const SIMPLE = 'simple'; export const SIMPLE = 'simple';
export const EDITORIAL = 'editorial'; export const EDITORIAL_WORKFLOW = 'editorial_workflow';

View File

@ -2,6 +2,7 @@ import React, { PropTypes } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { loadEntries } from '../actions/entries'; import { loadEntries } from '../actions/entries';
import { loadUnpublishedEntries } from '../actions/editorialWorkflow';
import { selectEntries } from '../reducers'; import { selectEntries } from '../reducers';
import { Loader } from '../components/UI'; import { Loader } from '../components/UI';
import EntryListing from '../components/EntryListing'; import EntryListing from '../components/EntryListing';
@ -9,7 +10,7 @@ import EntryListing from '../components/EntryListing';
class DashboardPage extends React.Component { class DashboardPage extends React.Component {
componentDidMount() { componentDidMount() {
const { collection, dispatch } = this.props; const { collection, dispatch } = this.props;
dispatch(loadUnpublishedEntries);
if (collection) { if (collection) {
dispatch(loadEntries(collection)); dispatch(loadEntries(collection));
} }

View File

@ -1,5 +1,5 @@
import YAMLFrontmatter from './yaml-frontmatter'; import YAMLFrontmatter from './yaml-frontmatter';
export function resolveFormat(collection, entry) { export function resolveFormat(collectionOrEntity, entry) {
return new YAMLFrontmatter(); return new YAMLFrontmatter();
} }

View File

@ -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;

View File

@ -7,13 +7,16 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => {
switch (action.type) { switch (action.type) {
case ENTRY_REQUEST: case ENTRY_REQUEST:
return state.setIn(['entities', `${action.payload.collection}.${action.payload.slug}`, 'isFetching'], true); return state.setIn(['entities', `${action.payload.collection}.${action.payload.slug}`, 'isFetching'], true);
case ENTRY_SUCCESS: case ENTRY_SUCCESS:
return state.setIn( return state.setIn(
['entities', `${action.payload.collection}.${action.payload.entry.slug}`], ['entities', `${action.payload.collection}.${action.payload.entry.slug}`],
fromJS(action.payload.entry) fromJS(action.payload.entry)
); );
case ENTRIES_REQUEST: case ENTRIES_REQUEST:
return state.setIn(['pages', action.payload.collection, 'isFetching'], true); return state.setIn(['pages', action.payload.collection, 'isFetching'], true);
case ENTRIES_SUCCESS: case ENTRIES_SUCCESS:
const { collection, entries, pages } = action.payload; const { collection, entries, pages } = action.payload;
return state.withMutations((map) => { return state.withMutations((map) => {
@ -25,6 +28,7 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => {
ids: List(entries.map((entry) => entry.slug)) ids: List(entries.map((entry) => entry.slug))
})); }));
}); });
default: default:
return state; return state;
} }

View File

@ -2,6 +2,7 @@ import auth from './auth';
import config from './config'; import config from './config';
import editor from './editor'; import editor from './editor';
import entries, * as fromEntries from './entries'; import entries, * as fromEntries from './entries';
import editorialWorkflow, * as fromEditorialWorkflow from './editorialWorkflow';
import entryDraft from './entryDraft'; import entryDraft from './entryDraft';
import collections from './collections'; import collections from './collections';
import medias, * as fromMedias from './medias'; import medias, * as fromMedias from './medias';
@ -12,18 +13,27 @@ const reducers = {
collections, collections,
editor, editor,
entries, entries,
editorialWorkflow,
entryDraft, entryDraft,
medias medias
}; };
export default reducers; export default reducers;
/*
* Selectors
*/
export const selectEntry = (state, collection, slug) => export const selectEntry = (state, collection, slug) =>
fromEntries.selectEntry(state.entries, collection, slug); fromEntries.selectEntry(state.entries, collection, slug);
export const selectEntries = (state, collection) => export const selectEntries = (state, collection) =>
fromEntries.selectEntries(state.entries, 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) => export const getMedia = (state, path) =>
fromMedias.getMedia(state.medias, path); fromMedias.getMedia(state.medias, path);