persistence draft
Persisting individual media file objects
This commit is contained in:
parent
a0be4e0ae8
commit
83d03c63ec
@ -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));
|
||||||
|
@ -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))
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
14
src/valueObjects/ImageProxy.js
Normal file
14
src/valueObjects/ImageProxy.js
Normal 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;
|
||||||
|
};
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user