Skip to content

Commit

Permalink
feat(app): add module calibrate button to protocol setup (#13380)
Browse files Browse the repository at this point in the history
* feat(app): add calibrate button to Desktop app protocol setup
  • Loading branch information
koji authored Aug 25, 2023
1 parent 5e1b981 commit 1b9c82c
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 79 deletions.
2 changes: 1 addition & 1 deletion app/src/assets/localization/en/protocol_setup.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"calibrate_deck_to_proceed_to_pipette_calibration": "Calibrate your deck in order to proceed to pipette calibration",
"calibrate_deck_to_proceed_to_tip_length_calibration": "Calibrate your deck in order to proceed to tip length calibration",
"calibrate_gripper_failure_reason": "Calibrate the required gripper to continue",
"calibrate_now": "Calibrate Now",
"calibrate_now": "Calibrate now",
"calibrate_pipette_failure_reason": "Calibrate the required pipette(s) to continue",
"calibrate_tiprack_failure_reason": "Calibrate the required tip lengths to continue",
"calibrate": "calibrate",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
import { Banner } from '../../../../atoms/Banner'
import { StyledText } from '../../../../atoms/text'
import { StatusLabel } from '../../../../atoms/StatusLabel'
import { TertiaryButton } from '../../../../atoms/buttons'
import { UnMatchedModuleWarning } from './UnMatchedModuleWarning'
import { MultipleModulesModal } from './MultipleModulesModal'
import {
Expand All @@ -36,6 +37,7 @@ import {
useUnmatchedModulesForProtocol,
} from '../../hooks'
import { HeaterShakerWizard } from '../../HeaterShakerWizard'
import { ModuleWizardFlows } from '../../../ModuleWizardFlows'
import { getModuleImage } from './utils'

import type { ModuleModel } from '@opentrons/shared-data'
Expand Down Expand Up @@ -139,7 +141,7 @@ export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => {
marginRight={SPACING.spacing16}
width="15%"
>
{t('connection_status')}
{t('status')}
</StyledText>
</Flex>
<Flex
Expand Down Expand Up @@ -187,14 +189,14 @@ interface ModulesListItemProps {
isOt3: boolean
}

export const ModulesListItem = ({
export function ModulesListItem({
moduleModel,
displayName,
location,
attachedModuleMatch,
heaterShakerModuleFromProtocol,
isOt3,
}: ModulesListItemProps): JSX.Element => {
}: ModulesListItemProps): JSX.Element {
const { t } = useTranslation('protocol_setup')
const moduleConnectionStatus =
attachedModuleMatch != null
Expand All @@ -209,7 +211,7 @@ export const ModulesListItem = ({
attachedModuleMatch.moduleType === HEATERSHAKER_MODULE_TYPE
? attachedModuleMatch
: null

const [showModuleWizard, setShowModuleWizard] = React.useState<boolean>(false)
let subText: JSX.Element | null = null
if (moduleModel === HEATERSHAKER_MODULE_V1) {
subText = (
Expand Down Expand Up @@ -252,76 +254,100 @@ export const ModulesListItem = ({
</StyledText>
)
}

const RenderModuleStatus = (): JSX.Element => {
const handleCalibrate = (): void => {
setShowModuleWizard(true)
}
if (attachedModuleMatch == null) {
return (
<StatusLabel
id={location}
status={moduleConnectionStatus}
backgroundColor={COLORS.warningBackgroundLight}
iconColor={COLORS.warningEnabled}
textColor={COLORS.warningText}
/>
)
} else if (attachedModuleMatch.moduleOffset?.last_modified != null) {
return (
<StatusLabel
id={location}
status={moduleConnectionStatus}
backgroundColor={COLORS.successBackgroundLight}
iconColor={COLORS.successEnabled}
textColor={COLORS.successText}
/>
)
} else {
return (
<TertiaryButton onClick={handleCalibrate}>
{t('calibrate_now')}
</TertiaryButton>
)
}
}

return (
<Box
border={BORDERS.styleSolid}
borderColor={COLORS.medGreyEnabled}
borderWidth="1px"
borderRadius={BORDERS.radiusSoftCorners}
padding={SPACING.spacing16}
backgroundColor={COLORS.white}
data-testid="ModulesListItem_Row"
>
{showHeaterShakerFlow && heaterShakerModuleFromProtocol != null ? (
<HeaterShakerWizard
onCloseClick={() => setShowHeaterShakerFlow(false)}
moduleFromProtocol={heaterShakerModuleFromProtocol}
attachedModule={heaterShakerAttachedModule}
<>
{showModuleWizard && attachedModuleMatch != null ? (
<ModuleWizardFlows
attachedModule={attachedModuleMatch}
closeFlow={() => setShowModuleWizard(false)}
/>
) : null}
<Flex
flexDirection={DIRECTION_ROW}
alignItems={JUSTIFY_CENTER}
justifyContent={JUSTIFY_SPACE_BETWEEN}
<Box
border={BORDERS.styleSolid}
borderColor={COLORS.medGreyEnabled}
borderWidth="1px"
borderRadius={BORDERS.radiusSoftCorners}
padding={SPACING.spacing16}
backgroundColor={COLORS.white}
data-testid="ModulesListItem_Row"
>
<Flex alignItems={JUSTIFY_CENTER} width="45%">
<img width="60px" height="54px" src={getModuleImage(moduleModel)} />
<Flex flexDirection={DIRECTION_COLUMN}>
<StyledText
css={TYPOGRAPHY.pSemiBold}
marginLeft={SPACING.spacing20}
>
{displayName}
</StyledText>
{subText}
{showHeaterShakerFlow && heaterShakerModuleFromProtocol != null ? (
<HeaterShakerWizard
onCloseClick={() => setShowHeaterShakerFlow(false)}
moduleFromProtocol={heaterShakerModuleFromProtocol}
attachedModule={heaterShakerAttachedModule}
/>
) : null}
<Flex
flexDirection={DIRECTION_ROW}
alignItems={JUSTIFY_CENTER}
justifyContent={JUSTIFY_SPACE_BETWEEN}
>
<Flex alignItems={JUSTIFY_CENTER} width="45%">
<img width="60px" height="54px" src={getModuleImage(moduleModel)} />
<Flex flexDirection={DIRECTION_COLUMN}>
<StyledText
css={TYPOGRAPHY.pSemiBold}
marginLeft={SPACING.spacing20}
>
{displayName}
</StyledText>
{subText}
</Flex>
</Flex>
<StyledText as="p" width="15%">
{t('slot_location', {
slotName:
getModuleType(moduleModel) === 'thermocyclerModuleType'
? isOt3
? TC_MODULE_LOCATION_OT3
: TC_MODULE_LOCATION_OT2
: location,
})}
</StyledText>
<Flex width="15%">
{moduleModel === MAGNETIC_BLOCK_V1 ? (
<StyledText as="p"> {t('n_a')}</StyledText>
) : (
<RenderModuleStatus />
)}
</Flex>
</Flex>
<StyledText as="p" width="15%">
{t('slot_location', {
slotName:
getModuleType(moduleModel) === 'thermocyclerModuleType'
? isOt3
? TC_MODULE_LOCATION_OT3
: TC_MODULE_LOCATION_OT2
: location,
})}
</StyledText>
<Flex width="15%">
{moduleModel === MAGNETIC_BLOCK_V1 ? (
<StyledText as="p"> {t('n_a')}</StyledText>
) : (
<StatusLabel
id={location}
status={moduleConnectionStatus}
backgroundColor={
attachedModuleMatch != null
? COLORS.successBackgroundLight
: COLORS.warningBackgroundLight
}
iconColor={
attachedModuleMatch != null
? COLORS.successEnabled
: COLORS.warningEnabled
}
textColor={
attachedModuleMatch != null
? COLORS.successText
: COLORS.warningText
}
/>
)}
</Flex>
</Flex>
</Box>
</Box>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,17 @@ import {
useUnmatchedModulesForProtocol,
} from '../../../hooks'
import { HeaterShakerWizard } from '../../../HeaterShakerWizard'
import { ModuleWizardFlows } from '../../../../ModuleWizardFlows'
import { SetupModulesList } from '../SetupModulesList'

import type { ModuleModel, ModuleType } from '@opentrons/shared-data'

jest.mock('../../../hooks')
jest.mock('../UnMatchedModuleWarning')
jest.mock('../../../HeaterShakerWizard')
jest.mock('../../../../ModuleWizardFlows')
jest.mock('../MultipleModulesModal')

const mockUseIsOt3 = useIsOT3 as jest.MockedFunction<typeof useIsOT3>
const mockUseModuleRenderInfoForProtocolById = useModuleRenderInfoForProtocolById as jest.MockedFunction<
typeof useModuleRenderInfoForProtocolById
Expand All @@ -48,6 +51,9 @@ const mockUseRunHasStarted = useRunHasStarted as jest.MockedFunction<
const mockMultipleModulesModal = MultipleModulesModal as jest.MockedFunction<
typeof MultipleModulesModal
>
const mockModuleWizardFlows = ModuleWizardFlows as jest.MockedFunction<
typeof ModuleWizardFlows
>
const ROBOT_NAME = 'otie'
const RUN_ID = '1'
const MOCK_MAGNETIC_MODULE_COORDS = [10, 20, 0]
Expand Down Expand Up @@ -75,6 +81,16 @@ const mockTCModule = {
displayName: 'Thermocycler Module',
}

const mockCalibratedData = {
offset: {
x: 0.1640625,
y: -1.2421875,
z: -1.759999999999991,
},
slot: '7',
last_modified: '2023-06-01T14:42:20.131798+00:00',
}

const render = (props: React.ComponentProps<typeof SetupModulesList>) => {
return renderWithProviders(<SetupModulesList {...props} />, {
i18nInstance: i18n,
Expand All @@ -100,6 +116,7 @@ describe('SetupModulesList', () => {
missingModuleIds: [],
remainingAttachedModules: [],
})
mockModuleWizardFlows.mockReturnValue(<div>mock ModuleWizardFlows</div>)
})
afterEach(() => resetAllWhenMocks())

Expand All @@ -111,7 +128,7 @@ describe('SetupModulesList', () => {
const { getByText } = render(props)
getByText('Module Name')
getByText('Location')
getByText('Connection Status')
getByText('Status')
})

it('should render a magnetic module that is connected', () => {
Expand All @@ -126,7 +143,10 @@ describe('SetupModulesList', () => {
nestedLabwareId: null,
protocolLoadOrder: 0,
slotName: '1',
attachedModuleMatch: mockMagneticModuleGen2,
attachedModuleMatch: {
...mockMagneticModuleGen2,
moduleOffset: mockCalibratedData,
},
},
} as any)

Expand Down Expand Up @@ -182,7 +202,10 @@ describe('SetupModulesList', () => {
nestedLabwareId: null,
protocolLoadOrder: 0,
slotName: '7',
attachedModuleMatch: mockThermocycler,
attachedModuleMatch: {
...mockThermocycler,
moduleOffset: mockCalibratedData,
},
},
} as any)
mockUseIsOt3.mockReturnValue(false)
Expand All @@ -193,7 +216,7 @@ describe('SetupModulesList', () => {
getByText('Connected')
})

it('should render a thermocycler module that is connected, OT3', () => {
it('should render a thermocycler module that is connected but not calibrated, OT3', () => {
when(mockUseUnmatchedModulesForProtocol)
.calledWith(ROBOT_NAME, RUN_ID)
.mockReturnValue({
Expand All @@ -216,6 +239,39 @@ describe('SetupModulesList', () => {
} as any)
mockUseIsOt3.mockReturnValue(true)

const { getByText } = render(props)
getByText('Thermocycler Module')
getByText('Slot A1+B1')
getByText('Calibrate now').click()
getByText('mock ModuleWizardFlows')
})

it('should render a thermocycler module that is connected, OT3', () => {
when(mockUseUnmatchedModulesForProtocol)
.calledWith(ROBOT_NAME, RUN_ID)
.mockReturnValue({
missingModuleIds: [],
remainingAttachedModules: [],
})
mockUseModuleRenderInfoForProtocolById.mockReturnValue({
[mockTCModule.moduleId]: {
moduleId: mockTCModule.moduleId,
x: MOCK_TC_COORDS[0],
y: MOCK_TC_COORDS[1],
z: MOCK_TC_COORDS[2],
moduleDef: mockTCModule as any,
nestedLabwareDef: null,
nestedLabwareId: null,
protocolLoadOrder: 0,
slotName: '7',
attachedModuleMatch: {
...mockThermocycler,
moduleOffset: mockCalibratedData,
},
},
} as any)
mockUseIsOt3.mockReturnValue(true)

const { getByText } = render(props)
getByText('Thermocycler Module')
getByText('Slot A1+B1')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ describe('SetupDeckCalibration', () => {
getByText('Not calibrated yet')
expect(
getByRole('link', {
name: 'Calibrate Now',
name: 'Calibrate now',
}).getAttribute('href')
).toBe('/devices/otie/robot-settings/calibration/dashboard')
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ describe('SetupPipetteCalibrationItem', () => {
getByText('Not calibrated yet')
expect(
getByRole('link', {
name: 'Calibrate Now',
name: 'Calibrate now',
}).getAttribute('href')
).toBe('/devices/otie/robot-settings/calibration/dashboard')
})
Expand Down Expand Up @@ -144,7 +144,7 @@ describe('SetupPipetteCalibrationItem', () => {
})
getByText('Left Mount')
getByText(mockPipetteInfo.pipetteSpecs.displayName)
const attach = getByRole('button', { name: 'Calibrate Now' })
const attach = getByRole('button', { name: 'Calibrate now' })
fireEvent.click(attach)
getByText('pipette wizard flows')
})
Expand Down
Loading

0 comments on commit 1b9c82c

Please sign in to comment.