diff --git a/app/src/App/OnDeviceDisplayApp.tsx b/app/src/App/OnDeviceDisplayApp.tsx
index 835e005d256..1459ff5071f 100644
--- a/app/src/App/OnDeviceDisplayApp.tsx
+++ b/app/src/App/OnDeviceDisplayApp.tsx
@@ -32,6 +32,7 @@ import { RobotDashboard } from '../pages/RobotDashboard'
import { RobotSettingsDashboard } from '../pages/RobotSettingsDashboard'
import { ProtocolDashboard } from '../pages/ProtocolDashboard'
import { ProtocolDetails } from '../pages/ProtocolDetails'
+import { QuickTransferFlow } from '../organisms/QuickTransferFlow'
import { RunningProtocol } from '../pages/RunningProtocol'
import { RunSummary } from '../pages/RunSummary'
import { UpdateRobot } from '../pages/UpdateRobot/UpdateRobot'
@@ -73,6 +74,7 @@ export const ON_DEVICE_DISPLAY_PATHS = [
'/network-setup/wifi',
'/protocols',
'/protocols/:protocolId',
+ '/quick-transfer',
'/robot-settings',
'/robot-settings/rename-robot',
'/robot-settings/update-robot',
@@ -109,6 +111,8 @@ function getPathComponent(
return
case '/protocols/:protocolId':
return
+ case `/quick-transfer`:
+ return
case '/robot-settings':
return
case '/robot-settings/rename-robot':
diff --git a/app/src/assets/localization/en/index.ts b/app/src/assets/localization/en/index.ts
index c74aab09de5..51acf92db53 100644
--- a/app/src/assets/localization/en/index.ts
+++ b/app/src/assets/localization/en/index.ts
@@ -21,6 +21,7 @@ import protocol_details from './protocol_details.json'
import protocol_info from './protocol_info.json'
import protocol_list from './protocol_list.json'
import protocol_setup from './protocol_setup.json'
+import quick_transfer from './quick_transfer.json'
import robot_calibration from './robot_calibration.json'
import robot_controls from './robot_controls.json'
import run_details from './run_details.json'
@@ -51,6 +52,7 @@ export const en = {
protocol_info,
protocol_list,
protocol_setup,
+ quick_transfer,
robot_calibration,
robot_controls,
run_details,
diff --git a/app/src/assets/localization/en/quick_transfer.json b/app/src/assets/localization/en/quick_transfer.json
new file mode 100644
index 00000000000..45732a28114
--- /dev/null
+++ b/app/src/assets/localization/en/quick_transfer.json
@@ -0,0 +1,18 @@
+{
+ "create_new_transfer": "Create new quick transfer",
+ "select_attached_pipette": "Select attached pipette",
+ "select_dest_labware": "Select destination labware",
+ "select_dest_wells": "Select destination wells",
+ "select_source_labware": "Select source labware",
+ "select_source_wells": "Select source wells",
+ "select_tip_rack": "Select tip rack",
+ "set_aspirate_volume": "Set aspirate volume",
+ "set_dispense_volume": "Set dispense volume",
+ "set_transfer_volume": "Set transfer volume",
+ "use_deck_slots": "Quick transfers use deck slots B2-D2. These slots hold a tip rack, a source labware, and a destination labware.Make sure that your deck configuration is up to date to avoid collisions.",
+ "tip_rack": "Tip rack",
+ "labware": "Labware",
+ "pipette_currently_attached": "Quick transfer options depend on the pipettes currently attached to your robot.",
+ "well_selection": "Well selection",
+ "well_ratio": "Quick transfers with multiple source wells can either be one-to-one (select {{wells}} for this transfer) or consolidate (select 1 destination well)."
+}
diff --git a/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx b/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx
index da15b3af90e..cddbb2cd7a3 100644
--- a/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx
+++ b/app/src/organisms/ChildNavigation/ChildNavigation.stories.tsx
@@ -15,6 +15,13 @@ const Template: Story> = args => (
export const Default = Template.bind({})
Default.args = {
header: 'Header',
+ onClickBack: () => {},
+}
+
+export const TitleNoBackButton = Template.bind({})
+TitleNoBackButton.args = {
+ header: 'Header',
+ onClickBack: undefined,
}
export const TitleWithNormalSmallButton = Template.bind({})
@@ -22,6 +29,16 @@ TitleWithNormalSmallButton.args = {
header: 'Header',
buttonText: 'ButtonText',
onClickButton: () => {},
+ onClickBack: () => {},
+}
+
+export const TitleWithNormalSmallButtonDisabled = Template.bind({})
+TitleWithNormalSmallButtonDisabled.args = {
+ header: 'Header',
+ buttonText: 'ButtonText',
+ onClickButton: () => {},
+ onClickBack: () => {},
+ buttonIsDisabled: true,
}
export const TitleWithLinkButton = Template.bind({})
@@ -32,6 +49,7 @@ TitleWithLinkButton.args = {
iconName: 'information',
iconPlacement: 'startIcon',
onClickButton: () => {},
+ onClickBack: () => {},
}
export const TitleWithTwoButtons = Template.bind({})
@@ -47,4 +65,5 @@ TitleWithTwoButtons.args = {
buttonText: 'ButtonText',
onClickButton: () => {},
secondaryButtonProps,
+ onClickBack: () => {},
}
diff --git a/app/src/organisms/ChildNavigation/__tests__/ChildNavigation.test.tsx b/app/src/organisms/ChildNavigation/__tests__/ChildNavigation.test.tsx
index 8f53b640187..8e2a1c7ec0e 100644
--- a/app/src/organisms/ChildNavigation/__tests__/ChildNavigation.test.tsx
+++ b/app/src/organisms/ChildNavigation/__tests__/ChildNavigation.test.tsx
@@ -72,4 +72,26 @@ describe('ChildNavigation', () => {
fireEvent.click(secondaryButton)
expect(mockOnClickSecondaryButton).toHaveBeenCalled()
})
+ it.fails(
+ 'should not render back button if onClickBack does not exist',
+ () => {
+ props = {
+ ...props,
+ onClickBack: undefined,
+ }
+ render(props)
+ screen.getByTestId('ChildNavigation_Back_Button')
+ }
+ )
+ it('should render button as disabled', () => {
+ props = {
+ ...props,
+ buttonText: 'mock button',
+ onClickButton: mockOnClickButton,
+ buttonIsDisabled: true,
+ }
+ render(props)
+ const button = screen.getByTestId('ChildNavigation_Primary_Button')
+ expect(button).toBeDisabled()
+ })
})
diff --git a/app/src/organisms/ChildNavigation/index.tsx b/app/src/organisms/ChildNavigation/index.tsx
index afe3c1f7508..e076f7191af 100644
--- a/app/src/organisms/ChildNavigation/index.tsx
+++ b/app/src/organisms/ChildNavigation/index.tsx
@@ -20,20 +20,21 @@ import { ODD_FOCUS_VISIBLE } from '../../atoms/buttons/constants'
import { SmallButton } from '../../atoms/buttons'
import { InlineNotification } from '../../atoms/InlineNotification'
-import type { IconName } from '@opentrons/components'
+import type { IconName, StyleProps } from '@opentrons/components'
import type { InlineNotificationProps } from '../../atoms/InlineNotification'
import type {
IconPlacement,
SmallButtonTypes,
} from '../../atoms/buttons/SmallButton'
-interface ChildNavigationProps {
+interface ChildNavigationProps extends StyleProps {
header: string
- onClickBack: React.MouseEventHandler
+ onClickBack?: React.MouseEventHandler
buttonText?: React.ReactNode
inlineNotification?: InlineNotificationProps
onClickButton?: React.MouseEventHandler
buttonType?: SmallButtonTypes
+ buttonIsDisabled?: boolean
iconName?: IconName
iconPlacement?: IconPlacement
secondaryButtonProps?: React.ComponentProps
@@ -49,6 +50,8 @@ export function ChildNavigation({
iconName,
iconPlacement,
secondaryButtonProps,
+ buttonIsDisabled,
+ ...styleProps
}: ChildNavigationProps): JSX.Element {
return (
-
-
-
+ {onClickBack != null ? (
+
+
+
+ ) : null}
{header}
@@ -87,6 +93,8 @@ export function ChildNavigation({
onClick={onClickButton}
iconName={iconName}
iconPlacement={iconPlacement}
+ disabled={buttonIsDisabled}
+ data-testid="ChildNavigation_Primary_Button"
/>
) : null}
diff --git a/app/src/organisms/QuickTransferFlow/CreateNewTransfer.tsx b/app/src/organisms/QuickTransferFlow/CreateNewTransfer.tsx
new file mode 100644
index 00000000000..f1b795e5fc3
--- /dev/null
+++ b/app/src/organisms/QuickTransferFlow/CreateNewTransfer.tsx
@@ -0,0 +1,74 @@
+import * as React from 'react'
+import { useTranslation, Trans } from 'react-i18next'
+import {
+ Flex,
+ SPACING,
+ StyledText,
+ DeckConfigurator,
+ TYPOGRAPHY,
+ DIRECTION_COLUMN,
+} from '@opentrons/components'
+import { SmallButton } from '../../atoms/buttons'
+import { useDeckConfigurationQuery } from '@opentrons/react-api-client'
+import { ChildNavigation } from '../ChildNavigation'
+
+interface CreateNewTransferProps {
+ onNext: () => void
+ exitButtonProps: React.ComponentProps
+}
+
+export function CreateNewTransfer(props: CreateNewTransferProps): JSX.Element {
+ const { i18n, t } = useTranslation(['quick_transfer', 'shared'])
+ const deckConfig = useDeckConfigurationQuery().data ?? []
+ return (
+
+
+
+
+
+
+ ),
+ }}
+ />
+
+
+ {}}
+ handleClickRemove={() => {}}
+ additionalStaticFixtures={[
+ { location: 'cutoutB2', label: t('tip_rack') },
+ { location: 'cutoutC2', label: t('labware') },
+ { location: 'cutoutD2', label: t('labware') },
+ ]}
+ />
+
+
+
+
+ )
+}
diff --git a/app/src/organisms/QuickTransferFlow/__tests__/CreateNewTransfer.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/CreateNewTransfer.test.tsx
new file mode 100644
index 00000000000..abeba9a2b1d
--- /dev/null
+++ b/app/src/organisms/QuickTransferFlow/__tests__/CreateNewTransfer.test.tsx
@@ -0,0 +1,62 @@
+import * as React from 'react'
+import { fireEvent, screen } from '@testing-library/react'
+import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest'
+import { DeckConfigurator } from '@opentrons/components'
+
+import { renderWithProviders } from '../../../__testing-utils__'
+import { i18n } from '../../../i18n'
+import { CreateNewTransfer } from '../CreateNewTransfer'
+
+import type * as OpentronsComponents from '@opentrons/components'
+
+vi.mock('@opentrons/components', async importOriginal => {
+ const actual = await importOriginal()
+ return {
+ ...actual,
+ DeckConfigurator: vi.fn(),
+ }
+})
+const render = (props: React.ComponentProps) => {
+ return renderWithProviders(, {
+ i18nInstance: i18n,
+ })
+}
+
+describe('CreateNewTransfer', () => {
+ let props: React.ComponentProps
+
+ beforeEach(() => {
+ props = {
+ onNext: vi.fn(),
+ exitButtonProps: {
+ buttonType: 'tertiaryLowLight',
+ buttonText: 'Exit',
+ onClick: vi.fn(),
+ },
+ }
+ })
+ afterEach(() => {
+ vi.resetAllMocks()
+ })
+
+ it('renders the create new transfer screen and header', () => {
+ render(props)
+ screen.getByText('Create new quick transfer')
+ screen.getByText(
+ 'Quick transfers use deck slots B2-D2. These slots hold a tip rack, a source labware, and a destination labware.'
+ )
+ screen.getByText(
+ 'Make sure that your deck configuration is up to date to avoid collisions.'
+ )
+ expect(vi.mocked(DeckConfigurator)).toHaveBeenCalled()
+ })
+ it('renders exit and continue buttons and they work as expected', () => {
+ render(props)
+ const exitBtn = screen.getByText('Exit')
+ fireEvent.click(exitBtn)
+ expect(props.exitButtonProps.onClick).toHaveBeenCalled()
+ const continueBtn = screen.getByText('Continue')
+ fireEvent.click(continueBtn)
+ expect(props.onNext).toHaveBeenCalled()
+ })
+})
diff --git a/app/src/organisms/QuickTransferFlow/constants.ts b/app/src/organisms/QuickTransferFlow/constants.ts
new file mode 100644
index 00000000000..3241759a044
--- /dev/null
+++ b/app/src/organisms/QuickTransferFlow/constants.ts
@@ -0,0 +1,9 @@
+export const ACTIONS = {
+ SELECT_PIPETTE: 'SELECT_PIPETTE',
+ SELECT_TIP_RACK: 'SELECT_TIP_RACK',
+ SET_SOURCE_LABWARE: 'SET_SOURCE_LABWARE',
+ SET_SOURCE_WELLS: 'SET_SOURCE_WELLS',
+ SET_DEST_LABWARE: 'SET_DEST_LABWARE',
+ SET_DEST_WELLS: 'SET_DEST_WELLS',
+ SET_VOLUME: 'SET_VOLUME',
+} as const
diff --git a/app/src/organisms/QuickTransferFlow/index.tsx b/app/src/organisms/QuickTransferFlow/index.tsx
new file mode 100644
index 00000000000..4031c7aa7bf
--- /dev/null
+++ b/app/src/organisms/QuickTransferFlow/index.tsx
@@ -0,0 +1,167 @@
+import * as React from 'react'
+import { useHistory } from 'react-router-dom'
+import { useTranslation } from 'react-i18next'
+import { Flex, StepMeter, SPACING } from '@opentrons/components'
+import { SmallButton } from '../../atoms/buttons'
+import { ChildNavigation } from '../ChildNavigation'
+import { CreateNewTransfer } from './CreateNewTransfer'
+
+import type {
+ QuickTransferSetupState,
+ QuickTransferWizardAction,
+} from './types'
+
+const QUICK_TRANSFER_WIZARD_STEPS = 8
+
+// const initialQuickTransferState: QuickTransferSetupState = {}
+export function reducer(
+ state: QuickTransferSetupState,
+ action: QuickTransferWizardAction
+): QuickTransferSetupState {
+ switch (action.type) {
+ case 'SELECT_PIPETTE': {
+ return {
+ pipette: action.pipette,
+ }
+ }
+ case 'SELECT_TIP_RACK': {
+ return {
+ pipette: state.pipette,
+ tipRack: action.tipRack,
+ }
+ }
+ case 'SET_SOURCE_LABWARE': {
+ return {
+ pipette: state.pipette,
+ tipRack: state.tipRack,
+ source: action.labware,
+ }
+ }
+ case 'SET_SOURCE_WELLS': {
+ return {
+ pipette: state.pipette,
+ tipRack: state.tipRack,
+ source: state.source,
+ sourceWells: action.wells,
+ }
+ }
+ case 'SET_DEST_LABWARE': {
+ return {
+ pipette: state.pipette,
+ tipRack: state.tipRack,
+ source: state.source,
+ sourceWells: state.sourceWells,
+ destination: action.labware,
+ }
+ }
+ case 'SET_DEST_WELLS': {
+ return {
+ pipette: state.pipette,
+ tipRack: state.tipRack,
+ source: state.source,
+ sourceWells: state.sourceWells,
+ destination: state.destination,
+ destinationWells: action.wells,
+ }
+ }
+ case 'SET_VOLUME': {
+ return {
+ pipette: state.pipette,
+ tipRack: state.tipRack,
+ source: state.source,
+ sourceWells: state.sourceWells,
+ destination: state.destination,
+ destinationWells: state.destinationWells,
+ volume: action.volume,
+ }
+ }
+ }
+}
+
+export const QuickTransferFlow = (): JSX.Element => {
+ const history = useHistory()
+ const { i18n, t } = useTranslation(['quick_transfer', 'shared'])
+ // const [state, dispatch] = React.useReducer(reducer, initialQuickTransferState)
+ const [currentStep, setCurrentStep] = React.useState(1)
+ const [continueIsDisabled] = React.useState(false)
+
+ // every child component will take state as a prop, an anonymous
+ // dispatch function related to that step (except create new),
+ // and a function to disable the continue button
+
+ const exitButtonProps: React.ComponentProps = {
+ buttonType: 'tertiaryLowLight',
+ buttonText: i18n.format(t('shared:exit'), 'capitalize'),
+ onClick: () => {
+ history.push('protocols')
+ },
+ }
+ const ORDERED_STEP_HEADERS: string[] = [
+ t('create_new_transfer'),
+ t('select_attached_pipette'),
+ t('select_tip_rack'),
+ t('select_source_labware'),
+ t('select_source_wells'),
+ t('select_dest_labware'),
+ t('select_dest_wells'),
+ t('set_transfer_volume'),
+ ]
+
+ const header = ORDERED_STEP_HEADERS[currentStep - 1]
+ let modalContent: JSX.Element | null = null
+ if (currentStep === 1) {
+ modalContent = (
+ setCurrentStep(prevStep => prevStep + 1)}
+ exitButtonProps={exitButtonProps}
+ />
+ )
+ } else {
+ modalContent = null
+ }
+
+ // until each page is wired up, show header title with empty screen
+
+ return (
+ <>
+
+ {modalContent == null ? (
+
+ {
+ setCurrentStep(prevStep => prevStep - 1)
+ }
+ }
+ buttonText={i18n.format(t('shared:continue'), 'capitalize')}
+ onClickButton={() => {
+ if (currentStep === 8) {
+ history.push('protocols')
+ } else {
+ setCurrentStep(prevStep => prevStep + 1)
+ }
+ }}
+ buttonIsDisabled={continueIsDisabled}
+ secondaryButtonProps={{
+ buttonType: 'tertiaryLowLight',
+ buttonText: i18n.format(t('shared:exit'), 'capitalize'),
+ onClick: () => {
+ history.push('protocols')
+ },
+ }}
+ top={SPACING.spacing8}
+ />
+ {modalContent}
+
+ ) : (
+ modalContent
+ )}
+ >
+ )
+}
diff --git a/app/src/organisms/QuickTransferFlow/types.ts b/app/src/organisms/QuickTransferFlow/types.ts
new file mode 100644
index 00000000000..2087a98be37
--- /dev/null
+++ b/app/src/organisms/QuickTransferFlow/types.ts
@@ -0,0 +1,51 @@
+import { ACTIONS } from './constants'
+import type { PipetteData } from '@opentrons/api-client'
+import type { LabwareDefinition1 } from '@opentrons/shared-data'
+
+export interface QuickTransferSetupState {
+ pipette?: PipetteData
+ tipRack?: LabwareDefinition1
+ source?: LabwareDefinition1
+ sourceWells?: string[]
+ destination?: LabwareDefinition1
+ destinationWells?: string[]
+ volume?: number
+}
+
+export type QuickTransferWizardAction =
+ | SelectPipetteAction
+ | SelectTipRackAction
+ | SetSourceLabwareAction
+ | SetSourceWellsAction
+ | SetDestLabwareAction
+ | SetDestWellsAction
+ | SetVolumeAction
+
+interface SelectPipetteAction {
+ type: typeof ACTIONS.SELECT_PIPETTE
+ pipette: PipetteData
+}
+interface SelectTipRackAction {
+ type: typeof ACTIONS.SELECT_TIP_RACK
+ tipRack: LabwareDefinition1
+}
+interface SetSourceLabwareAction {
+ type: typeof ACTIONS.SET_SOURCE_LABWARE
+ labware: LabwareDefinition1
+}
+interface SetSourceWellsAction {
+ type: typeof ACTIONS.SET_SOURCE_WELLS
+ wells: string[]
+}
+interface SetDestLabwareAction {
+ type: typeof ACTIONS.SET_DEST_LABWARE
+ labware: LabwareDefinition1
+}
+interface SetDestWellsAction {
+ type: typeof ACTIONS.SET_DEST_WELLS
+ wells: string[]
+}
+interface SetVolumeAction {
+ type: typeof ACTIONS.SET_VOLUME
+ volume: number
+}
diff --git a/app/src/pages/ProtocolDashboard/index.tsx b/app/src/pages/ProtocolDashboard/index.tsx
index e326ab7176c..dd4aa89b1ac 100644
--- a/app/src/pages/ProtocolDashboard/index.tsx
+++ b/app/src/pages/ProtocolDashboard/index.tsx
@@ -1,4 +1,5 @@
import * as React from 'react'
+import { useHistory } from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux'
import { useTranslation } from 'react-i18next'
@@ -38,6 +39,7 @@ import type { ProtocolResource } from '@opentrons/shared-data'
export function ProtocolDashboard(): JSX.Element {
const protocols = useAllProtocolsQuery()
const runs = useNotifyAllRunsQuery()
+ const history = useHistory()
const { t } = useTranslation('protocol_info')
const dispatch = useDispatch()
const [navMenuIsOpened, setNavMenuIsOpened] = React.useState(false)
@@ -58,6 +60,9 @@ export function ProtocolDashboard(): JSX.Element {
const pinnedProtocolIds = useSelector(getPinnedProtocolIds) ?? []
const pinnedProtocols: ProtocolResource[] = []
+ // TODO(sb, 4/15/24): The quick transfer button is going to be moved to a new quick transfer
+ // tab before the feature is released. Because of this, we're not adding test cov
+ // for this button in ProtocolDashboard
const enableQuickTransferFF = useFeatureFlag('enableQuickTransfer')
// We only need to grab out the pinned protocol data once all the protocols load
@@ -280,7 +285,7 @@ export function ProtocolDashboard(): JSX.Element {
buttonText={t('quick_transfer')}
iconName="plus"
onClick={() => {
- console.log('launch quick transfer flow')
+ history.push('/quick-transfer')
}}
/>
)}
diff --git a/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx b/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx
index dc900541fd6..f29b2cffc02 100644
--- a/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx
+++ b/components/src/hardware-sim/DeckConfigurator/DeckConfigurator.stories.tsx
@@ -71,6 +71,12 @@ const deckConfig: CutoutConfig[] = [
},
]
+const staticFixtures = [
+ { location: 'cutoutB2', label: 'Tip rack' },
+ { location: 'cutoutC2', label: 'Labware' },
+ { location: 'cutoutD2', label: 'Labware' },
+]
+
export const Default = Template.bind({})
Default.args = {
deckConfig,
@@ -85,3 +91,12 @@ ReadOnly.args = {
handleClickRemove: cutoutId => console.log(`remove at ${cutoutId}`),
readOnly: true,
}
+
+export const ReadOnlyWithStaticFixtures = Template.bind({})
+ReadOnlyWithStaticFixtures.args = {
+ deckConfig,
+ handleClickAdd: () => {},
+ handleClickRemove: () => {},
+ readOnly: true,
+ additionalStaticFixtures: staticFixtures,
+}
diff --git a/components/src/hardware-sim/DeckConfigurator/StaticFixture.tsx b/components/src/hardware-sim/DeckConfigurator/StaticFixture.tsx
new file mode 100644
index 00000000000..a3722d51269
--- /dev/null
+++ b/components/src/hardware-sim/DeckConfigurator/StaticFixture.tsx
@@ -0,0 +1,56 @@
+import * as React from 'react'
+
+import { Btn, Text } from '../../primitives'
+import { TYPOGRAPHY } from '../../ui-style-constants'
+import { RobotCoordsForeignObject } from '../Deck/RobotCoordsForeignObject'
+import {
+ CONFIG_STYLE_READ_ONLY,
+ FIXTURE_HEIGHT,
+ MIDDLE_SLOT_FIXTURE_WIDTH,
+ Y_ADJUSTMENT,
+ COLUMN_2_X_ADJUSTMENT,
+} from './constants'
+
+import type { CutoutId, DeckDefinition } from '@opentrons/shared-data'
+
+interface StaticFixtureProps {
+ deckDefinition: DeckDefinition
+ fixtureLocation: CutoutId
+ label: string
+}
+
+/**
+ * this component allows us to add static labeled fixtures to the center column of a deck
+ * config map
+ */
+
+export function StaticFixture(props: StaticFixtureProps): JSX.Element {
+ const { deckDefinition, fixtureLocation, label } = props
+
+ const staticCutout = deckDefinition.locations.cutouts.find(
+ cutout => cutout.id === fixtureLocation
+ )
+
+ /**
+ * deck definition cutout position is the position of the single slot located within that cutout
+ * so, to get the position of the cutout itself we must add an adjustment to the slot position
+ */
+ const [xSlotPosition = 0, ySlotPosition = 0] = staticCutout?.position ?? []
+ const y = ySlotPosition + Y_ADJUSTMENT
+ const x = xSlotPosition + COLUMN_2_X_ADJUSTMENT
+
+ return (
+
+ {}}>
+ {label}
+
+
+ )
+}
diff --git a/components/src/hardware-sim/DeckConfigurator/constants.ts b/components/src/hardware-sim/DeckConfigurator/constants.ts
index 53faef10b7e..388dcdedecc 100644
--- a/components/src/hardware-sim/DeckConfigurator/constants.ts
+++ b/components/src/hardware-sim/DeckConfigurator/constants.ts
@@ -9,10 +9,12 @@ import { RESPONSIVENESS, SPACING } from '../../ui-style-constants'
* Position is relative to deck definition slot positions and a custom stroke applied to the single slot fixture SVG
*/
export const FIXTURE_HEIGHT = 102.0
+export const MIDDLE_SLOT_FIXTURE_WIDTH = 158.5
export const SINGLE_SLOT_FIXTURE_WIDTH = 243.5
export const STAGING_AREA_FIXTURE_WIDTH = 314.5
export const COLUMN_1_X_ADJUSTMENT = -100
+export const COLUMN_2_X_ADJUSTMENT = -15.5
export const COLUMN_3_X_ADJUSTMENT = -15.5
export const Y_ADJUSTMENT = -8
diff --git a/components/src/hardware-sim/DeckConfigurator/index.tsx b/components/src/hardware-sim/DeckConfigurator/index.tsx
index 9378471d8e0..8477b20e875 100644
--- a/components/src/hardware-sim/DeckConfigurator/index.tsx
+++ b/components/src/hardware-sim/DeckConfigurator/index.tsx
@@ -18,6 +18,7 @@ import { EmptyConfigFixture } from './EmptyConfigFixture'
import { StagingAreaConfigFixture } from './StagingAreaConfigFixture'
import { TrashBinConfigFixture } from './TrashBinConfigFixture'
import { WasteChuteConfigFixture } from './WasteChuteConfigFixture'
+import { StaticFixture } from './StaticFixture'
import type { CutoutId, DeckConfiguration } from '@opentrons/shared-data'
@@ -30,6 +31,7 @@ interface DeckConfiguratorProps {
readOnly?: boolean
showExpansion?: boolean
children?: React.ReactNode
+ additionalStaticFixtures?: Array<{ location: CutoutId; label: string }>
}
export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element {
@@ -41,6 +43,7 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element {
darkFill = COLORS.black90,
readOnly = false,
showExpansion = true,
+ additionalStaticFixtures,
children,
} = props
const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE)
@@ -143,6 +146,14 @@ export function DeckConfigurator(props: DeckConfiguratorProps): JSX.Element {
fixtureLocation={cutoutId}
/>
))}
+ {additionalStaticFixtures?.map(staticFixture => (
+
+ ))}