From 89834d5e1b5739ca454574aeb44211a43be1ba0a Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Mon, 2 Dec 2024 14:34:07 -0500 Subject: [PATCH 1/9] feat(app): implement stall recoveries (#17002) Adds recovery from stall or collision errors to the app. This should implement these flows: https://www.figma.com/design/OGssKRmCOvuXSqUpK2qXrV/Feature%3A-Error-Recovery-November-Release?node-id=9765-66609&t=65NkMVGZlPdCG7z6-4 When there's a stall, which can happen on pretty much any command, we should now prompt the user to home and retry. To home, they have to make sure the machine is safe, so we will go through DTWiz. ## Reviews - [ ] did i miss anything ## Testing - [x] stalls should get you the home and retry button - [x] you should enter the DTWiz if >0 pipettes have a tip - [x] if 0 pipettes have a tip the DTwiz should be skipped (or if there are no pipettes) - [x] Retrying should in fact work Closes EXEC-725 --- api/src/opentrons/hardware_control/api.py | 10 ++ api/src/opentrons/hardware_control/ot3api.py | 7 +- .../protocols/hardware_manager.py | 6 +- .../commands/unsafe/unsafe_engage_axes.py | 5 +- .../unsafe/update_position_estimators.py | 5 +- .../protocol_engine/execution/gantry_mover.py | 26 +++ .../commands/unsafe/test_engage_axes.py | 33 ++-- .../unsafe/test_update_position_estimators.py | 32 ++-- .../localization/en/error_recovery.json | 10 ++ .../ErrorRecoveryWizard.tsx | 7 + .../RecoveryOptions/CancelRun.tsx | 4 +- .../RecoveryOptions/FillWellAndSkip.tsx | 4 +- .../RecoveryOptions/HomeAndRetry.tsx | 147 +++++++++++++++++ .../RecoveryOptions/IgnoreErrorSkipStep.tsx | 4 +- .../RecoveryOptions/ManageTips.tsx | 55 ++++++- .../RecoveryOptions/ManualMoveLwAndSkip.tsx | 4 +- .../ManualReplaceLwAndRetry.tsx | 4 +- .../RecoveryOptions/RetryNewTips.tsx | 4 +- .../RecoveryOptions/RetrySameTips.tsx | 4 +- .../RecoveryOptions/RetryStep.tsx | 4 +- .../RecoveryOptions/SelectRecoveryOption.tsx | 7 + .../RecoveryOptions/SkipStepNewTips.tsx | 4 +- .../RecoveryOptions/SkipStepSameTips.tsx | 4 +- .../__tests__/HomeAndRetry.test.tsx | 154 ++++++++++++++++++ .../__tests__/SelectRecoveryOptions.test.tsx | 41 +++++ .../RecoveryOptions/index.ts | 1 + .../__tests__/ErrorRecoveryWizard.test.tsx | 16 ++ .../organisms/ErrorRecoveryFlows/constants.ts | 33 ++++ .../__tests__/useRecoveryOptionCopy.test.tsx | 5 + .../ErrorRecoveryFlows/hooks/useErrorName.ts | 2 + .../hooks/useFailedLabwareUtils.ts | 3 +- .../hooks/useRecoveryCommands.ts | 12 ++ .../hooks/useRecoveryOptionCopy.tsx | 2 + .../hooks/useRecoveryToasts.ts | 1 + .../shared/ErrorDetailsModal.tsx | 15 ++ .../shared/RecoveryDoorOpenSpecial.tsx | 17 ++ .../shared/RetryStepInfo.tsx | 4 +- .../shared/TwoColLwInfoAndDeck.tsx | 9 +- .../__tests__/ErrorDetailsModal.test.tsx | 23 +++ .../utils/__tests__/getErrorKind.test.ts | 15 ++ .../ErrorRecoveryFlows/utils/getErrorKind.ts | 2 + 41 files changed, 689 insertions(+), 56 deletions(-) create mode 100644 app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/HomeAndRetry.tsx create mode 100644 app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/HomeAndRetry.test.tsx diff --git a/api/src/opentrons/hardware_control/api.py b/api/src/opentrons/hardware_control/api.py index 2e2bbfef116..c52fae64131 100644 --- a/api/src/opentrons/hardware_control/api.py +++ b/api/src/opentrons/hardware_control/api.py @@ -921,6 +921,16 @@ def engaged_axes(self) -> Dict[Axis, bool]: async def disengage_axes(self, which: List[Axis]) -> None: await self._backend.disengage_axes([ot2_axis_to_string(ax) for ax in which]) + def axis_is_present(self, axis: Axis) -> bool: + is_ot2 = axis in Axis.ot2_axes() + if not is_ot2: + return False + if axis in Axis.pipette_axes(): + mount = Axis.to_ot2_mount(axis) + if self.attached_pipettes.get(mount) is None: + return False + return True + @ExecutionManagerProvider.wait_for_running async def _fast_home(self, axes: Sequence[str], margin: float) -> Dict[str, float]: converted_axes = "".join(axes) diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 1b3275f4c7e..6c4b4f291bc 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -1674,7 +1674,12 @@ async def disengage_axes(self, which: List[Axis]) -> None: await self._backend.disengage_axes(which) async def engage_axes(self, which: List[Axis]) -> None: - await self._backend.engage_axes(which) + await self._backend.engage_axes( + [axis for axis in which if self._backend.axis_is_present(axis)] + ) + + def axis_is_present(self, axis: Axis) -> bool: + return self._backend.axis_is_present(axis) async def get_limit_switches(self) -> Dict[Axis, bool]: res = await self._backend.get_limit_switches() diff --git a/api/src/opentrons/hardware_control/protocols/hardware_manager.py b/api/src/opentrons/hardware_control/protocols/hardware_manager.py index ee0228ae3b8..d2bfd94a06b 100644 --- a/api/src/opentrons/hardware_control/protocols/hardware_manager.py +++ b/api/src/opentrons/hardware_control/protocols/hardware_manager.py @@ -1,7 +1,7 @@ from typing import Dict, Optional from typing_extensions import Protocol -from ..types import SubSystem, SubSystemState +from ..types import SubSystem, SubSystemState, Axis class HardwareManager(Protocol): @@ -45,3 +45,7 @@ def attached_subsystems(self) -> Dict[SubSystem, SubSystemState]: async def get_serial_number(self) -> Optional[str]: """Get the robot serial number, if provisioned. If not provisioned, will be None.""" ... + + def axis_is_present(self, axis: Axis) -> bool: + """Get whether a motor axis is present on the machine.""" + ... diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py index 02bc22b0396..4f80db24f42 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py @@ -52,10 +52,7 @@ async def execute( """Enable exes.""" ot3_hardware_api = ensure_ot3_hardware(self._hardware_api) await ot3_hardware_api.engage_axes( - [ - self._gantry_mover.motor_axis_to_hardware_axis(axis) - for axis in params.axes - ] + self._gantry_mover.motor_axes_to_present_hardware_axes(params.axes) ) return SuccessData( public=UnsafeEngageAxesResult(), diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/update_position_estimators.py b/api/src/opentrons/protocol_engine/commands/unsafe/update_position_estimators.py index ff06b6c22ed..6b050d6472f 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/update_position_estimators.py @@ -58,10 +58,7 @@ async def execute( """Update axis position estimators from their encoders.""" ot3_hardware_api = ensure_ot3_hardware(self._hardware_api) await ot3_hardware_api.update_axis_position_estimations( - [ - self._gantry_mover.motor_axis_to_hardware_axis(axis) - for axis in params.axes - ] + self._gantry_mover.motor_axes_to_present_hardware_axes(params.axes) ) return SuccessData( public=UpdatePositionEstimatorsResult(), diff --git a/api/src/opentrons/protocol_engine/execution/gantry_mover.py b/api/src/opentrons/protocol_engine/execution/gantry_mover.py index c77a9e1bad2..5413de8741c 100644 --- a/api/src/opentrons/protocol_engine/execution/gantry_mover.py +++ b/api/src/opentrons/protocol_engine/execution/gantry_mover.py @@ -159,6 +159,12 @@ def pick_mount_from_axis_map(self, axis_map: Dict[MotorAxis, float]) -> Mount: """Find a mount axis in the axis_map if it exists otherwise default to left mount.""" ... + def motor_axes_to_present_hardware_axes( + self, motor_axes: List[MotorAxis] + ) -> List[HardwareAxis]: + """Transform a list of engine axes into a list of hardware axes, filtering out non-present axes.""" + ... + class HardwareGantryMover(GantryMover): """Hardware API based gantry movement handler.""" @@ -167,6 +173,18 @@ def __init__(self, hardware_api: HardwareControlAPI, state_view: StateView) -> N self._hardware_api = hardware_api self._state_view = state_view + def motor_axes_to_present_hardware_axes( + self, motor_axes: List[MotorAxis] + ) -> List[HardwareAxis]: + """Get hardware axes from engine axes while filtering out non-present axes.""" + return [ + self.motor_axis_to_hardware_axis(motor_axis) + for motor_axis in motor_axes + if self._hardware_api.axis_is_present( + self.motor_axis_to_hardware_axis(motor_axis) + ) + ] + def motor_axis_to_hardware_axis(self, motor_axis: MotorAxis) -> HardwareAxis: """Transform an engine motor axis into a hardware axis.""" return _MOTOR_AXIS_TO_HARDWARE_AXIS[motor_axis] @@ -643,6 +661,14 @@ async def prepare_for_mount_movement(self, mount: Mount) -> None: """Retract the 'idle' mount if necessary.""" pass + def motor_axes_to_present_hardware_axes( + self, motor_axes: List[MotorAxis] + ) -> List[HardwareAxis]: + """Get present hardware axes from a list of engine axes. In simulation, all axes are present.""" + return [ + self.motor_axis_to_hardware_axis(motor_axis) for motor_axis in motor_axes + ] + def create_gantry_mover( state_view: StateView, hardware_api: HardwareControlAPI diff --git a/api/tests/opentrons/protocol_engine/commands/unsafe/test_engage_axes.py b/api/tests/opentrons/protocol_engine/commands/unsafe/test_engage_axes.py index 72fb761ad23..1f40523e4e1 100644 --- a/api/tests/opentrons/protocol_engine/commands/unsafe/test_engage_axes.py +++ b/api/tests/opentrons/protocol_engine/commands/unsafe/test_engage_axes.py @@ -22,21 +22,28 @@ async def test_engage_axes_implementation( ) data = UnsafeEngageAxesParams( - axes=[MotorAxis.LEFT_Z, MotorAxis.LEFT_PLUNGER, MotorAxis.X, MotorAxis.Y] - ) - - decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.LEFT_Z)).then_return( - Axis.Z_L + axes=[ + MotorAxis.LEFT_Z, + MotorAxis.LEFT_PLUNGER, + MotorAxis.X, + MotorAxis.Y, + MotorAxis.RIGHT_Z, + MotorAxis.RIGHT_PLUNGER, + ] ) decoy.when( - gantry_mover.motor_axis_to_hardware_axis(MotorAxis.LEFT_PLUNGER) - ).then_return(Axis.P_L) - decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.X)).then_return( - Axis.X - ) - decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.Y)).then_return( - Axis.Y - ) + gantry_mover.motor_axes_to_present_hardware_axes( + [ + MotorAxis.LEFT_Z, + MotorAxis.LEFT_PLUNGER, + MotorAxis.X, + MotorAxis.Y, + MotorAxis.RIGHT_Z, + MotorAxis.RIGHT_PLUNGER, + ] + ) + ).then_return([Axis.Z_L, Axis.P_L, Axis.X, Axis.Y]) + decoy.when( await ot3_hardware_api.update_axis_position_estimations( [Axis.Z_L, Axis.P_L, Axis.X, Axis.Y] diff --git a/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py b/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py index da381635ce3..e281502308c 100644 --- a/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py +++ b/api/tests/opentrons/protocol_engine/commands/unsafe/test_update_position_estimators.py @@ -22,21 +22,27 @@ async def test_update_position_estimators_implementation( ) data = UpdatePositionEstimatorsParams( - axes=[MotorAxis.LEFT_Z, MotorAxis.LEFT_PLUNGER, MotorAxis.X, MotorAxis.Y] - ) - - decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.LEFT_Z)).then_return( - Axis.Z_L + axes=[ + MotorAxis.LEFT_Z, + MotorAxis.LEFT_PLUNGER, + MotorAxis.X, + MotorAxis.Y, + MotorAxis.RIGHT_Z, + MotorAxis.RIGHT_PLUNGER, + ] ) decoy.when( - gantry_mover.motor_axis_to_hardware_axis(MotorAxis.LEFT_PLUNGER) - ).then_return(Axis.P_L) - decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.X)).then_return( - Axis.X - ) - decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.Y)).then_return( - Axis.Y - ) + gantry_mover.motor_axes_to_present_hardware_axes( + [ + MotorAxis.LEFT_Z, + MotorAxis.LEFT_PLUNGER, + MotorAxis.X, + MotorAxis.Y, + MotorAxis.RIGHT_Z, + MotorAxis.RIGHT_PLUNGER, + ] + ) + ).then_return([Axis.Z_L, Axis.P_L, Axis.X, Axis.Y]) result = await subject.execute(data) diff --git a/app/src/assets/localization/en/error_recovery.json b/app/src/assets/localization/en/error_recovery.json index b46e276c48b..e4e1b5164eb 100644 --- a/app/src/assets/localization/en/error_recovery.json +++ b/app/src/assets/localization/en/error_recovery.json @@ -8,6 +8,7 @@ "blowout_failed": "Blowout failed", "cancel_run": "Cancel run", "canceling_run": "Canceling run", + "carefully_move_labware": "Carefully move any misplaced labware and clean up any spilled liquid.Close the robot door before proceeding.", "change_location": "Change location", "change_tip_pickup_location": "Change tip pick-up location", "choose_a_recovery_action": "Choose a recovery action", @@ -32,6 +33,9 @@ "gripper_errors_occur_when": "Gripper errors occur when the gripper stalls or collides with another object on the deck and are usually caused by improperly placed labware or inaccurate labware offsets", "gripper_releasing_labware": "Gripper releasing labware", "gripper_will_release_in_s": "Gripper will release labware in {{seconds}} seconds", + "home_and_retry": "Home gantry and retry step", + "home_gantry": "Home gantry", + "home_now": "Home now", "homing_pipette_dangerous": "Homing the {{mount}} pipette with liquid in the tips may damage it. You must remove all tips before using the pipette again.", "if_issue_persists_gripper_error": " If the issue persists, cancel the run and rerun gripper calibration", "if_issue_persists_overpressure": " If the issue persists, cancel the run and make the necessary changes to the protocol", @@ -57,7 +61,9 @@ "overpressure_is_usually_caused": "Overpressure is usually caused by a tip contacting labware, a clog, or moving viscous liquid too quickly", "pick_up_tips": "Pick up tips", "pipette_overpressure": "Pipette overpressure", + "prepare_deck_for_homing": "Prepare deck for homing", "proceed_to_cancel": "Proceed to cancel", + "proceed_to_home": "Proceed to home", "proceed_to_tip_selection": "Proceed to tip selection", "recovery_action_failed": "{{action}} failed", "recovery_mode": "Recovery mode", @@ -96,6 +102,8 @@ "skip_to_next_step_same_tips": "Skip to next step with same tips", "skipping_to_step_succeeded": "Skipping to step {{step}} succeeded.", "skipping_to_step_succeeded_na": "Skipping to next step succeeded.", + "stall_or_collision_detected_when": "A stall or collision is detected when the robot's motors are blocked", + "stall_or_collision_error": "Stall or collision", "stand_back": "Stand back, robot is in motion", "stand_back_picking_up_tips": "Stand back, picking up tips", "stand_back_resuming": "Stand back, resuming current step", @@ -105,7 +113,9 @@ "take_necessary_actions": "First, take any necessary actions to prepare the robot to retry the failed step.Then, close the robot door before proceeding.", "take_necessary_actions_failed_pickup": "First, take any necessary actions to prepare the robot to retry the failed tip pickup.Then, close the robot door before proceeding.", "take_necessary_actions_failed_tip_drop": "First, take any necessary actions to prepare the robot to retry the failed tip drop.Then, close the robot door before proceeding.", + "take_necessary_actions_home": "Take any necessary actions to prepare the robot to move the gantry to its home position.Close the robot door before proceeding.", "terminate_remote_activity": "Terminate remote activity", + "the_robot_must_return_to_home_position": "The robot must return to its home position before proceeding", "tip_drop_failed": "Tip drop failed", "tip_not_detected": "Tip not detected", "tip_presence_errors_are_caused": "Tip presence errors are usually caused by improperly placed labware or inaccurate labware offsets", diff --git a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx index 2c6f047f80d..bd52195faf8 100644 --- a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx @@ -19,6 +19,7 @@ import { IgnoreErrorSkipStep, ManualMoveLwAndSkip, ManualReplaceLwAndRetry, + HomeAndRetry, } from './RecoveryOptions' import { useErrorDetailsModal, @@ -225,6 +226,10 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element { return } + const buildHomeAndRetry = (): JSX.Element => { + return + } + switch (props.recoveryMap.route) { case RECOVERY_MAP.OPTION_SELECTION.ROUTE: return buildSelectRecoveryOption() @@ -264,6 +269,8 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element { return buildRecoveryInProgress() case RECOVERY_MAP.ROBOT_DOOR_OPEN.ROUTE: return buildManuallyRouteToDoorOpen() + case RECOVERY_MAP.HOME_AND_RETRY.ROUTE: + return buildHomeAndRetry() default: return buildSelectRecoveryOption() } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx index b3cdd5fe257..fa66d614011 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/CancelRun.tsx @@ -35,7 +35,9 @@ export function CancelRun(props: RecoveryContentProps): JSX.Element { case CANCEL_RUN.STEPS.CONFIRM_CANCEL: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `CancelRun: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx index dc74ed7e529..d01ea7dfe4e 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx @@ -34,7 +34,9 @@ export function FillWellAndSkip(props: RecoveryContentProps): JSX.Element { case CANCEL_RUN.STEPS.CONFIRM_CANCEL: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `FillWellAndSkip: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/HomeAndRetry.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/HomeAndRetry.tsx new file mode 100644 index 00000000000..00ebdfb35ee --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/HomeAndRetry.tsx @@ -0,0 +1,147 @@ +import { Trans, useTranslation } from 'react-i18next' +import { LegacyStyledText } from '@opentrons/components' +import { RECOVERY_MAP } from '../constants' +import { + TwoColTextAndFailedStepNextStep, + TwoColLwInfoAndDeck, + SelectTips, + RecoveryDoorOpenSpecial, + RetryStepInfo, +} from '../shared' +import { ManageTips } from './ManageTips' +import { SelectRecoveryOption } from './SelectRecoveryOption' + +import type { RecoveryContentProps } from '../types' + +const { HOME_AND_RETRY } = RECOVERY_MAP +export function HomeAndRetry(props: RecoveryContentProps): JSX.Element { + const { recoveryMap } = props + const { route, step } = recoveryMap + switch (step) { + case HOME_AND_RETRY.STEPS.PREPARE_DECK_FOR_HOME: { + return + } + case HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE: { + // TODO: Make this work the same way as e.g. RetryNewTips by changing one of them. Or both of them. + return + } + case HOME_AND_RETRY.STEPS.REPLACE_TIPS: { + return + } + case HOME_AND_RETRY.STEPS.SELECT_TIPS: { + return + } + case HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY: { + return + } + case HOME_AND_RETRY.STEPS.CLOSE_DOOR_AND_HOME: { + return + } + case HOME_AND_RETRY.STEPS.CONFIRM_RETRY: { + return + } + default: + console.warn( + `HomeAndRetry: ${step} in ${route} not explicitly handled. Rerouting.}` + ) + return + } +} + +export function RetryAfterHome(props: RecoveryContentProps): JSX.Element { + const { recoveryMap, routeUpdateActions } = props + const { step, route } = recoveryMap + const { HOME_AND_RETRY } = RECOVERY_MAP + const { proceedToRouteAndStep } = routeUpdateActions + + const buildContent = (): JSX.Element => { + switch (step) { + case HOME_AND_RETRY.STEPS.CONFIRM_RETRY: + return ( + + proceedToRouteAndStep( + HOME_AND_RETRY.ROUTE, + HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY + ) + } + /> + ) + default: + console.warn( + `RetryStep: ${step} in ${route} not explicitly handled. Rerouting.` + ) + return + } + } + return buildContent() +} + +export function PrepareDeckForHome(props: RecoveryContentProps): JSX.Element { + const { t } = useTranslation('error_recovery') + const { routeUpdateActions, tipStatusUtils } = props + const { proceedToRouteAndStep } = routeUpdateActions + const primaryBtnOnClick = (): Promise => + proceedToRouteAndStep( + RECOVERY_MAP.HOME_AND_RETRY.ROUTE, + tipStatusUtils.areTipsAttached + ? RECOVERY_MAP.HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE + : RECOVERY_MAP.HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY + ) + const buildBodyText = (): JSX.Element => ( + }} + /> + ) + return ( + + ) +} + +export function HomeGantryBeforeRetry( + props: RecoveryContentProps +): JSX.Element { + const { t } = useTranslation('error_recovery') + const { routeUpdateActions, tipStatusUtils } = props + const { proceedToRouteAndStep } = routeUpdateActions + const { HOME_AND_RETRY } = RECOVERY_MAP + const buildBodyText = (): JSX.Element => ( + }} + /> + ) + const secondaryBtnOnClick = (): Promise => + proceedToRouteAndStep( + RECOVERY_MAP.HOME_AND_RETRY.ROUTE, + tipStatusUtils.areTipsAttached + ? RECOVERY_MAP.HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE + : RECOVERY_MAP.HOME_AND_RETRY.STEPS.PREPARE_DECK_FOR_HOME + ) + + const primaryBtnOnClick = (): Promise => + proceedToRouteAndStep( + HOME_AND_RETRY.ROUTE, + HOME_AND_RETRY.STEPS.CLOSE_DOOR_AND_HOME + ) + return ( + + ) +} diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx index c17e947853b..16cb755d4da 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx @@ -41,7 +41,9 @@ export function IgnoreErrorSkipStep(props: RecoveryContentProps): JSX.Element { case IGNORE_AND_SKIP.STEPS.SKIP_STEP: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `IgnoreErrorAndSkipStep: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx index 1609acfa0ca..9061ba9b638 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManageTips.tsx @@ -34,7 +34,7 @@ export function ManageTips(props: RecoveryContentProps): JSX.Element { routeAlternativelyIfNoPipette(props) const buildContent = (): JSX.Element => { - const { DROP_TIP_FLOWS } = RECOVERY_MAP + const { DROP_TIP_FLOWS, HOME_AND_RETRY } = RECOVERY_MAP const { step, route } = recoveryMap switch (step) { @@ -44,8 +44,12 @@ export function ManageTips(props: RecoveryContentProps): JSX.Element { case DROP_TIP_FLOWS.STEPS.CHOOSE_BLOWOUT: case DROP_TIP_FLOWS.STEPS.CHOOSE_TIP_DROP: return + case HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE: + return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `ManageTips: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } @@ -68,11 +72,23 @@ export function BeginRemoval({ } = routeUpdateActions const { cancelRun } = recoveryCommands const { selectedRecoveryOption } = currentRecoveryOptionUtils - const { ROBOT_CANCELING, RETRY_NEW_TIPS } = RECOVERY_MAP + const { + ROBOT_CANCELING, + RETRY_NEW_TIPS, + HOME_AND_RETRY, + DROP_TIP_FLOWS, + } = RECOVERY_MAP const mount = aPipetteWithTip?.mount const primaryOnClick = (): void => { - void proceedNextStep() + if (selectedRecoveryOption === HOME_AND_RETRY.ROUTE) { + void proceedToRouteAndStep( + DROP_TIP_FLOWS.ROUTE, + DROP_TIP_FLOWS.STEPS.BEFORE_BEGINNING + ) + } else { + void proceedNextStep() + } } const secondaryOnClick = (): void => { @@ -81,6 +97,11 @@ export function BeginRemoval({ RETRY_NEW_TIPS.ROUTE, RETRY_NEW_TIPS.STEPS.REPLACE_TIPS ) + } else if (selectedRecoveryOption === HOME_AND_RETRY.ROUTE) { + void proceedToRouteAndStep( + HOME_AND_RETRY.ROUTE, + HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY + ) } else { void handleMotionRouting(true, ROBOT_CANCELING.ROUTE).then(() => { cancelRun() @@ -149,7 +170,12 @@ function DropTipFlowsContainer( recoveryCommands, currentRecoveryOptionUtils, } = props - const { DROP_TIP_FLOWS, ROBOT_CANCELING, RETRY_NEW_TIPS } = RECOVERY_MAP + const { + DROP_TIP_FLOWS, + ROBOT_CANCELING, + RETRY_NEW_TIPS, + HOME_AND_RETRY, + } = RECOVERY_MAP const { proceedToRouteAndStep, handleMotionRouting } = routeUpdateActions const { selectedRecoveryOption } = currentRecoveryOptionUtils const { setTipStatusResolved } = tipStatusUtils @@ -163,6 +189,11 @@ function DropTipFlowsContainer( RETRY_NEW_TIPS.ROUTE, RETRY_NEW_TIPS.STEPS.REPLACE_TIPS ) + } else if (selectedRecoveryOption === HOME_AND_RETRY.ROUTE) { + void proceedToRouteAndStep( + HOME_AND_RETRY.ROUTE, + HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY + ) } else { void setTipStatusResolved(onEmptyCache, onTipsDetected) } @@ -208,6 +239,7 @@ export function useDropTipFlowUtils({ SKIP_STEP_WITH_NEW_TIPS, ERROR_WHILE_RECOVERING, DROP_TIP_FLOWS, + HOME_AND_RETRY, } = RECOVERY_MAP const { runId, gripperErrorFirstPipetteWithTip } = tipStatusUtils const { step } = recoveryMap @@ -220,6 +252,7 @@ export function useDropTipFlowUtils({ switch (selectedRecoveryOption) { case RETRY_NEW_TIPS.ROUTE: case SKIP_STEP_WITH_NEW_TIPS.ROUTE: + case HOME_AND_RETRY.ROUTE: return t('proceed_to_tip_selection') default: return t('proceed_to_cancel') @@ -243,6 +276,10 @@ export function useDropTipFlowUtils({ SKIP_STEP_WITH_NEW_TIPS.STEPS.REPLACE_TIPS ) } + case HOME_AND_RETRY.ROUTE: + return () => { + routeTo(selectedRecoveryOption, HOME_AND_RETRY.STEPS.REPLACE_TIPS) + } default: return null } @@ -336,6 +373,7 @@ function routeAlternativelyIfNoPipette(props: RecoveryContentProps): void { RETRY_NEW_TIPS, SKIP_STEP_WITH_NEW_TIPS, OPTION_SELECTION, + HOME_AND_RETRY, } = RECOVERY_MAP if (tipStatusUtils.aPipetteWithTip == null) @@ -354,6 +392,13 @@ function routeAlternativelyIfNoPipette(props: RecoveryContentProps): void { ) break } + case HOME_AND_RETRY.ROUTE: { + proceedToRouteAndStep( + selectedRecoveryOption, + HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY + ) + break + } default: { proceedToRouteAndStep(OPTION_SELECTION.ROUTE) } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManualMoveLwAndSkip.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManualMoveLwAndSkip.tsx index 123493480f7..5cf8ef81a65 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManualMoveLwAndSkip.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManualMoveLwAndSkip.tsx @@ -28,7 +28,9 @@ export function ManualMoveLwAndSkip(props: RecoveryContentProps): JSX.Element { case MANUAL_MOVE_AND_SKIP.STEPS.SKIP: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `ManualMoveLwAndSkipStep: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManualReplaceLwAndRetry.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManualReplaceLwAndRetry.tsx index 11ffe783d42..313d3d1f086 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManualReplaceLwAndRetry.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/ManualReplaceLwAndRetry.tsx @@ -30,7 +30,9 @@ export function ManualReplaceLwAndRetry( case MANUAL_REPLACE_AND_RETRY.STEPS.RETRY: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `ManualReplaceLwAndRetry: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryNewTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryNewTips.tsx index f6e86cd6923..003e776824d 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryNewTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryNewTips.tsx @@ -35,7 +35,9 @@ export function RetryNewTips(props: RecoveryContentProps): JSX.Element { case RETRY_NEW_TIPS.STEPS.RETRY: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `RetryNewTips: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetrySameTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetrySameTips.tsx index 93a0d84689d..0c28eb2a2da 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetrySameTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetrySameTips.tsx @@ -18,7 +18,9 @@ export function RetrySameTips(props: RecoveryContentProps): JSX.Element { case RETRY_SAME_TIPS.STEPS.RETRY: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `RetrySameTips: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryStep.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryStep.tsx index 9b1f4d2c85a..a30b68d4358 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryStep.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryStep.tsx @@ -14,7 +14,9 @@ export function RetryStep(props: RecoveryContentProps): JSX.Element { case RETRY_STEP.STEPS.CONFIRM_RETRY: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `RetryStep: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx index 59888c39c42..e271cc3be23 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx @@ -168,9 +168,16 @@ export function getRecoveryOptions(errorKind: ErrorKind): RecoveryRoute[] { return GRIPPER_ERROR_OPTIONS case ERROR_KINDS.GENERAL_ERROR: return GENERAL_ERROR_OPTIONS + case ERROR_KINDS.STALL_OR_COLLISION: + return STALL_OR_COLLISION_OPTIONS } } +export const STALL_OR_COLLISION_OPTIONS: RecoveryRoute[] = [ + RECOVERY_MAP.HOME_AND_RETRY.ROUTE, + RECOVERY_MAP.CANCEL_RUN.ROUTE, +] + export const NO_LIQUID_DETECTED_OPTIONS: RecoveryRoute[] = [ RECOVERY_MAP.MANUAL_FILL_AND_SKIP.ROUTE, RECOVERY_MAP.IGNORE_AND_SKIP.ROUTE, diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepNewTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepNewTips.tsx index 647bded71e1..b237afd82f0 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepNewTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepNewTips.tsx @@ -29,7 +29,9 @@ export function SkipStepNewTips( case SKIP_STEP_WITH_NEW_TIPS.STEPS.SKIP: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `SkipStepNewTips: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepSameTips.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepSameTips.tsx index 2b56012d5ab..9990d94171a 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepSameTips.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SkipStepSameTips.tsx @@ -14,7 +14,9 @@ export function SkipStepSameTips(props: RecoveryContentProps): JSX.Element { case SKIP_STEP_WITH_SAME_TIPS.STEPS.SKIP: return default: - console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) + console.warn( + `SkipStepSameTips: ${step} in ${route} not explicitly handled. Rerouting.` + ) return } } diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/HomeAndRetry.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/HomeAndRetry.test.tsx new file mode 100644 index 00000000000..3286041b7fb --- /dev/null +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/HomeAndRetry.test.tsx @@ -0,0 +1,154 @@ +import type * as React from 'react' +import { describe, it, vi, beforeEach, afterEach } from 'vitest' +import { screen } from '@testing-library/react' + +import { mockRecoveryContentProps } from '../../__fixtures__' +import { renderWithProviders } from '/app/__testing-utils__' +import { i18n } from '/app/i18n' +import { RECOVERY_MAP } from '../../constants' +import { SelectRecoveryOption } from '../SelectRecoveryOption' +import { HomeAndRetry } from '../HomeAndRetry' +import { TipSelection } from '../../shared/TipSelection' + +vi.mock('../SelectRecoveryOption') +vi.mock('../../shared/TipSelection') + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('HomeAndRetry', () => { + let props: React.ComponentProps + beforeEach(() => { + props = { + ...mockRecoveryContentProps, + currentRecoveryOptionUtils: { + ...mockRecoveryContentProps.currentRecoveryOptionUtils, + selectedRecoveryOption: RECOVERY_MAP.HOME_AND_RETRY.ROUTE, + }, + } + vi.mocked(SelectRecoveryOption).mockReturnValue( +
MOCK_SELECT_RECOVERY_OPTION
+ ) + vi.mocked(TipSelection).mockReturnValue(
WELL_SELECTION
) + }) + afterEach(() => { + vi.resetAllMocks() + }) + it(`renders PrepareDeckForHome when step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.PREPARE_DECK_FOR_HOME}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.PREPARE_DECK_FOR_HOME, + }, + } + render(props) + screen.getByText('Prepare deck for homing') + }) + it(`renders ManageTips when step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE, + }, + tipStatusUtils: { + ...props.tipStatusUtils, + aPipetteWithTip: { + mount: 'left', + } as any, + }, + } + render(props) + screen.getByText('Remove any attached tips') + }) + it(`renders labware info when step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.REPLACE_TIPS}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.REPLACE_TIPS, + }, + failedLabwareUtils: { + ...props.failedLabwareUtils, + relevantWellName: 'A2', + failedLabwareLocations: { + ...props.failedLabwareUtils.failedLabwareLocations, + displayNameCurrentLoc: 'B2', + }, + }, + } + + render(props) + screen.getByText('Replace used tips in rack location A2 in B2') + }) + it(`renders SelectTips when step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.SELECT_TIPS}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.SELECT_TIPS, + }, + failedLabwareUtils: { + ...props.failedLabwareUtils, + failedLabwareLocations: { + ...props.failedLabwareUtils.failedLabwareLocations, + displayNameCurrentLoc: 'B2', + }, + }, + } + render(props) + screen.getByText('Select tip pick-up location') + }) + it(`renders HomeGantryBeforeRetry when step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY, + }, + } + render(props) + screen.getByText('Home gantry') + }) + it(`renders the special door open handler when step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.CLOSE_DOOR_AND_HOME}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.CLOSE_DOOR_AND_HOME, + }, + doorStatusUtils: { + ...props.doorStatusUtils, + isDoorOpen: true, + }, + } + render(props) + screen.getByText('Close the robot door') + }) + it(`renders RetryAfterHome awhen step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.CONFIRM_RETRY}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.CONFIRM_RETRY, + }, + } + render(props) + screen.getByText('Retry step') + }) + it(`renders SelectRecoveryOption as a fallback`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + step: 'UNKNOWN_STEP' as any, + }, + } + render(props) + screen.getByText('MOCK_SELECT_RECOVERY_OPTION') + }) +}) diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx index a0dd0c778ca..62fe8eea3c8 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx @@ -18,6 +18,7 @@ import { TIP_NOT_DETECTED_OPTIONS, TIP_DROP_FAILED_OPTIONS, GRIPPER_ERROR_OPTIONS, + STALL_OR_COLLISION_OPTIONS, } from '../SelectRecoveryOption' import { RECOVERY_MAP, ERROR_KINDS } from '../../constants' import { clickButtonLabeled } from '../../__tests__/util' @@ -95,6 +96,9 @@ describe('SelectRecoveryOption', () => { expect.any(String) ) .thenReturn('Skip to next step with same tips') + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.HOME_AND_RETRY.ROUTE, expect.any(String)) + .thenReturn('Home gantry and retry') }) it('sets the selected recovery option when clicking continue', () => { @@ -231,6 +235,22 @@ describe('SelectRecoveryOption', () => { RECOVERY_MAP.RETRY_STEP.ROUTE ) }) + it('renders appropriate "Stall or collision" copy and click behavior', () => { + props = { + ...props, + errorKind: ERROR_KINDS.STALL_OR_COLLISION, + } + renderSelectRecoveryOption(props) + screen.getByText('Choose a recovery action') + const homeGantryAndRetry = screen.getAllByRole('label', { + name: 'Home gantry and retry', + }) + fireEvent.click(homeGantryAndRetry[0]) + clickButtonLabeled('Continue') + expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( + RECOVERY_MAP.HOME_AND_RETRY.ROUTE + ) + }) }) describe('RecoveryOptions', () => { let props: React.ComponentProps @@ -292,6 +312,9 @@ describe('RecoveryOptions', () => { expect.any(String) ) .thenReturn('Manually replace labware on deck and retry step') + when(mockGetRecoveryOptionCopy) + .calledWith(RECOVERY_MAP.HOME_AND_RETRY.ROUTE, expect.any(String)) + .thenReturn('Home gantry and retry') }) it('renders valid recovery options for a general error errorKind', () => { @@ -415,6 +438,17 @@ describe('RecoveryOptions', () => { }) screen.getByRole('label', { name: 'Cancel run' }) }) + it(`renders valid recovery options for a ${ERROR_KINDS.STALL_OR_COLLISION} errorKind`, () => { + props = { + ...props, + validRecoveryOptions: STALL_OR_COLLISION_OPTIONS, + } + renderRecoveryOptions(props) + screen.getByRole('label', { + name: 'Home gantry and retry', + }) + screen.getByRole('label', { name: 'Cancel run' }) + }) }) describe('getRecoveryOptions', () => { @@ -475,4 +509,11 @@ describe('getRecoveryOptions', () => { ) expect(overpressureWhileDispensingOptions).toBe(GRIPPER_ERROR_OPTIONS) }) + + it(`returns valid options when the errorKind is ${ERROR_KINDS.STALL_OR_COLLISION}`, () => { + const stallOrCollisionOptions = getRecoveryOptions( + ERROR_KINDS.STALL_OR_COLLISION + ) + expect(stallOrCollisionOptions).toBe(STALL_OR_COLLISION_OPTIONS) + }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts index 0e50d054523..0ad8f530709 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts @@ -10,3 +10,4 @@ export { SkipStepNewTips } from './SkipStepNewTips' export { IgnoreErrorSkipStep } from './IgnoreErrorSkipStep' export { ManualMoveLwAndSkip } from './ManualMoveLwAndSkip' export { ManualReplaceLwAndRetry } from './ManualReplaceLwAndRetry' +export { HomeAndRetry } from './HomeAndRetry' diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx index dd915b72afb..d97072e45f3 100644 --- a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx @@ -24,6 +24,7 @@ import { IgnoreErrorSkipStep, ManualReplaceLwAndRetry, ManualMoveLwAndSkip, + HomeAndRetry, } from '../RecoveryOptions' import { RecoveryInProgress } from '../RecoveryInProgress' import { RecoveryError } from '../RecoveryError' @@ -188,6 +189,7 @@ describe('ErrorRecoveryContent', () => { ROBOT_RELEASING_LABWARE, MANUAL_REPLACE_AND_RETRY, MANUAL_MOVE_AND_SKIP, + HOME_AND_RETRY, } = RECOVERY_MAP let props: React.ComponentProps @@ -225,6 +227,7 @@ describe('ErrorRecoveryContent', () => { vi.mocked(RecoveryDoorOpenSpecial).mockReturnValue(
MOCK_DOOR_OPEN_SPECIAL
) + vi.mocked(HomeAndRetry).mockReturnValue(
MOCK_HOME_AND_RETRY
) }) it(`returns SelectRecoveryOption when the route is ${OPTION_SELECTION.ROUTE}`, () => { @@ -505,4 +508,17 @@ describe('ErrorRecoveryContent', () => { screen.getByText('MOCK_DOOR_OPEN_SPECIAL') }) + + it(`returns HomeAndRetry when the route is ${HOME_AND_RETRY.ROUTE}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + route: HOME_AND_RETRY.ROUTE, + }, + } + renderRecoveryContent(props) + + screen.getByText('MOCK_HOME_AND_RETRY') + }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/constants.ts b/app/src/organisms/ErrorRecoveryFlows/constants.ts index 75835fd29f3..8be1b6adbe1 100644 --- a/app/src/organisms/ErrorRecoveryFlows/constants.ts +++ b/app/src/organisms/ErrorRecoveryFlows/constants.ts @@ -20,6 +20,7 @@ export const DEFINED_ERROR_TYPES = { TIP_PHYSICALLY_MISSING: 'tipPhysicallyMissing', TIP_PHYSICALLY_ATTACHED: 'tipPhysicallyAttached', GRIPPER_MOVEMENT: 'gripperMovement', + STALL_OR_COLLISION: 'stallOrCollision', } // Client-defined error-handling flows. @@ -32,6 +33,7 @@ export const ERROR_KINDS = { TIP_NOT_DETECTED: 'TIP_NOT_DETECTED', TIP_DROP_FAILED: 'TIP_DROP_FAILED', GRIPPER_ERROR: 'GRIPPER_ERROR', + STALL_OR_COLLISION: 'STALL_OR_COLLISION', } as const // TODO(jh, 06-14-24): Consolidate motion routes to a single route with several steps. @@ -55,6 +57,18 @@ export const RECOVERY_MAP = { DROP_TIP_GENERAL_ERROR: 'drop-tip-general-error', }, }, + HOME_AND_RETRY: { + ROUTE: 'home-and-retry', + STEPS: { + PREPARE_DECK_FOR_HOME: 'prepare-deck-for-home', + REMOVE_TIPS_FROM_PIPETTE: 'remove-tips-from-pipette', + REPLACE_TIPS: 'replace-tips', + SELECT_TIPS: 'select-tips', + HOME_BEFORE_RETRY: 'home-before-retry', + CLOSE_DOOR_AND_HOME: 'close-door-and-home', + CONFIRM_RETRY: 'confirm-retry', + }, + }, ROBOT_CANCELING: { ROUTE: 'robot-cancel-run', STEPS: { @@ -210,6 +224,7 @@ const { MANUAL_REPLACE_AND_RETRY, SKIP_STEP_WITH_NEW_TIPS, SKIP_STEP_WITH_SAME_TIPS, + HOME_AND_RETRY, } = RECOVERY_MAP // The deterministic ordering of steps for a given route. @@ -277,6 +292,15 @@ export const STEP_ORDER: StepOrder = { ERROR_WHILE_RECOVERING.STEPS.DROP_TIP_TIP_DROP_FAILED, ERROR_WHILE_RECOVERING.STEPS.DROP_TIP_BLOWOUT_FAILED, ], + [HOME_AND_RETRY.ROUTE]: [ + HOME_AND_RETRY.STEPS.PREPARE_DECK_FOR_HOME, + HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE, + HOME_AND_RETRY.STEPS.REPLACE_TIPS, + HOME_AND_RETRY.STEPS.SELECT_TIPS, + HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY, + HOME_AND_RETRY.STEPS.CLOSE_DOOR_AND_HOME, + HOME_AND_RETRY.STEPS.CONFIRM_RETRY, + ], } // Contains metadata specific to all routes and/or steps. @@ -333,6 +357,15 @@ export const RECOVERY_MAP_METADATA: RecoveryRouteStepMetadata = { [ROBOT_DOOR_OPEN.ROUTE]: { [ROBOT_DOOR_OPEN.STEPS.DOOR_OPEN]: { allowDoorOpen: false }, }, + [HOME_AND_RETRY.ROUTE]: { + [HOME_AND_RETRY.STEPS.PREPARE_DECK_FOR_HOME]: { allowDoorOpen: true }, + [HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE]: { allowDoorOpen: true }, + [HOME_AND_RETRY.STEPS.REPLACE_TIPS]: { allowDoorOpen: true }, + [HOME_AND_RETRY.STEPS.SELECT_TIPS]: { allowDoorOpen: true }, + [HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY]: { allowDoorOpen: true }, + [HOME_AND_RETRY.STEPS.CLOSE_DOOR_AND_HOME]: { allowDoorOpen: true }, + [HOME_AND_RETRY.STEPS.CONFIRM_RETRY]: { allowDoorOpen: true }, + }, [ROBOT_DOOR_OPEN_SPECIAL.ROUTE]: { [ROBOT_DOOR_OPEN_SPECIAL.STEPS.DOOR_OPEN]: { allowDoorOpen: true }, }, diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx index 62a810cd96e..11e8a574246 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx @@ -111,6 +111,11 @@ describe('useRecoveryOptionCopy', () => { screen.getByText('Manually replace labware on deck and retry step') }) + it(`renders the correct copy for ${RECOVERY_MAP.HOME_AND_RETRY.ROUTE}`, () => { + render({ route: RECOVERY_MAP.HOME_AND_RETRY.ROUTE }) + screen.getByText('Home gantry and retry step') + }) + it('renders "Unknown action" for an unknown recovery option', () => { render({ route: 'unknown_route' as RecoveryRoute }) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts index 6acd0df2f45..0279b8b675a 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts @@ -23,6 +23,8 @@ export function useErrorName(errorKind: ErrorKind): string { return t('tip_drop_failed') case ERROR_KINDS.GRIPPER_ERROR: return t('gripper_error') + case ERROR_KINDS.STALL_OR_COLLISION: + return t('stall_or_collision_error') default: return t('error') } diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts index bc077d4c624..f1a57aa965f 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts @@ -148,6 +148,7 @@ export function getRelevantFailedLabwareCmdFrom({ case ERROR_KINDS.OVERPRESSURE_PREPARE_TO_ASPIRATE: case ERROR_KINDS.OVERPRESSURE_WHILE_ASPIRATING: case ERROR_KINDS.OVERPRESSURE_WHILE_DISPENSING: + case ERROR_KINDS.STALL_OR_COLLISION: return getRelevantPickUpTipCommand(failedCommandByRunRecord, runCommands) case ERROR_KINDS.GRIPPER_ERROR: return failedCommandByRunRecord as MoveLabwareRunTimeCommand @@ -155,7 +156,7 @@ export function getRelevantFailedLabwareCmdFrom({ return null default: console.error( - 'No labware associated with failed command. Handle case explicitly.' + `useFailedLabwareUtils: No labware associated with error kind ${errorKind}. Handle case explicitly.` ) return null } diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts index 3e4b20225c5..01f5c4a7c94 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts @@ -70,6 +70,8 @@ export interface UseRecoveryCommandsResult { homeExceptPlungers: () => Promise /* A non-terminal recovery command */ moveLabwareWithoutPause: () => Promise + /* A non-terminal recovery-command */ + homeAll: () => Promise } // TODO(jh, 07-24-24): Create tighter abstractions for terminal vs. non-terminal commands. @@ -307,6 +309,10 @@ export function useRecoveryCommands({ return chainRunRecoveryCommands([HOME_EXCEPT_PLUNGERS]) }, [chainRunRecoveryCommands]) + const homeAll = useCallback((): Promise => { + return chainRunRecoveryCommands([HOME_ALL]) + }, [chainRunRecoveryCommands]) + const moveLabwareWithoutPause = useCallback((): Promise => { const moveLabwareCmd = buildMoveLabwareWithoutPause( unvalidatedFailedCommand @@ -329,6 +335,7 @@ export function useRecoveryCommands({ moveLabwareWithoutPause, skipFailedCommand, ignoreErrorKindThisRun, + homeAll, } } @@ -371,6 +378,11 @@ export const HOME_EXCEPT_PLUNGERS: CreateCommand = { }, } +export const HOME_ALL: CreateCommand = { + commandType: 'home', + params: {}, +} + const buildMoveLabwareWithoutPause = ( failedCommand: FailedCommand | null ): CreateCommand | null => { diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx index b364af7f9d5..6c7f2f8fc94 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx @@ -26,6 +26,8 @@ export function useRecoveryOptionCopy(): ( } case RECOVERY_MAP.CANCEL_RUN.ROUTE: return t('cancel_run') + case RECOVERY_MAP.HOME_AND_RETRY.ROUTE: + return t('home_and_retry') case RECOVERY_MAP.RETRY_NEW_TIPS.ROUTE: return t('retry_with_new_tips') case RECOVERY_MAP.RETRY_SAME_TIPS.ROUTE: diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts index 9fef84caca9..533b9877f72 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts @@ -171,6 +171,7 @@ function handleRecoveryOptionAction( case RECOVERY_MAP.RETRY_NEW_TIPS.ROUTE: case RECOVERY_MAP.RETRY_STEP.ROUTE: case RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE: + case RECOVERY_MAP.HOME_AND_RETRY.ROUTE: return currentStepReturnVal default: { return null diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx index 7eb207a9fe7..603ac5af6c3 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx @@ -67,6 +67,7 @@ export function ErrorDetailsModal(props: ErrorDetailsModalProps): JSX.Element { case ERROR_KINDS.OVERPRESSURE_WHILE_DISPENSING: case ERROR_KINDS.TIP_NOT_DETECTED: case ERROR_KINDS.GRIPPER_ERROR: + case ERROR_KINDS.STALL_OR_COLLISION: return true default: return false @@ -213,6 +214,8 @@ export function NotificationBanner({ return case ERROR_KINDS.GRIPPER_ERROR: return + case ERROR_KINDS.STALL_OR_COLLISION: + return default: console.error('Handle error kind notification banners explicitly.') return
@@ -258,6 +261,18 @@ export function GripperErrorBanner(): JSX.Element { ) } +export function StallErrorBanner(): JSX.Element { + const { t } = useTranslation('error_recovery') + + return ( + + ) +} + // TODO(jh, 07-24-24): Using shared height/width constants for intervention modal sizing and the ErrorDetailsModal sizing // would be ideal. const DESKTOP_STEP_INFO_STYLE = css` diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryDoorOpenSpecial.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryDoorOpenSpecial.tsx index 98744985225..00b64839b90 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryDoorOpenSpecial.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryDoorOpenSpecial.tsx @@ -52,6 +52,7 @@ export function RecoveryDoorOpenSpecial({ switch (selectedRecoveryOption) { case RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE: case RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE: + case RECOVERY_MAP.HOME_AND_RETRY.ROUTE: return t('door_open_robot_home') default: { console.error( @@ -62,6 +63,16 @@ export function RecoveryDoorOpenSpecial({ } } + const handleHomeAllAndRoute = ( + route: RecoveryRoute, + step?: RouteStep + ): void => { + void handleMotionRouting(true, RECOVERY_MAP.ROBOT_IN_MOTION.ROUTE) + .then(() => recoveryCommands.homeAll()) + .finally(() => handleMotionRouting(false)) + .then(() => proceedToRouteAndStep(route, step)) + } + const handleHomeExceptPlungersAndRoute = ( route: RecoveryRoute, step?: RouteStep @@ -87,6 +98,12 @@ export function RecoveryDoorOpenSpecial({ RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE ) break + case RECOVERY_MAP.HOME_AND_RETRY.ROUTE: + handleHomeAllAndRoute( + RECOVERY_MAP.HOME_AND_RETRY.ROUTE, + RECOVERY_MAP.HOME_AND_RETRY.STEPS.CONFIRM_RETRY + ) + break default: { console.error( `Unhandled special-cased door open on route ${selectedRecoveryOption}.` diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RetryStepInfo.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RetryStepInfo.tsx index c9f7567ee94..fd7a1dbaf5f 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/RetryStepInfo.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/RetryStepInfo.tsx @@ -7,7 +7,9 @@ import { TwoColTextAndFailedStepNextStep } from './TwoColTextAndFailedStepNextSt import type { RecoveryContentProps } from '../types' -export function RetryStepInfo(props: RecoveryContentProps): JSX.Element { +export function RetryStepInfo( + props: RecoveryContentProps & { secondaryBtnOnClickOverride?: () => void } +): JSX.Element { const { routeUpdateActions, recoveryCommands, errorKind } = props const { ROBOT_RETRYING_STEP } = RECOVERY_MAP const { t } = useTranslation('error_recovery') diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx index 00fa95072c1..9bf8f12bc22 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx @@ -34,6 +34,7 @@ export function TwoColLwInfoAndDeck( SKIP_STEP_WITH_NEW_TIPS, MANUAL_MOVE_AND_SKIP, MANUAL_REPLACE_AND_RETRY, + HOME_AND_RETRY, } = RECOVERY_MAP const { selectedRecoveryOption } = currentRecoveryOptionUtils const { relevantWellName, failedLabware } = failedLabwareUtils @@ -55,6 +56,7 @@ export function TwoColLwInfoAndDeck( return t('manually_move_lw_on_deck') case MANUAL_REPLACE_AND_RETRY.ROUTE: return t('manually_replace_lw_on_deck') + case HOME_AND_RETRY.ROUTE: case RETRY_NEW_TIPS.ROUTE: case SKIP_STEP_WITH_NEW_TIPS.ROUTE: { // Only special case the "full" 96-channel nozzle config. @@ -72,7 +74,7 @@ export function TwoColLwInfoAndDeck( } default: console.error( - 'Unexpected recovery option. Handle retry step copy explicitly.' + `TwoColLwInfoAndDeck: Unexpected recovery option: ${selectedRecoveryOption}. Handle retry step copy explicitly.` ) return 'UNEXPECTED RECOVERY OPTION' } @@ -84,14 +86,15 @@ export function TwoColLwInfoAndDeck( case MANUAL_REPLACE_AND_RETRY.ROUTE: return t('ensure_lw_is_accurately_placed') case RETRY_NEW_TIPS.ROUTE: - case SKIP_STEP_WITH_NEW_TIPS.ROUTE: { + case SKIP_STEP_WITH_NEW_TIPS.ROUTE: + case HOME_AND_RETRY.ROUTE: { return isPartialTipConfigValid ? t('replace_tips_and_select_loc_partial_tip') : t('replace_tips_and_select_location') } default: console.error( - 'Unexpected recovery option. Handle retry step copy explicitly.' + `TwoColLwInfoAndDeck:buildBannerText: Unexpected recovery option ${selectedRecoveryOption}. Handle retry step copy explicitly.` ) return 'UNEXPECTED RECOVERY OPTION' } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx index d759aaf3d78..ce754df9cfa 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx @@ -14,6 +14,7 @@ import { OverpressureBanner, TipNotDetectedBanner, GripperErrorBanner, + StallErrorBanner, } from '../ErrorDetailsModal' vi.mock('react-dom', () => ({ @@ -201,3 +202,25 @@ describe('GripperErrorBanner', () => { ) }) }) + +describe('StallErrorBanner', () => { + beforeEach(() => { + vi.mocked(InlineNotification).mockReturnValue( +
MOCK_INLINE_NOTIFICATION
+ ) + }) + it('renders the InlineNotification', () => { + renderWithProviders(, { + i18nInstance: i18n, + }) + expect(vi.mocked(InlineNotification)).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'alert', + heading: + "A stall or collision is detected when the robot's motors are blocked", + message: 'The robot must return to its home position before proceeding', + }), + {} + ) + }) +}) diff --git a/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts b/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts index e9b5722ffa8..fb3637c0eb5 100644 --- a/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts +++ b/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts @@ -68,6 +68,21 @@ describe('getErrorKind', () => { errorType: 'someHithertoUnknownDefinedErrorType', expectedError: ERROR_KINDS.GENERAL_ERROR, }, + ...([ + 'aspirate', + 'dispense', + 'blowOut', + 'moveToWell', + 'moveToAddressableArea', + 'dropTip', + 'pickUpTip', + 'prepareToAspirate', + ] as const).map(cmd => ({ + commandType: cmd, + errorType: DEFINED_ERROR_TYPES.STALL_OR_COLLISION, + expectedError: ERROR_KINDS.STALL_OR_COLLISION, + isDefined: true, + })), ])( 'returns $expectedError for $commandType with errorType $errorType', ({ commandType, errorType, expectedError, isDefined = true }) => { diff --git a/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts b/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts index 1dc5e023a6c..73fe862eb3b 100644 --- a/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts +++ b/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts @@ -54,6 +54,8 @@ export function getErrorKind( errorType === DEFINED_ERROR_TYPES.GRIPPER_MOVEMENT ) { return ERROR_KINDS.GRIPPER_ERROR + } else if (errorType === DEFINED_ERROR_TYPES.STALL_OR_COLLISION) { + return ERROR_KINDS.STALL_OR_COLLISION } } From 690fcc31496fc4a29d4b965c97623c970c32e031 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Mon, 2 Dec 2024 14:55:22 -0500 Subject: [PATCH 2/9] fix(app): Properly truncate ODD command text (#17003) --- .../ErrorRecoveryFlows/RecoverySplash.tsx | 15 ++++++++++++ .../CurrentRunningProtocolCommand.tsx | 23 +++++++++---------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoverySplash.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoverySplash.tsx index cd31843f834..153d8c12931 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoverySplash.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoverySplash.tsx @@ -6,6 +6,7 @@ import { ALIGN_CENTER, COLORS, DIRECTION_COLUMN, + RESPONSIVENESS, DISPLAY_FLEX, Flex, Icon, @@ -200,6 +201,7 @@ export function RecoverySplash(props: RecoverySplashProps): JSX.Element | null { overflowWrap={OVERFLOW_WRAP_BREAK_WORD} color={COLORS.white} textAlign={TEXT_ALIGN_CENTER} + css={TEXT_TRUNCATION_STYLE} /> @@ -253,6 +255,7 @@ export function RecoverySplash(props: RecoverySplashProps): JSX.Element | null { overflow="hidden" overflowWrap={OVERFLOW_WRAP_BREAK_WORD} textAlign={TEXT_ALIGN_CENTER} + css={TEXT_TRUNCATION_STYLE} /> @@ -301,6 +304,18 @@ const SplashFrame = styled(Flex)` padding-bottom: 0px; ` +const TEXT_TRUNCATION_STYLE = css` + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + font-size: ${TYPOGRAPHY.fontSize22}; + } +` + const SHARED_BUTTON_STYLE_ODD = css` width: 29rem; height: 13.5rem; diff --git a/app/src/organisms/ODD/RunningProtocol/CurrentRunningProtocolCommand.tsx b/app/src/organisms/ODD/RunningProtocol/CurrentRunningProtocolCommand.tsx index 92c1d1f5733..d25e356ad0d 100644 --- a/app/src/organisms/ODD/RunningProtocol/CurrentRunningProtocolCommand.tsx +++ b/app/src/organisms/ODD/RunningProtocol/CurrentRunningProtocolCommand.tsx @@ -75,32 +75,31 @@ const RUN_TIMER_STYLE = css` color: ${COLORS.black90}; ` -const COMMAND_ROW_STYLE_ANIMATED = css` +const COMMAND_ROW_STYLE_BASE = css` font-size: 1.375rem; line-height: 1.75rem; font-weight: ${TYPOGRAPHY.fontWeightRegular}; text-align: center; - width: fit-content; + width: 100%; + max-width: 100%; margin: auto; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; + max-height: 4.6rem; // This ensures we don't show any extra text after truncating. + word-break: break-word; + white-space: normal; +` + +const COMMAND_ROW_STYLE_ANIMATED = css` + ${COMMAND_ROW_STYLE_BASE} animation: ${fadeIn} 1.5s ease-in-out; ${ODD_ANIMATION_OPTIMIZATIONS} ` const COMMAND_ROW_STYLE = css` - font-size: 1.375rem; - line-height: 1.75rem; - font-weight: ${TYPOGRAPHY.fontWeightRegular}; - text-align: center; - width: fit-content; - margin: auto; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - overflow: hidden; + ${COMMAND_ROW_STYLE_BASE} ` interface RunTimerInfo { From 985d6bc0f6660cdcde48c968a751873c426fb72f Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Mon, 2 Dec 2024 15:06:10 -0500 Subject: [PATCH 3/9] refactor(app): update pinned protocol copy (#17007) Closes RQA-3708 --- app/src/assets/localization/en/protocol_info.json | 2 ++ .../pages/ODD/ProtocolDashboard/PinnedProtocol.tsx | 14 +++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/assets/localization/en/protocol_info.json b/app/src/assets/localization/en/protocol_info.json index 3307c45363f..66c2fc9aa89 100644 --- a/app/src/assets/localization/en/protocol_info.json +++ b/app/src/assets/localization/en/protocol_info.json @@ -10,6 +10,7 @@ "creation_method": "Creation Method", "custom_labware_not_supported": "Robot doesn't support custom labware", "date_added": "Date Added", + "date_added_date": "Date added {{date}}", "delete_protocol": "Delete protocol", "description": "Description", "drag_file_here": "Drag and drop protocol file here", @@ -41,6 +42,7 @@ "labware_position_check_complete_toast_with_offsets": "Labware Position Check complete. {{count}} Labware Offset created.", "labware_title": "Required Labware", "last_run": "Last Run", + "last_run_time": "Last run {{time}}", "last_updated": "Last Updated", "launch_protocol_designer": "Open Protocol Designer", "manual_steps_learn_more": "Learn more about manual steps", diff --git a/app/src/pages/ODD/ProtocolDashboard/PinnedProtocol.tsx b/app/src/pages/ODD/ProtocolDashboard/PinnedProtocol.tsx index 226000381ad..2620bd6de52 100644 --- a/app/src/pages/ODD/ProtocolDashboard/PinnedProtocol.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/PinnedProtocol.tsx @@ -1,7 +1,6 @@ import * as React from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' -import { formatDistance } from 'date-fns' import styled, { css } from 'styled-components' import { @@ -22,6 +21,7 @@ import { import { LongPressModal } from './LongPressModal' import { formatTimeWithUtcLabel } from '/app/resources/runs' +import { useUpdatedLastRunTime } from './hooks' import type { UseLongPressResult } from '@opentrons/components' import type { ProtocolResource } from '@opentrons/shared-data' @@ -87,6 +87,8 @@ export function PinnedProtocol(props: PinnedProtocolProps): JSX.Element { const protocolName = protocol.metadata.protocolName ?? protocol.files[0].name const { t } = useTranslation('protocol_info') + const updatedLastRun = useUpdatedLastRunTime(lastRun) + // ToDo (kk:06/18/2024) this will be removed later const handleProtocolClick = ( longpress: UseLongPressResult, @@ -155,14 +157,12 @@ export function PinnedProtocol(props: PinnedProtocolProps): JSX.Element { color={COLORS.grey60} > - {lastRun !== undefined - ? `${formatDistance(new Date(lastRun), new Date(), { - addSuffix: true, - }).replace('about ', '')}` - : t('no_history')} + {t('last_run_time', { time: updatedLastRun })} - {formatTimeWithUtcLabel(protocol.createdAt)} + {t('date_added_date', { + date: formatTimeWithUtcLabel(protocol.createdAt), + })} {longpress.isLongPressed && ( From 8873008d84df9dc23ad6b8c1defe84fe9aeb2b76 Mon Sep 17 00:00:00 2001 From: Jamey Huffnagle Date: Mon, 2 Dec 2024 15:06:47 -0500 Subject: [PATCH 4/9] fix(actions): Fix memory script (#17004) Looks like this is behaving differently on CI than my local docker container tests. Luckily, the error is pretty straightforward: Error: Mixpanel request failed: 401, Unauthorized, Expected project_id parameter to be a number when specified --- .github/actions/.gitattributes | 2 +- .../odd-resource-analysis/action/lib/helpers/mixpanel.js | 2 +- .github/actions/odd-resource-analysis/dist/index.js | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/actions/.gitattributes b/.github/actions/.gitattributes index a0e3fe76dd7..72f4684a29d 100644 --- a/.github/actions/.gitattributes +++ b/.github/actions/.gitattributes @@ -1 +1 @@ -.github/actions/odd-resource-analysis/dist/* binary \ No newline at end of file +odd-resource-analysis/dist/* binary \ No newline at end of file diff --git a/.github/actions/odd-resource-analysis/action/lib/helpers/mixpanel.js b/.github/actions/odd-resource-analysis/action/lib/helpers/mixpanel.js index a518d1f4b40..f7a6b01767b 100644 --- a/.github/actions/odd-resource-analysis/action/lib/helpers/mixpanel.js +++ b/.github/actions/odd-resource-analysis/action/lib/helpers/mixpanel.js @@ -33,7 +33,7 @@ async function getMixpanelResourceMonitorDataFor({ where, }) { const params = new URLSearchParams({ - project_id: projectId, + project_id: Number(projectId), from_date: fromDate, to_date: toDate, event: '["resourceMonitorReport"]', diff --git a/.github/actions/odd-resource-analysis/dist/index.js b/.github/actions/odd-resource-analysis/dist/index.js index cca88b8a17d..f122c0d5702 100644 --- a/.github/actions/odd-resource-analysis/dist/index.js +++ b/.github/actions/odd-resource-analysis/dist/index.js @@ -88,8 +88,11 @@ function processMixpanelData(data) { const systemMemory = [] data.forEach(entry => { - const { systemUptimeHrs, systemAvailMemMb, processesDetails } = - entry.properties + const { + systemUptimeHrs, + systemAvailMemMb, + processesDetails, + } = entry.properties const uptime = parseFloat(systemUptimeHrs) // Validate uptime before adding any measurements @@ -582,7 +585,7 @@ async function getMixpanelResourceMonitorDataFor({ where, }) { const params = new URLSearchParams({ - project_id: projectId, + project_id: Number(projectId), from_date: fromDate, to_date: toDate, event: '["resourceMonitorReport"]', From 5009d3c8ec9ceec99401abf14d615f976670fbeb Mon Sep 17 00:00:00 2001 From: koji Date: Mon, 2 Dec 2024 15:26:57 -0500 Subject: [PATCH 5/9] refactor(app): clean up react import statements (#16998) * refactor(app): clean up react import statements (#16998) --- app/src/App/Navbar.tsx | 4 +- app/src/atoms/Slideout/index.tsx | 10 ++-- .../__tests__/CustomKeyboard.test.tsx | 16 ++++--- .../AlphanumericKeyboard/index.tsx | 4 +- .../__tests__/FullKeyboard.test.tsx | 16 ++++--- .../SoftwareKeyboard/FullKeyboard/index.tsx | 8 ++-- .../__tests__/IndividualKey.test.tsx | 10 ++-- .../__tests__/NumericalKeyboard.test.tsx | 20 ++++---- .../molecules/CollapsibleSection/index.tsx | 8 ++-- .../molecules/InstrumentCard/MenuOverlay.tsx | 13 ++--- .../InterventionModal/DeckMapContent.tsx | 7 +-- .../ModalContentOneColSimpleButtons.tsx | 10 ++-- .../JogControls/DirectionControl.tsx | 11 +++-- app/src/molecules/JogControls/index.tsx | 11 ++--- .../molecules/ToggleGroup/useToggleGroup.tsx | 8 ++-- app/src/molecules/UploadInput/index.tsx | 28 ++++++----- .../organisms/ApplyHistoricOffsets/index.tsx | 8 ++-- .../Desktop/Alerts/AlertsProvider.tsx | 10 ++-- .../organisms/Desktop/CalibrateDeck/index.tsx | 7 +-- .../Desktop/CalibratePipetteOffset/index.tsx | 7 +-- .../AskForCalibrationBlockModal.tsx | 9 ++-- .../Desktop/CalibrateTipLength/index.tsx | 7 +-- .../Desktop/CalibrationPanels/SaveZPoint.tsx | 7 +-- .../Desktop/CheckCalibration/index.tsx | 10 ++-- .../Desktop/ChooseProtocolSlideout/index.tsx | 36 +++++++------- .../AvailableRobotOption.tsx | 9 ++-- .../index.tsx | 25 +++++----- .../ChangePipette/CheckPipettesButton.tsx | 8 ++-- .../Desktop/Devices/GripperCard/index.tsx | 19 ++++---- .../Devices/PipetteCard/FlexPipetteCard.tsx | 26 +++++----- .../ProtocolAnalysisErrorBanner.tsx | 7 +-- .../modals/ConfirmCancelModal.tsx | 12 +++-- .../modals/RunFailedModal.tsx | 7 +-- .../ProtocolRun/ProtocolRunHeader/index.tsx | 10 ++-- .../Devices/ProtocolRun/ProtocolRunSetup.tsx | 13 +++-- .../Desktop/Devices/RobotOverflowMenu.tsx | 15 +++--- .../Devices/RobotOverviewOverflowMenu.tsx | 20 ++++---- .../DeviceResetSlideout.tsx | 11 +++-- .../FactoryModeSlideout.tsx | 21 ++++---- .../RenameRobotSlideout.tsx | 10 ++-- .../AdvancedTab/Troubleshooting.tsx | 16 +++---- .../AdvancedTab/UpdateRobotSoftware.tsx | 9 ++-- .../ConnectModal/UploadKeyInput.tsx | 20 ++++---- .../RobotSettings/RobotSettingsAdvanced.tsx | 28 +++++------ .../RobotSettingsFeatureFlags.tsx | 8 ++-- .../RobotUpdateProgressModal.tsx | 29 ++++++----- .../__tests__/useLPCSuccessToast.test.ts | 9 ++-- .../LabwareCard/CustomLabwareOverflowMenu.tsx | 15 +++--- .../Desktop/Labware/LabwareCard/hooks.tsx | 7 +-- .../StyledComponents/ExpandingTitle.tsx | 15 +++--- .../Desktop/ProtocolAnalysisFailure/index.tsx | 17 +++---- .../ProtocolLabwareDetails.tsx | 7 +-- .../RobotConfigurationDetails.tsx | 13 ++--- .../Desktop/ProtocolsLanding/ProtocolList.tsx | 15 +++--- .../CalibrationDetails/OverflowMenu.tsx | 25 +++++----- .../AddFixtureModal.tsx | 19 ++++---- .../EmergencyStop/EstopPressedModal.tsx | 11 +++-- .../shared/ErrorDetailsModal.tsx | 19 ++++---- .../LabwarePositionCheck/AttachProbe.tsx | 11 ++--- .../LabwarePositionCheck/CheckItem.tsx | 7 +-- .../LabwarePositionCheck/DetachProbe.tsx | 9 ++-- .../IntroScreen/index.tsx | 7 +-- .../LabwarePositionCheck/JogToWell.tsx | 17 +++---- .../LabwarePositionCheck/PickUpTip.tsx | 7 +-- .../organisms/LabwarePositionCheck/index.tsx | 10 ++-- .../ModuleCard/ConfirmAttachmentModal.tsx | 8 ++-- .../ModuleCard/HeaterShakerSlideout.tsx | 7 +-- app/src/organisms/ModuleWizardFlows/index.tsx | 27 +++++------ .../organisms/ODD/InstrumentInfo/index.tsx | 17 +++---- .../AttachedInstrumentMountItem.tsx | 18 ++++--- .../ProtocolInstrumentMountItem.tsx | 37 +++++++------- .../ODD/Navigation/NavigationMenu.tsx | 7 +-- app/src/organisms/ODD/Navigation/index.tsx | 12 +++-- .../SelectAuthenticationType.tsx | 9 ++-- .../ODD/NetworkSettings/SetWifiSsid.tsx | 8 ++-- .../ProtocolSetupDeckConfiguration/index.tsx | 19 ++++---- .../ProtocolSetupLabware/index.tsx | 13 ++--- .../ProtocolSetupLiquids/index.tsx | 12 +++-- .../FixtureTable.tsx | 17 +++---- .../ModuleTable.tsx | 11 +++-- .../ProtocolSetupModulesAndDeck.tsx | 13 ++--- .../QuickTransferAdvancedSettings/AirGap.tsx | 13 ++--- .../BaseSettings.tsx | 9 ++-- .../QuickTransferAdvancedSettings/BlowOut.tsx | 11 +++-- .../QuickTransferAdvancedSettings/Delay.tsx | 17 +++---- .../FlowRate.tsx | 9 ++-- .../QuickTransferAdvancedSettings/Mix.tsx | 21 ++++---- .../PipettePath.tsx | 21 ++++---- .../TipPosition.tsx | 9 ++-- .../TouchTip.tsx | 15 +++--- .../QuickTransferAdvancedSettings/index.tsx | 11 ++--- .../QuickTransferFlow/SelectDestLabware.tsx | 13 +++-- .../ODD/QuickTransferFlow/SelectDestWells.tsx | 31 ++++++------ .../ODD/QuickTransferFlow/SelectPipette.tsx | 19 ++++---- .../QuickTransferFlow/SelectSourceLabware.tsx | 13 +++-- .../QuickTransferFlow/SelectSourceWells.tsx | 13 ++--- .../ODD/QuickTransferFlow/SelectTipRack.tsx | 13 ++--- .../QuickTransferFlow/SummaryAndSettings.tsx | 15 +++--- .../TipManagement/ChangeTip.tsx | 7 +-- .../TipManagement/TipDropLocation.tsx | 7 +-- .../QuickTransferFlow/TipManagement/index.tsx | 11 ++--- .../ODD/QuickTransferFlow/VolumeEntry.tsx | 11 +++-- .../organisms/ODD/QuickTransferFlow/index.tsx | 15 +++--- .../LanguageSetting.tsx | 9 ++-- .../RobotSettingsJoinOtherNetwork.tsx | 9 ++-- .../TouchScreenSleep.tsx | 11 +++-- .../RobotSettingsDashboard/UpdateChannel.tsx | 9 ++-- .../PipetteWizardFlows/ChoosePipette.tsx | 14 +++--- .../PipetteWizardFlows/DetachPipette.tsx | 19 ++++---- .../organisms/PipetteWizardFlows/Results.tsx | 7 +-- .../MaintenanceRunStatusProvider.tsx | 15 +++--- .../TakeoverModal/MaintenanceRunTakeover.tsx | 14 +++--- app/src/organisms/ToasterOven/ToasterOven.tsx | 9 ++-- .../WellSelection/Selection384Wells.tsx | 27 ++++++----- .../organisms/WellSelection/SelectionRect.tsx | 19 ++++---- .../ODD/ConnectViaWifi/JoinOtherNetwork.tsx | 9 ++-- app/src/pages/ODD/DeckConfiguration/index.tsx | 14 +++--- .../InstrumentDetailOverflowMenu.tsx | 13 ++--- .../ODD/ProtocolDashboard/PinnedProtocol.tsx | 7 +-- app/src/pages/ODD/ProtocolSetup/index.tsx | 48 +++++++++---------- .../QuickTransferDashboard/PinnedTransfer.tsx | 7 +-- .../QuickTransferCard.tsx | 11 +++-- .../resources/deck_configuration/hooks.tsx | 9 ++-- 123 files changed, 874 insertions(+), 793 deletions(-) diff --git a/app/src/App/Navbar.tsx b/app/src/App/Navbar.tsx index db8a4acd005..1471ca4c593 100644 --- a/app/src/App/Navbar.tsx +++ b/app/src/App/Navbar.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { NavLink, useNavigate } from 'react-router-dom' import styled from 'styled-components' @@ -118,7 +118,7 @@ export function Navbar({ routes }: { routes: RouteProps[] }): JSX.Element { ({ navLinkTo }: RouteProps) => navLinkTo != null ) - const debouncedNavigate = React.useCallback( + const debouncedNavigate = useCallback( debounce((path: string) => { navigate(path) }, DEBOUNCE_DURATION_MS), diff --git a/app/src/atoms/Slideout/index.tsx b/app/src/atoms/Slideout/index.tsx index b834bd1c6e5..53a6888efa0 100644 --- a/app/src/atoms/Slideout/index.tsx +++ b/app/src/atoms/Slideout/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useRef, useState, useEffect } from 'react' import { css } from 'styled-components' import { useTranslation } from 'react-i18next' @@ -124,9 +124,9 @@ export const Slideout = (props: SlideoutProps): JSX.Element => { multiSlideoutSpecs, } = props const { t } = useTranslation('shared') - const slideOutRef = React.useRef(null) - const [isReachedBottom, setIsReachedBottom] = React.useState(false) - const hasBeenExpanded = React.useRef(isExpanded ?? false) + const slideOutRef = useRef(null) + const [isReachedBottom, setIsReachedBottom] = useState(false) + const hasBeenExpanded = useRef(isExpanded ?? false) const handleScroll = (): void => { if (slideOutRef.current == null) return const { scrollTop, scrollHeight, clientHeight } = slideOutRef.current @@ -137,7 +137,7 @@ export const Slideout = (props: SlideoutProps): JSX.Element => { } } - React.useEffect(() => { + useEffect(() => { handleScroll() }, [slideOutRef]) diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/__tests__/CustomKeyboard.test.tsx b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/__tests__/CustomKeyboard.test.tsx index b4a9abaae89..2fdf2e30b7e 100644 --- a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/__tests__/CustomKeyboard.test.tsx +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/__tests__/CustomKeyboard.test.tsx @@ -1,17 +1,19 @@ -import * as React from 'react' +import { useRef } from 'react' import { describe, it, expect, vi } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, renderHook, screen } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' import { AlphanumericKeyboard } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('AlphanumericKeyboard', () => { it('should render alphanumeric keyboard - lower case', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -55,7 +57,7 @@ describe('AlphanumericKeyboard', () => { }) }) it('should render alphanumeric keyboard - upper case, when clicking ABC key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -103,7 +105,7 @@ describe('AlphanumericKeyboard', () => { }) it('should render alphanumeric keyboard - numbers, when clicking number key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -133,7 +135,7 @@ describe('AlphanumericKeyboard', () => { }) it('should render alphanumeric keyboard - lower case when layout is numbers and clicking abc ', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -182,7 +184,7 @@ describe('AlphanumericKeyboard', () => { }) it('should switch each alphanumeric keyboard properly', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, diff --git a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx index 4ab8dab1274..73ec9306fee 100644 --- a/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/AlphanumericKeyboard/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import Keyboard from 'react-simple-keyboard' import { useSelector } from 'react-redux' import { getAppLanguage } from '/app/redux/config' @@ -24,7 +24,7 @@ export function AlphanumericKeyboard({ keyboardRef, debug = false, // If true, will input a \n }: AlphanumericKeyboardProps): JSX.Element { - const [layoutName, setLayoutName] = React.useState('default') + const [layoutName, setLayoutName] = useState('default') const appLanguage = useSelector(getAppLanguage) const onKeyPress = (button: string): void => { if (button === '{ABC}') handleShift() diff --git a/app/src/atoms/SoftwareKeyboard/FullKeyboard/__tests__/FullKeyboard.test.tsx b/app/src/atoms/SoftwareKeyboard/FullKeyboard/__tests__/FullKeyboard.test.tsx index 728ae462083..90786ff6a6f 100644 --- a/app/src/atoms/SoftwareKeyboard/FullKeyboard/__tests__/FullKeyboard.test.tsx +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/__tests__/FullKeyboard.test.tsx @@ -1,17 +1,19 @@ -import * as React from 'react' +import { useRef } from 'react' import { describe, it, expect, vi } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, renderHook, screen } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' import { FullKeyboard } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('FullKeyboard', () => { it('should render FullKeyboard keyboard', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -59,7 +61,7 @@ describe('FullKeyboard', () => { }) it('should render full keyboard when hitting ABC key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -108,7 +110,7 @@ describe('FullKeyboard', () => { }) it('should render full keyboard when hitting 123 key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -158,7 +160,7 @@ describe('FullKeyboard', () => { }) it('should render the software keyboards when hitting #+= key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -204,7 +206,7 @@ describe('FullKeyboard', () => { }) it('should call mock function when clicking a key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, diff --git a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx index eed2a0b5934..2846930ad1e 100644 --- a/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx +++ b/app/src/atoms/SoftwareKeyboard/FullKeyboard/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { KeyboardReact as Keyboard } from 'react-simple-keyboard' import { useSelector } from 'react-redux' import { getAppLanguage } from '/app/redux/config' @@ -7,6 +7,8 @@ import { layoutCandidates, fullKeyboardLayout, } from '../constants' + +import type { MutableRefObject } from 'react' import type { KeyboardReactInterface } from 'react-simple-keyboard' import '../index.css' @@ -15,7 +17,7 @@ import './index.css' // TODO (kk:04/05/2024) add debug to make debugging easy interface FullKeyboardProps { onChange: (input: string) => void - keyboardRef: React.MutableRefObject + keyboardRef: MutableRefObject debug?: boolean } @@ -24,7 +26,7 @@ export function FullKeyboard({ keyboardRef, debug = false, }: FullKeyboardProps): JSX.Element { - const [layoutName, setLayoutName] = React.useState('default') + const [layoutName, setLayoutName] = useState('default') const appLanguage = useSelector(getAppLanguage) const handleShift = (button: string): void => { switch (button) { diff --git a/app/src/atoms/SoftwareKeyboard/IndividualKey/__tests__/IndividualKey.test.tsx b/app/src/atoms/SoftwareKeyboard/IndividualKey/__tests__/IndividualKey.test.tsx index b29404ba226..ecdbdf9aa78 100644 --- a/app/src/atoms/SoftwareKeyboard/IndividualKey/__tests__/IndividualKey.test.tsx +++ b/app/src/atoms/SoftwareKeyboard/IndividualKey/__tests__/IndividualKey.test.tsx @@ -1,17 +1,19 @@ -import * as React from 'react' +import { useRef } from 'react' import { describe, it, vi, expect } from 'vitest' import { fireEvent, renderHook, screen } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' import { IndividualKey } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('IndividualKey', () => { it('should render the text key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -22,7 +24,7 @@ describe('IndividualKey', () => { }) it('should call mock function when clicking text key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, diff --git a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/__tests__/NumericalKeyboard.test.tsx b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/__tests__/NumericalKeyboard.test.tsx index 1bda1caaa71..722f50dfcf1 100644 --- a/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/__tests__/NumericalKeyboard.test.tsx +++ b/app/src/atoms/SoftwareKeyboard/NumericalKeyboard/__tests__/NumericalKeyboard.test.tsx @@ -1,17 +1,19 @@ -import * as React from 'react' +import { useRef } from 'react' import { describe, it, expect, vi } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, renderHook, screen } from '@testing-library/react' import { renderWithProviders } from '/app/__testing-utils__' import { NumericalKeyboard } from '..' -const render = (props: React.ComponentProps) => { +import type { ComponentProps } from 'react' + +const render = (props: ComponentProps) => { return renderWithProviders()[0] } describe('NumericalKeyboard', () => { it('should render numerical keyboard isDecimal: false and hasHyphen: false', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -41,7 +43,7 @@ describe('NumericalKeyboard', () => { }) it('should render numerical keyboard isDecimal: false and hasHyphen: true', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -72,7 +74,7 @@ describe('NumericalKeyboard', () => { }) it('should render numerical keyboard isDecimal: true and hasHyphen: false', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -103,7 +105,7 @@ describe('NumericalKeyboard', () => { }) it('should render numerical keyboard isDecimal: true and hasHyphen: true', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -135,7 +137,7 @@ describe('NumericalKeyboard', () => { }) it('should call mock function when clicking num key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -149,7 +151,7 @@ describe('NumericalKeyboard', () => { }) it('should call mock function when clicking decimal point key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, @@ -163,7 +165,7 @@ describe('NumericalKeyboard', () => { }) it('should call mock function when clicking hyphen key', () => { - const { result } = renderHook(() => React.useRef(null)) + const { result } = renderHook(() => useRef(null)) const props = { onChange: vi.fn(), keyboardRef: result.current, diff --git a/app/src/molecules/CollapsibleSection/index.tsx b/app/src/molecules/CollapsibleSection/index.tsx index 3b9d6c4b8d0..3a359edeb4f 100644 --- a/app/src/molecules/CollapsibleSection/index.tsx +++ b/app/src/molecules/CollapsibleSection/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { css } from 'styled-components' import { @@ -12,6 +12,8 @@ import { LegacyStyledText, TYPOGRAPHY, } from '@opentrons/components' + +import type { ReactNode } from 'react' import type { StyleProps } from '@opentrons/components' const ACCORDION_STYLE = css` @@ -26,7 +28,7 @@ const ACCORDION_STYLE = css` interface CollapsibleSectionProps extends StyleProps { title: string - children: React.ReactNode + children: ReactNode isExpandedInitially?: boolean } @@ -34,7 +36,7 @@ export function CollapsibleSection( props: CollapsibleSectionProps ): JSX.Element { const { title, children, isExpandedInitially = true, ...styleProps } = props - const [isExpanded, setIsExpanded] = React.useState(isExpandedInitially) + const [isExpanded, setIsExpanded] = useState(isExpandedInitially) return ( + label: ReactNode + onClick: MouseEventHandler disabled?: boolean } @@ -41,14 +42,14 @@ export function MenuOverlay(props: MenuOverlayProps): JSX.Element { right="0" whiteSpace={NO_WRAP} zIndex={10} - onClick={(e: React.MouseEvent) => { + onClick={(e: MouseEvent) => { e.preventDefault() e.stopPropagation() setShowMenuOverlay(false) }} > {menuOverlayItems.map((menuOverlayItem, i) => ( - + {/* insert a divider before the last item if desired */} {hasDivider && i === menuOverlayItems.length - 1 ? ( @@ -59,7 +60,7 @@ export function MenuOverlay(props: MenuOverlayProps): JSX.Element { > {menuOverlayItem.label} - + ))} ) diff --git a/app/src/molecules/InterventionModal/DeckMapContent.tsx b/app/src/molecules/InterventionModal/DeckMapContent.tsx index a45bc920e0a..6bafed02bd1 100644 --- a/app/src/molecules/InterventionModal/DeckMapContent.tsx +++ b/app/src/molecules/InterventionModal/DeckMapContent.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect } from 'react' import { css } from 'styled-components' import { Box, @@ -11,6 +11,7 @@ import { useDeckLocationSelect, } from '@opentrons/components' +import type { ComponentProps } from 'react' import type { LabwareDefinition2, RobotType, @@ -22,7 +23,7 @@ export type MapKind = 'intervention' | 'deck-config' export interface InterventionStyleDeckMapContentProps extends Pick< - React.ComponentProps, + ComponentProps, 'deckConfig' | 'robotType' | 'labwareOnDeck' | 'modulesOnDeck' > { kind: 'intervention' @@ -107,7 +108,7 @@ function DeckConfigStyleDeckMapContent({ robotType, 'default' ) - React.useEffect(() => { + useEffect(() => { setSelectedLocation != null && setSelectedLocation(selectedLocation) }, [selectedLocation, setSelectedLocation]) return <>{DeckLocationSelect} diff --git a/app/src/molecules/InterventionModal/ModalContentOneColSimpleButtons.tsx b/app/src/molecules/InterventionModal/ModalContentOneColSimpleButtons.tsx index c70ddbea7d1..9302192aa68 100644 --- a/app/src/molecules/InterventionModal/ModalContentOneColSimpleButtons.tsx +++ b/app/src/molecules/InterventionModal/ModalContentOneColSimpleButtons.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { Flex, DIRECTION_COLUMN, @@ -9,10 +9,12 @@ import { } from '@opentrons/components' import { OneColumn } from './OneColumn' +import type { ChangeEventHandler } from 'react' + export interface ButtonProps { label: string value: string - onChange?: React.ChangeEventHandler + onChange?: ChangeEventHandler } export interface ModalContentOneColSimpleButtonsProps { @@ -20,14 +22,14 @@ export interface ModalContentOneColSimpleButtonsProps { firstButton: ButtonProps secondButton: ButtonProps furtherButtons?: ButtonProps[] - onSelect?: React.ChangeEventHandler + onSelect?: ChangeEventHandler initialSelected?: string } export function ModalContentOneColSimpleButtons( props: ModalContentOneColSimpleButtonsProps ): JSX.Element { - const [selected, setSelected] = React.useState( + const [selected, setSelected] = useState( props.initialSelected ?? null ) const furtherButtons = props.furtherButtons ?? [] diff --git a/app/src/molecules/JogControls/DirectionControl.tsx b/app/src/molecules/JogControls/DirectionControl.tsx index 8b05e533e4d..d51615fd97e 100644 --- a/app/src/molecules/JogControls/DirectionControl.tsx +++ b/app/src/molecules/JogControls/DirectionControl.tsx @@ -1,5 +1,5 @@ // jog controls component -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' @@ -31,8 +31,9 @@ import { ControlContainer } from './ControlContainer' import { HORIZONTAL_PLANE, VERTICAL_PLANE } from './constants' import { TouchControlButton } from './TouchControlButton' -import type { IconName } from '@opentrons/components' +import type { MouseEvent } from 'react' import type { CSSProperties } from 'styled-components' +import type { IconName } from '@opentrons/components' import type { Jog, Plane, Sign, Bearing, Axis, StepSize } from './types' interface Control { @@ -223,12 +224,12 @@ interface DirectionControlProps { export function DirectionControl(props: DirectionControlProps): JSX.Element { const { planes, jog, stepSize, initialPlane } = props - const [currentPlane, setCurrentPlane] = React.useState( + const [currentPlane, setCurrentPlane] = useState( initialPlane ?? planes[0] ) const { t } = useTranslation(['robot_calibration']) - const handlePlane = (event: React.MouseEvent): void => { + const handlePlane = (event: MouseEvent): void => { setCurrentPlane(event.currentTarget.value as Plane) event.currentTarget.blur() } @@ -449,7 +450,7 @@ export function TouchDirectionControl( props: DirectionControlProps ): JSX.Element { const { planes, jog, stepSize, initialPlane } = props - const [currentPlane, setCurrentPlane] = React.useState( + const [currentPlane, setCurrentPlane] = useState( initialPlane ?? planes[0] ) const { i18n, t } = useTranslation(['robot_calibration']) diff --git a/app/src/molecules/JogControls/index.tsx b/app/src/molecules/JogControls/index.tsx index c9d0d7b49f0..0208739d025 100644 --- a/app/src/molecules/JogControls/index.tsx +++ b/app/src/molecules/JogControls/index.tsx @@ -1,5 +1,5 @@ // jog controls component -import * as React from 'react' +import { useState } from 'react' import { css } from 'styled-components' import { Flex, @@ -20,15 +20,16 @@ import { DEFAULT_STEP_SIZES, } from './constants' -import type { Jog, Plane, StepSize } from './types' +import type { ReactNode } from 'react' import type { StyleProps } from '@opentrons/components' +import type { Jog, Plane, StepSize } from './types' export type { Jog } export interface JogControlsProps extends StyleProps { jog: Jog planes?: Plane[] stepSizes?: StepSize[] - auxiliaryControl?: React.ReactNode | null + auxiliaryControl?: ReactNode | null directionControlButtonColor?: string initialPlane?: Plane isOnDevice?: boolean @@ -53,9 +54,7 @@ export function JogControls(props: JogControlsProps): JSX.Element { isOnDevice = false, ...styleProps } = props - const [currentStepSize, setCurrentStepSize] = React.useState( - stepSizes[0] - ) + const [currentStepSize, setCurrentStepSize] = useState(stepSizes[0]) const controls = isOnDevice ? ( <> diff --git a/app/src/molecules/ToggleGroup/useToggleGroup.tsx b/app/src/molecules/ToggleGroup/useToggleGroup.tsx index ebe3efd14c8..5b356ba74cd 100644 --- a/app/src/molecules/ToggleGroup/useToggleGroup.tsx +++ b/app/src/molecules/ToggleGroup/useToggleGroup.tsx @@ -1,13 +1,15 @@ -import * as React from 'react' +import { useState } from 'react' import { ToggleGroup } from '@opentrons/components' import { useTrackEvent } from '/app/redux/analytics' +import type { ReactNode } from 'react' + export const useToggleGroup = ( left: string, right: string, trackEventName?: string -): [string, React.ReactNode] => { - const [selectedValue, setSelectedValue] = React.useState(left) +): [string, ReactNode] => { + const [selectedValue, setSelectedValue] = useState(left) const trackEvent = useTrackEvent() const handleLeftClick = (): void => { setSelectedValue(left) diff --git a/app/src/molecules/UploadInput/index.tsx b/app/src/molecules/UploadInput/index.tsx index 77dc5a2616d..89877f38c33 100644 --- a/app/src/molecules/UploadInput/index.tsx +++ b/app/src/molecules/UploadInput/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useRef, useState } from 'react' import styled, { css } from 'styled-components' import { useTranslation } from 'react-i18next' import { @@ -18,6 +18,12 @@ import { TYPOGRAPHY, } from '@opentrons/components' +import type { + ChangeEventHandler, + DragEventHandler, + MouseEventHandler, +} from 'react' + const StyledLabel = styled.label` display: ${DISPLAY_FLEX}; cursor: ${CURSOR_POINTER}; @@ -66,39 +72,37 @@ export function UploadInput(props: UploadInputProps): JSX.Element | null { } = props const { t } = useTranslation('protocol_info') - const fileInput = React.useRef(null) - const [isFileOverDropZone, setIsFileOverDropZone] = React.useState( - false - ) - const [isHover, setIsHover] = React.useState(false) - const handleDrop: React.DragEventHandler = e => { + const fileInput = useRef(null) + const [isFileOverDropZone, setIsFileOverDropZone] = useState(false) + const [isHover, setIsHover] = useState(false) + const handleDrop: DragEventHandler = e => { e.preventDefault() e.stopPropagation() Array.from(e.dataTransfer.files).forEach(f => onUpload(f)) setIsFileOverDropZone(false) } - const handleDragEnter: React.DragEventHandler = e => { + const handleDragEnter: DragEventHandler = e => { e.preventDefault() e.stopPropagation() } - const handleDragLeave: React.DragEventHandler = e => { + const handleDragLeave: DragEventHandler = e => { e.preventDefault() e.stopPropagation() setIsFileOverDropZone(false) setIsHover(false) } - const handleDragOver: React.DragEventHandler = e => { + const handleDragOver: DragEventHandler = e => { e.preventDefault() e.stopPropagation() setIsFileOverDropZone(true) setIsHover(true) } - const handleClick: React.MouseEventHandler = _event => { + const handleClick: MouseEventHandler = _event => { onClick != null ? onClick() : fileInput.current?.click() } - const onChange: React.ChangeEventHandler = event => { + const onChange: ChangeEventHandler = event => { ;[...(event.target.files ?? [])].forEach(f => onUpload(f)) if ('value' in event.currentTarget) event.currentTarget.value = '' } diff --git a/app/src/organisms/ApplyHistoricOffsets/index.tsx b/app/src/organisms/ApplyHistoricOffsets/index.tsx index 6925145c012..60b166fe8d8 100644 --- a/app/src/organisms/ApplyHistoricOffsets/index.tsx +++ b/app/src/organisms/ApplyHistoricOffsets/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useSelector } from 'react-redux' import pick from 'lodash/pick' @@ -25,6 +25,8 @@ import { LabwareOffsetTabs } from '/app/organisms/LabwareOffsetTabs' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' import { LabwareOffsetTable } from './LabwareOffsetTable' import { getIsLabwareOffsetCodeSnippetsOn } from '/app/redux/config' + +import type { ChangeEvent } from 'react' import type { LabwareOffset } from '@opentrons/api-client' import type { LoadedLabware, @@ -58,7 +60,7 @@ export function ApplyHistoricOffsets( modules, commands, } = props - const [showOffsetDataModal, setShowOffsetDataModal] = React.useState(false) + const [showOffsetDataModal, setShowOffsetDataModal] = useState(false) const { t } = useTranslation('labware_position_check') const isLabwareOffsetCodeSnippetsOn = useSelector( getIsLabwareOffsetCodeSnippetsOn @@ -85,7 +87,7 @@ export function ApplyHistoricOffsets( return ( ) => { + onChange={(e: ChangeEvent) => { setShouldApplyOffsets(e.currentTarget.checked) }} value={shouldApplyOffsets} diff --git a/app/src/organisms/Desktop/Alerts/AlertsProvider.tsx b/app/src/organisms/Desktop/Alerts/AlertsProvider.tsx index 21aeccbf855..79b6088b445 100644 --- a/app/src/organisms/Desktop/Alerts/AlertsProvider.tsx +++ b/app/src/organisms/Desktop/Alerts/AlertsProvider.tsx @@ -1,21 +1,23 @@ -import * as React from 'react' +import { createContext, useRef } from 'react' import { AlertsModal } from '.' import { useToaster } from '/app/organisms/ToasterOven' +import type { ReactNode } from 'react' + export interface AlertsContextProps { removeActiveAppUpdateToast: () => void } -export const AlertsContext = React.createContext({ +export const AlertsContext = createContext({ removeActiveAppUpdateToast: () => null, }) interface AlertsProps { - children: React.ReactNode + children: ReactNode } export function Alerts({ children }: AlertsProps): JSX.Element { - const toastRef = React.useRef(null) + const toastRef = useRef(null) const { eatToast } = useToaster() const removeActiveAppUpdateToast = (): void => { diff --git a/app/src/organisms/Desktop/CalibrateDeck/index.tsx b/app/src/organisms/Desktop/CalibrateDeck/index.tsx index 783b3d0ba13..b1201dc4553 100644 --- a/app/src/organisms/Desktop/CalibrateDeck/index.tsx +++ b/app/src/organisms/Desktop/CalibrateDeck/index.tsx @@ -1,5 +1,5 @@ // Deck Calibration Orchestration Component -import * as React from 'react' +import { useMemo } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { useQueryClient } from 'react-query' @@ -27,6 +27,7 @@ import { useCalibrationError, } from '/app/organisms/Desktop/CalibrationError' +import type { ComponentType } from 'react' import type { Mount } from '@opentrons/components' import type { CalibrationLabware, @@ -37,7 +38,7 @@ import type { CalibrationPanelProps } from '/app/organisms/Desktop/CalibrationPa import type { CalibrateDeckParentProps } from './types' const PANEL_BY_STEP: Partial< - Record> + Record> > = { [Sessions.DECK_STEP_SESSION_STARTED]: Introduction, [Sessions.DECK_STEP_LABWARE_LOADED]: DeckSetup, @@ -89,7 +90,7 @@ export function CalibrateDeck({ const errorInfo = useCalibrationError(requestIds, session?.id) - const isMulti = React.useMemo(() => { + const isMulti = useMemo(() => { const spec = instrument && getPipetteModelSpecs(instrument.model) return spec ? spec.channels > 1 : false }, [instrument]) diff --git a/app/src/organisms/Desktop/CalibratePipetteOffset/index.tsx b/app/src/organisms/Desktop/CalibratePipetteOffset/index.tsx index dce310da9c6..2359d275384 100644 --- a/app/src/organisms/Desktop/CalibratePipetteOffset/index.tsx +++ b/app/src/organisms/Desktop/CalibratePipetteOffset/index.tsx @@ -1,5 +1,5 @@ // Pipette Offset Calibration Orchestration Component -import * as React from 'react' +import { useMemo } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { useQueryClient } from 'react-query' @@ -27,6 +27,7 @@ import { useCalibrationError, } from '/app/organisms/Desktop/CalibrationError' +import type { ComponentType } from 'react' import type { Mount } from '@opentrons/components' import type { CalibrationLabware, @@ -37,7 +38,7 @@ import type { CalibratePipetteOffsetParentProps } from './types' import type { CalibrationPanelProps } from '/app/organisms/Desktop/CalibrationPanels/types' const PANEL_BY_STEP: Partial< - Record> + Record> > = { [Sessions.PIP_OFFSET_STEP_SESSION_STARTED]: Introduction, [Sessions.PIP_OFFSET_STEP_LABWARE_LOADED]: DeckSetup, @@ -88,7 +89,7 @@ export function CalibratePipetteOffset({ const calBlock: CalibrationLabware | null = labware != null ? labware.find(l => !l.isTiprack) ?? null : null - const isMulti = React.useMemo(() => { + const isMulti = useMemo(() => { const spec = instrument != null ? getPipetteModelSpecs(instrument.model) : null return spec != null ? spec.channels > 1 : false diff --git a/app/src/organisms/Desktop/CalibrateTipLength/AskForCalibrationBlockModal.tsx b/app/src/organisms/Desktop/CalibrateTipLength/AskForCalibrationBlockModal.tsx index 62a8ca00ef0..85eb76ae126 100644 --- a/app/src/organisms/Desktop/CalibrateTipLength/AskForCalibrationBlockModal.tsx +++ b/app/src/organisms/Desktop/CalibrateTipLength/AskForCalibrationBlockModal.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { Trans, useTranslation } from 'react-i18next' import { @@ -24,6 +24,7 @@ import { WizardHeader } from '/app/molecules/WizardHeader' import { getTopPortalEl } from '/app/App/portal' import { setUseTrashSurfaceForTipCal } from '/app/redux/calibration' +import type { ChangeEvent } from 'react' import type { Dispatch } from '/app/redux/types' const BLOCK_REQUEST_EMAIL_BODY = @@ -41,9 +42,7 @@ interface Props { export function AskForCalibrationBlockModal(props: Props): JSX.Element { const { t } = useTranslation(['robot_calibration', 'shared', 'branded']) - const [rememberPreference, setRememberPreference] = React.useState( - true - ) + const [rememberPreference, setRememberPreference] = useState(true) const dispatch = useDispatch() const makeSetHasBlock = (hasBlock: boolean) => (): void => { @@ -108,7 +107,7 @@ export function AskForCalibrationBlockModal(props: Props): JSX.Element { > ) => { + onChange={(e: ChangeEvent) => { setRememberPreference(e.currentTarget.checked) }} value={rememberPreference} diff --git a/app/src/organisms/Desktop/CalibrateTipLength/index.tsx b/app/src/organisms/Desktop/CalibrateTipLength/index.tsx index 02ac74e09d5..e905f1b77c7 100644 --- a/app/src/organisms/Desktop/CalibrateTipLength/index.tsx +++ b/app/src/organisms/Desktop/CalibrateTipLength/index.tsx @@ -1,5 +1,5 @@ // Tip Length Calibration Orchestration Component -import * as React from 'react' +import { useMemo } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { useQueryClient } from 'react-query' @@ -30,6 +30,7 @@ import { import slotOneRemoveBlockAsset from '/app/assets/videos/tip-length-cal/Slot_1_Remove_CalBlock_(330x260)REV1.webm' import slotThreeRemoveBlockAsset from '/app/assets/videos/tip-length-cal/Slot_3_Remove_CalBlock_(330x260)REV1.webm' +import type { ComponentType } from 'react' import type { Mount } from '@opentrons/components' import type { SessionCommandParams, @@ -43,7 +44,7 @@ export { AskForCalibrationBlockModal } from './AskForCalibrationBlockModal' export { ConfirmRecalibrationModal } from './ConfirmRecalibrationModal' const PANEL_BY_STEP: Partial< - Record> + Record> > = { sessionStarted: Introduction, labwareLoaded: DeckSetup, @@ -80,7 +81,7 @@ export function CalibrateTipLength({ const queryClient = useQueryClient() const host = useHost() - const isMulti = React.useMemo(() => { + const isMulti = useMemo(() => { const spec = instrument != null ? getPipetteModelSpecs(instrument.model) : null return spec != null ? spec.channels > 1 : false diff --git a/app/src/organisms/Desktop/CalibrationPanels/SaveZPoint.tsx b/app/src/organisms/Desktop/CalibrationPanels/SaveZPoint.tsx index 7c85aa4b020..094b94a2025 100644 --- a/app/src/organisms/Desktop/CalibrationPanels/SaveZPoint.tsx +++ b/app/src/organisms/Desktop/CalibrationPanels/SaveZPoint.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useMemo } from 'react' import { css } from 'styled-components' import { Trans, useTranslation } from 'react-i18next' import { @@ -29,6 +29,7 @@ import slot5LeftSingleDemoAsset from '/app/assets/videos/cal-movement/SLOT_5_LEF import slot5RightMultiDemoAsset from '/app/assets/videos/cal-movement/SLOT_5_RIGHT_MULTI_Z.webm' import slot5RightSingleDemoAsset from '/app/assets/videos/cal-movement/SLOT_5_RIGHT_SINGLE_Z.webm' +import type { MouseEventHandler } from 'react' import type { Axis, Sign, StepSize } from '/app/molecules/JogControls/types' import type { CalibrationPanelProps } from './types' @@ -46,7 +47,7 @@ const assetMap = { export function SaveZPoint(props: CalibrationPanelProps): JSX.Element { const { t } = useTranslation('robot_calibration') const { isMulti, mount, sendCommands, sessionType } = props - const demoAsset = React.useMemo( + const demoAsset = useMemo( () => mount && assetMap[mount][isMulti ? 'multi' : 'single'], [mount, isMulti] ) @@ -62,7 +63,7 @@ export function SaveZPoint(props: CalibrationPanelProps): JSX.Element { const isHealthCheck = sessionType === Sessions.SESSION_TYPE_CALIBRATION_HEALTH_CHECK - const proceed: React.MouseEventHandler = _event => { + const proceed: MouseEventHandler = _event => { isHealthCheck ? sendCommands( { command: Sessions.checkCommands.COMPARE_POINT }, diff --git a/app/src/organisms/Desktop/CheckCalibration/index.tsx b/app/src/organisms/Desktop/CheckCalibration/index.tsx index f2dae1c242e..10bf25e93f9 100644 --- a/app/src/organisms/Desktop/CheckCalibration/index.tsx +++ b/app/src/organisms/Desktop/CheckCalibration/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useMemo } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' @@ -22,7 +22,9 @@ import { WizardHeader } from '/app/molecules/WizardHeader' import { getTopPortalEl } from '/app/App/portal' import { ReturnTip } from './ReturnTip' import { ResultsSummary } from './ResultsSummary' +import { CHECK_PIPETTE_RANK_FIRST } from '/app/redux/sessions' +import type { ComponentType } from 'react' import type { Mount } from '@opentrons/components' import type { CalibrationLabware, @@ -30,15 +32,13 @@ import type { RobotCalibrationCheckStep, SessionCommandParams, } from '/app/redux/sessions/types' - import type { CalibrationPanelProps } from '/app/organisms/Desktop/CalibrationPanels/types' import type { CalibrationCheckParentProps } from './types' -import { CHECK_PIPETTE_RANK_FIRST } from '/app/redux/sessions' const ROBOT_CALIBRATION_CHECK_SUBTITLE = 'Calibration health check' const PANEL_BY_STEP: { - [step in RobotCalibrationCheckStep]?: React.ComponentType + [step in RobotCalibrationCheckStep]?: ComponentType } = { [Sessions.CHECK_STEP_SESSION_STARTED]: Introduction, [Sessions.CHECK_STEP_LABWARE_LOADED]: DeckSetup, @@ -124,7 +124,7 @@ export function CheckCalibration( cleanUpAndExit() }, true) - const isMulti = React.useMemo(() => { + const isMulti = useMemo(() => { const spec = activePipette && getPipetteModelSpecs(activePipette.model) return spec ? spec.channels > 1 : false }, [activePipette]) diff --git a/app/src/organisms/Desktop/ChooseProtocolSlideout/index.tsx b/app/src/organisms/Desktop/ChooseProtocolSlideout/index.tsx index a4824c76c5c..5fa0536eb60 100644 --- a/app/src/organisms/Desktop/ChooseProtocolSlideout/index.tsx +++ b/app/src/organisms/Desktop/ChooseProtocolSlideout/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect, useState, Fragment } from 'react' import first from 'lodash/first' import { Trans, useTranslation } from 'react-i18next' import { Link, NavLink, useNavigate } from 'react-router-dom' @@ -62,6 +62,7 @@ import { } from '/app/transformations/runs' import { getAnalysisStatus } from '/app/organisms/Desktop/ProtocolsLanding/utils' +import type { MouseEventHandler } from 'react' import type { DropdownOption } from '@opentrons/components' import type { RunTimeParameter } from '@opentrons/shared-data' import type { Robot } from '/app/redux/discovery/types' @@ -100,7 +101,7 @@ export function ChooseProtocolSlideoutComponent( const [ showRestoreValuesTooltip, setShowRestoreValuesTooltip, - ] = React.useState(false) + ] = useState(false) const { robot, showSlideout, onCloseClick } = props const { name } = robot @@ -108,26 +109,25 @@ export function ChooseProtocolSlideoutComponent( const [ selectedProtocol, setSelectedProtocol, - ] = React.useState(null) - const [ - runTimeParametersOverrides, - setRunTimeParametersOverrides, - ] = React.useState([]) - const [currentPage, setCurrentPage] = React.useState(1) - const [hasParamError, setHasParamError] = React.useState(false) - const [hasMissingFileParam, setHasMissingFileParam] = React.useState( + ] = useState(null) + const [runTimeParametersOverrides, setRunTimeParametersOverrides] = useState< + RunTimeParameter[] + >([]) + const [currentPage, setCurrentPage] = useState(1) + const [hasParamError, setHasParamError] = useState(false) + const [hasMissingFileParam, setHasMissingFileParam] = useState( runTimeParametersOverrides?.some( parameter => parameter.type === 'csv_file' ) ?? false ) - const [isInputFocused, setIsInputFocused] = React.useState(false) + const [isInputFocused, setIsInputFocused] = useState(false) - React.useEffect(() => { + useEffect(() => { setRunTimeParametersOverrides( selectedProtocol?.mostRecentAnalysis?.runTimeParameters ?? [] ) }, [selectedProtocol]) - React.useEffect(() => { + useEffect(() => { setHasParamError(errors.length > 0) setHasMissingFileParam( runTimeParametersOverrides.some( @@ -149,7 +149,7 @@ export function ChooseProtocolSlideoutComponent( const missingAnalysisData = analysisStatus === 'error' || analysisStatus === 'stale' - const [shouldApplyOffsets, setShouldApplyOffsets] = React.useState(true) + const [shouldApplyOffsets, setShouldApplyOffsets] = useState(true) const offsetCandidates = useOffsetCandidatesForAnalysis( (!missingAnalysisData ? selectedProtocol?.mostRecentAnalysis : null) ?? null, @@ -211,7 +211,7 @@ export function ChooseProtocolSlideoutComponent( })) : [] ) - const handleProceed: React.MouseEventHandler = () => { + const handleProceed: MouseEventHandler = () => { if (selectedProtocol != null) { trackCreateProtocolRunEvent({ name: 'createProtocolRecordRequest' }) const dataFilesForProtocolMap = runTimeParametersOverrides.reduce< @@ -725,7 +725,7 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { ).filter( protocol => protocol.mostRecentAnalysis?.robotType === robot.robotModel ) - React.useEffect(() => { + useEffect(() => { handleSelectProtocol(first(storedProtocols) ?? null) }, []) @@ -744,7 +744,7 @@ function StoredProtocolList(props: StoredProtocolListProps): JSX.Element { const requiresCsvRunTimeParameter = analysisStatus === 'parameterRequired' return ( - + ) : null} - + ) })} diff --git a/app/src/organisms/Desktop/ChooseRobotSlideout/AvailableRobotOption.tsx b/app/src/organisms/Desktop/ChooseRobotSlideout/AvailableRobotOption.tsx index f06464c2f2a..992e25706dc 100644 --- a/app/src/organisms/Desktop/ChooseRobotSlideout/AvailableRobotOption.tsx +++ b/app/src/organisms/Desktop/ChooseRobotSlideout/AvailableRobotOption.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { css } from 'styled-components' import { Trans, useTranslation } from 'react-i18next' @@ -24,6 +24,7 @@ import OT2_PNG from '/app/assets/images/OT2-R_HERO.png' import FLEX_PNG from '/app/assets/images/FLEX.png' import { useCurrentRunId, useNotifyRunQuery } from '/app/resources/runs' +import type { Dispatch as ReactDispatch } from 'react' import type { IconName } from '@opentrons/components' import type { Runs } from '@opentrons/api-client' import type { Robot } from '/app/redux/discovery/types' @@ -35,7 +36,7 @@ interface AvailableRobotOptionProps { onClick: () => void isSelected: boolean isSelectedRobotOnDifferentSoftwareVersion: boolean - registerRobotBusyStatus: React.Dispatch + registerRobotBusyStatus: ReactDispatch isError?: boolean showIdleOnly?: boolean } @@ -59,7 +60,7 @@ export function AvailableRobotOption( getRobotModelByName(state, robotName) ) - const [isBusy, setIsBusy] = React.useState(true) + const [isBusy, setIsBusy] = useState(true) const currentRunId = useCurrentRunId( { @@ -112,7 +113,7 @@ export function AvailableRobotOption( iconName = 'usb' } - React.useEffect(() => { + useEffect(() => { dispatch(fetchStatus(robotName)) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) diff --git a/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/index.tsx b/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/index.tsx index f4850a649cf..e3946a4cc99 100644 --- a/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/index.tsx +++ b/app/src/organisms/Desktop/ChooseRobotToRunProtocolSlideout/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import first from 'lodash/first' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' @@ -33,6 +33,8 @@ import { ApplyHistoricOffsets } from '/app/organisms/ApplyHistoricOffsets' import { useOffsetCandidatesForAnalysis } from '/app/organisms/ApplyHistoricOffsets/hooks/useOffsetCandidatesForAnalysis' import { ChooseRobotSlideout } from '../ChooseRobotSlideout' import { useCreateRunFromProtocol } from './useCreateRunFromProtocol' + +import type { MouseEventHandler } from 'react' import type { StyleProps } from '@opentrons/components' import type { RunTimeParameter } from '@opentrons/shared-data' import type { Robot } from '/app/redux/discovery/types' @@ -65,16 +67,14 @@ export function ChooseRobotToRunProtocolSlideoutComponent( setSelectedRobot, } = props const navigate = useNavigate() - const [shouldApplyOffsets, setShouldApplyOffsets] = React.useState( - true - ) + const [shouldApplyOffsets, setShouldApplyOffsets] = useState(true) const { protocolKey, srcFileNames, srcFiles, mostRecentAnalysis, } = storedProtocolData - const [currentPage, setCurrentPage] = React.useState(1) + const [currentPage, setCurrentPage] = useState(1) const { trackCreateProtocolRunEvent } = useTrackCreateProtocolRunEvent( storedProtocolData, selectedRobot?.name ?? '' @@ -82,12 +82,11 @@ export function ChooseRobotToRunProtocolSlideoutComponent( const runTimeParameters = storedProtocolData.mostRecentAnalysis?.runTimeParameters ?? [] - const [ - runTimeParametersOverrides, - setRunTimeParametersOverrides, - ] = React.useState(runTimeParameters) - const [hasParamError, setHasParamError] = React.useState(false) - const [hasMissingFileParam, setHasMissingFileParam] = React.useState( + const [runTimeParametersOverrides, setRunTimeParametersOverrides] = useState< + RunTimeParameter[] + >(runTimeParameters) + const [hasParamError, setHasParamError] = useState(false) + const [hasMissingFileParam, setHasMissingFileParam] = useState( runTimeParameters?.some(parameter => parameter.type === 'csv_file') ?? false ) @@ -133,7 +132,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent( })) : [] ) - const handleProceed: React.MouseEventHandler = () => { + const handleProceed: MouseEventHandler = () => { trackCreateProtocolRunEvent({ name: 'createProtocolRecordRequest' }) const dataFilesForProtocolMap = runTimeParametersOverrides.reduce< Record @@ -353,7 +352,7 @@ export function ChooseRobotToRunProtocolSlideoutComponent( export function ChooseRobotToRunProtocolSlideout( props: ChooseRobotToRunProtocolSlideoutProps ): JSX.Element | null { - const [selectedRobot, setSelectedRobot] = React.useState(null) + const [selectedRobot, setSelectedRobot] = useState(null) return ( void } @@ -26,7 +28,7 @@ export function CheckPipettesButton( ): JSX.Element | null { const { onDone, children, direction } = props const { t } = useTranslation('change_pipette') - const [isPending, setIsPending] = React.useState(false) + const [isPending, setIsPending] = useState(false) const { refetch: refetchPipettes } = usePipettesQuery( { refresh: true }, { diff --git a/app/src/organisms/Desktop/Devices/GripperCard/index.tsx b/app/src/organisms/Desktop/Devices/GripperCard/index.tsx index c8a5280049e..0d425640c11 100644 --- a/app/src/organisms/Desktop/Devices/GripperCard/index.tsx +++ b/app/src/organisms/Desktop/Devices/GripperCard/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { Trans, useTranslation } from 'react-i18next' import { css } from 'styled-components' import { @@ -15,6 +15,7 @@ import { GripperWizardFlows } from '/app/organisms/GripperWizardFlows' import { AboutGripperSlideout } from './AboutGripperSlideout' import { GRIPPER_FLOW_TYPES } from '/app/organisms/GripperWizardFlows/constants' +import type { MouseEventHandler } from 'react' import type { BadGripper, GripperData } from '@opentrons/api-client' import type { GripperModel } from '@opentrons/shared-data' import type { GripperWizardFlowType } from '/app/organisms/GripperWizardFlows/types' @@ -53,26 +54,24 @@ export function GripperCard({ const [ openWizardFlowType, setOpenWizardFlowType, - ] = React.useState(null) + ] = useState(null) const [ showAboutGripperSlideout, setShowAboutGripperSlideout, - ] = React.useState(false) + ] = useState(false) - const handleAttach: React.MouseEventHandler = () => { + const handleAttach: MouseEventHandler = () => { setOpenWizardFlowType(GRIPPER_FLOW_TYPES.ATTACH) } - const handleDetach: React.MouseEventHandler = () => { + const handleDetach: MouseEventHandler = () => { setOpenWizardFlowType(GRIPPER_FLOW_TYPES.DETACH) } - const handleCalibrate: React.MouseEventHandler = () => { + const handleCalibrate: MouseEventHandler = () => { setOpenWizardFlowType(GRIPPER_FLOW_TYPES.RECALIBRATE) } - const [pollForSubsystemUpdate, setPollForSubsystemUpdate] = React.useState( - false - ) + const [pollForSubsystemUpdate, setPollForSubsystemUpdate] = useState(false) const { data: subsystemUpdateData } = useCurrentSubsystemUpdateQuery( 'gripper', { @@ -84,7 +83,7 @@ export function GripperCard({ // detected until the update has been done for 5 seconds // this gives the instruments endpoint time to start reporting // a good instrument - React.useEffect(() => { + useEffect(() => { if (attachedGripper?.ok === false) { setPollForSubsystemUpdate(true) } else if ( diff --git a/app/src/organisms/Desktop/Devices/PipetteCard/FlexPipetteCard.tsx b/app/src/organisms/Desktop/Devices/PipetteCard/FlexPipetteCard.tsx index 3a1354f7680..50848a92d0c 100644 --- a/app/src/organisms/Desktop/Devices/PipetteCard/FlexPipetteCard.tsx +++ b/app/src/organisms/Desktop/Devices/PipetteCard/FlexPipetteCard.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { Trans, useTranslation } from 'react-i18next' import { css } from 'styled-components' import { @@ -29,6 +29,7 @@ import { import { AboutPipetteSlideout } from './AboutPipetteSlideout' +import type { MouseEventHandler } from 'react' import type { BadPipette, HostConfig, @@ -79,12 +80,11 @@ export function FlexPipetteCard({ const [ showAboutPipetteSlideout, setShowAboutPipetteSlideout, - ] = React.useState(false) - const [showChoosePipette, setShowChoosePipette] = React.useState(false) - const [ - selectedPipette, - setSelectedPipette, - ] = React.useState(SINGLE_MOUNT_PIPETTES) + ] = useState(false) + const [showChoosePipette, setShowChoosePipette] = useState(false) + const [selectedPipette, setSelectedPipette] = useState( + SINGLE_MOUNT_PIPETTES + ) const attachedPipetteIs96Channel = attachedPipette?.ok && attachedPipette.instrumentName === 'p1000_96' const selectedPipetteForWizard = attachedPipetteIs96Channel @@ -107,7 +107,7 @@ export function FlexPipetteCard({ host, }) } - const handleChoosePipette: React.MouseEventHandler = () => { + const handleChoosePipette: MouseEventHandler = () => { setShowChoosePipette(true) } const handleAttach = (): void => { @@ -115,17 +115,15 @@ export function FlexPipetteCard({ handleLaunchPipetteWizardFlows(FLOWS.ATTACH) } - const handleDetach: React.MouseEventHandler = () => { + const handleDetach: MouseEventHandler = () => { handleLaunchPipetteWizardFlows(FLOWS.DETACH) } - const handleCalibrate: React.MouseEventHandler = () => { + const handleCalibrate: MouseEventHandler = () => { handleLaunchPipetteWizardFlows(FLOWS.CALIBRATE) } - const [pollForSubsystemUpdate, setPollForSubsystemUpdate] = React.useState( - false - ) + const [pollForSubsystemUpdate, setPollForSubsystemUpdate] = useState(false) const subsystem = attachedPipette?.subsystem ?? null const { data: subsystemUpdateData } = useCurrentSubsystemUpdateQuery( subsystem, @@ -139,7 +137,7 @@ export function FlexPipetteCard({ // detected until the update has been done for 5 seconds // this gives the instruments endpoint time to start reporting // a good instrument - React.useEffect(() => { + useEffect(() => { if (attachedPipette?.ok === false) { setPollForSubsystemUpdate(true) } else if ( diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/ProtocolAnalysisErrorBanner.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/ProtocolAnalysisErrorBanner.tsx index 18a8f0e682a..753ed296d86 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/ProtocolAnalysisErrorBanner.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderBannerContainer/ProtocolAnalysisErrorBanner.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { Trans, useTranslation } from 'react-i18next' @@ -18,6 +18,7 @@ import { import { getTopPortalEl } from '/app/App/portal' +import type { MouseEventHandler } from 'react' import type { AnalysisError } from '@opentrons/shared-data' interface ProtocolAnalysisErrorBannerProps { @@ -29,9 +30,9 @@ export function ProtocolAnalysisErrorBanner( ): JSX.Element { const { errors } = props const { t } = useTranslation(['run_details']) - const [showErrorDetails, setShowErrorDetails] = React.useState(false) + const [showErrorDetails, setShowErrorDetails] = useState(false) - const handleToggleDetails: React.MouseEventHandler = e => { + const handleToggleDetails: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() setShowErrorDetails(!showErrorDetails) diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmCancelModal.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmCancelModal.tsx index 083b7f752fc..770217dad82 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmCancelModal.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/ConfirmCancelModal.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' @@ -27,6 +27,7 @@ import { useTrackProtocolRunEvent } from '/app/redux-resources/analytics' import { useIsFlex } from '/app/redux-resources/robots' import { ANALYTICS_PROTOCOL_RUN_ACTION } from '/app/redux/analytics' +import type { MouseEventHandler } from 'react' import type { RunStatus } from '@opentrons/api-client' export interface UseConfirmCancelModalResult { @@ -35,7 +36,7 @@ export interface UseConfirmCancelModalResult { } export function useConfirmCancelModal(): UseConfirmCancelModalResult { - const [showModal, setShowModal] = React.useState(false) + const [showModal, setShowModal] = useState(false) const toggleModal = (): void => { setShowModal(!showModal) @@ -58,14 +59,14 @@ export function ConfirmCancelModal( const { stopRun } = useStopRunMutation() const isFlex = useIsFlex(robotName) const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId, robotName) - const [isCanceling, setIsCanceling] = React.useState(false) + const [isCanceling, setIsCanceling] = useState(false) const { t } = useTranslation('run_details') const cancelRunAlertInfo = isFlex ? t('cancel_run_alert_info_flex') : t('cancel_run_alert_info_ot2') - const cancelRun: React.MouseEventHandler = (e): void => { + const cancelRun: MouseEventHandler = (e): void => { e.preventDefault() e.stopPropagation() setIsCanceling(true) @@ -78,7 +79,8 @@ export function ConfirmCancelModal( }, }) } - React.useEffect(() => { + + useEffect(() => { if ( runStatus === RUN_STATUS_STOP_REQUESTED || runStatus === RUN_STATUS_STOPPED diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/RunFailedModal.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/RunFailedModal.tsx index 77041e48c18..33d7949a449 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/RunFailedModal.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/RunHeaderModalContainer/modals/RunFailedModal.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' @@ -25,6 +25,7 @@ import { import { useDownloadRunLog } from '../../../../hooks' import { RUN_STATUS_SUCCEEDED } from '@opentrons/api-client' +import type { MouseEventHandler } from 'react' import type { RunStatus } from '@opentrons/api-client' import type { ModalProps } from '@opentrons/components' import type { RunCommandError } from '@opentrons/shared-data' @@ -41,7 +42,7 @@ export interface UseRunFailedModalResult { export function useRunFailedModal( runErrors: UseRunErrorsResult ): UseRunFailedModalResult { - const [showRunFailedModal, setShowRunFailedModal] = React.useState(false) + const [showRunFailedModal, setShowRunFailedModal] = useState(false) const toggleModal = (): void => { setShowRunFailedModal(!showRunFailedModal) @@ -95,7 +96,7 @@ export function RunFailedModal({ toggleModal() } - const handleDownloadClick: React.MouseEventHandler = e => { + const handleDownloadClick: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() downloadRunLog() diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/index.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/index.tsx index 40375135db9..fa13d31487b 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/index.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunHeader/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { css } from 'styled-components' @@ -30,8 +30,10 @@ import { RunHeaderContent } from './RunHeaderContent' import { EQUIPMENT_POLL_MS } from './constants' import { isCancellableStatus } from './utils' +import type { RefObject } from 'react' + export interface ProtocolRunHeaderProps { - protocolRunHeaderRef: React.RefObject | null + protocolRunHeaderRef: RefObject | null robotName: string runId: string makeHandleJumpToStep: (index: number) => () => void @@ -70,7 +72,7 @@ export function ProtocolRunHeader( runErrors, }) - React.useEffect(() => { + useEffect(() => { if (protocolData != null && !isRobotViewable) { navigate('/devices') } @@ -78,7 +80,7 @@ export function ProtocolRunHeader( // To persist "run again" loading conditions into a new run, we need a scalar that persists longer than // the runControl isResetRunLoading, which completes before we want to change user-facing copy/CTAs. - const isResetRunLoadingRef = React.useRef(false) + const isResetRunLoadingRef = useRef(false) if (runStatus === RUN_STATUS_IDLE || runStatus === RUN_STATUS_RUNNING) { isResetRunLoadingRef.current = false } diff --git a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx index 8e10948795a..c1382acb777 100644 --- a/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx +++ b/app/src/organisms/Desktop/Devices/ProtocolRun/ProtocolRunSetup.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' @@ -60,11 +60,12 @@ import { SetupLiquids } from './SetupLiquids' import { EmptySetupStep } from './EmptySetupStep' import { HowLPCWorksModal } from './SetupLabwarePositionCheck/HowLPCWorksModal' +import type { RefObject } from 'react' import type { Dispatch, State } from '/app/redux/types' import type { StepKey } from '/app/redux/protocol-runs' interface ProtocolRunSetupProps { - protocolRunHeaderRef: React.RefObject | null + protocolRunHeaderRef: RefObject | null robotName: string runId: string } @@ -92,9 +93,7 @@ export function ProtocolRunSetup({ const isFlex = useIsFlex(robotName) const runHasStarted = useRunHasStarted(runId) const { analysisErrors } = useProtocolAnalysisErrors(runId) - const [expandedStepKey, setExpandedStepKey] = React.useState( - null - ) + const [expandedStepKey, setExpandedStepKey] = useState(null) const robotType = isFlex ? FLEX_ROBOT_TYPE : OT2_ROBOT_TYPE const deckConfigCompatibility = useDeckConfigurationCompatibility( robotType, @@ -481,14 +480,14 @@ function StepRightElement(props: StepRightElementProps): JSX.Element | null { function LearnAboutLPC(): JSX.Element { const { t } = useTranslation('protocol_setup') - const [showLPCHelpModal, setShowLPCHelpModal] = React.useState(false) + const [showLPCHelpModal, setShowLPCHelpModal] = useState(false) return ( <> { + onClick={(e: MouseEvent) => { // clicking link shouldn't toggle step expanded state e.preventDefault() e.stopPropagation() diff --git a/app/src/organisms/Desktop/Devices/RobotOverflowMenu.tsx b/app/src/organisms/Desktop/Devices/RobotOverflowMenu.tsx index 287c2ff032f..d2f5f3e8a82 100644 --- a/app/src/organisms/Desktop/Devices/RobotOverflowMenu.tsx +++ b/app/src/organisms/Desktop/Devices/RobotOverflowMenu.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' @@ -31,6 +31,7 @@ import { useCurrentRunId } from '/app/resources/runs' import { ConnectionTroubleshootingModal } from './ConnectionTroubleshootingModal' import { useIsRobotBusy } from '/app/redux-resources/robots' +import type { MouseEventHandler, MouseEvent, ReactNode } from 'react' import type { StyleProps } from '@opentrons/components' import type { DiscoveredRobot } from '/app/redux/discovery/types' import type { Dispatch } from '/app/redux/types' @@ -54,11 +55,11 @@ export function RobotOverflowMenu(props: RobotOverflowMenuProps): JSX.Element { const [ showChooseProtocolSlideout, setShowChooseProtocolSlideout, - ] = React.useState(false) + ] = useState(false) const [ showConnectionTroubleshootingModal, setShowConnectionTroubleshootingModal, - ] = React.useState(false) + ] = useState(false) const isRobotOnWrongVersionOfSoftware = useIsRobotOnWrongVersionOfSoftware( robot.name @@ -66,20 +67,20 @@ export function RobotOverflowMenu(props: RobotOverflowMenuProps): JSX.Element { const isRobotBusy = useIsRobotBusy({ poll: true }) - const handleClickRun: React.MouseEventHandler = e => { + const handleClickRun: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() setShowChooseProtocolSlideout(true) setShowOverflowMenu(false) } - const handleClickConnectionTroubleshooting: React.MouseEventHandler = e => { + const handleClickConnectionTroubleshooting: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() setShowConnectionTroubleshootingModal(true) setShowOverflowMenu(false) } - let menuItems: React.ReactNode + let menuItems: ReactNode if (robot.status === CONNECTABLE && runId == null) { menuItems = ( <> @@ -161,7 +162,7 @@ export function RobotOverflowMenu(props: RobotOverflowMenuProps): JSX.Element { data-testid={`RobotCard_${String(robot.name)}_overflowMenu`} flexDirection={DIRECTION_COLUMN} position={POSITION_RELATIVE} - onClick={(e: React.MouseEvent) => { + onClick={(e: MouseEvent) => { e.stopPropagation() }} {...styleProps} diff --git a/app/src/organisms/Desktop/Devices/RobotOverviewOverflowMenu.tsx b/app/src/organisms/Desktop/Devices/RobotOverviewOverflowMenu.tsx index 3b6dda678ca..5ff5ac357b6 100644 --- a/app/src/organisms/Desktop/Devices/RobotOverviewOverflowMenu.tsx +++ b/app/src/organisms/Desktop/Devices/RobotOverviewOverflowMenu.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { css } from 'styled-components' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' @@ -35,6 +35,8 @@ import { useIsRobotBusy } from '/app/redux-resources/robots' import { useCanDisconnect } from '/app/resources/networking/hooks' import { useIsEstopNotDisengaged } from '/app/resources/devices/hooks/useIsEstopNotDisengaged' import { useCurrentRunId } from '/app/resources/runs' + +import type { MouseEventHandler, MouseEvent } from 'react' import type { DiscoveredRobot } from '/app/redux/discovery/types' import type { Dispatch } from '/app/redux/types' @@ -61,25 +63,23 @@ export const RobotOverviewOverflowMenu = ( const dispatch = useDispatch() - const handleClickRestart: React.MouseEventHandler = () => { + const handleClickRestart: MouseEventHandler = () => { dispatch(restartRobot(robot.name)) } - const handleClickHomeGantry: React.MouseEventHandler = () => { + const handleClickHomeGantry: MouseEventHandler = () => { dispatch(home(robot.name, ROBOT)) } const [ showChooseProtocolSlideout, setShowChooseProtocolSlideout, - ] = React.useState(false) - const [showDisconnectModal, setShowDisconnectModal] = React.useState( - false - ) + ] = useState(false) + const [showDisconnectModal, setShowDisconnectModal] = useState(false) const canDisconnect = useCanDisconnect(robot.name) - const handleClickDisconnect: React.MouseEventHandler = () => { + const handleClickDisconnect: MouseEventHandler = () => { setShowDisconnectModal(true) } @@ -87,7 +87,7 @@ export const RobotOverviewOverflowMenu = ( dispatch(checkShellUpdate()) }) - const handleClickRun: React.MouseEventHandler = () => { + const handleClickRun: MouseEventHandler = () => { setShowChooseProtocolSlideout(true) } @@ -125,7 +125,7 @@ export const RobotOverviewOverflowMenu = ( top="2.25rem" right={0} flexDirection={DIRECTION_COLUMN} - onClick={(e: React.MouseEvent) => { + onClick={(e: MouseEvent) => { e.preventDefault() e.stopPropagation() setShowOverflowMenu(false) diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx index d4ddba6e764..763ca9f0cb2 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/DeviceResetSlideout.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { useSelector, useDispatch } from 'react-redux' import { useTranslation } from 'react-i18next' import snakeCase from 'lodash/snakeCase' @@ -40,6 +40,7 @@ import { import { useRobot, useIsFlex } from '/app/redux-resources/robots' import { useNotifyAllRunsQuery } from '/app/resources/runs' +import type { MouseEventHandler } from 'react' import type { State, Dispatch } from '/app/redux/types' import type { ResetConfigRequest } from '/app/redux/robot-admin/types' @@ -61,7 +62,7 @@ export function DeviceResetSlideout({ const doTrackEvent = useTrackEvent() const robot = useRobot(robotName) const dispatch = useDispatch() - const [resetOptions, setResetOptions] = React.useState({}) + const [resetOptions, setResetOptions] = useState({}) const runsQueryResponse = useNotifyAllRunsQuery() const isFlex = useIsFlex(robotName) @@ -98,11 +99,11 @@ export function DeviceResetSlideout({ ? options.filter(opt => opt.id.includes('authorizedKeys')) : [] - React.useEffect(() => { + useEffect(() => { dispatch(fetchResetConfigOptions(robotName)) }, [dispatch, robotName]) - const downloadCalibrationLogs: React.MouseEventHandler = e => { + const downloadCalibrationLogs: MouseEventHandler = e => { e.preventDefault() doTrackEvent({ name: ANALYTICS_CALIBRATION_DATA_DOWNLOADED, @@ -120,7 +121,7 @@ export function DeviceResetSlideout({ ) } - const downloadRunHistoryLogs: React.MouseEventHandler = e => { + const downloadRunHistoryLogs: MouseEventHandler = e => { e.preventDefault() const runsHistory = runsQueryResponse != null ? runsQueryResponse.data?.data : [] diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/FactoryModeSlideout.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/FactoryModeSlideout.tsx index 1472204ce8f..ff083c9b3e5 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/FactoryModeSlideout.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/FactoryModeSlideout.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { useDispatch } from 'react-redux' import { useForm, Controller } from 'react-hook-form' import { Trans, useTranslation } from 'react-i18next' @@ -28,6 +28,7 @@ import { FileUpload } from '/app/molecules/FileUpload' import { UploadInput } from '/app/molecules/UploadInput' import { restartRobot } from '/app/redux/robot-admin' +import type { ChangeEvent, MouseEventHandler } from 'react' import type { FieldError, Resolver } from 'react-hook-form' import type { RobotSettingsField } from '@opentrons/api-client' import type { Dispatch } from '/app/redux/types' @@ -63,11 +64,11 @@ export function FactoryModeSlideout({ const last = sn?.substring(sn.length - 4) - const [currentStep, setCurrentStep] = React.useState(1) - const [toggleValue, setToggleValue] = React.useState(false) - const [file, setFile] = React.useState(null) - const [fileError, setFileError] = React.useState(null) - const [isUploading, setIsUploading] = React.useState(false) + const [currentStep, setCurrentStep] = useState(1) + const [toggleValue, setToggleValue] = useState(false) + const [file, setFile] = useState(null) + const [fileError, setFileError] = useState(null) + const [isUploading, setIsUploading] = useState(false) const onFinishCompleteClick = (): void => { dispatch(restartRobot(robotName)) @@ -142,11 +143,11 @@ export function FactoryModeSlideout({ void handleSubmit(onSubmit)() } - const handleToggleClick: React.MouseEventHandler = () => { + const handleToggleClick: MouseEventHandler = () => { setToggleValue(toggleValue => !toggleValue) } - const handleCompleteClick: React.MouseEventHandler = () => { + const handleCompleteClick: MouseEventHandler = () => { setIsUploading(true) updateRobotSetting({ id: 'enableOEMMode', value: toggleValue }) } @@ -173,7 +174,7 @@ export function FactoryModeSlideout({ } } - React.useEffect(() => { + useEffect(() => { // initialize local state to OEM mode value if (isOEMMode != null) { setToggleValue(isOEMMode) @@ -229,7 +230,7 @@ export function FactoryModeSlideout({ id="factoryModeInput" name="factoryModeInput" type="text" - onChange={(e: React.ChangeEvent) => { + onChange={(e: ChangeEvent) => { field.onChange(e) clearErrors() }} diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx index 5c02dc8eb95..55d9806a88f 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/AdvancedTabSlideouts/RenameRobotSlideout.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useSelector, useDispatch } from 'react-redux' import { useNavigate } from 'react-router-dom' import { useForm, Controller } from 'react-hook-form' @@ -24,9 +24,11 @@ import { useTrackEvent, ANALYTICS_RENAME_ROBOT } from '/app/redux/analytics' import { Slideout } from '/app/atoms/Slideout' import { useIsFlex } from '/app/redux-resources/robots' +import type { ChangeEvent } from 'react' import type { Resolver, FieldError } from 'react-hook-form' import type { UpdatedRobotName } from '@opentrons/api-client' import type { State, Dispatch } from '/app/redux/types' + interface RenameRobotSlideoutProps { isExpanded: boolean onCloseClick: () => void @@ -49,9 +51,7 @@ export function RenameRobotSlideout({ robotName, }: RenameRobotSlideoutProps): JSX.Element { const { t } = useTranslation('device_settings') - const [previousRobotName, setPreviousRobotName] = React.useState( - robotName - ) + const [previousRobotName, setPreviousRobotName] = useState(robotName) const isFlex = useIsFlex(robotName) const trackEvent = useTrackEvent() const navigate = useNavigate() @@ -190,7 +190,7 @@ export function RenameRobotSlideout({ id="newRobotName" name="newRobotName" type="text" - onChange={(e: React.ChangeEvent) => { + onChange={(e: ChangeEvent) => { field.onChange(e) trigger('newRobotName') }} diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/Troubleshooting.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/Troubleshooting.tsx index 45b2b96f462..a216c71cbd4 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/Troubleshooting.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/AdvancedTab/Troubleshooting.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { saveAs } from 'file-saver' import JSZip from 'jszip' @@ -25,6 +25,7 @@ import { useToaster } from '/app/organisms/ToasterOven' import { CONNECTABLE } from '/app/redux/discovery' import { useRobot } from '/app/redux-resources/robots' +import type { MouseEventHandler } from 'react' import type { IconProps } from '@opentrons/components' interface TroubleshootingProps { @@ -38,16 +39,15 @@ export function Troubleshooting({ const robot = useRobot(robotName) const controlDisabled = robot?.status !== CONNECTABLE const logsAvailable = robot?.health?.logs != null - const [ - isDownloadingRobotLogs, - setIsDownloadingRobotLogs, - ] = React.useState(false) + const [isDownloadingRobotLogs, setIsDownloadingRobotLogs] = useState( + false + ) const { makeToast, eatToast } = useToaster() const toastIcon: IconProps = { name: 'ot-spinner', spin: true } const host = useHost() - const handleClick: React.MouseEventHandler = () => { + const handleClick: MouseEventHandler = () => { setIsDownloadingRobotLogs(true) const toastId = makeToast(t('downloading_logs') as string, INFO_TOAST, { disableTimeout: true, @@ -99,8 +99,8 @@ export function Troubleshooting({ } } - // set ref on component to check if component is mounted https://react.dev/reference/react/useRef#manipulating-the-dom-with-a-ref - const mounted = React.useRef(null) + // set ref on component to check if component is mounted https://dev/reference/react/useRef#manipulating-the-dom-with-a-ref + const mounted = useRef(null) return ( (null) + const inputRef = useRef(null) const dispatchStartRobotUpdate = useDispatchStartRobotUpdate() - const handleChange: React.ChangeEventHandler = event => { + const handleChange: ChangeEventHandler = event => { const { files } = event.target if (files?.length === 1 && !updateDisabled) { dispatchStartRobotUpdate(robotName, files[0].path) @@ -65,7 +66,7 @@ export function UpdateRobotSoftware({ } } - const handleClick: React.MouseEventHandler = () => { + const handleClick: MouseEventHandler = () => { inputRef.current?.click() } diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/UploadKeyInput.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/UploadKeyInput.tsx index 6d450c336f2..4e79bba2cdc 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/UploadKeyInput.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/ConnectNetwork/ConnectModal/UploadKeyInput.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { forwardRef, useEffect, useRef } from 'react' import styled from 'styled-components' import { useSelector } from 'react-redux' import last from 'lodash/last' @@ -6,6 +6,7 @@ import last from 'lodash/last' import { useDispatchApiRequest } from '/app/redux/robot-api' import { postWifiKeys, getWifiKeyByRequestId } from '/app/redux/networking' +import type { ChangeEventHandler, ForwardedRef } from 'react' import type { State } from '/app/redux/types' export interface UploadKeyInputProps { @@ -28,17 +29,17 @@ const HiddenInput = styled.input` const UploadKeyInputComponent = ( props: UploadKeyInputProps, - ref: React.ForwardedRef + ref: ForwardedRef ): JSX.Element => { const { robotName, label, onUpload } = props const [dispatchApi, requestIds] = useDispatchApiRequest() - const handleUpload = React.useRef<(key: string) => void>() + const handleUpload = useRef<(key: string) => void>() const createdKeyId = useSelector((state: State) => { return getWifiKeyByRequestId(state, robotName, last(requestIds) ?? null) })?.id - const handleFileInput: React.ChangeEventHandler = event => { + const handleFileInput: ChangeEventHandler = event => { if (event.target.files && event.target.files.length > 0) { const file = event.target.files[0] event.target.value = '' @@ -47,11 +48,11 @@ const UploadKeyInputComponent = ( } } - React.useEffect(() => { + useEffect(() => { handleUpload.current = onUpload }, [onUpload]) - React.useEffect(() => { + useEffect(() => { if (createdKeyId != null && handleUpload.current) { handleUpload.current(createdKeyId) } @@ -67,7 +68,6 @@ const UploadKeyInputComponent = ( ) } -export const UploadKeyInput = React.forwardRef< - HTMLInputElement, - UploadKeyInputProps ->(UploadKeyInputComponent) +export const UploadKeyInput = forwardRef( + UploadKeyInputComponent +) diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/RobotSettingsAdvanced.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/RobotSettingsAdvanced.tsx index feb67b08d9f..adf2c92a9a5 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/RobotSettingsAdvanced.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/RobotSettingsAdvanced.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { createPortal } from 'react-dom' import { useSelector, useDispatch } from 'react-redux' @@ -50,6 +50,7 @@ import { getRobotSerialNumber, UNREACHABLE } from '/app/redux/discovery' import { getTopPortalEl } from '/app/App/portal' import { useIsEstopNotDisengaged } from '/app/resources/devices/hooks/useIsEstopNotDisengaged' +import type { MouseEventHandler } from 'react' import type { State, Dispatch } from '/app/redux/types' import type { RobotSettings, @@ -69,19 +70,18 @@ export function RobotSettingsAdvanced({ const [ showRenameRobotSlideout, setShowRenameRobotSlideout, - ] = React.useState(false) + ] = useState(false) const [ showDeviceResetSlideout, setShowDeviceResetSlideout, - ] = React.useState(false) - const [ - showDeviceResetModal, - setShowDeviceResetModal, - ] = React.useState(false) + ] = useState(false) + const [showDeviceResetModal, setShowDeviceResetModal] = useState( + false + ) const [ showFactoryModeSlideout, setShowFactoryModeSlideout, - ] = React.useState(false) + ] = useState(false) const isRobotBusy = useIsRobotBusy({ poll: true }) const isEstopNotDisengaged = useIsEstopNotDisengaged(robotName) @@ -95,10 +95,8 @@ export function RobotSettingsAdvanced({ const reachable = robot?.status !== UNREACHABLE const sn = robot?.status != null ? getRobotSerialNumber(robot) : null - const [isRobotReachable, setIsRobotReachable] = React.useState( - reachable - ) - const [resetOptions, setResetOptions] = React.useState({}) + const [isRobotReachable, setIsRobotReachable] = useState(reachable) + const [resetOptions, setResetOptions] = useState({}) const findSettings = (id: string): RobotSettingsField | undefined => settings?.find(s => s.id === id) @@ -124,11 +122,11 @@ export function RobotSettingsAdvanced({ const dispatch = useDispatch() - React.useEffect(() => { + useEffect(() => { dispatch(fetchSettings(robotName)) }, [dispatch, robotName]) - React.useEffect(() => { + useEffect(() => { updateRobotStatus(isRobotBusy) }, [isRobotBusy, updateRobotStatus]) @@ -291,7 +289,7 @@ export function FeatureFlagToggle({ if (id == null) return null - const handleClick: React.MouseEventHandler = () => { + const handleClick: MouseEventHandler = () => { if (!isRobotBusy) { dispatch(updateSetting(robotName, id, !value)) } diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/RobotSettingsFeatureFlags.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/RobotSettingsFeatureFlags.tsx index 363589867fc..aa1210cdecc 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/RobotSettingsFeatureFlags.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/RobotSettingsFeatureFlags.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect } from 'react' import { useSelector, useDispatch } from 'react-redux' import { @@ -17,6 +17,8 @@ import { getRobotSettings, fetchSettings, } from '/app/redux/robot-settings' + +import type { MouseEventHandler } from 'react' import type { State, Dispatch } from '/app/redux/types' import type { RobotSettings, @@ -50,7 +52,7 @@ export function RobotSettingsFeatureFlags({ const dispatch = useDispatch() - React.useEffect(() => { + useEffect(() => { dispatch(fetchSettings(robotName)) }, [dispatch, robotName]) @@ -81,7 +83,7 @@ export function FeatureFlagToggle({ if (id == null) return null - const handleClick: React.MouseEventHandler = () => { + const handleClick: MouseEventHandler = () => { dispatch(updateSetting(robotName, id, !value)) } diff --git a/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx b/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx index 955ead2de49..8a4fc045a65 100644 --- a/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx +++ b/app/src/organisms/Desktop/Devices/RobotSettings/UpdateBuildroot/RobotUpdateProgressModal.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { css } from 'styled-components' @@ -32,6 +32,7 @@ import { INIT_STATUS, } from '/app/resources/health/hooks' +import type { ChangeEventHandler } from 'react' import type { State } from '/app/redux/types' import type { SetStatusBarCreateCommand } from '@opentrons/shared-data/protocol' import type { RobotUpdateSession } from '/app/redux/robot-update/types' @@ -67,8 +68,8 @@ export function RobotUpdateProgressModal({ }: RobotUpdateProgressModalProps): JSX.Element { const dispatch = useDispatch() const { t } = useTranslation('device_settings') - const [showFileSelect, setShowFileSelect] = React.useState(false) - const installFromFileRef = React.useRef(null) + const [showFileSelect, setShowFileSelect] = useState(false) + const installFromFileRef = useRef(null) const completeRobotUpdateHandler = (): void => { if (closeUpdateBuildroot != null) closeUpdateBuildroot() @@ -85,14 +86,14 @@ export function RobotUpdateProgressModal({ useStatusBarAnimation(error != null) useCleanupRobotUpdateSessionOnDismount() - const handleFileSelect: React.ChangeEventHandler = event => { + const handleFileSelect: ChangeEventHandler = event => { const { files } = event.target if (files?.length === 1) { dispatch(startRobotUpdate(robotName, files[0].path)) } setShowFileSelect(false) } - React.useEffect(() => { + useEffect(() => { if (showFileSelect && installFromFileRef.current) installFromFileRef.current.click() }, [showFileSelect]) @@ -233,13 +234,11 @@ function useAllowExitIfUpdateStalled( progressPercent: number, robotInitStatus: RobotInitializationStatus ): boolean { - const [letUserExitUpdate, setLetUserExitUpdate] = React.useState( - false - ) - const prevSeenUpdateProgress = React.useRef(null) - const exitTimeoutRef = React.useRef(null) + const [letUserExitUpdate, setLetUserExitUpdate] = useState(false) + const prevSeenUpdateProgress = useRef(null) + const exitTimeoutRef = useRef(null) - React.useEffect(() => { + useEffect(() => { if (updateStep === 'initial' && prevSeenUpdateProgress.current !== null) { prevSeenUpdateProgress.current = null } else if (progressPercent !== prevSeenUpdateProgress.current) { @@ -258,7 +257,7 @@ function useAllowExitIfUpdateStalled( } }, [progressPercent, updateStep, robotInitStatus]) - React.useEffect(() => { + useEffect(() => { return () => { if (exitTimeoutRef.current) clearTimeout(exitTimeoutRef.current) } @@ -298,13 +297,13 @@ function useStatusBarAnimation(isError: boolean): void { } } - React.useEffect(startUpdatingAnimation, []) - React.useEffect(startIdleAnimationIfFailed, [isError]) + useEffect(startUpdatingAnimation, []) + useEffect(startIdleAnimationIfFailed, [isError]) } function useCleanupRobotUpdateSessionOnDismount(): void { const dispatch = useDispatch() - React.useEffect(() => { + useEffect(() => { return () => { dispatch(clearRobotUpdateSession()) } diff --git a/app/src/organisms/Desktop/Devices/hooks/__tests__/useLPCSuccessToast.test.ts b/app/src/organisms/Desktop/Devices/hooks/__tests__/useLPCSuccessToast.test.ts index a64b65252a1..6cb6897d8ca 100644 --- a/app/src/organisms/Desktop/Devices/hooks/__tests__/useLPCSuccessToast.test.ts +++ b/app/src/organisms/Desktop/Devices/hooks/__tests__/useLPCSuccessToast.test.ts @@ -1,11 +1,10 @@ -import * as React from 'react' +import { useContext } from 'react' import { vi, it, expect, describe } from 'vitest' import { renderHook } from '@testing-library/react' import { useLPCSuccessToast } from '..' -import type * as ReactType from 'react' vi.mock('react', async importOriginal => { - const actualReact = await importOriginal() + const actualReact = await importOriginal() return { ...actualReact, useContext: vi.fn(), @@ -14,7 +13,7 @@ vi.mock('react', async importOriginal => { describe('useLPCSuccessToast', () => { it('return true when useContext returns true', () => { - vi.mocked(React.useContext).mockReturnValue({ + vi.mocked(useContext).mockReturnValue({ setIsShowingLPCSuccessToast: true, }) const { result } = renderHook(() => useLPCSuccessToast()) @@ -23,7 +22,7 @@ describe('useLPCSuccessToast', () => { }) }) it('return false when useContext returns false', () => { - vi.mocked(React.useContext).mockReturnValue({ + vi.mocked(useContext).mockReturnValue({ setIsShowingLPCSuccessToast: false, }) const { result } = renderHook(() => useLPCSuccessToast()) diff --git a/app/src/organisms/Desktop/Labware/LabwareCard/CustomLabwareOverflowMenu.tsx b/app/src/organisms/Desktop/Labware/LabwareCard/CustomLabwareOverflowMenu.tsx index 6183fdf00de..08826189798 100644 --- a/app/src/organisms/Desktop/Labware/LabwareCard/CustomLabwareOverflowMenu.tsx +++ b/app/src/organisms/Desktop/Labware/LabwareCard/CustomLabwareOverflowMenu.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useDispatch } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -36,6 +36,7 @@ import { openCustomLabwareDirectory, } from '/app/redux/custom-labware' +import type { MouseEventHandler } from 'react' import type { Dispatch } from '/app/redux/types' const LABWARE_CREATOR_HREF = 'https://labware.opentrons.com/create/' @@ -51,7 +52,7 @@ export function CustomLabwareOverflowMenu( const { filename, onDelete } = props const { t } = useTranslation(['labware_landing', 'shared']) const dispatch = useDispatch() - const [showOverflowMenu, setShowOverflowMenu] = React.useState(false) + const [showOverflowMenu, setShowOverflowMenu] = useState(false) const overflowMenuRef = useOnClickOutside({ onClickOutside: () => { setShowOverflowMenu(false) @@ -67,24 +68,24 @@ export function CustomLabwareOverflowMenu( dispatch(deleteCustomLabwareFile(filename)) onDelete?.() }, true) - const handleOpenInFolder: React.MouseEventHandler = e => { + const handleOpenInFolder: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() setShowOverflowMenu(false) dispatch(openCustomLabwareDirectory()) } - const handleClickDelete: React.MouseEventHandler = e => { + const handleClickDelete: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() setShowOverflowMenu(false) confirmDeleteLabware() } - const handleOverflowClick: React.MouseEventHandler = e => { + const handleOverflowClick: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() setShowOverflowMenu(currentShowOverflowMenu => !currentShowOverflowMenu) } - const handleClickLabwareCreator: React.MouseEventHandler = e => { + const handleClickLabwareCreator: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() trackEvent({ @@ -95,7 +96,7 @@ export function CustomLabwareOverflowMenu( window.open(LABWARE_CREATOR_HREF, '_blank') } - const handleCancelModal: React.MouseEventHandler = e => { + const handleCancelModal: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() cancelDeleteLabware() diff --git a/app/src/organisms/Desktop/Labware/LabwareCard/hooks.tsx b/app/src/organisms/Desktop/Labware/LabwareCard/hooks.tsx index 62e0589ac51..3a5890d7f60 100644 --- a/app/src/organisms/Desktop/Labware/LabwareCard/hooks.tsx +++ b/app/src/organisms/Desktop/Labware/LabwareCard/hooks.tsx @@ -1,7 +1,8 @@ -import * as React from 'react' +import { useEffect } from 'react' +import type { RefObject } from 'react' export function useCloseOnOutsideClick( - ref: React.RefObject, + ref: RefObject, onClose: () => void ): void { const handleClick = (e: MouseEvent): void => { @@ -11,7 +12,7 @@ export function useCloseOnOutsideClick( } } - React.useEffect(() => { + useEffect(() => { document.addEventListener('click', handleClick) return () => { document.removeEventListener('click', handleClick) diff --git a/app/src/organisms/Desktop/Labware/LabwareDetails/StyledComponents/ExpandingTitle.tsx b/app/src/organisms/Desktop/Labware/LabwareDetails/StyledComponents/ExpandingTitle.tsx index c92a63434cb..a21a769ca43 100644 --- a/app/src/organisms/Desktop/Labware/LabwareDetails/StyledComponents/ExpandingTitle.tsx +++ b/app/src/organisms/Desktop/Labware/LabwareDetails/StyledComponents/ExpandingTitle.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { ALIGN_CENTER, Box, @@ -6,20 +6,21 @@ import { Flex, Icon, JUSTIFY_SPACE_BETWEEN, - Link, - SIZE_1, LegacyStyledText, + Link, TYPOGRAPHY, } from '@opentrons/components' import { Divider } from '/app/atoms/structure' +import type { ReactNode } from 'react' + interface ExpandingTitleProps { - label: React.ReactNode - diagram?: React.ReactNode + label: ReactNode + diagram?: ReactNode } export function ExpandingTitle(props: ExpandingTitleProps): JSX.Element { - const [diagramVisible, setDiagramVisible] = React.useState(false) + const [diagramVisible, setDiagramVisible] = useState(false) const toggleDiagramVisible = (): void => { setDiagramVisible(currentDiagramVisible => !currentDiagramVisible) } @@ -39,7 +40,7 @@ export function ExpandingTitle(props: ExpandingTitleProps): JSX.Element { )} diff --git a/app/src/organisms/Desktop/ProtocolAnalysisFailure/index.tsx b/app/src/organisms/Desktop/ProtocolAnalysisFailure/index.tsx index b2b904dd19d..3fe86f0385f 100644 --- a/app/src/organisms/Desktop/ProtocolAnalysisFailure/index.tsx +++ b/app/src/organisms/Desktop/ProtocolAnalysisFailure/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useDispatch } from 'react-redux' import { useTranslation, Trans } from 'react-i18next' @@ -6,15 +6,15 @@ import { css } from 'styled-components' import { ALIGN_CENTER, - Btn, Banner, + Btn, Flex, JUSTIFY_FLEX_END, - Modal, JUSTIFY_SPACE_BETWEEN, + LegacyStyledText, + Modal, PrimaryButton, SPACING, - LegacyStyledText, TYPOGRAPHY, WRAP_REVERSE, } from '@opentrons/components' @@ -22,6 +22,7 @@ import { import { analyzeProtocol } from '/app/redux/protocol-storage' import { getTopPortalEl } from '/app/App/portal' +import type { MouseEventHandler } from 'react' import type { Dispatch } from '/app/redux/types' interface ProtocolAnalysisFailureProps { errors: string[] @@ -34,19 +35,19 @@ export function ProtocolAnalysisFailure( const { errors, protocolKey } = props const { t } = useTranslation(['protocol_list', 'shared']) const dispatch = useDispatch() - const [showErrorDetails, setShowErrorDetails] = React.useState(false) + const [showErrorDetails, setShowErrorDetails] = useState(false) - const handleClickShowDetails: React.MouseEventHandler = e => { + const handleClickShowDetails: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() setShowErrorDetails(true) } - const handleClickHideDetails: React.MouseEventHandler = e => { + const handleClickHideDetails: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() setShowErrorDetails(false) } - const handleClickReanalyze: React.MouseEventHandler = e => { + const handleClickReanalyze: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() dispatch(analyzeProtocol(protocolKey)) diff --git a/app/src/organisms/Desktop/ProtocolDetails/ProtocolLabwareDetails.tsx b/app/src/organisms/Desktop/ProtocolDetails/ProtocolLabwareDetails.tsx index 4fe95406355..cb74884c89e 100644 --- a/app/src/organisms/Desktop/ProtocolDetails/ProtocolLabwareDetails.tsx +++ b/app/src/organisms/Desktop/ProtocolDetails/ProtocolLabwareDetails.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { @@ -23,6 +23,7 @@ import { Divider } from '/app/atoms/structure' import { getTopPortalEl } from '/app/App/portal' import { LabwareDetails } from '/app/organisms/Desktop/Labware/LabwareDetails' +import type { MouseEventHandler } from 'react' import type { LoadLabwareRunTimeCommand } from '@opentrons/shared-data' import type { LabwareDefAndDate } from '/app/local-resources/labware' @@ -164,9 +165,9 @@ export const LabwareDetailOverflowMenu = ( const [ showLabwareDetailSlideout, setShowLabwareDetailSlideout, - ] = React.useState(false) + ] = useState(false) - const handleClickMenuItem: React.MouseEventHandler = e => { + const handleClickMenuItem: MouseEventHandler = e => { e.preventDefault() setShowOverflowMenu(false) setShowLabwareDetailSlideout(true) diff --git a/app/src/organisms/Desktop/ProtocolDetails/RobotConfigurationDetails.tsx b/app/src/organisms/Desktop/ProtocolDetails/RobotConfigurationDetails.tsx index 5789a8af6e6..d53a6eba889 100644 --- a/app/src/organisms/Desktop/ProtocolDetails/RobotConfigurationDetails.tsx +++ b/app/src/organisms/Desktop/ProtocolDetails/RobotConfigurationDetails.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { Fragment } from 'react' import { useTranslation } from 'react-i18next' import { @@ -31,6 +31,7 @@ import { Divider } from '/app/atoms/structure' import { getRobotTypeDisplayName } from '../ProtocolsLanding/utils' import { getSlotsForThermocycler } from './utils' +import type { ReactNode } from 'react' import type { CutoutConfigProtocolSpec, LoadModuleRunTimeCommand, @@ -153,7 +154,7 @@ export const RobotConfigurationDetails = ( ) : null} {requiredModuleDetails.map((module, index) => { return ( - + } /> - + ) })} {nonStandardRequiredFixtureDetails.map((fixture, index) => { return ( - + } /> - + ) })} @@ -217,7 +218,7 @@ export const RobotConfigurationDetails = ( interface RobotConfigurationDetailsItemProps { label: string - item: React.ReactNode + item: ReactNode } export const RobotConfigurationDetailsItem = ( diff --git a/app/src/organisms/Desktop/ProtocolsLanding/ProtocolList.tsx b/app/src/organisms/Desktop/ProtocolsLanding/ProtocolList.tsx index 112999787d0..9cd58925e34 100644 --- a/app/src/organisms/Desktop/ProtocolsLanding/ProtocolList.tsx +++ b/app/src/organisms/Desktop/ProtocolsLanding/ProtocolList.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' @@ -35,6 +35,7 @@ import { ProtocolUploadInput } from './ProtocolUploadInput' import { ProtocolCard } from './ProtocolCard' import { EmptyStateLinks } from './EmptyStateLinks' +import type { MouseEventHandler } from 'react' import type { StoredProtocolData, ProtocolSort, @@ -61,17 +62,17 @@ export function ProtocolList(props: ProtocolListProps): JSX.Element | null { const [ showImportProtocolSlideout, setShowImportProtocolSlideout, - ] = React.useState(false) + ] = useState(false) const [ showChooseRobotToRunProtocolSlideout, setShowChooseRobotToRunProtocolSlideout, - ] = React.useState(false) + ] = useState(false) const [ showSendProtocolToFlexSlideout, setShowSendProtocolToFlexSlideout, - ] = React.useState(false) + ] = useState(false) const sortBy = useSelector(getProtocolsDesktopSortKey) ?? 'alphabetical' - const [showSortByMenu, setShowSortByMenu] = React.useState(false) + const [showSortByMenu, setShowSortByMenu] = useState(false) const toggleSetShowSortByMenu = (): void => { setShowSortByMenu(!showSortByMenu) } @@ -80,13 +81,13 @@ export function ProtocolList(props: ProtocolListProps): JSX.Element | null { const [ selectedProtocol, setSelectedProtocol, - ] = React.useState(null) + ] = useState(null) const sortedStoredProtocols = useSortedProtocols(sortBy, storedProtocols) const dispatch = useDispatch() - const handleClickOutside: React.MouseEventHandler = e => { + const handleClickOutside: MouseEventHandler = e => { e.preventDefault() setShowSortByMenu(false) } diff --git a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx index ee95d1abf73..1db51029549 100644 --- a/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx +++ b/app/src/organisms/Desktop/RobotSettingsCalibration/CalibrationDetails/OverflowMenu.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { saveAs } from 'file-saver' import { css } from 'styled-components' @@ -35,6 +35,7 @@ import { FLOWS } from '/app/organisms/PipetteWizardFlows/constants' import { useIsEstopNotDisengaged } from '/app/resources/devices' import { useAttachedPipettesFromInstrumentsQuery } from '/app/resources/instruments' +import type { MouseEvent } from 'react' import type { Mount } from '@opentrons/components' import type { PipetteName } from '@opentrons/shared-data' import type { DeleteCalRequestParams } from '@opentrons/api-client' @@ -82,10 +83,9 @@ export function OverflowMenu({ const tipLengthCalibrations = useAllTipLengthCalibrationsQuery().data?.data const { isRunRunning: isRunning } = useRunStatuses() - const [ - showPipetteWizardFlows, - setShowPipetteWizardFlows, - ] = React.useState(false) + const [showPipetteWizardFlows, setShowPipetteWizardFlows] = useState( + false + ) const isEstopNotDisengaged = useIsEstopNotDisengaged(robotName) const isPipetteForFlex = isFlexPipette(pipetteName as PipetteName) const ot3PipCal = @@ -103,7 +103,7 @@ export function OverflowMenu({ calType === 'pipetteOffset' ? applicablePipetteOffsetCal != null : applicableTipLengthCal != null - const handleRecalibrate = (e: React.MouseEvent): void => { + const handleRecalibrate = (e: MouseEvent): void => { e.preventDefault() if ( !isRunning && @@ -116,7 +116,7 @@ export function OverflowMenu({ setShowOverflowMenu(currentShowOverflowMenu => !currentShowOverflowMenu) } - const handleDownload = (e: React.MouseEvent): void => { + const handleDownload = (e: MouseEvent): void => { e.preventDefault() doTrackEvent({ name: ANALYTICS_CALIBRATION_DATA_DOWNLOADED, @@ -137,19 +137,18 @@ export function OverflowMenu({ setShowOverflowMenu(currentShowOverflowMenu => !currentShowOverflowMenu) } - React.useEffect(() => { + useEffect(() => { if (isRunning) { updateRobotStatus(true) } }, [isRunning, updateRobotStatus]) const { deleteCalibration } = useDeleteCalibrationMutation() - const [ - selectedPipette, - setSelectedPipette, - ] = React.useState(SINGLE_MOUNT_PIPETTES) + const [selectedPipette, setSelectedPipette] = useState( + SINGLE_MOUNT_PIPETTES + ) - const handleDeleteCalibration = (e: React.MouseEvent): void => { + const handleDeleteCalibration = (e: MouseEvent): void => { e.preventDefault() let params: DeleteCalRequestParams if (calType === 'pipetteOffset') { diff --git a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx index 16062be3034..bf3be7e788e 100644 --- a/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx +++ b/app/src/organisms/DeviceDetailsDeckConfiguration/AddFixtureModal.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' @@ -7,15 +7,15 @@ import { BORDERS, Btn, COLORS, + CURSOR_DEFAULT, DIRECTION_COLUMN, DIRECTION_ROW, Flex, JUSTIFY_SPACE_BETWEEN, LegacyStyledText, - SPACING, Modal, + SPACING, TYPOGRAPHY, - CURSOR_DEFAULT, } from '@opentrons/components' import { useModulesQuery, @@ -25,11 +25,11 @@ import { getCutoutDisplayName, getFixtureDisplayName, ABSORBANCE_READER_CUTOUTS, - ABSORBANCE_READER_V1, ABSORBANCE_READER_V1_FIXTURE, + ABSORBANCE_READER_V1, HEATER_SHAKER_CUTOUTS, - HEATERSHAKER_MODULE_V1, HEATERSHAKER_MODULE_V1_FIXTURE, + HEATERSHAKER_MODULE_V1, MAGNETIC_BLOCK_V1_FIXTURE, SINGLE_CENTER_CUTOUTS, SINGLE_LEFT_CUTOUTS, @@ -38,8 +38,8 @@ import { STAGING_AREA_RIGHT_SLOT_FIXTURE, STAGING_AREA_SLOT_WITH_MAGNETIC_BLOCK_V1_FIXTURE, TEMPERATURE_MODULE_CUTOUTS, - TEMPERATURE_MODULE_V2, TEMPERATURE_MODULE_V2_FIXTURE, + TEMPERATURE_MODULE_V2, THERMOCYCLER_MODULE_CUTOUTS, THERMOCYCLER_MODULE_V2, THERMOCYCLER_V2_FRONT_FIXTURE, @@ -54,6 +54,7 @@ import { TertiaryButton } from '/app/atoms/buttons' import { OddModal } from '/app/molecules/OddModal' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration/' +import type { MouseEventHandler } from 'react' import type { CutoutConfig, CutoutId, @@ -101,9 +102,7 @@ export function AddFixtureModal({ // only show provided options if given as props initialStage = 'providedOptions' } - const [optionStage, setOptionStage] = React.useState( - initialStage - ) + const [optionStage, setOptionStage] = useState(initialStage) const modalHeader: OddModalHeaderBaseProps = { title: t('add_to_slot', { @@ -425,7 +424,7 @@ const GO_BACK_BUTTON_STYLE = css` ` interface FixtureOptionProps { - onClickHandler: React.MouseEventHandler + onClickHandler: MouseEventHandler optionName: string buttonText: string isOnDevice: boolean diff --git a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx index 7c78de6b8e2..ccebc8e124a 100644 --- a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx +++ b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -30,11 +30,12 @@ import { SmallButton } from '/app/atoms/buttons' import { OddModal } from '/app/molecules/OddModal' import { getIsOnDevice } from '/app/redux/config' +import type { MouseEventHandler } from 'react' +import type { ModalProps } from '@opentrons/components' import type { OddModalHeaderBaseProps, ModalSize, } from '/app/molecules/OddModal/types' -import type { ModalProps } from '@opentrons/components' // Note (07/13/2023) After the launch, we will unify the modal components into one component. // Then TouchScreenModal and DesktopModal will be TouchScreenContent and DesktopContent that only render each content. @@ -81,7 +82,7 @@ function TouchscreenModal({ setIsWaitingForResumeOperation, }: EstopPressedModalProps): JSX.Element { const { t } = useTranslation(['device_settings', 'branded']) - const [isResuming, setIsResuming] = React.useState(false) + const [isResuming, setIsResuming] = useState(false) const { acknowledgeEstopDisengage } = useAcknowledgeEstopDisengageMutation() const { @@ -156,7 +157,7 @@ function DesktopModal({ setIsWaitingForResumeOperation, }: EstopPressedModalProps): JSX.Element { const { t } = useTranslation('device_settings') - const [isResuming, setIsResuming] = React.useState(false) + const [isResuming, setIsResuming] = useState(false) const { acknowledgeEstopDisengage } = useAcknowledgeEstopDisengageMutation() const { handlePlaceReaderLid, @@ -174,7 +175,7 @@ function DesktopModal({ width: '47rem', } - const handleClick: React.MouseEventHandler = (e): void => { + const handleClick: MouseEventHandler = (e): void => { e.preventDefault() setIsResuming(true) setIsWaitingForResumeOperation() diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx index 603ac5af6c3..070e974bed3 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx @@ -1,17 +1,17 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' import { css } from 'styled-components' import { - Flex, - StyledText, - SPACING, - COLORS, - ModalShell, - ModalHeader, BORDERS, + COLORS, DIRECTION_COLUMN, + Flex, + ModalHeader, + ModalShell, + SPACING, + StyledText, } from '@opentrons/components' import { useErrorName } from '../hooks' @@ -22,6 +22,7 @@ import { InlineNotification } from '/app/atoms/InlineNotification' import { StepInfo } from './StepInfo' import { getErrorKind } from '../utils' +import type { ReactNode } from 'react' import type { LabwareDefinition2, RobotType } from '@opentrons/shared-data' import type { IconProps } from '@opentrons/components' import type { OddModalHeaderBaseProps } from '/app/molecules/OddModal/types' @@ -33,7 +34,7 @@ export function useErrorDetailsModal(): { showModal: boolean toggleModal: () => void } { - const [showModal, setShowModal] = React.useState(false) + const [showModal, setShowModal] = useState(false) const toggleModal = (): void => { setShowModal(!showModal) @@ -113,7 +114,7 @@ export function ErrorDetailsModal(props: ErrorDetailsModalProps): JSX.Element { } type ErrorDetailsModalType = ErrorDetailsModalProps & { - children: React.ReactNode + children: ReactNode modalHeader: OddModalHeaderBaseProps toggleModal: () => void desktopType: DesktopSizeType diff --git a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx index b421b4be81f..afd9efba19f 100644 --- a/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/AttachProbe.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect, useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import { RESPONSIVENESS, @@ -15,6 +15,7 @@ import attachProbe8 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach import attachProbe96 from '/app/assets/videos/pipette-wizard-flows/Pipette_Attach_Probe_96.webm' import { GenericWizardTile } from '/app/molecules/GenericWizardTile' +import type { Dispatch } from 'react' import type { CompletedProtocolAnalysis, CreateCommand, @@ -31,7 +32,7 @@ import type { interface AttachProbeProps extends AttachProbeStep { protocolData: CompletedProtocolAnalysis proceed: () => void - registerPosition: React.Dispatch + registerPosition: Dispatch chainRunCommands: ReturnType['chainRunCommands'] setFatalError: (errorMessage: string) => void workingOffsets: WorkingOffset[] @@ -52,9 +53,7 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { setFatalError, isOnDevice, } = props - const [showUnableToDetect, setShowUnableToDetect] = React.useState( - false - ) + const [showUnableToDetect, setShowUnableToDetect] = useState(false) const pipette = protocolData.pipettes.find(p => p.id === pipetteId) const pipetteName = pipette?.pipetteName @@ -72,7 +71,7 @@ export const AttachProbe = (props: AttachProbeProps): JSX.Element | null => { const pipetteMount = pipette?.mount - React.useEffect(() => { + useEffect(() => { // move into correct position for probe attach on mount chainRunCommands( [ diff --git a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx b/app/src/organisms/LabwarePositionCheck/CheckItem.tsx index 5d5008554a6..734ee6468b1 100644 --- a/app/src/organisms/LabwarePositionCheck/CheckItem.tsx +++ b/app/src/organisms/LabwarePositionCheck/CheckItem.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect } from 'react' import omit from 'lodash/omit' import isEqual from 'lodash/isEqual' import { Trans, useTranslation } from 'react-i18next' @@ -29,6 +29,7 @@ import { getCurrentOffsetForLabwareInLocation } from '/app/transformations/analy import { getIsOnDevice } from '/app/redux/config' import { getDisplayLocation } from './utils/getDisplayLocation' +import type { Dispatch } from 'react' import type { LabwareOffset } from '@opentrons/api-client' import type { CompletedProtocolAnalysis, @@ -54,7 +55,7 @@ interface CheckItemProps extends Omit { proceed: () => void chainRunCommands: ReturnType['chainRunCommands'] setFatalError: (errorMessage: string) => void - registerPosition: React.Dispatch + registerPosition: Dispatch workingOffsets: WorkingOffset[] existingOffsets: LabwareOffset[] handleJog: Jog @@ -131,7 +132,7 @@ export const CheckItem = (props: CheckItemProps): JSX.Element | null => { o.initialPosition != null )?.initialPosition - React.useEffect(() => { + useEffect(() => { if (initialPosition == null && modulePrepCommands.length > 0) { chainRunCommands(modulePrepCommands, false) .then(() => {}) diff --git a/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx b/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx index da0952ca407..dd040654a23 100644 --- a/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx +++ b/app/src/organisms/LabwarePositionCheck/DetachProbe.tsx @@ -1,10 +1,10 @@ -import * as React from 'react' +import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' import { + LegacyStyledText, RESPONSIVENESS, SPACING, - LegacyStyledText, TYPOGRAPHY, } from '@opentrons/components' import { RobotMotionLoader } from './RobotMotionLoader' @@ -14,6 +14,7 @@ import detachProbe8 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach import detachProbe96 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_96.webm' import { GenericWizardTile } from '/app/molecules/GenericWizardTile' +import type { Dispatch } from 'react' import type { CompletedProtocolAnalysis } from '@opentrons/shared-data' import type { Jog } from '/app/molecules/JogControls/types' import type { useChainRunCommands } from '/app/resources/runs' @@ -27,7 +28,7 @@ import type { LabwareOffset } from '@opentrons/api-client' interface DetachProbeProps extends DetachProbeStep { protocolData: CompletedProtocolAnalysis proceed: () => void - registerPosition: React.Dispatch + registerPosition: Dispatch chainRunCommands: ReturnType['chainRunCommands'] setFatalError: (errorMessage: string) => void workingOffsets: WorkingOffset[] @@ -58,7 +59,7 @@ export const DetachProbe = (props: DetachProbeProps): JSX.Element | null => { } const pipetteMount = pipette?.mount - React.useEffect(() => { + useEffect(() => { // move into correct position for probe detach on mount chainRunCommands( [ diff --git a/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx b/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx index dbaa5970c6c..44e5eb67ded 100644 --- a/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/IntroScreen/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { Trans, useTranslation } from 'react-i18next' import { css } from 'styled-components' @@ -32,6 +32,7 @@ import { CALIBRATION_PROBE } from '/app/organisms/PipetteWizardFlows/constants' import { TerseOffsetTable } from '../ResultsSummary' import { getLabwareDefinitionsFromCommands } from '/app/local-resources/labware' +import type { Dispatch } from 'react' import type { LabwareOffset } from '@opentrons/api-client' import type { CompletedProtocolAnalysis, @@ -49,7 +50,7 @@ const SUPPORT_PAGE_URL = 'https://support.opentrons.com/s/ot2-calibration' export const IntroScreen = (props: { proceed: () => void protocolData: CompletedProtocolAnalysis - registerPosition: React.Dispatch + registerPosition: Dispatch chainRunCommands: ReturnType['chainRunCommands'] handleJog: Jog setFatalError: (errorMessage: string) => void @@ -160,7 +161,7 @@ interface ViewOffsetsProps { function ViewOffsets(props: ViewOffsetsProps): JSX.Element { const { existingOffsets, labwareDefinitions } = props const { t, i18n } = useTranslation('labware_position_check') - const [showOffsetsTable, setShowOffsetsModal] = React.useState(false) + const [showOffsetsTable, setShowOffsetsModal] = useState(false) const latestCurrentOffsets = getLatestCurrentOffsets(existingOffsets) return existingOffsets.length > 0 ? ( <> diff --git a/app/src/organisms/LabwarePositionCheck/JogToWell.tsx b/app/src/organisms/LabwarePositionCheck/JogToWell.tsx index e212af695a5..bce4808a514 100644 --- a/app/src/organisms/LabwarePositionCheck/JogToWell.tsx +++ b/app/src/organisms/LabwarePositionCheck/JogToWell.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect, useState } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' @@ -11,14 +11,14 @@ import { Flex, JUSTIFY_SPACE_BETWEEN, LabwareRender, + LegacyStyledText, + ModalShell, PipetteRender, PrimaryButton, RESPONSIVENESS, RobotWorkSpace, SecondaryButton, SPACING, - LegacyStyledText, - ModalShell, TYPOGRAPHY, WELL_LABEL_OPTIONS, } from '@opentrons/components' @@ -40,6 +40,7 @@ import { NeedHelpLink } from '/app/molecules/OT2CalibrationNeedHelpLink' import { JogControls } from '/app/molecules/JogControls' import { LiveOffsetValue } from './LiveOffsetValue' +import type { ReactNode } from 'react' import type { PipetteName, LabwareDefinition2 } from '@opentrons/shared-data' import type { WellStroke } from '@opentrons/components' import type { VectorOffset } from '@opentrons/api-client' @@ -55,8 +56,8 @@ interface JogToWellProps { handleJog: Jog pipetteName: PipetteName labwareDef: LabwareDefinition2 - header: React.ReactNode - body: React.ReactNode + header: ReactNode + body: ReactNode initialPosition: VectorOffset existingOffset: VectorOffset shouldUseMetalProbe: boolean @@ -76,12 +77,12 @@ export const JogToWell = (props: JogToWellProps): JSX.Element | null => { shouldUseMetalProbe, } = props - const [joggedPosition, setJoggedPosition] = React.useState( + const [joggedPosition, setJoggedPosition] = useState( initialPosition ) const isOnDevice = useSelector(getIsOnDevice) - const [showFullJogControls, setShowFullJogControls] = React.useState(false) - React.useEffect(() => { + const [showFullJogControls, setShowFullJogControls] = useState(false) + useEffect(() => { // NOTE: this will perform a "null" jog when the jog controls mount so // if a user reaches the "confirm exit" modal (unmounting this component) // and clicks "go back" we are able so initialize the live offset to whatever diff --git a/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx b/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx index c7505a13d92..de76e855097 100644 --- a/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx +++ b/app/src/organisms/LabwarePositionCheck/PickUpTip.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { Trans, useTranslation } from 'react-i18next' import isEqual from 'lodash/isEqual' import { @@ -27,6 +27,7 @@ import { getDisplayLocation } from './utils/getDisplayLocation' import { useSelector } from 'react-redux' import { getIsOnDevice } from '/app/redux/config' +import type { Dispatch } from 'react' import type { CompletedProtocolAnalysis, CreateCommand, @@ -46,7 +47,7 @@ import type { TFunction } from 'i18next' interface PickUpTipProps extends PickUpTipStep { protocolData: CompletedProtocolAnalysis proceed: () => void - registerPosition: React.Dispatch + registerPosition: Dispatch chainRunCommands: ReturnType['chainRunCommands'] setFatalError: (errorMessage: string) => void workingOffsets: WorkingOffset[] @@ -77,7 +78,7 @@ export const PickUpTip = (props: PickUpTipProps): JSX.Element | null => { protocolHasModules, currentStepIndex, } = props - const [showTipConfirmation, setShowTipConfirmation] = React.useState(false) + const [showTipConfirmation, setShowTipConfirmation] = useState(false) const isOnDevice = useSelector(getIsOnDevice) const labwareDef = getLabwareDef(labwareId, protocolData) const pipette = protocolData.pipettes.find(p => p.id === pipetteId) diff --git a/app/src/organisms/LabwarePositionCheck/index.tsx b/app/src/organisms/LabwarePositionCheck/index.tsx index b1453f9267c..e96191c584e 100644 --- a/app/src/organisms/LabwarePositionCheck/index.tsx +++ b/app/src/organisms/LabwarePositionCheck/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { Component } from 'react' import { useLogger } from '../../logger' import { LabwarePositionCheckComponent } from './LabwarePositionCheckComponent' import { FatalErrorModal } from './FatalErrorModal' @@ -6,6 +6,8 @@ import { getIsOnDevice } from '/app/redux/config' import { useSelector } from 'react-redux' import { FLEX_ROBOT_TYPE } from '@opentrons/shared-data' + +import type { ErrorInfo, ReactNode } from 'react' import type { CompletedProtocolAnalysis, RobotType, @@ -48,7 +50,7 @@ export const LabwarePositionCheck = ( } interface ErrorBoundaryProps { - children: React.ReactNode + children: ReactNode onClose: () => void shouldUseMetalProbe: boolean logger: ReturnType @@ -60,7 +62,7 @@ interface ErrorBoundaryProps { }) => JSX.Element isOnDevice: boolean } -class ErrorBoundary extends React.Component< +class ErrorBoundary extends Component< ErrorBoundaryProps, { error: Error | null } > { @@ -69,7 +71,7 @@ class ErrorBoundary extends React.Component< this.state = { error: null } } - componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { + componentDidCatch(error: Error, errorInfo: ErrorInfo): void { this.props.logger.error(`LPC error message: ${error.message}`) this.props.logger.error( `LPC error component stack: ${errorInfo.componentStack}` diff --git a/app/src/organisms/ModuleCard/ConfirmAttachmentModal.tsx b/app/src/organisms/ModuleCard/ConfirmAttachmentModal.tsx index 7031f176425..4a9cb9f3e4a 100644 --- a/app/src/organisms/ModuleCard/ConfirmAttachmentModal.tsx +++ b/app/src/organisms/ModuleCard/ConfirmAttachmentModal.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' import { @@ -17,6 +17,8 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { updateConfigValue } from '/app/redux/config' + +import type { ChangeEvent } from 'react' import type { Dispatch } from '/app/redux/types' import type { UpdateConfigValueAction } from '/app/redux/config/types' @@ -38,7 +40,7 @@ export const ConfirmAttachmentModal = ( ): JSX.Element | null => { const { isProceedToRunModal, onCloseClick, onConfirmClick } = props const { t } = useTranslation(['heater_shaker', 'shared']) - const [isDismissed, setIsDismissed] = React.useState(false) + const [isDismissed, setIsDismissed] = useState(false) const dispatch = useDispatch() const confirmAttached = (): void => { @@ -81,7 +83,7 @@ export const ConfirmAttachmentModal = ( }`} > ) => { + onChange={(e: ChangeEvent) => { setIsDismissed(e.currentTarget.checked) }} value={isDismissed} diff --git a/app/src/organisms/ModuleCard/HeaterShakerSlideout.tsx b/app/src/organisms/ModuleCard/HeaterShakerSlideout.tsx index 1b189f3174d..bccf5fb3a30 100644 --- a/app/src/organisms/ModuleCard/HeaterShakerSlideout.tsx +++ b/app/src/organisms/ModuleCard/HeaterShakerSlideout.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useCreateLiveCommandMutation } from '@opentrons/react-api-client' @@ -21,6 +21,7 @@ import { import { Slideout } from '/app/atoms/Slideout' import { SubmitPrimaryButton } from '/app/atoms/buttons' +import type { MouseEventHandler } from 'react' import type { HeaterShakerModule } from '/app/redux/modules/types' import type { HeaterShakerSetTargetTemperatureCreateCommand } from '@opentrons/shared-data' @@ -35,12 +36,12 @@ export const HeaterShakerSlideout = ( ): JSX.Element | null => { const { module, onCloseClick, isExpanded } = props const { t } = useTranslation('device_details') - const [hsValue, setHsValue] = React.useState(null) + const [hsValue, setHsValue] = useState(null) const { createLiveCommand } = useCreateLiveCommandMutation() const moduleName = getModuleDisplayName(module.moduleModel) const modulePart = t('temperature') - const sendSetTemperatureCommand: React.MouseEventHandler = e => { + const sendSetTemperatureCommand: MouseEventHandler = e => { e.preventDefault() e.stopPropagation() diff --git a/app/src/organisms/ModuleWizardFlows/index.tsx b/app/src/organisms/ModuleWizardFlows/index.tsx index 27a3555d70c..b802cd6ca85 100644 --- a/app/src/organisms/ModuleWizardFlows/index.tsx +++ b/app/src/organisms/ModuleWizardFlows/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { createPortal } from 'react-dom' import { useSelector } from 'react-redux' import { Trans, useTranslation } from 'react-i18next' @@ -37,6 +37,7 @@ import { useNotifyCurrentMaintenanceRun, } from '/app/resources/maintenance_runs' +import type { SetStateAction } from 'react' import type { AttachedModule, CommandData } from '@opentrons/api-client' import { RUN_STATUS_FAILED } from '@opentrons/api-client' import type { @@ -107,7 +108,7 @@ export const ModuleWizardFlows = ( ) ) ?? [] - const [currentStepIndex, setCurrentStepIndex] = React.useState(0) + const [currentStepIndex, setCurrentStepIndex] = useState(0) const totalStepCount = moduleCalibrationSteps.length - 1 const currentStep = moduleCalibrationSteps?.[currentStepIndex] @@ -116,18 +117,16 @@ export const ModuleWizardFlows = ( currentStepIndex === 0 ? currentStepIndex : currentStepIndex - 1 ) } - const [createdMaintenanceRunId, setCreatedMaintenanceRunId] = React.useState< + const [createdMaintenanceRunId, setCreatedMaintenanceRunId] = useState< string | null >(null) - const [createdAdapterId, setCreatedAdapterId] = React.useState( - null - ) + const [createdAdapterId, setCreatedAdapterId] = useState(null) // we should start checking for run deletion only after the maintenance run is created // and the useCurrentRun poll has returned that created id const [ monitorMaintenanceRunForDeletion, setMonitorMaintenanceRunForDeletion, - ] = React.useState(false) + ] = useState(false) const { data: maintenanceRunData } = useNotifyCurrentMaintenanceRun({ refetchInterval: RUN_REFETCH_INTERVAL, @@ -142,16 +141,14 @@ export const ModuleWizardFlows = ( createTargetedMaintenanceRun, isLoading: isCreateLoading, } = useCreateTargetedMaintenanceRunMutation({ - onSuccess: (response: { - data: { id: React.SetStateAction } - }) => { + onSuccess: (response: { data: { id: SetStateAction } }) => { setCreatedMaintenanceRunId(response.data.id) }, }) // this will close the modal in case the run was deleted by the terminate // activity modal on the ODD - React.useEffect(() => { + useEffect(() => { if ( createdMaintenanceRunId !== null && maintenanceRunData?.data.id === createdMaintenanceRunId @@ -171,8 +168,8 @@ export const ModuleWizardFlows = ( closeFlow, ]) - const [errorMessage, setErrorMessage] = React.useState(null) - const [isExiting, setIsExiting] = React.useState(false) + const [errorMessage, setErrorMessage] = useState(null) + const [isExiting, setIsExiting] = useState(false) const proceed = (): void => { if (!isCommandMutationLoading) { setCurrentStepIndex( @@ -216,9 +213,9 @@ export const ModuleWizardFlows = ( } } - const [isRobotMoving, setIsRobotMoving] = React.useState(false) + const [isRobotMoving, setIsRobotMoving] = useState(false) - React.useEffect(() => { + useEffect(() => { if (isCommandMutationLoading || isExiting) { setIsRobotMoving(true) } else { diff --git a/app/src/organisms/ODD/InstrumentInfo/index.tsx b/app/src/organisms/ODD/InstrumentInfo/index.tsx index 68c3ebd5388..ecfb9456b05 100644 --- a/app/src/organisms/ODD/InstrumentInfo/index.tsx +++ b/app/src/organisms/ODD/InstrumentInfo/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { @@ -8,8 +8,8 @@ import { Flex, JUSTIFY_CENTER, JUSTIFY_SPACE_BETWEEN, - SPACING, LegacyStyledText, + SPACING, TYPOGRAPHY, } from '@opentrons/components' import { @@ -23,6 +23,7 @@ import { FLOWS } from '/app/organisms/PipetteWizardFlows/constants' import { GRIPPER_FLOW_TYPES } from '/app/organisms/GripperWizardFlows/constants' import { formatTimeWithUtcLabel } from '/app/resources/runs' +import type { ComponentProps, MouseEventHandler } from 'react' import type { InstrumentData } from '@opentrons/api-client' import type { PipetteMount } from '@opentrons/shared-data' import type { StyleProps } from '@opentrons/components' @@ -36,14 +37,14 @@ export const InstrumentInfo = (props: InstrumentInfoProps): JSX.Element => { const { t, i18n } = useTranslation('instruments_dashboard') const { instrument } = props const navigate = useNavigate() - const [wizardProps, setWizardProps] = React.useState< - | React.ComponentProps - | React.ComponentProps + const [wizardProps, setWizardProps] = useState< + | ComponentProps + | ComponentProps | null >(null) const sharedGripperWizardProps: Pick< - React.ComponentProps, + ComponentProps, 'attachedGripper' | 'closeFlow' > = { attachedGripper: instrument, @@ -58,7 +59,7 @@ export const InstrumentInfo = (props: InstrumentInfoProps): JSX.Element => { instrument.mount !== 'extension' && instrument.data?.channels === 96 - const handleDetach: React.MouseEventHandler = () => { + const handleDetach: MouseEventHandler = () => { if (instrument != null && instrument.ok) { setWizardProps( instrument.mount === 'extension' @@ -85,7 +86,7 @@ export const InstrumentInfo = (props: InstrumentInfoProps): JSX.Element => { ) } } - const handleRecalibrate: React.MouseEventHandler = () => { + const handleRecalibrate: MouseEventHandler = () => { if (instrument != null && instrument.ok) { setWizardProps( instrument.mount === 'extension' diff --git a/app/src/organisms/ODD/InstrumentMountItem/AttachedInstrumentMountItem.tsx b/app/src/organisms/ODD/InstrumentMountItem/AttachedInstrumentMountItem.tsx index d2de81b8fdc..7af7dd4029a 100644 --- a/app/src/organisms/ODD/InstrumentMountItem/AttachedInstrumentMountItem.tsx +++ b/app/src/organisms/ODD/InstrumentMountItem/AttachedInstrumentMountItem.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useNavigate } from 'react-router-dom' import { SINGLE_MOUNT_PIPETTES } from '@opentrons/shared-data' @@ -12,6 +12,7 @@ import { FLOWS } from '/app/organisms/PipetteWizardFlows/constants' import { GRIPPER_FLOW_TYPES } from '/app/organisms/GripperWizardFlows/constants' import { LabeledMount } from './LabeledMount' +import type { ComponentProps, MouseEventHandler } from 'react' import type { InstrumentData } from '@opentrons/api-client' import type { GripperModel, PipetteModel } from '@opentrons/shared-data' import type { Mount } from '/app/redux/pipettes/types' @@ -24,8 +25,8 @@ interface AttachedInstrumentMountItemProps { attachedInstrument: InstrumentData | null setWizardProps: ( props: - | React.ComponentProps - | React.ComponentProps + | ComponentProps + | ComponentProps | null ) => void } @@ -36,15 +37,12 @@ export function AttachedInstrumentMountItem( const navigate = useNavigate() const { mount, attachedInstrument, setWizardProps } = props - const [showChoosePipetteModal, setShowChoosePipetteModal] = React.useState( - false + const [showChoosePipetteModal, setShowChoosePipetteModal] = useState(false) + const [selectedPipette, setSelectedPipette] = useState( + SINGLE_MOUNT_PIPETTES ) - const [ - selectedPipette, - setSelectedPipette, - ] = React.useState(SINGLE_MOUNT_PIPETTES) - const handleClick: React.MouseEventHandler = () => { + const handleClick: MouseEventHandler = () => { if (attachedInstrument == null && mount !== 'extension') { setShowChoosePipetteModal(true) } else if (attachedInstrument == null && mount === 'extension') { diff --git a/app/src/organisms/ODD/InstrumentMountItem/ProtocolInstrumentMountItem.tsx b/app/src/organisms/ODD/InstrumentMountItem/ProtocolInstrumentMountItem.tsx index be034e8fb7a..86ae69e4107 100644 --- a/app/src/organisms/ODD/InstrumentMountItem/ProtocolInstrumentMountItem.tsx +++ b/app/src/organisms/ODD/InstrumentMountItem/ProtocolInstrumentMountItem.tsx @@ -1,17 +1,17 @@ -import * as React from 'react' +import { useState, useMemo } from 'react' import styled from 'styled-components' import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, - Flex, - COLORS, - SPACING, - TYPOGRAPHY, - Icon, - DIRECTION_COLUMN, ALIGN_FLEX_START, BORDERS, + COLORS, + DIRECTION_COLUMN, + Flex, + Icon, JUSTIFY_FLEX_START, + SPACING, + TYPOGRAPHY, } from '@opentrons/components' import { NINETY_SIX_CHANNEL, @@ -27,6 +27,7 @@ import { FLOWS } from '/app/organisms/PipetteWizardFlows/constants' import { PipetteWizardFlows } from '/app/organisms/PipetteWizardFlows' import { GripperWizardFlows } from '/app/organisms/GripperWizardFlows' +import type { MouseEventHandler } from 'react' import type { InstrumentData } from '@opentrons/api-client' import type { GripperModel, @@ -61,26 +62,24 @@ export function ProtocolInstrumentMountItem( ): JSX.Element { const { i18n, t } = useTranslation('protocol_setup') const { mount, attachedInstrument, speccedName } = props - const [ - showPipetteWizardFlow, - setShowPipetteWizardFlow, - ] = React.useState(false) - const [ - showGripperWizardFlow, - setShowGripperWizardFlow, - ] = React.useState(false) - const memoizedAttachedGripper = React.useMemo( + const [showPipetteWizardFlow, setShowPipetteWizardFlow] = useState( + false + ) + const [showGripperWizardFlow, setShowGripperWizardFlow] = useState( + false + ) + const memoizedAttachedGripper = useMemo( () => attachedInstrument?.instrumentType === 'gripper' && attachedInstrument.ok ? attachedInstrument : null, [] ) - const [flowType, setFlowType] = React.useState(FLOWS.ATTACH) + const [flowType, setFlowType] = useState(FLOWS.ATTACH) const selectedPipette = speccedName === 'p1000_96' ? NINETY_SIX_CHANNEL : SINGLE_MOUNT_PIPETTES - const handleCalibrate: React.MouseEventHandler = () => { + const handleCalibrate: MouseEventHandler = () => { setFlowType(FLOWS.CALIBRATE) if (mount === 'extension') { setShowGripperWizardFlow(true) @@ -88,7 +87,7 @@ export function ProtocolInstrumentMountItem( setShowPipetteWizardFlow(true) } } - const handleAttach: React.MouseEventHandler = () => { + const handleAttach: MouseEventHandler = () => { setFlowType(FLOWS.ATTACH) if (mount === 'extension') { setShowGripperWizardFlow(true) diff --git a/app/src/organisms/ODD/Navigation/NavigationMenu.tsx b/app/src/organisms/ODD/Navigation/NavigationMenu.tsx index d2347bf52b0..0b1cbb40e92 100644 --- a/app/src/organisms/ODD/Navigation/NavigationMenu.tsx +++ b/app/src/organisms/ODD/Navigation/NavigationMenu.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useDispatch } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -21,10 +21,11 @@ import { useLights } from '/app/resources/devices' import { getTopPortalEl } from '/app/App/portal' import { RestartRobotConfirmationModal } from './RestartRobotConfirmationModal' +import type { MouseEventHandler } from 'react' import type { Dispatch } from '/app/redux/types' interface NavigationMenuProps { - onClick: React.MouseEventHandler + onClick: MouseEventHandler robotName: string setShowNavMenu: (showNavMenu: boolean) => void } @@ -37,7 +38,7 @@ export function NavigationMenu(props: NavigationMenuProps): JSX.Element { const [ showRestartRobotConfirmationModal, setShowRestartRobotConfirmationModal, - ] = React.useState(false) + ] = useState(false) const navigate = useNavigate() diff --git a/app/src/organisms/ODD/Navigation/index.tsx b/app/src/organisms/ODD/Navigation/index.tsx index 8b60946f929..e562793e36b 100644 --- a/app/src/organisms/ODD/Navigation/index.tsx +++ b/app/src/organisms/ODD/Navigation/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect, useRef } from 'react' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { useLocation, NavLink } from 'react-router-dom' @@ -32,6 +32,8 @@ import { useScrollPosition } from '/app/local-resources/dom-utils' import { useNetworkConnection } from '/app/resources/networking/hooks/useNetworkConnection' import { getLocalRobot } from '/app/redux/discovery' import { NavigationMenu } from './NavigationMenu' + +import type { Dispatch, SetStateAction } from 'react' import type { ON_DEVICE_DISPLAY_PATHS } from '/app/App/OnDeviceDisplayApp' const NAV_LINKS: Array = [ @@ -65,7 +67,7 @@ const CHAR_LIMIT_NO_ICON = 15 interface NavigationProps { // optionalProps for setting the zIndex and position between multiple sticky elements // used for ProtocolDashboard - setNavMenuIsOpened?: React.Dispatch> + setNavMenuIsOpened?: Dispatch> longPressModalIsOpened?: boolean } export function Navigation(props: NavigationProps): JSX.Element { @@ -73,7 +75,7 @@ export function Navigation(props: NavigationProps): JSX.Element { const { t } = useTranslation('top_navigation') const location = useLocation() const localRobot = useSelector(getLocalRobot) - const [showNavMenu, setShowNavMenu] = React.useState(false) + const [showNavMenu, setShowNavMenu] = useState(false) const robotName = localRobot?.name != null ? localRobot.name : 'no name' // We need to display an icon for what type of network connection (if any) @@ -95,8 +97,8 @@ export function Navigation(props: NavigationProps): JSX.Element { const { scrollRef, isScrolled } = useScrollPosition() - const navBarScrollRef = React.useRef(null) - React.useEffect(() => { + const navBarScrollRef = useRef(null) + useEffect(() => { navBarScrollRef?.current?.scrollIntoView({ behavior: 'auto', inline: 'center', diff --git a/app/src/organisms/ODD/NetworkSettings/SelectAuthenticationType.tsx b/app/src/organisms/ODD/NetworkSettings/SelectAuthenticationType.tsx index b918d48df5e..9f163929220 100644 --- a/app/src/organisms/ODD/NetworkSettings/SelectAuthenticationType.tsx +++ b/app/src/organisms/ODD/NetworkSettings/SelectAuthenticationType.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -21,6 +21,7 @@ import { getNetworkInterfaces, fetchStatus } from '/app/redux/networking' import { useIsUnboxingFlowOngoing } from '/app/redux-resources/config' import { AlternativeSecurityTypeModal } from './AlternativeSecurityTypeModal' +import type { ChangeEvent } from 'react' import type { WifiSecurityType } from '@opentrons/api-client' import type { Dispatch, State } from '/app/redux/types' @@ -44,7 +45,7 @@ export function SelectAuthenticationType({ const [ showAlternativeSecurityTypeModal, setShowAlternativeSecurityTypeModal, - ] = React.useState(false) + ] = useState(false) const securityButtons = [ { @@ -59,11 +60,11 @@ export function SelectAuthenticationType({ }, ] - const handleChange = (event: React.ChangeEvent): void => { + const handleChange = (event: ChangeEvent): void => { setSelectedAuthType(event.target.value as WifiSecurityType) } - React.useEffect(() => { + useEffect(() => { dispatch(fetchStatus(robotName)) }, [robotName, dispatch]) diff --git a/app/src/organisms/ODD/NetworkSettings/SetWifiSsid.tsx b/app/src/organisms/ODD/NetworkSettings/SetWifiSsid.tsx index d2ea891a254..114d906215b 100644 --- a/app/src/organisms/ODD/NetworkSettings/SetWifiSsid.tsx +++ b/app/src/organisms/ODD/NetworkSettings/SetWifiSsid.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useRef } from 'react' import { useTranslation } from 'react-i18next' import { @@ -15,10 +15,12 @@ import { import { FullKeyboard } from '/app/atoms/SoftwareKeyboard' import { useIsUnboxingFlowOngoing } from '/app/redux-resources/config' +import type { Dispatch, SetStateAction } from 'react' + interface SetWifiSsidProps { errorMessage?: string | null inputSsid: string - setInputSsid: React.Dispatch> + setInputSsid: Dispatch> } export function SetWifiSsid({ @@ -27,7 +29,7 @@ export function SetWifiSsid({ setInputSsid, }: SetWifiSsidProps): JSX.Element { const { t } = useTranslation(['device_settings', 'shared']) - const keyboardRef = React.useRef(null) + const keyboardRef = useRef(null) const isUnboxingFlowOngoing = useIsUnboxingFlowOngoing() return ( diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupDeckConfiguration/index.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupDeckConfiguration/index.tsx index a7148788639..4f67deb6da6 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupDeckConfiguration/index.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupDeckConfiguration/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' @@ -26,6 +26,7 @@ import { useMostRecentCompletedAnalysis } from '/app/resources/runs' import { getTopPortalEl } from '/app/App/portal' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' +import type { Dispatch, SetStateAction } from 'react' import type { CutoutFixtureId, CutoutId, @@ -37,7 +38,7 @@ import type { SetupScreens } from '../types' interface ProtocolSetupDeckConfigurationProps { cutoutId: CutoutId | null runId: string - setSetupScreen: React.Dispatch> + setSetupScreen: Dispatch> providedFixtureOptions: CutoutFixtureId[] } @@ -53,14 +54,12 @@ export function ProtocolSetupDeckConfiguration({ 'shared', ]) - const [ - showConfigurationModal, - setShowConfigurationModal, - ] = React.useState(true) - const [ - showDiscardChangeModal, - setShowDiscardChangeModal, - ] = React.useState(false) + const [showConfigurationModal, setShowConfigurationModal] = useState( + true + ) + const [showDiscardChangeModal, setShowDiscardChangeModal] = useState( + false + ) const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) const deckConfig = useNotifyDeckConfigurationQuery()?.data ?? [] diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/index.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/index.tsx index 2d440fc9516..3daa713de88 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/index.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLabware/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' @@ -52,6 +52,7 @@ import { useMostRecentCompletedAnalysis } from '/app/resources/runs' import { LabwareMapView } from './LabwareMapView' import { SingleLabwareModal } from './SingleLabwareModal' +import type { Dispatch, SetStateAction } from 'react' import type { UseQueryResult } from 'react-query' import type { HeaterShakerCloseLatchCreateCommand, @@ -71,7 +72,7 @@ const DECK_CONFIG_POLL_MS = 5000 export interface ProtocolSetupLabwareProps { runId: string - setSetupScreen: React.Dispatch> + setSetupScreen: Dispatch> isConfirmed: boolean setIsConfirmed: (confirmed: boolean) => void } @@ -83,12 +84,12 @@ export function ProtocolSetupLabware({ setIsConfirmed, }: ProtocolSetupLabwareProps): JSX.Element { const { t } = useTranslation('protocol_setup') - const [showMapView, setShowMapView] = React.useState(false) + const [showMapView, setShowMapView] = useState(false) const [ showLabwareDetailsModal, setShowLabwareDetailsModal, - ] = React.useState(false) - const [selectedLabware, setSelectedLabware] = React.useState< + ] = useState(false) + const [selectedLabware, setSelectedLabware] = useState< | (LabwareDefinition2 & { location: LabwareLocation nickName: string | null @@ -289,7 +290,7 @@ function LabwareLatch({ createLiveCommand, isLoading: isLiveCommandLoading, } = useCreateLiveCommandMutation() - const [isRefetchingModules, setIsRefetchingModules] = React.useState(false) + const [isRefetchingModules, setIsRefetchingModules] = useState(false) const isLatchLoading = isLiveCommandLoading || isRefetchingModules || diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/index.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/index.tsx index 153315d294b..d8aa63d4b26 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/index.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupLiquids/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { Fragment, useState } from 'react' import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, @@ -25,12 +25,14 @@ import { SmallButton } from '/app/atoms/buttons' import { useMostRecentCompletedAnalysis } from '/app/resources/runs' import { getTotalVolumePerLiquidId } from '/app/transformations/analysis' import { LiquidDetails } from './LiquidDetails' + +import type { Dispatch, SetStateAction } from 'react' import type { ParsedLiquid, RunTimeCommand } from '@opentrons/shared-data' import type { SetupScreens } from '../types' export interface ProtocolSetupLiquidsProps { runId: string - setSetupScreen: React.Dispatch> + setSetupScreen: Dispatch> isConfirmed: boolean setIsConfirmed: (confirmed: boolean) => void } @@ -95,13 +97,13 @@ export function ProtocolSetupLiquids({ {liquidsInLoadOrder?.map(liquid => ( - + - + ))} @@ -116,7 +118,7 @@ interface LiquidsListProps { export function LiquidsList(props: LiquidsListProps): JSX.Element { const { liquid, runId, commands } = props - const [openItem, setOpenItem] = React.useState(false) + const [openItem, setOpenItem] = useState(false) const labwareByLiquidId = parseLabwareInfoByLiquidId(commands ?? []) return ( diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/FixtureTable.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/FixtureTable.tsx index 3491d7530c7..7d990e112a0 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/FixtureTable.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/FixtureTable.tsx @@ -1,4 +1,5 @@ -import * as React from 'react' +import { useState, Fragment } from 'react' +import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, @@ -29,7 +30,9 @@ import { SmallButton } from '/app/atoms/buttons' import { useDeckConfigurationCompatibility } from '/app/resources/deck_configuration/hooks' import { getRequiredDeckConfig } from '/app/resources/deck_configuration/utils' import { LocationConflictModal } from '/app/organisms/LocationConflictModal' +import { getLocalRobot } from '/app/redux/discovery' +import type { Dispatch, SetStateAction } from 'react' import type { CompletedProtocolAnalysis, CutoutFixtureId, @@ -39,13 +42,11 @@ import type { } from '@opentrons/shared-data' import type { SetupScreens } from '../types' import type { CutoutConfigAndCompatibility } from '/app/resources/deck_configuration/hooks' -import { useSelector } from 'react-redux' -import { getLocalRobot } from '/app/redux/discovery' interface FixtureTableProps { robotType: RobotType mostRecentAnalysis: CompletedProtocolAnalysis | null - setSetupScreen: React.Dispatch> + setSetupScreen: Dispatch> setCutoutId: (cutoutId: CutoutId) => void setProvidedFixtureOptions: (providedFixtureOptions: CutoutFixtureId[]) => void } @@ -134,7 +135,7 @@ export function FixtureTable({ interface FixtureTableItemProps extends CutoutConfigAndCompatibility { lastItem: boolean - setSetupScreen: React.Dispatch> + setSetupScreen: Dispatch> setCutoutId: (cutoutId: CutoutId) => void setProvidedFixtureOptions: (providedFixtureOptions: CutoutFixtureId[]) => void deckDef: DeckDefinition @@ -158,7 +159,7 @@ function FixtureTableItem({ const [ showLocationConflictModal, setShowLocationConflictModal, - ] = React.useState(false) + ] = useState(false) const isCurrentFixtureCompatible = cutoutFixtureId != null && @@ -215,7 +216,7 @@ function FixtureTableItem({ ) } return ( - + {showLocationConflictModal ? ( { @@ -265,6 +266,6 @@ function FixtureTableItem({ {chipLabel} - + ) } diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ModuleTable.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ModuleTable.tsx index 1a0b93c6f57..d4b0a32ad2d 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ModuleTable.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ModuleTable.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' @@ -39,6 +39,7 @@ import { } from '/app/resources/runs' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' +import type { Dispatch, SetStateAction } from 'react' import type { CommandData } from '@opentrons/api-client' import type { CutoutConfig, DeckDefinition } from '@opentrons/shared-data' import type { ModulePrepCommandsType } from '/app/local-resources/modules' @@ -59,7 +60,7 @@ export function ModuleTable(props: ModuleTableProps): JSX.Element { const [ prepCommandErrorMessage, setPrepCommandErrorMessage, - ] = React.useState('') + ] = useState('') const { data: deckConfig } = useNotifyDeckConfigurationQuery({ refetchInterval: DECK_CONFIG_REFETCH_INTERVAL, @@ -119,7 +120,7 @@ interface ModuleTableItemProps { isLoading: boolean module: AttachedProtocolModuleMatch prepCommandErrorMessage: string - setPrepCommandErrorMessage: React.Dispatch> + setPrepCommandErrorMessage: Dispatch> deckDef: DeckDefinition robotName: string } @@ -162,11 +163,11 @@ function ModuleTableItem({ ) const isModuleReady = module.attachedModuleMatch != null - const [showModuleWizard, setShowModuleWizard] = React.useState(false) + const [showModuleWizard, setShowModuleWizard] = useState(false) const [ showLocationConflictModal, setShowLocationConflictModal, - ] = React.useState(false) + ] = useState(false) let moduleStatus: JSX.Element = ( <> diff --git a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ProtocolSetupModulesAndDeck.tsx b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ProtocolSetupModulesAndDeck.tsx index bcbc2b57bba..542ba3d3624 100644 --- a/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ProtocolSetupModulesAndDeck.tsx +++ b/app/src/organisms/ODD/ProtocolSetup/ProtocolSetupModulesAndDeck/ProtocolSetupModulesAndDeck.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' @@ -37,6 +37,7 @@ import { ModuleTable } from './ModuleTable' import { ModulesAndDeckMapView } from './ModulesAndDeckMapView' import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configuration' +import type { Dispatch, SetStateAction } from 'react' import type { CutoutId, CutoutFixtureId } from '@opentrons/shared-data' import type { SetupScreens } from '../types' @@ -45,7 +46,7 @@ const DECK_CONFIG_POLL_MS = 5000 interface ProtocolSetupModulesAndDeckProps { runId: string - setSetupScreen: React.Dispatch> + setSetupScreen: Dispatch> setCutoutId: (cutoutId: CutoutId) => void setProvidedFixtureOptions: (providedFixtureOptions: CutoutFixtureId[]) => void } @@ -62,7 +63,7 @@ export function ProtocolSetupModulesAndDeck({ const { i18n, t } = useTranslation('protocol_setup') const navigate = useNavigate() const runStatus = useRunStatus(runId) - React.useEffect(() => { + useEffect(() => { if (runStatus === RUN_STATUS_STOPPED) { navigate('/protocols') } @@ -70,12 +71,12 @@ export function ProtocolSetupModulesAndDeck({ const [ showSetupInstructionsModal, setShowSetupInstructionsModal, - ] = React.useState(false) - const [showMapView, setShowMapView] = React.useState(false) + ] = useState(false) + const [showMapView, setShowMapView] = useState(false) const [ clearModuleMismatchBanner, setClearModuleMismatchBanner, - ] = React.useState(false) + ] = useState(false) const mostRecentAnalysis = useMostRecentCompletedAnalysis(runId) const deckDef = getDeckDefFromRobotType(FLEX_ROBOT_TYPE) diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/AirGap.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/AirGap.tsx index d1cf5ae2c0a..e5c8efbcc69 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/AirGap.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/AirGap.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' @@ -19,6 +19,7 @@ import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { ACTIONS } from '../constants' +import type { Dispatch } from 'react' import type { QuickTransferSummaryState, QuickTransferSummaryAction, @@ -30,7 +31,7 @@ import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard' interface AirGapProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch kind: FlowRateKind } @@ -38,15 +39,15 @@ export function AirGap(props: AirGapProps): JSX.Element { const { kind, onBack, state, dispatch } = props const { t } = useTranslation('quick_transfer') const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() - const keyboardRef = React.useRef(null) + const keyboardRef = useRef(null) - const [airGapEnabled, setAirGapEnabled] = React.useState( + const [airGapEnabled, setAirGapEnabled] = useState( kind === 'aspirate' ? state.airGapAspirate != null : state.airGapDispense != null ) - const [currentStep, setCurrentStep] = React.useState(1) - const [volume, setVolume] = React.useState( + const [currentStep, setCurrentStep] = useState(1) + const [volume, setVolume] = useState( kind === 'aspirate' ? state.airGapAspirate ?? null : state.airGapDispense ?? null diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BaseSettings.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BaseSettings.tsx index 9f300b335ad..297054b1d03 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BaseSettings.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BaseSettings.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, @@ -17,6 +17,7 @@ import { import { FlowRateEntry } from './FlowRate' import { PipettePath } from './PipettePath' +import type { Dispatch } from 'react' import type { QuickTransferSummaryAction, QuickTransferSummaryState, @@ -24,15 +25,13 @@ import type { interface BaseSettingsProps { state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch } export function BaseSettings(props: BaseSettingsProps): JSX.Element | null { const { state, dispatch } = props const { t } = useTranslation(['quick_transfer', 'shared']) - const [selectedSetting, setSelectedSetting] = React.useState( - null - ) + const [selectedSetting, setSelectedSetting] = useState(null) let pipettePath: string = '' if (state.path === 'single') { diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx index 55984b27d8b..e961b480960 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/BlowOut.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import isEqual from 'lodash/isEqual' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' @@ -22,6 +22,7 @@ import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { ACTIONS } from '../constants' +import type { Dispatch } from 'react' import type { DeckConfiguration } from '@opentrons/shared-data' import type { QuickTransferSummaryState, @@ -35,7 +36,7 @@ import { i18n } from '/app/i18n' interface BlowOutProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch kind: FlowRateKind } @@ -96,11 +97,11 @@ export function BlowOut(props: BlowOutProps): JSX.Element { const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] - const [isBlowOutEnabled, setisBlowOutEnabled] = React.useState( + const [isBlowOutEnabled, setisBlowOutEnabled] = useState( state.blowOut != null ) - const [currentStep, setCurrentStep] = React.useState(1) - const [blowOutLocation, setBlowOutLocation] = React.useState< + const [currentStep, setCurrentStep] = useState(1) + const [blowOutLocation, setBlowOutLocation] = useState< BlowOutLocation | undefined >(state.blowOut) diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx index 0692cc904ac..556ddc181c8 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Delay.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' @@ -8,8 +8,8 @@ import { DIRECTION_COLUMN, Flex, InputField, - RadioButton, POSITION_FIXED, + RadioButton, SPACING, } from '@opentrons/components' import { ANALYTICS_QUICK_TRANSFER_SETTING_SAVED } from '/app/redux/analytics' @@ -18,6 +18,7 @@ import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { ACTIONS } from '../constants' +import type { Dispatch } from 'react' import type { QuickTransferSummaryState, QuickTransferSummaryAction, @@ -29,7 +30,7 @@ import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard' interface DelayProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch kind: FlowRateKind } @@ -37,20 +38,20 @@ export function Delay(props: DelayProps): JSX.Element { const { kind, onBack, state, dispatch } = props const { t } = useTranslation('quick_transfer') const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() - const keyboardRef = React.useRef(null) + const keyboardRef = useRef(null) - const [currentStep, setCurrentStep] = React.useState(1) - const [delayIsEnabled, setDelayIsEnabled] = React.useState( + const [currentStep, setCurrentStep] = useState(1) + const [delayIsEnabled, setDelayIsEnabled] = useState( kind === 'aspirate' ? state.delayAspirate != null : state.delayDispense != null ) - const [delayDuration, setDelayDuration] = React.useState( + const [delayDuration, setDelayDuration] = useState( kind === 'aspirate' ? state.delayAspirate?.delayDuration ?? null : state.delayDispense?.delayDuration ?? null ) - const [position, setPosition] = React.useState( + const [position, setPosition] = useState( kind === 'aspirate' ? state.delayAspirate?.positionFromBottom ?? null : state.delayDispense?.positionFromBottom ?? null diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx index 88279b1c76a..1b2bed604fc 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/FlowRate.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useRef } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' @@ -24,6 +24,7 @@ import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { ACTIONS } from '../constants' +import type { Dispatch } from 'react' import type { SupportedTip } from '@opentrons/shared-data' import type { QuickTransferSummaryState, @@ -34,7 +35,7 @@ import type { interface FlowRateEntryProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch kind: FlowRateKind } @@ -42,9 +43,9 @@ export function FlowRateEntry(props: FlowRateEntryProps): JSX.Element { const { onBack, state, dispatch, kind } = props const { i18n, t } = useTranslation(['quick_transfer', 'shared']) const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() - const keyboardRef = React.useRef(null) + const keyboardRef = useRef(null) - const [flowRate, setFlowRate] = React.useState( + const [flowRate, setFlowRate] = useState( kind === 'aspirate' ? state.aspirateFlowRate : state.dispenseFlowRate ) diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Mix.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Mix.tsx index 3774662bc38..b6c7862310c 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Mix.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/Mix.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' @@ -8,8 +8,8 @@ import { DIRECTION_COLUMN, Flex, InputField, - RadioButton, POSITION_FIXED, + RadioButton, SPACING, } from '@opentrons/components' @@ -18,19 +18,20 @@ import { getTopPortalEl } from '/app/App/portal' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { ACTIONS } from '../constants' +import { i18n } from '/app/i18n' +import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard' +import type { Dispatch } from 'react' import type { QuickTransferSummaryState, QuickTransferSummaryAction, FlowRateKind, } from '../types' -import { i18n } from '/app/i18n' -import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard' interface MixProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch kind: FlowRateKind } @@ -38,20 +39,20 @@ export function Mix(props: MixProps): JSX.Element { const { kind, onBack, state, dispatch } = props const { t } = useTranslation('quick_transfer') const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() - const keyboardRef = React.useRef(null) + const keyboardRef = useRef(null) - const [mixIsEnabled, setMixIsEnabled] = React.useState( + const [mixIsEnabled, setMixIsEnabled] = useState( kind === 'aspirate' ? state.mixOnAspirate != null : state.mixOnDispense != null ) - const [currentStep, setCurrentStep] = React.useState(1) - const [mixVolume, setMixVolume] = React.useState( + const [currentStep, setCurrentStep] = useState(1) + const [mixVolume, setMixVolume] = useState( kind === 'aspirate' ? state.mixOnAspirate?.mixVolume ?? null : state.mixOnDispense?.mixVolume ?? null ) - const [mixReps, setMixReps] = React.useState( + const [mixReps, setMixReps] = useState( kind === 'aspirate' ? state.mixOnAspirate?.repititions ?? null : state.mixOnDispense?.repititions ?? null diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx index 9db8923bd58..eabbe9fc678 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/PipettePath.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useRef } from 'react' import isEqual from 'lodash/isEqual' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' @@ -9,8 +9,8 @@ import { DIRECTION_COLUMN, Flex, InputField, - RadioButton, POSITION_FIXED, + RadioButton, SPACING, } from '@opentrons/components' @@ -25,6 +25,7 @@ import { ACTIONS } from '../constants' import { i18n } from '/app/i18n' import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard' +import type { Dispatch } from 'react' import type { PathOption, QuickTransferSummaryState, @@ -35,25 +36,25 @@ import type { interface PipettePathProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch } export function PipettePath(props: PipettePathProps): JSX.Element { const { onBack, state, dispatch } = props const { t } = useTranslation('quick_transfer') const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() - const keyboardRef = React.useRef(null) + const keyboardRef = useRef(null) const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] - const [selectedPath, setSelectedPath] = React.useState(state.path) - const [currentStep, setCurrentStep] = React.useState(1) - const [blowOutLocation, setBlowOutLocation] = React.useState< + const [selectedPath, setSelectedPath] = useState(state.path) + const [currentStep, setCurrentStep] = useState(1) + const [blowOutLocation, setBlowOutLocation] = useState< BlowOutLocation | undefined >(state.blowOut) - const [disposalVolume, setDisposalVolume] = React.useState< - number | undefined - >(state?.disposalVolume) + const [disposalVolume, setDisposalVolume] = useState( + state?.disposalVolume + ) const maxPipetteVolume = Object.values(state.pipette.liquids)[0].maxVolume const tipVolume = Object.values(state.tipRack.wells)[0].totalLiquidVolume diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx index 92082cf9c7d..8a5abf1a169 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TipPosition.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { @@ -19,6 +19,7 @@ import { ACTIONS } from '../constants' import { createPortal } from 'react-dom' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' +import type { Dispatch } from 'react' import type { QuickTransferSummaryState, QuickTransferSummaryAction, @@ -28,7 +29,7 @@ import type { interface TipPositionEntryProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch kind: FlowRateKind // TODO: rename flowRateKind to be generic } @@ -36,9 +37,9 @@ export function TipPositionEntry(props: TipPositionEntryProps): JSX.Element { const { onBack, state, dispatch, kind } = props const { i18n, t } = useTranslation(['quick_transfer', 'shared']) const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() - const keyboardRef = React.useRef(null) + const keyboardRef = useRef(null) - const [tipPosition, setTipPosition] = React.useState( + const [tipPosition, setTipPosition] = useState( kind === 'aspirate' ? state.tipPositionAspirate : state.tipPositionDispense ) diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx index a30b204d4a4..41780d65181 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' @@ -8,8 +8,8 @@ import { DIRECTION_COLUMN, Flex, InputField, - RadioButton, POSITION_FIXED, + RadioButton, SPACING, } from '@opentrons/components' @@ -21,6 +21,7 @@ import { i18n } from '/app/i18n' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard' +import type { Dispatch } from 'react' import type { QuickTransferSummaryState, QuickTransferSummaryAction, @@ -30,7 +31,7 @@ import type { interface TouchTipProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch kind: FlowRateKind } @@ -38,15 +39,15 @@ export function TouchTip(props: TouchTipProps): JSX.Element { const { kind, onBack, state, dispatch } = props const { t } = useTranslation('quick_transfer') const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() - const keyboardRef = React.useRef(null) + const keyboardRef = useRef(null) - const [touchTipIsEnabled, setTouchTipIsEnabled] = React.useState( + const [touchTipIsEnabled, setTouchTipIsEnabled] = useState( kind === 'aspirate' ? state.touchTipAspirate != null : state.touchTipDispense != null ) - const [currentStep, setCurrentStep] = React.useState(1) - const [position, setPosition] = React.useState( + const [currentStep, setCurrentStep] = useState(1) + const [position, setPosition] = useState( kind === 'aspirate' ? state.touchTipAspirate ?? null : state.touchTipDispense ?? null diff --git a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/index.tsx b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/index.tsx index 2911b7975d1..33a603b6f75 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/index.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { @@ -34,13 +34,14 @@ import { TouchTip } from './TouchTip' import { AirGap } from './AirGap' import { BlowOut } from './BlowOut' +import type { Dispatch } from 'react' import type { QuickTransferSummaryAction, QuickTransferSummaryState, } from '../types' interface QuickTransferAdvancedSettingsProps { state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch } export function QuickTransferAdvancedSettings( @@ -48,13 +49,11 @@ export function QuickTransferAdvancedSettings( ): JSX.Element | null { const { state, dispatch } = props const { t, i18n } = useTranslation(['quick_transfer', 'shared']) - const [selectedSetting, setSelectedSetting] = React.useState( - null - ) + const [selectedSetting, setSelectedSetting] = useState(null) const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() const { makeSnackbar } = useToaster() - React.useEffect(() => { + useEffect(() => { trackEventWithRobotSerial({ name: ANALYTICS_QUICK_TRANSFER_ADVANCED_SETTINGS_TAB, properties: {}, diff --git a/app/src/organisms/ODD/QuickTransferFlow/SelectDestLabware.tsx b/app/src/organisms/ODD/QuickTransferFlow/SelectDestLabware.tsx index ba90e54de4d..62921e8e08d 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/SelectDestLabware.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/SelectDestLabware.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Flex, @@ -15,6 +15,7 @@ import { import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { getCompatibleLabwareByCategory } from './utils' +import type { ComponentProps, Dispatch } from 'react' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { SmallButton } from '/app/atoms/buttons' import type { LabwareFilter } from '/app/local-resources/labware' @@ -26,9 +27,9 @@ import type { interface SelectDestLabwareProps { onNext: () => void onBack: () => void - exitButtonProps: React.ComponentProps + exitButtonProps: ComponentProps state: QuickTransferWizardState - dispatch: React.Dispatch + dispatch: Dispatch } export function SelectDestLabware( @@ -44,10 +45,8 @@ export function SelectDestLabware( if (state.pipette?.channels === 1) { labwareDisplayCategoryFilters.push('tubeRack') } - const [selectedCategory, setSelectedCategory] = React.useState( - 'all' - ) - const [selectedLabware, setSelectedLabware] = React.useState< + const [selectedCategory, setSelectedCategory] = useState('all') + const [selectedLabware, setSelectedLabware] = useState< LabwareDefinition2 | 'source' | undefined >(state.destination) diff --git a/app/src/organisms/ODD/QuickTransferFlow/SelectDestWells.tsx b/app/src/organisms/ODD/QuickTransferFlow/SelectDestWells.tsx index 0cb402f6ee8..41dcf155fb7 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/SelectDestWells.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/SelectDestWells.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import without from 'lodash/without' @@ -24,6 +24,12 @@ import { RECTANGULAR_WELL_96_PLATE_DEFINITION_URI, } from './SelectSourceWells' +import type { + ComponentProps, + Dispatch, + SetStateAction, + MouseEvent, +} from 'react' import type { SmallButton } from '/app/atoms/buttons' import type { OddModalHeaderBaseProps } from '/app/molecules/OddModal/types' import type { @@ -35,7 +41,7 @@ interface SelectDestWellsProps { onNext: () => void onBack: () => void state: QuickTransferWizardState - dispatch: React.Dispatch + dispatch: Dispatch } export function SelectDestWells(props: SelectDestWellsProps): JSX.Element { @@ -53,12 +59,11 @@ export function SelectDestWells(props: SelectDestWellsProps): JSX.Element { const [ showNumberWellsSelectedErrorModal, setShowNumberWellsSelectedErrorModal, - ] = React.useState(false) - const [selectedWells, setSelectedWells] = React.useState(destinationWellGroup) - const [ - isNumberWellsSelectedError, - setIsNumberWellsSelectedError, - ] = React.useState(false) + ] = useState(false) + const [selectedWells, setSelectedWells] = useState(destinationWellGroup) + const [isNumberWellsSelectedError, setIsNumberWellsSelectedError] = useState( + false + ) const selectedWellCount = Object.keys(selectedWells).length const sourceWellCount = state.sourceWells?.length ?? 0 @@ -88,7 +93,7 @@ export function SelectDestWells(props: SelectDestWellsProps): JSX.Element { } const is384WellPlate = labwareDefinition?.parameters.format === '384Standard' - const [analyticsStartTime] = React.useState(new Date()) + const [analyticsStartTime] = useState(new Date()) const handleClickNext = (): void => { if ( @@ -130,10 +135,10 @@ export function SelectDestWells(props: SelectDestWellsProps): JSX.Element { } } - const resetButtonProps: React.ComponentProps = { + const resetButtonProps: ComponentProps = { buttonType: 'tertiaryLowLight', buttonText: t('shared:reset'), - onClick: (e: React.MouseEvent) => { + onClick: (e: MouseEvent) => { setIsNumberWellsSelectedError(false) setSelectedWells({}) e.currentTarget.blur?.() @@ -214,9 +219,7 @@ function NumberWellsSelectedErrorModal({ selectionUnit, selectionUnits, }: { - setShowNumberWellsSelectedErrorModal: React.Dispatch< - React.SetStateAction - > + setShowNumberWellsSelectedErrorModal: Dispatch> wellCount: number selectionUnit: string selectionUnits: string diff --git a/app/src/organisms/ODD/QuickTransferFlow/SelectPipette.tsx b/app/src/organisms/ODD/QuickTransferFlow/SelectPipette.tsx index 3331800e1a9..0b01a93977d 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/SelectPipette.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/SelectPipette.tsx @@ -1,18 +1,19 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { + DIRECTION_COLUMN, Flex, - SPACING, LegacyStyledText, - TYPOGRAPHY, - DIRECTION_COLUMN, RadioButton, + SPACING, + TYPOGRAPHY, } from '@opentrons/components' import { useInstrumentsQuery } from '@opentrons/react-api-client' import { RIGHT, LEFT } from '@opentrons/shared-data' import { usePipetteSpecsV2 } from '/app/local-resources/instruments' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' +import type { ComponentProps, Dispatch } from 'react' import type { PipetteData, Mount } from '@opentrons/api-client' import type { SmallButton } from '/app/atoms/buttons' import type { @@ -23,9 +24,9 @@ import type { interface SelectPipetteProps { onNext: () => void onBack: () => void - exitButtonProps: React.ComponentProps + exitButtonProps: ComponentProps state: QuickTransferWizardState - dispatch: React.Dispatch + dispatch: Dispatch } export function SelectPipette(props: SelectPipetteProps): JSX.Element { @@ -44,9 +45,9 @@ export function SelectPipette(props: SelectPipetteProps): JSX.Element { const rightPipetteSpecs = usePipetteSpecsV2(rightPipette?.instrumentModel) // automatically select 96 channel if it is attached - const [selectedPipette, setSelectedPipette] = React.useState< - Mount | undefined - >(leftPipetteSpecs?.channels === 96 ? LEFT : state.mount) + const [selectedPipette, setSelectedPipette] = useState( + leftPipetteSpecs?.channels === 96 ? LEFT : state.mount + ) const handleClickNext = (): void => { const selectedPipetteSpecs = diff --git a/app/src/organisms/ODD/QuickTransferFlow/SelectSourceLabware.tsx b/app/src/organisms/ODD/QuickTransferFlow/SelectSourceLabware.tsx index 2d4752a5aa1..c51e4782c1d 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/SelectSourceLabware.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/SelectSourceLabware.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Flex, @@ -15,6 +15,7 @@ import { import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { getCompatibleLabwareByCategory } from './utils' +import type { ComponentProps, Dispatch } from 'react' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { SmallButton } from '/app/atoms/buttons' import type { LabwareFilter } from '/app/local-resources/labware' @@ -26,9 +27,9 @@ import type { interface SelectSourceLabwareProps { onNext: () => void onBack: () => void - exitButtonProps: React.ComponentProps + exitButtonProps: ComponentProps state: QuickTransferWizardState - dispatch: React.Dispatch + dispatch: Dispatch } export function SelectSourceLabware( @@ -44,11 +45,9 @@ export function SelectSourceLabware( if (state.pipette?.channels === 1) { labwareDisplayCategoryFilters.push('tubeRack') } - const [selectedCategory, setSelectedCategory] = React.useState( - 'all' - ) + const [selectedCategory, setSelectedCategory] = useState('all') - const [selectedLabware, setSelectedLabware] = React.useState< + const [selectedLabware, setSelectedLabware] = useState< LabwareDefinition2 | undefined >(state.source) diff --git a/app/src/organisms/ODD/QuickTransferFlow/SelectSourceWells.tsx b/app/src/organisms/ODD/QuickTransferFlow/SelectSourceWells.tsx index a78ec884560..1a643780e08 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/SelectSourceWells.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/SelectSourceWells.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import without from 'lodash/without' import { @@ -14,6 +14,7 @@ import { WellSelection } from '/app/organisms/WellSelection' import { ANALYTICS_QUICK_TRANSFER_WELL_SELECTION_DURATION } from '/app/redux/analytics' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' +import type { ComponentProps, Dispatch, MouseEvent } from 'react' import type { SmallButton } from '/app/atoms/buttons' import type { QuickTransferWizardState, @@ -24,7 +25,7 @@ interface SelectSourceWellsProps { onNext: () => void onBack: () => void state: QuickTransferWizardState - dispatch: React.Dispatch + dispatch: Dispatch } export const CIRCULAR_WELL_96_PLATE_DEFINITION_URI = @@ -42,8 +43,8 @@ export function SelectSourceWells(props: SelectSourceWellsProps): JSX.Element { return { ...acc, [well]: null } }, {}) - const [selectedWells, setSelectedWells] = React.useState(sourceWellGroup) - const [startingTimeStamp] = React.useState(new Date()) + const [selectedWells, setSelectedWells] = useState(sourceWellGroup) + const [startingTimeStamp] = useState(new Date()) const is384WellPlate = state.source?.parameters.format === '384Standard' const handleClickNext = (): void => { @@ -62,10 +63,10 @@ export function SelectSourceWells(props: SelectSourceWellsProps): JSX.Element { onNext() } - const resetButtonProps: React.ComponentProps = { + const resetButtonProps: ComponentProps = { buttonType: 'tertiaryLowLight', buttonText: t('shared:reset'), - onClick: (e: React.MouseEvent) => { + onClick: (e: MouseEvent) => { setSelectedWells({}) e.currentTarget.blur?.() }, diff --git a/app/src/organisms/ODD/QuickTransferFlow/SelectTipRack.tsx b/app/src/organisms/ODD/QuickTransferFlow/SelectTipRack.tsx index e8ebd52d90c..f5fd1abae85 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/SelectTipRack.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/SelectTipRack.tsx @@ -1,14 +1,15 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { - Flex, - SPACING, DIRECTION_COLUMN, + Flex, RadioButton, + SPACING, } from '@opentrons/components' import { getAllDefinitions } from '@opentrons/shared-data' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' +import type { ComponentProps, Dispatch } from 'react' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { SmallButton } from '/app/atoms/buttons' import type { @@ -19,9 +20,9 @@ import type { interface SelectTipRackProps { onNext: () => void onBack: () => void - exitButtonProps: React.ComponentProps + exitButtonProps: ComponentProps state: QuickTransferWizardState - dispatch: React.Dispatch + dispatch: Dispatch } export function SelectTipRack(props: SelectTipRackProps): JSX.Element { @@ -32,7 +33,7 @@ export function SelectTipRack(props: SelectTipRackProps): JSX.Element { const selectedPipetteDefaultTipracks = state.pipette?.liquids.default.defaultTipracks ?? [] - const [selectedTipRack, setSelectedTipRack] = React.useState< + const [selectedTipRack, setSelectedTipRack] = useState< LabwareDefinition2 | undefined >(state.tipRack) diff --git a/app/src/organisms/ODD/QuickTransferFlow/SummaryAndSettings.tsx b/app/src/organisms/ODD/QuickTransferFlow/SummaryAndSettings.tsx index 6ddba5ca50e..2567b703b93 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/SummaryAndSettings.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/SummaryAndSettings.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useReducer } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { useQueryClient } from 'react-query' @@ -33,11 +33,12 @@ import { SaveOrRunModal } from './SaveOrRunModal' import { getInitialSummaryState, createQuickTransferFile } from './utils' import { quickTransferSummaryReducer } from './reducers' +import type { ComponentProps } from 'react' import type { SmallButton } from '/app/atoms/buttons' import type { QuickTransferWizardState } from './types' interface SummaryAndSettingsProps { - exitButtonProps: React.ComponentProps + exitButtonProps: ComponentProps state: QuickTransferWizardState analyticsStartTime: Date } @@ -51,18 +52,14 @@ export function SummaryAndSettings( const queryClient = useQueryClient() const host = useHost() const { t } = useTranslation(['quick_transfer', 'shared']) - const [showSaveOrRunModal, setShowSaveOrRunModal] = React.useState( - false - ) + const [showSaveOrRunModal, setShowSaveOrRunModal] = useState(false) const displayCategory: string[] = [ 'overview', 'advanced_settings', 'tip_management', ] - const [selectedCategory, setSelectedCategory] = React.useState( - 'overview' - ) + const [selectedCategory, setSelectedCategory] = useState('overview') const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] const initialSummaryState = getInitialSummaryState({ @@ -71,7 +68,7 @@ export function SummaryAndSettings( state: wizardFlowState, deckConfig, }) - const [state, dispatch] = React.useReducer( + const [state, dispatch] = useReducer( quickTransferSummaryReducer, initialSummaryState ) diff --git a/app/src/organisms/ODD/QuickTransferFlow/TipManagement/ChangeTip.tsx b/app/src/organisms/ODD/QuickTransferFlow/TipManagement/ChangeTip.tsx index 7c72dbe202e..dd3869ff329 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/TipManagement/ChangeTip.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/TipManagement/ChangeTip.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' @@ -16,6 +16,7 @@ import { getTopPortalEl } from '/app/App/portal' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' +import type { Dispatch } from 'react' import type { ChangeTipOptions, QuickTransferSummaryState, @@ -25,7 +26,7 @@ import type { interface ChangeTipProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch } export function ChangeTip(props: ChangeTipProps): JSX.Element { @@ -53,7 +54,7 @@ export function ChangeTip(props: ChangeTipProps): JSX.Element { const [ selectedChangeTipOption, setSelectedChangeTipOption, - ] = React.useState(state.changeTip) + ] = useState(state.changeTip) const handleClickSave = (): void => { if (selectedChangeTipOption !== state.changeTip) { diff --git a/app/src/organisms/ODD/QuickTransferFlow/TipManagement/TipDropLocation.tsx b/app/src/organisms/ODD/QuickTransferFlow/TipManagement/TipDropLocation.tsx index 6dae428684b..b61a73389cd 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/TipManagement/TipDropLocation.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/TipManagement/TipDropLocation.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' import { @@ -21,6 +21,7 @@ import { useNotifyDeckConfigurationQuery } from '/app/resources/deck_configurati import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' +import type { Dispatch } from 'react' import type { QuickTransferSummaryState, QuickTransferSummaryAction, @@ -30,7 +31,7 @@ import type { CutoutConfig } from '@opentrons/shared-data' interface TipDropLocationProps { onBack: () => void state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch } export function TipDropLocation(props: TipDropLocationProps): JSX.Element { @@ -56,7 +57,7 @@ export function TipDropLocation(props: TipDropLocationProps): JSX.Element { const [ selectedTipDropLocation, setSelectedTipDropLocation, - ] = React.useState(state.dropTipLocation) + ] = useState(state.dropTipLocation) const handleClickSave = (): void => { if (selectedTipDropLocation.cutoutId !== state.dropTipLocation.cutoutId) { diff --git a/app/src/organisms/ODD/QuickTransferFlow/TipManagement/index.tsx b/app/src/organisms/ODD/QuickTransferFlow/TipManagement/index.tsx index 03f33c965f2..4a87e67c2c7 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/TipManagement/index.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/TipManagement/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { ALIGN_CENTER, @@ -21,6 +21,7 @@ import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' import { ChangeTip } from './ChangeTip' import { TipDropLocation } from './TipDropLocation' +import type { Dispatch } from 'react' import type { QuickTransferSummaryAction, QuickTransferSummaryState, @@ -28,18 +29,16 @@ import type { interface TipManagementProps { state: QuickTransferSummaryState - dispatch: React.Dispatch + dispatch: Dispatch } export function TipManagement(props: TipManagementProps): JSX.Element | null { const { state, dispatch } = props const { t } = useTranslation(['quick_transfer', 'shared']) const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() - const [selectedSetting, setSelectedSetting] = React.useState( - null - ) + const [selectedSetting, setSelectedSetting] = useState(null) - React.useEffect(() => { + useEffect(() => { trackEventWithRobotSerial({ name: ANALYTICS_QUICK_TRANSFER_TIP_MANAGEMENT_TAB, properties: {}, diff --git a/app/src/organisms/ODD/QuickTransferFlow/VolumeEntry.tsx b/app/src/organisms/ODD/QuickTransferFlow/VolumeEntry.tsx index 0a6526676d6..58bf2f570e7 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/VolumeEntry.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/VolumeEntry.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useRef } from 'react' import { useTranslation } from 'react-i18next' import { @@ -14,6 +14,7 @@ import { NumericalKeyboard } from '/app/atoms/SoftwareKeyboard' import { getVolumeRange } from './utils' import { CONSOLIDATE, DISTRIBUTE } from './constants' +import type { ComponentProps, Dispatch } from 'react' import type { SmallButton } from '/app/atoms/buttons' import type { QuickTransferWizardState, @@ -23,17 +24,17 @@ import type { interface VolumeEntryProps { onNext: () => void onBack: () => void - exitButtonProps: React.ComponentProps + exitButtonProps: ComponentProps state: QuickTransferWizardState - dispatch: React.Dispatch + dispatch: Dispatch } export function VolumeEntry(props: VolumeEntryProps): JSX.Element { const { onNext, onBack, exitButtonProps, state, dispatch } = props const { i18n, t } = useTranslation(['quick_transfer', 'shared']) - const keyboardRef = React.useRef(null) + const keyboardRef = useRef(null) - const [volume, setVolume] = React.useState( + const [volume, setVolume] = useState( state.volume ? state.volume.toString() : '' ) const volumeRange = getVolumeRange(state) diff --git a/app/src/organisms/ODD/QuickTransferFlow/index.tsx b/app/src/organisms/ODD/QuickTransferFlow/index.tsx index f7d94a46000..fb0a76b6f47 100644 --- a/app/src/organisms/ODD/QuickTransferFlow/index.tsx +++ b/app/src/organisms/ODD/QuickTransferFlow/index.tsx @@ -1,10 +1,10 @@ -import * as React from 'react' +import { useState, useReducer } from 'react' import { useNavigate } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { - useConditionalConfirm, - StepMeter, POSITION_STICKY, + StepMeter, + useConditionalConfirm, } from '@opentrons/components' import { ANALYTICS_QUICK_TRANSFER_EXIT_EARLY } from '/app/redux/analytics' import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics' @@ -20,6 +20,7 @@ import { VolumeEntry } from './VolumeEntry' import { SummaryAndSettings } from './SummaryAndSettings' import { quickTransferWizardReducer } from './reducers' +import type { ComponentProps } from 'react' import type { SmallButton } from '/app/atoms/buttons' import type { QuickTransferWizardState } from './types' @@ -30,13 +31,13 @@ export const QuickTransferFlow = (): JSX.Element => { const navigate = useNavigate() const { i18n, t } = useTranslation(['quick_transfer', 'shared']) const { trackEventWithRobotSerial } = useTrackEventWithRobotSerial() - const [state, dispatch] = React.useReducer( + const [state, dispatch] = useReducer( quickTransferWizardReducer, initialQuickTransferState ) - const [currentStep, setCurrentStep] = React.useState(0) + const [currentStep, setCurrentStep] = useState(0) - const [analyticsStartTime] = React.useState(new Date()) + const [analyticsStartTime] = useState(new Date()) const { confirm: confirmExit, @@ -52,7 +53,7 @@ export const QuickTransferFlow = (): JSX.Element => { navigate('/quick-transfer') }, true) - const exitButtonProps: React.ComponentProps = { + const exitButtonProps: ComponentProps = { buttonType: 'tertiaryLowLight', buttonText: i18n.format(t('shared:exit'), 'capitalize'), onClick: confirmExit, diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/LanguageSetting.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/LanguageSetting.tsx index a935a5571ad..49f58e26993 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/LanguageSetting.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/LanguageSetting.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { Fragment } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import styled from 'styled-components' @@ -17,6 +17,7 @@ import { LANGUAGES } from '/app/i18n' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { getAppLanguage, updateConfigValue } from '/app/redux/config' +import type { ChangeEvent } from 'react' import type { Dispatch } from '/app/redux/types' import type { SetSettingOption } from './types' @@ -49,7 +50,7 @@ export function LanguageSetting({ const appLanguage = useSelector(getAppLanguage) - const handleChange = (event: React.ChangeEvent): void => { + const handleChange = (event: ChangeEvent): void => { dispatch(updateConfigValue('language.appLanguage', event.target.value)) } @@ -68,7 +69,7 @@ export function LanguageSetting({ padding={`${SPACING.spacing16} ${SPACING.spacing40} ${SPACING.spacing40} ${SPACING.spacing40}`} > {LANGUAGES.map(lng => ( - + - + ))} diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsJoinOtherNetwork.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsJoinOtherNetwork.tsx index fed06e4572c..f3645066924 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsJoinOtherNetwork.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/NetworkSettings/RobotSettingsJoinOtherNetwork.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { DIRECTION_COLUMN, Flex } from '@opentrons/components' @@ -6,11 +6,12 @@ import { DIRECTION_COLUMN, Flex } from '@opentrons/components' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' import { SetWifiSsid } from '../../NetworkSettings' +import type { Dispatch, SetStateAction } from 'react' import type { SetSettingOption } from '../types' interface RobotSettingsJoinOtherNetworkProps { setCurrentOption: SetSettingOption - setSelectedSsid: React.Dispatch> + setSelectedSsid: Dispatch> } /** @@ -22,8 +23,8 @@ export function RobotSettingsJoinOtherNetwork({ }: RobotSettingsJoinOtherNetworkProps): JSX.Element { const { i18n, t } = useTranslation('device_settings') - const [inputSsid, setInputSsid] = React.useState('') - const [errorMessage, setErrorMessage] = React.useState(null) + const [inputSsid, setInputSsid] = useState('') + const [errorMessage, setErrorMessage] = useState(null) const handleContinue = (): void => { if (inputSsid.length >= 2 && inputSsid.length <= 32) { diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/TouchScreenSleep.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/TouchScreenSleep.tsx index ea3d879088f..cb123b261be 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/TouchScreenSleep.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/TouchScreenSleep.tsx @@ -1,12 +1,12 @@ -import * as React from 'react' +import { useEffect, useRef } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { DIRECTION_COLUMN, Flex, - SPACING, RadioButton, + SPACING, } from '@opentrons/components' import { ChildNavigation } from '/app/organisms/ODD/ChildNavigation' @@ -16,6 +16,7 @@ import { } from '/app/redux/config' import { SLEEP_NEVER_MS } from '/app/local-resources/config' +import type { ChangeEvent } from 'react' import type { Dispatch } from '/app/redux/types' import type { SetSettingOption } from './types' @@ -31,7 +32,7 @@ export function TouchScreenSleep({ const { t } = useTranslation(['device_settings']) const { sleepMs } = useSelector(getOnDeviceDisplaySettings) ?? SLEEP_NEVER_MS const dispatch = useDispatch() - const screenRef = React.useRef(null) + const screenRef = useRef(null) // Note (kj:02/10/2023) value's unit is ms const settingsButtons = [ @@ -44,7 +45,7 @@ export function TouchScreenSleep({ { label: t('one_hour'), value: SLEEP_TIME_MS * 60 }, ] - const handleChange = (event: React.ChangeEvent): void => { + const handleChange = (event: ChangeEvent): void => { dispatch( updateConfigValue( 'onDeviceDisplaySettings.sleepMs', @@ -53,7 +54,7 @@ export function TouchScreenSleep({ ) } - React.useEffect(() => { + useEffect(() => { if (screenRef.current != null) screenRef.current.scrollIntoView() }, []) diff --git a/app/src/organisms/ODD/RobotSettingsDashboard/UpdateChannel.tsx b/app/src/organisms/ODD/RobotSettingsDashboard/UpdateChannel.tsx index c9668e8a079..642aeae4af3 100644 --- a/app/src/organisms/ODD/RobotSettingsDashboard/UpdateChannel.tsx +++ b/app/src/organisms/ODD/RobotSettingsDashboard/UpdateChannel.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { Fragment } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import styled from 'styled-components' @@ -22,6 +22,7 @@ import { updateConfigValue, } from '/app/redux/config' +import type { ChangeEvent } from 'react' import type { Dispatch } from '/app/redux/types' interface LabelProps { @@ -59,7 +60,7 @@ export function UpdateChannel({ ? channelOptions.filter(option => option.value !== 'alpha') : channelOptions - const handleChange = (event: React.ChangeEvent): void => { + const handleChange = (event: ChangeEvent): void => { dispatch(updateConfigValue('update.channel', event.target.value)) } @@ -87,7 +88,7 @@ export function UpdateChannel({ marginTop={SPACING.spacing24} > {modifiedChannelOptions.map(radio => ( - + ) : null} - + ))} diff --git a/app/src/organisms/PipetteWizardFlows/ChoosePipette.tsx b/app/src/organisms/PipetteWizardFlows/ChoosePipette.tsx index f8d31f1adec..1a0b45001c2 100644 --- a/app/src/organisms/PipetteWizardFlows/ChoosePipette.tsx +++ b/app/src/organisms/PipetteWizardFlows/ChoosePipette.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useSelector } from 'react-redux' import { css } from 'styled-components' @@ -46,6 +46,7 @@ import { ExitModal } from './ExitModal' import { FLOWS } from './constants' import { getIsGantryEmpty } from './utils' +import type { Dispatch, SetStateAction, ReactNode } from 'react' import type { StyleProps } from '@opentrons/components' import type { PipetteMount } from '@opentrons/shared-data' import type { SelectablePipettes } from './types' @@ -108,7 +109,7 @@ const SELECTED_OPTIONS_STYLE = css` interface ChoosePipetteProps { proceed: () => void selectedPipette: SelectablePipettes - setSelectedPipette: React.Dispatch> + setSelectedPipette: Dispatch> exit: () => void mount: PipetteMount } @@ -117,10 +118,9 @@ export const ChoosePipette = (props: ChoosePipetteProps): JSX.Element => { const isOnDevice = useSelector(getIsOnDevice) const { t } = useTranslation(['pipette_wizard_flows', 'shared']) const attachedPipettesByMount = useAttachedPipettesFromInstrumentsQuery() - const [ - showExitConfirmation, - setShowExitConfirmation, - ] = React.useState(false) + const [showExitConfirmation, setShowExitConfirmation] = useState( + false + ) const bothMounts = getIsGantryEmpty(attachedPipettesByMount) ? t('ninety_six_channel', { @@ -281,7 +281,7 @@ export const ChoosePipette = (props: ChoosePipetteProps): JSX.Element => { interface PipetteMountOptionProps extends StyleProps { isSelected: boolean onClick: () => void - children: React.ReactNode + children: ReactNode } function PipetteMountOption(props: PipetteMountOptionProps): JSX.Element { const { isSelected, onClick, children, ...styleProps } = props diff --git a/app/src/organisms/PipetteWizardFlows/DetachPipette.tsx b/app/src/organisms/PipetteWizardFlows/DetachPipette.tsx index 46e5f92e389..9d83f3d3e75 100644 --- a/app/src/organisms/PipetteWizardFlows/DetachPipette.tsx +++ b/app/src/organisms/PipetteWizardFlows/DetachPipette.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { css } from 'styled-components' import { RIGHT, WEIGHT_OF_96_CHANNEL } from '@opentrons/shared-data' @@ -27,12 +27,14 @@ import { Skeleton } from '/app/atoms/Skeleton' import { SmallButton } from '/app/atoms/buttons' import { BODY_STYLE, SECTIONS } from './constants' import { getPipetteAnimations, getPipetteAnimations96 } from './utils' -import type { PipetteWizardStepProps } from './types' + +import type { Dispatch, ReactNode, SetStateAction } from 'react' import type { PipetteData } from '@opentrons/api-client' +import type { PipetteWizardStepProps } from './types' interface DetachPipetteProps extends PipetteWizardStepProps { isFetching: boolean - setFetching: React.Dispatch> + setFetching: Dispatch> } const BACKGROUND_SIZE = '47rem' @@ -82,7 +84,7 @@ export const DetachPipette = (props: DetachPipetteProps): JSX.Element => { flowType, section: SECTIONS.DETACH_PIPETTE, } - const memoizedAttachedPipettes = React.useMemo(() => attachedPipettes, []) + const memoizedAttachedPipettes = useMemo(() => attachedPipettes, []) const is96ChannelPipette = memoizedAttachedPipettes[mount]?.instrumentName === 'p1000_96' const pipetteName = @@ -121,10 +123,9 @@ export const DetachPipette = (props: DetachPipetteProps): JSX.Element => { }) } - const [ - showPipetteStillAttached, - setShowPipetteStillAttached, - ] = React.useState(false) + const [showPipetteStillAttached, setShowPipetteStillAttached] = useState( + false + ) const handleOnClick = (): void => { setFetching(true) @@ -142,7 +143,7 @@ export const DetachPipette = (props: DetachPipetteProps): JSX.Element => { } const channel = memoizedAttachedPipettes[mount]?.data.channels - let bodyText: React.ReactNode =
+ let bodyText: ReactNode =
if (isFetching) { bodyText = ( <> diff --git a/app/src/organisms/PipetteWizardFlows/Results.tsx b/app/src/organisms/PipetteWizardFlows/Results.tsx index 96ed9098ac9..4ede1cb6828 100644 --- a/app/src/organisms/PipetteWizardFlows/Results.tsx +++ b/app/src/organisms/PipetteWizardFlows/Results.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { css } from 'styled-components' import { useTranslation } from 'react-i18next' import { @@ -22,6 +22,7 @@ import { usePipetteNameSpecs } from '/app/local-resources/instruments' import { CheckPipetteButton } from './CheckPipetteButton' import { FLOWS } from './constants' +import type { Dispatch, SetStateAction } from 'react' import type { LoadedPipette, MotorAxes, @@ -34,7 +35,7 @@ interface ResultsProps extends PipetteWizardStepProps { currentStepIndex: number totalStepCount: number isFetching: boolean - setFetching: React.Dispatch> + setFetching: Dispatch> hasCalData: boolean requiredPipette?: LoadedPipette nextMount?: string @@ -78,7 +79,7 @@ export const Results = (props: ResultsProps): JSX.Element => { usePipetteNameSpecs(requiredPipette?.pipetteName as PipetteName) ?.displayName ?? null - const [numberOfTryAgains, setNumberOfTryAgains] = React.useState(0) + const [numberOfTryAgains, setNumberOfTryAgains] = useState(0) let header: string = 'unknown results screen' let iconColor: string = COLORS.green50 let isSuccess: boolean = true diff --git a/app/src/organisms/TakeoverModal/MaintenanceRunStatusProvider.tsx b/app/src/organisms/TakeoverModal/MaintenanceRunStatusProvider.tsx index 6ba2707752b..293340a9d85 100644 --- a/app/src/organisms/TakeoverModal/MaintenanceRunStatusProvider.tsx +++ b/app/src/organisms/TakeoverModal/MaintenanceRunStatusProvider.tsx @@ -1,7 +1,8 @@ -import * as React from 'react' - +import { useState, useEffect, useMemo, createContext } from 'react' import { useNotifyCurrentMaintenanceRun } from '/app/resources/maintenance_runs' +import type { ReactNode } from 'react' + interface MaintenanceRunIds { currentRunId: string | null oddRunId: string | null @@ -12,19 +13,19 @@ export interface MaintenanceRunStatus { setOddRunIds: (state: MaintenanceRunIds) => void } -export const MaintenanceRunContext = React.createContext({ +export const MaintenanceRunContext = createContext({ getRunIds: () => ({ currentRunId: null, oddRunId: null }), setOddRunIds: () => {}, }) interface MaintenanceRunProviderProps { - children?: React.ReactNode + children?: ReactNode } export function MaintenanceRunStatusProvider( props: MaintenanceRunProviderProps ): JSX.Element { - const [oddRunIds, setOddRunIds] = React.useState({ + const [oddRunIds, setOddRunIds] = useState({ currentRunId: null, oddRunId: null, }) @@ -33,14 +34,14 @@ export function MaintenanceRunStatusProvider( refetchInterval: 5000, }).data?.data.id - React.useEffect(() => { + useEffect(() => { setOddRunIds(prevState => ({ ...prevState, currentRunId: currentRunIdQueryResult ?? null, })) }, [currentRunIdQueryResult]) - const maintenanceRunStatus = React.useMemo( + const maintenanceRunStatus = useMemo( () => ({ getRunIds: () => oddRunIds, setOddRunIds, diff --git a/app/src/organisms/TakeoverModal/MaintenanceRunTakeover.tsx b/app/src/organisms/TakeoverModal/MaintenanceRunTakeover.tsx index b4cef390203..bb2b380cef1 100644 --- a/app/src/organisms/TakeoverModal/MaintenanceRunTakeover.tsx +++ b/app/src/organisms/TakeoverModal/MaintenanceRunTakeover.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useDeleteMaintenanceRunMutation } from '@opentrons/react-api-client' @@ -7,8 +7,10 @@ import { TakeoverModal } from './TakeoverModal' import { MaintenanceRunStatusProvider } from './MaintenanceRunStatusProvider' import { useMaintenanceRunTakeover } from './useMaintenanceRunTakeover' +import type { ReactNode } from 'react' + interface MaintenanceRunTakeoverProps { - children: React.ReactNode + children: ReactNode } export function MaintenanceRunTakeover({ @@ -22,18 +24,18 @@ export function MaintenanceRunTakeover({ } interface MaintenanceRunTakeoverModalProps { - children: React.ReactNode + children: ReactNode } export function MaintenanceRunTakeoverModal( props: MaintenanceRunTakeoverModalProps ): JSX.Element { const { i18n, t } = useTranslation(['shared', 'branded']) - const [isLoading, setIsLoading] = React.useState(false) + const [isLoading, setIsLoading] = useState(false) const [ showConfirmTerminateModal, setShowConfirmTerminateModal, - ] = React.useState(false) + ] = useState(false) const { oddRunId, currentRunId } = useMaintenanceRunTakeover().getRunIds() const isMaintenanceRunCurrent = currentRunId != null @@ -50,7 +52,7 @@ export function MaintenanceRunTakeoverModal( } } - React.useEffect(() => { + useEffect(() => { if (currentRunId == null) { setIsLoading(false) setShowConfirmTerminateModal(false) diff --git a/app/src/organisms/ToasterOven/ToasterOven.tsx b/app/src/organisms/ToasterOven/ToasterOven.tsx index 4f29be519ae..c3130793750 100644 --- a/app/src/organisms/ToasterOven/ToasterOven.tsx +++ b/app/src/organisms/ToasterOven/ToasterOven.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useSelector } from 'react-redux' import { v4 as uuidv4 } from 'uuid' @@ -17,6 +17,7 @@ import { import { getIsOnDevice } from '/app/redux/config' import { ToasterContext } from './ToasterContext' +import type { ReactNode } from 'react' import type { SnackbarProps } from '@opentrons/components' import type { ToastProps, @@ -25,7 +26,7 @@ import type { import type { MakeSnackbarOptions, MakeToastOptions } from './ToasterContext' interface ToasterOvenProps { - children: React.ReactNode + children: ReactNode } /** @@ -34,8 +35,8 @@ interface ToasterOvenProps { * @returns */ export function ToasterOven({ children }: ToasterOvenProps): JSX.Element { - const [toasts, setToasts] = React.useState([]) - const [snackbar, setSnackbar] = React.useState(null) + const [toasts, setToasts] = useState([]) + const [snackbar, setSnackbar] = useState(null) const isOnDevice = useSelector(getIsOnDevice) ?? null const displayType: 'desktop' | 'odd' = diff --git a/app/src/organisms/WellSelection/Selection384Wells.tsx b/app/src/organisms/WellSelection/Selection384Wells.tsx index d0fcddef752..94e2eaf9e74 100644 --- a/app/src/organisms/WellSelection/Selection384Wells.tsx +++ b/app/src/organisms/WellSelection/Selection384Wells.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import { useTranslation } from 'react-i18next' import flatten from 'lodash/flatten' @@ -15,6 +15,7 @@ import { import { IconButton } from '/app/atoms/buttons/IconButton' +import type { Dispatch, SetStateAction, ReactNode } from 'react' import type { WellGroup } from '@opentrons/components' import type { LabwareDefinition2, @@ -26,7 +27,7 @@ interface Selection384WellsProps { channels: PipetteChannels definition: LabwareDefinition2 deselectWells: (wells: string[]) => void - labwareRender: React.ReactNode + labwareRender: ReactNode selectWells: (wellGroup: WellGroup) => unknown } @@ -43,18 +44,18 @@ export function Selection384Wells({ labwareRender, selectWells, }: Selection384WellsProps): JSX.Element { - const [selectBy, setSelectBy] = React.useState<'columns' | 'wells'>('columns') + const [selectBy, setSelectBy] = useState<'columns' | 'wells'>('columns') - const [lastSelectedIndex, setLastSelectedIndex] = React.useState< - number | null - >(null) + const [lastSelectedIndex, setLastSelectedIndex] = useState( + null + ) - const [startingWellState, setStartingWellState] = React.useState< + const [startingWellState, setStartingWellState] = useState< Record >({ A1: false, A2: false, B1: false, B2: false }) // to reset last selected index and starting well state on page-level selected well reset - React.useEffect(() => { + useEffect(() => { if (Object.keys(allSelectedWells).length === 0) { setLastSelectedIndex(null) if (channels === 96) { @@ -180,8 +181,8 @@ export function Selection384Wells({ interface SelectByProps { selectBy: 'columns' | 'wells' - setSelectBy: React.Dispatch> - setLastSelectedIndex: React.Dispatch> + setSelectBy: Dispatch> + setLastSelectedIndex: Dispatch> } function SelectBy({ @@ -244,8 +245,8 @@ function StartingWell({ deselectWells: (wells: string[]) => void selectWells: (wellGroup: WellGroup) => void startingWellState: Record - setStartingWellState: React.Dispatch< - React.SetStateAction> + setStartingWellState: Dispatch< + SetStateAction> > wells: string[] }): JSX.Element { @@ -255,7 +256,7 @@ function StartingWell({ channels === 8 ? ['A1', 'B1'] : ['A1', 'A2', 'B1', 'B2'] // on mount, select A1 well group for 96-channel - React.useEffect(() => { + useEffect(() => { // deselect all wells on mount; clears well selection when navigating back within quick transfer flow // otherwise, selected wells and lastSelectedIndex pointer will be out of sync deselectWells(wells) diff --git a/app/src/organisms/WellSelection/SelectionRect.tsx b/app/src/organisms/WellSelection/SelectionRect.tsx index 7c9d1ac0357..6d241d5f0c0 100644 --- a/app/src/organisms/WellSelection/SelectionRect.tsx +++ b/app/src/organisms/WellSelection/SelectionRect.tsx @@ -1,19 +1,20 @@ -import * as React from 'react' +import { useState, useRef, useCallback, useEffect } from 'react' import { Flex, JUSTIFY_CENTER } from '@opentrons/components' +import type { MouseEventHandler, ReactNode, TouchEventHandler } from 'react' import type { DragRect, GenericRect } from './types' interface SelectionRectProps { onSelectionMove?: (rect: GenericRect) => void onSelectionDone?: (rect: GenericRect) => void - children?: React.ReactNode + children?: ReactNode } export function SelectionRect(props: SelectionRectProps): JSX.Element { const { onSelectionMove, onSelectionDone, children } = props - const [positions, setPositions] = React.useState(null) - const parentRef = React.useRef(null) + const [positions, setPositions] = useState(null) + const parentRef = useRef(null) const getRect = (args: DragRect): GenericRect => { const { xStart, yStart, xDynamic, yDynamic } = args @@ -25,7 +26,7 @@ export function SelectionRect(props: SelectionRectProps): JSX.Element { } } - const handleDrag = React.useCallback( + const handleDrag = useCallback( (e: TouchEvent | MouseEvent): void => { let xDynamic: number let yDynamic: number @@ -55,7 +56,7 @@ export function SelectionRect(props: SelectionRectProps): JSX.Element { [onSelectionMove] ) - const handleDragEnd = React.useCallback( + const handleDragEnd = useCallback( (e: TouchEvent | MouseEvent): void => { if (!(e instanceof TouchEvent) && !(e instanceof MouseEvent)) { return @@ -70,7 +71,7 @@ export function SelectionRect(props: SelectionRectProps): JSX.Element { [onSelectionDone, positions] ) - const handleTouchStart: React.TouchEventHandler = e => { + const handleTouchStart: TouchEventHandler = e => { const touch = e.touches[0] setPositions({ xStart: touch.clientX, @@ -80,7 +81,7 @@ export function SelectionRect(props: SelectionRectProps): JSX.Element { }) } - const handleMouseDown: React.MouseEventHandler = e => { + const handleMouseDown: MouseEventHandler = e => { setPositions({ xStart: e.clientX, xDynamic: e.clientX, @@ -89,7 +90,7 @@ export function SelectionRect(props: SelectionRectProps): JSX.Element { }) } - React.useEffect(() => { + useEffect(() => { document.addEventListener('touchmove', handleDrag) document.addEventListener('touchend', handleDragEnd) document.addEventListener('mousemove', handleDrag) diff --git a/app/src/pages/ODD/ConnectViaWifi/JoinOtherNetwork.tsx b/app/src/pages/ODD/ConnectViaWifi/JoinOtherNetwork.tsx index 4abd146238a..3366f9180b0 100644 --- a/app/src/pages/ODD/ConnectViaWifi/JoinOtherNetwork.tsx +++ b/app/src/pages/ODD/ConnectViaWifi/JoinOtherNetwork.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { Flex, DIRECTION_COLUMN } from '@opentrons/components' @@ -6,11 +6,12 @@ import { Flex, DIRECTION_COLUMN } from '@opentrons/components' import { SetWifiSsid } from '/app/organisms/ODD/NetworkSettings' import { RobotSetupHeader } from '/app/organisms/ODD/RobotSetupHeader' +import type { Dispatch, SetStateAction } from 'react' import type { WifiScreenOption } from './' interface JoinOtherNetworkProps { setCurrentOption: (option: WifiScreenOption) => void - setSelectedSsid: React.Dispatch> + setSelectedSsid: Dispatch> } export function JoinOtherNetwork({ @@ -19,8 +20,8 @@ export function JoinOtherNetwork({ }: JoinOtherNetworkProps): JSX.Element { const { i18n, t } = useTranslation('device_settings') - const [inputSsid, setInputSsid] = React.useState('') - const [errorMessage, setErrorMessage] = React.useState(null) + const [inputSsid, setInputSsid] = useState('') + const [errorMessage, setErrorMessage] = useState(null) const handleContinue = (): void => { if (inputSsid.length >= 2 && inputSsid.length <= 32) { diff --git a/app/src/pages/ODD/DeckConfiguration/index.tsx b/app/src/pages/ODD/DeckConfiguration/index.tsx index 2de5de02c46..71b8b5905bc 100644 --- a/app/src/pages/ODD/DeckConfiguration/index.tsx +++ b/app/src/pages/ODD/DeckConfiguration/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { createPortal } from 'react-dom' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' @@ -20,6 +20,7 @@ import { useNotifyDeckConfigurationQuery, } from '/app/resources/deck_configuration' +import type { ComponentProps } from 'react' import type { SmallButton } from '/app/atoms/buttons' export function DeckConfigurationEditor(): JSX.Element { @@ -32,7 +33,7 @@ export function DeckConfigurationEditor(): JSX.Element { const [ showSetupInstructionsModal, setShowSetupInstructionsModal, - ] = React.useState(false) + ] = useState(false) const isOnDevice = true const { @@ -41,10 +42,9 @@ export function DeckConfigurationEditor(): JSX.Element { addFixtureModal, } = useDeckConfigurationEditingTools(isOnDevice) - const [ - showDiscardChangeModal, - setShowDiscardChangeModal, - ] = React.useState(false) + const [showDiscardChangeModal, setShowDiscardChangeModal] = useState( + false + ) const deckConfig = useNotifyDeckConfigurationQuery().data ?? [] @@ -52,7 +52,7 @@ export function DeckConfigurationEditor(): JSX.Element { navigate(-1) } - const secondaryButtonProps: React.ComponentProps = { + const secondaryButtonProps: ComponentProps = { onClick: () => { setShowSetupInstructionsModal(true) }, diff --git a/app/src/pages/ODD/InstrumentDetail/InstrumentDetailOverflowMenu.tsx b/app/src/pages/ODD/InstrumentDetail/InstrumentDetailOverflowMenu.tsx index 96ea37d14d3..c49ef3d4f34 100644 --- a/app/src/pages/ODD/InstrumentDetail/InstrumentDetailOverflowMenu.tsx +++ b/app/src/pages/ODD/InstrumentDetail/InstrumentDetailOverflowMenu.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import NiceModal, { useModal } from '@ebay/nice-modal-react' import { useTranslation } from 'react-i18next' import { createPortal } from 'react-dom' @@ -26,6 +26,7 @@ import { FLOWS } from '/app/organisms/PipetteWizardFlows/constants' import { GRIPPER_FLOW_TYPES } from '/app/organisms/GripperWizardFlows/constants' import { getTopPortalEl } from '/app/App/portal' +import type { ComponentProps, MouseEventHandler } from 'react' import type { PipetteData, GripperData, @@ -55,13 +56,13 @@ const InstrumentDetailsOverflowMenu = NiceModal.create( const { instrument, host, enableDTWiz } = props const { t } = useTranslation('robot_controls') const modal = useModal() - const [wizardProps, setWizardProps] = React.useState< - | React.ComponentProps - | React.ComponentProps + const [wizardProps, setWizardProps] = useState< + | ComponentProps + | ComponentProps | null >(null) const sharedGripperWizardProps: Pick< - React.ComponentProps, + ComponentProps, 'attachedGripper' | 'closeFlow' > = { attachedGripper: instrument, @@ -75,7 +76,7 @@ const InstrumentDetailsOverflowMenu = NiceModal.create( instrument.mount !== 'extension' && instrument.data?.channels === 96 - const handleRecalibrate: React.MouseEventHandler = () => { + const handleRecalibrate: MouseEventHandler = () => { if (instrument?.ok) { setWizardProps( instrument.mount === 'extension' diff --git a/app/src/pages/ODD/ProtocolDashboard/PinnedProtocol.tsx b/app/src/pages/ODD/ProtocolDashboard/PinnedProtocol.tsx index 2620bd6de52..27db6210d68 100644 --- a/app/src/pages/ODD/ProtocolDashboard/PinnedProtocol.tsx +++ b/app/src/pages/ODD/ProtocolDashboard/PinnedProtocol.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import styled, { css } from 'styled-components' @@ -23,6 +23,7 @@ import { LongPressModal } from './LongPressModal' import { formatTimeWithUtcLabel } from '/app/resources/runs' import { useUpdatedLastRunTime } from './hooks' +import type { Dispatch, SetStateAction } from 'react' import type { UseLongPressResult } from '@opentrons/components' import type { ProtocolResource } from '@opentrons/shared-data' @@ -63,7 +64,7 @@ const cardStyleBySize: { interface PinnedProtocolProps { protocol: ProtocolResource - longPress: React.Dispatch> + longPress: Dispatch> setShowDeleteConfirmationModal: (showDeleteConfirmationModal: boolean) => void setTargetProtocolId: (targetProtocolId: string) => void cardSize?: CardSizeType @@ -98,7 +99,7 @@ export function PinnedProtocol(props: PinnedProtocolProps): JSX.Element { navigate(`/protocols/${protocolId}`) } } - React.useEffect(() => { + useEffect(() => { if (longpress.isLongPressed) { longPress(true) } diff --git a/app/src/pages/ODD/ProtocolSetup/index.tsx b/app/src/pages/ODD/ProtocolSetup/index.tsx index 03a03626a55..553eec5aadd 100644 --- a/app/src/pages/ODD/ProtocolSetup/index.tsx +++ b/app/src/pages/ODD/ProtocolSetup/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect, useState } from 'react' import last from 'lodash/last' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' @@ -82,7 +82,14 @@ import { useProtocolAnalysisErrors, } from '/app/resources/runs' import { useScrollPosition } from '/app/local-resources/dom-utils' +import { + getLabwareSetupItemGroups, + getProtocolUsesGripper, + useRequiredProtocolHardwareFromAnalysis, + useMissingProtocolHardwareFromAnalysis, +} from '/app/transformations/commands' +import type { Dispatch, SetStateAction } from 'react' import type { Run } from '@opentrons/api-client' import type { CutoutFixtureId, CutoutId } from '@opentrons/shared-data' import type { OnDeviceRouteParams } from '/app/App/types' @@ -92,19 +99,13 @@ import type { ProtocolHardware, ProtocolFixture, } from '/app/transformations/commands' -import { - getLabwareSetupItemGroups, - getProtocolUsesGripper, - useRequiredProtocolHardwareFromAnalysis, - useMissingProtocolHardwareFromAnalysis, -} from '/app/transformations/commands' const FETCH_DURATION_MS = 5000 const ANALYSIS_POLL_MS = 5000 interface PrepareToRunProps { runId: string - setSetupScreen: React.Dispatch> + setSetupScreen: Dispatch> confirmAttachment: () => void confirmStepsComplete: () => void play: () => void @@ -147,7 +148,7 @@ function PrepareToRun({ const [ isPollingForCompletedAnalysis, setIsPollingForCompletedAnalysis, - ] = React.useState(mostRecentAnalysisSummary?.status !== 'completed') + ] = useState(mostRecentAnalysisSummary?.status !== 'completed') const { data: mostRecentAnalysis = null, @@ -165,7 +166,7 @@ function PrepareToRun({ navigate('/protocols') } - React.useEffect(() => { + useEffect(() => { if (mostRecentAnalysis?.status === 'completed') { setIsPollingForCompletedAnalysis(false) } else { @@ -229,10 +230,9 @@ function PrepareToRun({ parameter.type === 'csv_file' || parameter.value !== parameter.default ) - const [ - showConfirmCancelModal, - setShowConfirmCancelModal, - ] = React.useState(false) + const [showConfirmCancelModal, setShowConfirmCancelModal] = useState( + false + ) const deckConfigCompatibility = useDeckConfigurationCompatibility( robotType, @@ -670,7 +670,7 @@ export function ProtocolSetup(): JSX.Element { const [ showAnalysisFailedModal, setShowAnalysisFailedModal, - ] = React.useState(true) + ] = useState(true) const robotType = useRobotType(robotName) const attachedModules = useAttachedModules({ @@ -684,7 +684,7 @@ export function ProtocolSetup(): JSX.Element { const [ isPollingForCompletedAnalysis, setIsPollingForCompletedAnalysis, - ] = React.useState(mostRecentAnalysisSummary?.status !== 'completed') + ] = useState(mostRecentAnalysisSummary?.status !== 'completed') const { data: mostRecentAnalysis = null, @@ -699,7 +699,7 @@ export function ProtocolSetup(): JSX.Element { const areLiquidsInProtocol = (mostRecentAnalysis?.liquids?.length ?? 0) > 0 - React.useEffect(() => { + useEffect(() => { if (mostRecentAnalysis?.status === 'completed') { setIsPollingForCompletedAnalysis(false) } else { @@ -754,14 +754,14 @@ export function ProtocolSetup(): JSX.Element { handleProceedToRunClick, !configBypassHeaterShakerAttachmentConfirmation ) - const [cutoutId, setCutoutId] = React.useState(null) - const [providedFixtureOptions, setProvidedFixtureOptions] = React.useState< + const [cutoutId, setCutoutId] = useState(null) + const [providedFixtureOptions, setProvidedFixtureOptions] = useState< CutoutFixtureId[] >([]) // TODO(jh 10-31-24): Refactor the below to utilize useMissingStepsModal. - const [labwareConfirmed, setLabwareConfirmed] = React.useState(false) - const [liquidsConfirmed, setLiquidsConfirmed] = React.useState(false) - const [offsetsConfirmed, setOffsetsConfirmed] = React.useState(false) + const [labwareConfirmed, setLabwareConfirmed] = useState(false) + const [liquidsConfirmed, setLiquidsConfirmed] = useState(false) + const [offsetsConfirmed, setOffsetsConfirmed] = useState(false) const missingSteps = [ !offsetsConfirmed ? t('applied_labware_offsets') : null, !labwareConfirmed ? t('labware_placement') : null, @@ -779,9 +779,7 @@ export function ProtocolSetup(): JSX.Element { const isHeaterShakerInProtocol = useIsHeaterShakerInProtocol() // orchestrate setup subpages/components - const [setupScreen, setSetupScreen] = React.useState( - 'prepare to run' - ) + const [setupScreen, setSetupScreen] = useState('prepare to run') const setupComponentByScreen = { 'prepare to run': ( > + longPress: Dispatch> setShowDeleteConfirmationModal: (showDeleteConfirmationModal: boolean) => void setTargetTransferId: (targetProtocolId: string) => void cardSize?: CardSizeType @@ -83,7 +84,7 @@ export function PinnedTransfer(props: { navigate(`/quick-transfer/${transferId}`) } } - React.useEffect(() => { + useEffect(() => { if (longpress.isLongPressed) { longPress(true) } diff --git a/app/src/pages/ODD/QuickTransferDashboard/QuickTransferCard.tsx b/app/src/pages/ODD/QuickTransferDashboard/QuickTransferCard.tsx index a0f99a4367e..fe4a939d543 100644 --- a/app/src/pages/ODD/QuickTransferDashboard/QuickTransferCard.tsx +++ b/app/src/pages/ODD/QuickTransferDashboard/QuickTransferCard.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { Trans, useTranslation } from 'react-i18next' import { useQueryClient } from 'react-query' @@ -36,6 +36,7 @@ import { OddModal } from '/app/molecules/OddModal' import { LongPressModal } from './LongPressModal' import { formatTimeWithUtcLabel } from '/app/resources/runs' +import type { Dispatch, SetStateAction } from 'react' import type { UseLongPressResult } from '@opentrons/components' import type { ProtocolResource } from '@opentrons/shared-data' import type { OddModalHeaderBaseProps } from '/app/molecules/OddModal/types' @@ -44,7 +45,7 @@ const REFETCH_INTERVAL = 5000 export function QuickTransferCard(props: { quickTransfer: ProtocolResource - longPress: React.Dispatch> + longPress: Dispatch> setShowDeleteConfirmationModal: (showDeleteConfirmationModal: boolean) => void setTargetTransferId: (targetTransferId: string) => void }): JSX.Element { @@ -55,11 +56,11 @@ export function QuickTransferCard(props: { setTargetTransferId, } = props const navigate = useNavigate() - const [showIcon, setShowIcon] = React.useState(false) + const [showIcon, setShowIcon] = useState(false) const [ showFailedAnalysisModal, setShowFailedAnalysisModal, - ] = React.useState(false) + ] = useState(false) const { t, i18n } = useTranslation(['quick_transfer', 'branded']) const transferName = quickTransfer.metadata.protocolName ?? quickTransfer.files[0].name @@ -113,7 +114,7 @@ export function QuickTransferCard(props: { } } - React.useEffect(() => { + useEffect(() => { if (longpress.isLongPressed) { longPress(true) setTargetTransferId(quickTransfer.id) diff --git a/app/src/resources/deck_configuration/hooks.tsx b/app/src/resources/deck_configuration/hooks.tsx index 79a2e80124b..935c81a4cb2 100644 --- a/app/src/resources/deck_configuration/hooks.tsx +++ b/app/src/resources/deck_configuration/hooks.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState } from 'react' import { getInitialAndMovedLabwareInSlots } from '@opentrons/components' import { FLEX_ROBOT_TYPE, @@ -15,6 +15,7 @@ import { SINGLE_SLOT_FIXTURES, } from '@opentrons/shared-data' +import type { ReactNode } from 'react' import type { CompletedProtocolAnalysis, CutoutConfigProtocolSpec, @@ -118,7 +119,7 @@ interface DeckConfigurationEditingTools { cutoutId: CutoutId, cutoutFixtureId: CutoutFixtureId ) => void - addFixtureModal: React.ReactNode + addFixtureModal: ReactNode } export function useDeckConfigurationEditingTools( isOnDevice: boolean @@ -129,9 +130,7 @@ export function useDeckConfigurationEditingTools( refetchInterval: DECK_CONFIG_REFETCH_INTERVAL, }).data ?? [] const { updateDeckConfiguration } = useUpdateDeckConfigurationMutation() - const [targetCutoutId, setTargetCutoutId] = React.useState( - null - ) + const [targetCutoutId, setTargetCutoutId] = useState(null) const addFixtureToCutout = (cutoutId: CutoutId): void => { setTargetCutoutId(cutoutId) From 4c7a409fe2baecd05040c0e42747887d387046e7 Mon Sep 17 00:00:00 2001 From: Jeremy Leon Date: Mon, 2 Dec 2024 16:48:03 -0500 Subject: [PATCH 6/9] feat(api, shared-data): add correctionByVolume to liquid class schema and definitions (#16972) Adds a correctionByVolume field to liquid class definitions in the schema, pydantic models and PAPI classes for liquid class properties. --- .../protocol_api/_liquid_properties.py | 14 +++ api/tests/opentrons/conftest.py | 2 + .../test_liquid_class_properties.py | 12 ++ shared-data/command/schemas/11.json | 105 ++++++++++++++++++ .../liquid-class/definitions/1/water.json | 33 ++++++ .../fixtures/1/fixture_glycerol50.json | 12 ++ shared-data/liquid-class/schemas/1.json | 23 ++++ .../liquid_classes/liquid_class_definition.py | 18 +++ 8 files changed, 219 insertions(+) diff --git a/api/src/opentrons/protocol_api/_liquid_properties.py b/api/src/opentrons/protocol_api/_liquid_properties.py index 5aaed51edbe..d27d2bd1ba0 100644 --- a/api/src/opentrons/protocol_api/_liquid_properties.py +++ b/api/src/opentrons/protocol_api/_liquid_properties.py @@ -314,6 +314,7 @@ class BaseLiquidHandlingProperties: _position_reference: PositionReference _offset: Coordinate _flow_rate_by_volume: LiquidHandlingPropertyByVolume + _correction_by_volume: LiquidHandlingPropertyByVolume _delay: DelayProperties @property @@ -341,6 +342,10 @@ def offset(self, new_offset: Sequence[float]) -> None: def flow_rate_by_volume(self) -> LiquidHandlingPropertyByVolume: return self._flow_rate_by_volume + @property + def correction_by_volume(self) -> LiquidHandlingPropertyByVolume: + return self._correction_by_volume + @property def delay(self) -> DelayProperties: return self._delay @@ -543,6 +548,9 @@ def build_aspirate_properties( _flow_rate_by_volume=LiquidHandlingPropertyByVolume( aspirate_properties.flowRateByVolume ), + _correction_by_volume=LiquidHandlingPropertyByVolume( + aspirate_properties.correctionByVolume + ), _pre_wet=aspirate_properties.preWet, _mix=_build_mix_properties(aspirate_properties.mix), _delay=_build_delay_properties(aspirate_properties.delay), @@ -560,6 +568,9 @@ def build_single_dispense_properties( _flow_rate_by_volume=LiquidHandlingPropertyByVolume( single_dispense_properties.flowRateByVolume ), + _correction_by_volume=LiquidHandlingPropertyByVolume( + single_dispense_properties.correctionByVolume + ), _mix=_build_mix_properties(single_dispense_properties.mix), _push_out_by_volume=LiquidHandlingPropertyByVolume( single_dispense_properties.pushOutByVolume @@ -581,6 +592,9 @@ def build_multi_dispense_properties( _flow_rate_by_volume=LiquidHandlingPropertyByVolume( multi_dispense_properties.flowRateByVolume ), + _correction_by_volume=LiquidHandlingPropertyByVolume( + multi_dispense_properties.correctionByVolume + ), _conditioning_by_volume=LiquidHandlingPropertyByVolume( multi_dispense_properties.conditioningByVolume ), diff --git a/api/tests/opentrons/conftest.py b/api/tests/opentrons/conftest.py index e8ca2b059ff..7be480cfe0b 100755 --- a/api/tests/opentrons/conftest.py +++ b/api/tests/opentrons/conftest.py @@ -828,6 +828,7 @@ def minimal_liquid_class_def2() -> LiquidClassSchemaV1: positionReference=PositionReference.WELL_BOTTOM, offset=Coordinate(x=0, y=0, z=-5), flowRateByVolume=[(10.0, 40.0), (20.0, 30.0)], + correctionByVolume=[(15.0, 1.5), (30.0, -5.0)], preWet=True, mix=MixProperties(enable=False), delay=DelayProperties( @@ -853,6 +854,7 @@ def minimal_liquid_class_def2() -> LiquidClassSchemaV1: positionReference=PositionReference.WELL_BOTTOM, offset=Coordinate(x=0, y=0, z=-5), flowRateByVolume=[(10.0, 40.0), (20.0, 30.0)], + correctionByVolume=[(15.0, -1.5), (30.0, 5.0)], mix=MixProperties(enable=False), pushOutByVolume=[(10.0, 7.0), (20.0, 10.0)], delay=DelayProperties(enable=False), diff --git a/api/tests/opentrons/protocol_api/test_liquid_class_properties.py b/api/tests/opentrons/protocol_api/test_liquid_class_properties.py index f7033afb5be..356add50ae8 100644 --- a/api/tests/opentrons/protocol_api/test_liquid_class_properties.py +++ b/api/tests/opentrons/protocol_api/test_liquid_class_properties.py @@ -45,6 +45,10 @@ def test_build_aspirate_settings() -> None: assert aspirate_properties.position_reference.value == "well-bottom" assert aspirate_properties.offset == Coordinate(x=0, y=0, z=-5) assert aspirate_properties.flow_rate_by_volume.as_dict() == {10: 50.0} + assert aspirate_properties.correction_by_volume.as_dict() == { + 1.0: -2.5, + 10.0: 3, + } assert aspirate_properties.pre_wet is True assert aspirate_properties.mix.enabled is True assert aspirate_properties.mix.repetitions == 3 @@ -94,6 +98,10 @@ def test_build_single_dispense_settings() -> None: 10.0: 40.0, 20.0: 30.0, } + assert single_dispense_properties.correction_by_volume.as_dict() == { + 2.0: -1.5, + 20.0: 2, + } assert single_dispense_properties.mix.enabled is True assert single_dispense_properties.mix.repetitions == 3 assert single_dispense_properties.mix.volume == 15 @@ -146,6 +154,10 @@ def test_build_multi_dispense_settings() -> None: 10.0: 40.0, 20.0: 30.0, } + assert multi_dispense_properties.correction_by_volume.as_dict() == { + 3.0: -0.5, + 30.0: 1, + } assert multi_dispense_properties.conditioning_by_volume.as_dict() == { 5.0: 5.0, } diff --git a/shared-data/command/schemas/11.json b/shared-data/command/schemas/11.json index 38a39ea7902..2df16125574 100644 --- a/shared-data/command/schemas/11.json +++ b/shared-data/command/schemas/11.json @@ -2133,6 +2133,40 @@ ] } }, + "correctionByVolume": { + "title": "Correctionbyvolume", + "description": "Settings for volume correction keyed by by target aspiration volume, representing additional volume the plunger should move to accurately hit target volume.", + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] + }, + { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + } + ] + } + ] + } + }, "preWet": { "title": "Prewet", "description": "Whether to perform a pre-wet action.", @@ -2163,6 +2197,7 @@ "positionReference", "offset", "flowRateByVolume", + "correctionByVolume", "preWet", "mix", "delay" @@ -2411,6 +2446,40 @@ ] } }, + "correctionByVolume": { + "title": "Correctionbyvolume", + "description": "Settings for volume correction keyed by by target dispense volume, representing additional volume the plunger should move to accurately hit target volume.", + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] + }, + { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + } + ] + } + ] + } + }, "mix": { "title": "Mix", "description": "Mixing settings for after a dispense", @@ -2472,6 +2541,7 @@ "positionReference", "offset", "flowRateByVolume", + "correctionByVolume", "mix", "pushOutByVolume", "delay" @@ -2553,6 +2623,40 @@ ] } }, + "correctionByVolume": { + "title": "Correctionbyvolume", + "description": "Settings for volume correction keyed by by target dispense volume, representing additional volume the plunger should move to accurately hit target volume.", + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0.0 + } + ] + }, + { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "number" + } + ] + } + ] + } + }, "conditioningByVolume": { "title": "Conditioningbyvolume", "description": "Settings for conditioning volume keyed by target dispense volume.", @@ -2641,6 +2745,7 @@ "positionReference", "offset", "flowRateByVolume", + "correctionByVolume", "conditioningByVolume", "disposalByVolume", "delay" diff --git a/shared-data/liquid-class/definitions/1/water.json b/shared-data/liquid-class/definitions/1/water.json index b84e1676d5b..b9447aa9c52 100644 --- a/shared-data/liquid-class/definitions/1/water.json +++ b/shared-data/liquid-class/definitions/1/water.json @@ -64,6 +64,7 @@ [10.0, 24.0], [50.0, 35.0] ], + "correctionByVolume": [[0.0, 0.0]], "preWet": false, "mix": { "enable": false, @@ -133,6 +134,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 50.0]], + "correctionByVolume": [[0.0, 0.0]], "mix": { "enable": false, "params": { @@ -208,6 +210,7 @@ "z": 2 }, "flowRateByVolume": [[50.0, 50.0]], + "correctionByVolume": [[0.0, 0.0]], "conditioningByVolume": [ [1.0, 5.0], [45.0, 5.0], @@ -288,6 +291,7 @@ [10.0, 24.0], [50.0, 35.0] ], + "correctionByVolume": [[0.0, 0.0]], "preWet": false, "mix": { "enable": false, @@ -357,6 +361,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 50.0]], + "correctionByVolume": [[0.0, 0.0]], "mix": { "enable": false, "params": { @@ -432,6 +437,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 50.0]], + "correctionByVolume": [[0.0, 0.0]], "conditioningByVolume": [ [1.0, 5.0], [45.0, 5.0], @@ -512,6 +518,7 @@ [10.0, 478.0], [50.0, 478.0] ], + "correctionByVolume": [[0.0, 0.0]], "preWet": false, "mix": { "enable": false, @@ -585,6 +592,7 @@ [10.0, 478.0], [50.0, 478.0] ], + "correctionByVolume": [[0.0, 0.0]], "mix": { "enable": false, "params": { @@ -658,6 +666,7 @@ [10.0, 478.0], [50.0, 478.0] ], + "correctionByVolume": [[0.0, 0.0]], "conditioningByVolume": [ [1.0, 5.0], [45.0, 5.0], @@ -729,6 +738,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], "preWet": false, "mix": { "enable": false, @@ -798,6 +808,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], "mix": { "enable": false, "params": { @@ -867,6 +878,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], "conditioningByVolume": [ [1.0, 5.0], [195.0, 5.0], @@ -938,6 +950,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], "preWet": false, "mix": { "enable": false, @@ -1007,6 +1020,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], "mix": { "enable": false, "params": { @@ -1076,6 +1090,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], "conditioningByVolume": [ [1.0, 5.0], [995.0, 5.0], @@ -1156,6 +1171,7 @@ [10.0, 478.0], [50.0, 478.0] ], + "correctionByVolume": [[0.0, 0.0]], "preWet": false, "mix": { "enable": false, @@ -1229,6 +1245,7 @@ [10.0, 478.0], [50.0, 478.0] ], + "correctionByVolume": [[0.0, 0.0]], "mix": { "enable": false, "params": { @@ -1302,6 +1319,7 @@ [10.0, 478.0], [50.0, 478.0] ], + "correctionByVolume": [[0.0, 0.0]], "conditioningByVolume": [ [1.0, 5.0], [45.0, 5.0], @@ -1373,6 +1391,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], "preWet": false, "mix": { "enable": false, @@ -1442,6 +1461,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], "mix": { "enable": false, "params": { @@ -1511,6 +1531,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], "conditioningByVolume": [ [1.0, 5.0], [195.0, 5.0], @@ -1582,6 +1603,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], "preWet": false, "mix": { "enable": false, @@ -1651,6 +1673,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], "mix": { "enable": false, "params": { @@ -1720,6 +1743,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 716.0]], + "correctionByVolume": [[0.0, 0.0]], "conditioningByVolume": [ [1.0, 5.0], [995.0, 5.0], @@ -1796,6 +1820,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], "preWet": false, "mix": { "enable": false, @@ -1865,6 +1890,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], "mix": { "enable": false, "params": { @@ -1934,6 +1960,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], "conditioningByVolume": [ [1.0, 5.0], [45.0, 5.0], @@ -2005,6 +2032,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], "preWet": false, "mix": { "enable": false, @@ -2074,6 +2102,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], "mix": { "enable": false, "params": { @@ -2143,6 +2172,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], "conditioningByVolume": [ [1.0, 5.0], [195.0, 5.0], @@ -2214,6 +2244,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], "preWet": false, "mix": { "enable": false, @@ -2283,6 +2314,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], "mix": { "enable": false, "params": { @@ -2352,6 +2384,7 @@ "z": 2 }, "flowRateByVolume": [[1.0, 200.0]], + "correctionByVolume": [[0.0, 0.0]], "conditioningByVolume": [ [1.0, 5.0], [995.0, 5.0], diff --git a/shared-data/liquid-class/fixtures/1/fixture_glycerol50.json b/shared-data/liquid-class/fixtures/1/fixture_glycerol50.json index 20fe7b44a3c..9c24a40452d 100644 --- a/shared-data/liquid-class/fixtures/1/fixture_glycerol50.json +++ b/shared-data/liquid-class/fixtures/1/fixture_glycerol50.json @@ -59,6 +59,10 @@ "z": -5 }, "flowRateByVolume": [[10.0, 50.0]], + "correctionByVolume": [ + [1.0, -2.5], + [10.0, 3.0] + ], "preWet": true, "mix": { "enable": true, @@ -134,6 +138,10 @@ [10.0, 40.0], [20.0, 30.0] ], + "correctionByVolume": [ + [2.0, -1.5], + [20.0, 2.0] + ], "mix": { "enable": true, "params": { @@ -208,6 +216,10 @@ [10.0, 40.0], [20.0, 30.0] ], + "correctionByVolume": [ + [3.0, -0.5], + [30.0, 1.0] + ], "conditioningByVolume": [[5.0, 5.0]], "disposalByVolume": [[5.0, 3.0]], "delay": { diff --git a/shared-data/liquid-class/schemas/1.json b/shared-data/liquid-class/schemas/1.json index f3aa85a6168..633be549d4c 100644 --- a/shared-data/liquid-class/schemas/1.json +++ b/shared-data/liquid-class/schemas/1.json @@ -144,6 +144,17 @@ }, "minItems": 1 }, + "correctionByVolume": { + "type": "array", + "description": "Settings for volume correction keyed by target aspiration/dispense volume, representing additional volume the plunger should move to accurately hit target volume.", + "items": { + "type": "array", + "items": { "type": "number" }, + "minItems": 2, + "maxItems": 2 + }, + "minItems": 1 + }, "mix": { "type": "object", "description": "Mixing properties.", @@ -310,6 +321,9 @@ "flowRateByVolume": { "$ref": "#/definitions/flowRateByVolume" }, + "correctionByVolume": { + "$ref": "#/definitions/correctionByVolume" + }, "preWet": { "type": "boolean", "description": "Whether to perform a pre-wet action." @@ -327,6 +341,7 @@ "positionReference", "offset", "flowRateByVolume", + "correctionByVolume", "preWet", "mix", "delay" @@ -352,6 +367,9 @@ "flowRateByVolume": { "$ref": "#/definitions/flowRateByVolume" }, + "correctionByVolume": { + "$ref": "#/definitions/correctionByVolume" + }, "mix": { "$ref": "#/definitions/mix" }, @@ -368,6 +386,7 @@ "positionReference", "offset", "flowRateByVolume", + "correctionByVolume", "mix", "pushOutByVolume", "delay" @@ -393,6 +412,9 @@ "flowRateByVolume": { "$ref": "#/definitions/flowRateByVolume" }, + "correctionByVolume": { + "$ref": "#/definitions/correctionByVolume" + }, "conditioningByVolume": { "$ref": "#/definitions/conditioningByVolume" }, @@ -409,6 +431,7 @@ "positionReference", "offset", "flowRateByVolume", + "correctionByVolume", "conditioningByVolume", "disposalByVolume", "delay" diff --git a/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py b/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py index 62add6a32b0..2c2de84e07e 100644 --- a/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py +++ b/shared-data/python/opentrons_shared_data/liquid_classes/liquid_class_definition.py @@ -31,6 +31,9 @@ LiquidHandlingPropertyByVolume = Sequence[Tuple[_NonNegativeNumber, _NonNegativeNumber]] """Settings for liquid class settings that are interpolated by volume.""" +CorrectionByVolume = Sequence[Tuple[_NonNegativeNumber, _Number]] +"""Settings for correctionByVolume, which unlike other `byVolume` properties allows negative values with volume.""" + class PositionReference(Enum): """Positional reference for liquid handling operations.""" @@ -253,6 +256,11 @@ class AspirateProperties(BaseModel): ..., description="Settings for flow rate keyed by target aspiration volume.", ) + correctionByVolume: CorrectionByVolume = Field( + ..., + description="Settings for volume correction keyed by by target aspiration volume," + " representing additional volume the plunger should move to accurately hit target volume.", + ) preWet: bool = Field(..., description="Whether to perform a pre-wet action.") mix: MixProperties = Field( ..., description="Mixing settings for before an aspirate" @@ -277,6 +285,11 @@ class SingleDispenseProperties(BaseModel): ..., description="Settings for flow rate keyed by target dispense volume.", ) + correctionByVolume: CorrectionByVolume = Field( + ..., + description="Settings for volume correction keyed by by target dispense volume," + " representing additional volume the plunger should move to accurately hit target volume.", + ) mix: MixProperties = Field(..., description="Mixing settings for after a dispense") pushOutByVolume: LiquidHandlingPropertyByVolume = Field( ..., description="Settings for pushout keyed by target dispense volume." @@ -301,6 +314,11 @@ class MultiDispenseProperties(BaseModel): ..., description="Settings for flow rate keyed by target dispense volume.", ) + correctionByVolume: CorrectionByVolume = Field( + ..., + description="Settings for volume correction keyed by by target dispense volume," + " representing additional volume the plunger should move to accurately hit target volume.", + ) conditioningByVolume: LiquidHandlingPropertyByVolume = Field( ..., description="Settings for conditioning volume keyed by target dispense volume.", From 6c0d418f69b980bdb269ef683e16d4ff36d57370 Mon Sep 17 00:00:00 2001 From: Sanniti Pimpley Date: Mon, 2 Dec 2024 17:50:10 -0500 Subject: [PATCH 7/9] feat(api): add new InstrumentContext.transfer_liquid() method (#16819) Closes AUTH-843 # Overview Adds `InstrumentContext.transfer_liquid()` method that does the following- - validates parameters of `transfer_liquid()` - loads the liquid class properties for the relevant pipette and tiprack into protocol engine - delegates to engine core to perform the actual transfer This PR does not cover engine core's transfer method execution. ## Risk assessment No risk so far since this is a code-only change. --- .../protocol_api/_liquid_properties.py | 139 ++++++++- .../protocol_api/core/engine/instrument.py | 51 +++- .../opentrons/protocol_api/core/instrument.py | 31 +- .../core/legacy/legacy_instrument_core.py | 27 +- .../legacy_instrument_core.py | 27 +- .../protocol_api/instrument_context.py | 116 ++++++- api/src/opentrons/protocol_api/validation.py | 83 +++++ .../protocol_engine/clients/sync_client.py | 6 + .../protocol_engine/commands/__init__.py | 16 + .../advanced_control/transfers/common.py | 13 +- .../advanced_control/transfers/transfer.py | 1 - .../core/engine/test_instrument_core.py | 62 ++++ .../protocol_api/test_instrument_context.py | 283 +++++++++++++++++- .../test_liquid_class_properties.py | 4 + .../opentrons/protocol_api/test_validation.py | 150 +++++++++- 15 files changed, 989 insertions(+), 20 deletions(-) diff --git a/api/src/opentrons/protocol_api/_liquid_properties.py b/api/src/opentrons/protocol_api/_liquid_properties.py index d27d2bd1ba0..dc848cfb7e2 100644 --- a/api/src/opentrons/protocol_api/_liquid_properties.py +++ b/api/src/opentrons/protocol_api/_liquid_properties.py @@ -1,15 +1,19 @@ from dataclasses import dataclass from numpy import interp -from typing import Optional, Dict, Sequence, Tuple +from typing import Optional, Dict, Sequence, Tuple, List from opentrons_shared_data.liquid_classes.liquid_class_definition import ( AspirateProperties as SharedDataAspirateProperties, SingleDispenseProperties as SharedDataSingleDispenseProperties, MultiDispenseProperties as SharedDataMultiDispenseProperties, DelayProperties as SharedDataDelayProperties, + DelayParams as SharedDataDelayParams, TouchTipProperties as SharedDataTouchTipProperties, + LiquidClassTouchTipParams as SharedDataTouchTipParams, MixProperties as SharedDataMixProperties, + MixParams as SharedDataMixParams, BlowoutProperties as SharedDataBlowoutProperties, + BlowoutParams as SharedDataBlowoutParams, ByTipTypeSetting as SharedByTipTypeSetting, Submerge as SharedDataSubmerge, RetractAspirate as SharedDataRetractAspirate, @@ -37,6 +41,10 @@ def as_dict(self) -> Dict[float, float]: """Get a dictionary representation of all set volumes and values along with the default.""" return self._properties_by_volume + def as_list_of_tuples(self) -> List[Tuple[float, float]]: + """Get as list of tuples.""" + return list(self._properties_by_volume.items()) + def get_for_volume(self, volume: float) -> float: """Get a value by volume for this property. Volumes not defined will be interpolated between set volumes.""" validated_volume = validation.ensure_positive_float(volume) @@ -101,6 +109,14 @@ def duration(self, new_duration: float) -> None: validated_duration = validation.ensure_positive_float(new_duration) self._duration = validated_duration + def as_shared_data_model(self) -> SharedDataDelayProperties: + return SharedDataDelayProperties( + enable=self._enabled, + params=SharedDataDelayParams(duration=self.duration) + if self.duration is not None + else None, + ) + @dataclass class TouchTipProperties: @@ -152,6 +168,27 @@ def speed(self, new_speed: float) -> None: validated_speed = validation.ensure_positive_float(new_speed) self._speed = validated_speed + def _get_shared_data_params(self) -> Optional[SharedDataTouchTipParams]: + """Get the touch tip params in schema v1 shape.""" + if ( + self._z_offset is not None + and self._mm_to_edge is not None + and self._speed is not None + ): + return SharedDataTouchTipParams( + zOffset=self._z_offset, + mmToEdge=self._mm_to_edge, + speed=self._speed, + ) + else: + return None + + def as_shared_data_model(self) -> SharedDataTouchTipProperties: + return SharedDataTouchTipProperties( + enable=self._enabled, + params=self._get_shared_data_params(), + ) + @dataclass class MixProperties: @@ -189,6 +226,22 @@ def volume(self, new_volume: float) -> None: validated_volume = validation.ensure_positive_float(new_volume) self._volume = validated_volume + def _get_shared_data_params(self) -> Optional[SharedDataMixParams]: + """Get the mix params in schema v1 shape.""" + if self._repetitions is not None and self._volume is not None: + return SharedDataMixParams( + repetitions=self._repetitions, + volume=self._volume, + ) + else: + return None + + def as_shared_data_model(self) -> SharedDataMixProperties: + return SharedDataMixProperties( + enable=self._enabled, + params=self._get_shared_data_params(), + ) + @dataclass class BlowoutProperties: @@ -227,6 +280,22 @@ def flow_rate(self, new_flow_rate: float) -> None: validated_flow_rate = validation.ensure_positive_float(new_flow_rate) self._flow_rate = validated_flow_rate + def _get_shared_data_params(self) -> Optional[SharedDataBlowoutParams]: + """Get the mix params in schema v1 shape.""" + if self._location is not None and self._flow_rate is not None: + return SharedDataBlowoutParams( + location=self._location, + flowRate=self._flow_rate, + ) + else: + return None + + def as_shared_data_model(self) -> SharedDataBlowoutProperties: + return SharedDataBlowoutProperties( + enable=self._enabled, + params=self._get_shared_data_params(), + ) + @dataclass class SubmergeRetractCommon: @@ -271,6 +340,14 @@ def delay(self) -> DelayProperties: class Submerge(SubmergeRetractCommon): ... + def as_shared_data_model(self) -> SharedDataSubmerge: + return SharedDataSubmerge( + positionReference=self._position_reference, + offset=self._offset, + speed=self._speed, + delay=self._delay.as_shared_data_model(), + ) + @dataclass class RetractAspirate(SubmergeRetractCommon): @@ -286,6 +363,16 @@ def air_gap_by_volume(self) -> LiquidHandlingPropertyByVolume: def touch_tip(self) -> TouchTipProperties: return self._touch_tip + def as_shared_data_model(self) -> SharedDataRetractAspirate: + return SharedDataRetractAspirate( + positionReference=self._position_reference, + offset=self._offset, + speed=self._speed, + airGapByVolume=self._air_gap_by_volume.as_list_of_tuples(), + touchTip=self._touch_tip.as_shared_data_model(), + delay=self._delay.as_shared_data_model(), + ) + @dataclass class RetractDispense(SubmergeRetractCommon): @@ -306,6 +393,17 @@ def touch_tip(self) -> TouchTipProperties: def blowout(self) -> BlowoutProperties: return self._blowout + def as_shared_data_model(self) -> SharedDataRetractDispense: + return SharedDataRetractDispense( + positionReference=self._position_reference, + offset=self._offset, + speed=self._speed, + airGapByVolume=self._air_gap_by_volume.as_list_of_tuples(), + blowout=self._blowout.as_shared_data_model(), + touchTip=self._touch_tip.as_shared_data_model(), + delay=self._delay.as_shared_data_model(), + ) + @dataclass class BaseLiquidHandlingProperties: @@ -375,6 +473,19 @@ def retract(self) -> RetractAspirate: def mix(self) -> MixProperties: return self._mix + def as_shared_data_model(self) -> SharedDataAspirateProperties: + return SharedDataAspirateProperties( + submerge=self._submerge.as_shared_data_model(), + retract=self._retract.as_shared_data_model(), + positionReference=self._position_reference, + offset=self._offset, + flowRateByVolume=self._flow_rate_by_volume.as_list_of_tuples(), + preWet=self._pre_wet, + mix=self._mix.as_shared_data_model(), + delay=self._delay.as_shared_data_model(), + correctionByVolume=self._correction_by_volume.as_list_of_tuples(), + ) + @dataclass class SingleDispenseProperties(BaseLiquidHandlingProperties): @@ -395,6 +506,19 @@ def retract(self) -> RetractDispense: def mix(self) -> MixProperties: return self._mix + def as_shared_data_model(self) -> SharedDataSingleDispenseProperties: + return SharedDataSingleDispenseProperties( + submerge=self._submerge.as_shared_data_model(), + retract=self._retract.as_shared_data_model(), + positionReference=self._position_reference, + offset=self._offset, + flowRateByVolume=self._flow_rate_by_volume.as_list_of_tuples(), + mix=self._mix.as_shared_data_model(), + pushOutByVolume=self._push_out_by_volume.as_list_of_tuples(), + delay=self._delay.as_shared_data_model(), + correctionByVolume=self._correction_by_volume.as_list_of_tuples(), + ) + @dataclass class MultiDispenseProperties(BaseLiquidHandlingProperties): @@ -415,6 +539,19 @@ def conditioning_by_volume(self) -> LiquidHandlingPropertyByVolume: def disposal_by_volume(self) -> LiquidHandlingPropertyByVolume: return self._disposal_by_volume + def as_shared_data_model(self) -> SharedDataMultiDispenseProperties: + return SharedDataMultiDispenseProperties( + submerge=self._submerge.as_shared_data_model(), + retract=self._retract.as_shared_data_model(), + positionReference=self._position_reference, + offset=self._offset, + flowRateByVolume=self._flow_rate_by_volume.as_list_of_tuples(), + conditioningByVolume=self._conditioning_by_volume.as_list_of_tuples(), + disposalByVolume=self._disposal_by_volume.as_list_of_tuples(), + delay=self._delay.as_shared_data_model(), + correctionByVolume=self._correction_by_volume.as_list_of_tuples(), + ) + @dataclass class TransferProperties: diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 8fc707541f0..010f3110fdb 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -2,13 +2,13 @@ from __future__ import annotations -from typing import Optional, TYPE_CHECKING, cast, Union -from opentrons.protocols.api_support.types import APIVersion - +from typing import Optional, TYPE_CHECKING, cast, Union, List from opentrons.types import Location, Mount, NozzleConfigurationType, NozzleMapInterface from opentrons.hardware_control import SyncHardwareAPI from opentrons.hardware_control.dev_types import PipetteDict from opentrons.protocols.api_support.util import FlowRates, find_value_for_api_version +from opentrons.protocols.api_support.types import APIVersion +from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2 from opentrons.protocol_engine import commands as cmd from opentrons.protocol_engine import ( DeckPoint, @@ -27,6 +27,7 @@ PRIMARY_NOZZLE_LITERAL, NozzleLayoutConfigurationType, AddressableOffsetVector, + LiquidClassRecord, ) from opentrons.protocol_engine.errors.exceptions import TipNotAttachedError from opentrons.protocol_engine.clients import SyncClient as EngineClient @@ -38,14 +39,13 @@ from opentrons.protocol_api._nozzle_layout import NozzleLayout from . import overlap_versions, pipette_movement_conflict -from ..instrument import AbstractInstrument from .well import WellCore - +from ..instrument import AbstractInstrument from ...disposal_locations import TrashBin, WasteChute if TYPE_CHECKING: from .protocol import ProtocolCore - + from opentrons.protocol_api._liquid import LiquidClass _DISPENSE_VOLUME_VALIDATION_ADDED_IN = APIVersion(2, 17) @@ -864,6 +864,45 @@ def configure_nozzle_layout( ) ) + def load_liquid_class( + self, + liquid_class: LiquidClass, + pipette_load_name: str, + tiprack_uri: str, + ) -> str: + """Load a liquid class into the engine and return its ID.""" + transfer_props = liquid_class.get_for( + pipette=pipette_load_name, tiprack=tiprack_uri + ) + + liquid_class_record = LiquidClassRecord( + liquidClassName=liquid_class.name, + pipetteModel=self.get_model(), # TODO: verify this is the correct 'model' to use + tiprack=tiprack_uri, + aspirate=transfer_props.aspirate.as_shared_data_model(), + singleDispense=transfer_props.dispense.as_shared_data_model(), + multiDispense=transfer_props.multi_dispense.as_shared_data_model() + if transfer_props.multi_dispense + else None, + ) + result = self._engine_client.execute_command_without_recovery( + cmd.LoadLiquidClassParams( + liquidClassRecord=liquid_class_record, + ) + ) + return result.liquidClassId + + def transfer_liquid( + self, + liquid_class_id: str, + volume: float, + source: List[WellCore], + dest: List[WellCore], + new_tip: TransferTipPolicyV2, + trash_location: Union[WellCore, Location, TrashBin, WasteChute], + ) -> None: + """Execute transfer using liquid class properties.""" + def retract(self) -> None: """Retract this instrument to the top of the gantry.""" z_axis = self._engine_client.state.pipettes.get_z_axis(self._pipette_id) diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index f110bde928d..bc1ec3669df 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -3,13 +3,14 @@ from __future__ import annotations from abc import abstractmethod, ABC -from typing import Any, Generic, Optional, TypeVar, Union +from typing import Any, Generic, Optional, TypeVar, Union, List from opentrons import types from opentrons.hardware_control.dev_types import PipetteDict from opentrons.protocols.api_support.util import FlowRates +from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2 from opentrons.protocol_api._nozzle_layout import NozzleLayout - +from opentrons.protocol_api._liquid import LiquidClass from ..disposal_locations import TrashBin, WasteChute from .well import WellCoreType @@ -309,6 +310,32 @@ def configure_nozzle_layout( """ ... + @abstractmethod + def load_liquid_class( + self, + liquid_class: LiquidClass, + pipette_load_name: str, + tiprack_uri: str, + ) -> str: + """Load the liquid class properties of given pipette and tiprack into the engine. + + Returns: ID of the liquid class record + """ + ... + + @abstractmethod + def transfer_liquid( + self, + liquid_class_id: str, + volume: float, + source: List[WellCoreType], + dest: List[WellCoreType], + new_tip: TransferTipPolicyV2, + trash_location: Union[WellCoreType, types.Location, TrashBin, WasteChute], + ) -> None: + """Transfer a liquid from source to dest according to liquid class properties.""" + ... + @abstractmethod def is_tip_tracking_available(self) -> bool: """Return whether auto tip tracking is available for the pipette's current nozzle configuration.""" diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py index 76d49b40557..d2d25051d49 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py @@ -1,12 +1,13 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Optional, Union, List from opentrons import types from opentrons.hardware_control import CriticalPoint from opentrons.hardware_control.dev_types import PipetteDict from opentrons.protocol_api.core.common import WellCore +from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2 from opentrons.protocols.api_support import instrument as instrument_support from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION from opentrons.protocols.api_support.labware_like import LabwareLike @@ -19,6 +20,7 @@ ) from opentrons.protocols.geometry import planning from opentrons.protocol_api._nozzle_layout import NozzleLayout +from opentrons.protocol_api._liquid import LiquidClass from ...disposal_locations import TrashBin, WasteChute from ..instrument import AbstractInstrument @@ -554,6 +556,29 @@ def configure_nozzle_layout( """This will never be called because it was added in API 2.16.""" pass + def load_liquid_class( + self, + liquid_class: LiquidClass, + pipette_load_name: str, + tiprack_uri: str, + ) -> str: + """This will never be called because it was added in ..""" + # TODO(spp, 2024-11-20): update the docstring and error to include API version + assert False, "load_liquid_class is not supported in legacy context" + + def transfer_liquid( + self, + liquid_class_id: str, + volume: float, + source: List[LegacyWellCore], + dest: List[LegacyWellCore], + new_tip: TransferTipPolicyV2, + trash_location: Union[LegacyWellCore, types.Location, TrashBin, WasteChute], + ) -> None: + """This will never be called because it was added in ..""" + # TODO(spp, 2024-11-20): update the docstring and error to include API version + assert False, "transfer_liquid is not supported in legacy context" + def get_active_channels(self) -> int: """This will never be called because it was added in API 2.16.""" assert False, "get_active_channels only supported in API 2.16 & later" diff --git a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py index f55bf05c447..ec194874528 100644 --- a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py @@ -1,12 +1,13 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING, Optional, Union +from typing import TYPE_CHECKING, Optional, Union, List from opentrons import types from opentrons.hardware_control.dev_types import PipetteDict from opentrons.hardware_control.types import HardwareAction from opentrons.protocol_api.core.common import WellCore +from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2 from opentrons.protocols.api_support import instrument as instrument_support from opentrons.protocols.api_support.labware_like import LabwareLike from opentrons.protocols.api_support.types import APIVersion @@ -24,6 +25,7 @@ from ...disposal_locations import TrashBin, WasteChute from opentrons.protocol_api._nozzle_layout import NozzleLayout +from opentrons.protocol_api._liquid import LiquidClass from ..instrument import AbstractInstrument @@ -472,6 +474,29 @@ def configure_nozzle_layout( """This will never be called because it was added in API 2.15.""" pass + def load_liquid_class( + self, + liquid_class: LiquidClass, + pipette_load_name: str, + tiprack_uri: str, + ) -> str: + """This will never be called because it was added in ..""" + # TODO(spp, 2024-11-20): update the docstring and error to include API version + assert False, "load_liquid_class is not supported in legacy context" + + def transfer_liquid( + self, + liquid_class_id: str, + volume: float, + source: List[LegacyWellCore], + dest: List[LegacyWellCore], + new_tip: TransferTipPolicyV2, + trash_location: Union[LegacyWellCore, types.Location, TrashBin, WasteChute], + ) -> None: + """Transfer a liquid from source to dest according to liquid class properties.""" + # TODO(spp, 2024-11-20): update the docstring and error to include API version + assert False, "transfer_liquid is not supported in legacy context" + def get_active_channels(self) -> int: """This will never be called because it was added in API 2.16.""" assert False, "get_active_channels only supported in API 2.16 & later" diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 7cc2d43bac2..8cc3e0bd14e 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -8,6 +8,8 @@ UnexpectedTipRemovalError, UnsupportedHardwareCommand, ) +from opentrons_shared_data.robot.types import RobotTypeEnum + from opentrons.legacy_broker import LegacyBroker from opentrons.hardware_control.dev_types import PipetteDict from opentrons import types @@ -16,7 +18,6 @@ from opentrons.legacy_commands import publisher from opentrons.protocols.advanced_control.mix import mix_from_kwargs from opentrons.protocols.advanced_control.transfers import transfer as v1_transfer - from opentrons.protocols.api_support.deck_type import NoTrashDefinedError from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support import instrument @@ -35,9 +36,13 @@ from .config import Clearances from .disposal_locations import TrashBin, WasteChute from ._nozzle_layout import NozzleLayout +from ._liquid import LiquidClass from . import labware, validation - -AdvancedLiquidHandling = v1_transfer.AdvancedLiquidHandling +from ..config import feature_flags +from ..protocols.advanced_control.transfers.common import ( + TransferTipPolicyV2, + TransferTipPolicyV2Type, +) _DEFAULT_ASPIRATE_CLEARANCE = 1.0 _DEFAULT_DISPENSE_CLEARANCE = 1.0 @@ -61,6 +66,8 @@ _AIR_GAP_TRACKING_ADDED_IN = APIVersion(2, 22) """The version after which air gaps should be implemented with a separate call instead of an aspirate for better liquid volume tracking.""" +AdvancedLiquidHandling = v1_transfer.AdvancedLiquidHandling + class InstrumentContext(publisher.CommandPublisher): """ @@ -1219,7 +1226,6 @@ def home_plunger(self) -> InstrumentContext: self._core.home_plunger() return self - # TODO (spp, 2024-03-08): verify if ok to & change source & dest types to AdvancedLiquidHandling @publisher.publish(command=cmds.distribute) @requires_version(2, 0) def distribute( @@ -1259,7 +1265,6 @@ def distribute( return self.transfer(volume, source, dest, **kwargs) - # TODO (spp, 2024-03-08): verify if ok to & change source & dest types to AdvancedLiquidHandling @publisher.publish(command=cmds.consolidate) @requires_version(2, 0) def consolidate( @@ -1505,6 +1510,107 @@ def _execute_transfer(self, plan: v1_transfer.TransferPlan) -> None: for cmd in plan: getattr(self, cmd["method"])(*cmd["args"], **cmd["kwargs"]) + def transfer_liquid( + self, + liquid_class: LiquidClass, + volume: float, + source: Union[ + labware.Well, Sequence[labware.Well], Sequence[Sequence[labware.Well]] + ], + dest: Union[ + labware.Well, Sequence[labware.Well], Sequence[Sequence[labware.Well]] + ], + new_tip: TransferTipPolicyV2Type = "once", + tip_drop_location: Optional[ + Union[types.Location, labware.Well, TrashBin, WasteChute] + ] = None, # Maybe call this 'tip_drop_location' which is similar to PD + ) -> InstrumentContext: + """Transfer liquid from source to dest using the specified liquid class properties. + + TODO: Add args description. + """ + if not feature_flags.allow_liquid_classes( + robot_type=RobotTypeEnum.robot_literal_to_enum( + self._protocol_core.robot_type + ) + ): + raise NotImplementedError("This method is not implemented.") + + flat_sources_list = validation.ensure_valid_flat_wells_list_for_transfer_v2( + source + ) + flat_dests_list = validation.ensure_valid_flat_wells_list_for_transfer_v2(dest) + for well in flat_sources_list + flat_dests_list: + instrument.validate_takes_liquid( + location=well.top(), + reject_module=True, + reject_adapter=True, + ) + if len(flat_sources_list) != len(flat_dests_list): + raise ValueError( + "Sources and destinations should be of the same length in order to perform a transfer." + " To transfer liquid from one source to many destinations, use 'distribute_liquid'," + " to transfer liquid onto one destinations from many sources, use 'consolidate_liquid'." + ) + + valid_new_tip = validation.ensure_new_tip_policy(new_tip) + if valid_new_tip == TransferTipPolicyV2.NEVER: + if self._last_tip_picked_up_from is None: + raise RuntimeError( + "Pipette has no tip attached to perform transfer." + " Either do a pick_up_tip beforehand or specify a new_tip parameter" + " of 'once' or 'always'." + ) + else: + tiprack = self._last_tip_picked_up_from.parent + else: + tiprack, well = labware.next_available_tip( + starting_tip=self.starting_tip, + tip_racks=self.tip_racks, + channels=self.active_channels, + nozzle_map=self._core.get_nozzle_map(), + ) + if self.current_volume != 0: + raise RuntimeError( + "A transfer on a liquid class cannot start with liquid already in the tip." + " Ensure that all previously aspirated liquid is dispensed before starting" + " a new transfer." + ) + + _trash_location: Union[types.Location, labware.Well, TrashBin, WasteChute] + if tip_drop_location is None: + saved_trash = self.trash_container + if isinstance(saved_trash, labware.Labware): + _trash_location = saved_trash.wells()[0] + else: + _trash_location = saved_trash + else: + _trash_location = tip_drop_location + + checked_trash_location = ( + validation.ensure_valid_tip_drop_location_for_transfer_v2( + tip_drop_location=_trash_location + ) + ) + liquid_class_id = self._core.load_liquid_class( + liquid_class=liquid_class, + pipette_load_name=self.name, + tiprack_uri=tiprack.uri, + ) + + self._core.transfer_liquid( + liquid_class_id=liquid_class_id, + volume=volume, + source=[well._core for well in flat_sources_list], + dest=[well._core for well in flat_dests_list], + new_tip=valid_new_tip, + trash_location=checked_trash_location._core + if isinstance(checked_trash_location, labware.Well) + else checked_trash_location, + ) + + return self + @requires_version(2, 0) def delay(self, *args: Any, **kwargs: Any) -> None: """ diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index 44123571081..f0db8a71e5e 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -21,6 +21,7 @@ from opentrons.protocols.api_support.types import APIVersion, ThermocyclerStep from opentrons.protocols.api_support.util import APIVersionError from opentrons.protocols.models import LabwareDefinition +from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2 from opentrons.types import ( Mount, DeckSlotName, @@ -634,3 +635,85 @@ def validate_coordinates(value: Sequence[float]) -> Tuple[float, float, float]: if not all(isinstance(v, (float, int)) for v in value): raise ValueError("All values in coordinates must be floats.") return float(value[0]), float(value[1]), float(value[2]) + + +def ensure_new_tip_policy(value: str) -> TransferTipPolicyV2: + """Ensure that new_tip value is a valid TransferTipPolicy value.""" + try: + return TransferTipPolicyV2(value.lower()) + except ValueError: + raise ValueError( + f"'{value}' is invalid value for 'new_tip'." + f" Acceptable value is either 'never', 'once', 'always' or 'per source'." + ) + + +def _verify_each_list_element_is_valid_location(locations: Sequence[Well]) -> None: + from .labware import Well + + for loc in locations: + if not isinstance(loc, Well): + raise ValueError( + f"'{loc}' is not a valid location for transfer." + f" Location should be a well instance." + ) + + +def ensure_valid_flat_wells_list_for_transfer_v2( + target: Union[Well, Sequence[Well], Sequence[Sequence[Well]]], +) -> List[Well]: + """Ensure that the given target(s) for a liquid transfer are valid and in a flat list.""" + from .labware import Well + + if isinstance(target, Well): + return [target] + + if isinstance(target, (list, tuple)): + if len(target) == 0: + raise ValueError("No target well(s) specified for transfer.") + if isinstance(target[0], (list, tuple)): + for sub_sequence in target: + _verify_each_list_element_is_valid_location(sub_sequence) + return [loc for sub_sequence in target for loc in sub_sequence] + else: + _verify_each_list_element_is_valid_location(target) + return list(target) + else: + raise ValueError( + f"'{target}' is not a valid location for transfer." + f" Location should be a well instance, or a 1-dimensional or" + f" 2-dimensional sequence of well instances." + ) + + +def ensure_valid_tip_drop_location_for_transfer_v2( + tip_drop_location: Union[Location, Well, TrashBin, WasteChute] +) -> Union[Location, Well, TrashBin, WasteChute]: + """Ensure that the tip drop location is valid for v2 transfer.""" + from .labware import Well + + if ( + isinstance(tip_drop_location, Well) + or isinstance(tip_drop_location, TrashBin) + or isinstance(tip_drop_location, WasteChute) + ): + return tip_drop_location + elif isinstance(tip_drop_location, Location): + _, maybe_well = tip_drop_location.labware.get_parent_labware_and_well() + + if maybe_well is None: + raise TypeError( + "If a location is specified as a `types.Location`" + " (for instance, as the result of a call to `Well.top()`)," + " it must be a location relative to a well," + " since that is where a tip is dropped." + " However, the given location doesn't refer to any well." + ) + return tip_drop_location + else: + raise TypeError( + f"If specified, location should be an instance of" + f" `types.Location` (e.g. the return value from `Well.top()`)" + f" or `Well` (e.g. `reservoir.wells()[0]`) or an instance of `TrashBin` or `WasteChute`." + f" However, it is '{tip_drop_location}'." + ) diff --git a/api/src/opentrons/protocol_engine/clients/sync_client.py b/api/src/opentrons/protocol_engine/clients/sync_client.py index 3460c13d463..71837a7a2ca 100644 --- a/api/src/opentrons/protocol_engine/clients/sync_client.py +++ b/api/src/opentrons/protocol_engine/clients/sync_client.py @@ -89,6 +89,12 @@ def execute_command_without_recovery( ) -> commands.TryLiquidProbeResult: pass + @overload + def execute_command_without_recovery( + self, params: commands.LoadLiquidClassParams + ) -> commands.LoadLiquidClassResult: + pass + def execute_command_without_recovery( self, params: commands.CommandParams ) -> commands.CommandResult: diff --git a/api/src/opentrons/protocol_engine/commands/__init__.py b/api/src/opentrons/protocol_engine/commands/__init__.py index 8e1e91bec50..f25293f85fb 100644 --- a/api/src/opentrons/protocol_engine/commands/__init__.py +++ b/api/src/opentrons/protocol_engine/commands/__init__.py @@ -147,6 +147,15 @@ LoadLiquidImplementation, ) +from .load_liquid_class import ( + LoadLiquidClass, + LoadLiquidClassParams, + LoadLiquidClassCreate, + LoadLiquidClassResult, + LoadLiquidClassCommandType, + LoadLiquidClassImplementation, +) + from .load_module import ( LoadModule, LoadModuleParams, @@ -553,6 +562,13 @@ "LoadLiquidParams", "LoadLiquidResult", "LoadLiquidCommandType", + # load liquid class command models + "LoadLiquidClass", + "LoadLiquidClassParams", + "LoadLiquidClassCreate", + "LoadLiquidClassResult", + "LoadLiquidClassImplementation", + "LoadLiquidClassCommandType", # hardware control command models # hardware module command bundles "absorbance_reader", diff --git a/api/src/opentrons/protocols/advanced_control/transfers/common.py b/api/src/opentrons/protocols/advanced_control/transfers/common.py index e7f41f2e8e9..c40a55beacd 100644 --- a/api/src/opentrons/protocols/advanced_control/transfers/common.py +++ b/api/src/opentrons/protocols/advanced_control/transfers/common.py @@ -1,5 +1,16 @@ """Common functions between v1 transfer and liquid-class-based transfer.""" -from typing import Iterable, Generator, Tuple, TypeVar +import enum +from typing import Iterable, Generator, Tuple, TypeVar, Literal + + +class TransferTipPolicyV2(enum.Enum): + ONCE = "once" + NEVER = "never" + ALWAYS = "always" + PER_SOURCE = "per source" + + +TransferTipPolicyV2Type = Literal["once", "always", "per source", "never"] Target = TypeVar("Target") diff --git a/api/src/opentrons/protocols/advanced_control/transfers/transfer.py b/api/src/opentrons/protocols/advanced_control/transfers/transfer.py index 1c6c9a78288..3f5f90ab550 100644 --- a/api/src/opentrons/protocols/advanced_control/transfers/transfer.py +++ b/api/src/opentrons/protocols/advanced_control/transfers/transfer.py @@ -22,7 +22,6 @@ from . import common as tx_commons from ..common import Mix, MixOpts, MixStrategy - AdvancedLiquidHandling = Union[ Well, types.Location, diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index b2dff4a7254..352dcb35c58 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -5,11 +5,15 @@ import pytest from decoy import Decoy from decoy import errors +from opentrons_shared_data.liquid_classes.liquid_class_definition import ( + LiquidClassSchemaV1, +) from opentrons_shared_data.pipette.types import PipetteNameType from opentrons.hardware_control import SyncHardwareAPI from opentrons.hardware_control.dev_types import PipetteDict +from opentrons.protocol_api._liquid_properties import TransferProperties from opentrons.protocol_engine import ( DeckPoint, LoadedPipette, @@ -35,6 +39,7 @@ SingleNozzleLayoutConfiguration, ColumnNozzleLayoutConfiguration, AddressableOffsetVector, + LiquidClassRecord, ) from opentrons.protocol_api.disposal_locations import ( TrashBin, @@ -42,6 +47,7 @@ DisposalOffset, ) from opentrons.protocol_api._nozzle_layout import NozzleLayout +from opentrons.protocol_api._liquid import LiquidClass from opentrons.protocol_api.core.engine import ( InstrumentCore, WellCore, @@ -1494,3 +1500,59 @@ def test_liquid_probe_with_recovery( ) ) ) + + +def test_load_liquid_class( + decoy: Decoy, + mock_engine_client: EngineClient, + subject: InstrumentCore, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should send the load liquid class command to the engine.""" + sample_aspirate_data = minimal_liquid_class_def2.byPipette[0].byTipType[0].aspirate + sample_single_dispense_data = ( + minimal_liquid_class_def2.byPipette[0].byTipType[0].singleDispense + ) + sample_multi_dispense_data = ( + minimal_liquid_class_def2.byPipette[0].byTipType[0].multiDispense + ) + + test_liq_class = decoy.mock(cls=LiquidClass) + test_transfer_props = decoy.mock(cls=TransferProperties) + + decoy.when( + test_liq_class.get_for("flex_1channel_50", "opentrons_flex_96_tiprack_50ul") + ).then_return(test_transfer_props) + decoy.when(test_liq_class.name).then_return("water") + decoy.when( + mock_engine_client.state.pipettes.get_model_name(subject.pipette_id) + ).then_return("flex_1channel_50") + decoy.when(test_transfer_props.aspirate.as_shared_data_model()).then_return( + sample_aspirate_data + ) + decoy.when(test_transfer_props.dispense.as_shared_data_model()).then_return( + sample_single_dispense_data + ) + decoy.when(test_transfer_props.multi_dispense.as_shared_data_model()).then_return( # type: ignore[union-attr] + sample_multi_dispense_data + ) + decoy.when( + mock_engine_client.execute_command_without_recovery( + cmd.LoadLiquidClassParams( + liquidClassRecord=LiquidClassRecord( + liquidClassName="water", + pipetteModel="flex_1channel_50", + tiprack="opentrons_flex_96_tiprack_50ul", + aspirate=sample_aspirate_data, + singleDispense=sample_single_dispense_data, + multiDispense=sample_multi_dispense_data, + ) + ) + ) + ).then_return(cmd.LoadLiquidClassResult(liquidClassId="liquid-class-id")) + result = subject.load_liquid_class( + liquid_class=test_liq_class, + pipette_load_name="flex_1channel_50", + tiprack_uri="opentrons_flex_96_tiprack_50ul", + ) + assert result == "liquid-class-id" diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 1caae624377..3f639aff922 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -10,12 +10,14 @@ from decoy import Decoy from pytest_lazyfixture import lazy_fixture # type: ignore[import-untyped] +from opentrons.config import feature_flags as ff from opentrons.protocol_engine.commands.pipetting_common import LiquidNotFoundError from opentrons.protocol_engine.errors.error_occurrence import ( ProtocolCommandFailedError, ) from opentrons.legacy_broker import LegacyBroker +from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2 from tests.opentrons.protocol_api.partial_tip_configurations import ( PipetteReliantNozzleConfigSpec, @@ -42,6 +44,7 @@ Well, labware, validation as mock_validation, + LiquidClass, ) from opentrons.protocol_api.validation import WellTarget, PointTarget from opentrons.protocol_api.core.common import InstrumentCore, ProtocolCore @@ -51,12 +54,16 @@ from opentrons.hardware_control.nozzle_manager import NozzleMap from opentrons.protocol_api.disposal_locations import TrashBin, WasteChute -from opentrons_shared_data.pipette.pipette_definition import ValidNozzleMaps from opentrons.types import Location, Mount, Point +from opentrons_shared_data.pipette.pipette_definition import ValidNozzleMaps from opentrons_shared_data.errors.exceptions import ( CommandPreconditionViolated, ) +from opentrons_shared_data.liquid_classes.liquid_class_definition import ( + LiquidClassSchemaV1, +) +from opentrons_shared_data.robot.types import RobotTypeEnum, RobotType from . import versions_at_or_above, versions_between @@ -1723,3 +1730,277 @@ def test_air_gap_uses_air_gap( decoy.verify(mock_move_to(top_location, publish=False)) decoy.verify(mock_instrument_core.air_gap_in_place(10, 11)) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_transfer_liquid_raises_for_invalid_locations( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if source or destination is invalid.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_raise(ValueError("Oh no")) + with pytest.raises(ValueError): + subject.transfer_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest=[[mock_well]], + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_transfer_liquid_raises_for_unequal_source_and_dest( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if source and destination are not of same length.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2(mock_well) + ).then_return([mock_well, mock_well]) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + with pytest.raises( + ValueError, match="Sources and destinations should be of the same length" + ): + subject.transfer_liquid( + liquid_class=test_liq_class, volume=10, source=mock_well, dest=[mock_well] + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_transfer_liquid_raises_for_non_liquid_handling_locations( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if source and destination are not of same length.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when( + mock_instrument_support.validate_takes_liquid( + mock_well.top(), reject_module=True, reject_adapter=True + ) + ).then_raise(ValueError("Uh oh")) + with pytest.raises(ValueError, match="Uh oh"): + subject.transfer_liquid( + liquid_class=test_liq_class, volume=10, source=[mock_well], dest=[mock_well] + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_transfer_liquid_raises_for_bad_tip_policy( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if new_tip is invalid.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when(mock_validation.ensure_new_tip_policy("once")).then_raise( + ValueError("Uh oh") + ) + with pytest.raises(ValueError, match="Uh oh"): + subject.transfer_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest=[mock_well], + new_tip="once", + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_transfer_liquid_raises_for_no_tip( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if there is no tip attached.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when(mock_validation.ensure_new_tip_policy("never")).then_return( + TransferTipPolicyV2.NEVER + ) + with pytest.raises(RuntimeError, match="Pipette has no tip"): + subject.transfer_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest=[mock_well], + new_tip="never", + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_transfer_liquid_raises_if_tip_has_liquid( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should raise errors if there is no tip attached.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + tip_racks = [decoy.mock(cls=Labware)] + + subject.starting_tip = None + subject.tip_racks = tip_racks + + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when(mock_validation.ensure_new_tip_policy("never")).then_return( + TransferTipPolicyV2.ONCE + ) + decoy.when(mock_instrument_core.get_nozzle_map()).then_return(MOCK_MAP) + decoy.when(mock_instrument_core.get_active_channels()).then_return(2) + decoy.when( + labware.next_available_tip( + starting_tip=None, + tip_racks=tip_racks, + channels=2, + nozzle_map=MOCK_MAP, + ) + ).then_return((decoy.mock(cls=Labware), decoy.mock(cls=Well))) + decoy.when(mock_instrument_core.get_current_volume()).then_return(1000) + with pytest.raises(RuntimeError, match="liquid already in the tip"): + subject.transfer_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest=[mock_well], + new_tip="never", + ) + + +@pytest.mark.parametrize("robot_type", ["OT-2 Standard", "OT-3 Standard"]) +def test_transfer_liquid_delegates_to_engine_core( + decoy: Decoy, + mock_protocol_core: ProtocolCore, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_feature_flags: None, + robot_type: RobotType, + minimal_liquid_class_def2: LiquidClassSchemaV1, +) -> None: + """It should load liquid class into engine and delegate the transfer execution to core.""" + test_liq_class = LiquidClass.create(minimal_liquid_class_def2) + mock_well = decoy.mock(cls=Well) + tip_racks = [decoy.mock(cls=Labware)] + trash_location = Location(point=Point(1, 2, 3), labware=mock_well) + next_tiprack = decoy.mock(cls=Labware) + subject.starting_tip = None + subject.tip_racks = tip_racks + + decoy.when(mock_protocol_core.robot_type).then_return(robot_type) + decoy.when( + ff.allow_liquid_classes(RobotTypeEnum.robot_literal_to_enum(robot_type)) + ).then_return(True) + decoy.when( + mock_validation.ensure_valid_flat_wells_list_for_transfer_v2([mock_well]) + ).then_return([mock_well]) + decoy.when(mock_validation.ensure_new_tip_policy("never")).then_return( + TransferTipPolicyV2.ONCE + ) + decoy.when(mock_instrument_core.get_nozzle_map()).then_return(MOCK_MAP) + decoy.when(mock_instrument_core.get_active_channels()).then_return(2) + decoy.when( + labware.next_available_tip( + starting_tip=None, + tip_racks=tip_racks, + channels=2, + nozzle_map=MOCK_MAP, + ) + ).then_return((next_tiprack, decoy.mock(cls=Well))) + decoy.when(mock_instrument_core.get_current_volume()).then_return(0) + decoy.when( + mock_validation.ensure_valid_tip_drop_location_for_transfer_v2(trash_location) + ).then_return(trash_location.move(Point(1, 2, 3))) + decoy.when(next_tiprack.uri).then_return("tiprack-uri") + decoy.when(mock_instrument_core.get_pipette_name()).then_return("pipette-name") + decoy.when( + mock_instrument_core.load_liquid_class( + liquid_class=test_liq_class, + pipette_load_name="pipette-name", + tiprack_uri="tiprack-uri", + ) + ).then_return("liq-class-id") + + subject.transfer_liquid( + liquid_class=test_liq_class, + volume=10, + source=[mock_well], + dest=[mock_well], + new_tip="never", + tip_drop_location=trash_location, + ) + decoy.verify( + mock_instrument_core.transfer_liquid( + liquid_class_id="liq-class-id", + volume=10, + source=[mock_well._core], + dest=[mock_well._core], + new_tip=TransferTipPolicyV2.ONCE, + trash_location=trash_location.move(Point(1, 2, 3)), + ) + ) diff --git a/api/tests/opentrons/protocol_api/test_liquid_class_properties.py b/api/tests/opentrons/protocol_api/test_liquid_class_properties.py index 356add50ae8..f335cb385bc 100644 --- a/api/tests/opentrons/protocol_api/test_liquid_class_properties.py +++ b/api/tests/opentrons/protocol_api/test_liquid_class_properties.py @@ -55,6 +55,7 @@ def test_build_aspirate_settings() -> None: assert aspirate_properties.mix.volume == 15 assert aspirate_properties.delay.enabled is True assert aspirate_properties.delay.duration == 2 + assert aspirate_properties.as_shared_data_model() == aspirate_data def test_build_single_dispense_settings() -> None: @@ -111,6 +112,7 @@ def test_build_single_dispense_settings() -> None: } assert single_dispense_properties.delay.enabled is True assert single_dispense_properties.delay.duration == 2.5 + assert single_dispense_properties.as_shared_data_model() == single_dispense_data def test_build_multi_dispense_settings() -> None: @@ -166,6 +168,7 @@ def test_build_multi_dispense_settings() -> None: } assert multi_dispense_properties.delay.enabled is True assert multi_dispense_properties.delay.duration == 1 + assert multi_dispense_properties.as_shared_data_model() == multi_dispense_data def test_build_multi_dispense_settings_none( @@ -181,6 +184,7 @@ def test_liquid_handling_property_by_volume() -> None: subject = LiquidHandlingPropertyByVolume([(5.0, 50.0), (10.0, 250.0)]) assert subject.as_dict() == {5.0: 50, 10.0: 250} assert subject.get_for_volume(7) == 130.0 + assert subject.as_list_of_tuples() == [(5.0, 50.0), (10.0, 250.0)] subject.set_for_volume(volume=7, value=175.5) assert subject.as_dict() == { diff --git a/api/tests/opentrons/protocol_api/test_validation.py b/api/tests/opentrons/protocol_api/test_validation.py index 9a111e6f81f..342e197535b 100644 --- a/api/tests/opentrons/protocol_api/test_validation.py +++ b/api/tests/opentrons/protocol_api/test_validation.py @@ -6,6 +6,7 @@ import pytest import re +from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2 from opentrons_shared_data.labware.labware_definition import ( LabwareRole, Parameters as LabwareDefinitionParameters, @@ -34,7 +35,13 @@ from opentrons.protocols.models import LabwareDefinition from opentrons.protocols.api_support.types import APIVersion from opentrons.protocols.api_support.util import APIVersionError -from opentrons.protocol_api import validation as subject, Well, Labware +from opentrons.protocol_api import ( + validation as subject, + Well, + Labware, + TrashBin, + WasteChute, +) @pytest.mark.parametrize( @@ -722,3 +729,144 @@ def test_ensure_only_gantry_axis_map_type( """Check that gantry axis_map validation occurs for the given scenarios.""" with pytest.raises(subject.IncorrectAxisError, match=error_message): subject.ensure_only_gantry_axis_map_type(axis_map, robot_type) + + +@pytest.mark.parametrize( + ["value", "expected_result"], + [ + ("once", TransferTipPolicyV2.ONCE), + ("NEVER", TransferTipPolicyV2.NEVER), + ("alWaYs", TransferTipPolicyV2.ALWAYS), + ("Per Source", TransferTipPolicyV2.PER_SOURCE), + ], +) +def test_ensure_new_tip_policy( + value: str, expected_result: TransferTipPolicyV2 +) -> None: + """It should return the expected tip policy.""" + assert subject.ensure_new_tip_policy(value) == expected_result + + +def test_ensure_new_tip_policy_raises() -> None: + """It should raise ValueError for invalid new_tip value.""" + with pytest.raises(ValueError, match="is invalid value for 'new_tip'"): + subject.ensure_new_tip_policy("blah") + + +@pytest.mark.parametrize( + ["target", "expected_raise"], + [ + ( + "a", + pytest.raises( + ValueError, match="'a' is not a valid location for transfer." + ), + ), + ( + ["a"], + pytest.raises( + ValueError, match="'a' is not a valid location for transfer." + ), + ), + ( + [("a",)], + pytest.raises( + ValueError, match="'a' is not a valid location for transfer." + ), + ), + ( + [], + pytest.raises( + ValueError, match="No target well\\(s\\) specified for transfer." + ), + ), + ], +) +def test_ensure_valid_flat_wells_list_raises_for_invalid_targets( + target: Any, + expected_raise: ContextManager[Any], +) -> None: + """It should raise an error if target location is invalid.""" + with expected_raise: + subject.ensure_valid_flat_wells_list_for_transfer_v2(target) + + +def test_ensure_valid_flat_wells_list_raises_for_mixed_targets(decoy: Decoy) -> None: + """It should raise appropriate error if target has mixed valid and invalid wells.""" + target1 = [decoy.mock(cls=Well), "a"] + with pytest.raises(ValueError, match="'a' is not a valid location for transfer."): + subject.ensure_valid_flat_wells_list_for_transfer_v2(target1) # type: ignore[arg-type] + + target2 = [[decoy.mock(cls=Well)], ["a"]] + with pytest.raises(ValueError, match="'a' is not a valid location for transfer."): + subject.ensure_valid_flat_wells_list_for_transfer_v2(target2) # type: ignore[arg-type] + + +def test_ensure_valid_flat_wells_list(decoy: Decoy) -> None: + """It should convert the locations to flat lists correctly.""" + target1 = decoy.mock(cls=Well) + target2 = decoy.mock(cls=Well) + + assert subject.ensure_valid_flat_wells_list_for_transfer_v2(target1) == [target1] + assert subject.ensure_valid_flat_wells_list_for_transfer_v2([target1, target2]) == [ + target1, + target2, + ] + assert subject.ensure_valid_flat_wells_list_for_transfer_v2( + [ + [target1, target1], + [target2, target2], + ] + ) == [target1, target1, target2, target2] + assert subject.ensure_valid_flat_wells_list_for_transfer_v2((target1, target2)) == [ + target1, + target2, + ] + assert subject.ensure_valid_flat_wells_list_for_transfer_v2( + ( + [target1, target1], + [target2, target2], + ) + ) == [target1, target1, target2, target2] + + +def test_ensure_valid_tip_drop_location_for_transfer_v2( + decoy: Decoy, +) -> None: + """It should check that the tip drop location is valid.""" + mock_well = decoy.mock(cls=Well) + mock_location = Location(point=Point(x=1, y=1, z=1), labware=mock_well) + mock_trash_bin = decoy.mock(cls=TrashBin) + mock_waste_chute = decoy.mock(cls=WasteChute) + assert ( + subject.ensure_valid_tip_drop_location_for_transfer_v2(mock_well) == mock_well + ) + assert ( + subject.ensure_valid_tip_drop_location_for_transfer_v2(mock_location) + == mock_location + ) + assert ( + subject.ensure_valid_tip_drop_location_for_transfer_v2(mock_trash_bin) + == mock_trash_bin + ) + assert ( + subject.ensure_valid_tip_drop_location_for_transfer_v2(mock_waste_chute) + == mock_waste_chute + ) + + +def test_ensure_valid_tip_drop_location_for_transfer_v2_raises(decoy: Decoy) -> None: + """It should raise an error for invalid tip drop locations.""" + with pytest.raises(TypeError, match="However, it is '\\['a'\\]'"): + subject.ensure_valid_tip_drop_location_for_transfer_v2(["a"]) # type: ignore[arg-type] + + mock_labware = decoy.mock(cls=Labware) + with pytest.raises(TypeError, match=f"However, it is '{mock_labware}'"): + subject.ensure_valid_tip_drop_location_for_transfer_v2(mock_labware) # type: ignore[arg-type] + + with pytest.raises( + TypeError, match="However, the given location doesn't refer to any well." + ): + subject.ensure_valid_tip_drop_location_for_transfer_v2( + Location(point=Point(x=1, y=1, z=1), labware=None) + ) From 34cb6118de4a56f905fe06d40fc23f1f2e115fe2 Mon Sep 17 00:00:00 2001 From: Jethary Date: Tue, 3 Dec 2024 08:42:26 -0500 Subject: [PATCH 8/9] fix(step-generation): fix blowout location from multi-dispense clsoes RESC-356 --- .../formLevel/stepFormToArgs/moveLiquidFormToArgs.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts index b77cd8a8f2e..19fba119f32 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts @@ -166,7 +166,12 @@ export const moveLiquidFormToArgs = ( 'dispense_delay_mmFromBottom' ) const blowoutLocation = - (fields.blowout_checkbox && fields.blowout_location) || null + (fields.blowout_checkbox && fields.blowout_location) || + (fields.disposalVolume_checkbox && + fields.disposalVolume_volume && + fields.blowout_location) || + null + const blowoutOffsetFromTopMm = blowoutLocation != null ? blowout_z_offset ?? DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP From c692fec0b01b6b17f2301eed1b090e4cedd8aaa1 Mon Sep 17 00:00:00 2001 From: Jethary Date: Tue, 3 Dec 2024 09:11:50 -0500 Subject: [PATCH 9/9] make sure blowout only emits for multiDispenses --- .../steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts index 19fba119f32..4ee644679dc 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts @@ -168,6 +168,7 @@ export const moveLiquidFormToArgs = ( const blowoutLocation = (fields.blowout_checkbox && fields.blowout_location) || (fields.disposalVolume_checkbox && + path === 'multiDispense' && fields.disposalVolume_volume && fields.blowout_location) || null