Doc Editor Redux refactoring (#1132)

* Split components into separate files

* Use redux

* Update tests
diff --git a/app/addons/documents/base.js b/app/addons/documents/base.js
index c5f7826..147eb84 100644
--- a/app/addons/documents/base.js
+++ b/app/addons/documents/base.js
@@ -20,6 +20,7 @@
 import sidebarReducers from "./sidebar/reducers";
 import partitionKeyReducers from "./partition-key/reducers";
 import revisionBrowserReducers from './rev-browser/reducers';
+import docEditorReducers from './doc-editor/reducers';
 import changesReducers from './changes/reducers';
 import "./assets/less/documents.less";
 
@@ -29,6 +30,7 @@
   sidebar: sidebarReducers,
   revisionBrowser: revisionBrowserReducers,
   partitionKey: partitionKeyReducers,
+  docEditor: docEditorReducers,
   changes: changesReducers,
   designDocInfo: designDocInfoReducers
 });
diff --git a/app/addons/documents/doc-editor/__tests__/doc-editor.actions.test.js b/app/addons/documents/doc-editor/__tests__/doc-editor.actions.test.js
index 603a0e6..425a27f 100644
--- a/app/addons/documents/doc-editor/__tests__/doc-editor.actions.test.js
+++ b/app/addons/documents/doc-editor/__tests__/doc-editor.actions.test.js
@@ -56,8 +56,9 @@
         }
       ]
     };
+    const mockDispatch = () => {};
 
-    Actions.uploadAttachment(params);
+    Actions.uploadAttachment(params)(mockDispatch);
     sinon.assert.calledWithExactly(
       fakeOpen,
       'PUT',
diff --git a/app/addons/documents/doc-editor/__tests__/doc-editor.components.test.js b/app/addons/documents/doc-editor/__tests__/doc-editor.components.test.js
index c247e91..35505f4 100644
--- a/app/addons/documents/doc-editor/__tests__/doc-editor.components.test.js
+++ b/app/addons/documents/doc-editor/__tests__/doc-editor.components.test.js
@@ -10,16 +10,21 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 
-import FauxtonAPI from "../../../../core/api";
-import React from "react";
-import ReactDOM from "react-dom";
-import Documents from "../../resources";
-import Components from "../components";
-import Actions from "../actions";
-import ActionTypes from "../actiontypes";
-import Databases from "../../../databases/base";
-import utils from "../../../../../test/mocha/testUtils";
-import {mount} from 'enzyme';
+import FauxtonAPI from '../../../../core/api';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import Documents from '../../resources';
+import AttachmentsPanelButton from '../components/AttachmentsPanelButton';
+import DocEditorScreen from '../components/DocEditorScreen';
+import DocEditorContainer from '../components/DocEditorContainer';
+import Databases from '../../../databases/base';
+import utils from '../../../../../test/mocha/testUtils';
+import { mount } from 'enzyme';
+import thunk from 'redux-thunk';
+import { Provider } from 'react-redux';
+import { createStore, applyMiddleware, combineReducers } from 'redux';
+import docEditorReducer from '../reducers';
+
 import '../../base';
 
 FauxtonAPI.router = new FauxtonAPI.Router([]);
@@ -54,26 +59,52 @@
 };
 
 const database = new Databases.Model({ id: 'a/special?db' });
+const defaultProps = {
+  isLoading: true,
+  isNewDoc: true,
+  database: database,
+  doc: new Documents.NewDoc(null, { database: database }),
+  conflictCount: 0,
+  saveDoc: () => {},
 
+  isCloneDocModalVisible: false,
+  showCloneDocModal: () => {},
+  hideCloneDocModal: () => {},
+  cloneDoc: () => {},
 
-describe('DocEditorController', () => {
+  isDeleteDocModalVisible: false,
+  showDeleteDocModal: () => {},
+  hideDeleteDocModal: () => {},
+  deleteDoc: () => {},
+
+  isUploadModalVisible: false,
+  uploadInProgress: false,
+  uploadPercentage: 0,
+  uploadErrorMessage: '',
+  numFilesUploaded: 0,
+  showUploadModal: () => {},
+  hideUploadModal: () => {},
+  cancelUpload: () => {},
+  resetUploadModal: () => {},
+  uploadAttachment: () => {}
+};
+
+describe('DocEditorScreen', () => {
+
   it('loading indicator appears on load', () => {
-    const el = mount(<Components.DocEditorController />);
+    const el = mount(<DocEditorScreen {...defaultProps} />);
     assert.equal(el.find('.loading-lines').length, 1);
   });
 
   it('new docs do not show the button row', () => {
-    const el = mount(<Components.DocEditorController isNewDoc={true} database={database} />);
-
     const doc = new Documents.Doc(docJSON, { database: database });
-    FauxtonAPI.dispatch({
-      type: ActionTypes.DOC_LOADED,
-      options: {
-        doc: doc
-      }
-    });
+    const el = mount(<DocEditorScreen
+      {...defaultProps}
+      isLoading={false}
+      isNewDoc={true}
+      database={database}
+      doc={doc} />);
 
-    el.update();
     assert.equal(el.find('.loading-lines').length, 0);
     assert.equal(el.find('.icon-circle-arrow-up').length, 0);
     assert.equal(el.find('.icon-repeat').length, 0);
@@ -81,58 +112,49 @@
   });
 
   it('view attachments button does not appear with no attachments', () => {
-    const el = mount(<Components.DocEditorController database={database} />);
-
     const doc = new Documents.Doc(docJSON, { database: database });
-    FauxtonAPI.dispatch({
-      type: ActionTypes.DOC_LOADED,
-      options: {
-        doc: doc
-      }
-    });
+    const el = mount(<DocEditorScreen
+      {...defaultProps}
+      isLoading={false}
+      isNewDoc={false}
+      database={database}
+      doc={doc} />);
+
     assert.equal(el.find('.view-attachments-section').length, 0);
   });
 
   it('view attachments button shows up when the doc has attachments', () => {
-    const el = mount(<Components.DocEditorController database={database} />);
-
     const doc = new Documents.Doc(docWithAttachmentsJSON, { database: database });
-    FauxtonAPI.dispatch({
-      type: ActionTypes.DOC_LOADED,
-      options: {
-        doc: doc
-      }
-    });
+    const el = mount(<DocEditorScreen
+      {...defaultProps}
+      isLoading={false}
+      isNewDoc={false}
+      database={database}
+      doc={doc} />);
 
-    el.update();
     assert.equal(el.find('.view-attachments-section').length, 1);
   });
 
   it('view attachments dropdown contains right number of docs', () => {
-    const el = mount(<Components.DocEditorController database={database} />);
-
     const doc = new Documents.Doc(docWithAttachmentsJSON, { database: database });
-    FauxtonAPI.dispatch({
-      type: ActionTypes.DOC_LOADED,
-      options: {
-        doc: doc
-      }
-    });
+    const el = mount(<DocEditorScreen
+      {...defaultProps}
+      isLoading={false}
+      isNewDoc={false}
+      database={database}
+      doc={doc} />);
+
     assert.equal(el.find('.view-attachments-section .dropdown-menu li').length, 2);
   });
 
   it('view attachments dropdown contains correct urls', () => {
-    const el = mount(
-      <Components.DocEditorController database={database} />
-    );
-
     const doc = new Documents.Doc(docWithAttachmentsJSON, { database: database });
-    FauxtonAPI.dispatch({
-      type: ActionTypes.DOC_LOADED,
-      options: {
-        doc: doc
-      }
-    });
+    const el = mount(<DocEditorScreen
+      {...defaultProps}
+      isLoading={false}
+      isNewDoc={false}
+      database={database}
+      doc={doc} />);
 
     const $attachmentNode = el.find('.view-attachments-section .dropdown-menu li');
     const attachmentURLactual = $attachmentNode.find('a').first().prop('href');
@@ -140,40 +162,54 @@
     assert.equal(attachmentURLactual, './a%2Fspecial%3Fdb/_design%2Ftest%23doc/one%252F.png');
   });
 
-  it.skip('setting deleteDocModal=true in store shows modal', () => {
-    mount(<Components.DocEditorController database={database} />);
-    const doc = new Documents.Doc(docWithAttachmentsJSON, { database: database });
-    FauxtonAPI.dispatch({
-      type: ActionTypes.DOC_LOADED,
-      options: {
-        doc: doc
-      }
-    });
+});
 
-    // uber-kludgy, but the delete doc modal is a generic dialog used multiple times, so this test first checks
-    // no modal is open, then confirms the open modal contains the delete dialog message
-    assert.equal($('body').find('.confirmation-modal').length, 0);
+describe('DocEditorContainer', () => {
+  const middlewares = [thunk];
+  const store = createStore(
+    combineReducers({ docEditor: docEditorReducer }),
+    applyMiddleware(...middlewares)
+  );
 
-    Actions.showDeleteDocModal();
-
-    const modalContent = $('body').find('.confirmation-modal .modal-body p')[0];
-    assert.ok(/Are you sure you want to delete this document\?/.test(modalContent.innerHTML));
+  it('clicking Delete button shows the confirmation modal', () => {
+    const wrapper = mount(
+      <Provider store={store}>
+        <DocEditorContainer
+          isNewDoc={false}
+          database={database} />
+      </Provider>
+    );
+    assert.equal(wrapper.find(DocEditorScreen).prop('isDeleteDocModalVisible'), false);
+    wrapper.find('button[title="Delete"]').simulate('click');
+    assert.equal(wrapper.find(DocEditorScreen).prop('isDeleteDocModalVisible'), true);
   });
 
-  it.skip('setting uploadDocModal=true in store shows modal', () => {
-    mount(<Components.DocEditorController database={database} />);
-    const doc = new Documents.Doc(docWithAttachmentsJSON, { database: database });
-    FauxtonAPI.dispatch({
-      type: ActionTypes.DOC_LOADED,
-      options: {
-        doc: doc
-      }
-    });
-
-    assert.equal($('body').find('.upload-file-modal').length, 0);
-    Actions.showUploadModal();
-    assert.notEqual($('body').find('.upload-file-modal').length, 0);
+  it('clicking Upload button shows the upload dialog', () => {
+    const wrapper = mount(
+      <Provider store={store}>
+        <DocEditorContainer
+          isNewDoc={false}
+          database={database} />
+      </Provider>
+    );
+    assert.equal(wrapper.find(DocEditorScreen).prop('isUploadModalVisible'), false);
+    wrapper.find('button[title="Upload Attachment"]').simulate('click');
+    assert.equal(wrapper.find(DocEditorScreen).prop('isUploadModalVisible'), true);
   });
+
+  it('clicking Clone button shows the clone doc dialog', () => {
+    const wrapper = mount(
+      <Provider store={store}>
+        <DocEditorContainer
+          isNewDoc={false}
+          database={database} />
+      </Provider>
+    );
+    assert.equal(wrapper.find(DocEditorScreen).prop('isCloneDocModalVisible'), false);
+    wrapper.find('button[title="Clone Document"]').simulate('click');
+    assert.equal(wrapper.find(DocEditorScreen).prop('isCloneDocModalVisible'), true);
+  });
+
 });
 
 
@@ -185,12 +221,12 @@
   });
 
   it('does not show up when loading', () => {
-    const el = mount(<Components.AttachmentsPanelButton isLoading={true} doc={doc} />);
+    const el = mount(<AttachmentsPanelButton isLoading={true} doc={doc} />);
     assert.equal(el.find('.panel-button').length, 0);
   });
 
   it('shows up after loading', () => {
-    const el = mount(<Components.AttachmentsPanelButton isLoading={false} doc={doc} />);
+    const el = mount(<AttachmentsPanelButton isLoading={false} doc={doc} />);
     assert.equal(el.find('button.panel-button').length, 1);
   });
 });
@@ -211,9 +247,12 @@
 
     FauxtonAPI.registerExtension('DocEditor:icons', CustomButton);
 
-    const el = mount(<Components.DocEditorController database={database} />);
+    const el = mount(<DocEditorScreen
+      {...defaultProps}
+      isLoading={false}
+      isNewDoc={false}
+      database={database} />);
     assert.isTrue(/Oh\sno\sshe\sdi'n't!/.test(el.html()));
-
     // confirm the database name was also included
     assert.equal(el.find("#testDatabaseName").text(), database.id);
   });
diff --git a/app/addons/documents/doc-editor/__tests__/doc-editor.reducers.test.js b/app/addons/documents/doc-editor/__tests__/doc-editor.reducers.test.js
new file mode 100644
index 0000000..093abe5
--- /dev/null
+++ b/app/addons/documents/doc-editor/__tests__/doc-editor.reducers.test.js
@@ -0,0 +1,70 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import FauxtonAPI from "../../../../core/api";
+import utils from "../../../../../test/mocha/testUtils";
+import Documents from "../../resources";
+import ActionTypes from "../actiontypes";
+import reducer from "../reducers";
+
+FauxtonAPI.router = new FauxtonAPI.Router([]);
+
+const assert = utils.assert;
+const doc = new Documents.Doc({id: 'foo'}, {database: 'bar'});
+
+describe('DocEditor Reducer', function () {
+
+  it('defines sensible defaults', function () {
+    const newState = reducer(undefined, { type: 'do_nothing'});
+
+    assert.equal(newState.isLoading, true);
+    assert.equal(newState.cloneDocModalVisible, false);
+    assert.equal(newState.deleteDocModalVisible, false);
+    assert.equal(newState.uploadModalVisible, false);
+    assert.equal(newState.numFilesUploaded, 0);
+    assert.equal(newState.uploadInProgress, false);
+    assert.equal(newState.uploadPercentage, 0);
+  });
+
+  it('marks loading as complete after doc is loaded', function () {
+    const newState = reducer(undefined, {
+      type: ActionTypes.DOC_LOADED,
+      options: { doc: doc }
+    });
+    assert.equal(newState.isLoading, false);
+  });
+
+  it('showCloneDocModal / hideCloneDocModal', function () {
+    const newStateShow = reducer(undefined, { type: ActionTypes.SHOW_CLONE_DOC_MODAL });
+    assert.equal(newStateShow.cloneDocModalVisible, true);
+
+    const newStateHide = reducer(undefined, { type: ActionTypes.HIDE_CLONE_DOC_MODAL });
+    assert.equal(newStateHide.cloneDocModalVisible, false);
+  });
+
+  it('showDeleteDocModal / hideDeleteDocModal', function () {
+    const newStateShow = reducer(undefined, { type: ActionTypes.SHOW_DELETE_DOC_CONFIRMATION_MODAL });
+    assert.equal(newStateShow.deleteDocModalVisible, true);
+
+    const newStateHide = reducer(undefined, { type: ActionTypes.HIDE_DELETE_DOC_CONFIRMATION_MODAL });
+    assert.equal(newStateHide.deleteDocModalVisible, false);
+  });
+
+  it('showUploadModal / hideUploadModal', function () {
+    const newStateShow = reducer(undefined, { type: ActionTypes.SHOW_UPLOAD_MODAL });
+    assert.equal(newStateShow.uploadModalVisible, true);
+
+    const newStateHide = reducer(undefined, { type: ActionTypes.HIDE_UPLOAD_MODAL });
+    assert.equal(newStateHide.uploadModalVisible, false);
+  });
+
+});
diff --git a/app/addons/documents/doc-editor/__tests__/doc-editor.stores.test.js b/app/addons/documents/doc-editor/__tests__/doc-editor.stores.test.js
deleted file mode 100644
index ac9c4bb..0000000
--- a/app/addons/documents/doc-editor/__tests__/doc-editor.stores.test.js
+++ /dev/null
@@ -1,78 +0,0 @@
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-import FauxtonAPI from "../../../../core/api";
-import Stores from "../stores";
-import Documents from "../../resources";
-import utils from "../../../../../test/mocha/testUtils";
-FauxtonAPI.router = new FauxtonAPI.Router([]);
-
-const assert = utils.assert;
-const store = Stores.docEditorStore;
-
-const doc = new Documents.Doc({id: 'foo'}, {database: 'bar'});
-
-describe('DocEditorStore', function () {
-  afterEach(function () {
-    store.reset();
-  });
-
-  it('defines sensible defaults', function () {
-    assert.equal(store.isLoading(), true);
-    assert.equal(store.isCloneDocModalVisible(), false);
-    assert.equal(store.isDeleteDocModalVisible(), false);
-    assert.equal(store.isUploadModalVisible(), false);
-    assert.equal(store.getNumFilesUploaded(), 0);
-    assert.equal(store.isUploadInProgress(), false);
-    assert.equal(store.getUploadLoadPercentage(), 0);
-  });
-
-  it('docLoaded() marks loading as complete', function () {
-    store.docLoaded({ doc: doc });
-    assert.equal(store.isLoading(), false);
-  });
-
-  it('showCloneDocModal / hideCloneDocModal', function () {
-    store.showCloneDocModal();
-    assert.equal(store.isCloneDocModalVisible(), true);
-    store.hideCloneDocModal();
-    assert.equal(store.isCloneDocModalVisible(), false);
-  });
-
-  it('showDeleteDocModal / hideCloneDocModal', function () {
-    store.showDeleteDocModal();
-    assert.equal(store.isDeleteDocModalVisible(), true);
-    store.hideDeleteDocModal();
-    assert.equal(store.isDeleteDocModalVisible(), false);
-  });
-
-  it('showCloneDocModal / hideCloneDocModal', function () {
-    store.showUploadModal();
-    assert.equal(store.isUploadModalVisible(), true);
-    store.hideUploadModal();
-    assert.equal(store.isUploadModalVisible(), false);
-  });
-
-  it('reset() resets all values', function () {
-    store.docLoaded({ doc: doc });
-    store.showCloneDocModal();
-    store.showDeleteDocModal();
-    store.showUploadModal();
-
-    store.reset();
-    assert.equal(store.isLoading(), true);
-    assert.equal(store.isCloneDocModalVisible(), false);
-    assert.equal(store.isDeleteDocModalVisible(), false);
-    assert.equal(store.isUploadModalVisible(), false);
-  });
-
-});
diff --git a/app/addons/documents/doc-editor/actions.js b/app/addons/documents/doc-editor/actions.js
index 6d0abc1..1212051 100644
--- a/app/addons/documents/doc-editor/actions.js
+++ b/app/addons/documents/doc-editor/actions.js
@@ -12,20 +12,24 @@
 
 /* global FormData */
 
-import FauxtonAPI from "../../../core/api";
-import { deleteRequest } from "../../../core/ajax";
-import ActionTypes from "./actiontypes";
+import FauxtonAPI from '../../../core/api';
+import { deleteRequest } from '../../../core/ajax';
+import ActionTypes from './actiontypes';
 
 var currentUploadHttpRequest;
 
-function initDocEditor (params) {
-  var doc = params.doc;
+const dispatchInitDocEditor = (params) => {
+  FauxtonAPI.reduxDispatch(initDocEditor(params));
+};
+
+const initDocEditor = (params) => (dispatch) => {
+  const doc = params.doc;
 
   // ensure a clean slate
-  FauxtonAPI.dispatch({ type: ActionTypes.RESET_DOC });
+  dispatch({ type: ActionTypes.RESET_DOC });
 
   doc.fetch().then(function () {
-    FauxtonAPI.dispatch({
+    dispatch({
       type: ActionTypes.DOC_LOADED,
       options: {
         doc: doc
@@ -42,9 +46,9 @@
 
     FauxtonAPI.navigate(FauxtonAPI.urls('allDocs', 'app', params.database.id, ''));
   });
-}
+};
 
-function saveDoc (doc, isValidDoc, onSave) {
+const saveDoc = (doc, isValidDoc, onSave) => {
   if (isValidDoc) {
     FauxtonAPI.addNotification({
       msg: 'Saving document.',
@@ -69,17 +73,17 @@
   } else {
     errorNotification('Please fix the JSON errors and try saving again.');
   }
-}
+};
 
-function showDeleteDocModal () {
-  FauxtonAPI.dispatch({ type: ActionTypes.SHOW_DELETE_DOC_CONFIRMATION_MODAL });
-}
+const showDeleteDocModal = () => (dispatch) => {
+  dispatch({ type: ActionTypes.SHOW_DELETE_DOC_CONFIRMATION_MODAL });
+};
 
-function hideDeleteDocModal () {
-  FauxtonAPI.dispatch({ type: ActionTypes.HIDE_DELETE_DOC_CONFIRMATION_MODAL });
-}
+const hideDeleteDocModal = () => (dispatch) => {
+  dispatch({ type: ActionTypes.HIDE_DELETE_DOC_CONFIRMATION_MODAL });
+};
 
-function deleteDoc (doc) {
+const deleteDoc = (doc) => {
   const databaseName = doc.database.safeID();
   const query = '?rev=' + doc.get('_rev');
   const url = FauxtonAPI.urls('document', 'server', databaseName, doc.safeID(), query);
@@ -100,22 +104,22 @@
       clear: true
     });
   });
-}
+};
 
-function showCloneDocModal () {
-  FauxtonAPI.dispatch({ type: ActionTypes.SHOW_CLONE_DOC_MODAL });
-}
+const showCloneDocModal = () => (dispatch) => {
+  dispatch({ type: ActionTypes.SHOW_CLONE_DOC_MODAL });
+};
 
-function hideCloneDocModal () {
-  FauxtonAPI.dispatch({ type: ActionTypes.HIDE_CLONE_DOC_MODAL });
-}
+const hideCloneDocModal = () => (dispatch) => {
+  dispatch({ type: ActionTypes.HIDE_CLONE_DOC_MODAL });
+};
 
-function cloneDoc (database, doc, newId) {
-
+const cloneDoc = (database, doc, newId) => {
   hideCloneDocModal();
 
   doc.copy(newId).then(() => {
-    FauxtonAPI.navigate('/database/' + database.safeID() + '/' + encodeURIComponent(newId), { trigger: true });
+    const url = FauxtonAPI.urls('document', 'app', database.safeID(), encodeURIComponent(newId));
+    FauxtonAPI.navigate(url, { trigger: true });
 
     FauxtonAPI.addNotification({
       msg: 'Document has been duplicated.'
@@ -128,20 +132,19 @@
       type: 'error'
     });
   });
+};
 
-}
+const showUploadModal = () => (dispatch) => {
+  dispatch({ type: ActionTypes.SHOW_UPLOAD_MODAL });
+};
 
-function showUploadModal () {
-  FauxtonAPI.dispatch({ type: ActionTypes.SHOW_UPLOAD_MODAL });
-}
+const hideUploadModal = () => (dispatch) => {
+  dispatch({ type: ActionTypes.HIDE_UPLOAD_MODAL });
+};
 
-function hideUploadModal () {
-  FauxtonAPI.dispatch({ type: ActionTypes.HIDE_UPLOAD_MODAL });
-}
-
-function uploadAttachment (params) {
+const uploadAttachment = (params) => (dispatch) => {
   if (params.files.length === 0) {
-    FauxtonAPI.dispatch({
+    dispatch({
       type: ActionTypes.FILE_UPLOAD_ERROR,
       options: {
         error: 'Please select a file to be uploaded.'
@@ -149,7 +152,7 @@
     });
     return;
   }
-  FauxtonAPI.dispatch({ type: ActionTypes.START_FILE_UPLOAD });
+  dispatch({ type: ActionTypes.START_FILE_UPLOAD });
 
   const query = '?rev=' + params.rev;
   const db = params.doc.getDatabase().safeID();
@@ -160,7 +163,7 @@
   const onProgress = (evt) => {
     if (evt.lengthComputable) {
       const percentComplete = evt.loaded / evt.total * 100;
-      FauxtonAPI.dispatch({
+      dispatch({
         type: ActionTypes.SET_FILE_UPLOAD_PERCENTAGE,
         options: {
           percent: percentComplete
@@ -170,20 +173,20 @@
   };
   const onSuccess = (doc) => {
     // re-initialize the document editor. Only announce it's been updated when
-    initDocEditor({
+    dispatch(initDocEditor({
       doc: doc,
       onLoaded: () => {
-        FauxtonAPI.dispatch({ type: ActionTypes.FILE_UPLOAD_SUCCESS });
+        dispatch({ type: ActionTypes.FILE_UPLOAD_SUCCESS });
         FauxtonAPI.addNotification({
           msg: 'Document saved successfully.',
           type: 'success',
           clear: true
         });
       }
-    });
+    }));
   };
   const onError = (msg) => {
-    FauxtonAPI.dispatch({
+    dispatch({
       type: ActionTypes.FILE_UPLOAD_ERROR,
       options: {
         error: msg
@@ -225,17 +228,17 @@
   httpRequest.setRequestHeader('Content-Type', file.type || `application/octet-stream`);
   httpRequest.setRequestHeader('Accept', 'application/json');
   httpRequest.send(file);
-}
+};
 
-function cancelUpload () {
+const cancelUpload = () => {
   if (currentUploadHttpRequest) {
     currentUploadHttpRequest.abort();
   }
-}
+};
 
-function resetUploadModal () {
-  FauxtonAPI.dispatch({ type: ActionTypes.RESET_UPLOAD_MODAL });
-}
+const resetUploadModal = () => (dispatch) => {
+  dispatch({ type: ActionTypes.RESET_UPLOAD_MODAL });
+};
 
 
 // helpers
@@ -249,23 +252,24 @@
 }
 
 export default {
-  initDocEditor: initDocEditor,
-  saveDoc: saveDoc,
+  dispatchInitDocEditor,
+  initDocEditor,
+  saveDoc,
 
   // clone doc
-  showCloneDocModal: showCloneDocModal,
-  hideCloneDocModal: hideCloneDocModal,
-  cloneDoc: cloneDoc,
+  showCloneDocModal,
+  hideCloneDocModal,
+  cloneDoc,
 
   // delete doc
-  showDeleteDocModal: showDeleteDocModal,
-  hideDeleteDocModal: hideDeleteDocModal,
-  deleteDoc: deleteDoc,
+  showDeleteDocModal,
+  hideDeleteDocModal,
+  deleteDoc,
 
   // upload modal
-  showUploadModal: showUploadModal,
-  hideUploadModal: hideUploadModal,
-  uploadAttachment: uploadAttachment,
-  cancelUpload: cancelUpload,
-  resetUploadModal: resetUploadModal
+  showUploadModal,
+  hideUploadModal,
+  uploadAttachment,
+  cancelUpload,
+  resetUploadModal
 };
diff --git a/app/addons/documents/doc-editor/components.js b/app/addons/documents/doc-editor/components.js
deleted file mode 100644
index 542ba8e..0000000
--- a/app/addons/documents/doc-editor/components.js
+++ /dev/null
@@ -1,453 +0,0 @@
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-import FauxtonAPI from "../../../core/api";
-import PropTypes from 'prop-types';
-import React from "react";
-import { Dropdown, MenuItem } from "react-bootstrap";
-import ReactDOM from "react-dom";
-import Actions from "./actions";
-import Stores from "./stores";
-import FauxtonComponents from "../../fauxton/components";
-import GeneralComponents from "../../components/react-components";
-import { Modal } from "react-bootstrap";
-import Helpers from "../../../helpers";
-
-var store = Stores.docEditorStore;
-
-class DocEditorController extends React.Component {
-  static defaultProps = {
-    database: {},
-    isNewDoc: false
-  };
-
-  getStoreState = () => {
-    return {
-      isLoading: store.isLoading(),
-      doc: store.getDoc(),
-      cloneDocModalVisible: store.isCloneDocModalVisible(),
-      uploadModalVisible: store.isUploadModalVisible(),
-      deleteDocModalVisible: store.isDeleteDocModalVisible(),
-      numFilesUploaded: store.getNumFilesUploaded(),
-      conflictCount: store.getDocConflictCount()
-    };
-  };
-
-  getCodeEditor = () => {
-    if (this.state.isLoading) {
-      return (<GeneralComponents.LoadLines />);
-    }
-
-    var code = JSON.stringify(this.state.doc.attributes, null, '  ');
-    var editorCommands = [{
-      name: 'save',
-      bindKey: { win: 'Ctrl-S', mac: 'Ctrl-S' },
-      exec: this.saveDoc
-    }];
-
-    return (
-      <GeneralComponents.CodeEditor
-        id="doc-editor"
-        ref={node => this.docEditor = node}
-        defaultCode={code}
-        mode="json"
-        autoFocus={true}
-        editorCommands={editorCommands}
-        notifyUnsavedChanges={true}
-        stringEditModalEnabled={true} />
-    );
-  };
-
-  componentDidMount() {
-    store.on('change', this.onChange, this);
-  }
-
-  componentWillUnmount() {
-    store.off('change', this.onChange);
-  }
-
-  UNSAFE_componentWillUpdate(nextProps, nextState) {
-    // Update the editor whenever a file is uploaded, a doc is cloned, or a new doc is loaded
-    if (this.state.numFilesUploaded !== nextState.numFilesUploaded ||
-        this.state.doc && this.state.doc.hasChanged() ||
-        (this.state.doc && nextState.doc && this.state.doc.id !== nextState.doc.id)) {
-      this.getEditor().setValue(JSON.stringify(nextState.doc.attributes, null, '  '));
-      this.onSaveComplete();
-    }
-  }
-
-  onChange = () => {
-    this.setState(this.getStoreState());
-  };
-
-  saveDoc = () => {
-    Actions.saveDoc(this.state.doc, this.checkDocIsValid(), this.onSaveComplete);
-  };
-
-  onSaveComplete = () => {
-    this.getEditor().clearChanges();
-  };
-
-  hideDeleteDocModal = () => {
-    Actions.hideDeleteDocModal();
-  };
-
-  deleteDoc = () => {
-    Actions.hideDeleteDocModal();
-    Actions.deleteDoc(this.state.doc);
-  };
-
-  getEditor = () => {
-    return (this.docEditor) ? this.docEditor.getEditor() : null;
-  };
-
-  checkDocIsValid = () => {
-    if (this.getEditor().hasErrors()) {
-      return false;
-    }
-    var json = JSON.parse(this.getEditor().getValue());
-    this.state.doc.clear().set(json, { validate: true });
-
-    return !this.state.doc.validationError;
-  };
-
-  clearChanges = () => {
-    this.docEditor.clearChanges();
-  };
-
-  getExtensionIcons = () => {
-    var extensions = FauxtonAPI.getExtensions('DocEditor:icons');
-    return _.map(extensions, (Extension, i) => {
-      return (<Extension doc={this.state.doc} key={i} database={this.props.database} />);
-    });
-  };
-
-  getButtonRow = () => {
-    if (this.props.isNewDoc) {
-      return false;
-    }
-    return (
-      <div>
-        <AttachmentsPanelButton doc={this.state.doc} isLoading={this.state.isLoading} />
-        <div className="doc-editor-extension-icons">{this.getExtensionIcons()}</div>
-
-        {this.state.conflictCount ? <PanelButton
-          title={`Conflicts (${this.state.conflictCount})`}
-          iconClass="icon-columns"
-          className="conflicts"
-          onClick={() => { FauxtonAPI.navigate(FauxtonAPI.urls('revision-browser', 'app', this.props.database.safeID(), this.state.doc.id));}}/> : null}
-
-        <PanelButton className="upload" title="Upload Attachment" iconClass="icon-circle-arrow-up" onClick={Actions.showUploadModal} />
-        <PanelButton title="Clone Document" iconClass="icon-repeat" onClick={Actions.showCloneDocModal} />
-        <PanelButton title="Delete" iconClass="icon-trash" onClick={Actions.showDeleteDocModal} />
-      </div>
-    );
-  };
-
-  state = this.getStoreState();
-
-  render() {
-    var saveButtonLabel = (this.props.isNewDoc) ? 'Create Document' : 'Save Changes';
-    let endpoint = FauxtonAPI.urls('allDocs', 'app', FauxtonAPI.url.encode(this.props.database.id));
-    return (
-      <div>
-        <div id="doc-editor-actions-panel">
-          <div className="doc-actions-left">
-            <button className="save-doc btn btn-primary save" type="button" onClick={this.saveDoc}>
-              <i className="icon fonticon-ok-circled"></i> {saveButtonLabel}
-            </button>
-            <div>
-              <a href={`#/${endpoint}`} className="js-back cancel-button">Cancel</a>
-            </div>
-          </div>
-          <div className="alignRight">
-            {this.getButtonRow()}
-          </div>
-        </div>
-
-        <div className="code-region">
-          <div className="bgEditorGutter"></div>
-          <div id="editor-container" className="doc-code">{this.getCodeEditor()}</div>
-
-        </div>
-
-        <UploadModal
-          ref={node => this.uploadModal = node}
-          visible={this.state.uploadModalVisible}
-          doc={this.state.doc} />
-        <CloneDocModal
-          doc={this.state.doc}
-          database={this.props.database}
-          visible={this.state.cloneDocModalVisible}
-          onSubmit={this.clearChanges} />
-        <FauxtonComponents.ConfirmationModal
-          title="Confirm Deletion"
-          visible={this.state.deleteDocModalVisible}
-          text="Are you sure you want to delete this document?"
-          onClose={this.hideDeleteDocModal}
-          onSubmit={this.deleteDoc}
-          successButtonLabel="Delete Document" />
-      </div>
-    );
-  }
-}
-
-class AttachmentsPanelButton extends React.Component {
-  static propTypes = {
-    isLoading: PropTypes.bool.isRequired,
-    doc: PropTypes.object
-  };
-
-  static defaultProps = {
-    isLoading: true,
-    doc: {}
-  };
-
-  getAttachmentList = () => {
-    const db = encodeURIComponent(this.props.doc.database.get('id'));
-    const doc = encodeURIComponent(this.props.doc.get('_id'));
-
-    return _.map(this.props.doc.get('_attachments'), (item, filename) => {
-      const url = FauxtonAPI.urls('document', 'attachment', db, doc, encodeURIComponent(filename));
-      return (
-        <MenuItem key={filename} href={url} target="_blank" data-bypass="true">
-          <strong>{filename}</strong>
-          <span className="attachment-delimiter">-</span>
-          <span>{item.content_type}{item.content_type ? ', ' : ''}{Helpers.formatSize(item.length)}</span>
-        </MenuItem>
-      );
-    });
-  };
-
-  render() {
-    if (this.props.isLoading || !this.props.doc.get('_attachments')) {
-      return false;
-    }
-
-    return (
-      <div className="panel-section view-attachments-section btn-group">
-        <Dropdown id="view-attachments-menu">
-          <Dropdown.Toggle noCaret className="panel-button dropdown-toggle btn" data-bypass="true">
-            <i className="icon icon-paper-clip"></i>
-            <span className="button-text">View Attachments</span>
-            <span className="caret"></span>
-          </Dropdown.Toggle>
-          <Dropdown.Menu>
-            {this.getAttachmentList()}
-          </Dropdown.Menu>
-        </Dropdown>
-      </div>
-    );
-  }
-}
-
-class PanelButton extends React.Component {
-  static propTypes = {
-    title: PropTypes.string.isRequired,
-    onClick: PropTypes.func.isRequired,
-    className: PropTypes.string
-  };
-
-  static defaultProps = {
-    title: '',
-    iconClass: '',
-    onClick: function () { },
-    className: ''
-  };
-
-  render() {
-    var iconClasses = 'icon ' + this.props.iconClass;
-    return (
-      <div className="panel-section">
-        <button className={`panel-button ${this.props.className}`} title={this.props.title} onClick={this.props.onClick}>
-          <i className={iconClasses}></i>
-          <span>{this.props.title}</span>
-        </button>
-      </div>
-    );
-  }
-}
-
-class UploadModal extends React.Component {
-  static propTypes = {
-    visible: PropTypes.bool.isRequired,
-    doc: PropTypes.object
-  };
-
-  getStoreState = () => {
-    return {
-      inProgress: store.isUploadInProgress(),
-      loadPercentage: store.getUploadLoadPercentage(),
-      errorMessage: store.getFileUploadErrorMsg()
-    };
-  };
-
-  componentDidMount() {
-    store.on('change', this.onChange, this);
-  }
-
-  componentWillUnmount() {
-    store.off('change', this.onChange);
-  }
-
-  onChange = () => {
-    this.setState(this.getStoreState());
-  };
-
-  closeModal = (e) => {
-    if (e) {
-      e.preventDefault();
-    }
-
-    if (this.state.inProgress) {
-      Actions.cancelUpload();
-    }
-    Actions.hideUploadModal();
-    Actions.resetUploadModal();
-  };
-
-  upload = () => {
-    Actions.uploadAttachment({
-      doc: this.props.doc,
-      rev: this.props.doc.get('_rev'),
-      files: this.attachments.files
-    });
-  };
-
-  state = this.getStoreState();
-
-  render() {
-    let errorClasses = 'alert alert-error';
-    if (this.state.errorMessage === '') {
-      errorClasses += ' hide';
-    }
-    let loadIndicatorClasses = 'progress progress-info';
-    let disabledAttribute = {disabled: 'disabled'};
-    if (!this.state.inProgress) {
-      loadIndicatorClasses += ' hide';
-      disabledAttribute = {};
-    }
-
-    return (
-      <Modal dialogClassName="upload-file-modal" show={this.props.visible} onHide={this.closeModal}>
-        <Modal.Header closeButton={true}>
-          <Modal.Title>Upload Attachment</Modal.Title>
-        </Modal.Header>
-        <Modal.Body>
-          <div className={errorClasses}>{this.state.errorMessage}</div>
-          <div>
-            <form ref={node => this.uploadForm = node} className="form">
-              <p>
-                Select a file to upload as an attachment to this document. Uploading a file saves the document as a new
-                revision.
-              </p>
-              <input ref={el => this.attachments = el} type="file" name="_attachments" {...disabledAttribute}/>
-              <br />
-            </form>
-
-            <div className={loadIndicatorClasses}>
-              <div className="bar" style={{ width: this.state.loadPercentage + '%'}}></div>
-            </div>
-          </div>
-        </Modal.Body>
-        <Modal.Footer>
-          <a href="#" data-bypass="true" className="cancel-link" onClick={this.closeModal}>Cancel</a>
-          <button href="#" id="upload-btn" data-bypass="true" className="btn btn-primary save" onClick={this.upload} {...disabledAttribute}>
-            <i className="icon icon-upload" /> Upload Attachment
-          </button>
-        </Modal.Footer>
-      </Modal>
-    );
-  }
-}
-
-class CloneDocModal extends React.Component {
-  static propTypes = {
-    visible: PropTypes.bool.isRequired,
-    doc: PropTypes.object,
-    database: PropTypes.object.isRequired,
-    onSubmit: PropTypes.func.isRequired
-  };
-
-  state = {
-    uuid: null
-  };
-
-  cloneDoc = () => {
-    if (this.props.onSubmit) {
-      this.props.onSubmit();
-    }
-
-    Actions.cloneDoc(this.props.database, this.props.doc, this.state.uuid);
-  };
-
-  componentDidUpdate() {
-    if (this.state.uuid === null) {
-      Helpers.getUUID().then((res) => {
-        if (res.uuids) {
-          this.setState({ uuid: res.uuids[0] });
-        }
-      }).catch(() => {});
-    }
-  }
-
-  closeModal = (e) => {
-    if (e) {
-      e.preventDefault();
-    }
-    Actions.hideCloneDocModal();
-  };
-
-  docIDChange = (e) => {
-    this.setState({ uuid: e.target.value });
-  };
-
-  render() {
-    if (this.state.uuid === null) {
-      return false;
-    }
-
-    return (
-      <Modal dialogClassName="clone-doc-modal" show={this.props.visible} onHide={this.closeModal}>
-        <Modal.Header closeButton={true}>
-          <Modal.Title>Clone Document</Modal.Title>
-        </Modal.Header>
-        <Modal.Body>
-          <form className="form" onSubmit={(e) => { e.preventDefault(); this.cloneDoc(); }}>
-            <p>
-              Document cloning copies the saved version of the document. Unsaved document changes will be discarded.
-            </p>
-            <p>
-              You can modify the following generated ID for your new document.
-            </p>
-            <input ref={node => this.newDocId = node} type="text" autoFocus={true} className="input-block-level"
-              onChange={this.docIDChange} value={this.state.uuid} />
-          </form>
-        </Modal.Body>
-        <Modal.Footer>
-          <a href="#" data-bypass="true" className="cancel-link" onClick={this.closeModal}>Cancel</a>
-          <button className="btn btn-primary save" onClick={this.cloneDoc}>
-            <i className="icon-repeat"></i> Clone Document
-          </button>
-        </Modal.Footer>
-      </Modal>
-    );
-  }
-}
-
-
-export default {
-  DocEditorController: DocEditorController,
-  AttachmentsPanelButton: AttachmentsPanelButton,
-  UploadModal: UploadModal,
-  CloneDocModal: CloneDocModal
-};
diff --git a/app/addons/documents/doc-editor/components/AttachmentsPanelButton.js b/app/addons/documents/doc-editor/components/AttachmentsPanelButton.js
new file mode 100644
index 0000000..043e333
--- /dev/null
+++ b/app/addons/documents/doc-editor/components/AttachmentsPanelButton.js
@@ -0,0 +1,68 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import FauxtonAPI from '../../../../core/api';
+import PropTypes from 'prop-types';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Dropdown, MenuItem } from 'react-bootstrap';
+import Helpers from '../../../../helpers';
+
+
+export default class AttachmentsPanelButton extends React.Component {
+  static propTypes = {
+    isLoading: PropTypes.bool.isRequired,
+    doc: PropTypes.object
+  };
+
+  static defaultProps = {
+    isLoading: true,
+    doc: {}
+  };
+
+  getAttachmentList = () => {
+    const db = encodeURIComponent(this.props.doc.database.get('id'));
+    const doc = encodeURIComponent(this.props.doc.get('_id'));
+
+    return _.map(this.props.doc.get('_attachments'), (item, filename) => {
+      const url = FauxtonAPI.urls('document', 'attachment', db, doc, encodeURIComponent(filename));
+      return (
+        <MenuItem key={filename} href={url} target="_blank" data-bypass="true">
+          <strong>{filename}</strong>
+          <span className="attachment-delimiter">-</span>
+          <span>{item.content_type}{item.content_type ? ', ' : ''}{Helpers.formatSize(item.length)}</span>
+        </MenuItem>
+      );
+    });
+  };
+
+  render() {
+    if (this.props.isLoading || !this.props.doc.get('_attachments')) {
+      return false;
+    }
+
+    return (
+      <div className="panel-section view-attachments-section btn-group">
+        <Dropdown id="view-attachments-menu">
+          <Dropdown.Toggle noCaret className="panel-button dropdown-toggle btn" data-bypass="true">
+            <i className="icon icon-paper-clip"></i>
+            <span className="button-text">View Attachments</span>
+            <span className="caret"></span>
+          </Dropdown.Toggle>
+          <Dropdown.Menu>
+            {this.getAttachmentList()}
+          </Dropdown.Menu>
+        </Dropdown>
+      </div>
+    );
+  }
+}
diff --git a/app/addons/documents/doc-editor/components/CloneDocModal.js b/app/addons/documents/doc-editor/components/CloneDocModal.js
new file mode 100644
index 0000000..3d6776e
--- /dev/null
+++ b/app/addons/documents/doc-editor/components/CloneDocModal.js
@@ -0,0 +1,94 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Modal } from 'react-bootstrap';
+import Helpers from '../../../../helpers';
+
+
+export default class CloneDocModal extends React.Component {
+  static propTypes = {
+    visible: PropTypes.bool.isRequired,
+    doc: PropTypes.object,
+    database: PropTypes.object.isRequired,
+    onSubmit: PropTypes.func.isRequired,
+    hideCloneDocModal: PropTypes.func.isRequired,
+    cloneDoc: PropTypes.func.isRequired
+  };
+
+  state = {
+    uuid: null
+  };
+
+  cloneDoc = () => {
+    if (this.props.onSubmit) {
+      this.props.onSubmit();
+    }
+
+    this.props.cloneDoc(this.props.database, this.props.doc, this.state.uuid);
+  };
+
+  componentDidUpdate() {
+    if (this.state.uuid === null) {
+      Helpers.getUUID().then((res) => {
+        if (res.uuids) {
+          this.setState({ uuid: res.uuids[0] });
+        }
+      }).catch(() => {});
+    }
+  }
+
+  closeModal = (e) => {
+    if (e) {
+      e.preventDefault();
+    }
+    this.props.hideCloneDocModal();
+  };
+
+  docIDChange = (e) => {
+    this.setState({ uuid: e.target.value });
+  };
+
+  render() {
+    if (this.state.uuid === null) {
+      return false;
+    }
+
+    return (
+      <Modal dialogClassName="clone-doc-modal" show={this.props.visible} onHide={this.closeModal}>
+        <Modal.Header closeButton={true}>
+          <Modal.Title>Clone Document</Modal.Title>
+        </Modal.Header>
+        <Modal.Body>
+          <form className="form" onSubmit={(e) => { e.preventDefault(); this.cloneDoc(); }}>
+            <p>
+              Document cloning copies the saved version of the document. Unsaved document changes will be discarded.
+            </p>
+            <p>
+              You can modify the following generated ID for your new document.
+            </p>
+            <input ref={node => this.newDocId = node} type="text" autoFocus={true} className="input-block-level"
+              onChange={this.docIDChange} value={this.state.uuid} />
+          </form>
+        </Modal.Body>
+        <Modal.Footer>
+          <a href="#" data-bypass="true" className="cancel-link" onClick={this.closeModal}>Cancel</a>
+          <button className="btn btn-primary save" onClick={this.cloneDoc}>
+            <i className="icon-repeat"></i> Clone Document
+          </button>
+        </Modal.Footer>
+      </Modal>
+    );
+  }
+}
diff --git a/app/addons/documents/doc-editor/components/DocEditorContainer.js b/app/addons/documents/doc-editor/components/DocEditorContainer.js
new file mode 100644
index 0000000..3946ca3
--- /dev/null
+++ b/app/addons/documents/doc-editor/components/DocEditorContainer.js
@@ -0,0 +1,74 @@
+import { connect } from 'react-redux';
+import Actions from '../actions';
+import DocEditorScreen from './DocEditorScreen';
+
+const mapStateToProps = ({ docEditor }, ownProps) => {
+  return {
+    isLoading: docEditor.isLoading,
+    isNewDoc: ownProps.isNewDoc,
+    doc: docEditor.doc,
+    database: ownProps.database,
+    conflictCount: docEditor.docConflictCount,
+
+    isCloneDocModalVisible: docEditor.cloneDocModalVisible,
+
+    isDeleteDocModalVisible: docEditor.deleteDocModalVisible,
+
+    isUploadModalVisible: docEditor.uploadModalVisible,
+    uploadInProgress: docEditor.uploadInProgress,
+    uploadPercentage: docEditor.uploadPercentage,
+    uploadErrorMessage: docEditor.uploadErrorMessage,
+    numFilesUploaded: docEditor.numFilesUploaded
+  };
+};
+
+const mapDispatchToProps = (dispatch) => {
+  return {
+    saveDoc: (doc, isValidDoc, onSave) => {
+      Actions.saveDoc(doc, isValidDoc, onSave);
+    },
+
+    showCloneDocModal: () => {
+      dispatch(Actions.showCloneDocModal());
+    },
+    hideCloneDocModal: () => {
+      dispatch(Actions.hideCloneDocModal());
+    },
+    cloneDoc: (database, doc, newId) => {
+      Actions.cloneDoc(database, doc, newId);
+    },
+
+    showDeleteDocModal: () => {
+      dispatch(Actions.showDeleteDocModal());
+    },
+    hideDeleteDocModal: () => {
+      dispatch(Actions.hideDeleteDocModal());
+    },
+    deleteDoc: (doc) => {
+      Actions.deleteDoc(doc);
+    },
+
+    showUploadModal: () => {
+      dispatch(Actions.showUploadModal());
+    },
+    hideUploadModal: () => {
+      dispatch(Actions.hideUploadModal());
+    },
+    cancelUpload: () => {
+      Actions.cancelUpload();
+    },
+    resetUploadModal: () => {
+      dispatch(Actions.resetUploadModal());
+    },
+    uploadAttachment: (params) => {
+      dispatch(Actions.uploadAttachment(params));
+    }
+  };
+};
+
+const DocEditorContainer = connect(
+  mapStateToProps,
+  mapDispatchToProps
+)(DocEditorScreen);
+
+export default DocEditorContainer;
diff --git a/app/addons/documents/doc-editor/components/DocEditorScreen.js b/app/addons/documents/doc-editor/components/DocEditorScreen.js
new file mode 100644
index 0000000..6518d04
--- /dev/null
+++ b/app/addons/documents/doc-editor/components/DocEditorScreen.js
@@ -0,0 +1,214 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import FauxtonAPI from '../../../../core/api';
+import FauxtonComponents from '../../../fauxton/components';
+import GeneralComponents from '../../../components/react-components';
+import AttachmentsPanelButton from './AttachmentsPanelButton';
+import CloneDocModal from './CloneDocModal';
+import PanelButton from './PanelButton';
+import UploadModal from './UploadModal';
+
+
+export default class DocEditorScreen extends React.Component {
+  static defaultProps = {
+    database: {},
+    isNewDoc: false
+  };
+
+  static propTypes = {
+    isLoading: PropTypes.bool.isRequired,
+    isNewDoc: PropTypes.bool.isRequired,
+    doc: PropTypes.object,
+    conflictCount: PropTypes.number.isRequired,
+    saveDoc: PropTypes.func.isRequired,
+
+    isCloneDocModalVisible: PropTypes.bool.isRequired,
+    database: PropTypes.object,
+    showCloneDocModal: PropTypes.func.isRequired,
+    hideCloneDocModal: PropTypes.func.isRequired,
+    cloneDoc: PropTypes.func.isRequired,
+
+    isDeleteDocModalVisible: PropTypes.bool.isRequired,
+    showDeleteDocModal: PropTypes.func.isRequired,
+    hideDeleteDocModal: PropTypes.func.isRequired,
+    deleteDoc: PropTypes.func.isRequired,
+
+    isUploadModalVisible: PropTypes.bool.isRequired,
+    uploadInProgress: PropTypes.bool.isRequired,
+    uploadPercentage: PropTypes.number.isRequired,
+    uploadErrorMessage: PropTypes.string,
+    numFilesUploaded: PropTypes.number.isRequired,
+    showUploadModal: PropTypes.func.isRequired,
+    hideUploadModal: PropTypes.func.isRequired,
+    cancelUpload: PropTypes.func.isRequired,
+    resetUploadModal: PropTypes.func.isRequired,
+    uploadAttachment: PropTypes.func.isRequired
+  }
+
+  getCodeEditor = () => {
+    if (this.props.isLoading) {
+      return (<GeneralComponents.LoadLines />);
+    }
+
+    var code = JSON.stringify(this.props.doc.attributes, null, '  ');
+    var editorCommands = [{
+      name: 'save',
+      bindKey: { win: 'Ctrl-S', mac: 'Ctrl-S' },
+      exec: this.saveDoc
+    }];
+
+    return (
+      <GeneralComponents.CodeEditor
+        id="doc-editor"
+        ref={node => this.docEditor = node}
+        defaultCode={code}
+        mode="json"
+        autoFocus={true}
+        editorCommands={editorCommands}
+        notifyUnsavedChanges={true}
+        stringEditModalEnabled={true} />
+    );
+  };
+
+  UNSAFE_componentWillUpdate(nextProps) {
+    // Update the editor whenever a file is uploaded, a doc is cloned, or a new doc is loaded
+    if (this.props.numFilesUploaded !== nextProps.numFilesUploaded ||
+        this.props.doc && this.props.doc.hasChanged() ||
+        (this.props.doc && nextProps.doc && this.props.doc.id !== nextProps.doc.id)) {
+      this.getEditor().setValue(JSON.stringify(nextProps.doc.attributes, null, '  '));
+      this.onSaveComplete();
+    }
+  }
+
+  saveDoc = () => {
+    this.props.saveDoc(this.props.doc, this.checkDocIsValid(), this.onSaveComplete);
+  };
+
+  onSaveComplete = () => {
+    this.getEditor().clearChanges();
+  };
+
+  hideDeleteDocModal = () => {
+    this.props.hideDeleteDocModal();
+  };
+
+  deleteDoc = () => {
+    this.props.hideDeleteDocModal();
+    this.props.deleteDoc(this.props.doc);
+  };
+
+  getEditor = () => {
+    return (this.docEditor) ? this.docEditor.getEditor() : null;
+  };
+
+  checkDocIsValid = () => {
+    if (this.getEditor().hasErrors()) {
+      return false;
+    }
+    var json = JSON.parse(this.getEditor().getValue());
+    this.props.doc.clear().set(json, { validate: true });
+
+    return !this.props.doc.validationError;
+  };
+
+  clearChanges = () => {
+    this.docEditor.clearChanges();
+  };
+
+  getExtensionIcons = () => {
+    var extensions = FauxtonAPI.getExtensions('DocEditor:icons');
+    return _.map(extensions, (Extension, i) => {
+      return (<Extension doc={this.props.doc} key={i} database={this.props.database} />);
+    });
+  };
+
+  getButtonRow = () => {
+    if (this.props.isNewDoc) {
+      return false;
+    }
+    return (
+      <div>
+        <AttachmentsPanelButton doc={this.props.doc} isLoading={this.props.isLoading} />
+        <div className="doc-editor-extension-icons">{this.getExtensionIcons()}</div>
+
+        {this.props.conflictCount ? <PanelButton
+          title={`Conflicts (${this.props.conflictCount})`}
+          iconClass="icon-columns"
+          className="conflicts"
+          onClick={() => { FauxtonAPI.navigate(FauxtonAPI.urls('revision-browser', 'app', this.props.database.safeID(), this.props.doc.id));}}/> : null}
+
+        <PanelButton className="upload" title="Upload Attachment" iconClass="icon-circle-arrow-up" onClick={this.props.showUploadModal} />
+        <PanelButton title="Clone Document" iconClass="icon-repeat" onClick={this.props.showCloneDocModal} />
+        <PanelButton title="Delete" iconClass="icon-trash" onClick={this.props.showDeleteDocModal} />
+      </div>
+    );
+  };
+
+  render() {
+    var saveButtonLabel = (this.props.isNewDoc) ? 'Create Document' : 'Save Changes';
+    let endpoint = FauxtonAPI.urls('allDocs', 'app', FauxtonAPI.url.encode(this.props.database.id));
+    return (
+      <div>
+        <div id="doc-editor-actions-panel">
+          <div className="doc-actions-left">
+            <button className="save-doc btn btn-primary save" type="button" onClick={this.saveDoc}>
+              <i className="icon fonticon-ok-circled"></i> {saveButtonLabel}
+            </button>
+            <div>
+              <a href={`#/${endpoint}`} className="js-back cancel-button">Cancel</a>
+            </div>
+          </div>
+          <div className="alignRight">
+            {this.getButtonRow()}
+          </div>
+        </div>
+
+        <div className="code-region">
+          <div className="bgEditorGutter"></div>
+          <div id="editor-container" className="doc-code">{this.getCodeEditor()}</div>
+
+        </div>
+
+        <UploadModal
+          ref={node => this.uploadModal = node}
+          visible={this.props.isUploadModalVisible}
+          doc={this.props.doc}
+          inProgress={this.props.uploadInProgress}
+          uploadPercentage={this.props.uploadPercentage}
+          errorMessage={this.props.uploadErrorMessage}
+          cancelUpload={this.props.cancelUpload}
+          hideUploadModal={this.props.hideUploadModal}
+          resetUploadModal={this.props.resetUploadModal}
+          uploadAttachment={this.props.uploadAttachment}/>
+        <CloneDocModal
+          doc={this.props.doc}
+          database={this.props.database}
+          visible={this.props.isCloneDocModalVisible}
+          onSubmit={this.clearChanges}
+          hideCloneDocModal={this.props.hideCloneDocModal}
+          cloneDoc={this.props.cloneDoc}/>
+        <span id='hey'>bb</span>
+        <FauxtonComponents.ConfirmationModal
+          title="Confirm Deletion"
+          visible={this.props.isDeleteDocModalVisible}
+          text="Are you sure you want to delete this document?"
+          onClose={this.hideDeleteDocModal}
+          onSubmit={this.deleteDoc}
+          successButtonLabel="Delete Document" />
+      </div>
+    );
+  }
+}
diff --git a/app/addons/documents/doc-editor/components/PanelButton.js b/app/addons/documents/doc-editor/components/PanelButton.js
new file mode 100644
index 0000000..fd7dc64
--- /dev/null
+++ b/app/addons/documents/doc-editor/components/PanelButton.js
@@ -0,0 +1,43 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import ReactDOM from 'react-dom';
+
+
+export default class PanelButton extends React.Component {
+  static propTypes = {
+    title: PropTypes.string.isRequired,
+    onClick: PropTypes.func.isRequired,
+    className: PropTypes.string
+  };
+
+  static defaultProps = {
+    title: '',
+    iconClass: '',
+    onClick: () => { },
+    className: ''
+  };
+
+  render() {
+    var iconClasses = 'icon ' + this.props.iconClass;
+    return (
+      <div className="panel-section">
+        <button className={`panel-button ${this.props.className}`} title={this.props.title} onClick={this.props.onClick}>
+          <i className={iconClasses}></i>
+          <span>{this.props.title}</span>
+        </button>
+      </div>
+    );
+  }
+}
diff --git a/app/addons/documents/doc-editor/components/UploadModal.js b/app/addons/documents/doc-editor/components/UploadModal.js
new file mode 100644
index 0000000..eb907f5
--- /dev/null
+++ b/app/addons/documents/doc-editor/components/UploadModal.js
@@ -0,0 +1,95 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Modal } from 'react-bootstrap';
+
+
+export default class UploadModal extends React.Component {
+  static propTypes = {
+    visible: PropTypes.bool.isRequired,
+    doc: PropTypes.object,
+    inProgress: PropTypes.bool.isRequired,
+    uploadPercentage: PropTypes.number.isRequired,
+    errorMessage: PropTypes.string,
+    cancelUpload: PropTypes.func.isRequired,
+    hideUploadModal: PropTypes.func.isRequired,
+    resetUploadModal: PropTypes.func.isRequired,
+    uploadAttachment: PropTypes.func.isRequired
+  };
+
+  closeModal = (e) => {
+    if (e) {
+      e.preventDefault();
+    }
+
+    if (this.props.inProgress) {
+      this.props.cancelUpload();
+    }
+    this.props.hideUploadModal();
+    this.props.resetUploadModal();
+  };
+
+  upload = () => {
+    this.props.uploadAttachment({
+      doc: this.props.doc,
+      rev: this.props.doc.get('_rev'),
+      files: this.attachments.files
+    });
+  };
+
+  render() {
+    let errorClasses = 'alert alert-error';
+    if (this.props.errorMessage === '') {
+      errorClasses += ' hide';
+    }
+    let loadIndicatorClasses = 'progress progress-info';
+    let disabledAttribute = {disabled: 'disabled'};
+    if (!this.props.inProgress) {
+      loadIndicatorClasses += ' hide';
+      disabledAttribute = {};
+    }
+
+    return (
+      <Modal dialogClassName="upload-file-modal" show={this.props.visible} onHide={this.closeModal}>
+        <Modal.Header closeButton={true}>
+          <Modal.Title>Upload Attachment</Modal.Title>
+        </Modal.Header>
+        <Modal.Body>
+          <div className={errorClasses}>{this.props.errorMessage}</div>
+          <div>
+            <form ref={node => this.uploadForm = node} className="form">
+              <p>
+                Select a file to upload as an attachment to this document. Uploading a file saves the document as a new
+                revision.
+              </p>
+              <input ref={el => this.attachments = el} type="file" name="_attachments" {...disabledAttribute}/>
+              <br />
+            </form>
+
+            <div className={loadIndicatorClasses}>
+              <div className="bar" style={{ width: this.props.uploadPercentage + '%'}}></div>
+            </div>
+          </div>
+        </Modal.Body>
+        <Modal.Footer>
+          <a href="#" data-bypass="true" className="cancel-link" onClick={this.closeModal}>Cancel</a>
+          <button href="#" id="upload-btn" data-bypass="true" className="btn btn-primary save" onClick={this.upload} {...disabledAttribute}>
+            <i className="icon icon-upload" /> Upload Attachment
+          </button>
+        </Modal.Footer>
+      </Modal>
+    );
+  }
+}
diff --git a/app/addons/documents/doc-editor/reducers.js b/app/addons/documents/doc-editor/reducers.js
new file mode 100644
index 0000000..4945e30
--- /dev/null
+++ b/app/addons/documents/doc-editor/reducers.js
@@ -0,0 +1,124 @@
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+import ActionTypes from "./actiontypes";
+
+const initialState = {
+  doc: null,
+  isLoading: true,
+  cloneDocModalVisible: false,
+  deleteDocModalVisible: false,
+  uploadModalVisible: false,
+
+  numFilesUploaded: 0,
+  uploadErrorMessage: '',
+  uploadInProgress: false,
+  uploadPercentage: 0,
+
+  docConflictCount: 0
+};
+
+export default function docEditor (state = initialState, action) {
+  const { options, type } = action;
+  switch (type) {
+
+    case ActionTypes.RESET_DOC:
+      return {
+        ...initialState
+      };
+
+    case ActionTypes.DOC_LOADED:
+      const conflictCount = options.doc.get('_conflicts') ? options.doc.get('_conflicts').length : 0;
+      options.doc.unset('_conflicts');
+      return {
+        ...state,
+        isLoading: false,
+        doc: options.doc,
+        docConflictCount: conflictCount,
+      };
+
+    case ActionTypes.SHOW_CLONE_DOC_MODAL:
+      return {
+        ...state,
+        cloneDocModalVisible: true
+      };
+
+    case ActionTypes.HIDE_CLONE_DOC_MODAL:
+      return {
+        ...state,
+        cloneDocModalVisible: false
+      };
+
+    case ActionTypes.SHOW_DELETE_DOC_CONFIRMATION_MODAL:
+      return {
+        ...state,
+        deleteDocModalVisible: true
+      };
+
+    case ActionTypes.HIDE_DELETE_DOC_CONFIRMATION_MODAL:
+      return {
+        ...state,
+        deleteDocModalVisible: false
+      };
+
+    case ActionTypes.SHOW_UPLOAD_MODAL:
+      return {
+        ...state,
+        uploadModalVisible: true
+      };
+
+    case ActionTypes.HIDE_UPLOAD_MODAL:
+      return {
+        ...state,
+        uploadModalVisible: false
+      };
+
+    case ActionTypes.FILE_UPLOAD_SUCCESS:
+      return {
+        ...state,
+        numFilesUploaded: state.numFilesUploaded + 1
+      };
+
+    case ActionTypes.FILE_UPLOAD_ERROR:
+      return {
+        ...state,
+        uploadInProgress: false,
+        uploadPercentage: 0,
+        uploadErrorMessage: options.error
+      };
+
+    case ActionTypes.RESET_UPLOAD_MODAL:
+      return {
+        ...state,
+        uploadInProgress: false,
+        uploadPercentage: 0,
+        uploadErrorMessage: ''
+      };
+
+    case ActionTypes.START_FILE_UPLOAD:
+      return {
+        ...state,
+        uploadInProgress: true,
+        uploadPercentage: 0,
+        fileUploadErrorMsg: ''
+      };
+
+    case ActionTypes.SET_FILE_UPLOAD_PERCENTAGE:
+      return {
+        ...state,
+        uploadPercentage: options.percent
+      };
+
+    default:
+      return state;
+  }
+}
diff --git a/app/addons/documents/doc-editor/stores.js b/app/addons/documents/doc-editor/stores.js
deleted file mode 100644
index b3806da..0000000
--- a/app/addons/documents/doc-editor/stores.js
+++ /dev/null
@@ -1,205 +0,0 @@
-// Licensed under the Apache License, Version 2.0 (the "License"); you may not
-// use this file except in compliance with the License. You may obtain a copy of
-// the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-// License for the specific language governing permissions and limitations under
-// the License.
-
-import FauxtonAPI from "../../../core/api";
-import ActionTypes from "./actiontypes";
-var Stores = {};
-
-Stores.DocEditorStore = FauxtonAPI.Store.extend({
-  initialize: function () {
-    this.reset();
-  },
-
-  reset: function () {
-    this._doc = null;
-    this._isLoading = true;
-    this._cloneDocModalVisible = false;
-    this._deleteDocModalVisible = false;
-    this._uploadModalVisible = false;
-
-    // file upload-related fields
-    this._numFilesUploaded = 0;
-    this._fileUploadErrorMsg = '';
-    this._uploadInProgress = false;
-    this._fileUploadLoadPercentage = 0;
-
-    this._docConflictCount = null;
-  },
-
-  isLoading: function () {
-    return this._isLoading;
-  },
-
-  getDocConflictCount: function () {
-    return this._docConflictCount;
-  },
-
-  docLoaded: function (options) {
-    this._isLoading = false;
-    this._docConflictCount = options.doc.get('_conflicts') ? options.doc.get('_conflicts').length : 0;
-    options.doc.unset('_conflicts');
-    this._doc = options.doc;
-  },
-
-  getDoc: function () {
-    return this._doc;
-  },
-
-  isCloneDocModalVisible: function () {
-    return this._cloneDocModalVisible;
-  },
-
-  showCloneDocModal: function () {
-    this._cloneDocModalVisible = true;
-  },
-
-  hideCloneDocModal: function () {
-    this._cloneDocModalVisible = false;
-  },
-
-  isDeleteDocModalVisible: function () {
-    return this._deleteDocModalVisible;
-  },
-
-  showDeleteDocModal: function () {
-    this._deleteDocModalVisible = true;
-  },
-
-  hideDeleteDocModal: function () {
-    this._deleteDocModalVisible = false;
-  },
-
-  isUploadModalVisible: function () {
-    return this._uploadModalVisible;
-  },
-
-  showUploadModal: function () {
-    this._uploadModalVisible = true;
-  },
-
-  hideUploadModal: function () {
-    this._uploadModalVisible = false;
-  },
-
-  getNumFilesUploaded: function () {
-    return this._numFilesUploaded;
-  },
-
-  getFileUploadErrorMsg: function () {
-    return this._fileUploadErrorMsg;
-  },
-
-  setFileUploadErrorMsg: function (error) {
-    this._uploadInProgress = false;
-    this._fileUploadLoadPercentage = 0;
-    this._fileUploadErrorMsg = error;
-  },
-
-  isUploadInProgress: function () {
-    return this._uploadInProgress;
-  },
-
-  getUploadLoadPercentage: function () {
-    return this._fileUploadLoadPercentage;
-  },
-
-  resetUploadModal: function () {
-    this._uploadInProgress = false;
-    this._fileUploadLoadPercentage = 0;
-    this._fileUploadErrorMsg = '';
-  },
-
-  startFileUpload: function () {
-    this._uploadInProgress = true;
-    this._fileUploadLoadPercentage = 0;
-    this._fileUploadErrorMsg = '';
-  },
-
-  dispatch: function (action) {
-    switch (action.type) {
-      case ActionTypes.RESET_DOC:
-        this.reset();
-        break;
-
-      case ActionTypes.DOC_LOADED:
-        this.docLoaded(action.options);
-        this.triggerChange();
-        break;
-
-      case ActionTypes.SHOW_CLONE_DOC_MODAL:
-        this.showCloneDocModal();
-        this.triggerChange();
-        break;
-
-      case ActionTypes.HIDE_CLONE_DOC_MODAL:
-        this.hideCloneDocModal();
-        this.triggerChange();
-        break;
-
-      case ActionTypes.SHOW_DELETE_DOC_CONFIRMATION_MODAL:
-        this.showDeleteDocModal();
-        this.triggerChange();
-        break;
-
-      case ActionTypes.HIDE_DELETE_DOC_CONFIRMATION_MODAL:
-        this.hideDeleteDocModal();
-        this.triggerChange();
-        break;
-
-      case ActionTypes.SHOW_UPLOAD_MODAL:
-        this.showUploadModal();
-        this.triggerChange();
-        break;
-
-      case ActionTypes.HIDE_UPLOAD_MODAL:
-        this.hideUploadModal();
-        this.triggerChange();
-        break;
-
-      case ActionTypes.FILE_UPLOAD_SUCCESS:
-        this._numFilesUploaded++;
-        this.triggerChange();
-        break;
-
-      case ActionTypes.FILE_UPLOAD_ERROR:
-        this.setFileUploadErrorMsg(action.options.error);
-        this.triggerChange();
-        break;
-
-      case ActionTypes.RESET_UPLOAD_MODAL:
-        this.resetUploadModal();
-        this.triggerChange();
-        break;
-
-      case ActionTypes.START_FILE_UPLOAD:
-        this.startFileUpload();
-        this.triggerChange();
-        break;
-
-      case ActionTypes.SET_FILE_UPLOAD_PERCENTAGE:
-        this._fileUploadLoadPercentage = action.options.percent;
-        this.triggerChange();
-        break;
-
-
-      default:
-        return;
-      // do nothing
-    }
-  }
-
-});
-
-Stores.docEditorStore = new Stores.DocEditorStore();
-Stores.docEditorStore.dispatchToken = FauxtonAPI.dispatcher.register(Stores.docEditorStore.dispatch.bind(Stores.docEditorStore));
-
-export default Stores;
diff --git a/app/addons/documents/routes-doc-editor.js b/app/addons/documents/routes-doc-editor.js
index aa09710..f2509ab 100644
--- a/app/addons/documents/routes-doc-editor.js
+++ b/app/addons/documents/routes-doc-editor.js
@@ -15,7 +15,7 @@
 import Documents from "./resources";
 import Databases from "../databases/base";
 import Actions from "./doc-editor/actions";
-import ReactComponents from "./doc-editor/components";
+import DocEditorContainer from "./doc-editor/components/DocEditorContainer";
 import RevBrowserContainer from './rev-browser/container';
 import {DocEditorLayout} from '../components/layouts';
 
@@ -77,13 +77,13 @@
       this.doc = new Documents.Doc({ _id: docId }, { database: this.database, fetchConflicts: true });
     }
 
-    Actions.initDocEditor({ doc: this.doc, database: this.database });
+    Actions.dispatchInitDocEditor({ doc: this.doc, database: this.database });
 
     return <DocEditorLayout
       crumbs={crumbs}
       endpoint={this.doc.url('apiurl')}
       docURL={this.doc.documentation()}
-      component={<ReactComponents.DocEditorController
+      component={<DocEditorContainer
         database={this.database}
         isNewDoc={docId ? false : true}
       />}