Moved draft state for an entry (when in edit mode) to Redux

This commit is contained in:
Cássio Zen 2016-06-08 04:42:24 -03:00
parent 2d48743f37
commit 9275aaec90
11 changed files with 206 additions and 112 deletions

View File

@ -1,18 +1,31 @@
import { currentBackend } from '../backends/backend'; import { currentBackend } from '../backends/backend';
/*
* Contant Declarations
*/
export const ENTRY_REQUEST = 'ENTRY_REQUEST'; 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';
export function entryLoading(collection, slug) { export const DRAFT_CREATE = 'DRAFT_CREATE';
export const DRAFT_DISCARD = 'DRAFT_DISCARD';
export const DRAFT_CHANGE = 'DRAFT_CHANGE';
export const DRAFT_ADD_MEDIA = 'DRAFT_ADD_MEDIA';
export const DRAFT_REMOVE_MEDIA = 'DRAFT_REMOVE_MEDIA';
export const ENTRY_PERSIST_REQUEST = 'ENTRY_PERSIST_REQUEST';
export const ENTRY_PERSIST_SUCCESS = 'ENTRY_PERSIST_SUCCESS';
export const ENTRY_PERSIST_FAILURE = 'ENTRY_PERSIST_FAILURE';
/*
* Simple Action Creators (Internal)
*/
function entryLoading(collection, slug) {
return { return {
type: ENTRY_REQUEST, type: ENTRY_REQUEST,
payload: { payload: {
@ -22,7 +35,7 @@ export function entryLoading(collection, slug) {
}; };
} }
export function entryLoaded(collection, entry) { function entryLoaded(collection, entry) {
return { return {
type: ENTRY_SUCCESS, type: ENTRY_SUCCESS,
payload: { payload: {
@ -32,35 +45,7 @@ export function entryLoaded(collection, entry) {
}; };
} }
export function entryPersisting(collection, entry) { function entriesLoading(collection) {
return {
type: ENTRY_PERSIST_REQUEST,
payload: {
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 { return {
type: ENTRIES_REQUEST, type: ENTRIES_REQUEST,
payload: { payload: {
@ -69,7 +54,7 @@ export function entriesLoading(collection) {
}; };
} }
export function entriesLoaded(collection, entries, pagination) { function entriesLoaded(collection, entries, pagination) {
return { return {
type: ENTRIES_SUCCESS, type: ENTRIES_SUCCESS,
payload: { payload: {
@ -80,7 +65,7 @@ export function entriesLoaded(collection, entries, pagination) {
}; };
} }
export function entriesFailed(collection, error) { function entriesFailed(collection, error) {
return { return {
type: ENTRIES_FAILURE, type: ENTRIES_FAILURE,
error: 'Failed to load entries', error: 'Failed to load entries',
@ -89,6 +74,74 @@ export function entriesFailed(collection, error) {
}; };
} }
function entryPersisting(collection, entry) {
return {
type: ENTRY_PERSIST_REQUEST,
payload: {
collection: collection,
entry: entry
}
};
}
function entryPersisted(collection, entry) {
return {
type: ENTRY_PERSIST_SUCCESS,
payload: {
collection: collection,
entry: entry
}
};
}
function entryPersistFail(collection, entry, error) {
return {
type: ENTRIES_FAILURE,
error: 'Failed to persist entry',
payload: error.toString()
};
}
/*
* Exported simple Action Creators
*/
export function createDraft(entry) {
return {
type: DRAFT_CREATE,
payload: entry
};
}
export function discardDraft() {
return {
type: DRAFT_DISCARD
};
}
export function changeDraft(entry) {
return {
type: DRAFT_CHANGE,
payload: entry
};
}
export function addMediaToDraft(mediaFile) {
return {
type: DRAFT_ADD_MEDIA,
payload: mediaFile
};
}
export function removeMediaFromDraft(mediaFile) {
return {
type: DRAFT_REMOVE_MEDIA,
payload: mediaFile
};
}
/*
* Exported Thunk Action Creators
*/
export function loadEntry(collection, slug) { export function loadEntry(collection, slug) {
return (dispatch, getState) => { return (dispatch, getState) => {
const state = getState(); const state = getState();
@ -107,8 +160,10 @@ export function loadEntries(collection) {
const backend = currentBackend(state.config); const backend = currentBackend(state.config);
dispatch(entriesLoading(collection)); dispatch(entriesLoading(collection));
backend.entries(collection) backend.entries(collection).then(
.then((response) => dispatch(entriesLoaded(collection, response.entries, response.pagination))); (response) => dispatch(entriesLoaded(collection, response.entries, response.pagination)),
(error) => dispatch(entriesFailed(collection, error))
);
}; };
} }

View File

@ -67,15 +67,16 @@ class Backend {
}; };
} }
persist(collection, entry, mediaFiles) { persist(collection, entryDraft) {
const entryData = entry.get('data').toJS();
const entryData = entryDraft.getIn(['entry', 'data']).toJS();
const entryObj = { const entryObj = {
path: entry.get('path'), path: entryDraft.getIn(['entry', 'path']),
slug: entry.get('slug'), slug: entryDraft.getIn(['entry', 'slug']),
raw: this.entryToRaw(collection, entryData) raw: this.entryToRaw(collection, entryData)
}; };
return this.implementation.persist(collection, entryObj, mediaFiles.toJS()); return this.implementation.persist(collection, entryObj, entryDraft.get('mediaFiles').toJS());
} }
entryToRaw(collection, entry) { entryToRaw(collection, entry) {

View File

@ -19,10 +19,11 @@ function getFileData(file) {
} }
// Only necessary in test-repo, where images won't actually be persisted on server // Only necessary in test-repo, where images won't actually be persisted on server
function changeFilePathstoBase64(content, mediaFiles, base64Files) { function changeFilePathstoBase64(mediaFolder, content, mediaFiles, base64Files) {
let _content = content; let _content = content;
mediaFiles.forEach((media, index) => { mediaFiles.forEach((media, index) => {
const reg = new RegExp('\\b' + media.uri + '\\b', 'g'); const reg = new RegExp('\\b' + mediaFolder + '/' + media.name + '\\b', 'g');
_content = _content.replace(reg, base64Files[index]); _content = _content.replace(reg, base64Files[index]);
}); });
@ -74,12 +75,11 @@ export default class TestRepo {
persist(collection, entry, mediaFiles = []) { persist(collection, entry, mediaFiles = []) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
Promise.all(mediaFiles.map((imageProxy) => getFileData(imageProxy.file))).then( Promise.all(mediaFiles.map((file) => getFileData(file))).then(
(base64Files) => { (base64Files) => {
const content = changeFilePathstoBase64(entry.raw, mediaFiles, base64Files); const content = changeFilePathstoBase64(this.config.get('media_folder'), entry.raw, mediaFiles, base64Files);
const folder = collection.get('folder'); const folder = collection.get('folder');
const fileName = entry.path.substring(entry.path.lastIndexOf('/') + 1); const fileName = entry.path.substring(entry.path.lastIndexOf('/') + 1);
window.repoFiles[folder][fileName]['content'] = content; window.repoFiles[folder][fileName]['content'] = content;
resolve({collection, entry}); resolve({collection, entry});
}, },

View File

@ -3,15 +3,15 @@ import Widgets from './Widgets';
export default class ControlPane extends React.Component { export default class ControlPane extends React.Component {
controlFor(field) { controlFor(field) {
const { entry } = this.props; const { entry, onChange, onAddMedia, onRemoveMedia } = this.props;
const widget = Widgets[field.get('widget')] || Widgets._unknown; const widget = Widgets[field.get('widget')] || Widgets._unknown;
return React.createElement(widget.Control, { return React.createElement(widget.Control, {
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) => onChange(entry.setIn(['data', field.get('name')], value)),
onAddMedia: (mediaFile) => this.props.onAddMedia(mediaFile), onAddMedia: onAddMedia,
onRemoveMedia: (mediaFile) => this.props.onRemoveMedia(mediaFile) onRemoveMedia: onRemoveMedia
}); });
} }

View File

@ -1,44 +1,11 @@
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) {
super(props);
this.state = {
entry: props.entry,
mediaFiles: Immutable.List()
};
this.handleChange = this.handleChange.bind(this);
this.handleAddMedia = this.handleAddMedia.bind(this);
this.handleRemoveMedia = this.handleRemoveMedia.bind(this);
this.handleSave = this.handleSave.bind(this);
}
handleChange(entry) {
this.setState({entry: entry});
}
handleAddMedia(mediaFile) {
this.setState({mediaFiles: this.state.mediaFiles.push(mediaFile)});
}
handleRemoveMedia(mediaFile) {
const newState = this.state.mediaFiles.filterNot((item) => item === mediaFile);
this.state = {
entry: this.props.entry,
mediaFiles: Immutable.List(newState)
};
}
handleSave() {
this.props.onPersist(this.state.entry, this.state.mediaFiles);
}
render() { render() {
const { collection, entry } = this.props; const { collection, entry, onChange, onAddMedia, onRemoveMedia, onPersist } = this.props;
return <div> return <div>
<h1>Entry in {collection.get('label')}</h1> <h1>Entry in {collection.get('label')}</h1>
<h2>{entry && entry.get('title')}</h2> <h2>{entry && entry.get('title')}</h2>
@ -46,17 +13,17 @@ export default class EntryEditor extends React.Component {
<div className="cms-control-pane" style={styles.pane}> <div className="cms-control-pane" style={styles.pane}>
<ControlPane <ControlPane
collection={collection} collection={collection}
entry={this.state.entry} entry={entry}
onChange={this.handleChange} onChange={onChange}
onAddMedia={this.handleAddMedia} onAddMedia={onAddMedia}
onRemoveMedia={this.handleRemoveMedia} onRemoveMedia={onRemoveMedia}
/> />
</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={entry}/>
</div> </div>
</div> </div>
<button onClick={this.handleSave}>Save</button> <button onClick={onPersist}>Save</button>
</div>; </div>;
} }
} }

View File

@ -62,8 +62,8 @@ export default class ImageControl extends React.Component {
}); });
if (file) { if (file) {
imageRef = new ImageProxy(file.name, file); imageRef = new ImageProxy(file.name, window.URL.createObjectURL(file, {oneTimeOnly: true}));
this.props.onAddMedia(imageRef); this.props.onAddMedia(file);
} }
this.props.onChange(imageRef); this.props.onChange(imageRef);

View File

@ -1,32 +1,56 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Map } from 'immutable'; import {
import { loadEntry, persist } from '../actions/entries'; loadEntry,
createDraft,
discardDraft,
changeDraft,
addMediaToDraft,
removeMediaFromDraft,
persist
} from '../actions/entries';
import { selectEntry } from '../reducers/entries'; import { selectEntry } from '../reducers/entries';
import EntryEditor from '../components/EntryEditor'; import EntryEditor from '../components/EntryEditor';
class EntryPage extends React.Component { class EntryPage extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.props.dispatch(loadEntry(props.collection, props.slug)); this.props.loadEntry(props.collection, props.slug);
this.handlePersist = this.handlePersist.bind(this); this.handlePersist = this.handlePersist.bind(this);
} }
handlePersist(entry, mediaFiles) { componentDidMount() {
this.props.dispatch(persist(this.props.collection, entry, mediaFiles)); if (this.props.entry) {
this.props.createDraft(this.props.entry);
}
}
componentWillReceiveProps(nextProps) {
if (this.props.entry !== nextProps.entry && !nextProps.entry.get('isFetching')) {
this.props.createDraft(nextProps.entry);
}
}
componentWillUnmount() {
this.props.discardDraft();
}
handlePersist() {
this.props.persist(this.props.collection, this.props.entryDraft);
} }
render() { render() {
const { entry, collection } = this.props; const { entry, entryDraft, collection, handleDraftChange, handleDraftAddMedia, handleDraftRemoveMedia } = this.props;
if (entry == null || entry.get('isFetching')) { if (entry == null || entryDraft.get('entry') == undefined || entry.get('isFetching')) {
return <div>Loading...</div>; return <div>Loading...</div>;
} }
return ( return (
<EntryEditor <EntryEditor
entry={entry || new Map()} entry={entryDraft.get('entry')}
collection={collection} collection={collection}
onChange={handleDraftChange}
onAddMedia={handleDraftAddMedia}
onRemoveMedia={handleDraftRemoveMedia}
onPersist={this.handlePersist} onPersist={this.handlePersist}
/> />
); );
@ -34,12 +58,22 @@ class EntryPage extends React.Component {
} }
function mapStateToProps(state, ownProps) { function mapStateToProps(state, ownProps) {
const { collections } = state; const { collections, entryDraft } = state;
const collection = collections.get(ownProps.params.name); const collection = collections.get(ownProps.params.name);
const slug = ownProps.params.slug; const slug = ownProps.params.slug;
const entry = selectEntry(state, collection.get('name'), slug); const entry = selectEntry(state, collection.get('name'), slug);
return {collection, collections, entryDraft, slug, entry};
return {collection, collections, slug, entry};
} }
export default connect(mapStateToProps)(EntryPage); export default connect(
mapStateToProps,
{
handleDraftChange: changeDraft,
handleDraftAddMedia: addMediaToDraft,
handleDraftRemoveMedia: removeMediaFromDraft,
loadEntry,
createDraft,
discardDraft,
persist
}
)(EntryPage);

View File

@ -7,6 +7,8 @@ import 'file?name=index.html!../example/index.html';
const store = configureStore(); const store = configureStore();
window.store = store;
const el = document.createElement('div'); const el = document.createElement('div');
document.body.appendChild(el); document.body.appendChild(el);

View File

@ -0,0 +1,34 @@
import { Map, List } from 'immutable';
import { DRAFT_CREATE, DRAFT_DISCARD, DRAFT_CHANGE, DRAFT_ADD_MEDIA, DRAFT_REMOVE_MEDIA } from '../actions/entries';
const initialState = Map({entry: Map(), mediaFiles: List()});
export function entryDraft(state = Map(), action) {
switch (action.type) {
case DRAFT_CREATE:
if (!action.payload) {
// New entry
return initialState;
}
// Existing Entry
return state.withMutations((state) => {
state.set('entry', action.payload);
state.set('mediaFiles', List());
});
case DRAFT_DISCARD:
return initialState;
case DRAFT_CHANGE:
return state.set('entry', action.payload);
case DRAFT_ADD_MEDIA:
return state.update('mediaFiles', (list) => list.push(action.payload));
case DRAFT_REMOVE_MEDIA:
const mediaIndex = state.get('mediaFiles').indexOf(action.payload);
if (mediaIndex === -1) return state;
return state.update('mediaFiles', (list) => list.splice(mediaIndex, 1));
default:
return state;
}
}

View File

@ -5,6 +5,7 @@ import { syncHistory, routeReducer } from 'react-router-redux';
import { auth } from '../reducers/auth'; import { auth } from '../reducers/auth';
import { config } from '../reducers/config'; import { config } from '../reducers/config';
import { entries } from '../reducers/entries'; import { entries } from '../reducers/entries';
import { entryDraft } from '../reducers/entryDraft';
import { collections } from '../reducers/collections'; import { collections } from '../reducers/collections';
const reducer = combineReducers({ const reducer = combineReducers({
@ -12,6 +13,7 @@ const reducer = combineReducers({
config, config,
collections, collections,
entries, entries,
entryDraft,
router: routeReducer router: routeReducer
}); });

View File

@ -3,12 +3,11 @@ export const setConfig = (configObj) => {
config = configObj; config = configObj;
}; };
export default function ImageProxy(value, file, uploaded = false) { export default function ImageProxy(value, objectURL, uploaded = false) {
this.value = value; this.value = value;
this.file = file;
this.uploaded = uploaded; this.uploaded = uploaded;
this.uri = config.media_folder && !uploaded ? config.media_folder + '/' + value : value; this.uri = config.media_folder && !uploaded ? config.media_folder + '/' + value : value;
this.toString = function() { this.toString = function() {
return uploaded ? this.uri : window.URL.createObjectURL(this.file, {oneTimeOnly: true}); return uploaded ? this.uri : objectURL;
}; };
} }