From 83d03c63ec0b956ec8ba255ab40f5aa3ef51a3d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Mon, 6 Jun 2016 21:53:22 -0300 Subject: [PATCH 01/13] persistence draft Persisting individual media file objects --- src/actions/config.js | 13 +++++- src/actions/entries.js | 56 +++++++++++++++++++++--- src/backends/backend.js | 16 +++++++ src/backends/test-repo/implementation.js | 5 +++ src/components/ControlPane.js | 3 +- src/components/EntryEditor.js | 24 +++++++++- src/components/Widgets/ImageControl.js | 41 ++++++++--------- src/containers/EntryPage.js | 9 +++- src/formats/yaml.js | 17 ++++++- src/valueObjects/ImageProxy.js | 14 ++++++ 10 files changed, 162 insertions(+), 36 deletions(-) create mode 100644 src/valueObjects/ImageProxy.js diff --git a/src/actions/config.js b/src/actions/config.js index 51f743cd..f506d705 100644 --- a/src/actions/config.js +++ b/src/actions/config.js @@ -1,6 +1,7 @@ import yaml from 'js-yaml'; import { currentBackend } from '../backends/backend'; import { authenticate } from '../actions/auth'; +import * as ImageProxy from '../valueObjects/ImageProxy'; export const CONFIG_REQUEST = 'CONFIG_REQUEST'; 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) { if (window.CMS_CONFIG) { - return configLoaded(window.CMS_CONFIG); + return configDidLoad(window.CMS_CONFIG); } return (dispatch, getState) => { dispatch(configLoading()); @@ -40,7 +49,7 @@ export function loadConfig(config) { } response.text().then(parseConfig).then((config) => { - dispatch(configLoaded(config)); + dispatch(configDidLoad(config)); const backend = currentBackend(config); const user = backend && backend.currentUser(); user && dispatch(authenticate(user)); diff --git a/src/actions/entries.js b/src/actions/entries.js index dffcecc8..a20d13d8 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -4,6 +4,10 @@ export const ENTRY_REQUEST = 'ENTRY_REQUEST'; export const ENTRY_SUCCESS = 'ENTRY_SUCCESS'; 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_SUCCESS = 'ENTRIES_SUCCESS'; 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 { - type: ENTRIES_SUCCESS, + type: ENTRY_PERSIST_REQUEST, payload: { - collection: collection.get('name'), - entries: entries, - pages: pagination + 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 { 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) { return { type: ENTRIES_FAILURE, @@ -76,6 +108,18 @@ export function loadEntries(collection) { dispatch(entriesLoading(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)) + ); }; } diff --git a/src/backends/backend.js b/src/backends/backend.js index 72dba0c6..8bb51aa4 100644 --- a/src/backends/backend.js +++ b/src/backends/backend.js @@ -66,6 +66,22 @@ class Backend { 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) { diff --git a/src/backends/test-repo/implementation.js b/src/backends/test-repo/implementation.js index 3f4ed4bd..c14bfe31 100644 --- a/src/backends/test-repo/implementation.js +++ b/src/backends/test-repo/implementation.js @@ -47,4 +47,9 @@ export default class TestRepo { 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}); + } } diff --git a/src/components/ControlPane.js b/src/components/ControlPane.js index 203bc581..c5225a55 100644 --- a/src/components/ControlPane.js +++ b/src/components/ControlPane.js @@ -9,7 +9,8 @@ export default class ControlPane extends React.Component { key: field.get('name'), field: field, 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) }); } diff --git a/src/components/EntryEditor.js b/src/components/EntryEditor.js index 488b5cb3..163f13e1 100644 --- a/src/components/EntryEditor.js +++ b/src/components/EntryEditor.js @@ -1,18 +1,32 @@ import React from 'react'; +import Immutable from 'immutable'; import ControlPane from './ControlPane'; import PreviewPane from './PreviewPane'; export default class EntryEditor extends React.Component { constructor(props) { super(props); - this.state = {entry: props.entry}; + this.state = { + entry: props.entry, + mediaFiles: Immutable.List() + }; this.handleChange = this.handleChange.bind(this); + this.handleAddMedia = this.handleAddMedia.bind(this); + this.handleSave = this.handleSave.bind(this); } handleChange(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() { const { collection, entry } = this.props; @@ -21,12 +35,18 @@ export default class EntryEditor extends React.Component {

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

- +
+ ; } } diff --git a/src/components/Widgets/ImageControl.js b/src/components/Widgets/ImageControl.js index 68d9c884..8d8efe98 100644 --- a/src/components/Widgets/ImageControl.js +++ b/src/components/Widgets/ImageControl.js @@ -1,14 +1,18 @@ import React from 'react'; +import ImageProxy from '../../valueObjects/ImageProxy'; export default class ImageControl extends React.Component { constructor(props) { super(props); 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.handleFileInputRef = this.handleFileInputRef.bind(this); this.handleClick = this.handleClick.bind(this); @@ -18,12 +22,12 @@ export default class ImageControl extends React.Component { } componentWillUnmount() { - this.revokeCurrentImage(); + this.revokeCurrentObjectURL(); } - revokeCurrentImage() { - if (this.state.currentImage instanceof File) { - window.URL.revokeObjectURL(this.state.currentImage); + revokeCurrentObjectURL() { + if (this.state.currentImage.file) { + window.URL.revokeObjectURL(this.state.currentImage.file); } } @@ -48,7 +52,8 @@ export default class ImageControl extends React.Component { handleChange(e) { e.stopPropagation(); e.preventDefault(); - this.revokeCurrentImage(); + this.revokeCurrentObjectURL(); + let imageRef = null; const fileList = e.dataTransfer ? e.dataTransfer.files : e.target.files; const files = [...fileList]; const imageType = /^image\//; @@ -61,27 +66,17 @@ export default class ImageControl extends React.Component { }); if (file) { - // Custom toString function on file, so it can be used on regular image fields - file.toString = function() { - return window.URL.createObjectURL(file); - }; + this.props.onAddMedia(file); + imageRef = new ImageProxy(file.name, file.size, window.URL.createObjectURL(file)); } - this.props.onChange(file); - this.setState({currentImage: file}); + this.props.onChange(imageRef); + this.setState({currentImage: {file:file, imageProxy: imageRef}}); } renderImageName() { - if (!this.state.currentImage) return null; - - 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; + if (!this.state.currentImage.imageProxy) return null; + return this.state.currentImage.imageProxy.uri; } render() { diff --git a/src/containers/EntryPage.js b/src/containers/EntryPage.js index 8abfdf18..584a1402 100644 --- a/src/containers/EntryPage.js +++ b/src/containers/EntryPage.js @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { Map } from 'immutable'; -import { loadEntry } from '../actions/entries'; +import { loadEntry, persist } from '../actions/entries'; import { selectEntry } from '../reducers/entries'; import EntryEditor from '../components/EntryEditor'; @@ -9,6 +9,12 @@ class EntryPage extends React.Component { constructor(props) { super(props); 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() { @@ -21,6 +27,7 @@ class EntryPage extends React.Component { ); } diff --git a/src/formats/yaml.js b/src/formats/yaml.js index c0384e4b..045d337d 100644 --- a/src/formats/yaml.js +++ b/src/formats/yaml.js @@ -1,5 +1,6 @@ import yaml from 'js-yaml'; import moment from 'moment'; +import ImageProxy from '../valueObjects/ImageProxy'; const MomentType = new yaml.Type('date', { 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({ 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 }); diff --git a/src/valueObjects/ImageProxy.js b/src/valueObjects/ImageProxy.js new file mode 100644 index 00000000..94e97672 --- /dev/null +++ b/src/valueObjects/ImageProxy.js @@ -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; + }; +} From 8d7f5702e5ffc42ebd027b4d76caec9b979c674e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Tue, 7 Jun 2016 11:38:19 -0300 Subject: [PATCH 02/13] Added onRemoveMedia callback --- src/components/ControlPane.js | 3 ++- src/components/EntryEditor.js | 10 ++++++++++ src/components/Widgets/ImageControl.js | 9 +++++---- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/components/ControlPane.js b/src/components/ControlPane.js index c5225a55..cd7c08de 100644 --- a/src/components/ControlPane.js +++ b/src/components/ControlPane.js @@ -10,7 +10,8 @@ export default class ControlPane extends React.Component { field: field, value: entry.getIn(['data', field.get('name')]), onChange: (value) => this.props.onChange(entry.setIn(['data', field.get('name')], value)), - onAddMedia: (mediaFile) => this.props.onAddMedia(mediaFile) + onAddMedia: (mediaFile) => this.props.onAddMedia(mediaFile), + onRemoveMedia: (mediaFile) => this.props.onRemoveMedia(mediaFile) }); } diff --git a/src/components/EntryEditor.js b/src/components/EntryEditor.js index 163f13e1..9bab99a9 100644 --- a/src/components/EntryEditor.js +++ b/src/components/EntryEditor.js @@ -12,6 +12,7 @@ export default class EntryEditor extends React.Component { }; this.handleChange = this.handleChange.bind(this); this.handleAddMedia = this.handleAddMedia.bind(this); + this.handleRemoveMedia = this.handleRemoveMedia.bind(this); this.handleSave = this.handleSave.bind(this); } @@ -23,6 +24,14 @@ export default class EntryEditor extends React.Component { 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); } @@ -40,6 +49,7 @@ export default class EntryEditor extends React.Component { entry={this.state.entry} onChange={this.handleChange} onAddMedia={this.handleAddMedia} + onRemoveMedia={this.handleRemoveMedia} />
diff --git a/src/components/Widgets/ImageControl.js b/src/components/Widgets/ImageControl.js index 8d8efe98..6040d5f2 100644 --- a/src/components/Widgets/ImageControl.js +++ b/src/components/Widgets/ImageControl.js @@ -12,7 +12,7 @@ export default class ImageControl extends React.Component { } }; - this.revokeCurrentObjectURL = this.revokeCurrentObjectURL.bind(this); + this.revokeCurrentImage = this.revokeCurrentImage.bind(this); this.handleChange = this.handleChange.bind(this); this.handleFileInputRef = this.handleFileInputRef.bind(this); this.handleClick = this.handleClick.bind(this); @@ -22,11 +22,12 @@ export default class ImageControl extends React.Component { } componentWillUnmount() { - this.revokeCurrentObjectURL(); + this.revokeCurrentImage(); } - revokeCurrentObjectURL() { + revokeCurrentImage() { if (this.state.currentImage.file) { + this.props.onRemoveMedia(this.state.currentImage.file); window.URL.revokeObjectURL(this.state.currentImage.file); } } @@ -52,7 +53,7 @@ export default class ImageControl extends React.Component { handleChange(e) { e.stopPropagation(); e.preventDefault(); - this.revokeCurrentObjectURL(); + this.revokeCurrentImage(); let imageRef = null; const fileList = e.dataTransfer ? e.dataTransfer.files : e.target.files; const files = [...fileList]; From 0cf5dc141aefd98c748b172e8d80add81d3c56dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Tue, 7 Jun 2016 20:15:28 -0300 Subject: [PATCH 03/13] in memmory persistence for test-repo --- src/backends/test-repo/implementation.js | 41 ++++++++++++++++++++++-- src/components/Widgets/ImageControl.js | 30 ++++++++--------- src/valueObjects/ImageProxy.js | 10 +++--- 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/src/backends/test-repo/implementation.js b/src/backends/test-repo/implementation.js index c14bfe31..157847da 100644 --- a/src/backends/test-repo/implementation.js +++ b/src/backends/test-repo/implementation.js @@ -5,6 +5,30 @@ 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(content, mediaFiles, base64Files) { + let _content = content; + mediaFiles.forEach((media, index) => { + const reg = new RegExp('\\b' + media.uri + '\\b', 'g'); + _content = _content.replace(reg, base64Files[index]); + }); + + return _content; +} + export default class TestRepo { constructor(config) { this.config = config; @@ -48,8 +72,19 @@ export default class TestRepo { )); } - persist(collection, entry, mediaFiles) { - alert('This will be the persisted data:\n' + entry.raw); - return Promise.resolve({collection, entry}); + persist(collection, entry, mediaFiles = []) { + return new Promise((resolve, reject) => { + Promise.all(mediaFiles.map((imageProxy) => getFileData(imageProxy.file))).then( + (base64Files) => { + const content = changeFilePathstoBase64(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}) + ); + }); } } diff --git a/src/components/Widgets/ImageControl.js b/src/components/Widgets/ImageControl.js index 6040d5f2..4cfc9027 100644 --- a/src/components/Widgets/ImageControl.js +++ b/src/components/Widgets/ImageControl.js @@ -1,15 +1,14 @@ import React from 'react'; import ImageProxy from '../../valueObjects/ImageProxy'; +const MAX_DISPLAY_LENGTH = 50; + export default class ImageControl extends React.Component { constructor(props) { super(props); this.state = { - currentImage: { - file: null, - imageProxy: new ImageProxy(props.value, null, null, true) - } + currentImage: new ImageProxy(props.value, null, true) }; this.revokeCurrentImage = this.revokeCurrentImage.bind(this); @@ -21,14 +20,9 @@ export default class ImageControl extends React.Component { this.renderImageName = this.renderImageName.bind(this); } - componentWillUnmount() { - this.revokeCurrentImage(); - } - revokeCurrentImage() { - if (this.state.currentImage.file) { - this.props.onRemoveMedia(this.state.currentImage.file); - window.URL.revokeObjectURL(this.state.currentImage.file); + if (this.state.currentImage && !this.state.currentImage.uploaded) { + this.props.onRemoveMedia(this.state.currentImage); } } @@ -67,17 +61,21 @@ export default class ImageControl extends React.Component { }); if (file) { - this.props.onAddMedia(file); - imageRef = new ImageProxy(file.name, file.size, window.URL.createObjectURL(file)); + imageRef = new ImageProxy(file.name, file); + this.props.onAddMedia(imageRef); } this.props.onChange(imageRef); - this.setState({currentImage: {file:file, imageProxy: imageRef}}); + this.setState({currentImage: imageRef}); } renderImageName() { - if (!this.state.currentImage.imageProxy) return null; - return this.state.currentImage.imageProxy.uri; + if (!this.state.currentImage) return null; + if (this.state.currentImage.uri.length < MAX_DISPLAY_LENGTH) { + return this.state.currentImage.uri; + } + + return this.state.currentImage.uri.substring(0, MAX_DISPLAY_LENGTH) + '\u2026'; } render() { diff --git a/src/valueObjects/ImageProxy.js b/src/valueObjects/ImageProxy.js index 94e97672..564d69a0 100644 --- a/src/valueObjects/ImageProxy.js +++ b/src/valueObjects/ImageProxy.js @@ -3,12 +3,12 @@ export const setConfig = (configObj) => { config = configObj; }; -export default function ImageProxy(name, size, objectURL, uploaded = false) { +export default function ImageProxy(value, file, uploaded = false) { + this.value = value; + this.file = file; this.uploaded = uploaded; - this.name = name; - this.size = size || 0; - this.uri = config.media_folder && !uploaded ? config.media_folder + '/' + name : name; + this.uri = config.media_folder && !uploaded ? config.media_folder + '/' + value : value; this.toString = function() { - return uploaded ? this.uri : objectURL; + return uploaded ? this.uri : window.URL.createObjectURL(this.file, {oneTimeOnly: true}); }; } From d5da853db1b21f8b5780a25de8f990305edbb9ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Tue, 7 Jun 2016 21:04:40 -0300 Subject: [PATCH 04/13] Only create ImageProxy if there's a value prop --- src/components/Widgets/ImageControl.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Widgets/ImageControl.js b/src/components/Widgets/ImageControl.js index 4cfc9027..f6322fd0 100644 --- a/src/components/Widgets/ImageControl.js +++ b/src/components/Widgets/ImageControl.js @@ -8,7 +8,7 @@ export default class ImageControl extends React.Component { super(props); this.state = { - currentImage: new ImageProxy(props.value, null, true) + currentImage: props.value ? new ImageProxy(props.value, null, true) : null }; this.revokeCurrentImage = this.revokeCurrentImage.bind(this); From 327cb883eee0258c3cb3e2f4131ee613b06a1d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Tue, 7 Jun 2016 21:27:34 -0300 Subject: [PATCH 05/13] Refinment: truncate in middle --- src/components/Widgets/ImageControl.js | 9 +++++---- src/lib/textHelper.js | 3 +++ 2 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 src/lib/textHelper.js diff --git a/src/components/Widgets/ImageControl.js b/src/components/Widgets/ImageControl.js index f6322fd0..8c05ce94 100644 --- a/src/components/Widgets/ImageControl.js +++ b/src/components/Widgets/ImageControl.js @@ -1,4 +1,5 @@ import React from 'react'; +import { truncateMiddle } from '../../lib/textHelper'; import ImageProxy from '../../valueObjects/ImageProxy'; const MAX_DISPLAY_LENGTH = 50; @@ -71,11 +72,11 @@ export default class ImageControl extends React.Component { renderImageName() { if (!this.state.currentImage) return null; - if (this.state.currentImage.uri.length < MAX_DISPLAY_LENGTH) { - return this.state.currentImage.uri; + const { uri } = this.state.currentImage; + if (uri.length <= MAX_DISPLAY_LENGTH) { + return uri; } - - return this.state.currentImage.uri.substring(0, MAX_DISPLAY_LENGTH) + '\u2026'; + return truncateMiddle(uri, MAX_DISPLAY_LENGTH); } render() { diff --git a/src/lib/textHelper.js b/src/lib/textHelper.js new file mode 100644 index 00000000..4fd0649a --- /dev/null +++ b/src/lib/textHelper.js @@ -0,0 +1,3 @@ +export function truncateMiddle(string, size) { + return string.substring(0, size / 2) + '\u2026' + string.substring(string.length - size / 2 + 1, string.length); +} From 2d48743f371afd9c860d1492d17bb73ab1f51627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Tue, 7 Jun 2016 21:33:12 -0300 Subject: [PATCH 06/13] moving string size check to helper lib --- src/components/Widgets/ImageControl.js | 6 +----- src/lib/textHelper.js | 5 ++++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/Widgets/ImageControl.js b/src/components/Widgets/ImageControl.js index 8c05ce94..5ca73aa7 100644 --- a/src/components/Widgets/ImageControl.js +++ b/src/components/Widgets/ImageControl.js @@ -72,11 +72,7 @@ export default class ImageControl extends React.Component { renderImageName() { if (!this.state.currentImage) return null; - const { uri } = this.state.currentImage; - if (uri.length <= MAX_DISPLAY_LENGTH) { - return uri; - } - return truncateMiddle(uri, MAX_DISPLAY_LENGTH); + return truncateMiddle(this.state.currentImage.uri, MAX_DISPLAY_LENGTH); } render() { diff --git a/src/lib/textHelper.js b/src/lib/textHelper.js index 4fd0649a..39ffd31a 100644 --- a/src/lib/textHelper.js +++ b/src/lib/textHelper.js @@ -1,3 +1,6 @@ -export function truncateMiddle(string, size) { +export function truncateMiddle(string = '', size) { + if (string.length <= size) { + return string; + } return string.substring(0, size / 2) + '\u2026' + string.substring(string.length - size / 2 + 1, string.length); } From 9275aaec90904ff4700ef10807953ad6d0a2f386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Wed, 8 Jun 2016 04:42:24 -0300 Subject: [PATCH 07/13] Moved draft state for an entry (when in edit mode) to Redux --- src/actions/entries.js | 133 ++++++++++++++++------- src/backends/backend.js | 11 +- src/backends/test-repo/implementation.js | 10 +- src/components/ControlPane.js | 8 +- src/components/EntryEditor.js | 47 ++------ src/components/Widgets/ImageControl.js | 4 +- src/containers/EntryPage.js | 62 ++++++++--- src/index.js | 2 + src/reducers/entryDraft.js | 34 ++++++ src/store/configureStore.js | 2 + src/valueObjects/ImageProxy.js | 5 +- 11 files changed, 206 insertions(+), 112 deletions(-) create mode 100644 src/reducers/entryDraft.js diff --git a/src/actions/entries.js b/src/actions/entries.js index a20d13d8..34b806c8 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -1,18 +1,31 @@ import { currentBackend } from '../backends/backend'; +/* + * Contant Declarations + */ export const ENTRY_REQUEST = 'ENTRY_REQUEST'; export const ENTRY_SUCCESS = 'ENTRY_SUCCESS'; 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_SUCCESS = 'ENTRIES_SUCCESS'; 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 { type: ENTRY_REQUEST, payload: { @@ -22,7 +35,7 @@ export function entryLoading(collection, slug) { }; } -export function entryLoaded(collection, entry) { +function entryLoaded(collection, entry) { return { type: ENTRY_SUCCESS, payload: { @@ -32,35 +45,7 @@ export function entryLoaded(collection, entry) { }; } -export function entryPersisting(collection, entry) { - 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) { +function entriesLoading(collection) { return { type: ENTRIES_REQUEST, payload: { @@ -69,7 +54,7 @@ export function entriesLoading(collection) { }; } -export function entriesLoaded(collection, entries, pagination) { +function entriesLoaded(collection, entries, pagination) { return { type: ENTRIES_SUCCESS, payload: { @@ -80,7 +65,7 @@ export function entriesLoaded(collection, entries, pagination) { }; } -export function entriesFailed(collection, error) { +function entriesFailed(collection, error) { return { type: ENTRIES_FAILURE, 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) { return (dispatch, getState) => { const state = getState(); @@ -107,8 +160,10 @@ export function loadEntries(collection) { const backend = currentBackend(state.config); dispatch(entriesLoading(collection)); - backend.entries(collection) - .then((response) => dispatch(entriesLoaded(collection, response.entries, response.pagination))); + backend.entries(collection).then( + (response) => dispatch(entriesLoaded(collection, response.entries, response.pagination)), + (error) => dispatch(entriesFailed(collection, error)) + ); }; } diff --git a/src/backends/backend.js b/src/backends/backend.js index 8bb51aa4..5b8c7108 100644 --- a/src/backends/backend.js +++ b/src/backends/backend.js @@ -67,15 +67,16 @@ class Backend { }; } - persist(collection, entry, mediaFiles) { - const entryData = entry.get('data').toJS(); + persist(collection, entryDraft) { + + const entryData = entryDraft.getIn(['entry', 'data']).toJS(); const entryObj = { - path: entry.get('path'), - slug: entry.get('slug'), + path: entryDraft.getIn(['entry', 'path']), + slug: entryDraft.getIn(['entry', 'slug']), 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) { diff --git a/src/backends/test-repo/implementation.js b/src/backends/test-repo/implementation.js index 157847da..5493fc6c 100644 --- a/src/backends/test-repo/implementation.js +++ b/src/backends/test-repo/implementation.js @@ -19,10 +19,11 @@ function getFileData(file) { } // 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; + 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]); }); @@ -74,12 +75,11 @@ export default class TestRepo { persist(collection, entry, mediaFiles = []) { return new Promise((resolve, reject) => { - Promise.all(mediaFiles.map((imageProxy) => getFileData(imageProxy.file))).then( + Promise.all(mediaFiles.map((file) => getFileData(file))).then( (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 fileName = entry.path.substring(entry.path.lastIndexOf('/') + 1); - window.repoFiles[folder][fileName]['content'] = content; resolve({collection, entry}); }, diff --git a/src/components/ControlPane.js b/src/components/ControlPane.js index cd7c08de..ea6f7162 100644 --- a/src/components/ControlPane.js +++ b/src/components/ControlPane.js @@ -3,15 +3,15 @@ import Widgets from './Widgets'; export default class ControlPane extends React.Component { controlFor(field) { - const { entry } = this.props; + const { entry, onChange, onAddMedia, onRemoveMedia } = this.props; const widget = Widgets[field.get('widget')] || Widgets._unknown; return React.createElement(widget.Control, { key: field.get('name'), field: field, value: entry.getIn(['data', field.get('name')]), - onChange: (value) => this.props.onChange(entry.setIn(['data', field.get('name')], value)), - onAddMedia: (mediaFile) => this.props.onAddMedia(mediaFile), - onRemoveMedia: (mediaFile) => this.props.onRemoveMedia(mediaFile) + onChange: (value) => onChange(entry.setIn(['data', field.get('name')], value)), + onAddMedia: onAddMedia, + onRemoveMedia: onRemoveMedia }); } diff --git a/src/components/EntryEditor.js b/src/components/EntryEditor.js index 9bab99a9..38e6cd3c 100644 --- a/src/components/EntryEditor.js +++ b/src/components/EntryEditor.js @@ -1,44 +1,11 @@ import React from 'react'; -import Immutable from 'immutable'; import ControlPane from './ControlPane'; import PreviewPane from './PreviewPane'; 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() { - const { collection, entry } = this.props; - + const { collection, entry, onChange, onAddMedia, onRemoveMedia, onPersist } = this.props; return

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

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

@@ -46,17 +13,17 @@ export default class EntryEditor extends React.Component {
- +
- +
; } } diff --git a/src/components/Widgets/ImageControl.js b/src/components/Widgets/ImageControl.js index 5ca73aa7..df8572c2 100644 --- a/src/components/Widgets/ImageControl.js +++ b/src/components/Widgets/ImageControl.js @@ -62,8 +62,8 @@ export default class ImageControl extends React.Component { }); if (file) { - imageRef = new ImageProxy(file.name, file); - this.props.onAddMedia(imageRef); + imageRef = new ImageProxy(file.name, window.URL.createObjectURL(file, {oneTimeOnly: true})); + this.props.onAddMedia(file); } this.props.onChange(imageRef); diff --git a/src/containers/EntryPage.js b/src/containers/EntryPage.js index 584a1402..123e7040 100644 --- a/src/containers/EntryPage.js +++ b/src/containers/EntryPage.js @@ -1,32 +1,56 @@ import React from 'react'; import { connect } from 'react-redux'; -import { Map } from 'immutable'; -import { loadEntry, persist } from '../actions/entries'; +import { + loadEntry, + createDraft, + discardDraft, + changeDraft, + addMediaToDraft, + removeMediaFromDraft, + persist +} from '../actions/entries'; import { selectEntry } from '../reducers/entries'; import EntryEditor from '../components/EntryEditor'; class EntryPage extends React.Component { constructor(props) { super(props); - this.props.dispatch(loadEntry(props.collection, props.slug)); - + this.props.loadEntry(props.collection, props.slug); this.handlePersist = this.handlePersist.bind(this); } - handlePersist(entry, mediaFiles) { - this.props.dispatch(persist(this.props.collection, entry, mediaFiles)); + componentDidMount() { + 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() { - const { entry, collection } = this.props; - if (entry == null || entry.get('isFetching')) { + const { entry, entryDraft, collection, handleDraftChange, handleDraftAddMedia, handleDraftRemoveMedia } = this.props; + if (entry == null || entryDraft.get('entry') == undefined || entry.get('isFetching')) { return
Loading...
; } - return ( ); @@ -34,12 +58,22 @@ class EntryPage extends React.Component { } function mapStateToProps(state, ownProps) { - const { collections } = state; + const { collections, entryDraft } = state; const collection = collections.get(ownProps.params.name); const slug = ownProps.params.slug; const entry = selectEntry(state, collection.get('name'), slug); - - return {collection, collections, slug, entry}; + return {collection, collections, entryDraft, slug, entry}; } -export default connect(mapStateToProps)(EntryPage); +export default connect( + mapStateToProps, + { + handleDraftChange: changeDraft, + handleDraftAddMedia: addMediaToDraft, + handleDraftRemoveMedia: removeMediaFromDraft, + loadEntry, + createDraft, + discardDraft, + persist + } +)(EntryPage); diff --git a/src/index.js b/src/index.js index 7f0ba47c..dd7e27f6 100644 --- a/src/index.js +++ b/src/index.js @@ -7,6 +7,8 @@ import 'file?name=index.html!../example/index.html'; const store = configureStore(); +window.store = store; + const el = document.createElement('div'); document.body.appendChild(el); diff --git a/src/reducers/entryDraft.js b/src/reducers/entryDraft.js new file mode 100644 index 00000000..cc260c86 --- /dev/null +++ b/src/reducers/entryDraft.js @@ -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; + } +} diff --git a/src/store/configureStore.js b/src/store/configureStore.js index d87b77a1..13784144 100644 --- a/src/store/configureStore.js +++ b/src/store/configureStore.js @@ -5,6 +5,7 @@ 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'; const reducer = combineReducers({ @@ -12,6 +13,7 @@ const reducer = combineReducers({ config, collections, entries, + entryDraft, router: routeReducer }); diff --git a/src/valueObjects/ImageProxy.js b/src/valueObjects/ImageProxy.js index 564d69a0..12560bb4 100644 --- a/src/valueObjects/ImageProxy.js +++ b/src/valueObjects/ImageProxy.js @@ -3,12 +3,11 @@ export const setConfig = (configObj) => { config = configObj; }; -export default function ImageProxy(value, file, uploaded = false) { +export default function ImageProxy(value, objectURL, uploaded = false) { this.value = value; - this.file = file; this.uploaded = uploaded; this.uri = config.media_folder && !uploaded ? config.media_folder + '/' + value : value; this.toString = function() { - return uploaded ? this.uri : window.URL.createObjectURL(this.file, {oneTimeOnly: true}); + return uploaded ? this.uri : objectURL; }; } From 1ba98fdb5ae7951836952d9f57cb1e933b3f1f60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Fri, 10 Jun 2016 00:16:01 -0300 Subject: [PATCH 08/13] refactor: Creating Medias reducer --- src/actions/config.js | 4 +- src/actions/entries.js | 17 +------- src/actions/media.js | 5 +++ src/backends/test-repo/implementation.js | 41 ++----------------- src/components/ControlPane.js | 4 +- src/components/EntryEditor.js | 6 +-- src/components/PreviewPane.js | 5 ++- src/components/Widgets/ImageControl.js | 19 +++++---- src/components/Widgets/ImagePreview.js | 4 +- src/containers/CollectionPage.js | 2 +- src/containers/EntryPage.js | 20 +++++---- src/formats/yaml.js | 6 +-- src/reducers/auth.js | 6 ++- src/reducers/collections.js | 4 +- src/reducers/config.js | 6 ++- src/reducers/entries.js | 18 ++++---- src/reducers/entryDraft.js | 18 ++++---- src/reducers/index.js | 27 ++++++++++++ src/reducers/medias.js | 23 +++++++++++ src/store/configureStore.js | 12 +----- .../{ImageProxy.js => MediaProxy.js} | 4 +- 21 files changed, 130 insertions(+), 121 deletions(-) create mode 100644 src/actions/media.js create mode 100644 src/reducers/index.js create mode 100644 src/reducers/medias.js rename src/valueObjects/{ImageProxy.js => MediaProxy.js} (56%) 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; }; From 1700f98e4e12bea2076e6f75c3afc53c1cf6021d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Fri, 10 Jun 2016 14:01:14 -0300 Subject: [PATCH 09/13] Refactor on upload media data fow --- src/actions/media.js | 9 ++++++-- src/components/ControlPane.js | 3 ++- src/components/EntryEditor.js | 3 ++- src/components/Widgets/ImageControl.js | 30 +++++++++++--------------- src/containers/EntryPage.js | 14 ++++++------ src/reducers/entryDraft.js | 6 ++++-- src/reducers/medias.js | 15 +++++++------ src/valueObjects/MediaProxy.js | 7 +++--- 8 files changed, 47 insertions(+), 40 deletions(-) diff --git a/src/actions/media.js b/src/actions/media.js index 20fc6df8..88f822ff 100644 --- a/src/actions/media.js +++ b/src/actions/media.js @@ -1,5 +1,10 @@ export const ADD_MEDIA = 'ADD_MEDIA'; +export const REMOVE_MEDIA = 'REMOVE_MEDIA'; -export function addMedia(file) { - return { type: ADD_MEDIA, payload: file }; +export function addMedia(mediaProxy) { + return {type: ADD_MEDIA, payload: mediaProxy}; +} + +export function removeMedia(uri) { + return {type: REMOVE_MEDIA, payload: uri}; } diff --git a/src/components/ControlPane.js b/src/components/ControlPane.js index 4dacd838..18d4fae5 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, getMedia, onChange, onAddMedia } = this.props; + const { entry, getMedia, onChange, onAddMedia, onRemoveMedia } = this.props; const widget = Widgets[field.get('widget')] || Widgets._unknown; return React.createElement(widget.Control, { key: field.get('name'), @@ -11,6 +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 7ad3f482..5c344364 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, getMedia, onChange, onAddMedia, onPersist } = this.props; + const { collection, entry, getMedia, onChange, onAddMedia, onRemoveMedia, onPersist } = this.props; return

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

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

@@ -17,6 +17,7 @@ export default class EntryEditor extends React.Component { getMedia={getMedia} onChange={onChange} onAddMedia={onAddMedia} + onRemoveMedia={onRemoveMedia} />
diff --git a/src/components/Widgets/ImageControl.js b/src/components/Widgets/ImageControl.js index 1e15a0b5..dbf01c67 100644 --- a/src/components/Widgets/ImageControl.js +++ b/src/components/Widgets/ImageControl.js @@ -1,5 +1,6 @@ import React from 'react'; import { truncateMiddle } from '../../lib/textHelper'; +import MediaProxy from '../../valueObjects/MediaProxy'; const MAX_DISPLAY_LENGTH = 50; @@ -7,11 +8,6 @@ export default class ImageControl extends React.Component { constructor(props) { super(props); - this.state = { - currentImage: props.value - }; - - this.revokeCurrentImage = this.revokeCurrentImage.bind(this); this.handleChange = this.handleChange.bind(this); this.handleFileInputRef = this.handleFileInputRef.bind(this); this.handleClick = this.handleClick.bind(this); @@ -20,12 +16,6 @@ export default class ImageControl extends React.Component { this.renderImageName = this.renderImageName.bind(this); } - revokeCurrentImage() { - if (this.state.currentImage) { - //this.props.onRemoveMedia(this.state.currentImage); - } - } - handleFileInputRef(el) { this._fileInput = el; } @@ -47,7 +37,7 @@ export default class ImageControl extends React.Component { handleChange(e) { e.stopPropagation(); e.preventDefault(); - this.revokeCurrentImage(); + const fileList = e.dataTransfer ? e.dataTransfer.files : e.target.files; const files = [...fileList]; const imageType = /^image\//; @@ -59,21 +49,25 @@ export default class ImageControl extends React.Component { } }); + this.props.onRemoveMedia(this.props.value); if (file) { - this.props.onAddMedia(file); - this.props.onChange(file.name); - this.setState({currentImage: file.name}); + const mediaProxy = new MediaProxy(file.name, file); + this.props.onAddMedia(mediaProxy); + this.props.onChange(mediaProxy.uri); } else { this.props.onChange(null); - this.setState({currentImage: null}); } } renderImageName() { + if (!this.props.value) return null; + if (this.value instanceof MediaProxy) { + return truncateMiddle(this.props.value.uri, MAX_DISPLAY_LENGTH); + } else { + return truncateMiddle(this.props.value, MAX_DISPLAY_LENGTH); + } - if (!this.state.currentImage) return null; - return truncateMiddle(this.props.getMedia(this.state.currentImage).uri, MAX_DISPLAY_LENGTH); } render() { diff --git a/src/containers/EntryPage.js b/src/containers/EntryPage.js index 25e6c1ec..aff9eb35 100644 --- a/src/containers/EntryPage.js +++ b/src/containers/EntryPage.js @@ -7,7 +7,7 @@ import { changeDraft, persist } from '../actions/entries'; -import { addMedia } from '../actions/media'; +import { addMedia, removeMedia } from '../actions/media'; import { selectEntry, getMedia } from '../reducers'; import EntryEditor from '../components/EntryEditor'; @@ -40,7 +40,7 @@ class EntryPage extends React.Component { render() { const { - entry, entryDraft, boundGetMedia, collection, handleDraftChange, handleAddMedia, handleDraftRemoveMedia + entry, entryDraft, boundGetMedia, collection, changeDraft, addMedia, removeMedia } = this.props; if (entry == null || entryDraft.get('entry') == undefined || entry.get('isFetching')) { @@ -51,8 +51,9 @@ class EntryPage extends React.Component { entry={entryDraft.get('entry')} getMedia={boundGetMedia} collection={collection} - onChange={handleDraftChange} - onAddMedia={handleAddMedia} + onChange={changeDraft} + onAddMedia={addMedia} + onRemoveMedia={removeMedia} onPersist={this.handlePersist} /> ); @@ -71,8 +72,9 @@ function mapStateToProps(state, ownProps) { export default connect( mapStateToProps, { - handleDraftChange: changeDraft, - handleAddMedia: addMedia, + changeDraft, + addMedia, + removeMedia, loadEntry, createDraft, discardDraft, diff --git a/src/reducers/entryDraft.js b/src/reducers/entryDraft.js index 7b99298a..67c999ff 100644 --- a/src/reducers/entryDraft.js +++ b/src/reducers/entryDraft.js @@ -1,6 +1,6 @@ import { Map, List } from 'immutable'; import { DRAFT_CREATE, DRAFT_DISCARD, DRAFT_CHANGE } from '../actions/entries'; -import { ADD_MEDIA } from '../actions/media'; +import { ADD_MEDIA, REMOVE_MEDIA } from '../actions/media'; const initialState = Map({entry: Map(), mediaFiles: List()}); @@ -22,7 +22,9 @@ const entryDraft = (state = Map(), action) => { return state.set('entry', action.payload); case ADD_MEDIA: - return state.update('mediaFiles', (list) => list.push(action.payload.name)); + return state.update('mediaFiles', (list) => list.push(action.payload.uri)); + case REMOVE_MEDIA: + return state.update('mediaFiles', (list) => list.filterNot((uri) => uri === action.payload)); default: return state; diff --git a/src/reducers/medias.js b/src/reducers/medias.js index 46f11f63..918a170d 100644 --- a/src/reducers/medias.js +++ b/src/reducers/medias.js @@ -1,11 +1,13 @@ import { Map } from 'immutable'; -import { ADD_MEDIA } from '../actions/media'; +import { ADD_MEDIA, REMOVE_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); + return state.set(action.payload.uri, action.payload); + case REMOVE_MEDIA: + return state.delete(action.payload); default: return state; } @@ -13,11 +15,10 @@ const medias = (state = Map(), action) => { 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})); +export const getMedia = (state, uri) => { + if (state.has(uri)) { + return state.get(uri); } else { - return new MediaProxy(filePath, null, filePath, true); + return new MediaProxy(uri, null, true); } }; diff --git a/src/valueObjects/MediaProxy.js b/src/valueObjects/MediaProxy.js index 338139eb..949e79d7 100644 --- a/src/valueObjects/MediaProxy.js +++ b/src/valueObjects/MediaProxy.js @@ -3,11 +3,12 @@ export const setConfig = (configObj) => { config = configObj; }; -export default function MediaProxy(value, objectURL, uri, uploaded = false) { +export default function MediaProxy(value, file, uploaded = false) { this.value = value; + this.file = file; this.uploaded = uploaded; - this.uri = uri || config.media_folder && config.media_folder + '/' + value; + this.uri = config.media_folder && !uploaded ? config.media_folder + '/' + value : value; this.toString = function() { - return uploaded ? this.uri : objectURL; + return uploaded ? this.uri : window.URL.createObjectURL(this.file, {oneTimeOnly: true}); }; } From 6ed7e786426d373ae52cf2c836cf76eb042eafc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Fri, 10 Jun 2016 18:48:38 -0300 Subject: [PATCH 10/13] mediaFiles backend persistence structure --- src/actions/entries.js | 10 ++++++---- src/backends/backend.js | 11 +++++++---- src/backends/test-repo/implementation.js | 2 +- src/reducers/medias.js | 7 +++++++ src/valueObjects/MediaProxy.js | 2 +- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/actions/entries.js b/src/actions/entries.js index 37cd4f1b..329c7318 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -83,12 +83,12 @@ function entryPersisting(collection, entry) { }; } -function entryPersisted(collection, entry) { +function entryPersisted(persistedEntry, persistedMediaFiles) { return { type: ENTRY_PERSIST_SUCCESS, payload: { - collection: collection, - entry: entry + persistedEntry: persistedEntry, + persistedMediaFiles: persistedMediaFiles } }; } @@ -158,7 +158,9 @@ export function persist(collection, entry, mediaFiles) { const backend = currentBackend(state.config); dispatch(entryPersisting(collection, entry)); backend.persist(collection, entry, mediaFiles).then( - (entry) => dispatch(entryPersisted(collection, entry)), + ({persistedEntry, persistedMediaFiles}) => { + dispatch(entryPersisted(persistedEntry, persistedMediaFiles)); + }, (error) => dispatch(entryPersistFail(collection, entry, error)) ); }; diff --git a/src/backends/backend.js b/src/backends/backend.js index 5b8c7108..760c7845 100644 --- a/src/backends/backend.js +++ b/src/backends/backend.js @@ -68,15 +68,18 @@ class Backend { } persist(collection, entryDraft) { - - const entryData = entryDraft.getIn(['entry', 'data']).toJS(); + const entryData = entryDraft.getIn(['entry', 'data']).toObject(); const entryObj = { path: entryDraft.getIn(['entry', 'path']), slug: entryDraft.getIn(['entry', 'slug']), raw: this.entryToRaw(collection, entryData) }; - - return this.implementation.persist(collection, entryObj, entryDraft.get('mediaFiles').toJS()); + return this.implementation.persist(collection, entryObj, entryDraft.get('mediaFiles').toJS()).then( + (response) => ({ + persistedEntry: this.entryWithFormat(collection)(response.persistedEntry), + persistedMediaFiles:response.persistedMediaFiles + }) + ); } entryToRaw(collection, entry) { diff --git a/src/backends/test-repo/implementation.js b/src/backends/test-repo/implementation.js index 4a3a8a32..2afc719b 100644 --- a/src/backends/test-repo/implementation.js +++ b/src/backends/test-repo/implementation.js @@ -52,6 +52,6 @@ export default class TestRepo { 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}); + return Promise.resolve({persistedEntry:entry, persistedMediaFiles:[]}); } } diff --git a/src/reducers/medias.js b/src/reducers/medias.js index 918a170d..e80d24dc 100644 --- a/src/reducers/medias.js +++ b/src/reducers/medias.js @@ -1,5 +1,6 @@ import { Map } from 'immutable'; import { ADD_MEDIA, REMOVE_MEDIA } from '../actions/media'; +import { ENTRY_PERSIST_SUCCESS } from '../actions/entries'; import MediaProxy from '../valueObjects/MediaProxy'; const medias = (state = Map(), action) => { @@ -8,6 +9,12 @@ const medias = (state = Map(), action) => { return state.set(action.payload.uri, action.payload); case REMOVE_MEDIA: return state.delete(action.payload); + case ENTRY_PERSIST_SUCCESS: + return state.map((media, uri) => { + if (action.payload.persistedMediaFiles.indexOf(uri) > -1) media.uploaded = true; + return media; + }); + default: return state; } diff --git a/src/valueObjects/MediaProxy.js b/src/valueObjects/MediaProxy.js index 949e79d7..13e6742c 100644 --- a/src/valueObjects/MediaProxy.js +++ b/src/valueObjects/MediaProxy.js @@ -9,6 +9,6 @@ export default function MediaProxy(value, file, uploaded = false) { this.uploaded = uploaded; this.uri = config.media_folder && !uploaded ? config.media_folder + '/' + value : value; this.toString = function() { - return uploaded ? this.uri : window.URL.createObjectURL(this.file, {oneTimeOnly: true}); + return this.uploaded ? this.uri : window.URL.createObjectURL(this.file, {oneTimeOnly: true}); }; } From 0f116e30bb867fc6858758be935cb59e61267722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Thu, 16 Jun 2016 17:15:59 -0300 Subject: [PATCH 11/13] First Draft --- architecture.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 architecture.md diff --git a/architecture.md b/architecture.md new file mode 100644 index 00000000..34422a4b --- /dev/null +++ b/architecture.md @@ -0,0 +1,51 @@ +# Technical Architecture + +Netlify CMS is a React Application, using Redux for state management with immutable data structures (immutable.js). + +## State shape / reducers +**Auth:** Keeps track of the logged state and the current user. + +**Config:** Holds the environment configuration (backend type, available collections & fields). + +**Collections** List of available collections, its fields and metadata information. + +**Entries:** Entries for each field. + +**EntryDraft:** Reused for each entry that is edited or created. It holds the entry's temporary data util it's persisted on the backend. + +**Medias:** Keeps references to all media files uploaded by the user during the current session. + +## Selectors: +Selectors are functions defined within reducers used to compute derived data from the Redux store. The available selectors are: + +**selectEntry:** Selects a single entry, given the collection and a slug. + +**selectEntries:** Selects all entries for a given collection. + +**getMedia:** Selects a single MediaProxy object for the given URI: + +## Value Objects: +**MediaProxy:** MediaProxy is a Value Object that holds information regarding a media file (such as an image, for example), whether it's persisted online or hold locally in cache. + +For files persisted online, the MediaProxy only keeps information about it's URI. For local files, the MediaProxy will keep a reference to the actual File object while generating the expected final URIs and on-demand blobs for local preview. + +The MediaProxy object can be used directly inside a media tag (such as ``), as it will always return something that can be used by the media tag to render correctly (either the URI for the online file or a single-use blob). + +## Components structure and Workflows +Components are separated into two main categories: Container components and presentational components. + + +### Entry Editing: +For either updating an existing entry or creating a new one, the `EntryEditor` is used and the flow is the same: +- When mounted, the `EntryPage` container component dispatches the `createDraft` action, setting the `entryDraft` state to a blank state (in case of a new entry) or to a copy of the selected entry (in case of an edit). +- The `EntryPage` will also render widgets for each field type in the given entry. +- Widgets are used for editing entry fields. There are different widgets for different field types, and they are always defined in a pair containing a `control` and a `preview` components. The control component is responsible for presenting the user with the appropriate interface for manipulating the current field value, while the preview component is responsible for displaying value with the appropriate styling. + +### Widget components implementation: +The control component receives 3 callbacks as props: onChange, onAddMedia & onRemoveMedia. + - onChange (Required): Should be called when the users changes the current value. It will ultimately end up updating the EntryDraft object in the Redux Store, thus updating the preview component. + - onAddMedia & onRemoveMedia (optionals): If the field accepts file uploads for media (images, for example), these callbacks should be invoked with a `MediaProxy` value object. `onAddMedia` will get the current media stored in the Redux state tree while `onRemoveMedia` will remove it. MediaProxy objects are stored in the `Medias` object and referenced in the `EntryDraft` object on the state tree. + +Both control and preview widgets receive a `getMedia` selector via props. Displaying the media (or its uri) for the user should always be done via `getMedia`, as it returns a MediaProxy that can return the correct value for both medias already persisted on server and cached media not yet uploaded. + +The actual persistence of the content and medias inserted into the control component are delegated to the backend implementation. The backend will be called with the updated values and a a list of mediaProxy objects for each field of the entry, and should return a promise that can resolve into the persisted entry object and the list of the persisted media URIs. From 36e4a54b74cc33a64a8323568390b31d9b34ec5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Thu, 16 Jun 2016 17:33:17 -0300 Subject: [PATCH 12/13] typo fix --- architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/architecture.md b/architecture.md index 34422a4b..9997bc7f 100644 --- a/architecture.md +++ b/architecture.md @@ -41,7 +41,7 @@ For either updating an existing entry or creating a new one, the `EntryEditor` i - The `EntryPage` will also render widgets for each field type in the given entry. - Widgets are used for editing entry fields. There are different widgets for different field types, and they are always defined in a pair containing a `control` and a `preview` components. The control component is responsible for presenting the user with the appropriate interface for manipulating the current field value, while the preview component is responsible for displaying value with the appropriate styling. -### Widget components implementation: +#### Widget components implementation: The control component receives 3 callbacks as props: onChange, onAddMedia & onRemoveMedia. - onChange (Required): Should be called when the users changes the current value. It will ultimately end up updating the EntryDraft object in the Redux Store, thus updating the preview component. - onAddMedia & onRemoveMedia (optionals): If the field accepts file uploads for media (images, for example), these callbacks should be invoked with a `MediaProxy` value object. `onAddMedia` will get the current media stored in the Redux state tree while `onRemoveMedia` will remove it. MediaProxy objects are stored in the `Medias` object and referenced in the `EntryDraft` object on the state tree. From 3f11a93056f98cbbae9207744ee4121973c5b030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A1ssio=20Zen?= Date: Thu, 16 Jun 2016 17:50:36 -0300 Subject: [PATCH 13/13] correct entity name --- src/reducers/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/reducers/index.js b/src/reducers/index.js index 19f3a266..29a7cdf3 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -23,5 +23,5 @@ export const selectEntry = (state, collection, slug) => export const selectEntries = (state, collection) => fromEntries.selectEntries(state.entries, collection); -export const getMedia = (state, fileName) => - fromMedias.getMedia(state.medias, fileName); +export const getMedia = (state, uri) => + fromMedias.getMedia(state.medias, uri);