diff --git a/src/actions/editorialWorkflow.js b/src/actions/editorialWorkflow.js index 8127eee6..7c1afdd3 100644 --- a/src/actions/editorialWorkflow.js +++ b/src/actions/editorialWorkflow.js @@ -231,20 +231,25 @@ export function persistUnpublishedEntry(collection, existingUnpublishedEntry) { const transactionID = uuid.v4(); const assetProxies = entryDraft.get('mediaFiles').map(path => getAsset(state, path)); const entry = entryDraft.get('entry'); - const transformedData = serializeValues(entryDraft.getIn(['entry', 'data']), collection.get('fields')); - const transformedEntry = entry.set('data', transformedData); - const transformedEntryDraft = entryDraft.set('entry', transformedEntry); - dispatch(unpublishedEntryPersisting(collection, transformedEntry, transactionID)); + /** + * Serialize the values of any fields with registered serializers, and + * update the entry and entryDraft with the serialized values. + */ + const serializedData = serializeValues(entryDraft.getIn(['entry', 'data']), collection.get('fields')); + const serializedEntry = entry.set('data', serializedData); + const serializedEntryDraft = entryDraft.set('entry', serializedEntry); + + dispatch(unpublishedEntryPersisting(collection, serializedEntry, transactionID)); const persistAction = existingUnpublishedEntry ? backend.persistUnpublishedEntry : backend.persistEntry; - return persistAction.call(backend, state.config, collection, transformedEntryDraft, assetProxies.toJS()) + return persistAction.call(backend, state.config, collection, serializedEntryDraft, assetProxies.toJS()) .then(() => { dispatch(notifSend({ message: 'Entry saved', kind: 'success', dismissAfter: 4000, })); - return dispatch(unpublishedEntryPersisted(collection, transformedEntry, transactionID)); + return dispatch(unpublishedEntryPersisted(collection, serializedEntry, transactionID)); }) .catch((error) => { dispatch(notifSend({ diff --git a/src/actions/entries.js b/src/actions/entries.js index 6bdfbf7c..26235db2 100644 --- a/src/actions/entries.js +++ b/src/actions/entries.js @@ -271,19 +271,24 @@ export function persistEntry(collection) { const backend = currentBackend(state.config); const assetProxies = entryDraft.get('mediaFiles').map(path => getAsset(state, path)); const entry = entryDraft.get('entry'); - const transformedData = serializeValues(entryDraft.getIn(['entry', 'data']), collection.get('fields')); - const transformedEntry = entry.set('data', transformedData); - const transformedEntryDraft = entryDraft.set('entry', transformedEntry); - dispatch(entryPersisting(collection, transformedEntry)); + + /** + * Serialize the values of any fields with registered serializers, and + * update the entry and entryDraft with the serialized values. + */ + const serializedData = serializeValues(entryDraft.getIn(['entry', 'data']), collection.get('fields')); + const serializedEntry = entry.set('data', serializedData); + const serializedEntryDraft = entryDraft.set('entry', serializedEntry); + dispatch(entryPersisting(collection, serializedEntry)); return backend - .persistEntry(state.config, collection, transformedEntryDraft, assetProxies.toJS()) + .persistEntry(state.config, collection, serializedEntryDraft, assetProxies.toJS()) .then(() => { dispatch(notifSend({ message: 'Entry saved', kind: 'success', dismissAfter: 4000, })); - return dispatch(entryPersisted(collection, transformedEntry)); + return dispatch(entryPersisted(collection, serializedEntry)); }) .catch((error) => { console.error(error); @@ -292,7 +297,7 @@ export function persistEntry(collection) { kind: 'danger', dismissAfter: 8000, })); - return dispatch(entryPersistFail(collection, transformedEntry, error)); + return dispatch(entryPersistFail(collection, serializedEntry, error)); }); }; } diff --git a/src/components/PreviewPane/Preview.js b/src/components/PreviewPane/Preview.js index e4409b5a..93ea02c3 100644 --- a/src/components/PreviewPane/Preview.js +++ b/src/components/PreviewPane/Preview.js @@ -9,6 +9,10 @@ const style = { fontFamily: 'Roboto, "Helvetica Neue", HelveticaNeue, Helvetica, Arial, sans-serif', }; +/** + * Use a stateful component so that child components can effectively utilize + * `shouldComponentUpdate`. + */ export default class Preview extends React.Component { render() { const { collection, fields, widgetFor } = this.props; diff --git a/src/components/PreviewPane/PreviewContent.js b/src/components/PreviewPane/PreviewContent.js index dce067fe..2baec537 100644 --- a/src/components/PreviewPane/PreviewContent.js +++ b/src/components/PreviewPane/PreviewContent.js @@ -1,9 +1,11 @@ import React, { PropTypes } from 'react'; import { ScrollSyncPane } from '../ScrollSync'; -// We need to create a lightweight component here so that we can -// access the context within the Frame. This allows us to attach -// the ScrollSyncPane to the body. +/** + * We need to create a lightweight component here so that we can access the + * context within the Frame. This allows us to attach the ScrollSyncPane to the + * body. + */ class PreviewContent extends React.Component { render() { const { previewComponent, previewProps } = this.props; diff --git a/src/components/PreviewPane/PreviewPane.js b/src/components/PreviewPane/PreviewPane.js index c704b3fc..d849eac6 100644 --- a/src/components/PreviewPane/PreviewPane.js +++ b/src/components/PreviewPane/PreviewPane.js @@ -17,6 +17,9 @@ export default class PreviewPane extends React.Component { const { fieldsMetaData, getAsset, entry } = props; const widget = resolveWidget(field.get('widget')); + /** + * Use an HOC to provide conditional updates for all previews. + */ return !widget.preview ? null : ( ({ error: false }); diff --git a/src/components/Widgets/Markdown/serializers/remarkSquashReferences.js b/src/components/Widgets/Markdown/serializers/remarkSquashReferences.js index 53762255..049e69cb 100644 --- a/src/components/Widgets/Markdown/serializers/remarkSquashReferences.js +++ b/src/components/Widgets/Markdown/serializers/remarkSquashReferences.js @@ -19,7 +19,7 @@ import mdastDefinitions from 'mdast-util-definitions'; * Yields: * * ``` - * ![alpha][http://example.com/example.jpg] + * ![alpha](http://example.com/example.jpg) * ``` * */ diff --git a/src/components/Widgets/PreviewHOC.js b/src/components/Widgets/PreviewHOC.js index 1ecaa529..27bdccb0 100644 --- a/src/components/Widgets/PreviewHOC.js +++ b/src/components/Widgets/PreviewHOC.js @@ -1,10 +1,13 @@ import React from 'react'; class PreviewHOC extends React.Component { + + /** + * Only re-render on value change, but always re-render objects and lists. + * Their child widgets will each also be wrapped with this component, and + * will only be updated on value change. + */ shouldComponentUpdate(nextProps) { - // Only re-render on value change, but always re-render objects and lists. - // Their child widgets will each also be wrapped with this component, and - // will only be updated on value change. const isWidgetContainer = ['object', 'list'].includes(nextProps.field.get('widget')); return isWidgetContainer || this.props.value !== nextProps.value; } diff --git a/src/containers/EntryPage.js b/src/containers/EntryPage.js index 5bc3a944..7a85c08d 100644 --- a/src/containers/EntryPage.js +++ b/src/containers/EntryPage.js @@ -68,6 +68,11 @@ class EntryPage extends React.Component { const { entry, newEntry, fields, collection } = nextProps; if (entry && !entry.get('isFetching') && !entry.get('error')) { + + /** + * Deserialize entry values for widgets with registered serializers before + * creating the entry draft. + */ const values = deserializeValues(entry.get('data'), fields); const deserializedEntry = entry.set('data', values); this.createDraft(deserializedEntry); diff --git a/src/lib/serializeEntryValues.js b/src/lib/serializeEntryValues.js index f6f9ae05..878b9524 100644 --- a/src/lib/serializeEntryValues.js +++ b/src/lib/serializeEntryValues.js @@ -20,22 +20,40 @@ import registry from './registry'; * registered deserialization handlers run on entry load, and serialization * handlers run on persist. */ - const runSerializer = (values, fields, method) => { + + /** + * Reduce the list of fields to a map where keys are field names and values + * are field values, serializing the values of fields whose widgets have + * registered serializers. If the field is a list or object, call recursively + * for nested fields. + */ return fields.reduce((acc, field) => { const fieldName = field.get('name'); const value = values.get(fieldName); const serializer = registry.getWidgetValueSerializer(field.get('widget')); const nestedFields = field.get('fields'); + + // Call recursively for fields within lists if (nestedFields && List.isList(value)) { return acc.set(fieldName, value.map(val => runSerializer(val, nestedFields, method))); - } else if (nestedFields && Map.isMap(value)) { + } + + // Call recursively for fields within objects + if (nestedFields && Map.isMap(value)) { return acc.set(fieldName, runSerializer(value, nestedFields, method)); - } else if (serializer && !isNil(value)) { + } + + // Run serialization method on value if not null or undefined + if (serializer && !isNil(value)) { return acc.set(fieldName, serializer[method](value)); - } else if (!isNil(value)) { + } + + // If no serializer is registered for the field's widget, use the field as is + if (!isNil(value)) { return acc.set(fieldName, value); } + return acc; }, Map()); }; diff --git a/src/reducers/entries.js b/src/reducers/entries.js index 49c20bf5..acd891bf 100644 --- a/src/reducers/entries.js +++ b/src/reducers/entries.js @@ -21,11 +21,10 @@ const entries = (state = Map({ entities: Map(), pages: Map() }), action) => { return state.setIn(['entities', `${ action.payload.collection }.${ action.payload.slug }`, 'isFetching'], true); case ENTRY_SUCCESS: - const result = state.setIn( + return state.setIn( ['entities', `${ action.payload.collection }.${ action.payload.entry.slug }`], fromJS(action.payload.entry) ); - return result; case ENTRIES_REQUEST: return state.setIn(['pages', action.payload.collection, 'isFetching'], true);