diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx index 400da9a6235..19eb12cb2db 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx @@ -70,6 +70,15 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { subHeader={t('fixtures_replace')} disabled={!hasTrash} goBack={() => { + // Note this is avoid the following case issue. + // https://github.com/Opentrons/opentrons/pull/17344#pullrequestreview-2576591908 + setValue( + 'additionalEquipment', + additionalEquipment.filter( + ae => ae === 'gripper' || ae === 'trashBin' + ) + ) + goBack(1) }} proceed={handleProceed} @@ -135,11 +144,18 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { filterOptions: getNumOptions( numSlotsAvailable >= MAX_SLOTS ? MAX_SLOTS - : numSlotsAvailable + numStagingAreas + : numSlotsAvailable ), onClick: (value: string) => { const inputNum = parseInt(value) - let updatedStagingAreas = [...additionalEquipment] + const currentStagingAreas = additionalEquipment.filter( + additional => additional === 'stagingArea' + ) + const otherEquipment = additionalEquipment.filter( + additional => additional !== 'stagingArea' + ) + let updatedStagingAreas = currentStagingAreas + // let updatedStagingAreas = [...additionalEquipment] if (inputNum > numStagingAreas) { const difference = inputNum - numStagingAreas @@ -148,13 +164,16 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { ...Array(difference).fill(ae), ] } else { - updatedStagingAreas = updatedStagingAreas.slice( + updatedStagingAreas = currentStagingAreas.slice( 0, inputNum ) } - setValue('additionalEquipment', updatedStagingAreas) + setValue('additionalEquipment', [ + ...otherEquipment, + ...updatedStagingAreas, + ]) }, } return ( diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx index 8365c62de5a..19e757ee0dc 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx @@ -178,6 +178,7 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { ) : null} {filteredSupportedModules + .sort((moduleA, moduleB) => moduleA.localeCompare(moduleB)) .filter(module => enableAbsorbanceReader ? module @@ -219,6 +220,9 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { gridGap={SPACING.spacing4} > {Object.entries(modules) + .sort(([, moduleA], [, moduleB]) => + moduleA.model.localeCompare(moduleB.model) + ) .reduce>( (acc, [key, module]) => { const existingModule = acc.find( @@ -253,7 +257,9 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { }, dropdownType: 'neutral' as DropdownBorder, filterOptions: getNumOptions( - numSlotsAvailable + module.count + module.model !== ABSORBANCE_READER_V1 + ? numSlotsAvailable + module.count + : numSlotsAvailable ), } return ( diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts index bda3d71da18..a167217bf25 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts @@ -1,14 +1,19 @@ import { it, describe, expect } from 'vitest' import { FLEX_ROBOT_TYPE, + ABSORBANCE_READER_V1, + ABSORBANCE_READER_TYPE, HEATERSHAKER_MODULE_TYPE, HEATERSHAKER_MODULE_V1, MAGNETIC_BLOCK_TYPE, MAGNETIC_BLOCK_V1, + MAGNETIC_MODULE_V1, + MAGNETIC_MODULE_V2, TEMPERATURE_MODULE_TYPE, TEMPERATURE_MODULE_V1, TEMPERATURE_MODULE_V2, THERMOCYCLER_MODULE_TYPE, + THERMOCYCLER_MODULE_V1, THERMOCYCLER_MODULE_V2, } from '@opentrons/shared-data' import { getNumSlotsAvailable, getTrashSlot } from '../utils' @@ -36,18 +41,60 @@ describe('getNumSlotsAvailable', () => { const result = getNumSlotsAvailable(null, [], 'gripper') expect(result).toBe(0) }) - it('should return 1 for a non MoaM module', () => { + + it('should return 1 for a non MoaM module - temperature module', () => { const result = getNumSlotsAvailable(null, [], TEMPERATURE_MODULE_V1) expect(result).toBe(1) }) + + it('should return 1 for a non MoaM module - absorbance plate reader', () => { + const result = getNumSlotsAvailable(null, [], ABSORBANCE_READER_V1) + expect(result).toBe(1) + }) + + it('should return 1 for a non MoaM module - thermocycler v1', () => { + const result = getNumSlotsAvailable(null, [], THERMOCYCLER_MODULE_V1) + expect(result).toBe(1) + }) + + it('should return 1 for a non MoaM module - magnetic module v1', () => { + const result = getNumSlotsAvailable(null, [], MAGNETIC_MODULE_V1) + expect(result).toBe(1) + }) + + it('should return 1 for a non MoaM module - magnetic module v2', () => { + const result = getNumSlotsAvailable(null, [], MAGNETIC_MODULE_V2) + expect(result).toBe(1) + }) + it('should return 2 for a thermocycler', () => { const result = getNumSlotsAvailable(null, [], THERMOCYCLER_MODULE_V2) expect(result).toBe(2) }) + it('should return 8 when there are no modules or additional equipment for a heater-shaker', () => { const result = getNumSlotsAvailable(null, [], HEATERSHAKER_MODULE_V1) expect(result).toBe(8) }) + + it('should return 3 when there a plate reader', () => { + const mockModules = { + 0: { + model: ABSORBANCE_READER_V1, + type: ABSORBANCE_READER_TYPE, + slot: 'B3', + }, + } + const mockAdditionalEquipment: AdditionalEquipment[] = ['trashBin'] + const result = getNumSlotsAvailable( + mockModules, + mockAdditionalEquipment, + 'stagingArea' + ) + // Note: the return value is 3 because trashBin can be placed slot1 and plate reader is on B3 + expect(result).toBe(3) + }) + it('should return 0 when there is a TC and 7 modules for a temperature module v2', () => { const mockModules = { 0: { @@ -90,6 +137,7 @@ describe('getNumSlotsAvailable', () => { const result = getNumSlotsAvailable(mockModules, [], TEMPERATURE_MODULE_V2) expect(result).toBe(0) }) + it('should return 1 when there are 9 additional equipment and 1 is a waste chute on the staging area and one is a gripper for a heater-shaker', () => { const mockAdditionalEquipment: AdditionalEquipment[] = [ 'trashBin', @@ -109,6 +157,7 @@ describe('getNumSlotsAvailable', () => { ) expect(result).toBe(1) }) + it('should return 1 when there is a full deck but one staging area for waste chute', () => { const mockModules = { 0: { @@ -148,6 +197,7 @@ describe('getNumSlotsAvailable', () => { ) expect(result).toBe(1) }) + it('should return 1 when there are 7 modules (with one magnetic block) and one trash for staging area', () => { const mockModules = { 0: { @@ -187,8 +237,10 @@ describe('getNumSlotsAvailable', () => { mockAdditionalEquipment, 'stagingArea' ) - expect(result).toBe(1) + // Note: the return value is 2 because trashBin can be placed slot1 + expect(result).toBe(2) }) + it('should return 1 when there are 8 modules with 2 magnetic blocks and one trash for staging area', () => { const mockModules = { 0: { @@ -233,7 +285,7 @@ describe('getNumSlotsAvailable', () => { mockAdditionalEquipment, 'stagingArea' ) - expect(result).toBe(1) + expect(result).toBe(2) }) it('should return 4 when there are 12 magnetic blocks for staging area', () => { const mockModules = { diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/constants.ts b/protocol-designer/src/pages/CreateNewProtocolWizard/constants.ts index 6e762e48f0a..5e1179c58f4 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/constants.ts +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/constants.ts @@ -127,7 +127,7 @@ export const DEFAULT_SLOT_MAP_FLEX: { [HEATERSHAKER_MODULE_V1]: 'D1', [MAGNETIC_BLOCK_V1]: 'D2', [TEMPERATURE_MODULE_V2]: 'C1', - [ABSORBANCE_READER_V1]: 'D3', + [ABSORBANCE_READER_V1]: 'B3', } export const DEFAULT_SLOT_MAP_OT2: { [moduleType in ModuleType]?: string } = { diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx index d3451605756..2e29273b9ac 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx @@ -12,6 +12,7 @@ import { useNavigate } from 'react-router-dom' import { FLEX_ROBOT_TYPE, getAreSlotsAdjacent, + ABSORBANCE_READER_MODELS, HEATERSHAKER_MODULE_TYPE, MAGNETIC_BLOCK_TYPE, MAGNETIC_MODULE_TYPE, @@ -283,11 +284,20 @@ export function CreateNewProtocolWizard(): JSX.Element | null { const stagingAreas = values.additionalEquipment.filter( equipment => equipment === 'stagingArea' ) + if (stagingAreas.length > 0) { + // Note: when plate reader is present, cutoutB3 is not available for StagingArea + const hasPlateReader = modules.some( + module => module.model === ABSORBANCE_READER_MODELS[0] + ) stagingAreas.forEach((_, index) => { - return dispatch( - createDeckFixture('stagingArea', STAGING_AREA_CUTOUTS_ORDERED[index]) - ) + const stagingAreaCutout = hasPlateReader + ? STAGING_AREA_CUTOUTS_ORDERED.filter( + cutout => cutout !== 'cutoutB3' + )[index] + : STAGING_AREA_CUTOUTS_ORDERED[index] + + return dispatch(createDeckFixture('stagingArea', stagingAreaCutout)) }) } diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx index 3ffb60d10f2..69abb7e6ae7 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx @@ -30,10 +30,11 @@ import type { import type { DropdownOption } from '@opentrons/components' import type { AdditionalEquipment, WizardFormState } from './types' -const TOTAL_OUTER_SLOTS = 8 -const MIDDLE_SLOT_NUM = 4 -const MAX_MAGNETIC_BLOCK_SLOTS = 12 -const TOTAL_LEFT_SLOTS = 4 +const NUM_SLOTS_OUTER = 8 +const NUM_SLOTS_MIDDLE = 4 +const NUM_SLOTS_COLUMN3 = 4 +const NUM_SLOTS_MAGNETIC_BLOCK = 12 + export const getNumOptions = (length: number): DropdownOption[] => { return Array.from({ length }, (_, i) => ({ name: `${i + 1}`, @@ -66,12 +67,12 @@ export const getNumSlotsAvailable = ( const magneticBlockCount = magneticBlocks.length const moduleCount = modules != null ? Object.keys(modules).length : 0 let filteredModuleLength = moduleCount - if (magneticBlockCount <= MIDDLE_SLOT_NUM) { + if (magneticBlockCount <= NUM_SLOTS_MIDDLE) { // Subtract magnetic blocks directly if their count is ≤ 4 filteredModuleLength -= magneticBlockCount } else { // Subtract the excess magnetic blocks beyond 4 - const extraMagneticBlocks = magneticBlockCount - MIDDLE_SLOT_NUM + const extraMagneticBlocks = magneticBlockCount - NUM_SLOTS_MIDDLE filteredModuleLength -= extraMagneticBlocks } if (hasTC) { @@ -86,11 +87,9 @@ export const getNumSlotsAvailable = ( case 'gripper': { return 0 } - // TODO: wire up absorbance reader - case ABSORBANCE_READER_V1: { - return 1 - } + // these modules don't support MoaM + case ABSORBANCE_READER_V1: case THERMOCYCLER_MODULE_V1: case TEMPERATURE_MODULE_V1: case MAGNETIC_MODULE_V1: @@ -105,43 +104,45 @@ export const getNumSlotsAvailable = ( return 2 } } + case 'trashBin': case HEATERSHAKER_MODULE_V1: case TEMPERATURE_MODULE_V2: { return ( - TOTAL_OUTER_SLOTS - + NUM_SLOTS_OUTER - (filteredModuleLength + filteredAdditionalEquipmentLength) ) } case 'stagingArea': { - const lengthMinusMagneticBlock = - moduleCount + (hasTC ? 1 : 0) - magneticBlockCount - let adjustedModuleLength = 0 - if (lengthMinusMagneticBlock > TOTAL_LEFT_SLOTS) { - adjustedModuleLength = lengthMinusMagneticBlock - TOTAL_LEFT_SLOTS - } - - const occupiedSlots = - adjustedModuleLength + filteredAdditionalEquipmentLength - - return TOTAL_LEFT_SLOTS - occupiedSlots + const modulesWithColumn3 = + modules !== null + ? Object.values(modules).filter(module => module.slot?.includes('3')) + .length + : 0 + const fixtureSlotsWithColumn3 = + additionalEquipment !== null + ? additionalEquipment.filter(slot => slot.includes('3')).length + : 0 + return NUM_SLOTS_COLUMN3 - modulesWithColumn3 - fixtureSlotsWithColumn3 } + case 'wasteChute': { const adjustmentForStagingArea = numStagingAreas >= 1 ? 1 : 0 return ( - TOTAL_OUTER_SLOTS - + NUM_SLOTS_OUTER - (filteredModuleLength + filteredAdditionalEquipmentLength - adjustmentForStagingArea) ) } + case MAGNETIC_BLOCK_V1: { const filteredAdditionalEquipmentForMagneticBlockLength = additionalEquipment.filter( ae => ae !== 'gripper' && ae !== 'stagingArea' )?.length return ( - MAX_MAGNETIC_BLOCK_SLOTS - + NUM_SLOTS_MAGNETIC_BLOCK - (filteredModuleLength + filteredAdditionalEquipmentForMagneticBlockLength) ) @@ -292,9 +293,21 @@ export const getTrashSlot = (values: WizardFormState): string => { equipment.includes('stagingArea') ) - const cutouts = stagingAreas.map( - (_, index) => STAGING_AREA_CUTOUTS_ORDERED[index] + // when plate reader is present, cutoutB3 is not available for StagingArea + const hasPlateReader = + modules !== null + ? Object.values(modules).some( + module => module.model === ABSORBANCE_READER_V1 + ) + : false + const cutouts = stagingAreas.map((_, index) => + hasPlateReader + ? STAGING_AREA_CUTOUTS_ORDERED.filter(cutout => cutout !== 'cutoutB3')[ + index + ] + : STAGING_AREA_CUTOUTS_ORDERED[index] ) + const hasWasteChute = additionalEquipment.find(equipment => equipment.includes('wasteChute') )