persistence draft

Persisting individual media file objects
This commit is contained in:
Cássio Zen 2016-06-06 21:53:22 -03:00
parent a0be4e0ae8
commit 83d03c63ec
10 changed files with 162 additions and 36 deletions

View File

@ -1,6 +1,7 @@
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import { currentBackend } from '../backends/backend'; import { currentBackend } from '../backends/backend';
import { authenticate } from '../actions/auth'; import { authenticate } from '../actions/auth';
import * as ImageProxy from '../valueObjects/ImageProxy';
export const CONFIG_REQUEST = 'CONFIG_REQUEST'; export const CONFIG_REQUEST = 'CONFIG_REQUEST';
export const CONFIG_SUCCESS = 'CONFIG_SUCCESS'; 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) { export function loadConfig(config) {
if (window.CMS_CONFIG) { if (window.CMS_CONFIG) {
return configLoaded(window.CMS_CONFIG); return configDidLoad(window.CMS_CONFIG);
} }
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(configLoading()); dispatch(configLoading());
@ -40,7 +49,7 @@ export function loadConfig(config) {
} }
response.text().then(parseConfig).then((config) => { response.text().then(parseConfig).then((config) => {
dispatch(configLoaded(config)); dispatch(configDidLoad(config));
const backend = currentBackend(config); const backend = currentBackend(config);
const user = backend && backend.currentUser(); const user = backend && backend.currentUser();
user && dispatch(authenticate(user)); user && dispatch(authenticate(user));

View File

@ -4,6 +4,10 @@ export const ENTRY_REQUEST = 'ENTRY_REQUEST';
export const ENTRY_SUCCESS = 'ENTRY_SUCCESS'; export const ENTRY_SUCCESS = 'ENTRY_SUCCESS';
export const ENTRY_FAILURE = 'ENTRY_FAILURE'; 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_REQUEST = 'ENTRIES_REQUEST';
export const ENTRIES_SUCCESS = 'ENTRIES_SUCCESS'; export const ENTRIES_SUCCESS = 'ENTRIES_SUCCESS';
export const ENTRIES_FAILURE = 'ENTRIES_FAILURE'; 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 { return {
type: ENTRIES_SUCCESS, type: ENTRY_PERSIST_REQUEST,
payload: { payload: {
collection: collection.get('name'), collection: collection,
entries: entries, entry: entry
pages: pagination
} }
}; };
} }
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) { export function entriesLoading(collection) {
return { return {
type: ENTRIES_REQUEST, 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) { export function entriesFailed(collection, error) {
return { return {
type: ENTRIES_FAILURE, type: ENTRIES_FAILURE,
@ -76,6 +108,18 @@ export function loadEntries(collection) {
dispatch(entriesLoading(collection)); dispatch(entriesLoading(collection));
backend.entries(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))
);
}; };
} }

View File

@ -66,6 +66,22 @@ class Backend {
return entry; 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) { export function resolveBackend(config) {

View File

@ -47,4 +47,9 @@ export default class TestRepo {
response.entries.filter((entry) => entry.slug === slug)[0] 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});
}
} }

View File

@ -9,7 +9,8 @@ export default class ControlPane extends React.Component {
key: field.get('name'), key: field.get('name'),
field: field, field: field,
value: entry.getIn(['data', field.get('name')]), 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)
}); });
} }

View File

@ -1,18 +1,32 @@
import React from 'react'; import React from 'react';
import Immutable from 'immutable';
import ControlPane from './ControlPane'; import ControlPane from './ControlPane';
import PreviewPane from './PreviewPane'; import PreviewPane from './PreviewPane';
export default class EntryEditor extends React.Component { export default class EntryEditor extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {entry: props.entry}; this.state = {
entry: props.entry,
mediaFiles: Immutable.List()
};
this.handleChange = this.handleChange.bind(this); this.handleChange = this.handleChange.bind(this);
this.handleAddMedia = this.handleAddMedia.bind(this);
this.handleSave = this.handleSave.bind(this);
} }
handleChange(entry) { handleChange(entry) {
this.setState({entry: 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() { render() {
const { collection, entry } = this.props; const { collection, entry } = this.props;
@ -21,12 +35,18 @@ export default class EntryEditor extends React.Component {
<h2>{entry && entry.get('title')}</h2> <h2>{entry && entry.get('title')}</h2>
<div className="cms-container" style={styles.container}> <div className="cms-container" style={styles.container}>
<div className="cms-control-pane" style={styles.pane}> <div className="cms-control-pane" style={styles.pane}>
<ControlPane collection={collection} entry={this.state.entry} onChange={this.handleChange}/> <ControlPane
collection={collection}
entry={this.state.entry}
onChange={this.handleChange}
onAddMedia={this.handleAddMedia}
/>
</div> </div>
<div className="cms-preview-pane" style={styles.pane}> <div className="cms-preview-pane" style={styles.pane}>
<PreviewPane collection={collection} entry={this.state.entry}/> <PreviewPane collection={collection} entry={this.state.entry}/>
</div> </div>
</div> </div>
<button onClick={this.handleSave}>Save</button>
</div>; </div>;
} }
} }

View File

@ -1,14 +1,18 @@
import React from 'react'; import React from 'react';
import ImageProxy from '../../valueObjects/ImageProxy';
export default class ImageControl extends React.Component { export default class ImageControl extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { 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.handleChange = this.handleChange.bind(this);
this.handleFileInputRef = this.handleFileInputRef.bind(this); this.handleFileInputRef = this.handleFileInputRef.bind(this);
this.handleClick = this.handleClick.bind(this); this.handleClick = this.handleClick.bind(this);
@ -18,12 +22,12 @@ export default class ImageControl extends React.Component {
} }
componentWillUnmount() { componentWillUnmount() {
this.revokeCurrentImage(); this.revokeCurrentObjectURL();
} }
revokeCurrentImage() { revokeCurrentObjectURL() {
if (this.state.currentImage instanceof File) { if (this.state.currentImage.file) {
window.URL.revokeObjectURL(this.state.currentImage); window.URL.revokeObjectURL(this.state.currentImage.file);
} }
} }
@ -48,7 +52,8 @@ export default class ImageControl extends React.Component {
handleChange(e) { handleChange(e) {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.revokeCurrentImage(); this.revokeCurrentObjectURL();
let imageRef = null;
const fileList = e.dataTransfer ? e.dataTransfer.files : e.target.files; const fileList = e.dataTransfer ? e.dataTransfer.files : e.target.files;
const files = [...fileList]; const files = [...fileList];
const imageType = /^image\//; const imageType = /^image\//;
@ -61,27 +66,17 @@ export default class ImageControl extends React.Component {
}); });
if (file) { if (file) {
// Custom toString function on file, so it can be used on regular image fields this.props.onAddMedia(file);
file.toString = function() { imageRef = new ImageProxy(file.name, file.size, window.URL.createObjectURL(file));
return window.URL.createObjectURL(file);
};
} }
this.props.onChange(file); this.props.onChange(imageRef);
this.setState({currentImage: file}); this.setState({currentImage: {file:file, imageProxy: imageRef}});
} }
renderImageName() { renderImageName() {
if (!this.state.currentImage) return null; if (!this.state.currentImage.imageProxy) return null;
return this.state.currentImage.imageProxy.uri;
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;
} }
render() { render() {

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Map } from 'immutable'; import { Map } from 'immutable';
import { loadEntry } from '../actions/entries'; import { loadEntry, persist } from '../actions/entries';
import { selectEntry } from '../reducers/entries'; import { selectEntry } from '../reducers/entries';
import EntryEditor from '../components/EntryEditor'; import EntryEditor from '../components/EntryEditor';
@ -9,6 +9,12 @@ class EntryPage extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.props.dispatch(loadEntry(props.collection, props.slug)); 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() { render() {
@ -21,6 +27,7 @@ class EntryPage extends React.Component {
<EntryEditor <EntryEditor
entry={entry || new Map()} entry={entry || new Map()}
collection={collection} collection={collection}
onPersist={this.handlePersist}
/> />
); );
} }

View File

@ -1,5 +1,6 @@
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import moment from 'moment'; import moment from 'moment';
import ImageProxy from '../valueObjects/ImageProxy';
const MomentType = new yaml.Type('date', { const MomentType = new yaml.Type('date', {
kind: 'scalar', 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({ const OutputSchema = new yaml.Schema({
include: yaml.DEFAULT_SAFE_SCHEMA.include, 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 explicit: yaml.DEFAULT_SAFE_SCHEMA.explicit
}); });

View File

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