Skip to content

Commit d5b7e61

Browse files
authored
feat(protocol-designer): add ability to clear staging slots directly (#16930)
closes RQA-3626
1 parent 7bbb1c2 commit d5b7e61

File tree

5 files changed

+123
-17
lines changed

5 files changed

+123
-17
lines changed

protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from '@opentrons/components'
1919
import {
2020
FLEX_ROBOT_TYPE,
21+
FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS,
2122
getModuleDisplayName,
2223
getModuleType,
2324
MAGNETIC_MODULE_TYPE,
@@ -58,7 +59,7 @@ import { LabwareTools } from './LabwareTools'
5859
import { MagnetModuleChangeContent } from './MagnetModuleChangeContent'
5960
import { getModuleModelsBySlot, getDeckErrors } from './utils'
6061

61-
import type { ModuleModel } from '@opentrons/shared-data'
62+
import type { AddressableAreaName, ModuleModel } from '@opentrons/shared-data'
6263
import type { ThunkDispatch } from '../../../types'
6364
import type { Fixture } from './constants'
6465

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

245-
const handleClear = (): void => {
246+
const handleClear = (keepExistingLabware = false): void => {
246247
onDeckProps?.setHoveredModule(null)
247248
onDeckProps?.setHoveredFixture(null)
248249
if (slot !== 'offDeck') {
249250
// clear module from slot
250251
if (createdModuleForSlot != null) {
251252
dispatch(deleteModule(createdModuleForSlot.id))
252253
}
253-
// clear fixture(s) from slot
254-
if (createFixtureForSlots != null && createFixtureForSlots.length > 0) {
255-
createFixtureForSlots.forEach(fixture =>
256-
dispatch(deleteDeckFixture(fixture.id))
257-
)
258-
}
259254
// clear labware from slot
260255
if (
261256
createdLabwareForSlot != null &&
262-
createdLabwareForSlot.labwareDefURI !== selectedLabwareDefUri
257+
(!keepExistingLabware ||
258+
createdLabwareForSlot.labwareDefURI !== selectedLabwareDefUri)
263259
) {
264260
dispatch(deleteContainer({ labwareId: createdLabwareForSlot.id }))
265261
}
266262
// clear nested labware from slot
267263
if (
268264
createdNestedLabwareForSlot != null &&
269-
createdNestedLabwareForSlot.labwareDefURI !==
270-
selectedNestedLabwareDefUri
265+
(!keepExistingLabware ||
266+
createdNestedLabwareForSlot.labwareDefURI !==
267+
selectedNestedLabwareDefUri)
271268
) {
272269
dispatch(deleteContainer({ labwareId: createdNestedLabwareForSlot.id }))
273270
}
274271
// clear labware on staging area 4th column slot
275-
if (matchingLabwareFor4thColumn != null) {
272+
if (matchingLabwareFor4thColumn != null && !keepExistingLabware) {
276273
dispatch(deleteContainer({ labwareId: matchingLabwareFor4thColumn.id }))
277274
}
275+
// clear fixture(s) from slot
276+
if (createFixtureForSlots != null && createFixtureForSlots.length > 0) {
277+
createFixtureForSlots.forEach(fixture =>
278+
dispatch(deleteDeckFixture(fixture.id))
279+
)
280+
// zoom out if you're clearing a staging area slot directly from a 4th column
281+
if (
282+
FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS.includes(
283+
slot as AddressableAreaName
284+
)
285+
) {
286+
dispatch(selectZoomedIntoSlot({ slot: null, cutout: null }))
287+
}
288+
}
278289
}
279290
handleResetToolbox()
280291
handleResetLabwareTools()
@@ -285,7 +296,7 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null {
285296
}
286297
const handleConfirm = (): void => {
287298
// clear entities first before recreating them
288-
handleClear()
299+
handleClear(true)
289300

290301
if (selectedFixture != null && cutout != null) {
291302
// create fixture(s)

protocol-designer/src/pages/Designer/DeckSetup/SlotOverflowMenu.tsx

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ import {
1515
StyledText,
1616
useOnClickOutside,
1717
} from '@opentrons/components'
18+
import {
19+
FLEX_ROBOT_TYPE,
20+
FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS,
21+
getCutoutIdFromAddressableArea,
22+
getDeckDefFromRobotType,
23+
} from '@opentrons/shared-data'
1824
import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations'
1925

2026
import { deleteModule } from '../../../step-forms/actions'
@@ -32,10 +38,12 @@ import { getStagingAreaAddressableAreas } from '../../../utils'
3238
import { selectors as labwareIngredSelectors } from '../../../labware-ingred/selectors'
3339
import type { MouseEvent, SetStateAction } from 'react'
3440
import type {
41+
AddressableAreaName,
3542
CoordinateTuple,
3643
CutoutId,
3744
DeckSlotId,
3845
} from '@opentrons/shared-data'
46+
3947
import type { LabwareOnDeck } from '../../../step-forms'
4048
import type { ThunkDispatch } from '../../../types'
4149

@@ -146,6 +154,10 @@ export function SlotOverflowMenu(
146154
const hasNoItems =
147155
moduleOnSlot == null && labwareOnSlot == null && fixturesOnSlot.length === 0
148156

157+
const isStagingSlot = FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS.includes(
158+
location as AddressableAreaName
159+
)
160+
149161
const handleClear = (): void => {
150162
// clear module from slot
151163
if (moduleOnSlot != null) {
@@ -167,6 +179,21 @@ export function SlotOverflowMenu(
167179
if (matchingLabware != null) {
168180
dispatch(deleteContainer({ labwareId: matchingLabware.id }))
169181
}
182+
// delete staging slot if addressable area is on staging slot
183+
if (isStagingSlot) {
184+
const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE)
185+
const cutoutId = getCutoutIdFromAddressableArea(location, deckDef)
186+
const stagingAreaEquipmentId = Object.values(
187+
additionalEquipmentOnDeck
188+
).find(({ location }) => location === cutoutId)?.id
189+
if (stagingAreaEquipmentId != null) {
190+
dispatch(deleteDeckFixture(stagingAreaEquipmentId))
191+
} else {
192+
console.error(
193+
`could not find equipment id for entity in ${location} with cutout id ${cutoutId}`
194+
)
195+
}
196+
}
170197
}
171198

172199
const showDuplicateBtn =
@@ -303,7 +330,7 @@ export function SlotOverflowMenu(
303330
) : null}
304331
<Divider marginY="0" />
305332
<MenuItem
306-
disabled={hasNoItems}
333+
disabled={hasNoItems && !isStagingSlot}
307334
onClick={(e: MouseEvent) => {
308335
if (matchingLabware != null) {
309336
setShowDeleteLabwareModal(true)

protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, vi, beforeEach } from 'vitest'
1+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
22
import '@testing-library/jest-dom/vitest'
33
import { fireEvent, screen } from '@testing-library/react'
44
import {
@@ -67,6 +67,9 @@ describe('DeckSetupTools', () => {
6767
})
6868
vi.mocked(getDismissedHints).mockReturnValue([])
6969
})
70+
afterEach(() => {
71+
vi.resetAllMocks()
72+
})
7073
it('should render the relevant modules and fixtures for slot D3 on Flex with tabs', () => {
7174
render(props)
7275
screen.getByText('Add a module')
@@ -92,6 +95,14 @@ describe('DeckSetupTools', () => {
9295
screen.getByText('mock labware tools')
9396
})
9497
it('should clear the slot from all items when the clear cta is called', () => {
98+
vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({
99+
selectedLabwareDefUri: 'mockUri',
100+
selectedNestedLabwareDefUri: 'mockUri',
101+
selectedFixture: null,
102+
selectedModuleModel: null,
103+
selectedSlot: { slot: 'D3', cutout: 'cutoutD3' },
104+
})
105+
95106
vi.mocked(getDeckSetupForActiveItem).mockReturnValue({
96107
labware: {
97108
labId: {

protocol-designer/src/pages/Designer/DeckSetup/__tests__/SlotOverflowMenu.test.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type * as React from 'react'
2-
import { describe, it, expect, vi, beforeEach } from 'vitest'
2+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
33
import '@testing-library/jest-dom/vitest'
44
import { fireEvent, screen } from '@testing-library/react'
55
import { fixture96Plate } from '@opentrons/shared-data'
@@ -42,6 +42,8 @@ const render = (props: React.ComponentProps<typeof SlotOverflowMenu>) => {
4242
})[0]
4343
}
4444

45+
const MOCK_STAGING_AREA_ID = 'MOCK_STAGING_AREA_ID'
46+
4547
describe('SlotOverflowMenu', () => {
4648
let props: React.ComponentProps<typeof SlotOverflowMenu>
4749

@@ -78,7 +80,11 @@ describe('SlotOverflowMenu', () => {
7880
},
7981
},
8082
additionalEquipmentOnDeck: {
81-
fixture: { name: 'stagingArea', id: 'mockId', location: 'cutoutD3' },
83+
fixture: {
84+
name: 'stagingArea',
85+
id: MOCK_STAGING_AREA_ID,
86+
location: 'cutoutD3',
87+
},
8288
},
8389
})
8490
vi.mocked(EditNickNameModal).mockReturnValue(
@@ -87,6 +93,10 @@ describe('SlotOverflowMenu', () => {
8793
vi.mocked(labwareIngredSelectors.getLiquidsByLabwareId).mockReturnValue({})
8894
})
8995

96+
afterEach(() => {
97+
vi.restoreAllMocks()
98+
})
99+
90100
it('should renders all buttons as enabled and clicking on them calls ctas', () => {
91101
render(props)
92102
fireEvent.click(
@@ -134,4 +144,25 @@ describe('SlotOverflowMenu', () => {
134144
expect(mockNavigate).toHaveBeenCalled()
135145
expect(vi.mocked(openIngredientSelector)).toHaveBeenCalled()
136146
})
147+
it('deletes the staging area slot and all labware and modules on top of it', () => {
148+
vi.mocked(labwareIngredSelectors.getLiquidsByLabwareId).mockReturnValue({
149+
labId2: { well1: { '0': { volume: 10 } } },
150+
})
151+
render(props)
152+
fireEvent.click(screen.getByRole('button', { name: 'Clear slot' }))
153+
154+
expect(vi.mocked(deleteDeckFixture)).toHaveBeenCalledOnce()
155+
expect(vi.mocked(deleteDeckFixture)).toHaveBeenCalledWith(
156+
MOCK_STAGING_AREA_ID
157+
)
158+
expect(vi.mocked(deleteContainer)).toHaveBeenCalledTimes(2)
159+
expect(vi.mocked(deleteContainer)).toHaveBeenNthCalledWith(1, {
160+
labwareId: 'labId',
161+
})
162+
expect(vi.mocked(deleteContainer)).toHaveBeenNthCalledWith(2, {
163+
labwareId: 'labId2',
164+
})
165+
expect(vi.mocked(deleteModule)).toHaveBeenCalledOnce()
166+
expect(vi.mocked(deleteModule)).toHaveBeenCalledWith('modId')
167+
})
137168
})

shared-data/js/helpers/index.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
RobotType,
1111
ThermalAdapterName,
1212
} from '../types'
13+
import type { AddressableAreaName, CutoutId } from '../../deck/types/schemaV5'
1314

1415
export { getWellNamePerMultiTip } from './getWellNamePerMultiTip'
1516
export { getWellTotalVolume } from './getWellTotalVolume'
@@ -373,3 +374,28 @@ export const getDeckDefFromRobotType = (
373374
? standardFlexDeckDef
374375
: standardOt2DeckDef
375376
}
377+
378+
export const getCutoutIdFromAddressableArea = (
379+
addressableAreaName: string,
380+
deckDefinition: DeckDefinition
381+
): CutoutId | null => {
382+
/**
383+
* Given an addressable area name, returns the cutout ID associated with it, or null if there is none
384+
*/
385+
386+
for (const cutoutFixture of deckDefinition.cutoutFixtures) {
387+
for (const [cutoutId, providedAreas] of Object.entries(
388+
cutoutFixture.providesAddressableAreas
389+
) as Array<[CutoutId, AddressableAreaName[]]>) {
390+
if (providedAreas.includes(addressableAreaName as AddressableAreaName)) {
391+
return cutoutId
392+
}
393+
}
394+
}
395+
396+
console.error(
397+
`${addressableAreaName} is not provided by any cutout fixtures in deck definition ${deckDefinition.otId}`
398+
)
399+
400+
return null
401+
}

0 commit comments

Comments
 (0)