diff --git a/app/src/assets/localization/en/error_recovery.json b/app/src/assets/localization/en/error_recovery.json index 8bf00af168d..65f1fa50314 100644 --- a/app/src/assets/localization/en/error_recovery.json +++ b/app/src/assets/localization/en/error_recovery.json @@ -16,6 +16,7 @@ "continue_run_now": "Continue run now", "continue_to_drop_tip": "Continue to drop tip", "error": "Error", + "error_details": "Error details", "error_on_robot": "Error on {{robot}}", "failed_dispense_step_not_completed": "The failed dispense step will not be completed. The run will continue from the next step with the attached tips.Close the robot door before proceeding.", "failed_step": "Failed step", @@ -40,6 +41,7 @@ "recovery_action_failed": "{{action}} failed", "recovery_mode": "Recovery Mode", "recovery_mode_explanation": "Recovery Mode provides you with guided and manual controls for handling errors at runtime.
You can make changes to ensure the step in progress when the error occurred can be completed or choose to cancel the protocol. When changes are made and no subsequent errors are detected, the method completes. Depending on the conditions that caused the error, you will only be provided with appropriate options.", + "remove_tips_from_pipette": "Remove tips from {{mount}} pipette before canceling the run?", "replace_tips_and_select_location": "It's best to replace tips and select the last location used for tip pickup.", "replace_used_tips_in_rack_location": "Replace used tips in rack location {{location}} in slot {{slot}}", "replace_with_new_tip_rack": "Replace with new tip rack in slot {{slot}}", @@ -72,6 +74,5 @@ "tip_not_detected": "Tip not detected", "view_error_details": "View error details", "view_recovery_options": "View recovery options", - "you_can_still_drop_tips": "You can still drop the attached tips before proceeding to tip selection.", - "remove_tips_from_pipette": "Remove tips from {{mount}} pipette before canceling the run?" + "you_can_still_drop_tips": "You can still drop the attached tips before proceeding to tip selection." } diff --git a/app/src/molecules/InterventionModal/index.tsx b/app/src/molecules/InterventionModal/index.tsx index 3faa3b34f2c..aec8c9fea22 100644 --- a/app/src/molecules/InterventionModal/index.tsx +++ b/app/src/molecules/InterventionModal/index.tsx @@ -179,6 +179,8 @@ const ICON_STYLE = css` width: ${SPACING.spacing16}; height: ${SPACING.spacing16}; margin: ${SPACING.spacing4}; + cursor: pointer; + @media (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) { width: ${SPACING.spacing32}; height: ${SPACING.spacing32}; diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx index 9b090a67259..f8338f2521b 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx @@ -297,7 +297,6 @@ export function ProtocolRunHeader({ <> {isERActive ? ( ( - + {t('view_error_details')} ) @@ -119,6 +126,7 @@ export function ErrorRecoveryComponent( !isDoorOpen && route === RECOVERY_MAP.DROP_TIP_FLOWS.ROUTE && step !== RECOVERY_MAP.DROP_TIP_FLOWS.STEPS.BEGIN_REMOVAL + const desktopType = isLargeDesktopStyle ? 'desktop-large' : 'desktop-small' return ( {showModal ? ( - + ) : null} {buildInterventionContent()} diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx index 5e6d6685195..9fe5f22c413 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx @@ -11,7 +11,6 @@ import { StyledText, RESPONSIVENESS, } from '@opentrons/components' -import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { RadioButton } from '../../../atoms/buttons' import { @@ -187,10 +186,10 @@ function DropTipFlowsContainer( props: RecoveryContentProps ): JSX.Element | null { const { + robotType, tipStatusUtils, routeUpdateActions, recoveryCommands, - isFlex, currentRecoveryOptionUtils, } = props const { DROP_TIP_FLOWS, ROBOT_CANCELING, RETRY_NEW_TIPS } = RECOVERY_MAP @@ -229,7 +228,7 @@ function DropTipFlowsContainer( return ( {title} { runStatus: RUN_STATUS_AWAITING_RECOVERY, failedCommand: mockFailedCommand, runId: 'MOCK_RUN_ID', - isFlex: true, protocolAnalysis: {} as any, } vi.mocked(ErrorRecoveryWizard).mockReturnValue(
MOCK WIZARD
) diff --git a/app/src/organisms/ErrorRecoveryFlows/constants.ts b/app/src/organisms/ErrorRecoveryFlows/constants.ts index 846f7e2efc0..d61805d1777 100644 --- a/app/src/organisms/ErrorRecoveryFlows/constants.ts +++ b/app/src/organisms/ErrorRecoveryFlows/constants.ts @@ -1,6 +1,6 @@ import { css } from 'styled-components' -import { SPACING, TYPOGRAPHY, RESPONSIVENESS } from '@opentrons/components' +import { SPACING, RESPONSIVENESS } from '@opentrons/components' import type { StepOrder } from './types' @@ -204,10 +204,6 @@ export const INVALID = 'INVALID' as const * Styling */ -export const BODY_TEXT_STYLE = css` - ${TYPOGRAPHY.bodyTextRegular}; -` - export const ODD_SECTION_TITLE_STYLE = css` margin-bottom: ${SPACING.spacing16}; ` diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts index ff05642ff18..c0d867ea25a 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts @@ -1,4 +1,5 @@ import { useInstrumentsQuery } from '@opentrons/react-api-client' +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { useRouteUpdateActions } from './useRouteUpdateActions' import { useRecoveryCommands } from './useRecoveryCommands' @@ -56,7 +57,6 @@ export interface ERUtilsResults { const SUBSEQUENT_COMMAND_DEPTH = 2 // Builds various Error Recovery utilities. export function useERUtils({ - isFlex, failedCommand, runId, toggleERWizard, @@ -96,7 +96,7 @@ export function useERUtils({ const tipStatusUtils = useRecoveryTipStatus({ runId, - isFlex, + isFlex: robotType === FLEX_ROBOT_TYPE, runRecord, attachedInstruments, }) diff --git a/app/src/organisms/ErrorRecoveryFlows/index.tsx b/app/src/organisms/ErrorRecoveryFlows/index.tsx index 6e4e2bf1fd3..677cd512986 100644 --- a/app/src/organisms/ErrorRecoveryFlows/index.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/index.tsx @@ -106,7 +106,6 @@ export interface ErrorRecoveryFlowsProps { runId: string runStatus: RunStatus | null failedCommand: FailedCommand | null - isFlex: boolean protocolAnalysis: CompletedProtocolAnalysis | null } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx index de4829d937f..f1921b83d02 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx @@ -1,9 +1,11 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' +import { css } from 'styled-components' import { Flex, + StyledText, SPACING, COLORS, BORDERS, @@ -12,16 +14,22 @@ import { import { useErrorName } from '../hooks' import { Modal } from '../../../molecules/Modal' -import { getTopPortalEl } from '../../../App/portal' +import { getModalPortalEl, getTopPortalEl } from '../../../App/portal' import { ERROR_KINDS } from '../constants' import { InlineNotification } from '../../../atoms/InlineNotification' import { StepInfo } from './StepInfo' import { getErrorKind } from '../utils' +import { + LegacyModalShell, + LegacyModalHeader, +} from '../../../molecules/LegacyModal' import type { RobotType } from '@opentrons/shared-data' +import type { IconProps } from '@opentrons/components' import type { ModalHeaderBaseProps } from '../../../molecules/Modal/types' import type { ERUtilsResults } from '../hooks' import type { ErrorRecoveryFlowsProps } from '..' +import type { DesktopSizeType } from '../types' export function useErrorDetailsModal(): { showModal: boolean @@ -41,6 +49,7 @@ type ErrorDetailsModalProps = ErrorRecoveryFlowsProps & toggleModal: () => void isOnDevice: boolean robotType: RobotType + desktopType: DesktopSizeType } export function ErrorDetailsModal(props: ErrorDetailsModalProps): JSX.Element { @@ -64,7 +73,101 @@ export function ErrorDetailsModal(props: ErrorDetailsModalProps): JSX.Element { hasExitIcon: true, } - return createPortal( + const buildModal = (): JSX.Element => { + if (isOnDevice) { + return createPortal( + + {getIsOverpressureErrorKind() ? : null} + , + getTopPortalEl() + ) + } else { + return createPortal( + + {getIsOverpressureErrorKind() ? : null} + , + getModalPortalEl() + ) + } + } + + return buildModal() +} + +type ErrorDetailsModalType = ErrorDetailsModalProps & { + children: React.ReactNode + modalHeader: ModalHeaderBaseProps + toggleModal: () => void + desktopType: DesktopSizeType +} + +export function ErrorDetailsModalDesktop( + props: ErrorDetailsModalType +): JSX.Element { + const { children, modalHeader, toggleModal, desktopType } = props + const { t } = useTranslation('error_recovery') + + const buildIcon = (): IconProps => { + return { + name: 'information', + color: COLORS.grey60, + size: SPACING.spacing20, + marginRight: SPACING.spacing8, + } + } + + const buildHeader = (): JSX.Element => { + return ( + + ) + } + + return ( + + + + {modalHeader.title} + + {children} + + + + + + ) +} + +export function ErrorDetailsModalODD( + props: ErrorDetailsModalType +): JSX.Element { + const { children, modalHeader, toggleModal } = props + + return ( - {getIsOverpressureErrorKind() ? ( - - ) : null} + {children} - + - , - getTopPortalEl() + ) } -export function OverpressureBanner(props: { - isOnDevice: boolean -}): JSX.Element | null { +export function OverpressureBanner(): JSX.Element | null { const { t } = useTranslation('error_recovery') - if (props.isOnDevice) { - return ( - - ) - } else { - return null - } + return ( + + ) } + +// TODO(jh, 07-24-24): Using shared height/width constants for intervention modal sizing and the ErrorDetailsModal sizing +// would be ideal. +const DESKTOP_STEP_INFO_STYLE = css` + background-color: ${COLORS.grey30}; + grid-gap: ${SPACING.spacing10}; + border-radius: ${BORDERS.borderRadius4}; + padding: ${SPACING.spacing6} ${SPACING.spacing24} ${SPACING.spacing6} + ${SPACING.spacing12}; +` + +const DESKTOP_MODAL_STYLE_BASE = css` + width: 47rem; +` + +const DESKTOP_MODAL_STYLE_SMALL = css` + ${DESKTOP_MODAL_STYLE_BASE} + height: 26rem; +` +const DESKTOP_MODAL_STYLE_LARGE = css` + ${DESKTOP_MODAL_STYLE_BASE} + height: 31rem; +` diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx index 00a853ee99a..e044d46054f 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx @@ -8,13 +8,14 @@ import { InterventionModal } from '../../../molecules/InterventionModal' import { getModalPortalEl, getTopPortalEl } from '../../../App/portal' import type { ModalType } from '../../../molecules/InterventionModal' +import type { DesktopSizeType } from '../types' export type RecoveryInterventionModalProps = Omit< React.ComponentProps, 'type' > & { /* If on desktop, specifies the hard-coded dimensions height of the modal. */ - desktopType: 'desktop-small' | 'desktop-large' + desktopType: DesktopSizeType isOnDevice: boolean } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/StepInfo.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/StepInfo.tsx index 54fb2464124..13cb6f3a702 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/StepInfo.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/StepInfo.tsx @@ -2,7 +2,7 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' -import { Flex, DISPLAY_INLINE, LegacyStyledText } from '@opentrons/components' +import { Flex, DISPLAY_INLINE, StyledText } from '@opentrons/components' import { CommandText } from '../../../molecules/Command' @@ -10,15 +10,17 @@ import type { StyleProps } from '@opentrons/components' import type { RecoveryContentProps } from '../types' interface StepInfoProps extends StyleProps { - textStyle: React.ComponentProps['as'] stepCounts: RecoveryContentProps['stepCounts'] failedCommand: RecoveryContentProps['failedCommand'] robotType: RecoveryContentProps['robotType'] protocolAnalysis: RecoveryContentProps['protocolAnalysis'] + desktopStyle?: React.ComponentProps['desktopStyle'] + oddStyle?: React.ComponentProps['oddStyle'] } export function StepInfo({ - textStyle, + desktopStyle, + oddStyle, stepCounts, failedCommand, robotType, @@ -35,18 +37,27 @@ export function StepInfo({ const currentCopy = currentStepNumber ?? '?' const totalCopy = totalStepCount ?? '?' + const desktopStyleDefaulted = desktopStyle ?? 'bodyDefaultRegular' + const oddStyleDefaulted = oddStyle ?? 'bodyTextRegular' + return ( - + {`${t('at_step')} ${currentCopy}/${totalCopy}: `} - + {analysisCommand != null && protocolAnalysis != null ? ( ) : null} diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx index 3eb590f1a35..b63464b4382 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx @@ -44,27 +44,6 @@ describe('useErrorDetailsModal', () => { }) }) -describe('ErrorDetailsModal', () => { - let props: React.ComponentProps - - beforeEach(() => { - props = { - ...mockRecoveryContentProps, - toggleModal: vi.fn(), - robotType: 'OT-3 Standard', - } - - vi.mocked(StepInfo).mockReturnValue(
MOCK_STEP_INFO
) - }) - - it('renders ErrorDetailsModal', () => { - renderWithProviders(, { - i18nInstance: i18n, - }) - expect(screen.getByText('MOCK_STEP_INFO')).toBeInTheDocument() - }) -}) - const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, @@ -79,6 +58,7 @@ describe('ErrorDetailsModal', () => { ...mockRecoveryContentProps, toggleModal: vi.fn(), robotType: 'OT-3 Standard', + desktopType: 'desktop-small', } vi.mocked(StepInfo).mockReturnValue(
MOCK_STEP_INFO
) @@ -87,7 +67,9 @@ describe('ErrorDetailsModal', () => { ) }) - it('renders the modal with the correct content', () => { + const IS_ODD = [true, false] + + it('renders the ODD modal with the correct content', () => { render(props) expect(vi.mocked(Modal)).toHaveBeenCalledWith( expect.objectContaining({ @@ -102,21 +84,30 @@ describe('ErrorDetailsModal', () => { expect(screen.getByText('MOCK_STEP_INFO')).toBeInTheDocument() }) - it('renders the OverpressureBanner when the error kind is an overpressure error', () => { - props.failedCommand = { - ...props.failedCommand, - commandType: 'aspirate', - error: { isDefined: true, errorType: 'overpressure' }, - } as any - render(props) + it('renders the desktop modal with the correct content', () => { + render({ ...props, isOnDevice: false }) - screen.getByText('MOCK_INLINE_NOTIFICATION') + screen.getByText('MOCK_STEP_INFO') + screen.getByText('Error details') }) - it('does not render the OverpressureBanner when the error kind is not an overpressure error', () => { - render(props) + IS_ODD.forEach(isOnDevice => { + it('renders the OverpressureBanner when the error kind is an overpressure error', () => { + props.failedCommand = { + ...props.failedCommand, + commandType: 'aspirate', + error: { isDefined: true, errorType: 'overpressure' }, + } as any + render({ ...props, isOnDevice }) + + screen.getByText('MOCK_INLINE_NOTIFICATION') + }) + + it('does not render the OverpressureBanner when the error kind is not an overpressure error', () => { + render({ ...props, isOnDevice }) - expect(screen.queryByText('MOCK_INLINE_NOTIFICATION')).toBeNull() + expect(screen.queryByText('MOCK_INLINE_NOTIFICATION')).toBeNull() + }) }) }) @@ -128,7 +119,7 @@ describe('OverpressureBanner', () => { }) it('renders the InlineNotification', () => { - renderWithProviders(, { + renderWithProviders(, { i18nInstance: i18n, }) expect(vi.mocked(InlineNotification)).toHaveBeenCalledWith( diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StepInfo.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StepInfo.test.tsx index 4e7e8b393fa..9396fcf8f7d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StepInfo.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StepInfo.test.tsx @@ -25,7 +25,7 @@ describe('StepInfo', () => { ...mockRecoveryContentProps, protocolAnalysis: { commands: [mockFailedCommand] } as any, }, - textStyle: 'h4', + desktopStyle: 'bodyDefaultRegular', stepCounts: { currentStepNumber: 5, totalStepCount: 10, diff --git a/app/src/organisms/ErrorRecoveryFlows/types.ts b/app/src/organisms/ErrorRecoveryFlows/types.ts index c1f0ea49329..747000f2dbb 100644 --- a/app/src/organisms/ErrorRecoveryFlows/types.ts +++ b/app/src/organisms/ErrorRecoveryFlows/types.ts @@ -63,3 +63,5 @@ export type RecoveryContentProps = ErrorRecoveryWizardProps & { errorKind: ErrorKind isOnDevice: boolean } + +export type DesktopSizeType = 'desktop-small' | 'desktop-large' diff --git a/app/src/pages/RunningProtocol/index.tsx b/app/src/pages/RunningProtocol/index.tsx index 5240196e9e1..869fc7c61d8 100644 --- a/app/src/pages/RunningProtocol/index.tsx +++ b/app/src/pages/RunningProtocol/index.tsx @@ -164,7 +164,6 @@ export function RunningProtocol(): JSX.Element { {isERActive ? (