Skip to content

Commit

Permalink
fix(protocol-designer): handle duplicate labware with a full deck (#1…
Browse files Browse the repository at this point in the history
…7083)

If a slot overflow is opened and duplicate labware is clicked, the
action will fail silently if no room is left on the robot deck. Here, we
use the `getNextAvailableDeckSlot` util to determine whether or not we
can dispatch a labware duplication on the deck. If not, we keep the menu
open, but render a snackbar letting the user know the reason for the
failure. Note that according to new design paradigms, the button remains
enabled in every case.
  • Loading branch information
ncdiehl11 authored Dec 11, 2024
1 parent b0c8e69 commit 62a6ff3
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"custom": "Custom labware definitions",
"customize_slot": "Customize slot",
"deck_hardware": "Deck hardware",
"deck_slots_full": "Deck slots are full",
"define_liquid": "Define a liquid",
"done": "Done",
"double_click_to_edit": "Double-click to edit",
Expand Down
59 changes: 39 additions & 20 deletions protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,26 @@ import {
getCutoutIdFromAddressableArea,
getDeckDefFromRobotType,
} from '@opentrons/shared-data'
import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations'

import { getRobotType } from '../../../file-data/selectors'
import {
deleteContainer,
duplicateLabware,
openIngredientSelector,
} from '../../../labware-ingred/actions'
import { getNextAvailableDeckSlot } from '../../../labware-ingred/utils'
import { deleteModule } from '../../../step-forms/actions'
import {
ConfirmDeleteStagingAreaModal,
EditNickNameModal,
} from '../../../organisms'
import { useKitchen } from '../../../organisms/Kitchen/hooks'
import { deleteDeckFixture } from '../../../step-forms/actions/additionalItems'
import {
deleteContainer,
duplicateLabware,
openIngredientSelector,
} from '../../../labware-ingred/actions'
import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations'

import { getStagingAreaAddressableAreas } from '../../../utils'
import { selectors as labwareIngredSelectors } from '../../../labware-ingred/selectors'

import type { MouseEvent, SetStateAction } from 'react'
import type {
AddressableAreaName,
Expand Down Expand Up @@ -68,6 +73,7 @@ const TOP_SLOT_Y_POSITION = 50
const TOP_SLOT_Y_POSITION_ALL_BUTTONS = 110
const TOP_SLOT_Y_POSITION_2_BUTTONS = 35
const STAGING_AREA_SLOTS = ['A4', 'B4', 'C4', 'D4']

interface SlotOverflowMenuProps {
// can be off-deck id or deck slot
location: DeckSlotId | string
Expand Down Expand Up @@ -105,11 +111,16 @@ export function SlotOverflowMenu(
labwareIngredSelectors.getLiquidsByLabwareId
)

const robotType = useSelector(getRobotType)

const { makeSnackbar } = useKitchen()

const {
labware: deckSetupLabware,
modules: deckSetupModules,
additionalEquipmentOnDeck,
} = deckSetup

const isOffDeckLocation = deckSetupLabware[location] != null

const moduleOnSlot = Object.values(deckSetupModules).find(
Expand All @@ -120,6 +131,9 @@ export function SlotOverflowMenu(
? lw.id === location
: lw.slot === location || lw.slot === moduleOnSlot?.id
)
const isSpace =
getNextAvailableDeckSlot(deckSetup, robotType, labwareOnSlot?.def) != null

const isLabwareTiprack = labwareOnSlot?.def.parameters.isTiprack ?? false
const isLabwareAnAdapter =
labwareOnSlot?.def.allowedRoles?.includes('adapter') ?? false
Expand Down Expand Up @@ -158,6 +172,24 @@ export function SlotOverflowMenu(
location as AddressableAreaName
)

const handleDuplicate = (): void => {
if (!isSpace) {
makeSnackbar(t('deck_slots_full') as string)
return
}

if (
labwareOnSlot != null &&
!isLabwareAnAdapter &&
nestedLabwareOnSlot == null
) {
dispatch(duplicateLabware(labwareOnSlot.id))
} else if (nestedLabwareOnSlot != null) {
dispatch(duplicateLabware(nestedLabwareOnSlot.id))
}
setShowMenuList(false)
}

const handleClear = (): void => {
// clear module from slot
if (moduleOnSlot != null) {
Expand Down Expand Up @@ -309,20 +341,7 @@ export function SlotOverflowMenu(
</StyledText>
</MenuItem>
{showDuplicateBtn ? (
<MenuItem
onClick={() => {
if (
labwareOnSlot != null &&
!isLabwareAnAdapter &&
nestedLabwareOnSlot == null
) {
dispatch(duplicateLabware(labwareOnSlot.id))
} else if (nestedLabwareOnSlot != null) {
dispatch(duplicateLabware(nestedLabwareOnSlot.id))
}
setShowMenuList(false)
}}
>
<MenuItem onClick={handleDuplicate}>
<StyledText desktopStyle="bodyDefaultRegular">
{t('duplicate')}
</StyledText>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import {
openIngredientSelector,
} from '../../../../labware-ingred/actions'
import { EditNickNameModal } from '../../../../organisms'
import { useKitchen } from '../../../../organisms/Kitchen/hooks'
import { deleteModule } from '../../../../step-forms/actions'
import { deleteDeckFixture } from '../../../../step-forms/actions/additionalItems'
import { getDeckSetupForActiveItem } from '../../../../top-selectors/labware-locations'
import { selectors as labwareIngredSelectors } from '../../../../labware-ingred/selectors'
import { getNextAvailableDeckSlot } from '../../../../labware-ingred/utils'
import { SlotOverflowMenu } from '../SlotOverflowMenu'

import type { NavigateFunction } from 'react-router-dom'
Expand All @@ -28,6 +30,9 @@ vi.mock('../../../../labware-ingred/actions')
vi.mock('../../../../labware-ingred/selectors')
vi.mock('../../../../step-forms/actions/additionalItems')
vi.mock('../../../../organisms')
vi.mock('../../../../file-data/selectors')
vi.mock('../../../../labware-ingred/utils')
vi.mock('../../../../organisms/Kitchen/hooks')
vi.mock('react-router-dom', async importOriginal => {
const actual = await importOriginal<NavigateFunction>()
return {
Expand All @@ -43,6 +48,7 @@ const render = (props: React.ComponentProps<typeof SlotOverflowMenu>) => {
}

const MOCK_STAGING_AREA_ID = 'MOCK_STAGING_AREA_ID'
const MOCK_MAKE_SNACKBAR = vi.fn()

describe('SlotOverflowMenu', () => {
let props: React.ComponentProps<typeof SlotOverflowMenu>
Expand Down Expand Up @@ -91,6 +97,12 @@ describe('SlotOverflowMenu', () => {
<div>mockEditNickNameModal</div>
)
vi.mocked(labwareIngredSelectors.getLiquidsByLabwareId).mockReturnValue({})
vi.mocked(getNextAvailableDeckSlot).mockReturnValue('A1')
vi.mocked(useKitchen).mockReturnValue({
makeSnackbar: MOCK_MAKE_SNACKBAR,
eatToast: vi.fn(),
bakeToast: vi.fn(),
})
})

afterEach(() => {
Expand Down Expand Up @@ -165,4 +177,11 @@ describe('SlotOverflowMenu', () => {
expect(vi.mocked(deleteModule)).toHaveBeenCalledOnce()
expect(vi.mocked(deleteModule)).toHaveBeenCalledWith('modId')
})

it('renders snackbar if duplicate is clicked and the deck is full', () => {
vi.mocked(getNextAvailableDeckSlot).mockReturnValue(null)
render(props)
fireEvent.click(screen.getByRole('button', { name: 'Duplicate labware' }))
expect(MOCK_MAKE_SNACKBAR).toHaveBeenCalled()
})
})

0 comments on commit 62a6ff3

Please sign in to comment.