diff --git a/components/src/molecules/Tabs/index.tsx b/components/src/molecules/Tabs/index.tsx index d9e6a7b349a..70811a3dd2a 100644 --- a/components/src/molecules/Tabs/index.tsx +++ b/components/src/molecules/Tabs/index.tsx @@ -1,8 +1,10 @@ import { css } from 'styled-components' -import { TYPOGRAPHY, SPACING, RESPONSIVENESS } from '../../ui-style-constants' +import { Tooltip } from '../../atoms' import { COLORS, BORDERS } from '../../helix-design-system' -import { POSITION_RELATIVE, DIRECTION_ROW } from '../../styles' import { Btn, Flex } from '../../primitives' +import { POSITION_RELATIVE, DIRECTION_ROW } from '../../styles' +import { useHoverTooltip } from '../../tooltips' +import { TYPOGRAPHY, SPACING, RESPONSIVENESS } from '../../ui-style-constants' const DEFAULT_TAB_STYLE = css` ${TYPOGRAPHY.pSemiBold} @@ -65,6 +67,7 @@ export interface TabProps { onClick: () => void isActive?: boolean disabled?: boolean + disabledReasonForTooltip?: string } export interface TabsProps { @@ -77,18 +80,36 @@ export function Tabs(props: TabsProps): JSX.Element { return ( {tabs.map((tab, index) => ( - { - tab.onClick() - }} - css={tab.isActive === true ? CURRENT_TAB_STYLE : DEFAULT_TAB_STYLE} - disabled={tab.disabled} - > - {tab.text} - + ))} ) } + +function Tab(props: TabProps): JSX.Element { + const { + text, + onClick, + isActive, + disabled = false, + disabledReasonForTooltip, + } = props + const [targetProps, tooltipProps] = useHoverTooltip() + return ( + <> + + {text} + + {disabled && disabledReasonForTooltip != null ? ( + + {disabledReasonForTooltip} + + ) : null} + + ) +} diff --git a/protocol-designer/src/assets/localization/en/starting_deck_state.json b/protocol-designer/src/assets/localization/en/starting_deck_state.json index fd77cb95dd8..afa602358ca 100644 --- a/protocol-designer/src/assets/localization/en/starting_deck_state.json +++ b/protocol-designer/src/assets/localization/en/starting_deck_state.json @@ -48,6 +48,7 @@ "onDeck": "On deck", "one_item": "No more than 1 {{hardware}} allowed on the deck at one time", "only_display_rec": "Only display recommended labware", + "plate_reader_no_labware": "Labware cannot be loaded onto a plate reader. You can move labware onto the plate reader when building your protocol", "protocol_starting_deck": "Protocol starting deck", "read_more_gen1_gen2": "Read more about the differences between GEN1 and GEN2 Magnetic Modules", "rename_lab": "Rename labware", diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx index e93ec99d887..4c41a8f8464 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx @@ -18,6 +18,7 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { + ABSORBANCE_READER_V1, FLEX_ROBOT_TYPE, FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS, getModuleDisplayName, @@ -228,7 +229,9 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { disabled: selectedFixture === 'wasteChute' || selectedFixture === 'wasteChuteAndStagingArea' || - selectedFixture === 'trashBin', + selectedFixture === 'trashBin' || + selectedModuleModel === ABSORBANCE_READER_V1, + disabledReasonForTooltip: t('plate_reader_no_labware'), isActive: tab === 'labware', onClick: () => { setTab('labware') diff --git a/protocol-designer/src/pages/Designer/DeckSetup/HighlightItems.tsx b/protocol-designer/src/pages/Designer/DeckSetup/HighlightItems.tsx index fd548de360b..c9e4726eae3 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/HighlightItems.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/HighlightItems.tsx @@ -191,6 +191,7 @@ export function HighlightItems(props: HighlightItemsProps): JSX.Element | null { ? hoveredItem.text ?? '' : selectedItemModule.text ?? '' } + slot={moduleOnDeck.slot} /> ) } diff --git a/protocol-designer/src/pages/Designer/DeckSetup/HoveredItems.tsx b/protocol-designer/src/pages/Designer/DeckSetup/HoveredItems.tsx index a96d2418607..7a478a37f98 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/HoveredItems.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/HoveredItems.tsx @@ -109,6 +109,7 @@ export const HoveredItems = ( orientation={orientation} isSelected={false} isLast={true} + slot={selectedSlot.slot} /> ) : null} diff --git a/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx index be4f457429e..ca8feac72d1 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/LabwareTools.tsx @@ -38,6 +38,7 @@ import { getOnlyLatestDefs } from '../../../labware-defs' import { ADAPTER_96_CHANNEL, getLabwareIsCompatible as _getLabwareIsCompatible, + getLabwareCompatibleWithAbsorbanceReader, } from '../../../utils/labwareModuleCompatibility' import { getHas96Channel } from '../../../utils' import { createCustomLabwareDef } from '../../../labware-defs/actions' @@ -49,6 +50,7 @@ import { selectLabware, selectNestedLabware, } from '../../../labware-ingred/actions' +import { getEnableAbsorbanceReader } from '../../../feature-flags/selectors' import { ALL_ORDERED_CATEGORIES, CUSTOM_CATEGORY, @@ -132,13 +134,17 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { robotType === OT2_ROBOT_TYPE ? isNextToHeaterShaker : false ) + const enablePlateReader = useSelector(getEnableAbsorbanceReader) + const getLabwareCompatible = useCallback( (def: LabwareDefinition2) => { // assume that custom (non-standard) labware is (potentially) compatible if (moduleType == null || !getLabwareDefIsStandard(def)) { return true } - return _getLabwareIsCompatible(def, moduleType) + return moduleType === ABSORBANCE_READER_TYPE + ? getLabwareCompatibleWithAbsorbanceReader(def) + : _getLabwareIsCompatible(def, moduleType) }, [moduleType] ) @@ -167,7 +173,8 @@ export function LabwareTools(props: LabwareToolsProps): JSX.Element { moduleType !== HEATERSHAKER_MODULE_TYPE) || (isAdapter96Channel && !has96Channel) || (slot === 'offDeck' && isAdapter) || - (PLATE_READER_LOADNAME === parameters.loadName && + (!enablePlateReader && + PLATE_READER_LOADNAME === parameters.loadName && moduleType !== ABSORBANCE_READER_TYPE) ) }, diff --git a/protocol-designer/src/pages/Designer/DeckSetup/ModuleLabel.tsx b/protocol-designer/src/pages/Designer/DeckSetup/ModuleLabel.tsx index 12849eba08a..568eeb5f0d9 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/ModuleLabel.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/ModuleLabel.tsx @@ -3,15 +3,19 @@ import { useSelector } from 'react-redux' import { DeckLabelSet } from '@opentrons/components' import { FLEX_ROBOT_TYPE, + FLEX_STANDARD_DECKID, HEATERSHAKER_MODULE_TYPE, - MAGNETIC_MODULE_TYPE, + OT2_STANDARD_DECKID, TEMPERATURE_MODULE_TYPE, - THERMOCYCLER_MODULE_TYPE, getModuleDef2, } from '@opentrons/shared-data' import { getRobotType } from '../../../file-data/selectors' import type { DeckLabelProps } from '@opentrons/components' -import type { CoordinateTuple, ModuleModel } from '@opentrons/shared-data' +import type { + CoordinateTuple, + DeckSlotId, + ModuleModel, +} from '@opentrons/shared-data' interface ModuleLabelProps { moduleModel: ModuleModel @@ -19,6 +23,7 @@ interface ModuleLabelProps { orientation: 'left' | 'right' isSelected: boolean isLast: boolean + slot: DeckSlotId | null isZoomed?: boolean labwareInfos?: DeckLabelProps[] labelName?: string @@ -33,6 +38,7 @@ export const ModuleLabel = (props: ModuleLabelProps): JSX.Element => { labwareInfos = [], isZoomed = true, labelName, + slot, } = props const robotType = useSelector(getRobotType) const labelContainerRef = useRef(null) @@ -45,30 +51,21 @@ export const ModuleLabel = (props: ModuleLabelProps): JSX.Element => { }, [labwareInfos]) const def = getModuleDef2(moduleModel) - const overhang = - def?.dimensions.labwareInterfaceXDimension != null - ? def.dimensions.xDimension - def?.dimensions.labwareInterfaceXDimension + const slotTransformKey = + robotType === FLEX_ROBOT_TYPE ? FLEX_STANDARD_DECKID : OT2_STANDARD_DECKID + const cornerOffsetsFromSlotFromTransform = + slot != null && !isZoomed + ? def?.slotTransforms?.[slotTransformKey]?.[slot]?.cornerOffsetFromSlot + : null + const tempAdjustmentX = + def?.moduleType === TEMPERATURE_MODULE_TYPE && orientation === 'right' + ? def?.dimensions.xDimension - (def?.dimensions.footprintXDimension ?? 0) // shift depending on side of deck + : 0 + const tempAdjustmentY = def?.moduleType === TEMPERATURE_MODULE_TYPE ? -1 : 0 + const heaterShakerAdjustmentX = + def?.moduleType === HEATERSHAKER_MODULE_TYPE && orientation === 'right' // shift depending on side of deck + ? 7 // TODO(ND: 12/18/2024): investigate further why the module definition does not contain sufficient info to find this offset : 0 - let leftOverhang = overhang - - switch (def?.moduleType) { - case TEMPERATURE_MODULE_TYPE: - leftOverhang = overhang * 2 - break - case HEATERSHAKER_MODULE_TYPE: - leftOverhang = overhang + 14 - break - case MAGNETIC_MODULE_TYPE: - leftOverhang = overhang + 8 - break - case THERMOCYCLER_MODULE_TYPE: - if (!isZoomed && robotType === FLEX_ROBOT_TYPE) { - leftOverhang = overhang + 20 - } - break - default: - break - } return ( { ...labwareInfos, ]} x={ - (orientation === 'right' - ? position[0] - overhang - : position[0] - leftOverhang) - def?.cornerOffsetFromSlot.x + position[0] + + def.cornerOffsetFromSlot.x + + (cornerOffsetsFromSlotFromTransform?.[0][3] ?? 0) + + tempAdjustmentX + + heaterShakerAdjustmentX - + 1 + } + y={ + position[1] + + def.cornerOffsetFromSlot.y + + (cornerOffsetsFromSlotFromTransform?.[1][3] ?? 0) - + labelContainerHeight + + tempAdjustmentY } - y={position[1] + def?.cornerOffsetFromSlot.y - labelContainerHeight} width={def?.dimensions.xDimension + 2} height={def?.dimensions.yDimension + 2} /> diff --git a/protocol-designer/src/pages/Designer/DeckSetup/SelectedHoveredItems.tsx b/protocol-designer/src/pages/Designer/DeckSetup/SelectedHoveredItems.tsx index 2400271ee22..b150cdb9274 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/SelectedHoveredItems.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/SelectedHoveredItems.tsx @@ -188,6 +188,7 @@ export const SelectedHoveredItems = ( orientation={orientation} isSelected={true} labwareInfos={labwareInfos} + slot={selectedSlot.slot} /> ) : null} diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx index 479724f3527..d584faf4d27 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/LabwareTools.test.tsx @@ -25,6 +25,7 @@ import type { LabwareDefinition2, PipetteV2Specs } from '@opentrons/shared-data' vi.mock('../../../../utils') vi.mock('../../../../step-forms/selectors') +vi.mock('../../../../feature-flags/selectors') vi.mock('../../../../file-data/selectors') vi.mock('../../../../labware-defs/selectors') vi.mock('../../../../labware-defs/actions') diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/utils.test.ts b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/utils.test.ts index 50325ad7197..7d48c7ea71d 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/utils.test.ts +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/utils.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest' import { + ABSORBANCE_READER_V1, FLEX_ROBOT_TYPE, HEATERSHAKER_MODULE_TYPE, HEATERSHAKER_MODULE_V1, @@ -45,12 +46,13 @@ describe('getModuleModelsBySlot', () => { }) it('renders all flex modules for B1', () => { expect(getModuleModelsBySlot(true, FLEX_ROBOT_TYPE, 'B1')).toEqual( - FLEX_MODULE_MODELS + FLEX_MODULE_MODELS.filter(model => model !== ABSORBANCE_READER_V1) ) }) it('renders all flex modules for C1', () => { const noTC = FLEX_MODULE_MODELS.filter( - model => model !== THERMOCYCLER_MODULE_V2 + model => + model !== THERMOCYCLER_MODULE_V2 && model !== ABSORBANCE_READER_V1 ) expect(getModuleModelsBySlot(true, FLEX_ROBOT_TYPE, 'C1')).toEqual(noTC) }) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/constants.ts b/protocol-designer/src/pages/Designer/DeckSetup/constants.ts index e1acb64424d..479b185a765 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/constants.ts +++ b/protocol-designer/src/pages/Designer/DeckSetup/constants.ts @@ -13,6 +13,7 @@ import { HEATERSHAKER_MODULE_TYPE, MAGNETIC_BLOCK_TYPE, ABSORBANCE_READER_TYPE, + ABSORBANCE_READER_V1, } from '@opentrons/shared-data' import type { ModuleModel, ModuleType } from '@opentrons/shared-data' @@ -92,9 +93,7 @@ export const RECOMMENDED_LABWARE_BY_MODULE: { [K in ModuleType]: string[] } = { 'nest_96_wellplate_2ml_deep', 'opentrons_96_wellplate_200ul_pcr_full_skirt', ], - [ABSORBANCE_READER_TYPE]: [ - 'opentrons_flex_lid_absorbance_plate_reader_module', - ], + [ABSORBANCE_READER_TYPE]: ['nest_96_wellplate_200ul_flat'], } export const MOAM_MODELS_WITH_FF: ModuleModel[] = [TEMPERATURE_MODULE_V2] @@ -102,6 +101,7 @@ export const MOAM_MODELS: ModuleModel[] = [ TEMPERATURE_MODULE_V2, HEATERSHAKER_MODULE_V1, MAGNETIC_BLOCK_V1, + ABSORBANCE_READER_V1, ] export const MAX_MOAM_MODULES = 7 diff --git a/protocol-designer/src/pages/Designer/DeckSetup/utils.ts b/protocol-designer/src/pages/Designer/DeckSetup/utils.ts index a288947365a..a891926d329 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/utils.ts +++ b/protocol-designer/src/pages/Designer/DeckSetup/utils.ts @@ -5,8 +5,9 @@ import { FLEX_ROBOT_TYPE, FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS, HEATERSHAKER_MODULE_TYPE, - MAGNETIC_BLOCK_V1, + HEATERSHAKER_MODULE_V1, OT2_ROBOT_TYPE, + TEMPERATURE_MODULE_V2, THERMOCYCLER_MODULE_TYPE, THERMOCYCLER_MODULE_V2, getAreSlotsAdjacent, @@ -61,33 +62,32 @@ export function getModuleModelsBySlot( robotType: RobotType, slot: DeckSlotId ): ModuleModel[] { - const FLEX_MIDDLE_SLOTS = ['B2', 'C2', 'A2', 'D2'] + const FLEX_MIDDLE_SLOTS = new Set(['B2', 'C2', 'A2', 'D2']) const OT2_MIDDLE_SLOTS = ['2', '5', '8', '11'] - const FILTERED_MODULES = enableAbsorbanceReader - ? FLEX_MODULE_MODELS - : FLEX_MODULE_MODELS.filter(model => model !== ABSORBANCE_READER_V1) - let moduleModels: ModuleModel[] = FILTERED_MODULES + const FLEX_RIGHT_SLOTS = new Set(['A3', 'B3', 'C3', 'D3']) + + let moduleModels: ModuleModel[] = FLEX_MODULE_MODELS switch (robotType) { case FLEX_ROBOT_TYPE: { - if (slot !== 'B1' && !FLEX_MIDDLE_SLOTS.includes(slot)) { - moduleModels = FILTERED_MODULES.filter( - model => model !== THERMOCYCLER_MODULE_V2 - ) - } - if (FLEX_MIDDLE_SLOTS.includes(slot)) { - moduleModels = FILTERED_MODULES.filter( - model => model === MAGNETIC_BLOCK_V1 - ) - } - if ( - FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS.includes( - slot as AddressableAreaName - ) - ) { - moduleModels = [] - } + moduleModels = FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS.includes( + slot as AddressableAreaName + ) + ? [] + : FLEX_MODULE_MODELS.filter(model => { + if (model === THERMOCYCLER_MODULE_V2) { + return slot === 'B1' + } else if (model === ABSORBANCE_READER_V1) { + return FLEX_RIGHT_SLOTS.has(slot) && enableAbsorbanceReader + } else if ( + model === TEMPERATURE_MODULE_V2 || + model === HEATERSHAKER_MODULE_V1 + ) { + return !FLEX_MIDDLE_SLOTS.has(slot) + } + return true + }) break } case OT2_ROBOT_TYPE: { diff --git a/protocol-designer/src/utils/labwareModuleCompatibility.ts b/protocol-designer/src/utils/labwareModuleCompatibility.ts index 52fd34ec9ed..1664e060f0a 100644 --- a/protocol-designer/src/utils/labwareModuleCompatibility.ts +++ b/protocol-designer/src/utils/labwareModuleCompatibility.ts @@ -11,6 +11,9 @@ import type { LabwareDefByDefURI } from '../labware-defs' import type { LabwareOnDeck } from '../step-forms' import type { LabwareDefinition2, ModuleType } from '@opentrons/shared-data' // NOTE: this does not distinguish btw versions. Standard labware only (assumes namespace is 'opentrons') + +const PLATE_READER_MAX_LABWARE_Z_MM = 16 + export const COMPATIBLE_LABWARE_ALLOWLIST_BY_MODULE_TYPE: Record< ModuleType, Readonly @@ -155,6 +158,18 @@ export const getLabwareIsCustom = ( return labwareOnDeck.labwareDefURI in customLabwares } +// This breaks pattern with other module compatibility checks, but it more exactly mirrors Protocol Engine's logic +// See api/src/opentrons/protocol_engine/state/labware.py for details +export const getLabwareCompatibleWithAbsorbanceReader = ( + def: LabwareDefinition2 +): boolean => { + return ( + Object.entries(def.wells).length === 96 && + !def.parameters.isTiprack && + def.dimensions.zDimension <= PLATE_READER_MAX_LABWARE_Z_MM + ) +} + export const getAdapterLabwareIsAMatch = ( labwareId: string, allLabware: LabwareOnDeck[], diff --git a/shared-data/module/definitions/3/absorbanceReaderV1.json b/shared-data/module/definitions/3/absorbanceReaderV1.json index 57b0bfc1b10..7ced1f05ded 100644 --- a/shared-data/module/definitions/3/absorbanceReaderV1.json +++ b/shared-data/module/definitions/3/absorbanceReaderV1.json @@ -11,8 +11,8 @@ "bareOverallHeight": 18.5, "overLabwareHeight": 0.0, "lidHeight": 60.0, - "xDimension": 95.5, - "yDimension": 155.3, + "xDimension": 155.3, + "yDimension": 95.5, "labwareInterfaceXDimension": 127.8, "labwareInterfaceYDimension": 85.5 },