Basic editing with some widgets
This commit is contained in:
parent
978b7290c5
commit
d2aa1adf7b
@ -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: {
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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)));
|
||||
};
|
||||
}
|
||||
|
@ -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));
|
||||
});
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
24
src/components/ControlPane.js
Normal file
24
src/components/ControlPane.js
Normal 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>;
|
||||
}
|
||||
}
|
33
src/components/EntryEditor.js
Normal file
33
src/components/EntryEditor.js
Normal 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>
|
||||
}
|
||||
}
|
23
src/components/PreviewPane.js
Normal file
23
src/components/PreviewPane.js
Normal 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
30
src/components/Widgets.js
Normal 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;
|
16
src/components/Widgets/ImageControl.js
Normal file
16
src/components/Widgets/ImageControl.js
Normal 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}/>;
|
||||
}
|
||||
}
|
13
src/components/Widgets/ImagePreview.js
Normal file
13
src/components/Widgets/ImagePreview.js
Normal 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;
|
||||
}
|
||||
}
|
40
src/components/Widgets/MarkdownControl.js
Normal file
40
src/components/Widgets/MarkdownControl.js
Normal 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}
|
||||
/>);
|
||||
}
|
||||
}
|
17
src/components/Widgets/MarkdownPreview.js
Normal file
17
src/components/Widgets/MarkdownPreview.js
Normal 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)));
|
||||
}
|
||||
}
|
16
src/components/Widgets/StringControl.js
Normal file
16
src/components/Widgets/StringControl.js
Normal 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}/>;
|
||||
}
|
||||
}
|
9
src/components/Widgets/StringPreview.js
Normal file
9
src/components/Widgets/StringPreview.js
Normal 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>;
|
||||
}
|
||||
}
|
10
src/components/Widgets/UnknownControl.js
Normal file
10
src/components/Widgets/UnknownControl.js
Normal 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>;
|
||||
}
|
||||
}
|
9
src/components/Widgets/UnknownPreview.js
Normal file
9
src/components/Widgets/UnknownPreview.js
Normal 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>;
|
||||
}
|
||||
}
|
@ -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>;
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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?$/,
|
||||
|
Loading…
x
Reference in New Issue
Block a user