diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js index 8e11645ca22f57e71bf20c836ac45bf76423c39a..4b58f3211bb843d7dfa1c8c05673d260c70ba9df 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/create.js @@ -1,13 +1,14 @@ import React, {Component} from 'react'; import { Redirect } from 'react-router-dom'; -import {InputText} from 'primereact/inputtext'; -import {Calendar} from 'primereact/calendar'; -import {InputTextarea} from 'primereact/inputtextarea'; -import {Dropdown} from 'primereact/dropdown'; -import {Button} from 'primereact/button'; -import {Dialog} from 'primereact/components/dialog/Dialog'; -import {Growl} from 'primereact/components/growl/Growl'; -import {ResourceInputList} from './ResourceInputList'; +import { InputText } from 'primereact/inputtext'; +import { Calendar } from 'primereact/calendar'; +import { InputTextarea } from 'primereact/inputtextarea'; +import { Dropdown } from 'primereact/dropdown'; +import { Button } from 'primereact/button'; +import { Dialog } from 'primereact/components/dialog/Dialog'; +import { Growl } from 'primereact/components/growl/Growl'; +import { ResourceInputList } from './ResourceInputList'; +import { CustomDialog } from '../../layout/components/CustomDialog'; import moment from 'moment' import _ from 'lodash'; @@ -24,9 +25,13 @@ export class CycleCreate extends Component { constructor(props) { super(props); this.state = { + showDialog: false, + isDirty: false, isLoading: true, dialog: { header: '', detail: ''}, cycle: { + name: '', + description: '', projects: [], quota: [], start: "", @@ -68,6 +73,8 @@ export class CycleCreate extends Component { this.saveCycle = this.saveCycle.bind(this); this.cancelCreate = this.cancelCreate.bind(this); this.reset = this.reset.bind(this); + this.checkIsDirty = this.checkIsDirty.bind(this); + this.close = this.close.bind(this); } componentDidMount() { @@ -111,11 +118,15 @@ export class CycleCreate extends Component { */ addNewResource(){ if (this.state.newResource) { - let resourceList = this.state.resourceList; + let resourceList = _.cloneDeep(this.state.resourceList); const newResource = _.remove(resourceList, {'name': this.state.newResource}); let resources = this.state.resources; resources.push(newResource[0]); - this.setState({resources: resources, resourceList: resourceList, newResource: null}); + if ( !this.state.isDirty && !_.isEqual(this.state.resourceList, resourceList) ) { + this.setState({resources: resources, resourceList: resourceList, newResource: null, isDirty: true}); + } else { + this.setState({resources: resources, resourceList: resourceList, newResource: null}); + } } } @@ -126,12 +137,16 @@ export class CycleCreate extends Component { removeResource(name) { let resources = this.state.resources; let resourceList = this.state.resourceList; - let cycleQuota = this.state.cycleQuota; + let cycleQuota = _.cloneDeep(this.state.cycleQuota); const removedResource = _.remove(resources, (resource) => { return resource.name === name }); resourceList.push(removedResource[0]); resourceList = _.sortBy(resourceList, 'name'); delete cycleQuota[name]; - this.setState({resourceList: resourceList, resources: resources, cycleQuota: cycleQuota}); + if ( !this.state.isDirty && !_.isEqual(this.state.cycleQuota, cycleQuota) ) { + this.setState({resourceList: resourceList, resources: resources, cycleQuota: cycleQuota, isDirty: true}); + } else { + this.setState({resourceList: resourceList, resources: resources, cycleQuota: cycleQuota}); + } } /** @@ -140,7 +155,7 @@ export class CycleCreate extends Component { * @param {any} value */ setCycleParams(key, value, type) { - let cycle = this.state.cycle; + let cycle = _.cloneDeep(this.state.cycle); switch(type) { case 'NUMBER': { cycle[key] = value?parseInt(value):0; @@ -150,9 +165,12 @@ export class CycleCreate extends Component { cycle[key] = value; break; } - } - this.setState({cycle: cycle, validForm: this.validateForm(key)}); + if ( !this.state.isDirty && !_.isEqual(this.state.cycle, cycle) ) { + this.setState({cycle: cycle, validForm: this.validateForm(key), isDirty: true}); + } else { + this.setState({cycle: cycle, validForm: this.validateForm(key)}); + } } /** @@ -161,23 +179,28 @@ export class CycleCreate extends Component { * @param {InputEvent} event */ setCycleQuotaParams(key, event) { - let cycleQuota = this.state.cycleQuota; + let cycleQuota = _.cloneDeep(this.state.cycleQuota); if (event.target.value) { let resource = _.find(this.state.resources, {'name': key}); let newValue = 0; if (this.resourceUnitMap[resource.quantity_value] && event.target.value.toString().indexOf(this.resourceUnitMap[resource.quantity_value].display)>=0) { - newValue = event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,''); + newValue = _.trim(event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,'','')); } else { - newValue = event.target.value; + newValue = _.trim(event.target.value); } - cycleQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:newValue; + cycleQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:Number(newValue); } else { let cycleQuota = this.state.cycleQuota; cycleQuota[key] = 0; } - this.setState({cycleQuota: cycleQuota}); + + if ( !this.state.isDirty && !_.isEqual(this.state.cycleQuota, cycleQuota) ) { + this.setState({cycleQuota: cycleQuota, isDirty: true}); + } else { + this.setState({cycleQuota: cycleQuota}); + } } /** @@ -246,7 +269,7 @@ export class CycleCreate extends Component { let stoptime = _.replace(this.state.cycle['stop'],'00:00:00', '23:59:59'); cycle['start'] = moment(cycle['start']).format("YYYY-MM-DDTHH:mm:ss"); cycle['stop'] = moment(stoptime).format("YYYY-MM-DDTHH:mm:ss"); - this.setState({cycle: cycle}); + this.setState({cycle: cycle, isDirty: false}); for (const resource in this.state.cycleQuota) { let resourceType = _.find(this.state.resources, {'name': resource}); if(resourceType){ @@ -276,6 +299,21 @@ export class CycleCreate extends Component { } } + /** + * warn before cancel the page if any changes detected + */ + checkIsDirty() { + if( this.state.isDirty ){ + this.setState({showDialog: true}); + } else { + this.cancelCreate(); + } + } + + close() { + this.setState({showDialog: false}); + } + /** * Function to cancel form creation and navigate to other page/component */ @@ -341,7 +379,7 @@ export class CycleCreate extends Component { <PageHeader location={this.props.location} title={'Cycle - Add'} actions={[{icon:'fa-window-close', title:'Click to Close Add Cycle', - props:{pathname: '/cycle' }}]}/> + type: 'button', actOn: 'click', props:{ callback: this.checkIsDirty }}]}/> { this.state.isLoading ? <AppLoader /> : <> <div> @@ -382,7 +420,7 @@ export class CycleCreate extends Component { <label htmlFor="cycleName" className="col-lg-2 col-md-2 col-sm-12">Start Date <span style={{color:'red'}}>*</span></label> <div className="col-lg-3 col-md-3 col-sm-12"> <Calendar - d dateFormat="dd-M-yy" + d dateFormat="dd-M-yy" value= {this.state.cycle.start} onChange= {e => this.setCycleParams('start',e.value)} onBlur= {e => this.setCycleParams('start',e.value)} @@ -435,8 +473,8 @@ export class CycleCreate extends Component { </div> <div className="p-field p-grid resource-input-grid"> <ResourceInputList list={this.state.resources} unitMap={this.resourceUnitMap} - cycleQuota={this.state.cycleQuota} callback={this.setCycleQuotaParams} - removeInputCallback={this.removeResource} /> + cycleQuota={this.state.cycleQuota} callback={this.setCycleQuotaParams} + removeInputCallback={this.removeResource} /> </div> </div> } @@ -447,7 +485,7 @@ export class CycleCreate extends Component { <Button label="Save" className="p-button-primary" id="save-btn" data-testid="save-btn" icon="pi pi-check" onClick={this.saveCycle} disabled={!this.state.validForm} /> </div> <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelCreate} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.checkIsDirty} /> </div> </div> </> @@ -471,6 +509,11 @@ export class CycleCreate extends Component { </div> </div> </Dialog> + + <CustomDialog type="confirmation" visible={this.state.showDialog} width="40vw" + header={'Edit Cycle'} message={'Leave this page? Changes you made may not be saved.'} + content={''} onClose={this.close} onCancel={this.close} onSubmit={this.cancelCreate}> + </CustomDialog> </div> </React.Fragment> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/edit.js index d04ac553d56581a384a458044384e5b64a349845..c5da35eb572dbe404944fac8e37a6d0c490b209c 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Cycle/edit.js @@ -3,26 +3,30 @@ import { Redirect } from 'react-router-dom'; import _ from 'lodash'; import moment from 'moment' -import {InputText} from 'primereact/inputtext'; -import {Calendar} from 'primereact/calendar'; -import {InputTextarea} from 'primereact/inputtextarea'; -import {Dropdown} from 'primereact/dropdown'; +import { InputText } from 'primereact/inputtext'; +import { Calendar } from 'primereact/calendar'; +import { InputTextarea } from 'primereact/inputtextarea'; +import { Dropdown } from 'primereact/dropdown'; import { Button } from 'primereact/button'; -import {Dialog} from 'primereact/components/dialog/Dialog'; -import {Growl} from 'primereact/components/growl/Growl'; - -import {ResourceInputList} from './ResourceInputList'; +import { Dialog } from 'primereact/components/dialog/Dialog'; +import { Growl } from 'primereact/components/growl/Growl'; +import { ResourceInputList } from './ResourceInputList'; +import { CustomDialog } from '../../layout/components/CustomDialog'; import AppLoader from '../../layout/components/AppLoader'; import PageHeader from '../../layout/components/PageHeader'; import CycleService from '../../services/cycle.service'; import UnitConverter from '../../utils/unit.converter'; import UIConstants from '../../utils/ui.constants'; + + export class CycleEdit extends Component { constructor(props) { super(props); this.state = { + showDialog: false, + isDirty: false, isLoading: true, dialog: { header: '', detail: ''}, cycle: { @@ -60,6 +64,8 @@ export class CycleEdit extends Component { this.resourceUnitMap = UnitConverter.resourceUnitMap; this.tooltipOptions = UIConstants.tooltipOptions; + this.checkIsDirty = this.checkIsDirty.bind(this); + this.close = this.close.bind(this); this.getCycleDetails = this.getCycleDetails.bind(this); this.cycleOptionTemplate = this.cycleOptionTemplate.bind(this); this.setCycleQuotaDefaults = this.setCycleQuotaDefaults.bind(this); @@ -99,15 +105,18 @@ export class CycleEdit extends Component { let resourceList = this.state.resourceList; let cycleQuota = {}; if (cycle) { - // Get cycle_quota for the cycle and asssign to the component variable - for (const id of cycle.quota_ids) { - let quota = await CycleService.getCycleQuota(id); - let resource = _.find(resourceList, ['name', quota.resource_type_id]); - quota.resource = resource; - this.cycleQuota.push(quota); - const conversionFactor = this.resourceUnitMap[resource.quantity_value]?this.resourceUnitMap[resource.quantity_value].conversionFactor:1; - cycleQuota[quota.resource_type_id] = quota.value / conversionFactor; - }; + if(cycle.quota_ids){ + // Get cycle_quota for the cycle and asssign to the component variable + for (const id of cycle.quota_ids) { + let quota = await CycleService.getCycleQuota(id); + let resource = _.find(resourceList, ['name', quota.resource_type_id]); + quota.resource = resource; + this.cycleQuota.push(quota); + const conversionFactor = this.resourceUnitMap[resource.quantity_value]?this.resourceUnitMap[resource.quantity_value].conversionFactor:1; + cycleQuota[quota.resource_type_id] = quota.value / conversionFactor; + }; + } + // Remove the already assigned resources from the resoureList const resources = _.remove(resourceList, (resource) => { return _.find(this.cycleQuota, {'resource_type_id': resource.name})!=null }); this.setState({cycle: cycle, resourceList: resourceList, resources: resources, @@ -149,11 +158,16 @@ export class CycleEdit extends Component { */ addNewResource(){ if (this.state.newResource) { - let resourceList = this.state.resourceList; + let resourceList = _.cloneDeep(this.state.resourceList); const newResource = _.remove(resourceList, {'name': this.state.newResource}); let resources = this.state.resources?this.state.resources:[]; resources.push(newResource[0]); - this.setState({resources: resources, resourceList: resourceList, newResource: null}); + if ( !this.state.isDirty && !_.isEqual(this.state.resourceList, resourceList)) { + this.setState({resources: resources, resourceList: resourceList, newResource: null, isDirty: true}); + } else { + this.setState({resources: resources, resourceList: resourceList, newResource: null}); + } + } } @@ -164,11 +178,16 @@ export class CycleEdit extends Component { removeResource(name) { let resources = this.state.resources; let resourceList = this.state.resourceList; - let cycleQuota = this.state.cycleQuota; + let cycleQuota = _.cloneDeep(this.state.cycleQuota); const removedResource = _.remove(resources, (resource) => { return resource.name === name }); resourceList.push(removedResource[0]); delete cycleQuota[name]; - this.setState({resourceList: resourceList, resources: resources, cycleQuota: cycleQuota}); + if ( !this.state.isDirty && !_.isEqual(this.state.cycleQuota, cycleQuota)) { + this.setState({resourceList: resourceList, resources: resources, cycleQuota: cycleQuota, isDirty: true}); + } else { + this.setState({resourceList: resourceList, resources: resources, cycleQuota: cycleQuota}); + } + } /** @@ -177,7 +196,7 @@ export class CycleEdit extends Component { * @param {any} value */ setCycleParams(key, value, type) { - let cycle = this.state.cycle; + let cycle = _.cloneDeep(this.state.cycle); switch(type) { case 'NUMBER': { cycle[key] = value?parseInt(value):0; @@ -188,7 +207,12 @@ export class CycleEdit extends Component { break; } } - this.setState({cycle: cycle, validForm: this.validateForm(key)}); + if ( !this.state.isDirty && !_.isEqual(this.state.cycle, cycle)) { + this.setState({cycle: cycle, validForm: this.validateForm(key), isDirty: true}); + } else { + this.setState({cycle: cycle, validForm: this.validateForm(key)}); + } + } /** @@ -197,22 +221,26 @@ export class CycleEdit extends Component { * @param {InputEvent} event */ setCycleQuotaParams(key, event) { - let cycleQuota = this.state.cycleQuota; + let cycleQuota = _.cloneDeep(this.state.cycleQuota); if (event.target.value) { let resource = _.find(this.state.resources, {'name': key}); let newValue = 0; if (this.resourceUnitMap[resource.quantity_value] && event.target.value.toString().indexOf(this.resourceUnitMap[resource.quantity_value].display)>=0) { - newValue = event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,''); + newValue = _.trim(event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,'')); } else { - newValue = event.target.value; + newValue = _.trim(event.target.value); } - cycleQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:newValue; + cycleQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:Number(newValue); } else { let cycleQuota = this.state.cycleQuota; cycleQuota[key] = 0; } - this.setState({cycleQuota: cycleQuota}); + if ( !this.state.isDirty && !_.isEqual(this.state.cycleQuota, cycleQuota)) { + this.setState({cycleQuota: cycleQuota, isDirty: true}); + } else { + this.setState({cycleQuota: cycleQuota}); + } } /** @@ -281,7 +309,7 @@ export class CycleEdit extends Component { let stoptime = _.replace(this.state.cycle['stop'],'00:00:00', '23:59:59'); cycle['start'] = moment(this.state.cycle['start']).format("YYYY-MM-DDTHH:mm:ss"); cycle['stop'] = moment(stoptime).format("YYYY-MM-DDTHH:mm:ss"); - this.setState({cycle: cycle}); + this.setState({cycle: cycle, isDirty: false}); CycleService.updateCycle(this.props.match.params.id, this.state.cycle) .then(async (cycle) => { if (cycle && this.state.cycle.updated_at !== cycle.updated_at) { @@ -348,6 +376,20 @@ export class CycleEdit extends Component { this.setState({dialogVisible: true, dialog: dialog}); } + /** + * warn before cancel the page if any changes detected + */ + checkIsDirty() { + if( this.state.isDirty ){ + this.setState({showDialog: true}); + } else { + this.cancelEdit(); + } + } + + close() { + this.setState({showDialog: false}); + } /** * Cancel edit and redirect to Cycle View page */ @@ -374,8 +416,7 @@ export class CycleEdit extends Component { </div> </div> */} <PageHeader location={this.props.location} title={'Cycle - Edit'} actions={[{icon:'fa-window-close', - link: this.props.history.goBack,title:'Click to Close Cycle-Edit', - props:{ pathname: `/cycle/view/${this.state.cycle.name}`}}]}/> + title:'Click to Close Cycle-Edit', type: 'button', actOn: 'click', props:{ callback: this.checkIsDirty }}]}/> { this.state.isLoading ? <AppLoader/> : <> @@ -478,7 +519,7 @@ export class CycleEdit extends Component { <Button label="Save" className="p-button-primary" id="save-btn" data-testid="save-btn" icon="pi pi-check" onClick={this.saveCycle} disabled={!this.state.validForm} /> </div> <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelEdit} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.checkIsDirty} /> </div> </div> @@ -502,6 +543,12 @@ export class CycleEdit extends Component { </div> </div> </Dialog> + + <CustomDialog type="confirmation" visible={this.state.showDialog} width="40vw" + header={'Edit Cycle'} message={'Leave this page? Changes you made may not be saved.'} + content={''} onClose={this.close} onCancel={this.close} onSubmit={this.cancelEdit}> + </CustomDialog> + </div> </React.Fragment> ); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js index 734294c72903e650f2bca44471ff75a0ff02ed06..500e4f98f705ef78061cc6fab54cc39d1dce87e5 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/create.js @@ -1,17 +1,17 @@ -import React, {Component} from 'react'; +import React, { Component } from 'react'; import { Redirect } from 'react-router-dom'; import _ from 'lodash'; -import {InputText} from 'primereact/inputtext'; -import {InputNumber} from 'primereact/inputnumber'; -import {InputTextarea} from 'primereact/inputtextarea'; -import {Checkbox} from 'primereact/checkbox'; -import {Dropdown} from 'primereact/dropdown'; -import {MultiSelect} from 'primereact/multiselect'; +import { InputText } from 'primereact/inputtext'; +import { InputNumber } from 'primereact/inputnumber'; +import { InputTextarea } from 'primereact/inputtextarea'; +import { Checkbox } from 'primereact/checkbox'; +import { Dropdown } from 'primereact/dropdown'; +import { MultiSelect } from 'primereact/multiselect'; import { Button } from 'primereact/button'; -import {Dialog} from 'primereact/components/dialog/Dialog'; -import {Growl} from 'primereact/components/growl/Growl'; - -import {ResourceInputList} from './ResourceInputList'; +import { Dialog } from 'primereact/components/dialog/Dialog'; +import { Growl } from 'primereact/components/growl/Growl'; +import { CustomDialog } from '../../layout/components/CustomDialog'; +import { ResourceInputList } from './ResourceInputList'; import AppLoader from '../../layout/components/AppLoader'; import PageHeader from '../../layout/components/PageHeader'; @@ -28,10 +28,15 @@ export class ProjectCreate extends Component { constructor(props) { super(props); this.state = { + showDialog: false, + isDirty: false, ltaStorage: [], isLoading: true, dialog: { header: '', detail: ''}, project: { + archive_subdirectory: '', + name: '', + description: '', trigger_priority: 1000, priority_rank: null, quota: [], // Mandatory Field in the back end, so an empty array is passed @@ -78,6 +83,8 @@ export class ProjectCreate extends Component { this.saveProject = this.saveProject.bind(this); this.cancelCreate = this.cancelCreate.bind(this); this.reset = this.reset.bind(this); + this.checkIsDirty = this.checkIsDirty.bind(this); + this.close = this.close.bind(this); } componentDidMount() { @@ -151,11 +158,15 @@ export class ProjectCreate extends Component { */ addNewResource(){ if (this.state.newResource) { - let resourceList = this.state.resourceList; + let resourceList = _.cloneDeep(this.state.resourceList); const newResource = _.remove(resourceList, {'name': this.state.newResource}); let resources = this.state.resources; resources.push(newResource[0]); - this.setState({resources: resources, resourceList: resourceList, newResource: null}); + if ( !this.state.isDirty && !_.isEqual(this.state.resourceList, resourceList) ) { + this.setState({resources: resources, resourceList: resourceList, newResource: null, isDirty: true}); + } else { + this.setState({resources: resources, resourceList: resourceList, newResource: null}); + } } } @@ -166,12 +177,17 @@ export class ProjectCreate extends Component { removeResource(name) { let resources = this.state.resources; let resourceList = this.state.resourceList; - let projectQuota = this.state.projectQuota; + let projectQuota = _.cloneDeep(this.state.projectQuota); const removedResource = _.remove(resources, (resource) => { return resource.name === name }); resourceList.push(removedResource[0]); resourceList = _.sortBy(resourceList, 'name'); delete projectQuota[name]; - this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota}); + if ( !this.state.isDirty && !_.isEqual(this.state.projectQuota, projectQuota) ) { + this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota, isDirty: true}); + } else { + this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota}); + } + } /** @@ -180,7 +196,7 @@ export class ProjectCreate extends Component { * @param {any} value */ setProjectParams(key, value, type) { - let project = this.state.project; + let project = _.cloneDeep(this.state.project); switch(type) { case 'NUMBER': { console.log("Parsing Number"); @@ -211,7 +227,11 @@ export class ProjectCreate extends Component { if (type==='PROJECT_NAME' & value!=="") { validForm = this.validateForm('archive_subdirectory'); } - this.setState({project: project, validForm: validForm}); + if ( !this.state.isDirty && !_.isEqual(this.state.project, project) ) { + this.setState({project: project, validForm: validForm, isDirty: true}); + } else { + this.setState({project: project, validForm: validForm}); + } } /** @@ -221,22 +241,26 @@ export class ProjectCreate extends Component { */ setProjectQuotaParams(key, event) { let projectQuota = this.state.projectQuota; + const previousValue = projectQuota[key]; if (event.target.value) { let resource = _.find(this.state.resources, {'name': key}); - let newValue = 0; if (this.resourceUnitMap[resource.quantity_value] && event.target.value.toString().indexOf(this.resourceUnitMap[resource.quantity_value].display)>=0) { - newValue = event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,''); + newValue = _.trim(event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,'')); } else { - newValue = event.target.value; + newValue = _.trim(event.target.value); } - projectQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:newValue; + projectQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:Number(newValue); } else { - let projectQuota = this.state.projectQuota; + // let projectQuota = this.state.projectQuota; projectQuota[key] = 0; } - this.setState({projectQuota: projectQuota}); + if ( !this.state.isDirty && !_.isEqual(previousValue, projectQuota[key]) ) { + this.setState({projectQuota: projectQuota, isDirty: true}); + } else { + this.setState({projectQuota: projectQuota}); + } } /** @@ -308,15 +332,30 @@ export class ProjectCreate extends Component { } else { dialog = {header: 'Success', detail: 'Project saved successfully with default Resource allocations. Do you want to view and edit them?'}; } - this.setState({project:project, dialogVisible: true, dialog: dialog}) + this.setState({project:project, dialogVisible: true, dialog: dialog, isDirty: false}); } else { this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to save Project'}); - this.setState({errors: project}); + this.setState({errors: project, isDirty: false}); } }); } } + /** + * warn before cancel the page if any changes detected + */ + checkIsDirty() { + if( this.state.isDirty ){ + this.setState({showDialog: true}); + } else { + this.cancelCreate(); + } + } + + close() { + this.setState({showDialog: false}); + } + /** * Function to cancel form creation and navigate to other page/component */ @@ -374,7 +413,8 @@ export class ProjectCreate extends Component { return ( <React.Fragment> <Growl ref={(el) => this.growl = el} /> - <PageHeader location={this.props.location} title={'Project - Add'} actions={[{icon:'fa-window-close',link:this.props.history.goBack, title:'Click to Close Project', props:{ pathname: '/project'}}]}/> + <PageHeader location={this.props.location} title={'Project - Add'} actions={[{icon:'fa-window-close', title:'Click to Close Project', + type: 'button', actOn: 'click', props:{ callback: this.checkIsDirty }}]}/> { this.state.isLoading ? <AppLoader /> : <> <div> @@ -543,7 +583,7 @@ export class ProjectCreate extends Component { <Button label="Save" className="p-button-primary" id="save-btn" data-testid="save-btn" icon="pi pi-check" onClick={this.saveProject} disabled={!this.state.validForm} /> </div> <div className="col-lg-1 col-md-2 col-sm-6"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelCreate} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.checkIsDirty} /> </div> </div> </> @@ -567,6 +607,11 @@ export class ProjectCreate extends Component { </div> </div> </Dialog> + + <CustomDialog type="confirmation" visible={this.state.showDialog} width="40vw" + header={'Edit Cycle'} message={'Leave this page? Changes you made may not be saved.'} + content={''} onClose={this.close} onCancel={this.close} onSubmit={this.cancelCreate}> + </CustomDialog> </div> </React.Fragment> ); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js index 5e7ae2b315fa1d92f5efbccd0232646bec6074a4..92ff3b59c768d74a7cadd7f3a372bfb9652bf082 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Project/edit.js @@ -1,18 +1,18 @@ -import React, {Component} from 'react'; +import React, { Component } from 'react'; import { Redirect } from 'react-router-dom'; import _ from 'lodash'; -import {InputText} from 'primereact/inputtext'; -import {InputNumber} from 'primereact/inputnumber'; -import {InputTextarea} from 'primereact/inputtextarea'; -import {Checkbox} from 'primereact/checkbox'; -import {Dropdown} from 'primereact/dropdown'; -import {MultiSelect} from 'primereact/multiselect'; +import { InputText } from 'primereact/inputtext'; +import { InputNumber } from 'primereact/inputnumber'; +import { InputTextarea } from 'primereact/inputtextarea'; +import { Checkbox } from 'primereact/checkbox'; +import { Dropdown } from 'primereact/dropdown'; +import { MultiSelect } from 'primereact/multiselect'; import { Button } from 'primereact/button'; -import {Dialog} from 'primereact/components/dialog/Dialog'; -import {Growl} from 'primereact/components/growl/Growl'; - -import {ResourceInputList} from './ResourceInputList'; +import { Dialog } from 'primereact/components/dialog/Dialog'; +import { Growl } from 'primereact/components/growl/Growl'; +import { CustomDialog } from '../../layout/components/CustomDialog'; +import { ResourceInputList } from './ResourceInputList'; import AppLoader from '../../layout/components/AppLoader'; import PageHeader from '../../layout/components/PageHeader'; @@ -25,6 +25,8 @@ export class ProjectEdit extends Component { constructor(props) { super(props); this.state = { + showDialog: false, + isDirty: false, isLoading: true, ltaStorage: [], dialog: { header: '', detail: ''}, @@ -73,6 +75,8 @@ export class ProjectEdit extends Component { this.saveProject = this.saveProject.bind(this); this.saveProjectQuota = this.saveProjectQuota.bind(this); this.cancelEdit = this.cancelEdit.bind(this); + this.checkIsDirty = this.checkIsDirty.bind(this); + this.close = this.close.bind(this); } componentDidMount() { @@ -171,12 +175,16 @@ export class ProjectEdit extends Component { */ addNewResource(){ if (this.state.newResource) { - let resourceList = this.state.resourceList; + let resourceList = _.cloneDeep(this.state.resourceList); const newResource = _.remove(resourceList, {'name': this.state.newResource}); let resources = this.state.resources?this.state.resources:[]; resources.push(newResource[0]); console.log(resources); - this.setState({resources: resources, resourceList: resourceList, newResource: null}); + if ( !this.state.isDirty && !_.isEqual(this.state.resourceList, resourceList) ) { + this.setState({resources: resources, resourceList: resourceList, newResource: null, isDirty: true}); + } else { + this.setState({resources: resources, resourceList: resourceList, newResource: null}); + } } } @@ -187,11 +195,15 @@ export class ProjectEdit extends Component { removeResource(name) { let resources = this.state.resources; let resourceList = this.state.resourceList; - let projectQuota = this.state.projectQuota; + let projectQuota = _.cloneDeep(this.state.projectQuota); const removedResource = _.remove(resources, (resource) => { return resource.name === name }); resourceList.push(removedResource[0]); delete projectQuota[name]; - this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota}); + if ( !this.state.isDirty && !_.isEqual(this.state.projectQuota, projectQuota) ) { + this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota, isDirty: true}); + } else { + this.setState({resourceList: resourceList, resources: resources, projectQuota: projectQuota}); + } } /** @@ -200,7 +212,7 @@ export class ProjectEdit extends Component { * @param {any} value */ setProjectParams(key, value, type) { - let project = this.state.project; + let project = _.cloneDeep(this.state.project); switch(type) { case 'NUMBER': { console.log("Parsing Number"); @@ -231,7 +243,11 @@ export class ProjectEdit extends Component { if (type==='PROJECT_NAME' & value!=="") { validForm = this.validateForm('archive_subdirectory'); } - this.setState({project: project, validForm: validForm}); + if ( !this.state.isDirty && !_.isEqual(this.state.project, project) ) { + this.setState({project: project, validForm: validForm, isDirty: true}); + } else { + this.setState({project: project, validForm: validForm}); + } } /** @@ -241,22 +257,27 @@ export class ProjectEdit extends Component { */ setProjectQuotaParams(key, event) { let projectQuota = this.state.projectQuota; + const previousValue = projectQuota[key]; if (event.target.value) { let resource = _.find(this.state.resources, {'name': key}); let newValue = 0; if (this.resourceUnitMap[resource.quantity_value] && event.target.value.toString().indexOf(this.resourceUnitMap[resource.quantity_value].display)>=0) { - newValue = event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,''); + newValue = _.trim(event.target.value.replace(this.resourceUnitMap[resource.quantity_value].display,'')); } else { - newValue = event.target.value; + newValue = _.trim(event.target.value); } - projectQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:newValue; + projectQuota[key] = (newValue==="NaN" || isNaN(newValue))?0:Number(newValue); } else { - let projectQuota = this.state.projectQuota; + // let projectQuota = this.state.projectQuota; projectQuota[key] = 0; } - this.setState({projectQuota: projectQuota}); + if ( !this.state.isDirty && !_.isEqual(previousValue, projectQuota[key]) ) { + this.setState({projectQuota: projectQuota, isDirty: true}); + } else { + this.setState({projectQuota: projectQuota}); + } } /** @@ -375,7 +396,22 @@ export class ProjectEdit extends Component { } else { dialog = {header: 'Error', detail: 'Project updated successfully but resource allocation not updated properly. Try again!'}; } - this.setState({dialogVisible: true, dialog: dialog}); + this.setState({dialogVisible: true, dialog: dialog, isDirty: false}); + } + + /** + * warn before cancel the page if any changes detected + */ + checkIsDirty() { + if( this.state.isDirty ){ + this.setState({showDialog: true}); + } else { + this.cancelEdit(); + } + } + + close() { + this.setState({showDialog: false}); } /** @@ -393,7 +429,8 @@ export class ProjectEdit extends Component { return ( <React.Fragment> <Growl ref={(el) => this.growl = el} /> - <PageHeader location={this.props.location} title={'Project - Edit'} actions={[{icon:'fa-window-close',link: this.props.history.goBack,title:'Click to Close Project Edit Page', props : { pathname: `/project/view/${this.state.project.name}`}}]}/> + <PageHeader location={this.props.location} title={'Project - Edit'} actions={[{icon:'fa-window-close', + title:'Click to Close Project Edit Page', type: 'button', actOn: 'click', props:{ callback: this.checkIsDirty }}]}/> { this.state.isLoading ? <AppLoader/> : <> @@ -559,7 +596,7 @@ export class ProjectEdit extends Component { <Button label="Save" className="p-button-primary" id="save-btn" data-testid="save-btn" icon="pi pi-check" onClick={this.saveProject} disabled={!this.state.validForm} /> </div> <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelEdit} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.checkIsDirty} /> </div> </div> @@ -583,6 +620,11 @@ export class ProjectEdit extends Component { </div> </div> </Dialog> + + <CustomDialog type="confirmation" visible={this.state.showDialog} width="40vw" + header={'Edit Cycle'} message={'Leave this page? Changes you made may not be saved.'} + content={''} onClose={this.close} onCancel={this.close} onSubmit={this.cancelEdit}> + </CustomDialog> </div> </React.Fragment> ); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js index 082a6912336dcc03b2fe3c78f89f69b659e7a598..9b5327dca02877ccf7deb82d05735a205aa3f68b 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/create.js @@ -1,15 +1,15 @@ -import React, {Component} from 'react'; +import React, { Component } from 'react'; import { Redirect } from 'react-router-dom'; import _ from 'lodash'; import $RefParser from "@apidevtools/json-schema-ref-parser"; import moment from 'moment'; -import {InputText} from 'primereact/inputtext'; -import {InputTextarea} from 'primereact/inputtextarea'; -import {Dropdown} from 'primereact/dropdown'; +import { InputText } from 'primereact/inputtext'; +import { InputTextarea } from 'primereact/inputtextarea'; +import { Dropdown } from 'primereact/dropdown'; import { Button } from 'primereact/button'; -import {Dialog} from 'primereact/components/dialog/Dialog'; -import {Growl} from 'primereact/components/growl/Growl'; - +import { Dialog } from 'primereact/components/dialog/Dialog'; +import { Growl } from 'primereact/components/growl/Growl'; +import { CustomDialog } from '../../layout/components/CustomDialog'; import AppLoader from '../../layout/components/AppLoader'; import Jeditor from '../../components/JSONEditor/JEditor'; import UnitConversion from '../../utils/unit.converter'; @@ -29,6 +29,8 @@ export class SchedulingUnitCreate extends Component { constructor(props) { super(props); this.state = { + showDialog: false, + isDirty: false, isLoading: true, // Flag for loading spinner dialog: { header: '', detail: ''}, // Dialog properties touched: {}, @@ -39,7 +41,9 @@ export class SchedulingUnitCreate extends Component { stationOptions: [], stationGroup: [], customSelectedStations: [], // custom stations - schedulingUnit: { + schedulingUnit: { + name: '', + description: '', project: (props.match?props.match.params.project:null) || null, }, projectDisabled: (props.match?(props.match.params.project? true:false):false), // Disable project selection if @@ -76,6 +80,8 @@ export class SchedulingUnitCreate extends Component { this.saveSchedulingUnit = this.saveSchedulingUnit.bind(this); this.cancelCreate = this.cancelCreate.bind(this); this.reset = this.reset.bind(this); + this.checkIsDirty = this.checkIsDirty.bind(this); + this.close = this.close.bind(this); } componentDidMount() { @@ -111,7 +117,7 @@ export class SchedulingUnitCreate extends Component { const projectSchedSets = _.filter(this.schedulingSets, {'project_id': projectName}); let schedulingUnit = this.state.schedulingUnit; schedulingUnit.project = projectName; - this.setState({schedulingUnit: schedulingUnit, schedulingSets: projectSchedSets, validForm: this.validateForm('project')}); + this.setState({schedulingUnit: schedulingUnit, schedulingSets: projectSchedSets, validForm: this.validateForm('project'), isDirty: true}); } /** @@ -176,7 +182,7 @@ export class SchedulingUnitCreate extends Component { } } - this.setState({observStrategy: observStrategy, paramsSchema: schema, paramsOutput: paramsOutput, stationGroup: station_group}); + this.setState({observStrategy: observStrategy, paramsSchema: schema, paramsOutput: paramsOutput, stationGroup: station_group, isDirty: true}); // Function called to clear the JSON Editor fields and reload with new schema if (this.state.editorFunction) { @@ -203,12 +209,16 @@ export class SchedulingUnitCreate extends Component { if (jsonOutput.scheduler === 'online' || jsonOutput.scheduler === 'dynamic') { err = err.filter(e => e.path !== 'root.time.at'); } - this.constraintParamsOutput = jsonOutput; + // this.constraintParamsOutput = jsonOutput; // condition goes here.. this.constraintValidEditor = err.length === 0; - this.setState({ constraintParamsOutput: jsonOutput, - constraintValidEditor: err.length === 0, - validForm: this.validateForm()}); + if ( !this.state.isDirty && this.state.constraintParamsOutput && !_.isEqual(this.state.constraintParamsOutput, jsonOutput) ) { + this.setState({ constraintParamsOutput: jsonOutput, constraintValidEditor: err.length === 0, validForm: this.validateForm(), isDirty: true}); + } else { + this.setState({ constraintParamsOutput: jsonOutput, constraintValidEditor: err.length === 0, validForm: this.validateForm()}); + } + + } /** @@ -230,9 +240,13 @@ export class SchedulingUnitCreate extends Component { [key]: true } }); - let schedulingUnit = this.state.schedulingUnit; + let schedulingUnit = _.cloneDeep(this.state.schedulingUnit); schedulingUnit[key] = value; - this.setState({schedulingUnit: schedulingUnit, validForm: this.validateForm(key), validEditor: this.validateEditor()}); + if ( !this.state.isDirty && !_.isEqual(this.state.schedulingUnit, schedulingUnit) ) { + this.setState({schedulingUnit: schedulingUnit, validForm: this.validateForm(key), validEditor: this.validateEditor(), isDirty: true}); + } else { + this.setState({schedulingUnit: schedulingUnit, validForm: this.validateForm(key), validEditor: this.validateEditor()}); + } this.validateEditor(); } @@ -361,12 +375,27 @@ export class SchedulingUnitCreate extends Component { if (!schedulingUnit.error) { // this.growl.show({severity: 'success', summary: 'Success', detail: 'Scheduling Unit and tasks created successfully!'}); const dialog = {header: 'Success', detail: 'Scheduling Unit and Tasks are created successfully. Do you want to create another Scheduling Unit?'}; - this.setState({schedulingUnit: schedulingUnit, dialogVisible: true, dialog: dialog}) + this.setState({schedulingUnit: schedulingUnit, dialogVisible: true, dialog: dialog, isDirty: false}); } else { this.growl.show({severity: 'error', summary: 'Error Occured', detail: schedulingUnit.message || 'Unable to save Scheduling Unit/Tasks'}); } } + /** + * warn before cancel the page if any changes detected + */ + checkIsDirty() { + if( this.state.isDirty ){ + this.setState({showDialog: true}); + } else { + this.cancelCreate(); + } + } + + close() { + this.setState({showDialog: false}); + } + /** * Cancel SU creation and redirect */ @@ -388,6 +417,7 @@ export class SchedulingUnitCreate extends Component { this.nameInput.element.focus(); this.setState({ dialogVisible: false, + isDirty: false, dialog: { header: '', detail: ''}, errors: [], schedulingSets: this.props.match.params.project?schedulingSets:[], @@ -415,18 +445,27 @@ export class SchedulingUnitCreate extends Component { } onUpdateStations = (state, selectedStations, missing_StationFieldsErrors, customSelectedStations) => { - this.setState({ - ...state, - selectedStations, - missing_StationFieldsErrors, - customSelectedStations - - }, () => { - this.setState({ - validForm: this.validateForm() + const selectedStation = this.state.selectedStations; + const customStation = this.state.customSelectedStations; + if ( !this.state.isDirty ) { + if (selectedStation && !_.isEqual(selectedStation, selectedStations)){ + this.setState({...state, selectedStations, missing_StationFieldsErrors, customSelectedStations }, () => { + this.setState({ validForm: this.validateForm(), isDirty: true }); + }); + } else if (customStation && !_.isEqual(customStation, customSelectedStations)){ + this.setState({...state, selectedStations, missing_StationFieldsErrors, customSelectedStations }, () => { + this.setState({ validForm: this.validateForm(), isDirty: true }); + }); + } else { + this.setState({...state, selectedStations, missing_StationFieldsErrors, customSelectedStations }, () => { + this.setState({ validForm: this.validateForm() }); + }); + } + } else { + this.setState({...state, selectedStations, missing_StationFieldsErrors, customSelectedStations }, () => { + this.setState({ validForm: this.validateForm() }); }); - - }); + } }; render() { @@ -450,7 +489,8 @@ export class SchedulingUnitCreate extends Component { <React.Fragment> <Growl ref={(el) => this.growl = el} /> <PageHeader location={this.props.location} title={'Scheduling Unit - Add'} - actions={[{icon: 'fa-window-close',link: this.props.history.goBack,title:'Click to close Scheduling Unit creation', props : { pathname: `/schedulingunit`}}]}/> + actions={[{icon: 'fa-window-close', title:'Click to close Scheduling Unit creation', + type: 'button', actOn: 'click', props:{ callback: this.checkIsDirty }}]}/> { this.state.isLoading ? <AppLoader /> : <> <div> @@ -559,7 +599,7 @@ export class SchedulingUnitCreate extends Component { disabled={!this.state.constraintValidEditor || !this.state.validEditor || !this.state.validForm} data-testid="save-btn" /> </div> <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelCreate} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.checkIsDirty} /> </div> </div> </div> @@ -585,6 +625,11 @@ export class SchedulingUnitCreate extends Component { </div> </div> </Dialog> + + <CustomDialog type="confirmation" visible={this.state.showDialog} width="40vw" + header={'Edit Cycle'} message={'Leave this page? Changes you made may not be saved.'} + content={''} onClose={this.close} onCancel={this.close} onSubmit={this.cancelCreate}> + </CustomDialog> </div> </React.Fragment> ); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js index 0c92c2c764bdfad792e9beaefe93facfe3d93d89..f5d9efeee31e130e9260b1b289287d0c99ae3078 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Scheduling/edit.js @@ -1,15 +1,15 @@ -import React, {Component} from 'react'; +import React, { Component } from 'react'; import { Redirect } from 'react-router-dom'; import moment from 'moment'; import _ from 'lodash'; import $RefParser from "@apidevtools/json-schema-ref-parser"; -import {InputText} from 'primereact/inputtext'; -import {InputTextarea} from 'primereact/inputtextarea'; -import {Dropdown} from 'primereact/dropdown'; +import { InputText } from 'primereact/inputtext'; +import { InputTextarea } from 'primereact/inputtextarea'; +import { Dropdown } from 'primereact/dropdown'; import { Button } from 'primereact/button'; -import {Growl} from 'primereact/components/growl/Growl'; - +import { Growl } from 'primereact/components/growl/Growl'; +import { CustomDialog } from '../../layout/components/CustomDialog'; import AppLoader from '../../layout/components/AppLoader'; import PageHeader from '../../layout/components/PageHeader'; import Jeditor from '../../components/JSONEditor/JEditor'; @@ -29,6 +29,8 @@ export class EditSchedulingUnit extends Component { constructor(props) { super(props); this.state = { + showDialog: false, + isDirty: false, isLoading: true, //Flag for loading spinner dialog: { header: '', detail: ''}, //Dialog properties redirect: null, //URL to redirect @@ -71,6 +73,8 @@ export class EditSchedulingUnit extends Component { this.saveSchedulingUnit = this.saveSchedulingUnit.bind(this); this.cancelCreate = this.cancelCreate.bind(this); this.setEditorOutputConstraint = this.setEditorOutputConstraint.bind(this); + this.checkIsDirty = this.checkIsDirty.bind(this); + this.close = this.close.bind(this); } /** @@ -188,9 +192,16 @@ export class EditSchedulingUnit extends Component { setEditorOutput(jsonOutput, errors) { this.paramsOutput = jsonOutput; this.validEditor = errors.length === 0; - this.setState({ paramsOutput: jsonOutput, - validEditor: errors.length === 0, - validForm: this.validateForm()}); + if ( !this.state.isDirty && this.state.paramsOutput && !_.isEqual(this.state.paramsOutput, jsonOutput) ) { + this.setState({ paramsOutput: jsonOutput, + validEditor: errors.length === 0, + validForm: this.validateForm(), isDirty: true}); + } else { + this.setState({ paramsOutput: jsonOutput, + validEditor: errors.length === 0, + validForm: this.validateForm()}); + } + } setEditorOutputConstraint(jsonOutput, errors) { @@ -200,9 +211,16 @@ export class EditSchedulingUnit extends Component { } this.constraintParamsOutput = jsonOutput || {}; this.constraintValidEditor = err.length === 0; - this.setState({ constraintParamsOutput: jsonOutput, - constraintValidEditor: err.length === 0, - validForm: this.validateForm()}); + if ( !this.state.isDirty && this.state.constraintParamsOutput && !_.isEqual(this.state.constraintParamsOutput, this.constraintParamsOutput) ) { + this.setState({ constraintParamsOutput: jsonOutput, + constraintValidEditor: err.length === 0, + validForm: this.validateForm(), isDirty: true}); + } else { + this.setState({ constraintParamsOutput: jsonOutput, + constraintValidEditor: err.length === 0, + validForm: this.validateForm()}); + } + } /** @@ -218,9 +236,14 @@ export class EditSchedulingUnit extends Component { * @param {object} value */ setSchedUnitParams(key, value) { - let schedulingUnit = this.state.schedulingUnit; + let schedulingUnit = _.cloneDeep(this.state.schedulingUnit); schedulingUnit[key] = value; - this.setState({schedulingUnit: schedulingUnit, validForm: this.validateForm(key), validEditor: this.validateEditor()}); + if ( !this.state.isDirty && !_.isEqual(this.state.schedulingUnit, schedulingUnit) ) { + this.setState({schedulingUnit: schedulingUnit, validForm: this.validateForm(key), validEditor: this.validateEditor(), isDirty: true}); + } else { + this.setState({schedulingUnit: schedulingUnit, validForm: this.validateForm(key), validEditor: this.validateEditor()}); + } + // this.setState({schedulingUnit: schedulingUnit, validForm: this.validateForm(key), validEditor: this.validateEditor()}); this.validateEditor(); } @@ -353,9 +376,24 @@ export class EditSchedulingUnit extends Component { } else { this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Template Missing.'}); } + this.setState({isDirty: false}); } + /** + * warn before cancel the page if any changes detected + */ + checkIsDirty() { + if( this.state.isDirty ){ + this.setState({showDialog: true}); + } else { + this.cancelCreate(); + } + } + close() { + this.setState({showDialog: false}); + } + /** * Cancel SU creation and redirect */ @@ -368,7 +406,28 @@ export class EditSchedulingUnit extends Component { } onUpdateStations = (state, selectedStations, missingStationFieldsErrors, customSelectedStations) => { - this.setState({ + const selectedStation = this.state.selectedStations; + const customStation = this.state.customSelectedStations; + if ( !this.state.isDirty ) { + if (selectedStation && !_.isEqual(selectedStation, selectedStations)){ + this.setState({...state, selectedStations, missingStationFieldsErrors, customSelectedStations }, () => { + this.setState({ validForm: this.validateForm(), isDirty: true }); + }); + } else if (customStation && !_.isEqual(customStation, customSelectedStations)){ + this.setState({...state, selectedStations, missingStationFieldsErrors, customSelectedStations }, () => { + this.setState({ validForm: this.validateForm(), isDirty: true }); + }); + } else { + this.setState({...state, selectedStations, missingStationFieldsErrors, customSelectedStations }, () => { + this.setState({ validForm: this.validateForm() }); + }); + } + } else { + this.setState({...state, selectedStations, missingStationFieldsErrors, customSelectedStations }, () => { + this.setState({ validForm: this.validateForm() }); + }); + } + /* this.setState({ ...state, selectedStations, missingStationFieldsErrors, @@ -377,7 +436,7 @@ export class EditSchedulingUnit extends Component { this.setState({ validForm: this.validateForm() }); - }); + });*/ }; render() { @@ -400,7 +459,8 @@ export class EditSchedulingUnit extends Component { <React.Fragment> <Growl ref={el => (this.growl = el)} /> <PageHeader location={this.props.location} title={'Scheduling Unit - Edit'} - actions={[{icon: 'fa-window-close',link: this.props.history.goBack,title:'Click to Close Scheduling Unit View', props : { pathname: `/schedulingunit/view/draft/${this.props.match.params.id}`}}]}/> + actions={[{icon: 'fa-window-close', title:'Click to Close Scheduling Unit View', + type: 'button', actOn: 'click', props:{ callback: this.checkIsDirty }}]}/> { this.state.isLoading ? <AppLoader /> : <> <div> @@ -480,14 +540,12 @@ export class EditSchedulingUnit extends Component { </div> </div> </div> - <Stations stationGroup={this.state.stationGroup} onUpdateStations={this.onUpdateStations.bind(this)} /> - {this.state.constraintSchema && <div className="p-fluid"> <div className="p-grid"> <div className="p-col-12"> @@ -509,9 +567,16 @@ export class EditSchedulingUnit extends Component { disabled={!this.state.validEditor || !this.state.validForm} data-testid="save-btn" /> </div> <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelCreate} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.checkIsDirty} /> </div> </div> + + <div className="p-grid" data-testid="confirm_dialog"> + <CustomDialog type="confirmation" visible={this.state.showDialog} width="40vw" + header={'Edit Cycle'} message={'Leave this page? Changes you made may not be saved.'} + content={''} onClose={this.close} onCancel={this.close} onSubmit={this.cancelCreate}> + </CustomDialog> + </div> </div> </> diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js index 3cb5f2e8ac3b8c39edcfc6a4da138599190d6ca2..69f57dc7de417cc8c84f8a9da865de7fbc863e42 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Task/edit.js @@ -1,13 +1,13 @@ -import React, {Component} from 'react'; +import React, { Component} from 'react'; import { Link, Redirect } from 'react-router-dom'; import _ from 'lodash'; -import {InputText} from 'primereact/inputtext'; -import {InputTextarea} from 'primereact/inputtextarea'; -import {Chips} from 'primereact/chips'; -import {Dropdown} from 'primereact/dropdown'; +import { InputText } from 'primereact/inputtext'; +import { InputTextarea } from 'primereact/inputtextarea'; +import { Chips } from 'primereact/chips'; +import { Dropdown } from 'primereact/dropdown'; import { Button } from 'primereact/button'; - +import { CustomDialog } from '../../layout/components/CustomDialog'; import Jeditor from '../../components/JSONEditor/JEditor'; import TaskService from '../../services/task.service'; @@ -21,6 +21,8 @@ export class TaskEdit extends Component { constructor(props) { super(props); this.state = { + showDialog: false, + isDirty: false, task: { name: "", created_at: null, @@ -47,6 +49,8 @@ export class TaskEdit extends Component { this.validateForm = this.validateForm.bind(this); this.saveTask = this.saveTask.bind(this); this.cancelEdit = this.cancelEdit.bind(this); + this.checkIsDirty = this.checkIsDirty.bind(this); + this.close = this.close.bind(this); } /** @@ -71,8 +75,14 @@ export class TaskEdit extends Component { */ setTaskParams(key, value) { let task = this.state.task; + const taskValue = this.state.task[key]; task[key] = value; - this.setState({task: task, validForm: this.validateForm()}); + if ( !this.state.isDirty && taskValue && !_.isEqual(taskValue, value) ) { + this.setState({task: task, validForm: this.validateForm(), isDirty: true}); + } else { + this.setState({task: task, validForm: this.validateForm()}); + } + } /** @@ -95,7 +105,7 @@ export class TaskEdit extends Component { task.specifications_template = template.url; this.setState({taskSchema: null}); - this.setState({task: task, taskSchema: template.schema}); + this.setState({task: task, taskSchema: template.schema, isDirty: true}); this.state.editorFunction(); } @@ -126,6 +136,7 @@ export class TaskEdit extends Component { * Function to call the servie and pass the values to save */ saveTask() { + this.setState({isDirty: false}); let task = this.state.task; task.specifications_doc = this.templateOutput[task.specifications_template_id]; // Remove read only properties from the object before sending to API @@ -138,6 +149,21 @@ export class TaskEdit extends Component { }); } + /** + * warn before cancel the page if any changes detected + */ + checkIsDirty() { + if( this.state.isDirty ){ + this.setState({showDialog: true}); + } else { + this.cancelEdit(); + } + } + + close() { + this.setState({showDialog: false}); + } + cancelEdit() { this.props.history.goBack(); } @@ -204,14 +230,16 @@ export class TaskEdit extends Component { </Link> </div> </div> */} - <PageHeader location={this.props.location} title={'Task - Edit'} actions={[{icon: 'fa-window-close',link: this.props.history.goBack,title:'Click to Close Task Edit Page' ,props : { pathname: `/task/view/draft/${this.state.task?this.state.task.id:''}`}}]}/> + <PageHeader location={this.props.location} title={'Task - Edit'} actions={[{icon: 'fa-window-close', + title:'Click to Close Task Edit Page', props : { pathname: `/task/view/draft/${this.state.task?this.state.task.id:''}`}}]}/> {isLoading ? <AppLoader/> : <div> <div className="p-fluid"> <div className="p-field p-grid"> <label htmlFor="taskName" className="col-lg-2 col-md-2 col-sm-12">Name <span style={{color:'red'}}>*</span></label> <div className="col-lg-4 col-md-4 col-sm-12"> - <InputText className={this.state.errors.name ?'input-error':''} id="taskName" type="text" value={this.state.task.name} onChange={(e) => this.setTaskParams('name', e.target.value)}/> + <InputText className={this.state.errors.name ?'input-error':''} id="taskName" type="text" value={this.state.task.name} + onChange={(e) => this.setTaskParams('name', e.target.value)}/> <label className="error"> {this.state.errors.name ? this.state.errors.name : ""} </label> @@ -276,8 +304,14 @@ export class TaskEdit extends Component { <Button label="Save" className="p-button-primary" icon="pi pi-check" onClick={this.saveTask} disabled={!this.state.validEditor || !this.state.validForm} /> </div> <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelEdit} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.checkIsDirty} /> + </div> </div> + <div className="p-grid" data-testid="confirm_dialog"> + <CustomDialog type="confirmation" visible={this.state.showDialog} width="40vw" + header={'Edit Cycle'} message={'Leave this page? Changes you made may not be saved.'} + content={''} onClose={this.close} onCancel={this.close} onSubmit={this.cancelEdit}> + </CustomDialog> </div> </React.Fragment> ); diff --git a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.create.js b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.create.js index e95f7a29fd922f3b7ee9ce6ca29a45eea45f4bd3..5566059d92ecb32d95310d576634d3bafc7d2ba8 100644 --- a/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.create.js +++ b/SAS/TMSS/frontend/tmss_webapp/src/routes/Timeline/reservation.create.js @@ -1,17 +1,18 @@ -import React, {Component} from 'react'; +import React, { Component } from 'react'; import { Redirect } from 'react-router-dom'; - -import {Growl} from 'primereact/components/growl/Growl'; +import _ from 'lodash'; +import { Growl } from 'primereact/components/growl/Growl'; import AppLoader from '../../layout/components/AppLoader'; import PageHeader from '../../layout/components/PageHeader'; import UIConstants from '../../utils/ui.constants'; -import {Calendar} from 'primereact/calendar'; +import { Calendar } from 'primereact/calendar'; import { InputMask } from 'primereact/inputmask'; -import {Dropdown} from 'primereact/dropdown'; -import {InputText} from 'primereact/inputtext'; -import {InputTextarea} from 'primereact/inputtextarea'; +import { Dropdown } from 'primereact/dropdown'; +import {InputText } from 'primereact/inputtext'; +import { InputTextarea } from 'primereact/inputtextarea'; import { Button } from 'primereact/button'; -import {Dialog} from 'primereact/components/dialog/Dialog'; +import { Dialog } from 'primereact/components/dialog/Dialog'; +import { CustomDialog } from '../../layout/components/CustomDialog'; import ProjectService from '../../services/project.service'; import ReservationService from '../../services/reservation.service'; import UnitService from '../../utils/unit.converter'; @@ -24,6 +25,8 @@ export class ReservationCreate extends Component { constructor(props) { super(props); this.state= { + showDialog: false, + isDirty: false, isLoading: true, redirect: null, paramsSchema: null, // JSON Schema to be generated from strategy template to pass to JSON editor @@ -59,6 +62,8 @@ export class ReservationCreate extends Component { this.saveReservation = this.saveReservation.bind(this); this.reset = this.reset.bind(this); this.cancelCreate = this.cancelCreate.bind(this); + this.checkIsDirty = this.checkIsDirty.bind(this); + this.close = this.close.bind(this); this.initReservation = this.initReservation.bind(this); } @@ -100,13 +105,21 @@ export class ReservationCreate extends Component { * @param {object} value */ setReservationParams(key, value) { - - let reservation = this.state.reservation; + let reservation = _.cloneDeep(this.state.reservation); reservation[key] = value; - this.setState({reservation: reservation, validForm: this.validateForm(key), validEditor: this.validateEditor(),touched: { - ...this.state.touched, - [key]: true - }}); + if ( !this.state.isDirty && !_.isEqual(this.state.reservation, reservation) ) { + this.setState({reservation: reservation, validForm: this.validateForm(key), validEditor: this.validateEditor(), touched: { + ...this.state.touched, + [key]: true + }, isDirty: true}); + } else { + this.setState({reservation: reservation, validForm: this.validateForm(key), validEditor: this.validateEditor(),touched: { + ...this.state.touched, + [key]: true + }}); + } + + } /** @@ -124,7 +137,8 @@ export class ReservationCreate extends Component { setParams(key, value, type) { if(key === 'duration' && !this.validateDuration( value)) { this.setState({ - durationError: true + durationError: true, + isDirty: true }) return; } @@ -139,7 +153,7 @@ export class ReservationCreate extends Component { break; } } - this.setState({reservation: reservation, validForm: this.validateForm(key), durationError: false}); + this.setState({reservation: reservation, validForm: this.validateForm(key), durationError: false, isDirty: true}); } /** @@ -205,9 +219,16 @@ export class ReservationCreate extends Component { setEditorOutput(jsonOutput, errors) { this.paramsOutput = jsonOutput; this.validEditor = errors.length === 0; - this.setState({ paramsOutput: jsonOutput, - validEditor: errors.length === 0, - validForm: this.validateForm()}); + if ( !this.state.isDirty && this.state.paramsOutput && !_.isEqual(this.state.paramsOutput, jsonOutput) ) { + this.setState({ paramsOutput: jsonOutput, + validEditor: errors.length === 0, + validForm: this.validateForm(), + isDirty: true}); + } else { + this.setState({ paramsOutput: jsonOutput, + validEditor: errors.length === 0, + validForm: this.validateForm()}); + } } saveReservation(){ @@ -220,9 +241,9 @@ export class ReservationCreate extends Component { reservation = ReservationService.saveReservation(reservation); if (reservation && reservation !== null){ const dialog = {header: 'Success', detail: 'Reservation is created successfully. Do you want to create another Reservation?'}; - this.setState({ dialogVisible: true, dialog: dialog, paramsOutput: {}}) + this.setState({ dialogVisible: true, dialog: dialog, paramsOutput: {}, showDialog: false, isDirty: false}) } else { - this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to save Reservation'}); + this.growl.show({severity: 'error', summary: 'Error Occured', detail: 'Unable to save Reservation', showDialog: false, isDirty: false}); } } @@ -248,6 +269,8 @@ export class ReservationCreate extends Component { validFields: {}, touched:false, stationGroup: [], + showDialog: false, + isDirty: false }); this.initReservation(); } @@ -259,6 +282,21 @@ export class ReservationCreate extends Component { this.props.history.goBack(); } + /** + * warn before cancel the page if any changes detected + */ + checkIsDirty() { + if( this.state.isDirty ){ + this.setState({showDialog: true}); + } else { + this.cancelCreate(); + } + } + + close() { + this.setState({showDialog: false}); + } + render() { if (this.state.redirect) { return <Redirect to={ {pathname: this.state.redirect} }></Redirect> @@ -278,7 +316,8 @@ export class ReservationCreate extends Component { <React.Fragment> <Growl ref={(el) => this.growl = el} /> <PageHeader location={this.props.location} title={'Reservation - Add'} - actions={[{icon: 'fa-window-close' ,title:'Click to close Reservation creation', props : { pathname: `/su/timelineview/reservation/reservation/list`}}]}/> + actions={[{icon: 'fa-window-close' ,title:'Click to close Reservation creation', + type: 'button', actOn: 'click', props:{ callback: this.checkIsDirty }}]}/> { this.state.isLoading ? <AppLoader /> : <> <div> @@ -377,7 +416,7 @@ export class ReservationCreate extends Component { disabled={!this.state.validEditor || !this.state.validForm} data-testid="save-btn" /> </div> <div className="p-col-1"> - <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.cancelCreate} /> + <Button label="Cancel" className="p-button-danger" icon="pi pi-times" onClick={this.checkIsDirty} /> </div> </div> </div> @@ -402,6 +441,11 @@ export class ReservationCreate extends Component { </div> </div> </Dialog> + + <CustomDialog type="confirmation" visible={this.state.showDialog} width="40vw" + header={'Edit Cycle'} message={'Leave this page? Changes you made may not be saved.'} + content={''} onClose={this.close} onCancel={this.close} onSubmit={this.cancelCreate}> + </CustomDialog> </div> </React.Fragment> );