From 033d987a13810a14911301524ad935e4c156d8ed Mon Sep 17 00:00:00 2001 From: Jethary Date: Wed, 21 Jun 2023 11:06:27 -0400 Subject: [PATCH 01/13] flex support in modules section --- components/src/slotmap/SlotMap.tsx | 16 +- .../src/slotmap/__tests__/SlotMap.test.tsx | 16 +- .../EditModulesModal/ConnectedSlotMap.tsx | 4 +- .../modals/EditModulesModal/index.tsx | 20 +- .../components/modules/EditModulesCard.tsx | 19 +- .../src/components/modules/GripperRow.tsx | 45 +++++ .../src/components/modules/ModuleRow.tsx | 16 +- protocol-designer/src/modules/moduleData.ts | 178 ++++++++++++++++-- 8 files changed, 277 insertions(+), 37 deletions(-) create mode 100644 protocol-designer/src/components/modules/GripperRow.tsx diff --git a/components/src/slotmap/SlotMap.tsx b/components/src/slotmap/SlotMap.tsx index 7f6ffd2738d..e659802868b 100644 --- a/components/src/slotmap/SlotMap.tsx +++ b/components/src/slotmap/SlotMap.tsx @@ -13,15 +13,23 @@ export interface SlotMapProps { collisionSlots?: string[] /** Optional error styling */ isError?: boolean + isOt3?: boolean } -const SLOT_MAP_SLOTS = [ +const OT2_SLOT_MAP_SLOTS = [ ['10', '11'], ['7', '8', '9'], ['4', '5', '6'], ['1', '2', '3'], ] +const FLEX_SLOT_MAP_SLOTS = [ + ['A1', 'A2', 'A3'], + ['B1', 'B2', 'B3'], + ['C1', 'C2', 'C3'], + ['D1', 'D2', 'D3'], +] + const slotWidth = 33 const slotHeight = 23 const iconSize = 20 @@ -29,12 +37,14 @@ const numRows = 4 const numCols = 3 export function SlotMap(props: SlotMapProps): JSX.Element { - const { collisionSlots, occupiedSlots, isError } = props + const { collisionSlots, occupiedSlots, isError, isOt3 } = props + const slots = isOt3 ? FLEX_SLOT_MAP_SLOTS : OT2_SLOT_MAP_SLOTS + return ( - {SLOT_MAP_SLOTS.flatMap((row, rowIndex) => + {slots.flatMap((row, rowIndex) => row.map((slot, colIndex) => { const isCollisionSlot = collisionSlots && collisionSlots.includes(slot) diff --git a/components/src/slotmap/__tests__/SlotMap.test.tsx b/components/src/slotmap/__tests__/SlotMap.test.tsx index 0973d423365..d65ad1246cb 100644 --- a/components/src/slotmap/__tests__/SlotMap.test.tsx +++ b/components/src/slotmap/__tests__/SlotMap.test.tsx @@ -5,13 +5,13 @@ import { SlotMap } from '../SlotMap' import { Icon } from '../../icons' describe('SlotMap', () => { - it('component renders 11 slots', () => { + it('component renders 11 slots for ot-2', () => { const wrapper = shallow() expect(wrapper.find('rect')).toHaveLength(11) }) - it('component renders crash info icon when collision slots present', () => { + it('component renders crash info icon when collision slots present for ot-2', () => { const wrapper = shallow( ) @@ -29,4 +29,16 @@ describe('SlotMap', () => { expect(wrapperWithError.find('.slot_occupied')).toHaveLength(1) expect(wrapperWithError.find('.slot_occupied.slot_error')).toHaveLength(1) }) + + it('should render 12 slots for flex', () => { + const wrapper = shallow() + expect(wrapper.find('rect')).toHaveLength(12) + }) + + it('component renders crash info icon when collision slots present for flex', () => { + const wrapper = shallow( + + ) + expect(wrapper.find(Icon)).toHaveLength(1) + }) }) diff --git a/protocol-designer/src/components/modals/EditModulesModal/ConnectedSlotMap.tsx b/protocol-designer/src/components/modals/EditModulesModal/ConnectedSlotMap.tsx index 9968ee36005..bea8bea2a1f 100644 --- a/protocol-designer/src/components/modals/EditModulesModal/ConnectedSlotMap.tsx +++ b/protocol-designer/src/components/modals/EditModulesModal/ConnectedSlotMap.tsx @@ -5,18 +5,20 @@ import styles from './EditModules.css' interface ConnectedSlotMapProps { fieldName: string + isOt3: boolean } export const ConnectedSlotMap = ( props: ConnectedSlotMapProps ): JSX.Element | null => { - const { fieldName } = props + const { fieldName, isOt3 } = props const [field, meta] = useField(fieldName) return field.value ? (
) : null diff --git a/protocol-designer/src/components/modals/EditModulesModal/index.tsx b/protocol-designer/src/components/modals/EditModulesModal/index.tsx index 8eb6c657fb1..865c31fe822 100644 --- a/protocol-designer/src/components/modals/EditModulesModal/index.tsx +++ b/protocol-designer/src/components/modals/EditModulesModal/index.tsx @@ -31,7 +31,8 @@ import { actions as stepFormActions, } from '../../../step-forms' import { - SUPPORTED_MODULE_SLOTS, + SUPPORTED_MODULE_SLOTS_OT2, + SUPPORTED_MODULE_SLOTS_FLEX, getAllModuleSlotsByType, } from '../../../modules/moduleData' import { selectors as featureFlagSelectors } from '../../../feature-flags' @@ -47,6 +48,7 @@ import { useResetSlotOnModelChange } from './form-state' import { ModuleOnDeck } from '../../../step-forms/types' import { ModelModuleInfo } from '../../EditModules' +import { getRobotType } from '../../../file-data/selectors' export interface EditModulesModalProps { moduleType: ModuleType @@ -75,7 +77,11 @@ export const EditModulesModal = (props: EditModulesModalProps): JSX.Element => { onCloseClick, moduleOnDeck, } = props - const supportedModuleSlot = SUPPORTED_MODULE_SLOTS[moduleType][0].value + const robotType = useSelector(getRobotType) + const supportedModuleSlot = + robotType === 'OT-2 Standard' + ? SUPPORTED_MODULE_SLOTS_OT2[moduleType][0].value + : SUPPORTED_MODULE_SLOTS_FLEX[moduleType][0].value const initialDeckSetup = useSelector(stepFormSelectors.getInitialDeckSetup) const dispatch = useDispatch() @@ -222,6 +228,7 @@ const EditModulesModalComponent = ( const disabledModuleRestriction = useSelector( featureFlagSelectors.getDisableModuleRestrictions ) + const robotType = useSelector(getRobotType) const noCollisionIssue = selectedModel && !isModuleWithCollisionIssue(selectedModel) @@ -278,15 +285,18 @@ const EditModulesModalComponent = (
- + )} diff --git a/protocol-designer/src/components/modules/EditModulesCard.tsx b/protocol-designer/src/components/modules/EditModulesCard.tsx index 4c4365c56da..9d1fcdbb9d7 100644 --- a/protocol-designer/src/components/modules/EditModulesCard.tsx +++ b/protocol-designer/src/components/modules/EditModulesCard.tsx @@ -19,6 +19,7 @@ import { SUPPORTED_MODULE_TYPES } from '../../modules' import { getRobotType } from '../../file-data/selectors' import { CrashInfoBox } from './CrashInfoBox' import { ModuleRow } from './ModuleRow' +import { GripperRow } from './GripperRow' import { isModuleWithCollisionIssue } from './utils' import styles from './styles.css' @@ -29,12 +30,12 @@ export interface Props { export function EditModulesCard(props: Props): JSX.Element { const { modules, openEditModuleModal } = props - const pipettesByMount = useSelector( stepFormSelectors.getPipettesForEditPipetteForm ) const robotType = useSelector(getRobotType) - + // const savedStepForms = useSelector(getSavedStepForms) + // const additionalEquipment = savedStepForms.additionalEquipment const magneticModuleOnDeck = modules[MAGNETIC_MODULE_TYPE] const temperatureModuleOnDeck = modules[TEMPERATURE_MODULE_TYPE] const heaterShakerOnDeck = modules[HEATERSHAKER_MODULE_TYPE] @@ -67,18 +68,19 @@ export function EditModulesCard(props: Props): JSX.Element { ].some(pipetteSpecs => pipetteSpecs?.channels !== 1) const warningsEnabled = !moduleRestrictionsDisabled + const isOt3 = robotType === 'OT-3 Standard' const SUPPORTED_MODULE_TYPES_FILTERED = SUPPORTED_MODULE_TYPES.filter( moduleType => - robotType === 'OT-3 Standard' + isOt3 ? moduleType !== 'magneticModuleType' : moduleType !== 'magneticBlockType' ) return ( - +
- {warningsEnabled && ( + {warningsEnabled && !isOt3 && ( ) } else { @@ -111,6 +114,12 @@ export function EditModulesCard(props: Props): JSX.Element { ) } })} + {isOt3 ? ( + console.log('wire this up')} + isGripperAdded={true} + /> + ) : null}
) diff --git a/protocol-designer/src/components/modules/GripperRow.tsx b/protocol-designer/src/components/modules/GripperRow.tsx new file mode 100644 index 00000000000..e1390240f5a --- /dev/null +++ b/protocol-designer/src/components/modules/GripperRow.tsx @@ -0,0 +1,45 @@ +import * as React from 'react' +import styled from 'styled-components' +import { + OutlineButton, + Flex, + JUSTIFY_SPACE_BETWEEN, + ALIGN_CENTER, +} from '@opentrons/components' +import gripperImage from '../../images/flex_gripper.svg' +import styles from './styles.css' + +interface GripperRowProps { + handleAddGripper: () => void + isGripperAdded: boolean +} + +export function GripperRow(props: GripperRowProps): JSX.Element { + const { handleAddGripper, isGripperAdded } = props + + return ( + + +

{'Flex Gripper'}

+ +
+
+ + {isGripperAdded ? 'Remove' : 'Add'} + +
+
+ ) +} + +const AdditionalItemImage = styled.img` + width: 6rem; + max-height: 4rem; + display: block; +` diff --git a/protocol-designer/src/components/modules/ModuleRow.tsx b/protocol-designer/src/components/modules/ModuleRow.tsx index 435a2197e5e..f44de7e2d89 100644 --- a/protocol-designer/src/components/modules/ModuleRow.tsx +++ b/protocol-designer/src/components/modules/ModuleRow.tsx @@ -22,9 +22,10 @@ import { ModuleDiagram } from './ModuleDiagram' import { isModuleWithCollisionIssue } from './utils' import styles from './styles.css' -import { ModuleType } from '@opentrons/shared-data' +import { ModuleType, THERMOCYCLER_MODULE_TYPE } from '@opentrons/shared-data' interface Props { + isOt3?: boolean moduleOnDeck?: ModuleOnDeck showCollisionWarnings?: boolean type: ModuleType @@ -32,12 +33,16 @@ interface Props { } export function ModuleRow(props: Props): JSX.Element { - const { moduleOnDeck, openEditModuleModal, showCollisionWarnings } = props + const { + moduleOnDeck, + openEditModuleModal, + showCollisionWarnings, + isOt3, + } = props const type: ModuleType = moduleOnDeck?.type || props.type const model = moduleOnDeck?.model const slot = moduleOnDeck?.slot - /* TODO (ka 2020-2-3): This logic is very specific to this individual implementation of SlotMap. Kept it here (for now?) because it spells out the different cases. @@ -66,8 +71,10 @@ export function ModuleRow(props: Props): JSX.Element { if (slot === SPAN7_8_10_11_SLOT) { slotDisplayName = 'Slot 7' occupiedSlotsForMap = ['7', '8', '10', '11'] + // TC on Flex + } else if (isOt3 && type === THERMOCYCLER_MODULE_TYPE && slot === 'B1') { + occupiedSlotsForMap = ['A1', 'B1'] } - // If collisionSlots are populated, check which slot is occupied // and render module specific crash warning. This logic assumes // default module slot placement magnet = Slot1 temperature = Slot3 @@ -142,6 +149,7 @@ export function ModuleRow(props: Props): JSX.Element { )} diff --git a/protocol-designer/src/modules/moduleData.ts b/protocol-designer/src/modules/moduleData.ts index 9c8795828ef..874b0c63dc1 100644 --- a/protocol-designer/src/modules/moduleData.ts +++ b/protocol-designer/src/modules/moduleData.ts @@ -6,17 +6,18 @@ import { HEATERSHAKER_MODULE_TYPE, ModuleType, MAGNETIC_BLOCK_TYPE, + RobotType, } from '@opentrons/shared-data' import { DropdownOption } from '@opentrons/components' export const SUPPORTED_MODULE_TYPES: ModuleType[] = [ HEATERSHAKER_MODULE_TYPE, + MAGNETIC_BLOCK_TYPE, MAGNETIC_MODULE_TYPE, TEMPERATURE_MODULE_TYPE, THERMOCYCLER_MODULE_TYPE, - MAGNETIC_BLOCK_TYPE, ] type SupportedSlotMap = Record -export const SUPPORTED_MODULE_SLOTS: SupportedSlotMap = { +export const SUPPORTED_MODULE_SLOTS_OT2: SupportedSlotMap = { [MAGNETIC_MODULE_TYPE]: [ { name: 'Slot 1 (supported)', @@ -48,7 +49,39 @@ export const SUPPORTED_MODULE_SLOTS: SupportedSlotMap = { }, ], } -const ALL_MODULE_SLOTS: DropdownOption[] = [ +export const SUPPORTED_MODULE_SLOTS_FLEX: SupportedSlotMap = { + [MAGNETIC_MODULE_TYPE]: [ + { + name: 'Slot D1 (supported)', + value: 'D1', + }, + ], + [TEMPERATURE_MODULE_TYPE]: [ + { + name: 'Slot D3 (supported)', + value: 'D3', + }, + ], + [THERMOCYCLER_MODULE_TYPE]: [ + { + name: 'Thermocycler slots', + value: 'B1', + }, + ], + [HEATERSHAKER_MODULE_TYPE]: [ + { + name: 'Slot D1 (supported)', + value: 'D1', + }, + ], + [MAGNETIC_BLOCK_TYPE]: [ + { + name: 'Slot D2 (supported)', + value: 'D2', + }, + ], +} +const ALL_MODULE_SLOTS_OT2: DropdownOption[] = [ { name: 'Slot 1', value: '1', @@ -78,7 +111,8 @@ const ALL_MODULE_SLOTS: DropdownOption[] = [ value: '10', }, ] -const HEATER_SHAKER_SLOTS: DropdownOption[] = [ + +const HEATER_SHAKER_SLOTS_OT2: DropdownOption[] = [ { name: 'Slot 1', value: '1', @@ -104,22 +138,132 @@ const HEATER_SHAKER_SLOTS: DropdownOption[] = [ value: '10', }, ] +const HS_AND_TEMP_SLOTS_FLEX: DropdownOption[] = [ + { + name: 'Slot D1', + value: 'D1', + }, + { + name: 'Slot D3', + value: 'D3', + }, + { + name: 'Slot C1', + value: 'C1', + }, + { + name: 'Slot C3', + value: 'C3', + }, + { + name: 'Slot B1', + value: 'B1', + }, + { + name: 'Slot B3', + value: 'B3', + }, + { + name: 'Slot A1', + value: 'A1', + }, + { + name: 'Slot A3', + value: 'A3', + }, +] + +const MAG_BLOCK_SLOTS_FLEX: DropdownOption[] = [ + { + name: 'Slot D1', + value: 'D1', + }, + { + name: 'Slot D2', + value: 'D2', + }, + { + name: 'Slot D3', + value: 'D3', + }, + { + name: 'Slot C1', + value: 'C1', + }, + { + name: 'Slot C2', + value: 'C2', + }, + { + name: 'Slot C3', + value: 'C3', + }, + { + name: 'Slot B1', + value: 'B1', + }, + { + name: 'Slot B2', + value: 'B2', + }, + { + name: 'Slot B3', + value: 'B3', + }, + { + name: 'Slot A1', + value: 'A1', + }, + { + name: 'Slot A2', + value: 'A2', + }, + { + name: 'Slot A3', + value: 'A3', + }, +] export function getAllModuleSlotsByType( - moduleType: ModuleType + moduleType: ModuleType, + robotType: RobotType ): DropdownOption[] { - const supportedSlotOption = SUPPORTED_MODULE_SLOTS[moduleType] + const supportedSlotOption = + robotType === 'OT-2 Standard' + ? SUPPORTED_MODULE_SLOTS_OT2[moduleType] + : SUPPORTED_MODULE_SLOTS_FLEX[moduleType] - if (moduleType === THERMOCYCLER_MODULE_TYPE) { - return supportedSlotOption - } - if (moduleType === HEATERSHAKER_MODULE_TYPE) { - return supportedSlotOption.concat( - HEATER_SHAKER_SLOTS.filter(s => s.value !== supportedSlotOption[0].value) + let slot = supportedSlotOption + + if (robotType === 'OT-2 Standard') { + if (moduleType === THERMOCYCLER_MODULE_TYPE) { + slot = supportedSlotOption + } + if (moduleType === HEATERSHAKER_MODULE_TYPE) { + slot = supportedSlotOption.concat( + HEATER_SHAKER_SLOTS_OT2.filter( + s => s.value !== supportedSlotOption[0].value + ) + ) + } + const allOtherSlots = ALL_MODULE_SLOTS_OT2.filter( + s => s.value !== supportedSlotOption[0].value ) + slot = supportedSlotOption.concat(allOtherSlots) + } else { + if (moduleType === THERMOCYCLER_MODULE_TYPE) { + slot = supportedSlotOption + } else if ( + moduleType === HEATERSHAKER_MODULE_TYPE || + moduleType === TEMPERATURE_MODULE_TYPE + ) { + slot = HS_AND_TEMP_SLOTS_FLEX.filter( + s => s.value !== supportedSlotOption[0].value + ) + } else { + slot = MAG_BLOCK_SLOTS_FLEX.filter( + s => s.value !== supportedSlotOption[0].value + ) + } } - - const allOtherSlots = ALL_MODULE_SLOTS.filter( - s => s.value !== supportedSlotOption[0].value - ) - return supportedSlotOption.concat(allOtherSlots) + return slot } From 64f1bcb931d7a6ffa04d71c6c2a32099ae6045fb Mon Sep 17 00:00:00 2001 From: Jethary Date: Wed, 21 Jun 2023 11:08:00 -0400 Subject: [PATCH 02/13] schema version to 7 on PD side only --- protocol-designer/src/file-data/selectors/fileCreator.ts | 8 ++++---- shared-data/protocol/index.ts | 1 + step-generation/src/types.ts | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/protocol-designer/src/file-data/selectors/fileCreator.ts b/protocol-designer/src/file-data/selectors/fileCreator.ts index ee509377c2d..945db829eb6 100644 --- a/protocol-designer/src/file-data/selectors/fileCreator.ts +++ b/protocol-designer/src/file-data/selectors/fileCreator.ts @@ -45,13 +45,13 @@ import type { import type { CreateCommand, ProtocolFile, -} from '@opentrons/shared-data/protocol/types/schemaV6' +} from '@opentrons/shared-data/protocol/types/schemaV7' import type { Selector } from '../../types' import type { LoadLabwareCreateCommand, LoadModuleCreateCommand, LoadPipetteCreateCommand, -} from '@opentrons/shared-data/protocol/types/schemaV6/command/setup' +} from '@opentrons/shared-data/protocol/types/schemaV7/command/setup' import type { PipetteName } from '@opentrons/shared-data/js/pipettes' // TODO: BC: 2018-02-21 uncomment this assert, causes test failures // assert(!isEmpty(process.env.OT_PD_VERSION), 'Could not find application version!') @@ -338,8 +338,8 @@ export const createFile: Selector = createSelector( } return { ...protocolFile, - $otSharedSchema: '#/protocol/schemas/6', - schemaVersion: 6, + $otSharedSchema: '#/protocol/schemas/7', + schemaVersion: 7, modules, commands, } diff --git a/shared-data/protocol/index.ts b/shared-data/protocol/index.ts index 6a6e2a5478d..d4908043480 100644 --- a/shared-data/protocol/index.ts +++ b/shared-data/protocol/index.ts @@ -16,4 +16,5 @@ export type JsonProtocolFile = | Readonly> | Readonly> +// TODO(jr, 6/21/23): update to schemaV7 export * from './types/schemaV6' diff --git a/step-generation/src/types.ts b/step-generation/src/types.ts index 7820639432d..65a6f3bbe36 100644 --- a/step-generation/src/types.ts +++ b/step-generation/src/types.ts @@ -8,13 +8,13 @@ import { LabwareLocation, } from '@opentrons/shared-data' import type { - CreateCommand, LabwareDefinition2, ModuleType, ModuleModel, PipetteNameSpecs, PipetteName, } from '@opentrons/shared-data' +import type { CreateCommand } from '@opentrons/shared-data/protocol/types/schemaV7/command' import type { AtomicProfileStep, EngageMagnetParams, From 69cbeeb218ae059be37fc2fd3850fa3e7e878edd Mon Sep 17 00:00:00 2001 From: Jethary Date: Wed, 21 Jun 2023 11:11:18 -0400 Subject: [PATCH 03/13] modify getRobotModelFromPipettes to work for 96 channel --- protocol-designer/src/file-data/selectors/fileCreator.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/protocol-designer/src/file-data/selectors/fileCreator.ts b/protocol-designer/src/file-data/selectors/fileCreator.ts index 945db829eb6..debdf9c3079 100644 --- a/protocol-designer/src/file-data/selectors/fileCreator.ts +++ b/protocol-designer/src/file-data/selectors/fileCreator.ts @@ -299,7 +299,9 @@ export const createFile: Selector = createSelector( } => { const loadedPipettes = Object.values(pipettes) const pipetteGEN = loadedPipettes.some( - pipette => getPipetteNameSpecs(pipette.name)?.displayCategory === GEN3 + pipette => + getPipetteNameSpecs(pipette.name)?.displayCategory === GEN3 || + getPipetteNameSpecs(pipette.name)?.channels === 96 ) ? GEN3 : GEN2 From a15a0fff075f9f941d88a2263602592c29d6f05a Mon Sep 17 00:00:00 2001 From: Jethary Date: Wed, 21 Jun 2023 21:28:05 -0400 Subject: [PATCH 04/13] create action, reducer, selector for gripper --- .../components/FileSidebar/FileSidebar.tsx | 44 +++++++++++-- .../utils/__tests__/getUnusedEntities.test.ts | 62 ++++++++++++++++++- .../FileSidebar/utils/getUnusedEntities.ts | 18 +++++- .../modals/CreateFileWizard/index.tsx | 6 ++ .../modals/CreateFileWizard/types.ts | 2 +- .../components/modules/EditModulesCard.tsx | 20 ++++-- .../src/components/modules/GripperRow.tsx | 9 +-- .../src/file-data/selectors/fileCreator.ts | 8 +-- protocol-designer/src/load-file/reducers.ts | 1 + .../src/localization/en/alert.json | 5 ++ .../src/step-forms/actions/additionalItems.ts | 7 +++ .../src/step-forms/reducers/index.ts | 59 +++++++++++++++++- .../src/step-forms/selectors/index.ts | 16 ++++- .../src/step-forms/test/reducers.test.ts | 20 +++--- .../top-selectors/labware-locations/index.ts | 5 +- robot-server/simulators/test.json | 14 +++-- step-generation/src/types.ts | 10 ++- 17 files changed, 254 insertions(+), 52 deletions(-) create mode 100644 protocol-designer/src/step-forms/actions/additionalItems.ts diff --git a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx index 87da5e531cd..9d4c17e7e8d 100644 --- a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx +++ b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import { useSelector } from 'react-redux' import cx from 'classnames' import { DeprecatedPrimaryButton, @@ -7,10 +8,12 @@ import { SidePanel, } from '@opentrons/components' import { i18n } from '../../localization' -import { useBlockingHint } from '../Hints/useBlockingHint' -import { KnowledgeBaseLink } from '../KnowledgeBaseLink' import { resetScrollElements } from '../../ui/steps/utils' +import { getRobotType } from '../../file-data/selectors' +import { getAdditionalEquipment } from '../../step-forms/selectors' import { Portal } from '../portals/MainPageModalPortal' +import { useBlockingHint } from '../Hints/useBlockingHint' +import { KnowledgeBaseLink } from '../KnowledgeBaseLink' import { getUnusedEntities } from './utils' import modalStyles from '../modals/modal.css' import styles from './FileSidebar.css' @@ -44,6 +47,7 @@ interface MissingContent { noCommands: boolean pipettesWithoutStep: PipetteOnDeck[] modulesWithoutStep: ModuleOnDeck[] + gripperWithoutStep: boolean } const LOAD_COMMANDS: Array = [ @@ -57,6 +61,7 @@ function getWarningContent({ noCommands, pipettesWithoutStep, modulesWithoutStep, + gripperWithoutStep, }: MissingContent): WarningContent | null { if (noCommands) { return { @@ -73,6 +78,18 @@ function getWarningContent({ } } + if (gripperWithoutStep) { + return { + content: ( + <> +

{i18n.t('alert.export_warnings.unused_gripper.body1')}

+

{i18n.t('alert.export_warnings.unused_gripper.body2')}

+ + ), + heading: i18n.t('alert.export_warnings.unused_gripper.heading'), + } + } + const pipettesDetails = pipettesWithoutStep .map(pipette => `${pipette.mount} ${pipette.spec.displayName}`) .join(' and ') @@ -164,6 +181,11 @@ export function FileSidebar(props: Props): JSX.Element { showExportWarningModal, setShowExportWarningModal, ] = React.useState(false) + const robotType = useSelector(getRobotType) + const additionalEquipment = useSelector(getAdditionalEquipment) + const isGripperAttached = Object.values(additionalEquipment).some( + equipment => equipment?.name === 'gripper' + ) const [showBlockingHint, setShowBlockingHint] = React.useState(false) @@ -174,20 +196,31 @@ export function FileSidebar(props: Props): JSX.Element { command => !LOAD_COMMANDS.includes(command.commandType) ) ?? [] + const hasMoveLabware = + fileData?.commands.find(command => command.commandType === 'moveLabware') != + null + const noCommands = fileData ? nonLoadCommands.length === 0 : true const pipettesWithoutStep = getUnusedEntities( pipettesOnDeck, savedStepForms, - 'pipette' + 'pipette', + robotType ) const modulesWithoutStep = getUnusedEntities( modulesOnDeck, savedStepForms, - 'moduleId' + 'moduleId', + robotType ) + const gripperWithoutStep = isGripperAttached && !hasMoveLabware + const hasWarning = - noCommands || modulesWithoutStep.length || pipettesWithoutStep.length + noCommands || + modulesWithoutStep.length || + pipettesWithoutStep.length || + gripperWithoutStep const warning = hasWarning && @@ -195,6 +228,7 @@ export function FileSidebar(props: Props): JSX.Element { noCommands, pipettesWithoutStep, modulesWithoutStep, + gripperWithoutStep, }) const getExportHintContent = (): { diff --git a/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedEntities.test.ts b/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedEntities.test.ts index 97f9051650e..0b9b2763a92 100644 --- a/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedEntities.test.ts +++ b/protocol-designer/src/components/FileSidebar/utils/__tests__/getUnusedEntities.test.ts @@ -8,6 +8,8 @@ import { TEMPERATURE_MODULE_TYPE, MAGNETIC_MODULE_V1, TEMPERATURE_MODULE_V1, + MAGNETIC_BLOCK_TYPE, + MAGNETIC_BLOCK_V1, } from '@opentrons/shared-data' import { TEMPERATURE_DEACTIVATED } from '@opentrons/step-generation' import { SavedStepFormState } from '../../../../step-forms' @@ -41,7 +43,12 @@ describe('getUnusedEntities', () => { }, } - const result = getUnusedEntities(pipettesOnDeck, stepForms, 'pipette') + const result = getUnusedEntities( + pipettesOnDeck, + stepForms, + 'pipette', + 'OT-2 Standard' + ) expect(result).toEqual([pipettesOnDeck.pipette456]) }) @@ -82,7 +89,58 @@ describe('getUnusedEntities', () => { }, } - const result = getUnusedEntities(modulesOnDeck, stepForms, 'moduleId') + const result = getUnusedEntities( + modulesOnDeck, + stepForms, + 'moduleId', + 'OT-2 Standard' + ) + + expect(result).toEqual([modulesOnDeck.temperature456]) + }) + + it('filters out magnetic block and shows module entities not used in steps are returned for Flex', () => { + const stepForms: SavedStepFormState = { + step123: { + moduleId: 'magnet123', + id: 'step123', + magnetAction: 'engage', + engageHeight: '10', + stepType: 'magnet', + stepName: 'magnet', + stepDetails: '', + }, + } + const modulesOnDeck = { + magnet123: { + id: 'magnet123', + type: MAGNETIC_BLOCK_TYPE, + model: MAGNETIC_BLOCK_V1, + slot: '3', + moduleState: { + type: MAGNETIC_BLOCK_TYPE, + engaged: false, + }, + }, + temperature456: { + id: 'temperature456', + type: TEMPERATURE_MODULE_TYPE, + model: TEMPERATURE_MODULE_V1, + moduleState: { + type: TEMPERATURE_MODULE_TYPE, + status: TEMPERATURE_DEACTIVATED, + targetTemperature: null, + }, + slot: '9', + }, + } + + const result = getUnusedEntities( + modulesOnDeck, + stepForms, + 'moduleId', + 'OT-3 Standard' + ) expect(result).toEqual([modulesOnDeck.temperature456]) }) diff --git a/protocol-designer/src/components/FileSidebar/utils/getUnusedEntities.ts b/protocol-designer/src/components/FileSidebar/utils/getUnusedEntities.ts index 8e5a6867a2d..48f4e224efd 100644 --- a/protocol-designer/src/components/FileSidebar/utils/getUnusedEntities.ts +++ b/protocol-designer/src/components/FileSidebar/utils/getUnusedEntities.ts @@ -1,23 +1,35 @@ import some from 'lodash/some' import reduce from 'lodash/reduce' +import type { RobotType } from '@opentrons/shared-data' import type { SavedStepFormState } from '../../../step-forms' /** Pull out all entities never specified by step forms. Assumes that all forms share the entityKey */ export function getUnusedEntities( entities: Record, stepForms: SavedStepFormState, - entityKey: 'pipette' | 'moduleId' + entityKey: 'pipette' | 'moduleId', + robotType: RobotType ): T[] { - const a = reduce( + const unusedEntities = reduce( entities, (acc, entity: T, entityId): T[] => { const stepContainsEntity = some( stepForms, form => form[entityKey] === entityId ) + + if ( + robotType === 'OT-3 Standard' && + entityKey === 'moduleId' && + (entity as any).type === 'magneticBlockType' + ) { + return acc + } + return stepContainsEntity ? acc : [...acc, entity] }, [] as T[] ) - return a + + return unusedEntities } diff --git a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx index b9d08a62fa2..8892fd1e3ce 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx @@ -50,6 +50,7 @@ import { WizardHeader } from './WizardHeader' import type { NormalizedPipette } from '@opentrons/step-generation' import type { FormState } from './types' +import { toggleIsGripperRequired } from '../../../step-forms/actions/additionalItems' type WizardStep = | 'robotType' @@ -183,6 +184,11 @@ export function CreateFileWizard(): JSX.Element | null { modules.forEach(moduleArgs => dispatch(stepFormActions.createModule(moduleArgs)) ) + // add gripper + if (values.additionalEquipment.includes('gripper')) { + dispatch(toggleIsGripperRequired()) + console.log(values.additionalEquipment) + } // auto-generate tipracks for pipettes const newTiprackModels: string[] = uniq( pipettes.map(pipette => pipette.tiprackDefURI) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/types.ts b/protocol-designer/src/components/modals/CreateFileWizard/types.ts index 7a833617724..0273da1126c 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/types.ts +++ b/protocol-designer/src/components/modals/CreateFileWizard/types.ts @@ -6,7 +6,7 @@ import type { import type { NewProtocolFields } from '../../../load-file' -type AdditionalEquipment = 'gripper' +export type AdditionalEquipment = 'gripper' export interface FormState { fields: NewProtocolFields diff --git a/protocol-designer/src/components/modules/EditModulesCard.tsx b/protocol-designer/src/components/modules/EditModulesCard.tsx index 9d1fcdbb9d7..553379cfa23 100644 --- a/protocol-designer/src/components/modules/EditModulesCard.tsx +++ b/protocol-designer/src/components/modules/EditModulesCard.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { Card } from '@opentrons/components' import { MAGNETIC_MODULE_TYPE, @@ -16,6 +16,8 @@ import { } from '../../step-forms' import { selectors as featureFlagSelectors } from '../../feature-flags' import { SUPPORTED_MODULE_TYPES } from '../../modules' +import { getAdditionalEquipment } from '../../step-forms/selectors' +import { toggleIsGripperRequired } from '../../step-forms/actions/additionalItems' import { getRobotType } from '../../file-data/selectors' import { CrashInfoBox } from './CrashInfoBox' import { ModuleRow } from './ModuleRow' @@ -33,9 +35,14 @@ export function EditModulesCard(props: Props): JSX.Element { const pipettesByMount = useSelector( stepFormSelectors.getPipettesForEditPipetteForm ) + const additionalEquipment = useSelector(getAdditionalEquipment) + const isGripperAttached = Object.values(additionalEquipment).some( + equipment => equipment?.name === 'gripper' + ) + + const dispatch = useDispatch() const robotType = useSelector(getRobotType) - // const savedStepForms = useSelector(getSavedStepForms) - // const additionalEquipment = savedStepForms.additionalEquipment + const magneticModuleOnDeck = modules[MAGNETIC_MODULE_TYPE] const temperatureModuleOnDeck = modules[TEMPERATURE_MODULE_TYPE] const heaterShakerOnDeck = modules[HEATERSHAKER_MODULE_TYPE] @@ -77,6 +84,9 @@ export function EditModulesCard(props: Props): JSX.Element { : moduleType !== 'magneticBlockType' ) + const handleGripperClick = (): void => { + dispatch(toggleIsGripperRequired()) + } return (
@@ -116,8 +126,8 @@ export function EditModulesCard(props: Props): JSX.Element { })} {isOt3 ? ( console.log('wire this up')} - isGripperAdded={true} + handleGripper={handleGripperClick} + isGripperAdded={isGripperAttached} /> ) : null}
diff --git a/protocol-designer/src/components/modules/GripperRow.tsx b/protocol-designer/src/components/modules/GripperRow.tsx index e1390240f5a..4dbb4d70a66 100644 --- a/protocol-designer/src/components/modules/GripperRow.tsx +++ b/protocol-designer/src/components/modules/GripperRow.tsx @@ -10,12 +10,12 @@ import gripperImage from '../../images/flex_gripper.svg' import styles from './styles.css' interface GripperRowProps { - handleAddGripper: () => void + handleGripper: () => void isGripperAdded: boolean } export function GripperRow(props: GripperRowProps): JSX.Element { - const { handleAddGripper, isGripperAdded } = props + const { handleGripper, isGripperAdded } = props return ( @@ -27,10 +27,7 @@ export function GripperRow(props: GripperRowProps): JSX.Element { className={styles.modules_button_group} style={{ alignSelf: ALIGN_CENTER }} > - + {isGripperAdded ? 'Remove' : 'Add'} diff --git a/protocol-designer/src/file-data/selectors/fileCreator.ts b/protocol-designer/src/file-data/selectors/fileCreator.ts index debdf9c3079..0e995aa25f5 100644 --- a/protocol-designer/src/file-data/selectors/fileCreator.ts +++ b/protocol-designer/src/file-data/selectors/fileCreator.ts @@ -45,13 +45,13 @@ import type { import type { CreateCommand, ProtocolFile, -} from '@opentrons/shared-data/protocol/types/schemaV7' +} from '@opentrons/shared-data/protocol/types/schemaV6' import type { Selector } from '../../types' import type { LoadLabwareCreateCommand, LoadModuleCreateCommand, LoadPipetteCreateCommand, -} from '@opentrons/shared-data/protocol/types/schemaV7/command/setup' +} from '@opentrons/shared-data/protocol/types/schemaV6/command/setup' import type { PipetteName } from '@opentrons/shared-data/js/pipettes' // TODO: BC: 2018-02-21 uncomment this assert, causes test failures // assert(!isEmpty(process.env.OT_PD_VERSION), 'Could not find application version!') @@ -340,8 +340,8 @@ export const createFile: Selector = createSelector( } return { ...protocolFile, - $otSharedSchema: '#/protocol/schemas/7', - schemaVersion: 7, + $otSharedSchema: '#/protocol/schemas/6', + schemaVersion: 6, modules, commands, } diff --git a/protocol-designer/src/load-file/reducers.ts b/protocol-designer/src/load-file/reducers.ts index 4726ab0a6cc..129e40eed99 100644 --- a/protocol-designer/src/load-file/reducers.ts +++ b/protocol-designer/src/load-file/reducers.ts @@ -66,6 +66,7 @@ const unsavedChanges = ( case 'CREATE_MODULE': case 'DELETE_MODULE': case 'EDIT_MODULE': + case 'IS_GRIPPER_REQUIRED': return true default: diff --git a/protocol-designer/src/localization/en/alert.json b/protocol-designer/src/localization/en/alert.json index 0aecc51bbd4..984cdca4508 100644 --- a/protocol-designer/src/localization/en/alert.json +++ b/protocol-designer/src/localization/en/alert.json @@ -201,6 +201,11 @@ "heading": "Unused modules", "body1": "The {{modulesDetails}} specified in your protocol are not currently used in any step. In order to run this protocol you will need to power up and connect the modules to your robot.", "body2": "If you don't intend to use these modules, please consider removing them from your protocol." + }, + "unused_gripper": { + "heading": "Unused gripper", + "body1": "The Flex Gripper is specified in your protocol is not currently used in any step. In order to run this protocol you will need to connect it to your robot.", + "bodh2": "If you don't intend to use the Flex Gripper, please consider removing it from your protocol." } }, "crash": { diff --git a/protocol-designer/src/step-forms/actions/additionalItems.ts b/protocol-designer/src/step-forms/actions/additionalItems.ts new file mode 100644 index 00000000000..b4566148321 --- /dev/null +++ b/protocol-designer/src/step-forms/actions/additionalItems.ts @@ -0,0 +1,7 @@ +export interface SetGripperAction { + type: 'IS_GRIPPER_REQUIRED' +} + +export const toggleIsGripperRequired = (): SetGripperAction => ({ + type: 'IS_GRIPPER_REQUIRED', +}) diff --git a/protocol-designer/src/step-forms/reducers/index.ts b/protocol-designer/src/step-forms/reducers/index.ts index a401ccbc5ec..f3e1fc87ff4 100644 --- a/protocol-designer/src/step-forms/reducers/index.ts +++ b/protocol-designer/src/step-forms/reducers/index.ts @@ -47,7 +47,10 @@ import { getLabwareOnModule } from '../../ui/modules/utils' import { nestedCombineReducers } from './nestedCombineReducers' import { PROFILE_CYCLE, PROFILE_STEP } from '../../form-types' import { Reducer } from 'redux' -import { NormalizedPipetteById } from '@opentrons/step-generation' +import { + NoramlizedAdditionalEquipmentById, + NormalizedPipetteById, +} from '@opentrons/step-generation' import { LoadFileAction } from '../../load-file' import { CreateContainerAction, @@ -111,6 +114,7 @@ import { ResetBatchEditFieldChangesAction, SaveStepFormsMultiAction, } from '../actions' +import { SetGripperAction } from '../actions/additionalItems' type FormState = FormData | null const unsavedFormInitialState = null // the `unsavedForm` state holds temporary form info that is saved or thrown away with "cancel". @@ -134,6 +138,7 @@ export type UnsavedFormActions = | EditProfileCycleAction | EditProfileStepAction | SelectMultipleStepsAction + | SetGripperAction export const unsavedForm = ( rootState: RootState, action: UnsavedFormActions @@ -193,6 +198,7 @@ export const unsavedForm = ( case 'CANCEL_STEP_FORM': case 'CREATE_MODULE': case 'DELETE_MODULE': + case 'IS_GRIPPER_REQUIRED': case 'DELETE_STEP': case 'DELETE_MULTIPLE_STEPS': case 'SELECT_MULTIPLE_STEPS': @@ -481,6 +487,7 @@ export type SavedStepFormsActions = | SwapSlotContentsAction | ReplaceCustomLabwareDef | EditModuleAction + | SetGripperAction export const _editModuleFormUpdate = ({ savedForm, moduleId, @@ -983,7 +990,6 @@ export const savedStepForms = ( { ...savedStepForms } ) } - case 'REPLACE_CUSTOM_LABWARE_DEF': { // no mismatch, it's safe to keep all steps as they are if (!action.payload.isOverwriteMismatched) return savedStepForms @@ -1259,6 +1265,50 @@ export const pipetteInvariantProperties: Reducer< }, initialPipetteState ) + +const initialAdditionalEquipmentState = {} + +export const additionalEquipmentInvariantProperties = handleActions( + { + LOAD_FILE: ( + state: NoramlizedAdditionalEquipmentById + ): NoramlizedAdditionalEquipmentById => { + const additionalEquipmentId = uuid() + const updatedEquipment = { + [additionalEquipmentId]: { + name: 'gripper' as const, + id: additionalEquipmentId, + }, + } + return { ...state, ...updatedEquipment } + }, + IS_GRIPPER_REQUIRED: ( + state: NoramlizedAdditionalEquipmentById + ): NoramlizedAdditionalEquipmentById => { + const additionalEquipmentId = Object.keys(state)[0] + const existingEquipment = state[additionalEquipmentId] + + let updatedEquipment + + if (existingEquipment && existingEquipment.name === 'gripper') { + updatedEquipment = {} + } else { + const newAdditionalEquipmentId = uuid() + updatedEquipment = { + [newAdditionalEquipmentId]: { + name: 'gripper' as const, + id: newAdditionalEquipmentId, + }, + } + } + + return updatedEquipment + }, + DEFAULT: (): NoramlizedAdditionalEquipmentById => ({}), + }, + initialAdditionalEquipmentState +) + export type OrderedStepIdsState = StepIdType[] const initialOrderedStepIdsState: string[] = [] // @ts-expect-error(sa, 2021-6-10): cannot use string literals as action type @@ -1379,6 +1429,7 @@ export interface RootState { labwareInvariantProperties: NormalizedLabwareById pipetteInvariantProperties: NormalizedPipetteById moduleInvariantProperties: ModuleEntities + additionalEquipmentInvariantProperties: NoramlizedAdditionalEquipmentById presavedStepForm: PresavedStepFormState savedStepForms: SavedStepFormState unsavedForm: FormState @@ -1402,6 +1453,10 @@ export const rootReducer: Reducer = nestedCombineReducers( prevStateFallback.moduleInvariantProperties, action ), + additionalEquipmentInvariantProperties: additionalEquipmentInvariantProperties( + prevStateFallback.additionalEquipmentInvariantProperties, + action + ), labwareDefs: labwareDefsRootReducer(prevStateFallback.labwareDefs, action), // 'forms' reducers get full rootReducer state savedStepForms: savedStepForms(state, action), diff --git a/protocol-designer/src/step-forms/selectors/index.ts b/protocol-designer/src/step-forms/selectors/index.ts index 11ff1e00e39..49f7ee12c4b 100644 --- a/protocol-designer/src/step-forms/selectors/index.ts +++ b/protocol-designer/src/step-forms/selectors/index.ts @@ -15,7 +15,10 @@ import { PipetteName, MAGNETIC_BLOCK_TYPE, } from '@opentrons/shared-data' -import { TEMPERATURE_DEACTIVATED } from '@opentrons/step-generation' +import { + NoramlizedAdditionalEquipmentById, + TEMPERATURE_DEACTIVATED, +} from '@opentrons/step-generation' import { INITIAL_DECK_SETUP_STEP_ID } from '../../constants' import { getFormWarnings, @@ -147,11 +150,22 @@ export const _getPipetteEntitiesRootState: ( labwareDefSelectors._getLabwareDefsByIdRootState, denormalizePipetteEntities ) + export const getPipetteEntities: Selector< BaseState, PipetteEntities > = createSelector(rootSelector, _getPipetteEntitiesRootState) +export const _getAdditionalEquipmentRootState: ( + arg: RootState +) => NoramlizedAdditionalEquipmentById = rs => + rs.additionalEquipmentInvariantProperties + +export const getAdditionalEquipment: Selector< + BaseState, + NoramlizedAdditionalEquipmentById +> = createSelector(rootSelector, _getAdditionalEquipmentRootState) + const _getInitialDeckSetupStepFormRootState: ( arg: RootState ) => FormData = rs => rs.savedStepForms[INITIAL_DECK_SETUP_STEP_ID] diff --git a/protocol-designer/src/step-forms/test/reducers.test.ts b/protocol-designer/src/step-forms/test/reducers.test.ts index 33cca837c49..83728c72b76 100644 --- a/protocol-designer/src/step-forms/test/reducers.test.ts +++ b/protocol-designer/src/step-forms/test/reducers.test.ts @@ -336,6 +336,7 @@ describe('labwareInvariantProperties reducer', () => { }) }) }) + describe('moduleInvariantProperties reducer', () => { let prevState: Record const existingModuleId = 'existingModuleId' @@ -977,42 +978,39 @@ describe('savedStepForms reducer: initial deck setup step', () => { }> = [ { testName: 'create mag mod -> override mag step module id', - // @ts-expect-error(sa, 2021-6-14): not a valid module model action: { type: 'CREATE_MODULE', payload: { id: 'newMagdeckId', slot: '1', type: MAGNETIC_MODULE_TYPE, - model: 'someMagModel', + model: 'magneticModuleV1', }, }, expectedModuleId: 'newMagdeckId', }, { testName: 'create temp mod -> DO NOT override mag step module id', - // @ts-expect-error(sa, 2021-6-14): not a valid module model action: { type: 'CREATE_MODULE', payload: { id: 'tempdeckId', slot: '1', type: TEMPERATURE_MODULE_TYPE, - model: 'someTempModel', + model: 'temperatureModuleV1', }, }, expectedModuleId: 'magdeckId', }, { testName: 'create TC -> DO NOT override mag step module id', - // @ts-expect-error(sa, 2021-6-14): not a valid module model action: { type: 'CREATE_MODULE', payload: { id: 'ThermocyclerId', slot: '1', type: THERMOCYCLER_MODULE_TYPE, - model: 'someThermoModel', + model: 'thermocyclerModuleV1', }, }, expectedModuleId: 'magdeckId', @@ -1025,42 +1023,39 @@ describe('savedStepForms reducer: initial deck setup step', () => { }> = [ { testName: 'create TC -> override TC step module id', - // @ts-expect-error(sa, 2021-6-14): not a valid module model action: { type: 'CREATE_MODULE', payload: { id: 'NewTCId', slot: SPAN7_8_10_11_SLOT, type: THERMOCYCLER_MODULE_TYPE, - model: 'someTCModel', + model: 'thermocyclerModuleV1', }, }, expectedModuleId: 'NewTCId', }, { testName: 'create temp mod -> DO NOT override TC step module id', - // @ts-expect-error(sa, 2021-6-14): not a valid module model action: { type: 'CREATE_MODULE', payload: { id: 'tempdeckId', slot: '1', type: TEMPERATURE_MODULE_TYPE, - model: 'someTempModel', + model: 'temperatureModuleV1', }, }, expectedModuleId: 'TCId', }, { testName: 'create magnetic mod -> DO NOT override TC step module id', - // @ts-expect-error(sa, 2021-6-14): not a valid module model action: { type: 'CREATE_MODULE', payload: { id: 'newMagdeckId', slot: '1', type: MAGNETIC_MODULE_TYPE, - model: 'someMagModel', + model: 'magneticModuleV2', }, }, expectedModuleId: 'TCId', @@ -1652,6 +1647,7 @@ describe('unsavedForm reducer', () => { 'SAVE_STEP_FORM', 'SELECT_TERMINAL_ITEM', 'SELECT_MULTIPLE_STEPS', + 'IS_GRIPPER_REQUIRED', ] actionTypes.forEach(actionType => { it(`should clear the unsaved form when any ${actionType} action is dispatched`, () => { diff --git a/protocol-designer/src/top-selectors/labware-locations/index.ts b/protocol-designer/src/top-selectors/labware-locations/index.ts index 2940a00e986..c9701fd1c86 100644 --- a/protocol-designer/src/top-selectors/labware-locations/index.ts +++ b/protocol-designer/src/top-selectors/labware-locations/index.ts @@ -1,5 +1,4 @@ import { createSelector } from 'reselect' -import { useSelector } from 'react-redux' import mapValues from 'lodash/mapValues' import { THERMOCYCLER_MODULE_TYPE, @@ -91,8 +90,8 @@ export const getUnocuppiedLabwareLocationOptions: Selector< > = createSelector( getRobotStateAtActiveItem, getModuleEntities, - (robotState, moduleEntities) => { - const robotType = useSelector(getRobotType) + getRobotType, + (robotState, moduleEntities, robotType) => { const deckDef = getDeckDefFromRobotType(robotType) const allSlotIds = deckDef.locations.orderedSlots.map(slot => slot.id) if (robotState == null) return null diff --git a/robot-server/simulators/test.json b/robot-server/simulators/test.json index 05c9a83e801..b647e5a3915 100644 --- a/robot-server/simulators/test.json +++ b/robot-server/simulators/test.json @@ -1,18 +1,20 @@ { + "machine": "OT-3 Standard", + "strict_attached_instruments": false, "attached_instruments": { "right": { - "model": "p300_single_v1", + "model": "p1000_single_v3.4", "id": "321", - "max_volume": 300, - "name": "p300_single", + "max_volume": 1000, + "name": "p1000_single_gen3", "tip_length": 0, "channels": 1 }, "left": { - "model": "p10_single_v1", + "model": "p50_single_v3.4", "id": "123", - "max_volume": 10, - "name": "p10_single", + "max_volume": 50, + "name": "p50_single_gen3", "tip_length": 0, "channels": 1 } diff --git a/step-generation/src/types.ts b/step-generation/src/types.ts index 65a6f3bbe36..df485508ac8 100644 --- a/step-generation/src/types.ts +++ b/step-generation/src/types.ts @@ -8,13 +8,13 @@ import { LabwareLocation, } from '@opentrons/shared-data' import type { + CreateCommand, LabwareDefinition2, ModuleType, ModuleModel, PipetteNameSpecs, PipetteName, } from '@opentrons/shared-data' -import type { CreateCommand } from '@opentrons/shared-data/protocol/types/schemaV7/command' import type { AtomicProfileStep, EngageMagnetParams, @@ -110,6 +110,13 @@ export interface NormalizedPipetteById { } } +export interface NoramlizedAdditionalEquipmentById { + [additionalEquipmentId: string]: { + name: 'gripper' + id: string + } +} + export type NormalizedPipette = NormalizedPipetteById[keyof NormalizedPipetteById] // "entities" have only properties that are time-invariant @@ -123,7 +130,6 @@ export type PipetteEntity = NormalizedPipette & { export interface PipetteEntities { [pipetteId: string]: PipetteEntity } - // ===== MIX-IN TYPES ===== export type ChangeTipOptions = | 'always' From 771ec021af4c9c202abc1177f293ec7846e7c36f Mon Sep 17 00:00:00 2001 From: Jethary Date: Thu, 22 Jun 2023 08:30:48 -0400 Subject: [PATCH 05/13] add logic to fileSidebar for unused gripper --- .../components/FileSidebar/FileSidebar.tsx | 23 +++++++++++----- .../__tests__/FileSidebar.test.tsx | 26 +++++++++++++++++-- .../src/components/FileSidebar/index.ts | 20 +++++++++++++- .../modals/CreateFileWizard/index.tsx | 3 +-- .../src/localization/en/alert.json | 2 +- 5 files changed, 62 insertions(+), 12 deletions(-) diff --git a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx index 9d4c17e7e8d..0ac2a1ccf86 100644 --- a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx +++ b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx @@ -1,5 +1,4 @@ import * as React from 'react' -import { useSelector } from 'react-redux' import cx from 'classnames' import { DeprecatedPrimaryButton, @@ -9,8 +8,6 @@ import { } from '@opentrons/components' import { i18n } from '../../localization' import { resetScrollElements } from '../../ui/steps/utils' -import { getRobotType } from '../../file-data/selectors' -import { getAdditionalEquipment } from '../../step-forms/selectors' import { Portal } from '../portals/MainPageModalPortal' import { useBlockingHint } from '../Hints/useBlockingHint' import { KnowledgeBaseLink } from '../KnowledgeBaseLink' @@ -25,7 +22,11 @@ import type { ModuleOnDeck, PipetteOnDeck, } from '../../step-forms' -import type { CreateCommand, ProtocolFile } from '@opentrons/shared-data' +import type { + CreateCommand, + ProtocolFile, + RobotType, +} from '@opentrons/shared-data' export interface Props { loadFile: (event: React.ChangeEvent) => unknown @@ -36,6 +37,15 @@ export interface Props { pipettesOnDeck: InitialDeckSetup['pipettes'] modulesOnDeck: InitialDeckSetup['modules'] savedStepForms: SavedStepFormState + robotType: RobotType + additionalEquipment: AdditionalEquipment +} + +export interface AdditionalEquipment { + [additionalEquipmentId: string]: { + name: 'gripper' + id: string + } } interface WarningContent { @@ -176,13 +186,13 @@ export function FileSidebar(props: Props): JSX.Element { modulesOnDeck, pipettesOnDeck, savedStepForms, + robotType, + additionalEquipment, } = props const [ showExportWarningModal, setShowExportWarningModal, ] = React.useState(false) - const robotType = useSelector(getRobotType) - const additionalEquipment = useSelector(getAdditionalEquipment) const isGripperAttached = Object.values(additionalEquipment).some( equipment => equipment?.name === 'gripper' ) @@ -214,6 +224,7 @@ export function FileSidebar(props: Props): JSX.Element { robotType ) + // TODO(jr, 6/22/23): need to refactor when the gripper toggle is wired up const gripperWithoutStep = isGripperAttached && !hasMoveLabware const hasWarning = diff --git a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx index 8c278e40e92..7a461299ea8 100644 --- a/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx +++ b/protocol-designer/src/components/FileSidebar/__tests__/FileSidebar.test.tsx @@ -15,10 +15,12 @@ import { fixtureP300Single, } from '@opentrons/shared-data/pipette/fixtures/name' import fixture_tiprack_10_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_10_ul.json' -import { FileSidebar, v6WarningContent } from '../FileSidebar' import { useBlockingHint } from '../../Hints/useBlockingHint' +import { FileSidebar, v6WarningContent } from '../FileSidebar' jest.mock('../../Hints/useBlockingHint') +jest.mock('../../../file-data/selectors') +jest.mock('../../../step-forms/selectors') const mockUseBlockingHint = useBlockingHint as jest.MockedFunction< typeof useBlockingHint @@ -50,6 +52,8 @@ describe('FileSidebar', () => { pipettesOnDeck: {}, modulesOnDeck: {}, savedStepForms: {}, + robotType: 'OT-2 Standard', + additionalEquipment: {}, } commands = [ @@ -94,7 +98,6 @@ describe('FileSidebar', () => { afterEach(() => { jest.resetAllMocks() }) - it('create new button creates new protocol', () => { const wrapper = shallow() const createButton = wrapper.find(OutlineButton).at(0) @@ -154,6 +157,25 @@ describe('FileSidebar', () => { ) }) + it('warning modal is shown when export is clicked with unused gripper', () => { + const gripperId = 'gripperId' + props.modulesOnDeck = modulesOnDeck + props.savedStepForms = savedStepForms + // @ts-expect-error(sa, 2021-6-22): props.fileData might be null + props.fileData.commands = commands + props.additionalEquipment = { + [gripperId]: { name: 'gripper', id: gripperId }, + } + + const wrapper = shallow() + const downloadButton = wrapper.find(DeprecatedPrimaryButton).at(0) + downloadButton.simulate('click') + const alertModal = wrapper.find(AlertModal) + + expect(alertModal).toHaveLength(1) + expect(alertModal.prop('heading')).toEqual('Unused gripper') + }) + it('warning modal is shown when export is clicked with unused module', () => { props.modulesOnDeck = modulesOnDeck props.savedStepForms = savedStepForms diff --git a/protocol-designer/src/components/FileSidebar/index.ts b/protocol-designer/src/components/FileSidebar/index.ts index 70d0b0bb36c..85390d1cf49 100644 --- a/protocol-designer/src/components/FileSidebar/index.ts +++ b/protocol-designer/src/components/FileSidebar/index.ts @@ -3,11 +3,18 @@ import { i18n } from '../../localization' import { actions, selectors } from '../../navigation' import { selectors as fileDataSelectors } from '../../file-data' import { selectors as stepFormSelectors } from '../../step-forms' +import { getRobotType } from '../../file-data/selectors' +import { getAdditionalEquipment } from '../../step-forms/selectors' import { actions as loadFileActions, selectors as loadFileSelectors, } from '../../load-file' -import { FileSidebar as FileSidebarComponent, Props } from './FileSidebar' +import { + AdditionalEquipment, + FileSidebar as FileSidebarComponent, + Props, +} from './FileSidebar' +import type { RobotType } from '@opentrons/shared-data' import type { BaseState, ThunkDispatch } from '../../types' import type { SavedStepFormState, InitialDeckSetup } from '../../step-forms' @@ -19,6 +26,8 @@ interface SP { pipettesOnDeck: InitialDeckSetup['pipettes'] modulesOnDeck: InitialDeckSetup['modules'] savedStepForms: SavedStepFormState + robotType: RobotType + additionalEquipment: AdditionalEquipment } export const FileSidebar = connect( mapStateToProps, @@ -31,12 +40,17 @@ function mapStateToProps(state: BaseState): SP { const fileData = fileDataSelectors.createFile(state) const canDownload = selectors.getCurrentPage(state) !== 'file-splash' const initialDeckSetup = stepFormSelectors.getInitialDeckSetup(state) + const robotType = getRobotType(state) + const additionalEquipment = getAdditionalEquipment(state) + return { canDownload, fileData, pipettesOnDeck: initialDeckSetup.pipettes, modulesOnDeck: initialDeckSetup.modules, savedStepForms: stepFormSelectors.getSavedStepForms(state), + robotType: robotType, + additionalEquipment: additionalEquipment, // Ignore clicking 'CREATE NEW' button in these cases _canCreateNew: !selectors.getNewProtocolModal(state), _hasUnsavedChanges: loadFileSelectors.getHasUnsavedChanges(state), @@ -57,6 +71,8 @@ function mergeProps( pipettesOnDeck, modulesOnDeck, savedStepForms, + robotType, + additionalEquipment, } = stateProps const { dispatch } = dispatchProps return { @@ -77,5 +93,7 @@ function mergeProps( pipettesOnDeck, modulesOnDeck, savedStepForms, + robotType, + additionalEquipment, } } diff --git a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx index 8892fd1e3ce..ce6479472bc 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx @@ -41,6 +41,7 @@ import * as labwareDefSelectors from '../../../labware-defs/selectors' import * as labwareDefActions from '../../../labware-defs/actions' import * as labwareIngredActions from '../../../labware-ingred/actions' import { actions as steplistActions } from '../../../steplist' +import { toggleIsGripperRequired } from '../../../step-forms/actions/additionalItems' import { RobotTypeTile } from './RobotTypeTile' import { MetadataTile } from './MetadataTile' import { FirstPipetteTypeTile, SecondPipetteTypeTile } from './PipetteTypeTile' @@ -50,7 +51,6 @@ import { WizardHeader } from './WizardHeader' import type { NormalizedPipette } from '@opentrons/step-generation' import type { FormState } from './types' -import { toggleIsGripperRequired } from '../../../step-forms/actions/additionalItems' type WizardStep = | 'robotType' @@ -187,7 +187,6 @@ export function CreateFileWizard(): JSX.Element | null { // add gripper if (values.additionalEquipment.includes('gripper')) { dispatch(toggleIsGripperRequired()) - console.log(values.additionalEquipment) } // auto-generate tipracks for pipettes const newTiprackModels: string[] = uniq( diff --git a/protocol-designer/src/localization/en/alert.json b/protocol-designer/src/localization/en/alert.json index 984cdca4508..fba5d2acb9c 100644 --- a/protocol-designer/src/localization/en/alert.json +++ b/protocol-designer/src/localization/en/alert.json @@ -205,7 +205,7 @@ "unused_gripper": { "heading": "Unused gripper", "body1": "The Flex Gripper is specified in your protocol is not currently used in any step. In order to run this protocol you will need to connect it to your robot.", - "bodh2": "If you don't intend to use the Flex Gripper, please consider removing it from your protocol." + "body2": "If you don't intend to use the Flex Gripper, please consider removing it from your protocol." } }, "crash": { From 3cd8a4324b24da6aa109b0e4b64958579f58b1cc Mon Sep 17 00:00:00 2001 From: Jethary Date: Thu, 22 Jun 2023 09:27:25 -0400 Subject: [PATCH 06/13] feat(protocol-designer): update modules section to accommodate gripper closes RLIQ-416, RAUT-504, RAUT-506 --- .../src/components/DeckSetup/index.tsx | 4 ++-- .../modals/CreateFileWizard/MetadataTile.tsx | 2 +- .../src/components/modules/GripperRow.tsx | 11 ++++++++--- protocol-designer/src/localization/en/shared.json | 4 +++- robot-server/simulators/test.json | 14 ++++++-------- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/protocol-designer/src/components/DeckSetup/index.tsx b/protocol-designer/src/components/DeckSetup/index.tsx index e414fe2b65a..28672bfe923 100644 --- a/protocol-designer/src/components/DeckSetup/index.tsx +++ b/protocol-designer/src/components/DeckSetup/index.tsx @@ -432,7 +432,7 @@ export const DeckSetup = (): JSX.Element => { }) return ( - + <>
{drilledDown && }
@@ -458,7 +458,7 @@ export const DeckSetup = (): JSX.Element => {
-
+ ) } diff --git a/protocol-designer/src/components/modals/CreateFileWizard/MetadataTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/MetadataTile.tsx index 6aad2584ef0..74deacf3769 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/MetadataTile.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/MetadataTile.tsx @@ -14,9 +14,9 @@ import { } from '@opentrons/components' import { InputField } from './InputField' import { GoBackLink } from './GoBackLink' +import { HandleEnter } from './HandleEnter' import type { WizardTileProps } from './types' -import { HandleEnter } from './HandleEnter' export function MetadataTile(props: WizardTileProps): JSX.Element { const { i18n, t } = useTranslation() diff --git a/protocol-designer/src/components/modules/GripperRow.tsx b/protocol-designer/src/components/modules/GripperRow.tsx index 4dbb4d70a66..35eefeec92c 100644 --- a/protocol-designer/src/components/modules/GripperRow.tsx +++ b/protocol-designer/src/components/modules/GripperRow.tsx @@ -1,10 +1,12 @@ import * as React from 'react' import styled from 'styled-components' +import { useTranslation } from 'react-i18next' import { OutlineButton, Flex, JUSTIFY_SPACE_BETWEEN, ALIGN_CENTER, + DIRECTION_COLUMN, } from '@opentrons/components' import gripperImage from '../../images/flex_gripper.svg' import styles from './styles.css' @@ -16,11 +18,12 @@ interface GripperRowProps { export function GripperRow(props: GripperRowProps): JSX.Element { const { handleGripper, isGripperAdded } = props + const { i18n, t } = useTranslation() return ( - -

{'Flex Gripper'}

+ +

Flex Gripper

- {isGripperAdded ? 'Remove' : 'Add'} + {isGripperAdded + ? i18n.format(t('shared.remove'), 'capitalize') + : i18n.format(t('shared.add'), 'capitalize')}
diff --git a/protocol-designer/src/localization/en/shared.json b/protocol-designer/src/localization/en/shared.json index c57c7aece34..5a2b4c32e8a 100644 --- a/protocol-designer/src/localization/en/shared.json +++ b/protocol-designer/src/localization/en/shared.json @@ -2,5 +2,7 @@ "exit": "exit", "go_back": "go back", "next": "next", - "step": "Step {{current}} / {{max}}" + "step": "Step {{current}} / {{max}}", + "remove": "remove", + "add": "add" } diff --git a/robot-server/simulators/test.json b/robot-server/simulators/test.json index b647e5a3915..05c9a83e801 100644 --- a/robot-server/simulators/test.json +++ b/robot-server/simulators/test.json @@ -1,20 +1,18 @@ { - "machine": "OT-3 Standard", - "strict_attached_instruments": false, "attached_instruments": { "right": { - "model": "p1000_single_v3.4", + "model": "p300_single_v1", "id": "321", - "max_volume": 1000, - "name": "p1000_single_gen3", + "max_volume": 300, + "name": "p300_single", "tip_length": 0, "channels": 1 }, "left": { - "model": "p50_single_v3.4", + "model": "p10_single_v1", "id": "123", - "max_volume": 50, - "name": "p50_single_gen3", + "max_volume": 10, + "name": "p10_single", "tip_length": 0, "channels": 1 } From 179da926f2b95584aea22ecd950cf6e70468d7e2 Mon Sep 17 00:00:00 2001 From: Jethary Date: Thu, 22 Jun 2023 10:22:25 -0400 Subject: [PATCH 07/13] fix tests --- .../components/__tests__/FilePage.test.tsx | 3 +++ .../__tests__/EditModulesModal.test.tsx | 5 ++++ .../__tests__/EditModulesCard.test.tsx | 25 +++++++++++++++++-- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/protocol-designer/src/components/__tests__/FilePage.test.tsx b/protocol-designer/src/components/__tests__/FilePage.test.tsx index 4a6da3e9626..1c9b2bff5f8 100644 --- a/protocol-designer/src/components/__tests__/FilePage.test.tsx +++ b/protocol-designer/src/components/__tests__/FilePage.test.tsx @@ -6,6 +6,7 @@ import { FilePage, Props } from '../FilePage' import { EditModules } from '../EditModules' import { EditModulesCard } from '../modules' import { ModulesForEditModulesCard } from '../../step-forms' +import { getAdditionalEquipment } from '../../step-forms/selectors' jest.mock('../EditModules') jest.mock('../../step-forms/utils') @@ -14,6 +15,7 @@ jest.mock('../../feature-flags') jest.mock('../../file-data/selectors') const editModulesMock: jest.MockedFunction = EditModules +const editGetAdditionalEquipment: jest.MockedFunction = getAdditionalEquipment describe('File Page', () => { let props: Props @@ -33,6 +35,7 @@ describe('File Page', () => { getState: () => ({ mock: 'this is a mocked out getState' }), } editModulesMock.mockImplementation(() =>
mock edit modules
) + editGetAdditionalEquipment.mockReturnValue({}) }) const render = (props: Props) => diff --git a/protocol-designer/src/components/modals/EditModulesModal/__tests__/EditModulesModal.test.tsx b/protocol-designer/src/components/modals/EditModulesModal/__tests__/EditModulesModal.test.tsx index 205ca846a58..83f2c646835 100644 --- a/protocol-designer/src/components/modals/EditModulesModal/__tests__/EditModulesModal.test.tsx +++ b/protocol-designer/src/components/modals/EditModulesModal/__tests__/EditModulesModal.test.tsx @@ -30,6 +30,7 @@ import { import * as moduleData from '../../../../modules/moduleData' import { MODELS_FOR_MODULE_TYPE } from '../../../../constants' import { selectors as featureSelectors } from '../../../../feature-flags' +import { getRobotType } from '../../../../file-data/selectors' import { getLabwareIsCompatible } from '../../../../utils/labwareModuleCompatibility' import { isModuleWithCollisionIssue } from '../../../modules/utils' import { PDAlert } from '../../../alerts/PDAlert' @@ -51,6 +52,7 @@ jest.mock('../../../../step-forms/selectors') jest.mock('../../../modules/utils') jest.mock('../../../../step-forms/utils') jest.mock('../form-state') +jest.mock('../../../../file-data/selectors') const MODEL_FIELD = 'selectedModel' const SLOT_FIELD = 'selectedSlot' @@ -73,10 +75,13 @@ const getLabwareOnSlotMock: jest.MockedFunction = getLabwareOnSlot const getIsLabwareAboveHeightMock: jest.MockedFunction = getIsLabwareAboveHeight +const getRobotTypeMock: jest.MockedFunction = getRobotType + describe('Edit Modules Modal', () => { let mockStore: any let props: EditModulesModalProps beforeEach(() => { + getRobotTypeMock.mockReturnValue('OT-2 Standard') getInitialDeckSetupMock.mockReturnValue(getMockDeckSetup()) getSlotIdsBlockedBySpanningMock.mockReturnValue([]) getLabwareOnSlotMock.mockReturnValue({}) diff --git a/protocol-designer/src/components/modules/__tests__/EditModulesCard.test.tsx b/protocol-designer/src/components/modules/__tests__/EditModulesCard.test.tsx index c07cd5a34d3..24f45d00da7 100644 --- a/protocol-designer/src/components/modules/__tests__/EditModulesCard.test.tsx +++ b/protocol-designer/src/components/modules/__tests__/EditModulesCard.test.tsx @@ -16,10 +16,12 @@ import { } from '../../../step-forms' import { getRobotType } from '../../../file-data/selectors' import { FormPipette } from '../../../step-forms/types' +import { getAdditionalEquipment } from '../../../step-forms/selectors' import { SUPPORTED_MODULE_TYPES } from '../../../modules' import { EditModulesCard } from '../EditModulesCard' import { CrashInfoBox } from '../CrashInfoBox' import { ModuleRow } from '../ModuleRow' +import { GripperRow } from '../GripperRow' jest.mock('../../../feature-flags') jest.mock('../../../step-forms/selectors') @@ -34,7 +36,9 @@ const getPipettesForEditPipetteFormMock = stepFormSelectors.getPipettesForEditPi const mockGetRobotType = getRobotType as jest.MockedFunction< typeof getRobotType > - +const mockGetAdditionalEquipment = getAdditionalEquipment as jest.MockedFunction< + typeof getAdditionalEquipment +> describe('EditModulesCard', () => { let store: any let crashableMagneticModule: ModuleOnDeck | undefined @@ -72,7 +76,7 @@ describe('EditModulesCard', () => { pipetteName: 'p300_multi_test', tiprackDefURI: 'tiprack300', } - + mockGetAdditionalEquipment.mockReturnValue({}) mockGetRobotType.mockReturnValue('OT-2 Standard') getDisableModuleRestrictionsMock.mockReturnValue(false) getPipettesForEditPipetteFormMock.mockReturnValue({ @@ -195,6 +199,7 @@ describe('EditModulesCard', () => { expect( wrapper.find(ModuleRow).filter({ type: MAGNETIC_MODULE_TYPE }).props() ).toEqual({ + isOt3: false, type: MAGNETIC_MODULE_TYPE, moduleOnDeck: crashableMagneticModule, showCollisionWarnings: true, @@ -233,4 +238,20 @@ describe('EditModulesCard', () => { }) }) }) + it('displays gripper row with no gripper', () => { + mockGetRobotType.mockReturnValue('OT-3 Standard') + const wrapper = render(props) + expect(wrapper.find(GripperRow)).toHaveLength(1) + expect(wrapper.find(GripperRow).props().isGripperAdded).toEqual(false) + }) + it('displays gripper row with gripper attached', () => { + const mockGripperId = 'gripeprId' + mockGetRobotType.mockReturnValue('OT-3 Standard') + mockGetAdditionalEquipment.mockReturnValue({ + [mockGripperId]: { name: 'gripper', id: mockGripperId }, + }) + const wrapper = render(props) + expect(wrapper.find(GripperRow)).toHaveLength(1) + expect(wrapper.find(GripperRow).props().isGripperAdded).toEqual(true) + }) }) From 5014cdf9877bb10c79b1d8f98293f896603464c2 Mon Sep 17 00:00:00 2001 From: Jethary Date: Thu, 22 Jun 2023 11:07:21 -0400 Subject: [PATCH 08/13] remove loadFile action in additionalEquipmentInvariantProperties --- protocol-designer/src/step-forms/reducers/index.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/protocol-designer/src/step-forms/reducers/index.ts b/protocol-designer/src/step-forms/reducers/index.ts index f3e1fc87ff4..fd12a721e98 100644 --- a/protocol-designer/src/step-forms/reducers/index.ts +++ b/protocol-designer/src/step-forms/reducers/index.ts @@ -1270,18 +1270,6 @@ const initialAdditionalEquipmentState = {} export const additionalEquipmentInvariantProperties = handleActions( { - LOAD_FILE: ( - state: NoramlizedAdditionalEquipmentById - ): NoramlizedAdditionalEquipmentById => { - const additionalEquipmentId = uuid() - const updatedEquipment = { - [additionalEquipmentId]: { - name: 'gripper' as const, - id: additionalEquipmentId, - }, - } - return { ...state, ...updatedEquipment } - }, IS_GRIPPER_REQUIRED: ( state: NoramlizedAdditionalEquipmentById ): NoramlizedAdditionalEquipmentById => { From e33af4ebd904392c3f401923d56a7c0e7b3b4e61 Mon Sep 17 00:00:00 2001 From: Jethary Date: Thu, 22 Jun 2023 12:17:44 -0400 Subject: [PATCH 09/13] fix loadFile action logic in additionalEquipmentInvariantProperties --- .../src/step-forms/reducers/index.ts | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/protocol-designer/src/step-forms/reducers/index.ts b/protocol-designer/src/step-forms/reducers/index.ts index fd12a721e98..808d9225f29 100644 --- a/protocol-designer/src/step-forms/reducers/index.ts +++ b/protocol-designer/src/step-forms/reducers/index.ts @@ -1268,8 +1268,35 @@ export const pipetteInvariantProperties: Reducer< const initialAdditionalEquipmentState = {} -export const additionalEquipmentInvariantProperties = handleActions( +export const additionalEquipmentInvariantProperties = handleActions( { + // TODO(jr, 6/22/23): wire this up when schemav7 migration is complete + // Additionally, wire up the the gripper logic using the toggle in moveLabware step + // @ts-expect-error + LOAD_FILE: ( + state, + action: LoadFileAction + ): NoramlizedAdditionalEquipmentById => { + const { file } = action.payload + const gripper = + // @ts-expect-error (jr, 6/22/23): moveLabware doesn't exist in schemav6 + file.commands.filter(command => command.commandType === 'moveLabware') + const hasGripper = gripper.length > 0 + // @ts-expect-error (jr, 6/22/23): OT-3 Standard doesn't exist on schemav6 + const isOt3 = file.robot.model === 'OT-3 Standard' + const additionalEquipmentId = uuid() + const updatedEquipment = { + [additionalEquipmentId]: { + name: 'gripper' as const, + id: additionalEquipmentId, + }, + } + if (hasGripper && isOt3) { + return { ...state, ...updatedEquipment } + } else { + return { ...state } + } + }, IS_GRIPPER_REQUIRED: ( state: NoramlizedAdditionalEquipmentById ): NoramlizedAdditionalEquipmentById => { From d14d735f3a0b246dc11b2e31adb384c32afe2297 Mon Sep 17 00:00:00 2001 From: Jethary Date: Thu, 22 Jun 2023 14:14:44 -0400 Subject: [PATCH 10/13] address comments and wire up unused gripper modal logic --- components/src/slotmap/SlotMap.tsx | 8 +-- .../src/slotmap/__tests__/SlotMap.test.tsx | 10 +++- .../src/components/DeckSetup/index.tsx | 50 +++++++++---------- .../components/FileSidebar/FileSidebar.tsx | 12 +++-- .../FileSidebar/utils/getUnusedEntities.ts | 4 +- .../EditModulesModal/ConnectedSlotMap.tsx | 7 +-- .../modals/EditModulesModal/index.tsx | 5 +- .../components/modules/EditModulesCard.tsx | 13 ++--- .../src/components/modules/ModuleRow.tsx | 15 ++++-- .../src/file-data/selectors/fileCreator.ts | 2 + .../src/step-forms/actions/additionalItems.ts | 8 +-- .../src/step-forms/reducers/index.ts | 36 ++++++------- .../src/step-forms/selectors/index.ts | 6 +-- .../src/step-forms/test/reducers.test.ts | 2 +- step-generation/src/types.ts | 2 +- 15 files changed, 100 insertions(+), 80 deletions(-) diff --git a/components/src/slotmap/SlotMap.tsx b/components/src/slotmap/SlotMap.tsx index e659802868b..ad51503e197 100644 --- a/components/src/slotmap/SlotMap.tsx +++ b/components/src/slotmap/SlotMap.tsx @@ -1,5 +1,6 @@ import * as React from 'react' import cx from 'classnames' +import { RobotType } from '@opentrons/shared-data' import { Icon } from '../icons' import styles from './styles.css' @@ -13,7 +14,7 @@ export interface SlotMapProps { collisionSlots?: string[] /** Optional error styling */ isError?: boolean - isOt3?: boolean + robotType?: RobotType } const OT2_SLOT_MAP_SLOTS = [ @@ -37,8 +38,9 @@ const numRows = 4 const numCols = 3 export function SlotMap(props: SlotMapProps): JSX.Element { - const { collisionSlots, occupiedSlots, isError, isOt3 } = props - const slots = isOt3 ? FLEX_SLOT_MAP_SLOTS : OT2_SLOT_MAP_SLOTS + const { collisionSlots, occupiedSlots, isError, robotType } = props + const slots = + robotType === 'OT-3 Standard' ? FLEX_SLOT_MAP_SLOTS : OT2_SLOT_MAP_SLOTS return ( { }) it('should render 12 slots for flex', () => { - const wrapper = shallow() + const wrapper = shallow( + + ) expect(wrapper.find('rect')).toHaveLength(12) }) it('component renders crash info icon when collision slots present for flex', () => { const wrapper = shallow( - + ) expect(wrapper.find(Icon)).toHaveLength(1) }) diff --git a/protocol-designer/src/components/DeckSetup/index.tsx b/protocol-designer/src/components/DeckSetup/index.tsx index 28672bfe923..48fcb6fb5ed 100644 --- a/protocol-designer/src/components/DeckSetup/index.tsx +++ b/protocol-designer/src/components/DeckSetup/index.tsx @@ -432,33 +432,31 @@ export const DeckSetup = (): JSX.Element => { }) return ( - <> -
- {drilledDown && } -
- - {({ deckSlotsById, getRobotCoordsFromDOMCoords }) => ( - - )} - -
+
+ {drilledDown && } +
+ + {({ deckSlotsById, getRobotCoordsFromDOMCoords }) => ( + + )} +
- +
) } diff --git a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx index 0ac2a1ccf86..0495dd9a9e9 100644 --- a/protocol-designer/src/components/FileSidebar/FileSidebar.tsx +++ b/protocol-designer/src/components/FileSidebar/FileSidebar.tsx @@ -206,9 +206,12 @@ export function FileSidebar(props: Props): JSX.Element { command => !LOAD_COMMANDS.includes(command.commandType) ) ?? [] - const hasMoveLabware = - fileData?.commands.find(command => command.commandType === 'moveLabware') != - null + const gripperInUse = + fileData?.commands.find( + command => + command.commandType === 'moveLabware' && + command.params.strategy === 'usingGripper' + ) != null const noCommands = fileData ? nonLoadCommands.length === 0 : true const pipettesWithoutStep = getUnusedEntities( @@ -224,8 +227,7 @@ export function FileSidebar(props: Props): JSX.Element { robotType ) - // TODO(jr, 6/22/23): need to refactor when the gripper toggle is wired up - const gripperWithoutStep = isGripperAttached && !hasMoveLabware + const gripperWithoutStep = isGripperAttached && !gripperInUse const hasWarning = noCommands || diff --git a/protocol-designer/src/components/FileSidebar/utils/getUnusedEntities.ts b/protocol-designer/src/components/FileSidebar/utils/getUnusedEntities.ts index 48f4e224efd..7968ef72ba2 100644 --- a/protocol-designer/src/components/FileSidebar/utils/getUnusedEntities.ts +++ b/protocol-designer/src/components/FileSidebar/utils/getUnusedEntities.ts @@ -1,6 +1,6 @@ import some from 'lodash/some' import reduce from 'lodash/reduce' -import type { RobotType } from '@opentrons/shared-data' +import { FLEX_ROBOT_TYPE, RobotType } from '@opentrons/shared-data' import type { SavedStepFormState } from '../../../step-forms' /** Pull out all entities never specified by step forms. Assumes that all forms share the entityKey */ @@ -19,7 +19,7 @@ export function getUnusedEntities( ) if ( - robotType === 'OT-3 Standard' && + robotType === FLEX_ROBOT_TYPE && entityKey === 'moduleId' && (entity as any).type === 'magneticBlockType' ) { diff --git a/protocol-designer/src/components/modals/EditModulesModal/ConnectedSlotMap.tsx b/protocol-designer/src/components/modals/EditModulesModal/ConnectedSlotMap.tsx index bea8bea2a1f..1280482675a 100644 --- a/protocol-designer/src/components/modals/EditModulesModal/ConnectedSlotMap.tsx +++ b/protocol-designer/src/components/modals/EditModulesModal/ConnectedSlotMap.tsx @@ -1,24 +1,25 @@ import * as React from 'react' import { useField } from 'formik' import { SlotMap } from '@opentrons/components' +import { RobotType } from '@opentrons/shared-data' import styles from './EditModules.css' interface ConnectedSlotMapProps { fieldName: string - isOt3: boolean + robotType: RobotType } export const ConnectedSlotMap = ( props: ConnectedSlotMapProps ): JSX.Element | null => { - const { fieldName, isOt3 } = props + const { fieldName, robotType } = props const [field, meta] = useField(fieldName) return field.value ? (
) : null diff --git a/protocol-designer/src/components/modals/EditModulesModal/index.tsx b/protocol-designer/src/components/modals/EditModulesModal/index.tsx index 865c31fe822..2912ddbd3a4 100644 --- a/protocol-designer/src/components/modals/EditModulesModal/index.tsx +++ b/protocol-designer/src/components/modals/EditModulesModal/index.tsx @@ -18,6 +18,7 @@ import { HEATERSHAKER_MODULE_TYPE, ModuleType, ModuleModel, + OT2_STANDARD_MODEL, } from '@opentrons/shared-data' import { i18n } from '../../../localization' import { @@ -79,7 +80,7 @@ export const EditModulesModal = (props: EditModulesModalProps): JSX.Element => { } = props const robotType = useSelector(getRobotType) const supportedModuleSlot = - robotType === 'OT-2 Standard' + robotType === OT2_STANDARD_MODEL ? SUPPORTED_MODULE_SLOTS_OT2[moduleType][0].value : SUPPORTED_MODULE_SLOTS_FLEX[moduleType][0].value const initialDeckSetup = useSelector(stepFormSelectors.getInitialDeckSetup) @@ -295,7 +296,7 @@ const EditModulesModalComponent = ( )} diff --git a/protocol-designer/src/components/modules/EditModulesCard.tsx b/protocol-designer/src/components/modules/EditModulesCard.tsx index 553379cfa23..d30b66b8624 100644 --- a/protocol-designer/src/components/modules/EditModulesCard.tsx +++ b/protocol-designer/src/components/modules/EditModulesCard.tsx @@ -8,6 +8,7 @@ import { ModuleType, PipetteName, getPipetteNameSpecs, + FLEX_ROBOT_TYPE, } from '@opentrons/shared-data' import { selectors as stepFormSelectors, @@ -75,11 +76,11 @@ export function EditModulesCard(props: Props): JSX.Element { ].some(pipetteSpecs => pipetteSpecs?.channels !== 1) const warningsEnabled = !moduleRestrictionsDisabled - const isOt3 = robotType === 'OT-3 Standard' + const isFlex = robotType === FLEX_ROBOT_TYPE const SUPPORTED_MODULE_TYPES_FILTERED = SUPPORTED_MODULE_TYPES.filter( moduleType => - isOt3 + isFlex ? moduleType !== 'magneticModuleType' : moduleType !== 'magneticBlockType' ) @@ -88,9 +89,9 @@ export function EditModulesCard(props: Props): JSX.Element { dispatch(toggleIsGripperRequired()) } return ( - +
- {warningsEnabled && !isOt3 && ( + {warningsEnabled && !isFlex && ( ) } else { @@ -124,7 +125,7 @@ export function EditModulesCard(props: Props): JSX.Element { ) } })} - {isOt3 ? ( + {isFlex ? (
)} diff --git a/protocol-designer/src/file-data/selectors/fileCreator.ts b/protocol-designer/src/file-data/selectors/fileCreator.ts index 0e995aa25f5..6373400f736 100644 --- a/protocol-designer/src/file-data/selectors/fileCreator.ts +++ b/protocol-designer/src/file-data/selectors/fileCreator.ts @@ -291,6 +291,8 @@ export const createFile: Selector = createSelector( [pipetteId: string]: { name: PipetteName } } + // TODO(jr 6/22/23): entire method should be replaced with the contents of robotType key + // on the protocol file instead of inferring it from pipette types. const getRobotModelFromPipettes = ( pipettes: RobotModel ): { diff --git a/protocol-designer/src/step-forms/actions/additionalItems.ts b/protocol-designer/src/step-forms/actions/additionalItems.ts index b4566148321..b7e53ceb5ca 100644 --- a/protocol-designer/src/step-forms/actions/additionalItems.ts +++ b/protocol-designer/src/step-forms/actions/additionalItems.ts @@ -1,7 +1,7 @@ -export interface SetGripperAction { - type: 'IS_GRIPPER_REQUIRED' +export interface ToggleIsGripperRequiredAction { + type: 'TOGGLE_IS_GRIPPER_REQUIRED' } -export const toggleIsGripperRequired = (): SetGripperAction => ({ - type: 'IS_GRIPPER_REQUIRED', +export const toggleIsGripperRequired = (): ToggleIsGripperRequiredAction => ({ + type: 'TOGGLE_IS_GRIPPER_REQUIRED', }) diff --git a/protocol-designer/src/step-forms/reducers/index.ts b/protocol-designer/src/step-forms/reducers/index.ts index 808d9225f29..10c72d94c5c 100644 --- a/protocol-designer/src/step-forms/reducers/index.ts +++ b/protocol-designer/src/step-forms/reducers/index.ts @@ -48,7 +48,7 @@ import { nestedCombineReducers } from './nestedCombineReducers' import { PROFILE_CYCLE, PROFILE_STEP } from '../../form-types' import { Reducer } from 'redux' import { - NoramlizedAdditionalEquipmentById, + NormalizedAdditionalEquipmentById, NormalizedPipetteById, } from '@opentrons/step-generation' import { LoadFileAction } from '../../load-file' @@ -114,7 +114,7 @@ import { ResetBatchEditFieldChangesAction, SaveStepFormsMultiAction, } from '../actions' -import { SetGripperAction } from '../actions/additionalItems' +import { ToggleIsGripperRequiredAction } from '../actions/additionalItems' type FormState = FormData | null const unsavedFormInitialState = null // the `unsavedForm` state holds temporary form info that is saved or thrown away with "cancel". @@ -138,7 +138,7 @@ export type UnsavedFormActions = | EditProfileCycleAction | EditProfileStepAction | SelectMultipleStepsAction - | SetGripperAction + | ToggleIsGripperRequiredAction export const unsavedForm = ( rootState: RootState, action: UnsavedFormActions @@ -198,7 +198,7 @@ export const unsavedForm = ( case 'CANCEL_STEP_FORM': case 'CREATE_MODULE': case 'DELETE_MODULE': - case 'IS_GRIPPER_REQUIRED': + case 'TOGGLE_IS_GRIPPER_REQUIRED': case 'DELETE_STEP': case 'DELETE_MULTIPLE_STEPS': case 'SELECT_MULTIPLE_STEPS': @@ -487,7 +487,7 @@ export type SavedStepFormsActions = | SwapSlotContentsAction | ReplaceCustomLabwareDef | EditModuleAction - | SetGripperAction + | ToggleIsGripperRequiredAction export const _editModuleFormUpdate = ({ savedForm, moduleId, @@ -1268,19 +1268,21 @@ export const pipetteInvariantProperties: Reducer< const initialAdditionalEquipmentState = {} -export const additionalEquipmentInvariantProperties = handleActions( +export const additionalEquipmentInvariantProperties = handleActions( { - // TODO(jr, 6/22/23): wire this up when schemav7 migration is complete - // Additionally, wire up the the gripper logic using the toggle in moveLabware step // @ts-expect-error LOAD_FILE: ( state, action: LoadFileAction - ): NoramlizedAdditionalEquipmentById => { + ): NormalizedAdditionalEquipmentById => { const { file } = action.payload - const gripper = - // @ts-expect-error (jr, 6/22/23): moveLabware doesn't exist in schemav6 - file.commands.filter(command => command.commandType === 'moveLabware') + const gripper = file.commands.filter( + command => + // @ts-expect-error (jr, 6/22/23): moveLabware doesn't exist in schemav6 + command.commandType === 'moveLabware' && + // @ts-expect-error (jr, 6/22/23): moveLabware doesn't exist in schemav6 + command.params.strategy === 'usingGripper' + ) const hasGripper = gripper.length > 0 // @ts-expect-error (jr, 6/22/23): OT-3 Standard doesn't exist on schemav6 const isOt3 = file.robot.model === 'OT-3 Standard' @@ -1297,9 +1299,9 @@ export const additionalEquipmentInvariantProperties = handleActions { + TOGGLE_IS_GRIPPER_REQUIRED: ( + state: NormalizedAdditionalEquipmentById + ): NormalizedAdditionalEquipmentById => { const additionalEquipmentId = Object.keys(state)[0] const existingEquipment = state[additionalEquipmentId] @@ -1319,7 +1321,7 @@ export const additionalEquipmentInvariantProperties = handleActions ({}), + DEFAULT: (): NormalizedAdditionalEquipmentById => ({}), }, initialAdditionalEquipmentState ) @@ -1444,7 +1446,7 @@ export interface RootState { labwareInvariantProperties: NormalizedLabwareById pipetteInvariantProperties: NormalizedPipetteById moduleInvariantProperties: ModuleEntities - additionalEquipmentInvariantProperties: NoramlizedAdditionalEquipmentById + additionalEquipmentInvariantProperties: NormalizedAdditionalEquipmentById presavedStepForm: PresavedStepFormState savedStepForms: SavedStepFormState unsavedForm: FormState diff --git a/protocol-designer/src/step-forms/selectors/index.ts b/protocol-designer/src/step-forms/selectors/index.ts index 49f7ee12c4b..6b412bcf285 100644 --- a/protocol-designer/src/step-forms/selectors/index.ts +++ b/protocol-designer/src/step-forms/selectors/index.ts @@ -16,7 +16,7 @@ import { MAGNETIC_BLOCK_TYPE, } from '@opentrons/shared-data' import { - NoramlizedAdditionalEquipmentById, + NormalizedAdditionalEquipmentById, TEMPERATURE_DEACTIVATED, } from '@opentrons/step-generation' import { INITIAL_DECK_SETUP_STEP_ID } from '../../constants' @@ -158,12 +158,12 @@ export const getPipetteEntities: Selector< export const _getAdditionalEquipmentRootState: ( arg: RootState -) => NoramlizedAdditionalEquipmentById = rs => +) => NormalizedAdditionalEquipmentById = rs => rs.additionalEquipmentInvariantProperties export const getAdditionalEquipment: Selector< BaseState, - NoramlizedAdditionalEquipmentById + NormalizedAdditionalEquipmentById > = createSelector(rootSelector, _getAdditionalEquipmentRootState) const _getInitialDeckSetupStepFormRootState: ( diff --git a/protocol-designer/src/step-forms/test/reducers.test.ts b/protocol-designer/src/step-forms/test/reducers.test.ts index 83728c72b76..4c5c4ad8192 100644 --- a/protocol-designer/src/step-forms/test/reducers.test.ts +++ b/protocol-designer/src/step-forms/test/reducers.test.ts @@ -1647,7 +1647,7 @@ describe('unsavedForm reducer', () => { 'SAVE_STEP_FORM', 'SELECT_TERMINAL_ITEM', 'SELECT_MULTIPLE_STEPS', - 'IS_GRIPPER_REQUIRED', + 'TOGGLE_IS_GRIPPER_REQUIRED', ] actionTypes.forEach(actionType => { it(`should clear the unsaved form when any ${actionType} action is dispatched`, () => { diff --git a/step-generation/src/types.ts b/step-generation/src/types.ts index df485508ac8..3c7d0849223 100644 --- a/step-generation/src/types.ts +++ b/step-generation/src/types.ts @@ -110,7 +110,7 @@ export interface NormalizedPipetteById { } } -export interface NoramlizedAdditionalEquipmentById { +export interface NormalizedAdditionalEquipmentById { [additionalEquipmentId: string]: { name: 'gripper' id: string From 913236cc7be670cc14a3c53614d1b7b988c0cc1d Mon Sep 17 00:00:00 2001 From: Jethary Date: Thu, 22 Jun 2023 14:25:02 -0400 Subject: [PATCH 11/13] fix test --- .../src/components/modules/__tests__/EditModulesCard.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/protocol-designer/src/components/modules/__tests__/EditModulesCard.test.tsx b/protocol-designer/src/components/modules/__tests__/EditModulesCard.test.tsx index 24f45d00da7..b5dd495d96c 100644 --- a/protocol-designer/src/components/modules/__tests__/EditModulesCard.test.tsx +++ b/protocol-designer/src/components/modules/__tests__/EditModulesCard.test.tsx @@ -7,6 +7,7 @@ import { MAGNETIC_MODULE_V2, TEMPERATURE_MODULE_TYPE, TEMPERATURE_MODULE_V1, + OT2_ROBOT_TYPE, } from '@opentrons/shared-data' import { TEMPERATURE_DEACTIVATED } from '@opentrons/step-generation' import { selectors as featureFlagSelectors } from '../../../feature-flags' @@ -199,7 +200,7 @@ describe('EditModulesCard', () => { expect( wrapper.find(ModuleRow).filter({ type: MAGNETIC_MODULE_TYPE }).props() ).toEqual({ - isOt3: false, + robotType: OT2_ROBOT_TYPE, type: MAGNETIC_MODULE_TYPE, moduleOnDeck: crashableMagneticModule, showCollisionWarnings: true, From 0515d9ea7f1e645a3c802420957d9ead0b619fa2 Mon Sep 17 00:00:00 2001 From: Jethary Date: Thu, 22 Jun 2023 14:58:34 -0400 Subject: [PATCH 12/13] hide gripper toggle for ot2 and disable when gripper not attached --- .../forms/MoveLabwareForm/index.tsx | 61 +++++++++++++++---- .../__tests__/EditModulesCard.test.tsx | 9 +-- .../src/localization/en/tooltip.json | 5 ++ 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/protocol-designer/src/components/StepEditForm/forms/MoveLabwareForm/index.tsx b/protocol-designer/src/components/StepEditForm/forms/MoveLabwareForm/index.tsx index 2f69c273174..cc474fa89dc 100644 --- a/protocol-designer/src/components/StepEditForm/forms/MoveLabwareForm/index.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/MoveLabwareForm/index.tsx @@ -1,5 +1,12 @@ import * as React from 'react' -import { FormGroup } from '@opentrons/components' +import { useSelector } from 'react-redux' +import { + FormGroup, + Tooltip, + TOOLTIP_BOTTOM, + TOOLTIP_FIXED, + useHoverTooltip, +} from '@opentrons/components' import { i18n } from '../../../../localization' import { LabwareField, @@ -7,10 +14,22 @@ import { LabwareLocationField, } from '../../fields' import styles from '../../StepEditForm.css' -import type { StepFormProps } from '../../types' +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' +import { getRobotType } from '../../../../file-data/selectors' +import { getAdditionalEquipment } from '../../../../step-forms/selectors' +import { StepFormProps } from '../../types' export const MoveLabwareForm = (props: StepFormProps): JSX.Element => { const { propsForFields } = props + const robotType = useSelector(getRobotType) + const additionalEquipment = useSelector(getAdditionalEquipment) + const isGripperAttached = Object.values(additionalEquipment).some( + equipment => equipment?.name === 'gripper' + ) + const [targetProps, tooltipProps] = useHoverTooltip({ + placement: TOOLTIP_BOTTOM, + strategy: TOOLTIP_FIXED, + }) return (
@@ -26,16 +45,34 @@ export const MoveLabwareForm = (props: StepFormProps): JSX.Element => { > - - - + {robotType === FLEX_ROBOT_TYPE ? ( + <> + {!isGripperAttached ? ( + + {i18n.t( + 'tooltip.step_fields.moveLabware.disabled.gripper_not_used' + )} + + ) : null} +
+ + + +
+ + ) : null}
{ tiprackDefURI: 'tiprack300', } mockGetAdditionalEquipment.mockReturnValue({}) - mockGetRobotType.mockReturnValue('OT-2 Standard') + mockGetRobotType.mockReturnValue(OT2_ROBOT_TYPE) getDisableModuleRestrictionsMock.mockReturnValue(false) getPipettesForEditPipetteFormMock.mockReturnValue({ left: crashablePipette, @@ -224,7 +225,7 @@ describe('EditModulesCard', () => { }) }) it('displays module row with module to add when no moduleData for Flex', () => { - mockGetRobotType.mockReturnValue('OT-3 Standard') + mockGetRobotType.mockReturnValue(FLEX_ROBOT_TYPE) const wrapper = render(props) const SUPPORTED_MODULE_TYPES_FILTERED = SUPPORTED_MODULE_TYPES.filter( moduleType => moduleType !== 'magneticModuleType' @@ -240,14 +241,14 @@ describe('EditModulesCard', () => { }) }) it('displays gripper row with no gripper', () => { - mockGetRobotType.mockReturnValue('OT-3 Standard') + mockGetRobotType.mockReturnValue(FLEX_ROBOT_TYPE) const wrapper = render(props) expect(wrapper.find(GripperRow)).toHaveLength(1) expect(wrapper.find(GripperRow).props().isGripperAdded).toEqual(false) }) it('displays gripper row with gripper attached', () => { const mockGripperId = 'gripeprId' - mockGetRobotType.mockReturnValue('OT-3 Standard') + mockGetRobotType.mockReturnValue(FLEX_ROBOT_TYPE) mockGetAdditionalEquipment.mockReturnValue({ [mockGripperId]: { name: 'gripper', id: mockGripperId }, }) diff --git a/protocol-designer/src/localization/en/tooltip.json b/protocol-designer/src/localization/en/tooltip.json index 4fbdad37f45..b23c5312cf4 100644 --- a/protocol-designer/src/localization/en/tooltip.json +++ b/protocol-designer/src/localization/en/tooltip.json @@ -70,6 +70,11 @@ "blowout_checkbox": "Redundant with disposal volume" } }, + "moveLabware": { + "disabled": { + "gripper_not_used": "Attach Gripper to move labware" + } + }, "batch_edit": { "disabled": { "pipette-different": "Cannot edit unless selected steps share the same pipette.", From 27f89737c0bcdc5a19355e355bdae0cd39b484cd Mon Sep 17 00:00:00 2001 From: Jethary Date: Fri, 23 Jun 2023 10:38:47 -0400 Subject: [PATCH 13/13] change all OT-3 Standard strings to FLEX_ROBOT_TYPE const --- components/src/slotmap/SlotMap.tsx | 4 ++-- components/src/slotmap/__tests__/SlotMap.test.tsx | 5 +++-- .../src/components/modules/ModuleRow.tsx | 12 ++++++------ protocol-designer/src/step-forms/reducers/index.ts | 3 ++- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/components/src/slotmap/SlotMap.tsx b/components/src/slotmap/SlotMap.tsx index ad51503e197..e3c08cd7e7f 100644 --- a/components/src/slotmap/SlotMap.tsx +++ b/components/src/slotmap/SlotMap.tsx @@ -1,6 +1,6 @@ import * as React from 'react' import cx from 'classnames' -import { RobotType } from '@opentrons/shared-data' +import { FLEX_ROBOT_TYPE, RobotType } from '@opentrons/shared-data' import { Icon } from '../icons' import styles from './styles.css' @@ -40,7 +40,7 @@ const numCols = 3 export function SlotMap(props: SlotMapProps): JSX.Element { const { collisionSlots, occupiedSlots, isError, robotType } = props const slots = - robotType === 'OT-3 Standard' ? FLEX_SLOT_MAP_SLOTS : OT2_SLOT_MAP_SLOTS + robotType === FLEX_ROBOT_TYPE ? FLEX_SLOT_MAP_SLOTS : OT2_SLOT_MAP_SLOTS return ( { it('should render 12 slots for flex', () => { const wrapper = shallow( - + ) expect(wrapper.find('rect')).toHaveLength(12) }) @@ -42,7 +43,7 @@ describe('SlotMap', () => { ) expect(wrapper.find(Icon)).toHaveLength(1) diff --git a/protocol-designer/src/components/modules/ModuleRow.tsx b/protocol-designer/src/components/modules/ModuleRow.tsx index c1304e45c49..1c87d93cfa7 100644 --- a/protocol-designer/src/components/modules/ModuleRow.tsx +++ b/protocol-designer/src/components/modules/ModuleRow.tsx @@ -12,6 +12,10 @@ import { SIZE_1, SPACING, } from '@opentrons/components' +import { + FLEX_ROBOT_TYPE, + THERMOCYCLER_MODULE_TYPE, +} from '@opentrons/shared-data' import { i18n } from '../../localization' import { actions as stepFormActions, ModuleOnDeck } from '../../step-forms' import { @@ -22,11 +26,7 @@ import { ModuleDiagram } from './ModuleDiagram' import { isModuleWithCollisionIssue } from './utils' import styles from './styles.css' -import { - ModuleType, - RobotType, - THERMOCYCLER_MODULE_TYPE, -} from '@opentrons/shared-data' +import type { ModuleType, RobotType } from '@opentrons/shared-data' interface Props { robotType?: RobotType @@ -44,7 +44,7 @@ export function ModuleRow(props: Props): JSX.Element { robotType, } = props const type: ModuleType = moduleOnDeck?.type || props.type - const isFlex = robotType === 'OT-3 Standard' + const isFlex = robotType === FLEX_ROBOT_TYPE const model = moduleOnDeck?.model const slot = moduleOnDeck?.slot diff --git a/protocol-designer/src/step-forms/reducers/index.ts b/protocol-designer/src/step-forms/reducers/index.ts index 10c72d94c5c..2b3fb861daa 100644 --- a/protocol-designer/src/step-forms/reducers/index.ts +++ b/protocol-designer/src/step-forms/reducers/index.ts @@ -7,6 +7,7 @@ import omit from 'lodash/omit' import omitBy from 'lodash/omitBy' import reduce from 'lodash/reduce' import { + FLEX_ROBOT_TYPE, getLabwareDefaultEngageHeight, getLabwareDefURI, getModuleType, @@ -1285,7 +1286,7 @@ export const additionalEquipmentInvariantProperties = handleActions 0 // @ts-expect-error (jr, 6/22/23): OT-3 Standard doesn't exist on schemav6 - const isOt3 = file.robot.model === 'OT-3 Standard' + const isOt3 = file.robot.model === FLEX_ROBOT_TYPE const additionalEquipmentId = uuid() const updatedEquipment = { [additionalEquipmentId]: {