diff --git a/app/src/assets/localization/en/heater_shaker.json b/app/src/assets/localization/en/heater_shaker.json index 4eedadbb2b7..7d8b250838a 100644 --- a/app/src/assets/localization/en/heater_shaker.json +++ b/app/src/assets/localization/en/heater_shaker.json @@ -29,7 +29,6 @@ "check_alignment": "Check alignment.", "a_properly_attached_adapter": "A properly attached adapter will sit evenly on the module.", "check_alignment_instructions": "Check attachment by rocking the adapter back and forth.", - "modal_title": "{{name}} - Attach Heater Shaker Module", "intro_title": "Use this guide to attach the Heater Shaker Module to your robot’s deck for secure shaking.", "intro_subtitle": "You will need:", "intro_heater_shaker_mod": "Heater Shaker Module", @@ -58,6 +57,7 @@ "heater_shaker_anchor_description": "The 2 Anchors keep the module attached to the deck while it is shaking. The screw above each anchor is used to extend and retract them. See animation below. Extending the bolts slightly increases the module’s footprint, which allows it to be more firmly attached to the edges of a slot.", "step_4_of_4": "Step 4 of 4: Test shake", "test_shake_banner_information": "If you want to add labware to the module before doing a test shake, you can use the labware latch controls to hold the latches open.", + "test_shake_banner_labware_information": "If you want to add the {{labware}} to the module before doing a test shake, you can use the labware latch controls.", "open_labware_latch": "Open Labware Latch", "close_labware_latch": "Close Labware Latch", "labware_latch": "Labware Latch", @@ -89,5 +89,7 @@ "module_anchors_extended": "Before the run begins, module should have both anchors fully extended for a firm attachment to Slot {{slot}}.", "thermal_adapter_attached_to_module": "The thermal adapter should be attached to the module.", "proceed_to_run": "Proceed to run", - "confirm_attachment": "Confirm attachment" + "confirm_attachment": "Confirm attachment", + "closed": "Closed", + "closed_and_locked": "Closed and Locked" } diff --git a/app/src/atoms/InputField/index.tsx b/app/src/atoms/InputField/index.tsx index 8fe2d80c8db..39d62bd2881 100644 --- a/app/src/atoms/InputField/index.tsx +++ b/app/src/atoms/InputField/index.tsx @@ -113,6 +113,11 @@ function Input(props: InputFieldProps): JSX.Element { &:disabled { border: ${SPACING.spacingXXS} ${BORDERS.styleSolid} ${COLORS.greyDisabled}; } + input[type='number']::-webkit-inner-spin-button, + input[type='number']::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } ` return ( diff --git a/app/src/organisms/Devices/HeaterShakerWizard/TestShake.tsx b/app/src/organisms/Devices/HeaterShakerWizard/TestShake.tsx index fc382e43946..f932c859fa9 100644 --- a/app/src/organisms/Devices/HeaterShakerWizard/TestShake.tsx +++ b/app/src/organisms/Devices/HeaterShakerWizard/TestShake.tsx @@ -1,5 +1,6 @@ import React from 'react' -import { useTranslation } from 'react-i18next' +import { Trans, useTranslation } from 'react-i18next' +import { useCreateLiveCommandMutation } from '@opentrons/react-api-client' import { ALIGN_CENTER, ALIGN_FLEX_START, @@ -8,30 +9,82 @@ import { DIRECTION_ROW, Flex, Icon, - InputField, SIZE_AUTO, SPACING, Text, TYPOGRAPHY, + Tooltip, + useHoverTooltip, } from '@opentrons/components' -import { RPM } from '@opentrons/shared-data' +import { RPM, HS_RPM_MAX, HS_RPM_MIN } from '@opentrons/shared-data' +import { + useHeaterShakerFromProtocol, + useLatchCommand, +} from '../ModuleCard/hooks' import { HeaterShakerModuleCard } from './HeaterShakerModuleCard' import { TertiaryButton } from '../../../atoms/Buttons' import { CollapsibleStep } from '../../ProtocolSetup/RunSetupCard/CollapsibleStep' import { Divider } from '../../../atoms/structure' +import { InputField } from '../../../atoms/InputField' import type { HeaterShakerModule } from '../../../redux/modules/types' +import type { + HeaterShakerSetTargetShakeSpeedCreateCommand, + HeaterShakerStopShakeCreateCommand, +} from '@opentrons/shared-data/protocol/types/schemaV6/command/module' interface TestShakeProps { module: HeaterShakerModule setCurrentPage: React.Dispatch> + hasProtocol: boolean | undefined } export function TestShake(props: TestShakeProps): JSX.Element { - const { module, setCurrentPage } = props - const { t } = useTranslation('heater_shaker') - + const { module, setCurrentPage, hasProtocol } = props + const { t } = useTranslation(['heater_shaker', 'device_details']) + const { createLiveCommand } = useCreateLiveCommandMutation() + const heaterShakerFromProtocol = useHeaterShakerFromProtocol() const [isExpanded, setExpanded] = React.useState(false) + const [shakeValue, setShakeValue] = React.useState(null) + const [targetProps, tooltipProps] = useHoverTooltip() + const { toggleLatch, isLatchClosed } = useLatchCommand(module) + const isShaking = module.data.speedStatus !== 'idle' + + const setShakeCommand: HeaterShakerSetTargetShakeSpeedCreateCommand = { + commandType: 'heaterShakerModule/setTargetShakeSpeed', + params: { + moduleId: module.id, + rpm: shakeValue !== null ? parseInt(shakeValue) : 0, + }, + } + + const stopShakeCommand: HeaterShakerStopShakeCreateCommand = { + commandType: 'heaterShakerModule/stopShake', + params: { + moduleId: module.id, + }, + } + + const handleShakeCommand = (): void => { + if (shakeValue !== null) { + createLiveCommand({ + command: isShaking ? stopShakeCommand : setShakeCommand, + }).catch((e: Error) => { + console.error( + `error setting module status with command type ${ + stopShakeCommand.commandType ?? setShakeCommand.commandType + }: ${e.message}` + ) + }) + } + setShakeValue(null) + } + + const errorMessage = + shakeValue != null && + (parseInt(shakeValue) < HS_RPM_MIN || parseInt(shakeValue) > HS_RPM_MAX) + ? t('input_out_of_range', { ns: 'device_details' }) + : null return ( @@ -65,8 +118,26 @@ export function TestShake(props: TestShakeProps): JSX.Element { paddingBottom={SPACING.spacing4} > - {/* TODO(sh, 2022-02-22): Dynamically render this text if a labware/protocol exists */} - {t('test_shake_banner_information')} + , + block: ( + + ), + }} + /> @@ -76,31 +147,61 @@ export function TestShake(props: TestShakeProps): JSX.Element { fontSize={TYPOGRAPHY.fontSizeCaption} > - - {t('open_labware_latch')} + + {isLatchClosed ? t('open_labware_latch') : t('close_labware_latch')} + {isShaking ? ( + + {t('cannot_open_latch', { ns: 'heater_shaker' })} + + ) : null} - + {t('set_shake_speed')} - {/* TODO(sh, 2022-02-22): Wire up input when end points are updated */} - - - {t('min_max_rpm', { min: '200', max: '1800' })} - + setShakeValue(e.target.value)} + type="number" + caption={t('min_max_rpm', { + ns: 'heater_shaker', + min: HS_RPM_MIN, + max: HS_RPM_MAX, + })} + error={errorMessage} + /> - {t('start_shaking')} + {isShaking ? t('stop_shaking') : t('start_shaking')} + {!isLatchClosed ? ( + + {t('cannot_shake', { ns: 'heater_shaker' })} + + ) : null} diff --git a/app/src/organisms/Devices/HeaterShakerWizard/__tests__/HeaterShakerWizard.test.tsx b/app/src/organisms/Devices/HeaterShakerWizard/__tests__/HeaterShakerWizard.test.tsx index bc1b6fa64ee..2014f93fe4a 100644 --- a/app/src/organisms/Devices/HeaterShakerWizard/__tests__/HeaterShakerWizard.test.tsx +++ b/app/src/organisms/Devices/HeaterShakerWizard/__tests__/HeaterShakerWizard.test.tsx @@ -1,9 +1,9 @@ import * as React from 'react' +import { MemoryRouter } from 'react-router-dom' import { fireEvent } from '@testing-library/react' import { renderWithProviders } from '@opentrons/components' import { i18n } from '../../../../i18n' -import { getAttachedModules } from '../../../../redux/modules' -import { getConnectedRobotName } from '../../../../redux/robot/selectors' +import { useAttachedModules } from '../../hooks' import { mockHeaterShaker } from '../../../../redux/modules/__fixtures__' import { HeaterShakerWizard } from '..' import { Introduction } from '../Introduction' @@ -13,17 +13,16 @@ import { AttachAdapter } from '../AttachAdapter' import { PowerOn } from '../PowerOn' import { TestShake } from '../TestShake' -jest.mock('../../../../redux/robot/selectors') +jest.mock('../../hooks') jest.mock('../Introduction') jest.mock('../KeyParts') jest.mock('../AttachModule') jest.mock('../AttachAdapter') jest.mock('../PowerOn') jest.mock('../TestShake') -jest.mock('../../../../redux/modules') -const mockGetConnectedRobotName = getConnectedRobotName as jest.MockedFunction< - typeof getConnectedRobotName +const mockUseAttachedModules = useAttachedModules as jest.MockedFunction< + typeof useAttachedModules > const mockIntroduction = Introduction as jest.MockedFunction< typeof Introduction @@ -37,14 +36,19 @@ const mockAttachAdapter = AttachAdapter as jest.MockedFunction< > const mockPowerOn = PowerOn as jest.MockedFunction const mockTestShake = TestShake as jest.MockedFunction -const mockGetAttachedModules = getAttachedModules as jest.MockedFunction< - typeof getAttachedModules -> -const render = (props: React.ComponentProps) => { - return renderWithProviders(, { - i18nInstance: i18n, - })[0] +const render = ( + props: React.ComponentProps, + path = '/' +) => { + return renderWithProviders( + + + , + { + i18nInstance: i18n, + } + )[0] } describe('HeaterShakerWizard', () => { @@ -52,19 +56,18 @@ describe('HeaterShakerWizard', () => { onCloseClick: jest.fn(), } beforeEach(() => { - mockGetConnectedRobotName.mockReturnValue('Mock Robot') + mockUseAttachedModules.mockReturnValue([mockHeaterShaker]) mockIntroduction.mockReturnValue(
Mock Introduction
) mockKeyParts.mockReturnValue(
Mock Key Parts
) mockAttachModule.mockReturnValue(
Mock Attach Module
) mockAttachAdapter.mockReturnValue(
Mock Attach Adapter
) mockPowerOn.mockReturnValue(
Mock Power On
) mockTestShake.mockReturnValue(
Mock Test Shake
) - mockGetAttachedModules.mockReturnValue([mockHeaterShaker]) }) it('renders the main modal component of the wizard', () => { const { getByText } = render(props) - getByText('Mock Robot - Attach Heater Shaker Module') + getByText(/Attach Heater Shaker Module/i) getByText('Mock Introduction') }) @@ -95,7 +98,7 @@ describe('HeaterShakerWizard', () => { }) it('renders power on component and the test shake button is not disabled', () => { - mockGetAttachedModules.mockReturnValue([]) + mockUseAttachedModules.mockReturnValue([]) const { getByText, getByRole } = render(props) diff --git a/app/src/organisms/Devices/HeaterShakerWizard/__tests__/TestShake.test.tsx b/app/src/organisms/Devices/HeaterShakerWizard/__tests__/TestShake.test.tsx index 720eb245c0b..038b6e18f59 100644 --- a/app/src/organisms/Devices/HeaterShakerWizard/__tests__/TestShake.test.tsx +++ b/app/src/organisms/Devices/HeaterShakerWizard/__tests__/TestShake.test.tsx @@ -1,13 +1,32 @@ import * as React from 'react' -import { renderWithProviders } from '@opentrons/components' +import { nestedTextMatcher, renderWithProviders } from '@opentrons/components' import { fireEvent } from '@testing-library/react' +import { useCreateLiveCommandMutation } from '@opentrons/react-api-client' +import { + useHeaterShakerFromProtocol, + useLatchCommand, +} from '../../ModuleCard/hooks' import { i18n } from '../../../../i18n' import { TestShake } from '../TestShake' import { HeaterShakerModuleCard } from '../HeaterShakerModuleCard' +import heaterShakerCommands from '@opentrons/shared-data/protocol/fixtures/6/heaterShakerCommands.json' import { mockHeaterShaker } from '../../../../redux/modules/__fixtures__' +import type { ProtocolModuleInfo } from '../../../ProtocolSetup/utils/getProtocolModulesInfo' + +jest.mock('@opentrons/react-api-client') jest.mock('../HeaterShakerModuleCard') +jest.mock('../../ModuleCard/hooks') +const mockUseLiveCommandMutation = useCreateLiveCommandMutation as jest.MockedFunction< + typeof useCreateLiveCommandMutation +> +const mockUseLatchCommand = useLatchCommand as jest.MockedFunction< + typeof useLatchCommand +> +const mockUseHeaterShakerFromProtocol = useHeaterShakerFromProtocol as jest.MockedFunction< + typeof useHeaterShakerFromProtocol +> const mockHeaterShakerModuleCard = HeaterShakerModuleCard as jest.MockedFunction< typeof HeaterShakerModuleCard > @@ -18,16 +37,112 @@ const render = (props: React.ComponentProps) => { })[0] } +const HEATER_SHAKER_PROTOCOL_MODULE_INFO = { + moduleId: 'heater_shaker_id', + x: 0, + y: 0, + z: 0, + moduleDef: mockHeaterShaker as any, + nestedLabwareDef: heaterShakerCommands.labwareDefinitions['example/plate/1'], + nestedLabwareDisplayName: 'Source Plate', + nestedLabwareId: null, + protocolLoadOrder: 1, + slotName: '1', +} as ProtocolModuleInfo + +const mockOpenLatchHeaterShaker = { + id: 'heatershaker_id', + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + port: '/dev/ot_module_heatershaker0', + serial: 'jkl123', + revision: 'heatershaker_v4.0', + fwVersion: 'v2.0.0', + status: 'idle', + hasAvailableUpdate: true, + data: { + labwareLatchStatus: 'idle_open', + speedStatus: 'idle', + temperatureStatus: 'idle', + currentSpeed: null, + currentTemp: null, + targetSpeed: null, + targetTemp: null, + errorDetails: null, + }, + usbPort: { hub: 1, port: 1 }, +} as any + +const mockCloseLatchHeaterShaker = { + id: 'heatershaker_id', + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + port: '/dev/ot_module_heatershaker0', + serial: 'jkl123', + revision: 'heatershaker_v4.0', + fwVersion: 'v2.0.0', + status: 'idle', + hasAvailableUpdate: true, + data: { + labwareLatchStatus: 'idle_closed', + speedStatus: 'idle', + temperatureStatus: 'idle', + currentSpeed: null, + currentTemp: null, + targetSpeed: null, + targetTemp: null, + errorDetails: null, + }, + usbPort: { hub: 1, port: 1 }, +} as any + +const mockMovingHeaterShaker = { + id: 'heatershaker_id', + model: 'heaterShakerModuleV1', + type: 'heaterShakerModuleType', + port: '/dev/ot_module_thermocycler0', + serial: 'jkl123', + revision: 'heatershaker_v4.0', + fwVersion: 'v2.0.0', + status: 'idle', + hasAvailableUpdate: true, + data: { + labwareLatchStatus: 'idle_closed', + speedStatus: 'speeding up', + temperatureStatus: 'idle', + currentSpeed: null, + currentTemp: null, + targetSpeed: null, + targetTemp: null, + errorDetails: null, + }, + usbPort: { hub: 1, port: 1 }, +} as any + describe('TestShake', () => { let props: React.ComponentProps + let mockCreateLiveCommand = jest.fn() beforeEach(() => { props = { setCurrentPage: jest.fn(), module: mockHeaterShaker, + hasProtocol: false, } + mockCreateLiveCommand = jest.fn() + mockCreateLiveCommand.mockResolvedValue(null) + mockUseLiveCommandMutation.mockReturnValue({ + createLiveCommand: mockCreateLiveCommand, + } as any) mockHeaterShakerModuleCard.mockReturnValue(
Mock Heater Shaker Module Card
) + mockUseLatchCommand.mockReturnValue({ + handleLatch: jest.fn(), + isLatchClosed: true, + } as any) + mockUseHeaterShakerFromProtocol.mockReturnValue( + HEATER_SHAKER_PROTOCOL_MODULE_INFO + ) }) it('renders the correct title', () => { const { getByText } = render(props) @@ -42,19 +157,50 @@ describe('TestShake', () => { ) }) + it('renders labware name in the banner description when there is a protocol', () => { + props = { + setCurrentPage: jest.fn(), + module: mockHeaterShaker, + hasProtocol: true, + } + const { getByText } = render(props) + getByText( + nestedTextMatcher( + 'If you want to add the Source Plate to the module before doing a test shake, you can use the labware latch controls.' + ) + ) + }) + it('renders a heater shaker module card', () => { const { getByText } = render(props) getByText('Mock Heater Shaker Module Card') }) - it('renders the open labware latch button and is enabled', () => { + it('renders the close labware latch button and is enabled when latch status is open', () => { + props = { + module: mockHeaterShaker, + setCurrentPage: jest.fn(), + hasProtocol: false, + } + + mockUseLatchCommand.mockReturnValue({ + toggleLatch: jest.fn(), + isLatchClosed: false, + }) + const { getByRole } = render(props) - const button = getByRole('button', { name: /Open Labware Latch/i }) + const button = getByRole('button', { name: /Close Labware Latch/i }) expect(button).toBeEnabled() }) it('renders the start shaking button and is enabled', () => { + props = { + module: mockCloseLatchHeaterShaker, + setCurrentPage: jest.fn(), + hasProtocol: false, + } + const { getByRole } = render(props) const button = getByRole('button', { name: /Start Shaking/i }) expect(button).toBeEnabled() @@ -64,7 +210,7 @@ describe('TestShake', () => { const { getByText, getByRole } = render(props) getByText('Set shake speed') - getByRole('textbox') + getByRole('spinbutton') }) it('renders troubleshooting accordion and contents', () => { @@ -85,4 +231,104 @@ describe('TestShake', () => { const buttonStep2 = getByRole('button', { name: /Go to Step 2/i }) expect(buttonStep2).toBeEnabled() }) + + it('start shake button should be disabled if the labware latch is open', () => { + props = { + module: mockOpenLatchHeaterShaker, + setCurrentPage: jest.fn(), + hasProtocol: false, + } + + mockUseLatchCommand.mockReturnValue({ + toggleLatch: jest.fn(), + isLatchClosed: false, + }) + + const { getByRole } = render(props) + const button = getByRole('button', { name: /Start/i }) + expect(button).toBeDisabled() + }) + + it('clicking the open latch button should open the heater shaker latch', () => { + props = { + module: mockCloseLatchHeaterShaker, + setCurrentPage: jest.fn(), + hasProtocol: false, + } + + mockUseLatchCommand.mockReturnValue({ + toggleLatch: jest.fn(), + isLatchClosed: true, + }) + + const { getByRole } = render(props) + const button = getByRole('button', { name: /Open Labware Latch/i }) + fireEvent.click(button) + expect(mockUseLatchCommand).toHaveBeenCalled() + }) + + it('clicking the close latch button should close the heater shaker latch', () => { + props = { + module: mockOpenLatchHeaterShaker, + setCurrentPage: jest.fn(), + hasProtocol: false, + } + + mockUseLatchCommand.mockReturnValue({ + toggleLatch: jest.fn(), + isLatchClosed: false, + }) + + const { getByRole } = render(props) + const button = getByRole('button', { name: /Close Labware Latch/i }) + fireEvent.click(button) + expect(mockUseLatchCommand).toHaveBeenCalled() + }) + + it('entering an input for shake speed and clicking start should begin shaking', () => { + props = { + module: mockCloseLatchHeaterShaker, + setCurrentPage: jest.fn(), + hasProtocol: false, + } + + const { getByRole } = render(props) + const button = getByRole('button', { name: /Start Shaking/i }) + const input = getByRole('spinbutton') + fireEvent.change(input, { target: { value: '300' } }) + fireEvent.click(button) + + expect(mockCreateLiveCommand).toHaveBeenCalledWith({ + command: { + commandType: 'heaterShakerModule/setTargetShakeSpeed', + params: { + moduleId: 'heatershaker_id', + rpm: 300, + }, + }, + }) + }) + + it('when the heater shaker is shaking clicking stop should deactivate the shaking', () => { + props = { + module: mockMovingHeaterShaker, + setCurrentPage: jest.fn(), + hasProtocol: false, + } + + const { getByRole } = render(props) + const button = getByRole('button', { name: /Stop Shaking/i }) + const input = getByRole('spinbutton') + fireEvent.change(input, { target: { value: '0' } }) + fireEvent.click(button) + + expect(mockCreateLiveCommand).toHaveBeenCalledWith({ + command: { + commandType: 'heaterShakerModule/stopShake', + params: { + moduleId: mockHeaterShaker.id, + }, + }, + }) + }) }) diff --git a/app/src/organisms/Devices/HeaterShakerWizard/index.tsx b/app/src/organisms/Devices/HeaterShakerWizard/index.tsx index 7240d449ac2..b1076613b05 100644 --- a/app/src/organisms/Devices/HeaterShakerWizard/index.tsx +++ b/app/src/organisms/Devices/HeaterShakerWizard/index.tsx @@ -1,13 +1,9 @@ import * as React from 'react' +import { useParams } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { Portal } from '../../../App/portal' -import { useSelector } from 'react-redux' -import { getConnectedRobotName } from '../../../redux/robot/selectors' import { Interstitial } from '../../../atoms/Interstitial/Interstitial' -import { - getAttachedModules, - HEATERSHAKER_MODULE_TYPE, -} from '../../../redux/modules' +import { HEATERSHAKER_MODULE_TYPE } from '../../../redux/modules' import { PrimaryButton, SecondaryButton } from '../../../atoms/Buttons' import { Introduction } from './Introduction' import { KeyParts } from './KeyParts' @@ -24,23 +20,23 @@ import { useHoverTooltip, } from '@opentrons/components' -import type { State } from '../../../redux/types' +import type { NextGenRouteParams } from '../../../App/NextGenApp' import type { HeaterShakerModule } from '../../../redux/modules/types' +import { useAttachedModules } from '../hooks' interface HeaterShakerWizardProps { onCloseClick: () => unknown + hasProtocol?: boolean } export const HeaterShakerWizard = ( props: HeaterShakerWizardProps ): JSX.Element | null => { - const { onCloseClick } = props + const { onCloseClick, hasProtocol } = props const { t } = useTranslation(['heater_shaker', 'shared']) const [currentPage, setCurrentPage] = React.useState(0) - const robotName = useSelector((state: State) => getConnectedRobotName(state)) - const attachedModules = useSelector((state: State) => - getAttachedModules(state, robotName === null ? null : robotName) - ) + const { robotName } = useParams() + const attachedModules = useAttachedModules(robotName) const [targetProps, tooltipProps] = useHoverTooltip() const heaterShaker = (attachedModules.find( @@ -78,7 +74,11 @@ export const HeaterShakerWizard = ( case 5: buttonContent = t('complete') return ( - + ) default: return null diff --git a/app/src/organisms/Devices/ModuleCard/HeaterShakerModuleData.tsx b/app/src/organisms/Devices/ModuleCard/HeaterShakerModuleData.tsx index dd4984b3a95..6226f0631d8 100644 --- a/app/src/organisms/Devices/ModuleCard/HeaterShakerModuleData.tsx +++ b/app/src/organisms/Devices/ModuleCard/HeaterShakerModuleData.tsx @@ -13,13 +13,21 @@ import { DIRECTION_ROW, TYPOGRAPHY, C_SKY_BLUE, + TEXT_TRANSFORM_CAPITALIZE, + SIZE_1, } from '@opentrons/components' import { StatusLabel } from '../../../atoms/StatusLabel' +import type { + LatchStatus, + SpeedStatus, + TemperatureStatus, +} from '../../../redux/modules/api-types' + interface HeaterShakerModuleDataProps { - heaterStatus: string - shakerStatus: string - latchStatus: string + heaterStatus: TemperatureStatus + shakerStatus: SpeedStatus + latchStatus: LatchStatus targetTemp: number | null currentTemp: number | null targetSpeed: number | null @@ -40,7 +48,8 @@ export const HeaterShakerModuleData = ( currentSpeed, showTemperatureData, } = props - const { t } = useTranslation(['device_details', 'heater_shaker']) + const { t } = useTranslation(['device_details', 'heater_shaker', 'shared']) + const isShaking = shakerStatus !== 'idle' const getStatusLabelProps = ( status: string | null @@ -75,6 +84,38 @@ export const HeaterShakerModuleData = ( return StatusLabelProps } + const getLatchStatus = (latchStatus: LatchStatus): JSX.Element | string => { + switch (latchStatus) { + case 'opening': + case 'idle_open': + case 'idle_unknown': { + return ( + + {t('open', { ns: 'shared' })} + + ) + } + case 'closing': + case 'idle_closed': { + if (isShaking) { + return ( + + {t('closed_and_locked', { ns: 'heater_shaker' })} + + ) + } else { + return ( + + {t('closed', { ns: 'heater_shaker' })} + + ) + } + } + default: + return latchStatus + } + } + return ( <> @@ -162,13 +203,14 @@ export const HeaterShakerModuleData = ( alignItems={ALIGN_FLEX_START} > - {/* {TODO(sh, 2022-02-22): Conditionally render icon based on latch status} */} - - {latchStatus} + {isShaking && ( + + )} + {getLatchStatus(latchStatus)} diff --git a/app/src/organisms/Devices/ModuleCard/TestShakeSlideout.tsx b/app/src/organisms/Devices/ModuleCard/TestShakeSlideout.tsx index b3a82d560ff..13ff2204364 100644 --- a/app/src/organisms/Devices/ModuleCard/TestShakeSlideout.tsx +++ b/app/src/organisms/Devices/ModuleCard/TestShakeSlideout.tsx @@ -92,7 +92,7 @@ export const TestShakeSlideout = ( const errorMessage = shakeValue != null && (parseInt(shakeValue) < HS_RPM_MIN || parseInt(shakeValue) > HS_RPM_MAX) - ? t('input_out_of_range') + ? t('input_out_of_range', { ns: 'device_details' }) : null return ( diff --git a/app/src/organisms/Devices/ModuleCard/__tests__/ConfirmAttachmentModal.test.tsx b/app/src/organisms/Devices/ModuleCard/__tests__/ConfirmAttachmentModal.test.tsx index 998f8bb2427..a1dc8cfacf9 100644 --- a/app/src/organisms/Devices/ModuleCard/__tests__/ConfirmAttachmentModal.test.tsx +++ b/app/src/organisms/Devices/ModuleCard/__tests__/ConfirmAttachmentModal.test.tsx @@ -70,7 +70,7 @@ describe('ConfirmAttachmentBanner', () => { expect(props.onCloseClick).toHaveBeenCalled() }) - it('renders the correct modal info when accessed through proceed to run CTA and clicks proceed to run button ', () => { + it('renders the correct modal info when accessed through proceed to run CTA and clicks proceed to run button', () => { props = { onCloseClick: jest.fn(), isProceedToRunModal: true, diff --git a/app/src/organisms/Devices/ModuleCard/__tests__/HeaterShakerModuleData.test.tsx b/app/src/organisms/Devices/ModuleCard/__tests__/HeaterShakerModuleData.test.tsx index 289c11b8454..46fb26c464a 100644 --- a/app/src/organisms/Devices/ModuleCard/__tests__/HeaterShakerModuleData.test.tsx +++ b/app/src/organisms/Devices/ModuleCard/__tests__/HeaterShakerModuleData.test.tsx @@ -73,8 +73,8 @@ describe('HeaterShakerModuleData', () => { it('renders a shaking status', () => { props = { - heaterStatus: 'shaking', - shakerStatus: 'idle', + heaterStatus: 'idle', + shakerStatus: 'speeding up', latchStatus: 'idle_unknown', targetTemp: null, currentTemp: null, @@ -93,6 +93,6 @@ describe('HeaterShakerModuleData', () => { const { getByText } = render(props) getByText('Target: N/A RPM') getByText('Labware Latch') - getByText('idle_unknown') + getByText(/Open/i) }) }) diff --git a/app/src/organisms/ProtocolSetup/RunSetupCard/ModuleSetup/HeaterShakerSetupWizard/HeaterShakerBanner.tsx b/app/src/organisms/ProtocolSetup/RunSetupCard/ModuleSetup/HeaterShakerSetupWizard/HeaterShakerBanner.tsx index c007965c4fc..3cc04454c6c 100644 --- a/app/src/organisms/ProtocolSetup/RunSetupCard/ModuleSetup/HeaterShakerSetupWizard/HeaterShakerBanner.tsx +++ b/app/src/organisms/ProtocolSetup/RunSetupCard/ModuleSetup/HeaterShakerSetupWizard/HeaterShakerBanner.tsx @@ -17,7 +17,10 @@ export function HeaterShakerBanner( return ( <> {showWizard && ( - setShowWizard(false)} /> + setShowWizard(false)} + hasProtocol={true} + /> )}