diff --git a/package.json b/package.json
index dd503d0d..312193cf 100644
--- a/package.json
+++ b/package.json
@@ -9,8 +9,18 @@
"test:watch": "npm test -- --watch",
"build": "webpack --config webpack.config.js",
"storybook": "start-storybook -p 9001",
- "storybook-build": "build-storybook -o dist"
+ "storybook-build": "build-storybook -o dist",
+ "lint": "eslint .",
+ "lint:fix": "npm run lint -- --fix",
+ "lint:staged": "lint-staged"
},
+ "lint-staged": {
+ "*.@(js|jsx)": [
+ "eslint --fix",
+ "git add"
+ ]
+ },
+ "pre-commit": "lint:staged",
"keywords": [
"netlify",
"cms"
@@ -21,7 +31,7 @@
"@kadira/storybook": "^1.36.0",
"autoprefixer": "^6.3.3",
"babel-core": "^6.5.1",
- "babel-eslint": "^4.1.8",
+ "babel-eslint": "^6.1.2",
"babel-loader": "^6.2.2",
"babel-plugin-lodash": "^3.2.0",
"babel-plugin-transform-class-properties": "^6.5.2",
@@ -32,20 +42,21 @@
"babel-register": "^6.5.2",
"babel-runtime": "^6.5.0",
"css-loader": "^0.23.1",
- "eslint": "^1.10.3",
- "eslint-loader": "^1.2.1",
+ "eslint": "^3.5.0",
"eslint-plugin-react": "^5.1.1",
"exports-loader": "^0.6.3",
"file-loader": "^0.8.5",
"immutable": "^3.7.6",
"imports-loader": "^0.6.5",
"js-yaml": "^3.5.3",
+ "lint-staged": "^3.0.2",
"mocha": "^2.4.5",
"moment": "^2.11.2",
"normalizr": "^2.0.0",
"postcss-cssnext": "^2.7.0",
"postcss-import": "^8.1.2",
"postcss-loader": "^0.9.1",
+ "pre-commit": "^1.1.3",
"react": "^15.1.0",
"react-dom": "^15.1.0",
"react-hot-loader": "^3.0.0-beta.2",
@@ -75,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",
diff --git a/src/actions/editorialWorkflow.js b/src/actions/editorialWorkflow.js
index 32322840..9b1cb70b 100644
--- a/src/actions/editorialWorkflow.js
+++ b/src/actions/editorialWorkflow.js
@@ -1,4 +1,5 @@
import { currentBackend } from '../backends/backend';
+import { getMedia } from '../reducers';
import { EDITORIAL_WORKFLOW } from '../constants/publishModes';
/*
* Contant Declarations
@@ -10,6 +11,14 @@ export const UNPUBLISHED_ENTRIES_REQUEST = 'UNPUBLISHED_ENTRIES_REQUEST';
export const UNPUBLISHED_ENTRIES_SUCCESS = 'UNPUBLISHED_ENTRIES_SUCCESS';
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';
+
+export const UNPUBLISHED_ENTRY_PUBLISH_REQUEST = 'UNPUBLISHED_ENTRY_PUBLISH_REQUEST';
+export const UNPUBLISHED_ENTRY_PUBLISH_SUCCESS = 'UNPUBLISHED_ENTRY_PUBLISH_SUCCESS';
/*
* Simple Action Creators (Internal)
@@ -53,6 +62,56 @@ function unpublishedEntriesFailed(error) {
};
}
+
+function unpublishedEntryPersisting(entry) {
+ return {
+ type: UNPUBLISHED_ENTRY_PERSIST_REQUEST,
+ payload: { entry }
+ };
+}
+
+function unpublishedEntryPersisted(entry) {
+ return {
+ type: UNPUBLISHED_ENTRY_PERSIST_SUCCESS,
+ payload: { entry }
+ };
+}
+
+function unpublishedEntryPersistedFail(error) {
+ return {
+ type: UNPUBLISHED_ENTRY_PERSIST_SUCCESS,
+ 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 }
+ };
+}
+
+function unpublishedEntryPublishRequest(collection, slug, status) {
+ return {
+ type: UNPUBLISHED_ENTRY_PUBLISH_REQUEST,
+ payload: { collection, slug, status }
+ };
+}
+
+function unpublishedEntryPublished(collection, slug, status) {
+ return {
+ type: UNPUBLISHED_ENTRY_PUBLISH_SUCCESS,
+ payload: { collection, slug, status }
+ };
+}
+
/*
* Exported Thunk Action Creators
*/
@@ -79,3 +138,42 @@ export function loadUnpublishedEntries() {
);
};
}
+
+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(entry));
+ backend.persistUnpublishedEntry(state.config, collection, entry, MediaProxies.toJS()).then(
+ () => {
+ 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));
+ });
+ };
+}
+
+export function publishUnpublishedEntry(collection, slug, status) {
+ return (dispatch, getState) => {
+ const state = getState();
+ const backend = currentBackend(state.config);
+ dispatch(unpublishedEntryPublishRequest(collection, slug, status));
+ backend.publishUnpublishedEntry(collection, slug, status)
+ .then(() => {
+ dispatch(unpublishedEntryPublished(collection, slug, status));
+ });
+ };
+}
diff --git a/src/backends/backend.js b/src/backends/backend.js
index c2c4d100..1cb9bc42 100644
--- a/src/backends/backend.js
+++ b/src/backends/backend.js
@@ -105,7 +105,7 @@ class Backend {
});
}
- persistEntry(config, collection, entryDraft, MediaFiles) {
+ persistEntry(config, collection, entryDraft, MediaFiles, options) {
const newEntry = entryDraft.getIn(['entry', 'newRecord']) || false;
const parsedData = {
@@ -139,10 +139,23 @@ class Backend {
const collectionName = collection.get('name');
return this.implementation.persistEntry(entryObj, MediaFiles, {
- newEntry, parsedData, commitMessage, collectionName, mode
+ newEntry, parsedData, commitMessage, collectionName, mode, ...options
});
}
+ 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);
+ }
+
+ publishUnpublishedEntry(collection, slug, status) {
+ return this.implementation.publishUnpublishedEntry(collection, slug, status);
+ }
+
+
entryToRaw(collection, entry) {
const format = resolveFormat(collection, entry);
return format && format.toFile(entry);
diff --git a/src/backends/github/API.js b/src/backends/github/API.js
index 210a229f..70770aed 100644
--- a/src/backends/github/API.js
+++ b/src/backends/github/API.js
@@ -1,13 +1,12 @@
import LocalForage from 'localforage';
import MediaProxy from '../../valueObjects/MediaProxy';
import { Base64 } from 'js-base64';
-import { EDITORIAL_WORKFLOW, status } from '../../constants/publishModes';
+import _ from 'lodash';
+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;
@@ -54,7 +53,8 @@ export default class API {
const headers = this.requestHeaders(options.headers || {});
const url = this.urlFor(path, options);
return fetch(url, { ...options, headers: headers }).then((response) => {
- if (response.headers.get('Content-Type').match(/json/)) {
+ const contentType = response.headers.get('Content-Type');
+ if (contentType && contentType.match(/json/)) {
return this.parseJsonResponse(response);
}
@@ -98,17 +98,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) {
@@ -136,26 +147,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 };
});
}
@@ -183,37 +182,111 @@ export default class API {
subtree[filename] = file;
file.file = true;
});
- return Promise.all(uploadPromises)
- .then(() => this.getBranch())
+ return Promise.all(uploadPromises).then(() => {
+ if (!options.mode || (options.mode && options.mode === SIMPLE)) {
+ return this.getBranch()
+ .then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree))
+ .then(changeTree => this.commit(options.commitMessage, changeTree))
+ .then(response => this.patchBranch(this.branch, response.sha));
+ } else if (options.mode && options.mode === EDITORIAL_WORKFLOW) {
+ const mediaFilesList = mediaFiles.map(file => file.path);
+ return this.editorialWorkflowGit(fileTree, entry, mediaFilesList, options);
+ }
+ });
+ }
+
+ editorialWorkflowGit(fileTree, entry, filesList, options) {
+ const contentKey = options.collectionName ? `${options.collectionName}-${entry.slug}` : entry.slug;
+ const branchName = `cms/${contentKey}`;
+ const unpublished = options.unpublished || false;
+
+ if (!unpublished) {
+ // Open new editorial review workflow for this entry - Create new metadata and commit to new branch
+ const contentKey = options.collectionName ? `${options.collectionName}-${entry.slug}` : entry.slug;
+ const branchName = `cms/${contentKey}`;
+
+ return this.getBranch()
+ .then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree))
+ .then(changeTree => this.commit(options.commitMessage, changeTree))
+ .then(commitResponse => this.createBranch(branchName, commitResponse.sha))
+ .then(branchResponse => this.createPR(options.commitMessage, branchName))
+ .then((prResponse) => {
+ return this.user().then(user => {
+ return user.name ? user.name : user.login;
+ })
+ .then(username => this.storeMetadata(contentKey, {
+ type: 'PR',
+ pr: {
+ number: prResponse.number,
+ head: prResponse.head && prResponse.head.sha
+ },
+ user: username,
+ status: status.first(),
+ branch: branchName,
+ collection: options.collectionName,
+ title: options.parsedData && options.parsedData.title,
+ description: options.parsedData && options.parsedData.description,
+ objects: {
+ entry: entry.path,
+ files: filesList
+ },
+ timeStamp: new Date().toISOString()
+ }));
+ });
+ } else {
+ // Entry is already on editorial review workflow - just update metadata and commit to existing branch
+ return this.getBranch(branchName)
.then(branchData => this.updateTree(branchData.commit.sha, '/', fileTree))
.then(changeTree => this.commit(options.commitMessage, changeTree))
.then((response) => {
- if (options.mode && options.mode === EDITORIAL_WORKFLOW) {
- const contentKey = options.collectionName ? `${options.collectionName}-${entry.slug}` : entry.slug;
- const branchName = `cms/${contentKey}`;
- return this.user().then(user => {
- return user.name ? user.name : user.login;
- })
- .then(username => this.storeMetadata(contentKey, {
- type: 'PR',
- user: username,
- status: status.first(),
- branch: branchName,
- collection: options.collectionName,
+ const contentKey = options.collectionName ? `${options.collectionName}-${entry.slug}` : entry.slug;
+ const branchName = `cms/${contentKey}`;
+ return this.user().then(user => {
+ return user.name ? user.name : user.login;
+ })
+ .then(username => this.retrieveMetadata(contentKey))
+ .then(metadata => {
+ let files = metadata.objects && metadata.objects.files || [];
+ files = files.concat(filesList);
+
+ return {
+ ...metadata,
title: options.parsedData && options.parsedData.title,
description: options.parsedData && options.parsedData.description,
objects: {
entry: entry.path,
- files: mediaFiles.map(file => file.path)
+ files: _.uniq(files)
},
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);
- }
+ };
+ })
+ .then(updatedMetadata => this.storeMetadata(contentKey, updatedMetadata))
+ .then(this.patchBranch(branchName, response.sha));
});
+ }
+ }
+
+ 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));
+ }
+
+ publishUnpublishedEntry(collection, slug, status) {
+ const contentKey = collection ? `${collection}-${slug}` : slug;
+ return this.retrieveMetadata(contentKey)
+ .then(metadata => {
+ const headSha = metadata.pr && metadata.pr.head;
+ const number = metadata.pr && metadata.pr.number;
+ return this.mergePR(headSha, number);
+ })
+ .then(() => this.deleteBranch(`cms/${contentKey}`));
}
createRef(type, name, sha) {
@@ -223,10 +296,6 @@ export default class API {
});
}
- createBranch(branchName, sha) {
- return this.createRef('heads', branchName, sha);
- }
-
patchRef(type, name, sha) {
return this.request(`${this.repoURL}/git/refs/${type}/${name}`, {
method: 'PATCH',
@@ -234,12 +303,26 @@ export default class API {
});
}
+ deleteRef(type, name, sha) {
+ return this.request(`${this.repoURL}/git/refs/${type}/${name}`, {
+ method: 'DELETE',
+ });
+ }
+
+ getBranch(branch = this.branch) {
+ return this.request(`${this.repoURL}/branches/${branch}`);
+ }
+
+ createBranch(branchName, sha) {
+ return this.createRef('heads', branchName, sha);
+ }
+
patchBranch(branchName, sha) {
return this.patchRef('heads', branchName, sha);
}
- getBranch() {
- return this.request(`${this.repoURL}/branches/${this.branch}`);
+ deleteBranch(branchName) {
+ return this.deleteRef('heads', branchName);
}
createPR(title, head, base = 'master') {
@@ -250,6 +333,16 @@ export default class API {
});
}
+ mergePR(headSha, number) {
+ return this.request(`${this.repoURL}/pulls/${number}/merge`, {
+ method: 'PUT',
+ body: JSON.stringify({
+ commit_message: 'Automatically generated. Merged on Netlify CMS.',
+ sha: headSha
+ }),
+ });
+ }
+
getTree(sha) {
return sha ? this.request(`${this.repoURL}/git/trees/${sha}`) : Promise.resolve({ tree: [] });
}
diff --git a/src/backends/github/AuthenticationPage.js b/src/backends/github/AuthenticationPage.js
index f7100604..c85dde2f 100644
--- a/src/backends/github/AuthenticationPage.js
+++ b/src/backends/github/AuthenticationPage.js
@@ -21,9 +21,9 @@ export default class AuthenticationPage extends React.Component {
auth = new Authenticator();
}
- auth.authenticate({provider: 'github', scope: 'repo'}, (err, data) => {
+ auth.authenticate({ provider: 'github', scope: 'repo' }, (err, data) => {
if (err) {
- this.setState({loginError: err.toString()});
+ this.setState({ loginError: err.toString() });
return;
}
this.props.onLogin(data);
diff --git a/src/backends/github/implementation.js b/src/backends/github/implementation.js
index 6ce1c865..2d270261 100644
--- a/src/backends/github/implementation.js
+++ b/src/backends/github/implementation.js
@@ -98,4 +98,12 @@ export default class GitHub {
))[0]
));
}
+
+ updateUnpublishedEntryStatus(collection, slug, newStatus) {
+ return this.api.updateUnpublishedEntryStatus(collection, slug, newStatus);
+ }
+
+ publishUnpublishedEntry(collection, slug, status) {
+ return this.api.publishUnpublishedEntry(collection, slug, status);
+ }
}
diff --git a/src/backends/netlify-git/AuthenticationPage.js b/src/backends/netlify-git/AuthenticationPage.js
index 4503a660..74e508c5 100644
--- a/src/backends/netlify-git/AuthenticationPage.js
+++ b/src/backends/netlify-git/AuthenticationPage.js
@@ -1,5 +1,4 @@
import React from 'react';
-import Authenticator from '../../lib/netlify-auth';
export default class AuthenticationPage extends React.Component {
static propTypes = {
@@ -14,8 +13,8 @@ export default class AuthenticationPage extends React.Component {
handleLogin(e) {
e.preventDefault();
- const {email, password} = this.state;
- this.setState({authenticating: true});
+ const { email, password } = this.state;
+ this.setState({ authenticating: true });
fetch(`${AuthenticationPage.url}/token`, {
method: 'POST',
body: 'grant_type=client_credentials',
@@ -27,18 +26,18 @@ export default class AuthenticationPage extends React.Component {
console.log(response);
if (response.ok) {
return response.json().then((data) => {
- this.props.onLogin(Object.assign({email}, data));
+ this.props.onLogin(Object.assign({ email }, data));
});
}
response.json().then((data) => {
- this.setState({loginError: data.msg});
- })
- })
+ this.setState({ loginError: data.msg });
+ });
+ });
}
handleChange(key) {
return (e) => {
- this.setState({[key]: e.target.value});
+ this.setState({ [key]: e.target.value });
};
}
diff --git a/src/backends/test-repo/AuthenticationPage.js b/src/backends/test-repo/AuthenticationPage.js
index fd5c3ddc..ce20b0bf 100644
--- a/src/backends/test-repo/AuthenticationPage.js
+++ b/src/backends/test-repo/AuthenticationPage.js
@@ -7,7 +7,7 @@ export default class AuthenticationPage extends React.Component {
constructor(props) {
super(props);
- this.state = {email: ''};
+ this.state = { email: '' };
this.handleLogin = this.handleLogin.bind(this);
this.handleEmailChange = this.handleEmailChange.bind(this);
}
@@ -18,7 +18,7 @@ export default class AuthenticationPage extends React.Component {
}
handleEmailChange(e) {
- this.setState({email: e.target.value});
+ this.setState({ email: e.target.value });
}
render() {
diff --git a/src/components/EntryEditor.css b/src/components/EntryEditor.css
index 03c128bd..9cacb875 100644
--- a/src/components/EntryEditor.css
+++ b/src/components/EntryEditor.css
@@ -12,6 +12,7 @@
height: 45px;
border-top: 1px solid #e8eae8;
padding: 10px 20px;
+ z-index: 10;
}
.controlPane {
width: 50%;
diff --git a/src/components/EntryEditor.js b/src/components/EntryEditor.js
index ea83f7bb..fd308698 100644
--- a/src/components/EntryEditor.js
+++ b/src/components/EntryEditor.js
@@ -27,14 +27,14 @@ export default class EntryEditor extends React.Component {
calculateHeight() {
const height = window.innerHeight - 54;
console.log('setting height to %s', height);
- this.setState({height});
+ this.setState({ height });
}
render() {
const { collection, entry, getMedia, onChange, onAddMedia, onRemoveMedia, onPersist } = this.props;
- const {height} = this.state;
+ const { height } = this.state;
- return
+ return
{
const noop = function() {};
-export default function Icon({ style, className = '', type, onClick = noop}) {
+export default function Icon({ style, className = '', type, onClick = noop }) {
return ;
}
diff --git a/src/components/UI/index.js b/src/components/UI/index.js
index 7f538b2a..87b473a5 100644
--- a/src/components/UI/index.js
+++ b/src/components/UI/index.js
@@ -1,3 +1,4 @@
export { default as Card } from './card/Card';
export { default as Loader } from './loader/Loader';
export { default as Icon } from './icon/Icon';
+export { default as Toast } from './toast/Toast';
diff --git a/src/components/UI/theme.css b/src/components/UI/theme.css
index b8add9c7..87c78c64 100644
--- a/src/components/UI/theme.css
+++ b/src/components/UI/theme.css
@@ -1,5 +1,6 @@
:root {
--defaultColor: #333;
+ --defaultColorLight: #eee;
--backgroundColor: #fff;
--shadowColor: rgba(0, 0, 0, 0.117647);
--successColor: #1c7;
diff --git a/src/components/UI/toast/Toast.css b/src/components/UI/toast/Toast.css
new file mode 100644
index 00000000..2c5bb930
--- /dev/null
+++ b/src/components/UI/toast/Toast.css
@@ -0,0 +1,40 @@
+@import "../theme.css";
+
+.toast {
+ composes: base container rounded depth;
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ z-index: 100;
+ width: 350px;
+ padding: 20px 10px;
+ font-size: 0.9rem;
+ text-align: center;
+ color: var(--defaultColorLight);
+ overflow: hidden;
+ opacity: 1;
+ transition: opacity .3s ease-in;
+}
+
+.hidden {
+ opacity: 0;
+}
+
+.icon {
+ position: absolute;
+ top: calc(50% - 15px);
+ left: 15px;
+ font-size: 30px;
+}
+
+.success {
+ background-color: var(--successColor);
+}
+
+.warning {
+ background-color: var(--warningColor);
+}
+
+.error {
+ background-color: var(--errorColor);
+}
diff --git a/src/components/UI/toast/Toast.js b/src/components/UI/toast/Toast.js
new file mode 100644
index 00000000..34671df4
--- /dev/null
+++ b/src/components/UI/toast/Toast.js
@@ -0,0 +1,74 @@
+import React, { PropTypes } from 'react';
+import { Icon } from '../index';
+import styles from './Toast.css';
+
+export default class Toast extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ shown: false
+ };
+
+ this.autoHideTimeout = this.autoHideTimeout.bind(this);
+ }
+
+ componentWillMount() {
+ if (this.props.show) {
+ this.autoHideTimeout();
+ this.setState({ shown: true });
+ }
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps !== this.props) {
+ if (nextProps.show) this.autoHideTimeout();
+ this.setState({ shown: nextProps.show });
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.timeOut) {
+ clearTimeout(this.timeOut);
+ }
+ }
+
+ autoHideTimeout() {
+ clearTimeout(this.timeOut);
+ this.timeOut = setTimeout(() => {
+ this.setState({ shown: false });
+ }, 4000);
+ }
+
+ render() {
+ const { style, type, className, children } = this.props;
+ const icons = {
+ success: 'check',
+ warning: 'attention',
+ error: 'alert'
+ };
+ const classes = [styles.toast];
+ if (className) classes.push(className);
+
+ let icon = '';
+ if (type) {
+ classes.push(styles[type]);
+ icon = ;
+ }
+
+ if (!this.state.shown) {
+ classes.push(styles.hidden);
+ }
+
+ return (
+ {icon}{children}
+ );
+ }
+}
+
+Toast.propTypes = {
+ style: PropTypes.object,
+ type: PropTypes.oneOf(['success', 'warning', 'error']).isRequired,
+ className: PropTypes.string,
+ show: PropTypes.bool,
+ children: PropTypes.node
+};
diff --git a/src/components/UnpublishedListing.css b/src/components/UnpublishedListing.css
index b37d8bf2..ebd7bcd6 100644
--- a/src/components/UnpublishedListing.css
+++ b/src/components/UnpublishedListing.css
@@ -1,23 +1,32 @@
+.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 {
width: 100% !important;
margin: 7px 0;
- & h1 {
+ & h2 {
font-size: 17px;
& small {
font-weight: normal;
@@ -29,4 +38,16 @@
font-size: 12px;
margin-top: 5px;
}
+
+ & button {
+ margin: 10px 10px 0 0;
+ float: right;
+ }
+}
+
+
+.clear::after {
+ content:"";
+ display:block;
+ clear:both;
}
diff --git a/src/components/UnpublishedListing.js b/src/components/UnpublishedListing.js
index 97a711f4..9d09b6a0 100644
--- a/src/components/UnpublishedListing.js
+++ b/src/components/UnpublishedListing.js
@@ -1,21 +1,104 @@
-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';
import { Link } from 'react-router';
-import { statusDescriptions } from '../constants/publishModes';
+import { status, 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(
+
+
{statusDescriptions.get(status)}
+ {children}
+
+ );
+}
+const columnTargetSpec = {
+ drop(props, monitor) {
+ const slug = monitor.getItem().slug;
+ const collection = monitor.getItem().collection;
+ const oldStatus = monitor.getItem().ownStatus;
+ 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({ slug, collection, ownStatus, onRequestPublish, connectDragSource, children }) {
+ return connectDragSource(
+
+
+ {children}
+ {(ownStatus === status.last()) &&
+ Publish now
+ }
+
+
+ );
+}
+const cardDragSpec = {
+ beginDrag(props) {
+ return {
+ slug: props.slug,
+ collection: props.collection,
+ ownStatus: props.ownStatus
+ };
+ }
+};
+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);
+ this.requestPublish = this.requestPublish.bind(this);
+ }
+
+ requestPublish(collection, slug, ownStatus) {
+ if (ownStatus !== status.last()) return;
+ if (window.confirm('Are you sure you want to publish this entry?')) {
+ this.props.handlePublish(collection, slug, ownStatus);
+ }
+ }
+
renderColumns(entries, column) {
if (!entries) return;
if (!column) {
return entries.entrySeq().map(([currColumn, currEntries]) => (
-
-
{statusDescriptions.get(currColumn)}
+
{this.renderColumns(currEntries, currColumn)}
-
+
));
} else {
return
@@ -24,11 +107,20 @@ export default class UnpublishedListing extends React.Component {
const author = entry.getIn(['data', 'author'], entry.getIn(['metaData', 'user']));
const timeStamp = moment(entry.getIn(['metaData', 'timeStamp'])).format('llll');
const link = `/editorialworkflow/${entry.getIn(['metaData', 'collection'])}/${entry.getIn(['metaData', 'status'])}/${entry.get('slug')}`;
+ const slug = entry.get('slug');
+ const status = entry.getIn(['metaData', 'status']);
+ const collection = entry.getIn(['metaData', 'collection']);
return (
-
- {entry.getIn(['data', 'title'])} by {author}
+
+ {entry.getIn(['data', 'title'])} by {author}
Last updated: {timeStamp} by {entry.getIn(['metaData', 'user'])}
-
+
);
}
)}
@@ -38,11 +130,12 @@ export default class UnpublishedListing extends React.Component {
render() {
const columns = this.renderColumns(this.props.entries);
-
return (
-
+
Editorial Workflow
+
{columns}
+
);
}
@@ -50,4 +143,8 @@ export default class UnpublishedListing extends React.Component {
UnpublishedListing.propTypes = {
entries: ImmutablePropTypes.orderedMap,
+ handleChangeStatus: PropTypes.func.isRequired,
+ handlePublish: PropTypes.func.isRequired,
};
+
+export default DragDropContext(HTML5Backend)(UnpublishedListing);
diff --git a/src/components/Widgets/MarkdownControl.js b/src/components/Widgets/MarkdownControl.js
index 54abac8b..483be419 100644
--- a/src/components/Widgets/MarkdownControl.js
+++ b/src/components/Widgets/MarkdownControl.js
@@ -60,7 +60,7 @@ class MarkdownControl extends React.Component {
return (
- { this.renderEditor() }
+ {this.renderEditor()}
);
}
diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/schema.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/schema.js
index ea00ade2..412dae5e 100644
--- a/src/components/Widgets/MarkdownControlElements/VisualEditor/schema.js
+++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/schema.js
@@ -62,4 +62,4 @@ export const SCHEMA = {
borderRadius: '4px'
}
}
-}
+};
diff --git a/src/components/Widgets/richText.js b/src/components/Widgets/richText.js
index b5d4b323..082631e0 100644
--- a/src/components/Widgets/richText.js
+++ b/src/components/Widgets/richText.js
@@ -46,7 +46,7 @@ function processEditorPlugins(plugins) {
- { plugin.fields.map(field => `${field.label}: “${node.data.get(field.name)}”`) }
+ {plugin.fields.map(field => `${field.label}: “${node.data.get(field.name)}”`)}
);
diff --git a/src/components/stories/Card.js b/src/components/stories/Card.js
index 95eeabe7..368a85cf 100644
--- a/src/components/stories/Card.js
+++ b/src/components/stories/Card.js
@@ -39,4 +39,4 @@ storiesOf('Card', module)
header and footer elements are also not subject to margin
- ))
+ ));
diff --git a/src/components/stories/Toast.js b/src/components/stories/Toast.js
new file mode 100644
index 00000000..6ac4b7c6
--- /dev/null
+++ b/src/components/stories/Toast.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import { Toast } from '../UI';
+import { storiesOf } from '@kadira/storybook';
+
+
+storiesOf('Toast', module)
+ .add('Success', () => (
+
+ A Toast Message
+
+ )).add('Waring', () => (
+
+ A Toast Message
+
+ )).add('Error', () => (
+
+ A Toast Message
+
+ ));
diff --git a/src/components/stories/index.js b/src/components/stories/index.js
index a966ecf4..21f91079 100644
--- a/src/components/stories/index.js
+++ b/src/components/stories/index.js
@@ -1,2 +1,3 @@
import './Card';
import './Icon';
+import './Toast';
diff --git a/src/containers/App.js b/src/containers/App.js
index 5b6af1c9..64a56d4d 100644
--- a/src/containers/App.js
+++ b/src/containers/App.js
@@ -4,6 +4,7 @@ import { IndexLink } from 'react-router';
import { loadConfig } from '../actions/config';
import { loginUser } from '../actions/auth';
import { currentBackend } from '../backends/backend';
+import { Loader } from '../components/UI';
import { SHOW_COLLECTION, CREATE_COLLECTION, HELP } from '../actions/findbar';
import FindBar from './FindBar';
import styles from './App.css';
@@ -27,7 +28,7 @@ class App extends React.Component {
configLoading() {
return
-
Loading configuration...
+ Loading configuration...
;
}
diff --git a/src/containers/EntryPage.js b/src/containers/EntryPage.js
index 1693b587..36cfb71a 100644
--- a/src/containers/EntryPage.js
+++ b/src/containers/EntryPage.js
@@ -57,7 +57,7 @@ class EntryPage extends React.Component {
const {
entry, entryDraft, boundGetMedia, collection, changeDraft, addMedia, removeMedia
} = this.props;
-
+
if (entryDraft == null || entryDraft.get('entry') == undefined || entry && entry.get('isFetching')) {
return
Loading...
;
}
diff --git a/src/containers/FindBar.js b/src/containers/FindBar.js
index ce13254d..f228b824 100644
--- a/src/containers/FindBar.js
+++ b/src/containers/FindBar.js
@@ -13,7 +13,12 @@ class FindBar extends Component {
constructor(props) {
super(props);
this._compiledCommands = [];
- this._searchCommand = { search: true, regexp:`(?:${SEARCH})?(.*)`, param:{ name:'searchTerm', display:'' }, token: SEARCH };
+ this._searchCommand = {
+ search: true,
+ regexp: `(?:${SEARCH})?(.*)`,
+ param: { name: 'searchTerm', display: '' },
+ token: SEARCH
+ };
this.state = {
value: '',
placeholder: PLACEHOLDER,
@@ -68,7 +73,7 @@ class FindBar extends Component {
if (match && match[1]) {
regexp += '(.*)';
- param = { name:match[1], display:match[2] || this._camelCaseToSpace(match[1]) };
+ param = { name: match[1], display: match[2] || this._camelCaseToSpace(match[1]) };
}
return Object.assign({}, command, {
@@ -144,6 +149,7 @@ class FindBar extends Component {
getSuggestions() {
return this._getSuggestions(this.state.value, this.state.activeScope, this._compiledCommands, this.props.defaultCommands);
}
+
// Memoized version
_getSuggestions(value, scope, commands, defaultCommands) {
if (scope) return []; // No autocomplete for scoped input
@@ -152,7 +158,7 @@ class FindBar extends Component {
.filter(command => defaultCommands.indexOf(command.id) !== -1)
.map(result => (
Object.assign({}, result, { string: result.token }
- )));
+ )));
}
const results = fuzzy.filter(value, commands, {
@@ -162,8 +168,8 @@ class FindBar extends Component {
});
const returnResults = results.slice(0, 4).map(result => (
- Object.assign({}, result.original, { string:result.string }
- )));
+ Object.assign({}, result.original, { string: result.string }
+ )));
returnResults.push(this._searchCommand);
return returnResults;
@@ -178,7 +184,7 @@ class FindBar extends Component {
index = (
highlightedIndex === this.getSuggestions().length - 1 ||
this.state.isOpen === false
- ) ? 0 : highlightedIndex + 1;
+ ) ? 0 : highlightedIndex + 1;
this.setState({
highlightedIndex: index,
isOpen: true,
@@ -290,7 +296,7 @@ class FindBar extends Component {
let children;
if (!command.search) {
children = (
-
+
);
} else {
children = (
@@ -299,7 +305,8 @@ class FindBar extends Component {
Search... :
Search for:
}
-
{this.state.value}
+
{this.state.value}
+
);
}
return (
@@ -317,7 +324,7 @@ class FindBar extends Component {
return commands.length === 0 ? null : (
- { commands }
+ {commands}
Your past searches and commands
@@ -328,7 +335,7 @@ class FindBar extends Component {
renderActiveScope() {
if (this.state.activeScope === SEARCH) {
- return
;
+ return
;
} else {
return
{this.state.activeScope}
;
}
@@ -358,6 +365,7 @@ class FindBar extends Component {
);
}
}
+
FindBar.propTypes = {
commands: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string.isRequired,
diff --git a/src/containers/editorialWorkflow/CollectionPageHOC.js b/src/containers/editorialWorkflow/CollectionPageHOC.js
index bee4a2ac..6aeacb71 100644
--- a/src/containers/editorialWorkflow/CollectionPageHOC.js
+++ b/src/containers/editorialWorkflow/CollectionPageHOC.js
@@ -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, publishUnpublishedEntry } from '../../actions/editorialWorkflow';
import { selectUnpublishedEntries } from '../../reducers';
import { EDITORIAL_WORKFLOW, status } from '../../constants/publishModes';
import UnpublishedListing from '../../components/UnpublishedListing';
@@ -20,12 +20,16 @@ export default function CollectionPageHOC(CollectionPage) {
}
render() {
- const { isEditorialWorkflow, unpublishedEntries } = this.props;
+ const { isEditorialWorkflow, unpublishedEntries, updateUnpublishedEntryStatus, publishUnpublishedEntry } = this.props;
if (!isEditorialWorkflow) return super.render();
return (
-
+
{super.render()}
);
@@ -56,5 +60,8 @@ export default function CollectionPageHOC(CollectionPage) {
return returnObj;
}
- return connect(mapStateToProps)(CollectionPageHOC);
+ return connect(mapStateToProps, {
+ updateUnpublishedEntryStatus,
+ publishUnpublishedEntry
+ })(CollectionPageHOC);
}
diff --git a/src/containers/editorialWorkflow/EntryPageHOC.js b/src/containers/editorialWorkflow/EntryPageHOC.js
index 1840c210..4f38e293 100644
--- a/src/containers/editorialWorkflow/EntryPageHOC.js
+++ b/src/containers/editorialWorkflow/EntryPageHOC.js
@@ -1,7 +1,7 @@
import React from 'react';
import { EDITORIAL_WORKFLOW } from '../../constants/publishModes';
import { selectUnpublishedEntry } from '../../reducers';
-import { loadUnpublishedEntry } from '../../actions/editorialWorkflow';
+import { loadUnpublishedEntry, persistUnpublishedEntry } from '../../actions/editorialWorkflow';
import { connect } from 'react-redux';
export default function EntryPageHOC(EntryPage) {
@@ -22,10 +22,6 @@ export default function EntryPageHOC(EntryPage) {
const slug = ownProps.params.slug;
const entry = selectUnpublishedEntry(state, status, slug);
returnObj.entry = entry;
-
- returnObj.persistEntry = () => {
- // TODO - for now, simply ignore
- };
}
return returnObj;
}
@@ -39,6 +35,10 @@ export default function EntryPageHOC(EntryPage) {
returnObj.loadEntry = (collection, slug) => {
dispatch(loadUnpublishedEntry(collection, status, slug));
};
+
+ returnObj.persistEntry = (collection, entryDraft) => {
+ dispatch(persistUnpublishedEntry(collection, entryDraft));
+ };
}
return returnObj;
}
diff --git a/src/formats/yaml.js b/src/formats/yaml.js
index 9fd73c07..d939c068 100644
--- a/src/formats/yaml.js
+++ b/src/formats/yaml.js
@@ -41,6 +41,6 @@ export default class YAML {
}
toFile(data) {
- return yaml.safeDump(data, {schema: OutputSchema});
+ return yaml.safeDump(data, { schema: OutputSchema });
}
}
diff --git a/src/lib/registry.js b/src/lib/registry.js
index 4d7f14e9..e4de5183 100644
--- a/src/lib/registry.js
+++ b/src/lib/registry.js
@@ -1,5 +1,5 @@
-import {List} from 'immutable';
-import {newEditorPlugin} from '../components/Widgets/MarkdownControlElements/plugins';
+import { List } from 'immutable';
+import { newEditorPlugin } from '../components/Widgets/MarkdownControlElements/plugins';
const _registry = {
templates: {},
diff --git a/src/reducers/editorialWorkflow.js b/src/reducers/editorialWorkflow.js
index beee2071..246656f0 100644
--- a/src/reducers/editorialWorkflow.js
+++ b/src/reducers/editorialWorkflow.js
@@ -1,7 +1,12 @@
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,
+ UNPUBLISHED_ENTRY_PUBLISH_SUCCESS
} from '../actions/editorialWorkflow';
import { CONFIG_SUCCESS } from '../actions/config';
@@ -39,6 +44,23 @@ const unpublishedEntries = (state = null, action) => {
ids: List(entries.map((entry) => entry.slug))
}));
});
+
+ case UNPUBLISHED_ENTRY_STATUS_CHANGE_SUCCESS:
+ return state.withMutations((map) => {
+ let entry = map.getIn(['entities', `${action.payload.oldStatus}.${action.payload.slug}`]);
+ entry = entry.setIn(['metaData', 'status'], action.payload.newStatus);
+
+ let entities = map.get('entities').filter((val, key) => (
+ key !== `${action.payload.oldStatus}.${action.payload.slug}`
+ ));
+ entities = entities.set(`${action.payload.newStatus}.${action.payload.slug}`, entry);
+
+ map.set('entities', entities);
+ });
+
+ case UNPUBLISHED_ENTRY_PUBLISH_SUCCESS:
+ return state.deleteIn(['entities', `${action.payload.status}.${action.payload.slug}`]);
+
default:
return state;
}
diff --git a/test/reducers/auth.spec.js b/test/reducers/auth.spec.js
index 1d766f53..83830fd5 100644
--- a/test/reducers/auth.spec.js
+++ b/test/reducers/auth.spec.js
@@ -16,15 +16,15 @@ describe('auth', () => {
expect(
auth(undefined, authenticating())
).toEqual(
- Immutable.Map({isFetching: true})
+ Immutable.Map({ isFetching: true })
);
});
it('should handle authentication', () => {
expect(
- auth(undefined, authenticate({email: 'joe@example.com'}))
+ auth(undefined, authenticate({ email: 'joe@example.com' }))
).toEqual(
- Immutable.fromJS({user: {email: 'joe@example.com'}})
+ Immutable.fromJS({ user: { email: 'joe@example.com' } })
);
});
diff --git a/test/reducers/collections.spec.js b/test/reducers/collections.spec.js
index f3f31583..18ba43d0 100644
--- a/test/reducers/collections.spec.js
+++ b/test/reducers/collections.spec.js
@@ -15,39 +15,39 @@ describe('collections', () => {
it('should load the collections from the config', () => {
expect(
- collections(undefined, configLoaded({collections: [
- {name: 'posts', folder: '_posts', fields: [{name: 'title', widget: 'string'}]}
- ]}))
+ collections(undefined, configLoaded({ collections: [
+ { name: 'posts', folder: '_posts', fields: [{ name: 'title', widget: 'string' }] }
+ ] }))
).toEqual(
OrderedMap({
- posts: fromJS({name: 'posts', folder: '_posts', fields: [{name: 'title', widget: 'string'}]})
+ posts: fromJS({ name: 'posts', folder: '_posts', fields: [{ name: 'title', widget: 'string' }] })
})
);
});
it('should mark entries as loading', () => {
const state = OrderedMap({
- 'posts': Map({name: 'posts'})
+ 'posts': Map({ name: 'posts' })
});
expect(
- collections(state, entriesLoading(Map({name: 'posts'})))
+ collections(state, entriesLoading(Map({ name: 'posts' })))
).toEqual(
OrderedMap({
- 'posts': Map({name: 'posts', isFetching: true})
+ 'posts': Map({ name: 'posts', isFetching: true })
})
);
});
it('should handle loaded entries', () => {
const state = OrderedMap({
- 'posts': Map({name: 'posts'})
+ 'posts': Map({ name: 'posts' })
});
- const entries = [{slug: 'a', path: ''}, {slug: 'b', title: 'B'}];
+ const entries = [{ slug: 'a', path: '' }, { slug: 'b', title: 'B' }];
expect(
- collections(state, entriesLoaded(Map({name: 'posts'}), entries))
+ collections(state, entriesLoaded(Map({ name: 'posts' }), entries))
).toEqual(
OrderedMap({
- 'posts': fromJS({name: 'posts', entries: entries})
+ 'posts': fromJS({ name: 'posts', entries: entries })
})
);
});
diff --git a/test/reducers/config.spec.js b/test/reducers/config.spec.js
index 75aa9aad..9167e542 100644
--- a/test/reducers/config.spec.js
+++ b/test/reducers/config.spec.js
@@ -14,9 +14,9 @@ describe('config', () => {
it('should handle an update', () => {
expect(
- config(Immutable.Map({'a': 'b', 'c': 'd'}), configLoaded({'a': 'changed', 'e': 'new'}))
+ config(Immutable.Map({ 'a': 'b', 'c': 'd' }), configLoaded({ 'a': 'changed', 'e': 'new' }))
).toEqual(
- Immutable.Map({'a': 'changed', 'e': 'new'})
+ Immutable.Map({ 'a': 'changed', 'e': 'new' })
);
});
@@ -24,15 +24,15 @@ describe('config', () => {
expect(
config(undefined, configLoading())
).toEqual(
- Immutable.Map({isFetching: true})
+ Immutable.Map({ isFetching: true })
);
});
it('should handle an error', () => {
expect(
- config(Immutable.Map({isFetching: true}), configFailed(new Error('Config could not be loaded')))
+ config(Immutable.Map({ isFetching: true }), configFailed(new Error('Config could not be loaded')))
).toEqual(
- Immutable.Map({error: 'Error: Config could not be loaded'})
+ Immutable.Map({ error: 'Error: Config could not be loaded' })
);
});
});