diff --git a/components/src/hardware-sim/DeckConfigurator/index.tsx b/components/src/hardware-sim/DeckConfigurator/index.tsx index 4a03ff6c866..65bb6968a45 100644 --- a/components/src/hardware-sim/DeckConfigurator/index.tsx +++ b/components/src/hardware-sim/DeckConfigurator/index.tsx @@ -38,6 +38,8 @@ import { MagneticBlockFixture } from './MagneticBlockFixture' import { ThermocyclerFixture } from './ThermocyclerFixture' import { AbsorbanceReaderFixture } from './AbsorbanceReaderFixture' +export * from './constants' + interface DeckConfiguratorProps { deckConfig: DeckConfiguration handleClickAdd: (cutoutId: CutoutId) => void diff --git a/components/src/molecules/DropdownMenu/index.tsx b/components/src/molecules/DropdownMenu/index.tsx index 3fb38f8f531..3b692c0b3dc 100644 --- a/components/src/molecules/DropdownMenu/index.tsx +++ b/components/src/molecules/DropdownMenu/index.tsx @@ -24,6 +24,7 @@ import { MenuItem } from '../../atoms/MenuList/MenuItem' import { Tooltip } from '../../atoms/Tooltip' import { StyledText } from '../../atoms/StyledText' import { LiquidIcon } from '../LiquidIcon' +import { DeckInfoLabel } from '../DeckInfoLabel' export interface DropdownOption { name: string @@ -32,6 +33,8 @@ export interface DropdownOption { liquidColor?: string /** optional dropdown option for adding the deck label */ deckLabel?: string + /** subtext below the name */ + subtext?: string disabled?: boolean tooltipText?: string } @@ -250,7 +253,11 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { {currentOption.liquidColor != null ? ( ) : null} + {currentOption.deckLabel != null ? ( + + ) : null} ) : null} - {option.name} + {option.deckLabel != null ? ( + + ) : null} + + + {option.name} + + + {option.subtext} + + {option.tooltipText != null ? ( diff --git a/protocol-designer/src/molecules/DropdownStepFormField/index.tsx b/protocol-designer/src/molecules/DropdownStepFormField/index.tsx index 2714a73156b..d40fcc7063f 100644 --- a/protocol-designer/src/molecules/DropdownStepFormField/index.tsx +++ b/protocol-designer/src/molecules/DropdownStepFormField/index.tsx @@ -1,8 +1,10 @@ import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' import { + ALIGN_CENTER, COLORS, DIRECTION_COLUMN, + DeckInfoLabel, DropdownMenu, Flex, ListItem, @@ -108,10 +110,28 @@ export function DropdownStepFormField( {title} - - - {options[0].name} - + + {options[0].deckLabel != null ? ( + + ) : null} + + + {options[0].name} + + + {options[0].subtext} + + diff --git a/protocol-designer/src/organisms/MaterialsListModal/__tests__/MaterialsListModal.test.tsx b/protocol-designer/src/organisms/MaterialsListModal/__tests__/MaterialsListModal.test.tsx index d18fa1e52de..26e2d0fb098 100644 --- a/protocol-designer/src/organisms/MaterialsListModal/__tests__/MaterialsListModal.test.tsx +++ b/protocol-designer/src/organisms/MaterialsListModal/__tests__/MaterialsListModal.test.tsx @@ -137,7 +137,7 @@ describe('MaterialsListModal', () => { lidTargetTemp: null, lidOpen: false, }, - slot: 'span7_8_10_11', + slot: '7', type: 'thermocyclerModuleType', }, ] as ModuleOnDeck[] diff --git a/protocol-designer/src/top-selectors/labware-locations/index.ts b/protocol-designer/src/top-selectors/labware-locations/index.ts index 34cd5fad561..6a2c5143b00 100644 --- a/protocol-designer/src/top-selectors/labware-locations/index.ts +++ b/protocol-designer/src/top-selectors/labware-locations/index.ts @@ -166,8 +166,8 @@ export const getUnoccupiedLabwareLocationOptions: Selector< { name: modIdWithAdapter != null - ? `${adapterDisplayName} on top of ${moduleUnderAdapter} in slot ${moduleSlotInfo}` - : `${adapterDisplayName} on slot ${adapterSlotInfo}`, + ? `${moduleSlotInfo} on ${moduleUnderAdapter} with ${adapterDisplayName}` + : `${adapterSlotInfo} with ${adapterDisplayName}`, value: labwareId, }, ] @@ -186,13 +186,9 @@ export const getUnoccupiedLabwareLocationOptions: Selector< : [ ...acc, { - name: `${getModuleDisplayName( + name: `${modOnDeck.slot} on ${getModuleDisplayName( moduleEntities[modId].model - )} in slot ${ - modOnDeck.slot === 'span7_8_10_11' - ? '7, 8, 10, 11' - : modOnDeck.slot - }`, + )}`, value: modId, }, ] @@ -234,7 +230,7 @@ export const getUnoccupiedLabwareLocationOptions: Selector< ) }) .map(slotId => ({ name: slotId, value: slotId })) - const offDeck = { name: 'Off-Deck', value: 'offDeck' } + const offDeck = { name: 'Off-deck', value: 'offDeck' } const wasteChuteSlot = { name: 'Waste Chute in D3', value: WASTE_CHUTE_CUTOUT, diff --git a/protocol-designer/src/ui/labware/__tests__/selectors.test.ts b/protocol-designer/src/ui/labware/__tests__/selectors.test.ts index 00359673dbb..e2c74b75508 100644 --- a/protocol-designer/src/ui/labware/__tests__/selectors.test.ts +++ b/protocol-designer/src/ui/labware/__tests__/selectors.test.ts @@ -9,7 +9,6 @@ import { THERMOCYCLER_MODULE_TYPE, THERMOCYCLER_MODULE_V1, } from '@opentrons/shared-data' -import { SPAN7_8_10_11_SLOT } from '../../../constants' import { getDisposalOptions, getLabwareOptions, @@ -102,7 +101,7 @@ describe('labware selectors', () => { expect( // @ts-expect-error(sa, 2021-6-15): resultFunc getDisposalOptions.resultFunc(additionalEquipmentEntities) - ).toEqual([{ name: 'Trash Bin', value: mockTrashId }]) + ).toEqual([{ name: 'Trash bin', value: mockTrashId }]) }) it('filters out additional equipment that is NOT trash when multiple trash bins present', () => { const mockTrashId = 'mockTrashId' @@ -129,8 +128,8 @@ describe('labware selectors', () => { // @ts-expect-error(sa, 2021-6-15): resultFunc getDisposalOptions.resultFunc(additionalEquipmentEntities) ).toEqual([ - { name: 'Trash Bin', value: mockTrashId }, - { name: 'Trash Bin', value: mockTrashId2 }, + { name: 'Trash bin', value: mockTrashId }, + { name: 'Trash bin', value: mockTrashId2 }, ]) }) }) @@ -142,7 +141,12 @@ describe('labware selectors', () => { getLabwareOptions.resultFunc( {}, {}, - { labware: {}, modules: {}, pipettes: {} }, + { + labware: {}, + modules: {}, + pipettes: {}, + additionalEquipmentOnDeck: {}, + }, {}, {}, {} @@ -153,13 +157,13 @@ describe('labware selectors', () => { it('should return labware options when no modules are present, with no tipracks', () => { const labwareEntities = { ...tipracks, - ...trash, ...otherLabware, } const initialDeckSetup = { labware: labwareEntities, modules: {}, pipettes: {}, + additionalEquipmentOnDeck: {}, } expect( // @ts-expect-error(sa, 2021-6-15): resultFunc @@ -171,13 +175,10 @@ describe('labware selectors', () => { {}, {} ) - ).toEqual([ - { name: 'Source Plate', value: 'wellPlateId' }, - { name: 'Trash', value: mockTrash }, - ]) + ).toEqual([{ name: 'Source Plate', value: 'wellPlateId' }]) }) - it('should return labware options with module prefixes when a labware is on module', () => { + it('should return labware options with no module prefixes even when a labware is on module', () => { const labware = { wellPlateId: { ...otherLabware.wellPlateId, @@ -206,6 +207,9 @@ describe('labware selectors', () => { ...trash, ...labware, }, + additionalEquipmentOnDeck: { + trash: { id: 'trash', location: 'cutout12', name: 'trashBin' }, + }, modules: { magModuleId: { id: 'magModuleId', @@ -223,7 +227,7 @@ describe('labware selectors', () => { id: 'thermocyclerId', type: THERMOCYCLER_MODULE_TYPE, model: THERMOCYCLER_MODULE_V1, - slot: SPAN7_8_10_11_SLOT, + slot: '8', }, heaterShakerId: { id: 'heaterShakerId', @@ -253,11 +257,11 @@ describe('labware selectors', () => { {} ) ).toEqual([ - { name: 'HS Plate in Heater-Shaker', value: 'hsPlateId' }, - { name: 'TC Plate in Thermocycler', value: 'tcPlateId' }, - { name: 'Temp Plate in Temperature Module', value: 'tempPlateId' }, + { name: 'HS Plate in 6', value: 'hsPlateId' }, + { name: 'TC Plate in A1+B1', value: 'tcPlateId' }, + { name: 'Temp Plate in 3', value: 'tempPlateId' }, { name: 'Trash', value: mockTrash }, - { name: 'Well Plate in Magnetic Module', value: 'wellPlateId' }, + { name: 'Well Plate in 1', value: 'wellPlateId' }, ]) }) @@ -272,7 +276,6 @@ describe('labware selectors', () => { const initialDeckSetup = { pipettes: {}, labware: { - ...trash, ...labware, }, modules: { @@ -283,6 +286,9 @@ describe('labware selectors', () => { slot: '1', }, }, + additionalEquipmentOnDeck: { + trash: { id: 'trash', name: 'trashBin', location: 'cutout12' }, + }, } const nicknames: Record = { @@ -312,14 +318,14 @@ describe('labware selectors', () => { ) ).toEqual([ { name: 'Trash', value: mockTrash }, - { name: 'Well Plate in Magnetic Module', value: 'wellPlateId' }, + { name: 'Well Plate in 1', value: 'wellPlateId' }, ]) }) }) describe('_sortLabwareDropdownOptions', () => { const trashOption = { - name: 'Trash Bin', + name: 'Trash bin', value: mockTrash, } const zzzPlateOption = { name: 'Zzz Plate', value: 'zzz' } diff --git a/protocol-designer/src/ui/labware/selectors.ts b/protocol-designer/src/ui/labware/selectors.ts index 2839d001078..34d5818611f 100644 --- a/protocol-designer/src/ui/labware/selectors.ts +++ b/protocol-designer/src/ui/labware/selectors.ts @@ -1,25 +1,32 @@ import { createSelector } from 'reselect' import mapValues from 'lodash/mapValues' import reduce from 'lodash/reduce' -import { getIsTiprack, getLabwareDisplayName } from '@opentrons/shared-data' +import { + TRASH_BIN_DISPLAY_NAME, + WASTE_CHUTE_DISPLAY_NAME, +} from '@opentrons/components' +import { + FLEX_ROBOT_TYPE, + OT2_ROBOT_TYPE, + getIsTiprack, + getLabwareDisplayName, +} from '@opentrons/shared-data' import * as stepFormSelectors from '../../step-forms/selectors' import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' -import { getModuleShortNames, getModuleUnderLabware } from '../modules/utils' -import { getLabwareOffDeck, getLabwareInColumn4 } from './utils' +import { getLabwareLatestSlot } from './utils' import type { LabwareEntity, AdditionalEquipmentEntity, } from '@opentrons/step-generation' import type { DropdownOption } from '@opentrons/components' +import type { RobotType } from '@opentrons/shared-data' import type { Selector } from '../../types' import type { AllTemporalPropertiesForTimelineFrame, SavedStepFormState, } from '../../step-forms' -const TRASH = 'Trash Bin' - export const getLabwareNicknamesById: Selector< Record > = createSelector( @@ -37,8 +44,8 @@ export const _sortLabwareDropdownOptions = ( ): DropdownOption[] => options.sort((a, b) => { // special case for trash (always at the bottom of the list) - if (a.name === TRASH) return 1 - if (b.name === TRASH) return -1 + if (a.name === TRASH_BIN_DISPLAY_NAME) return 1 + if (b.name === TRASH_BIN_DISPLAY_NAME) return -1 // sort by name everything else by name return a.name.localeCompare(b.name) }) @@ -47,35 +54,21 @@ const getNickname = ( nicknamesById: Record, initialDeckSetup: AllTemporalPropertiesForTimelineFrame, labwareId: string, - savedStepForms: SavedStepFormState + savedStepForms: SavedStepFormState, + robotType: RobotType ): string => { - const isOffDeck = getLabwareOffDeck( + const latestSlot = getLabwareLatestSlot( initialDeckSetup, savedStepForms ?? {}, - labwareId - ) - - const moduleOnDeck = getModuleUnderLabware( - initialDeckSetup, - savedStepForms ?? {}, - labwareId - ) - const module = - moduleOnDeck != null ? getModuleShortNames(moduleOnDeck.type) : null - - const isLabwareInColumn4 = getLabwareInColumn4( - initialDeckSetup, - savedStepForms ?? {}, - labwareId + labwareId, + robotType ) let nickName: string = nicknamesById[labwareId] - if (module != null) { - nickName = `${nicknamesById[labwareId]} in ${module}` - } else if (isOffDeck) { + if (latestSlot != null && latestSlot !== 'offDeck') { + nickName = `${nicknamesById[labwareId]} in ${latestSlot}` + } else if (latestSlot != null && latestSlot === 'offDeck') { nickName = `${nicknamesById[labwareId]} off-deck` - } else if (isLabwareInColumn4) { - nickName = `${nicknamesById[labwareId]} in staging area slot` } return nickName } @@ -110,6 +103,12 @@ export const getMoveLabwareOptions: Selector = createSelector( const wasteChuteLocation = Object.values(additionalEquipmentEntities).find( aE => aE.name === 'wasteChute' )?.location + const trashBinLocation = Object.values(additionalEquipmentEntities).find( + aE => aE.name === 'trashBin' + )?.location + const robotType = + trashBinLocation === 'cutout12' ? OT2_ROBOT_TYPE : FLEX_ROBOT_TYPE + const moveLabwareOptions = reduce( labwareEntities, ( @@ -131,7 +130,8 @@ export const getMoveLabwareOptions: Selector = createSelector( nicknamesById, initialDeckSetup, labwareId, - savedStepForms + savedStepForms, + robotType ) // filter out moving trash, adapters, and labware in @@ -171,6 +171,12 @@ export const getLabwareOptions: Selector = createSelector( const wasteChuteLocation = Object.values(additionalEquipmentEntities).find( aE => aE.name === 'wasteChute' )?.location + const trashBinLocation = Object.values(additionalEquipmentEntities).find( + aE => aE.name === 'trashBin' + )?.location + const robotType = + trashBinLocation === 'cutout12' ? OT2_ROBOT_TYPE : FLEX_ROBOT_TYPE + const labwareOptions = reduce( labwareEntities, ( @@ -191,7 +197,8 @@ export const getLabwareOptions: Selector = createSelector( nicknamesById, initialDeckSetup, labwareId, - savedStepForms + savedStepForms, + robotType ) return getIsTiprack(labwareEntity.def) || @@ -222,7 +229,7 @@ export const getWasteChuteOption: Selector = createSelect const wasteChuteOption: DropdownOption | null = wasteChuteEntity != null ? { - name: 'Waste Chute', + name: WASTE_CHUTE_DISPLAY_NAME, value: wasteChuteEntity.id, } : null @@ -246,7 +253,7 @@ export const getDisposalOptions = createSelector( ? [ ...acc, { - name: TRASH, + name: TRASH_BIN_DISPLAY_NAME, value: additionalEquipment.id ?? '', }, ] diff --git a/protocol-designer/src/ui/labware/utils.ts b/protocol-designer/src/ui/labware/utils.ts index 2377f7976db..d55c35b578f 100644 --- a/protocol-designer/src/ui/labware/utils.ts +++ b/protocol-designer/src/ui/labware/utils.ts @@ -1,39 +1,54 @@ -import { COLUMN_4_SLOTS } from '@opentrons/step-generation' +import { getHasWasteChute } from '@opentrons/step-generation' +import { WASTE_CHUTE_DISPLAY_NAME } from '@opentrons/components' +import { + FLEX_ROBOT_TYPE, + TC_MODULE_LOCATION_OT2, + TC_MODULE_LOCATION_OT3, + THERMOCYCLER_MODULE_TYPE, +} from '@opentrons/shared-data' +import type { RobotType } from '@opentrons/shared-data' import type { InitialDeckSetup, SavedStepFormState } from '../../step-forms' -export function getLabwareOffDeck( - initialDeckSetup: InitialDeckSetup, - savedStepFormState: SavedStepFormState, - labwareId: string -): boolean { - // latest moveLabware step related to labwareId - const moveLabwareStep = Object.values(savedStepFormState) - .filter( - state => - state.stepType === 'moveLabware' && - labwareId != null && - state.labware === labwareId - ) - .reverse()[0] - - if (moveLabwareStep?.newLocation === 'offDeck') { - return true - } else if ( - moveLabwareStep == null && - initialDeckSetup.labware[labwareId]?.slot === 'offDeck' - ) { - return true - } else return false +function resolveSlotLocation( + modules: InitialDeckSetup['modules'], + labware: InitialDeckSetup['labware'], + location: string, + robotType: RobotType +): string { + const TCSlot = + robotType === FLEX_ROBOT_TYPE + ? TC_MODULE_LOCATION_OT3 + : TC_MODULE_LOCATION_OT2 + if (location === 'offDeck') { + return 'offDeck' + } else if (modules[location] != null) { + return modules[location].type === THERMOCYCLER_MODULE_TYPE + ? TCSlot + : modules[location].slot + } else if (labware[location] != null) { + const adapter = labware[location] + if (modules[adapter.slot] != null) { + return modules[adapter.slot].type === THERMOCYCLER_MODULE_TYPE + ? TCSlot + : modules[adapter.slot].slot + } else { + return adapter.slot + } + } else { + return location + } } -export function getLabwareInColumn4( +export function getLabwareLatestSlot( initialDeckSetup: InitialDeckSetup, savedStepForms: SavedStepFormState, - labwareId: string -): boolean { - const isStartingInColumn4 = COLUMN_4_SLOTS.includes( - initialDeckSetup.labware[labwareId]?.slot - ) + labwareId: string, + robotType: RobotType +): string | null { + const { modules, labware, additionalEquipmentOnDeck } = initialDeckSetup + const initialSlot = labware[labwareId]?.slot + const hasWasteChute = getHasWasteChute(additionalEquipmentOnDeck) + // latest moveLabware step related to labwareId const moveLabwareStep = Object.values(savedStepForms) .filter( @@ -45,13 +60,25 @@ export function getLabwareInColumn4( .reverse()[0] if ( - moveLabwareStep?.newLocation != null && - COLUMN_4_SLOTS.includes(moveLabwareStep.newLocation as string) + hasWasteChute && + (initialSlot === 'D3' || moveLabwareStep?.newLocation === 'D3') ) { - return true - } else if (moveLabwareStep == null && isStartingInColumn4) { - return true + return WASTE_CHUTE_DISPLAY_NAME + } + + if (moveLabwareStep?.newLocation != null) { + return resolveSlotLocation( + modules, + labware, + moveLabwareStep.newLocation as string, + robotType + ) + } else if (moveLabwareStep == null) { + return resolveSlotLocation(modules, labware, initialSlot, robotType) } else { - return false + console.warn( + `Expected to find labware's location but could not with initial slot ${initialSlot}` + ) + return null } } diff --git a/protocol-designer/src/ui/modules/utils.ts b/protocol-designer/src/ui/modules/utils.ts index e1d26bb840c..d347c1b5388 100644 --- a/protocol-designer/src/ui/modules/utils.ts +++ b/protocol-designer/src/ui/modules/utils.ts @@ -1,7 +1,13 @@ import values from 'lodash/values' import { - MAGNETIC_MODULE_V1, + ABSORBANCE_READER_TYPE, getLabwareDefaultEngageHeight, + HEATERSHAKER_MODULE_TYPE, + MAGNETIC_BLOCK_TYPE, + MAGNETIC_MODULE_TYPE, + MAGNETIC_MODULE_V1, + TEMPERATURE_MODULE_TYPE, + THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' import type { DropdownOption } from '@opentrons/components' import type { ModuleType } from '@opentrons/shared-data' @@ -76,17 +82,17 @@ export function getModuleUnderLabware( export const getModuleShortNames = (type: ModuleType): string => { switch (type) { - case 'heaterShakerModuleType': - return 'Heater-Shaker' - case 'magneticBlockType': + case HEATERSHAKER_MODULE_TYPE: + return 'Heater-Shaker Module' + case MAGNETIC_BLOCK_TYPE: return 'Magnetic Block' - case 'magneticModuleType': + case MAGNETIC_MODULE_TYPE: return 'Magnetic Module' - case 'temperatureModuleType': + case TEMPERATURE_MODULE_TYPE: return 'Temperature Module' - case 'thermocyclerModuleType': + case THERMOCYCLER_MODULE_TYPE: return 'Thermocycler' - case 'absorbanceReaderType': + case ABSORBANCE_READER_TYPE: return 'Absorbance Reader' } } @@ -110,22 +116,25 @@ export function getModuleLabwareOptions( )?.id if (labwareOnAdapterId != null) { return { - name: `${nicknamesById[labwareOnAdapterId]} in ${ - nicknamesById[labware.id] - } in ${module} in slot ${moduleOnDeck.slot}`, + name: `${nicknamesById[labware.id]} with ${ + nicknamesById[labwareOnAdapterId] + }`, + deckLabel: moduleOnDeck.slot, + subtext: module, value: moduleOnDeck.id, } } else { return { - name: `${nicknamesById[labware.id]} in ${module} in slot ${ - moduleOnDeck.slot - }`, + name: nicknamesById[labware.id], + deckLabel: moduleOnDeck.slot, + subtext: module, value: moduleOnDeck.id, } } } else { return { - name: `No labware in ${module} in slot ${moduleOnDeck.slot}`, + name: module, + deckLabel: moduleOnDeck.slot, value: moduleOnDeck.id, } }