Editorial Workflow skeleton
This commit is contained in:
parent
b0e62d1ca9
commit
f0e608a209
@ -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)) {
|
||||||
|
53
src/actions/editorialWorkflow.js
Normal file
53
src/actions/editorialWorkflow.js
Normal 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))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
@ -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';
|
||||||
|
@ -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');
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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: []
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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' }))
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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';
|
@ -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));
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
37
src/reducers/editorialWorkflow.js
Normal file
37
src/reducers/editorialWorkflow.js
Normal 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;
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user