Test repo can now be used to list entries
This commit is contained in:
parent
67cdd92bfb
commit
978b7290c5
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -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
49
src/actions/entries.js
Normal 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));
|
||||
});
|
||||
};
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
@ -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'});
|
||||
}
|
||||
}
|
||||
|
19
src/components/EntryListing.js
Normal file
19
src/components/EntryListing.js
Normal 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>;
|
||||
}
|
||||
}
|
61
src/containers/CollectionPage.js
Normal file
61
src/containers/CollectionPage.js
Normal 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);
|
@ -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);
|
33
src/containers/EntryPage.js
Normal file
33
src/containers/EntryPage.js
Normal 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
5
src/formats/formats.js
Normal file
@ -0,0 +1,5 @@
|
||||
import YAMLFrontmatter from './yaml-frontmatter';
|
||||
|
||||
export function resolveFormat(collection, entry) {
|
||||
return new YAMLFrontmatter();
|
||||
}
|
31
src/formats/yaml-frontmatter.js
Normal file
31
src/formats/yaml-frontmatter.js
Normal 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
31
src/formats/yaml.js
Normal 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});
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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})
|
||||
})
|
||||
);
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user