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}
/>}