From 5415917f1fe7c9e4f00e9572ac5f9dbb4bdb474d Mon Sep 17 00:00:00 2001 From: Brian Arthur Cooper Date: Tue, 23 Apr 2024 17:32:12 -0400 Subject: [PATCH] fix(app): resolve module location conflicts through deck config during protocol setup on ODD (#14966) Resolve location conflicts in the on device display's protocol setup flow by updating deck configuration accordingly. Closes PLAT-287, Closes PLAT-291 --- .../localization/en/protocol_setup.json | 7 +- .../AddFixtureModal.tsx | 2 - .../SetupLabware/SetupLabwareMap.tsx | 16 +---- .../SetupLiquids/SetupLiquidsMap.tsx | 15 +--- .../__tests__/SetupLiquidsMap.test.tsx | 6 +- .../ChooseModuleToConfigureModal.tsx | 68 ++++++++++++++++--- .../LocationConflictModal.tsx | 23 ++++--- .../SetupModuleAndDeck/NotConfiguredModal.tsx | 2 +- .../SetupModuleAndDeck/SetupFixtureList.tsx | 8 ++- .../SetupModuleAndDeck/SetupModulesList.tsx | 4 ++ .../SetupModuleAndDeck/SetupModulesMap.tsx | 24 +++++-- .../__tests__/LocationConflictModal.test.tsx | 22 ++++-- .../__tests__/NotConfiguredModal.test.tsx | 2 +- .../__tests__/SetupFixtureList.test.tsx | 3 + .../ProtocolRun/SetupModuleAndDeck/index.tsx | 1 + .../ModuleWizardFlows/SelectLocation.tsx | 7 ++ .../__tests__/ProtocolSetupLabware.test.tsx | 17 ++++- .../organisms/ProtocolSetupLabware/index.tsx | 12 +++- .../FixtureTable.tsx | 37 +++++----- .../ModuleTable.tsx | 58 +++++----------- .../__tests__/FixtureTable.test.tsx | 11 ++- .../ProtocolSetupModulesAndDeck.test.tsx | 6 +- .../__tests__/utils.test.tsx | 42 ++++++++---- .../ProtocolSetupModulesAndDeck/index.tsx | 64 ++++++++++++----- .../ProtocolSetupModulesAndDeck/utils.ts | 32 +++++++-- .../__tests__/ProtocolSetup.test.tsx | 4 +- app/src/pages/ProtocolSetup/index.tsx | 2 +- .../__tests__/useNotifyService.test.ts | 1 + .../DeckConfigurator/HeaterShakerFixture.tsx | 10 +-- .../DeckConfigurator/MagneticBlockFixture.tsx | 10 +-- .../StagingAreaConfigFixture.tsx | 10 +-- .../TemperatureModuleFixture.tsx | 11 +-- .../DeckConfigurator/ThermocyclerFixture.tsx | 10 +-- .../TrashBinConfigFixture.tsx | 10 +-- .../WasteChuteConfigFixture.tsx | 10 +-- .../DeckConfigurator/constants.ts | 21 ++++++ .../hardware-sim/DeckConfigurator/index.tsx | 11 ++- setup-vitest.ts | 1 + 38 files changed, 384 insertions(+), 216 deletions(-) diff --git a/app/src/assets/localization/en/protocol_setup.json b/app/src/assets/localization/en/protocol_setup.json index 74fbf93d3c2..360fbd2cc4e 100644 --- a/app/src/assets/localization/en/protocol_setup.json +++ b/app/src/assets/localization/en/protocol_setup.json @@ -3,10 +3,11 @@ "action_needed": "Action needed", "adapter_slot_location_module": "Slot {{slotName}}, {{adapterName}} on {{moduleName}}", "adapter_slot_location": "Slot {{slotName}}, {{adapterName}}", - "add_fixture_to_deck": "Add this fixture to your deck configuration. It will be referenced during protocol analysis.", "add_fixture": "Add {{fixtureName}} to deck configuration", "additional_labware": "{{count}} additional labware", "additional_off_deck_labware": "Additional Off-Deck Labware", + "add_this_deck_hardware": "Add this deck hardware to your deck configuration. It will be referenced during protocol analysis.", + "add_to_slot": "Add to slot {{slotName}}", "attach_gripper_failure_reason": "Attach the required gripper to continue", "attach_gripper": "attach gripper", "attach_module": "Attach module before calibrating", @@ -38,6 +39,7 @@ "calibration_status": "calibration status", "calibration": "Calibration", "cancel_and_restart_to_edit": "Cancel the run and restart setup to edit", + "cancel_protocol_and_edit_deck_config": "Cancel protocol and edit deck configuration", "choose_enum": "Choose {{displayName}}", "closing": "Closing...", "complete_setup_before_proceeding": "complete setup before continuing run", @@ -142,7 +144,6 @@ "module_setup_step_title": "Modules", "module_slot_location": "Slot {{slotName}}, {{moduleName}}", "module": "Module", - "modules_and_deck": "Modules & deck", "modules_connected_plural": "{{count}} modules attached", "modules_connected": "{{count}} module attached", "modules_setup_step_title": "Module Setup", @@ -249,12 +250,14 @@ "slot_number": "Slot Number", "status": "Status", "step": "STEP {{index}}", + "there_are_no_unconfigured_modules": "There are no un-configured {{module}} connected to the robot. Plug one in or remove an existing {{module}}, move it to the right place, and update the deck configuration.", "tip_length_cal_description_bullet": "Perform Tip Length Calibration for each new tip type used on a pipette.", "tip_length_cal_description": "This measures the Z distance between the bottom of the tip and the pipetteā€™s nozzle. If you redo the tip length calibration for the tip you used to calibrate a pipette, you will also have to redo that Pipette Offset Calibration.", "tip_length_cal_title": "Tip Length Calibration", "tip_length_calibration": "tip length calibration", "total_vol": "total volume", "update_deck": "Update deck", + "update_deck_config": "Update deck configuration", "updated": "Updated", "usb_connected_no_port_info": "USB Port Connected", "usb_port_connected": "USB Port {{port}}", diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx index 99f2328b1e4..91fb38c4cf2 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx @@ -59,8 +59,6 @@ import type { import type { ModalHeaderBaseProps } from '../../molecules/Modal/types' import type { LegacyModalProps } from '../../molecules/LegacyModal' -// type CutoutContents = Omit - interface AddFixtureModalProps { cutoutId: CutoutId setShowAddFixtureModal: (showAddFixtureModal: boolean) => void diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx index 0505cf0c921..533f134590d 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/SetupLabwareMap.tsx @@ -17,8 +17,6 @@ import { } from '@opentrons/shared-data' import { getLabwareSetupItemGroups } from '../../../../pages/Protocols/utils' -import { getAttachedProtocolModuleMatches } from '../../../ProtocolSetupModulesAndDeck/utils' -import { useAttachedModules } from '../../hooks' import { LabwareInfoOverlay } from '../LabwareInfoOverlay' import { getLabwareRenderInfo } from '../utils/getLabwareRenderInfo' import { getProtocolModulesInfo } from '../utils/getProtocolModulesInfo' @@ -30,8 +28,6 @@ import type { ProtocolAnalysisOutput, } from '@opentrons/shared-data' -const ATTACHED_MODULE_POLL_MS = 5000 - interface SetupLabwareMapProps { runId: string protocolAnalysis: CompletedProtocolAnalysis | ProtocolAnalysisOutput | null @@ -41,11 +37,6 @@ export function SetupLabwareMap({ runId, protocolAnalysis, }: SetupLabwareMapProps): JSX.Element | null { - const attachedModules = - useAttachedModules({ - refetchInterval: ATTACHED_MODULE_POLL_MS, - }) ?? [] - // early return null if no protocol analysis if (protocolAnalysis == null) return null @@ -56,16 +47,11 @@ export function SetupLabwareMap({ const protocolModulesInfo = getProtocolModulesInfo(protocolAnalysis, deckDef) - const attachedProtocolModuleMatches = getAttachedProtocolModuleMatches( - attachedModules, - protocolModulesInfo - ) - const initialLoadedLabwareByAdapter = parseInitialLoadedLabwareByAdapter( commands ) - const modulesOnDeck = attachedProtocolModuleMatches.map(module => { + const modulesOnDeck = protocolModulesInfo.map(module => { const labwareInAdapterInMod = module.nestedLabwareId != null ? initialLoadedLabwareByAdapter[module.nestedLabwareId] diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx index 0519b557065..352bcf021e8 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/SetupLiquidsMap.tsx @@ -21,13 +21,11 @@ import { THERMOCYCLER_MODULE_V1, } from '@opentrons/shared-data' -import { useAttachedModules } from '../../hooks' import { LabwareInfoOverlay } from '../LabwareInfoOverlay' import { LiquidsLabwareDetailsModal } from './LiquidsLabwareDetailsModal' import { getWellFillFromLabwareId } from './utils' import { getLabwareRenderInfo } from '../utils/getLabwareRenderInfo' import { getStandardDeckViewLayerBlockList } from '../utils/getStandardDeckViewLayerBlockList' -import { getAttachedProtocolModuleMatches } from '../../../ProtocolSetupModulesAndDeck/utils' import { getProtocolModulesInfo } from '../utils/getProtocolModulesInfo' import type { @@ -35,8 +33,6 @@ import type { ProtocolAnalysisOutput, } from '@opentrons/shared-data' -const ATTACHED_MODULE_POLL_MS = 5000 - interface SetupLiquidsMapProps { runId: string protocolAnalysis: CompletedProtocolAnalysis | ProtocolAnalysisOutput | null @@ -50,10 +46,6 @@ export function SetupLiquidsMap( const [liquidDetailsLabwareId, setLiquidDetailsLabwareId] = React.useState< string | null >(null) - const attachedModules = - useAttachedModules({ - refetchInterval: ATTACHED_MODULE_POLL_MS, - }) ?? [] if (protocolAnalysis == null) return null @@ -75,12 +67,7 @@ export function SetupLiquidsMap( const protocolModulesInfo = getProtocolModulesInfo(protocolAnalysis, deckDef) - const attachedProtocolModuleMatches = getAttachedProtocolModuleMatches( - attachedModules, - protocolModulesInfo - ) - - const modulesOnDeck = attachedProtocolModuleMatches.map(module => { + const modulesOnDeck = protocolModulesInfo.map(module => { const labwareInAdapterInMod = module.nestedLabwareId != null ? initialLoadedLabwareByAdapter[module.nestedLabwareId] diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx index 81e5a005143..fa9e45852b5 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupLiquids/__tests__/SetupLiquidsMap.test.tsx @@ -212,7 +212,8 @@ describe('SetupLiquidsMap', () => { when(vi.mocked(getAttachedProtocolModuleMatches)) .calledWith( mockFetchModulesSuccessActionPayloadModules, - mockProtocolModuleInfo + mockProtocolModuleInfo, + [] ) .thenReturn([ { @@ -299,7 +300,8 @@ describe('SetupLiquidsMap', () => { when(vi.mocked(getAttachedProtocolModuleMatches)) .calledWith( mockFetchModulesSuccessActionPayloadModules, - mockProtocolModuleInfo + mockProtocolModuleInfo, + [] ) .thenReturn([ { diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/ChooseModuleToConfigureModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/ChooseModuleToConfigureModal.tsx index 59bc0b6e52e..6a6264b80c7 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/ChooseModuleToConfigureModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/ChooseModuleToConfigureModal.tsx @@ -1,17 +1,17 @@ import * as React from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' +import { useHistory } from 'react-router-dom' import { useDeckConfigurationQuery, useModulesQuery, } from '@opentrons/react-api-client' import { ALIGN_CENTER, - COLORS, DIRECTION_COLUMN, DIRECTION_ROW, Flex, - Icon, + PrimaryButton, SPACING, StyledText, TYPOGRAPHY, @@ -20,14 +20,19 @@ import { getFixtureDisplayName, getCutoutFixturesForModuleModel, MAGNETIC_BLOCK_V1, + getModuleDisplayName, } from '@opentrons/shared-data' import { getTopPortalEl } from '../../../../App/portal' import { LegacyModal } from '../../../../molecules/LegacyModal' import { Modal } from '../../../../molecules/Modal' +import { FixtureOption } from '../../../DeviceDetailsDeckConfiguration/AddFixtureModal' + +import { SmallButton } from '../../../../atoms/buttons' +import { useCloseCurrentRun } from '../../../ProtocolUpload/hooks' import type { ModuleModel, DeckDefinition } from '@opentrons/shared-data' -import { FixtureOption } from '../../../DeviceDetailsDeckConfiguration/AddFixtureModal' +const EQUIPMENT_POLL_MS = 5000 interface ModuleFixtureOption { moduleModel: ModuleModel usbPort?: number @@ -39,6 +44,8 @@ interface ChooseModuleToConfigureModalProps { deckDef: DeckDefinition isOnDevice: boolean requiredModuleModel: ModuleModel + robotName: string + displaySlotName: string } export const ChooseModuleToConfigureModal = ( @@ -50,9 +57,14 @@ export const ChooseModuleToConfigureModal = ( deckDef, requiredModuleModel, isOnDevice, + robotName, + displaySlotName, } = props const { t } = useTranslation(['protocol_setup', 'shared']) - const attachedModules = useModulesQuery().data?.data ?? [] + const history = useHistory() + const { closeCurrentRun } = useCloseCurrentRun() + const attachedModules = + useModulesQuery({ refetchInterval: EQUIPMENT_POLL_MS })?.data?.data ?? [] const deckConfig = useDeckConfigurationQuery()?.data ?? [] const unconfiguredModuleMatches = attachedModules.filter( @@ -94,17 +106,52 @@ export const ChooseModuleToConfigureModal = ( ) } ) + const handleCancelRun = (): void => { + closeCurrentRun() + } + const handleNavigateToDeviceDetails = (): void => { + history.push(`/devices/${robotName}`) + } + const emptyState = ( + + + {t('there_are_no_unconfigured_modules', { + module: getModuleDisplayName(requiredModuleModel), + })} + + {isOnDevice ? ( + + ) : ( + + {t('update_deck_config')} + + )} + + ) + + const contents = + fixtureOptions.length > 0 ? ( + + {t('add_this_deck_hardware')} + + {fixtureOptions} + + + ) : ( + emptyState + ) return createPortal( isOnDevice ? ( @@ -114,7 +161,7 @@ export const ChooseModuleToConfigureModal = ( paddingTop={SPACING.spacing8} gridGap={SPACING.spacing8} > - {fixtureOptions} + {contents} @@ -127,9 +174,8 @@ export const ChooseModuleToConfigureModal = ( gridGap={SPACING.spacing10} alignItems={ALIGN_CENTER} > - - {t('deck_conflict')} + {t('add_to_slot', { slotName: displaySlotName })} } @@ -143,7 +189,7 @@ export const ChooseModuleToConfigureModal = ( paddingTop={SPACING.spacing8} gridGap={SPACING.spacing8} > - {fixtureOptions} + {contents} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx index c696b4ecbdf..1783bd31754 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/LocationConflictModal.tsx @@ -48,6 +48,7 @@ interface LocationConflictModalProps { onCloseClick: () => void cutoutId: CutoutId deckDef: DeckDefinition + robotName: string missingLabwareDisplayName?: string | null requiredFixtureId?: CutoutFixtureId requiredModule?: ModuleModel @@ -60,6 +61,7 @@ export const LocationConflictModal = ( const { onCloseClick, cutoutId, + robotName, missingLabwareDisplayName, requiredFixtureId, requiredModule, @@ -153,7 +155,11 @@ export const LocationConflictModal = ( protocolSpecifiesDisplayName = getModuleDisplayName(requiredModule) } - if (showModuleSelect && requiredModule) { + const displaySlotName = isThermocycler + ? 'A1 + B1' + : getCutoutDisplayName(cutoutId) + + if (showModuleSelect && requiredModule != null) { return createPortal( , getTopPortalEl() ) } + return createPortal( isOnDevice ? ( - {t('slot_location', { - slotName: isThermocycler - ? 'A1 + B1' - : getCutoutDisplayName(cutoutId), - })} + {t('slot_location', { slotName: displaySlotName })} - {t('slot_location', { - slotName: isThermocycler - ? 'A1 + B1' - : getCutoutDisplayName(cutoutId), - })} + {t('slot_location', { slotName: displaySlotName })} - {t('add_fixture_to_deck')} + {t('add_this_deck_hardware')} { - const { deckConfigCompatibility } = props + const { deckConfigCompatibility, robotName } = props const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) return ( <> @@ -53,6 +55,7 @@ export const SetupFixtureList = (props: SetupFixtureListProps): JSX.Element => { ) @@ -63,6 +66,7 @@ export const SetupFixtureList = (props: SetupFixtureListProps): JSX.Element => { interface FixtureListItemProps extends CutoutConfigAndCompatibility { deckDef: DeckDefinition + robotName: string } export function FixtureListItem({ @@ -71,6 +75,7 @@ export function FixtureListItem({ compatibleCutoutFixtureIds, missingLabwareDisplayName, deckDef, + robotName, }: FixtureListItemProps): JSX.Element { const { t } = useTranslation('protocol_setup') @@ -135,6 +140,7 @@ export function FixtureListItem({ deckDef={deckDef} missingLabwareDisplayName={missingLabwareDisplayName} requiredFixtureId={compatibleCutoutFixtureIds[0]} + robotName={robotName} /> ) : null} {showSetupInstructionsModal ? ( diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx index cf258c2bc00..4024fcac296 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesList.tsx @@ -127,6 +127,7 @@ export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => { calibrationStatus={calibrationStatus} conflictedFixture={conflictedFixture} deckDef={deckDef} + robotName={robotName} /> ) } @@ -145,6 +146,7 @@ interface ModulesListItemProps { calibrationStatus: ProtocolCalibrationStatus deckDef: DeckDefinition conflictedFixture: CutoutConfig | null + robotName: string } export function ModulesListItem({ @@ -157,6 +159,7 @@ export function ModulesListItem({ calibrationStatus, conflictedFixture, deckDef, + robotName, }: ModulesListItemProps): JSX.Element { const { t } = useTranslation(['protocol_setup', 'module_wizard_flows']) const moduleConnectionStatus = @@ -286,6 +289,7 @@ export function ModulesListItem({ cutoutId={cutoutIdForSlotName} requiredModule={moduleModel} deckDef={deckDef} + robotName={robotName} /> ) : null} {showModuleWizard && attachedModuleMatch != null ? ( diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx index a5c6ab1ecbd..76aea98a8cc 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/SetupModulesMap.tsx @@ -19,8 +19,10 @@ import { ModuleInfo } from '../../ModuleInfo' import { useAttachedModules, useStoredProtocolAnalysis } from '../../hooks' import { getProtocolModulesInfo } from '../utils/getProtocolModulesInfo' import { getStandardDeckViewLayerBlockList } from '../utils/getStandardDeckViewLayerBlockList' +import { useDeckConfigurationQuery } from '@opentrons/react-api-client' const ATTACHED_MODULE_POLL_MS = 5000 +const DECK_CONFIG_POLL_MS = 5000 interface SetupModulesMapProps { runId: string @@ -33,7 +35,9 @@ export const SetupModulesMap = ({ const robotProtocolAnalysis = useMostRecentCompletedAnalysis(runId) const storedProtocolAnalysis = useStoredProtocolAnalysis(runId) const protocolAnalysis = robotProtocolAnalysis ?? storedProtocolAnalysis - + const { data: actualDeckConfig = [] } = useDeckConfigurationQuery({ + refetchInterval: DECK_CONFIG_POLL_MS, + }) const attachedModules = useAttachedModules({ refetchInterval: ATTACHED_MODULE_POLL_MS, @@ -44,11 +48,13 @@ export const SetupModulesMap = ({ const robotType = protocolAnalysis.robotType ?? FLEX_ROBOT_TYPE const deckDef = getDeckDefFromRobotType(robotType) + const protocolModulesInfo = getProtocolModulesInfo(protocolAnalysis, deckDef) const attachedProtocolModuleMatches = getAttachedProtocolModuleMatches( attachedModules, - protocolModulesInfo + protocolModulesInfo, + actualDeckConfig ) const modulesOnDeck = attachedProtocolModuleMatches.map(module => ({ @@ -64,7 +70,9 @@ export const SetupModulesMap = ({ ), })) - const deckConfig = getSimplestDeckConfigForProtocol(protocolAnalysis) + const simplestProtocolDeckConfig = getSimplestDeckConfigForProtocol( + protocolAnalysis + ) return ( ({ - cutoutId, - cutoutFixtureId, - }))} + deckConfig={simplestProtocolDeckConfig.map( + ({ cutoutId, cutoutFixtureId }) => ({ + cutoutId, + cutoutFixtureId, + }) + )} deckLayerBlocklist={getStandardDeckViewLayerBlockList(robotType)} robotType={robotType} labwareOnDeck={[]} diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx index 2557f0ba001..d72a00a9f5f 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/LocationConflictModal.test.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import { UseQueryResult } from 'react-query' +import { MemoryRouter } from 'react-router-dom' import { screen, fireEvent } from '@testing-library/react' import '@testing-library/jest-dom/vitest' import { describe, it, beforeEach, vi, afterEach, expect } from 'vitest' @@ -16,12 +17,14 @@ import { useUpdateDeckConfigurationMutation, } from '@opentrons/react-api-client' import { i18n } from '../../../../../i18n' +import { mockHeaterShaker } from '../../../../../redux/modules/__fixtures__' +import { useCloseCurrentRun } from '../../../../ProtocolUpload/hooks' import { LocationConflictModal } from '../LocationConflictModal' import type { DeckConfiguration } from '@opentrons/shared-data' -import { mockHeaterShaker } from '../../../../../redux/modules/__fixtures__' vi.mock('@opentrons/react-api-client') +vi.mock('../../../../ProtocolUpload/hooks') const mockFixture = { cutoutId: 'cutoutB3', @@ -29,9 +32,14 @@ const mockFixture = { } const render = (props: React.ComponentProps) => { - return renderWithProviders(, { - i18nInstance: i18n, - })[0] + return renderWithProviders( + + + , + { + i18nInstance: i18n, + } + )[0] } describe('LocationConflictModal', () => { @@ -43,7 +51,11 @@ describe('LocationConflictModal', () => { cutoutId: 'cutoutB3', requiredFixtureId: TRASH_BIN_ADAPTER_FIXTURE, deckDef: ot3StandardDeckV5 as any, + robotName: 'otie', } + vi.mocked(useCloseCurrentRun).mockReturnValue({ + closeCurrentRun: vi.fn(), + } as any) vi.mocked(useModulesQuery).mockReturnValue({ data: { data: [] } } as any) vi.mocked(useDeckConfigurationQuery).mockReturnValue({ data: [mockFixture], @@ -77,6 +89,7 @@ describe('LocationConflictModal', () => { cutoutId: 'cutoutB3', requiredModule: 'heaterShakerModuleV1', deckDef: ot3StandardDeckV5 as any, + robotName: 'otie', } render(props) screen.getByText('Protocol specifies') @@ -103,6 +116,7 @@ describe('LocationConflictModal', () => { requiredFixtureId: SINGLE_RIGHT_SLOT_FIXTURE, missingLabwareDisplayName: 'a tiprack', deckDef: ot3StandardDeckV5 as any, + robotName: 'otie', } render(props) screen.getByText('Deck location conflict') diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx index f2adbfe736d..b124a000f53 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/NotConfiguredModal.test.tsx @@ -41,7 +41,7 @@ describe('NotConfiguredModal', () => { const { getByText, getByRole } = render(props) getByText('Add Trash bin to deck configuration') getByText( - 'Add this fixture to your deck configuration. It will be referenced during protocol analysis.' + 'Add this deck hardware to your deck configuration. It will be referenced during protocol analysis.' ) getByText('Trash bin') fireEvent.click(getByRole('button', { name: 'Add' })) diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx index 2aba1928899..3571eef7b31 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupFixtureList.test.tsx @@ -69,6 +69,7 @@ describe('SetupFixtureList', () => { beforeEach(() => { props = { deckConfigCompatibility: mockDeckConfigCompatibility, + robotName: 'otie', } vi.mocked(LocationConflictModal).mockReturnValue(
mock location conflict modal
@@ -100,6 +101,7 @@ describe('SetupFixtureList', () => { it('should render the headers and a fixture with conflicted status', () => { props = { deckConfigCompatibility: mockConflictDeckConfigCompatibility, + robotName: 'otie', } render(props) screen.getByText('Location conflict') @@ -110,6 +112,7 @@ describe('SetupFixtureList', () => { it('should render the headers and a fixture with not configured status and button', () => { props = { deckConfigCompatibility: mockNotConfiguredDeckConfigCompatibility, + robotName: 'otie', } render(props) screen.getByText('Not configured') diff --git a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx index f1e06c2471a..0de1a163356 100644 --- a/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx +++ b/app/src/organisms/Devices/ProtocolRun/SetupModuleAndDeck/index.tsx @@ -121,6 +121,7 @@ export const SetupModuleAndDeck = ({ {requiredDeckConfigCompatibility.length > 0 ? ( ) : null}
diff --git a/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx b/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx index af0301549d0..2c78ecfb26b 100644 --- a/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx +++ b/app/src/organisms/ModuleWizardFlows/SelectLocation.tsx @@ -174,6 +174,13 @@ export const SelectLocation = ( handleClickAdd={handleAddFixture} handleClickRemove={handleRemoveFixture} editableCutoutIds={editableCutoutIds} + selectedCutoutId={ + deckConfig.find( + ({ cutoutId, opentronsModuleSerialNumber }) => + Object.keys(configuredFixtureIdByCutoutId).includes(cutoutId) && + attachedModule.serialNumber === opentronsModuleSerialNumber + )?.cutoutId + } height="250px" /> } diff --git a/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx b/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx index b44a314983a..18f02ec5e5c 100644 --- a/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx +++ b/app/src/organisms/ProtocolSetupLabware/__tests__/ProtocolSetupLabware.test.tsx @@ -7,8 +7,12 @@ import { describe, it, vi, beforeEach, afterEach, expect } from 'vitest' import { useCreateLiveCommandMutation, useModulesQuery, + useDeckConfigurationQuery, } from '@opentrons/react-api-client' -import { ot3StandardDeckV5 as ot3StandardDeckDef } from '@opentrons/shared-data' +import { + HEATERSHAKER_MODULE_V1_FIXTURE, + ot3StandardDeckV5 as ot3StandardDeckDef, +} from '@opentrons/shared-data' import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' @@ -33,6 +37,7 @@ vi.mock('@opentrons/react-api-client', async importOriginal => { ...actual, useCreateLiveCommandMutation: vi.fn(), useModulesQuery: vi.fn(), + useDeckConfigurationQuery: vi.fn(), } }) @@ -76,6 +81,16 @@ describe('ProtocolSetupLabware', () => { vi.mocked(useCreateLiveCommandMutation).mockReturnValue({ createLiveCommand: mockCreateLiveCommand, } as any) + vi.mocked(useDeckConfigurationQuery).mockReturnValue({ + data: [ + { + cutoutId: 'cutoutB1', + cutoutFixtureId: HEATERSHAKER_MODULE_V1_FIXTURE, + opentronsModuleSerialNumber: + mockUseModulesQueryClosed.data.data[0].serialNumber, + }, + ], + } as any) }) afterEach(() => { vi.clearAllMocks() diff --git a/app/src/organisms/ProtocolSetupLabware/index.tsx b/app/src/organisms/ProtocolSetupLabware/index.tsx index 3bc1ad62c56..33e44ab7534 100644 --- a/app/src/organisms/ProtocolSetupLabware/index.tsx +++ b/app/src/organisms/ProtocolSetupLabware/index.tsx @@ -34,6 +34,7 @@ import { import { parseInitialLoadedLabwareByAdapter } from '@opentrons/api-client' import { useCreateLiveCommandMutation, + useDeckConfigurationQuery, useModulesQuery, } from '@opentrons/react-api-client' @@ -64,7 +65,8 @@ import type { SetupScreens } from '../../pages/ProtocolSetup' import type { AttachedProtocolModuleMatch } from '../ProtocolSetupModulesAndDeck/utils' import { LabwareMapViewModal } from './LabwareMapViewModal' -const MODULE_REFETCH_INTERVAL = 5000 +const MODULE_REFETCH_INTERVAL_MS = 5000 +const DECK_CONFIG_POLL_MS = 5000 const LabwareThumbnail = styled.svg` transform: scale(1, -1); @@ -97,11 +99,14 @@ export function ProtocolSetupLabware({ const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) + const { data: deckConfig = [] } = useDeckConfigurationQuery({ + refetchInterval: DECK_CONFIG_POLL_MS, + }) const { offDeckItems, onDeckItems } = getLabwareSetupItemGroups( mostRecentAnalysis?.commands ?? [] ) const moduleQuery = useModulesQuery({ - refetchInterval: MODULE_REFETCH_INTERVAL, + refetchInterval: MODULE_REFETCH_INTERVAL_MS, }) const attachedModules = moduleQuery?.data?.data ?? [] const protocolModulesInfo = @@ -111,7 +116,8 @@ export function ProtocolSetupLabware({ const attachedProtocolModuleMatches = getAttachedProtocolModuleMatches( attachedModules, - protocolModulesInfo + protocolModulesInfo, + deckConfig ) const initialLoadedLabwareByAdapter = parseInitialLoadedLabwareByAdapter( mostRecentAnalysis?.commands ?? [] diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx index 23d490af287..82c9d9670f3 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/FixtureTable.tsx @@ -5,7 +5,6 @@ import { BORDERS, COLORS, Chip, - DIRECTION_COLUMN, DIRECTION_ROW, Flex, JUSTIFY_SPACE_BETWEEN, @@ -15,6 +14,7 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { + FLEX_MODULE_ADDRESSABLE_AREAS, getCutoutDisplayName, getDeckDefFromRobotType, getFixtureDisplayName, @@ -36,6 +36,8 @@ import type { } from '@opentrons/shared-data' import type { SetupScreens } from '../../pages/ProtocolSetup' import type { CutoutConfigAndCompatibility } from '../../resources/deck_configuration/hooks' +import { useSelector } from 'react-redux' +import { getLocalRobot } from '../../redux/discovery' interface FixtureTableProps { robotType: RobotType @@ -45,6 +47,11 @@ interface FixtureTableProps { setProvidedFixtureOptions: (providedFixtureOptions: CutoutFixtureId[]) => void } +/** + * Table of all "non-module" fixtures e.g. staging slot, waste chute, trash bin... + * @param props + * @returns JSX.Element + */ export function FixtureTable({ robotType, mostRecentAnalysis, @@ -52,8 +59,6 @@ export function FixtureTable({ setCutoutId, setProvidedFixtureOptions, }: FixtureTableProps): JSX.Element | null { - const { t } = useTranslation('protocol_setup') - const requiredFixtureDetails = getSimplestDeckConfigForProtocol( mostRecentAnalysis ) @@ -62,6 +67,8 @@ export function FixtureTable({ mostRecentAnalysis ) const deckDef = getDeckDefFromRobotType(robotType) + const localRobot = useSelector(getLocalRobot) + const robotName = localRobot?.name != null ? localRobot.name : '' const requiredDeckConfigCompatibility = getRequiredDeckConfig( deckConfigCompatibility @@ -77,21 +84,11 @@ export function FixtureTable({ ) return sortedDeckConfigCompatibility.length > 0 ? ( - - - {t('fixture')} - {t('location')} - {t('status')} - + <> {sortedDeckConfigCompatibility.map((fixtureCompatibility, index) => { - return ( + return fixtureCompatibility.requiredAddressableAreas.some(raa => + FLEX_MODULE_ADDRESSABLE_AREAS.includes(raa) + ) ? null : ( ) })} - + ) : null } @@ -113,6 +111,7 @@ interface FixtureTableItemProps extends CutoutConfigAndCompatibility { setCutoutId: (cutoutId: CutoutId) => void setProvidedFixtureOptions: (providedFixtureOptions: CutoutFixtureId[]) => void deckDef: DeckDefinition + robotName: string } function FixtureTableItem({ @@ -125,6 +124,7 @@ function FixtureTableItem({ setCutoutId, setProvidedFixtureOptions, deckDef, + robotName, }: FixtureTableItemProps): JSX.Element { const { t, i18n } = useTranslation('protocol_setup') @@ -190,6 +190,7 @@ function FixtureTableItem({ isOnDevice={true} missingLabwareDisplayName={missingLabwareDisplayName} deckDef={deckDef} + robotName={robotName} /> ) : null} - - {t('module')} - {t('location')} - {t('status')} - + <> {attachedProtocolModuleMatches.map(module => { - const cutoutIdForSlotName = getCutoutIdForSlotName( + const moduleFixtures = getCutoutFixturesForModuleModel( + module.moduleDef.model, + deckDef + ) + const moduleCutoutIds = getCutoutIdsFromModuleSlotName( module.slotName, + moduleFixtures, deckDef ) - - const isMagneticBlockModule = - module.moduleDef.moduleType === MAGNETIC_BLOCK_TYPE - - const isThermocycler = - module.moduleDef.moduleType === THERMOCYCLER_MODULE_TYPE - const conflictedFixture = deckConfig?.find( - fixture => - (fixture.cutoutId === cutoutIdForSlotName || - // special-case A1 for the thermocycler to require a single slot fixture - (fixture.cutoutId === 'cutoutA1' && isThermocycler)) && - fixture.cutoutFixtureId != null && - // do not generate a conflict for single slot fixtures, because modules are not yet fixtures - !SINGLE_SLOT_FIXTURES.includes(fixture.cutoutFixtureId) && - // special case the magnetic module because unlike other modules it sits in a slot that can also be provided by a staging area fixture - (!isMagneticBlockModule || - fixture.cutoutFixtureId !== STAGING_AREA_RIGHT_SLOT_FIXTURE) + ({ cutoutId, cutoutFixtureId }) => + moduleCutoutIds.includes(cutoutId) && + !moduleFixtures.some(({ id }) => cutoutFixtureId === id) && + module.attachedModuleMatch == null ) ?? null - return ( ) })} - + ) } @@ -140,6 +115,7 @@ interface ModuleTableItemProps { prepCommandErrorMessage: string setPrepCommandErrorMessage: React.Dispatch> deckDef: DeckDefinition + robotName: string } function ModuleTableItem({ @@ -151,6 +127,7 @@ function ModuleTableItem({ setPrepCommandErrorMessage, conflictedFixture, deckDef, + robotName, }: ModuleTableItemProps): JSX.Element { const { i18n, t } = useTranslation(['protocol_setup', 'module_wizard_flows']) @@ -276,6 +253,7 @@ function ModuleTableItem({ requiredModule={module.moduleDef.model} deckDef={deckDef} isOnDevice={true} + robotName={robotName} /> ) : null} { setCutoutId: mockSetCutoutId, setProvidedFixtureOptions: mockSetProvidedFixtureOptions, } + vi.mocked(getLocalRobot).mockReturnValue(mockConnectedRobot) vi.mocked(LocationConflictModal).mockReturnValue(
mock location conflict modal
) @@ -60,13 +64,6 @@ describe('FixtureTable', () => { vi.clearAllMocks() }) - it('should render table header and contents', () => { - render(props) - screen.getByText('Fixture') - screen.getByText('Location') - screen.getByText('Status') - }) - it('should render the current status - configured', () => { render(props) screen.getByText('Configured') diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx index ead32d65d38..bf2dfbe5dc4 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/ProtocolSetupModulesAndDeck.test.tsx @@ -107,7 +107,7 @@ describe('ProtocolSetupModulesAndDeck', () => { .calledWith(mockRobotSideAnalysis, flexDeckDef) .thenReturn([]) when(vi.mocked(getAttachedProtocolModuleMatches)) - .calledWith([], []) + .calledWith([], [], []) .thenReturn([]) when(vi.mocked(getUnmatchedModulesForProtocol)) .calledWith([], []) @@ -148,7 +148,7 @@ describe('ProtocolSetupModulesAndDeck', () => { }, ]) render() - screen.getByText('Module') + screen.getByText('Deck hardware') screen.getByText('Location') screen.getByText('Status') screen.getByText('Setup Instructions') @@ -313,7 +313,7 @@ describe('ProtocolSetupModulesAndDeck', () => { vi.mocked(getAttachedProtocolModuleMatches).mockReturnValue([ { ...mockProtocolModuleInfo[0], - attachedModuleMatch: calibratedMockApiHeaterShaker, + attachedModuleMatch: undefined, slotName: 'D3', }, ]) diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/utils.test.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/utils.test.tsx index 97c76148799..b96d972ca36 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/utils.test.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/__tests__/utils.test.tsx @@ -1,7 +1,10 @@ import { describe, it, expect } from 'vitest' -import { getModuleDef2 } from '@opentrons/shared-data' +import { + TEMPERATURE_MODULE_V2_FIXTURE, + getModuleDef2, +} from '@opentrons/shared-data' -import { mockTemperatureModule } from '../../../redux/modules/__fixtures__' +import { mockTemperatureModuleGen2 } from '../../../redux/modules/__fixtures__' import { getAttachedProtocolModuleMatches, getUnmatchedModulesForProtocol, @@ -12,7 +15,7 @@ const temperatureProtocolModule = { x: 0, y: 0, z: 0, - moduleDef: getModuleDef2('temperatureModuleV1'), + moduleDef: getModuleDef2('temperatureModuleV2'), nestedLabwareDef: null, nestedLabwareId: null, nestedLabwareDisplayName: null, @@ -37,7 +40,8 @@ describe('getAttachedProtocolModuleMatches', () => { it('returns no module matches when no modules attached', () => { const result = getAttachedProtocolModuleMatches( [], - [temperatureProtocolModule, magneticProtocolModule] + [temperatureProtocolModule, magneticProtocolModule], + [] ) expect(result).toEqual([ { ...temperatureProtocolModule, attachedModuleMatch: null }, @@ -47,8 +51,15 @@ describe('getAttachedProtocolModuleMatches', () => { it('returns no module matches when no modules match', () => { const result = getAttachedProtocolModuleMatches( - [mockTemperatureModule], - [magneticProtocolModule] + [mockTemperatureModuleGen2], + [magneticProtocolModule], + [ + { + cutoutId: 'cutoutD1', + cutoutFixtureId: TEMPERATURE_MODULE_V2_FIXTURE, + opentronsModuleSerialNumber: mockTemperatureModuleGen2.serialNumber, + }, + ] ) expect(result).toEqual([ { ...magneticProtocolModule, attachedModuleMatch: null }, @@ -57,13 +68,20 @@ describe('getAttachedProtocolModuleMatches', () => { it('returns module match when modules match', () => { const result = getAttachedProtocolModuleMatches( - [mockTemperatureModule], - [temperatureProtocolModule, magneticProtocolModule] + [mockTemperatureModuleGen2], + [temperatureProtocolModule, magneticProtocolModule], + [ + { + cutoutId: 'cutoutD1', + cutoutFixtureId: TEMPERATURE_MODULE_V2_FIXTURE, + opentronsModuleSerialNumber: mockTemperatureModuleGen2.serialNumber, + }, + ] ) expect(result).toEqual([ { ...temperatureProtocolModule, - attachedModuleMatch: mockTemperatureModule, + attachedModuleMatch: mockTemperatureModuleGen2, }, { ...magneticProtocolModule, attachedModuleMatch: null }, ]) @@ -81,7 +99,7 @@ describe('getUnmatchedModulesForProtocol', () => { it('returns no missing module ids or remaining attached modules when attached modules match', () => { const result = getUnmatchedModulesForProtocol( - [mockTemperatureModule], + [mockTemperatureModuleGen2], [temperatureProtocolModule] ) expect(result).toEqual({ @@ -103,12 +121,12 @@ describe('getUnmatchedModulesForProtocol', () => { it('returns remaining attached modules when protocol modules and attached modules do not match', () => { const result = getUnmatchedModulesForProtocol( - [mockTemperatureModule], + [mockTemperatureModuleGen2], [magneticProtocolModule] ) expect(result).toEqual({ missingModuleIds: ['mockMagneticModuleId'], - remainingAttachedModules: [mockTemperatureModule], + remainingAttachedModules: [mockTemperatureModuleGen2], }) }) }) diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx b/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx index 3a03a91c9c6..86f51d42afe 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/index.tsx @@ -2,7 +2,14 @@ import * as React from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' -import { DIRECTION_COLUMN, Flex, SPACING } from '@opentrons/components' +import { + COLORS, + DIRECTION_COLUMN, + Flex, + SPACING, + StyledText, + TYPOGRAPHY, +} from '@opentrons/components' import { FLEX_ROBOT_TYPE, getDeckDefFromRobotType, @@ -26,8 +33,10 @@ import { ModulesAndDeckMapViewModal } from './ModulesAndDeckMapViewModal' import type { CutoutId, CutoutFixtureId } from '@opentrons/shared-data' import type { SetupScreens } from '../../pages/ProtocolSetup' +import { useDeckConfigurationQuery } from '@opentrons/react-api-client' const ATTACHED_MODULE_POLL_MS = 5000 +const DECK_CONFIG_POLL_MS = 5000 interface ProtocolSetupModulesAndDeckProps { runId: string @@ -59,7 +68,9 @@ export function ProtocolSetupModulesAndDeck({ const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) - + const { data: deckConfig = [] } = useDeckConfigurationQuery({ + refetchInterval: DECK_CONFIG_POLL_MS, + }) const attachedModules = useAttachedModules({ refetchInterval: ATTACHED_MODULE_POLL_MS, @@ -72,7 +83,8 @@ export function ProtocolSetupModulesAndDeck({ const attachedProtocolModuleMatches = getAttachedProtocolModuleMatches( attachedModules, - protocolModulesInfo + protocolModulesInfo, + deckConfig ) const hasModules = attachedProtocolModuleMatches.length > 0 @@ -105,7 +117,7 @@ export function ProtocolSetupModulesAndDeck({ getTopPortalEl() )} setSetupScreen('prepare to run')} buttonText={i18n.format(t('setup_instructions'), 'titleCase')} buttonType="tertiaryLowLight" @@ -116,7 +128,7 @@ export function ProtocolSetupModulesAndDeck({ {isModuleMismatch && !clearModuleMismatchBanner ? ( @@ -131,20 +143,36 @@ export function ProtocolSetupModulesAndDeck({ /> ) : null} - {hasModules ? ( - + + {i18n.format(t('deck_hardware'), 'titleCase')} + + {t('location')} + {t('status')} + + + {hasModules ? ( + + ) : null} + - ) : null} - +
setShowDeckMapModal(true)} /> diff --git a/app/src/organisms/ProtocolSetupModulesAndDeck/utils.ts b/app/src/organisms/ProtocolSetupModulesAndDeck/utils.ts index 113cba73075..cc921ef6049 100644 --- a/app/src/organisms/ProtocolSetupModulesAndDeck/utils.ts +++ b/app/src/organisms/ProtocolSetupModulesAndDeck/utils.ts @@ -1,6 +1,11 @@ import { + DeckConfiguration, + FLEX_ROBOT_TYPE, NON_CONNECTING_MODULE_TYPES, checkModuleCompatibility, + getCutoutFixturesForModuleModel, + getCutoutIdsFromModuleSlotName, + getDeckDefFromRobotType, getModuleType, } from '@opentrons/shared-data' @@ -11,14 +16,26 @@ export type AttachedProtocolModuleMatch = ProtocolModuleInfo & { attachedModuleMatch: AttachedModule | null } +// NOTE: this is a FLEX only function // some logic copied from useModuleRenderInfoForProtocolById export function getAttachedProtocolModuleMatches( attachedModules: AttachedModule[], - protocolModulesInfo: ProtocolModuleInfo[] + protocolModulesInfo: ProtocolModuleInfo[], + deckConfig: DeckConfiguration ): AttachedProtocolModuleMatch[] { + const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) // this is only used for Flex ODD const matchedAttachedModules: AttachedModule[] = [] const attachedProtocolModuleMatches = protocolModulesInfo.map( protocolModule => { + const moduleFixtures = getCutoutFixturesForModuleModel( + protocolModule.moduleDef.model, + deckDef + ) + const moduleCutoutIds = getCutoutIdsFromModuleSlotName( + protocolModule.slotName, + moduleFixtures, + deckDef + ) const compatibleAttachedModule = attachedModules.find( attachedModule => @@ -27,10 +44,17 @@ export function getAttachedProtocolModuleMatches( protocolModule.moduleDef.model ) && // check id instead of object reference in useModuleRenderInfoForProtocolById - matchedAttachedModules.find( + !matchedAttachedModules.some( matchedAttachedModule => - matchedAttachedModule.id === attachedModule.id - ) == null + matchedAttachedModule.serialNumber === + attachedModule.serialNumber + ) && + // check deck config has module with expected serial number in expected location + deckConfig.some( + ({ cutoutId, opentronsModuleSerialNumber }) => + attachedModule.serialNumber === opentronsModuleSerialNumber && + moduleCutoutIds.includes(cutoutId) + ) ) ?? null if (compatibleAttachedModule !== null) { matchedAttachedModules.push(compatibleAttachedModule) diff --git a/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx b/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx index 030e3c1a9a0..0c7497166aa 100644 --- a/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx +++ b/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx @@ -296,7 +296,7 @@ describe('ProtocolSetup', () => { render(`/runs/${RUN_ID}/setup/`) screen.getByText('Prepare to run') screen.getByText('Instruments') - screen.getByText('Modules & deck') + screen.getByText('Deck hardware') screen.getByText('Labware') screen.getByText('Labware Position Check') screen.getByText('Liquids') @@ -326,7 +326,7 @@ describe('ProtocolSetup', () => { .calledWith([], mockProtocolModuleInfo) .thenReturn({ missingModuleIds: [], remainingAttachedModules: [] }) render(`/runs/${RUN_ID}/setup/`) - fireEvent.click(screen.getByText('Modules & deck')) + fireEvent.click(screen.getByText('Deck hardware')) expect(vi.mocked(ProtocolSetupModulesAndDeck)).toHaveBeenCalled() }) diff --git a/app/src/pages/ProtocolSetup/index.tsx b/app/src/pages/ProtocolSetup/index.tsx index 14b871f839c..0c73eb1e50d 100644 --- a/app/src/pages/ProtocolSetup/index.tsx +++ b/app/src/pages/ProtocolSetup/index.tsx @@ -705,7 +705,7 @@ function PrepareToRun({ /> setSetupScreen('modules')} - title={t('modules_and_deck')} + title={t('deck_hardware')} detail={modulesDetail} subDetail={modulesSubDetail} status={modulesStatus} diff --git a/app/src/resources/__tests__/useNotifyService.test.ts b/app/src/resources/__tests__/useNotifyService.test.ts index fdb531ab1cd..ce513e3e572 100644 --- a/app/src/resources/__tests__/useNotifyService.test.ts +++ b/app/src/resources/__tests__/useNotifyService.test.ts @@ -14,6 +14,7 @@ import type { Mock } from 'vitest' import type { HostConfig } from '@opentrons/api-client' import type { QueryOptionsWithPolling } from '../useNotifyService' +vi.unmock('../useNotifyService') vi.mock('react-redux') vi.mock('@opentrons/react-api-client') vi.mock('../../redux/analytics') diff --git a/components/src/hardware-sim/DeckConfigurator/HeaterShakerFixture.tsx b/components/src/hardware-sim/DeckConfigurator/HeaterShakerFixture.tsx index 129fd46993a..a28448d0c51 100644 --- a/components/src/hardware-sim/DeckConfigurator/HeaterShakerFixture.tsx +++ b/components/src/hardware-sim/DeckConfigurator/HeaterShakerFixture.tsx @@ -12,6 +12,7 @@ import { FIXTURE_HEIGHT, COLUMN_3_SINGLE_SLOT_FIXTURE_WIDTH, Y_ADJUSTMENT, + CONFIG_STYLE_SELECTED, } from './constants' import type { @@ -30,6 +31,7 @@ interface HeaterShakerFixtureProps { fixtureLocation: CutoutId, cutoutFixtureId: CutoutFixtureId ) => void + selected?: boolean } const HEATER_SHAKER_MODULE_FIXTURE_DISPLAY_NAME = 'Heater-Shaker' @@ -42,6 +44,7 @@ export function HeaterShakerFixture( handleClickRemove, fixtureLocation, cutoutFixtureId, + selected = false, } = props const cutoutDef = deckDefinition.locations.cutouts.find( @@ -67,6 +70,7 @@ export function HeaterShakerFixture( const y = ySlotPosition + Y_ADJUSTMENT + const editableStyle = selected ? CONFIG_STYLE_SELECTED : CONFIG_STYLE_EDITABLE return ( void hasStagingArea?: boolean + selected?: boolean } const MAGNETIC_BLOCK_FIXTURE_DISPLAY_NAME = 'Mag Block' @@ -48,6 +50,7 @@ export function MagneticBlockFixture( handleClickRemove, cutoutFixtureId, hasStagingArea, + selected = false, } = props const standardSlotCutout = deckDefinition.locations.cutouts.find( @@ -98,6 +101,7 @@ export function MagneticBlockFixture( const y = ySlotPosition + Y_ADJUSTMENT + const editableStyle = selected ? CONFIG_STYLE_SELECTED : CONFIG_STYLE_EDITABLE return ( void + selected?: boolean } export function StagingAreaConfigFixture( @@ -39,6 +41,7 @@ export function StagingAreaConfigFixture( handleClickRemove, fixtureLocation, cutoutFixtureId, + selected = false, } = props const stagingAreaCutout = deckDefinition.locations.cutouts.find( @@ -55,6 +58,7 @@ export function StagingAreaConfigFixture( const x = xSlotPosition + COLUMN_3_X_ADJUSTMENT const y = ySlotPosition + Y_ADJUSTMENT + const editableStyle = selected ? CONFIG_STYLE_SELECTED : CONFIG_STYLE_EDITABLE return ( void + selected?: boolean } export function TemperatureModuleFixture( @@ -42,6 +44,7 @@ export function TemperatureModuleFixture( handleClickRemove, fixtureLocation, cutoutFixtureId, + selected = false, } = props const cutoutDef = deckDefinition.locations.cutouts.find( @@ -67,6 +70,8 @@ export function TemperatureModuleFixture( const y = ySlotPosition + Y_ADJUSTMENT + const editableStyle = selected ? CONFIG_STYLE_SELECTED : CONFIG_STYLE_EDITABLE + return ( void + selected?: boolean } const THERMOCYCLER_FIXTURE_DISPLAY_NAME = 'Thermocycler' @@ -39,6 +41,7 @@ export function ThermocyclerFixture( handleClickRemove, fixtureLocation, cutoutFixtureId, + selected = false, } = props const cutoutDef = deckDefinition.locations.cutouts.find( @@ -54,6 +57,7 @@ export function ThermocyclerFixture( const x = xSlotPosition + COLUMN_1_X_ADJUSTMENT const y = ySlotPosition + Y_ADJUSTMENT + const editableStyle = selected ? CONFIG_STYLE_SELECTED : CONFIG_STYLE_EDITABLE return ( void + selected?: boolean } export function TrashBinConfigFixture( @@ -40,6 +42,7 @@ export function TrashBinConfigFixture( handleClickRemove, fixtureLocation, cutoutFixtureId, + selected = false, } = props const trashBinCutout = deckDefinition.locations.cutouts.find( @@ -65,6 +68,7 @@ export function TrashBinConfigFixture( const y = ySlotPosition + Y_ADJUSTMENT + const editableStyle = selected ? CONFIG_STYLE_SELECTED : CONFIG_STYLE_EDITABLE return ( void hasStagingAreas?: boolean + selected?: boolean } export function WasteChuteConfigFixture( @@ -42,6 +44,7 @@ export function WasteChuteConfigFixture( fixtureLocation, cutoutFixtureId, hasStagingAreas = false, + selected = false, } = props const wasteChuteCutout = deckDefinition.locations.cutouts.find( @@ -58,6 +61,7 @@ export function WasteChuteConfigFixture( const x = xSlotPosition + COLUMN_3_X_ADJUSTMENT const y = ySlotPosition + Y_ADJUSTMENT + const editableStyle = selected ? CONFIG_STYLE_SELECTED : CONFIG_STYLE_EDITABLE return ( height?: string + selectedCutoutId?: CutoutId } export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { @@ -58,6 +59,7 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { handleClickRemove, additionalStaticFixtures, children, + selectedCutoutId, lightFill = COLORS.grey35, darkFill = COLORS.black90, editableCutoutIds = deckConfig.map(({ cutoutId }) => cutoutId), @@ -107,7 +109,6 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { return ( {deckDef.locations.cutouts.map(cutout => ( @@ -131,6 +132,7 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { } fixtureLocation={cutoutId} cutoutFixtureId={cutoutFixtureId} + selected={cutoutId === selectedCutoutId} /> ))} {emptyCutouts.map(({ cutoutId }) => ( @@ -150,6 +152,7 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { } fixtureLocation={cutoutId} cutoutFixtureId={cutoutFixtureId} + selected={cutoutId === selectedCutoutId} /> ))} {wasteChuteStagingAreaFixtures.map(({ cutoutId, cutoutFixtureId }) => ( @@ -161,6 +164,7 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { } fixtureLocation={cutoutId} cutoutFixtureId={cutoutFixtureId} + selected={cutoutId === selectedCutoutId} hasStagingAreas /> ))} @@ -173,6 +177,7 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { } fixtureLocation={cutoutId} cutoutFixtureId={cutoutFixtureId} + selected={cutoutId === selectedCutoutId} /> ))} {temperatureModuleFixtures.map(({ cutoutId, cutoutFixtureId }) => ( @@ -184,6 +189,7 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { } fixtureLocation={cutoutId} cutoutFixtureId={cutoutFixtureId} + selected={cutoutId === selectedCutoutId} /> ))} {heaterShakerFixtures.map(({ cutoutId, cutoutFixtureId }) => ( @@ -195,6 +201,7 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { } fixtureLocation={cutoutId} cutoutFixtureId={cutoutFixtureId} + selected={cutoutId === selectedCutoutId} /> ))} {magneticBlockFixtures.map(({ cutoutId, cutoutFixtureId }) => ( @@ -206,6 +213,7 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { } fixtureLocation={cutoutId} cutoutFixtureId={cutoutFixtureId} + selected={cutoutId === selectedCutoutId} hasStagingArea={ cutoutFixtureId === STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE } @@ -220,6 +228,7 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element { } fixtureLocation={cutoutId} cutoutFixtureId={cutoutFixtureId} + selected={cutoutId === selectedCutoutId} /> ))} {additionalStaticFixtures?.map(staticFixture => ( diff --git a/setup-vitest.ts b/setup-vitest.ts index bf9d07a6ba7..eb30f021428 100644 --- a/setup-vitest.ts +++ b/setup-vitest.ts @@ -7,6 +7,7 @@ vi.mock('electron-store') vi.mock('electron-updater') vi.mock('electron') vi.mock('./app/src/redux/shell/remote') +vi.mock('./app/src/resources/useNotifyService') process.env.OT_PD_VERSION = 'fake_PD_version' global._PKG_VERSION_ = 'test environment'