diff --git a/app/src/organisms/Devices/HeaterShakerWizard/TestShake.tsx b/app/src/organisms/Devices/HeaterShakerWizard/TestShake.tsx index aa8d64cd803..f776250b94a 100644 --- a/app/src/organisms/Devices/HeaterShakerWizard/TestShake.tsx +++ b/app/src/organisms/Devices/HeaterShakerWizard/TestShake.tsx @@ -1,6 +1,9 @@ import React from 'react' import { Trans, useTranslation } from 'react-i18next' -import { useCreateLiveCommandMutation } from '@opentrons/react-api-client' +import { + useCreateCommandMutation, + useCreateLiveCommandMutation, +} from '@opentrons/react-api-client' import { ALIGN_CENTER, ALIGN_FLEX_START, @@ -23,6 +26,7 @@ import { InputField } from '../../../atoms/InputField' import { Collapsible } from '../ModuleCard/Collapsible' import { useLatchControls } from '../ModuleCard/hooks' import { HeaterShakerModuleCard } from './HeaterShakerModuleCard' +import { useModuleIdFromRun } from '../ModuleCard/useModuleIdFromRun' import type { HeaterShakerModule } from '../../../redux/modules/types' import type { @@ -35,22 +39,28 @@ interface TestShakeProps { module: HeaterShakerModule setCurrentPage: React.Dispatch> moduleFromProtocol?: ProtocolModuleInfo + runId?: string } export function TestShake(props: TestShakeProps): JSX.Element { - const { module, setCurrentPage, moduleFromProtocol } = props + const { module, setCurrentPage, moduleFromProtocol, runId } = props const { t } = useTranslation(['heater_shaker', 'device_details']) const { createLiveCommand } = useCreateLiveCommandMutation() + const { createCommand } = useCreateCommandMutation() const [isExpanded, setExpanded] = React.useState(false) const [shakeValue, setShakeValue] = React.useState(null) const [targetProps, tooltipProps] = useHoverTooltip() - const { toggleLatch, isLatchClosed } = useLatchControls(module) + const { toggleLatch, isLatchClosed } = useLatchControls(module, runId) + const { moduleIdFromRun } = useModuleIdFromRun( + module, + runId != null ? runId : null + ) const isShaking = module.data.speedStatus !== 'idle' const setShakeCommand: HeaterShakerSetTargetShakeSpeedCreateCommand = { commandType: 'heaterShakerModule/setTargetShakeSpeed', params: { - moduleId: module.id, + moduleId: runId != null ? moduleIdFromRun : module.id, rpm: shakeValue !== null ? parseInt(shakeValue) : 0, }, } @@ -58,12 +68,23 @@ export function TestShake(props: TestShakeProps): JSX.Element { const stopShakeCommand: HeaterShakerStopShakeCreateCommand = { commandType: 'heaterShakerModule/stopShake', params: { - moduleId: module.id, + moduleId: runId != null ? moduleIdFromRun : module.id, }, } const handleShakeCommand = (): void => { - if (shakeValue !== null) { + if (runId != null) { + createCommand({ + runId: runId, + command: isShaking ? stopShakeCommand : setShakeCommand, + }).catch((e: Error) => { + console.error( + `error setting module status with command type ${ + stopShakeCommand.commandType ?? setShakeCommand.commandType + } and run id ${runId}: ${e.message}` + ) + }) + } else { createLiveCommand({ command: isShaking ? stopShakeCommand : setShakeCommand, }).catch((e: Error) => { @@ -184,7 +205,7 @@ export function TestShake(props: TestShakeProps): JSX.Element { marginLeft={SIZE_AUTO} marginTop={SPACING.spacing4} onClick={handleShakeCommand} - disabled={!isLatchClosed} + disabled={!isLatchClosed || (shakeValue === null && !isShaking)} {...targetProps} > {isShaking ? t('stop_shaking') : t('start_shaking')} diff --git a/app/src/organisms/Devices/HeaterShakerWizard/__tests__/TestShake.test.tsx b/app/src/organisms/Devices/HeaterShakerWizard/__tests__/TestShake.test.tsx index cef61d49252..4751cae7a1e 100644 --- a/app/src/organisms/Devices/HeaterShakerWizard/__tests__/TestShake.test.tsx +++ b/app/src/organisms/Devices/HeaterShakerWizard/__tests__/TestShake.test.tsx @@ -1,29 +1,40 @@ import * as React from 'react' import { nestedTextMatcher, renderWithProviders } from '@opentrons/components' import { fireEvent } from '@testing-library/react' -import { useCreateLiveCommandMutation } from '@opentrons/react-api-client' +import { + useCreateCommandMutation, + useCreateLiveCommandMutation, +} from '@opentrons/react-api-client' import { i18n } from '../../../../i18n' import { useLatchControls } from '../../ModuleCard/hooks' 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 { useModuleIdFromRun } from '../../ModuleCard/useModuleIdFromRun' import type { ProtocolModuleInfo } from '../../../Devices/ProtocolRun/utils/getProtocolModulesInfo' jest.mock('@opentrons/react-api-client') jest.mock('../HeaterShakerModuleCard') jest.mock('../../ModuleCard/hooks') +jest.mock('../../ModuleCard/useModuleIdFromRun') const mockUseLiveCommandMutation = useCreateLiveCommandMutation as jest.MockedFunction< typeof useCreateLiveCommandMutation > +const mockUseCommandMutation = useCreateCommandMutation as jest.MockedFunction< + typeof useCreateCommandMutation +> const mockUseLatchControls = useLatchControls as jest.MockedFunction< typeof useLatchControls > const mockHeaterShakerModuleCard = HeaterShakerModuleCard as jest.MockedFunction< typeof HeaterShakerModuleCard > +const mockUseModuleIdFromRun = useModuleIdFromRun as jest.MockedFunction< + typeof useModuleIdFromRun +> const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -113,6 +124,7 @@ const mockMovingHeaterShaker = { describe('TestShake', () => { let props: React.ComponentProps let mockCreateLiveCommand = jest.fn() + let mockCreateCommand = jest.fn() beforeEach(() => { props = { setCurrentPage: jest.fn(), @@ -124,6 +136,10 @@ describe('TestShake', () => { mockUseLiveCommandMutation.mockReturnValue({ createLiveCommand: mockCreateLiveCommand, } as any) + mockCreateCommand = jest.fn() + mockUseCommandMutation.mockReturnValue({ + createCommand: mockCreateCommand, + } as any) mockHeaterShakerModuleCard.mockReturnValue(
Mock Heater Shaker Module Card
) @@ -131,6 +147,9 @@ describe('TestShake', () => { handleLatch: jest.fn(), isLatchClosed: true, } as any) + mockUseModuleIdFromRun.mockReturnValue({ + moduleIdFromRun: 'heatershaker_id', + }) }) it('renders the correct title', () => { const { getByText } = render(props) @@ -191,7 +210,7 @@ describe('TestShake', () => { const { getByRole } = render(props) const button = getByRole('button', { name: /Start Shaking/i }) - expect(button).toBeEnabled() + expect(button).toBeDisabled() }) it('renders an input field for speed setting', () => { diff --git a/app/src/organisms/Devices/HeaterShakerWizard/index.tsx b/app/src/organisms/Devices/HeaterShakerWizard/index.tsx index 412d75bb1b9..dfa05c208ed 100644 --- a/app/src/organisms/Devices/HeaterShakerWizard/index.tsx +++ b/app/src/organisms/Devices/HeaterShakerWizard/index.tsx @@ -30,12 +30,13 @@ import type { ProtocolModuleInfo } from '../../Devices/ProtocolRun/utils/getProt interface HeaterShakerWizardProps { onCloseClick: () => unknown moduleFromProtocol?: ProtocolModuleInfo + runId?: string } export const HeaterShakerWizard = ( props: HeaterShakerWizardProps ): JSX.Element | null => { - const { onCloseClick, moduleFromProtocol } = props + const { onCloseClick, moduleFromProtocol, runId } = props const { t } = useTranslation(['heater_shaker', 'shared']) const [currentPage, setCurrentPage] = React.useState(0) const { robotName } = useParams() @@ -99,6 +100,7 @@ export const HeaterShakerWizard = ( module={heaterShaker} setCurrentPage={setCurrentPage} moduleFromProtocol={moduleFromProtocol} + runId={runId} /> ) : null ) diff --git a/app/src/organisms/Devices/ModuleCard/HeaterShakerSlideout.tsx b/app/src/organisms/Devices/ModuleCard/HeaterShakerSlideout.tsx index 80fbda4160a..bd15cf19a32 100644 --- a/app/src/organisms/Devices/ModuleCard/HeaterShakerSlideout.tsx +++ b/app/src/organisms/Devices/ModuleCard/HeaterShakerSlideout.tsx @@ -25,6 +25,7 @@ import { TYPOGRAPHY, useConditionalConfirm, } from '@opentrons/components' +import { useModuleIdFromRun } from './useModuleIdFromRun' import { PrimaryButton } from '../../../atoms/buttons' import { getIsHeaterShakerAttached } from '../../../redux/config' import { InputField } from '../../../atoms/InputField' @@ -54,6 +55,10 @@ export const HeaterShakerSlideout = ( const { createCommand } = useCreateCommandMutation() const moduleName = getModuleDisplayName(module.moduleModel) const configHasHeaterShakerAttached = useSelector(getIsHeaterShakerAttached) + const { moduleIdFromRun } = useModuleIdFromRun( + module, + runId != null ? runId : null + ) const modulePart = isSetShake ? t('shake_speed') : t('temperature') const sendShakeSpeedCommand = (): void => { @@ -61,7 +66,7 @@ export const HeaterShakerSlideout = ( const setShakeCommand: HeaterShakerSetTargetShakeSpeedCreateCommand = { commandType: 'heaterShakerModule/setTargetShakeSpeed', params: { - moduleId: module.id, + moduleId: runId != null ? moduleIdFromRun : module.id, rpm: parseInt(hsValue), }, } @@ -98,7 +103,7 @@ export const HeaterShakerSlideout = ( const setTempCommand: HeaterShakerStartSetTargetTemperatureCreateCommand = { commandType: 'heaterShakerModule/startSetTargetTemperature', params: { - moduleId: module.id, + moduleId: runId != null ? moduleIdFromRun : module.id, // @ts-expect-error TODO: remove this after https://github.com/Opentrons/opentrons/pull/10182 merges temperature: parseInt(hsValue), }, diff --git a/app/src/organisms/Devices/ModuleCard/MagneticModuleSlideout.tsx b/app/src/organisms/Devices/ModuleCard/MagneticModuleSlideout.tsx index 8e48fe6f7ce..7d8e7d320c8 100644 --- a/app/src/organisms/Devices/ModuleCard/MagneticModuleSlideout.tsx +++ b/app/src/organisms/Devices/ModuleCard/MagneticModuleSlideout.tsx @@ -4,6 +4,7 @@ import { useCreateCommandMutation, useCreateLiveCommandMutation, } from '@opentrons/react-api-client' +import { useModuleIdFromRun } from './useModuleIdFromRun' import { Flex, Text, @@ -80,6 +81,10 @@ export const MagneticModuleSlideout = ( const [engageHeightValue, setEngageHeightValue] = React.useState< string | null >(null) + const { moduleIdFromRun } = useModuleIdFromRun( + module, + runId != null ? runId : null + ) const moduleName = getModuleDisplayName(module.moduleModel) const info = getInfoByModel(module.moduleModel) @@ -114,7 +119,7 @@ export const MagneticModuleSlideout = ( const setEngageCommand: MagneticModuleEngageMagnetCreateCommand = { commandType: 'magneticModule/engage', params: { - moduleId: module.id, + moduleId: runId != null ? moduleIdFromRun : module.id, height: parseInt(engageHeightValue), }, } diff --git a/app/src/organisms/Devices/ModuleCard/ModuleOverflowMenu.tsx b/app/src/organisms/Devices/ModuleCard/ModuleOverflowMenu.tsx index 03a16404307..cb3d6484ce8 100644 --- a/app/src/organisms/Devices/ModuleCard/ModuleOverflowMenu.tsx +++ b/app/src/organisms/Devices/ModuleCard/ModuleOverflowMenu.tsx @@ -30,6 +30,7 @@ export const ModuleOverflowMenu = ( handleTestShakeClick, handleWizardClick, } = props + const [targetProps, tooltipProps] = useHoverTooltip() const { menuOverflowItemsByModuleType } = useModuleOverflowMenu( module, diff --git a/app/src/organisms/Devices/ModuleCard/TemperatureModuleData.tsx b/app/src/organisms/Devices/ModuleCard/TemperatureModuleData.tsx index e1edbcf05db..564dc44b456 100644 --- a/app/src/organisms/Devices/ModuleCard/TemperatureModuleData.tsx +++ b/app/src/organisms/Devices/ModuleCard/TemperatureModuleData.tsx @@ -62,7 +62,7 @@ export const TemperatureModuleData = ( data-testid={`temp_module_data`} > - {t(targetTemp === null ? 'na_temp' : 'target_temp', { + {t(targetTemp == null ? 'na_temp' : 'target_temp', { temp: targetTemp, })} diff --git a/app/src/organisms/Devices/ModuleCard/TemperatureModuleSlideout.tsx b/app/src/organisms/Devices/ModuleCard/TemperatureModuleSlideout.tsx index 9d608795439..b9fd0f009a9 100644 --- a/app/src/organisms/Devices/ModuleCard/TemperatureModuleSlideout.tsx +++ b/app/src/organisms/Devices/ModuleCard/TemperatureModuleSlideout.tsx @@ -19,6 +19,7 @@ import { TEMP_MAX, TEMP_MIN, } from '@opentrons/shared-data' +import { useModuleIdFromRun } from './useModuleIdFromRun' import { Slideout } from '../../../atoms/Slideout' import { PrimaryButton } from '../../../atoms/buttons' import { InputField } from '../../../atoms/InputField' @@ -40,6 +41,10 @@ export const TemperatureModuleSlideout = ( const { t } = useTranslation('device_details') const { createLiveCommand } = useCreateLiveCommandMutation() const { createCommand } = useCreateCommandMutation() + const { moduleIdFromRun } = useModuleIdFromRun( + module, + runId != null ? runId : null + ) const name = getModuleDisplayName(module.moduleModel) const [temperatureValue, setTemperatureValue] = React.useState( null @@ -50,7 +55,7 @@ export const TemperatureModuleSlideout = ( const saveTempCommand: TemperatureModuleSetTargetTemperatureCreateCommand = { commandType: 'temperatureModule/setTargetTemperature', params: { - moduleId: module.id, + moduleId: runId != null ? moduleIdFromRun : module.id, celsius: parseInt(temperatureValue), }, } diff --git a/app/src/organisms/Devices/ModuleCard/TestShakeSlideout.tsx b/app/src/organisms/Devices/ModuleCard/TestShakeSlideout.tsx index 4a121527516..d6599ec7fd0 100644 --- a/app/src/organisms/Devices/ModuleCard/TestShakeSlideout.tsx +++ b/app/src/organisms/Devices/ModuleCard/TestShakeSlideout.tsx @@ -35,6 +35,7 @@ import { InputField } from '../../../atoms/InputField' import { Tooltip } from '../../../atoms/Tooltip' import { HeaterShakerWizard } from '../HeaterShakerWizard' import { useLatchControls } from './hooks' +import { useModuleIdFromRun } from './useModuleIdFromRun' import { Collapsible } from './Collapsible' import type { HeaterShakerModule } from '../../../redux/modules/types' @@ -60,6 +61,10 @@ export const TestShakeSlideout = ( const name = getModuleDisplayName(module.moduleModel) const [targetProps, tooltipProps] = useHoverTooltip() const { toggleLatch, isLatchClosed } = useLatchControls(module, runId) + const { moduleIdFromRun } = useModuleIdFromRun( + module, + runId != null ? runId : null + ) const [showCollapsed, setShowCollapsed] = React.useState(false) const [shakeValue, setShakeValue] = React.useState(null) @@ -69,7 +74,7 @@ export const TestShakeSlideout = ( const setShakeCommand: HeaterShakerSetTargetShakeSpeedCreateCommand = { commandType: 'heaterShakerModule/setTargetShakeSpeed', params: { - moduleId: module.id, + moduleId: runId != null ? moduleIdFromRun : module.id, rpm: shakeValue !== null ? parseInt(shakeValue) : 0, }, } @@ -77,34 +82,32 @@ export const TestShakeSlideout = ( const stopShakeCommand: HeaterShakerStopShakeCreateCommand = { commandType: 'heaterShakerModule/stopShake', params: { - moduleId: module.id, + moduleId: runId != null ? moduleIdFromRun : module.id, }, } const handleShakeCommand = (): void => { - if (shakeValue !== null) { - if (runId != null) { - createCommand({ - runId: runId, - command: isShaking ? stopShakeCommand : setShakeCommand, - }).catch((e: Error) => { - console.error( - `error setting module status with command type ${ - stopShakeCommand.commandType ?? setShakeCommand.commandType - }: ${e.message}` - ) - }) - } else { - createLiveCommand({ - command: isShaking ? stopShakeCommand : setShakeCommand, - }).catch((e: Error) => { - console.error( - `error setting module status with command type ${ - stopShakeCommand.commandType ?? setShakeCommand.commandType - }: ${e.message}` - ) - }) - } + if (runId != null) { + createCommand({ + runId: runId, + command: isShaking ? stopShakeCommand : setShakeCommand, + }).catch((e: Error) => { + console.error( + `error setting module status with command type ${ + stopShakeCommand.commandType ?? setShakeCommand.commandType + }: ${e.message}` + ) + }) + } else { + 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) } @@ -249,7 +252,7 @@ export const TestShakeSlideout = ( marginTop={SPACING.spacing3} paddingX={SPACING.spacing4} onClick={handleShakeCommand} - disabled={!isLatchClosed} + disabled={!isLatchClosed || (shakeValue === null && !isShaking)} {...targetProps} > {isShaking diff --git a/app/src/organisms/Devices/ModuleCard/ThermocyclerModuleSlideout.tsx b/app/src/organisms/Devices/ModuleCard/ThermocyclerModuleSlideout.tsx index 5b8cac7e7e5..8e3d153f7d8 100644 --- a/app/src/organisms/Devices/ModuleCard/ThermocyclerModuleSlideout.tsx +++ b/app/src/organisms/Devices/ModuleCard/ThermocyclerModuleSlideout.tsx @@ -24,6 +24,7 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { PrimaryButton } from '../../../atoms/buttons' +import { useModuleIdFromRun } from './useModuleIdFromRun' import type { ThermocyclerModule } from '../../../redux/modules/types' import type { @@ -47,6 +48,10 @@ export const ThermocyclerModuleSlideout = ( const [tempValue, setTempValue] = React.useState(null) const { createLiveCommand } = useCreateLiveCommandMutation() const { createCommand } = useCreateCommandMutation() + const { moduleIdFromRun } = useModuleIdFromRun( + module, + runId != null ? runId : null + ) const moduleName = getModuleDisplayName(module.moduleModel) const modulePart = isSecondaryTemp ? 'Lid' : 'Block' const tempRanges = getTCTempRange(isSecondaryTemp) @@ -71,14 +76,14 @@ export const ThermocyclerModuleSlideout = ( const saveLidCommand: TCSetTargetLidTemperatureCreateCommand = { commandType: 'thermocycler/setTargetLidTemperature', params: { - moduleId: module.id, + moduleId: runId != null ? moduleIdFromRun : module.id, celsius: parseInt(tempValue), }, } const saveBlockCommand: TCSetTargetBlockTemperatureCreateCommand = { commandType: 'thermocycler/setTargetBlockTemperature', params: { - moduleId: module.id, + moduleId: runId != null ? moduleIdFromRun : module.id, celsius: parseInt(tempValue), // TODO(jr, 3/17/22): add volume, which will be provided by PD protocols }, diff --git a/app/src/organisms/Devices/ModuleCard/__tests__/HeaterShakerSlideout.test.tsx b/app/src/organisms/Devices/ModuleCard/__tests__/HeaterShakerSlideout.test.tsx index 500f905bd1f..37f4795ef9c 100644 --- a/app/src/organisms/Devices/ModuleCard/__tests__/HeaterShakerSlideout.test.tsx +++ b/app/src/organisms/Devices/ModuleCard/__tests__/HeaterShakerSlideout.test.tsx @@ -6,6 +6,7 @@ import { } from '@opentrons/react-api-client' import { fireEvent } from '@testing-library/react' import { i18n } from '../../../../i18n' +import { useModuleIdFromRun } from '../useModuleIdFromRun' import { getIsHeaterShakerAttached } from '../../../../redux/config' import { mockHeaterShaker } from '../../../../redux/modules/__fixtures__' import { HeaterShakerSlideout } from '../HeaterShakerSlideout' @@ -14,6 +15,7 @@ import { ConfirmAttachmentModal } from '../ConfirmAttachmentModal' jest.mock('@opentrons/react-api-client') jest.mock('../ConfirmAttachmentModal') jest.mock('../../../../redux/config') +jest.mock('../useModuleIdFromRun') const mockGetIsHeaterShakerAttached = getIsHeaterShakerAttached as jest.MockedFunction< typeof getIsHeaterShakerAttached @@ -27,6 +29,9 @@ const mockUseCommandMutation = useCreateCommandMutation as jest.MockedFunction< const mockConfirmAttachmentModal = ConfirmAttachmentModal as jest.MockedFunction< typeof ConfirmAttachmentModal > +const mockUseModuleIdFromRun = useModuleIdFromRun as jest.MockedFunction< + typeof useModuleIdFromRun +> const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -56,6 +61,9 @@ describe('HeaterShakerSlideout', () => { mockConfirmAttachmentModal.mockReturnValue(
mock confirm attachment modal
) + mockUseModuleIdFromRun.mockReturnValue({ + moduleIdFromRun: 'heatershaker_id', + }) }) afterEach(() => { diff --git a/app/src/organisms/Devices/ModuleCard/__tests__/MagneticModuleSlideout.test.tsx b/app/src/organisms/Devices/ModuleCard/__tests__/MagneticModuleSlideout.test.tsx index f0a4f47c4c4..afbd2519027 100644 --- a/app/src/organisms/Devices/ModuleCard/__tests__/MagneticModuleSlideout.test.tsx +++ b/app/src/organisms/Devices/ModuleCard/__tests__/MagneticModuleSlideout.test.tsx @@ -6,6 +6,7 @@ import { useCreateCommandMutation, useCreateLiveCommandMutation, } from '@opentrons/react-api-client' +import { useModuleIdFromRun } from '../useModuleIdFromRun' import { MagneticModuleSlideout } from '../MagneticModuleSlideout' import { @@ -14,6 +15,7 @@ import { } from '../../../../redux/modules/__fixtures__' jest.mock('@opentrons/react-api-client') +jest.mock('../useModuleIdFromRun') const mockUseLiveCommandMutation = useCreateLiveCommandMutation as jest.MockedFunction< typeof useCreateLiveCommandMutation @@ -21,6 +23,9 @@ const mockUseLiveCommandMutation = useCreateLiveCommandMutation as jest.MockedFu const mockUseCommandMutation = useCreateCommandMutation as jest.MockedFunction< typeof useCreateCommandMutation > +const mockUseModuleIdFromRun = useModuleIdFromRun as jest.MockedFunction< + typeof useModuleIdFromRun +> const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -48,6 +53,7 @@ describe('MagneticModuleSlideout', () => { mockUseCommandMutation.mockReturnValue({ createCommand: mockCreateCommand, } as any) + mockUseModuleIdFromRun.mockReturnValue({ moduleIdFromRun: 'magdeck_id' }) }) afterEach(() => { jest.resetAllMocks() diff --git a/app/src/organisms/Devices/ModuleCard/__tests__/ModuleCard.test.tsx b/app/src/organisms/Devices/ModuleCard/__tests__/ModuleCard.test.tsx index 9e29ff0cdd1..b28e98d66ec 100644 --- a/app/src/organisms/Devices/ModuleCard/__tests__/ModuleCard.test.tsx +++ b/app/src/organisms/Devices/ModuleCard/__tests__/ModuleCard.test.tsx @@ -10,6 +10,7 @@ import { } from '../../../../redux/robot-api' import { Toast } from '../../../../atoms/Toast' import { useCurrentRunStatus } from '../../../RunTimeControl/hooks' +import { useModuleIdFromRun } from '../useModuleIdFromRun' import * as RobotApi from '../../../../redux/robot-api' import { MagneticModuleData } from '../MagneticModuleData' import { TemperatureModuleData } from '../TemperatureModuleData' @@ -42,6 +43,7 @@ jest.mock('../../../RunTimeControl/hooks') jest.mock('../FirmwareUpdateFailedModal') jest.mock('../../../../redux/robot-api') jest.mock('../../../../atoms/Toast') +jest.mock('../useModuleIdFromRun') jest.mock('react-router-dom', () => { const reactRouterDom = jest.requireActual('react-router-dom') return { @@ -50,6 +52,9 @@ jest.mock('react-router-dom', () => { } }) +const mockUseModuleIdFromRun = useModuleIdFromRun as jest.MockedFunction< + typeof useModuleIdFromRun +> const mockMagneticModuleData = MagneticModuleData as jest.MockedFunction< typeof MagneticModuleData > @@ -156,17 +161,18 @@ describe('ModuleCard', () => { }) it('renders information for a magnetic module with mocked status', () => { + mockUseModuleIdFromRun.mockReturnValue({ moduleIdFromRun: 'magdeck_id' }) const { getByText, getByAltText } = render({ module: mockMagneticModule, robotName: mockRobot.name, }) - getByText('Magnetic Module GEN1') getByText('Mock Magnetic Module Data') getByText('usb port 1') getByAltText('magneticModuleV1') }) it('renders information if module is connected via hub', () => { + mockUseModuleIdFromRun.mockReturnValue({ moduleIdFromRun: 'magdeck_id' }) const { getByText, getByAltText } = render({ module: mockMagneticModuleHub, robotName: mockRobot.name, @@ -180,6 +186,7 @@ describe('ModuleCard', () => { mockTemperatureModuleData.mockReturnValue(
Mock Temperature Module Data
) + mockUseModuleIdFromRun.mockReturnValue({ moduleIdFromRun: 'tempdeck_id' }) const { getByText, getByAltText } = render({ module: mockTemperatureModuleGen2, @@ -192,6 +199,9 @@ describe('ModuleCard', () => { }) it('renders information for a thermocycler module with mocked status', () => { + mockUseModuleIdFromRun.mockReturnValue({ + moduleIdFromRun: 'thermocycler_id', + }) const { getByText, getByAltText } = render({ module: mockThermocycler, robotName: mockRobot.name, @@ -205,6 +215,9 @@ describe('ModuleCard', () => { it('renders information for a heater shaker module with mocked status', () => { mockGetIsHeaterShakerAttached.mockReturnValue(true) + mockUseModuleIdFromRun.mockReturnValue({ + moduleIdFromRun: 'heatershaker_id', + }) const { getByText, getByAltText } = render({ module: mockHeaterShaker, robotName: mockRobot.name, @@ -217,6 +230,7 @@ describe('ModuleCard', () => { }) it('renders kebab icon and is clickable', () => { + mockUseModuleIdFromRun.mockReturnValue({ moduleIdFromRun: 'magdeck_id' }) const { getByRole, getByText } = render({ module: mockMagneticModule, robotName: mockRobot.name, @@ -234,7 +248,7 @@ describe('ModuleCard', () => { when(mockUseCurrentRunStatus) .calledWith(expect.any(Object)) .mockReturnValue(RUN_STATUS_RUNNING) - + mockUseModuleIdFromRun.mockReturnValue({ moduleIdFromRun: 'magdeck_id' }) const { getByRole, getByText } = render({ module: mockMagneticModule, robotName: mockRobot.name, @@ -247,6 +261,9 @@ describe('ModuleCard', () => { }) it('renders information for a heater shaker module when it is hot, showing the too hot banner', () => { + mockUseModuleIdFromRun.mockReturnValue({ + moduleIdFromRun: 'heatershaker_id', + }) const { getByText } = render({ module: mockHotHeaterShaker, robotName: mockRobot.name, @@ -254,6 +271,9 @@ describe('ModuleCard', () => { getByText(nestedTextMatcher('Module is hot to the touch')) }) it('renders information success toast when update has completed', () => { + mockUseModuleIdFromRun.mockReturnValue({ + moduleIdFromRun: 'heatershaker_id', + }) mockGetRequestById.mockReturnValue({ status: RobotApi.SUCCESS, response: { @@ -270,6 +290,7 @@ describe('ModuleCard', () => { getByText('mock toast') }) it('renders information for a magnetic module when an update is available so update banner renders', () => { + mockUseModuleIdFromRun.mockReturnValue({ moduleIdFromRun: 'magdeck_id' }) const { getByText } = render({ module: mockMagneticModuleHub, robotName: mockRobot.name, @@ -290,6 +311,7 @@ describe('ModuleCard', () => { }, error: { message: 'ruh roh' }, }) + mockUseModuleIdFromRun.mockReturnValue({ moduleIdFromRun: 'magdeck_id' }) const { getByText } = render({ module: mockMagneticModuleHub, @@ -305,6 +327,7 @@ describe('ModuleCard', () => { mockGetRequestById.mockReturnValue({ status: RobotApi.PENDING, }) + mockUseModuleIdFromRun.mockReturnValue({ moduleIdFromRun: 'magdeck_id' }) const { getByText, getByLabelText } = render({ module: mockMagneticModuleHub, robotName: mockRobot.name, diff --git a/app/src/organisms/Devices/ModuleCard/__tests__/ModuleOverflowMenu.test.tsx b/app/src/organisms/Devices/ModuleCard/__tests__/ModuleOverflowMenu.test.tsx index be533ef1bbe..b29ec4fa575 100644 --- a/app/src/organisms/Devices/ModuleCard/__tests__/ModuleOverflowMenu.test.tsx +++ b/app/src/organisms/Devices/ModuleCard/__tests__/ModuleOverflowMenu.test.tsx @@ -9,6 +9,13 @@ import { mockHeaterShaker, } from '../../../../redux/modules/__fixtures__' import { ModuleOverflowMenu } from '../ModuleOverflowMenu' +import { useModuleIdFromRun } from '../useModuleIdFromRun' + +jest.mock('../useModuleIdFromRun') + +const mockUseModuleIdFromRun = useModuleIdFromRun as jest.MockedFunction< + typeof useModuleIdFromRun +> const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -164,6 +171,7 @@ const mockTCBlockHeating = { describe('ModuleOverflowMenu', () => { let props: React.ComponentProps beforeEach(() => { + mockUseModuleIdFromRun.mockReturnValue({ moduleIdFromRun: 'magdeck_id' }) props = { module: mockMagneticModule, handleSlideoutClick: jest.fn(), diff --git a/app/src/organisms/Devices/ModuleCard/__tests__/TemperatureModuleSlideout.test.tsx b/app/src/organisms/Devices/ModuleCard/__tests__/TemperatureModuleSlideout.test.tsx index b456fdaf856..7e40ba68214 100644 --- a/app/src/organisms/Devices/ModuleCard/__tests__/TemperatureModuleSlideout.test.tsx +++ b/app/src/organisms/Devices/ModuleCard/__tests__/TemperatureModuleSlideout.test.tsx @@ -7,12 +7,14 @@ import { import { fireEvent } from '@testing-library/react' import { renderWithProviders } from '@opentrons/components' import { TemperatureModuleSlideout } from '../TemperatureModuleSlideout' +import { useModuleIdFromRun } from '../useModuleIdFromRun' import { mockTemperatureModule, mockTemperatureModuleGen2, } from '../../../../redux/modules/__fixtures__' jest.mock('@opentrons/react-api-client') +jest.mock('../useModuleIdFromRun') const mockUseLiveCommandMutation = useCreateLiveCommandMutation as jest.MockedFunction< typeof useCreateLiveCommandMutation @@ -20,6 +22,9 @@ const mockUseLiveCommandMutation = useCreateLiveCommandMutation as jest.MockedFu const mockUseCommandMutation = useCreateCommandMutation as jest.MockedFunction< typeof useCreateCommandMutation > +const mockUseModuleIdFromRun = useModuleIdFromRun as jest.MockedFunction< + typeof useModuleIdFromRun +> const render = ( props: React.ComponentProps @@ -51,6 +56,7 @@ describe('TemperatureModuleSlideout', () => { mockUseCommandMutation.mockReturnValue({ createCommand: mockCreateCommand, } as any) + mockUseModuleIdFromRun.mockReturnValue({ moduleIdFromRun: 'tempdeck_id' }) }) afterEach(() => { jest.resetAllMocks() diff --git a/app/src/organisms/Devices/ModuleCard/__tests__/TestShakeSlideout.test.tsx b/app/src/organisms/Devices/ModuleCard/__tests__/TestShakeSlideout.test.tsx index 0aaf2440c3e..0b871891b17 100644 --- a/app/src/organisms/Devices/ModuleCard/__tests__/TestShakeSlideout.test.tsx +++ b/app/src/organisms/Devices/ModuleCard/__tests__/TestShakeSlideout.test.tsx @@ -10,10 +10,12 @@ import { TestShakeSlideout } from '../TestShakeSlideout' import { HeaterShakerModuleCard } from '../../HeaterShakerWizard/HeaterShakerModuleCard' import { mockHeaterShaker } from '../../../../redux/modules/__fixtures__' import { useLatchControls } from '../hooks' +import { useModuleIdFromRun } from '../useModuleIdFromRun' jest.mock('@opentrons/react-api-client') jest.mock('../../HeaterShakerWizard/HeaterShakerModuleCard') jest.mock('../hooks') +jest.mock('../useModuleIdFromRun') const mockUseLiveCommandMutation = useCreateLiveCommandMutation as jest.MockedFunction< typeof useCreateLiveCommandMutation @@ -27,6 +29,9 @@ const mockHeaterShakerModuleCard = HeaterShakerModuleCard as jest.MockedFunction const mockUseLatchControls = useLatchControls as jest.MockedFunction< typeof useLatchControls > +const mockUseModuleIdFromRun = useModuleIdFromRun as jest.MockedFunction< + typeof useModuleIdFromRun +> const render = (props: React.ComponentProps) => { return renderWithProviders(, { @@ -128,6 +133,9 @@ describe('TestShakeSlideout', () => { mockHeaterShakerModuleCard.mockReturnValue(
Mock Heater Shaker Module Card
) + mockUseModuleIdFromRun.mockReturnValue({ + moduleIdFromRun: 'heatershaker_id', + }) }) afterEach(() => { @@ -162,7 +170,7 @@ describe('TestShakeSlideout', () => { getByText('Shake speed') const button = getByRole('button', { name: /Start/i }) - expect(button).toBeEnabled() + expect(button).toBeDisabled() }) it('renders a troubleshoot accordion and contents when it is clicked', () => { diff --git a/app/src/organisms/Devices/ModuleCard/__tests__/ThermocyclerModuleSlideout.test.tsx b/app/src/organisms/Devices/ModuleCard/__tests__/ThermocyclerModuleSlideout.test.tsx index ab40cc2c0fc..3c18ca8f018 100644 --- a/app/src/organisms/Devices/ModuleCard/__tests__/ThermocyclerModuleSlideout.test.tsx +++ b/app/src/organisms/Devices/ModuleCard/__tests__/ThermocyclerModuleSlideout.test.tsx @@ -6,11 +6,13 @@ import { useCreateLiveCommandMutation, } from '@opentrons/react-api-client' import { i18n } from '../../../../i18n' +import { useModuleIdFromRun } from '../useModuleIdFromRun' import { ThermocyclerModuleSlideout } from '../ThermocyclerModuleSlideout' import { mockThermocycler } from '../../../../redux/modules/__fixtures__' jest.mock('@opentrons/react-api-client') +jest.mock('../useModuleIdFromRun') const mockUseLiveCommandMutation = useCreateLiveCommandMutation as jest.MockedFunction< typeof useCreateLiveCommandMutation @@ -18,6 +20,9 @@ const mockUseLiveCommandMutation = useCreateLiveCommandMutation as jest.MockedFu const mockUseCommandMutation = useCreateCommandMutation as jest.MockedFunction< typeof useCreateCommandMutation > +const mockUseModuleIdFromRun = useModuleIdFromRun as jest.MockedFunction< + typeof useModuleIdFromRun +> const render = ( props: React.ComponentProps @@ -43,6 +48,9 @@ describe('ThermocyclerModuleSlideout', () => { mockUseCommandMutation.mockReturnValue({ createCommand: mockCreateCommand, } as any) + mockUseModuleIdFromRun.mockReturnValue({ + moduleIdFromRun: 'thermocycler_id', + }) }) afterEach(() => { jest.resetAllMocks() diff --git a/app/src/organisms/Devices/ModuleCard/__tests__/hooks.test.tsx b/app/src/organisms/Devices/ModuleCard/__tests__/hooks.test.tsx index 9ddc3fd3608..101caf7ab37 100644 --- a/app/src/organisms/Devices/ModuleCard/__tests__/hooks.test.tsx +++ b/app/src/organisms/Devices/ModuleCard/__tests__/hooks.test.tsx @@ -20,6 +20,7 @@ import { useModuleOverflowMenu, useIsHeaterShakerInProtocol, } from '../hooks' +import { useModuleIdFromRun } from '../useModuleIdFromRun' import { mockHeaterShaker, @@ -35,6 +36,7 @@ jest.mock('@opentrons/react-api-client') jest.mock('../../../Devices/ProtocolRun/utils/getProtocolModulesInfo') jest.mock('../../../ProtocolUpload/hooks') jest.mock('../../hooks') +jest.mock('../useModuleIdFromRun') const mockUseProtocolDetailsForRun = useProtocolDetailsForRun as jest.MockedFunction< typeof useProtocolDetailsForRun @@ -52,6 +54,9 @@ const mockUseCreateCommandMutation = useCreateCommandMutation as jest.MockedFunc const mockUseCurrentRunId = useCurrentRunId as jest.MockedFunction< typeof useCurrentRunId > +const mockUseModuleIdFromRun = useModuleIdFromRun as jest.MockedFunction< + typeof useModuleIdFromRun +> const mockCloseLatchHeaterShaker = { id: 'heatershaker_id', @@ -231,6 +236,9 @@ describe('useLatchControls', () => { {children} ) + mockUseModuleIdFromRun.mockReturnValue({ + moduleIdFromRun: 'heatershaker_id', + }) const { result } = renderHook(() => useLatchControls(mockHeaterShaker), { wrapper, }) diff --git a/app/src/organisms/Devices/ModuleCard/__tests__/useModuleIdFromRun.test.tsx b/app/src/organisms/Devices/ModuleCard/__tests__/useModuleIdFromRun.test.tsx new file mode 100644 index 00000000000..e14748d28a0 --- /dev/null +++ b/app/src/organisms/Devices/ModuleCard/__tests__/useModuleIdFromRun.test.tsx @@ -0,0 +1,177 @@ +import * as React from 'react' +import { Provider } from 'react-redux' +import { createStore } from 'redux' +import { renderHook } from '@testing-library/react-hooks' +import { useAttachedModules, useProtocolDetailsForRun } from '../../hooks' +import { useModuleIdFromRun } from '../useModuleIdFromRun' +import { mockMagneticModule } from '../../../../redux/modules/__fixtures__' + +import type { Store } from 'redux' +import type { State } from '../../../../redux/types' +import { MagneticModule } from '../../../../redux/modules/types' + +jest.mock('../../hooks') + +const mockUseProtocolDetailsForRun = useProtocolDetailsForRun as jest.MockedFunction< + typeof useProtocolDetailsForRun +> +const mockUseAttachedModules = useAttachedModules as jest.MockedFunction< + typeof useAttachedModules +> + +const RUN_ID = '1' + +const mockMagneticModule2: MagneticModule = { + id: 'magdeck_id', + moduleModel: 'magneticModuleV1', + moduleType: 'magneticModuleType', + serialNumber: 'abc123', + hardwareRevision: 'mag_deck_v4.0', + firmwareVersion: 'v2.0.0', + hasAvailableUpdate: true, + data: { + engaged: false, + height: 42, + status: 'disengaged', + }, + usbPort: { path: '/dev/ot_module_magdeck0', port: 1, hub: null }, +} + +describe('useModuleIdFromRun', () => { + const store: Store = createStore(jest.fn(), {}) + + beforeEach(() => { + store.dispatch = jest.fn() + mockUseProtocolDetailsForRun.mockReturnValue({ + protocolData: { + commands: [ + { + id: '3be3bc12-6e05-498a-ace7-5a7fda59d4d7', + createdAt: '2022-05-25T17:13:52.004179+00:00', + commandType: 'loadModule', + key: '3be3bc12-6e05-498a-ace7-5a7fda59d4d7', + status: 'succeeded', + params: { + model: 'magneticModuleV1', + location: { + slotName: '3', + }, + moduleId: 'magneticModuleId_1', + }, + result: { + moduleId: 'magneticModuleId_1', + definition: { + otSharedSchema: 'module/schemas/2', + moduleType: 'magneticModuleType', + model: 'magneticModuleV1', + labwareOffset: { + x: 0.125, + y: -0.125, + z: 82.25, + }, + dimensions: { + bareOverallHeight: 110.152, + overLabwareHeight: 4.052, + }, + calibrationPoint: { + x: 124.875, + y: 2.75, + z: 82.25, + }, + displayName: 'Magnetic Module GEN1', + quirks: [], + slotTransforms: {}, + compatibleWith: [], + }, + model: 'magneticModuleV1', + serialNumber: + 'fake-serial-number-0c3bd51c-f519-43a3-a5ae-7cdc01b8e57b', + }, + startedAt: '2022-05-25T17:13:52.006056+00:00', + completedAt: '2022-05-25T17:13:52.006701+00:00', + }, + { + id: '3be3bc12-6e05-498a-ace7-abcdefgh', + createdAt: '2022-05-25T17:13:52.004179+00:00', + commandType: 'loadModule', + key: '3be3bc12-6e05-498a-ace7-abcdefgh', + status: 'succeeded', + params: { + model: 'magneticModuleV1', + location: { + slotName: '6', + }, + moduleId: 'magneticModuleId_2', + }, + result: { + moduleId: 'magneticModuleId_2', + definition: { + otSharedSchema: 'module/schemas/2', + moduleType: 'magneticModuleType', + model: 'magneticModuleV1', + labwareOffset: { + x: 0.125, + y: -0.125, + z: 82.25, + }, + dimensions: { + bareOverallHeight: 110.152, + overLabwareHeight: 4.052, + }, + calibrationPoint: { + x: 124.875, + y: 2.75, + z: 82.25, + }, + displayName: 'Magnetic Module GEN1', + quirks: [], + slotTransforms: {}, + compatibleWith: [], + }, + model: 'magneticModuleV1', + serialNumber: + 'fake-serial-number-0c3bd51c-f519-43a3-a5ae-abcdefgh', + }, + startedAt: '2022-05-25T17:13:52.006056+00:00', + completedAt: '2022-05-25T17:13:52.006701+00:00', + }, + ], + }, + } as any) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + it('should return a module id from protocol analysis that matches the module attached', () => { + const wrapper: React.FunctionComponent<{}> = ({ children }) => ( + {children} + ) + mockUseAttachedModules.mockReturnValue([mockMagneticModule]) + const { result } = renderHook( + () => useModuleIdFromRun(mockMagneticModule, RUN_ID), + { wrapper } + ) + const moduleIdFromRun = result.current + + expect(moduleIdFromRun.moduleIdFromRun).toBe('magneticModuleId_1') + }) + + it('should return a the correct module id when there is multiples of a module', () => { + const wrapper: React.FunctionComponent<{}> = ({ children }) => ( + {children} + ) + mockUseAttachedModules.mockReturnValue([ + mockMagneticModule, + mockMagneticModule2, + ]) + const { result } = renderHook( + () => useModuleIdFromRun(mockMagneticModule2, RUN_ID), + { wrapper } + ) + const moduleIdFromRun = result.current + + expect(moduleIdFromRun.moduleIdFromRun).toBe('magneticModuleId_2') + }) +}) diff --git a/app/src/organisms/Devices/ModuleCard/hooks.tsx b/app/src/organisms/Devices/ModuleCard/hooks.tsx index 78f65cd532d..210dc983695 100644 --- a/app/src/organisms/Devices/ModuleCard/hooks.tsx +++ b/app/src/organisms/Devices/ModuleCard/hooks.tsx @@ -17,6 +17,7 @@ import { MenuItem } from '../../../atoms/MenuList/MenuItem' import { Tooltip } from '../../../atoms/Tooltip' import { useCurrentRunId } from '../../ProtocolUpload/hooks' import { useProtocolDetailsForRun } from '../hooks' +import { useModuleIdFromRun } from './useModuleIdFromRun' import type { HeaterShakerCloseLatchCreateCommand, @@ -54,6 +55,10 @@ export function useLatchControls( ): LatchControls { const { createLiveCommand } = useCreateLiveCommandMutation() const { createCommand } = useCreateCommandMutation() + const { moduleIdFromRun } = useModuleIdFromRun( + module, + runId != null ? runId : null + ) const isLatchClosed = module.moduleType === 'heaterShakerModuleType' && @@ -66,7 +71,7 @@ export function useLatchControls( commandType: isLatchClosed ? 'heaterShakerModule/openLatch' : 'heaterShakerModule/closeLatch', - params: { moduleId: module.id }, + params: { moduleId: runId != null ? moduleIdFromRun : module.id }, } const toggleLatch = (): void => { @@ -125,6 +130,7 @@ export function useModuleOverflowMenu( const { createCommand } = useCreateCommandMutation() const { toggleLatch, isLatchClosed } = useLatchControls(module, runId) const [targetProps, tooltipProps] = useHoverTooltip() + const { moduleIdFromRun } = useModuleIdFromRun(module, runId) const isLatchDisabled = module.moduleType === HEATERSHAKER_MODULE_TYPE && @@ -195,7 +201,7 @@ export function useModuleOverflowMenu( | TCDeactivateBlockCreateCommand | HeaterShakerStopShakeCreateCommand = { commandType: deactivateModuleCommandType, - params: { moduleId: module.id }, + params: { moduleId: runId != null ? moduleIdFromRun : module.id }, } if (runId != null) { createCommand({ diff --git a/app/src/organisms/Devices/ModuleCard/index.tsx b/app/src/organisms/Devices/ModuleCard/index.tsx index dfd9d261793..275ea019247 100644 --- a/app/src/organisms/Devices/ModuleCard/index.tsx +++ b/app/src/organisms/Devices/ModuleCard/index.tsx @@ -223,9 +223,15 @@ export const ModuleCard = (props: ModuleCardProps): JSX.Element | null => { width={'100%'} data-testid={`ModuleCard_${module.serialNumber}`} > - {showWizard && ( - setShowWizard(false)} /> - )} + {showWizard && + (runId != null ? ( + setShowWizard(false)} + runId={runId} + /> + ) : ( + setShowWizard(false)} /> + ))} {showSlideout && runId != null ? ( + item.moduleModel === module.moduleModel || + compatibleWith.includes(item.moduleModel) + ) + + const loadModuleCommands = protocolData?.commands.filter( + command => + command.commandType === 'loadModule' && + (command.params.model === module.moduleModel || + compatibleWith.includes(command.params.model)) + ) + + const moduleIndex = filteredModules.findIndex( + attachedModule => attachedModule.serialNumber === module.serialNumber + ) + + const moduleIdFromRun = loadModuleCommands?.[moduleIndex].result.moduleId + + return { moduleIdFromRun } +} diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx index 3b8e5eb7c0e..ef0c077e318 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunModuleControls.tsx @@ -1,3 +1,4 @@ +import * as React from 'react' import { DIRECTION_COLUMN, Flex, @@ -5,10 +6,16 @@ import { SPACING, WRAP, } from '@opentrons/components' -import * as React from 'react' -import { useModuleRenderInfoForProtocolById } from '../hooks' +import { useCreateCommandMutation } from '@opentrons/react-api-client' +import { + useModuleRenderInfoForProtocolById, + useProtocolDetailsForRun, +} from '../hooks' import { ModuleCard } from '../ModuleCard' +import type { LoadModuleRunTimeCommand } from '@opentrons/shared-data/protocol/types/schemaV6/command/setup' +import type { RunTimeCommand } from '@opentrons/shared-data' + interface ProtocolRunModuleControlsProps { robotName: string runId: string @@ -26,6 +33,41 @@ export const ProtocolRunModuleControls = ({ module => module.attachedModuleMatch != null ) + const { protocolData } = useProtocolDetailsForRun(runId) + const { createCommand } = useCreateCommandMutation() + + const loadCommands: LoadModuleRunTimeCommand[] = + protocolData !== null + ? protocolData?.commands.filter( + (command: RunTimeCommand): command is LoadModuleRunTimeCommand => + command.commandType === 'loadModule' + ) + : [] + + React.useEffect(() => { + if (protocolData != null) { + const setupLoadCommands = loadCommands.map(command => { + const commandWithModuleId = { + ...command, + params: { + ...command.params, + moduleId: command.result?.moduleId, + }, + } + return commandWithModuleId + }) + + setupLoadCommands.forEach(loadCommand => { + createCommand({ + runId: runId, + command: loadCommand, + }).catch((e: Error) => { + console.error(`error issuing command to robot: ${e.message}`) + }) + }) + } + }, []) + return ( ) : null} {showMultipleModulesModal ? ( diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx index 65da40d8eb4..df739697db8 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunModuleControls.test.tsx @@ -7,14 +7,22 @@ import { } from '@opentrons/components' import { ProtocolRunModuleControls } from '../ProtocolRunModuleControls' import { ModuleCard } from '../../ModuleCard' -import { useModuleRenderInfoForProtocolById } from '../../hooks' +import { + useModuleRenderInfoForProtocolById, + useProtocolDetailsForRun, +} from '../../hooks' import { mockMagneticModuleGen2, mockTemperatureModuleGen2, mockThermocycler, mockHeaterShaker, } from '../../../../redux/modules/__fixtures__' -import { ModuleModel, ModuleType } from '@opentrons/shared-data' +import fixtureAnalysis from '../../../../organisms/RunDetails/__fixtures__/analysis.json' +import { + ModuleModel, + ModuleType, + ProtocolAnalysisFile, +} from '@opentrons/shared-data' jest.mock('../../ModuleCard') jest.mock('../../hooks') @@ -23,6 +31,11 @@ const mockModuleCard = ModuleCard as jest.MockedFunction const mockUseModuleRenderInfoForProtocolById = useModuleRenderInfoForProtocolById as jest.MockedFunction< typeof useModuleRenderInfoForProtocolById > +const mockUseProtocolDetailsForRun = useProtocolDetailsForRun as jest.MockedFunction< + typeof useProtocolDetailsForRun +> + +const _fixtureAnalysis = (fixtureAnalysis as unknown) as ProtocolAnalysisFile const ROBOT_NAME = 'otie' const RUN_ID = 'test123' @@ -63,6 +76,14 @@ const render = ( } describe('ProtocolRunModuleControls', () => { + beforeEach(() => { + when(mockUseProtocolDetailsForRun).calledWith(RUN_ID).mockReturnValue({ + protocolData: _fixtureAnalysis, + displayName: 'mock display name', + protocolKey: 'fakeProtocolKey', + }) + }) + afterEach(() => { jest.resetAllMocks() resetAllWhenMocks() diff --git a/app/src/organisms/ProtocolSetup/RunSetupCard/ModuleSetup/HeaterShakerSetupWizard/HeaterShakerBanner.tsx b/app/src/organisms/ProtocolSetup/RunSetupCard/ModuleSetup/HeaterShakerSetupWizard/HeaterShakerBanner.tsx index f592e47111c..529c0566bd8 100644 --- a/app/src/organisms/ProtocolSetup/RunSetupCard/ModuleSetup/HeaterShakerSetupWizard/HeaterShakerBanner.tsx +++ b/app/src/organisms/ProtocolSetup/RunSetupCard/ModuleSetup/HeaterShakerSetupWizard/HeaterShakerBanner.tsx @@ -9,13 +9,14 @@ import { Banner, BannerItem } from '../Banner/Banner' interface HeaterShakerBannerProps { displayName: string modules: ModuleRenderInfoForProtocol[] + runId: string } export function HeaterShakerBanner( props: HeaterShakerBannerProps ): JSX.Element | null { const [showWizard, setShowWizard] = React.useState(false) - const { displayName, modules } = props + const { displayName, modules, runId } = props const { t } = useTranslation('heater_shaker') return ( @@ -25,6 +26,7 @@ export function HeaterShakerBanner( setShowWizard(false)} moduleFromProtocol={module} + runId={runId} /> )} {index > 0 && } diff --git a/app/src/organisms/ProtocolSetup/RunSetupCard/ModuleSetup/HeaterShakerSetupWizard/__tests__/HeaterShakerBanner.test.tsx b/app/src/organisms/ProtocolSetup/RunSetupCard/ModuleSetup/HeaterShakerSetupWizard/__tests__/HeaterShakerBanner.test.tsx index 5bebd290270..35c7c1fe450 100644 --- a/app/src/organisms/ProtocolSetup/RunSetupCard/ModuleSetup/HeaterShakerSetupWizard/__tests__/HeaterShakerBanner.test.tsx +++ b/app/src/organisms/ProtocolSetup/RunSetupCard/ModuleSetup/HeaterShakerSetupWizard/__tests__/HeaterShakerBanner.test.tsx @@ -63,6 +63,7 @@ describe('HeaterShakerBanner', () => { let props: React.ComponentProps beforeEach(() => { props = { + runId: '1', displayName: 'HeaterShakerV1', modules: [HEATER_SHAKER_PROTOCOL_MODULE_INFO], } @@ -83,6 +84,7 @@ describe('HeaterShakerBanner', () => { it('should not render heater shaker wizard button if no heater shaker is present', () => { props = { + runId: '1', displayName: 'HeaterShakerV1', modules: [], } @@ -94,6 +96,7 @@ describe('HeaterShakerBanner', () => { it('should render two heater shaker banner items when there are two heater shakers in the protocol', () => { props = { + runId: '1', displayName: 'HeaterShakerV1', modules: [ HEATER_SHAKER_PROTOCOL_MODULE_INFO,