Basic editing with some widgets

This commit is contained in:
Mathias Biilmann Christensen 2016-05-30 16:55:32 -07:00
parent 978b7290c5
commit d2aa1adf7b
23 changed files with 307 additions and 32 deletions

View File

@ -1,7 +1,10 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>This is an example</title>
<link rel="stylesheet" href="https://facebook.github.io/draft-js/css/draft.css"/>
<script>
window.repoFiles = {
_posts: {

View File

@ -54,5 +54,13 @@
"webpack-dev-server": "^1.14.1",
"webpack-postcss-tools": "^1.1.1",
"whatwg-fetch": "^0.11.0"
},
"dependencies": {
"commonmark": "^0.24.0",
"commonmark-react-renderer": "^4.1.2",
"draft-js": "^0.7.0",
"draft-js-export-markdown": "^0.2.0",
"draft-js-import-markdown": "^0.1.6",
"json-loader": "^0.5.4"
}
}

View File

@ -33,6 +33,6 @@ export function loginUser(credentials) {
dispatch(authenticating());
backend.authenticate(credentials)
.then((user) => dispatch(authenticate(user)))
.catch((err) => dispatch(authError(err)));
//.catch((err) => dispatch(authError(err)));
};
}

View File

@ -1,4 +1,6 @@
import yaml from 'js-yaml';
import { currentBackend } from '../backends/backend';
import { authenticate } from '../actions/auth';
export const CONFIG_REQUEST = 'CONFIG_REQUEST';
export const CONFIG_SUCCESS = 'CONFIG_SUCCESS';
@ -37,7 +39,12 @@ export function loadConfig(config) {
throw `Failed to load config.yml (${response.status})`;
}
response.text().then(parseConfig).then((config) => dispatch(configLoaded(config)));
response.text().then(parseConfig).then((config) => {
dispatch(configLoaded(config));
const backend = currentBackend(config);
const user = backend && backend.currentUser();
user && dispatch(authenticate(user));
});
}).catch((err) => {
dispatch(configFailed(err));
});

View File

@ -1,17 +1,16 @@
import TestRepoBackend from './test-repo/Implementation';
import { resolveFormat } from '../formats/formats';
export function resolveBackend(config) {
const name = config.getIn(['backend', 'name']);
if (name == null) {
throw 'No backend defined in configuration';
class LocalStorageAuthStore {
storageKey = 'nf-cms-user';
retrieve() {
const data = window.localStorage.getItem(this.storageKey);
return data && JSON.parse(data);
}
switch (name) {
case 'test-repo':
return new Backend(new TestRepoBackend(config));
default:
throw `Backend not found: ${name}`;
store(userData) {
window.localStorage.setItem(this.storageKey, JSON.stringify(userData));
}
}
@ -34,12 +33,15 @@ class Backend {
}
authenticate(credentials) {
return this.implementation.authenticate(credentials);
return this.implementation.authenticate(credentials).then((user) => {
if (this.authStore) { this.authStore.store(user); }
return user;
});
}
entries(collection) {
return this.implementation.entries(collection).then((entries) => (
(entries || []).map((entry) => {
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);
@ -54,6 +56,22 @@ class Backend {
}
}
export function resolveBackend(config) {
const name = config.getIn(['backend', 'name']);
if (name == null) {
throw 'No backend defined in configuration';
}
const authStore = new LocalStorageAuthStore();
switch (name) {
case 'test-repo':
return new Backend(new TestRepoBackend(config), authStore);
default:
throw `Backend not found: ${name}`;
}
}
export const currentBackend = (function() {
let backend = null;

View File

@ -1,7 +1,7 @@
import AuthenticationPage from './AuthenticationPage';
function getSlug(path) {
const m = path.match(/([^\/]+)(\.[^\/\.]+)?$/);
const m = path.match(/([^\/]+?)(\.[^\/\.]+)?$/);
return m && m[1];
}
@ -38,6 +38,8 @@ export default class TestRepo {
}
entry(collection, slug) {
return Promise.resolve({slug: slug, title: 'hello'});
return this.entries(collection).then((entries) => (
entries.filter((entry) => entry.slug === slug)[0]
));
}
}

View File

@ -0,0 +1,24 @@
import React from 'react';
import Widgets from './Widgets';
export default class ControlPane extends React.Component {
controlFor(field) {
const { entry } = this.props;
const widget = Widgets[field.get('widget')] || Widgets._unknown;
return React.createElement(widget.Control, {
key: field.get('name'),
field: field,
value: entry.get(field.get('name')),
onChange: (value) => this.props.onChange(entry.set(field.get('name'), value))
});
}
render() {
const { collection } = this.props;
if (!collection) { return null; }
return <div>
{collection.get('fields').map((field) => <div>{this.controlFor(field)}</div>)}
</div>;
}
}

View File

@ -0,0 +1,33 @@
import React from 'react';
import ControlPane from './ControlPane';
import PreviewPane from './PreviewPane';
export default class EntryEditor extends React.Component {
constructor(props) {
super(props);
this.state = {entry: props.entry};
this.handleChange = this.handleChange.bind(this);
}
handleChange(entry) {
console.log('Got new entry: %o', entry.toObject());
this.setState({entry: entry});
}
render() {
const { collection, entry } = this.props;
return <div>
<h1>Entry in {collection.get('label')}</h1>
<h2>{entry && entry.get('title')}</h2>
<div className="cms-container">
<div className="cms-control-pane">
<ControlPane collection={collection} entry={this.state.entry} onChange={this.handleChange}/>
</div>
<div className="cms-preview-pane">
<PreviewPane collection={collection} entry={this.state.entry}/>
</div>
</div>
</div>
}
}

View File

@ -0,0 +1,23 @@
import React from 'react';
import Widgets from './Widgets';
export default class PreviewPane extends React.Component {
previewFor(field) {
const { entry } = this.props;
const widget = Widgets[field.get('widget')] || Widgets._unknown;
return React.createElement(widget.Preview, {
key: field.get('name'),
field: field,
value: entry.get(field.get('name'))
});
}
render() {
const { collection } = this.props;
if (!collection) { return null; }
return <div>
{collection.get('fields').map((field) => <div>{this.previewFor(field)}</div>)}
</div>;
}
}

30
src/components/Widgets.js Normal file
View File

@ -0,0 +1,30 @@
import UnknownControl from './widgets/UnknownControl';
import UnknownPreview from './widgets/UnknownPreview';
import StringControl from './widgets/StringControl';
import StringPreview from './widgets/StringPreview';
import MarkdownControl from './widgets/MarkdownControl';
import MarkdownPreview from './widgets/MarkdownPreview';
import ImageControl from './widgets/ImageControl';
import ImagePreview from './widgets/ImagePreview';
const Widgets = {
_unknown: {
Control: UnknownControl,
Preview: UnknownPreview
},
string: {
Control: StringControl,
Preview: StringPreview
},
markdown: {
Control: MarkdownControl,
Preview: MarkdownPreview
},
image: {
Control: ImageControl,
Preview: ImagePreview
}
};
export default Widgets;

View File

@ -0,0 +1,16 @@
import React from 'react';
export default class ImageControl extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.props.onChange(e.target.value);
}
render() {
return <input type="file" onChange={this.handleChange}/>;
}
}

View File

@ -0,0 +1,13 @@
import React from 'react';
export default class ImagePreview extends React.Component {
constructor(props) {
super(props);
}
render() {
const { value } = this.props;
return value ? <img src={value}/> : null;
}
}

View File

@ -0,0 +1,40 @@
import React from 'react';
import {Editor, EditorState, RichUtils} from 'draft-js';
import {stateToMarkdown} from 'draft-js-export-markdown';
import {stateFromMarkdown} from 'draft-js-import-markdown';
export default class MarkdownControl extends React.Component {
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createWithContent(stateFromMarkdown(props.value || ''))
};
this.handleChange = this.handleChange.bind(this);
this.handleKeyCommand = this.handleKeyCommand.bind(this);
}
handleChange(editorState) {
const content = editorState.getCurrentContent();
this.setState({editorState});
this.props.onChange(stateToMarkdown(content));
}
handleKeyCommand(command) {
const newState = RichUtils.handleKeyCommand(this.state.editorState, command);
if (newState) {
this.handleChange(newState);
return true;
}
return false;
}
render() {
const {editorState} = this.state;
return (
<Editor
editorState={editorState}
onChange={this.handleChange}
handleKeyCommand={this.handleKeyCommand}
/>);
}
}

View File

@ -0,0 +1,17 @@
import React from 'react';
import CommonMark from 'commonmark';
import ReactRenderer from'commonmark-react-renderer';
const parser = new CommonMark.Parser();
const renderer = new ReactRenderer();
export default class MarkdownPreview extends React.Component {
render() {
const { value } = this.props;
console.log(value);
if (value == null) { return null; }
const ast = parser.parse(value);
return React.createElement.apply(React, ['div', {}].concat(renderer.render(ast)));
}
}

View File

@ -0,0 +1,16 @@
import React from 'react';
export default class StringControl extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.props.onChange(e.target.value);
}
render() {
return <input value={this.props.value} onChange={this.handleChange}/>;
}
}

View File

@ -0,0 +1,9 @@
import React from 'react';
export default class StringPreview extends React.Component {
render() {
const { value } = this.props;
return <span>{value}</span>;
}
}

View File

@ -0,0 +1,10 @@
import React from 'react';
export default class UnknownControl extends React.Component {
render() {
const { field } = this.props;
console.log('field: %o', field.toObject());
return <div>No control for widget '{field.get('widget')}'.</div>;
}
}

View File

@ -0,0 +1,9 @@
import React from 'react';
export default class UnknownPreview extends React.Component {
render() {
const { field } = this.props;
return <div>No preview for widget '{field.widget}'.</div>;
}
}

View File

@ -39,9 +39,7 @@ class DashboardPage extends React.Component {
)).toArray()}
</div>
<div>
{slug ? children :
entries ? <EntryListing collection={collection} entries={entries}/> : 'No entries...'
}
{entries ? <EntryListing collection={collection} entries={entries}/> : 'No entries...'}
</div>
</div>;
}

View File

@ -1,8 +1,7 @@
import React from 'react';
import { Link } from 'react-router';
import { connect } from 'react-redux';
import { loadEntries } from '../actions/entries';
import EntryListing from '../components/EntryListing';
import { Map } from 'immutable';
import EntryEditor from '../components/EntryEditor';
class DashboardPage extends React.Component {
componentDidMount() {
@ -14,18 +13,17 @@ class DashboardPage extends React.Component {
render() {
const { collection, entry } = this.props;
return <div>
<h1>Entry in {collection.get('label')}</h1>
<h2>{entry && entry.get('title')}</h2>
</div>;
return <EntryEditor entry={entry || new Map()} collection={collection}/>;
}
}
function mapStateToProps(state, ownProps) {
const { collections } = state;
const collection = collections.get(ownProps.params.name);
// const entryName = `${collection.get('name')}/${ownProps.params.slug}`;
return {
collection: collections.get(ownProps.params.name),
collection: collection,
collections: collections
};
}

View File

@ -8,6 +8,7 @@ export function auth(state = null, action) {
case AUTH_SUCCESS:
return Immutable.fromJS({user: action.payload});
case AUTH_FAILURE:
console.error(action.payload);
return Immutable.Map({error: action.payload.toString()});
default:
return state;

View File

@ -1,17 +1,16 @@
import React from 'react';
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
import App from '../containers/App';
import DashboardPage from '../containers/CollectionPage';
import CollectionPage 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>
<IndexRoute component={CollectionPage}/>
<Route path="/collections/:name" component={CollectionPage}/>
<Route path="/collections/:name/entries/:slug" component={EntryPage}/>
<Route path="*" component={NotFoundPage}/>
</Route>
</Router>

View File

@ -9,6 +9,7 @@ module.exports = {
test: /\.((png)|(eot)|(woff)|(woff2)|(ttf)|(svg)|(gif))(\?v=\d+\.\d+\.\d+)?$/,
loader: 'file?name=/[hash].[ext]'
},
{ test: /\.json$/, loader: 'json-loader' },
{
loader: 'babel',
test: /\.js?$/,