From 3dffaef58c370c04da09dd1723d867675476ffcd Mon Sep 17 00:00:00 2001 From: Sakib Hossain Date: Fri, 27 May 2022 14:30:06 -0400 Subject: [PATCH] refactor(app): load modules to retrieve moduleId for run setup module controls (#10451) * refactor(app): load modules to retrieve moduleId for run setup module controls This PR loads the modules in the protocol and then retrieves the moduleId from the protocol analysis and feeds it to the corresponding createCommand for each module action. --- .../Devices/HeaterShakerWizard/TestShake.tsx | 35 +++- .../__tests__/TestShake.test.tsx | 23 ++- .../Devices/HeaterShakerWizard/index.tsx | 4 +- .../ModuleCard/HeaterShakerSlideout.tsx | 9 +- .../ModuleCard/MagneticModuleSlideout.tsx | 7 +- .../Devices/ModuleCard/ModuleOverflowMenu.tsx | 1 + .../ModuleCard/TemperatureModuleData.tsx | 2 +- .../ModuleCard/TemperatureModuleSlideout.tsx | 7 +- .../Devices/ModuleCard/TestShakeSlideout.tsx | 55 +++--- .../ModuleCard/ThermocyclerModuleSlideout.tsx | 9 +- .../__tests__/HeaterShakerSlideout.test.tsx | 8 + .../__tests__/MagneticModuleSlideout.test.tsx | 6 + .../ModuleCard/__tests__/ModuleCard.test.tsx | 27 ++- .../__tests__/ModuleOverflowMenu.test.tsx | 8 + .../TemperatureModuleSlideout.test.tsx | 6 + .../__tests__/TestShakeSlideout.test.tsx | 10 +- .../ThermocyclerModuleSlideout.test.tsx | 8 + .../ModuleCard/__tests__/hooks.test.tsx | 8 + .../__tests__/useModuleIdFromRun.test.tsx | 177 ++++++++++++++++++ .../organisms/Devices/ModuleCard/hooks.tsx | 10 +- .../organisms/Devices/ModuleCard/index.tsx | 12 +- .../Devices/ModuleCard/useModuleIdFromRun.ts | 38 ++++ .../ProtocolRun/ProtocolRunModuleControls.tsx | 46 ++++- .../Devices/ProtocolRun/SetupModules.tsx | 1 + .../ProtocolRunModuleControls.test.tsx | 25 ++- .../HeaterShakerBanner.tsx | 4 +- .../__tests__/HeaterShakerBanner.test.tsx | 3 + 27 files changed, 493 insertions(+), 56 deletions(-) create mode 100644 app/src/organisms/Devices/ModuleCard/__tests__/useModuleIdFromRun.test.tsx create mode 100644 app/src/organisms/Devices/ModuleCard/useModuleIdFromRun.ts 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,