diff --git a/package.json b/package.json index c2cf789e7..70a523566 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "react-day-picker": "^7.0.7", "react-dom": "^15.4.0", "react-select": "^2.0.0", + "react-toastify": "^4.5.2", "scriptjs": "^2.5.7", "shivie8": "^1.0.0", "source-map-support": "^0.4.6", diff --git a/src/clincoded/static/components/app.js b/src/clincoded/static/components/app.js index abd34e556..cb5563fb3 100644 --- a/src/clincoded/static/components/app.js +++ b/src/clincoded/static/components/app.js @@ -3,6 +3,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import createReactClass from 'create-react-class'; import url from 'url'; +import { ToastContainer } from 'react-toastify'; import { content_views } from './globals'; import { Auth0, HistoryAndTriggers } from './mixins'; import { NavbarMixin, Nav, Navbar, NavItem } from '../libs/bootstrap/navigation'; @@ -268,6 +269,7 @@ var App = module.exports = createReactClass({ + @@ -275,6 +277,7 @@ var App = module.exports = createReactClass({ __html: '\n\n' + jsonScriptEscape(JSON.stringify(this.props.context)) + '\n\n' }}>
+
{this.state.demoWarning ? Note: This is a demo version of the site. Data entered will be deleted upon release of updated versions, which occurs roughly once per month. Please contact us with any questions at clingen-helpdesk@lists.stanford.edu } /> diff --git a/src/clincoded/static/components/case_control_curation.js b/src/clincoded/static/components/case_control_curation.js index ef8f1949a..d05250834 100644 --- a/src/clincoded/static/components/case_control_curation.js +++ b/src/clincoded/static/components/case_control_curation.js @@ -10,6 +10,7 @@ import { RestMixin } from './rest'; import { Form, FormMixin, Input } from '../libs/bootstrap/form'; import { PanelGroup, Panel } from '../libs/bootstrap/panel'; import { parsePubmed } from '../libs/parse-pubmed'; +import { showErrorNotification } from '../libs/error_notification'; import * as methods from './methods'; import * as CaseControlEvalScore from './case_control/evaluation_score'; import * as CuratorHistory from './curator_history'; @@ -1557,6 +1558,12 @@ var CaseControlViewer = createReactClass({ window.location.href = '/curation-central/?gdm=' + tempGdm.uuid + '&pmid=' + tempPmid; }, + handleScoreSubmitError: function(err) { + this.setState({ submitBusy: false }); + showErrorNotification(); + console.log('Case-control score submit error: ', err); + }, + scoreSubmit: function(e) { let caseControl = this.props.context; /*****************************************************/ @@ -1572,10 +1579,11 @@ var CaseControlViewer = createReactClass({ // Update and create score object when the score object has the scoreStatus key/value pair if (this.state.userScoreObj.uuid) { return this.putRestData('/evidencescore/' + this.state.userScoreObj.uuid, newUserScoreObj).then(modifiedScoreObj => { - this.setState({submitBusy: false}); return Promise.resolve(modifiedScoreObj['@graph'][0]['@id']); }).then(data => { this.handlePageRedirect(); + }).catch(err => { + this.handleScoreSubmitError(err); }); } else { return this.postRestData('/evidencescore/', newUserScoreObj).then(newScoreObject => { @@ -1596,12 +1604,13 @@ var CaseControlViewer = createReactClass({ newCaseControl.scores.push(newScoreObjectUuid); return this.putRestData('/casecontrol/' + caseControl.uuid, newCaseControl).then(updatedCaseControlObj => { - this.setState({submitBusy: false}); return Promise.resolve(updatedCaseControlObj['@graph'][0]); }); }); }).then(data => { this.handlePageRedirect(); + }).catch(err => { + this.handleScoreSubmitError(err); }); } } else if (Object.keys(newUserScoreObj).length && !newUserScoreObj.hasOwnProperty('score')) { @@ -1633,12 +1642,13 @@ var CaseControlViewer = createReactClass({ }); } return this.putRestData('/casecontrol/' + caseControl.uuid, newCaseControl).then(updatedCaseControlObj => { - this.setState({submitBusy: false}); return Promise.resolve(updatedCaseControlObj['@graph'][0]); }); }); }).then(data => { this.handlePageRedirect(); + }).catch(err => { + this.handleScoreSubmitError(err); }); } } @@ -2118,7 +2128,7 @@ var CaseControlViewer = createReactClass({ {isEvidenceScored || (!isEvidenceScored && affiliation && affiliatedCaseControl) || (!isEvidenceScored && !affiliation && userCaseControl) ? - : null} diff --git a/src/clincoded/static/components/create_gene_disease.js b/src/clincoded/static/components/create_gene_disease.js index a49756951..5ec293574 100644 --- a/src/clincoded/static/components/create_gene_disease.js +++ b/src/clincoded/static/components/create_gene_disease.js @@ -33,7 +33,8 @@ var CreateGeneDisease = createReactClass({ diseaseUuid: null, diseaseError: null, adjectives: [], - adjectiveDisabled: true + adjectiveDisabled: true, + submitBusy: false, }; }, @@ -113,6 +114,7 @@ var CreateGeneDisease = createReactClass({ this.saveFormValue('moiAdjective', moiAdjectiveValue); } if (this.validateForm()) { + this.setState({ submitBusy: true }); // Get the free-text values for the Orphanet ID and the Gene ID to check against the DB /** * FIXME: Need to delete orphanet reference @@ -128,7 +130,10 @@ var CreateGeneDisease = createReactClass({ this.getRestDatas([ '/genes/' + geneId ], [ - function() { this.setFormErrors('hgncgene', 'HGNC gene symbol not found'); }.bind(this) + function() { + this.setState({ submitBusy: false }); + this.setFormErrors('hgncgene', 'HGNC gene symbol not found'); + }.bind(this) ]).then(response => { return this.getRestData('/search?type=disease&diseaseId=' + diseaseObj.diseaseId).then(diseaseSearch => { let diseaseUuid; @@ -189,13 +194,14 @@ var CreateGeneDisease = createReactClass({ }); } else { // Found matching GDM. See of the user wants to curate it. - this.setState({gdm: gdmSearch['@graph'][0]}); + this.setState({ gdm: gdmSearch['@graph'][0], submitBusy: false }); this.child.openModal(); } }); }).catch(e => { // Some unexpected error happened parseAndLogError.bind(undefined, 'fetchedRequest'); + this.setState({ submitBusy: false }); }); } }, @@ -230,6 +236,7 @@ var CreateGeneDisease = createReactClass({ let adjectiveDisabled = this.state.adjectiveDisabled; const moiKeys = Object.keys(modesOfInheritance); let gdm = this.state.gdm; + const { submitBusy } = this.state; return (
@@ -267,7 +274,7 @@ var CreateGeneDisease = createReactClass({

The above options (gene, disease, mode of inheritance, or adjective) can be altered for a Gene:Disease record up until a PMID has been added to the record. This includes adding an adjective to a Gene:Disease:Mode of inheritance record that has already been created or editing an adjective associated with a record.

- +
{gdm && gdm.gene && gdm.disease && gdm.modeInheritance ? diff --git a/src/clincoded/static/components/experimental_curation.js b/src/clincoded/static/components/experimental_curation.js index c75eb8ae1..017f233bf 100644 --- a/src/clincoded/static/components/experimental_curation.js +++ b/src/clincoded/static/components/experimental_curation.js @@ -9,6 +9,7 @@ import { curator_page, content_views, history_views, queryKeyValue, dbxref_prefi import { RestMixin } from './rest'; import { Form, FormMixin, Input } from '../libs/bootstrap/form'; import { PanelGroup, Panel } from '../libs/bootstrap/panel'; +import { showErrorNotification } from '../libs/error_notification'; import { AddResourceId } from './add_external_resource'; import * as CuratorHistory from './curator_history'; import * as methods from './methods'; @@ -2571,6 +2572,12 @@ const ExperimentalViewer = createReactClass({ window.location.href = '/curation-central/?gdm=' + tempGdm.uuid + '&pmid=' + tempPmid; }, + handleScoreSubmitError: function(err) { + this.setState({ submitBusy: false }); + showErrorNotification(); + console.log('Experimental score submit error: ', err); + }, + // Handle the score submit button scoreSubmit: function(e) { let experimental = this.props.context; @@ -2592,10 +2599,11 @@ const ExperimentalViewer = createReactClass({ // Update and create score object when the score object has the scoreStatus key/value pair if (this.state.userScoreObj.uuid) { return this.putRestData('/evidencescore/' + this.state.userScoreObj.uuid, newUserScoreObj).then(modifiedScoreObj => { - this.setState({submitBusy: false}); return Promise.resolve(modifiedScoreObj['@graph'][0]['@id']); }).then(data => { this.handlePageRedirect(); + }).catch(err => { + this.handleScoreSubmitError(err); }); } else { return this.postRestData('/evidencescore/', newUserScoreObj).then(newScoreObject => { @@ -2616,12 +2624,13 @@ const ExperimentalViewer = createReactClass({ newExperimental.scores.push(newScoreObjectUuid); return this.putRestData('/experimental/' + experimental.uuid, newExperimental).then(updatedExperimentalObj => { - this.setState({submitBusy: false}); return Promise.resolve(updatedExperimentalObj['@graph'][0]); }); }); }).then(data => { this.handlePageRedirect(); + }).catch(err => { + this.handleScoreSubmitError(err); }); } } else if (!newUserScoreObj.scoreStatus) { @@ -2653,12 +2662,13 @@ const ExperimentalViewer = createReactClass({ }); } return this.putRestData('/experimental/' + experimental.uuid, newExperimental).then(updatedExperimentalObj => { - this.setState({submitBusy: false}); return Promise.resolve(updatedExperimentalObj['@graph'][0]); }); }); }).then(data => { this.handlePageRedirect(); + }).catch(err => { + this.handleScoreSubmitError(err); }); } } @@ -3273,7 +3283,7 @@ const ExperimentalViewer = createReactClass({ {isEvidenceScored || (!isEvidenceScored && affiliation && affiliatedExperimental) || (!isEvidenceScored && !affiliation && userExperimental) ? + formError={this.state.formError} affiliation={affiliation} submitBusy={this.state.submitBusy} /> : null} {!isEvidenceScored && ((affiliation && !affiliatedExperimental) || (!affiliation && !userExperimental)) ?
diff --git a/src/clincoded/static/components/individual_curation.js b/src/clincoded/static/components/individual_curation.js index c6bb19914..76c70abdc 100644 --- a/src/clincoded/static/components/individual_curation.js +++ b/src/clincoded/static/components/individual_curation.js @@ -11,6 +11,7 @@ import { Form, FormMixin, Input } from '../libs/bootstrap/form'; import { PanelGroup, Panel } from '../libs/bootstrap/panel'; import { parseAndLogError } from './mixins'; import { parsePubmed } from '../libs/parse-pubmed'; +import { showErrorNotification } from '../libs/error_notification'; import { AddResourceId } from './add_external_resource'; import * as CuratorHistory from './curator_history'; import * as methods from './methods'; @@ -1765,6 +1766,12 @@ const IndividualViewer = createReactClass({ window.location.href = '/curation-central/?gdm=' + tempGdm.uuid + '&pmid=' + tempPmid; }, + handleScoreSubmitError: function(err) { + this.setState({ submitBusy: false }); + showErrorNotification(); + console.log('Individual score submit error: ', err); + }, + scoreSubmit: function(e) { let individual = this.props.context; /*****************************************************/ @@ -1794,10 +1801,11 @@ const IndividualViewer = createReactClass({ // Update and create score object when the score object has the scoreStatus key/value pair if (this.state.userScoreObj.uuid) { return this.putRestData('/evidencescore/' + this.state.userScoreObj.uuid, newUserScoreObj).then(modifiedScoreObj => { - this.setState({submitBusy: false}); return Promise.resolve(modifiedScoreObj['@graph'][0]['@id']); }).then(data => { this.handlePageRedirect(); + }).catch(err => { + this.handleScoreSubmitError(err); }); } else { return this.postRestData('/evidencescore/', newUserScoreObj).then(newScoreObject => { @@ -1818,12 +1826,13 @@ const IndividualViewer = createReactClass({ newIndividual.scores.push(newScoreObjectUuid); return this.putRestData('/individual/' + individual.uuid, newIndividual).then(updatedIndividualObj => { - this.setState({submitBusy: false}); return Promise.resolve(updatedIndividualObj['@graph'][0]); }); }); }).then(data => { this.handlePageRedirect(); + }).catch(err => { + this.handleScoreSubmitError(err); }); } } else if (!newUserScoreObj.scoreStatus) { @@ -1855,12 +1864,13 @@ const IndividualViewer = createReactClass({ }); } return this.putRestData('/individual/' + individual.uuid, newIndividual).then(updatedIndividualObj => { - this.setState({submitBusy: false}); return Promise.resolve(updatedIndividualObj['@graph'][0]); }); }); }).then(data => { this.handlePageRedirect(); + }).catch(err => { + this.handleScoreSubmitError(err); }); } } @@ -2167,7 +2177,7 @@ const IndividualViewer = createReactClass({ + variantInfo={variants} gdm={this.state.gdm} pmid={tempPmid ? tempPmid : null} submitBusy={this.state.submitBusy} /> : null} {!isEvidenceScored && ((affiliation && !affiliatedIndividual) || (!affiliation && !userIndividual)) ?
diff --git a/src/clincoded/static/components/score/case_control_score.js b/src/clincoded/static/components/score/case_control_score.js index 76c6d698a..391aaffb3 100644 --- a/src/clincoded/static/components/score/case_control_score.js +++ b/src/clincoded/static/components/score/case_control_score.js @@ -199,7 +199,7 @@ var ScoreCaseControl = module.exports.ScoreCaseControl = createReactClass({
{this.props.scoreSubmit ?
-
: null} diff --git a/src/clincoded/static/components/score/experimental_score.js b/src/clincoded/static/components/score/experimental_score.js index 94c6028b2..d8d6afd17 100644 --- a/src/clincoded/static/components/score/experimental_score.js +++ b/src/clincoded/static/components/score/experimental_score.js @@ -467,7 +467,7 @@ var ScoreExperimental = module.exports.ScoreExperimental = createReactClass({
{this.props.scoreSubmit ?
-
: null} diff --git a/src/clincoded/static/components/score/individual_score.js b/src/clincoded/static/components/score/individual_score.js index c4676f9a1..4eccf9ba1 100644 --- a/src/clincoded/static/components/score/individual_score.js +++ b/src/clincoded/static/components/score/individual_score.js @@ -649,7 +649,7 @@ const ScoreIndividual = module.exports.ScoreIndividual = createReactClass({
{this.props.scoreSubmit ?
-
: null} diff --git a/src/clincoded/static/libs/error_notification.js b/src/clincoded/static/libs/error_notification.js new file mode 100644 index 000000000..76ca93539 --- /dev/null +++ b/src/clincoded/static/libs/error_notification.js @@ -0,0 +1,23 @@ +import React from 'react'; +import { toast } from 'react-toastify'; + +const defaultMessage = Something went wrong! Help us improve your experience by sending an error report to clingen-helpdesk@lists.stanford.edu; +const defaultOptions = { + type: 'error', // 'default', 'sucess', 'info', 'warning', 'error' + autoClose: 5000, // How long until the notification dismisses. Default is 5000. Set to false to disable autoClose + hideProgressBar: true, + pauseOnHover: true, + bodyClassName: 'toast-body', +}; + +/** + * + * @param {string | node} message A string or React element for what to display within the notification + * @param {object} options https://www.npmjs.com/package/react-toastify/v/4.5.2 to see what the available options are + */ +const showErrorNotification = (message = defaultMessage, options = defaultOptions) => { + const combinedOptions = Object.assign({}, defaultOptions, options); + toast(message, combinedOptions); +}; + +export { showErrorNotification }; \ No newline at end of file diff --git a/src/clincoded/static/scss/clincoded/_base.scss b/src/clincoded/static/scss/clincoded/_base.scss index 2586735c9..6baf1803e 100644 --- a/src/clincoded/static/scss/clincoded/_base.scss +++ b/src/clincoded/static/scss/clincoded/_base.scss @@ -155,6 +155,10 @@ dl.dl-horizontal { border-radius: 0; } +.toast-body { + padding: 4px; +} + .alert-demo { color: white; background: #880000;