Merge branch 'react-pr' of https://github.com/netlify/netlify-cms into react-pr

This commit is contained in:
Cássio Zen 2016-09-13 14:31:26 -03:00
commit 4108b88315
24 changed files with 193 additions and 124 deletions

View File

@ -4,13 +4,23 @@
"description": "Netlify CMS lets content editors work on structured content stored in git", "description": "Netlify CMS lets content editors work on structured content stored in git",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "webpack-dev-server -d --inline --config webpack.config.js", "start": "webpack-dev-server --config webpack.config.js",
"test": "NODE_ENV=test mocha --recursive --compilers js:babel-register --require ./test/setup.js", "test": "NODE_ENV=test mocha --recursive --compilers js:babel-register --require ./test/setup.js",
"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,24 +42,24 @@
"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",
"extract-text-webpack-plugin": "^1.0.1",
"express": "^4.13.4",
"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-immutable-proptypes": "^1.6.0", "react-immutable-proptypes": "^1.6.0",
"react-lazy-load": "^3.0.3", "react-lazy-load": "^3.0.3",
"react-pure-render": "^1.0.2", "react-pure-render": "^1.0.2",
@ -60,8 +70,8 @@
"redux-thunk": "^1.0.3", "redux-thunk": "^1.0.3",
"style-loader": "^0.13.0", "style-loader": "^0.13.0",
"url-loader": "^0.5.7", "url-loader": "^0.5.7",
"webpack": "^1.12.13", "webpack": "^1.13.2",
"webpack-dev-server": "^1.14.1", "webpack-dev-server": "^1.15.1",
"webpack-postcss-tools": "^1.1.1", "webpack-postcss-tools": "^1.1.1",
"whatwg-fetch": "^1.0.0" "whatwg-fetch": "^1.0.0"
}, },

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

@ -60,7 +60,7 @@ class MarkdownControl extends React.Component {
return ( return (
<div> <div>
{ this.renderEditor() } {this.renderEditor()}
</div> </div>
); );
} }

View File

@ -62,4 +62,4 @@ export const SCHEMA = {
borderRadius: '4px' borderRadius: '4px'
} }
} }
} };

View File

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

View File

@ -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}>&copy; Thousand Cats Corp</footer> <footer style={styles.footer}>&copy; Thousand Cats Corp</footer>
</Card> </Card>
)) ));

View File

@ -57,7 +57,7 @@ class EntryPage extends React.Component {
const { const {
entry, entryDraft, boundGetMedia, collection, changeDraft, addMedia, removeMedia entry, entryDraft, boundGetMedia, collection, changeDraft, addMedia, removeMedia
} = this.props; } = this.props;
if (entryDraft == null || entryDraft.get('entry') == undefined || entry && entry.get('isFetching')) { if (entryDraft == null || entryDraft.get('entry') == undefined || entry && entry.get('isFetching')) {
return <div>Loading...</div>; return <div>Loading...</div>;
} }

View File

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

View File

@ -41,6 +41,6 @@ export default class YAML {
} }
toFile(data) { toFile(data) {
return yaml.safeDump(data, {schema: OutputSchema}); return yaml.safeDump(data, { schema: OutputSchema });
} }
} }

View File

@ -3,8 +3,9 @@ html {
-ms-text-size-adjust: 100%; -ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
margin: 0; margin: 0;
font-family: Roboto,"Helvetica Neue",HelveticaNeue,Helvetica,Arial,sans-serif; font-family: Roboto, "Helvetica Neue", HelveticaNeue, Helvetica, Arial, sans-serif;
} }
*, *:before, *:after { *, *:before, *:after {
box-sizing: inherit; box-sizing: inherit;
} }
@ -13,15 +14,15 @@ body {
font-family: 'Roboto', sans-serif; font-family: 'Roboto', sans-serif;
height: 100%; height: 100%;
background-color: #f2f5f4; background-color: #f2f5f4;
color:#7c8382; color: #7c8382;
margin: 0; margin: 0;
} }
header { header {
background-color: #272e30; background-color: #272e30;
box-shadow: 0 1px 2px 0 rgba(0,0,0,0.22); box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.22);
height: 54px; height: 54px;
border-bottom:2px solid #3ab7a5; border-bottom: 2px solid #3ab7a5;
position: fixed; position: fixed;
width: 100%; width: 100%;
z-index: 999; z-index: 999;
@ -35,23 +36,24 @@ h1, h2, h3, h4, h5, h6, p {
margin: 0; margin: 0;
} }
h1{ h1 {
color: #3ab7a5; color: #3ab7a5;
border-bottom: 1px solid #3ab7a5; border-bottom: 1px solid #3ab7a5;
margin: 30px auto 25px; margin: 30px auto 25px;
padding-bottom: 15px; padding-bottom: 15px;
font-size: 25px; font-size: 25px;
} }
header input{ header input {
margin-bottom:0; margin-bottom: 0;
} }
button{
button {
border: 1px solid #3ab7a5; border: 1px solid #3ab7a5;
padding: 3px 20px; padding: 3px 20px;
font-size: 12px; font-size: 12px;
line-height: 18px; line-height: 18px;
background-color:#fff; background-color: #fff;
cursor: pointer; cursor: pointer;
} }
@ -120,7 +122,7 @@ button{
margin-top: 1px; margin-top: 1px;
z-index: 99999 !important; z-index: 99999 !important;
background: #fff; background: #fff;
box-shadow: 0 1px 3px rgba(0,0,0,.1); box-shadow: 0 1px 3px rgba(0, 0, 0, .1);
border: 1px solid #f9f9f9; border: 1px solid #f9f9f9;
} }
& .rdtOpen .rdtPicker { & .rdtOpen .rdtPicker {
@ -217,10 +219,10 @@ button{
& .rdtNext span { & .rdtNext span {
display: block; display: block;
-webkit-touch-callout: none; /* iOS Safari */ -webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Chrome/Safari/Opera */ -webkit-user-select: none; /* Chrome/Safari/Opera */
-khtml-user-select: none; /* Konqueror */ -khtml-user-select: none; /* Konqueror */
-moz-user-select: none; /* Firefox */ -moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */ -ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; user-select: none;
} }
@ -293,10 +295,10 @@ button{
display: block; display: block;
-webkit-touch-callout: none; /* iOS Safari */ -webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Chrome/Safari/Opera */ -webkit-user-select: none; /* Chrome/Safari/Opera */
-khtml-user-select: none; /* Konqueror */ -khtml-user-select: none; /* Konqueror */
-moz-user-select: none; /* Firefox */ -moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */ -ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; user-select: none;
} }
& .rdtCounter .rdtBtn:hover { & .rdtCounter .rdtBtn:hover {

View File

@ -1,31 +1,33 @@
import React from 'react'; import React from 'react';
import { render } from 'react-dom'; import { render } from 'react-dom';
import { Provider } from 'react-redux'; import { AppContainer } from 'react-hot-loader'
import { Router } from 'react-router'; import Root from './root'
import registry from './lib/registry'; import registry from './lib/registry';
import configureStore from './store/configureStore';
import routes from './routing/routes';
import history, { syncHistory } from './routing/history';
import 'file?name=index.html!../example/index.html'; import 'file?name=index.html!../example/index.html';
import './index.css'; import './index.css';
const store = configureStore(); // Create mount element dynamically
// Create an enhanced history that syncs navigation events with the store
syncHistory(store);
const el = document.createElement('div'); const el = document.createElement('div');
el.id = 'root'; el.id = 'root';
document.body.appendChild(el); document.body.appendChild(el);
render(( render((
<Provider store={store}> <AppContainer>
<Router history={history}> <Root />
{routes} </AppContainer>
</Router>
</Provider>
), el); ), el);
if (module.hot) {
module.hot.accept('./root', () => {
const NextRoot = require('./root').default;
render((
<AppContainer>
<NextRoot />
</AppContainer>
), el);
});
}
window.CMS = {}; window.CMS = {};
console.log('reg: ', registry); console.log('reg: ', registry);
for (const method in registry) { for (const method in registry) {

View File

@ -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: {},

View File

@ -0,0 +1,8 @@
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import reducers from '.';
export default combineReducers({
...reducers,
routing: routerReducer
});

21
src/root.js Normal file
View File

@ -0,0 +1,21 @@
import React from 'react';
import { Provider } from 'react-redux';
import { Router } from 'react-router';
import routes from './routing/routes';
import history, { syncHistory } from './routing/history';
import configureStore from './store/configureStore';
const store = configureStore();
// Create an enhanced history that syncs navigation events with the store
syncHistory(store);
const Root = () => (
<Provider store={store}>
<Router history={history}>
{routes}
</Router>
</Provider>
);
export default Root;

View File

@ -1,18 +1,20 @@
import { createStore, applyMiddleware, combineReducers, compose } from 'redux'; import { createStore, applyMiddleware, compose } from 'redux';
import thunkMiddleware from 'redux-thunk'; import thunkMiddleware from 'redux-thunk';
import { routerReducer } from 'react-router-redux'; import reducer from '../reducers/combinedReducer';
import reducers from '../reducers';
const reducer = combineReducers({ export default function configureStore(initialState) {
...reducers, const store = createStore(reducer, initialState, compose(
routing: routerReducer applyMiddleware(thunkMiddleware),
}); window.devToolsExtension ? window.devToolsExtension() : f => f
));
const createStoreWithMiddleware = compose( if (module.hot) {
applyMiddleware(thunkMiddleware), // Enable Webpack hot module replacement for reducers
window.devToolsExtension ? window.devToolsExtension() : (f) => f module.hot.accept('../reducers/combinedReducer', () => {
)(createStore); const nextReducer = require('../reducers/combinedReducer') // eslint-disable-line
store.replaceReducer(nextReducer);
});
}
export default (initialState) => ( return store;
createStoreWithMiddleware(reducer, initialState) }
);

View File

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

View File

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

View File

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

View File

@ -1,8 +1,10 @@
/* global module, __dirname, require */ /* global module, __dirname, require */
var webpack = require('webpack'); var webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var path = require('path'); var path = require('path');
const HOST = 'localhost';
const PORT = '8080';
module.exports = { module.exports = {
module: { module: {
loaders: [ loaders: [
@ -13,7 +15,7 @@ module.exports = {
{ test: /\.json$/, loader: 'json-loader' }, { test: /\.json$/, loader: 'json-loader' },
{ {
test: /\.css$/, test: /\.css$/,
loader: ExtractTextPlugin.extract("style", "css?modules&importLoaders=1&&localIdentName=cms__[name]__[local]!postcss"), loader: 'style!css?modules&importLoaders=1&&localIdentName=cms__[name]__[local]!postcss',
}, },
{ {
loader: 'babel', loader: 'babel',
@ -22,7 +24,13 @@ module.exports = {
query: { query: {
cacheDirectory: true, cacheDirectory: true,
presets: ['react', 'es2015'], presets: ['react', 'es2015'],
plugins: ['transform-class-properties', 'transform-object-assign', 'transform-object-rest-spread', 'lodash'] plugins: [
'transform-class-properties',
'transform-object-assign',
'transform-object-rest-spread',
'lodash',
'react-hot-loader/babel'
]
} }
} }
] ]
@ -34,7 +42,9 @@ module.exports = {
], ],
plugins: [ plugins: [
new ExtractTextPlugin('cms.css', { allChunks: true }), new webpack.optimize.OccurenceOrderPlugin(),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin(),
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch' 'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch'
}) })
@ -42,16 +52,23 @@ module.exports = {
context: path.join(__dirname, 'src'), context: path.join(__dirname, 'src'),
entry: { entry: {
cms: './index', cms: [
'webpack/hot/dev-server',
`webpack-dev-server/client?http://${HOST}:${PORT}/`,
'react-hot-loader/patch',
'./index'
],
}, },
output: { output: {
path: path.join(__dirname, 'dist'), path: path.join(__dirname, 'dist'),
filename: '[name].js' filename: '[name].js',
publicPath: `http://${HOST}:${PORT}/`,
}, },
externals: [/^vendor\/.+\.js$/], externals: [/^vendor\/.+\.js$/],
devServer: { devServer: {
hot: true,
contentBase: 'example/', contentBase: 'example/',
historyApiFallback: true, historyApiFallback: true,
devTool: 'source-map' devTool: 'cheap-module-source-map'
}, },
}; };