Skip to content

Commit 7848d6f

Browse files
committed
feat: improved messages for unsaved changes
1 parent 4304c34 commit 7848d6f

File tree

8 files changed

+231
-104
lines changed

8 files changed

+231
-104
lines changed

src/frontend/submission_form/src/main_layout/Menu.js

Lines changed: 173 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
import React from 'react';
1+
import React, {useState} from 'react';
22
import {Nav, NavItem, OverlayTrigger, Tooltip} from "react-bootstrap";
33
import {IndexLinkContainer} from "react-router-bootstrap";
44
import Glyphicon from "react-bootstrap/lib/Glyphicon";
55
import {MENU_INDEX, WIDGET, WIDGET_TITLE} from "../constants";
66
import {useDispatch, useSelector} from "react-redux";
7-
import {setSelectedWidget} from "../redux/actions/widgetActions";
7+
import {setSelectedWidget, saveWidgetData} from "../redux/actions/widgetActions";
8+
import {withRouter} from "react-router-dom";
9+
import UnsavedChangesModal from "../components/modals/UnsavedChangesModal";
810

9-
const Menu = ({urlQuery, onMenuItemClick = () => {}}) => {
11+
const Menu = ({urlQuery, onMenuItemClick = () => {}, history}) => {
1012
const dispatch = useDispatch();
13+
const selectedWidget = useSelector((state) => state.widget.selectedWidget);
14+
const [showUnsavedModal, setShowUnsavedModal] = useState(false);
15+
const [pendingNavigation, setPendingNavigation] = useState(null);
1116

1217
// Utility function to check if a widget has changes
1318
const hasWidgetChanges = (widgetState) => {
@@ -92,6 +97,89 @@ const Menu = ({urlQuery, onMenuItemClick = () => {}}) => {
9297
return hasChanges;
9398
};
9499

100+
// Get widget state name from menu index
101+
const getWidgetStateName = (menuIndex) => {
102+
const widgetMap = {
103+
[MENU_INDEX[WIDGET.OVERVIEW]]: 'overview',
104+
[MENU_INDEX[WIDGET.GENETICS]]: 'genetics',
105+
[MENU_INDEX[WIDGET.REAGENT]]: 'reagent',
106+
[MENU_INDEX[WIDGET.EXPRESSION]]: 'expression',
107+
[MENU_INDEX[WIDGET.INTERACTIONS]]: 'interactions',
108+
[MENU_INDEX[WIDGET.PHENOTYPES]]: 'phenotypes',
109+
[MENU_INDEX[WIDGET.DISEASE]]: 'disease',
110+
[MENU_INDEX[WIDGET.COMMENTS]]: 'comments'
111+
};
112+
return widgetMap[menuIndex] || null;
113+
};
114+
115+
// Get widget title from menu index
116+
const getWidgetTitle = (menuIndex) => {
117+
const reverseMap = Object.keys(MENU_INDEX).find(key => MENU_INDEX[key] === menuIndex);
118+
return reverseMap ? WIDGET_TITLE[reverseMap] : '';
119+
};
120+
121+
// Get current widget state
122+
const currentWidgetState = useSelector((state) => {
123+
const widgetName = getWidgetStateName(selectedWidget);
124+
return widgetName ? state[widgetName] : null;
125+
});
126+
127+
// Handle navigation attempt
128+
const handleNavigationAttempt = (targetMenuIndex, targetPath) => {
129+
const hasChanges = hasWidgetChanges(currentWidgetState);
130+
131+
// If no unsaved changes, navigate directly
132+
if (!hasChanges) {
133+
dispatch(setSelectedWidget(targetMenuIndex));
134+
history.push(targetPath + urlQuery);
135+
onMenuItemClick();
136+
window.scrollTo(0, 0);
137+
return;
138+
}
139+
140+
// Show modal for unsaved changes
141+
setPendingNavigation({
142+
menuIndex: targetMenuIndex,
143+
path: targetPath
144+
});
145+
setShowUnsavedModal(true);
146+
};
147+
148+
// Handle save and continue
149+
const handleSaveAndContinue = () => {
150+
const widgetName = getWidgetStateName(selectedWidget);
151+
if (widgetName && pendingNavigation) {
152+
dispatch(saveWidgetData(widgetName));
153+
// Wait a moment for save to process, then navigate
154+
setTimeout(() => {
155+
dispatch(setSelectedWidget(pendingNavigation.menuIndex));
156+
history.push(pendingNavigation.path + urlQuery);
157+
onMenuItemClick();
158+
window.scrollTo(0, 0);
159+
setShowUnsavedModal(false);
160+
setPendingNavigation(null);
161+
}, 500);
162+
}
163+
};
164+
165+
// Handle continue without saving
166+
const handleContinueWithoutSaving = () => {
167+
if (pendingNavigation) {
168+
dispatch(setSelectedWidget(pendingNavigation.menuIndex));
169+
history.push(pendingNavigation.path + urlQuery);
170+
onMenuItemClick();
171+
window.scrollTo(0, 0);
172+
setShowUnsavedModal(false);
173+
setPendingNavigation(null);
174+
}
175+
};
176+
177+
// Handle cancel navigation
178+
const handleCancelNavigation = () => {
179+
setShowUnsavedModal(false);
180+
setPendingNavigation(null);
181+
};
182+
95183
// Reusable menu item component
96184
const MenuItemWithIcon = ({ widget, title, children }) => {
97185
const widgetState = useSelector((state) => state[widget]);
@@ -118,57 +206,89 @@ const Menu = ({urlQuery, onMenuItemClick = () => {}}) => {
118206

119207
return (
120208
<div>
209+
<UnsavedChangesModal
210+
show={showUnsavedModal}
211+
onHide={handleCancelNavigation}
212+
onSaveAndContinue={handleSaveAndContinue}
213+
onContinueWithoutSaving={handleContinueWithoutSaving}
214+
currentWidget={getWidgetTitle(selectedWidget)}
215+
targetWidget={pendingNavigation ? getWidgetTitle(pendingNavigation.menuIndex) : ''}
216+
/>
121217
<div className="panel panel-default" style={{marginBottom: '10px'}}>
122218
<div className="panel-body">
123-
<Nav bsStyle="pills" stacked onSelect={(sel) => dispatch(setSelectedWidget(sel))}>
124-
<IndexLinkContainer to={WIDGET.OVERVIEW + urlQuery}
125-
active={useSelector((state) => state.widget.selectedWidget) === MENU_INDEX[WIDGET.OVERVIEW]}>
126-
<NavItem eventKey={MENU_INDEX[WIDGET.OVERVIEW]} onClick={onMenuItemClick}>
127-
<MenuItemWithIcon widget="overview" title={WIDGET_TITLE[WIDGET.OVERVIEW]} />
128-
</NavItem>
129-
</IndexLinkContainer>
130-
<IndexLinkContainer to={WIDGET.GENETICS + urlQuery}
131-
active={useSelector((state) => state.widget.selectedWidget) === MENU_INDEX[WIDGET.GENETICS]}>
132-
<NavItem eventKey={MENU_INDEX[WIDGET.GENETICS]} onClick={onMenuItemClick}>
133-
<MenuItemWithIcon widget="genetics" title={WIDGET_TITLE[WIDGET.GENETICS]} />
134-
</NavItem>
135-
</IndexLinkContainer>
136-
<IndexLinkContainer to={WIDGET.REAGENT + urlQuery}
137-
active={useSelector((state) => state.widget.selectedWidget) === MENU_INDEX[WIDGET.REAGENT]}>
138-
<NavItem eventKey={MENU_INDEX[WIDGET.REAGENT]} onClick={onMenuItemClick}>
139-
<MenuItemWithIcon widget="reagent" title={WIDGET_TITLE[WIDGET.REAGENT]} />
140-
</NavItem>
141-
</IndexLinkContainer>
142-
<IndexLinkContainer to={WIDGET.EXPRESSION + urlQuery}
143-
active={useSelector((state) => state.widget.selectedWidget) === MENU_INDEX[WIDGET.EXPRESSION]}>
144-
<NavItem eventKey={MENU_INDEX[WIDGET.EXPRESSION]} onClick={onMenuItemClick}>
145-
<MenuItemWithIcon widget="expression" title={WIDGET_TITLE[WIDGET.EXPRESSION]} />
146-
</NavItem>
147-
</IndexLinkContainer>
148-
<IndexLinkContainer to={WIDGET.INTERACTIONS + urlQuery}
149-
active={useSelector((state) => state.widget.selectedWidget) === MENU_INDEX[WIDGET.INTERACTIONS]}>
150-
<NavItem eventKey={MENU_INDEX[WIDGET.INTERACTIONS]} onClick={onMenuItemClick}>
151-
<MenuItemWithIcon widget="interactions" title={WIDGET_TITLE[WIDGET.INTERACTIONS]} />
152-
</NavItem>
153-
</IndexLinkContainer>
154-
<IndexLinkContainer to={WIDGET.PHENOTYPES + urlQuery}
155-
active={useSelector((state) => state.widget.selectedWidget) === MENU_INDEX[WIDGET.PHENOTYPES]}>
156-
<NavItem eventKey={MENU_INDEX[WIDGET.PHENOTYPES]} onClick={onMenuItemClick}>
157-
<MenuItemWithIcon widget="phenotypes" title={WIDGET_TITLE[WIDGET.PHENOTYPES]} />
158-
</NavItem>
159-
</IndexLinkContainer>
160-
<IndexLinkContainer to={WIDGET.DISEASE + urlQuery}
161-
active={useSelector((state) => state.widget.selectedWidget) === MENU_INDEX[WIDGET.DISEASE]}>
162-
<NavItem eventKey={MENU_INDEX[WIDGET.DISEASE]} onClick={onMenuItemClick}>
163-
<MenuItemWithIcon widget="disease" title={WIDGET_TITLE[WIDGET.DISEASE]} />
164-
</NavItem>
165-
</IndexLinkContainer>
166-
<IndexLinkContainer to={WIDGET.COMMENTS + urlQuery}
167-
active={useSelector((state) => state.widget.selectedWidget) === MENU_INDEX[WIDGET.COMMENTS]}>
168-
<NavItem eventKey={MENU_INDEX[WIDGET.COMMENTS]} onClick={onMenuItemClick}>
169-
<MenuItemWithIcon widget="comments" title={WIDGET_TITLE[WIDGET.COMMENTS]} />
170-
</NavItem>
171-
</IndexLinkContainer>
219+
<Nav bsStyle="pills" stacked>
220+
<NavItem
221+
active={selectedWidget === MENU_INDEX[WIDGET.OVERVIEW]}
222+
onClick={(e) => {
223+
e.preventDefault();
224+
handleNavigationAttempt(MENU_INDEX[WIDGET.OVERVIEW], WIDGET.OVERVIEW);
225+
}}
226+
>
227+
<MenuItemWithIcon widget="overview" title={WIDGET_TITLE[WIDGET.OVERVIEW]} />
228+
</NavItem>
229+
<NavItem
230+
active={selectedWidget === MENU_INDEX[WIDGET.GENETICS]}
231+
onClick={(e) => {
232+
e.preventDefault();
233+
handleNavigationAttempt(MENU_INDEX[WIDGET.GENETICS], WIDGET.GENETICS);
234+
}}
235+
>
236+
<MenuItemWithIcon widget="genetics" title={WIDGET_TITLE[WIDGET.GENETICS]} />
237+
</NavItem>
238+
<NavItem
239+
active={selectedWidget === MENU_INDEX[WIDGET.REAGENT]}
240+
onClick={(e) => {
241+
e.preventDefault();
242+
handleNavigationAttempt(MENU_INDEX[WIDGET.REAGENT], WIDGET.REAGENT);
243+
}}
244+
>
245+
<MenuItemWithIcon widget="reagent" title={WIDGET_TITLE[WIDGET.REAGENT]} />
246+
</NavItem>
247+
<NavItem
248+
active={selectedWidget === MENU_INDEX[WIDGET.EXPRESSION]}
249+
onClick={(e) => {
250+
e.preventDefault();
251+
handleNavigationAttempt(MENU_INDEX[WIDGET.EXPRESSION], WIDGET.EXPRESSION);
252+
}}
253+
>
254+
<MenuItemWithIcon widget="expression" title={WIDGET_TITLE[WIDGET.EXPRESSION]} />
255+
</NavItem>
256+
<NavItem
257+
active={selectedWidget === MENU_INDEX[WIDGET.INTERACTIONS]}
258+
onClick={(e) => {
259+
e.preventDefault();
260+
handleNavigationAttempt(MENU_INDEX[WIDGET.INTERACTIONS], WIDGET.INTERACTIONS);
261+
}}
262+
>
263+
<MenuItemWithIcon widget="interactions" title={WIDGET_TITLE[WIDGET.INTERACTIONS]} />
264+
</NavItem>
265+
<NavItem
266+
active={selectedWidget === MENU_INDEX[WIDGET.PHENOTYPES]}
267+
onClick={(e) => {
268+
e.preventDefault();
269+
handleNavigationAttempt(MENU_INDEX[WIDGET.PHENOTYPES], WIDGET.PHENOTYPES);
270+
}}
271+
>
272+
<MenuItemWithIcon widget="phenotypes" title={WIDGET_TITLE[WIDGET.PHENOTYPES]} />
273+
</NavItem>
274+
<NavItem
275+
active={selectedWidget === MENU_INDEX[WIDGET.DISEASE]}
276+
onClick={(e) => {
277+
e.preventDefault();
278+
handleNavigationAttempt(MENU_INDEX[WIDGET.DISEASE], WIDGET.DISEASE);
279+
}}
280+
>
281+
<MenuItemWithIcon widget="disease" title={WIDGET_TITLE[WIDGET.DISEASE]} />
282+
</NavItem>
283+
<NavItem
284+
active={selectedWidget === MENU_INDEX[WIDGET.COMMENTS]}
285+
onClick={(e) => {
286+
e.preventDefault();
287+
handleNavigationAttempt(MENU_INDEX[WIDGET.COMMENTS], WIDGET.COMMENTS);
288+
}}
289+
>
290+
<MenuItemWithIcon widget="comments" title={WIDGET_TITLE[WIDGET.COMMENTS]} />
291+
</NavItem>
172292
</Nav>
173293
</div>
174294
</div>
@@ -204,4 +324,4 @@ const Menu = ({urlQuery, onMenuItemClick = () => {}}) => {
204324
);
205325
}
206326

207-
export default Menu;
327+
export default withRouter(Menu);

src/frontend/submission_form/src/widgets/Disease.js

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {useDispatch, useSelector} from "react-redux";
1212
import {getCheckboxDBVal, transformEntitiesIntoAfpString} from "../AFPValues";
1313
import {saveWidgetData, saveWidgetDataSilently} from "../redux/actions/widgetActions";
1414
import {WIDGET} from "../constants";
15+
import SaveButton from "../components/SaveButton";
1516

1617
const Disease = () => {
1718
const dispatch = useDispatch();
@@ -123,24 +124,24 @@ Diabetes"
123124
</Panel>
124125
)}
125126
<div align="right">
126-
<Button bsStyle="primary" bsSize="small" onClick={() => {
127-
// Validate that if disease is checked, at least one disease name is selected
128-
if (disease.checked && diseaseNames.length === 0) {
129-
setShowDiseaseRequiredModal(true);
130-
return;
131-
}
132-
133-
// Filter out "checked" from details - it's a legacy value
134-
const diseaseDetails = (disease.details === "checked") ? "" : disease.details;
135-
const payload = {
136-
disease: getCheckboxDBVal(disease.checked, diseaseDetails),
127+
<SaveButton
128+
payload={{
129+
disease: getCheckboxDBVal(disease.checked, (disease.details === "checked") ? "" : disease.details),
137130
disease_list: diseaseNames,
138131
person_id: "two" + person.personId,
139132
passwd: paperPassword
140-
};
141-
dispatch(saveWidgetData(payload, WIDGET.DISEASE));
142-
}}>Save and go to next section
143-
</Button>
133+
}}
134+
widgetName={WIDGET.DISEASE}
135+
buttonText="Save and go to next section"
136+
onBeforeSave={() => {
137+
// Validate that if disease is checked, at least one disease name is selected
138+
if (disease.checked && diseaseNames.length === 0) {
139+
setShowDiseaseRequiredModal(true);
140+
return false;
141+
}
142+
return true;
143+
}}
144+
/>
144145
</div>
145146

146147
<DiseaseRequiredModal

src/frontend/submission_form/src/widgets/Expression.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
import {getCheckboxDBVal} from "../AFPValues";
2121
import {WIDGET} from "../constants";
2222
import {saveWidgetData, saveWidgetDataSilently} from "../redux/actions/widgetActions";
23+
import SaveButton from "../components/SaveButton";
2324

2425
const Expression = () =>{
2526
const dispatch = useDispatch();
@@ -140,17 +141,17 @@ const Expression = () =>{
140141
</Panel.Body>
141142
</Panel>
142143
<div align="right">
143-
<Button bsStyle="primary" bsSize="small" onClick={() => {
144-
let payload = {
144+
<SaveButton
145+
payload={{
145146
anatomic_expr: getCheckboxDBVal(expression.checked, expression.details),
146147
site_action: getCheckboxDBVal(siteOfAction.checked, siteOfAction.details),
147148
time_action: getCheckboxDBVal(timeOfAction.checked, timeOfAction.details),
148149
additional_expr: getCheckboxDBVal(additionalExpr.checked, additionalExpr.details),
149150
passwd: paperPassword
150-
};
151-
dispatch(saveWidgetData(payload, WIDGET.EXPRESSION));
152-
}}>Save and go to next section
153-
</Button>
151+
}}
152+
widgetName={WIDGET.EXPRESSION}
153+
buttonText="Save and go to next section"
154+
/>
154155
</div>
155156
</div>
156157
);

src/frontend/submission_form/src/widgets/Genetics.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import ControlLabel from "react-bootstrap/lib/ControlLabel";
2424
import Modal from "react-bootstrap/lib/Modal";
2525
import PropTypes from "prop-types";
2626
import axios from "axios";
27+
import SaveButton from "../components/SaveButton";
2728

2829
const Genetics = ({hideAlleles, hideStrains, toggleEntityVisibilityCallback}) => {
2930
const dispatch = useDispatch();
@@ -381,18 +382,18 @@ const Genetics = ({hideAlleles, hideStrains, toggleEntityVisibilityCallback}) =>
381382
</Panel>
382383
</form>
383384
<div align="right">
384-
<Button bsStyle="primary" bsSize="small" onClick={() => {
385-
let payload = {
385+
<SaveButton
386+
payload={{
386387
alleles_list: transformEntitiesIntoAfpString(alleles, ""),
387388
allele_seq_change: getCheckboxDBVal(sequenceChange.checked),
388389
other_alleles: JSON.stringify(otherAlleles),
389390
strains_list: transformEntitiesIntoAfpString(strains, ""),
390391
other_strains: JSON.stringify(otherStrains),
391392
passwd: paperPassword
392-
};
393-
dispatch(saveWidgetData(payload, WIDGET.GENETICS));
394-
}}>Save and go to next section
395-
</Button>
393+
}}
394+
widgetName={WIDGET.GENETICS}
395+
buttonText="Save and go to next section"
396+
/>
396397
</div>
397398
<Modal show={strainAlreadyPresentError} onHide={() => dispatch(setStrainAlreadyPresentError(false))}>
398399
<Modal.Header closeButton>

0 commit comments

Comments
 (0)