Skip to content

Commit

Permalink
feat(app): warning modal if steps aren't complete
Browse files Browse the repository at this point in the history
Add a warning modal for steps that have not yet been completed when you
proceed to run.
  • Loading branch information
sfoster1 committed Aug 6, 2024
1 parent 5ab7f03 commit b4c2a4b
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 14 deletions.
67 changes: 67 additions & 0 deletions app/src/organisms/Devices/ProtocolRun/ConfirmMissingStepsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import {
ALIGN_CENTER,
DIRECTION_COLUMN,
DIRECTION_ROW,
Flex,
JUSTIFY_FLEX_END,
Link,

Check failure on line 9 in app/src/organisms/Devices/ProtocolRun/ConfirmMissingStepsModal.tsx

View workflow job for this annotation

GitHub Actions / js checks

'Link' is defined but never used

Check failure on line 9 in app/src/organisms/Devices/ProtocolRun/ConfirmMissingStepsModal.tsx

View workflow job for this annotation

GitHub Actions / js checks

'Link' is defined but never used
PrimaryButton,
SecondaryButton,
SPACING,
LegacyStyledText,
TEXT_ALIGN_CENTER,

Check failure on line 14 in app/src/organisms/Devices/ProtocolRun/ConfirmMissingStepsModal.tsx

View workflow job for this annotation

GitHub Actions / js checks

'TEXT_ALIGN_CENTER' is defined but never used

Check failure on line 14 in app/src/organisms/Devices/ProtocolRun/ConfirmMissingStepsModal.tsx

View workflow job for this annotation

GitHub Actions / js checks

'TEXT_ALIGN_CENTER' is defined but never used
TYPOGRAPHY,
} from '@opentrons/components'
import { LegacyModal } from '../../../molecules/LegacyModal'

interface ConfirmMissingStepsModalProps {
onCloseClick: () => void
onConfirmClick: () => void
missingSteps: string[]
}
export const ConfirmMissingStepsModal = (
props: ConfirmMissingStepsModalProps
): JSX.Element | null => {
const { missingSteps, onCloseClick, onConfirmClick } = props
const { t, i18n } = useTranslation(['protocol_setup', 'shared'])

const confirmAttached = (): void => {
onConfirmClick()
onCloseClick()
}

return (
<LegacyModal
title={t('are_you_sure_you_want_to_proceed')}
type="warning"
onClose={onCloseClick}
>
<Flex flexDirection={DIRECTION_COLUMN} fontSize={TYPOGRAPHY.fontSizeP}>
<LegacyStyledText paddingBottom={SPACING.spacing4}>
{t('you_havent_confirmed', {
missingSteps: new Intl.ListFormat('en', {
style: 'short',
type: 'conjunction',
}).format(missingSteps.map(step => t(step))),
})}
</LegacyStyledText>
</Flex>
<Flex
flexDirection={DIRECTION_ROW}
paddingTop={SPACING.spacing32}
justifyContent={JUSTIFY_FLEX_END}
alignItems={ALIGN_CENTER}
gap={SPACING.spacing8}
>
<SecondaryButton onClick={onCloseClick}>
{i18n.format(t('shared:go_back'), 'capitalize')}
</SecondaryButton>
<PrimaryButton onClick={confirmAttached}>
{t('start_run')}
</PrimaryButton>
</Flex>
</LegacyModal>
)
}
35 changes: 31 additions & 4 deletions app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import {
} from '../../../organisms/RunTimeControl/hooks'
import { useIsHeaterShakerInProtocol } from '../../ModuleCard/hooks'
import { ConfirmAttachmentModal } from '../../ModuleCard/ConfirmAttachmentModal'
import { ConfirmMissingStepsModal } from './ConfirmMissingStepsModal'
import {
useProtocolDetailsForRun,
useProtocolAnalysisErrors,
Expand Down Expand Up @@ -132,13 +133,15 @@ interface ProtocolRunHeaderProps {
robotName: string
runId: string
makeHandleJumpToStep: (index: number) => () => void
missingSetupSteps: string[]
}

export function ProtocolRunHeader({
protocolRunHeaderRef,
robotName,
runId,
makeHandleJumpToStep,
missingSetupSteps,
}: ProtocolRunHeaderProps): JSX.Element | null {
const { t } = useTranslation(['run_details', 'shared'])
const navigate = useNavigate()
Expand Down Expand Up @@ -447,6 +450,7 @@ export function ProtocolRunHeader({
isDoorOpen={isDoorOpen}
isFixtureMismatch={isFixtureMismatch}
isResetRunLoadingRef={isResetRunLoadingRef}
missingSetupSteps={missingSetupSteps}
/>
</Flex>
</Box>
Expand Down Expand Up @@ -591,6 +595,7 @@ interface ActionButtonProps {
isDoorOpen: boolean
isFixtureMismatch: boolean
isResetRunLoadingRef: React.MutableRefObject<boolean>
missingSetupSteps: string[]
}

// TODO(jh, 04-22-2024): Refactor switch cases into separate factories to increase readability and testability.
Expand All @@ -603,6 +608,7 @@ function ActionButton(props: ActionButtonProps): JSX.Element {
isDoorOpen,
isFixtureMismatch,
isResetRunLoadingRef,
missingSetupSteps,
} = props
const navigate = useNavigate()
const { t } = useTranslation(['run_details', 'shared'])
Expand Down Expand Up @@ -682,12 +688,20 @@ function ActionButton(props: ActionButtonProps): JSX.Element {
)
const {
confirm: confirmAttachment,
showConfirmation: showConfirmationModal,
cancel: cancelExit,
showConfirmation: showHSConfirmationModal,
cancel: cancelExitHSConfirmation,
} = useConditionalConfirm(
handleProceedToRunClick,
!configBypassHeaterShakerAttachmentConfirmation
)
const {
confirm: confirmMissingSteps,
showConfirmation: showMissingStepsConfirmationModal,
cancel: cancelExitMissingStepsConfirmation,
} = useConditionalConfirm(
handleProceedToRunClick,
missingSetupSteps.length !== 0
)
const robotAnalyticsData = useRobotAnalyticsData(robotName)

const isHeaterShakerInProtocol = useIsHeaterShakerInProtocol()
Expand Down Expand Up @@ -745,6 +759,11 @@ function ActionButton(props: ActionButtonProps): JSX.Element {
handleButtonClick = () => {
if (isHeaterShakerShaking && isHeaterShakerInProtocol) {
setShowIsShakingModal(true)
} else if (
missingSetupSteps.length !== 0 &&
(runStatus === RUN_STATUS_IDLE || runStatus === RUN_STATUS_STOPPED)
) {
confirmMissingSteps()
} else if (
isHeaterShakerInProtocol &&
!isHeaterShakerShaking &&
Expand Down Expand Up @@ -825,13 +844,21 @@ function ActionButton(props: ActionButtonProps): JSX.Element {
startRun={play}
/>
)}
{showConfirmationModal && (
{showHSConfirmationModal && (
<ConfirmAttachmentModal
onCloseClick={cancelExit}
onCloseClick={cancelExitHSConfirmation}
isProceedToRunModal={true}
onConfirmClick={handleProceedToRunClick}
/>
)}
{showMissingStepsConfirmationModal && (
<ConfirmMissingStepsModal
onCloseClick={cancelExitMissingStepsConfirmation}
onConfirmClick={handleProceedToRunClick}
missingSteps={missingSetupSteps}
/>
)}
{}
</>
)
}
Expand Down
28 changes: 27 additions & 1 deletion app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,33 @@ export type StepKey =
| typeof LABWARE_SETUP_KEY
| typeof LIQUID_SETUP_KEY

export type MissingStep =
| 'applied_labware_offsets'
| 'labware_placement'
| 'liquids'

export type MissingSteps = MissingStep[]

export const initialMissingSteps = (): MissingSteps => [
'applied_labware_offsets',
'labware_placement',
'liquids',
]

interface ProtocolRunSetupProps {
protocolRunHeaderRef: React.RefObject<HTMLDivElement> | null
robotName: string
runId: string
setMissingSteps: (missingSteps: MissingSteps) => void
missingSteps: MissingSteps
}

export function ProtocolRunSetup({
protocolRunHeaderRef,
robotName,
runId,
setMissingSteps,
missingSteps,
}: ProtocolRunSetupProps): JSX.Element | null {
const { t, i18n } = useTranslation('protocol_setup')
const robotProtocolAnalysis = useMostRecentCompletedAnalysis(runId)
Expand Down Expand Up @@ -246,8 +263,13 @@ export function ProtocolRunSetup({
<SetupLabwarePositionCheck
{...{ runId, robotName }}
setOffsetsConfirmed={confirmed => {
confirmed && setExpandedStepKey(LABWARE_SETUP_KEY)
setLpcComplete(confirmed)
if (confirmed) {
setExpandedStepKey(LABWARE_SETUP_KEY)
setMissingSteps(
missingSteps.filter(step => step !== 'applied_labware_offsets')
)
}
}}
offsetsConfirmed={lpcComplete}
/>
Expand All @@ -270,6 +292,9 @@ export function ProtocolRunSetup({
setLabwareConfirmed={(confirmed: boolean) => {
setLabwareSetupComplete(confirmed)
if (confirmed) {
setMissingSteps(
missingSteps.filter(step => step !== 'labware_placement')
)
const nextStep =
targetStepKeyInOrder.findIndex(v => v === LABWARE_SETUP_KEY) ===
targetStepKeyInOrder.length - 1
Expand Down Expand Up @@ -298,6 +323,7 @@ export function ProtocolRunSetup({
setLiquidSetupConfirmed={(confirmed: boolean) => {
setLiquidSetupComplete(confirmed)
if (confirmed) {
setMissingSteps(missingSteps.filter(step => step != 'liquids'))

Check failure on line 326 in app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx

View workflow job for this annotation

GitHub Actions / js checks

Expected '!==' and instead saw '!='

Check failure on line 326 in app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx

View workflow job for this annotation

GitHub Actions / js checks

Expected '!==' and instead saw '!='
setExpandedStepKey(null)
}
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,9 @@ import {
ProtocolDropTipModal,
useProtocolDropTipModal,
} from '../ProtocolDropTipModal'
import { ConfirmMissingStepsModal } from '../ConfirmMissingStepsModal'

import type { MissingSteps } from '../ProtocolRunSetup'
import type { UseQueryResult } from 'react-query'
import type { NavigateFunction } from 'react-router-dom'
import type { Mock } from 'vitest'
Expand Down Expand Up @@ -153,6 +155,7 @@ vi.mock('../../../ProtocolUpload/hooks/useMostRecentRunId')
vi.mock('../../../../resources/runs')
vi.mock('../../../ErrorRecoveryFlows')
vi.mock('../ProtocolDropTipModal')
vi.mock('../ConfirmMissingStepsModal')

const ROBOT_NAME = 'otie'
const RUN_ID = '95e67900-bc9f-4fbf-92c6-cc4d7226a51b'
Expand Down Expand Up @@ -215,6 +218,7 @@ const mockDoorStatus = {
doorRequiredClosedForProtocol: true,
},
}
let mockMissingSteps: MissingSteps = []

const render = () => {
return renderWithProviders(
Expand All @@ -224,6 +228,7 @@ const render = () => {
robotName={ROBOT_NAME}
runId={RUN_ID}
makeHandleJumpToStep={vi.fn(() => vi.fn())}
missingSetupSteps={mockMissingSteps}
/>
</BrowserRouter>,
{ i18nInstance: i18n }
Expand All @@ -240,7 +245,7 @@ describe('ProtocolRunHeader', () => {
mockTrackProtocolRunEvent = vi.fn(() => new Promise(resolve => resolve({})))
mockCloseCurrentRun = vi.fn()
mockDetermineTipStatus = vi.fn()

mockMissingSteps = []
vi.mocked(useTrackEvent).mockReturnValue(mockTrackEvent)
vi.mocked(ConfirmCancelModal).mockReturnValue(
<div>Mock ConfirmCancelModal</div>
Expand All @@ -267,6 +272,9 @@ describe('ProtocolRunHeader', () => {
vi.mocked(ConfirmAttachmentModal).mockReturnValue(
<div>mock confirm attachment modal</div>
)
vi.mocked(ConfirmMissingStepsModal).mockReturnValue(
<div>mock missing steps modal</div>
)
when(vi.mocked(useProtocolAnalysisErrors)).calledWith(RUN_ID).thenReturn({
analysisErrors: null,
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { SetupLiquids } from '../SetupLiquids'
import { SetupModuleAndDeck } from '../SetupModuleAndDeck'
import { EmptySetupStep } from '../EmptySetupStep'
import { ProtocolRunSetup } from '../ProtocolRunSetup'
import type { MissingSteps } from '../ProtocolRunSetup'
import { useNotifyRunQuery } from '../../../../resources/runs'

import type * as SharedData from '@opentrons/shared-data'
Expand Down Expand Up @@ -68,12 +69,18 @@ vi.mock('@opentrons/shared-data', async importOriginal => {
const ROBOT_NAME = 'otie'
const RUN_ID = '1'
const MOCK_PROTOCOL_LIQUID_KEY = { liquids: [] }
let mockMissingSteps: MissingSteps = []
const mockSetMissingSteps = vi.fn((missingSteps: MissingSteps) => {
mockMissingSteps = missingSteps
})
const render = () => {
return renderWithProviders(
<ProtocolRunSetup
protocolRunHeaderRef={null}
robotName={ROBOT_NAME}
runId={RUN_ID}
missingSteps={mockMissingSteps}
setMissingSteps={mockSetMissingSteps}
/>,
{
i18nInstance: i18n,
Expand All @@ -83,6 +90,7 @@ const render = () => {

describe('ProtocolRunSetup', () => {
beforeEach(() => {
mockMissingSteps = []
when(vi.mocked(useIsFlex)).calledWith(ROBOT_NAME).thenReturn(false)
when(vi.mocked(useMostRecentCompletedAnalysis))
.calledWith(RUN_ID)
Expand Down
Loading

0 comments on commit b4c2a4b

Please sign in to comment.