Skip to content

Commit 37aeb02

Browse files
committed
Fix duplicate mission scheduling from a single click
The inspection-area verification dialog fired the schedule request from a useEffect that ran on mount. Under React StrictMode (dev) this effect is invoked twice, sending two POST /missions/schedule requests per action and creating two MissionRuns. Move the scheduling request out of the effect and into the click handler. The dialog is now a presentational component that only decides which verification message to show; the decision (getInspectionAreaDialogType) is computed at the call site, and the request is fired exactly once from the user action. When no verification is needed, the request fires directly without opening a dialog.
1 parent 050ef9a commit 37aeb02

4 files changed

Lines changed: 98 additions & 65 deletions

File tree

frontend/src/components/Displays/InspectionAreaVerificationDialogs/ScheduleMissionWithInspectionAreaVerification.tsx

Lines changed: 20 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,46 @@
1-
import { useEffect } from 'react'
21
import {
32
ConflictingMissionInspectionAreasDialog,
43
ConflictingRobotInspectionAreaDialog,
54
} from './ConflictingInspectionAreaDialog'
65
import { UnknownInspectionAreaDialog } from './UnknownInspectionAreaDialog'
7-
import { useAssetContext } from 'components/Contexts/AssetContext'
86
import { InspectionArea } from 'models/InspectionArea'
7+
import { RobotWithoutTelemetry } from 'models/Robot'
8+
import { getUniqueInspectionAreas, InspectionAreaDialogType } from './getInspectionAreaDialogType'
99

1010
interface IProps {
11-
scheduleMissions: () => void
11+
dialogType: InspectionAreaDialogType
1212
closeDialog: () => void
13-
robotId: string
13+
robot: RobotWithoutTelemetry | undefined
1414
missionInspectionAreas: InspectionArea[]
1515
}
1616

17-
enum DialogTypes {
18-
unknownNewInspectionArea,
19-
conflictingMissionInspectionAreas,
20-
conflictingRobotInspectionArea,
21-
unknown,
22-
}
23-
2417
export const ScheduleMissionWithInspectionAreaVerification = ({
25-
robotId,
26-
missionInspectionAreas,
27-
scheduleMissions,
18+
dialogType,
2819
closeDialog,
20+
robot,
21+
missionInspectionAreas,
2922
}: IProps) => {
30-
const { enabledRobots } = useAssetContext()
31-
32-
const unikMissionInspectionAreas = missionInspectionAreas.filter(
33-
(inspectionArea, index, self) => self.findIndex((i) => i.id === inspectionArea.id) === index
23+
const uniqueMissionInspectionAreaNames = getUniqueInspectionAreas(missionInspectionAreas).map(
24+
(area) => area?.inspectionAreaName ?? ''
3425
)
3526

36-
const selectedRobot = enabledRobots.find((robot) => robot.id === robotId)
37-
38-
const getDialogToOpen = (): DialogTypes | undefined => {
39-
if (!selectedRobot) return DialogTypes.unknown
40-
if (unikMissionInspectionAreas.length > 1) return DialogTypes.conflictingMissionInspectionAreas
41-
if (unikMissionInspectionAreas.length === 0) return DialogTypes.unknownNewInspectionArea
42-
if (
43-
selectedRobot.currentInspectionAreaId &&
44-
unikMissionInspectionAreas[0]?.id !== selectedRobot.currentInspectionAreaId
45-
) {
46-
return DialogTypes.conflictingRobotInspectionArea
47-
}
48-
return undefined
49-
}
50-
51-
const resolvedDialog = getDialogToOpen()
52-
const dialogToOpen = resolvedDialog ?? DialogTypes.unknown
53-
const shouldScheduleDirectly = !!selectedRobot && resolvedDialog === undefined
54-
55-
useEffect(() => {
56-
if (shouldScheduleDirectly) scheduleMissions()
57-
}, [shouldScheduleDirectly])
58-
59-
const unikMissionInspectionAreaNames = unikMissionInspectionAreas.map((area) => area?.inspectionAreaName ?? '')
60-
6127
return (
6228
<>
63-
{dialogToOpen === DialogTypes.conflictingMissionInspectionAreas && (
29+
{dialogType === InspectionAreaDialogType.conflictingMissionInspectionAreas && (
6430
<ConflictingMissionInspectionAreasDialog
6531
closeDialog={closeDialog}
66-
missionInspectionAreaNames={unikMissionInspectionAreaNames}
67-
/>
68-
)}
69-
{dialogToOpen === DialogTypes.conflictingRobotInspectionArea && selectedRobot?.currentInspectionAreaId && (
70-
<ConflictingRobotInspectionAreaDialog
71-
closeDialog={closeDialog}
72-
robotInspectionAreaId={selectedRobot?.currentInspectionAreaId}
73-
desiredInspectionAreaName={unikMissionInspectionAreaNames![0]}
32+
missionInspectionAreaNames={uniqueMissionInspectionAreaNames}
7433
/>
7534
)}
76-
{dialogToOpen === DialogTypes.unknownNewInspectionArea && (
35+
{dialogType === InspectionAreaDialogType.conflictingRobotInspectionArea &&
36+
robot?.currentInspectionAreaId && (
37+
<ConflictingRobotInspectionAreaDialog
38+
closeDialog={closeDialog}
39+
robotInspectionAreaId={robot.currentInspectionAreaId}
40+
desiredInspectionAreaName={uniqueMissionInspectionAreaNames[0]}
41+
/>
42+
)}
43+
{dialogType === InspectionAreaDialogType.unknownNewInspectionArea && (
7744
<UnknownInspectionAreaDialog closeDialog={closeDialog} />
7845
)}
7946
</>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { InspectionArea } from 'models/InspectionArea'
2+
import { RobotWithoutTelemetry } from 'models/Robot'
3+
4+
export enum InspectionAreaDialogType {
5+
unknownNewInspectionArea,
6+
conflictingMissionInspectionAreas,
7+
conflictingRobotInspectionArea,
8+
unknown,
9+
}
10+
11+
export const getUniqueInspectionAreas = (inspectionAreas: InspectionArea[]): InspectionArea[] =>
12+
inspectionAreas.filter((area, index, self) => self.findIndex((i) => i.id === area.id) === index)
13+
14+
/**
15+
* Decides which inspection-area verification dialog (if any) must be shown before a
16+
* mission can be scheduled. Returns null when scheduling can proceed directly.
17+
*
18+
* Kept as a pure function so the decision happens in the click handler rather than in a
19+
* render effect, which avoids duplicate scheduling requests under React StrictMode.
20+
*/
21+
export const getInspectionAreaDialogType = (
22+
robot: RobotWithoutTelemetry | undefined,
23+
missionInspectionAreas: InspectionArea[]
24+
): InspectionAreaDialogType | null => {
25+
if (!robot) return InspectionAreaDialogType.unknown
26+
27+
const uniqueInspectionAreas = getUniqueInspectionAreas(missionInspectionAreas)
28+
29+
if (uniqueInspectionAreas.length > 1) return InspectionAreaDialogType.conflictingMissionInspectionAreas
30+
if (uniqueInspectionAreas.length === 0) return InspectionAreaDialogType.unknownNewInspectionArea
31+
if (robot.currentInspectionAreaId && uniqueInspectionAreas[0]?.id !== robot.currentInspectionAreaId)
32+
return InspectionAreaDialogType.conflictingRobotInspectionArea
33+
34+
return null
35+
}

frontend/src/components/Displays/MissionButtons/MissionRestartButton.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,13 @@ import { FailedRequestAlertContent, FailedRequestAlertListContent } from 'compon
99
import { Mission } from 'models/Mission'
1010
import { AlertCategory } from 'components/Alerts/AlertsBanner'
1111
import { ScheduleMissionWithInspectionAreaVerification } from '../InspectionAreaVerificationDialogs/ScheduleMissionWithInspectionAreaVerification'
12+
import {
13+
getInspectionAreaDialogType,
14+
InspectionAreaDialogType,
15+
} from '../InspectionAreaVerificationDialogs/getInspectionAreaDialogType'
1216
import { useBackendApi } from 'api/UseBackendApi'
1317
import { InstallationContext } from 'components/Contexts/InstallationContext'
18+
import { useAssetContext } from 'components/Contexts/AssetContext'
1419

1520
const Centered = styled.div`
1621
display: flex;
@@ -37,12 +42,15 @@ enum ReRunOptions {
3742
export const MissionRestartButton = ({ mission, hasFailedTasks, smallButton }: MissionProps) => {
3843
const { TranslateText } = useLanguageContext()
3944
const { installation } = useContext(InstallationContext)
45+
const { enabledRobots } = useAssetContext()
4046
const { setAlert, setListAlert } = useAlertContext()
4147
const [isOpen, setIsOpen] = useState<boolean>(false)
4248
const [isLocationVerificationOpen, setIsLocationVerificationOpen] = useState<boolean>(false)
43-
const [selectedRerunOption, setSelectedRerunOption] = useState<ReRunOptions>()
49+
const [verificationDialogType, setVerificationDialogType] = useState<InspectionAreaDialogType | null>(null)
4450
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null)
4551

52+
const liveRobot = enabledRobots.find((robot) => robot.id === mission.robot.id)
53+
4654
const navigate = useNavigate()
4755
const navigateToHome = () => {
4856
const path = `/${installation.installationCode}`
@@ -70,7 +78,14 @@ export const MissionRestartButton = ({ mission, hasFailedTasks, smallButton }: M
7078
}
7179

7280
const selectRerunOption = (rerunOption: ReRunOptions) => {
73-
setSelectedRerunOption(rerunOption)
81+
const dialogType = getInspectionAreaDialogType(liveRobot, [mission.inspectionArea])
82+
83+
if (dialogType === null) {
84+
startReRun(rerunOption)
85+
return
86+
}
87+
88+
setVerificationDialogType(dialogType)
7489
setIsLocationVerificationOpen(true)
7590
}
7691

@@ -116,11 +131,11 @@ export const MissionRestartButton = ({ mission, hasFailedTasks, smallButton }: M
116131
)}
117132
</Menu>
118133
</EdsProvider>
119-
{isLocationVerificationOpen && (
134+
{isLocationVerificationOpen && verificationDialogType !== null && (
120135
<ScheduleMissionWithInspectionAreaVerification
121-
scheduleMissions={() => startReRun(selectedRerunOption!)}
136+
dialogType={verificationDialogType}
122137
closeDialog={() => setIsLocationVerificationOpen(false)}
123-
robotId={mission.robot.id}
138+
robot={liveRobot}
124139
missionInspectionAreas={[mission.inspectionArea]}
125140
/>
126141
)}

frontend/src/pages/InspectionPage/ScheduleMissionDialogs.tsx

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ import { FailedRequestAlertContent, FailedRequestAlertListContent } from 'compon
1212
import { AlertType, useAlertContext } from 'components/Contexts/AlertContext'
1313
import { AlertCategory } from 'components/Alerts/AlertsBanner'
1414
import { ScheduleMissionWithInspectionAreaVerification } from 'components/Displays/InspectionAreaVerificationDialogs/ScheduleMissionWithInspectionAreaVerification'
15+
import {
16+
getInspectionAreaDialogType,
17+
InspectionAreaDialogType,
18+
} from 'components/Displays/InspectionAreaVerificationDialogs/getInspectionAreaDialogType'
1519
import { phone_width } from 'utils/constants'
1620
import { useBackendApi } from 'api/UseBackendApi'
1721

@@ -61,6 +65,7 @@ export const ScheduleMissionDialog = (props: IProps) => {
6165
const { setLoadingRobotMissionSet } = useMissionsContext()
6266
const { setAlert, setListAlert } = useAlertContext()
6367
const [isInspectionAreaVerificationDialogOpen, setIsInspectionAreaVerificationDialogOpen] = useState<boolean>(false)
68+
const [verificationDialogType, setVerificationDialogType] = useState<InspectionAreaDialogType | null>(null)
6469
const [missionsToSchedule, setMissionsToSchedule] = useState<MissionDefinition[]>()
6570
const backendApi = useBackendApi()
6671
const filteredRobots = enabledRobots.filter(
@@ -82,16 +87,27 @@ export const ScheduleMissionDialog = (props: IProps) => {
8287
const onScheduleButtonPress = (missions: MissionDefinition[]) => () => {
8388
if (!selectedRobot) return
8489

90+
const dialogType = getInspectionAreaDialogType(
91+
selectedRobot,
92+
missions.map((mission) => mission.inspectionArea)
93+
)
94+
95+
if (dialogType === null) {
96+
scheduleMissions(missions)
97+
return
98+
}
99+
85100
setMissionsToSchedule(missions)
101+
setVerificationDialogType(dialogType)
86102
setIsInspectionAreaVerificationDialogOpen(true)
87103
}
88104

89-
const scheduleMissions = () => {
105+
const scheduleMissions = (missions: MissionDefinition[]) => {
90106
setIsInspectionAreaVerificationDialogOpen(false)
91107

92-
if (!selectedRobot || !missionsToSchedule) return
108+
if (!selectedRobot) return
93109

94-
missionsToSchedule.forEach((mission) => {
110+
missions.forEach((mission) => {
95111
backendApi.scheduleMissionDefinition(mission.id, selectedRobot.id).catch((e) => {
96112
setAlert(
97113
AlertType.RequestFail,
@@ -193,12 +209,12 @@ export const ScheduleMissionDialog = (props: IProps) => {
193209
</StyledDialogContent>
194210
</StyledDialog>
195211
</StyledMissionDialog>
196-
{isInspectionAreaVerificationDialogOpen && (
212+
{isInspectionAreaVerificationDialogOpen && verificationDialogType !== null && (
197213
<ScheduleMissionWithInspectionAreaVerification
198-
scheduleMissions={scheduleMissions}
214+
dialogType={verificationDialogType}
199215
closeDialog={closeScheduleDialogs}
200-
robotId={selectedRobot!.id}
201-
missionInspectionAreas={props.selectedMissions.map((mission) => mission.inspectionArea)}
216+
robot={selectedRobot}
217+
missionInspectionAreas={missionsToSchedule?.map((mission) => mission.inspectionArea) ?? []}
202218
/>
203219
)}
204220
</>

0 commit comments

Comments
 (0)