From 0780093be9d7fa821e00f7c3f1228b8c86aed986 Mon Sep 17 00:00:00 2001 From: ncdiehl11 Date: Thu, 19 Dec 2024 13:34:54 -0500 Subject: [PATCH 1/2] feat(protocol-designer): prevent user from adding plate reader without gripper Here, I prevent saving adding a plate reader in DeckSetupTools if a gripper has not already been added. In a followup, we will enforce a timeline error at for plate reader stepforms that modify the lid state of the reader. Closes AUTH-1095 --- .../CreateNewProtocolWizard/SelectModules.tsx | 2 + .../Designer/DeckSetup/DeckSetupTools.tsx | 27 ++++++++---- .../__tests__/DeckSetupTools.test.tsx | 41 +++++++++++++++++++ 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx index dfd05979524..568859d205d 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx @@ -14,6 +14,7 @@ import { WRAP, } from '@opentrons/components' import { + ABSORBANCE_READER_TYPE, ABSORBANCE_READER_V1, FLEX_ROBOT_TYPE, getModuleDisplayName, @@ -70,6 +71,7 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { TEMPERATURE_MODULE_TYPE, HEATERSHAKER_MODULE_TYPE, MAGNETIC_BLOCK_TYPE, + ABSORBANCE_READER_TYPE, ] const handleAddModule = ( diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx index 4c41a8f8464..2f29fbd416c 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx @@ -18,6 +18,7 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { + ABSORBANCE_READER_TYPE, ABSORBANCE_READER_V1, FLEX_ROBOT_TYPE, FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS, @@ -36,6 +37,7 @@ import { deleteDeckFixture, } from '../../../step-forms/actions/additionalItems' import { createModule, deleteModule } from '../../../step-forms/actions' +import { getAdditionalEquipment } from '../../../step-forms/selectors' import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations' import { createContainer, @@ -110,6 +112,10 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { const [changeModuleWarningInfo, displayModuleWarning] = useState( false ) + const additionalEquipment = useSelector(getAdditionalEquipment) + const isGripperAttached = Object.values(additionalEquipment).some( + equipment => equipment?.name === 'gripper' + ) const [selectedHardware, setSelectedHardware] = useState< ModuleModel | Fixture | null >(null) @@ -320,13 +326,20 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { } if (selectedModuleModel != null) { // create module - dispatch( - createModule({ - slot, - type: getModuleType(selectedModuleModel), - model: selectedModuleModel, - }) - ) + const moduleType = getModuleType(selectedModuleModel) + // enforce gripper present in order to add plate reader + if (moduleType === ABSORBANCE_READER_TYPE && !isGripperAttached) { + makeSnackbar('Gripper required to add absorbance reader') + return + } else { + dispatch( + createModule({ + slot, + type: moduleType, + model: selectedModuleModel, + }) + ) + } } if ( (slot === 'offDeck' && selectedLabwareDefUri != null) || diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx index 5eab480710e..6a0c7d03c87 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, screen } from '@testing-library/react' import { + ABSORBANCE_READER_V1, FLEX_ROBOT_TYPE, HEATERSHAKER_MODULE_V1, fixture96Plate, @@ -9,7 +10,9 @@ import { import { i18n } from '../../../../assets/localization' import { renderWithProviders } from '../../../../__testing-utils__' import { deleteContainer } from '../../../../labware-ingred/actions' +import { useKitchen } from '../../../../organisms/Kitchen/hooks' import { deleteModule } from '../../../../step-forms/actions' +import { getAdditionalEquipment } from '../../../../step-forms/selectors' import { getRobotType } from '../../../../file-data/selectors' import { getEnableAbsorbanceReader } from '../../../../feature-flags/selectors' import { deleteDeckFixture } from '../../../../step-forms/actions/additionalItems' @@ -31,12 +34,16 @@ vi.mock('../../../../step-forms/actions') vi.mock('../../../../step-forms/actions/additionalItems') vi.mock('../../../../labware-ingred/selectors') vi.mock('../../../../tutorial/selectors') +vi.mock('../../../../step-forms/selectors') +vi.mock('../../../../organisms/Kitchen/hooks') const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } +const mockMakeSnackbar = vi.fn() + describe('DeckSetupTools', () => { let props: React.ComponentProps @@ -66,6 +73,12 @@ describe('DeckSetupTools', () => { pipettes: {}, }) vi.mocked(getDismissedHints).mockReturnValue([]) + vi.mocked(getAdditionalEquipment).mockReturnValue({}) + vi.mocked(useKitchen).mockReturnValue({ + makeSnackbar: mockMakeSnackbar, + bakeToast: vi.fn(), + eatToast: vi.fn(), + }) }) afterEach(() => { vi.resetAllMocks() @@ -164,4 +177,32 @@ describe('DeckSetupTools', () => { fireEvent.click(screen.getByText('Done')) expect(props.onCloseClick).toHaveBeenCalled() }) + it('should save plate reader if gripper configured', () => { + vi.mocked(getAdditionalEquipment).mockReturnValue({ + gripperUri: { name: 'gripper', id: 'gripperId' }, + }) + vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ + selectedLabwareDefUri: null, + selectedNestedLabwareDefUri: null, + selectedFixture: null, + selectedModuleModel: ABSORBANCE_READER_V1, + selectedSlot: { slot: 'D3', cutout: 'cutoutD3' }, + }) + render(props) + fireEvent.click(screen.getByText('Done')) + expect(props.onCloseClick).toHaveBeenCalled() + }) + it('should prevent saving plate reader and make toast if gripper not configured', () => { + vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ + selectedLabwareDefUri: null, + selectedNestedLabwareDefUri: null, + selectedFixture: null, + selectedModuleModel: ABSORBANCE_READER_V1, + selectedSlot: { slot: 'D3', cutout: 'cutoutD3' }, + }) + render(props) + fireEvent.click(screen.getByText('Done')) + expect(props.onCloseClick).not.toHaveBeenCalled() + expect(mockMakeSnackbar).toHaveBeenCalled() + }) }) From 9dc36a53f47f6f9b8f8208466fc5b10f0ea0b176 Mon Sep 17 00:00:00 2001 From: ncdiehl11 Date: Thu, 19 Dec 2024 14:02:14 -0500 Subject: [PATCH 2/2] address feedback --- .../localization/en/starting_deck_state.json | 1 + .../pages/Designer/DeckSetup/DeckSetupTools.tsx | 17 ++++++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/protocol-designer/src/assets/localization/en/starting_deck_state.json b/protocol-designer/src/assets/localization/en/starting_deck_state.json index afa602358ca..acf45939403 100644 --- a/protocol-designer/src/assets/localization/en/starting_deck_state.json +++ b/protocol-designer/src/assets/localization/en/starting_deck_state.json @@ -34,6 +34,7 @@ "edit_slot": "Edit slot", "edit": "Edit", "gen1_gen2_different_units": "Switching between GEN1 and GEN2 Magnetic Modules will clear all non-default engage heights from existing magnet steps in your protocol. GEN1 and GEN2 Magnetic Modules do not use the same units.", + "gripper_required_for_plate_reader": "Gripper required to add absorbance reader", "heater_shaker_adjacent_to": "A module is adjacent to this slot. The Heater-Shaker cannot be placed next to a module", "heater_shaker_adjacent": "A Heater-Shaker is adjacent to this slot. Modules cannot be placed next to a Heater-Shaker", "heater_shaker_trash": "The heater-shaker cannot be next to the trash bin", diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx index 2f29fbd416c..c498a71534c 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx @@ -329,17 +329,16 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { const moduleType = getModuleType(selectedModuleModel) // enforce gripper present in order to add plate reader if (moduleType === ABSORBANCE_READER_TYPE && !isGripperAttached) { - makeSnackbar('Gripper required to add absorbance reader') + makeSnackbar(t('gripper_required_for_plate_reader') as string) return - } else { - dispatch( - createModule({ - slot, - type: moduleType, - model: selectedModuleModel, - }) - ) } + dispatch( + createModule({ + slot, + type: moduleType, + model: selectedModuleModel, + }) + ) } if ( (slot === 'offDeck' && selectedLabwareDefUri != null) ||