diff --git a/src/actions/config.js b/src/actions/config.js index 51f743cd..f506d705 100644 --- a/src/actions/config.js +++ b/src/actions/config.js @@ -1,6 +1,7 @@ import yaml from 'js-yaml'; import { currentBackend } from '../backends/backend'; import { authenticate } from '../actions/auth'; +import * as ImageProxy from '../valueObjects/ImageProxy'; export const CONFIG_REQUEST = 'CONFIG_REQUEST'; export const CONFIG_SUCCESS = 'CONFIG_SUCCESS'; @@ -27,9 +28,17 @@ export function configFailed(err) { }; } +export function configDidLoad(config) { + return (dispatch) => { + ImageProxy.setConfig(config); + dispatch(configLoaded(config)); + }; +} + + export function loadConfig(config) { if (window.CMS_CONFIG) { - return configLoaded(window.CMS_CONFIG); + return configDidLoad(window.CMS_CONFIG); } return (dispatch, getState) => { dispatch(configLoading()); @@ -40,7 +49,7 @@ export function loadConfig(config) { } response.text().then(parseConfig).then((config) => { - dispatch(configLoaded(config)); + dispatch(configDidLoad(config)); const backend = currentBackend(config); const user = backend && backend.currentUser(); user && dispatch(authenticate(user)); diff --git a/src/actions/entries.js b/src/actions/entries.js index dffcecc8..a20d13d8 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -4,6 +4,10 @@ export const ENTRY_REQUEST = 'ENTRY_REQUEST'; export const ENTRY_SUCCESS = 'ENTRY_SUCCESS'; export const ENTRY_FAILURE = 'ENTRY_FAILURE'; +export const ENTRY_PERSIST_REQUEST = 'ENTRY_PERSIST_REQUEST'; +export const ENTRY_PERSIST_SUCCESS = 'ENTRY_PERSIST_SUCCESS'; +export const ENTRY_PERSIST_FAILURE = 'ENTRY_PERSIST_FAILURE'; + export const ENTRIES_REQUEST = 'ENTRIES_REQUEST'; export const ENTRIES_SUCCESS = 'ENTRIES_SUCCESS'; export const ENTRIES_FAILURE = 'ENTRIES_FAILURE'; @@ -28,17 +32,34 @@ export function entryLoaded(collection, entry) { }; } -export function entriesLoaded(collection, entries, pagination) { +export function entryPersisting(collection, entry) { return { - type: ENTRIES_SUCCESS, + type: ENTRY_PERSIST_REQUEST, payload: { - collection: collection.get('name'), - entries: entries, - pages: pagination + collection: collection, + entry: entry } }; } +export function entryPersisted(collection, entry) { + return { + type: ENTRY_PERSIST_SUCCESS, + payload: { + collection: collection, + entry: entry + } + }; +} + +export function entryPersistFail(collection, entry, error) { + return { + type: ENTRIES_FAILURE, + error: 'Failed to persist entry', + payload: error.toString() + }; +} + export function entriesLoading(collection) { return { type: ENTRIES_REQUEST, @@ -48,6 +69,17 @@ export function entriesLoading(collection) { }; } +export function entriesLoaded(collection, entries, pagination) { + return { + type: ENTRIES_SUCCESS, + payload: { + collection: collection.get('name'), + entries: entries, + pages: pagination + } + }; +} + export function entriesFailed(collection, error) { return { type: ENTRIES_FAILURE, @@ -76,6 +108,18 @@ export function loadEntries(collection) { dispatch(entriesLoading(collection)); backend.entries(collection) - .then((response) => dispatch(entriesLoaded(collection, response.entries, response.pagination))) + .then((response) => dispatch(entriesLoaded(collection, response.entries, response.pagination))); + }; +} + +export function persist(collection, entry, mediaFiles) { + return (dispatch, getState) => { + const state = getState(); + const backend = currentBackend(state.config); + dispatch(entryPersisting(collection, entry)); + backend.persist(collection, entry, mediaFiles).then( + (entry) => dispatch(entryPersisted(collection, entry)), + (error) => dispatch(entryPersistFail(collection, entry, error)) + ); }; } diff --git a/src/backends/backend.js b/src/backends/backend.js index 72dba0c6..8bb51aa4 100644 --- a/src/backends/backend.js +++ b/src/backends/backend.js @@ -66,6 +66,22 @@ class Backend { return entry; }; } + + persist(collection, entry, mediaFiles) { + const entryData = entry.get('data').toJS(); + const entryObj = { + path: entry.get('path'), + slug: entry.get('slug'), + raw: this.entryToRaw(collection, entryData) + }; + + return this.implementation.persist(collection, entryObj, mediaFiles.toJS()); + } + + entryToRaw(collection, entry) { + const format = resolveFormat(collection, entry); + return format && format.toFile(entry); + } } export function resolveBackend(config) { diff --git a/src/backends/test-repo/implementation.js b/src/backends/test-repo/implementation.js index 3f4ed4bd..c14bfe31 100644 --- a/src/backends/test-repo/implementation.js +++ b/src/backends/test-repo/implementation.js @@ -47,4 +47,9 @@ export default class TestRepo { response.entries.filter((entry) => entry.slug === slug)[0] )); } + + persist(collection, entry, mediaFiles) { + alert('This will be the persisted data:\n' + entry.raw); + return Promise.resolve({collection, entry}); + } } diff --git a/src/components/ControlPane.js b/src/components/ControlPane.js index 203bc581..c5225a55 100644 --- a/src/components/ControlPane.js +++ b/src/components/ControlPane.js @@ -9,7 +9,8 @@ export default class ControlPane extends React.Component { key: field.get('name'), field: field, value: entry.getIn(['data', field.get('name')]), - onChange: (value) => this.props.onChange(entry.setIn(['data', field.get('name')], value)) + onChange: (value) => this.props.onChange(entry.setIn(['data', field.get('name')], value)), + onAddMedia: (mediaFile) => this.props.onAddMedia(mediaFile) }); } diff --git a/src/components/EntryEditor.js b/src/components/EntryEditor.js index 488b5cb3..163f13e1 100644 --- a/src/components/EntryEditor.js +++ b/src/components/EntryEditor.js @@ -1,18 +1,32 @@ import React from 'react'; +import Immutable from 'immutable'; 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.state = { + entry: props.entry, + mediaFiles: Immutable.List() + }; this.handleChange = this.handleChange.bind(this); + this.handleAddMedia = this.handleAddMedia.bind(this); + this.handleSave = this.handleSave.bind(this); } handleChange(entry) { this.setState({entry: entry}); } + handleAddMedia(mediaFile) { + this.setState({mediaFiles: this.state.mediaFiles.push(mediaFile)}); + } + + handleSave() { + this.props.onPersist(this.state.entry, this.state.mediaFiles); + } + render() { const { collection, entry } = this.props; @@ -21,12 +35,18 @@ export default class EntryEditor extends React.Component {

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

- +
+ ; } } diff --git a/src/components/Widgets/ImageControl.js b/src/components/Widgets/ImageControl.js index 68d9c884..8d8efe98 100644 --- a/src/components/Widgets/ImageControl.js +++ b/src/components/Widgets/ImageControl.js @@ -1,14 +1,18 @@ import React from 'react'; +import ImageProxy from '../../valueObjects/ImageProxy'; export default class ImageControl extends React.Component { constructor(props) { super(props); this.state = { - currentImage: props.value + currentImage: { + file: null, + imageProxy: new ImageProxy(props.value, null, null, true) + } }; - this.revokeCurrentImage = this.revokeCurrentImage.bind(this); + this.revokeCurrentObjectURL = this.revokeCurrentObjectURL.bind(this); this.handleChange = this.handleChange.bind(this); this.handleFileInputRef = this.handleFileInputRef.bind(this); this.handleClick = this.handleClick.bind(this); @@ -18,12 +22,12 @@ export default class ImageControl extends React.Component { } componentWillUnmount() { - this.revokeCurrentImage(); + this.revokeCurrentObjectURL(); } - revokeCurrentImage() { - if (this.state.currentImage instanceof File) { - window.URL.revokeObjectURL(this.state.currentImage); + revokeCurrentObjectURL() { + if (this.state.currentImage.file) { + window.URL.revokeObjectURL(this.state.currentImage.file); } } @@ -48,7 +52,8 @@ export default class ImageControl extends React.Component { handleChange(e) { e.stopPropagation(); e.preventDefault(); - this.revokeCurrentImage(); + this.revokeCurrentObjectURL(); + let imageRef = null; const fileList = e.dataTransfer ? e.dataTransfer.files : e.target.files; const files = [...fileList]; const imageType = /^image\//; @@ -61,27 +66,17 @@ export default class ImageControl extends React.Component { }); if (file) { - // Custom toString function on file, so it can be used on regular image fields - file.toString = function() { - return window.URL.createObjectURL(file); - }; + this.props.onAddMedia(file); + imageRef = new ImageProxy(file.name, file.size, window.URL.createObjectURL(file)); } - this.props.onChange(file); - this.setState({currentImage: file}); + this.props.onChange(imageRef); + this.setState({currentImage: {file:file, imageProxy: imageRef}}); } renderImageName() { - if (!this.state.currentImage) return null; - - if (this.state.currentImage instanceof File) { - return this.state.currentImage.name; - } else if (typeof this.state.currentImage === 'string') { - const fileName = this.state.currentImage; - return fileName.substring(fileName.lastIndexOf('/') + 1); - } - - return null; + if (!this.state.currentImage.imageProxy) return null; + return this.state.currentImage.imageProxy.uri; } render() { diff --git a/src/containers/EntryPage.js b/src/containers/EntryPage.js index 8abfdf18..584a1402 100644 --- a/src/containers/EntryPage.js +++ b/src/containers/EntryPage.js @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { Map } from 'immutable'; -import { loadEntry } from '../actions/entries'; +import { loadEntry, persist } from '../actions/entries'; import { selectEntry } from '../reducers/entries'; import EntryEditor from '../components/EntryEditor'; @@ -9,6 +9,12 @@ class EntryPage extends React.Component { constructor(props) { super(props); this.props.dispatch(loadEntry(props.collection, props.slug)); + + this.handlePersist = this.handlePersist.bind(this); + } + + handlePersist(entry, mediaFiles) { + this.props.dispatch(persist(this.props.collection, entry, mediaFiles)); } render() { @@ -21,6 +27,7 @@ class EntryPage extends React.Component { ); } diff --git a/src/formats/yaml.js b/src/formats/yaml.js index c0384e4b..045d337d 100644 --- a/src/formats/yaml.js +++ b/src/formats/yaml.js @@ -1,5 +1,6 @@ import yaml from 'js-yaml'; import moment from 'moment'; +import ImageProxy from '../valueObjects/ImageProxy'; const MomentType = new yaml.Type('date', { kind: 'scalar', @@ -14,9 +15,23 @@ const MomentType = new yaml.Type('date', { } }); +const ImageType = new yaml.Type('image', { + kind: 'scalar', + instanceOf: ImageProxy, + represent: function(value) { + return `${value.uri}`; + }, + resolve: function(value) { + if (value === null) return false; + if (value instanceof ImageProxy) return true; + return false; + } +}); + + const OutputSchema = new yaml.Schema({ include: yaml.DEFAULT_SAFE_SCHEMA.include, - implicit: [MomentType].concat(yaml.DEFAULT_SAFE_SCHEMA.implicit), + implicit: [MomentType, ImageType].concat(yaml.DEFAULT_SAFE_SCHEMA.implicit), explicit: yaml.DEFAULT_SAFE_SCHEMA.explicit }); diff --git a/src/valueObjects/ImageProxy.js b/src/valueObjects/ImageProxy.js new file mode 100644 index 00000000..94e97672 --- /dev/null +++ b/src/valueObjects/ImageProxy.js @@ -0,0 +1,14 @@ +let config; +export const setConfig = (configObj) => { + config = configObj; +}; + +export default function ImageProxy(name, size, objectURL, uploaded = false) { + this.uploaded = uploaded; + this.name = name; + this.size = size || 0; + this.uri = config.media_folder && !uploaded ? config.media_folder + '/' + name : name; + this.toString = function() { + return uploaded ? this.uri : objectURL; + }; +}