From 3a27f066efc20d8b48b3306f973a70d274dc06c7 Mon Sep 17 00:00:00 2001
From: Jethary Alcid <66035149+jerader@users.noreply.github.com>
Date: Thu, 8 Aug 2024 17:46:01 -0400
Subject: [PATCH] feat(protocol-designer): deck setup skeleton for redesign
(#15899)
closes AUTH-635
---
.../src/components/DeckSetup/index.tsx | 2 +-
.../src/components/DeckSetupManager.tsx | 4 +-
.../localization/en/protocol_overview.json | 2 +
.../DeckSetup/ControlSelect.tsx | 89 +++++
.../DeckSetup/DeckSetup.module.css | 25 ++
.../DeckSetup/DeckSetupDetails.tsx | 358 ++++++++++++++++++
.../DeckSetup/SlotDetailsContainer.tsx | 30 ++
.../ProtocolOverview/DeckSetup/index.tsx | 258 +++++++++++++
.../pages/ProtocolOverview/DeckSetup/utils.ts | 20 +
.../__tests__/ProtocolOverview.test.tsx | 27 +-
.../src/pages/ProtocolOverview/index.tsx | 25 +-
11 files changed, 834 insertions(+), 6 deletions(-)
create mode 100644 protocol-designer/src/pages/ProtocolOverview/DeckSetup/ControlSelect.tsx
create mode 100644 protocol-designer/src/pages/ProtocolOverview/DeckSetup/DeckSetup.module.css
create mode 100644 protocol-designer/src/pages/ProtocolOverview/DeckSetup/DeckSetupDetails.tsx
create mode 100644 protocol-designer/src/pages/ProtocolOverview/DeckSetup/SlotDetailsContainer.tsx
create mode 100644 protocol-designer/src/pages/ProtocolOverview/DeckSetup/index.tsx
create mode 100644 protocol-designer/src/pages/ProtocolOverview/DeckSetup/utils.ts
diff --git a/protocol-designer/src/components/DeckSetup/index.tsx b/protocol-designer/src/components/DeckSetup/index.tsx
index 4ca74461909..736753e715a 100644
--- a/protocol-designer/src/components/DeckSetup/index.tsx
+++ b/protocol-designer/src/components/DeckSetup/index.tsx
@@ -479,7 +479,7 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => {
)
}
-export const DeckSetup = (): JSX.Element => {
+export const LegacyDeckSetup = (): JSX.Element => {
const drilledDown =
useSelector(labwareIngredSelectors.getDrillDownLabwareId) != null
const selectedTerminalItemId = useSelector(getSelectedTerminalItemId)
diff --git a/protocol-designer/src/components/DeckSetupManager.tsx b/protocol-designer/src/components/DeckSetupManager.tsx
index 8aabeba5090..2c065b7a545 100644
--- a/protocol-designer/src/components/DeckSetupManager.tsx
+++ b/protocol-designer/src/components/DeckSetupManager.tsx
@@ -4,7 +4,7 @@ import {
getBatchEditSelectedStepTypes,
getHoveredItem,
} from '../ui/steps/selectors'
-import { DeckSetup } from './DeckSetup'
+import { LegacyDeckSetup } from './DeckSetup'
import { NullDeckState } from './DeckSetup/NullDeckState'
import { OffDeckLabwareButton } from './OffDeckLabwareButton'
@@ -17,7 +17,7 @@ export const DeckSetupManager = (): JSX.Element => {
return (
<>
-
+
>
)
} else {
diff --git a/protocol-designer/src/localization/en/protocol_overview.json b/protocol-designer/src/localization/en/protocol_overview.json
index 475116fc440..1ffb61db81d 100644
--- a/protocol-designer/src/localization/en/protocol_overview.json
+++ b/protocol-designer/src/localization/en/protocol_overview.json
@@ -1,3 +1,5 @@
{
+ "add_labware": "Add labware",
+ "edit": "Edit",
"protocol_overview": "Protocol overview"
}
diff --git a/protocol-designer/src/pages/ProtocolOverview/DeckSetup/ControlSelect.tsx b/protocol-designer/src/pages/ProtocolOverview/DeckSetup/ControlSelect.tsx
new file mode 100644
index 00000000000..a12f6e32dff
--- /dev/null
+++ b/protocol-designer/src/pages/ProtocolOverview/DeckSetup/ControlSelect.tsx
@@ -0,0 +1,89 @@
+import * as React from 'react'
+import { useTranslation } from 'react-i18next'
+import cx from 'classnames'
+import { css } from 'styled-components'
+import { useSelector } from 'react-redux'
+import {
+ Flex,
+ LegacyStyledText,
+ RobotCoordsForeignDiv,
+} from '@opentrons/components'
+import { START_TERMINAL_ITEM_ID } from '../../../steplist'
+import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations'
+
+import type { CoordinateTuple, Dimensions } from '@opentrons/shared-data'
+import type { TerminalItemId } from '../../../steplist'
+
+import styles from './DeckSetup.module.css'
+
+interface ControlSelectProps {
+ slotPosition: CoordinateTuple | null
+ slotBoundingBox: Dimensions
+ slotId: string
+ addEquipment: (slotId: string) => void
+ hover: string | null
+ setHover: React.Dispatch>
+ slotTopLayerId: string // can be AddressableAreaName, moduleId, labwareId
+ selectedTerminalItemId?: TerminalItemId | null
+}
+
+export const ControlSelect = (
+ props: ControlSelectProps
+): JSX.Element | null => {
+ const {
+ slotBoundingBox,
+ slotPosition,
+ slotId,
+ selectedTerminalItemId,
+ addEquipment,
+ hover,
+ setHover,
+ slotTopLayerId,
+ } = props
+ const { t } = useTranslation('protocol_overview')
+ const activeDeckSetup = useSelector(getDeckSetupForActiveItem)
+ const moduleId = Object.keys(activeDeckSetup.modules).find(
+ moduleId => slotTopLayerId === moduleId
+ )
+
+ if (selectedTerminalItemId !== START_TERMINAL_ITEM_ID || slotPosition == null)
+ return null
+
+ return (
+ {
+ setHover(slotId)
+ },
+ onMouseLeave: () => {
+ setHover(null)
+ },
+ onClick: () => {
+ addEquipment(slotId)
+ },
+ }}
+ >
+
+ {
+ addEquipment(slotId)
+ }}
+ >
+
+ {moduleId != null ? t('add_labware') : t('edit')}
+
+
+
+
+ )
+}
diff --git a/protocol-designer/src/pages/ProtocolOverview/DeckSetup/DeckSetup.module.css b/protocol-designer/src/pages/ProtocolOverview/DeckSetup/DeckSetup.module.css
new file mode 100644
index 00000000000..b4b88d3a8f8
--- /dev/null
+++ b/protocol-designer/src/pages/ProtocolOverview/DeckSetup/DeckSetup.module.css
@@ -0,0 +1,25 @@
+.slot_overlay {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1;
+ padding: 0.5rem;
+ background-color: color-mod(var(--c-black) alpha(0.75));
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
+ align-items: flex-start;
+ color: white;
+ font-size: var(--fs-body-1);
+ border-radius: 0.5rem;
+}
+
+.appear_on_mouseover {
+ opacity: 0;
+
+ &:hover {
+ opacity: 1;
+ }
+}
diff --git a/protocol-designer/src/pages/ProtocolOverview/DeckSetup/DeckSetupDetails.tsx b/protocol-designer/src/pages/ProtocolOverview/DeckSetup/DeckSetupDetails.tsx
new file mode 100644
index 00000000000..ddf4e25897e
--- /dev/null
+++ b/protocol-designer/src/pages/ProtocolOverview/DeckSetup/DeckSetupDetails.tsx
@@ -0,0 +1,358 @@
+import * as React from 'react'
+import compact from 'lodash/compact'
+import values from 'lodash/values'
+
+import { Module } from '@opentrons/components'
+import { MODULES_WITH_COLLISION_ISSUES } from '@opentrons/step-generation'
+import {
+ getAddressableAreaFromSlotId,
+ getLabwareHasQuirk,
+ getModuleDef2,
+ getPositionFromSlotId,
+ inferModuleOrientationFromSlot,
+ inferModuleOrientationFromXCoordinate,
+ isAddressableAreaStandardSlot,
+ SPAN7_8_10_11_SLOT,
+ THERMOCYCLER_MODULE_TYPE,
+} from '@opentrons/shared-data'
+import {
+ getSlotIdsBlockedBySpanning,
+ getSlotIsEmpty,
+} from '../../../step-forms'
+import { LabwareOnDeck } from '../../../components/DeckSetup/LabwareOnDeck'
+import { SlotWarning } from '../../../components/DeckSetup/SlotWarning'
+import { getStagingAreaAddressableAreas } from '../../../utils'
+import { ControlSelect } from './ControlSelect'
+
+import type { ModuleTemporalProperties } from '@opentrons/step-generation'
+import type {
+ AddressableAreaName,
+ CutoutId,
+ DeckDefinition,
+ Dimensions,
+} from '@opentrons/shared-data'
+import type {
+ InitialDeckSetup,
+ LabwareOnDeck as LabwareOnDeckType,
+ ModuleOnDeck,
+} from '../../../step-forms'
+import type { TerminalItemId } from '../../../steplist'
+
+interface DeckSetupDetailsProps {
+ activeDeckSetup: InitialDeckSetup
+ showGen1MultichannelCollisionWarnings: boolean
+ deckDef: DeckDefinition
+ stagingAreaCutoutIds: CutoutId[]
+ trashSlot: string | null
+ addEquipment: (slotId: string) => void
+ hover: string | null
+ setHover: React.Dispatch>
+ selectedTerminalItemId?: TerminalItemId | null
+}
+
+export const DeckSetupDetails = (props: DeckSetupDetailsProps): JSX.Element => {
+ const {
+ activeDeckSetup,
+ showGen1MultichannelCollisionWarnings,
+ deckDef,
+ trashSlot,
+ addEquipment,
+ stagingAreaCutoutIds,
+ selectedTerminalItemId,
+ hover,
+ setHover,
+ } = props
+ const slotIdsBlockedBySpanning = getSlotIdsBlockedBySpanning(activeDeckSetup)
+
+ const allLabware: LabwareOnDeckType[] = Object.keys(
+ activeDeckSetup.labware
+ ).reduce((acc, labwareId) => {
+ const labware = activeDeckSetup.labware[labwareId]
+ return getLabwareHasQuirk(labware.def, 'fixedTrash')
+ ? acc
+ : [...acc, labware]
+ }, [])
+
+ const allModules: ModuleOnDeck[] = values(activeDeckSetup.modules)
+
+ // NOTE: naively hard-coded to show warning north of slots 1 or 3 when occupied by any module
+ const multichannelWarningSlotIds: AddressableAreaName[] = showGen1MultichannelCollisionWarnings
+ ? compact([
+ allModules.some(
+ moduleOnDeck =>
+ moduleOnDeck.slot === '1' &&
+ MODULES_WITH_COLLISION_ISSUES.includes(moduleOnDeck.model)
+ )
+ ? deckDef.locations.addressableAreas.find(s => s.id === '4')?.id
+ : null,
+ allModules.some(
+ moduleOnDeck =>
+ moduleOnDeck.slot === '3' &&
+ MODULES_WITH_COLLISION_ISSUES.includes(moduleOnDeck.model)
+ )
+ ? deckDef.locations.addressableAreas.find(s => s.id === '6')?.id
+ : null,
+ ])
+ : []
+
+ return (
+ <>
+ {/* all modules */}
+ {allModules.map(moduleOnDeck => {
+ const slotId =
+ moduleOnDeck.slot === SPAN7_8_10_11_SLOT ? '7' : moduleOnDeck.slot
+
+ const slotPosition = getPositionFromSlotId(slotId, deckDef)
+ if (slotPosition == null) {
+ console.warn(`no slot ${slotId} for module ${moduleOnDeck.id}`)
+ return null
+ }
+ const moduleDef = getModuleDef2(moduleOnDeck.model)
+
+ const getModuleInnerProps = (
+ moduleState: ModuleTemporalProperties['moduleState']
+ ): React.ComponentProps['innerProps'] => {
+ if (moduleState.type === THERMOCYCLER_MODULE_TYPE) {
+ let lidMotorState = 'unknown'
+ if (
+ selectedTerminalItemId === '__initial_setup__' ||
+ moduleState.lidOpen === true
+ ) {
+ lidMotorState = 'open'
+ } else if (moduleState.lidOpen === false) {
+ lidMotorState = 'closed'
+ }
+ return {
+ lidMotorState,
+ blockTargetTemp: moduleState.blockTargetTemp,
+ }
+ } else if (
+ 'targetTemperature' in moduleState &&
+ moduleState.type === 'temperatureModuleType'
+ ) {
+ return {
+ targetTemperature: moduleState.targetTemperature,
+ }
+ } else if ('targetTemp' in moduleState) {
+ return {
+ targetTemp: moduleState.targetTemp,
+ }
+ }
+ }
+ const labwareLoadedOnModule = allLabware.find(
+ lw => lw.slot === moduleOnDeck.id
+ )
+ const labwareInterfaceBoundingBox = {
+ xDimension: moduleDef.dimensions.labwareInterfaceXDimension ?? 0,
+ yDimension: moduleDef.dimensions.labwareInterfaceYDimension ?? 0,
+ zDimension: 0,
+ }
+ const controlSelectDimensions = {
+ xDimension: labwareLoadedOnModule?.def.dimensions.xDimension ?? 0,
+ yDimension: labwareLoadedOnModule?.def.dimensions.yDimension ?? 0,
+ zDimension: labwareLoadedOnModule?.def.dimensions.zDimension ?? 0,
+ }
+ return (
+
+
+ {labwareLoadedOnModule != null ? (
+ <>
+
+
+ >
+ ) : null}
+
+ {labwareLoadedOnModule == null ? (
+
+ ) : null}
+
+
+ )
+ })}
+
+ {/* on-deck warnings for OT-2 and GEN1 8-channels only */}
+ {multichannelWarningSlotIds.map(slotId => {
+ const slotPosition = getPositionFromSlotId(slotId, deckDef)
+ const slotBoundingBox = getAddressableAreaFromSlotId(slotId, deckDef)
+ ?.boundingBox
+ return slotPosition != null && slotBoundingBox != null ? (
+
+ ) : null
+ })}
+
+ {/* SlotControls for all empty deck */}
+ {deckDef.locations.addressableAreas
+ .filter(addressableArea => {
+ const stagingAreaAddressableAreas = getStagingAreaAddressableAreas(
+ stagingAreaCutoutIds
+ )
+ const addressableAreas =
+ isAddressableAreaStandardSlot(addressableArea.id, deckDef) ||
+ stagingAreaAddressableAreas.includes(addressableArea.id)
+ return (
+ addressableAreas &&
+ !slotIdsBlockedBySpanning.includes(addressableArea.id) &&
+ getSlotIsEmpty(activeDeckSetup, addressableArea.id) &&
+ addressableArea.id !== trashSlot
+ )
+ })
+ .map(addressableArea => {
+ return (
+
+
+
+ )
+ })}
+
+ {/* all labware on deck NOT those in modules */}
+ {allLabware.map(labware => {
+ if (
+ labware.slot === 'offDeck' ||
+ allModules.some(m => m.id === labware.slot) ||
+ allLabware.some(lab => lab.id === labware.slot)
+ )
+ return null
+
+ const slotPosition = getPositionFromSlotId(labware.slot, deckDef)
+ const slotBoundingBox = getAddressableAreaFromSlotId(
+ labware.slot,
+ deckDef
+ )?.boundingBox
+ if (slotPosition == null || slotBoundingBox == null) {
+ console.warn(`no slot ${labware.slot} for labware ${labware.id}!`)
+ return null
+ }
+ return (
+
+
+
+
+ )
+ })}
+
+ {/* all nested labwares on deck */}
+ {allLabware.map(labware => {
+ if (
+ allModules.some(m => m.id === labware.slot) ||
+ labware.slot === 'offDeck'
+ )
+ return null
+ if (
+ deckDef.locations.addressableAreas.some(
+ addressableArea => addressableArea.id === labware.slot
+ )
+ ) {
+ return null
+ }
+ const slotForOnTheDeck = allLabware.find(lab => lab.id === labware.slot)
+ ?.slot
+ const slotForOnMod = allModules.find(mod => mod.id === slotForOnTheDeck)
+ ?.slot
+ let slotPosition = null
+ if (slotForOnMod != null) {
+ slotPosition = getPositionFromSlotId(slotForOnMod, deckDef)
+ } else if (slotForOnTheDeck != null) {
+ slotPosition = getPositionFromSlotId(slotForOnTheDeck, deckDef)
+ }
+ if (slotPosition == null) {
+ console.warn(`no slot ${labware.slot} for labware ${labware.id}!`)
+ return null
+ }
+ const slotBoundingBox: Dimensions = {
+ xDimension: labware.def.dimensions.xDimension,
+ yDimension: labware.def.dimensions.yDimension,
+ zDimension: labware.def.dimensions.zDimension,
+ }
+ const slotOnDeck =
+ slotForOnTheDeck != null
+ ? allModules.find(module => module.id === slotForOnTheDeck)?.slot
+ : null
+ return (
+
+
+
+
+ )
+ })}
+ >
+ )
+}
diff --git a/protocol-designer/src/pages/ProtocolOverview/DeckSetup/SlotDetailsContainer.tsx b/protocol-designer/src/pages/ProtocolOverview/DeckSetup/SlotDetailsContainer.tsx
new file mode 100644
index 00000000000..62b24855db6
--- /dev/null
+++ b/protocol-designer/src/pages/ProtocolOverview/DeckSetup/SlotDetailsContainer.tsx
@@ -0,0 +1,30 @@
+import * as React from 'react'
+
+import {
+ LegacyStyledText,
+ RobotCoordsForeignObject,
+} from '@opentrons/components'
+import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data'
+
+import type { RobotType } from '@opentrons/shared-data'
+
+interface SlotDetailContainerProps {
+ robotType: RobotType
+}
+
+export const SlotDetailsContainer = (
+ props: SlotDetailContainerProps
+): JSX.Element | null => {
+ const { robotType } = props
+ return (
+
+ {/* TODO(ja, 8/6/24): wire up slot information */}
+ Slot information
+
+ )
+}
diff --git a/protocol-designer/src/pages/ProtocolOverview/DeckSetup/index.tsx b/protocol-designer/src/pages/ProtocolOverview/DeckSetup/index.tsx
new file mode 100644
index 00000000000..44048778521
--- /dev/null
+++ b/protocol-designer/src/pages/ProtocolOverview/DeckSetup/index.tsx
@@ -0,0 +1,258 @@
+import * as React from 'react'
+
+import { useSelector } from 'react-redux'
+import {
+ COLORS,
+ DeckFromLayers,
+ Flex,
+ FlexTrash,
+ LegacyStyledText,
+ PrimaryButton,
+ RobotCoordinateSpaceWithRef,
+ SingleSlotFixture,
+ SlotLabels,
+ StagingAreaFixture,
+ WasteChuteFixture,
+ WasteChuteStagingAreaFixture,
+} from '@opentrons/components'
+import {
+ FLEX_ROBOT_TYPE,
+ getDeckDefFromRobotType,
+ isAddressableAreaStandardSlot,
+ OT2_ROBOT_TYPE,
+ STAGING_AREA_CUTOUTS,
+ TRASH_BIN_ADAPTER_FIXTURE,
+ WASTE_CHUTE_CUTOUT,
+} from '@opentrons/shared-data'
+import { getSelectedTerminalItemId } from '../../../ui/steps'
+import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations'
+import { getDisableModuleRestrictions } from '../../../feature-flags/selectors'
+import { getRobotType } from '../../../file-data/selectors'
+import { getHasGen1MultiChannelPipette } from '../../../step-forms'
+import { SlotDetailsContainer } from './SlotDetailsContainer'
+import { DeckSetupDetails } from './DeckSetupDetails'
+import { getCutoutIdForAddressableArea } from './utils'
+
+import type { StagingAreaLocation, TrashCutoutId } from '@opentrons/components'
+import type { AddressableAreaName, CutoutId } from '@opentrons/shared-data'
+import type {
+ AdditionalEquipmentEntity,
+ DeckSlot,
+} from '@opentrons/step-generation'
+
+interface DeckSetupProps {
+ onCancel: () => void
+ onSave: () => void
+}
+interface OpenSlot {
+ cutoutId: CutoutId
+ slot: DeckSlot
+}
+
+const WASTE_CHUTE_SPACE = 30
+const OT2_STANDARD_DECK_VIEW_LAYER_BLOCK_LIST: string[] = [
+ 'calibrationMarkings',
+ 'fixedBase',
+ 'doorStops',
+ 'metalFrame',
+ 'removalHandle',
+ 'removableDeckOutline',
+ 'screwHoles',
+ 'fixedTrash',
+]
+
+const lightFill = COLORS.grey35
+const darkFill = COLORS.grey60
+
+export const DeckSetup = (props: DeckSetupProps): JSX.Element => {
+ const { onCancel, onSave } = props
+ const selectedTerminalItemId = useSelector(getSelectedTerminalItemId)
+ const activeDeckSetup = useSelector(getDeckSetupForActiveItem)
+ const _disableCollisionWarnings = useSelector(getDisableModuleRestrictions)
+ const robotType = useSelector(getRobotType)
+ const deckDef = React.useMemo(() => getDeckDefFromRobotType(robotType), [])
+ const [hover, setHover] = React.useState(null)
+ const [zoomIn, setZoomInOnSlot] = React.useState(null)
+ const trash = Object.values(activeDeckSetup.additionalEquipmentOnDeck).find(
+ ae => ae.name === 'trashBin'
+ )
+
+ const addEquipment = (slotId: string): void => {
+ const cutoutId =
+ getCutoutIdForAddressableArea(
+ slotId as AddressableAreaName,
+ deckDef.cutoutFixtures
+ ) ?? 'cutoutD1'
+ setZoomInOnSlot({ cutoutId, slot: slotId })
+ }
+
+ const trashSlot = trash?.location
+
+ const _hasGen1MultichannelPipette = React.useMemo(
+ () => getHasGen1MultiChannelPipette(activeDeckSetup.pipettes),
+ [activeDeckSetup.pipettes]
+ )
+ const showGen1MultichannelCollisionWarnings =
+ !_disableCollisionWarnings && _hasGen1MultichannelPipette
+
+ const trashBinFixtures = [
+ {
+ cutoutId: trash?.location as CutoutId,
+ cutoutFixtureId: TRASH_BIN_ADAPTER_FIXTURE,
+ },
+ ]
+ const wasteChuteFixtures = Object.values(
+ activeDeckSetup.additionalEquipmentOnDeck
+ ).filter(
+ aE =>
+ WASTE_CHUTE_CUTOUT.includes(aE.location as CutoutId) &&
+ aE.name === 'wasteChute'
+ )
+ const stagingAreaFixtures: AdditionalEquipmentEntity[] = Object.values(
+ activeDeckSetup.additionalEquipmentOnDeck
+ ).filter(
+ aE =>
+ STAGING_AREA_CUTOUTS.includes(aE.location as CutoutId) &&
+ aE.name === 'stagingArea'
+ )
+
+ const wasteChuteStagingAreaFixtures = Object.values(
+ activeDeckSetup.additionalEquipmentOnDeck
+ ).filter(
+ aE =>
+ STAGING_AREA_CUTOUTS.includes(aE.location as CutoutId) &&
+ aE.name === 'stagingArea' &&
+ aE.location === WASTE_CHUTE_CUTOUT &&
+ wasteChuteFixtures.length > 0
+ )
+
+ const hasWasteChute =
+ wasteChuteFixtures.length > 0 || wasteChuteStagingAreaFixtures.length > 0
+
+ const filteredAddressableAreas = deckDef.locations.addressableAreas.filter(
+ aa => isAddressableAreaStandardSlot(aa.id, deckDef)
+ )
+ return (
+ <>
+ cancel
+ save
+ {zoomIn != null ? (
+ // TODO(ja, 8/6/24): still need to develop the zoomed in slot
+ you zoomed in on the slot!
+ ) : (
+
+
+ {() => (
+ <>
+ {robotType === OT2_ROBOT_TYPE ? (
+
+ ) : (
+ <>
+ {filteredAddressableAreas.map(addressableArea => {
+ const cutoutId = getCutoutIdForAddressableArea(
+ addressableArea.id,
+ deckDef.cutoutFixtures
+ )
+ return cutoutId != null ? (
+
+ ) : null
+ })}
+ {stagingAreaFixtures.map(fixture => (
+
+ ))}
+ {trash != null
+ ? trashBinFixtures.map(({ cutoutId }) =>
+ cutoutId != null ? (
+
+
+
+
+ ) : null
+ )
+ : null}
+ {wasteChuteFixtures.map(fixture => (
+
+ ))}
+ {wasteChuteStagingAreaFixtures.map(fixture => (
+
+ ))}
+ >
+ )}
+ areas.location as CutoutId
+ )}
+ {...{
+ deckDef,
+ showGen1MultichannelCollisionWarnings,
+ }}
+ />
+ 0}
+ />
+ {hover != null ? (
+
+ ) : null}
+ >
+ )}
+
+
+ )}
+ >
+ )
+}
diff --git a/protocol-designer/src/pages/ProtocolOverview/DeckSetup/utils.ts b/protocol-designer/src/pages/ProtocolOverview/DeckSetup/utils.ts
new file mode 100644
index 00000000000..7e0e3457352
--- /dev/null
+++ b/protocol-designer/src/pages/ProtocolOverview/DeckSetup/utils.ts
@@ -0,0 +1,20 @@
+import type {
+ AddressableAreaName,
+ CutoutFixture,
+ CutoutId,
+} from '@opentrons/shared-data'
+
+export function getCutoutIdForAddressableArea(
+ addressableArea: AddressableAreaName,
+ cutoutFixtures: CutoutFixture[]
+): CutoutId | null {
+ return cutoutFixtures.reduce((acc, cutoutFixture) => {
+ const [cutoutId] =
+ Object.entries(
+ cutoutFixture.providesAddressableAreas
+ ).find(([_cutoutId, providedAAs]) =>
+ providedAAs.includes(addressableArea)
+ ) ?? []
+ return (cutoutId as CutoutId) ?? acc
+ }, null)
+}
diff --git a/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx b/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx
index d2b34d8ff5e..2a12f2b1df8 100644
--- a/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx
+++ b/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolOverview.test.tsx
@@ -1,3 +1,26 @@
-import { it } from 'vitest'
+import * as React from 'react'
+import { describe, it, vi, beforeEach } from 'vitest'
+import { renderWithProviders } from '../../../__testing-utils__'
+import { i18n } from '../../../localization'
+import { DeckSetup } from '../DeckSetup'
+import { ProtocolOverview } from '../index'
+import { fireEvent, screen } from '@testing-library/react'
-it.todo('write test for ProtocolOverview')
+vi.mock('../DeckSetup')
+
+const render = () => {
+ return renderWithProviders(, {
+ i18nInstance: i18n,
+ })[0]
+}
+
+describe('ProtocolOverview', () => {
+ beforeEach(() => {
+ vi.mocked(DeckSetup).mockReturnValue(mock DeckSetup
)
+ })
+ it('renders the deck setup component when the button is clicked', () => {
+ render()
+ fireEvent.click(screen.getByText('go to deck setup'))
+ screen.getByText('mock DeckSetup')
+ })
+})
diff --git a/protocol-designer/src/pages/ProtocolOverview/index.tsx b/protocol-designer/src/pages/ProtocolOverview/index.tsx
index a6be423cc74..298a3b5e323 100644
--- a/protocol-designer/src/pages/ProtocolOverview/index.tsx
+++ b/protocol-designer/src/pages/ProtocolOverview/index.tsx
@@ -1,8 +1,31 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
+import { Flex, PrimaryButton, SPACING } from '@opentrons/components'
+import { DeckSetup } from './DeckSetup'
export function ProtocolOverview(): JSX.Element {
const { t } = useTranslation('protocol_overview')
+ const [deckSetup, setDeckSetup] = React.useState(false)
- return {t('protocol_overview')}
+ return deckSetup ? (
+ {
+ setDeckSetup(false)
+ }}
+ onSave={() => {
+ setDeckSetup(false)
+ }}
+ />
+ ) : (
+
+ {t('protocol_overview')}
+ {
+ setDeckSetup(true)
+ }}
+ >
+ go to deck setup
+
+
+ )
}