From 0b3a34500e318ae475ad0e1668e97b7f1d6dde2b Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Wed, 18 Sep 2024 13:42:15 -0400 Subject: [PATCH] refactor(app): Refactor drop tip flows presentation layer (#16285) Closes EXEC-520 This PR cleans up drop tip wizard, removing things like negative margin offsets, unifying view components when possible, and separating presentation concerns into a modalStyle, which can be either intervention (used by Error Recovery) or simple (used in non-ER circumstances, currently). There is still some special-cased CSS based on modalStyle, but there's no way around that, because that's how it is in design. The only "functional" change is all the proceed/go back buttons now do not shift up or down from step to step! --- .../localization/en/drop_tip_wizard.json | 16 +- .../InProgressModal/InProgressModal.tsx | 1 + .../Devices/PipetteCard/FlexPipetteCard.tsx | 1 + .../organisms/Devices/PipetteCard/index.tsx | 1 + .../hooks/useRunHeaderDropTip.ts | 1 + .../modals/ProtocolDropTipModal.tsx | 2 +- .../DropTipWizardFlows/BeforeBeginning.tsx | 280 ---------------- .../DropTipWizardFlows/ChooseLocation.tsx | 155 --------- .../DropTipWizardFlows/ConfirmPosition.tsx | 154 +++++++++ .../DropTipWizardFlows/DropTipWizard.tsx | 277 ++++++---------- .../DropTipWizardFlows/DropTipWizardFlows.tsx | 9 +- .../DropTipWizardFlows/ErrorInfo.tsx | 72 ++++ .../DropTipWizardFlows/ExitConfirmation.tsx | 118 +++---- .../DropTipWizardFlows/JogToPosition.tsx | 312 ------------------ .../organisms/DropTipWizardFlows/Success.tsx | 90 ----- .../DropTipWizardFlows/TipsAttachedModal.tsx | 1 + .../DropTipWizardFlows/__fixtures__/index.ts | 1 + .../__tests__/DropTipWizard.test.tsx | 46 +-- .../hooks/useDropTipMaintenanceRun.tsx | 2 +- .../shared/DropTipFooterButtons.tsx | 106 ++++++ .../DropTipWizardFlows/shared/index.ts | 1 + .../steps/BeforeBeginning.tsx | 261 +++++++++++++++ .../steps/ChooseLocation.tsx | 92 ++++++ .../steps/JogToPosition.tsx | 100 ++++++ .../DropTipWizardFlows/steps/Success.tsx | 110 ++++++ .../DropTipWizardFlows/steps/index.ts | 4 + app/src/organisms/DropTipWizardFlows/types.ts | 1 + .../RecoveryOptions/ManageTips.tsx | 18 +- .../shared/RecoveryInterventionModal.tsx | 6 + .../InstrumentDetailOverflowMenu.tsx | 1 + 30 files changed, 1112 insertions(+), 1127 deletions(-) delete mode 100644 app/src/organisms/DropTipWizardFlows/BeforeBeginning.tsx delete mode 100644 app/src/organisms/DropTipWizardFlows/ChooseLocation.tsx create mode 100644 app/src/organisms/DropTipWizardFlows/ConfirmPosition.tsx create mode 100644 app/src/organisms/DropTipWizardFlows/ErrorInfo.tsx delete mode 100644 app/src/organisms/DropTipWizardFlows/JogToPosition.tsx delete mode 100644 app/src/organisms/DropTipWizardFlows/Success.tsx create mode 100644 app/src/organisms/DropTipWizardFlows/shared/DropTipFooterButtons.tsx create mode 100644 app/src/organisms/DropTipWizardFlows/shared/index.ts create mode 100644 app/src/organisms/DropTipWizardFlows/steps/BeforeBeginning.tsx create mode 100644 app/src/organisms/DropTipWizardFlows/steps/ChooseLocation.tsx create mode 100644 app/src/organisms/DropTipWizardFlows/steps/JogToPosition.tsx create mode 100644 app/src/organisms/DropTipWizardFlows/steps/Success.tsx create mode 100644 app/src/organisms/DropTipWizardFlows/steps/index.ts diff --git a/app/src/assets/localization/en/drop_tip_wizard.json b/app/src/assets/localization/en/drop_tip_wizard.json index e622fbce4cf..d683b3bdabf 100644 --- a/app/src/assets/localization/en/drop_tip_wizard.json +++ b/app/src/assets/localization/en/drop_tip_wizard.json @@ -4,28 +4,30 @@ "blowout_complete": "Blowout complete", "blowout_liquid": "Blow out liquid", "cant_safely_drop_tips": "Can't safely drop tips", - "choose_blowout_location": "choose blowout location", - "choose_drop_tip_location": "choose tip-drop location", + "choose_blowout_location": "Choose blowout location", + "choose_drop_tip_location": "Choose tip-drop location", "confirm_blowout_location": "Is the pipette positioned where the liquids should be blown out?", "confirm_drop_tip_location": "Is the pipette positioned where the tips should be dropped?", + "confirm_position": "Confirm position", "confirm_removal_and_home": "Confirm removal and home", + "continue": "Continue", "drop_tip_complete": "Tip drop complete", "drop_tip_failed": "The drop tip could not be completed. Contact customer support for assistance.", - "drop_tips": "drop tips", + "drop_tips": "Drop tips", "error_dropping_tips": "Error dropping tips", + "exit": "Exit", "exit_and_home_pipette": "Exit and home pipette", "getting_ready": "Getting ready…", - "go_back": "go back", + "go_back": "Go back", "jog_too_far": "Jog too far?", "liquid_damages_pipette": "Homing the pipette with liquid in the tips may damage it. You must remove all tips before using the pipette again.", "liquid_damages_this_pipette": "Homing the {{mount}} pipette with liquid in the tips may damage it. You must remove all tips before using the pipette again.", - "move_to_slot": "move to slot", + "move_to_slot": "Move to slot", "no_proceed_to_drop_tip": "No, proceed to tip removal", "position_and_blowout": "Ensure that the pipette tip is centered above and level with where you want the liquid to be blown out. If it isn't, use the controls below or your keyboard to jog the pipette until it is properly aligned.", "position_and_drop_tip": "Ensure that the pipette tip is centered above and level with where you want to drop the tips. If it isn't, use the controls below or your keyboard to jog the pipette until it is properly aligned.", - "position_the_pipette": "position the pipette", + "position_the_pipette": "Position the pipette", "remove_any_attached_tips": "Remove any attached tips", - "remove_attached_tips": "Remove any attached tips", "remove_the_tips_from_pipette": "You may want to remove the tips from the pipette before using it again in a protocol.", "remove_the_tips_manually": "Remove the tips manually. Then home the gantry. Homing with tips attached could pull liquid into the pipette and damage it.", "remove_tips": "Remove tips", diff --git a/app/src/molecules/InProgressModal/InProgressModal.tsx b/app/src/molecules/InProgressModal/InProgressModal.tsx index 70d6c3eadc4..24c5d9a8f71 100644 --- a/app/src/molecules/InProgressModal/InProgressModal.tsx +++ b/app/src/molecules/InProgressModal/InProgressModal.tsx @@ -52,6 +52,7 @@ const MODAL_STYLE = css` justify-content: ${JUSTIFY_CENTER}; padding: ${SPACING.spacing32}; height: 24.625rem; + width: 100%; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { max-height: 29.5rem; height: 100%; diff --git a/app/src/organisms/Devices/PipetteCard/FlexPipetteCard.tsx b/app/src/organisms/Devices/PipetteCard/FlexPipetteCard.tsx index 621fdc058e2..97354a7bb70 100644 --- a/app/src/organisms/Devices/PipetteCard/FlexPipetteCard.tsx +++ b/app/src/organisms/Devices/PipetteCard/FlexPipetteCard.tsx @@ -271,6 +271,7 @@ export function FlexPipetteCard({ mount={mount} instrumentModelSpecs={pipetteModelSpecs} closeFlow={toggleDTWiz} + modalStyle="simple" /> ) : null} {attachedPipette?.ok && showAboutPipetteSlideout ? ( diff --git a/app/src/organisms/Devices/PipetteCard/index.tsx b/app/src/organisms/Devices/PipetteCard/index.tsx index 182eb5339b8..a75f4e505f1 100644 --- a/app/src/organisms/Devices/PipetteCard/index.tsx +++ b/app/src/organisms/Devices/PipetteCard/index.tsx @@ -111,6 +111,7 @@ export const PipetteCard = (props: PipetteCardProps): JSX.Element => { mount={mount} instrumentModelSpecs={pipetteModelSpecs} closeFlow={toggleDTWiz} + modalStyle="simple" /> ) : null} {showSlideout && diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useRunHeaderDropTip.ts b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useRunHeaderDropTip.ts index 013ae24f3aa..659d0ba8595 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useRunHeaderDropTip.ts +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/hooks/useRunHeaderDropTip.ts @@ -97,6 +97,7 @@ export function useRunHeaderDropTip({ mount: aPipetteWithTip.mount, instrumentModelSpecs: aPipetteWithTip.specs, closeFlow: onCloseFlow, + modalStyle: 'simple', }, } : { showDTWiz: false, dtWizProps: null } diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ProtocolDropTipModal.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ProtocolDropTipModal.tsx index 68e6c3601b6..adb65926d72 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ProtocolDropTipModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ProtocolDropTipModal.tsx @@ -118,7 +118,7 @@ export function ProtocolDropTipModal({ const buildHeader = (): JSX.Element => { return ( { - const { i18n, t } = useTranslation(['drop_tip_wizard', 'shared']) - const [flowType, setFlowType] = React.useState< - 'blowout' | 'drop_tips' | null - >(null) - - const handleProceed = (): void => { - if (flowType === 'blowout') { - void proceedToRoute(DT_ROUTES.BLOWOUT) - } else if (flowType === 'drop_tips') { - void proceedToRoute(DT_ROUTES.DROP_TIP) - } - } - - const buildTopText = (): string => { - if (issuedCommandsType === 'fixit') { - return fixitCommandTypeUtils?.copyOverrides - .beforeBeginningTopText as string - } else { - return t('before_you_begin_do_you_want_to_blowout') - } - } - - if (isOnDevice) { - return ( - - {buildTopText()} - - { - setFlowType('blowout') - }} - buttonText={i18n.format(t('yes_blow_out_liquid'), 'capitalize')} - justifyContent={JUSTIFY_FLEX_START} - paddingLeft={SPACING.spacing24} - height="5.25rem" - /> - - - { - setFlowType('drop_tips') - }} - buttonText={i18n.format(t('no_proceed_to_drop_tip'), 'capitalize')} - justifyContent={JUSTIFY_FLEX_START} - paddingLeft={SPACING.spacing24} - height="5.25rem" - /> - - - {fixitCommandTypeUtils != null ? ( - - ) : null} - - - - ) - } else { - return ( - - {buildTopText()} - - { - setFlowType('blowout') - }} - css={ - flowType === 'blowout' - ? SELECTED_OPTIONS_STYLE - : UNSELECTED_OPTIONS_STYLE - } - > - - - {t('yes_blow_out_liquid')} - - - { - setFlowType('drop_tips') - }} - css={ - flowType === 'drop_tips' - ? SELECTED_OPTIONS_STYLE - : UNSELECTED_OPTIONS_STYLE - } - > - - - {t('no_proceed_to_drop_tip')} - - - - - {/* */} - {fixitCommandTypeUtils != null ? ( - - ) : null} - - {i18n.format(t('shared:continue'), 'capitalize')} - - - - ) - } -} - -const UNSELECTED_OPTIONS_STYLE = css` - background-color: ${COLORS.white}; - border: 1px solid ${COLORS.grey30}; - border-radius: ${BORDERS.borderRadius8}; - height: 12.5625rem; - width: 14.5625rem; - cursor: ${CURSOR_POINTER}; - flex-direction: ${DIRECTION_COLUMN}; - justify-content: ${JUSTIFY_CENTER}; - align-items: ${ALIGN_CENTER}; - grid-gap: ${SPACING.spacing8}; - - &:hover { - border: 1px solid ${COLORS.grey35}; - } - - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - flex-direction: ${DIRECTION_ROW}; - justify-content: ${JUSTIFY_FLEX_START}; - background-color: ${COLORS.blue35}; - border-width: 0; - border-radius: ${BORDERS.borderRadius16}; - padding: ${SPACING.spacing24}; - height: 5.25rem; - width: 57.8125rem; - - &:hover { - border-width: 0px; - } - } -` -const SELECTED_OPTIONS_STYLE = css` - ${UNSELECTED_OPTIONS_STYLE} - border: 1px solid ${COLORS.blue50}; - background-color: ${COLORS.blue30}; - - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - border-width: 0px; - background-color: ${COLORS.blue50}; - color: ${COLORS.white}; - - &:hover { - border-width: 0px; - background-color: ${COLORS.blue50}; - } - } -` - -const Title = styled.h1` - ${TYPOGRAPHY.h1Default}; - margin-bottom: ${SPACING.spacing8}; - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - ${TYPOGRAPHY.level4HeaderSemiBold}; - margin-bottom: 0; - height: ${SPACING.spacing40}; - display: ${DISPLAY_INLINE_BLOCK}; - } -` - -const ODD_TITLE_STYLE = css` - ${TYPOGRAPHY.level4HeaderSemiBold} - margin-bottom: ${SPACING.spacing16}; -` - -const TILE_CONTAINER_STYLE = css` - flex-direction: ${DIRECTION_COLUMN}; - justify-content: ${JUSTIFY_SPACE_BETWEEN}; - padding: ${SPACING.spacing32}; - height: 100%; - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - height: 29.5rem; - } -` diff --git a/app/src/organisms/DropTipWizardFlows/ChooseLocation.tsx b/app/src/organisms/DropTipWizardFlows/ChooseLocation.tsx deleted file mode 100644 index 18766553999..00000000000 --- a/app/src/organisms/DropTipWizardFlows/ChooseLocation.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import * as React from 'react' -import styled, { css } from 'styled-components' -import { useTranslation } from 'react-i18next' - -import { - ALIGN_CENTER, - ALIGN_FLEX_END, - Btn, - DIRECTION_COLUMN, - Flex, - JUSTIFY_SPACE_BETWEEN, - JUSTIFY_FLEX_START, - PrimaryButton, - RESPONSIVENESS, - SPACING, - LegacyStyledText, - TYPOGRAPHY, - DISPLAY_INLINE_BLOCK, -} from '@opentrons/components' -import { getDeckDefFromRobotType } from '@opentrons/shared-data' - -import { SmallButton, TextOnlyButton } from '../../atoms/buttons' -import { TwoColumn, DeckMapContent } from '../../molecules/InterventionModal' - -import type { - AddressableAreaName, - ModuleLocation, -} from '@opentrons/shared-data' -import type { DropTipWizardContainerProps } from './types' - -// TODO: get help link article URL - -type ChooseLocationProps = DropTipWizardContainerProps & { - handleProceed: () => void - handleGoBack: () => void - title: string - body: string | JSX.Element - moveToAddressableArea: (addressableArea: AddressableAreaName) => Promise -} -const Title = styled.h1` - ${TYPOGRAPHY.h1Default}; - margin-bottom: ${SPACING.spacing8}; - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - ${TYPOGRAPHY.level4HeaderSemiBold}; - margin-bottom: 0; - height: ${SPACING.spacing40}; - display: ${DISPLAY_INLINE_BLOCK}; - } -` - -export const ChooseLocation = ( - props: ChooseLocationProps -): JSX.Element | null => { - const { - handleProceed, - handleGoBack, - title, - body, - robotType, - moveToAddressableArea, - issuedCommandsType, - } = props - const { i18n, t } = useTranslation(['drop_tip_wizard', 'shared']) - const [ - selectedLocation, - setSelectedLocation, - ] = React.useState() - const deckDef = getDeckDefFromRobotType(robotType) - - const handleConfirmPosition = (): void => { - const deckSlot = deckDef.locations.addressableAreas.find( - l => l.id === selectedLocation?.slotName - )?.id - - if (deckSlot != null) { - void moveToAddressableArea(deckSlot).then(() => { - handleProceed() - }) - } - } - return ( - - - - {title} - {body} - - - - - { - handleGoBack() - }} - > - - - - {i18n.format(t('move_to_slot'), 'capitalize')} - - - - - ) -} - -const ALIGN_BUTTONS = css` - align-items: ${ALIGN_FLEX_END}; - - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - align-items: ${ALIGN_CENTER}; - } -` - -const CONTAINER_STYLE = css` - flex-direction: ${DIRECTION_COLUMN}; - justify-content: ${JUSTIFY_SPACE_BETWEEN}; - padding: ${SPACING.spacing32}; - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - justify-content: ${JUSTIFY_FLEX_START}; - gap: ${SPACING.spacing32}; - padding: none; - height: 29.5rem; - } -` diff --git a/app/src/organisms/DropTipWizardFlows/ConfirmPosition.tsx b/app/src/organisms/DropTipWizardFlows/ConfirmPosition.tsx new file mode 100644 index 00000000000..6742672beca --- /dev/null +++ b/app/src/organisms/DropTipWizardFlows/ConfirmPosition.tsx @@ -0,0 +1,154 @@ +import * as React from 'react' +import { css } from 'styled-components' +import { useTranslation } from 'react-i18next' + +import { + ALIGN_CENTER, + COLORS, + DIRECTION_COLUMN, + Flex, + Icon, + JUSTIFY_CENTER, + SPACING, + DISPLAY_FLEX, + TEXT_ALIGN_CENTER, + RESPONSIVENESS, + StyledText, +} from '@opentrons/components' + +import { POSITION_AND_BLOWOUT, POSITION_AND_DROP_TIP } from './constants' +import { DropTipFooterButtons } from './shared' + +import type { DropTipWizardContainerProps } from './types' + +export interface UseConfirmPositionResult { + showConfirmPosition: boolean + isRobotPipetteMoving: boolean + toggleShowConfirmPosition: () => void + toggleIsRobotPipetteMoving: () => void +} + +// Handles confirming the position. Because pipette drop tip/blowout actions do not trigger +// an "in-motion" the same way other commands do, we synthetically create an "in motion", disabling +// it once the step has completed or failed. +export function useConfirmPosition( + currentStep: DropTipWizardContainerProps['currentStep'] +): UseConfirmPositionResult { + const [showConfirmPosition, setShowConfirmPosition] = React.useState(false) + const [isRobotPipetteMoving, setIsRobotPipetteMoving] = React.useState(false) + + const toggleShowConfirmPosition = (): void => { + setShowConfirmPosition(!showConfirmPosition) + } + + const toggleIsRobotPipetteMoving = (): void => { + setIsRobotPipetteMoving(!isRobotPipetteMoving) + } + + // NOTE: The useEffect logic is potentially problematic on views that are not steps, but it is not currently. + React.useEffect(() => { + if ( + currentStep !== POSITION_AND_BLOWOUT && + currentStep !== POSITION_AND_DROP_TIP && + isRobotPipetteMoving && + showConfirmPosition + ) { + toggleShowConfirmPosition() + toggleIsRobotPipetteMoving() + } + }, [currentStep, isRobotPipetteMoving]) + + return { + showConfirmPosition, + toggleShowConfirmPosition, + toggleIsRobotPipetteMoving, + isRobotPipetteMoving, + } +} + +type ConfirmPositionProps = DropTipWizardContainerProps & + UseConfirmPositionResult + +export function ConfirmPosition({ + toggleShowConfirmPosition, + toggleIsRobotPipetteMoving, + currentStep, + dropTipCommands, + proceed, + modalStyle, +}: ConfirmPositionProps): JSX.Element { + const { blowoutOrDropTip } = dropTipCommands + const { t } = useTranslation('drop_tip_wizard') + + const buildPrimaryBtnText = (): string => + currentStep === POSITION_AND_BLOWOUT ? t('blowout_liquid') : t('drop_tips') + + const handleProceed = (): void => { + toggleIsRobotPipetteMoving() + void blowoutOrDropTip(currentStep, proceed) + } + + return ( + <> + + + + {currentStep === POSITION_AND_BLOWOUT + ? t('confirm_blowout_location') + : t('confirm_drop_tip_location')} + + + + + ) +} + +const SHARED_CONTAINER_STYLE = ` + display: ${DISPLAY_FLEX}; + flex-direction: ${DIRECTION_COLUMN}; + grid-gap: ${SPACING.spacing16}; + padding: ${SPACING.spacing40} ${SPACING.spacing16}; + align-items: ${ALIGN_CENTER}; + justify-content: ${JUSTIFY_CENTER}; + text-align: ${TEXT_ALIGN_CENTER}; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + grid-gap: ${SPACING.spacing24}; + padding: ${SPACING.spacing40}; + } +` + +const INTERVENTION_CONTAINER_STYLE = css` + ${SHARED_CONTAINER_STYLE} + margin-top: ${SPACING.spacing60}; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + margin-top: ${SPACING.spacing48}; + } +` + +const SIMPLE_CONTAINER_STYLE = css` + ${SHARED_CONTAINER_STYLE} + margin-top: ${SPACING.spacing32}; +` + +const ICON_STYLE = css` + width: 40px; + height: 40px; + color: ${COLORS.yellow50}; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + width: 60px; + height: 60px; + } +` diff --git a/app/src/organisms/DropTipWizardFlows/DropTipWizard.tsx b/app/src/organisms/DropTipWizardFlows/DropTipWizard.tsx index aaf2acf7706..e07f3792f73 100644 --- a/app/src/organisms/DropTipWizardFlows/DropTipWizard.tsx +++ b/app/src/organisms/DropTipWizardFlows/DropTipWizard.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import { createPortal } from 'react-dom' -import { Trans, useTranslation } from 'react-i18next' +import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { css } from 'styled-components' @@ -10,17 +10,16 @@ import { DIRECTION_COLUMN, RESPONSIVENESS, Flex, - JUSTIFY_FLEX_END, JUSTIFY_SPACE_BETWEEN, POSITION_ABSOLUTE, SPACING, - LegacyStyledText, - ModalShell, useConditionalConfirm, + ModalShell, + DISPLAY_FLEX, + OVERFLOW_HIDDEN, } from '@opentrons/components' import { getTopPortalEl } from '../../App/portal' -import { SimpleWizardBody } from '../../molecules/SimpleWizardBody' import { getIsOnDevice } from '../../redux/config' import { ExitConfirmation } from './ExitConfirmation' import { @@ -29,17 +28,20 @@ import { CHOOSE_BLOWOUT_LOCATION, CHOOSE_DROP_TIP_LOCATION, DROP_TIP_SUCCESS, - DT_ROUTES, POSITION_AND_BLOWOUT, POSITION_AND_DROP_TIP, } from './constants' -import { BeforeBeginning } from './BeforeBeginning' -import { ChooseLocation } from './ChooseLocation' -import { JogToPosition } from './JogToPosition' -import { Success } from './Success' -import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal' +import { + BeforeBeginning, + ChooseLocation, + JogToPosition, + Success, +} from './steps' +import { InProgressModal } from '../../molecules/InProgressModal' import { useDropTipErrorComponents } from './hooks' import { DropTipWizardHeader } from './DropTipWizardHeader' +import { ErrorInfo } from './ErrorInfo' +import { ConfirmPosition, useConfirmPosition } from './ConfirmPosition' import type { DropTipWizardFlowsProps } from '.' import type { DropTipWizardContainerProps, IssuedCommandsType } from './types' @@ -111,8 +113,6 @@ export function DropTipWizard(props: DropTipWizardProps): JSX.Element { ) } -// TODO(jh, 06-07-24): All content views could use refactoring and DQA. Create shared components from designs. -// Convince design not to use SimpleWizardBody. EXEC-520. export function DropTipWizardContainer( props: DropTipWizardContainerProps ): JSX.Element { @@ -132,58 +132,32 @@ export function DropTipWizardContainer( export function DropTipWizardFixitType( props: DropTipWizardContainerProps ): JSX.Element { - return + return ( + + + + ) } export function DropTipWizardSetupType( props: DropTipWizardContainerProps ): JSX.Element { - const { - activeMaintenanceRunId, - isCommandInProgress, - isExiting, - showConfirmExit, - errorDetails, - } = props - - // TODO(jh: 06-10-24): This is not ideal. See EXEC-520. - const inMotion = - isCommandInProgress || isExiting || activeMaintenanceRunId == null - const simpleWizardPaddingOverrides = - inMotion || showConfirmExit || errorDetails - return createPortal( props.isOnDevice ? ( - + - + ) : ( } - overflow="hidden" > - + + + ), getTopPortalEl() @@ -194,69 +168,44 @@ export const DropTipWizardContent = ( props: DropTipWizardContainerProps ): JSX.Element => { const { - isOnDevice, activeMaintenanceRunId, currentStep, errorDetails, isCommandInProgress, - fixitCommandTypeUtils, issuedCommandsType, isExiting, - proceed, - proceedToRoute, showConfirmExit, - dropTipCommands, - proceedWithConditionalClose, - goBackRunValid, - confirmExit, - cancelExit, - toggleExitInitiated, - errorComponents, } = props - const { t, i18n } = useTranslation('drop_tip_wizard') + const { t } = useTranslation('drop_tip_wizard') + const confirmPositionUtils = useConfirmPosition(currentStep) function buildGettingReady(): JSX.Element { return } function buildRobotInMotion(): JSX.Element { - return ( - <> - {issuedCommandsType === 'fixit' ? : null} - - - ) + return } - function buildShowExitConfirmation(): JSX.Element { + function buildRobotPipetteMoving(): JSX.Element { return ( - { - toggleExitInitiated() - confirmExit() - }} + ) } - function buildErrorScreen(): JSX.Element { - const { button, subHeader } = errorComponents + function buildShowExitConfirmation(): JSX.Element { + return + } - return ( - - {button} - - ) + function buildErrorScreen(): JSX.Element { + return } function buildBeforeBeginning(): JSX.Element { @@ -264,100 +213,19 @@ export const DropTipWizardContent = ( } function buildChooseLocation(): JSX.Element { - const { moveToAddressableArea } = dropTipCommands - - let bodyTextKey: string - if (currentStep === CHOOSE_BLOWOUT_LOCATION) { - bodyTextKey = isOnDevice - ? 'select_blowout_slot_odd' - : 'select_blowout_slot' - } else { - bodyTextKey = isOnDevice - ? 'select_drop_tip_slot_odd' - : 'select_drop_tip_slot' - } - - return ( - }} - /> - } - moveToAddressableArea={moveToAddressableArea} - /> - ) + return } function buildJogToPosition(): JSX.Element { - const { handleJog, blowoutOrDropTip } = dropTipCommands + return + } - return ( - blowoutOrDropTip(currentStep, proceed)} - handleGoBack={goBackRunValid} - body={ - currentStep === POSITION_AND_BLOWOUT - ? t('position_and_blowout') - : t('position_and_drop_tip') - } - /> - ) + function buildConfirmPosition(): JSX.Element { + return } function buildSuccess(): JSX.Element { - const { tipDropComplete } = fixitCommandTypeUtils?.buttonOverrides ?? {} - - // Route to the drop tip route if user is at the blowout success screen, otherwise proceed conditionally. - const handleProceed = (): void => { - if (currentStep === BLOWOUT_SUCCESS) { - void proceedToRoute(DT_ROUTES.DROP_TIP) - } else { - // Clear the error recovery submap upon completion of drop tip wizard. - fixitCommandTypeUtils?.reportMap(null) - - if (tipDropComplete != null) { - tipDropComplete() - } else { - proceedWithConditionalClose() - } - } - } - - const buildProceedText = (): string => { - if (fixitCommandTypeUtils != null && currentStep === DROP_TIP_SUCCESS) { - return fixitCommandTypeUtils.copyOverrides.tipDropCompleteBtnCopy - } else { - return currentStep === BLOWOUT_SUCCESS - ? i18n.format(t('shared:continue'), 'capitalize') - : i18n.format(t('shared:exit'), 'capitalize') - } - } - - return ( - - ) + return } function buildModalContent(): JSX.Element { @@ -371,6 +239,8 @@ export const DropTipWizardContent = ( return buildGettingReady() } else if (isCommandInProgress || isExiting) { return buildRobotInMotion() + } else if (confirmPositionUtils.showConfirmPosition) { + return buildConfirmPosition() } else if (showConfirmExit) { return buildShowExitConfirmation() } else if (errorDetails != null) { @@ -392,6 +262,8 @@ export const DropTipWizardContent = ( currentStep === DROP_TIP_SUCCESS ) { return buildSuccess() + } else if (confirmPositionUtils.isRobotPipetteMoving) { + return buildRobotPipetteMoving() } else { return
UNASSIGNED STEP
} @@ -415,8 +287,55 @@ function useInitiateExit(): { return { isExitInitiated, toggleExitInitiated } } -const ERROR_MODAL_FIXIT_STYLE = css` +const SHARED_STYLE = ` + display: ${DISPLAY_FLEX}; + flex-direction: ${DIRECTION_COLUMN}; + overflow: ${OVERFLOW_HIDDEN}; +` + +const INTERVENTION_CONTAINER_STYLE = css` + ${SHARED_STYLE} + padding: ${SPACING.spacing32}; + grid-gap: ${SPACING.spacing24}; + height: 100%; + width: 100%; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + grid-gap: ${SPACING.spacing32}; + } +` + +const SIMPLE_CONTAINER_STYLE = css` + ${SHARED_STYLE} + width: 47rem; + min-height: 26.75rem; + + // TODO(jh 09-17-24): This is effectively making a ModalShell analogue on the ODD, since one does not exist. + // Consider making one. + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + position: ${POSITION_ABSOLUTE}; + width: 62rem; + height: 35.5rem; + left: 16px; + top: 16px; + border: ${BORDERS.lineBorder}; + box-shadow: ${BORDERS.shadowSmall}; + border-radius: ${BORDERS.borderRadius16}; + background-color: ${COLORS.white}; + } +` + +const SIMPLE_CONTENT_CONTAINER_STYLE = css` + display: ${DISPLAY_FLEX}; + flex-direction: ${DIRECTION_COLUMN}; + justify-content: ${JUSTIFY_SPACE_BETWEEN}; + width: 100%; + height: 100%; + padding: ${SPACING.spacing32}; + flex: 1; + grid-gap: ${SPACING.spacing24}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - margin-top: -${SPACING.spacing68}; // See EXEC-520. This clearly isn't ideal. + grid-gap: ${SPACING.spacing32}; } ` diff --git a/app/src/organisms/DropTipWizardFlows/DropTipWizardFlows.tsx b/app/src/organisms/DropTipWizardFlows/DropTipWizardFlows.tsx index 555ba854c36..6c7c4530af2 100644 --- a/app/src/organisms/DropTipWizardFlows/DropTipWizardFlows.tsx +++ b/app/src/organisms/DropTipWizardFlows/DropTipWizardFlows.tsx @@ -5,7 +5,11 @@ import { DropTipWizard } from './DropTipWizard' import type { PipetteModelSpecs, RobotType } from '@opentrons/shared-data' import type { PipetteData } from '@opentrons/api-client' -import type { FixitCommandTypeUtils, IssuedCommandsType } from './types' +import type { + DropTipModalStyle, + FixitCommandTypeUtils, + IssuedCommandsType, +} from './types' /** Provides the user toggle for rendering Drop Tip Wizard Flows. * @@ -31,7 +35,8 @@ export interface DropTipWizardFlowsProps { instrumentModelSpecs: PipetteModelSpecs /* isTakeover allows for optionally specifying a different callback if a different client cancels the "setup" type flow. */ closeFlow: (isTakeover?: boolean) => void - /* Optional. If provided, DT will issue "fixit" commands and render alternate Error Recovery compatible views. */ + modalStyle: DropTipModalStyle + /* Optional. If provided, DT will issue "fixit" commands. */ fixitCommandTypeUtils?: FixitCommandTypeUtils } diff --git a/app/src/organisms/DropTipWizardFlows/ErrorInfo.tsx b/app/src/organisms/DropTipWizardFlows/ErrorInfo.tsx new file mode 100644 index 00000000000..4ae0ef5ad60 --- /dev/null +++ b/app/src/organisms/DropTipWizardFlows/ErrorInfo.tsx @@ -0,0 +1,72 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { css } from 'styled-components' + +import { + DISPLAY_FLEX, + DIRECTION_COLUMN, + SPACING, + ALIGN_CENTER, + COLORS, + Icon, + Flex, + StyledText, + JUSTIFY_CENTER, + JUSTIFY_FLEX_END, + RESPONSIVENESS, + TEXT_ALIGN_CENTER, +} from '@opentrons/components' + +import type { DropTipWizardContainerProps } from './types' + +export function ErrorInfo({ + errorComponents, + errorDetails, +}: DropTipWizardContainerProps): JSX.Element { + const { button, subHeader } = errorComponents + const { t } = useTranslation('drop_tip_wizard') + + return ( + <> + + + + {errorDetails?.header ?? t('error_dropping_tips')} + + + {subHeader} + + + {button} + + ) +} + +const CONTAINER_STYLE = css` + display: ${DISPLAY_FLEX}; + flex-direction: ${DIRECTION_COLUMN}; + grid-gap: ${SPACING.spacing16}; + padding: ${SPACING.spacing40} ${SPACING.spacing16}; + align-items: ${ALIGN_CENTER}; + justify-content: ${JUSTIFY_CENTER}; + text-align: ${TEXT_ALIGN_CENTER}; + margin-top: ${SPACING.spacing16}; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + grid-gap: ${SPACING.spacing24}; + padding: ${SPACING.spacing40}; + } +` + +const ICON_STYLE = css` + width: 40px; + height: 40px; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + width: 60px; + height: 60px; + } +` diff --git a/app/src/organisms/DropTipWizardFlows/ExitConfirmation.tsx b/app/src/organisms/DropTipWizardFlows/ExitConfirmation.tsx index 453bf527307..69f1a3270a7 100644 --- a/app/src/organisms/DropTipWizardFlows/ExitConfirmation.tsx +++ b/app/src/organisms/DropTipWizardFlows/ExitConfirmation.tsx @@ -1,40 +1,43 @@ import * as React from 'react' -import { useSelector } from 'react-redux' import { Trans, useTranslation } from 'react-i18next' +import { css } from 'styled-components' import { - Flex, COLORS, - SPACING, - AlertPrimaryButton, - JUSTIFY_FLEX_END, StyledText, - PrimaryButton, + Icon, + Flex, + RESPONSIVENESS, + DISPLAY_FLEX, + SPACING, + DIRECTION_COLUMN, + ALIGN_CENTER, + JUSTIFY_CENTER, + TEXT_ALIGN_CENTER, } from '@opentrons/components' -import { getIsOnDevice } from '../../redux/config' -import { SimpleWizardBody } from '../../molecules/SimpleWizardBody' -import { SmallButton } from '../../atoms/buttons' +import { DropTipFooterButtons } from './shared' import type { DropTipWizardContainerProps } from './types' -type ExitConfirmationProps = DropTipWizardContainerProps & { - handleExit: () => void - handleGoBack: () => void -} - -export function ExitConfirmation(props: ExitConfirmationProps): JSX.Element { - const { handleGoBack, handleExit, mount } = props - const { t } = useTranslation(['drop_tip_wizard', 'shared']) +export function ExitConfirmation( + props: DropTipWizardContainerProps +): JSX.Element { + const { mount, cancelExit, toggleExitInitiated, confirmExit } = props + const { t } = useTranslation('drop_tip_wizard') - const isOnDevice = useSelector(getIsOnDevice) + const handleExit = (): void => { + toggleExitInitiated() + confirmExit() + } return ( - + + + + {t('remove_any_attached_tips')} + - } - marginTop={isOnDevice ? '-2rem' : undefined} - > - {isOnDevice ? ( - - - - - ) : ( - - - {t('shared:go_back')} - - - {t('exit_and_home_pipette')} - - - )} - +
+ + ) } + +const CONTAINER_STYLE = css` + display: ${DISPLAY_FLEX}; + flex-direction: ${DIRECTION_COLUMN}; + grid-gap: ${SPACING.spacing16}; + padding: ${SPACING.spacing40} ${SPACING.spacing16}; + align-items: ${ALIGN_CENTER}; + justify-content: ${JUSTIFY_CENTER}; + text-align: ${TEXT_ALIGN_CENTER}; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + grid-gap: ${SPACING.spacing24}; + padding: ${SPACING.spacing40}; + } +` + +const ICON_STYLE = css` + width: 40px; + height: 40px; + color: ${COLORS.red50}; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + width: 60px; + height: 60px; + } +` diff --git a/app/src/organisms/DropTipWizardFlows/JogToPosition.tsx b/app/src/organisms/DropTipWizardFlows/JogToPosition.tsx deleted file mode 100644 index 7ecc9c4a1e1..00000000000 --- a/app/src/organisms/DropTipWizardFlows/JogToPosition.tsx +++ /dev/null @@ -1,312 +0,0 @@ -import * as React from 'react' -import styled from 'styled-components' -import { useTranslation } from 'react-i18next' -import { POSITION_AND_BLOWOUT } from './constants' -import { - ALIGN_CENTER, - ALIGN_FLEX_START, - COLORS, - DIRECTION_COLUMN, - Flex, - Icon, - JUSTIFY_CENTER, - JUSTIFY_END, - JUSTIFY_FLEX_END, - JUSTIFY_SPACE_BETWEEN, - PrimaryButton, - RESPONSIVENESS, - SecondaryButton, - SPACING, - LegacyStyledText, - TEXT_ALIGN_CENTER, - TYPOGRAPHY, -} from '@opentrons/components' -// import { NeedHelpLink } from '../CalibrationPanels' -import { JogControls } from '../../molecules/JogControls' -import { SmallButton, TextOnlyButton } from '../../atoms/buttons' -import { InProgressModal } from '../../molecules/InProgressModal/InProgressModal' -import { SimpleWizardBody } from '../../molecules/SimpleWizardBody' - -import type { Jog } from '../../molecules/JogControls' -import type { DropTipWizardContainerProps } from './types' - -// TODO: get help link article URL -// const NEED_HELP_URL = '' - -const Header = styled.h1` - ${TYPOGRAPHY.h1Default} - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - ${TYPOGRAPHY.level4HeaderSemiBold} - } -` - -type ConfirmPositionProps = DropTipWizardContainerProps & { - handlePipetteAction: () => void - handleGoBack: () => void -} - -const ConfirmPosition = (props: ConfirmPositionProps): JSX.Element | null => { - const { - handlePipetteAction, - handleGoBack, - isOnDevice, - currentStep, - issuedCommandsType, - } = props - const { i18n, t } = useTranslation(['drop_tip_wizard', 'shared']) - const flowTitle = t('drop_tips') - - if (isOnDevice) { - return ( - - - - - - - {currentStep === POSITION_AND_BLOWOUT - ? t('confirm_blowout_location', { flow: flowTitle }) - : t('confirm_drop_tip_location', { flow: flowTitle })} - - - - - - - - - - - - ) - } else { - return ( - - - {/* */} - - {issuedCommandsType === 'setup' ? ( - - {t('shared:go_back')} - - ) : ( - - )} - - {currentStep === POSITION_AND_BLOWOUT - ? i18n.format(t('blowout_liquid'), 'capitalize') - : i18n.format(t('drop_tips'), 'capitalize')} - - - - - ) - } -} - -type JogToPositionProps = DropTipWizardContainerProps & { - handleGoBack: () => void - handleJog: Jog - handleProceed: () => void - body: string -} - -export const JogToPosition = ( - props: JogToPositionProps -): JSX.Element | null => { - const { - handleGoBack, - handleJog, - handleProceed, - body, - currentStep, - isOnDevice, - issuedCommandsType, - } = props - const { i18n, t } = useTranslation(['drop_tip_wizard', 'shared']) - const [ - showPositionConfirmation, - setShowPositionConfirmation, - ] = React.useState(false) - // Includes special case homing only present in this step. - const [isRobotInMotion, setIsRobotInMotion] = React.useState(false) - - const onGoBack = (): void => { - setIsRobotInMotion(true) - handleGoBack() - } - - if (showPositionConfirmation) { - return isRobotInMotion ? ( - - ) : ( - { - setIsRobotInMotion(true) - handleProceed() - }} - handleGoBack={() => { - setShowPositionConfirmation(false) - }} - /> - ) - } - - if (isOnDevice) { - return ( - - - - - - - - { - setShowPositionConfirmation(true) - }} - /> - - - - ) - } else { - return ( - - - -
- {i18n.format(t('position_the_pipette'), 'capitalize')} -
- {body} -
- {/* no animations */} - {issuedCommandsType === 'setup' ? ( - - ) : null} - - - - {/* */} - {issuedCommandsType === 'setup' ? ( - - {t('shared:go_back')} - - ) : ( - - )} - { - setShowPositionConfirmation(true) - }} - > - {t('shared:confirm_position')} - - -
- ) - } -} diff --git a/app/src/organisms/DropTipWizardFlows/Success.tsx b/app/src/organisms/DropTipWizardFlows/Success.tsx deleted file mode 100644 index a071a4ea4fc..00000000000 --- a/app/src/organisms/DropTipWizardFlows/Success.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import * as React from 'react' - -import { - StyledText, - PrimaryButton, - TEXT_TRANSFORM_CAPITALIZE, - JUSTIFY_FLEX_END, - ALIGN_CENTER, - Flex, - SPACING, - DIRECTION_COLUMN, - RESPONSIVENESS, - JUSTIFY_CENTER, -} from '@opentrons/components' - -import { SmallButton } from '../../atoms/buttons' -import SuccessIcon from '../../assets/images/icon_success.png' - -import type { DropTipWizardContainerProps } from './types' -import { css } from 'styled-components' - -type SuccessProps = DropTipWizardContainerProps & { - message: string - proceedText: string - handleProceed: () => void -} -export const Success = (props: SuccessProps): JSX.Element => { - const { - message, - proceedText, - handleProceed, - isOnDevice, - issuedCommandsType, - } = props - - return ( - - - Success Icon - - {message} - - - - {isOnDevice ? ( - - ) : ( - {proceedText} - )} - - - ) -} - -const WIZARD_CONTAINER_STYLE = css` - min-height: 394px; - flex-direction: ${DIRECTION_COLUMN}; - justify-content: ${JUSTIFY_CENTER}; - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - height: 472px; - } -` diff --git a/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx b/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx index 36a50f8e47f..b4863f5d2c9 100644 --- a/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx +++ b/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx @@ -121,6 +121,7 @@ const TipsAttachedModal = NiceModal.create( closeFlow={isTakeover => { cleanUpAndClose(isTakeover) }} + modalStyle="simple" /> ) : null} diff --git a/app/src/organisms/DropTipWizardFlows/__fixtures__/index.ts b/app/src/organisms/DropTipWizardFlows/__fixtures__/index.ts index 77ba7c3dc32..2f62804995c 100644 --- a/app/src/organisms/DropTipWizardFlows/__fixtures__/index.ts +++ b/app/src/organisms/DropTipWizardFlows/__fixtures__/index.ts @@ -24,6 +24,7 @@ export const mockDropTipWizardContainerProps: DropTipWizardContainerProps = { robotType: 'OT-3 Standard', isExiting: false, mount: 'left', + modalStyle: 'simple', isOnDevice: true, fixitCommandTypeUtils: undefined, instrumentModelSpecs: MOCK_ACTUAL_PIPETTE, diff --git a/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizard.test.tsx b/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizard.test.tsx index 52d0c1cfe6e..5f66db10011 100644 --- a/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizard.test.tsx +++ b/app/src/organisms/DropTipWizardFlows/__tests__/DropTipWizard.test.tsx @@ -7,13 +7,15 @@ import { i18n } from '../../../i18n' import { mockDropTipWizardContainerProps } from '../__fixtures__' import { DropTipWizardContent, DropTipWizardContainer } from '../DropTipWizard' import { DropTipWizardHeader } from '../DropTipWizardHeader' -import { InProgressModal } from '../../../molecules/InProgressModal/InProgressModal' +import { InProgressModal } from '../../../molecules/InProgressModal' import { ExitConfirmation } from '../ExitConfirmation' -import { SimpleWizardBody } from '../../../molecules/SimpleWizardBody' -import { BeforeBeginning } from '../BeforeBeginning' -import { ChooseLocation } from '../ChooseLocation' -import { JogToPosition } from '../JogToPosition' -import { Success } from '../Success' +import { + BeforeBeginning, + ChooseLocation, + JogToPosition, + Success, +} from '../steps' +import { ErrorInfo } from '../ErrorInfo' import { BEFORE_BEGINNING, CHOOSE_BLOWOUT_LOCATION, @@ -24,13 +26,10 @@ import { DROP_TIP_SUCCESS, } from '../constants' -vi.mock('../../../molecules/InProgressModal/InProgressModal') +vi.mock('../../../molecules/InProgressModal') vi.mock('../ExitConfirmation') -vi.mock('../../../molecules/SimpleWizardBody') -vi.mock('../BeforeBeginning') -vi.mock('../ChooseLocation') -vi.mock('../JogToPosition') -vi.mock('../Success') +vi.mock('../steps') +vi.mock('../ErrorInfo') vi.mock('../DropTipWizardHeader') const renderDropTipWizardContainer = ( @@ -85,11 +84,11 @@ describe('DropTipWizardContent', () => { vi.mocked(ExitConfirmation).mockReturnValue(
MOCK_EXIT_CONFIRMATION
) - vi.mocked(SimpleWizardBody).mockReturnValue(
MOCK_ERROR_SCREEN
) vi.mocked(BeforeBeginning).mockReturnValue(
MOCK_BEFORE_BEGINNING
) vi.mocked(ChooseLocation).mockReturnValue(
MOCK_CHOOSE_LOCATION
) vi.mocked(JogToPosition).mockReturnValue(
MOCK_JOG_TO_POSITION
) vi.mocked(Success).mockReturnValue(
MOCK_SUCCESS
) + vi.mocked(ErrorInfo).mockReturnValue(
MOCK_ERROR_INFO
) }) it(`renders InProgressModal when activeMaintenanceRunId is null`, () => { @@ -116,13 +115,13 @@ describe('DropTipWizardContent', () => { screen.getByText('MOCK_EXIT_CONFIRMATION') }) - it(`renders SimpleWizardBody when errorDetails is not null`, () => { + it(`renders ErrorInfo when errorDetails is not null`, () => { renderDropTipWizardContent({ ...props, errorDetails: { message: 'MOCK_MESSAGE' }, }) - screen.getByText('MOCK_ERROR_SCREEN') + screen.getByText('MOCK_ERROR_INFO') }) it(`renders BeforeBeginning when currentStep is ${BEFORE_BEGINNING}`, () => { @@ -172,21 +171,4 @@ describe('DropTipWizardContent', () => { screen.getByText('MOCK_SUCCESS') }) - - it('renders alternative success button copy when the commandType is fixit', () => { - renderDropTipWizardContent({ - ...props, - currentStep: DROP_TIP_SUCCESS, - fixitCommandTypeUtils: { - copyOverrides: { tipDropCompleteBtnCopy: 'proceed_to_tip_selection' }, - } as any, - }) - - expect(vi.mocked(Success)).toHaveBeenCalledWith( - expect.objectContaining({ - proceedText: 'proceed_to_tip_selection', - }), - expect.anything() - ) - }) }) diff --git a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipMaintenanceRun.tsx b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipMaintenanceRun.tsx index 64f3f05ec96..2b181d6937b 100644 --- a/app/src/organisms/DropTipWizardFlows/hooks/useDropTipMaintenanceRun.tsx +++ b/app/src/organisms/DropTipWizardFlows/hooks/useDropTipMaintenanceRun.tsx @@ -62,7 +62,7 @@ export function useDropTipMaintenanceRun({ type UseCreateDropTipMaintenanceRunParams = Omit< UseDropTipMaintenanceRunParams, - 'robotType' | 'closeFlow' + 'robotType' | 'closeFlow' | 'modalStyle' > & { setCreatedMaintenanceRunId: (id: string) => void instrumentModelName?: PipetteModelSpecs['name'] diff --git a/app/src/organisms/DropTipWizardFlows/shared/DropTipFooterButtons.tsx b/app/src/organisms/DropTipWizardFlows/shared/DropTipFooterButtons.tsx new file mode 100644 index 00000000000..e2f4bcd65ee --- /dev/null +++ b/app/src/organisms/DropTipWizardFlows/shared/DropTipFooterButtons.tsx @@ -0,0 +1,106 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { css } from 'styled-components' + +import { + PrimaryButton, + AlertPrimaryButton, + Flex, + JUSTIFY_SPACE_BETWEEN, + ALIGN_CENTER, + Box, + RESPONSIVENESS, + ALIGN_FLEX_END, +} from '@opentrons/components' + +import { TextOnlyButton, SmallButton } from '../../../atoms/buttons' + +interface DropTipFooterButtonsProps { + primaryBtnOnClick: () => void + primaryBtnTextOverride?: string + primaryBtnDisabled?: boolean + primaryBtnStyle?: 'defaultStyle' | 'alertStyle' + /* Typically the "Go back" button. If no onClick is supplied, the button does not render. */ + secondaryBtnOnClick?: () => void +} + +export function DropTipFooterButtons( + props: DropTipFooterButtonsProps +): JSX.Element { + return ( + + + + + + + ) +} + +function DropTipGoBackButton({ + secondaryBtnOnClick, +}: DropTipFooterButtonsProps): JSX.Element | null { + const showGoBackBtn = secondaryBtnOnClick != null + const { t } = useTranslation('drop_tip_wizard') + return showGoBackBtn ? ( + + + + ) : ( + + ) +} + +function DropTipPrimaryBtn({ + primaryBtnOnClick, + primaryBtnTextOverride, + primaryBtnDisabled, + primaryBtnStyle, +}: DropTipFooterButtonsProps): JSX.Element { + const { t } = useTranslation('drop_tip_wizard') + + return ( + <> + + {primaryBtnStyle === 'alertStyle' ? ( + + {primaryBtnTextOverride ?? t('continue')} + + ) : ( + + {primaryBtnTextOverride ?? t('continue')} + + )} + + ) +} + +const DESKTOP_ONLY_BUTTON = css` + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + display: none; + } +` + +const ODD_ONLY_BUTTON = css` + @media not (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { + display: none; + } +` diff --git a/app/src/organisms/DropTipWizardFlows/shared/index.ts b/app/src/organisms/DropTipWizardFlows/shared/index.ts new file mode 100644 index 00000000000..1405936b4f8 --- /dev/null +++ b/app/src/organisms/DropTipWizardFlows/shared/index.ts @@ -0,0 +1 @@ +export * from './DropTipFooterButtons' diff --git a/app/src/organisms/DropTipWizardFlows/steps/BeforeBeginning.tsx b/app/src/organisms/DropTipWizardFlows/steps/BeforeBeginning.tsx new file mode 100644 index 00000000000..36e9b1f4632 --- /dev/null +++ b/app/src/organisms/DropTipWizardFlows/steps/BeforeBeginning.tsx @@ -0,0 +1,261 @@ +import * as React from 'react' +import { css } from 'styled-components' +import { useTranslation } from 'react-i18next' + +import { + ALIGN_CENTER, + BORDERS, + COLORS, + DIRECTION_COLUMN, + CURSOR_POINTER, + DIRECTION_ROW, + DISPLAY_FLEX, + Flex, + JUSTIFY_CENTER, + JUSTIFY_FLEX_START, + JUSTIFY_SPACE_AROUND, + RESPONSIVENESS, + SPACING, + LegacyStyledText, + StyledText, +} from '@opentrons/components' + +import { MediumButton } from '../../../atoms/buttons' +import { DT_ROUTES } from '../constants' +import { DropTipFooterButtons } from '../shared' + +import blowoutVideo from '../../../assets/videos/droptip-wizard/Blowout-Liquid.webm' +import droptipVideo from '../../../assets/videos/droptip-wizard/Drop-tip.webm' + +import type { DropTipWizardContainerProps } from '../types' + +type FlowType = 'blowout' | 'drop_tips' | null + +export const BeforeBeginning = ({ + proceedToRoute, + isOnDevice, + issuedCommandsType, + fixitCommandTypeUtils, + modalStyle, +}: DropTipWizardContainerProps): JSX.Element | null => { + const { t } = useTranslation('drop_tip_wizard') + const [flowType, setFlowType] = React.useState(null) + + const handleProceed = (): void => { + if (flowType === 'blowout') { + void proceedToRoute(DT_ROUTES.BLOWOUT) + } else if (flowType === 'drop_tips') { + void proceedToRoute(DT_ROUTES.DROP_TIP) + } + } + + const buildTopText = (): string => { + if (issuedCommandsType === 'fixit') { + return fixitCommandTypeUtils?.copyOverrides + .beforeBeginningTopText as string + } else { + return t('before_you_begin_do_you_want_to_blowout') + } + } + + if (isOnDevice) { + return ( + <> + + + {buildTopText()} + + { + setFlowType('blowout') + }} + buttonText={t('yes_blow_out_liquid')} + /> + { + setFlowType('drop_tips') + }} + buttonText={t('no_proceed_to_drop_tip')} + /> + + + + ) + } else { + return ( + <> + + + {buildTopText()} + + + { + setFlowType('blowout') + }} + videoSrc={blowoutVideo} + text={t('yes_blow_out_liquid')} + /> + { + setFlowType('drop_tips') + }} + videoSrc={droptipVideo} + text={t('no_proceed_to_drop_tip')} + /> + + + + + ) + } +} + +function DropTipOption({ + flowType, + currentFlow, + onClick, + videoSrc, + text, +}: { + flowType: 'blowout' | 'drop_tips' + currentFlow: FlowType + onClick: () => void + videoSrc: string + text: string +}): JSX.Element { + return ( + + + {text} + + ) +} + +const UNSELECTED_OPTIONS_STYLE = css` + background-color: ${COLORS.white}; + border: 1px solid ${COLORS.grey30}; + border-radius: ${BORDERS.borderRadius8}; + height: 12.5625rem; + width: 14.5625rem; + cursor: ${CURSOR_POINTER}; + flex-direction: ${DIRECTION_COLUMN}; + justify-content: ${JUSTIFY_CENTER}; + align-items: ${ALIGN_CENTER}; + grid-gap: ${SPACING.spacing8}; + + &:hover { + border: 1px solid ${COLORS.grey35}; + } + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + flex-direction: ${DIRECTION_ROW}; + justify-content: ${JUSTIFY_FLEX_START}; + background-color: ${COLORS.blue35}; + border-width: 0; + border-radius: ${BORDERS.borderRadius16}; + padding: ${SPACING.spacing24}; + height: 5.25rem; + width: 57.8125rem; + + &:hover { + border-width: 0px; + } + } +` +const SELECTED_OPTIONS_STYLE = css` + ${UNSELECTED_OPTIONS_STYLE} + border: 1px solid ${COLORS.blue50}; + background-color: ${COLORS.blue30}; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + border-width: 0px; + background-color: ${COLORS.blue50}; + color: ${COLORS.white}; + + &:hover { + border-width: 0px; + background-color: ${COLORS.blue50}; + } + } +` + +const CONTAINER_STYLE = css` + display: ${DISPLAY_FLEX}; + flex-direction: ${DIRECTION_COLUMN}; + grid-gap: ${SPACING.spacing16}; +` + +const ODD_MEDIUM_BUTTON_STYLE = css` + flex: 1; + justify-content: ${JUSTIFY_FLEX_START}; + padding-left: ${SPACING.spacing24}; + height: 5.25rem; +` + +const SHARED_GIF_CONTAINER_STYLE = ` + justify-content: ${JUSTIFY_SPACE_AROUND}; + align-items: ${ALIGN_CENTER}; + grid-gap: ${SPACING.spacing16}; +` + +const SIMPLE_DESKTOP_GIF_CONTAINER_STYLE = css` + ${SHARED_GIF_CONTAINER_STYLE} + height: 18.75rem; +` + +const INTERVENTION_DESKTOP_GIF_CONTAINER_STYLE = css` + ${SHARED_GIF_CONTAINER_STYLE} + height: 14.563rem; +` diff --git a/app/src/organisms/DropTipWizardFlows/steps/ChooseLocation.tsx b/app/src/organisms/DropTipWizardFlows/steps/ChooseLocation.tsx new file mode 100644 index 00000000000..a7915c123ec --- /dev/null +++ b/app/src/organisms/DropTipWizardFlows/steps/ChooseLocation.tsx @@ -0,0 +1,92 @@ +import * as React from 'react' +import { Trans, useTranslation } from 'react-i18next' + +import { + DIRECTION_COLUMN, + Flex, + SPACING, + LegacyStyledText, + StyledText, +} from '@opentrons/components' +import { getDeckDefFromRobotType } from '@opentrons/shared-data' + +import { TwoColumn, DeckMapContent } from '../../../molecules/InterventionModal' +import { CHOOSE_BLOWOUT_LOCATION } from '../constants' +import { DropTipFooterButtons } from '../shared' + +import type { ModuleLocation } from '@opentrons/shared-data' +import type { DropTipWizardContainerProps } from '../types' + +export const ChooseLocation = ({ + robotType, + dropTipCommands, + proceedWithConditionalClose, + goBackRunValid, + currentStep, + isOnDevice, +}: DropTipWizardContainerProps): JSX.Element | null => { + const { moveToAddressableArea } = dropTipCommands + const { t } = useTranslation('drop_tip_wizard') + const [ + selectedLocation, + setSelectedLocation, + ] = React.useState() + const deckDef = getDeckDefFromRobotType(robotType) + + const handleConfirmPosition = (): void => { + const deckSlot = deckDef.locations.addressableAreas.find( + l => l.id === selectedLocation?.slotName + )?.id + + if (deckSlot != null) { + void moveToAddressableArea(deckSlot).then(() => { + proceedWithConditionalClose() + }) + } + } + + const buildTitleText = (): string => + currentStep === CHOOSE_BLOWOUT_LOCATION + ? t('choose_blowout_location') + : t('choose_drop_tip_location') + + const buildBodyText = (): string => { + if (currentStep === CHOOSE_BLOWOUT_LOCATION) { + return isOnDevice ? 'select_blowout_slot_odd' : 'select_blowout_slot' + } else { + return isOnDevice ? 'select_drop_tip_slot_odd' : 'select_drop_tip_slot' + } + } + + return ( + <> + + + + {buildTitleText()} + + + }} + /> + + + + + + + ) +} diff --git a/app/src/organisms/DropTipWizardFlows/steps/JogToPosition.tsx b/app/src/organisms/DropTipWizardFlows/steps/JogToPosition.tsx new file mode 100644 index 00000000000..7277b78aff5 --- /dev/null +++ b/app/src/organisms/DropTipWizardFlows/steps/JogToPosition.tsx @@ -0,0 +1,100 @@ +import * as React from 'react' +import { useTranslation } from 'react-i18next' +import { css } from 'styled-components' + +import { + DIRECTION_COLUMN, + Flex, + StyledText, + SPACING, + LegacyStyledText, + RESPONSIVENESS, +} from '@opentrons/components' + +import { POSITION_AND_BLOWOUT } from '../constants' +import { JogControls } from '../../../molecules/JogControls' +import { DropTipFooterButtons } from '../shared' + +import type { DropTipWizardContainerProps } from '../types' +import type { UseConfirmPositionResult } from '../ConfirmPosition' + +type JogToPositionProps = DropTipWizardContainerProps & UseConfirmPositionResult + +export const JogToPosition = ({ + goBackRunValid, + dropTipCommands, + currentStep, + isOnDevice, + toggleShowConfirmPosition, + modalStyle, +}: JogToPositionProps): JSX.Element | null => { + const { handleJog } = dropTipCommands + const { t } = useTranslation('drop_tip_wizard') + + return ( + <> + + + {t('position_the_pipette')} + + + {currentStep === POSITION_AND_BLOWOUT + ? t('position_and_blowout') + : t('position_and_drop_tip')} + + + + + + + + ) +} + +const TITLE_SECTION_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + grid-gap: ${SPACING.spacing16}; + + @media (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { + display: none; + } +` + +const SHARED_CONTENT_SECTION_STYLE = ` + flex-direction: ${DIRECTION_COLUMN}; + grid-gap: ${SPACING.spacing16}; +` + +const SIMPLE_CONTENT_SECTION_STYLE = css` + ${SHARED_CONTENT_SECTION_STYLE} + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + grid-gap: 1.5rem; + } +` + +const INTERVENTION_CONTENT_SECTION_STYLE = css` + ${SHARED_CONTENT_SECTION_STYLE} + grid-gap: ${SPACING.spacing40}; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + grid-gap: 0.9rem; + } +` diff --git a/app/src/organisms/DropTipWizardFlows/steps/Success.tsx b/app/src/organisms/DropTipWizardFlows/steps/Success.tsx new file mode 100644 index 00000000000..12c56fa48de --- /dev/null +++ b/app/src/organisms/DropTipWizardFlows/steps/Success.tsx @@ -0,0 +1,110 @@ +import * as React from 'react' +import { css } from 'styled-components' +import { useTranslation } from 'react-i18next' + +import { + StyledText, + ALIGN_CENTER, + Flex, + SPACING, + DIRECTION_COLUMN, + RESPONSIVENESS, + JUSTIFY_CENTER, +} from '@opentrons/components' + +import { DropTipFooterButtons } from '../shared' +import { BLOWOUT_SUCCESS, DROP_TIP_SUCCESS, DT_ROUTES } from '../constants' + +import SuccessIcon from '../../../assets/images/icon_success.png' + +import type { DropTipWizardContainerProps } from '../types' + +export const Success = ({ + currentStep, + proceedToRoute, + fixitCommandTypeUtils, + proceedWithConditionalClose, + modalStyle, +}: DropTipWizardContainerProps): JSX.Element => { + const { tipDropComplete } = fixitCommandTypeUtils?.buttonOverrides ?? {} + const { t } = useTranslation('drop_tip_wizard') + + // Route to the drop tip route if user is at the blowout success screen, otherwise proceed conditionally. + const handleProceed = (): void => { + if (currentStep === BLOWOUT_SUCCESS) { + void proceedToRoute(DT_ROUTES.DROP_TIP) + } else { + // Clear the error recovery submap upon completion of drop tip wizard. + fixitCommandTypeUtils?.reportMap(null) + + if (tipDropComplete != null) { + tipDropComplete() + } else { + proceedWithConditionalClose() + } + } + } + + const buildProceedText = (): string => { + if (fixitCommandTypeUtils != null && currentStep === DROP_TIP_SUCCESS) { + return fixitCommandTypeUtils.copyOverrides.tipDropCompleteBtnCopy + } else { + return currentStep === BLOWOUT_SUCCESS ? t('continue') : t('exit') + } + } + + return ( + <> + + Success Icon + + {currentStep === BLOWOUT_SUCCESS + ? t('blowout_complete') + : t('drop_tip_complete')} + + + + + ) +} + +const WIZARD_CONTAINER_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + justify-content: ${JUSTIFY_CENTER}; + align-items: ${ALIGN_CENTER}; + grid-gap: ${SPACING.spacing24}; + height: 100%; + width: 100%; +` + +const SHARED_IMAGE_STYLE = ` + width: 170px; + height: 141px; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + width: 282px; + height: 234px; + margin-top: 0; + } +` + +const SIMPLE_IMAGE_STYLE = css` + ${SHARED_IMAGE_STYLE} + margin-top: ${SPACING.spacing32}; +` + +const INTERVENTION_IMAGE_STYLE = css` + ${SHARED_IMAGE_STYLE} + margin-top: ${SPACING.spacing60}; +` diff --git a/app/src/organisms/DropTipWizardFlows/steps/index.ts b/app/src/organisms/DropTipWizardFlows/steps/index.ts new file mode 100644 index 00000000000..df34b2caa6f --- /dev/null +++ b/app/src/organisms/DropTipWizardFlows/steps/index.ts @@ -0,0 +1,4 @@ +export * from './BeforeBeginning' +export * from './ChooseLocation' +export * from './JogToPosition' +export * from './Success' diff --git a/app/src/organisms/DropTipWizardFlows/types.ts b/app/src/organisms/DropTipWizardFlows/types.ts index 4238d9ac8a0..43243ba673b 100644 --- a/app/src/organisms/DropTipWizardFlows/types.ts +++ b/app/src/organisms/DropTipWizardFlows/types.ts @@ -11,6 +11,7 @@ export interface ErrorDetails { } export type IssuedCommandsType = 'setup' | 'fixit' +export type DropTipModalStyle = 'simple' | 'intervention' interface CopyOverrides { tipDropCompleteBtnCopy: string diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx index fa159677903..b830f9b3114 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx @@ -184,15 +184,14 @@ function DropTipFlowsContainer( const fixitCommandTypeUtils = useDropTipFlowUtils(props) return ( - - - + ) } @@ -204,7 +203,6 @@ export function useDropTipFlowUtils({ subMapUtils, routeUpdateActions, recoveryMap, - failedPipetteInfo, }: RecoveryContentProps): FixitCommandTypeUtils { const { t } = useTranslation('error_recovery') const { diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx index e044d46054f..d0a098ded3d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx @@ -50,6 +50,7 @@ export function RecoveryInterventionModal({ const SMALL_MODAL_STYLE = css` height: 22rem; padding: ${SPACING.spacing32}; + width: 100%; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { padding: ${SPACING.spacing32}; @@ -58,4 +59,9 @@ const SMALL_MODAL_STYLE = css` ` const LARGE_MODAL_STYLE = css` height: 26.75rem; + width: 100%; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + height: 100%; + } ` diff --git a/app/src/pages/ODD/InstrumentDetail/InstrumentDetailOverflowMenu.tsx b/app/src/pages/ODD/InstrumentDetail/InstrumentDetailOverflowMenu.tsx index e21ff66864f..f2572335241 100644 --- a/app/src/pages/ODD/InstrumentDetail/InstrumentDetailOverflowMenu.tsx +++ b/app/src/pages/ODD/InstrumentDetail/InstrumentDetailOverflowMenu.tsx @@ -152,6 +152,7 @@ const InstrumentDetailsOverflowMenu = NiceModal.create( mount={instrument.mount} instrumentModelSpecs={pipetteModelSpecs} closeFlow={modal.remove} + modalStyle="simple" /> ) : null}