diff --git a/app/src/assets/localization/en/labware_position_check.json b/app/src/assets/localization/en/labware_position_check.json index 4072826650a..7cdd8cce2d1 100644 --- a/app/src/assets/localization/en/labware_position_check.json +++ b/app/src/assets/localization/en/labware_position_check.json @@ -2,7 +2,9 @@ "adapter_in_mod_in_slot": "{{adapter}} in {{module}} in {{slot}}", "adapter_in_slot": "{{adapter}} in {{slot}}", "adapter_in_tc": "{{adapter}} in {{module}}", + "add": "Add", "all_modules_and_labware_from_protocol": "All modules and labware used in the protocol {{protocol_name}}", + "applied_location_offsets": "Applied Location Offsets", "applied_offset_data": "Applied Labware Offset data", "apply_offset_data": "Apply labware offset data", "apply_offsets": "apply offsets", @@ -24,9 +26,10 @@ "confirm_position_and_move": "Confirm position, move to slot {{next_slot}}", "confirm_position_and_pick_up_tip": "Confirm position, pick up tip", "confirm_position_and_return_tip": "Confirm position, return tip to Slot {{next_slot}} and home", + "default_labware_offset": "Default Labware Offset", "detach_probe": "Remove calibration probe", - "ensure_nozzle_position_odd": "Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, tap Move pipette and then jog the pipette until it is properly aligned.", "ensure_nozzle_position_desktop": "Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, use the controls below or your keyboard to jog the pipette until it is properly aligned.", + "ensure_nozzle_position_odd": "Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, tap Move pipette and then jog the pipette until it is properly aligned.", "exit_screen_confirm_exit": "Exit and discard all labware offsets", "exit_screen_go_back": "Go back to labware position check", "exit_screen_subtitle": "If you exit now, all labware offsets will be discarded. This cannot be undone.", @@ -35,9 +38,10 @@ "install_probe": "Take the calibration probe from its storage location. Ensure its collar is fully unlocked. Push the pipette ejector up and press the probe firmly onto the {{location}} pipette nozzle as far as it can go. Twist the collar to lock the probe. Test that the probe is secure by gently pulling it back and forth.", "jog_controls_adjustment": "Need to make an adjustment?", "jupyter_notebook": "Jupyter Notebook", + "labware": "labware", "labware_display_location_text": "Deck Slot {{slot}}", - "labware_offset_data": "labware offset data", "labware_offset": "Labware Offset", + "labware_offset_data": "labware offset data", "labware_offsets_deleted_warning": "Once you begin Labware Position Check, previously created Labware Offsets will be discarded.", "labware_offsets_summary_labware": "Labware", "labware_offsets_summary_location": "Location", @@ -46,23 +50,23 @@ "labware_position_check_description": "Labware Position Check is a guided workflow that checks every labware on the deck for an added degree of precision in your protocol.Labware Position Check first checks tip racks, and then checks all other labware used in your protocol.", "labware_position_check_overview": "Labware Position Check Overview", "labware_position_check_title": "Labware Position Check", - "labware_step_detail_labware_plural": "The tips should be centered above column 1 in {{labware_name}} and level with the top of the labware.", "labware_step_detail_labware": "The tip should be centered above A1 in {{labware_name}} and level with the top of the labware.", + "labware_step_detail_labware_plural": "The tips should be centered above column 1 in {{labware_name}} and level with the top of the labware.", "labware_step_detail_link": "See how to tell if the pipette is centered", "labware_step_detail_modal_heading": "How to tell if the pipette is centered and level", + "labware_step_detail_modal_nozzle": "To ensure that the nozzle is centered, check from a second side of your OT-2.", "labware_step_detail_modal_nozzle_image_1_text": "Viewed from front, it appears centered...", "labware_step_detail_modal_nozzle_image_2_nozzle_text": "Nozzle is not centered", "labware_step_detail_modal_nozzle_image_2_text": "...but viewed from side, it requires adjustment", + "labware_step_detail_modal_nozzle_or_tip": "To ensure the nozzle or tip is level with the top of the labware, position yourself at eye-level and/or slide a sheet of paper between the nozzle and tip.", "labware_step_detail_modal_nozzle_or_tip_image_1_text": "Viewed from standing height, it appears level...", "labware_step_detail_modal_nozzle_or_tip_image_2_nozzle_text": "Nozzle is not level", "labware_step_detail_modal_nozzle_or_tip_image_2_text": "... but viewed from eye-level, it requires adjustment", "labware_step_detail_modal_nozzle_or_tip_image_3_text": "If you’re having trouble, slide 1 sheet of printer paper between the nozzle and the tip. A single piece of paper should barely pass between them.", - "labware_step_detail_modal_nozzle_or_tip": "To ensure the nozzle or tip is level with the top of the labware, position yourself at eye-level and/or slide a sheet of paper between the nozzle and tip.", - "labware_step_detail_modal_nozzle": "To ensure that the nozzle is centered, check from a second side of your OT-2.", - "labware_step_detail_tiprack_plural": "The pipette nozzles should be centered above column 1 in {{tiprack_name}} and level with the top of the tips.", "labware_step_detail_tiprack": "The pipette nozzle should be centered above A1 in {{tiprack_name}} and level with the top of the tip.", - "labware": "labware", + "labware_step_detail_tiprack_plural": "The pipette nozzles should be centered above column 1 in {{tiprack_name}} and level with the top of the tips.", "learn_more": "Learn more", + "legacy_no_offset_data": "No offset data available", "location": "location", "lpc_complete_summary_screen_heading": "Labware Position Check Complete", "module_display_location_text": "{{moduleName}} in Deck Slot {{slot}}", @@ -73,10 +77,10 @@ "new_labware_offset_data": "New labware offset data", "ninety_six_probe_location": "A1 (back left corner)", "no_labware_offsets": "No Labware Offset", + "no_offset_data": "No offset data", "no_offset_data_available": "No labware offset data available", "no_offset_data_on_robot": "This robot has no useable labware offset data for this run.", - "no_offset_data": "No offset data available", - "offsets": "offsets", + "offsets": "Offsets", "pick_up_tip_from_rack_in_location": "Pick up tip from tip rack in {{location}}", "picking_up_tip_title": "Picking up tip in slot {{slot}}", "pipette_nozzle": "pipette nozzle furthest from you", @@ -98,13 +102,14 @@ "robot_has_no_offsets_from_previous_runs": "Labware offset data references previous protocol run labware locations to save you time. If all the labware in this protocol have been checked in previous runs, that data will be applied to this run. You can add new offsets with Labware Position Check in later steps.", "robot_has_offsets_from_previous_runs": "This robot has offsets for labware used in this protocol. If you apply these offsets, you can still adjust them with Labware Position Check.", "robot_in_motion": "Stand back, robot is in motion.", - "run_labware_position_check": "run labware position check", "run": "Run", + "run_labware_position_check": "run labware position check", "secondary_pipette_tipracks_section": "Check tip racks with {{secondary_mount}} Pipette", "see_how_offsets_work": "See how labware offsets work", - "slot_location": "slot location", - "slot_name": "slot {{slotName}}", + "select_labware_from_list": "Select a labware from the list to check its stored offset data", "slot": "Slot {{slotName}}", + "slot_location": "Slot Location", + "slot_name": "slot {{slotName}}", "start_position_check": "begin labware position check, move to Slot {{initial_labware_slot}}", "stored_offset_data": "Apply Stored Labware Offset Data?", "stored_offsets_for_this_protocol": "Stored Labware Offset data that applies to this protocol", diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx index 84e7fb82e65..0dd2bc9557b 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/__tests__/ProtocolRunSetup.test.tsx @@ -33,6 +33,7 @@ import { useRobot, useIsFlex } from '/app/redux-resources/robots' import { useRequiredSetupStepsInOrder } from '/app/redux-resources/runs' import { useStoredProtocolAnalysis } from '/app/resources/analysis' import { getMissingSetupSteps } from '/app/redux/protocol-runs' +import { useLPCFlows } from '/app/organisms/LabwarePositionCheck' import { SetupLabware } from '../SetupLabware' import { SetupRobotCalibration } from '../SetupRobotCalibration' @@ -67,6 +68,7 @@ vi.mock('/app/resources/deck_configuration/hooks') vi.mock('/app/redux-resources/robots') vi.mock('/app/redux-resources/runs') vi.mock('/app/resources/analysis') +vi.mock('/app/organisms/LabwarePositionCheck') vi.mock('@opentrons/shared-data', async importOriginal => { const actualSharedData = await importOriginal() return { @@ -186,6 +188,12 @@ describe('ProtocolRunSetup', () => { when(vi.mocked(useModuleCalibrationStatus)) .calledWith(ROBOT_NAME, RUN_ID) .thenReturn({ complete: true }) + vi.mocked(useLPCFlows).mockReturnValue({ + launchLPC: vi.fn(), + lpcProps: null, + showLPC: false, + isLaunchingLPC: false, + }) }) afterEach(() => { vi.resetAllMocks() diff --git a/app/src/organisms/LabwarePositionCheck/LPCFlows/LPCFlows.tsx b/app/src/organisms/LabwarePositionCheck/LPCFlows/LPCFlows.tsx index 5c7ab9bdf3c..5dd041ede77 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCFlows/LPCFlows.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCFlows/LPCFlows.tsx @@ -4,20 +4,28 @@ import type { RobotType, CompletedProtocolAnalysis, DeckConfiguration, + LabwareDefinition2, } from '@opentrons/shared-data' import type { LabwareOffset } from '@opentrons/api-client' +import type { LPCLabwareInfo } from '/app/redux/protocol-runs' + +// Inject the props specific to the legacy LPC flows, too. +export interface LegacySupportLPCFlowsProps extends LPCFlowsProps { + existingOffsets: LabwareOffset[] +} export interface LPCFlowsProps { onCloseClick: () => void runId: string robotType: RobotType deckConfig: DeckConfiguration - existingOffsets: LabwareOffset[] + labwareDefs: LabwareDefinition2[] + labwareInfo: LPCLabwareInfo mostRecentAnalysis: CompletedProtocolAnalysis protocolName: string maintenanceRunId: string } -export function LPCFlows(props: LPCFlowsProps): JSX.Element { +export function LPCFlows(props: LegacySupportLPCFlowsProps): JSX.Element { return } diff --git a/app/src/organisms/LabwarePositionCheck/LPCFlows/hooks/index.ts b/app/src/organisms/LabwarePositionCheck/LPCFlows/hooks/index.ts new file mode 100644 index 00000000000..93372f3db52 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/LPCFlows/hooks/index.ts @@ -0,0 +1 @@ +export { useLPCLabwareInfo } from './useLPCLabwareInfo' diff --git a/app/src/organisms/LabwarePositionCheck/LPCFlows/hooks/useLPCLabwareInfo/getLPCLabwareInfoFrom.ts b/app/src/organisms/LabwarePositionCheck/LPCFlows/hooks/useLPCLabwareInfo/getLPCLabwareInfoFrom.ts new file mode 100644 index 00000000000..2d55747c2e6 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/LPCFlows/hooks/useLPCLabwareInfo/getLPCLabwareInfoFrom.ts @@ -0,0 +1,93 @@ +import isEqual from 'lodash/isEqual' + +import { getLabwareDisplayName, getLabwareDefURI } from '@opentrons/shared-data' + +import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type { LPCLabwareInfo, OffsetDetails } from '/app/redux/protocol-runs' +import type { LabwareLocationCombo } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' +import type { UseLPCLabwareInfoProps } from '.' + +interface GetLPCLabwareInfoParams { + lwURIs: string[] + currentOffsets: UseLPCLabwareInfoProps['currentOffsets'] + lwLocationCombos: LabwareLocationCombo[] + labwareDefs: UseLPCLabwareInfoProps['labwareDefs'] +} + +export function getLPCLabwareInfoFrom( + params: GetLPCLabwareInfoParams +): LPCLabwareInfo { + return { selectedLabware: null, labware: getLabwareInfoRecords(params) } +} + +function getLabwareInfoRecords( + params: GetLPCLabwareInfoParams +): LPCLabwareInfo['labware'] { + const labwareDetails: LPCLabwareInfo['labware'] = {} + + params.lwURIs.forEach(uri => { + if (!(uri in labwareDetails)) { + labwareDetails[uri] = { + id: getALabwareIdFromUri({ ...params, uri }), + displayName: getDisplayNameFromUri({ ...params, uri }), + offsetDetails: getOffsetDetailsForLabware({ ...params, uri }), + } + } + }) + + return labwareDetails +} + +type GetLPCLabwareInfoForURI = Omit & { + uri: string +} + +function getALabwareIdFromUri({ + uri, + lwLocationCombos, +}: GetLPCLabwareInfoForURI): string { + return ( + lwLocationCombos.find(combo => combo.definitionUri === uri)?.labwareId ?? '' + ) +} + +function getDisplayNameFromUri({ + uri, + labwareDefs, +}: GetLPCLabwareInfoForURI): string { + const matchedDef = labwareDefs?.find( + def => getLabwareDefURI(def) === uri + ) as LabwareDefinition2 + + return getLabwareDisplayName(matchedDef) +} + +// NOTE: A lot of the logic here acts as temporary adapter that resolves the app's current way of getting offset data (scraping the run record) +// and the end goal of treating labware as first class citizens. +function getOffsetDetailsForLabware({ + currentOffsets, + lwLocationCombos, + uri, +}: GetLPCLabwareInfoForURI): OffsetDetails[] { + return lwLocationCombos.flatMap(comboInfo => { + const { definitionUri, location, ...restInfo } = comboInfo + + const existingOffset = + currentOffsets.find( + offset => + uri === offset.definitionUri && + isEqual(offset.location, comboInfo.location) + ) ?? null + + return { + existingOffset: existingOffset ?? null, + workingOffset: null, + locationDetails: { + ...location, + ...restInfo, + definitionUri, + kind: 'location-specific', + }, + } + }) +} diff --git a/app/src/organisms/LabwarePositionCheck/LPCFlows/hooks/useLPCLabwareInfo/getUniqueLabwareLocationComboInfo.ts b/app/src/organisms/LabwarePositionCheck/LPCFlows/hooks/useLPCLabwareInfo/getUniqueLabwareLocationComboInfo.ts new file mode 100644 index 00000000000..36339823640 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/LPCFlows/hooks/useLPCLabwareInfo/getUniqueLabwareLocationComboInfo.ts @@ -0,0 +1,56 @@ +import { isEqual } from 'lodash' + +import { getLabwareDefURI } from '@opentrons/shared-data' + +import { getLabwareLocationCombos } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' + +import type { + CompletedProtocolAnalysis, + LabwareDefinition2, +} from '@opentrons/shared-data' +import type { LabwareLocationCombo } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' + +export interface GetUniqueLocationComboInfoParams { + protocolData: CompletedProtocolAnalysis | null + labwareDefs: LabwareDefinition2[] | null +} + +export function getUniqueLabwareLocationComboInfo({ + labwareDefs, + protocolData, +}: GetUniqueLocationComboInfoParams): LabwareLocationCombo[] { + if (protocolData == null || labwareDefs == null) { + return [] + } + + const { commands, labware, modules = [] } = protocolData + const labwareLocationCombos = getLabwareLocationCombos( + commands, + labware, + modules + ) + + // Filter out duplicate labware and labware that is not LPC-able. + return labwareLocationCombos.reduce( + (acc, labwareLocationCombo) => { + const labwareDef = labwareDefs.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] + }, + [] + ) +} diff --git a/app/src/organisms/LabwarePositionCheck/LPCFlows/hooks/useLPCLabwareInfo/index.ts b/app/src/organisms/LabwarePositionCheck/LPCFlows/hooks/useLPCLabwareInfo/index.ts new file mode 100644 index 00000000000..56e4bbaf697 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/LPCFlows/hooks/useLPCLabwareInfo/index.ts @@ -0,0 +1,50 @@ +import { useMemo } from 'react' + +import { getUniqueLabwareLocationComboInfo } from './getUniqueLabwareLocationComboInfo' +import { getLPCLabwareInfoFrom } from './getLPCLabwareInfoFrom' + +import type { LabwareOffset } from '@opentrons/api-client' +import type { LPCLabwareInfo } from '/app/redux/protocol-runs' +import type { GetUniqueLocationComboInfoParams } from './getUniqueLabwareLocationComboInfo' + +export type UseLPCLabwareInfoProps = GetUniqueLocationComboInfoParams & { + currentOffsets: LabwareOffset[] +} + +// TODO(jh, 01-22-25): This interface will change substantially the switch to /labwareOffsets. + +// Structures LPC-able labware info for injection into LPC flows. +export function useLPCLabwareInfo({ + currentOffsets, + labwareDefs, + protocolData, +}: UseLPCLabwareInfoProps): LPCLabwareInfo { + // Analysis-derived data is the source of truth, because we must account for labware that has offsets AND account for labware + // that does not have offsets. This will change with the LPC HTTP API refactors. + const lwURIs = getLabwareURIsFromAnalysis(protocolData) + const lwLocationCombos = useMemo( + () => + getUniqueLabwareLocationComboInfo({ + labwareDefs, + protocolData, + }), + [labwareDefs != null, protocolData != null] + ) + + return useMemo( + () => + getLPCLabwareInfoFrom({ + lwURIs, + currentOffsets, + lwLocationCombos, + labwareDefs, + }), + [lwURIs.length, currentOffsets.length, lwLocationCombos.length] + ) +} + +function getLabwareURIsFromAnalysis( + analysis: UseLPCLabwareInfoProps['protocolData'] +): string[] { + return analysis?.labware.map(lwInfo => lwInfo.definitionUri) ?? [] +} diff --git a/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts b/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts index 1b31d79de0a..836faf02327 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts +++ b/app/src/organisms/LabwarePositionCheck/LPCFlows/useLPCFlows.ts @@ -1,5 +1,6 @@ -import { useEffect, useState } from 'react' +import { useEffect, useMemo, useState } from 'react' +import { getLabwareDefinitionsFromCommands } from '@opentrons/components' import { useCreateMaintenanceRunLabwareDefinitionMutation, useDeleteMaintenanceRunMutation, @@ -8,14 +9,18 @@ import { import { useCreateTargetedMaintenanceRunMutation, - useNotifyRunQuery, useMostRecentCompletedAnalysis, + useNotifyRunQuery, } from '/app/resources/runs' import { useNotifyCurrentMaintenanceRun } from '/app/resources/maintenance_runs' +import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' +import { useLPCLabwareInfo } from '/app/organisms/LabwarePositionCheck/LPCFlows/hooks' import type { RobotType } from '@opentrons/shared-data' -import type { LPCFlowsProps } from '/app/organisms/LabwarePositionCheck/LPCFlows/LPCFlows' -import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' +import type { + LegacySupportLPCFlowsProps, + LPCFlowsProps, +} from '/app/organisms/LabwarePositionCheck/LPCFlows/LPCFlows' interface UseLPCFlowsBase { showLPC: boolean @@ -29,7 +34,7 @@ interface UseLPCFlowsIdle extends UseLPCFlowsBase { } interface UseLPCFlowsLaunched extends UseLPCFlowsBase { showLPC: true - lpcProps: LPCFlowsProps + lpcProps: LegacySupportLPCFlowsProps isLaunchingLPC: false } export type UseLPCFlowsResult = UseLPCFlowsIdle | UseLPCFlowsLaunched @@ -49,11 +54,21 @@ export function useLPCFlows({ const [isLaunching, setIsLaunching] = useState(false) const [hasCreatedLPCRun, setHasCreatedLPCRun] = useState(false) - const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) const deckConfig = useNotifyDeckConfigurationQuery().data + const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) const currentOffsets = runRecord?.data?.labwareOffsets ?? [] const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) + const labwareDefs = useMemo( + () => getLabwareDefinitionsFromCommands(mostRecentAnalysis?.commands ?? []), + [mostRecentAnalysis != null] + ) + const labwareInfo = useLPCLabwareInfo({ + currentOffsets, + labwareDefs, + protocolData: mostRecentAnalysis, + }) + useMonitorMaintenanceRunForDeletion({ maintenanceRunId, setMaintenanceRunId }) const { @@ -130,10 +145,12 @@ export function useLPCFlows({ runId, robotType, deckConfig, - existingOffsets: currentOffsets, + labwareDefs, + labwareInfo, mostRecentAnalysis, protocolName, maintenanceRunId, + existingOffsets: currentOffsets, }, } : { launchLPC, isLaunchingLPC: isLaunching, lpcProps: null, showLPC } diff --git a/app/src/organisms/LabwarePositionCheck/LPCWizardContainer.tsx b/app/src/organisms/LabwarePositionCheck/LPCWizardContainer.tsx index 2215cd14bc6..d5a703a79ec 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCWizardContainer.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCWizardContainer.tsx @@ -3,9 +3,11 @@ import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { LPCWizardFlex } from './LPCWizardFlex' import { LegacyLabwarePositionCheck } from '/app/organisms/LegacyLabwarePositionCheck' -import type { LPCFlowsProps } from '/app/organisms/LabwarePositionCheck/LPCFlows' +import type { LegacySupportLPCFlowsProps } from '/app/organisms/LabwarePositionCheck/LPCFlows' -export function LPCWizardContainer(props: LPCFlowsProps): JSX.Element { +export function LPCWizardContainer( + props: LegacySupportLPCFlowsProps +): JSX.Element { switch (props.robotType) { case FLEX_ROBOT_TYPE: return diff --git a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx index 4706c2eaaf8..11cb64c88e6 100644 --- a/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx +++ b/app/src/organisms/LabwarePositionCheck/LPCWizardFlex.tsx @@ -7,10 +7,10 @@ import { ModalShell } from '@opentrons/components' import { getTopPortalEl } from '/app/App/portal' import { BeforeBeginning, - CheckItem, + HandleLabware, AttachProbe, DetachProbe, - ResultsSummary, + LPCComplete, } from '/app/organisms/LabwarePositionCheck/steps' import { ExitConfirmation } from './ExitConfirmation' import { RobotMotionLoader } from './RobotMotionLoader' @@ -20,8 +20,12 @@ import { useLPCCommands, useLPCInitialState, } from '/app/organisms/LabwarePositionCheck/hooks' -import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' -import { closeLPC, proceedStep } from '/app/redux/protocol-runs' +import { + closeLPC, + proceedStep, + LPC_STEP, + selectCurrentStep, +} from '/app/redux/protocol-runs' import { getIsOnDevice } from '/app/redux/config' import type { LPCFlowsProps } from '/app/organisms/LabwarePositionCheck/LPCFlows' @@ -34,7 +38,6 @@ export interface LPCWizardFlexProps extends Omit {} export function LPCWizardFlex(props: LPCWizardFlexProps): JSX.Element { const { onCloseClick, ...rest } = props - // TODO(jh, 01-14-25): Also inject goBack functionality once designs are finalized. const proceed = (): void => { dispatch(proceedStep(props.runId)) } @@ -118,18 +121,13 @@ function LPCWizardHeader({ function LPCWizardContent(props: LPCWizardContentProps): JSX.Element { const { t } = useTranslation('shared') - const currentStep = useSelector( - (state: State) => - state.protocolRuns[props.runId]?.lpc?.steps.current ?? null - ) + const currentStep = useSelector(selectCurrentStep(props.runId)) const { isRobotMoving, errorMessage, showExitConfirmation, } = props.commandUtils - // TODO(jh, 01-14-25): Handle open door behavior. - // Handle special cases that are shared by multiple steps first. if (isRobotMoving) { return @@ -146,24 +144,24 @@ function LPCWizardContent(props: LPCWizardContentProps): JSX.Element { } // Handle step-based routing. - switch (currentStep.section) { - case NAV_STEPS.BEFORE_BEGINNING: - return + switch (currentStep) { + case LPC_STEP.BEFORE_BEGINNING: + return - case NAV_STEPS.CHECK_POSITIONS: - return + case LPC_STEP.ATTACH_PROBE: + return - case NAV_STEPS.ATTACH_PROBE: - return + case LPC_STEP.HANDLE_LABWARE: + return - case NAV_STEPS.DETACH_PROBE: - return + case LPC_STEP.DETACH_PROBE: + return - case NAV_STEPS.RESULTS_SUMMARY: - return + case LPC_STEP.LPC_COMPLETE: + return default: console.error('Unhandled LPC step.') - return + return } } diff --git a/app/src/organisms/LabwarePositionCheck/constants.ts b/app/src/organisms/LabwarePositionCheck/constants.ts deleted file mode 100644 index 9ccd9b81eef..00000000000 --- a/app/src/organisms/LabwarePositionCheck/constants.ts +++ /dev/null @@ -1,7 +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 diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/labware.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/labware.ts index 95cc6db6ccc..fb5b8275cb1 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/labware.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/labware.ts @@ -1,14 +1,10 @@ import type { CreateCommand } from '@opentrons/shared-data' -import type { CheckPositionsStep } from '/app/organisms/LabwarePositionCheck/types' +import type { OffsetLocationDetails } from '/app/redux/protocol-runs' -export interface BuildMoveLabwareOffDeckParams { - step: CheckPositionsStep -} - -export function moveLabwareOffDeckCommands({ - step, -}: BuildMoveLabwareOffDeckParams): CreateCommand[] { - const { adapterId, labwareId } = step +export function moveLabwareOffDeckCommands( + offsetLocationDetails: OffsetLocationDetails +): CreateCommand[] { + const { adapterId, labwareId } = offsetLocationDetails return adapterId != null ? [ diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/modules.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/modules.ts index 8c5487a66e9..4982886d5f6 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/modules.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/modules.ts @@ -5,27 +5,23 @@ import { 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 { LegacyLabwareOffsetLocation } from '@opentrons/api-client' +import type { OffsetLocationDetails } from '/app/redux/protocol-runs' -export interface BuildModulePrepCommandsParams { - step: CheckPositionsStep -} - -export function modulePrepCommands({ - step, -}: BuildModulePrepCommandsParams): CreateCommand[] { - const { moduleId, location } = step +export function modulePrepCommands( + offsetLocationDetails: OffsetLocationDetails +): CreateCommand[] { + const { moduleId, moduleModel } = offsetLocationDetails const moduleType = (moduleId != null && + moduleModel != null && 'moduleModel' in location && location.moduleModel != null && - getModuleType(location.moduleModel)) ?? + getModuleType(moduleModel)) ?? null if (moduleId == null || moduleType == null) { @@ -79,11 +75,9 @@ export const moduleInitDuringLPCCommands = ( // Not all modules require cleanup after each labware LPC. export const moduleCleanupDuringLPCCommands = ( - step: CheckPositionsStep + offsetLocationDetails: OffsetLocationDetails ): CreateCommand[] => { - const { moduleId, location } = step - - return [...heaterShakerCleanupCommands(moduleId, location)] + return [...heaterShakerCleanupCommands(offsetLocationDetails)] } const heaterShakerInitCommands = ( @@ -127,14 +121,16 @@ const thermocyclerInitCommands = ( } const heaterShakerCleanupCommands = ( - moduleId: string | undefined, - location: LegacyLabwareOffsetLocation + offsetLocationDetails: OffsetLocationDetails ): CreateCommand[] => { + const { moduleId, moduleModel } = offsetLocationDetails + const moduleType = (moduleId != null && + moduleModel != null && 'moduleModel' in location && location.moduleModel != null && - getModuleType(location.moduleModel)) ?? + getModuleType(moduleModel)) ?? null return moduleId != null && diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/pipettes.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/pipettes.ts index fba1f7b025f..0bf32bdcbe0 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/pipettes.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/commands/pipettes.ts @@ -5,8 +5,8 @@ import type { LoadedPipette, MotorAxes, } from '@opentrons/shared-data' -import type { CheckPositionsStep } from '/app/organisms/LabwarePositionCheck/types' import type { Axis, Sign, StepSize } from '/app/molecules/JogControls/types' +import type { OffsetLocationDetails } from '/app/redux/protocol-runs' const PROBE_LENGTH_MM = 44.5 @@ -15,9 +15,10 @@ export const savePositionCommands = (pipetteId: string): CreateCommand[] => [ ] export const moveToWellCommands = ( - step: CheckPositionsStep + offsetLocationDetails: OffsetLocationDetails, + pipetteId: string ): CreateCommand[] => { - const { pipetteId, labwareId } = step + const { labwareId } = offsetLocationDetails return [ { @@ -115,7 +116,6 @@ export const moveToMaintenancePosition = ( } export const verifyProbeAttachmentAndHomeCommands = ( - pipetteId: string, pipette: LoadedPipette | null ): CreateCommand[] => { const pipetteMount = pipette?.mount @@ -125,7 +125,7 @@ export const verifyProbeAttachmentAndHomeCommands = ( { commandType: 'verifyTipPresence', params: { - pipetteId, + pipetteId: pipette?.id ?? '', expectedState: 'present', followSingularSensor: 'primary', }, diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwFinalPosition.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwFinalPosition.ts index 9c159b4c518..133bab43a8e 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwFinalPosition.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwFinalPosition.ts @@ -11,7 +11,7 @@ import type { CreateCommand, } from '@opentrons/shared-data' import type { UseLPCCommandWithChainRunChildProps } from './types' -import type { BuildMoveLabwareOffDeckParams } from './commands' +import type { OffsetLocationDetails } from '/app/redux/protocol-runs' interface UseHandleConfirmPositionProps extends UseLPCCommandWithChainRunChildProps { @@ -22,10 +22,8 @@ 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 | null - } + offsetLocationDetails: OffsetLocationDetails, + pipette: LoadedPipette ) => Promise } @@ -34,26 +32,20 @@ export function useHandleConfirmLwFinalPosition({ chainLPCCommands, }: UseHandleConfirmPositionProps): UseHandleConfirmPositionResult { const handleConfirmLwFinalPosition = ( - params: BuildMoveLabwareOffDeckParams & { - onSuccess: () => void - pipette: LoadedPipette | null - } + offsetLocationDetails: OffsetLocationDetails, + pipette: LoadedPipette ): Promise => { - const { onSuccess, pipette, step } = params - const { pipetteId } = step - const confirmCommands: CreateCommand[] = [ - ...savePositionCommands(pipetteId), + ...savePositionCommands(pipette.id), ...retractPipetteAxesSequentiallyCommands(pipette), - ...moduleCleanupDuringLPCCommands(step), - ...moveLabwareOffDeckCommands(params), + ...moduleCleanupDuringLPCCommands(offsetLocationDetails), + ...moveLabwareOffDeckCommands(offsetLocationDetails), ] return chainLPCCommands(confirmCommands, 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 { diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwModulePlacement.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwModulePlacement.ts index eb58121b6bf..b37763a032f 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwModulePlacement.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwModulePlacement.ts @@ -10,7 +10,7 @@ import type { CreateCommand, } from '@opentrons/shared-data' import type { UseLPCCommandWithChainRunChildProps } from './types' -import type { CheckPositionsStep } from '/app/organisms/LabwarePositionCheck/types' +import type { OffsetLocationDetails } from '/app/redux/protocol-runs' export interface UseHandleConfirmPlacementProps extends UseLPCCommandWithChainRunChildProps { @@ -21,7 +21,8 @@ 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 + offsetLocationDetails: OffsetLocationDetails, + pipetteId: string ) => Promise } @@ -31,14 +32,13 @@ export function useHandleConfirmLwModulePlacement({ setErrorMessage, }: UseHandleConfirmPlacementProps): UseHandleConfirmPlacementResult { const handleConfirmLwModulePlacement = ( - params: BuildMoveLabwareCommandParams + offsetLocationDetails: OffsetLocationDetails, + pipetteId: string ): Promise => { - const { pipetteId } = params.step - const confirmCommands: CreateCommand[] = [ - ...buildMoveLabwareCommand(params), + ...buildMoveLabwareCommand(offsetLocationDetails), ...moduleInitDuringLPCCommands(mostRecentAnalysis), - ...moveToWellCommands(params.step), + ...moveToWellCommands(offsetLocationDetails, pipetteId), ...savePositionCommands(pipetteId), ] @@ -62,17 +62,21 @@ export function useHandleConfirmLwModulePlacement({ return { handleConfirmLwModulePlacement } } -interface BuildMoveLabwareCommandParams { - step: CheckPositionsStep -} +function buildMoveLabwareCommand( + offsetLocationDetails: OffsetLocationDetails +): MoveLabwareCreateCommand[] { + const { labwareId, moduleId, adapterId, slotName } = offsetLocationDetails -function buildMoveLabwareCommand({ - step, -}: BuildMoveLabwareCommandParams): MoveLabwareCreateCommand[] { - const { labwareId, moduleId, adapterId, location } = step + // TODO(jh, 01-29-25): Once default offsets are implemented, we'll have to load them + // into a slot somehow. Talk to Design. + const locationSpecificSlotName = slotName as string - const newLocation = - moduleId != null ? { moduleId } : { slotName: location.slotName } + const newLocationLabware = + moduleId != null ? { moduleId } : { slotName: locationSpecificSlotName } + const newLocationAdapter = + adapterId != null + ? { labwareId: adapterId } + : { slotName: locationSpecificSlotName } if (adapterId != null) { return [ @@ -80,7 +84,7 @@ function buildMoveLabwareCommand({ commandType: 'moveLabware' as const, params: { labwareId: adapterId, - newLocation, + newLocation: newLocationLabware, strategy: 'manualMoveWithoutPause', }, }, @@ -88,10 +92,7 @@ function buildMoveLabwareCommand({ commandType: 'moveLabware' as const, params: { labwareId, - newLocation: - adapterId != null - ? { labwareId: adapterId } - : { slotName: location.slotName }, + newLocation: newLocationAdapter, strategy: 'manualMoveWithoutPause', }, }, @@ -102,7 +103,7 @@ function buildMoveLabwareCommand({ commandType: 'moveLabware' as const, params: { labwareId, - newLocation, + newLocation: newLocationLabware, strategy: 'manualMoveWithoutPause', }, }, diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts index 9d49cd7835e..d88617262ae 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleJog.ts @@ -4,6 +4,7 @@ import { useSelector } from 'react-redux' import { useCreateMaintenanceCommandMutation } from '@opentrons/react-api-client' import { moveRelativeCommand } from './commands' +import { selectActivePipette } from '/app/redux/protocol-runs' import type { Coordinates } from '@opentrons/shared-data' import type { @@ -12,7 +13,6 @@ 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 @@ -33,11 +33,10 @@ export function useHandleJog({ maintenanceRunId, setErrorMessage, }: UseHandleJogProps): UseHandleJogResult { - const { current: currentStep } = - useSelector((state: State) => state.protocolRuns[runId]?.lpc?.steps) ?? {} - const [isJogging, setIsJogging] = useState(false) const [jogQueue, setJogQueue] = useState Promise>>([]) + const pipette = useSelector(selectActivePipette(runId)) + const pipetteId = pipette?.id const { createMaintenanceCommand: createSilentCommand, } = useCreateMaintenanceCommandMutation() @@ -50,11 +49,6 @@ export function useHandleJog({ onSuccess?: (position: Coordinates | null) => void ): Promise => { return new Promise((resolve, reject) => { - const pipetteId = - currentStep != null && 'pipetteId' in currentStep - ? currentStep.pipetteId - : null - if (pipetteId != null) { createSilentCommand({ maintenanceRunId, @@ -81,7 +75,7 @@ export function useHandleJog({ } }) }, - [currentStep, maintenanceRunId, createSilentCommand, setErrorMessage] + [pipetteId, 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 0f944bf74f0..6e87000b46f 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandlePrepModules.ts @@ -1,56 +1,34 @@ -import { useSelector } from 'react-redux' - import { modulePrepCommands } from './commands' -import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' -import { selectActiveLwInitialPosition } from '/app/redux/protocol-runs' +import type { CommandData, VectorOffset } from '@opentrons/api-client' 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' +import type { OffsetLocationDetails } from '/app/redux/protocol-runs' export interface UseHandlePrepModulesResult { handleCheckItemsPrepModules: ( - step: LabwarePositionCheckStep | null + offsetLocationDetails: OffsetLocationDetails, + initialPosition: VectorOffset | null ) => Promise } // Prep module(s) before LPCing a specific labware involving module(s). export function useHandlePrepModules({ - runId, chainLPCCommands, }: UseLPCCommandWithChainRunChildProps): UseHandlePrepModulesResult { - const selectInitialPositionFrom = useSelector( - (state: State) => (step: LabwarePositionCheckStep | null) => - selectActiveLwInitialPosition(step, runId, state) - ) - const handleCheckItemsPrepModules = ( - step: LabwarePositionCheckStep | null + offsetLocationDetails: OffsetLocationDetails, + initialPosition: VectorOffset | null ): Promise => { - const initialPosition = selectInitialPositionFrom(step) - - if (step?.section === NAV_STEPS.CHECK_POSITIONS) { - const prepCommands: CreateCommand[] = modulePrepCommands({ - step, - }) + const prepCommands: CreateCommand[] = modulePrepCommands( + offsetLocationDetails + ) - if ( - initialPosition == null && - // Only run these commands during the appropriate step. - step.section === NAV_STEPS.CHECK_POSITIONS && - prepCommands.length > 0 - ) { - return chainLPCCommands(prepCommands, false) - } else { - return Promise.resolve([]) - } + if (initialPosition == null && prepCommands.length > 0) { + return chainLPCCommands(prepCommands, false) + } else { + return Promise.resolve([]) } - - return Promise.reject( - new Error(`Cannot prep modules during unsupported step: ${step?.section}`) - ) } return { handleCheckItemsPrepModules } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts index 8fe77d5c60a..9c3abf18996 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleProbeCommands.ts @@ -9,15 +9,14 @@ import type { CreateCommand, LoadedPipette } from '@opentrons/shared-data' import type { UseLPCCommandWithChainRunChildProps } from './types' export interface UseProbeCommandsResult { - createProbeAttachmentHandler: ( - pipetteId: string, + handleProbeAttachment: ( pipette: LoadedPipette | null, onSuccess: () => void - ) => () => Promise - createProbeDetachmentHandler: ( + ) => Promise + handleProbeDetachment: ( pipette: LoadedPipette | null, onSuccess: () => void - ) => () => Promise + ) => Promise unableToDetect: boolean setShowUnableToDetect: (canDetect: boolean) => void } @@ -27,45 +26,42 @@ export function useHandleProbeCommands({ }: UseLPCCommandWithChainRunChildProps): UseProbeCommandsResult { const [showUnableToDetect, setShowUnableToDetect] = useState(false) - const createProbeAttachmentHandler = ( - pipetteId: string, + const handleProbeAttachment = ( pipette: LoadedPipette | null, onSuccess: () => void - ): (() => Promise) => { + ): Promise => { const attachmentCommands: CreateCommand[] = [ - ...verifyProbeAttachmentAndHomeCommands(pipetteId, pipette), + ...verifyProbeAttachmentAndHomeCommands(pipette), ] - return () => - chainLPCCommands(attachmentCommands, false, true) - .catch(() => { - setShowUnableToDetect(true) - return Promise.reject(new Error('Unable to detect probe.')) - }) - .then(() => { - setShowUnableToDetect(false) - onSuccess() - }) + return chainLPCCommands(attachmentCommands, false, true) + .catch(() => { + setShowUnableToDetect(true) + return Promise.reject(new Error('Unable to detect probe.')) + }) + .then(() => { + setShowUnableToDetect(false) + onSuccess() + }) } - const createProbeDetachmentHandler = ( + const handleProbeDetachment = ( pipette: LoadedPipette | null, onSuccess: () => void - ): (() => Promise) => { + ): Promise => { const detatchmentCommands: CreateCommand[] = [ ...retractPipetteAxesSequentiallyCommands(pipette), ] - return () => - chainLPCCommands(detatchmentCommands, false).then(() => { - onSuccess() - }) + return chainLPCCommands(detatchmentCommands, false).then(() => { + onSuccess() + }) } return { - createProbeAttachmentHandler, + handleProbeAttachment, unableToDetect: showUnableToDetect, setShowUnableToDetect, - createProbeDetachmentHandler, + handleProbeDetachment, } } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleResetLwModulesOnDeck.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleResetLwModulesOnDeck.ts index b64965e0cc9..0aa61672967 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleResetLwModulesOnDeck.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleResetLwModulesOnDeck.ts @@ -6,14 +6,11 @@ import { import type { CreateCommand } from '@opentrons/shared-data' import type { UseLPCCommandWithChainRunChildProps } from './types' -import type { - BuildMoveLabwareOffDeckParams, - BuildModulePrepCommandsParams, -} from './commands' +import type { OffsetLocationDetails } from '/app/redux/protocol-runs' export interface UseHandleResetLwModulesOnDeckResult { handleResetLwModulesOnDeck: ( - params: BuildModulePrepCommandsParams & BuildMoveLabwareOffDeckParams + offsetLocationDetails: OffsetLocationDetails ) => Promise } @@ -21,12 +18,12 @@ export function useHandleResetLwModulesOnDeck({ chainLPCCommands, }: UseLPCCommandWithChainRunChildProps): UseHandleResetLwModulesOnDeckResult { const handleResetLwModulesOnDeck = ( - params: BuildModulePrepCommandsParams & BuildMoveLabwareOffDeckParams + offsetLocationDetails: OffsetLocationDetails ): Promise => { const resetCommands: CreateCommand[] = [ - ...modulePrepCommands(params), + ...modulePrepCommands(offsetLocationDetails), ...fullHomeCommands(), - ...moveLabwareOffDeckCommands(params as BuildMoveLabwareOffDeckParams), + ...moveLabwareOffDeckCommands(offsetLocationDetails), ] return chainLPCCommands(resetCommands, false).then(() => Promise.resolve()) diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts index df3270555f6..382870b33bc 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleStartLPC.ts @@ -14,20 +14,20 @@ import type { import type { UseLPCCommandWithChainRunChildProps } from './types' export interface UseHandleStartLPCResult { - createStartLPCHandler: ( + handleStartLPC: ( pipette: LoadedPipette | null, onSuccess: () => void - ) => () => Promise + ) => Promise } export function useHandleStartLPC({ chainLPCCommands, mostRecentAnalysis, }: UseLPCCommandWithChainRunChildProps): UseHandleStartLPCResult { - const createStartLPCHandler = ( + const handleStartLPC = ( pipette: LoadedPipette | null, onSuccess: () => void - ): (() => Promise) => { + ): Promise => { const startCommands: CreateCommand[] = [ ...buildInstrumentLabwarePrepCommands(mostRecentAnalysis), ...moduleInitBeforeAnyLPCCommands(mostRecentAnalysis), @@ -35,13 +35,12 @@ export function useHandleStartLPC({ ...moveToMaintenancePosition(pipette), ] - return () => - chainLPCCommands(startCommands, false).then(() => { - onSuccess() - }) + return chainLPCCommands(startCommands, false).then(() => { + onSuccess() + }) } - return { createStartLPCHandler } + return { handleStartLPC } } // Load all pipettes and labware into the maintenance run by utilizing the protocol resource. diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts index 180cf73f892..f549d860984 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleValidMoveToMaintenancePosition.ts @@ -1,16 +1,13 @@ 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 { 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 + pipette: LoadedPipette | null ) => Promise } @@ -19,21 +16,9 @@ export function useHandleValidMoveToMaintenancePosition({ }: UseLPCCommandWithChainRunChildProps): UseHandleValidMoveToMaintenancePositionResult { return { handleValidMoveToMaintenancePosition: ( - pipette: LoadedPipette | null, - step: LabwarePositionCheckStep | null + pipette: LoadedPipette | null ): Promise => { - if ( - step?.section === NAV_STEPS.ATTACH_PROBE || - step?.section === NAV_STEPS.DETACH_PROBE - ) { - return chainLPCCommands(moveToMaintenancePosition(pipette), false) - } else { - return Promise.reject( - new Error( - `Does not move to maintenance position if step is not a probe step. Step: ${step?.section}` - ) - ) - } + return chainLPCCommands(moveToMaintenancePosition(pipette), false) }, } } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/index.ts index c39cc30d305..9362a9e8319 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/index.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/index.ts @@ -1,12 +1,9 @@ import { useEffect } from 'react' import { useDispatch } from 'react-redux' -import { getLabwareDefinitionsFromCommands } from '@opentrons/components' +import { startLPC, LPC_STEPS } from '/app/redux/protocol-runs' +import { getActivePipetteId } from './utils' -import { startLPC } from '/app/redux/protocol-runs' -import { getLPCSteps } from './utils' - -import type { RunTimeCommand } from '@opentrons/shared-data' import type { LPCWizardState } from '/app/redux/protocol-runs' import type { LPCWizardFlexProps } from '/app/organisms/LabwarePositionCheck/LPCWizardFlex' @@ -16,29 +13,23 @@ export interface UseLPCInitialStateProps export function useLPCInitialState({ mostRecentAnalysis, runId, + labwareDefs, ...rest }: UseLPCInitialStateProps): void { const dispatch = useDispatch() useEffect(() => { - const protocolCommands: RunTimeCommand[] = mostRecentAnalysis.commands - const labwareDefs = getLabwareDefinitionsFromCommands(protocolCommands) - const LPCSteps = getLPCSteps({ - protocolData: mostRecentAnalysis, - labwareDefs, - }) + const activePipetteId = getActivePipetteId(mostRecentAnalysis.pipettes) const initialState: LPCWizardState = { ...rest, protocolData: mostRecentAnalysis, labwareDefs, - workingOffsets: [], + activePipetteId, steps: { currentStepIndex: 0, - totalStepCount: LPCSteps.length, - current: LPCSteps[0], - all: LPCSteps, - next: LPCSteps[1], + totalStepCount: LPC_STEPS.length, + all: LPC_STEPS, }, } diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/getActivePipetteId.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/getActivePipetteId.ts new file mode 100644 index 00000000000..901f5080fb0 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/getActivePipetteId.ts @@ -0,0 +1,20 @@ +import { getPipetteNameSpecs } from '@opentrons/shared-data' + +import type { LoadedPipette } from '@opentrons/shared-data' + +// Return the pipetteId for the pipette in the protocol with the highest channel count. +export function getActivePipetteId(pipettes: LoadedPipette[]): string { + // TODO(jh, 01-30-25): Actually handle the error here if it were to happen. + 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 +} diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/getLPCSteps/getProbeBasedLPCSteps.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/getLPCSteps/getProbeBasedLPCSteps.ts deleted file mode 100644 index b6184663762..00000000000 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/getLPCSteps/getProbeBasedLPCSteps.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { isEqual } from 'lodash' - -import { getLabwareDefURI, getPipetteNameSpecs } from '@opentrons/shared-data' - -import { NAV_STEPS } from '../../../../constants' -import { getLabwareLocationCombos } from '/app/organisms/LegacyApplyHistoricOffsets/hooks/getLabwareLocationCombos' - -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' -import type { GetLPCStepsParams } from '.' - -export function getProbeBasedLPCSteps( - params: GetLPCStepsParams -): LabwarePositionCheckStep[] { - const { protocolData } = params - - return [ - { section: NAV_STEPS.BEFORE_BEGINNING }, - { - section: NAV_STEPS.ATTACH_PROBE, - pipetteId: getPrimaryPipetteId(protocolData.pipettes), - }, - ...getAllCheckSectionSteps(params), - { - section: NAV_STEPS.DETACH_PROBE, - pipetteId: getPrimaryPipetteId(protocolData.pipettes), - }, - { section: NAV_STEPS.RESULTS_SUMMARY }, - ] -} - -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 labwareLocations = labwareLocationCombos.reduce( - (acc, labwareLocationCombo) => { - const labwareDef = labwareDefs.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: NAV_STEPS.CHECK_POSITIONS, - labwareId: labwareId, - pipetteId: getPrimaryPipetteId(pipettes), - location, - moduleId, - adapterId, - definitionUri, - }) - ) -} diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/getLPCSteps/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/getLPCSteps/index.ts deleted file mode 100644 index 7121d2cdf98..00000000000 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/getLPCSteps/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { getProbeBasedLPCSteps } from './getProbeBasedLPCSteps' - -import type { - CompletedProtocolAnalysis, - LabwareDefinition2, -} from '@opentrons/shared-data' -import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' - -export interface GetLPCStepsParams { - protocolData: CompletedProtocolAnalysis - labwareDefs: LabwareDefinition2[] -} - -// Prepare all LPC steps for injection. -export function getLPCSteps( - params: GetLPCStepsParams -): LabwarePositionCheckStep[] { - 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(params) - } - } else { - console.error('expected pipettes to be in protocol data') - return [] - } -} diff --git a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/index.ts b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/index.ts index 3fd9dba02b5..06380987129 100644 --- a/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/index.ts +++ b/app/src/organisms/LabwarePositionCheck/hooks/useLPCInitialState/utils/index.ts @@ -1 +1 @@ -export * from './getLPCSteps' +export * from './getActivePipetteId' diff --git a/app/src/organisms/LabwarePositionCheck/redux/index.ts b/app/src/organisms/LabwarePositionCheck/redux/index.ts deleted file mode 100644 index 51a3b4100a9..00000000000 --- a/app/src/organisms/LabwarePositionCheck/redux/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '../types' diff --git a/app/src/organisms/LabwarePositionCheck/redux/types.ts b/app/src/organisms/LabwarePositionCheck/redux/types.ts deleted file mode 100644 index d40d18d91d7..00000000000 --- a/app/src/organisms/LabwarePositionCheck/redux/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { LabwarePositionCheckStep } from '/app/organisms/LabwarePositionCheck/types' - -// TODO(jh, 01-16-25): Remove this once `steps` are refactored out of Redux. - -export 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 5ff197f899d..da84e634b34 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/AttachProbe.tsx @@ -21,9 +21,7 @@ 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 { AttachProbeStep, LPCStepProps } from '../types' -import type { State } from '/app/redux/types' -import type { LPCWizardState } from '/app/redux/protocol-runs' +import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' const StyledVideo = styled.video` padding-top: ${SPACING.spacing4}; @@ -44,33 +42,17 @@ export function AttachProbe({ runId, proceed, commandUtils, - step, -}: LPCStepProps): JSX.Element { +}: LPCWizardContentProps): JSX.Element { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) const isOnDevice = useSelector(getIsOnDevice) - const { steps } = useSelector( - (state: State) => state.protocolRuns[runId]?.lpc as LPCWizardState - ) - const { pipetteId } = step + const pipette = useSelector(selectActivePipette(runId)) const { - createProbeAttachmentHandler, - handleCheckItemsPrepModules, + handleProbeAttachment, toggleRobotMoving, setShowUnableToDetect, unableToDetect, } = commandUtils - const pipette = useSelector((state: State) => - selectActivePipette(step, runId, state) - ) - const channels = useSelector((state: State) => - selectActivePipetteChannelCount(step, runId, state) - ) - - const handleProbeAttached = createProbeAttachmentHandler( - pipetteId, - pipette, - proceed - ) + const channels = useSelector(selectActivePipetteChannelCount(runId)) const { probeLocation, probeVideoSrc } = ((): { probeLocation: string @@ -91,14 +73,13 @@ export function AttachProbe({ const handleProbeCheck = (): void => { void toggleRobotMoving(true) - .then(() => handleProbeAttached()) + .then(() => handleProbeAttachment(pipette, proceed)) .finally(() => toggleRobotMoving(false)) } const handleProceed = (): void => { void toggleRobotMoving(true) - .then(() => handleProbeAttached()) - .then(() => handleCheckItemsPrepModules(steps.next)) + .then(() => handleProbeAttachment(pipette, proceed)) .finally(() => toggleRobotMoving(false)) } diff --git a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx index c63568c4c60..cfda3476d6b 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/BeforeBeginning/index.tsx @@ -14,15 +14,13 @@ 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 { + selectActivePipette, + selectLabwareOffsetsForAllLw, +} 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' +import type { LPCWizardContentProps } 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' @@ -31,20 +29,15 @@ export function BeforeBeginning({ runId, proceed, commandUtils, -}: LPCStepProps): JSX.Element { +}: LPCWizardContentProps): JSX.Element { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) 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(activePipette, proceed) + const activePipette = useSelector(selectActivePipette(runId)) + const existingOffsets = useSelector(selectLabwareOffsetsForAllLw(runId)) + const { protocolName, labwareDefs } = useSelector( + (state: State) => state.protocolRuns[runId]?.lpc + ) ?? { protocolName: '', labwareDefs: [] } + const { handleStartLPC, toggleRobotMoving } = commandUtils const requiredEquipmentList = [ { @@ -59,7 +52,7 @@ export function BeforeBeginning({ const handleProceed = (): void => { void toggleRobotMoving(true) - .then(() => handleStartLPC()) + .then(() => handleStartLPC(activePipette, proceed)) .finally(() => toggleRobotMoving(false)) } diff --git a/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx index 1659fe375db..d6261151e7a 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' import { useSelector } from 'react-redux' @@ -19,9 +20,7 @@ import detachProbe1 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach 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' +import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' const StyledVideo = styled.video` padding-top: ${SPACING.spacing4}; @@ -42,18 +41,23 @@ export const DetachProbe = ({ runId, proceed, commandUtils, -}: LPCStepProps): JSX.Element => { +}: LPCWizardContentProps): JSX.Element => { const { t, i18n } = useTranslation(['labware_position_check', 'shared']) - const { current: currentStep } = useSelector( - (state: State) => state.protocolRuns[runId]?.lpc?.steps as StepsInfo - ) - const { createProbeDetachmentHandler, toggleRobotMoving } = commandUtils - const pipette = useSelector((state: State) => - selectActivePipette(currentStep, runId, state) - ) - const channels = useSelector((state: State) => - selectActivePipetteChannelCount(currentStep, runId, state) - ) + const { + handleProbeDetachment, + toggleRobotMoving, + handleValidMoveToMaintenancePosition, + } = commandUtils + const pipette = useSelector(selectActivePipette(runId)) + const channels = useSelector(selectActivePipetteChannelCount(runId)) + + // TODO(jh, 01-30-25): This will break the flows, but currently, DetachProbe is inaccessible. + // This onClick behavior should be tied directly to the "exit" button. + useEffect(() => { + void toggleRobotMoving(true) + .then(() => handleValidMoveToMaintenancePosition(pipette)) + .finally(() => toggleRobotMoving(false)) + }, []) const probeVideoSrc = ((): string => { switch (channels) { @@ -66,11 +70,9 @@ export const DetachProbe = ({ } })() - const handleProbeDetached = createProbeDetachmentHandler(pipette, proceed) - const handleProceed = (): void => { void toggleRobotMoving(true) - .then(() => handleProbeDetached()) + .then(() => handleProbeDetachment(pipette, proceed)) .finally(() => toggleRobotMoving(false)) } diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/CheckItem.tsx similarity index 67% rename from app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx rename to app/src/organisms/LabwarePositionCheck/steps/HandleLabware/CheckItem.tsx index fa7366af6fc..b8eabaa54ab 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/CheckItem.tsx @@ -8,63 +8,61 @@ import { getLabwareDisplayLocation, } from '@opentrons/components' -import { NAV_STEPS } from '/app/organisms/LabwarePositionCheck/constants' import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' import { UnorderedList } from '/app/molecules/UnorderedList' import { + applyOffset, + clearSelectedLabware, setFinalPosition, setInitialPosition, } from '/app/redux/protocol-runs/actions' -import { JogToWell } from './JogToWell' -import { PrepareSpace } from './PrepareSpace' +import { EditOffset } from './EditOffset' +import { PrepareLabware } from './PrepareLabware' import { PlaceItemInstruction } from './PlaceItemInstruction' import { - selectActiveLwInitialPosition, + selectSelectedLwInitialPosition, selectActivePipette, - selectIsActiveLwTipRack, + selectIsSelectedLwTipRack, + selectSelectedLabwareInfo, } 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 { LoadedPipette } from '@opentrons/shared-data' import type { State } from '/app/redux/types' -import type { LPCWizardState } from '/app/redux/protocol-runs' +import type { + LPCWizardState, + OffsetLocationDetails, + SelectedLabwareInfo, +} from '/app/redux/protocol-runs' +import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' -export function CheckItem( - props: LPCStepProps -): JSX.Element { - const { runId, proceed, commandUtils, step } = props - const { labwareId, location } = step +export function CheckItem(props: LPCWizardContentProps): JSX.Element { + const { runId, commandUtils } = props const { handleJog, - handleCheckItemsPrepModules, handleConfirmLwModulePlacement, handleConfirmLwFinalPosition, handleResetLwModulesOnDeck, - handleValidMoveToMaintenancePosition, toggleRobotMoving, } = commandUtils const dispatch = useDispatch() const isOnDevice = useSelector(getIsOnDevice) - const { protocolData, labwareDefs, steps } = useSelector( + const { protocolData, labwareDefs } = 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 = 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 pipette = useSelector(selectActivePipette(runId)) as LoadedPipette + const pipetteId = pipette.id + const initialPosition = useSelector(selectSelectedLwInitialPosition(runId)) + const isLwTiprack = useSelector(selectIsSelectedLwTipRack(runId)) + const selectedLwInfo = useSelector( + selectSelectedLabwareInfo(runId) + ) as SelectedLabwareInfo + const offsetLocationDetails = selectedLwInfo.offsetLocationDetails as OffsetLocationDetails const buildDisplayParams = (): Omit< DisplayLocationParams, @@ -74,7 +72,7 @@ export function CheckItem( loadedModules: protocolData.modules, loadedLabwares: protocolData.labware, robotType: FLEX_ROBOT_TYPE, - location, + location: offsetLocationDetails, }) const slotOnlyDisplayLocation = getLabwareDisplayLocation({ @@ -89,12 +87,14 @@ export function CheckItem( const handlePrepareProceed = (): void => { void toggleRobotMoving(true) - .then(() => handleConfirmLwModulePlacement({ step })) + .then(() => + handleConfirmLwModulePlacement(offsetLocationDetails, pipetteId) + ) .then(position => { dispatch( setInitialPosition(runId, { - labwareId, - location, + labwareUri: selectedLwInfo.uri, + location: offsetLocationDetails, position, }) ) @@ -102,43 +102,37 @@ export function CheckItem( .finally(() => toggleRobotMoving(false)) } - // TODO(jh, 01-14-25): Revisit next step injection after refactoring the store (after designs settle). const handleJogProceed = (): void => { void toggleRobotMoving(true) - .then(() => - handleConfirmLwFinalPosition({ - step, - onSuccess: proceed, - pipette, - }) - ) + .then(() => handleConfirmLwFinalPosition(offsetLocationDetails, pipette)) .then(position => { dispatch( setFinalPosition(runId, { - labwareId, - location, + labwareUri: selectedLwInfo.uri, + location: offsetLocationDetails, position, }) ) }) + // TODO(jh, 01-30-25): This entire sequence of dispatches can be reduced to one dispatch + // after the API changes, but should be separate until then. See APPLY_OFFSET comment in LPC reducer. .then(() => { - if (steps.next?.section === NAV_STEPS.CHECK_POSITIONS) { - return handleCheckItemsPrepModules(steps.next) - } else { - return handleValidMoveToMaintenancePosition(pipette, steps.next) - } + dispatch(applyOffset(runId, selectedLwInfo.uri)) + }) + .then(() => { + dispatch(clearSelectedLabware(runId)) }) .finally(() => toggleRobotMoving(false)) } const handleGoBack = (): void => { void toggleRobotMoving(true) - .then(() => handleResetLwModulesOnDeck({ step })) + .then(() => handleResetLwModulesOnDeck(offsetLocationDetails)) .then(() => { dispatch( setInitialPosition(runId, { - labwareId, - location, + labwareUri: selectedLwInfo.uri, + location: offsetLocationDetails, position: null, }) ) @@ -146,11 +140,10 @@ export function CheckItem( .finally(() => toggleRobotMoving(false)) } - // TODO(jh 01-15-24): These should be separate steps, but let's wait for designs to settle. return ( {initialPosition != null ? ( - ) : ( - , ]} /> } confirmPlacement={handlePrepareProceed} - {...props} + selectedLwInfo={selectedLwInfo} /> )} diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/LiveOffsetValue.tsx b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/EditOffset/LiveOffsetValue.tsx similarity index 92% rename from app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/LiveOffsetValue.tsx rename to app/src/organisms/LabwarePositionCheck/steps/HandleLabware/EditOffset/LiveOffsetValue.tsx index 18ead803548..3ea93e2774d 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/LiveOffsetValue.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/EditOffset/LiveOffsetValue.tsx @@ -19,10 +19,7 @@ import { import { getIsOnDevice } from '/app/redux/config' import type { StyleProps } from '@opentrons/components' -import type { - CheckPositionsStep, - LPCStepProps, -} from '/app/organisms/LabwarePositionCheck/types' +import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' interface OffsetVectorProps extends StyleProps { x: number @@ -31,7 +28,7 @@ interface OffsetVectorProps extends StyleProps { } export function LiveOffsetValue( - props: OffsetVectorProps & LPCStepProps + props: OffsetVectorProps & LPCWizardContentProps ): JSX.Element { const { x, y, z, ...styleProps } = props const { i18n, t } = useTranslation('labware_position_check') diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/EditOffset/index.tsx similarity index 86% rename from app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/index.tsx rename to app/src/organisms/LabwarePositionCheck/steps/HandleLabware/EditOffset/index.tsx index 26a0dd599e6..8ca3186b54e 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/JogToWell/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/EditOffset/index.tsx @@ -35,33 +35,28 @@ import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' import { JogControls } from '/app/molecules/JogControls' import { LiveOffsetValue } from './LiveOffsetValue' import { - selectActiveLwExistingOffset, - selectActiveLwInitialPosition, + selectSelectedLwExistingOffset, + selectSelectedLwInitialPosition, selectActivePipette, - selectIsActiveLwTipRack, - selectItemLabwareDef, + selectIsSelectedLwTipRack, + selectSelectedLabwareDef, } from '/app/redux/protocol-runs' import { getIsOnDevice } from '/app/redux/config' +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 { ReactNode } from 'react' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { VectorOffset } from '@opentrons/api-client' import type { Jog } from '/app/molecules/JogControls' -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' +import type { LPCWizardContentProps } 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 { +interface JogToWellProps extends LPCWizardContentProps { header: ReactNode body: ReactNode handleConfirmPosition: () => void @@ -69,7 +64,7 @@ interface JogToWellProps extends LPCStepProps { handleJog: Jog } -export function JogToWell(props: JogToWellProps): JSX.Element { +export function EditOffset(props: JogToWellProps): JSX.Element { const { runId, header, @@ -79,30 +74,18 @@ export function JogToWell(props: JogToWellProps): JSX.Element { handleJog, } = props const { t } = useTranslation(['labware_position_check', 'shared']) - const { steps } = useSelector( - (state: State) => state.protocolRuns[runId]?.lpc as LPCWizardState - ) - const { current: currentStep } = steps 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 initialPosition = + useSelector(selectSelectedLwInitialPosition(runId)) ?? IDENTITY_VECTOR + const pipetteName = + useSelector(selectActivePipette(runId))?.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) + selectSelectedLabwareDef(runId) + ) as LabwareDefinition2 + const isTipRack = useSelector(selectIsSelectedLwTipRack(runId)) + const activeLwExistingOffset = useSelector( + selectSelectedLwExistingOffset(runId) ) const [joggedPosition, setJoggedPosition] = useState( diff --git a/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareDetails/AppliedLocationOffsetsContainer.tsx b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareDetails/AppliedLocationOffsetsContainer.tsx new file mode 100644 index 00000000000..1b7917f6ae3 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareDetails/AppliedLocationOffsetsContainer.tsx @@ -0,0 +1,151 @@ +import { useTranslation } from 'react-i18next' +import { useDispatch, useSelector } from 'react-redux' +import { css } from 'styled-components' + +import { + Flex, + StyledText, + ListButton, + DeckInfoLabel, + SPACING, + getLabwareDisplayLocation, + JUSTIFY_SPACE_BETWEEN, + DIRECTION_COLUMN, +} from '@opentrons/components' +import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' + +import { + selectSelectedLabwareInfo, + selectSelectedLwInitialPosition, + selectSelectedOffsetDetails, + setSelectedLabware, +} from '/app/redux/protocol-runs' + +import type { State } from '/app/redux/types' +import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' +import type { + LPCWizardState, + OffsetDetails, + SelectedLabwareInfo, +} from '/app/redux/protocol-runs' + +export function AppliedLocationOffsetsContainer( + props: LPCWizardContentProps +): JSX.Element { + const offsetDetails = useSelector(selectSelectedOffsetDetails(props.runId)) + + return ( + + {offsetDetails.map(offset => ( + + ))} + {/* Gives extra scrollable space. */} + + + ) +} + +interface LabwareLocationItemProps extends LPCWizardContentProps { + offsetDetail: OffsetDetails +} + +function LabwareLocationItemContainer( + props: LabwareLocationItemProps +): JSX.Element { + const { t } = useTranslation('labware_position_check') + + return ( + + + + {t('slot_location')} + + {t('offsets')} + + + + ) +} + +function LabwareLocationItem({ + runId, + offsetDetail, + commandUtils, +}: LabwareLocationItemProps): JSX.Element { + const { t: commandTextT } = useTranslation('protocol_command_text') + const { toggleRobotMoving, handleCheckItemsPrepModules } = commandUtils + const dispatch = useDispatch() + + const { protocolData } = useSelector( + (state: State) => state.protocolRuns[runId]?.lpc as LPCWizardState + ) + const selectedLw = useSelector( + selectSelectedLabwareInfo(runId) + ) as SelectedLabwareInfo + const initialPosition = useSelector(selectSelectedLwInitialPosition(runId)) + + const slotCopy = getLabwareDisplayLocation({ + t: commandTextT, + loadedModules: protocolData.modules, + loadedLabwares: protocolData.labware, + robotType: FLEX_ROBOT_TYPE, + location: { slotName: offsetDetail.locationDetails.slotName as string }, + detailLevel: 'slot-only', + }) + + const handleOnClick = (): void => { + void toggleRobotMoving(true) + .then(() => { + dispatch( + setSelectedLabware( + runId, + selectedLw.uri, + offsetDetail.locationDetails + ) + ) + }) + .then(() => + handleCheckItemsPrepModules( + offsetDetail.locationDetails, + initialPosition + ) + ) + .finally(() => toggleRobotMoving(false)) + } + + return ( + + + {/* TODO(jh, 01-30-31): Add a new detail level to getLabwareDisplayLocation instead of slicing. */} + + + + ) +} + +const APPLIED_LOCATION_CONTAINER_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + grid-gap: ${SPACING.spacing24}; +` + +const HEADER_STYLE = css` + padding: 0 1.375rem; + grid-gap: 3.813rem; +` + +const LOCATION_ITEM_CONTAINER_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + grid-gap: ${SPACING.spacing8}; +` + +const BUTTON_TEXT_STYLE = css` + justify-content: ${JUSTIFY_SPACE_BETWEEN}; +` + +const BOX_STYLE = css` + height: ${SPACING.spacing40}; +` diff --git a/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareDetails/DefaultLocationOffset.tsx b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareDetails/DefaultLocationOffset.tsx new file mode 100644 index 00000000000..617c3f28d9c --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareDetails/DefaultLocationOffset.tsx @@ -0,0 +1,66 @@ +import { css } from 'styled-components' +import { useTranslation } from 'react-i18next' + +import { + ListButton, + Flex, + Tag, + StyledText, + PrimaryButton, + Icon, + SPACING, + DIRECTION_COLUMN, + JUSTIFY_SPACE_BETWEEN, + ALIGN_CENTER, +} from '@opentrons/components' + +import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' + +export function DefaultLocationOffset( + props: LPCWizardContentProps +): JSX.Element { + const { t } = useTranslation('labware_position_check') + + return ( + + + + + {t('default_labware_offset')} + + + + + + + + + {t('add')} + + + + + ) +} + +const BUTTON_ALL_CONTENT_STYLE = css` + grid-gap: ${SPACING.spacing24}; + justify-content: ${JUSTIFY_SPACE_BETWEEN}; + width: 100%; +` + +const BUTTON_LEFT_CONTENT_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + grid-gap: ${SPACING.spacing8}; +` + +const BUTTON_TEXT_CONTAINER_STYLE = css` + grid-gap: ${SPACING.spacing8}; + justify-content: ${JUSTIFY_SPACE_BETWEEN}; + align-items: ${ALIGN_CENTER}; +` + +const ADD_ICON_STYLE = css` + width: 1.75rem; + height: 1.75rem; +` diff --git a/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareDetails/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareDetails/index.tsx new file mode 100644 index 00000000000..2aff38442e7 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareDetails/index.tsx @@ -0,0 +1,16 @@ +import { Flex } from '@opentrons/components' + +import { AppliedLocationOffsetsContainer } from './AppliedLocationOffsetsContainer' +import { DefaultLocationOffset } from './DefaultLocationOffset' +import { LIST_CONTAINER_STYLE } from '/app/organisms/LabwarePositionCheck/steps/HandleLabware/contants' + +import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' + +export function LPCLabwareDetails(props: LPCWizardContentProps): JSX.Element { + return ( + + + + + ) +} diff --git a/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareList/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareList/index.tsx new file mode 100644 index 00000000000..da16d7232bc --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/LPCLabwareList/index.tsx @@ -0,0 +1,71 @@ +import { css } from 'styled-components' +import { useTranslation } from 'react-i18next' +import { useDispatch, useSelector } from 'react-redux' + +import { + Flex, + StyledText, + SPACING, + ListButton, + DIRECTION_COLUMN, +} from '@opentrons/components' + +import { + selectAllLabwareInfo, + setSelectedLabwareName, +} from '/app/redux/protocol-runs' +import { LIST_CONTAINER_STYLE } from '/app/organisms/LabwarePositionCheck/steps/HandleLabware/contants' + +import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' +import type { LabwareDetails } from '/app/redux/protocol-runs' + +export function LPCLabwareList(props: LPCWizardContentProps): JSX.Element { + const { t } = useTranslation('labware_position_check') + const labwareInfo = useSelector(selectAllLabwareInfo(props.runId)) + + return ( + + {t('select_labware_from_list')} + + {Object.entries(labwareInfo).map(([uri, info]) => ( + + ))} + + + ) +} + +interface LabwareItemProps extends LPCWizardContentProps { + uri: string + info: LabwareDetails +} + +function LabwareItem({ uri, info, runId }: LabwareItemProps): JSX.Element { + const dispatch = useDispatch() + + const handleOnClick = (): void => { + dispatch(setSelectedLabwareName(runId, uri)) + } + + return ( + + + {info.displayName} + + + ) +} + +const LIST_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + grid-gap: ${SPACING.spacing8}; +` + +const BUTTON_TEXT_STYLE = css` + grid-gap: ${SPACING.spacing24}; +` diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PlaceItemInstruction.tsx b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/PlaceItemInstruction.tsx similarity index 77% rename from app/src/organisms/LabwarePositionCheck/steps/CheckItem/PlaceItemInstruction.tsx rename to app/src/organisms/LabwarePositionCheck/steps/HandleLabware/PlaceItemInstruction.tsx index 7fc4487b278..2ac53d07757 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PlaceItemInstruction.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/PlaceItemInstruction.tsx @@ -5,37 +5,32 @@ import { TYPOGRAPHY, LegacyStyledText } from '@opentrons/components' import { selectActiveAdapterDisplayName, - selectLwDisplayName, + selectSelectedLwDisplayName, } from '/app/redux/protocol-runs' -import type { State } from '/app/redux/types' -import type { - CheckPositionsStep, - LPCStepProps, -} from '/app/organisms/LabwarePositionCheck/types' +import type { SelectedLabwareInfo } from '/app/redux/protocol-runs' -interface PlaceItemInstructionProps extends LPCStepProps { +import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' + +interface PlaceItemInstructionProps extends LPCWizardContentProps { isLwTiprack: boolean slotOnlyDisplayLocation: string fullDisplayLocation: string + labwareInfo: SelectedLabwareInfo } export function PlaceItemInstruction({ runId, - step, isLwTiprack, slotOnlyDisplayLocation, fullDisplayLocation, + labwareInfo, }: PlaceItemInstructionProps): JSX.Element { const { t } = useTranslation('labware_position_check') - const { adapterId } = step - const labwareDisplayName = useSelector((state: State) => - selectLwDisplayName(runId, state) - ) - const adapterDisplayName = useSelector((state: State) => - selectActiveAdapterDisplayName(runId, state) - ) + const { adapterId } = labwareInfo.offsetLocationDetails ?? { adapterId: null } + const labwareDisplayName = useSelector(selectSelectedLwDisplayName(runId)) + const adapterDisplayName = useSelector(selectActiveAdapterDisplayName(runId)) if (isLwTiprack) { return ( diff --git a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PrepareSpace.tsx b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/PrepareLabware/index.tsx similarity index 79% rename from app/src/organisms/LabwarePositionCheck/steps/CheckItem/PrepareSpace.tsx rename to app/src/organisms/LabwarePositionCheck/steps/HandleLabware/PrepareLabware/index.tsx index d308d986a11..02143241727 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/CheckItem/PrepareSpace.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/PrepareLabware/index.tsx @@ -24,42 +24,46 @@ import { import { SmallButton } from '/app/atoms/buttons' import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' -import { selectItemLabwareDef } from '/app/redux/protocol-runs' +import { selectSelectedLabwareDef } from '/app/redux/protocol-runs' import { getIsOnDevice } from '/app/redux/config' import type { ReactNode } from 'react' import type { LabwareDefinition2 } from '@opentrons/shared-data' -import type { - CheckPositionsStep, - LPCStepProps, -} from '/app/organisms/LabwarePositionCheck/types' import type { State } from '/app/redux/types' -import type { LPCWizardState } from '/app/redux/protocol-runs' +import type { + LPCWizardState, + OffsetLocationDetails, + SelectedLabwareInfo, +} from '/app/redux/protocol-runs' +import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' const LPC_HELP_LINK_URL = 'https://support.opentrons.com/s/article/How-Labware-Offsets-work-on-the-OT-2' -interface PrepareSpaceProps extends LPCStepProps { +interface PrepareSpaceProps extends LPCWizardContentProps { header: ReactNode body: ReactNode confirmPlacement: () => void + selectedLwInfo: SelectedLabwareInfo } -export function PrepareSpace({ +export function PrepareLabware({ runId, header, body, confirmPlacement, + selectedLwInfo, }: PrepareSpaceProps): JSX.Element { const { i18n, t } = useTranslation(['labware_position_check', 'shared']) - const { protocolData, deckConfig, steps } = useSelector( + const { protocolData, deckConfig } = 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 + selectSelectedLabwareDef(runId) + ) as LabwareDefinition2 + const offsetLocationDetails = selectedLwInfo.offsetLocationDetails as OffsetLocationDetails + const { moduleModel } = offsetLocationDetails return ( @@ -74,20 +78,16 @@ export function PrepareSpace({ modulesOnDeck={protocolData.modules.map(mod => ({ moduleModel: mod.model, moduleLocation: mod.location, - nestedLabwareDef: - 'moduleModel' in location && location.moduleModel != null - ? labwareDef - : null, + nestedLabwareDef: moduleModel != null ? labwareDef : null, innerProps: - 'moduleModel' in location && - location.moduleModel != null && - getModuleType(location.moduleModel) === THERMOCYCLER_MODULE_TYPE + moduleModel != null && + getModuleType(moduleModel) === THERMOCYCLER_MODULE_TYPE ? { lidMotorState: 'open' } : {}, }))} labwareOnDeck={[ { - labwareLocation: location, + labwareLocation: offsetLocationDetails, definition: labwareDef, }, ].filter( diff --git a/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/contants.ts b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/contants.ts new file mode 100644 index 00000000000..4d2daa14776 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/contants.ts @@ -0,0 +1,13 @@ +import { css } from 'styled-components' + +import { SPACING, DIRECTION_COLUMN } from '@opentrons/components' + +/** + * Styles + */ + +export const LIST_CONTAINER_STYLE = css` + flex-direction: ${DIRECTION_COLUMN}; + padding: ${SPACING.spacing32} ${SPACING.spacing60}; + grid-gap: ${SPACING.spacing24}; +` diff --git a/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/index.tsx new file mode 100644 index 00000000000..e168b19df09 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/index.tsx @@ -0,0 +1,33 @@ +import { useSelector } from 'react-redux' + +import { + selectSelectedLabwareFlowType, + selectSelectedLabwareInfo, +} from '/app/redux/protocol-runs' +import { CheckItem } from './CheckItem' +import { LPCLabwareList } from './LPCLabwareList' +import { LPCLabwareDetails } from './LPCLabwareDetails' + +import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' + +export function HandleLabware(props: LPCWizardContentProps): JSX.Element { + const selectedLw = useSelector(selectSelectedLabwareInfo(props.runId)) + const offsetFlowType = useSelector(selectSelectedLabwareFlowType(props.runId)) + + if (selectedLw == null) { + return + } else if (selectedLw.offsetLocationDetails == null) { + return + } else { + switch (offsetFlowType) { + case 'default': + return + case 'location-specific': + return + default: { + console.error(`Unexpected offsetFlowType: ${offsetFlowType}`) + return + } + } + } +} diff --git a/app/src/organisms/LabwarePositionCheck/steps/LPCComplete/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/LPCComplete/index.tsx new file mode 100644 index 00000000000..f40e117dba0 --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/steps/LPCComplete/index.tsx @@ -0,0 +1,13 @@ +import { useEffect } from 'react' + +import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types' + +export function LPCComplete(props: LPCWizardContentProps): JSX.Element { + useEffect(() => { + setTimeout(() => { + props.onCloseClick() + }, 5000) + }, []) + + return <>LPC COMPLETE +} diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx deleted file mode 100644 index 46d8d430bbb..00000000000 --- a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/OffsetTable.tsx +++ /dev/null @@ -1,147 +0,0 @@ -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 { - BORDERS, - COLORS, - Flex, - SPACING, - LegacyStyledText, - TYPOGRAPHY, - getLabwareDisplayLocation, -} from '@opentrons/components' - -import { selectLwDisplayName } from '/app/redux/protocol-runs' - -import type { LabwareDefinition2 } from '@opentrons/shared-data' -import type { LegacyLabwareOffsetCreateData } from '@opentrons/api-client' -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: LegacyLabwareOffsetCreateData[] - labwareDefinitions: LabwareDefinition2[] -} - -export function OffsetTable({ - offsets, - runId, - labwareDefinitions, -}: OffsetTableProps): JSX.Element { - 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') - - return ( - - - - {t('location')} - {t('labware')} - {t('labware_offset_data')} - - - - - {offsets.map(({ location, vector }, index) => { - const displayLocation = getLabwareDisplayLocation({ - location, - allRunDefs: labwareDefinitions, - detailLevel: 'full', - t, - loadedModules: protocolData.modules, - loadedLabwares: protocolData.labware, - robotType: FLEX_ROBOT_TYPE, - }) - - return ( - - - - {displayLocation} - - - - {lwDisplayName} - - - {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; -` - -const LeftRoundedTableDatum = styled(TableDatum)` - border-radius: ${BORDERS.borderRadius4} 0 0 ${BORDERS.borderRadius4}; -` - -const RightRoundedTableDatum = styled(TableDatum)` - border-radius: 0 ${BORDERS.borderRadius4} ${BORDERS.borderRadius4} 0; -` diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/TableComponent.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/TableComponent.tsx deleted file mode 100644 index 7e1dce4d58e..00000000000 --- a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/TableComponent.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useSelector } from 'react-redux' - -import { TerseOffsetTable } from '/app/organisms/TerseOffsetTable' -import { OffsetTable } from './OffsetTable' -import { getIsOnDevice } from '/app/redux/config' - -import type { LegacyLabwareOffsetCreateData } from '@opentrons/api-client' -import type { - LPCStepProps, - ResultsSummaryStep, -} from '/app/organisms/LabwarePositionCheck/types' -import type { State } from '/app/redux/types' -import type { LPCWizardState } from '/app/redux/protocol-runs' - -interface TableComponentProps extends LPCStepProps { - offsetsToApply: LegacyLabwareOffsetCreateData[] -} - -export function TableComponent(props: TableComponentProps): JSX.Element { - const { offsetsToApply, runId } = props - const isOnDevice = useSelector(getIsOnDevice) - const { labwareDefs } = useSelector( - (state: State) => state.protocolRuns[runId]?.lpc as LPCWizardState - ) - - return isOnDevice ? ( - - ) : ( - - ) -} diff --git a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/index.tsx b/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/index.tsx deleted file mode 100644 index f63d5f8518d..00000000000 --- a/app/src/organisms/LabwarePositionCheck/steps/ResultsSummary/index.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import styled, { css } from 'styled-components' -import { useSelector } from 'react-redux' -import { useTranslation } from 'react-i18next' - -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, - getIsOnDevice, -} from '/app/redux/config' -import { SmallButton } from '/app/atoms/buttons' -import { LabwareOffsetTabs } from '/app/organisms/LabwareOffsetTabs' -import { TableComponent } from './TableComponent' - -import type { - LPCStepProps, - ResultsSummaryStep, -} from '/app/organisms/LabwarePositionCheck/types' -import type { State } from '/app/redux/types' -import type { LPCWizardState } from '/app/redux/protocol-runs' - -// 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 { commandUtils, runId } = props - const isOnDevice = useSelector(getIsOnDevice) - const { protocolData } = useSelector( - (state: State) => state.protocolRuns[runId]?.lpc as LPCWizardState - ) - const { - isApplyingOffsets, - handleApplyOffsetsAndClose, - buildOffsetsToApply, - toggleRobotMoving, - } = commandUtils - const { i18n, t } = useTranslation('labware_position_check') - const offsetsToApply = buildOffsetsToApply() - const isLabwareOffsetCodeSnippetsOn = useSelector( - getIsLabwareOffsetCodeSnippetsOn - ) - - const handleProceed = (): void => { - void toggleRobotMoving(true).then(() => - handleApplyOffsetsAndClose(offsetsToApply) - ) - } - - return ( - - -
{t('new_labware_offset_data')}
- {isLabwareOffsetCodeSnippetsOn ? ( - - } - JupyterComponent={ - - } - CommandLineComponent={ - - } - marginTop={SPACING.spacing16} - /> - ) : ( - - )} -
- {isOnDevice ? ( - - ) : ( - - - - - {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} - } -` diff --git a/app/src/organisms/LabwarePositionCheck/steps/index.ts b/app/src/organisms/LabwarePositionCheck/steps/index.ts index 9bf7efbad46..62bf40d486f 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/index.ts +++ b/app/src/organisms/LabwarePositionCheck/steps/index.ts @@ -1,5 +1,5 @@ export { BeforeBeginning } from './BeforeBeginning' +export { HandleLabware } from './HandleLabware' export { AttachProbe } from './AttachProbe' -export { CheckItem } from './CheckItem' export { DetachProbe } from './DetachProbe' -export { ResultsSummary } from './ResultsSummary' +export { LPCComplete } from './LPCComplete' diff --git a/app/src/organisms/LabwarePositionCheck/types/index.ts b/app/src/organisms/LabwarePositionCheck/types/index.ts index 4da2755de80..f71cb81b5a5 100644 --- a/app/src/organisms/LabwarePositionCheck/types/index.ts +++ b/app/src/organisms/LabwarePositionCheck/types/index.ts @@ -1,2 +1 @@ -export * from './steps' export * from './content' diff --git a/app/src/organisms/LabwarePositionCheck/types/steps.ts b/app/src/organisms/LabwarePositionCheck/types/steps.ts deleted file mode 100644 index 17caa519d1b..00000000000 --- a/app/src/organisms/LabwarePositionCheck/types/steps.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { LegacyLabwareOffsetLocation } from '@opentrons/api-client' -import type { NAV_STEPS } from '../constants' -import type { LPCWizardContentProps } from './content' - -export type LabwarePositionCheckStep = - | BeforeBeginningStep - | AttachProbeStep - | CheckPositionsStep - | DetachProbeStep - | ResultsSummaryStep - -export type LPCStepProps = Omit< - LPCWizardContentProps, - 'step' -> & { - step: Extract -} - -export interface PerformLPCStep { - pipetteId: string - labwareId: string - location: LegacyLabwareOffsetLocation - definitionUri: string - adapterId?: string - moduleId?: string -} - -export interface BeforeBeginningStep { - section: typeof NAV_STEPS.BEFORE_BEGINNING -} - -export interface AttachProbeStep { - section: typeof NAV_STEPS.ATTACH_PROBE - pipetteId: string -} - -export interface CheckPositionsStep extends PerformLPCStep { - section: typeof NAV_STEPS.CHECK_POSITIONS -} - -export interface DetachProbeStep { - section: typeof NAV_STEPS.DETACH_PROBE - pipetteId: string -} - -export interface ResultsSummaryStep { - section: typeof NAV_STEPS.RESULTS_SUMMARY -} diff --git a/app/src/organisms/LegacyApplyHistoricOffsets/index.tsx b/app/src/organisms/LegacyApplyHistoricOffsets/index.tsx index de216131c79..fc60b5b8a64 100644 --- a/app/src/organisms/LegacyApplyHistoricOffsets/index.tsx +++ b/app/src/organisms/LegacyApplyHistoricOffsets/index.tsx @@ -97,7 +97,7 @@ export function LegacyApplyHistoricOffsets( - {t(noOffsetData ? 'no_offset_data' : 'apply_offset_data')} + {t(noOffsetData ? 'legacy_no_offset_data' : 'apply_offset_data')} } diff --git a/app/src/redux/protocol-runs/actions/lpc.ts b/app/src/redux/protocol-runs/actions/lpc.ts index 5ec472e094c..d5a90ec07b5 100644 --- a/app/src/redux/protocol-runs/actions/lpc.ts +++ b/app/src/redux/protocol-runs/actions/lpc.ts @@ -4,6 +4,11 @@ import { SET_FINAL_POSITION, START_LPC, FINISH_LPC, + GO_BACK_STEP, + SET_SELECTED_LABWARE, + CLEAR_SELECTED_LABWARE, + SET_SELECTED_LABWARE_NAME, + APPLY_OFFSET, } from '../constants' import type { @@ -14,12 +19,55 @@ import type { PositionParams, ProceedStepAction, FinishLPCAction, + GoBackStepAction, + SelectedLabwareAction, + ClearSelectedLabwareAction, + SelectedLabwareNameAction, + OffsetLocationDetails, + ApplyOffsetAction, } from '../types' export const proceedStep = (runId: string): ProceedStepAction => ({ type: PROCEED_STEP, payload: { runId }, }) + +export const goBackStep = (runId: string): GoBackStepAction => ({ + type: GO_BACK_STEP, + payload: { runId }, +}) + +export const setSelectedLabwareName = ( + runId: string, + labwareUri: string +): SelectedLabwareNameAction => ({ + type: SET_SELECTED_LABWARE_NAME, + payload: { + runId, + labwareUri, + }, +}) + +export const setSelectedLabware = ( + runId: string, + labwareUri: string, + location: OffsetLocationDetails | null +): SelectedLabwareAction => ({ + type: SET_SELECTED_LABWARE, + payload: { + runId, + labwareUri, + location, + }, +}) + +export const clearSelectedLabware = ( + runId: string +): ClearSelectedLabwareAction => ({ + type: CLEAR_SELECTED_LABWARE, + payload: { runId }, +}) + export const setInitialPosition = ( runId: string, params: PositionParams @@ -36,6 +84,14 @@ export const setFinalPosition = ( payload: { ...params, runId }, }) +export const applyOffset = ( + runId: string, + labwareUri: string +): ApplyOffsetAction => ({ + type: APPLY_OFFSET, + payload: { runId, labwareUri }, +}) + export const startLPC = ( runId: string, state: LPCWizardState diff --git a/app/src/redux/protocol-runs/constants/lpc.ts b/app/src/redux/protocol-runs/constants/lpc.ts deleted file mode 100644 index 669c8ec503a..00000000000 --- a/app/src/redux/protocol-runs/constants/lpc.ts +++ /dev/null @@ -1,5 +0,0 @@ -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/constants/lpc/actions.ts b/app/src/redux/protocol-runs/constants/lpc/actions.ts new file mode 100644 index 00000000000..6fd006a9759 --- /dev/null +++ b/app/src/redux/protocol-runs/constants/lpc/actions.ts @@ -0,0 +1,10 @@ +export const START_LPC = 'START_LPC' +export const FINISH_LPC = 'FINISH_LPC' +export const PROCEED_STEP = 'PROCEED_STEP' +export const GO_BACK_STEP = 'GO_BACK_STEP' +export const SET_SELECTED_LABWARE_NAME = 'SET_SELECTED_LABWARE_NAME' +export const SET_SELECTED_LABWARE = 'SET_SELECTED_LABWARE' +export const CLEAR_SELECTED_LABWARE = 'CLEAR_SELECTED_LABWARE' +export const SET_INITIAL_POSITION = 'SET_INITIAL_POSITION' +export const SET_FINAL_POSITION = 'SET_FINAL_POSITION' +export const APPLY_OFFSET = 'APPLY_OFFSET' diff --git a/app/src/redux/protocol-runs/constants/lpc/index.ts b/app/src/redux/protocol-runs/constants/lpc/index.ts new file mode 100644 index 00000000000..3469ff9cd4a --- /dev/null +++ b/app/src/redux/protocol-runs/constants/lpc/index.ts @@ -0,0 +1,2 @@ +export * from './actions' +export * from './steps' diff --git a/app/src/redux/protocol-runs/constants/lpc/steps.ts b/app/src/redux/protocol-runs/constants/lpc/steps.ts new file mode 100644 index 00000000000..e2ce6f337e1 --- /dev/null +++ b/app/src/redux/protocol-runs/constants/lpc/steps.ts @@ -0,0 +1,25 @@ +/** + * A step is associated with a view or multiple views that are a part of the + * core flow. They are driven by CTA and are not side effects of robot state. + * + * Advancing a step advances the step counter, going back a step lowers + * the step counter. If advancing/going back to a different view does not alter the step counter, + * then the view should either be associated with an existing step or should be independent of any step (ex, "robot in motion"). + * + */ +export const LPC_STEP = { + BEFORE_BEGINNING: 'BEFORE_BEGINNING', + ATTACH_PROBE: 'ATTACH_PROBE', + HANDLE_LABWARE: 'HANDLE_LABWARE', + DETACH_PROBE: 'DETACH_PROBE', + LPC_COMPLETE: 'LPC_COMPLETE', +} as const + +// All LPC steps, in order. +export const LPC_STEPS = [ + LPC_STEP.BEFORE_BEGINNING, + LPC_STEP.ATTACH_PROBE, + LPC_STEP.HANDLE_LABWARE, + LPC_STEP.DETACH_PROBE, + LPC_STEP.LPC_COMPLETE, +] diff --git a/app/src/redux/protocol-runs/reducer/index.ts b/app/src/redux/protocol-runs/reducer/index.ts index 48facfbe8d1..6626b30bc0b 100644 --- a/app/src/redux/protocol-runs/reducer/index.ts +++ b/app/src/redux/protocol-runs/reducer/index.ts @@ -32,6 +32,10 @@ export const protocolRunReducer: Reducer = ( case Constants.START_LPC: case Constants.FINISH_LPC: case Constants.PROCEED_STEP: + case Constants.GO_BACK_STEP: + case Constants.SET_SELECTED_LABWARE_NAME: + case Constants.SET_SELECTED_LABWARE: + case Constants.CLEAR_SELECTED_LABWARE: case Constants.SET_INITIAL_POSITION: case Constants.SET_FINAL_POSITION: { const runId = action.payload.runId diff --git a/app/src/redux/protocol-runs/reducer/lpc.ts b/app/src/redux/protocol-runs/reducer/lpc.ts index ca27fac6273..60c766b7295 100644 --- a/app/src/redux/protocol-runs/reducer/lpc.ts +++ b/app/src/redux/protocol-runs/reducer/lpc.ts @@ -1,15 +1,25 @@ import { PROCEED_STEP, + SET_SELECTED_LABWARE, SET_INITIAL_POSITION, SET_FINAL_POSITION, FINISH_LPC, START_LPC, + GO_BACK_STEP, + SET_SELECTED_LABWARE_NAME, + CLEAR_SELECTED_LABWARE, + APPLY_OFFSET, } from '../constants' -import { updateWorkingOffset } from './transforms' +import { updateOffsetsForURI } from './transforms' -import type { LPCWizardAction, LPCWizardState } from '../types' +import type { + LPCWizardAction, + LPCWizardState, + SelectedLabwareInfo, +} from '../types' // TODO(jh, 01-17-25): A lot of this state should live above the LPC slice, in the general protocolRuns slice instead. +// We should make selectors for that state, too! export function LPCReducer( state: LPCWizardState | undefined, action: LPCWizardAction @@ -27,28 +37,101 @@ export function LPCReducer( ? 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, + }, + } + } + + case GO_BACK_STEP: { + const { currentStepIndex } = state.steps + const newStepIdx = currentStepIndex > 0 ? currentStepIndex - 1 : 0 return { ...state, steps: { ...state.steps, currentStepIndex: newStepIdx, - current: state.steps.all[newStepIdx], - next: nextStep, + }, + } + } + + case SET_SELECTED_LABWARE_NAME: { + const lwUri = action.payload.labwareUri + const thisLwInfo = state.labwareInfo.labware[lwUri] + + const selectedLabware: SelectedLabwareInfo = { + uri: action.payload.labwareUri, + id: thisLwInfo.id, + offsetLocationDetails: null, + } + + return { + ...state, + labwareInfo: { + ...state.labwareInfo, + selectedLabware, + }, + } + } + + case SET_SELECTED_LABWARE: { + const lwUri = action.payload.labwareUri + const thisLwInfo = state.labwareInfo.labware[lwUri] + + const selectedLabware: SelectedLabwareInfo = { + uri: action.payload.labwareUri, + id: thisLwInfo.id, + offsetLocationDetails: action.payload.location, + } + + return { + ...state, + labwareInfo: { + ...state.labwareInfo, + selectedLabware, + }, + } + } + + case CLEAR_SELECTED_LABWARE: { + return { + ...state, + labwareInfo: { + ...state.labwareInfo, + selectedLabware: null, }, } } case SET_INITIAL_POSITION: - case SET_FINAL_POSITION: + case SET_FINAL_POSITION: { + const lwUri = action.payload.labwareUri + return { ...state, - workingOffsets: updateWorkingOffset(state.workingOffsets, action), + labwareInfo: { + ...state.labwareInfo, + labware: { + ...state.labwareInfo.labware, + [lwUri]: { + ...state.labwareInfo.labware[lwUri], + offsetDetails: updateOffsetsForURI(state, action), + }, + }, + }, } + } + + case APPLY_OFFSET: { + // TODO(jh, 01-30-25): Update the existing offset in the store, and clear the + // the working offset state. This will break the legacy LPC "apply all offsets" + // functionality, so this must be implemented simultaneously with the API changes. + break + } case FINISH_LPC: return undefined diff --git a/app/src/redux/protocol-runs/reducer/transforms/lpc.ts b/app/src/redux/protocol-runs/reducer/transforms/lpc.ts index 3d08fadee62..7ca2eece148 100644 --- a/app/src/redux/protocol-runs/reducer/transforms/lpc.ts +++ b/app/src/redux/protocol-runs/reducer/transforms/lpc.ts @@ -1,47 +1,65 @@ +import type { + LPCWizardAction, + LPCWizardState, + OffsetDetails, +} from '../../types' import isEqual from 'lodash/isEqual' +import { + SET_FINAL_POSITION, + SET_INITIAL_POSITION, +} from '/app/redux/protocol-runs' -import type { LPCWizardAction, WorkingOffset } from '../../types' - -export function updateWorkingOffset( - workingOffsets: WorkingOffset[], +// Handle positional updates, only updating the working offset that matches the location specified in the action. +export function updateOffsetsForURI( + state: LPCWizardState, action: Extract< LPCWizardAction, { type: 'SET_INITIAL_POSITION' | 'SET_FINAL_POSITION' } > -): WorkingOffset[] { +): OffsetDetails[] { const { type, payload } = action - const { labwareId, location, position } = payload - const existingRecordIndex = workingOffsets.findIndex( - record => - record.labwareId === labwareId && isEqual(record.location, location) + const { labwareUri, position, location } = payload + const { offsetDetails } = state.labwareInfo.labware[labwareUri] + const relevantDetailsIdx = offsetDetails.findIndex(detail => + isEqual(location, detail.locationDetails) ) - if (existingRecordIndex < 0) { - return [ - ...workingOffsets, - { - labwareId, - location, - initialPosition: type === 'SET_INITIAL_POSITION' ? position : null, - finalPosition: type === 'SET_FINAL_POSITION' ? position : null, - }, - ] + if (relevantDetailsIdx < 0) { + console.warn(`No matching location found for ${labwareUri}`) + return offsetDetails } 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), + const relevantDetail = offsetDetails[relevantDetailsIdx] + const newOffsetDetails = [ + ...offsetDetails.slice(0, relevantDetailsIdx), + ...offsetDetails.slice(relevantDetailsIdx + 1), ] + + if (relevantDetail.workingOffset == null) { + const newWorkingDetail = { + initialPosition: type === SET_INITIAL_POSITION ? position : null, + finalPosition: type === SET_FINAL_POSITION ? position : null, + } + + return [ + ...newOffsetDetails, + { ...relevantDetail, workingOffset: newWorkingDetail }, + ] + } else { + const newWorkingDetail = + type === SET_INITIAL_POSITION + ? { + initialPosition: position, + finalPosition: null, + } + : { + ...relevantDetail.workingOffset, + finalPosition: position, + } + + return [ + ...newOffsetDetails, + { ...relevantDetail, workingOffset: newWorkingDetail }, + ] + } } } diff --git a/app/src/redux/protocol-runs/selectors/lpc/index.ts b/app/src/redux/protocol-runs/selectors/lpc/index.ts index 5bd4a518ac2..0b9e54e22e1 100644 --- a/app/src/redux/protocol-runs/selectors/lpc/index.ts +++ b/app/src/redux/protocol-runs/selectors/lpc/index.ts @@ -1,2 +1,3 @@ export * from './labware' export * from './pipettes' +export * from './steps' diff --git a/app/src/redux/protocol-runs/selectors/lpc/labware.ts b/app/src/redux/protocol-runs/selectors/lpc/labware.ts deleted file mode 100644 index fd831163cc2..00000000000 --- a/app/src/redux/protocol-runs/selectors/lpc/labware.ts +++ /dev/null @@ -1,254 +0,0 @@ -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, - LegacyLabwareOffsetLocation, -} 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: LegacyLabwareOffsetLocation - 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/redux/protocol-runs/selectors/lpc/labware/index.ts b/app/src/redux/protocol-runs/selectors/lpc/labware/index.ts new file mode 100644 index 00000000000..0cb1066ebb1 --- /dev/null +++ b/app/src/redux/protocol-runs/selectors/lpc/labware/index.ts @@ -0,0 +1,2 @@ +export * from './offsets' +export * from './info' diff --git a/app/src/redux/protocol-runs/selectors/lpc/labware/info.ts b/app/src/redux/protocol-runs/selectors/lpc/labware/info.ts new file mode 100644 index 00000000000..1dce07cc514 --- /dev/null +++ b/app/src/redux/protocol-runs/selectors/lpc/labware/info.ts @@ -0,0 +1,165 @@ +import { createSelector } from 'reselect' + +import { getIsTiprack, getLabwareDisplayName } from '@opentrons/shared-data' + +import { + getItemLabwareDef, + getSelectedLabwareOffsetDetails, + getSelectedLabwareDefFrom, +} from '../transforms' + +import type { Selector } from 'reselect' +import type { + LegacyLabwareOffsetLocation, + VectorOffset, +} from '@opentrons/api-client' +import type { State } from '/app/redux/types' +import type { Coordinates, LabwareDefinition2 } from '@opentrons/shared-data' +import type { + LPCFlowType, + LPCLabwareInfo, + SelectedLabwareInfo, +} from '/app/redux/protocol-runs' + +export const selectAllLabwareInfo = ( + runId: string +): Selector => + createSelector( + (state: State) => state.protocolRuns[runId]?.lpc?.labwareInfo.labware, + labware => labware ?? {} + ) + +export const selectSelectedLabwareInfo = ( + runId: string +): Selector => + createSelector( + (state: State) => + state.protocolRuns[runId]?.lpc?.labwareInfo.selectedLabware, + selectedLabware => selectedLabware ?? null + ) + +export const selectSelectedLwInitialPosition = ( + runId: string +): Selector => + createSelector( + (state: State) => getSelectedLabwareOffsetDetails(runId, state), + details => { + const workingOffset = details?.workingOffset + + if (workingOffset == null) { + return null + } else { + return workingOffset.initialPosition + } + } + ) + +export interface SelectOffsetsToApplyResult { + definitionUri: string + location: LegacyLabwareOffsetLocation + vector: Coordinates +} + +export const selectSelectedLabwareFlowType = ( + runId: string +): Selector => + createSelector( + (state: State) => + state.protocolRuns[runId]?.lpc?.labwareInfo.selectedLabware, + selectedLabware => { + if (selectedLabware?.offsetLocationDetails == null) { + return null + } else { + if (selectedLabware.offsetLocationDetails.kind === 'default') { + return 'default' + } else { + return 'location-specific' + } + } + } + ) + +export const selectSelectedLabwareDisplayName = ( + runId: string +): Selector => + createSelector( + (state: State) => state.protocolRuns[runId]?.lpc?.labwareInfo.labware, + (state: State) => + state.protocolRuns[runId]?.lpc?.labwareInfo.selectedLabware?.uri, + (lw, uri) => { + if (lw == null || uri == null) { + console.warn('Cannot access invalid labware') + return '' + } else { + return lw[uri].displayName + } + } + ) + +export const selectIsSelectedLwTipRack = ( + runId: string +): Selector => + createSelector( + (state: State) => getSelectedLabwareDefFrom(runId, state), + def => (def != null ? getIsTiprack(def) : false) + ) + +export const selectSelectedLwDisplayName = ( + runId: string +): Selector => + createSelector( + (state: State) => getSelectedLabwareDefFrom(runId, state), + def => (def != null ? getLabwareDisplayName(def) : '') + ) + +export const selectActiveAdapterDisplayName = ( + runId: string +): Selector => + createSelector( + (state: State) => + state.protocolRuns[runId]?.lpc?.labwareInfo.selectedLabware, + (state: State) => state?.protocolRuns[runId]?.lpc?.labwareDefs, + (state: State) => state?.protocolRuns[runId]?.lpc?.protocolData, + (selectedLabware, labwareDefs, analysis) => { + const adapterId = selectedLabware?.offsetLocationDetails?.adapterId + + if (selectedLabware == null || labwareDefs == null || analysis == null) { + console.warn('No selected labware or store not properly initialized.') + return '' + } + + return adapterId != null + ? getItemLabwareDef({ + labwareId: adapterId, + loadedLabware: analysis.labware, + labwareDefs, + })?.metadata.displayName ?? '' + : '' + } + ) + +export const selectSelectedLabwareDef = ( + runId: string +): Selector => + createSelector( + (state: State) => + state.protocolRuns[runId]?.lpc?.labwareInfo.selectedLabware, + (state: State) => state.protocolRuns[runId]?.lpc?.labwareDefs, + (state: State) => state.protocolRuns[runId]?.lpc?.protocolData.labware, + (selectedLabware, labwareDefs, loadedLabware) => { + if ( + selectedLabware == null || + labwareDefs == null || + loadedLabware == null + ) { + console.warn('No selected labware or store not properly initialized.') + return null + } else { + return getItemLabwareDef({ + labwareId: selectedLabware.id, + labwareDefs, + loadedLabware, + }) + } + } + ) diff --git a/app/src/redux/protocol-runs/selectors/lpc/labware/offsets.ts b/app/src/redux/protocol-runs/selectors/lpc/labware/offsets.ts new file mode 100644 index 00000000000..ad0f85f3c83 --- /dev/null +++ b/app/src/redux/protocol-runs/selectors/lpc/labware/offsets.ts @@ -0,0 +1,134 @@ +import { createSelector } from 'reselect' + +import { + getVectorDifference, + getVectorSum, + IDENTITY_VECTOR, +} from '@opentrons/shared-data' + +import { + getSelectedLabwareOffsetDetails, + getOffsetDetailsForAllLabware, +} from '../transforms' + +import type { Selector } from 'reselect' +import type { VectorOffset, LabwareOffset } from '@opentrons/api-client' +import type { State } from '/app/redux/types' +import type { + LabwareDetails, + OffsetDetails, + SelectOffsetsToApplyResult, +} from '/app/redux/protocol-runs' + +export const selectSelectedOffsetDetails = ( + runId: string +): Selector => + createSelector( + (state: State) => + state.protocolRuns[runId]?.lpc?.labwareInfo.selectedLabware?.uri, + (state: State) => state.protocolRuns[runId]?.lpc?.labwareInfo.labware, + (uri, lw) => { + if (uri == null || lw == null) { + console.warn('Failed to access labware details.') + return [] + } else { + return lw[uri].offsetDetails ?? [] + } + } + ) + +export const selectSelectedLwExistingOffset = ( + runId: string +): Selector => + createSelector( + (state: State) => getSelectedLabwareOffsetDetails(runId, state), + details => { + const existingVector = details?.existingOffset?.vector + + if (existingVector == null) { + console.warn('No existing offset vector found for active labware') + return IDENTITY_VECTOR + } else { + return existingVector ?? IDENTITY_VECTOR + } + } + ) + +export const selectOffsetsToApply = ( + runId: string +): Selector => + createSelector( + (state: State) => getOffsetDetailsForAllLabware(runId, state), + (state: State) => state.protocolRuns[runId]?.lpc?.protocolData, + (allDetails, protocolData): SelectOffsetsToApplyResult[] => { + if (protocolData == null) { + console.warn('LPC state not initalized before selector use.') + return [] + } + + return allDetails.flatMap( + ({ workingOffset, existingOffset, locationDetails }) => { + const definitionUri = locationDetails.definitionUri + const { initialPosition, finalPosition } = workingOffset ?? {} + + if ( + finalPosition == null || + initialPosition == null || + definitionUri == null || + existingOffset == null || + // The slotName is null when applying a default offset. This condition + // is effectively a stub to maintain compatability with the legacy HTTP API, + // and will be refactored soon. + locationDetails.slotName == null + ) { + console.error( + `Cannot generate offsets for labware with incomplete details. ID: ${locationDetails.labwareId}` + ) + return [] + } + + const existingOffsetVector = existingOffset.vector + const finalVector = getVectorSum( + existingOffsetVector, + getVectorDifference(finalPosition, initialPosition) + ) + return [ + { + definitionUri, + location: { ...locationDetails }, + vector: finalVector, + }, + ] + } + ) + } + ) + +// TODO(jh, 01-29-25): Revisit this once "View Offsets" is refactored out of LPC. +export const selectLabwareOffsetsForAllLw = ( + runId: string +): Selector => + createSelector( + (state: State) => state.protocolRuns[runId]?.lpc?.labwareInfo.labware, + (labware): LabwareOffset[] => { + if (labware == null) { + console.warn('Labware info not initialized in state') + return [] + } + + return Object.values(labware).flatMap((details: LabwareDetails) => + details.offsetDetails.map(offsetDetail => ({ + id: details.id, + createdAt: offsetDetail?.existingOffset?.createdAt ?? '', + definitionUri: offsetDetail.locationDetails.definitionUri, + location: { + slotName: + offsetDetail.locationDetails.slotName ?? 'DEFAULT_OFFSET_STUB', + moduleModel: offsetDetail.locationDetails.moduleModel, + definitionUri: offsetDetail.locationDetails.definitionUri, + }, + vector: offsetDetail?.existingOffset?.vector ?? IDENTITY_VECTOR, + })) + ) + } + ) diff --git a/app/src/redux/protocol-runs/selectors/lpc/pipettes.ts b/app/src/redux/protocol-runs/selectors/lpc/pipettes.ts index 1070e80946e..748081b18cb 100644 --- a/app/src/redux/protocol-runs/selectors/lpc/pipettes.ts +++ b/app/src/redux/protocol-runs/selectors/lpc/pipettes.ts @@ -1,39 +1,36 @@ +import { createSelector } from 'reselect' + import { getPipetteNameSpecs } from '@opentrons/shared-data' +import type { Selector } from 'reselect' 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, - 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) ?? null + runId: string +): Selector => + createSelector( + (state: State) => state.protocolRuns[runId]?.lpc?.activePipetteId, + (state: State) => state.protocolRuns[runId]?.lpc?.protocolData, + (activePipetteId, protocolData) => { + if (activePipetteId == null || protocolData == null) { + console.warn('LPC state not initalized before selector use.') + return null + } else { + return ( + protocolData?.pipettes.find( + pipette => pipette.id === activePipetteId + ) ?? null + ) + } + } ) -} export const selectActivePipetteChannelCount = ( - step: LabwarePositionCheckStep, - runId: string, - state: State -): PipetteChannels => { - const pipetteName = selectActivePipette(step, runId, state)?.pipetteName - - return pipetteName != null - ? getPipetteNameSpecs(pipetteName)?.channels ?? 1 - : 1 -} + runId: string +): Selector => + createSelector( + (state: State) => selectActivePipette(runId)(state)?.pipetteName, + pipetteName => + pipetteName != null ? getPipetteNameSpecs(pipetteName)?.channels ?? 1 : 1 + ) diff --git a/app/src/redux/protocol-runs/selectors/lpc/steps.ts b/app/src/redux/protocol-runs/selectors/lpc/steps.ts new file mode 100644 index 00000000000..a4d34160140 --- /dev/null +++ b/app/src/redux/protocol-runs/selectors/lpc/steps.ts @@ -0,0 +1,15 @@ +import { createSelector } from 'reselect' + +import { LPC_STEP } from '/app/redux/protocol-runs' + +import type { Selector } from 'reselect' +import type { State } from '../../../types' +import type { LPCStep } from '/app/redux/protocol-runs' + +export const selectCurrentStep = (runId: string): Selector => + createSelector( + (state: State) => state.protocolRuns[runId]?.lpc?.steps.currentStepIndex, + (state: State) => state.protocolRuns[runId]?.lpc?.steps.all, + (currentIdx, allSteps) => + allSteps?.[currentIdx ?? 0] ?? LPC_STEP.BEFORE_BEGINNING + ) diff --git a/app/src/redux/protocol-runs/selectors/lpc/transforms.ts b/app/src/redux/protocol-runs/selectors/lpc/transforms.ts index 780e5336133..9705fd82b17 100644 --- a/app/src/redux/protocol-runs/selectors/lpc/transforms.ts +++ b/app/src/redux/protocol-runs/selectors/lpc/transforms.ts @@ -1,9 +1,13 @@ +import isEqual from 'lodash/isEqual' + import { getLabwareDefURI } from '@opentrons/shared-data' import type { CompletedProtocolAnalysis, LabwareDefinition2, } from '@opentrons/shared-data' +import type { State } from '/app/redux/types' +import type { LabwareDetails, OffsetDetails } from '/app/redux/protocol-runs' interface GetLabwareDefsForLPCParams { labwareId: string @@ -11,11 +15,11 @@ interface GetLabwareDefsForLPCParams { labwareDefs: LabwareDefinition2[] } -export function getItemLabwareDef({ +export const getItemLabwareDef = ({ labwareId, loadedLabware, labwareDefs, -}: GetLabwareDefsForLPCParams): LabwareDefinition2 | null { +}: GetLabwareDefsForLPCParams): LabwareDefinition2 | null => { const labwareDefUri = loadedLabware.find(l => l.id === labwareId)?.definitionUri ?? null @@ -27,3 +31,53 @@ export function getItemLabwareDef({ labwareDefs.find(def => getLabwareDefURI(def) === labwareDefUri) ?? null ) } + +export const getSelectedLabwareOffsetDetails = ( + runId: string, + state: State +): OffsetDetails | null => { + const selectedLabware = + state.protocolRuns[runId]?.lpc?.labwareInfo.selectedLabware + const offsetDetails = + state.protocolRuns[runId]?.lpc?.labwareInfo.labware[ + selectedLabware?.uri ?? '' + ].offsetDetails + + return ( + offsetDetails?.find(offset => + isEqual(offset.locationDetails, selectedLabware?.offsetLocationDetails) + ) ?? null + ) +} + +export const getSelectedLabwareDefFrom = ( + runId: string, + state: State +): LabwareDefinition2 | null => { + const selectedLabware = + state.protocolRuns[runId]?.lpc?.labwareInfo.selectedLabware + const labwareDefs = state?.protocolRuns[runId]?.lpc?.labwareDefs + const analysis = state?.protocolRuns[runId]?.lpc?.protocolData + + if (selectedLabware == null || labwareDefs == null || analysis == null) { + console.warn('No selected labware or store not properly initialized.') + return null + } else { + return getItemLabwareDef({ + labwareId: selectedLabware.id, + labwareDefs, + loadedLabware: analysis.labware, + }) + } +} + +export const getOffsetDetailsForAllLabware = ( + runId: string, + state: State +): OffsetDetails[] => { + const labware = state?.protocolRuns[runId]?.lpc?.labwareInfo.labware ?? {} + + return Object(labware).values( + (details: LabwareDetails) => details.offsetDetails + ) +} diff --git a/app/src/redux/protocol-runs/types/lpc.ts b/app/src/redux/protocol-runs/types/lpc.ts index 0e31a166bf1..843ae3e8aaf 100644 --- a/app/src/redux/protocol-runs/types/lpc.ts +++ b/app/src/redux/protocol-runs/types/lpc.ts @@ -1,38 +1,100 @@ import type { DeckConfiguration, + ModuleModel, LabwareDefinition2, CompletedProtocolAnalysis, } from '@opentrons/shared-data' -import type { - LegacyLabwareOffsetLocation, - VectorOffset, - LabwareOffset, -} from '@opentrons/api-client' +import type { VectorOffset } from '@opentrons/api-client' +import type { LPC_STEP } from '/app/redux/protocol-runs' -// 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' +type LabwareURI = string +type LabwareId = string -export interface PositionParams { - labwareId: string - location: LegacyLabwareOffsetLocation - position: VectorOffset | null +export type LPCStep = keyof typeof LPC_STEP + +export type LPCFlowType = 'default' | 'location-specific' +export type LPCOffsetKind = 'default' | 'location-specific' | 'hardcoded' + +export interface StepInfo { + currentStepIndex: number + totalStepCount: number + all: LPCStep[] +} + +export interface ExistingOffset { + createdAt: string + vector: VectorOffset } export interface WorkingOffset { - labwareId: string - location: LegacyLabwareOffsetLocation initialPosition: VectorOffset | null finalPosition: VectorOffset | null } +export interface PositionParams { + labwareUri: string + location: OffsetLocationDetails + position: VectorOffset | null +} + +interface LPCLabwareOffsetDetails { + kind: LPCOffsetKind + labwareId: string + definitionUri: string + moduleModel?: ModuleModel + moduleId?: string + adapterId?: string +} + +export interface LPCLabwareOffsetDefaultDetails + extends LPCLabwareOffsetDetails { + slotName: null + kind: 'default' +} + +export interface LPCLabwareOffsetAppliedLocationDetails + extends LPCLabwareOffsetDetails { + slotName: string + kind: 'location-specific' +} + +export interface OffsetDetails { + existingOffset: ExistingOffset | null + workingOffset: WorkingOffset | null + locationDetails: OffsetLocationDetails +} + +export interface LabwareDetails { + id: LabwareId + displayName: string + offsetDetails: OffsetDetails[] +} + +export type OffsetLocationDetails = + | LPCLabwareOffsetDefaultDetails + | LPCLabwareOffsetAppliedLocationDetails + +export interface SelectedLabwareInfo { + uri: LabwareURI + id: LabwareId + /* Indicates the type of LPC offset flow the user is performing, a "default" flow, a "location-specific" flow, or no active flow. + * There is no `slotName` when a user performs the default offset flow. + * Until the user is in a default or location-specific offset flow, there are no location details. */ + offsetLocationDetails: OffsetLocationDetails | null +} + +export interface LPCLabwareInfo { + selectedLabware: SelectedLabwareInfo | null + labware: Record +} + export interface LPCWizardState { - workingOffsets: WorkingOffset[] + steps: StepInfo + activePipetteId: string + labwareInfo: LPCLabwareInfo protocolData: CompletedProtocolAnalysis labwareDefs: LabwareDefinition2[] deckConfig: DeckConfiguration - steps: StepsInfo - existingOffsets: LabwareOffset[] protocolName: string maintenanceRunId: string } @@ -52,6 +114,33 @@ export interface ProceedStepAction { payload: { runId: string } } +export interface GoBackStepAction { + type: 'GO_BACK_STEP' + payload: { runId: string } +} + +export interface SelectedLabwareNameAction { + type: 'SET_SELECTED_LABWARE_NAME' + payload: { + runId: string + labwareUri: LabwareURI + } +} + +export interface SelectedLabwareAction { + type: 'SET_SELECTED_LABWARE' + payload: { + runId: string + labwareUri: LabwareURI + location: OffsetLocationDetails | null + } +} + +export interface ClearSelectedLabwareAction { + type: 'CLEAR_SELECTED_LABWARE' + payload: { runId: string } +} + export interface InitialPositionAction { type: 'SET_INITIAL_POSITION' payload: PositionParams & { runId: string } @@ -62,9 +151,19 @@ export interface FinalPositionAction { payload: PositionParams & { runId: string } } +export interface ApplyOffsetAction { + type: 'APPLY_OFFSET' + payload: { runId: string; labwareUri: LabwareURI } +} + export type LPCWizardAction = | StartLPCAction | FinishLPCAction + | SelectedLabwareNameAction + | SelectedLabwareAction + | ClearSelectedLabwareAction | InitialPositionAction | FinalPositionAction + | ApplyOffsetAction | ProceedStepAction + | GoBackStepAction