Skip to content

Commit 28254f8

Browse files
committed
feat(app): add ErrorRecovery ErrorDetails desktop support
1 parent 430a81a commit 28254f8

File tree

11 files changed

+189
-65
lines changed

11 files changed

+189
-65
lines changed

app/src/assets/localization/en/error_recovery.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"continue_run_now": "Continue run now",
1717
"continue_to_drop_tip": "Continue to drop tip",
1818
"error": "Error",
19+
"error_details": "Error details",
1920
"error_on_robot": "Error on {{robot}}",
2021
"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>",
2122
"failed_step": "Failed step",
@@ -40,6 +41,7 @@
4041
"recovery_action_failed": "{{action}} failed",
4142
"recovery_mode": "Recovery Mode",
4243
"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>",
44+
"remove_tips_from_pipette": "Remove tips from {{mount}} pipette before canceling the run?",
4345
"replace_tips_and_select_location": "It's best to replace tips and select the last location used for tip pickup.",
4446
"replace_used_tips_in_rack_location": "Replace used tips in rack location {{location}} in slot {{slot}}",
4547
"replace_with_new_tip_rack": "Replace with new tip rack in slot {{slot}}",
@@ -72,6 +74,5 @@
7274
"tip_not_detected": "Tip not detected",
7375
"view_error_details": "View error details",
7476
"view_recovery_options": "View recovery options",
75-
"you_can_still_drop_tips": "You can still drop the attached tips before proceeding to tip selection.",
76-
"remove_tips_from_pipette": "Remove tips from {{mount}} pipette before canceling the run?"
77+
"you_can_still_drop_tips": "You can still drop the attached tips before proceeding to tip selection."
7778
}

app/src/molecules/InterventionModal/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ const ICON_STYLE = css`
179179
width: ${SPACING.spacing16};
180180
height: ${SPACING.spacing16};
181181
margin: ${SPACING.spacing4};
182+
cursor: pointer;
183+
182184
@media (${RESPONSIVENESS.touchscreenMediaQuerySpecs}) {
183185
width: ${SPACING.spacing32};
184186
height: ${SPACING.spacing32};

app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as React from 'react'
22
import { useTranslation } from 'react-i18next'
3+
import { css } from 'styled-components'
34

45
import { StyledText } from '@opentrons/components'
56

@@ -101,7 +102,13 @@ export function ErrorRecoveryComponent(
101102
}
102103

103104
const buildIconHeading = (): JSX.Element => (
104-
<StyledText oddStyle="bodyTextSemiBold" desktopStyle="bodyDefaultSemiBold">
105+
<StyledText
106+
oddStyle="bodyTextSemiBold"
107+
desktopStyle="bodyDefaultSemiBold"
108+
css={css`
109+
cursor: pointer;
110+
`}
111+
>
105112
{t('view_error_details')}
106113
</StyledText>
107114
)
@@ -119,18 +126,23 @@ export function ErrorRecoveryComponent(
119126
!isDoorOpen &&
120127
route === RECOVERY_MAP.DROP_TIP_FLOWS.ROUTE &&
121128
step !== RECOVERY_MAP.DROP_TIP_FLOWS.STEPS.BEGIN_REMOVAL
129+
const desktopType = isLargeDesktopStyle ? 'desktop-large' : 'desktop-small'
122130

123131
return (
124132
<RecoveryInterventionModal
125133
iconHeading={buildIconHeading()}
126134
titleHeading={buildTitleHeading()}
127135
iconHeadingOnClick={toggleModal}
128136
iconName="information"
129-
desktopType={isLargeDesktopStyle ? 'desktop-large' : 'desktop-small'}
137+
desktopType={desktopType}
130138
isOnDevice={isOnDevice}
131139
>
132140
{showModal ? (
133-
<ErrorDetailsModal {...props} toggleModal={toggleModal} />
141+
<ErrorDetailsModal
142+
{...props}
143+
toggleModal={toggleModal}
144+
desktopType={desktopType}
145+
/>
134146
) : null}
135147
{buildInterventionContent()}
136148
</RecoveryInterventionModal>

app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
StyledText,
1212
RESPONSIVENESS,
1313
} from '@opentrons/components'
14-
import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data'
1514

1615
import { RadioButton } from '../../../atoms/buttons'
1716
import {

app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
StyledText,
2121
JUSTIFY_END,
2222
PrimaryButton,
23-
ALIGN_FLEX_END,
2423
SecondaryButton,
2524
} from '@opentrons/components'
2625

app/src/organisms/ErrorRecoveryFlows/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { css } from 'styled-components'
22

3-
import { SPACING, TYPOGRAPHY, RESPONSIVENESS } from '@opentrons/components'
3+
import { SPACING, RESPONSIVENESS } from '@opentrons/components'
44

55
import type { StepOrder } from './types'
66

app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx

Lines changed: 138 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import * as React from 'react'
22
import { useTranslation } from 'react-i18next'
33
import { createPortal } from 'react-dom'
4+
import { css } from 'styled-components'
45

56
import {
67
Flex,
8+
StyledText,
79
SPACING,
810
COLORS,
911
BORDERS,
@@ -12,16 +14,22 @@ import {
1214

1315
import { useErrorName } from '../hooks'
1416
import { Modal } from '../../../molecules/Modal'
15-
import { getTopPortalEl } from '../../../App/portal'
17+
import { getModalPortalEl, getTopPortalEl } from '../../../App/portal'
1618
import { ERROR_KINDS } from '../constants'
1719
import { InlineNotification } from '../../../atoms/InlineNotification'
1820
import { StepInfo } from './StepInfo'
1921
import { getErrorKind } from '../utils'
22+
import {
23+
LegacyModalShell,
24+
LegacyModalHeader,
25+
} from '../../../molecules/LegacyModal'
2026

2127
import type { RobotType } from '@opentrons/shared-data'
28+
import type { IconProps } from '@opentrons/components'
2229
import type { ModalHeaderBaseProps } from '../../../molecules/Modal/types'
2330
import type { ERUtilsResults } from '../hooks'
2431
import type { ErrorRecoveryFlowsProps } from '..'
32+
import type { DesktopSizeType } from '../types'
2533

2634
export function useErrorDetailsModal(): {
2735
showModal: boolean
@@ -41,6 +49,7 @@ type ErrorDetailsModalProps = ErrorRecoveryFlowsProps &
4149
toggleModal: () => void
4250
isOnDevice: boolean
4351
robotType: RobotType
52+
desktopType: DesktopSizeType
4453
}
4554

4655
export function ErrorDetailsModal(props: ErrorDetailsModalProps): JSX.Element {
@@ -64,44 +73,152 @@ export function ErrorDetailsModal(props: ErrorDetailsModalProps): JSX.Element {
6473
hasExitIcon: true,
6574
}
6675

67-
return createPortal(
76+
const buildModal = (): JSX.Element => {
77+
if (isOnDevice) {
78+
return createPortal(
79+
<ErrorDetailsModalODD
80+
{...props}
81+
toggleModal={toggleModal}
82+
modalHeader={modalHeader}
83+
>
84+
{getIsOverpressureErrorKind() ? <OverpressureBanner /> : null}
85+
</ErrorDetailsModalODD>,
86+
getTopPortalEl()
87+
)
88+
} else {
89+
return createPortal(
90+
<ErrorDetailsModalDesktop
91+
{...props}
92+
toggleModal={toggleModal}
93+
modalHeader={modalHeader}
94+
>
95+
{getIsOverpressureErrorKind() ? <OverpressureBanner /> : null}
96+
</ErrorDetailsModalDesktop>,
97+
getModalPortalEl()
98+
)
99+
}
100+
}
101+
102+
return buildModal()
103+
}
104+
105+
type ErrorDetailsModalType = ErrorDetailsModalProps & {
106+
children: React.ReactNode
107+
modalHeader: ModalHeaderBaseProps
108+
toggleModal: () => void
109+
desktopType: DesktopSizeType
110+
}
111+
112+
export function ErrorDetailsModalDesktop(
113+
props: ErrorDetailsModalType
114+
): JSX.Element {
115+
const { children, modalHeader, toggleModal, desktopType } = props
116+
const { t } = useTranslation('error_recovery')
117+
118+
const buildIcon = (): IconProps => {
119+
return {
120+
name: 'information',
121+
color: COLORS.grey60,
122+
size: SPACING.spacing20,
123+
marginRight: SPACING.spacing8,
124+
}
125+
}
126+
127+
const buildHeader = (): JSX.Element => {
128+
return (
129+
<LegacyModalHeader
130+
onClose={toggleModal}
131+
title={t('error_details')}
132+
icon={buildIcon()}
133+
color={COLORS.black90}
134+
backgroundColor={COLORS.white}
135+
/>
136+
)
137+
}
138+
139+
return (
140+
<LegacyModalShell
141+
header={buildHeader()}
142+
css={
143+
desktopType === 'desktop-small'
144+
? DESKTOP_MODAL_STYLE_SMALL
145+
: DESKTOP_MODAL_STYLE_LARGE
146+
}
147+
>
148+
<Flex
149+
padding={SPACING.spacing24}
150+
gridGap={SPACING.spacing24}
151+
flexDirection={DIRECTION_COLUMN}
152+
>
153+
<StyledText desktopStyle="headingSmallBold">
154+
{modalHeader.title}
155+
</StyledText>
156+
{children}
157+
<Flex css={DESKTOP_STEP_INFO_STYLE}>
158+
<StepInfo {...props} desktopStyle="bodyDefaultRegular" />
159+
</Flex>
160+
</Flex>
161+
</LegacyModalShell>
162+
)
163+
}
164+
165+
export function ErrorDetailsModalODD(
166+
props: ErrorDetailsModalType
167+
): JSX.Element {
168+
const { children, modalHeader, toggleModal } = props
169+
170+
return (
68171
<Modal
69172
header={modalHeader}
70173
onOutsideClick={toggleModal}
71174
zIndex={15}
72175
gridGap={SPACING.spacing32}
73176
>
74177
<Flex gridGap={SPACING.spacing24} flexDirection={DIRECTION_COLUMN}>
75-
{getIsOverpressureErrorKind() ? (
76-
<OverpressureBanner isOnDevice={isOnDevice} />
77-
) : null}
178+
{children}
78179
<Flex
79180
gridGap={SPACING.spacing16}
80181
backgroundColor={COLORS.grey35}
81182
borderRadius={BORDERS.borderRadius8}
82183
padding={`${SPACING.spacing16} ${SPACING.spacing20}`}
83184
>
84-
<StepInfo {...props} textStyle="label" />
185+
<StepInfo {...props} desktopStyle="bodyDefaultRegular" />
85186
</Flex>
86187
</Flex>
87-
</Modal>,
88-
getTopPortalEl()
188+
</Modal>
89189
)
90190
}
91191

92-
export function OverpressureBanner(props: {
93-
isOnDevice: boolean
94-
}): JSX.Element | null {
192+
export function OverpressureBanner(): JSX.Element | null {
95193
const { t } = useTranslation('error_recovery')
96194

97-
if (props.isOnDevice) {
98-
return (
99-
<InlineNotification
100-
type="alert"
101-
heading={t('overpressure_is_usually_caused')}
102-
/>
103-
)
104-
} else {
105-
return null
106-
}
195+
return (
196+
<InlineNotification
197+
type="alert"
198+
heading={t('overpressure_is_usually_caused')}
199+
/>
200+
)
107201
}
202+
203+
// TODO(jh, 07-24-24): Using shared height/width constants for intervention modal sizing and the ErrorDetailsModal sizing
204+
// would be ideal.
205+
const DESKTOP_STEP_INFO_STYLE = css`
206+
background-color: ${COLORS.grey30};
207+
grid-gap: ${SPACING.spacing10};
208+
border-radius: ${BORDERS.borderRadius4};
209+
padding: ${SPACING.spacing6} ${SPACING.spacing24} ${SPACING.spacing6}
210+
${SPACING.spacing12};
211+
`
212+
213+
const DESKTOP_MODAL_STYLE_BASE = css`
214+
width: 47rem;
215+
`
216+
217+
const DESKTOP_MODAL_STYLE_SMALL = css`
218+
${DESKTOP_MODAL_STYLE_BASE}
219+
height: 26rem;
220+
`
221+
const DESKTOP_MODAL_STYLE_LARGE = css`
222+
${DESKTOP_MODAL_STYLE_BASE}
223+
height: 31rem;
224+
`

app/src/organisms/ErrorRecoveryFlows/shared/RecoveryInterventionModal.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@ import { InterventionModal } from '../../../molecules/InterventionModal'
88
import { getModalPortalEl, getTopPortalEl } from '../../../App/portal'
99

1010
import type { ModalType } from '../../../molecules/InterventionModal'
11+
import type { DesktopSizeType } from '../types'
1112

1213
export type RecoveryInterventionModalProps = Omit<
1314
React.ComponentProps<typeof InterventionModal>,
1415
'type'
1516
> & {
1617
/* If on desktop, specifies the hard-coded dimensions height of the modal. */
17-
desktopType: 'desktop-small' | 'desktop-large'
18+
desktopType: DesktopSizeType
1819
isOnDevice: boolean
1920
}
2021

0 commit comments

Comments
 (0)