From 2d910f566343f29a314d1c03d95e8c9a499c17c4 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Wed, 18 Dec 2024 09:56:38 -0500 Subject: [PATCH 01/33] refactor(app): dupe LegacyLPC -> LPC --- .../LabwarePositionCheck/AttachProbe.tsx | 196 +++++ .../LabwarePositionCheck/CheckItem.tsx | 511 +++++++++++++ .../LabwarePositionCheck/DetachProbe.tsx | 155 ++++ .../LabwarePositionCheck/ExitConfirmation.tsx | 143 ++++ .../LabwarePositionCheck/FatalErrorModal.tsx | 132 ++++ .../IntroScreen/getPrepCommands.ts | 160 ++++ .../IntroScreen/index.tsx | 225 ++++++ .../LabwarePositionCheck/JogToWell.tsx | 273 +++++++ .../LabwarePositionCheckComponent.tsx | 435 +++++++++++ .../LabwarePositionCheck/LiveOffsetValue.tsx | 73 ++ .../LabwarePositionCheck/PickUpTip.tsx | 460 ++++++++++++ .../LabwarePositionCheck/PrepareSpace.tsx | 141 ++++ .../LabwarePositionCheck/ResultsSummary.tsx | 463 ++++++++++++ .../LabwarePositionCheck/ReturnTip.tsx | 228 ++++++ .../RobotMotionLoader.tsx | 52 ++ .../TerseOffsetTable.stories.tsx | 109 +++ .../LabwarePositionCheck/TipConfirmation.tsx | 84 +++ .../LabwarePositionCheck/TwoUpTileLayout.tsx | 66 ++ .../__fixtures__/index.ts | 4 + .../__fixtures__/mockCompletedAnalysis.ts | 79 ++ .../__fixtures__/mockExistingOffsets.ts | 18 + .../__fixtures__/mockLabwareDef.ts | 11 + .../__fixtures__/mockTipRackDef.ts | 11 + .../__fixtures__/mockWorkingOffsets.ts | 13 + .../__tests__/CheckItem.test.tsx | 702 ++++++++++++++++++ .../__tests__/ExitConfirmation.test.tsx | 55 ++ .../__tests__/PickUpTip.test.tsx | 466 ++++++++++++ .../__tests__/ResultsSummary.test.tsx | 91 +++ .../__tests__/ReturnTip.test.tsx | 258 +++++++ .../__tests__/RobotMotionLoader.test.tsx | 20 + .../__tests__/TipConfirmation.test.tsx | 39 + .../__tests__/useLaunchLPC.test.tsx | 199 +++++ .../LabwarePositionCheck/constants.ts | 11 + .../getLabwarePositionCheckSteps.ts | 52 ++ .../organisms/LabwarePositionCheck/index.tsx | 104 +++ .../organisms/LabwarePositionCheck/types.ts | 111 +++ .../LabwarePositionCheck/useLaunchLPC.tsx | 89 +++ .../doesPipetteVisitAllTipracks.test.ts | 112 +++ .../__tests__/getPrimaryPipetteId.test.ts | 227 ++++++ .../utils/doesPipetteVisitAllTipracks.ts | 40 + .../utils/getDisplayLocation.ts | 66 ++ .../utils/getPrimaryPipetteId.ts | 70 ++ .../utils/getProbeBasedLPCSteps.ts | 90 +++ .../utils/getTipBasedLPCSteps.ts | 198 +++++ .../LabwarePositionCheck/utils/labware.ts | 260 +++++++ 45 files changed, 7302 insertions(+) create mode 100644 app/src/organisms/LabwarePositionCheck/AttachProbe.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/CheckItem.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/DetachProbe.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/IntroScreen/getPrepCommands.ts create mode 100644 app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/JogToWell.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/LiveOffsetValue.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/PickUpTip.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/ReturnTip.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/RobotMotionLoader.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/TipConfirmation.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/TwoUpTileLayout.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/__fixtures__/index.ts create mode 100644 app/src/organisms/LabwarePositionCheck/__fixtures__/mockCompletedAnalysis.ts create mode 100644 app/src/organisms/LabwarePositionCheck/__fixtures__/mockExistingOffsets.ts create mode 100644 app/src/organisms/LabwarePositionCheck/__fixtures__/mockLabwareDef.ts create mode 100644 app/src/organisms/LabwarePositionCheck/__fixtures__/mockTipRackDef.ts create mode 100644 app/src/organisms/LabwarePositionCheck/__fixtures__/mockWorkingOffsets.ts create mode 100644 app/src/organisms/LabwarePositionCheck/__tests__/CheckItem.test.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/__tests__/ExitConfirmation.test.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/__tests__/PickUpTip.test.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/__tests__/ResultsSummary.test.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/__tests__/ReturnTip.test.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/__tests__/RobotMotionLoader.test.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/__tests__/TipConfirmation.test.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/constants.ts create mode 100644 app/src/organisms/LabwarePositionCheck/getLabwarePositionCheckSteps.ts create mode 100644 app/src/organisms/LabwarePositionCheck/index.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/types.ts create mode 100644 app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/utils/__tests__/doesPipetteVisitAllTipracks.test.ts create mode 100644 app/src/organisms/LabwarePositionCheck/utils/__tests__/getPrimaryPipetteId.test.ts create mode 100644 app/src/organisms/LabwarePositionCheck/utils/doesPipetteVisitAllTipracks.ts create mode 100644 app/src/organisms/LabwarePositionCheck/utils/getDisplayLocation.ts create mode 100644 app/src/organisms/LabwarePositionCheck/utils/getPrimaryPipetteId.ts create mode 100644 app/src/organisms/LabwarePositionCheck/utils/getProbeBasedLPCSteps.ts create mode 100644 app/src/organisms/LabwarePositionCheck/utils/getTipBasedLPCSteps.ts create mode 100644 app/src/organisms/LabwarePositionCheck/utils/labware.ts diff --git a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx new file mode 100644 index 00000000000..afd9efba19f --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx @@ -0,0 +1,196 @@ +import { useEffect, useState } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { + RESPONSIVENESS, + SPACING, + LegacyStyledText, + TYPOGRAPHY, +} from '@opentrons/components' +import { getPipetteNameSpecs } from '@opentrons/shared-data' +import { css } from 'styled-components' +import { ProbeNotAttached } from '/app/organisms/PipetteWizardFlows/ProbeNotAttached' +import { RobotMotionLoader } from './RobotMotionLoader' +import attachProbe1 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_1.webm' +import attachProbe8 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_8.webm' +import attachProbe96 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_96.webm' +import { GenericWizardTile } from '/app/molecules/GenericWizardTile' + +import type { Dispatch } from 'react' +import type { + CompletedProtocolAnalysis, + CreateCommand, +} from '@opentrons/shared-data' +import type { LabwareOffset } from '@opentrons/api-client' +import type { Jog } from '/app/molecules/JogControls/types' +import type { useChainRunCommands } from '/app/resources/runs' +import type { + AttachProbeStep, + RegisterPositionAction, + WorkingOffset, +} from './types' + +interface AttachProbeProps extends AttachProbeStep { + protocolData: CompletedProtocolAnalysis + proceed: () => void + registerPosition: Dispatch + chainRunCommands: ReturnType['chainRunCommands'] + setFatalError: (errorMessage: string) => void + workingOffsets: WorkingOffset[] + existingOffsets: LabwareOffset[] + handleJog: Jog + isRobotMoving: boolean + isOnDevice: boolean +} + +export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { + const { t, i18n } = useTranslation(['labware_position_check', 'shared']) + const { + pipetteId, + protocolData, + proceed, + chainRunCommands, + isRobotMoving, + setFatalError, + isOnDevice, + } = props + const [showUnableToDetect, setShowUnableToDetect] = useState(false) + + const pipette = protocolData.pipettes.find(p => p.id === pipetteId) + const pipetteName = pipette?.pipetteName + const pipetteChannels = + pipetteName != null ? getPipetteNameSpecs(pipetteName)?.channels ?? 1 : 1 + let probeVideoSrc = attachProbe1 + let probeLocation = '' + if (pipetteChannels === 8) { + probeLocation = t('backmost') + probeVideoSrc = attachProbe8 + } else if (pipetteChannels === 96) { + probeLocation = t('ninety_six_probe_location') + probeVideoSrc = attachProbe96 + } + + const pipetteMount = pipette?.mount + + useEffect(() => { + // move into correct position for probe attach on mount + chainRunCommands( + [ + { + commandType: 'calibration/moveToMaintenancePosition' as const, + params: { + mount: pipetteMount ?? 'left', + }, + }, + ], + false + ).catch(error => { + setFatalError(error.message as string) + }) + }, []) + + if (pipetteName == null || pipetteMount == null) return null + + const pipetteZMotorAxis: 'leftZ' | 'rightZ' = + pipetteMount === 'left' ? 'leftZ' : 'rightZ' + + const handleProbeAttached = (): void => { + const verifyCommands: CreateCommand[] = [ + { + commandType: 'verifyTipPresence', + params: { + pipetteId, + expectedState: 'present', + followSingularSensor: 'primary', + }, + }, + ] + const homeCommands: CreateCommand[] = [ + { commandType: 'home', params: { axes: [pipetteZMotorAxis] } }, + { + commandType: 'retractAxis' as const, + params: { + axis: pipetteZMotorAxis, + }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'x' }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'y' }, + }, + ] + chainRunCommands(verifyCommands, false) + .then(() => { + chainRunCommands(homeCommands, false) + .then(() => { + proceed() + }) + .catch((e: Error) => { + setFatalError( + `AttachProbe failed to move to safe location after probe attach with message: ${e.message}` + ) + }) + }) + .catch((e: Error) => { + setShowUnableToDetect(true) + }) + } + + if (isRobotMoving) + return ( + + ) + else if (showUnableToDetect) + return ( + + ) + + return ( + + + + } + bodyText={ + + , + }} + /> + + } + proceedButtonText={i18n.format(t('shared:continue'), 'capitalize')} + proceed={handleProbeAttached} + /> + ) +} + +export const BODY_STYLE = css` + ${TYPOGRAPHY.pRegular}; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + font-size: 1.275rem; + line-height: 1.75rem; + } +` diff --git a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx b/app/src/organisms/LabwarePositionCheck/CheckItem.tsx new file mode 100644 index 00000000000..734ee6468b1 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/CheckItem.tsx @@ -0,0 +1,511 @@ +import { useEffect } from 'react' +import omit from 'lodash/omit' +import isEqual from 'lodash/isEqual' +import { Trans, useTranslation } from 'react-i18next' +import { + DIRECTION_COLUMN, + Flex, + LegacyStyledText, + TYPOGRAPHY, +} from '@opentrons/components' +import { RobotMotionLoader } from './RobotMotionLoader' +import { PrepareSpace } from './PrepareSpace' +import { JogToWell } from './JogToWell' +import { + FLEX_ROBOT_TYPE, + getIsTiprack, + getLabwareDefURI, + getLabwareDisplayName, + getModuleType, + HEATERSHAKER_MODULE_TYPE, + IDENTITY_VECTOR, + THERMOCYCLER_MODULE_TYPE, +} from '@opentrons/shared-data' +import { useSelector } from 'react-redux' +import { getLabwareDef } from './utils/labware' +import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' +import { UnorderedList } from '/app/molecules/UnorderedList' +import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' +import { getIsOnDevice } from '/app/redux/config' +import { getDisplayLocation } from './utils/getDisplayLocation' + +import type { Dispatch } from 'react' +import type { LabwareOffset } from '@opentrons/api-client' +import type { + CompletedProtocolAnalysis, + CreateCommand, + LabwareLocation, + MoveLabwareCreateCommand, + RobotType, +} from '@opentrons/shared-data' +import type { useChainRunCommands } from '/app/resources/runs' +import type { + CheckLabwareStep, + RegisterPositionAction, + WorkingOffset, +} from './types' +import type { Jog } from '/app/molecules/JogControls/types' +import type { TFunction } from 'i18next' + +const PROBE_LENGTH_MM = 44.5 + +interface CheckItemProps extends Omit { + section: 'CHECK_LABWARE' | 'CHECK_TIP_RACKS' | 'CHECK_POSITIONS' + protocolData: CompletedProtocolAnalysis + proceed: () => void + chainRunCommands: ReturnType['chainRunCommands'] + setFatalError: (errorMessage: string) => void + registerPosition: Dispatch + workingOffsets: WorkingOffset[] + existingOffsets: LabwareOffset[] + handleJog: Jog + isRobotMoving: boolean + robotType: RobotType + shouldUseMetalProbe: boolean +} +export const CheckItem = (props: CheckItemProps): JSX.Element | null => { + const { + labwareId, + pipetteId, + moduleId, + adapterId, + location, + protocolData, + chainRunCommands, + registerPosition, + workingOffsets, + proceed, + handleJog, + isRobotMoving, + existingOffsets, + setFatalError, + robotType, + shouldUseMetalProbe, + } = props + const { t, i18n } = useTranslation(['labware_position_check', 'shared']) + const isOnDevice = useSelector(getIsOnDevice) + const labwareDef = getLabwareDef(labwareId, protocolData) + const pipette = protocolData.pipettes.find( + pipette => pipette.id === pipetteId + ) + const adapterDisplayName = + adapterId != null + ? getLabwareDef(adapterId, protocolData)?.metadata.displayName + : '' + + const pipetteMount = pipette?.mount + const pipetteName = pipette?.pipetteName + let modulePrepCommands: CreateCommand[] = [] + const moduleType = + (moduleId != null && + 'moduleModel' in location && + location.moduleModel != null && + getModuleType(location.moduleModel)) ?? + null + if (moduleId != null && moduleType === THERMOCYCLER_MODULE_TYPE) { + modulePrepCommands = [ + { + commandType: 'thermocycler/openLid', + params: { moduleId }, + }, + ] + } else if (moduleId != null && moduleType === HEATERSHAKER_MODULE_TYPE) { + modulePrepCommands = [ + { + commandType: 'heaterShaker/closeLabwareLatch', + params: { moduleId }, + }, + { + commandType: 'heaterShaker/deactivateShaker', + params: { moduleId }, + }, + { + commandType: 'heaterShaker/openLabwareLatch', + params: { moduleId }, + }, + ] + } + const initialPosition = workingOffsets.find( + o => + o.labwareId === labwareId && + isEqual(o.location, location) && + o.initialPosition != null + )?.initialPosition + + useEffect(() => { + if (initialPosition == null && modulePrepCommands.length > 0) { + chainRunCommands(modulePrepCommands, false) + .then(() => {}) + .catch((e: Error) => { + setFatalError( + `CheckItem module prep commands failed with message: ${e?.message}` + ) + }) + } + }, [moduleId]) + + if (pipetteName == null || labwareDef == null || pipetteMount == null) + return null + + const labwareDefs = getLabwareDefinitionsFromCommands(protocolData.commands) + const pipetteZMotorAxis: 'leftZ' | 'rightZ' = + pipetteMount === 'left' ? 'leftZ' : 'rightZ' + const isTiprack = getIsTiprack(labwareDef) + const displayLocation = getDisplayLocation( + location, + labwareDefs, + t as TFunction, + i18n + ) + const slotOnlyDisplayLocation = getDisplayLocation( + location, + labwareDefs, + t as TFunction, + i18n, + true + ) + const labwareDisplayName = getLabwareDisplayName(labwareDef) + + let placeItemInstruction: JSX.Element = ( + + ), + }} + /> + ) + + if (isTiprack) { + placeItemInstruction = ( + + ), + }} + /> + ) + } else if (adapterId != null) { + placeItemInstruction = ( + + ), + }} + /> + ) + } + + let newLocation: LabwareLocation + if (moduleId != null) { + newLocation = { moduleId } + } else { + newLocation = { slotName: location.slotName } + } + + let moveLabware: MoveLabwareCreateCommand[] + if (adapterId != null) { + moveLabware = [ + { + commandType: 'moveLabware' as const, + params: { + labwareId: adapterId, + newLocation, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation: + adapterId != null + ? { labwareId: adapterId } + : { slotName: location.slotName }, + strategy: 'manualMoveWithoutPause', + }, + }, + ] + } else { + moveLabware = [ + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation, + strategy: 'manualMoveWithoutPause', + }, + }, + ] + } + const handleConfirmPlacement = (): void => { + chainRunCommands( + [ + ...moveLabware, + ...protocolData.modules.reduce((acc, mod) => { + if (getModuleType(mod.model) === HEATERSHAKER_MODULE_TYPE) { + return [ + ...acc, + { + commandType: 'heaterShaker/closeLabwareLatch', + params: { moduleId: mod.id }, + }, + ] + } + return acc + }, []), + { + commandType: 'moveToWell' as const, + params: { + pipetteId, + labwareId, + wellName: 'A1', + wellLocation: { + origin: 'top' as const, + offset: + robotType === FLEX_ROBOT_TYPE + ? { x: 0, y: 0, z: PROBE_LENGTH_MM } + : IDENTITY_VECTOR, + }, + }, + }, + { commandType: 'savePosition', params: { pipetteId } }, + ], + false + ) + .then(responses => { + const finalResponse = responses[responses.length - 1] + if (finalResponse.data.commandType === 'savePosition') { + const { position } = finalResponse.data?.result ?? { position: null } + registerPosition({ + type: 'initialPosition', + labwareId, + location, + position, + }) + } else { + setFatalError( + `CheckItem failed to save position for initial placement.` + ) + } + }) + .catch((e: Error) => { + setFatalError( + `CheckItem failed to save position for initial placement with message: ${e.message}` + ) + }) + } + const moveLabwareOffDeck: CreateCommand[] = + adapterId != null + ? [ + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveLabware' as const, + params: { + labwareId: adapterId, + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + ] + : [ + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + ] + + const handleConfirmPosition = (): void => { + const heaterShakerPrepCommands: CreateCommand[] = + moduleId != null && + moduleType != null && + moduleType === HEATERSHAKER_MODULE_TYPE + ? [ + { + commandType: 'heaterShaker/openLabwareLatch', + params: { moduleId }, + }, + ] + : [] + const confirmPositionCommands: CreateCommand[] = [ + { + commandType: 'retractAxis' as const, + params: { + axis: pipetteZMotorAxis, + }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'x' }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'y' }, + }, + ...heaterShakerPrepCommands, + ...moveLabwareOffDeck, + ] + + chainRunCommands( + [ + { commandType: 'savePosition', params: { pipetteId } }, + ...confirmPositionCommands, + ], + false + ) + .then(responses => { + const firstResponse = responses[0] + if (firstResponse.data.commandType === 'savePosition') { + const { position } = firstResponse.data?.result ?? { position: null } + registerPosition({ + type: 'finalPosition', + labwareId, + location, + position, + }) + proceed() + } else { + setFatalError('CheckItem failed to save final position with message') + } + }) + .catch((e: Error) => { + setFatalError( + `CheckItem failed to move from final position with message: ${e.message}` + ) + }) + } + const handleGoBack = (): void => { + chainRunCommands( + [ + ...modulePrepCommands, + { commandType: 'home', params: {} }, + ...moveLabwareOffDeck, + ], + false + ) + .then(() => { + registerPosition({ + type: 'initialPosition', + labwareId, + location, + position: null, + }) + }) + .catch((e: Error) => { + setFatalError(`CheckItem failed to home: ${e.message}`) + }) + } + + const existingOffset = + getCurrentOffsetForLabwareInLocation( + existingOffsets, + getLabwareDefURI(labwareDef), + location + )?.vector ?? IDENTITY_VECTOR + + if (isRobotMoving) + return ( + + ) + return ( + + {initialPosition != null ? ( + , + bold: , + }} + /> + } + labwareDef={labwareDef} + pipetteName={pipetteName} + handleConfirmPosition={handleConfirmPosition} + handleGoBack={handleGoBack} + handleJog={handleJog} + initialPosition={initialPosition} + existingOffset={existingOffset} + shouldUseMetalProbe={shouldUseMetalProbe} + /> + ) : ( + + } + labwareDef={labwareDef} + confirmPlacement={handleConfirmPlacement} + robotType={robotType} + /> + )} + + ) +} diff --git a/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx b/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx new file mode 100644 index 00000000000..dd040654a23 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx @@ -0,0 +1,155 @@ +import { useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { css } from 'styled-components' +import { + LegacyStyledText, + RESPONSIVENESS, + SPACING, + TYPOGRAPHY, +} from '@opentrons/components' +import { RobotMotionLoader } from './RobotMotionLoader' +import { getPipetteNameSpecs } from '@opentrons/shared-data' +import detachProbe1 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_1.webm' +import detachProbe8 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_8.webm' +import detachProbe96 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_96.webm' +import { GenericWizardTile } from '/app/molecules/GenericWizardTile' + +import type { Dispatch } from 'react' +import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' +import type { Jog } from '/app/molecules/JogControls/types' +import type { useChainRunCommands } from '/app/resources/runs' +import type { + DetachProbeStep, + RegisterPositionAction, + WorkingOffset, +} from './types' +import type { LabwareOffset } from '@opentrons/api-client' + +interface DetachProbeProps extends DetachProbeStep { + protocolData: CompletedProtocolAnalysis + proceed: () => void + registerPosition: Dispatch + chainRunCommands: ReturnType['chainRunCommands'] + setFatalError: (errorMessage: string) => void + workingOffsets: WorkingOffset[] + existingOffsets: LabwareOffset[] + handleJog: Jog + isRobotMoving: boolean +} +export const DetachProbe = (props: DetachProbeProps): JSX.Element | null => { + const { t, i18n } = useTranslation(['labware_position_check', 'shared']) + const { + pipetteId, + protocolData, + proceed, + chainRunCommands, + isRobotMoving, + setFatalError, + } = props + + const pipette = protocolData.pipettes.find(p => p.id === pipetteId) + const pipetteName = pipette?.pipetteName + const pipetteChannels = + pipetteName != null ? getPipetteNameSpecs(pipetteName)?.channels ?? 1 : 1 + let probeVideoSrc = detachProbe1 + if (pipetteChannels === 8) { + probeVideoSrc = detachProbe8 + } else if (pipetteChannels === 96) { + probeVideoSrc = detachProbe96 + } + const pipetteMount = pipette?.mount + + useEffect(() => { + // move into correct position for probe detach on mount + chainRunCommands( + [ + { + commandType: 'calibration/moveToMaintenancePosition' as const, + params: { + mount: pipetteMount ?? 'left', + }, + }, + ], + false + ).catch(error => { + setFatalError(error.message as string) + }) + }, []) + + if (pipetteName == null || pipetteMount == null) return null + + const pipetteZMotorAxis: 'leftZ' | 'rightZ' = + pipetteMount === 'left' ? 'leftZ' : 'rightZ' + + const handleProbeDetached = (): void => { + chainRunCommands( + [ + { + commandType: 'retractAxis' as const, + params: { + axis: pipetteZMotorAxis, + }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'x' }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'y' }, + }, + ], + false + ) + .then(() => { + proceed() + }) + .catch((e: Error) => { + setFatalError( + `DetachProbe failed to move to safe location after probe detach with message: ${e.message}` + ) + }) + } + + if (isRobotMoving) + return ( + + ) + + return ( + + + + } + bodyText={ + + {i18n.format(t('remove_probe'), 'capitalize')} + + } + proceedButtonText={t('confirm_detached')} + proceed={handleProbeDetached} + /> + ) +} + +export const BODY_STYLE = css` + ${TYPOGRAPHY.pRegular}; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + font-size: 1.275rem; + line-height: 1.75rem; + } +` diff --git a/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx b/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx new file mode 100644 index 00000000000..ab6cb857035 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx @@ -0,0 +1,143 @@ +import styled from 'styled-components' +import { useTranslation } from 'react-i18next' +import { + AlertPrimaryButton, + ALIGN_CENTER, + COLORS, + DIRECTION_COLUMN, + Flex, + Icon, + JUSTIFY_CENTER, + JUSTIFY_FLEX_END, + RESPONSIVENESS, + SecondaryButton, + SIZE_3, + SPACING, + LegacyStyledText, + TEXT_ALIGN_CENTER, + TYPOGRAPHY, +} from '@opentrons/components' +import { useSelector } from 'react-redux' +import { getIsOnDevice } from '/app/redux/config' +import { SmallButton } from '/app/atoms/buttons' + +interface ExitConfirmationProps { + onGoBack: () => void + onConfirmExit: () => void + shouldUseMetalProbe: boolean +} + +export const ExitConfirmation = (props: ExitConfirmationProps): JSX.Element => { + const { i18n, t } = useTranslation(['labware_position_check', 'shared']) + const { onGoBack, onConfirmExit, shouldUseMetalProbe } = props + const isOnDevice = useSelector(getIsOnDevice) + return ( + + + + {isOnDevice ? ( + <> + + {shouldUseMetalProbe + ? t('remove_probe_before_exit') + : t('exit_screen_title')} + + + + {t('exit_screen_subtitle')} + + + + ) : ( + <> + + {shouldUseMetalProbe + ? t('remove_probe_before_exit') + : t('exit_screen_title')} + + + {t('exit_screen_subtitle')} + + + )} + + {isOnDevice ? ( + + + + + ) : ( + + + + {t('shared:go_back')} + + + {shouldUseMetalProbe + ? t('remove_calibration_probe') + : i18n.format(t('shared:exit'), 'capitalize')} + + + + )} + + ) +} + +const ConfirmationHeader = styled.h1` + margin-top: ${SPACING.spacing24}; + ${TYPOGRAPHY.h1Default} + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + ${TYPOGRAPHY.level4HeaderSemiBold} + } +` + +const ConfirmationHeaderODD = styled.h1` + margin-top: ${SPACING.spacing24}; + ${TYPOGRAPHY.level3HeaderBold} + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + ${TYPOGRAPHY.level4HeaderSemiBold} + } +` +const ConfirmationBodyODD = styled.h1` + ${TYPOGRAPHY.level4HeaderRegular} + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + ${TYPOGRAPHY.level4HeaderRegular} + } + color: ${COLORS.grey60}; +` diff --git a/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx b/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx new file mode 100644 index 00000000000..ee98e776055 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx @@ -0,0 +1,132 @@ +import { createPortal } from 'react-dom' +import styled from 'styled-components' +import { useTranslation } from 'react-i18next' +import { + ALIGN_CENTER, + ALIGN_FLEX_END, + BORDERS, + COLORS, + DIRECTION_COLUMN, + Flex, + Icon, + JUSTIFY_SPACE_BETWEEN, + PrimaryButton, + RESPONSIVENESS, + SPACING, + LegacyStyledText, + ModalShell, + TEXT_ALIGN_CENTER, + TEXT_TRANSFORM_CAPITALIZE, + TYPOGRAPHY, +} from '@opentrons/components' +import { getTopPortalEl } from '/app/App/portal' +import { WizardHeader } from '/app/molecules/WizardHeader' +import { i18n } from '/app/i18n' + +const SUPPORT_EMAIL = 'support@opentrons.com' +interface FatalErrorProps { + errorMessage: string + shouldUseMetalProbe: boolean + onClose: () => void +} + +interface FatalErrorModalProps extends FatalErrorProps { + isOnDevice: boolean +} + +export function FatalErrorModal(props: FatalErrorModalProps): JSX.Element { + const { t } = useTranslation(['labware_position_check', 'shared', 'branded']) + const { onClose, isOnDevice } = props + return createPortal( + isOnDevice ? ( + + + + + ) : ( + + } + > + + + ), + getTopPortalEl() + ) +} + +export function FatalError(props: FatalErrorProps): JSX.Element { + const { errorMessage, shouldUseMetalProbe, onClose } = props + const { t } = useTranslation(['labware_position_check', 'shared', 'branded']) + return ( + + + + {i18n.format(t('shared:something_went_wrong'), 'sentenceCase')} + + {shouldUseMetalProbe ? ( + + {t('remove_probe_before_exit')} + + ) : null} + + {t('branded:help_us_improve_send_error_report', { + support_email: SUPPORT_EMAIL, + })} + + + + {t('shared:exit')} + + + ) +} + +const ErrorHeader = styled.h1` + text-align: ${TEXT_ALIGN_CENTER}; + ${TYPOGRAPHY.h1Default} + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + ${TYPOGRAPHY.level4HeaderSemiBold} + } +` + +const ErrorTextArea = styled.textarea` + min-height: 6rem; + width: 30rem; + background-color: #f8f8f8; + border: ${BORDERS.lineBorder}; + border-radius: ${BORDERS.borderRadius4}; + padding: ${SPACING.spacing8}; + margin: ${SPACING.spacing16} 0; + font-size: ${TYPOGRAPHY.fontSizeCaption}; + font-family: monospace; + resize: none; +` diff --git a/app/src/organisms/LabwarePositionCheck/IntroScreen/getPrepCommands.ts b/app/src/organisms/LabwarePositionCheck/IntroScreen/getPrepCommands.ts new file mode 100644 index 00000000000..4e8119e4d74 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/IntroScreen/getPrepCommands.ts @@ -0,0 +1,160 @@ +import { + getModuleType, + HEATERSHAKER_MODULE_TYPE, + THERMOCYCLER_MODULE_TYPE, + ABSORBANCE_READER_TYPE, +} from '@opentrons/shared-data' + +import type { + CompletedProtocolAnalysis, + CreateCommand, + HeaterShakerCloseLatchCreateCommand, + HeaterShakerDeactivateShakerCreateCommand, + HomeCreateCommand, + RunTimeCommand, + SetupRunTimeCommand, + TCOpenLidCreateCommand, + AbsorbanceReaderOpenLidCreateCommand, +} from '@opentrons/shared-data' + +type LPCPrepCommand = + | HomeCreateCommand + | SetupRunTimeCommand + | TCOpenLidCreateCommand + | HeaterShakerDeactivateShakerCreateCommand + | HeaterShakerCloseLatchCreateCommand + | AbsorbanceReaderOpenLidCreateCommand + +export function getPrepCommands( + protocolData: CompletedProtocolAnalysis +): LPCPrepCommand[] { + // load commands come from the protocol resource + const loadCommands: LPCPrepCommand[] = + protocolData.commands + .filter(isLoadCommand) + .reduce((acc, command) => { + if ( + command.commandType === 'loadPipette' && + command.result?.pipetteId != null + ) { + const { pipetteId } = command.result + const loadWithPipetteId = { + ...command, + params: { + ...command.params, + pipetteId, + }, + } + return [...acc, loadWithPipetteId] + } else if ( + command.commandType === 'loadLabware' && + command.result?.labwareId != null + ) { + // load all labware off-deck so that LPC can move them on individually later + return [ + ...acc, + { + ...command, + params: { + ...command.params, + location: 'offDeck', + // python protocols won't have labwareId in the params, we want to + // use the same labwareIds that came back as the result of analysis + labwareId: command.result.labwareId, + }, + }, + ] + } else if ( + command.commandType === 'loadModule' && + command.result?.moduleId != null + ) { + return [ + ...acc, + { + ...command, + params: { + ...command.params, + // python protocols won't have moduleId in the params, we want to + // use the same moduleIds that came back as the result of analysis + moduleId: command.result.moduleId, + }, + }, + ] + } + return [...acc, command] + }, []) ?? [] + + const TCCommands = protocolData.modules.reduce( + (acc, module) => { + if (getModuleType(module.model) === THERMOCYCLER_MODULE_TYPE) { + return [ + ...acc, + { + commandType: 'thermocycler/openLid', + params: { moduleId: module.id }, + }, + ] + } + return acc + }, + [] + ) + + const AbsorbanceCommands = protocolData.modules.reduce( + (acc, module) => { + if (getModuleType(module.model) === ABSORBANCE_READER_TYPE) { + return [ + ...acc, + { + commandType: 'home', + params: {}, + }, + { + commandType: 'absorbanceReader/openLid', + params: { moduleId: module.id }, + }, + ] + } + return acc + }, + [] + ) + + const HSCommands = protocolData.modules.reduce< + HeaterShakerCloseLatchCreateCommand[] + >((acc, module) => { + if (getModuleType(module.model) === HEATERSHAKER_MODULE_TYPE) { + return [ + ...acc, + { + commandType: 'heaterShaker/closeLabwareLatch', + params: { moduleId: module.id }, + }, + ] + } + return acc + }, []) + const homeCommand: HomeCreateCommand = { + commandType: 'home', + params: {}, + } + // prepCommands will be run when a user starts LPC + return [ + ...loadCommands, + ...TCCommands, + ...AbsorbanceCommands, + ...HSCommands, + homeCommand, + ] +} + +function isLoadCommand( + command: RunTimeCommand +): command is SetupRunTimeCommand { + const loadCommands: Array = [ + 'loadLabware', + 'loadModule', + 'loadPipette', + ] + return loadCommands.includes(command.commandType) +} diff --git a/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx b/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx new file mode 100644 index 00000000000..44e5eb67ded --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx @@ -0,0 +1,225 @@ +import { useState } from 'react' +import { createPortal } from 'react-dom' +import { Trans, useTranslation } from 'react-i18next' +import { css } from 'styled-components' + +import { + ALIGN_CENTER, + Box, + Btn, + COLORS, + DIRECTION_COLUMN, + Flex, + Icon, + JUSTIFY_SPACE_BETWEEN, + PrimaryButton, + ModalShell, + SPACING, + LegacyStyledText, + TYPOGRAPHY, +} from '@opentrons/components' +import { RobotMotionLoader } from '../RobotMotionLoader' +import { getPrepCommands } from './getPrepCommands' +import { WizardRequiredEquipmentList } from '/app/molecules/WizardRequiredEquipmentList' +import { getLatestCurrentOffsets } from '/app/transformations/runs' +import { getIsOnDevice } from '/app/redux/config' +import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' +import { useSelector } from 'react-redux' +import { TwoUpTileLayout } from '../TwoUpTileLayout' +import { getTopPortalEl } from '/app/App/portal' +import { SmallButton } from '/app/atoms/buttons' +import { CALIBRATION_PROBE } from '/app/organisms/PipetteWizardFlows/constants' +import { TerseOffsetTable } from '../ResultsSummary' +import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' + +import type { Dispatch } from 'react' +import type { LabwareOffset } from '@opentrons/api-client' +import type { + CompletedProtocolAnalysis, + LabwareDefinition2, +} from '@opentrons/shared-data' +import type { useChainRunCommands } from '/app/resources/runs' +import type { RegisterPositionAction } from '../types' +import type { Jog } from '/app/molecules/JogControls' + +export const INTERVAL_MS = 3000 + +// TODO(BC, 09/01/23): replace updated support article link for LPC on OT-2/Flex +const SUPPORT_PAGE_URL = 'https://support.opentrons.com/s/ot2-calibration' + +export const IntroScreen = (props: { + proceed: () => void + protocolData: CompletedProtocolAnalysis + registerPosition: Dispatch + chainRunCommands: ReturnType['chainRunCommands'] + handleJog: Jog + setFatalError: (errorMessage: string) => void + isRobotMoving: boolean + existingOffsets: LabwareOffset[] + protocolName: string + shouldUseMetalProbe: boolean +}): JSX.Element | null => { + const { + proceed, + protocolData, + chainRunCommands, + isRobotMoving, + setFatalError, + existingOffsets, + protocolName, + shouldUseMetalProbe, + } = props + const isOnDevice = useSelector(getIsOnDevice) + const { t, i18n } = useTranslation(['labware_position_check', 'shared']) + const handleClickStartLPC = (): void => { + const prepCommands = getPrepCommands(protocolData) + chainRunCommands(prepCommands, false) + .then(() => { + proceed() + }) + .catch((e: Error) => { + setFatalError( + `IntroScreen failed to issue prep commands with message: ${e.message}` + ) + }) + } + const requiredEquipmentList = [ + { + loadName: t('all_modules_and_labware_from_protocol', { + protocol_name: protocolName, + }), + displayName: t('all_modules_and_labware_from_protocol', { + protocol_name: protocolName, + }), + }, + ] + if (shouldUseMetalProbe) { + requiredEquipmentList.push(CALIBRATION_PROBE) + } + + if (isRobotMoving) { + return ( + + ) + } + return ( + }} + /> + } + rightElement={ + + } + footer={ + + {isOnDevice ? ( + + ) : ( + + )} + {isOnDevice ? ( + + ) : ( + + {i18n.format(t('shared:get_started'), 'capitalize')} + + )} + + } + /> + ) +} + +const VIEW_OFFSETS_BUTTON_STYLE = css` + ${TYPOGRAPHY.pSemiBold}; + color: ${COLORS.black90}; + font-size: ${TYPOGRAPHY.fontSize22}; + &:hover { + opacity: 100%; + } + &:active { + opacity: 70%; + } +` +interface ViewOffsetsProps { + existingOffsets: LabwareOffset[] + labwareDefinitions: LabwareDefinition2[] +} +function ViewOffsets(props: ViewOffsetsProps): JSX.Element { + const { existingOffsets, labwareDefinitions } = props + const { t, i18n } = useTranslation('labware_position_check') + const [showOffsetsTable, setShowOffsetsModal] = useState(false) + const latestCurrentOffsets = getLatestCurrentOffsets(existingOffsets) + return existingOffsets.length > 0 ? ( + <> + { + setShowOffsetsModal(true) + }} + css={VIEW_OFFSETS_BUTTON_STYLE} + aria-label="show labware offsets" + > + + + {i18n.format(t('view_current_offsets'), 'capitalize')} + + + {showOffsetsTable + ? createPortal( + + {i18n.format(t('labware_offset_data'), 'capitalize')} + + } + footer={ + { + setShowOffsetsModal(false) + }} + /> + } + > + + + + , + getTopPortalEl() + ) + : null} + + ) : ( + + ) +} diff --git a/app/src/organisms/LabwarePositionCheck/JogToWell.tsx b/app/src/organisms/LabwarePositionCheck/JogToWell.tsx new file mode 100644 index 00000000000..bce4808a514 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/JogToWell.tsx @@ -0,0 +1,273 @@ +import { useEffect, useState } from 'react' +import { createPortal } from 'react-dom' +import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' +import styled, { css } from 'styled-components' +import { + ALIGN_CENTER, + ALIGN_FLEX_START, + COLORS, + DIRECTION_COLUMN, + Flex, + JUSTIFY_SPACE_BETWEEN, + LabwareRender, + LegacyStyledText, + ModalShell, + PipetteRender, + PrimaryButton, + RESPONSIVENESS, + RobotWorkSpace, + SecondaryButton, + SPACING, + TYPOGRAPHY, + WELL_LABEL_OPTIONS, +} from '@opentrons/components' +import { + getIsTiprack, + getPipetteNameSpecs, + getVectorDifference, + getVectorSum, +} from '@opentrons/shared-data' + +import levelWithTip from '/app/assets/images/lpc_level_with_tip.svg' +import levelWithLabware from '/app/assets/images/lpc_level_with_labware.svg' +import levelProbeWithTip from '/app/assets/images/lpc_level_probe_with_tip.svg' +import levelProbeWithLabware from '/app/assets/images/lpc_level_probe_with_labware.svg' +import { getIsOnDevice } from '/app/redux/config' +import { getTopPortalEl } from '/app/App/portal' +import { SmallButton } from '/app/atoms/buttons' +import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' +import { JogControls } from '/app/molecules/JogControls' +import { LiveOffsetValue } from './LiveOffsetValue' + +import type { ReactNode } from 'react' +import type { PipetteName, LabwareDefinition2 } from '@opentrons/shared-data' +import type { WellStroke } from '@opentrons/components' +import type { VectorOffset } from '@opentrons/api-client' +import type { Jog } from '/app/molecules/JogControls' + +const DECK_MAP_VIEWBOX = '-10 -10 150 105' +const LPC_HELP_LINK_URL = + 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' + +interface JogToWellProps { + handleConfirmPosition: () => void + handleGoBack: () => void + handleJog: Jog + pipetteName: PipetteName + labwareDef: LabwareDefinition2 + header: ReactNode + body: ReactNode + initialPosition: VectorOffset + existingOffset: VectorOffset + shouldUseMetalProbe: boolean +} +export const JogToWell = (props: JogToWellProps): JSX.Element | null => { + const { t } = useTranslation(['labware_position_check', 'shared']) + const { + header, + body, + pipetteName, + labwareDef, + handleConfirmPosition, + handleGoBack, + handleJog, + initialPosition, + existingOffset, + shouldUseMetalProbe, + } = props + + const [joggedPosition, setJoggedPosition] = useState( + initialPosition + ) + const isOnDevice = useSelector(getIsOnDevice) + const [showFullJogControls, setShowFullJogControls] = useState(false) + useEffect(() => { + // NOTE: this will perform a "null" jog when the jog controls mount so + // if a user reaches the "confirm exit" modal (unmounting this component) + // and clicks "go back" we are able so initialize the live offset to whatever + // distance they had already jogged before clicking exit. + // the `mounted` variable prevents a possible memory leak (see https://legacy.reactjs.org/docs/hooks-effect.html#example-using-hooks-1) + let mounted = true + if (mounted) { + handleJog('x', 1, 0, setJoggedPosition) + } + return () => { + mounted = false + } + }, []) + + let wellsToHighlight: string[] = [] + if ( + getPipetteNameSpecs(pipetteName)?.channels === 8 && + !shouldUseMetalProbe + ) { + wellsToHighlight = labwareDef.ordering[0] + } else { + wellsToHighlight = ['A1'] + } + + const wellStroke: WellStroke = wellsToHighlight.reduce( + (acc, wellName) => ({ ...acc, [wellName]: COLORS.blue50 }), + {} + ) + + const liveOffset = getVectorSum( + existingOffset, + getVectorDifference(joggedPosition, initialPosition) + ) + const isTipRack = getIsTiprack(labwareDef) + let levelSrc = isTipRack ? levelWithTip : levelWithLabware + if (shouldUseMetalProbe) { + levelSrc = isTipRack ? levelProbeWithTip : levelProbeWithLabware + } + return ( + + + +
{header}
+ {body} + +
+ + + {() => ( + <> + + + + )} + + {`level + +
+ {isOnDevice ? ( + + + + { + setShowFullJogControls(true) + }} + /> + + + {showFullJogControls + ? createPortal( + + {t('move_to_a1_position')} + + } + footer={ + { + setShowFullJogControls(false) + }} + /> + } + > + + handleJog(axis, direction, step, setJoggedPosition) + } + isOnDevice={true} + /> + , + getTopPortalEl() + ) + : null} + + ) : ( + <> + + handleJog(axis, direction, step, setJoggedPosition) + } + /> + + + + + {t('shared:go_back')} + + + {t('shared:confirm_position')} + + + + + )} +
+ ) +} + +const Header = styled.h1` + ${TYPOGRAPHY.h1Default} + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + ${TYPOGRAPHY.level4HeaderSemiBold} + } +` diff --git a/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx b/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx new file mode 100644 index 00000000000..6f0953093a6 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx @@ -0,0 +1,435 @@ +import { useState, useEffect, useReducer } from 'react' +import { createPortal } from 'react-dom' +import isEqual from 'lodash/isEqual' +import { useSelector } from 'react-redux' +import { useTranslation } from 'react-i18next' + +import { useConditionalConfirm, ModalShell } from '@opentrons/components' +import { + useCreateLabwareOffsetMutation, + useCreateMaintenanceCommandMutation, +} from '@opentrons/react-api-client' +import { FIXED_TRASH_ID, FLEX_ROBOT_TYPE } from '@opentrons/shared-data' + +import { getTopPortalEl } from '/app/App/portal' +// import { useTrackEvent } from '/app/redux/analytics' +import { IntroScreen } from './IntroScreen' +import { ExitConfirmation } from './ExitConfirmation' +import { CheckItem } from './CheckItem' +import { WizardHeader } from '/app/molecules/WizardHeader' +import { getIsOnDevice } from '/app/redux/config' +import { AttachProbe } from './AttachProbe' +import { DetachProbe } from './DetachProbe' +import { PickUpTip } from './PickUpTip' +import { ReturnTip } from './ReturnTip' +import { ResultsSummary } from './ResultsSummary' +import { FatalError } from './FatalErrorModal' +import { RobotMotionLoader } from './RobotMotionLoader' +import { + useChainMaintenanceCommands, + useNotifyCurrentMaintenanceRun, +} from '/app/resources/maintenance_runs' +import { getLabwarePositionCheckSteps } from './getLabwarePositionCheckSteps' + +import type { + CompletedProtocolAnalysis, + Coordinates, + CreateCommand, + DropTipCreateCommand, + RobotType, +} from '@opentrons/shared-data' +import type { + LabwareOffsetCreateData, + LabwareOffset, + CommandData, +} from '@opentrons/api-client' +import type { Axis, Sign, StepSize } from '/app/molecules/JogControls/types' +import type { RegisterPositionAction, WorkingOffset } from './types' + +const RUN_REFETCH_INTERVAL = 5000 +const JOG_COMMAND_TIMEOUT = 10000 // 10 seconds +interface LabwarePositionCheckModalProps { + runId: string + maintenanceRunId: string + robotType: RobotType + mostRecentAnalysis: CompletedProtocolAnalysis | null + existingOffsets: LabwareOffset[] + onCloseClick: () => unknown + protocolName: string + setMaintenanceRunId: (id: string | null) => void + isDeletingMaintenanceRun: boolean + caughtError?: Error +} + +export const LabwarePositionCheckComponent = ( + props: LabwarePositionCheckModalProps +): JSX.Element | null => { + const { + mostRecentAnalysis, + existingOffsets, + robotType, + runId, + maintenanceRunId, + onCloseClick, + setMaintenanceRunId, + protocolName, + isDeletingMaintenanceRun, + } = props + const { t } = useTranslation(['labware_position_check', 'shared']) + const isOnDevice = useSelector(getIsOnDevice) + const protocolData = mostRecentAnalysis + const shouldUseMetalProbe = robotType === FLEX_ROBOT_TYPE + + // we should start checking for run deletion only after the maintenance run is created + // and the useCurrentRun poll has returned that created id + const [ + monitorMaintenanceRunForDeletion, + setMonitorMaintenanceRunForDeletion, + ] = useState(false) + + const { data: maintenanceRunData } = useNotifyCurrentMaintenanceRun({ + refetchInterval: RUN_REFETCH_INTERVAL, + enabled: maintenanceRunId != null, + }) + + // this will close the modal in case the run was deleted by the terminate + // activity modal on the ODD + useEffect(() => { + if ( + maintenanceRunId !== null && + maintenanceRunData?.data.id === maintenanceRunId + ) { + setMonitorMaintenanceRunForDeletion(true) + } + if ( + maintenanceRunData?.data.id !== maintenanceRunId && + monitorMaintenanceRunForDeletion + ) { + setMaintenanceRunId(null) + } + }, [ + maintenanceRunData?.data.id, + maintenanceRunId, + monitorMaintenanceRunForDeletion, + setMaintenanceRunId, + ]) + + const [fatalError, setFatalError] = useState(null) + const [isApplyingOffsets, setIsApplyingOffsets] = useState(false) + const [{ workingOffsets, tipPickUpOffset }, registerPosition] = useReducer( + ( + state: { + workingOffsets: WorkingOffset[] + tipPickUpOffset: Coordinates | null + }, + action: RegisterPositionAction + ) => { + if (action.type === 'tipPickUpOffset') { + return { ...state, tipPickUpOffset: action.offset } + } + + if ( + action.type === 'initialPosition' || + action.type === 'finalPosition' + ) { + const { labwareId, location, position } = action + const existingRecordIndex = state.workingOffsets.findIndex( + record => + record.labwareId === labwareId && isEqual(record.location, location) + ) + if (existingRecordIndex >= 0) { + if (action.type === 'initialPosition') { + return { + ...state, + workingOffsets: [ + ...state.workingOffsets.slice(0, existingRecordIndex), + { + ...state.workingOffsets[existingRecordIndex], + initialPosition: position, + finalPosition: null, + }, + ...state.workingOffsets.slice(existingRecordIndex + 1), + ], + } + } else if (action.type === 'finalPosition') { + return { + ...state, + workingOffsets: [ + ...state.workingOffsets.slice(0, existingRecordIndex), + { + ...state.workingOffsets[existingRecordIndex], + finalPosition: position, + }, + ...state.workingOffsets.slice(existingRecordIndex + 1), + ], + } + } + } + return { + ...state, + workingOffsets: [ + ...state.workingOffsets, + { + labwareId, + location, + initialPosition: + action.type === 'initialPosition' ? position : null, + finalPosition: action.type === 'finalPosition' ? position : null, + }, + ], + } + } else { + return state + } + }, + { workingOffsets: [], tipPickUpOffset: null } + ) + const [isExiting, setIsExiting] = useState(false) + const { + createMaintenanceCommand: createSilentCommand, + } = useCreateMaintenanceCommandMutation() + const { + chainRunCommands, + isCommandMutationLoading: isCommandChainLoading, + } = useChainMaintenanceCommands() + + const { createLabwareOffset } = useCreateLabwareOffsetMutation() + const [currentStepIndex, setCurrentStepIndex] = useState(0) + const handleCleanUpAndClose = (): void => { + setIsExiting(true) + const dropTipToBeSafeCommands: DropTipCreateCommand[] = shouldUseMetalProbe + ? [] + : (protocolData?.pipettes ?? []).map(pip => ({ + commandType: 'dropTip' as const, + params: { + pipetteId: pip.id, + labwareId: FIXED_TRASH_ID, + wellName: 'A1', + wellLocation: { origin: 'default' as const }, + }, + })) + chainRunCommands( + maintenanceRunId, + [ + { + commandType: 'retractAxis' as const, + params: { + axis: 'leftZ', + }, + }, + { + commandType: 'retractAxis' as const, + params: { + axis: 'rightZ', + }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'x' }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'y' }, + }, + ...dropTipToBeSafeCommands, + { commandType: 'home' as const, params: {} }, + ], + true + ) + .then(() => props.onCloseClick()) + .catch(() => props.onCloseClick()) + } + const { + confirm: confirmExitLPC, + showConfirmation, + cancel: cancelExitLPC, + } = useConditionalConfirm(handleCleanUpAndClose, true) + + const proceed = (): void => { + setCurrentStepIndex( + currentStepIndex !== LPCSteps.length - 1 + ? currentStepIndex + 1 + : currentStepIndex + ) + } + if (protocolData == null) return null + const LPCSteps = getLabwarePositionCheckSteps( + protocolData, + shouldUseMetalProbe + ) + const totalStepCount = LPCSteps.length - 1 + const currentStep = LPCSteps?.[currentStepIndex] + if (currentStep == null) return null + + const protocolHasModules = protocolData.modules.length > 0 + + const handleJog = ( + axis: Axis, + dir: Sign, + step: StepSize, + onSuccess?: (position: Coordinates | null) => void + ): void => { + const pipetteId = 'pipetteId' in currentStep ? currentStep.pipetteId : null + if (pipetteId != null) { + createSilentCommand({ + maintenanceRunId, + command: { + commandType: 'moveRelative', + params: { pipetteId, distance: step * dir, axis }, + }, + waitUntilComplete: true, + timeout: JOG_COMMAND_TIMEOUT, + }) + .then(data => { + onSuccess?.( + (data?.data?.result?.position ?? null) as Coordinates | null + ) + }) + .catch((e: Error) => { + setFatalError(`error issuing jog command: ${e.message}`) + }) + } else { + setFatalError(`could not find pipette to jog with id: ${pipetteId ?? ''}`) + } + } + const chainMaintenanceRunCommands = ( + commands: CreateCommand[], + continuePastCommandFailure: boolean + ): Promise => + chainRunCommands(maintenanceRunId, commands, continuePastCommandFailure) + const movementStepProps = { + proceed, + protocolData, + chainRunCommands: chainMaintenanceRunCommands, + setFatalError, + registerPosition, + handleJog, + isRobotMoving: isCommandChainLoading, + workingOffsets, + existingOffsets, + robotType, + } + + const handleApplyOffsets = (offsets: LabwareOffsetCreateData[]): void => { + setIsApplyingOffsets(true) + Promise.all(offsets.map(data => createLabwareOffset({ runId, data }))) + .then(() => { + onCloseClick() + setIsApplyingOffsets(false) + }) + .catch((e: Error) => { + setFatalError(`error applying labware offsets: ${e.message}`) + setIsApplyingOffsets(false) + }) + } + + let modalContent: JSX.Element =
UNASSIGNED STEP
+ if (isExiting) { + modalContent = ( + + ) + } else if (fatalError != null) { + modalContent = ( + + ) + } else if (showConfirmation) { + modalContent = ( + + ) + } else if (currentStep.section === 'BEFORE_BEGINNING') { + modalContent = ( + + ) + } else if ( + currentStep.section === 'CHECK_POSITIONS' || + currentStep.section === 'CHECK_TIP_RACKS' || + currentStep.section === 'CHECK_LABWARE' + ) { + modalContent = ( + + ) + } else if (currentStep.section === 'ATTACH_PROBE') { + modalContent = ( + + ) + } else if (currentStep.section === 'DETACH_PROBE') { + modalContent = + } else if (currentStep.section === 'PICK_UP_TIP') { + modalContent = ( + + ) + } else if (currentStep.section === 'RETURN_TIP') { + modalContent = ( + + ) + } else if (currentStep.section === 'RESULTS_SUMMARY') { + modalContent = ( + + ) + } + const wizardHeader = ( + + ) + return createPortal( + isOnDevice ? ( + + {wizardHeader} + {modalContent} + + ) : ( + + {modalContent} + + ), + getTopPortalEl() + ) +} diff --git a/app/src/organisms/LabwarePositionCheck/LiveOffsetValue.tsx b/app/src/organisms/LabwarePositionCheck/LiveOffsetValue.tsx new file mode 100644 index 00000000000..21b3af917f9 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/LiveOffsetValue.tsx @@ -0,0 +1,73 @@ +import { Fragment } from 'react' +import { + ALIGN_CENTER, + BORDERS, + COLORS, + DIRECTION_COLUMN, + Flex, + Icon, + SIZE_1, + SPACING, + LegacyStyledText, + TYPOGRAPHY, +} from '@opentrons/components' + +import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' +import { getIsOnDevice } from '/app/redux/config' + +import type { StyleProps } from '@opentrons/components' + +interface OffsetVectorProps extends StyleProps { + x: number + y: number + z: number +} + +export function LiveOffsetValue(props: OffsetVectorProps): JSX.Element { + const { x, y, z, ...styleProps } = props + const axisLabels = ['X', 'Y', 'Z'] + const { i18n, t } = useTranslation('labware_position_check') + const isOnDevice = useSelector(getIsOnDevice) + + return ( + + + {i18n.format(t('labware_offset_data'), 'capitalize')} + + + + {[x, y, z].map((axis, index) => ( + + + {axisLabels[index]} + + {axis.toFixed(1)} + + ))} + + + ) +} diff --git a/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx b/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx new file mode 100644 index 00000000000..de76e855097 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx @@ -0,0 +1,460 @@ +import { useState } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import isEqual from 'lodash/isEqual' +import { + DIRECTION_COLUMN, + Flex, + LegacyStyledText, + TYPOGRAPHY, +} from '@opentrons/components' +import { + getLabwareDefURI, + getLabwareDisplayName, + getModuleType, + getVectorDifference, + HEATERSHAKER_MODULE_TYPE, + IDENTITY_VECTOR, +} from '@opentrons/shared-data' +import { RobotMotionLoader } from './RobotMotionLoader' +import { PrepareSpace } from './PrepareSpace' +import { JogToWell } from './JogToWell' +import { UnorderedList } from '/app/molecules/UnorderedList' +import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' +import { TipConfirmation } from './TipConfirmation' +import { getLabwareDef } from './utils/labware' +import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' +import { getDisplayLocation } from './utils/getDisplayLocation' +import { useSelector } from 'react-redux' +import { getIsOnDevice } from '/app/redux/config' + +import type { Dispatch } from 'react' +import type { + CompletedProtocolAnalysis, + CreateCommand, + MoveLabwareCreateCommand, + RobotType, +} from '@opentrons/shared-data' +import type { useChainRunCommands } from '/app/resources/runs' +import type { Jog } from '/app/molecules/JogControls/types' +import type { + PickUpTipStep, + RegisterPositionAction, + WorkingOffset, +} from './types' +import type { LabwareOffset } from '@opentrons/api-client' +import type { TFunction } from 'i18next' + +interface PickUpTipProps extends PickUpTipStep { + protocolData: CompletedProtocolAnalysis + proceed: () => void + registerPosition: Dispatch + chainRunCommands: ReturnType['chainRunCommands'] + setFatalError: (errorMessage: string) => void + workingOffsets: WorkingOffset[] + existingOffsets: LabwareOffset[] + handleJog: Jog + isRobotMoving: boolean + robotType: RobotType + protocolHasModules: boolean + currentStepIndex: number +} +export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { + const { t, i18n } = useTranslation(['labware_position_check', 'shared']) + const { + labwareId, + pipetteId, + location, + protocolData, + proceed, + chainRunCommands, + registerPosition, + handleJog, + isRobotMoving, + existingOffsets, + workingOffsets, + setFatalError, + adapterId, + robotType, + protocolHasModules, + currentStepIndex, + } = props + const [showTipConfirmation, setShowTipConfirmation] = useState(false) + const isOnDevice = useSelector(getIsOnDevice) + const labwareDef = getLabwareDef(labwareId, protocolData) + const pipette = protocolData.pipettes.find(p => p.id === pipetteId) + const pipetteName = pipette?.pipetteName + const pipetteMount = pipette?.mount + if (pipetteName == null || labwareDef == null || pipetteMount == null) + return null + const pipetteZMotorAxis: 'leftZ' | 'rightZ' = + pipetteMount === 'left' ? 'leftZ' : 'rightZ' + + const displayLocation = getDisplayLocation( + location, + getLabwareDefinitionsFromCommands(protocolData.commands), + t as TFunction, + i18n + ) + const labwareDisplayName = getLabwareDisplayName(labwareDef) + const instructions = [ + ...(protocolHasModules && currentStepIndex === 1 + ? [t('place_modules')] + : []), + isOnDevice ? t('clear_all_slots_odd') : t('clear_all_slots'), + + ), + }} + />, + ] + + const initialPosition = workingOffsets.find( + o => + o.labwareId === labwareId && + isEqual(o.location, location) && + o.initialPosition != null + )?.initialPosition + + let moveLabware: MoveLabwareCreateCommand[] + if (adapterId != null) { + moveLabware = [ + { + commandType: 'moveLabware' as const, + params: { + labwareId: adapterId, + newLocation: { slotName: location.slotName }, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation: + adapterId != null + ? { labwareId: adapterId } + : { slotName: location.slotName }, + strategy: 'manualMoveWithoutPause', + }, + }, + ] + } else { + moveLabware = [ + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation: location, + strategy: 'manualMoveWithoutPause', + }, + }, + ] + } + + const handleConfirmPlacement = (): void => { + const modulePrepCommands = protocolData.modules.reduce( + (acc, module) => { + if (getModuleType(module.model) === HEATERSHAKER_MODULE_TYPE) { + return [ + ...acc, + { + commandType: 'heaterShaker/closeLabwareLatch', + params: { moduleId: module.id }, + }, + ] + } + return acc + }, + [] + ) + chainRunCommands( + [ + ...modulePrepCommands, + ...moveLabware, + { + commandType: 'moveToWell' as const, + params: { + pipetteId, + labwareId, + wellName: 'A1', + wellLocation: { origin: 'top' as const }, + }, + }, + { commandType: 'savePosition', params: { pipetteId } }, + ], + false + ) + .then(responses => { + const finalResponse = responses[responses.length - 1] + if (finalResponse.data.commandType === 'savePosition') { + const { position } = finalResponse.data?.result ?? { position: null } + registerPosition({ + type: 'initialPosition', + labwareId, + location, + position, + }) + } else { + setFatalError( + `PickUpTip failed to save position for initial placement.` + ) + } + }) + .catch((e: Error) => { + setFatalError( + `PickUpTip failed to save position for initial placement with message: ${e.message}` + ) + }) + } + const handleConfirmPosition = (): void => { + chainRunCommands( + [{ commandType: 'savePosition', params: { pipetteId } }], + false + ) + .then(responses => { + if (responses[0].data.commandType === 'savePosition') { + const { position } = responses[0].data?.result ?? { position: null } + const offset = + initialPosition != null && position != null + ? getVectorDifference(position, initialPosition) + : undefined + registerPosition({ + type: 'finalPosition', + labwareId, + location, + position, + }) + registerPosition({ type: 'tipPickUpOffset', offset: offset ?? null }) + chainRunCommands( + [ + { + commandType: 'pickUpTip', + params: { + pipetteId, + labwareId, + wellName: 'A1', + wellLocation: { origin: 'top', offset }, + }, + }, + ], + false + ) + .then(() => { + setShowTipConfirmation(true) + }) + .catch((e: Error) => { + setFatalError( + `PickUpTip failed to move from final position with message: ${e.message}` + ) + }) + } + }) + .catch((e: Error) => { + setFatalError( + `PickUpTip failed to save final position with message: ${e.message}` + ) + }) + } + + const moveLabwareOffDeck: MoveLabwareCreateCommand[] = + adapterId != null + ? [ + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveLabware' as const, + params: { + labwareId: adapterId, + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + ] + : [ + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + ] + + const handleConfirmTipAttached = (): void => { + chainRunCommands( + [ + { + commandType: 'retractAxis' as const, + params: { + axis: pipetteZMotorAxis, + }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'x' }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'y' }, + }, + ...moveLabwareOffDeck, + ], + false + ) + .then(() => { + proceed() + }) + .catch((e: Error) => { + setFatalError( + `PickUpTip failed to move to safe location after tip pick up with message: ${e.message}` + ) + }) + } + const handleInvalidateTip = (): void => { + chainRunCommands( + [ + { + commandType: 'dropTip', + params: { + pipetteId, + labwareId, + wellName: 'A1', + }, + }, + { + commandType: 'moveToWell' as const, + params: { + pipetteId, + labwareId, + wellName: 'A1', + wellLocation: { origin: 'top' as const }, + }, + }, + ], + false + ) + .then(() => { + registerPosition({ type: 'tipPickUpOffset', offset: null }) + registerPosition({ + type: 'finalPosition', + labwareId, + location, + position: null, + }) + setShowTipConfirmation(false) + }) + .catch((e: Error) => { + setFatalError(`PickUpTip failed to drop tip with message: ${e.message}`) + }) + } + const handleGoBack = (): void => { + chainRunCommands( + [ + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + ], + false + ) + .then(() => { + registerPosition({ + type: 'initialPosition', + labwareId, + location, + position: null, + }) + }) + .catch((e: Error) => { + setFatalError( + `PickUpTip failed to clear tip rack with message: ${e.message}` + ) + }) + } + + const existingOffset = + getCurrentOffsetForLabwareInLocation( + existingOffsets, + getLabwareDefURI(labwareDef), + location + )?.vector ?? IDENTITY_VECTOR + + if (isRobotMoving) + return ( + + ) + return showTipConfirmation ? ( + + ) : ( + + {initialPosition != null ? ( + , + bold: , + }} + values={{ + tip_type: t('pipette_nozzle'), + item_location: t('check_tip_location'), + }} + /> + } + labwareDef={labwareDef} + pipetteName={pipetteName} + handleConfirmPosition={handleConfirmPosition} + handleGoBack={handleGoBack} + handleJog={handleJog} + initialPosition={initialPosition} + existingOffset={existingOffset} + shouldUseMetalProbe={false} + /> + ) : ( + } + labwareDef={labwareDef} + confirmPlacement={handleConfirmPlacement} + robotType={robotType} + /> + )} + + ) +} diff --git a/app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx b/app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx new file mode 100644 index 00000000000..8820acfef33 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx @@ -0,0 +1,141 @@ +import type * as React from 'react' +import styled, { css } from 'styled-components' +import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' +import { + DIRECTION_COLUMN, + JUSTIFY_SPACE_BETWEEN, + RESPONSIVENESS, + SPACING, + Flex, + DIRECTION_ROW, + JUSTIFY_CENTER, + TYPOGRAPHY, + JUSTIFY_FLEX_END, + PrimaryButton, + BaseDeck, + ALIGN_FLEX_START, +} from '@opentrons/components' +import { THERMOCYCLER_MODULE_TYPE, getModuleType } from '@opentrons/shared-data' + +import { getIsOnDevice } from '/app/redux/config' +import { SmallButton } from '/app/atoms/buttons' +import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' +import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' + +import type { + CompletedProtocolAnalysis, + LabwareDefinition2, + RobotType, +} from '@opentrons/shared-data' +import type { CheckLabwareStep } from './types' + +const LPC_HELP_LINK_URL = + 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' + +const TILE_CONTAINER_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + justify-content: ${JUSTIFY_SPACE_BETWEEN}; + padding: ${SPACING.spacing32}; + height: 24.625rem; + flex: 1; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + height: 29.5rem; + } +` + +const Title = styled.h1` + ${TYPOGRAPHY.h1Default}; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + ${TYPOGRAPHY.level4HeaderSemiBold}; + } +` +interface PrepareSpaceProps extends Omit { + section: + | 'CHECK_LABWARE' + | 'CHECK_TIP_RACKS' + | 'PICK_UP_TIP' + | 'RETURN_TIP' + | 'CHECK_POSITIONS' + labwareDef: LabwareDefinition2 + protocolData: CompletedProtocolAnalysis + confirmPlacement: () => void + header: React.ReactNode + body: React.ReactNode + robotType: RobotType +} +export const PrepareSpace = (props: PrepareSpaceProps): JSX.Element | null => { + const { i18n, t } = useTranslation(['labware_position_check', 'shared']) + const { location, labwareDef, protocolData, header, body, robotType } = props + + const isOnDevice = useSelector(getIsOnDevice) + const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] + + if (protocolData == null || robotType == null) return null + + return ( + + + + {header} + {body} + + + ({ + moduleModel: mod.model, + moduleLocation: mod.location, + nestedLabwareDef: + 'moduleModel' in location && location.moduleModel != null + ? labwareDef + : null, + innerProps: + 'moduleModel' in location && + location.moduleModel != null && + getModuleType(location.moduleModel) === THERMOCYCLER_MODULE_TYPE + ? { lidMotorState: 'open' } + : {}, + }))} + labwareOnDeck={[ + { + labwareLocation: location, + definition: labwareDef, + }, + ].filter( + () => !('moduleModel' in location && location.moduleModel != null) + )} + deckConfig={deckConfig} + /> + + + {isOnDevice ? ( + + + + ) : ( + + + + {i18n.format(t('shared:confirm_placement'), 'capitalize')} + + + )} + + ) +} diff --git a/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx b/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx new file mode 100644 index 00000000000..eafda1a2c8a --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx @@ -0,0 +1,463 @@ +import { useMemo, Fragment } from 'react' +import styled, { css } from 'styled-components' +import { useSelector } from 'react-redux' +import isEqual from 'lodash/isEqual' +import { useTranslation } from 'react-i18next' +import { + getLabwareDefURI, + getLabwareDisplayName, + getModuleType, + getVectorDifference, + getVectorSum, + IDENTITY_VECTOR, +} from '@opentrons/shared-data' +import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' +import { + ALIGN_CENTER, + ALIGN_FLEX_END, + BORDERS, + COLORS, + DeckInfoLabel, + DIRECTION_COLUMN, + Flex, + Icon, + JUSTIFY_SPACE_BETWEEN, + MODULE_ICON_NAME_BY_TYPE, + OVERFLOW_AUTO, + PrimaryButton, + RESPONSIVENESS, + SPACING, + LegacyStyledText, + TYPOGRAPHY, + DIRECTION_ROW, +} from '@opentrons/components' +import { PythonLabwareOffsetSnippet } from '/app/molecules/PythonLabwareOffsetSnippet' +import { + getIsLabwareOffsetCodeSnippetsOn, + getIsOnDevice, +} from '/app/redux/config' +import { SmallButton } from '/app/atoms/buttons' +import { LabwareOffsetTabs } from '/app/organisms/LabwareOffsetTabs' +import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' +import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' +import { getDisplayLocation } from './utils/getDisplayLocation' + +import type { + CompletedProtocolAnalysis, + LabwareDefinition2, +} from '@opentrons/shared-data' +import type { + LabwareOffset, + LabwareOffsetCreateData, +} from '@opentrons/api-client' +import type { ResultsSummaryStep, WorkingOffset } from './types' +import type { TFunction } from 'i18next' + +const LPC_HELP_LINK_URL = + 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' + +interface ResultsSummaryProps extends ResultsSummaryStep { + protocolData: CompletedProtocolAnalysis + workingOffsets: WorkingOffset[] + existingOffsets: LabwareOffset[] + handleApplyOffsets: (offsets: LabwareOffsetCreateData[]) => void + isApplyingOffsets: boolean + isDeletingMaintenanceRun: boolean +} +export const ResultsSummary = ( + props: ResultsSummaryProps +): JSX.Element | null => { + const { i18n, t } = useTranslation('labware_position_check') + const { + protocolData, + workingOffsets, + handleApplyOffsets, + existingOffsets, + isApplyingOffsets, + isDeletingMaintenanceRun, + } = props + const labwareDefinitions = getLabwareDefinitionsFromCommands( + protocolData.commands + ) + const isSubmittingAndClosing = isApplyingOffsets || isDeletingMaintenanceRun + const isLabwareOffsetCodeSnippetsOn = useSelector( + getIsLabwareOffsetCodeSnippetsOn + ) + const isOnDevice = useSelector(getIsOnDevice) + + const offsetsToApply = useMemo(() => { + return workingOffsets.map( + ({ initialPosition, finalPosition, labwareId, location }) => { + const definitionUri = + protocolData.labware.find(l => l.id === labwareId)?.definitionUri ?? + null + if ( + finalPosition == null || + initialPosition == null || + definitionUri == null + ) { + throw new Error( + `cannot create offset for labware with id ${labwareId}, in location ${JSON.stringify( + location + )}, with initial position ${String( + initialPosition + )}, and final position ${String(finalPosition)}` + ) + } + + const existingOffset = + getCurrentOffsetForLabwareInLocation( + existingOffsets, + definitionUri, + location + )?.vector ?? IDENTITY_VECTOR + const vector = getVectorSum( + existingOffset, + getVectorDifference(finalPosition, initialPosition) + ) + return { definitionUri, location, vector } + } + ) + }, [workingOffsets]) + + const TableComponent = isOnDevice ? ( + + ) : ( + + ) + const JupyterSnippet = ( + + ) + const CommandLineSnippet = ( + + ) + + return ( + + +
{t('new_labware_offset_data')}
+ {isLabwareOffsetCodeSnippetsOn ? ( + + ) : ( + TableComponent + )} +
+ {isOnDevice ? ( + { + handleApplyOffsets(offsetsToApply) + }} + buttonText={i18n.format(t('apply_offsets'), 'capitalize')} + iconName={isSubmittingAndClosing ? 'ot-spinner' : null} + iconPlacement={isSubmittingAndClosing ? 'startIcon' : null} + disabled={isSubmittingAndClosing} + /> + ) : ( + + + { + handleApplyOffsets(offsetsToApply) + }} + disabled={isSubmittingAndClosing} + > + + {isSubmittingAndClosing ? ( + + ) : null} + + {i18n.format(t('apply_offsets'), 'capitalize')} + + + + + )} +
+ ) +} + +const Table = styled('table')` + ${TYPOGRAPHY.labelRegular} + table-layout: auto; + width: 100%; + border-spacing: 0 ${SPACING.spacing4}; + margin: ${SPACING.spacing16} 0; + text-align: left; +` +const TableHeader = styled('th')` + text-transform: ${TYPOGRAPHY.textTransformUppercase}; + color: ${COLORS.black90}; + font-weight: ${TYPOGRAPHY.fontWeightRegular}; + font-size: ${TYPOGRAPHY.fontSizeCaption}; + padding: ${SPACING.spacing4}; +` +const TableRow = styled('tr')` + background-color: ${COLORS.grey20}; +` + +const TableDatum = styled('td')` + padding: ${SPACING.spacing4}; + white-space: break-spaces; + text-overflow: wrap; +` + +const Header = styled.h1` + ${TYPOGRAPHY.h1Default} + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + ${TYPOGRAPHY.level4HeaderSemiBold} + } +` + +interface OffsetTableProps { + offsets: LabwareOffsetCreateData[] + labwareDefinitions: LabwareDefinition2[] +} + +const OffsetTable = (props: OffsetTableProps): JSX.Element => { + const { offsets, labwareDefinitions } = props + const { t, i18n } = useTranslation('labware_position_check') + return ( + + + + {t('location')} + {t('labware')} + {t('labware_offset_data')} + + + + + {offsets.map(({ location, definitionUri, vector }, index) => { + const labwareDef = labwareDefinitions.find( + def => getLabwareDefURI(def) === definitionUri + ) + const labwareDisplayName = + labwareDef != null ? getLabwareDisplayName(labwareDef) : '' + + return ( + + + + {getDisplayLocation( + location, + labwareDefinitions, + t as TFunction, + i18n + )} + + + + {labwareDisplayName} + + + {isEqual(vector, IDENTITY_VECTOR) ? ( + {t('no_labware_offsets')} + ) : ( + + {[vector.x, vector.y, vector.z].map((axis, index) => ( + + 0 ? SPACING.spacing8 : 0} + marginRight={SPACING.spacing4} + fontWeight={TYPOGRAPHY.fontWeightSemiBold} + > + {['X', 'Y', 'Z'][index]} + + + {axis.toFixed(1)} + + + ))} + + )} + + + ) + })} + +
+ ) +} + +// Very similar to the OffsetTable, but abbreviates certain things to be optimized +// for smaller screens +export const TerseOffsetTable = (props: OffsetTableProps): JSX.Element => { + const { offsets, labwareDefinitions } = props + const { i18n, t } = useTranslation('labware_position_check') + return ( + + + + + {i18n.format(t('slot_location'), 'capitalize')} + + {i18n.format(t('labware'), 'capitalize')} + {i18n.format(t('offsets'), 'capitalize')} + + + + + {offsets.map(({ location, definitionUri, vector }, index) => { + const labwareDef = labwareDefinitions.find( + def => getLabwareDefURI(def) === definitionUri + ) + const labwareDisplayName = + labwareDef != null ? getLabwareDisplayName(labwareDef) : '' + return ( + + + + + {location.moduleModel != null ? ( + + ) : null} + + + + + {labwareDisplayName} + + + + {isEqual(vector, IDENTITY_VECTOR) ? ( + {t('no_labware_offsets')} + ) : ( + + {[vector.x, vector.y, vector.z].map((axis, index) => ( + + 0 ? SPACING.spacing8 : 0} + marginRight={SPACING.spacing4} + fontWeight={TYPOGRAPHY.fontWeightSemiBold} + > + {['X', 'Y', 'Z'][index]} + + + {axis.toFixed(1)} + + + ))} + + )} + + + ) + })} + + + ) +} + +const TerseTable = styled('table')` + table-layout: auto; + width: 100%; + border-spacing: 0 ${SPACING.spacing4}; + margin: ${SPACING.spacing16} 0; + text-align: left; + tr td:first-child { + border-top-left-radius: ${BORDERS.borderRadius8}; + border-bottom-left-radius: ${BORDERS.borderRadius8}; + padding-left: ${SPACING.spacing12}; + } + tr td:last-child { + border-top-right-radius: ${BORDERS.borderRadius8}; + border-bottom-right-radius: ${BORDERS.borderRadius8}; + padding-right: ${SPACING.spacing12}; + } +` +const TerseHeader = styled('th')` + font-size: ${TYPOGRAPHY.fontSize20}; + line-height: ${TYPOGRAPHY.lineHeight24}; + font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; +` +const TerseTableRow = styled('tr')` + background-color: ${COLORS.grey35}; +` + +const TerseTableDatum = styled('td')` + padding: ${SPACING.spacing12} 0; + white-space: break-spaces; + text-overflow: wrap; +` diff --git a/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx b/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx new file mode 100644 index 00000000000..fce1f443829 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx @@ -0,0 +1,228 @@ +import { Trans, useTranslation } from 'react-i18next' +import { + DIRECTION_COLUMN, + Flex, + LegacyStyledText, + TYPOGRAPHY, +} from '@opentrons/components' + +import { + getLabwareDisplayName, + getModuleType, + HEATERSHAKER_MODULE_TYPE, +} from '@opentrons/shared-data' +import { UnorderedList } from '/app/molecules/UnorderedList' +import { getLabwareDef } from './utils/labware' +import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' +import { getDisplayLocation } from './utils/getDisplayLocation' +import { RobotMotionLoader } from './RobotMotionLoader' +import { PrepareSpace } from './PrepareSpace' +import { useSelector } from 'react-redux' +import { getIsOnDevice } from '/app/redux/config' + +import type { + CompletedProtocolAnalysis, + CreateCommand, + RobotType, + MoveLabwareCreateCommand, +} from '@opentrons/shared-data' +import type { VectorOffset } from '@opentrons/api-client' +import type { useChainRunCommands } from '/app/resources/runs' +import type { ReturnTipStep } from './types' +import type { TFunction } from 'i18next' + +interface ReturnTipProps extends ReturnTipStep { + protocolData: CompletedProtocolAnalysis + proceed: () => void + chainRunCommands: ReturnType['chainRunCommands'] + setFatalError: (errorMessage: string) => void + tipPickUpOffset: VectorOffset | null + isRobotMoving: boolean + robotType: RobotType +} +export const ReturnTip = (props: ReturnTipProps): JSX.Element | null => { + const { t, i18n } = useTranslation(['labware_position_check', 'shared']) + const { + pipetteId, + labwareId, + location, + protocolData, + proceed, + tipPickUpOffset, + isRobotMoving, + chainRunCommands, + setFatalError, + adapterId, + } = props + + const isOnDevice = useSelector(getIsOnDevice) + + const labwareDef = getLabwareDef(labwareId, protocolData) + if (labwareDef == null) return null + + const displayLocation = getDisplayLocation( + location, + getLabwareDefinitionsFromCommands(protocolData.commands), + t as TFunction, + i18n + ) + const labwareDisplayName = getLabwareDisplayName(labwareDef) + + const instructions = [ + isOnDevice ? t('clear_all_slots_odd') : t('clear_all_slots'), + + ), + }} + />, + ] + + let moveLabware: MoveLabwareCreateCommand[] + if (adapterId != null) { + moveLabware = [ + { + commandType: 'moveLabware' as const, + params: { + labwareId: adapterId, + newLocation: { slotName: location.slotName }, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation: + adapterId != null + ? { labwareId: adapterId } + : { slotName: location.slotName }, + strategy: 'manualMoveWithoutPause', + }, + }, + ] + } else { + moveLabware = [ + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation: location, + strategy: 'manualMoveWithoutPause', + }, + }, + ] + } + + const moveLabwareOffDeck: MoveLabwareCreateCommand[] = + adapterId != null + ? [ + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveLabware' as const, + params: { + labwareId: adapterId, + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + ] + : [ + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + ] + + const handleConfirmPlacement = (): void => { + const modulePrepCommands = protocolData.modules.reduce( + (acc, module) => { + if (getModuleType(module.model) === HEATERSHAKER_MODULE_TYPE) { + return [ + ...acc, + { + commandType: 'heaterShaker/closeLabwareLatch', + params: { moduleId: module.id }, + }, + ] + } + return acc + }, + [] + ) + chainRunCommands( + [ + ...modulePrepCommands, + ...moveLabware, + { + commandType: 'moveToWell' as const, + params: { + pipetteId, + labwareId, + wellName: 'A1', + wellLocation: { + origin: 'top' as const, + offset: tipPickUpOffset ?? undefined, + }, + }, + }, + { + commandType: 'dropTip' as const, + params: { + pipetteId, + labwareId, + wellName: 'A1', + wellLocation: { + origin: 'default' as const, + offset: tipPickUpOffset ?? undefined, + }, + }, + }, + ...moveLabwareOffDeck, + { commandType: 'home' as const, params: {} }, + ], + false + ) + .then(() => { + proceed() + }) + .catch((e: Error) => { + setFatalError(`ReturnTip failed with message: ${e.message}`) + }) + } + + if (isRobotMoving) + return ( + + ) + return ( + + } + labwareDef={labwareDef} + confirmPlacement={handleConfirmPlacement} + /> + + ) +} diff --git a/app/src/organisms/LabwarePositionCheck/RobotMotionLoader.tsx b/app/src/organisms/LabwarePositionCheck/RobotMotionLoader.tsx new file mode 100644 index 00000000000..577dae6eff7 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/RobotMotionLoader.tsx @@ -0,0 +1,52 @@ +import styled from 'styled-components' +import { + ALIGN_CENTER, + COLORS, + DIRECTION_COLUMN, + Flex, + Icon, + JUSTIFY_CENTER, + RESPONSIVENESS, + SIZE_4, + SPACING, + LegacyStyledText, + TYPOGRAPHY, +} from '@opentrons/components' + +interface RobotMotionLoaderProps { + header?: string + body?: string +} + +export function RobotMotionLoader(props: RobotMotionLoaderProps): JSX.Element { + const { header, body } = props + return ( + + + {header != null ? {header} : null} + {body != null ? {body} : null} + + ) +} + +const LoadingText = styled.h1` + ${TYPOGRAPHY.h1Default} + + p { + text-transform: lowercase; + } + + p::first-letter { + text-transform: uppercase; + } + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + ${TYPOGRAPHY.level4HeaderSemiBold} + } +` diff --git a/app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx b/app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx new file mode 100644 index 00000000000..da7c52de513 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx @@ -0,0 +1,109 @@ +import type * as React from 'react' +import { + ALIGN_FLEX_END, + DIRECTION_COLUMN, + Flex, + JUSTIFY_SPACE_BETWEEN, + SPACING, + VIEWPORT, +} from '@opentrons/components' +import { + fixture12Trough, + fixtureTiprack10ul, + getLabwareDefURI, +} from '@opentrons/shared-data' + +import { SmallButton } from '/app/atoms/buttons' +import { TerseOffsetTable } from './ResultsSummary' + +import type { Story, Meta } from '@storybook/react' +import type { LabwareDefinition2 } from '@opentrons/shared-data' + +export default { + title: 'ODD/Organisms/TerseOffsetTable', + component: TerseOffsetTable, + parameters: VIEWPORT.touchScreenViewport, +} as Meta + +// Note: 59rem(944px) is the size of ODD +const Template: Story> = ({ + ...args +}) => ( + + + +

new labware offset data

+ +
+ { + console.log('FAKE BUTTON') + }} + buttonText="Apply offsets" + /> +
+
+) + +export const Basic = Template.bind({}) +Basic.args = { + offsets: [ + { + definitionUri: getLabwareDefURI(fixture12Trough as LabwareDefinition2), + location: { slotName: 'A1' }, + vector: { x: 1, y: 2, z: 3 }, + }, + { + definitionUri: getLabwareDefURI(fixture12Trough as LabwareDefinition2), + location: { slotName: 'A2' }, + vector: { x: 1, y: 2, z: 3 }, + }, + { + definitionUri: getLabwareDefURI(fixture12Trough as LabwareDefinition2), + location: { slotName: 'A3' }, + vector: { x: 1, y: 2, z: 3 }, + }, + { + definitionUri: getLabwareDefURI(fixture12Trough as LabwareDefinition2), + location: { slotName: 'B1' }, + vector: { x: 1, y: 2, z: 3 }, + }, + { + definitionUri: getLabwareDefURI(fixture12Trough as LabwareDefinition2), + location: { slotName: 'B2' }, + vector: { x: 1, y: 2, z: 3 }, + }, + { + definitionUri: getLabwareDefURI(fixture12Trough as LabwareDefinition2), + location: { slotName: 'B3' }, + vector: { x: 1, y: 2, z: 3 }, + }, + { + definitionUri: getLabwareDefURI(fixture12Trough as LabwareDefinition2), + location: { slotName: 'C1' }, + vector: { x: 1, y: 2, z: 3 }, + }, + { + definitionUri: getLabwareDefURI(fixture12Trough as LabwareDefinition2), + location: { slotName: 'C2' }, + vector: { x: 1, y: 2, z: 3 }, + }, + { + definitionUri: getLabwareDefURI(fixture12Trough as LabwareDefinition2), + location: { slotName: 'C3' }, + vector: { x: 1, y: 2, z: 3 }, + }, + ], + labwareDefinitions: [fixture12Trough, fixtureTiprack10ul], +} diff --git a/app/src/organisms/LabwarePositionCheck/TipConfirmation.tsx b/app/src/organisms/LabwarePositionCheck/TipConfirmation.tsx new file mode 100644 index 00000000000..ec8c87daea4 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/TipConfirmation.tsx @@ -0,0 +1,84 @@ +import { + ALIGN_CENTER, + COLORS, + DIRECTION_COLUMN, + Flex, + JUSTIFY_FLEX_END, + JUSTIFY_SPACE_BETWEEN, + PrimaryButton, + SecondaryButton, + SPACING, + LegacyStyledText, +} from '@opentrons/components' +import { useTranslation } from 'react-i18next' + +import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' +import { useSelector } from 'react-redux' +import { getIsOnDevice } from '/app/redux/config' +import { SimpleWizardBody } from '/app/molecules/SimpleWizardBody' +import { SmallButton } from '/app/atoms/buttons' +import { i18n } from '/app/i18n' + +const LPC_HELP_LINK_URL = + 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' + +interface TipConfirmationProps { + invalidateTip: () => void + confirmTip: () => void +} + +export function TipConfirmation(props: TipConfirmationProps): JSX.Element { + const { invalidateTip, confirmTip } = props + const { t } = useTranslation('shared') + const isOnDevice = useSelector(getIsOnDevice) + return isOnDevice ? ( + + + + + + + ) : ( + + + {t('did_pipette_pick_up_tip')} + + + + + + {i18n.format(t('try_again'), 'capitalize')} + + + {i18n.format(t('yes'), 'capitalize')} + + + + + ) +} diff --git a/app/src/organisms/LabwarePositionCheck/TwoUpTileLayout.tsx b/app/src/organisms/LabwarePositionCheck/TwoUpTileLayout.tsx new file mode 100644 index 00000000000..7c6cd309bb4 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/TwoUpTileLayout.tsx @@ -0,0 +1,66 @@ +import type * as React from 'react' +import styled, { css } from 'styled-components' +import { + DIRECTION_COLUMN, + Flex, + SPACING, + JUSTIFY_SPACE_BETWEEN, + DIRECTION_ROW, + TYPOGRAPHY, + JUSTIFY_CENTER, + RESPONSIVENESS, + DISPLAY_INLINE_BLOCK, +} from '@opentrons/components' + +const Title = styled.h1` + ${TYPOGRAPHY.h1Default}; + margin-bottom: ${SPACING.spacing8}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + ${TYPOGRAPHY.level4HeaderSemiBold}; + margin-bottom: 0; + height: ${SPACING.spacing40}; + display: ${DISPLAY_INLINE_BLOCK}; + } +` + +const TILE_CONTAINER_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + justify-content: ${JUSTIFY_SPACE_BETWEEN}; + padding: ${SPACING.spacing32}; + height: 24.625rem; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + height: 29.5rem; + } +` +export interface TwoUpTileLayoutProps { + /** main header text on left half */ + title: string + /** paragraph text below title on left half */ + body: React.ReactNode + /** entire contents of the right half */ + rightElement: React.ReactNode + /** footer underneath both halves of content */ + footer: React.ReactNode +} + +export function TwoUpTileLayout(props: TwoUpTileLayoutProps): JSX.Element { + const { title, body, rightElement, footer } = props + return ( + + + + {title} + {body} + + + {rightElement} + + + {footer} + + ) +} diff --git a/app/src/organisms/LabwarePositionCheck/__fixtures__/index.ts b/app/src/organisms/LabwarePositionCheck/__fixtures__/index.ts new file mode 100644 index 00000000000..493230e2035 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/__fixtures__/index.ts @@ -0,0 +1,4 @@ +export * from './mockWorkingOffsets' +export * from './mockExistingOffsets' +export * from './mockTipRackDef' +export * from './mockCompletedAnalysis' diff --git a/app/src/organisms/LabwarePositionCheck/__fixtures__/mockCompletedAnalysis.ts b/app/src/organisms/LabwarePositionCheck/__fixtures__/mockCompletedAnalysis.ts new file mode 100644 index 00000000000..9743796b945 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/__fixtures__/mockCompletedAnalysis.ts @@ -0,0 +1,79 @@ +import { getLabwareDefURI } from '@opentrons/shared-data' +import { mockTipRackDef } from './mockTipRackDef' +import { mockLabwareDef } from './mockLabwareDef' + +import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' + +export const mockCompletedAnalysis: CompletedProtocolAnalysis = { + id: 'fakeAnalysisId', + status: 'completed', + result: 'ok', + errors: [], + labware: [ + { + id: 'labwareId1', + loadName: 'fakeLoadName', + definitionUri: getLabwareDefURI(mockTipRackDef), + location: { slotName: '1' }, + }, + { + id: 'labwareId2', + loadName: 'fakeSecondLoadName', + definitionUri: getLabwareDefURI(mockLabwareDef), + location: { slotName: '2' }, + }, + ], + pipettes: [ + { + id: 'pipetteId1', + pipetteName: 'p10_single', + mount: 'left', + }, + ], + modules: [], + liquids: [], + commands: [ + { + commandType: 'loadLabware', + id: 'fakeCommandId', + status: 'succeeded', + createdAt: 'fakeCreatedAtTimestamp', + startedAt: 'fakeStartedAtTimestamp', + completedAt: 'fakecompletedAtTimestamp', + error: null, + params: { + labwareId: 'labwareId1', + location: { slotName: '1' }, + version: 1, + loadName: 'mockLoadname', + namespace: 'mockNamespace', + }, + result: { + labwareId: 'labwareId1', + definition: mockTipRackDef, + offset: { x: 0, y: 0, z: 0 }, + }, + }, + { + commandType: 'loadLabware', + id: 'fakeSecondCommandId', + status: 'succeeded', + createdAt: 'fakeCreatedAtTimestamp', + startedAt: 'fakeStartedAtTimestamp', + completedAt: 'fakecompletedAtTimestamp', + error: null, + params: { + labwareId: 'labwareId2', + location: { slotName: '2' }, + version: 1, + loadName: 'mockLoadname', + namespace: 'mockNamespace', + }, + result: { + labwareId: 'labwareId2', + definition: mockLabwareDef, + offset: { x: 0, y: 0, z: 0 }, + }, + }, + ], +} diff --git a/app/src/organisms/LabwarePositionCheck/__fixtures__/mockExistingOffsets.ts b/app/src/organisms/LabwarePositionCheck/__fixtures__/mockExistingOffsets.ts new file mode 100644 index 00000000000..63296abfab4 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/__fixtures__/mockExistingOffsets.ts @@ -0,0 +1,18 @@ +import { getLabwareDefURI } from '@opentrons/shared-data' +import { mockTipRackDef } from './mockTipRackDef' + +export const mockExistingOffset = { + id: 'offset1', + createdAt: 'fake_timestamp', + definitionUri: getLabwareDefURI(mockTipRackDef), + location: { slotName: '2' }, + vector: { x: 1, y: 2, z: 3 }, +} +export const mockOtherExistingOffset = { + id: 'offset2', + createdAt: 'fake_timestamp', + definitionUri: getLabwareDefURI(mockTipRackDef), + location: { slotName: '4' }, + vector: { x: 4, y: 5, z: 6 }, +} +export const mockExistingOffsets = [mockExistingOffset, mockOtherExistingOffset] diff --git a/app/src/organisms/LabwarePositionCheck/__fixtures__/mockLabwareDef.ts b/app/src/organisms/LabwarePositionCheck/__fixtures__/mockLabwareDef.ts new file mode 100644 index 00000000000..450d7754a98 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/__fixtures__/mockLabwareDef.ts @@ -0,0 +1,11 @@ +import { fixture96Plate } from '@opentrons/shared-data' +import type { LabwareDefinition2 } from '@opentrons/shared-data' + +export const mockLabwareDef: LabwareDefinition2 = { + ...(fixture96Plate as LabwareDefinition2), + metadata: { + displayName: 'Mock Labware Definition', + displayCategory: 'wellPlate', + displayVolumeUnits: 'mL', + }, +} diff --git a/app/src/organisms/LabwarePositionCheck/__fixtures__/mockTipRackDef.ts b/app/src/organisms/LabwarePositionCheck/__fixtures__/mockTipRackDef.ts new file mode 100644 index 00000000000..0c7288b338a --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/__fixtures__/mockTipRackDef.ts @@ -0,0 +1,11 @@ +import { fixtureTiprack10ul } from '@opentrons/shared-data' +import type { LabwareDefinition2 } from '@opentrons/shared-data' + +export const mockTipRackDef: LabwareDefinition2 = { + ...(fixtureTiprack10ul as LabwareDefinition2), + metadata: { + displayName: 'Mock TipRack Definition', + displayCategory: 'tipRack', + displayVolumeUnits: 'mL', + }, +} diff --git a/app/src/organisms/LabwarePositionCheck/__fixtures__/mockWorkingOffsets.ts b/app/src/organisms/LabwarePositionCheck/__fixtures__/mockWorkingOffsets.ts new file mode 100644 index 00000000000..e4cde4850c7 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/__fixtures__/mockWorkingOffsets.ts @@ -0,0 +1,13 @@ +export const mockWorkingOffset = { + labwareId: 'labwareId1', + location: { slotName: '1' }, + initialPosition: { x: 1, y: 2, z: 3 }, + finalPosition: { x: 2, y: 3, z: 4 }, +} +export const mockOtherWorkingOffset = { + labwareId: 'labwareId1', + location: { slotName: '3' }, + initialPosition: { x: 3, y: 4, z: 5 }, + finalPosition: { x: 6, y: 7, z: 8 }, +} +export const mockWorkingOffsets = [mockWorkingOffset, mockOtherWorkingOffset] diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/CheckItem.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/CheckItem.test.tsx new file mode 100644 index 00000000000..17442dfc42b --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/__tests__/CheckItem.test.tsx @@ -0,0 +1,702 @@ +import type * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, vi, beforeEach, afterEach, expect } from 'vitest' + +import { + FLEX_ROBOT_TYPE, + HEATERSHAKER_MODULE_V1, + OT2_ROBOT_TYPE, + THERMOCYCLER_MODULE_V2, +} from '@opentrons/shared-data' + +import { nestedTextMatcher, renderWithProviders } from '/app/__testing-utils__' +import { i18n } from '/app/i18n' +import { CheckItem } from '../CheckItem' +import { SECTIONS } from '../constants' +import { mockCompletedAnalysis, mockExistingOffsets } from '../__fixtures__' + +import type { Mock } from 'vitest' + +vi.mock('/app/redux/config') +vi.mock('../../Desktop/Devices/hooks') + +const mockStartPosition = { x: 10, y: 20, z: 30 } +const mockEndPosition = { x: 9, y: 19, z: 29 } + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('CheckItem', () => { + let props: React.ComponentProps + let mockChainRunCommands: Mock + + beforeEach(() => { + mockChainRunCommands = vi.fn().mockImplementation(() => Promise.resolve([])) + props = { + section: SECTIONS.CHECK_LABWARE, + pipetteId: mockCompletedAnalysis.pipettes[0].id, + labwareId: mockCompletedAnalysis.labware[0].id, + definitionUri: mockCompletedAnalysis.labware[0].definitionUri, + location: { slotName: 'D1' }, + protocolData: mockCompletedAnalysis, + proceed: vi.fn(), + chainRunCommands: mockChainRunCommands, + handleJog: vi.fn(), + registerPosition: vi.fn(), + setFatalError: vi.fn(), + workingOffsets: [], + existingOffsets: mockExistingOffsets, + isRobotMoving: false, + robotType: FLEX_ROBOT_TYPE, + shouldUseMetalProbe: false, + } + }) + afterEach(() => { + vi.resetAllMocks() + }) + it('renders correct copy when preparing space with tip rack', () => { + render(props) + screen.getByRole('heading', { name: 'Prepare tip rack in Slot D1' }) + screen.getByText( + 'Clear all deck slots of labware, leaving modules in place' + ) + screen.getAllByText(/Place/i) + screen.getAllByText(/a full Mock TipRack Definition/i) + screen.getAllByText(/into/i) + screen.getAllByText(/Slot D1/i) + screen.getByRole('link', { name: 'Need help?' }) + screen.getByRole('button', { name: 'Confirm placement' }) + }) + it('renders correct copy when preparing space with non tip rack labware', () => { + props = { + ...props, + labwareId: mockCompletedAnalysis.labware[1].id, + location: { slotName: 'D2' }, + } + + render(props) + screen.getByRole('heading', { name: 'Prepare labware in Slot D2' }) + screen.getByText( + 'Clear all deck slots of labware, leaving modules in place' + ) + screen.getAllByText(/Place a/i) + screen.getAllByText(/Mock Labware Definition/i) + screen.getAllByText(/into/i) + screen.getAllByText(/Slot D2/i) + screen.getByRole('link', { name: 'Need help?' }) + screen.getByRole('button', { name: 'Confirm placement' }) + }) + it('executes correct chained commands when confirm placement CTA is clicked then go back', async () => { + vi.mocked(mockChainRunCommands).mockImplementation(() => + Promise.resolve([ + {}, + {}, + { + data: { + commandType: 'savePosition', + result: { position: mockStartPosition }, + }, + }, + ]) + ) + render(props) + fireEvent.click(screen.getByRole('button', { name: 'Confirm placement' })) + await new Promise((resolve, reject) => setTimeout(resolve)) + expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 1, + [ + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: { slotName: 'D1' }, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveToWell', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + wellLocation: { origin: 'top', offset: { x: 0, y: 0, z: 44.5 } }, + }, + }, + { + commandType: 'savePosition', + params: { pipetteId: 'pipetteId1' }, + }, + ], + false + ) + expect(props.registerPosition).toHaveBeenNthCalledWith(1, { + type: 'initialPosition', + labwareId: 'labwareId1', + location: { slotName: 'D1' }, + position: mockStartPosition, + }) + }) + it('executes correct chained commands when confirm placement CTA is clicked then go back on OT-2', async () => { + props = { + ...props, + robotType: OT2_ROBOT_TYPE, + location: { slotName: '1' }, + } + vi.mocked(mockChainRunCommands).mockImplementation(() => + Promise.resolve([ + {}, + {}, + { + data: { + commandType: 'savePosition', + result: { position: mockStartPosition }, + }, + }, + ]) + ) + render(props) + fireEvent.click(screen.getByRole('button', { name: 'Confirm placement' })) + await new Promise((resolve, reject) => setTimeout(resolve)) + + expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 1, + [ + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: { slotName: '1' }, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveToWell', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + wellLocation: { origin: 'top', offset: { x: 0, y: 0, z: 0 } }, + }, + }, + { + commandType: 'savePosition', + params: { pipetteId: 'pipetteId1' }, + }, + ], + false + ) + expect(props.registerPosition).toHaveBeenNthCalledWith(1, { + type: 'initialPosition', + labwareId: 'labwareId1', + location: { slotName: '1' }, + position: mockStartPosition, + }) + }) + + it('executes correct chained commands when confirm placement CTA is clicked then go back on Flex', async () => { + props = { ...props, robotType: FLEX_ROBOT_TYPE } + vi.mocked(mockChainRunCommands).mockImplementation(() => + Promise.resolve([ + {}, + {}, + { + data: { + commandType: 'savePosition', + result: { position: mockStartPosition }, + }, + }, + ]) + ) + render(props) + fireEvent.click(screen.getByRole('button', { name: 'Confirm placement' })) + await new Promise((resolve, reject) => setTimeout(resolve)) + + expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 1, + [ + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: { slotName: 'D1' }, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveToWell', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + wellLocation: { origin: 'top', offset: { x: 0, y: 0, z: 44.5 } }, + }, + }, + { + commandType: 'savePosition', + params: { pipetteId: 'pipetteId1' }, + }, + ], + false + ) + expect(props.registerPosition).toHaveBeenNthCalledWith(1, { + type: 'initialPosition', + labwareId: 'labwareId1', + location: { slotName: 'D1' }, + position: mockStartPosition, + }) + }) + + it('renders the correct copy for moving a labware onto an adapter', () => { + props = { + ...props, + labwareId: mockCompletedAnalysis.labware[1].id, + adapterId: 'labwareId2', + } + render(props) + screen.getByText('Prepare labware in Slot D1') + screen.getByText( + nestedTextMatcher( + 'Place a Mock Labware Definition followed by a Mock Labware Definition into Slot D1' + ) + ) + }) + it('executes correct chained commands when confirm placement CTA is clicked for when there is an adapter', async () => { + props = { + ...props, + adapterId: 'labwareId2', + } + vi.mocked(mockChainRunCommands).mockImplementation(() => + Promise.resolve([ + {}, + {}, + { + data: { + commandType: 'savePosition', + result: { position: mockStartPosition }, + }, + }, + ]) + ) + render(props) + fireEvent.click(screen.getByRole('button', { name: 'Confirm placement' })) + await new Promise((resolve, reject) => setTimeout(resolve)) + + expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 1, + [ + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId2', + newLocation: { slotName: 'D1' }, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: { labwareId: 'labwareId2' }, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveToWell', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + wellLocation: { origin: 'top', offset: { x: 0, y: 0, z: 44.5 } }, + }, + }, + { + commandType: 'savePosition', + params: { pipetteId: 'pipetteId1' }, + }, + ], + false + ) + expect(props.registerPosition).toHaveBeenNthCalledWith(1, { + type: 'initialPosition', + labwareId: 'labwareId1', + location: { slotName: 'D1' }, + position: mockStartPosition, + }) + }) + it('executes correct chained commands when go back clicked', async () => { + props = { + ...props, + workingOffsets: [ + { + location: { slotName: 'D1' }, + labwareId: 'labwareId1', + initialPosition: { x: 1, y: 2, z: 3 }, + finalPosition: null, + }, + ], + } + render(props) + fireEvent.click(screen.getByRole('button', { name: 'Go back' })) + await new Promise((resolve, reject) => setTimeout(resolve)) + + expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 1, + [ + { commandType: 'home', params: {} }, + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + ], + false + ) + expect(props.registerPosition).toHaveBeenNthCalledWith(1, { + type: 'initialPosition', + labwareId: 'labwareId1', + location: { slotName: 'D1' }, + position: null, + }) + }) + it('executes correct chained commands when confirm position clicked', async () => { + vi.mocked(mockChainRunCommands).mockImplementation(() => + Promise.resolve([ + { + data: { + commandType: 'savePosition', + result: { position: mockEndPosition }, + }, + }, + {}, + {}, + ]) + ) + props = { + ...props, + workingOffsets: [ + { + location: { slotName: 'D1' }, + labwareId: 'labwareId1', + initialPosition: { x: 1, y: 2, z: 3 }, + finalPosition: null, + }, + ], + } + render(props) + fireEvent.click(screen.getByRole('button', { name: 'Confirm position' })) + await new Promise((resolve, reject) => setTimeout(resolve)) + + expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 1, + [ + { + commandType: 'savePosition', + params: { pipetteId: 'pipetteId1' }, + }, + { + commandType: 'retractAxis' as const, + params: { + axis: 'leftZ', + }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'x' }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'y' }, + }, + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + ], + false + ) + expect(props.registerPosition).toHaveBeenNthCalledWith(1, { + type: 'finalPosition', + labwareId: 'labwareId1', + location: { slotName: 'D1' }, + position: mockEndPosition, + }) + }) + + it('executes heater shaker open latch command on component mount if step is on HS', async () => { + props = { ...props, robotType: FLEX_ROBOT_TYPE } + props = { + ...props, + location: { slotName: 'D1', moduleModel: HEATERSHAKER_MODULE_V1 }, + moduleId: 'firstHSId', + protocolData: { + ...props.protocolData, + modules: [ + { + id: 'firstHSId', + model: HEATERSHAKER_MODULE_V1, + location: { slotName: 'D3' }, + serialNumber: 'firstHSSerial', + }, + { + id: 'secondHSId', + model: HEATERSHAKER_MODULE_V1, + location: { slotName: 'A1' }, + serialNumber: 'secondHSSerial', + }, + ], + }, + } + render(props) + await new Promise((resolve, reject) => setTimeout(resolve)) + + expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 1, + [ + { + commandType: 'heaterShaker/closeLabwareLatch', + params: { moduleId: 'firstHSId' }, + }, + { + commandType: 'heaterShaker/deactivateShaker', + params: { moduleId: 'firstHSId' }, + }, + { + commandType: 'heaterShaker/openLabwareLatch', + params: { moduleId: 'firstHSId' }, + }, + ], + false + ) + fireEvent.click(screen.getByRole('button', { name: 'Confirm placement' })) + + expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 2, + [ + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: { moduleId: 'firstHSId' }, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'heaterShaker/closeLabwareLatch', + params: { moduleId: 'firstHSId' }, + }, + { + commandType: 'heaterShaker/closeLabwareLatch', + params: { moduleId: 'secondHSId' }, + }, + { + commandType: 'moveToWell', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + wellLocation: { origin: 'top', offset: { x: 0, y: 0, z: 44.5 } }, + }, + }, + { + commandType: 'savePosition', + params: { pipetteId: 'pipetteId1' }, + }, + ], + false + ) + }) + + it('executes correct chained commands when confirm position clicked with HS and adapter', async () => { + props = { + ...props, + location: { slotName: 'D1', moduleModel: HEATERSHAKER_MODULE_V1 }, + adapterId: 'adapterId', + moduleId: 'heaterShakerId', + protocolData: { + ...props.protocolData, + modules: [ + { + id: 'heaterShakerId', + model: HEATERSHAKER_MODULE_V1, + location: { slotName: 'D3' }, + serialNumber: 'firstHSSerial', + }, + ], + }, + workingOffsets: [ + { + location: { slotName: 'D1', moduleModel: HEATERSHAKER_MODULE_V1 }, + labwareId: 'labwareId1', + initialPosition: { x: 1, y: 2, z: 3 }, + finalPosition: null, + }, + ], + } + vi.mocked(mockChainRunCommands).mockImplementation(() => + Promise.resolve([ + { + data: { + commandType: 'savePosition', + result: { position: mockEndPosition }, + }, + }, + {}, + {}, + {}, + {}, + {}, + {}, + ]) + ) + + render(props) + fireEvent.click(screen.getByRole('button', { name: 'Confirm position' })) + await new Promise((resolve, reject) => setTimeout(resolve)) + + expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 1, + [ + { + commandType: 'savePosition', + params: { pipetteId: 'pipetteId1' }, + }, + { + commandType: 'retractAxis' as const, + params: { + axis: 'leftZ', + }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'x' }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'y' }, + }, + { + commandType: 'heaterShaker/openLabwareLatch', + params: { moduleId: 'heaterShakerId' }, + }, + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveLabware', + params: { + labwareId: 'adapterId', + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + ], + false + ) + expect(props.registerPosition).toHaveBeenNthCalledWith(1, { + type: 'finalPosition', + labwareId: 'labwareId1', + location: { slotName: 'D1', moduleModel: HEATERSHAKER_MODULE_V1 }, + position: mockEndPosition, + }) + }) + + it('executes thermocycler open lid command on mount if checking labware on thermocycler', () => { + props = { + ...props, + location: { slotName: 'B1', moduleModel: THERMOCYCLER_MODULE_V2 }, + moduleId: 'tcId', + protocolData: { + ...props.protocolData, + modules: [ + { + id: 'tcId', + model: THERMOCYCLER_MODULE_V2, + location: { slotName: 'B1' }, + serialNumber: 'tcSerial', + }, + ], + }, + } + render(props) + expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 1, + [ + { + commandType: 'thermocycler/openLid', + params: { moduleId: 'tcId' }, + }, + ], + false + ) + }) + it('executes correct chained commands when confirm placement CTA is clicked when using probe for LPC', async () => { + props = { + ...props, + robotType: FLEX_ROBOT_TYPE, + } + vi.mocked(mockChainRunCommands).mockImplementation(() => + Promise.resolve([ + {}, + {}, + { + data: { + commandType: 'savePosition', + result: { position: mockStartPosition }, + }, + }, + ]) + ) + render(props) + fireEvent.click(screen.getByRole('button', { name: 'Confirm placement' })) + await new Promise((resolve, reject) => setTimeout(resolve)) + + expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 1, + [ + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: { slotName: 'D1' }, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveToWell', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + wellLocation: { origin: 'top', offset: { x: 0, y: 0, z: 44.5 } }, + }, + }, + { + commandType: 'savePosition', + params: { pipetteId: 'pipetteId1' }, + }, + ], + false + ) + expect(props.registerPosition).toHaveBeenNthCalledWith(1, { + type: 'initialPosition', + labwareId: 'labwareId1', + location: { slotName: 'D1' }, + position: mockStartPosition, + }) + }) +}) diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/ExitConfirmation.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/ExitConfirmation.test.tsx new file mode 100644 index 00000000000..6a93da71dc5 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/__tests__/ExitConfirmation.test.tsx @@ -0,0 +1,55 @@ +import type * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest' +import { ExitConfirmation } from '../ExitConfirmation' +import { renderWithProviders } from '/app/__testing-utils__' +import { i18n } from '/app/i18n' + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('ExitConfirmation', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + onGoBack: vi.fn(), + onConfirmExit: vi.fn(), + shouldUseMetalProbe: false, + } + }) + afterEach(() => { + vi.restoreAllMocks() + }) + it('should render correct copy', () => { + render(props) + screen.getByText('Exit before completing Labware Position Check?') + screen.getByText( + 'If you exit now, all labware offsets will be discarded. This cannot be undone.' + ) + screen.getByRole('button', { name: 'Exit' }) + screen.getByRole('button', { name: 'Go back' }) + }) + it('should invoke callback props when ctas are clicked', () => { + render(props) + fireEvent.click(screen.getByRole('button', { name: 'Go back' })) + expect(props.onGoBack).toHaveBeenCalled() + fireEvent.click(screen.getByRole('button', { name: 'Exit' })) + expect(props.onConfirmExit).toHaveBeenCalled() + }) + it('should render correct copy for golden tip LPC', () => { + render({ + ...props, + shouldUseMetalProbe: true, + }) + screen.getByText('Remove the calibration probe before exiting') + screen.getByText( + 'If you exit now, all labware offsets will be discarded. This cannot be undone.' + ) + screen.getByRole('button', { name: 'Remove calibration probe' }) + screen.getByRole('button', { name: 'Go back' }) + }) +}) diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/PickUpTip.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/PickUpTip.test.tsx new file mode 100644 index 00000000000..c23e1c1af2c --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/__tests__/PickUpTip.test.tsx @@ -0,0 +1,466 @@ +import type * as React from 'react' +import { fireEvent, screen, waitFor } from '@testing-library/react' +import { it, describe, beforeEach, vi, afterEach, expect } from 'vitest' +import { FLEX_ROBOT_TYPE, HEATERSHAKER_MODULE_V1 } from '@opentrons/shared-data' +import { i18n } from '/app/i18n' +import { useProtocolMetadata } from '/app/resources/protocols' +import { getIsOnDevice } from '/app/redux/config' +import { PickUpTip } from '../PickUpTip' +import { SECTIONS } from '../constants' +import { mockCompletedAnalysis, mockExistingOffsets } from '../__fixtures__' +import type { CommandData } from '@opentrons/api-client' +import { nestedTextMatcher, renderWithProviders } from '/app/__testing-utils__' +import type { Mock } from 'vitest' + +vi.mock('/app/resources/protocols') +vi.mock('/app/redux/config') + +const mockStartPosition = { x: 10, y: 20, z: 30 } + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('PickUpTip', () => { + let props: React.ComponentProps + let mockChainRunCommands: Mock + + beforeEach(() => { + mockChainRunCommands = vi.fn().mockImplementation(() => Promise.resolve()) + vi.mocked(getIsOnDevice).mockReturnValue(false) + props = { + section: SECTIONS.PICK_UP_TIP, + pipetteId: mockCompletedAnalysis.pipettes[0].id, + labwareId: mockCompletedAnalysis.labware[0].id, + definitionUri: mockCompletedAnalysis.labware[0].definitionUri, + location: { slotName: 'D1' }, + protocolData: mockCompletedAnalysis, + proceed: vi.fn(), + chainRunCommands: mockChainRunCommands, + handleJog: vi.fn(), + registerPosition: vi.fn(), + setFatalError: vi.fn(), + workingOffsets: [], + existingOffsets: mockExistingOffsets, + isRobotMoving: false, + robotType: FLEX_ROBOT_TYPE, + protocolHasModules: false, + currentStepIndex: 1, + } + vi.mocked(useProtocolMetadata).mockReturnValue({ + robotType: 'OT-3 Standard', + }) + }) + afterEach(() => { + vi.resetAllMocks() + }) + it('renders correct copy when preparing space on desktop if protocol has modules', () => { + props.protocolHasModules = true + render(props) + screen.getByRole('heading', { name: 'Prepare tip rack in Slot D1' }) + screen.getByText('Place modules on deck') + screen.getByText( + 'Clear all deck slots of labware, leaving modules in place' + ) + screen.getByText('a full Mock TipRack Definition') + screen.getByText('Slot D1') + screen.getByRole('button', { name: 'Confirm placement' }) + }) + it('renders correct copy when preparing space on touchscreen if protocol has modules', () => { + vi.mocked(getIsOnDevice).mockReturnValue(true) + props.protocolHasModules = true + render(props) + screen.getByRole('heading', { name: 'Prepare tip rack in Slot D1' }) + screen.getByText('Place modules on deck') + screen.getByText('Clear all deck slots of labware') + screen.getByText('a full Mock TipRack Definition') + screen.getByText('Slot D1') + }) + it('renders correct copy when preparing space on desktop if protocol has no modules', () => { + render(props) + screen.getByRole('heading', { name: 'Prepare tip rack in Slot D1' }) + screen.getByText( + 'Clear all deck slots of labware, leaving modules in place' + ) + screen.getByText('a full Mock TipRack Definition') + screen.getByText('Slot D1') + screen.getByRole('button', { name: 'Confirm placement' }) + }) + it('renders correct copy when preparing space on touchscreen if protocol has no modules', () => { + vi.mocked(getIsOnDevice).mockReturnValue(true) + render(props) + screen.getByRole('heading', { name: 'Prepare tip rack in Slot D1' }) + screen.getByText('Clear all deck slots of labware') + screen.getByText('a full Mock TipRack Definition') + screen.getByText('Slot D1') + }) + it('renders correct copy when confirming position on desktop', () => { + render({ + ...props, + workingOffsets: [ + { + location: { slotName: 'D1' }, + labwareId: 'labwareId1', + initialPosition: { x: 1, y: 2, z: 3 }, + finalPosition: null, + }, + ], + }) + screen.getByRole('heading', { + name: 'Pick up tip from tip rack in Slot D1', + }) + screen.getByText( + "Ensure that the pipette nozzle furthest from you is centered above and level with the top of the tip in the A1 position. If it isn't, use the controls below or your keyboard to jog the pipette until it is properly aligned." + ) + screen.getByRole('link', { name: 'Need help?' }) + }) + it('renders correct copy when confirming position on touchscreen', () => { + vi.mocked(getIsOnDevice).mockReturnValue(true) + render({ + ...props, + workingOffsets: [ + { + location: { slotName: 'D1' }, + labwareId: 'labwareId1', + initialPosition: { x: 1, y: 2, z: 3 }, + finalPosition: null, + }, + ], + }) + screen.getByRole('heading', { + name: 'Pick up tip from tip rack in Slot D1', + }) + screen.getByText( + nestedTextMatcher( + "Ensure that the pipette nozzle furthest from you is centered above and level with the top of the tip in the A1 position. If it isn't, tap Move pipette and then jog the pipette until it is properly aligned." + ) + ) + }) + it('executes correct chained commands when confirm placement CTA is clicked', () => { + vi.mocked(mockChainRunCommands).mockImplementation(() => + Promise.resolve([{} as CommandData]) + ) + render(props) + const confirm = screen.getByRole('button', { name: 'Confirm placement' }) + fireEvent.click(confirm) + expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 1, + [ + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: { slotName: 'D1' }, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveToWell', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + wellLocation: { origin: 'top', offset: undefined }, + }, + }, + { + commandType: 'savePosition', + params: { pipetteId: 'pipetteId1' }, + }, + ], + false + ) + }) + + it('executes correct chained commands when confirm position CTA is clicked and user tries again', async () => { + vi.mocked(mockChainRunCommands).mockImplementation(() => + Promise.resolve([ + { + data: { + commandType: 'savePosition', + result: { position: mockStartPosition }, + }, + }, + {}, + {}, + ]) + ) + + render({ + ...props, + workingOffsets: [ + { + location: { slotName: 'D1' }, + labwareId: 'labwareId1', + initialPosition: { x: 1, y: 2, z: 3 }, + finalPosition: null, + }, + ], + }) + + const forward = screen.getByRole('button', { name: 'forward' }) + fireEvent.click(forward) + expect(props.handleJog).toHaveBeenCalled() + const confirm = screen.getByRole('button', { name: 'Confirm position' }) + fireEvent.click(confirm) + await new Promise((resolve, reject) => setTimeout(resolve)) + + await waitFor(() => { + expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 1, + [ + { + commandType: 'savePosition', + params: { pipetteId: 'pipetteId1' }, + }, + ], + false + ) + }) + await waitFor(() => { + expect(props.registerPosition).toHaveBeenNthCalledWith(1, { + type: 'finalPosition', + labwareId: 'labwareId1', + location: { slotName: 'D1' }, + position: { x: 10, y: 20, z: 30 }, + }) + }) + await waitFor(() => { + expect(props.registerPosition).toHaveBeenNthCalledWith(2, { + type: 'tipPickUpOffset', + offset: { x: 9, y: 18, z: 27 }, + }) + }) + await waitFor(() => { + expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 2, + [ + { + commandType: 'pickUpTip', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + wellLocation: { origin: 'top', offset: { x: 9, y: 18, z: 27 } }, + }, + }, + ], + false + ) + screen.getByRole('heading', { + name: 'Did pipette pick up tip successfully?', + }) + }) + const tryAgain = screen.getByRole('button', { name: 'Try again' }) + fireEvent.click(tryAgain) + await new Promise((resolve, reject) => setTimeout(resolve)) + + await waitFor(() => { + expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 3, + [ + { + commandType: 'dropTip', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + }, + }, + { + commandType: 'moveToWell', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + wellLocation: { origin: 'top' }, + }, + }, + ], + false + ) + }) + await waitFor(() => { + expect(props.registerPosition).toHaveBeenNthCalledWith(3, { + type: 'tipPickUpOffset', + offset: null, + }) + }) + }) + it('proceeds after confirm position and pick up tip', async () => { + vi.mocked(mockChainRunCommands).mockImplementation(() => + Promise.resolve([ + { + data: { + commandType: 'savePosition', + result: { position: mockStartPosition }, + }, + }, + {}, + {}, + ]) + ) + render({ + ...props, + workingOffsets: [ + { + location: { slotName: 'D1' }, + labwareId: 'labwareId1', + initialPosition: { x: 1, y: 2, z: 3 }, + finalPosition: null, + }, + ], + }) + + const confirm = screen.getByRole('button', { name: 'Confirm position' }) + fireEvent.click(confirm) + await new Promise((resolve, reject) => setTimeout(resolve)) + + await waitFor(() => { + expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 1, + [ + { + commandType: 'savePosition', + params: { pipetteId: 'pipetteId1' }, + }, + ], + false + ) + }) + await waitFor(() => { + expect(props.registerPosition).toHaveBeenNthCalledWith(1, { + type: 'finalPosition', + labwareId: 'labwareId1', + location: { slotName: 'D1' }, + position: { x: 10, y: 20, z: 30 }, + }) + }) + await waitFor(() => { + expect(props.registerPosition).toHaveBeenNthCalledWith(2, { + type: 'tipPickUpOffset', + offset: { x: 9, y: 18, z: 27 }, + }) + }) + await waitFor(() => { + expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 2, + [ + { + commandType: 'pickUpTip', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + wellLocation: { origin: 'top', offset: { x: 9, y: 18, z: 27 } }, + }, + }, + ], + false + ) + screen.getByRole('heading', { + name: 'Did pipette pick up tip successfully?', + }) + }) + const yesButton = screen.getByRole('button', { name: 'Yes' }) + fireEvent.click(yesButton) + await new Promise((resolve, reject) => setTimeout(resolve)) + + await waitFor(() => { + expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 3, + [ + { + commandType: 'retractAxis' as const, + params: { + axis: 'leftZ', + }, + }, + { + commandType: 'retractAxis' as const, + params: { + axis: 'x', + }, + }, + { + commandType: 'retractAxis' as const, + params: { + axis: 'y', + }, + }, + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + ], + false + ) + }) + await waitFor(() => { + expect(props.proceed).toHaveBeenCalled() + }) + }) + it('executes heater shaker closed latch commands for every hs module before other commands', () => { + props = { + ...props, + protocolData: { + ...props.protocolData, + modules: [ + { + id: 'firstHSId', + model: HEATERSHAKER_MODULE_V1, + location: { slotName: 'D3' }, + serialNumber: 'firstHSSerial', + }, + { + id: 'secondHSId', + model: HEATERSHAKER_MODULE_V1, + location: { slotName: 'A1' }, + serialNumber: 'secondHSSerial', + }, + ], + }, + } + render(props) + const confirm = screen.getByRole('button', { name: 'Confirm placement' }) + fireEvent.click(confirm) + expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 1, + [ + { + commandType: 'heaterShaker/closeLabwareLatch', + params: { moduleId: 'firstHSId' }, + }, + { + commandType: 'heaterShaker/closeLabwareLatch', + params: { moduleId: 'secondHSId' }, + }, + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: { slotName: 'D1' }, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveToWell', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + wellLocation: { origin: 'top' }, + }, + }, + { commandType: 'savePosition', params: { pipetteId: 'pipetteId1' } }, + ], + false + ) + }) +}) diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/ResultsSummary.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/ResultsSummary.test.tsx new file mode 100644 index 00000000000..24101904de4 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/__tests__/ResultsSummary.test.tsx @@ -0,0 +1,91 @@ +import type * as React from 'react' +import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { i18n } from '/app/i18n' +import { renderWithProviders } from '/app/__testing-utils__' +import { getIsLabwareOffsetCodeSnippetsOn } from '/app/redux/config' +import { ResultsSummary } from '../ResultsSummary' +import { SECTIONS } from '../constants' +import { mockTipRackDefinition } from '/app/redux/custom-labware/__fixtures__' +import { + mockCompletedAnalysis, + mockExistingOffsets, + mockWorkingOffsets, +} from '../__fixtures__' + +vi.mock('/app/redux/config') + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('ResultsSummary', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + section: SECTIONS.RESULTS_SUMMARY, + protocolData: mockCompletedAnalysis, + workingOffsets: mockWorkingOffsets, + existingOffsets: mockExistingOffsets, + isApplyingOffsets: false, + isDeletingMaintenanceRun: false, + handleApplyOffsets: vi.fn(), + } + }) + afterEach(() => { + vi.restoreAllMocks() + }) + it('renders correct copy', () => { + render(props) + screen.getByText('New labware offset data') + screen.getByRole('button', { name: 'Apply offsets' }) + screen.getByRole('link', { name: 'Need help?' }) + screen.getByRole('columnheader', { name: 'location' }) + screen.getByRole('columnheader', { name: 'labware' }) + screen.getByRole('columnheader', { name: 'labware offset data' }) + }) + it('calls handle apply offsets function when button is clicked', () => { + render(props) + fireEvent.click(screen.getByRole('button', { name: 'Apply offsets' })) + expect(props.handleApplyOffsets).toHaveBeenCalled() + }) + it('does disables the CTA to apply offsets when offsets are already being applied', () => { + props.isApplyingOffsets = true + render(props) + const button = screen.getByRole('button', { name: 'Apply offsets' }) + expect(button).toBeDisabled() + fireEvent.click(button) + expect(props.handleApplyOffsets).not.toHaveBeenCalled() + }) + it('does disables the CTA to apply offsets when the maintenance run is being deleted', () => { + props.isDeletingMaintenanceRun = true + render(props) + const button = screen.getByRole('button', { name: 'Apply offsets' }) + expect(button).toBeDisabled() + fireEvent.click(button) + expect(props.handleApplyOffsets).not.toHaveBeenCalled() + }) + it('renders a row per offset to apply', () => { + render(props) + expect( + screen.queryAllByRole('cell', { + name: mockTipRackDefinition.metadata.displayName, + }) + ).toHaveLength(2) + screen.getByRole('cell', { name: 'Slot 1' }) + screen.getByRole('cell', { name: 'Slot 3' }) + screen.getByRole('cell', { name: 'X 1.0 Y 1.0 Z 1.0' }) + screen.getByRole('cell', { name: 'X 3.0 Y 3.0 Z 3.0' }) + }) + + it('renders tabbed offset data with snippets when config option is selected', () => { + vi.mocked(getIsLabwareOffsetCodeSnippetsOn).mockReturnValue(true) + render(props) + expect(screen.getByText('Table View')).toBeTruthy() + expect(screen.getByText('Jupyter Notebook')).toBeTruthy() + expect(screen.getByText('Command Line Interface (SSH)')).toBeTruthy() + }) +}) diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/ReturnTip.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/ReturnTip.test.tsx new file mode 100644 index 00000000000..0af86097f9c --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/__tests__/ReturnTip.test.tsx @@ -0,0 +1,258 @@ +import type * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' + +import { FLEX_ROBOT_TYPE, HEATERSHAKER_MODULE_V1 } from '@opentrons/shared-data' + +import { renderWithProviders } from '/app/__testing-utils__' +import { i18n } from '/app/i18n' +import { SECTIONS } from '../constants' +import { mockCompletedAnalysis } from '../__fixtures__' +import { useProtocolMetadata } from '/app/resources/protocols' +import { getIsOnDevice } from '/app/redux/config' +import { ReturnTip } from '../ReturnTip' + +vi.mock('/app/redux/config') +vi.mock('/app/resources/protocols') + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('ReturnTip', () => { + let props: React.ComponentProps + let mockChainRunCommands + + beforeEach(() => { + mockChainRunCommands = vi.fn().mockImplementation(() => Promise.resolve()) + vi.mocked(getIsOnDevice).mockReturnValue(false) + props = { + section: SECTIONS.RETURN_TIP, + pipetteId: mockCompletedAnalysis.pipettes[0].id, + labwareId: mockCompletedAnalysis.labware[0].id, + definitionUri: mockCompletedAnalysis.labware[0].definitionUri, + location: { slotName: 'D1' }, + protocolData: mockCompletedAnalysis, + proceed: vi.fn(), + setFatalError: vi.fn(), + chainRunCommands: mockChainRunCommands, + tipPickUpOffset: null, + isRobotMoving: false, + robotType: FLEX_ROBOT_TYPE, + } + vi.mocked(useProtocolMetadata).mockReturnValue({ + robotType: 'OT-3 Standard', + }) + }) + afterEach(() => { + vi.restoreAllMocks() + }) + it('renders correct copy on desktop', () => { + render(props) + screen.getByRole('heading', { name: 'Return tip rack to Slot D1' }) + screen.getByText( + 'Clear all deck slots of labware, leaving modules in place' + ) + screen.getByText(/Mock TipRack Definition/i) + screen.getByText(/that you used before back into/i) + screen.getByText('Slot D1') + screen.getByText( + /The pipette will return tips to their original location in the rack./i + ) + screen.getByRole('link', { name: 'Need help?' }) + }) + it('renders correct copy on device', () => { + vi.mocked(getIsOnDevice).mockReturnValue(true) + render(props) + screen.getByRole('heading', { name: 'Return tip rack to Slot D1' }) + screen.getByText('Clear all deck slots of labware') + screen.getByText(/Mock TipRack Definition/i) + screen.getByText(/that you used before back into/i) + screen.getByText('Slot D1') + screen.getByText( + /The pipette will return tips to their original location in the rack./i + ) + }) + it('executes correct chained commands when CTA is clicked', async () => { + render(props) + fireEvent.click(screen.getByRole('button', { name: 'Confirm placement' })) + await expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 1, + [ + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: { slotName: 'D1' }, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveToWell', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + wellLocation: { origin: 'top', offset: undefined }, + }, + }, + { + commandType: 'dropTip', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + wellLocation: { origin: 'default', offset: undefined }, + }, + }, + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + { commandType: 'home', params: {} }, + ], + false + ) + // temporary comment-out + // await expect(props.proceed).toHaveBeenCalled() + }) + it('executes correct chained commands with tip pick up offset when CTA is clicked', async () => { + props = { + ...props, + tipPickUpOffset: { x: 10, y: 11, z: 12 }, + } + render(props) + fireEvent.click(screen.getByRole('button', { name: 'Confirm placement' })) + await expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 1, + [ + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: { slotName: 'D1' }, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveToWell', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + wellLocation: { origin: 'top', offset: { x: 10, y: 11, z: 12 } }, + }, + }, + { + commandType: 'dropTip', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + wellLocation: { + origin: 'default', + offset: { x: 10, y: 11, z: 12 }, + }, + }, + }, + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + { commandType: 'home', params: {} }, + ], + false + ) + // temporary comment-out + // await expect(props.proceed).toHaveBeenCalled() + }) + it('executes heater shaker closed latch commands for every hs module before other commands', async () => { + props = { + ...props, + tipPickUpOffset: { x: 10, y: 11, z: 12 }, + protocolData: { + ...props.protocolData, + modules: [ + { + id: 'firstHSId', + model: HEATERSHAKER_MODULE_V1, + location: { slotName: 'D3' }, + serialNumber: 'firstHSSerial', + }, + { + id: 'secondHSId', + model: HEATERSHAKER_MODULE_V1, + location: { slotName: 'A1' }, + serialNumber: 'secondHSSerial', + }, + ], + }, + } + render(props) + fireEvent.click(screen.getByRole('button', { name: 'Confirm placement' })) + await expect(props.chainRunCommands).toHaveBeenNthCalledWith( + 1, + [ + { + commandType: 'heaterShaker/closeLabwareLatch', + params: { moduleId: 'firstHSId' }, + }, + { + commandType: 'heaterShaker/closeLabwareLatch', + params: { moduleId: 'secondHSId' }, + }, + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: { slotName: 'D1' }, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveToWell', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + wellLocation: { origin: 'top', offset: { x: 10, y: 11, z: 12 } }, + }, + }, + { + commandType: 'dropTip', + params: { + pipetteId: 'pipetteId1', + labwareId: 'labwareId1', + wellName: 'A1', + wellLocation: { + origin: 'default', + offset: { x: 10, y: 11, z: 12 }, + }, + }, + }, + { + commandType: 'moveLabware', + params: { + labwareId: 'labwareId1', + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + { commandType: 'home', params: {} }, + ], + false + ) + // temporary comment-out + // await expect(props.proceed).toHaveBeenCalled() + }) +}) diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/RobotMotionLoader.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/RobotMotionLoader.test.tsx new file mode 100644 index 00000000000..fc2c49aa8f5 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/__tests__/RobotMotionLoader.test.tsx @@ -0,0 +1,20 @@ +import { screen } from '@testing-library/react' +import { describe, it } from 'vitest' +import { renderWithProviders } from '/app/__testing-utils__' +import { i18n } from '/app/i18n' +import { RobotMotionLoader } from '../RobotMotionLoader' + +const mockHeader = 'Stand back, robot needs some space right now' + +const render = () => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('Robot in Motion Modal', () => { + it('should render robot in motion loader with header', () => { + render() + screen.getByRole('heading', { name: mockHeader }) + }) +}) diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/TipConfirmation.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/TipConfirmation.test.tsx new file mode 100644 index 00000000000..8f8878a7122 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/__tests__/TipConfirmation.test.tsx @@ -0,0 +1,39 @@ +import type * as React from 'react' +import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest' +import { fireEvent, screen } from '@testing-library/react' +import { TipConfirmation } from '../TipConfirmation' +import { i18n } from '/app/i18n' +import { renderWithProviders } from '/app/__testing-utils__' + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('TipConfirmation', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + invalidateTip: vi.fn(), + confirmTip: vi.fn(), + } + }) + afterEach(() => { + vi.restoreAllMocks() + }) + it('should render correct copy', () => { + render(props) + screen.getByText('Did pipette pick up tip successfully?') + screen.getByRole('button', { name: 'Yes' }) + screen.getByRole('button', { name: 'Try again' }) + }) + it('should invoke callback props when ctas are clicked', () => { + render(props) + fireEvent.click(screen.getByRole('button', { name: 'Try again' })) + expect(props.invalidateTip).toHaveBeenCalled() + fireEvent.click(screen.getByRole('button', { name: 'Yes' })) + expect(props.confirmTip).toHaveBeenCalled() + }) +}) diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx new file mode 100644 index 00000000000..a16cfc4a8a4 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx @@ -0,0 +1,199 @@ +import type * as React from 'react' +import { Provider } from 'react-redux' +import configureStore from 'redux-mock-store' +import { when } from 'vitest-when' +import { + act, + fireEvent, + renderHook, + screen, + waitFor, +} from '@testing-library/react' +import { describe, it, beforeEach, afterEach, vi, expect } from 'vitest' +import { QueryClient, QueryClientProvider } from 'react-query' + +import { + useCreateMaintenanceRunLabwareDefinitionMutation, + useDeleteMaintenanceRunMutation, +} from '@opentrons/react-api-client' +import { FLEX_ROBOT_TYPE, fixtureTiprack300ul } from '@opentrons/shared-data' + +import { renderWithProviders } from '/app/__testing-utils__' +import { + useCreateTargetedMaintenanceRunMutation, + useNotifyRunQuery, + useMostRecentCompletedAnalysis, +} from '/app/resources/runs' +import { useLaunchLPC } from '../useLaunchLPC' +import { LabwarePositionCheck } from '..' + +import type { Mock } from 'vitest' +import type { LabwareOffset } from '@opentrons/api-client' +import type { LabwareDefinition2 } from '@opentrons/shared-data' + +vi.mock('../') +vi.mock('@opentrons/react-api-client') +vi.mock('/app/resources/runs') + +const MOCK_RUN_ID = 'mockRunId' +const MOCK_MAINTENANCE_RUN_ID = 'mockMaintenanceRunId' +const mockCurrentOffsets: LabwareOffset[] = [ + { + createdAt: '2022-12-20T14:06:23.562082+00:00', + definitionUri: 'opentrons/opentrons_96_tiprack_10ul/1', + id: 'dceac542-bca4-4313-82ba-d54a19dab204', + location: { slotName: '2' }, + vector: { x: 1, y: 2, z: 3 }, + }, + { + createdAt: '2022-12-20T14:06:23.562878+00:00', + definitionUri: + 'opentrons/opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat/1', + id: '70ae2e31-716b-4e1f-a90c-9b0dfd4d7feb', + location: { slotName: '1', moduleModel: 'heaterShakerModuleV1' }, + vector: { x: 0, y: 0, z: 0 }, + }, +] +const mockLabwareDef = fixtureTiprack300ul as LabwareDefinition2 + +describe('useLaunchLPC hook', () => { + let wrapper: React.FunctionComponent<{ children: React.ReactNode }> + let mockCreateMaintenanceRun: Mock + let mockCreateLabwareDefinition: Mock + let mockDeleteMaintenanceRun: Mock + const mockStore = configureStore() + + beforeEach(() => { + const queryClient = new QueryClient() + mockCreateMaintenanceRun = vi.fn((_data, opts) => { + const results = { data: { id: MOCK_MAINTENANCE_RUN_ID } } + opts?.onSuccess(results) + return Promise.resolve(results) + }) + mockCreateLabwareDefinition = vi.fn(_data => + Promise.resolve({ data: { definitionUri: 'fakeDefUri' } }) + ) + mockDeleteMaintenanceRun = vi.fn((_data, opts) => { + opts?.onSettled() + }) + const store = mockStore({ isOnDevice: false }) + wrapper = ({ children }) => ( + + + {children} + + + ) + vi.mocked(LabwarePositionCheck).mockImplementation(({ onCloseClick }) => ( +
{ + onCloseClick() + }} + > + exit +
+ )) + when(vi.mocked(useNotifyRunQuery)) + .calledWith(MOCK_RUN_ID, { staleTime: Infinity }) + .thenReturn({ + data: { + data: { + labwareOffsets: mockCurrentOffsets, + }, + }, + } as any) + when(vi.mocked(useCreateTargetedMaintenanceRunMutation)) + .calledWith() + .thenReturn({ + createTargetedMaintenanceRun: mockCreateMaintenanceRun, + } as any) + when(vi.mocked(useCreateMaintenanceRunLabwareDefinitionMutation)) + .calledWith() + .thenReturn({ + createLabwareDefinition: mockCreateLabwareDefinition, + } as any) + when(vi.mocked(useDeleteMaintenanceRunMutation)) + .calledWith() + .thenReturn({ + deleteMaintenanceRun: mockDeleteMaintenanceRun, + } as any) + when(vi.mocked(useMostRecentCompletedAnalysis)) + .calledWith(MOCK_RUN_ID) + .thenReturn({ + commands: [ + { + key: 'CommandKey0', + commandType: 'loadLabware', + params: { + labwareId: 'firstLabwareId', + location: { slotName: '1' }, + displayName: 'first labware nickname', + }, + result: { + labwareId: 'firstLabwareId', + definition: mockLabwareDef, + offset: { x: 0, y: 0, z: 0 }, + }, + id: 'CommandId0', + status: 'succeeded', + error: null, + createdAt: 'fakeCreatedAtTimestamp', + startedAt: 'fakeStartedAtTimestamp', + completedAt: 'fakeCompletedAtTimestamp', + }, + ], + } as any) + }) + afterEach(() => { + vi.resetAllMocks() + }) + + it('returns and no wizard by default', () => { + const { result } = renderHook( + () => useLaunchLPC(MOCK_RUN_ID, FLEX_ROBOT_TYPE), + { wrapper } + ) + expect(result.current.launchLPCWizard).toEqual(null) + }) + + it('returns creates maintenance run with current offsets and definitions when create callback is called, closes and deletes when exit is clicked', async () => { + const { result } = renderHook( + () => useLaunchLPC(MOCK_RUN_ID, FLEX_ROBOT_TYPE), + { wrapper } + ) + act(() => { + result.current.launchLPC() + }) + await waitFor(() => { + expect(mockCreateLabwareDefinition).toHaveBeenCalledWith({ + maintenanceRunId: MOCK_MAINTENANCE_RUN_ID, + labwareDef: mockLabwareDef, + }) + }) + + await waitFor(() => { + expect(mockCreateMaintenanceRun).toHaveBeenCalledWith({ + labwareOffsets: mockCurrentOffsets.map( + ({ vector, location, definitionUri }) => ({ + vector, + location, + definitionUri, + }) + ), + }) + }) + + await waitFor(() => { + expect(result.current.launchLPCWizard).not.toBeNull() + }) + renderWithProviders(result.current.launchLPCWizard ?? <>) + fireEvent.click(screen.getByText('exit')) + expect(mockDeleteMaintenanceRun).toHaveBeenCalledWith( + MOCK_MAINTENANCE_RUN_ID, + { + onSettled: expect.any(Function), + } + ) + expect(result.current.launchLPCWizard).toBeNull() + }) +}) diff --git a/app/src/organisms/LabwarePositionCheck/constants.ts b/app/src/organisms/LabwarePositionCheck/constants.ts new file mode 100644 index 00000000000..f35be0d04d2 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/constants.ts @@ -0,0 +1,11 @@ +export const SECTIONS = { + BEFORE_BEGINNING: 'BEFORE_BEGINNING', + ATTACH_PROBE: 'ATTACH_PROBE', + CHECK_TIP_RACKS: 'CHECK_TIP_RACKS', + PICK_UP_TIP: 'PICK_UP_TIP', + CHECK_LABWARE: 'CHECK_LABWARE', + CHECK_POSITIONS: 'CHECK_POSITIONS', + RETURN_TIP: 'RETURN_TIP', + DETACH_PROBE: 'DETACH_PROBE', + RESULTS_SUMMARY: 'RESULTS_SUMMARY', +} as const diff --git a/app/src/organisms/LabwarePositionCheck/getLabwarePositionCheckSteps.ts b/app/src/organisms/LabwarePositionCheck/getLabwarePositionCheckSteps.ts new file mode 100644 index 00000000000..1c51c06827f --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/getLabwarePositionCheckSteps.ts @@ -0,0 +1,52 @@ +import { getPrimaryPipetteId } from './utils/getPrimaryPipetteId' +import { getTipBasedLPCSteps } from './utils/getTipBasedLPCSteps' +import { getProbeBasedLPCSteps } from './utils/getProbeBasedLPCSteps' +import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' +import type { LabwarePositionCheckStep } from './types' + +export const getLabwarePositionCheckSteps = ( + protocolData: CompletedProtocolAnalysis, + shouldUseMetalProbe: boolean +): LabwarePositionCheckStep[] => { + if (protocolData != null && 'pipettes' in protocolData) { + if (protocolData.pipettes.length === 0) { + throw new Error( + 'no pipettes loaded within protocol, labware position check cannot be performed' + ) + } + if (shouldUseMetalProbe) return getProbeBasedLPCSteps(protocolData) + + // filter out any pipettes that are not being used in the protocol + const pipettesUsedInProtocol: CompletedProtocolAnalysis['pipettes'] = protocolData.pipettes.filter( + ({ id }) => + protocolData.commands.some( + command => + command.commandType === 'pickUpTip' && + command.params.pipetteId === id + ) + ) + const { labware, modules, commands } = protocolData + if (pipettesUsedInProtocol.length === 0) { + throw new Error( + 'pipettes do not pick up a tip within protocol, labware position check cannot be performed' + ) + } + const pipettesById = pipettesUsedInProtocol.reduce( + (acc, pip) => ({ ...acc, [pip.id]: pip }), + {} + ) + const primaryPipetteId = getPrimaryPipetteId(pipettesById, commands) + const secondaryPipetteId = + pipettesUsedInProtocol.find(({ id }) => id !== primaryPipetteId)?.id ?? + null + return getTipBasedLPCSteps({ + primaryPipetteId, + secondaryPipetteId, + labware, + modules, + commands, + }) + } + console.error('expected pipettes to be in protocol data') + return [] +} diff --git a/app/src/organisms/LabwarePositionCheck/index.tsx b/app/src/organisms/LabwarePositionCheck/index.tsx new file mode 100644 index 00000000000..e96191c584e --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/index.tsx @@ -0,0 +1,104 @@ +import { Component } from 'react' +import { useLogger } from '../../logger' +import { LabwarePositionCheckComponent } from './LabwarePositionCheckComponent' +import { FatalErrorModal } from './FatalErrorModal' +import { getIsOnDevice } from '/app/redux/config' +import { useSelector } from 'react-redux' + +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' + +import type { ErrorInfo, ReactNode } from 'react' +import type { + CompletedProtocolAnalysis, + RobotType, +} from '@opentrons/shared-data' +import type { LabwareOffset } from '@opentrons/api-client' + +interface LabwarePositionCheckModalProps { + onCloseClick: () => void + runId: string + maintenanceRunId: string + robotType: RobotType + existingOffsets: LabwareOffset[] + mostRecentAnalysis: CompletedProtocolAnalysis | null + protocolName: string + caughtError?: Error + setMaintenanceRunId: (id: string | null) => void + isDeletingMaintenanceRun: boolean +} + +// We explicitly wrap LabwarePositionCheckComponent in an ErrorBoundary because an error might occur while pulling in +// the component's dependencies (like useLabwarePositionCheck). If we wrapped the contents of LabwarePositionCheckComponent +// in an ErrorBoundary as part of its return value (render), an error could occur before this point, meaning the error boundary +// would never get invoked +export const LabwarePositionCheck = ( + props: LabwarePositionCheckModalProps +): JSX.Element => { + const logger = useLogger(new URL('', import.meta.url).pathname) + const isOnDevice = useSelector(getIsOnDevice) + return ( + + + + ) +} + +interface ErrorBoundaryProps { + children: ReactNode + onClose: () => void + shouldUseMetalProbe: boolean + logger: ReturnType + ErrorComponent: (props: { + errorMessage: string + shouldUseMetalProbe: boolean + onClose: () => void + isOnDevice: boolean + }) => JSX.Element + isOnDevice: boolean +} +class ErrorBoundary extends Component< + ErrorBoundaryProps, + { error: Error | null } +> { + constructor(props: ErrorBoundaryProps) { + super(props) + this.state = { error: null } + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo): void { + this.props.logger.error(`LPC error message: ${error.message}`) + this.props.logger.error( + `LPC error component stack: ${errorInfo.componentStack}` + ) + this.setState({ + error, + }) + } + + render(): ErrorBoundaryProps['children'] | JSX.Element { + const { + ErrorComponent, + children, + shouldUseMetalProbe, + isOnDevice, + } = this.props + const { error } = this.state + if (error != null) + return ( + + ) + // Normally, just render children + return children + } +} diff --git a/app/src/organisms/LabwarePositionCheck/types.ts b/app/src/organisms/LabwarePositionCheck/types.ts new file mode 100644 index 00000000000..2ddd14c25d6 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/types.ts @@ -0,0 +1,111 @@ +import type { SECTIONS } from './constants' +import type { useCreateCommandMutation } from '@opentrons/react-api-client' +import type { LabwareOffsetLocation, VectorOffset } from '@opentrons/api-client' +import type { LabwareDefinition2 } from '@opentrons/shared-data' + +export type LabwarePositionCheckStep = + | BeforeBeginningStep + | CheckTipRacksStep + | AttachProbeStep + | PickUpTipStep + | CheckLabwareStep + | CheckPositionsStep + | ReturnTipStep + | DetachProbeStep + | ResultsSummaryStep +export interface BeforeBeginningStep { + section: typeof SECTIONS.BEFORE_BEGINNING +} +export interface CheckTipRacksStep { + section: typeof SECTIONS.CHECK_TIP_RACKS + pipetteId: string + labwareId: string + location: LabwareOffsetLocation + definitionUri: string + adapterId?: string +} +export interface AttachProbeStep { + section: typeof SECTIONS.ATTACH_PROBE + pipetteId: string +} +export interface PickUpTipStep { + section: typeof SECTIONS.PICK_UP_TIP + pipetteId: string + labwareId: string + location: LabwareOffsetLocation + definitionUri: string + adapterId?: string +} +export interface CheckPositionsStep { + section: typeof SECTIONS.CHECK_POSITIONS + pipetteId: string + labwareId: string + location: LabwareOffsetLocation + definitionUri: string + moduleId?: string +} +export interface CheckLabwareStep { + section: typeof SECTIONS.CHECK_LABWARE + pipetteId: string + labwareId: string + location: LabwareOffsetLocation + definitionUri: string + moduleId?: string + adapterId?: string +} +export interface ReturnTipStep { + section: typeof SECTIONS.RETURN_TIP + pipetteId: string + labwareId: string + location: LabwareOffsetLocation + definitionUri: string + adapterId?: string +} +export interface DetachProbeStep { + section: typeof SECTIONS.DETACH_PROBE + pipetteId: string +} +export interface ResultsSummaryStep { + section: typeof SECTIONS.RESULTS_SUMMARY +} + +type CreateCommandMutate = ReturnType< + typeof useCreateCommandMutation +>['createCommand'] +export type CreateRunCommand = ( + params: Omit[0], 'runId'>, + options?: Parameters[1] +) => ReturnType + +interface InitialPositionAction { + type: 'initialPosition' + labwareId: string + location: LabwareOffsetLocation + position: VectorOffset | null +} +interface FinalPositionAction { + type: 'finalPosition' + labwareId: string + location: LabwareOffsetLocation + position: VectorOffset | null +} +interface TipPickUpOffsetAction { + type: 'tipPickUpOffset' + offset: VectorOffset | null +} +export type RegisterPositionAction = + | InitialPositionAction + | FinalPositionAction + | TipPickUpOffsetAction +export interface WorkingOffset { + labwareId: string + location: LabwareOffsetLocation + initialPosition: VectorOffset | null + finalPosition: VectorOffset | null +} + +export interface LabwareToOrder { + definition: LabwareDefinition2 + labwareId: string + slot: string +} diff --git a/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx b/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx new file mode 100644 index 00000000000..1ac2392b370 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx @@ -0,0 +1,89 @@ +import { useState } from 'react' + +import { + useCreateMaintenanceRunLabwareDefinitionMutation, + useDeleteMaintenanceRunMutation, +} from '@opentrons/react-api-client' + +import { + useCreateTargetedMaintenanceRunMutation, + useNotifyRunQuery, + useMostRecentCompletedAnalysis, +} from '/app/resources/runs' +import { LabwarePositionCheck } from '.' +import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' + +import type { RobotType } from '@opentrons/shared-data' + +export function useLaunchLPC( + runId: string, + robotType: RobotType, + protocolName?: string +): { launchLPC: () => void; launchLPCWizard: JSX.Element | null } { + const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) + const { + createTargetedMaintenanceRun, + } = useCreateTargetedMaintenanceRunMutation() + const { + deleteMaintenanceRun, + isLoading: isDeletingMaintenanceRun, + } = useDeleteMaintenanceRunMutation() + const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) + const [maintenanceRunId, setMaintenanceRunId] = useState(null) + const currentOffsets = runRecord?.data?.labwareOffsets ?? [] + const { + createLabwareDefinition, + } = useCreateMaintenanceRunLabwareDefinitionMutation() + + const handleCloseLPC = (): void => { + if (maintenanceRunId != null) { + deleteMaintenanceRun(maintenanceRunId, { + onSettled: () => { + setMaintenanceRunId(null) + }, + }) + } + } + return { + launchLPC: () => + createTargetedMaintenanceRun({ + labwareOffsets: currentOffsets.map( + ({ vector, location, definitionUri }) => ({ + vector, + location, + definitionUri, + }) + ), + }).then(maintenanceRun => + // TODO(BC, 2023-05-15): replace this with a call to the protocol run's GET labware_definitions + // endpoint once it's made we should be adding the definitions to the maintenance run by + // reading from the current protocol run, and not from the analysis + Promise.all( + getLabwareDefinitionsFromCommands( + mostRecentAnalysis?.commands ?? [] + ).map(def => { + createLabwareDefinition({ + maintenanceRunId: maintenanceRun?.data?.id, + labwareDef: def, + }) + }) + ).then(() => { + setMaintenanceRunId(maintenanceRun.data.id) + }) + ), + launchLPCWizard: + maintenanceRunId != null ? ( + + ) : null, + } +} diff --git a/app/src/organisms/LabwarePositionCheck/utils/__tests__/doesPipetteVisitAllTipracks.test.ts b/app/src/organisms/LabwarePositionCheck/utils/__tests__/doesPipetteVisitAllTipracks.test.ts new file mode 100644 index 00000000000..d3c7b511c58 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/utils/__tests__/doesPipetteVisitAllTipracks.test.ts @@ -0,0 +1,112 @@ +import { describe, it, expect } from 'vitest' +import { doesPipetteVisitAllTipracks } from '../doesPipetteVisitAllTipracks' +import { multiple_tipracks, one_tiprack } from '@opentrons/shared-data' +import type { + LoadedLabware, + ProtocolAnalysisOutput, + RunTimeCommand, +} from '@opentrons/shared-data' + +// TODO: update these fixtures to be v6 protocols +const protocolMultipleTipracks = (multiple_tipracks as unknown) as ProtocolAnalysisOutput +const labwareDefinitionsMultipleTipracks = multiple_tipracks.labwareDefinitions as {} +const protocolOneTiprack = (one_tiprack as unknown) as ProtocolAnalysisOutput +const labwareDefinitionsOneTiprack = one_tiprack.labwareDefinitions as {} +const labwareWithDefinitionUri = [ + { + id: 'fixedTrash', + displayName: 'Trash', + definitionUri: 'opentrons/opentrons_1_trash_1100ml_fixed/1', + loadName: 'opentrons_1_trash_1100ml_fixed', + }, + { + id: + '50d3ebb0-0042-11ec-8258-f7ffdf5ad45a:opentrons/opentrons_96_tiprack_300ul/1', + displayName: 'Opentrons 96 Tip Rack 300 µL', + definitionUri: 'opentrons/opentrons_96_tiprack_300ul/1', + loadName: 'opentrons_96_tiprack_300ul', + }, + { + id: + '9fbc1db0-0042-11ec-8258-f7ffdf5ad45a:opentrons/nest_12_reservoir_15ml/1', + displayName: 'NEST 12 Well Reservoir 15 mL', + definitionUri: 'opentrons/nest_12_reservoir_15ml/1', + loadName: 'nest_12_reservoir_15ml', + }, + { + id: 'e24818a0-0042-11ec-8258-f7ffdf5ad45a', + displayName: 'Opentrons 96 Tip Rack 300 µL (1)', + definitionUri: 'opentrons/opentrons_96_tiprack_300ul/1', + loadName: 'opentrons_96_tiprack_300ul', + }, +] as LoadedLabware[] + +describe('doesPipetteVisitAllTipracks', () => { + it('should return true when the pipette visits both tipracks', () => { + const pipetteId = 'c235a5a0-0042-11ec-8258-f7ffdf5ad45a' // this is just taken from the protocol fixture + const labware = labwareWithDefinitionUri + const commands: RunTimeCommand[] = protocolMultipleTipracks.commands + + expect( + doesPipetteVisitAllTipracks( + pipetteId, + labware, + labwareDefinitionsMultipleTipracks, + commands + ) + ).toBe(true) + }) + it('should return false when the pipette does NOT visit all tipracks', () => { + const pipetteId = '50d23e00-0042-11ec-8258-f7ffdf5ad45a' // this is just taken from the protocol fixture + const labware = labwareWithDefinitionUri + const commands: RunTimeCommand[] = protocolMultipleTipracks.commands + + expect( + doesPipetteVisitAllTipracks( + pipetteId, + labware, + labwareDefinitionsMultipleTipracks, + commands + ) + ).toBe(false) + }) + it('should return true when there is only one tiprack and pipette visits it', () => { + const pipetteId = 'pipetteId' // this is just taken from the protocol fixture + const labware = [ + { + id: 'fixedTrash', + displayName: 'Trash', + definitionUri: 'opentrons/opentrons_1_trash_1100ml_fixed/1', + loadName: 'opentrons_1_trash_1100ml_fixed', + }, + { + id: 'tiprackId', + displayName: 'Opentrons 96 Tip Rack 10 µL', + definitionUri: 'opentrons/opentrons_96_tiprack_10ul/1', + loadName: 'opentrons_96_tiprack_10ul', + }, + { + id: 'sourcePlateId', + displayName: 'Source Plate', + definitionUri: 'example/plate/1', + loadName: 'plate', + }, + { + id: 'destPlateId', + displayName: 'Dest Plate', + definitionUri: 'example/plate/1', + loadName: 'plate', + }, + ] as LoadedLabware[] + const commands: RunTimeCommand[] = protocolOneTiprack.commands + + expect( + doesPipetteVisitAllTipracks( + pipetteId, + labware, + labwareDefinitionsOneTiprack, + commands + ) + ).toBe(true) + }) +}) diff --git a/app/src/organisms/LabwarePositionCheck/utils/__tests__/getPrimaryPipetteId.test.ts b/app/src/organisms/LabwarePositionCheck/utils/__tests__/getPrimaryPipetteId.test.ts new file mode 100644 index 00000000000..7256c0245e8 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/utils/__tests__/getPrimaryPipetteId.test.ts @@ -0,0 +1,227 @@ +import { describe, it, expect } from 'vitest' +import { getPrimaryPipetteId } from '../getPrimaryPipetteId' +import type { + LoadedPipette, + LoadPipetteRunTimeCommand, +} from '@opentrons/shared-data' + +describe('getPrimaryPipetteId', () => { + it('should return the one and only pipette if there is only one pipette in the protocol', () => { + const mockPipettesById: { [id: string]: LoadedPipette } = { + p10SingleId: { + id: 'p10SingleId', + pipetteName: 'p10_single', + mount: 'left', + }, + } + expect(getPrimaryPipetteId(mockPipettesById, [])).toBe('p10SingleId') + }) + it('should throw an error if there are two pipettes with the same mount', () => { + const loadPipetteCommands: LoadPipetteRunTimeCommand[] = [ + { + id: '1', + commandType: 'loadPipette', + params: { + pipetteId: 'p10SingleId', + mount: 'left', + }, + }, + { + id: '1', + commandType: 'loadPipette', + params: { + pipetteId: 'p10MultiId', + mount: 'left', + }, + }, + ] as any + + const pipettesById: { [id: string]: LoadedPipette } = { + p10SingleId: { + id: 'p10SingleId', + pipetteName: 'p10_single', + mount: 'left', + }, + p10MultiId: { + id: 'p10SingleId', + pipetteName: 'p10_multi', + mount: 'left', + }, + } + expect(() => + getPrimaryPipetteId(pipettesById, loadPipetteCommands) + ).toThrow( + 'expected to find both left pipette and right pipette but could not' + ) + }) + it('should return the pipette with fewer channels', () => { + const loadPipetteCommands: LoadPipetteRunTimeCommand[] = [ + { + id: '1', + commandType: 'loadPipette', + params: { + pipetteId: 'p10SingleId', + mount: 'left', + }, + result: { + pipetteId: 'p10SingleId', + }, + }, + { + id: '1', + commandType: 'loadPipette', + params: { + pipetteId: 'p10MultiId', + mount: 'right', + }, + result: { + pipetteId: 'p10MultiId', + }, + }, + ] as any + + const pipettesById: { [id: string]: LoadedPipette } = { + p10SingleId: { + id: 'p10SingleId', + pipetteName: 'p10_single', + mount: 'left', + }, + p10MultiId: { + id: 'p10SingleId', + pipetteName: 'p10_multi', + mount: 'right', + }, + } + + expect(getPrimaryPipetteId(pipettesById, loadPipetteCommands)).toBe( + 'p10SingleId' + ) + }) + it('should return the smaller pipette', () => { + const loadPipetteCommands: LoadPipetteRunTimeCommand[] = [ + { + id: '1', + commandType: 'loadPipette', + params: { + pipetteId: 'p10SingleId', + mount: 'left', + }, + result: { + pipetteId: 'p10SingleId', + }, + }, + { + id: '1', + commandType: 'loadPipette', + params: { + pipetteId: 'p50SingleId', + mount: 'right', + }, + result: { + pipetteId: 'p50SingleId', + }, + }, + ] as any + + const pipettesById: { [id: string]: LoadedPipette } = { + p10SingleId: { + id: 'p10SingleId', + pipetteName: 'p10_single', + mount: 'left', + }, + p50SingleId: { + id: 'p50SingleId', + pipetteName: 'p50_single', + mount: 'right', + }, + } + expect(getPrimaryPipetteId(pipettesById, loadPipetteCommands)).toBe( + 'p10SingleId' + ) + }) + it('should return the newer model', () => { + const loadPipetteCommands: LoadPipetteRunTimeCommand[] = [ + { + id: '1', + commandType: 'loadPipette', + params: { + pipetteId: 'p300SingleId', + mount: 'left', + }, + result: { + pipetteId: 'p300SingleId', + }, + }, + { + id: '1', + commandType: 'loadPipette', + params: { + pipetteId: 'p300SingleGen2Id', + mount: 'right', + }, + result: { + pipetteId: 'p300SingleGen2Id', + }, + }, + ] as any + + const pipettesById: { [id: string]: LoadedPipette } = { + p300SingleId: { + id: 'p300SingleId', + pipetteName: 'p300_single', + mount: 'left', + }, + p300SingleGen2Id: { + id: 'p300SingleGen2Id', + pipetteName: 'p300_single_gen2', + mount: 'right', + }, + } + expect(getPrimaryPipetteId(pipettesById, loadPipetteCommands)).toBe( + 'p300SingleGen2Id' + ) + }) + + it('should return the left pipette when all else is the same', () => { + const loadPipetteCommands: LoadPipetteRunTimeCommand[] = [ + { + id: '1', + commandType: 'loadPipette', + params: { + pipetteId: 'p300SingleLeftId', + mount: 'left', + }, + result: { + pipetteId: 'p300SingleLeftId', + }, + }, + { + id: '1', + commandType: 'loadPipette', + params: { + pipetteId: 'p300SingleRightId', + mount: 'right', + }, + result: { + pipetteId: 'p300SingleRightId', + }, + }, + ] as any + + const pipettesById: { [id: string]: LoadedPipette } = { + p300SingleLeftId: { + id: 'p300SingleLeftId', + pipetteName: 'p300_single', + mount: 'left', + }, + p300SingleRightId: { + id: 'p300SingleRightId', + pipetteName: 'p300_single', + mount: 'right', + }, + } + expect(getPrimaryPipetteId(pipettesById, loadPipetteCommands)).toBe( + 'p300SingleLeftId' + ) + }) +}) diff --git a/app/src/organisms/LabwarePositionCheck/utils/doesPipetteVisitAllTipracks.ts b/app/src/organisms/LabwarePositionCheck/utils/doesPipetteVisitAllTipracks.ts new file mode 100644 index 00000000000..f2f336ae0fd --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/utils/doesPipetteVisitAllTipracks.ts @@ -0,0 +1,40 @@ +import { getIsTiprack } from '@opentrons/shared-data' + +import type { + LoadedLabware, + RunTimeCommand, + LabwareDefinition2, +} from '@opentrons/shared-data' + +import { + getPickUpTipCommandsWithPipette, + getTipracksVisited, +} from '/app/transformations/commands' + +export const doesPipetteVisitAllTipracks = ( + pipetteId: string, + labware: LoadedLabware[], + labwareDefinitions: Record, + commands: RunTimeCommand[] +): boolean => { + const numberOfTipracks = labware.reduce( + (numberOfTipracks, currentLabware) => { + const labwareDef = labwareDefinitions[currentLabware.definitionUri] + return getIsTiprack(labwareDef) ? numberOfTipracks + 1 : numberOfTipracks + }, + 0 + ) + const pickUpTipCommandsWithPipette = getPickUpTipCommandsWithPipette( + commands, + pipetteId + ) + + const tipracksVisited = getTipracksVisited(pickUpTipCommandsWithPipette) + + pickUpTipCommandsWithPipette.reduce((visited, command) => { + const tiprack = command.params.labwareId + return visited.includes(tiprack) ? visited : [...visited, tiprack] + }, []) + + return numberOfTipracks === tipracksVisited.length +} diff --git a/app/src/organisms/LabwarePositionCheck/utils/getDisplayLocation.ts b/app/src/organisms/LabwarePositionCheck/utils/getDisplayLocation.ts new file mode 100644 index 00000000000..d70b741c48d --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/utils/getDisplayLocation.ts @@ -0,0 +1,66 @@ +import { + getModuleDisplayName, + getModuleType, + THERMOCYCLER_MODULE_TYPE, + getLabwareDefURI, +} from '@opentrons/shared-data' +import type { i18n, TFunction } from 'i18next' +import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type { LabwareOffsetLocation } from '@opentrons/api-client' + +export function getDisplayLocation( + location: LabwareOffsetLocation, + labwareDefinitions: LabwareDefinition2[], + t: TFunction, + i18n: i18n, + slotOnly?: boolean +): string { + const slotDisplayLocation = i18n.format( + t('slot_name', { slotName: location.slotName }), + 'titleCase' + ) + if (slotOnly) { + return slotDisplayLocation + } + + if ('definitionUri' in location && location.definitionUri != null) { + const adapterDisplayName = labwareDefinitions.find( + def => getLabwareDefURI(def) === location.definitionUri + )?.metadata.displayName + + if ('moduleModel' in location && location.moduleModel != null) { + const { moduleModel } = location + const moduleDisplayName = getModuleDisplayName(moduleModel) + if (getModuleType(moduleModel) === THERMOCYCLER_MODULE_TYPE) { + return t('adapter_in_tc', { + adapter: adapterDisplayName, + module: moduleDisplayName, + }) + } else { + return t('adapter_in_mod_in_slot', { + adapter: adapterDisplayName, + module: moduleDisplayName, + slot: slotDisplayLocation, + }) + } + } else { + return t('adapter_in_slot', { + adapter: adapterDisplayName, + slot: slotDisplayLocation, + }) + } + } else if ('moduleModel' in location && location.moduleModel != null) { + const { moduleModel } = location + const moduleDisplayName = getModuleDisplayName(moduleModel) + if (getModuleType(moduleModel) === THERMOCYCLER_MODULE_TYPE) { + return moduleDisplayName + } else { + return t('module_in_slot', { + module: moduleDisplayName, + slot: slotDisplayLocation, + }) + } + } else { + return slotDisplayLocation + } +} diff --git a/app/src/organisms/LabwarePositionCheck/utils/getPrimaryPipetteId.ts b/app/src/organisms/LabwarePositionCheck/utils/getPrimaryPipetteId.ts new file mode 100644 index 00000000000..caa799f6dad --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/utils/getPrimaryPipetteId.ts @@ -0,0 +1,70 @@ +import { getPipetteNameSpecs } from '@opentrons/shared-data' +import type { + LoadedPipette, + LoadPipetteRunTimeCommand, + RunTimeCommand, +} from '@opentrons/shared-data' + +export const getPrimaryPipetteId = ( + pipettesById: { [id: string]: LoadedPipette }, + commands: RunTimeCommand[] +): string => { + if (Object.keys(pipettesById).length === 1) { + return Object.keys(pipettesById)[0] + } + + const leftPipetteId = commands.find( + (command: RunTimeCommand): command is LoadPipetteRunTimeCommand => + command.commandType === 'loadPipette' && command.params.mount === 'left' + )?.result?.pipetteId + const rightPipetteId = commands.find( + (command: RunTimeCommand): command is LoadPipetteRunTimeCommand => + command.commandType === 'loadPipette' && command.params.mount === 'right' + )?.result?.pipetteId + + if (leftPipetteId == null || rightPipetteId == null) { + throw new Error( + 'expected to find both left pipette and right pipette but could not' + ) + } + + const leftPipette = pipettesById[leftPipetteId] + const rightPipette = pipettesById[rightPipetteId] + + const leftPipetteSpecs = getPipetteNameSpecs(leftPipette.pipetteName) + const rightPipetteSpecs = getPipetteNameSpecs(rightPipette.pipetteName) + + if (leftPipetteSpecs == null) { + throw new Error( + `could not find pipette specs for ${String(leftPipette.pipetteName)}` + ) + } + if (rightPipetteSpecs == null) { + throw new Error( + `could not find pipette specs for ${String(rightPipette.pipetteName)}` + ) + } + + // prefer pipettes with fewer channels + if (leftPipetteSpecs.channels !== rightPipetteSpecs.channels) { + return leftPipetteSpecs.channels < rightPipetteSpecs.channels + ? leftPipetteId + : rightPipetteId + } + // prefer pipettes with smaller maxVolume + if (leftPipetteSpecs.maxVolume !== rightPipetteSpecs.maxVolume) { + return leftPipetteSpecs.maxVolume < rightPipetteSpecs.maxVolume + ? leftPipetteId + : rightPipetteId + } + + const leftPipetteGenerationCompare = leftPipetteSpecs.displayCategory.localeCompare( + rightPipetteSpecs.displayCategory + ) + // prefer new pipette models + if (leftPipetteGenerationCompare !== 0) { + return leftPipetteGenerationCompare > 0 ? leftPipetteId : rightPipetteId + } + // if all else is the same, prefer the left pipette + return leftPipetteId +} diff --git a/app/src/organisms/LabwarePositionCheck/utils/getProbeBasedLPCSteps.ts b/app/src/organisms/LabwarePositionCheck/utils/getProbeBasedLPCSteps.ts new file mode 100644 index 00000000000..d584399457d --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/utils/getProbeBasedLPCSteps.ts @@ -0,0 +1,90 @@ +import { isEqual } from 'lodash' +import { SECTIONS } from '../constants' +import { getLabwareDefURI, getPipetteNameSpecs } from '@opentrons/shared-data' +import { getLabwareLocationCombos } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' +import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' + +import type { + CompletedProtocolAnalysis, + LoadedPipette, +} from '@opentrons/shared-data' +import type { LabwarePositionCheckStep, CheckPositionsStep } from '../types' +import type { LabwareLocationCombo } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' + +function getPrimaryPipetteId(pipettes: LoadedPipette[]): string { + if (pipettes.length < 1) { + throw new Error( + 'no pipettes in protocol, cannot determine primary pipette for LPC' + ) + } + return pipettes.reduce((acc, pip) => { + return (getPipetteNameSpecs(acc.pipetteName)?.channels ?? 0) > + (getPipetteNameSpecs(pip.pipetteName)?.channels ?? 0) + ? pip + : acc + }, pipettes[0]).id +} + +export const getProbeBasedLPCSteps = ( + protocolData: CompletedProtocolAnalysis +): LabwarePositionCheckStep[] => { + return [ + { section: SECTIONS.BEFORE_BEGINNING }, + { + section: SECTIONS.ATTACH_PROBE, + pipetteId: getPrimaryPipetteId(protocolData.pipettes), + }, + ...getAllCheckSectionSteps(protocolData), + { + section: SECTIONS.DETACH_PROBE, + pipetteId: getPrimaryPipetteId(protocolData.pipettes), + }, + { section: SECTIONS.RESULTS_SUMMARY }, + ] +} + +function getAllCheckSectionSteps( + protocolData: CompletedProtocolAnalysis +): CheckPositionsStep[] { + const { pipettes, commands, labware, modules = [] } = protocolData + const labwareLocationCombos = getLabwareLocationCombos( + commands, + labware, + modules + ) + const labwareDefinitions = getLabwareDefinitionsFromCommands(commands) + const labwareLocations = labwareLocationCombos.reduce( + (acc, labwareLocationCombo) => { + const labwareDef = labwareDefinitions.find( + def => getLabwareDefURI(def) === labwareLocationCombo.definitionUri + ) + if ( + (labwareDef?.allowedRoles ?? []).includes('adapter') || + (labwareDef?.allowedRoles ?? []).includes('lid') + ) { + return acc + } + // remove duplicate definitionUri in same location + const comboAlreadyExists = acc.some( + accLocationCombo => + labwareLocationCombo.definitionUri === + accLocationCombo.definitionUri && + isEqual(labwareLocationCombo.location, accLocationCombo.location) + ) + return comboAlreadyExists ? acc : [...acc, labwareLocationCombo] + }, + [] + ) + + return labwareLocations.map( + ({ location, labwareId, moduleId, adapterId, definitionUri }) => ({ + section: SECTIONS.CHECK_POSITIONS, + labwareId: labwareId, + pipetteId: getPrimaryPipetteId(pipettes), + location, + moduleId, + adapterId, + definitionUri: definitionUri, + }) + ) +} diff --git a/app/src/organisms/LabwarePositionCheck/utils/getTipBasedLPCSteps.ts b/app/src/organisms/LabwarePositionCheck/utils/getTipBasedLPCSteps.ts new file mode 100644 index 00000000000..2aba09b84f8 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/utils/getTipBasedLPCSteps.ts @@ -0,0 +1,198 @@ +import { isEqual } from 'lodash' +import { SECTIONS } from '../constants' +import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' +import { + getLabwareDefURI, + getIsTiprack, + FIXED_TRASH_ID, +} from '@opentrons/shared-data' +import { getLabwareLocationCombos } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' + +import type { + LabwarePositionCheckStep, + CheckTipRacksStep, + PickUpTipStep, + CheckLabwareStep, + ReturnTipStep, +} from '../types' +import type { + RunTimeCommand, + ProtocolAnalysisOutput, + PickUpTipRunTimeCommand, +} from '@opentrons/shared-data' +import type { LabwareLocationCombo } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' + +interface LPCArgs { + primaryPipetteId: string + secondaryPipetteId: string | null + labware: ProtocolAnalysisOutput['labware'] + modules: ProtocolAnalysisOutput['modules'] + commands: RunTimeCommand[] +} + +export const getTipBasedLPCSteps = ( + args: LPCArgs +): LabwarePositionCheckStep[] => { + const checkTipRacksSectionSteps = getCheckTipRackSectionSteps(args) + if (checkTipRacksSectionSteps.length < 1) return [] + const allButLastTiprackCheckSteps = checkTipRacksSectionSteps.slice( + 0, + checkTipRacksSectionSteps.length - 1 + ) + const lastTiprackCheckStep = + checkTipRacksSectionSteps[checkTipRacksSectionSteps.length - 1] + + const pickUpTipSectionStep: PickUpTipStep = { + section: SECTIONS.PICK_UP_TIP, + labwareId: lastTiprackCheckStep.labwareId, + pipetteId: lastTiprackCheckStep.pipetteId, + location: lastTiprackCheckStep.location, + adapterId: lastTiprackCheckStep.adapterId, + definitionUri: lastTiprackCheckStep.definitionUri, + } + const checkLabwareSectionSteps = getCheckLabwareSectionSteps(args) + + const returnTipSectionStep: ReturnTipStep = { + section: SECTIONS.RETURN_TIP, + labwareId: lastTiprackCheckStep.labwareId, + pipetteId: lastTiprackCheckStep.pipetteId, + location: lastTiprackCheckStep.location, + adapterId: lastTiprackCheckStep.adapterId, + definitionUri: lastTiprackCheckStep.definitionUri, + } + + return [ + { section: SECTIONS.BEFORE_BEGINNING }, + ...allButLastTiprackCheckSteps, + pickUpTipSectionStep, + ...checkLabwareSectionSteps, + returnTipSectionStep, + { section: SECTIONS.RESULTS_SUMMARY }, + ] +} + +function getCheckTipRackSectionSteps(args: LPCArgs): CheckTipRacksStep[] { + const { + secondaryPipetteId, + primaryPipetteId, + commands, + labware, + modules = [], + } = args + + const labwareLocationCombos = getLabwareLocationCombos( + commands, + labware, + modules + ) + const uniqPrimaryPipettePickUpTipCommands = commands.reduce< + PickUpTipRunTimeCommand[] + >((acc, command) => { + if ( + command.commandType === 'pickUpTip' && + command.params.pipetteId === primaryPipetteId && + !acc.some(c => c.params.labwareId === command.params.labwareId) + ) { + return [...acc, command] + } + return acc + }, []) + const onlySecondaryPipettePickUpTipCommands = commands.reduce< + PickUpTipRunTimeCommand[] + >((acc, command) => { + if ( + command.commandType === 'pickUpTip' && + command.params.pipetteId === secondaryPipetteId && + !uniqPrimaryPipettePickUpTipCommands.some( + c => c.params.labwareId === command.params.labwareId + ) && + !acc.some(c => c.params.labwareId === command.params.labwareId) + ) { + return [...acc, command] + } + return acc + }, []) + + return [ + ...onlySecondaryPipettePickUpTipCommands, + ...uniqPrimaryPipettePickUpTipCommands, + ].reduce((acc, { params }) => { + const labwareLocations = labwareLocationCombos.reduce< + LabwareLocationCombo[] + >((acc, labwareLocationCombo) => { + // remove labware that isn't accessed by a pickup tip command + if (labwareLocationCombo.labwareId !== params.labwareId) { + return acc + } + // remove duplicate definitionUri in same location + const comboAlreadyExists = acc.some( + accLocationCombo => + labwareLocationCombo.definitionUri === + accLocationCombo.definitionUri && + isEqual(labwareLocationCombo.location, accLocationCombo.location) + ) + return comboAlreadyExists ? acc : [...acc, labwareLocationCombo] + }, []) + + return [ + ...acc, + ...labwareLocations.map(({ location, adapterId, definitionUri }) => ({ + section: SECTIONS.CHECK_TIP_RACKS, + labwareId: params.labwareId, + pipetteId: params.pipetteId, + location, + adapterId, + definitionUri: definitionUri, + })), + ] + }, []) +} + +function getCheckLabwareSectionSteps(args: LPCArgs): CheckLabwareStep[] { + const { labware, modules, commands, primaryPipetteId } = args + const labwareDefinitions = getLabwareDefinitionsFromCommands(commands) + + const deDupedLabwareLocationCombos = getLabwareLocationCombos( + commands, + labware, + modules + ).reduce((acc, labwareLocationCombo) => { + const labwareDef = labwareDefinitions.find( + def => getLabwareDefURI(def) === labwareLocationCombo.definitionUri + ) + if (labwareLocationCombo.labwareId === FIXED_TRASH_ID) return acc + if (labwareDef == null) { + throw new Error( + `could not find labware definition within protocol with uri: ${labwareLocationCombo.definitionUri}` + ) + } + const isTiprack = getIsTiprack(labwareDef) + const adapter = (labwareDef?.allowedRoles ?? []).includes('adapter') + if (isTiprack || adapter) return acc // skip any labware that is a tiprack or adapter + + const comboAlreadyExists = acc.some( + accLocationCombo => + labwareLocationCombo.definitionUri === accLocationCombo.definitionUri && + isEqual(labwareLocationCombo.location, accLocationCombo.location) + ) + return comboAlreadyExists ? acc : [...acc, labwareLocationCombo] + }, []) + + return deDupedLabwareLocationCombos.reduce( + (acc, { labwareId, location, moduleId, adapterId, definitionUri }) => { + return [ + ...acc, + { + section: SECTIONS.CHECK_LABWARE, + labwareId, + pipetteId: primaryPipetteId, + location, + moduleId, + adapterId, + definitionUri, + }, + ] + }, + [] + ) +} diff --git a/app/src/organisms/LabwarePositionCheck/utils/labware.ts b/app/src/organisms/LabwarePositionCheck/utils/labware.ts new file mode 100644 index 00000000000..70096061c33 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/utils/labware.ts @@ -0,0 +1,260 @@ +import reduce from 'lodash/reduce' +import { + getIsTiprack, + getTiprackVolume, + getLabwareDefURI, +} from '@opentrons/shared-data' +import { getModuleInitialLoadInfo } from '/app/transformations/commands' +import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' +import type { + CompletedProtocolAnalysis, + LabwareDefinition2, + LabwareLocation, + LoadLabwareRunTimeCommand, + PickUpTipRunTimeCommand, + ProtocolAnalysisOutput, + RunTimeCommand, +} from '@opentrons/shared-data' +import type { LabwareToOrder } from '../types' + +export const tipRackOrderSort = ( + tiprack1: LabwareToOrder, + tiprack2: LabwareToOrder +): -1 | 1 => { + const tiprack1Volume = getTiprackVolume(tiprack1.definition) + const tiprack2Volume = getTiprackVolume(tiprack2.definition) + + if (tiprack1Volume !== tiprack2Volume) { + return tiprack1Volume > tiprack2Volume ? -1 : 1 + } + return orderBySlot(tiprack1, tiprack2) +} + +export const orderBySlot = ( + labware1: LabwareToOrder, + labware2: LabwareToOrder +): -1 | 1 => { + if (labware1.slot < labware2.slot) { + return -1 + } + return 1 +} + +interface Labware { + [labwareId: string]: { + definitionId: string + displayName?: string + } +} + +export const getTiprackIdsInOrder = ( + labware: Labware, + labwareDefinitions: Record, + commands: RunTimeCommand[] +): string[] => { + const unorderedTipracks = reduce( + labware, + (tipracks, currentLabware, labwareId) => { + const labwareDef = labwareDefinitions[currentLabware.definitionId] + const isTiprack = getIsTiprack(labwareDef) + if (isTiprack) { + const tipRackLocations = getAllUniqLocationsForLabware( + labwareId, + commands + ) + return [ + ...tipracks, + ...tipRackLocations.map(loc => ({ + definition: labwareDef, + labwareId: labwareId, + slot: loc !== 'offDeck' && 'slotName' in loc ? loc.slotName : '', + })), + ] + } + return tipracks + }, + [] + ) + const orderedTiprackIds = unorderedTipracks + .sort(tipRackOrderSort) + .map(({ labwareId }) => labwareId) + + return orderedTiprackIds +} + +export const getAllTipracksIdsThatPipetteUsesInOrder = ( + pipetteId: string, + commands: RunTimeCommand[], + labware: ProtocolAnalysisOutput['labware'] +): string[] => { + const pickUpTipCommandsWithPipette: PickUpTipRunTimeCommand[] = commands.filter( + (command): command is PickUpTipRunTimeCommand => + command.commandType === 'pickUpTip' && + command.params.pipetteId === pipetteId + ) + + const tipRackIdsVisited = pickUpTipCommandsWithPipette.reduce( + (visitedIds, command) => { + const tipRackId = command.params.labwareId + return visitedIds.includes(tipRackId) + ? visitedIds + : [...visitedIds, tipRackId] + }, + [] + ) + const labwareDefinitions = getLabwareDefinitionsFromCommands(commands) + + const orderedTiprackIds = tipRackIdsVisited + .reduce((acc, tipRackId) => { + const tiprackEntity = labware.find(l => l.id === tipRackId) + const definition = labwareDefinitions.find( + def => getLabwareDefURI(def) === tiprackEntity?.definitionUri + ) + const tipRackLocations = getAllUniqLocationsForLabware( + tipRackId, + commands + ) + + if (definition == null) { + throw new Error( + `could not find labware definition within protocol with uri: ${tiprackEntity?.definitionUri}` + ) + } + return [ + ...acc, + ...tipRackLocations.map(loc => ({ + labwareId: tipRackId, + definition, + slot: loc !== 'offDeck' && 'slotName' in loc ? loc.slotName : '', + })), + ] + }, []) + .sort(tipRackOrderSort) + .map(({ labwareId }) => labwareId) + + return orderedTiprackIds +} + +export const getLabwareIdsInOrder = ( + labware: ProtocolAnalysisOutput['labware'], + commands: RunTimeCommand[] +): string[] => { + const labwareDefinitions = getLabwareDefinitionsFromCommands(commands) + + const unorderedLabware = labware.reduce( + (acc, currentLabware) => { + const labwareDef = labwareDefinitions.find( + def => getLabwareDefURI(def) === currentLabware.definitionUri + ) + if (labwareDef == null) { + throw new Error( + `could not find labware definition within protocol with uri: ${currentLabware.definitionUri}` + ) + } + // skip any labware that is a tip rack or trash + const isTiprack = getIsTiprack(labwareDef) + const isTrash = labwareDef.parameters.format === 'trash' + if (isTiprack || isTrash) return acc + + const labwareLocations = getAllUniqLocationsForLabware( + currentLabware.id, + commands + ) + return [ + ...acc, + ...labwareLocations.reduce((innerAcc, loc) => { + let slot = '' + if (loc === 'offDeck') { + slot = 'offDeck' + } else if ('moduleId' in loc) { + slot = getModuleInitialLoadInfo(loc.moduleId, commands).location + .slotName + } else if ('labwareId' in loc) { + const matchingAdapter = commands.find( + (command): command is LoadLabwareRunTimeCommand => + command.commandType === 'loadLabware' && + command.result?.labwareId === loc.labwareId + ) + const adapterLocation = matchingAdapter?.params.location + if (adapterLocation === 'offDeck') { + slot = 'offDeck' + } else if ( + adapterLocation != null && + 'slotName' in adapterLocation + ) { + slot = adapterLocation.slotName + } else if ( + adapterLocation != null && + 'moduleId' in adapterLocation + ) { + slot = getModuleInitialLoadInfo( + adapterLocation.moduleId, + commands + ).location.slotName + } + } else { + slot = + 'addressableAreaName' in loc + ? loc.addressableAreaName + : loc.slotName + } + return [ + ...innerAcc, + { definition: labwareDef, labwareId: currentLabware.id, slot }, + ] + }, []), + ] + }, + [] + ) + const orderedLabwareIds = unorderedLabware + .sort(orderBySlot) + .map(({ labwareId }) => labwareId) + + return orderedLabwareIds +} + +const TRASH_ID = 'fixedTrash' + +export const getAllUniqLocationsForLabware = ( + labwareId: string, + commands: RunTimeCommand[] +): LabwareLocation[] => { + if (labwareId === TRASH_ID) { + return [{ slotName: '12' }] + } + const labwareLocation = commands.reduce( + (acc, command: RunTimeCommand) => { + if ( + command.commandType === 'loadLabware' && + command.result?.definition.parameters.format !== 'trash' && + command.result?.labwareId === labwareId + ) { + const { location } = command.params + return [...acc, location] + } + return acc + }, + [] + ) + + if (labwareLocation.length === 0) { + throw new Error( + 'expected to be able to find at least one labware location, but could not' + ) + } + + return labwareLocation +} + +export function getLabwareDef( + labwareId: string, + protocolData: CompletedProtocolAnalysis +): LabwareDefinition2 | undefined { + const labwareDefUri = protocolData.labware.find(l => l.id === labwareId) + ?.definitionUri + const labwareDefinitions = getLabwareDefinitionsFromCommands( + protocolData.commands + ) + return labwareDefinitions.find(def => getLabwareDefURI(def) === labwareDefUri) +} From 067e2b35eb68c9c5b8ab16d9970e47c934687a30 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Wed, 18 Dec 2024 11:02:22 -0500 Subject: [PATCH 02/33] refactor(app): wire up the non-legacy LPC flows behind FFs --- .../ProtocolRun/SetupLabwarePositionCheck/index.tsx | 7 +++++-- .../LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx | 8 ++++---- app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx | 4 ++-- .../ODD/ProtocolSetup/ProtocolSetupOffsets/index.tsx | 6 ++---- app/src/pages/ODD/ProtocolSetup/index.tsx | 7 ++++--- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx index a0322a4110d..15b92524a98 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx @@ -29,6 +29,8 @@ import { useLPCDisabledReason, } from '/app/resources/runs' import { useRobotType } from '/app/redux-resources/robots' +import { useLaunchLPC } from '/app/organisms/LabwarePositionCheck/useLaunchLPC' + import type { LabwareOffset } from '@opentrons/api-client' interface SetupLabwarePositionCheckProps { @@ -100,6 +102,7 @@ export function SetupLabwarePositionCheck( robotType, protocolName ) + const { launchLPC, LPCWizard } = useLaunchLPC(runId, robotType, protocolName) const nonIdentityOffsets = getLatestCurrentOffsets(sortedOffsets) @@ -155,7 +158,7 @@ export function SetupLabwarePositionCheck( { - isNewLpc ? (() => null)() : launchLegacyLPC() + isNewLpc ? launchLPC() : launchLegacyLPC() setIsShowingLPCSuccessToast(false) }} id="LabwareSetup_checkLabwarePositionsButton" @@ -170,7 +173,7 @@ export function SetupLabwarePositionCheck( ) : null}
- {isNewLpc ? null : LegacyLPCWizard} + {isNewLpc ? LPCWizard : LegacyLPCWizard} ) } diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx index a16cfc4a8a4..fb983097d01 100644 --- a/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx +++ b/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx @@ -153,7 +153,7 @@ describe('useLaunchLPC hook', () => { () => useLaunchLPC(MOCK_RUN_ID, FLEX_ROBOT_TYPE), { wrapper } ) - expect(result.current.launchLPCWizard).toEqual(null) + expect(result.current.LPCWizard).toEqual(null) }) it('returns creates maintenance run with current offsets and definitions when create callback is called, closes and deletes when exit is clicked', async () => { @@ -184,9 +184,9 @@ describe('useLaunchLPC hook', () => { }) await waitFor(() => { - expect(result.current.launchLPCWizard).not.toBeNull() + expect(result.current.LPCWizard).not.toBeNull() }) - renderWithProviders(result.current.launchLPCWizard ?? <>) + renderWithProviders(result.current.LPCWizard ?? <>) fireEvent.click(screen.getByText('exit')) expect(mockDeleteMaintenanceRun).toHaveBeenCalledWith( MOCK_MAINTENANCE_RUN_ID, @@ -194,6 +194,6 @@ describe('useLaunchLPC hook', () => { onSettled: expect.any(Function), } ) - expect(result.current.launchLPCWizard).toBeNull() + expect(result.current.LPCWizard).toBeNull() }) }) diff --git a/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx b/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx index 1ac2392b370..18c906d2998 100644 --- a/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx +++ b/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx @@ -19,7 +19,7 @@ export function useLaunchLPC( runId: string, robotType: RobotType, protocolName?: string -): { launchLPC: () => void; launchLPCWizard: JSX.Element | null } { +): { launchLPC: () => void; LPCWizard: JSX.Element | null } { const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) const { createTargetedMaintenanceRun, @@ -71,7 +71,7 @@ export function useLaunchLPC( setMaintenanceRunId(maintenanceRun.data.id) }) ), - launchLPCWizard: + LPCWizard: maintenanceRunId != null ? ( void - isNewLpc: boolean } export function ProtocolSetupOffsets({ @@ -43,7 +42,6 @@ export function ProtocolSetupOffsets({ launchLPC, lpcDisabledReason, LPCWizard, - isNewLpc, }: ProtocolSetupOffsetsProps): JSX.Element { const { t } = useTranslation('protocol_setup') const { makeSnackbar } = useToaster() @@ -77,7 +75,7 @@ export function ProtocolSetupOffsets({ const nonIdentityOffsets = getLatestCurrentOffsets(sortedOffsets) return ( <> - {isNewLpc ? null : LPCWizard} + {LPCWizard} {LPCWizard == null && ( <> null)() : launchLPC() + launchLPC() } }} /> diff --git a/app/src/pages/ODD/ProtocolSetup/index.tsx b/app/src/pages/ODD/ProtocolSetup/index.tsx index 1df659c633b..bb3122bf993 100644 --- a/app/src/pages/ODD/ProtocolSetup/index.tsx +++ b/app/src/pages/ODD/ProtocolSetup/index.tsx @@ -88,6 +88,7 @@ import { useRequiredProtocolHardwareFromAnalysis, useMissingProtocolHardwareFromAnalysis, } from '/app/transformations/commands' +import { useLaunchLPC } from '/app/organisms/LabwarePositionCheck/useLaunchLPC' import type { Dispatch, SetStateAction } from 'react' import type { Run } from '@opentrons/api-client' @@ -741,6 +742,7 @@ export function ProtocolSetup(): JSX.Element { robotType, protocolName ) + const { launchLPC, LPCWizard } = useLaunchLPC(runId, robotType, protocolName) const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) const robotAnalyticsData = useRobotAnalyticsData(robotName) @@ -824,11 +826,10 @@ export function ProtocolSetup(): JSX.Element { runId={runId} setSetupScreen={setSetupScreen} lpcDisabledReason={lpcDisabledReason} - launchLPC={launchLegacyLPC} - LPCWizard={LegacyLPCWizard} + launchLPC={isNewLpc ? launchLPC : launchLegacyLPC} + LPCWizard={isNewLpc ? LPCWizard : LegacyLPCWizard} isConfirmed={offsetsConfirmed} setIsConfirmed={setOffsetsConfirmed} - isNewLpc={isNewLpc} /> ), labware: ( From c8c3835b8e9e152cb3afc6230366ab57fb9c656d Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Wed, 18 Dec 2024 11:27:34 -0500 Subject: [PATCH 03/33] merge LPC component and hook into same file --- .../SetupLabwarePositionCheck/index.tsx | 4 +- .../{useLaunchLPC.tsx => LPCFlows.tsx} | 31 +++++- .../__tests__/useLaunchLPC.test.tsx | 6 +- .../organisms/LabwarePositionCheck/index.ts | 1 + .../organisms/LabwarePositionCheck/index.tsx | 104 ------------------ app/src/pages/ODD/ProtocolSetup/index.tsx | 4 +- 6 files changed, 35 insertions(+), 115 deletions(-) rename app/src/organisms/LabwarePositionCheck/{useLaunchLPC.tsx => LPCFlows.tsx} (77%) create mode 100644 app/src/organisms/LabwarePositionCheck/index.ts delete mode 100644 app/src/organisms/LabwarePositionCheck/index.tsx diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx index 15b92524a98..6400b598ad6 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx @@ -29,7 +29,7 @@ import { useLPCDisabledReason, } from '/app/resources/runs' import { useRobotType } from '/app/redux-resources/robots' -import { useLaunchLPC } from '/app/organisms/LabwarePositionCheck/useLaunchLPC' +import { useLPCFlows } from '/app/src/organisms/LabwarePositionCheck/useLPCFlows' import type { LabwareOffset } from '@opentrons/api-client' @@ -102,7 +102,7 @@ export function SetupLabwarePositionCheck( robotType, protocolName ) - const { launchLPC, LPCWizard } = useLaunchLPC(runId, robotType, protocolName) + const { launchLPC, LPCWizard } = useLPCFlows(runId, robotType, protocolName) const nonIdentityOffsets = getLatestCurrentOffsets(sortedOffsets) diff --git a/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx b/app/src/organisms/LabwarePositionCheck/LPCFlows.tsx similarity index 77% rename from app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx rename to app/src/organisms/LabwarePositionCheck/LPCFlows.tsx index 18c906d2998..3e7dadec68c 100644 --- a/app/src/organisms/LabwarePositionCheck/useLaunchLPC.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCFlows.tsx @@ -10,12 +10,18 @@ import { useNotifyRunQuery, useMostRecentCompletedAnalysis, } from '/app/resources/runs' -import { LabwarePositionCheck } from '.' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' +import { LabwarePositionCheckComponent } from './LabwarePositionCheckComponent' -import type { RobotType } from '@opentrons/shared-data' +import type { + RobotType, + CompletedProtocolAnalysis, +} from '@opentrons/shared-data' +import type { LabwareOffset } from '@opentrons/api-client' -export function useLaunchLPC( +//TOME TODO: This needds to take all props that get passed to LPC, just like in ER. + +export function useLPCFlows( runId: string, robotType: RobotType, protocolName?: string @@ -73,7 +79,7 @@ export function useLaunchLPC( ), LPCWizard: maintenanceRunId != null ? ( - void + runId: string + maintenanceRunId: string + robotType: RobotType + existingOffsets: LabwareOffset[] + mostRecentAnalysis: CompletedProtocolAnalysis | null + protocolName: string + caughtError?: Error + setMaintenanceRunId: (id: string | null) => void + isDeletingMaintenanceRun: boolean +} + +function LPCFlows(props: LPCFlowsProps): JSX.Element { + return +} diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx index fb983097d01..b998e782cf9 100644 --- a/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx +++ b/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx @@ -24,7 +24,7 @@ import { useNotifyRunQuery, useMostRecentCompletedAnalysis, } from '/app/resources/runs' -import { useLaunchLPC } from '../useLaunchLPC' +import { useLPCFlows } from '../useLPCFlows' import { LabwarePositionCheck } from '..' import type { Mock } from 'vitest' @@ -150,7 +150,7 @@ describe('useLaunchLPC hook', () => { it('returns and no wizard by default', () => { const { result } = renderHook( - () => useLaunchLPC(MOCK_RUN_ID, FLEX_ROBOT_TYPE), + () => useLPCFlows(MOCK_RUN_ID, FLEX_ROBOT_TYPE), { wrapper } ) expect(result.current.LPCWizard).toEqual(null) @@ -158,7 +158,7 @@ describe('useLaunchLPC hook', () => { it('returns creates maintenance run with current offsets and definitions when create callback is called, closes and deletes when exit is clicked', async () => { const { result } = renderHook( - () => useLaunchLPC(MOCK_RUN_ID, FLEX_ROBOT_TYPE), + () => useLPCFlows(MOCK_RUN_ID, FLEX_ROBOT_TYPE), { wrapper } ) act(() => { diff --git a/app/src/organisms/LabwarePositionCheck/index.ts b/app/src/organisms/LabwarePositionCheck/index.ts new file mode 100644 index 00000000000..d595197b5f1 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/index.ts @@ -0,0 +1 @@ +export * from './LPCFlows' diff --git a/app/src/organisms/LabwarePositionCheck/index.tsx b/app/src/organisms/LabwarePositionCheck/index.tsx deleted file mode 100644 index e96191c584e..00000000000 --- a/app/src/organisms/LabwarePositionCheck/index.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { Component } from 'react' -import { useLogger } from '../../logger' -import { LabwarePositionCheckComponent } from './LabwarePositionCheckComponent' -import { FatalErrorModal } from './FatalErrorModal' -import { getIsOnDevice } from '/app/redux/config' -import { useSelector } from 'react-redux' - -import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' - -import type { ErrorInfo, ReactNode } from 'react' -import type { - CompletedProtocolAnalysis, - RobotType, -} from '@opentrons/shared-data' -import type { LabwareOffset } from '@opentrons/api-client' - -interface LabwarePositionCheckModalProps { - onCloseClick: () => void - runId: string - maintenanceRunId: string - robotType: RobotType - existingOffsets: LabwareOffset[] - mostRecentAnalysis: CompletedProtocolAnalysis | null - protocolName: string - caughtError?: Error - setMaintenanceRunId: (id: string | null) => void - isDeletingMaintenanceRun: boolean -} - -// We explicitly wrap LabwarePositionCheckComponent in an ErrorBoundary because an error might occur while pulling in -// the component's dependencies (like useLabwarePositionCheck). If we wrapped the contents of LabwarePositionCheckComponent -// in an ErrorBoundary as part of its return value (render), an error could occur before this point, meaning the error boundary -// would never get invoked -export const LabwarePositionCheck = ( - props: LabwarePositionCheckModalProps -): JSX.Element => { - const logger = useLogger(new URL('', import.meta.url).pathname) - const isOnDevice = useSelector(getIsOnDevice) - return ( - - - - ) -} - -interface ErrorBoundaryProps { - children: ReactNode - onClose: () => void - shouldUseMetalProbe: boolean - logger: ReturnType - ErrorComponent: (props: { - errorMessage: string - shouldUseMetalProbe: boolean - onClose: () => void - isOnDevice: boolean - }) => JSX.Element - isOnDevice: boolean -} -class ErrorBoundary extends Component< - ErrorBoundaryProps, - { error: Error | null } -> { - constructor(props: ErrorBoundaryProps) { - super(props) - this.state = { error: null } - } - - componentDidCatch(error: Error, errorInfo: ErrorInfo): void { - this.props.logger.error(`LPC error message: ${error.message}`) - this.props.logger.error( - `LPC error component stack: ${errorInfo.componentStack}` - ) - this.setState({ - error, - }) - } - - render(): ErrorBoundaryProps['children'] | JSX.Element { - const { - ErrorComponent, - children, - shouldUseMetalProbe, - isOnDevice, - } = this.props - const { error } = this.state - if (error != null) - return ( - - ) - // Normally, just render children - return children - } -} diff --git a/app/src/pages/ODD/ProtocolSetup/index.tsx b/app/src/pages/ODD/ProtocolSetup/index.tsx index bb3122bf993..0684f967bc0 100644 --- a/app/src/pages/ODD/ProtocolSetup/index.tsx +++ b/app/src/pages/ODD/ProtocolSetup/index.tsx @@ -88,7 +88,7 @@ import { useRequiredProtocolHardwareFromAnalysis, useMissingProtocolHardwareFromAnalysis, } from '/app/transformations/commands' -import { useLaunchLPC } from '/app/organisms/LabwarePositionCheck/useLaunchLPC' +import { useLPCFlows } from '/app/src/organisms/LabwarePositionCheck/useLPCFlows' import type { Dispatch, SetStateAction } from 'react' import type { Run } from '@opentrons/api-client' @@ -742,7 +742,7 @@ export function ProtocolSetup(): JSX.Element { robotType, protocolName ) - const { launchLPC, LPCWizard } = useLaunchLPC(runId, robotType, protocolName) + const { launchLPC, LPCWizard } = useLPCFlows(runId, robotType, protocolName) const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) const robotAnalyticsData = useRobotAnalyticsData(robotName) From 5c4db0b42bdaf480ed11bf03bb77dd72fba28049 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Thu, 2 Jan 2025 09:51:54 -0500 Subject: [PATCH 04/33] refactor(app): create flex & ot2 specific lpc containers --- .../Desktop/ChooseProtocolSlideout/index.tsx | 4 +- .../index.tsx | 4 +- .../Devices/ProtocolRun/ProtocolRunSetup.tsx | 4 +- .../SetupLabwarePositionCheck.test.tsx | 2 +- .../SetupLabwarePositionCheck/index.tsx | 17 +- .../LabwarePositionCheck/LPCFlows.tsx | 162 ++-- .../LPCWizardContainer.tsx | 31 + ...onCheckComponent.tsx => LPCWizardFlex.tsx} | 37 +- .../__tests__/CheckItem.test.tsx | 702 ------------------ .../__tests__/ExitConfirmation.test.tsx | 55 -- .../__tests__/PickUpTip.test.tsx | 466 ------------ .../__tests__/ResultsSummary.test.tsx | 91 --- .../__tests__/ReturnTip.test.tsx | 258 ------- .../__tests__/RobotMotionLoader.test.tsx | 20 - .../__tests__/TipConfirmation.test.tsx | 39 - .../__tests__/useLaunchLPC.test.tsx | 199 ----- .../LabwarePositionCheck/components/index.ts | 2 + .../LabwarePositionCheck/constants.ts | 11 - .../LabwarePositionCheck/constants/index.ts | 1 + .../LabwarePositionCheck/constants/routing.ts | 23 + .../LabwarePositionCheck/hooks/index.ts | 0 .../organisms/LabwarePositionCheck/types.ts | 20 +- .../utils/getProbeBasedLPCSteps.ts | 12 +- .../utils/getTipBasedLPCSteps.ts | 16 +- app/src/pages/ODD/ProtocolSetup/index.tsx | 22 +- .../RobotSettingsList.tsx | 4 +- 26 files changed, 237 insertions(+), 1965 deletions(-) create mode 100644 app/src/organisms/LabwarePositionCheck/LPCWizardContainer.tsx rename app/src/organisms/LabwarePositionCheck/{LabwarePositionCheckComponent.tsx => LPCWizardFlex.tsx} (95%) delete mode 100644 app/src/organisms/LabwarePositionCheck/__tests__/CheckItem.test.tsx delete mode 100644 app/src/organisms/LabwarePositionCheck/__tests__/ExitConfirmation.test.tsx delete mode 100644 app/src/organisms/LabwarePositionCheck/__tests__/PickUpTip.test.tsx delete mode 100644 app/src/organisms/LabwarePositionCheck/__tests__/ResultsSummary.test.tsx delete mode 100644 app/src/organisms/LabwarePositionCheck/__tests__/ReturnTip.test.tsx delete mode 100644 app/src/organisms/LabwarePositionCheck/__tests__/RobotMotionLoader.test.tsx delete mode 100644 app/src/organisms/LabwarePositionCheck/__tests__/TipConfirmation.test.tsx delete mode 100644 app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/components/index.ts delete mode 100644 app/src/organisms/LabwarePositionCheck/constants.ts create mode 100644 app/src/organisms/LabwarePositionCheck/constants/index.ts create mode 100644 app/src/organisms/LabwarePositionCheck/constants/routing.ts create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/index.ts diff --git a/app/src/organisms/Desktop/ChooseProtocolSlideout/index.tsx b/app/src/organisms/Desktop/ChooseProtocolSlideout/index.tsx index 9368fb1b697..829a311dc87 100644 --- a/app/src/organisms/Desktop/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/Desktop/ChooseProtocolSlideout/index.tsx @@ -107,7 +107,7 @@ export function ChooseProtocolSlideoutComponent( const { robot, showSlideout, onCloseClick } = props const { name } = robot - const isNewLpc = useFeatureFlag('lpcRedesign') + const isNewLPC = useFeatureFlag('lpcRedesign') const [ selectedProtocol, @@ -655,7 +655,7 @@ export function ChooseProtocolSlideoutComponent( } > {currentPage === 1 - ? !isNewLpc && ( + ? !isNewLPC && ( (true) const { protocolKey, @@ -221,7 +221,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent( ) - const offsetsComponent = isNewLpc ? null : ( + const offsetsComponent = isNewLPC ? null : ( ), description: t('labware_position_check_step_description'), diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx index 67d33d6c982..6b0b3d5ad20 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx @@ -51,7 +51,7 @@ const render = () => { setOffsetsConfirmed={confirmOffsets} robotName={ROBOT_NAME} runId={RUN_ID} - isNewLpc={false} + isNewLPC={false} /> , { diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx index 6400b598ad6..02648343551 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/index.tsx @@ -29,7 +29,7 @@ import { useLPCDisabledReason, } from '/app/resources/runs' import { useRobotType } from '/app/redux-resources/robots' -import { useLPCFlows } from '/app/src/organisms/LabwarePositionCheck/useLPCFlows' +import { useLPCFlows, LPCFlows } from '/app/organisms/LabwarePositionCheck' import type { LabwareOffset } from '@opentrons/api-client' @@ -38,7 +38,7 @@ interface SetupLabwarePositionCheckProps { setOffsetsConfirmed: (confirmed: boolean) => void robotName: string runId: string - isNewLpc: boolean + isNewLPC: boolean } export function SetupLabwarePositionCheck( @@ -49,7 +49,7 @@ export function SetupLabwarePositionCheck( runId, setOffsetsConfirmed, offsetsConfirmed, - isNewLpc, + isNewLPC, } = props const { t, i18n } = useTranslation('protocol_setup') @@ -102,7 +102,11 @@ export function SetupLabwarePositionCheck( robotType, protocolName ) - const { launchLPC, LPCWizard } = useLPCFlows(runId, robotType, protocolName) + const { launchLPC, lpcProps, showLPC } = useLPCFlows({ + runId, + robotType, + protocolName, + }) const nonIdentityOffsets = getLatestCurrentOffsets(sortedOffsets) @@ -158,7 +162,7 @@ export function SetupLabwarePositionCheck( { - isNewLpc ? launchLPC() : launchLegacyLPC() + isNewLPC ? launchLPC() : launchLegacyLPC() setIsShowingLPCSuccessToast(false) }} id="LabwareSetup_checkLabwarePositionsButton" @@ -173,7 +177,8 @@ export function SetupLabwarePositionCheck( ) : null} - {isNewLpc ? LPCWizard : LegacyLPCWizard} + {isNewLPC ? null : LegacyLPCWizard} + {showLPC && } ) } diff --git a/app/src/organisms/LabwarePositionCheck/LPCFlows.tsx b/app/src/organisms/LabwarePositionCheck/LPCFlows.tsx index 3e7dadec68c..4c958fc89c6 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCFlows.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCFlows.tsx @@ -11,7 +11,7 @@ import { useMostRecentCompletedAnalysis, } from '/app/resources/runs' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' -import { LabwarePositionCheckComponent } from './LabwarePositionCheckComponent' +import { LPCWizardContainer } from './LPCWizardContainer' import type { RobotType, @@ -19,94 +19,140 @@ import type { } from '@opentrons/shared-data' import type { LabwareOffset } from '@opentrons/api-client' -//TOME TODO: This needds to take all props that get passed to LPC, just like in ER. +interface UseLPCFlowsBase { + showLPC: boolean + lpcProps: LPCFlowsProps | null + isLaunchingLPC: boolean + launchLPC: () => Promise +} +interface UseLPCFlowsIdle extends UseLPCFlowsBase { + showLPC: false + lpcProps: null +} +interface UseLPCFlowsLaunched extends UseLPCFlowsBase { + showLPC: true + lpcProps: LPCFlowsProps + isLaunchingLPC: false +} +export type UseLPCFlowsResult = UseLPCFlowsIdle | UseLPCFlowsLaunched + +export interface UseLPCFlowsProps { + runId: string + robotType: RobotType + protocolName: string | undefined +} + +export function useLPCFlows({ + runId, + robotType, + protocolName, +}: UseLPCFlowsProps): UseLPCFlowsResult { + const [maintenanceRunId, setMaintenanceRunId] = useState(null) + const [isLaunching, setIsLaunching] = useState(false) + const [hasCreatedLPCRun, setHasCreatedLPCRun] = useState(false) -export function useLPCFlows( - runId: string, - robotType: RobotType, - protocolName?: string -): { launchLPC: () => void; LPCWizard: JSX.Element | null } { - const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) const { createTargetedMaintenanceRun, } = useCreateTargetedMaintenanceRunMutation() + const { + createLabwareDefinition, + } = useCreateMaintenanceRunLabwareDefinitionMutation() const { deleteMaintenanceRun, isLoading: isDeletingMaintenanceRun, } = useDeleteMaintenanceRunMutation() + + const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) - const [maintenanceRunId, setMaintenanceRunId] = useState(null) + const currentOffsets = runRecord?.data?.labwareOffsets ?? [] - const { - createLabwareDefinition, - } = useCreateMaintenanceRunLabwareDefinitionMutation() const handleCloseLPC = (): void => { if (maintenanceRunId != null) { deleteMaintenanceRun(maintenanceRunId, { onSettled: () => { setMaintenanceRunId(null) + setHasCreatedLPCRun(false) }, }) } } - return { - launchLPC: () => - createTargetedMaintenanceRun({ - labwareOffsets: currentOffsets.map( - ({ vector, location, definitionUri }) => ({ - vector, - location, - definitionUri, - }) - ), - }).then(maintenanceRun => - // TODO(BC, 2023-05-15): replace this with a call to the protocol run's GET labware_definitions - // endpoint once it's made we should be adding the definitions to the maintenance run by - // reading from the current protocol run, and not from the analysis - Promise.all( - getLabwareDefinitionsFromCommands( - mostRecentAnalysis?.commands ?? [] - ).map(def => { - createLabwareDefinition({ - maintenanceRunId: maintenanceRun?.data?.id, - labwareDef: def, - }) - }) - ).then(() => { - setMaintenanceRunId(maintenanceRun.data.id) + + const launchLPC = (): Promise => { + setIsLaunching(true) + + return createTargetedMaintenanceRun({ + labwareOffsets: currentOffsets.map( + ({ vector, location, definitionUri }) => ({ + vector, + location, + definitionUri, }) ), - LPCWizard: - maintenanceRunId != null ? ( - - ) : null, + }).then(maintenanceRun => + // TOME: TODO: Swap this out with the nifty new hook. + + // TODO(BC, 2023-05-15): replace this with a call to the protocol run's GET labware_definitions + // endpoint once it's made we should be adding the definitions to the maintenance run by + // reading from the current protocol run, and not from the analysis + Promise.all( + getLabwareDefinitionsFromCommands( + mostRecentAnalysis?.commands ?? [] + ).map(def => { + createLabwareDefinition({ + maintenanceRunId: maintenanceRun?.data?.id, + labwareDef: def, + }) + }) + ).then(() => { + setMaintenanceRunId(maintenanceRun.data.id) + setIsLaunching(false) + setHasCreatedLPCRun(true) + }) + ) } + + const showLPC = + hasCreatedLPCRun && maintenanceRunId != null && protocolName != null + + return showLPC + ? { + launchLPC, + isLaunchingLPC: false, + showLPC, + lpcProps: { + onCloseClick: handleCloseLPC, + runId, + robotType, + existingOffsets: currentOffsets, + mostRecentAnalysis, + protocolName, + maintenanceRunUtils: { + maintenanceRunId, + setMaintenanceRunId, + isDeletingMaintenanceRun, + }, + }, + } + : { launchLPC, isLaunchingLPC: isLaunching, lpcProps: null, showLPC } } -interface LPCFlowsProps { +interface LPCFlowsMaintenanceRunProps { + maintenanceRunId: string + setMaintenanceRunId: (id: string | null) => void + isDeletingMaintenanceRun: boolean +} + +export interface LPCFlowsProps { onCloseClick: () => void runId: string - maintenanceRunId: string robotType: RobotType existingOffsets: LabwareOffset[] mostRecentAnalysis: CompletedProtocolAnalysis | null protocolName: string - caughtError?: Error - setMaintenanceRunId: (id: string | null) => void - isDeletingMaintenanceRun: boolean + maintenanceRunUtils: LPCFlowsMaintenanceRunProps } -function LPCFlows(props: LPCFlowsProps): JSX.Element { - return +export function LPCFlows(props: LPCFlowsProps): JSX.Element { + return } diff --git a/app/src/organisms/LabwarePositionCheck/LPCWizardContainer.tsx b/app/src/organisms/LabwarePositionCheck/LPCWizardContainer.tsx new file mode 100644 index 00000000000..ad29f667fda --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/LPCWizardContainer.tsx @@ -0,0 +1,31 @@ +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' + +import { LPCWizardFlex } from './LPCWizardFlex' +import { LegacyLabwarePositionCheck } from '/app/organisms/LegacyLabwarePositionCheck' + +import type { LPCFlowsProps } from '/app/organisms/LabwarePositionCheck/LPCFlows' + +export function LPCWizardContainer(props: LPCFlowsProps): JSX.Element { + return props.robotType === FLEX_ROBOT_TYPE ? ( + + ) : ( + + ) +} + +function LPCLegacyAdapter(props: LPCFlowsProps): JSX.Element { + const { + setMaintenanceRunId, + maintenanceRunId, + isDeletingMaintenanceRun, + } = props.maintenanceRunUtils + + return ( + + ) +} diff --git a/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx similarity index 95% rename from app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx rename to app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx index 6f0953093a6..c9e3b535b61 100644 --- a/app/src/organisms/LabwarePositionCheck/LabwarePositionCheckComponent.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx @@ -32,50 +32,37 @@ import { import { getLabwarePositionCheckSteps } from './getLabwarePositionCheckSteps' import type { - CompletedProtocolAnalysis, Coordinates, CreateCommand, DropTipCreateCommand, - RobotType, } from '@opentrons/shared-data' import type { LabwareOffsetCreateData, - LabwareOffset, CommandData, } from '@opentrons/api-client' import type { Axis, Sign, StepSize } from '/app/molecules/JogControls/types' import type { RegisterPositionAction, WorkingOffset } from './types' +import type { LPCFlowsProps } from '/app/organisms/LabwarePositionCheck/LPCFlows' const RUN_REFETCH_INTERVAL = 5000 const JOG_COMMAND_TIMEOUT = 10000 // 10 seconds -interface LabwarePositionCheckModalProps { - runId: string - maintenanceRunId: string - robotType: RobotType - mostRecentAnalysis: CompletedProtocolAnalysis | null - existingOffsets: LabwareOffset[] - onCloseClick: () => unknown - protocolName: string - setMaintenanceRunId: (id: string | null) => void - isDeletingMaintenanceRun: boolean - caughtError?: Error -} -export const LabwarePositionCheckComponent = ( - props: LabwarePositionCheckModalProps -): JSX.Element | null => { +export const LPCWizardFlex = (props: LPCFlowsProps): JSX.Element | null => { const { mostRecentAnalysis, existingOffsets, robotType, runId, - maintenanceRunId, onCloseClick, - setMaintenanceRunId, protocolName, - isDeletingMaintenanceRun, + maintenanceRunUtils, } = props const { t } = useTranslation(['labware_position_check', 'shared']) + const { + maintenanceRunId, + setMaintenanceRunId, + isDeletingMaintenanceRun, + } = maintenanceRunUtils const isOnDevice = useSelector(getIsOnDevice) const protocolData = mostRecentAnalysis const shouldUseMetalProbe = robotType === FLEX_ROBOT_TYPE @@ -236,8 +223,12 @@ export const LabwarePositionCheckComponent = ( ], true ) - .then(() => props.onCloseClick()) - .catch(() => props.onCloseClick()) + .then(() => { + props.onCloseClick() + }) + .catch(() => { + props.onCloseClick() + }) } const { confirm: confirmExitLPC, diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/CheckItem.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/CheckItem.test.tsx deleted file mode 100644 index 17442dfc42b..00000000000 --- a/app/src/organisms/LabwarePositionCheck/__tests__/CheckItem.test.tsx +++ /dev/null @@ -1,702 +0,0 @@ -import type * as React from 'react' -import { fireEvent, screen } from '@testing-library/react' -import { describe, it, vi, beforeEach, afterEach, expect } from 'vitest' - -import { - FLEX_ROBOT_TYPE, - HEATERSHAKER_MODULE_V1, - OT2_ROBOT_TYPE, - THERMOCYCLER_MODULE_V2, -} from '@opentrons/shared-data' - -import { nestedTextMatcher, renderWithProviders } from '/app/__testing-utils__' -import { i18n } from '/app/i18n' -import { CheckItem } from '../CheckItem' -import { SECTIONS } from '../constants' -import { mockCompletedAnalysis, mockExistingOffsets } from '../__fixtures__' - -import type { Mock } from 'vitest' - -vi.mock('/app/redux/config') -vi.mock('../../Desktop/Devices/hooks') - -const mockStartPosition = { x: 10, y: 20, z: 30 } -const mockEndPosition = { x: 9, y: 19, z: 29 } - -const render = (props: React.ComponentProps) => { - return renderWithProviders(, { - i18nInstance: i18n, - })[0] -} - -describe('CheckItem', () => { - let props: React.ComponentProps - let mockChainRunCommands: Mock - - beforeEach(() => { - mockChainRunCommands = vi.fn().mockImplementation(() => Promise.resolve([])) - props = { - section: SECTIONS.CHECK_LABWARE, - pipetteId: mockCompletedAnalysis.pipettes[0].id, - labwareId: mockCompletedAnalysis.labware[0].id, - definitionUri: mockCompletedAnalysis.labware[0].definitionUri, - location: { slotName: 'D1' }, - protocolData: mockCompletedAnalysis, - proceed: vi.fn(), - chainRunCommands: mockChainRunCommands, - handleJog: vi.fn(), - registerPosition: vi.fn(), - setFatalError: vi.fn(), - workingOffsets: [], - existingOffsets: mockExistingOffsets, - isRobotMoving: false, - robotType: FLEX_ROBOT_TYPE, - shouldUseMetalProbe: false, - } - }) - afterEach(() => { - vi.resetAllMocks() - }) - it('renders correct copy when preparing space with tip rack', () => { - render(props) - screen.getByRole('heading', { name: 'Prepare tip rack in Slot D1' }) - screen.getByText( - 'Clear all deck slots of labware, leaving modules in place' - ) - screen.getAllByText(/Place/i) - screen.getAllByText(/a full Mock TipRack Definition/i) - screen.getAllByText(/into/i) - screen.getAllByText(/Slot D1/i) - screen.getByRole('link', { name: 'Need help?' }) - screen.getByRole('button', { name: 'Confirm placement' }) - }) - it('renders correct copy when preparing space with non tip rack labware', () => { - props = { - ...props, - labwareId: mockCompletedAnalysis.labware[1].id, - location: { slotName: 'D2' }, - } - - render(props) - screen.getByRole('heading', { name: 'Prepare labware in Slot D2' }) - screen.getByText( - 'Clear all deck slots of labware, leaving modules in place' - ) - screen.getAllByText(/Place a/i) - screen.getAllByText(/Mock Labware Definition/i) - screen.getAllByText(/into/i) - screen.getAllByText(/Slot D2/i) - screen.getByRole('link', { name: 'Need help?' }) - screen.getByRole('button', { name: 'Confirm placement' }) - }) - it('executes correct chained commands when confirm placement CTA is clicked then go back', async () => { - vi.mocked(mockChainRunCommands).mockImplementation(() => - Promise.resolve([ - {}, - {}, - { - data: { - commandType: 'savePosition', - result: { position: mockStartPosition }, - }, - }, - ]) - ) - render(props) - fireEvent.click(screen.getByRole('button', { name: 'Confirm placement' })) - await new Promise((resolve, reject) => setTimeout(resolve)) - expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 1, - [ - { - commandType: 'moveLabware', - params: { - labwareId: 'labwareId1', - newLocation: { slotName: 'D1' }, - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'moveToWell', - params: { - pipetteId: 'pipetteId1', - labwareId: 'labwareId1', - wellName: 'A1', - wellLocation: { origin: 'top', offset: { x: 0, y: 0, z: 44.5 } }, - }, - }, - { - commandType: 'savePosition', - params: { pipetteId: 'pipetteId1' }, - }, - ], - false - ) - expect(props.registerPosition).toHaveBeenNthCalledWith(1, { - type: 'initialPosition', - labwareId: 'labwareId1', - location: { slotName: 'D1' }, - position: mockStartPosition, - }) - }) - it('executes correct chained commands when confirm placement CTA is clicked then go back on OT-2', async () => { - props = { - ...props, - robotType: OT2_ROBOT_TYPE, - location: { slotName: '1' }, - } - vi.mocked(mockChainRunCommands).mockImplementation(() => - Promise.resolve([ - {}, - {}, - { - data: { - commandType: 'savePosition', - result: { position: mockStartPosition }, - }, - }, - ]) - ) - render(props) - fireEvent.click(screen.getByRole('button', { name: 'Confirm placement' })) - await new Promise((resolve, reject) => setTimeout(resolve)) - - expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 1, - [ - { - commandType: 'moveLabware', - params: { - labwareId: 'labwareId1', - newLocation: { slotName: '1' }, - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'moveToWell', - params: { - pipetteId: 'pipetteId1', - labwareId: 'labwareId1', - wellName: 'A1', - wellLocation: { origin: 'top', offset: { x: 0, y: 0, z: 0 } }, - }, - }, - { - commandType: 'savePosition', - params: { pipetteId: 'pipetteId1' }, - }, - ], - false - ) - expect(props.registerPosition).toHaveBeenNthCalledWith(1, { - type: 'initialPosition', - labwareId: 'labwareId1', - location: { slotName: '1' }, - position: mockStartPosition, - }) - }) - - it('executes correct chained commands when confirm placement CTA is clicked then go back on Flex', async () => { - props = { ...props, robotType: FLEX_ROBOT_TYPE } - vi.mocked(mockChainRunCommands).mockImplementation(() => - Promise.resolve([ - {}, - {}, - { - data: { - commandType: 'savePosition', - result: { position: mockStartPosition }, - }, - }, - ]) - ) - render(props) - fireEvent.click(screen.getByRole('button', { name: 'Confirm placement' })) - await new Promise((resolve, reject) => setTimeout(resolve)) - - expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 1, - [ - { - commandType: 'moveLabware', - params: { - labwareId: 'labwareId1', - newLocation: { slotName: 'D1' }, - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'moveToWell', - params: { - pipetteId: 'pipetteId1', - labwareId: 'labwareId1', - wellName: 'A1', - wellLocation: { origin: 'top', offset: { x: 0, y: 0, z: 44.5 } }, - }, - }, - { - commandType: 'savePosition', - params: { pipetteId: 'pipetteId1' }, - }, - ], - false - ) - expect(props.registerPosition).toHaveBeenNthCalledWith(1, { - type: 'initialPosition', - labwareId: 'labwareId1', - location: { slotName: 'D1' }, - position: mockStartPosition, - }) - }) - - it('renders the correct copy for moving a labware onto an adapter', () => { - props = { - ...props, - labwareId: mockCompletedAnalysis.labware[1].id, - adapterId: 'labwareId2', - } - render(props) - screen.getByText('Prepare labware in Slot D1') - screen.getByText( - nestedTextMatcher( - 'Place a Mock Labware Definition followed by a Mock Labware Definition into Slot D1' - ) - ) - }) - it('executes correct chained commands when confirm placement CTA is clicked for when there is an adapter', async () => { - props = { - ...props, - adapterId: 'labwareId2', - } - vi.mocked(mockChainRunCommands).mockImplementation(() => - Promise.resolve([ - {}, - {}, - { - data: { - commandType: 'savePosition', - result: { position: mockStartPosition }, - }, - }, - ]) - ) - render(props) - fireEvent.click(screen.getByRole('button', { name: 'Confirm placement' })) - await new Promise((resolve, reject) => setTimeout(resolve)) - - expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 1, - [ - { - commandType: 'moveLabware', - params: { - labwareId: 'labwareId2', - newLocation: { slotName: 'D1' }, - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'moveLabware', - params: { - labwareId: 'labwareId1', - newLocation: { labwareId: 'labwareId2' }, - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'moveToWell', - params: { - pipetteId: 'pipetteId1', - labwareId: 'labwareId1', - wellName: 'A1', - wellLocation: { origin: 'top', offset: { x: 0, y: 0, z: 44.5 } }, - }, - }, - { - commandType: 'savePosition', - params: { pipetteId: 'pipetteId1' }, - }, - ], - false - ) - expect(props.registerPosition).toHaveBeenNthCalledWith(1, { - type: 'initialPosition', - labwareId: 'labwareId1', - location: { slotName: 'D1' }, - position: mockStartPosition, - }) - }) - it('executes correct chained commands when go back clicked', async () => { - props = { - ...props, - workingOffsets: [ - { - location: { slotName: 'D1' }, - labwareId: 'labwareId1', - initialPosition: { x: 1, y: 2, z: 3 }, - finalPosition: null, - }, - ], - } - render(props) - fireEvent.click(screen.getByRole('button', { name: 'Go back' })) - await new Promise((resolve, reject) => setTimeout(resolve)) - - expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 1, - [ - { commandType: 'home', params: {} }, - { - commandType: 'moveLabware', - params: { - labwareId: 'labwareId1', - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - ], - false - ) - expect(props.registerPosition).toHaveBeenNthCalledWith(1, { - type: 'initialPosition', - labwareId: 'labwareId1', - location: { slotName: 'D1' }, - position: null, - }) - }) - it('executes correct chained commands when confirm position clicked', async () => { - vi.mocked(mockChainRunCommands).mockImplementation(() => - Promise.resolve([ - { - data: { - commandType: 'savePosition', - result: { position: mockEndPosition }, - }, - }, - {}, - {}, - ]) - ) - props = { - ...props, - workingOffsets: [ - { - location: { slotName: 'D1' }, - labwareId: 'labwareId1', - initialPosition: { x: 1, y: 2, z: 3 }, - finalPosition: null, - }, - ], - } - render(props) - fireEvent.click(screen.getByRole('button', { name: 'Confirm position' })) - await new Promise((resolve, reject) => setTimeout(resolve)) - - expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 1, - [ - { - commandType: 'savePosition', - params: { pipetteId: 'pipetteId1' }, - }, - { - commandType: 'retractAxis' as const, - params: { - axis: 'leftZ', - }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'x' }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'y' }, - }, - { - commandType: 'moveLabware', - params: { - labwareId: 'labwareId1', - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - ], - false - ) - expect(props.registerPosition).toHaveBeenNthCalledWith(1, { - type: 'finalPosition', - labwareId: 'labwareId1', - location: { slotName: 'D1' }, - position: mockEndPosition, - }) - }) - - it('executes heater shaker open latch command on component mount if step is on HS', async () => { - props = { ...props, robotType: FLEX_ROBOT_TYPE } - props = { - ...props, - location: { slotName: 'D1', moduleModel: HEATERSHAKER_MODULE_V1 }, - moduleId: 'firstHSId', - protocolData: { - ...props.protocolData, - modules: [ - { - id: 'firstHSId', - model: HEATERSHAKER_MODULE_V1, - location: { slotName: 'D3' }, - serialNumber: 'firstHSSerial', - }, - { - id: 'secondHSId', - model: HEATERSHAKER_MODULE_V1, - location: { slotName: 'A1' }, - serialNumber: 'secondHSSerial', - }, - ], - }, - } - render(props) - await new Promise((resolve, reject) => setTimeout(resolve)) - - expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 1, - [ - { - commandType: 'heaterShaker/closeLabwareLatch', - params: { moduleId: 'firstHSId' }, - }, - { - commandType: 'heaterShaker/deactivateShaker', - params: { moduleId: 'firstHSId' }, - }, - { - commandType: 'heaterShaker/openLabwareLatch', - params: { moduleId: 'firstHSId' }, - }, - ], - false - ) - fireEvent.click(screen.getByRole('button', { name: 'Confirm placement' })) - - expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 2, - [ - { - commandType: 'moveLabware', - params: { - labwareId: 'labwareId1', - newLocation: { moduleId: 'firstHSId' }, - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'heaterShaker/closeLabwareLatch', - params: { moduleId: 'firstHSId' }, - }, - { - commandType: 'heaterShaker/closeLabwareLatch', - params: { moduleId: 'secondHSId' }, - }, - { - commandType: 'moveToWell', - params: { - pipetteId: 'pipetteId1', - labwareId: 'labwareId1', - wellName: 'A1', - wellLocation: { origin: 'top', offset: { x: 0, y: 0, z: 44.5 } }, - }, - }, - { - commandType: 'savePosition', - params: { pipetteId: 'pipetteId1' }, - }, - ], - false - ) - }) - - it('executes correct chained commands when confirm position clicked with HS and adapter', async () => { - props = { - ...props, - location: { slotName: 'D1', moduleModel: HEATERSHAKER_MODULE_V1 }, - adapterId: 'adapterId', - moduleId: 'heaterShakerId', - protocolData: { - ...props.protocolData, - modules: [ - { - id: 'heaterShakerId', - model: HEATERSHAKER_MODULE_V1, - location: { slotName: 'D3' }, - serialNumber: 'firstHSSerial', - }, - ], - }, - workingOffsets: [ - { - location: { slotName: 'D1', moduleModel: HEATERSHAKER_MODULE_V1 }, - labwareId: 'labwareId1', - initialPosition: { x: 1, y: 2, z: 3 }, - finalPosition: null, - }, - ], - } - vi.mocked(mockChainRunCommands).mockImplementation(() => - Promise.resolve([ - { - data: { - commandType: 'savePosition', - result: { position: mockEndPosition }, - }, - }, - {}, - {}, - {}, - {}, - {}, - {}, - ]) - ) - - render(props) - fireEvent.click(screen.getByRole('button', { name: 'Confirm position' })) - await new Promise((resolve, reject) => setTimeout(resolve)) - - expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 1, - [ - { - commandType: 'savePosition', - params: { pipetteId: 'pipetteId1' }, - }, - { - commandType: 'retractAxis' as const, - params: { - axis: 'leftZ', - }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'x' }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'y' }, - }, - { - commandType: 'heaterShaker/openLabwareLatch', - params: { moduleId: 'heaterShakerId' }, - }, - { - commandType: 'moveLabware', - params: { - labwareId: 'labwareId1', - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'moveLabware', - params: { - labwareId: 'adapterId', - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - ], - false - ) - expect(props.registerPosition).toHaveBeenNthCalledWith(1, { - type: 'finalPosition', - labwareId: 'labwareId1', - location: { slotName: 'D1', moduleModel: HEATERSHAKER_MODULE_V1 }, - position: mockEndPosition, - }) - }) - - it('executes thermocycler open lid command on mount if checking labware on thermocycler', () => { - props = { - ...props, - location: { slotName: 'B1', moduleModel: THERMOCYCLER_MODULE_V2 }, - moduleId: 'tcId', - protocolData: { - ...props.protocolData, - modules: [ - { - id: 'tcId', - model: THERMOCYCLER_MODULE_V2, - location: { slotName: 'B1' }, - serialNumber: 'tcSerial', - }, - ], - }, - } - render(props) - expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 1, - [ - { - commandType: 'thermocycler/openLid', - params: { moduleId: 'tcId' }, - }, - ], - false - ) - }) - it('executes correct chained commands when confirm placement CTA is clicked when using probe for LPC', async () => { - props = { - ...props, - robotType: FLEX_ROBOT_TYPE, - } - vi.mocked(mockChainRunCommands).mockImplementation(() => - Promise.resolve([ - {}, - {}, - { - data: { - commandType: 'savePosition', - result: { position: mockStartPosition }, - }, - }, - ]) - ) - render(props) - fireEvent.click(screen.getByRole('button', { name: 'Confirm placement' })) - await new Promise((resolve, reject) => setTimeout(resolve)) - - expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 1, - [ - { - commandType: 'moveLabware', - params: { - labwareId: 'labwareId1', - newLocation: { slotName: 'D1' }, - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'moveToWell', - params: { - pipetteId: 'pipetteId1', - labwareId: 'labwareId1', - wellName: 'A1', - wellLocation: { origin: 'top', offset: { x: 0, y: 0, z: 44.5 } }, - }, - }, - { - commandType: 'savePosition', - params: { pipetteId: 'pipetteId1' }, - }, - ], - false - ) - expect(props.registerPosition).toHaveBeenNthCalledWith(1, { - type: 'initialPosition', - labwareId: 'labwareId1', - location: { slotName: 'D1' }, - position: mockStartPosition, - }) - }) -}) diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/ExitConfirmation.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/ExitConfirmation.test.tsx deleted file mode 100644 index 6a93da71dc5..00000000000 --- a/app/src/organisms/LabwarePositionCheck/__tests__/ExitConfirmation.test.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import type * as React from 'react' -import { fireEvent, screen } from '@testing-library/react' -import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest' -import { ExitConfirmation } from '../ExitConfirmation' -import { renderWithProviders } from '/app/__testing-utils__' -import { i18n } from '/app/i18n' - -const render = (props: React.ComponentProps) => { - return renderWithProviders(, { - i18nInstance: i18n, - })[0] -} - -describe('ExitConfirmation', () => { - let props: React.ComponentProps - - beforeEach(() => { - props = { - onGoBack: vi.fn(), - onConfirmExit: vi.fn(), - shouldUseMetalProbe: false, - } - }) - afterEach(() => { - vi.restoreAllMocks() - }) - it('should render correct copy', () => { - render(props) - screen.getByText('Exit before completing Labware Position Check?') - screen.getByText( - 'If you exit now, all labware offsets will be discarded. This cannot be undone.' - ) - screen.getByRole('button', { name: 'Exit' }) - screen.getByRole('button', { name: 'Go back' }) - }) - it('should invoke callback props when ctas are clicked', () => { - render(props) - fireEvent.click(screen.getByRole('button', { name: 'Go back' })) - expect(props.onGoBack).toHaveBeenCalled() - fireEvent.click(screen.getByRole('button', { name: 'Exit' })) - expect(props.onConfirmExit).toHaveBeenCalled() - }) - it('should render correct copy for golden tip LPC', () => { - render({ - ...props, - shouldUseMetalProbe: true, - }) - screen.getByText('Remove the calibration probe before exiting') - screen.getByText( - 'If you exit now, all labware offsets will be discarded. This cannot be undone.' - ) - screen.getByRole('button', { name: 'Remove calibration probe' }) - screen.getByRole('button', { name: 'Go back' }) - }) -}) diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/PickUpTip.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/PickUpTip.test.tsx deleted file mode 100644 index c23e1c1af2c..00000000000 --- a/app/src/organisms/LabwarePositionCheck/__tests__/PickUpTip.test.tsx +++ /dev/null @@ -1,466 +0,0 @@ -import type * as React from 'react' -import { fireEvent, screen, waitFor } from '@testing-library/react' -import { it, describe, beforeEach, vi, afterEach, expect } from 'vitest' -import { FLEX_ROBOT_TYPE, HEATERSHAKER_MODULE_V1 } from '@opentrons/shared-data' -import { i18n } from '/app/i18n' -import { useProtocolMetadata } from '/app/resources/protocols' -import { getIsOnDevice } from '/app/redux/config' -import { PickUpTip } from '../PickUpTip' -import { SECTIONS } from '../constants' -import { mockCompletedAnalysis, mockExistingOffsets } from '../__fixtures__' -import type { CommandData } from '@opentrons/api-client' -import { nestedTextMatcher, renderWithProviders } from '/app/__testing-utils__' -import type { Mock } from 'vitest' - -vi.mock('/app/resources/protocols') -vi.mock('/app/redux/config') - -const mockStartPosition = { x: 10, y: 20, z: 30 } - -const render = (props: React.ComponentProps) => { - return renderWithProviders(, { - i18nInstance: i18n, - })[0] -} - -describe('PickUpTip', () => { - let props: React.ComponentProps - let mockChainRunCommands: Mock - - beforeEach(() => { - mockChainRunCommands = vi.fn().mockImplementation(() => Promise.resolve()) - vi.mocked(getIsOnDevice).mockReturnValue(false) - props = { - section: SECTIONS.PICK_UP_TIP, - pipetteId: mockCompletedAnalysis.pipettes[0].id, - labwareId: mockCompletedAnalysis.labware[0].id, - definitionUri: mockCompletedAnalysis.labware[0].definitionUri, - location: { slotName: 'D1' }, - protocolData: mockCompletedAnalysis, - proceed: vi.fn(), - chainRunCommands: mockChainRunCommands, - handleJog: vi.fn(), - registerPosition: vi.fn(), - setFatalError: vi.fn(), - workingOffsets: [], - existingOffsets: mockExistingOffsets, - isRobotMoving: false, - robotType: FLEX_ROBOT_TYPE, - protocolHasModules: false, - currentStepIndex: 1, - } - vi.mocked(useProtocolMetadata).mockReturnValue({ - robotType: 'OT-3 Standard', - }) - }) - afterEach(() => { - vi.resetAllMocks() - }) - it('renders correct copy when preparing space on desktop if protocol has modules', () => { - props.protocolHasModules = true - render(props) - screen.getByRole('heading', { name: 'Prepare tip rack in Slot D1' }) - screen.getByText('Place modules on deck') - screen.getByText( - 'Clear all deck slots of labware, leaving modules in place' - ) - screen.getByText('a full Mock TipRack Definition') - screen.getByText('Slot D1') - screen.getByRole('button', { name: 'Confirm placement' }) - }) - it('renders correct copy when preparing space on touchscreen if protocol has modules', () => { - vi.mocked(getIsOnDevice).mockReturnValue(true) - props.protocolHasModules = true - render(props) - screen.getByRole('heading', { name: 'Prepare tip rack in Slot D1' }) - screen.getByText('Place modules on deck') - screen.getByText('Clear all deck slots of labware') - screen.getByText('a full Mock TipRack Definition') - screen.getByText('Slot D1') - }) - it('renders correct copy when preparing space on desktop if protocol has no modules', () => { - render(props) - screen.getByRole('heading', { name: 'Prepare tip rack in Slot D1' }) - screen.getByText( - 'Clear all deck slots of labware, leaving modules in place' - ) - screen.getByText('a full Mock TipRack Definition') - screen.getByText('Slot D1') - screen.getByRole('button', { name: 'Confirm placement' }) - }) - it('renders correct copy when preparing space on touchscreen if protocol has no modules', () => { - vi.mocked(getIsOnDevice).mockReturnValue(true) - render(props) - screen.getByRole('heading', { name: 'Prepare tip rack in Slot D1' }) - screen.getByText('Clear all deck slots of labware') - screen.getByText('a full Mock TipRack Definition') - screen.getByText('Slot D1') - }) - it('renders correct copy when confirming position on desktop', () => { - render({ - ...props, - workingOffsets: [ - { - location: { slotName: 'D1' }, - labwareId: 'labwareId1', - initialPosition: { x: 1, y: 2, z: 3 }, - finalPosition: null, - }, - ], - }) - screen.getByRole('heading', { - name: 'Pick up tip from tip rack in Slot D1', - }) - screen.getByText( - "Ensure that the pipette nozzle furthest from you is centered above and level with the top of the tip in the A1 position. If it isn't, use the controls below or your keyboard to jog the pipette until it is properly aligned." - ) - screen.getByRole('link', { name: 'Need help?' }) - }) - it('renders correct copy when confirming position on touchscreen', () => { - vi.mocked(getIsOnDevice).mockReturnValue(true) - render({ - ...props, - workingOffsets: [ - { - location: { slotName: 'D1' }, - labwareId: 'labwareId1', - initialPosition: { x: 1, y: 2, z: 3 }, - finalPosition: null, - }, - ], - }) - screen.getByRole('heading', { - name: 'Pick up tip from tip rack in Slot D1', - }) - screen.getByText( - nestedTextMatcher( - "Ensure that the pipette nozzle furthest from you is centered above and level with the top of the tip in the A1 position. If it isn't, tap Move pipette and then jog the pipette until it is properly aligned." - ) - ) - }) - it('executes correct chained commands when confirm placement CTA is clicked', () => { - vi.mocked(mockChainRunCommands).mockImplementation(() => - Promise.resolve([{} as CommandData]) - ) - render(props) - const confirm = screen.getByRole('button', { name: 'Confirm placement' }) - fireEvent.click(confirm) - expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 1, - [ - { - commandType: 'moveLabware', - params: { - labwareId: 'labwareId1', - newLocation: { slotName: 'D1' }, - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'moveToWell', - params: { - pipetteId: 'pipetteId1', - labwareId: 'labwareId1', - wellName: 'A1', - wellLocation: { origin: 'top', offset: undefined }, - }, - }, - { - commandType: 'savePosition', - params: { pipetteId: 'pipetteId1' }, - }, - ], - false - ) - }) - - it('executes correct chained commands when confirm position CTA is clicked and user tries again', async () => { - vi.mocked(mockChainRunCommands).mockImplementation(() => - Promise.resolve([ - { - data: { - commandType: 'savePosition', - result: { position: mockStartPosition }, - }, - }, - {}, - {}, - ]) - ) - - render({ - ...props, - workingOffsets: [ - { - location: { slotName: 'D1' }, - labwareId: 'labwareId1', - initialPosition: { x: 1, y: 2, z: 3 }, - finalPosition: null, - }, - ], - }) - - const forward = screen.getByRole('button', { name: 'forward' }) - fireEvent.click(forward) - expect(props.handleJog).toHaveBeenCalled() - const confirm = screen.getByRole('button', { name: 'Confirm position' }) - fireEvent.click(confirm) - await new Promise((resolve, reject) => setTimeout(resolve)) - - await waitFor(() => { - expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 1, - [ - { - commandType: 'savePosition', - params: { pipetteId: 'pipetteId1' }, - }, - ], - false - ) - }) - await waitFor(() => { - expect(props.registerPosition).toHaveBeenNthCalledWith(1, { - type: 'finalPosition', - labwareId: 'labwareId1', - location: { slotName: 'D1' }, - position: { x: 10, y: 20, z: 30 }, - }) - }) - await waitFor(() => { - expect(props.registerPosition).toHaveBeenNthCalledWith(2, { - type: 'tipPickUpOffset', - offset: { x: 9, y: 18, z: 27 }, - }) - }) - await waitFor(() => { - expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 2, - [ - { - commandType: 'pickUpTip', - params: { - pipetteId: 'pipetteId1', - labwareId: 'labwareId1', - wellName: 'A1', - wellLocation: { origin: 'top', offset: { x: 9, y: 18, z: 27 } }, - }, - }, - ], - false - ) - screen.getByRole('heading', { - name: 'Did pipette pick up tip successfully?', - }) - }) - const tryAgain = screen.getByRole('button', { name: 'Try again' }) - fireEvent.click(tryAgain) - await new Promise((resolve, reject) => setTimeout(resolve)) - - await waitFor(() => { - expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 3, - [ - { - commandType: 'dropTip', - params: { - pipetteId: 'pipetteId1', - labwareId: 'labwareId1', - wellName: 'A1', - }, - }, - { - commandType: 'moveToWell', - params: { - pipetteId: 'pipetteId1', - labwareId: 'labwareId1', - wellName: 'A1', - wellLocation: { origin: 'top' }, - }, - }, - ], - false - ) - }) - await waitFor(() => { - expect(props.registerPosition).toHaveBeenNthCalledWith(3, { - type: 'tipPickUpOffset', - offset: null, - }) - }) - }) - it('proceeds after confirm position and pick up tip', async () => { - vi.mocked(mockChainRunCommands).mockImplementation(() => - Promise.resolve([ - { - data: { - commandType: 'savePosition', - result: { position: mockStartPosition }, - }, - }, - {}, - {}, - ]) - ) - render({ - ...props, - workingOffsets: [ - { - location: { slotName: 'D1' }, - labwareId: 'labwareId1', - initialPosition: { x: 1, y: 2, z: 3 }, - finalPosition: null, - }, - ], - }) - - const confirm = screen.getByRole('button', { name: 'Confirm position' }) - fireEvent.click(confirm) - await new Promise((resolve, reject) => setTimeout(resolve)) - - await waitFor(() => { - expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 1, - [ - { - commandType: 'savePosition', - params: { pipetteId: 'pipetteId1' }, - }, - ], - false - ) - }) - await waitFor(() => { - expect(props.registerPosition).toHaveBeenNthCalledWith(1, { - type: 'finalPosition', - labwareId: 'labwareId1', - location: { slotName: 'D1' }, - position: { x: 10, y: 20, z: 30 }, - }) - }) - await waitFor(() => { - expect(props.registerPosition).toHaveBeenNthCalledWith(2, { - type: 'tipPickUpOffset', - offset: { x: 9, y: 18, z: 27 }, - }) - }) - await waitFor(() => { - expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 2, - [ - { - commandType: 'pickUpTip', - params: { - pipetteId: 'pipetteId1', - labwareId: 'labwareId1', - wellName: 'A1', - wellLocation: { origin: 'top', offset: { x: 9, y: 18, z: 27 } }, - }, - }, - ], - false - ) - screen.getByRole('heading', { - name: 'Did pipette pick up tip successfully?', - }) - }) - const yesButton = screen.getByRole('button', { name: 'Yes' }) - fireEvent.click(yesButton) - await new Promise((resolve, reject) => setTimeout(resolve)) - - await waitFor(() => { - expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 3, - [ - { - commandType: 'retractAxis' as const, - params: { - axis: 'leftZ', - }, - }, - { - commandType: 'retractAxis' as const, - params: { - axis: 'x', - }, - }, - { - commandType: 'retractAxis' as const, - params: { - axis: 'y', - }, - }, - { - commandType: 'moveLabware', - params: { - labwareId: 'labwareId1', - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - ], - false - ) - }) - await waitFor(() => { - expect(props.proceed).toHaveBeenCalled() - }) - }) - it('executes heater shaker closed latch commands for every hs module before other commands', () => { - props = { - ...props, - protocolData: { - ...props.protocolData, - modules: [ - { - id: 'firstHSId', - model: HEATERSHAKER_MODULE_V1, - location: { slotName: 'D3' }, - serialNumber: 'firstHSSerial', - }, - { - id: 'secondHSId', - model: HEATERSHAKER_MODULE_V1, - location: { slotName: 'A1' }, - serialNumber: 'secondHSSerial', - }, - ], - }, - } - render(props) - const confirm = screen.getByRole('button', { name: 'Confirm placement' }) - fireEvent.click(confirm) - expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 1, - [ - { - commandType: 'heaterShaker/closeLabwareLatch', - params: { moduleId: 'firstHSId' }, - }, - { - commandType: 'heaterShaker/closeLabwareLatch', - params: { moduleId: 'secondHSId' }, - }, - { - commandType: 'moveLabware', - params: { - labwareId: 'labwareId1', - newLocation: { slotName: 'D1' }, - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'moveToWell', - params: { - pipetteId: 'pipetteId1', - labwareId: 'labwareId1', - wellName: 'A1', - wellLocation: { origin: 'top' }, - }, - }, - { commandType: 'savePosition', params: { pipetteId: 'pipetteId1' } }, - ], - false - ) - }) -}) diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/ResultsSummary.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/ResultsSummary.test.tsx deleted file mode 100644 index 24101904de4..00000000000 --- a/app/src/organisms/LabwarePositionCheck/__tests__/ResultsSummary.test.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import type * as React from 'react' -import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest' -import { fireEvent, screen } from '@testing-library/react' -import { i18n } from '/app/i18n' -import { renderWithProviders } from '/app/__testing-utils__' -import { getIsLabwareOffsetCodeSnippetsOn } from '/app/redux/config' -import { ResultsSummary } from '../ResultsSummary' -import { SECTIONS } from '../constants' -import { mockTipRackDefinition } from '/app/redux/custom-labware/__fixtures__' -import { - mockCompletedAnalysis, - mockExistingOffsets, - mockWorkingOffsets, -} from '../__fixtures__' - -vi.mock('/app/redux/config') - -const render = (props: React.ComponentProps) => { - return renderWithProviders(, { - i18nInstance: i18n, - })[0] -} - -describe('ResultsSummary', () => { - let props: React.ComponentProps - - beforeEach(() => { - props = { - section: SECTIONS.RESULTS_SUMMARY, - protocolData: mockCompletedAnalysis, - workingOffsets: mockWorkingOffsets, - existingOffsets: mockExistingOffsets, - isApplyingOffsets: false, - isDeletingMaintenanceRun: false, - handleApplyOffsets: vi.fn(), - } - }) - afterEach(() => { - vi.restoreAllMocks() - }) - it('renders correct copy', () => { - render(props) - screen.getByText('New labware offset data') - screen.getByRole('button', { name: 'Apply offsets' }) - screen.getByRole('link', { name: 'Need help?' }) - screen.getByRole('columnheader', { name: 'location' }) - screen.getByRole('columnheader', { name: 'labware' }) - screen.getByRole('columnheader', { name: 'labware offset data' }) - }) - it('calls handle apply offsets function when button is clicked', () => { - render(props) - fireEvent.click(screen.getByRole('button', { name: 'Apply offsets' })) - expect(props.handleApplyOffsets).toHaveBeenCalled() - }) - it('does disables the CTA to apply offsets when offsets are already being applied', () => { - props.isApplyingOffsets = true - render(props) - const button = screen.getByRole('button', { name: 'Apply offsets' }) - expect(button).toBeDisabled() - fireEvent.click(button) - expect(props.handleApplyOffsets).not.toHaveBeenCalled() - }) - it('does disables the CTA to apply offsets when the maintenance run is being deleted', () => { - props.isDeletingMaintenanceRun = true - render(props) - const button = screen.getByRole('button', { name: 'Apply offsets' }) - expect(button).toBeDisabled() - fireEvent.click(button) - expect(props.handleApplyOffsets).not.toHaveBeenCalled() - }) - it('renders a row per offset to apply', () => { - render(props) - expect( - screen.queryAllByRole('cell', { - name: mockTipRackDefinition.metadata.displayName, - }) - ).toHaveLength(2) - screen.getByRole('cell', { name: 'Slot 1' }) - screen.getByRole('cell', { name: 'Slot 3' }) - screen.getByRole('cell', { name: 'X 1.0 Y 1.0 Z 1.0' }) - screen.getByRole('cell', { name: 'X 3.0 Y 3.0 Z 3.0' }) - }) - - it('renders tabbed offset data with snippets when config option is selected', () => { - vi.mocked(getIsLabwareOffsetCodeSnippetsOn).mockReturnValue(true) - render(props) - expect(screen.getByText('Table View')).toBeTruthy() - expect(screen.getByText('Jupyter Notebook')).toBeTruthy() - expect(screen.getByText('Command Line Interface (SSH)')).toBeTruthy() - }) -}) diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/ReturnTip.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/ReturnTip.test.tsx deleted file mode 100644 index 0af86097f9c..00000000000 --- a/app/src/organisms/LabwarePositionCheck/__tests__/ReturnTip.test.tsx +++ /dev/null @@ -1,258 +0,0 @@ -import type * as React from 'react' -import { fireEvent, screen } from '@testing-library/react' -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' - -import { FLEX_ROBOT_TYPE, HEATERSHAKER_MODULE_V1 } from '@opentrons/shared-data' - -import { renderWithProviders } from '/app/__testing-utils__' -import { i18n } from '/app/i18n' -import { SECTIONS } from '../constants' -import { mockCompletedAnalysis } from '../__fixtures__' -import { useProtocolMetadata } from '/app/resources/protocols' -import { getIsOnDevice } from '/app/redux/config' -import { ReturnTip } from '../ReturnTip' - -vi.mock('/app/redux/config') -vi.mock('/app/resources/protocols') - -const render = (props: React.ComponentProps) => { - return renderWithProviders(, { - i18nInstance: i18n, - })[0] -} - -describe('ReturnTip', () => { - let props: React.ComponentProps - let mockChainRunCommands - - beforeEach(() => { - mockChainRunCommands = vi.fn().mockImplementation(() => Promise.resolve()) - vi.mocked(getIsOnDevice).mockReturnValue(false) - props = { - section: SECTIONS.RETURN_TIP, - pipetteId: mockCompletedAnalysis.pipettes[0].id, - labwareId: mockCompletedAnalysis.labware[0].id, - definitionUri: mockCompletedAnalysis.labware[0].definitionUri, - location: { slotName: 'D1' }, - protocolData: mockCompletedAnalysis, - proceed: vi.fn(), - setFatalError: vi.fn(), - chainRunCommands: mockChainRunCommands, - tipPickUpOffset: null, - isRobotMoving: false, - robotType: FLEX_ROBOT_TYPE, - } - vi.mocked(useProtocolMetadata).mockReturnValue({ - robotType: 'OT-3 Standard', - }) - }) - afterEach(() => { - vi.restoreAllMocks() - }) - it('renders correct copy on desktop', () => { - render(props) - screen.getByRole('heading', { name: 'Return tip rack to Slot D1' }) - screen.getByText( - 'Clear all deck slots of labware, leaving modules in place' - ) - screen.getByText(/Mock TipRack Definition/i) - screen.getByText(/that you used before back into/i) - screen.getByText('Slot D1') - screen.getByText( - /The pipette will return tips to their original location in the rack./i - ) - screen.getByRole('link', { name: 'Need help?' }) - }) - it('renders correct copy on device', () => { - vi.mocked(getIsOnDevice).mockReturnValue(true) - render(props) - screen.getByRole('heading', { name: 'Return tip rack to Slot D1' }) - screen.getByText('Clear all deck slots of labware') - screen.getByText(/Mock TipRack Definition/i) - screen.getByText(/that you used before back into/i) - screen.getByText('Slot D1') - screen.getByText( - /The pipette will return tips to their original location in the rack./i - ) - }) - it('executes correct chained commands when CTA is clicked', async () => { - render(props) - fireEvent.click(screen.getByRole('button', { name: 'Confirm placement' })) - await expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 1, - [ - { - commandType: 'moveLabware', - params: { - labwareId: 'labwareId1', - newLocation: { slotName: 'D1' }, - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'moveToWell', - params: { - pipetteId: 'pipetteId1', - labwareId: 'labwareId1', - wellName: 'A1', - wellLocation: { origin: 'top', offset: undefined }, - }, - }, - { - commandType: 'dropTip', - params: { - pipetteId: 'pipetteId1', - labwareId: 'labwareId1', - wellName: 'A1', - wellLocation: { origin: 'default', offset: undefined }, - }, - }, - { - commandType: 'moveLabware', - params: { - labwareId: 'labwareId1', - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - { commandType: 'home', params: {} }, - ], - false - ) - // temporary comment-out - // await expect(props.proceed).toHaveBeenCalled() - }) - it('executes correct chained commands with tip pick up offset when CTA is clicked', async () => { - props = { - ...props, - tipPickUpOffset: { x: 10, y: 11, z: 12 }, - } - render(props) - fireEvent.click(screen.getByRole('button', { name: 'Confirm placement' })) - await expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 1, - [ - { - commandType: 'moveLabware', - params: { - labwareId: 'labwareId1', - newLocation: { slotName: 'D1' }, - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'moveToWell', - params: { - pipetteId: 'pipetteId1', - labwareId: 'labwareId1', - wellName: 'A1', - wellLocation: { origin: 'top', offset: { x: 10, y: 11, z: 12 } }, - }, - }, - { - commandType: 'dropTip', - params: { - pipetteId: 'pipetteId1', - labwareId: 'labwareId1', - wellName: 'A1', - wellLocation: { - origin: 'default', - offset: { x: 10, y: 11, z: 12 }, - }, - }, - }, - { - commandType: 'moveLabware', - params: { - labwareId: 'labwareId1', - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - { commandType: 'home', params: {} }, - ], - false - ) - // temporary comment-out - // await expect(props.proceed).toHaveBeenCalled() - }) - it('executes heater shaker closed latch commands for every hs module before other commands', async () => { - props = { - ...props, - tipPickUpOffset: { x: 10, y: 11, z: 12 }, - protocolData: { - ...props.protocolData, - modules: [ - { - id: 'firstHSId', - model: HEATERSHAKER_MODULE_V1, - location: { slotName: 'D3' }, - serialNumber: 'firstHSSerial', - }, - { - id: 'secondHSId', - model: HEATERSHAKER_MODULE_V1, - location: { slotName: 'A1' }, - serialNumber: 'secondHSSerial', - }, - ], - }, - } - render(props) - fireEvent.click(screen.getByRole('button', { name: 'Confirm placement' })) - await expect(props.chainRunCommands).toHaveBeenNthCalledWith( - 1, - [ - { - commandType: 'heaterShaker/closeLabwareLatch', - params: { moduleId: 'firstHSId' }, - }, - { - commandType: 'heaterShaker/closeLabwareLatch', - params: { moduleId: 'secondHSId' }, - }, - { - commandType: 'moveLabware', - params: { - labwareId: 'labwareId1', - newLocation: { slotName: 'D1' }, - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'moveToWell', - params: { - pipetteId: 'pipetteId1', - labwareId: 'labwareId1', - wellName: 'A1', - wellLocation: { origin: 'top', offset: { x: 10, y: 11, z: 12 } }, - }, - }, - { - commandType: 'dropTip', - params: { - pipetteId: 'pipetteId1', - labwareId: 'labwareId1', - wellName: 'A1', - wellLocation: { - origin: 'default', - offset: { x: 10, y: 11, z: 12 }, - }, - }, - }, - { - commandType: 'moveLabware', - params: { - labwareId: 'labwareId1', - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - { commandType: 'home', params: {} }, - ], - false - ) - // temporary comment-out - // await expect(props.proceed).toHaveBeenCalled() - }) -}) diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/RobotMotionLoader.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/RobotMotionLoader.test.tsx deleted file mode 100644 index fc2c49aa8f5..00000000000 --- a/app/src/organisms/LabwarePositionCheck/__tests__/RobotMotionLoader.test.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { screen } from '@testing-library/react' -import { describe, it } from 'vitest' -import { renderWithProviders } from '/app/__testing-utils__' -import { i18n } from '/app/i18n' -import { RobotMotionLoader } from '../RobotMotionLoader' - -const mockHeader = 'Stand back, robot needs some space right now' - -const render = () => { - return renderWithProviders(, { - i18nInstance: i18n, - })[0] -} - -describe('Robot in Motion Modal', () => { - it('should render robot in motion loader with header', () => { - render() - screen.getByRole('heading', { name: mockHeader }) - }) -}) diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/TipConfirmation.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/TipConfirmation.test.tsx deleted file mode 100644 index 8f8878a7122..00000000000 --- a/app/src/organisms/LabwarePositionCheck/__tests__/TipConfirmation.test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import type * as React from 'react' -import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest' -import { fireEvent, screen } from '@testing-library/react' -import { TipConfirmation } from '../TipConfirmation' -import { i18n } from '/app/i18n' -import { renderWithProviders } from '/app/__testing-utils__' - -const render = (props: React.ComponentProps) => { - return renderWithProviders(, { - i18nInstance: i18n, - })[0] -} - -describe('TipConfirmation', () => { - let props: React.ComponentProps - - beforeEach(() => { - props = { - invalidateTip: vi.fn(), - confirmTip: vi.fn(), - } - }) - afterEach(() => { - vi.restoreAllMocks() - }) - it('should render correct copy', () => { - render(props) - screen.getByText('Did pipette pick up tip successfully?') - screen.getByRole('button', { name: 'Yes' }) - screen.getByRole('button', { name: 'Try again' }) - }) - it('should invoke callback props when ctas are clicked', () => { - render(props) - fireEvent.click(screen.getByRole('button', { name: 'Try again' })) - expect(props.invalidateTip).toHaveBeenCalled() - fireEvent.click(screen.getByRole('button', { name: 'Yes' })) - expect(props.confirmTip).toHaveBeenCalled() - }) -}) diff --git a/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx b/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx deleted file mode 100644 index b998e782cf9..00000000000 --- a/app/src/organisms/LabwarePositionCheck/__tests__/useLaunchLPC.test.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import type * as React from 'react' -import { Provider } from 'react-redux' -import configureStore from 'redux-mock-store' -import { when } from 'vitest-when' -import { - act, - fireEvent, - renderHook, - screen, - waitFor, -} from '@testing-library/react' -import { describe, it, beforeEach, afterEach, vi, expect } from 'vitest' -import { QueryClient, QueryClientProvider } from 'react-query' - -import { - useCreateMaintenanceRunLabwareDefinitionMutation, - useDeleteMaintenanceRunMutation, -} from '@opentrons/react-api-client' -import { FLEX_ROBOT_TYPE, fixtureTiprack300ul } from '@opentrons/shared-data' - -import { renderWithProviders } from '/app/__testing-utils__' -import { - useCreateTargetedMaintenanceRunMutation, - useNotifyRunQuery, - useMostRecentCompletedAnalysis, -} from '/app/resources/runs' -import { useLPCFlows } from '../useLPCFlows' -import { LabwarePositionCheck } from '..' - -import type { Mock } from 'vitest' -import type { LabwareOffset } from '@opentrons/api-client' -import type { LabwareDefinition2 } from '@opentrons/shared-data' - -vi.mock('../') -vi.mock('@opentrons/react-api-client') -vi.mock('/app/resources/runs') - -const MOCK_RUN_ID = 'mockRunId' -const MOCK_MAINTENANCE_RUN_ID = 'mockMaintenanceRunId' -const mockCurrentOffsets: LabwareOffset[] = [ - { - createdAt: '2022-12-20T14:06:23.562082+00:00', - definitionUri: 'opentrons/opentrons_96_tiprack_10ul/1', - id: 'dceac542-bca4-4313-82ba-d54a19dab204', - location: { slotName: '2' }, - vector: { x: 1, y: 2, z: 3 }, - }, - { - createdAt: '2022-12-20T14:06:23.562878+00:00', - definitionUri: - 'opentrons/opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat/1', - id: '70ae2e31-716b-4e1f-a90c-9b0dfd4d7feb', - location: { slotName: '1', moduleModel: 'heaterShakerModuleV1' }, - vector: { x: 0, y: 0, z: 0 }, - }, -] -const mockLabwareDef = fixtureTiprack300ul as LabwareDefinition2 - -describe('useLaunchLPC hook', () => { - let wrapper: React.FunctionComponent<{ children: React.ReactNode }> - let mockCreateMaintenanceRun: Mock - let mockCreateLabwareDefinition: Mock - let mockDeleteMaintenanceRun: Mock - const mockStore = configureStore() - - beforeEach(() => { - const queryClient = new QueryClient() - mockCreateMaintenanceRun = vi.fn((_data, opts) => { - const results = { data: { id: MOCK_MAINTENANCE_RUN_ID } } - opts?.onSuccess(results) - return Promise.resolve(results) - }) - mockCreateLabwareDefinition = vi.fn(_data => - Promise.resolve({ data: { definitionUri: 'fakeDefUri' } }) - ) - mockDeleteMaintenanceRun = vi.fn((_data, opts) => { - opts?.onSettled() - }) - const store = mockStore({ isOnDevice: false }) - wrapper = ({ children }) => ( - - - {children} - - - ) - vi.mocked(LabwarePositionCheck).mockImplementation(({ onCloseClick }) => ( -
{ - onCloseClick() - }} - > - exit -
- )) - when(vi.mocked(useNotifyRunQuery)) - .calledWith(MOCK_RUN_ID, { staleTime: Infinity }) - .thenReturn({ - data: { - data: { - labwareOffsets: mockCurrentOffsets, - }, - }, - } as any) - when(vi.mocked(useCreateTargetedMaintenanceRunMutation)) - .calledWith() - .thenReturn({ - createTargetedMaintenanceRun: mockCreateMaintenanceRun, - } as any) - when(vi.mocked(useCreateMaintenanceRunLabwareDefinitionMutation)) - .calledWith() - .thenReturn({ - createLabwareDefinition: mockCreateLabwareDefinition, - } as any) - when(vi.mocked(useDeleteMaintenanceRunMutation)) - .calledWith() - .thenReturn({ - deleteMaintenanceRun: mockDeleteMaintenanceRun, - } as any) - when(vi.mocked(useMostRecentCompletedAnalysis)) - .calledWith(MOCK_RUN_ID) - .thenReturn({ - commands: [ - { - key: 'CommandKey0', - commandType: 'loadLabware', - params: { - labwareId: 'firstLabwareId', - location: { slotName: '1' }, - displayName: 'first labware nickname', - }, - result: { - labwareId: 'firstLabwareId', - definition: mockLabwareDef, - offset: { x: 0, y: 0, z: 0 }, - }, - id: 'CommandId0', - status: 'succeeded', - error: null, - createdAt: 'fakeCreatedAtTimestamp', - startedAt: 'fakeStartedAtTimestamp', - completedAt: 'fakeCompletedAtTimestamp', - }, - ], - } as any) - }) - afterEach(() => { - vi.resetAllMocks() - }) - - it('returns and no wizard by default', () => { - const { result } = renderHook( - () => useLPCFlows(MOCK_RUN_ID, FLEX_ROBOT_TYPE), - { wrapper } - ) - expect(result.current.LPCWizard).toEqual(null) - }) - - it('returns creates maintenance run with current offsets and definitions when create callback is called, closes and deletes when exit is clicked', async () => { - const { result } = renderHook( - () => useLPCFlows(MOCK_RUN_ID, FLEX_ROBOT_TYPE), - { wrapper } - ) - act(() => { - result.current.launchLPC() - }) - await waitFor(() => { - expect(mockCreateLabwareDefinition).toHaveBeenCalledWith({ - maintenanceRunId: MOCK_MAINTENANCE_RUN_ID, - labwareDef: mockLabwareDef, - }) - }) - - await waitFor(() => { - expect(mockCreateMaintenanceRun).toHaveBeenCalledWith({ - labwareOffsets: mockCurrentOffsets.map( - ({ vector, location, definitionUri }) => ({ - vector, - location, - definitionUri, - }) - ), - }) - }) - - await waitFor(() => { - expect(result.current.LPCWizard).not.toBeNull() - }) - renderWithProviders(result.current.LPCWizard ?? <>) - fireEvent.click(screen.getByText('exit')) - expect(mockDeleteMaintenanceRun).toHaveBeenCalledWith( - MOCK_MAINTENANCE_RUN_ID, - { - onSettled: expect.any(Function), - } - ) - expect(result.current.LPCWizard).toBeNull() - }) -}) diff --git a/app/src/organisms/LabwarePositionCheck/components/index.ts b/app/src/organisms/LabwarePositionCheck/components/index.ts new file mode 100644 index 00000000000..c0c33ab559c --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/components/index.ts @@ -0,0 +1,2 @@ +//TOME TODO: The shared UI should go here. Might want to rename it something, IDK. +// TOME TODO: Because there will likely be Flex/OT-2 specific screens. Shared shouldn't be a top level field. Maybe name it components or something. diff --git a/app/src/organisms/LabwarePositionCheck/constants.ts b/app/src/organisms/LabwarePositionCheck/constants.ts deleted file mode 100644 index f35be0d04d2..00000000000 --- a/app/src/organisms/LabwarePositionCheck/constants.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const SECTIONS = { - BEFORE_BEGINNING: 'BEFORE_BEGINNING', - ATTACH_PROBE: 'ATTACH_PROBE', - CHECK_TIP_RACKS: 'CHECK_TIP_RACKS', - PICK_UP_TIP: 'PICK_UP_TIP', - CHECK_LABWARE: 'CHECK_LABWARE', - CHECK_POSITIONS: 'CHECK_POSITIONS', - RETURN_TIP: 'RETURN_TIP', - DETACH_PROBE: 'DETACH_PROBE', - RESULTS_SUMMARY: 'RESULTS_SUMMARY', -} as const diff --git a/app/src/organisms/LabwarePositionCheck/constants/index.ts b/app/src/organisms/LabwarePositionCheck/constants/index.ts new file mode 100644 index 00000000000..d6f2f62a6d5 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/constants/index.ts @@ -0,0 +1 @@ +export * from './routing' diff --git a/app/src/organisms/LabwarePositionCheck/constants/routing.ts b/app/src/organisms/LabwarePositionCheck/constants/routing.ts new file mode 100644 index 00000000000..d96edaee732 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/constants/routing.ts @@ -0,0 +1,23 @@ +export const NAV_STEPS = { + BEFORE_BEGINNING: 'BEFORE_BEGINNING', + ATTACH_PROBE: 'ATTACH_PROBE', + CHECK_TIP_RACKS: 'CHECK_TIP_RACKS', + PICK_UP_TIP: 'PICK_UP_TIP', + CHECK_LABWARE: 'CHECK_LABWARE', + CHECK_POSITIONS: 'CHECK_POSITIONS', + RETURN_TIP: 'RETURN_TIP', + DETACH_PROBE: 'DETACH_PROBE', + RESULTS_SUMMARY: 'RESULTS_SUMMARY', +} as const + +export const NAV_MOTION = { + IN_MOTION: 'IN_MOTION', +} + +// For errors, door open CTAs, etc. +export const NAV_ALERTS = {} + +// TOME: TODO: Can we separate the flex steps from the OT-2 steps? Yeah definitely, since there's no tip rack checking in Flex. +export const NAV_STEPS_FLEX: Array = [] + +export const NAV_STEPS_OT2: Array = [] diff --git a/app/src/organisms/LabwarePositionCheck/hooks/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/index.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/src/organisms/LabwarePositionCheck/types.ts b/app/src/organisms/LabwarePositionCheck/types.ts index 2ddd14c25d6..79155d426f5 100644 --- a/app/src/organisms/LabwarePositionCheck/types.ts +++ b/app/src/organisms/LabwarePositionCheck/types.ts @@ -1,4 +1,4 @@ -import type { SECTIONS } from './constants' +import type { NAV_STEPS } from './constants' import type { useCreateCommandMutation } from '@opentrons/react-api-client' import type { LabwareOffsetLocation, VectorOffset } from '@opentrons/api-client' import type { LabwareDefinition2 } from '@opentrons/shared-data' @@ -14,10 +14,10 @@ export type LabwarePositionCheckStep = | DetachProbeStep | ResultsSummaryStep export interface BeforeBeginningStep { - section: typeof SECTIONS.BEFORE_BEGINNING + section: typeof NAV_STEPS.BEFORE_BEGINNING } export interface CheckTipRacksStep { - section: typeof SECTIONS.CHECK_TIP_RACKS + section: typeof NAV_STEPS.CHECK_TIP_RACKS pipetteId: string labwareId: string location: LabwareOffsetLocation @@ -25,11 +25,11 @@ export interface CheckTipRacksStep { adapterId?: string } export interface AttachProbeStep { - section: typeof SECTIONS.ATTACH_PROBE + section: typeof NAV_STEPS.ATTACH_PROBE pipetteId: string } export interface PickUpTipStep { - section: typeof SECTIONS.PICK_UP_TIP + section: typeof NAV_STEPS.PICK_UP_TIP pipetteId: string labwareId: string location: LabwareOffsetLocation @@ -37,7 +37,7 @@ export interface PickUpTipStep { adapterId?: string } export interface CheckPositionsStep { - section: typeof SECTIONS.CHECK_POSITIONS + section: typeof NAV_STEPS.CHECK_POSITIONS pipetteId: string labwareId: string location: LabwareOffsetLocation @@ -45,7 +45,7 @@ export interface CheckPositionsStep { moduleId?: string } export interface CheckLabwareStep { - section: typeof SECTIONS.CHECK_LABWARE + section: typeof NAV_STEPS.CHECK_LABWARE pipetteId: string labwareId: string location: LabwareOffsetLocation @@ -54,7 +54,7 @@ export interface CheckLabwareStep { adapterId?: string } export interface ReturnTipStep { - section: typeof SECTIONS.RETURN_TIP + section: typeof NAV_STEPS.RETURN_TIP pipetteId: string labwareId: string location: LabwareOffsetLocation @@ -62,11 +62,11 @@ export interface ReturnTipStep { adapterId?: string } export interface DetachProbeStep { - section: typeof SECTIONS.DETACH_PROBE + section: typeof NAV_STEPS.DETACH_PROBE pipetteId: string } export interface ResultsSummaryStep { - section: typeof SECTIONS.RESULTS_SUMMARY + section: typeof NAV_STEPS.RESULTS_SUMMARY } type CreateCommandMutate = ReturnType< diff --git a/app/src/organisms/LabwarePositionCheck/utils/getProbeBasedLPCSteps.ts b/app/src/organisms/LabwarePositionCheck/utils/getProbeBasedLPCSteps.ts index d584399457d..3deb4a0acf6 100644 --- a/app/src/organisms/LabwarePositionCheck/utils/getProbeBasedLPCSteps.ts +++ b/app/src/organisms/LabwarePositionCheck/utils/getProbeBasedLPCSteps.ts @@ -1,5 +1,5 @@ import { isEqual } from 'lodash' -import { SECTIONS } from '../constants' +import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' import { getLabwareDefURI, getPipetteNameSpecs } from '@opentrons/shared-data' import { getLabwareLocationCombos } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' @@ -29,17 +29,17 @@ export const getProbeBasedLPCSteps = ( protocolData: CompletedProtocolAnalysis ): LabwarePositionCheckStep[] => { return [ - { section: SECTIONS.BEFORE_BEGINNING }, + { section: NAV_STEPS.BEFORE_BEGINNING }, { - section: SECTIONS.ATTACH_PROBE, + section: NAV_STEPS.ATTACH_PROBE, pipetteId: getPrimaryPipetteId(protocolData.pipettes), }, ...getAllCheckSectionSteps(protocolData), { - section: SECTIONS.DETACH_PROBE, + section: NAV_STEPS.DETACH_PROBE, pipetteId: getPrimaryPipetteId(protocolData.pipettes), }, - { section: SECTIONS.RESULTS_SUMMARY }, + { section: NAV_STEPS.RESULTS_SUMMARY }, ] } @@ -78,7 +78,7 @@ function getAllCheckSectionSteps( return labwareLocations.map( ({ location, labwareId, moduleId, adapterId, definitionUri }) => ({ - section: SECTIONS.CHECK_POSITIONS, + section: NAV_STEPS.CHECK_POSITIONS, labwareId: labwareId, pipetteId: getPrimaryPipetteId(pipettes), location, diff --git a/app/src/organisms/LabwarePositionCheck/utils/getTipBasedLPCSteps.ts b/app/src/organisms/LabwarePositionCheck/utils/getTipBasedLPCSteps.ts index 2aba09b84f8..8575bdd7154 100644 --- a/app/src/organisms/LabwarePositionCheck/utils/getTipBasedLPCSteps.ts +++ b/app/src/organisms/LabwarePositionCheck/utils/getTipBasedLPCSteps.ts @@ -1,5 +1,5 @@ import { isEqual } from 'lodash' -import { SECTIONS } from '../constants' +import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' import { getLabwareDefURI, @@ -43,7 +43,7 @@ export const getTipBasedLPCSteps = ( checkTipRacksSectionSteps[checkTipRacksSectionSteps.length - 1] const pickUpTipSectionStep: PickUpTipStep = { - section: SECTIONS.PICK_UP_TIP, + section: NAV_STEPS.PICK_UP_TIP, labwareId: lastTiprackCheckStep.labwareId, pipetteId: lastTiprackCheckStep.pipetteId, location: lastTiprackCheckStep.location, @@ -53,7 +53,7 @@ export const getTipBasedLPCSteps = ( const checkLabwareSectionSteps = getCheckLabwareSectionSteps(args) const returnTipSectionStep: ReturnTipStep = { - section: SECTIONS.RETURN_TIP, + section: NAV_STEPS.RETURN_TIP, labwareId: lastTiprackCheckStep.labwareId, pipetteId: lastTiprackCheckStep.pipetteId, location: lastTiprackCheckStep.location, @@ -62,15 +62,17 @@ export const getTipBasedLPCSteps = ( } return [ - { section: SECTIONS.BEFORE_BEGINNING }, + { section: NAV_STEPS.BEFORE_BEGINNING }, ...allButLastTiprackCheckSteps, pickUpTipSectionStep, ...checkLabwareSectionSteps, returnTipSectionStep, - { section: SECTIONS.RESULTS_SUMMARY }, + { section: NAV_STEPS.RESULTS_SUMMARY }, ] } +// TOME: TODO: Once you get things stable, you can do the labware definition stuff to get +// whether or not is a tiprack. function getCheckTipRackSectionSteps(args: LPCArgs): CheckTipRacksStep[] { const { secondaryPipetteId, @@ -137,7 +139,7 @@ function getCheckTipRackSectionSteps(args: LPCArgs): CheckTipRacksStep[] { return [ ...acc, ...labwareLocations.map(({ location, adapterId, definitionUri }) => ({ - section: SECTIONS.CHECK_TIP_RACKS, + section: NAV_STEPS.CHECK_TIP_RACKS, labwareId: params.labwareId, pipetteId: params.pipetteId, location, @@ -183,7 +185,7 @@ function getCheckLabwareSectionSteps(args: LPCArgs): CheckLabwareStep[] { return [ ...acc, { - section: SECTIONS.CHECK_LABWARE, + section: NAV_STEPS.CHECK_LABWARE, labwareId, pipetteId: primaryPipetteId, location, diff --git a/app/src/pages/ODD/ProtocolSetup/index.tsx b/app/src/pages/ODD/ProtocolSetup/index.tsx index 0684f967bc0..05dad45ae09 100644 --- a/app/src/pages/ODD/ProtocolSetup/index.tsx +++ b/app/src/pages/ODD/ProtocolSetup/index.tsx @@ -88,7 +88,7 @@ import { useRequiredProtocolHardwareFromAnalysis, useMissingProtocolHardwareFromAnalysis, } from '/app/transformations/commands' -import { useLPCFlows } from '/app/src/organisms/LabwarePositionCheck/useLPCFlows' +import { useLPCFlows, LPCFlows } from '/app/organisms/LabwarePositionCheck' import type { Dispatch, SetStateAction } from 'react' import type { Run } from '@opentrons/api-client' @@ -659,7 +659,7 @@ export function ProtocolSetup(): JSX.Element { const { runId } = useParams< keyof OnDeviceRouteParams >() as OnDeviceRouteParams - const isNewLpc = useFeatureFlag('lpcRedesign') + const isNewLPC = useFeatureFlag('lpcRedesign') const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) const { analysisErrors } = useProtocolAnalysisErrors(runId) const { t } = useTranslation(['protocol_setup']) @@ -742,7 +742,11 @@ export function ProtocolSetup(): JSX.Element { robotType, protocolName ) - const { launchLPC, LPCWizard } = useLPCFlows(runId, robotType, protocolName) + const { launchLPC, showLPC, lpcProps } = useLPCFlows({ + runId, + robotType, + protocolName, + }) const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) const robotAnalyticsData = useRobotAnalyticsData(robotName) @@ -826,8 +830,16 @@ export function ProtocolSetup(): JSX.Element { runId={runId} setSetupScreen={setSetupScreen} lpcDisabledReason={lpcDisabledReason} - launchLPC={isNewLpc ? launchLPC : launchLegacyLPC} - LPCWizard={isNewLpc ? LPCWizard : LegacyLPCWizard} + launchLPC={isNewLPC ? launchLPC : launchLegacyLPC} + LPCWizard={ + isNewLPC ? ( + showLPC ? ( + + ) : null + ) : ( + LegacyLPCWizard + ) + } isConfirmed={offsetsConfirmed} setIsConfirmed={setOffsetsConfirmed} /> diff --git a/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx b/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx index 6dd08c20823..2a878cf7e16 100644 --- a/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx +++ b/app/src/pages/ODD/RobotSettingsDashboard/RobotSettingsList.tsx @@ -61,7 +61,7 @@ export function RobotSettingsList(props: RobotSettingsListProps): JSX.Element { 'app_settings', 'branded', ]) - const isNewLpc = useFeatureFlag('lpcRedesign') + const isNewLPC = useFeatureFlag('lpcRedesign') const dispatch = useDispatch() const localRobot = useSelector(getLocalRobot) const robotName = localRobot?.name != null ? localRobot.name : 'no name' @@ -186,7 +186,7 @@ export function RobotSettingsList(props: RobotSettingsListProps): JSX.Element { }} iconName="privacy" /> - {!isNewLpc && ( + {!isNewLPC && ( Date: Thu, 2 Jan 2025 10:27:35 -0500 Subject: [PATCH 05/33] refactor(app, api-client, react-api-client): GET run defs rather than iterating over commands --- .../createMaintenanceRunLabwareDefinition.ts | 12 +++- .../LabwarePositionCheck/LPCFlows.tsx | 66 ++++++++++--------- ...MaintenanceRunLabwareDefinitionMutation.ts | 7 +- 3 files changed, 49 insertions(+), 36 deletions(-) diff --git a/api-client/src/maintenance_runs/createMaintenanceRunLabwareDefinition.ts b/api-client/src/maintenance_runs/createMaintenanceRunLabwareDefinition.ts index 85615b01849..0ee55b9e86c 100644 --- a/api-client/src/maintenance_runs/createMaintenanceRunLabwareDefinition.ts +++ b/api-client/src/maintenance_runs/createMaintenanceRunLabwareDefinition.ts @@ -3,14 +3,20 @@ import { POST, request } from '../request' import type { ResponsePromise } from '../request' import type { HostConfig } from '../types' import type { LabwareDefinitionSummary } from './types' -import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type { + LabwareDefinition2, + LabwareDefinition3, +} from '@opentrons/shared-data' export function createMaintenanceRunLabwareDefinition( config: HostConfig, maintenanceRunId: string, - data: LabwareDefinition2 + data: LabwareDefinition2 | LabwareDefinition3 ): ResponsePromise { - return request( + return request< + LabwareDefinitionSummary, + { data: LabwareDefinition2 | LabwareDefinition3 } + >( POST, `/maintenance_runs/${maintenanceRunId}/labware_definitions`, { data }, diff --git a/app/src/organisms/LabwarePositionCheck/LPCFlows.tsx b/app/src/organisms/LabwarePositionCheck/LPCFlows.tsx index 4c958fc89c6..4058eb22007 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCFlows.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCFlows.tsx @@ -3,6 +3,7 @@ import { useState } from 'react' import { useCreateMaintenanceRunLabwareDefinitionMutation, useDeleteMaintenanceRunMutation, + useRunLoadedLabwareDefinitions, } from '@opentrons/react-api-client' import { @@ -10,7 +11,6 @@ import { useNotifyRunQuery, useMostRecentCompletedAnalysis, } from '/app/resources/runs' -import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' import { LPCWizardContainer } from './LPCWizardContainer' import type { @@ -61,23 +61,34 @@ export function useLPCFlows({ deleteMaintenanceRun, isLoading: isDeletingMaintenanceRun, } = useDeleteMaintenanceRunMutation() + useRunLoadedLabwareDefinitions(runId, { + // TOME TODO: Ideally we don't have to do this POST, since the server has the defs already? + onSuccess: res => { + Promise.all( + res.data.map(def => { + if ('schemaVersion' in def) { + createLabwareDefinition({ + maintenanceRunId: maintenanceRunId as string, + labwareDef: def, + }) + } + }) + ).then(() => { + setHasCreatedLPCRun(true) + }) + }, + onSettled: () => { + // TOME TODO: Think about potentially error handling if there's some sort of failure here? + setIsLaunching(false) + }, + enabled: maintenanceRunId != null, + }) const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) const currentOffsets = runRecord?.data?.labwareOffsets ?? [] - const handleCloseLPC = (): void => { - if (maintenanceRunId != null) { - deleteMaintenanceRun(maintenanceRunId, { - onSettled: () => { - setMaintenanceRunId(null) - setHasCreatedLPCRun(false) - }, - }) - } - } - const launchLPC = (): Promise => { setIsLaunching(true) @@ -89,27 +100,20 @@ export function useLPCFlows({ definitionUri, }) ), - }).then(maintenanceRun => - // TOME: TODO: Swap this out with the nifty new hook. + }).then(maintenanceRun => { + setMaintenanceRunId(maintenanceRun.data.id) + }) + } - // TODO(BC, 2023-05-15): replace this with a call to the protocol run's GET labware_definitions - // endpoint once it's made we should be adding the definitions to the maintenance run by - // reading from the current protocol run, and not from the analysis - Promise.all( - getLabwareDefinitionsFromCommands( - mostRecentAnalysis?.commands ?? [] - ).map(def => { - createLabwareDefinition({ - maintenanceRunId: maintenanceRun?.data?.id, - labwareDef: def, - }) - }) - ).then(() => { - setMaintenanceRunId(maintenanceRun.data.id) - setIsLaunching(false) - setHasCreatedLPCRun(true) + const handleCloseLPC = (): void => { + if (maintenanceRunId != null) { + deleteMaintenanceRun(maintenanceRunId, { + onSettled: () => { + setMaintenanceRunId(null) + setHasCreatedLPCRun(false) + }, }) - ) + } } const showLPC = diff --git a/react-api-client/src/maintenance_runs/useCreateMaintenanceRunLabwareDefinitionMutation.ts b/react-api-client/src/maintenance_runs/useCreateMaintenanceRunLabwareDefinitionMutation.ts index 8b36eb3d100..ac69446d593 100644 --- a/react-api-client/src/maintenance_runs/useCreateMaintenanceRunLabwareDefinitionMutation.ts +++ b/react-api-client/src/maintenance_runs/useCreateMaintenanceRunLabwareDefinitionMutation.ts @@ -10,11 +10,14 @@ import type { LabwareDefinitionSummary, HostConfig, } from '@opentrons/api-client' -import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type { + LabwareDefinition2, + LabwareDefinition3, +} from '@opentrons/shared-data' interface CreateMaintenanceRunLabwareDefinitionMutateParams { maintenanceRunId: string - labwareDef: LabwareDefinition2 + labwareDef: LabwareDefinition2 | LabwareDefinition3 } export type UseCreateLabwareDefinitionMutationResult = UseMutationResult< From 179c9a76c71b5676ea777fe67a4cede0b07e4211 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Thu, 2 Jan 2025 15:05:18 -0500 Subject: [PATCH 06/33] consolidate maintenance run mgmt logic in the lpc hook --- .../LPCFlows/LPCFlows.tsx | 21 +++++ .../LabwarePositionCheck/LPCFlows/index.ts | 4 + .../{LPCFlows.tsx => LPCFlows/useLPCFlows.ts} | 90 +++++++++++-------- .../LPCWizardContainer.tsx | 34 +++---- .../LabwarePositionCheck/LPCWizardFlex.tsx | 52 +---------- .../LabwarePositionCheck/ResultsSummary.tsx | 4 +- .../LabwarePositionCheck/hooks/index.ts | 1 + .../LabwarePositionCheck/hooks/useLPCUtils.ts | 5 ++ .../LabwarePositionCheckComponent.tsx | 49 +--------- .../ResultsSummary.tsx | 4 +- .../LegacyLabwarePositionCheck/index.tsx | 2 - 11 files changed, 102 insertions(+), 164 deletions(-) create mode 100644 app/src/organisms/LabwarePositionCheck/LPCFlows/LPCFlows.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/LPCFlows/index.ts rename app/src/organisms/LabwarePositionCheck/{LPCFlows.tsx => LPCFlows/useLPCFlows.ts} (66%) create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCUtils.ts diff --git a/app/src/organisms/LabwarePositionCheck/LPCFlows/LPCFlows.tsx b/app/src/organisms/LabwarePositionCheck/LPCFlows/LPCFlows.tsx new file mode 100644 index 00000000000..4aa6fd62e59 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/LPCFlows/LPCFlows.tsx @@ -0,0 +1,21 @@ +import { LPCWizardContainer } from '/app/organisms/LabwarePositionCheck/LPCWizardContainer' + +import type { + RobotType, + CompletedProtocolAnalysis, +} from '@opentrons/shared-data' +import type { LabwareOffset } from '@opentrons/api-client' + +export interface LPCFlowsProps { + onCloseClick: () => void + runId: string + robotType: RobotType + existingOffsets: LabwareOffset[] + mostRecentAnalysis: CompletedProtocolAnalysis | null + protocolName: string + maintenanceRunId: string +} + +export function LPCFlows(props: LPCFlowsProps): JSX.Element { + return +} diff --git a/app/src/organisms/LabwarePositionCheck/LPCFlows/index.ts b/app/src/organisms/LabwarePositionCheck/LPCFlows/index.ts new file mode 100644 index 00000000000..5f8b4c9bb88 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/LPCFlows/index.ts @@ -0,0 +1,4 @@ +export { useLPCFlows } from './useLPCFlows' +export * from './LPCFlows' + +export type { UseLPCFlowsProps, UseLPCFlowsResult } from './useLPCFlows' diff --git a/app/src/organisms/LabwarePositionCheck/LPCFlows.tsx b/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts similarity index 66% rename from app/src/organisms/LabwarePositionCheck/LPCFlows.tsx rename to app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts index 4058eb22007..8a1b337eb3b 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCFlows.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { useCreateMaintenanceRunLabwareDefinitionMutation, @@ -11,13 +11,10 @@ import { useNotifyRunQuery, useMostRecentCompletedAnalysis, } from '/app/resources/runs' -import { LPCWizardContainer } from './LPCWizardContainer' +import { useNotifyCurrentMaintenanceRun } from '/app/resources/maintenance_runs' -import type { - RobotType, - CompletedProtocolAnalysis, -} from '@opentrons/shared-data' -import type { LabwareOffset } from '@opentrons/api-client' +import type { RobotType } from '@opentrons/shared-data' +import type { LPCFlowsProps } from '/app/organisms/LabwarePositionCheck/LPCFlows/LPCFlows' interface UseLPCFlowsBase { showLPC: boolean @@ -51,16 +48,19 @@ export function useLPCFlows({ const [isLaunching, setIsLaunching] = useState(false) const [hasCreatedLPCRun, setHasCreatedLPCRun] = useState(false) + const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) + const currentOffsets = runRecord?.data?.labwareOffsets ?? [] + const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) + + useMonitorMaintenanceRunForDeletion({ maintenanceRunId, setMaintenanceRunId }) + const { createTargetedMaintenanceRun, } = useCreateTargetedMaintenanceRunMutation() const { createLabwareDefinition, } = useCreateMaintenanceRunLabwareDefinitionMutation() - const { - deleteMaintenanceRun, - isLoading: isDeletingMaintenanceRun, - } = useDeleteMaintenanceRunMutation() + const { deleteMaintenanceRun } = useDeleteMaintenanceRunMutation() useRunLoadedLabwareDefinitions(runId, { // TOME TODO: Ideally we don't have to do this POST, since the server has the defs already? onSuccess: res => { @@ -84,11 +84,6 @@ export function useLPCFlows({ enabled: maintenanceRunId != null, }) - const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) - const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) - - const currentOffsets = runRecord?.data?.labwareOffsets ?? [] - const launchLPC = (): Promise => { setIsLaunching(true) @@ -131,32 +126,53 @@ export function useLPCFlows({ existingOffsets: currentOffsets, mostRecentAnalysis, protocolName, - maintenanceRunUtils: { - maintenanceRunId, - setMaintenanceRunId, - isDeletingMaintenanceRun, - }, + maintenanceRunId, }, } : { launchLPC, isLaunchingLPC: isLaunching, lpcProps: null, showLPC } } -interface LPCFlowsMaintenanceRunProps { - maintenanceRunId: string - setMaintenanceRunId: (id: string | null) => void - isDeletingMaintenanceRun: boolean -} +const RUN_REFETCH_INTERVAL = 5000 -export interface LPCFlowsProps { - onCloseClick: () => void - runId: string - robotType: RobotType - existingOffsets: LabwareOffset[] - mostRecentAnalysis: CompletedProtocolAnalysis | null - protocolName: string - maintenanceRunUtils: LPCFlowsMaintenanceRunProps -} +// TODO(jh, 01-02-25): Monitor for deletion behavior exists in several other flows. We should consolidate it. -export function LPCFlows(props: LPCFlowsProps): JSX.Element { - return +// Closes the modal in case the run was deleted by the terminate activity modal on the ODD +function useMonitorMaintenanceRunForDeletion({ + maintenanceRunId, + setMaintenanceRunId, +}: { + maintenanceRunId: string | null + setMaintenanceRunId: (id: string | null) => void +}): void { + const [ + monitorMaintenanceRunForDeletion, + setMonitorMaintenanceRunForDeletion, + ] = useState(false) + + // We should start checking for run deletion only after the maintenance run is created + // and the useCurrentRun poll has returned that created id + const { data: maintenanceRunData } = useNotifyCurrentMaintenanceRun({ + refetchInterval: RUN_REFETCH_INTERVAL, + enabled: maintenanceRunId != null, + }) + + useEffect(() => { + if ( + maintenanceRunId !== null && + maintenanceRunData?.data.id === maintenanceRunId + ) { + setMonitorMaintenanceRunForDeletion(true) + } + if ( + maintenanceRunData?.data.id !== maintenanceRunId && + monitorMaintenanceRunForDeletion + ) { + setMaintenanceRunId(null) + } + }, [ + maintenanceRunData?.data.id, + maintenanceRunId, + monitorMaintenanceRunForDeletion, + setMaintenanceRunId, + ]) } diff --git a/app/src/organisms/LabwarePositionCheck/LPCWizardContainer.tsx b/app/src/organisms/LabwarePositionCheck/LPCWizardContainer.tsx index ad29f667fda..2215cd14bc6 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCWizardContainer.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCWizardContainer.tsx @@ -1,4 +1,4 @@ -import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' +import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { LPCWizardFlex } from './LPCWizardFlex' import { LegacyLabwarePositionCheck } from '/app/organisms/LegacyLabwarePositionCheck' @@ -6,26 +6,14 @@ import { LegacyLabwarePositionCheck } from '/app/organisms/LegacyLabwarePosition import type { LPCFlowsProps } from '/app/organisms/LabwarePositionCheck/LPCFlows' export function LPCWizardContainer(props: LPCFlowsProps): JSX.Element { - return props.robotType === FLEX_ROBOT_TYPE ? ( - - ) : ( - - ) -} - -function LPCLegacyAdapter(props: LPCFlowsProps): JSX.Element { - const { - setMaintenanceRunId, - maintenanceRunId, - isDeletingMaintenanceRun, - } = props.maintenanceRunUtils - - return ( - - ) + switch (props.robotType) { + case FLEX_ROBOT_TYPE: + return + case OT2_ROBOT_TYPE: + return + default: { + console.error('Unhandled robot type in LPC.') + return <> + } + } } diff --git a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx index c9e3b535b61..55feaefc934 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useReducer } from 'react' +import { useState, useReducer } from 'react' import { createPortal } from 'react-dom' import isEqual from 'lodash/isEqual' import { useSelector } from 'react-redux' @@ -25,10 +25,7 @@ import { ReturnTip } from './ReturnTip' import { ResultsSummary } from './ResultsSummary' import { FatalError } from './FatalErrorModal' import { RobotMotionLoader } from './RobotMotionLoader' -import { - useChainMaintenanceCommands, - useNotifyCurrentMaintenanceRun, -} from '/app/resources/maintenance_runs' +import { useChainMaintenanceCommands } from '/app/resources/maintenance_runs' import { getLabwarePositionCheckSteps } from './getLabwarePositionCheckSteps' import type { @@ -44,7 +41,6 @@ import type { Axis, Sign, StepSize } from '/app/molecules/JogControls/types' import type { RegisterPositionAction, WorkingOffset } from './types' import type { LPCFlowsProps } from '/app/organisms/LabwarePositionCheck/LPCFlows' -const RUN_REFETCH_INTERVAL = 5000 const JOG_COMMAND_TIMEOUT = 10000 // 10 seconds export const LPCWizardFlex = (props: LPCFlowsProps): JSX.Element | null => { @@ -55,52 +51,13 @@ export const LPCWizardFlex = (props: LPCFlowsProps): JSX.Element | null => { runId, onCloseClick, protocolName, - maintenanceRunUtils, + maintenanceRunId, } = props const { t } = useTranslation(['labware_position_check', 'shared']) - const { - maintenanceRunId, - setMaintenanceRunId, - isDeletingMaintenanceRun, - } = maintenanceRunUtils const isOnDevice = useSelector(getIsOnDevice) const protocolData = mostRecentAnalysis const shouldUseMetalProbe = robotType === FLEX_ROBOT_TYPE - // we should start checking for run deletion only after the maintenance run is created - // and the useCurrentRun poll has returned that created id - const [ - monitorMaintenanceRunForDeletion, - setMonitorMaintenanceRunForDeletion, - ] = useState(false) - - const { data: maintenanceRunData } = useNotifyCurrentMaintenanceRun({ - refetchInterval: RUN_REFETCH_INTERVAL, - enabled: maintenanceRunId != null, - }) - - // this will close the modal in case the run was deleted by the terminate - // activity modal on the ODD - useEffect(() => { - if ( - maintenanceRunId !== null && - maintenanceRunData?.data.id === maintenanceRunId - ) { - setMonitorMaintenanceRunForDeletion(true) - } - if ( - maintenanceRunData?.data.id !== maintenanceRunId && - monitorMaintenanceRunForDeletion - ) { - setMaintenanceRunId(null) - } - }, [ - maintenanceRunData?.data.id, - maintenanceRunId, - monitorMaintenanceRunForDeletion, - setMaintenanceRunId, - ]) - const [fatalError, setFatalError] = useState(null) const [isApplyingOffsets, setIsApplyingOffsets] = useState(false) const [{ workingOffsets, tipPickUpOffset }, registerPosition] = useReducer( @@ -306,11 +263,9 @@ export const LPCWizardFlex = (props: LPCFlowsProps): JSX.Element | null => { Promise.all(offsets.map(data => createLabwareOffset({ runId, data }))) .then(() => { onCloseClick() - setIsApplyingOffsets(false) }) .catch((e: Error) => { setFatalError(`error applying labware offsets: ${e.message}`) - setIsApplyingOffsets(false) }) } @@ -393,7 +348,6 @@ export const LPCWizardFlex = (props: LPCFlowsProps): JSX.Element | null => { existingOffsets, handleApplyOffsets, isApplyingOffsets, - isDeletingMaintenanceRun, }} /> ) diff --git a/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx b/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx index eafda1a2c8a..70e59be141e 100644 --- a/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx +++ b/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx @@ -62,7 +62,6 @@ interface ResultsSummaryProps extends ResultsSummaryStep { existingOffsets: LabwareOffset[] handleApplyOffsets: (offsets: LabwareOffsetCreateData[]) => void isApplyingOffsets: boolean - isDeletingMaintenanceRun: boolean } export const ResultsSummary = ( props: ResultsSummaryProps @@ -74,12 +73,11 @@ export const ResultsSummary = ( handleApplyOffsets, existingOffsets, isApplyingOffsets, - isDeletingMaintenanceRun, } = props const labwareDefinitions = getLabwareDefinitionsFromCommands( protocolData.commands ) - const isSubmittingAndClosing = isApplyingOffsets || isDeletingMaintenanceRun + const isSubmittingAndClosing = isApplyingOffsets const isLabwareOffsetCodeSnippetsOn = useSelector( getIsLabwareOffsetCodeSnippetsOn ) diff --git a/app/src/organisms/LabwarePositionCheck/hooks/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/index.ts index e69de29bb2d..c2e16ba6704 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/index.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/index.ts @@ -0,0 +1 @@ +export * from './useLPCUtils' diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCUtils.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCUtils.ts new file mode 100644 index 00000000000..f1d526b8219 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCUtils.ts @@ -0,0 +1,5 @@ +export interface UseLPCUtilsResult {} + +export function useLPCUtils(): UseLPCUtilsResult { + return {} +} diff --git a/app/src/organisms/LegacyLabwarePositionCheck/LabwarePositionCheckComponent.tsx b/app/src/organisms/LegacyLabwarePositionCheck/LabwarePositionCheckComponent.tsx index 6f0953093a6..f761f4bc395 100644 --- a/app/src/organisms/LegacyLabwarePositionCheck/LabwarePositionCheckComponent.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/LabwarePositionCheckComponent.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useReducer } from 'react' +import { useState, useReducer } from 'react' import { createPortal } from 'react-dom' import isEqual from 'lodash/isEqual' import { useSelector } from 'react-redux' @@ -25,10 +25,7 @@ import { ReturnTip } from './ReturnTip' import { ResultsSummary } from './ResultsSummary' import { FatalError } from './FatalErrorModal' import { RobotMotionLoader } from './RobotMotionLoader' -import { - useChainMaintenanceCommands, - useNotifyCurrentMaintenanceRun, -} from '/app/resources/maintenance_runs' +import { useChainMaintenanceCommands } from '/app/resources/maintenance_runs' import { getLabwarePositionCheckSteps } from './getLabwarePositionCheckSteps' import type { @@ -46,7 +43,6 @@ import type { import type { Axis, Sign, StepSize } from '/app/molecules/JogControls/types' import type { RegisterPositionAction, WorkingOffset } from './types' -const RUN_REFETCH_INTERVAL = 5000 const JOG_COMMAND_TIMEOUT = 10000 // 10 seconds interface LabwarePositionCheckModalProps { runId: string @@ -56,8 +52,6 @@ interface LabwarePositionCheckModalProps { existingOffsets: LabwareOffset[] onCloseClick: () => unknown protocolName: string - setMaintenanceRunId: (id: string | null) => void - isDeletingMaintenanceRun: boolean caughtError?: Error } @@ -71,49 +65,13 @@ export const LabwarePositionCheckComponent = ( runId, maintenanceRunId, onCloseClick, - setMaintenanceRunId, protocolName, - isDeletingMaintenanceRun, } = props const { t } = useTranslation(['labware_position_check', 'shared']) const isOnDevice = useSelector(getIsOnDevice) const protocolData = mostRecentAnalysis const shouldUseMetalProbe = robotType === FLEX_ROBOT_TYPE - // we should start checking for run deletion only after the maintenance run is created - // and the useCurrentRun poll has returned that created id - const [ - monitorMaintenanceRunForDeletion, - setMonitorMaintenanceRunForDeletion, - ] = useState(false) - - const { data: maintenanceRunData } = useNotifyCurrentMaintenanceRun({ - refetchInterval: RUN_REFETCH_INTERVAL, - enabled: maintenanceRunId != null, - }) - - // this will close the modal in case the run was deleted by the terminate - // activity modal on the ODD - useEffect(() => { - if ( - maintenanceRunId !== null && - maintenanceRunData?.data.id === maintenanceRunId - ) { - setMonitorMaintenanceRunForDeletion(true) - } - if ( - maintenanceRunData?.data.id !== maintenanceRunId && - monitorMaintenanceRunForDeletion - ) { - setMaintenanceRunId(null) - } - }, [ - maintenanceRunData?.data.id, - maintenanceRunId, - monitorMaintenanceRunForDeletion, - setMaintenanceRunId, - ]) - const [fatalError, setFatalError] = useState(null) const [isApplyingOffsets, setIsApplyingOffsets] = useState(false) const [{ workingOffsets, tipPickUpOffset }, registerPosition] = useReducer( @@ -315,11 +273,9 @@ export const LabwarePositionCheckComponent = ( Promise.all(offsets.map(data => createLabwareOffset({ runId, data }))) .then(() => { onCloseClick() - setIsApplyingOffsets(false) }) .catch((e: Error) => { setFatalError(`error applying labware offsets: ${e.message}`) - setIsApplyingOffsets(false) }) } @@ -402,7 +358,6 @@ export const LabwarePositionCheckComponent = ( existingOffsets, handleApplyOffsets, isApplyingOffsets, - isDeletingMaintenanceRun, }} /> ) diff --git a/app/src/organisms/LegacyLabwarePositionCheck/ResultsSummary.tsx b/app/src/organisms/LegacyLabwarePositionCheck/ResultsSummary.tsx index eafda1a2c8a..70e59be141e 100644 --- a/app/src/organisms/LegacyLabwarePositionCheck/ResultsSummary.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/ResultsSummary.tsx @@ -62,7 +62,6 @@ interface ResultsSummaryProps extends ResultsSummaryStep { existingOffsets: LabwareOffset[] handleApplyOffsets: (offsets: LabwareOffsetCreateData[]) => void isApplyingOffsets: boolean - isDeletingMaintenanceRun: boolean } export const ResultsSummary = ( props: ResultsSummaryProps @@ -74,12 +73,11 @@ export const ResultsSummary = ( handleApplyOffsets, existingOffsets, isApplyingOffsets, - isDeletingMaintenanceRun, } = props const labwareDefinitions = getLabwareDefinitionsFromCommands( protocolData.commands ) - const isSubmittingAndClosing = isApplyingOffsets || isDeletingMaintenanceRun + const isSubmittingAndClosing = isApplyingOffsets const isLabwareOffsetCodeSnippetsOn = useSelector( getIsLabwareOffsetCodeSnippetsOn ) diff --git a/app/src/organisms/LegacyLabwarePositionCheck/index.tsx b/app/src/organisms/LegacyLabwarePositionCheck/index.tsx index f09721a9ac2..9a576e366e1 100644 --- a/app/src/organisms/LegacyLabwarePositionCheck/index.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/index.tsx @@ -23,8 +23,6 @@ interface LabwarePositionCheckModalProps { mostRecentAnalysis: CompletedProtocolAnalysis | null protocolName: string caughtError?: Error - setMaintenanceRunId: (id: string | null) => void - isDeletingMaintenanceRun: boolean } // We explicitly wrap LabwarePositionCheckComponent in an ErrorBoundary because an error might occur while pulling in From ad42b7869fce31d9a5434d05c448ad118886d152 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Fri, 3 Jan 2025 10:40:31 -0500 Subject: [PATCH 07/33] refactor(app): refactor lpc redux Move redux out of the presentation layer and introduce more of the commonly used boilerplate. The intention is to shift more and more state into redux. --- .../LabwarePositionCheck/AttachProbe.tsx | 12 +-- .../LabwarePositionCheck/CheckItem.tsx | 62 ++++++------ .../LabwarePositionCheck/DetachProbe.tsx | 14 +-- .../IntroScreen/index.tsx | 10 +- .../LPCFlows/useLPCFlows.ts | 2 +- .../LabwarePositionCheck/LPCWizardFlex.tsx | 95 +++---------------- .../LabwarePositionCheck/PickUpTip.tsx | 80 +++++++++------- .../LabwarePositionCheck/ResultsSummary.tsx | 13 ++- .../LabwarePositionCheck/ReturnTip.tsx | 12 ++- .../LabwarePositionCheck/hooks/index.ts | 2 +- .../hooks/useLPCInitialState.ts | 12 +++ .../LabwarePositionCheck/hooks/useLPCUtils.ts | 5 - .../LabwarePositionCheck/redux/actions.ts | 34 +++++++ .../LabwarePositionCheck/redux/constants.ts | 3 + .../LabwarePositionCheck/redux/index.ts | 4 + .../LabwarePositionCheck/redux/reducer.ts | 29 ++++++ .../LabwarePositionCheck/redux/transforms.ts | 48 ++++++++++ .../LabwarePositionCheck/redux/types.ts | 42 ++++++++ .../redux/useLPCReducer.ts | 19 ++++ .../organisms/LabwarePositionCheck/types.ts | 38 +------- 20 files changed, 326 insertions(+), 210 deletions(-) create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState.ts delete mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCUtils.ts create mode 100644 app/src/organisms/LabwarePositionCheck/redux/actions.ts create mode 100644 app/src/organisms/LabwarePositionCheck/redux/constants.ts create mode 100644 app/src/organisms/LabwarePositionCheck/redux/index.ts create mode 100644 app/src/organisms/LabwarePositionCheck/redux/reducer.ts create mode 100644 app/src/organisms/LabwarePositionCheck/redux/transforms.ts create mode 100644 app/src/organisms/LabwarePositionCheck/redux/types.ts create mode 100644 app/src/organisms/LabwarePositionCheck/redux/useLPCReducer.ts diff --git a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx index afd9efba19f..335e4487fae 100644 --- a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx @@ -23,19 +23,19 @@ import type { import type { LabwareOffset } from '@opentrons/api-client' import type { Jog } from '/app/molecules/JogControls/types' import type { useChainRunCommands } from '/app/resources/runs' +import type { AttachProbeStep } from './types' import type { - AttachProbeStep, - RegisterPositionAction, - WorkingOffset, -} from './types' + LPCWizardAction, + LPCWizardState, +} from '/app/organisms/LabwarePositionCheck/redux' interface AttachProbeProps extends AttachProbeStep { protocolData: CompletedProtocolAnalysis proceed: () => void - registerPosition: Dispatch + dispatch: Dispatch + state: LPCWizardState chainRunCommands: ReturnType['chainRunCommands'] setFatalError: (errorMessage: string) => void - workingOffsets: WorkingOffset[] existingOffsets: LabwareOffset[] handleJog: Jog isRobotMoving: boolean diff --git a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx b/app/src/organisms/LabwarePositionCheck/CheckItem.tsx index 734ee6468b1..49d3843316b 100644 --- a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx +++ b/app/src/organisms/LabwarePositionCheck/CheckItem.tsx @@ -28,6 +28,10 @@ import { UnorderedList } from '/app/molecules/UnorderedList' import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' import { getIsOnDevice } from '/app/redux/config' import { getDisplayLocation } from './utils/getDisplayLocation' +import { + setFinalPosition, + setInitialPosition, +} from '/app/organisms/LabwarePositionCheck/redux/actions' import type { Dispatch } from 'react' import type { LabwareOffset } from '@opentrons/api-client' @@ -39,13 +43,13 @@ import type { RobotType, } from '@opentrons/shared-data' import type { useChainRunCommands } from '/app/resources/runs' -import type { - CheckLabwareStep, - RegisterPositionAction, - WorkingOffset, -} from './types' +import type { CheckLabwareStep } from './types' import type { Jog } from '/app/molecules/JogControls/types' import type { TFunction } from 'i18next' +import type { + LPCWizardAction, + LPCWizardState, +} from '/app/organisms/LabwarePositionCheck/redux' const PROBE_LENGTH_MM = 44.5 @@ -55,8 +59,8 @@ interface CheckItemProps extends Omit { proceed: () => void chainRunCommands: ReturnType['chainRunCommands'] setFatalError: (errorMessage: string) => void - registerPosition: Dispatch - workingOffsets: WorkingOffset[] + dispatch: Dispatch + state: LPCWizardState existingOffsets: LabwareOffset[] handleJog: Jog isRobotMoving: boolean @@ -72,8 +76,8 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { location, protocolData, chainRunCommands, - registerPosition, - workingOffsets, + state, + dispatch, proceed, handleJog, isRobotMoving, @@ -83,6 +87,7 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { shouldUseMetalProbe, } = props const { t, i18n } = useTranslation(['labware_position_check', 'shared']) + const { workingOffsets } = state const isOnDevice = useSelector(getIsOnDevice) const labwareDef = getLabwareDef(labwareId, protocolData) const pipette = protocolData.pipettes.find( @@ -306,12 +311,13 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { const finalResponse = responses[responses.length - 1] if (finalResponse.data.commandType === 'savePosition') { const { position } = finalResponse.data?.result ?? { position: null } - registerPosition({ - type: 'initialPosition', - labwareId, - location, - position, - }) + dispatch( + setInitialPosition({ + labwareId, + location, + position, + }) + ) } else { setFatalError( `CheckItem failed to save position for initial placement.` @@ -397,12 +403,13 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { const firstResponse = responses[0] if (firstResponse.data.commandType === 'savePosition') { const { position } = firstResponse.data?.result ?? { position: null } - registerPosition({ - type: 'finalPosition', - labwareId, - location, - position, - }) + dispatch( + setFinalPosition({ + labwareId, + location, + position, + }) + ) proceed() } else { setFatalError('CheckItem failed to save final position with message') @@ -424,12 +431,13 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { false ) .then(() => { - registerPosition({ - type: 'initialPosition', - labwareId, - location, - position: null, - }) + dispatch( + setInitialPosition({ + labwareId, + location, + position: null, + }) + ) }) .catch((e: Error) => { setFatalError(`CheckItem failed to home: ${e.message}`) diff --git a/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx b/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx index dd040654a23..c0b33bff7a8 100644 --- a/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx @@ -18,21 +18,21 @@ import type { Dispatch } from 'react' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' import type { Jog } from '/app/molecules/JogControls/types' import type { useChainRunCommands } from '/app/resources/runs' -import type { - DetachProbeStep, - RegisterPositionAction, - WorkingOffset, -} from './types' +import type { DetachProbeStep } from './types' import type { LabwareOffset } from '@opentrons/api-client' +import type { + LPCWizardAction, + LPCWizardState, +} from '/app/organisms/LabwarePositionCheck/redux' interface DetachProbeProps extends DetachProbeStep { protocolData: CompletedProtocolAnalysis proceed: () => void - registerPosition: Dispatch chainRunCommands: ReturnType['chainRunCommands'] setFatalError: (errorMessage: string) => void - workingOffsets: WorkingOffset[] existingOffsets: LabwareOffset[] + dispatch: Dispatch + state: LPCWizardState handleJog: Jog isRobotMoving: boolean } diff --git a/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx b/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx index 44e5eb67ded..d202c2a61e8 100644 --- a/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx @@ -39,10 +39,11 @@ import type { LabwareDefinition2, } from '@opentrons/shared-data' import type { useChainRunCommands } from '/app/resources/runs' -import type { RegisterPositionAction } from '../types' import type { Jog } from '/app/molecules/JogControls' - -export const INTERVAL_MS = 3000 +import type { + LPCWizardAction, + LPCWizardState, +} from '/app/organisms/LabwarePositionCheck/redux' // TODO(BC, 09/01/23): replace updated support article link for LPC on OT-2/Flex const SUPPORT_PAGE_URL = 'https://support.opentrons.com/s/ot2-calibration' @@ -50,7 +51,8 @@ const SUPPORT_PAGE_URL = 'https://support.opentrons.com/s/ot2-calibration' export const IntroScreen = (props: { proceed: () => void protocolData: CompletedProtocolAnalysis - registerPosition: Dispatch + dispatch: Dispatch + state: LPCWizardState chainRunCommands: ReturnType['chainRunCommands'] handleJog: Jog setFatalError: (errorMessage: string) => void diff --git a/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts b/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts index 8a1b337eb3b..d61172f0c50 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts +++ b/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts @@ -64,7 +64,7 @@ export function useLPCFlows({ useRunLoadedLabwareDefinitions(runId, { // TOME TODO: Ideally we don't have to do this POST, since the server has the defs already? onSuccess: res => { - Promise.all( + void Promise.all( res.data.map(def => { if ('schemaVersion' in def) { createLabwareDefinition({ diff --git a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx index 55feaefc934..cda17acb64d 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx @@ -1,6 +1,5 @@ -import { useState, useReducer } from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' -import isEqual from 'lodash/isEqual' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -27,6 +26,8 @@ import { FatalError } from './FatalErrorModal' import { RobotMotionLoader } from './RobotMotionLoader' import { useChainMaintenanceCommands } from '/app/resources/maintenance_runs' import { getLabwarePositionCheckSteps } from './getLabwarePositionCheckSteps' +import { useLPCInitialState } from '/app/organisms/LabwarePositionCheck/hooks' +import { useLPCReducer } from '/app/organisms/LabwarePositionCheck/redux' import type { Coordinates, @@ -38,12 +39,11 @@ import type { CommandData, } from '@opentrons/api-client' import type { Axis, Sign, StepSize } from '/app/molecules/JogControls/types' -import type { RegisterPositionAction, WorkingOffset } from './types' import type { LPCFlowsProps } from '/app/organisms/LabwarePositionCheck/LPCFlows' const JOG_COMMAND_TIMEOUT = 10000 // 10 seconds -export const LPCWizardFlex = (props: LPCFlowsProps): JSX.Element | null => { +export function LPCWizardFlex(props: LPCFlowsProps): JSX.Element | null { const { mostRecentAnalysis, existingOffsets, @@ -60,74 +60,12 @@ export const LPCWizardFlex = (props: LPCFlowsProps): JSX.Element | null => { const [fatalError, setFatalError] = useState(null) const [isApplyingOffsets, setIsApplyingOffsets] = useState(false) - const [{ workingOffsets, tipPickUpOffset }, registerPosition] = useReducer( - ( - state: { - workingOffsets: WorkingOffset[] - tipPickUpOffset: Coordinates | null - }, - action: RegisterPositionAction - ) => { - if (action.type === 'tipPickUpOffset') { - return { ...state, tipPickUpOffset: action.offset } - } - if ( - action.type === 'initialPosition' || - action.type === 'finalPosition' - ) { - const { labwareId, location, position } = action - const existingRecordIndex = state.workingOffsets.findIndex( - record => - record.labwareId === labwareId && isEqual(record.location, location) - ) - if (existingRecordIndex >= 0) { - if (action.type === 'initialPosition') { - return { - ...state, - workingOffsets: [ - ...state.workingOffsets.slice(0, existingRecordIndex), - { - ...state.workingOffsets[existingRecordIndex], - initialPosition: position, - finalPosition: null, - }, - ...state.workingOffsets.slice(existingRecordIndex + 1), - ], - } - } else if (action.type === 'finalPosition') { - return { - ...state, - workingOffsets: [ - ...state.workingOffsets.slice(0, existingRecordIndex), - { - ...state.workingOffsets[existingRecordIndex], - finalPosition: position, - }, - ...state.workingOffsets.slice(existingRecordIndex + 1), - ], - } - } - } - return { - ...state, - workingOffsets: [ - ...state.workingOffsets, - { - labwareId, - location, - initialPosition: - action.type === 'initialPosition' ? position : null, - finalPosition: action.type === 'finalPosition' ? position : null, - }, - ], - } - } else { - return state - } - }, - { workingOffsets: [], tipPickUpOffset: null } - ) + // TOME TODO: Like with ER, separate wizard and content. The wizard injects the data layer to the content layer. + + const initialState = useLPCInitialState() + const { state, dispatch } = useLPCReducer(initialState) + const [isExiting, setIsExiting] = useState(false) const { createMaintenanceCommand: createSilentCommand, @@ -250,10 +188,10 @@ export const LPCWizardFlex = (props: LPCFlowsProps): JSX.Element | null => { protocolData, chainRunCommands: chainMaintenanceRunCommands, setFatalError, - registerPosition, + dispatch, handleJog, isRobotMoving: isCommandChainLoading, - workingOffsets, + state, existingOffsets, robotType, } @@ -331,24 +269,19 @@ export const LPCWizardFlex = (props: LPCFlowsProps): JSX.Element | null => { /> ) } else if (currentStep.section === 'RETURN_TIP') { - modalContent = ( - - ) + modalContent = } else if (currentStep.section === 'RESULTS_SUMMARY') { modalContent = ( ) } diff --git a/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx b/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx index de76e855097..f13ebd139e4 100644 --- a/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx +++ b/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx @@ -36,22 +36,27 @@ import type { } from '@opentrons/shared-data' import type { useChainRunCommands } from '/app/resources/runs' import type { Jog } from '/app/molecules/JogControls/types' -import type { - PickUpTipStep, - RegisterPositionAction, - WorkingOffset, -} from './types' +import type { PickUpTipStep } from './types' import type { LabwareOffset } from '@opentrons/api-client' import type { TFunction } from 'i18next' +import type { + LPCWizardAction, + LPCWizardState, +} from '/app/organisms/LabwarePositionCheck/redux' +import { + setFinalPosition, + setInitialPosition, + setTipPickupOffset, +} from '/app/organisms/LabwarePositionCheck/redux/actions' interface PickUpTipProps extends PickUpTipStep { protocolData: CompletedProtocolAnalysis proceed: () => void - registerPosition: Dispatch chainRunCommands: ReturnType['chainRunCommands'] setFatalError: (errorMessage: string) => void - workingOffsets: WorkingOffset[] existingOffsets: LabwareOffset[] + dispatch: Dispatch + state: LPCWizardState handleJog: Jog isRobotMoving: boolean robotType: RobotType @@ -67,17 +72,18 @@ export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { protocolData, proceed, chainRunCommands, - registerPosition, + dispatch, + state, handleJog, isRobotMoving, existingOffsets, - workingOffsets, setFatalError, adapterId, robotType, protocolHasModules, currentStepIndex, } = props + const { workingOffsets } = state const [showTipConfirmation, setShowTipConfirmation] = useState(false) const isOnDevice = useSelector(getIsOnDevice) const labwareDef = getLabwareDef(labwareId, protocolData) @@ -197,12 +203,13 @@ export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { const finalResponse = responses[responses.length - 1] if (finalResponse.data.commandType === 'savePosition') { const { position } = finalResponse.data?.result ?? { position: null } - registerPosition({ - type: 'initialPosition', - labwareId, - location, - position, - }) + dispatch( + setInitialPosition({ + labwareId, + location, + position, + }) + ) } else { setFatalError( `PickUpTip failed to save position for initial placement.` @@ -227,13 +234,14 @@ export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { initialPosition != null && position != null ? getVectorDifference(position, initialPosition) : undefined - registerPosition({ - type: 'finalPosition', - labwareId, - location, - position, - }) - registerPosition({ type: 'tipPickUpOffset', offset: offset ?? null }) + dispatch( + setFinalPosition({ + labwareId, + location, + position, + }) + ) + dispatch(setTipPickupOffset(offset ?? null)) chainRunCommands( [ { @@ -350,13 +358,14 @@ export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { false ) .then(() => { - registerPosition({ type: 'tipPickUpOffset', offset: null }) - registerPosition({ - type: 'finalPosition', - labwareId, - location, - position: null, - }) + dispatch(setTipPickupOffset(null)) + dispatch( + setFinalPosition({ + labwareId, + location, + position: null, + }) + ) setShowTipConfirmation(false) }) .catch((e: Error) => { @@ -378,12 +387,13 @@ export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { false ) .then(() => { - registerPosition({ - type: 'initialPosition', - labwareId, - location, - position: null, - }) + dispatch( + setInitialPosition({ + labwareId, + location, + position: null, + }) + ) }) .catch((e: Error) => { setFatalError( diff --git a/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx b/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx index 70e59be141e..5ddf6a9439c 100644 --- a/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx +++ b/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx @@ -42,6 +42,7 @@ import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analy import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' import { getDisplayLocation } from './utils/getDisplayLocation' +import type { Dispatch } from 'react' import type { CompletedProtocolAnalysis, LabwareDefinition2, @@ -50,15 +51,20 @@ import type { LabwareOffset, LabwareOffsetCreateData, } from '@opentrons/api-client' -import type { ResultsSummaryStep, WorkingOffset } from './types' +import type { ResultsSummaryStep } from './types' import type { TFunction } from 'i18next' +import type { + LPCWizardAction, + LPCWizardState, +} from '/app/organisms/LabwarePositionCheck/redux' const LPC_HELP_LINK_URL = 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' interface ResultsSummaryProps extends ResultsSummaryStep { protocolData: CompletedProtocolAnalysis - workingOffsets: WorkingOffset[] + dispatch: Dispatch + state: LPCWizardState existingOffsets: LabwareOffset[] handleApplyOffsets: (offsets: LabwareOffsetCreateData[]) => void isApplyingOffsets: boolean @@ -69,11 +75,12 @@ export const ResultsSummary = ( const { i18n, t } = useTranslation('labware_position_check') const { protocolData, - workingOffsets, + state, handleApplyOffsets, existingOffsets, isApplyingOffsets, } = props + const { workingOffsets } = state const labwareDefinitions = getLabwareDefinitionsFromCommands( protocolData.commands ) diff --git a/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx b/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx index fce1f443829..c0085272656 100644 --- a/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx +++ b/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx @@ -26,17 +26,22 @@ import type { RobotType, MoveLabwareCreateCommand, } from '@opentrons/shared-data' -import type { VectorOffset } from '@opentrons/api-client' import type { useChainRunCommands } from '/app/resources/runs' import type { ReturnTipStep } from './types' import type { TFunction } from 'i18next' +import type { Dispatch } from 'react' +import type { + LPCWizardAction, + LPCWizardState, +} from '/app/organisms/LabwarePositionCheck/redux' interface ReturnTipProps extends ReturnTipStep { protocolData: CompletedProtocolAnalysis proceed: () => void chainRunCommands: ReturnType['chainRunCommands'] setFatalError: (errorMessage: string) => void - tipPickUpOffset: VectorOffset | null + dispatch: Dispatch + state: LPCWizardState isRobotMoving: boolean robotType: RobotType } @@ -48,12 +53,13 @@ export const ReturnTip = (props: ReturnTipProps): JSX.Element | null => { location, protocolData, proceed, - tipPickUpOffset, + state, isRobotMoving, chainRunCommands, setFatalError, adapterId, } = props + const { tipPickUpOffset } = state const isOnDevice = useSelector(getIsOnDevice) diff --git a/app/src/organisms/LabwarePositionCheck/hooks/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/index.ts index c2e16ba6704..f8e2d5dc434 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/index.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/index.ts @@ -1 +1 @@ -export * from './useLPCUtils' +export * from './useLPCInitialState' diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState.ts new file mode 100644 index 00000000000..3cdb9b38bd4 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState.ts @@ -0,0 +1,12 @@ +// TOME TODO: I think you could reconsider naming this to something like useLPCState +// by the time you finish this. IDK yet. It might make more sense to inject the state +// and the dispatch into the wizard and have some sort of useInitialLPCState hook. + +import type { LPCWizardState } from '/app/organisms/LabwarePositionCheck/redux' + +export function useLPCInitialState(): LPCWizardState { + return { + workingOffsets: [], + tipPickUpOffset: null, + } +} diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCUtils.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCUtils.ts deleted file mode 100644 index f1d526b8219..00000000000 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCUtils.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface UseLPCUtilsResult {} - -export function useLPCUtils(): UseLPCUtilsResult { - return {} -} diff --git a/app/src/organisms/LabwarePositionCheck/redux/actions.ts b/app/src/organisms/LabwarePositionCheck/redux/actions.ts new file mode 100644 index 00000000000..3f87b8d80fc --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/redux/actions.ts @@ -0,0 +1,34 @@ +import { + SET_INITIAL_POSITION, + SET_FINAL_POSITION, + SET_TIP_PICKUP_OFFSET, +} from './constants' + +import type { Coordinates } from '@opentrons/shared-data' +import type { + InitialPositionAction, + FinalPositionAction, + TipPickUpOffsetAction, + PositionParams, +} from './types' + +export const setTipPickupOffset = ( + offset: Coordinates | null +): TipPickUpOffsetAction => ({ + type: SET_TIP_PICKUP_OFFSET, + payload: { offset }, +}) + +export const setInitialPosition = ( + params: PositionParams +): InitialPositionAction => ({ + type: SET_INITIAL_POSITION, + payload: params, +}) + +export const setFinalPosition = ( + params: PositionParams +): FinalPositionAction => ({ + type: SET_FINAL_POSITION, + payload: params, +}) diff --git a/app/src/organisms/LabwarePositionCheck/redux/constants.ts b/app/src/organisms/LabwarePositionCheck/redux/constants.ts new file mode 100644 index 00000000000..d7ff8c2625c --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/redux/constants.ts @@ -0,0 +1,3 @@ +export const SET_INITIAL_POSITION = 'SET_INITIAL_POSITION' +export const SET_FINAL_POSITION = 'SET_FINAL_POSITION' +export const SET_TIP_PICKUP_OFFSET = 'SET_TIP_PICKUP_OFFSET' diff --git a/app/src/organisms/LabwarePositionCheck/redux/index.ts b/app/src/organisms/LabwarePositionCheck/redux/index.ts new file mode 100644 index 00000000000..5db41ef2b05 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/redux/index.ts @@ -0,0 +1,4 @@ +export * from './useLPCReducer' +export * from './actions' + +export type { LPCWizardAction, LPCWizardState } from './types' diff --git a/app/src/organisms/LabwarePositionCheck/redux/reducer.ts b/app/src/organisms/LabwarePositionCheck/redux/reducer.ts new file mode 100644 index 00000000000..ee3fa7cb77f --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/redux/reducer.ts @@ -0,0 +1,29 @@ +import { updateWorkingOffset } from './transforms' +import { + SET_INITIAL_POSITION, + SET_FINAL_POSITION, + SET_TIP_PICKUP_OFFSET, +} from './constants' + +import type { LPCWizardAction } from './types' +import type { LPCWizardState } from '.' + +export function LPCReducer( + state: LPCWizardState, + action: LPCWizardAction +): LPCWizardState { + switch (action.type) { + case SET_TIP_PICKUP_OFFSET: + return { ...state, tipPickUpOffset: action.payload.offset } + + case SET_INITIAL_POSITION: + case SET_FINAL_POSITION: + return { + ...state, + workingOffsets: updateWorkingOffset(state.workingOffsets, action), + } + + default: + return state + } +} diff --git a/app/src/organisms/LabwarePositionCheck/redux/transforms.ts b/app/src/organisms/LabwarePositionCheck/redux/transforms.ts new file mode 100644 index 00000000000..b3d9654e948 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/redux/transforms.ts @@ -0,0 +1,48 @@ +import isEqual from 'lodash/isEqual' + +import type { LPCWizardAction } from './types' +import type { WorkingOffset } from '/app/organisms/LabwarePositionCheck/types' + +export function updateWorkingOffset( + workingOffsets: WorkingOffset[], + action: Extract< + LPCWizardAction, + { type: 'SET_INITIAL_POSITION' | 'SET_FINAL_POSITION' } + > +): WorkingOffset[] { + const { type, payload } = action + const { labwareId, location, position } = payload + const existingRecordIndex = workingOffsets.findIndex( + record => + record.labwareId === labwareId && isEqual(record.location, location) + ) + + if (existingRecordIndex < 0) { + return [ + ...workingOffsets, + { + labwareId, + location, + initialPosition: type === 'SET_INITIAL_POSITION' ? position : null, + finalPosition: type === 'SET_FINAL_POSITION' ? position : null, + }, + ] + } else { + const updatedOffset = { + ...workingOffsets[existingRecordIndex], + ...(type === 'SET_INITIAL_POSITION' && { + initialPosition: position, + finalPosition: null, + }), + ...(type === 'SET_FINAL_POSITION' && { + finalPosition: position, + }), + } + + return [ + ...workingOffsets.slice(0, existingRecordIndex), + updatedOffset, + ...workingOffsets.slice(existingRecordIndex + 1), + ] + } +} diff --git a/app/src/organisms/LabwarePositionCheck/redux/types.ts b/app/src/organisms/LabwarePositionCheck/redux/types.ts new file mode 100644 index 00000000000..005eaf66df7 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/redux/types.ts @@ -0,0 +1,42 @@ +import type { Coordinates } from '@opentrons/shared-data' +import type { LabwareOffsetLocation, VectorOffset } from '@opentrons/api-client' + +interface WorkingOffset { + labwareId: string + location: LabwareOffsetLocation + initialPosition: VectorOffset | null + finalPosition: VectorOffset | null +} + +export interface PositionParams { + labwareId: string + location: LabwareOffsetLocation + position: VectorOffset | null +} + +export interface InitialPositionAction { + type: 'SET_INITIAL_POSITION' + payload: PositionParams +} + +export interface FinalPositionAction { + type: 'SET_FINAL_POSITION' + payload: PositionParams +} + +export interface TipPickUpOffsetAction { + type: 'SET_TIP_PICKUP_OFFSET' + payload: { + offset: Coordinates | null + } +} + +export interface LPCWizardState { + workingOffsets: WorkingOffset[] + tipPickUpOffset: Coordinates | null +} + +export type LPCWizardAction = + | InitialPositionAction + | FinalPositionAction + | TipPickUpOffsetAction diff --git a/app/src/organisms/LabwarePositionCheck/redux/useLPCReducer.ts b/app/src/organisms/LabwarePositionCheck/redux/useLPCReducer.ts new file mode 100644 index 00000000000..05efeabefe7 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/redux/useLPCReducer.ts @@ -0,0 +1,19 @@ +import { useReducer } from 'react' + +import { LPCReducer } from './reducer' + +import type { Dispatch } from 'react' +import type { LPCWizardAction, LPCWizardState } from './types' + +interface UseLPCReducerResult { + state: LPCWizardState + dispatch: Dispatch +} + +export function useLPCReducer( + initialState: LPCWizardState +): UseLPCReducerResult { + const [state, dispatch] = useReducer(LPCReducer, initialState) + + return { state, dispatch } +} diff --git a/app/src/organisms/LabwarePositionCheck/types.ts b/app/src/organisms/LabwarePositionCheck/types.ts index 79155d426f5..3acb90bd142 100644 --- a/app/src/organisms/LabwarePositionCheck/types.ts +++ b/app/src/organisms/LabwarePositionCheck/types.ts @@ -1,6 +1,5 @@ import type { NAV_STEPS } from './constants' -import type { useCreateCommandMutation } from '@opentrons/react-api-client' -import type { LabwareOffsetLocation, VectorOffset } from '@opentrons/api-client' +import type { LabwareOffsetLocation } from '@opentrons/api-client' import type { LabwareDefinition2 } from '@opentrons/shared-data' export type LabwarePositionCheckStep = @@ -69,41 +68,6 @@ export interface ResultsSummaryStep { section: typeof NAV_STEPS.RESULTS_SUMMARY } -type CreateCommandMutate = ReturnType< - typeof useCreateCommandMutation ->['createCommand'] -export type CreateRunCommand = ( - params: Omit[0], 'runId'>, - options?: Parameters[1] -) => ReturnType - -interface InitialPositionAction { - type: 'initialPosition' - labwareId: string - location: LabwareOffsetLocation - position: VectorOffset | null -} -interface FinalPositionAction { - type: 'finalPosition' - labwareId: string - location: LabwareOffsetLocation - position: VectorOffset | null -} -interface TipPickUpOffsetAction { - type: 'tipPickUpOffset' - offset: VectorOffset | null -} -export type RegisterPositionAction = - | InitialPositionAction - | FinalPositionAction - | TipPickUpOffsetAction -export interface WorkingOffset { - labwareId: string - location: LabwareOffsetLocation - initialPosition: VectorOffset | null - finalPosition: VectorOffset | null -} - export interface LabwareToOrder { definition: LabwareDefinition2 labwareId: string From fbd9be2c3b8735da8af437c82d3f80444f8a2745 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Mon, 6 Jan 2025 09:20:23 -0500 Subject: [PATCH 08/33] refactor(app): roughly split the presentation and data layers A first rough pass at splitting data from presentation. This requires a good bit of typing clean up as well. A couple params from legacy lpc flows are made optional. --- .../LabwarePositionCheck/AttachProbe.tsx | 41 +--- .../getPrepCommands.ts | 0 .../index.tsx | 35 +-- .../LabwarePositionCheck/CheckItem.tsx | 60 ++--- .../LabwarePositionCheck/DetachProbe.tsx | 36 +-- .../LabwarePositionCheck/ExitConfirmation.tsx | 18 +- .../LabwarePositionCheck/FatalErrorModal.tsx | 52 +--- .../LPCFlows/LPCFlows.tsx | 2 +- .../LPCFlows/useLPCFlows.ts | 5 +- .../LabwarePositionCheck/LPCWizardFlex.tsx | 228 +++++++++--------- .../LabwarePositionCheck/PickUpTip.tsx | 65 ++--- .../LabwarePositionCheck/PrepareSpace.tsx | 56 +++-- .../LabwarePositionCheck/ResultsSummary.tsx | 27 +-- .../LabwarePositionCheck/ReturnTip.tsx | 35 +-- .../LabwarePositionCheck/components/index.ts | 1 - .../LabwarePositionCheck/redux/transforms.ts | 3 +- .../LabwarePositionCheck/redux/types.ts | 2 +- .../LabwarePositionCheck/types/content.ts | 55 +++++ .../LabwarePositionCheck/types/index.ts | 2 + .../{types.ts => types/steps.ts} | 71 +++--- .../LabwarePositionCheckComponent.tsx | 49 +++- .../ResultsSummary.tsx | 4 +- .../LegacyLabwarePositionCheck/index.tsx | 2 + 23 files changed, 397 insertions(+), 452 deletions(-) rename app/src/organisms/LabwarePositionCheck/{IntroScreen => BeforeBeginning}/getPrepCommands.ts (100%) rename app/src/organisms/LabwarePositionCheck/{IntroScreen => BeforeBeginning}/index.tsx (87%) create mode 100644 app/src/organisms/LabwarePositionCheck/types/content.ts create mode 100644 app/src/organisms/LabwarePositionCheck/types/index.ts rename app/src/organisms/LabwarePositionCheck/{types.ts => types/steps.ts} (54%) diff --git a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx index 335e4487fae..e214664810c 100644 --- a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx @@ -15,44 +15,23 @@ import attachProbe8 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach import attachProbe96 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_96.webm' import { GenericWizardTile } from '/app/molecules/GenericWizardTile' -import type { Dispatch } from 'react' -import type { - CompletedProtocolAnalysis, - CreateCommand, -} from '@opentrons/shared-data' -import type { LabwareOffset } from '@opentrons/api-client' -import type { Jog } from '/app/molecules/JogControls/types' -import type { useChainRunCommands } from '/app/resources/runs' -import type { AttachProbeStep } from './types' -import type { - LPCWizardAction, - LPCWizardState, -} from '/app/organisms/LabwarePositionCheck/redux' +import type { CreateCommand } from '@opentrons/shared-data' +import type { AttachProbeStep, LPCStepProps } from './types' -interface AttachProbeProps extends AttachProbeStep { - protocolData: CompletedProtocolAnalysis - proceed: () => void - dispatch: Dispatch - state: LPCWizardState - chainRunCommands: ReturnType['chainRunCommands'] - setFatalError: (errorMessage: string) => void - existingOffsets: LabwareOffset[] - handleJog: Jog - isRobotMoving: boolean - isOnDevice: boolean -} - -export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { +export const AttachProbe = ( + props: LPCStepProps +): JSX.Element | null => { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const { - pipetteId, + step, protocolData, proceed, chainRunCommands, isRobotMoving, - setFatalError, + setErrorMessage, isOnDevice, } = props + const { pipetteId } = step const [showUnableToDetect, setShowUnableToDetect] = useState(false) const pipette = protocolData.pipettes.find(p => p.id === pipetteId) @@ -84,7 +63,7 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { ], false ).catch(error => { - setFatalError(error.message as string) + setErrorMessage(error.message as string) }) }, []) @@ -128,7 +107,7 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { proceed() }) .catch((e: Error) => { - setFatalError( + setErrorMessage( `AttachProbe failed to move to safe location after probe attach with message: ${e.message}` ) }) diff --git a/app/src/organisms/LabwarePositionCheck/IntroScreen/getPrepCommands.ts b/app/src/organisms/LabwarePositionCheck/BeforeBeginning/getPrepCommands.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/IntroScreen/getPrepCommands.ts rename to app/src/organisms/LabwarePositionCheck/BeforeBeginning/getPrepCommands.ts diff --git a/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx b/app/src/organisms/LabwarePositionCheck/BeforeBeginning/index.tsx similarity index 87% rename from app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx rename to app/src/organisms/LabwarePositionCheck/BeforeBeginning/index.tsx index d202c2a61e8..19d755e003e 100644 --- a/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/BeforeBeginning/index.tsx @@ -32,41 +32,26 @@ import { CALIBRATION_PROBE } from '/app/organisms/PipetteWizardFlows/constants' import { TerseOffsetTable } from '../ResultsSummary' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' -import type { Dispatch } from 'react' import type { LabwareOffset } from '@opentrons/api-client' +import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { - CompletedProtocolAnalysis, - LabwareDefinition2, -} from '@opentrons/shared-data' -import type { useChainRunCommands } from '/app/resources/runs' -import type { Jog } from '/app/molecules/JogControls' -import type { - LPCWizardAction, - LPCWizardState, -} from '/app/organisms/LabwarePositionCheck/redux' + LPCStepProps, + BeforeBeginningStep, +} from '/app/organisms/LabwarePositionCheck/types' // TODO(BC, 09/01/23): replace updated support article link for LPC on OT-2/Flex const SUPPORT_PAGE_URL = 'https://support.opentrons.com/s/ot2-calibration' -export const IntroScreen = (props: { - proceed: () => void - protocolData: CompletedProtocolAnalysis - dispatch: Dispatch - state: LPCWizardState - chainRunCommands: ReturnType['chainRunCommands'] - handleJog: Jog - setFatalError: (errorMessage: string) => void - isRobotMoving: boolean - existingOffsets: LabwareOffset[] - protocolName: string - shouldUseMetalProbe: boolean -}): JSX.Element | null => { +// TOME TODO: Get rid of the null. +export function BeforeBeginning( + props: LPCStepProps +): JSX.Element | null { const { proceed, protocolData, chainRunCommands, isRobotMoving, - setFatalError, + setErrorMessage, existingOffsets, protocolName, shouldUseMetalProbe, @@ -80,7 +65,7 @@ export const IntroScreen = (props: { proceed() }) .catch((e: Error) => { - setFatalError( + setErrorMessage( `IntroScreen failed to issue prep commands with message: ${e.message}` ) }) diff --git a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx b/app/src/organisms/LabwarePositionCheck/CheckItem.tsx index 49d3843316b..6d6777afa1d 100644 --- a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx +++ b/app/src/organisms/LabwarePositionCheck/CheckItem.tsx @@ -33,47 +33,27 @@ import { setInitialPosition, } from '/app/organisms/LabwarePositionCheck/redux/actions' -import type { Dispatch } from 'react' -import type { LabwareOffset } from '@opentrons/api-client' import type { - CompletedProtocolAnalysis, CreateCommand, LabwareLocation, MoveLabwareCreateCommand, - RobotType, } from '@opentrons/shared-data' -import type { useChainRunCommands } from '/app/resources/runs' -import type { CheckLabwareStep } from './types' -import type { Jog } from '/app/molecules/JogControls/types' -import type { TFunction } from 'i18next' import type { - LPCWizardAction, - LPCWizardState, -} from '/app/organisms/LabwarePositionCheck/redux' + CheckLabwareStep, + CheckPositionsStep, + CheckTipRacksStep, + LPCStepProps, +} from './types' +import type { TFunction } from 'i18next' const PROBE_LENGTH_MM = 44.5 -interface CheckItemProps extends Omit { - section: 'CHECK_LABWARE' | 'CHECK_TIP_RACKS' | 'CHECK_POSITIONS' - protocolData: CompletedProtocolAnalysis - proceed: () => void - chainRunCommands: ReturnType['chainRunCommands'] - setFatalError: (errorMessage: string) => void - dispatch: Dispatch - state: LPCWizardState - existingOffsets: LabwareOffset[] - handleJog: Jog - isRobotMoving: boolean - robotType: RobotType - shouldUseMetalProbe: boolean -} -export const CheckItem = (props: CheckItemProps): JSX.Element | null => { +// TOME TODO: Get rid of the 'null' or jsx here. +export function CheckItem( + props: LPCStepProps +): JSX.Element | null { const { - labwareId, - pipetteId, - moduleId, - adapterId, - location, + step, protocolData, chainRunCommands, state, @@ -82,10 +62,11 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { handleJog, isRobotMoving, existingOffsets, - setFatalError, + setErrorMessage, robotType, shouldUseMetalProbe, } = props + const { labwareId, pipetteId, moduleId, adapterId, location } = step const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const { workingOffsets } = state const isOnDevice = useSelector(getIsOnDevice) @@ -142,7 +123,7 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { chainRunCommands(modulePrepCommands, false) .then(() => {}) .catch((e: Error) => { - setFatalError( + setErrorMessage( `CheckItem module prep commands failed with message: ${e?.message}` ) }) @@ -319,13 +300,13 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { }) ) } else { - setFatalError( + setErrorMessage( `CheckItem failed to save position for initial placement.` ) } }) .catch((e: Error) => { - setFatalError( + setErrorMessage( `CheckItem failed to save position for initial placement with message: ${e.message}` ) }) @@ -412,11 +393,13 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { ) proceed() } else { - setFatalError('CheckItem failed to save final position with message') + setErrorMessage( + 'CheckItem failed to save final position with message' + ) } }) .catch((e: Error) => { - setFatalError( + setErrorMessage( `CheckItem failed to move from final position with message: ${e.message}` ) }) @@ -440,7 +423,7 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { ) }) .catch((e: Error) => { - setFatalError(`CheckItem failed to home: ${e.message}`) + setErrorMessage(`CheckItem failed to home: ${e.message}`) }) } @@ -512,6 +495,7 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { labwareDef={labwareDef} confirmPlacement={handleConfirmPlacement} robotType={robotType} + location={step.location} /> )} diff --git a/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx b/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx index c0b33bff7a8..996a005f457 100644 --- a/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx @@ -14,40 +14,22 @@ import detachProbe8 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach import detachProbe96 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_96.webm' import { GenericWizardTile } from '/app/molecules/GenericWizardTile' -import type { Dispatch } from 'react' -import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' -import type { Jog } from '/app/molecules/JogControls/types' -import type { useChainRunCommands } from '/app/resources/runs' -import type { DetachProbeStep } from './types' -import type { LabwareOffset } from '@opentrons/api-client' -import type { - LPCWizardAction, - LPCWizardState, -} from '/app/organisms/LabwarePositionCheck/redux' +import type { DetachProbeStep, LPCStepProps } from './types' -interface DetachProbeProps extends DetachProbeStep { - protocolData: CompletedProtocolAnalysis - proceed: () => void - chainRunCommands: ReturnType['chainRunCommands'] - setFatalError: (errorMessage: string) => void - existingOffsets: LabwareOffset[] - dispatch: Dispatch - state: LPCWizardState - handleJog: Jog - isRobotMoving: boolean -} -export const DetachProbe = (props: DetachProbeProps): JSX.Element | null => { +export const DetachProbe = ( + props: LPCStepProps +): JSX.Element | null => { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const { - pipetteId, + step, protocolData, proceed, chainRunCommands, isRobotMoving, - setFatalError, + setErrorMessage, } = props - const pipette = protocolData.pipettes.find(p => p.id === pipetteId) + const pipette = protocolData.pipettes.find(p => p.id === step.pipetteId) const pipetteName = pipette?.pipetteName const pipetteChannels = pipetteName != null ? getPipetteNameSpecs(pipetteName)?.channels ?? 1 : 1 @@ -72,7 +54,7 @@ export const DetachProbe = (props: DetachProbeProps): JSX.Element | null => { ], false ).catch(error => { - setFatalError(error.message as string) + setErrorMessage(error.message as string) }) }, []) @@ -105,7 +87,7 @@ export const DetachProbe = (props: DetachProbeProps): JSX.Element | null => { proceed() }) .catch((e: Error) => { - setFatalError( + setErrorMessage( `DetachProbe failed to move to safe location after probe detach with message: ${e.message}` ) }) diff --git a/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx b/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx index ab6cb857035..eeab4fbb3a4 100644 --- a/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx +++ b/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx @@ -21,15 +21,11 @@ import { useSelector } from 'react-redux' import { getIsOnDevice } from '/app/redux/config' import { SmallButton } from '/app/atoms/buttons' -interface ExitConfirmationProps { - onGoBack: () => void - onConfirmExit: () => void - shouldUseMetalProbe: boolean -} +import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' -export const ExitConfirmation = (props: ExitConfirmationProps): JSX.Element => { +export const ExitConfirmation = (props: LPCWizardContentProps): JSX.Element => { const { i18n, t } = useTranslation(['labware_position_check', 'shared']) - const { onGoBack, onConfirmExit, shouldUseMetalProbe } = props + const { confirmExitLPC, cancelExitLPC, shouldUseMetalProbe } = props const isOnDevice = useSelector(getIsOnDevice) return ( { gridGap={SPACING.spacing8} > { alignItems={ALIGN_CENTER} > - + {t('shared:go_back')} {shouldUseMetalProbe diff --git a/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx b/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx index ee98e776055..57bc81ee47d 100644 --- a/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx +++ b/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx @@ -1,6 +1,6 @@ -import { createPortal } from 'react-dom' import styled from 'styled-components' import { useTranslation } from 'react-i18next' + import { ALIGN_CENTER, ALIGN_FLEX_END, @@ -14,57 +14,19 @@ import { RESPONSIVENESS, SPACING, LegacyStyledText, - ModalShell, TEXT_ALIGN_CENTER, TEXT_TRANSFORM_CAPITALIZE, TYPOGRAPHY, } from '@opentrons/components' -import { getTopPortalEl } from '/app/App/portal' -import { WizardHeader } from '/app/molecules/WizardHeader' -import { i18n } from '/app/i18n' -const SUPPORT_EMAIL = 'support@opentrons.com' -interface FatalErrorProps { - errorMessage: string - shouldUseMetalProbe: boolean - onClose: () => void -} +import { i18n } from '/app/i18n' -interface FatalErrorModalProps extends FatalErrorProps { - isOnDevice: boolean -} +import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' -export function FatalErrorModal(props: FatalErrorModalProps): JSX.Element { - const { t } = useTranslation(['labware_position_check', 'shared', 'branded']) - const { onClose, isOnDevice } = props - return createPortal( - isOnDevice ? ( - - - - - ) : ( - - } - > - - - ), - getTopPortalEl() - ) -} +const SUPPORT_EMAIL = 'support@opentrons.com' -export function FatalError(props: FatalErrorProps): JSX.Element { - const { errorMessage, shouldUseMetalProbe, onClose } = props +export function FatalError(props: LPCWizardContentProps): JSX.Element { + const { errorMessage, shouldUseMetalProbe, onCloseClick } = props const { t } = useTranslation(['labware_position_check', 'shared', 'branded']) return ( {t('shared:exit')} diff --git a/app/src/organisms/LabwarePositionCheck/LPCFlows/LPCFlows.tsx b/app/src/organisms/LabwarePositionCheck/LPCFlows/LPCFlows.tsx index 4aa6fd62e59..6b826d47bb3 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCFlows/LPCFlows.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCFlows/LPCFlows.tsx @@ -11,7 +11,7 @@ export interface LPCFlowsProps { runId: string robotType: RobotType existingOffsets: LabwareOffset[] - mostRecentAnalysis: CompletedProtocolAnalysis | null + mostRecentAnalysis: CompletedProtocolAnalysis protocolName: string maintenanceRunId: string } diff --git a/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts b/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts index d61172f0c50..0c0d0bc4501 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts +++ b/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts @@ -112,7 +112,10 @@ export function useLPCFlows({ } const showLPC = - hasCreatedLPCRun && maintenanceRunId != null && protocolName != null + hasCreatedLPCRun && + maintenanceRunId != null && + protocolName != null && + mostRecentAnalysis != null return showLPC ? { diff --git a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx index cda17acb64d..f8b4dcb9ad3 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx @@ -12,7 +12,7 @@ import { FIXED_TRASH_ID, FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { getTopPortalEl } from '/app/App/portal' // import { useTrackEvent } from '/app/redux/analytics' -import { IntroScreen } from './IntroScreen' +import { BeforeBeginning } from './BeforeBeginning' import { ExitConfirmation } from './ExitConfirmation' import { CheckItem } from './CheckItem' import { WizardHeader } from '/app/molecules/WizardHeader' @@ -40,25 +40,25 @@ import type { } from '@opentrons/api-client' import type { Axis, Sign, StepSize } from '/app/molecules/JogControls/types' import type { LPCFlowsProps } from '/app/organisms/LabwarePositionCheck/LPCFlows' +import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' +import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' const JOG_COMMAND_TIMEOUT = 10000 // 10 seconds -export function LPCWizardFlex(props: LPCFlowsProps): JSX.Element | null { +export function LPCWizardFlex(props: LPCFlowsProps): JSX.Element { const { mostRecentAnalysis, - existingOffsets, robotType, runId, onCloseClick, protocolName, maintenanceRunId, } = props - const { t } = useTranslation(['labware_position_check', 'shared']) const isOnDevice = useSelector(getIsOnDevice) const protocolData = mostRecentAnalysis const shouldUseMetalProbe = robotType === FLEX_ROBOT_TYPE - const [fatalError, setFatalError] = useState(null) + const [errorMessage, setErrorMessage] = useState(null) const [isApplyingOffsets, setIsApplyingOffsets] = useState(false) // TOME TODO: Like with ER, separate wizard and content. The wizard injects the data layer to the content layer. @@ -138,14 +138,12 @@ export function LPCWizardFlex(props: LPCFlowsProps): JSX.Element | null { : currentStepIndex ) } - if (protocolData == null) return null const LPCSteps = getLabwarePositionCheckSteps( protocolData, shouldUseMetalProbe ) const totalStepCount = LPCSteps.length - 1 const currentStep = LPCSteps?.[currentStepIndex] - if (currentStep == null) return null const protocolHasModules = protocolData.modules.length > 0 @@ -172,10 +170,12 @@ export function LPCWizardFlex(props: LPCFlowsProps): JSX.Element | null { ) }) .catch((e: Error) => { - setFatalError(`error issuing jog command: ${e.message}`) + setErrorMessage(`error issuing jog command: ${e.message}`) }) } else { - setFatalError(`could not find pipette to jog with id: ${pipetteId ?? ''}`) + setErrorMessage( + `could not find pipette to jog with id: ${pipetteId ?? ''}` + ) } } const chainMaintenanceRunCommands = ( @@ -183,18 +183,6 @@ export function LPCWizardFlex(props: LPCFlowsProps): JSX.Element | null { continuePastCommandFailure: boolean ): Promise => chainRunCommands(maintenanceRunId, commands, continuePastCommandFailure) - const movementStepProps = { - proceed, - protocolData, - chainRunCommands: chainMaintenanceRunCommands, - setFatalError, - dispatch, - handleJog, - isRobotMoving: isCommandChainLoading, - state, - existingOffsets, - robotType, - } const handleApplyOffsets = (offsets: LabwareOffsetCreateData[]): void => { setIsApplyingOffsets(true) @@ -203,111 +191,121 @@ export function LPCWizardFlex(props: LPCFlowsProps): JSX.Element | null { onCloseClick() }) .catch((e: Error) => { - setFatalError(`error applying labware offsets: ${e.message}`) + setErrorMessage(`error applying labware offsets: ${e.message}`) }) } - let modalContent: JSX.Element =
UNASSIGNED STEP
- if (isExiting) { - modalContent = ( - - ) - } else if (fatalError != null) { - modalContent = ( - - ) - } else if (showConfirmation) { - modalContent = ( - - ) - } else if (currentStep.section === 'BEFORE_BEGINNING') { - modalContent = ( - - ) - } else if ( - currentStep.section === 'CHECK_POSITIONS' || - currentStep.section === 'CHECK_TIP_RACKS' || - currentStep.section === 'CHECK_LABWARE' - ) { - modalContent = ( - - ) - } else if (currentStep.section === 'ATTACH_PROBE') { - modalContent = ( - - ) - } else if (currentStep.section === 'DETACH_PROBE') { - modalContent = - } else if (currentStep.section === 'PICK_UP_TIP') { - modalContent = ( - - ) - } else if (currentStep.section === 'RETURN_TIP') { - modalContent = - } else if (currentStep.section === 'RESULTS_SUMMARY') { - modalContent = ( - - ) - } - const wizardHeader = ( - ) +} + +function LPCWizardFlexComponent(props: LPCWizardContentProps): JSX.Element { return createPortal( - isOnDevice ? ( + props.isOnDevice ? ( - {wizardHeader} - {modalContent} + + ) : ( - - {modalContent} + }> + ), getTopPortalEl() ) } + +function LPCWizardHeader({ + errorMessage, + currentStepIndex, + totalStepCount, + showConfirmation, + isExiting, + confirmExitLPC, +}: LPCWizardContentProps): JSX.Element { + const { t } = useTranslation('labware_position_check') + + return ( + + ) +} + +function LPCWizardContent(props: LPCWizardContentProps): JSX.Element { + const { step, ...restProps } = props + const { t } = useTranslation('shared') + + // Handle special cases first. + if (props.isExiting) { + return + } + if (props.errorMessage != null) { + return + } + if (props.showConfirmation) { + return + } + + // Handle step-based routing. + switch (step.section) { + case NAV_STEPS.BEFORE_BEGINNING: + return + + case NAV_STEPS.CHECK_POSITIONS: + case NAV_STEPS.CHECK_TIP_RACKS: + case NAV_STEPS.CHECK_LABWARE: + return + + case NAV_STEPS.ATTACH_PROBE: + return + + case NAV_STEPS.DETACH_PROBE: + return + + case NAV_STEPS.PICK_UP_TIP: + return + + case NAV_STEPS.RETURN_TIP: + return + + case NAV_STEPS.RESULTS_SUMMARY: + return + + default: + console.error('Unhandled LPC step.') + return + } +} diff --git a/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx b/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx index f13ebd139e4..1773dd559a9 100644 --- a/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx +++ b/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx @@ -26,49 +26,24 @@ import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' import { getDisplayLocation } from './utils/getDisplayLocation' import { useSelector } from 'react-redux' import { getIsOnDevice } from '/app/redux/config' +import { + setFinalPosition, + setInitialPosition, + setTipPickupOffset, +} from '/app/organisms/LabwarePositionCheck/redux/actions' -import type { Dispatch } from 'react' import type { - CompletedProtocolAnalysis, CreateCommand, MoveLabwareCreateCommand, - RobotType, } from '@opentrons/shared-data' -import type { useChainRunCommands } from '/app/resources/runs' -import type { Jog } from '/app/molecules/JogControls/types' -import type { PickUpTipStep } from './types' -import type { LabwareOffset } from '@opentrons/api-client' +import type { LPCStepProps, PickUpTipStep } from './types' import type { TFunction } from 'i18next' -import type { - LPCWizardAction, - LPCWizardState, -} from '/app/organisms/LabwarePositionCheck/redux' -import { - setFinalPosition, - setInitialPosition, - setTipPickupOffset, -} from '/app/organisms/LabwarePositionCheck/redux/actions' -interface PickUpTipProps extends PickUpTipStep { - protocolData: CompletedProtocolAnalysis - proceed: () => void - chainRunCommands: ReturnType['chainRunCommands'] - setFatalError: (errorMessage: string) => void - existingOffsets: LabwareOffset[] - dispatch: Dispatch - state: LPCWizardState - handleJog: Jog - isRobotMoving: boolean - robotType: RobotType - protocolHasModules: boolean - currentStepIndex: number -} -export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { +export const PickUpTip = ( + props: LPCStepProps +): JSX.Element | null => { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const { - labwareId, - pipetteId, - location, protocolData, proceed, chainRunCommands, @@ -77,12 +52,13 @@ export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { handleJog, isRobotMoving, existingOffsets, - setFatalError, - adapterId, + setErrorMessage, robotType, protocolHasModules, currentStepIndex, + step, } = props + const { labwareId, pipetteId, location, adapterId } = step const { workingOffsets } = state const [showTipConfirmation, setShowTipConfirmation] = useState(false) const isOnDevice = useSelector(getIsOnDevice) @@ -211,13 +187,13 @@ export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { }) ) } else { - setFatalError( + setErrorMessage( `PickUpTip failed to save position for initial placement.` ) } }) .catch((e: Error) => { - setFatalError( + setErrorMessage( `PickUpTip failed to save position for initial placement with message: ${e.message}` ) }) @@ -260,14 +236,14 @@ export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { setShowTipConfirmation(true) }) .catch((e: Error) => { - setFatalError( + setErrorMessage( `PickUpTip failed to move from final position with message: ${e.message}` ) }) } }) .catch((e: Error) => { - setFatalError( + setErrorMessage( `PickUpTip failed to save final position with message: ${e.message}` ) }) @@ -329,7 +305,7 @@ export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { proceed() }) .catch((e: Error) => { - setFatalError( + setErrorMessage( `PickUpTip failed to move to safe location after tip pick up with message: ${e.message}` ) }) @@ -369,7 +345,9 @@ export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { setShowTipConfirmation(false) }) .catch((e: Error) => { - setFatalError(`PickUpTip failed to drop tip with message: ${e.message}`) + setErrorMessage( + `PickUpTip failed to drop tip with message: ${e.message}` + ) }) } const handleGoBack = (): void => { @@ -396,7 +374,7 @@ export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { ) }) .catch((e: Error) => { - setFatalError( + setErrorMessage( `PickUpTip failed to clear tip rack with message: ${e.message}` ) }) @@ -463,6 +441,7 @@ export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { labwareDef={labwareDef} confirmPlacement={handleConfirmPlacement} robotType={robotType} + location={step.location} /> )}
diff --git a/app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx b/app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx index 8820acfef33..b8f9ecc72a3 100644 --- a/app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx +++ b/app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx @@ -1,7 +1,7 @@ -import type * as React from 'react' import styled, { css } from 'styled-components' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' + import { DIRECTION_COLUMN, JUSTIFY_SPACE_BETWEEN, @@ -23,12 +23,17 @@ import { SmallButton } from '/app/atoms/buttons' import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' +import type { ReactNode } from 'react' +import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { - CompletedProtocolAnalysis, - LabwareDefinition2, - RobotType, -} from '@opentrons/shared-data' -import type { CheckLabwareStep } from './types' + CheckLabwareStep, + CheckPositionsStep, + CheckTipRacksStep, + LPCStepProps, + PerformLPCStep, + PickUpTipStep, + ReturnTipStep, +} from './types' const LPC_HELP_LINK_URL = 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' @@ -51,23 +56,32 @@ const Title = styled.h1` ${TYPOGRAPHY.level4HeaderSemiBold}; } ` -interface PrepareSpaceProps extends Omit { - section: - | 'CHECK_LABWARE' - | 'CHECK_TIP_RACKS' - | 'PICK_UP_TIP' - | 'RETURN_TIP' - | 'CHECK_POSITIONS' + +interface PrepareSpaceProps + extends LPCStepProps< + | CheckLabwareStep + | CheckTipRacksStep + | CheckPositionsStep + | PickUpTipStep + | ReturnTipStep + > { + header: ReactNode + body: ReactNode labwareDef: LabwareDefinition2 - protocolData: CompletedProtocolAnalysis + location: PerformLPCStep['location'] confirmPlacement: () => void - header: React.ReactNode - body: React.ReactNode - robotType: RobotType } -export const PrepareSpace = (props: PrepareSpaceProps): JSX.Element | null => { + +export const PrepareSpace = ({ + location, + labwareDef, + protocolData, + header, + body, + robotType, + confirmPlacement, +}: PrepareSpaceProps): JSX.Element | null => { const { i18n, t } = useTranslation(['labware_position_check', 'shared']) - const { location, labwareDef, protocolData, header, body, robotType } = props const isOnDevice = useSelector(getIsOnDevice) const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] @@ -125,13 +139,13 @@ export const PrepareSpace = (props: PrepareSpaceProps): JSX.Element | null => { t('shared:confirm_placement'), 'capitalize' )} - onClick={props.confirmPlacement} + onClick={confirmPlacement} />
) : ( - + {i18n.format(t('shared:confirm_placement'), 'capitalize')} diff --git a/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx b/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx index 5ddf6a9439c..0faac7d9419 100644 --- a/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx +++ b/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx @@ -42,35 +42,16 @@ import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analy import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' import { getDisplayLocation } from './utils/getDisplayLocation' -import type { Dispatch } from 'react' -import type { - CompletedProtocolAnalysis, - LabwareDefinition2, -} from '@opentrons/shared-data' -import type { - LabwareOffset, - LabwareOffsetCreateData, -} from '@opentrons/api-client' -import type { ResultsSummaryStep } from './types' +import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { LPCStepProps, ResultsSummaryStep } from './types' import type { TFunction } from 'i18next' -import type { - LPCWizardAction, - LPCWizardState, -} from '/app/organisms/LabwarePositionCheck/redux' const LPC_HELP_LINK_URL = 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' -interface ResultsSummaryProps extends ResultsSummaryStep { - protocolData: CompletedProtocolAnalysis - dispatch: Dispatch - state: LPCWizardState - existingOffsets: LabwareOffset[] - handleApplyOffsets: (offsets: LabwareOffsetCreateData[]) => void - isApplyingOffsets: boolean -} export const ResultsSummary = ( - props: ResultsSummaryProps + props: LPCStepProps ): JSX.Element | null => { const { i18n, t } = useTranslation('labware_position_check') const { diff --git a/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx b/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx index c0085272656..abc44c4fff8 100644 --- a/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx +++ b/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx @@ -21,44 +21,26 @@ import { useSelector } from 'react-redux' import { getIsOnDevice } from '/app/redux/config' import type { - CompletedProtocolAnalysis, CreateCommand, - RobotType, MoveLabwareCreateCommand, } from '@opentrons/shared-data' -import type { useChainRunCommands } from '/app/resources/runs' -import type { ReturnTipStep } from './types' +import type { LPCStepProps, ReturnTipStep } from './types' import type { TFunction } from 'i18next' -import type { Dispatch } from 'react' -import type { - LPCWizardAction, - LPCWizardState, -} from '/app/organisms/LabwarePositionCheck/redux' -interface ReturnTipProps extends ReturnTipStep { - protocolData: CompletedProtocolAnalysis - proceed: () => void - chainRunCommands: ReturnType['chainRunCommands'] - setFatalError: (errorMessage: string) => void - dispatch: Dispatch - state: LPCWizardState - isRobotMoving: boolean - robotType: RobotType -} -export const ReturnTip = (props: ReturnTipProps): JSX.Element | null => { +export const ReturnTip = ( + props: LPCStepProps +): JSX.Element | null => { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const { - pipetteId, - labwareId, - location, protocolData, proceed, state, isRobotMoving, chainRunCommands, - setFatalError, - adapterId, + setErrorMessage, + step, } = props + const { pipetteId, location, labwareId, adapterId } = step const { tipPickUpOffset } = state const isOnDevice = useSelector(getIsOnDevice) @@ -212,7 +194,7 @@ export const ReturnTip = (props: ReturnTipProps): JSX.Element | null => { proceed() }) .catch((e: Error) => { - setFatalError(`ReturnTip failed with message: ${e.message}`) + setErrorMessage(`ReturnTip failed with message: ${e.message}`) }) } @@ -228,6 +210,7 @@ export const ReturnTip = (props: ReturnTipProps): JSX.Element | null => { body={} labwareDef={labwareDef} confirmPlacement={handleConfirmPlacement} + location={step.location} />
) diff --git a/app/src/organisms/LabwarePositionCheck/components/index.ts b/app/src/organisms/LabwarePositionCheck/components/index.ts index c0c33ab559c..160e4750db2 100644 --- a/app/src/organisms/LabwarePositionCheck/components/index.ts +++ b/app/src/organisms/LabwarePositionCheck/components/index.ts @@ -1,2 +1 @@ //TOME TODO: The shared UI should go here. Might want to rename it something, IDK. -// TOME TODO: Because there will likely be Flex/OT-2 specific screens. Shared shouldn't be a top level field. Maybe name it components or something. diff --git a/app/src/organisms/LabwarePositionCheck/redux/transforms.ts b/app/src/organisms/LabwarePositionCheck/redux/transforms.ts index b3d9654e948..39dc482ce5c 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/transforms.ts +++ b/app/src/organisms/LabwarePositionCheck/redux/transforms.ts @@ -1,7 +1,6 @@ import isEqual from 'lodash/isEqual' -import type { LPCWizardAction } from './types' -import type { WorkingOffset } from '/app/organisms/LabwarePositionCheck/types' +import type { LPCWizardAction, WorkingOffset } from './types' export function updateWorkingOffset( workingOffsets: WorkingOffset[], diff --git a/app/src/organisms/LabwarePositionCheck/redux/types.ts b/app/src/organisms/LabwarePositionCheck/redux/types.ts index 005eaf66df7..9d7bfc18190 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/types.ts +++ b/app/src/organisms/LabwarePositionCheck/redux/types.ts @@ -1,7 +1,7 @@ import type { Coordinates } from '@opentrons/shared-data' import type { LabwareOffsetLocation, VectorOffset } from '@opentrons/api-client' -interface WorkingOffset { +export interface WorkingOffset { labwareId: string location: LabwareOffsetLocation initialPosition: VectorOffset | null diff --git a/app/src/organisms/LabwarePositionCheck/types/content.ts b/app/src/organisms/LabwarePositionCheck/types/content.ts new file mode 100644 index 00000000000..35867bc7344 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/types/content.ts @@ -0,0 +1,55 @@ +import type { Dispatch } from 'react' +import type { + CompletedProtocolAnalysis, + LabwareDefinition2, + RobotType, +} from '@opentrons/shared-data' +import type { + LabwareOffset, + LabwareOffsetCreateData, +} from '@opentrons/api-client' +import type { Jog } from '/app/molecules/JogControls/types' +import type { useChainRunCommands } from '/app/resources/runs' +import type { + LPCWizardAction, + LPCWizardState, +} from '/app/organisms/LabwarePositionCheck/redux' +import type { LabwarePositionCheckStep } from './steps' +import type { LPCFlowsProps } from '/app/organisms/LabwarePositionCheck' + +// TOME TODO: REDUX! Pretty much all of this should be in redux or in the data layer. + +export interface LPCWizardContentProps extends LPCFlowsProps { + step: LabwarePositionCheckStep + protocolName: string + protocolData: CompletedProtocolAnalysis + proceed: () => void + dispatch: Dispatch + state: LPCWizardState + // TOME TODO: Get rid of this and all supportive logic now that flows are basically Flex only. + shouldUseMetalProbe: boolean + currentStepIndex: number + totalStepCount: number + showConfirmation: boolean + isExiting: boolean + confirmExitLPC: () => void + cancelExitLPC: () => void + chainRunCommands: ReturnType['chainRunCommands'] + errorMessage: string | null + setErrorMessage: (errorMessage: string) => void + existingOffsets: LabwareOffset[] + handleJog: Jog + isRobotMoving: boolean + isOnDevice: boolean + protocolHasModules: boolean + handleApplyOffsets: (offsets: LabwareOffsetCreateData[]) => void + isApplyingOffsets: boolean + // TOME TODO: Can safely remove this too. + robotType: RobotType +} + +export interface LabwareToOrder { + definition: LabwareDefinition2 + labwareId: string + slot: string +} diff --git a/app/src/organisms/LabwarePositionCheck/types/index.ts b/app/src/organisms/LabwarePositionCheck/types/index.ts new file mode 100644 index 00000000000..4da2755de80 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/types/index.ts @@ -0,0 +1,2 @@ +export * from './steps' +export * from './content' diff --git a/app/src/organisms/LabwarePositionCheck/types.ts b/app/src/organisms/LabwarePositionCheck/types/steps.ts similarity index 54% rename from app/src/organisms/LabwarePositionCheck/types.ts rename to app/src/organisms/LabwarePositionCheck/types/steps.ts index 3acb90bd142..95c06576d36 100644 --- a/app/src/organisms/LabwarePositionCheck/types.ts +++ b/app/src/organisms/LabwarePositionCheck/types/steps.ts @@ -1,6 +1,6 @@ -import type { NAV_STEPS } from './constants' import type { LabwareOffsetLocation } from '@opentrons/api-client' -import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type { NAV_STEPS } from '../constants' +import type { LPCWizardContentProps } from './content' export type LabwarePositionCheckStep = | BeforeBeginningStep @@ -12,64 +12,59 @@ export type LabwarePositionCheckStep = | ReturnTipStep | DetachProbeStep | ResultsSummaryStep -export interface BeforeBeginningStep { - section: typeof NAV_STEPS.BEFORE_BEGINNING + +export type LPCStepProps = Omit< + LPCWizardContentProps, + 'step' +> & { + step: Extract } -export interface CheckTipRacksStep { - section: typeof NAV_STEPS.CHECK_TIP_RACKS + +export interface PerformLPCStep { pipetteId: string labwareId: string location: LabwareOffsetLocation definitionUri: string adapterId?: string + moduleId?: string } + +// TOME TODO: This all should be in redux. + +export interface BeforeBeginningStep { + section: typeof NAV_STEPS.BEFORE_BEGINNING +} + export interface AttachProbeStep { section: typeof NAV_STEPS.ATTACH_PROBE pipetteId: string } -export interface PickUpTipStep { - section: typeof NAV_STEPS.PICK_UP_TIP - pipetteId: string - labwareId: string - location: LabwareOffsetLocation - definitionUri: string - adapterId?: string + +export interface CheckTipRacksStep extends PerformLPCStep { + section: typeof NAV_STEPS.CHECK_TIP_RACKS } -export interface CheckPositionsStep { + +export interface CheckPositionsStep extends PerformLPCStep { section: typeof NAV_STEPS.CHECK_POSITIONS - pipetteId: string - labwareId: string - location: LabwareOffsetLocation - definitionUri: string - moduleId?: string } -export interface CheckLabwareStep { + +export interface CheckLabwareStep extends PerformLPCStep { section: typeof NAV_STEPS.CHECK_LABWARE - pipetteId: string - labwareId: string - location: LabwareOffsetLocation - definitionUri: string - moduleId?: string - adapterId?: string } -export interface ReturnTipStep { + +export interface PickUpTipStep extends PerformLPCStep { + section: typeof NAV_STEPS.PICK_UP_TIP +} + +export interface ReturnTipStep extends PerformLPCStep { section: typeof NAV_STEPS.RETURN_TIP - pipetteId: string - labwareId: string - location: LabwareOffsetLocation - definitionUri: string - adapterId?: string } + export interface DetachProbeStep { section: typeof NAV_STEPS.DETACH_PROBE pipetteId: string } + export interface ResultsSummaryStep { section: typeof NAV_STEPS.RESULTS_SUMMARY } - -export interface LabwareToOrder { - definition: LabwareDefinition2 - labwareId: string - slot: string -} diff --git a/app/src/organisms/LegacyLabwarePositionCheck/LabwarePositionCheckComponent.tsx b/app/src/organisms/LegacyLabwarePositionCheck/LabwarePositionCheckComponent.tsx index f761f4bc395..6ff45018b1a 100644 --- a/app/src/organisms/LegacyLabwarePositionCheck/LabwarePositionCheckComponent.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/LabwarePositionCheckComponent.tsx @@ -1,4 +1,4 @@ -import { useState, useReducer } from 'react' +import { useState, useEffect, useReducer } from 'react' import { createPortal } from 'react-dom' import isEqual from 'lodash/isEqual' import { useSelector } from 'react-redux' @@ -25,7 +25,10 @@ import { ReturnTip } from './ReturnTip' import { ResultsSummary } from './ResultsSummary' import { FatalError } from './FatalErrorModal' import { RobotMotionLoader } from './RobotMotionLoader' -import { useChainMaintenanceCommands } from '/app/resources/maintenance_runs' +import { + useChainMaintenanceCommands, + useNotifyCurrentMaintenanceRun, +} from '/app/resources/maintenance_runs' import { getLabwarePositionCheckSteps } from './getLabwarePositionCheckSteps' import type { @@ -43,6 +46,7 @@ import type { import type { Axis, Sign, StepSize } from '/app/molecules/JogControls/types' import type { RegisterPositionAction, WorkingOffset } from './types' +const RUN_REFETCH_INTERVAL = 5000 const JOG_COMMAND_TIMEOUT = 10000 // 10 seconds interface LabwarePositionCheckModalProps { runId: string @@ -52,6 +56,8 @@ interface LabwarePositionCheckModalProps { existingOffsets: LabwareOffset[] onCloseClick: () => unknown protocolName: string + setMaintenanceRunId?: (id: string | null) => void + isDeletingMaintenanceRun?: boolean caughtError?: Error } @@ -65,13 +71,49 @@ export const LabwarePositionCheckComponent = ( runId, maintenanceRunId, onCloseClick, + setMaintenanceRunId, protocolName, + isDeletingMaintenanceRun, } = props const { t } = useTranslation(['labware_position_check', 'shared']) const isOnDevice = useSelector(getIsOnDevice) const protocolData = mostRecentAnalysis const shouldUseMetalProbe = robotType === FLEX_ROBOT_TYPE + // we should start checking for run deletion only after the maintenance run is created + // and the useCurrentRun poll has returned that created id + const [ + monitorMaintenanceRunForDeletion, + setMonitorMaintenanceRunForDeletion, + ] = useState(false) + + const { data: maintenanceRunData } = useNotifyCurrentMaintenanceRun({ + refetchInterval: RUN_REFETCH_INTERVAL, + enabled: maintenanceRunId != null, + }) + + // this will close the modal in case the run was deleted by the terminate + // activity modal on the ODD + useEffect(() => { + if ( + maintenanceRunId !== null && + maintenanceRunData?.data.id === maintenanceRunId + ) { + setMonitorMaintenanceRunForDeletion(true) + } + if ( + maintenanceRunData?.data.id !== maintenanceRunId && + monitorMaintenanceRunForDeletion + ) { + setMaintenanceRunId?.(null) + } + }, [ + maintenanceRunData?.data.id, + maintenanceRunId, + monitorMaintenanceRunForDeletion, + setMaintenanceRunId, + ]) + const [fatalError, setFatalError] = useState(null) const [isApplyingOffsets, setIsApplyingOffsets] = useState(false) const [{ workingOffsets, tipPickUpOffset }, registerPosition] = useReducer( @@ -273,9 +315,11 @@ export const LabwarePositionCheckComponent = ( Promise.all(offsets.map(data => createLabwareOffset({ runId, data }))) .then(() => { onCloseClick() + setIsApplyingOffsets(false) }) .catch((e: Error) => { setFatalError(`error applying labware offsets: ${e.message}`) + setIsApplyingOffsets(false) }) } @@ -358,6 +402,7 @@ export const LabwarePositionCheckComponent = ( existingOffsets, handleApplyOffsets, isApplyingOffsets, + isDeletingMaintenanceRun, }} /> ) diff --git a/app/src/organisms/LegacyLabwarePositionCheck/ResultsSummary.tsx b/app/src/organisms/LegacyLabwarePositionCheck/ResultsSummary.tsx index 70e59be141e..212346baaac 100644 --- a/app/src/organisms/LegacyLabwarePositionCheck/ResultsSummary.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/ResultsSummary.tsx @@ -62,6 +62,7 @@ interface ResultsSummaryProps extends ResultsSummaryStep { existingOffsets: LabwareOffset[] handleApplyOffsets: (offsets: LabwareOffsetCreateData[]) => void isApplyingOffsets: boolean + isDeletingMaintenanceRun?: boolean } export const ResultsSummary = ( props: ResultsSummaryProps @@ -73,11 +74,12 @@ export const ResultsSummary = ( handleApplyOffsets, existingOffsets, isApplyingOffsets, + isDeletingMaintenanceRun, } = props const labwareDefinitions = getLabwareDefinitionsFromCommands( protocolData.commands ) - const isSubmittingAndClosing = isApplyingOffsets + const isSubmittingAndClosing = isApplyingOffsets || isDeletingMaintenanceRun const isLabwareOffsetCodeSnippetsOn = useSelector( getIsLabwareOffsetCodeSnippetsOn ) diff --git a/app/src/organisms/LegacyLabwarePositionCheck/index.tsx b/app/src/organisms/LegacyLabwarePositionCheck/index.tsx index 9a576e366e1..777fead7780 100644 --- a/app/src/organisms/LegacyLabwarePositionCheck/index.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/index.tsx @@ -22,6 +22,8 @@ interface LabwarePositionCheckModalProps { existingOffsets: LabwareOffset[] mostRecentAnalysis: CompletedProtocolAnalysis | null protocolName: string + setMaintenanceRunId?: (id: string | null) => void + isDeletingMaintenanceRun?: boolean caughtError?: Error } From 28bd24fd1842d50ffb1d7d6d1e0b3da6a4278f26 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Mon, 6 Jan 2025 11:04:32 -0500 Subject: [PATCH 09/33] refactor(app): remove all OT-2/Flex conditional logic from the Flex flows Because the Flex/OT-2 flows are controlled outside of the LPC presentation layer, we don't need any of the conditional logic here. --- .../BeforeBeginning/index.tsx | 28 +- .../LabwarePositionCheck/CheckItem.tsx | 15 +- .../LabwarePositionCheck/ExitConfirmation.tsx | 25 +- .../LabwarePositionCheck/FatalErrorModal.tsx | 22 +- .../LabwarePositionCheck/JogToWell.tsx | 70 +++-- .../LabwarePositionCheck/LPCWizardFlex.tsx | 37 +-- .../LabwarePositionCheck/PickUpTip.tsx | 4 +- .../LabwarePositionCheck/PrepareSpace.tsx | 15 +- .../LabwarePositionCheck/components/index.ts | 2 +- .../getLabwarePositionCheckSteps.ts | 47 +--- .../LabwarePositionCheck/types/content.ts | 8 +- .../doesPipetteVisitAllTipracks.test.ts | 112 -------- .../__tests__/getPrimaryPipetteId.test.ts | 227 ---------------- .../utils/doesPipetteVisitAllTipracks.ts | 40 --- .../utils/getDisplayLocation.ts | 2 + .../utils/getPrimaryPipetteId.ts | 70 ----- .../utils/getTipBasedLPCSteps.ts | 200 -------------- .../LabwarePositionCheck/utils/labware.ts | 247 +----------------- 18 files changed, 99 insertions(+), 1072 deletions(-) delete mode 100644 app/src/organisms/LabwarePositionCheck/utils/__tests__/doesPipetteVisitAllTipracks.test.ts delete mode 100644 app/src/organisms/LabwarePositionCheck/utils/__tests__/getPrimaryPipetteId.test.ts delete mode 100644 app/src/organisms/LabwarePositionCheck/utils/doesPipetteVisitAllTipracks.ts delete mode 100644 app/src/organisms/LabwarePositionCheck/utils/getPrimaryPipetteId.ts delete mode 100644 app/src/organisms/LabwarePositionCheck/utils/getTipBasedLPCSteps.ts diff --git a/app/src/organisms/LabwarePositionCheck/BeforeBeginning/index.tsx b/app/src/organisms/LabwarePositionCheck/BeforeBeginning/index.tsx index 19d755e003e..62cb6ddefba 100644 --- a/app/src/organisms/LabwarePositionCheck/BeforeBeginning/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/BeforeBeginning/index.tsx @@ -28,7 +28,6 @@ import { useSelector } from 'react-redux' import { TwoUpTileLayout } from '../TwoUpTileLayout' import { getTopPortalEl } from '/app/App/portal' import { SmallButton } from '/app/atoms/buttons' -import { CALIBRATION_PROBE } from '/app/organisms/PipetteWizardFlows/constants' import { TerseOffsetTable } from '../ResultsSummary' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' @@ -42,20 +41,15 @@ import type { // TODO(BC, 09/01/23): replace updated support article link for LPC on OT-2/Flex const SUPPORT_PAGE_URL = 'https://support.opentrons.com/s/ot2-calibration' -// TOME TODO: Get rid of the null. -export function BeforeBeginning( - props: LPCStepProps -): JSX.Element | null { - const { - proceed, - protocolData, - chainRunCommands, - isRobotMoving, - setErrorMessage, - existingOffsets, - protocolName, - shouldUseMetalProbe, - } = props +export function BeforeBeginning({ + proceed, + protocolData, + chainRunCommands, + isRobotMoving, + setErrorMessage, + existingOffsets, + protocolName, +}: LPCStepProps): JSX.Element { const isOnDevice = useSelector(getIsOnDevice) const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const handleClickStartLPC = (): void => { @@ -80,10 +74,8 @@ export function BeforeBeginning( }), }, ] - if (shouldUseMetalProbe) { - requiredEquipmentList.push(CALIBRATION_PROBE) - } + // TOME TODO: Render this above if possible. if (isRobotMoving) { return ( diff --git a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx b/app/src/organisms/LabwarePositionCheck/CheckItem.tsx index 6d6777afa1d..cc8a864919e 100644 --- a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx +++ b/app/src/organisms/LabwarePositionCheck/CheckItem.tsx @@ -12,7 +12,6 @@ import { RobotMotionLoader } from './RobotMotionLoader' import { PrepareSpace } from './PrepareSpace' import { JogToWell } from './JogToWell' import { - FLEX_ROBOT_TYPE, getIsTiprack, getLabwareDefURI, getLabwareDisplayName, @@ -63,8 +62,6 @@ export function CheckItem( isRobotMoving, existingOffsets, setErrorMessage, - robotType, - shouldUseMetalProbe, } = props const { labwareId, pipetteId, moduleId, adapterId, location } = step const { t, i18n } = useTranslation(['labware_position_check', 'shared']) @@ -277,10 +274,7 @@ export function CheckItem( wellName: 'A1', wellLocation: { origin: 'top' as const, - offset: - robotType === FLEX_ROBOT_TYPE - ? { x: 0, y: 0, z: PROBE_LENGTH_MM } - : IDENTITY_VECTOR, + offset: { x: 0, y: 0, z: PROBE_LENGTH_MM }, }, }, }, @@ -442,6 +436,7 @@ export function CheckItem( {initialPosition != null ? ( ) : ( )} diff --git a/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx b/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx index eeab4fbb3a4..e3f246210a0 100644 --- a/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx +++ b/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx @@ -23,9 +23,12 @@ import { SmallButton } from '/app/atoms/buttons' import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' -export const ExitConfirmation = (props: LPCWizardContentProps): JSX.Element => { +export const ExitConfirmation = ({ + confirmExitLPC, + cancelExitLPC, +}: LPCWizardContentProps): JSX.Element => { const { i18n, t } = useTranslation(['labware_position_check', 'shared']) - const { confirmExitLPC, cancelExitLPC, shouldUseMetalProbe } = props + const isOnDevice = useSelector(getIsOnDevice) return ( { {isOnDevice ? ( <> - {shouldUseMetalProbe - ? t('remove_probe_before_exit') - : t('exit_screen_title')} + {t('remove_probe_before_exit')} @@ -57,9 +58,7 @@ export const ExitConfirmation = (props: LPCWizardContentProps): JSX.Element => { ) : ( <> - {shouldUseMetalProbe - ? t('remove_probe_before_exit') - : t('exit_screen_title')} + {t('remove_probe_before_exit')} {t('exit_screen_subtitle')} @@ -81,11 +80,7 @@ export const ExitConfirmation = (props: LPCWizardContentProps): JSX.Element => { /> @@ -104,9 +99,7 @@ export const ExitConfirmation = (props: LPCWizardContentProps): JSX.Element => { onClick={confirmExitLPC} textTransform={TYPOGRAPHY.textTransformCapitalize} > - {shouldUseMetalProbe - ? t('remove_calibration_probe') - : i18n.format(t('shared:exit'), 'capitalize')} + {t('remove_calibration_probe')} diff --git a/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx b/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx index 57bc81ee47d..4bfba7b9562 100644 --- a/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx +++ b/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx @@ -25,8 +25,10 @@ import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/ const SUPPORT_EMAIL = 'support@opentrons.com' -export function FatalError(props: LPCWizardContentProps): JSX.Element { - const { errorMessage, shouldUseMetalProbe, onCloseClick } = props +export function FatalError({ + errorMessage, + onCloseClick, +}: LPCWizardContentProps): JSX.Element { const { t } = useTranslation(['labware_position_check', 'shared', 'branded']) return ( {i18n.format(t('shared:something_went_wrong'), 'sentenceCase')} - {shouldUseMetalProbe ? ( - - {t('remove_probe_before_exit')} - - ) : null} + + {t('remove_probe_before_exit')} + {t('branded:help_us_improve_send_error_report', { support_email: SUPPORT_EMAIL, diff --git a/app/src/organisms/LabwarePositionCheck/JogToWell.tsx b/app/src/organisms/LabwarePositionCheck/JogToWell.tsx index bce4808a514..2f39fdc5b3f 100644 --- a/app/src/organisms/LabwarePositionCheck/JogToWell.tsx +++ b/app/src/organisms/LabwarePositionCheck/JogToWell.tsx @@ -24,13 +24,10 @@ import { } from '@opentrons/components' import { getIsTiprack, - getPipetteNameSpecs, getVectorDifference, getVectorSum, } from '@opentrons/shared-data' -import levelWithTip from '/app/assets/images/lpc_level_with_tip.svg' -import levelWithLabware from '/app/assets/images/lpc_level_with_labware.svg' import levelProbeWithTip from '/app/assets/images/lpc_level_probe_with_tip.svg' import levelProbeWithLabware from '/app/assets/images/lpc_level_probe_with_labware.svg' import { getIsOnDevice } from '/app/redux/config' @@ -45,37 +42,45 @@ import type { PipetteName, LabwareDefinition2 } from '@opentrons/shared-data' import type { WellStroke } from '@opentrons/components' import type { VectorOffset } from '@opentrons/api-client' import type { Jog } from '/app/molecules/JogControls' +import type { + CheckLabwareStep, + CheckPositionsStep, + CheckTipRacksStep, + LPCStepProps, + PickUpTipStep, +} from '/app/organisms/LabwarePositionCheck/types' const DECK_MAP_VIEWBOX = '-10 -10 150 105' const LPC_HELP_LINK_URL = 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' -interface JogToWellProps { - handleConfirmPosition: () => void - handleGoBack: () => void - handleJog: Jog - pipetteName: PipetteName - labwareDef: LabwareDefinition2 +interface JogToWellProps + extends LPCStepProps< + CheckLabwareStep | CheckTipRacksStep | CheckPositionsStep | PickUpTipStep + > { header: ReactNode body: ReactNode + labwareDef: LabwareDefinition2 initialPosition: VectorOffset + handleConfirmPosition: () => void + handleGoBack: () => void + handleJog: Jog existingOffset: VectorOffset - shouldUseMetalProbe: boolean + pipetteName: PipetteName } -export const JogToWell = (props: JogToWellProps): JSX.Element | null => { + +export function JogToWell({ + header, + body, + pipetteName, + labwareDef, + handleConfirmPosition, + handleGoBack, + handleJog, + initialPosition, + existingOffset, +}: JogToWellProps): JSX.Element { const { t } = useTranslation(['labware_position_check', 'shared']) - const { - header, - body, - pipetteName, - labwareDef, - handleConfirmPosition, - handleGoBack, - handleJog, - initialPosition, - existingOffset, - shouldUseMetalProbe, - } = props const [joggedPosition, setJoggedPosition] = useState( initialPosition @@ -97,16 +102,7 @@ export const JogToWell = (props: JogToWellProps): JSX.Element | null => { } }, []) - let wellsToHighlight: string[] = [] - if ( - getPipetteNameSpecs(pipetteName)?.channels === 8 && - !shouldUseMetalProbe - ) { - wellsToHighlight = labwareDef.ordering[0] - } else { - wellsToHighlight = ['A1'] - } - + const wellsToHighlight = ['A1'] const wellStroke: WellStroke = wellsToHighlight.reduce( (acc, wellName) => ({ ...acc, [wellName]: COLORS.blue50 }), {} @@ -117,10 +113,8 @@ export const JogToWell = (props: JogToWellProps): JSX.Element | null => { getVectorDifference(joggedPosition, initialPosition) ) const isTipRack = getIsTiprack(labwareDef) - let levelSrc = isTipRack ? levelWithTip : levelWithLabware - if (shouldUseMetalProbe) { - levelSrc = isTipRack ? levelProbeWithTip : levelProbeWithLabware - } + const levelSrc = isTipRack ? levelProbeWithTip : levelProbeWithLabware + return ( { )} diff --git a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx index f8b4dcb9ad3..56790f7c448 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx @@ -8,7 +8,6 @@ import { useCreateLabwareOffsetMutation, useCreateMaintenanceCommandMutation, } from '@opentrons/react-api-client' -import { FIXED_TRASH_ID, FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { getTopPortalEl } from '/app/App/portal' // import { useTrackEvent } from '/app/redux/analytics' @@ -29,11 +28,7 @@ import { getLabwarePositionCheckSteps } from './getLabwarePositionCheckSteps' import { useLPCInitialState } from '/app/organisms/LabwarePositionCheck/hooks' import { useLPCReducer } from '/app/organisms/LabwarePositionCheck/redux' -import type { - Coordinates, - CreateCommand, - DropTipCreateCommand, -} from '@opentrons/shared-data' +import type { Coordinates, CreateCommand } from '@opentrons/shared-data' import type { LabwareOffsetCreateData, CommandData, @@ -45,18 +40,17 @@ import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' const JOG_COMMAND_TIMEOUT = 10000 // 10 seconds -export function LPCWizardFlex(props: LPCFlowsProps): JSX.Element { +export function LPCWizardFlex( + props: Omit +): JSX.Element { const { mostRecentAnalysis, - robotType, runId, onCloseClick, protocolName, maintenanceRunId, } = props const isOnDevice = useSelector(getIsOnDevice) - const protocolData = mostRecentAnalysis - const shouldUseMetalProbe = robotType === FLEX_ROBOT_TYPE const [errorMessage, setErrorMessage] = useState(null) const [isApplyingOffsets, setIsApplyingOffsets] = useState(false) @@ -79,17 +73,7 @@ export function LPCWizardFlex(props: LPCFlowsProps): JSX.Element { const [currentStepIndex, setCurrentStepIndex] = useState(0) const handleCleanUpAndClose = (): void => { setIsExiting(true) - const dropTipToBeSafeCommands: DropTipCreateCommand[] = shouldUseMetalProbe - ? [] - : (protocolData?.pipettes ?? []).map(pip => ({ - commandType: 'dropTip' as const, - params: { - pipetteId: pip.id, - labwareId: FIXED_TRASH_ID, - wellName: 'A1', - wellLocation: { origin: 'default' as const }, - }, - })) + chainRunCommands( maintenanceRunId, [ @@ -113,7 +97,6 @@ export function LPCWizardFlex(props: LPCFlowsProps): JSX.Element { commandType: 'retractAxis' as const, params: { axis: 'y' }, }, - ...dropTipToBeSafeCommands, { commandType: 'home' as const, params: {} }, ], true @@ -138,14 +121,11 @@ export function LPCWizardFlex(props: LPCFlowsProps): JSX.Element { : currentStepIndex ) } - const LPCSteps = getLabwarePositionCheckSteps( - protocolData, - shouldUseMetalProbe - ) + const LPCSteps = getLabwarePositionCheckSteps(mostRecentAnalysis) const totalStepCount = LPCSteps.length - 1 const currentStep = LPCSteps?.[currentStepIndex] - const protocolHasModules = protocolData.modules.length > 0 + const protocolHasModules = mostRecentAnalysis.modules.length > 0 const handleJog = ( axis: Axis, @@ -199,12 +179,11 @@ export function LPCWizardFlex(props: LPCFlowsProps): JSX.Element { {initialPosition != null ? ( ) : ( } labwareDef={labwareDef} confirmPlacement={handleConfirmPlacement} - robotType={robotType} location={step.location} /> )} diff --git a/app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx b/app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx index b8f9ecc72a3..07bf5f38423 100644 --- a/app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx +++ b/app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx @@ -16,7 +16,11 @@ import { BaseDeck, ALIGN_FLEX_START, } from '@opentrons/components' -import { THERMOCYCLER_MODULE_TYPE, getModuleType } from '@opentrons/shared-data' +import { + THERMOCYCLER_MODULE_TYPE, + getModuleType, + FLEX_ROBOT_TYPE, +} from '@opentrons/shared-data' import { getIsOnDevice } from '/app/redux/config' import { SmallButton } from '/app/atoms/buttons' @@ -72,22 +76,19 @@ interface PrepareSpaceProps confirmPlacement: () => void } -export const PrepareSpace = ({ +export function PrepareSpace({ location, labwareDef, protocolData, header, body, - robotType, confirmPlacement, -}: PrepareSpaceProps): JSX.Element | null => { +}: PrepareSpaceProps): JSX.Element { const { i18n, t } = useTranslation(['labware_position_check', 'shared']) const isOnDevice = useSelector(getIsOnDevice) const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] - if (protocolData == null || robotType == null) return null - return ( @@ -105,7 +106,7 @@ export const PrepareSpace = ({ alignItems={ALIGN_FLEX_START} > ({ moduleModel: mod.model, moduleLocation: mod.location, diff --git a/app/src/organisms/LabwarePositionCheck/components/index.ts b/app/src/organisms/LabwarePositionCheck/components/index.ts index 160e4750db2..3f0bbda81f3 100644 --- a/app/src/organisms/LabwarePositionCheck/components/index.ts +++ b/app/src/organisms/LabwarePositionCheck/components/index.ts @@ -1 +1 @@ -//TOME TODO: The shared UI should go here. Might want to rename it something, IDK. +// TOME TODO: The shared UI should go here. Might want to rename it something, IDK. diff --git a/app/src/organisms/LabwarePositionCheck/getLabwarePositionCheckSteps.ts b/app/src/organisms/LabwarePositionCheck/getLabwarePositionCheckSteps.ts index 1c51c06827f..ed935ee938b 100644 --- a/app/src/organisms/LabwarePositionCheck/getLabwarePositionCheckSteps.ts +++ b/app/src/organisms/LabwarePositionCheck/getLabwarePositionCheckSteps.ts @@ -1,52 +1,21 @@ -import { getPrimaryPipetteId } from './utils/getPrimaryPipetteId' -import { getTipBasedLPCSteps } from './utils/getTipBasedLPCSteps' import { getProbeBasedLPCSteps } from './utils/getProbeBasedLPCSteps' + import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' import type { LabwarePositionCheckStep } from './types' export const getLabwarePositionCheckSteps = ( - protocolData: CompletedProtocolAnalysis, - shouldUseMetalProbe: boolean + protocolData: CompletedProtocolAnalysis ): LabwarePositionCheckStep[] => { - if (protocolData != null && 'pipettes' in protocolData) { + if ('pipettes' in protocolData) { if (protocolData.pipettes.length === 0) { throw new Error( 'no pipettes loaded within protocol, labware position check cannot be performed' ) + } else { + return getProbeBasedLPCSteps(protocolData) } - if (shouldUseMetalProbe) return getProbeBasedLPCSteps(protocolData) - - // filter out any pipettes that are not being used in the protocol - const pipettesUsedInProtocol: CompletedProtocolAnalysis['pipettes'] = protocolData.pipettes.filter( - ({ id }) => - protocolData.commands.some( - command => - command.commandType === 'pickUpTip' && - command.params.pipetteId === id - ) - ) - const { labware, modules, commands } = protocolData - if (pipettesUsedInProtocol.length === 0) { - throw new Error( - 'pipettes do not pick up a tip within protocol, labware position check cannot be performed' - ) - } - const pipettesById = pipettesUsedInProtocol.reduce( - (acc, pip) => ({ ...acc, [pip.id]: pip }), - {} - ) - const primaryPipetteId = getPrimaryPipetteId(pipettesById, commands) - const secondaryPipetteId = - pipettesUsedInProtocol.find(({ id }) => id !== primaryPipetteId)?.id ?? - null - return getTipBasedLPCSteps({ - primaryPipetteId, - secondaryPipetteId, - labware, - modules, - commands, - }) + } else { + console.error('expected pipettes to be in protocol data') + return [] } - console.error('expected pipettes to be in protocol data') - return [] } diff --git a/app/src/organisms/LabwarePositionCheck/types/content.ts b/app/src/organisms/LabwarePositionCheck/types/content.ts index 35867bc7344..a8ffa12f9c6 100644 --- a/app/src/organisms/LabwarePositionCheck/types/content.ts +++ b/app/src/organisms/LabwarePositionCheck/types/content.ts @@ -2,7 +2,6 @@ import type { Dispatch } from 'react' import type { CompletedProtocolAnalysis, LabwareDefinition2, - RobotType, } from '@opentrons/shared-data' import type { LabwareOffset, @@ -19,15 +18,14 @@ import type { LPCFlowsProps } from '/app/organisms/LabwarePositionCheck' // TOME TODO: REDUX! Pretty much all of this should be in redux or in the data layer. -export interface LPCWizardContentProps extends LPCFlowsProps { +export interface LPCWizardContentProps + extends Omit { step: LabwarePositionCheckStep protocolName: string protocolData: CompletedProtocolAnalysis proceed: () => void dispatch: Dispatch state: LPCWizardState - // TOME TODO: Get rid of this and all supportive logic now that flows are basically Flex only. - shouldUseMetalProbe: boolean currentStepIndex: number totalStepCount: number showConfirmation: boolean @@ -44,8 +42,6 @@ export interface LPCWizardContentProps extends LPCFlowsProps { protocolHasModules: boolean handleApplyOffsets: (offsets: LabwareOffsetCreateData[]) => void isApplyingOffsets: boolean - // TOME TODO: Can safely remove this too. - robotType: RobotType } export interface LabwareToOrder { diff --git a/app/src/organisms/LabwarePositionCheck/utils/__tests__/doesPipetteVisitAllTipracks.test.ts b/app/src/organisms/LabwarePositionCheck/utils/__tests__/doesPipetteVisitAllTipracks.test.ts deleted file mode 100644 index d3c7b511c58..00000000000 --- a/app/src/organisms/LabwarePositionCheck/utils/__tests__/doesPipetteVisitAllTipracks.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { doesPipetteVisitAllTipracks } from '../doesPipetteVisitAllTipracks' -import { multiple_tipracks, one_tiprack } from '@opentrons/shared-data' -import type { - LoadedLabware, - ProtocolAnalysisOutput, - RunTimeCommand, -} from '@opentrons/shared-data' - -// TODO: update these fixtures to be v6 protocols -const protocolMultipleTipracks = (multiple_tipracks as unknown) as ProtocolAnalysisOutput -const labwareDefinitionsMultipleTipracks = multiple_tipracks.labwareDefinitions as {} -const protocolOneTiprack = (one_tiprack as unknown) as ProtocolAnalysisOutput -const labwareDefinitionsOneTiprack = one_tiprack.labwareDefinitions as {} -const labwareWithDefinitionUri = [ - { - id: 'fixedTrash', - displayName: 'Trash', - definitionUri: 'opentrons/opentrons_1_trash_1100ml_fixed/1', - loadName: 'opentrons_1_trash_1100ml_fixed', - }, - { - id: - '50d3ebb0-0042-11ec-8258-f7ffdf5ad45a:opentrons/opentrons_96_tiprack_300ul/1', - displayName: 'Opentrons 96 Tip Rack 300 µL', - definitionUri: 'opentrons/opentrons_96_tiprack_300ul/1', - loadName: 'opentrons_96_tiprack_300ul', - }, - { - id: - '9fbc1db0-0042-11ec-8258-f7ffdf5ad45a:opentrons/nest_12_reservoir_15ml/1', - displayName: 'NEST 12 Well Reservoir 15 mL', - definitionUri: 'opentrons/nest_12_reservoir_15ml/1', - loadName: 'nest_12_reservoir_15ml', - }, - { - id: 'e24818a0-0042-11ec-8258-f7ffdf5ad45a', - displayName: 'Opentrons 96 Tip Rack 300 µL (1)', - definitionUri: 'opentrons/opentrons_96_tiprack_300ul/1', - loadName: 'opentrons_96_tiprack_300ul', - }, -] as LoadedLabware[] - -describe('doesPipetteVisitAllTipracks', () => { - it('should return true when the pipette visits both tipracks', () => { - const pipetteId = 'c235a5a0-0042-11ec-8258-f7ffdf5ad45a' // this is just taken from the protocol fixture - const labware = labwareWithDefinitionUri - const commands: RunTimeCommand[] = protocolMultipleTipracks.commands - - expect( - doesPipetteVisitAllTipracks( - pipetteId, - labware, - labwareDefinitionsMultipleTipracks, - commands - ) - ).toBe(true) - }) - it('should return false when the pipette does NOT visit all tipracks', () => { - const pipetteId = '50d23e00-0042-11ec-8258-f7ffdf5ad45a' // this is just taken from the protocol fixture - const labware = labwareWithDefinitionUri - const commands: RunTimeCommand[] = protocolMultipleTipracks.commands - - expect( - doesPipetteVisitAllTipracks( - pipetteId, - labware, - labwareDefinitionsMultipleTipracks, - commands - ) - ).toBe(false) - }) - it('should return true when there is only one tiprack and pipette visits it', () => { - const pipetteId = 'pipetteId' // this is just taken from the protocol fixture - const labware = [ - { - id: 'fixedTrash', - displayName: 'Trash', - definitionUri: 'opentrons/opentrons_1_trash_1100ml_fixed/1', - loadName: 'opentrons_1_trash_1100ml_fixed', - }, - { - id: 'tiprackId', - displayName: 'Opentrons 96 Tip Rack 10 µL', - definitionUri: 'opentrons/opentrons_96_tiprack_10ul/1', - loadName: 'opentrons_96_tiprack_10ul', - }, - { - id: 'sourcePlateId', - displayName: 'Source Plate', - definitionUri: 'example/plate/1', - loadName: 'plate', - }, - { - id: 'destPlateId', - displayName: 'Dest Plate', - definitionUri: 'example/plate/1', - loadName: 'plate', - }, - ] as LoadedLabware[] - const commands: RunTimeCommand[] = protocolOneTiprack.commands - - expect( - doesPipetteVisitAllTipracks( - pipetteId, - labware, - labwareDefinitionsOneTiprack, - commands - ) - ).toBe(true) - }) -}) diff --git a/app/src/organisms/LabwarePositionCheck/utils/__tests__/getPrimaryPipetteId.test.ts b/app/src/organisms/LabwarePositionCheck/utils/__tests__/getPrimaryPipetteId.test.ts deleted file mode 100644 index 7256c0245e8..00000000000 --- a/app/src/organisms/LabwarePositionCheck/utils/__tests__/getPrimaryPipetteId.test.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { getPrimaryPipetteId } from '../getPrimaryPipetteId' -import type { - LoadedPipette, - LoadPipetteRunTimeCommand, -} from '@opentrons/shared-data' - -describe('getPrimaryPipetteId', () => { - it('should return the one and only pipette if there is only one pipette in the protocol', () => { - const mockPipettesById: { [id: string]: LoadedPipette } = { - p10SingleId: { - id: 'p10SingleId', - pipetteName: 'p10_single', - mount: 'left', - }, - } - expect(getPrimaryPipetteId(mockPipettesById, [])).toBe('p10SingleId') - }) - it('should throw an error if there are two pipettes with the same mount', () => { - const loadPipetteCommands: LoadPipetteRunTimeCommand[] = [ - { - id: '1', - commandType: 'loadPipette', - params: { - pipetteId: 'p10SingleId', - mount: 'left', - }, - }, - { - id: '1', - commandType: 'loadPipette', - params: { - pipetteId: 'p10MultiId', - mount: 'left', - }, - }, - ] as any - - const pipettesById: { [id: string]: LoadedPipette } = { - p10SingleId: { - id: 'p10SingleId', - pipetteName: 'p10_single', - mount: 'left', - }, - p10MultiId: { - id: 'p10SingleId', - pipetteName: 'p10_multi', - mount: 'left', - }, - } - expect(() => - getPrimaryPipetteId(pipettesById, loadPipetteCommands) - ).toThrow( - 'expected to find both left pipette and right pipette but could not' - ) - }) - it('should return the pipette with fewer channels', () => { - const loadPipetteCommands: LoadPipetteRunTimeCommand[] = [ - { - id: '1', - commandType: 'loadPipette', - params: { - pipetteId: 'p10SingleId', - mount: 'left', - }, - result: { - pipetteId: 'p10SingleId', - }, - }, - { - id: '1', - commandType: 'loadPipette', - params: { - pipetteId: 'p10MultiId', - mount: 'right', - }, - result: { - pipetteId: 'p10MultiId', - }, - }, - ] as any - - const pipettesById: { [id: string]: LoadedPipette } = { - p10SingleId: { - id: 'p10SingleId', - pipetteName: 'p10_single', - mount: 'left', - }, - p10MultiId: { - id: 'p10SingleId', - pipetteName: 'p10_multi', - mount: 'right', - }, - } - - expect(getPrimaryPipetteId(pipettesById, loadPipetteCommands)).toBe( - 'p10SingleId' - ) - }) - it('should return the smaller pipette', () => { - const loadPipetteCommands: LoadPipetteRunTimeCommand[] = [ - { - id: '1', - commandType: 'loadPipette', - params: { - pipetteId: 'p10SingleId', - mount: 'left', - }, - result: { - pipetteId: 'p10SingleId', - }, - }, - { - id: '1', - commandType: 'loadPipette', - params: { - pipetteId: 'p50SingleId', - mount: 'right', - }, - result: { - pipetteId: 'p50SingleId', - }, - }, - ] as any - - const pipettesById: { [id: string]: LoadedPipette } = { - p10SingleId: { - id: 'p10SingleId', - pipetteName: 'p10_single', - mount: 'left', - }, - p50SingleId: { - id: 'p50SingleId', - pipetteName: 'p50_single', - mount: 'right', - }, - } - expect(getPrimaryPipetteId(pipettesById, loadPipetteCommands)).toBe( - 'p10SingleId' - ) - }) - it('should return the newer model', () => { - const loadPipetteCommands: LoadPipetteRunTimeCommand[] = [ - { - id: '1', - commandType: 'loadPipette', - params: { - pipetteId: 'p300SingleId', - mount: 'left', - }, - result: { - pipetteId: 'p300SingleId', - }, - }, - { - id: '1', - commandType: 'loadPipette', - params: { - pipetteId: 'p300SingleGen2Id', - mount: 'right', - }, - result: { - pipetteId: 'p300SingleGen2Id', - }, - }, - ] as any - - const pipettesById: { [id: string]: LoadedPipette } = { - p300SingleId: { - id: 'p300SingleId', - pipetteName: 'p300_single', - mount: 'left', - }, - p300SingleGen2Id: { - id: 'p300SingleGen2Id', - pipetteName: 'p300_single_gen2', - mount: 'right', - }, - } - expect(getPrimaryPipetteId(pipettesById, loadPipetteCommands)).toBe( - 'p300SingleGen2Id' - ) - }) - - it('should return the left pipette when all else is the same', () => { - const loadPipetteCommands: LoadPipetteRunTimeCommand[] = [ - { - id: '1', - commandType: 'loadPipette', - params: { - pipetteId: 'p300SingleLeftId', - mount: 'left', - }, - result: { - pipetteId: 'p300SingleLeftId', - }, - }, - { - id: '1', - commandType: 'loadPipette', - params: { - pipetteId: 'p300SingleRightId', - mount: 'right', - }, - result: { - pipetteId: 'p300SingleRightId', - }, - }, - ] as any - - const pipettesById: { [id: string]: LoadedPipette } = { - p300SingleLeftId: { - id: 'p300SingleLeftId', - pipetteName: 'p300_single', - mount: 'left', - }, - p300SingleRightId: { - id: 'p300SingleRightId', - pipetteName: 'p300_single', - mount: 'right', - }, - } - expect(getPrimaryPipetteId(pipettesById, loadPipetteCommands)).toBe( - 'p300SingleLeftId' - ) - }) -}) diff --git a/app/src/organisms/LabwarePositionCheck/utils/doesPipetteVisitAllTipracks.ts b/app/src/organisms/LabwarePositionCheck/utils/doesPipetteVisitAllTipracks.ts deleted file mode 100644 index f2f336ae0fd..00000000000 --- a/app/src/organisms/LabwarePositionCheck/utils/doesPipetteVisitAllTipracks.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { getIsTiprack } from '@opentrons/shared-data' - -import type { - LoadedLabware, - RunTimeCommand, - LabwareDefinition2, -} from '@opentrons/shared-data' - -import { - getPickUpTipCommandsWithPipette, - getTipracksVisited, -} from '/app/transformations/commands' - -export const doesPipetteVisitAllTipracks = ( - pipetteId: string, - labware: LoadedLabware[], - labwareDefinitions: Record, - commands: RunTimeCommand[] -): boolean => { - const numberOfTipracks = labware.reduce( - (numberOfTipracks, currentLabware) => { - const labwareDef = labwareDefinitions[currentLabware.definitionUri] - return getIsTiprack(labwareDef) ? numberOfTipracks + 1 : numberOfTipracks - }, - 0 - ) - const pickUpTipCommandsWithPipette = getPickUpTipCommandsWithPipette( - commands, - pipetteId - ) - - const tipracksVisited = getTipracksVisited(pickUpTipCommandsWithPipette) - - pickUpTipCommandsWithPipette.reduce((visited, command) => { - const tiprack = command.params.labwareId - return visited.includes(tiprack) ? visited : [...visited, tiprack] - }, []) - - return numberOfTipracks === tipracksVisited.length -} diff --git a/app/src/organisms/LabwarePositionCheck/utils/getDisplayLocation.ts b/app/src/organisms/LabwarePositionCheck/utils/getDisplayLocation.ts index d70b741c48d..21f1c06c1fa 100644 --- a/app/src/organisms/LabwarePositionCheck/utils/getDisplayLocation.ts +++ b/app/src/organisms/LabwarePositionCheck/utils/getDisplayLocation.ts @@ -8,6 +8,8 @@ import type { i18n, TFunction } from 'i18next' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { LabwareOffsetLocation } from '@opentrons/api-client' +// TOME TODO: I think this is no longer needed given the new utils, but double check. + export function getDisplayLocation( location: LabwareOffsetLocation, labwareDefinitions: LabwareDefinition2[], diff --git a/app/src/organisms/LabwarePositionCheck/utils/getPrimaryPipetteId.ts b/app/src/organisms/LabwarePositionCheck/utils/getPrimaryPipetteId.ts deleted file mode 100644 index caa799f6dad..00000000000 --- a/app/src/organisms/LabwarePositionCheck/utils/getPrimaryPipetteId.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { getPipetteNameSpecs } from '@opentrons/shared-data' -import type { - LoadedPipette, - LoadPipetteRunTimeCommand, - RunTimeCommand, -} from '@opentrons/shared-data' - -export const getPrimaryPipetteId = ( - pipettesById: { [id: string]: LoadedPipette }, - commands: RunTimeCommand[] -): string => { - if (Object.keys(pipettesById).length === 1) { - return Object.keys(pipettesById)[0] - } - - const leftPipetteId = commands.find( - (command: RunTimeCommand): command is LoadPipetteRunTimeCommand => - command.commandType === 'loadPipette' && command.params.mount === 'left' - )?.result?.pipetteId - const rightPipetteId = commands.find( - (command: RunTimeCommand): command is LoadPipetteRunTimeCommand => - command.commandType === 'loadPipette' && command.params.mount === 'right' - )?.result?.pipetteId - - if (leftPipetteId == null || rightPipetteId == null) { - throw new Error( - 'expected to find both left pipette and right pipette but could not' - ) - } - - const leftPipette = pipettesById[leftPipetteId] - const rightPipette = pipettesById[rightPipetteId] - - const leftPipetteSpecs = getPipetteNameSpecs(leftPipette.pipetteName) - const rightPipetteSpecs = getPipetteNameSpecs(rightPipette.pipetteName) - - if (leftPipetteSpecs == null) { - throw new Error( - `could not find pipette specs for ${String(leftPipette.pipetteName)}` - ) - } - if (rightPipetteSpecs == null) { - throw new Error( - `could not find pipette specs for ${String(rightPipette.pipetteName)}` - ) - } - - // prefer pipettes with fewer channels - if (leftPipetteSpecs.channels !== rightPipetteSpecs.channels) { - return leftPipetteSpecs.channels < rightPipetteSpecs.channels - ? leftPipetteId - : rightPipetteId - } - // prefer pipettes with smaller maxVolume - if (leftPipetteSpecs.maxVolume !== rightPipetteSpecs.maxVolume) { - return leftPipetteSpecs.maxVolume < rightPipetteSpecs.maxVolume - ? leftPipetteId - : rightPipetteId - } - - const leftPipetteGenerationCompare = leftPipetteSpecs.displayCategory.localeCompare( - rightPipetteSpecs.displayCategory - ) - // prefer new pipette models - if (leftPipetteGenerationCompare !== 0) { - return leftPipetteGenerationCompare > 0 ? leftPipetteId : rightPipetteId - } - // if all else is the same, prefer the left pipette - return leftPipetteId -} diff --git a/app/src/organisms/LabwarePositionCheck/utils/getTipBasedLPCSteps.ts b/app/src/organisms/LabwarePositionCheck/utils/getTipBasedLPCSteps.ts deleted file mode 100644 index 8575bdd7154..00000000000 --- a/app/src/organisms/LabwarePositionCheck/utils/getTipBasedLPCSteps.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { isEqual } from 'lodash' -import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' -import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' -import { - getLabwareDefURI, - getIsTiprack, - FIXED_TRASH_ID, -} from '@opentrons/shared-data' -import { getLabwareLocationCombos } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' - -import type { - LabwarePositionCheckStep, - CheckTipRacksStep, - PickUpTipStep, - CheckLabwareStep, - ReturnTipStep, -} from '../types' -import type { - RunTimeCommand, - ProtocolAnalysisOutput, - PickUpTipRunTimeCommand, -} from '@opentrons/shared-data' -import type { LabwareLocationCombo } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' - -interface LPCArgs { - primaryPipetteId: string - secondaryPipetteId: string | null - labware: ProtocolAnalysisOutput['labware'] - modules: ProtocolAnalysisOutput['modules'] - commands: RunTimeCommand[] -} - -export const getTipBasedLPCSteps = ( - args: LPCArgs -): LabwarePositionCheckStep[] => { - const checkTipRacksSectionSteps = getCheckTipRackSectionSteps(args) - if (checkTipRacksSectionSteps.length < 1) return [] - const allButLastTiprackCheckSteps = checkTipRacksSectionSteps.slice( - 0, - checkTipRacksSectionSteps.length - 1 - ) - const lastTiprackCheckStep = - checkTipRacksSectionSteps[checkTipRacksSectionSteps.length - 1] - - const pickUpTipSectionStep: PickUpTipStep = { - section: NAV_STEPS.PICK_UP_TIP, - labwareId: lastTiprackCheckStep.labwareId, - pipetteId: lastTiprackCheckStep.pipetteId, - location: lastTiprackCheckStep.location, - adapterId: lastTiprackCheckStep.adapterId, - definitionUri: lastTiprackCheckStep.definitionUri, - } - const checkLabwareSectionSteps = getCheckLabwareSectionSteps(args) - - const returnTipSectionStep: ReturnTipStep = { - section: NAV_STEPS.RETURN_TIP, - labwareId: lastTiprackCheckStep.labwareId, - pipetteId: lastTiprackCheckStep.pipetteId, - location: lastTiprackCheckStep.location, - adapterId: lastTiprackCheckStep.adapterId, - definitionUri: lastTiprackCheckStep.definitionUri, - } - - return [ - { section: NAV_STEPS.BEFORE_BEGINNING }, - ...allButLastTiprackCheckSteps, - pickUpTipSectionStep, - ...checkLabwareSectionSteps, - returnTipSectionStep, - { section: NAV_STEPS.RESULTS_SUMMARY }, - ] -} - -// TOME: TODO: Once you get things stable, you can do the labware definition stuff to get -// whether or not is a tiprack. -function getCheckTipRackSectionSteps(args: LPCArgs): CheckTipRacksStep[] { - const { - secondaryPipetteId, - primaryPipetteId, - commands, - labware, - modules = [], - } = args - - const labwareLocationCombos = getLabwareLocationCombos( - commands, - labware, - modules - ) - const uniqPrimaryPipettePickUpTipCommands = commands.reduce< - PickUpTipRunTimeCommand[] - >((acc, command) => { - if ( - command.commandType === 'pickUpTip' && - command.params.pipetteId === primaryPipetteId && - !acc.some(c => c.params.labwareId === command.params.labwareId) - ) { - return [...acc, command] - } - return acc - }, []) - const onlySecondaryPipettePickUpTipCommands = commands.reduce< - PickUpTipRunTimeCommand[] - >((acc, command) => { - if ( - command.commandType === 'pickUpTip' && - command.params.pipetteId === secondaryPipetteId && - !uniqPrimaryPipettePickUpTipCommands.some( - c => c.params.labwareId === command.params.labwareId - ) && - !acc.some(c => c.params.labwareId === command.params.labwareId) - ) { - return [...acc, command] - } - return acc - }, []) - - return [ - ...onlySecondaryPipettePickUpTipCommands, - ...uniqPrimaryPipettePickUpTipCommands, - ].reduce((acc, { params }) => { - const labwareLocations = labwareLocationCombos.reduce< - LabwareLocationCombo[] - >((acc, labwareLocationCombo) => { - // remove labware that isn't accessed by a pickup tip command - if (labwareLocationCombo.labwareId !== params.labwareId) { - return acc - } - // remove duplicate definitionUri in same location - const comboAlreadyExists = acc.some( - accLocationCombo => - labwareLocationCombo.definitionUri === - accLocationCombo.definitionUri && - isEqual(labwareLocationCombo.location, accLocationCombo.location) - ) - return comboAlreadyExists ? acc : [...acc, labwareLocationCombo] - }, []) - - return [ - ...acc, - ...labwareLocations.map(({ location, adapterId, definitionUri }) => ({ - section: NAV_STEPS.CHECK_TIP_RACKS, - labwareId: params.labwareId, - pipetteId: params.pipetteId, - location, - adapterId, - definitionUri: definitionUri, - })), - ] - }, []) -} - -function getCheckLabwareSectionSteps(args: LPCArgs): CheckLabwareStep[] { - const { labware, modules, commands, primaryPipetteId } = args - const labwareDefinitions = getLabwareDefinitionsFromCommands(commands) - - const deDupedLabwareLocationCombos = getLabwareLocationCombos( - commands, - labware, - modules - ).reduce((acc, labwareLocationCombo) => { - const labwareDef = labwareDefinitions.find( - def => getLabwareDefURI(def) === labwareLocationCombo.definitionUri - ) - if (labwareLocationCombo.labwareId === FIXED_TRASH_ID) return acc - if (labwareDef == null) { - throw new Error( - `could not find labware definition within protocol with uri: ${labwareLocationCombo.definitionUri}` - ) - } - const isTiprack = getIsTiprack(labwareDef) - const adapter = (labwareDef?.allowedRoles ?? []).includes('adapter') - if (isTiprack || adapter) return acc // skip any labware that is a tiprack or adapter - - const comboAlreadyExists = acc.some( - accLocationCombo => - labwareLocationCombo.definitionUri === accLocationCombo.definitionUri && - isEqual(labwareLocationCombo.location, accLocationCombo.location) - ) - return comboAlreadyExists ? acc : [...acc, labwareLocationCombo] - }, []) - - return deDupedLabwareLocationCombos.reduce( - (acc, { labwareId, location, moduleId, adapterId, definitionUri }) => { - return [ - ...acc, - { - section: NAV_STEPS.CHECK_LABWARE, - labwareId, - pipetteId: primaryPipetteId, - location, - moduleId, - adapterId, - definitionUri, - }, - ] - }, - [] - ) -} diff --git a/app/src/organisms/LabwarePositionCheck/utils/labware.ts b/app/src/organisms/LabwarePositionCheck/utils/labware.ts index 70096061c33..fb7227f157a 100644 --- a/app/src/organisms/LabwarePositionCheck/utils/labware.ts +++ b/app/src/organisms/LabwarePositionCheck/utils/labware.ts @@ -1,252 +1,13 @@ -import reduce from 'lodash/reduce' -import { - getIsTiprack, - getTiprackVolume, - getLabwareDefURI, -} from '@opentrons/shared-data' -import { getModuleInitialLoadInfo } from '/app/transformations/commands' +import { getLabwareDefURI } from '@opentrons/shared-data' + import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' + import type { CompletedProtocolAnalysis, LabwareDefinition2, - LabwareLocation, - LoadLabwareRunTimeCommand, - PickUpTipRunTimeCommand, - ProtocolAnalysisOutput, - RunTimeCommand, } from '@opentrons/shared-data' -import type { LabwareToOrder } from '../types' - -export const tipRackOrderSort = ( - tiprack1: LabwareToOrder, - tiprack2: LabwareToOrder -): -1 | 1 => { - const tiprack1Volume = getTiprackVolume(tiprack1.definition) - const tiprack2Volume = getTiprackVolume(tiprack2.definition) - - if (tiprack1Volume !== tiprack2Volume) { - return tiprack1Volume > tiprack2Volume ? -1 : 1 - } - return orderBySlot(tiprack1, tiprack2) -} - -export const orderBySlot = ( - labware1: LabwareToOrder, - labware2: LabwareToOrder -): -1 | 1 => { - if (labware1.slot < labware2.slot) { - return -1 - } - return 1 -} - -interface Labware { - [labwareId: string]: { - definitionId: string - displayName?: string - } -} - -export const getTiprackIdsInOrder = ( - labware: Labware, - labwareDefinitions: Record, - commands: RunTimeCommand[] -): string[] => { - const unorderedTipracks = reduce( - labware, - (tipracks, currentLabware, labwareId) => { - const labwareDef = labwareDefinitions[currentLabware.definitionId] - const isTiprack = getIsTiprack(labwareDef) - if (isTiprack) { - const tipRackLocations = getAllUniqLocationsForLabware( - labwareId, - commands - ) - return [ - ...tipracks, - ...tipRackLocations.map(loc => ({ - definition: labwareDef, - labwareId: labwareId, - slot: loc !== 'offDeck' && 'slotName' in loc ? loc.slotName : '', - })), - ] - } - return tipracks - }, - [] - ) - const orderedTiprackIds = unorderedTipracks - .sort(tipRackOrderSort) - .map(({ labwareId }) => labwareId) - - return orderedTiprackIds -} - -export const getAllTipracksIdsThatPipetteUsesInOrder = ( - pipetteId: string, - commands: RunTimeCommand[], - labware: ProtocolAnalysisOutput['labware'] -): string[] => { - const pickUpTipCommandsWithPipette: PickUpTipRunTimeCommand[] = commands.filter( - (command): command is PickUpTipRunTimeCommand => - command.commandType === 'pickUpTip' && - command.params.pipetteId === pipetteId - ) - - const tipRackIdsVisited = pickUpTipCommandsWithPipette.reduce( - (visitedIds, command) => { - const tipRackId = command.params.labwareId - return visitedIds.includes(tipRackId) - ? visitedIds - : [...visitedIds, tipRackId] - }, - [] - ) - const labwareDefinitions = getLabwareDefinitionsFromCommands(commands) - - const orderedTiprackIds = tipRackIdsVisited - .reduce((acc, tipRackId) => { - const tiprackEntity = labware.find(l => l.id === tipRackId) - const definition = labwareDefinitions.find( - def => getLabwareDefURI(def) === tiprackEntity?.definitionUri - ) - const tipRackLocations = getAllUniqLocationsForLabware( - tipRackId, - commands - ) - - if (definition == null) { - throw new Error( - `could not find labware definition within protocol with uri: ${tiprackEntity?.definitionUri}` - ) - } - return [ - ...acc, - ...tipRackLocations.map(loc => ({ - labwareId: tipRackId, - definition, - slot: loc !== 'offDeck' && 'slotName' in loc ? loc.slotName : '', - })), - ] - }, []) - .sort(tipRackOrderSort) - .map(({ labwareId }) => labwareId) - - return orderedTiprackIds -} - -export const getLabwareIdsInOrder = ( - labware: ProtocolAnalysisOutput['labware'], - commands: RunTimeCommand[] -): string[] => { - const labwareDefinitions = getLabwareDefinitionsFromCommands(commands) - - const unorderedLabware = labware.reduce( - (acc, currentLabware) => { - const labwareDef = labwareDefinitions.find( - def => getLabwareDefURI(def) === currentLabware.definitionUri - ) - if (labwareDef == null) { - throw new Error( - `could not find labware definition within protocol with uri: ${currentLabware.definitionUri}` - ) - } - // skip any labware that is a tip rack or trash - const isTiprack = getIsTiprack(labwareDef) - const isTrash = labwareDef.parameters.format === 'trash' - if (isTiprack || isTrash) return acc - - const labwareLocations = getAllUniqLocationsForLabware( - currentLabware.id, - commands - ) - return [ - ...acc, - ...labwareLocations.reduce((innerAcc, loc) => { - let slot = '' - if (loc === 'offDeck') { - slot = 'offDeck' - } else if ('moduleId' in loc) { - slot = getModuleInitialLoadInfo(loc.moduleId, commands).location - .slotName - } else if ('labwareId' in loc) { - const matchingAdapter = commands.find( - (command): command is LoadLabwareRunTimeCommand => - command.commandType === 'loadLabware' && - command.result?.labwareId === loc.labwareId - ) - const adapterLocation = matchingAdapter?.params.location - if (adapterLocation === 'offDeck') { - slot = 'offDeck' - } else if ( - adapterLocation != null && - 'slotName' in adapterLocation - ) { - slot = adapterLocation.slotName - } else if ( - adapterLocation != null && - 'moduleId' in adapterLocation - ) { - slot = getModuleInitialLoadInfo( - adapterLocation.moduleId, - commands - ).location.slotName - } - } else { - slot = - 'addressableAreaName' in loc - ? loc.addressableAreaName - : loc.slotName - } - return [ - ...innerAcc, - { definition: labwareDef, labwareId: currentLabware.id, slot }, - ] - }, []), - ] - }, - [] - ) - const orderedLabwareIds = unorderedLabware - .sort(orderBySlot) - .map(({ labwareId }) => labwareId) - - return orderedLabwareIds -} - -const TRASH_ID = 'fixedTrash' - -export const getAllUniqLocationsForLabware = ( - labwareId: string, - commands: RunTimeCommand[] -): LabwareLocation[] => { - if (labwareId === TRASH_ID) { - return [{ slotName: '12' }] - } - const labwareLocation = commands.reduce( - (acc, command: RunTimeCommand) => { - if ( - command.commandType === 'loadLabware' && - command.result?.definition.parameters.format !== 'trash' && - command.result?.labwareId === labwareId - ) { - const { location } = command.params - return [...acc, location] - } - return acc - }, - [] - ) - - if (labwareLocation.length === 0) { - throw new Error( - 'expected to be able to find at least one labware location, but could not' - ) - } - - return labwareLocation -} +// TOME TODO: Definitely understand how this works and see if it's necessary/can be simplified. export function getLabwareDef( labwareId: string, protocolData: CompletedProtocolAnalysis From 2b422fba0dedd3399bb287a8dc0bdc03caf6f3b2 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Mon, 6 Jan 2025 12:20:04 -0500 Subject: [PATCH 10/33] refactor(app): name space steps and shared components, removing OT2 specific steps --- .../LabwarePositionCheck/LPCWizardFlex.tsx | 29 +- .../LabwarePositionCheck/PickUpTip.tsx | 447 ------------------ .../LabwarePositionCheck/ReturnTip.tsx | 217 --------- .../TerseOffsetTable.stories.tsx | 6 +- .../LabwarePositionCheck/TipConfirmation.tsx | 84 ---- .../LabwarePositionCheck/components/index.ts | 1 - .../LabwarePositionCheck/constants/routing.ts | 4 - .../{ => shared}/ExitConfirmation.tsx | 0 .../JogToWell}/LiveOffsetValue.tsx | 0 .../JogToWell/index.tsx} | 8 +- .../{ => shared}/PrepareSpace.tsx | 19 +- .../{ => shared}/RobotMotionLoader.tsx | 2 + .../LabwarePositionCheck/shared/index.ts | 4 + .../{ => steps}/AttachProbe.tsx | 14 +- .../BeforeBeginning}/TwoUpTileLayout.tsx | 2 + .../BeforeBeginning/getPrepCommands.ts | 0 .../{ => steps}/BeforeBeginning/index.tsx | 4 +- .../{ => steps}/CheckItem.tsx | 43 +- .../{ => steps}/DetachProbe.tsx | 14 +- .../{ => steps}/ResultsSummary.tsx | 15 +- .../LabwarePositionCheck/steps/index.ts | 5 + .../LabwarePositionCheck/types/steps.ts | 20 - .../LabwarePositionCheck/utils/index.ts | 3 + 23 files changed, 87 insertions(+), 854 deletions(-) delete mode 100644 app/src/organisms/LabwarePositionCheck/PickUpTip.tsx delete mode 100644 app/src/organisms/LabwarePositionCheck/ReturnTip.tsx delete mode 100644 app/src/organisms/LabwarePositionCheck/TipConfirmation.tsx delete mode 100644 app/src/organisms/LabwarePositionCheck/components/index.ts rename app/src/organisms/LabwarePositionCheck/{ => shared}/ExitConfirmation.tsx (100%) rename app/src/organisms/LabwarePositionCheck/{ => shared/JogToWell}/LiveOffsetValue.tsx (100%) rename app/src/organisms/LabwarePositionCheck/{JogToWell.tsx => shared/JogToWell/index.tsx} (97%) rename app/src/organisms/LabwarePositionCheck/{ => shared}/PrepareSpace.tsx (92%) rename app/src/organisms/LabwarePositionCheck/{ => shared}/RobotMotionLoader.tsx (91%) create mode 100644 app/src/organisms/LabwarePositionCheck/shared/index.ts rename app/src/organisms/LabwarePositionCheck/{ => steps}/AttachProbe.tsx (93%) rename app/src/organisms/LabwarePositionCheck/{ => steps/BeforeBeginning}/TwoUpTileLayout.tsx (96%) rename app/src/organisms/LabwarePositionCheck/{ => steps}/BeforeBeginning/getPrepCommands.ts (100%) rename app/src/organisms/LabwarePositionCheck/{ => steps}/BeforeBeginning/index.tsx (97%) rename app/src/organisms/LabwarePositionCheck/{ => steps}/CheckItem.tsx (95%) rename app/src/organisms/LabwarePositionCheck/{ => steps}/DetachProbe.tsx (92%) rename app/src/organisms/LabwarePositionCheck/{ => steps}/ResultsSummary.tsx (97%) create mode 100644 app/src/organisms/LabwarePositionCheck/steps/index.ts create mode 100644 app/src/organisms/LabwarePositionCheck/utils/index.ts diff --git a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx index 56790f7c448..1be740188b0 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx @@ -10,19 +10,20 @@ import { } from '@opentrons/react-api-client' import { getTopPortalEl } from '/app/App/portal' -// import { useTrackEvent } from '/app/redux/analytics' -import { BeforeBeginning } from './BeforeBeginning' -import { ExitConfirmation } from './ExitConfirmation' -import { CheckItem } from './CheckItem' +import { + BeforeBeginning, + CheckItem, + AttachProbe, + DetachProbe, + ResultsSummary, +} from '/app/organisms/LabwarePositionCheck/steps' +import { + RobotMotionLoader, + ExitConfirmation, +} from '/app/organisms/LabwarePositionCheck/shared' import { WizardHeader } from '/app/molecules/WizardHeader' import { getIsOnDevice } from '/app/redux/config' -import { AttachProbe } from './AttachProbe' -import { DetachProbe } from './DetachProbe' -import { PickUpTip } from './PickUpTip' -import { ReturnTip } from './ReturnTip' -import { ResultsSummary } from './ResultsSummary' import { FatalError } from './FatalErrorModal' -import { RobotMotionLoader } from './RobotMotionLoader' import { useChainMaintenanceCommands } from '/app/resources/maintenance_runs' import { getLabwarePositionCheckSteps } from './getLabwarePositionCheckSteps' import { useLPCInitialState } from '/app/organisms/LabwarePositionCheck/hooks' @@ -264,8 +265,6 @@ function LPCWizardContent(props: LPCWizardContentProps): JSX.Element { return case NAV_STEPS.CHECK_POSITIONS: - case NAV_STEPS.CHECK_TIP_RACKS: - case NAV_STEPS.CHECK_LABWARE: return case NAV_STEPS.ATTACH_PROBE: @@ -274,12 +273,6 @@ function LPCWizardContent(props: LPCWizardContentProps): JSX.Element { case NAV_STEPS.DETACH_PROBE: return - case NAV_STEPS.PICK_UP_TIP: - return - - case NAV_STEPS.RETURN_TIP: - return - case NAV_STEPS.RESULTS_SUMMARY: return diff --git a/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx b/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx deleted file mode 100644 index 3c2d03ac647..00000000000 --- a/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx +++ /dev/null @@ -1,447 +0,0 @@ -import { useState } from 'react' -import { Trans, useTranslation } from 'react-i18next' -import isEqual from 'lodash/isEqual' -import { - DIRECTION_COLUMN, - Flex, - LegacyStyledText, - TYPOGRAPHY, -} from '@opentrons/components' -import { - getLabwareDefURI, - getLabwareDisplayName, - getModuleType, - getVectorDifference, - HEATERSHAKER_MODULE_TYPE, - IDENTITY_VECTOR, -} from '@opentrons/shared-data' -import { RobotMotionLoader } from './RobotMotionLoader' -import { PrepareSpace } from './PrepareSpace' -import { JogToWell } from './JogToWell' -import { UnorderedList } from '/app/molecules/UnorderedList' -import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' -import { TipConfirmation } from './TipConfirmation' -import { getLabwareDef } from './utils/labware' -import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' -import { getDisplayLocation } from './utils/getDisplayLocation' -import { useSelector } from 'react-redux' -import { getIsOnDevice } from '/app/redux/config' -import { - setFinalPosition, - setInitialPosition, - setTipPickupOffset, -} from '/app/organisms/LabwarePositionCheck/redux/actions' - -import type { - CreateCommand, - MoveLabwareCreateCommand, -} from '@opentrons/shared-data' -import type { LPCStepProps, PickUpTipStep } from './types' -import type { TFunction } from 'i18next' - -export const PickUpTip = ( - props: LPCStepProps -): JSX.Element | null => { - const { t, i18n } = useTranslation(['labware_position_check', 'shared']) - const { - protocolData, - proceed, - chainRunCommands, - dispatch, - state, - handleJog, - isRobotMoving, - existingOffsets, - setErrorMessage, - protocolHasModules, - currentStepIndex, - step, - } = props - const { labwareId, pipetteId, location, adapterId } = step - const { workingOffsets } = state - const [showTipConfirmation, setShowTipConfirmation] = useState(false) - const isOnDevice = useSelector(getIsOnDevice) - const labwareDef = getLabwareDef(labwareId, protocolData) - const pipette = protocolData.pipettes.find(p => p.id === pipetteId) - const pipetteName = pipette?.pipetteName - const pipetteMount = pipette?.mount - if (pipetteName == null || labwareDef == null || pipetteMount == null) - return null - const pipetteZMotorAxis: 'leftZ' | 'rightZ' = - pipetteMount === 'left' ? 'leftZ' : 'rightZ' - - const displayLocation = getDisplayLocation( - location, - getLabwareDefinitionsFromCommands(protocolData.commands), - t as TFunction, - i18n - ) - const labwareDisplayName = getLabwareDisplayName(labwareDef) - const instructions = [ - ...(protocolHasModules && currentStepIndex === 1 - ? [t('place_modules')] - : []), - isOnDevice ? t('clear_all_slots_odd') : t('clear_all_slots'), - - ), - }} - />, - ] - - const initialPosition = workingOffsets.find( - o => - o.labwareId === labwareId && - isEqual(o.location, location) && - o.initialPosition != null - )?.initialPosition - - let moveLabware: MoveLabwareCreateCommand[] - if (adapterId != null) { - moveLabware = [ - { - commandType: 'moveLabware' as const, - params: { - labwareId: adapterId, - newLocation: { slotName: location.slotName }, - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'moveLabware' as const, - params: { - labwareId, - newLocation: - adapterId != null - ? { labwareId: adapterId } - : { slotName: location.slotName }, - strategy: 'manualMoveWithoutPause', - }, - }, - ] - } else { - moveLabware = [ - { - commandType: 'moveLabware' as const, - params: { - labwareId, - newLocation: location, - strategy: 'manualMoveWithoutPause', - }, - }, - ] - } - - const handleConfirmPlacement = (): void => { - const modulePrepCommands = protocolData.modules.reduce( - (acc, module) => { - if (getModuleType(module.model) === HEATERSHAKER_MODULE_TYPE) { - return [ - ...acc, - { - commandType: 'heaterShaker/closeLabwareLatch', - params: { moduleId: module.id }, - }, - ] - } - return acc - }, - [] - ) - chainRunCommands( - [ - ...modulePrepCommands, - ...moveLabware, - { - commandType: 'moveToWell' as const, - params: { - pipetteId, - labwareId, - wellName: 'A1', - wellLocation: { origin: 'top' as const }, - }, - }, - { commandType: 'savePosition', params: { pipetteId } }, - ], - false - ) - .then(responses => { - const finalResponse = responses[responses.length - 1] - if (finalResponse.data.commandType === 'savePosition') { - const { position } = finalResponse.data?.result ?? { position: null } - dispatch( - setInitialPosition({ - labwareId, - location, - position, - }) - ) - } else { - setErrorMessage( - `PickUpTip failed to save position for initial placement.` - ) - } - }) - .catch((e: Error) => { - setErrorMessage( - `PickUpTip failed to save position for initial placement with message: ${e.message}` - ) - }) - } - const handleConfirmPosition = (): void => { - chainRunCommands( - [{ commandType: 'savePosition', params: { pipetteId } }], - false - ) - .then(responses => { - if (responses[0].data.commandType === 'savePosition') { - const { position } = responses[0].data?.result ?? { position: null } - const offset = - initialPosition != null && position != null - ? getVectorDifference(position, initialPosition) - : undefined - dispatch( - setFinalPosition({ - labwareId, - location, - position, - }) - ) - dispatch(setTipPickupOffset(offset ?? null)) - chainRunCommands( - [ - { - commandType: 'pickUpTip', - params: { - pipetteId, - labwareId, - wellName: 'A1', - wellLocation: { origin: 'top', offset }, - }, - }, - ], - false - ) - .then(() => { - setShowTipConfirmation(true) - }) - .catch((e: Error) => { - setErrorMessage( - `PickUpTip failed to move from final position with message: ${e.message}` - ) - }) - } - }) - .catch((e: Error) => { - setErrorMessage( - `PickUpTip failed to save final position with message: ${e.message}` - ) - }) - } - - const moveLabwareOffDeck: MoveLabwareCreateCommand[] = - adapterId != null - ? [ - { - commandType: 'moveLabware' as const, - params: { - labwareId, - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'moveLabware' as const, - params: { - labwareId: adapterId, - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - ] - : [ - { - commandType: 'moveLabware' as const, - params: { - labwareId, - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - ] - - const handleConfirmTipAttached = (): void => { - chainRunCommands( - [ - { - commandType: 'retractAxis' as const, - params: { - axis: pipetteZMotorAxis, - }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'x' }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'y' }, - }, - ...moveLabwareOffDeck, - ], - false - ) - .then(() => { - proceed() - }) - .catch((e: Error) => { - setErrorMessage( - `PickUpTip failed to move to safe location after tip pick up with message: ${e.message}` - ) - }) - } - const handleInvalidateTip = (): void => { - chainRunCommands( - [ - { - commandType: 'dropTip', - params: { - pipetteId, - labwareId, - wellName: 'A1', - }, - }, - { - commandType: 'moveToWell' as const, - params: { - pipetteId, - labwareId, - wellName: 'A1', - wellLocation: { origin: 'top' as const }, - }, - }, - ], - false - ) - .then(() => { - dispatch(setTipPickupOffset(null)) - dispatch( - setFinalPosition({ - labwareId, - location, - position: null, - }) - ) - setShowTipConfirmation(false) - }) - .catch((e: Error) => { - setErrorMessage( - `PickUpTip failed to drop tip with message: ${e.message}` - ) - }) - } - const handleGoBack = (): void => { - chainRunCommands( - [ - { - commandType: 'moveLabware' as const, - params: { - labwareId, - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - ], - false - ) - .then(() => { - dispatch( - setInitialPosition({ - labwareId, - location, - position: null, - }) - ) - }) - .catch((e: Error) => { - setErrorMessage( - `PickUpTip failed to clear tip rack with message: ${e.message}` - ) - }) - } - - const existingOffset = - getCurrentOffsetForLabwareInLocation( - existingOffsets, - getLabwareDefURI(labwareDef), - location - )?.vector ?? IDENTITY_VECTOR - - if (isRobotMoving) - return ( - - ) - return showTipConfirmation ? ( - - ) : ( - - {initialPosition != null ? ( - , - bold: , - }} - values={{ - tip_type: t('pipette_nozzle'), - item_location: t('check_tip_location'), - }} - /> - } - labwareDef={labwareDef} - pipetteName={pipetteName} - handleConfirmPosition={handleConfirmPosition} - handleGoBack={handleGoBack} - handleJog={handleJog} - initialPosition={initialPosition} - existingOffset={existingOffset} - /> - ) : ( - } - labwareDef={labwareDef} - confirmPlacement={handleConfirmPlacement} - location={step.location} - /> - )} - - ) -} diff --git a/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx b/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx deleted file mode 100644 index abc44c4fff8..00000000000 --- a/app/src/organisms/LabwarePositionCheck/ReturnTip.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import { Trans, useTranslation } from 'react-i18next' -import { - DIRECTION_COLUMN, - Flex, - LegacyStyledText, - TYPOGRAPHY, -} from '@opentrons/components' - -import { - getLabwareDisplayName, - getModuleType, - HEATERSHAKER_MODULE_TYPE, -} from '@opentrons/shared-data' -import { UnorderedList } from '/app/molecules/UnorderedList' -import { getLabwareDef } from './utils/labware' -import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' -import { getDisplayLocation } from './utils/getDisplayLocation' -import { RobotMotionLoader } from './RobotMotionLoader' -import { PrepareSpace } from './PrepareSpace' -import { useSelector } from 'react-redux' -import { getIsOnDevice } from '/app/redux/config' - -import type { - CreateCommand, - MoveLabwareCreateCommand, -} from '@opentrons/shared-data' -import type { LPCStepProps, ReturnTipStep } from './types' -import type { TFunction } from 'i18next' - -export const ReturnTip = ( - props: LPCStepProps -): JSX.Element | null => { - const { t, i18n } = useTranslation(['labware_position_check', 'shared']) - const { - protocolData, - proceed, - state, - isRobotMoving, - chainRunCommands, - setErrorMessage, - step, - } = props - const { pipetteId, location, labwareId, adapterId } = step - const { tipPickUpOffset } = state - - const isOnDevice = useSelector(getIsOnDevice) - - const labwareDef = getLabwareDef(labwareId, protocolData) - if (labwareDef == null) return null - - const displayLocation = getDisplayLocation( - location, - getLabwareDefinitionsFromCommands(protocolData.commands), - t as TFunction, - i18n - ) - const labwareDisplayName = getLabwareDisplayName(labwareDef) - - const instructions = [ - isOnDevice ? t('clear_all_slots_odd') : t('clear_all_slots'), - - ), - }} - />, - ] - - let moveLabware: MoveLabwareCreateCommand[] - if (adapterId != null) { - moveLabware = [ - { - commandType: 'moveLabware' as const, - params: { - labwareId: adapterId, - newLocation: { slotName: location.slotName }, - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'moveLabware' as const, - params: { - labwareId, - newLocation: - adapterId != null - ? { labwareId: adapterId } - : { slotName: location.slotName }, - strategy: 'manualMoveWithoutPause', - }, - }, - ] - } else { - moveLabware = [ - { - commandType: 'moveLabware' as const, - params: { - labwareId, - newLocation: location, - strategy: 'manualMoveWithoutPause', - }, - }, - ] - } - - const moveLabwareOffDeck: MoveLabwareCreateCommand[] = - adapterId != null - ? [ - { - commandType: 'moveLabware' as const, - params: { - labwareId, - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'moveLabware' as const, - params: { - labwareId: adapterId, - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - ] - : [ - { - commandType: 'moveLabware' as const, - params: { - labwareId, - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - ] - - const handleConfirmPlacement = (): void => { - const modulePrepCommands = protocolData.modules.reduce( - (acc, module) => { - if (getModuleType(module.model) === HEATERSHAKER_MODULE_TYPE) { - return [ - ...acc, - { - commandType: 'heaterShaker/closeLabwareLatch', - params: { moduleId: module.id }, - }, - ] - } - return acc - }, - [] - ) - chainRunCommands( - [ - ...modulePrepCommands, - ...moveLabware, - { - commandType: 'moveToWell' as const, - params: { - pipetteId, - labwareId, - wellName: 'A1', - wellLocation: { - origin: 'top' as const, - offset: tipPickUpOffset ?? undefined, - }, - }, - }, - { - commandType: 'dropTip' as const, - params: { - pipetteId, - labwareId, - wellName: 'A1', - wellLocation: { - origin: 'default' as const, - offset: tipPickUpOffset ?? undefined, - }, - }, - }, - ...moveLabwareOffDeck, - { commandType: 'home' as const, params: {} }, - ], - false - ) - .then(() => { - proceed() - }) - .catch((e: Error) => { - setErrorMessage(`ReturnTip failed with message: ${e.message}`) - }) - } - - if (isRobotMoving) - return ( - - ) - return ( - - } - labwareDef={labwareDef} - confirmPlacement={handleConfirmPlacement} - location={step.location} - /> - - ) -} diff --git a/app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx b/app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx index da7c52de513..7fd221bcabf 100644 --- a/app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx +++ b/app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx @@ -13,8 +13,8 @@ import { getLabwareDefURI, } from '@opentrons/shared-data' -import { SmallButton } from '/app/atoms/buttons' -import { TerseOffsetTable } from './ResultsSummary' +import { SmallButton } from '../../atoms/buttons' +import { TerseOffsetTable } from './steps/ResultsSummary' import type { Story, Meta } from '@storybook/react' import type { LabwareDefinition2 } from '@opentrons/shared-data' @@ -25,6 +25,8 @@ export default { parameters: VIEWPORT.touchScreenViewport, } as Meta +// TOME TODO: This should be moved into whatever dir handles the actual component. + // Note: 59rem(944px) is the size of ODD const Template: Story> = ({ ...args diff --git a/app/src/organisms/LabwarePositionCheck/TipConfirmation.tsx b/app/src/organisms/LabwarePositionCheck/TipConfirmation.tsx deleted file mode 100644 index ec8c87daea4..00000000000 --- a/app/src/organisms/LabwarePositionCheck/TipConfirmation.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { - ALIGN_CENTER, - COLORS, - DIRECTION_COLUMN, - Flex, - JUSTIFY_FLEX_END, - JUSTIFY_SPACE_BETWEEN, - PrimaryButton, - SecondaryButton, - SPACING, - LegacyStyledText, -} from '@opentrons/components' -import { useTranslation } from 'react-i18next' - -import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' -import { useSelector } from 'react-redux' -import { getIsOnDevice } from '/app/redux/config' -import { SimpleWizardBody } from '/app/molecules/SimpleWizardBody' -import { SmallButton } from '/app/atoms/buttons' -import { i18n } from '/app/i18n' - -const LPC_HELP_LINK_URL = - 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' - -interface TipConfirmationProps { - invalidateTip: () => void - confirmTip: () => void -} - -export function TipConfirmation(props: TipConfirmationProps): JSX.Element { - const { invalidateTip, confirmTip } = props - const { t } = useTranslation('shared') - const isOnDevice = useSelector(getIsOnDevice) - return isOnDevice ? ( - - - - - - - ) : ( - - - {t('did_pipette_pick_up_tip')} - - - - - - {i18n.format(t('try_again'), 'capitalize')} - - - {i18n.format(t('yes'), 'capitalize')} - - - - - ) -} diff --git a/app/src/organisms/LabwarePositionCheck/components/index.ts b/app/src/organisms/LabwarePositionCheck/components/index.ts deleted file mode 100644 index 3f0bbda81f3..00000000000 --- a/app/src/organisms/LabwarePositionCheck/components/index.ts +++ /dev/null @@ -1 +0,0 @@ -// TOME TODO: The shared UI should go here. Might want to rename it something, IDK. diff --git a/app/src/organisms/LabwarePositionCheck/constants/routing.ts b/app/src/organisms/LabwarePositionCheck/constants/routing.ts index d96edaee732..09f7a5a602e 100644 --- a/app/src/organisms/LabwarePositionCheck/constants/routing.ts +++ b/app/src/organisms/LabwarePositionCheck/constants/routing.ts @@ -1,11 +1,7 @@ export const NAV_STEPS = { BEFORE_BEGINNING: 'BEFORE_BEGINNING', ATTACH_PROBE: 'ATTACH_PROBE', - CHECK_TIP_RACKS: 'CHECK_TIP_RACKS', - PICK_UP_TIP: 'PICK_UP_TIP', - CHECK_LABWARE: 'CHECK_LABWARE', CHECK_POSITIONS: 'CHECK_POSITIONS', - RETURN_TIP: 'RETURN_TIP', DETACH_PROBE: 'DETACH_PROBE', RESULTS_SUMMARY: 'RESULTS_SUMMARY', } as const diff --git a/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx b/app/src/organisms/LabwarePositionCheck/shared/ExitConfirmation.tsx similarity index 100% rename from app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx rename to app/src/organisms/LabwarePositionCheck/shared/ExitConfirmation.tsx diff --git a/app/src/organisms/LabwarePositionCheck/LiveOffsetValue.tsx b/app/src/organisms/LabwarePositionCheck/shared/JogToWell/LiveOffsetValue.tsx similarity index 100% rename from app/src/organisms/LabwarePositionCheck/LiveOffsetValue.tsx rename to app/src/organisms/LabwarePositionCheck/shared/JogToWell/LiveOffsetValue.tsx diff --git a/app/src/organisms/LabwarePositionCheck/JogToWell.tsx b/app/src/organisms/LabwarePositionCheck/shared/JogToWell/index.tsx similarity index 97% rename from app/src/organisms/LabwarePositionCheck/JogToWell.tsx rename to app/src/organisms/LabwarePositionCheck/shared/JogToWell/index.tsx index 2f39fdc5b3f..dbf13d7aa32 100644 --- a/app/src/organisms/LabwarePositionCheck/JogToWell.tsx +++ b/app/src/organisms/LabwarePositionCheck/shared/JogToWell/index.tsx @@ -43,21 +43,15 @@ import type { WellStroke } from '@opentrons/components' import type { VectorOffset } from '@opentrons/api-client' import type { Jog } from '/app/molecules/JogControls' import type { - CheckLabwareStep, CheckPositionsStep, - CheckTipRacksStep, LPCStepProps, - PickUpTipStep, } from '/app/organisms/LabwarePositionCheck/types' const DECK_MAP_VIEWBOX = '-10 -10 150 105' const LPC_HELP_LINK_URL = 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' -interface JogToWellProps - extends LPCStepProps< - CheckLabwareStep | CheckTipRacksStep | CheckPositionsStep | PickUpTipStep - > { +interface JogToWellProps extends LPCStepProps { header: ReactNode body: ReactNode labwareDef: LabwareDefinition2 diff --git a/app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx b/app/src/organisms/LabwarePositionCheck/shared/PrepareSpace.tsx similarity index 92% rename from app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx rename to app/src/organisms/LabwarePositionCheck/shared/PrepareSpace.tsx index 07bf5f38423..e22a1ab385f 100644 --- a/app/src/organisms/LabwarePositionCheck/PrepareSpace.tsx +++ b/app/src/organisms/LabwarePositionCheck/shared/PrepareSpace.tsx @@ -29,15 +29,7 @@ import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configurati import type { ReactNode } from 'react' import type { LabwareDefinition2 } from '@opentrons/shared-data' -import type { - CheckLabwareStep, - CheckPositionsStep, - CheckTipRacksStep, - LPCStepProps, - PerformLPCStep, - PickUpTipStep, - ReturnTipStep, -} from './types' +import type { CheckPositionsStep, LPCStepProps, PerformLPCStep } from '../types' const LPC_HELP_LINK_URL = 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' @@ -61,14 +53,7 @@ const Title = styled.h1` } ` -interface PrepareSpaceProps - extends LPCStepProps< - | CheckLabwareStep - | CheckTipRacksStep - | CheckPositionsStep - | PickUpTipStep - | ReturnTipStep - > { +interface PrepareSpaceProps extends LPCStepProps { header: ReactNode body: ReactNode labwareDef: LabwareDefinition2 diff --git a/app/src/organisms/LabwarePositionCheck/RobotMotionLoader.tsx b/app/src/organisms/LabwarePositionCheck/shared/RobotMotionLoader.tsx similarity index 91% rename from app/src/organisms/LabwarePositionCheck/RobotMotionLoader.tsx rename to app/src/organisms/LabwarePositionCheck/shared/RobotMotionLoader.tsx index 577dae6eff7..56e376cca2b 100644 --- a/app/src/organisms/LabwarePositionCheck/RobotMotionLoader.tsx +++ b/app/src/organisms/LabwarePositionCheck/shared/RobotMotionLoader.tsx @@ -18,6 +18,8 @@ interface RobotMotionLoaderProps { body?: string } +// TOME TODO: IDK if this actually needs to be a shared component. It would be ideal if it wasn't. + export function RobotMotionLoader(props: RobotMotionLoaderProps): JSX.Element { const { header, body } = props return ( diff --git a/app/src/organisms/LabwarePositionCheck/shared/index.ts b/app/src/organisms/LabwarePositionCheck/shared/index.ts new file mode 100644 index 00000000000..438c6c9d13c --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/shared/index.ts @@ -0,0 +1,4 @@ +export { PrepareSpace } from './PrepareSpace' +export { ExitConfirmation } from './ExitConfirmation' +export { JogToWell } from './JogToWell' +export { RobotMotionLoader } from './RobotMotionLoader' diff --git a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx b/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx similarity index 93% rename from app/src/organisms/LabwarePositionCheck/AttachProbe.tsx rename to app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx index e214664810c..90cdd619e55 100644 --- a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx @@ -9,18 +9,17 @@ import { import { getPipetteNameSpecs } from '@opentrons/shared-data' import { css } from 'styled-components' import { ProbeNotAttached } from '/app/organisms/PipetteWizardFlows/ProbeNotAttached' -import { RobotMotionLoader } from './RobotMotionLoader' +import { RobotMotionLoader } from '/app/organisms/LabwarePositionCheck/shared' +import { GenericWizardTile } from '/app/molecules/GenericWizardTile' + import attachProbe1 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_1.webm' import attachProbe8 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_8.webm' import attachProbe96 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_96.webm' -import { GenericWizardTile } from '/app/molecules/GenericWizardTile' import type { CreateCommand } from '@opentrons/shared-data' -import type { AttachProbeStep, LPCStepProps } from './types' +import type { AttachProbeStep, LPCStepProps } from '../types' -export const AttachProbe = ( - props: LPCStepProps -): JSX.Element | null => { +export function AttachProbe(props: LPCStepProps): JSX.Element { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const { step, @@ -67,7 +66,8 @@ export const AttachProbe = ( }) }, []) - if (pipetteName == null || pipetteMount == null) return null + // TOME TODO: Instead of returning null, show an error. + // if (pipetteName == null || pipetteMount == null) return null const pipetteZMotorAxis: 'leftZ' | 'rightZ' = pipetteMount === 'left' ? 'leftZ' : 'rightZ' diff --git a/app/src/organisms/LabwarePositionCheck/TwoUpTileLayout.tsx b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/TwoUpTileLayout.tsx similarity index 96% rename from app/src/organisms/LabwarePositionCheck/TwoUpTileLayout.tsx rename to app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/TwoUpTileLayout.tsx index 7c6cd309bb4..edd6386fe18 100644 --- a/app/src/organisms/LabwarePositionCheck/TwoUpTileLayout.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/TwoUpTileLayout.tsx @@ -43,6 +43,8 @@ export interface TwoUpTileLayoutProps { footer: React.ReactNode } +// TOME TODO: Just used with IntroScreen, so can move there. + export function TwoUpTileLayout(props: TwoUpTileLayoutProps): JSX.Element { const { title, body, rightElement, footer } = props return ( diff --git a/app/src/organisms/LabwarePositionCheck/BeforeBeginning/getPrepCommands.ts b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/getPrepCommands.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/BeforeBeginning/getPrepCommands.ts rename to app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/getPrepCommands.ts diff --git a/app/src/organisms/LabwarePositionCheck/BeforeBeginning/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx similarity index 97% rename from app/src/organisms/LabwarePositionCheck/BeforeBeginning/index.tsx rename to app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx index 62cb6ddefba..46861d4951e 100644 --- a/app/src/organisms/LabwarePositionCheck/BeforeBeginning/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx @@ -18,14 +18,14 @@ import { LegacyStyledText, TYPOGRAPHY, } from '@opentrons/components' -import { RobotMotionLoader } from '../RobotMotionLoader' +import { RobotMotionLoader } from '/app/organisms/LabwarePositionCheck/shared' import { getPrepCommands } from './getPrepCommands' import { WizardRequiredEquipmentList } from '/app/molecules/WizardRequiredEquipmentList' import { getLatestCurrentOffsets } from '/app/transformations/runs' import { getIsOnDevice } from '/app/redux/config' import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' import { useSelector } from 'react-redux' -import { TwoUpTileLayout } from '../TwoUpTileLayout' +import { TwoUpTileLayout } from './TwoUpTileLayout' import { getTopPortalEl } from '/app/App/portal' import { SmallButton } from '/app/atoms/buttons' import { TerseOffsetTable } from '../ResultsSummary' diff --git a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx similarity index 95% rename from app/src/organisms/LabwarePositionCheck/CheckItem.tsx rename to app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx index cc8a864919e..d31201a4856 100644 --- a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx @@ -1,16 +1,21 @@ import { useEffect } from 'react' +import { useSelector } from 'react-redux' import omit from 'lodash/omit' import isEqual from 'lodash/isEqual' import { Trans, useTranslation } from 'react-i18next' + import { DIRECTION_COLUMN, Flex, LegacyStyledText, TYPOGRAPHY, } from '@opentrons/components' -import { RobotMotionLoader } from './RobotMotionLoader' -import { PrepareSpace } from './PrepareSpace' -import { JogToWell } from './JogToWell' + +import { + RobotMotionLoader, + PrepareSpace, + JogToWell, +} from '/app/organisms/LabwarePositionCheck/shared' import { getIsTiprack, getLabwareDefURI, @@ -20,13 +25,14 @@ import { IDENTITY_VECTOR, THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' -import { useSelector } from 'react-redux' -import { getLabwareDef } from './utils/labware' +import { + getLabwareDef, + getDisplayLocation, +} from '/app/organisms/LabwarePositionCheck/utils' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' import { UnorderedList } from '/app/molecules/UnorderedList' import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' import { getIsOnDevice } from '/app/redux/config' -import { getDisplayLocation } from './utils/getDisplayLocation' import { setFinalPosition, setInitialPosition, @@ -36,21 +42,18 @@ import type { CreateCommand, LabwareLocation, MoveLabwareCreateCommand, + LabwareDefinition2, + PipetteName, } from '@opentrons/shared-data' -import type { - CheckLabwareStep, - CheckPositionsStep, - CheckTipRacksStep, - LPCStepProps, -} from './types' +import type { CheckPositionsStep, LPCStepProps } from '../types' import type { TFunction } from 'i18next' const PROBE_LENGTH_MM = 44.5 // TOME TODO: Get rid of the 'null' or jsx here. export function CheckItem( - props: LPCStepProps -): JSX.Element | null { + props: LPCStepProps +): JSX.Element { const { step, protocolData, @@ -67,7 +70,10 @@ export function CheckItem( const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const { workingOffsets } = state const isOnDevice = useSelector(getIsOnDevice) - const labwareDef = getLabwareDef(labwareId, protocolData) + const labwareDef = getLabwareDef( + labwareId, + protocolData + ) as LabwareDefinition2 const pipette = protocolData.pipettes.find( pipette => pipette.id === pipetteId ) @@ -77,7 +83,7 @@ export function CheckItem( : '' const pipetteMount = pipette?.mount - const pipetteName = pipette?.pipetteName + const pipetteName = pipette?.pipetteName as PipetteName let modulePrepCommands: CreateCommand[] = [] const moduleType = (moduleId != null && @@ -127,8 +133,9 @@ export function CheckItem( } }, [moduleId]) - if (pipetteName == null || labwareDef == null || pipetteMount == null) - return null + // TOME TODO: Error instead of returning null. + // if (pipetteName == null || labwareDef == null || pipetteMount == null) + // return null const labwareDefs = getLabwareDefinitionsFromCommands(protocolData.commands) const pipetteZMotorAxis: 'leftZ' | 'rightZ' = diff --git a/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx similarity index 92% rename from app/src/organisms/LabwarePositionCheck/DetachProbe.tsx rename to app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx index 996a005f457..d741bb554e6 100644 --- a/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx @@ -1,24 +1,27 @@ import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' + import { LegacyStyledText, RESPONSIVENESS, SPACING, TYPOGRAPHY, } from '@opentrons/components' -import { RobotMotionLoader } from './RobotMotionLoader' import { getPipetteNameSpecs } from '@opentrons/shared-data' + +import { RobotMotionLoader } from '/app/organisms/LabwarePositionCheck/shared' +import { GenericWizardTile } from '/app/molecules/GenericWizardTile' + import detachProbe1 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_1.webm' import detachProbe8 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_8.webm' import detachProbe96 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_96.webm' -import { GenericWizardTile } from '/app/molecules/GenericWizardTile' -import type { DetachProbeStep, LPCStepProps } from './types' +import type { DetachProbeStep, LPCStepProps } from '../types' export const DetachProbe = ( props: LPCStepProps -): JSX.Element | null => { +): JSX.Element => { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const { step, @@ -58,7 +61,8 @@ export const DetachProbe = ( }) }, []) - if (pipetteName == null || pipetteMount == null) return null + // TOME TODO: Error instead of returning null. + // if (pipetteName == null || pipetteMount == null) return null const pipetteZMotorAxis: 'leftZ' | 'rightZ' = pipetteMount === 'left' ? 'leftZ' : 'rightZ' diff --git a/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx similarity index 97% rename from app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx rename to app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx index 0faac7d9419..8e42d8046f6 100644 --- a/app/src/organisms/LabwarePositionCheck/ResultsSummary.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx @@ -3,6 +3,7 @@ import styled, { css } from 'styled-components' import { useSelector } from 'react-redux' import isEqual from 'lodash/isEqual' import { useTranslation } from 'react-i18next' + import { getLabwareDefURI, getLabwareDisplayName, @@ -11,7 +12,6 @@ import { getVectorSum, IDENTITY_VECTOR, } from '@opentrons/shared-data' -import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' import { ALIGN_CENTER, ALIGN_FLEX_END, @@ -31,6 +31,8 @@ import { TYPOGRAPHY, DIRECTION_ROW, } from '@opentrons/components' + +import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' import { PythonLabwareOffsetSnippet } from '/app/molecules/PythonLabwareOffsetSnippet' import { getIsLabwareOffsetCodeSnippetsOn, @@ -40,19 +42,19 @@ import { SmallButton } from '/app/atoms/buttons' import { LabwareOffsetTabs } from '/app/organisms/LabwareOffsetTabs' import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' -import { getDisplayLocation } from './utils/getDisplayLocation' +import { getDisplayLocation } from '/app/organisms/LabwarePositionCheck/utils' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { LabwareOffsetCreateData } from '@opentrons/api-client' -import type { LPCStepProps, ResultsSummaryStep } from './types' +import type { LPCStepProps, ResultsSummaryStep } from '../types' import type { TFunction } from 'i18next' const LPC_HELP_LINK_URL = 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' -export const ResultsSummary = ( +export function ResultsSummary( props: LPCStepProps -): JSX.Element | null => { +): JSX.Element { const { i18n, t } = useTranslation('labware_position_check') const { protocolData, @@ -333,6 +335,9 @@ const OffsetTable = (props: OffsetTableProps): JSX.Element => { ) } +// TODO TOME: This needs to be an organism. It's used outside of LPC, too. Be sure +// to move the story, too. + // Very similar to the OffsetTable, but abbreviates certain things to be optimized // for smaller screens export const TerseOffsetTable = (props: OffsetTableProps): JSX.Element => { diff --git a/app/src/organisms/LabwarePositionCheck/steps/index.ts b/app/src/organisms/LabwarePositionCheck/steps/index.ts new file mode 100644 index 00000000000..9bf7efbad46 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/steps/index.ts @@ -0,0 +1,5 @@ +export { BeforeBeginning } from './BeforeBeginning' +export { AttachProbe } from './AttachProbe' +export { CheckItem } from './CheckItem' +export { DetachProbe } from './DetachProbe' +export { ResultsSummary } from './ResultsSummary' diff --git a/app/src/organisms/LabwarePositionCheck/types/steps.ts b/app/src/organisms/LabwarePositionCheck/types/steps.ts index 95c06576d36..b2270f3a5ec 100644 --- a/app/src/organisms/LabwarePositionCheck/types/steps.ts +++ b/app/src/organisms/LabwarePositionCheck/types/steps.ts @@ -4,12 +4,8 @@ import type { LPCWizardContentProps } from './content' export type LabwarePositionCheckStep = | BeforeBeginningStep - | CheckTipRacksStep | AttachProbeStep - | PickUpTipStep - | CheckLabwareStep | CheckPositionsStep - | ReturnTipStep | DetachProbeStep | ResultsSummaryStep @@ -40,26 +36,10 @@ export interface AttachProbeStep { pipetteId: string } -export interface CheckTipRacksStep extends PerformLPCStep { - section: typeof NAV_STEPS.CHECK_TIP_RACKS -} - export interface CheckPositionsStep extends PerformLPCStep { section: typeof NAV_STEPS.CHECK_POSITIONS } -export interface CheckLabwareStep extends PerformLPCStep { - section: typeof NAV_STEPS.CHECK_LABWARE -} - -export interface PickUpTipStep extends PerformLPCStep { - section: typeof NAV_STEPS.PICK_UP_TIP -} - -export interface ReturnTipStep extends PerformLPCStep { - section: typeof NAV_STEPS.RETURN_TIP -} - export interface DetachProbeStep { section: typeof NAV_STEPS.DETACH_PROBE pipetteId: string diff --git a/app/src/organisms/LabwarePositionCheck/utils/index.ts b/app/src/organisms/LabwarePositionCheck/utils/index.ts new file mode 100644 index 00000000000..fa267d52a4f --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/utils/index.ts @@ -0,0 +1,3 @@ +export * from './labware' +export * from './getProbeBasedLPCSteps' +export * from './getDisplayLocation' From b3ad7ca1ee6cfa006602e56ce4583845820c364b Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Mon, 6 Jan 2025 12:36:59 -0500 Subject: [PATCH 11/33] refactor(app): make TerseOffsetTable an organism TerseOffsetTable is used in other places in the app outside of LPC, so it should be an organism. --- .../steps/BeforeBeginning/index.tsx | 2 +- .../steps/ResultsSummary.tsx | 123 +-------------- .../ProtocolSetupOffsets/index.tsx | 2 +- .../TerseOffsetTable.stories.tsx | 6 +- app/src/organisms/TerseOffsetTable/index.tsx | 147 ++++++++++++++++++ 5 files changed, 152 insertions(+), 128 deletions(-) rename app/src/organisms/{LabwarePositionCheck => TerseOffsetTable}/TerseOffsetTable.stories.tsx (93%) create mode 100644 app/src/organisms/TerseOffsetTable/index.tsx diff --git a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx index 46861d4951e..4d3f09f5b96 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx @@ -28,7 +28,7 @@ import { useSelector } from 'react-redux' import { TwoUpTileLayout } from './TwoUpTileLayout' import { getTopPortalEl } from '/app/App/portal' import { SmallButton } from '/app/atoms/buttons' -import { TerseOffsetTable } from '../ResultsSummary' +import { TerseOffsetTable } from '/app/organisms/TerseOffsetTable' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' import type { LabwareOffset } from '@opentrons/api-client' diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx index 8e42d8046f6..3d463912c00 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx @@ -7,7 +7,6 @@ import { useTranslation } from 'react-i18next' import { getLabwareDefURI, getLabwareDisplayName, - getModuleType, getVectorDifference, getVectorSum, IDENTITY_VECTOR, @@ -17,19 +16,16 @@ import { ALIGN_FLEX_END, BORDERS, COLORS, - DeckInfoLabel, DIRECTION_COLUMN, Flex, Icon, JUSTIFY_SPACE_BETWEEN, - MODULE_ICON_NAME_BY_TYPE, OVERFLOW_AUTO, PrimaryButton, RESPONSIVENESS, SPACING, LegacyStyledText, TYPOGRAPHY, - DIRECTION_ROW, } from '@opentrons/components' import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' @@ -43,6 +39,7 @@ import { LabwareOffsetTabs } from '/app/organisms/LabwareOffsetTabs' import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' import { getDisplayLocation } from '/app/organisms/LabwarePositionCheck/utils' +import { TerseOffsetTable } from '/app/organisms/TerseOffsetTable' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { LabwareOffsetCreateData } from '@opentrons/api-client' @@ -334,121 +331,3 @@ const OffsetTable = (props: OffsetTableProps): JSX.Element => { ) } - -// TODO TOME: This needs to be an organism. It's used outside of LPC, too. Be sure -// to move the story, too. - -// Very similar to the OffsetTable, but abbreviates certain things to be optimized -// for smaller screens -export const TerseOffsetTable = (props: OffsetTableProps): JSX.Element => { - const { offsets, labwareDefinitions } = props - const { i18n, t } = useTranslation('labware_position_check') - return ( - - - - - {i18n.format(t('slot_location'), 'capitalize')} - - {i18n.format(t('labware'), 'capitalize')} - {i18n.format(t('offsets'), 'capitalize')} - - - - - {offsets.map(({ location, definitionUri, vector }, index) => { - const labwareDef = labwareDefinitions.find( - def => getLabwareDefURI(def) === definitionUri - ) - const labwareDisplayName = - labwareDef != null ? getLabwareDisplayName(labwareDef) : '' - return ( - - - - - {location.moduleModel != null ? ( - - ) : null} - - - - - {labwareDisplayName} - - - - {isEqual(vector, IDENTITY_VECTOR) ? ( - {t('no_labware_offsets')} - ) : ( - - {[vector.x, vector.y, vector.z].map((axis, index) => ( - - 0 ? SPACING.spacing8 : 0} - marginRight={SPACING.spacing4} - fontWeight={TYPOGRAPHY.fontWeightSemiBold} - > - {['X', 'Y', 'Z'][index]} - - - {axis.toFixed(1)} - - - ))} - - )} - - - ) - })} - - - ) -} - -const TerseTable = styled('table')` - table-layout: auto; - width: 100%; - border-spacing: 0 ${SPACING.spacing4}; - margin: ${SPACING.spacing16} 0; - text-align: left; - tr td:first-child { - border-top-left-radius: ${BORDERS.borderRadius8}; - border-bottom-left-radius: ${BORDERS.borderRadius8}; - padding-left: ${SPACING.spacing12}; - } - tr td:last-child { - border-top-right-radius: ${BORDERS.borderRadius8}; - border-bottom-right-radius: ${BORDERS.borderRadius8}; - padding-right: ${SPACING.spacing12}; - } -` -const TerseHeader = styled('th')` - font-size: ${TYPOGRAPHY.fontSize20}; - line-height: ${TYPOGRAPHY.lineHeight24}; - font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; -` -const TerseTableRow = styled('tr')` - background-color: ${COLORS.grey35}; -` - -const TerseTableDatum = styled('td')` - padding: ${SPACING.spacing12} 0; - white-space: break-spaces; - text-overflow: wrap; -` diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupOffsets/index.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupOffsets/index.tsx index 485f16cec8d..61eb8625047 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupOffsets/index.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupOffsets/index.tsx @@ -16,7 +16,7 @@ import { useToaster } from '/app/organisms/ToasterOven' import { ODDBackButton } from '/app/molecules/ODDBackButton' import { FloatingActionButton, SmallButton } from '/app/atoms/buttons' import type { SetupScreens } from '../types' -import { TerseOffsetTable } from '/app/organisms/LegacyLabwarePositionCheck/ResultsSummary' +import { TerseOffsetTable } from '/app/organisms/TerseOffsetTable' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' import { useNotifyRunQuery, diff --git a/app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx b/app/src/organisms/TerseOffsetTable/TerseOffsetTable.stories.tsx similarity index 93% rename from app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx rename to app/src/organisms/TerseOffsetTable/TerseOffsetTable.stories.tsx index 7fd221bcabf..0a24182e887 100644 --- a/app/src/organisms/LabwarePositionCheck/TerseOffsetTable.stories.tsx +++ b/app/src/organisms/TerseOffsetTable/TerseOffsetTable.stories.tsx @@ -13,8 +13,8 @@ import { getLabwareDefURI, } from '@opentrons/shared-data' -import { SmallButton } from '../../atoms/buttons' -import { TerseOffsetTable } from './steps/ResultsSummary' +import { SmallButton } from '../../../atoms/buttons' +import { TerseOffsetTable } from '.' import type { Story, Meta } from '@storybook/react' import type { LabwareDefinition2 } from '@opentrons/shared-data' @@ -25,8 +25,6 @@ export default { parameters: VIEWPORT.touchScreenViewport, } as Meta -// TOME TODO: This should be moved into whatever dir handles the actual component. - // Note: 59rem(944px) is the size of ODD const Template: Story> = ({ ...args diff --git a/app/src/organisms/TerseOffsetTable/index.tsx b/app/src/organisms/TerseOffsetTable/index.tsx new file mode 100644 index 00000000000..5fdbaf162a2 --- /dev/null +++ b/app/src/organisms/TerseOffsetTable/index.tsx @@ -0,0 +1,147 @@ +import { Fragment } from 'react' +import styled from 'styled-components' +import isEqual from 'lodash/isEqual' +import { useTranslation } from 'react-i18next' + +import { + getLabwareDefURI, + getLabwareDisplayName, + getModuleType, + IDENTITY_VECTOR, +} from '@opentrons/shared-data' +import { + BORDERS, + COLORS, + DeckInfoLabel, + Flex, + MODULE_ICON_NAME_BY_TYPE, + SPACING, + LegacyStyledText, + TYPOGRAPHY, + DIRECTION_ROW, +} from '@opentrons/components' + +import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { LabwareDefinition2 } from '@opentrons/shared-data' + +export interface TerseOffsetTableProps { + offsets: LabwareOffsetCreateData[] + labwareDefinitions: LabwareDefinition2[] +} + +// Very similar to the OffsetTable, but abbreviates certain things to be optimized +// for smaller screens. +export function TerseOffsetTable({ + offsets, + labwareDefinitions, +}: TerseOffsetTableProps): JSX.Element { + const { i18n, t } = useTranslation('labware_position_check') + return ( + + + + + {i18n.format(t('slot_location'), 'capitalize')} + + {i18n.format(t('labware'), 'capitalize')} + {i18n.format(t('offsets'), 'capitalize')} + + + + + {offsets.map(({ location, definitionUri, vector }, index) => { + const labwareDef = labwareDefinitions.find( + def => getLabwareDefURI(def) === definitionUri + ) + const labwareDisplayName = + labwareDef != null ? getLabwareDisplayName(labwareDef) : '' + return ( + + + + + {location.moduleModel != null ? ( + + ) : null} + + + + + {labwareDisplayName} + + + + {isEqual(vector, IDENTITY_VECTOR) ? ( + {t('no_labware_offsets')} + ) : ( + + {[vector.x, vector.y, vector.z].map((axis, index) => ( + + 0 ? SPACING.spacing8 : 0} + marginRight={SPACING.spacing4} + fontWeight={TYPOGRAPHY.fontWeightSemiBold} + > + {['X', 'Y', 'Z'][index]} + + + {axis.toFixed(1)} + + + ))} + + )} + + + ) + })} + + + ) +} + +const TerseTable = styled('table')` + table-layout: auto; + width: 100%; + border-spacing: 0 ${SPACING.spacing4}; + margin: ${SPACING.spacing16} 0; + text-align: left; + tr td:first-child { + border-top-left-radius: ${BORDERS.borderRadius8}; + border-bottom-left-radius: ${BORDERS.borderRadius8}; + padding-left: ${SPACING.spacing12}; + } + tr td:last-child { + border-top-right-radius: ${BORDERS.borderRadius8}; + border-bottom-right-radius: ${BORDERS.borderRadius8}; + padding-right: ${SPACING.spacing12}; + } +` +const TerseHeader = styled('th')` + font-size: ${TYPOGRAPHY.fontSize20}; + line-height: ${TYPOGRAPHY.lineHeight24}; + font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; +` +const TerseTableRow = styled('tr')` + background-color: ${COLORS.grey35}; +` + +const TerseTableDatum = styled('td')` + padding: ${SPACING.spacing12} 0; + white-space: break-spaces; + text-overflow: wrap; +` From 6370491c640be5a1e092dabed44a3f77cbc0818e Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Mon, 6 Jan 2025 13:23:40 -0500 Subject: [PATCH 12/33] refactor(app): remove the get labware display util We already have a global util that does the same thing. --- ...{FatalErrorModal.tsx => LPCErrorModal.tsx} | 2 +- .../LabwarePositionCheck/LPCWizardFlex.tsx | 8 +-- .../LabwarePositionCheck/steps/CheckItem.tsx | 49 ++++++------- .../steps/ResultsSummary.tsx | 43 ++++++++---- .../utils/getDisplayLocation.ts | 68 ------------------- .../getProbeBasedLPCSteps.ts | 10 ++- .../getLPCSteps/index.ts} | 8 +-- .../LabwarePositionCheck/utils/index.ts | 3 +- 8 files changed, 72 insertions(+), 119 deletions(-) rename app/src/organisms/LabwarePositionCheck/{FatalErrorModal.tsx => LPCErrorModal.tsx} (98%) delete mode 100644 app/src/organisms/LabwarePositionCheck/utils/getDisplayLocation.ts rename app/src/organisms/LabwarePositionCheck/utils/{ => getLPCSteps}/getProbeBasedLPCSteps.ts (96%) rename app/src/organisms/LabwarePositionCheck/{getLabwarePositionCheckSteps.ts => utils/getLPCSteps/index.ts} (69%) diff --git a/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx b/app/src/organisms/LabwarePositionCheck/LPCErrorModal.tsx similarity index 98% rename from app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx rename to app/src/organisms/LabwarePositionCheck/LPCErrorModal.tsx index 4bfba7b9562..713789cf3bd 100644 --- a/app/src/organisms/LabwarePositionCheck/FatalErrorModal.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCErrorModal.tsx @@ -25,7 +25,7 @@ import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/ const SUPPORT_EMAIL = 'support@opentrons.com' -export function FatalError({ +export function LPCErrorModal({ errorMessage, onCloseClick, }: LPCWizardContentProps): JSX.Element { diff --git a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx index 1be740188b0..e1612138e9a 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx @@ -23,9 +23,9 @@ import { } from '/app/organisms/LabwarePositionCheck/shared' import { WizardHeader } from '/app/molecules/WizardHeader' import { getIsOnDevice } from '/app/redux/config' -import { FatalError } from './FatalErrorModal' +import { LPCErrorModal } from './LPCErrorModal' import { useChainMaintenanceCommands } from '/app/resources/maintenance_runs' -import { getLabwarePositionCheckSteps } from './getLabwarePositionCheckSteps' +import { getLPCSteps } from './utils' import { useLPCInitialState } from '/app/organisms/LabwarePositionCheck/hooks' import { useLPCReducer } from '/app/organisms/LabwarePositionCheck/redux' @@ -122,7 +122,7 @@ export function LPCWizardFlex( : currentStepIndex ) } - const LPCSteps = getLabwarePositionCheckSteps(mostRecentAnalysis) + const LPCSteps = getLPCSteps(mostRecentAnalysis) const totalStepCount = LPCSteps.length - 1 const currentStep = LPCSteps?.[currentStepIndex] @@ -253,7 +253,7 @@ function LPCWizardContent(props: LPCWizardContentProps): JSX.Element { return } if (props.errorMessage != null) { - return + return } if (props.showConfirmation) { return diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx index d31201a4856..9cce1a61c63 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx @@ -1,6 +1,5 @@ import { useEffect } from 'react' import { useSelector } from 'react-redux' -import omit from 'lodash/omit' import isEqual from 'lodash/isEqual' import { Trans, useTranslation } from 'react-i18next' @@ -17,6 +16,7 @@ import { JogToWell, } from '/app/organisms/LabwarePositionCheck/shared' import { + FLEX_ROBOT_TYPE, getIsTiprack, getLabwareDefURI, getLabwareDisplayName, @@ -25,11 +25,11 @@ import { IDENTITY_VECTOR, THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' +import { getLabwareDef } from '/app/organisms/LabwarePositionCheck/utils' import { - getLabwareDef, - getDisplayLocation, -} from '/app/organisms/LabwarePositionCheck/utils' -import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' + getLabwareDefinitionsFromCommands, + getLabwareDisplayLocation, +} from '/app/local-resources/labware' import { UnorderedList } from '/app/molecules/UnorderedList' import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' import { getIsOnDevice } from '/app/redux/config' @@ -46,7 +46,6 @@ import type { PipetteName, } from '@opentrons/shared-data' import type { CheckPositionsStep, LPCStepProps } from '../types' -import type { TFunction } from 'i18next' const PROBE_LENGTH_MM = 44.5 @@ -67,7 +66,7 @@ export function CheckItem( setErrorMessage, } = props const { labwareId, pipetteId, moduleId, adapterId, location } = step - const { t, i18n } = useTranslation(['labware_position_check', 'shared']) + const { t } = useTranslation(['labware_position_check', 'shared']) const { workingOffsets } = state const isOnDevice = useSelector(getIsOnDevice) const labwareDef = getLabwareDef( @@ -137,23 +136,30 @@ export function CheckItem( // if (pipetteName == null || labwareDef == null || pipetteMount == null) // return null + // TOME TODO: This runs every check item. This needs to be memoized. + const labwareDefs = getLabwareDefinitionsFromCommands(protocolData.commands) const pipetteZMotorAxis: 'leftZ' | 'rightZ' = pipetteMount === 'left' ? 'leftZ' : 'rightZ' const isTiprack = getIsTiprack(labwareDef) - const displayLocation = getDisplayLocation( + const displayLocation = getLabwareDisplayLocation({ location, - labwareDefs, - t as TFunction, - i18n - ) - const slotOnlyDisplayLocation = getDisplayLocation( + allRunDefs: labwareDefs, + detailLevel: 'full', + t, + loadedModules: protocolData.modules, + loadedLabwares: protocolData.labware, + robotType: FLEX_ROBOT_TYPE, + }) + const slotOnlyDisplayLocation = getLabwareDisplayLocation({ location, - labwareDefs, - t as TFunction, - i18n, - true - ) + detailLevel: 'slot-only', + t, + loadedModules: protocolData.modules, + loadedLabwares: protocolData.labware, + robotType: FLEX_ROBOT_TYPE, + }) + const labwareDisplayName = getLabwareDisplayName(labwareDef) let placeItemInstruction: JSX.Element = ( @@ -196,12 +202,7 @@ export function CheckItem( tOptions={{ adapter: adapterDisplayName, labware: labwareDisplayName, - location: getDisplayLocation( - omit(location, ['definitionUri']), // only want the adapter's location here - labwareDefs, - t as TFunction, - i18n - ), + location: slotOnlyDisplayLocation, }} components={{ bold: ( diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx index 3d463912c00..6df0082eaff 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx @@ -5,6 +5,7 @@ import isEqual from 'lodash/isEqual' import { useTranslation } from 'react-i18next' import { + FLEX_ROBOT_TYPE, getLabwareDefURI, getLabwareDisplayName, getVectorDifference, @@ -37,14 +38,15 @@ import { import { SmallButton } from '/app/atoms/buttons' import { LabwareOffsetTabs } from '/app/organisms/LabwareOffsetTabs' import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' -import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' -import { getDisplayLocation } from '/app/organisms/LabwarePositionCheck/utils' +import { + getLabwareDefinitionsFromCommands, + getLabwareDisplayLocation, +} from '/app/local-resources/labware' import { TerseOffsetTable } from '/app/organisms/TerseOffsetTable' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { LabwareOffsetCreateData } from '@opentrons/api-client' import type { LPCStepProps, ResultsSummaryStep } from '../types' -import type { TFunction } from 'i18next' const LPC_HELP_LINK_URL = 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' @@ -52,7 +54,6 @@ const LPC_HELP_LINK_URL = export function ResultsSummary( props: LPCStepProps ): JSX.Element { - const { i18n, t } = useTranslation('labware_position_check') const { protocolData, state, @@ -60,7 +61,10 @@ export function ResultsSummary( existingOffsets, isApplyingOffsets, } = props + const { i18n, t } = useTranslation('labware_position_check') const { workingOffsets } = state + + // TOME TODO: Yeah no. Hoist this out of everything and make it a content prop. const labwareDefinitions = getLabwareDefinitionsFromCommands( protocolData.commands ) @@ -70,6 +74,7 @@ export function ResultsSummary( ) const isOnDevice = useSelector(getIsOnDevice) + // TOME: TODO: I believe this should be in a selector. const offsetsToApply = useMemo(() => { return workingOffsets.map( ({ initialPosition, finalPosition, labwareId, location }) => { @@ -114,6 +119,7 @@ export function ResultsSummary( ) const JupyterSnippet = ( @@ -247,14 +253,18 @@ const Header = styled.h1` } ` -interface OffsetTableProps { +interface OffsetTableProps extends LPCStepProps { offsets: LabwareOffsetCreateData[] labwareDefinitions: LabwareDefinition2[] } -const OffsetTable = (props: OffsetTableProps): JSX.Element => { - const { offsets, labwareDefinitions } = props - const { t, i18n } = useTranslation('labware_position_check') +const OffsetTable = ({ + offsets, + labwareDefinitions, + protocolData, +}: OffsetTableProps): JSX.Element => { + const { t } = useTranslation('labware_position_check') + return ( @@ -267,6 +277,16 @@ const OffsetTable = (props: OffsetTableProps): JSX.Element => { {offsets.map(({ location, definitionUri, vector }, index) => { + const displayLocation = getLabwareDisplayLocation({ + location, + allRunDefs: labwareDefinitions, + detailLevel: 'full', + t, + loadedModules: protocolData.modules, + loadedLabwares: protocolData.labware, + robotType: FLEX_ROBOT_TYPE, + }) + const labwareDef = labwareDefinitions.find( def => getLabwareDefURI(def) === definitionUri ) @@ -285,12 +305,7 @@ const OffsetTable = (props: OffsetTableProps): JSX.Element => { as="p" textTransform={TYPOGRAPHY.textTransformCapitalize} > - {getDisplayLocation( - location, - labwareDefinitions, - t as TFunction, - i18n - )} + {displayLocation} diff --git a/app/src/organisms/LabwarePositionCheck/utils/getDisplayLocation.ts b/app/src/organisms/LabwarePositionCheck/utils/getDisplayLocation.ts deleted file mode 100644 index 21f1c06c1fa..00000000000 --- a/app/src/organisms/LabwarePositionCheck/utils/getDisplayLocation.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { - getModuleDisplayName, - getModuleType, - THERMOCYCLER_MODULE_TYPE, - getLabwareDefURI, -} from '@opentrons/shared-data' -import type { i18n, TFunction } from 'i18next' -import type { LabwareDefinition2 } from '@opentrons/shared-data' -import type { LabwareOffsetLocation } from '@opentrons/api-client' - -// TOME TODO: I think this is no longer needed given the new utils, but double check. - -export function getDisplayLocation( - location: LabwareOffsetLocation, - labwareDefinitions: LabwareDefinition2[], - t: TFunction, - i18n: i18n, - slotOnly?: boolean -): string { - const slotDisplayLocation = i18n.format( - t('slot_name', { slotName: location.slotName }), - 'titleCase' - ) - if (slotOnly) { - return slotDisplayLocation - } - - if ('definitionUri' in location && location.definitionUri != null) { - const adapterDisplayName = labwareDefinitions.find( - def => getLabwareDefURI(def) === location.definitionUri - )?.metadata.displayName - - if ('moduleModel' in location && location.moduleModel != null) { - const { moduleModel } = location - const moduleDisplayName = getModuleDisplayName(moduleModel) - if (getModuleType(moduleModel) === THERMOCYCLER_MODULE_TYPE) { - return t('adapter_in_tc', { - adapter: adapterDisplayName, - module: moduleDisplayName, - }) - } else { - return t('adapter_in_mod_in_slot', { - adapter: adapterDisplayName, - module: moduleDisplayName, - slot: slotDisplayLocation, - }) - } - } else { - return t('adapter_in_slot', { - adapter: adapterDisplayName, - slot: slotDisplayLocation, - }) - } - } else if ('moduleModel' in location && location.moduleModel != null) { - const { moduleModel } = location - const moduleDisplayName = getModuleDisplayName(moduleModel) - if (getModuleType(moduleModel) === THERMOCYCLER_MODULE_TYPE) { - return moduleDisplayName - } else { - return t('module_in_slot', { - module: moduleDisplayName, - slot: slotDisplayLocation, - }) - } - } else { - return slotDisplayLocation - } -} diff --git a/app/src/organisms/LabwarePositionCheck/utils/getProbeBasedLPCSteps.ts b/app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/getProbeBasedLPCSteps.ts similarity index 96% rename from app/src/organisms/LabwarePositionCheck/utils/getProbeBasedLPCSteps.ts rename to app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/getProbeBasedLPCSteps.ts index 3deb4a0acf6..37643bb3659 100644 --- a/app/src/organisms/LabwarePositionCheck/utils/getProbeBasedLPCSteps.ts +++ b/app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/getProbeBasedLPCSteps.ts @@ -1,6 +1,8 @@ import { isEqual } from 'lodash' -import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' + import { getLabwareDefURI, getPipetteNameSpecs } from '@opentrons/shared-data' + +import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' import { getLabwareLocationCombos } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' @@ -8,7 +10,10 @@ import type { CompletedProtocolAnalysis, LoadedPipette, } from '@opentrons/shared-data' -import type { LabwarePositionCheckStep, CheckPositionsStep } from '../types' +import type { + LabwarePositionCheckStep, + CheckPositionsStep, +} from '/app/organisms/LabwarePositionCheck/types' import type { LabwareLocationCombo } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' function getPrimaryPipetteId(pipettes: LoadedPipette[]): string { @@ -17,6 +22,7 @@ function getPrimaryPipetteId(pipettes: LoadedPipette[]): string { 'no pipettes in protocol, cannot determine primary pipette for LPC' ) } + return pipettes.reduce((acc, pip) => { return (getPipetteNameSpecs(acc.pipetteName)?.channels ?? 0) > (getPipetteNameSpecs(pip.pipetteName)?.channels ?? 0) diff --git a/app/src/organisms/LabwarePositionCheck/getLabwarePositionCheckSteps.ts b/app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/index.ts similarity index 69% rename from app/src/organisms/LabwarePositionCheck/getLabwarePositionCheckSteps.ts rename to app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/index.ts index ed935ee938b..606feda13ce 100644 --- a/app/src/organisms/LabwarePositionCheck/getLabwarePositionCheckSteps.ts +++ b/app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/index.ts @@ -1,11 +1,11 @@ -import { getProbeBasedLPCSteps } from './utils/getProbeBasedLPCSteps' +import { getProbeBasedLPCSteps } from './getProbeBasedLPCSteps' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' -import type { LabwarePositionCheckStep } from './types' +import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' -export const getLabwarePositionCheckSteps = ( +export function getLPCSteps( protocolData: CompletedProtocolAnalysis -): LabwarePositionCheckStep[] => { +): LabwarePositionCheckStep[] { if ('pipettes' in protocolData) { if (protocolData.pipettes.length === 0) { throw new Error( diff --git a/app/src/organisms/LabwarePositionCheck/utils/index.ts b/app/src/organisms/LabwarePositionCheck/utils/index.ts index fa267d52a4f..47804106c25 100644 --- a/app/src/organisms/LabwarePositionCheck/utils/index.ts +++ b/app/src/organisms/LabwarePositionCheck/utils/index.ts @@ -1,3 +1,2 @@ export * from './labware' -export * from './getProbeBasedLPCSteps' -export * from './getDisplayLocation' +export * from './getLPCSteps' From 17a856dfbfd68ee9c8fa08d30acf88bb947f87fa Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Mon, 6 Jan 2025 14:04:14 -0500 Subject: [PATCH 13/33] refactor(app): reduce command iteration in LPC We iterate over the same command set a lot to get labware definitions. We can just do this once. --- .../LabwarePositionCheck/LPCWizardFlex.tsx | 37 +++++++------ .../steps/BeforeBeginning/index.tsx | 6 +-- .../LabwarePositionCheck/steps/CheckItem.tsx | 26 +++++---- .../steps/ResultsSummary.tsx | 14 ++--- .../LabwarePositionCheck/types/content.ts | 1 + .../utils/getItemLabwareDef.ts | 25 +++++++++ .../getLPCSteps/getProbeBasedLPCSteps.ts | 53 +++++++++---------- .../utils/getLPCSteps/index.ts | 18 +++++-- .../LabwarePositionCheck/utils/index.ts | 2 +- .../LabwarePositionCheck/utils/labware.ts | 21 -------- 10 files changed, 104 insertions(+), 99 deletions(-) create mode 100644 app/src/organisms/LabwarePositionCheck/utils/getItemLabwareDef.ts delete mode 100644 app/src/organisms/LabwarePositionCheck/utils/labware.ts diff --git a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx index e1612138e9a..666b8b62a12 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useMemo, useState } from 'react' import { createPortal } from 'react-dom' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -38,19 +38,14 @@ import type { Axis, Sign, StepSize } from '/app/molecules/JogControls/types' import type { LPCFlowsProps } from '/app/organisms/LabwarePositionCheck/LPCFlows' import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' +import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' const JOG_COMMAND_TIMEOUT = 10000 // 10 seconds export function LPCWizardFlex( props: Omit ): JSX.Element { - const { - mostRecentAnalysis, - runId, - onCloseClick, - protocolName, - maintenanceRunId, - } = props + const { mostRecentAnalysis, runId, onCloseClick, maintenanceRunId } = props const isOnDevice = useSelector(getIsOnDevice) const [errorMessage, setErrorMessage] = useState(null) @@ -70,6 +65,11 @@ export function LPCWizardFlex( isCommandMutationLoading: isCommandChainLoading, } = useChainMaintenanceCommands() + const labwareDefs = useMemo( + () => getLabwareDefinitionsFromCommands(mostRecentAnalysis.commands), + [mostRecentAnalysis] + ) + const { createLabwareOffset } = useCreateLabwareOffsetMutation() const [currentStepIndex, setCurrentStepIndex] = useState(0) const handleCleanUpAndClose = (): void => { @@ -122,7 +122,10 @@ export function LPCWizardFlex( : currentStepIndex ) } - const LPCSteps = getLPCSteps(mostRecentAnalysis) + const LPCSteps = getLPCSteps({ + protocolData: mostRecentAnalysis, + labwareDefs, + }) const totalStepCount = LPCSteps.length - 1 const currentStep = LPCSteps?.[currentStepIndex] @@ -178,10 +181,8 @@ export function LPCWizardFlex( return ( ) } @@ -262,22 +265,22 @@ function LPCWizardContent(props: LPCWizardContentProps): JSX.Element { // Handle step-based routing. switch (step.section) { case NAV_STEPS.BEFORE_BEGINNING: - return + return case NAV_STEPS.CHECK_POSITIONS: - return + return case NAV_STEPS.ATTACH_PROBE: - return + return case NAV_STEPS.DETACH_PROBE: - return + return case NAV_STEPS.RESULTS_SUMMARY: - return + return default: console.error('Unhandled LPC step.') - return + return } } diff --git a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx index 4d3f09f5b96..1935c078414 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx @@ -29,7 +29,6 @@ import { TwoUpTileLayout } from './TwoUpTileLayout' import { getTopPortalEl } from '/app/App/portal' import { SmallButton } from '/app/atoms/buttons' import { TerseOffsetTable } from '/app/organisms/TerseOffsetTable' -import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' import type { LabwareOffset } from '@opentrons/api-client' import type { LabwareDefinition2 } from '@opentrons/shared-data' @@ -49,6 +48,7 @@ export function BeforeBeginning({ setErrorMessage, existingOffsets, protocolName, + labwareDefs, }: LPCStepProps): JSX.Element { const isOnDevice = useSelector(getIsOnDevice) const { t, i18n } = useTranslation(['labware_position_check', 'shared']) @@ -99,9 +99,7 @@ export function BeforeBeginning({ {isOnDevice ? ( ) : ( diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx index 9cce1a61c63..39a5aedf217 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx @@ -25,11 +25,8 @@ import { IDENTITY_VECTOR, THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' -import { getLabwareDef } from '/app/organisms/LabwarePositionCheck/utils' -import { - getLabwareDefinitionsFromCommands, - getLabwareDisplayLocation, -} from '/app/local-resources/labware' +import { getItemLabwareDef } from '/app/organisms/LabwarePositionCheck/utils' +import { getLabwareDisplayLocation } from '/app/local-resources/labware' import { UnorderedList } from '/app/molecules/UnorderedList' import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' import { getIsOnDevice } from '/app/redux/config' @@ -42,14 +39,12 @@ import type { CreateCommand, LabwareLocation, MoveLabwareCreateCommand, - LabwareDefinition2, PipetteName, } from '@opentrons/shared-data' import type { CheckPositionsStep, LPCStepProps } from '../types' const PROBE_LENGTH_MM = 44.5 -// TOME TODO: Get rid of the 'null' or jsx here. export function CheckItem( props: LPCStepProps ): JSX.Element { @@ -64,21 +59,27 @@ export function CheckItem( isRobotMoving, existingOffsets, setErrorMessage, + labwareDefs, } = props const { labwareId, pipetteId, moduleId, adapterId, location } = step const { t } = useTranslation(['labware_position_check', 'shared']) const { workingOffsets } = state const isOnDevice = useSelector(getIsOnDevice) - const labwareDef = getLabwareDef( + const labwareDef = getItemLabwareDef({ labwareId, - protocolData - ) as LabwareDefinition2 + loadedLabware: protocolData.labware, + labwareDefs, + }) const pipette = protocolData.pipettes.find( pipette => pipette.id === pipetteId ) const adapterDisplayName = adapterId != null - ? getLabwareDef(adapterId, protocolData)?.metadata.displayName + ? getItemLabwareDef({ + labwareId: adapterId, + loadedLabware: protocolData.labware, + labwareDefs, + })?.metadata.displayName : '' const pipetteMount = pipette?.mount @@ -136,9 +137,6 @@ export function CheckItem( // if (pipetteName == null || labwareDef == null || pipetteMount == null) // return null - // TOME TODO: This runs every check item. This needs to be memoized. - - const labwareDefs = getLabwareDefinitionsFromCommands(protocolData.commands) const pipetteZMotorAxis: 'leftZ' | 'rightZ' = pipetteMount === 'left' ? 'leftZ' : 'rightZ' const isTiprack = getIsTiprack(labwareDef) diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx index 6df0082eaff..4125686bd98 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx @@ -38,10 +38,7 @@ import { import { SmallButton } from '/app/atoms/buttons' import { LabwareOffsetTabs } from '/app/organisms/LabwareOffsetTabs' import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' -import { - getLabwareDefinitionsFromCommands, - getLabwareDisplayLocation, -} from '/app/local-resources/labware' +import { getLabwareDisplayLocation } from '/app/local-resources/labware' import { TerseOffsetTable } from '/app/organisms/TerseOffsetTable' import type { LabwareDefinition2 } from '@opentrons/shared-data' @@ -60,14 +57,11 @@ export function ResultsSummary( handleApplyOffsets, existingOffsets, isApplyingOffsets, + labwareDefs, } = props const { i18n, t } = useTranslation('labware_position_check') const { workingOffsets } = state - // TOME TODO: Yeah no. Hoist this out of everything and make it a content prop. - const labwareDefinitions = getLabwareDefinitionsFromCommands( - protocolData.commands - ) const isSubmittingAndClosing = isApplyingOffsets const isLabwareOffsetCodeSnippetsOn = useSelector( getIsLabwareOffsetCodeSnippetsOn @@ -113,12 +107,12 @@ export function ResultsSummary( const TableComponent = isOnDevice ? ( ) : ( ) diff --git a/app/src/organisms/LabwarePositionCheck/types/content.ts b/app/src/organisms/LabwarePositionCheck/types/content.ts index a8ffa12f9c6..bbb02d4da40 100644 --- a/app/src/organisms/LabwarePositionCheck/types/content.ts +++ b/app/src/organisms/LabwarePositionCheck/types/content.ts @@ -42,6 +42,7 @@ export interface LPCWizardContentProps protocolHasModules: boolean handleApplyOffsets: (offsets: LabwareOffsetCreateData[]) => void isApplyingOffsets: boolean + labwareDefs: LabwareDefinition2[] } export interface LabwareToOrder { diff --git a/app/src/organisms/LabwarePositionCheck/utils/getItemLabwareDef.ts b/app/src/organisms/LabwarePositionCheck/utils/getItemLabwareDef.ts new file mode 100644 index 00000000000..45e20766a5b --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/utils/getItemLabwareDef.ts @@ -0,0 +1,25 @@ +import { getLabwareDefURI } from '@opentrons/shared-data' + +import type { + CompletedProtocolAnalysis, + LabwareDefinition2, +} from '@opentrons/shared-data' + +interface GetLabwareDefsForLPCParams { + labwareId: string + loadedLabware: CompletedProtocolAnalysis['labware'] + labwareDefs: LabwareDefinition2[] +} + +export function getItemLabwareDef({ + labwareId, + loadedLabware, + labwareDefs, +}: GetLabwareDefsForLPCParams): LabwareDefinition2 { + const labwareDefUri = loadedLabware.find(l => l.id === labwareId) + ?.definitionUri + + return labwareDefs.find( + def => getLabwareDefURI(def) === labwareDefUri + ) as LabwareDefinition2 // Safe assumption +} diff --git a/app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/getProbeBasedLPCSteps.ts b/app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/getProbeBasedLPCSteps.ts index 37643bb3659..2ba2ed586f4 100644 --- a/app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/getProbeBasedLPCSteps.ts +++ b/app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/getProbeBasedLPCSteps.ts @@ -4,43 +4,27 @@ import { getLabwareDefURI, getPipetteNameSpecs } from '@opentrons/shared-data' import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' import { getLabwareLocationCombos } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' -import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' -import type { - CompletedProtocolAnalysis, - LoadedPipette, -} from '@opentrons/shared-data' +import type { LoadedPipette } from '@opentrons/shared-data' import type { LabwarePositionCheckStep, CheckPositionsStep, } from '/app/organisms/LabwarePositionCheck/types' import type { LabwareLocationCombo } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' - -function getPrimaryPipetteId(pipettes: LoadedPipette[]): string { - if (pipettes.length < 1) { - throw new Error( - 'no pipettes in protocol, cannot determine primary pipette for LPC' - ) - } - - return pipettes.reduce((acc, pip) => { - return (getPipetteNameSpecs(acc.pipetteName)?.channels ?? 0) > - (getPipetteNameSpecs(pip.pipetteName)?.channels ?? 0) - ? pip - : acc - }, pipettes[0]).id -} +import type { GetLPCStepsParams } from '.' export const getProbeBasedLPCSteps = ( - protocolData: CompletedProtocolAnalysis + params: GetLPCStepsParams ): LabwarePositionCheckStep[] => { + const { protocolData } = params + return [ { section: NAV_STEPS.BEFORE_BEGINNING }, { section: NAV_STEPS.ATTACH_PROBE, pipetteId: getPrimaryPipetteId(protocolData.pipettes), }, - ...getAllCheckSectionSteps(protocolData), + ...getAllCheckSectionSteps(params), { section: NAV_STEPS.DETACH_PROBE, pipetteId: getPrimaryPipetteId(protocolData.pipettes), @@ -49,19 +33,34 @@ export const getProbeBasedLPCSteps = ( ] } -function getAllCheckSectionSteps( - protocolData: CompletedProtocolAnalysis -): CheckPositionsStep[] { +function getPrimaryPipetteId(pipettes: LoadedPipette[]): string { + if (pipettes.length < 1) { + throw new Error( + 'no pipettes in protocol, cannot determine primary pipette for LPC' + ) + } + + return pipettes.reduce((acc, pip) => { + return (getPipetteNameSpecs(acc.pipetteName)?.channels ?? 0) > + (getPipetteNameSpecs(pip.pipetteName)?.channels ?? 0) + ? pip + : acc + }, pipettes[0]).id +} + +function getAllCheckSectionSteps({ + labwareDefs, + protocolData, +}: GetLPCStepsParams): CheckPositionsStep[] { const { pipettes, commands, labware, modules = [] } = protocolData const labwareLocationCombos = getLabwareLocationCombos( commands, labware, modules ) - const labwareDefinitions = getLabwareDefinitionsFromCommands(commands) const labwareLocations = labwareLocationCombos.reduce( (acc, labwareLocationCombo) => { - const labwareDef = labwareDefinitions.find( + const labwareDef = labwareDefs.find( def => getLabwareDefURI(def) === labwareLocationCombo.definitionUri ) if ( diff --git a/app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/index.ts b/app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/index.ts index 606feda13ce..1398fd3514b 100644 --- a/app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/index.ts +++ b/app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/index.ts @@ -1,18 +1,26 @@ import { getProbeBasedLPCSteps } from './getProbeBasedLPCSteps' -import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' +import type { + CompletedProtocolAnalysis, + LabwareDefinition2, +} from '@opentrons/shared-data' import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' -export function getLPCSteps( +export interface GetLPCStepsParams { protocolData: CompletedProtocolAnalysis + labwareDefs: LabwareDefinition2[] +} + +export function getLPCSteps( + params: GetLPCStepsParams ): LabwarePositionCheckStep[] { - if ('pipettes' in protocolData) { - if (protocolData.pipettes.length === 0) { + if ('pipettes' in params.protocolData) { + if (params.protocolData.pipettes.length === 0) { throw new Error( 'no pipettes loaded within protocol, labware position check cannot be performed' ) } else { - return getProbeBasedLPCSteps(protocolData) + return getProbeBasedLPCSteps(params) } } else { console.error('expected pipettes to be in protocol data') diff --git a/app/src/organisms/LabwarePositionCheck/utils/index.ts b/app/src/organisms/LabwarePositionCheck/utils/index.ts index 47804106c25..ffb5ad25acd 100644 --- a/app/src/organisms/LabwarePositionCheck/utils/index.ts +++ b/app/src/organisms/LabwarePositionCheck/utils/index.ts @@ -1,2 +1,2 @@ -export * from './labware' +export * from './getItemLabwareDef' export * from './getLPCSteps' diff --git a/app/src/organisms/LabwarePositionCheck/utils/labware.ts b/app/src/organisms/LabwarePositionCheck/utils/labware.ts deleted file mode 100644 index fb7227f157a..00000000000 --- a/app/src/organisms/LabwarePositionCheck/utils/labware.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { getLabwareDefURI } from '@opentrons/shared-data' - -import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' - -import type { - CompletedProtocolAnalysis, - LabwareDefinition2, -} from '@opentrons/shared-data' - -// TOME TODO: Definitely understand how this works and see if it's necessary/can be simplified. -export function getLabwareDef( - labwareId: string, - protocolData: CompletedProtocolAnalysis -): LabwareDefinition2 | undefined { - const labwareDefUri = protocolData.labware.find(l => l.id === labwareId) - ?.definitionUri - const labwareDefinitions = getLabwareDefinitionsFromCommands( - protocolData.commands - ) - return labwareDefinitions.find(def => getLabwareDefURI(def) === labwareDefUri) -} From a9deeb4239b0874978db3281317c16a9471a5981 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Tue, 7 Jan 2025 15:53:10 -0500 Subject: [PATCH 14/33] refactor(app): inject all commands into the presentation layer About half the commands were injected into the presentation and half of them were not, so now we inject all of them. All commands are injected via a singular hook, useLPCCommands. There's a good bit of cleanup still left to do, but this commit is the first step. --- .../LabwarePositionCheck/LPCErrorModal.tsx | 4 +- .../LabwarePositionCheck/LPCWizardFlex.tsx | 188 ++-------- .../LabwarePositionCheck/hooks/index.ts | 1 + .../hooks/useLPCCommands/helpers.ts | 96 +++++ .../hooks/useLPCCommands/index.ts | 106 ++++++ .../hooks/useLPCCommands/types.ts | 13 + .../useLPCCommands/useApplyLPCOffsets.ts | 34 ++ .../useHandleConditionalCleanup.ts | 66 ++++ .../useHandleConfirmLwFinalPosition.ts | 103 ++++++ .../useHandleConfirmLwModulePlacement.ts | 132 +++++++ .../hooks/useLPCCommands/useHandleJog.ts | 110 ++++++ .../useLPCCommands/useHandlePrepModules.ts | 30 ++ .../useLPCCommands/useHandleProbeCommands.ts | 133 +++++++ .../useHandleResetLwModulesOnDeck.ts | 31 ++ .../useLPCCommands/useHandleStartLPC.ts} | 24 +- .../hooks/useLPCInitialState.ts | 4 - .../shared/ExitConfirmation.tsx | 4 +- .../steps/AttachProbe.tsx | 96 ++--- .../steps/BeforeBeginning/index.tsx | 26 +- .../LabwarePositionCheck/steps/CheckItem.tsx | 334 +++--------------- .../steps/DetachProbe.tsx | 68 +--- .../steps/ResultsSummary.tsx | 6 +- .../LabwarePositionCheck/types/content.ts | 31 +- .../PipetteWizardFlows/ProbeNotAttached.tsx | 2 + 24 files changed, 1025 insertions(+), 617 deletions(-) create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/helpers.ts create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/types.ts create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConditionalCleanup.ts create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwFinalPosition.ts create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwModulePlacement.ts create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleResetLwModulesOnDeck.ts rename app/src/organisms/LabwarePositionCheck/{steps/BeforeBeginning/getPrepCommands.ts => hooks/useLPCCommands/useHandleStartLPC.ts} (86%) diff --git a/app/src/organisms/LabwarePositionCheck/LPCErrorModal.tsx b/app/src/organisms/LabwarePositionCheck/LPCErrorModal.tsx index 713789cf3bd..1b39c9c1896 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCErrorModal.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCErrorModal.tsx @@ -26,10 +26,12 @@ import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/ const SUPPORT_EMAIL = 'support@opentrons.com' export function LPCErrorModal({ - errorMessage, + commandUtils, onCloseClick, }: LPCWizardContentProps): JSX.Element { const { t } = useTranslation(['labware_position_check', 'shared', 'branded']) + const { errorMessage } = commandUtils + return ( ): JSX.Element { - const { mostRecentAnalysis, runId, onCloseClick, maintenanceRunId } = props + const { mostRecentAnalysis } = props + const [currentStepIndex, setCurrentStepIndex] = useState(0) const isOnDevice = useSelector(getIsOnDevice) - - const [errorMessage, setErrorMessage] = useState(null) - const [isApplyingOffsets, setIsApplyingOffsets] = useState(false) - - // TOME TODO: Like with ER, separate wizard and content. The wizard injects the data layer to the content layer. - - const initialState = useLPCInitialState() - const { state, dispatch } = useLPCReducer(initialState) - - const [isExiting, setIsExiting] = useState(false) - const { - createMaintenanceCommand: createSilentCommand, - } = useCreateMaintenanceCommandMutation() - const { - chainRunCommands, - isCommandMutationLoading: isCommandChainLoading, - } = useChainMaintenanceCommands() - const labwareDefs = useMemo( () => getLabwareDefinitionsFromCommands(mostRecentAnalysis.commands), [mostRecentAnalysis] ) - const { createLabwareOffset } = useCreateLabwareOffsetMutation() - const [currentStepIndex, setCurrentStepIndex] = useState(0) - const handleCleanUpAndClose = (): void => { - setIsExiting(true) - - chainRunCommands( - maintenanceRunId, - [ - { - commandType: 'retractAxis' as const, - params: { - axis: 'leftZ', - }, - }, - { - commandType: 'retractAxis' as const, - params: { - axis: 'rightZ', - }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'x' }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'y' }, - }, - { commandType: 'home' as const, params: {} }, - ], - true - ) - .then(() => { - props.onCloseClick() - }) - .catch(() => { - props.onCloseClick() - }) - } - const { - confirm: confirmExitLPC, - showConfirmation, - cancel: cancelExitLPC, - } = useConditionalConfirm(handleCleanUpAndClose, true) - + const LPCSteps = getLPCSteps({ + protocolData: mostRecentAnalysis, + labwareDefs, + }) + const totalStepCount = LPCSteps.length - 1 + const currentStep = LPCSteps?.[currentStepIndex] const proceed = (): void => { setCurrentStepIndex( currentStepIndex !== LPCSteps.length - 1 @@ -122,62 +56,13 @@ export function LPCWizardFlex( : currentStepIndex ) } - const LPCSteps = getLPCSteps({ - protocolData: mostRecentAnalysis, - labwareDefs, - }) - const totalStepCount = LPCSteps.length - 1 - const currentStep = LPCSteps?.[currentStepIndex] - const protocolHasModules = mostRecentAnalysis.modules.length > 0 + // TOME TODO: Like with ER, separate wizard and content. The wizard injects the data layer to the content layer. - const handleJog = ( - axis: Axis, - dir: Sign, - step: StepSize, - onSuccess?: (position: Coordinates | null) => void - ): void => { - const pipetteId = 'pipetteId' in currentStep ? currentStep.pipetteId : null - if (pipetteId != null) { - createSilentCommand({ - maintenanceRunId, - command: { - commandType: 'moveRelative', - params: { pipetteId, distance: step * dir, axis }, - }, - waitUntilComplete: true, - timeout: JOG_COMMAND_TIMEOUT, - }) - .then(data => { - onSuccess?.( - (data?.data?.result?.position ?? null) as Coordinates | null - ) - }) - .catch((e: Error) => { - setErrorMessage(`error issuing jog command: ${e.message}`) - }) - } else { - setErrorMessage( - `could not find pipette to jog with id: ${pipetteId ?? ''}` - ) - } - } - const chainMaintenanceRunCommands = ( - commands: CreateCommand[], - continuePastCommandFailure: boolean - ): Promise => - chainRunCommands(maintenanceRunId, commands, continuePastCommandFailure) + const initialState = useLPCInitialState() + const { state, dispatch } = useLPCReducer(initialState) - const handleApplyOffsets = (offsets: LabwareOffsetCreateData[]): void => { - setIsApplyingOffsets(true) - Promise.all(offsets.map(data => createLabwareOffset({ runId, data }))) - .then(() => { - onCloseClick() - }) - .catch((e: Error) => { - setErrorMessage(`error applying labware offsets: ${e.message}`) - }) - } + const LPCHandlerUtils = useLPCCommands({ ...props, step: currentStep }) return ( ) @@ -224,14 +98,17 @@ function LPCWizardFlexComponent(props: LPCWizardContentProps): JSX.Element { } function LPCWizardHeader({ - errorMessage, currentStepIndex, totalStepCount, - showConfirmation, - isExiting, - confirmExitLPC, + commandUtils, }: LPCWizardContentProps): JSX.Element { const { t } = useTranslation('labware_position_check') + const { + errorMessage, + showExitConfirmation, + isExiting, + confirmExitLPC, + } = commandUtils return ( } - if (props.errorMessage != null) { + if (errorMessage != null) { return } - if (props.showConfirmation) { + if (showExitConfirmation) { return } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/index.ts index f8e2d5dc434..aca4c22cc83 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/index.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/index.ts @@ -1 +1,2 @@ export * from './useLPCInitialState' +export * from './useLPCCommands' diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/helpers.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/helpers.ts new file mode 100644 index 00000000000..21033f8117c --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/helpers.ts @@ -0,0 +1,96 @@ +import { + getModuleType, + HEATERSHAKER_MODULE_TYPE, + THERMOCYCLER_MODULE_TYPE, +} from '@opentrons/shared-data' + +import type { CreateCommand } from '@opentrons/shared-data' +import type { CheckPositionsStep } from '/app/organisms/LabwarePositionCheck/types' + +export interface BuildModulePrepCommandsParams { + step: CheckPositionsStep +} + +export function buildModulePrepCommands({ + step, +}: BuildModulePrepCommandsParams): CreateCommand[] { + const { moduleId, location } = step + + const moduleType = + (moduleId != null && + 'moduleModel' in location && + location.moduleModel != null && + getModuleType(location.moduleModel)) ?? + null + + if (moduleId == null || moduleType == null) { + return [] + } else { + switch (moduleType) { + case THERMOCYCLER_MODULE_TYPE: + return [ + { + commandType: 'thermocycler/openLid', + params: { moduleId }, + }, + ] + case HEATERSHAKER_MODULE_TYPE: + return [ + { + commandType: 'heaterShaker/closeLabwareLatch', + params: { moduleId }, + }, + { + commandType: 'heaterShaker/deactivateShaker', + params: { moduleId }, + }, + { + commandType: 'heaterShaker/openLabwareLatch', + params: { moduleId }, + }, + ] + default: + return [] + } + } +} + +export interface BuildMoveLabwareOffDeckParams { + step: CheckPositionsStep +} + +export function buildMoveLabwareOffDeck({ + step, +}: BuildMoveLabwareOffDeckParams): CreateCommand[] { + const { adapterId, labwareId } = step + + return adapterId != null + ? [ + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveLabware' as const, + params: { + labwareId: adapterId, + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + ] + : [ + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + ] +} diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts new file mode 100644 index 00000000000..356d726140e --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts @@ -0,0 +1,106 @@ +import { useState } from 'react' + +import { useApplyLPCOffsets } from './useApplyLPCOffsets' +import { useHandleJog } from './useHandleJog' +import { useHandleConditionalCleanup } from './useHandleConditionalCleanup' +import { useChainMaintenanceCommands } from '/app/resources/maintenance_runs' +import { useHandleProbeCommands } from './useHandleProbeCommands' +import { useHandleStartLPC } from './useHandleStartLPC' +import { useHandlePrepModules } from './useHandlePrepModules' +import { useHandleConfirmLwModulePlacement } from './useHandleConfirmLwModulePlacement' +import { useHandleConfirmLwFinalPosition } from './useHandleConfirmLwFinalPosition' +import { useHandleResetLwModulesOnDeck } from '/app/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleResetLwModulesOnDeck' + +import type { CreateCommand } from '@opentrons/shared-data' +import type { CommandData } from '@opentrons/api-client' +import type { LPCFlowsProps } from '/app/organisms/LabwarePositionCheck' +import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' +import type { UseProbeCommandsResult } from './useHandleProbeCommands' +import type { UseHandleConditionalCleanupResult } from './useHandleConditionalCleanup' +import type { UseHandleJogResult } from './useHandleJog' +import type { UseApplyLPCOffsetsResult } from './useApplyLPCOffsets' +import type { UseHandleStartLPCResult } from './useHandleStartLPC' +import type { UseHandlePrepModulesResult } from './useHandlePrepModules' +import type { UseHandleConfirmPlacementResult } from './useHandleConfirmLwModulePlacement' +import type { UseHandleConfirmPositionResult } from './useHandleConfirmLwFinalPosition' +import type { UseHandleResetLwModulesOnDeckResult } from './useHandleResetLwModulesOnDeck' + +export interface UseLPCCommandsProps extends Omit { + step: LabwarePositionCheckStep +} + +export type UseLPCCommandsResult = UseApplyLPCOffsetsResult & + UseHandleJogResult & + UseHandleConditionalCleanupResult & + UseProbeCommandsResult & + UseHandleStartLPCResult & + UseHandlePrepModulesResult & + UseHandleConfirmPlacementResult & + UseHandleConfirmPositionResult & + UseHandleResetLwModulesOnDeckResult & { + errorMessage: string | null + isRobotMoving: boolean + } + +// Consolidates all command handlers and handler state for injection into LPC. +export function useLPCCommands( + props: UseLPCCommandsProps +): UseLPCCommandsResult { + const [errorMessage, setErrorMessage] = useState(null) + + const { + chainRunCommands, + isCommandMutationLoading: isRobotMoving, + } = useChainMaintenanceCommands() + + const chainLPCCommands = ( + commands: CreateCommand[], + continuePastCommandFailure: boolean + ): Promise => + chainRunCommands( + props.maintenanceRunId, + commands, + continuePastCommandFailure + ).catch((e: Error) => { + setErrorMessage(`Error during LPC command: ${e.message}`) + return Promise.resolve([]) + }) + + const applyLPCOffsetsUtils = useApplyLPCOffsets(props) + const handleJogUtils = useHandleJog({ ...props, setErrorMessage }) + const handleConditionalCleanupUtils = useHandleConditionalCleanup(props) + const handleProbeCommands = useHandleProbeCommands({ + ...props, + chainLPCCommands, + }) + const handleStartLPC = useHandleStartLPC({ ...props, chainLPCCommands }) + const handlePrepModules = useHandlePrepModules({ ...props, chainLPCCommands }) + const handleConfirmLwModulePlacement = useHandleConfirmLwModulePlacement({ + ...props, + chainLPCCommands, + setErrorMessage, + }) + const handleConfirmLwFinalPosition = useHandleConfirmLwFinalPosition({ + ...props, + chainLPCCommands, + setErrorMessage, + }) + const handleResetLwModulesOnDeck = useHandleResetLwModulesOnDeck({ + ...props, + chainLPCCommands, + }) + + return { + errorMessage, + isRobotMoving, + ...applyLPCOffsetsUtils, + ...handleJogUtils, + ...handleConditionalCleanupUtils, + ...handleProbeCommands, + ...handleStartLPC, + ...handlePrepModules, + ...handleConfirmLwModulePlacement, + ...handleConfirmLwFinalPosition, + ...handleResetLwModulesOnDeck, + } +} diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/types.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/types.ts new file mode 100644 index 00000000000..e9907553f66 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/types.ts @@ -0,0 +1,13 @@ +import type { CreateCommand } from '@opentrons/shared-data' +import type { CommandData } from '@opentrons/api-client' +import type { UseLPCCommandsProps } from '.' + +export interface UseLPCCommandChildProps extends UseLPCCommandsProps {} + +export interface UseLPCCommandWithChainRunChildProps + extends UseLPCCommandChildProps { + chainLPCCommands: ( + commands: CreateCommand[], + continuePastCommandFailure: boolean + ) => Promise +} diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts new file mode 100644 index 00000000000..4c39061ef19 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts @@ -0,0 +1,34 @@ +import { useState } from 'react' + +import { useCreateLabwareOffsetMutation } from '@opentrons/react-api-client' + +import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { UseLPCCommandChildProps } from './types' + +export interface UseApplyLPCOffsetsResult { + handleApplyOffsets: (offsets: LabwareOffsetCreateData[]) => void + isApplyingOffsets: boolean +} + +export function useApplyLPCOffsets({ + onCloseClick, + runId, +}: UseLPCCommandChildProps): UseApplyLPCOffsetsResult { + const [isApplyingOffsets, setIsApplyingOffsets] = useState(false) + + const { createLabwareOffset } = useCreateLabwareOffsetMutation() + + const handleApplyOffsets = (offsets: LabwareOffsetCreateData[]): void => { + setIsApplyingOffsets(true) + Promise.all(offsets.map(data => createLabwareOffset({ runId, data }))) + .then(() => { + onCloseClick() + setIsApplyingOffsets(false) + }) + .catch((e: Error) => { + throw new Error(`error applying labware offsets: ${e.message}`) + }) + } + + return { isApplyingOffsets, handleApplyOffsets } +} diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConditionalCleanup.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConditionalCleanup.ts new file mode 100644 index 00000000000..20dc19ff947 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConditionalCleanup.ts @@ -0,0 +1,66 @@ +import { useState } from 'react' +import { useConditionalConfirm } from '@opentrons/components' +import { useChainMaintenanceCommands } from '/app/resources/maintenance_runs' + +import type { UseLPCCommandChildProps } from './types' + +export interface UseHandleConditionalCleanupResult { + isExiting: boolean + showExitConfirmation: boolean + confirmExitLPC: () => void + cancelExitLPC: () => void +} + +// TOME TODO: Pull out all the commands into their own file, since there is a good +// bit of redundancy. + +export function useHandleConditionalCleanup({ + onCloseClick, + maintenanceRunId, +}: UseLPCCommandChildProps): UseHandleConditionalCleanupResult { + const [isExiting, setIsExiting] = useState(false) + + const { chainRunCommands } = useChainMaintenanceCommands() + + const handleCleanUpAndClose = (): void => { + setIsExiting(true) + + void chainRunCommands( + maintenanceRunId, + [ + { + commandType: 'retractAxis' as const, + params: { + axis: 'leftZ', + }, + }, + { + commandType: 'retractAxis' as const, + params: { + axis: 'rightZ', + }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'x' }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'y' }, + }, + { commandType: 'home' as const, params: {} }, + ], + true + ).finally(() => { + onCloseClick() + }) + } + + const { + confirm: confirmExitLPC, + showConfirmation: showExitConfirmation, + cancel: cancelExitLPC, + } = useConditionalConfirm(handleCleanUpAndClose, true) + + return { isExiting, confirmExitLPC, cancelExitLPC, showExitConfirmation } +} diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwFinalPosition.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwFinalPosition.ts new file mode 100644 index 00000000000..a035563f2f7 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwFinalPosition.ts @@ -0,0 +1,103 @@ +import { getModuleType, HEATERSHAKER_MODULE_TYPE } from '@opentrons/shared-data' +import { buildMoveLabwareOffDeck } from './helpers' + +import type { + CreateCommand, + LoadedPipette, + Coordinates, +} from '@opentrons/shared-data' +import type { UseLPCCommandWithChainRunChildProps } from './types' +import type { BuildMoveLabwareOffDeckParams } from './helpers' + +interface UseHandleConfirmPositionProps + extends UseLPCCommandWithChainRunChildProps { + setErrorMessage: (msg: string | null) => void +} + +export interface UseHandleConfirmPositionResult { + /* Initiate commands to return specific modules to a post-run condition before + * non-plunger homing the utilized pipette and saving the LPC position. */ + handleConfirmLwFinalPosition: ( + params: BuildMoveLabwareOffDeckParams & { + onSuccess: () => void + pipette: LoadedPipette | undefined + } + ) => Promise +} + +export function useHandleConfirmLwFinalPosition({ + setErrorMessage, + chainLPCCommands, +}: UseHandleConfirmPositionProps): UseHandleConfirmPositionResult { + const handleConfirmLwFinalPosition = ( + params: BuildMoveLabwareOffDeckParams & { + onSuccess: () => void + pipette: LoadedPipette | undefined + } + ): Promise => { + const { onSuccess, pipette, step } = params + const { moduleId, pipetteId, location } = step + + const moduleType = + (moduleId != null && + 'moduleModel' in location && + location.moduleModel != null && + getModuleType(location.moduleModel)) ?? + null + const pipetteZMotorAxis: 'leftZ' | 'rightZ' = + pipette?.mount === 'left' ? 'leftZ' : 'rightZ' + + const heaterShakerPrepCommands: CreateCommand[] = + moduleId != null && + moduleType != null && + moduleType === HEATERSHAKER_MODULE_TYPE + ? [ + { + commandType: 'heaterShaker/openLabwareLatch', + params: { moduleId }, + }, + ] + : [] + const confirmPositionCommands: CreateCommand[] = [ + { + commandType: 'retractAxis' as const, + params: { + axis: pipetteZMotorAxis, + }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'x' }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'y' }, + }, + ...heaterShakerPrepCommands, + ...buildMoveLabwareOffDeck(params), + ] + + return chainLPCCommands( + [ + { commandType: 'savePosition', params: { pipetteId } }, + ...confirmPositionCommands, + ], + false + ).then(responses => { + const firstResponse = responses[0] + if (firstResponse.data.commandType === 'savePosition') { + const { position } = firstResponse.data?.result ?? { position: null } + onSuccess() + + return Promise.resolve(position) + } else { + setErrorMessage('CheckItem failed to save final position with message') + return Promise.reject( + new Error('CheckItem failed to save final position with message') + ) + } + }) + } + + return { handleConfirmLwFinalPosition } +} diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwModulePlacement.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwModulePlacement.ts new file mode 100644 index 00000000000..0da1959eaaa --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwModulePlacement.ts @@ -0,0 +1,132 @@ +import { getModuleType, HEATERSHAKER_MODULE_TYPE } from '@opentrons/shared-data' + +import type { + CreateCommand, + MoveLabwareCreateCommand, + Coordinates, +} from '@opentrons/shared-data' +import type { UseLPCCommandWithChainRunChildProps } from './types' +import type { CheckPositionsStep } from '/app/organisms/LabwarePositionCheck/types' + +const PROBE_LENGTH_MM = 44.5 + +export interface UseHandleConfirmPlacementProps + extends UseLPCCommandWithChainRunChildProps { + setErrorMessage: (msg: string | null) => void +} + +export interface UseHandleConfirmPlacementResult { + /* Initiate commands to finalize pre-protocol run conditions for specific modules + before moving the pipette to the initial LPC position. */ + handleConfirmLwModulePlacement: ( + params: BuildMoveLabwareCommandParams + ) => Promise +} + +export function useHandleConfirmLwModulePlacement({ + chainLPCCommands, + mostRecentAnalysis, + setErrorMessage, +}: UseHandleConfirmPlacementProps): UseHandleConfirmPlacementResult { + const handleConfirmLwModulePlacement = ( + params: BuildMoveLabwareCommandParams + ): Promise => { + const { pipetteId, labwareId } = params.step + + return chainLPCCommands( + [ + ...buildMoveLabwareCommand(params), + ...mostRecentAnalysis.modules.reduce((acc, mod) => { + if (getModuleType(mod.model) === HEATERSHAKER_MODULE_TYPE) { + return [ + ...acc, + { + commandType: 'heaterShaker/closeLabwareLatch', + params: { moduleId: mod.id }, + }, + ] + } + return acc + }, []), + { + commandType: 'moveToWell' as const, + params: { + pipetteId, + labwareId, + wellName: 'A1', + wellLocation: { + origin: 'top' as const, + offset: { x: 0, y: 0, z: PROBE_LENGTH_MM }, + }, + }, + }, + { commandType: 'savePosition', params: { pipetteId } }, + ], + false + ).then(responses => { + const finalResponse = responses[responses.length - 1] + if (finalResponse.data.commandType === 'savePosition') { + const { position } = finalResponse.data.result ?? { position: null } + + return Promise.resolve(position) + } else { + setErrorMessage( + 'CheckItem failed to save position for initial placement.' + ) + return Promise.reject( + new Error('CheckItem failed to save position for initial placement.') + ) + } + }) + } + + return { handleConfirmLwModulePlacement } +} + +interface BuildMoveLabwareCommandParams { + step: CheckPositionsStep +} + +function buildMoveLabwareCommand({ + step, +}: BuildMoveLabwareCommandParams): MoveLabwareCreateCommand[] { + const { labwareId, moduleId, adapterId, location } = step + + const newLocation = + moduleId != null ? { moduleId } : { slotName: location.slotName } + + if (adapterId != null) { + return [ + { + commandType: 'moveLabware' as const, + params: { + labwareId: adapterId, + newLocation, + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation: + adapterId != null + ? { labwareId: adapterId } + : { slotName: location.slotName }, + strategy: 'manualMoveWithoutPause', + }, + }, + ] + } else { + return [ + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation, + strategy: 'manualMoveWithoutPause', + }, + }, + ] + } +} diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts new file mode 100644 index 00000000000..fa4dc71b85c --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts @@ -0,0 +1,110 @@ +import { useCallback, useEffect, useState } from 'react' + +import { useCreateMaintenanceCommandMutation } from '@opentrons/react-api-client' + +import type { Coordinates } from '@opentrons/shared-data' +import type { + Axis, + Jog, + Sign, + StepSize, +} from '/app/molecules/JogControls/types' + +import type { UseLPCCommandChildProps } from './types' + +const JOG_COMMAND_TIMEOUT_MS = 10000 +const MAX_QUEUED_JOGS = 3 + +interface UseHandleJogProps extends UseLPCCommandChildProps { + setErrorMessage: (msg: string | null) => void +} + +export interface UseHandleJogResult { + handleJog: Jog +} + +// TODO(jh, 01-06-25): This debounced jog logic is used elsewhere in the app, ex, Drop tip wizard. We should consolidate it. +export function useHandleJog({ + maintenanceRunId, + step: currentStep, + setErrorMessage, +}: UseHandleJogProps): UseHandleJogResult { + const [isJogging, setIsJogging] = useState(false) + const [jogQueue, setJogQueue] = useState Promise>>([]) + const { + createMaintenanceCommand: createSilentCommand, + } = useCreateMaintenanceCommandMutation() + + const executeJog = useCallback( + ( + axis: Axis, + dir: Sign, + step: StepSize, + onSuccess?: (position: Coordinates | null) => void + ): Promise => { + return new Promise(() => { + const pipetteId = + 'pipetteId' in currentStep ? currentStep.pipetteId : null + + if (pipetteId != null) { + createSilentCommand({ + maintenanceRunId, + command: { + commandType: 'moveRelative', + params: { pipetteId, distance: step * dir, axis }, + }, + waitUntilComplete: true, + timeout: JOG_COMMAND_TIMEOUT_MS, + }) + .then(data => { + onSuccess?.( + (data?.data?.result?.position ?? null) as Coordinates | null + ) + }) + .catch((e: Error) => { + setErrorMessage(`Error issuing jog command: ${e.message}`) + }) + } else { + setErrorMessage( + `Could not find pipette to jog with id: ${pipetteId ?? ''}` + ) + } + }) + }, + [currentStep, maintenanceRunId] + ) + + const processJogQueue = useCallback((): void => { + if (jogQueue.length > 0 && !isJogging) { + setIsJogging(true) + const nextJog = jogQueue[0] + setJogQueue(prevQueue => prevQueue.slice(1)) + void nextJog().finally(() => { + setIsJogging(false) + }) + } + }, [jogQueue, isJogging]) + + useEffect(() => { + processJogQueue() + }, [processJogQueue, jogQueue.length, isJogging]) + + const handleJog = useCallback( + ( + axis: Axis, + dir: Sign, + step: StepSize, + onSuccess?: (position: Coordinates | null) => void + ): void => { + setJogQueue(prevQueue => { + if (prevQueue.length < MAX_QUEUED_JOGS) { + return [...prevQueue, () => executeJog(axis, dir, step, onSuccess)] + } + return prevQueue + }) + }, + [executeJog] + ) + + return { handleJog } +} diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts new file mode 100644 index 00000000000..b547dabd76e --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts @@ -0,0 +1,30 @@ +import { buildModulePrepCommands } from './helpers' + +import type { VectorOffset } from '@opentrons/api-client' +import type { UseLPCCommandWithChainRunChildProps } from './types' +import type { BuildModulePrepCommandsParams } from './helpers' + +interface HandlePrepModulesParams extends BuildModulePrepCommandsParams { + initialPosition: VectorOffset | undefined | null +} + +export interface UseHandlePrepModulesResult { + handlePrepModules: (params: HandlePrepModulesParams) => void +} + +export function useHandlePrepModules({ + chainLPCCommands, +}: UseLPCCommandWithChainRunChildProps): UseHandlePrepModulesResult { + const handlePrepModules = ({ + initialPosition, + ...rest + }: HandlePrepModulesParams): void => { + const prepCmds = buildModulePrepCommands(rest) + + if (initialPosition == null && prepCmds.length > 0) { + void chainLPCCommands(prepCmds, false) + } + } + + return { handlePrepModules } +} diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts new file mode 100644 index 00000000000..e446c175275 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts @@ -0,0 +1,133 @@ +import type { CreateCommand, LoadedPipette } from '@opentrons/shared-data' +import type { UseLPCCommandWithChainRunChildProps } from './types' +import { useState } from 'react' + +export interface UseProbeCommandsResult { + moveToMaintenancePosition: (pipette: LoadedPipette | undefined) => void + createProbeAttachmentHandler: ( + pipetteId: string, + pipette: LoadedPipette | undefined, + onSuccess: () => void + ) => () => Promise + createProbeDetachmentHandler: ( + pipette: LoadedPipette | undefined, + onSuccess: () => void + ) => () => Promise + unableToDetect: boolean + setShowUnableToDetect: (canDetect: boolean) => void +} + +export function useHandleProbeCommands({ + chainLPCCommands, +}: UseLPCCommandWithChainRunChildProps): UseProbeCommandsResult { + const [showUnableToDetect, setShowUnableToDetect] = useState(false) + + const moveToMaintenancePosition = ( + pipette: LoadedPipette | undefined + ): void => { + const pipetteMount = pipette?.mount + + void chainLPCCommands( + [ + { + commandType: 'calibration/moveToMaintenancePosition' as const, + params: { + mount: pipetteMount ?? 'left', + }, + }, + ], + false + ) + } + + const createProbeAttachmentHandler = ( + pipetteId: string, + pipette: LoadedPipette | undefined, + onSuccess: () => void + ): (() => Promise) => { + const pipetteMount = pipette?.mount + const pipetteZMotorAxis: 'leftZ' | 'rightZ' = + pipetteMount === 'left' ? 'leftZ' : 'rightZ' + + const verifyCommands: CreateCommand[] = [ + { + commandType: 'verifyTipPresence', + params: { + pipetteId, + expectedState: 'present', + followSingularSensor: 'primary', + }, + }, + ] + const homeCommands: CreateCommand[] = [ + { commandType: 'home', params: { axes: [pipetteZMotorAxis] } }, + { + commandType: 'retractAxis' as const, + params: { + axis: pipetteZMotorAxis, + }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'x' }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'y' }, + }, + ] + + return () => + chainLPCCommands(verifyCommands, false) + .then(() => chainLPCCommands(homeCommands, false)) + .then(() => { + onSuccess() + }) + .catch(() => { + setShowUnableToDetect(true) + + // Stop propagation to prevent error screen routing. + return Promise.resolve() + }) + } + + const createProbeDetachmentHandler = ( + pipette: LoadedPipette | undefined, + onSuccess: () => void + ): (() => Promise) => { + const pipetteMount = pipette?.mount + const pipetteZMotorAxis: 'leftZ' | 'rightZ' = + pipetteMount === 'left' ? 'leftZ' : 'rightZ' + + return () => + chainLPCCommands( + [ + { + commandType: 'retractAxis' as const, + params: { + axis: pipetteZMotorAxis, + }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'x' }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'y' }, + }, + ], + false + ).then(() => { + onSuccess() + }) + } + + return { + moveToMaintenancePosition, + createProbeAttachmentHandler, + unableToDetect: showUnableToDetect, + setShowUnableToDetect, + createProbeDetachmentHandler, + } +} diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleResetLwModulesOnDeck.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleResetLwModulesOnDeck.ts new file mode 100644 index 00000000000..b7769dd2801 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleResetLwModulesOnDeck.ts @@ -0,0 +1,31 @@ +import { buildModulePrepCommands, buildMoveLabwareOffDeck } from './helpers' + +import type { UseLPCCommandWithChainRunChildProps } from './types' +import type { + BuildMoveLabwareOffDeckParams, + BuildModulePrepCommandsParams, +} from './helpers' + +export interface UseHandleResetLwModulesOnDeckResult { + handleResetLwModulesOnDeck: ( + params: BuildModulePrepCommandsParams & BuildMoveLabwareOffDeckParams + ) => Promise +} + +export function useHandleResetLwModulesOnDeck({ + chainLPCCommands, +}: UseLPCCommandWithChainRunChildProps): UseHandleResetLwModulesOnDeckResult { + const handleResetLwModulesOnDeck = ( + params: BuildModulePrepCommandsParams & BuildMoveLabwareOffDeckParams + ): Promise => + chainLPCCommands( + [ + ...buildModulePrepCommands(params), + { commandType: 'home', params: {} }, + ...buildMoveLabwareOffDeck(params), + ], + false + ).then(() => Promise.resolve()) + + return { handleResetLwModulesOnDeck } +} diff --git a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/getPrepCommands.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts similarity index 86% rename from app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/getPrepCommands.ts rename to app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts index 4e8119e4d74..d19e0081c84 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/getPrepCommands.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts @@ -16,6 +16,28 @@ import type { TCOpenLidCreateCommand, AbsorbanceReaderOpenLidCreateCommand, } from '@opentrons/shared-data' +import type { UseLPCCommandWithChainRunChildProps } from './types' + +export interface UseHandleStartLPCResult { + createStartLPCHandler: (onSuccess: () => void) => () => void +} + +export function useHandleStartLPC({ + chainLPCCommands, + mostRecentAnalysis, +}: UseLPCCommandWithChainRunChildProps): UseHandleStartLPCResult { + const createStartLPCHandler = (onSuccess: () => void): (() => void) => { + const prepCommands = getPrepCommands(mostRecentAnalysis) + + return (): void => { + void chainLPCCommands(prepCommands, false).then(() => { + onSuccess() + }) + } + } + + return { createStartLPCHandler } +} type LPCPrepCommand = | HomeCreateCommand @@ -25,7 +47,7 @@ type LPCPrepCommand = | HeaterShakerCloseLatchCreateCommand | AbsorbanceReaderOpenLidCreateCommand -export function getPrepCommands( +function getPrepCommands( protocolData: CompletedProtocolAnalysis ): LPCPrepCommand[] { // load commands come from the protocol resource diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState.ts index 3cdb9b38bd4..491f021e9db 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState.ts @@ -1,7 +1,3 @@ -// TOME TODO: I think you could reconsider naming this to something like useLPCState -// by the time you finish this. IDK yet. It might make more sense to inject the state -// and the dispatch into the wizard and have some sort of useInitialLPCState hook. - import type { LPCWizardState } from '/app/organisms/LabwarePositionCheck/redux' export function useLPCInitialState(): LPCWizardState { diff --git a/app/src/organisms/LabwarePositionCheck/shared/ExitConfirmation.tsx b/app/src/organisms/LabwarePositionCheck/shared/ExitConfirmation.tsx index e3f246210a0..e2738b4b95c 100644 --- a/app/src/organisms/LabwarePositionCheck/shared/ExitConfirmation.tsx +++ b/app/src/organisms/LabwarePositionCheck/shared/ExitConfirmation.tsx @@ -24,10 +24,10 @@ import { SmallButton } from '/app/atoms/buttons' import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' export const ExitConfirmation = ({ - confirmExitLPC, - cancelExitLPC, + commandUtils, }: LPCWizardContentProps): JSX.Element => { const { i18n, t } = useTranslation(['labware_position_check', 'shared']) + const { confirmExitLPC, cancelExitLPC } = commandUtils const isOnDevice = useSelector(getIsOnDevice) return ( diff --git a/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx b/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx index 90cdd619e55..511c027f7b7 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect } from 'react' import { Trans, useTranslation } from 'react-i18next' import { RESPONSIVENESS, @@ -16,23 +16,25 @@ import attachProbe1 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach import attachProbe8 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_8.webm' import attachProbe96 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_96.webm' -import type { CreateCommand } from '@opentrons/shared-data' import type { AttachProbeStep, LPCStepProps } from '../types' -export function AttachProbe(props: LPCStepProps): JSX.Element { +export function AttachProbe({ + step, + protocolData, + proceed, + isOnDevice, + commandUtils, +}: LPCStepProps): JSX.Element { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const { - step, - protocolData, - proceed, - chainRunCommands, + moveToMaintenancePosition, + setShowUnableToDetect, + unableToDetect, isRobotMoving, - setErrorMessage, - isOnDevice, - } = props - const { pipetteId } = step - const [showUnableToDetect, setShowUnableToDetect] = useState(false) + createProbeAttachmentHandler, + } = commandUtils + const { pipetteId } = step const pipette = protocolData.pipettes.find(p => p.id === pipetteId) const pipetteName = pipette?.pipetteName const pipetteChannels = @@ -47,81 +49,25 @@ export function AttachProbe(props: LPCStepProps): JSX.Element { probeVideoSrc = attachProbe96 } - const pipetteMount = pipette?.mount + const handleProbeAttached = createProbeAttachmentHandler( + pipetteId, + pipette, + proceed + ) useEffect(() => { // move into correct position for probe attach on mount - chainRunCommands( - [ - { - commandType: 'calibration/moveToMaintenancePosition' as const, - params: { - mount: pipetteMount ?? 'left', - }, - }, - ], - false - ).catch(error => { - setErrorMessage(error.message as string) - }) + moveToMaintenancePosition(pipette) }, []) // TOME TODO: Instead of returning null, show an error. // if (pipetteName == null || pipetteMount == null) return null - const pipetteZMotorAxis: 'leftZ' | 'rightZ' = - pipetteMount === 'left' ? 'leftZ' : 'rightZ' - - const handleProbeAttached = (): void => { - const verifyCommands: CreateCommand[] = [ - { - commandType: 'verifyTipPresence', - params: { - pipetteId, - expectedState: 'present', - followSingularSensor: 'primary', - }, - }, - ] - const homeCommands: CreateCommand[] = [ - { commandType: 'home', params: { axes: [pipetteZMotorAxis] } }, - { - commandType: 'retractAxis' as const, - params: { - axis: pipetteZMotorAxis, - }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'x' }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'y' }, - }, - ] - chainRunCommands(verifyCommands, false) - .then(() => { - chainRunCommands(homeCommands, false) - .then(() => { - proceed() - }) - .catch((e: Error) => { - setErrorMessage( - `AttachProbe failed to move to safe location after probe attach with message: ${e.message}` - ) - }) - }) - .catch((e: Error) => { - setShowUnableToDetect(true) - }) - } - if (isRobotMoving) return ( ) - else if (showUnableToDetect) + else if (unableToDetect) return ( ): JSX.Element { const isOnDevice = useSelector(getIsOnDevice) const { t, i18n } = useTranslation(['labware_position_check', 'shared']) - const handleClickStartLPC = (): void => { - const prepCommands = getPrepCommands(protocolData) - chainRunCommands(prepCommands, false) - .then(() => { - proceed() - }) - .catch((e: Error) => { - setErrorMessage( - `IntroScreen failed to issue prep commands with message: ${e.message}` - ) - }) - } + const { createStartLPCHandler, isRobotMoving } = commandUtils + + const handleStartLPC = createStartLPCHandler(proceed) + const requiredEquipmentList = [ { loadName: t('all_modules_and_labware_from_protocol', { @@ -107,10 +95,10 @@ export function BeforeBeginning({ {isOnDevice ? ( ) : ( - + {i18n.format(t('shared:get_started'), 'capitalize')} )} diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx index 39a5aedf217..897367a6bf3 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx @@ -20,10 +20,7 @@ import { getIsTiprack, getLabwareDefURI, getLabwareDisplayName, - getModuleType, - HEATERSHAKER_MODULE_TYPE, IDENTITY_VECTOR, - THERMOCYCLER_MODULE_TYPE, } from '@opentrons/shared-data' import { getItemLabwareDef } from '/app/organisms/LabwarePositionCheck/utils' import { getLabwareDisplayLocation } from '/app/local-resources/labware' @@ -35,35 +32,33 @@ import { setInitialPosition, } from '/app/organisms/LabwarePositionCheck/redux/actions' -import type { - CreateCommand, - LabwareLocation, - MoveLabwareCreateCommand, - PipetteName, -} from '@opentrons/shared-data' +import type { PipetteName } from '@opentrons/shared-data' import type { CheckPositionsStep, LPCStepProps } from '../types' -const PROBE_LENGTH_MM = 44.5 - export function CheckItem( props: LPCStepProps ): JSX.Element { const { step, protocolData, - chainRunCommands, state, dispatch, proceed, - handleJog, - isRobotMoving, existingOffsets, - setErrorMessage, labwareDefs, + commandUtils, } = props const { labwareId, pipetteId, moduleId, adapterId, location } = step - const { t } = useTranslation(['labware_position_check', 'shared']) + const { + handleJog, + handlePrepModules, + handleConfirmLwModulePlacement, + handleConfirmLwFinalPosition, + handleResetLwModulesOnDeck, + isRobotMoving, + } = commandUtils const { workingOffsets } = state + const { t } = useTranslation(['labware_position_check', 'shared']) const isOnDevice = useSelector(getIsOnDevice) const labwareDef = getItemLabwareDef({ labwareId, @@ -82,38 +77,8 @@ export function CheckItem( })?.metadata.displayName : '' - const pipetteMount = pipette?.mount const pipetteName = pipette?.pipetteName as PipetteName - let modulePrepCommands: CreateCommand[] = [] - const moduleType = - (moduleId != null && - 'moduleModel' in location && - location.moduleModel != null && - getModuleType(location.moduleModel)) ?? - null - if (moduleId != null && moduleType === THERMOCYCLER_MODULE_TYPE) { - modulePrepCommands = [ - { - commandType: 'thermocycler/openLid', - params: { moduleId }, - }, - ] - } else if (moduleId != null && moduleType === HEATERSHAKER_MODULE_TYPE) { - modulePrepCommands = [ - { - commandType: 'heaterShaker/closeLabwareLatch', - params: { moduleId }, - }, - { - commandType: 'heaterShaker/deactivateShaker', - params: { moduleId }, - }, - { - commandType: 'heaterShaker/openLabwareLatch', - params: { moduleId }, - }, - ] - } + const initialPosition = workingOffsets.find( o => o.labwareId === labwareId && @@ -122,23 +87,53 @@ export function CheckItem( )?.initialPosition useEffect(() => { - if (initialPosition == null && modulePrepCommands.length > 0) { - chainRunCommands(modulePrepCommands, false) - .then(() => {}) - .catch((e: Error) => { - setErrorMessage( - `CheckItem module prep commands failed with message: ${e?.message}` - ) - }) - } + handlePrepModules({ step, initialPosition }) }, [moduleId]) + const handleDispatchConfirmInitialPlacement = (): void => { + void handleConfirmLwModulePlacement({ step }).then(position => { + dispatch( + setInitialPosition({ + labwareId, + location, + position, + }) + ) + }) + } + + const handleDispatchConfirmFinalPlacement = (): void => { + void handleConfirmLwFinalPosition({ + step, + onSuccess: proceed, + pipette, + }).then(position => { + dispatch( + setFinalPosition({ + labwareId, + location, + position, + }) + ) + }) + } + + const handleDispatchResetLwModulesOnDeck = (): void => { + void handleResetLwModulesOnDeck({ step }).then(() => { + dispatch( + setInitialPosition({ + labwareId, + location, + position: null, + }) + ) + }) + } + // TOME TODO: Error instead of returning null. // if (pipetteName == null || labwareDef == null || pipetteMount == null) // return null - const pipetteZMotorAxis: 'leftZ' | 'rightZ' = - pipetteMount === 'left' ? 'leftZ' : 'rightZ' const isTiprack = getIsTiprack(labwareDef) const displayLocation = getLabwareDisplayLocation({ location, @@ -214,219 +209,6 @@ export function CheckItem( ) } - let newLocation: LabwareLocation - if (moduleId != null) { - newLocation = { moduleId } - } else { - newLocation = { slotName: location.slotName } - } - - let moveLabware: MoveLabwareCreateCommand[] - if (adapterId != null) { - moveLabware = [ - { - commandType: 'moveLabware' as const, - params: { - labwareId: adapterId, - newLocation, - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'moveLabware' as const, - params: { - labwareId, - newLocation: - adapterId != null - ? { labwareId: adapterId } - : { slotName: location.slotName }, - strategy: 'manualMoveWithoutPause', - }, - }, - ] - } else { - moveLabware = [ - { - commandType: 'moveLabware' as const, - params: { - labwareId, - newLocation, - strategy: 'manualMoveWithoutPause', - }, - }, - ] - } - const handleConfirmPlacement = (): void => { - chainRunCommands( - [ - ...moveLabware, - ...protocolData.modules.reduce((acc, mod) => { - if (getModuleType(mod.model) === HEATERSHAKER_MODULE_TYPE) { - return [ - ...acc, - { - commandType: 'heaterShaker/closeLabwareLatch', - params: { moduleId: mod.id }, - }, - ] - } - return acc - }, []), - { - commandType: 'moveToWell' as const, - params: { - pipetteId, - labwareId, - wellName: 'A1', - wellLocation: { - origin: 'top' as const, - offset: { x: 0, y: 0, z: PROBE_LENGTH_MM }, - }, - }, - }, - { commandType: 'savePosition', params: { pipetteId } }, - ], - false - ) - .then(responses => { - const finalResponse = responses[responses.length - 1] - if (finalResponse.data.commandType === 'savePosition') { - const { position } = finalResponse.data?.result ?? { position: null } - dispatch( - setInitialPosition({ - labwareId, - location, - position, - }) - ) - } else { - setErrorMessage( - `CheckItem failed to save position for initial placement.` - ) - } - }) - .catch((e: Error) => { - setErrorMessage( - `CheckItem failed to save position for initial placement with message: ${e.message}` - ) - }) - } - const moveLabwareOffDeck: CreateCommand[] = - adapterId != null - ? [ - { - commandType: 'moveLabware' as const, - params: { - labwareId, - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'moveLabware' as const, - params: { - labwareId: adapterId, - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - ] - : [ - { - commandType: 'moveLabware' as const, - params: { - labwareId, - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - ] - - const handleConfirmPosition = (): void => { - const heaterShakerPrepCommands: CreateCommand[] = - moduleId != null && - moduleType != null && - moduleType === HEATERSHAKER_MODULE_TYPE - ? [ - { - commandType: 'heaterShaker/openLabwareLatch', - params: { moduleId }, - }, - ] - : [] - const confirmPositionCommands: CreateCommand[] = [ - { - commandType: 'retractAxis' as const, - params: { - axis: pipetteZMotorAxis, - }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'x' }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'y' }, - }, - ...heaterShakerPrepCommands, - ...moveLabwareOffDeck, - ] - - chainRunCommands( - [ - { commandType: 'savePosition', params: { pipetteId } }, - ...confirmPositionCommands, - ], - false - ) - .then(responses => { - const firstResponse = responses[0] - if (firstResponse.data.commandType === 'savePosition') { - const { position } = firstResponse.data?.result ?? { position: null } - dispatch( - setFinalPosition({ - labwareId, - location, - position, - }) - ) - proceed() - } else { - setErrorMessage( - 'CheckItem failed to save final position with message' - ) - } - }) - .catch((e: Error) => { - setErrorMessage( - `CheckItem failed to move from final position with message: ${e.message}` - ) - }) - } - const handleGoBack = (): void => { - chainRunCommands( - [ - ...modulePrepCommands, - { commandType: 'home', params: {} }, - ...moveLabwareOffDeck, - ], - false - ) - .then(() => { - dispatch( - setInitialPosition({ - labwareId, - location, - position: null, - }) - ) - }) - .catch((e: Error) => { - setErrorMessage(`CheckItem failed to home: ${e.message}`) - }) - } - const existingOffset = getCurrentOffsetForLabwareInLocation( existingOffsets, @@ -442,7 +224,6 @@ export function CheckItem( {initialPosition != null ? ( ) : ( } labwareDef={labwareDef} - confirmPlacement={handleConfirmPlacement} + confirmPlacement={handleDispatchConfirmInitialPlacement} location={step.location} + {...props} /> )} diff --git a/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx index d741bb554e6..510a5f76988 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx @@ -19,18 +19,18 @@ import detachProbe96 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detac import type { DetachProbeStep, LPCStepProps } from '../types' -export const DetachProbe = ( - props: LPCStepProps -): JSX.Element => { +export const DetachProbe = ({ + step, + protocolData, + proceed, + commandUtils, +}: LPCStepProps): JSX.Element => { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const { - step, - protocolData, - proceed, - chainRunCommands, + moveToMaintenancePosition, + createProbeDetachmentHandler, isRobotMoving, - setErrorMessage, - } = props + } = commandUtils const pipette = protocolData.pipettes.find(p => p.id === step.pipetteId) const pipetteName = pipette?.pipetteName @@ -42,61 +42,17 @@ export const DetachProbe = ( } else if (pipetteChannels === 96) { probeVideoSrc = detachProbe96 } - const pipetteMount = pipette?.mount + + const handleProbeDetached = createProbeDetachmentHandler(pipette, proceed) useEffect(() => { // move into correct position for probe detach on mount - chainRunCommands( - [ - { - commandType: 'calibration/moveToMaintenancePosition' as const, - params: { - mount: pipetteMount ?? 'left', - }, - }, - ], - false - ).catch(error => { - setErrorMessage(error.message as string) - }) + moveToMaintenancePosition(pipette) }, []) // TOME TODO: Error instead of returning null. // if (pipetteName == null || pipetteMount == null) return null - const pipetteZMotorAxis: 'leftZ' | 'rightZ' = - pipetteMount === 'left' ? 'leftZ' : 'rightZ' - - const handleProbeDetached = (): void => { - chainRunCommands( - [ - { - commandType: 'retractAxis' as const, - params: { - axis: pipetteZMotorAxis, - }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'x' }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'y' }, - }, - ], - false - ) - .then(() => { - proceed() - }) - .catch((e: Error) => { - setErrorMessage( - `DetachProbe failed to move to safe location after probe detach with message: ${e.message}` - ) - }) - } - if (isRobotMoving) return ( diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx index 4125686bd98..7dd920ada52 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx @@ -54,11 +54,11 @@ export function ResultsSummary( const { protocolData, state, - handleApplyOffsets, existingOffsets, - isApplyingOffsets, labwareDefs, + commandUtils, } = props + const { isApplyingOffsets, handleApplyOffsets } = commandUtils const { i18n, t } = useTranslation('labware_position_check') const { workingOffsets } = state @@ -66,6 +66,8 @@ export function ResultsSummary( const isLabwareOffsetCodeSnippetsOn = useSelector( getIsLabwareOffsetCodeSnippetsOn ) + + // TOME TODO: This should be a global prop. const isOnDevice = useSelector(getIsOnDevice) // TOME: TODO: I believe this should be in a selector. diff --git a/app/src/organisms/LabwarePositionCheck/types/content.ts b/app/src/organisms/LabwarePositionCheck/types/content.ts index bbb02d4da40..21317c8f78c 100644 --- a/app/src/organisms/LabwarePositionCheck/types/content.ts +++ b/app/src/organisms/LabwarePositionCheck/types/content.ts @@ -3,50 +3,29 @@ import type { CompletedProtocolAnalysis, LabwareDefinition2, } from '@opentrons/shared-data' -import type { - LabwareOffset, - LabwareOffsetCreateData, -} from '@opentrons/api-client' -import type { Jog } from '/app/molecules/JogControls/types' -import type { useChainRunCommands } from '/app/resources/runs' +import type { LabwareOffset } from '@opentrons/api-client' import type { LPCWizardAction, LPCWizardState, } from '/app/organisms/LabwarePositionCheck/redux' import type { LabwarePositionCheckStep } from './steps' import type { LPCFlowsProps } from '/app/organisms/LabwarePositionCheck' +import type { UseLPCCommandsResult } from '/app/organisms/LabwarePositionCheck/hooks' // TOME TODO: REDUX! Pretty much all of this should be in redux or in the data layer. -export interface LPCWizardContentProps - extends Omit { +export type LPCWizardContentProps = Omit & { step: LabwarePositionCheckStep protocolName: string protocolData: CompletedProtocolAnalysis proceed: () => void dispatch: Dispatch state: LPCWizardState + // TOME TODO: Consider adding the commands state to the state state. + commandUtils: UseLPCCommandsResult currentStepIndex: number totalStepCount: number - showConfirmation: boolean - isExiting: boolean - confirmExitLPC: () => void - cancelExitLPC: () => void - chainRunCommands: ReturnType['chainRunCommands'] - errorMessage: string | null - setErrorMessage: (errorMessage: string) => void existingOffsets: LabwareOffset[] - handleJog: Jog - isRobotMoving: boolean isOnDevice: boolean - protocolHasModules: boolean - handleApplyOffsets: (offsets: LabwareOffsetCreateData[]) => void - isApplyingOffsets: boolean labwareDefs: LabwareDefinition2[] } - -export interface LabwareToOrder { - definition: LabwareDefinition2 - labwareId: string - slot: string -} diff --git a/app/src/organisms/PipetteWizardFlows/ProbeNotAttached.tsx b/app/src/organisms/PipetteWizardFlows/ProbeNotAttached.tsx index d98fde280f7..376984673d9 100644 --- a/app/src/organisms/PipetteWizardFlows/ProbeNotAttached.tsx +++ b/app/src/organisms/PipetteWizardFlows/ProbeNotAttached.tsx @@ -23,6 +23,8 @@ interface ProbeNotAttachedProps { isOnDevice: boolean } +// TODO(jh 01-07-25): This component is utilized by other flows. Let's hoist it out of PipetteWizardFlows. + export const ProbeNotAttached = ( props: ProbeNotAttachedProps ): JSX.Element | null => { From d85419a42dfcb47533373e7cabbc35622ee5677c Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Tue, 7 Jan 2025 16:36:48 -0500 Subject: [PATCH 15/33] refactor(app): remove multiple instances of isOnDevice Why do many when one does the trick? --- .../LabwarePositionCheck/steps/BeforeBeginning/index.tsx | 4 +--- .../organisms/LabwarePositionCheck/steps/CheckItem.tsx | 4 +--- .../LabwarePositionCheck/steps/ResultsSummary.tsx | 9 ++------- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx index af735ae42fa..715c901452c 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx @@ -21,9 +21,7 @@ import { import { RobotMotionLoader } from '/app/organisms/LabwarePositionCheck/shared' import { WizardRequiredEquipmentList } from '/app/molecules/WizardRequiredEquipmentList' import { getLatestCurrentOffsets } from '/app/transformations/runs' -import { getIsOnDevice } from '/app/redux/config' import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' -import { useSelector } from 'react-redux' import { TwoUpTileLayout } from './TwoUpTileLayout' import { getTopPortalEl } from '/app/App/portal' import { SmallButton } from '/app/atoms/buttons' @@ -45,8 +43,8 @@ export function BeforeBeginning({ protocolName, labwareDefs, commandUtils, + isOnDevice, }: LPCStepProps): JSX.Element { - const isOnDevice = useSelector(getIsOnDevice) const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const { createStartLPCHandler, isRobotMoving } = commandUtils diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx index 897367a6bf3..f50e4ae01ab 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx @@ -1,5 +1,4 @@ import { useEffect } from 'react' -import { useSelector } from 'react-redux' import isEqual from 'lodash/isEqual' import { Trans, useTranslation } from 'react-i18next' @@ -26,7 +25,6 @@ import { getItemLabwareDef } from '/app/organisms/LabwarePositionCheck/utils' import { getLabwareDisplayLocation } from '/app/local-resources/labware' import { UnorderedList } from '/app/molecules/UnorderedList' import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' -import { getIsOnDevice } from '/app/redux/config' import { setFinalPosition, setInitialPosition, @@ -47,6 +45,7 @@ export function CheckItem( existingOffsets, labwareDefs, commandUtils, + isOnDevice, } = props const { labwareId, pipetteId, moduleId, adapterId, location } = step const { @@ -59,7 +58,6 @@ export function CheckItem( } = commandUtils const { workingOffsets } = state const { t } = useTranslation(['labware_position_check', 'shared']) - const isOnDevice = useSelector(getIsOnDevice) const labwareDef = getItemLabwareDef({ labwareId, loadedLabware: protocolData.labware, diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx index 7dd920ada52..b9d77bb0a05 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx @@ -31,10 +31,7 @@ import { import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' import { PythonLabwareOffsetSnippet } from '/app/molecules/PythonLabwareOffsetSnippet' -import { - getIsLabwareOffsetCodeSnippetsOn, - getIsOnDevice, -} from '/app/redux/config' +import { getIsLabwareOffsetCodeSnippetsOn } from '/app/redux/config' import { SmallButton } from '/app/atoms/buttons' import { LabwareOffsetTabs } from '/app/organisms/LabwareOffsetTabs' import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' @@ -57,6 +54,7 @@ export function ResultsSummary( existingOffsets, labwareDefs, commandUtils, + isOnDevice, } = props const { isApplyingOffsets, handleApplyOffsets } = commandUtils const { i18n, t } = useTranslation('labware_position_check') @@ -67,9 +65,6 @@ export function ResultsSummary( getIsLabwareOffsetCodeSnippetsOn ) - // TOME TODO: This should be a global prop. - const isOnDevice = useSelector(getIsOnDevice) - // TOME: TODO: I believe this should be in a selector. const offsetsToApply = useMemo(() => { return workingOffsets.map( From 769986a40df5a7a6967e455f6365bfc5f6b1cd0c Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Tue, 7 Jan 2025 16:40:54 -0500 Subject: [PATCH 16/33] refactor(app): hoist RobotMotionLoader out of every step component --- .../organisms/LabwarePositionCheck/LPCWizardFlex.tsx | 9 +++++++-- .../LabwarePositionCheck/steps/AttachProbe.tsx | 12 ++++-------- .../steps/BeforeBeginning/index.tsx | 10 ++-------- .../LabwarePositionCheck/steps/CheckItem.tsx | 6 ------ .../LabwarePositionCheck/steps/DetachProbe.tsx | 7 ------- 5 files changed, 13 insertions(+), 31 deletions(-) diff --git a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx index 2012340677b..abc7252871f 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx @@ -127,10 +127,15 @@ function LPCWizardHeader({ function LPCWizardContent(props: LPCWizardContentProps): JSX.Element { const { t } = useTranslation('shared') const { step, ...restProps } = props - const { isExiting, errorMessage, showExitConfirmation } = props.commandUtils + const { + isExiting, + isRobotMoving, + errorMessage, + showExitConfirmation, + } = props.commandUtils // Handle special cases first. - if (isExiting) { + if (isExiting || isRobotMoving) { return } if (errorMessage != null) { diff --git a/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx b/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx index 511c027f7b7..ad188920daf 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx @@ -1,5 +1,7 @@ import { useEffect } from 'react' import { Trans, useTranslation } from 'react-i18next' +import { css } from 'styled-components' + import { RESPONSIVENESS, SPACING, @@ -7,9 +9,8 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { getPipetteNameSpecs } from '@opentrons/shared-data' -import { css } from 'styled-components' + import { ProbeNotAttached } from '/app/organisms/PipetteWizardFlows/ProbeNotAttached' -import { RobotMotionLoader } from '/app/organisms/LabwarePositionCheck/shared' import { GenericWizardTile } from '/app/molecules/GenericWizardTile' import attachProbe1 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_1.webm' @@ -30,7 +31,6 @@ export function AttachProbe({ moveToMaintenancePosition, setShowUnableToDetect, unableToDetect, - isRobotMoving, createProbeAttachmentHandler, } = commandUtils @@ -63,11 +63,7 @@ export function AttachProbe({ // TOME TODO: Instead of returning null, show an error. // if (pipetteName == null || pipetteMount == null) return null - if (isRobotMoving) - return ( - - ) - else if (unableToDetect) + if (unableToDetect) return ( ): JSX.Element { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) - const { createStartLPCHandler, isRobotMoving } = commandUtils + const { createStartLPCHandler } = commandUtils const handleStartLPC = createStartLPCHandler(proceed) @@ -61,12 +61,6 @@ export function BeforeBeginning({ }, ] - // TOME TODO: Render this above if possible. - if (isRobotMoving) { - return ( - - ) - } return ( - ) return ( {initialPosition != null ? ( diff --git a/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx index 510a5f76988..69c907415c6 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx @@ -10,7 +10,6 @@ import { } from '@opentrons/components' import { getPipetteNameSpecs } from '@opentrons/shared-data' -import { RobotMotionLoader } from '/app/organisms/LabwarePositionCheck/shared' import { GenericWizardTile } from '/app/molecules/GenericWizardTile' import detachProbe1 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_1.webm' @@ -29,7 +28,6 @@ export const DetachProbe = ({ const { moveToMaintenancePosition, createProbeDetachmentHandler, - isRobotMoving, } = commandUtils const pipette = protocolData.pipettes.find(p => p.id === step.pipetteId) @@ -53,11 +51,6 @@ export const DetachProbe = ({ // TOME TODO: Error instead of returning null. // if (pipetteName == null || pipetteMount == null) return null - if (isRobotMoving) - return ( - - ) - return ( Date: Wed, 8 Jan 2025 12:11:53 -0500 Subject: [PATCH 17/33] refactor(app): encapsulate robot commands The commands utilized by the LPC handlers are largely shared and should be consolidated and given structure so it's easier to reason about them, since we'll continually need to support new modules in LPC. This commit inadvertently fixes a bug in which LPC assumes certain axes were homed before they may actually be in practice. --- .../hooks/useLPCCommands/commands/gantry.ts | 5 + .../hooks/useLPCCommands/commands/index.ts | 4 + .../hooks/useLPCCommands/commands/labware.ts | 42 +++++ .../hooks/useLPCCommands/commands/modules.ts | 150 ++++++++++++++++++ .../hooks/useLPCCommands/commands/pipettes.ts | 140 ++++++++++++++++ .../hooks/useLPCCommands/helpers.ts | 96 ----------- .../useHandleConditionalCleanup.ts | 43 ++--- .../useHandleConfirmLwFinalPosition.ts | 64 ++------ .../useHandleConfirmLwModulePlacement.ts | 51 ++---- .../hooks/useLPCCommands/useHandleJog.ts | 8 +- .../useLPCCommands/useHandlePrepModules.ts | 12 +- .../useLPCCommands/useHandleProbeCommands.ts | 87 +++------- .../useHandleResetLwModulesOnDeck.ts | 27 ++-- .../hooks/useLPCCommands/useHandleStartLPC.ts | 103 ++---------- 14 files changed, 442 insertions(+), 390 deletions(-) create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/gantry.ts create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/index.ts create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/labware.ts create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/modules.ts create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/pipettes.ts delete mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/helpers.ts diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/gantry.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/gantry.ts new file mode 100644 index 00000000000..8183153008f --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/gantry.ts @@ -0,0 +1,5 @@ +import type { CreateCommand } from '@opentrons/shared-data' + +export const fullHomeCommands = (): CreateCommand[] => [ + { commandType: 'home' as const, params: {} }, +] diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/index.ts new file mode 100644 index 00000000000..0fa392d6074 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/index.ts @@ -0,0 +1,4 @@ +export * from './labware' +export * from './modules' +export * from './pipettes' +export * from './gantry' diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/labware.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/labware.ts new file mode 100644 index 00000000000..95cc6db6ccc --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/labware.ts @@ -0,0 +1,42 @@ +import type { CreateCommand } from '@opentrons/shared-data' +import type { CheckPositionsStep } from '/app/organisms/LabwarePositionCheck/types' + +export interface BuildMoveLabwareOffDeckParams { + step: CheckPositionsStep +} + +export function moveLabwareOffDeckCommands({ + step, +}: BuildMoveLabwareOffDeckParams): CreateCommand[] { + const { adapterId, labwareId } = step + + return adapterId != null + ? [ + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + { + commandType: 'moveLabware' as const, + params: { + labwareId: adapterId, + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + ] + : [ + { + commandType: 'moveLabware' as const, + params: { + labwareId, + newLocation: 'offDeck', + strategy: 'manualMoveWithoutPause', + }, + }, + ] +} diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/modules.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/modules.ts new file mode 100644 index 00000000000..6fd4f57c1d3 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/modules.ts @@ -0,0 +1,150 @@ +import { + ABSORBANCE_READER_TYPE, + getModuleType, + HEATERSHAKER_MODULE_TYPE, + THERMOCYCLER_MODULE_TYPE, +} from '@opentrons/shared-data' + +import type { CheckPositionsStep } from '/app/organisms/LabwarePositionCheck/types' +import type { + CompletedProtocolAnalysis, + CreateCommand, +} from '@opentrons/shared-data' +import type { LabwareOffsetLocation } from '@opentrons/api-client' + +export interface BuildModulePrepCommandsParams { + step: CheckPositionsStep +} + +export function modulePrepCommands({ + step, +}: BuildModulePrepCommandsParams): CreateCommand[] { + const { moduleId, location } = step + + const moduleType = + (moduleId != null && + 'moduleModel' in location && + location.moduleModel != null && + getModuleType(location.moduleModel)) ?? + null + + if (moduleId == null || moduleType == null) { + return [] + } else { + switch (moduleType) { + case THERMOCYCLER_MODULE_TYPE: + return [ + { + commandType: 'thermocycler/openLid', + params: { moduleId }, + }, + ] + case HEATERSHAKER_MODULE_TYPE: + return [ + { + commandType: 'heaterShaker/closeLabwareLatch', + params: { moduleId }, + }, + { + commandType: 'heaterShaker/deactivateShaker', + params: { moduleId }, + }, + { + commandType: 'heaterShaker/openLabwareLatch', + params: { moduleId }, + }, + ] + default: + return [] + } + } +} + +// The module initialization that must happen before the start of any LPC. This should +// include commands that open lids and place modules in a known state that makes +// each individual LPC straightforward (ex, close the latches on the HS now, so +// we can simply open the latches when prepping for an LPC involving the HS). +export const moduleInitBeforeAnyLPCCommands = ( + analysis: CompletedProtocolAnalysis +): CreateCommand[] => [ + ...thermocyclerInitCommands(analysis), + ...absorbanceReaderInitCommands(analysis), + ...heaterShakerInitCommands(analysis), +] + +// Not all modules require initialization before each labware LPC. +export const moduleInitDuringLPCCommands = ( + analysis: CompletedProtocolAnalysis +): CreateCommand[] => [...heaterShakerInitCommands(analysis)] + +// Not all modules require cleanup after each labware LPC. +export const moduleCleanupDuringLPCCommands = ( + step: CheckPositionsStep +): CreateCommand[] => { + const { moduleId, location } = step + + return [...heaterShakerCleanupCommands(moduleId, location)] +} + +const heaterShakerInitCommands = ( + analysis: CompletedProtocolAnalysis +): CreateCommand[] => { + return analysis.modules + .filter(mod => getModuleType(mod.model) === HEATERSHAKER_MODULE_TYPE) + .map(mod => ({ + commandType: 'heaterShaker/closeLabwareLatch', + params: { moduleId: mod.id }, + })) +} + +const absorbanceReaderInitCommands = ( + analysis: CompletedProtocolAnalysis +): CreateCommand[] => { + // @ts-expect-error Home command does not need params. + return analysis.modules + .filter(mod => getModuleType(mod.model) === ABSORBANCE_READER_TYPE) + .flatMap(mod => [ + { + commandType: 'home', + params: {}, + }, + { + commandType: 'absorbanceReader/openLid', + params: { moduleId: mod.id }, + }, + ]) +} + +const thermocyclerInitCommands = ( + analysis: CompletedProtocolAnalysis +): CreateCommand[] => { + return analysis.modules + .filter(mod => getModuleType(mod.model) === THERMOCYCLER_MODULE_TYPE) + .map(mod => ({ + commandType: 'thermocycler/openLid', + params: { moduleId: mod.id }, + })) +} + +const heaterShakerCleanupCommands = ( + moduleId: string | undefined, + location: LabwareOffsetLocation +): CreateCommand[] => { + const moduleType = + (moduleId != null && + 'moduleModel' in location && + location.moduleModel != null && + getModuleType(location.moduleModel)) ?? + null + + return moduleId != null && + moduleType != null && + moduleType === HEATERSHAKER_MODULE_TYPE + ? [ + { + commandType: 'heaterShaker/openLabwareLatch', + params: { moduleId }, + }, + ] + : [] +} diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/pipettes.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/pipettes.ts new file mode 100644 index 00000000000..3bf1291b149 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/pipettes.ts @@ -0,0 +1,140 @@ +import { fullHomeCommands } from './gantry' + +import type { + CreateCommand, + LoadedPipette, + MotorAxes, +} from '@opentrons/shared-data' +import type { CheckPositionsStep } from '/app/organisms/LabwarePositionCheck/types' +import type { Axis, Sign, StepSize } from '/app/molecules/JogControls/types' + +const PROBE_LENGTH_MM = 44.5 + +export const savePositionCommands = (pipetteId: string): CreateCommand[] => [ + { commandType: 'savePosition', params: { pipetteId } }, +] + +export const moveToWellCommands = ( + step: CheckPositionsStep +): CreateCommand[] => { + const { pipetteId, labwareId } = step + + return [ + { + commandType: 'moveToWell' as const, + params: { + pipetteId, + labwareId, + wellName: 'A1', + wellLocation: { + origin: 'top' as const, + offset: { x: 0, y: 0, z: PROBE_LENGTH_MM }, + }, + }, + }, + ] +} + +export const retractSafelyAndHomeCommands = (): CreateCommand[] => [ + { + commandType: 'retractAxis' as const, + params: { + axis: 'leftZ', + }, + }, + { + commandType: 'retractAxis' as const, + params: { + axis: 'rightZ', + }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'x' }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'y' }, + }, + ...fullHomeCommands(), +] + +export const retractPipetteAxesSequentiallyCommands = ( + pipette: LoadedPipette | undefined +): CreateCommand[] => { + const pipetteZMotorAxis = pipette?.mount === 'left' ? 'leftZ' : 'rightZ' + + return [ + { + commandType: 'retractAxis' as const, + params: { + axis: pipetteZMotorAxis, + }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'x' }, + }, + { + commandType: 'retractAxis' as const, + params: { axis: 'y' }, + }, + ] +} + +export interface MoveRelativeCommandParams { + pipetteId: string + axis: Axis + dir: Sign + step: StepSize +} + +export const moveRelativeCommand = ({ + pipetteId, + axis, + dir, + step, +}: MoveRelativeCommandParams): CreateCommand => ({ + commandType: 'moveRelative', + params: { pipetteId, distance: step * dir, axis }, +}) + +export const moveToMaintenancePositionCommands = ( + pipette: LoadedPipette | undefined +): CreateCommand[] => { + const pipetteMount = pipette?.mount + + return [ + { + commandType: 'calibration/moveToMaintenancePosition' as const, + params: { + mount: pipetteMount ?? 'left', + }, + }, + ] +} + +export const verifyProbeAttachmentAndHomeCommands = ( + pipetteId: string, + pipette: LoadedPipette | undefined +): CreateCommand[] => { + const pipetteMount = pipette?.mount + const pipetteZMotorAxis = pipetteMount === 'left' ? 'leftZ' : 'rightZ' + + return [ + { + commandType: 'verifyTipPresence', + params: { + pipetteId, + expectedState: 'present', + followSingularSensor: 'primary', + }, + }, + homeSelectAxesSequentiallyCommand([pipetteZMotorAxis, 'x', 'y']), + ] +} + +const homeSelectAxesSequentiallyCommand = (axes: MotorAxes): CreateCommand => ({ + commandType: 'home', + params: { axes }, +}) diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/helpers.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/helpers.ts deleted file mode 100644 index 21033f8117c..00000000000 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/helpers.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { - getModuleType, - HEATERSHAKER_MODULE_TYPE, - THERMOCYCLER_MODULE_TYPE, -} from '@opentrons/shared-data' - -import type { CreateCommand } from '@opentrons/shared-data' -import type { CheckPositionsStep } from '/app/organisms/LabwarePositionCheck/types' - -export interface BuildModulePrepCommandsParams { - step: CheckPositionsStep -} - -export function buildModulePrepCommands({ - step, -}: BuildModulePrepCommandsParams): CreateCommand[] { - const { moduleId, location } = step - - const moduleType = - (moduleId != null && - 'moduleModel' in location && - location.moduleModel != null && - getModuleType(location.moduleModel)) ?? - null - - if (moduleId == null || moduleType == null) { - return [] - } else { - switch (moduleType) { - case THERMOCYCLER_MODULE_TYPE: - return [ - { - commandType: 'thermocycler/openLid', - params: { moduleId }, - }, - ] - case HEATERSHAKER_MODULE_TYPE: - return [ - { - commandType: 'heaterShaker/closeLabwareLatch', - params: { moduleId }, - }, - { - commandType: 'heaterShaker/deactivateShaker', - params: { moduleId }, - }, - { - commandType: 'heaterShaker/openLabwareLatch', - params: { moduleId }, - }, - ] - default: - return [] - } - } -} - -export interface BuildMoveLabwareOffDeckParams { - step: CheckPositionsStep -} - -export function buildMoveLabwareOffDeck({ - step, -}: BuildMoveLabwareOffDeckParams): CreateCommand[] { - const { adapterId, labwareId } = step - - return adapterId != null - ? [ - { - commandType: 'moveLabware' as const, - params: { - labwareId, - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - { - commandType: 'moveLabware' as const, - params: { - labwareId: adapterId, - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - ] - : [ - { - commandType: 'moveLabware' as const, - params: { - labwareId, - newLocation: 'offDeck', - strategy: 'manualMoveWithoutPause', - }, - }, - ] -} diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConditionalCleanup.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConditionalCleanup.ts index 20dc19ff947..63af35de66e 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConditionalCleanup.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConditionalCleanup.ts @@ -1,8 +1,12 @@ import { useState } from 'react' + import { useConditionalConfirm } from '@opentrons/components' + import { useChainMaintenanceCommands } from '/app/resources/maintenance_runs' +import { retractSafelyAndHomeCommands } from './commands' import type { UseLPCCommandChildProps } from './types' +import type { CreateCommand } from '@opentrons/shared-data' export interface UseHandleConditionalCleanupResult { isExiting: boolean @@ -11,9 +15,6 @@ export interface UseHandleConditionalCleanupResult { cancelExitLPC: () => void } -// TOME TODO: Pull out all the commands into their own file, since there is a good -// bit of redundancy. - export function useHandleConditionalCleanup({ onCloseClick, maintenanceRunId, @@ -25,35 +26,13 @@ export function useHandleConditionalCleanup({ const handleCleanUpAndClose = (): void => { setIsExiting(true) - void chainRunCommands( - maintenanceRunId, - [ - { - commandType: 'retractAxis' as const, - params: { - axis: 'leftZ', - }, - }, - { - commandType: 'retractAxis' as const, - params: { - axis: 'rightZ', - }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'x' }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'y' }, - }, - { commandType: 'home' as const, params: {} }, - ], - true - ).finally(() => { - onCloseClick() - }) + const cleanupCommands: CreateCommand[] = [...retractSafelyAndHomeCommands()] + + void chainRunCommands(maintenanceRunId, cleanupCommands, true).finally( + () => { + onCloseClick() + } + ) } const { diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwFinalPosition.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwFinalPosition.ts index a035563f2f7..789438d2e80 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwFinalPosition.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwFinalPosition.ts @@ -1,13 +1,17 @@ -import { getModuleType, HEATERSHAKER_MODULE_TYPE } from '@opentrons/shared-data' -import { buildMoveLabwareOffDeck } from './helpers' +import { + moduleCleanupDuringLPCCommands, + moveLabwareOffDeckCommands, + retractPipetteAxesSequentiallyCommands, + savePositionCommands, +} from './commands' import type { - CreateCommand, LoadedPipette, Coordinates, + CreateCommand, } from '@opentrons/shared-data' import type { UseLPCCommandWithChainRunChildProps } from './types' -import type { BuildMoveLabwareOffDeckParams } from './helpers' +import type { BuildMoveLabwareOffDeckParams } from './commands' interface UseHandleConfirmPositionProps extends UseLPCCommandWithChainRunChildProps { @@ -36,54 +40,16 @@ export function useHandleConfirmLwFinalPosition({ } ): Promise => { const { onSuccess, pipette, step } = params - const { moduleId, pipetteId, location } = step - - const moduleType = - (moduleId != null && - 'moduleModel' in location && - location.moduleModel != null && - getModuleType(location.moduleModel)) ?? - null - const pipetteZMotorAxis: 'leftZ' | 'rightZ' = - pipette?.mount === 'left' ? 'leftZ' : 'rightZ' + const { pipetteId } = step - const heaterShakerPrepCommands: CreateCommand[] = - moduleId != null && - moduleType != null && - moduleType === HEATERSHAKER_MODULE_TYPE - ? [ - { - commandType: 'heaterShaker/openLabwareLatch', - params: { moduleId }, - }, - ] - : [] - const confirmPositionCommands: CreateCommand[] = [ - { - commandType: 'retractAxis' as const, - params: { - axis: pipetteZMotorAxis, - }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'x' }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'y' }, - }, - ...heaterShakerPrepCommands, - ...buildMoveLabwareOffDeck(params), + const confirmCommands: CreateCommand[] = [ + ...savePositionCommands(pipetteId), + ...retractPipetteAxesSequentiallyCommands(pipette), + ...moduleCleanupDuringLPCCommands(step), + ...moveLabwareOffDeckCommands(params), ] - return chainLPCCommands( - [ - { commandType: 'savePosition', params: { pipetteId } }, - ...confirmPositionCommands, - ], - false - ).then(responses => { + return chainLPCCommands(confirmCommands, false).then(responses => { const firstResponse = responses[0] if (firstResponse.data.commandType === 'savePosition') { const { position } = firstResponse.data?.result ?? { position: null } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwModulePlacement.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwModulePlacement.ts index 0da1959eaaa..eb58121b6bf 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwModulePlacement.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwModulePlacement.ts @@ -1,15 +1,17 @@ -import { getModuleType, HEATERSHAKER_MODULE_TYPE } from '@opentrons/shared-data' +import { + moduleInitDuringLPCCommands, + moveToWellCommands, + savePositionCommands, +} from './commands' import type { - CreateCommand, MoveLabwareCreateCommand, Coordinates, + CreateCommand, } from '@opentrons/shared-data' import type { UseLPCCommandWithChainRunChildProps } from './types' import type { CheckPositionsStep } from '/app/organisms/LabwarePositionCheck/types' -const PROBE_LENGTH_MM = 44.5 - export interface UseHandleConfirmPlacementProps extends UseLPCCommandWithChainRunChildProps { setErrorMessage: (msg: string | null) => void @@ -31,39 +33,16 @@ export function useHandleConfirmLwModulePlacement({ const handleConfirmLwModulePlacement = ( params: BuildMoveLabwareCommandParams ): Promise => { - const { pipetteId, labwareId } = params.step + const { pipetteId } = params.step - return chainLPCCommands( - [ - ...buildMoveLabwareCommand(params), - ...mostRecentAnalysis.modules.reduce((acc, mod) => { - if (getModuleType(mod.model) === HEATERSHAKER_MODULE_TYPE) { - return [ - ...acc, - { - commandType: 'heaterShaker/closeLabwareLatch', - params: { moduleId: mod.id }, - }, - ] - } - return acc - }, []), - { - commandType: 'moveToWell' as const, - params: { - pipetteId, - labwareId, - wellName: 'A1', - wellLocation: { - origin: 'top' as const, - offset: { x: 0, y: 0, z: PROBE_LENGTH_MM }, - }, - }, - }, - { commandType: 'savePosition', params: { pipetteId } }, - ], - false - ).then(responses => { + const confirmCommands: CreateCommand[] = [ + ...buildMoveLabwareCommand(params), + ...moduleInitDuringLPCCommands(mostRecentAnalysis), + ...moveToWellCommands(params.step), + ...savePositionCommands(pipetteId), + ] + + return chainLPCCommands(confirmCommands, false).then(responses => { const finalResponse = responses[responses.length - 1] if (finalResponse.data.commandType === 'savePosition') { const { position } = finalResponse.data.result ?? { position: null } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts index fa4dc71b85c..3f19de3d4f8 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts @@ -2,6 +2,8 @@ import { useCallback, useEffect, useState } from 'react' import { useCreateMaintenanceCommandMutation } from '@opentrons/react-api-client' +import { moveRelativeCommand } from './commands' + import type { Coordinates } from '@opentrons/shared-data' import type { Axis, @@ -9,7 +11,6 @@ import type { Sign, StepSize, } from '/app/molecules/JogControls/types' - import type { UseLPCCommandChildProps } from './types' const JOG_COMMAND_TIMEOUT_MS = 10000 @@ -49,10 +50,7 @@ export function useHandleJog({ if (pipetteId != null) { createSilentCommand({ maintenanceRunId, - command: { - commandType: 'moveRelative', - params: { pipetteId, distance: step * dir, axis }, - }, + command: moveRelativeCommand({ pipetteId, axis, dir, step }), waitUntilComplete: true, timeout: JOG_COMMAND_TIMEOUT_MS, }) diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts index b547dabd76e..417d962951b 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts @@ -1,8 +1,9 @@ -import { buildModulePrepCommands } from './helpers' +import { modulePrepCommands } from './commands' import type { VectorOffset } from '@opentrons/api-client' +import type { CreateCommand } from '@opentrons/shared-data' import type { UseLPCCommandWithChainRunChildProps } from './types' -import type { BuildModulePrepCommandsParams } from './helpers' +import type { BuildModulePrepCommandsParams } from './commands' interface HandlePrepModulesParams extends BuildModulePrepCommandsParams { initialPosition: VectorOffset | undefined | null @@ -12,6 +13,7 @@ export interface UseHandlePrepModulesResult { handlePrepModules: (params: HandlePrepModulesParams) => void } +// Prep module(s) before LPCing a specific labware involving module(s). export function useHandlePrepModules({ chainLPCCommands, }: UseLPCCommandWithChainRunChildProps): UseHandlePrepModulesResult { @@ -19,10 +21,10 @@ export function useHandlePrepModules({ initialPosition, ...rest }: HandlePrepModulesParams): void => { - const prepCmds = buildModulePrepCommands(rest) + const prepCommands: CreateCommand[] = modulePrepCommands(rest) - if (initialPosition == null && prepCmds.length > 0) { - void chainLPCCommands(prepCmds, false) + if (initialPosition == null && prepCommands.length > 0) { + void chainLPCCommands(prepCommands, false) } } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts index e446c175275..252ccee4587 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts @@ -1,6 +1,13 @@ +import { useState } from 'react' + +import { + moveToMaintenancePositionCommands, + retractPipetteAxesSequentiallyCommands, + verifyProbeAttachmentAndHomeCommands, +} from './commands' + import type { CreateCommand, LoadedPipette } from '@opentrons/shared-data' import type { UseLPCCommandWithChainRunChildProps } from './types' -import { useState } from 'react' export interface UseProbeCommandsResult { moveToMaintenancePosition: (pipette: LoadedPipette | undefined) => void @@ -25,19 +32,11 @@ export function useHandleProbeCommands({ const moveToMaintenancePosition = ( pipette: LoadedPipette | undefined ): void => { - const pipetteMount = pipette?.mount + const maintenancePositionCommands: CreateCommand[] = [ + ...moveToMaintenancePositionCommands(pipette), + ] - void chainLPCCommands( - [ - { - commandType: 'calibration/moveToMaintenancePosition' as const, - params: { - mount: pipetteMount ?? 'left', - }, - }, - ], - false - ) + void chainLPCCommands(maintenancePositionCommands, false) } const createProbeAttachmentHandler = ( @@ -45,47 +44,19 @@ export function useHandleProbeCommands({ pipette: LoadedPipette | undefined, onSuccess: () => void ): (() => Promise) => { - const pipetteMount = pipette?.mount - const pipetteZMotorAxis: 'leftZ' | 'rightZ' = - pipetteMount === 'left' ? 'leftZ' : 'rightZ' - - const verifyCommands: CreateCommand[] = [ - { - commandType: 'verifyTipPresence', - params: { - pipetteId, - expectedState: 'present', - followSingularSensor: 'primary', - }, - }, - ] - const homeCommands: CreateCommand[] = [ - { commandType: 'home', params: { axes: [pipetteZMotorAxis] } }, - { - commandType: 'retractAxis' as const, - params: { - axis: pipetteZMotorAxis, - }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'x' }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'y' }, - }, + const attachmentCommands: CreateCommand[] = [ + ...verifyProbeAttachmentAndHomeCommands(pipetteId, pipette), ] return () => - chainLPCCommands(verifyCommands, false) - .then(() => chainLPCCommands(homeCommands, false)) + chainLPCCommands(attachmentCommands, false) .then(() => { onSuccess() }) .catch(() => { setShowUnableToDetect(true) + // TOME TODO: You probably want to hoist this component out of the step. // Stop propagation to prevent error screen routing. return Promise.resolve() }) @@ -95,30 +66,12 @@ export function useHandleProbeCommands({ pipette: LoadedPipette | undefined, onSuccess: () => void ): (() => Promise) => { - const pipetteMount = pipette?.mount - const pipetteZMotorAxis: 'leftZ' | 'rightZ' = - pipetteMount === 'left' ? 'leftZ' : 'rightZ' + const detatchmentCommands: CreateCommand[] = [ + ...retractPipetteAxesSequentiallyCommands(pipette), + ] return () => - chainLPCCommands( - [ - { - commandType: 'retractAxis' as const, - params: { - axis: pipetteZMotorAxis, - }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'x' }, - }, - { - commandType: 'retractAxis' as const, - params: { axis: 'y' }, - }, - ], - false - ).then(() => { + chainLPCCommands(detatchmentCommands, false).then(() => { onSuccess() }) } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleResetLwModulesOnDeck.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleResetLwModulesOnDeck.ts index b7769dd2801..b64965e0cc9 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleResetLwModulesOnDeck.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleResetLwModulesOnDeck.ts @@ -1,10 +1,15 @@ -import { buildModulePrepCommands, buildMoveLabwareOffDeck } from './helpers' +import { + fullHomeCommands, + modulePrepCommands, + moveLabwareOffDeckCommands, +} from './commands' +import type { CreateCommand } from '@opentrons/shared-data' import type { UseLPCCommandWithChainRunChildProps } from './types' import type { BuildMoveLabwareOffDeckParams, BuildModulePrepCommandsParams, -} from './helpers' +} from './commands' export interface UseHandleResetLwModulesOnDeckResult { handleResetLwModulesOnDeck: ( @@ -17,15 +22,15 @@ export function useHandleResetLwModulesOnDeck({ }: UseLPCCommandWithChainRunChildProps): UseHandleResetLwModulesOnDeckResult { const handleResetLwModulesOnDeck = ( params: BuildModulePrepCommandsParams & BuildMoveLabwareOffDeckParams - ): Promise => - chainLPCCommands( - [ - ...buildModulePrepCommands(params), - { commandType: 'home', params: {} }, - ...buildMoveLabwareOffDeck(params), - ], - false - ).then(() => Promise.resolve()) + ): Promise => { + const resetCommands: CreateCommand[] = [ + ...modulePrepCommands(params), + ...fullHomeCommands(), + ...moveLabwareOffDeckCommands(params as BuildMoveLabwareOffDeckParams), + ] + + return chainLPCCommands(resetCommands, false).then(() => Promise.resolve()) + } return { handleResetLwModulesOnDeck } } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts index d19e0081c84..6bcc47fdaa0 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts @@ -1,20 +1,10 @@ -import { - getModuleType, - HEATERSHAKER_MODULE_TYPE, - THERMOCYCLER_MODULE_TYPE, - ABSORBANCE_READER_TYPE, -} from '@opentrons/shared-data' +import { fullHomeCommands, moduleInitBeforeAnyLPCCommands } from './commands' import type { CompletedProtocolAnalysis, CreateCommand, - HeaterShakerCloseLatchCreateCommand, - HeaterShakerDeactivateShakerCreateCommand, - HomeCreateCommand, RunTimeCommand, SetupRunTimeCommand, - TCOpenLidCreateCommand, - AbsorbanceReaderOpenLidCreateCommand, } from '@opentrons/shared-data' import type { UseLPCCommandWithChainRunChildProps } from './types' @@ -27,10 +17,14 @@ export function useHandleStartLPC({ mostRecentAnalysis, }: UseLPCCommandWithChainRunChildProps): UseHandleStartLPCResult { const createStartLPCHandler = (onSuccess: () => void): (() => void) => { - const prepCommands = getPrepCommands(mostRecentAnalysis) + const startCommands: CreateCommand[] = [ + ...buildInstrumentLabwarePrepCommands(mostRecentAnalysis), + ...moduleInitBeforeAnyLPCCommands(mostRecentAnalysis), + ...fullHomeCommands(), + ] return (): void => { - void chainLPCCommands(prepCommands, false).then(() => { + void chainLPCCommands(startCommands, false).then(() => { onSuccess() }) } @@ -39,22 +33,16 @@ export function useHandleStartLPC({ return { createStartLPCHandler } } -type LPCPrepCommand = - | HomeCreateCommand - | SetupRunTimeCommand - | TCOpenLidCreateCommand - | HeaterShakerDeactivateShakerCreateCommand - | HeaterShakerCloseLatchCreateCommand - | AbsorbanceReaderOpenLidCreateCommand - -function getPrepCommands( +// Load all pipettes and labware into the maintenance run by utilizing the protocol resource. +// Labware is loaded off-deck so that LPC can move them on individually later. +// Next, emit module-specific setup commands to prepare for LPC. +function buildInstrumentLabwarePrepCommands( protocolData: CompletedProtocolAnalysis -): LPCPrepCommand[] { - // load commands come from the protocol resource - const loadCommands: LPCPrepCommand[] = +): SetupRunTimeCommand[] { + return ( protocolData.commands .filter(isLoadCommand) - .reduce((acc, command) => { + .reduce((acc, command) => { if ( command.commandType === 'loadPipette' && command.result?.pipetteId != null @@ -72,7 +60,6 @@ function getPrepCommands( command.commandType === 'loadLabware' && command.result?.labwareId != null ) { - // load all labware off-deck so that LPC can move them on individually later return [ ...acc, { @@ -105,69 +92,7 @@ function getPrepCommands( } return [...acc, command] }, []) ?? [] - - const TCCommands = protocolData.modules.reduce( - (acc, module) => { - if (getModuleType(module.model) === THERMOCYCLER_MODULE_TYPE) { - return [ - ...acc, - { - commandType: 'thermocycler/openLid', - params: { moduleId: module.id }, - }, - ] - } - return acc - }, - [] - ) - - const AbsorbanceCommands = protocolData.modules.reduce( - (acc, module) => { - if (getModuleType(module.model) === ABSORBANCE_READER_TYPE) { - return [ - ...acc, - { - commandType: 'home', - params: {}, - }, - { - commandType: 'absorbanceReader/openLid', - params: { moduleId: module.id }, - }, - ] - } - return acc - }, - [] ) - - const HSCommands = protocolData.modules.reduce< - HeaterShakerCloseLatchCreateCommand[] - >((acc, module) => { - if (getModuleType(module.model) === HEATERSHAKER_MODULE_TYPE) { - return [ - ...acc, - { - commandType: 'heaterShaker/closeLabwareLatch', - params: { moduleId: module.id }, - }, - ] - } - return acc - }, []) - const homeCommand: HomeCreateCommand = { - commandType: 'home', - params: {}, - } - // prepCommands will be run when a user starts LPC - return [ - ...loadCommands, - ...TCCommands, - ...AbsorbanceCommands, - ...HSCommands, - homeCommand, - ] } function isLoadCommand( From 410fec59ceab13267e4df0b049f16fbb5f212407 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Wed, 8 Jan 2025 15:31:45 -0500 Subject: [PATCH 18/33] refactor(app): componentize views in LPC steps Some LPC steps contained pseudo components that really should be their own components. This helps React Fiber properly optimize renders, too. --- .../hooks/useLPCCommands/index.ts | 3 +- .../steps/AttachProbe.tsx | 110 +++--- .../steps/BeforeBeginning/ViewOffsets.tsx | 109 ++++++ .../steps/BeforeBeginning/index.tsx | 99 +---- .../LabwarePositionCheck/steps/CheckItem.tsx | 200 ++++++----- .../steps/DetachProbe.tsx | 23 +- .../steps/ResultsSummary.tsx | 339 ------------------ .../steps/ResultsSummary/OffsetTable.tsx | 149 ++++++++ .../steps/ResultsSummary/TableComponent.tsx | 29 ++ .../steps/ResultsSummary/index.tsx | 206 +++++++++++ 10 files changed, 674 insertions(+), 593 deletions(-) create mode 100644 app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/ViewOffsets.tsx delete mode 100644 app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/TableComponent.tsx create mode 100644 app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/index.tsx diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts index 356d726140e..6fdd5a51bce 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts @@ -24,8 +24,9 @@ import type { UseHandlePrepModulesResult } from './useHandlePrepModules' import type { UseHandleConfirmPlacementResult } from './useHandleConfirmLwModulePlacement' import type { UseHandleConfirmPositionResult } from './useHandleConfirmLwFinalPosition' import type { UseHandleResetLwModulesOnDeckResult } from './useHandleResetLwModulesOnDeck' +import type { LPCWizardFlexProps } from '/app/organisms/LabwarePositionCheck/LPCWizardFlex' -export interface UseLPCCommandsProps extends Omit { +export interface UseLPCCommandsProps extends LPCWizardFlexProps { step: LabwarePositionCheckStep } diff --git a/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx b/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx index ad188920daf..514f9ba52ac 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx @@ -33,37 +33,43 @@ export function AttachProbe({ unableToDetect, createProbeAttachmentHandler, } = commandUtils - const { pipetteId } = step + // TOME TODO: This pipette logic should almost certainly be in a selector, too. const pipette = protocolData.pipettes.find(p => p.id === pipetteId) - const pipetteName = pipette?.pipetteName - const pipetteChannels = - pipetteName != null ? getPipetteNameSpecs(pipetteName)?.channels ?? 1 : 1 - let probeVideoSrc = attachProbe1 - let probeLocation = '' - if (pipetteChannels === 8) { - probeLocation = t('backmost') - probeVideoSrc = attachProbe8 - } else if (pipetteChannels === 96) { - probeLocation = t('ninety_six_probe_location') - probeVideoSrc = attachProbe96 - } - const handleProbeAttached = createProbeAttachmentHandler( pipetteId, pipette, proceed ) + // Move into correct position for probe attach on mount useEffect(() => { - // move into correct position for probe attach on mount moveToMaintenancePosition(pipette) }, []) - // TOME TODO: Instead of returning null, show an error. - // if (pipetteName == null || pipetteMount == null) return null + // TOME TODO: Maybe a selector here? + const { probeLocation, probeVideoSrc } = ((): { + probeLocation: string + probeVideoSrc: string + } => { + const pipetteName = pipette?.pipetteName + const pipetteChannels = + pipetteName != null ? getPipetteNameSpecs(pipetteName)?.channels ?? 1 : 1 - if (unableToDetect) + switch (pipetteChannels) { + case 1: + return { probeLocation: '', probeVideoSrc: attachProbe1 } + case 8: + return { probeLocation: t('backmost'), probeVideoSrc: attachProbe8 } + case 96: + return { + probeLocation: t('ninety_six_probe_location'), + probeVideoSrc: attachProbe96, + } + } + })() + + if (unableToDetect) { return ( ) - - return ( - - - - } - bodyText={ - - , - }} - /> - - } - proceedButtonText={i18n.format(t('shared:continue'), 'capitalize')} - proceed={handleProbeAttached} - /> - ) + } else { + return ( + + + + } + bodyText={ + + , + }} + /> + + } + proceedButtonText={i18n.format(t('shared:continue'), 'capitalize')} + proceed={handleProbeAttached} + /> + ) + } } -export const BODY_STYLE = css` +const VIDEO_STYLE = css` + padding-top: ${SPACING.spacing4}; + width: 100%; + min-height: 18rem; +` + +const BODY_STYLE = css` ${TYPOGRAPHY.pRegular}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { diff --git a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/ViewOffsets.tsx b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/ViewOffsets.tsx new file mode 100644 index 00000000000..327871ed3b9 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/ViewOffsets.tsx @@ -0,0 +1,109 @@ +import { useState } from 'react' +import { createPortal } from 'react-dom' +import { useTranslation } from 'react-i18next' +import { css } from 'styled-components' + +import { + ALIGN_CENTER, + Box, + Btn, + COLORS, + DIRECTION_COLUMN, + Flex, + Icon, + JUSTIFY_SPACE_BETWEEN, + ModalShell, + SPACING, + LegacyStyledText, + TYPOGRAPHY, +} from '@opentrons/components' + +import { getLatestCurrentOffsets } from '/app/transformations/runs' +import { getTopPortalEl } from '/app/App/portal' +import { SmallButton } from '/app/atoms/buttons' +import { TerseOffsetTable } from '/app/organisms/TerseOffsetTable' + +import type { LabwareOffset } from '@opentrons/api-client' +import type { LabwareDefinition2 } from '@opentrons/shared-data' + +interface ViewOffsetsProps { + existingOffsets: LabwareOffset[] + labwareDefinitions: LabwareDefinition2[] +} +export function ViewOffsets(props: ViewOffsetsProps): JSX.Element { + const { existingOffsets, labwareDefinitions } = props + const { t, i18n } = useTranslation('labware_position_check') + const [showOffsetsTable, setShowOffsetsModal] = useState(false) + const latestCurrentOffsets = getLatestCurrentOffsets(existingOffsets) + return existingOffsets.length > 0 ? ( + <> + { + setShowOffsetsModal(true) + }} + css={VIEW_OFFSETS_BUTTON_STYLE} + aria-label="show labware offsets" + > + + + {i18n.format(t('view_current_offsets'), 'capitalize')} + + + {showOffsetsTable + ? createPortal( + + {i18n.format(t('labware_offset_data'), 'capitalize')} + + } + footer={ + { + setShowOffsetsModal(false) + }} + /> + } + > + + + + , + getTopPortalEl() + ) + : null} + + ) : ( + + ) +} + +const VIEW_OFFSETS_BUTTON_STYLE = css` + ${TYPOGRAPHY.pSemiBold}; + color: ${COLORS.black90}; + font-size: ${TYPOGRAPHY.fontSize22}; + &:hover { + opacity: 100%; + } + &:active { + opacity: 70%; + } +` diff --git a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx index 0ffb655810f..6f839027c00 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx @@ -1,34 +1,18 @@ -import { useState } from 'react' -import { createPortal } from 'react-dom' import { Trans, useTranslation } from 'react-i18next' -import { css } from 'styled-components' import { - ALIGN_CENTER, - Box, - Btn, - COLORS, - DIRECTION_COLUMN, Flex, - Icon, JUSTIFY_SPACE_BETWEEN, PrimaryButton, - ModalShell, - SPACING, LegacyStyledText, - TYPOGRAPHY, } from '@opentrons/components' import { WizardRequiredEquipmentList } from '/app/molecules/WizardRequiredEquipmentList' -import { getLatestCurrentOffsets } from '/app/transformations/runs' import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' import { TwoUpTileLayout } from './TwoUpTileLayout' -import { getTopPortalEl } from '/app/App/portal' +import { ViewOffsets } from './ViewOffsets' import { SmallButton } from '/app/atoms/buttons' -import { TerseOffsetTable } from '/app/organisms/TerseOffsetTable' -import type { LabwareOffset } from '@opentrons/api-client' -import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { LPCStepProps, BeforeBeginningStep, @@ -99,84 +83,3 @@ export function BeforeBeginning({ /> ) } - -const VIEW_OFFSETS_BUTTON_STYLE = css` - ${TYPOGRAPHY.pSemiBold}; - color: ${COLORS.black90}; - font-size: ${TYPOGRAPHY.fontSize22}; - &:hover { - opacity: 100%; - } - &:active { - opacity: 70%; - } -` -interface ViewOffsetsProps { - existingOffsets: LabwareOffset[] - labwareDefinitions: LabwareDefinition2[] -} -function ViewOffsets(props: ViewOffsetsProps): JSX.Element { - const { existingOffsets, labwareDefinitions } = props - const { t, i18n } = useTranslation('labware_position_check') - const [showOffsetsTable, setShowOffsetsModal] = useState(false) - const latestCurrentOffsets = getLatestCurrentOffsets(existingOffsets) - return existingOffsets.length > 0 ? ( - <> - { - setShowOffsetsModal(true) - }} - css={VIEW_OFFSETS_BUTTON_STYLE} - aria-label="show labware offsets" - > - - - {i18n.format(t('view_current_offsets'), 'capitalize')} - - - {showOffsetsTable - ? createPortal( - - {i18n.format(t('labware_offset_data'), 'capitalize')} - - } - footer={ - { - setShowOffsetsModal(false) - }} - /> - } - > - - - - , - getTopPortalEl() - ) - : null} - - ) : ( - - ) -} diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx index f4420ff25e5..6c7cbba12ef 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx @@ -29,7 +29,7 @@ import { setInitialPosition, } from '/app/organisms/LabwarePositionCheck/redux/actions' -import type { PipetteName } from '@opentrons/shared-data' +import type { PipetteName, LabwareDefinition2 } from '@opentrons/shared-data' import type { CheckPositionsStep, LPCStepProps } from '../types' export function CheckItem( @@ -46,7 +46,7 @@ export function CheckItem( commandUtils, isOnDevice, } = props - const { labwareId, pipetteId, moduleId, adapterId, location } = step + const { labwareId, pipetteId, moduleId, location } = step const { handleJog, handlePrepModules, @@ -56,7 +56,10 @@ export function CheckItem( } = commandUtils const { workingOffsets } = state const { t } = useTranslation(['labware_position_check', 'shared']) - const labwareDef = getItemLabwareDef({ + + // TOME TODO: Pretty mcuh all of this goes into selectors. + + const itemLabwareDef = getItemLabwareDef({ labwareId, loadedLabware: protocolData.labware, labwareDefs, @@ -64,14 +67,6 @@ export function CheckItem( const pipette = protocolData.pipettes.find( pipette => pipette.id === pipetteId ) - const adapterDisplayName = - adapterId != null - ? getItemLabwareDef({ - labwareId: adapterId, - loadedLabware: protocolData.labware, - labwareDefs, - })?.metadata.displayName - : '' const pipetteName = pipette?.pipetteName as PipetteName @@ -126,20 +121,7 @@ export function CheckItem( }) } - // TOME TODO: Error instead of returning null. - // if (pipetteName == null || labwareDef == null || pipetteMount == null) - // return null - - const isTiprack = getIsTiprack(labwareDef) - const displayLocation = getLabwareDisplayLocation({ - location, - allRunDefs: labwareDefs, - detailLevel: 'full', - t, - loadedModules: protocolData.modules, - loadedLabwares: protocolData.labware, - robotType: FLEX_ROBOT_TYPE, - }) + const isLwTiprack = getIsTiprack(itemLabwareDef) const slotOnlyDisplayLocation = getLabwareDisplayLocation({ location, detailLevel: 'slot-only', @@ -149,66 +131,10 @@ export function CheckItem( robotType: FLEX_ROBOT_TYPE, }) - const labwareDisplayName = getLabwareDisplayName(labwareDef) - - let placeItemInstruction: JSX.Element = ( - - ), - }} - /> - ) - - if (isTiprack) { - placeItemInstruction = ( - - ), - }} - /> - ) - } else if (adapterId != null) { - placeItemInstruction = ( - - ), - }} - /> - ) - } - const existingOffset = getCurrentOffsetForLabwareInLocation( existingOffsets, - getLabwareDefURI(labwareDef), + getLabwareDefURI(itemLabwareDef) as string, location )?.vector ?? IDENTITY_VECTOR @@ -217,7 +143,7 @@ export function CheckItem( {initialPosition != null ? ( } - labwareDef={labwareDef} + labwareDef={itemLabwareDef} pipetteName={pipetteName} handleConfirmPosition={handleDispatchConfirmFinalPlacement} handleGoBack={handleDispatchResetLwModulesOnDeck} @@ -252,18 +178,24 @@ export function CheckItem( ) : ( , ]} /> } - labwareDef={labwareDef} + labwareDef={itemLabwareDef} confirmPlacement={handleDispatchConfirmInitialPlacement} location={step.location} {...props} @@ -272,3 +204,95 @@ export function CheckItem( ) } + +interface PlaceItemInstructionProps extends LPCStepProps { + itemLabwareDef: LabwareDefinition2 + isLwTiprack: boolean + slotOnlyDisplayLocation: string +} + +function PlaceItemInstruction({ + protocolData, + labwareDefs, + step, + itemLabwareDef, + isLwTiprack, + slotOnlyDisplayLocation, +}: PlaceItemInstructionProps): JSX.Element { + const { t } = useTranslation('labware_position_check') + const { location, adapterId } = step + const labwareDisplayName = getLabwareDisplayName(itemLabwareDef) + + const displayLocation = getLabwareDisplayLocation({ + location, + allRunDefs: labwareDefs, + detailLevel: 'full', + t, + loadedModules: protocolData.modules, + loadedLabwares: protocolData.labware, + robotType: FLEX_ROBOT_TYPE, + }) + + const adapterDisplayName = + adapterId != null + ? getItemLabwareDef({ + labwareId: adapterId, + loadedLabware: protocolData.labware, + labwareDefs, + })?.metadata.displayName + : '' + + if (isLwTiprack) { + return ( + + ), + }} + /> + ) + } else if (adapterId != null) { + return ( + + ), + }} + /> + ) + } else { + return ( + + ), + }} + /> + ) + } +} diff --git a/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx index 69c907415c6..46a6dcacd7b 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx @@ -30,6 +30,7 @@ export const DetachProbe = ({ createProbeDetachmentHandler, } = commandUtils + // TOME: TODO: Yeah, same sort of selector here. const pipette = protocolData.pipettes.find(p => p.id === step.pipetteId) const pipetteName = pipette?.pipetteName const pipetteChannels = @@ -48,24 +49,12 @@ export const DetachProbe = ({ moveToMaintenancePosition(pipette) }, []) - // TOME TODO: Error instead of returning null. - // if (pipetteName == null || pipetteMount == null) return null - return ( + } @@ -80,7 +69,13 @@ export const DetachProbe = ({ ) } -export const BODY_STYLE = css` +const VIDEO_STYLE = css` + padding-top: ${SPACING.spacing4}; + width: 100%; + min-height: 18rem; +` + +const BODY_STYLE = css` ${TYPOGRAPHY.pRegular}; @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx deleted file mode 100644 index b9d77bb0a05..00000000000 --- a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary.tsx +++ /dev/null @@ -1,339 +0,0 @@ -import { useMemo, Fragment } from 'react' -import styled, { css } from 'styled-components' -import { useSelector } from 'react-redux' -import isEqual from 'lodash/isEqual' -import { useTranslation } from 'react-i18next' - -import { - FLEX_ROBOT_TYPE, - getLabwareDefURI, - getLabwareDisplayName, - getVectorDifference, - getVectorSum, - IDENTITY_VECTOR, -} from '@opentrons/shared-data' -import { - ALIGN_CENTER, - ALIGN_FLEX_END, - BORDERS, - COLORS, - DIRECTION_COLUMN, - Flex, - Icon, - JUSTIFY_SPACE_BETWEEN, - OVERFLOW_AUTO, - PrimaryButton, - RESPONSIVENESS, - SPACING, - LegacyStyledText, - TYPOGRAPHY, -} from '@opentrons/components' - -import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' -import { PythonLabwareOffsetSnippet } from '/app/molecules/PythonLabwareOffsetSnippet' -import { getIsLabwareOffsetCodeSnippetsOn } from '/app/redux/config' -import { SmallButton } from '/app/atoms/buttons' -import { LabwareOffsetTabs } from '/app/organisms/LabwareOffsetTabs' -import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' -import { getLabwareDisplayLocation } from '/app/local-resources/labware' -import { TerseOffsetTable } from '/app/organisms/TerseOffsetTable' - -import type { LabwareDefinition2 } from '@opentrons/shared-data' -import type { LabwareOffsetCreateData } from '@opentrons/api-client' -import type { LPCStepProps, ResultsSummaryStep } from '../types' - -const LPC_HELP_LINK_URL = - 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' - -export function ResultsSummary( - props: LPCStepProps -): JSX.Element { - const { - protocolData, - state, - existingOffsets, - labwareDefs, - commandUtils, - isOnDevice, - } = props - const { isApplyingOffsets, handleApplyOffsets } = commandUtils - const { i18n, t } = useTranslation('labware_position_check') - const { workingOffsets } = state - - const isSubmittingAndClosing = isApplyingOffsets - const isLabwareOffsetCodeSnippetsOn = useSelector( - getIsLabwareOffsetCodeSnippetsOn - ) - - // TOME: TODO: I believe this should be in a selector. - const offsetsToApply = useMemo(() => { - return workingOffsets.map( - ({ initialPosition, finalPosition, labwareId, location }) => { - const definitionUri = - protocolData.labware.find(l => l.id === labwareId)?.definitionUri ?? - null - if ( - finalPosition == null || - initialPosition == null || - definitionUri == null - ) { - throw new Error( - `cannot create offset for labware with id ${labwareId}, in location ${JSON.stringify( - location - )}, with initial position ${String( - initialPosition - )}, and final position ${String(finalPosition)}` - ) - } - - const existingOffset = - getCurrentOffsetForLabwareInLocation( - existingOffsets, - definitionUri, - location - )?.vector ?? IDENTITY_VECTOR - const vector = getVectorSum( - existingOffset, - getVectorDifference(finalPosition, initialPosition) - ) - return { definitionUri, location, vector } - } - ) - }, [workingOffsets]) - - const TableComponent = isOnDevice ? ( - - ) : ( - - ) - const JupyterSnippet = ( - - ) - const CommandLineSnippet = ( - - ) - - return ( - - -
{t('new_labware_offset_data')}
- {isLabwareOffsetCodeSnippetsOn ? ( - - ) : ( - TableComponent - )} -
- {isOnDevice ? ( - { - handleApplyOffsets(offsetsToApply) - }} - buttonText={i18n.format(t('apply_offsets'), 'capitalize')} - iconName={isSubmittingAndClosing ? 'ot-spinner' : null} - iconPlacement={isSubmittingAndClosing ? 'startIcon' : null} - disabled={isSubmittingAndClosing} - /> - ) : ( - - - { - handleApplyOffsets(offsetsToApply) - }} - disabled={isSubmittingAndClosing} - > - - {isSubmittingAndClosing ? ( - - ) : null} - - {i18n.format(t('apply_offsets'), 'capitalize')} - - - - - )} -
- ) -} - -const Table = styled('table')` - ${TYPOGRAPHY.labelRegular} - table-layout: auto; - width: 100%; - border-spacing: 0 ${SPACING.spacing4}; - margin: ${SPACING.spacing16} 0; - text-align: left; -` -const TableHeader = styled('th')` - text-transform: ${TYPOGRAPHY.textTransformUppercase}; - color: ${COLORS.black90}; - font-weight: ${TYPOGRAPHY.fontWeightRegular}; - font-size: ${TYPOGRAPHY.fontSizeCaption}; - padding: ${SPACING.spacing4}; -` -const TableRow = styled('tr')` - background-color: ${COLORS.grey20}; -` - -const TableDatum = styled('td')` - padding: ${SPACING.spacing4}; - white-space: break-spaces; - text-overflow: wrap; -` - -const Header = styled.h1` - ${TYPOGRAPHY.h1Default} - - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - ${TYPOGRAPHY.level4HeaderSemiBold} - } -` - -interface OffsetTableProps extends LPCStepProps { - offsets: LabwareOffsetCreateData[] - labwareDefinitions: LabwareDefinition2[] -} - -const OffsetTable = ({ - offsets, - labwareDefinitions, - protocolData, -}: OffsetTableProps): JSX.Element => { - const { t } = useTranslation('labware_position_check') - - return ( -
- - - {t('location')} - {t('labware')} - {t('labware_offset_data')} - - - - - {offsets.map(({ location, definitionUri, vector }, index) => { - const displayLocation = getLabwareDisplayLocation({ - location, - allRunDefs: labwareDefinitions, - detailLevel: 'full', - t, - loadedModules: protocolData.modules, - loadedLabwares: protocolData.labware, - robotType: FLEX_ROBOT_TYPE, - }) - - const labwareDef = labwareDefinitions.find( - def => getLabwareDefURI(def) === definitionUri - ) - const labwareDisplayName = - labwareDef != null ? getLabwareDisplayName(labwareDef) : '' - - return ( - - - - {displayLocation} - - - - {labwareDisplayName} - - - {isEqual(vector, IDENTITY_VECTOR) ? ( - {t('no_labware_offsets')} - ) : ( - - {[vector.x, vector.y, vector.z].map((axis, index) => ( - - 0 ? SPACING.spacing8 : 0} - marginRight={SPACING.spacing4} - fontWeight={TYPOGRAPHY.fontWeightSemiBold} - > - {['X', 'Y', 'Z'][index]} - - - {axis.toFixed(1)} - - - ))} - - )} - - - ) - })} - -
- ) -} diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx new file mode 100644 index 00000000000..d30cdf359ea --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx @@ -0,0 +1,149 @@ +import { Fragment } from 'react' +import styled from 'styled-components' +import isEqual from 'lodash/isEqual' +import { useTranslation } from 'react-i18next' + +import { + FLEX_ROBOT_TYPE, + getLabwareDefURI, + getLabwareDisplayName, + IDENTITY_VECTOR, +} from '@opentrons/shared-data' +import { + BORDERS, + COLORS, + Flex, + SPACING, + LegacyStyledText, + TYPOGRAPHY, +} from '@opentrons/components' + +import { getLabwareDisplayLocation } from '/app/local-resources/labware' + +import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { + LPCStepProps, + ResultsSummaryStep, +} from '/app/organisms/LabwarePositionCheck/types' + +interface OffsetTableProps extends LPCStepProps { + offsets: LabwareOffsetCreateData[] + labwareDefinitions: LabwareDefinition2[] +} + +export function OffsetTable({ + offsets, + labwareDefinitions, + protocolData, +}: OffsetTableProps): JSX.Element { + const { t } = useTranslation('labware_position_check') + + return ( + + + + {t('location')} + {t('labware')} + {t('labware_offset_data')} + + + + + {offsets.map(({ location, definitionUri, vector }, index) => { + const displayLocation = getLabwareDisplayLocation({ + location, + allRunDefs: labwareDefinitions, + detailLevel: 'full', + t, + loadedModules: protocolData.modules, + loadedLabwares: protocolData.labware, + robotType: FLEX_ROBOT_TYPE, + }) + + const labwareDef = labwareDefinitions.find( + def => getLabwareDefURI(def) === definitionUri + ) + const labwareDisplayName = + labwareDef != null ? getLabwareDisplayName(labwareDef) : '' + + return ( + + + + {displayLocation} + + + + {labwareDisplayName} + + + {isEqual(vector, IDENTITY_VECTOR) ? ( + {t('no_labware_offsets')} + ) : ( + + {[vector.x, vector.y, vector.z].map((axis, index) => ( + + 0 ? SPACING.spacing8 : 0} + marginRight={SPACING.spacing4} + fontWeight={TYPOGRAPHY.fontWeightSemiBold} + > + {['X', 'Y', 'Z'][index]} + + + {axis.toFixed(1)} + + + ))} + + )} + + + ) + })} + +
+ ) +} + +const Table = styled('table')` + ${TYPOGRAPHY.labelRegular} + table-layout: auto; + width: 100%; + border-spacing: 0 ${SPACING.spacing4}; + margin: ${SPACING.spacing16} 0; + text-align: left; +` + +const TableHeader = styled('th')` + text-transform: ${TYPOGRAPHY.textTransformUppercase}; + color: ${COLORS.black90}; + font-weight: ${TYPOGRAPHY.fontWeightRegular}; + font-size: ${TYPOGRAPHY.fontSizeCaption}; + padding: ${SPACING.spacing4}; +` + +const TableRow = styled('tr')` + background-color: ${COLORS.grey20}; +` + +const TableDatum = styled('td')` + padding: ${SPACING.spacing4}; + white-space: break-spaces; + text-overflow: wrap; +` diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/TableComponent.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/TableComponent.tsx new file mode 100644 index 00000000000..2b870a00aa6 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/TableComponent.tsx @@ -0,0 +1,29 @@ +import { TerseOffsetTable } from '/app/organisms/TerseOffsetTable' +import { OffsetTable } from './OffsetTable' + +import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { + LPCStepProps, + ResultsSummaryStep, +} from '/app/organisms/LabwarePositionCheck/types' + +interface TableComponent extends LPCStepProps { + offsetsToApply: LabwareOffsetCreateData[] +} + +export function TableComponent(props: TableComponent): JSX.Element { + const { isOnDevice, labwareDefs, offsetsToApply } = props + + return isOnDevice ? ( + + ) : ( + + ) +} diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/index.tsx new file mode 100644 index 00000000000..ea95bc80a77 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/index.tsx @@ -0,0 +1,206 @@ +import { useMemo } from 'react' +import styled, { css } from 'styled-components' +import { useSelector } from 'react-redux' +import { useTranslation } from 'react-i18next' + +import { + getVectorDifference, + getVectorSum, + IDENTITY_VECTOR, +} from '@opentrons/shared-data' +import { + ALIGN_CENTER, + ALIGN_FLEX_END, + COLORS, + DIRECTION_COLUMN, + Flex, + Icon, + JUSTIFY_SPACE_BETWEEN, + OVERFLOW_AUTO, + PrimaryButton, + RESPONSIVENESS, + SPACING, + LegacyStyledText, + TYPOGRAPHY, +} from '@opentrons/components' + +import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' +import { PythonLabwareOffsetSnippet } from '/app/molecules/PythonLabwareOffsetSnippet' +import { getIsLabwareOffsetCodeSnippetsOn } from '/app/redux/config' +import { SmallButton } from '/app/atoms/buttons' +import { LabwareOffsetTabs } from '/app/organisms/LabwareOffsetTabs' +import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' +import { TableComponent } from './TableComponent' + +import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { + LPCStepProps, + ResultsSummaryStep, +} from '/app/organisms/LabwarePositionCheck/types' + +// TODO(jh, 01-08-25): This support link will likely need updating as a part of RPRD-173, too. +const LPC_HELP_LINK_URL = + 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' + +export function ResultsSummary( + props: LPCStepProps +): JSX.Element { + const { + protocolData, + state, + existingOffsets, + commandUtils, + isOnDevice, + } = props + const { isApplyingOffsets, handleApplyOffsets } = commandUtils + const { i18n, t } = useTranslation('labware_position_check') + const { workingOffsets } = state + const isLabwareOffsetCodeSnippetsOn = useSelector( + getIsLabwareOffsetCodeSnippetsOn + ) + + // TOME: TODO: I believe this should be in a selector. + const offsetsToApply = useMemo(() => { + return workingOffsets.map( + ({ initialPosition, finalPosition, labwareId, location }) => { + const definitionUri = + protocolData.labware.find(l => l.id === labwareId)?.definitionUri ?? + null + if ( + finalPosition == null || + initialPosition == null || + definitionUri == null + ) { + throw new Error( + `cannot create offset for labware with id ${labwareId}, in location ${JSON.stringify( + location + )}, with initial position ${String( + initialPosition + )}, and final position ${String(finalPosition)}` + ) + } + + const existingOffset = + getCurrentOffsetForLabwareInLocation( + existingOffsets, + definitionUri, + location + )?.vector ?? IDENTITY_VECTOR + const vector = getVectorSum( + existingOffset, + getVectorDifference(finalPosition, initialPosition) + ) + return { definitionUri, location, vector } + } + ) + }, [workingOffsets]) + + return ( + + +
{t('new_labware_offset_data')}
+ {isLabwareOffsetCodeSnippetsOn ? ( + + } + JupyterComponent={ + + } + CommandLineComponent={ + + } + marginTop={SPACING.spacing16} + /> + ) : ( + + )} +
+ {isOnDevice ? ( + { + handleApplyOffsets(offsetsToApply) + }} + buttonText={i18n.format(t('apply_offsets'), 'capitalize')} + iconName={isApplyingOffsets ? 'ot-spinner' : null} + iconPlacement={isApplyingOffsets ? 'startIcon' : null} + disabled={isApplyingOffsets} + /> + ) : ( + + + { + handleApplyOffsets(offsetsToApply) + }} + disabled={isApplyingOffsets} + > + + {isApplyingOffsets ? ( + + ) : null} + + {i18n.format(t('apply_offsets'), 'capitalize')} + + + + + )} +
+ ) +} + +const PARENT_CONTAINER_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + justify-content: ${JUSTIFY_SPACE_BETWEEN}; + padding: ${SPACING.spacing32}; + min-height: 29.5rem; +` + +const SHARED_CONTAINER_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + max-height: 20rem; + overflow-y: ${OVERFLOW_AUTO}; + + &::-webkit-scrollbar { + width: 0.75rem; + background-color: transparent; + } + &::-webkit-scrollbar-thumb { + background: ${COLORS.grey50}; + border-radius: 11px; + } +` + +const DESKTOP_BUTTON_STYLE = css` + width: 100%; + margin-top: ${SPACING.spacing32}; + justify-content: ${JUSTIFY_SPACE_BETWEEN}; + align-items: ${ALIGN_CENTER}; +` + +const Header = styled.h1` + ${TYPOGRAPHY.h1Default} + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + ${TYPOGRAPHY.level4HeaderSemiBold} + } +` From d88966fa6af59c8c207fe37937dd323128f3f6d5 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Wed, 8 Jan 2025 17:03:56 -0500 Subject: [PATCH 19/33] refactor(app): move all state inside redux --- .../LabwarePositionCheck/LPCWizardFlex.tsx | 77 ++++++------------- .../hooks/useLPCCommands/index.ts | 5 +- .../hooks/useLPCCommands/useHandleJog.ts | 4 +- .../hooks/useLPCInitialState.ts | 35 ++++++++- .../LabwarePositionCheck/redux/actions.ts | 8 ++ .../LabwarePositionCheck/redux/constants.ts | 1 + .../LabwarePositionCheck/redux/reducer.ts | 18 +++++ .../LabwarePositionCheck/redux/types.ts | 23 +++++- .../shared/PrepareSpace.tsx | 3 +- .../steps/AttachProbe.tsx | 8 +- .../steps/BeforeBeginning/index.tsx | 5 +- .../LabwarePositionCheck/steps/CheckItem.tsx | 11 +-- .../steps/DetachProbe.tsx | 3 +- .../steps/ResultsSummary/OffsetTable.tsx | 3 +- .../steps/ResultsSummary/TableComponent.tsx | 7 +- .../steps/ResultsSummary/index.tsx | 10 +-- .../LabwarePositionCheck/types/content.ts | 19 +---- 17 files changed, 137 insertions(+), 103 deletions(-) diff --git a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx index abc7252871f..d7cf3ccd485 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx @@ -1,6 +1,4 @@ -import { useMemo, useState } from 'react' import { createPortal } from 'react-dom' -import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { ModalShell } from '@opentrons/components' @@ -18,63 +16,38 @@ import { ExitConfirmation, } from '/app/organisms/LabwarePositionCheck/shared' import { WizardHeader } from '/app/molecules/WizardHeader' -import { getIsOnDevice } from '/app/redux/config' import { LPCErrorModal } from './LPCErrorModal' -import { getLPCSteps } from './utils' import { useLPCCommands, useLPCInitialState, } from '/app/organisms/LabwarePositionCheck/hooks' -import { useLPCReducer } from '/app/organisms/LabwarePositionCheck/redux' +import { + proceedStep, + useLPCReducer, +} from '/app/organisms/LabwarePositionCheck/redux' import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' -import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' import type { LPCFlowsProps } from '/app/organisms/LabwarePositionCheck/LPCFlows' import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' -export function LPCWizardFlex( - props: Omit -): JSX.Element { - const { mostRecentAnalysis } = props - const [currentStepIndex, setCurrentStepIndex] = useState(0) - const isOnDevice = useSelector(getIsOnDevice) - const labwareDefs = useMemo( - () => getLabwareDefinitionsFromCommands(mostRecentAnalysis.commands), - [mostRecentAnalysis] - ) - - const LPCSteps = getLPCSteps({ - protocolData: mostRecentAnalysis, - labwareDefs, - }) - const totalStepCount = LPCSteps.length - 1 - const currentStep = LPCSteps?.[currentStepIndex] - const proceed = (): void => { - setCurrentStepIndex( - currentStepIndex !== LPCSteps.length - 1 - ? currentStepIndex + 1 - : currentStepIndex - ) - } +export interface LPCWizardFlexProps extends Omit {} - // TOME TODO: Like with ER, separate wizard and content. The wizard injects the data layer to the content layer. - - const initialState = useLPCInitialState() +export function LPCWizardFlex(props: LPCWizardFlexProps): JSX.Element { + const initialState = useLPCInitialState({ ...props }) const { state, dispatch } = useLPCReducer(initialState) - const LPCHandlerUtils = useLPCCommands({ ...props, step: currentStep }) + const LPCHandlerUtils = useLPCCommands({ ...props, state }) + + // TOME TODO: Confirm Go back functionality works if we have it? + const proceed = (): void => { + dispatch(proceedStep()) + } return ( @@ -83,7 +56,7 @@ export function LPCWizardFlex( function LPCWizardFlexComponent(props: LPCWizardContentProps): JSX.Element { return createPortal( - props.isOnDevice ? ( + props.state.isOnDevice ? ( @@ -98,11 +71,11 @@ function LPCWizardFlexComponent(props: LPCWizardContentProps): JSX.Element { } function LPCWizardHeader({ - currentStepIndex, - totalStepCount, + state, commandUtils, }: LPCWizardContentProps): JSX.Element { const { t } = useTranslation('labware_position_check') + const { currentStepIndex, totalStepCount } = state.steps const { errorMessage, showExitConfirmation, @@ -126,7 +99,7 @@ function LPCWizardHeader({ function LPCWizardContent(props: LPCWizardContentProps): JSX.Element { const { t } = useTranslation('shared') - const { step, ...restProps } = props + const { current: currentStep } = props.state.steps const { isExiting, isRobotMoving, @@ -134,7 +107,7 @@ function LPCWizardContent(props: LPCWizardContentProps): JSX.Element { showExitConfirmation, } = props.commandUtils - // Handle special cases first. + // Handle special cases that are shared by multiple steps first. if (isExiting || isRobotMoving) { return } @@ -146,24 +119,24 @@ function LPCWizardContent(props: LPCWizardContentProps): JSX.Element { } // Handle step-based routing. - switch (step.section) { + switch (currentStep.section) { case NAV_STEPS.BEFORE_BEGINNING: - return + return case NAV_STEPS.CHECK_POSITIONS: - return + return case NAV_STEPS.ATTACH_PROBE: - return + return case NAV_STEPS.DETACH_PROBE: - return + return case NAV_STEPS.RESULTS_SUMMARY: - return + return default: console.error('Unhandled LPC step.') - return + return } } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts index 6fdd5a51bce..7c1fa987a1b 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts @@ -13,8 +13,6 @@ import { useHandleResetLwModulesOnDeck } from '/app/organisms/LabwarePositionChe import type { CreateCommand } from '@opentrons/shared-data' import type { CommandData } from '@opentrons/api-client' -import type { LPCFlowsProps } from '/app/organisms/LabwarePositionCheck' -import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' import type { UseProbeCommandsResult } from './useHandleProbeCommands' import type { UseHandleConditionalCleanupResult } from './useHandleConditionalCleanup' import type { UseHandleJogResult } from './useHandleJog' @@ -25,9 +23,10 @@ import type { UseHandleConfirmPlacementResult } from './useHandleConfirmLwModule import type { UseHandleConfirmPositionResult } from './useHandleConfirmLwFinalPosition' import type { UseHandleResetLwModulesOnDeckResult } from './useHandleResetLwModulesOnDeck' import type { LPCWizardFlexProps } from '/app/organisms/LabwarePositionCheck/LPCWizardFlex' +import type { LPCWizardState } from '/app/organisms/LabwarePositionCheck/redux' export interface UseLPCCommandsProps extends LPCWizardFlexProps { - step: LabwarePositionCheckStep + state: LPCWizardState } export type UseLPCCommandsResult = UseApplyLPCOffsetsResult & diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts index 3f19de3d4f8..fdfa5ac6e4a 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts @@ -27,9 +27,11 @@ export interface UseHandleJogResult { // TODO(jh, 01-06-25): This debounced jog logic is used elsewhere in the app, ex, Drop tip wizard. We should consolidate it. export function useHandleJog({ maintenanceRunId, - step: currentStep, + state, setErrorMessage, }: UseHandleJogProps): UseHandleJogResult { + const { current: currentStep } = state.steps + const [isJogging, setIsJogging] = useState(false) const [jogQueue, setJogQueue] = useState Promise>>([]) const { diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState.ts index 491f021e9db..890dc5421da 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState.ts @@ -1,8 +1,41 @@ +import { useSelector } from 'react-redux' + +import { getIsOnDevice } from '/app/redux/config' +import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' +import { getLPCSteps } from '/app/organisms/LabwarePositionCheck/utils' + +import type { RunTimeCommand } from '@opentrons/shared-data' import type { LPCWizardState } from '/app/organisms/LabwarePositionCheck/redux' +import type { LPCWizardFlexProps } from '/app/organisms/LabwarePositionCheck/LPCWizardFlex' + +export interface UseLPCInitialStateProps extends LPCWizardFlexProps {} + +export function useLPCInitialState( + props: UseLPCInitialStateProps +): LPCWizardState { + const { mostRecentAnalysis } = props + const isOnDevice = useSelector(getIsOnDevice) + + const protocolCommands: RunTimeCommand[] = mostRecentAnalysis.commands + const labwareDefs = getLabwareDefinitionsFromCommands(protocolCommands) + + const LPCSteps = getLPCSteps({ + protocolData: mostRecentAnalysis, + labwareDefs, + }) -export function useLPCInitialState(): LPCWizardState { return { + ...props, + protocolData: props.mostRecentAnalysis, + isOnDevice, + labwareDefs, workingOffsets: [], tipPickUpOffset: null, + steps: { + currentStepIndex: 0, + totalStepCount: LPCSteps.length - 1, + current: LPCSteps[0], + all: LPCSteps, + }, } } diff --git a/app/src/organisms/LabwarePositionCheck/redux/actions.ts b/app/src/organisms/LabwarePositionCheck/redux/actions.ts index 3f87b8d80fc..2bdb0b51f54 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/actions.ts +++ b/app/src/organisms/LabwarePositionCheck/redux/actions.ts @@ -1,4 +1,5 @@ import { + PROCEED_STEP, SET_INITIAL_POSITION, SET_FINAL_POSITION, SET_TIP_PICKUP_OFFSET, @@ -6,12 +7,19 @@ import { import type { Coordinates } from '@opentrons/shared-data' import type { + ProceedStepAction, InitialPositionAction, FinalPositionAction, TipPickUpOffsetAction, PositionParams, } from './types' +export const proceedStep = (): ProceedStepAction => ({ + type: PROCEED_STEP, + payload: {}, +}) + +// TOME TODO: I think you should be using this somewhere... export const setTipPickupOffset = ( offset: Coordinates | null ): TipPickUpOffsetAction => ({ diff --git a/app/src/organisms/LabwarePositionCheck/redux/constants.ts b/app/src/organisms/LabwarePositionCheck/redux/constants.ts index d7ff8c2625c..c4eb7c323f4 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/constants.ts +++ b/app/src/organisms/LabwarePositionCheck/redux/constants.ts @@ -1,3 +1,4 @@ +export const PROCEED_STEP = 'PROCEED_STEP' export const SET_INITIAL_POSITION = 'SET_INITIAL_POSITION' export const SET_FINAL_POSITION = 'SET_FINAL_POSITION' export const SET_TIP_PICKUP_OFFSET = 'SET_TIP_PICKUP_OFFSET' diff --git a/app/src/organisms/LabwarePositionCheck/redux/reducer.ts b/app/src/organisms/LabwarePositionCheck/redux/reducer.ts index ee3fa7cb77f..1a11544d65f 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/reducer.ts +++ b/app/src/organisms/LabwarePositionCheck/redux/reducer.ts @@ -1,5 +1,6 @@ import { updateWorkingOffset } from './transforms' import { + PROCEED_STEP, SET_INITIAL_POSITION, SET_FINAL_POSITION, SET_TIP_PICKUP_OFFSET, @@ -13,6 +14,23 @@ export function LPCReducer( action: LPCWizardAction ): LPCWizardState { switch (action.type) { + case PROCEED_STEP: { + const { currentStepIndex, totalStepCount } = state.steps + const newStepIdx = + currentStepIndex !== totalStepCount + ? currentStepIndex + 1 + : currentStepIndex + + return { + ...state, + steps: { + ...state.steps, + currentStepIndex: newStepIdx, + current: state.steps.all[newStepIdx], + }, + } + } + case SET_TIP_PICKUP_OFFSET: return { ...state, tipPickUpOffset: action.payload.offset } diff --git a/app/src/organisms/LabwarePositionCheck/redux/types.ts b/app/src/organisms/LabwarePositionCheck/redux/types.ts index 9d7bfc18190..b1daad667f0 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/types.ts +++ b/app/src/organisms/LabwarePositionCheck/redux/types.ts @@ -1,5 +1,7 @@ -import type { Coordinates } from '@opentrons/shared-data' +import type { Coordinates, LabwareDefinition2 } from '@opentrons/shared-data' import type { LabwareOffsetLocation, VectorOffset } from '@opentrons/api-client' +import type { LPCWizardFlexProps } from '/app/organisms/LabwarePositionCheck/LPCWizardFlex' +import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' export interface WorkingOffset { labwareId: string @@ -14,6 +16,11 @@ export interface PositionParams { position: VectorOffset | null } +export interface ProceedStepAction { + type: 'PROCEED_STEP' + payload: Record +} + export interface InitialPositionAction { type: 'SET_INITIAL_POSITION' payload: PositionParams @@ -31,12 +38,24 @@ export interface TipPickUpOffsetAction { } } -export interface LPCWizardState { +interface StepsInfo { + currentStepIndex: number + totalStepCount: number + current: LabwarePositionCheckStep + all: LabwarePositionCheckStep[] +} + +export interface LPCWizardState extends LPCWizardFlexProps { + isOnDevice: boolean workingOffsets: WorkingOffset[] tipPickUpOffset: Coordinates | null + protocolData: LPCWizardFlexProps['mostRecentAnalysis'] + labwareDefs: LabwareDefinition2[] + steps: StepsInfo } export type LPCWizardAction = | InitialPositionAction | FinalPositionAction | TipPickUpOffsetAction + | ProceedStepAction diff --git a/app/src/organisms/LabwarePositionCheck/shared/PrepareSpace.tsx b/app/src/organisms/LabwarePositionCheck/shared/PrepareSpace.tsx index e22a1ab385f..fb60408e336 100644 --- a/app/src/organisms/LabwarePositionCheck/shared/PrepareSpace.tsx +++ b/app/src/organisms/LabwarePositionCheck/shared/PrepareSpace.tsx @@ -64,12 +64,13 @@ interface PrepareSpaceProps extends LPCStepProps { export function PrepareSpace({ location, labwareDef, - protocolData, + state, header, body, confirmPlacement, }: PrepareSpaceProps): JSX.Element { const { i18n, t } = useTranslation(['labware_position_check', 'shared']) + const { protocolData } = state const isOnDevice = useSelector(getIsOnDevice) const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] diff --git a/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx b/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx index 514f9ba52ac..a9af9db4528 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx @@ -20,20 +20,20 @@ import attachProbe96 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attac import type { AttachProbeStep, LPCStepProps } from '../types' export function AttachProbe({ - step, - protocolData, proceed, - isOnDevice, commandUtils, + state, + step, }: LPCStepProps): JSX.Element { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) + const { protocolData, isOnDevice } = state + const { pipetteId } = step const { moveToMaintenancePosition, setShowUnableToDetect, unableToDetect, createProbeAttachmentHandler, } = commandUtils - const { pipetteId } = step // TOME TODO: This pipette logic should almost certainly be in a selector, too. const pipette = protocolData.pipettes.find(p => p.id === pipetteId) const handleProbeAttached = createProbeAttachmentHandler( diff --git a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx index 6f839027c00..00f0981010a 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx @@ -24,12 +24,11 @@ const SUPPORT_PAGE_URL = 'https://support.opentrons.com/s/ot2-calibration' export function BeforeBeginning({ proceed, existingOffsets, - protocolName, - labwareDefs, commandUtils, - isOnDevice, + state, }: LPCStepProps): JSX.Element { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) + const { isOnDevice, protocolName, labwareDefs } = state const { createStartLPCHandler } = commandUtils const handleStartLPC = createStartLPCHandler(proceed) diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx index 6c7cbba12ef..4b678e0d218 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx @@ -36,15 +36,12 @@ export function CheckItem( props: LPCStepProps ): JSX.Element { const { - step, - protocolData, state, dispatch, proceed, existingOffsets, - labwareDefs, commandUtils, - isOnDevice, + step, } = props const { labwareId, pipetteId, moduleId, location } = step const { @@ -54,7 +51,7 @@ export function CheckItem( handleConfirmLwFinalPosition, handleResetLwModulesOnDeck, } = commandUtils - const { workingOffsets } = state + const { workingOffsets, isOnDevice, labwareDefs, protocolData } = state const { t } = useTranslation(['labware_position_check', 'shared']) // TOME TODO: Pretty mcuh all of this goes into selectors. @@ -212,14 +209,14 @@ interface PlaceItemInstructionProps extends LPCStepProps { } function PlaceItemInstruction({ - protocolData, - labwareDefs, step, itemLabwareDef, isLwTiprack, slotOnlyDisplayLocation, + state, }: PlaceItemInstructionProps): JSX.Element { const { t } = useTranslation('labware_position_check') + const { protocolData, labwareDefs } = state const { location, adapterId } = step const labwareDisplayName = getLabwareDisplayName(itemLabwareDef) diff --git a/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx index 46a6dcacd7b..626b8088993 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx @@ -19,12 +19,13 @@ import detachProbe96 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detac import type { DetachProbeStep, LPCStepProps } from '../types' export const DetachProbe = ({ + state, step, - protocolData, proceed, commandUtils, }: LPCStepProps): JSX.Element => { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) + const { protocolData } = state const { moveToMaintenancePosition, createProbeDetachmentHandler, diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx index d30cdf359ea..7b0a7c0d281 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx @@ -35,9 +35,10 @@ interface OffsetTableProps extends LPCStepProps { export function OffsetTable({ offsets, labwareDefinitions, - protocolData, + state, }: OffsetTableProps): JSX.Element { const { t } = useTranslation('labware_position_check') + const { protocolData } = state return ( diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/TableComponent.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/TableComponent.tsx index 2b870a00aa6..ca869f243d2 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/TableComponent.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/TableComponent.tsx @@ -7,12 +7,13 @@ import type { ResultsSummaryStep, } from '/app/organisms/LabwarePositionCheck/types' -interface TableComponent extends LPCStepProps { +interface TableComponentProps extends LPCStepProps { offsetsToApply: LabwareOffsetCreateData[] } -export function TableComponent(props: TableComponent): JSX.Element { - const { isOnDevice, labwareDefs, offsetsToApply } = props +export function TableComponent(props: TableComponentProps): JSX.Element { + const { state, offsetsToApply } = props + const { isOnDevice, labwareDefs } = state return isOnDevice ? ( ): JSX.Element { - const { - protocolData, - state, - existingOffsets, - commandUtils, - isOnDevice, - } = props + const { existingOffsets, commandUtils, state } = props + const { protocolData, isOnDevice, workingOffsets } = state const { isApplyingOffsets, handleApplyOffsets } = commandUtils const { i18n, t } = useTranslation('labware_position_check') - const { workingOffsets } = state const isLabwareOffsetCodeSnippetsOn = useSelector( getIsLabwareOffsetCodeSnippetsOn ) diff --git a/app/src/organisms/LabwarePositionCheck/types/content.ts b/app/src/organisms/LabwarePositionCheck/types/content.ts index 21317c8f78c..7bb2793dd51 100644 --- a/app/src/organisms/LabwarePositionCheck/types/content.ts +++ b/app/src/organisms/LabwarePositionCheck/types/content.ts @@ -1,31 +1,18 @@ import type { Dispatch } from 'react' -import type { - CompletedProtocolAnalysis, - LabwareDefinition2, -} from '@opentrons/shared-data' import type { LabwareOffset } from '@opentrons/api-client' import type { LPCWizardAction, LPCWizardState, } from '/app/organisms/LabwarePositionCheck/redux' -import type { LabwarePositionCheckStep } from './steps' -import type { LPCFlowsProps } from '/app/organisms/LabwarePositionCheck' import type { UseLPCCommandsResult } from '/app/organisms/LabwarePositionCheck/hooks' +import type { LPCWizardFlexProps } from '/app/organisms/LabwarePositionCheck/LPCWizardFlex' -// TOME TODO: REDUX! Pretty much all of this should be in redux or in the data layer. - -export type LPCWizardContentProps = Omit & { - step: LabwarePositionCheckStep - protocolName: string - protocolData: CompletedProtocolAnalysis +export type LPCWizardContentProps = Pick & { proceed: () => void dispatch: Dispatch state: LPCWizardState // TOME TODO: Consider adding the commands state to the state state. commandUtils: UseLPCCommandsResult - currentStepIndex: number - totalStepCount: number + // TOME TODO: This should also be in state existingOffsets: LabwareOffset[] - isOnDevice: boolean - labwareDefs: LabwareDefinition2[] } From 0a0ef08d9bec3ba3b861dea362bcb53c26443d40 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Thu, 9 Jan 2025 09:37:12 -0500 Subject: [PATCH 20/33] refactor(app): hoist refactored components out of shared A couple components are now not shared in order to keep render control flow simpler. This just moves them out of the shared dir and cleans up a bit of their CSS styling in the process. --- .../{shared => }/ExitConfirmation.tsx | 69 +++++++++++-------- .../LabwarePositionCheck/LPCWizardFlex.tsx | 6 +- .../{shared => }/RobotMotionLoader.tsx | 20 +++--- .../LabwarePositionCheck/shared/index.ts | 1 - 4 files changed, 53 insertions(+), 43 deletions(-) rename app/src/organisms/LabwarePositionCheck/{shared => }/ExitConfirmation.tsx (75%) rename app/src/organisms/LabwarePositionCheck/{shared => }/RobotMotionLoader.tsx (73%) diff --git a/app/src/organisms/LabwarePositionCheck/shared/ExitConfirmation.tsx b/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx similarity index 75% rename from app/src/organisms/LabwarePositionCheck/shared/ExitConfirmation.tsx rename to app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx index e2738b4b95c..61864daf358 100644 --- a/app/src/organisms/LabwarePositionCheck/shared/ExitConfirmation.tsx +++ b/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx @@ -1,5 +1,7 @@ -import styled from 'styled-components' +import styled, { css } from 'styled-components' import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' + import { AlertPrimaryButton, ALIGN_CENTER, @@ -17,32 +19,23 @@ import { TEXT_ALIGN_CENTER, TYPOGRAPHY, } from '@opentrons/components' -import { useSelector } from 'react-redux' + import { getIsOnDevice } from '/app/redux/config' import { SmallButton } from '/app/atoms/buttons' import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' -export const ExitConfirmation = ({ +export function ExitConfirmation({ commandUtils, -}: LPCWizardContentProps): JSX.Element => { +}: LPCWizardContentProps): JSX.Element { const { i18n, t } = useTranslation(['labware_position_check', 'shared']) const { confirmExitLPC, cancelExitLPC } = commandUtils const isOnDevice = useSelector(getIsOnDevice) + return ( - - + + {isOnDevice ? ( <> @@ -67,12 +60,7 @@ export const ExitConfirmation = ({ )} {isOnDevice ? ( - + ) : ( - + {t('shared:go_back')} @@ -108,6 +91,35 @@ export const ExitConfirmation = ({ ) } +const CONTAINER_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + padding: ${SPACING.spacing32}; + min-height: 29.5rem; +` + +const CONTENT_CONTAINER_STYLE = css` + flex: 1; + flex-direction: ${DIRECTION_COLUMN}; + justify-content: ${JUSTIFY_CENTER}; + align-items: ${ALIGN_CENTER}; + padding-left: ${SPACING.spacing32}; + padding-right: ${SPACING.spacing32}; +` + +const BUTTON_CONTAINER_STYLE = css` + width: 100%; + margin-top: ${SPACING.spacing32}; + justify-content: ${JUSTIFY_FLEX_END}; + align-items: ${ALIGN_CENTER}; +` + +const BUTTON_CONTAINER_STYLE_ODD = css` + width: 100%; + justify-content: ${JUSTIFY_FLEX_END}; + align-items: ${ALIGN_CENTER}; + grid-gap: ${SPACING.spacing8}; +` + const ConfirmationHeader = styled.h1` margin-top: ${SPACING.spacing24}; ${TYPOGRAPHY.h1Default} @@ -123,6 +135,7 @@ const ConfirmationHeaderODD = styled.h1` ${TYPOGRAPHY.level4HeaderSemiBold} } ` + const ConfirmationBodyODD = styled.h1` ${TYPOGRAPHY.level4HeaderRegular} @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { diff --git a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx index d7cf3ccd485..b6880fd9c3d 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx @@ -11,10 +11,8 @@ import { DetachProbe, ResultsSummary, } from '/app/organisms/LabwarePositionCheck/steps' -import { - RobotMotionLoader, - ExitConfirmation, -} from '/app/organisms/LabwarePositionCheck/shared' +import { ExitConfirmation } from './ExitConfirmation' +import { RobotMotionLoader } from './RobotMotionLoader' import { WizardHeader } from '/app/molecules/WizardHeader' import { LPCErrorModal } from './LPCErrorModal' import { diff --git a/app/src/organisms/LabwarePositionCheck/shared/RobotMotionLoader.tsx b/app/src/organisms/LabwarePositionCheck/RobotMotionLoader.tsx similarity index 73% rename from app/src/organisms/LabwarePositionCheck/shared/RobotMotionLoader.tsx rename to app/src/organisms/LabwarePositionCheck/RobotMotionLoader.tsx index 56e376cca2b..5b83f147b8b 100644 --- a/app/src/organisms/LabwarePositionCheck/shared/RobotMotionLoader.tsx +++ b/app/src/organisms/LabwarePositionCheck/RobotMotionLoader.tsx @@ -1,4 +1,4 @@ -import styled from 'styled-components' +import styled, { css } from 'styled-components' import { ALIGN_CENTER, COLORS, @@ -18,18 +18,10 @@ interface RobotMotionLoaderProps { body?: string } -// TOME TODO: IDK if this actually needs to be a shared component. It would be ideal if it wasn't. - export function RobotMotionLoader(props: RobotMotionLoaderProps): JSX.Element { const { header, body } = props return ( - + {header != null ? {header} : null} {body != null ? {body} : null} @@ -52,3 +44,11 @@ const LoadingText = styled.h1` ${TYPOGRAPHY.level4HeaderSemiBold} } ` + +const CONTAINER_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + justify-content: ${JUSTIFY_CENTER}; + align-items: ${ALIGN_CENTER}; + min-height: 29.5rem; + grid-gap: ${SPACING.spacing24}; +` diff --git a/app/src/organisms/LabwarePositionCheck/shared/index.ts b/app/src/organisms/LabwarePositionCheck/shared/index.ts index 438c6c9d13c..1138356b3b0 100644 --- a/app/src/organisms/LabwarePositionCheck/shared/index.ts +++ b/app/src/organisms/LabwarePositionCheck/shared/index.ts @@ -1,4 +1,3 @@ export { PrepareSpace } from './PrepareSpace' export { ExitConfirmation } from './ExitConfirmation' export { JogToWell } from './JogToWell' -export { RobotMotionLoader } from './RobotMotionLoader' From 01bf56e3e37161bc70c27cd33b2396e9fd9fa41e Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Thu, 9 Jan 2025 10:41:21 -0500 Subject: [PATCH 21/33] refactor(app): move the remaining components out of shared There are no longer any base components that are shared between steps, so we can nix the directory. --- .../hooks/useLPCInitialState.ts | 4 +- .../LabwarePositionCheck/redux/types.ts | 7 +- .../LabwarePositionCheck/shared/index.ts | 3 - .../CheckItem}/JogToWell/LiveOffsetValue.tsx | 0 .../CheckItem}/JogToWell/index.tsx | 86 +++++++++++------- .../CheckItem}/PrepareSpace.tsx | 88 ++++++++++--------- .../{CheckItem.tsx => CheckItem/index.tsx} | 15 ++-- 7 files changed, 117 insertions(+), 86 deletions(-) delete mode 100644 app/src/organisms/LabwarePositionCheck/shared/index.ts rename app/src/organisms/LabwarePositionCheck/{shared => steps/CheckItem}/JogToWell/LiveOffsetValue.tsx (100%) rename app/src/organisms/LabwarePositionCheck/{shared => steps/CheckItem}/JogToWell/index.tsx (85%) rename app/src/organisms/LabwarePositionCheck/{shared => steps/CheckItem}/PrepareSpace.tsx (79%) rename app/src/organisms/LabwarePositionCheck/steps/{CheckItem.tsx => CheckItem/index.tsx} (96%) diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState.ts index 890dc5421da..c2a86d48824 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState.ts @@ -3,6 +3,7 @@ import { useSelector } from 'react-redux' import { getIsOnDevice } from '/app/redux/config' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' import { getLPCSteps } from '/app/organisms/LabwarePositionCheck/utils' +import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' import type { RunTimeCommand } from '@opentrons/shared-data' import type { LPCWizardState } from '/app/organisms/LabwarePositionCheck/redux' @@ -18,7 +19,7 @@ export function useLPCInitialState( const protocolCommands: RunTimeCommand[] = mostRecentAnalysis.commands const labwareDefs = getLabwareDefinitionsFromCommands(protocolCommands) - + const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] const LPCSteps = getLPCSteps({ protocolData: mostRecentAnalysis, labwareDefs, @@ -31,6 +32,7 @@ export function useLPCInitialState( labwareDefs, workingOffsets: [], tipPickUpOffset: null, + deckConfig, steps: { currentStepIndex: 0, totalStepCount: LPCSteps.length - 1, diff --git a/app/src/organisms/LabwarePositionCheck/redux/types.ts b/app/src/organisms/LabwarePositionCheck/redux/types.ts index b1daad667f0..8dd7fcc17a4 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/types.ts +++ b/app/src/organisms/LabwarePositionCheck/redux/types.ts @@ -1,4 +1,8 @@ -import type { Coordinates, LabwareDefinition2 } from '@opentrons/shared-data' +import type { + Coordinates, + DeckConfiguration, + LabwareDefinition2, +} from '@opentrons/shared-data' import type { LabwareOffsetLocation, VectorOffset } from '@opentrons/api-client' import type { LPCWizardFlexProps } from '/app/organisms/LabwarePositionCheck/LPCWizardFlex' import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' @@ -51,6 +55,7 @@ export interface LPCWizardState extends LPCWizardFlexProps { tipPickUpOffset: Coordinates | null protocolData: LPCWizardFlexProps['mostRecentAnalysis'] labwareDefs: LabwareDefinition2[] + deckConfig: DeckConfiguration steps: StepsInfo } diff --git a/app/src/organisms/LabwarePositionCheck/shared/index.ts b/app/src/organisms/LabwarePositionCheck/shared/index.ts deleted file mode 100644 index 1138356b3b0..00000000000 --- a/app/src/organisms/LabwarePositionCheck/shared/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { PrepareSpace } from './PrepareSpace' -export { ExitConfirmation } from './ExitConfirmation' -export { JogToWell } from './JogToWell' diff --git a/app/src/organisms/LabwarePositionCheck/shared/JogToWell/LiveOffsetValue.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/LiveOffsetValue.tsx similarity index 100% rename from app/src/organisms/LabwarePositionCheck/shared/JogToWell/LiveOffsetValue.tsx rename to app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/LiveOffsetValue.tsx diff --git a/app/src/organisms/LabwarePositionCheck/shared/JogToWell/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/index.tsx similarity index 85% rename from app/src/organisms/LabwarePositionCheck/shared/JogToWell/index.tsx rename to app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/index.tsx index dbf13d7aa32..e13b7d8c897 100644 --- a/app/src/organisms/LabwarePositionCheck/shared/JogToWell/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/index.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' import styled, { css } from 'styled-components' + import { ALIGN_CENTER, ALIGN_FLEX_START, @@ -28,9 +28,6 @@ import { getVectorSum, } from '@opentrons/shared-data' -import levelProbeWithTip from '/app/assets/images/lpc_level_probe_with_tip.svg' -import levelProbeWithLabware from '/app/assets/images/lpc_level_probe_with_labware.svg' -import { getIsOnDevice } from '/app/redux/config' import { getTopPortalEl } from '/app/App/portal' import { SmallButton } from '/app/atoms/buttons' import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' @@ -47,6 +44,9 @@ import type { LPCStepProps, } from '/app/organisms/LabwarePositionCheck/types' +import levelProbeWithTip from '/app/assets/images/lpc_level_probe_with_tip.svg' +import levelProbeWithLabware from '/app/assets/images/lpc_level_probe_with_labware.svg' + const DECK_MAP_VIEWBOX = '-10 -10 150 105' const LPC_HELP_LINK_URL = 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' @@ -73,13 +73,14 @@ export function JogToWell({ handleJog, initialPosition, existingOffset, + state, }: JogToWellProps): JSX.Element { const { t } = useTranslation(['labware_position_check', 'shared']) + const { isOnDevice } = state const [joggedPosition, setJoggedPosition] = useState( initialPosition ) - const isOnDevice = useSelector(getIsOnDevice) const [showFullJogControls, setShowFullJogControls] = useState(false) useEffect(() => { // NOTE: this will perform a "null" jog when the jog controls mount so @@ -96,6 +97,9 @@ export function JogToWell({ } }, []) + // TOME TODO: Steps should honestly should include more details about the instruments used + // or be made into selectors. + const wellsToHighlight = ['A1'] const wellStroke: WellStroke = wellsToHighlight.reduce( (acc, wellName) => ({ ...acc, [wellName]: COLORS.blue50 }), @@ -110,24 +114,14 @@ export function JogToWell({ const levelSrc = isTipRack ? levelProbeWithTip : levelProbeWithLabware return ( - - - + + +
{header}
{body}
- + {() => ( <> @@ -156,18 +150,13 @@ export function JogToWell({ {isOnDevice ? ( - + - + - + - + {t('shared:go_back')} @@ -252,6 +236,42 @@ export function JogToWell({ ) } +const CONTAINER_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + justify-content: ${JUSTIFY_SPACE_BETWEEN}; + padding: ${SPACING.spacing32}; + min-height: 29.5rem; +` + +const CONTENT_GRID_STYLE = css` + grid-gap: ${SPACING.spacing24}; +` + +const INFO_CONTAINER_STYLE = css` + flex: 1; + flex-direction: ${DIRECTION_COLUMN}; + grid-gap: ${SPACING.spacing8}; + align-items: ${ALIGN_FLEX_START}; +` + +const RENDER_CONTAINER_STYLE = css` + flex: 1; + align-items: ${ALIGN_CENTER}; + grid-gap: ${SPACING.spacing20}; +` + +const FOOTER_CONTAINER_STYLE = css` + width: 100%; + margin-top: ${SPACING.spacing32}; + justify-content: ${JUSTIFY_SPACE_BETWEEN}; + align-items: ${ALIGN_CENTER}; +` + +const BUTTON_GROUP_STYLE = css` + grid-gap: ${SPACING.spacing8}; + align-items: ${ALIGN_CENTER}; +` + const Header = styled.h1` ${TYPOGRAPHY.h1Default} diff --git a/app/src/organisms/LabwarePositionCheck/shared/PrepareSpace.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PrepareSpace.tsx similarity index 79% rename from app/src/organisms/LabwarePositionCheck/shared/PrepareSpace.tsx rename to app/src/organisms/LabwarePositionCheck/steps/CheckItem/PrepareSpace.tsx index fb60408e336..a2e0f32a6c3 100644 --- a/app/src/organisms/LabwarePositionCheck/shared/PrepareSpace.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PrepareSpace.tsx @@ -1,6 +1,5 @@ import styled, { css } from 'styled-components' import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' import { DIRECTION_COLUMN, @@ -22,47 +21,27 @@ import { FLEX_ROBOT_TYPE, } from '@opentrons/shared-data' -import { getIsOnDevice } from '/app/redux/config' import { SmallButton } from '/app/atoms/buttons' import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' -import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' import type { ReactNode } from 'react' import type { LabwareDefinition2 } from '@opentrons/shared-data' -import type { CheckPositionsStep, LPCStepProps, PerformLPCStep } from '../types' +import type { + CheckPositionsStep, + LPCStepProps, +} from '/app/organisms/LabwarePositionCheck/types' const LPC_HELP_LINK_URL = 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' -const TILE_CONTAINER_STYLE = css` - flex-direction: ${DIRECTION_COLUMN}; - justify-content: ${JUSTIFY_SPACE_BETWEEN}; - padding: ${SPACING.spacing32}; - height: 24.625rem; - flex: 1; - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - height: 29.5rem; - } -` - -const Title = styled.h1` - ${TYPOGRAPHY.h1Default}; - - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - ${TYPOGRAPHY.level4HeaderSemiBold}; - } -` - interface PrepareSpaceProps extends LPCStepProps { header: ReactNode body: ReactNode labwareDef: LabwareDefinition2 - location: PerformLPCStep['location'] confirmPlacement: () => void } export function PrepareSpace({ - location, labwareDef, state, header, @@ -70,27 +49,17 @@ export function PrepareSpace({ confirmPlacement, }: PrepareSpaceProps): JSX.Element { const { i18n, t } = useTranslation(['labware_position_check', 'shared']) - const { protocolData } = state - - const isOnDevice = useSelector(getIsOnDevice) - const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] + const { protocolData, isOnDevice, deckConfig, steps } = state + const { location } = steps.current as CheckPositionsStep // safely enforced by iface return ( - - - + + + {header} {body} - + ({ @@ -140,3 +109,40 @@ export function PrepareSpace({ ) } + +const PARENT_CONTAINER_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + justify-content: ${JUSTIFY_SPACE_BETWEEN}; + padding: ${SPACING.spacing32}; + height: 24.625rem; + flex: 1; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + height: 29.5rem; + } +` + +const TITLE_CONTAINER_STYLE = css` + flex: 2; + flex-direction: ${DIRECTION_COLUMN}; + grid-gap: ${SPACING.spacing16}; +` + +const CONTENT_CONTAINER_STYLE = css` + flex: 1; + flex-direction: ${DIRECTION_ROW}; + grid-gap: ${SPACING.spacing40}; +` + +const DECK_CONTAINER_STYLE = css` + flex: 3; + justify-content: ${JUSTIFY_CENTER}; + align-items: ${ALIGN_FLEX_START}; +` + +const Title = styled.h1` + ${TYPOGRAPHY.h1Default}; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + ${TYPOGRAPHY.level4HeaderSemiBold}; + } +` diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx similarity index 96% rename from app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx rename to app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx index 4b678e0d218..bcbafc54b22 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx @@ -9,10 +9,6 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { - PrepareSpace, - JogToWell, -} from '/app/organisms/LabwarePositionCheck/shared' import { FLEX_ROBOT_TYPE, getIsTiprack, @@ -28,9 +24,14 @@ import { setFinalPosition, setInitialPosition, } from '/app/organisms/LabwarePositionCheck/redux/actions' +import { JogToWell } from './JogToWell' +import { PrepareSpace } from './PrepareSpace' import type { PipetteName, LabwareDefinition2 } from '@opentrons/shared-data' -import type { CheckPositionsStep, LPCStepProps } from '../types' +import type { + CheckPositionsStep, + LPCStepProps, +} from '/app/organisms/LabwarePositionCheck/types' export function CheckItem( props: LPCStepProps @@ -54,7 +55,8 @@ export function CheckItem( const { workingOffsets, isOnDevice, labwareDefs, protocolData } = state const { t } = useTranslation(['labware_position_check', 'shared']) - // TOME TODO: Pretty mcuh all of this goes into selectors. + // TOME TODO: Pretty mcuh all of this goes into selectors, and a lot of it + // should go into JogToWell as well. const itemLabwareDef = getItemLabwareDef({ labwareId, @@ -194,7 +196,6 @@ export function CheckItem( } labwareDef={itemLabwareDef} confirmPlacement={handleDispatchConfirmInitialPlacement} - location={step.location} {...props} /> )} From 4ae76cd8605e8c6270470682295700943b3c3b12 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Thu, 9 Jan 2025 13:16:29 -0500 Subject: [PATCH 22/33] refactor(app): relocate general utils General utils are no longer generally used and should be relocated appropriately for better namespacing. --- .../{useLPCInitialState.ts => useLPCInitialState/index.ts} | 2 +- .../utils/getLPCSteps/getProbeBasedLPCSteps.ts | 6 +++--- .../useLPCInitialState}/utils/getLPCSteps/index.ts | 1 + .../hooks/useLPCInitialState/utils/index.ts | 1 + .../LabwarePositionCheck/steps/CheckItem/index.tsx | 2 +- .../getItemLabwareDef.ts => steps/CheckItem/utils.ts} | 2 +- app/src/organisms/LabwarePositionCheck/utils/index.ts | 2 -- 7 files changed, 8 insertions(+), 8 deletions(-) rename app/src/organisms/LabwarePositionCheck/hooks/{useLPCInitialState.ts => useLPCInitialState/index.ts} (94%) rename app/src/organisms/LabwarePositionCheck/{ => hooks/useLPCInitialState}/utils/getLPCSteps/getProbeBasedLPCSteps.ts (96%) rename app/src/organisms/LabwarePositionCheck/{ => hooks/useLPCInitialState}/utils/getLPCSteps/index.ts (95%) create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/index.ts rename app/src/organisms/LabwarePositionCheck/{utils/getItemLabwareDef.ts => steps/CheckItem/utils.ts} (93%) delete mode 100644 app/src/organisms/LabwarePositionCheck/utils/index.ts diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/index.ts similarity index 94% rename from app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState.ts rename to app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/index.ts index c2a86d48824..d988d0b202b 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/index.ts @@ -2,8 +2,8 @@ import { useSelector } from 'react-redux' import { getIsOnDevice } from '/app/redux/config' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' -import { getLPCSteps } from '/app/organisms/LabwarePositionCheck/utils' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' +import { getLPCSteps } from './utils' import type { RunTimeCommand } from '@opentrons/shared-data' import type { LPCWizardState } from '/app/organisms/LabwarePositionCheck/redux' diff --git a/app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/getProbeBasedLPCSteps.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/getLPCSteps/getProbeBasedLPCSteps.ts similarity index 96% rename from app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/getProbeBasedLPCSteps.ts rename to app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/getLPCSteps/getProbeBasedLPCSteps.ts index 2ba2ed586f4..4b6241c4fe2 100644 --- a/app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/getProbeBasedLPCSteps.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/getLPCSteps/getProbeBasedLPCSteps.ts @@ -13,9 +13,9 @@ import type { import type { LabwareLocationCombo } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' import type { GetLPCStepsParams } from '.' -export const getProbeBasedLPCSteps = ( +export function getProbeBasedLPCSteps( params: GetLPCStepsParams -): LabwarePositionCheckStep[] => { +): LabwarePositionCheckStep[] { const { protocolData } = params return [ @@ -89,7 +89,7 @@ function getAllCheckSectionSteps({ location, moduleId, adapterId, - definitionUri: definitionUri, + definitionUri, }) ) } diff --git a/app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/getLPCSteps/index.ts similarity index 95% rename from app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/index.ts rename to app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/getLPCSteps/index.ts index 1398fd3514b..7121d2cdf98 100644 --- a/app/src/organisms/LabwarePositionCheck/utils/getLPCSteps/index.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/getLPCSteps/index.ts @@ -11,6 +11,7 @@ export interface GetLPCStepsParams { labwareDefs: LabwareDefinition2[] } +// Prepare all LPC steps for injection. export function getLPCSteps( params: GetLPCStepsParams ): LabwarePositionCheckStep[] { diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/index.ts new file mode 100644 index 00000000000..3fd9dba02b5 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/index.ts @@ -0,0 +1 @@ +export * from './getLPCSteps' diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx index bcbafc54b22..d00c0a29bca 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx @@ -16,7 +16,6 @@ import { getLabwareDisplayName, IDENTITY_VECTOR, } from '@opentrons/shared-data' -import { getItemLabwareDef } from '/app/organisms/LabwarePositionCheck/utils' import { getLabwareDisplayLocation } from '/app/local-resources/labware' import { UnorderedList } from '/app/molecules/UnorderedList' import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' @@ -26,6 +25,7 @@ import { } from '/app/organisms/LabwarePositionCheck/redux/actions' import { JogToWell } from './JogToWell' import { PrepareSpace } from './PrepareSpace' +import { getItemLabwareDef } from './utils' import type { PipetteName, LabwareDefinition2 } from '@opentrons/shared-data' import type { diff --git a/app/src/organisms/LabwarePositionCheck/utils/getItemLabwareDef.ts b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/utils.ts similarity index 93% rename from app/src/organisms/LabwarePositionCheck/utils/getItemLabwareDef.ts rename to app/src/organisms/LabwarePositionCheck/steps/CheckItem/utils.ts index 45e20766a5b..6a1710be04d 100644 --- a/app/src/organisms/LabwarePositionCheck/utils/getItemLabwareDef.ts +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/utils.ts @@ -21,5 +21,5 @@ export function getItemLabwareDef({ return labwareDefs.find( def => getLabwareDefURI(def) === labwareDefUri - ) as LabwareDefinition2 // Safe assumption + ) as LabwareDefinition2 } diff --git a/app/src/organisms/LabwarePositionCheck/utils/index.ts b/app/src/organisms/LabwarePositionCheck/utils/index.ts deleted file mode 100644 index ffb5ad25acd..00000000000 --- a/app/src/organisms/LabwarePositionCheck/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './getItemLabwareDef' -export * from './getLPCSteps' From f33f9ff0543d34ef6d434538a4d3e31c1294a501 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Mon, 13 Jan 2025 09:22:35 -0500 Subject: [PATCH 23/33] refactor(app): remove tip pickup offset from redux store We don't actually use tip pickup offset data in the Flex LPC flows, since we use a probe (and don't pick up tips). --- .../organisms/LabwarePositionCheck/redux/actions.ts | 12 ------------ .../LabwarePositionCheck/redux/constants.ts | 1 - .../organisms/LabwarePositionCheck/redux/reducer.ts | 4 ---- .../organisms/LabwarePositionCheck/redux/types.ts | 8 -------- 4 files changed, 25 deletions(-) diff --git a/app/src/organisms/LabwarePositionCheck/redux/actions.ts b/app/src/organisms/LabwarePositionCheck/redux/actions.ts index 2bdb0b51f54..3bcbe3b8e90 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/actions.ts +++ b/app/src/organisms/LabwarePositionCheck/redux/actions.ts @@ -2,15 +2,12 @@ import { PROCEED_STEP, SET_INITIAL_POSITION, SET_FINAL_POSITION, - SET_TIP_PICKUP_OFFSET, } from './constants' -import type { Coordinates } from '@opentrons/shared-data' import type { ProceedStepAction, InitialPositionAction, FinalPositionAction, - TipPickUpOffsetAction, PositionParams, } from './types' @@ -18,15 +15,6 @@ export const proceedStep = (): ProceedStepAction => ({ type: PROCEED_STEP, payload: {}, }) - -// TOME TODO: I think you should be using this somewhere... -export const setTipPickupOffset = ( - offset: Coordinates | null -): TipPickUpOffsetAction => ({ - type: SET_TIP_PICKUP_OFFSET, - payload: { offset }, -}) - export const setInitialPosition = ( params: PositionParams ): InitialPositionAction => ({ diff --git a/app/src/organisms/LabwarePositionCheck/redux/constants.ts b/app/src/organisms/LabwarePositionCheck/redux/constants.ts index c4eb7c323f4..d4803a0cadf 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/constants.ts +++ b/app/src/organisms/LabwarePositionCheck/redux/constants.ts @@ -1,4 +1,3 @@ export const PROCEED_STEP = 'PROCEED_STEP' export const SET_INITIAL_POSITION = 'SET_INITIAL_POSITION' export const SET_FINAL_POSITION = 'SET_FINAL_POSITION' -export const SET_TIP_PICKUP_OFFSET = 'SET_TIP_PICKUP_OFFSET' diff --git a/app/src/organisms/LabwarePositionCheck/redux/reducer.ts b/app/src/organisms/LabwarePositionCheck/redux/reducer.ts index 1a11544d65f..2d2a3ae8d7b 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/reducer.ts +++ b/app/src/organisms/LabwarePositionCheck/redux/reducer.ts @@ -3,7 +3,6 @@ import { PROCEED_STEP, SET_INITIAL_POSITION, SET_FINAL_POSITION, - SET_TIP_PICKUP_OFFSET, } from './constants' import type { LPCWizardAction } from './types' @@ -31,9 +30,6 @@ export function LPCReducer( } } - case SET_TIP_PICKUP_OFFSET: - return { ...state, tipPickUpOffset: action.payload.offset } - case SET_INITIAL_POSITION: case SET_FINAL_POSITION: return { diff --git a/app/src/organisms/LabwarePositionCheck/redux/types.ts b/app/src/organisms/LabwarePositionCheck/redux/types.ts index 8dd7fcc17a4..1ad56bca941 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/types.ts +++ b/app/src/organisms/LabwarePositionCheck/redux/types.ts @@ -35,13 +35,6 @@ export interface FinalPositionAction { payload: PositionParams } -export interface TipPickUpOffsetAction { - type: 'SET_TIP_PICKUP_OFFSET' - payload: { - offset: Coordinates | null - } -} - interface StepsInfo { currentStepIndex: number totalStepCount: number @@ -62,5 +55,4 @@ export interface LPCWizardState extends LPCWizardFlexProps { export type LPCWizardAction = | InitialPositionAction | FinalPositionAction - | TipPickUpOffsetAction | ProceedStepAction From e39650aed985c2dccd630c98aca4b231afe56a29 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Mon, 13 Jan 2025 12:55:09 -0500 Subject: [PATCH 24/33] refactor(app): move all state derivation logic to selectors --- .../LabwarePositionCheck/ExitConfirmation.tsx | 9 +- .../useLPCCommands/useHandlePrepModules.ts | 2 +- .../LabwarePositionCheck/redux/index.ts | 1 + .../redux/selectors/index.ts | 2 + .../redux/selectors/labware.ts | 116 +++++++++++ .../redux/selectors/pipettes.ts | 27 +++ .../steps/AttachProbe.tsx | 39 ++-- .../steps/BeforeBeginning/TwoUpTileLayout.tsx | 51 ++--- .../steps/BeforeBeginning/ViewOffsets.tsx | 3 + .../CheckItem/JogToWell/LiveOffsetValue.tsx | 24 +-- .../steps/CheckItem/JogToWell/index.tsx | 76 ++++---- .../steps/CheckItem/PlaceItemInstruction.tsx | 97 ++++++++++ .../steps/CheckItem/PrepareSpace.tsx | 4 +- .../steps/CheckItem/index.tsx | 182 ++---------------- .../steps/CheckItem/utils.ts | 16 +- .../steps/DetachProbe.tsx | 30 +-- .../steps/ResultsSummary/OffsetTable.tsx | 23 +-- .../steps/ResultsSummary/index.tsx | 4 +- 18 files changed, 404 insertions(+), 302 deletions(-) create mode 100644 app/src/organisms/LabwarePositionCheck/redux/selectors/index.ts create mode 100644 app/src/organisms/LabwarePositionCheck/redux/selectors/labware.ts create mode 100644 app/src/organisms/LabwarePositionCheck/redux/selectors/pipettes.ts create mode 100644 app/src/organisms/LabwarePositionCheck/steps/CheckItem/PlaceItemInstruction.tsx diff --git a/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx b/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx index 61864daf358..387b4b6700c 100644 --- a/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx +++ b/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx @@ -1,6 +1,5 @@ import styled, { css } from 'styled-components' import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' import { AlertPrimaryButton, @@ -20,24 +19,22 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { getIsOnDevice } from '/app/redux/config' import { SmallButton } from '/app/atoms/buttons' import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' export function ExitConfirmation({ commandUtils, + state, }: LPCWizardContentProps): JSX.Element { const { i18n, t } = useTranslation(['labware_position_check', 'shared']) const { confirmExitLPC, cancelExitLPC } = commandUtils - const isOnDevice = useSelector(getIsOnDevice) - return ( - {isOnDevice ? ( + {state.isOnDevice ? ( <> {t('remove_probe_before_exit')} @@ -59,7 +56,7 @@ export function ExitConfirmation({ )} - {isOnDevice ? ( + {state.isOnDevice ? ( { + const labwareId = + 'labwareId' in state.steps.current ? state.steps.current.labwareId : '' + + return ( + state.workingOffsets.find( + o => + o.labwareId === labwareId && + isEqual(o.location, location) && + o.initialPosition != null + )?.initialPosition ?? null + ) +} + +export const selectActiveLwExistingOffset = ( + state: LPCWizardState +): VectorOffset => { + const { existingOffsets, steps } = state + + if ( + !('labwareId' in steps.current) || + !('location' in steps.current) || + !('slotName' in steps.current.location) + ) { + console.warn( + `No labwareId or location in current step: ${steps.current.section}` + ) + return IDENTITY_VECTOR + } else { + const lwUri = getLabwareDefURI( + selectItemLabwareDef(state) as LabwareDefinition2 + ) + + return ( + // @ts-expect-error Typechecking for slotName done above. + getCurrentOffsetForLabwareInLocation(existingOffsets, lwUri, location) + ?.vector ?? IDENTITY_VECTOR + ) + } +} + +export const selectIsActiveLwTipRack = (state: LPCWizardState): boolean => { + if ('labwareId' in state.steps.current) { + return getIsTiprack(selectItemLabwareDef(state) as LabwareDefinition2) + } else { + console.warn('No labwareId in step.') + return false + } +} + +export const selectLwDisplayName = (state: LPCWizardState): string => { + if ('labwareId' in state.steps.current) { + return getLabwareDisplayName( + selectItemLabwareDef(state) as LabwareDefinition2 + ) + } else { + console.warn('No labwareId in step.') + return '' + } +} + +export const selectActiveAdapterDisplayName = ( + state: LPCWizardState +): string => { + const { protocolData, labwareDefs, steps } = state + + return 'adapterId' in steps.current && steps.current.adapterId != null + ? getItemLabwareDef({ + labwareId: steps.current.adapterId, + loadedLabware: protocolData.labware, + labwareDefs, + })?.metadata.displayName ?? '' + : '' +} + +// TOME TODO: getItemLabwareDef should be moved to a general utils place I think. +export const selectItemLabwareDef = createSelector( + (state: LPCWizardState) => state.steps.current, + (state: LPCWizardState) => state.labwareDefs, + (state: LPCWizardState) => state.protocolData.labware, + (current, labwareDefs, loadedLabware) => { + const labwareId = 'labwareId' in current ? current.labwareId : '' + + if (labwareId === '') { + console.warn(`No labwareId associated with step: ${current.section}`) + return null + } + + return getItemLabwareDef({ + labwareId, + labwareDefs, + loadedLabware, + }) + } +) diff --git a/app/src/organisms/LabwarePositionCheck/redux/selectors/pipettes.ts b/app/src/organisms/LabwarePositionCheck/redux/selectors/pipettes.ts new file mode 100644 index 00000000000..7d1de925912 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/redux/selectors/pipettes.ts @@ -0,0 +1,27 @@ +import { getPipetteNameSpecs } from '@opentrons/shared-data' + +import type { LPCWizardState } from '/app/organisms/LabwarePositionCheck/redux' +import type { LoadedPipette, PipetteChannels } from '@opentrons/shared-data' + +export const selectActivePipette = ( + state: LPCWizardState +): LoadedPipette | undefined => { + const { protocolData, steps } = state + const pipetteId = 'pipetteId' in steps.current ? steps.current.pipetteId : '' + + if (pipetteId === '') { + console.warn(`No matching pipette found for pipetteId ${pipetteId}`) + } + + return protocolData.pipettes.find(pipette => pipette.id === pipetteId) +} + +export const selectActivePipetteChannelCount = ( + state: LPCWizardState +): PipetteChannels => { + const pipetteName = selectActivePipette(state)?.pipetteName + + return pipetteName != null + ? getPipetteNameSpecs(pipetteName)?.channels ?? 1 + : 1 +} diff --git a/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx b/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx index a9af9db4528..02c35394e68 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx @@ -8,10 +8,13 @@ import { LegacyStyledText, TYPOGRAPHY, } from '@opentrons/components' -import { getPipetteNameSpecs } from '@opentrons/shared-data' import { ProbeNotAttached } from '/app/organisms/PipetteWizardFlows/ProbeNotAttached' import { GenericWizardTile } from '/app/molecules/GenericWizardTile' +import { + selectActivePipette, + selectActivePipetteChannelCount, +} from '/app/organisms/LabwarePositionCheck/redux' import attachProbe1 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_1.webm' import attachProbe8 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_8.webm' @@ -26,7 +29,7 @@ export function AttachProbe({ step, }: LPCStepProps): JSX.Element { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) - const { protocolData, isOnDevice } = state + const { isOnDevice } = state const { pipetteId } = step const { moveToMaintenancePosition, @@ -34,29 +37,14 @@ export function AttachProbe({ unableToDetect, createProbeAttachmentHandler, } = commandUtils - // TOME TODO: This pipette logic should almost certainly be in a selector, too. - const pipette = protocolData.pipettes.find(p => p.id === pipetteId) - const handleProbeAttached = createProbeAttachmentHandler( - pipetteId, - pipette, - proceed - ) - - // Move into correct position for probe attach on mount - useEffect(() => { - moveToMaintenancePosition(pipette) - }, []) - - // TOME TODO: Maybe a selector here? + const pipette = selectActivePipette(state) const { probeLocation, probeVideoSrc } = ((): { probeLocation: string probeVideoSrc: string } => { - const pipetteName = pipette?.pipetteName - const pipetteChannels = - pipetteName != null ? getPipetteNameSpecs(pipetteName)?.channels ?? 1 : 1 + const channels = selectActivePipetteChannelCount(state) - switch (pipetteChannels) { + switch (channels) { case 1: return { probeLocation: '', probeVideoSrc: attachProbe1 } case 8: @@ -69,6 +57,17 @@ export function AttachProbe({ } })() + const handleProbeAttached = createProbeAttachmentHandler( + pipetteId, + pipette, + proceed + ) + + // Move into correct position for probe attach on mount + useEffect(() => { + moveToMaintenancePosition(pipette) + }, []) + if (unableToDetect) { return ( ) } + +const Title = styled.h1` + ${TYPOGRAPHY.h1Default}; + margin-bottom: ${SPACING.spacing8}; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + ${TYPOGRAPHY.level4HeaderSemiBold}; + margin-bottom: 0; + height: ${SPACING.spacing40}; + display: ${DISPLAY_INLINE_BLOCK}; + } +` + +const TILE_CONTAINER_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + justify-content: ${JUSTIFY_SPACE_BETWEEN}; + padding: ${SPACING.spacing32}; + height: 24.625rem; + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + height: 29.5rem; + } +` diff --git a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/ViewOffsets.tsx b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/ViewOffsets.tsx index 327871ed3b9..c0747e01b24 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/ViewOffsets.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/ViewOffsets.tsx @@ -33,8 +33,11 @@ interface ViewOffsetsProps { export function ViewOffsets(props: ViewOffsetsProps): JSX.Element { const { existingOffsets, labwareDefinitions } = props const { t, i18n } = useTranslation('labware_position_check') + const [showOffsetsTable, setShowOffsetsModal] = useState(false) + const latestCurrentOffsets = getLatestCurrentOffsets(existingOffsets) + return existingOffsets.length > 0 ? ( <> +): JSX.Element { + const { x, y, z, state, ...styleProps } = props const { i18n, t } = useTranslation('labware_position_check') - const isOnDevice = useSelector(getIsOnDevice) return ( - + {[x, y, z].map((axis, index) => ( - {axisLabels[index]} + {['X', 'Y', 'Z'][index]} {axis.toFixed(1)} diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/index.tsx index e13b7d8c897..4cb7817c151 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/index.tsx @@ -23,9 +23,9 @@ import { WELL_LABEL_OPTIONS, } from '@opentrons/components' import { - getIsTiprack, getVectorDifference, getVectorSum, + IDENTITY_VECTOR, } from '@opentrons/shared-data' import { getTopPortalEl } from '/app/App/portal' @@ -33,10 +33,16 @@ import { SmallButton } from '/app/atoms/buttons' import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' import { JogControls } from '/app/molecules/JogControls' import { LiveOffsetValue } from './LiveOffsetValue' +import { + selectActiveLwExistingOffset, + selectActiveLwInitialPosition, + selectActivePipette, + selectIsActiveLwTipRack, + selectItemLabwareDef, +} from '/app/organisms/LabwarePositionCheck/redux' import type { ReactNode } from 'react' -import type { PipetteName, LabwareDefinition2 } from '@opentrons/shared-data' -import type { WellStroke } from '@opentrons/components' +import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { VectorOffset } from '@opentrons/api-client' import type { Jog } from '/app/molecules/JogControls' import type { @@ -54,34 +60,40 @@ const LPC_HELP_LINK_URL = interface JogToWellProps extends LPCStepProps { header: ReactNode body: ReactNode - labwareDef: LabwareDefinition2 - initialPosition: VectorOffset handleConfirmPosition: () => void handleGoBack: () => void handleJog: Jog - existingOffset: VectorOffset - pipetteName: PipetteName } -export function JogToWell({ - header, - body, - pipetteName, - labwareDef, - handleConfirmPosition, - handleGoBack, - handleJog, - initialPosition, - existingOffset, - state, -}: JogToWellProps): JSX.Element { +export function JogToWell(props: JogToWellProps): JSX.Element { + const { + header, + body, + handleConfirmPosition, + handleGoBack, + handleJog, + state, + } = props const { t } = useTranslation(['labware_position_check', 'shared']) const { isOnDevice } = state + const initialPosition = + selectActiveLwInitialPosition(state) ?? IDENTITY_VECTOR + const pipetteName = selectActivePipette(state)?.pipetteName ?? 'p1000_single' + const itemLwDef = selectItemLabwareDef(state) as LabwareDefinition2 // Safe if component only used with CheckItem step. + const isTipRack = selectIsActiveLwTipRack(state) + const [joggedPosition, setJoggedPosition] = useState( initialPosition ) const [showFullJogControls, setShowFullJogControls] = useState(false) + + const levelSrc = isTipRack ? levelProbeWithTip : levelProbeWithLabware + const liveOffset = getVectorSum( + selectActiveLwExistingOffset(state), + getVectorDifference(joggedPosition, initialPosition) + ) + useEffect(() => { // NOTE: this will perform a "null" jog when the jog controls mount so // if a user reaches the "confirm exit" modal (unmounting this component) @@ -97,44 +109,28 @@ export function JogToWell({ } }, []) - // TOME TODO: Steps should honestly should include more details about the instruments used - // or be made into selectors. - - const wellsToHighlight = ['A1'] - const wellStroke: WellStroke = wellsToHighlight.reduce( - (acc, wellName) => ({ ...acc, [wellName]: COLORS.blue50 }), - {} - ) - - const liveOffset = getVectorSum( - existingOffset, - getVectorDifference(joggedPosition, initialPosition) - ) - const isTipRack = getIsTiprack(labwareDef) - const levelSrc = isTipRack ? levelProbeWithTip : levelProbeWithLabware - return (
{header}
{body} - +
{() => ( <> diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PlaceItemInstruction.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PlaceItemInstruction.tsx new file mode 100644 index 00000000000..ad13aa00baa --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PlaceItemInstruction.tsx @@ -0,0 +1,97 @@ +import { Trans, useTranslation } from 'react-i18next' + +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' +import { TYPOGRAPHY, LegacyStyledText } from '@opentrons/components' + +import { + selectActiveAdapterDisplayName, + selectLwDisplayName, +} from '/app/organisms/LabwarePositionCheck/redux' +import { getLabwareDisplayLocation } from '/app/local-resources/labware' + +import type { + CheckPositionsStep, + LPCStepProps, +} from '/app/organisms/LabwarePositionCheck/types' + +interface PlaceItemInstructionProps extends LPCStepProps { + isLwTiprack: boolean + slotOnlyDisplayLocation: string +} + +export function PlaceItemInstruction({ + step, + isLwTiprack, + slotOnlyDisplayLocation, + state, +}: PlaceItemInstructionProps): JSX.Element { + const { t } = useTranslation('labware_position_check') + const { protocolData, labwareDefs } = state + const { location, adapterId } = step + + const labwareDisplayName = selectLwDisplayName(state) + const adapterDisplayName = selectActiveAdapterDisplayName(state) + const displayLocation = getLabwareDisplayLocation({ + location, + allRunDefs: labwareDefs, + detailLevel: 'full', + t, + loadedModules: protocolData.modules, + loadedLabwares: protocolData.labware, + robotType: FLEX_ROBOT_TYPE, + }) + + if (isLwTiprack) { + return ( + + ), + }} + /> + ) + } else if (adapterId != null) { + return ( + + ), + }} + /> + ) + } else { + return ( + + ), + }} + /> + ) + } +} diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PrepareSpace.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PrepareSpace.tsx index a2e0f32a6c3..c87bea5ac45 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PrepareSpace.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PrepareSpace.tsx @@ -30,6 +30,7 @@ import type { CheckPositionsStep, LPCStepProps, } from '/app/organisms/LabwarePositionCheck/types' +import { selectItemLabwareDef } from '/app/organisms/LabwarePositionCheck/redux' const LPC_HELP_LINK_URL = 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' @@ -37,12 +38,10 @@ const LPC_HELP_LINK_URL = interface PrepareSpaceProps extends LPCStepProps { header: ReactNode body: ReactNode - labwareDef: LabwareDefinition2 confirmPlacement: () => void } export function PrepareSpace({ - labwareDef, state, header, body, @@ -51,6 +50,7 @@ export function PrepareSpace({ const { i18n, t } = useTranslation(['labware_position_check', 'shared']) const { protocolData, isOnDevice, deckConfig, steps } = state const { location } = steps.current as CheckPositionsStep // safely enforced by iface + const labwareDef = selectItemLabwareDef(state) as LabwareDefinition2 // CheckItem always has lwId on step. return ( diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx index d00c0a29bca..0fc816d82d9 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx @@ -1,33 +1,24 @@ import { useEffect } from 'react' -import isEqual from 'lodash/isEqual' import { Trans, useTranslation } from 'react-i18next' -import { - DIRECTION_COLUMN, - Flex, - LegacyStyledText, - TYPOGRAPHY, -} from '@opentrons/components' +import { DIRECTION_COLUMN, Flex, LegacyStyledText } from '@opentrons/components' -import { - FLEX_ROBOT_TYPE, - getIsTiprack, - getLabwareDefURI, - getLabwareDisplayName, - IDENTITY_VECTOR, -} from '@opentrons/shared-data' +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { getLabwareDisplayLocation } from '/app/local-resources/labware' import { UnorderedList } from '/app/molecules/UnorderedList' -import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' import { setFinalPosition, setInitialPosition, } from '/app/organisms/LabwarePositionCheck/redux/actions' import { JogToWell } from './JogToWell' import { PrepareSpace } from './PrepareSpace' -import { getItemLabwareDef } from './utils' +import { PlaceItemInstruction } from './PlaceItemInstruction' +import { + selectActiveLwInitialPosition, + selectActivePipette, + selectIsActiveLwTipRack, +} from '/app/organisms/LabwarePositionCheck/redux' -import type { PipetteName, LabwareDefinition2 } from '@opentrons/shared-data' import type { CheckPositionsStep, LPCStepProps, @@ -36,15 +27,8 @@ import type { export function CheckItem( props: LPCStepProps ): JSX.Element { - const { - state, - dispatch, - proceed, - existingOffsets, - commandUtils, - step, - } = props - const { labwareId, pipetteId, moduleId, location } = step + const { state, dispatch, proceed, commandUtils, step } = props + const { labwareId, moduleId, location } = step const { handleJog, handlePrepModules, @@ -52,29 +36,20 @@ export function CheckItem( handleConfirmLwFinalPosition, handleResetLwModulesOnDeck, } = commandUtils - const { workingOffsets, isOnDevice, labwareDefs, protocolData } = state + const { isOnDevice, protocolData } = state const { t } = useTranslation(['labware_position_check', 'shared']) - // TOME TODO: Pretty mcuh all of this goes into selectors, and a lot of it - // should go into JogToWell as well. - - const itemLabwareDef = getItemLabwareDef({ - labwareId, - loadedLabware: protocolData.labware, - labwareDefs, + const pipette = selectActivePipette(state) + const initialPosition = selectActiveLwInitialPosition(state) + const isLwTiprack = selectIsActiveLwTipRack(state) + const slotOnlyDisplayLocation = getLabwareDisplayLocation({ + location, + detailLevel: 'slot-only', + t, + loadedModules: protocolData.modules, + loadedLabwares: protocolData.labware, + robotType: FLEX_ROBOT_TYPE, }) - const pipette = protocolData.pipettes.find( - pipette => pipette.id === pipetteId - ) - - const pipetteName = pipette?.pipetteName as PipetteName - - const initialPosition = workingOffsets.find( - o => - o.labwareId === labwareId && - isEqual(o.location, location) && - o.initialPosition != null - )?.initialPosition useEffect(() => { handlePrepModules({ step, initialPosition }) @@ -120,23 +95,6 @@ export function CheckItem( }) } - const isLwTiprack = getIsTiprack(itemLabwareDef) - const slotOnlyDisplayLocation = getLabwareDisplayLocation({ - location, - detailLevel: 'slot-only', - t, - loadedModules: protocolData.modules, - loadedLabwares: protocolData.labware, - robotType: FLEX_ROBOT_TYPE, - }) - - const existingOffset = - getCurrentOffsetForLabwareInLocation( - existingOffsets, - getLabwareDefURI(itemLabwareDef) as string, - location - )?.vector ?? IDENTITY_VECTOR - return ( {initialPosition != null ? ( @@ -165,13 +123,9 @@ export function CheckItem( }} /> } - labwareDef={itemLabwareDef} - pipetteName={pipetteName} handleConfirmPosition={handleDispatchConfirmFinalPlacement} handleGoBack={handleDispatchResetLwModulesOnDeck} handleJog={handleJog} - initialPosition={initialPosition} - existingOffset={existingOffset} {...props} /> ) : ( @@ -186,7 +140,6 @@ export function CheckItem( isOnDevice ? t('clear_all_slots_odd') : t('clear_all_slots'), } - labwareDef={itemLabwareDef} confirmPlacement={handleDispatchConfirmInitialPlacement} {...props} /> @@ -202,95 +154,3 @@ export function CheckItem( ) } - -interface PlaceItemInstructionProps extends LPCStepProps { - itemLabwareDef: LabwareDefinition2 - isLwTiprack: boolean - slotOnlyDisplayLocation: string -} - -function PlaceItemInstruction({ - step, - itemLabwareDef, - isLwTiprack, - slotOnlyDisplayLocation, - state, -}: PlaceItemInstructionProps): JSX.Element { - const { t } = useTranslation('labware_position_check') - const { protocolData, labwareDefs } = state - const { location, adapterId } = step - const labwareDisplayName = getLabwareDisplayName(itemLabwareDef) - - const displayLocation = getLabwareDisplayLocation({ - location, - allRunDefs: labwareDefs, - detailLevel: 'full', - t, - loadedModules: protocolData.modules, - loadedLabwares: protocolData.labware, - robotType: FLEX_ROBOT_TYPE, - }) - - const adapterDisplayName = - adapterId != null - ? getItemLabwareDef({ - labwareId: adapterId, - loadedLabware: protocolData.labware, - labwareDefs, - })?.metadata.displayName - : '' - - if (isLwTiprack) { - return ( - - ), - }} - /> - ) - } else if (adapterId != null) { - return ( - - ), - }} - /> - ) - } else { - return ( - - ), - }} - /> - ) - } -} diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/utils.ts b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/utils.ts index 6a1710be04d..780e5336133 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/utils.ts +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/utils.ts @@ -15,11 +15,15 @@ export function getItemLabwareDef({ labwareId, loadedLabware, labwareDefs, -}: GetLabwareDefsForLPCParams): LabwareDefinition2 { - const labwareDefUri = loadedLabware.find(l => l.id === labwareId) - ?.definitionUri +}: GetLabwareDefsForLPCParams): LabwareDefinition2 | null { + const labwareDefUri = + loadedLabware.find(l => l.id === labwareId)?.definitionUri ?? null - return labwareDefs.find( - def => getLabwareDefURI(def) === labwareDefUri - ) as LabwareDefinition2 + if (labwareDefUri == null) { + console.warn(`Null labware def found for labwareId: ${labwareId}`) + } + + return ( + labwareDefs.find(def => getLabwareDefURI(def) === labwareDefUri) ?? null + ) } diff --git a/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx index 626b8088993..3222a6cd080 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx @@ -8,9 +8,12 @@ import { SPACING, TYPOGRAPHY, } from '@opentrons/components' -import { getPipetteNameSpecs } from '@opentrons/shared-data' import { GenericWizardTile } from '/app/molecules/GenericWizardTile' +import { + selectActivePipette, + selectActivePipetteChannelCount, +} from '/app/organisms/LabwarePositionCheck/redux' import detachProbe1 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_1.webm' import detachProbe8 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_8.webm' @@ -20,28 +23,27 @@ import type { DetachProbeStep, LPCStepProps } from '../types' export const DetachProbe = ({ state, - step, proceed, commandUtils, }: LPCStepProps): JSX.Element => { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) - const { protocolData } = state const { moveToMaintenancePosition, createProbeDetachmentHandler, } = commandUtils + const pipette = selectActivePipette(state) + const probeVideoSrc = ((): string => { + const channels = selectActivePipetteChannelCount(state) - // TOME: TODO: Yeah, same sort of selector here. - const pipette = protocolData.pipettes.find(p => p.id === step.pipetteId) - const pipetteName = pipette?.pipetteName - const pipetteChannels = - pipetteName != null ? getPipetteNameSpecs(pipetteName)?.channels ?? 1 : 1 - let probeVideoSrc = detachProbe1 - if (pipetteChannels === 8) { - probeVideoSrc = detachProbe8 - } else if (pipetteChannels === 96) { - probeVideoSrc = detachProbe96 - } + switch (channels) { + case 1: + return detachProbe1 + case 8: + return detachProbe8 + case 96: + return detachProbe96 + } + })() const handleProbeDetached = createProbeDetachmentHandler(pipette, proceed) diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx index 7b0a7c0d281..869cb6a79f8 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx @@ -3,12 +3,7 @@ import styled from 'styled-components' import isEqual from 'lodash/isEqual' import { useTranslation } from 'react-i18next' -import { - FLEX_ROBOT_TYPE, - getLabwareDefURI, - getLabwareDisplayName, - IDENTITY_VECTOR, -} from '@opentrons/shared-data' +import { FLEX_ROBOT_TYPE, IDENTITY_VECTOR } from '@opentrons/shared-data' import { BORDERS, COLORS, @@ -18,6 +13,7 @@ import { TYPOGRAPHY, } from '@opentrons/components' +import { selectLwDisplayName } from '/app/organisms/LabwarePositionCheck/redux' import { getLabwareDisplayLocation } from '/app/local-resources/labware' import type { LabwareDefinition2 } from '@opentrons/shared-data' @@ -37,9 +33,10 @@ export function OffsetTable({ labwareDefinitions, state, }: OffsetTableProps): JSX.Element { - const { t } = useTranslation('labware_position_check') const { protocolData } = state + const { t } = useTranslation('labware_position_check') + return (
@@ -51,7 +48,7 @@ export function OffsetTable({ - {offsets.map(({ location, definitionUri, vector }, index) => { + {offsets.map(({ location, vector }, index) => { const displayLocation = getLabwareDisplayLocation({ location, allRunDefs: labwareDefinitions, @@ -62,12 +59,6 @@ export function OffsetTable({ robotType: FLEX_ROBOT_TYPE, }) - const labwareDef = labwareDefinitions.find( - def => getLabwareDefURI(def) === definitionUri - ) - const labwareDisplayName = - labwareDef != null ? getLabwareDisplayName(labwareDef) : '' - return ( - {labwareDisplayName} + + {selectLwDisplayName(state)} + { return workingOffsets.map( ({ initialPosition, finalPosition, labwareId, location }) => { From a36ef27adff5457d46b774456ecf734bdce2fe3d Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Mon, 13 Jan 2025 17:14:41 -0500 Subject: [PATCH 25/33] refactor(app): add an error-handled build finalized offsets selector --- .../LPCFlows/useLPCFlows.ts | 1 - .../LabwarePositionCheck/constants.ts | 7 +++ .../LabwarePositionCheck/constants/index.ts | 1 - .../LabwarePositionCheck/constants/routing.ts | 19 ------- .../hooks/useLPCCommands/index.ts | 9 ++- .../useLPCCommands/useApplyLPCOffsets.ts | 9 ++- .../useLPCCommands/useBuildOffsetsToApply.ts | 31 ++++++++++ .../redux/selectors/labware.ts | 43 ++++++++++++++ .../CheckItem/JogToWell/LiveOffsetValue.tsx | 2 + .../steps/ResultsSummary/index.tsx | 57 +++---------------- 10 files changed, 105 insertions(+), 74 deletions(-) create mode 100644 app/src/organisms/LabwarePositionCheck/constants.ts delete mode 100644 app/src/organisms/LabwarePositionCheck/constants/index.ts delete mode 100644 app/src/organisms/LabwarePositionCheck/constants/routing.ts create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useBuildOffsetsToApply.ts diff --git a/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts b/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts index 0c0d0bc4501..a4ce3e08aa2 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts +++ b/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts @@ -62,7 +62,6 @@ export function useLPCFlows({ } = useCreateMaintenanceRunLabwareDefinitionMutation() const { deleteMaintenanceRun } = useDeleteMaintenanceRunMutation() useRunLoadedLabwareDefinitions(runId, { - // TOME TODO: Ideally we don't have to do this POST, since the server has the defs already? onSuccess: res => { void Promise.all( res.data.map(def => { diff --git a/app/src/organisms/LabwarePositionCheck/constants.ts b/app/src/organisms/LabwarePositionCheck/constants.ts new file mode 100644 index 00000000000..9ccd9b81eef --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/constants.ts @@ -0,0 +1,7 @@ +export const NAV_STEPS = { + BEFORE_BEGINNING: 'BEFORE_BEGINNING', + ATTACH_PROBE: 'ATTACH_PROBE', + CHECK_POSITIONS: 'CHECK_POSITIONS', + DETACH_PROBE: 'DETACH_PROBE', + RESULTS_SUMMARY: 'RESULTS_SUMMARY', +} as const diff --git a/app/src/organisms/LabwarePositionCheck/constants/index.ts b/app/src/organisms/LabwarePositionCheck/constants/index.ts deleted file mode 100644 index d6f2f62a6d5..00000000000 --- a/app/src/organisms/LabwarePositionCheck/constants/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './routing' diff --git a/app/src/organisms/LabwarePositionCheck/constants/routing.ts b/app/src/organisms/LabwarePositionCheck/constants/routing.ts deleted file mode 100644 index 09f7a5a602e..00000000000 --- a/app/src/organisms/LabwarePositionCheck/constants/routing.ts +++ /dev/null @@ -1,19 +0,0 @@ -export const NAV_STEPS = { - BEFORE_BEGINNING: 'BEFORE_BEGINNING', - ATTACH_PROBE: 'ATTACH_PROBE', - CHECK_POSITIONS: 'CHECK_POSITIONS', - DETACH_PROBE: 'DETACH_PROBE', - RESULTS_SUMMARY: 'RESULTS_SUMMARY', -} as const - -export const NAV_MOTION = { - IN_MOTION: 'IN_MOTION', -} - -// For errors, door open CTAs, etc. -export const NAV_ALERTS = {} - -// TOME: TODO: Can we separate the flex steps from the OT-2 steps? Yeah definitely, since there's no tip rack checking in Flex. -export const NAV_STEPS_FLEX: Array = [] - -export const NAV_STEPS_OT2: Array = [] diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts index 7c1fa987a1b..c75ff44965a 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts @@ -9,7 +9,8 @@ import { useHandleStartLPC } from './useHandleStartLPC' import { useHandlePrepModules } from './useHandlePrepModules' import { useHandleConfirmLwModulePlacement } from './useHandleConfirmLwModulePlacement' import { useHandleConfirmLwFinalPosition } from './useHandleConfirmLwFinalPosition' -import { useHandleResetLwModulesOnDeck } from '/app/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleResetLwModulesOnDeck' +import { useHandleResetLwModulesOnDeck } from './useHandleResetLwModulesOnDeck' +import { useBuildOffsetsToApply } from './useBuildOffsetsToApply' import type { CreateCommand } from '@opentrons/shared-data' import type { CommandData } from '@opentrons/api-client' @@ -24,6 +25,7 @@ import type { UseHandleConfirmPositionResult } from './useHandleConfirmLwFinalPo import type { UseHandleResetLwModulesOnDeckResult } from './useHandleResetLwModulesOnDeck' import type { LPCWizardFlexProps } from '/app/organisms/LabwarePositionCheck/LPCWizardFlex' import type { LPCWizardState } from '/app/organisms/LabwarePositionCheck/redux' +import type { UseBuildOffsetsToApplyResult } from './useBuildOffsetsToApply' export interface UseLPCCommandsProps extends LPCWizardFlexProps { state: LPCWizardState @@ -37,6 +39,7 @@ export type UseLPCCommandsResult = UseApplyLPCOffsetsResult & UseHandlePrepModulesResult & UseHandleConfirmPlacementResult & UseHandleConfirmPositionResult & + UseBuildOffsetsToApplyResult & UseHandleResetLwModulesOnDeckResult & { errorMessage: string | null isRobotMoving: boolean @@ -66,7 +69,8 @@ export function useLPCCommands( return Promise.resolve([]) }) - const applyLPCOffsetsUtils = useApplyLPCOffsets(props) + const applyLPCOffsetsUtils = useApplyLPCOffsets({ ...props, setErrorMessage }) + const buildLPCOffsets = useBuildOffsetsToApply({ ...props, setErrorMessage }) const handleJogUtils = useHandleJog({ ...props, setErrorMessage }) const handleConditionalCleanupUtils = useHandleConditionalCleanup(props) const handleProbeCommands = useHandleProbeCommands({ @@ -94,6 +98,7 @@ export function useLPCCommands( errorMessage, isRobotMoving, ...applyLPCOffsetsUtils, + ...buildLPCOffsets, ...handleJogUtils, ...handleConditionalCleanupUtils, ...handleProbeCommands, diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts index 4c39061ef19..f42c786d3e1 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts @@ -5,6 +5,10 @@ import { useCreateLabwareOffsetMutation } from '@opentrons/react-api-client' import type { LabwareOffsetCreateData } from '@opentrons/api-client' import type { UseLPCCommandChildProps } from './types' +export interface UseApplyLPCOffsetsProps extends UseLPCCommandChildProps { + setErrorMessage: (msg: string | null) => void +} + export interface UseApplyLPCOffsetsResult { handleApplyOffsets: (offsets: LabwareOffsetCreateData[]) => void isApplyingOffsets: boolean @@ -13,7 +17,8 @@ export interface UseApplyLPCOffsetsResult { export function useApplyLPCOffsets({ onCloseClick, runId, -}: UseLPCCommandChildProps): UseApplyLPCOffsetsResult { + setErrorMessage, +}: UseApplyLPCOffsetsProps): UseApplyLPCOffsetsResult { const [isApplyingOffsets, setIsApplyingOffsets] = useState(false) const { createLabwareOffset } = useCreateLabwareOffsetMutation() @@ -26,7 +31,7 @@ export function useApplyLPCOffsets({ setIsApplyingOffsets(false) }) .catch((e: Error) => { - throw new Error(`error applying labware offsets: ${e.message}`) + setErrorMessage(`Error applying labware offsets: ${e.message}`) }) } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useBuildOffsetsToApply.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useBuildOffsetsToApply.ts new file mode 100644 index 00000000000..b0ce243b41d --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useBuildOffsetsToApply.ts @@ -0,0 +1,31 @@ +import type { LabwareOffsetCreateData } from '@opentrons/api-client' +import type { UseLPCCommandChildProps } from '/app/organisms/LabwarePositionCheck/hooks/useLPCCommands/types' +import { selectOffsetsToApply } from '/app/organisms/LabwarePositionCheck/redux' + +export interface UseBuildOffsetsToApplyResult { + buildOffsetsToApply: () => LabwareOffsetCreateData[] +} + +export interface UseApplyLPCOffsetsProps extends UseLPCCommandChildProps { + setErrorMessage: (msg: string | null) => void +} + +export function useBuildOffsetsToApply({ + state, + setErrorMessage, +}: UseApplyLPCOffsetsProps): UseBuildOffsetsToApplyResult { + return { + buildOffsetsToApply: () => { + try { + return selectOffsetsToApply(state) + } catch (e) { + if (e instanceof Error) { + setErrorMessage(e.message) + } else { + setErrorMessage('Failed to create finalized labware offsets.') + } + return [] + } + }, + } +} diff --git a/app/src/organisms/LabwarePositionCheck/redux/selectors/labware.ts b/app/src/organisms/LabwarePositionCheck/redux/selectors/labware.ts index 295e8e93196..b769ece2fab 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/selectors/labware.ts +++ b/app/src/organisms/LabwarePositionCheck/redux/selectors/labware.ts @@ -5,6 +5,8 @@ import { getIsTiprack, getLabwareDisplayName, getLabwareDefURI, + getVectorSum, + getVectorDifference, IDENTITY_VECTOR, } from '@opentrons/shared-data' @@ -60,6 +62,47 @@ export const selectActiveLwExistingOffset = ( } } +export const selectOffsetsToApply = createSelector( + (state: LPCWizardState) => state.workingOffsets, + (state: LPCWizardState) => state.protocolData, + (state: LPCWizardState) => state.existingOffsets, + (workingOffsets, protocolData, existingOffsets) => { + return workingOffsets.map( + ({ initialPosition, finalPosition, labwareId, location }) => { + const definitionUri = + protocolData.labware.find(l => l.id === labwareId)?.definitionUri ?? + null + + if ( + finalPosition == null || + initialPosition == null || + definitionUri == null + ) { + throw new Error( + `cannot create offset for labware with id ${labwareId}, in location ${JSON.stringify( + location + )}, with initial position ${String( + initialPosition + )}, and final position ${String(finalPosition)}` + ) + } else { + const existingOffset = + getCurrentOffsetForLabwareInLocation( + existingOffsets, + definitionUri, + location + )?.vector ?? IDENTITY_VECTOR + const vector = getVectorSum( + existingOffset, + getVectorDifference(finalPosition, initialPosition) + ) + return { definitionUri, location, vector } + } + } + ) + } +) + export const selectIsActiveLwTipRack = (state: LPCWizardState): boolean => { if ('labwareId' in state.steps.current) { return getIsTiprack(selectItemLabwareDef(state) as LabwareDefinition2) diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/LiveOffsetValue.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/LiveOffsetValue.tsx index 8cfc4c372e7..f8154e4f921 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/LiveOffsetValue.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/LiveOffsetValue.tsx @@ -32,6 +32,8 @@ export function LiveOffsetValue( const { x, y, z, state, ...styleProps } = props const { i18n, t } = useTranslation('labware_position_check') + // TOME TODO: Pull these out into their own CSS styles. + return ( ): JSX.Element { - const { existingOffsets, commandUtils, state } = props - const { protocolData, isOnDevice, workingOffsets } = state - const { isApplyingOffsets, handleApplyOffsets } = commandUtils + const { commandUtils, state } = props + const { protocolData, isOnDevice } = state + const { + isApplyingOffsets, + handleApplyOffsets, + buildOffsetsToApply, + } = commandUtils const { i18n, t } = useTranslation('labware_position_check') + const offsetsToApply = buildOffsetsToApply() const isLabwareOffsetCodeSnippetsOn = useSelector( getIsLabwareOffsetCodeSnippetsOn ) - // TOME: TODO: I believe this should be in a utility fn. - // The one tricky thing here is handling the error, because you want to bubble this. - // Just make this a command AND a selector. You have access to state. - const offsetsToApply = useMemo(() => { - return workingOffsets.map( - ({ initialPosition, finalPosition, labwareId, location }) => { - const definitionUri = - protocolData.labware.find(l => l.id === labwareId)?.definitionUri ?? - null - if ( - finalPosition == null || - initialPosition == null || - definitionUri == null - ) { - throw new Error( - `cannot create offset for labware with id ${labwareId}, in location ${JSON.stringify( - location - )}, with initial position ${String( - initialPosition - )}, and final position ${String(finalPosition)}` - ) - } - - const existingOffset = - getCurrentOffsetForLabwareInLocation( - existingOffsets, - definitionUri, - location - )?.vector ?? IDENTITY_VECTOR - const vector = getVectorSum( - existingOffset, - getVectorDifference(finalPosition, initialPosition) - ) - return { definitionUri, location, vector } - } - ) - }, [workingOffsets]) - return ( From 8be24d999db8533aedfc52780b4ab6e098ab08f7 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Tue, 14 Jan 2025 12:17:51 -0500 Subject: [PATCH 26/33] clean up --- .../LPCFlows/useLPCFlows.ts | 17 +++-- .../LabwarePositionCheck/LPCWizardFlex.tsx | 6 +- .../hooks/useLPCCommands/commands/pipettes.ts | 13 +++- .../hooks/useLPCCommands/index.ts | 20 ++++-- .../hooks/useLPCCommands/types.ts | 3 +- .../hooks/useLPCCommands/useHandleJog.ts | 11 ++-- .../useLPCCommands/useHandlePrepModules.ts | 40 +++++++----- .../useLPCCommands/useHandleProbeCommands.ts | 16 +---- .../hooks/useLPCCommands/useHandleStartLPC.ts | 8 ++- ...useHandleValidMoveToMaintenancePosition.ts | 30 +++++++++ .../hooks/useLPCInitialState/index.ts | 3 +- .../getLPCSteps/getProbeBasedLPCSteps.ts | 2 +- .../LabwarePositionCheck/redux/reducer.ts | 7 ++- .../redux/selectors/labware.ts | 41 +++++++----- .../redux/selectors/pipettes.ts | 9 ++- .../LabwarePositionCheck/redux/types.ts | 1 + .../steps/AttachProbe.tsx | 32 +++++----- .../steps/BeforeBeginning/index.tsx | 3 +- .../CheckItem/JogToWell/LiveOffsetValue.tsx | 44 +++++++------ .../steps/CheckItem/JogToWell/index.tsx | 8 ++- .../steps/CheckItem/PlaceItemInstruction.tsx | 26 ++++---- .../steps/CheckItem/index.tsx | 62 ++++++++++++------- .../steps/DetachProbe.tsx | 16 ++--- .../LabwarePositionCheck/types/content.ts | 4 -- .../LabwarePositionCheck/types/steps.ts | 2 - .../{steps/CheckItem => }/utils.ts | 0 26 files changed, 256 insertions(+), 168 deletions(-) create mode 100644 app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts rename app/src/organisms/LabwarePositionCheck/{steps/CheckItem => }/utils.ts (100%) diff --git a/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts b/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts index a4ce3e08aa2..24f39a0cf74 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts +++ b/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts @@ -61,6 +61,8 @@ export function useLPCFlows({ createLabwareDefinition, } = useCreateMaintenanceRunLabwareDefinitionMutation() const { deleteMaintenanceRun } = useDeleteMaintenanceRunMutation() + // TODO(jh, 01-14-25): There's no external error handing if LPC fails this series of POST requests. + // If the server doesn't absorb this functionality for the redesign, add error handling. useRunLoadedLabwareDefinitions(runId, { onSuccess: res => { void Promise.all( @@ -77,7 +79,6 @@ export function useLPCFlows({ }) }, onSettled: () => { - // TOME TODO: Think about potentially error handling if there's some sort of failure here? setIsLaunching(false) }, enabled: maintenanceRunId != null, @@ -102,7 +103,7 @@ export function useLPCFlows({ const handleCloseLPC = (): void => { if (maintenanceRunId != null) { deleteMaintenanceRun(maintenanceRunId, { - onSettled: () => { + onSuccess: () => { setMaintenanceRunId(null) setHasCreatedLPCRun(false) }, @@ -115,6 +116,11 @@ export function useLPCFlows({ maintenanceRunId != null && protocolName != null && mostRecentAnalysis != null + console.log('=>(useLPCFlows.ts:119) mostRecentAnalysis', mostRecentAnalysis) + console.log('=>(useLPCFlows.ts:119) protocolName', protocolName) + console.log('=>(useLPCFlows.ts:119) maintenanceRunId', maintenanceRunId) + console.log('=>(useLPCFlows.ts:119) hasCreatedLPCRun', hasCreatedLPCRun) + console.log('=>(useLPCFlows.ts:119) showLPC', showLPC) return showLPC ? { @@ -159,13 +165,14 @@ function useMonitorMaintenanceRunForDeletion({ }) useEffect(() => { - if ( + if (maintenanceRunId === null) { + setMonitorMaintenanceRunForDeletion(false) + } else if ( maintenanceRunId !== null && maintenanceRunData?.data.id === maintenanceRunId ) { setMonitorMaintenanceRunForDeletion(true) - } - if ( + } else if ( maintenanceRunData?.data.id !== maintenanceRunId && monitorMaintenanceRunForDeletion ) { diff --git a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx index b6880fd9c3d..51ccf6870a8 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx @@ -36,7 +36,7 @@ export function LPCWizardFlex(props: LPCWizardFlexProps): JSX.Element { const LPCHandlerUtils = useLPCCommands({ ...props, state }) - // TOME TODO: Confirm Go back functionality works if we have it? + // TODO(jh, 01-14-25): Also inject goBack functionality once designs are finalized. const proceed = (): void => { dispatch(proceedStep()) } @@ -84,7 +84,7 @@ function LPCWizardHeader({ return ( diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/pipettes.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/pipettes.ts index 3bf1291b149..feb66548e55 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/pipettes.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/pipettes.ts @@ -1,12 +1,17 @@ import { fullHomeCommands } from './gantry' +import { selectActivePipette } from '/app/organisms/LabwarePositionCheck/redux' import type { CreateCommand, LoadedPipette, MotorAxes, } from '@opentrons/shared-data' -import type { CheckPositionsStep } from '/app/organisms/LabwarePositionCheck/types' +import type { + CheckPositionsStep, + LabwarePositionCheckStep, +} from '/app/organisms/LabwarePositionCheck/types' import type { Axis, Sign, StepSize } from '/app/molecules/JogControls/types' +import type { LPCWizardState } from '/app/organisms/LabwarePositionCheck/redux' const PROBE_LENGTH_MM = 44.5 @@ -99,9 +104,11 @@ export const moveRelativeCommand = ({ params: { pipetteId, distance: step * dir, axis }, }) -export const moveToMaintenancePositionCommands = ( - pipette: LoadedPipette | undefined +export const moveToMaintenancePosition = ( + step: LabwarePositionCheckStep, + state: LPCWizardState ): CreateCommand[] => { + const pipette = selectActivePipette(step, state) const pipetteMount = pipette?.mount return [ diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts index c75ff44965a..f9a5159b5a1 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts @@ -11,6 +11,7 @@ import { useHandleConfirmLwModulePlacement } from './useHandleConfirmLwModulePla import { useHandleConfirmLwFinalPosition } from './useHandleConfirmLwFinalPosition' import { useHandleResetLwModulesOnDeck } from './useHandleResetLwModulesOnDeck' import { useBuildOffsetsToApply } from './useBuildOffsetsToApply' +import { useHandleValidMoveToMaintenancePosition } from './useHandleValidMoveToMaintenancePosition' import type { CreateCommand } from '@opentrons/shared-data' import type { CommandData } from '@opentrons/api-client' @@ -26,6 +27,7 @@ import type { UseHandleResetLwModulesOnDeckResult } from './useHandleResetLwModu import type { LPCWizardFlexProps } from '/app/organisms/LabwarePositionCheck/LPCWizardFlex' import type { LPCWizardState } from '/app/organisms/LabwarePositionCheck/redux' import type { UseBuildOffsetsToApplyResult } from './useBuildOffsetsToApply' +import type { UseHandleValidMoveToMaintenancePositionResult } from './useHandleValidMoveToMaintenancePosition' export interface UseLPCCommandsProps extends LPCWizardFlexProps { state: LPCWizardState @@ -40,7 +42,8 @@ export type UseLPCCommandsResult = UseApplyLPCOffsetsResult & UseHandleConfirmPlacementResult & UseHandleConfirmPositionResult & UseBuildOffsetsToApplyResult & - UseHandleResetLwModulesOnDeckResult & { + UseHandleResetLwModulesOnDeckResult & + UseHandleValidMoveToMaintenancePositionResult & { errorMessage: string | null isRobotMoving: boolean } @@ -58,15 +61,20 @@ export function useLPCCommands( const chainLPCCommands = ( commands: CreateCommand[], - continuePastCommandFailure: boolean + continuePastCommandFailure: boolean, + shouldPropogateError?: boolean // Let a higher level handler manage the error. ): Promise => chainRunCommands( props.maintenanceRunId, commands, continuePastCommandFailure ).catch((e: Error) => { - setErrorMessage(`Error during LPC command: ${e.message}`) - return Promise.resolve([]) + if (!shouldPropogateError) { + setErrorMessage(`Error during LPC command: ${e.message}`) + return Promise.resolve([]) + } else { + return Promise.reject(e) + } }) const applyLPCOffsetsUtils = useApplyLPCOffsets({ ...props, setErrorMessage }) @@ -93,6 +101,9 @@ export function useLPCCommands( ...props, chainLPCCommands, }) + const handleValidMoveToMaintenancePosition = useHandleValidMoveToMaintenancePosition( + { ...props, chainLPCCommands } + ) return { errorMessage, @@ -107,5 +118,6 @@ export function useLPCCommands( ...handleConfirmLwModulePlacement, ...handleConfirmLwFinalPosition, ...handleResetLwModulesOnDeck, + ...handleValidMoveToMaintenancePosition, } } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/types.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/types.ts index e9907553f66..58590bfcbca 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/types.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/types.ts @@ -8,6 +8,7 @@ export interface UseLPCCommandWithChainRunChildProps extends UseLPCCommandChildProps { chainLPCCommands: ( commands: CreateCommand[], - continuePastCommandFailure: boolean + continuePastCommandFailure: boolean, + shouldPropogateError?: boolean ) => Promise } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts index fdfa5ac6e4a..b95f53caa2c 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts @@ -24,7 +24,6 @@ export interface UseHandleJogResult { handleJog: Jog } -// TODO(jh, 01-06-25): This debounced jog logic is used elsewhere in the app, ex, Drop tip wizard. We should consolidate it. export function useHandleJog({ maintenanceRunId, state, @@ -45,7 +44,7 @@ export function useHandleJog({ step: StepSize, onSuccess?: (position: Coordinates | null) => void ): Promise => { - return new Promise(() => { + return new Promise((resolve, reject) => { const pipetteId = 'pipetteId' in currentStep ? currentStep.pipetteId : null @@ -60,18 +59,22 @@ export function useHandleJog({ onSuccess?.( (data?.data?.result?.position ?? null) as Coordinates | null ) + resolve() }) .catch((e: Error) => { setErrorMessage(`Error issuing jog command: ${e.message}`) + reject(e) }) } else { - setErrorMessage( + const error = new Error( `Could not find pipette to jog with id: ${pipetteId ?? ''}` ) + setErrorMessage(error.message) + reject(error) } }) }, - [currentStep, maintenanceRunId] + [currentStep, maintenanceRunId, createSilentCommand, setErrorMessage] ) const processJogQueue = useCallback((): void => { diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts index acc18b4d08d..18853110dc4 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts @@ -1,32 +1,42 @@ import { modulePrepCommands } from './commands' +import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' +import { selectActiveLwInitialPosition } from '/app/organisms/LabwarePositionCheck/redux' -import type { VectorOffset } from '@opentrons/api-client' import type { CreateCommand } from '@opentrons/shared-data' import type { UseLPCCommandWithChainRunChildProps } from './types' -import type { BuildModulePrepCommandsParams } from './commands' - -interface HandlePrepModulesParams extends BuildModulePrepCommandsParams { - initialPosition: VectorOffset | null -} +import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' export interface UseHandlePrepModulesResult { - handlePrepModules: (params: HandlePrepModulesParams) => void + handleCheckItemsPrepModules: (step: LabwarePositionCheckStep | null) => void } // Prep module(s) before LPCing a specific labware involving module(s). export function useHandlePrepModules({ chainLPCCommands, + state, }: UseLPCCommandWithChainRunChildProps): UseHandlePrepModulesResult { - const handlePrepModules = ({ - initialPosition, - ...rest - }: HandlePrepModulesParams): void => { - const prepCommands: CreateCommand[] = modulePrepCommands(rest) + const handleCheckItemsPrepModules = ( + step: LabwarePositionCheckStep | null + ): void => { + const initialPosition = selectActiveLwInitialPosition(step, state) + + if (step?.section === NAV_STEPS.CHECK_POSITIONS) { + const prepCommands: CreateCommand[] = modulePrepCommands({ + step, + }) - if (initialPosition == null && prepCommands.length > 0) { - void chainLPCCommands(prepCommands, false) + if ( + initialPosition == null && + // Only run these commands during the appropriate step. + step.section === NAV_STEPS.CHECK_POSITIONS && + prepCommands.length > 0 + ) { + void chainLPCCommands(prepCommands, false) + } + } else { + console.warn('Cannot prep modules during unsupported step.') } } - return { handlePrepModules } + return { handleCheckItemsPrepModules } } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts index 252ccee4587..319d22c419f 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts @@ -1,7 +1,6 @@ import { useState } from 'react' import { - moveToMaintenancePositionCommands, retractPipetteAxesSequentiallyCommands, verifyProbeAttachmentAndHomeCommands, } from './commands' @@ -10,7 +9,6 @@ import type { CreateCommand, LoadedPipette } from '@opentrons/shared-data' import type { UseLPCCommandWithChainRunChildProps } from './types' export interface UseProbeCommandsResult { - moveToMaintenancePosition: (pipette: LoadedPipette | undefined) => void createProbeAttachmentHandler: ( pipetteId: string, pipette: LoadedPipette | undefined, @@ -29,16 +27,6 @@ export function useHandleProbeCommands({ }: UseLPCCommandWithChainRunChildProps): UseProbeCommandsResult { const [showUnableToDetect, setShowUnableToDetect] = useState(false) - const moveToMaintenancePosition = ( - pipette: LoadedPipette | undefined - ): void => { - const maintenancePositionCommands: CreateCommand[] = [ - ...moveToMaintenancePositionCommands(pipette), - ] - - void chainLPCCommands(maintenancePositionCommands, false) - } - const createProbeAttachmentHandler = ( pipetteId: string, pipette: LoadedPipette | undefined, @@ -49,14 +37,13 @@ export function useHandleProbeCommands({ ] return () => - chainLPCCommands(attachmentCommands, false) + chainLPCCommands(attachmentCommands, false, true) .then(() => { onSuccess() }) .catch(() => { setShowUnableToDetect(true) - // TOME TODO: You probably want to hoist this component out of the step. // Stop propagation to prevent error screen routing. return Promise.resolve() }) @@ -77,7 +64,6 @@ export function useHandleProbeCommands({ } return { - moveToMaintenancePosition, createProbeAttachmentHandler, unableToDetect: showUnableToDetect, setShowUnableToDetect, diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts index 6bcc47fdaa0..adf94d2776a 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts @@ -1,4 +1,8 @@ -import { fullHomeCommands, moduleInitBeforeAnyLPCCommands } from './commands' +import { + fullHomeCommands, + moduleInitBeforeAnyLPCCommands, + moveToMaintenancePosition, +} from './commands' import type { CompletedProtocolAnalysis, @@ -15,12 +19,14 @@ export interface UseHandleStartLPCResult { export function useHandleStartLPC({ chainLPCCommands, mostRecentAnalysis, + state, }: UseLPCCommandWithChainRunChildProps): UseHandleStartLPCResult { const createStartLPCHandler = (onSuccess: () => void): (() => void) => { const startCommands: CreateCommand[] = [ ...buildInstrumentLabwarePrepCommands(mostRecentAnalysis), ...moduleInitBeforeAnyLPCCommands(mostRecentAnalysis), ...fullHomeCommands(), + ...moveToMaintenancePosition(state.steps.current, state), ] return (): void => { diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts new file mode 100644 index 00000000000..893f7054d42 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts @@ -0,0 +1,30 @@ +import { moveToMaintenancePosition } from '/app/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands' +import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' + +import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' +import type { UseLPCCommandWithChainRunChildProps } from '/app/organisms/LabwarePositionCheck/hooks/useLPCCommands/types' + +export interface UseHandleValidMoveToMaintenancePositionResult { + /* Only move to maintenance position during probe steps. */ + handleValidMoveToMaintenancePosition: ( + step: LabwarePositionCheckStep | null + ) => void +} + +export function useHandleValidMoveToMaintenancePosition({ + state, + chainLPCCommands, +}: UseLPCCommandWithChainRunChildProps): UseHandleValidMoveToMaintenancePositionResult { + return { + handleValidMoveToMaintenancePosition: ( + step: LabwarePositionCheckStep | null + ) => { + if ( + step?.section === NAV_STEPS.ATTACH_PROBE || + step?.section === NAV_STEPS.DETACH_PROBE + ) { + void chainLPCCommands(moveToMaintenancePosition(step, state), false) + } + }, + } +} diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/index.ts index d988d0b202b..939d4938e2b 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/index.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/index.ts @@ -35,9 +35,10 @@ export function useLPCInitialState( deckConfig, steps: { currentStepIndex: 0, - totalStepCount: LPCSteps.length - 1, + totalStepCount: LPCSteps.length, current: LPCSteps[0], all: LPCSteps, + next: LPCSteps[1], }, } } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/getLPCSteps/getProbeBasedLPCSteps.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/getLPCSteps/getProbeBasedLPCSteps.ts index 4b6241c4fe2..b6184663762 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/getLPCSteps/getProbeBasedLPCSteps.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/getLPCSteps/getProbeBasedLPCSteps.ts @@ -2,7 +2,7 @@ import { isEqual } from 'lodash' import { getLabwareDefURI, getPipetteNameSpecs } from '@opentrons/shared-data' -import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' +import { NAV_STEPS } from '../../../../constants' import { getLabwareLocationCombos } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' import type { LoadedPipette } from '@opentrons/shared-data' diff --git a/app/src/organisms/LabwarePositionCheck/redux/reducer.ts b/app/src/organisms/LabwarePositionCheck/redux/reducer.ts index 2d2a3ae8d7b..091def56cd3 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/reducer.ts +++ b/app/src/organisms/LabwarePositionCheck/redux/reducer.ts @@ -16,16 +16,21 @@ export function LPCReducer( case PROCEED_STEP: { const { currentStepIndex, totalStepCount } = state.steps const newStepIdx = - currentStepIndex !== totalStepCount + currentStepIndex + 1 < totalStepCount ? currentStepIndex + 1 : currentStepIndex + const nextStepIdx = + newStepIdx + 1 < totalStepCount ? newStepIdx + 1 : null + const nextStep = nextStepIdx != null ? state.steps.all[nextStepIdx] : null + return { ...state, steps: { ...state.steps, currentStepIndex: newStepIdx, current: state.steps.all[newStepIdx], + next: nextStep, }, } } diff --git a/app/src/organisms/LabwarePositionCheck/redux/selectors/labware.ts b/app/src/organisms/LabwarePositionCheck/redux/selectors/labware.ts index b769ece2fab..cf71a3d2ce0 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/selectors/labware.ts +++ b/app/src/organisms/LabwarePositionCheck/redux/selectors/labware.ts @@ -10,29 +10,35 @@ import { IDENTITY_VECTOR, } from '@opentrons/shared-data' -import { getItemLabwareDef } from '/app/organisms/LabwarePositionCheck/steps/CheckItem/utils' +import { getItemLabwareDef } from '/app/organisms/LabwarePositionCheck/utils' +import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' import type { VectorOffset } from '@opentrons/api-client' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { LPCWizardState } from '/app/organisms/LabwarePositionCheck/redux/types' -import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' +import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' // TODO(jh, 01-13-25): Remove the explicit type casting after restructuring "step". export const selectActiveLwInitialPosition = ( + step: LabwarePositionCheckStep | null, state: LPCWizardState ): VectorOffset | null => { - const labwareId = - 'labwareId' in state.steps.current ? state.steps.current.labwareId : '' - - return ( - state.workingOffsets.find( - o => - o.labwareId === labwareId && - isEqual(o.location, location) && - o.initialPosition != null - )?.initialPosition ?? null - ) + if (step != null) { + const labwareId = 'labwareId' in step ? step.labwareId : '' + const location = 'location' in step ? step.location : '' + + return ( + state.workingOffsets.find( + o => + o.labwareId === labwareId && + isEqual(o.location, location) && + o.initialPosition != null + )?.initialPosition ?? null + ) + } else { + return null + } } export const selectActiveLwExistingOffset = ( @@ -55,9 +61,11 @@ export const selectActiveLwExistingOffset = ( ) return ( - // @ts-expect-error Typechecking for slotName done above. - getCurrentOffsetForLabwareInLocation(existingOffsets, lwUri, location) - ?.vector ?? IDENTITY_VECTOR + getCurrentOffsetForLabwareInLocation( + existingOffsets, + lwUri, + steps.current.location + )?.vector ?? IDENTITY_VECTOR ) } } @@ -137,7 +145,6 @@ export const selectActiveAdapterDisplayName = ( : '' } -// TOME TODO: getItemLabwareDef should be moved to a general utils place I think. export const selectItemLabwareDef = createSelector( (state: LPCWizardState) => state.steps.current, (state: LPCWizardState) => state.labwareDefs, diff --git a/app/src/organisms/LabwarePositionCheck/redux/selectors/pipettes.ts b/app/src/organisms/LabwarePositionCheck/redux/selectors/pipettes.ts index 7d1de925912..a56316bca3d 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/selectors/pipettes.ts +++ b/app/src/organisms/LabwarePositionCheck/redux/selectors/pipettes.ts @@ -2,12 +2,14 @@ import { getPipetteNameSpecs } from '@opentrons/shared-data' import type { LPCWizardState } from '/app/organisms/LabwarePositionCheck/redux' import type { LoadedPipette, PipetteChannels } from '@opentrons/shared-data' +import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' export const selectActivePipette = ( + step: LabwarePositionCheckStep, state: LPCWizardState ): LoadedPipette | undefined => { - const { protocolData, steps } = state - const pipetteId = 'pipetteId' in steps.current ? steps.current.pipetteId : '' + const { protocolData } = state + const pipetteId = 'pipetteId' in step ? step.pipetteId : '' if (pipetteId === '') { console.warn(`No matching pipette found for pipetteId ${pipetteId}`) @@ -17,9 +19,10 @@ export const selectActivePipette = ( } export const selectActivePipetteChannelCount = ( + step: LabwarePositionCheckStep, state: LPCWizardState ): PipetteChannels => { - const pipetteName = selectActivePipette(state)?.pipetteName + const pipetteName = selectActivePipette(step, state)?.pipetteName return pipetteName != null ? getPipetteNameSpecs(pipetteName)?.channels ?? 1 diff --git a/app/src/organisms/LabwarePositionCheck/redux/types.ts b/app/src/organisms/LabwarePositionCheck/redux/types.ts index 1ad56bca941..1394db3e8a5 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/types.ts +++ b/app/src/organisms/LabwarePositionCheck/redux/types.ts @@ -39,6 +39,7 @@ interface StepsInfo { currentStepIndex: number totalStepCount: number current: LabwarePositionCheckStep + next: LabwarePositionCheckStep | null all: LabwarePositionCheckStep[] } diff --git a/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx b/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx index 02c35394e68..5ec34b4c53c 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx @@ -1,4 +1,3 @@ -import { useEffect } from 'react' import { Trans, useTranslation } from 'react-i18next' import { css } from 'styled-components' @@ -29,20 +28,26 @@ export function AttachProbe({ step, }: LPCStepProps): JSX.Element { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) - const { isOnDevice } = state + const { isOnDevice, steps } = state const { pipetteId } = step const { - moveToMaintenancePosition, setShowUnableToDetect, unableToDetect, createProbeAttachmentHandler, + handleCheckItemsPrepModules, } = commandUtils - const pipette = selectActivePipette(state) + const pipette = selectActivePipette(step, state) + const handleProbeAttached = createProbeAttachmentHandler( + pipetteId, + pipette, + proceed + ) + const { probeLocation, probeVideoSrc } = ((): { probeLocation: string probeVideoSrc: string } => { - const channels = selectActivePipetteChannelCount(state) + const channels = selectActivePipetteChannelCount(step, state) switch (channels) { case 1: @@ -57,16 +62,11 @@ export function AttachProbe({ } })() - const handleProbeAttached = createProbeAttachmentHandler( - pipetteId, - pipette, - proceed - ) - - // Move into correct position for probe attach on mount - useEffect(() => { - moveToMaintenancePosition(pipette) - }, []) + const handleConfirmProbeAttached = (): void => { + void handleProbeAttached().then(() => { + handleCheckItemsPrepModules(steps.next) + }) + } if (unableToDetect) { return ( @@ -98,7 +98,7 @@ export function AttachProbe({ } proceedButtonText={i18n.format(t('shared:continue'), 'capitalize')} - proceed={handleProbeAttached} + proceed={handleConfirmProbeAttached} /> ) } diff --git a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx index 00f0981010a..bac8dfc467e 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx @@ -23,12 +23,11 @@ const SUPPORT_PAGE_URL = 'https://support.opentrons.com/s/ot2-calibration' export function BeforeBeginning({ proceed, - existingOffsets, commandUtils, state, }: LPCStepProps): JSX.Element { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) - const { isOnDevice, protocolName, labwareDefs } = state + const { isOnDevice, protocolName, labwareDefs, existingOffsets } = state const { createStartLPCHandler } = commandUtils const handleStartLPC = createStartLPCHandler(proceed) diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/LiveOffsetValue.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/LiveOffsetValue.tsx index f8154e4f921..ee3b08e5493 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/LiveOffsetValue.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/LiveOffsetValue.tsx @@ -1,5 +1,6 @@ import { Fragment } from 'react' import { useTranslation } from 'react-i18next' +import { css } from 'styled-components' import { ALIGN_CENTER, @@ -32,14 +33,8 @@ export function LiveOffsetValue( const { x, y, z, state, ...styleProps } = props const { i18n, t } = useTranslation('labware_position_check') - // TOME TODO: Pull these out into their own CSS styles. - return ( - + {i18n.format(t('labware_offset_data'), 'capitalize')} - + {[x, y, z].map((axis, index) => ( - + {['X', 'Y', 'Z'][index]} {axis.toFixed(1)} @@ -75,3 +59,23 @@ export function LiveOffsetValue( ) } + +const FLEX_CONTAINER_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + margin-top: ${SPACING.spacing8}; + margin-bottom: ${SPACING.spacing8}; + grid-gap: ${SPACING.spacing4}; +` + +const OFFSET_CONTAINER_STYLE = css` + align-items: ${ALIGN_CENTER}; + border: ${BORDERS.styleSolid} 1px ${COLORS.grey30}; + border-radius: ${BORDERS.borderRadius4}; + padding: ${SPACING.spacing8}; +` + +const OFFSET_LABEL_STYLE = css` + margin-left: ${SPACING.spacing8}; + margin-right: ${SPACING.spacing4}; + font-weight: ${TYPOGRAPHY.fontWeightSemiBold}; +` diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/index.tsx index 4cb7817c151..69dd0783cf3 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/index.tsx @@ -75,11 +75,13 @@ export function JogToWell(props: JogToWellProps): JSX.Element { state, } = props const { t } = useTranslation(['labware_position_check', 'shared']) - const { isOnDevice } = state + const { isOnDevice, steps } = state + const { current: currentStep } = steps const initialPosition = - selectActiveLwInitialPosition(state) ?? IDENTITY_VECTOR - const pipetteName = selectActivePipette(state)?.pipetteName ?? 'p1000_single' + selectActiveLwInitialPosition(currentStep, state) ?? IDENTITY_VECTOR + const pipetteName = + selectActivePipette(currentStep, state)?.pipetteName ?? 'p1000_single' const itemLwDef = selectItemLabwareDef(state) as LabwareDefinition2 // Safe if component only used with CheckItem step. const isTipRack = selectIsActiveLwTipRack(state) diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PlaceItemInstruction.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PlaceItemInstruction.tsx index ad13aa00baa..7c69fc426e4 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PlaceItemInstruction.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PlaceItemInstruction.tsx @@ -1,13 +1,11 @@ import { Trans, useTranslation } from 'react-i18next' -import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { TYPOGRAPHY, LegacyStyledText } from '@opentrons/components' import { selectActiveAdapterDisplayName, selectLwDisplayName, } from '/app/organisms/LabwarePositionCheck/redux' -import { getLabwareDisplayLocation } from '/app/local-resources/labware' import type { CheckPositionsStep, @@ -17,36 +15,31 @@ import type { interface PlaceItemInstructionProps extends LPCStepProps { isLwTiprack: boolean slotOnlyDisplayLocation: string + fullDisplayLocation: string } export function PlaceItemInstruction({ step, isLwTiprack, slotOnlyDisplayLocation, + fullDisplayLocation, state, }: PlaceItemInstructionProps): JSX.Element { const { t } = useTranslation('labware_position_check') - const { protocolData, labwareDefs } = state - const { location, adapterId } = step + const { adapterId } = step const labwareDisplayName = selectLwDisplayName(state) const adapterDisplayName = selectActiveAdapterDisplayName(state) - const displayLocation = getLabwareDisplayLocation({ - location, - allRunDefs: labwareDefs, - detailLevel: 'full', - t, - loadedModules: protocolData.modules, - loadedLabwares: protocolData.labware, - robotType: FLEX_ROBOT_TYPE, - }) if (isLwTiprack) { return ( ): JSX.Element { const { state, dispatch, proceed, commandUtils, step } = props - const { labwareId, moduleId, location } = step + const { labwareId, location } = step const { handleJog, - handlePrepModules, + handleCheckItemsPrepModules, handleConfirmLwModulePlacement, handleConfirmLwFinalPosition, handleResetLwModulesOnDeck, + handleValidMoveToMaintenancePosition, } = commandUtils - const { isOnDevice, protocolData } = state + const { isOnDevice, protocolData, labwareDefs, steps } = state const { t } = useTranslation(['labware_position_check', 'shared']) + const { t: commandTextT } = useTranslation('protocol_command_text') - const pipette = selectActivePipette(state) - const initialPosition = selectActiveLwInitialPosition(state) + const pipette = selectActivePipette(step, state) + const initialPosition = selectActiveLwInitialPosition(step, state) const isLwTiprack = selectIsActiveLwTipRack(state) - const slotOnlyDisplayLocation = getLabwareDisplayLocation({ - location, - detailLevel: 'slot-only', - t, + + const buildDisplayParams = (): Omit< + DisplayLocationParams, + 'detailLevel' + > => ({ + t: commandTextT, loadedModules: protocolData.modules, loadedLabwares: protocolData.labware, robotType: FLEX_ROBOT_TYPE, + location, }) - useEffect(() => { - handlePrepModules({ step, initialPosition }) - }, [moduleId]) + const slotOnlyDisplayLocation = getLabwareDisplayLocation({ + detailLevel: 'slot-only', + ...buildDisplayParams(), + }) + const fullDisplayLocation = getLabwareDisplayLocation({ + detailLevel: 'full', + allRunDefs: labwareDefs, + ...buildDisplayParams(), + }) const handleDispatchConfirmInitialPlacement = (): void => { void handleConfirmLwModulePlacement({ step }).then(position => { @@ -67,20 +78,28 @@ export function CheckItem( }) } + // TODO(jh, 01-14-25): Revisit next step injection after refactoring the store (after designs settle). const handleDispatchConfirmFinalPlacement = (): void => { void handleConfirmLwFinalPosition({ step, onSuccess: proceed, pipette, - }).then(position => { - dispatch( - setFinalPosition({ - labwareId, - location, - position, - }) - ) }) + .then(position => { + dispatch( + setFinalPosition({ + labwareId, + location, + position, + }) + ) + }) + .then(() => { + handleCheckItemsPrepModules(steps.next) + }) + .then(() => { + handleValidMoveToMaintenancePosition(steps.next) + }) } const handleDispatchResetLwModulesOnDeck = (): void => { @@ -142,6 +161,7 @@ export function CheckItem( key={slotOnlyDisplayLocation} isLwTiprack={isLwTiprack} slotOnlyDisplayLocation={slotOnlyDisplayLocation} + fullDisplayLocation={fullDisplayLocation} {...props} />, ]} diff --git a/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx index 3222a6cd080..66097f53ed3 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx @@ -1,4 +1,3 @@ -import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' @@ -27,13 +26,11 @@ export const DetachProbe = ({ commandUtils, }: LPCStepProps): JSX.Element => { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) - const { - moveToMaintenancePosition, - createProbeDetachmentHandler, - } = commandUtils - const pipette = selectActivePipette(state) + const { current: currentStep } = state.steps + const { createProbeDetachmentHandler } = commandUtils + const pipette = selectActivePipette(currentStep, state) const probeVideoSrc = ((): string => { - const channels = selectActivePipetteChannelCount(state) + const channels = selectActivePipetteChannelCount(currentStep, state) switch (channels) { case 1: @@ -47,11 +44,6 @@ export const DetachProbe = ({ const handleProbeDetached = createProbeDetachmentHandler(pipette, proceed) - useEffect(() => { - // move into correct position for probe detach on mount - moveToMaintenancePosition(pipette) - }, []) - return ( & { proceed: () => void dispatch: Dispatch state: LPCWizardState - // TOME TODO: Consider adding the commands state to the state state. commandUtils: UseLPCCommandsResult - // TOME TODO: This should also be in state - existingOffsets: LabwareOffset[] } diff --git a/app/src/organisms/LabwarePositionCheck/types/steps.ts b/app/src/organisms/LabwarePositionCheck/types/steps.ts index b2270f3a5ec..3cc781aebff 100644 --- a/app/src/organisms/LabwarePositionCheck/types/steps.ts +++ b/app/src/organisms/LabwarePositionCheck/types/steps.ts @@ -25,8 +25,6 @@ export interface PerformLPCStep { moduleId?: string } -// TOME TODO: This all should be in redux. - export interface BeforeBeginningStep { section: typeof NAV_STEPS.BEFORE_BEGINNING } diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/utils.ts b/app/src/organisms/LabwarePositionCheck/utils.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/steps/CheckItem/utils.ts rename to app/src/organisms/LabwarePositionCheck/utils.ts From da584ad0ae21028832aefe1c8d767155f53e8754 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Wed, 15 Jan 2025 10:03:20 -0500 Subject: [PATCH 27/33] refactor(app): promise chain commands Doing so gives us more granular control over when to show robot in motion views. --- .../LabwarePositionCheck/ExitConfirmation.tsx | 12 ++- .../LabwarePositionCheck/LPCErrorModal.tsx | 10 ++- .../LPCFlows/useLPCFlows.ts | 5 -- .../LabwarePositionCheck/LPCWizardFlex.tsx | 4 +- .../hooks/useLPCCommands/index.ts | 12 ++- .../useLPCCommands/useApplyLPCOffsets.ts | 15 +++- .../useLPCCommands/useHandlePrepModules.ts | 15 ++-- .../hooks/useLPCCommands/useHandleStartLPC.ts | 11 +-- ...useHandleValidMoveToMaintenancePosition.ts | 13 +++- .../steps/AttachProbe.tsx | 12 +-- .../steps/BeforeBeginning/index.tsx | 12 ++- .../steps/CheckItem/index.tsx | 78 ++++++++++--------- .../steps/DetachProbe.tsx | 10 ++- .../steps/ResultsSummary/index.tsx | 20 ++--- 14 files changed, 141 insertions(+), 88 deletions(-) diff --git a/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx b/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx index 387b4b6700c..a65c198d2c6 100644 --- a/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx +++ b/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx @@ -28,7 +28,13 @@ export function ExitConfirmation({ state, }: LPCWizardContentProps): JSX.Element { const { i18n, t } = useTranslation(['labware_position_check', 'shared']) - const { confirmExitLPC, cancelExitLPC } = commandUtils + const { confirmExitLPC, cancelExitLPC, toggleRobotMoving } = commandUtils + + const handleConfirmExit = (): void => { + toggleRobotMoving(true).then(() => { + confirmExitLPC() + }) + } return ( @@ -64,7 +70,7 @@ export function ExitConfirmation({ buttonType="secondary" /> @@ -76,7 +82,7 @@ export function ExitConfirmation({ {t('shared:go_back')} {t('remove_calibration_probe')} diff --git a/app/src/organisms/LabwarePositionCheck/LPCErrorModal.tsx b/app/src/organisms/LabwarePositionCheck/LPCErrorModal.tsx index 1b39c9c1896..ba25c3afea0 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCErrorModal.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCErrorModal.tsx @@ -30,7 +30,13 @@ export function LPCErrorModal({ onCloseClick, }: LPCWizardContentProps): JSX.Element { const { t } = useTranslation(['labware_position_check', 'shared', 'branded']) - const { errorMessage } = commandUtils + const { errorMessage, toggleRobotMoving } = commandUtils + + const handleClose = (): void => { + void toggleRobotMoving(true).then(() => { + onCloseClick() + }) + } return ( {t('shared:exit')} diff --git a/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts b/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts index 24f39a0cf74..7d771ba706b 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts +++ b/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts @@ -116,11 +116,6 @@ export function useLPCFlows({ maintenanceRunId != null && protocolName != null && mostRecentAnalysis != null - console.log('=>(useLPCFlows.ts:119) mostRecentAnalysis', mostRecentAnalysis) - console.log('=>(useLPCFlows.ts:119) protocolName', protocolName) - console.log('=>(useLPCFlows.ts:119) maintenanceRunId', maintenanceRunId) - console.log('=>(useLPCFlows.ts:119) hasCreatedLPCRun', hasCreatedLPCRun) - console.log('=>(useLPCFlows.ts:119) showLPC', showLPC) return showLPC ? { diff --git a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx index 51ccf6870a8..970b01cdfb0 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx @@ -81,6 +81,7 @@ function LPCWizardHeader({ confirmExitLPC, } = commandUtils + // TODO(jh 01-15-24): Revisit the onExit conditions. Can we simplify? return ( } if (errorMessage != null) { diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts index f9a5159b5a1..025e7493c5c 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts @@ -46,6 +46,7 @@ export type UseLPCCommandsResult = UseApplyLPCOffsetsResult & UseHandleValidMoveToMaintenancePositionResult & { errorMessage: string | null isRobotMoving: boolean + toggleRobotMoving: (isMoving: boolean) => Promise } // Consolidates all command handlers and handler state for injection into LPC. @@ -53,11 +54,9 @@ export function useLPCCommands( props: UseLPCCommandsProps ): UseLPCCommandsResult { const [errorMessage, setErrorMessage] = useState(null) + const [isRobotMoving, setIsRobotMoving] = useState(false) - const { - chainRunCommands, - isCommandMutationLoading: isRobotMoving, - } = useChainMaintenanceCommands() + const { chainRunCommands } = useChainMaintenanceCommands() const chainLPCCommands = ( commands: CreateCommand[], @@ -108,6 +107,11 @@ export function useLPCCommands( return { errorMessage, isRobotMoving, + toggleRobotMoving: (isMoving: boolean) => + new Promise(resolve => { + setIsRobotMoving(isMoving) + resolve() + }), ...applyLPCOffsetsUtils, ...buildLPCOffsets, ...handleJogUtils, diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts index f42c786d3e1..64c505d9fbd 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useApplyLPCOffsets.ts @@ -10,7 +10,9 @@ export interface UseApplyLPCOffsetsProps extends UseLPCCommandChildProps { } export interface UseApplyLPCOffsetsResult { - handleApplyOffsets: (offsets: LabwareOffsetCreateData[]) => void + handleApplyOffsetsAndClose: ( + offsets: LabwareOffsetCreateData[] + ) => Promise isApplyingOffsets: boolean } @@ -23,17 +25,22 @@ export function useApplyLPCOffsets({ const { createLabwareOffset } = useCreateLabwareOffsetMutation() - const handleApplyOffsets = (offsets: LabwareOffsetCreateData[]): void => { + const handleApplyOffsetsAndClose = ( + offsets: LabwareOffsetCreateData[] + ): Promise => { setIsApplyingOffsets(true) - Promise.all(offsets.map(data => createLabwareOffset({ runId, data }))) + return Promise.all( + offsets.map(data => createLabwareOffset({ runId, data })) + ) .then(() => { onCloseClick() setIsApplyingOffsets(false) }) .catch((e: Error) => { setErrorMessage(`Error applying labware offsets: ${e.message}`) + return Promise.reject(new Error('Could not apply offsets.')) }) } - return { isApplyingOffsets, handleApplyOffsets } + return { isApplyingOffsets, handleApplyOffsetsAndClose } } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts index 18853110dc4..71303abbf48 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts @@ -5,9 +5,12 @@ import { selectActiveLwInitialPosition } from '/app/organisms/LabwarePositionChe import type { CreateCommand } from '@opentrons/shared-data' import type { UseLPCCommandWithChainRunChildProps } from './types' import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' +import type { CommandData } from '@opentrons/api-client' export interface UseHandlePrepModulesResult { - handleCheckItemsPrepModules: (step: LabwarePositionCheckStep | null) => void + handleCheckItemsPrepModules: ( + step: LabwarePositionCheckStep | null + ) => Promise } // Prep module(s) before LPCing a specific labware involving module(s). @@ -17,7 +20,7 @@ export function useHandlePrepModules({ }: UseLPCCommandWithChainRunChildProps): UseHandlePrepModulesResult { const handleCheckItemsPrepModules = ( step: LabwarePositionCheckStep | null - ): void => { + ): Promise => { const initialPosition = selectActiveLwInitialPosition(step, state) if (step?.section === NAV_STEPS.CHECK_POSITIONS) { @@ -31,11 +34,13 @@ export function useHandlePrepModules({ step.section === NAV_STEPS.CHECK_POSITIONS && prepCommands.length > 0 ) { - void chainLPCCommands(prepCommands, false) + return chainLPCCommands(prepCommands, false) } - } else { - console.warn('Cannot prep modules during unsupported step.') } + + return Promise.reject( + new Error('Cannot prep modules during unsupported step.') + ) } return { handleCheckItemsPrepModules } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts index adf94d2776a..462b0ff685f 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts @@ -13,7 +13,7 @@ import type { import type { UseLPCCommandWithChainRunChildProps } from './types' export interface UseHandleStartLPCResult { - createStartLPCHandler: (onSuccess: () => void) => () => void + createStartLPCHandler: (onSuccess: () => void) => () => Promise } export function useHandleStartLPC({ @@ -21,7 +21,9 @@ export function useHandleStartLPC({ mostRecentAnalysis, state, }: UseLPCCommandWithChainRunChildProps): UseHandleStartLPCResult { - const createStartLPCHandler = (onSuccess: () => void): (() => void) => { + const createStartLPCHandler = ( + onSuccess: () => void + ): (() => Promise) => { const startCommands: CreateCommand[] = [ ...buildInstrumentLabwarePrepCommands(mostRecentAnalysis), ...moduleInitBeforeAnyLPCCommands(mostRecentAnalysis), @@ -29,11 +31,10 @@ export function useHandleStartLPC({ ...moveToMaintenancePosition(state.steps.current, state), ] - return (): void => { - void chainLPCCommands(startCommands, false).then(() => { + return () => + chainLPCCommands(startCommands, false).then(() => { onSuccess() }) - } } return { createStartLPCHandler } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts index 893f7054d42..e0f8a2c93d3 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts @@ -1,6 +1,7 @@ import { moveToMaintenancePosition } from '/app/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands' import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' +import type { CommandData } from '@opentrons/api-client' import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' import type { UseLPCCommandWithChainRunChildProps } from '/app/organisms/LabwarePositionCheck/hooks/useLPCCommands/types' @@ -8,7 +9,7 @@ export interface UseHandleValidMoveToMaintenancePositionResult { /* Only move to maintenance position during probe steps. */ handleValidMoveToMaintenancePosition: ( step: LabwarePositionCheckStep | null - ) => void + ) => Promise } export function useHandleValidMoveToMaintenancePosition({ @@ -18,12 +19,18 @@ export function useHandleValidMoveToMaintenancePosition({ return { handleValidMoveToMaintenancePosition: ( step: LabwarePositionCheckStep | null - ) => { + ): Promise => { if ( step?.section === NAV_STEPS.ATTACH_PROBE || step?.section === NAV_STEPS.DETACH_PROBE ) { - void chainLPCCommands(moveToMaintenancePosition(step, state), false) + return chainLPCCommands(moveToMaintenancePosition(step, state), false) + } else { + return Promise.reject( + new Error( + 'Does not move to maintenance position if step is not a probe step.' + ) + ) } }, } diff --git a/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx b/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx index 5ec34b4c53c..08d1d0f3fff 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx @@ -35,6 +35,7 @@ export function AttachProbe({ unableToDetect, createProbeAttachmentHandler, handleCheckItemsPrepModules, + toggleRobotMoving, } = commandUtils const pipette = selectActivePipette(step, state) const handleProbeAttached = createProbeAttachmentHandler( @@ -62,10 +63,11 @@ export function AttachProbe({ } })() - const handleConfirmProbeAttached = (): void => { - void handleProbeAttached().then(() => { - handleCheckItemsPrepModules(steps.next) - }) + const handleProceed = (): void => { + void toggleRobotMoving(true) + .then(() => handleProbeAttached()) + .then(() => handleCheckItemsPrepModules(steps.next)) + .finally(() => toggleRobotMoving(false)) } if (unableToDetect) { @@ -98,7 +100,7 @@ export function AttachProbe({ } proceedButtonText={i18n.format(t('shared:continue'), 'capitalize')} - proceed={handleConfirmProbeAttached} + proceed={handleProceed} /> ) } diff --git a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx index bac8dfc467e..b520fe1dfab 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx @@ -28,7 +28,7 @@ export function BeforeBeginning({ }: LPCStepProps): JSX.Element { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const { isOnDevice, protocolName, labwareDefs, existingOffsets } = state - const { createStartLPCHandler } = commandUtils + const { createStartLPCHandler, toggleRobotMoving } = commandUtils const handleStartLPC = createStartLPCHandler(proceed) @@ -43,6 +43,12 @@ export function BeforeBeginning({ }, ] + const handleProceed = (): void => { + void toggleRobotMoving(true) + .then(() => handleStartLPC()) + .finally(() => toggleRobotMoving(false)) + } + return ( ) : ( - + {i18n.format(t('shared:get_started'), 'capitalize')} )} diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx index 800d9b9c79a..960c2e9387d 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx @@ -36,6 +36,7 @@ export function CheckItem( handleConfirmLwFinalPosition, handleResetLwModulesOnDeck, handleValidMoveToMaintenancePosition, + toggleRobotMoving, } = commandUtils const { isOnDevice, protocolData, labwareDefs, steps } = state const { t } = useTranslation(['labware_position_check', 'shared']) @@ -66,25 +67,31 @@ export function CheckItem( ...buildDisplayParams(), }) - const handleDispatchConfirmInitialPlacement = (): void => { - void handleConfirmLwModulePlacement({ step }).then(position => { - dispatch( - setInitialPosition({ - labwareId, - location, - position, - }) - ) - }) + const handlePrepareProceed = (): void => { + void toggleRobotMoving(true) + .then(() => handleConfirmLwModulePlacement({ step })) + .then(position => { + dispatch( + setInitialPosition({ + labwareId, + location, + position, + }) + ) + }) + .finally(() => toggleRobotMoving(false)) } // TODO(jh, 01-14-25): Revisit next step injection after refactoring the store (after designs settle). - const handleDispatchConfirmFinalPlacement = (): void => { - void handleConfirmLwFinalPosition({ - step, - onSuccess: proceed, - pipette, - }) + const handleJogProceed = (): void => { + void toggleRobotMoving(true) + .then(() => + handleConfirmLwFinalPosition({ + step, + onSuccess: proceed, + pipette, + }) + ) .then(position => { dispatch( setFinalPosition({ @@ -94,26 +101,27 @@ export function CheckItem( }) ) }) - .then(() => { - handleCheckItemsPrepModules(steps.next) - }) - .then(() => { - handleValidMoveToMaintenancePosition(steps.next) - }) + .then(() => handleCheckItemsPrepModules(steps.next)) + .then(() => handleValidMoveToMaintenancePosition(steps.next)) + .finally(() => toggleRobotMoving(false)) } - const handleDispatchResetLwModulesOnDeck = (): void => { - void handleResetLwModulesOnDeck({ step }).then(() => { - dispatch( - setInitialPosition({ - labwareId, - location, - position: null, - }) - ) - }) + const handleGoBack = (): void => { + void toggleRobotMoving(true) + .then(() => handleResetLwModulesOnDeck({ step })) + .then(() => { + dispatch( + setInitialPosition({ + labwareId, + location, + position: null, + }) + ) + }) + .finally(() => toggleRobotMoving(false)) } + // TODO(jh 01-15-24): These should be separate steps, but let's wait for designs to settle. return ( {initialPosition != null ? ( @@ -142,8 +150,8 @@ export function CheckItem( }} /> } - handleConfirmPosition={handleDispatchConfirmFinalPlacement} - handleGoBack={handleDispatchResetLwModulesOnDeck} + handleConfirmPosition={handleJogProceed} + handleGoBack={handleGoBack} handleJog={handleJog} {...props} /> @@ -167,7 +175,7 @@ export function CheckItem( ]} /> } - confirmPlacement={handleDispatchConfirmInitialPlacement} + confirmPlacement={handlePrepareProceed} {...props} /> )} diff --git a/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx index 66097f53ed3..78c46a656c9 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx @@ -27,7 +27,7 @@ export const DetachProbe = ({ }: LPCStepProps): JSX.Element => { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const { current: currentStep } = state.steps - const { createProbeDetachmentHandler } = commandUtils + const { createProbeDetachmentHandler, toggleRobotMoving } = commandUtils const pipette = selectActivePipette(currentStep, state) const probeVideoSrc = ((): string => { const channels = selectActivePipetteChannelCount(currentStep, state) @@ -44,6 +44,12 @@ export const DetachProbe = ({ const handleProbeDetached = createProbeDetachmentHandler(pipette, proceed) + const handleProceed = (): void => { + void toggleRobotMoving(true) + .then(() => handleProbeDetached()) + .finally(() => toggleRobotMoving(false)) + } + return ( } proceedButtonText={t('confirm_detached')} - proceed={handleProbeDetached} + proceed={handleProceed} /> ) } diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/index.tsx index 453e4b95ba5..45777142bf5 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/index.tsx @@ -41,8 +41,9 @@ export function ResultsSummary( const { protocolData, isOnDevice } = state const { isApplyingOffsets, - handleApplyOffsets, + handleApplyOffsetsAndClose, buildOffsetsToApply, + toggleRobotMoving, } = commandUtils const { i18n, t } = useTranslation('labware_position_check') const offsetsToApply = buildOffsetsToApply() @@ -50,6 +51,12 @@ export function ResultsSummary( getIsLabwareOffsetCodeSnippetsOn ) + const handleProceed = (): void => { + void toggleRobotMoving(true).then(() => + handleApplyOffsetsAndClose(offsetsToApply) + ) + } + return ( @@ -86,9 +93,7 @@ export function ResultsSummary( {isOnDevice ? ( { - handleApplyOffsets(offsetsToApply) - }} + onClick={handleProceed} buttonText={i18n.format(t('apply_offsets'), 'capitalize')} iconName={isApplyingOffsets ? 'ot-spinner' : null} iconPlacement={isApplyingOffsets ? 'startIcon' : null} @@ -97,12 +102,7 @@ export function ResultsSummary( ) : ( - { - handleApplyOffsets(offsetsToApply) - }} - disabled={isApplyingOffsets} - > + {isApplyingOffsets ? ( Date: Wed, 15 Jan 2025 10:46:46 -0500 Subject: [PATCH 28/33] nicer command errors (and test cleanup) --- .../__tests__/SetupLabwarePositionCheck.test.tsx | 3 +++ .../hooks/useLPCCommands/useHandlePrepModules.ts | 4 +++- .../useHandleValidMoveToMaintenancePosition.ts | 2 +- .../LabwarePositionCheck/steps/CheckItem/index.tsx | 10 ++++++++-- app/src/organisms/LegacyLabwarePositionCheck/index.tsx | 2 ++ .../ODD/ProtocolSetup/__tests__/ProtocolSetup.test.tsx | 3 +++ 6 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx index 6b0b3d5ad20..e3f396c5ed5 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/SetupLabwarePositionCheck/__tests__/SetupLabwarePositionCheck.test.tsx @@ -24,6 +24,7 @@ import { useUnmatchedModulesForProtocol, } from '/app/resources/runs' import { useRobotType } from '/app/redux-resources/robots' +import { useLPCFlows } from '/app/organisms/LabwarePositionCheck' import type { Mock } from 'vitest' @@ -34,6 +35,7 @@ vi.mock('/app/redux/config') vi.mock('../../../hooks/useLPCSuccessToast') vi.mock('@opentrons/react-api-client') vi.mock('/app/resources/runs') +vi.mock('/app/organisms/LabwarePositionCheck') const DISABLED_REASON = 'MOCK_DISABLED_REASON' const ROBOT_NAME = 'otie' @@ -106,6 +108,7 @@ describe('SetupLabwarePositionCheck', () => { vi.mocked(useProtocolAnalysisAsDocumentQuery).mockReturnValue({ data: null, } as any) + vi.mocked(useLPCFlows).mockReturnValue({ launchLPC: mockLaunchLPC } as any) }) afterEach(() => { diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts index 71303abbf48..3d68e457845 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts @@ -35,11 +35,13 @@ export function useHandlePrepModules({ prepCommands.length > 0 ) { return chainLPCCommands(prepCommands, false) + } else { + return Promise.resolve([]) } } return Promise.reject( - new Error('Cannot prep modules during unsupported step.') + new Error(`Cannot prep modules during unsupported step: ${step?.section}`) ) } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts index e0f8a2c93d3..05a9ec13f5f 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts @@ -28,7 +28,7 @@ export function useHandleValidMoveToMaintenancePosition({ } else { return Promise.reject( new Error( - 'Does not move to maintenance position if step is not a probe step.' + `Does not move to maintenance position if step is not a probe step. Step: ${step?.section}` ) ) } diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx index 960c2e9387d..cede9664579 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx @@ -2,6 +2,7 @@ import { Trans, useTranslation } from 'react-i18next' import { DIRECTION_COLUMN, Flex, LegacyStyledText } from '@opentrons/components' +import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { getLabwareDisplayLocation } from '/app/local-resources/labware' import { UnorderedList } from '/app/molecules/UnorderedList' @@ -101,8 +102,13 @@ export function CheckItem( }) ) }) - .then(() => handleCheckItemsPrepModules(steps.next)) - .then(() => handleValidMoveToMaintenancePosition(steps.next)) + .then(() => { + if (state.steps.next?.section === NAV_STEPS.CHECK_POSITIONS) { + return handleCheckItemsPrepModules(steps.next) + } else { + return handleValidMoveToMaintenancePosition(steps.next) + } + }) .finally(() => toggleRobotMoving(false)) } diff --git a/app/src/organisms/LegacyLabwarePositionCheck/index.tsx b/app/src/organisms/LegacyLabwarePositionCheck/index.tsx index 777fead7780..52a87f9b687 100644 --- a/app/src/organisms/LegacyLabwarePositionCheck/index.tsx +++ b/app/src/organisms/LegacyLabwarePositionCheck/index.tsx @@ -31,6 +31,8 @@ interface LabwarePositionCheckModalProps { // the component's dependencies (like useLabwarePositionCheck). If we wrapped the contents of LabwarePositionCheckComponent // in an ErrorBoundary as part of its return value (render), an error could occur before this point, meaning the error boundary // would never get invoked + +// LegacyFlows are utilized by the OT-2, and should never actually be utilized by the Flex despite offering Flex support. export const LegacyLabwarePositionCheck = ( props: LabwarePositionCheckModalProps ): JSX.Element => { diff --git a/app/src/pages/ODD/ProtocolSetup/__tests__/ProtocolSetup.test.tsx b/app/src/pages/ODD/ProtocolSetup/__tests__/ProtocolSetup.test.tsx index 5863d70ba93..d87bad08d52 100644 --- a/app/src/pages/ODD/ProtocolSetup/__tests__/ProtocolSetup.test.tsx +++ b/app/src/pages/ODD/ProtocolSetup/__tests__/ProtocolSetup.test.tsx @@ -65,6 +65,7 @@ import { import { mockConnectableRobot } from '/app/redux/discovery/__fixtures__' import { mockRunTimeParameterData } from '/app/organisms/ODD/ProtocolSetup/__fixtures__' import { useScrollPosition } from '/app/local-resources/dom-utils' +import { useLPCFlows } from '/app/organisms/LabwarePositionCheck' import type { UseQueryResult } from 'react-query' import type * as SharedData from '@opentrons/shared-data' @@ -115,6 +116,7 @@ vi.mock('/app/redux-resources/analytics') vi.mock('/app/redux-resources/robots') vi.mock('/app/resources/modules') vi.mock('/app/local-resources/dom-utils') +vi.mock('/app/organisms/LabwarePositionCheck') const render = (path = '/') => { return renderWithProviders( @@ -328,6 +330,7 @@ describe('ProtocolSetup', () => { isScrolled: false, scrollRef: {} as any, }) + vi.mocked(useLPCFlows).mockReturnValue({ launchLPC: mockLaunchLPC } as any) }) it('should render text, image, and buttons', () => { From 391edd8cef263148d29fa327eaf6d65117422a86 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Thu, 16 Jan 2025 12:37:28 -0500 Subject: [PATCH 29/33] nullish coalescence is a thing --- .../organisms/ODD/ProtocolSetup/ProtocolSetupOffsets/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupOffsets/index.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupOffsets/index.tsx index 61eb8625047..874b933e890 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupOffsets/index.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupOffsets/index.tsx @@ -75,8 +75,7 @@ export function ProtocolSetupOffsets({ const nonIdentityOffsets = getLatestCurrentOffsets(sortedOffsets) return ( <> - {LPCWizard} - {LPCWizard == null && ( + {LPCWizard ?? ( <> Date: Thu, 16 Jan 2025 15:20:43 -0500 Subject: [PATCH 30/33] namespacing for the protocol-runs store --- .../LabwarePositionCheck/redux/types.ts | 18 +----- app/src/redux/protocol-runs/actions/index.ts | 1 + .../{actions.ts => actions/setup.ts} | 4 +- .../redux/protocol-runs/constants/index.ts | 1 + .../{constants.ts => constants/setup.ts} | 0 app/src/redux/protocol-runs/reducer.ts | 63 ------------------- app/src/redux/protocol-runs/reducer/index.ts | 33 ++++++++++ app/src/redux/protocol-runs/reducer/setup.ts | 48 ++++++++++++++ .../redux/protocol-runs/selectors/index.ts | 1 + .../{selectors.ts => selectors/setup.ts} | 4 +- app/src/redux/protocol-runs/types/index.ts | 11 ++++ .../{types.ts => types/setup.ts} | 10 +-- 12 files changed, 101 insertions(+), 93 deletions(-) create mode 100644 app/src/redux/protocol-runs/actions/index.ts rename app/src/redux/protocol-runs/{actions.ts => actions/setup.ts} (87%) create mode 100644 app/src/redux/protocol-runs/constants/index.ts rename app/src/redux/protocol-runs/{constants.ts => constants/setup.ts} (100%) delete mode 100644 app/src/redux/protocol-runs/reducer.ts create mode 100644 app/src/redux/protocol-runs/reducer/index.ts create mode 100644 app/src/redux/protocol-runs/reducer/setup.ts create mode 100644 app/src/redux/protocol-runs/selectors/index.ts rename app/src/redux/protocol-runs/{selectors.ts => selectors/setup.ts} (96%) create mode 100644 app/src/redux/protocol-runs/types/index.ts rename app/src/redux/protocol-runs/{types.ts => types/setup.ts} (88%) diff --git a/app/src/organisms/LabwarePositionCheck/redux/types.ts b/app/src/organisms/LabwarePositionCheck/redux/types.ts index 1394db3e8a5..504a258683d 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/types.ts +++ b/app/src/organisms/LabwarePositionCheck/redux/types.ts @@ -1,10 +1,4 @@ -import type { - Coordinates, - DeckConfiguration, - LabwareDefinition2, -} from '@opentrons/shared-data' import type { LabwareOffsetLocation, VectorOffset } from '@opentrons/api-client' -import type { LPCWizardFlexProps } from '/app/organisms/LabwarePositionCheck/LPCWizardFlex' import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' export interface WorkingOffset { @@ -35,7 +29,7 @@ export interface FinalPositionAction { payload: PositionParams } -interface StepsInfo { +export interface StepsInfo { currentStepIndex: number totalStepCount: number current: LabwarePositionCheckStep @@ -43,16 +37,6 @@ interface StepsInfo { all: LabwarePositionCheckStep[] } -export interface LPCWizardState extends LPCWizardFlexProps { - isOnDevice: boolean - workingOffsets: WorkingOffset[] - tipPickUpOffset: Coordinates | null - protocolData: LPCWizardFlexProps['mostRecentAnalysis'] - labwareDefs: LabwareDefinition2[] - deckConfig: DeckConfiguration - steps: StepsInfo -} - export type LPCWizardAction = | InitialPositionAction | FinalPositionAction diff --git a/app/src/redux/protocol-runs/actions/index.ts b/app/src/redux/protocol-runs/actions/index.ts new file mode 100644 index 00000000000..5a774a6b36e --- /dev/null +++ b/app/src/redux/protocol-runs/actions/index.ts @@ -0,0 +1 @@ +export * from './setup' diff --git a/app/src/redux/protocol-runs/actions.ts b/app/src/redux/protocol-runs/actions/setup.ts similarity index 87% rename from app/src/redux/protocol-runs/actions.ts rename to app/src/redux/protocol-runs/actions/setup.ts index 378ee297ed2..86a8397736b 100644 --- a/app/src/redux/protocol-runs/actions.ts +++ b/app/src/redux/protocol-runs/actions/setup.ts @@ -1,5 +1,5 @@ -import * as Constants from './constants' -import type * as Types from './types' +import * as Constants from '../constants' +import type * as Types from '../types' export const updateRunSetupStepsComplete = ( runId: string, diff --git a/app/src/redux/protocol-runs/constants/index.ts b/app/src/redux/protocol-runs/constants/index.ts new file mode 100644 index 00000000000..5a774a6b36e --- /dev/null +++ b/app/src/redux/protocol-runs/constants/index.ts @@ -0,0 +1 @@ +export * from './setup' diff --git a/app/src/redux/protocol-runs/constants.ts b/app/src/redux/protocol-runs/constants/setup.ts similarity index 100% rename from app/src/redux/protocol-runs/constants.ts rename to app/src/redux/protocol-runs/constants/setup.ts diff --git a/app/src/redux/protocol-runs/reducer.ts b/app/src/redux/protocol-runs/reducer.ts deleted file mode 100644 index 0b2d8378a67..00000000000 --- a/app/src/redux/protocol-runs/reducer.ts +++ /dev/null @@ -1,63 +0,0 @@ -import * as Constants from './constants' - -import type { Reducer } from 'redux' -import type { Action } from '../types' - -import type { ProtocolRunState, RunSetupStatus } from './types' - -const INITIAL_STATE: ProtocolRunState = {} - -const INITIAL_SETUP_STEP_STATE = { complete: false, required: true } - -const INITIAL_RUN_SETUP_STATE: RunSetupStatus = { - [Constants.ROBOT_CALIBRATION_STEP_KEY]: INITIAL_SETUP_STEP_STATE, - [Constants.MODULE_SETUP_STEP_KEY]: INITIAL_SETUP_STEP_STATE, - [Constants.LPC_STEP_KEY]: INITIAL_SETUP_STEP_STATE, - [Constants.LABWARE_SETUP_STEP_KEY]: INITIAL_SETUP_STEP_STATE, - [Constants.LIQUID_SETUP_STEP_KEY]: INITIAL_SETUP_STEP_STATE, -} - -export const protocolRunReducer: Reducer = ( - state = INITIAL_STATE, - action -) => { - switch (action.type) { - case Constants.UPDATE_RUN_SETUP_STEPS_COMPLETE: { - return { - ...state, - [action.payload.runId]: { - setup: Constants.SETUP_STEP_KEYS.reduce( - (currentState, step) => ({ - ...currentState, - [step]: { - complete: - action.payload.complete[step] ?? currentState[step].complete, - required: currentState[step].required, - }, - }), - state[action.payload.runId]?.setup ?? INITIAL_RUN_SETUP_STATE - ), - }, - } - } - case Constants.UPDATE_RUN_SETUP_STEPS_REQUIRED: { - return { - ...state, - [action.payload.runId]: { - setup: Constants.SETUP_STEP_KEYS.reduce( - (currentState, step) => ({ - ...currentState, - [step]: { - required: - action.payload.required[step] ?? currentState[step].required, - complete: currentState[step].complete, - }, - }), - state[action.payload.runId]?.setup ?? INITIAL_RUN_SETUP_STATE - ), - }, - } - } - } - return state -} diff --git a/app/src/redux/protocol-runs/reducer/index.ts b/app/src/redux/protocol-runs/reducer/index.ts new file mode 100644 index 00000000000..c346eeee0c2 --- /dev/null +++ b/app/src/redux/protocol-runs/reducer/index.ts @@ -0,0 +1,33 @@ +import * as Constants from '../constants' + +import type { Reducer } from 'redux' + +import type { Action } from '../../types' +import type { ProtocolRunState } from '../types' + +import { setup } from './setup' + +const INITIAL_STATE: ProtocolRunState = {} + +export const protocolRunReducer: Reducer = ( + state = INITIAL_STATE, + action +) => { + switch (action.type) { + case Constants.UPDATE_RUN_SETUP_STEPS_COMPLETE: + case Constants.UPDATE_RUN_SETUP_STEPS_REQUIRED: { + const runId = action.payload.runId + const currentRunState = state[runId] + + return { + ...state, + [runId]: { + ...currentRunState, + setup: setup(currentRunState?.setup, action), + }, + } + } + default: + return state + } +} diff --git a/app/src/redux/protocol-runs/reducer/setup.ts b/app/src/redux/protocol-runs/reducer/setup.ts new file mode 100644 index 00000000000..74bb4fd2b4b --- /dev/null +++ b/app/src/redux/protocol-runs/reducer/setup.ts @@ -0,0 +1,48 @@ +import * as Constants from '../constants' +import type { RunSetupStatus, ProtocolRunAction } from '../types' + +const INITIAL_SETUP_STEP_STATE = { complete: false, required: true } + +export const INITIAL_RUN_SETUP_STATE: RunSetupStatus = { + [Constants.ROBOT_CALIBRATION_STEP_KEY]: INITIAL_SETUP_STEP_STATE, + [Constants.MODULE_SETUP_STEP_KEY]: INITIAL_SETUP_STEP_STATE, + [Constants.LPC_STEP_KEY]: INITIAL_SETUP_STEP_STATE, + [Constants.LABWARE_SETUP_STEP_KEY]: INITIAL_SETUP_STEP_STATE, + [Constants.LIQUID_SETUP_STEP_KEY]: INITIAL_SETUP_STEP_STATE, +} + +export function setup( + state: RunSetupStatus = INITIAL_RUN_SETUP_STATE, + action: ProtocolRunAction +): RunSetupStatus { + switch (action.type) { + case Constants.UPDATE_RUN_SETUP_STEPS_COMPLETE: + return Constants.SETUP_STEP_KEYS.reduce( + (currentState, step) => ({ + ...currentState, + [step]: { + complete: + action.payload.complete[step] ?? currentState[step].complete, + required: currentState[step].required, + }, + }), + state + ) + + case Constants.UPDATE_RUN_SETUP_STEPS_REQUIRED: + return Constants.SETUP_STEP_KEYS.reduce( + (currentState, step) => ({ + ...currentState, + [step]: { + required: + action.payload.required[step] ?? currentState[step].required, + complete: currentState[step].complete, + }, + }), + state + ) + + default: + return state + } +} diff --git a/app/src/redux/protocol-runs/selectors/index.ts b/app/src/redux/protocol-runs/selectors/index.ts new file mode 100644 index 00000000000..5a774a6b36e --- /dev/null +++ b/app/src/redux/protocol-runs/selectors/index.ts @@ -0,0 +1 @@ +export * from './setup' diff --git a/app/src/redux/protocol-runs/selectors.ts b/app/src/redux/protocol-runs/selectors/setup.ts similarity index 96% rename from app/src/redux/protocol-runs/selectors.ts rename to app/src/redux/protocol-runs/selectors/setup.ts index 14149b603bb..0be0168e048 100644 --- a/app/src/redux/protocol-runs/selectors.ts +++ b/app/src/redux/protocol-runs/selectors/setup.ts @@ -1,5 +1,5 @@ -import type { State } from '../types' -import type * as Types from './types' +import type { State } from '../../types' +import type * as Types from '../types' export const getSetupStepComplete: ( state: State, diff --git a/app/src/redux/protocol-runs/types/index.ts b/app/src/redux/protocol-runs/types/index.ts new file mode 100644 index 00000000000..acae8290e8f --- /dev/null +++ b/app/src/redux/protocol-runs/types/index.ts @@ -0,0 +1,11 @@ +import type { RunSetupStatus } from './setup' + +export * from './setup' + +export interface PerRunUIState { + setup: RunSetupStatus +} + +export type ProtocolRunState = Partial<{ + readonly [runId: string]: PerRunUIState +}> diff --git a/app/src/redux/protocol-runs/types.ts b/app/src/redux/protocol-runs/types/setup.ts similarity index 88% rename from app/src/redux/protocol-runs/types.ts rename to app/src/redux/protocol-runs/types/setup.ts index c14d556d495..b1d4c3b8e70 100644 --- a/app/src/redux/protocol-runs/types.ts +++ b/app/src/redux/protocol-runs/types/setup.ts @@ -6,7 +6,7 @@ import type { LIQUID_SETUP_STEP_KEY, UPDATE_RUN_SETUP_STEPS_COMPLETE, UPDATE_RUN_SETUP_STEPS_REQUIRED, -} from './constants' +} from '../constants' export type RobotCalibrationStepKey = typeof ROBOT_CALIBRATION_STEP_KEY export type ModuleSetupStepKey = typeof MODULE_SETUP_STEP_KEY @@ -32,14 +32,6 @@ export type RunSetupStatus = { [Step in StepKey]: StepState } -export interface PerRunUIState { - setup: RunSetupStatus -} - -export type ProtocolRunState = Partial<{ - readonly [runId: string]: PerRunUIState -}> - export interface UpdateRunSetupStepsCompleteAction { type: typeof UPDATE_RUN_SETUP_STEPS_COMPLETE payload: { From d7bc992754d9b40e4019f3adfe4fdb3de0a376cc Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Fri, 17 Jan 2025 14:55:29 -0500 Subject: [PATCH 31/33] wire up to the global store --- .../LabwarePositionCheck/ExitConfirmation.tsx | 8 +- .../LabwarePositionCheck/LPCWizardFlex.tsx | 59 ++-- .../hooks/useLPCCommands/commands/pipettes.ts | 15 +- .../hooks/useLPCCommands/index.ts | 5 +- .../useLPCCommands/useBuildOffsetsToApply.ts | 16 +- .../useHandleConfirmLwFinalPosition.ts | 4 +- .../hooks/useLPCCommands/useHandleJog.ts | 11 +- .../useLPCCommands/useHandlePrepModules.ts | 14 +- .../useLPCCommands/useHandleProbeCommands.ts | 19 +- .../hooks/useLPCCommands/useHandleStartLPC.ts | 10 +- ...useHandleValidMoveToMaintenancePosition.ts | 6 +- .../hooks/useLPCInitialState/index.ts | 31 ++- .../LabwarePositionCheck/redux/actions.ts | 30 --- .../LabwarePositionCheck/redux/index.ts | 6 +- .../redux/selectors/labware.ts | 166 ------------ .../LabwarePositionCheck/redux/types.ts | 34 +-- .../redux/useLPCReducer.ts | 19 -- .../steps/AttachProbe.tsx | 35 ++- .../steps/BeforeBeginning/index.tsx | 20 +- .../CheckItem/JogToWell/LiveOffsetValue.tsx | 10 +- .../steps/CheckItem/JogToWell/index.tsx | 40 ++- .../steps/CheckItem/PlaceItemInstruction.tsx | 14 +- .../steps/CheckItem/PrepareSpace.tsx | 17 +- .../steps/CheckItem/index.tsx | 39 ++- .../steps/DetachProbe.tsx | 21 +- .../steps/ResultsSummary/OffsetTable.tsx | 18 +- .../steps/ResultsSummary/TableComponent.tsx | 12 +- .../steps/ResultsSummary/index.tsx | 14 +- .../LabwarePositionCheck/types/content.ts | 12 +- app/src/redux/protocol-runs/actions/index.ts | 1 + app/src/redux/protocol-runs/actions/lpc.ts | 50 ++++ .../redux/protocol-runs/constants/index.ts | 1 + .../protocol-runs/constants/lpc.ts} | 2 + app/src/redux/protocol-runs/reducer/index.ts | 43 ++- .../protocol-runs/reducer/lpc.ts} | 13 +- app/src/redux/protocol-runs/reducer/setup.ts | 2 +- .../protocol-runs/reducer/transforms/index.ts | 1 + .../protocol-runs/reducer/transforms/lpc.ts} | 2 +- .../redux/protocol-runs/selectors/index.ts | 1 + .../protocol-runs/selectors/lpc}/index.ts | 0 .../protocol-runs/selectors/lpc/labware.ts | 251 ++++++++++++++++++ .../protocol-runs/selectors/lpc}/pipettes.ts | 23 +- .../selectors/lpc/transforms.ts} | 0 app/src/redux/protocol-runs/types/index.ts | 7 +- app/src/redux/protocol-runs/types/lpc.ts | 70 +++++ app/src/redux/protocol-runs/types/setup.ts | 2 +- 46 files changed, 759 insertions(+), 415 deletions(-) delete mode 100644 app/src/organisms/LabwarePositionCheck/redux/actions.ts delete mode 100644 app/src/organisms/LabwarePositionCheck/redux/selectors/labware.ts delete mode 100644 app/src/organisms/LabwarePositionCheck/redux/useLPCReducer.ts create mode 100644 app/src/redux/protocol-runs/actions/lpc.ts rename app/src/{organisms/LabwarePositionCheck/redux/constants.ts => redux/protocol-runs/constants/lpc.ts} (67%) rename app/src/{organisms/LabwarePositionCheck/redux/reducer.ts => redux/protocol-runs/reducer/lpc.ts} (85%) create mode 100644 app/src/redux/protocol-runs/reducer/transforms/index.ts rename app/src/{organisms/LabwarePositionCheck/redux/transforms.ts => redux/protocol-runs/reducer/transforms/lpc.ts} (94%) rename app/src/{organisms/LabwarePositionCheck/redux/selectors => redux/protocol-runs/selectors/lpc}/index.ts (100%) create mode 100644 app/src/redux/protocol-runs/selectors/lpc/labware.ts rename app/src/{organisms/LabwarePositionCheck/redux/selectors => redux/protocol-runs/selectors/lpc}/pipettes.ts (52%) rename app/src/{organisms/LabwarePositionCheck/utils.ts => redux/protocol-runs/selectors/lpc/transforms.ts} (100%) create mode 100644 app/src/redux/protocol-runs/types/lpc.ts diff --git a/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx b/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx index a65c198d2c6..d5dbd072d04 100644 --- a/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx +++ b/app/src/organisms/LabwarePositionCheck/ExitConfirmation.tsx @@ -1,5 +1,6 @@ import styled, { css } from 'styled-components' import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' import { AlertPrimaryButton, @@ -20,15 +21,16 @@ import { } from '@opentrons/components' import { SmallButton } from '/app/atoms/buttons' +import { getIsOnDevice } from '/app/redux/config' import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' export function ExitConfirmation({ commandUtils, - state, }: LPCWizardContentProps): JSX.Element { const { i18n, t } = useTranslation(['labware_position_check', 'shared']) const { confirmExitLPC, cancelExitLPC, toggleRobotMoving } = commandUtils + const isOnDevice = useSelector(getIsOnDevice) const handleConfirmExit = (): void => { toggleRobotMoving(true).then(() => { @@ -40,7 +42,7 @@ export function ExitConfirmation({ - {state.isOnDevice ? ( + {isOnDevice ? ( <> {t('remove_probe_before_exit')} @@ -62,7 +64,7 @@ export function ExitConfirmation({ )} - {state.isOnDevice ? ( + {isOnDevice ? ( {} export function LPCWizardFlex(props: LPCWizardFlexProps): JSX.Element { - const initialState = useLPCInitialState({ ...props }) - const { state, dispatch } = useLPCReducer(initialState) - - const LPCHandlerUtils = useLPCCommands({ ...props, state }) + const { onCloseClick, ...rest } = props // TODO(jh, 01-14-25): Also inject goBack functionality once designs are finalized. const proceed = (): void => { - dispatch(proceedStep()) + dispatch(proceedStep(props.runId)) + } + const onCloseClickDispatch = (): void => { + onCloseClick() } + const dispatch = useDispatch() + const LPCHandlerUtils = useLPCCommands({ + ...props, + onCloseClick: onCloseClickDispatch, + }) + + useLPCInitialState({ ...rest }) + + // Clean up state on LPC close. + useEffect(() => { + return () => { + dispatch(closeLPC(props.runId)) + } + }, []) return ( ) } function LPCWizardFlexComponent(props: LPCWizardContentProps): JSX.Element { + const isOnDevice = useSelector(getIsOnDevice) + return createPortal( - props.state.isOnDevice ? ( + isOnDevice ? ( @@ -69,11 +85,15 @@ function LPCWizardFlexComponent(props: LPCWizardContentProps): JSX.Element { } function LPCWizardHeader({ - state, + runId, commandUtils, }: LPCWizardContentProps): JSX.Element { const { t } = useTranslation('labware_position_check') - const { currentStepIndex, totalStepCount } = state.steps + const { currentStepIndex, totalStepCount } = useSelector((state: State) => ({ + currentStepIndex: + state.protocolRuns[runId]?.lpc?.steps.currentStepIndex ?? 0, + totalStepCount: state.protocolRuns[runId]?.lpc?.steps.totalStepCount ?? 0, + })) const { errorMessage, showExitConfirmation, @@ -98,7 +118,10 @@ function LPCWizardHeader({ function LPCWizardContent(props: LPCWizardContentProps): JSX.Element { const { t } = useTranslation('shared') - const { current: currentStep } = props.state.steps + const currentStep = useSelector( + (state: State) => + state.protocolRuns[props.runId]?.lpc?.steps.current ?? null + ) const { isRobotMoving, errorMessage, @@ -117,6 +140,10 @@ function LPCWizardContent(props: LPCWizardContentProps): JSX.Element { if (showExitConfirmation) { return } + if (currentStep == null) { + console.error('LPC store not properly initialized.') + return <> + } // Handle step-based routing. switch (currentStep.section) { diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/pipettes.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/pipettes.ts index feb66548e55..fba1f7b025f 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/pipettes.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/pipettes.ts @@ -1,17 +1,12 @@ import { fullHomeCommands } from './gantry' -import { selectActivePipette } from '/app/organisms/LabwarePositionCheck/redux' import type { CreateCommand, LoadedPipette, MotorAxes, } from '@opentrons/shared-data' -import type { - CheckPositionsStep, - LabwarePositionCheckStep, -} from '/app/organisms/LabwarePositionCheck/types' +import type { CheckPositionsStep } from '/app/organisms/LabwarePositionCheck/types' import type { Axis, Sign, StepSize } from '/app/molecules/JogControls/types' -import type { LPCWizardState } from '/app/organisms/LabwarePositionCheck/redux' const PROBE_LENGTH_MM = 44.5 @@ -65,7 +60,7 @@ export const retractSafelyAndHomeCommands = (): CreateCommand[] => [ ] export const retractPipetteAxesSequentiallyCommands = ( - pipette: LoadedPipette | undefined + pipette: LoadedPipette | null ): CreateCommand[] => { const pipetteZMotorAxis = pipette?.mount === 'left' ? 'leftZ' : 'rightZ' @@ -105,10 +100,8 @@ export const moveRelativeCommand = ({ }) export const moveToMaintenancePosition = ( - step: LabwarePositionCheckStep, - state: LPCWizardState + pipette: LoadedPipette | null ): CreateCommand[] => { - const pipette = selectActivePipette(step, state) const pipetteMount = pipette?.mount return [ @@ -123,7 +116,7 @@ export const moveToMaintenancePosition = ( export const verifyProbeAttachmentAndHomeCommands = ( pipetteId: string, - pipette: LoadedPipette | undefined + pipette: LoadedPipette | null ): CreateCommand[] => { const pipetteMount = pipette?.mount const pipetteZMotorAxis = pipetteMount === 'left' ? 'leftZ' : 'rightZ' diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts index 025e7493c5c..606d25a7e45 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/index.ts @@ -25,13 +25,10 @@ import type { UseHandleConfirmPlacementResult } from './useHandleConfirmLwModule import type { UseHandleConfirmPositionResult } from './useHandleConfirmLwFinalPosition' import type { UseHandleResetLwModulesOnDeckResult } from './useHandleResetLwModulesOnDeck' import type { LPCWizardFlexProps } from '/app/organisms/LabwarePositionCheck/LPCWizardFlex' -import type { LPCWizardState } from '/app/organisms/LabwarePositionCheck/redux' import type { UseBuildOffsetsToApplyResult } from './useBuildOffsetsToApply' import type { UseHandleValidMoveToMaintenancePositionResult } from './useHandleValidMoveToMaintenancePosition' -export interface UseLPCCommandsProps extends LPCWizardFlexProps { - state: LPCWizardState -} +export interface UseLPCCommandsProps extends LPCWizardFlexProps {} export type UseLPCCommandsResult = UseApplyLPCOffsetsResult & UseHandleJogResult & diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useBuildOffsetsToApply.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useBuildOffsetsToApply.ts index b0ce243b41d..6b4b01d6632 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useBuildOffsetsToApply.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useBuildOffsetsToApply.ts @@ -1,6 +1,10 @@ +import { useStore } from 'react-redux' + +import { selectOffsetsToApply } from '/app/redux/protocol-runs' + +import type { State } from '/app/redux/types' import type { LabwareOffsetCreateData } from '@opentrons/api-client' import type { UseLPCCommandChildProps } from '/app/organisms/LabwarePositionCheck/hooks/useLPCCommands/types' -import { selectOffsetsToApply } from '/app/organisms/LabwarePositionCheck/redux' export interface UseBuildOffsetsToApplyResult { buildOffsetsToApply: () => LabwareOffsetCreateData[] @@ -11,13 +15,19 @@ export interface UseApplyLPCOffsetsProps extends UseLPCCommandChildProps { } export function useBuildOffsetsToApply({ - state, + runId, setErrorMessage, }: UseApplyLPCOffsetsProps): UseBuildOffsetsToApplyResult { + // Utilizing useStore instead of useSelector enables error handling within the selector + // but only invoke the selector when it's actually needed. + const store = useStore() + return { buildOffsetsToApply: () => { try { - return selectOffsetsToApply(state) + const selectOffsets = selectOffsetsToApply(runId) + const offsetsToApply = selectOffsets(store.getState()) + return offsetsToApply } catch (e) { if (e instanceof Error) { setErrorMessage(e.message) diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwFinalPosition.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwFinalPosition.ts index 789438d2e80..9c159b4c518 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwFinalPosition.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwFinalPosition.ts @@ -24,7 +24,7 @@ export interface UseHandleConfirmPositionResult { handleConfirmLwFinalPosition: ( params: BuildMoveLabwareOffDeckParams & { onSuccess: () => void - pipette: LoadedPipette | undefined + pipette: LoadedPipette | null } ) => Promise } @@ -36,7 +36,7 @@ export function useHandleConfirmLwFinalPosition({ const handleConfirmLwFinalPosition = ( params: BuildMoveLabwareOffDeckParams & { onSuccess: () => void - pipette: LoadedPipette | undefined + pipette: LoadedPipette | null } ): Promise => { const { onSuccess, pipette, step } = params diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts index b95f53caa2c..eb277aa4179 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts @@ -1,4 +1,5 @@ import { useCallback, useEffect, useState } from 'react' +import { useSelector } from 'react-redux' import { useCreateMaintenanceCommandMutation } from '@opentrons/react-api-client' @@ -11,6 +12,7 @@ import type { Sign, StepSize, } from '/app/molecules/JogControls/types' +import type { State } from '/app/redux/types' import type { UseLPCCommandChildProps } from './types' const JOG_COMMAND_TIMEOUT_MS = 10000 @@ -25,11 +27,12 @@ export interface UseHandleJogResult { } export function useHandleJog({ + runId, maintenanceRunId, - state, setErrorMessage, }: UseHandleJogProps): UseHandleJogResult { - const { current: currentStep } = state.steps + const { current: currentStep } = + useSelector((state: State) => state.protocolRuns[runId]?.lpc?.steps) ?? {} const [isJogging, setIsJogging] = useState(false) const [jogQueue, setJogQueue] = useState Promise>>([]) @@ -46,7 +49,9 @@ export function useHandleJog({ ): Promise => { return new Promise((resolve, reject) => { const pipetteId = - 'pipetteId' in currentStep ? currentStep.pipetteId : null + currentStep != null && 'pipetteId' in currentStep + ? currentStep.pipetteId + : null if (pipetteId != null) { createSilentCommand({ diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts index 3d68e457845..0f944bf74f0 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts @@ -1,11 +1,14 @@ +import { useSelector } from 'react-redux' + import { modulePrepCommands } from './commands' import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' -import { selectActiveLwInitialPosition } from '/app/organisms/LabwarePositionCheck/redux' +import { selectActiveLwInitialPosition } from '/app/redux/protocol-runs' import type { CreateCommand } from '@opentrons/shared-data' import type { UseLPCCommandWithChainRunChildProps } from './types' import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' import type { CommandData } from '@opentrons/api-client' +import type { State } from '/app/redux/types' export interface UseHandlePrepModulesResult { handleCheckItemsPrepModules: ( @@ -15,13 +18,18 @@ export interface UseHandlePrepModulesResult { // Prep module(s) before LPCing a specific labware involving module(s). export function useHandlePrepModules({ + runId, chainLPCCommands, - state, }: UseLPCCommandWithChainRunChildProps): UseHandlePrepModulesResult { + const selectInitialPositionFrom = useSelector( + (state: State) => (step: LabwarePositionCheckStep | null) => + selectActiveLwInitialPosition(step, runId, state) + ) + const handleCheckItemsPrepModules = ( step: LabwarePositionCheckStep | null ): Promise => { - const initialPosition = selectActiveLwInitialPosition(step, state) + const initialPosition = selectInitialPositionFrom(step) if (step?.section === NAV_STEPS.CHECK_POSITIONS) { const prepCommands: CreateCommand[] = modulePrepCommands({ diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts index 319d22c419f..8fe77d5c60a 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts @@ -11,11 +11,11 @@ import type { UseLPCCommandWithChainRunChildProps } from './types' export interface UseProbeCommandsResult { createProbeAttachmentHandler: ( pipetteId: string, - pipette: LoadedPipette | undefined, + pipette: LoadedPipette | null, onSuccess: () => void ) => () => Promise createProbeDetachmentHandler: ( - pipette: LoadedPipette | undefined, + pipette: LoadedPipette | null, onSuccess: () => void ) => () => Promise unableToDetect: boolean @@ -29,7 +29,7 @@ export function useHandleProbeCommands({ const createProbeAttachmentHandler = ( pipetteId: string, - pipette: LoadedPipette | undefined, + pipette: LoadedPipette | null, onSuccess: () => void ): (() => Promise) => { const attachmentCommands: CreateCommand[] = [ @@ -38,19 +38,18 @@ export function useHandleProbeCommands({ return () => chainLPCCommands(attachmentCommands, false, true) - .then(() => { - onSuccess() - }) .catch(() => { setShowUnableToDetect(true) - - // Stop propagation to prevent error screen routing. - return Promise.resolve() + return Promise.reject(new Error('Unable to detect probe.')) + }) + .then(() => { + setShowUnableToDetect(false) + onSuccess() }) } const createProbeDetachmentHandler = ( - pipette: LoadedPipette | undefined, + pipette: LoadedPipette | null, onSuccess: () => void ): (() => Promise) => { const detatchmentCommands: CreateCommand[] = [ diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts index 462b0ff685f..df3270555f6 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts @@ -9,26 +9,30 @@ import type { CreateCommand, RunTimeCommand, SetupRunTimeCommand, + LoadedPipette, } from '@opentrons/shared-data' import type { UseLPCCommandWithChainRunChildProps } from './types' export interface UseHandleStartLPCResult { - createStartLPCHandler: (onSuccess: () => void) => () => Promise + createStartLPCHandler: ( + pipette: LoadedPipette | null, + onSuccess: () => void + ) => () => Promise } export function useHandleStartLPC({ chainLPCCommands, mostRecentAnalysis, - state, }: UseLPCCommandWithChainRunChildProps): UseHandleStartLPCResult { const createStartLPCHandler = ( + pipette: LoadedPipette | null, onSuccess: () => void ): (() => Promise) => { const startCommands: CreateCommand[] = [ ...buildInstrumentLabwarePrepCommands(mostRecentAnalysis), ...moduleInitBeforeAnyLPCCommands(mostRecentAnalysis), ...fullHomeCommands(), - ...moveToMaintenancePosition(state.steps.current, state), + ...moveToMaintenancePosition(pipette), ] return () => diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts index 05a9ec13f5f..180cf73f892 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts @@ -2,29 +2,31 @@ import { moveToMaintenancePosition } from '/app/organisms/LabwarePositionCheck/h import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' import type { CommandData } from '@opentrons/api-client' +import type { LoadedPipette } from '@opentrons/shared-data' import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' import type { UseLPCCommandWithChainRunChildProps } from '/app/organisms/LabwarePositionCheck/hooks/useLPCCommands/types' export interface UseHandleValidMoveToMaintenancePositionResult { /* Only move to maintenance position during probe steps. */ handleValidMoveToMaintenancePosition: ( + pipette: LoadedPipette | null, step: LabwarePositionCheckStep | null ) => Promise } export function useHandleValidMoveToMaintenancePosition({ - state, chainLPCCommands, }: UseLPCCommandWithChainRunChildProps): UseHandleValidMoveToMaintenancePositionResult { return { handleValidMoveToMaintenancePosition: ( + pipette: LoadedPipette | null, step: LabwarePositionCheckStep | null ): Promise => { if ( step?.section === NAV_STEPS.ATTACH_PROBE || step?.section === NAV_STEPS.DETACH_PROBE ) { - return chainLPCCommands(moveToMaintenancePosition(step, state), false) + return chainLPCCommands(moveToMaintenancePosition(pipette), false) } else { return Promise.reject( new Error( diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/index.ts index 939d4938e2b..b7974ea0093 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/index.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/index.ts @@ -1,21 +1,24 @@ -import { useSelector } from 'react-redux' +import { useDispatch } from 'react-redux' -import { getIsOnDevice } from '/app/redux/config' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' +import { startLPC } from '/app/redux/protocol-runs' import { getLPCSteps } from './utils' import type { RunTimeCommand } from '@opentrons/shared-data' -import type { LPCWizardState } from '/app/organisms/LabwarePositionCheck/redux' +import type { LPCWizardState } from '/app/redux/protocol-runs' import type { LPCWizardFlexProps } from '/app/organisms/LabwarePositionCheck/LPCWizardFlex' -export interface UseLPCInitialStateProps extends LPCWizardFlexProps {} +export interface UseLPCInitialStateProps + extends Omit {} -export function useLPCInitialState( - props: UseLPCInitialStateProps -): LPCWizardState { - const { mostRecentAnalysis } = props - const isOnDevice = useSelector(getIsOnDevice) +// Inject initial LPC state into Redux. +export function useLPCInitialState({ + mostRecentAnalysis, + runId, + ...rest +}: UseLPCInitialStateProps): void { + const dispatch = useDispatch() const protocolCommands: RunTimeCommand[] = mostRecentAnalysis.commands const labwareDefs = getLabwareDefinitionsFromCommands(protocolCommands) @@ -25,13 +28,11 @@ export function useLPCInitialState( labwareDefs, }) - return { - ...props, - protocolData: props.mostRecentAnalysis, - isOnDevice, + const initialState: LPCWizardState = { + ...rest, + protocolData: mostRecentAnalysis, labwareDefs, workingOffsets: [], - tipPickUpOffset: null, deckConfig, steps: { currentStepIndex: 0, @@ -41,4 +42,6 @@ export function useLPCInitialState( next: LPCSteps[1], }, } + + dispatch(startLPC(runId, initialState)) } diff --git a/app/src/organisms/LabwarePositionCheck/redux/actions.ts b/app/src/organisms/LabwarePositionCheck/redux/actions.ts deleted file mode 100644 index 3bcbe3b8e90..00000000000 --- a/app/src/organisms/LabwarePositionCheck/redux/actions.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { - PROCEED_STEP, - SET_INITIAL_POSITION, - SET_FINAL_POSITION, -} from './constants' - -import type { - ProceedStepAction, - InitialPositionAction, - FinalPositionAction, - PositionParams, -} from './types' - -export const proceedStep = (): ProceedStepAction => ({ - type: PROCEED_STEP, - payload: {}, -}) -export const setInitialPosition = ( - params: PositionParams -): InitialPositionAction => ({ - type: SET_INITIAL_POSITION, - payload: params, -}) - -export const setFinalPosition = ( - params: PositionParams -): FinalPositionAction => ({ - type: SET_FINAL_POSITION, - payload: params, -}) diff --git a/app/src/organisms/LabwarePositionCheck/redux/index.ts b/app/src/organisms/LabwarePositionCheck/redux/index.ts index cf84d3724db..51a3b4100a9 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/index.ts +++ b/app/src/organisms/LabwarePositionCheck/redux/index.ts @@ -1,5 +1 @@ -export * from './useLPCReducer' -export * from './actions' -export * from './selectors' - -export type { LPCWizardAction, LPCWizardState } from './types' +export * from '../types' diff --git a/app/src/organisms/LabwarePositionCheck/redux/selectors/labware.ts b/app/src/organisms/LabwarePositionCheck/redux/selectors/labware.ts deleted file mode 100644 index cf71a3d2ce0..00000000000 --- a/app/src/organisms/LabwarePositionCheck/redux/selectors/labware.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { createSelector } from 'reselect' -import isEqual from 'lodash/isEqual' - -import { - getIsTiprack, - getLabwareDisplayName, - getLabwareDefURI, - getVectorSum, - getVectorDifference, - IDENTITY_VECTOR, -} from '@opentrons/shared-data' - -import { getItemLabwareDef } from '/app/organisms/LabwarePositionCheck/utils' -import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' - -import type { VectorOffset } from '@opentrons/api-client' -import type { LabwareDefinition2 } from '@opentrons/shared-data' -import type { LPCWizardState } from '/app/organisms/LabwarePositionCheck/redux/types' -import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' - -// TODO(jh, 01-13-25): Remove the explicit type casting after restructuring "step". - -export const selectActiveLwInitialPosition = ( - step: LabwarePositionCheckStep | null, - state: LPCWizardState -): VectorOffset | null => { - if (step != null) { - const labwareId = 'labwareId' in step ? step.labwareId : '' - const location = 'location' in step ? step.location : '' - - return ( - state.workingOffsets.find( - o => - o.labwareId === labwareId && - isEqual(o.location, location) && - o.initialPosition != null - )?.initialPosition ?? null - ) - } else { - return null - } -} - -export const selectActiveLwExistingOffset = ( - state: LPCWizardState -): VectorOffset => { - const { existingOffsets, steps } = state - - if ( - !('labwareId' in steps.current) || - !('location' in steps.current) || - !('slotName' in steps.current.location) - ) { - console.warn( - `No labwareId or location in current step: ${steps.current.section}` - ) - return IDENTITY_VECTOR - } else { - const lwUri = getLabwareDefURI( - selectItemLabwareDef(state) as LabwareDefinition2 - ) - - return ( - getCurrentOffsetForLabwareInLocation( - existingOffsets, - lwUri, - steps.current.location - )?.vector ?? IDENTITY_VECTOR - ) - } -} - -export const selectOffsetsToApply = createSelector( - (state: LPCWizardState) => state.workingOffsets, - (state: LPCWizardState) => state.protocolData, - (state: LPCWizardState) => state.existingOffsets, - (workingOffsets, protocolData, existingOffsets) => { - return workingOffsets.map( - ({ initialPosition, finalPosition, labwareId, location }) => { - const definitionUri = - protocolData.labware.find(l => l.id === labwareId)?.definitionUri ?? - null - - if ( - finalPosition == null || - initialPosition == null || - definitionUri == null - ) { - throw new Error( - `cannot create offset for labware with id ${labwareId}, in location ${JSON.stringify( - location - )}, with initial position ${String( - initialPosition - )}, and final position ${String(finalPosition)}` - ) - } else { - const existingOffset = - getCurrentOffsetForLabwareInLocation( - existingOffsets, - definitionUri, - location - )?.vector ?? IDENTITY_VECTOR - const vector = getVectorSum( - existingOffset, - getVectorDifference(finalPosition, initialPosition) - ) - return { definitionUri, location, vector } - } - } - ) - } -) - -export const selectIsActiveLwTipRack = (state: LPCWizardState): boolean => { - if ('labwareId' in state.steps.current) { - return getIsTiprack(selectItemLabwareDef(state) as LabwareDefinition2) - } else { - console.warn('No labwareId in step.') - return false - } -} - -export const selectLwDisplayName = (state: LPCWizardState): string => { - if ('labwareId' in state.steps.current) { - return getLabwareDisplayName( - selectItemLabwareDef(state) as LabwareDefinition2 - ) - } else { - console.warn('No labwareId in step.') - return '' - } -} - -export const selectActiveAdapterDisplayName = ( - state: LPCWizardState -): string => { - const { protocolData, labwareDefs, steps } = state - - return 'adapterId' in steps.current && steps.current.adapterId != null - ? getItemLabwareDef({ - labwareId: steps.current.adapterId, - loadedLabware: protocolData.labware, - labwareDefs, - })?.metadata.displayName ?? '' - : '' -} - -export const selectItemLabwareDef = createSelector( - (state: LPCWizardState) => state.steps.current, - (state: LPCWizardState) => state.labwareDefs, - (state: LPCWizardState) => state.protocolData.labware, - (current, labwareDefs, loadedLabware) => { - const labwareId = 'labwareId' in current ? current.labwareId : '' - - if (labwareId === '') { - console.warn(`No labwareId associated with step: ${current.section}`) - return null - } - - return getItemLabwareDef({ - labwareId, - labwareDefs, - loadedLabware, - }) - } -) diff --git a/app/src/organisms/LabwarePositionCheck/redux/types.ts b/app/src/organisms/LabwarePositionCheck/redux/types.ts index 504a258683d..d40d18d91d7 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/types.ts +++ b/app/src/organisms/LabwarePositionCheck/redux/types.ts @@ -1,33 +1,6 @@ -import type { LabwareOffsetLocation, VectorOffset } from '@opentrons/api-client' import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' -export interface WorkingOffset { - labwareId: string - location: LabwareOffsetLocation - initialPosition: VectorOffset | null - finalPosition: VectorOffset | null -} - -export interface PositionParams { - labwareId: string - location: LabwareOffsetLocation - position: VectorOffset | null -} - -export interface ProceedStepAction { - type: 'PROCEED_STEP' - payload: Record -} - -export interface InitialPositionAction { - type: 'SET_INITIAL_POSITION' - payload: PositionParams -} - -export interface FinalPositionAction { - type: 'SET_FINAL_POSITION' - payload: PositionParams -} +// TODO(jh, 01-16-25): Remove this once `steps` are refactored out of Redux. export interface StepsInfo { currentStepIndex: number @@ -36,8 +9,3 @@ export interface StepsInfo { next: LabwarePositionCheckStep | null all: LabwarePositionCheckStep[] } - -export type LPCWizardAction = - | InitialPositionAction - | FinalPositionAction - | ProceedStepAction diff --git a/app/src/organisms/LabwarePositionCheck/redux/useLPCReducer.ts b/app/src/organisms/LabwarePositionCheck/redux/useLPCReducer.ts deleted file mode 100644 index 05efeabefe7..00000000000 --- a/app/src/organisms/LabwarePositionCheck/redux/useLPCReducer.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { useReducer } from 'react' - -import { LPCReducer } from './reducer' - -import type { Dispatch } from 'react' -import type { LPCWizardAction, LPCWizardState } from './types' - -interface UseLPCReducerResult { - state: LPCWizardState - dispatch: Dispatch -} - -export function useLPCReducer( - initialState: LPCWizardState -): UseLPCReducerResult { - const [state, dispatch] = useReducer(LPCReducer, initialState) - - return { state, dispatch } -} diff --git a/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx b/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx index 08d1d0f3fff..d33f9aa46f3 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx @@ -1,5 +1,6 @@ import { Trans, useTranslation } from 'react-i18next' import { css } from 'styled-components' +import { useSelector } from 'react-redux' import { RESPONSIVENESS, @@ -13,31 +14,43 @@ import { GenericWizardTile } from '/app/molecules/GenericWizardTile' import { selectActivePipette, selectActivePipetteChannelCount, -} from '/app/organisms/LabwarePositionCheck/redux' +} from '/app/redux/protocol-runs' +import { getIsOnDevice } from '/app/redux/config' import attachProbe1 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_1.webm' import attachProbe8 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_8.webm' import attachProbe96 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_96.webm' import type { AttachProbeStep, LPCStepProps } from '../types' +import type { State } from '/app/redux/types' +import type { LPCWizardState } from '/app/redux/protocol-runs' export function AttachProbe({ + runId, proceed, commandUtils, - state, step, }: LPCStepProps): JSX.Element { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) - const { isOnDevice, steps } = state + const isOnDevice = useSelector(getIsOnDevice) + const { steps } = useSelector( + (state: State) => state.protocolRuns[runId]?.lpc as LPCWizardState + ) const { pipetteId } = step const { - setShowUnableToDetect, - unableToDetect, createProbeAttachmentHandler, handleCheckItemsPrepModules, toggleRobotMoving, + setShowUnableToDetect, + unableToDetect, } = commandUtils - const pipette = selectActivePipette(step, state) + const pipette = useSelector((state: State) => + selectActivePipette(step, runId, state) + ) + const channels = useSelector((state: State) => + selectActivePipetteChannelCount(step, runId, state) + ) + const handleProbeAttached = createProbeAttachmentHandler( pipetteId, pipette, @@ -48,8 +61,6 @@ export function AttachProbe({ probeLocation: string probeVideoSrc: string } => { - const channels = selectActivePipetteChannelCount(step, state) - switch (channels) { case 1: return { probeLocation: '', probeVideoSrc: attachProbe1 } @@ -63,6 +74,12 @@ export function AttachProbe({ } })() + const handleProbeCheck = (): void => { + void toggleRobotMoving(true) + .then(() => handleProbeAttached()) + .finally(() => toggleRobotMoving(false)) + } + const handleProceed = (): void => { void toggleRobotMoving(true) .then(() => handleProbeAttached()) @@ -73,7 +90,7 @@ export function AttachProbe({ if (unableToDetect) { return ( diff --git a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx index b520fe1dfab..c63568c4c60 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx @@ -1,4 +1,5 @@ import { Trans, useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' import { Flex, @@ -12,25 +13,38 @@ import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' import { TwoUpTileLayout } from './TwoUpTileLayout' import { ViewOffsets } from './ViewOffsets' import { SmallButton } from '/app/atoms/buttons' +import { getIsOnDevice } from '/app/redux/config' +import { selectActivePipette } from '/app/redux/protocol-runs' import type { LPCStepProps, BeforeBeginningStep, + LabwarePositionCheckStep, } from '/app/organisms/LabwarePositionCheck/types' +import type { State } from '/app/redux/types' +import type { LPCWizardState } from '/app/redux/protocol-runs' // TODO(BC, 09/01/23): replace updated support article link for LPC on OT-2/Flex const SUPPORT_PAGE_URL = 'https://support.opentrons.com/s/ot2-calibration' export function BeforeBeginning({ + runId, proceed, commandUtils, - state, }: LPCStepProps): JSX.Element { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) - const { isOnDevice, protocolName, labwareDefs, existingOffsets } = state + const isOnDevice = useSelector(getIsOnDevice) + const activePipette = useSelector((state: State) => { + const step = state.protocolRuns[runId]?.lpc?.steps + .current as LabwarePositionCheckStep + return selectActivePipette(step, runId, state) ?? null + }) + const { protocolName, labwareDefs, existingOffsets } = useSelector( + (state: State) => state.protocolRuns[runId]?.lpc as LPCWizardState + ) const { createStartLPCHandler, toggleRobotMoving } = commandUtils - const handleStartLPC = createStartLPCHandler(proceed) + const handleStartLPC = createStartLPCHandler(activePipette, proceed) const requiredEquipmentList = [ { diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/LiveOffsetValue.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/LiveOffsetValue.tsx index ee3b08e5493..18ead803548 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/LiveOffsetValue.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/LiveOffsetValue.tsx @@ -1,6 +1,7 @@ import { Fragment } from 'react' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' +import { useSelector } from 'react-redux' import { ALIGN_CENTER, @@ -15,6 +16,8 @@ import { TYPOGRAPHY, } from '@opentrons/components' +import { getIsOnDevice } from '/app/redux/config' + import type { StyleProps } from '@opentrons/components' import type { CheckPositionsStep, @@ -30,15 +33,16 @@ interface OffsetVectorProps extends StyleProps { export function LiveOffsetValue( props: OffsetVectorProps & LPCStepProps ): JSX.Element { - const { x, y, z, state, ...styleProps } = props + const { x, y, z, ...styleProps } = props const { i18n, t } = useTranslation('labware_position_check') + const isOnDevice = useSelector(getIsOnDevice) return ( - + {[x, y, z].map((axis, index) => ( diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/index.tsx index 69dd0783cf3..26a0dd599e6 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/index.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' +import { useSelector } from 'react-redux' import { ALIGN_CENTER, @@ -39,7 +40,8 @@ import { selectActivePipette, selectIsActiveLwTipRack, selectItemLabwareDef, -} from '/app/organisms/LabwarePositionCheck/redux' +} from '/app/redux/protocol-runs' +import { getIsOnDevice } from '/app/redux/config' import type { ReactNode } from 'react' import type { LabwareDefinition2 } from '@opentrons/shared-data' @@ -49,9 +51,11 @@ import type { CheckPositionsStep, LPCStepProps, } from '/app/organisms/LabwarePositionCheck/types' +import type { LPCWizardState } from '/app/redux/protocol-runs' import levelProbeWithTip from '/app/assets/images/lpc_level_probe_with_tip.svg' import levelProbeWithLabware from '/app/assets/images/lpc_level_probe_with_labware.svg' +import type { State } from '/app/redux/types' const DECK_MAP_VIEWBOX = '-10 -10 150 105' const LPC_HELP_LINK_URL = @@ -67,23 +71,39 @@ interface JogToWellProps extends LPCStepProps { export function JogToWell(props: JogToWellProps): JSX.Element { const { + runId, header, body, handleConfirmPosition, handleGoBack, handleJog, - state, } = props const { t } = useTranslation(['labware_position_check', 'shared']) - const { isOnDevice, steps } = state + const { steps } = useSelector( + (state: State) => state.protocolRuns[runId]?.lpc as LPCWizardState + ) const { current: currentStep } = steps - const initialPosition = - selectActiveLwInitialPosition(currentStep, state) ?? IDENTITY_VECTOR - const pipetteName = - selectActivePipette(currentStep, state)?.pipetteName ?? 'p1000_single' - const itemLwDef = selectItemLabwareDef(state) as LabwareDefinition2 // Safe if component only used with CheckItem step. - const isTipRack = selectIsActiveLwTipRack(state) + const isOnDevice = useSelector(getIsOnDevice) + const initialPosition = useSelector( + (state: State) => + selectActiveLwInitialPosition(currentStep, runId, state) ?? + IDENTITY_VECTOR + ) + const pipetteName = useSelector( + (state: State) => + selectActivePipette(currentStep, runId, state)?.pipetteName ?? + 'p1000_single' + ) + const itemLwDef = useSelector( + selectItemLabwareDef(runId) + ) as LabwareDefinition2 // Safe if component only used with CheckItem step. + const isTipRack = useSelector((state: State) => + selectIsActiveLwTipRack(runId, state) + ) + const activeLwExistingOffset = useSelector((state: State) => + selectActiveLwExistingOffset(runId, state) + ) const [joggedPosition, setJoggedPosition] = useState( initialPosition @@ -92,7 +112,7 @@ export function JogToWell(props: JogToWellProps): JSX.Element { const levelSrc = isTipRack ? levelProbeWithTip : levelProbeWithLabware const liveOffset = getVectorSum( - selectActiveLwExistingOffset(state), + activeLwExistingOffset, getVectorDifference(joggedPosition, initialPosition) ) diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PlaceItemInstruction.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PlaceItemInstruction.tsx index 7c69fc426e4..7fc4487b278 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PlaceItemInstruction.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PlaceItemInstruction.tsx @@ -1,12 +1,14 @@ import { Trans, useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' import { TYPOGRAPHY, LegacyStyledText } from '@opentrons/components' import { selectActiveAdapterDisplayName, selectLwDisplayName, -} from '/app/organisms/LabwarePositionCheck/redux' +} from '/app/redux/protocol-runs' +import type { State } from '/app/redux/types' import type { CheckPositionsStep, LPCStepProps, @@ -19,17 +21,21 @@ interface PlaceItemInstructionProps extends LPCStepProps { } export function PlaceItemInstruction({ + runId, step, isLwTiprack, slotOnlyDisplayLocation, fullDisplayLocation, - state, }: PlaceItemInstructionProps): JSX.Element { const { t } = useTranslation('labware_position_check') const { adapterId } = step - const labwareDisplayName = selectLwDisplayName(state) - const adapterDisplayName = selectActiveAdapterDisplayName(state) + const labwareDisplayName = useSelector((state: State) => + selectLwDisplayName(runId, state) + ) + const adapterDisplayName = useSelector((state: State) => + selectActiveAdapterDisplayName(runId, state) + ) if (isLwTiprack) { return ( diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PrepareSpace.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PrepareSpace.tsx index c87bea5ac45..d308d986a11 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PrepareSpace.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PrepareSpace.tsx @@ -1,5 +1,6 @@ import styled, { css } from 'styled-components' import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' import { DIRECTION_COLUMN, @@ -23,6 +24,8 @@ import { import { SmallButton } from '/app/atoms/buttons' import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' +import { selectItemLabwareDef } from '/app/redux/protocol-runs' +import { getIsOnDevice } from '/app/redux/config' import type { ReactNode } from 'react' import type { LabwareDefinition2 } from '@opentrons/shared-data' @@ -30,7 +33,8 @@ import type { CheckPositionsStep, LPCStepProps, } from '/app/organisms/LabwarePositionCheck/types' -import { selectItemLabwareDef } from '/app/organisms/LabwarePositionCheck/redux' +import type { State } from '/app/redux/types' +import type { LPCWizardState } from '/app/redux/protocol-runs' const LPC_HELP_LINK_URL = 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' @@ -42,15 +46,20 @@ interface PrepareSpaceProps extends LPCStepProps { } export function PrepareSpace({ - state, + runId, header, body, confirmPlacement, }: PrepareSpaceProps): JSX.Element { const { i18n, t } = useTranslation(['labware_position_check', 'shared']) - const { protocolData, isOnDevice, deckConfig, steps } = state + const { protocolData, deckConfig, steps } = useSelector( + (state: State) => state.protocolRuns[runId]?.lpc as LPCWizardState + ) + const isOnDevice = useSelector(getIsOnDevice) + const labwareDef = useSelector( + selectItemLabwareDef(runId) + ) as LabwareDefinition2 // CheckItem always has lwId on step. const { location } = steps.current as CheckPositionsStep // safely enforced by iface - const labwareDef = selectItemLabwareDef(state) as LabwareDefinition2 // CheckItem always has lwId on step. return ( diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx index cede9664579..7f6f124e694 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx @@ -1,4 +1,5 @@ import { Trans, useTranslation } from 'react-i18next' +import { useDispatch, useSelector } from 'react-redux' import { DIRECTION_COLUMN, Flex, LegacyStyledText } from '@opentrons/components' @@ -9,7 +10,7 @@ import { UnorderedList } from '/app/molecules/UnorderedList' import { setFinalPosition, setInitialPosition, -} from '/app/organisms/LabwarePositionCheck/redux/actions' +} from '/app/redux/protocol-runs/actions' import { JogToWell } from './JogToWell' import { PrepareSpace } from './PrepareSpace' import { PlaceItemInstruction } from './PlaceItemInstruction' @@ -17,18 +18,21 @@ import { selectActiveLwInitialPosition, selectActivePipette, selectIsActiveLwTipRack, -} from '/app/organisms/LabwarePositionCheck/redux' +} from '/app/redux/protocol-runs' +import { getIsOnDevice } from '/app/redux/config' import type { CheckPositionsStep, LPCStepProps, } from '/app/organisms/LabwarePositionCheck/types' import type { DisplayLocationParams } from '/app/local-resources/labware' +import type { State } from '/app/redux/types' +import type { LPCWizardState } from '/app/redux/protocol-runs' export function CheckItem( props: LPCStepProps ): JSX.Element { - const { state, dispatch, proceed, commandUtils, step } = props + const { runId, proceed, commandUtils, step } = props const { labwareId, location } = step const { handleJog, @@ -39,13 +43,24 @@ export function CheckItem( handleValidMoveToMaintenancePosition, toggleRobotMoving, } = commandUtils - const { isOnDevice, protocolData, labwareDefs, steps } = state + const dispatch = useDispatch() + + const isOnDevice = useSelector(getIsOnDevice) + const { protocolData, labwareDefs, steps } = useSelector( + (state: State) => state.protocolRuns[runId]?.lpc as LPCWizardState + ) const { t } = useTranslation(['labware_position_check', 'shared']) const { t: commandTextT } = useTranslation('protocol_command_text') - const pipette = selectActivePipette(step, state) - const initialPosition = selectActiveLwInitialPosition(step, state) - const isLwTiprack = selectIsActiveLwTipRack(state) + const pipette = useSelector( + (state: State) => selectActivePipette(step, runId, state) ?? null + ) + const initialPosition = useSelector((state: State) => + selectActiveLwInitialPosition(step, runId, state) + ) + const isLwTiprack = useSelector((state: State) => + selectIsActiveLwTipRack(runId, state) + ) const buildDisplayParams = (): Omit< DisplayLocationParams, @@ -73,7 +88,7 @@ export function CheckItem( .then(() => handleConfirmLwModulePlacement({ step })) .then(position => { dispatch( - setInitialPosition({ + setInitialPosition(runId, { labwareId, location, position, @@ -95,7 +110,7 @@ export function CheckItem( ) .then(position => { dispatch( - setFinalPosition({ + setFinalPosition(runId, { labwareId, location, position, @@ -103,10 +118,10 @@ export function CheckItem( ) }) .then(() => { - if (state.steps.next?.section === NAV_STEPS.CHECK_POSITIONS) { + if (steps.next?.section === NAV_STEPS.CHECK_POSITIONS) { return handleCheckItemsPrepModules(steps.next) } else { - return handleValidMoveToMaintenancePosition(steps.next) + return handleValidMoveToMaintenancePosition(pipette, steps.next) } }) .finally(() => toggleRobotMoving(false)) @@ -117,7 +132,7 @@ export function CheckItem( .then(() => handleResetLwModulesOnDeck({ step })) .then(() => { dispatch( - setInitialPosition({ + setInitialPosition(runId, { labwareId, location, position: null, diff --git a/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx index 78c46a656c9..671caaf6ba9 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx @@ -1,5 +1,6 @@ import { useTranslation } from 'react-i18next' import { css } from 'styled-components' +import { useSelector } from 'react-redux' import { LegacyStyledText, @@ -12,26 +13,34 @@ import { GenericWizardTile } from '/app/molecules/GenericWizardTile' import { selectActivePipette, selectActivePipetteChannelCount, -} from '/app/organisms/LabwarePositionCheck/redux' +} from '/app/redux/protocol-runs' import detachProbe1 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_1.webm' import detachProbe8 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_8.webm' import detachProbe96 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_96.webm' import type { DetachProbeStep, LPCStepProps } from '../types' +import type { State } from '/app/redux/types' +import type { StepsInfo } from '/app/organisms/LabwarePositionCheck/redux/types' export const DetachProbe = ({ - state, + runId, proceed, commandUtils, }: LPCStepProps): JSX.Element => { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) - const { current: currentStep } = state.steps + const { current: currentStep } = useSelector( + (state: State) => state.protocolRuns[runId]?.lpc?.steps as StepsInfo + ) const { createProbeDetachmentHandler, toggleRobotMoving } = commandUtils - const pipette = selectActivePipette(currentStep, state) - const probeVideoSrc = ((): string => { - const channels = selectActivePipetteChannelCount(currentStep, state) + const pipette = useSelector((state: State) => + selectActivePipette(currentStep, runId, state) + ) + const channels = useSelector((state: State) => + selectActivePipetteChannelCount(currentStep, runId, state) + ) + const probeVideoSrc = ((): string => { switch (channels) { case 1: return detachProbe1 diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx index 869cb6a79f8..b5514344c24 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx @@ -2,6 +2,7 @@ import { Fragment } from 'react' import styled from 'styled-components' import isEqual from 'lodash/isEqual' import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' import { FLEX_ROBOT_TYPE, IDENTITY_VECTOR } from '@opentrons/shared-data' import { @@ -13,7 +14,7 @@ import { TYPOGRAPHY, } from '@opentrons/components' -import { selectLwDisplayName } from '/app/organisms/LabwarePositionCheck/redux' +import { selectLwDisplayName } from '/app/redux/protocol-runs' import { getLabwareDisplayLocation } from '/app/local-resources/labware' import type { LabwareDefinition2 } from '@opentrons/shared-data' @@ -22,6 +23,8 @@ import type { LPCStepProps, ResultsSummaryStep, } from '/app/organisms/LabwarePositionCheck/types' +import type { LPCWizardState } from '/app/redux/protocol-runs' +import type { State } from '/app/redux/types' interface OffsetTableProps extends LPCStepProps { offsets: LabwareOffsetCreateData[] @@ -30,10 +33,15 @@ interface OffsetTableProps extends LPCStepProps { export function OffsetTable({ offsets, + runId, labwareDefinitions, - state, }: OffsetTableProps): JSX.Element { - const { protocolData } = state + const { protocolData } = useSelector( + (state: State) => state.protocolRuns[runId]?.lpc as LPCWizardState + ) + const lwDisplayName = useSelector((state: State) => + selectLwDisplayName(runId, state) + ) const { t } = useTranslation('labware_position_check') @@ -75,9 +83,7 @@ export function OffsetTable({ - - {selectLwDisplayName(state)} - + {lwDisplayName} { offsetsToApply: LabwareOffsetCreateData[] } export function TableComponent(props: TableComponentProps): JSX.Element { - const { state, offsetsToApply } = props - const { isOnDevice, labwareDefs } = state + const { offsetsToApply, runId } = props + const isOnDevice = useSelector(getIsOnDevice) + const { labwareDefs } = useSelector( + (state: State) => state.protocolRuns[runId]?.lpc as LPCWizardState + ) return isOnDevice ? ( ): JSX.Element { - const { commandUtils, state } = props - const { protocolData, isOnDevice } = state + const { commandUtils, runId } = props + const isOnDevice = useSelector(getIsOnDevice) + const { protocolData } = useSelector( + (state: State) => state.protocolRuns[runId]?.lpc as LPCWizardState + ) const { isApplyingOffsets, handleApplyOffsetsAndClose, diff --git a/app/src/organisms/LabwarePositionCheck/types/content.ts b/app/src/organisms/LabwarePositionCheck/types/content.ts index 142a105ad95..d32b02191bb 100644 --- a/app/src/organisms/LabwarePositionCheck/types/content.ts +++ b/app/src/organisms/LabwarePositionCheck/types/content.ts @@ -1,14 +1,10 @@ -import type { Dispatch } from 'react' -import type { - LPCWizardAction, - LPCWizardState, -} from '/app/organisms/LabwarePositionCheck/redux' import type { UseLPCCommandsResult } from '/app/organisms/LabwarePositionCheck/hooks' import type { LPCWizardFlexProps } from '/app/organisms/LabwarePositionCheck/LPCWizardFlex' -export type LPCWizardContentProps = Pick & { +export type LPCWizardContentProps = Pick< + LPCWizardFlexProps, + 'onCloseClick' | 'runId' +> & { proceed: () => void - dispatch: Dispatch - state: LPCWizardState commandUtils: UseLPCCommandsResult } diff --git a/app/src/redux/protocol-runs/actions/index.ts b/app/src/redux/protocol-runs/actions/index.ts index 5a774a6b36e..9571602c772 100644 --- a/app/src/redux/protocol-runs/actions/index.ts +++ b/app/src/redux/protocol-runs/actions/index.ts @@ -1 +1,2 @@ export * from './setup' +export * from './lpc' diff --git a/app/src/redux/protocol-runs/actions/lpc.ts b/app/src/redux/protocol-runs/actions/lpc.ts new file mode 100644 index 00000000000..5ec472e094c --- /dev/null +++ b/app/src/redux/protocol-runs/actions/lpc.ts @@ -0,0 +1,50 @@ +import { + PROCEED_STEP, + SET_INITIAL_POSITION, + SET_FINAL_POSITION, + START_LPC, + FINISH_LPC, +} from '../constants' + +import type { + FinalPositionAction, + InitialPositionAction, + StartLPCAction, + LPCWizardState, + PositionParams, + ProceedStepAction, + FinishLPCAction, +} from '../types' + +export const proceedStep = (runId: string): ProceedStepAction => ({ + type: PROCEED_STEP, + payload: { runId }, +}) +export const setInitialPosition = ( + runId: string, + params: PositionParams +): InitialPositionAction => ({ + type: SET_INITIAL_POSITION, + payload: { ...params, runId }, +}) + +export const setFinalPosition = ( + runId: string, + params: PositionParams +): FinalPositionAction => ({ + type: SET_FINAL_POSITION, + payload: { ...params, runId }, +}) + +export const startLPC = ( + runId: string, + state: LPCWizardState +): StartLPCAction => ({ + type: START_LPC, + payload: { runId, state }, +}) + +export const closeLPC = (runId: string): FinishLPCAction => ({ + type: FINISH_LPC, + payload: { runId }, +}) diff --git a/app/src/redux/protocol-runs/constants/index.ts b/app/src/redux/protocol-runs/constants/index.ts index 5a774a6b36e..9571602c772 100644 --- a/app/src/redux/protocol-runs/constants/index.ts +++ b/app/src/redux/protocol-runs/constants/index.ts @@ -1 +1,2 @@ export * from './setup' +export * from './lpc' diff --git a/app/src/organisms/LabwarePositionCheck/redux/constants.ts b/app/src/redux/protocol-runs/constants/lpc.ts similarity index 67% rename from app/src/organisms/LabwarePositionCheck/redux/constants.ts rename to app/src/redux/protocol-runs/constants/lpc.ts index d4803a0cadf..669c8ec503a 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/constants.ts +++ b/app/src/redux/protocol-runs/constants/lpc.ts @@ -1,3 +1,5 @@ +export const START_LPC = 'START_LPC' +export const FINISH_LPC = 'FINISH_LPC' export const PROCEED_STEP = 'PROCEED_STEP' export const SET_INITIAL_POSITION = 'SET_INITIAL_POSITION' export const SET_FINAL_POSITION = 'SET_FINAL_POSITION' diff --git a/app/src/redux/protocol-runs/reducer/index.ts b/app/src/redux/protocol-runs/reducer/index.ts index c346eeee0c2..504cb12b749 100644 --- a/app/src/redux/protocol-runs/reducer/index.ts +++ b/app/src/redux/protocol-runs/reducer/index.ts @@ -1,11 +1,12 @@ import * as Constants from '../constants' +import { LPCReducer } from './lpc' import type { Reducer } from 'redux' import type { Action } from '../../types' import type { ProtocolRunState } from '../types' -import { setup } from './setup' +import { setupReducer } from './setup' const INITIAL_STATE: ProtocolRunState = {} @@ -23,10 +24,48 @@ export const protocolRunReducer: Reducer = ( ...state, [runId]: { ...currentRunState, - setup: setup(currentRunState?.setup, action), + setup: setupReducer(currentRunState?.setup, action), }, } } + + case Constants.START_LPC: { + const runId = action.payload.runId + const lpcState = action.payload.state + const currentRunState = state[runId] + + if (currentRunState != null && currentRunState.lpc == null) { + return { + ...state, + [runId]: { + ...currentRunState, + lpc: lpcState, + }, + } + } else { + return state + } + } + case Constants.FINISH_LPC: + case Constants.PROCEED_STEP: + case Constants.SET_INITIAL_POSITION: + case Constants.SET_FINAL_POSITION: { + const runId = action.payload.runId + const currentRunState = state[runId] + + if (currentRunState?.lpc == null) { + return state + } + + return { + ...state, + [runId]: { + ...currentRunState, + lpc: LPCReducer(currentRunState.lpc, action), + }, + } + } + default: return state } diff --git a/app/src/organisms/LabwarePositionCheck/redux/reducer.ts b/app/src/redux/protocol-runs/reducer/lpc.ts similarity index 85% rename from app/src/organisms/LabwarePositionCheck/redux/reducer.ts rename to app/src/redux/protocol-runs/reducer/lpc.ts index 091def56cd3..5417f55de22 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/reducer.ts +++ b/app/src/redux/protocol-runs/reducer/lpc.ts @@ -1,17 +1,17 @@ -import { updateWorkingOffset } from './transforms' import { PROCEED_STEP, SET_INITIAL_POSITION, SET_FINAL_POSITION, -} from './constants' + FINISH_LPC, +} from '../constants' +import { updateWorkingOffset } from './transforms' -import type { LPCWizardAction } from './types' -import type { LPCWizardState } from '.' +import type { LPCWizardAction, LPCWizardState } from '../types' export function LPCReducer( state: LPCWizardState, action: LPCWizardAction -): LPCWizardState { +): LPCWizardState | undefined { switch (action.type) { case PROCEED_STEP: { const { currentStepIndex, totalStepCount } = state.steps @@ -42,6 +42,9 @@ export function LPCReducer( workingOffsets: updateWorkingOffset(state.workingOffsets, action), } + case FINISH_LPC: + return undefined + default: return state } diff --git a/app/src/redux/protocol-runs/reducer/setup.ts b/app/src/redux/protocol-runs/reducer/setup.ts index 74bb4fd2b4b..6cac26dc22f 100644 --- a/app/src/redux/protocol-runs/reducer/setup.ts +++ b/app/src/redux/protocol-runs/reducer/setup.ts @@ -11,7 +11,7 @@ export const INITIAL_RUN_SETUP_STATE: RunSetupStatus = { [Constants.LIQUID_SETUP_STEP_KEY]: INITIAL_SETUP_STEP_STATE, } -export function setup( +export function setupReducer( state: RunSetupStatus = INITIAL_RUN_SETUP_STATE, action: ProtocolRunAction ): RunSetupStatus { diff --git a/app/src/redux/protocol-runs/reducer/transforms/index.ts b/app/src/redux/protocol-runs/reducer/transforms/index.ts new file mode 100644 index 00000000000..5c5cf75aeae --- /dev/null +++ b/app/src/redux/protocol-runs/reducer/transforms/index.ts @@ -0,0 +1 @@ +export * from './lpc' diff --git a/app/src/organisms/LabwarePositionCheck/redux/transforms.ts b/app/src/redux/protocol-runs/reducer/transforms/lpc.ts similarity index 94% rename from app/src/organisms/LabwarePositionCheck/redux/transforms.ts rename to app/src/redux/protocol-runs/reducer/transforms/lpc.ts index 39dc482ce5c..3d08fadee62 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/transforms.ts +++ b/app/src/redux/protocol-runs/reducer/transforms/lpc.ts @@ -1,6 +1,6 @@ import isEqual from 'lodash/isEqual' -import type { LPCWizardAction, WorkingOffset } from './types' +import type { LPCWizardAction, WorkingOffset } from '../../types' export function updateWorkingOffset( workingOffsets: WorkingOffset[], diff --git a/app/src/redux/protocol-runs/selectors/index.ts b/app/src/redux/protocol-runs/selectors/index.ts index 5a774a6b36e..9571602c772 100644 --- a/app/src/redux/protocol-runs/selectors/index.ts +++ b/app/src/redux/protocol-runs/selectors/index.ts @@ -1 +1,2 @@ export * from './setup' +export * from './lpc' diff --git a/app/src/organisms/LabwarePositionCheck/redux/selectors/index.ts b/app/src/redux/protocol-runs/selectors/lpc/index.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/redux/selectors/index.ts rename to app/src/redux/protocol-runs/selectors/lpc/index.ts diff --git a/app/src/redux/protocol-runs/selectors/lpc/labware.ts b/app/src/redux/protocol-runs/selectors/lpc/labware.ts new file mode 100644 index 00000000000..afce4ae46e1 --- /dev/null +++ b/app/src/redux/protocol-runs/selectors/lpc/labware.ts @@ -0,0 +1,251 @@ +import { createSelector } from 'reselect' +import isEqual from 'lodash/isEqual' + +import { + getIsTiprack, + getLabwareDisplayName, + getLabwareDefURI, + getVectorSum, + getVectorDifference, + IDENTITY_VECTOR, +} from '@opentrons/shared-data' + +import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analysis' +import { getItemLabwareDef } from './transforms' + +import type { Selector } from 'reselect' +import type { VectorOffset, LabwareOffsetLocation } from '@opentrons/api-client' +import type { LabwareDefinition2, Coordinates } from '@opentrons/shared-data' +import type { State } from '../../../types' + +// TODO(jh, 01-16-25): Revisit once LPC `step` refactors are completed. +// eslint-disable-next-line opentrons/no-imports-across-applications +import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' + +// TODO(jh, 01-13-25): Remove the explicit type casting after restructuring "step". +// TODO(jh, 01-17-25): As LPC selectors become finalized, wrap them in createSelector. + +export const selectActiveLwInitialPosition = ( + step: LabwarePositionCheckStep | null, + runId: string, + state: State +): VectorOffset | null => { + const { workingOffsets } = state.protocolRuns[runId]?.lpc ?? {} + + if (step != null && workingOffsets != null) { + const labwareId = 'labwareId' in step ? step.labwareId : '' + const location = 'location' in step ? step.location : '' + + return ( + workingOffsets.find( + o => + o.labwareId === labwareId && + isEqual(o.location, location) && + o.initialPosition != null + )?.initialPosition ?? null + ) + } else { + if (workingOffsets == null) { + console.warn('LPC state not initalized before selector use.') + } + + return null + } +} + +export const selectActiveLwExistingOffset = ( + runId: string, + state: State +): VectorOffset => { + const { existingOffsets, steps } = state.protocolRuns[runId]?.lpc ?? {} + + if (existingOffsets == null || steps == null) { + console.warn('LPC state not initalized before selector use.') + return IDENTITY_VECTOR + } else if ( + !('labwareId' in steps.current) || + !('location' in steps.current) || + !('slotName' in steps.current.location) + ) { + console.warn( + `No labwareId or location in current step: ${steps.current.section}` + ) + return IDENTITY_VECTOR + } else { + const lwUri = getLabwareDefURI( + getItemLabwareDefFrom(runId, state) as LabwareDefinition2 + ) + + return ( + getCurrentOffsetForLabwareInLocation( + existingOffsets, + lwUri, + steps.current.location + )?.vector ?? IDENTITY_VECTOR + ) + } +} + +export interface SelectOffsetsToApplyResult { + definitionUri: string + location: LabwareOffsetLocation + vector: Coordinates +} + +export const selectOffsetsToApply = ( + runId: string +): Selector => + createSelector( + (state: State) => state.protocolRuns[runId]?.lpc?.workingOffsets, + (state: State) => state.protocolRuns[runId]?.lpc?.protocolData, + (state: State) => state.protocolRuns[runId]?.lpc?.existingOffsets, + (workingOffsets, protocolData, existingOffsets) => { + if ( + workingOffsets == null || + protocolData == null || + existingOffsets == null + ) { + console.warn('LPC state not initalized before selector use.') + return [] + } + + return workingOffsets.map( + ({ initialPosition, finalPosition, labwareId, location }) => { + const definitionUri = + protocolData.labware.find(l => l.id === labwareId)?.definitionUri ?? + null + + if ( + finalPosition == null || + initialPosition == null || + definitionUri == null + ) { + throw new Error( + `cannot create offset for labware with id ${labwareId}, in location ${JSON.stringify( + location + )}, with initial position ${String( + initialPosition + )}, and final position ${String(finalPosition)}` + ) + } else { + const existingOffset = + getCurrentOffsetForLabwareInLocation( + existingOffsets, + definitionUri, + location + )?.vector ?? IDENTITY_VECTOR + const vector = getVectorSum( + existingOffset, + getVectorDifference(finalPosition, initialPosition) + ) + return { definitionUri, location, vector } + } + } + ) + } + ) + +export const selectIsActiveLwTipRack = ( + runId: string, + state: State +): boolean => { + const { current } = state.protocolRuns[runId]?.lpc?.steps ?? {} + + if (current != null && 'labwareId' in current) { + return getIsTiprack( + getItemLabwareDefFrom(runId, state) as LabwareDefinition2 + ) + } else { + console.warn( + 'No labwareId in step or LPC state not initalized before selector use.' + ) + return false + } +} + +export const selectLwDisplayName = (runId: string, state: State): string => { + const { current } = state.protocolRuns[runId]?.lpc?.steps ?? {} + + if (current != null && 'labwareId' in current) { + return getLabwareDisplayName( + getItemLabwareDefFrom(runId, state) as LabwareDefinition2 + ) + } else { + console.warn( + 'No labwareId in step or LPC state not initalized before selector use.' + ) + return '' + } +} + +export const selectActiveAdapterDisplayName = ( + runId: string, + state: State +): string => { + const { protocolData, labwareDefs, steps } = + state.protocolRuns[runId]?.lpc ?? {} + + if (protocolData == null || labwareDefs == null || steps == null) { + console.warn('LPC state not initialized before selector use.') + return '' + } + + return 'adapterId' in steps.current && steps.current.adapterId != null + ? getItemLabwareDef({ + labwareId: steps.current.adapterId, + loadedLabware: protocolData.labware, + labwareDefs, + })?.metadata.displayName ?? '' + : '' +} + +export const selectItemLabwareDef = ( + runId: string +): Selector => + createSelector( + (state: State) => state.protocolRuns[runId]?.lpc?.steps.current, + (state: State) => state.protocolRuns[runId]?.lpc?.labwareDefs, + (state: State) => state.protocolRuns[runId]?.lpc?.protocolData.labware, + (current, labwareDefs, loadedLabware) => { + const labwareId = + current != null && 'labwareId' in current ? current.labwareId : '' + + if (labwareId === '' || labwareDefs == null || loadedLabware == null) { + console.warn( + `No labwareId associated with step: ${current?.section} or LPC state not initialized before selector use.` + ) + return null + } + + return getItemLabwareDef({ + labwareId, + labwareDefs, + loadedLabware, + }) + } + ) + +const getItemLabwareDefFrom = ( + runId: string, + state: State +): LabwareDefinition2 | null => { + const current = state.protocolRuns[runId]?.lpc?.steps.current + const labwareDefs = state.protocolRuns[runId]?.lpc?.labwareDefs + const loadedLabware = state.protocolRuns[runId]?.lpc?.protocolData.labware + + const labwareId = + current != null && 'labwareId' in current ? current.labwareId : '' + + if (labwareId === '' || labwareDefs == null || loadedLabware == null) { + console.warn( + `No labwareId associated with step: ${current?.section} or LPC state not initialized before selector use.` + ) + return null + } + + return getItemLabwareDef({ + labwareId, + labwareDefs, + loadedLabware, + }) +} diff --git a/app/src/organisms/LabwarePositionCheck/redux/selectors/pipettes.ts b/app/src/redux/protocol-runs/selectors/lpc/pipettes.ts similarity index 52% rename from app/src/organisms/LabwarePositionCheck/redux/selectors/pipettes.ts rename to app/src/redux/protocol-runs/selectors/lpc/pipettes.ts index a56316bca3d..1070e80946e 100644 --- a/app/src/organisms/LabwarePositionCheck/redux/selectors/pipettes.ts +++ b/app/src/redux/protocol-runs/selectors/lpc/pipettes.ts @@ -1,28 +1,37 @@ import { getPipetteNameSpecs } from '@opentrons/shared-data' -import type { LPCWizardState } from '/app/organisms/LabwarePositionCheck/redux' import type { LoadedPipette, PipetteChannels } from '@opentrons/shared-data' + +// TODO(jh, 01-16-25): Revisit once LPC `step` refactors are completed. +// eslint-disable-next-line opentrons/no-imports-across-applications import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' +import type { State } from '../../../types' export const selectActivePipette = ( step: LabwarePositionCheckStep, - state: LPCWizardState -): LoadedPipette | undefined => { - const { protocolData } = state + runId: string, + state: State +): LoadedPipette | null => { + const { protocolData } = state.protocolRuns[runId]?.lpc ?? {} const pipetteId = 'pipetteId' in step ? step.pipetteId : '' if (pipetteId === '') { console.warn(`No matching pipette found for pipetteId ${pipetteId}`) + } else if (protocolData == null) { + console.warn('LPC state not initalized before selector use.') } - return protocolData.pipettes.find(pipette => pipette.id === pipetteId) + return ( + protocolData?.pipettes.find(pipette => pipette.id === pipetteId) ?? null + ) } export const selectActivePipetteChannelCount = ( step: LabwarePositionCheckStep, - state: LPCWizardState + runId: string, + state: State ): PipetteChannels => { - const pipetteName = selectActivePipette(step, state)?.pipetteName + const pipetteName = selectActivePipette(step, runId, state)?.pipetteName return pipetteName != null ? getPipetteNameSpecs(pipetteName)?.channels ?? 1 diff --git a/app/src/organisms/LabwarePositionCheck/utils.ts b/app/src/redux/protocol-runs/selectors/lpc/transforms.ts similarity index 100% rename from app/src/organisms/LabwarePositionCheck/utils.ts rename to app/src/redux/protocol-runs/selectors/lpc/transforms.ts diff --git a/app/src/redux/protocol-runs/types/index.ts b/app/src/redux/protocol-runs/types/index.ts index acae8290e8f..2f62ed0fd8f 100644 --- a/app/src/redux/protocol-runs/types/index.ts +++ b/app/src/redux/protocol-runs/types/index.ts @@ -1,11 +1,16 @@ -import type { RunSetupStatus } from './setup' +import type { RunSetupStatus, RunSetupStepsAction } from './setup' +import type { LPCWizardAction, LPCWizardState } from './lpc' export * from './setup' +export * from './lpc' export interface PerRunUIState { setup: RunSetupStatus + lpc?: LPCWizardState } export type ProtocolRunState = Partial<{ readonly [runId: string]: PerRunUIState }> + +export type ProtocolRunAction = RunSetupStepsAction | LPCWizardAction diff --git a/app/src/redux/protocol-runs/types/lpc.ts b/app/src/redux/protocol-runs/types/lpc.ts new file mode 100644 index 00000000000..aec42e6eb98 --- /dev/null +++ b/app/src/redux/protocol-runs/types/lpc.ts @@ -0,0 +1,70 @@ +import type { + DeckConfiguration, + LabwareDefinition2, + CompletedProtocolAnalysis, +} from '@opentrons/shared-data' +import type { + LabwareOffsetLocation, + VectorOffset, + LabwareOffset, +} from '@opentrons/api-client' + +// TODO(jh, 01-16-25): Make sure there's no cross importing after `steps` is refactored. +// eslint-disable-next-line opentrons/no-imports-across-applications +import type { StepsInfo } from '/app/organisms/LabwarePositionCheck/redux/types' + +export interface PositionParams { + labwareId: string + location: LabwareOffsetLocation + position: VectorOffset | null +} + +export interface WorkingOffset { + labwareId: string + location: LabwareOffsetLocation + initialPosition: VectorOffset | null + finalPosition: VectorOffset | null +} + +export interface LPCWizardState { + workingOffsets: WorkingOffset[] + protocolData: CompletedProtocolAnalysis + labwareDefs: LabwareDefinition2[] + deckConfig: DeckConfiguration + steps: StepsInfo + existingOffsets: LabwareOffset[] + protocolName: string + maintenanceRunId: string +} + +export interface StartLPCAction { + type: 'START_LPC' + payload: { runId: string; state: LPCWizardState } +} + +export interface FinishLPCAction { + type: 'FINISH_LPC' + payload: { runId: string } +} + +export interface ProceedStepAction { + type: 'PROCEED_STEP' + payload: { runId: string } +} + +export interface InitialPositionAction { + type: 'SET_INITIAL_POSITION' + payload: PositionParams & { runId: string } +} + +export interface FinalPositionAction { + type: 'SET_FINAL_POSITION' + payload: PositionParams & { runId: string } +} + +export type LPCWizardAction = + | StartLPCAction + | FinishLPCAction + | InitialPositionAction + | FinalPositionAction + | ProceedStepAction diff --git a/app/src/redux/protocol-runs/types/setup.ts b/app/src/redux/protocol-runs/types/setup.ts index b1d4c3b8e70..72185a9a096 100644 --- a/app/src/redux/protocol-runs/types/setup.ts +++ b/app/src/redux/protocol-runs/types/setup.ts @@ -48,6 +48,6 @@ export interface UpdateRunSetupStepsRequiredAction { } } -export type ProtocolRunAction = +export type RunSetupStepsAction = | UpdateRunSetupStepsCompleteAction | UpdateRunSetupStepsRequiredAction From 494e45fc7c54627fcdf7041638345e7ea81c52b2 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Fri, 17 Jan 2025 15:51:04 -0500 Subject: [PATCH 32/33] new todo --- app/src/redux/protocol-runs/reducer/lpc.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/redux/protocol-runs/reducer/lpc.ts b/app/src/redux/protocol-runs/reducer/lpc.ts index 5417f55de22..24dc9c103d5 100644 --- a/app/src/redux/protocol-runs/reducer/lpc.ts +++ b/app/src/redux/protocol-runs/reducer/lpc.ts @@ -8,6 +8,7 @@ import { updateWorkingOffset } from './transforms' import type { LPCWizardAction, LPCWizardState } from '../types' +// TODO(jh, 01-17-25): A lot of this state should live above the LPC slice, in the general protocolRuns slice instead. export function LPCReducer( state: LPCWizardState, action: LPCWizardAction From ffd9b08f068054816ae476382b140bd2f6923aeb Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Fri, 17 Jan 2025 16:00:22 -0500 Subject: [PATCH 33/33] lint --- .../ErrorRecoveryFlows/hooks/useRecoveryToasts.ts | 1 - .../hooks/useLPCInitialState/index.ts | 3 ++- .../LabwarePositionCheck/steps/CheckItem/index.tsx | 10 +++++++--- .../steps/ResultsSummary/OffsetTable.tsx | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts index 533b9877f72..7c0ea974149 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts @@ -110,7 +110,6 @@ export function useRecoveryFullCommandText( ): string | null { const { commandTextData, stepNumber } = props - // TODO TOME: I think you are looking one command to far, for some reason. const relevantCmdIdx = stepNumber ?? -1 const relevantCmd = commandTextData?.commands[relevantCmdIdx - 1] ?? null diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/index.ts index b7974ea0093..1be51e556e3 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/index.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/index.ts @@ -1,6 +1,7 @@ import { useDispatch } from 'react-redux' -import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' +import { getLabwareDefinitionsFromCommands } from '@opentrons/components' + import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' import { startLPC } from '/app/redux/protocol-runs' import { getLPCSteps } from './utils' diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx index 7f6f124e694..fa7366af6fc 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx @@ -1,11 +1,15 @@ import { Trans, useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' -import { DIRECTION_COLUMN, Flex, LegacyStyledText } from '@opentrons/components' +import { + DIRECTION_COLUMN, + Flex, + LegacyStyledText, + getLabwareDisplayLocation, +} from '@opentrons/components' import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' -import { getLabwareDisplayLocation } from '/app/local-resources/labware' import { UnorderedList } from '/app/molecules/UnorderedList' import { setFinalPosition, @@ -21,11 +25,11 @@ import { } from '/app/redux/protocol-runs' import { getIsOnDevice } from '/app/redux/config' +import type { DisplayLocationParams } from '@opentrons/components' import type { CheckPositionsStep, LPCStepProps, } from '/app/organisms/LabwarePositionCheck/types' -import type { DisplayLocationParams } from '/app/local-resources/labware' import type { State } from '/app/redux/types' import type { LPCWizardState } from '/app/redux/protocol-runs' diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx index b5514344c24..e471d16ba51 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx @@ -12,10 +12,10 @@ import { SPACING, LegacyStyledText, TYPOGRAPHY, + getLabwareDisplayLocation, } from '@opentrons/components' import { selectLwDisplayName } from '/app/redux/protocol-runs' -import { getLabwareDisplayLocation } from '/app/local-resources/labware' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { LabwareOffsetCreateData } from '@opentrons/api-client'