Skip to content

Commit bcc1fdc

Browse files
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
1 parent e9ae32c commit bcc1fdc

File tree

11 files changed

+135
-24
lines changed

11 files changed

+135
-24
lines changed

app/addons/components/components/codeeditor.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
// License for the specific language governing permissions and limitations under
1111
// the License.
1212
import React from "react";
13-
import ReactDOM from "react-dom";
1413
import FauxtonAPI from "../../../core/api";
1514
import ace from "brace";
1615
import "brace/ext/searchbox";
@@ -30,7 +29,7 @@ export class CodeEditor extends React.Component {
3029
// this sets the default value for the editor. On the fly changes are stored in state in this component only. To
3130
// change the editor content after initial construction use CodeEditor.setValue()
3231
defaultCode: '',
33-
32+
disabled: false,
3433
showGutter: true,
3534
highlightActiveLine: true,
3635
showPrintMargin: false,
@@ -119,6 +118,7 @@ export class CodeEditor extends React.Component {
119118
if (this.props.autoFocus) {
120119
this.editor.focus();
121120
}
121+
this.editor.setReadOnly(props.disabled);
122122
};
123123

124124
addCommands = () => {
@@ -361,7 +361,10 @@ export class CodeEditor extends React.Component {
361361
return (
362362
<div>
363363
<div ref={node => this.ace = node} className="js-editor" id={this.props.id}></div>
364-
<button ref={node => this.stringEditIcon = node} className="btn string-edit" title="Edit string" disabled={!this.state.stringEditIconVisible}
364+
<button ref={node => this.stringEditIcon = node}
365+
className="btn string-edit"
366+
title="Edit string"
367+
disabled={!this.state.stringEditIconVisible || this.props.disabled}
365368
style={this.state.stringEditIconStyle} onClick={this.openStringEditModal}>
366369
<i className="icon icon-edit"></i>
367370
</button>

app/addons/documents/assets/less/doc-editor.less

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,16 @@
8585
text-decoration: none;
8686
cursor: pointer;
8787
}
88+
&--disabled {
89+
opacity: 0.5;
90+
cursor: not-allowed;
91+
color: #666;
92+
}
93+
&--disabled:hover {
94+
opacity: 0.5;
95+
cursor: not-allowed;
96+
color: #666;
97+
}
8898
}
8999
.panel-button {
90100
color: #666;
@@ -93,6 +103,10 @@
93103
text-decoration: none;
94104
cursor: pointer;
95105
}
106+
&:disabled {
107+
color: @grayLight;
108+
cursor: not-allowed;
109+
}
96110
}
97111

98112
.bgEditorGutter {

app/addons/documents/doc-editor/__tests__/doc-editor.actions.test.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,41 @@ describe('DocEditorActions', () => {
129129
);
130130
});
131131

132+
it('sets and resets the isSaving state', () => {
133+
sinon.stub(FauxtonAPI, 'addNotification');
134+
sinon.stub(FauxtonAPI, 'navigate');
135+
sinon.stub(FauxtonAPI, 'urls').callsFake((p1, p2, p3, p4, p5, p6) => {
136+
return [p1, p2, p3, p4, p5, p6].join('/');
137+
});
138+
const mockDispatch = sinon.stub();
139+
const mockRes = {
140+
then: cb => {
141+
cb();
142+
return {
143+
fail: () => {},
144+
};
145+
}
146+
};
147+
const mockDoc = {
148+
database: {
149+
id: 'mock_db'
150+
},
151+
save: sinon.stub().returns(mockRes),
152+
prettyJSON: sinon.stub(),
153+
};
154+
Actions.saveDoc(mockDoc, true, () => {}, '')(mockDispatch);
155+
sinon.assert.calledWithExactly(
156+
mockDispatch,
157+
{
158+
type: 'SAVING_DOCUMENT'
159+
}
160+
);
161+
sinon.assert.calledWithExactly(
162+
mockDispatch,
163+
{
164+
type: 'SAVING_DOCUMENT_COMPLETED'
165+
}
166+
);
167+
});
168+
132169
});

app/addons/documents/doc-editor/__tests__/doc-editor.reducers.test.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe('DocEditor Reducer', function () {
2525
const newState = reducer(undefined, { type: 'do_nothing'});
2626

2727
expect(newState.isLoading).toBe(true);
28+
expect(newState.isSaving).toBe(false);
2829
expect(newState.cloneDocModalVisible).toBe(false);
2930
expect(newState.deleteDocModalVisible).toBe(false);
3031
expect(newState.uploadModalVisible).toBe(false);
@@ -65,4 +66,12 @@ describe('DocEditor Reducer', function () {
6566
expect(newStateHide.uploadModalVisible).toBe(false);
6667
});
6768

69+
it('saving document in-progress / completed', function () {
70+
const newStateSaving = reducer(undefined, { type: ActionTypes.SAVING_DOCUMENT });
71+
expect(newStateSaving.isSaving).toBe(true);
72+
73+
const newStateSavingDone = reducer(undefined, { type: ActionTypes.SAVING_DOCUMENT_COMPLETED });
74+
expect(newStateSavingDone.isSaving).toBe(false);
75+
});
76+
6877
});

app/addons/documents/doc-editor/actions.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,21 +46,25 @@ const initDocEditor = (params) => (dispatch) => {
4646
});
4747
};
4848

49-
const saveDoc = (doc, isValidDoc, onSave, navigateToUrl) => {
49+
const saveDoc = (doc, isValidDoc, onSave, navigateToUrl) => dispatch => {
5050
if (isValidDoc) {
51-
FauxtonAPI.addNotification({
52-
msg: 'Saving document.',
53-
clear: true
54-
});
51+
dispatch({ type: ActionTypes.SAVING_DOCUMENT });
5552

5653
doc.save().then(function () {
5754
onSave(doc.prettyJSON());
55+
dispatch({ type: ActionTypes.SAVING_DOCUMENT_COMPLETED });
56+
FauxtonAPI.addNotification({
57+
msg: 'Document saved successfully.',
58+
type: 'success',
59+
clear: true
60+
});
5861
if (navigateToUrl) {
5962
FauxtonAPI.navigate(navigateToUrl, {trigger: true});
6063
} else {
6164
FauxtonAPI.navigate('#/' + FauxtonAPI.urls('allDocs', 'app', FauxtonAPI.url.encode(doc.database.id)), {trigger: true});
6265
}
6366
}).fail(function (xhr) {
67+
dispatch({ type: ActionTypes.SAVING_DOCUMENT_COMPLETED });
6468
FauxtonAPI.addNotification({
6569
msg: 'Save failed: ' + JSON.parse(xhr.responseText).reason,
6670
type: 'error',

app/addons/documents/doc-editor/actiontypes.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,7 @@ export default {
2525
FILE_UPLOAD_SUCCESS: 'FILE_UPLOAD_SUCCESS',
2626
FILE_UPLOAD_ERROR: 'FILE_UPLOAD_ERROR',
2727
START_FILE_UPLOAD: 'START_FILE_UPLOAD',
28-
SET_FILE_UPLOAD_PERCENTAGE: 'SET_FILE_UPLOAD_PERCENTAGE'
28+
SET_FILE_UPLOAD_PERCENTAGE: 'SET_FILE_UPLOAD_PERCENTAGE',
29+
SAVING_DOCUMENT: 'SAVING_DOCUMENT',
30+
SAVING_DOCUMENT_COMPLETED: 'SAVING_DOCUMENT_COMPLETED',
2931
};

app/addons/documents/doc-editor/components/AttachmentsPanelButton.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@ import Helpers from '../../../../helpers';
2121
export default class AttachmentsPanelButton extends React.Component {
2222
static propTypes = {
2323
isLoading: PropTypes.bool.isRequired,
24+
disabled: PropTypes.bool,
2425
doc: PropTypes.object
2526
};
2627

2728
static defaultProps = {
2829
isLoading: true,
30+
disabled: false,
2931
doc: {}
3032
};
3133

@@ -52,7 +54,7 @@ export default class AttachmentsPanelButton extends React.Component {
5254

5355
return (
5456
<div className="panel-section view-attachments-section btn-group">
55-
<Dropdown id="view-attachments-menu">
57+
<Dropdown id="view-attachments-menu" disabled={this.props.disabled} >
5658
<Dropdown.Toggle noCaret className="panel-button dropdown-toggle btn" data-bypass="true">
5759
<i className="icon icon-paper-clip"></i>
5860
<span className="button-text">View Attachments</span>

app/addons/documents/doc-editor/components/DocEditorContainer.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import DocEditorScreen from './DocEditorScreen';
1717
const mapStateToProps = ({ docEditor, databases }, ownProps) => {
1818
return {
1919
isLoading: docEditor.isLoading || databases.isLoadingDbInfo,
20+
isSaving: docEditor.isSaving,
2021
isNewDoc: ownProps.isNewDoc,
2122
isDbPartitioned: databases.isDbPartitioned,
2223
doc: docEditor.doc,
@@ -38,7 +39,7 @@ const mapStateToProps = ({ docEditor, databases }, ownProps) => {
3839
const mapDispatchToProps = (dispatch) => {
3940
return {
4041
saveDoc: (doc, isValidDoc, onSave, navigateToUrl) => {
41-
Actions.saveDoc(doc, isValidDoc, onSave, navigateToUrl);
42+
dispatch(Actions.saveDoc(doc, isValidDoc, onSave, navigateToUrl));
4243
},
4344

4445
showCloneDocModal: () => {

app/addons/documents/doc-editor/components/DocEditorScreen.js

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export default class DocEditorScreen extends React.Component {
3030

3131
static propTypes = {
3232
isLoading: PropTypes.bool.isRequired,
33+
isSaving: PropTypes.bool.isRequired,
3334
isNewDoc: PropTypes.bool.isRequired,
3435
isDbPartitioned: PropTypes.bool.isRequired,
3536
doc: PropTypes.object,
@@ -83,6 +84,7 @@ export default class DocEditorScreen extends React.Component {
8384
id="doc-editor"
8485
ref={node => this.docEditor = node}
8586
defaultCode={code}
87+
disabled={this.props.isSaving}
8688
mode="json"
8789
autoFocus={true}
8890
editorCommands={editorCommands}
@@ -136,7 +138,9 @@ export default class DocEditorScreen extends React.Component {
136138
};
137139

138140
clearChanges = () => {
139-
this.docEditor.clearChanges();
141+
if (this.docEditor) {
142+
this.docEditor.clearChanges();
143+
}
140144
};
141145

142146
getExtensionIcons = () => {
@@ -152,36 +156,54 @@ export default class DocEditorScreen extends React.Component {
152156
}
153157
return (
154158
<div>
155-
<AttachmentsPanelButton doc={this.props.doc} isLoading={this.props.isLoading} />
159+
<AttachmentsPanelButton
160+
doc={this.props.doc}
161+
isLoading={this.props.isLoading}
162+
disabled={this.props.isSaving} />
156163
<div className="doc-editor-extension-icons">{this.getExtensionIcons()}</div>
157164

158165
{this.props.conflictCount ? <PanelButton
159166
title={`Conflicts (${this.props.conflictCount})`}
160167
iconClass="icon-columns"
161168
className="conflicts"
169+
disabled={this.props.isSaving}
162170
onClick={() => { FauxtonAPI.navigate(FauxtonAPI.urls('revision-browser', 'app', this.props.database.safeID(), this.props.doc.id));}}/> : null}
163171

164-
<PanelButton className="upload" title="Upload Attachment" iconClass="icon-circle-arrow-up" onClick={this.props.showUploadModal} />
165-
<PanelButton title="Clone Document" iconClass="icon-repeat" onClick={this.props.showCloneDocModal} />
166-
<PanelButton title="Delete" iconClass="icon-trash" onClick={this.props.showDeleteDocModal} />
172+
<PanelButton className="upload"
173+
title="Upload Attachment"
174+
iconClass="icon-circle-arrow-up"
175+
disabled={this.props.isSaving}
176+
onClick={this.props.showUploadModal} />
177+
<PanelButton title="Clone Document"
178+
iconClass="icon-repeat"
179+
disabled={this.props.isSaving}
180+
onClick={this.props.showCloneDocModal} />
181+
<PanelButton title="Delete"
182+
iconClass="icon-trash"
183+
disabled={this.props.isSaving}
184+
onClick={this.props.showDeleteDocModal} />
167185
</div>
168186
);
169187
};
170188

171189
render() {
172-
const saveButtonLabel = (this.props.isNewDoc) ? 'Create Document' : 'Save Changes';
190+
const saveButtonLabel = this.props.isSaving ?
191+
'Saving...' :
192+
(this.props.isNewDoc ? 'Create Document' : 'Save Changes');
173193
const endpoint = this.props.previousUrl ?
174194
this.props.previousUrl :
175195
FauxtonAPI.urls('allDocs', 'app', FauxtonAPI.url.encode(this.props.database.id));
196+
let cancelBtClass = `js-back cancel-button ${this.props.isSaving ? 'cancel-button--disabled' : ''}`;
176197
return (
177198
<div>
178199
<div id="doc-editor-actions-panel">
179200
<div className="doc-actions-left">
180-
<button className="save-doc btn btn-primary save" type="button" onClick={this.saveDoc}>
201+
<button disabled={this.props.isSaving} className="save-doc btn btn-primary save" type="button" onClick={this.saveDoc}>
181202
<i className="icon fonticon-ok-circled"></i> {saveButtonLabel}
182203
</button>
183204
<div>
184-
<a href={`#/${endpoint}`} className="js-back cancel-button">Cancel</a>
205+
<a href={this.props.isSaving ? undefined : `#/${endpoint}`}
206+
className={cancelBtClass}>Cancel</a>
185207
</div>
186208
</div>
187209
<div className="alignRight">

app/addons/documents/doc-editor/components/PanelButton.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,32 @@
1212

1313
import PropTypes from 'prop-types';
1414
import React from 'react';
15-
import ReactDOM from 'react-dom';
16-
1715

1816
export default class PanelButton extends React.Component {
1917
static propTypes = {
2018
title: PropTypes.string.isRequired,
2119
onClick: PropTypes.func.isRequired,
22-
className: PropTypes.string
20+
className: PropTypes.string,
21+
disabled: PropTypes.bool
2322
};
2423

2524
static defaultProps = {
2625
title: '',
2726
iconClass: '',
2827
onClick: () => { },
29-
className: ''
28+
className: '',
29+
disabled: false
3030
};
3131

3232
render() {
3333
var iconClasses = 'icon ' + this.props.iconClass;
3434
return (
3535
<div className="panel-section">
36-
<button className={`panel-button ${this.props.className}`} title={this.props.title} onClick={this.props.onClick}>
36+
<button className={`panel-button ${this.props.className}`}
37+
title={this.props.title}
38+
onClick={this.props.onClick}
39+
disabled={this.props.disabled} >
40+
3741
<i className={iconClasses}></i>
3842
<span>{this.props.title}</span>
3943
</button>

app/addons/documents/doc-editor/reducers.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import ActionTypes from "./actiontypes";
1515
const initialState = {
1616
doc: null,
1717
isLoading: true,
18+
isSaving: false,
1819
cloneDocModalVisible: false,
1920
deleteDocModalVisible: false,
2021
uploadModalVisible: false,
@@ -118,6 +119,18 @@ export default function docEditor (state = initialState, action) {
118119
uploadPercentage: options.percent
119120
};
120121

122+
case ActionTypes.SAVING_DOCUMENT:
123+
return {
124+
...state,
125+
isSaving: true,
126+
};
127+
128+
case ActionTypes.SAVING_DOCUMENT_COMPLETED:
129+
return {
130+
...state,
131+
isSaving: false,
132+
};
133+
121134
default:
122135
return state;
123136
}

0 commit comments

Comments
 (0)