Skip to content

Commit

Permalink
feat(protocol-designer): add ability to clear staging slots directly (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
shlokamin authored Nov 22, 2024
1 parent 7bbb1c2 commit d5b7e61
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 17 deletions.
37 changes: 24 additions & 13 deletions protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '@opentrons/components'
import {
FLEX_ROBOT_TYPE,
FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS,
getModuleDisplayName,
getModuleType,
MAGNETIC_MODULE_TYPE,
Expand Down Expand Up @@ -58,7 +59,7 @@ import { LabwareTools } from './LabwareTools'
import { MagnetModuleChangeContent } from './MagnetModuleChangeContent'
import { getModuleModelsBySlot, getDeckErrors } from './utils'

import type { ModuleModel } from '@opentrons/shared-data'
import type { AddressableAreaName, ModuleModel } from '@opentrons/shared-data'
import type { ThunkDispatch } from '../../../types'
import type { Fixture } from './constants'

Expand Down Expand Up @@ -242,39 +243,49 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null {
handleResetSearchTerm()
}

const handleClear = (): void => {
const handleClear = (keepExistingLabware = false): void => {
onDeckProps?.setHoveredModule(null)
onDeckProps?.setHoveredFixture(null)
if (slot !== 'offDeck') {
// clear module from slot
if (createdModuleForSlot != null) {
dispatch(deleteModule(createdModuleForSlot.id))
}
// clear fixture(s) from slot
if (createFixtureForSlots != null && createFixtureForSlots.length > 0) {
createFixtureForSlots.forEach(fixture =>
dispatch(deleteDeckFixture(fixture.id))
)
}
// clear labware from slot
if (
createdLabwareForSlot != null &&
createdLabwareForSlot.labwareDefURI !== selectedLabwareDefUri
(!keepExistingLabware ||
createdLabwareForSlot.labwareDefURI !== selectedLabwareDefUri)
) {
dispatch(deleteContainer({ labwareId: createdLabwareForSlot.id }))
}
// clear nested labware from slot
if (
createdNestedLabwareForSlot != null &&
createdNestedLabwareForSlot.labwareDefURI !==
selectedNestedLabwareDefUri
(!keepExistingLabware ||
createdNestedLabwareForSlot.labwareDefURI !==
selectedNestedLabwareDefUri)
) {
dispatch(deleteContainer({ labwareId: createdNestedLabwareForSlot.id }))
}
// clear labware on staging area 4th column slot
if (matchingLabwareFor4thColumn != null) {
if (matchingLabwareFor4thColumn != null && !keepExistingLabware) {
dispatch(deleteContainer({ labwareId: matchingLabwareFor4thColumn.id }))
}
// clear fixture(s) from slot
if (createFixtureForSlots != null && createFixtureForSlots.length > 0) {
createFixtureForSlots.forEach(fixture =>
dispatch(deleteDeckFixture(fixture.id))
)
// zoom out if you're clearing a staging area slot directly from a 4th column
if (
FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS.includes(
slot as AddressableAreaName
)
) {
dispatch(selectZoomedIntoSlot({ slot: null, cutout: null }))
}
}
}
handleResetToolbox()
handleResetLabwareTools()
Expand All @@ -285,7 +296,7 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null {
}
const handleConfirm = (): void => {
// clear entities first before recreating them
handleClear()
handleClear(true)

if (selectedFixture != null && cutout != null) {
// create fixture(s)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ import {
StyledText,
useOnClickOutside,
} from '@opentrons/components'
import {
FLEX_ROBOT_TYPE,
FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS,
getCutoutIdFromAddressableArea,
getDeckDefFromRobotType,
} from '@opentrons/shared-data'
import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations'

import { deleteModule } from '../../../step-forms/actions'
Expand All @@ -32,10 +38,12 @@ import { getStagingAreaAddressableAreas } from '../../../utils'
import { selectors as labwareIngredSelectors } from '../../../labware-ingred/selectors'
import type { MouseEvent, SetStateAction } from 'react'
import type {
AddressableAreaName,
CoordinateTuple,
CutoutId,
DeckSlotId,
} from '@opentrons/shared-data'

import type { LabwareOnDeck } from '../../../step-forms'
import type { ThunkDispatch } from '../../../types'

Expand Down Expand Up @@ -146,6 +154,10 @@ export function SlotOverflowMenu(
const hasNoItems =
moduleOnSlot == null && labwareOnSlot == null && fixturesOnSlot.length === 0

const isStagingSlot = FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS.includes(
location as AddressableAreaName
)

const handleClear = (): void => {
// clear module from slot
if (moduleOnSlot != null) {
Expand All @@ -167,6 +179,21 @@ export function SlotOverflowMenu(
if (matchingLabware != null) {
dispatch(deleteContainer({ labwareId: matchingLabware.id }))
}
// delete staging slot if addressable area is on staging slot
if (isStagingSlot) {
const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE)
const cutoutId = getCutoutIdFromAddressableArea(location, deckDef)
const stagingAreaEquipmentId = Object.values(
additionalEquipmentOnDeck
).find(({ location }) => location === cutoutId)?.id
if (stagingAreaEquipmentId != null) {
dispatch(deleteDeckFixture(stagingAreaEquipmentId))
} else {
console.error(
`could not find equipment id for entity in ${location} with cutout id ${cutoutId}`
)
}
}
}

const showDuplicateBtn =
Expand Down Expand Up @@ -303,7 +330,7 @@ export function SlotOverflowMenu(
) : null}
<Divider marginY="0" />
<MenuItem
disabled={hasNoItems}
disabled={hasNoItems && !isStagingSlot}
onClick={(e: MouseEvent) => {
if (matchingLabware != null) {
setShowDeleteLabwareModal(true)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import '@testing-library/jest-dom/vitest'
import { fireEvent, screen } from '@testing-library/react'
import {
Expand Down Expand Up @@ -67,6 +67,9 @@ describe('DeckSetupTools', () => {
})
vi.mocked(getDismissedHints).mockReturnValue([])
})
afterEach(() => {
vi.resetAllMocks()
})
it('should render the relevant modules and fixtures for slot D3 on Flex with tabs', () => {
render(props)
screen.getByText('Add a module')
Expand All @@ -92,6 +95,14 @@ describe('DeckSetupTools', () => {
screen.getByText('mock labware tools')
})
it('should clear the slot from all items when the clear cta is called', () => {
vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({
selectedLabwareDefUri: 'mockUri',
selectedNestedLabwareDefUri: 'mockUri',
selectedFixture: null,
selectedModuleModel: null,
selectedSlot: { slot: 'D3', cutout: 'cutoutD3' },
})

vi.mocked(getDeckSetupForActiveItem).mockReturnValue({
labware: {
labId: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type * as React from 'react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import '@testing-library/jest-dom/vitest'
import { fireEvent, screen } from '@testing-library/react'
import { fixture96Plate } from '@opentrons/shared-data'
Expand Down Expand Up @@ -42,6 +42,8 @@ const render = (props: React.ComponentProps<typeof SlotOverflowMenu>) => {
})[0]
}

const MOCK_STAGING_AREA_ID = 'MOCK_STAGING_AREA_ID'

describe('SlotOverflowMenu', () => {
let props: React.ComponentProps<typeof SlotOverflowMenu>

Expand Down Expand Up @@ -78,7 +80,11 @@ describe('SlotOverflowMenu', () => {
},
},
additionalEquipmentOnDeck: {
fixture: { name: 'stagingArea', id: 'mockId', location: 'cutoutD3' },
fixture: {
name: 'stagingArea',
id: MOCK_STAGING_AREA_ID,
location: 'cutoutD3',
},
},
})
vi.mocked(EditNickNameModal).mockReturnValue(
Expand All @@ -87,6 +93,10 @@ describe('SlotOverflowMenu', () => {
vi.mocked(labwareIngredSelectors.getLiquidsByLabwareId).mockReturnValue({})
})

afterEach(() => {
vi.restoreAllMocks()
})

it('should renders all buttons as enabled and clicking on them calls ctas', () => {
render(props)
fireEvent.click(
Expand Down Expand Up @@ -134,4 +144,25 @@ describe('SlotOverflowMenu', () => {
expect(mockNavigate).toHaveBeenCalled()
expect(vi.mocked(openIngredientSelector)).toHaveBeenCalled()
})
it('deletes the staging area slot and all labware and modules on top of it', () => {
vi.mocked(labwareIngredSelectors.getLiquidsByLabwareId).mockReturnValue({
labId2: { well1: { '0': { volume: 10 } } },
})
render(props)
fireEvent.click(screen.getByRole('button', { name: 'Clear slot' }))

expect(vi.mocked(deleteDeckFixture)).toHaveBeenCalledOnce()
expect(vi.mocked(deleteDeckFixture)).toHaveBeenCalledWith(
MOCK_STAGING_AREA_ID
)
expect(vi.mocked(deleteContainer)).toHaveBeenCalledTimes(2)
expect(vi.mocked(deleteContainer)).toHaveBeenNthCalledWith(1, {
labwareId: 'labId',
})
expect(vi.mocked(deleteContainer)).toHaveBeenNthCalledWith(2, {
labwareId: 'labId2',
})
expect(vi.mocked(deleteModule)).toHaveBeenCalledOnce()
expect(vi.mocked(deleteModule)).toHaveBeenCalledWith('modId')
})
})
26 changes: 26 additions & 0 deletions shared-data/js/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
RobotType,
ThermalAdapterName,
} from '../types'
import type { AddressableAreaName, CutoutId } from '../../deck/types/schemaV5'

export { getWellNamePerMultiTip } from './getWellNamePerMultiTip'
export { getWellTotalVolume } from './getWellTotalVolume'
Expand Down Expand Up @@ -373,3 +374,28 @@ export const getDeckDefFromRobotType = (
? standardFlexDeckDef
: standardOt2DeckDef
}

export const getCutoutIdFromAddressableArea = (
addressableAreaName: string,
deckDefinition: DeckDefinition
): CutoutId | null => {
/**
* Given an addressable area name, returns the cutout ID associated with it, or null if there is none
*/

for (const cutoutFixture of deckDefinition.cutoutFixtures) {
for (const [cutoutId, providedAreas] of Object.entries(
cutoutFixture.providesAddressableAreas
) as Array<[CutoutId, AddressableAreaName[]]>) {
if (providedAreas.includes(addressableAreaName as AddressableAreaName)) {
return cutoutId
}
}
}

console.error(
`${addressableAreaName} is not provided by any cutout fixtures in deck definition ${deckDefinition.otId}`
)

return null
}

0 comments on commit d5b7e61

Please sign in to comment.