Skip to content

Commit

Permalink
feat(app): add ErrorRecovery ErrorDetails desktop support
Browse files Browse the repository at this point in the history
  • Loading branch information
mjhuff committed Jul 24, 2024
1 parent 430a81a commit 28254f8
Show file tree
Hide file tree
Showing 11 changed files with 189 additions and 65 deletions.
5 changes: 3 additions & 2 deletions app/src/assets/localization/en/error_recovery.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<block>The failed dispense step will not be completed. The run will continue from the next step.</block><block>Close the robot door before proceeding.</block>",
"failed_step": "Failed step",
Expand All @@ -40,6 +41,7 @@
"recovery_action_failed": "{{action}} failed",
"recovery_mode": "Recovery Mode",
"recovery_mode_explanation": "<block>Recovery Mode provides you with guided and manual controls for handling errors at runtime.</block><br/><block>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.</block>",
"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}}",
Expand Down Expand Up @@ -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."
}
2 changes: 2 additions & 0 deletions app/src/molecules/InterventionModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
18 changes: 15 additions & 3 deletions app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { css } from 'styled-components'

import { StyledText } from '@opentrons/components'

Expand Down Expand Up @@ -101,7 +102,13 @@ export function ErrorRecoveryComponent(
}

const buildIconHeading = (): JSX.Element => (
<StyledText oddStyle="bodyTextSemiBold" desktopStyle="bodyDefaultSemiBold">
<StyledText
oddStyle="bodyTextSemiBold"
desktopStyle="bodyDefaultSemiBold"
css={css`
cursor: pointer;
`}
>
{t('view_error_details')}
</StyledText>
)
Expand All @@ -119,18 +126,23 @@ 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 (
<RecoveryInterventionModal
iconHeading={buildIconHeading()}
titleHeading={buildTitleHeading()}
iconHeadingOnClick={toggleModal}
iconName="information"
desktopType={isLargeDesktopStyle ? 'desktop-large' : 'desktop-small'}
desktopType={desktopType}
isOnDevice={isOnDevice}
>
{showModal ? (
<ErrorDetailsModal {...props} toggleModal={toggleModal} />
<ErrorDetailsModal
{...props}
toggleModal={toggleModal}
desktopType={desktopType}
/>
) : null}
{buildInterventionContent()}
</RecoveryInterventionModal>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 0 additions & 1 deletion app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
StyledText,
JUSTIFY_END,
PrimaryButton,
ALIGN_FLEX_END,
SecondaryButton,
} from '@opentrons/components'

Expand Down
2 changes: 1 addition & 1 deletion app/src/organisms/ErrorRecoveryFlows/constants.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down
159 changes: 138 additions & 21 deletions app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand All @@ -41,6 +49,7 @@ type ErrorDetailsModalProps = ErrorRecoveryFlowsProps &
toggleModal: () => void
isOnDevice: boolean
robotType: RobotType
desktopType: DesktopSizeType
}

export function ErrorDetailsModal(props: ErrorDetailsModalProps): JSX.Element {
Expand All @@ -64,44 +73,152 @@ export function ErrorDetailsModal(props: ErrorDetailsModalProps): JSX.Element {
hasExitIcon: true,
}

return createPortal(
const buildModal = (): JSX.Element => {
if (isOnDevice) {
return createPortal(
<ErrorDetailsModalODD
{...props}
toggleModal={toggleModal}
modalHeader={modalHeader}
>
{getIsOverpressureErrorKind() ? <OverpressureBanner /> : null}
</ErrorDetailsModalODD>,
getTopPortalEl()
)
} else {
return createPortal(
<ErrorDetailsModalDesktop
{...props}
toggleModal={toggleModal}
modalHeader={modalHeader}
>
{getIsOverpressureErrorKind() ? <OverpressureBanner /> : null}
</ErrorDetailsModalDesktop>,
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 (
<LegacyModalHeader
onClose={toggleModal}
title={t('error_details')}
icon={buildIcon()}
color={COLORS.black90}
backgroundColor={COLORS.white}
/>
)
}

return (
<LegacyModalShell
header={buildHeader()}
css={
desktopType === 'desktop-small'
? DESKTOP_MODAL_STYLE_SMALL
: DESKTOP_MODAL_STYLE_LARGE
}
>
<Flex
padding={SPACING.spacing24}
gridGap={SPACING.spacing24}
flexDirection={DIRECTION_COLUMN}
>
<StyledText desktopStyle="headingSmallBold">
{modalHeader.title}
</StyledText>
{children}
<Flex css={DESKTOP_STEP_INFO_STYLE}>
<StepInfo {...props} desktopStyle="bodyDefaultRegular" />
</Flex>
</Flex>
</LegacyModalShell>
)
}

export function ErrorDetailsModalODD(
props: ErrorDetailsModalType
): JSX.Element {
const { children, modalHeader, toggleModal } = props

return (
<Modal
header={modalHeader}
onOutsideClick={toggleModal}
zIndex={15}
gridGap={SPACING.spacing32}
>
<Flex gridGap={SPACING.spacing24} flexDirection={DIRECTION_COLUMN}>
{getIsOverpressureErrorKind() ? (
<OverpressureBanner isOnDevice={isOnDevice} />
) : null}
{children}
<Flex
gridGap={SPACING.spacing16}
backgroundColor={COLORS.grey35}
borderRadius={BORDERS.borderRadius8}
padding={`${SPACING.spacing16} ${SPACING.spacing20}`}
>
<StepInfo {...props} textStyle="label" />
<StepInfo {...props} desktopStyle="bodyDefaultRegular" />
</Flex>
</Flex>
</Modal>,
getTopPortalEl()
</Modal>
)
}

export function OverpressureBanner(props: {
isOnDevice: boolean
}): JSX.Element | null {
export function OverpressureBanner(): JSX.Element | null {
const { t } = useTranslation('error_recovery')

if (props.isOnDevice) {
return (
<InlineNotification
type="alert"
heading={t('overpressure_is_usually_caused')}
/>
)
} else {
return null
}
return (
<InlineNotification
type="alert"
heading={t('overpressure_is_usually_caused')}
/>
)
}

// 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;
`
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof InterventionModal>,
'type'
> & {
/* If on desktop, specifies the hard-coded dimensions height of the modal. */
desktopType: 'desktop-small' | 'desktop-large'
desktopType: DesktopSizeType
isOnDevice: boolean
}

Expand Down
Loading

0 comments on commit 28254f8

Please sign in to comment.