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
},