You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by am...@apache.org on 2020/05/20 16:27:54 UTC

[couchdb-fauxton] branch master updated: Updates the UX when saving a document (#1280)

This is an automated email from the ASF dual-hosted git repository.

amaranhao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/couchdb-fauxton.git


The following commit(s) were added to refs/heads/master by this push:
     new bcc1fdc  Updates the UX when saving a document (#1280)
bcc1fdc is described below

commit bcc1fdc20381b201338e37d6add5ee1ded4aca23
Author: Antonio Maranhao <30...@users.noreply.github.com>
AuthorDate: Wed May 20 12:27:47 2020 -0400

    Updates the UX when saving a document (#1280)
    
    * Updates the UX when saving a document
    
    After pressing Save Document, the 'Saving document' notification is no longer displayed.
    Instead the UI controls are disabled while the operation is in progress, and a
    error/success notification is displayed when the operation completes.
    
    * Fix intermitent Nightwatch test failure
---
 app/addons/components/components/codeeditor.js     |  9 +++--
 app/addons/documents/assets/less/doc-editor.less   | 14 ++++++++
 .../__tests__/doc-editor.actions.test.js           | 37 +++++++++++++++++++++
 .../__tests__/doc-editor.reducers.test.js          |  9 +++++
 app/addons/documents/doc-editor/actions.js         | 14 +++++---
 app/addons/documents/doc-editor/actiontypes.js     |  4 ++-
 .../components/AttachmentsPanelButton.js           |  4 ++-
 .../doc-editor/components/DocEditorContainer.js    |  3 +-
 .../doc-editor/components/DocEditorScreen.js       | 38 +++++++++++++++++-----
 .../documents/doc-editor/components/PanelButton.js | 14 +++++---
 app/addons/documents/doc-editor/reducers.js        | 13 ++++++++
 11 files changed, 135 insertions(+), 24 deletions(-)

diff --git a/app/addons/components/components/codeeditor.js b/app/addons/components/components/codeeditor.js
index 7854764..ca7a9e3 100644
--- a/app/addons/components/components/codeeditor.js
+++ b/app/addons/components/components/codeeditor.js
@@ -10,7 +10,6 @@
 // License for the specific language governing permissions and limitations under
 // the License.
 import React from "react";
-import ReactDOM from "react-dom";
 import FauxtonAPI from "../../../core/api";
 import ace from "brace";
 import "brace/ext/searchbox";
@@ -30,7 +29,7 @@ export class CodeEditor extends React.Component {
     // this sets the default value for the editor. On the fly changes are stored in state in this component only. To
     // change the editor content after initial construction use CodeEditor.setValue()
     defaultCode: '',
-
+    disabled: false,
     showGutter: true,
     highlightActiveLine: true,
     showPrintMargin: false,
@@ -119,6 +118,7 @@ export class CodeEditor extends React.Component {
     if (this.props.autoFocus) {
       this.editor.focus();
     }
+    this.editor.setReadOnly(props.disabled);
   };
 
   addCommands = () => {
@@ -361,7 +361,10 @@ export class CodeEditor extends React.Component {
     return (
       <div>
         <div ref={node => this.ace = node} className="js-editor" id={this.props.id}></div>
-        <button ref={node => this.stringEditIcon = node} className="btn string-edit" title="Edit string" disabled={!this.state.stringEditIconVisible}
+        <button ref={node => this.stringEditIcon = node}
+          className="btn string-edit"
+          title="Edit string"
+          disabled={!this.state.stringEditIconVisible || this.props.disabled}
           style={this.state.stringEditIconStyle} onClick={this.openStringEditModal}>
           <i className="icon icon-edit"></i>
         </button>
diff --git a/app/addons/documents/assets/less/doc-editor.less b/app/addons/documents/assets/less/doc-editor.less
index b0f3e9e..2866610 100644
--- a/app/addons/documents/assets/less/doc-editor.less
+++ b/app/addons/documents/assets/less/doc-editor.less
@@ -85,6 +85,16 @@
       text-decoration: none;
       cursor: pointer;
     }
+    &--disabled {
+      opacity: 0.5;
+      cursor: not-allowed;
+      color: #666;
+    }
+    &--disabled:hover {
+      opacity: 0.5;
+      cursor: not-allowed;
+      color: #666;
+    }
   }
   .panel-button {
     color: #666;
@@ -93,6 +103,10 @@
       text-decoration: none;
       cursor: pointer;
     }
+    &:disabled {
+      color: @grayLight;
+      cursor: not-allowed;
+    }
   }
 
   .bgEditorGutter {
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 9cfc505..317b678 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
@@ -129,4 +129,41 @@ describe('DocEditorActions', () => {
     );
   });
 
+  it('sets and resets the isSaving state', () => {
+    sinon.stub(FauxtonAPI, 'addNotification');
+    sinon.stub(FauxtonAPI, 'navigate');
+    sinon.stub(FauxtonAPI, 'urls').callsFake((p1, p2, p3, p4, p5, p6) => {
+      return [p1, p2, p3, p4, p5, p6].join('/');
+    });
+    const mockDispatch = sinon.stub();
+    const mockRes = {
+      then: cb => {
+        cb();
+        return {
+          fail: () => {},
+        };
+      }
+    };
+    const mockDoc = {
+      database: {
+        id: 'mock_db'
+      },
+      save: sinon.stub().returns(mockRes),
+      prettyJSON: sinon.stub(),
+    };
+    Actions.saveDoc(mockDoc, true, () => {}, '')(mockDispatch);
+    sinon.assert.calledWithExactly(
+      mockDispatch,
+      {
+        type: 'SAVING_DOCUMENT'
+      }
+    );
+    sinon.assert.calledWithExactly(
+      mockDispatch,
+      {
+        type: 'SAVING_DOCUMENT_COMPLETED'
+      }
+    );
+  });
+
 });
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
index 5ac1050..a775fa2 100644
--- a/app/addons/documents/doc-editor/__tests__/doc-editor.reducers.test.js
+++ b/app/addons/documents/doc-editor/__tests__/doc-editor.reducers.test.js
@@ -25,6 +25,7 @@ describe('DocEditor Reducer', function () {
     const newState = reducer(undefined, { type: 'do_nothing'});
 
     expect(newState.isLoading).toBe(true);
+    expect(newState.isSaving).toBe(false);
     expect(newState.cloneDocModalVisible).toBe(false);
     expect(newState.deleteDocModalVisible).toBe(false);
     expect(newState.uploadModalVisible).toBe(false);
@@ -65,4 +66,12 @@ describe('DocEditor Reducer', function () {
     expect(newStateHide.uploadModalVisible).toBe(false);
   });
 
+  it('saving document in-progress / completed', function () {
+    const newStateSaving = reducer(undefined, { type: ActionTypes.SAVING_DOCUMENT });
+    expect(newStateSaving.isSaving).toBe(true);
+
+    const newStateSavingDone = reducer(undefined, { type: ActionTypes.SAVING_DOCUMENT_COMPLETED });
+    expect(newStateSavingDone.isSaving).toBe(false);
+  });
+
 });
diff --git a/app/addons/documents/doc-editor/actions.js b/app/addons/documents/doc-editor/actions.js
index 95ce259..2132506 100644
--- a/app/addons/documents/doc-editor/actions.js
+++ b/app/addons/documents/doc-editor/actions.js
@@ -46,21 +46,25 @@ const initDocEditor = (params) => (dispatch) => {
   });
 };
 
-const saveDoc = (doc, isValidDoc, onSave, navigateToUrl) => {
+const saveDoc = (doc, isValidDoc, onSave, navigateToUrl) => dispatch => {
   if (isValidDoc) {
-    FauxtonAPI.addNotification({
-      msg: 'Saving document.',
-      clear: true
-    });
+    dispatch({ type: ActionTypes.SAVING_DOCUMENT });
 
     doc.save().then(function () {
       onSave(doc.prettyJSON());
+      dispatch({ type: ActionTypes.SAVING_DOCUMENT_COMPLETED });
+      FauxtonAPI.addNotification({
+        msg: 'Document saved successfully.',
+        type: 'success',
+        clear: true
+      });
       if (navigateToUrl) {
         FauxtonAPI.navigate(navigateToUrl, {trigger: true});
       } else {
         FauxtonAPI.navigate('#/' + FauxtonAPI.urls('allDocs', 'app',  FauxtonAPI.url.encode(doc.database.id)), {trigger: true});
       }
     }).fail(function (xhr) {
+      dispatch({ type: ActionTypes.SAVING_DOCUMENT_COMPLETED });
       FauxtonAPI.addNotification({
         msg: 'Save failed: ' + JSON.parse(xhr.responseText).reason,
         type: 'error',
diff --git a/app/addons/documents/doc-editor/actiontypes.js b/app/addons/documents/doc-editor/actiontypes.js
index 25df4b9..c9191ba 100644
--- a/app/addons/documents/doc-editor/actiontypes.js
+++ b/app/addons/documents/doc-editor/actiontypes.js
@@ -25,5 +25,7 @@ export default {
   FILE_UPLOAD_SUCCESS: 'FILE_UPLOAD_SUCCESS',
   FILE_UPLOAD_ERROR: 'FILE_UPLOAD_ERROR',
   START_FILE_UPLOAD: 'START_FILE_UPLOAD',
-  SET_FILE_UPLOAD_PERCENTAGE: 'SET_FILE_UPLOAD_PERCENTAGE'
+  SET_FILE_UPLOAD_PERCENTAGE: 'SET_FILE_UPLOAD_PERCENTAGE',
+  SAVING_DOCUMENT: 'SAVING_DOCUMENT',
+  SAVING_DOCUMENT_COMPLETED: 'SAVING_DOCUMENT_COMPLETED',
 };
diff --git a/app/addons/documents/doc-editor/components/AttachmentsPanelButton.js b/app/addons/documents/doc-editor/components/AttachmentsPanelButton.js
index 043e333..36b683e 100644
--- a/app/addons/documents/doc-editor/components/AttachmentsPanelButton.js
+++ b/app/addons/documents/doc-editor/components/AttachmentsPanelButton.js
@@ -21,11 +21,13 @@ import Helpers from '../../../../helpers';
 export default class AttachmentsPanelButton extends React.Component {
   static propTypes = {
     isLoading: PropTypes.bool.isRequired,
+    disabled: PropTypes.bool,
     doc: PropTypes.object
   };
 
   static defaultProps = {
     isLoading: true,
+    disabled: false,
     doc: {}
   };
 
@@ -52,7 +54,7 @@ export default class AttachmentsPanelButton extends React.Component {
 
     return (
       <div className="panel-section view-attachments-section btn-group">
-        <Dropdown id="view-attachments-menu">
+        <Dropdown id="view-attachments-menu" disabled={this.props.disabled} >
           <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>
diff --git a/app/addons/documents/doc-editor/components/DocEditorContainer.js b/app/addons/documents/doc-editor/components/DocEditorContainer.js
index 17eeba4..4b3d8f3 100644
--- a/app/addons/documents/doc-editor/components/DocEditorContainer.js
+++ b/app/addons/documents/doc-editor/components/DocEditorContainer.js
@@ -17,6 +17,7 @@ import DocEditorScreen from './DocEditorScreen';
 const mapStateToProps = ({ docEditor, databases }, ownProps) => {
   return {
     isLoading: docEditor.isLoading || databases.isLoadingDbInfo,
+    isSaving: docEditor.isSaving,
     isNewDoc: ownProps.isNewDoc,
     isDbPartitioned: databases.isDbPartitioned,
     doc: docEditor.doc,
@@ -38,7 +39,7 @@ const mapStateToProps = ({ docEditor, databases }, ownProps) => {
 const mapDispatchToProps = (dispatch) => {
   return {
     saveDoc: (doc, isValidDoc, onSave, navigateToUrl) => {
-      Actions.saveDoc(doc, isValidDoc, onSave, navigateToUrl);
+      dispatch(Actions.saveDoc(doc, isValidDoc, onSave, navigateToUrl));
     },
 
     showCloneDocModal: () => {
diff --git a/app/addons/documents/doc-editor/components/DocEditorScreen.js b/app/addons/documents/doc-editor/components/DocEditorScreen.js
index 242b3e6..448e4c7 100644
--- a/app/addons/documents/doc-editor/components/DocEditorScreen.js
+++ b/app/addons/documents/doc-editor/components/DocEditorScreen.js
@@ -30,6 +30,7 @@ export default class DocEditorScreen extends React.Component {
 
   static propTypes = {
     isLoading: PropTypes.bool.isRequired,
+    isSaving: PropTypes.bool.isRequired,
     isNewDoc: PropTypes.bool.isRequired,
     isDbPartitioned: PropTypes.bool.isRequired,
     doc: PropTypes.object,
@@ -83,6 +84,7 @@ export default class DocEditorScreen extends React.Component {
         id="doc-editor"
         ref={node => this.docEditor = node}
         defaultCode={code}
+        disabled={this.props.isSaving}
         mode="json"
         autoFocus={true}
         editorCommands={editorCommands}
@@ -136,7 +138,9 @@ export default class DocEditorScreen extends React.Component {
   };
 
   clearChanges = () => {
-    this.docEditor.clearChanges();
+    if (this.docEditor) {
+      this.docEditor.clearChanges();
+    }
   };
 
   getExtensionIcons = () => {
@@ -152,36 +156,54 @@ export default class DocEditorScreen extends React.Component {
     }
     return (
       <div>
-        <AttachmentsPanelButton doc={this.props.doc} isLoading={this.props.isLoading} />
+        <AttachmentsPanelButton
+          doc={this.props.doc}
+          isLoading={this.props.isLoading}
+          disabled={this.props.isSaving} />
         <div className="doc-editor-extension-icons">{this.getExtensionIcons()}</div>
 
         {this.props.conflictCount ? <PanelButton
           title={`Conflicts (${this.props.conflictCount})`}
           iconClass="icon-columns"
           className="conflicts"
+          disabled={this.props.isSaving}
           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} />
+        <PanelButton className="upload"
+          title="Upload Attachment"
+          iconClass="icon-circle-arrow-up"
+          disabled={this.props.isSaving}
+          onClick={this.props.showUploadModal} />
+        <PanelButton title="Clone Document"
+          iconClass="icon-repeat"
+          disabled={this.props.isSaving}
+          onClick={this.props.showCloneDocModal} />
+        <PanelButton title="Delete"
+          iconClass="icon-trash"
+          disabled={this.props.isSaving}
+          onClick={this.props.showDeleteDocModal} />
       </div>
     );
   };
 
   render() {
-    const saveButtonLabel = (this.props.isNewDoc) ? 'Create Document' : 'Save Changes';
+    const saveButtonLabel = this.props.isSaving ?
+      'Saving...' :
+      (this.props.isNewDoc ? 'Create Document' : 'Save Changes');
     const endpoint = this.props.previousUrl ?
       this.props.previousUrl :
       FauxtonAPI.urls('allDocs', 'app', FauxtonAPI.url.encode(this.props.database.id));
+    let cancelBtClass = `js-back cancel-button ${this.props.isSaving ? 'cancel-button--disabled' : ''}`;
     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}>
+            <button disabled={this.props.isSaving} 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>
+              <a href={this.props.isSaving ? undefined : `#/${endpoint}`}
+                className={cancelBtClass}>Cancel</a>
             </div>
           </div>
           <div className="alignRight">
diff --git a/app/addons/documents/doc-editor/components/PanelButton.js b/app/addons/documents/doc-editor/components/PanelButton.js
index fd7dc64..682f9df 100644
--- a/app/addons/documents/doc-editor/components/PanelButton.js
+++ b/app/addons/documents/doc-editor/components/PanelButton.js
@@ -12,28 +12,32 @@
 
 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
+    className: PropTypes.string,
+    disabled: PropTypes.bool
   };
 
   static defaultProps = {
     title: '',
     iconClass: '',
     onClick: () => { },
-    className: ''
+    className: '',
+    disabled: false
   };
 
   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}>
+        <button className={`panel-button ${this.props.className}`}
+          title={this.props.title}
+          onClick={this.props.onClick}
+          disabled={this.props.disabled} >
+
           <i className={iconClasses}></i>
           <span>{this.props.title}</span>
         </button>
diff --git a/app/addons/documents/doc-editor/reducers.js b/app/addons/documents/doc-editor/reducers.js
index 4945e30..694c0bb 100644
--- a/app/addons/documents/doc-editor/reducers.js
+++ b/app/addons/documents/doc-editor/reducers.js
@@ -15,6 +15,7 @@ import ActionTypes from "./actiontypes";
 const initialState = {
   doc: null,
   isLoading: true,
+  isSaving: false,
   cloneDocModalVisible: false,
   deleteDocModalVisible: false,
   uploadModalVisible: false,
@@ -118,6 +119,18 @@ export default function docEditor (state = initialState, action) {
         uploadPercentage: options.percent
       };
 
+    case ActionTypes.SAVING_DOCUMENT:
+      return {
+        ...state,
+        isSaving: true,
+      };
+
+    case ActionTypes.SAVING_DOCUMENT_COMPLETED:
+      return {
+        ...state,
+        isSaving: false,
+      };
+
     default:
       return state;
   }