diff --git a/htdocs/js/loris-scripts.js b/htdocs/js/loris-scripts.js index d07ae39f48..f49b6e74f9 100644 --- a/htdocs/js/loris-scripts.js +++ b/htdocs/js/loris-scripts.js @@ -1,5 +1,70 @@ /* eslint new-cap: ["error", {capIsNewExceptions: ["DynamicTable", "FileUpload"]}]*/ +/** + * Display the login modal when a request returns a 401 response. + */ +function handleUnauthorized() { + if (!window.$ || !window.loris) { + return; + } + + if (!$('#login-modal').length) { + return; + } + + if ($('#login-modal').hasClass('in')) { + $('#login-modal-error').show(); + return; + } + + $('#login-modal').modal('show'); + $('#modal-login') + .off('click.lorisFetch') + .on('click.lorisFetch', function(e) { + e.preventDefault(); + let data = { + username: $('#modal-username').val(), + password: $('#modal-password').val(), + login: 'Login', + }; + + window.lorisFetch(window.loris.BaseURL, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + body: new URLSearchParams(data), + }) + .then((response) => { + if (!response.ok) { + throw new Error('request_failed'); + } + $('#login-modal-error').hide(); + $('#login-modal').modal('hide'); + }) + .catch(() => { + $('#login-modal-error').show(); + }); + }); +} + +if (!window.lorisFetch) { + window.lorisFetch = function(input, init) { + const options = Object.assign( + { + credentials: 'same-origin', + }, + init || {} + ); + return fetch(input, options).then((response) => { + if (response.status === 401) { + handleUnauthorized(); + } + return response; + }); + }; +} + $(document).ready(function() { $('#menu-toggle').click(function(e) { e.preventDefault(); @@ -8,30 +73,3 @@ $(document).ready(function() { $('.dynamictable').DynamicTable(); $('.fileUpload').FileUpload(); }); - -$(document).ajaxError(function(event, jqxhr, settings, thrownError) { - if (jqxhr.status === 401) { - if ($('#login-modal').hasClass('in')) { - $('#login-modal-error').show(); - } else { - $('#login-modal').modal('show'); - $('#modal-login').click(function(e) { - e.preventDefault(); - let data = { - username: $('#modal-username').val(), - password: $('#modal-password').val(), - login: 'Login', - }; - $.ajax({ - type: 'post', - url: loris.BaseURL, - data: data, - success: function() { - $('#login-modal-error').hide(); - $('#login-modal').modal('hide'); - }, - }); - }); - } - } -}); diff --git a/jslib/lorisFetch.js b/jslib/lorisFetch.js new file mode 100644 index 0000000000..1aba03792c --- /dev/null +++ b/jslib/lorisFetch.js @@ -0,0 +1,81 @@ +/** + * Display the login modal when a request returns a 401 response. + */ +function handleUnauthorized() { + if (typeof window === 'undefined') { + return; + } + if (!window.$ || !window.loris) { + return; + } + + const $ = window.$; + if (!$('#login-modal').length) { + return; + } + + if ($('#login-modal').hasClass('in')) { + $('#login-modal-error').show(); + return; + } + + $('#login-modal').modal('show'); + $('#modal-login') + .off('click.lorisFetch') + .on('click.lorisFetch', function(e) { + e.preventDefault(); + let data = { + username: $('#modal-username').val(), + password: $('#modal-password').val(), + login: 'Login', + }; + + fetch(window.loris.BaseURL, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', + }, + body: new URLSearchParams(data), + credentials: 'same-origin', + }) + .then((response) => { + if (!response.ok) { + throw new Error('request_failed'); + } + $('#login-modal-error').hide(); + $('#login-modal').modal('hide'); + }) + .catch(() => { + $('#login-modal-error').show(); + }); + }); +} + +/** + * Wrapper around fetch that keeps credentials and handles 401s. + * + * @param {*} input + * @param {object=} init + * @return {Promise} + */ +function lorisFetch(input, init) { + const options = Object.assign( + { + credentials: 'same-origin', + }, + init || {} + ); + + return fetch(input, options).then((response) => { + if (response.status === 401) { + handleUnauthorized(); + } + return response; + }); +} + +if (typeof window !== 'undefined') { + window.lorisFetch = lorisFetch; +} + +export default lorisFetch; diff --git a/modules/candidate_parameters/jsx/CandidateInfo.js b/modules/candidate_parameters/jsx/CandidateInfo.js index 72e2ad2cc2..ab75ab4865 100644 --- a/modules/candidate_parameters/jsx/CandidateInfo.js +++ b/modules/candidate_parameters/jsx/CandidateInfo.js @@ -11,6 +11,8 @@ import { ButtonElement, TextareaElement, } from 'jsx/Form'; +import CandidateParametersClient from './CandidateParametersClient'; +import lorisFetch from 'jslib/lorisFetch'; /** * Candiate info component @@ -35,6 +37,7 @@ class CandidateInfo extends Component { isLoaded: false, loadedData: 0, }; + this.client = new CandidateParametersClient(); this.setFormData = this.setFormData.bind(this); this.onSubmit = this.onSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this); @@ -46,34 +49,28 @@ class CandidateInfo extends Component { */ componentDidMount() { const {t} = this.props; - let that = this; - $.ajax( - this.props.dataURL, - { - dataType: 'json', - success: function(data) { - let formData = { - flaggedCaveatemptor: data.flagged_caveatemptor, - flaggedOther: data.flagged_other, - flaggedReason: data.flagged_reason, - }; + this.client.getData(this.props.candID, this.props.tabName) + .then((data) => { + let formData = { + flaggedCaveatemptor: data.flagged_caveatemptor, + flaggedOther: data.flagged_other, + flaggedReason: data.flagged_reason, + }; - // Add parameter values to formData - Object.assign(formData, data.parameter_values); + // Add parameter values to formData + Object.assign(formData, data.parameter_values); - that.setState({ - Data: data, - isLoaded: true, - formData: formData, - }); - }, - error: function(data, errorCode, errorMsg) { - that.setState({ - error: t('An error occured while loading the page.', {ns: 'loris'}), - }); - }, - } - ); + this.setState({ + Data: data, + isLoaded: true, + formData: formData, + }); + }) + .catch(() => { + this.setState({ + error: t('An error occured while loading the page.', {ns: 'loris'}), + }); + }); } /** @@ -334,37 +331,43 @@ class CandidateInfo extends Component { formData.append('tab', this.props.tabName); formData.append('candID', this.state.Data.candID); - $.ajax( - { - type: 'POST', - url: self.props.action, - data: formData, - cache: false, - contentType: false, - processData: false, - success: function(data) { + lorisFetch(self.props.action, { + method: 'POST', + body: formData, + }) + .then(async (response) => { + if (!response.ok) { + let errorMessage = ''; + let text = await response.text(); + if (text) { + try { + errorMessage = JSON.parse(text).message || ''; + } catch (err) { + errorMessage = ''; + } + } + let error = new Error('request_failed'); + error.lorisMessage = errorMessage; + throw error; + } + self.setState( + { + updateResult: 'success', + } + ); + self.showAlertMessage(); + }) + .catch((err) => { + if (err.lorisMessage !== undefined && err.lorisMessage !== '') { self.setState( { - updateResult: 'success', + updateResult: 'error', + errorMessage: err.lorisMessage, } ); self.showAlertMessage(); - }, - error: function(err) { - if (err.responseText !== '') { - let errorMessage = JSON.parse(err.responseText).message; - self.setState( - { - updateResult: 'error', - errorMessage: errorMessage, - } - ); - self.showAlertMessage(); - } - }, - - } - ); + } + }); } /** @@ -390,8 +393,8 @@ class CandidateInfo extends Component { } } CandidateInfo.propTypes = { - dataURL: PropTypes.string, - tabName: PropTypes.string, + candID: PropTypes.string.isRequired, + tabName: PropTypes.string.isRequired, action: PropTypes.string, t: PropTypes.string.isRequired, }; diff --git a/modules/candidate_parameters/jsx/CandidateParameters.js b/modules/candidate_parameters/jsx/CandidateParameters.js index 425d98a59b..9a6541ea75 100644 --- a/modules/candidate_parameters/jsx/CandidateParameters.js +++ b/modules/candidate_parameters/jsx/CandidateParameters.js @@ -48,6 +48,7 @@ class CandidateParameters extends Component { @@ -161,4 +162,3 @@ window.addEventListener('load', () => { ); }); - diff --git a/modules/candidate_parameters/jsx/CandidateParametersClient.js b/modules/candidate_parameters/jsx/CandidateParametersClient.js new file mode 100644 index 0000000000..7a61dbc239 --- /dev/null +++ b/modules/candidate_parameters/jsx/CandidateParametersClient.js @@ -0,0 +1,41 @@ +import lorisFetch from 'jslib/lorisFetch'; + +/** + * Candidate parameters JSON client. + */ +class CandidateParametersClient { + /** + * @constructor + */ + constructor() { + this.baseURL = new URL( + `${loris.BaseURL}/candidate_parameters/ajax/getData.php`, + window.location.origin + ); + } + + /** + * Fetch candidate tab JSON data. + * + * @param {string} candID + * @param {string} tabName + * @return {Promise} + */ + getData(candID, tabName) { + const url = new URL(this.baseURL); + url.searchParams.set('candID', candID); + url.searchParams.set('data', tabName); + + return lorisFetch(url.toString(), { + method: 'GET', + headers: {'Accept': 'application/json'}, + }).then((response) => { + if (!response.ok) { + throw new Error('request_failed'); + } + return response.json(); + }); + } +} + +export default CandidateParametersClient; diff --git a/modules/candidate_parameters/jsx/ConsentStatus.js b/modules/candidate_parameters/jsx/ConsentStatus.js index 65236cd0cc..3f42c06593 100644 --- a/modules/candidate_parameters/jsx/ConsentStatus.js +++ b/modules/candidate_parameters/jsx/ConsentStatus.js @@ -6,6 +6,8 @@ import swal from 'sweetalert2'; import {VerticalTabs, TabPane} from 'Tabs'; import Loader from 'Loader'; +import CandidateParametersClient from './CandidateParametersClient'; +import lorisFetch from 'jslib/lorisFetch'; import { FormElement, StaticElement, @@ -35,6 +37,7 @@ class ConsentStatus extends Component { submitDisabled: false, showHistory: false, }; + this.client = new CandidateParametersClient(); /** * Bind component instance to custom methods @@ -55,14 +58,12 @@ class ConsentStatus extends Component { } /** - * Retrieve data from the provided URL and save it in state + * Retrieve data and save it in state. */ fetchData() { const {t} = this.props; - $.ajax(this.props.dataURL, { - method: 'GET', - dataType: 'json', - success: (data) => { + this.client.getData(this.props.candID, this.props.tabName) + .then((data) => { let formData = {}; let consents = data.consents; for (let cStatus in consents) { @@ -98,14 +99,13 @@ class ConsentStatus extends Component { formData: formData, isLoaded: true, }); - }, - error: (error) => { + }) + .catch((error) => { console.error(error); this.setState({ error: true, }); - }, - }); + }); } /** @@ -265,14 +265,20 @@ class ConsentStatus extends Component { // Disable submit button to prevent form resubmission this.setState({submitDisabled: true}); - $.ajax({ - type: 'POST', - url: this.props.action, - data: formData, - cache: false, - contentType: false, - processData: false, - success: (data) => { + lorisFetch(this.props.action, { + method: 'POST', + body: formData, + }) + .then(async (response) => { + if (!response.ok) { + let errorMessage = await response.text(); + if (!errorMessage) { + errorMessage = t('Failed to update!', {ns: 'candidate_parameters'}); + } + let error = new Error('request_failed'); + error.lorisMessage = errorMessage; + throw error; + } swal.fire({ title: t('Success!', {ns: 'loris'}), text: t('Update successful.', {ns: 'candidate_parameters'}), @@ -287,12 +293,12 @@ class ConsentStatus extends Component { } ); this.fetchData(); - }, - error: (error) => { + }) + .catch((error) => { console.error(error); // Enable submit button for form resubmission this.setState({submitDisabled: false}); - let errorMessage = error.responseText || + let errorMessage = error.lorisMessage || t('Failed to update!', {ns: 'candidate_parameters'}); swal.fire({ title: t('Error!', {ns: 'loris'}), @@ -300,8 +306,7 @@ class ConsentStatus extends Component { type: 'error', confirmButtonText: t('OK', {ns: 'loris'}), }); - }, - }); + }); } /** @@ -574,9 +579,9 @@ class ConsentStatus extends Component { } ConsentStatus.propTypes = { - dataURL: PropTypes.string.isRequired, + candID: PropTypes.string.isRequired, action: PropTypes.string.isRequired, - tabName: PropTypes.string, + tabName: PropTypes.string.isRequired, t: PropTypes.string.isRequired, }; diff --git a/modules/candidate_parameters/jsx/FamilyInfo.js b/modules/candidate_parameters/jsx/FamilyInfo.js index 75048dfdcf..56dc082479 100644 --- a/modules/candidate_parameters/jsx/FamilyInfo.js +++ b/modules/candidate_parameters/jsx/FamilyInfo.js @@ -8,6 +8,8 @@ import { ButtonElement, SelectElement, } from 'jsx/Form'; +import CandidateParametersClient from './CandidateParametersClient'; +import lorisFetch from 'jslib/lorisFetch'; /** * Family info component @@ -28,6 +30,7 @@ class FamilyInfo extends Component { isLoaded: false, loadedData: 0, }; + this.client = new CandidateParametersClient(); this.setFormData = this.setFormData.bind(this); this.onSubmit = this.onSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this); @@ -41,37 +44,21 @@ class FamilyInfo extends Component { */ fetchData() { const {t} = this.props; - $.ajax( - this.props.dataURL, - { - dataType: 'json', - xhr: function() { - let xhr = new window.XMLHttpRequest(); - xhr.addEventListener( - 'progress', - function(evt) { - this.setState({ - loadedData: evt.loaded, - }); - }.bind(this)); - return xhr; - }.bind(this), - success: function(data) { - this.setState({ - Data: data, - isLoaded: true, - familyMembers: data.existingFamilyMembers, - }); - }.bind(this), - error: function(data, errorCode, errorMsg) { - this.setState({ - error: t('An error occurred when loading the form!', - {ns: 'candidate_parameters'} - ), - }); - }.bind(this), - } - ); + this.client.getData(this.props.candID, this.props.tabName) + .then((data) => { + this.setState({ + Data: data, + isLoaded: true, + familyMembers: data.existingFamilyMembers, + }); + }) + .catch(() => { + this.setState({ + error: t('An error occurred when loading the form!', + {ns: 'candidate_parameters'} + ), + }); + }); } /** @@ -281,14 +268,25 @@ class FamilyInfo extends Component { familyMembers: familyMembers, }); - $.ajax({ - type: 'POST', - url: this.props.action, - data: formData, - cache: false, - contentType: false, - processData: false, - success: function(data) { + lorisFetch(this.props.action, { + method: 'POST', + body: formData, + }) + .then(async (response) => { + if (!response.ok) { + let errorMessage = ''; + let text = await response.text(); + if (text) { + try { + errorMessage = JSON.parse(text).message || ''; + } catch (err) { + errorMessage = ''; + } + } + let error = new Error('request_failed'); + error.lorisMessage = errorMessage; + throw error; + } self.setState({ updateResult: 'success', formData: {}, @@ -304,9 +302,9 @@ class FamilyInfo extends Component { }); // rerender components self.forceUpdate(); - }, - error: function(err) { - let errorMessage = JSON.parse(err.responseText).message; + }) + .catch((err) => { + let errorMessage = err.lorisMessage || ''; self.setState( { updateResult: 'error', @@ -314,9 +312,7 @@ class FamilyInfo extends Component { } ); self.showAlertMessage(); - }, - - }); + }); } /** @@ -335,10 +331,10 @@ class FamilyInfo extends Component { self.setState( { updateResult: null, - } ); - }); + } + ); } /** @@ -373,38 +369,46 @@ class FamilyInfo extends Component { formData.append('candID', this.state.Data.candID); formData.append('familyDCCID', candID); - $.ajax({ - type: 'POST', - url: this.props.action, - data: formData, - cache: false, - contentType: false, - processData: false, - success: function(data) { + lorisFetch(this.props.action, { + method: 'POST', + body: formData, + }) + .then(async (response) => { + if (!response.ok) { + let errorMessage = ''; + let text = await response.text(); + if (text) { + try { + errorMessage = JSON.parse(text).message || ''; + } catch (err) { + errorMessage = ''; + } + } + let error = new Error('request_failed'); + error.lorisMessage = errorMessage; + throw error; + } self.setState( { updateResult: 'success', }); self.showAlertMessage(); - }, - error: function(err) { - if (err.responseText !== '') { - let errorMessage = JSON.parse(err.responseText).message; + }) + .catch((err) => { + if (err.lorisMessage !== undefined && err.lorisMessage !== '') { self.setState( { updateResult: 'error', - errorMessage: errorMessage, + errorMessage: err.lorisMessage, }); self.showAlertMessage(); } - }, - }); + }); } } FamilyInfo.propTypes = { - dataURL: PropTypes.string, - tabName: PropTypes.string, - candID: PropTypes.string, + tabName: PropTypes.string.isRequired, + candID: PropTypes.string.isRequired, action: PropTypes.string, t: PropTypes.string.isRequired, }; diff --git a/modules/candidate_parameters/jsx/ParticipantStatus.js b/modules/candidate_parameters/jsx/ParticipantStatus.js index 99b7f272be..58fde0c116 100644 --- a/modules/candidate_parameters/jsx/ParticipantStatus.js +++ b/modules/candidate_parameters/jsx/ParticipantStatus.js @@ -10,6 +10,8 @@ import { TextareaElement, ButtonElement, } from 'jsx/Form'; +import CandidateParametersClient from './CandidateParametersClient'; +import lorisFetch from 'jslib/lorisFetch'; /** * Participant status component @@ -29,6 +31,7 @@ class ParticipantStatus extends Component { isLoaded: false, loadedData: 0, }; + this.client = new CandidateParametersClient(); this.fetchData = this.fetchData.bind(this); this.setFormData = this.setFormData.bind(this); this.onSubmit = this.onSubmit.bind(this); @@ -48,50 +51,30 @@ class ParticipantStatus extends Component { */ fetchData() { const {t} = this.props; - let that = this; - $.ajax( - this.props.dataURL, - { - dataType: 'json', - xhr: function() { - let xhr = new window.XMLHttpRequest(); - xhr.addEventListener( - 'progress', - function(evt) { - that.setState( - { - loadedData: evt.loaded, - } - ); - } - ); - return xhr; - }, - success: function(data) { - let formData = {}; - formData.participantStatus = data.participantStatus; - formData.participantSuboptions = data.participantSuboptions; - formData.reasonSpecify = data.reasonSpecify; + this.client.getData(this.props.candID, this.props.tabName) + .then((data) => { + let formData = {}; + formData.participantStatus = data.participantStatus; + formData.participantSuboptions = data.participantSuboptions; + formData.reasonSpecify = data.reasonSpecify; - that.setState( - { - Data: data, - formData: formData, - isLoaded: true, - } - ); - }, - error: function(data, errorCode, errorMsg) { - that.setState( - { - error: t('An error occured while loading the page.', - {ns: 'loris'} - ), - } - ); - }, - } - ); + this.setState( + { + Data: data, + formData: formData, + isLoaded: true, + } + ); + }) + .catch(() => { + this.setState( + { + error: t('An error occured while loading the page.', + {ns: 'loris'} + ), + } + ); + }); } /** @@ -297,37 +280,44 @@ class ParticipantStatus extends Component { formData.append('tab', this.props.tabName); formData.append('candID', this.state.Data.candID); - $.ajax( - { - type: 'POST', - url: self.props.action, - data: formData, - cache: false, - contentType: false, - processData: false, - success: function(data) { + lorisFetch(self.props.action, { + method: 'POST', + body: formData, + }) + .then(async (response) => { + if (!response.ok) { + let errorMessage = ''; + let text = await response.text(); + if (text) { + try { + errorMessage = JSON.parse(text).message || ''; + } catch (err) { + errorMessage = ''; + } + } + let error = new Error('request_failed'); + error.lorisMessage = errorMessage; + throw error; + } + self.setState( + { + updateResult: 'success', + } + ); + self.showAlertMessage(); + self.fetchData(); + }) + .catch((err) => { + if (err.lorisMessage !== undefined && err.lorisMessage !== '') { self.setState( { - updateResult: 'success', + updateResult: 'error', + errorMessage: err.lorisMessage, } ); self.showAlertMessage(); - self.fetchData(); - }, - error: function(err) { - if (err.responseText !== '') { - let errorMessage = JSON.parse(err.responseText).message; - self.setState( - { - updateResult: 'error', - errorMessage: errorMessage, - } - ); - self.showAlertMessage(); - } - }, - } - ); + } + }); } /** @@ -353,8 +343,8 @@ class ParticipantStatus extends Component { } } ParticipantStatus.propTypes = { - dataURL: PropTypes.string, - tabName: PropTypes.string, + candID: PropTypes.string.isRequired, + tabName: PropTypes.string.isRequired, action: PropTypes.string, t: PropTypes.string.isRequired, }; diff --git a/modules/candidate_parameters/jsx/ProbandInfo.js b/modules/candidate_parameters/jsx/ProbandInfo.js index 71484d9c7b..e6fc22a43e 100644 --- a/modules/candidate_parameters/jsx/ProbandInfo.js +++ b/modules/candidate_parameters/jsx/ProbandInfo.js @@ -12,6 +12,8 @@ import { DateElement, TextareaElement, } from 'jsx/Form'; +import CandidateParametersClient from './CandidateParametersClient'; +import lorisFetch from 'jslib/lorisFetch'; /** * Proband Info Component. @@ -35,6 +37,7 @@ class ProbandInfo extends Component { isLoaded: false, loadedData: 0, }; + this.client = new CandidateParametersClient(); /** * Bind component instance to custom methods @@ -57,10 +60,8 @@ class ProbandInfo extends Component { */ fetchData() { const {t} = this.props; - $.ajax(this.props.dataURL, { - method: 'GET', - dataType: 'json', - success: (data) => { + this.client.getData(this.props.candID, this.props.tabName) + .then((data) => { const formData = { ProbandSex: data.ProbandSex, ProbandDoB: data.ProbandDoB, @@ -76,15 +77,14 @@ class ProbandInfo extends Component { isLoaded: true, sexOptions: data.sexOptions, }); - }, - error: (error) => { + }) + .catch(() => { this.setState({ error: t('An error occurred when loading the form!', {ns: 'candidate_parameters'} ), }); - }, - }); + }); } /** @@ -158,31 +158,40 @@ class ProbandInfo extends Component { formData.append('tab', this.props.tabName); formData.append('candID', this.state.Data.candID); - $.ajax({ - type: 'POST', - url: this.props.action, - data: formData, - cache: false, - contentType: false, - processData: false, - success: (data) => { + lorisFetch(this.props.action, { + method: 'POST', + body: formData, + }) + .then(async (response) => { + if (!response.ok) { + let errorMessage = ''; + let text = await response.text(); + if (text) { + try { + errorMessage = JSON.parse(text).message || ''; + } catch (err) { + errorMessage = ''; + } + } + let error = new Error('request_failed'); + error.lorisMessage = errorMessage; + throw error; + } this.setState({ updateResult: 'success', }); this.showAlertMessage(); this.fetchData(); - }, - error: (error) => { - if (error.responseText !== '') { - let errorMessage = JSON.parse(error.responseText).message; + }) + .catch((error) => { + if (error.lorisMessage !== undefined && error.lorisMessage !== '') { this.setState({ updateResult: 'error', - errorMessage: errorMessage, + errorMessage: error.lorisMessage, }); this.showAlertMessage(); } - }, - }); + }); } /** @@ -368,9 +377,9 @@ class ProbandInfo extends Component { } ProbandInfo.propTypes = { - dataURL: PropTypes.string.isRequired, + candID: PropTypes.string.isRequired, action: PropTypes.string.isRequired, - tabName: PropTypes.string, + tabName: PropTypes.string.isRequired, t: PropTypes.string.isRequired, };