Merge branch 'react-pr' into dashboard-link
This commit is contained in:
commit
6d0535dc26
23
package.json
23
package.json
@ -9,8 +9,18 @@
|
|||||||
"test:watch": "npm test -- --watch",
|
"test:watch": "npm test -- --watch",
|
||||||
"build": "webpack --config webpack.config.js",
|
"build": "webpack --config webpack.config.js",
|
||||||
"storybook": "start-storybook -p 9001",
|
"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": [
|
"keywords": [
|
||||||
"netlify",
|
"netlify",
|
||||||
"cms"
|
"cms"
|
||||||
@ -21,7 +31,7 @@
|
|||||||
"@kadira/storybook": "^1.36.0",
|
"@kadira/storybook": "^1.36.0",
|
||||||
"autoprefixer": "^6.3.3",
|
"autoprefixer": "^6.3.3",
|
||||||
"babel-core": "^6.5.1",
|
"babel-core": "^6.5.1",
|
||||||
"babel-eslint": "^4.1.8",
|
"babel-eslint": "^6.1.2",
|
||||||
"babel-loader": "^6.2.2",
|
"babel-loader": "^6.2.2",
|
||||||
"babel-plugin-lodash": "^3.2.0",
|
"babel-plugin-lodash": "^3.2.0",
|
||||||
"babel-plugin-transform-class-properties": "^6.5.2",
|
"babel-plugin-transform-class-properties": "^6.5.2",
|
||||||
@ -32,20 +42,21 @@
|
|||||||
"babel-register": "^6.5.2",
|
"babel-register": "^6.5.2",
|
||||||
"babel-runtime": "^6.5.0",
|
"babel-runtime": "^6.5.0",
|
||||||
"css-loader": "^0.23.1",
|
"css-loader": "^0.23.1",
|
||||||
"eslint": "^1.10.3",
|
"eslint": "^3.5.0",
|
||||||
"eslint-loader": "^1.2.1",
|
|
||||||
"eslint-plugin-react": "^5.1.1",
|
"eslint-plugin-react": "^5.1.1",
|
||||||
"exports-loader": "^0.6.3",
|
"exports-loader": "^0.6.3",
|
||||||
"file-loader": "^0.8.5",
|
"file-loader": "^0.8.5",
|
||||||
"immutable": "^3.7.6",
|
"immutable": "^3.7.6",
|
||||||
"imports-loader": "^0.6.5",
|
"imports-loader": "^0.6.5",
|
||||||
"js-yaml": "^3.5.3",
|
"js-yaml": "^3.5.3",
|
||||||
|
"lint-staged": "^3.0.2",
|
||||||
"mocha": "^2.4.5",
|
"mocha": "^2.4.5",
|
||||||
"moment": "^2.11.2",
|
"moment": "^2.11.2",
|
||||||
"normalizr": "^2.0.0",
|
"normalizr": "^2.0.0",
|
||||||
"postcss-cssnext": "^2.7.0",
|
"postcss-cssnext": "^2.7.0",
|
||||||
"postcss-import": "^8.1.2",
|
"postcss-import": "^8.1.2",
|
||||||
"postcss-loader": "^0.9.1",
|
"postcss-loader": "^0.9.1",
|
||||||
|
"pre-commit": "^1.1.3",
|
||||||
"react": "^15.1.0",
|
"react": "^15.1.0",
|
||||||
"react-dom": "^15.1.0",
|
"react-dom": "^15.1.0",
|
||||||
"react-hot-loader": "^3.0.0-beta.2",
|
"react-hot-loader": "^3.0.0-beta.2",
|
||||||
@ -75,8 +86,10 @@
|
|||||||
"markup-it": "git+https://github.com/cassiozen/markup-it.git",
|
"markup-it": "git+https://github.com/cassiozen/markup-it.git",
|
||||||
"pluralize": "^3.0.0",
|
"pluralize": "^3.0.0",
|
||||||
"prismjs": "^1.5.1",
|
"prismjs": "^1.5.1",
|
||||||
"react-datetime": "^2.6.0",
|
|
||||||
"react-addons-css-transition-group": "^15.3.1",
|
"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",
|
"react-portal": "^2.2.1",
|
||||||
"selection-position": "^1.0.0",
|
"selection-position": "^1.0.0",
|
||||||
"semaphore": "^1.0.5",
|
"semaphore": "^1.0.5",
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { currentBackend } from '../backends/backend';
|
import { currentBackend } from '../backends/backend';
|
||||||
|
import { getMedia } from '../reducers';
|
||||||
import { EDITORIAL_WORKFLOW } from '../constants/publishModes';
|
import { EDITORIAL_WORKFLOW } from '../constants/publishModes';
|
||||||
/*
|
/*
|
||||||
* Contant Declarations
|
* 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_SUCCESS = 'UNPUBLISHED_ENTRIES_SUCCESS';
|
||||||
export const UNPUBLISHED_ENTRIES_FAILURE = 'UNPUBLISHED_ENTRIES_FAILURE';
|
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)
|
* 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
|
* 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));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -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 newEntry = entryDraft.getIn(['entry', 'newRecord']) || false;
|
||||||
|
|
||||||
const parsedData = {
|
const parsedData = {
|
||||||
@ -139,10 +139,23 @@ class Backend {
|
|||||||
const collectionName = collection.get('name');
|
const collectionName = collection.get('name');
|
||||||
|
|
||||||
return this.implementation.persistEntry(entryObj, MediaFiles, {
|
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) {
|
entryToRaw(collection, entry) {
|
||||||
const format = resolveFormat(collection, entry);
|
const format = resolveFormat(collection, entry);
|
||||||
return format && format.toFile(entry);
|
return format && format.toFile(entry);
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
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_WORKFLOW, status } from '../../constants/publishModes';
|
import _ from 'lodash';
|
||||||
|
import { SIMPLE, EDITORIAL_WORKFLOW, status } from '../../constants/publishModes';
|
||||||
|
|
||||||
const API_ROOT = 'https://api.github.com';
|
const API_ROOT = 'https://api.github.com';
|
||||||
|
|
||||||
export default class API {
|
export default class API {
|
||||||
|
|
||||||
|
|
||||||
constructor(token, repo, branch) {
|
constructor(token, repo, branch) {
|
||||||
this.token = token;
|
this.token = token;
|
||||||
this.repo = repo;
|
this.repo = repo;
|
||||||
@ -54,7 +53,8 @@ export default class API {
|
|||||||
const headers = this.requestHeaders(options.headers || {});
|
const headers = this.requestHeaders(options.headers || {});
|
||||||
const url = this.urlFor(path, options);
|
const url = this.urlFor(path, options);
|
||||||
return fetch(url, { ...options, headers: headers }).then((response) => {
|
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);
|
return this.parseJsonResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,17 +98,28 @@ export default class API {
|
|||||||
return this.uploadBlob(fileTree[`${key}.json`])
|
return this.uploadBlob(fileTree[`${key}.json`])
|
||||||
.then(item => this.updateTree(branchData.sha, '/', fileTree))
|
.then(item => this.updateTree(branchData.sha, '/', fileTree))
|
||||||
.then(changeTree => this.commit(`Updating “${key}” metadata`, changeTree))
|
.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) {
|
retrieveMetadata(key) {
|
||||||
return this.request(`${this.repoURL}/contents/${key}.json`, {
|
const cache = LocalForage.getItem(`gh.meta.${key}`);
|
||||||
params: { ref: 'refs/meta/_netlify_cms' },
|
return cache.then((cached) => {
|
||||||
headers: { Accept: 'application/vnd.github.VERSION.raw' },
|
if (cached && cached.expires > Date.now()) { return cached.data; }
|
||||||
cache: 'no-store',
|
|
||||||
})
|
return this.request(`${this.repoURL}/contents/${key}.json`, {
|
||||||
.then(response => JSON.parse(response));
|
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) {
|
readFile(path, sha, branch = this.branch) {
|
||||||
@ -136,26 +147,14 @@ export default class API {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readUnpublishedBranchFile(contentKey) {
|
readUnpublishedBranchFile(contentKey) {
|
||||||
const cache = LocalForage.getItem(`gh.unpublished.${contentKey}`);
|
let metaData;
|
||||||
return cache.then((cached) => {
|
return this.retrieveMetadata(contentKey)
|
||||||
if (cached && cached.expires > Date.now()) { return cached.data; }
|
.then(data => {
|
||||||
|
metaData = data;
|
||||||
let metaData;
|
return this.readFile(data.objects.entry, null, data.branch);
|
||||||
return this.retrieveMetadata(contentKey)
|
})
|
||||||
.then(data => {
|
.then(file => {
|
||||||
metaData = data;
|
return { metaData, file };
|
||||||
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;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,37 +182,111 @@ export default class API {
|
|||||||
subtree[filename] = file;
|
subtree[filename] = file;
|
||||||
file.file = true;
|
file.file = true;
|
||||||
});
|
});
|
||||||
return Promise.all(uploadPromises)
|
return Promise.all(uploadPromises).then(() => {
|
||||||
.then(() => this.getBranch())
|
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(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_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.user().then(user => {
|
||||||
return this.user().then(user => {
|
return user.name ? user.name : user.login;
|
||||||
return user.name ? user.name : user.login;
|
})
|
||||||
})
|
.then(username => this.retrieveMetadata(contentKey))
|
||||||
.then(username => this.storeMetadata(contentKey, {
|
.then(metadata => {
|
||||||
type: 'PR',
|
let files = metadata.objects && metadata.objects.files || [];
|
||||||
user: username,
|
files = files.concat(filesList);
|
||||||
status: status.first(),
|
|
||||||
branch: branchName,
|
return {
|
||||||
collection: options.collectionName,
|
...metadata,
|
||||||
title: options.parsedData && options.parsedData.title,
|
title: options.parsedData && options.parsedData.title,
|
||||||
description: options.parsedData && options.parsedData.description,
|
description: options.parsedData && options.parsedData.description,
|
||||||
objects: {
|
objects: {
|
||||||
entry: entry.path,
|
entry: entry.path,
|
||||||
files: mediaFiles.map(file => file.path)
|
files: _.uniq(files)
|
||||||
},
|
},
|
||||||
timeStamp: new Date().toISOString()
|
timeStamp: new Date().toISOString()
|
||||||
}))
|
};
|
||||||
.then(this.createBranch(branchName, response.sha))
|
})
|
||||||
.then(this.createPR(options.commitMessage, `cms/${contentKey}`));
|
.then(updatedMetadata => this.storeMetadata(contentKey, updatedMetadata))
|
||||||
} else {
|
.then(this.patchBranch(branchName, response.sha));
|
||||||
return this.patchBranch(this.branch, 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) {
|
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) {
|
patchRef(type, name, sha) {
|
||||||
return this.request(`${this.repoURL}/git/refs/${type}/${name}`, {
|
return this.request(`${this.repoURL}/git/refs/${type}/${name}`, {
|
||||||
method: 'PATCH',
|
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) {
|
patchBranch(branchName, sha) {
|
||||||
return this.patchRef('heads', branchName, sha);
|
return this.patchRef('heads', branchName, sha);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBranch() {
|
deleteBranch(branchName) {
|
||||||
return this.request(`${this.repoURL}/branches/${this.branch}`);
|
return this.deleteRef('heads', branchName);
|
||||||
}
|
}
|
||||||
|
|
||||||
createPR(title, head, base = 'master') {
|
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) {
|
getTree(sha) {
|
||||||
return sha ? this.request(`${this.repoURL}/git/trees/${sha}`) : Promise.resolve({ tree: [] });
|
return sha ? this.request(`${this.repoURL}/git/trees/${sha}`) : Promise.resolve({ tree: [] });
|
||||||
}
|
}
|
||||||
|
@ -21,9 +21,9 @@ export default class AuthenticationPage extends React.Component {
|
|||||||
auth = new Authenticator();
|
auth = new Authenticator();
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.authenticate({provider: 'github', scope: 'repo'}, (err, data) => {
|
auth.authenticate({ provider: 'github', scope: 'repo' }, (err, data) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.setState({loginError: err.toString()});
|
this.setState({ loginError: err.toString() });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.props.onLogin(data);
|
this.props.onLogin(data);
|
||||||
|
@ -98,4 +98,12 @@ export default class GitHub {
|
|||||||
))[0]
|
))[0]
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateUnpublishedEntryStatus(collection, slug, newStatus) {
|
||||||
|
return this.api.updateUnpublishedEntryStatus(collection, slug, newStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
publishUnpublishedEntry(collection, slug, status) {
|
||||||
|
return this.api.publishUnpublishedEntry(collection, slug, status);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Authenticator from '../../lib/netlify-auth';
|
|
||||||
|
|
||||||
export default class AuthenticationPage extends React.Component {
|
export default class AuthenticationPage extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -14,8 +13,8 @@ export default class AuthenticationPage extends React.Component {
|
|||||||
|
|
||||||
handleLogin(e) {
|
handleLogin(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const {email, password} = this.state;
|
const { email, password } = this.state;
|
||||||
this.setState({authenticating: true});
|
this.setState({ authenticating: true });
|
||||||
fetch(`${AuthenticationPage.url}/token`, {
|
fetch(`${AuthenticationPage.url}/token`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: 'grant_type=client_credentials',
|
body: 'grant_type=client_credentials',
|
||||||
@ -27,18 +26,18 @@ export default class AuthenticationPage extends React.Component {
|
|||||||
console.log(response);
|
console.log(response);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return response.json().then((data) => {
|
return response.json().then((data) => {
|
||||||
this.props.onLogin(Object.assign({email}, data));
|
this.props.onLogin(Object.assign({ email }, data));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
response.json().then((data) => {
|
response.json().then((data) => {
|
||||||
this.setState({loginError: data.msg});
|
this.setState({ loginError: data.msg });
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
handleChange(key) {
|
handleChange(key) {
|
||||||
return (e) => {
|
return (e) => {
|
||||||
this.setState({[key]: e.target.value});
|
this.setState({ [key]: e.target.value });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ export default class AuthenticationPage extends React.Component {
|
|||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {email: ''};
|
this.state = { email: '' };
|
||||||
this.handleLogin = this.handleLogin.bind(this);
|
this.handleLogin = this.handleLogin.bind(this);
|
||||||
this.handleEmailChange = this.handleEmailChange.bind(this);
|
this.handleEmailChange = this.handleEmailChange.bind(this);
|
||||||
}
|
}
|
||||||
@ -18,7 +18,7 @@ export default class AuthenticationPage extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleEmailChange(e) {
|
handleEmailChange(e) {
|
||||||
this.setState({email: e.target.value});
|
this.setState({ email: e.target.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
height: 45px;
|
height: 45px;
|
||||||
border-top: 1px solid #e8eae8;
|
border-top: 1px solid #e8eae8;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
.controlPane {
|
.controlPane {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
|
@ -27,14 +27,14 @@ export default class EntryEditor extends React.Component {
|
|||||||
calculateHeight() {
|
calculateHeight() {
|
||||||
const height = window.innerHeight - 54;
|
const height = window.innerHeight - 54;
|
||||||
console.log('setting height to %s', height);
|
console.log('setting height to %s', height);
|
||||||
this.setState({height});
|
this.setState({ height });
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { collection, entry, getMedia, onChange, onAddMedia, onRemoveMedia, onPersist } = this.props;
|
const { collection, entry, getMedia, onChange, onAddMedia, onRemoveMedia, onPersist } = this.props;
|
||||||
const {height} = this.state;
|
const { height } = this.state;
|
||||||
|
|
||||||
return <div className={styles.entryEditor} style={{height}}>
|
return <div className={styles.entryEditor} style={{ height }}>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.controlPane}>
|
<div className={styles.controlPane}>
|
||||||
<ControlPane
|
<ControlPane
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
.frame {
|
.frame {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
border: none;
|
border: none;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ export default class PreviewPane extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderPreview() {
|
renderPreview() {
|
||||||
const props = Object.assign({}, this.props, {widgetFor: this.widgetFor});
|
const props = Object.assign({}, this.props, { widgetFor: this.widgetFor });
|
||||||
const component = registry.getPreviewTemplate(props.collection.get('name')) || Preview;
|
const component = registry.getPreviewTemplate(props.collection.get('name')) || Preview;
|
||||||
|
|
||||||
render(React.createElement(component, props), this.previewEl);
|
render(React.createElement(component, props), this.previewEl);
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
|
@import "../theme.css";
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
composes: base from "../theme.css";
|
composes: base container rounded depth;
|
||||||
composes: container from "../theme.css";
|
|
||||||
composes: rounded from "../theme.css";
|
|
||||||
composes: depth from "../theme.css";
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
width: 240px;
|
width: 240px;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ const availableIcons = [
|
|||||||
'bold', 'italic', 'list', 'font', 'text-height', 'text-width', 'align-left', 'align-center', 'align-right',
|
'bold', 'italic', 'list', 'font', 'text-height', 'text-width', 'align-left', 'align-center', 'align-right',
|
||||||
'align-justify', 'indent-left', 'indent-right', 'list-bullet', 'list-numbered', 'strike', 'underline', 'table',
|
'align-justify', 'indent-left', 'indent-right', 'list-bullet', 'list-numbered', 'strike', 'underline', 'table',
|
||||||
'superscript', 'subscript', 'header', 'h1', 'h2', 'paragraph', 'link', 'unlink', 'quote-left', 'quote-right', 'code',
|
'superscript', 'subscript', 'header', 'h1', 'h2', 'paragraph', 'link', 'unlink', 'quote-left', 'quote-right', 'code',
|
||||||
'picture','video',
|
'picture', 'video',
|
||||||
// Entypo
|
// Entypo
|
||||||
'note', 'note-beamed',
|
'note', 'note-beamed',
|
||||||
'music',
|
'music',
|
||||||
@ -199,7 +199,7 @@ const iconPropType = (props, propName) => {
|
|||||||
|
|
||||||
const noop = function() {};
|
const noop = function() {};
|
||||||
|
|
||||||
export default function Icon({ style, className = '', type, onClick = noop}) {
|
export default function Icon({ style, className = '', type, onClick = noop }) {
|
||||||
return <span className={`${styles.root} ${styles[type]} ${className}`} style={style} onClick={onClick} />;
|
return <span className={`${styles.root} ${styles[type]} ${className}`} style={style} onClick={onClick} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export { default as Card } from './card/Card';
|
export { default as Card } from './card/Card';
|
||||||
export { default as Loader } from './loader/Loader';
|
export { default as Loader } from './loader/Loader';
|
||||||
export { default as Icon } from './icon/Icon';
|
export { default as Icon } from './icon/Icon';
|
||||||
|
export { default as Toast } from './toast/Toast';
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
:root {
|
:root {
|
||||||
--defaultColor: #333;
|
--defaultColor: #333;
|
||||||
|
--defaultColorLight: #eee;
|
||||||
--backgroundColor: #fff;
|
--backgroundColor: #fff;
|
||||||
--shadowColor: rgba(0, 0, 0, 0.117647);
|
--shadowColor: rgba(0, 0, 0, 0.117647);
|
||||||
--successColor: #1c7;
|
--successColor: #1c7;
|
||||||
|
40
src/components/UI/toast/Toast.css
Normal file
40
src/components/UI/toast/Toast.css
Normal file
@ -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);
|
||||||
|
}
|
74
src/components/UI/toast/Toast.js
Normal file
74
src/components/UI/toast/Toast.js
Normal file
@ -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 = <Icon type={icons[type]} className={styles.icon} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.state.shown) {
|
||||||
|
classes.push(styles.hidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.join(' ')} style={style}>{icon}{children}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.propTypes = {
|
||||||
|
style: PropTypes.object,
|
||||||
|
type: PropTypes.oneOf(['success', 'warning', 'error']).isRequired,
|
||||||
|
className: PropTypes.string,
|
||||||
|
show: PropTypes.bool,
|
||||||
|
children: PropTypes.node
|
||||||
|
};
|
@ -1,23 +1,32 @@
|
|||||||
|
.container {
|
||||||
|
display: table;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.column {
|
.column {
|
||||||
position: relative;
|
display: table-cell;
|
||||||
display: inline-block;
|
|
||||||
vertical-align: top;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 28%;
|
width: 33%;
|
||||||
|
height: 100%;
|
||||||
|
transition: background-color .5s ease;
|
||||||
& h2 {
|
& h2 {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.highlighted {
|
||||||
|
background-color: #e1eeea;
|
||||||
|
}
|
||||||
|
|
||||||
.column:not(:last-child) {
|
.column:not(:last-child) {
|
||||||
margin-right: 8%;
|
padding-right: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
margin: 7px 0;
|
margin: 7px 0;
|
||||||
|
|
||||||
& h1 {
|
& h2 {
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
& small {
|
& small {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@ -29,4 +38,16 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& button {
|
||||||
|
margin: 10px 10px 0 0;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.clear::after {
|
||||||
|
content:"";
|
||||||
|
display:block;
|
||||||
|
clear:both;
|
||||||
}
|
}
|
||||||
|
@ -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 ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Card } from './UI';
|
import { Card } from './UI';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { statusDescriptions } from '../constants/publishModes';
|
import { status, statusDescriptions } from '../constants/publishModes';
|
||||||
import styles from './UnpublishedListing.css';
|
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().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(
|
||||||
|
<div>
|
||||||
|
<Card className={styles.card}>
|
||||||
|
{children}
|
||||||
|
{(ownStatus === status.last()) &&
|
||||||
|
<button onClick={onRequestPublish}>Publish now</button>
|
||||||
|
}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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) {
|
renderColumns(entries, column) {
|
||||||
if (!entries) return;
|
if (!entries) return;
|
||||||
|
|
||||||
if (!column) {
|
if (!column) {
|
||||||
return entries.entrySeq().map(([currColumn, currEntries]) => (
|
return entries.entrySeq().map(([currColumn, currEntries]) => (
|
||||||
<div key={currColumn} className={styles.column}>
|
<Column
|
||||||
<h2>{statusDescriptions.get(currColumn)}</h2>
|
key={currColumn}
|
||||||
|
status={currColumn}
|
||||||
|
onChangeStatus={this.props.handleChangeStatus}
|
||||||
|
>
|
||||||
{this.renderColumns(currEntries, currColumn)}
|
{this.renderColumns(currEntries, currColumn)}
|
||||||
</div>
|
</Column>
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
return <div>
|
return <div>
|
||||||
@ -24,11 +107,20 @@ export default class UnpublishedListing extends React.Component {
|
|||||||
const author = entry.getIn(['data', 'author'], entry.getIn(['metaData', 'user']));
|
const author = entry.getIn(['data', 'author'], entry.getIn(['metaData', 'user']));
|
||||||
const timeStamp = moment(entry.getIn(['metaData', 'timeStamp'])).format('llll');
|
const timeStamp = moment(entry.getIn(['metaData', 'timeStamp'])).format('llll');
|
||||||
const link = `/editorialworkflow/${entry.getIn(['metaData', 'collection'])}/${entry.getIn(['metaData', 'status'])}/${entry.get('slug')}`;
|
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 (
|
return (
|
||||||
<Card key={entry.get('slug')} className={styles.card}>
|
<EntryCard
|
||||||
<h1><Link to={link}>{entry.getIn(['data', 'title'])}</Link> <small>by {author}</small></h1>
|
key={slug}
|
||||||
|
slug={slug}
|
||||||
|
ownStatus={status}
|
||||||
|
collection={collection}
|
||||||
|
onRequestPublish={this.requestPublish.bind(this, collection, slug, status)} // eslint-disable-line
|
||||||
|
>
|
||||||
|
<h2><Link to={link}>{entry.getIn(['data', 'title'])}</Link> <small>by {author}</small></h2>
|
||||||
<p>Last updated: {timeStamp} by {entry.getIn(['metaData', 'user'])}</p>
|
<p>Last updated: {timeStamp} by {entry.getIn(['metaData', 'user'])}</p>
|
||||||
</Card>
|
</EntryCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
@ -38,11 +130,12 @@ export default class UnpublishedListing extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const columns = this.renderColumns(this.props.entries);
|
const columns = this.renderColumns(this.props.entries);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={styles.clear}>
|
||||||
<h1>Editorial Workflow</h1>
|
<h1>Editorial Workflow</h1>
|
||||||
|
<div className={styles.container}>
|
||||||
{columns}
|
{columns}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -50,4 +143,8 @@ export default class UnpublishedListing extends React.Component {
|
|||||||
|
|
||||||
UnpublishedListing.propTypes = {
|
UnpublishedListing.propTypes = {
|
||||||
entries: ImmutablePropTypes.orderedMap,
|
entries: ImmutablePropTypes.orderedMap,
|
||||||
|
handleChangeStatus: PropTypes.func.isRequired,
|
||||||
|
handlePublish: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default DragDropContext(HTML5Backend)(UnpublishedListing);
|
||||||
|
@ -60,7 +60,7 @@ class MarkdownControl extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
{ this.renderEditor() }
|
{this.renderEditor()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -62,4 +62,4 @@ export const SCHEMA = {
|
|||||||
borderRadius: '4px'
|
borderRadius: '4px'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
@ -46,7 +46,7 @@ function processEditorPlugins(plugins) {
|
|||||||
<div {...props.attributes} className={className}>
|
<div {...props.attributes} className={className}>
|
||||||
<div className="plugin_icon" contentEditable={false}><Icon type={plugin.icon}/></div>
|
<div className="plugin_icon" contentEditable={false}><Icon type={plugin.icon}/></div>
|
||||||
<div className="plugin_fields" contentEditable={false}>
|
<div className="plugin_fields" contentEditable={false}>
|
||||||
{ plugin.fields.map(field => `${field.label}: “${node.data.get(field.name)}”`) }
|
{plugin.fields.map(field => `${field.label}: “${node.data.get(field.name)}”`)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -39,4 +39,4 @@ storiesOf('Card', module)
|
|||||||
<p>header and footer elements are also not subject to margin</p>
|
<p>header and footer elements are also not subject to margin</p>
|
||||||
<footer style={styles.footer}>© Thousand Cats Corp</footer>
|
<footer style={styles.footer}>© Thousand Cats Corp</footer>
|
||||||
</Card>
|
</Card>
|
||||||
))
|
));
|
||||||
|
19
src/components/stories/Toast.js
Normal file
19
src/components/stories/Toast.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Toast } from '../UI';
|
||||||
|
import { storiesOf } from '@kadira/storybook';
|
||||||
|
|
||||||
|
|
||||||
|
storiesOf('Toast', module)
|
||||||
|
.add('Success', () => (
|
||||||
|
<div>
|
||||||
|
<Toast type='success' show>A Toast Message</Toast>
|
||||||
|
</div>
|
||||||
|
)).add('Waring', () => (
|
||||||
|
<div>
|
||||||
|
<Toast type='warning' show>A Toast Message</Toast>
|
||||||
|
</div>
|
||||||
|
)).add('Error', () => (
|
||||||
|
<div>
|
||||||
|
<Toast type='error' show>A Toast Message</Toast>
|
||||||
|
</div>
|
||||||
|
));
|
@ -1,2 +1,3 @@
|
|||||||
import './Card';
|
import './Card';
|
||||||
import './Icon';
|
import './Icon';
|
||||||
|
import './Toast';
|
||||||
|
@ -4,6 +4,7 @@ import { IndexLink } from 'react-router';
|
|||||||
import { loadConfig } from '../actions/config';
|
import { loadConfig } from '../actions/config';
|
||||||
import { loginUser } from '../actions/auth';
|
import { loginUser } from '../actions/auth';
|
||||||
import { currentBackend } from '../backends/backend';
|
import { currentBackend } from '../backends/backend';
|
||||||
|
import { Loader } from '../components/UI';
|
||||||
import { SHOW_COLLECTION, CREATE_COLLECTION, HELP } from '../actions/findbar';
|
import { SHOW_COLLECTION, CREATE_COLLECTION, HELP } from '../actions/findbar';
|
||||||
import FindBar from './FindBar';
|
import FindBar from './FindBar';
|
||||||
import styles from './App.css';
|
import styles from './App.css';
|
||||||
@ -27,7 +28,7 @@ class App extends React.Component {
|
|||||||
|
|
||||||
configLoading() {
|
configLoading() {
|
||||||
return <div>
|
return <div>
|
||||||
<h1>Loading configuration...</h1>
|
<Loader active>Loading configuration...</Loader>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,7 +13,12 @@ class FindBar extends Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this._compiledCommands = [];
|
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 = {
|
this.state = {
|
||||||
value: '',
|
value: '',
|
||||||
placeholder: PLACEHOLDER,
|
placeholder: PLACEHOLDER,
|
||||||
@ -68,7 +73,7 @@ class FindBar extends Component {
|
|||||||
|
|
||||||
if (match && match[1]) {
|
if (match && match[1]) {
|
||||||
regexp += '(.*)';
|
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, {
|
return Object.assign({}, command, {
|
||||||
@ -144,6 +149,7 @@ class FindBar extends Component {
|
|||||||
getSuggestions() {
|
getSuggestions() {
|
||||||
return this._getSuggestions(this.state.value, this.state.activeScope, this._compiledCommands, this.props.defaultCommands);
|
return this._getSuggestions(this.state.value, this.state.activeScope, this._compiledCommands, this.props.defaultCommands);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Memoized version
|
// Memoized version
|
||||||
_getSuggestions(value, scope, commands, defaultCommands) {
|
_getSuggestions(value, scope, commands, defaultCommands) {
|
||||||
if (scope) return []; // No autocomplete for scoped input
|
if (scope) return []; // No autocomplete for scoped input
|
||||||
@ -152,7 +158,7 @@ class FindBar extends Component {
|
|||||||
.filter(command => defaultCommands.indexOf(command.id) !== -1)
|
.filter(command => defaultCommands.indexOf(command.id) !== -1)
|
||||||
.map(result => (
|
.map(result => (
|
||||||
Object.assign({}, result, { string: result.token }
|
Object.assign({}, result, { string: result.token }
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = fuzzy.filter(value, commands, {
|
const results = fuzzy.filter(value, commands, {
|
||||||
@ -162,8 +168,8 @@ class FindBar extends Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const returnResults = results.slice(0, 4).map(result => (
|
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);
|
returnResults.push(this._searchCommand);
|
||||||
|
|
||||||
return returnResults;
|
return returnResults;
|
||||||
@ -178,7 +184,7 @@ class FindBar extends Component {
|
|||||||
index = (
|
index = (
|
||||||
highlightedIndex === this.getSuggestions().length - 1 ||
|
highlightedIndex === this.getSuggestions().length - 1 ||
|
||||||
this.state.isOpen === false
|
this.state.isOpen === false
|
||||||
) ? 0 : highlightedIndex + 1;
|
) ? 0 : highlightedIndex + 1;
|
||||||
this.setState({
|
this.setState({
|
||||||
highlightedIndex: index,
|
highlightedIndex: index,
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
@ -290,7 +296,7 @@ class FindBar extends Component {
|
|||||||
let children;
|
let children;
|
||||||
if (!command.search) {
|
if (!command.search) {
|
||||||
children = (
|
children = (
|
||||||
<span><span dangerouslySetInnerHTML={{__html: command.string}} /></span>
|
<span><span dangerouslySetInnerHTML={{ __html: command.string }}/></span>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
children = (
|
children = (
|
||||||
@ -299,7 +305,8 @@ class FindBar extends Component {
|
|||||||
<span><Icon type="search"/>Search... </span> :
|
<span><Icon type="search"/>Search... </span> :
|
||||||
<span className={styles.faded}><Icon type="search"/>Search for: </span>
|
<span className={styles.faded}><Icon type="search"/>Search for: </span>
|
||||||
}
|
}
|
||||||
<strong>{this.state.value}</strong></span>
|
<strong>{this.state.value}</strong>
|
||||||
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@ -317,7 +324,7 @@ class FindBar extends Component {
|
|||||||
return commands.length === 0 ? null : (
|
return commands.length === 0 ? null : (
|
||||||
<div className={styles.menu}>
|
<div className={styles.menu}>
|
||||||
<div className={styles.suggestions}>
|
<div className={styles.suggestions}>
|
||||||
{ commands }
|
{commands}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.history}>
|
<div className={styles.history}>
|
||||||
Your past searches and commands
|
Your past searches and commands
|
||||||
@ -328,7 +335,7 @@ class FindBar extends Component {
|
|||||||
|
|
||||||
renderActiveScope() {
|
renderActiveScope() {
|
||||||
if (this.state.activeScope === SEARCH) {
|
if (this.state.activeScope === SEARCH) {
|
||||||
return <div className={styles.inputScope}><Icon type="search"/> </div>;
|
return <div className={styles.inputScope}><Icon type="search"/></div>;
|
||||||
} else {
|
} else {
|
||||||
return <div className={styles.inputScope}>{this.state.activeScope}</div>;
|
return <div className={styles.inputScope}>{this.state.activeScope}</div>;
|
||||||
}
|
}
|
||||||
@ -358,6 +365,7 @@ class FindBar extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FindBar.propTypes = {
|
FindBar.propTypes = {
|
||||||
commands: PropTypes.arrayOf(PropTypes.shape({
|
commands: PropTypes.arrayOf(PropTypes.shape({
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { PropTypes } from 'react';
|
import React, { PropTypes } from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { OrderedMap } from 'immutable';
|
import { OrderedMap } from 'immutable';
|
||||||
import { loadUnpublishedEntries } from '../../actions/editorialWorkflow';
|
import { loadUnpublishedEntries, updateUnpublishedEntryStatus, publishUnpublishedEntry } from '../../actions/editorialWorkflow';
|
||||||
import { selectUnpublishedEntries } from '../../reducers';
|
import { selectUnpublishedEntries } from '../../reducers';
|
||||||
import { EDITORIAL_WORKFLOW, status } from '../../constants/publishModes';
|
import { EDITORIAL_WORKFLOW, status } from '../../constants/publishModes';
|
||||||
import UnpublishedListing from '../../components/UnpublishedListing';
|
import UnpublishedListing from '../../components/UnpublishedListing';
|
||||||
@ -20,12 +20,16 @@ export default function CollectionPageHOC(CollectionPage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isEditorialWorkflow, unpublishedEntries } = this.props;
|
const { isEditorialWorkflow, unpublishedEntries, updateUnpublishedEntryStatus, publishUnpublishedEntry } = this.props;
|
||||||
if (!isEditorialWorkflow) return super.render();
|
if (!isEditorialWorkflow) return super.render();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.alignable}>
|
<div className={styles.alignable}>
|
||||||
<UnpublishedListing entries={unpublishedEntries}/>
|
<UnpublishedListing
|
||||||
|
entries={unpublishedEntries}
|
||||||
|
handleChangeStatus={updateUnpublishedEntryStatus}
|
||||||
|
handlePublish={publishUnpublishedEntry}
|
||||||
|
/>
|
||||||
{super.render()}
|
{super.render()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -56,5 +60,8 @@ export default function CollectionPageHOC(CollectionPage) {
|
|||||||
return returnObj;
|
return returnObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
return connect(mapStateToProps)(CollectionPageHOC);
|
return connect(mapStateToProps, {
|
||||||
|
updateUnpublishedEntryStatus,
|
||||||
|
publishUnpublishedEntry
|
||||||
|
})(CollectionPageHOC);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { EDITORIAL_WORKFLOW } from '../../constants/publishModes';
|
import { EDITORIAL_WORKFLOW } from '../../constants/publishModes';
|
||||||
import { selectUnpublishedEntry } from '../../reducers';
|
import { selectUnpublishedEntry } from '../../reducers';
|
||||||
import { loadUnpublishedEntry } from '../../actions/editorialWorkflow';
|
import { loadUnpublishedEntry, persistUnpublishedEntry } from '../../actions/editorialWorkflow';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
export default function EntryPageHOC(EntryPage) {
|
export default function EntryPageHOC(EntryPage) {
|
||||||
@ -22,10 +22,6 @@ export default function EntryPageHOC(EntryPage) {
|
|||||||
const slug = ownProps.params.slug;
|
const slug = ownProps.params.slug;
|
||||||
const entry = selectUnpublishedEntry(state, status, slug);
|
const entry = selectUnpublishedEntry(state, status, slug);
|
||||||
returnObj.entry = entry;
|
returnObj.entry = entry;
|
||||||
|
|
||||||
returnObj.persistEntry = () => {
|
|
||||||
// TODO - for now, simply ignore
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return returnObj;
|
return returnObj;
|
||||||
}
|
}
|
||||||
@ -39,6 +35,10 @@ export default function EntryPageHOC(EntryPage) {
|
|||||||
returnObj.loadEntry = (collection, slug) => {
|
returnObj.loadEntry = (collection, slug) => {
|
||||||
dispatch(loadUnpublishedEntry(collection, status, slug));
|
dispatch(loadUnpublishedEntry(collection, status, slug));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
returnObj.persistEntry = (collection, entryDraft) => {
|
||||||
|
dispatch(persistUnpublishedEntry(collection, entryDraft));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return returnObj;
|
return returnObj;
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,6 @@ export default class YAML {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toFile(data) {
|
toFile(data) {
|
||||||
return yaml.safeDump(data, {schema: OutputSchema});
|
return yaml.safeDump(data, { schema: OutputSchema });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {List} from 'immutable';
|
import { List } from 'immutable';
|
||||||
import {newEditorPlugin} from '../components/Widgets/MarkdownControlElements/plugins';
|
import { newEditorPlugin } from '../components/Widgets/MarkdownControlElements/plugins';
|
||||||
|
|
||||||
const _registry = {
|
const _registry = {
|
||||||
templates: {},
|
templates: {},
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
import { Map, List, fromJS } from 'immutable';
|
import { Map, List, fromJS } from 'immutable';
|
||||||
import { EDITORIAL_WORKFLOW } from '../constants/publishModes';
|
import { EDITORIAL_WORKFLOW } from '../constants/publishModes';
|
||||||
import {
|
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';
|
} from '../actions/editorialWorkflow';
|
||||||
import { CONFIG_SUCCESS } from '../actions/config';
|
import { CONFIG_SUCCESS } from '../actions/config';
|
||||||
|
|
||||||
@ -39,6 +44,23 @@ const unpublishedEntries = (state = null, action) => {
|
|||||||
ids: List(entries.map((entry) => entry.slug))
|
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:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -16,15 +16,15 @@ describe('auth', () => {
|
|||||||
expect(
|
expect(
|
||||||
auth(undefined, authenticating())
|
auth(undefined, authenticating())
|
||||||
).toEqual(
|
).toEqual(
|
||||||
Immutable.Map({isFetching: true})
|
Immutable.Map({ isFetching: true })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle authentication', () => {
|
it('should handle authentication', () => {
|
||||||
expect(
|
expect(
|
||||||
auth(undefined, authenticate({email: 'joe@example.com'}))
|
auth(undefined, authenticate({ email: 'joe@example.com' }))
|
||||||
).toEqual(
|
).toEqual(
|
||||||
Immutable.fromJS({user: {email: 'joe@example.com'}})
|
Immutable.fromJS({ user: { email: 'joe@example.com' } })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -15,39 +15,39 @@ describe('collections', () => {
|
|||||||
|
|
||||||
it('should load the collections from the config', () => {
|
it('should load the collections from the config', () => {
|
||||||
expect(
|
expect(
|
||||||
collections(undefined, configLoaded({collections: [
|
collections(undefined, configLoaded({ collections: [
|
||||||
{name: 'posts', folder: '_posts', fields: [{name: 'title', widget: 'string'}]}
|
{ name: 'posts', folder: '_posts', fields: [{ name: 'title', widget: 'string' }] }
|
||||||
]}))
|
] }))
|
||||||
).toEqual(
|
).toEqual(
|
||||||
OrderedMap({
|
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', () => {
|
it('should mark entries as loading', () => {
|
||||||
const state = OrderedMap({
|
const state = OrderedMap({
|
||||||
'posts': Map({name: 'posts'})
|
'posts': Map({ name: 'posts' })
|
||||||
});
|
});
|
||||||
expect(
|
expect(
|
||||||
collections(state, entriesLoading(Map({name: 'posts'})))
|
collections(state, entriesLoading(Map({ name: 'posts' })))
|
||||||
).toEqual(
|
).toEqual(
|
||||||
OrderedMap({
|
OrderedMap({
|
||||||
'posts': Map({name: 'posts', isFetching: true})
|
'posts': Map({ name: 'posts', isFetching: true })
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle loaded entries', () => {
|
it('should handle loaded entries', () => {
|
||||||
const state = OrderedMap({
|
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(
|
expect(
|
||||||
collections(state, entriesLoaded(Map({name: 'posts'}), entries))
|
collections(state, entriesLoaded(Map({ name: 'posts' }), entries))
|
||||||
).toEqual(
|
).toEqual(
|
||||||
OrderedMap({
|
OrderedMap({
|
||||||
'posts': fromJS({name: 'posts', entries: entries})
|
'posts': fromJS({ name: 'posts', entries: entries })
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -14,9 +14,9 @@ describe('config', () => {
|
|||||||
|
|
||||||
it('should handle an update', () => {
|
it('should handle an update', () => {
|
||||||
expect(
|
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(
|
).toEqual(
|
||||||
Immutable.Map({'a': 'changed', 'e': 'new'})
|
Immutable.Map({ 'a': 'changed', 'e': 'new' })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -24,15 +24,15 @@ describe('config', () => {
|
|||||||
expect(
|
expect(
|
||||||
config(undefined, configLoading())
|
config(undefined, configLoading())
|
||||||
).toEqual(
|
).toEqual(
|
||||||
Immutable.Map({isFetching: true})
|
Immutable.Map({ isFetching: true })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle an error', () => {
|
it('should handle an error', () => {
|
||||||
expect(
|
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(
|
).toEqual(
|
||||||
Immutable.Map({error: 'Error: Config could not be loaded'})
|
Immutable.Map({ error: 'Error: Config could not be loaded' })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user