diff --git a/example/example.css b/example/example.css new file mode 100644 index 00000000..3ec853c0 --- /dev/null +++ b/example/example.css @@ -0,0 +1,15 @@ +html, body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + color: #444; +} +body { + padding: 20px; +} + +h1 { + font-weight: bold; + color: #666; + font-size: 32px; + margin-top: 20px; +} diff --git a/example/index.html b/example/index.html index aa1c14e8..cbf4ef07 100644 --- a/example/index.html +++ b/example/index.html @@ -69,6 +69,26 @@ diff --git a/package.json b/package.json index c4432c59..9f48f4d9 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "markup-it": "git+https://github.com/cassiozen/markup-it.git", "pluralize": "^3.0.0", "prismjs": "^1.5.1", + "react-datetime": "^2.6.0", "react-addons-css-transition-group": "^15.3.1", "react-portal": "^2.2.1", "selection-position": "^1.0.0", diff --git a/src/backends/netlify-git/API.js b/src/backends/netlify-git/API.js index 25e288c8..873286e7 100644 --- a/src/backends/netlify-git/API.js +++ b/src/backends/netlify-git/API.js @@ -55,7 +55,7 @@ export default class API { } checkMetadataRef() { - return this.request(`${this.repoURL}/git/refs/meta/_netlify_cms?${Date.now()}`, { + return this.request(`${this.repoURL}/refs/meta/_netlify_cms?${Date.now()}`, { cache: 'no-store', }) .then(response => response.object) @@ -66,7 +66,7 @@ export default class API { }; return this.uploadBlob(readme) - .then(item => this.request(`${this.repoURL}/git/trees`, { + .then(item => this.request(`${this.repoURL}/trees`, { method: 'POST', body: JSON.stringify({ tree: [{ path: 'README.md', mode: '100644', type: 'blob', sha: item.sha }] }) })) @@ -99,9 +99,9 @@ export default class API { return cache.then((cached) => { if (cached && cached.expires > Date.now()) { return cached.data; } - return this.request(`${this.repoURL}/files/${key}.json`, { + return this.request(`${this.repoURL}/files/${key}.json?ref=refs/meta/_netlify_cms`, { params: { ref: 'refs/meta/_netlify_cms' }, - headers: { Accept: 'application/vnd.github.VERSION.raw' }, + headers: { 'Content-Type': 'application/vnd.netlify.raw' }, cache: 'no-store', }).then((result) => { LocalForage.setItem(`gh.meta.${key}`, { @@ -119,7 +119,7 @@ export default class API { if (cached) { return cached; } return this.request(`${this.repoURL}/files/${path}`, { - headers: { Accept: 'application/vnd.github.VERSION.raw' }, + headers: { 'Content-Type': 'application/vnd.netlify.raw' }, params: { ref: this.branch }, cache: false, raw: true @@ -173,7 +173,7 @@ export default class API { } createRef(type, name, sha) { - return this.request(`${this.repoURL}/git/refs`, { + return this.request(`${this.repoURL}/refs`, { method: 'POST', body: JSON.stringify({ ref: `refs/${type}/${name}`, sha }), }); @@ -184,7 +184,7 @@ export default class API { } patchRef(type, name, sha) { - return this.request(`${this.repoURL}/git/refs/${type}/${name}`, { + return this.request(`${this.repoURL}/refs/${type}/${name}`, { method: 'PATCH', body: JSON.stringify({ sha }) }); @@ -195,7 +195,7 @@ export default class API { } getBranch() { - return this.request(`${this.repoURL}/branches/${this.branch}`); + return this.request(`${this.repoURL}/refs/heads/${this.branch}`); } createPR(title, head, base = 'master') { @@ -207,7 +207,7 @@ export default class API { } getTree(sha) { - return sha ? this.request(`${this.repoURL}/git/trees/${sha}`) : Promise.resolve({ tree: [] }); + return sha ? this.request(`${this.repoURL}/trees/${sha}`) : Promise.resolve({ tree: [] }); } toBase64(str) { @@ -220,7 +220,7 @@ export default class API { const content = item instanceof MediaProxy ? item.toBase64() : this.toBase64(item.raw); return content.then((contentBase64) => { - return this.request(`${this.repoURL}/git/blobs`, { + return this.request(`${this.repoURL}/blobs`, { method: 'POST', body: JSON.stringify({ content: contentBase64, @@ -263,7 +263,7 @@ export default class API { } return Promise.all(updates) .then((updates) => { - return this.request(`${this.repoURL}/git/trees`, { + return this.request(`${this.repoURL}/trees`, { method: 'POST', body: JSON.stringify({ base_tree: sha, tree: updates }) }); @@ -276,7 +276,7 @@ export default class API { commit(message, changeTree) { const tree = changeTree.sha; const parents = changeTree.parentSha ? [changeTree.parentSha] : []; - return this.request(`${this.repoURL}/git/commits`, { + return this.request(`${this.repoURL}/commits`, { method: 'POST', body: JSON.stringify({ message, tree, parents }) }); diff --git a/src/components/Cards/ImageCard.js b/src/components/Cards/ImageCard.js index 307b63a9..9473ca61 100644 --- a/src/components/Cards/ImageCard.js +++ b/src/components/Cards/ImageCard.js @@ -9,7 +9,7 @@ export default class ImageCard extends React.Component { return ( -

{text}

+

{text}

{description ?

{description}

: null}
diff --git a/src/components/ControlPane.js b/src/components/ControlPane.js index a86ecaf5..fc39041e 100644 --- a/src/components/ControlPane.js +++ b/src/components/ControlPane.js @@ -1,26 +1,29 @@ import React, { PropTypes } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import Widgets from './Widgets'; +import { resolveWidget } from './Widgets'; export default class ControlPane extends React.Component { controlFor(field) { const { entry, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props; - const widget = Widgets[field.get('widget')] || Widgets._unknown; - return React.createElement(widget.Control, { - field: field, - value: entry.getIn(['data', field.get('name')]), - onChange: (value) => onChange(entry.setIn(['data', field.get('name')], value)), - onAddMedia: onAddMedia, - onRemoveMedia: onRemoveMedia, - getMedia: getMedia - }); + const widget = resolveWidget(field.get('widget')); + return
+ + {React.createElement(widget.control, { + field: field, + value: entry.getIn(['data', field.get('name')]), + onChange: (value) => onChange(entry.setIn(['data', field.get('name')], value)), + onAddMedia: onAddMedia, + onRemoveMedia: onRemoveMedia, + getMedia: getMedia + })} +
; } render() { const { collection } = this.props; if (!collection) { return null; } return
- {collection.get('fields').map((field) =>
{this.controlFor(field)}
)} + {collection.get('fields').map((field) =>
{this.controlFor(field)}
)}
; } } diff --git a/src/components/EntryEditor.css b/src/components/EntryEditor.css new file mode 100644 index 00000000..03c128bd --- /dev/null +++ b/src/components/EntryEditor.css @@ -0,0 +1,25 @@ +.entryEditor { + display: flex; + flex-direction: column; + height: 100%; +} +.container { + display: flex; + height: 100%; +} +.footer { + background: #fff; + height: 45px; + border-top: 1px solid #e8eae8; + padding: 10px 20px; +} +.controlPane { + width: 50%; + max-height: 100%; + overflow: auto; + padding: 0 20px; + border-right: 1px solid #e8eae8; +} +.previewPane { + width: 50%; +} diff --git a/src/components/EntryEditor.js b/src/components/EntryEditor.js index 1ab0f003..ea83f7bb 100644 --- a/src/components/EntryEditor.js +++ b/src/components/EntryEditor.js @@ -2,38 +2,60 @@ import React, { PropTypes } from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ControlPane from './ControlPane'; import PreviewPane from './PreviewPane'; +import styles from './EntryEditor.css'; -export default function EntryEditor({ collection, entry, getMedia, onChange, onAddMedia, onRemoveMedia, onPersist }) { - return
-

Entry in {collection.get('label')}

-

{entry && entry.get('title')}

-
-
- -
-
- -
-
- -
; -} - -const styles = { - container: { - display: 'flex' - }, - pane: { - width: '50%' +export default class EntryEditor extends React.Component { + constructor(props) { + super(props); + this.state = {}; + this.handleResize = this.handleResize.bind(this); } -}; + + componentDidMount() { + this.calculateHeight(); + window.addEventListener('resize', this.handleResize, false); + } + + componengWillUnmount() { + window.removeEventListener('resize', this.handleResize); + } + + handleResize() { + this.calculateHeight(); + } + + calculateHeight() { + const height = window.innerHeight - 54; + console.log('setting height to %s', height); + this.setState({height}); + } + + render() { + const { collection, entry, getMedia, onChange, onAddMedia, onRemoveMedia, onPersist } = this.props; + const {height} = this.state; + + return
+
+
+ +
+
+ +
+
+
+ +
+
; + } +} EntryEditor.propTypes = { collection: ImmutablePropTypes.map.isRequired, diff --git a/src/components/EntryListing.js b/src/components/EntryListing.js index 28baa927..7ca45393 100644 --- a/src/components/EntryListing.js +++ b/src/components/EntryListing.js @@ -60,7 +60,8 @@ export default class EntryListing extends React.Component { cardFor(collection, entry, link) { //const { entry, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props; - const card = Cards[collection.getIn(['card', 'type'])] || Cards._unknown; + const cartType = collection.getIn(['card', 'type']) || 'alltype'; + const card = Cards[cartType] || Cards._unknown; return React.createElement(card, { key: entry.get('slug'), collection: collection, diff --git a/src/components/PreviewPane.css b/src/components/PreviewPane.css new file mode 100644 index 00000000..6bf62a0a --- /dev/null +++ b/src/components/PreviewPane.css @@ -0,0 +1,6 @@ +.frame { + width: 100%; + height: 100%; + border: none; + background: #fff; +} diff --git a/src/components/PreviewPane.js b/src/components/PreviewPane.js index 2964ae93..9ce76ab5 100644 --- a/src/components/PreviewPane.js +++ b/src/components/PreviewPane.js @@ -1,12 +1,15 @@ import React, { PropTypes } from 'react'; +import { render } from 'react-dom'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import Widgets from './Widgets'; +import registry from '../lib/registry'; +import { resolveWidget } from './Widgets'; +import styles from './PreviewPane.css'; -export default class PreviewPane extends React.Component { +class Preview extends React.Component { previewFor(field) { const { entry, getMedia } = this.props; - const widget = Widgets[field.get('widget')] || Widgets._unknown; - return React.createElement(widget.Preview, { + const widget = resolveWidget(field.get('widget')); + return React.createElement(widget.preview, { field: field, value: entry.getIn(['data', field.get('name')]), getMedia: getMedia, @@ -17,13 +20,69 @@ export default class PreviewPane extends React.Component { const { collection } = this.props; if (!collection) { return null; } - return
{collection.get('fields').map((field) =>
{this.previewFor(field)}
)}
; } } +Preview.propTypes = { + collection: ImmutablePropTypes.map.isRequired, + entry: ImmutablePropTypes.map.isRequired, + getMedia: PropTypes.func.isRequired, +}; + +export default class PreviewPane extends React.Component { + constructor(props) { + super(props); + this.handleIframeRef = this.handleIframeRef.bind(this); + this.widgetFor = this.widgetFor.bind(this); + } + + componentDidUpdate() { + this.renderPreview(); + } + + widgetFor(name) { + const { collection, entry, getMedia } = this.props; + const field = collection.get('fields').find((field) => field.get('name') === name); + const widget = resolveWidget(field.get('widget')); + return React.createElement(widget.preview, { + field: field, + value: entry.getIn(['data', field.get('name')]), + getMedia: getMedia, + }); + } + + renderPreview() { + const props = Object.assign({}, this.props, {widgetFor: this.widgetFor}); + const component = registry.getPreviewTemplate(props.collection.get('name')) || Preview; + + render(React.createElement(component, props), this.previewEl); + } + + handleIframeRef(ref) { + if (ref) { + registry.getPreviewStyles().forEach((style) => { + const linkEl = document.createElement('link'); + linkEl.setAttribute('rel', 'stylesheet'); + linkEl.setAttribute('href', style); + ref.contentDocument.head.appendChild(linkEl); + }); + this.previewEl = document.createElement('div'); + ref.contentDocument.body.appendChild(this.previewEl); + this.renderPreview(); + } + } + + render() { + const { collection } = this.props; + if (!collection) { return null; } + + return ; + } +} + PreviewPane.propTypes = { collection: ImmutablePropTypes.map.isRequired, entry: ImmutablePropTypes.map.isRequired, diff --git a/src/components/UnpublishedListing.js b/src/components/UnpublishedListing.js index af179aac..b57eebd9 100644 --- a/src/components/UnpublishedListing.js +++ b/src/components/UnpublishedListing.js @@ -1,6 +1,6 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import dateFormat from 'dateFormat'; +import moment from 'moment'; import { Card } from './UI'; import { Link } from 'react-router' import { statusDescriptions } from '../constants/publishModes'; @@ -22,7 +22,7 @@ export default class UnpublishedListing extends React.Component { {entries.map(entry => { // Look for an "author" field. Fallback to username on backend implementation; const author = entry.getIn(['data', 'author'], entry.getIn(['metaData', 'user'])); - const timeStamp = dateFormat(Date.parse(entry.getIn(['metaData', 'timeStamp'])), 'longDate'); + const timeStamp = moment(entry.getIn(['metaData', 'timeStamp'])).formate('llll'); const link = `/editorialworkflow/${entry.getIn(['metaData', 'collection'])}/${entry.getIn(['metaData', 'status'])}/${entry.get('slug')}`; return ( diff --git a/src/components/Widgets.js b/src/components/Widgets.js index 0a86b6b7..e731ed78 100644 --- a/src/components/Widgets.js +++ b/src/components/Widgets.js @@ -1,30 +1,24 @@ +import registry from '../lib/registry'; import UnknownControl from './Widgets/UnknownControl'; import UnknownPreview from './Widgets/UnknownPreview'; import StringControl from './Widgets/StringControl'; import StringPreview from './Widgets/StringPreview'; +import TextControl from './Widgets/TextControl'; +import TextPreview from './Widgets/TextPreview'; import MarkdownControl from './Widgets/MarkdownControl'; import MarkdownPreview from './Widgets/MarkdownPreview'; import ImageControl from './Widgets/ImageControl'; import ImagePreview from './Widgets/ImagePreview'; +import DateTimeControl from './Widgets/DateTimeControl'; +import DateTimePreview from './Widgets/DateTimePreview'; +registry.registerWidget('string', StringControl, StringPreview); +registry.registerWidget('text', TextControl, TextPreview); +registry.registerWidget('markdown', MarkdownControl, MarkdownPreview); +registry.registerWidget('image', ImageControl, ImagePreview); +registry.registerWidget('datetime', DateTimeControl, DateTimePreview); +registry.registerWidget('_unknown', UnknownControl, UnknownPreview); -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; +export function resolveWidget(name) { + return registry.getWidget(name) || registry.getWidget('_unknown'); +} diff --git a/src/components/Widgets/DateTimeControl.js b/src/components/Widgets/DateTimeControl.js new file mode 100644 index 00000000..7f49868a --- /dev/null +++ b/src/components/Widgets/DateTimeControl.js @@ -0,0 +1,22 @@ +import React, { PropTypes } from 'react'; +import DateTime from 'react-datetime'; + +export default class DateTimeControl extends React.Component { + constructor(props) { + super(props); + this.handleChange = this.handleChange.bind(this); + } + + handleChange(datetime) { + this.props.onChange(datetime); + } + + render() { + return ; + } +} + +DateTimeControl.propTypes = { + onChange: PropTypes.func.isRequired, + value: PropTypes.object, +}; diff --git a/src/components/Widgets/DateTimePreview.js b/src/components/Widgets/DateTimePreview.js new file mode 100644 index 00000000..972e068c --- /dev/null +++ b/src/components/Widgets/DateTimePreview.js @@ -0,0 +1,9 @@ +import React, { PropTypes } from 'react'; + +export default function StringPreview({ value }) { + return {value}; +} + +StringPreview.propTypes = { + value: PropTypes.node, +}; diff --git a/src/components/Widgets/ImageControl.js b/src/components/Widgets/ImageControl.js index 2006215b..5c680599 100644 --- a/src/components/Widgets/ImageControl.js +++ b/src/components/Widgets/ImageControl.js @@ -78,8 +78,8 @@ export default class ImageControl extends React.Component { onDragOver={this.handleDragOver} onDrop={this.handleChange} > - - {imageName ? imageName : 'Click or drop image here.'} + + {imageName ? imageName : 'Tip: Click here to upload an image from your file browser, or drag an image directly into this box from your desktop'} - +
+ {null && } - +
+ {null && } plugin.id))); } else { rawJson = emptyParagraphBlock; diff --git a/src/plugins/index.js b/src/components/Widgets/MarkdownControlElements/plugins.js similarity index 52% rename from src/plugins/index.js rename to src/components/Widgets/MarkdownControlElements/plugins.js index 485a0c4c..fa0e0d34 100644 --- a/src/plugins/index.js +++ b/src/components/Widgets/MarkdownControlElements/plugins.js @@ -16,23 +16,6 @@ const EditorComponent = Record({ toPreview: function(attributes) { return 'Plugin'; } }); -function CMS() { - this.registerEditorComponent = (config) => { - const configObj = new EditorComponent({ - id: config.id || config.label.replace(/[^A-Z0-9]+/ig, '_'), - label: config.label, - icon: config.icon, - fields: config.fields, - pattern: config.pattern, - fromBlock: _.isFunction(config.fromBlock) ? config.fromBlock.bind(null) : null, - toBlock: _.isFunction(config.toBlock) ? config.toBlock.bind(null) : null, - toPreview: _.isFunction(config.toPreview) ? config.toPreview.bind(null) : config.toBlock.bind(null) - }); - - plugins.editor = plugins.editor.push(configObj); - }; -} - class Plugin extends Component { getChildContext() { @@ -51,8 +34,18 @@ Plugin.childContextTypes = { plugins: PropTypes.object }; +export function newEditorPlugin(config) { + const configObj = new EditorComponent({ + id: config.id || config.label.replace(/[^A-Z0-9]+/ig, '_'), + label: config.label, + icon: config.icon, + fields: config.fields, + pattern: config.pattern, + fromBlock: _.isFunction(config.fromBlock) ? config.fromBlock.bind(null) : null, + toBlock: _.isFunction(config.toBlock) ? config.toBlock.bind(null) : null, + toPreview: _.isFunction(config.toPreview) ? config.toPreview.bind(null) : config.toBlock.bind(null) + }); -export const initPluginAPI = () => { - window.CMS = new CMS(); - return Plugin; -}; + + return configObj; +} diff --git a/src/components/Widgets/TextControl.js b/src/components/Widgets/TextControl.js new file mode 100644 index 00000000..aaeec4e3 --- /dev/null +++ b/src/components/Widgets/TextControl.js @@ -0,0 +1,37 @@ +import React, { PropTypes } from 'react'; + +export default class StringControl extends React.Component { + constructor(props) { + super(props); + this.handleChange = this.handleChange.bind(this); + this.handleRef = this.handleRef.bind(this); + } + + componentDidMount() { + this.updateHeight(); + } + + handleChange(e) { + this.props.onChange(e.target.value); + this.updateHeight(); + } + + updateHeight() { + if (this.element.scrollHeight > this.element.clientHeight) { + this.element.style.height = this.element.scrollHeight + 'px'; + } + } + + handleRef(ref) { + this.element = ref; + } + + render() { + return