diff --git a/app/src/assets/localization/en/protocol_command_text.json b/app/src/assets/localization/en/protocol_command_text.json index 0363d853d0c..b4e35e6fa18 100644 --- a/app/src/assets/localization/en/protocol_command_text.json +++ b/app/src/assets/localization/en/protocol_command_text.json @@ -89,8 +89,7 @@ "single": "single", "single_nozzle_layout": "single nozzle layout", "slot": "Slot {{slot_name}}", - "stacker_display_name": "Stacker {{stacker_slot}}", - "stacker_column_display_name": "Stacker in Column {{stacker_slot}}", + "stacker_hopper_display": "Stacker {{row}}", "target_temperature": "target temperature", "tc_awaiting_for_duration": "Waiting for Thermocycler profile to complete", "tc_run_profile_steps": "Temperature: {{celsius}}°C, hold time: {{duration}}", diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunModuleControls.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunModuleControls.tsx index dde855141ac..99274341849 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunModuleControls.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunModuleControls.tsx @@ -9,6 +9,7 @@ import { SPACING, } from '@opentrons/components' import { useInstrumentsQuery } from '@opentrons/react-api-client' +import { getModuleDeckLabel } from '@opentrons/shared-data' import { ModuleCard } from '/app/organisms/ModuleCard' import { useModuleApiRequests } from '/app/organisms/ModuleCard/utils' @@ -98,7 +99,6 @@ export const ProtocolRunModuleControls = ({ const halfAttachedModulesSize = Math.ceil(attachedModules?.length / 2) const leftColumnModules = attachedModules?.slice(0, halfAttachedModulesSize) const rightColumnModules = attachedModules?.slice(halfAttachedModulesSize) - return attachedModules.length === 0 ? ( { }) => { // filter out the magnetic block here, because it is handled by the SetupFixturesList if (moduleDef.moduleType === MAGNETIC_BLOCK_TYPE) return null - // if the module is a flex stacker in D4, check if it needs a waste chute + // if the module is a flex stacker in row D, check if it needs a waste chute // combo fixture if ( moduleDef.moduleType === FLEX_STACKER_MODULE_TYPE && - slotName === 'D4' + slotName[0] === 'D' ) { const deckConfigCompatabilityD3 = deckConfigCompatibility?.find( configItem => configItem.cutoutId === 'cutoutD3' @@ -154,6 +153,7 @@ export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => { moduleDef.model )}_slot_${slotName}`} moduleModel={moduleDef.model} + moduleType={moduleDef.moduleType} displayName={moduleDef.displayName} slotName={slotName} attachedModuleMatch={attachedModuleMatch} @@ -176,6 +176,7 @@ export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => { moduleDef.model )}_slot_${slotName}`} moduleModel={moduleDef.model} + moduleType={moduleDef.moduleType} displayName={moduleDef.displayName} slotName={slotName} attachedModuleMatch={attachedModuleMatch} @@ -201,6 +202,7 @@ export const SetupModulesList = (props: SetupModulesListProps): JSX.Element => { interface ModulesListItemProps { moduleModel: ModuleModel + moduleType: ModuleType displayName: string slotName: string attachedModuleMatch: AttachedModule | null @@ -219,6 +221,7 @@ interface ModulesListItemProps { export function ModulesListItem({ moduleModel, + moduleType, displayName, slotName, attachedModuleMatch, @@ -468,11 +471,7 @@ export function ModulesListItem({ justifyContent={JUSTIFY_CENTER} > - {getModuleType(moduleModel) === 'thermocyclerModuleType' - ? isFlex - ? TC_MODULE_LOCATION_OT3 - : TC_MODULE_LOCATION_OT2 - : slotName} + {getModuleDeckLabel(moduleType, slotName)} {portDisplay != null ? ( {portDisplay} diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx index b1ea984db85..02c0eabd8e2 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupModuleAndDeck/__tests__/SetupModulesList.test.tsx @@ -52,7 +52,7 @@ const MOCK_SECOND_MAGNETIC_MODULE_COORDS = [100, 200, 0] const mockMagneticModule = { moduleId: 'someMagneticModule', model: 'magneticModuleV2' as ModuleModel, - type: 'magneticModuleType' as ModuleType, + moduleType: 'magneticModuleType' as ModuleType, labwareOffset: { x: 5, y: 5, z: 5 }, cornerOffsetFromSlot: { x: 1, y: 1, z: 1 }, calibrationPoint: { x: 0, y: 0 }, @@ -65,7 +65,7 @@ const mockTCModule = { labwareOffset: { x: 3, y: 3, z: 3 }, moduleId: 'TCModuleId', model: 'thermocyclerModuleV1' as ModuleModel, - type: 'thermocyclerModuleType' as ModuleType, + moduleType: 'thermocyclerModuleType' as ModuleType, displayName: 'Thermocycler Module', } @@ -217,7 +217,7 @@ describe('SetupModulesList', () => { nestedLabwareDef: null, nestedLabwareId: null, protocolLoadOrder: 0, - slotName: '7', + slotName: 'B1', attachedModuleMatch: mockThermocycler, }, } as any) @@ -278,7 +278,7 @@ describe('SetupModulesList', () => { nestedLabwareDef: null, nestedLabwareId: null, protocolLoadOrder: 0, - slotName: '7', + slotName: 'B1', attachedModuleMatch: { ...mockThermocycler, moduleOffset: mockCalibratedData, diff --git a/app/src/organisms/Desktop/ProtocolDetails/RobotConfigurationDetails.tsx b/app/src/organisms/Desktop/ProtocolDetails/RobotConfigurationDetails.tsx index 3982730ed5f..2f3b349679e 100644 --- a/app/src/organisms/Desktop/ProtocolDetails/RobotConfigurationDetails.tsx +++ b/app/src/organisms/Desktop/ProtocolDetails/RobotConfigurationDetails.tsx @@ -14,23 +14,33 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { + FLEX_STACKER_MODULE_TYPE, + FLEX_STACKER_WITH_MAG_BLOCK_FIXTURE, + FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS, FLEX_USB_MODULE_FIXTURES, + getAASlotDisplayName, + getAAWithFakesFromVSId, getCutoutDisplayName, getFixtureDisplayName, + getModuleDeckLabel, getModuleDisplayName, getModuleType, getPipetteNameSpecs, + getVisualSlotIdForAA, + MAGNETIC_BLOCK_ADDRESSABLE_AREAS, MAGNETIC_BLOCK_FIXTURES, MAGNETIC_BLOCK_TYPE, + MAGNETIC_BLOCK_V1_FIXTURE, SINGLE_SLOT_FIXTURES, - THERMOCYCLER_MODULE_TYPE, + STAGING_AREA_RIGHT_SLOT_FIXTURE, + STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE, + WASTE_CHUTE_FLEX_STACKER_FIXTURES, } from '@opentrons/shared-data' import { InstrumentContainer } from '/app/atoms/InstrumentContainer' import { Divider } from '/app/atoms/structure' import { getRobotTypeDisplayName } from '../ProtocolsLanding/utils' -import { getSlotsForThermocycler } from './utils' import type { TFunction } from 'i18next' import type { ReactNode } from 'react' @@ -114,12 +124,52 @@ export const RobotConfigurationDetails = ( // filter out single slot fixtures as they're implicit // also filter out usb module fixtures as they're handled by required modules - const nonStandardRequiredFixtureDetails = requiredFixtureDetails.filter( - fixture => - ![...SINGLE_SLOT_FIXTURES, ...FLEX_USB_MODULE_FIXTURES].includes( - fixture.cutoutFixtureId as SingleSlotCutoutFixtureId + const nonStandardRequiredFixtureDetails = requiredFixtureDetails.reduce< + CutoutConfigProtocolSpec[] + >((acc, fixture) => { + if ( + [ + ...SINGLE_SLOT_FIXTURES, + ...FLEX_USB_MODULE_FIXTURES, + ...WASTE_CHUTE_FLEX_STACKER_FIXTURES, + ].includes(fixture.cutoutFixtureId as SingleSlotCutoutFixtureId) + ) { + return acc + } else if ( + FLEX_STACKER_WITH_MAG_BLOCK_FIXTURE === fixture.cutoutFixtureId || + fixture.cutoutFixtureId === + STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE + ) { + const magBlockAA = fixture.requiredAddressableAreas.find(aa => + MAGNETIC_BLOCK_ADDRESSABLE_AREAS.includes(aa) ) - ) + acc.push({ + ...fixture, + cutoutFixtureId: MAGNETIC_BLOCK_V1_FIXTURE, + requiredAddressableAreas: [ + magBlockAA ?? fixture.requiredAddressableAreas[0], + ], + }) + if ( + fixture.cutoutFixtureId === + STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE + ) { + const stagingAreaAA = fixture.requiredAddressableAreas.find(aa => + FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS.includes(aa) + ) + acc.push({ + ...fixture, + cutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + requiredAddressableAreas: [ + stagingAreaAA ?? fixture.requiredAddressableAreas[0], + ], + }) + } + } else { + acc.push(fixture) + } + return acc + }, []) return ( @@ -163,16 +213,26 @@ export const RobotConfigurationDetails = ( a.params.location.slotName.localeCompare(b.params.location.slotName) ) .map((module, index) => { + const moduleType = getModuleType(module.params.model) + + const fixtureD3 = requiredFixtureDetails.find( + fixture => fixture.cutoutId === 'cutoutD3' + ) + const moduleDisplayName = + moduleType === FLEX_STACKER_MODULE_TYPE && + module.params.location.slotName === 'D3' && + fixtureD3 != null + ? getFixtureDisplayName(t as TFunction, fixtureD3.cutoutFixtureId) + : getModuleDisplayName(module.params.model) + return ( - {getModuleDisplayName(module.params.model)} + {moduleDisplayName} } @@ -195,11 +255,21 @@ export const RobotConfigurationDetails = ( ) })} {nonStandardRequiredFixtureDetails.map((fixture, index) => { + const visualSlotId = getVisualSlotIdForAA( + fixture.cutoutId, + fixture.cutoutFixtureId, + fixture.requiredAddressableAreas[0] + ) + const AAName = getAAWithFakesFromVSId(visualSlotId) return ( {MAGNETIC_BLOCK_FIXTURES.includes(fixture.cutoutFixtureId) ? ( diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx index c864e8bd2cb..04d6cc05869 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/index.tsx @@ -146,6 +146,7 @@ export function DeviceDetailsDeckConfiguration({ cutoutFixtureId, addressableAreaId ) + return { ...acc, displayList: [ diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx index 04237d680d8..88649424917 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/LeftColumnLabwareInfo.tsx @@ -76,7 +76,9 @@ export function LeftColumnLabwareInfo({ labwareName: failedLabwareNames.name ?? '', labwareNickname: failedLabwareNames.nickName, currentLocationProps: { - deckLabel: displayNameCurrentLoc.toUpperCase(), + deckLabel: `STACKER ${( + displayNameCurrentLoc?.toUpperCase() ?? '' + ).charAt(0)}`, }, } case STACKER_STALLED_SKIP.STEPS.PLACE_LABWARE_ON_SHUTTLE: @@ -86,7 +88,9 @@ export function LeftColumnLabwareInfo({ labwareName: failedLabwareNames.name ?? '', labwareNickname: failedLabwareNames.nickName, currentLocationProps: { - deckLabel: displayNameNewLoc?.toUpperCase() ?? '', + deckLabel: `${(displayNameNewLoc?.toUpperCase() ?? '').charAt( + 0 + )}4`, }, } default: diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/StackerHopperLwInfo.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/StackerHopperLwInfo.tsx index cde1d8af1ae..0227f6aae56 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/StackerHopperLwInfo.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/StackerHopperLwInfo.tsx @@ -22,8 +22,8 @@ export function StackerHopperLwInfo(props: RecoveryContentProps): JSX.Element { diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/StackerShuttleLwInfo.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/StackerShuttleLwInfo.tsx index 3f8463b0149..dfae410a511 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/StackerShuttleLwInfo.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/StackerShuttleLwInfo.tsx @@ -19,8 +19,8 @@ export function StackerShuttleLwInfo(props: RecoveryContentProps): JSX.Element { diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx index a4989dea41c..17c6104f6ec 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx @@ -186,7 +186,7 @@ export function TwoColLwInfoAndDeck( {...props} title={buildTitle()} type={buildType()} - layout={'default'} + layout="default" bannerText={buildBannerText()} /> {buildDeckView()} diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StackerShuttleLwInfo.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StackerShuttleLwInfo.test.tsx index a2ff083964a..05b0ebbcb62 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StackerShuttleLwInfo.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/StackerShuttleLwInfo.test.tsx @@ -78,7 +78,7 @@ describe('Render StackerShuttleLwInfo', () => { expect.objectContaining({ title: 'Load labware onto labware shuttle', type: 'location', - layout: 'stacked', + layout: 'default', showQuantity: false, }), expect.anything() diff --git a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx index e0b0db538ea..136908f7cb2 100644 --- a/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx +++ b/app/src/organisms/InterventionModal/MoveLabwareInterventionContent.tsx @@ -1,43 +1,26 @@ import { useTranslation } from 'react-i18next' -import { css } from 'styled-components' import { - ALIGN_CENTER, - BORDERS, Box, COLORS, - DeckInfoLabel, DIRECTION_COLUMN, - DISPLAY_NONE, Flex, + getLabwareDisplayLocation, getLoadedLabware, - getLoadedModule, - Icon, LabwareRender, - LegacyStyledText, MoveLabwareOnDeck, - RESPONSIVENESS, SPACING, - TEXT_TRANSFORM_UPPERCASE, - TYPOGRAPHY, } from '@opentrons/components' import { getDeckDefFromRobotType, getLoadedLabwareDefinitionsByUri, - getModuleType, - GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA, - OT2_ROBOT_TYPE, - TC_MODULE_LOCATION_OT2, - TC_MODULE_LOCATION_OT3, - THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' -import { Divider } from '/app/atoms/structure' +import { InterventionInfo } from '/app/molecules/InterventionModal/InterventionContent' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' import { getLabwareNameFromRunData, - getModuleModelFromRunData, getRunLabwareRenderInfo, getRunModuleRenderInfo, } from './utils' @@ -45,61 +28,10 @@ import { import type { RunData } from '@opentrons/api-client' import type { CompletedProtocolAnalysis, - LabwareDefinitionsByUri, - LabwareLocation, MoveLabwareRunTimeCommand, RobotType, } from '@opentrons/shared-data' -const LABWARE_DESCRIPTION_STYLE = css` - flex-direction: ${DIRECTION_COLUMN}; - grid-gap: ${SPACING.spacing8}; - padding: ${SPACING.spacing16}; - background-color: ${COLORS.grey20}; - border-radius: ${BORDERS.borderRadius4}; - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - background-color: ${COLORS.grey35}; - border-radius: ${BORDERS.borderRadius8}; - } -` - -const LABWARE_NAME_TITLE_STYLE = css` - font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - display: ${DISPLAY_NONE}; - } -` - -const LABWARE_NAME_STYLE = css` - color: ${COLORS.grey60}; - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - ${TYPOGRAPHY.bodyTextBold} - color: ${COLORS.black90}; - } -` - -const DIVIDER_STYLE = css` - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - display: ${DISPLAY_NONE}; - } -` - -const LABWARE_DIRECTION_STYLE = css` - align-items: ${ALIGN_CENTER}; - grid-gap: ${SPACING.spacing4}; - text-transform: ${TEXT_TRANSFORM_UPPERCASE}; - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - grid-gap: ${SPACING.spacing8}; - } -` - -const ICON_STYLE = css` - height: 1.5rem; - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - height: 2.5rem; - } -` - export interface MoveLabwareInterventionProps { command: MoveLabwareRunTimeCommand analysis: CompletedProtocolAnalysis | null @@ -115,7 +47,7 @@ export function MoveLabwareInterventionContent({ robotType, isOnDevice, }: MoveLabwareInterventionProps): JSX.Element | null { - const { t } = useTranslation(['protocol_setup', 'protocol_command_text']) + const { t, i18n } = useTranslation('protocol_command_text') const analysisCommands = analysis?.commands ?? [] const labwareDefsByUri = getLoadedLabwareDefinitionsByUri(analysisCommands) @@ -161,6 +93,24 @@ export function MoveLabwareInterventionContent({ ? labwareDefsByUri?.[movedLabwareDefUri] ?? null : null if (oldLabwareLocation == null || movedLabwareDef == null) return null + const oldDisplayLabwareLocation = getLabwareDisplayLocation({ + location: oldLabwareLocation, + loadedModules: run.modules, + loadedLabwares: run.labware, + robotType: 'OT-3 Standard', + detailLevel: 'slot-only', + includeSlotText: false, + t, + }) + const newDisplayLabwareLocation = getLabwareDisplayLocation({ + location: command.params.newLocation, + loadedModules: run.modules, + loadedLabwares: run.labware, + robotType: 'OT-3 Standard', + detailLevel: 'slot-only', + includeSlotText: false, + t, + }) return ( - - - - {t('labware_name')} - - - {labwareName} - - - - - - - - - - + @@ -238,92 +172,3 @@ export function MoveLabwareInterventionContent({ ) } - -interface LabwareDisplayLocationProps { - protocolData: RunData - location: LabwareLocation - robotType: RobotType - labwareDefsByUri: LabwareDefinitionsByUri -} -function LabwareDisplayLocation( - props: LabwareDisplayLocationProps -): JSX.Element { - const { t } = useTranslation('protocol_command_text') - const { protocolData, location, robotType } = props - let displayLocation: string = '' - if (location === 'offDeck' || location === 'systemLocation') { - // TODO(BC, 08/28/23): remove this string cast after update i18next to >23 (see https://www.i18next.com/overview/typescript#argument-of-type-defaulttfuncreturn-is-not-assignable-to-parameter-of-type-xyz) - displayLocation = String(t('offdeck')) - } else if ('slotName' in location) { - displayLocation = location.slotName - } else if ('addressableAreaName' in location) { - const aaLocation = location.addressableAreaName - displayLocation = - aaLocation === GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA - ? t('waste_chute') - : aaLocation - } else if ('moduleId' in location) { - const moduleModel = getModuleModelFromRunData( - protocolData, - location.moduleId - ) - if (moduleModel == null) { - console.warn('labware is located on an unknown module model') - } else { - const slotName = - getLoadedModule(protocolData.modules, location.moduleId)?.location - ?.slotName ?? '' - const isModuleUnderAdapterThermocycler = - getModuleType(moduleModel) === THERMOCYCLER_MODULE_TYPE - if (isModuleUnderAdapterThermocycler) { - displayLocation = - robotType === OT2_ROBOT_TYPE - ? TC_MODULE_LOCATION_OT2 - : TC_MODULE_LOCATION_OT3 - } else { - displayLocation = slotName - } - } - } else if ('labwareId' in location) { - const adapter = protocolData.labware.find( - lw => lw.id === location.labwareId - ) - if (adapter == null) { - console.warn('labware is located on an unknown adapter') - } else if ( - adapter.location === 'offDeck' || - adapter.location === 'systemLocation' - ) { - displayLocation = t('off_deck') - } else if ('slotName' in adapter.location) { - displayLocation = adapter.location.slotName - } else if ('addressableAreaName' in adapter.location) { - displayLocation = adapter.location.addressableAreaName - } else if ('moduleId' in adapter.location) { - const moduleIdUnderAdapter = adapter.location.moduleId - const moduleModel = protocolData.modules.find( - module => module.id === moduleIdUnderAdapter - )?.model - if (moduleModel == null) { - console.warn('labware is located on an adapter on an unknown module') - } else { - const slotName = - getLoadedModule(protocolData.modules, adapter.location.moduleId) - ?.location?.slotName ?? '' - const isModuleUnderAdapterThermocycler = - getModuleType(moduleModel) === THERMOCYCLER_MODULE_TYPE - if (isModuleUnderAdapterThermocycler) { - displayLocation = - robotType === OT2_ROBOT_TYPE - ? TC_MODULE_LOCATION_OT2 - : TC_MODULE_LOCATION_OT3 - } else { - displayLocation = slotName - } - } - } else { - console.warn('display location could not be established: ', location) - } - } - return -} diff --git a/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx b/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx index be449057d39..a10160ee45e 100644 --- a/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx +++ b/app/src/organisms/InterventionModal/__tests__/InterventionModal.test.tsx @@ -172,7 +172,6 @@ describe('InterventionModal', () => { } render(props) screen.getByText('Move labware on Otie') - screen.getByText('Labware name') screen.getByText('mockLabware') screen.queryAllByText('A1') screen.queryAllByText('D3') @@ -211,7 +210,6 @@ describe('InterventionModal', () => { } render(props) screen.getByText('Move labware on Otie') - screen.getByText('Labware name') screen.getByText('mockLabwareInStagingArea') screen.queryAllByText('B4') screen.queryAllByText('C4') @@ -246,7 +244,6 @@ describe('InterventionModal', () => { } render(props) screen.getByText('Move labware on Otie') - screen.getByText('Labware name') screen.getByText('mockLabware') screen.queryAllByText('A1') screen.queryAllByText('C1') @@ -284,7 +281,6 @@ describe('InterventionModal', () => { } as any, } render(props) - screen.getByText('Labware name') screen.getByText('mockLabwareInStagingArea') screen.queryAllByText('B4') screen.queryAllByText('Waste Chute') diff --git a/app/src/organisms/LocationConflictModal/getFilteredDeckConfigFixtureCompatibility.ts b/app/src/organisms/LocationConflictModal/getFilteredDeckConfigFixtureCompatibility.ts deleted file mode 100644 index 0a75c6b2aba..00000000000 --- a/app/src/organisms/LocationConflictModal/getFilteredDeckConfigFixtureCompatibility.ts +++ /dev/null @@ -1,152 +0,0 @@ -import { - FLEX_STACKER_ADDRESSABLE_AREAS, - FLEX_STACKER_V1_FIXTURE, - FLEX_USB_MODULE_ADDRESSABLE_AREAS, - MAGNETIC_BLOCK_ADDRESSABLE_AREAS, - MAGNETIC_BLOCK_FIXTURES, - MAGNETIC_BLOCK_V1_FIXTURE, - SINGLE_SLOT_FIXTURES, - STAGING_AREA_FIXTURES, - STAGING_AREA_RIGHT_SLOT_FIXTURE, - STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE, - THERMOCYCLER_V2_FRONT_FIXTURE, - THERMOCYCLER_V2_REAR_FIXTURE, -} from '@opentrons/shared-data' - -import type { CutoutFixtureId } from '@opentrons/shared-data' -import type { CutoutConfigAndCompatibility } from '/app/resources/deck_configuration/hooks' - -export const getFilteredDeckConfigFixtureCompatibility = ( - deckConfigCompatibility: CutoutConfigAndCompatibility[] -): CutoutConfigAndCompatibility[] => { - // if both A1 and B1 need to be empty but the thermocycler is attached, only - // show a conflict for A1 to avoid redundancy - const hasTwoLabwareThermocyclerConflicts = - deckConfigCompatibility.some( - ({ cutoutFixtureId, compatibleCutoutFixtureIds }) => - cutoutFixtureId === THERMOCYCLER_V2_FRONT_FIXTURE && - compatibleCutoutFixtureIds.some(fixtureId => - SINGLE_SLOT_FIXTURES.includes(fixtureId) - ) - ) && - deckConfigCompatibility.some( - ({ cutoutFixtureId, compatibleCutoutFixtureIds }) => - cutoutFixtureId === THERMOCYCLER_V2_REAR_FIXTURE && - compatibleCutoutFixtureIds.some(fixtureId => - SINGLE_SLOT_FIXTURES.includes(fixtureId) - ) - ) - return deckConfigCompatibility - .filter(({ cutoutFixtureId }) => { - return ( - !hasTwoLabwareThermocyclerConflicts || - !(cutoutFixtureId === THERMOCYCLER_V2_REAR_FIXTURE) - ) - }) - .reduce((acc, compatabilityItem) => { - // filter out all fixtures that only provide usb module addressable areas - // (i.e. everything but MagBlockV1 and StagingAreaWithMagBlockV1) - // as they're handled in the Modules Table - if ( - compatabilityItem.requiredAddressableAreas.every(raa => - FLEX_USB_MODULE_ADDRESSABLE_AREAS.includes(raa) - ) || - (compatabilityItem.requiredAddressableAreas.some(raa => - FLEX_STACKER_ADDRESSABLE_AREAS.includes(raa) - ) && - !compatabilityItem.requiredAddressableAreas.some(raa => - MAGNETIC_BLOCK_ADDRESSABLE_AREAS.includes(raa) - )) - ) { - return acc - } - // if there is a magnetic block combination fixture, separate it out - // and show two line items in the table - if ( - compatabilityItem.compatibleCutoutFixtureIds.some(fixtureId => - MAGNETIC_BLOCK_FIXTURES.includes(fixtureId) - ) - ) { - const magBlockRequirement = { - ...compatabilityItem, - partialRequiredCutoutFixtureId: MAGNETIC_BLOCK_V1_FIXTURE, - } - acc.push(magBlockRequirement) - if ( - compatabilityItem.compatibleCutoutFixtureIds.length === 1 && - compatabilityItem.compatibleCutoutFixtureIds[0] === - STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE - ) { - const stagingAreaRequirement = { - ...compatabilityItem, - partialRequiredCutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, - } - acc.push(stagingAreaRequirement) - } - return acc - } else { - acc.push(compatabilityItem) - return acc - } - }, []) -} - -/** - * This function determines if the currently configured fixture is compatible - * with the required fixture for a protocol - * @param cutoutFixtureId: currently configured fixtureId - * @param compatibleCutoutFixtureIds: array of fixtureIds that are compatible for this cutout in the protocol - * @param partialRequiredCutoutFixtureId: required fixtureId that may fulfill a subset of the requirement for - * one of the compatible cutout fixtureIds - * @returns boolean indicating if the current fixture conflicts with the required fixture - */ -export const isFixtureCompatible = ( - cutoutFixtureId: CutoutFixtureId, - compatibleCutoutFixtureIds: CutoutFixtureId[], - partialRequiredCutoutFixtureId?: CutoutFixtureId -): boolean => { - if (partialRequiredCutoutFixtureId === MAGNETIC_BLOCK_V1_FIXTURE) { - return MAGNETIC_BLOCK_FIXTURES.includes(cutoutFixtureId) - } else if ( - partialRequiredCutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE - ) { - return STAGING_AREA_FIXTURES.includes(cutoutFixtureId) - } else { - return ( - cutoutFixtureId != null && - compatibleCutoutFixtureIds.includes(cutoutFixtureId) - ) - } -} - -/** - * Assuming the current fixture is not compatible, this function checks if there - * is a conflicting fixture configured, or if the fixture is just missing. - * @param cutoutFixtureId: currently configured fixtureId - * @param partialRequiredCutoutFixtureId: required fixtureId that may be able to coexist with - * a non-single slot fixture in the same cutout - * @returns boolean indicating if the current fixture conflicts with the required fixture - */ -export const isConflictingFixtureConfigured = ( - cutoutFixtureId: CutoutFixtureId, - partialRequiredCutoutFixtureId?: CutoutFixtureId -): boolean => { - if (partialRequiredCutoutFixtureId === MAGNETIC_BLOCK_V1_FIXTURE) { - return ( - !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) && - cutoutFixtureId !== STAGING_AREA_RIGHT_SLOT_FIXTURE && - cutoutFixtureId !== FLEX_STACKER_V1_FIXTURE - ) - } else if ( - partialRequiredCutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE - ) { - return ( - !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) && - cutoutFixtureId !== MAGNETIC_BLOCK_V1_FIXTURE - ) - } else { - return ( - cutoutFixtureId != null && !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) - ) - } -} diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/FixtureTable.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/FixtureTable.tsx index b259cbb3f8e..ddbe860c799 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/FixtureTable.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/FixtureTable.tsx @@ -24,14 +24,14 @@ import { import { SmallButton } from '/app/atoms/buttons' import { LocationConflictModal } from '/app/organisms/LocationConflictModal' +import { NotConfiguredModal } from '/app/organisms/LocationConflictModal/NotConfiguredModal' +import { getLocalRobot } from '/app/redux/discovery' import { getFilteredDeckConfigFixtureCompatibility, + getRequiredDeckConfig, isConflictingFixtureConfigured, isFixtureCompatible, -} from '/app/organisms/LocationConflictModal/getFilteredDeckConfigFixtureCompatibility' -import { NotConfiguredModal } from '/app/organisms/LocationConflictModal/NotConfiguredModal' -import { getLocalRobot } from '/app/redux/discovery' -import { getRequiredDeckConfig } from '/app/resources/deck_configuration/utils' +} from '/app/resources/deck_configuration/utils' import type { TFunction } from 'i18next' import type { diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ModuleTableItem.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ModuleTableItem.tsx index 5d3636167b4..851b9b8ae2f 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ModuleTableItem.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ModuleTableItem.tsx @@ -19,10 +19,8 @@ import { ABSORBANCE_READER_TYPE, FLEX_STACKER_MODULE_TYPE, getFixtureDisplayName, + getModuleDeckLabel, getModuleDisplayName, - getModuleType, - TC_MODULE_LOCATION_OT3, - THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' import { SmallButton } from '/app/atoms/buttons' @@ -45,7 +43,6 @@ import type { CutoutConfig, CutoutFixtureId, DeckDefinition, - ModuleModel, } from '@opentrons/shared-data' import type { ModulePrepCommandsType } from '/app/local-resources/modules' import type { ProtocolCalibrationStatus } from '/app/resources/runs' @@ -292,17 +289,6 @@ export function ModuleTableItem({ } } - const getModuleLocation = (moduleModel: ModuleModel): string => { - const moduleType = getModuleType(moduleModel) - if (moduleType === THERMOCYCLER_MODULE_TYPE) { - return TC_MODULE_LOCATION_OT3 - } else if (moduleType === FLEX_STACKER_MODULE_TYPE) { - return `${module.slotName.charAt(0)}4` - } else { - return module.slotName - } - } - return ( <> {showLocationConflictModal && conflictedFixture != null ? ( @@ -338,7 +324,10 @@ export function ModuleTableItem({ trash.location.split('cutout')[1] === slotId ) - const tcSlot = robotType === FLEX_ROBOT_TYPE ? 'A1+B1' : '7,8,10,11' return (
@@ -105,10 +101,11 @@ export function SlotDetails(props: SlotDetailsProps): JSX.Element { Slot diff --git a/app/src/pages/ODD/ProtocolDetails/Hardware.tsx b/app/src/pages/ODD/ProtocolDetails/Hardware.tsx index e8782a04719..7c3653b9bef 100644 --- a/app/src/pages/ODD/ProtocolDetails/Hardware.tsx +++ b/app/src/pages/ODD/ProtocolDetails/Hardware.tsx @@ -16,13 +16,13 @@ import { import { getCutoutDisplayName, getFixtureDisplayName, + getModuleDeckLabel, getModuleDisplayName, getModuleType, GRIPPER_V1_2, MAGNETIC_BLOCK_FIXTURES, MAGNETIC_BLOCK_TYPE, - TC_MODULE_LOCATION_OT3, - THERMOCYCLER_MODULE_TYPE, + STAGING_AREA_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' import { @@ -100,6 +100,11 @@ const useHardwareName = ( return gripperDisplayName } else if (protocolHardware.hardwareType === 'pipette') { return pipetteDisplayName + } else if ( + protocolHardware.hardwareType === 'module' && + protocolHardware.comboFixtureId != null + ) { + return getFixtureDisplayName(t, protocolHardware.comboFixtureId) } else if (protocolHardware.hardwareType === 'module') { return getModuleDisplayName(protocolHardware.moduleModel) } else { @@ -122,17 +127,21 @@ function HardwareItem({ ) if (hardware.hardwareType === 'module') { - const slot = - getModuleType(hardware.moduleModel) === THERMOCYCLER_MODULE_TYPE - ? TC_MODULE_LOCATION_OT3 - : hardware.slot - location = - } else if (hardware.hardwareType === 'fixture') { location = ( ) + } else if (hardware.hardwareType === 'fixture') { + const cutoutDisplayName = getCutoutDisplayName(hardware.location.cutout) + const slotName = + hardware.cutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE + ? `${cutoutDisplayName[0]}4` + : cutoutDisplayName + location = } const isMagneticBlockFixture = hardware.hardwareType === 'fixture' && diff --git a/app/src/pages/ODD/ProtocolDetails/__tests__/Hardware.test.tsx b/app/src/pages/ODD/ProtocolDetails/__tests__/Hardware.test.tsx index 9dd153c9a50..d60e1f1a996 100644 --- a/app/src/pages/ODD/ProtocolDetails/__tests__/Hardware.test.tsx +++ b/app/src/pages/ODD/ProtocolDetails/__tests__/Hardware.test.tsx @@ -105,6 +105,6 @@ describe('Hardware', () => { screen.getByRole('row', { name: '3 Temperature Module GEN2' }) screen.getByRole('row', { name: 'A1+B1 Thermocycler Module GEN2' }) screen.getByRole('row', { name: 'D3 Waste Chute' }) - screen.getByRole('row', { name: 'B3 Staging Area Slot' }) + screen.getByRole('row', { name: 'B4 Staging Area Slot' }) }) }) diff --git a/app/src/pages/ODD/QuickTransferDetails/Hardware.tsx b/app/src/pages/ODD/QuickTransferDetails/Hardware.tsx index a8b35059fc9..8858de8d691 100644 --- a/app/src/pages/ODD/QuickTransferDetails/Hardware.tsx +++ b/app/src/pages/ODD/QuickTransferDetails/Hardware.tsx @@ -16,6 +16,7 @@ import { import { getCutoutDisplayName, getFixtureDisplayName, + getModuleDeckLabel, getModuleDisplayName, getModuleType, GRIPPER_V1_2, @@ -120,7 +121,14 @@ function HardwareItem({ ) if (hardware.hardwareType === 'module') { - location = + location = ( + + ) } else if (hardware.hardwareType === 'fixture') { location = ( { + // if both A1 and B1 need to be empty but the thermocycler is attached, only + // show a conflict for A1 to avoid redundancy + const hasTwoLabwareThermocyclerConflicts = + deckConfigCompatibility.some( + ({ cutoutFixtureId, compatibleCutoutFixtureIds }) => + cutoutFixtureId === THERMOCYCLER_V2_FRONT_FIXTURE && + compatibleCutoutFixtureIds.some(fixtureId => + SINGLE_SLOT_FIXTURES.includes(fixtureId) + ) + ) && + deckConfigCompatibility.some( + ({ cutoutFixtureId, compatibleCutoutFixtureIds }) => + cutoutFixtureId === THERMOCYCLER_V2_REAR_FIXTURE && + compatibleCutoutFixtureIds.some(fixtureId => + SINGLE_SLOT_FIXTURES.includes(fixtureId) + ) + ) + return deckConfigCompatibility + .filter(({ cutoutFixtureId }) => { + return ( + !hasTwoLabwareThermocyclerConflicts || + !(cutoutFixtureId === THERMOCYCLER_V2_REAR_FIXTURE) + ) + }) + .reduce( + (acc, compatabilityItem) => { + // filter out all fixtures that only provide usb module addressable areas + // (i.e. everything but MagBlockV1 and StagingAreaWithMagBlockV1) + // as they're handled in the Modules Table + if ( + compatabilityItem.requiredAddressableAreas.every(raa => + FLEX_USB_MODULE_ADDRESSABLE_AREAS.includes(raa) + ) || + (compatabilityItem.requiredAddressableAreas.some(raa => + FLEX_STACKER_ADDRESSABLE_AREAS.includes(raa) + ) && + !compatabilityItem.requiredAddressableAreas.some(raa => + MAGNETIC_BLOCK_ADDRESSABLE_AREAS.includes(raa) + )) + ) { + return acc + } + // if there is a magnetic block combination fixture, separate it out + // and show two line items in the table + if ( + compatabilityItem.compatibleCutoutFixtureIds.some(fixtureId => + MAGNETIC_BLOCK_FIXTURES.includes(fixtureId) + ) + ) { + const magBlockRequirement = { + ...compatabilityItem, + partialRequiredCutoutFixtureId: MAGNETIC_BLOCK_V1_FIXTURE, + } + acc.push(magBlockRequirement) + if ( + compatabilityItem.compatibleCutoutFixtureIds.length === 1 && + compatabilityItem.compatibleCutoutFixtureIds[0] === + STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE + ) { + const stagingAreaRequirement = { + ...compatabilityItem, + partialRequiredCutoutFixtureId: STAGING_AREA_RIGHT_SLOT_FIXTURE, + } + acc.push(stagingAreaRequirement) + } + return acc + } else { + acc.push(compatabilityItem) + return acc + } + }, + [] + ) +} + +/** + * This function determines if the currently configured fixture is compatible + * with the required fixture for a protocol + * @param cutoutFixtureId: currently configured fixtureId + * @param compatibleCutoutFixtureIds: array of fixtureIds that are compatible for this cutout in the protocol + * @param partialRequiredCutoutFixtureId: required fixtureId that may fulfill a subset of the requirement for + * one of the compatible cutout fixtureIds + * @returns boolean indicating if the current fixture conflicts with the required fixture + */ +export const isFixtureCompatible = ( + cutoutFixtureId: CutoutFixtureId, + compatibleCutoutFixtureIds: CutoutFixtureId[], + partialRequiredCutoutFixtureId?: CutoutFixtureId +): boolean => { + if (partialRequiredCutoutFixtureId === MAGNETIC_BLOCK_V1_FIXTURE) { + return MAGNETIC_BLOCK_FIXTURES.includes(cutoutFixtureId) + } else if ( + partialRequiredCutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE + ) { + return STAGING_AREA_FIXTURES.includes(cutoutFixtureId) + } else { + return ( + cutoutFixtureId != null && + compatibleCutoutFixtureIds.includes(cutoutFixtureId) + ) + } +} + +/** + * Assuming the current fixture is not compatible, this function checks if there + * is a conflicting fixture configured, or if the fixture is just missing. + * @param cutoutFixtureId: currently configured fixtureId + * @param partialRequiredCutoutFixtureId: required fixtureId that may be able to coexist with + * a non-single slot fixture in the same cutout + * @returns boolean indicating if the current fixture conflicts with the required fixture + */ +export const isConflictingFixtureConfigured = ( + cutoutFixtureId: CutoutFixtureId, + partialRequiredCutoutFixtureId?: CutoutFixtureId +): boolean => { + if (partialRequiredCutoutFixtureId === MAGNETIC_BLOCK_V1_FIXTURE) { + return ( + !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) && + cutoutFixtureId !== STAGING_AREA_RIGHT_SLOT_FIXTURE && + cutoutFixtureId !== FLEX_STACKER_V1_FIXTURE + ) + } else if ( + partialRequiredCutoutFixtureId === STAGING_AREA_RIGHT_SLOT_FIXTURE + ) { + return ( + !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) && + cutoutFixtureId !== MAGNETIC_BLOCK_V1_FIXTURE + ) + } else { + return ( + cutoutFixtureId != null && !SINGLE_SLOT_FIXTURES.includes(cutoutFixtureId) + ) + } +} diff --git a/app/src/resources/runs/useModuleRenderInfoForProtocolById.ts b/app/src/resources/runs/useModuleRenderInfoForProtocolById.ts index 34dcfb8a102..91907f911f8 100644 --- a/app/src/resources/runs/useModuleRenderInfoForProtocolById.ts +++ b/app/src/resources/runs/useModuleRenderInfoForProtocolById.ts @@ -1,7 +1,6 @@ import { checkModuleCompatibility, FLEX_ROBOT_TYPE, - FLEX_STACKER_MODULE_TYPE, getCutoutFixturesForModuleModel, getCutoutIdsFromModuleSlotName, getDeckDefFromRobotType, @@ -120,10 +119,6 @@ export function useModuleRenderInfoForProtocolById( ...acc, [moduleInfo.moduleId]: { ...moduleInfo, - slotName: - moduleInfo.moduleDef.moduleType === FLEX_STACKER_MODULE_TYPE - ? `${moduleInfo.slotName.charAt(0)}4` - : moduleInfo.slotName, }, }), {} diff --git a/app/src/transformations/commands/hooks/__tests__/useMissingProtocolHardware.test.tsx b/app/src/transformations/commands/hooks/__tests__/useMissingProtocolHardware.test.tsx index 7d5354dfb89..b22f4261683 100644 --- a/app/src/transformations/commands/hooks/__tests__/useMissingProtocolHardware.test.tsx +++ b/app/src/transformations/commands/hooks/__tests__/useMissingProtocolHardware.test.tsx @@ -209,6 +209,7 @@ describe.only('useMissingProtocolHardware', () => { { hardwareType: 'module', moduleModel: 'heaterShakerModuleV1', + comboFixtureId: null, slot: 'D3', connected: false, hasSlotConflict: false, @@ -243,6 +244,7 @@ describe.only('useMissingProtocolHardware', () => { { hardwareType: 'module', moduleModel: 'heaterShakerModuleV1', + comboFixtureId: null, slot: 'D3', connected: false, hasSlotConflict: true, @@ -338,6 +340,7 @@ describe.only('useMissingProtocolHardware', () => { { hardwareType: 'module', moduleModel: 'heaterShakerModuleV1', + comboFixtureId: null, slot: 'D3', connected: false, hasSlotConflict: true, diff --git a/app/src/transformations/commands/hooks/types.ts b/app/src/transformations/commands/hooks/types.ts index c2a37e639c9..fdc6efc9da2 100644 --- a/app/src/transformations/commands/hooks/types.ts +++ b/app/src/transformations/commands/hooks/types.ts @@ -15,6 +15,7 @@ export interface ProtocolPipette { export interface ProtocolModule { hardwareType: 'module' moduleModel: ModuleModel + comboFixtureId?: CutoutFixtureId | null slot: string connected: boolean hasSlotConflict: boolean diff --git a/app/src/transformations/commands/hooks/useRequiredProtocolHardwareFromAnalysis.ts b/app/src/transformations/commands/hooks/useRequiredProtocolHardwareFromAnalysis.ts index 1cc1c77514a..983b58ea313 100644 --- a/app/src/transformations/commands/hooks/useRequiredProtocolHardwareFromAnalysis.ts +++ b/app/src/transformations/commands/hooks/useRequiredProtocolHardwareFromAnalysis.ts @@ -5,16 +5,18 @@ import { import { FLEX_ROBOT_TYPE, FLEX_SINGLE_SLOT_ADDRESSABLE_AREAS, - FLEX_USB_MODULE_ADDRESSABLE_AREAS, + FLEX_STACKER_MODULE_TYPE, getCutoutFixtureIdsForModuleModel, getCutoutFixturesForModuleModel, getCutoutIdForSlotName, getDeckDefFromRobotType, getModuleType, MAGNETIC_BLOCK_TYPE, + WASTE_CHUTE_FLEX_STACKER_FIXTURES, } from '@opentrons/shared-data' import { + getFilteredDeckConfigFixtureCompatibility, useDeckConfigurationCompatibility, useNotifyDeckConfigurationQuery, } from '/app/resources/deck_configuration' @@ -80,6 +82,19 @@ export const useRequiredProtocolHardwareFromAnalysis = ( location.slotName, deckDef ) + const moduleType = getModuleType(model) + const fixtureD3 = deckConfigCompatibility.find( + fixture => fixture.cutoutId === 'cutoutD3' + ) + const comboFixtureId = + moduleType === FLEX_STACKER_MODULE_TYPE && + location.slotName === 'D3' && + fixtureD3 != null && + WASTE_CHUTE_FLEX_STACKER_FIXTURES.includes( + fixtureD3.compatibleCutoutFixtureIds[0] + ) + ? fixtureD3.compatibleCutoutFixtureIds[0] + : null const moduleFixtures = getCutoutFixturesForModuleModel(model, deckDef) const configuredModuleSerialNumber = @@ -101,6 +116,7 @@ export const useRequiredProtocolHardwareFromAnalysis = ( hardwareType: 'module', moduleModel: model, slot: location.slotName, + comboFixtureId, connected: isConnected, hasSlotConflict: deckConfig.some( ({ cutoutId, cutoutFixtureId }) => @@ -137,24 +153,28 @@ export const useRequiredProtocolHardwareFromAnalysis = ( return atLeastOneAA && notOnlySingleSlot } ) + const filteredDeckConfigCompatibility = getFilteredDeckConfigFixtureCompatibility( + requiredDeckConfigCompatibility + ) - const requiredFixtures = requiredDeckConfigCompatibility - // filter out all fixtures that only provide usb module addressable areas - // as they're handled in the requiredModules section via hardwareType === 'module' - .filter( - ({ requiredAddressableAreas }) => - !requiredAddressableAreas.every(modAA => - FLEX_USB_MODULE_ADDRESSABLE_AREAS.includes(modAA) - ) - ) - .map(({ cutoutFixtureId, cutoutId, compatibleCutoutFixtureIds }) => ({ - hardwareType: 'fixture' as const, - cutoutFixtureId: compatibleCutoutFixtureIds[0], - location: { cutout: cutoutId }, - hasSlotConflict: - cutoutFixtureId != null && - !compatibleCutoutFixtureIds.includes(cutoutFixtureId), - })) + const requiredFixtures = filteredDeckConfigCompatibility.map( + ({ + cutoutFixtureId, + compatibleCutoutFixtureIds, + cutoutId, + partialRequiredCutoutFixtureId, + }) => { + return { + hardwareType: 'fixture' as const, + cutoutFixtureId: + partialRequiredCutoutFixtureId ?? compatibleCutoutFixtureIds[0], + location: { cutout: cutoutId }, + hasSlotConflict: + cutoutFixtureId != null && + !compatibleCutoutFixtureIds.includes(cutoutFixtureId), + } + } + ) return { requiredProtocolHardware: [ diff --git a/components/src/assets/localization/en/protocol_command_text.json b/components/src/assets/localization/en/protocol_command_text.json index e793395f289..8af72b98f0c 100644 --- a/components/src/assets/localization/en/protocol_command_text.json +++ b/components/src/assets/localization/en/protocol_command_text.json @@ -93,6 +93,7 @@ "single": "single", "single_nozzle_layout": "single nozzle layout", "slot": "Slot {{slot_name}}", + "stacker_hopper_display": "Stacker {{row}}", "target_temperature": "target temperature", "tc_awaiting_for_duration": "Waiting for Thermocycler profile to complete", "tc_run_profile_steps": "Temperature: {{celsius}}°C, hold time: {{duration}}", diff --git a/components/src/organisms/CommandText/useCommandTextString/utils/__tests__/getLabwareDisplayLocation.test.tsx b/components/src/organisms/CommandText/useCommandTextString/utils/__tests__/getLabwareDisplayLocation.test.tsx index d793f1fc26c..91082fbac4d 100644 --- a/components/src/organisms/CommandText/useCommandTextString/utils/__tests__/getLabwareDisplayLocation.test.tsx +++ b/components/src/organisms/CommandText/useCommandTextString/utils/__tests__/getLabwareDisplayLocation.test.tsx @@ -18,7 +18,10 @@ import { getModuleDisplayLocation } from '../getModuleDisplayLocation' import { getModuleModel } from '../getModuleModel' import type { ComponentProps } from 'react' -import type { LabwareLocation } from '@opentrons/shared-data' +import type { + LabwareLocation, + LabwareLocationSequence, +} from '@opentrons/shared-data' vi.mock('../getModuleModel') vi.mock('../getModuleDisplayLocation') @@ -38,7 +41,7 @@ const TestWrapper = ({ location, params, }: { - location: LabwareLocation | null + location: LabwareLocation | LabwareLocationSequence | null params: any }) => { const { t } = useTranslation('protocol_command_text') @@ -59,128 +62,480 @@ describe('getLabwareDisplayLocation with translations', () => { robotType: FLEX_ROBOT_TYPE, allRunDefs: [], } + const detailLevels = ['full', 'slot-only'] - it('should return an empty string for null location', () => { - render({ location: null, params: defaultParams }) - expect(screen.queryByText(/.+/)).toBeNull() - }) + describe('for LabwareLocation type', () => { + it('should return an empty string for null location', () => { + render({ + location: null, + params: defaultParams, + }) + expect(screen.queryByText(/.+/)).toBeNull() + }) - it('should return "off deck" for offDeck location', () => { - render({ location: 'offDeck', params: defaultParams }) + it('should return "off deck" for offDeck location', () => { + render({ + location: 'offDeck', + params: defaultParams, + }) + screen.getByText('off deck') + }) - screen.getByText('off deck') - }) + it('should return a slot name for slot location', () => { + render({ + location: { slotName: 'A1' }, + params: defaultParams, + }) + screen.getByText('Slot A1') + }) - it('should return a slot name for slot location', () => { - render({ location: { slotName: 'A1' }, params: defaultParams }) + it('should return an addressable area name for an addressable area location', () => { + render({ + location: { addressableAreaName: 'B2' }, + params: defaultParams, + }) + screen.getByText('Slot B2') + }) - screen.getByText('Slot A1') - }) + it('should special case the slotName if it contains "waste chute"', () => { + render({ + location: { slotName: 'gripperWasteChute' }, + params: defaultParams, + }) + screen.getByText('Waste Chute') + }) - it('should return an addressable area name for an addressable area location', () => { - render({ location: { addressableAreaName: 'B2' }, params: defaultParams }) + it('should special case the slotName if it contains "trash bin"', () => { + render({ + location: { slotName: 'trashBin' }, + params: defaultParams, + }) + screen.getByText('Trash Bin') + }) - screen.getByText('Slot B2') - }) + describe('module location', () => { + detailLevels.forEach(detailLevel => { + it(`should return a module location for a module location with detailLevel "${detailLevel}"`, () => { + const mockModuleModel = 'temperatureModuleV2' + vi.mocked(getModuleModel).mockReturnValue(mockModuleModel) + vi.mocked(getModuleDisplayLocation).mockReturnValue('3') + vi.mocked(getModuleDisplayName).mockReturnValue('Temperature Module') + vi.mocked(getModuleType).mockReturnValue('temperatureModuleType') + vi.mocked(getOccludedSlotCountForModule).mockReturnValue(1) - it('should return a module location for a module location', () => { - const mockModuleModel = 'temperatureModuleV2' - vi.mocked(getModuleModel).mockReturnValue(mockModuleModel) - vi.mocked(getModuleDisplayLocation).mockReturnValue('3') - vi.mocked(getModuleDisplayName).mockReturnValue('Temperature Module') - vi.mocked(getModuleType).mockReturnValue('temperatureModuleType') - vi.mocked(getOccludedSlotCountForModule).mockReturnValue(1) + render({ + location: { moduleId: 'temp123' }, + params: { ...defaultParams, detailLevel }, + }) - render({ location: { moduleId: 'temp123' }, params: defaultParams }) + if (detailLevel === 'full') { + screen.getByText('Temperature Module in Slot 3') + } else { + screen.getByText('Slot 3') + } + }) + }) + }) - screen.getByText('Temperature Module in Slot 3') - }) + describe('adapter location', () => { + detailLevels.forEach(detailLevel => { + it(`should return an adapter location for an adapter location with detailLevel "${detailLevel}"`, () => { + const mockLoadedLabwares = [ + { + id: 'adapter123', + definitionUri: 'adapter-uri', + location: { slotName: 'D1' }, + }, + ] + const mockAllRunDefs = [ + { uri: 'adapter-uri', metadata: { displayName: 'Mock Adapter' } }, + ] + vi.mocked(getLabwareDefURI).mockReturnValue('adapter-uri') + vi.mocked(getLabwareDisplayName).mockReturnValue('Mock Adapter') - it('should return an adapter location for an adapter location', () => { - const mockLoadedLabwares = [ - { - id: 'adapter123', - definitionUri: 'adapter-uri', - location: { slotName: 'D1' }, - }, - ] - const mockAllRunDefs = [ - { uri: 'adapter-uri', metadata: { displayName: 'Mock Adapter' } }, - ] - vi.mocked(getLabwareDefURI).mockReturnValue('adapter-uri') - vi.mocked(getLabwareDisplayName).mockReturnValue('Mock Adapter') - - render({ - location: { labwareId: 'adapter123' }, - params: { - ...defaultParams, - loadedLabwares: mockLoadedLabwares, - allRunDefs: mockAllRunDefs, - detailLevel: 'full', - }, + render({ + location: { labwareId: 'adapter123' }, + params: { + ...defaultParams, + loadedLabwares: mockLoadedLabwares, + allRunDefs: mockAllRunDefs, + detailLevel, + }, + }) + + if (detailLevel === 'full') { + screen.getByText('Mock Adapter in Slot D1') + } else { + screen.getByText('Slot D1') + } + }) + }) }) - screen.getByText('Mock Adapter in Slot D1') - }) + describe('adapter on module location', () => { + detailLevels.forEach(detailLevel => { + it(`should handle an adapter on module location with detailLevel "${detailLevel}"`, () => { + const mockLoadedLabwares = [ + { + id: 'adapter123', + definitionUri: 'adapter-uri', + location: { moduleId: 'temp123' }, + }, + ] + const mockLoadedModules = [ + { id: 'temp123', model: 'temperatureModuleV2' }, + ] + const mockAllRunDefs = [ + { uri: 'adapter-uri', metadata: { displayName: 'Mock Adapter' } }, + ] - it('should return a slot-only location when detailLevel is "slot-only"', () => { - render({ - location: { slotName: 'C1' }, - params: { ...defaultParams, detailLevel: 'slot-only' }, - }) + vi.mocked(getLabwareDefURI).mockReturnValue('adapter-uri') + vi.mocked(getModuleModel).mockReturnValue('temperatureModuleV2') + vi.mocked(getLabwareDisplayName).mockReturnValue('Mock Adapter') + vi.mocked(getModuleDisplayLocation).mockReturnValue('2') + vi.mocked(getModuleDisplayName).mockReturnValue('Temperature Module') + vi.mocked(getModuleType).mockReturnValue('temperatureModuleType') + vi.mocked(getOccludedSlotCountForModule).mockReturnValue(1) - screen.getByText('Slot C1') - }) + render({ + location: { labwareId: 'adapter123' }, + params: { + ...defaultParams, + loadedLabwares: mockLoadedLabwares, + loadedModules: mockLoadedModules, + allRunDefs: mockAllRunDefs, + detailLevel, + }, + }) - it('should special case the slotName if it contains "waste chute"', () => { - render({ - location: { slotName: 'gripperWasteChute' }, - params: { ...defaultParams, detailLevel: 'slot-only' }, + if (detailLevel === 'full') { + screen.getByText('Mock Adapter on Temperature Module in Slot 2') + } else { + screen.getByText('Slot 2') + } + }) + }) }) - - screen.getByText('Waste Chute') }) - it('should special case the slotName if it contains "trash bin"', () => { - render({ - location: { slotName: 'trashBin' }, - params: { ...defaultParams, detailLevel: 'slot-only' }, - }) + describe('for LabwareLocationSequence type', () => { + describe('single sequence component tests', () => { + it('should handle onAddressableArea sequence', () => { + const locationSequence: LabwareLocationSequence = [ + { kind: 'onAddressableArea', addressableAreaName: 'A1' }, + { + kind: 'onCutoutFixture', + cutoutId: 'cutoutA1', + possibleCutoutFixtureIds: ['singleLeftSlot'], + }, + ] + render({ + location: locationSequence, + params: defaultParams, + }) + screen.getByText('Slot A1') + }) - screen.getByText('Trash Bin') - }) + it('should handle notOnDeck sequence', () => { + const locationSequence: LabwareLocationSequence = [ + { kind: 'notOnDeck', logicalLocationName: 'offDeck' }, + ] + render({ + location: locationSequence, + params: defaultParams, + }) + screen.getByText('off deck') + }) + + describe('labware on a module', () => { + detailLevels.forEach(detailLevel => { + it(`should handle onModule sequence with detailLevel "${detailLevel}"`, () => { + const locationSequence: LabwareLocationSequence = [ + { + kind: 'onAddressableArea', + addressableAreaName: 'thermocyclerModuleV2', + }, + { kind: 'onModule', moduleId: 'mockModuleId' }, + { + kind: 'onCutoutFixture', + cutoutId: 'cutoutId', + possibleCutoutFixtureIds: ['thermocyclerModuleV2Front'], + }, + ] + + vi.mocked(getModuleModel).mockReturnValue('thermocyclerModuleV2') + vi.mocked(getModuleDisplayLocation).mockReturnValue('B1') + vi.mocked(getModuleDisplayName).mockReturnValue( + 'Thermocycler Module' + ) + vi.mocked(getModuleType).mockReturnValue('thermocyclerModuleType') + + render({ + location: locationSequence, + params: { ...defaultParams, detailLevel }, + }) + + if (detailLevel === 'full') { + screen.getByText('Thermocycler Module in Slot A1+B1') + } else { + screen.getByText('Slot A1+B1') + } + }) + }) + + detailLevels.forEach(detailLevel => { + it(`should handle labware on a stacker module with detailLevel "${detailLevel}"`, () => { + const locationSequence: LabwareLocationSequence = [ + { + kind: 'onAddressableArea', + addressableAreaName: 'flexStackerModuleV1D4', + }, + { kind: 'onModule', moduleId: 'mockModuleId' }, + { + kind: 'onCutoutFixture', + cutoutId: 'cutoutD3', + possibleCutoutFixtureIds: [ + 'flexStackerModuleV1WithWasteChuteRightAdapterNoCover', + ], + }, + ] + vi.mocked(getModuleModel).mockReturnValue('flexStackerModuleV1') + vi.mocked(getModuleDisplayLocation).mockReturnValue('D3') + vi.mocked(getModuleDisplayName).mockReturnValue('Flex Stacker') + vi.mocked(getModuleType).mockReturnValue('flexStackerModuleType') - it('should handle an adapter on module location when the detail level is full', () => { - const mockLoadedLabwares = [ - { - id: 'adapter123', - definitionUri: 'adapter-uri', - location: { moduleId: 'temp123' }, - }, - ] - const mockLoadedModules = [{ id: 'temp123', model: 'temperatureModuleV2' }] - const mockAllRunDefs = [ - { uri: 'adapter-uri', metadata: { displayName: 'Mock Adapter' } }, - ] - - vi.mocked(getLabwareDefURI).mockReturnValue('adapter-uri') - vi.mocked(getLabwareDisplayName).mockReturnValue('Mock Adapter') - vi.mocked(getModuleDisplayLocation).mockReturnValue('2') - vi.mocked(getModuleDisplayName).mockReturnValue('Temperature Module') - vi.mocked(getModuleType).mockReturnValue('temperatureModuleType') - vi.mocked(getOccludedSlotCountForModule).mockReturnValue(1) - - render({ - location: { labwareId: 'adapter123' }, - params: { - ...defaultParams, - loadedLabwares: mockLoadedLabwares, - loadedModules: mockLoadedModules, - allRunDefs: mockAllRunDefs, - detailLevel: 'full', - }, + render({ + location: locationSequence, + params: { ...defaultParams, detailLevel }, + }) + + screen.getByText('Slot D4') + }) + }) + + detailLevels.forEach(detailLevel => { + it(`should handle labware in stacker hopper with detailLevel "${detailLevel}"`, () => { + const locationSequence: LabwareLocationSequence = [ + { kind: 'inStackerHopper', moduleId: 'UUID' }, + ] + + vi.mocked(getModuleModel).mockReturnValue('flexStackerModuleV1') + vi.mocked(getModuleDisplayLocation).mockReturnValue('D3') + vi.mocked(getModuleDisplayName).mockReturnValue('Flex Stacker') + vi.mocked(getModuleType).mockReturnValue('flexStackerModuleType') + + render({ + location: locationSequence, + params: { ...defaultParams, detailLevel }, + }) + + screen.getByText('Stacker D') + }) + }) + }) + + describe('labware on another labware', () => { + detailLevels.forEach(detailLevel => { + it(`should handle onLabware sequence with detailLevel "${detailLevel}"`, () => { + const locationSequence: LabwareLocationSequence = [ + { kind: 'onLabware', labwareId: 'labwareABC', lidId: null }, + { kind: 'onAddressableArea', addressableAreaName: 'A3' }, + { + kind: 'onCutoutFixture', + cutoutId: 'cutoutA3', + possibleCutoutFixtureIds: [ + 'flexStackerModuleV1', + 'singleRightSlot', + ], + }, + ] + const mockLoadedLabwares = [ + { + id: 'labwareABC', + definitionUri: 'labware-uri', + location: { slotName: 'A3' }, + }, + ] + const mockAllRunDefs = [ + { uri: 'labware-uri', metadata: { displayName: 'Mock Labware' } }, + ] + + vi.mocked(getLabwareDefURI).mockReturnValue('labware-uri') + vi.mocked(getLabwareDisplayName).mockReturnValue('Mock Labware') + + render({ + location: locationSequence, + params: { + ...defaultParams, + detailLevel, + loadedLabwares: mockLoadedLabwares, + allRunDefs: mockAllRunDefs, + }, + }) + + if (detailLevel === 'full') { + screen.getByText('Mock Labware in Slot A3') + } else { + screen.getByText('Slot A3') + } + }) + }) + }) }) - screen.getByText('Mock Adapter on Temperature Module in Slot 2') + describe('complex sequence component tests', () => { + describe('labware on module sequence', () => { + detailLevels.forEach(detailLevel => { + it(`should handle labware on module sequence with detailLevel "${detailLevel}"`, () => { + const locationSequence: LabwareLocationSequence = [ + { kind: 'onLabware', labwareId: 'adapter1234', lidId: null }, + { + addressableAreaName: 'temperatureModuleV2C1', + kind: 'onAddressableArea', + }, + { kind: 'onModule', moduleId: 'temp123' }, + { + cutoutId: 'cutoutC1', + kind: 'onCutoutFixture', + possibleCutoutFixtureIds: ['temperatureModuleV2'], + }, + ] + + const mockLoadedLabwares = [ + { + id: 'adapter1234', + definitionUri: 'adapter-uri', + location: { moduleId: 'temp123' }, + }, + ] + const mockLoadedModules = [ + { id: 'temp123', model: 'temperatureModuleV2' }, + ] + const mockAllRunDefs = [ + { uri: 'adapter-uri', metadata: { displayName: 'Mock Adapter' } }, + ] + + vi.mocked(getLabwareDefURI).mockReturnValue('adapter-uri') + vi.mocked(getLabwareDisplayName).mockReturnValue('Mock Adapter') + vi.mocked(getModuleModel).mockReturnValue('temperatureModuleV2') + vi.mocked(getModuleDisplayLocation).mockReturnValue('C1') + vi.mocked(getModuleDisplayName).mockReturnValue( + 'Temperature Module' + ) + vi.mocked(getModuleType).mockReturnValue('temperatureModuleType') + vi.mocked(getOccludedSlotCountForModule).mockReturnValue(1) + + render({ + location: locationSequence, + params: { + ...defaultParams, + detailLevel, + loadedLabwares: mockLoadedLabwares, + loadedModules: mockLoadedModules, + allRunDefs: mockAllRunDefs, + }, + }) + + if (detailLevel === 'full') { + screen.getByText('Mock Adapter on Temperature Module in Slot C1') + } else { + screen.getByText('Slot C1') + } + }) + }) + }) + + it('should only display the top labware in multiple labware stacking sequence', () => { + const locationSequence: LabwareLocationSequence = [ + { kind: 'onLabware', labwareId: 'topLabware', lidId: null }, + { kind: 'onLabware', labwareId: 'adapter123', lidId: null }, + { kind: 'onAddressableArea', addressableAreaName: 'A3' }, + { + kind: 'onCutoutFixture', + cutoutId: 'cutoutA3', + possibleCutoutFixtureIds: [ + 'flexStackerModuleV1', + 'singleRightSlot', + ], + }, + ] + + const mockLoadedLabwares = [ + { + id: 'topLabware', + definitionUri: 'top-labware-uri', + location: { labwareId: 'adapter123' }, + }, + { + id: 'adapter123', + definitionUri: 'adapter-uri', + location: { slotName: 'A3' }, + }, + ] + const mockAllRunDefs = [ + { uri: 'top-labware-uri', metadata: { displayName: 'Top Labware' } }, + { uri: 'adapter-uri', metadata: { displayName: 'Mock Adapter' } }, + ] + + vi.mocked(getLabwareDefURI) + .mockReturnValueOnce('top-labware-uri') + .mockReturnValueOnce('adapter-uri') + vi.mocked(getLabwareDisplayName) + .mockReturnValueOnce('Top Labware') + .mockReturnValueOnce('Mock Adapter') + + render({ + location: locationSequence, + params: { + ...defaultParams, + detailLevel: 'full', + loadedLabwares: mockLoadedLabwares, + allRunDefs: mockAllRunDefs, + }, + }) + + screen.getByText('Top Labware in Slot A3') + }) + + it('should handle waste chute sequence', () => { + const locationSequence: LabwareLocationSequence = [ + { + addressableAreaName: 'gripperWasteChute', + kind: 'onAddressableArea', + }, + { + cutoutId: 'cutoutD3', + kind: 'onCutoutFixture', + possibleCutoutFixtureIds: [ + 'stagingAreaSlotWithWasteChuteRightAdapterNoCover', + ], + }, + ] + + render({ + location: locationSequence, + params: defaultParams, + }) + + screen.getByText('Waste Chute') + }) + + it('should handle trash bin sequence', () => { + const locationSequence: LabwareLocationSequence = [ + { kind: 'onAddressableArea', addressableAreaName: 'movableTrashA3' }, + { + kind: 'onCutoutFixture', + cutoutId: 'cutoutA3', + possibleCutoutFixtureIds: ['movableTrashA3'], + }, + ] + + render({ + location: locationSequence, + params: defaultParams, + }) + screen.getByText('Trash Bin') + }) + }) }) }) diff --git a/components/src/organisms/CommandText/useCommandTextString/utils/commandText/getLoadCommandText.ts b/components/src/organisms/CommandText/useCommandTextString/utils/commandText/getLoadCommandText.ts index 93bcef5b204..e79e1c49a95 100644 --- a/components/src/organisms/CommandText/useCommandTextString/utils/commandText/getLoadCommandText.ts +++ b/components/src/organisms/CommandText/useCommandTextString/utils/commandText/getLoadCommandText.ts @@ -2,12 +2,11 @@ import find from 'lodash/find' import { getAllLiquidClassDefs, + getModuleDeckLabel, getModuleDisplayName, getModuleType, getOccludedSlotCountForModule, getPipetteSpecsV2, - THERMOCYCLER_MODULE_V1, - THERMOCYCLER_MODULE_V2, } from '@opentrons/shared-data' import { getLabwareDisplayLocation } from '../getLabwareDisplayLocation' @@ -42,21 +41,18 @@ export const getLoadCommandText = ({ }) } case 'loadModule': { + const moduleType = getModuleType(command.params.model) const occludedSlotCount = getOccludedSlotCountForModule( - getModuleType(command.params.model), + moduleType, robotType ) - let slotName = command.params.location.slotName - if ( - THERMOCYCLER_MODULE_V2 === command.params.model || - THERMOCYCLER_MODULE_V1 === command.params.model - ) { - slotName = 'A1 + B1' - } return t('load_module_protocol_setup', { count: occludedSlotCount, module: getModuleDisplayName(command.params.model), - slot_name: slotName, + slot_name: getModuleDeckLabel( + moduleType, + command.params.location.slotName + ), }) } case 'loadLid': diff --git a/components/src/organisms/CommandText/useCommandTextString/utils/getLabwareDisplayLocation.tsx b/components/src/organisms/CommandText/useCommandTextString/utils/getLabwareDisplayLocation.tsx index 07102895082..44b5a1c84cd 100644 --- a/components/src/organisms/CommandText/useCommandTextString/utils/getLabwareDisplayLocation.tsx +++ b/components/src/organisms/CommandText/useCommandTextString/utils/getLabwareDisplayLocation.tsx @@ -1,11 +1,10 @@ import { - FLEX_STACKER_MODULE_V1, + FLEX_STACKER_MODULE_TYPE, + getModuleDeckLabel, getModuleDisplayName, getModuleType, getOccludedSlotCountForModule, MOVABLE_TRASH_ADDRESSABLE_AREAS, - THERMOCYCLER_MODULE_V1, - THERMOCYCLER_MODULE_V2, TRASH_BIN_FIXTURE, WASTE_CHUTE_ADDRESSABLE_AREAS, } from '@opentrons/shared-data' @@ -30,6 +29,7 @@ export interface DisplayLocationSlotOnlyParams extends Omit { t: TFunction isOnDevice?: boolean + includeSlotText?: boolean location?: LabwareLocation | LabwareLocationSequence | null } export interface DisplayLocationFullParams @@ -46,7 +46,8 @@ export type DisplayLocationParams = // labware, ex, "in module XYZ in slot C1". // If 'slot-only', return only the slot name, ex "in slot C1". export function getLabwareDisplayLocation( - params: DisplayLocationParams + params: DisplayLocationParams, + includeSlotText: boolean = true ): string { const { t, isOnDevice = false, location } = params const locationResult = Array.isArray(location) @@ -62,12 +63,7 @@ export function getLabwareDisplayLocation( return '' } - const { slotName: initialSlotName, moduleModel, adapterName } = locationResult - const slotName = - moduleModel === THERMOCYCLER_MODULE_V1 || - moduleModel === THERMOCYCLER_MODULE_V2 - ? 'A1+B1' - : initialSlotName + const { slotName, moduleModel, adapterName } = locationResult if (slotName === 'offDeck' || slotName === 'systemLocation') { return t('off_deck') @@ -79,41 +75,32 @@ export function getLabwareDisplayLocation( // Simple slot location else if (moduleModel == null && adapterName == null) { const validatedSlotCopy = handleSpecialSlotNames(slotName, t) - return isOnDevice ? validatedSlotCopy.odd : validatedSlotCopy.desktop + return isOnDevice || !includeSlotText + ? validatedSlotCopy.odd + : validatedSlotCopy.desktop } // Module location without adapter else if (moduleModel != null && adapterName == null) { + const moduleSlot = getModuleDeckLabel(getModuleType(moduleModel), slotName) + if (getModuleType(moduleModel) === FLEX_STACKER_MODULE_TYPE) { + // in hopper location always return Stacker {{row}} + return t('stacker_hopper_display', { + row: getSlotRow(moduleSlot as string), + }) + } if (params.detailLevel === 'slot-only') { - switch (moduleModel) { - case THERMOCYCLER_MODULE_V1: - case THERMOCYCLER_MODULE_V2: - return t('slot', { slot_name: 'A1+B1' }) - case FLEX_STACKER_MODULE_V1: - return t('stacker_display_name', { - stacker_slot: getSlotColumn(slotName), - }) - default: - return t('slot', { slot_name: slotName }) - } - } else { - switch (moduleModel) { - case FLEX_STACKER_MODULE_V1: - return t('stacker_column_display_name', { - stacker_slot: getSlotColumn(slotName), - }) - default: - return isOnDevice - ? `${getModuleDisplayName(moduleModel)}, ${slotName}` - : t('module_in_slot', { - count: getOccludedSlotCountForModule( - getModuleType(moduleModel), - params.robotType - ), - module: getModuleDisplayName(moduleModel), - slot_name: slotName, - }) - } + return includeSlotText ? t('slot', { slot_name: moduleSlot }) : moduleSlot } + return isOnDevice + ? `${getModuleDisplayName(moduleModel)}, ${moduleSlot}` + : t('module_in_slot', { + count: getOccludedSlotCountForModule( + getModuleType(moduleModel), + params.robotType + ), + module: getModuleDisplayName(moduleModel), + slot_name: moduleSlot, + }) } // Adapter locations else if (adapterName != null) { @@ -138,7 +125,7 @@ export function getLabwareDisplayLocation( } } -function getSlotColumn(slotName: string): string { +function getSlotRow(slotName: string): string { return slotName.charAt(0) } diff --git a/components/src/organisms/CommandText/useCommandTextString/utils/getLabwareLocation.tsx b/components/src/organisms/CommandText/useCommandTextString/utils/getLabwareLocation.tsx index de72d1b39b2..7025531cdac 100644 --- a/components/src/organisms/CommandText/useCommandTextString/utils/getLabwareLocation.tsx +++ b/components/src/organisms/CommandText/useCommandTextString/utils/getLabwareLocation.tsx @@ -1,9 +1,11 @@ import { + FLEX_STACKER_MODULE_TYPE, FLEX_STACKER_MODULE_V1, getCutoutDisplayName, getLabwareDefURI, getLabwareDisplayName, getModuleModelFromAddressableArea, + getModuleType, getSlotFromAddressableAreaName, MOVABLE_TRASH_ADDRESSABLE_AREAS, WASTE_CHUTE_ADDRESSABLE_AREAS, @@ -130,9 +132,21 @@ export function getLabwareLocationFromSequence( : moduleModel ?? undefined, } } - } - // TODO(tz, 4-16-25): add inHopperLocation when logic is merged - else if (detailLevel === 'full') { + } else if (sequenceItem.kind === 'inStackerHopper') { + const moduleModel = getModuleModel(loadedModules, sequenceItem.moduleId) + if (moduleModel == null) { + console.error('labware is located on an unknown module model') + } else { + return { + ...acc, + slotName: getModuleDisplayLocation( + loadedModules, + sequenceItem.moduleId + ), + moduleModel, + } + } + } else if (detailLevel === 'full') { const { allRunDefs } = params as SequenceFullParams if (sequenceItem.kind === 'onLabware' && acc.adapterName == null) { if (!Array.isArray(loadedLabwares)) { @@ -195,7 +209,10 @@ export function getLabwareLocation( return { slotName, - moduleModel, + moduleModel: + getModuleType(moduleModel) === FLEX_STACKER_MODULE_TYPE + ? undefined + : moduleModel, } } else if ('labwareId' in location) { if (!Array.isArray(loadedLabwares)) { diff --git a/components/src/organisms/CommandText/useCommandTextString/utils/getModuleDisplayLocation.ts b/components/src/organisms/CommandText/useCommandTextString/utils/getModuleDisplayLocation.ts index 878dbbf6ab9..fc528fdaeeb 100644 --- a/components/src/organisms/CommandText/useCommandTextString/utils/getModuleDisplayLocation.ts +++ b/components/src/organisms/CommandText/useCommandTextString/utils/getModuleDisplayLocation.ts @@ -1,3 +1,5 @@ +import { FLEX_STACKER_MODULE_TYPE, getModuleType } from '@opentrons/shared-data' + import { getLoadedModule } from './getLoadedModule' import type { LoadedModules } from './types' @@ -7,5 +9,12 @@ export function getModuleDisplayLocation( moduleId: string ): string { const loadedModule = getLoadedModule(loadedModules, moduleId) - return loadedModule != null ? loadedModule.location.slotName : '' + if (loadedModule == null) { + console.warn(`Module with ID ${moduleId} not found in loaded modules`) + return '' + } + const slotName = loadedModule.location.slotName + return getModuleType(loadedModule.model) === FLEX_STACKER_MODULE_TYPE + ? `${slotName[0]}4` + : slotName } diff --git a/shared-data/js/helpers/getModuleDeckLabel.ts b/shared-data/js/helpers/getModuleDeckLabel.ts new file mode 100644 index 00000000000..d8890c7599a --- /dev/null +++ b/shared-data/js/helpers/getModuleDeckLabel.ts @@ -0,0 +1,41 @@ +import { + FLEX_STACKER_MODULE_TYPE, + TC_MODULE_LOCATION_OT2, + TC_MODULE_LOCATION_OT3, + THERMOCYCLER_MODULE_TYPE, +} from '..' + +import type { ModuleType } from '../types' + +const getFlexStackerDisplayLocationFromSlotName = ( + slotName: string +): string => { + return `${slotName}+${slotName[0]}4` +} + +/** + * Returns a string to represent a module's deck location, to be used in Deck Configuration and Protocol Setup. + * @param moduleType - The type of the module. + * @param slotName - The slot name where the module is located. + * @returns A string representing the module's deck label. + * + * For all labware loaded on/offdeck or on module, use `getLabwareDeckLabel` instead. + */ +export function getModuleDeckLabel( + moduleType: ModuleType, + slotName: string +): string { + switch (moduleType) { + case THERMOCYCLER_MODULE_TYPE: + if (slotName === '7') { + // OT-2 thermocycler module slotName is always '7' + return TC_MODULE_LOCATION_OT2 + } else { + return TC_MODULE_LOCATION_OT3 + } + case FLEX_STACKER_MODULE_TYPE: + return getFlexStackerDisplayLocationFromSlotName(slotName) + default: + return slotName + } +} diff --git a/shared-data/js/helpers/index.ts b/shared-data/js/helpers/index.ts index ba6ec141768..4ba5b7e5b0b 100644 --- a/shared-data/js/helpers/index.ts +++ b/shared-data/js/helpers/index.ts @@ -60,6 +60,7 @@ export * from './getLiquidsByIdForLabware' export * from './getStackedItemsOnStartingDeck' export * from './getStandardDeckViewLayerBlockList' export * from './getWellFillFromLabwareId' +export * from './getModuleDeckLabel' export const getLabwareDefIsStandard = (def: LabwareDefinition): boolean => def?.namespace === OPENTRONS_LABWARE_NAMESPACE diff --git a/shared-data/js/helpers/parseProtocolCommands.ts b/shared-data/js/helpers/parseProtocolCommands.ts index fe1500ba2cd..9a8822c9e18 100644 --- a/shared-data/js/helpers/parseProtocolCommands.ts +++ b/shared-data/js/helpers/parseProtocolCommands.ts @@ -2,7 +2,9 @@ import reduce from 'lodash/reduce' import { DEFAULT_LIQUID_COLORS } from '../constants' +import { getModuleType } from '../modules' import { getLabwareDefURI } from './getLabwareDefURI' +import { getModuleDeckLabel } from './getModuleDeckLabel' import type { LabwareLocation, @@ -336,7 +338,13 @@ export function parseInitialLoadedModulesBySlot( loadModuleCommandsReversed, (acc, command) => 'slotName' in command.params.location - ? { ...acc, [command.params.location.slotName]: command } + ? { + ...acc, + [getModuleDeckLabel( + getModuleType(command.params.model), + command.params.location.slotName + )]: command, + } : acc, {} )