Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(protocol-designer): add ability to clear staging slots directly #16930

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -137,6 +145,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 @@ -158,6 +170,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 @@ -293,7 +320,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
}
Loading