Test repo can now be used to list entries

This commit is contained in:
Mathias Biilmann Christensen 2016-02-25 20:40:35 -08:00
parent 67cdd92bfb
commit 978b7290c5
17 changed files with 363 additions and 41 deletions

View File

@ -2,9 +2,44 @@
<html>
<head>
<title>This is an example</title>
<script>
window.repoFiles = {
_posts: {
"2015-02-14-this-is-a-post.md": {
content: "---\ntitle: This is a post\ndate: 2015-02-14T00:00:00.000Z\n---\n\n# I Am a Title in Markdown\n\nHello, world\n"
}
},
_faqs: {
"what-is-netlify-cms.md": {
content: "---\ntitle: What is netlify CMS?\ndate: 2015-11-02T00:00.000Z\n---\n\n# Netlify CMS is Content Manager for Static Site Generators\n\nStatic sites are many times faster, cheaper and safer and traditional dynamic websites.\n\nModern static site generators like Jekyll, Middleman, Roots or Hugo are powerful publishing and development systems, but when we build sites for non-technical users, we need a layer on top of them.\n\nNetlify CMS is there to let your marketing team push new content to your public site, or to let technical writers work on your documentation.\n\nNetlify CMS integrates with Git and turns normal content editors into git comitters.\n\n"
}
},
_data: {
"settings.json": {
content: '{"site_title": "CMS Demo", "posts": {"front_limit": 5, "author": "Matt Biilmann"}}'
},
"authors.yml": {
content: 'authors:\n - name: Mathias\n description: Co-founder @ Netlify\n'
}
}
}
var ONE_DAY = 60 * 60 * 24 * 1000;
for (var i= 0; i<10; i++) {
var date = new Date();
date.setTime(date.getTime() + ONE_DAY);
var dateString = '' + date.getFullYear() + '-' + (date.getMonth() + 1) + '-' + date.getDate();
var slug = dateString + "-post-number-" + i + ".md";
window.repoFiles._posts[slug] = {
content: "---\ntitle: \"This is post # " + (10-i) + "\"\ndate: " + dateString + "T00:99:99.999Z\n---\n\n# The post is number " + i + "\n\nAnd this is yet another identical post body"
}
}
</script>
</head>
<body>
<script src='/cms.js'></script>
</body>
</html>

View File

@ -36,6 +36,7 @@
"imports-loader": "^0.6.5",
"js-yaml": "^3.5.3",
"mocha": "^2.4.5",
"moment": "^2.11.2",
"normalizr": "^2.0.0",
"react": "^0.14.7",
"react-dom": "^0.14.7",

View File

@ -32,7 +32,7 @@ export function loadConfig(config) {
return (dispatch, getState) => {
dispatch(configLoading());
fetch('config.yml').then((response) => {
fetch('/config.yml').then((response) => {
if (response.status !== 200) {
throw `Failed to load config.yml (${response.status})`;
}

49
src/actions/entries.js Normal file
View File

@ -0,0 +1,49 @@
import { currentBackend } from '../backends/backend';
export const ENTRIES_REQUEST = 'ENTRIES_REQUEST';
export const ENTRIES_SUCCESS = 'ENTRIES_SUCCESS';
export const ENTRIES_FAILURE = 'ENTRIES_FAILURE';
export function entriesLoaded(collection, entries) {
return {
type: ENTRIES_SUCCESS,
payload: {
collection: collection.get('name'),
entries: entries
}
};
}
export function entriesLoading(collection) {
return {
type: ENTRIES_REQUEST,
payload: {
collection: collection.get('name')
}
};
}
export function entriesFailed(collection, error) {
return {
type: ENTRIES_FAILURE,
error: 'Failed to load entries',
payload: error.toString(),
meta: {collection: collection.get('name')}
};
}
export function loadEntries(collection) {
return (dispatch, getState) => {
if (collection.get('isFetching')) { return; }
const state = getState();
const backend = currentBackend(state.config);
dispatch(entriesLoading(collection));
backend.entries(collection)
.then((entries) => dispatch(entriesLoaded(collection, entries)))
.catch((err) => {
console.error(err);
return dispatch(entriesFailed(collection, err));
});
};
}

View File

@ -1,4 +1,5 @@
import TestRepoBackend from './test-repo/Implementation';
import { resolveFormat } from '../formats/formats';
export function resolveBackend(config) {
const name = config.getIn(['backend', 'name']);
@ -32,8 +33,24 @@ class Backend {
return this.implementation.authComponent();
}
authenticate(state) {
return this.implementation.authenticate(state);
authenticate(credentials) {
return this.implementation.authenticate(credentials);
}
entries(collection) {
return this.implementation.entries(collection).then((entries) => (
(entries || []).map((entry) => {
const format = resolveFormat(collection, entry);
if (entry && entry.raw) {
entry.data = format && format.fromFile(entry.raw);
}
return entry;
})
));
}
entry(collection, slug) {
return this.implementation.entry(collection, slug);
}
}

View File

@ -12,7 +12,8 @@ export default class AuthenticationPage extends React.Component {
this.handleEmailChange = this.handleEmailChange.bind(this);
}
handleLogin() {
handleLogin(e) {
e.preventDefault();
this.props.onLogin(this.state);
}
@ -21,13 +22,13 @@ export default class AuthenticationPage extends React.Component {
}
render() {
return <div>
return <form onSubmit={this.handleLogin}>
<p>
<label>Your name or email: <input type='text' onChange={this.handleEmailChange}/></label>
</p>
<p>
<button onClick={this.handleLogin}>Login</button>
<button type='submit'>Login</button>
</p>
</div>;
</form>;
}
}

View File

@ -1,8 +1,16 @@
import AuthenticationPage from './AuthenticationPage';
function getSlug(path) {
const m = path.match(/([^\/]+)(\.[^\/\.]+)?$/);
return m && m[1];
}
export default class TestRepo {
constructor(config) {
this.config = config;
if (window.repoFiles == null) {
throw 'The TestRepo backend needs a "window.repoFiles" object.';
}
}
authComponent() {
@ -12,4 +20,24 @@ export default class TestRepo {
authenticate(state) {
return Promise.resolve({email: state.email});
}
entries(collection) {
const entries = [];
const folder = collection.get('folder');
if (folder) {
for (var path in window.repoFiles[folder]) {
entries.push({
path: folder + '/' + path,
slug: getSlug(path),
raw: window.repoFiles[folder][path].content
});
}
}
return Promise.resolve(entries);
}
entry(collection, slug) {
return Promise.resolve({slug: slug, title: 'hello'});
}
}

View File

@ -0,0 +1,19 @@
import React from 'react';
import { Link } from 'react-router';
export default class EntryListing extends React.Component {
render() {
const { collection, entries } = this.props;
const name = collection.get('name');
return <div>
<h2>Listing entries!</h2>
{entries.map((entry) => {
const path = `/collections/${name}/entries/${entry.get('slug')}`;
return <Link key={entry.get('slug')} to={path}>
<h3>{entry.getIn(['data', 'title'])}</h3>
</Link>;
})}
</div>;
}
}

View File

@ -0,0 +1,61 @@
import React from 'react';
import { Link } from 'react-router';
import { connect } from 'react-redux';
import { loadEntries } from '../actions/entries';
import EntryListing from '../components/EntryListing';
class DashboardPage extends React.Component {
componentDidMount() {
const { collection, dispatch } = this.props;
if (collection) {
dispatch(loadEntries(collection));
}
}
componentWillReceiveProps(nextProps) {
const { collection, dispatch } = this.props;
if (nextProps.collection !== collection) {
dispatch(loadEntries(nextProps.collection));
}
}
render() {
const { collections, collection, slug, children } = this.props;
if (collections == null) {
return <h1>No collections defined in your config.yml</h1>;
}
const entries = collection.get('entries');
return <div>
<h1>Dashboard</h1>
<div>
{collections.map((collection) => (
<div key={collection.get('name')}>
<Link to={`/collections/${collection.get('name')}`}>{collection.get('name')}</Link>
</div>
)).toArray()}
</div>
<div>
{slug ? children :
entries ? <EntryListing collection={collection} entries={entries}/> : 'No entries...'
}
</div>
</div>;
}
}
function mapStateToProps(state, ownProps) {
const { collections } = state;
const { name, slug } = ownProps.params;
return {
slug: slug,
collection: name ? collections.get(name) : collections.first(),
collections: collections
};
}
export default connect(mapStateToProps)(DashboardPage);

View File

@ -1,26 +0,0 @@
import React from 'react';
import { Link } from 'react-router';
import { connect } from 'react-redux';
class DashboardPage extends React.Component {
render() {
const { collections } = this.props;
return <div>
<h1>Dashboard</h1>
{collections && collections.map((collection) => (
<div key={collection.get('name')}>
<Link to={`/collections/${collection.get('name')}`}>{collection.get('name')}</Link>
</div>
)).toArray()}
</div>;
}
}
function mapStateToProps(state) {
return {
collections: state.collections
};
}
export default connect(mapStateToProps)(DashboardPage);

View File

@ -0,0 +1,33 @@
import React from 'react';
import { Link } from 'react-router';
import { connect } from 'react-redux';
import { loadEntries } from '../actions/entries';
import EntryListing from '../components/EntryListing';
class DashboardPage extends React.Component {
componentDidMount() {
}
componentWillReceiveProps(nextProps) {
}
render() {
const { collection, entry } = this.props;
return <div>
<h1>Entry in {collection.get('label')}</h1>
<h2>{entry && entry.get('title')}</h2>
</div>;
}
}
function mapStateToProps(state, ownProps) {
const { collections } = state;
return {
collection: collections.get(ownProps.params.name),
collections: collections
};
}
export default connect(mapStateToProps)(DashboardPage);

5
src/formats/formats.js Normal file
View File

@ -0,0 +1,5 @@
import YAMLFrontmatter from './yaml-frontmatter';
export function resolveFormat(collection, entry) {
return new YAMLFrontmatter();
}

View File

@ -0,0 +1,31 @@
import YAML from './yaml';
const regexp = /^---\n([^]*?)\n---\n([^]*)$/;
export default class YAMLFrontmatter {
fromFile(content) {
const match = content.match(regexp);
const obj = match ? new YAML().fromFile(match[1]) : {};
obj.body = match ? (match[2] || '').replace(/^\n+/, '') : content;
return obj;
}
toFile(data) {
const meta = {};
let body = '';
let content = '';
for (var key in data) {
if (key === 'body') {
body = data[key];
} else {
meta[key] = data[key];
}
}
content += '---\n';
content += new YAML().toFile(meta);
content += '---\n\n';
content += body;
return content;
}
}

31
src/formats/yaml.js Normal file
View File

@ -0,0 +1,31 @@
import yaml from 'js-yaml';
import moment from 'moment';
const MomentType = new yaml.Type('date', {
kind: 'scalar',
predicate: function(value) {
return moment.isMoment(value);
},
represent: function(value) {
return value.format(value._f);
},
resolve: function(value) {
return moment.isMoment(value) && value._f;
}
});
const OutputSchema = new yaml.Schema({
include: yaml.DEFAULT_SAFE_SCHEMA.include,
implicit: [MomentType].concat(yaml.DEFAULT_SAFE_SCHEMA.implicit),
explicit: yaml.DEFAULT_SAFE_SCHEMA.explicit
});
export default class YAML {
fromFile(content) {
return yaml.safeLoad(content);
}
toFile(data) {
return yaml.safeDump(data, {schema: OutputSchema});
}
}

View File

@ -1,15 +1,20 @@
import Immutable from 'immutable';
import { OrderedMap, fromJS } from 'immutable';
import { CONFIG_SUCCESS } from '../actions/config';
import { ENTRIES_REQUEST, ENTRIES_SUCCESS } from '../actions/entries';
export function collections(state = null, action) {
switch (action.type) {
case CONFIG_SUCCESS:
const collections = action.payload && action.payload.collections;
return Immutable.OrderedMap().withMutations((map) => {
return OrderedMap().withMutations((map) => {
(collections || []).forEach(function(collection) {
map.set(collection.name, Immutable.fromJS(collection));
map.set(collection.name, fromJS(collection));
});
});
case ENTRIES_REQUEST:
return state && state.setIn([action.payload.collection, 'isFetching'], true);
case ENTRIES_SUCCESS:
return state && state.setIn([action.payload.collection, 'entries'], fromJS(action.payload.entries));
default:
return state;
}

View File

@ -1,13 +1,17 @@
import React from 'react';
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
import App from '../containers/App';
import DashboardPage from '../containers/DashboardPage';
import DashboardPage from '../containers/CollectionPage';
import EntryPage from '../containers/EntryPage';
import NotFoundPage from '../containers/NotFoundPage';
export default () => (
<Router history={browserHistory}>
<Route path="/" component={App}>
<IndexRoute component={DashboardPage}/>
<Route path="/collections/:name" component={DashboardPage}>
<Route path="/collections/:name/entries/:slug" component={EntryPage}/>
</Route>
<Route path="*" component={NotFoundPage}/>
</Route>
</Router>

View File

@ -1,6 +1,7 @@
import expect from 'expect';
import Immutable from 'immutable';
import { Map, OrderedMap, fromJS } from 'immutable';
import { configLoaded } from '../../src/actions/config';
import { entriesLoading, entriesLoaded } from '../../src/actions/entries';
import { collections } from '../../src/reducers/collections';
describe('collections', () => {
@ -18,8 +19,35 @@ describe('collections', () => {
{name: 'posts', folder: '_posts', fields: [{name: 'title', widget: 'string'}]}
]}))
).toEqual(
Immutable.OrderedMap({
posts: Immutable.fromJS({name: 'posts', folder: '_posts', fields: [{name: 'title', widget: 'string'}]})
OrderedMap({
posts: fromJS({name: 'posts', folder: '_posts', fields: [{name: 'title', widget: 'string'}]})
})
);
});
it('should mark entries as loading', () => {
const state = OrderedMap({
'posts': Map({name: 'posts'})
});
expect(
collections(state, entriesLoading(Map({name: 'posts'})))
).toEqual(
OrderedMap({
'posts': Map({name: 'posts', isFetching: true})
})
);
});
it('should handle loaded entries', () => {
const state = OrderedMap({
'posts': Map({name: 'posts'})
});
const entries = [{slug: 'a', path: ''}, {slug: 'b', title: 'B'}];
expect(
collections(state, entriesLoaded(Map({name: 'posts'}), entries))
).toEqual(
OrderedMap({
'posts': fromJS({name: 'posts', entries: entries})
})
);
});