diff --git a/src/actions/config.js b/src/actions/config.js index f506d705..c0148f3e 100644 --- a/src/actions/config.js +++ b/src/actions/config.js @@ -1,7 +1,7 @@ import yaml from 'js-yaml'; import { currentBackend } from '../backends/backend'; 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_SUCCESS = 'CONFIG_SUCCESS'; @@ -30,7 +30,7 @@ export function configFailed(err) { export function configDidLoad(config) { return (dispatch) => { - ImageProxy.setConfig(config); + MediaProxy.setConfig(config); dispatch(configLoaded(config)); }; } diff --git a/src/actions/entries.js b/src/actions/entries.js index 34b806c8..37cd4f1b 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -14,8 +14,7 @@ export const ENTRIES_FAILURE = 'ENTRIES_FAILURE'; 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'; @@ -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 */ diff --git a/src/actions/media.js b/src/actions/media.js new file mode 100644 index 00000000..20fc6df8 --- /dev/null +++ b/src/actions/media.js @@ -0,0 +1,5 @@ +export const ADD_MEDIA = 'ADD_MEDIA'; + +export function addMedia(file) { + return { type: ADD_MEDIA, payload: file }; +} diff --git a/src/backends/test-repo/implementation.js b/src/backends/test-repo/implementation.js index 5493fc6c..4a3a8a32 100644 --- a/src/backends/test-repo/implementation.js +++ b/src/backends/test-repo/implementation.js @@ -5,31 +5,6 @@ function getSlug(path) { 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 { constructor(config) { this.config = config; @@ -74,17 +49,9 @@ export default class TestRepo { } persist(collection, entry, mediaFiles = []) { - return new Promise((resolve, reject) => { - Promise.all(mediaFiles.map((file) => getFileData(file))).then( - (base64Files) => { - const content = changeFilePathstoBase64(this.config.get('media_folder'), entry.raw, mediaFiles, base64Files); - 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}) - ); - }); + const folder = collection.get('folder'); + const fileName = entry.path.substring(entry.path.lastIndexOf('/') + 1); + window.repoFiles[folder][fileName]['content'] = entry.raw; + return Promise.resolve({collection, entry}); } } diff --git a/src/components/ControlPane.js b/src/components/ControlPane.js index ea6f7162..4dacd838 100644 --- a/src/components/ControlPane.js +++ b/src/components/ControlPane.js @@ -3,7 +3,7 @@ import Widgets from './Widgets'; export default class ControlPane extends React.Component { controlFor(field) { - const { entry, onChange, onAddMedia, onRemoveMedia } = this.props; + const { entry, getMedia, onChange, onAddMedia } = this.props; const widget = Widgets[field.get('widget')] || Widgets._unknown; return React.createElement(widget.Control, { key: field.get('name'), @@ -11,7 +11,7 @@ export default class ControlPane extends React.Component { value: entry.getIn(['data', field.get('name')]), onChange: (value) => onChange(entry.setIn(['data', field.get('name')], value)), onAddMedia: onAddMedia, - onRemoveMedia: onRemoveMedia + getMedia: getMedia }); } diff --git a/src/components/EntryEditor.js b/src/components/EntryEditor.js index 38e6cd3c..7ad3f482 100644 --- a/src/components/EntryEditor.js +++ b/src/components/EntryEditor.js @@ -5,7 +5,7 @@ import PreviewPane from './PreviewPane'; export default class EntryEditor extends React.Component { render() { - const { collection, entry, onChange, onAddMedia, onRemoveMedia, onPersist } = this.props; + const { collection, entry, getMedia, onChange, onAddMedia, onPersist } = this.props; return

Entry in {collection.get('label')}

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

@@ -14,13 +14,13 @@ export default class EntryEditor extends React.Component {
- +
diff --git a/src/components/PreviewPane.js b/src/components/PreviewPane.js index baca6c86..bce60c92 100644 --- a/src/components/PreviewPane.js +++ b/src/components/PreviewPane.js @@ -3,12 +3,13 @@ import Widgets from './Widgets'; export default class PreviewPane extends React.Component { previewFor(field) { - const { entry } = this.props; + const { entry, getMedia } = this.props; const widget = Widgets[field.get('widget')] || Widgets._unknown; return React.createElement(widget.Preview, { key: field.get('name'), field: field, - value: entry.getIn(['data', field.get('name')]) + value: entry.getIn(['data', field.get('name')]), + getMedia: getMedia, }); } diff --git a/src/components/Widgets/ImageControl.js b/src/components/Widgets/ImageControl.js index df8572c2..1e15a0b5 100644 --- a/src/components/Widgets/ImageControl.js +++ b/src/components/Widgets/ImageControl.js @@ -1,6 +1,5 @@ import React from 'react'; import { truncateMiddle } from '../../lib/textHelper'; -import ImageProxy from '../../valueObjects/ImageProxy'; const MAX_DISPLAY_LENGTH = 50; @@ -9,7 +8,7 @@ export default class ImageControl extends React.Component { super(props); this.state = { - currentImage: props.value ? new ImageProxy(props.value, null, true) : null + currentImage: props.value }; this.revokeCurrentImage = this.revokeCurrentImage.bind(this); @@ -22,8 +21,8 @@ export default class ImageControl extends React.Component { } revokeCurrentImage() { - if (this.state.currentImage && !this.state.currentImage.uploaded) { - this.props.onRemoveMedia(this.state.currentImage); + if (this.state.currentImage) { + //this.props.onRemoveMedia(this.state.currentImage); } } @@ -49,7 +48,6 @@ export default class ImageControl extends React.Component { e.stopPropagation(); e.preventDefault(); this.revokeCurrentImage(); - let imageRef = null; const fileList = e.dataTransfer ? e.dataTransfer.files : e.target.files; const files = [...fileList]; const imageType = /^image\//; @@ -62,17 +60,20 @@ export default class ImageControl extends React.Component { }); if (file) { - imageRef = new ImageProxy(file.name, window.URL.createObjectURL(file, {oneTimeOnly: true})); 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() { + 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() { diff --git a/src/components/Widgets/ImagePreview.js b/src/components/Widgets/ImagePreview.js index 06a23cda..8e0a857e 100644 --- a/src/components/Widgets/ImagePreview.js +++ b/src/components/Widgets/ImagePreview.js @@ -6,7 +6,7 @@ export default class ImagePreview extends React.Component { } render() { - const { value } = this.props; - return value ? : null; + const { value, getMedia } = this.props; + return value ? : null; } } diff --git a/src/containers/CollectionPage.js b/src/containers/CollectionPage.js index f8e0072f..2311b8d3 100644 --- a/src/containers/CollectionPage.js +++ b/src/containers/CollectionPage.js @@ -2,7 +2,7 @@ import React from 'react'; import { Link } from 'react-router'; import { connect } from 'react-redux'; import { loadEntries } from '../actions/entries'; -import { selectEntries } from '../reducers/entries'; +import { selectEntries } from '../reducers'; import EntryListing from '../components/EntryListing'; class DashboardPage extends React.Component { diff --git a/src/containers/EntryPage.js b/src/containers/EntryPage.js index 123e7040..25e6c1ec 100644 --- a/src/containers/EntryPage.js +++ b/src/containers/EntryPage.js @@ -5,11 +5,10 @@ import { createDraft, discardDraft, changeDraft, - addMediaToDraft, - removeMediaFromDraft, persist } from '../actions/entries'; -import { selectEntry } from '../reducers/entries'; +import { addMedia } from '../actions/media'; +import { selectEntry, getMedia } from '../reducers'; import EntryEditor from '../components/EntryEditor'; class EntryPage extends React.Component { @@ -40,17 +39,20 @@ class EntryPage extends React.Component { } 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')) { return
Loading...
; } return ( ); @@ -62,15 +64,15 @@ function mapStateToProps(state, ownProps) { const collection = collections.get(ownProps.params.name); const slug = ownProps.params.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( mapStateToProps, { handleDraftChange: changeDraft, - handleDraftAddMedia: addMediaToDraft, - handleDraftRemoveMedia: removeMediaFromDraft, + handleAddMedia: addMedia, loadEntry, createDraft, discardDraft, diff --git a/src/formats/yaml.js b/src/formats/yaml.js index 045d337d..944f3e9e 100644 --- a/src/formats/yaml.js +++ b/src/formats/yaml.js @@ -1,6 +1,6 @@ import yaml from 'js-yaml'; import moment from 'moment'; -import ImageProxy from '../valueObjects/ImageProxy'; +import MediaProxy from '../valueObjects/MediaProxy'; const MomentType = new yaml.Type('date', { kind: 'scalar', @@ -17,13 +17,13 @@ const MomentType = new yaml.Type('date', { const ImageType = new yaml.Type('image', { kind: 'scalar', - instanceOf: ImageProxy, + instanceOf: MediaProxy, represent: function(value) { return `${value.uri}`; }, resolve: function(value) { if (value === null) return false; - if (value instanceof ImageProxy) return true; + if (value instanceof MediaProxy) return true; return false; } }); diff --git a/src/reducers/auth.js b/src/reducers/auth.js index 845ec634..69fbdbff 100644 --- a/src/reducers/auth.js +++ b/src/reducers/auth.js @@ -1,7 +1,7 @@ import Immutable from 'immutable'; import { AUTH_REQUEST, AUTH_SUCCESS, AUTH_FAILURE } from '../actions/auth'; -export function auth(state = null, action) { +const auth = (state = null, action) => { switch (action.type) { case AUTH_REQUEST: return Immutable.Map({isFetching: true}); @@ -13,4 +13,6 @@ export function auth(state = null, action) { default: return state; } -} +}; + +export default auth; diff --git a/src/reducers/collections.js b/src/reducers/collections.js index 48c24002..ca6e61ce 100644 --- a/src/reducers/collections.js +++ b/src/reducers/collections.js @@ -1,7 +1,7 @@ import { OrderedMap, fromJS } from 'immutable'; import { CONFIG_SUCCESS } from '../actions/config'; -export function collections(state = null, action) { +const collections = (state = null, action) => { switch (action.type) { case CONFIG_SUCCESS: const collections = action.payload && action.payload.collections; @@ -14,3 +14,5 @@ export function collections(state = null, action) { return state; } } + +export default collections; diff --git a/src/reducers/config.js b/src/reducers/config.js index c343001a..135f2af1 100644 --- a/src/reducers/config.js +++ b/src/reducers/config.js @@ -1,7 +1,7 @@ import Immutable from 'immutable'; import { CONFIG_REQUEST, CONFIG_SUCCESS, CONFIG_FAILURE } from '../actions/config'; -export function config(state = null, action) { +const config = (state = null, action) => { switch (action.type) { case CONFIG_REQUEST: return Immutable.Map({isFetching: true}); @@ -12,4 +12,6 @@ export function config(state = null, action) { default: return state; } -} +}; + +export default config; diff --git a/src/reducers/entries.js b/src/reducers/entries.js index 6c54c39e..ae77ea05 100644 --- a/src/reducers/entries.js +++ b/src/reducers/entries.js @@ -3,7 +3,7 @@ import { ENTRY_REQUEST, ENTRY_SUCCESS, ENTRIES_REQUEST, ENTRIES_SUCCESS } 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) { case ENTRY_REQUEST: 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: return state; } -} +}; -export function selectEntry(state, collection, slug) { - return state.entries.getIn(['entities', `${collection}.${slug}`]); -} +export const selectEntry = (state, collection, slug) => ( + state.getIn(['entities', `${collection}.${slug}`]) +); -export function selectEntries(state, collection) { - const slugs = state.entries.getIn(['pages', collection, 'ids']); +export const selectEntries = (state, collection) => { + const slugs = state.getIn(['pages', collection, 'ids']); return slugs && slugs.map((slug) => selectEntry(state, collection, slug)); -} +}; + +export default entries; diff --git a/src/reducers/entryDraft.js b/src/reducers/entryDraft.js index cc260c86..7b99298a 100644 --- a/src/reducers/entryDraft.js +++ b/src/reducers/entryDraft.js @@ -1,9 +1,10 @@ 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()}); -export function entryDraft(state = Map(), action) { +const entryDraft = (state = Map(), action) => { switch (action.type) { case DRAFT_CREATE: if (!action.payload) { @@ -20,15 +21,12 @@ export function entryDraft(state = Map(), action) { 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)); + case ADD_MEDIA: + return state.update('mediaFiles', (list) => list.push(action.payload.name)); default: return state; } -} +}; + +export default entryDraft; diff --git a/src/reducers/index.js b/src/reducers/index.js new file mode 100644 index 00000000..19f3a266 --- /dev/null +++ b/src/reducers/index.js @@ -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); diff --git a/src/reducers/medias.js b/src/reducers/medias.js new file mode 100644 index 00000000..46f11f63 --- /dev/null +++ b/src/reducers/medias.js @@ -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); + } +}; diff --git a/src/store/configureStore.js b/src/store/configureStore.js index 13784144..3107dba0 100644 --- a/src/store/configureStore.js +++ b/src/store/configureStore.js @@ -2,18 +2,10 @@ import { createStore, applyMiddleware, combineReducers, compose } from 'redux'; import thunkMiddleware from 'redux-thunk'; import { browserHistory } from 'react-router'; import { syncHistory, routeReducer } from 'react-router-redux'; -import { auth } from '../reducers/auth'; -import { config } from '../reducers/config'; -import { entries } from '../reducers/entries'; -import { entryDraft } from '../reducers/entryDraft'; -import { collections } from '../reducers/collections'; +import reducers from '../reducers'; const reducer = combineReducers({ - auth, - config, - collections, - entries, - entryDraft, + ...reducers, router: routeReducer }); diff --git a/src/valueObjects/ImageProxy.js b/src/valueObjects/MediaProxy.js similarity index 56% rename from src/valueObjects/ImageProxy.js rename to src/valueObjects/MediaProxy.js index 12560bb4..338139eb 100644 --- a/src/valueObjects/ImageProxy.js +++ b/src/valueObjects/MediaProxy.js @@ -3,10 +3,10 @@ export const setConfig = (configObj) => { config = configObj; }; -export default function ImageProxy(value, objectURL, uploaded = false) { +export default function MediaProxy(value, objectURL, uri, uploaded = false) { this.value = value; 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() { return uploaded ? this.uri : objectURL; };