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

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