Editorial workflow Drag'nDrop
This commit is contained in:
parent
e2b6e140f3
commit
0b447d483d
@ -86,8 +86,10 @@
|
||||
"markup-it": "git+https://github.com/cassiozen/markup-it.git",
|
||||
"pluralize": "^3.0.0",
|
||||
"prismjs": "^1.5.1",
|
||||
"react-datetime": "^2.6.0",
|
||||
"react-addons-css-transition-group": "^15.3.1",
|
||||
"react-datetime": "^2.6.0",
|
||||
"react-dnd": "^2.1.4",
|
||||
"react-dnd-html5-backend": "^2.1.2",
|
||||
"react-portal": "^2.2.1",
|
||||
"selection-position": "^1.0.0",
|
||||
"semaphore": "^1.0.5",
|
||||
|
@ -14,6 +14,9 @@ export const UNPUBLISHED_ENTRIES_FAILURE = 'UNPUBLISHED_ENTRIES_FAILURE';
|
||||
export const UNPUBLISHED_ENTRY_PERSIST_REQUEST = 'UNPUBLISHED_ENTRY_PERSIST_REQUEST';
|
||||
export const UNPUBLISHED_ENTRY_PERSIST_SUCCESS = 'UNPUBLISHED_ENTRY_PERSIST_SUCCESS';
|
||||
|
||||
export const UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST = 'UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST';
|
||||
export const UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS = 'UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS';
|
||||
|
||||
/*
|
||||
* Simple Action Creators (Internal)
|
||||
*/
|
||||
@ -57,24 +60,39 @@ function unpublishedEntriesFailed(error) {
|
||||
}
|
||||
|
||||
|
||||
function unpublishedEntryPersisting(status, entry) {
|
||||
function unpublishedEntryPersisting(entry) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PERSIST_REQUEST,
|
||||
payload: { status, entry }
|
||||
payload: { entry }
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPersisted(status, entry) {
|
||||
function unpublishedEntryPersisted(entry) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PERSIST_SUCCESS,
|
||||
payload: { status, entry }
|
||||
payload: { entry }
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryPersistedFail(status, entry) {
|
||||
function unpublishedEntryPersistedFail(error) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_PERSIST_SUCCESS,
|
||||
payload: { status, entry }
|
||||
payload: { error }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function unpublishedEntryStatusChangeRequest(collection, slug, oldStatus, newStatus) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_STATUS_CHANGE_REQUEST,
|
||||
payload: { collection, slug, oldStatus, newStatus }
|
||||
};
|
||||
}
|
||||
|
||||
function unpublishedEntryStatusChangePersisted(collection, slug, oldStatus, newStatus) {
|
||||
return {
|
||||
type: UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS,
|
||||
payload: { collection, slug, oldStatus, newStatus }
|
||||
};
|
||||
}
|
||||
|
||||
@ -105,17 +123,29 @@ export function loadUnpublishedEntries() {
|
||||
};
|
||||
}
|
||||
|
||||
export function persistUnpublishedEntry(collection, status, entry) {
|
||||
export function persistUnpublishedEntry(collection, entry) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
const MediaProxies = entry && entry.get('mediaFiles').map(path => getMedia(state, path));
|
||||
dispatch(unpublishedEntryPersisting(status, entry));
|
||||
backend.persistUnpublishedEntry(state.config, collection, status, entry, MediaProxies.toJS()).then(
|
||||
dispatch(unpublishedEntryPersisting(entry));
|
||||
backend.persistUnpublishedEntry(state.config, collection, entry, MediaProxies.toJS()).then(
|
||||
() => {
|
||||
dispatch(unpublishedEntryPersisted(status, entry));
|
||||
dispatch(unpublishedEntryPersisted(entry));
|
||||
},
|
||||
(error) => dispatch(unpublishedEntryPersistedFail(error))
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function updateUnpublishedEntryStatus(collection, slug, oldStatus, newStatus) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const backend = currentBackend(state.config);
|
||||
dispatch(unpublishedEntryStatusChangeRequest(collection, slug, oldStatus, newStatus));
|
||||
backend.updateUnpublishedEntryStatus(collection, slug, newStatus)
|
||||
.then(() => {
|
||||
dispatch(unpublishedEntryStatusChangePersisted(collection, slug, oldStatus, newStatus));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -143,8 +143,12 @@ class Backend {
|
||||
});
|
||||
}
|
||||
|
||||
persistUnpublishedEntry(config, collection, status, entryDraft, MediaFiles) {
|
||||
return this.persistEntry(config, collection, entryDraft, MediaFiles, { unpublished: true, status });
|
||||
persistUnpublishedEntry(config, collection, entryDraft, MediaFiles) {
|
||||
return this.persistEntry(config, collection, entryDraft, MediaFiles, { unpublished: true });
|
||||
}
|
||||
|
||||
updateUnpublishedEntryStatus(collection, slug, newStatus) {
|
||||
return this.implementation.updateUnpublishedEntryStatus(collection, slug, newStatus);
|
||||
}
|
||||
|
||||
|
||||
|
@ -7,8 +7,6 @@ import { SIMPLE, EDITORIAL_WORKFLOW, status } from '../../constants/publishModes
|
||||
const API_ROOT = 'https://api.github.com';
|
||||
|
||||
export default class API {
|
||||
|
||||
|
||||
constructor(token, repo, branch) {
|
||||
this.token = token;
|
||||
this.repo = repo;
|
||||
@ -99,17 +97,28 @@ export default class API {
|
||||
return this.uploadBlob(fileTree[`${key}.json`])
|
||||
.then(item => this.updateTree(branchData.sha, '/', fileTree))
|
||||
.then(changeTree => this.commit(`Updating “${key}” metadata`, changeTree))
|
||||
.then(response => this.patchRef('meta', '_netlify_cms', response.sha));
|
||||
.then(response => this.patchRef('meta', '_netlify_cms', response.sha))
|
||||
.then(() => {
|
||||
LocalForage.setItem(`gh.meta.${key}`, {
|
||||
expires: Date.now() + 300000, // In 5 minutes
|
||||
data
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
retrieveMetadata(key) {
|
||||
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));
|
||||
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));
|
||||
});
|
||||
}
|
||||
|
||||
readFile(path, sha, branch = this.branch) {
|
||||
@ -137,26 +146,14 @@ export default class API {
|
||||
}
|
||||
|
||||
readUnpublishedBranchFile(contentKey) {
|
||||
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;
|
||||
});
|
||||
let metaData;
|
||||
return this.retrieveMetadata(contentKey)
|
||||
.then(data => {
|
||||
metaData = data;
|
||||
return this.readFile(data.objects.entry, null, data.branch);
|
||||
})
|
||||
.then(file => {
|
||||
return { metaData, file };
|
||||
});
|
||||
}
|
||||
|
||||
@ -248,7 +245,6 @@ export default class API {
|
||||
|
||||
return {
|
||||
...metadata,
|
||||
status: options.status,
|
||||
title: options.parsedData && options.parsedData.title,
|
||||
description: options.parsedData && options.parsedData.description,
|
||||
objects: {
|
||||
@ -264,6 +260,18 @@ export default class API {
|
||||
}
|
||||
}
|
||||
|
||||
updateUnpublishedEntryStatus(collection, slug, status) {
|
||||
const contentKey = collection ? `${collection}-${slug}` : slug;
|
||||
return this.retrieveMetadata(contentKey)
|
||||
.then(metadata => {
|
||||
return {
|
||||
...metadata,
|
||||
status
|
||||
};
|
||||
})
|
||||
.then(updatedMetadata => this.storeMetadata(contentKey, updatedMetadata));
|
||||
}
|
||||
|
||||
createRef(type, name, sha) {
|
||||
return this.request(`${this.repoURL}/git/refs`, {
|
||||
method: 'POST',
|
||||
|
@ -98,4 +98,8 @@ export default class GitHub {
|
||||
))[0]
|
||||
));
|
||||
}
|
||||
|
||||
updateUnpublishedEntryStatus(collection, slug, newStatus) {
|
||||
return this.api.updateUnpublishedEntryStatus(collection, slug, newStatus);
|
||||
}
|
||||
}
|
||||
|
@ -1,16 +1,25 @@
|
||||
.container {
|
||||
display: table;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.column {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
width: 28%;
|
||||
width: 33%;
|
||||
height: 100%;
|
||||
transition: background-color .5s ease;
|
||||
& h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.highlighted {
|
||||
background-color: #e1eeea;
|
||||
}
|
||||
|
||||
.column:not(:last-child) {
|
||||
margin-right: 8%;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
.card {
|
||||
@ -30,3 +39,10 @@
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.clear::after {
|
||||
content:"";
|
||||
display:block;
|
||||
clear:both;
|
||||
}
|
||||
|
@ -1,4 +1,6 @@
|
||||
import React from 'react';
|
||||
import React, { PropTypes } from 'react';
|
||||
import { DragDropContext, DragSource, DropTarget } from 'react-dnd';
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import moment from 'moment';
|
||||
import { Card } from './UI';
|
||||
@ -6,16 +8,93 @@ import { Link } from 'react-router';
|
||||
import { statusDescriptions } from '../constants/publishModes';
|
||||
import styles from './UnpublishedListing.css';
|
||||
|
||||
export default class UnpublishedListing extends React.Component {
|
||||
const CARD = 'card';
|
||||
|
||||
/*
|
||||
* Column DropTarget Component
|
||||
*/
|
||||
function Column({ connectDropTarget, status, isOver, children }) {
|
||||
const className = isOver ? `${styles.column} ${styles.highlighted}` : styles.column;
|
||||
return connectDropTarget(
|
||||
<div className={className}>
|
||||
<h2>{statusDescriptions.get(status)}</h2>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const columnTargetSpec = {
|
||||
drop(props, monitor) {
|
||||
const slug = monitor.getItem().slug;
|
||||
const collection = monitor.getItem().collection;
|
||||
const oldStatus = monitor.getItem().currentStatus;
|
||||
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 }) {
|
||||
return connectDragSource(
|
||||
<div>
|
||||
<Card className={styles.card}>
|
||||
{children}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const cardDragSpec = {
|
||||
beginDrag(props) {
|
||||
return {
|
||||
slug: props.slug,
|
||||
collection: props.collection,
|
||||
currentStatus: props.status
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
function cardCollect(connect, monitor) {
|
||||
return {
|
||||
connectDragSource: connect.dragSource()
|
||||
};
|
||||
}
|
||||
|
||||
EntryCard = DragSource(CARD, cardDragSpec, cardCollect)(EntryCard);
|
||||
|
||||
/*
|
||||
* The actual exported component implementation
|
||||
*/
|
||||
class UnpublishedListing extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.renderColumns = this.renderColumns.bind(this);
|
||||
}
|
||||
|
||||
renderColumns(entries, column) {
|
||||
if (!entries) return;
|
||||
|
||||
if (!column) {
|
||||
return entries.entrySeq().map(([currColumn, currEntries]) => (
|
||||
<div key={currColumn} className={styles.column}>
|
||||
<h2>{statusDescriptions.get(currColumn)}</h2>
|
||||
<Column
|
||||
key={currColumn}
|
||||
status={currColumn}
|
||||
onChangeStatus={this.props.handleChangeStatus}
|
||||
>
|
||||
{this.renderColumns(currEntries, currColumn)}
|
||||
</div>
|
||||
</Column>
|
||||
));
|
||||
} else {
|
||||
return <div>
|
||||
@ -25,10 +104,15 @@ export default class UnpublishedListing extends React.Component {
|
||||
const timeStamp = moment(entry.getIn(['metaData', 'timeStamp'])).format('llll');
|
||||
const link = `/editorialworkflow/${entry.getIn(['metaData', 'collection'])}/${entry.getIn(['metaData', 'status'])}/${entry.get('slug')}`;
|
||||
return (
|
||||
<Card key={entry.get('slug')} className={styles.card}>
|
||||
<EntryCard
|
||||
key={entry.get('slug')}
|
||||
slug={entry.get('slug')}
|
||||
status={entry.getIn(['metaData', 'status'])}
|
||||
collection={entry.getIn(['metaData', 'collection'])}
|
||||
>
|
||||
<h1><Link to={link}>{entry.getIn(['data', 'title'])}</Link> <small>by {author}</small></h1>
|
||||
<p>Last updated: {timeStamp} by {entry.getIn(['metaData', 'user'])}</p>
|
||||
</Card>
|
||||
</EntryCard>
|
||||
);
|
||||
}
|
||||
)}
|
||||
@ -40,9 +124,11 @@ export default class UnpublishedListing extends React.Component {
|
||||
const columns = this.renderColumns(this.props.entries);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.clear}>
|
||||
<h1>Editorial Workflow</h1>
|
||||
<div className={styles.container}>
|
||||
{columns}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -50,4 +136,7 @@ export default class UnpublishedListing extends React.Component {
|
||||
|
||||
UnpublishedListing.propTypes = {
|
||||
entries: ImmutablePropTypes.orderedMap,
|
||||
handleChangeStatus: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default DragDropContext(HTML5Backend)(UnpublishedListing);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { PropTypes } from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import { OrderedMap } from 'immutable';
|
||||
import { loadUnpublishedEntries } from '../../actions/editorialWorkflow';
|
||||
import { loadUnpublishedEntries, updateUnpublishedEntryStatus } from '../../actions/editorialWorkflow';
|
||||
import { selectUnpublishedEntries } from '../../reducers';
|
||||
import { EDITORIAL_WORKFLOW, status } from '../../constants/publishModes';
|
||||
import UnpublishedListing from '../../components/UnpublishedListing';
|
||||
@ -19,13 +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;
|
||||
if (!isEditorialWorkflow) return super.render();
|
||||
|
||||
return (
|
||||
<div className={styles.alignable}>
|
||||
<UnpublishedListing entries={unpublishedEntries}/>
|
||||
<UnpublishedListing entries={unpublishedEntries} handleChangeStatus={this.handleChangeStatus.bind(this)} />
|
||||
{super.render()}
|
||||
</div>
|
||||
);
|
||||
@ -56,5 +60,5 @@ export default function CollectionPageHOC(CollectionPage) {
|
||||
return returnObj;
|
||||
}
|
||||
|
||||
return connect(mapStateToProps)(CollectionPageHOC);
|
||||
return connect(mapStateToProps, { updateUnpublishedEntryStatus })(CollectionPageHOC);
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ export default function EntryPageHOC(EntryPage) {
|
||||
};
|
||||
|
||||
returnObj.persistEntry = (collection, entryDraft) => {
|
||||
dispatch(persistUnpublishedEntry(collection, status, entryDraft));
|
||||
dispatch(persistUnpublishedEntry(collection, entryDraft));
|
||||
};
|
||||
}
|
||||
return returnObj;
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { Map, List, fromJS } from 'immutable';
|
||||
import { EDITORIAL_WORKFLOW } from '../constants/publishModes';
|
||||
import {
|
||||
UNPUBLISHED_ENTRY_REQUEST, UNPUBLISHED_ENTRY_SUCCESS, UNPUBLISHED_ENTRIES_REQUEST, UNPUBLISHED_ENTRIES_SUCCESS
|
||||
UNPUBLISHED_ENTRY_REQUEST,
|
||||
UNPUBLISHED_ENTRY_SUCCESS,
|
||||
UNPUBLISHED_ENTRIES_REQUEST,
|
||||
UNPUBLISHED_ENTRIES_SUCCESS,
|
||||
UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS
|
||||
} from '../actions/editorialWorkflow';
|
||||
import { CONFIG_SUCCESS } from '../actions/config';
|
||||
|
||||
@ -39,6 +43,21 @@ const unpublishedEntries = (state = null, action) => {
|
||||
ids: List(entries.map((entry) => entry.slug))
|
||||
}));
|
||||
});
|
||||
|
||||
case UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS:
|
||||
const { slug, oldStatus, newStatus } = action.payload;
|
||||
return state.withMutations((map) => {
|
||||
const entry = map.getIn(['entities', `${oldStatus}.${slug}`]);
|
||||
|
||||
let entities = map.get('entities').filter((val, key) => (
|
||||
key !== `${oldStatus}.${slug}`
|
||||
));
|
||||
|
||||
entities = entities.set(`${newStatus}.${slug}`, entry);
|
||||
|
||||
map.set('entities', entities);
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user