diff --git a/app/src/assets/localization/en/error_recovery.json b/app/src/assets/localization/en/error_recovery.json index 39671bbffd9..605c2b55527 100644 --- a/app/src/assets/localization/en/error_recovery.json +++ b/app/src/assets/localization/en/error_recovery.json @@ -35,7 +35,8 @@ "next_step": "Next step", "next_try_another_action": "Next, you can try another recovery action or cancel the run.", "no_liquid_detected": "No liquid detected", - "overpressure_is_usually_caused": "Overpressure is usually caused by a tip contacting labware, a clog, or moving viscous liquid too quickly. If the issue persists, cancel the run and make the necessary changes to the protocol.", + "overpressure_is_usually_caused": "Overpressure is usually caused by a tip contacting labware, a clog, or moving viscous liquid too quickly", + "if_issue_persists": " If the issue persists, cancel the run and make the necessary changes to the protocol", "pick_up_tips": "Pick up tips", "pipette_overpressure": "Pipette overpressure", "preserve_aspirated_liquid": "First, do you need to preserve aspirated liquid?", diff --git a/app/src/assets/localization/en/run_details.json b/app/src/assets/localization/en/run_details.json index 20c29bddbd3..f8ce9a7590e 100644 --- a/app/src/assets/localization/en/run_details.json +++ b/app/src/assets/localization/en/run_details.json @@ -154,5 +154,6 @@ "view_current_step": "View current step", "view_error": "View error", "view_error_details": "View error details", + "view_warning_details": "View warning details", "warning_details": "Warning details" } diff --git a/app/src/atoms/buttons/LargeButton.tsx b/app/src/atoms/buttons/LargeButton.tsx index c33c16e5c21..59f7d248c47 100644 --- a/app/src/atoms/buttons/LargeButton.tsx +++ b/app/src/atoms/buttons/LargeButton.tsx @@ -151,24 +151,30 @@ export function LargeButton(props: LargeButtonProps): JSX.Element { } &:active { - background-color: ${LARGE_BUTTON_PROPS_BY_TYPE[buttonType] - .activeBackgroundColor}; - ${activeColorFor(buttonType)}; + background-color: ${disabled + ? LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledBackgroundColor + : LARGE_BUTTON_PROPS_BY_TYPE[buttonType].activeBackgroundColor}; + ${!disabled && activeColorFor(buttonType)}; border: ${BORDERS.borderRadius4} solid - ${LARGE_BUTTON_PROPS_BY_TYPE[buttonType].activeBackgroundColor}; + ${disabled + ? LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledBackgroundColor + : LARGE_BUTTON_PROPS_BY_TYPE[buttonType].activeBackgroundColor}; } &:active #btn-icon { ${activeIconStyle(buttonType)}; } &:focus-visible { - background-color: ${LARGE_BUTTON_PROPS_BY_TYPE[buttonType] - .focusVisibleBackgroundColor}; - ${activeColorFor(buttonType)}; + background-color: ${disabled + ? LARGE_BUTTON_PROPS_BY_TYPE[buttonType].disabledBackgroundColor + : LARGE_BUTTON_PROPS_BY_TYPE[buttonType].focusVisibleBackgroundColor}; + ${!disabled && activeColorFor(buttonType)}; padding: calc(${SPACING.spacing24} + ${SPACING.spacing2}); border: ${SPACING.spacing2} solid ${COLORS.transparent}; - outline: 3px solid - ${LARGE_BUTTON_PROPS_BY_TYPE[buttonType].focusVisibleOutlineColor}; + outline: ${disabled + ? 'none' + : `3px solid + ${LARGE_BUTTON_PROPS_BY_TYPE[buttonType].focusVisibleOutlineColor}`}; background-clip: padding-box; box-shadow: none; } diff --git a/app/src/molecules/Modal/ModalHeader.tsx b/app/src/molecules/Modal/ModalHeader.tsx index 546ed8981ce..a305b67b8bc 100644 --- a/app/src/molecules/Modal/ModalHeader.tsx +++ b/app/src/molecules/Modal/ModalHeader.tsx @@ -43,6 +43,7 @@ export function ModalHeader(props: ModalHeaderBaseProps): JSX.Element { color={iconColor} size="2rem" alignSelf={ALIGN_CENTER} + marginRight={SPACING.spacing16} /> ) : null} - {t('view_error')} + {runStatus === RUN_STATUS_SUCCEEDED + ? t('view_warning_details') + : t('view_error_details')} diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx index daa105277f4..ee91f1ff901 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx @@ -799,7 +799,7 @@ describe('ProtocolRunHeader', () => { .thenReturn(RUN_STATUS_FAILED) render() - fireEvent.click(screen.getByText('View error')) + fireEvent.click(screen.getByText('View error details')) screen.getByText('mock RunFailedModal') }) diff --git a/app/src/organisms/DropTipWizardFlows/BeforeBeginning.tsx b/app/src/organisms/DropTipWizardFlows/BeforeBeginning.tsx index bb43a743268..d516c5d7067 100644 --- a/app/src/organisms/DropTipWizardFlows/BeforeBeginning.tsx +++ b/app/src/organisms/DropTipWizardFlows/BeforeBeginning.tsx @@ -99,7 +99,7 @@ export const BeforeBeginning = ({ ? JUSTIFY_SPACE_BETWEEN : JUSTIFY_FLEX_END } - marginTop={issuedCommandsType === 'fixit' ? '6.875rem' : undefined} + marginTop={issuedCommandsType === 'fixit' ? '6.875rem' : 'auto'} > {fixitCommandTypeUtils != null ? ( { Success Icon {message} diff --git a/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx b/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx index f2a19f3ea37..ac2fc5913d9 100644 --- a/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx +++ b/app/src/organisms/DropTipWizardFlows/TipsAttachedModal.tsx @@ -1,5 +1,4 @@ import * as React from 'react' -import capitalize from 'lodash/capitalize' import NiceModal, { useModal } from '@ebay/nice-modal-react' import { Trans, useTranslation } from 'react-i18next' @@ -82,9 +81,7 @@ const TipsAttachedModal = NiceModal.create( } const is96Channel = specs.channels === 96 - const displayMountText = is96Channel - ? '96-Channel' - : capitalize(mount as string) + const displayMountText = is96Channel ? '96-Channel' : (mount as string) return ( diff --git a/app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx b/app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx index a56b4eb16d5..30b916f7b11 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx @@ -122,7 +122,7 @@ export function RunPausedSplash( position={POSITION_ABSOLUTE} flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing60} - paddingY={SPACING.spacing40} + padding={SPACING.spacing40} backgroundColor={COLORS.red50} zIndex={5} > @@ -131,7 +131,7 @@ export function RunPausedSplash( {title} - + { it('should render a generic paused screen if there is no handled errorType', () => { render(props) - screen.getByText('Error') + screen.getByText('Tip not detected') screen.getByText('MOCK STEP INFO') }) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts index 28d619c8cd0..bde35d89fbf 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts @@ -17,7 +17,8 @@ export function useErrorName(errorKind: ErrorKind): string { return t('pipette_overpressure') case ERROR_KINDS.OVERPRESSURE_WHILE_DISPENSING: return t('pipette_overpressure') + // The only "general error" case currently is tipPhysicallyMissing. default: - return t('error') + return t('tip_not_detected') } } diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts index d8ebf9eb5ca..eff2565a2eb 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryTipStatus.ts @@ -73,10 +73,6 @@ export function useRecoveryTipStatus( setIsLoadingTipStatus(false) setFailedCommandPipette(head(failedCommandPipettes) ?? null) - console.log( - '=>(useRecoveryTipStatus.ts:76) failedCommandPipettes', - failedCommandPipettes - ) return Promise.resolve(pipettesWithTip) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx index 15e1b2a515f..0e452c51aa1 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx @@ -200,6 +200,7 @@ export function OverpressureBanner(): JSX.Element | null { ) } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/TwoColTextAndFailedStepNextStep.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/TwoColTextAndFailedStepNextStep.tsx index a1f959c72fb..b98bb68e2c5 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/TwoColTextAndFailedStepNextStep.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/TwoColTextAndFailedStepNextStep.tsx @@ -46,9 +46,9 @@ export function TwoColTextAndFailedStepNextStep( @@ -58,12 +58,16 @@ export function TwoColTextAndFailedStepNextStep( > {leftColTitle} - - {leftColBodyText} - + {typeof leftColBodyText === 'string' ? ( + + {leftColBodyText} + + ) : ( + leftColBodyText + )} diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx index 6714a6e5050..06058e50efc 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx @@ -74,7 +74,7 @@ describe('ErrorDetailsModal', () => { expect(vi.mocked(Modal)).toHaveBeenCalledWith( expect.objectContaining({ header: { - title: 'Error', + title: 'Tip not detected', hasExitIcon: true, }, onOutsideClick: props.toggleModal, @@ -129,7 +129,9 @@ describe('OverpressureBanner', () => { expect.objectContaining({ type: 'alert', heading: - 'Overpressure is usually caused by a tip contacting labware, a clog, or moving viscous liquid too quickly. If the issue persists, cancel the run and make the necessary changes to the protocol.', + 'Overpressure is usually caused by a tip contacting labware, a clog, or moving viscous liquid too quickly', + message: + ' If the issue persists, cancel the run and make the necessary changes to the protocol', }), {} ) diff --git a/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx b/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx index f3c218a8392..2fb5f75efb2 100644 --- a/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx +++ b/app/src/organisms/OnDeviceDisplay/RobotDashboard/RecentRunProtocolCard.tsx @@ -37,10 +37,6 @@ import { Skeleton } from '../../../atoms/Skeleton' import { useMissingProtocolHardware } from '../../../pages/Protocols/hooks' import { useCloneRun } from '../../ProtocolUpload/hooks' import { useRerunnableStatusText } from './hooks' -import { - useRobotInitializationStatus, - INIT_STATUS, -} from '../../../resources/health/hooks' import type { Run, RunData, RunStatus } from '@opentrons/api-client' import type { ProtocolResource } from '@opentrons/shared-data' @@ -96,9 +92,6 @@ export function ProtocolWithLastRun({ navigate(`runs/${createRunResponse.data.id}/setup`) } const { cloneRun } = useCloneRun(runData.id, onResetSuccess) - const robotInitStatus = useRobotInitializationStatus() - const isRobotInitializing = - robotInitStatus === INIT_STATUS.INITIALIZING || robotInitStatus == null const [showSpinner, setShowSpinner] = React.useState(false) const protocolName = @@ -173,7 +166,7 @@ export function ProtocolWithLastRun({ } ).replace('about ', '') - return isProtocolFetching || isLookingForHardware || isRobotInitializing ? ( + return isProtocolFetching || isLookingForHardware ? ( { runTimeParameters: [], }, } as any) - vi.mocked(useRobotInitializationStatus).mockReturnValue( - INIT_STATUS.SUCCEEDED - ) when(useTrackProtocolRunEvent).calledWith(RUN_ID, ROBOT_NAME).thenReturn({ trackProtocolRunEvent: mockTrackProtocolRunEvent, }) @@ -269,20 +261,6 @@ describe('RecentRunProtocolCard', () => { screen.getByText('mock Skeleton') }) - it('should render the skeleton when the robot server is initializing', () => { - vi.mocked(useRobotInitializationStatus).mockReturnValue( - INIT_STATUS.INITIALIZING - ) - render(props) - screen.getByText('mock Skeleton') - }) - - it('should render the skeleton when the robot server is unresponsive', () => { - vi.mocked(useRobotInitializationStatus).mockReturnValue(null) - render(props) - screen.getByText('mock Skeleton') - }) - it('should push to protocol details if protocol contains runtime parameters', () => { vi.mocked(useProtocolAnalysisAsDocumentQuery).mockReturnValue({ data: simpleAnalysisFileFixture, diff --git a/app/src/organisms/RunDetails/ConfirmCancelModal.tsx b/app/src/organisms/RunDetails/ConfirmCancelModal.tsx index b0627b45eba..fc2de5392e2 100644 --- a/app/src/organisms/RunDetails/ConfirmCancelModal.tsx +++ b/app/src/organisms/RunDetails/ConfirmCancelModal.tsx @@ -75,10 +75,8 @@ export function ConfirmCancelModal( onClose={isCanceling ? undefined : onClose} title={t('cancel_run_modal_heading')} > - - - {cancelRunAlertInfo} - + + {cancelRunAlertInfo} {t('cancel_run_module_info')} diff --git a/app/src/pages/RunSummary/index.tsx b/app/src/pages/RunSummary/index.tsx index 0bdc68a4c4e..524a90d8a43 100644 --- a/app/src/pages/RunSummary/index.tsx +++ b/app/src/pages/RunSummary/index.tsx @@ -67,6 +67,7 @@ import { handleTipsAttachedModal } from '../../organisms/DropTipWizardFlows/Tips import { useTipAttachmentStatus } from '../../organisms/DropTipWizardFlows' import { useRecoveryAnalytics } from '../../organisms/ErrorRecoveryFlows/hooks' +import type { IconName } from '@opentrons/components' import type { OnDeviceRouteParams } from '../../App/types' import type { PipetteWithTip } from '../../organisms/DropTipWizardFlows' @@ -164,25 +165,50 @@ export function RunSummary(): JSX.Element { } ) // TODO(jh, 08-14-24): The backend never returns the "user cancelled a run" error and cancelledWithoutRecovery becomes unnecessary. - const cancelledWithoutRecovery = - !enteredER && runStatus === RUN_STATUS_STOPPED - const showErrorDetailsBtn = - !cancelledWithoutRecovery && - ((runRecord?.data.errors != null && runRecord?.data.errors.length > 0) || - (commandErrorList != null && commandErrorList?.data.length > 0)) - - let headerText = + const hasCommandErrors = commandErrorList != null && commandErrorList.data.length > 0 + const disableErrorDetailsBtn = !( + hasCommandErrors || + (runRecord?.data.errors != null && runRecord?.data.errors.length > 0) + ) + + let headerText: string | null = null + if (runStatus === RUN_STATUS_SUCCEEDED) { + headerText = hasCommandErrors ? t('run_completed_with_warnings_splash') : t('run_completed_splash') - if (runStatus === RUN_STATUS_FAILED) { + } else if (runStatus === RUN_STATUS_FAILED) { headerText = t('run_failed_splash') } else if (runStatus === RUN_STATUS_STOPPED) { - if (enteredER) { - headerText = t('run_canceled_with_errors_splash') - } else { - headerText = t('run_canceled_splash') + headerText = + enteredER && !disableErrorDetailsBtn + ? t('run_canceled_with_errors_splash') + : t('run_canceled_splash') + } + + const buildHeaderIcon = (): JSX.Element | null => { + let iconName: IconName | null = null + let iconColor: string | null = null + + if (runStatus === RUN_STATUS_SUCCEEDED) { + if (hasCommandErrors) { + iconName = 'ot-check' + iconColor = COLORS.yellow50 + } else { + iconName = 'ot-check' + iconColor = COLORS.green50 + } + } else if (runStatus === RUN_STATUS_FAILED) { + iconName = 'ot-alert' + iconColor = COLORS.red50 + } else if (runStatus === RUN_STATUS_STOPPED) { + iconName = 'ot-alert' + iconColor = COLORS.red50 } + + return iconName != null && iconColor != null ? ( + + ) : null } const { @@ -249,7 +275,7 @@ export function RunSummary(): JSX.Element { isRunCurrent, onSkipAndHome: () => { closeCurrentRun({ - onSuccess: () => { + onSettled: () => { navigate('/') }, }) @@ -259,7 +285,7 @@ export function RunSummary(): JSX.Element { returnToQuickTransfer() } else { closeCurrentRun({ - onSuccess: () => { + onSettled: () => { navigate('/') }, }) @@ -359,7 +385,7 @@ export function RunSummary(): JSX.Element { /> {didRunSucceed - ? t('run_complete_splash') + ? t('run_completed_splash') : t('run_failed_splash')} @@ -391,12 +417,10 @@ export function RunSummary(): JSX.Element { gridGap={SPACING.spacing16} > - - {headerText} + {buildHeaderIcon()} + {headerText != null ? ( + {headerText} + ) : null} {protocolName} @@ -449,14 +473,17 @@ export function RunSummary(): JSX.Element { } css={showRunAgainSpinner ? RUN_AGAIN_CLICKED_STYLE : undefined} /> - {showErrorDetailsBtn ? ( - - ) : null} + )}