Skip to content

Commit 4b9155b

Browse files
authored
TestRun. Save changes prompt is shown (#230)
* TestRun. Save changes prompt is shown closes Visual-Regression-Tracker/Visual-Regression-Tracker#297
1 parent a33af13 commit 4b9155b

File tree

4 files changed

+113
-64
lines changed

4 files changed

+113
-64
lines changed

src/components/TestDetailsDialog/ArrowButtons.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { IconButton, makeStyles, Tooltip } from "@material-ui/core";
22
import { NavigateNext, NavigateBefore } from "@material-ui/icons";
33
import React from "react";
44
import { useHotkeys } from "react-hotkeys-hook";
5-
import { useTestRunDispatch, selectTestRun } from "../../contexts";
65
import { TestRun } from "../../types";
76

87
const useStyles = makeStyles((theme) => ({
@@ -23,25 +22,26 @@ const useStyles = makeStyles((theme) => ({
2322
export const ArrowButtons: React.FunctionComponent<{
2423
testRuns: TestRun[];
2524
selectedTestRunIndex: number;
26-
}> = ({ testRuns, selectedTestRunIndex }) => {
25+
handleNavigation: (testRunId: string) => void;
26+
}> = ({ testRuns, selectedTestRunIndex, handleNavigation }) => {
2727
const classes = useStyles();
28-
const testRunDispatch = useTestRunDispatch();
2928

3029
const navigateNext = () => {
3130
if (selectedTestRunIndex + 1 < testRuns.length) {
3231
const next = testRuns[selectedTestRunIndex + 1];
33-
selectTestRun(testRunDispatch, next.id);
32+
handleNavigation(next.id);
3433
}
3534
};
36-
useHotkeys("right", navigateNext, [selectedTestRunIndex]);
3735

3836
const navigateBefore = () => {
3937
if (selectedTestRunIndex > 0) {
4038
const prev = testRuns[selectedTestRunIndex - 1];
41-
selectTestRun(testRunDispatch, prev.id);
39+
handleNavigation(prev.id);
4240
}
4341
};
44-
useHotkeys("left", navigateBefore, [selectedTestRunIndex]);
42+
43+
useHotkeys("right", navigateNext, [selectedTestRunIndex, handleNavigation]);
44+
useHotkeys("left", navigateBefore, [selectedTestRunIndex, handleNavigation]);
4545

4646
return (
4747
<React.Fragment>

src/components/TestDetailsDialog/TestDetailsModal.tsx

Lines changed: 46 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { useHotkeys } from "react-hotkeys-hook";
1919
import { TestRun } from "../../types";
2020
import { testRunService, staticService } from "../../services";
2121
import { TestStatus } from "../../types/testStatus";
22-
import { useHistory, Prompt } from "react-router-dom";
22+
import { useHistory } from "react-router-dom";
2323
import { IgnoreArea, UpdateIgnoreAreaDto } from "../../types/ignoreArea";
2424
import { KonvaEventObject } from "konva/types/Node";
2525
import {
@@ -34,7 +34,7 @@ import {
3434
import { TestRunDetails } from "../TestRunDetails";
3535
import useImage from "use-image";
3636
import { routes } from "../../constants";
37-
import { useTestRunDispatch, selectTestRun } from "../../contexts";
37+
import { useTestRunDispatch } from "../../contexts";
3838
import { DrawArea } from "../DrawArea";
3939
import { CommentsPopper } from "../CommentsPopper";
4040
import { useSnackbar } from "notistack";
@@ -61,7 +61,9 @@ const useStyles = makeStyles((theme) => ({
6161

6262
const TestDetailsModal: React.FunctionComponent<{
6363
testRun: TestRun;
64-
}> = ({ testRun }) => {
64+
touched: boolean;
65+
handleClose: () => void;
66+
}> = ({ testRun, touched, handleClose }) => {
6567
const classes = useStyles();
6668
const history = useHistory();
6769
const { enqueueSnackbar } = useSnackbar();
@@ -75,6 +77,13 @@ const TestDetailsModal: React.FunctionComponent<{
7577
const [stageInitPos, setStageInitPos] = React.useState(defaultStagePos);
7678
const [stageOffset, setStageOffset] = React.useState(defaultStagePos);
7779
const [processing, setProcessing] = React.useState(false);
80+
const [isDrawMode, setIsDrawMode] = useState(false);
81+
const [valueOfIgnoreOrCompare, setValueOfIgnoreOrCompare] = useState(
82+
"Ignore Areas"
83+
);
84+
const [isDiffShown, setIsDiffShown] = useState(!!testRun.diffName);
85+
const [selectedRectId, setSelectedRectId] = React.useState<string>();
86+
const [ignoreAreas, setIgnoreAreas] = React.useState<IgnoreArea[]>([]);
7887

7988
const [image, imageStatus] = useImage(
8089
staticService.getImage(testRun.imageName)
@@ -86,17 +95,32 @@ const TestDetailsModal: React.FunctionComponent<{
8695
staticService.getImage(testRun.diffName)
8796
);
8897

89-
const [isDrawMode, setIsDrawMode] = useState(false);
90-
const [valueOfIgnoreOrCompare, setValueOfIgnoreOrCompare] = useState(
91-
"Ignore Areas"
92-
);
93-
const [isDiffShown, setIsDiffShown] = useState(false);
94-
const [selectedRectId, setSelectedRectId] = React.useState<string>();
98+
React.useEffect(() => {
99+
fitStageToScreen();
100+
// eslint-disable-next-line
101+
}, [image]);
95102

96-
const [ignoreAreas, setIgnoreAreas] = React.useState<IgnoreArea[]>(
97-
JSON.parse(testRun.ignoreAreas)
103+
React.useEffect(() => {
104+
setIgnoreAreas(JSON.parse(testRun.ignoreAreas));
105+
}, [testRun]);
106+
107+
const isImageSizeDiffer = React.useMemo(
108+
() =>
109+
testRun.baselineName &&
110+
testRun.imageName &&
111+
(image?.height !== baselineImage?.height ||
112+
image?.width !== baselineImage?.width),
113+
[image, baselineImage, testRun.baselineName, testRun.imageName]
98114
);
99115

116+
const handleIgnoreAreaChange = (ignoreAreas: IgnoreArea[]) => {
117+
setIgnoreAreas(ignoreAreas);
118+
testRunDispatch({
119+
type: "touched",
120+
payload: testRun.ignoreAreas !== JSON.stringify(ignoreAreas),
121+
});
122+
};
123+
100124
const removeSelection = (event: KonvaEventObject<MouseEvent>) => {
101125
// deselect when clicked not on Rect
102126
const isRectClicked = event.target.className === "Rect";
@@ -106,7 +130,7 @@ const TestDetailsModal: React.FunctionComponent<{
106130
};
107131

108132
const deleteIgnoreArea = (id: string) => {
109-
setIgnoreAreas(ignoreAreas.filter((area) => area.id !== id));
133+
handleIgnoreAreaChange(ignoreAreas.filter((area) => area.id !== id));
110134
setSelectedRectId(undefined);
111135
};
112136

@@ -138,12 +162,13 @@ const TestDetailsModal: React.FunctionComponent<{
138162
head(ignoreAreas)
139163
);
140164

141-
setIgnoreAreas(invertedIgnoreAreas);
165+
handleIgnoreAreaChange(invertedIgnoreAreas);
142166
saveTestRun(
143167
invertedIgnoreAreas,
144168
"Selected area has been inverted to ignore areas and saved."
145169
);
146170
}
171+
testRunDispatch({ type: "touched", payload: false });
147172
};
148173

149174
const onIgnoreOrCompareSelectChange = (value: string) => {
@@ -154,14 +179,6 @@ const TestDetailsModal: React.FunctionComponent<{
154179
}
155180
};
156181

157-
const handleClose = () => {
158-
selectTestRun(testRunDispatch, undefined);
159-
};
160-
161-
const isIgnoreAreasSaved = () => {
162-
return testRun.ignoreAreas === JSON.stringify(ignoreAreas);
163-
};
164-
165182
const setOriginalSize = () => {
166183
setStageScale(1);
167184
resetPositioin();
@@ -220,42 +237,16 @@ const TestDetailsModal: React.FunctionComponent<{
220237
}
221238
};
222239

223-
React.useEffect(() => {
224-
setIgnoreAreas(JSON.parse(testRun.ignoreAreas));
225-
}, [testRun]);
226-
227-
React.useEffect(() => {
228-
fitStageToScreen();
229-
// eslint-disable-next-line
230-
}, [image]);
231-
232-
React.useEffect(() => {
233-
setIsDiffShown(!!testRun.diffName);
234-
}, [testRun.diffName]);
235-
236-
const isImageSizeDiffer = React.useMemo(
237-
() =>
238-
testRun.baselineName &&
239-
testRun.imageName &&
240-
(image?.height !== baselineImage?.height ||
241-
image?.width !== baselineImage?.width),
242-
[image, baselineImage, testRun.baselineName, testRun.imageName]
243-
);
244-
245240
useHotkeys(
246241
"d",
247242
() =>
248243
shouldDiffHotKeyBeActive && setIsDiffShown((isDiffShown) => !isDiffShown)
249244
);
250-
useHotkeys("ESC", () => handleClose());
245+
useHotkeys("ESC", handleClose, [handleClose]);
251246
const shouldDiffHotKeyBeActive = !!testRun.diffName;
252247

253248
return (
254249
<React.Fragment>
255-
<Prompt
256-
when={!isIgnoreAreasSaved()}
257-
message={`You have unsaved changes that will be lost`}
258-
/>
259250
<AppBar position="sticky">
260251
<Toolbar>
261252
<Grid container justify="space-between">
@@ -353,7 +344,9 @@ const TestDetailsModal: React.FunctionComponent<{
353344
<Grid item>
354345
<IconButton
355346
disabled={ignoreAreas.length === 0}
356-
onClick={() => setIgnoreAreas([])}
347+
onClick={() => {
348+
handleIgnoreAreaChange([]);
349+
}}
357350
>
358351
<LayersClear />
359352
</IconButton>
@@ -374,7 +367,7 @@ const TestDetailsModal: React.FunctionComponent<{
374367
</Tooltip>
375368
<Grid item>
376369
<IconButton
377-
disabled={isIgnoreAreasSaved()}
370+
disabled={!touched}
378371
onClick={() => saveIgnoreAreasOrCompareArea()}
379372
>
380373
<Save />
@@ -430,7 +423,7 @@ const TestDetailsModal: React.FunctionComponent<{
430423
imageState={[baselineImage, baselineImageStatus]}
431424
ignoreAreas={[]}
432425
tempIgnoreAreas={[]}
433-
setIgnoreAreas={setIgnoreAreas}
426+
setIgnoreAreas={handleIgnoreAreaChange}
434427
selectedRectId={selectedRectId}
435428
setSelectedRectId={setSelectedRectId}
436429
onStageClick={removeSelection}
@@ -450,7 +443,7 @@ const TestDetailsModal: React.FunctionComponent<{
450443
imageState={[diffImage, diffImageStatus]}
451444
ignoreAreas={ignoreAreas}
452445
tempIgnoreAreas={JSON.parse(testRun.tempIgnoreAreas)}
453-
setIgnoreAreas={setIgnoreAreas}
446+
setIgnoreAreas={handleIgnoreAreaChange}
454447
selectedRectId={selectedRectId}
455448
setSelectedRectId={setSelectedRectId}
456449
onStageClick={removeSelection}
@@ -468,7 +461,7 @@ const TestDetailsModal: React.FunctionComponent<{
468461
imageState={[image, imageStatus]}
469462
ignoreAreas={ignoreAreas}
470463
tempIgnoreAreas={JSON.parse(testRun.tempIgnoreAreas)}
471-
setIgnoreAreas={setIgnoreAreas}
464+
setIgnoreAreas={handleIgnoreAreaChange}
472465
selectedRectId={selectedRectId}
473466
setSelectedRectId={setSelectedRectId}
474467
onStageClick={removeSelection}

src/components/TestDetailsDialog/index.tsx

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
import { Dialog, makeStyles } from "@material-ui/core";
1+
import { Dialog, makeStyles, Typography } from "@material-ui/core";
22
import React from "react";
3-
import { useTestRunState } from "../../contexts";
3+
import {
4+
selectTestRun,
5+
useTestRunDispatch,
6+
useTestRunState,
7+
} from "../../contexts";
8+
import { BaseModal } from "../BaseModal";
49
import { ArrowButtons } from "./ArrowButtons";
510
import TestDetailsModal from "./TestDetailsModal";
611

@@ -13,11 +18,16 @@ const useStyles = makeStyles((theme) => ({
1318
export const TestDetailsDialog: React.FunctionComponent = () => {
1419
const classes = useStyles();
1520
const {
21+
testRun,
22+
touched,
1623
testRuns: allTestRuns,
1724
filteredTestRunIds,
1825
sortedTestRunIds,
1926
selectedTestRunId,
2027
} = useTestRunState();
28+
const testRunDispatch = useTestRunDispatch();
29+
const [notSavedChangesModal, setNotSavedChangesModal] = React.useState(false);
30+
const [navigationTargetId, setNavigationTargetId] = React.useState<string>();
2131

2232
const testRuns = React.useMemo(() => {
2333
const filtered = filteredTestRunIds
@@ -40,16 +50,46 @@ export const TestDetailsDialog: React.FunctionComponent = () => {
4050
[testRuns, selectedTestRunId]
4151
);
4252

43-
if (selectedTestRunIndex === undefined || !testRuns[selectedTestRunIndex]) {
53+
const handleNavigation = React.useCallback(
54+
(id?: string) => {
55+
if (touched) {
56+
setNavigationTargetId(id);
57+
setNotSavedChangesModal(true);
58+
} else {
59+
selectTestRun(testRunDispatch, id);
60+
}
61+
},
62+
[testRunDispatch, touched]
63+
);
64+
65+
if (!testRun) {
4466
return null;
4567
}
4668

4769
return (
4870
<Dialog open={true} fullScreen className={classes.modal}>
49-
<TestDetailsModal testRun={testRuns[selectedTestRunIndex]} />
71+
<TestDetailsModal
72+
testRun={testRun}
73+
touched={touched}
74+
handleClose={() => handleNavigation()}
75+
/>
5076
<ArrowButtons
5177
testRuns={testRuns}
5278
selectedTestRunIndex={selectedTestRunIndex}
79+
handleNavigation={handleNavigation}
80+
/>
81+
<BaseModal
82+
open={notSavedChangesModal}
83+
title={"You have not saved ignore areas"}
84+
submitButtonText={"Discard"}
85+
onCancel={() => setNotSavedChangesModal(false)}
86+
content={
87+
<Typography>{`Are you sure you want to discard changes?`}</Typography>
88+
}
89+
onSubmit={() => {
90+
selectTestRun(testRunDispatch, navigationTargetId);
91+
setNotSavedChangesModal(false);
92+
}}
5393
/>
5494
</Dialog>
5595
);

src/contexts/testRun.context.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ interface ISortAction {
5757
payload?: Array<string | number>;
5858
}
5959

60+
interface ITouchedAction {
61+
type: "touched";
62+
payload: boolean;
63+
}
64+
6065
type IAction =
6166
| IRequestAction
6267
| IGetAction
@@ -67,6 +72,7 @@ type IAction =
6772
| IRejectAction
6873
| IFilterAction
6974
| ISortAction
75+
| ITouchedAction
7076
| ISelectAction;
7177

7278
type Dispatch = (action: IAction) => void;
@@ -75,6 +81,8 @@ type State = {
7581
sortedTestRunIds?: Array<string | number>;
7682
filteredTestRunIds?: Array<string | number>;
7783
testRuns: Array<TestRun>;
84+
testRun?: TestRun;
85+
touched: boolean;
7886
loading: boolean;
7987
};
8088

@@ -86,6 +94,7 @@ const TestRunDispatchContext = React.createContext<Dispatch | undefined>(
8694
);
8795

8896
const initialState: State = {
97+
touched: false,
8998
testRuns: [],
9099
loading: false,
91100
};
@@ -95,7 +104,9 @@ function testRunReducer(state: State, action: IAction): State {
95104
case "select":
96105
return {
97106
...state,
107+
touched: false,
98108
selectedTestRunId: action.payload,
109+
testRun: state.testRuns.find((item) => item.id === action.payload),
99110
};
100111
case "filter":
101112
return {
@@ -146,6 +157,11 @@ function testRunReducer(state: State, action: IAction): State {
146157
return t;
147158
}),
148159
};
160+
case "touched":
161+
return {
162+
...state,
163+
touched: action.payload,
164+
};
149165
default:
150166
return state;
151167
}

0 commit comments

Comments
 (0)