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