From 5ab7f03a31154d70e19132097a2cf87d4c4d5a9b Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Tue, 6 Aug 2024 11:08:17 -0400 Subject: [PATCH] feat(app): ODD: confirm run modal for steps We need a nag modal that doesn't let you start a run unless you acknowledge you haven't done some of the steps yet. Fun fact we get intl for free inside i18next in 21, but we have 19. --- .../localization/en/protocol_setup.json | 9 ++- .../organisms/ProtocolSetupLiquids/index.tsx | 4 +- .../ConfirmSetupStepsCompleteModal.tsx | 68 +++++++++++++++++++ app/src/pages/ProtocolSetup/index.tsx | 49 ++++++++++--- 4 files changed, 115 insertions(+), 15 deletions(-) create mode 100644 app/src/pages/ProtocolSetup/ConfirmSetupStepsCompleteModal.tsx diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index e339bd298a9..2eef6a6c15c 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -8,6 +8,8 @@ "add_to_slot": "Add to slot {{slotName}}", "additional_labware": "{{count}} additional labware", "additional_off_deck_labware": "Additional Off-Deck Labware", + "applied_labware_offsets": "applied labware offsets", + "are_you_sure_you_want_to_proceed": "Are you sure you want to proceed to run?", "attach_gripper_failure_reason": "Attach the required gripper to continue", "attach_gripper": "attach gripper", "attach_module": "Attach module before calibrating", @@ -104,6 +106,7 @@ "labware_latch": "Labware Latch", "labware_location": "Labware Location", "labware_name": "Labware name", + "labware_placement": "labware placement", "labware_position_check_not_available_analyzing_on_robot": "Labware Position Check is not available while protocol is analyzing on robot", "labware_position_check_not_available_empty_protocol": "Labware Position Check requires that the protocol loads labware and pipettes", "labware_position_check_not_available": "Labware Position Check is not available after run has started", @@ -121,11 +124,11 @@ "learn_more": "Learn more", "liquid_information": "Liquid information", "liquid_name": "Liquid name", + "liquids": "liquids", "liquid_setup_step_description": "View liquid starting locations and volumes", "liquid_setup_step_title": "Liquids", "liquids_not_in_setup": "No liquids used in this protocol", "liquids_not_in_the_protocol": "no liquids are specified for this protocol.", - "liquids": "Liquids", "liquids_ready": "Liquids ready", "liquids_confirmed": "Liquids confirmed", "list_view": "List View", @@ -270,6 +273,7 @@ "setup_is_view_only": "Setup is view-only once run has started", "slot_location": "Slot {{slotName}}", "slot_number": "Slot Number", + "start_run": "Start run", "status": "Status", "step": "STEP {{index}}", "there_are_no_unconfigured_modules": "No {{module}} is connected. Attach one and place it in {{slot}}.", @@ -297,5 +301,6 @@ "view_setup_instructions": "View setup instructions", "volume": "Volume", "what_labware_offset_is": "A Labware Offset is a type of positional adjustment that accounts for small, real-world variances in the overall position of the labware on a robot’s deck. Labware Offset data is unique to a specific combination of labware definition, deck slot, and robot.", - "with_the_chosen_value": "With the chosen values, the following error occurred:" + "with_the_chosen_value": "With the chosen values, the following error occurred:", + "you_havent_confirmed": "You haven't confirmed the {{missingSteps}} yet. Ensure these are correct before proceeding to run the protocol." } diff --git a/app/src/organisms/ProtocolSetupLiquids/index.tsx b/app/src/organisms/ProtocolSetupLiquids/index.tsx index 426ec5976ab..883054c6963 100644 --- a/app/src/organisms/ProtocolSetupLiquids/index.tsx +++ b/app/src/organisms/ProtocolSetupLiquids/index.tsx @@ -43,7 +43,7 @@ export function ProtocolSetupLiquids({ isConfirmed, setIsConfirmed, }: ProtocolSetupLiquidsProps): JSX.Element { - const { t } = useTranslation('protocol_setup') + const { t, i18n } = useTranslation('protocol_setup') const protocolData = useMostRecentCompletedAnalysis(runId) const liquidsInLoadOrder = parseLiquidsInLoadOrder( protocolData?.liquids ?? [], @@ -56,7 +56,7 @@ export function ProtocolSetupLiquids({ justifyContent={JUSTIFY_SPACE_BETWEEN} > { setSetupScreen('prepare to run') }} diff --git a/app/src/pages/ProtocolSetup/ConfirmSetupStepsCompleteModal.tsx b/app/src/pages/ProtocolSetup/ConfirmSetupStepsCompleteModal.tsx new file mode 100644 index 00000000000..1757704e597 --- /dev/null +++ b/app/src/pages/ProtocolSetup/ConfirmSetupStepsCompleteModal.tsx @@ -0,0 +1,68 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +import { + DIRECTION_COLUMN, + Flex, + SPACING, + LegacyStyledText, +} from '@opentrons/components' + +import { SmallButton } from '../../atoms/buttons' +import { Modal } from '../../molecules/Modal' + +import type { ModalHeaderBaseProps } from '../../molecules/Modal/types' + +interface ConfirmSetupStepsCompleteModalProps { + onCloseClick: () => void + onConfirmClick: () => void + missingSteps: string[] +} + +export function ConfirmSetupStepsCompleteModal({ + onCloseClick, + missingSteps, + onConfirmClick, +}: ConfirmSetupStepsCompleteModalProps): JSX.Element { + const { i18n, t } = useTranslation(['protocol_setup', 'shared']) + const modalHeader: ModalHeaderBaseProps = { + title: t('are_you_sure_you_want_to_proceed'), + hasExitIcon: true, + } + + const handleStartRun = (): void => { + onConfirmClick() + onCloseClick() + } + + return ( + + + + {t('you_havent_confirmed', { + missingSteps: new Intl.ListFormat('en', { + style: 'short', + type: 'conjunction', + }).format(missingSteps), + })} + + + { + onCloseClick() + }} + /> + + + + + ) +} diff --git a/app/src/pages/ProtocolSetup/index.tsx b/app/src/pages/ProtocolSetup/index.tsx index da5bb3c4de4..855181290f0 100644 --- a/app/src/pages/ProtocolSetup/index.tsx +++ b/app/src/pages/ProtocolSetup/index.tsx @@ -86,6 +86,7 @@ import { } from '../../redux/analytics' import { getIsHeaterShakerAttached } from '../../redux/config' import { ConfirmAttachedModal } from './ConfirmAttachedModal' +import { ConfirmSetupStepsCompleteModal } from './ConfirmSetupStepsCompleteModal' import { getLatestCurrentOffsets } from '../../organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/utils' import { CloseButton, PlayButton } from './Buttons' import { useDeckConfigurationCompatibility } from '../../resources/deck_configuration/hooks' @@ -274,6 +275,7 @@ interface PrepareToRunProps { runId: string setSetupScreen: React.Dispatch> confirmAttachment: () => void + confirmStepsComplete: () => void play: () => void robotName: string runRecord: Run | null @@ -292,6 +294,7 @@ function PrepareToRun({ labwareConfirmed, liquidsConfirmed, offsetsConfirmed, + confirmStepsComplete, }: PrepareToRunProps): JSX.Element { const { t, i18n } = useTranslation(['protocol_setup', 'shared']) const navigate = useNavigate() @@ -516,12 +519,13 @@ function PrepareToRun({ if (isDoorOpen) { makeSnackbar(t('shared:close_robot_door') as string) } else { - if ( - isHeaterShakerInProtocol && - isReadyToRun && - runStatus === RUN_STATUS_IDLE - ) { - confirmAttachment() + if (isReadyToRun && runStatus === RUN_STATUS_IDLE) { + if (!(labwareConfirmed && offsetsConfirmed && liquidsConfirmed)) { + confirmStepsComplete() + } + if (isHeaterShakerInProtocol) { + confirmAttachment() + } } else { if (isReadyToRun) { play() @@ -793,7 +797,7 @@ function PrepareToRun({ onClickSetupStep={() => { setSetupScreen('liquids') }} - title={t('liquids')} + title={i18n.format(t('liquids'), 'capitalize')} status={liquidsConfirmed ? 'ready' : 'general'} detail={ liquidsInProtocol.length > 0 @@ -840,6 +844,7 @@ export function ProtocolSetup(): JSX.Element { >() as OnDeviceRouteParams const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) const { analysisErrors } = useProtocolAnalysisErrors(runId) + const { t } = useTranslation(['protocol_setup']) const localRobot = useSelector(getLocalRobot) const robotName = localRobot?.name != null ? localRobot.name : 'no name' const robotSerialNumber = @@ -925,8 +930,8 @@ export function ProtocolSetup(): JSX.Element { ) const { confirm: confirmAttachment, - showConfirmation: showConfirmationModal, - cancel: cancelExit, + showConfirmation: showHSConfirmationModal, + cancel: cancelExitHSConfirmation, } = useConditionalConfirm( handleProceedToRunClick, !configBypassHeaterShakerAttachmentConfirmation @@ -938,6 +943,20 @@ export function ProtocolSetup(): JSX.Element { const [labwareConfirmed, setLabwareConfirmed] = React.useState(false) const [liquidsConfirmed, setLiquidsConfirmed] = React.useState(false) const [offsetsConfirmed, setOffsetsConfirmed] = React.useState(false) + const missingSteps = [ + !offsetsConfirmed ? t('applied_labware_offsets') : null, + !labwareConfirmed ? t('labware_placement') : null, + !liquidsConfirmed ? t('liquids') : null, + ].filter(s => s != null) + console.log(missingSteps) + const { + confirm: confirmMissingSteps, + showConfirmation: showMissingStepsConfirmation, + cancel: cancelExitMissingStepsConfirmation, + } = useConditionalConfirm( + handleProceedToRunClick, + !(labwareConfirmed && liquidsConfirmed && offsetsConfirmed) + ) // orchestrate setup subpages/components const [setupScreen, setSetupScreen] = React.useState( @@ -949,6 +968,7 @@ export function ProtocolSetup(): JSX.Element { runId={runId} setSetupScreen={setSetupScreen} confirmAttachment={confirmAttachment} + confirmStepsComplete={confirmMissingSteps} play={play} robotName={robotName} runRecord={runRecord ?? null} @@ -1019,9 +1039,16 @@ export function ProtocolSetup(): JSX.Element { errors={analysisErrors.map(error => error.detail)} /> ) : null} - {showConfirmationModal ? ( + {showMissingStepsConfirmation ? ( + + ) : null} + {showHSConfirmationModal ? (