refactor: Creating Medias reducer

This commit is contained in:
Cássio Zen 2016-06-10 00:16:01 -03:00
parent 9275aaec90
commit 1ba98fdb5a
21 changed files with 130 additions and 121 deletions

View File

@ -1,7 +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'; import * as MediaProxy from '../valueObjects/MediaProxy';
export const CONFIG_REQUEST = 'CONFIG_REQUEST'; export const CONFIG_REQUEST = 'CONFIG_REQUEST';
export const CONFIG_SUCCESS = 'CONFIG_SUCCESS'; export const CONFIG_SUCCESS = 'CONFIG_SUCCESS';
@ -30,7 +30,7 @@ export function configFailed(err) {
export function configDidLoad(config) { export function configDidLoad(config) {
return (dispatch) => { return (dispatch) => {
ImageProxy.setConfig(config); MediaProxy.setConfig(config);
dispatch(configLoaded(config)); dispatch(configLoaded(config));
}; };
} }

View File

@ -14,8 +14,7 @@ export const ENTRIES_FAILURE = 'ENTRIES_FAILURE';
export const DRAFT_CREATE = 'DRAFT_CREATE'; export const DRAFT_CREATE = 'DRAFT_CREATE';
export const DRAFT_DISCARD = 'DRAFT_DISCARD'; export const DRAFT_DISCARD = 'DRAFT_DISCARD';
export const DRAFT_CHANGE = 'DRAFT_CHANGE'; 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_REQUEST = 'ENTRY_PERSIST_REQUEST';
export const ENTRY_PERSIST_SUCCESS = 'ENTRY_PERSIST_SUCCESS'; export const ENTRY_PERSIST_SUCCESS = 'ENTRY_PERSIST_SUCCESS';
@ -125,20 +124,6 @@ export function changeDraft(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 * Exported Thunk Action Creators
*/ */

5
src/actions/media.js Normal file
View File

@ -0,0 +1,5 @@
export const ADD_MEDIA = 'ADD_MEDIA';
export function addMedia(file) {
return { type: ADD_MEDIA, payload: file };
}

View File

@ -5,31 +5,6 @@ function getSlug(path) {
return m && m[1]; return m && m[1];
} }
function getFileData(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.onerror = function() {
reject('Unable to read file');
};
reader.readAsDataURL(file);
});
}
// Only necessary in test-repo, where images won't actually be persisted on server
function changeFilePathstoBase64(mediaFolder, content, mediaFiles, base64Files) {
let _content = content;
mediaFiles.forEach((media, index) => {
const reg = new RegExp('\\b' + mediaFolder + '/' + media.name + '\\b', 'g');
_content = _content.replace(reg, base64Files[index]);
});
return _content;
}
export default class TestRepo { export default class TestRepo {
constructor(config) { constructor(config) {
this.config = config; this.config = config;
@ -74,17 +49,9 @@ export default class TestRepo {
} }
persist(collection, entry, mediaFiles = []) { persist(collection, entry, mediaFiles = []) {
return new Promise((resolve, reject) => { const folder = collection.get('folder');
Promise.all(mediaFiles.map((file) => getFileData(file))).then( const fileName = entry.path.substring(entry.path.lastIndexOf('/') + 1);
(base64Files) => { window.repoFiles[folder][fileName]['content'] = entry.raw;
const content = changeFilePathstoBase64(this.config.get('media_folder'), entry.raw, mediaFiles, base64Files); return Promise.resolve({collection, entry});
const folder = collection.get('folder');
const fileName = entry.path.substring(entry.path.lastIndexOf('/') + 1);
window.repoFiles[folder][fileName]['content'] = content;
resolve({collection, entry});
},
(error) => reject({collection, entry, error})
);
});
} }
} }

View File

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

View File

@ -5,7 +5,7 @@ import PreviewPane from './PreviewPane';
export default class EntryEditor extends React.Component { export default class EntryEditor extends React.Component {
render() { render() {
const { collection, entry, onChange, onAddMedia, onRemoveMedia, onPersist } = this.props; const { collection, entry, getMedia, onChange, onAddMedia, 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>
@ -14,13 +14,13 @@ export default class EntryEditor extends React.Component {
<ControlPane <ControlPane
collection={collection} collection={collection}
entry={entry} entry={entry}
getMedia={getMedia}
onChange={onChange} onChange={onChange}
onAddMedia={onAddMedia} onAddMedia={onAddMedia}
onRemoveMedia={onRemoveMedia}
/> />
</div> </div>
<div className="cms-preview-pane" style={styles.pane}> <div className="cms-preview-pane" style={styles.pane}>
<PreviewPane collection={collection} entry={entry}/> <PreviewPane collection={collection} entry={entry} getMedia={getMedia} />
</div> </div>
</div> </div>
<button onClick={onPersist}>Save</button> <button onClick={onPersist}>Save</button>

View File

@ -3,12 +3,13 @@ import Widgets from './Widgets';
export default class PreviewPane extends React.Component { export default class PreviewPane extends React.Component {
previewFor(field) { previewFor(field) {
const { entry } = this.props; const { entry, getMedia } = this.props;
const widget = Widgets[field.get('widget')] || Widgets._unknown; const widget = Widgets[field.get('widget')] || Widgets._unknown;
return React.createElement(widget.Preview, { return React.createElement(widget.Preview, {
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')]),
getMedia: getMedia,
}); });
} }

View File

@ -1,6 +1,5 @@
import React from 'react'; import React from 'react';
import { truncateMiddle } from '../../lib/textHelper'; import { truncateMiddle } from '../../lib/textHelper';
import ImageProxy from '../../valueObjects/ImageProxy';
const MAX_DISPLAY_LENGTH = 50; const MAX_DISPLAY_LENGTH = 50;
@ -9,7 +8,7 @@ export default class ImageControl extends React.Component {
super(props); super(props);
this.state = { this.state = {
currentImage: props.value ? new ImageProxy(props.value, null, true) : null currentImage: props.value
}; };
this.revokeCurrentImage = this.revokeCurrentImage.bind(this); this.revokeCurrentImage = this.revokeCurrentImage.bind(this);
@ -22,8 +21,8 @@ export default class ImageControl extends React.Component {
} }
revokeCurrentImage() { revokeCurrentImage() {
if (this.state.currentImage && !this.state.currentImage.uploaded) { if (this.state.currentImage) {
this.props.onRemoveMedia(this.state.currentImage); //this.props.onRemoveMedia(this.state.currentImage);
} }
} }
@ -49,7 +48,6 @@ export default class ImageControl extends React.Component {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
this.revokeCurrentImage(); this.revokeCurrentImage();
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\//;
@ -62,17 +60,20 @@ export default class ImageControl extends React.Component {
}); });
if (file) { if (file) {
imageRef = new ImageProxy(file.name, window.URL.createObjectURL(file, {oneTimeOnly: true}));
this.props.onAddMedia(file); this.props.onAddMedia(file);
this.props.onChange(file.name);
this.setState({currentImage: file.name});
} else {
this.props.onChange(null);
this.setState({currentImage: null});
} }
this.props.onChange(imageRef);
this.setState({currentImage: imageRef});
} }
renderImageName() { renderImageName() {
if (!this.state.currentImage) return null; if (!this.state.currentImage) return null;
return truncateMiddle(this.state.currentImage.uri, MAX_DISPLAY_LENGTH); return truncateMiddle(this.props.getMedia(this.state.currentImage).uri, MAX_DISPLAY_LENGTH);
} }
render() { render() {

View File

@ -6,7 +6,7 @@ export default class ImagePreview extends React.Component {
} }
render() { render() {
const { value } = this.props; const { value, getMedia } = this.props;
return value ? <img src={value}/> : null; return value ? <img src={getMedia(value)}/> : null;
} }
} }

View File

@ -2,7 +2,7 @@ import React from 'react';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { loadEntries } from '../actions/entries'; import { loadEntries } from '../actions/entries';
import { selectEntries } from '../reducers/entries'; import { selectEntries } from '../reducers';
import EntryListing from '../components/EntryListing'; import EntryListing from '../components/EntryListing';
class DashboardPage extends React.Component { class DashboardPage extends React.Component {

View File

@ -5,11 +5,10 @@ import {
createDraft, createDraft,
discardDraft, discardDraft,
changeDraft, changeDraft,
addMediaToDraft,
removeMediaFromDraft,
persist persist
} from '../actions/entries'; } from '../actions/entries';
import { selectEntry } from '../reducers/entries'; import { addMedia } from '../actions/media';
import { selectEntry, getMedia } from '../reducers';
import EntryEditor from '../components/EntryEditor'; import EntryEditor from '../components/EntryEditor';
class EntryPage extends React.Component { class EntryPage extends React.Component {
@ -40,17 +39,20 @@ class EntryPage extends React.Component {
} }
render() { render() {
const { entry, entryDraft, collection, handleDraftChange, handleDraftAddMedia, handleDraftRemoveMedia } = this.props; const {
entry, entryDraft, boundGetMedia, collection, handleDraftChange, handleAddMedia, handleDraftRemoveMedia
} = this.props;
if (entry == null || entryDraft.get('entry') == undefined || 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={entryDraft.get('entry')} entry={entryDraft.get('entry')}
getMedia={boundGetMedia}
collection={collection} collection={collection}
onChange={handleDraftChange} onChange={handleDraftChange}
onAddMedia={handleDraftAddMedia} onAddMedia={handleAddMedia}
onRemoveMedia={handleDraftRemoveMedia}
onPersist={this.handlePersist} onPersist={this.handlePersist}
/> />
); );
@ -62,15 +64,15 @@ function mapStateToProps(state, ownProps) {
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}; const boundGetMedia = getMedia.bind(null, state);
return {collection, collections, entryDraft, boundGetMedia, slug, entry};
} }
export default connect( export default connect(
mapStateToProps, mapStateToProps,
{ {
handleDraftChange: changeDraft, handleDraftChange: changeDraft,
handleDraftAddMedia: addMediaToDraft, handleAddMedia: addMedia,
handleDraftRemoveMedia: removeMediaFromDraft,
loadEntry, loadEntry,
createDraft, createDraft,
discardDraft, discardDraft,

View File

@ -1,6 +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'; import MediaProxy from '../valueObjects/MediaProxy';
const MomentType = new yaml.Type('date', { const MomentType = new yaml.Type('date', {
kind: 'scalar', kind: 'scalar',
@ -17,13 +17,13 @@ const MomentType = new yaml.Type('date', {
const ImageType = new yaml.Type('image', { const ImageType = new yaml.Type('image', {
kind: 'scalar', kind: 'scalar',
instanceOf: ImageProxy, instanceOf: MediaProxy,
represent: function(value) { represent: function(value) {
return `${value.uri}`; return `${value.uri}`;
}, },
resolve: function(value) { resolve: function(value) {
if (value === null) return false; if (value === null) return false;
if (value instanceof ImageProxy) return true; if (value instanceof MediaProxy) return true;
return false; return false;
} }
}); });

View File

@ -1,7 +1,7 @@
import Immutable from 'immutable'; import Immutable from 'immutable';
import { AUTH_REQUEST, AUTH_SUCCESS, AUTH_FAILURE } from '../actions/auth'; import { AUTH_REQUEST, AUTH_SUCCESS, AUTH_FAILURE } from '../actions/auth';
export function auth(state = null, action) { const auth = (state = null, action) => {
switch (action.type) { switch (action.type) {
case AUTH_REQUEST: case AUTH_REQUEST:
return Immutable.Map({isFetching: true}); return Immutable.Map({isFetching: true});
@ -13,4 +13,6 @@ export function auth(state = null, action) {
default: default:
return state; return state;
} }
} };
export default auth;

View File

@ -1,7 +1,7 @@
import { OrderedMap, fromJS } from 'immutable'; import { OrderedMap, fromJS } from 'immutable';
import { CONFIG_SUCCESS } from '../actions/config'; import { CONFIG_SUCCESS } from '../actions/config';
export function collections(state = null, action) { const collections = (state = null, action) => {
switch (action.type) { switch (action.type) {
case CONFIG_SUCCESS: case CONFIG_SUCCESS:
const collections = action.payload && action.payload.collections; const collections = action.payload && action.payload.collections;
@ -14,3 +14,5 @@ export function collections(state = null, action) {
return state; return state;
} }
} }
export default collections;

View File

@ -1,7 +1,7 @@
import Immutable from 'immutable'; import Immutable from 'immutable';
import { CONFIG_REQUEST, CONFIG_SUCCESS, CONFIG_FAILURE } from '../actions/config'; import { CONFIG_REQUEST, CONFIG_SUCCESS, CONFIG_FAILURE } from '../actions/config';
export function config(state = null, action) { const config = (state = null, action) => {
switch (action.type) { switch (action.type) {
case CONFIG_REQUEST: case CONFIG_REQUEST:
return Immutable.Map({isFetching: true}); return Immutable.Map({isFetching: true});
@ -12,4 +12,6 @@ export function config(state = null, action) {
default: default:
return state; return state;
} }
} };
export default config;

View File

@ -3,7 +3,7 @@ import {
ENTRY_REQUEST, ENTRY_SUCCESS, ENTRIES_REQUEST, ENTRIES_SUCCESS ENTRY_REQUEST, ENTRY_SUCCESS, ENTRIES_REQUEST, ENTRIES_SUCCESS
} from '../actions/entries'; } from '../actions/entries';
export function entries(state = Map({entities: Map(), pages: Map()}), action) { const entries = (state = Map({entities: Map(), pages: Map()}), action) => {
switch (action.type) { switch (action.type) {
case ENTRY_REQUEST: case ENTRY_REQUEST:
return state.setIn(['entities', `${action.payload.collection}.${action.payload.slug}`, 'isFetching'], true); return state.setIn(['entities', `${action.payload.collection}.${action.payload.slug}`, 'isFetching'], true);
@ -28,13 +28,15 @@ export function entries(state = Map({entities: Map(), pages: Map()}), action) {
default: default:
return state; return state;
} }
} };
export function selectEntry(state, collection, slug) { export const selectEntry = (state, collection, slug) => (
return state.entries.getIn(['entities', `${collection}.${slug}`]); state.getIn(['entities', `${collection}.${slug}`])
} );
export function selectEntries(state, collection) { export const selectEntries = (state, collection) => {
const slugs = state.entries.getIn(['pages', collection, 'ids']); const slugs = state.getIn(['pages', collection, 'ids']);
return slugs && slugs.map((slug) => selectEntry(state, collection, slug)); return slugs && slugs.map((slug) => selectEntry(state, collection, slug));
} };
export default entries;

View File

@ -1,9 +1,10 @@
import { Map, List } from 'immutable'; import { Map, List } from 'immutable';
import { DRAFT_CREATE, DRAFT_DISCARD, DRAFT_CHANGE, DRAFT_ADD_MEDIA, DRAFT_REMOVE_MEDIA } from '../actions/entries'; import { DRAFT_CREATE, DRAFT_DISCARD, DRAFT_CHANGE } from '../actions/entries';
import { ADD_MEDIA } from '../actions/media';
const initialState = Map({entry: Map(), mediaFiles: List()}); const initialState = Map({entry: Map(), mediaFiles: List()});
export function entryDraft(state = Map(), action) { const entryDraft = (state = Map(), action) => {
switch (action.type) { switch (action.type) {
case DRAFT_CREATE: case DRAFT_CREATE:
if (!action.payload) { if (!action.payload) {
@ -20,15 +21,12 @@ export function entryDraft(state = Map(), action) {
case DRAFT_CHANGE: case DRAFT_CHANGE:
return state.set('entry', action.payload); return state.set('entry', action.payload);
case DRAFT_ADD_MEDIA: case ADD_MEDIA:
return state.update('mediaFiles', (list) => list.push(action.payload)); return state.update('mediaFiles', (list) => list.push(action.payload.name));
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: default:
return state; return state;
} }
} };
export default entryDraft;

27
src/reducers/index.js Normal file
View File

@ -0,0 +1,27 @@
import auth from './auth';
import config from './config';
import entries, * as fromEntries from './entries';
import entryDraft from './entryDraft';
import collections from './collections';
import medias, * as fromMedias from './medias';
const reducers = {
auth,
config,
collections,
entries,
entryDraft,
medias
};
export default reducers;
export const selectEntry = (state, collection, slug) =>
fromEntries.selectEntry(state.entries, collection, slug);
export const selectEntries = (state, collection) =>
fromEntries.selectEntries(state.entries, collection);
export const getMedia = (state, fileName) =>
fromMedias.getMedia(state.medias, fileName);

23
src/reducers/medias.js Normal file
View File

@ -0,0 +1,23 @@
import { Map } from 'immutable';
import { ADD_MEDIA } from '../actions/media';
import MediaProxy from '../valueObjects/MediaProxy';
const medias = (state = Map(), action) => {
switch (action.type) {
case ADD_MEDIA:
return state.set(action.payload.name, action.payload);
default:
return state;
}
};
export default medias;
export const getMedia = (state, filePath) => {
const fileName = filePath.substring(filePath.lastIndexOf('/') + 1);
if (state.has(fileName)) {
return new MediaProxy(fileName, window.URL.createObjectURL(state.get(fileName), {oneTimeOnly: true}));
} else {
return new MediaProxy(filePath, null, filePath, true);
}
};

View File

@ -2,18 +2,10 @@ import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
import thunkMiddleware from 'redux-thunk'; import thunkMiddleware from 'redux-thunk';
import { browserHistory } from 'react-router'; import { browserHistory } from 'react-router';
import { syncHistory, routeReducer } from 'react-router-redux'; import { syncHistory, routeReducer } from 'react-router-redux';
import { auth } from '../reducers/auth'; import reducers from '../reducers';
import { config } from '../reducers/config';
import { entries } from '../reducers/entries';
import { entryDraft } from '../reducers/entryDraft';
import { collections } from '../reducers/collections';
const reducer = combineReducers({ const reducer = combineReducers({
auth, ...reducers,
config,
collections,
entries,
entryDraft,
router: routeReducer router: routeReducer
}); });

View File

@ -3,10 +3,10 @@ export const setConfig = (configObj) => {
config = configObj; config = configObj;
}; };
export default function ImageProxy(value, objectURL, uploaded = false) { export default function MediaProxy(value, objectURL, uri, uploaded = false) {
this.value = value; this.value = value;
this.uploaded = uploaded; this.uploaded = uploaded;
this.uri = config.media_folder && !uploaded ? config.media_folder + '/' + value : value; this.uri = uri || config.media_folder && config.media_folder + '/' + value;
this.toString = function() { this.toString = function() {
return uploaded ? this.uri : objectURL; return uploaded ? this.uri : objectURL;
}; };