diff --git a/.github/workflows/app-test-build-deploy.yaml b/.github/workflows/app-test-build-deploy.yaml index d443fae35a0..f0bfe7d8946 100644 --- a/.github/workflows/app-test-build-deploy.yaml +++ b/.github/workflows/app-test-build-deploy.yaml @@ -318,12 +318,12 @@ jobs: if: startsWith(matrix.os, 'windows') && contains(needs.determine-build-type.outputs.type, 'release') shell: cmd env: - SM_HOST: ${{ secrets.SM_HOST_V2 }} + SM_HOST: ${{ secrets.SM_HOST }} SM_CLIENT_CERT_FILE: "D:\\Certificate_pkcs12.p12" - SM_CLIENT_CERT_PASSWORD: ${{secrets.SM_CLIENT_CERT_PASSWORD_V2}} - SM_API_KEY: ${{secrets.SM_API_KEY_V2}} + SM_CLIENT_CERT_PASSWORD: ${{secrets.SM_CLIENT_CERT_PASSWORD}} + SM_API_KEY: ${{secrets.SM_API_KEY}} run: | - curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/Keylockertools-windows-x64.msi/download -H "x-api-key:${{secrets.SM_API_KEY_V2}}" -o Keylockertools-windows-x64.msi + curl -X GET https://one.digicert.com/signingmanager/api-ui/v1/releases/Keylockertools-windows-x64.msi/download -H "x-api-key:${{secrets.SM_API_KEY}}" -o Keylockertools-windows-x64.msi msiexec /i Keylockertools-windows-x64.msi /quiet /qn smksp_registrar.exe list smctl.exe keypair ls @@ -331,15 +331,6 @@ jobs: smksp_cert_sync.exe smctl.exe healthcheck --all - # Do the frontend dist bundle - - name: 'bundle ${{matrix.variant}} frontend' - env: - OT_APP_MIXPANEL_ID: ${{ secrets.OT_APP_MIXPANEL_ID }} - OT_APP_INTERCOM_ID: ${{ secrets.OT_APP_INTERCOM_ID }} - OPENTRONS_PROJECT: ${{ steps.project.outputs.project }} - run: | - make -C app dist - # build the desktop app and deploy it - name: 'build ${{matrix.variant}} app for ${{ matrix.os }}' if: matrix.target == 'desktop' @@ -348,14 +339,18 @@ jobs: OT_APP_MIXPANEL_ID: ${{ secrets.OT_APP_MIXPANEL_ID }} OT_APP_INTERCOM_ID: ${{ secrets.OT_APP_INTERCOM_ID }} WINDOWS_SIGN: ${{ format('{0}', contains(needs.determine-build-type.outputs.type, 'release')) }} - SM_CODE_SIGNING_CERT_SHA1_HASH: ${{secrets.SM_CODE_SIGNING_CERT_SHA1_HASH_V2}} - SM_KEYPAIR_ALIAS: ${{secrets.SM_KEYPAIR_ALIAS_V2}} + SM_HOST: ${{secrets.SM_HOST}} + SM_CLIENT_CERT_FILE: "D:\\Certificate_pkcs12.p12" + SM_CLIENT_CERT_PASSWORD: ${{secrets.SM_CLIENT_CERT_PASSWORD}} + SM_API_KEY: ${{secrets.SM_API_KEY}} + SM_CODE_SIGNING_CERT_SHA1_HASH: ${{secrets.SM_CODE_SIGNING_CERT_SHA1_HASH}} + SM_KEYPAIR_ALIAS: ${{secrets.SM_KEYPAIR_ALIAS}} WINDOWS_CSC_FILEPATH: "D:\\opentrons_labworks_inc.crt" - CSC_LINK: ${{ secrets.OT_APP_CSC_MACOS_V2 }} - CSC_KEY_PASSWORD: ${{ secrets.OT_APP_CSC_KEY_MACOS_V2 }} - APPLE_ID: ${{ secrets.OT_APP_APPLE_ID_V2 }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.OT_APP_APPLE_ID_PASSWORD_V2 }} - APPLE_TEAM_ID: ${{ secrets.OT_APP_APPLE_TEAM_ID_V2 }} + CSC_LINK: ${{ secrets.OT_APP_CSC_MACOS }} + CSC_KEY_PASSWORD: ${{ secrets.OT_APP_CSC_KEY_MACOS }} + APPLE_ID: ${{ secrets.OT_APP_APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.OT_APP_APPLE_ID_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.OT_APP_APPLE_TEAM_ID }} HOST_PYTHON: python OPENTRONS_PROJECT: ${{ steps.project.outputs.project }} OT_APP_DEPLOY_BUCKET: ${{ steps.project.outputs.bucket }} diff --git a/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py b/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py index 23575165eff..f8b1e99f37d 100644 --- a/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py +++ b/abr-testing/abr_testing/protocol_simulation/abr_sim_check.py @@ -1,34 +1,109 @@ """Check ABR Protocols Simulate Successfully.""" from abr_testing.protocol_simulation import simulation_metrics import os -import traceback from pathlib import Path +from typing import Dict, List, Tuple, Union +import traceback -def run(file_to_simulate: Path) -> None: +def run( + file_dict: Dict[str, Dict[str, Union[str, Path]]], labware_defs: List[Path] +) -> None: """Simulate protocol and raise errors.""" - protocol_name = file_to_simulate.stem - try: - simulation_metrics.main(file_to_simulate, False) - except Exception: - print(f"Error in protocol: {protocol_name}") - traceback.print_exc() + for file in file_dict: + path = file_dict[file]["path"] + csv_params = "" + try: + csv_params = str(file_dict[file]["csv"]) + except KeyError: + pass + try: + print(f"Simulating {file}") + simulation_metrics.main( + protocol_file_path=Path(path), + save=False, + parameters=csv_params, + extra_files=labware_defs, + ) + except Exception as e: + traceback.print_exc() + print(str(e)) + print("\n") + + +def search(seq: str, dictionary: dict) -> str: + """Search for specific sequence in file.""" + for key in dictionary.keys(): + parts = key.split("_") + if parts[0] == seq: + return key + return "" + + +def get_files() -> Tuple[Dict[str, Dict[str, Union[str, Path]]], List[Path]]: + """Map protocols with corresponding csv files.""" + file_dict: Dict[str, Dict[str, Union[str, Path]]] = {} + labware_defs = [] + for root, directories, _ in os.walk(root_dir): + for directory in directories: + if directory == "active_protocols": + active_dir = os.path.join(root, directory) + for file in os.listdir( + active_dir + ): # Iterate over files in `active_protocols` + if file.endswith(".py") and file not in exclude: + file_dict[file] = {} + file_dict[file]["path"] = Path( + os.path.abspath( + os.path.join(root_dir, os.path.join(directory, file)) + ) + ) + if directory == "csv_parameters": + active_dir = os.path.join(root, directory) + for file in os.listdir( + active_dir + ): # Iterate over files in `active_protocols` + if file.endswith(".csv") and file not in exclude: + search_str = file.split("_")[0] + protocol = search(search_str, file_dict) + if protocol: + file_dict[protocol]["csv"] = str( + os.path.abspath( + os.path.join( + root_dir, os.path.join(directory, file) + ) + ) + ) + if directory == "custom_labware": + active_dir = os.path.join(root, directory) + for file in os.listdir( + active_dir + ): # Iterate over files in `active_protocols` + if file.endswith(".json") and file not in exclude: + labware_defs.append( + Path( + os.path.abspath( + os.path.join( + root_dir, os.path.join(directory, file) + ) + ) + ) + ) + return (file_dict, labware_defs) if __name__ == "__main__": # Directory to search + global root_dir root_dir = "abr_testing/protocols" - + global exclude exclude = [ "__init__.py", + "helpers.py", "shared_vars_and_funcs.py", + "9_parameters.csv", ] - # Walk through the root directory and its subdirectories - for root, dirs, files in os.walk(root_dir): - for file in files: - if file.endswith(".py"): # If it's a Python file - if file in exclude: - continue - file_path = Path(os.path.join(root, file)) - print(f"Simulating protocol: {file_path.stem}") - run(file_path) + print("Simulating Protocols") + file_dict, labware_defs = get_files() + # print(file_dict) + run(file_dict, labware_defs) diff --git a/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py b/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py index 9d21109f37e..9075e6f3a59 100644 --- a/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py +++ b/abr-testing/abr_testing/protocol_simulation/simulation_metrics.py @@ -6,6 +6,7 @@ from opentrons.cli import analyze import json import argparse +import traceback from datetime import datetime from abr_testing.automation import google_sheets_tool from abr_testing.data_collection import read_robot_logs @@ -13,13 +14,39 @@ from abr_testing.tools import plate_reader +def build_parser() -> Any: + """Builds argument parser.""" + parser = argparse.ArgumentParser(description="Read run logs on google drive.") + parser.add_argument( + "storage_directory", + metavar="STORAGE_DIRECTORY", + type=str, + nargs=1, + help="Path to long term storage directory for run logs.", + ) + parser.add_argument( + "sheet_name", + metavar="SHEET_NAME", + type=str, + nargs=1, + help="Name of sheet to upload results to", + ) + parser.add_argument( + "protocol_file_path", + metavar="PROTOCOL_FILE_PATH", + type=str, + nargs="*", + help="Path to protocol file(s)", + ) + return parser + + def set_api_level(protocol_file_path: str) -> None: """Set API level for analysis.""" with open(protocol_file_path, "r") as file: file_contents = file.readlines() # Look for current'apiLevel:' for i, line in enumerate(file_contents): - print(line) if "apiLevel" in line: print(f"The current API level of this protocol is: {line}") change = ( @@ -27,12 +54,10 @@ def set_api_level(protocol_file_path: str) -> None: .strip() .upper() ) - if change == "Y": api_level = input("Protocol API Level to Simulate with: ") # Update new API level file_contents[i] = f"apiLevel: {api_level}\n" - print(f"Updated line: {file_contents[i]}") break with open(protocol_file_path, "w") as file: file.writelines(file_contents) @@ -242,6 +267,7 @@ def parse_results_volume( "Right Pipette Total Dispenses", "Gripper Pick Ups", "Gripper Pick Ups of opentrons_tough_pcr_auto_sealing_lid", + "Gripper Pick Ups of opentrons_tough_pcr_auto_sealing_lid", "Total Liquid Probes", "Average Liquid Probe Time (sec)", ] @@ -303,6 +329,7 @@ def parse_results_volume( total_time_row.append(str(end_time - start_time)) for metric in metrics: + print(f"Dictionary: {metric}\n\n") print(f"Dictionary: {metric}\n\n") for cmd in metric.keys(): values_row.append(str(metric[cmd])) @@ -326,18 +353,20 @@ def main( save: bool, storage_directory: str = os.curdir, google_sheet_name: str = "", - parameters: List[str] = [], + parameters: str = "", + extra_files: List[Path] = [], ) -> None: """Main module control.""" sys.exit = mock_exit # Replace sys.exit with the mock function - # Read file path from arguments - # protocol_file_path = Path(protocol_file_path_name) - protocol_name = protocol_file_path.stem - print("Simulating", protocol_name) + # Simulation run date file_date = datetime.now() file_date_formatted = file_date.strftime("%Y-%m-%d_%H-%M-%S") error_output = f"{storage_directory}\\test_debug" - # Run protocol simulation + protocol_name = protocol_file_path.stem + protocol_files = [protocol_file_path] + if extra_files != []: + protocol_files += extra_files + print("Simulating....") try: with Context(analyze) as ctx: if save: @@ -348,13 +377,12 @@ def main( json_file_output = open(json_file_path, "wb+") # log_output_file = f"{protocol_name}_log" if parameters: - print(f"Parameter: {parameters[0]}\n") csv_params = {} - csv_params["parameters_csv"] = parameters[0] + csv_params["parameters_csv"] = parameters rtp_json = json.dumps(csv_params) ctx.invoke( analyze, - files=[protocol_file_path], + files=protocol_files, rtp_files=rtp_json, json_output=json_file_output, human_json_output=None, @@ -366,7 +394,7 @@ def main( else: ctx.invoke( analyze, - files=[protocol_file_path], + files=protocol_files, json_output=json_file_output, human_json_output=None, log_output=error_output, @@ -377,11 +405,11 @@ def main( else: if parameters: csv_params = {} - csv_params["parameters_csv"] = parameters[0] + csv_params["parameters_csv"] = parameters rtp_json = json.dumps(csv_params) ctx.invoke( analyze, - files=[protocol_file_path], + files=protocol_files, rtp_files=rtp_json, json_output=None, human_json_output=None, @@ -392,16 +420,18 @@ def main( else: ctx.invoke( analyze, - files=[protocol_file_path], + files=protocol_files, json_output=None, human_json_output=None, log_output=error_output, log_level="ERROR", check=True, ) - + print("done!") except SystemExit as e: print(f"SystemExit caught with code: {e}") + if e != 0: + traceback.print_exc finally: # Reset sys.exit to the original behavior sys.exit = original_exit @@ -411,11 +441,12 @@ def main( if not errors: pass else: - print(errors) - sys.exit(1) + print(f"Error:\n{errors}") + raise except FileNotFoundError: print("error simulating ...") - sys.exit() + raise + open_file.close if save: try: credentials_path = os.path.join(storage_directory, "credentials.json") @@ -443,31 +474,46 @@ def main( google_sheet.write_to_row(row) +def check_params(protocol_path: str) -> str: + """Check if protocol requires supporting files.""" + print("checking for parameters") + with open(protocol_path, "r") as f: + lines = f.readlines() + file_as_str = "".join(lines) + if ( + "parameters.add_csv_file" in file_as_str + or "helpers.create_csv_parameter" in file_as_str + ): + params = "" + while not params: + name = Path(protocol_file_path).stem + params = input( + f"Protocol {name} needs a CSV parameter file. Please enter the path: " + ) + if os.path.exists(params): + return params + else: + params = "" + print("Invalid file path") + return "" + + +def get_extra_files(protocol_file_path: str) -> tuple[str, List[Path]]: + """Get supporting files for protocol simulation if needed.""" + params = check_params(protocol_file_path) + needs_files = input("Does your protocol utilize custom labware? (y/n): ") + labware_files = [] + if needs_files == "y": + num_labware = input("How many custom labware?: ") + for labware_num in range(int(num_labware)): + path = input("Enter custom labware definition: ") + labware_files.append(Path(path)) + return (params, labware_files) + + if __name__ == "__main__": CLEAN_PROTOCOL = True - parser = argparse.ArgumentParser(description="Read run logs on google drive.") - parser.add_argument( - "storage_directory", - metavar="STORAGE_DIRECTORY", - type=str, - nargs=1, - help="Path to long term storage directory for run logs.", - ) - parser.add_argument( - "sheet_name", - metavar="SHEET_NAME", - type=str, - nargs=1, - help="Name of sheet to upload results to", - ) - parser.add_argument( - "protocol_file_path", - metavar="PROTOCOL_FILE_PATH", - type=str, - nargs="*", - help="Path to protocol file", - ) - args = parser.parse_args() + args = build_parser().parse_args() storage_directory = args.storage_directory[0] sheet_name = args.sheet_name[0] protocol_file_path: str = args.protocol_file_path[0] @@ -484,6 +530,7 @@ def main( while not choice: choice = input( "Remove air_gap commands to ensure accurate results: (continue)? (Y/N): " + "Remove air_gap commands to ensure accurate results: (continue)? (Y/N): " ) if choice.upper() == "Y": SETUP = False @@ -500,20 +547,18 @@ def main( # Change api level if CLEAN_PROTOCOL: set_api_level(protocol_file_path) - if parameters: - main( - Path(protocol_file_path), - True, - storage_directory, - sheet_name, - parameters=parameters, - ) - else: + params, extra_files = get_extra_files(protocol_file_path) + try: main( protocol_file_path=Path(protocol_file_path), save=True, storage_directory=storage_directory, google_sheet_name=sheet_name, + parameters=params, + extra_files=extra_files, ) + except Exception as e: + traceback.print_exc() + sys.exit(str(e)) else: sys.exit(0) diff --git a/api/src/opentrons/protocol_api/_liquid.py b/api/src/opentrons/protocol_api/_liquid.py index 12c9a140ce3..75e2c6fb6f2 100644 --- a/api/src/opentrons/protocol_api/_liquid.py +++ b/api/src/opentrons/protocol_api/_liquid.py @@ -1,15 +1,13 @@ -from __future__ import annotations - from dataclasses import dataclass -from typing import Optional, Dict +from typing import Optional, Sequence from opentrons_shared_data.liquid_classes.liquid_class_definition import ( LiquidClassSchemaV1, -) - -from ._liquid_properties import ( - TransferProperties, - build_transfer_properties, + AspirateProperties, + SingleDispenseProperties, + MultiDispenseProperties, + ByPipetteSetting, + ByTipTypeSetting, ) @@ -31,29 +29,46 @@ class Liquid: display_color: Optional[str] +# TODO (spp, 2024-10-17): create PAPI-equivalent types for all the properties +# and have validation on value updates with user-facing error messages +@dataclass +class TransferProperties: + _aspirate: AspirateProperties + _dispense: SingleDispenseProperties + _multi_dispense: Optional[MultiDispenseProperties] + + @property + def aspirate(self) -> AspirateProperties: + """Aspirate properties.""" + return self._aspirate + + @property + def dispense(self) -> SingleDispenseProperties: + """Single dispense properties.""" + return self._dispense + + @property + def multi_dispense(self) -> Optional[MultiDispenseProperties]: + """Multi dispense properties.""" + return self._multi_dispense + + @dataclass class LiquidClass: """A data class that contains properties of a specific class of liquids.""" _name: str _display_name: str - _by_pipette_setting: Dict[str, Dict[str, TransferProperties]] + _by_pipette_setting: Sequence[ByPipetteSetting] @classmethod def create(cls, liquid_class_definition: LiquidClassSchemaV1) -> "LiquidClass": """Liquid class factory method.""" - by_pipette_settings: Dict[str, Dict[str, TransferProperties]] = {} - for by_pipette in liquid_class_definition.byPipette: - tip_settings: Dict[str, TransferProperties] = {} - for tip_type in by_pipette.byTipType: - tip_settings[tip_type.tiprack] = build_transfer_properties(tip_type) - by_pipette_settings[by_pipette.pipetteModel] = tip_settings - return cls( _name=liquid_class_definition.liquidClassName, _display_name=liquid_class_definition.displayName, - _by_pipette_setting=by_pipette_settings, + _by_pipette_setting=liquid_class_definition.byPipette, ) @property @@ -66,16 +81,26 @@ def display_name(self) -> str: def get_for(self, pipette: str, tiprack: str) -> TransferProperties: """Get liquid class transfer properties for the specified pipette and tip.""" - try: - settings_for_pipette = self._by_pipette_setting[pipette] - except KeyError: + settings_for_pipette: Sequence[ByPipetteSetting] = [ + pip_setting + for pip_setting in self._by_pipette_setting + if pip_setting.pipetteModel == pipette + ] + if len(settings_for_pipette) == 0: raise ValueError( f"No properties found for {pipette} in {self._name} liquid class" ) - try: - transfer_properties = settings_for_pipette[tiprack] - except KeyError: + settings_for_tip: Sequence[ByTipTypeSetting] = [ + tip_setting + for tip_setting in settings_for_pipette[0].byTipType + if tip_setting.tiprack == tiprack + ] + if len(settings_for_tip) == 0: raise ValueError( f"No properties found for {tiprack} in {self._name} liquid class" ) - return transfer_properties + return TransferProperties( + _aspirate=settings_for_tip[0].aspirate, + _dispense=settings_for_tip[0].singleDispense, + _multi_dispense=settings_for_tip[0].multiDispense, + ) diff --git a/api/src/opentrons/protocol_api/_liquid_properties.py b/api/src/opentrons/protocol_api/_liquid_properties.py deleted file mode 100644 index 8bd7aa6cfd8..00000000000 --- a/api/src/opentrons/protocol_api/_liquid_properties.py +++ /dev/null @@ -1,540 +0,0 @@ -from dataclasses import dataclass -from typing import Optional, Dict, Sequence - -from opentrons_shared_data.liquid_classes.liquid_class_definition import ( - AspirateProperties as SharedDataAspirateProperties, - SingleDispenseProperties as SharedDataSingleDispenseProperties, - MultiDispenseProperties as SharedDataMultiDispenseProperties, - DelayProperties as SharedDataDelayProperties, - TouchTipProperties as SharedDataTouchTipProperties, - MixProperties as SharedDataMixProperties, - BlowoutProperties as SharedDataBlowoutProperties, - ByTipTypeSetting as SharedByTipTypeSetting, - Submerge as SharedDataSubmerge, - RetractAspirate as SharedDataRetractAspirate, - RetractDispense as SharedDataRetractDispense, - BlowoutLocation, - PositionReference, - Coordinate, -) - -# TODO replace this with a class that can extrapolate given volumes to the correct float, -# also figure out how we want people to be able to set this -LiquidHandlingPropertyByVolume = Dict[str, float] - - -@dataclass -class DelayProperties: - - _enabled: bool - _duration: Optional[float] - - @property - def enabled(self) -> bool: - return self._enabled - - @enabled.setter - def enabled(self, enable: bool) -> None: - # TODO insert bool validation here - if enable and self._duration is None: - raise ValueError("duration must be set before enabling delay.") - self._enabled = enable - - @property - def duration(self) -> Optional[float]: - return self._duration - - @duration.setter - def duration(self, new_duration: float) -> None: - # TODO insert positive float validation here - self._duration = new_duration - - -@dataclass -class TouchTipProperties: - - _enabled: bool - _z_offset: Optional[float] - _mm_to_edge: Optional[float] - _speed: Optional[float] - - @property - def enabled(self) -> bool: - return self._enabled - - @enabled.setter - def enabled(self, enable: bool) -> None: - # TODO insert bool validation here - if enable and ( - self._z_offset is None or self._mm_to_edge is None or self._speed is None - ): - raise ValueError( - "z_offset, mm_to_edge and speed must be set before enabling touch tip." - ) - self._enabled = enable - - @property - def z_offset(self) -> Optional[float]: - return self._z_offset - - @z_offset.setter - def z_offset(self, new_offset: float) -> None: - # TODO validation for float - self._z_offset = new_offset - - @property - def mm_to_edge(self) -> Optional[float]: - return self._mm_to_edge - - @mm_to_edge.setter - def mm_to_edge(self, new_mm: float) -> None: - # TODO validation for float - self._z_offset = new_mm - - @property - def speed(self) -> Optional[float]: - return self._speed - - @speed.setter - def speed(self, new_speed: float) -> None: - # TODO insert positive float validation here - self._speed = new_speed - - -@dataclass -class MixProperties: - - _enabled: bool - _repetitions: Optional[int] - _volume: Optional[float] - - @property - def enabled(self) -> bool: - return self._enabled - - @enabled.setter - def enabled(self, enable: bool) -> None: - # TODO insert bool validation here - if enable and (self._repetitions is None or self._volume is None): - raise ValueError("repetitions and volume must be set before enabling mix.") - self._enabled = enable - - @property - def repetitions(self) -> Optional[int]: - return self._repetitions - - @repetitions.setter - def repetitions(self, new_repetitions: int) -> None: - # TODO validations for positive int - self._repetitions = new_repetitions - - @property - def volume(self) -> Optional[float]: - return self._volume - - @volume.setter - def volume(self, new_volume: float) -> None: - # TODO validations for volume float - self._volume = new_volume - - -@dataclass -class BlowoutProperties: - - _enabled: bool - _location: Optional[BlowoutLocation] - _flow_rate: Optional[float] - - @property - def enabled(self) -> bool: - return self._enabled - - @enabled.setter - def enabled(self, enable: bool) -> None: - # TODO insert bool validation here - if enable and (self._location is None or self._flow_rate is None): - raise ValueError( - "location and flow_rate must be set before enabling blowout." - ) - self._enabled = enable - - @property - def location(self) -> Optional[BlowoutLocation]: - return self._location - - @location.setter - def location(self, new_location: str) -> None: - # TODO blowout location validation - self._location = BlowoutLocation(new_location) - - @property - def flow_rate(self) -> Optional[float]: - return self._flow_rate - - @flow_rate.setter - def flow_rate(self, new_flow_rate: float) -> None: - # TODO validations for positive float - self._flow_rate = new_flow_rate - - -@dataclass -class SubmergeRetractCommon: - - _position_reference: PositionReference - _offset: Coordinate - _speed: float - _delay: DelayProperties - - @property - def position_reference(self) -> PositionReference: - return self._position_reference - - @position_reference.setter - def position_reference(self, new_position: str) -> None: - # TODO validation for position reference - self._position_reference = PositionReference(new_position) - - @property - def offset(self) -> Coordinate: - return self._offset - - @offset.setter - def offset(self, new_offset: Sequence[float]) -> None: - # TODO validate valid coordinates - self._offset = Coordinate(x=new_offset[0], y=new_offset[1], z=new_offset[2]) - - @property - def speed(self) -> float: - return self._speed - - @speed.setter - def speed(self, new_speed: float) -> None: - # TODO insert positive float validation here - self._speed = new_speed - - @property - def delay(self) -> DelayProperties: - return self._delay - - -@dataclass -class Submerge(SubmergeRetractCommon): - ... - - -@dataclass -class RetractAspirate(SubmergeRetractCommon): - - _air_gap_by_volume: LiquidHandlingPropertyByVolume - _touch_tip: TouchTipProperties - - @property - def air_gap_by_volume(self) -> LiquidHandlingPropertyByVolume: - return self._air_gap_by_volume - - @property - def touch_tip(self) -> TouchTipProperties: - return self._touch_tip - - -@dataclass -class RetractDispense(SubmergeRetractCommon): - - _air_gap_by_volume: LiquidHandlingPropertyByVolume - _touch_tip: TouchTipProperties - _blowout: BlowoutProperties - - @property - def air_gap_by_volume(self) -> LiquidHandlingPropertyByVolume: - return self._air_gap_by_volume - - @property - def touch_tip(self) -> TouchTipProperties: - return self._touch_tip - - @property - def blowout(self) -> BlowoutProperties: - return self._blowout - - -@dataclass -class BaseLiquidHandlingProperties: - - _submerge: Submerge - _position_reference: PositionReference - _offset: Coordinate - _flow_rate_by_volume: LiquidHandlingPropertyByVolume - _delay: DelayProperties - - @property - def submerge(self) -> Submerge: - return self._submerge - - @property - def position_reference(self) -> PositionReference: - return self._position_reference - - @position_reference.setter - def position_reference(self, new_position: str) -> None: - # TODO validation for position reference - self._position_reference = PositionReference(new_position) - - @property - def offset(self) -> Coordinate: - return self._offset - - @offset.setter - def offset(self, new_offset: Sequence[float]) -> None: - # TODO validate valid coordinates - self._offset = Coordinate(x=new_offset[0], y=new_offset[1], z=new_offset[2]) - - @property - def flow_rate_by_volume(self) -> LiquidHandlingPropertyByVolume: - return self._flow_rate_by_volume - - @property - def delay(self) -> DelayProperties: - return self._delay - - -@dataclass -class AspirateProperties(BaseLiquidHandlingProperties): - - _retract: RetractAspirate - _pre_wet: bool - _mix: MixProperties - - @property - def pre_wet(self) -> bool: - return self._pre_wet - - @pre_wet.setter - def pre_wet(self, new_setting: bool) -> None: - # TODO boolean validation - self._pre_wet = new_setting - - @property - def retract(self) -> RetractAspirate: - return self._retract - - @property - def mix(self) -> MixProperties: - return self._mix - - -@dataclass -class SingleDispenseProperties(BaseLiquidHandlingProperties): - - _retract: RetractDispense - _push_out_by_volume: LiquidHandlingPropertyByVolume - _mix: MixProperties - - @property - def push_out_by_volume(self) -> LiquidHandlingPropertyByVolume: - return self._push_out_by_volume - - @property - def retract(self) -> RetractDispense: - return self._retract - - @property - def mix(self) -> MixProperties: - return self._mix - - -@dataclass -class MultiDispenseProperties(BaseLiquidHandlingProperties): - - _retract: RetractDispense - _conditioning_by_volume: LiquidHandlingPropertyByVolume - _disposal_by_volume: LiquidHandlingPropertyByVolume - - @property - def retract(self) -> RetractDispense: - return self._retract - - @property - def conditioning_by_volume(self) -> LiquidHandlingPropertyByVolume: - return self._conditioning_by_volume - - @property - def disposal_by_volume(self) -> LiquidHandlingPropertyByVolume: - return self._disposal_by_volume - - -# TODO (spp, 2024-10-17): create PAPI-equivalent types for all the properties -# and have validation on value updates with user-facing error messages -@dataclass -class TransferProperties: - _aspirate: AspirateProperties - _dispense: SingleDispenseProperties - _multi_dispense: Optional[MultiDispenseProperties] - - @property - def aspirate(self) -> AspirateProperties: - """Aspirate properties.""" - return self._aspirate - - @property - def dispense(self) -> SingleDispenseProperties: - """Single dispense properties.""" - return self._dispense - - @property - def multi_dispense(self) -> Optional[MultiDispenseProperties]: - """Multi dispense properties.""" - return self._multi_dispense - - -def _build_delay_properties( - delay_properties: SharedDataDelayProperties, -) -> DelayProperties: - if delay_properties.params is not None: - duration = delay_properties.params.duration - else: - duration = None - return DelayProperties(_enabled=delay_properties.enable, _duration=duration) - - -def _build_touch_tip_properties( - touch_tip_properties: SharedDataTouchTipProperties, -) -> TouchTipProperties: - if touch_tip_properties.params is not None: - z_offset = touch_tip_properties.params.zOffset - mm_to_edge = touch_tip_properties.params.mmToEdge - speed = touch_tip_properties.params.speed - else: - z_offset = None - mm_to_edge = None - speed = None - return TouchTipProperties( - _enabled=touch_tip_properties.enable, - _z_offset=z_offset, - _mm_to_edge=mm_to_edge, - _speed=speed, - ) - - -def _build_mix_properties( - mix_properties: SharedDataMixProperties, -) -> MixProperties: - if mix_properties.params is not None: - repetitions = mix_properties.params.repetitions - volume = mix_properties.params.volume - else: - repetitions = None - volume = None - return MixProperties( - _enabled=mix_properties.enable, _repetitions=repetitions, _volume=volume - ) - - -def _build_blowout_properties( - blowout_properties: SharedDataBlowoutProperties, -) -> BlowoutProperties: - if blowout_properties.params is not None: - location = blowout_properties.params.location - flow_rate = blowout_properties.params.flowRate - else: - location = None - flow_rate = None - return BlowoutProperties( - _enabled=blowout_properties.enable, _location=location, _flow_rate=flow_rate - ) - - -def _build_submerge( - submerge_properties: SharedDataSubmerge, -) -> Submerge: - return Submerge( - _position_reference=submerge_properties.positionReference, - _offset=submerge_properties.offset, - _speed=submerge_properties.speed, - _delay=_build_delay_properties(submerge_properties.delay), - ) - - -def _build_retract_aspirate( - retract_aspirate: SharedDataRetractAspirate, -) -> RetractAspirate: - return RetractAspirate( - _position_reference=retract_aspirate.positionReference, - _offset=retract_aspirate.offset, - _speed=retract_aspirate.speed, - _air_gap_by_volume=retract_aspirate.airGapByVolume, - _touch_tip=_build_touch_tip_properties(retract_aspirate.touchTip), - _delay=_build_delay_properties(retract_aspirate.delay), - ) - - -def _build_retract_dispense( - retract_dispense: SharedDataRetractDispense, -) -> RetractDispense: - return RetractDispense( - _position_reference=retract_dispense.positionReference, - _offset=retract_dispense.offset, - _speed=retract_dispense.speed, - _air_gap_by_volume=retract_dispense.airGapByVolume, - _blowout=_build_blowout_properties(retract_dispense.blowout), - _touch_tip=_build_touch_tip_properties(retract_dispense.touchTip), - _delay=_build_delay_properties(retract_dispense.delay), - ) - - -def build_aspirate_properties( - aspirate_properties: SharedDataAspirateProperties, -) -> AspirateProperties: - return AspirateProperties( - _submerge=_build_submerge(aspirate_properties.submerge), - _retract=_build_retract_aspirate(aspirate_properties.retract), - _position_reference=aspirate_properties.positionReference, - _offset=aspirate_properties.offset, - _flow_rate_by_volume=aspirate_properties.flowRateByVolume, - _pre_wet=aspirate_properties.preWet, - _mix=_build_mix_properties(aspirate_properties.mix), - _delay=_build_delay_properties(aspirate_properties.delay), - ) - - -def build_single_dispense_properties( - single_dispense_properties: SharedDataSingleDispenseProperties, -) -> SingleDispenseProperties: - return SingleDispenseProperties( - _submerge=_build_submerge(single_dispense_properties.submerge), - _retract=_build_retract_dispense(single_dispense_properties.retract), - _position_reference=single_dispense_properties.positionReference, - _offset=single_dispense_properties.offset, - _flow_rate_by_volume=single_dispense_properties.flowRateByVolume, - _mix=_build_mix_properties(single_dispense_properties.mix), - _push_out_by_volume=single_dispense_properties.pushOutByVolume, - _delay=_build_delay_properties(single_dispense_properties.delay), - ) - - -def build_multi_dispense_properties( - multi_dispense_properties: Optional[SharedDataMultiDispenseProperties], -) -> Optional[MultiDispenseProperties]: - if multi_dispense_properties is None: - return None - return MultiDispenseProperties( - _submerge=_build_submerge(multi_dispense_properties.submerge), - _retract=_build_retract_dispense(multi_dispense_properties.retract), - _position_reference=multi_dispense_properties.positionReference, - _offset=multi_dispense_properties.offset, - _flow_rate_by_volume=multi_dispense_properties.flowRateByVolume, - _conditioning_by_volume=multi_dispense_properties.conditioningByVolume, - _disposal_by_volume=multi_dispense_properties.disposalByVolume, - _delay=_build_delay_properties(multi_dispense_properties.delay), - ) - - -def build_transfer_properties( - by_tip_type_setting: SharedByTipTypeSetting, -) -> TransferProperties: - return TransferProperties( - _aspirate=build_aspirate_properties(by_tip_type_setting.aspirate), - _dispense=build_single_dispense_properties(by_tip_type_setting.singleDispense), - _multi_dispense=build_multi_dispense_properties( - by_tip_type_setting.multiDispense - ), - ) diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index dc174988069..825d45bfded 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -104,19 +104,6 @@ def set_default_speed(self, speed: float) -> None: pipette_id=self._pipette_id, speed=speed ) - def air_gap_in_place(self, volume: float, flow_rate: float) -> None: - """Aspirate a given volume of air from the current location of the pipette. - - Args: - volume: The volume of air to aspirate, in microliters. - folw_rate: The flow rate of air into the pipette, in microliters/s - """ - self._engine_client.execute_command( - cmd.AirGapInPlaceParams( - pipetteId=self._pipette_id, volume=volume, flowRate=flow_rate - ) - ) - def aspirate( self, location: Location, diff --git a/api/src/opentrons/protocol_api/core/engine/well.py b/api/src/opentrons/protocol_api/core/engine/well.py index dba1dc6c840..6743a8a39c5 100644 --- a/api/src/opentrons/protocol_api/core/engine/well.py +++ b/api/src/opentrons/protocol_api/core/engine/well.py @@ -130,10 +130,7 @@ def load_liquid( liquid: Liquid, volume: float, ) -> None: - """Load liquid into a well. - - If the well is known to be empty, use ``load_empty()`` instead of calling this with a 0.0 volume. - """ + """Load liquid into a well.""" self._engine_client.execute_command( cmd.LoadLiquidParams( labwareId=self._labware_id, @@ -142,22 +139,6 @@ def load_liquid( ) ) - def load_empty( - self, - ) -> None: - """Inform the system that a well is known to be empty. - - This should be done early in the protocol, at the same time as a load_liquid command might - be used. - """ - self._engine_client.execute_command( - cmd.LoadLiquidParams( - labwareId=self._labware_id, - liquidId="EMPTY", - volumeByWell={self._name: 0.0}, - ) - ) - def from_center_cartesian(self, x: float, y: float, z: float) -> Point: """Gets point in deck coordinates based on percentage of the radius of each axis.""" well_size = self._engine_client.state.labware.get_well_size( diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index f88633a7a6d..7d1816e1044 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -24,14 +24,6 @@ def get_default_speed(self) -> float: def set_default_speed(self, speed: float) -> None: ... - @abstractmethod - def air_gap_in_place(self, volume: float, flow_rate: float) -> None: - """Aspirate a given volume of air from the current location of the pipette. - Args: - volume: The volume of air to aspirate, in microliters. - flow_rate: The flow rate of air into the pipette, in microliters. - """ - @abstractmethod def aspirate( self, 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 c112fc32abc..ed1e0d607c9 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 @@ -72,9 +72,6 @@ def set_default_speed(self, speed: float) -> None: """Sets the speed at which the robot's gantry moves.""" self._default_speed = speed - def air_gap_in_place(self, volume: float, flow_rate: float) -> None: - assert False, "Air gap tracking only available in API version 2.22 and later" - def aspirate( self, location: types.Location, diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py index 891f0f1b681..a88dd2eee80 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py @@ -114,10 +114,6 @@ def load_liquid( """Load liquid into a well.""" raise APIVersionError(api_element="Loading a liquid") - def load_empty(self) -> None: - """Mark a well as empty.""" - assert False, "load_empty only supported on engine core" - def from_center_cartesian(self, x: float, y: float, z: float) -> Point: """Gets point in deck coordinates based on percentage of the radius of each axis.""" return self._geometry.from_center_cartesian(x, y, z) 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 f02d1e66fd1..55bde6c0a75 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 @@ -83,9 +83,6 @@ def get_default_speed(self) -> float: def set_default_speed(self, speed: float) -> None: self._default_speed = speed - def air_gap_in_place(self, volume: float, flow_rate: float) -> None: - assert False, "Air gap tracking only available in API version 2.22 and later" - def aspirate( self, location: types.Location, diff --git a/api/src/opentrons/protocol_api/core/well.py b/api/src/opentrons/protocol_api/core/well.py index 24489bb04e7..bd58963a59c 100644 --- a/api/src/opentrons/protocol_api/core/well.py +++ b/api/src/opentrons/protocol_api/core/well.py @@ -79,10 +79,6 @@ def load_liquid( ) -> None: """Load liquid into a well.""" - @abstractmethod - def load_empty(self) -> None: - """Mark a well as containing no liquid.""" - @abstractmethod def from_center_cartesian(self, x: float, y: float, z: float) -> Point: """Gets point in deck coordinates based on percentage of the radius of each axis.""" diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 93c485f8087..880626b53c9 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -60,8 +60,6 @@ """The version after which offsets for deck configured trash containers and changes to alternating tip drop behavior were introduced.""" _PARTIAL_NOZZLE_CONFIGURATION_SINGLE_ROW_PARTIAL_COLUMN_ADDED_IN = APIVersion(2, 20) """The version after which partial nozzle configurations of single, row, and partial column layouts became available.""" -_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.""" class InstrumentContext(publisher.CommandPublisher): @@ -755,12 +753,7 @@ def air_gap( ``pipette.air_gap(height=2)``. If you call ``air_gap`` with a single, unnamed argument, it will always be interpreted as a volume. - .. note:: - Before API version 2.22, this function was implemented as an aspirate, and - dispensing into a well would add the air gap volume to the liquid tracked in - the well. At or above API version 2.22, air gap volume is not counted as liquid - when dispensing into a well. """ if not self._core.has_tip(): raise UnexpectedTipRemovalError("air_gap", self.name, self.mount) @@ -772,12 +765,7 @@ def air_gap( raise RuntimeError("No previous Well cached to perform air gap") target = loc.labware.as_well().top(height) self.move_to(target, publish=False) - if self.api_version >= _AIR_GAP_TRACKING_ADDED_IN: - c_vol = self._core.get_available_volume() if volume is None else volume - flow_rate = self._core.get_aspirate_flow_rate() - self._core.air_gap_in_place(c_vol, flow_rate) - else: - self.aspirate(volume) + self.aspirate(volume) return self @publisher.publish(command=cmds.return_tip) diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index 825cc19668a..0e8a17d07d3 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -280,20 +280,12 @@ def load_liquid(self, liquid: Liquid, volume: float) -> None: :param Liquid liquid: The liquid to load into the well. :param float volume: The volume of liquid to load, in µL. - - .. note:: - In API version 2.22 and later, use :py:meth:`~.Well.load_empty()` to mark a well as empty at the beginning of a protocol, rather than using this method with ``volume=0``. """ self._core.load_liquid( liquid=liquid, volume=volume, ) - @requires_version(2, 22) - def load_empty(self) -> None: - """Mark a well as empty.""" - self._core.load_empty() - def _from_center_cartesian(self, x: float, y: float, z: float) -> Point: """ Private version of from_center_cartesian. Present only for backward diff --git a/api/src/opentrons/protocol_engine/commands/__init__.py b/api/src/opentrons/protocol_engine/commands/__init__.py index 69a3d0c12c1..649bb4b6507 100644 --- a/api/src/opentrons/protocol_engine/commands/__init__.py +++ b/api/src/opentrons/protocol_engine/commands/__init__.py @@ -41,14 +41,6 @@ CommandDefinedErrorData, ) -from .air_gap_in_place import ( - AirGapInPlace, - AirGapInPlaceParams, - AirGapInPlaceCreate, - AirGapInPlaceResult, - AirGapInPlaceCommandType, -) - from .aspirate import ( Aspirate, AspirateParams, @@ -363,12 +355,6 @@ "hash_protocol_command_params", # command schema generation "generate_command_schema", - # air gap command models - "AirGapInPlace", - "AirGapInPlaceCreate", - "AirGapInPlaceParams", - "AirGapInPlaceResult", - "AirGapInPlaceCommandType", # aspirate command models "Aspirate", "AspirateCreate", diff --git a/api/src/opentrons/protocol_engine/commands/air_gap_in_place.py b/api/src/opentrons/protocol_engine/commands/air_gap_in_place.py deleted file mode 100644 index 461a446f3e4..00000000000 --- a/api/src/opentrons/protocol_engine/commands/air_gap_in_place.py +++ /dev/null @@ -1,160 +0,0 @@ -"""AirGap in place command request, result, and implementation models.""" - -from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Type, Union -from typing_extensions import Literal - -from opentrons_shared_data.errors.exceptions import PipetteOverpressureError - -from opentrons.hardware_control import HardwareControlAPI - -from .pipetting_common import ( - PipetteIdMixin, - AspirateVolumeMixin, - FlowRateMixin, - BaseLiquidHandlingResult, - OverpressureError, -) -from .command import ( - AbstractCommandImpl, - BaseCommand, - BaseCommandCreate, - SuccessData, - DefinedErrorData, -) -from ..errors.error_occurrence import ErrorOccurrence -from ..errors.exceptions import PipetteNotReadyToAspirateError -from ..state.update_types import StateUpdate -from ..types import AspiratedFluid, FluidKind - -if TYPE_CHECKING: - from ..execution import PipettingHandler, GantryMover - from ..resources import ModelUtils - from ..state.state import StateView - from ..notes import CommandNoteAdder - -AirGapInPlaceCommandType = Literal["airGapInPlace"] - - -class AirGapInPlaceParams(PipetteIdMixin, AspirateVolumeMixin, FlowRateMixin): - """Payload required to air gap in place.""" - - pass - - -class AirGapInPlaceResult(BaseLiquidHandlingResult): - """Result data from the execution of a AirGapInPlace command.""" - - pass - - -_ExecuteReturn = Union[ - SuccessData[AirGapInPlaceResult], - DefinedErrorData[OverpressureError], -] - - -class AirGapInPlaceImplementation( - AbstractCommandImpl[AirGapInPlaceParams, _ExecuteReturn] -): - """AirGapInPlace command implementation.""" - - def __init__( - self, - pipetting: PipettingHandler, - hardware_api: HardwareControlAPI, - state_view: StateView, - command_note_adder: CommandNoteAdder, - model_utils: ModelUtils, - gantry_mover: GantryMover, - **kwargs: object, - ) -> None: - self._pipetting = pipetting - self._state_view = state_view - self._hardware_api = hardware_api - self._command_note_adder = command_note_adder - self._model_utils = model_utils - self._gantry_mover = gantry_mover - - async def execute(self, params: AirGapInPlaceParams) -> _ExecuteReturn: - """Air gap without moving the pipette. - - Raises: - TipNotAttachedError: if no tip is attached to the pipette. - PipetteNotReadyToAirGapError: pipette plunger is not ready. - """ - ready_to_aspirate = self._pipetting.get_is_ready_to_aspirate( - pipette_id=params.pipetteId, - ) - - if not ready_to_aspirate: - raise PipetteNotReadyToAspirateError( - "Pipette cannot air gap in place because of a previous blow out." - " The first aspirate following a blow-out must be from a specific well" - " so the plunger can be reset in a known safe position." - ) - - state_update = StateUpdate() - - try: - current_position = await self._gantry_mover.get_position(params.pipetteId) - volume = await self._pipetting.aspirate_in_place( - pipette_id=params.pipetteId, - volume=params.volume, - flow_rate=params.flowRate, - command_note_adder=self._command_note_adder, - ) - except PipetteOverpressureError as e: - return DefinedErrorData( - public=OverpressureError( - id=self._model_utils.generate_id(), - createdAt=self._model_utils.get_timestamp(), - wrappedErrors=[ - ErrorOccurrence.from_failed( - id=self._model_utils.generate_id(), - createdAt=self._model_utils.get_timestamp(), - error=e, - ) - ], - errorInfo=( - { - "retryLocation": ( - current_position.x, - current_position.y, - current_position.z, - ) - } - ), - ), - state_update=state_update, - ) - else: - state_update.set_fluid_aspirated( - pipette_id=params.pipetteId, - fluid=AspiratedFluid(kind=FluidKind.AIR, volume=volume), - ) - return SuccessData( - public=AirGapInPlaceResult(volume=volume), - state_update=state_update, - ) - - -class AirGapInPlace( - BaseCommand[AirGapInPlaceParams, AirGapInPlaceResult, OverpressureError] -): - """AirGapInPlace command model.""" - - commandType: AirGapInPlaceCommandType = "airGapInPlace" - params: AirGapInPlaceParams - result: Optional[AirGapInPlaceResult] - - _ImplementationCls: Type[AirGapInPlaceImplementation] = AirGapInPlaceImplementation - - -class AirGapInPlaceCreate(BaseCommandCreate[AirGapInPlaceParams]): - """AirGapInPlace command request model.""" - - commandType: AirGapInPlaceCommandType = "airGapInPlace" - params: AirGapInPlaceParams - - _CommandCls: Type[AirGapInPlace] = AirGapInPlace diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py index b5541c79792..00d57a93e9a 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate.py @@ -25,14 +25,7 @@ from opentrons.hardware_control import HardwareControlAPI from ..state.update_types import StateUpdate, CLEAR -from ..types import ( - WellLocation, - WellOrigin, - CurrentWell, - DeckPoint, - AspiratedFluid, - FluidKind, -) +from ..types import WellLocation, WellOrigin, CurrentWell, DeckPoint if TYPE_CHECKING: from ..execution import MovementHandler, PipettingHandler @@ -148,7 +141,6 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: well_name=well_name, volume_added=CLEAR, ) - state_update.set_fluid_unknown(pipette_id=params.pipetteId) return DefinedErrorData( public=OverpressureError( id=self._model_utils.generate_id(), @@ -170,10 +162,6 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: well_name=well_name, volume_added=-volume_aspirated, ) - state_update.set_fluid_aspirated( - pipette_id=params.pipetteId, - fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=volume_aspirated), - ) return SuccessData( public=AspirateResult( volume=volume_aspirated, diff --git a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py index f25b6c24bbb..4c7ab2cc01c 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate_in_place.py @@ -25,7 +25,7 @@ from ..errors.error_occurrence import ErrorOccurrence from ..errors.exceptions import PipetteNotReadyToAspirateError from ..state.update_types import StateUpdate, CLEAR -from ..types import CurrentWell, AspiratedFluid, FluidKind +from ..types import CurrentWell if TYPE_CHECKING: from ..execution import PipettingHandler, GantryMover @@ -115,7 +115,6 @@ async def execute(self, params: AspirateInPlaceParams) -> _ExecuteReturn: well_name=current_location.well_name, volume_added=CLEAR, ) - state_update.set_fluid_unknown(pipette_id=params.pipetteId) return DefinedErrorData( public=OverpressureError( id=self._model_utils.generate_id(), @@ -140,10 +139,6 @@ async def execute(self, params: AspirateInPlaceParams) -> _ExecuteReturn: state_update=state_update, ) else: - state_update.set_fluid_aspirated( - pipette_id=params.pipetteId, - fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=volume), - ) if ( isinstance(current_location, CurrentWell) and current_location.pipette_id == params.pipetteId @@ -153,7 +148,6 @@ async def execute(self, params: AspirateInPlaceParams) -> _ExecuteReturn: well_name=current_location.well_name, volume_added=-volume, ) - return SuccessData( public=AspirateInPlaceResult(volume=volume), state_update=state_update, diff --git a/api/src/opentrons/protocol_engine/commands/blow_out.py b/api/src/opentrons/protocol_engine/commands/blow_out.py index c450fa894ed..e13378b5541 100644 --- a/api/src/opentrons/protocol_engine/commands/blow_out.py +++ b/api/src/opentrons/protocol_engine/commands/blow_out.py @@ -93,7 +93,6 @@ async def execute(self, params: BlowOutParams) -> _ExecuteReturn: pipette_id=params.pipetteId, flow_rate=params.flowRate ) except PipetteOverpressureError as e: - state_update.set_fluid_unknown(pipette_id=params.pipetteId) return DefinedErrorData( public=OverpressureError( id=self._model_utils.generate_id(), @@ -113,10 +112,8 @@ async def execute(self, params: BlowOutParams) -> _ExecuteReturn: ) }, ), - state_update=state_update, ) else: - state_update.set_fluid_empty(pipette_id=params.pipetteId) return SuccessData( public=BlowOutResult(position=deck_point), state_update=state_update, diff --git a/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py b/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py index 04a38b8915c..0b9aaec77b2 100644 --- a/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/blow_out_in_place.py @@ -19,7 +19,6 @@ SuccessData, ) from ..errors.error_occurrence import ErrorOccurrence -from ..state import update_types from opentrons.hardware_control import HardwareControlAPI @@ -73,14 +72,12 @@ def __init__( async def execute(self, params: BlowOutInPlaceParams) -> _ExecuteReturn: """Blow-out without moving the pipette.""" - state_update = update_types.StateUpdate() try: current_position = await self._gantry_mover.get_position(params.pipetteId) await self._pipetting.blow_out_in_place( pipette_id=params.pipetteId, flow_rate=params.flowRate ) except PipetteOverpressureError as e: - state_update.set_fluid_unknown(pipette_id=params.pipetteId) return DefinedErrorData( public=OverpressureError( id=self._model_utils.generate_id(), @@ -100,11 +97,11 @@ async def execute(self, params: BlowOutInPlaceParams) -> _ExecuteReturn: ) }, ), - state_update=state_update, ) else: - state_update.set_fluid_empty(pipette_id=params.pipetteId) - return SuccessData(public=BlowOutInPlaceResult(), state_update=state_update) + return SuccessData( + public=BlowOutInPlaceResult(), + ) class BlowOutInPlace( diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py index c59c538ddd3..0e0a4cf3112 100644 --- a/api/src/opentrons/protocol_engine/commands/command_unions.py +++ b/api/src/opentrons/protocol_engine/commands/command_unions.py @@ -31,14 +31,6 @@ SetRailLightsResult, ) -from .air_gap_in_place import ( - AirGapInPlace, - AirGapInPlaceParams, - AirGapInPlaceCreate, - AirGapInPlaceResult, - AirGapInPlaceCommandType, -) - from .aspirate import ( Aspirate, AspirateParams, @@ -328,7 +320,6 @@ Command = Annotated[ Union[ - AirGapInPlace, Aspirate, AspirateInPlace, Comment, @@ -407,7 +398,6 @@ ] CommandParams = Union[ - AirGapInPlaceParams, AspirateParams, AspirateInPlaceParams, CommentParams, @@ -484,7 +474,6 @@ ] CommandType = Union[ - AirGapInPlaceCommandType, AspirateCommandType, AspirateInPlaceCommandType, CommentCommandType, @@ -562,7 +551,6 @@ CommandCreate = Annotated[ Union[ - AirGapInPlaceCreate, AspirateCreate, AspirateInPlaceCreate, CommentCreate, @@ -641,7 +629,6 @@ ] CommandResult = Union[ - AirGapInPlaceResult, AspirateResult, AspirateInPlaceResult, CommentResult, diff --git a/api/src/opentrons/protocol_engine/commands/dispense.py b/api/src/opentrons/protocol_engine/commands/dispense.py index 603fa7396a7..a7fee20c762 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense.py +++ b/api/src/opentrons/protocol_engine/commands/dispense.py @@ -112,7 +112,6 @@ async def execute(self, params: DispenseParams) -> _ExecuteReturn: well_name=well_name, volume_added=CLEAR, ) - state_update.set_fluid_unknown(pipette_id=params.pipetteId) return DefinedErrorData( public=OverpressureError( id=self._model_utils.generate_id(), @@ -129,17 +128,11 @@ async def execute(self, params: DispenseParams) -> _ExecuteReturn: state_update=state_update, ) else: - volume_added = ( - self._state_view.pipettes.get_liquid_dispensed_by_ejecting_volume( - pipette_id=params.pipetteId, volume=volume - ) - ) state_update.set_liquid_operated( labware_id=labware_id, well_name=well_name, - volume_added=volume_added if volume_added is not None else CLEAR, + volume_added=volume, ) - state_update.set_fluid_ejected(pipette_id=params.pipetteId, volume=volume) return SuccessData( public=DispenseResult(volume=volume, position=deck_point), state_update=state_update, diff --git a/api/src/opentrons/protocol_engine/commands/dispense_in_place.py b/api/src/opentrons/protocol_engine/commands/dispense_in_place.py index ee7cae42dc1..7df9471b038 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/dispense_in_place.py @@ -94,7 +94,6 @@ async def execute(self, params: DispenseInPlaceParams) -> _ExecuteReturn: well_name=current_location.well_name, volume_added=CLEAR, ) - state_update.set_fluid_unknown(pipette_id=params.pipetteId) return DefinedErrorData( public=OverpressureError( id=self._model_utils.generate_id(), @@ -119,20 +118,14 @@ async def execute(self, params: DispenseInPlaceParams) -> _ExecuteReturn: state_update=state_update, ) else: - state_update.set_fluid_ejected(pipette_id=params.pipetteId, volume=volume) if ( isinstance(current_location, CurrentWell) and current_location.pipette_id == params.pipetteId ): - volume_added = ( - self._state_view.pipettes.get_liquid_dispensed_by_ejecting_volume( - pipette_id=params.pipetteId, volume=volume - ) - ) state_update.set_liquid_operated( labware_id=current_location.labware_id, well_name=current_location.well_name, - volume_added=volume_added if volume_added is not None else CLEAR, + volume_added=volume, ) return SuccessData( public=DispenseInPlaceResult(volume=volume), diff --git a/api/src/opentrons/protocol_engine/commands/drop_tip.py b/api/src/opentrons/protocol_engine/commands/drop_tip.py index ad0954c5a32..81a34a5ad39 100644 --- a/api/src/opentrons/protocol_engine/commands/drop_tip.py +++ b/api/src/opentrons/protocol_engine/commands/drop_tip.py @@ -150,14 +150,12 @@ async def execute(self, params: DropTipParams) -> _ExecuteReturn: state_update_if_false_positive.update_pipette_tip_state( pipette_id=params.pipetteId, tip_geometry=None ) - state_update.set_fluid_unknown(pipette_id=pipette_id) return DefinedErrorData( public=error, state_update=state_update, state_update_if_false_positive=state_update_if_false_positive, ) else: - state_update.set_fluid_unknown(pipette_id=pipette_id) state_update.update_pipette_tip_state( pipette_id=params.pipetteId, tip_geometry=None ) diff --git a/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py b/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py index 0f98b32ff58..aa40384ac6a 100644 --- a/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/drop_tip_in_place.py @@ -65,6 +65,7 @@ def __init__( async def execute(self, params: DropTipInPlaceParams) -> _ExecuteReturn: """Drop a tip using the requested pipette.""" state_update = update_types.StateUpdate() + try: await self._tip_handler.drop_tip( pipette_id=params.pipetteId, home_after=params.homeAfter @@ -74,7 +75,6 @@ async def execute(self, params: DropTipInPlaceParams) -> _ExecuteReturn: state_update_if_false_positive.update_pipette_tip_state( pipette_id=params.pipetteId, tip_geometry=None ) - state_update.set_fluid_unknown(pipette_id=params.pipetteId) error = TipPhysicallyAttachedError( id=self._model_utils.generate_id(), createdAt=self._model_utils.get_timestamp(), @@ -92,7 +92,6 @@ async def execute(self, params: DropTipInPlaceParams) -> _ExecuteReturn: state_update_if_false_positive=state_update_if_false_positive, ) else: - state_update.set_fluid_unknown(pipette_id=params.pipetteId) state_update.update_pipette_tip_state( pipette_id=params.pipetteId, tip_geometry=None ) diff --git a/api/src/opentrons/protocol_engine/commands/load_liquid.py b/api/src/opentrons/protocol_engine/commands/load_liquid.py index f6aa037fa01..5dd4737410e 100644 --- a/api/src/opentrons/protocol_engine/commands/load_liquid.py +++ b/api/src/opentrons/protocol_engine/commands/load_liquid.py @@ -5,8 +5,6 @@ from typing_extensions import Literal from opentrons.protocol_engine.state.update_types import StateUpdate -from opentrons.protocol_engine.types import LiquidId -from opentrons.protocol_engine.errors import InvalidLiquidError from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ..errors.error_occurrence import ErrorOccurrence @@ -21,9 +19,9 @@ class LoadLiquidParams(BaseModel): """Payload required to load a liquid into a well.""" - liquidId: LiquidId = Field( + liquidId: str = Field( ..., - description="Unique identifier of the liquid to load. If this is the sentinel value EMPTY, all values of volumeByWell must be 0.", + description="Unique identifier of the liquid to load.", ) labwareId: str = Field( ..., @@ -31,7 +29,7 @@ class LoadLiquidParams(BaseModel): ) volumeByWell: Dict[str, float] = Field( ..., - description="Volume of liquid, in µL, loaded into each well by name, in this labware. If the liquid id is the sentinel value EMPTY, all volumes must be 0.", + description="Volume of liquid, in µL, loaded into each well by name, in this labware.", ) @@ -59,12 +57,6 @@ async def execute(self, params: LoadLiquidParams) -> SuccessData[LoadLiquidResul self._state_view.labware.validate_liquid_allowed_in_labware( labware_id=params.labwareId, wells=params.volumeByWell ) - if params.liquidId == "EMPTY": - for well_name, volume in params.volumeByWell.items(): - if volume != 0.0: - raise InvalidLiquidError( - 'loadLiquid commands that specify the special liquid "EMPTY" must set volume to be 0.0, but the volume for {well_name} is {volume}' - ) state_update = StateUpdate() state_update.set_liquid_loaded( diff --git a/api/src/opentrons/protocol_engine/commands/load_pipette.py b/api/src/opentrons/protocol_engine/commands/load_pipette.py index 6d8d74b4fa2..7d09bf33028 100644 --- a/api/src/opentrons/protocol_engine/commands/load_pipette.py +++ b/api/src/opentrons/protocol_engine/commands/load_pipette.py @@ -127,7 +127,6 @@ async def execute( serial_number=loaded_pipette.serial_number, config=loaded_pipette.static_config, ) - state_update.set_fluid_unknown(pipette_id=loaded_pipette.pipette_id) return SuccessData( public=LoadPipetteResult(pipetteId=loaded_pipette.pipette_id), diff --git a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py index 86967c6502f..898929566fe 100644 --- a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py +++ b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py @@ -146,11 +146,9 @@ async def execute( pipette_id=pipette_id, tip_geometry=e.tip_geometry, ) - state_update_if_false_positive.set_fluid_empty(pipette_id=pipette_id) state_update.mark_tips_as_used( pipette_id=pipette_id, labware_id=labware_id, well_name=well_name ) - state_update.set_fluid_unknown(pipette_id=pipette_id) return DefinedErrorData( public=TipPhysicallyMissingError( id=self._model_utils.generate_id(), @@ -174,7 +172,6 @@ async def execute( state_update.mark_tips_as_used( pipette_id=pipette_id, labware_id=labware_id, well_name=well_name ) - state_update.set_fluid_empty(pipette_id=pipette_id) return SuccessData( public=PickUpTipResult( tipVolume=tip_geometry.volume, diff --git a/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py b/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py index f5525b3c90e..01012be1d7f 100644 --- a/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/prepare_to_aspirate.py @@ -18,7 +18,6 @@ SuccessData, ) from ..errors.error_occurrence import ErrorOccurrence -from ..state import update_types if TYPE_CHECKING: from ..execution import PipettingHandler, GantryMover @@ -65,13 +64,11 @@ def __init__( async def execute(self, params: PrepareToAspirateParams) -> _ExecuteReturn: """Prepare the pipette to aspirate.""" current_position = await self._gantry_mover.get_position(params.pipetteId) - state_update = update_types.StateUpdate() try: await self._pipetting_handler.prepare_for_aspirate( pipette_id=params.pipetteId, ) except PipetteOverpressureError as e: - state_update.set_fluid_unknown(pipette_id=params.pipetteId) return DefinedErrorData( public=OverpressureError( id=self._model_utils.generate_id(), @@ -93,12 +90,10 @@ async def execute(self, params: PrepareToAspirateParams) -> _ExecuteReturn: } ), ), - state_update=state_update, ) else: - state_update.set_fluid_empty(pipette_id=params.pipetteId) return SuccessData( - public=PrepareToAspirateResult(), state_update=state_update + public=PrepareToAspirateResult(), ) diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py index 4c767625782..4738b7c9b97 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py @@ -10,7 +10,6 @@ from ..pipetting_common import PipetteIdMixin, FlowRateMixin from ...resources import ensure_ot3_hardware from ...errors.error_occurrence import ErrorOccurrence -from ...state import update_types from opentrons.hardware_control import HardwareControlAPI from opentrons.hardware_control.types import Axis @@ -67,11 +66,9 @@ async def execute( await self._pipetting.blow_out_in_place( pipette_id=params.pipetteId, flow_rate=params.flowRate ) - state_update = update_types.StateUpdate() - state_update.set_fluid_empty(pipette_id=params.pipetteId) return SuccessData( - public=UnsafeBlowOutInPlaceResult(), state_update=state_update + public=UnsafeBlowOutInPlaceResult(), ) diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py index 5aa4e292f63..ff749711cfb 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py @@ -77,7 +77,6 @@ async def execute( state_update.update_pipette_tip_state( pipette_id=params.pipetteId, tip_geometry=None ) - state_update.set_fluid_unknown(pipette_id=params.pipetteId) return SuccessData( public=UnsafeDropTipInPlaceResult(), state_update=state_update diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index e9f1acddeed..b25dfdb2d0e 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -77,7 +77,6 @@ OperationLocationNotInWellError, InvalidDispenseVolumeError, StorageLimitReachedError, - InvalidLiquidError, ) from .error_occurrence import ErrorOccurrence, ProtocolCommandFailedError @@ -138,7 +137,6 @@ "InvalidTargetSpeedError", "InvalidBlockVolumeError", "InvalidHoldTimeError", - "InvalidLiquidError", "CannotPerformModuleAction", "ResumeFromRecoveryNotAllowedError", "PauseNotAllowedError", diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index 36b0d2ccbef..12f45f4936d 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -244,19 +244,6 @@ def __init__( super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) -class InvalidLiquidError(ProtocolEngineError): - """Raised when attempting to add a liquid with an invalid property.""" - - def __init__( - self, - message: Optional[str] = None, - details: Optional[Dict[str, Any]] = None, - wrapping: Optional[Sequence[EnumeratedError]] = None, - ) -> None: - """Build an InvalidLiquidError.""" - super().__init__(ErrorCodes.INVALID_PROTOCOL_DATA, message, details, wrapping) - - class LabwareDefinitionDoesNotExistError(ProtocolEngineError): """Raised when referencing a labware definition that does not exist.""" diff --git a/api/src/opentrons/protocol_engine/protocol_engine.py b/api/src/opentrons/protocol_engine/protocol_engine.py index 574c3d076f9..ced32b20cc3 100644 --- a/api/src/opentrons/protocol_engine/protocol_engine.py +++ b/api/src/opentrons/protocol_engine/protocol_engine.py @@ -566,12 +566,9 @@ def add_liquid( description=(description or ""), displayColor=color, ) - validated_liquid = self._state_store.liquid.validate_liquid_allowed( - liquid=liquid - ) - self._action_dispatcher.dispatch(AddLiquidAction(liquid=validated_liquid)) - return validated_liquid + self._action_dispatcher.dispatch(AddLiquidAction(liquid=liquid)) + return liquid def add_addressable_area(self, addressable_area_name: str) -> None: """Add an addressable area to state.""" diff --git a/api/src/opentrons/protocol_engine/state/fluid_stack.py b/api/src/opentrons/protocol_engine/state/fluid_stack.py deleted file mode 100644 index 95465e531b2..00000000000 --- a/api/src/opentrons/protocol_engine/state/fluid_stack.py +++ /dev/null @@ -1,138 +0,0 @@ -"""Implements fluid stack tracking for pipettes. - -Inside a pipette's tip, there can be a mix of kinds of fluids - here, "fluid" means "liquid" (i.e. a protocol-relevant -working liquid that is aspirated or dispensed from wells) or "air" (i.e. because there was an air gap). Since sometimes -you want air gaps in different places - physically-below liquid to prevent dripping, physically-above liquid to provide -extra room to push the plunger - we need to support some notion of at least phsyical ordinal position of air and liquid, -and we do so as a logical stack because that's physically relevant. -""" -from logging import getLogger -from numpy import isclose -from ..types import AspiratedFluid, FluidKind - -_LOG = getLogger(__name__) - - -class FluidStack: - """A FluidStack data structure is a list of AspiratedFluids, with stack-style (last-in-first-out) ordering. - - The front of the list is the physical-top of the liquid stack (logical-bottom of the stack data structure) - and the back of the list is the physical-bottom of the liquid stack (logical-top of the stack data structure). - The state is internal and the interaction surface is the methods. This is a mutating API. - """ - - _FluidStack = list[AspiratedFluid] - - _fluid_stack: _FluidStack - - def __init__(self, _fluid_stack: _FluidStack | None = None) -> None: - """Build a FluidStack. - - The argument is provided for testing and shouldn't be generally used. - """ - self._fluid_stack = _fluid_stack or [] - - def add_fluid(self, new: AspiratedFluid) -> None: - """Add fluid to a stack. - - If the new fluid is of a different kind than what's on the physical-bottom of the stack, add a new record. - If the new fluid is of the same kind as what's on the physical-bottom of the stack, add the new volume to - the same record. - """ - if len(self._fluid_stack) == 0 or self._fluid_stack[-1].kind != new.kind: - # this is a new kind of fluid, append the record - self._fluid_stack.append(new) - else: - # this is more of the same kind of fluid, add the volumes - old_fluid = self._fluid_stack.pop(-1) - self._fluid_stack.append( - AspiratedFluid(kind=new.kind, volume=old_fluid.volume + new.volume) - ) - - def _alter_fluid_records( - self, remove: int, new_last: AspiratedFluid | None - ) -> None: - if remove >= len(self._fluid_stack) or len(self._fluid_stack) == 0: - self._fluid_stack = [] - return - if remove != 0: - removed = self._fluid_stack[:-remove] - else: - removed = self._fluid_stack - if new_last: - removed[-1] = new_last - self._fluid_stack = removed - - def remove_fluid(self, volume: float) -> None: - """Remove a specific amount of fluid from the physical-bottom of the stack. - - This will consume records that are wholly included in the provided volume and alter the remaining - final records (if any) to decrement the amount of volume removed from it. - - This function is designed to be used inside pipette store action handlers, which are generally not - exception-safe, and therefore swallows and logs errors. - """ - self._fluid_stack_iterator = reversed(self._fluid_stack) - removed_elements: list[AspiratedFluid] = [] - while volume > 0: - try: - last_stack_element = next(self._fluid_stack_iterator) - except StopIteration: - _LOG.error( - f"Attempting to remove more fluid than present, {volume}uL left over" - ) - self._alter_fluid_records(len(removed_elements), None) - return - if last_stack_element.volume < volume: - removed_elements.append(last_stack_element) - volume -= last_stack_element.volume - elif isclose(last_stack_element.volume, volume): - self._alter_fluid_records(len(removed_elements) + 1, None) - return - else: - self._alter_fluid_records( - len(removed_elements), - AspiratedFluid( - kind=last_stack_element.kind, - volume=last_stack_element.volume - volume, - ), - ) - return - - _LOG.error(f"Failed to handle removing {volume}uL from {self._fluid_stack}") - - def aspirated_volume(self, kind: FluidKind | None = None) -> float: - """Measure the total amount of fluid (optionally filtered by kind) in the stack.""" - volume = 0.0 - for el in self._fluid_stack: - if kind is not None and el.kind != kind: - continue - volume += el.volume - return volume - - def liquid_part_of_dispense_volume(self, volume: float) -> float: - """Get the amount of liquid in the specified volume starting at the physical-bottom of the stack.""" - liquid_volume = 0.0 - for el in reversed(self._fluid_stack): - if el.kind == FluidKind.LIQUID: - liquid_volume += min(volume, el.volume) - volume -= min(el.volume, volume) - if isclose(volume, 0.0): - return liquid_volume - return liquid_volume - - def __eq__(self, other: object) -> bool: - """Equality.""" - if isinstance(other, type(self)): - return other._fluid_stack == self._fluid_stack - return False - - def __repr__(self) -> str: - """String representation of a fluid stack.""" - if self._fluid_stack: - stringified_stack = ( - f'(top) {", ".join([str(item) for item in self._fluid_stack])} (bottom)' - ) - else: - stringified_stack = "empty" - return f"<{self.__class__.__name__}: {stringified_stack}>" diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py index 83499fb2510..dfdb0eec56f 100644 --- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -220,40 +220,28 @@ def _get_segment_capacity(segment: WellSegment) -> float: section_height = segment.topHeight - segment.bottomHeight match segment: case SphericalSegment(): - return ( - _volume_from_height_spherical( - target_height=segment.topHeight, - radius_of_curvature=segment.radiusOfCurvature, - ) - * segment.count + return _volume_from_height_spherical( + target_height=segment.topHeight, + radius_of_curvature=segment.radiusOfCurvature, ) case CuboidalFrustum(): - return ( - _volume_from_height_rectangular( - target_height=section_height, - bottom_length=segment.bottomYDimension, - bottom_width=segment.bottomXDimension, - top_length=segment.topYDimension, - top_width=segment.topXDimension, - total_frustum_height=section_height, - ) - * segment.count + return _volume_from_height_rectangular( + target_height=section_height, + bottom_length=segment.bottomYDimension, + bottom_width=segment.bottomXDimension, + top_length=segment.topYDimension, + top_width=segment.topXDimension, + total_frustum_height=section_height, ) case ConicalFrustum(): - return ( - _volume_from_height_circular( - target_height=section_height, - total_frustum_height=section_height, - bottom_radius=(segment.bottomDiameter / 2), - top_radius=(segment.topDiameter / 2), - ) - * segment.count + return _volume_from_height_circular( + target_height=section_height, + total_frustum_height=section_height, + bottom_radius=(segment.bottomDiameter / 2), + top_radius=(segment.topDiameter / 2), ) case SquaredConeSegment(): - return ( - _volume_from_height_squared_cone(section_height, segment) - * segment.count - ) + return _volume_from_height_squared_cone(section_height, segment) case _: # TODO: implement volume calculations for truncated circular and rounded rectangular segments raise NotImplementedError( @@ -284,7 +272,6 @@ def height_at_volume_within_section( section_height: float, ) -> float: """Calculate a height within a bounded section according to geometry.""" - target_volume_relative = target_volume_relative / section.count match section: case SphericalSegment(): return _height_from_volume_spherical( @@ -324,40 +311,28 @@ def volume_at_height_within_section( """Calculate a volume within a bounded section according to geometry.""" match section: case SphericalSegment(): - return ( - _volume_from_height_spherical( - target_height=target_height_relative, - radius_of_curvature=section.radiusOfCurvature, - ) - * section.count + return _volume_from_height_spherical( + target_height=target_height_relative, + radius_of_curvature=section.radiusOfCurvature, ) case ConicalFrustum(): - return ( - _volume_from_height_circular( - target_height=target_height_relative, - total_frustum_height=section_height, - bottom_radius=(section.bottomDiameter / 2), - top_radius=(section.topDiameter / 2), - ) - * section.count + return _volume_from_height_circular( + target_height=target_height_relative, + total_frustum_height=section_height, + bottom_radius=(section.bottomDiameter / 2), + top_radius=(section.topDiameter / 2), ) case CuboidalFrustum(): - return ( - _volume_from_height_rectangular( - target_height=target_height_relative, - total_frustum_height=section_height, - bottom_width=section.bottomXDimension, - bottom_length=section.bottomYDimension, - top_width=section.topXDimension, - top_length=section.topYDimension, - ) - * section.count + return _volume_from_height_rectangular( + target_height=target_height_relative, + total_frustum_height=section_height, + bottom_width=section.bottomXDimension, + bottom_length=section.bottomYDimension, + top_width=section.topXDimension, + top_length=section.topYDimension, ) case SquaredConeSegment(): - return ( - _volume_from_height_squared_cone(target_height_relative, section) - * section.count - ) + return _volume_from_height_squared_cone(target_height_relative, section) case _: # TODO(cm): this would be the NEST-96 2uL wells referenced in EXEC-712 # we need to input the math attached to that issue diff --git a/api/src/opentrons/protocol_engine/state/liquids.py b/api/src/opentrons/protocol_engine/state/liquids.py index 775223c6a60..9394e4261b1 100644 --- a/api/src/opentrons/protocol_engine/state/liquids.py +++ b/api/src/opentrons/protocol_engine/state/liquids.py @@ -1,11 +1,11 @@ """Basic liquid data state and store.""" from dataclasses import dataclass from typing import Dict, List -from opentrons.protocol_engine.types import Liquid, LiquidId +from opentrons.protocol_engine.types import Liquid from ._abstract_store import HasState, HandlesActions from ..actions import Action, AddLiquidAction -from ..errors import LiquidDoesNotExistError, InvalidLiquidError +from ..errors import LiquidDoesNotExistError @dataclass @@ -51,23 +51,11 @@ def get_all(self) -> List[Liquid]: """Get all protocol liquids.""" return list(self._state.liquids_by_id.values()) - def validate_liquid_id(self, liquid_id: LiquidId) -> LiquidId: + def validate_liquid_id(self, liquid_id: str) -> str: """Check if liquid_id exists in liquids.""" - is_empty = liquid_id == "EMPTY" - if is_empty: - return liquid_id has_liquid = liquid_id in self._state.liquids_by_id if not has_liquid: raise LiquidDoesNotExistError( f"Supplied liquidId: {liquid_id} does not exist in the loaded liquids." ) return liquid_id - - def validate_liquid_allowed(self, liquid: Liquid) -> Liquid: - """Validate that a liquid is legal to load.""" - is_empty = liquid.id == "EMPTY" - if is_empty: - raise InvalidLiquidError( - message='Protocols may not define a liquid with the special id "EMPTY".' - ) - return liquid diff --git a/api/src/opentrons/protocol_engine/state/pipettes.py b/api/src/opentrons/protocol_engine/state/pipettes.py index 8277204a4be..bb90e067ec6 100644 --- a/api/src/opentrons/protocol_engine/state/pipettes.py +++ b/api/src/opentrons/protocol_engine/state/pipettes.py @@ -2,17 +2,15 @@ from __future__ import annotations import dataclasses -from logging import getLogger from typing import ( Dict, List, Mapping, Optional, Tuple, + Union, ) -from typing_extensions import assert_never - from opentrons_shared_data.pipette import pipette_definition from opentrons.config.defaults_ot2 import Z_RETRACT_DISTANCE from opentrons.hardware_control.dev_types import PipetteDict @@ -23,7 +21,8 @@ ) from opentrons.types import MountType, Mount as HwMount, Point -from . import update_types, fluid_stack +from . import update_types +from .. import commands from .. import errors from ..types import ( LoadedPipette, @@ -37,13 +36,13 @@ ) from ..actions import ( Action, + FailCommandAction, SetPipetteMovementSpeedAction, + SucceedCommandAction, get_state_updates, ) from ._abstract_store import HasState, HandlesActions -LOG = getLogger(__name__) - @dataclasses.dataclass(frozen=True) class HardwarePipette: @@ -109,7 +108,7 @@ class PipetteState: # attributes are populated at the appropriate times. Refactor to a # single dict-of-many-things instead of many dicts-of-single-things. pipettes_by_id: Dict[str, LoadedPipette] - pipette_contents_by_id: Dict[str, Optional[fluid_stack.FluidStack]] + aspirated_volume_by_id: Dict[str, Optional[float]] current_location: Optional[CurrentPipetteLocation] current_deck_point: CurrentDeckPoint attached_tip_by_id: Dict[str, Optional[TipGeometry]] @@ -129,7 +128,7 @@ def __init__(self) -> None: """Initialize a PipetteStore and its state.""" self._state = PipetteState( pipettes_by_id={}, - pipette_contents_by_id={}, + aspirated_volume_by_id={}, attached_tip_by_id={}, current_location=None, current_deck_point=CurrentDeckPoint(mount=None, deck_point=None), @@ -148,9 +147,11 @@ def handle_action(self, action: Action) -> None: self._update_pipette_config(state_update) self._update_pipette_nozzle_map(state_update) self._update_tip_state(state_update) - self._update_volumes(state_update) - if isinstance(action, SetPipetteMovementSpeedAction): + if isinstance(action, (SucceedCommandAction, FailCommandAction)): + self._update_volumes(action) + + elif isinstance(action, SetPipetteMovementSpeedAction): self._state.movement_speed_by_id[action.pipette_id] = action.speed def _set_load_pipette(self, state_update: update_types.StateUpdate) -> None: @@ -165,6 +166,7 @@ def _set_load_pipette(self, state_update: update_types.StateUpdate) -> None: self._state.liquid_presence_detection_by_id[pipette_id] = ( state_update.loaded_pipette.liquid_presence_detection or False ) + self._state.aspirated_volume_by_id[pipette_id] = None self._state.movement_speed_by_id[pipette_id] = None self._state.attached_tip_by_id[pipette_id] = None @@ -175,6 +177,7 @@ def _update_tip_state(self, state_update: update_types.StateUpdate) -> None: attached_tip = state_update.pipette_tip_state.tip_geometry self._state.attached_tip_by_id[pipette_id] = attached_tip + self._state.aspirated_volume_by_id[pipette_id] = 0 static_config = self._state.static_config_by_id.get(pipette_id) if static_config: @@ -201,6 +204,7 @@ def _update_tip_state(self, state_update: update_types.StateUpdate) -> None: else: pipette_id = state_update.pipette_tip_state.pipette_id + self._state.aspirated_volume_by_id[pipette_id] = None self._state.attached_tip_by_id[pipette_id] = None static_config = self._state.static_config_by_id.get(pipette_id) @@ -304,40 +308,51 @@ def _update_pipette_nozzle_map( state_update.pipette_nozzle_map.pipette_id ] = state_update.pipette_nozzle_map.nozzle_map - def _update_volumes(self, state_update: update_types.StateUpdate) -> None: - if state_update.pipette_aspirated_fluid == update_types.NO_CHANGE: - return - if state_update.pipette_aspirated_fluid.type == "aspirated": - self._update_aspirated(state_update.pipette_aspirated_fluid) - elif state_update.pipette_aspirated_fluid.type == "ejected": - self._update_ejected(state_update.pipette_aspirated_fluid) - elif state_update.pipette_aspirated_fluid.type == "empty": - self._update_empty(state_update.pipette_aspirated_fluid) - elif state_update.pipette_aspirated_fluid.type == "unknown": - self._update_unknown(state_update.pipette_aspirated_fluid) - else: - assert_never(state_update.pipette_aspirated_fluid.type) - - def _update_aspirated( - self, update: update_types.PipetteAspiratedFluidUpdate + def _update_volumes( + self, action: Union[SucceedCommandAction, FailCommandAction] ) -> None: - self._fluid_stack_log_if_empty(update.pipette_id).add_fluid(update.fluid) + # todo(mm, 2024-10-10): Port these isinstance checks to StateUpdate. + # https://opentrons.atlassian.net/browse/EXEC-754 - def _update_ejected(self, update: update_types.PipetteEjectedFluidUpdate) -> None: - self._fluid_stack_log_if_empty(update.pipette_id).remove_fluid(update.volume) + if isinstance(action, SucceedCommandAction) and isinstance( + action.command.result, + (commands.AspirateResult, commands.AspirateInPlaceResult), + ): + pipette_id = action.command.params.pipetteId + previous_volume = self._state.aspirated_volume_by_id[pipette_id] or 0 + # PipetteHandler will have clamped action.command.result.volume for us, so + # next_volume should always be in bounds. + next_volume = previous_volume + action.command.result.volume - def _update_empty(self, update: update_types.PipetteEmptyFluidUpdate) -> None: - self._state.pipette_contents_by_id[update.pipette_id] = fluid_stack.FluidStack() + self._state.aspirated_volume_by_id[pipette_id] = next_volume - def _update_unknown(self, update: update_types.PipetteUnknownFluidUpdate) -> None: - self._state.pipette_contents_by_id[update.pipette_id] = None + elif isinstance(action, SucceedCommandAction) and isinstance( + action.command.result, + (commands.DispenseResult, commands.DispenseInPlaceResult), + ): + pipette_id = action.command.params.pipetteId + previous_volume = self._state.aspirated_volume_by_id[pipette_id] or 0 + # PipetteHandler will have clamped action.command.result.volume for us, so + # next_volume should always be in bounds. + next_volume = previous_volume - action.command.result.volume + self._state.aspirated_volume_by_id[pipette_id] = next_volume + + elif isinstance(action, SucceedCommandAction) and isinstance( + action.command.result, + ( + commands.BlowOutResult, + commands.BlowOutInPlaceResult, + commands.unsafe.UnsafeBlowOutInPlaceResult, + ), + ): + pipette_id = action.command.params.pipetteId + self._state.aspirated_volume_by_id[pipette_id] = None - def _fluid_stack_log_if_empty(self, pipette_id: str) -> fluid_stack.FluidStack: - stack = self._state.pipette_contents_by_id[pipette_id] - if stack is None: - LOG.error("Pipette state tried to alter an unknown-contents pipette") - return fluid_stack.FluidStack() - return stack + elif isinstance(action, SucceedCommandAction) and isinstance( + action.command.result, commands.PrepareToAspirateResult + ): + pipette_id = action.command.params.pipetteId + self._state.aspirated_volume_by_id[pipette_id] = 0 class PipetteView(HasState[PipetteState]): @@ -442,10 +457,6 @@ def get_all_attached_tips(self) -> List[Tuple[str, TipGeometry]]: def get_aspirated_volume(self, pipette_id: str) -> Optional[float]: """Get the currently aspirated volume of a pipette by ID. - This is the volume currently displaced by the plunger relative to its bottom position, - regardless of whether that volume likely contains liquid or air. This makes it the right - function to call to know how much more volume the plunger may displace. - Returns: The volume the pipette has aspirated. None, after blow-out and the plunger is in an unsafe position. @@ -457,50 +468,13 @@ def get_aspirated_volume(self, pipette_id: str) -> Optional[float]: self.validate_tip_state(pipette_id, True) try: - stack = self._state.pipette_contents_by_id[pipette_id] - if stack is None: - return None - return stack.aspirated_volume() + return self._state.aspirated_volume_by_id[pipette_id] except KeyError as e: raise errors.PipetteNotLoadedError( f"Pipette {pipette_id} not found; unable to get current volume." ) from e - def get_liquid_dispensed_by_ejecting_volume( - self, pipette_id: str, volume: float - ) -> Optional[float]: - """Get the amount of liquid (not air) that will be dispensed if the pipette ejects a specified volume. - - For instance, if the pipette contains, in vertical order, - 10 ul air - 80 ul liquid - 5 ul air - - then dispensing 10ul would result in 5ul of liquid; dispensing 85 ul would result in 80ul liquid; dispensing - 95ul would result in 80ul liquid. - - Returns: - The volume of liquid that would be dispensed by the requested volume. - None, after blow-out or when the plunger is in an unsafe position. - - Raises: - PipetteNotLoadedError: pipette ID does not exist. - TipnotAttachedError: No tip is attached to the pipette. - """ - self.validate_tip_state(pipette_id, True) - - try: - stack = self._state.pipette_contents_by_id[pipette_id] - if stack is None: - return None - return stack.liquid_part_of_dispense_volume(volume) - - except KeyError as e: - raise errors.PipetteNotLoadedError( - f"Pipette {pipette_id} not found; unable to get current liquid volume." - ) from e - def get_working_volume(self, pipette_id: str) -> float: """Get the working maximum volume of a pipette by ID. diff --git a/api/src/opentrons/protocol_engine/state/update_types.py b/api/src/opentrons/protocol_engine/state/update_types.py index 4487a503173..181d8820723 100644 --- a/api/src/opentrons/protocol_engine/state/update_types.py +++ b/api/src/opentrons/protocol_engine/state/update_types.py @@ -8,12 +8,7 @@ from opentrons.hardware_control.nozzle_manager import NozzleMap from opentrons.protocol_engine.resources import pipette_data_provider -from opentrons.protocol_engine.types import ( - DeckPoint, - LabwareLocation, - TipGeometry, - AspiratedFluid, -) +from opentrons.protocol_engine.types import DeckPoint, LabwareLocation, TipGeometry from opentrons.types import MountType from opentrons_shared_data.labware.labware_definition import LabwareDefinition from opentrons_shared_data.pipette.types import PipetteNameType @@ -210,40 +205,6 @@ class LiquidOperatedUpdate: volume_added: float | ClearType -@dataclasses.dataclass -class PipetteAspiratedFluidUpdate: - """Represents the pipette aspirating something. Might be air or liquid from a well.""" - - pipette_id: str - fluid: AspiratedFluid - type: typing.Literal["aspirated"] = "aspirated" - - -@dataclasses.dataclass -class PipetteEjectedFluidUpdate: - """Represents the pipette pushing something out. Might be air or liquid.""" - - pipette_id: str - volume: float - type: typing.Literal["ejected"] = "ejected" - - -@dataclasses.dataclass -class PipetteUnknownFluidUpdate: - """Represents the amount of fluid in the pipette becoming unknown.""" - - pipette_id: str - type: typing.Literal["unknown"] = "unknown" - - -@dataclasses.dataclass -class PipetteEmptyFluidUpdate: - """Sets the pipette to be valid and empty.""" - - pipette_id: str - type: typing.Literal["empty"] = "empty" - - @dataclasses.dataclass class StateUpdate: """Represents an update to perform on engine state.""" @@ -258,10 +219,6 @@ class StateUpdate: pipette_tip_state: PipetteTipStateUpdate | NoChangeType = NO_CHANGE - pipette_aspirated_fluid: PipetteAspiratedFluidUpdate | PipetteEjectedFluidUpdate | PipetteUnknownFluidUpdate | PipetteEmptyFluidUpdate | NoChangeType = ( - NO_CHANGE - ) - labware_location: LabwareLocationUpdate | NoChangeType = NO_CHANGE loaded_labware: LoadedLabwareUpdate | NoChangeType = NO_CHANGE @@ -449,27 +406,3 @@ def set_liquid_operated( well_name=well_name, volume_added=volume_added, ) - - def set_fluid_aspirated(self, pipette_id: str, fluid: AspiratedFluid) -> None: - """Update record of fluid held inside a pipette. See `PipetteAspiratedFluidUpdate`.""" - self.pipette_aspirated_fluid = PipetteAspiratedFluidUpdate( - type="aspirated", pipette_id=pipette_id, fluid=fluid - ) - - def set_fluid_ejected(self, pipette_id: str, volume: float) -> None: - """Update record of fluid held inside a pipette. See `PipetteEjectedFluidUpdate`.""" - self.pipette_aspirated_fluid = PipetteEjectedFluidUpdate( - type="ejected", pipette_id=pipette_id, volume=volume - ) - - def set_fluid_unknown(self, pipette_id: str) -> None: - """Update record of fluid held inside a pipette. See `PipetteUnknownFluidUpdate`.""" - self.pipette_aspirated_fluid = PipetteUnknownFluidUpdate( - type="unknown", pipette_id=pipette_id - ) - - def set_fluid_empty(self, pipette_id: str) -> None: - """Update record fo fluid held inside a pipette. See `PipetteEmptyFluidUpdate`.""" - self.pipette_aspirated_fluid = PipetteEmptyFluidUpdate( - type="empty", pipette_id=pipette_id - ) diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 5aa4c8c26e9..ea3a57945b2 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -423,21 +423,6 @@ class TipGeometry: volume: float -class FluidKind(str, Enum): - """A kind of fluid that can be inside a pipette.""" - - LIQUID = "LIQUID" - AIR = "AIR" - - -@dataclass(frozen=True) -class AspiratedFluid: - """Fluid inside a pipette.""" - - kind: FluidKind - volume: float - - class MovementAxis(str, Enum): """Axis on which to issue a relative movement.""" @@ -828,10 +813,6 @@ def _color_is_a_valid_hex(cls, v: str) -> str: return v -EmptyLiquidId = Literal["EMPTY"] -LiquidId = str | EmptyLiquidId - - class Liquid(BaseModel): """Payload required to create a liquid.""" diff --git a/api/src/opentrons/protocols/api_support/definitions.py b/api/src/opentrons/protocols/api_support/definitions.py index e2f6aee1a2a..ad692e03828 100644 --- a/api/src/opentrons/protocols/api_support/definitions.py +++ b/api/src/opentrons/protocols/api_support/definitions.py @@ -1,6 +1,6 @@ from .types import APIVersion -MAX_SUPPORTED_VERSION = APIVersion(2, 22) +MAX_SUPPORTED_VERSION = APIVersion(2, 21) """The maximum supported protocol API version in this release.""" MIN_SUPPORTED_VERSION = APIVersion(2, 0) diff --git a/api/src/opentrons/util/logging_config.py b/api/src/opentrons/util/logging_config.py index f9a59799d9d..6520bb912f6 100644 --- a/api/src/opentrons/util/logging_config.py +++ b/api/src/opentrons/util/logging_config.py @@ -36,7 +36,7 @@ def _host_config(level_value: int) -> Dict[str, Any]: "class": "logging.handlers.RotatingFileHandler", "formatter": "basic", "filename": serial_log_filename, - "maxBytes": 1000000, + "maxBytes": 5000000, "level": logging.DEBUG, "backupCount": 3, }, diff --git a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py index 9ccaac498f0..7b549fc035d 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_protocol_core.py @@ -1763,7 +1763,7 @@ def test_define_liquid_class( ) -> None: """It should create a LiquidClass and cache the definition.""" expected_liquid_class = LiquidClass( - _name="water1", _display_name="water 1", _by_pipette_setting={} + _name="water1", _display_name="water 1", _by_pipette_setting=[] ) decoy.when(liquid_classes.load_definition("water")).then_return( minimal_liquid_class_def1 diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 27d2f6ebb33..069330036ec 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -62,7 +62,6 @@ from opentrons_shared_data.errors.exceptions import ( CommandPreconditionViolated, ) -from . import versions_at_or_above, versions_between @pytest.fixture(autouse=True) @@ -1543,12 +1542,7 @@ def test_96_tip_config_invalid( assert subject._96_tip_config_valid() is True -@pytest.mark.parametrize( - "api_version", - versions_between( - low_exclusive_bound=APIVersion(2, 13), high_inclusive_bound=APIVersion(2, 21) - ), -) +@pytest.mark.parametrize("api_version", [APIVersion(2, 21)]) def test_mix_no_lpd( decoy: Decoy, mock_instrument_core: InstrumentCore, @@ -1599,7 +1593,7 @@ def test_mix_no_lpd( @pytest.mark.ot3_only -@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 21))) +@pytest.mark.parametrize("api_version", [APIVersion(2, 21)]) def test_mix_with_lpd( decoy: Decoy, mock_instrument_core: InstrumentCore, @@ -1647,60 +1641,3 @@ def test_mix_with_lpd( ignore_extra_args=True, times=1, ) - - -@pytest.mark.parametrize( - "api_version", - versions_between( - low_exclusive_bound=APIVersion(2, 13), high_inclusive_bound=APIVersion(2, 21) - ), -) -def test_air_gap_uses_aspirate( - decoy: Decoy, - mock_instrument_core: InstrumentCore, - mock_protocol_core: ProtocolCore, - subject: InstrumentContext, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """It should use its own aspirate function to aspirate air.""" - mock_well = decoy.mock(cls=Well) - top_location = Location(point=Point(9, 9, 14), labware=mock_well) - last_location = Location(point=Point(9, 9, 9), labware=mock_well) - mock_aspirate = decoy.mock(func=subject.aspirate) - mock_move_to = decoy.mock(func=subject.move_to) - monkeypatch.setattr(subject, "aspirate", mock_aspirate) - monkeypatch.setattr(subject, "move_to", mock_move_to) - - decoy.when(mock_instrument_core.has_tip()).then_return(True) - decoy.when(mock_protocol_core.get_last_location()).then_return(last_location) - decoy.when(mock_well.top(z=5.0)).then_return(top_location) - subject.air_gap(volume=10, height=5) - - decoy.verify(mock_move_to(top_location, publish=False)) - decoy.verify(mock_aspirate(10)) - - -@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) -def test_air_gap_uses_air_gap( - decoy: Decoy, - mock_instrument_core: InstrumentCore, - mock_protocol_core: ProtocolCore, - subject: InstrumentContext, - monkeypatch: pytest.MonkeyPatch, -) -> None: - """It should use its own aspirate function to aspirate air.""" - mock_well = decoy.mock(cls=Well) - top_location = Location(point=Point(9, 9, 14), labware=mock_well) - last_location = Location(point=Point(9, 9, 9), labware=mock_well) - mock_move_to = decoy.mock(func=subject.move_to) - monkeypatch.setattr(subject, "move_to", mock_move_to) - - decoy.when(mock_instrument_core.has_tip()).then_return(True) - decoy.when(mock_protocol_core.get_last_location()).then_return(last_location) - decoy.when(mock_well.top(z=5.0)).then_return(top_location) - decoy.when(mock_instrument_core.get_aspirate_flow_rate()).then_return(11) - - subject.air_gap(volume=10, height=5) - - decoy.verify(mock_move_to(top_location, publish=False)) - decoy.verify(mock_instrument_core.air_gap_in_place(10, 11)) diff --git a/api/tests/opentrons/protocol_api/test_liquid_class.py b/api/tests/opentrons/protocol_api/test_liquid_class.py index be0b432e32f..48f3788f496 100644 --- a/api/tests/opentrons/protocol_api/test_liquid_class.py +++ b/api/tests/opentrons/protocol_api/test_liquid_class.py @@ -12,7 +12,7 @@ def test_create_liquid_class( ) -> None: """It should create a LiquidClass from provided definition.""" assert LiquidClass.create(minimal_liquid_class_def1) == LiquidClass( - _name="water1", _display_name="water 1", _by_pipette_setting={} + _name="water1", _display_name="water 1", _by_pipette_setting=[] ) @@ -22,7 +22,7 @@ def test_get_for_pipette_and_tip( """It should get the properties for the specified pipette and tip.""" liq_class = LiquidClass.create(minimal_liquid_class_def2) result = liq_class.get_for("p20_single_gen2", "opentrons_96_tiprack_20ul") - assert result.aspirate.flow_rate_by_volume == {"default": 50, "10": 40, "20": 30} + assert result.aspirate.flowRateByVolume == {"default": 50, "10": 40, "20": 30} def test_get_for_raises_for_incorrect_pipette_or_tip( diff --git a/api/tests/opentrons/protocol_api/test_liquid_class_properties.py b/api/tests/opentrons/protocol_api/test_liquid_class_properties.py deleted file mode 100644 index b1699701f3c..00000000000 --- a/api/tests/opentrons/protocol_api/test_liquid_class_properties.py +++ /dev/null @@ -1,175 +0,0 @@ -"""Tests for LiquidClass properties and related functions.""" - -from opentrons_shared_data import load_shared_data -from opentrons_shared_data.liquid_classes.liquid_class_definition import ( - LiquidClassSchemaV1, - Coordinate, -) - -from opentrons.protocol_api._liquid_properties import ( - build_aspirate_properties, - build_single_dispense_properties, - build_multi_dispense_properties, -) - - -def test_build_aspirate_settings() -> None: - """It should convert the shared data aspirate settings to the PAPI type.""" - fixture_data = load_shared_data("liquid-class/fixtures/fixture_glycerol50.json") - liquid_class_model = LiquidClassSchemaV1.parse_raw(fixture_data) - aspirate_data = liquid_class_model.byPipette[0].byTipType[0].aspirate - - aspirate_properties = build_aspirate_properties(aspirate_data) - - assert aspirate_properties.submerge.position_reference.value == "liquid-meniscus" - assert aspirate_properties.submerge.offset == Coordinate(x=0, y=0, z=-5) - assert aspirate_properties.submerge.speed == 100 - assert aspirate_properties.submerge.delay.enabled is True - assert aspirate_properties.submerge.delay.duration == 1.5 - - assert aspirate_properties.retract.position_reference.value == "well-top" - assert aspirate_properties.retract.offset == Coordinate(x=0, y=0, z=5) - assert aspirate_properties.retract.speed == 100 - assert aspirate_properties.retract.air_gap_by_volume == { - "default": 2, - "5": 3, - "10": 4, - } - assert aspirate_properties.retract.touch_tip.enabled is True - assert aspirate_properties.retract.touch_tip.z_offset == 2 - assert aspirate_properties.retract.touch_tip.mm_to_edge == 1 - assert aspirate_properties.retract.touch_tip.speed == 50 - assert aspirate_properties.retract.delay.enabled is True - assert aspirate_properties.retract.delay.duration == 1 - - 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 == { - "default": 50, - "10": 40, - "20": 30, - } - assert aspirate_properties.pre_wet is True - assert aspirate_properties.mix.enabled is True - assert aspirate_properties.mix.repetitions == 3 - assert aspirate_properties.mix.volume == 15 - assert aspirate_properties.delay.enabled is True - assert aspirate_properties.delay.duration == 2 - - -def test_build_single_dispense_settings() -> None: - """It should convert the shared data single dispense settings to the PAPI type.""" - fixture_data = load_shared_data("liquid-class/fixtures/fixture_glycerol50.json") - liquid_class_model = LiquidClassSchemaV1.parse_raw(fixture_data) - single_dispense_data = liquid_class_model.byPipette[0].byTipType[0].singleDispense - - single_dispense_properties = build_single_dispense_properties(single_dispense_data) - - assert ( - single_dispense_properties.submerge.position_reference.value - == "liquid-meniscus" - ) - assert single_dispense_properties.submerge.offset == Coordinate(x=0, y=0, z=-5) - assert single_dispense_properties.submerge.speed == 100 - assert single_dispense_properties.submerge.delay.enabled is True - assert single_dispense_properties.submerge.delay.duration == 1.5 - - assert single_dispense_properties.retract.position_reference.value == "well-top" - assert single_dispense_properties.retract.offset == Coordinate(x=0, y=0, z=5) - assert single_dispense_properties.retract.speed == 100 - assert single_dispense_properties.retract.air_gap_by_volume == { - "default": 2, - "5": 3, - "10": 4, - } - assert single_dispense_properties.retract.touch_tip.enabled is True - assert single_dispense_properties.retract.touch_tip.z_offset == 2 - assert single_dispense_properties.retract.touch_tip.mm_to_edge == 1 - assert single_dispense_properties.retract.touch_tip.speed == 50 - assert single_dispense_properties.retract.blowout.enabled is True - assert single_dispense_properties.retract.blowout.location is not None - assert single_dispense_properties.retract.blowout.location.value == "trash" - assert single_dispense_properties.retract.blowout.flow_rate == 100 - assert single_dispense_properties.retract.delay.enabled is True - assert single_dispense_properties.retract.delay.duration == 1 - - assert single_dispense_properties.position_reference.value == "well-bottom" - assert single_dispense_properties.offset == Coordinate(x=0, y=0, z=-5) - assert single_dispense_properties.flow_rate_by_volume == { - "default": 50, - "10": 40, - "20": 30, - } - assert single_dispense_properties.mix.enabled is True - assert single_dispense_properties.mix.repetitions == 3 - assert single_dispense_properties.mix.volume == 15 - assert single_dispense_properties.push_out_by_volume == { - "default": 5, - "10": 7, - "20": 10, - } - assert single_dispense_properties.delay.enabled is True - assert single_dispense_properties.delay.duration == 2.5 - - -def test_build_multi_dispense_settings() -> None: - """It should convert the shared data multi dispense settings to the PAPI type.""" - fixture_data = load_shared_data("liquid-class/fixtures/fixture_glycerol50.json") - liquid_class_model = LiquidClassSchemaV1.parse_raw(fixture_data) - multi_dispense_data = liquid_class_model.byPipette[0].byTipType[0].multiDispense - - assert multi_dispense_data is not None - multi_dispense_properties = build_multi_dispense_properties(multi_dispense_data) - assert multi_dispense_properties is not None - - assert ( - multi_dispense_properties.submerge.position_reference.value == "liquid-meniscus" - ) - assert multi_dispense_properties.submerge.offset == Coordinate(x=0, y=0, z=-5) - assert multi_dispense_properties.submerge.speed == 100 - assert multi_dispense_properties.submerge.delay.enabled is True - assert multi_dispense_properties.submerge.delay.duration == 1.5 - - assert multi_dispense_properties.retract.position_reference.value == "well-top" - assert multi_dispense_properties.retract.offset == Coordinate(x=0, y=0, z=5) - assert multi_dispense_properties.retract.speed == 100 - assert multi_dispense_properties.retract.air_gap_by_volume == { - "default": 2, - "5": 3, - "10": 4, - } - assert multi_dispense_properties.retract.touch_tip.enabled is True - assert multi_dispense_properties.retract.touch_tip.z_offset == 2 - assert multi_dispense_properties.retract.touch_tip.mm_to_edge == 1 - assert multi_dispense_properties.retract.touch_tip.speed == 50 - assert multi_dispense_properties.retract.blowout.enabled is False - assert multi_dispense_properties.retract.blowout.location is None - assert multi_dispense_properties.retract.blowout.flow_rate is None - assert multi_dispense_properties.retract.delay.enabled is True - assert multi_dispense_properties.retract.delay.duration == 1 - - assert multi_dispense_properties.position_reference.value == "well-bottom" - assert multi_dispense_properties.offset == Coordinate(x=0, y=0, z=-5) - assert multi_dispense_properties.flow_rate_by_volume == { - "default": 50, - "10": 40, - "20": 30, - } - assert multi_dispense_properties.conditioning_by_volume == { - "default": 10, - "5": 5, - } - assert multi_dispense_properties.disposal_by_volume == { - "default": 2, - "5": 3, - } - assert multi_dispense_properties.delay.enabled is True - assert multi_dispense_properties.delay.duration == 1 - - -def test_build_multi_dispense_settings_none( - minimal_liquid_class_def2: LiquidClassSchemaV1, -) -> None: - """It should return None if there are no multi dispense properties in the model.""" - transfer_settings = minimal_liquid_class_def2.byPipette[0].byTipType[0] - assert build_multi_dispense_properties(transfer_settings.multiDispense) is None diff --git a/api/tests/opentrons/protocol_api/test_protocol_context.py b/api/tests/opentrons/protocol_api/test_protocol_context.py index 2c8e8b158af..2bedbd5fb6f 100644 --- a/api/tests/opentrons/protocol_api/test_protocol_context.py +++ b/api/tests/opentrons/protocol_api/test_protocol_context.py @@ -1227,7 +1227,7 @@ def test_define_liquid_class( ) -> None: """It should create the liquid class definition.""" expected_liquid_class = LiquidClass( - _name="volatile_100", _display_name="volatile 100%", _by_pipette_setting={} + _name="volatile_100", _display_name="volatile 100%", _by_pipette_setting=[] ) decoy.when(mock_core.define_liquid_class("volatile_90")).then_return( expected_liquid_class diff --git a/api/tests/opentrons/protocol_api/test_well.py b/api/tests/opentrons/protocol_api/test_well.py index b4817567dde..ef1eed84c62 100644 --- a/api/tests/opentrons/protocol_api/test_well.py +++ b/api/tests/opentrons/protocol_api/test_well.py @@ -8,8 +8,6 @@ from opentrons.protocol_api._liquid import Liquid from opentrons.types import Point, Location -from . import versions_at_or_above - @pytest.fixture def mock_well_core(decoy: Decoy) -> WellCore: @@ -142,13 +140,6 @@ def test_load_liquid(decoy: Decoy, mock_well_core: WellCore, subject: Well) -> N ) -@pytest.mark.parametrize("api_version", versions_at_or_above(APIVersion(2, 22))) -def test_load_empty(decoy: Decoy, mock_well_core: WellCore, subject: Well) -> None: - """It should mark a location as empty.""" - subject.load_empty() - decoy.verify(mock_well_core.load_empty(), times=1) - - def test_diameter(decoy: Decoy, mock_well_core: WellCore, subject: Well) -> None: """It should get the diameter from the core.""" decoy.when(mock_well_core.diameter).then_return(12.3) diff --git a/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py b/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py index eed90cc2478..049edae5c0f 100644 --- a/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py +++ b/api/tests/opentrons/protocol_api_integration/test_liquid_classes.py @@ -27,7 +27,7 @@ def test_liquid_class_creation_and_property_fetching( assert ( glycerol_50.get_for( pipette_left.name, tiprack.load_name - ).dispense.flow_rate_by_volume["default"] + ).dispense.flowRateByVolume["default"] == 50 ) assert ( diff --git a/api/tests/opentrons/protocol_engine/commands/test_air_gap_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_air_gap_in_place.py deleted file mode 100644 index 5d66a845dcc..00000000000 --- a/api/tests/opentrons/protocol_engine/commands/test_air_gap_in_place.py +++ /dev/null @@ -1,284 +0,0 @@ -"""Test aspirate-in-place commands.""" -from datetime import datetime - -import pytest -from decoy import Decoy, matchers - -from opentrons_shared_data.errors.exceptions import PipetteOverpressureError - -from opentrons.types import Point -from opentrons.hardware_control import API as HardwareAPI - -from opentrons.protocol_engine.execution import PipettingHandler, GantryMover -from opentrons.protocol_engine.commands.air_gap_in_place import ( - AirGapInPlaceParams, - AirGapInPlaceResult, - AirGapInPlaceImplementation, -) -from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData -from opentrons.protocol_engine.errors.exceptions import PipetteNotReadyToAspirateError -from opentrons.protocol_engine.notes import CommandNoteAdder -from opentrons.protocol_engine.resources import ModelUtils -from opentrons.protocol_engine.state.state import StateStore -from opentrons.protocol_engine.commands.pipetting_common import OverpressureError -from opentrons.protocol_engine.types import ( - CurrentWell, - CurrentPipetteLocation, - CurrentAddressableArea, - AspiratedFluid, - FluidKind, -) -from opentrons.protocol_engine.state import update_types - - -@pytest.fixture -def hardware_api(decoy: Decoy) -> HardwareAPI: - """Get a mock in the shape of a HardwareAPI.""" - return decoy.mock(cls=HardwareAPI) - - -@pytest.fixture -def state_store(decoy: Decoy) -> StateStore: - """Get a mock in the shape of a StateStore.""" - return decoy.mock(cls=StateStore) - - -@pytest.fixture -def pipetting(decoy: Decoy) -> PipettingHandler: - """Get a mock in the shape of a PipettingHandler.""" - return decoy.mock(cls=PipettingHandler) - - -@pytest.fixture -def subject( - pipetting: PipettingHandler, - state_store: StateStore, - hardware_api: HardwareAPI, - mock_command_note_adder: CommandNoteAdder, - model_utils: ModelUtils, - gantry_mover: GantryMover, -) -> AirGapInPlaceImplementation: - """Get the impelementation subject.""" - return AirGapInPlaceImplementation( - pipetting=pipetting, - hardware_api=hardware_api, - state_view=state_store, - command_note_adder=mock_command_note_adder, - model_utils=model_utils, - gantry_mover=gantry_mover, - ) - - -@pytest.mark.parametrize( - "location,stateupdateLabware,stateupdateWell", - [ - ( - CurrentWell( - pipette_id="pipette-id-abc", - labware_id="labware-id-1", - well_name="well-name-1", - ), - "labware-id-1", - "well-name-1", - ), - (None, None, None), - (CurrentAddressableArea("pipette-id-abc", "addressable-area-1"), None, None), - ], -) -async def test_air_gap_in_place_implementation( - decoy: Decoy, - pipetting: PipettingHandler, - state_store: StateStore, - hardware_api: HardwareAPI, - mock_command_note_adder: CommandNoteAdder, - subject: AirGapInPlaceImplementation, - location: CurrentPipetteLocation | None, - stateupdateLabware: str, - stateupdateWell: str, -) -> None: - """It should aspirate in place.""" - data = AirGapInPlaceParams( - pipetteId="pipette-id-abc", - volume=123, - flowRate=1.234, - ) - - decoy.when( - pipetting.get_is_ready_to_aspirate( - pipette_id="pipette-id-abc", - ) - ).then_return(True) - - decoy.when( - await pipetting.aspirate_in_place( - pipette_id="pipette-id-abc", - volume=123, - flow_rate=1.234, - command_note_adder=mock_command_note_adder, - ) - ).then_return(123) - - decoy.when(state_store.pipettes.get_current_location()).then_return(location) - - result = await subject.execute(params=data) - - if isinstance(location, CurrentWell): - assert result == SuccessData( - public=AirGapInPlaceResult(volume=123), - state_update=update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( - pipette_id="pipette-id-abc", - fluid=AspiratedFluid(kind=FluidKind.AIR, volume=123), - ) - ), - ) - else: - assert result == SuccessData( - public=AirGapInPlaceResult(volume=123), - state_update=update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( - pipette_id="pipette-id-abc", - fluid=AspiratedFluid(kind=FluidKind.AIR, volume=123), - ) - ), - ) - - -async def test_handle_air_gap_in_place_request_not_ready_to_aspirate( - decoy: Decoy, - pipetting: PipettingHandler, - state_store: StateStore, - hardware_api: HardwareAPI, - subject: AirGapInPlaceImplementation, -) -> None: - """Should raise an exception for not ready to aspirate.""" - data = AirGapInPlaceParams( - pipetteId="pipette-id-abc", - volume=123, - flowRate=1.234, - ) - - decoy.when( - pipetting.get_is_ready_to_aspirate( - pipette_id="pipette-id-abc", - ) - ).then_return(False) - - with pytest.raises( - PipetteNotReadyToAspirateError, - match="Pipette cannot air gap in place because of a previous blow out." - " The first aspirate following a blow-out must be from a specific well" - " so the plunger can be reset in a known safe position.", - ): - await subject.execute(params=data) - - -async def test_aspirate_raises_volume_error( - decoy: Decoy, - pipetting: PipettingHandler, - subject: AirGapInPlaceImplementation, - mock_command_note_adder: CommandNoteAdder, -) -> None: - """Should raise an assertion error for volume larger than working volume.""" - data = AirGapInPlaceParams( - pipetteId="abc", - volume=50, - flowRate=1.23, - ) - - decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id="abc")).then_return(True) - - decoy.when( - await pipetting.aspirate_in_place( - pipette_id="abc", - volume=50, - flow_rate=1.23, - command_note_adder=mock_command_note_adder, - ) - ).then_raise(AssertionError("blah blah")) - - with pytest.raises(AssertionError): - await subject.execute(data) - - -@pytest.mark.parametrize( - "location,stateupdateLabware,stateupdateWell", - [ - ( - CurrentWell( - pipette_id="pipette-id", - labware_id="labware-id-1", - well_name="well-name-1", - ), - "labware-id-1", - "well-name-1", - ), - (None, None, None), - (CurrentAddressableArea("pipette-id", "addressable-area-1"), None, None), - ], -) -async def test_overpressure_error( - decoy: Decoy, - gantry_mover: GantryMover, - pipetting: PipettingHandler, - subject: AirGapInPlaceImplementation, - model_utils: ModelUtils, - mock_command_note_adder: CommandNoteAdder, - state_store: StateStore, - location: CurrentPipetteLocation | None, - stateupdateLabware: str, - stateupdateWell: str, -) -> None: - """It should return an overpressure error if the hardware API indicates that.""" - pipette_id = "pipette-id" - - position = Point(x=1, y=2, z=3) - - error_id = "error-id" - error_timestamp = datetime(year=2020, month=1, day=2) - - data = AirGapInPlaceParams( - pipetteId=pipette_id, - volume=50, - flowRate=1.23, - ) - - decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id=pipette_id)).then_return( - True - ) - - decoy.when( - await pipetting.aspirate_in_place( - pipette_id=pipette_id, - volume=50, - flow_rate=1.23, - command_note_adder=mock_command_note_adder, - ), - ).then_raise(PipetteOverpressureError()) - - decoy.when(model_utils.generate_id()).then_return(error_id) - decoy.when(model_utils.get_timestamp()).then_return(error_timestamp) - decoy.when(await gantry_mover.get_position(pipette_id)).then_return(position) - decoy.when(state_store.pipettes.get_current_location()).then_return(location) - - result = await subject.execute(data) - - if isinstance(location, CurrentWell): - assert result == DefinedErrorData( - public=OverpressureError.construct( - id=error_id, - createdAt=error_timestamp, - wrappedErrors=[matchers.Anything()], - errorInfo={"retryLocation": (position.x, position.y, position.z)}, - ), - state_update=update_types.StateUpdate(), - ) - else: - assert result == DefinedErrorData( - public=OverpressureError.construct( - id=error_id, - createdAt=error_timestamp, - wrappedErrors=[matchers.Anything()], - errorInfo={"retryLocation": (position.x, position.y, position.z)}, - ), - ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py index 55950d51934..102114b1cc8 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py @@ -29,12 +29,7 @@ PipettingHandler, ) from opentrons.protocol_engine.resources.model_utils import ModelUtils -from opentrons.protocol_engine.types import ( - CurrentWell, - LoadedPipette, - AspiratedFluid, - FluidKind, -) +from opentrons.protocol_engine.types import CurrentWell, LoadedPipette from opentrons.hardware_control import HardwareControlAPI from opentrons.protocol_engine.notes import CommandNoteAdder @@ -119,9 +114,6 @@ async def test_aspirate_implementation_no_prep( well_name="A3", volume_added=-50, ), - pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( - pipette_id="abc", fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=50) - ), ), ) @@ -195,9 +187,6 @@ async def test_aspirate_implementation_with_prep( well_name="A3", volume_added=-50, ), - pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( - pipette_id="abc", fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=50) - ), ), ) @@ -338,9 +327,6 @@ async def test_overpressure_error( well_name=well_name, volume_added=update_types.CLEAR, ), - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id=pipette_id - ), ), ) @@ -406,8 +392,5 @@ async def test_aspirate_implementation_meniscus( well_name="A3", volume_added=-50, ), - pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( - pipette_id="abc", fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=50) - ), ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py index 034c7f51ede..85d8f4fab84 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate_in_place.py @@ -25,8 +25,6 @@ CurrentWell, CurrentPipetteLocation, CurrentAddressableArea, - AspiratedFluid, - FluidKind, ) from opentrons.protocol_engine.state import update_types @@ -130,22 +128,12 @@ async def test_aspirate_in_place_implementation( labware_id=stateupdateLabware, well_name=stateupdateWell, volume_added=-123, - ), - pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( - pipette_id="pipette-id-abc", - fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=123), - ), + ) ), ) else: assert result == SuccessData( public=AspirateInPlaceResult(volume=123), - state_update=update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( - pipette_id="pipette-id-abc", - fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=123), - ) - ), ) @@ -281,10 +269,7 @@ async def test_overpressure_error( labware_id=stateupdateLabware, well_name=stateupdateWell, volume_added=update_types.CLEAR, - ), - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="pipette-id" - ), + ) ), ) else: @@ -295,9 +280,4 @@ async def test_overpressure_error( wrappedErrors=[matchers.Anything()], errorInfo={"retryLocation": (position.x, position.y, position.z)}, ), - state_update=update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="pipette-id" - ) - ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_blow_out.py b/api/tests/opentrons/protocol_engine/commands/test_blow_out.py index d053aac0f0d..3e9aa6d82b8 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_blow_out.py +++ b/api/tests/opentrons/protocol_engine/commands/test_blow_out.py @@ -84,10 +84,7 @@ async def test_blow_out_implementation( well_name="C6", ), new_deck_point=DeckPoint(x=1, y=2, z=3), - ), - pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( - pipette_id="pipette-id" - ), + ) ), ) @@ -148,17 +145,4 @@ async def test_overpressure_error( wrappedErrors=[matchers.Anything()], errorInfo={"retryLocation": (1, 2, 3)}, ), - state_update=update_types.StateUpdate( - pipette_location=update_types.PipetteLocationUpdate( - pipette_id="pipette-id", - new_location=update_types.Well( - labware_id="labware-id", - well_name="C6", - ), - new_deck_point=DeckPoint(x=1, y=2, z=3), - ), - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="pipette-id" - ), - ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py index bc4ab782f64..49eced0670b 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_blow_out_in_place.py @@ -1,14 +1,11 @@ """Test blow-out-in-place commands.""" from datetime import datetime - -import pytest from decoy import Decoy, matchers from opentrons.protocol_engine.commands.pipetting_common import OverpressureError from opentrons.protocol_engine.execution.gantry_mover import GantryMover from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.protocol_engine.state.state import StateView -from opentrons.protocol_engine.state import update_types from opentrons.protocol_engine.commands.blow_out_in_place import ( BlowOutInPlaceParams, BlowOutInPlaceResult, @@ -21,6 +18,7 @@ from opentrons.hardware_control import HardwareControlAPI from opentrons.types import Point from opentrons_shared_data.errors.exceptions import PipetteOverpressureError +import pytest @pytest.fixture @@ -54,14 +52,7 @@ async def test_blow_out_in_place_implementation( result = await subject.execute(data) - assert result == SuccessData( - public=BlowOutInPlaceResult(), - state_update=update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( - pipette_id="pipette-id" - ) - ), - ) + assert result == SuccessData(public=BlowOutInPlaceResult()) decoy.verify( await pipetting.blow_out_in_place(pipette_id="pipette-id", flow_rate=1.234) @@ -109,9 +100,4 @@ async def test_overpressure_error( wrappedErrors=[matchers.Anything()], errorInfo={"retryLocation": (position.x, position.y, position.z)}, ), - state_update=update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="pipette-id" - ) - ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense.py b/api/tests/opentrons/protocol_engine/commands/test_dispense.py index a51b2cc7b84..a996e6915e8 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_dispense.py +++ b/api/tests/opentrons/protocol_engine/commands/test_dispense.py @@ -48,7 +48,6 @@ async def test_dispense_implementation( movement: MovementHandler, pipetting: PipettingHandler, subject: DispenseImplementation, - state_view: StateView, ) -> None: """It should move to the target location and then dispense.""" well_location = LiquidHandlingWellLocation( @@ -78,11 +77,6 @@ async def test_dispense_implementation( pipette_id="pipette-id-abc123", volume=50, flow_rate=1.23, push_out=None ) ).then_return(42) - decoy.when( - state_view.pipettes.get_liquid_dispensed_by_ejecting_volume( - pipette_id="pipette-id-abc123", volume=42 - ) - ).then_return(34) result = await subject.execute(data) @@ -100,10 +94,7 @@ async def test_dispense_implementation( liquid_operated=update_types.LiquidOperatedUpdate( labware_id="labware-id-abc123", well_name="A3", - volume_added=34, - ), - pipette_aspirated_fluid=update_types.PipetteEjectedFluidUpdate( - pipette_id="pipette-id-abc123", volume=42 + volume_added=42, ), ), ) @@ -179,8 +170,5 @@ async def test_overpressure_error( well_name="well-name", volume_added=update_types.CLEAR, ), - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="pipette-id" - ), ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py index bcfdba0ed57..5e432bef80a 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_dispense_in_place.py @@ -17,7 +17,7 @@ ) from opentrons.protocol_engine.commands.pipetting_common import OverpressureError from opentrons.protocol_engine.resources import ModelUtils -from opentrons.protocol_engine.state.state import StateView +from opentrons.protocol_engine.state.state import StateStore from opentrons.protocol_engine.types import ( CurrentWell, CurrentPipetteLocation, @@ -27,19 +27,9 @@ @pytest.fixture -def subject( - pipetting: PipettingHandler, - state_view: StateView, - gantry_mover: GantryMover, - model_utils: ModelUtils, -) -> DispenseInPlaceImplementation: - """Build a command implementation.""" - return DispenseInPlaceImplementation( - pipetting=pipetting, - state_view=state_view, - gantry_mover=gantry_mover, - model_utils=model_utils, - ) +def state_store(decoy: Decoy) -> StateStore: + """Get a mock in the shape of a StateStore.""" + return decoy.mock(cls=StateStore) @pytest.mark.parametrize( @@ -61,13 +51,21 @@ def subject( async def test_dispense_in_place_implementation( decoy: Decoy, pipetting: PipettingHandler, - state_view: StateView, - subject: DispenseInPlaceImplementation, + state_store: StateStore, + gantry_mover: GantryMover, + model_utils: ModelUtils, location: CurrentPipetteLocation | None, stateupdateLabware: str, stateupdateWell: str, ) -> None: """It should dispense in place.""" + subject = DispenseInPlaceImplementation( + pipetting=pipetting, + state_view=state_store, + gantry_mover=gantry_mover, + model_utils=model_utils, + ) + data = DispenseInPlaceParams( pipetteId="pipette-id-abc", volume=123, @@ -80,12 +78,7 @@ async def test_dispense_in_place_implementation( ) ).then_return(42) - decoy.when(state_view.pipettes.get_current_location()).then_return(location) - decoy.when( - state_view.pipettes.get_liquid_dispensed_by_ejecting_volume( - pipette_id="pipette-id-abc", volume=42 - ) - ).then_return(34) + decoy.when(state_store.pipettes.get_current_location()).then_return(location) result = await subject.execute(data) @@ -93,24 +86,16 @@ async def test_dispense_in_place_implementation( assert result == SuccessData( public=DispenseInPlaceResult(volume=42), state_update=update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteEjectedFluidUpdate( - pipette_id="pipette-id-abc", volume=42 - ), liquid_operated=update_types.LiquidOperatedUpdate( labware_id=stateupdateLabware, well_name=stateupdateWell, - volume_added=34, - ), + volume_added=42, + ) ), ) else: assert result == SuccessData( public=DispenseInPlaceResult(volume=42), - state_update=update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteEjectedFluidUpdate( - pipette_id="pipette-id-abc", volume=42 - ) - ), ) @@ -134,14 +119,20 @@ async def test_overpressure_error( decoy: Decoy, gantry_mover: GantryMover, pipetting: PipettingHandler, - state_view: StateView, + state_store: StateStore, model_utils: ModelUtils, - subject: DispenseInPlaceImplementation, location: CurrentPipetteLocation | None, stateupdateLabware: str, stateupdateWell: str, ) -> None: """It should return an overpressure error if the hardware API indicates that.""" + subject = DispenseInPlaceImplementation( + pipetting=pipetting, + state_view=state_store, + gantry_mover=gantry_mover, + model_utils=model_utils, + ) + pipette_id = "pipette-id" position = Point(x=1, y=2, z=3) @@ -168,7 +159,7 @@ async def test_overpressure_error( decoy.when(model_utils.generate_id()).then_return(error_id) decoy.when(model_utils.get_timestamp()).then_return(error_timestamp) decoy.when(await gantry_mover.get_position(pipette_id)).then_return(position) - decoy.when(state_view.pipettes.get_current_location()).then_return(location) + decoy.when(state_store.pipettes.get_current_location()).then_return(location) result = await subject.execute(data) @@ -185,10 +176,7 @@ async def test_overpressure_error( labware_id=stateupdateLabware, well_name=stateupdateWell, volume_added=update_types.CLEAR, - ), - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="pipette-id" - ), + ) ), ) else: @@ -198,10 +186,5 @@ async def test_overpressure_error( createdAt=error_timestamp, wrappedErrors=[matchers.Anything()], errorInfo={"retryLocation": (position.x, position.y, position.z)}, - ), - state_update=update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="pipette-id" - ) - ), + ) ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py index 9217a4a4287..1d113c999c3 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_drop_tip.py @@ -141,9 +141,6 @@ async def test_drop_tip_implementation( pipette_tip_state=update_types.PipetteTipStateUpdate( pipette_id="abc", tip_geometry=None ), - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="abc" - ), ), ) @@ -221,9 +218,6 @@ async def test_drop_tip_with_alternating_locations( pipette_tip_state=update_types.PipetteTipStateUpdate( pipette_id="abc", tip_geometry=None ), - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="abc" - ), ), ) @@ -297,14 +291,11 @@ async def test_tip_attached_error( ), new_deck_point=DeckPoint(x=111, y=222, z=333), ), - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="abc" - ), ), state_update_if_false_positive=update_types.StateUpdate( pipette_tip_state=update_types.PipetteTipStateUpdate( pipette_id="abc", tip_geometry=None, - ), + ) ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py b/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py index 9ea78e7dadd..292aa532879 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/test_drop_tip_in_place.py @@ -19,7 +19,6 @@ from opentrons.protocol_engine.state.update_types import ( PipetteTipStateUpdate, StateUpdate, - PipetteUnknownFluidUpdate, ) @@ -51,10 +50,7 @@ async def test_success( assert result == SuccessData( public=DropTipInPlaceResult(), state_update=StateUpdate( - pipette_tip_state=PipetteTipStateUpdate( - pipette_id="abc", tip_geometry=None - ), - pipette_aspirated_fluid=PipetteUnknownFluidUpdate(pipette_id="abc"), + pipette_tip_state=PipetteTipStateUpdate(pipette_id="abc", tip_geometry=None) ), ) @@ -93,12 +89,8 @@ async def test_tip_attached_error( createdAt=datetime(year=1, month=2, day=3), wrappedErrors=[matchers.Anything()], ), - state_update=StateUpdate( - pipette_aspirated_fluid=PipetteUnknownFluidUpdate(pipette_id="abc") - ), + state_update=StateUpdate(), state_update_if_false_positive=StateUpdate( - pipette_tip_state=PipetteTipStateUpdate( - pipette_id="abc", tip_geometry=None - ), + pipette_tip_state=PipetteTipStateUpdate(pipette_id="abc", tip_geometry=None) ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_liquid.py b/api/tests/opentrons/protocol_engine/commands/test_load_liquid.py index 6bd61061f3c..dbc584ae2a3 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_liquid.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_liquid.py @@ -9,7 +9,6 @@ LoadLiquidImplementation, LoadLiquidParams, ) -from opentrons.protocol_engine.errors import InvalidLiquidError from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.protocol_engine.state import update_types @@ -65,37 +64,3 @@ async def test_load_liquid_implementation( "labware-id", {"A1": 30.0, "B2": 100.0} ) ) - - -async def test_load_empty_liquid_requires_zero_volume( - decoy: Decoy, - subject: LoadLiquidImplementation, - mock_state_view: StateView, - model_utils: ModelUtils, -) -> None: - """Test that loadLiquid requires empty liquids to have 0 volume.""" - data = LoadLiquidParams( - labwareId="labware-id", liquidId="EMPTY", volumeByWell={"A1": 1.0} - ) - timestamp = datetime(year=2020, month=1, day=2) - decoy.when(model_utils.get_timestamp()).then_return(timestamp) - - with pytest.raises(InvalidLiquidError): - await subject.execute(data) - - decoy.verify(mock_state_view.liquid.validate_liquid_id("EMPTY")) - - data2 = LoadLiquidParams( - labwareId="labware-id", liquidId="EMPTY", volumeByWell={"A1": 0.0} - ) - result = await subject.execute(data2) - assert result == SuccessData( - public=LoadLiquidResult(), - state_update=update_types.StateUpdate( - liquid_loaded=update_types.LiquidLoadedUpdate( - labware_id="labware-id", - volumes=data2.volumeByWell, - last_loaded=timestamp, - ) - ), - ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py b/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py index a42bbc4e4d9..44a9db61863 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py +++ b/api/tests/opentrons/protocol_engine/commands/test_load_pipette.py @@ -3,7 +3,6 @@ LoadPipetteUpdate, PipetteConfigUpdate, StateUpdate, - PipetteUnknownFluidUpdate, ) import pytest from decoy import Decoy @@ -102,7 +101,6 @@ async def test_load_pipette_implementation( serial_number="some-serial-number", config=config_data, ), - pipette_aspirated_fluid=PipetteUnknownFluidUpdate(pipette_id="some id"), ), ) @@ -168,7 +166,6 @@ async def test_load_pipette_implementation_96_channel( serial_number="some id", config=config_data, ), - pipette_aspirated_fluid=PipetteUnknownFluidUpdate(pipette_id="pipette-id"), ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py index 5fb97a2f78f..00a3bc1c8a8 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py @@ -97,9 +97,6 @@ async def test_success( tips_used=update_types.TipsUsedUpdate( pipette_id="pipette-id", labware_id="labware-id", well_name="A3" ), - pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( - pipette_id="pipette-id" - ), ), ) @@ -166,16 +163,10 @@ async def test_tip_physically_missing_error( tips_used=update_types.TipsUsedUpdate( pipette_id="pipette-id", labware_id="labware-id", well_name="well-name" ), - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="pipette-id" - ), ), state_update_if_false_positive=update_types.StateUpdate( pipette_tip_state=update_types.PipetteTipStateUpdate( pipette_id="pipette-id", tip_geometry=sentinel.tip_geometry - ), - pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( - pipette_id="pipette-id" - ), + ) ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py b/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py index 2de35e38332..bb4f8c5f4d9 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py +++ b/api/tests/opentrons/protocol_engine/commands/test_prepare_to_aspirate.py @@ -17,7 +17,6 @@ from opentrons.protocol_engine.execution.gantry_mover import GantryMover from opentrons.protocol_engine.resources.model_utils import ModelUtils from opentrons.protocol_engine.commands.pipetting_common import OverpressureError -from opentrons.protocol_engine.state import update_types from opentrons_shared_data.errors.exceptions import PipetteOverpressureError @@ -33,7 +32,7 @@ def subject( ) -async def test_prepare_to_aspirate_implementation( +async def test_prepare_to_aspirate_implmenetation( decoy: Decoy, subject: PrepareToAspirateImplementation, pipetting: PipettingHandler ) -> None: """A PrepareToAspirate command should have an executing implementation.""" @@ -44,14 +43,7 @@ async def test_prepare_to_aspirate_implementation( ) result = await subject.execute(data) - assert result == SuccessData( - public=PrepareToAspirateResult(), - state_update=update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( - pipette_id="some id" - ) - ), - ) + assert result == SuccessData(public=PrepareToAspirateResult()) async def test_overpressure_error( @@ -92,9 +84,4 @@ async def test_overpressure_error( wrappedErrors=[matchers.Anything()], errorInfo={"retryLocation": (position.x, position.y, position.z)}, ), - state_update=update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="pipette-id" - ) - ), ) diff --git a/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_blow_out_in_place.py b/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_blow_out_in_place.py index 88ad9a8ecf8..aec5df2620d 100644 --- a/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_blow_out_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_blow_out_in_place.py @@ -3,7 +3,6 @@ from opentrons.types import MountType from opentrons.protocol_engine.state.state import StateView -from opentrons.protocol_engine.state import update_types from opentrons.protocol_engine.commands.unsafe.unsafe_blow_out_in_place import ( UnsafeBlowOutInPlaceParams, UnsafeBlowOutInPlaceResult, @@ -42,14 +41,7 @@ async def test_blow_out_in_place_implementation( result = await subject.execute(data) - assert result == SuccessData( - public=UnsafeBlowOutInPlaceResult(), - state_update=update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( - pipette_id="pipette-id" - ) - ), - ) + assert result == SuccessData(public=UnsafeBlowOutInPlaceResult()) decoy.verify( await ot3_hardware_api.update_axis_position_estimations([Axis.P_L]), diff --git a/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_drop_tip_in_place.py b/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_drop_tip_in_place.py index e7c684554c8..fb23d96d987 100644 --- a/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_drop_tip_in_place.py +++ b/api/tests/opentrons/protocol_engine/commands/unsafe/test_unsafe_drop_tip_in_place.py @@ -1,7 +1,6 @@ """Test unsafe drop tip in place commands.""" from opentrons.protocol_engine.state.update_types import ( PipetteTipStateUpdate, - PipetteUnknownFluidUpdate, StateUpdate, ) import pytest @@ -52,10 +51,7 @@ async def test_drop_tip_implementation( assert result == SuccessData( public=UnsafeDropTipInPlaceResult(), state_update=StateUpdate( - pipette_tip_state=PipetteTipStateUpdate( - pipette_id="abc", tip_geometry=None - ), - pipette_aspirated_fluid=PipetteUnknownFluidUpdate(pipette_id="abc"), + pipette_tip_state=PipetteTipStateUpdate(pipette_id="abc", tip_geometry=None) ), ) diff --git a/api/tests/opentrons/protocol_engine/state/test_fluid_stack.py b/api/tests/opentrons/protocol_engine/state/test_fluid_stack.py deleted file mode 100644 index e958b92036d..00000000000 --- a/api/tests/opentrons/protocol_engine/state/test_fluid_stack.py +++ /dev/null @@ -1,219 +0,0 @@ -"""Test pipette internal fluid tracking.""" -import pytest - -from opentrons.protocol_engine.state.fluid_stack import FluidStack -from opentrons.protocol_engine.types import AspiratedFluid, FluidKind - - -@pytest.mark.parametrize( - "fluids,resulting_stack", - [ - ( - [ - AspiratedFluid(FluidKind.LIQUID, 10), - AspiratedFluid(FluidKind.LIQUID, 10), - ], - [AspiratedFluid(FluidKind.LIQUID, 20)], - ), - ( - [AspiratedFluid(FluidKind.AIR, 10), AspiratedFluid(FluidKind.LIQUID, 20)], - [AspiratedFluid(FluidKind.AIR, 10), AspiratedFluid(FluidKind.LIQUID, 20)], - ), - ( - [ - AspiratedFluid(FluidKind.AIR, 10), - AspiratedFluid(FluidKind.LIQUID, 20), - AspiratedFluid(FluidKind.LIQUID, 10), - AspiratedFluid(FluidKind.AIR, 20), - ], - [ - AspiratedFluid(FluidKind.AIR, 10), - AspiratedFluid(FluidKind.LIQUID, 30), - AspiratedFluid(FluidKind.AIR, 20), - ], - ), - ], -) -def test_add_fluid( - fluids: list[AspiratedFluid], resulting_stack: list[AspiratedFluid] -) -> None: - """It should add fluids.""" - stack = FluidStack() - for fluid in fluids: - stack.add_fluid(fluid) - assert stack._fluid_stack == resulting_stack - - -@pytest.mark.parametrize( - "starting_fluids,remove_volume,resulting_stack", - [ - ([], 1, []), - ([], 0, []), - ( - [AspiratedFluid(FluidKind.LIQUID, 10)], - 0, - [AspiratedFluid(FluidKind.LIQUID, 10)], - ), - ( - [AspiratedFluid(FluidKind.LIQUID, 10)], - 5, - [AspiratedFluid(FluidKind.LIQUID, 5)], - ), - ([AspiratedFluid(FluidKind.LIQUID, 10)], 11, []), - ( - [AspiratedFluid(FluidKind.LIQUID, 10), AspiratedFluid(FluidKind.AIR, 10)], - 11, - [AspiratedFluid(FluidKind.LIQUID, 9)], - ), - ( - [AspiratedFluid(FluidKind.LIQUID, 10), AspiratedFluid(FluidKind.AIR, 10)], - 20, - [], - ), - ( - [ - AspiratedFluid(FluidKind.LIQUID, 10), - AspiratedFluid(FluidKind.AIR, 10), - AspiratedFluid(FluidKind.LIQUID, 10), - ], - 28, - [AspiratedFluid(FluidKind.LIQUID, 2)], - ), - ], -) -def test_remove_fluid( - starting_fluids: list[AspiratedFluid], - remove_volume: float, - resulting_stack: list[AspiratedFluid], -) -> None: - """It should remove fluids.""" - stack = FluidStack(_fluid_stack=[f for f in starting_fluids]) - stack.remove_fluid(remove_volume) - assert stack._fluid_stack == resulting_stack - - -@pytest.mark.parametrize( - "starting_fluids,filter,result", - [ - ([], None, 0), - ([], FluidKind.LIQUID, 0), - ([], FluidKind.AIR, 0), - ([AspiratedFluid(FluidKind.LIQUID, 10)], None, 10), - ([AspiratedFluid(FluidKind.LIQUID, 10)], FluidKind.LIQUID, 10), - ([AspiratedFluid(FluidKind.LIQUID, 10)], FluidKind.AIR, 0), - ( - [AspiratedFluid(FluidKind.LIQUID, 10), AspiratedFluid(FluidKind.AIR, 10)], - None, - 20, - ), - ( - [AspiratedFluid(FluidKind.LIQUID, 10), AspiratedFluid(FluidKind.AIR, 10)], - FluidKind.LIQUID, - 10, - ), - ( - [AspiratedFluid(FluidKind.LIQUID, 10), AspiratedFluid(FluidKind.AIR, 10)], - FluidKind.AIR, - 10, - ), - ], -) -def test_aspirated_volume( - starting_fluids: list[AspiratedFluid], filter: FluidKind | None, result: float -) -> None: - """It should represent aspirated volume with filtering.""" - stack = FluidStack(_fluid_stack=starting_fluids) - assert stack.aspirated_volume(kind=filter) == result - - -@pytest.mark.parametrize( - "starting_fluids,dispense_volume,result", - [ - ([], 0, 0), - ([], 1, 0), - ([AspiratedFluid(FluidKind.AIR, 10)], 10, 0), - ([AspiratedFluid(FluidKind.AIR, 10)], 0, 0), - ([AspiratedFluid(FluidKind.LIQUID, 10)], 10, 10), - ([AspiratedFluid(FluidKind.LIQUID, 10)], 0, 0), - ( - [ - AspiratedFluid(FluidKind.AIR, 10), - AspiratedFluid(FluidKind.LIQUID, 10), - ], - 10, - 10, - ), - ( - [ - AspiratedFluid(FluidKind.AIR, 10), - AspiratedFluid(FluidKind.LIQUID, 10), - ], - 20, - 10, - ), - ( - [ - AspiratedFluid(FluidKind.AIR, 10), - AspiratedFluid(FluidKind.LIQUID, 10), - ], - 30, - 10, - ), - ( - [ - AspiratedFluid(FluidKind.AIR, 10), - AspiratedFluid(FluidKind.LIQUID, 10), - ], - 5, - 5, - ), - ( - [ - AspiratedFluid(FluidKind.LIQUID, 10), - AspiratedFluid(FluidKind.AIR, 10), - ], - 5, - 0, - ), - ( - [ - AspiratedFluid(FluidKind.LIQUID, 10), - AspiratedFluid(FluidKind.AIR, 10), - ], - 10, - 0, - ), - ( - [ - AspiratedFluid(FluidKind.LIQUID, 10), - AspiratedFluid(FluidKind.AIR, 10), - ], - 11, - 1, - ), - ( - [ - AspiratedFluid(FluidKind.LIQUID, 10), - AspiratedFluid(FluidKind.AIR, 10), - ], - 20, - 10, - ), - ( - [ - AspiratedFluid(FluidKind.LIQUID, 10), - AspiratedFluid(FluidKind.AIR, 10), - ], - 30, - 10, - ), - ], -) -def test_liquid_part_of_dispense_volume( - starting_fluids: list[AspiratedFluid], - dispense_volume: float, - result: float, -) -> None: - """It should predict resulting liquid from a dispense.""" - stack = FluidStack(_fluid_stack=starting_fluids) - assert stack.liquid_part_of_dispense_volume(dispense_volume) == result diff --git a/api/tests/opentrons/protocol_engine/state/test_liquid_view.py b/api/tests/opentrons/protocol_engine/state/test_liquid_view.py index db1e6f274a1..f3424932b0e 100644 --- a/api/tests/opentrons/protocol_engine/state/test_liquid_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_liquid_view.py @@ -3,7 +3,7 @@ from opentrons.protocol_engine.state.liquids import LiquidState, LiquidView from opentrons.protocol_engine import Liquid -from opentrons.protocol_engine.errors import LiquidDoesNotExistError, InvalidLiquidError +from opentrons.protocol_engine.errors import LiquidDoesNotExistError @pytest.fixture @@ -33,22 +33,3 @@ def test_has_liquid(subject: LiquidView) -> None: with pytest.raises(LiquidDoesNotExistError): subject.validate_liquid_id("no-id") - - -def test_validate_liquid_prevents_empty(subject: LiquidView) -> None: - """It should not allow loading a liquid with the special id EMPTY.""" - with pytest.raises(InvalidLiquidError): - subject.validate_liquid_allowed( - Liquid(id="EMPTY", displayName="empty", description="nothing") - ) - - -def test_validate_liquid_allows_non_empty(subject: LiquidView) -> None: - """It should allow a valid liquid.""" - valid_liquid = Liquid( - id="some-id", - displayName="some-display-name", - description="some-description", - displayColor=None, - ) - assert subject.validate_liquid_allowed(valid_liquid) == valid_liquid diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py index 31b1a7f3a2c..c8eab566abe 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_store.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_store.py @@ -14,8 +14,6 @@ FlowRates, CurrentWell, TipGeometry, - AspiratedFluid, - FluidKind, ) from opentrons.protocol_engine.actions import ( SetPipetteMovementSpeedAction, @@ -32,7 +30,6 @@ from opentrons.protocol_engine.resources.pipette_data_provider import ( LoadedStaticPipetteData, ) -from opentrons.protocol_engine.state.fluid_stack import FluidStack from .command_fixtures import ( create_load_pipette_command, @@ -65,7 +62,7 @@ def test_sets_initial_state(subject: PipetteStore) -> None: assert result == PipetteState( pipettes_by_id={}, - pipette_contents_by_id={}, + aspirated_volume_by_id={}, current_location=None, current_deck_point=CurrentDeckPoint(mount=None, deck_point=None), attached_tip_by_id={}, @@ -226,15 +223,12 @@ def test_handles_load_pipette( config=config, serial_number="pipette-serial", ) - contents_update = update_types.PipetteUnknownFluidUpdate(pipette_id="pipette-id") subject.handle_action( SucceedCommandAction( command=dummy_command, state_update=update_types.StateUpdate( - loaded_pipette=load_pipette_update, - pipette_config=config_update, - pipette_aspirated_fluid=contents_update, + loaded_pipette=load_pipette_update, pipette_config=config_update ), ) ) @@ -246,7 +240,7 @@ def test_handles_load_pipette( pipetteName=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, ) - assert result.pipette_contents_by_id["pipette-id"] is None + assert result.aspirated_volume_by_id["pipette-id"] is None assert result.movement_speed_by_id["pipette-id"] is None assert result.attached_tip_by_id["pipette-id"] is None @@ -276,10 +270,7 @@ def test_handles_pick_up_and_drop_tip(subject: PipetteStore) -> None: pipette_name=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, liquid_presence_detection=None, - ), - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="abc" - ), + ) ), ) ) @@ -291,17 +282,14 @@ def test_handles_pick_up_and_drop_tip(subject: PipetteStore) -> None: pipette_tip_state=update_types.PipetteTipStateUpdate( pipette_id="abc", tip_geometry=TipGeometry(volume=42, length=101, diameter=8.0), - ), - pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( - pipette_id="abc" - ), + ) ), ) ) assert subject.state.attached_tip_by_id["abc"] == TipGeometry( volume=42, length=101, diameter=8.0 ) - assert subject.state.pipette_contents_by_id["abc"] == FluidStack() + assert subject.state.aspirated_volume_by_id["abc"] == 0 subject.handle_action( SucceedCommandAction( @@ -309,15 +297,12 @@ def test_handles_pick_up_and_drop_tip(subject: PipetteStore) -> None: state_update=update_types.StateUpdate( pipette_tip_state=update_types.PipetteTipStateUpdate( pipette_id="abc", tip_geometry=None - ), - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="abc" - ), + ) ), ) ) assert subject.state.attached_tip_by_id["abc"] is None - assert subject.state.pipette_contents_by_id["abc"] is None + assert subject.state.aspirated_volume_by_id["abc"] is None def test_handles_drop_tip_in_place(subject: PipetteStore) -> None: @@ -345,10 +330,7 @@ def test_handles_drop_tip_in_place(subject: PipetteStore) -> None: pipette_name=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, liquid_presence_detection=None, - ), - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="xyz" - ), + ) ), ) ) @@ -359,17 +341,14 @@ def test_handles_drop_tip_in_place(subject: PipetteStore) -> None: pipette_tip_state=update_types.PipetteTipStateUpdate( pipette_id="xyz", tip_geometry=TipGeometry(volume=42, length=101, diameter=8.0), - ), - pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( - pipette_id="xyz" - ), + ) ), ) ) assert subject.state.attached_tip_by_id["xyz"] == TipGeometry( volume=42, length=101, diameter=8.0 ) - assert subject.state.pipette_contents_by_id["xyz"] == FluidStack() + assert subject.state.aspirated_volume_by_id["xyz"] == 0 subject.handle_action( SucceedCommandAction( @@ -377,15 +356,12 @@ def test_handles_drop_tip_in_place(subject: PipetteStore) -> None: state_update=update_types.StateUpdate( pipette_tip_state=update_types.PipetteTipStateUpdate( pipette_id="xyz", tip_geometry=None - ), - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="xyz" - ), + ) ), ) ) assert subject.state.attached_tip_by_id["xyz"] is None - assert subject.state.pipette_contents_by_id["xyz"] is None + assert subject.state.aspirated_volume_by_id["xyz"] is None def test_handles_unsafe_drop_tip_in_place(subject: PipetteStore) -> None: @@ -413,10 +389,7 @@ def test_handles_unsafe_drop_tip_in_place(subject: PipetteStore) -> None: pipette_name=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, liquid_presence_detection=None, - ), - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="xyz" - ), + ) ), ) ) @@ -427,17 +400,14 @@ def test_handles_unsafe_drop_tip_in_place(subject: PipetteStore) -> None: pipette_tip_state=update_types.PipetteTipStateUpdate( pipette_id="xyz", tip_geometry=TipGeometry(volume=42, length=101, diameter=8.0), - ), - pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( - pipette_id="xyz" - ), + ) ), ) ) assert subject.state.attached_tip_by_id["xyz"] == TipGeometry( volume=42, length=101, diameter=8.0 ) - assert subject.state.pipette_contents_by_id["xyz"] == FluidStack() + assert subject.state.aspirated_volume_by_id["xyz"] == 0 subject.handle_action( SucceedCommandAction( @@ -445,46 +415,25 @@ def test_handles_unsafe_drop_tip_in_place(subject: PipetteStore) -> None: state_update=update_types.StateUpdate( pipette_tip_state=update_types.PipetteTipStateUpdate( pipette_id="xyz", tip_geometry=None - ), - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="xyz" - ), + ) ), ) ) assert subject.state.attached_tip_by_id["xyz"] is None - assert subject.state.pipette_contents_by_id["xyz"] is None + assert subject.state.aspirated_volume_by_id["xyz"] is None @pytest.mark.parametrize( - "aspirate_command,aspirate_update", + "aspirate_command", [ - ( - create_aspirate_command(pipette_id="pipette-id", volume=42, flow_rate=1.23), - update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( - pipette_id="pipette-id", - fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=42), - ) - ), - ), - ( - create_aspirate_in_place_command( - pipette_id="pipette-id", volume=42, flow_rate=1.23 - ), - update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( - pipette_id="pipette-id", - fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=42), - ) - ), + create_aspirate_command(pipette_id="pipette-id", volume=42, flow_rate=1.23), + create_aspirate_in_place_command( + pipette_id="pipette-id", volume=42, flow_rate=1.23 ), ], ) def test_aspirate_adds_volume( - subject: PipetteStore, - aspirate_command: cmd.Command, - aspirate_update: update_types.StateUpdate, + subject: PipetteStore, aspirate_command: cmd.Command ) -> None: """It should add volume to pipette after an aspirate.""" load_command = create_load_pipette_command( @@ -492,9 +441,6 @@ def test_aspirate_adds_volume( pipette_name=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, ) - pick_up_tip_command = create_pick_up_tip_command( - pipette_id="pipette-id", tip_volume=42, tip_length=101, tip_diameter=8.0 - ) subject.handle_action( SucceedCommandAction( @@ -505,76 +451,32 @@ def test_aspirate_adds_volume( pipette_name=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, liquid_presence_detection=None, - ), - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="pipette-id" - ), - ), - ) - ) - subject.handle_action( - SucceedCommandAction( - command=pick_up_tip_command, - state_update=update_types.StateUpdate( - pipette_tip_state=update_types.PipetteTipStateUpdate( - pipette_id="pipette-id", - tip_geometry=TipGeometry(volume=42, length=101, diameter=8.0), - ), - pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( - pipette_id="pipette-id" - ), + ) ), ) ) - subject.handle_action( - SucceedCommandAction( - command=aspirate_command, - state_update=aspirate_update, - ) - ) + subject.handle_action(SucceedCommandAction(command=aspirate_command)) - assert subject.state.pipette_contents_by_id["pipette-id"] == FluidStack( - _fluid_stack=[AspiratedFluid(kind=FluidKind.LIQUID, volume=42)] - ) + assert subject.state.aspirated_volume_by_id["pipette-id"] == 42 - subject.handle_action( - SucceedCommandAction(command=aspirate_command, state_update=aspirate_update) - ) + subject.handle_action(SucceedCommandAction(command=aspirate_command)) - assert subject.state.pipette_contents_by_id["pipette-id"] == FluidStack( - _fluid_stack=[AspiratedFluid(kind=FluidKind.LIQUID, volume=84)] - ) + assert subject.state.aspirated_volume_by_id["pipette-id"] == 84 @pytest.mark.parametrize( - "dispense_command,dispense_update", + "dispense_command", [ - ( - create_dispense_command(pipette_id="pipette-id", volume=21, flow_rate=1.23), - update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteEjectedFluidUpdate( - pipette_id="pipette-id", volume=21 - ) - ), - ), - ( - create_dispense_in_place_command( - pipette_id="pipette-id", - volume=21, - flow_rate=1.23, - ), - update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteEjectedFluidUpdate( - pipette_id="pipette-id", volume=21 - ) - ), + create_dispense_command(pipette_id="pipette-id", volume=21, flow_rate=1.23), + create_dispense_in_place_command( + pipette_id="pipette-id", + volume=21, + flow_rate=1.23, ), ], ) def test_dispense_subtracts_volume( - subject: PipetteStore, - dispense_command: cmd.Command, - dispense_update: update_types.StateUpdate, + subject: PipetteStore, dispense_command: cmd.Command ) -> None: """It should subtract volume from pipette after a dispense.""" load_command = create_load_pipette_command( @@ -582,10 +484,6 @@ def test_dispense_subtracts_volume( pipette_name=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, ) - pick_up_tip_command = create_pick_up_tip_command( - pipette_id="pipette-id", tip_volume=47, tip_length=101, tip_diameter=8.0 - ) - aspirate_command = create_aspirate_command( pipette_id="pipette-id", volume=42, @@ -601,51 +499,18 @@ def test_dispense_subtracts_volume( pipette_name=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, liquid_presence_detection=None, - ), - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="pipette-id" - ), - ), - ) - ) - subject.handle_action( - SucceedCommandAction( - command=pick_up_tip_command, - state_update=update_types.StateUpdate( - pipette_tip_state=update_types.PipetteTipStateUpdate( - pipette_id="pipette-id", - tip_geometry=TipGeometry(volume=47, length=101, diameter=8.0), - ), - pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( - pipette_id="pipette-id" - ), - ), - ) - ) - subject.handle_action( - SucceedCommandAction( - command=aspirate_command, - state_update=update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( - pipette_id="pipette-id", - fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=42), ) ), ) ) - subject.handle_action( - SucceedCommandAction(command=dispense_command, state_update=dispense_update) - ) + subject.handle_action(SucceedCommandAction(command=aspirate_command)) + subject.handle_action(SucceedCommandAction(command=dispense_command)) - assert subject.state.pipette_contents_by_id["pipette-id"] == FluidStack( - _fluid_stack=[AspiratedFluid(kind=FluidKind.LIQUID, volume=21)] - ) + assert subject.state.aspirated_volume_by_id["pipette-id"] == 21 - subject.handle_action( - SucceedCommandAction(command=dispense_command, state_update=dispense_update) - ) + subject.handle_action(SucceedCommandAction(command=dispense_command)) - assert subject.state.pipette_contents_by_id["pipette-id"] == FluidStack() + assert subject.state.aspirated_volume_by_id["pipette-id"] == 0 @pytest.mark.parametrize( @@ -665,10 +530,6 @@ def test_blow_out_clears_volume( pipette_name=PipetteNameType.P300_SINGLE, mount=MountType.LEFT, ) - pick_up_tip_command = create_pick_up_tip_command( - pipette_id="pipette-id", tip_volume=47, tip_length=101, tip_diameter=8.0 - ) - aspirate_command = create_aspirate_command( pipette_id="pipette-id", volume=42, @@ -688,43 +549,10 @@ def test_blow_out_clears_volume( ), ) ) - subject.handle_action( - SucceedCommandAction( - command=pick_up_tip_command, - state_update=update_types.StateUpdate( - pipette_tip_state=update_types.PipetteTipStateUpdate( - pipette_id="pipette-id", - tip_geometry=TipGeometry(volume=47, length=101, diameter=8.0), - ), - pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( - pipette_id="pipette-id" - ), - ), - ) - ) - subject.handle_action( - SucceedCommandAction( - command=aspirate_command, - state_update=update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteAspiratedFluidUpdate( - pipette_id="pipette-id", - fluid=AspiratedFluid(kind=FluidKind.LIQUID, volume=42), - ) - ), - ) - ) - subject.handle_action( - SucceedCommandAction( - command=blow_out_command, - state_update=update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="pipette-id" - ) - ), - ) - ) + subject.handle_action(SucceedCommandAction(command=aspirate_command)) + subject.handle_action(SucceedCommandAction(command=blow_out_command)) - assert subject.state.pipette_contents_by_id["pipette-id"] is None + assert subject.state.aspirated_volume_by_id["pipette-id"] is None def test_set_movement_speed(subject: PipetteStore) -> None: @@ -817,30 +645,14 @@ def test_add_pipette_config( @pytest.mark.parametrize( - "previous_cmd,previous_state", + "previous", [ - ( - create_blow_out_command(pipette_id="pipette-id", flow_rate=1.0), - update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="pipette-id" - ) - ), - ), - ( - create_dispense_command(pipette_id="pipette-id", volume=10, flow_rate=1.0), - update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteEjectedFluidUpdate( - pipette_id="pipette-id", volume=10 - ) - ), - ), + create_blow_out_command(pipette_id="pipette-id", flow_rate=1.0), + create_dispense_command(pipette_id="pipette-id", volume=10, flow_rate=1.0), ], ) def test_prepare_to_aspirate_marks_pipette_ready( - subject: PipetteStore, - previous_cmd: cmd.Command, - previous_state: update_types.StateUpdate, + subject: PipetteStore, previous: cmd.Command ) -> None: """It should mark a pipette as ready to aspirate.""" load_pipette_command = create_load_pipette_command( @@ -860,10 +672,7 @@ def test_prepare_to_aspirate_marks_pipette_ready( pipette_name=PipetteNameType.P50_MULTI_FLEX, mount=MountType.LEFT, liquid_presence_detection=None, - ), - pipette_aspirated_fluid=update_types.PipetteUnknownFluidUpdate( - pipette_id="pipette-id" - ), + ) ), ) ) @@ -874,29 +683,19 @@ def test_prepare_to_aspirate_marks_pipette_ready( pipette_tip_state=update_types.PipetteTipStateUpdate( pipette_id="pipette-id", tip_geometry=TipGeometry(volume=42, length=101, diameter=8.0), - ), - pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( - pipette_id="xyz" - ), + ) ), ) ) subject.handle_action( - SucceedCommandAction(command=previous_cmd, state_update=previous_state) + SucceedCommandAction( + command=previous, + ) ) prepare_to_aspirate_command = create_prepare_to_aspirate_command( pipette_id="pipette-id" ) - subject.handle_action( - SucceedCommandAction( - command=prepare_to_aspirate_command, - state_update=update_types.StateUpdate( - pipette_aspirated_fluid=update_types.PipetteEmptyFluidUpdate( - pipette_id="pipette-id" - ) - ), - ) - ) - assert subject.state.pipette_contents_by_id["pipette-id"] == FluidStack() + subject.handle_action(SucceedCommandAction(command=prepare_to_aspirate_command)) + assert subject.state.aspirated_volume_by_id["pipette-id"] == 0.0 diff --git a/api/tests/opentrons/protocol_engine/state/test_pipette_view.py b/api/tests/opentrons/protocol_engine/state/test_pipette_view.py index 60bb528ba85..3b4d04bd967 100644 --- a/api/tests/opentrons/protocol_engine/state/test_pipette_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_pipette_view.py @@ -1,10 +1,8 @@ """Tests for pipette state accessors in the protocol_engine state store.""" from collections import OrderedDict -from typing import cast, Dict, List, Optional, Tuple, NamedTuple import pytest -from decoy import Decoy - +from typing import cast, Dict, List, Optional, Tuple, NamedTuple from opentrons_shared_data.pipette.types import PipetteNameType from opentrons_shared_data.pipette import pipette_definition @@ -32,7 +30,6 @@ BoundingNozzlesOffsets, PipetteBoundingBoxOffsets, ) -from opentrons.protocol_engine.state import fluid_stack from opentrons.hardware_control.nozzle_manager import NozzleMap, NozzleConfigurationType from opentrons.protocol_engine.errors import TipNotAttachedError, PipetteNotLoadedError @@ -59,6 +56,7 @@ def get_pipette_view( pipettes_by_id: Optional[Dict[str, LoadedPipette]] = None, + aspirated_volume_by_id: Optional[Dict[str, Optional[float]]] = None, current_well: Optional[CurrentPipetteLocation] = None, current_deck_point: CurrentDeckPoint = CurrentDeckPoint( mount=None, deck_point=None @@ -69,14 +67,11 @@ def get_pipette_view( flow_rates_by_id: Optional[Dict[str, FlowRates]] = None, nozzle_layout_by_id: Optional[Dict[str, NozzleMap]] = None, liquid_presence_detection_by_id: Optional[Dict[str, bool]] = None, - pipette_contents_by_id: Optional[ - Dict[str, Optional[fluid_stack.FluidStack]] - ] = None, ) -> PipetteView: """Get a pipette view test subject with the specified state.""" state = PipetteState( pipettes_by_id=pipettes_by_id or {}, - pipette_contents_by_id=pipette_contents_by_id or {}, + aspirated_volume_by_id=aspirated_volume_by_id or {}, current_location=current_well, current_deck_point=current_deck_point, attached_tip_by_id=attached_tip_by_id or {}, @@ -239,12 +234,11 @@ def test_get_hardware_pipette_raises_with_name_mismatch() -> None: ) -def test_get_aspirated_volume(decoy: Decoy) -> None: +def test_get_aspirated_volume() -> None: """It should get the aspirate volume for a pipette.""" - stack = decoy.mock(cls=fluid_stack.FluidStack) subject = get_pipette_view( - pipette_contents_by_id={ - "pipette-id": stack, + aspirated_volume_by_id={ + "pipette-id": 42, "pipette-id-none": None, "pipette-id-no-tip": None, }, @@ -254,7 +248,6 @@ def test_get_aspirated_volume(decoy: Decoy) -> None: "pipette-id-no-tip": None, }, ) - decoy.when(stack.aspirated_volume()).then_return(42) assert subject.get_aspirated_volume("pipette-id") == 42 assert subject.get_aspirated_volume("pipette-id-none") is None @@ -333,11 +326,9 @@ def test_get_pipette_working_volume_raises_if_tip_volume_is_none( def test_get_pipette_available_volume( - supported_tip_fixture: pipette_definition.SupportedTipsDefinition, decoy: Decoy + supported_tip_fixture: pipette_definition.SupportedTipsDefinition, ) -> None: """It should get the available volume for a pipette.""" - stack = decoy.mock(cls=fluid_stack.FluidStack) - decoy.when(stack.aspirated_volume()).then_return(58) subject = get_pipette_view( attached_tip_by_id={ "pipette-id": TipGeometry( @@ -346,7 +337,7 @@ def test_get_pipette_available_volume( volume=100, ), }, - pipette_contents_by_id={"pipette-id": stack}, + aspirated_volume_by_id={"pipette-id": 58}, static_config_by_id={ "pipette-id": StaticPipetteConfig( min_volume=1, diff --git a/api/tests/opentrons/protocol_engine/test_protocol_engine.py b/api/tests/opentrons/protocol_engine/test_protocol_engine.py index bc581114ab2..ac83e987153 100644 --- a/api/tests/opentrons/protocol_engine/test_protocol_engine.py +++ b/api/tests/opentrons/protocol_engine/test_protocol_engine.py @@ -1133,18 +1133,21 @@ def test_add_liquid( decoy: Decoy, action_dispatcher: ActionDispatcher, subject: ProtocolEngine, - state_store: StateStore, ) -> None: """It should dispatch an AddLiquidAction action.""" - liquid_obj = Liquid(id="water-id", displayName="water", description="water desc") - decoy.when( - state_store.liquid.validate_liquid_allowed(liquid=liquid_obj) - ).then_return(liquid_obj) subject.add_liquid( id="water-id", name="water", description="water desc", color=None ) - decoy.verify(action_dispatcher.dispatch(AddLiquidAction(liquid=liquid_obj))) + decoy.verify( + action_dispatcher.dispatch( + AddLiquidAction( + liquid=Liquid( + id="water-id", displayName="water", description="water desc" + ) + ) + ) + ) async def test_use_attached_temp_and_mag_modules( diff --git a/app-shell-odd/Makefile b/app-shell-odd/Makefile index 5d2d7ac37bd..543ed2de95f 100644 --- a/app-shell-odd/Makefile +++ b/app-shell-odd/Makefile @@ -65,14 +65,12 @@ deps: .PHONY: package-deps package-deps: clean lib deps -# Note: keep the push dep separate from the dist target so it doesn't accidentally -# do a js dist when we want to only build electron .PHONY: dist-ot3 -dist-ot3: clean lib +dist-ot3: package-deps NO_USB_DETECTION=true OT_APP_DEPLOY_BUCKET=opentrons-app OT_APP_DEPLOY_FOLDER=builds OPENTRONS_PROJECT=$(OPENTRONS_PROJECT) $(builder) --linux --arm64 .PHONY: push-ot3 -push-ot3: dist-ot3 deps +push-ot3: dist-ot3 tar -zcvf opentrons-robot-app.tar.gz -C ./dist/linux-arm64-unpacked/ ./ scp $(if $(ssh_key),-i $(ssh_key)) $(ssh_opts) -r ./opentrons-robot-app.tar.gz root@$(host): ssh $(if $(ssh_key),-i $(ssh_key)) $(ssh_opts) root@$(host) "mount -o remount,rw / && systemctl stop opentrons-robot-app && rm -rf /opt/opentrons-app && mkdir -p /opt/opentrons-app" diff --git a/app-shell-odd/src/main.ts b/app-shell-odd/src/main.ts index 9eb17a016cc..ccb9ff61aa2 100644 --- a/app-shell-odd/src/main.ts +++ b/app-shell-odd/src/main.ts @@ -197,10 +197,7 @@ function installDevtools(): void { log.debug('Installing devtools') - install(extensions, { - loadExtensionOptions: { allowFileAccess: true }, - forceDownload: forceReinstall, - }) + install(extensions, forceReinstall) .then(() => log.debug('Devtools extensions installed')) .catch((error: unknown) => { log.warn('Failed to install devtools extensions', { diff --git a/app-shell/Makefile b/app-shell/Makefile index 74e4e4b1912..5daafd82f44 100644 --- a/app-shell/Makefile +++ b/app-shell/Makefile @@ -121,34 +121,32 @@ package dist-posix dist-osx dist-linux dist-win: export BUILD_ID := $(build_id) package dist-posix dist-osx dist-linux dist-win: export NO_PYTHON := $(if $(no_python_bundle),true,false) package dist-posix dist-osx dist-linux dist-win: export USE_HARD_LINKS := false -# Note: these depend on make -C app dist having been run; do not do this automatically because we separate these -# tasks in CI and even if you have a file dep it's easy to accidentally make the dist run. .PHONY: package -package: +package: package-deps $(builder) --dir .PHONY: dist-posix -dist-posix: clean lib +dist-posix: package-deps $(builder) --linux --mac $(MAKE) _dist-collect-artifacts .PHONY: dist-osx -dist-osx: clean lib +dist-osx: package-deps $(builder) --mac --x64 $(MAKE) _dist-collect-artifacts .PHONY: dist-linux -dist-linux: clean lib +dist-linux: package-deps $(builder) --linux $(MAKE) _dist-collect-artifacts .PHONY: dist-win -dist-win: clean lib +dist-win: package-deps $(builder) --win --x64 $(MAKE) _dist-collect-artifacts .PHONY: dist-ot3 -dist-ot3: clean lib +dist-ot3: package-deps NO_PYTHON=true $(builder) --linux --arm64 --dir cd dist/linux-arm64-unpacked diff --git a/app-shell/src/main.ts b/app-shell/src/main.ts index e09b9d0ae4c..0f4ab41733b 100644 --- a/app-shell/src/main.ts +++ b/app-shell/src/main.ts @@ -145,10 +145,7 @@ function installDevtools(): Promise { log.debug('Installing devtools') if (typeof install === 'function') { - return install(extensions, { - loadExtensionOptions: { allowFileAccess: true }, - forceDownload: forceReinstall, - }) + return install(extensions, forceReinstall) .then(() => log.debug('Devtools extensions installed')) .catch((error: unknown) => { log.warn('Failed to install devtools extensions', { diff --git a/app/src/assets/localization/en/device_details.json b/app/src/assets/localization/en/device_details.json index 17daa2c5955..ee0be003723 100644 --- a/app/src/assets/localization/en/device_details.json +++ b/app/src/assets/localization/en/device_details.json @@ -74,7 +74,7 @@ "instruments_and_modules": "Instruments and Modules", "labware_bottom": "Labware Bottom", "last_run_time": "last run {{number}}", - "left_right": "Left + Right Mounts", + "left_right": "Left+Right Mounts", "left": "left", "lights": "Lights", "link_firmware_update": "View Firmware Update", diff --git a/app/src/assets/localization/en/protocol_command_text.json b/app/src/assets/localization/en/protocol_command_text.json index c2ee88bcd5a..6dbee9af16f 100644 --- a/app/src/assets/localization/en/protocol_command_text.json +++ b/app/src/assets/localization/en/protocol_command_text.json @@ -5,7 +5,6 @@ "absorbance_reader_read": "Reading plate in Absorbance Reader", "adapter_in_mod_in_slot": "{{adapter}} on {{module}} in {{slot}}", "adapter_in_slot": "{{adapter}} in {{slot}}", - "air_gap_in_place": "Air gapping {{volume}} µL", "aspirate": "Aspirating {{volume}} µL from well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec", "aspirate_in_place": "Aspirating {{volume}} µL in place at {{flow_rate}} µL/sec ", "blowout": "Blowing out at well {{well_name}} of {{labware}} in {{labware_location}} at {{flow_rate}} µL/sec", diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/index.ts b/app/src/local-resources/commands/hooks/useCommandTextString/index.ts index 3b77c607052..0eb04ee588e 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/index.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/index.ts @@ -90,7 +90,6 @@ export function useCommandTextString( case 'dropTip': case 'dropTipInPlace': case 'pickUpTip': - case 'airGapInPlace': return { kind: 'generic', commandText: utils.getPipettingCommandText(fullParams), diff --git a/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getPipettingCommandText.ts b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getPipettingCommandText.ts index 2a0d87762b2..6ef1369691e 100644 --- a/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getPipettingCommandText.ts +++ b/app/src/local-resources/commands/hooks/useCommandTextString/utils/commandText/getPipettingCommandText.ts @@ -209,10 +209,6 @@ export const getPipettingCommandText = ({ const { flowRate, volume } = command.params return t('aspirate_in_place', { volume, flow_rate: flowRate }) } - case 'airGapInPlace': { - const { volume } = command.params - return t('air_gap_in_place', { volume }) - } default: { console.warn( 'PipettingCommandText encountered a command with an unrecognized commandType: ', diff --git a/app/src/pages/ODD/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx b/app/src/pages/ODD/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx index 5e78f28b4c9..7f2687e07b7 100644 --- a/app/src/pages/ODD/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx +++ b/app/src/pages/ODD/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx @@ -183,7 +183,7 @@ describe('InstrumentsDashboard', () => { }, } as any) render('/instruments') - screen.getByText('Left + Right Mounts') + screen.getByText('Left+Right Mounts') screen.getByText('extension Mount') }) }) diff --git a/app/vite.config.mts b/app/vite.config.mts index f10fedf4f7e..0d1ccadcc19 100644 --- a/app/vite.config.mts +++ b/app/vite.config.mts @@ -46,11 +46,7 @@ export default defineConfig( }, }, define: { - 'process.env': { - NODE_ENV: process.env.NODE_ENV, - OT_APP_MIXPANEL_ID: process.env.OT_APP_MIXPANEL_ID, - OPENTRONS_PROJECT: process.env.OPENTRONS_PROJECT, - }, + 'process.env': process.env, global: 'globalThis', _PKG_VERSION_: JSON.stringify(version), _OPENTRONS_PROJECT_: JSON.stringify(project), diff --git a/components/src/atoms/InputField/index.tsx b/components/src/atoms/InputField/index.tsx index fc1e1a4560f..14a4f520377 100644 --- a/components/src/atoms/InputField/index.tsx +++ b/components/src/atoms/InputField/index.tsx @@ -2,12 +2,7 @@ import * as React from 'react' import styled, { css } from 'styled-components' import { Flex } from '../../primitives' -import { - ALIGN_CENTER, - DIRECTION_COLUMN, - DIRECTION_ROW, - TEXT_ALIGN_RIGHT, -} from '../../styles' +import { ALIGN_CENTER, DIRECTION_COLUMN, TEXT_ALIGN_RIGHT } from '../../styles' import { BORDERS, COLORS } from '../../helix-design-system' import { Icon } from '../../icons' import { RESPONSIVENESS, SPACING, TYPOGRAPHY } from '../../ui-style-constants' @@ -73,18 +68,10 @@ export interface InputFieldProps { size?: 'medium' | 'small' /** react useRef to control input field instead of react event */ ref?: React.MutableRefObject - /** optional IconName to display icon aligned to left of input field */ leftIcon?: IconName - /** if true, show delete icon aligned to right of input field */ showDeleteIcon?: boolean - /** callback passed to optional delete icon onClick */ onDelete?: () => void - /** if true, style the background of input field to error state */ hasBackgroundError?: boolean - /** optional prop to override input field border radius */ - borderRadius?: string - /** optional prop to override input field padding */ - padding?: string } export const InputField = React.forwardRef( @@ -98,9 +85,6 @@ export const InputField = React.forwardRef( tabIndex = 0, showDeleteIcon = false, hasBackgroundError = false, - onDelete, - borderRadius, - padding, ...inputProps } = props const hasError = props.error != null @@ -122,10 +106,8 @@ export const InputField = React.forwardRef( const INPUT_FIELD = css` display: flex; background-color: ${hasBackgroundError ? COLORS.red30 : COLORS.white}; - border-radius: ${borderRadius != null - ? borderRadius - : BORDERS.borderRadius4}; - padding: ${padding != null ? padding : SPACING.spacing8}; + border-radius: ${BORDERS.borderRadius4}; + padding: ${SPACING.spacing8}; border: ${hasBackgroundError ? 'none' : `1px ${BORDERS.styleSolid} @@ -266,11 +248,7 @@ export const InputField = React.forwardRef( > {title != null ? ( - + ( ) : null} ) : null} - + ( {showDeleteIcon ? ( @@ -352,10 +325,7 @@ export const InputField = React.forwardRef( ) : null} {hasError ? ( - + {props.error} ) : null} diff --git a/components/src/atoms/ListButton/ListButtonChildren/ListButtonRadioButton.tsx b/components/src/atoms/ListButton/ListButtonChildren/ListButtonRadioButton.tsx index 366075df05e..52a58e5f4ec 100644 --- a/components/src/atoms/ListButton/ListButtonChildren/ListButtonRadioButton.tsx +++ b/components/src/atoms/ListButton/ListButtonChildren/ListButtonRadioButton.tsx @@ -1,3 +1,4 @@ +import type * as React from 'react' import styled, { css } from 'styled-components' import { SPACING } from '../../../ui-style-constants' import { BORDERS, COLORS } from '../../../helix-design-system' @@ -5,13 +6,12 @@ import { Flex } from '../../../primitives' import { StyledText } from '../../StyledText' import { CURSOR_POINTER } from '../../../styles' -import type { ChangeEventHandler, MouseEvent } from 'react' import type { StyleProps } from '../../../primitives' interface ListButtonRadioButtonProps extends StyleProps { buttonText: string buttonValue: string | number - onChange: ChangeEventHandler + onChange: React.ChangeEventHandler setNoHover?: () => void setHovered?: () => void disabled?: boolean @@ -34,11 +34,48 @@ export function ListButtonRadioButton( id = buttonText, } = props + const SettingButton = styled.input` + display: none; + ` + + const AVAILABLE_BUTTON_STYLE = css` + background: ${COLORS.white}; + color: ${COLORS.black90}; + + &:hover { + background-color: ${COLORS.grey10}; + } + ` + + const SELECTED_BUTTON_STYLE = css` + background: ${COLORS.blue50}; + color: ${COLORS.white}; + + &:active { + background-color: ${COLORS.blue60}; + } + ` + + const DISABLED_STYLE = css` + color: ${COLORS.grey40}; + background-color: ${COLORS.grey10}; + ` + + const SettingButtonLabel = styled.label` + border-radius: ${BORDERS.borderRadius8}; + cursor: ${CURSOR_POINTER}; + padding: 14px ${SPACING.spacing12}; + width: 100%; + + ${isSelected ? SELECTED_BUTTON_STYLE : AVAILABLE_BUTTON_STYLE} + ${disabled && DISABLED_STYLE} + ` + return ( { + onClick={(e: React.MouseEvent) => { e.stopPropagation() }} > @@ -52,8 +89,6 @@ export function ListButtonRadioButton( /> ) } - -const SettingButton = styled.input` - display: none; -` - -const AVAILABLE_BUTTON_STYLE = css` - background: ${COLORS.white}; - color: ${COLORS.black90}; - - &:hover { - background-color: ${COLORS.grey10}; - } -` - -const SELECTED_BUTTON_STYLE = css` - background: ${COLORS.blue50}; - color: ${COLORS.white}; - - &:active { - background-color: ${COLORS.blue60}; - } -` - -const DISABLED_STYLE = css` - color: ${COLORS.grey40}; - background-color: ${COLORS.grey10}; -` - -interface ButtonLabelProps { - isSelected: boolean - disabled: boolean -} - -const SettingButtonLabel = styled.label` - border-radius: ${BORDERS.borderRadius8}; - cursor: ${CURSOR_POINTER}; - padding: 14px ${SPACING.spacing12}; - width: 100%; - - ${({ isSelected }) => - isSelected ? SELECTED_BUTTON_STYLE : AVAILABLE_BUTTON_STYLE} - ${({ disabled }) => disabled && DISABLED_STYLE} -` diff --git a/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx b/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx index b9e31ad2782..aa04dd91722 100644 --- a/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx +++ b/components/src/atoms/ListItem/ListItemChildren/ListItemCustomize.tsx @@ -18,8 +18,6 @@ interface ListItemCustomizeProps { label?: string dropdown?: DropdownMenuProps tag?: TagProps - /** temporary prop for dropdown menu */ - forceDirection?: boolean } export function ListItemCustomize(props: ListItemCustomizeProps): JSX.Element { @@ -31,7 +29,6 @@ export function ListItemCustomize(props: ListItemCustomizeProps): JSX.Element { linkText, dropdown, tag, - forceDirection = false, } = props return ( @@ -52,9 +49,7 @@ export function ListItemCustomize(props: ListItemCustomizeProps): JSX.Element { {label} ) : null} - {dropdown != null ? ( - - ) : null} + {dropdown != null ? : null} {tag != null ? : null} {onClick != null && linkText != null ? ( diff --git a/components/src/atoms/MenuList/OverflowBtn.tsx b/components/src/atoms/MenuList/OverflowBtn.tsx index efe9195f03d..ec5958746f4 100644 --- a/components/src/atoms/MenuList/OverflowBtn.tsx +++ b/components/src/atoms/MenuList/OverflowBtn.tsx @@ -16,7 +16,7 @@ export const OverflowBtn: ( props: OverflowBtnProps, ref: React.ForwardedRef ): JSX.Element => { - const { fillColor, ...restProps } = props + const { fillColor } = props return ( void @@ -36,15 +29,27 @@ export function EmptySelectorButton( ): JSX.Element { const { onClick, text, iconName, textAlignment, disabled = false } = props + const StyledButton = styled.button` + border: none; + width: ${FLEX_MAX_CONTENT}; + height: ${FLEX_MAX_CONTENT}; + cursor: ${disabled ? CURSOR_DEFAULT : CURSOR_POINTER}; + &:focus-visible { + outline: 2px solid ${COLORS.white}; + box-shadow: 0 0 0 4px ${COLORS.blue50}; + border-radius: ${BORDERS.borderRadius8}; + } + ` + return ( - + ) } - -interface ButtonProps { - disabled: boolean -} - -const StyledButton = styled.button` - border: none; - width: ${FLEX_MAX_CONTENT}; - height: ${FLEX_MAX_CONTENT}; - cursor: ${({ disabled }) => (disabled ? CURSOR_DEFAULT : CURSOR_POINTER)}; - &:focus-visible { - outline: 2px solid ${white}; - box-shadow: 0 0 0 4px ${blue50}; - border-radius: ${borderRadius8}; - } -` diff --git a/components/src/atoms/buttons/RadioButton.tsx b/components/src/atoms/buttons/RadioButton.tsx index f960987f67f..e271f509a6c 100644 --- a/components/src/atoms/buttons/RadioButton.tsx +++ b/components/src/atoms/buttons/RadioButton.tsx @@ -1,20 +1,21 @@ import type * as React from 'react' import styled, { css } from 'styled-components' import { Flex } from '../../primitives' -import { COLORS, BORDERS } from '../../helix-design-system' -import { RESPONSIVENESS, SPACING } from '../../ui-style-constants' import { + ALIGN_CENTER, + BORDERS, + COLORS, CURSOR_DEFAULT, - CURSOR_POINTER, CURSOR_NOT_ALLOWED, + CURSOR_POINTER, DIRECTION_ROW, - ALIGN_CENTER, Icon, + RESPONSIVENESS, + SPACING, StyledText, -} from '../../index' -import type { IconName } from '../../icons' +} from '../..' +import type { IconName } from '../..' import type { StyleProps } from '../../primitives' -import type { FlattenSimpleInterpolation } from 'styled-components' interface RadioButtonProps extends StyleProps { buttonLabel: string | React.ReactNode @@ -27,7 +28,7 @@ interface RadioButtonProps extends StyleProps { radioButtonType?: 'large' | 'small' subButtonLabel?: string id?: string - maxLines?: number + maxLines?: number | null // used for mouseEnter and mouseLeave setNoHover?: () => void setHovered?: () => void @@ -50,12 +51,17 @@ export function RadioButton(props: RadioButtonProps): JSX.Element { : `RadioButtonId_${buttonValue}`, largeDesktopBorderRadius = false, iconName, - maxLines = 1, + maxLines = null, setHovered, setNoHover, } = props + const isLarge = radioButtonType === 'large' + const SettingButton = styled.input` + display: none; + ` + const AVAILABLE_BUTTON_STYLE = css` background: ${COLORS.blue35}; @@ -75,6 +81,46 @@ export function RadioButton(props: RadioButtonProps): JSX.Element { } ` + const DISABLED_BUTTON_STYLE = css` + background-color: ${COLORS.grey35}; + color: ${COLORS.grey50}; + + &:hover, + &:active { + background-color: ${COLORS.grey35}; + } + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + cursor: ${CURSOR_NOT_ALLOWED}; + } + ` + + const SettingButtonLabel = styled.label` + border-radius: ${!largeDesktopBorderRadius + ? BORDERS.borderRadius40 + : BORDERS.borderRadius8}; + cursor: ${CURSOR_POINTER}; + padding: ${SPACING.spacing12} ${SPACING.spacing16}; + width: 100%; + + ${isSelected ? SELECTED_BUTTON_STYLE : AVAILABLE_BUTTON_STYLE} + ${disabled && DISABLED_BUTTON_STYLE} + + &:focus-visible { + outline: 2px solid ${COLORS.blue55}; + } + + @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { + cursor: ${CURSOR_DEFAULT}; + padding: ${isLarge ? SPACING.spacing24 : SPACING.spacing20}; + border-radius: ${BORDERS.borderRadius16}; + display: ${maxLines != null ? '-webkit-box' : undefined}; + -webkit-line-clamp: ${maxLines ?? undefined}; + -webkit-box-orient: ${maxLines != null ? 'vertical' : undefined}; + word-wrap: break-word; + } + ` + const SUBBUTTON_LABEL_STYLE = css` color: ${disabled ? COLORS.grey50 @@ -83,15 +129,6 @@ export function RadioButton(props: RadioButtonProps): JSX.Element { : COLORS.grey60}; ` - const getButtonStyle = ( - isSelected: boolean, - disabled: boolean - ): FlattenSimpleInterpolation => { - if (disabled) return DISABLED_BUTTON_STYLE - if (isSelected) return SELECTED_BUTTON_STYLE - return AVAILABLE_BUTTON_STYLE - } - return ( ) } - -const DISABLED_BUTTON_STYLE = css` - background-color: ${COLORS.grey35}; - color: ${COLORS.grey50}; - - &:hover, - &:active { - background-color: ${COLORS.grey35}; - } - - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - cursor: ${CURSOR_NOT_ALLOWED}; - } -` - -const SettingButton = styled.input` - display: none; -` - -interface SettingsButtonLabelProps { - isSelected: boolean - disabled: boolean - largeDesktopBorderRadius: boolean - isLarge: boolean - maxLines?: number | null -} - -const SettingButtonLabel = styled.label` - border-radius: ${({ largeDesktopBorderRadius }) => - !largeDesktopBorderRadius ? BORDERS.borderRadius40 : BORDERS.borderRadius8}; - cursor: ${CURSOR_POINTER}; - padding: ${SPACING.spacing12} ${SPACING.spacing16}; - width: 100%; - - ${({ disabled }) => disabled && DISABLED_BUTTON_STYLE} - &:focus-visible { - outline: 2px solid ${COLORS.blue55}; - } - - @media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} { - cursor: ${CURSOR_DEFAULT}; - padding: ${({ largeDesktopBorderRadius }) => - largeDesktopBorderRadius ? SPACING.spacing24 : SPACING.spacing20}; - border-radius: ${BORDERS.borderRadius16}; - display: ${({ maxLines }) => (maxLines != null ? '-webkit-box' : 'none')}; - -webkit-line-clamp: ${({ maxLines }) => maxLines ?? 'none'}; - -webkit-box-orient: ${({ maxLines }) => - maxLines != null ? 'vertical' : 'none'}; - word-wrap: break-word; - } -` diff --git a/components/src/hardware-sim/Deck/DeckFromLayers.tsx b/components/src/hardware-sim/Deck/DeckFromLayers.tsx index badd7e80ca1..aaf8f979151 100644 --- a/components/src/hardware-sim/Deck/DeckFromLayers.tsx +++ b/components/src/hardware-sim/Deck/DeckFromLayers.tsx @@ -11,9 +11,9 @@ import { RemovalHandle, ScrewHoles, } from './OT2Layers' -import { ALL_OT2_DECK_LAYERS } from './constants' import type { RobotType } from '@opentrons/shared-data' +import { ALL_OT2_DECK_LAYERS } from './constants' export interface DeckFromLayersProps { robotType: RobotType @@ -21,18 +21,18 @@ export interface DeckFromLayersProps { } const OT2_LAYER_MAP: { - [layer in typeof ALL_OT2_DECK_LAYERS[number]]: () => JSX.Element + [layer in typeof ALL_OT2_DECK_LAYERS[number]]: JSX.Element } = { - fixedBase: () => , - fixedTrash: () => , - doorStops: () => , - metalFrame: () => , - removableDeckOutline: () => , - slotRidges: () => , - slotNumbers: () => , - calibrationMarkings: () => , - removalHandle: () => , - screwHoles: () => , + fixedBase: , + fixedTrash: , + doorStops: , + metalFrame: , + removableDeckOutline: , + slotRidges: , + slotNumbers: , + calibrationMarkings: , + removalHandle: , + screwHoles: , } /** @@ -47,12 +47,10 @@ export function DeckFromLayers(props: DeckFromLayersProps): JSX.Element | null { return ( - {ALL_OT2_DECK_LAYERS.filter(layer => !layerBlocklist.includes(layer)).map( - layer => { - const LayerComponent = OT2_LAYER_MAP[layer] - return - } - )} + {ALL_OT2_DECK_LAYERS.reduce((acc, layer) => { + if (layerBlocklist.includes(layer)) return acc + return [...acc, OT2_LAYER_MAP[layer]] + }, [])} ) } diff --git a/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpaceWithRef.tsx b/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpaceWithRef.tsx index a777299fb1c..530ed004532 100644 --- a/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpaceWithRef.tsx +++ b/components/src/hardware-sim/RobotCoordinateSpace/RobotCoordinateSpaceWithRef.tsx @@ -35,18 +35,18 @@ export function RobotCoordinateSpaceWithRef( {} ) - const PADDING = deckDef.otId === 'ot2_standard' ? 5 : 10 if (deckDef.otId === 'ot2_standard') { + const PADDING = 5 wholeDeckViewBox = `${viewBoxOriginX - PADDING} ${ viewBoxOriginY + PADDING * 5 } ${deckXDimension + PADDING * 2} ${deckYDimension - PADDING * 10}` } else { - wholeDeckViewBox = `${viewBoxOriginX + PADDING * 2} ${ - viewBoxOriginY - PADDING - } ${deckXDimension + PADDING * 4} ${deckYDimension + PADDING * 3}` + const PADDING = 20 + wholeDeckViewBox = `${viewBoxOriginX - PADDING} ${ + viewBoxOriginY + PADDING + } ${deckXDimension + PADDING * 2} ${deckYDimension + PADDING * 2}` } } - return ( /** optional disabled */ disabled?: boolean - /** force direction for pd after release this will be fixed and remove */ - forceDirection?: boolean } // TODO: (smb: 4/15/22) refactor this to use html select for accessibility @@ -90,7 +88,6 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { disabled = false, onFocus, onBlur, - forceDirection = false, } = props const [targetProps, tooltipProps] = useHoverTooltip() const [showDropdownMenu, setShowDropdownMenu] = React.useState(false) @@ -108,7 +105,6 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { }) React.useEffect(() => { - if (forceDirection) return const handlePositionCalculation = (): void => { const dropdownRect = dropDownMenuWrapperRef.current?.getBoundingClientRect() if (dropdownRect != null) { @@ -208,11 +204,7 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { gridGap={SPACING.spacing4} > {title !== null ? ( - + {closeButton} diff --git a/opentrons-ai-client/src/OpentronsAI.tsx b/opentrons-ai-client/src/OpentronsAI.tsx index e4bb9cca5fb..1f91faf0eed 100644 --- a/opentrons-ai-client/src/OpentronsAI.tsx +++ b/opentrons-ai-client/src/OpentronsAI.tsx @@ -20,7 +20,6 @@ import { CLIENT_MAX_WIDTH } from './resources/constants' import { Footer } from './molecules/Footer' import { HeaderWithMeter } from './molecules/HeaderWithMeter' import styled from 'styled-components' -import { ExitConfirmModal } from './molecules/ExitConfirmModal' export function OpentronsAI(): JSX.Element | null { const { isAuthenticated, isLoading, loginWithRedirect } = useAuth0() @@ -95,7 +94,6 @@ export function OpentronsAI(): JSX.Element | null { flex={1} > - diff --git a/opentrons-ai-client/src/OpentronsAIRoutes.tsx b/opentrons-ai-client/src/OpentronsAIRoutes.tsx index 32d09f351cf..b790b262ebe 100644 --- a/opentrons-ai-client/src/OpentronsAIRoutes.tsx +++ b/opentrons-ai-client/src/OpentronsAIRoutes.tsx @@ -1,18 +1,10 @@ import { Route, Navigate, Routes } from 'react-router-dom' import { Landing } from './pages/Landing' -import { UpdateProtocol } from './organisms/UpdateProtocol' import type { RouteProps } from './resources/types' -import { Chat } from './pages/Chat' import { CreateProtocol } from './pages/CreateProtocol' const opentronsAIRoutes: RouteProps[] = [ - { - Component: Chat, - name: 'Chat', - navLinkTo: '/chat', - path: '/chat', - }, { Component: CreateProtocol, name: 'Create A New Protocol', @@ -20,7 +12,7 @@ const opentronsAIRoutes: RouteProps[] = [ path: '/new-protocol', }, { - Component: UpdateProtocol, + Component: Landing, name: 'Update An Existing Protocol', navLinkTo: '/update-protocol', path: '/update-protocol', diff --git a/opentrons-ai-client/src/assets/images/modules/MagneticBlock_GEN1_HERO.png b/opentrons-ai-client/src/assets/images/modules/MagneticBlock_GEN1_HERO.png deleted file mode 100644 index 180dd977498..00000000000 Binary files a/opentrons-ai-client/src/assets/images/modules/MagneticBlock_GEN1_HERO.png and /dev/null differ diff --git a/opentrons-ai-client/src/assets/images/modules/heater_shaker_module_transparent.png b/opentrons-ai-client/src/assets/images/modules/heater_shaker_module_transparent.png deleted file mode 100644 index beefd651e45..00000000000 Binary files a/opentrons-ai-client/src/assets/images/modules/heater_shaker_module_transparent.png and /dev/null differ diff --git a/opentrons-ai-client/src/assets/images/modules/heatershaker.png b/opentrons-ai-client/src/assets/images/modules/heatershaker.png deleted file mode 100644 index 1df848f09e1..00000000000 Binary files a/opentrons-ai-client/src/assets/images/modules/heatershaker.png and /dev/null differ diff --git a/opentrons-ai-client/src/assets/images/modules/mag_block.png b/opentrons-ai-client/src/assets/images/modules/mag_block.png deleted file mode 100644 index 474f5775dd5..00000000000 Binary files a/opentrons-ai-client/src/assets/images/modules/mag_block.png and /dev/null differ diff --git a/opentrons-ai-client/src/assets/images/modules/magdeck_gen1.png b/opentrons-ai-client/src/assets/images/modules/magdeck_gen1.png deleted file mode 100644 index 7243441981a..00000000000 Binary files a/opentrons-ai-client/src/assets/images/modules/magdeck_gen1.png and /dev/null differ diff --git a/opentrons-ai-client/src/assets/images/modules/magdeck_gen2.png b/opentrons-ai-client/src/assets/images/modules/magdeck_gen2.png deleted file mode 100644 index ec8bd0d0d79..00000000000 Binary files a/opentrons-ai-client/src/assets/images/modules/magdeck_gen2.png and /dev/null differ diff --git a/opentrons-ai-client/src/assets/images/modules/magdeck_tempdeck_combined.png b/opentrons-ai-client/src/assets/images/modules/magdeck_tempdeck_combined.png deleted file mode 100644 index c2dbc55a869..00000000000 Binary files a/opentrons-ai-client/src/assets/images/modules/magdeck_tempdeck_combined.png and /dev/null differ diff --git a/opentrons-ai-client/src/assets/images/modules/module_pipette_collision_warning.png b/opentrons-ai-client/src/assets/images/modules/module_pipette_collision_warning.png deleted file mode 100644 index 788d4b5b932..00000000000 Binary files a/opentrons-ai-client/src/assets/images/modules/module_pipette_collision_warning.png and /dev/null differ diff --git a/opentrons-ai-client/src/assets/images/modules/temp_deck_gen_2_transparent.png b/opentrons-ai-client/src/assets/images/modules/temp_deck_gen_2_transparent.png deleted file mode 100644 index 6f8799ea74a..00000000000 Binary files a/opentrons-ai-client/src/assets/images/modules/temp_deck_gen_2_transparent.png and /dev/null differ diff --git a/opentrons-ai-client/src/assets/images/modules/tempdeck_gen1.png b/opentrons-ai-client/src/assets/images/modules/tempdeck_gen1.png deleted file mode 100644 index 668c1d7d911..00000000000 Binary files a/opentrons-ai-client/src/assets/images/modules/tempdeck_gen1.png and /dev/null differ diff --git a/opentrons-ai-client/src/assets/images/modules/tempdeck_gen2.png b/opentrons-ai-client/src/assets/images/modules/tempdeck_gen2.png deleted file mode 100644 index aaf5948e2ee..00000000000 Binary files a/opentrons-ai-client/src/assets/images/modules/tempdeck_gen2.png and /dev/null differ diff --git a/opentrons-ai-client/src/assets/images/modules/thermocycler.png b/opentrons-ai-client/src/assets/images/modules/thermocycler.png deleted file mode 100644 index fdae9b79c49..00000000000 Binary files a/opentrons-ai-client/src/assets/images/modules/thermocycler.png and /dev/null differ diff --git a/opentrons-ai-client/src/assets/images/modules/thermocycler_gen2.png b/opentrons-ai-client/src/assets/images/modules/thermocycler_gen2.png deleted file mode 100644 index e17a723c5d9..00000000000 Binary files a/opentrons-ai-client/src/assets/images/modules/thermocycler_gen2.png and /dev/null differ diff --git a/opentrons-ai-client/src/assets/localization/en/create_protocol.json b/opentrons-ai-client/src/assets/localization/en/create_protocol.json index 6c891525041..5bf2d5d6e23 100644 --- a/opentrons-ai-client/src/assets/localization/en/create_protocol.json +++ b/opentrons-ai-client/src/assets/localization/en/create_protocol.json @@ -11,30 +11,7 @@ "application_describe_caption": "Example: “The protocol performs automated liquid handling for Pierce BCA Protein Assay Kit to determine protein concentrations in various sample types, such as cell lysates and eluates of purification process.", "section_confirm_button": "Confirm", "instruments_title": "Instruments", - "instruments_robot_title": "What robot would you like to use?", - "opentrons_flex_label": "Opentrons Flex", - "opentrons_flex": "Opentrons Flex", - "opentrons_ot2_label": "Opentrons OT-2", - "opentrons_ot2": "Opentrons OT-2", - "instruments_pipettes_title": "What pipettes would you like to use?", - "two_pipettes_label": "Two pipettes", - "right_pipette_label": "Right mount", - "left_pipette_label": "Left mount", - "choose_pipette_placeholder": "Choose pipette", - "96_channel_1000ul_pipette_label": "96-Channel 1000µL pipette", - "96_channel_1000ul_pipette": "96-Channel 1000µL pipette", - "instruments_flex_gripper_title": "Do you want to use the Flex Gripper?", - "flex_gripper_yes_label": "Yes, use the Flex Gripper", - "flex_gripper": "Flex Gripper", - "flex_gripper_no_label": "No, do not use the Flex Gripper", "modules_title": "Modules", - "no_modules_added_yet": "No modules added yet", - "modules_remove_label": "remove", - "modules_adapter_label": "Adapter", - "heater_shaker_module_v1": "Heater-Shaker Module GEN1", - "temperature_module_v2": "Temperature Module GEN2", - "thermocycler_module_v2": "Thermocycler Module GEN2", - "magnetic_module_v1": "Magnetic Block GEN1", "labware_liquids_title": "Labware & Liquids", "steps_title": "Steps" } diff --git a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json index e321b939ade..6bf5b633936 100644 --- a/opentrons-ai-client/src/assets/localization/en/protocol_generator.json +++ b/opentrons-ai-client/src/assets/localization/en/protocol_generator.json @@ -3,19 +3,10 @@ "api": "API: An API level is 2.15", "application": "Application: Your protocol's name, describing what it does.", "commands": "Commands: List the protocol's steps, specifying quantities in microliters (uL) and giving exact source and destination locations.", - "cancel": "Cancel", "copyright": "Copyright © 2024 Opentrons", "copy_code": "Copy code", - "choose_file": "Choose file", "disclaimer": "OpentronsAI can make mistakes. Review your protocol before running it on an Opentrons robot.", - "drag_and_drop": "Drag and drop or browse your files", "example": "For example prompts, click the buttons in the left panel.", - "file_length_error": "The length of the file contents is 0. Please upload a file with content.", - "exit": "Exit", - "exit_confirmation_title": "Are you sure you want to exit?", - "exit_confirmation_body": "Exiting now will discard your progress.", - "exit_confirmation_cancel": "Continue editing", - "exit_confirmation_exit": "Exit without saving", "got_feedback": "Got feedback? We love to hear it.", "key_info": "Here are some key pieces of information to provide in your prompt:", "labware_and_tipracks": "Labware and tip racks: Use names from the Opentrons Labware Library.", @@ -30,10 +21,6 @@ "login": "Login", "logout": "Logout", "make_sure_your_prompt": "Write a prompt in a natural language for OpentronsAI to generate a protocol using the Opentrons Python Protocol API v2. The better the prompt, the better the quality of the protocol produced by OpentronsAI.", - "modify_intro": "Modify the following Python code using the Opentrons Python Protocol API v2. Ensure that the new labware and pipettes are compatible with the Flex robot. Ensure that you perform the correct Type of Update use the Details of Changes.\n\n", - "modify_python_code": "Original Python Code:\n", - "modify_type_of_update": "Type of update:\n- ", - "modify_details_of_change": "Details of Changes:\n- ", "modules_and_adapters": "Modules and adapters: Specify the modules and labware adapters required by your protocol.", "notes": "A few important things to note:", "opentrons": "Opentrons", @@ -43,26 +30,17 @@ "pcr": "PCR", "pipettes": "Pipettes: Specify your pipettes, including the volume, number of channels, and whether they’re mounted on the left or right.", "privacy_policy": "By continuing, you agree to the Opentrons Privacy Policy and End user license agreement", - "protocol_file": "Protocol file", - "provide_details_of_changes": "Provide details of changes you want to make", - "python_file_type_error": "Python file type required", "reagent_transfer_flex": "Reagent Transfer (Flex)", "reagent_transfer": "Reagent Transfer", "reload_page": "To start over and create a new protocol, simply reload the page.", "robot_type": "Robot type: Choose the OT-2 or Opentrons Flex.", "robot": "Robot: OT-2.", "share_your_thoughts": "Share your thoughts here", - "send_feedback": "Send feedback", - "send_feedback_input_title": "Share why the response was not helpful", - "send_feedback_to_opentrons": "Send feedback to Opentrons", "side_panel_body": "Write a prompt in natural language to generate a Reagent Transfer or a PCR protocol for the OT-2 or Opentrons Flex using the Opentrons Python Protocol API.", "side_panel_header": "Use natural language to generate protocols with OpentronsAI powered by OpenAI", "simulate_description": "Once OpentronsAI has written your protocol, type `simulate` in the prompt box to try it out.", - "submit_prompt": "Submit prompt", "try_example_prompts": "Stuck? Try these example prompts to get started.", - "type_of_update": "Type of update", "type_your_prompt": "Type your prompt...", - "update_existing_protocol": "Update an existing protocol", "well_allocations": "Well allocations: Describe where liquids should go in labware.", "what_if_you": "What if you don’t provide all of those pieces of information? OpentronsAI asks you to provide it!", "what_typeof_protocol": "What type of protocol do you need?", diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx index 7836d18f90f..afec6d800cc 100644 --- a/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx +++ b/opentrons-ai-client/src/molecules/ChatDisplay/__tests__/ChatDisplay.test.tsx @@ -5,24 +5,9 @@ import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { ChatDisplay } from '../index' -import { useForm, FormProvider } from 'react-hook-form' - -const RenderChatDisplay = (props: React.ComponentProps) => { - const methods = useForm({ - defaultValues: {}, - }) - - return ( - - - - ) -} const render = (props: React.ComponentProps) => { - return renderWithProviders(, { - i18nInstance: i18n, - }) + return renderWithProviders(, { i18nInstance: i18n }) } describe('ChatDisplay', () => { @@ -33,7 +18,6 @@ describe('ChatDisplay', () => { chat: { role: 'assistant', reply: 'mock text from the backend', - requestId: '12351234', }, chatId: 'mockId', } @@ -51,7 +35,6 @@ describe('ChatDisplay', () => { chat: { role: 'user', reply: 'mock text from user input', - requestId: '12351234', }, chatId: 'mockId', } diff --git a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx index 22dbee37f1a..4eaa840dbcb 100644 --- a/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx +++ b/opentrons-ai-client/src/molecules/ChatDisplay/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useState } from 'react' import styled from 'styled-components' import { useTranslation } from 'react-i18next' import Markdown from 'react-markdown' @@ -12,81 +12,27 @@ import { JUSTIFY_CENTER, JUSTIFY_FLEX_END, JUSTIFY_FLEX_START, + POSITION_ABSOLUTE, POSITION_RELATIVE, + PrimaryButton, SPACING, LegacyStyledText, TYPOGRAPHY, - StyledText, - DIRECTION_ROW, - OVERFLOW_AUTO, } from '@opentrons/components' import type { ChatData } from '../../resources/types' -import { useAtom } from 'jotai' -import { - chatDataAtom, - feedbackModalAtom, - scrollToBottomAtom, -} from '../../resources/atoms' -import { delay } from 'lodash' -import { useFormContext } from 'react-hook-form' interface ChatDisplayProps { chat: ChatData chatId: string } -const HoverShadow = styled(Flex)` - alignitems: ${ALIGN_CENTER}; - justifycontent: ${JUSTIFY_CENTER}; - padding: ${SPACING.spacing8}; - transition: box-shadow 0.3s ease; - border-radius: ${BORDERS.borderRadius8}; - - &:hover { - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); - border-radius: ${BORDERS.borderRadius8}; - } -` - -const StyledIcon = styled(Icon)` - color: ${COLORS.blue50}; -` - export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element { const { t } = useTranslation('protocol_generator') const [isCopied, setIsCopied] = useState(false) - const [, setShowFeedbackModal] = useAtom(feedbackModalAtom) - const { setValue } = useFormContext() - const [chatdata] = useAtom(chatDataAtom) - const [scrollToBottom, setScrollToBottom] = useAtom(scrollToBottomAtom) - const { role, reply, requestId } = chat + const { role, reply } = chat const isUser = role === 'user' - const setInputFieldToCorrespondingRequest = (): void => { - const prompt = chatdata.find( - chat => chat.role === 'user' && chat.requestId === requestId - )?.reply - setScrollToBottom(!scrollToBottom) - setValue('userPrompt', prompt) - } - - const handleFileDownload = (): void => { - const lastCodeBlock = document.querySelector(`#${chatId}`) - const code = lastCodeBlock?.textContent ?? '' - const blobParts: BlobPart[] = [code] - - const file = new File(blobParts, 'OpentronsAI.py', { type: 'text/python' }) - const url = URL.createObjectURL(file) - const a = document.createElement('a') - - document.body.appendChild(a) - a.href = url - a.download = 'OpentronsAI.py' - a.click() - window.URL.revokeObjectURL(url) - } - const handleClickCopy = async (): Promise => { const lastCodeBlock = document.querySelector(`#${chatId}`) const code = lastCodeBlock?.textContent ?? '' @@ -94,34 +40,29 @@ export function ChatDisplay({ chat, chatId }: ChatDisplayProps): JSX.Element { setIsCopied(true) } - useEffect(() => { - if (isCopied) - delay(() => { - setIsCopied(false) - }, 2000) - }, [isCopied]) - function CodeText(props: JSX.IntrinsicAttributes): JSX.Element { return } return ( - + - + {isUser ? t('you') : t('opentronsai')} - + {/* text should be markdown so this component will have a package or function to parse markdown */} {!isUser ? ( - - { - setInputFieldToCorrespondingRequest() - }} - > - - - { - setShowFeedbackModal(true) - }} - > - - - { - await handleClickCopy() - }} - > - + - - { - handleFileDownload() - }} - > - - - + + ) : null} diff --git a/opentrons-ai-client/src/molecules/ChatFooter/index.tsx b/opentrons-ai-client/src/molecules/ChatFooter/index.tsx index fef7596f6f4..b477da1dacd 100644 --- a/opentrons-ai-client/src/molecules/ChatFooter/index.tsx +++ b/opentrons-ai-client/src/molecules/ChatFooter/index.tsx @@ -15,9 +15,9 @@ export function ChatFooter(): JSX.Element { return ( @@ -32,4 +32,5 @@ const DISCLAIMER_TEXT_STYLE = css` font-size: ${TYPOGRAPHY.fontSize20}; line-height: ${TYPOGRAPHY.lineHeight24}; text-align: ${TYPOGRAPHY.textAlignCenter}; + padding-bottom: ${SPACING.spacing24}; ` diff --git a/opentrons-ai-client/src/molecules/ControlledEmptySelectorButtonGroup/__tests__/ControlledEmptySelectorButtonGroup.test.tsx b/opentrons-ai-client/src/molecules/ControlledEmptySelectorButtonGroup/__tests__/ControlledEmptySelectorButtonGroup.test.tsx deleted file mode 100644 index efb82f1a482..00000000000 --- a/opentrons-ai-client/src/molecules/ControlledEmptySelectorButtonGroup/__tests__/ControlledEmptySelectorButtonGroup.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { ControlledEmptySelectorButtonGroup } from '../index' -import { describe, it, expect } from 'vitest' -import { fireEvent, screen } from '@testing-library/react' -import { FormProvider, useForm } from 'react-hook-form' -import { MODULES_FIELD_NAME } from '../../../organisms/ModulesSection' -import type { DisplayModules } from '../../../organisms/ModulesSection' - -const modulesMock: DisplayModules[] = [ - { - type: 'heaterShakerModuleType', - model: 'heaterShakerModuleV1', - name: 'Heater-Shaker Module GEN1', - }, - { - type: 'temperatureModuleType', - model: 'temperatureModuleV2', - name: 'Temperature Module GEN2', - }, -] - -const TestFormProviderComponent = () => { - const methods = useForm({}) - - const selectedValue = methods.watch(MODULES_FIELD_NAME) ?? [] - - return ( - - - - {'selected values: ' + selectedValue.map((m: DisplayModules) => m.name)} - - ) -} - -const render = (): ReturnType => { - return renderWithProviders(, { - i18nInstance: i18n, - }) -} - -describe('ControlledEmptySelectorButtonGroup', () => { - it('should render ControlledEmptySelectorButtonGroup component', () => { - render() - - screen.getByText('Heater-Shaker Module GEN1') - screen.getByText('Temperature Module GEN2') - }) - - it('should add the value when the button is clicked', async () => { - render() - - const button1 = screen.getByText('Heater-Shaker Module GEN1') - - expect( - screen.queryByText( - 'selected values: Heater-Shaker Module GEN1,Temperature Module GEN2' - ) - ).not.toBeInTheDocument() - - fireEvent.click(button1) - - const button2 = screen.getByText('Temperature Module GEN2') - - fireEvent.click(button2) - - expect( - await screen.findByText( - 'selected values: Heater-Shaker Module GEN1,Temperature Module GEN2' - ) - ).toBeInTheDocument() - }) -}) diff --git a/opentrons-ai-client/src/molecules/ControlledEmptySelectorButtonGroup/index.tsx b/opentrons-ai-client/src/molecules/ControlledEmptySelectorButtonGroup/index.tsx deleted file mode 100644 index ad9791f1fcd..00000000000 --- a/opentrons-ai-client/src/molecules/ControlledEmptySelectorButtonGroup/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Flex, WRAP, SPACING, EmptySelectorButton } from '@opentrons/components' -import { Controller, useFormContext } from 'react-hook-form' -import type { DisplayModules } from '../../organisms/ModulesSection' -import { MODULES_FIELD_NAME } from '../../organisms/ModulesSection' - -export function ControlledEmptySelectorButtonGroup({ - modules, -}: { - modules: DisplayModules[] -}): JSX.Element | null { - const { watch } = useFormContext() - const modulesWatch: DisplayModules[] = watch(MODULES_FIELD_NAME) ?? [] - - return ( - { - return ( - - {modules.map(module => ( - { - if (modulesWatch.some(m => m.type === module.type)) { - return - } - field.onChange([...modulesWatch, module]) - }} - text={module.name} - textAlignment="left" - /> - ))} - - ) - }} - /> - ) -} diff --git a/opentrons-ai-client/src/molecules/ControlledRadioButtonGroup/__tests__/ControlledRadioButtonGroup.test.tsx b/opentrons-ai-client/src/molecules/ControlledRadioButtonGroup/__tests__/ControlledRadioButtonGroup.test.tsx deleted file mode 100644 index 22bae21beae..00000000000 --- a/opentrons-ai-client/src/molecules/ControlledRadioButtonGroup/__tests__/ControlledRadioButtonGroup.test.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { ControlledRadioButtonGroup } from '../index' -import { describe, it, expect } from 'vitest' -import { fireEvent, screen } from '@testing-library/react' -import { FormProvider, useForm } from 'react-hook-form' - -const radioButtonsMock = [ - { - id: 'radio1', - buttonLabel: 'Radio Label 1', - buttonValue: 'value 1', - }, - { - id: 'radio2', - buttonLabel: 'Radio Label 2', - buttonValue: 'value 2', - }, -] - -const TestFormProviderComponent = () => { - const methods = useForm({}) - - const selectedValue = methods.watch('radio-button-group-name') ?? 'none' - - return ( - - - - {'selected value: ' + selectedValue} - - ) -} - -const render = (): ReturnType => { - return renderWithProviders(, { - i18nInstance: i18n, - }) -} - -describe('ControlledRadioButtonGroup', () => { - it('should render ControlledRadioButtonGroup component', () => { - render() - - screen.getByText('Radio Label 1') - screen.getByText('Radio Label 2') - }) - - it('should select the correct option initially', () => { - const { rerender } = render() - - const radio1 = screen.getByLabelText('Radio Label 1') - const radio2 = screen.getByLabelText('Radio Label 2') - - expect(radio1).toBeChecked() - expect(radio2).not.toBeChecked() - - rerender() - - expect(screen.getByText('selected value: value 1')).toBeInTheDocument() - }) - - it('should change the selected value when the second radio is clicked', () => { - render() - - const radio1 = screen.getByLabelText('Radio Label 1') - const radio2 = screen.getByLabelText('Radio Label 2') - - expect(radio1).toBeChecked() - expect(radio2).not.toBeChecked() - - expect( - screen.queryByText('selected value: value 2') - ).not.toBeInTheDocument() - - fireEvent.click(radio2) - - expect(screen.getByLabelText('Radio Label 1')).not.toBeChecked() - expect(screen.getByLabelText('Radio Label 2')).toBeChecked() - - expect(screen.getByText('selected value: value 2')).toBeInTheDocument() - }) -}) diff --git a/opentrons-ai-client/src/molecules/ControlledRadioButtonGroup/index.tsx b/opentrons-ai-client/src/molecules/ControlledRadioButtonGroup/index.tsx deleted file mode 100644 index 9245c3a1a1a..00000000000 --- a/opentrons-ai-client/src/molecules/ControlledRadioButtonGroup/index.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { - COLORS, - DIRECTION_COLUMN, - DIRECTION_ROW, - Flex, - RadioButton, - SPACING, - StyledText, -} from '@opentrons/components' -import { Controller } from 'react-hook-form' - -interface ControlledRadioButtonGroupProps { - id?: string - name: string - title?: string - defaultValue?: string - rules?: any - radioButtons: Array<{ - id?: string - buttonLabel: string - buttonValue: string - }> -} - -export function ControlledRadioButtonGroup({ - id, - name, - title = '', - defaultValue = '', - rules, - radioButtons, -}: ControlledRadioButtonGroupProps): JSX.Element { - return ( - - {title !== '' && ( - - {title} - - )} - { - return ( - - {radioButtons.map((radioButton, index) => ( - { - field.onChange(e) - }} - /> - ))} - - ) - }} - /> - - ) -} diff --git a/opentrons-ai-client/src/molecules/ExitConfirmModal/__tests__/ExitConfirmModal.test.tsx b/opentrons-ai-client/src/molecules/ExitConfirmModal/__tests__/ExitConfirmModal.test.tsx deleted file mode 100644 index 1dc349c0507..00000000000 --- a/opentrons-ai-client/src/molecules/ExitConfirmModal/__tests__/ExitConfirmModal.test.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { ExitConfirmModal } from '../index' -import { describe, it, vi, expect } from 'vitest' -import { fireEvent, screen } from '@testing-library/react' -import type { NavigateFunction } from 'react-router-dom' -import { displayExitConfirmModalAtom } from '../../../resources/atoms' - -const mockNavigate = vi.fn() - -vi.mock('react-router-dom', async importOriginal => { - const actual = await importOriginal() - return { - ...actual, - useNavigate: () => mockNavigate, - } -}) - -const initialValuesMock = [[displayExitConfirmModalAtom, true]] - -const render = (): ReturnType => { - return renderWithProviders(, { - initialValues: initialValuesMock as any, - i18nInstance: i18n, - }) -} - -describe('ExitConfirmModal', () => { - it('should render ExitConfirmModal component', () => { - render() - - screen.getByText('Are you sure you want to exit?') - screen.getByText('Exiting now will discard your progress.') - }) - - it('should close modal when continue button is clicked', () => { - render() - - const continueButton = screen.getByText('Continue editing') - fireEvent.click(continueButton) - - expect(mockNavigate).not.toHaveBeenCalled() - expect( - screen.queryByText('Are you sure you want to exit?') - ).not.toBeInTheDocument() - }) - - it('should close modal and navigate to / when exit button is clicked', () => { - render() - - const exitButton = screen.getByText('Exit without saving') - fireEvent.click(exitButton) - - expect(mockNavigate).toHaveBeenCalledWith('/') - expect( - screen.queryByText('Are you sure you want to exit?') - ).not.toBeInTheDocument() - }) -}) diff --git a/opentrons-ai-client/src/molecules/ExitConfirmModal/index.tsx b/opentrons-ai-client/src/molecules/ExitConfirmModal/index.tsx deleted file mode 100644 index b975ddec40c..00000000000 --- a/opentrons-ai-client/src/molecules/ExitConfirmModal/index.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { - AlertPrimaryButton, - DIRECTION_COLUMN, - Flex, - JUSTIFY_FLEX_END, - Modal, - SecondaryButton, - SPACING, - StyledText, -} from '@opentrons/components' -import { useAtom } from 'jotai' -import { displayExitConfirmModalAtom } from '../../resources/atoms' -import { useNavigate } from 'react-router-dom' -import { useTranslation } from 'react-i18next' - -export function ExitConfirmModal(): JSX.Element { - const [ - displayExitConfirmModalState, - setDisplayExitConfirmModalState, - ] = useAtom(displayExitConfirmModalAtom) - const navigate = useNavigate() - const { t } = useTranslation('protocol_generator') - - if (!displayExitConfirmModalState) { - return <> - } - - function handleContinueClick(): void { - setDisplayExitConfirmModalState(false) - } - - function handleExitClick(): void { - setDisplayExitConfirmModalState(false) - navigate('/') - } - - return ( - - - - {t('exit_confirmation_body')} - - - - {t('exit_confirmation_cancel')} - - - {t('exit_confirmation_exit')} - - - - - ) -} diff --git a/opentrons-ai-client/src/molecules/FeedbackModal/__tests__/FeedbackModal.test.tsx b/opentrons-ai-client/src/molecules/FeedbackModal/__tests__/FeedbackModal.test.tsx deleted file mode 100644 index 15d17938e93..00000000000 --- a/opentrons-ai-client/src/molecules/FeedbackModal/__tests__/FeedbackModal.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { FeedbackModal } from '..' -import { renderWithProviders } from '../../../__testing-utils__' -import { screen } from '@testing-library/react' -import { describe, it, expect } from 'vitest' -import { i18n } from '../../../i18n' -import { feedbackModalAtom } from '../../../resources/atoms' - -const initialValues: Array<[any, any]> = [[feedbackModalAtom, true]] - -const render = (): ReturnType => { - return renderWithProviders(, { - i18nInstance: i18n, - initialValues, - }) -} - -describe('FeedbackModal', () => { - it('should render Feedback modal', () => { - render() - screen.getByText('Send feedback to Opentrons') - screen.getByText('Share why the response was not helpful') - screen.getByText('Cancel') - screen.getByText('Send feedback') - }) - - // should move this test to the chat page - it.skip('should set the showFeedbackModel atom to be false when cancel button is clicked', () => { - render() - expect(feedbackModalAtom.init).toBe(true) - - const cancelButton = screen.getByText('Cancel') - cancelButton.click() - // check if the feedbackModalAtom is set to false - expect(feedbackModalAtom.read).toBe(false) - }) -}) diff --git a/opentrons-ai-client/src/molecules/FeedbackModal/index.tsx b/opentrons-ai-client/src/molecules/FeedbackModal/index.tsx deleted file mode 100644 index e65aa7a504c..00000000000 --- a/opentrons-ai-client/src/molecules/FeedbackModal/index.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { - Modal, - Flex, - SPACING, - ALIGN_FLEX_END, - SecondaryButton, - StyledText, - PrimaryButton, - InputField, -} from '@opentrons/components' -import { useAtom } from 'jotai' -import { useTranslation } from 'react-i18next' -import { feedbackModalAtom } from '../../resources/atoms' -import { useState } from 'react' - -export function FeedbackModal(): JSX.Element { - const { t } = useTranslation('protocol_generator') - - const [feedbackValue, setFeedbackValue] = useState('') - const [, setShowFeedbackModal] = useAtom(feedbackModalAtom) - - return ( - { - setShowFeedbackModal(false) - }} - footer={ - - { - setShowFeedbackModal(false) - }} - > - - {t(`cancel`)} - - - { - setShowFeedbackModal(false) - }} - > - - {t(`send_feedback`)} - - - - } - > - { - setFeedbackValue(event.target.value as string) - }} - > - - ) -} diff --git a/opentrons-ai-client/src/molecules/FileUpload/index.tsx b/opentrons-ai-client/src/molecules/FileUpload/index.tsx deleted file mode 100644 index 551c3d0bd05..00000000000 --- a/opentrons-ai-client/src/molecules/FileUpload/index.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { css } from 'styled-components' - -import { - ALIGN_CENTER, - BORDERS, - Btn, - COLORS, - DIRECTION_COLUMN, - Flex, - Icon, - JUSTIFY_SPACE_BETWEEN, - SPACING, - LegacyStyledText, - truncateString, -} from '@opentrons/components' - -const FILE_UPLOAD_STYLE = css` - -&:hover > svg { - background: ${COLORS.black90}${COLORS.opacity20HexCode}; -} -&:active > svg { - background: ${COLORS.black90}${COLORS.opacity20HexCode}}; -} -` - -const FILE_UPLOAD_FOCUS_VISIBLE = css` - &:focus-visible { - border-radius: ${BORDERS.borderRadius4}; - box-shadow: 0 0 0 ${SPACING.spacing2} ${COLORS.blue50}; - } -` - -interface FileUploadProps { - file: File - fileError: string | null - handleClick: () => unknown -} - -export function FileUpload({ - file, - fileError, - handleClick, -}: FileUploadProps): JSX.Element { - return ( - - - - - {truncateString(file.name, 34, 19)} - - - - - {fileError != null ? ( - - {fileError} - - ) : null} - - ) -} diff --git a/opentrons-ai-client/src/molecules/Header/index.tsx b/opentrons-ai-client/src/molecules/Header/index.tsx index 71cb6b7933b..8221aa03e81 100644 --- a/opentrons-ai-client/src/molecules/Header/index.tsx +++ b/opentrons-ai-client/src/molecules/Header/index.tsx @@ -16,8 +16,6 @@ import { import { useAuth0 } from '@auth0/auth0-react' import { CLIENT_MAX_WIDTH } from '../../resources/constants' import { useTrackEvent } from '../../resources/hooks/useTrackEvent' -import { useAtom } from 'jotai' -import { displayExitConfirmModalAtom } from '../../resources/atoms' const HeaderBar = styled(Flex)` position: ${POSITION_RELATIVE}; @@ -47,28 +45,18 @@ const HeaderTitle = styled(StyledText)` font-size: 16px; ` -const LogoutOrExitButton = styled(LinkButton)` +const LogoutButton = styled(LinkButton)` color: ${COLORS.grey50}; font-size: ${TYPOGRAPHY.fontSizeH3}; ` -interface HeaderProps { - isExitButton?: boolean -} - -export function Header({ isExitButton = false }: HeaderProps): JSX.Element { +export function Header(): JSX.Element { const { t } = useTranslation('protocol_generator') const { logout } = useAuth0() const trackEvent = useTrackEvent() - const [, setDisplayExitConfirmModal] = useAtom(displayExitConfirmModalAtom) - - async function handleLoginOrExitClick(): Promise { - if (isExitButton) { - setDisplayExitConfirmModal(true) - return - } - await logout() + function handleLogout(): void { + logout() trackEvent({ name: 'user-logout', properties: {} }) } @@ -79,9 +67,7 @@ export function Header({ isExitButton = false }: HeaderProps): JSX.Element { {t('opentrons')} {t('ai')} - - {isExitButton ? t('exit') : t('logout')} - + {t('logout')} ) diff --git a/opentrons-ai-client/src/molecules/HeaderWithMeter/__tests__/HeaderWithMeter.test.tsx b/opentrons-ai-client/src/molecules/HeaderWithMeter/__tests__/HeaderWithMeter.test.tsx index 7c63eeb2bad..8d02aeb3e12 100644 --- a/opentrons-ai-client/src/molecules/HeaderWithMeter/__tests__/HeaderWithMeter.test.tsx +++ b/opentrons-ai-client/src/molecules/HeaderWithMeter/__tests__/HeaderWithMeter.test.tsx @@ -1,18 +1,8 @@ import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { HeaderWithMeter } from '../index' -import { describe, expect, it, vi } from 'vitest' -import { - screen, - render as rtlRender, - waitFor, - fireEvent, -} from '@testing-library/react' -import { ExitConfirmModal } from '../../ExitConfirmModal' - -vi.mock('react-router-dom', () => ({ - useNavigate: vi.fn(), -})) +import { describe, expect, it } from 'vitest' +import { screen, render as rtlRender } from '@testing-library/react' const render = (): ReturnType => { return renderWithProviders(, { @@ -58,29 +48,4 @@ describe('HeaderWithMeter', () => { rerender() expect(progressBar).toHaveAttribute('value', '0.2') }) - - it('should display the exit button instead of the logout button', () => { - render() - screen.getByText('Exit') - }) - - it('should display the exit confirm modal when exit button is clicked', async () => { - renderWithProviders( - <> - - - , - { - i18nInstance: i18n, - } - ) - - const exitButton = screen.getByText('Exit') - - fireEvent.click(exitButton) - - await waitFor(() => { - screen.getByText('Are you sure you want to exit?') - }) - }) }) diff --git a/opentrons-ai-client/src/molecules/HeaderWithMeter/index.tsx b/opentrons-ai-client/src/molecules/HeaderWithMeter/index.tsx index b59e344ec19..24bc1a89805 100644 --- a/opentrons-ai-client/src/molecules/HeaderWithMeter/index.tsx +++ b/opentrons-ai-client/src/molecules/HeaderWithMeter/index.tsx @@ -43,7 +43,7 @@ export function HeaderWithMeter({ justifyContent={JUSTIFY_SPACE_BETWEEN} width="100%" > -
+
) diff --git a/opentrons-ai-client/src/molecules/InputPrompt/index.tsx b/opentrons-ai-client/src/molecules/InputPrompt/index.tsx index c87d24f1975..d4c4cdf5f8d 100644 --- a/opentrons-ai-client/src/molecules/InputPrompt/index.tsx +++ b/opentrons-ai-client/src/molecules/InputPrompt/index.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next' import styled, { css } from 'styled-components' import { useFormContext } from 'react-hook-form' import { useAtom } from 'jotai' -import { v4 as uuidv4 } from 'uuid' import { ALIGN_CENTER, @@ -16,12 +15,7 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { SendButton } from '../../atoms/SendButton' -import { - chatDataAtom, - chatHistoryAtom, - chatPromptAtom, - tokenAtom, -} from '../../resources/atoms' +import { chatDataAtom, chatHistoryAtom, tokenAtom } from '../../resources/atoms' import { useApiCall } from '../../resources/hooks' import { calcTextAreaHeight } from '../../resources/utils/utils' import { @@ -35,29 +29,16 @@ import type { ChatData } from '../../resources/types' export function InputPrompt(): JSX.Element { const { t } = useTranslation('protocol_generator') - const { register, watch, reset, setValue } = useFormContext() - const [chatPromptAtomValue] = useAtom(chatPromptAtom) + const { register, watch, reset } = useFormContext() const [, setChatData] = useAtom(chatDataAtom) const [chatHistory, setChatHistory] = useAtom(chatHistoryAtom) const [token] = useAtom(tokenAtom) const [submitted, setSubmitted] = useState(false) const userPrompt = watch('userPrompt') ?? '' const { data, isLoading, callApi } = useApiCall() - const [requestId, setRequestId] = useState(uuidv4()) - - // This is to autofill the input field for when we navigate to the chat page from the existing/new protocol generator pages - useEffect(() => { - setValue('userPrompt', chatPromptAtomValue) - }, [chatPromptAtomValue, setValue]) - - useEffect(() => { - setValue('userPrompt', chatPromptAtomValue) - }, [chatPromptAtomValue, setValue]) const handleClick = async (): Promise => { - setRequestId(uuidv4()) const userInput: ChatData = { - requestId, role: 'user', reply: userPrompt, } @@ -109,7 +90,6 @@ export function InputPrompt(): JSX.Element { if (submitted && data != null && !isLoading) { const { role, reply } = data as ChatData const assistantResponse: ChatData = { - requestId, role, reply, } @@ -176,7 +156,6 @@ const LegacyStyledTextarea = styled.textarea` font-size: ${TYPOGRAPHY.fontSize20}; line-height: ${TYPOGRAPHY.lineHeight24}; padding: 1.2rem 0; - font-size: 1rem; ::placeholder { position: absolute; diff --git a/opentrons-ai-client/src/molecules/ModelDiagram/index.tsx b/opentrons-ai-client/src/molecules/ModelDiagram/index.tsx deleted file mode 100644 index e4211e7df98..00000000000 --- a/opentrons-ai-client/src/molecules/ModelDiagram/index.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { css } from 'styled-components' -import { - MAGNETIC_MODULE_TYPE, - TEMPERATURE_MODULE_TYPE, - THERMOCYCLER_MODULE_TYPE, - MAGNETIC_MODULE_V1, - MAGNETIC_MODULE_V2, - TEMPERATURE_MODULE_V1, - TEMPERATURE_MODULE_V2, - THERMOCYCLER_MODULE_V1, - HEATERSHAKER_MODULE_TYPE, - HEATERSHAKER_MODULE_V1, - THERMOCYCLER_MODULE_V2, - MAGNETIC_BLOCK_TYPE, - MAGNETIC_BLOCK_V1, - ABSORBANCE_READER_TYPE, - ABSORBANCE_READER_V1, -} from '@opentrons/shared-data' - -import magdeck_gen1 from '../../assets/images/modules/magdeck_gen1.png' -import magdeck_gen2 from '../../assets/images/modules/magdeck_gen2.png' -import tempdeck_gen1 from '../../assets/images/modules/tempdeck_gen1.png' -import temp_deck_gen_2_transparent from '../../assets/images/modules/temp_deck_gen_2_transparent.png' -import thermocycler from '../../assets/images/modules/thermocycler.png' -import thermocycler_gen2 from '../../assets/images/modules/thermocycler_gen2.png' -import heater_shaker_module_transparent from '../../assets/images/modules/heater_shaker_module_transparent.png' -import mag_block from '../../assets/images/modules/MagneticBlock_GEN1_HERO.png' -import type { ModuleType, ModuleModel } from '@opentrons/shared-data' - -interface Props { - type: ModuleType - model: ModuleModel -} - -type ModuleImg = { - [type in ModuleType]: { - [model in ModuleModel]?: string - } -} - -const MODULE_IMG_BY_TYPE: ModuleImg = { - [MAGNETIC_MODULE_TYPE]: { - [MAGNETIC_MODULE_V1]: magdeck_gen1, - [MAGNETIC_MODULE_V2]: magdeck_gen2, - }, - [TEMPERATURE_MODULE_TYPE]: { - [TEMPERATURE_MODULE_V1]: tempdeck_gen1, - [TEMPERATURE_MODULE_V2]: temp_deck_gen_2_transparent, - }, - [THERMOCYCLER_MODULE_TYPE]: { - [THERMOCYCLER_MODULE_V1]: thermocycler, - [THERMOCYCLER_MODULE_V2]: thermocycler_gen2, - }, - [HEATERSHAKER_MODULE_TYPE]: { - [HEATERSHAKER_MODULE_V1]: heater_shaker_module_transparent, - }, - [MAGNETIC_BLOCK_TYPE]: { - [MAGNETIC_BLOCK_V1]: mag_block, - }, - [ABSORBANCE_READER_TYPE]: { - // TODO (AA): update absorbance reader image - [ABSORBANCE_READER_V1]: heater_shaker_module_transparent, - }, -} - -const IMAGE_MAX_WIDTH = '96px' -export function ModuleDiagram(props: Props): JSX.Element { - const model = MODULE_IMG_BY_TYPE[props.type][props.model] - return ( - {props.type} - ) -} diff --git a/opentrons-ai-client/src/molecules/ModuleListItemGroup/__tests__/ModuleListItemGroup.test.tsx b/opentrons-ai-client/src/molecules/ModuleListItemGroup/__tests__/ModuleListItemGroup.test.tsx deleted file mode 100644 index 5c04e3a6b44..00000000000 --- a/opentrons-ai-client/src/molecules/ModuleListItemGroup/__tests__/ModuleListItemGroup.test.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { ModuleListItemGroup } from '../index' -import { describe, it, expect } from 'vitest' -import { fireEvent, screen } from '@testing-library/react' -import { FormProvider, useForm } from 'react-hook-form' -import type { DisplayModules } from '../../../organisms/ModulesSection' - -const modulesMock: DisplayModules[] = [ - { - type: 'heaterShakerModuleType', - model: 'heaterShakerModuleV1', - name: 'Heater-Shaker Module GEN1', - }, - { - type: 'temperatureModuleType', - model: 'temperatureModuleV2', - name: 'Temperature Module GEN2', - }, -] - -const TestFormProviderComponent = () => { - const methods = useForm({ - defaultValues: { - modules: modulesMock, - }, - }) - - return ( - - - - ) -} - -const render = (): ReturnType => { - return renderWithProviders(, { - i18nInstance: i18n, - }) -} - -describe('ModuleListItemGroup', () => { - it('should render ModuleListItemGroup component', () => { - render() - - expect(screen.getAllByText('Adapter').length).toBe(2) - expect(screen.getAllByText('remove').length).toBe(2) - - screen.getByAltText('heaterShakerModuleType') - screen.getByText('Heater-Shaker Module GEN1') - - screen.getByAltText('temperatureModuleType') - screen.getByText('Temperature Module GEN2') - }) - - it('should remove the list item if remove is clicked', async () => { - render() - - const removeListItemButton = screen.getAllByText('remove')[0] - - fireEvent.click(removeListItemButton) - - expect( - screen.queryByText('Heater-Shaker Module GEN1') - ).not.toBeInTheDocument() - }) - - it('should render the dropdown if adapters are available', () => { - render() - - expect(screen.getAllByText('Choose an adapter').length).toBe(2) - }) - - it('should be able to select an adapter', () => { - render() - - const dropdownButton = screen.getAllByText('Choose an adapter')[1] - - fireEvent.click(dropdownButton) - - const adapterOption = screen.getByText( - 'Opentrons 24 Well Aluminum Block with Generic 2 mL Screwcap' - ) - - fireEvent.click(adapterOption) - - expect( - screen.getByText( - 'Opentrons 24 Well Aluminum Block with Generic 2 mL Screwcap' - ) - ).toBeInTheDocument() - }) -}) diff --git a/opentrons-ai-client/src/molecules/ModuleListItemGroup/index.tsx b/opentrons-ai-client/src/molecules/ModuleListItemGroup/index.tsx deleted file mode 100644 index 878600fc97f..00000000000 --- a/opentrons-ai-client/src/molecules/ModuleListItemGroup/index.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { - Flex, - SPACING, - ALIGN_CENTER, - BORDERS, - COLORS, - ListItem, - ListItemCustomize, -} from '@opentrons/components' -import type { DropdownBorder } from '@opentrons/components' -import { - ABSORBANCE_READER_TYPE, - getAllDefinitions, - getModuleDisplayName, - HEATERSHAKER_MODULE_TYPE, - MAGNETIC_BLOCK_TYPE, - MAGNETIC_MODULE_TYPE, - TEMPERATURE_MODULE_TYPE, - THERMOCYCLER_MODULE_TYPE, -} from '@opentrons/shared-data' -import type { ModuleType } from '@opentrons/shared-data' -import { Controller, useFormContext } from 'react-hook-form' -import { ModuleDiagram } from '../ModelDiagram' -import { MODULES_FIELD_NAME } from '../../organisms/ModulesSection' -import type { DisplayModules } from '../../organisms/ModulesSection' -import { useTranslation } from 'react-i18next' -import { useMemo } from 'react' - -export const RECOMMENDED_LABWARE_BY_MODULE: { [K in ModuleType]: string[] } = { - [TEMPERATURE_MODULE_TYPE]: [ - 'opentrons_24_aluminumblock_generic_2ml_screwcap', - 'opentrons_96_well_aluminum_block', - 'opentrons_96_aluminumblock_generic_pcr_strip_200ul', - 'opentrons_24_aluminumblock_nest_1.5ml_screwcap', - 'opentrons_24_aluminumblock_nest_1.5ml_snapcap', - 'opentrons_24_aluminumblock_nest_2ml_screwcap', - 'opentrons_24_aluminumblock_nest_2ml_snapcap', - 'opentrons_24_aluminumblock_nest_0.5ml_screwcap', - 'opentrons_aluminum_flat_bottom_plate', - 'opentrons_96_deep_well_temp_mod_adapter', - ], - [MAGNETIC_MODULE_TYPE]: [ - 'nest_96_wellplate_100ul_pcr_full_skirt', - 'nest_96_wellplate_2ml_deep', - 'opentrons_96_wellplate_200ul_pcr_full_skirt', - ], - [THERMOCYCLER_MODULE_TYPE]: [ - 'nest_96_wellplate_100ul_pcr_full_skirt', - 'opentrons_96_wellplate_200ul_pcr_full_skirt', - ], - [HEATERSHAKER_MODULE_TYPE]: [ - 'opentrons_96_deep_well_adapter', - 'opentrons_96_flat_bottom_adapter', - 'opentrons_96_pcr_adapter', - 'opentrons_universal_flat_adapter', - ], - [MAGNETIC_BLOCK_TYPE]: [ - 'nest_96_wellplate_100ul_pcr_full_skirt', - 'nest_96_wellplate_2ml_deep', - 'opentrons_96_wellplate_200ul_pcr_full_skirt', - ], - [ABSORBANCE_READER_TYPE]: [ - 'opentrons_flex_lid_absorbance_plate_reader_module', - ], -} - -export function ModuleListItemGroup(): JSX.Element | null { - const { watch, setValue } = useFormContext() - const { t } = useTranslation('create_protocol') - const modulesWatch: DisplayModules[] = watch(MODULES_FIELD_NAME) ?? [] - - const allDefinitionsValues = useMemo( - () => Object.values(getAllDefinitions()), - [] - ) - - const getDefDisplayName = (value: string): string => { - return ( - allDefinitionsValues.find(def => def.parameters.loadName === value) - ?.metadata.displayName ?? value - ) - } - - return ( - <> - {modulesWatch?.map(module => { - const adapters = RECOMMENDED_LABWARE_BY_MODULE[module.type] - - return ( - { - const currentModule = field.value.find( - (m: DisplayModules) => m.type === module.type - ) - - return ( - - 0 - ? t('modules_adapter_label') - : undefined - } - linkText={t('modules_remove_label')} - dropdown={ - adapters != null && adapters.length > 0 - ? { - title: (null as unknown) as string, - currentOption: { - name: - getDefDisplayName( - currentModule?.adapter?.value as string - ) ?? 'Choose an adapter', - value: currentModule?.adapter?.value, - }, - onClick: (value: string) => { - field.onChange( - field.value.map((m: DisplayModules) => - m.type === module.type - ? { - ...m, - adapter: { - name: getDefDisplayName(value), - value, - }, - } - : m - ) - ) - }, - dropdownType: 'neutral' as DropdownBorder, - filterOptions: adapters?.map(adapter => ({ - name: getDefDisplayName(adapter), - value: adapter, - })), - } - : undefined - } - onClick={() => { - setValue( - MODULES_FIELD_NAME, - modulesWatch.filter(m => m.type !== module.type), - { shouldValidate: true } - ) - }} - header={getModuleDisplayName(module.model)} - leftHeaderItem={ - - - - } - /> - - ) - }} - /> - ) - })} - - ) -} diff --git a/opentrons-ai-client/src/molecules/PromptPreview/index.tsx b/opentrons-ai-client/src/molecules/PromptPreview/index.tsx index ce7687907a8..d74884ad1ae 100644 --- a/opentrons-ai-client/src/molecules/PromptPreview/index.tsx +++ b/opentrons-ai-client/src/molecules/PromptPreview/index.tsx @@ -24,7 +24,6 @@ interface PromptPreviewProps { const PromptPreviewContainer = styled(Flex)` flex-direction: ${DIRECTION_COLUMN}; width: 100%; - max-width: 516px; height: ${SIZE_AUTO}; padding-top: ${SPACING.spacing8}; background-color: ${COLORS.transparent}; @@ -79,7 +78,7 @@ export function PromptPreview({ key={`section-${index}`} title={section.title} items={section.items} - itemMaxWidth={index <= 1 ? '33.33%' : '100%'} + itemMaxWidth={index <= 2 ? '33.33%' : '100%'} /> ) )} diff --git a/opentrons-ai-client/src/molecules/UploadInput/index.tsx b/opentrons-ai-client/src/molecules/UploadInput/index.tsx deleted file mode 100644 index 77dc5a2616d..00000000000 --- a/opentrons-ai-client/src/molecules/UploadInput/index.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import * as React from 'react' -import styled, { css } from 'styled-components' -import { useTranslation } from 'react-i18next' -import { - ALIGN_CENTER, - BORDERS, - COLORS, - CURSOR_POINTER, - DIRECTION_COLUMN, - DISPLAY_FLEX, - Flex, - Icon, - JUSTIFY_CENTER, - LegacyStyledText, - POSITION_FIXED, - PrimaryButton, - SPACING, - TYPOGRAPHY, -} from '@opentrons/components' - -const StyledLabel = styled.label` - display: ${DISPLAY_FLEX}; - cursor: ${CURSOR_POINTER}; - flex-direction: ${DIRECTION_COLUMN}; - align-items: ${ALIGN_CENTER}; - width: 100%; - padding: ${SPACING.spacing32}; - border: 2px dashed ${COLORS.grey30}; - border-radius: ${BORDERS.borderRadius4}; - text-align: ${TYPOGRAPHY.textAlignCenter}; - background-color: ${COLORS.white}; - - &:hover { - border: 2px dashed ${COLORS.blue50}; - } -` -const DRAG_OVER_STYLES = css` - border: 2px dashed ${COLORS.blue50}; -` - -const StyledInput = styled.input` - position: ${POSITION_FIXED}; - clip: rect(1px 1px 1px 1px); -` - -export interface UploadInputProps { - /** Callback function that is called when a file is uploaded. */ - onUpload: (file: File) => unknown - /** Optional callback function that is called when the upload button is clicked. */ - onClick?: () => void - /** Optional text for the upload button. If undefined, the button displays Upload */ - uploadButtonText?: string - /** Optional text or JSX element that is displayed above the upload button. */ - uploadText?: string | JSX.Element - /** Optional text or JSX element that is displayed in the drag and drop area. */ - dragAndDropText?: string | JSX.Element -} - -export function UploadInput(props: UploadInputProps): JSX.Element | null { - const { - dragAndDropText, - onClick, - onUpload, - uploadButtonText, - uploadText, - } = 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 => { - e.preventDefault() - e.stopPropagation() - Array.from(e.dataTransfer.files).forEach(f => onUpload(f)) - setIsFileOverDropZone(false) - } - const handleDragEnter: React.DragEventHandler = e => { - e.preventDefault() - e.stopPropagation() - } - const handleDragLeave: React.DragEventHandler = e => { - e.preventDefault() - e.stopPropagation() - setIsFileOverDropZone(false) - setIsHover(false) - } - const handleDragOver: React.DragEventHandler = e => { - e.preventDefault() - e.stopPropagation() - setIsFileOverDropZone(true) - setIsHover(true) - } - - const handleClick: React.MouseEventHandler = _event => { - onClick != null ? onClick() : fileInput.current?.click() - } - - const onChange: React.ChangeEventHandler = event => { - ;[...(event.target.files ?? [])].forEach(f => onUpload(f)) - if ('value' in event.currentTarget) event.currentTarget.value = '' - } - - return ( - - {uploadText != null ? ( - <> - {typeof uploadText === 'string' ? ( - - {uploadText} - - ) : ( - <>{uploadText} - )} - - ) : null} - - {uploadButtonText ?? t('upload')} - - - { - setIsHover(true) - }} - onMouseLeave={() => { - setIsHover(false) - }} - css={isFileOverDropZone ? DRAG_OVER_STYLES : undefined} - > - - {dragAndDropText} - - - - ) -} diff --git a/opentrons-ai-client/src/organisms/InstrumentsSection/__tests__/InstrumentsSection.test.tsx b/opentrons-ai-client/src/organisms/InstrumentsSection/__tests__/InstrumentsSection.test.tsx deleted file mode 100644 index 1d710fbe703..00000000000 --- a/opentrons-ai-client/src/organisms/InstrumentsSection/__tests__/InstrumentsSection.test.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { fireEvent, screen, waitFor } from '@testing-library/react' -import { describe, it, expect } from 'vitest' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { InstrumentsSection } from '..' -import { FormProvider, useForm } from 'react-hook-form' - -const TestFormProviderComponent = () => { - const methods = useForm({ - defaultValues: {}, - }) - - return ( - - - - ) -} - -const render = (): ReturnType => { - return renderWithProviders(, { - i18nInstance: i18n, - }) -} - -describe('ApplicationSection', () => { - it('should render robot, pipette, flex gripper radios, mounts dropdowns, and confirm button', async () => { - render() - - expect( - screen.getByText('What robot would you like to use?') - ).toBeInTheDocument() - expect( - screen.getByText('What pipettes would you like to use?') - ).toBeInTheDocument() - await waitFor(() => { - expect(screen.getByText('Left mount')).toBeInTheDocument() - }) - expect(screen.getByText('Right mount')).toBeInTheDocument() - expect( - screen.getByText('Do you want to use the Flex Gripper?') - ).toBeInTheDocument() - expect(screen.getByText('Confirm')).toBeInTheDocument() - }) - - it('should not render left and right mount dropdowns if 96-Channel 1000µL pipette radio is selected', () => { - render() - - const pipettesRadioButton = screen.getByLabelText( - '96-Channel 1000µL pipette' - ) - fireEvent.click(pipettesRadioButton) - - expect(screen.queryByText('Left mount')).not.toBeInTheDocument() - expect(screen.queryByText('Right mount')).not.toBeInTheDocument() - }) - - it('should render only left and right mount dropdowns if Opentrons OT-2 is selected', () => { - render() - - const ot2Radio = screen.getByLabelText('Opentrons OT-2') - fireEvent.click(ot2Radio) - - expect( - screen.getByText('What pipettes would you like to use?') - ).toBeInTheDocument() - expect(screen.getByText('Left mount')).toBeInTheDocument() - expect(screen.getByText('Right mount')).toBeInTheDocument() - - expect(screen.queryByText('Two pipettes')).not.toBeInTheDocument() - expect( - screen.queryByText('96-Channel 1000µL pipette') - ).not.toBeInTheDocument() - expect( - screen.queryByText('Do you want to use the Flex Gripper') - ).not.toBeInTheDocument() - }) - - it('should enable confirm button when all fields are filled', async () => { - render() - - const confirmButton = screen.getByRole('button') - await waitFor(() => { - expect(confirmButton).not.toBeEnabled() - }) - - const leftMount = screen.getAllByText('Choose pipette')[0] - fireEvent.click(leftMount) - fireEvent.click(screen.getByText('Flex 1-Channel 50 μL')) - - const rightMount = screen.getByText('Choose pipette') - fireEvent.click(rightMount) - fireEvent.click(screen.getByText('Flex 8-Channel 50 μL')) - - await waitFor(() => { - expect(confirmButton).toBeEnabled() - }) - }) - - it('should disable confirm button when all fields are not filled', async () => { - render() - - const confirmButton = screen.getByRole('button') - await waitFor(() => { - expect(confirmButton).not.toBeEnabled() - }) - }) -}) diff --git a/opentrons-ai-client/src/organisms/InstrumentsSection/index.tsx b/opentrons-ai-client/src/organisms/InstrumentsSection/index.tsx deleted file mode 100644 index a0af6d54138..00000000000 --- a/opentrons-ai-client/src/organisms/InstrumentsSection/index.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import { - COLORS, - DIRECTION_COLUMN, - DISPLAY_FLEX, - Flex, - JUSTIFY_FLEX_END, - LargeButton, - SPACING, - StyledText, -} from '@opentrons/components' -import { useFormContext } from 'react-hook-form' -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' -import { useAtom } from 'jotai' -import { createProtocolAtom } from '../../resources/atoms' -import { INSTRUMENTS_STEP } from '../ProtocolSectionsContainer' -import { ControlledDropdownMenu } from '../../atoms/ControlledDropdownMenu' -import { ControlledRadioButtonGroup } from '../../molecules/ControlledRadioButtonGroup' -import { useMemo } from 'react' -import { - getAllPipetteNames, - getPipetteSpecsV2, - OT2_PIPETTES, - OT2_ROBOT_TYPE, - OT3_PIPETTES, -} from '@opentrons/shared-data' - -export const ROBOT_FIELD_NAME = 'instruments.robot' -export const PIPETTES_FIELD_NAME = 'instruments.pipettes' -export const FLEX_GRIPPER_FIELD_NAME = 'instruments.flexGripper' -export const LEFT_PIPETTE_FIELD_NAME = 'instruments.leftPipette' -export const RIGHT_PIPETTE_FIELD_NAME = 'instruments.rightPipette' -export const FLEX_GRIPPER = 'flex_gripper' -export const NO_FLEX_GRIPPER = 'no_flex_gripper' -export const OPENTRONS_FLEX = 'opentrons_flex' -export const OPENTRONS_OT2 = 'opentrons_ot2' -export const _96_CHANNEL_1000UL_PIPETTE = '96_channel_1000ul_pipette' -export const TWO_PIPETTES = 'two_pipettes' - -export function InstrumentsSection(): JSX.Element | null { - const { t } = useTranslation('create_protocol') - const { - formState: { isValid }, - watch, - } = useFormContext() - const [{ currentStep }, setCreateProtocolAtom] = useAtom(createProtocolAtom) - const robotType = watch(ROBOT_FIELD_NAME) - const isOtherPipettesSelected = watch(PIPETTES_FIELD_NAME) === TWO_PIPETTES - const isOpentronsOT2Selected = robotType === OPENTRONS_OT2 - - const robotRadioButtons = [ - { - id: OPENTRONS_FLEX, - buttonLabel: t('opentrons_flex_label'), - buttonValue: OPENTRONS_FLEX, - }, - { - id: OPENTRONS_OT2, - buttonLabel: t('opentrons_ot2_label'), - buttonValue: OPENTRONS_OT2, - }, - ] - - const pipetteRadioButtons = [ - { - id: TWO_PIPETTES, - buttonLabel: t('two_pipettes_label'), - buttonValue: TWO_PIPETTES, - }, - { - id: _96_CHANNEL_1000UL_PIPETTE, - buttonLabel: t('96_channel_1000ul_pipette_label'), - buttonValue: _96_CHANNEL_1000UL_PIPETTE, - }, - ] - - const flexGripperRadionButtons = [ - { - id: FLEX_GRIPPER, - buttonLabel: t('flex_gripper_yes_label'), - buttonValue: FLEX_GRIPPER, - }, - { - id: NO_FLEX_GRIPPER, - buttonLabel: t('flex_gripper_no_label'), - buttonValue: NO_FLEX_GRIPPER, - }, - ] - - const pipetteOptions = useMemo(() => { - const allPipetteOptions = getAllPipetteNames('maxVolume', 'channels') - .filter(name => - (robotType === OT2_ROBOT_TYPE ? OT2_PIPETTES : OT3_PIPETTES).includes( - name - ) - ) - .map(name => ({ - value: name, - name: getPipetteSpecsV2(name)?.displayName ?? '', - })) - return allPipetteOptions.filter(o => o.value !== 'p1000_96') - }, [robotType]) - - function handleConfirmButtonClick(): void { - const step = - currentStep > INSTRUMENTS_STEP ? currentStep : INSTRUMENTS_STEP + 1 - - setCreateProtocolAtom({ - currentStep: step, - focusStep: step, - }) - } - - return ( - - - - - {!isOpentronsOT2Selected && ( - - )} - - {(isOtherPipettesSelected || isOpentronsOT2Selected) && ( - - {isOpentronsOT2Selected && ( - - {t('instruments_pipettes_title')} - - )} - - - - )} - - - {!isOpentronsOT2Selected && ( - - )} - - - - - - ) -} - -const ButtonContainer = styled.div` - display: ${DISPLAY_FLEX}; - justify-content: ${JUSTIFY_FLEX_END}; -` - -const PipettesDropdown = styled.div<{ isOpentronsOT2Selected?: boolean }>` - display: flex; - flex-direction: column; - gap: ${props => - props.isOpentronsOT2Selected ?? false - ? SPACING.spacing16 - : SPACING.spacing8}; -` - -const PipettesSection = styled.div<{ isOpentronsOT2Selected?: boolean }>` - display: flex; - flex-direction: column; - gap: ${props => - props.isOpentronsOT2Selected ?? false - ? SPACING.spacing16 - : SPACING.spacing8}; -` diff --git a/opentrons-ai-client/src/pages/Chat/Chat.stories.tsx b/opentrons-ai-client/src/organisms/MainContentContainer/MainContainer.stories.tsx similarity index 59% rename from opentrons-ai-client/src/pages/Chat/Chat.stories.tsx rename to opentrons-ai-client/src/organisms/MainContentContainer/MainContainer.stories.tsx index 3a4d0d674d4..4f8fe5739fd 100644 --- a/opentrons-ai-client/src/pages/Chat/Chat.stories.tsx +++ b/opentrons-ai-client/src/organisms/MainContentContainer/MainContainer.stories.tsx @@ -1,12 +1,12 @@ import { I18nextProvider } from 'react-i18next' import { i18n } from '../../i18n' -import { Chat as ChatComponent } from './index' +import { MainContentContainer as MainContentContainerComponent } from './index' import type { Meta, StoryObj } from '@storybook/react' -const meta: Meta = { +const meta: Meta = { title: 'AI/organisms/ChatContainer', - component: ChatComponent, + component: MainContentContainerComponent, decorators: [ Story => ( @@ -16,5 +16,5 @@ const meta: Meta = { ], } export default meta -type Story = StoryObj +type Story = StoryObj export const ChatContainer: Story = {} diff --git a/opentrons-ai-client/src/pages/Chat/__tests__/Chat.test.tsx b/opentrons-ai-client/src/organisms/MainContentContainer/__tests__/MainContentContainer.test.tsx similarity index 68% rename from opentrons-ai-client/src/pages/Chat/__tests__/Chat.test.tsx rename to opentrons-ai-client/src/organisms/MainContentContainer/__tests__/MainContentContainer.test.tsx index 77874086534..4598eddc49e 100644 --- a/opentrons-ai-client/src/pages/Chat/__tests__/Chat.test.tsx +++ b/opentrons-ai-client/src/organisms/MainContentContainer/__tests__/MainContentContainer.test.tsx @@ -4,7 +4,7 @@ import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { PromptGuide } from '../../../molecules/PromptGuide' import { ChatFooter } from '../../../molecules/ChatFooter' -import { Chat } from '../index' +import { MainContentContainer } from '../index' vi.mock('../../../molecules/PromptGuide') vi.mock('../../../molecules/ChatFooter') @@ -12,27 +12,21 @@ vi.mock('../../../molecules/ChatFooter') window.HTMLElement.prototype.scrollIntoView = vi.fn() const render = (): ReturnType => { - return renderWithProviders(, { + return renderWithProviders(, { i18nInstance: i18n, }) } -describe('Chat', () => { +describe('MainContentContainer', () => { beforeEach(() => { vi.mocked(PromptGuide).mockReturnValue(
mock PromptGuide
) vi.mocked(ChatFooter).mockReturnValue(
mock ChatFooter
) }) - it('should render footer', () => { + it('should render prompt guide and text', () => { render() + screen.getByText('OpentronsAI') + screen.getByText('mock PromptGuide') screen.getByText('mock ChatFooter') }) - - it.skip('should not show the feedback modal when loading the page', () => { - render() - screen.getByText('Send feedback to Opentrons') - screen.getByText('Share why the response was not helpful') - screen.getByText('Cancel') - screen.getByText('Send feedback') - }) }) diff --git a/opentrons-ai-client/src/organisms/MainContentContainer/index.tsx b/opentrons-ai-client/src/organisms/MainContentContainer/index.tsx new file mode 100644 index 00000000000..b5b495a691e --- /dev/null +++ b/opentrons-ai-client/src/organisms/MainContentContainer/index.tsx @@ -0,0 +1,79 @@ +import { useRef, useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import styled from 'styled-components' +import { useAtom } from 'jotai' + +import { + COLORS, + DIRECTION_COLUMN, + Flex, + OVERFLOW_AUTO, + SPACING, + LegacyStyledText, +} from '@opentrons/components' +import { PromptGuide } from '../../molecules/PromptGuide' +import { ChatDisplay } from '../../molecules/ChatDisplay' +import { ChatFooter } from '../../molecules/ChatFooter' +import { chatDataAtom } from '../../resources/atoms' + +export function MainContentContainer(): JSX.Element { + const { t } = useTranslation('protocol_generator') + const [chatData] = useAtom(chatDataAtom) + const scrollRef = useRef(null) + + useEffect(() => { + if (scrollRef.current != null) + scrollRef.current.scrollIntoView({ + behavior: 'smooth', + block: 'nearest', + inline: 'nearest', + }) + }, [chatData.length]) + + return ( + + + + {/* Prompt Guide remain as a reference for users. */} + {t('opentronsai')} + + + + {chatData.length > 0 + ? chatData.map((chat, index) => ( + + )) + : null} + + + + + + + + ) +} + +const ChatDataContainer = styled(Flex)` + flex-direction: ${DIRECTION_COLUMN}; + grid-gap: ${SPACING.spacing40}; + width: 100%; +` diff --git a/opentrons-ai-client/src/organisms/ModulesSection/__tests__/ModulesSection.test.tsx b/opentrons-ai-client/src/organisms/ModulesSection/__tests__/ModulesSection.test.tsx deleted file mode 100644 index 0a556238930..00000000000 --- a/opentrons-ai-client/src/organisms/ModulesSection/__tests__/ModulesSection.test.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { fireEvent, screen, waitFor } from '@testing-library/react' -import { describe, it, expect } from 'vitest' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../i18n' -import { FormProvider, useForm } from 'react-hook-form' -import { ModulesSection } from '..' - -const TestFormProviderComponent = () => { - const methods = useForm({ - defaultValues: {}, - }) - - return ( - - - - ) -} - -const render = (): ReturnType => { - return renderWithProviders(, { - i18nInstance: i18n, - }) -} - -describe('ModulesSection', () => { - it('should render modules buttons, no modules added yet, and confirm button', async () => { - render() - - expect(screen.getAllByRole('button').length).toBe(5) - expect(screen.getByText('No modules added yet')).toBeInTheDocument() - expect(screen.getByText('Confirm')).toBeInTheDocument() - }) - - it('should render a list item with the selected module if user clicks the module button', () => { - render() - - const moduleButton = screen.getByText('Heater-Shaker Module GEN1') - fireEvent.click(moduleButton) - - expect(screen.getAllByText('Heater-Shaker Module GEN1').length).toBe(2) - expect(screen.queryByText('No modules added yet')).not.toBeInTheDocument() - }) - - it('should render multiple list items with the selected modules if user clicks multiple module buttons', () => { - render() - - const moduleButton1 = screen.getByText('Heater-Shaker Module GEN1') - fireEvent.click(moduleButton1) - - const moduleButton2 = screen.getByText('Temperature Module GEN2') - fireEvent.click(moduleButton2) - - expect(screen.getAllByText('Heater-Shaker Module GEN1').length).toBe(2) - expect(screen.getAllByText('Temperature Module GEN2').length).toBe(2) - }) - - it('should remove the module list item if user clicks the remove link', () => { - render() - - const moduleButton = screen.getByText('Heater-Shaker Module GEN1') - fireEvent.click(moduleButton) - - expect(screen.getAllByText('Heater-Shaker Module GEN1').length).toBe(2) - - const removeLink = screen.getByText('remove') - fireEvent.click(removeLink) - - expect(screen.getAllByText('Heater-Shaker Module GEN1').length).toBe(1) - }) - - it('should disable confirm button when all fields are not filled', async () => { - render() - - const confirmButton = screen.getByRole('button', { name: 'Confirm' }) - await waitFor(() => { - expect(confirmButton).not.toBeEnabled() - }) - }) - - it('should enable confirm button when all fields are filled', async () => { - render() - - const confirmButton = screen.getByRole('button', { name: 'Confirm' }) - await waitFor(() => { - expect(confirmButton).not.toBeEnabled() - }) - - const moduleButton = screen.getByText('Heater-Shaker Module GEN1') - fireEvent.click(moduleButton) - - await waitFor(() => { - expect(confirmButton).toBeEnabled() - }) - }) -}) diff --git a/opentrons-ai-client/src/organisms/ModulesSection/index.tsx b/opentrons-ai-client/src/organisms/ModulesSection/index.tsx deleted file mode 100644 index 85f068bc226..00000000000 --- a/opentrons-ai-client/src/organisms/ModulesSection/index.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { - DIRECTION_COLUMN, - DISPLAY_FLEX, - Flex, - InfoScreen, - JUSTIFY_FLEX_END, - LargeButton, - SPACING, -} from '@opentrons/components' -import { useFormContext } from 'react-hook-form' -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' -import { useAtom } from 'jotai' -import { createProtocolAtom } from '../../resources/atoms' -import { MODULES_STEP } from '../ProtocolSectionsContainer' -import { ControlledEmptySelectorButtonGroup } from '../../molecules/ControlledEmptySelectorButtonGroup' -import { ModuleListItemGroup } from '../../molecules/ModuleListItemGroup' -import type { ModuleType, ModuleModel } from '@opentrons/shared-data' - -export interface DisplayModules { - type: ModuleType - model: ModuleModel - name: string - adapter?: { - name: string - value: string - } -} - -export const MODULES_FIELD_NAME = 'modules' - -export function ModulesSection(): JSX.Element | null { - const { t } = useTranslation('create_protocol') - const { - formState: { isValid }, - watch, - } = useFormContext() - const [{ currentStep }, setCreateProtocolAtom] = useAtom(createProtocolAtom) - - const modules: DisplayModules[] = [ - { - type: 'heaterShakerModuleType', - model: 'heaterShakerModuleV1', - name: t('heater_shaker_module_v1'), - }, - { - type: 'temperatureModuleType', - model: 'temperatureModuleV2', - name: t('temperature_module_v2'), - }, - { - type: 'thermocyclerModuleType', - model: 'thermocyclerModuleV2', - name: t('thermocycler_module_v2'), - }, - { - type: 'magneticModuleType', - model: 'magneticModuleV1', - name: t('magnetic_module_v1'), - }, - ] - - function handleConfirmButtonClick(): void { - const step = currentStep > MODULES_STEP ? currentStep : MODULES_STEP + 1 - - setCreateProtocolAtom({ - currentStep: step, - focusStep: step, - }) - } - - const modulesWatch: DisplayModules[] = watch(MODULES_FIELD_NAME) ?? [] - - return ( - - - - {modulesWatch.length === 0 && ( - - )} - - - - - - - - ) -} - -const ButtonContainer = styled.div` - display: ${DISPLAY_FLEX}; - justify-content: ${JUSTIFY_FLEX_END}; -` diff --git a/opentrons-ai-client/src/organisms/ProtocolSectionsContainer/index.tsx b/opentrons-ai-client/src/organisms/ProtocolSectionsContainer/index.tsx index 4bbd370c00f..49314c1a143 100644 --- a/opentrons-ai-client/src/organisms/ProtocolSectionsContainer/index.tsx +++ b/opentrons-ai-client/src/organisms/ProtocolSectionsContainer/index.tsx @@ -6,8 +6,6 @@ import { ApplicationSection } from '../../organisms/ApplicationSection' import { createProtocolAtom } from '../../resources/atoms' import { useAtom } from 'jotai' import { useFormContext } from 'react-hook-form' -import { InstrumentsSection } from '../InstrumentsSection' -import { ModulesSection } from '../ModulesSection' export const APPLICATION_STEP = 0 export const INSTRUMENTS_STEP = 1 @@ -48,12 +46,12 @@ export function ProtocolSectionsContainer(): JSX.Element | null { { stepNumber: INSTRUMENTS_STEP, title: 'instruments_title', - Component: InstrumentsSection, + Component: () => Content, }, { stepNumber: MODULES_STEP, title: 'modules_title', - Component: ModulesSection, + Component: () => Content, }, { stepNumber: LABWARE_LIQUIDS_STEP, @@ -75,7 +73,7 @@ export function ProtocolSectionsContainer(): JSX.Element | null { }} isCompleted={displayCheckmark(stepNumber)} > - {focusStep === stepNumber && } + ))} diff --git a/opentrons-ai-client/src/organisms/UpdateProtocol/__tests__/UpdateProtocol.test.tsx b/opentrons-ai-client/src/organisms/UpdateProtocol/__tests__/UpdateProtocol.test.tsx deleted file mode 100644 index 04c3ad3b167..00000000000 --- a/opentrons-ai-client/src/organisms/UpdateProtocol/__tests__/UpdateProtocol.test.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { fireEvent, screen, waitFor } from '@testing-library/react' -import { describe, it, vi, beforeEach, expect } from 'vitest' -import { renderWithProviders } from '../../../__testing-utils__' -import type { NavigateFunction } from 'react-router-dom' - -import { UpdateProtocol } from '../index' -import { i18n } from '../../../i18n' - -// global.Blob = BlobPolyfill as any -global.Blob = require('node:buffer').Blob - -const mockNavigate = vi.fn() -const mockUseTrackEvent = vi.fn() -const mockUseChatData = vi.fn() - -vi.mock('../../../resources/hooks/useTrackEvent', () => ({ - useTrackEvent: () => mockUseTrackEvent, -})) - -File.prototype.text = vi.fn().mockResolvedValue('test file content') - -vi.mock('../../../resources/chatDataAtom', () => ({ - chatDataAtom: () => mockUseChatData, -})) - -vi.mock('react-router-dom', async importOriginal => { - const reactRouterDom = await importOriginal() - return { - ...reactRouterDom, - useNavigate: () => mockNavigate, - } -}) - -const render = () => { - return renderWithProviders(, { - i18nInstance: i18n, - }) -} - -describe('Update Protocol', () => { - beforeEach(() => { - vi.clearAllMocks() - }) - - it('should render', () => { - render() - expect(screen.getByText('Update an existing protocol')).toBeInTheDocument() - expect(screen.getByText('Choose file')).toBeInTheDocument() - expect(screen.getByText('Protocol file')).toBeInTheDocument() - expect(screen.getByText('Choose file')).toBeInTheDocument() - expect(screen.getByText('Type of update')).toBeInTheDocument() - expect(screen.getByText('Select an option')).toBeInTheDocument() - expect( - screen.getByText('Provide details of changes you want to make') - ).toBeInTheDocument() - }) - - it('should update the file value when the file is uploaded', async () => { - render() - - const blobParts: BlobPart[] = [ - 'x = 1\n', - 'x = 2\n', - 'x = 3\n', - 'x = 4\n', - 'print("x is 1.")\n', - ] - - const file = new File(blobParts, 'test-file.py', { type: 'text/python' }) - - fireEvent.drop(screen.getByTestId('file_drop_zone'), { - dataTransfer: { - files: [file], - }, - }) - - await waitFor(() => { - expect(screen.getByText('test-file.py')).toBeInTheDocument() - }) - }) - - it('should not proceed when you click the submit prompt when the progress percentage is not 1.0', () => { - render() - expect(mockNavigate).not.toHaveBeenCalled() - }) - - it.skip('should call navigate to the chat page when the submit prompt button is clicked when progress is 1.0', async () => { - render() - - // upload file - const blobParts: BlobPart[] = [ - 'x = 1\n', - 'x = 2\n', - 'x = 3\n', - 'x = 4\n', - 'print("x is 1.")\n', - ] - const file = new File(blobParts, 'test-file.py', { type: 'text/python' }) - fireEvent.drop(screen.getByTestId('file_drop_zone'), { - dataTransfer: { - files: [file], - }, - }) - - // input description - const describeInput = screen.getByRole('textbox') - fireEvent.change(describeInput, { target: { value: 'Test description' } }) - - expect(screen.getByDisplayValue('Test description')).toBeInTheDocument() - - // select update type - const applicationDropdown = screen.getByText('Select an option') - fireEvent.click(applicationDropdown) - - const basicOtherOption = screen.getByText('Other') - fireEvent.click(basicOtherOption) - - const submitPromptButton = screen.getByText('Submit prompt') - await waitFor(() => { - expect(submitPromptButton).toBeEnabled() - submitPromptButton.click() - }) - expect(mockNavigate).toHaveBeenCalledWith('/chat') - }) -}) diff --git a/opentrons-ai-client/src/organisms/UpdateProtocol/index.tsx b/opentrons-ai-client/src/organisms/UpdateProtocol/index.tsx deleted file mode 100644 index f0f8f4c7e12..00000000000 --- a/opentrons-ai-client/src/organisms/UpdateProtocol/index.tsx +++ /dev/null @@ -1,287 +0,0 @@ -import styled from 'styled-components' -import { - COLORS, - DIRECTION_COLUMN, - DIRECTION_ROW, - Flex, - InputField, - JUSTIFY_CENTER, - JUSTIFY_END, - LargeButton, - StyledText, - Link as LinkComponent, - DropdownMenu, -} from '@opentrons/components' -import type { DropdownOption } from '@opentrons/components' -import { UploadInput } from '../../molecules/UploadInput' -import { useEffect, useState } from 'react' -import type { ChangeEvent } from 'react' -import { Trans, useTranslation } from 'react-i18next' -import { FileUpload } from '../../molecules/FileUpload' -import { useNavigate } from 'react-router-dom' -import { chatPromptAtom, headerWithMeterAtom } from '../../resources/atoms' -import { CSSTransition } from 'react-transition-group' -import { useAtom } from 'jotai' - -const updateOptions: DropdownOption[] = [ - { - name: 'Adapt Python protocol from OT-2 to Flex', - value: 'adapt_python_protocol', - }, - { name: 'Change labware', value: 'change_labware' }, - { name: 'Change pipettes', value: 'change_pipettes' }, - { name: 'Other', value: 'other' }, -] - -const FadeWrapper = styled.div` - &.fade-enter { - opacity: 0; - } - &.fade-enter-active { - opacity: 1; - transition: opacity 1000ms; - } - &.fade-exit { - height: 100%; - opacity: 1; - } - &.fade-exit-active { - opacity: 0; - height: 0%; - transition: opacity 1000ms; - } -` - -const Container = styled(Flex)` - width: 100%; - flex-direction: ${DIRECTION_COLUMN}; - align-items: ${JUSTIFY_CENTER}; -` - -const Spacer = styled(Flex)` - height: 16px; -` - -const ContentBox = styled(Flex)` - background-color: white; - border-radius: 16px; - flex-direction: ${DIRECTION_COLUMN}; - justify-content: ${JUSTIFY_CENTER}; - padding: 32px 24px; - width: 60%; -` - -const HeadingText = styled(StyledText).attrs({ - desktopStyle: 'headingSmallBold', -})`` - -const BodyText = styled(StyledText).attrs({ - color: COLORS.grey60, - desktopStyle: 'bodyDefaultRegular', - paddingBottom: '8px', - paddingTop: '16px', -})`` - -const isValidProtocolFileName = (protocolFileName: string): boolean => { - return protocolFileName.endsWith('.py') -} - -export function UpdateProtocol(): JSX.Element { - const navigate = useNavigate() - const { t }: { t: (key: string) => string } = useTranslation( - 'protocol_generator' - ) - const [, setChatPrompt] = useAtom(chatPromptAtom) - const [headerState, setHeaderWithMeterAtom] = useAtom(headerWithMeterAtom) - const [updateType, setUpdateType] = useState(null) - const [detailsValue, setDetailsValue] = useState('') - const [fileValue, setFile] = useState(null) - const [pythonText, setPythonTextValue] = useState('') - const [errorText, setErrorText] = useState(null) - - useEffect(() => { - let progress = 0.0 - if (updateType !== null) { - progress += 0.33 - } - - if (detailsValue !== '') { - progress += 0.33 - } - - if (pythonText !== '' && fileValue !== null && errorText === null) { - progress += 0.34 - } - - setHeaderWithMeterAtom({ - displayHeaderWithMeter: true, - progress, - }) - }, [ - updateType, - detailsValue, - pythonText, - errorText, - fileValue, - setHeaderWithMeterAtom, - ]) - - const handleInputChange = (event: ChangeEvent): void => { - setDetailsValue(event.target.value) - } - - const handleFileUpload = async ( - file: File & { name: string } - ): Promise => { - if (isValidProtocolFileName(file.name)) { - const text = await file.text().catch(error => { - console.error('Error reading file:', error) - setErrorText(t('python_file_read_error')) - }) - - if (typeof text === 'string' && text !== '') { - setErrorText(null) - console.log('File read successfully:\n', text) - setPythonTextValue(text) - } else { - setErrorText(t('file_length_error')) - } - - setFile(file) - } else { - setErrorText(t('python_file_type_error')) - setFile(file) - } - } - - function processDataAndNavigateToChat(): void { - const introText = t('modify_intro') - const originalCodeText = - t('modify_python_code') + `\`\`\`python\n` + pythonText + `\n\`\`\`\n\n` - const updateTypeText = - t('modify_type_of_update') + updateType?.value + `\n\n` - const detailsText = t('modify_details_of_change') + detailsValue + '\n' - - const chatPrompt = `${introText}${originalCodeText}${updateTypeText}${detailsText}` - - console.log(chatPrompt) - - setChatPrompt(chatData => chatPrompt) - navigate('/chat') - } - - return ( - - - - {t('update_existing_protocol')} - {t('protocol_file')} - - - - {fileValue !== null ? ( - - - - ) : null} - - - - - - - - ), - }} - /> - - } - onUpload={async function (file: File) { - try { - await handleFileUpload(file) - } catch (error) { - // todo perhaps make this a toast? - console.error('Error uploading file:', error) - } - }} - /> - - - - - - { - const selectedOption = updateOptions.find(v => v.value === value) - if (selectedOption != null) { - setUpdateType(selectedOption) - } - }} - /> - - {t('provide_details_of_changes')} - - - - - - - ) -} diff --git a/opentrons-ai-client/src/pages/Chat/index.tsx b/opentrons-ai-client/src/pages/Chat/index.tsx deleted file mode 100644 index 7bedeb8dffe..00000000000 --- a/opentrons-ai-client/src/pages/Chat/index.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useForm, FormProvider } from 'react-hook-form' -import { DIRECTION_COLUMN, Flex, SPACING } from '@opentrons/components' - -import { useAtom } from 'jotai' -import { useRef, useEffect } from 'react' -import { - chatDataAtom, - feedbackModalAtom, - scrollToBottomAtom, -} from '../../resources/atoms' -import { ChatDisplay } from '../../molecules/ChatDisplay' -import { ChatFooter } from '../../molecules/ChatFooter' -import styled from 'styled-components' -import { FeedbackModal } from '../../molecules/FeedbackModal' - -export interface InputType { - userPrompt: string -} - -export function Chat(): JSX.Element | null { - const methods = useForm({ - defaultValues: { - userPrompt: '', - }, - }) - - const [chatData] = useAtom(chatDataAtom) - const scrollRef = useRef(null) - const [showFeedbackModal] = useAtom(feedbackModalAtom) - const [scrollToBottom] = useAtom(scrollToBottomAtom) - - useEffect(() => { - if (scrollRef.current != null) - scrollRef.current.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - inline: 'nearest', - }) - }, [chatData.length, scrollToBottom]) - - return ( - - - - - {chatData.length > 0 - ? chatData.map((chat, index) => ( - - )) - : null} - - - - - {showFeedbackModal ? : null} - - - ) -} - -const ChatDataContainer = styled(Flex)` - flex-direction: ${DIRECTION_COLUMN}; - width: 100%; -` diff --git a/opentrons-ai-client/src/pages/CreateProtocol/__tests__/CreateProtocol.test.tsx b/opentrons-ai-client/src/pages/CreateProtocol/__tests__/CreateProtocol.test.tsx index 9182f1778ca..871bab07a7b 100644 --- a/opentrons-ai-client/src/pages/CreateProtocol/__tests__/CreateProtocol.test.tsx +++ b/opentrons-ai-client/src/pages/CreateProtocol/__tests__/CreateProtocol.test.tsx @@ -4,11 +4,7 @@ import { renderWithProviders } from '../../../__testing-utils__' import { i18n } from '../../../i18n' import { CreateProtocol } from '..' import { Provider } from 'jotai' -import { - fillApplicationSectionAndClickConfirm, - fillInstrumentsSectionAndClickConfirm, - fillModulesSectionAndClickConfirm, -} from '../../../resources/utils/createProtocolTestUtils' +import { fillApplicationSectionAndClickConfirm } from '../../../resources/utils/createProtocolTestUtils' const render = (): ReturnType => { return renderWithProviders( @@ -42,7 +38,7 @@ describe('CreateProtocol', () => { const previewItems = screen.getAllByTestId('Tag_default') - expect(previewItems).toHaveLength(4) + expect(previewItems).toHaveLength(2) expect(previewItems[0]).toHaveTextContent('Basic aliquoting') expect(previewItems[1]).toHaveTextContent('Test description') }) @@ -85,54 +81,4 @@ describe('CreateProtocol', () => { expect(screen.getByTestId('accordion-ot-check')).toBeInTheDocument() }) - - it('should display the Prompt preview correctly for Instruments section', async () => { - render() - - await fillApplicationSectionAndClickConfirm() - await fillInstrumentsSectionAndClickConfirm() - - const previewItems = screen.getAllByTestId('Tag_default') - - expect(previewItems).toHaveLength(6) - expect(previewItems[0]).toHaveTextContent('Basic aliquoting') - expect(previewItems[1]).toHaveTextContent('Test description') - expect(previewItems[2]).toHaveTextContent('Opentrons Flex') - expect(previewItems[3]).toHaveTextContent('Flex 1-Channel 50 μL') - expect(previewItems[4]).toHaveTextContent('Flex 8-Channel 50 μL') - }) - - it('should open the Modules section when the Instruments section is completed', async () => { - render() - - expect(screen.getByRole('button', { name: 'Application' })).toHaveAttribute( - 'aria-expanded', - 'true' - ) - - await fillApplicationSectionAndClickConfirm() - await fillInstrumentsSectionAndClickConfirm() - - await waitFor(() => { - expect(screen.getByRole('button', { name: 'Modules' })).toHaveAttribute( - 'aria-expanded', - 'true' - ) - }) - }) - - it('should display the Prompt preview correctly for Modules section', async () => { - render() - - await fillApplicationSectionAndClickConfirm() - await fillInstrumentsSectionAndClickConfirm() - await fillModulesSectionAndClickConfirm() - - const previewItems = screen.getAllByTestId('Tag_default') - - expect(previewItems).toHaveLength(7) - expect(previewItems[6]).toHaveTextContent( - 'Heater-Shaker Module GEN1 with Opentrons 96 Deep Well Heater-Shaker Adapter' - ) - }) }) diff --git a/opentrons-ai-client/src/pages/CreateProtocol/index.tsx b/opentrons-ai-client/src/pages/CreateProtocol/index.tsx index b3bbd83169e..346e43c879a 100644 --- a/opentrons-ai-client/src/pages/CreateProtocol/index.tsx +++ b/opentrons-ai-client/src/pages/CreateProtocol/index.tsx @@ -11,23 +11,14 @@ import { useForm, FormProvider } from 'react-hook-form' import { createProtocolAtom, headerWithMeterAtom } from '../../resources/atoms' import { useAtom } from 'jotai' import { ProtocolSectionsContainer } from '../../organisms/ProtocolSectionsContainer' -import { generatePromptPreviewData } from '../../resources/utils/createProtocolUtils' -import type { DisplayModules } from '../../organisms/ModulesSection' +import { OTHER } from '../../organisms/ApplicationSection' -export interface CreateProtocolFormData { +interface CreateProtocolFormData { application: { scientificApplication: string otherApplication?: string description: string } - instruments: { - robot: string - pipettes: string - leftPipette: string - rightPipette: string - flexGripper: string - } - modules: DisplayModules[] } const TOTAL_STEPS = 5 @@ -35,7 +26,7 @@ const TOTAL_STEPS = 5 export function CreateProtocol(): JSX.Element | null { const { t } = useTranslation('create_protocol') const [, setHeaderWithMeterAtom] = useAtom(headerWithMeterAtom) - const [{ currentStep }, setCreateProtocolAtom] = useAtom(createProtocolAtom) + const [{ currentStep }] = useAtom(createProtocolAtom) const methods = useForm({ defaultValues: { @@ -44,7 +35,6 @@ export function CreateProtocol(): JSX.Element | null { otherApplication: '', description: '', }, - instruments: {}, }, }) @@ -59,20 +49,35 @@ export function CreateProtocol(): JSX.Element | null { }) }, [currentStep]) - useEffect(() => { - return () => { - setHeaderWithMeterAtom({ - displayHeaderWithMeter: false, - progress: 0, - }) + function generatePromptPreviewApplicationItems(): string[] { + const { + application: { scientificApplication, otherApplication, description }, + } = methods.watch() - methods.reset() - setCreateProtocolAtom({ - currentStep: 0, - focusStep: 0, - }) - } - }, []) + const scientificOrOtherApplication = + scientificApplication === OTHER + ? otherApplication + : scientificApplication !== '' + ? t(scientificApplication) + : '' + + return [ + scientificOrOtherApplication !== '' && scientificOrOtherApplication, + description !== '' && description, + ].filter(Boolean) + } + + function generatePromptPreviewData(): Array<{ + title: string + items: string[] + }> { + return [ + { + title: t('application_title'), + items: generatePromptPreviewApplicationItems(), + }, + ] + } return ( @@ -82,14 +87,13 @@ export function CreateProtocol(): JSX.Element | null { gap={SPACING.spacing32} margin={`${SPACING.spacing16} ${SPACING.spacing16}`} height="100%" - width="100%" > diff --git a/opentrons-ai-client/src/pages/Landing/index.tsx b/opentrons-ai-client/src/pages/Landing/index.tsx index e2cb76452ca..cda92f7052b 100644 --- a/opentrons-ai-client/src/pages/Landing/index.tsx +++ b/opentrons-ai-client/src/pages/Landing/index.tsx @@ -16,20 +16,12 @@ import { useTranslation } from 'react-i18next' import { useIsMobile } from '../../resources/hooks/useIsMobile' import { useNavigate } from 'react-router-dom' import { useTrackEvent } from '../../resources/hooks/useTrackEvent' -import { useAtom } from 'jotai' -import { headerWithMeterAtom } from '../../resources/atoms' -import { useEffect } from 'react' export function Landing(): JSX.Element | null { const navigate = useNavigate() const { t } = useTranslation('protocol_generator') const isMobile = useIsMobile() const trackEvent = useTrackEvent() - const [, setHeaderWithMeterAtom] = useAtom(headerWithMeterAtom) - - useEffect(() => { - setHeaderWithMeterAtom({ displayHeaderWithMeter: false, progress: 0.0 }) - }, [setHeaderWithMeterAtom]) function handleCreateNewProtocol(): void { trackEvent({ name: 'create-new-protocol', properties: {} }) @@ -57,7 +49,6 @@ export function Landing(): JSX.Element | null { justifyContent={JUSTIFY_CENTER} width="100%" maxWidth="548px" - minHeight="600px" gridGap={SPACING.spacing16} textAlign={TEXT_ALIGN_CENTER} > diff --git a/opentrons-ai-client/src/resources/atoms.ts b/opentrons-ai-client/src/resources/atoms.ts index ffacfe7afd8..3ea530c65f6 100644 --- a/opentrons-ai-client/src/resources/atoms.ts +++ b/opentrons-ai-client/src/resources/atoms.ts @@ -11,16 +11,8 @@ import type { /** ChatDataAtom is for chat data (user prompt and response from OpenAI API) */ export const chatDataAtom = atom([]) -/** ChatPromptAtom is for the prefilled userprompt when navigating to the chat page from existing/new protocol pages */ -export const chatPromptAtom = atom('') - -/** Scroll to bottom of chat atom */ -export const scrollToBottomAtom = atom(false) - export const chatHistoryAtom = atom([]) -export const feedbackModalAtom = atom(false) - export const tokenAtom = atom(null) export const mixpanelAtom = atom({ @@ -37,5 +29,3 @@ export const createProtocolAtom = atom({ currentStep: 0, focusStep: 0, }) - -export const displayExitConfirmModalAtom = atom(false) diff --git a/opentrons-ai-client/src/resources/types.ts b/opentrons-ai-client/src/resources/types.ts index cd6be5dc1b7..410bdfd98a6 100644 --- a/opentrons-ai-client/src/resources/types.ts +++ b/opentrons-ai-client/src/resources/types.ts @@ -8,8 +8,6 @@ export interface ChatData { reply: string /** for testing purpose will be removed and this is not used in the app */ fake?: boolean - /** uuid to map the chat prompt request to the response from the LLM */ - requestId: string } export interface Chat { diff --git a/opentrons-ai-client/src/resources/utils/createProtocolTestUtils.tsx b/opentrons-ai-client/src/resources/utils/createProtocolTestUtils.tsx index e612afa3dc4..8a24224394e 100644 --- a/opentrons-ai-client/src/resources/utils/createProtocolTestUtils.tsx +++ b/opentrons-ai-client/src/resources/utils/createProtocolTestUtils.tsx @@ -17,42 +17,3 @@ export async function fillApplicationSectionAndClickConfirm(): Promise { }) fireEvent.click(confirmButton) } - -export async function fillInstrumentsSectionAndClickConfirm(): Promise { - const leftMount = screen.getAllByText('Choose pipette')[0] - fireEvent.click(leftMount) - fireEvent.click(screen.getByText('Flex 1-Channel 50 μL')) - - const rightMount = screen.getAllByText('Choose pipette')[0] - fireEvent.click(rightMount) - fireEvent.click(screen.getByText('Flex 8-Channel 50 μL')) - - const confirmButton = screen.getByText('Confirm') - await waitFor(() => { - expect(confirmButton).toBeEnabled() - }) - fireEvent.click(confirmButton) -} - -export async function fillModulesSectionAndClickConfirm(): Promise { - const firstModuleButton = screen.getByText('Heater-Shaker Module GEN1') - fireEvent.click(firstModuleButton) - - expect( - screen.getAllByText('Heater-Shaker Module GEN1')[1] - ).toBeInTheDocument() - - const adapterDropdown = screen.getByText('Choose an adapter') - fireEvent.click(adapterDropdown) - - const adapterOption = screen.getByText( - 'Opentrons 96 Deep Well Heater-Shaker Adapter' - ) - fireEvent.click(adapterOption) - - const confirmButton = screen.getByText('Confirm') - await waitFor(() => { - expect(confirmButton).toBeEnabled() - }) - fireEvent.click(confirmButton) -} diff --git a/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx b/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx deleted file mode 100644 index 7e137fef854..00000000000 --- a/opentrons-ai-client/src/resources/utils/createProtocolUtils.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { getPipetteSpecsV2 } from '@opentrons/shared-data' -import type { PipetteName } from '@opentrons/shared-data' -import { OTHER } from '../../organisms/ApplicationSection' -import { - TWO_PIPETTES, - OPENTRONS_OT2, - OPENTRONS_FLEX, - FLEX_GRIPPER, -} from '../../organisms/InstrumentsSection' -import type { UseFormWatch } from 'react-hook-form' -import type { CreateProtocolFormData } from '../../pages/CreateProtocol' - -export function generatePromptPreviewApplicationItems( - watch: UseFormWatch, - t: any -): string[] { - const { - application: { scientificApplication, otherApplication, description }, - } = watch() - - const scientificOrOtherApplication = - scientificApplication === OTHER - ? otherApplication - : scientificApplication !== '' - ? t(scientificApplication) - : '' - - return [ - scientificOrOtherApplication !== '' && scientificOrOtherApplication, - description !== '' && description, - ].filter(Boolean) -} - -export function generatePromptPreviewInstrumentItems( - watch: UseFormWatch, - t: any -): string[] { - const { - instruments: { robot, pipettes, leftPipette, rightPipette, flexGripper }, - } = watch() - - const items = [] - - robot !== '' && items.push(t(robot)) - - if (pipettes === TWO_PIPETTES || robot === OPENTRONS_OT2) { - leftPipette !== '' && - items.push(getPipetteSpecsV2(leftPipette as PipetteName)?.displayName) - rightPipette !== '' && - items.push(getPipetteSpecsV2(rightPipette as PipetteName)?.displayName) - } else { - items.push(pipettes !== '' && t(pipettes)) - } - - if (robot === OPENTRONS_FLEX && flexGripper === FLEX_GRIPPER) { - items.push(t(flexGripper)) - } - - return items.filter(Boolean) -} - -export function generatePromptPreviewModulesItems( - watch: UseFormWatch, - t: any -): string[] { - const { modules } = watch() - - if (modules === undefined || modules?.length === 0) return [] - - const items = modules?.map(module => - module.adapter === undefined || module.adapter?.name === '' - ? module.name - : `${module.name} with ${module.adapter.name}` - ) - - return items.filter(Boolean) -} - -export function generatePromptPreviewData( - watch: UseFormWatch, - t: any -): Array<{ - title: string - items: string[] -}> { - return [ - { - title: t('application_title'), - items: generatePromptPreviewApplicationItems(watch, t), - }, - { - title: t('instruments_title'), - items: generatePromptPreviewInstrumentItems(watch, t), - }, - { - title: t('modules_title'), - items: generatePromptPreviewModulesItems(watch, t), - }, - ] -} diff --git a/protocol-designer/src/assets/images/path_multiAspirate.gif b/protocol-designer/src/assets/images/path_multiAspirate.gif index 544bf5bf53d..987ca65357c 100644 Binary files a/protocol-designer/src/assets/images/path_multiAspirate.gif and b/protocol-designer/src/assets/images/path_multiAspirate.gif differ diff --git a/protocol-designer/src/assets/images/path_multiDispense.gif b/protocol-designer/src/assets/images/path_multiDispense.gif index f547f26e17d..e36afb13a4b 100644 Binary files a/protocol-designer/src/assets/images/path_multiDispense.gif and b/protocol-designer/src/assets/images/path_multiDispense.gif differ diff --git a/protocol-designer/src/assets/images/path_single.gif b/protocol-designer/src/assets/images/path_single.gif index 50481ddfa66..2582be8241b 100644 Binary files a/protocol-designer/src/assets/images/path_single.gif and b/protocol-designer/src/assets/images/path_single.gif differ diff --git a/protocol-designer/src/assets/localization/en/create_new_protocol.json b/protocol-designer/src/assets/localization/en/create_new_protocol.json index 6b0cd5a6ade..1b9d1f72898 100644 --- a/protocol-designer/src/assets/localization/en/create_new_protocol.json +++ b/protocol-designer/src/assets/localization/en/create_new_protocol.json @@ -13,11 +13,11 @@ "incompatible_tip_body": "Using a pipette with an incompatible tip rack may result reduce pipette accuracy and collisions. We strongly recommend that you do not pair a pipette with an incompatible tip rack.", "incompatible_tips": "Incompatible tips", "labware_name": "Labware name", - "left_right": "Left + Right", + "left_right": "Left+Right", "modules_added": "Modules added", "name": "Name", "need_gripper": "Do you want to move labware automatically with the gripper?", - "pipette": "{{mount}} Mount", + "pip": "{{mount}} Pipette", "pipette_gen": "Pipette generation", "pipette_tips": "Pipette tips", "pipette_type": "Pipette type", @@ -32,8 +32,6 @@ "show_default_tips": "Show default tips", "show_tips": "Show incompatible tips", "slots_limit_reached": "Slots limit reached", - "staging_area_has_labware": "This staging area slot has labware", - "staging_area_will_delete_labware": "The staging area slot that you are about to delete has labware placed on it. If you make these changes to your protocol starting deck, the labware will be deleted as well.", "stagingArea": "Staging area", "swap_pipettes": "Swap pipettes", "tell_us": "Tell us about your protocol", diff --git a/protocol-designer/src/assets/localization/en/form.json b/protocol-designer/src/assets/localization/en/form.json index 5ed9f9d8331..1f3f4bc9e34 100644 --- a/protocol-designer/src/assets/localization/en/form.json +++ b/protocol-designer/src/assets/localization/en/form.json @@ -93,7 +93,7 @@ } }, "location": { - "dropTip": "Tip drop location", + "dropTip": "drop tip location", "label": "location", "pickUp": "pick up tip" }, @@ -176,12 +176,6 @@ "step_notes": { "label": "Step Notes" }, - "temperature": { - "caption": "Valid range is 4 – 95˚C", - "setTemperature": "Temperature", - "toggleOff": "Deactivate", - "toggleOn": "Set" - }, "thermocyclerAction": { "options": { "profile": "Program a Thermocycler profile", @@ -274,7 +268,6 @@ }, "mixRepetitions": "repetitions", "mixVolumeLabel": "mix volume", - "moduleState": "Module state", "multiDispenseOptionsLabel": "multi-dispense options", "section": { "dropTip": "drop tip", @@ -285,10 +278,9 @@ "tipRack": "tip rack", "wellSelectionLabel": { "columns": "columns", - "columns_aspirate_wells": "Source columns", - "columns_dispense_wells": "Destination columns", + "columns_aspirate_wells": "Select source columns", + "columns_dispense_wells": "Select destination columns", "columns_mix_wells": "Select columns", - "columns_wells": "Mix wells", "wells": "wells", "wells_aspirate_wells": "Select source wells", "wells_dispense_wells": "Select destination wells", diff --git a/protocol-designer/src/assets/localization/en/modal.json b/protocol-designer/src/assets/localization/en/modal.json index 2c065e2d6b6..8b891c0f405 100644 --- a/protocol-designer/src/assets/localization/en/modal.json +++ b/protocol-designer/src/assets/localization/en/modal.json @@ -55,8 +55,7 @@ "body1": "Welcome to Protocol Designer 8.2.0!", "body2": "We’re excited to release the new Opentrons Protocol Designer, now with a fresh redesign! Enjoy the same functionality with the added ability to:", "body3": "Add multiple Heater-Shaker Modules and Magnetic Blocks to the deck (Flex only).", - "body4": "All protocols now require Opentrons App version 8.2.0+ to run.", - "body5": "For more information, see the Protocol Designer Instruction Manual." + "body4": "All protocols now require Opentrons App version 8.0.0+ to run." } }, "labware_selection": { diff --git a/protocol-designer/src/assets/localization/en/protocol_overview.json b/protocol-designer/src/assets/localization/en/protocol_overview.json index b03097576cd..e1975f4e4ef 100644 --- a/protocol-designer/src/assets/localization/en/protocol_overview.json +++ b/protocol-designer/src/assets/localization/en/protocol_overview.json @@ -8,12 +8,11 @@ "edit_protocol": "Edit protocol", "edit": "Edit", "export_protocol": "Export protocol", - "extension": "Extension Mount", + "extension": "Extension mount", "gripper": "Opentrons Flex Gripper", "instruments": "Instruments", "labware": "Labware", - "left_mount": "Left Mount", - "left_right_mount": "Left + Right Mount", + "left_pip": "Left pipette", "liquid_defs": "Liquid Definitions", "liquids": "Liquids", "materials_list": "Materials list", @@ -29,7 +28,7 @@ "protocol_metadata": "Protocol Metadata", "protocol_steps": "Protocol Steps", "required_app_version": "Required app version", - "right_mount": "Right Mount", + "right_pip": "Right pipette", "robotType": "Robot type", "starting_deck": "Protocol Starting Deck", "steps": "{{count}} steps", diff --git a/protocol-designer/src/assets/localization/en/protocol_steps.json b/protocol-designer/src/assets/localization/en/protocol_steps.json index 56becba78e7..ea0339978ec 100644 --- a/protocol-designer/src/assets/localization/en/protocol_steps.json +++ b/protocol-designer/src/assets/localization/en/protocol_steps.json @@ -2,7 +2,6 @@ "add_details": "Add step details", "advanced_settings": "Advanced pipetting settings", "air_gap_volume": "Air gap volume", - "aspirate_labware": "Source labware", "aspirate": "Aspirate", "aspirated": "Aspirated", "batch_edit_steps": "Batch edit steps", @@ -17,7 +16,6 @@ "delay_position": "Delay position from bottom", "delete_steps": "Delete steps", "delete": "Delete step", - "dispense_labware": "Destination labware", "dispense": "Dispense", "dispensed": "Dispensed", "disposal_volume": "Disposal volume", @@ -74,7 +72,6 @@ "untilTemperature": "Pausing until{{module}}reaches", "untilTime": "Pausing for" }, - "pipette": "Pipette", "protocol_steps": "Protocol steps", "protocol_timeline": "Protocol timeline", "rename": "Rename", @@ -127,12 +124,10 @@ } }, "time": "Time", - "tiprack": "Tiprack", "tip_position": "{{prefix}} tip position", "touch_tip_position": "Touch tip position from top", "valid_range": "Valid range between {{min}} - {{max}} {{unit}}", "view_details": "View details", - "volume_per_well": "Volume per well", "well_name": "Well {{wellName}}", "well_order_title": "{{prefix}} well order", "well_position": "Well position (x,y,z): " diff --git a/protocol-designer/src/assets/localization/en/shared.json b/protocol-designer/src/assets/localization/en/shared.json index bdfbd8a2b36..dafebf2b62d 100644 --- a/protocol-designer/src/assets/localization/en/shared.json +++ b/protocol-designer/src/assets/localization/en/shared.json @@ -44,7 +44,7 @@ "labware_name_conflict": "Duplicate labware name", "labware": "Labware", "learn_more": "Learn more about the recent changes in the {{version}} release.", - "left_right": "Left + Right", + "left_right": "Left+Right", "left": "Left", "liquid": "Liquid", "magneticmoduletype": "Magnetic Module", @@ -104,7 +104,6 @@ "ot2": "Opentrons OT-2", "overwrite_labware": "Overwrite labware", "overwrite": "Click Overwrite to replace the existing labware with the new labware.", - "part": "Part {{current}} / {{max}}", "pipette": "Pipette", "pd_version": "Protocol designer version", "primary_order": "Primary order", diff --git a/protocol-designer/src/components/StepEditForm/fields/DropTipField/__tests__/DropTipField.test.tsx b/protocol-designer/src/components/StepEditForm/fields/DropTipField/__tests__/DropTipField.test.tsx index 195096f0c07..8c5426bdaae 100644 --- a/protocol-designer/src/components/StepEditForm/fields/DropTipField/__tests__/DropTipField.test.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/DropTipField/__tests__/DropTipField.test.tsx @@ -55,7 +55,7 @@ describe('DropTipField', () => { }) it('renders the label and dropdown field with trash bin selected as default', () => { render(props) - screen.getByText('Tip drop location') + screen.getByText('drop tip location') screen.getByRole('combobox', { name: '' }) screen.getByRole('option', { name: 'Trash Bin' }) screen.getByRole('option', { name: 'mock tip' }) diff --git a/protocol-designer/src/form-types.ts b/protocol-designer/src/form-types.ts index 1a9962ae582..58da6b5676b 100644 --- a/protocol-designer/src/form-types.ts +++ b/protocol-designer/src/form-types.ts @@ -187,6 +187,9 @@ export type PauseForm = AnnotationFields & { | typeof PAUSE_UNTIL_RESUME | typeof PAUSE_UNTIL_TIME | typeof PAUSE_UNTIL_TEMP + pauseHour?: string + pauseMinute?: string + pauseSecond?: string pauseMessage?: string pauseTemperature?: string pauseTime?: string @@ -361,7 +364,9 @@ export interface HydratedTemperatureFormData { } export interface HydratedHeaterShakerFormData { heaterShakerSetTimer: 'true' | 'false' | null - heaterShakerTimer: string | null + heaterShakerTimerMinutes: string | null + heaterShakerTimerSeconds: string | null + heaterShakerTimer?: string | null id: string latchOpen: boolean moduleId: string diff --git a/protocol-designer/src/load-file/migration/8_2_0.ts b/protocol-designer/src/load-file/migration/8_2_0.ts deleted file mode 100644 index 30be478de23..00000000000 --- a/protocol-designer/src/load-file/migration/8_2_0.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' -import { PAUSE_UNTIL_TIME } from '../../constants' - -import type { ProtocolFile } from '@opentrons/shared-data' -import type { PauseForm } from '../../form-types' -import type { DesignerApplicationData } from './utils/getLoadLiquidCommands' - -const getTimeFromIndividualUnits = ( - seconds: any, - minutes: any, - hours?: any -): string => { - const hoursString = hours !== undefined ? `${hours ?? 0}:` : '' - return `${hoursString}${minutes ?? 0}:${seconds ?? 0}` -} - -export const migrateFile = ( - appData: ProtocolFile -): ProtocolFile => { - const { designerApplication } = appData - - if (designerApplication == null || designerApplication?.data == null) { - throw Error('The designerApplication key in your file is corrupt.') - } - - const savedStepForms = designerApplication.data - ?.savedStepForms as DesignerApplicationData['savedStepForms'] - - const savedStepsWithConsolidatedTimeField = Object.values( - savedStepForms - ).reduce((acc, form) => { - if (form.stepType === 'pause') { - const { - id, - pauseHour, - pauseMinute, - pauseSecond, - pauseTime, - pauseAction, - } = form - const pauseFormIndividualTimeUnitsRemoved = Object.keys( - form as PauseForm - ).reduce( - (accInner, key) => - !['pauseSecond', 'pauseMinute', 'pauseHour'].includes(key) - ? { ...accInner, [key]: form[key] } - : accInner, - { pauseTime } - ) - - if (pauseAction !== PAUSE_UNTIL_TIME) { - return { - ...acc, - [id]: { ...pauseFormIndividualTimeUnitsRemoved, pauseTime: null }, - } - } - - return pauseTime != null - ? { ...acc, [id]: pauseFormIndividualTimeUnitsRemoved } - : { - ...acc, - [id]: { - ...pauseFormIndividualTimeUnitsRemoved, - pauseTime: getTimeFromIndividualUnits( - pauseSecond, - pauseMinute, - pauseHour - ), - }, - } - } else if (form.stepType === 'heaterShaker') { - const { - id, - heaterShakerTimerMinutes, - heaterShakerTimerSeconds, - heaterShakerTimer, - heaterShakerSetTimer, - } = form - - const heaterShakerFormIndividualTimeUnitsRemoved = Object.keys( - form as Object - ).reduce( - (accInner, key) => - !['heaterShakerTimerMinutes', 'heaterShakerTimerSeconds'].includes( - key - ) - ? { ...accInner, [key]: form[key] } - : accInner, - { heaterShakerTimer } - ) - if (!heaterShakerSetTimer) { - return { - ...acc, - [id]: { - ...heaterShakerFormIndividualTimeUnitsRemoved, - heaterShakerTimer: null, - }, - } - } - - return heaterShakerTimer != null - ? { ...acc, [id]: heaterShakerFormIndividualTimeUnitsRemoved } - : { - ...acc, - [id]: { - ...heaterShakerFormIndividualTimeUnitsRemoved, - heaterShakerTimer: getTimeFromIndividualUnits( - heaterShakerTimerSeconds, - heaterShakerTimerMinutes - ), - }, - } - } - return acc - }, {}) - - const updatedInitialStep = Object.values(savedStepForms).reduce( - (acc, form) => { - const { id, moduleLocationUpdate } = form - if ( - id === '__INITIAL_DECK_SETUP_STEP__' && - appData.robot.model === OT2_ROBOT_TYPE - ) { - const moduleLocationUpdateThermocyclerOT2Slot = Object.keys( - moduleLocationUpdate as Record - ).reduce((acc, key) => { - return moduleLocationUpdate[key] === 'span7_8_10_11' - ? { ...acc, [key]: '7' } - : { ...acc, [key]: moduleLocationUpdate[key] } - }, {}) - return { - ...acc, - [id]: { - ...form, - moduleLocationUpdate: moduleLocationUpdateThermocyclerOT2Slot, - }, - } - } - return acc - }, - {} - ) - - return { - ...appData, - designerApplication: { - ...designerApplication, - data: { - ...designerApplication.data, - savedStepForms: { - ...designerApplication.data.savedStepForms, - ...savedStepsWithConsolidatedTimeField, - ...updatedInitialStep, - }, - }, - }, - } -} diff --git a/protocol-designer/src/load-file/migration/index.ts b/protocol-designer/src/load-file/migration/index.ts index 1ef3f346153..531e6fc4dd9 100644 --- a/protocol-designer/src/load-file/migration/index.ts +++ b/protocol-designer/src/load-file/migration/index.ts @@ -11,7 +11,6 @@ import { migrateFile as migrateFileSix } from './6_0_0' import { migrateFile as migrateFileSeven } from './7_0_0' import { migrateFile as migrateFileEight } from './8_0_0' import { migrateFile as migrateFileEightOne } from './8_1_0' -import { migrateFile as migrateFileEightTwo } from './8_2_0' import type { PDProtocolFile } from '../../file-types' export const OLDEST_MIGRATEABLE_VERSION = '1.0.0' @@ -52,8 +51,6 @@ const allMigrationsByVersion: MigrationsByVersion = { '8.0.0': migrateFileEight, // @ts-expect-error '8.1.0': migrateFileEightOne, - // @ts-expect-error - '8.2.0': migrateFileEightTwo, } export const migration = ( file: any diff --git a/protocol-designer/src/molecules/DropdownStepFormField/index.tsx b/protocol-designer/src/molecules/DropdownStepFormField/index.tsx index 39da4a74ca9..6cd81c742c4 100644 --- a/protocol-designer/src/molecules/DropdownStepFormField/index.tsx +++ b/protocol-designer/src/molecules/DropdownStepFormField/index.tsx @@ -1,14 +1,5 @@ import { useTranslation } from 'react-i18next' -import { useEffect } from 'react' -import { - COLORS, - DIRECTION_COLUMN, - DropdownMenu, - Flex, - ListItem, - SPACING, - StyledText, -} from '@opentrons/components' +import { DropdownMenu, Flex, SPACING } from '@opentrons/components' import type { Options } from '@opentrons/components' import type { FieldProps } from '../../pages/Designer/ProtocolSteps/StepForm/types' @@ -37,49 +28,24 @@ export function DropdownStepFormField( const { t } = useTranslation('tooltip') const availableOptionId = options.find(opt => opt.value === value) - useEffect(() => { - if (options.length === 1) { - updateValue(options[0].value) - } - }, []) - return ( - {options.length > 1 || options.length === 0 ? ( - { - updateValue(value) - }} - /> - ) : ( - - - {title} - - - - - {options[0].name} - - - - - )} + { + updateValue(value) + }} + /> ) } diff --git a/protocol-designer/src/molecules/InputStepFormField/index.tsx b/protocol-designer/src/molecules/InputStepFormField/index.tsx index 13b113e7215..b0cf78383ea 100644 --- a/protocol-designer/src/molecules/InputStepFormField/index.tsx +++ b/protocol-designer/src/molecules/InputStepFormField/index.tsx @@ -12,7 +12,6 @@ interface InputStepFormFieldProps extends FieldProps { showTooltip?: boolean caption?: string formLevelError?: string | null - placeholder?: string } export function InputStepFormField( @@ -34,7 +33,6 @@ export function InputStepFormField( formLevelError, setIsPristine, type, - placeholder, ...otherProps } = props const { t } = useTranslation('tooltip') @@ -61,7 +59,6 @@ export function InputStepFormField( }} value={value ? String(value) : null} units={units} - placeholder={placeholder} /> ) diff --git a/protocol-designer/src/molecules/ToggleExpandStepFormField/index.tsx b/protocol-designer/src/molecules/ToggleExpandStepFormField/index.tsx index 5c34b465647..3bb253dca9d 100644 --- a/protocol-designer/src/molecules/ToggleExpandStepFormField/index.tsx +++ b/protocol-designer/src/molecules/ToggleExpandStepFormField/index.tsx @@ -46,7 +46,7 @@ export function ToggleExpandStepFormField( } = props const resetFieldValue = (): void => { - restProps.updateValue(null) + restProps.updateValue('null') } const onToggleUpdateValue = (): void => { diff --git a/protocol-designer/src/organisms/Alerts/FormAlerts.tsx b/protocol-designer/src/organisms/Alerts/FormAlerts.tsx index f9d7166943d..ad470253e69 100644 --- a/protocol-designer/src/organisms/Alerts/FormAlerts.tsx +++ b/protocol-designer/src/organisms/Alerts/FormAlerts.tsx @@ -22,7 +22,6 @@ import { } from '../../pages/Designer/ProtocolSteps/StepForm/utils' import { WarningContents } from './WarningContents' -import type { ReactNode } from 'react' import type { ProfileItem } from '@opentrons/step-generation' import type { StepFieldName } from '../../form-types' import type { ProfileFormError } from '../../steplist/formLevel/profileErrors' @@ -32,16 +31,10 @@ interface FormAlertsProps { showFormErrorsAndWarnings: boolean focusedField?: StepFieldName | null dirtyFields?: StepFieldName[] - page: number -} - -interface WarningType { - title: string - description: ReactNode | null } function FormAlertsComponent(props: FormAlertsProps): JSX.Element | null { - const { showFormErrorsAndWarnings, focusedField, dirtyFields, page } = props + const { showFormErrorsAndWarnings, focusedField, dirtyFields } = props const { t } = useTranslation('alert') const dispatch = useDispatch() @@ -77,8 +70,6 @@ function FormAlertsComponent(props: FormAlertsProps): JSX.Element | null { focusedField, dirtyFields: dirtyFields ?? [], errors: formLevelErrorsForUnsavedForm, - page, - showErrors: showFormErrorsAndWarnings, }) const profileItemsById: Record | null | undefined = @@ -132,32 +123,21 @@ function FormAlertsComponent(props: FormAlertsProps): JSX.Element | null { ) - const filteredFormErrorsForBanner = visibleFormErrors.reduce( - (acc, error) => { - return error.showAtForm ?? true - ? [ - ...acc, - { - title: error.title, - description: error.body ?? null, - }, - ] - : acc - }, - [] - ) - const formErrors = [ - ...filteredFormErrorsForBanner, - ...visibleDynamicFieldFormErrors.map(error => ({ + ...visibleFormErrors.map(error => ({ title: error.title, description: error.body ?? null, + showAtForm: error.showAtForm ?? true, + })), + ...visibleDynamicFieldFormErrors.map(error => ({ + title: error.title, + description: error.body || null, })), ] const formWarnings = visibleFormWarnings.map(warning => ({ title: warning.title, - description: warning.body ?? null, + description: warning.body || null, dismissId: warning.type, })) diff --git a/protocol-designer/src/organisms/Alerts/KnowledgeLink.tsx b/protocol-designer/src/organisms/Alerts/KnowledgeLink.tsx new file mode 100644 index 00000000000..f29919ba6ce --- /dev/null +++ b/protocol-designer/src/organisms/Alerts/KnowledgeLink.tsx @@ -0,0 +1,17 @@ +import type * as React from 'react' +import { Link } from '@opentrons/components' +import { links } from './linkConstants' + +interface KnowledgeLinkProps { + to: keyof typeof links + children: React.ReactNode +} + +export function KnowledgeLink(props: KnowledgeLinkProps): JSX.Element { + const { to, children } = props + return ( + + {children} + + ) +} diff --git a/protocol-designer/src/organisms/Alerts/__tests__/FormAlerts.test.tsx b/protocol-designer/src/organisms/Alerts/__tests__/FormAlerts.test.tsx index 5c7428d6996..26c5cdb02ce 100644 --- a/protocol-designer/src/organisms/Alerts/__tests__/FormAlerts.test.tsx +++ b/protocol-designer/src/organisms/Alerts/__tests__/FormAlerts.test.tsx @@ -38,7 +38,6 @@ describe('FormAlerts', () => { focusedField: null, dirtyFields: [], showFormErrorsAndWarnings: false, - page: 0, } vi.mocked(getFormLevelErrorsForUnsavedForm).mockReturnValue([]) vi.mocked(getFormWarningsForSelectedStep).mockReturnValue([]) diff --git a/protocol-designer/src/organisms/Alerts/linkConstants.ts b/protocol-designer/src/organisms/Alerts/linkConstants.ts new file mode 100644 index 00000000000..13b251961bd --- /dev/null +++ b/protocol-designer/src/organisms/Alerts/linkConstants.ts @@ -0,0 +1,16 @@ +export const KNOWLEDGEBASE_ROOT_URL = + 'https://support.opentrons.com/s/protocol-designer' + +export const links = { + airGap: `https://support.opentrons.com/en/articles/4398106-air-gap`, + multiDispense: `https://support.opentrons.com/en/articles/4170341-paths`, + protocolSteps: `https://support.opentrons.com/s/protocol-designer?tabset-92ba3=2`, + customLabware: `https://support.opentrons.com/en/articles/3136504-creating-custom-labware-definitions`, + recommendedLabware: + 'https://support.opentrons.com/s/article/What-labware-can-I-use-with-my-modules', + pipetteGen1MultiModuleCollision: + 'https://support.opentrons.com/en/articles/4168741-module-placement', + betaReleases: `https://support.opentrons.com/en/articles/3854833-opentrons-beta-software-releases`, + magneticModuleGenerations: + 'http://support.opentrons.com/en/articles/1820112-magnetic-module', +} as const diff --git a/protocol-designer/src/organisms/AnnouncementModal/announcements.tsx b/protocol-designer/src/organisms/AnnouncementModal/announcements.tsx index edbc801351a..b06dad7d704 100644 --- a/protocol-designer/src/organisms/AnnouncementModal/announcements.tsx +++ b/protocol-designer/src/organisms/AnnouncementModal/announcements.tsx @@ -1,12 +1,11 @@ +import type * as React from 'react' import { Trans, useTranslation } from 'react-i18next' import { css } from 'styled-components' import { - COLORS, DIRECTION_COLUMN, Flex, JUSTIFY_CENTER, JUSTIFY_SPACE_AROUND, - Link as LinkComponent, SPACING, StyledText, } from '@opentrons/components' @@ -20,16 +19,14 @@ import thermocyclerGen2 from '../../assets/images/modules/thermocycler_gen2.png' import liquidEnhancements from '../../assets/images/announcements/liquid-enhancements.gif' import opentronsFlex from '../../assets/images/OpentronsFlex.png' import deckConfigutation from '../../assets/images/deck_configuration.png' -import { DOC_URL } from '../KnowledgeLink' -import type { ReactNode } from 'react' import styles from './AnnouncementModal.module.css' export interface Announcement { announcementKey: string - image: ReactNode | null + image: React.ReactNode | null heading: string - message: ReactNode + message: React.ReactNode } const batchEditStyles = css` @@ -326,22 +323,7 @@ export const useAnnouncements = (): Announcement[] => { }} - i18nKey="announcements.redesign.body4" - /> - - - - ), - }} - i18nKey="announcements.redesign.body5" + i18nKey={'announcements.redesign.body4'} /> diff --git a/protocol-designer/src/organisms/ConfirmDeleteStagingAreaModal/__tests__/ConfirmDeleteStagingAreaModal.test.tsx b/protocol-designer/src/organisms/ConfirmDeleteStagingAreaModal/__tests__/ConfirmDeleteStagingAreaModal.test.tsx deleted file mode 100644 index 9f6e2991e73..00000000000 --- a/protocol-designer/src/organisms/ConfirmDeleteStagingAreaModal/__tests__/ConfirmDeleteStagingAreaModal.test.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { fireEvent, screen } from '@testing-library/react' -import { describe, it, beforeEach, vi, expect } from 'vitest' -import { renderWithProviders } from '../../../__testing-utils__' -import { i18n } from '../../../assets/localization' -import { ConfirmDeleteStagingAreaModal } from '..' -import type { ComponentProps } from 'react' - -const render = ( - props: ComponentProps -) => { - return renderWithProviders(, { - i18nInstance: i18n, - })[0] -} - -describe('ConfirmDeleteStagingAreaModal', () => { - let props: ComponentProps - - beforeEach(() => { - props = { - onClose: vi.fn(), - onConfirm: vi.fn(), - } - }) - it('renders the text and buttons work as expected', () => { - render(props) - screen.getByText('This staging area slot has labware') - screen.getByText( - 'The staging area slot that you are about to delete has labware placed on it. If you make these changes to your protocol starting deck, the labware will be deleted as well.' - ) - fireEvent.click(screen.getByText('Cancel')) - expect(props.onClose).toHaveBeenCalled() - fireEvent.click(screen.getByText('Continue')) - expect(props.onConfirm).toHaveBeenCalled() - }) -}) diff --git a/protocol-designer/src/organisms/ConfirmDeleteStagingAreaModal/index.tsx b/protocol-designer/src/organisms/ConfirmDeleteStagingAreaModal/index.tsx deleted file mode 100644 index c2d0c81f0ea..00000000000 --- a/protocol-designer/src/organisms/ConfirmDeleteStagingAreaModal/index.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useTranslation } from 'react-i18next' -import { createPortal } from 'react-dom' -import { - Flex, - JUSTIFY_END, - Modal, - PrimaryButton, - SecondaryButton, - SPACING, - StyledText, -} from '@opentrons/components' -import { getTopPortalEl } from '../../components/portals/TopPortal' -import { HandleEnter } from '../../atoms/HandleEnter' - -interface ConfirmDeleteStagingAreaModalProps { - onClose: () => void - onConfirm: () => void -} -export function ConfirmDeleteStagingAreaModal( - props: ConfirmDeleteStagingAreaModalProps -): JSX.Element { - const { onClose, onConfirm } = props - const { t, i18n } = useTranslation(['create_new_protocol', 'shared']) - - return createPortal( - - - { - onClose() - }} - > - {t('shared:cancel')} - - - {i18n.format(t('shared:continue'), 'capitalize')} - - - } - > - - {t('staging_area_will_delete_labware')} - - - , - getTopPortalEl() - ) -} diff --git a/protocol-designer/src/organisms/KnowledgeLink.tsx b/protocol-designer/src/organisms/KnowledgeLink.tsx deleted file mode 100644 index 8398539db57..00000000000 --- a/protocol-designer/src/organisms/KnowledgeLink.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Link } from '@opentrons/components' -import type { ReactNode } from 'react' - -interface KnowledgeLinkProps { - children: ReactNode -} - -export const DOC_URL = 'https://docs.opentrons.com/protocol-designer/' - -export function KnowledgeLink(props: KnowledgeLinkProps): JSX.Element { - const { children } = props - return ( - - {children} - - ) -} diff --git a/protocol-designer/src/organisms/MaterialsListModal/index.tsx b/protocol-designer/src/organisms/MaterialsListModal/index.tsx index 04fcded496d..14324594969 100644 --- a/protocol-designer/src/organisms/MaterialsListModal/index.tsx +++ b/protocol-designer/src/organisms/MaterialsListModal/index.tsx @@ -110,7 +110,10 @@ export function MaterialsListModal({ } content={ - + {t(`shared:${fixture.name}`)} @@ -142,7 +145,7 @@ export function MaterialsListModal({ content={ diff --git a/protocol-designer/src/organisms/PipetteInfoItem/__tests__/PipetteInfoItem.test.tsx b/protocol-designer/src/organisms/PipetteInfoItem/__tests__/PipetteInfoItem.test.tsx index ae60f21c4fb..53ab36986ea 100644 --- a/protocol-designer/src/organisms/PipetteInfoItem/__tests__/PipetteInfoItem.test.tsx +++ b/protocol-designer/src/organisms/PipetteInfoItem/__tests__/PipetteInfoItem.test.tsx @@ -34,7 +34,7 @@ describe('PipetteInfoItem', () => { it('renders pipette with edit and remove buttons', () => { render(props) screen.getByText('P1000 Single-Channel GEN1') - screen.getByText('Left Mount') + screen.getByText('Left Pipette') screen.getByText('mock display name') fireEvent.click(screen.getByText('Edit')) expect(props.editClick).toHaveBeenCalled() @@ -49,7 +49,7 @@ describe('PipetteInfoItem', () => { } render(props) screen.getByText('P1000 Single-Channel GEN1') - screen.getByText('Right Mount') + screen.getByText('Right Pipette') screen.getByText('mock display name') fireEvent.click(screen.getByText('Edit')) expect(props.editClick).toHaveBeenCalled() diff --git a/protocol-designer/src/organisms/PipetteInfoItem/index.tsx b/protocol-designer/src/organisms/PipetteInfoItem/index.tsx index 5b98a2c81a2..97ffe27f8cd 100644 --- a/protocol-designer/src/organisms/PipetteInfoItem/index.tsx +++ b/protocol-designer/src/organisms/PipetteInfoItem/index.tsx @@ -40,7 +40,7 @@ export function PipetteInfoItem(props: PipetteInfoItemProps): JSX.Element { {i18n.format( - t('pipette', { + t('pip', { mount: is96Channel ? t('left_right') : mount, }), 'titleCase' diff --git a/protocol-designer/src/organisms/SlotInformation/index.tsx b/protocol-designer/src/organisms/SlotInformation/index.tsx index a57de707dda..85e9a1ef636 100644 --- a/protocol-designer/src/organisms/SlotInformation/index.tsx +++ b/protocol-designer/src/organisms/SlotInformation/index.tsx @@ -64,7 +64,7 @@ export const SlotInformation: FC = ({ {liquids.join(', ')} } - description={{t('liquid')}} + description={t('liquid')} /> ) : ( diff --git a/protocol-designer/src/organisms/index.ts b/protocol-designer/src/organisms/index.ts index 72d3ae1ff92..3d6fee9b662 100644 --- a/protocol-designer/src/organisms/index.ts +++ b/protocol-designer/src/organisms/index.ts @@ -2,7 +2,6 @@ export * from './Alerts' export * from './AnnouncementModal' export * from './AssignLiquidsModal' export * from './BlockingHintModal' -export * from './ConfirmDeleteStagingAreaModal' export * from './DefineLiquidsModal' export * from './EditInstrumentsModal' export * from './EditNickNameModal' @@ -11,7 +10,6 @@ export * from './FileUploadMessagesModal/' export * from './GateModal' export * from './IncompatibleTipsModal' export * from './Kitchen' -export * from './KnowledgeLink' export * from './LabwareUploadModal' export * from './PipetteInfoItem' export * from './ProtocolMetadataNav' diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx index 023b588d7c0..e21122653ed 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectFixtures.tsx @@ -1,5 +1,6 @@ import { useTranslation } from 'react-i18next' import without from 'lodash/without' +import { THERMOCYCLER_MODULE_V2 } from '@opentrons/shared-data' import { ALIGN_CENTER, BORDERS, @@ -38,7 +39,13 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { const additionalEquipment = watch('additionalEquipment') const modules = watch('modules') const { t } = useTranslation(['create_new_protocol', 'shared']) + const numSlotsAvailable = getNumSlotsAvailable(modules, additionalEquipment) + const hasTC = + modules != null && + Object.values(modules).some( + module => module.model === THERMOCYCLER_MODULE_V2 + ) const hasTrash = additionalEquipment.some( ae => ae === 'trashBin' || ae === 'wasteChute' ) @@ -80,33 +87,25 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { ) : null} - {filteredAdditionalEquipment.map(equipment => { - const numSlotsAvailable = getNumSlotsAvailable( - modules, - additionalEquipment, - equipment - ) - - return ( - { - if (numSlotsAvailable === 0) { - makeSnackbar(t('slots_limit_reached') as string) - } else { - setValue('additionalEquipment', [ - ...additionalEquipment, - equipment, - ]) - } - }} - /> - ) - })} + {filteredAdditionalEquipment.map(equipment => ( + { + if (numSlotsAvailable === 0) { + makeSnackbar(t('slots_limit_reached') as string) + } else { + setValue('additionalEquipment', [ + ...additionalEquipment, + equipment, + ]) + } + }} + /> + ))} @@ -118,11 +117,6 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { const numStagingAreas = filteredAdditionalEquipmentWithoutGripper.filter( additionalEquipment => additionalEquipment === 'stagingArea' )?.length - const numSlotsAvailable = getNumSlotsAvailable( - modules, - additionalEquipment, - ae - ) const dropdownProps = { currentOption: { @@ -133,7 +127,7 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { filterOptions: getNumOptions( numSlotsAvailable >= MAX_SLOTS ? MAX_SLOTS - : numSlotsAvailable + numStagingAreas + : numSlotsAvailable + numStagingAreas - (hasTC ? 1 : 0) ), onClick: (value: string) => { const inputNum = parseInt(value) @@ -158,7 +152,6 @@ export function SelectFixtures(props: WizardTileProps): JSX.Element | null { return ( { setValue( diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx index 105fc2ecea5..b3bf5e225fa 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx @@ -21,6 +21,7 @@ import { getModuleType, HEATERSHAKER_MODULE_TYPE, MAGNETIC_BLOCK_TYPE, + MAGNETIC_BLOCK_V1, TEMPERATURE_MODULE_TYPE, } from '@opentrons/shared-data' import { uuid } from '../../utils' @@ -42,6 +43,9 @@ import type { ModuleModel, ModuleType } from '@opentrons/shared-data' import type { FormModule, FormModules } from '../../step-forms' import type { WizardTileProps } from './types' +const MAX_MAGNETIC_BLOCKS = 4 +const MAGNETIC_BLOCKS_ADJUSTMENT = 3 + export function SelectModules(props: WizardTileProps): JSX.Element | null { const { goBack, proceed, watch, setValue } = props const { t } = useTranslation(['create_new_protocol', 'shared']) @@ -55,6 +59,15 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { robotType === FLEX_ROBOT_TYPE ? FLEX_SUPPORTED_MODULE_MODELS : OT2_SUPPORTED_MODULE_MODELS + + const numSlotsAvailable = getNumSlotsAvailable(modules, additionalEquipment) + const hasNoAvailableSlots = numSlotsAvailable === 0 + const numMagneticBlocks = + modules != null + ? Object.values(modules).filter( + module => module.model === MAGNETIC_BLOCK_V1 + )?.length + : 0 const filteredSupportedModules = supportedModules.filter( moduleModel => !( @@ -72,10 +85,7 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { MAGNETIC_BLOCK_TYPE, ] - const handleAddModule = ( - moduleModel: ModuleModel, - hasNoAvailableSlots: boolean - ): void => { + const handleAddModule = (moduleModel: ModuleModel): void => { if (hasNoAvailableSlots) { makeSnackbar(t('slots_limit_reached') as string) } else { @@ -110,40 +120,37 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { module: FormModule, newQuantity: number ): void => { - if (!modules) return - - const modulesOfType = Object.entries(modules).filter( - ([, mod]) => mod.type === module.type - ) - const otherModules = Object.entries(modules).filter( - ([, mod]) => mod.type !== module.type - ) - - if (newQuantity > modulesOfType.length) { - const additionalModules: FormModules = {} - for (let i = 0; i < newQuantity - modulesOfType.length; i++) { + const moamModules = + modules != null + ? Object.entries(modules).filter( + ([key, mod]) => mod.type === module.type + ) + : [] + if (newQuantity > moamModules.length) { + const newModules = { ...modules } + for (let i = 0; i < newQuantity - moamModules.length; i++) { // @ts-expect-error: TS can't determine modules's type correctly - additionalModules[uuid()] = { + newModules[uuid()] = { model: module.model, type: module.type, slot: null, } } - - const newModules = Object.fromEntries([ - ...otherModules, - ...modulesOfType, - ...Object.entries(additionalModules), - ]) setValue('modules', newModules) - } else if (newQuantity < modulesOfType.length) { - const modulesToKeep = modulesOfType.slice(0, newQuantity) - const updatedModules = Object.fromEntries([ - ...otherModules, - ...modulesToKeep, - ]) + } else if (newQuantity < moamModules.length) { + const modulesToRemove = moamModules.length - newQuantity + const remainingModules: FormModules = {} + + Object.entries(modules).forEach(([key, mod]) => { + const shouldRemove = moamModules + .slice(-modulesToRemove) + .some(([removeKey]) => removeKey === key) + if (!shouldRemove) { + remainingModules[parseInt(key)] = mod + } + }) - setValue('modules', updatedModules) + setValue('modules', remainingModules) } } @@ -179,25 +186,26 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { ? module : module !== ABSORBANCE_READER_V1 ) - .map(moduleModel => { - const numSlotsAvailable = getNumSlotsAvailable( - modules, - additionalEquipment, - moduleModel - ) - return ( - { - handleAddModule(moduleModel, numSlotsAvailable === 0) - }} - /> - ) - })} + .map(moduleModel => ( + { + handleAddModule(moduleModel) + }} + /> + ))} {modules != null && Object.keys(modules).length > 0 ? ( { - const numSlotsAvailable = getNumSlotsAvailable( - modules, - additionalEquipment, - module.model - ) const dropdownProps = { currentOption: { name: `${module.count}`, @@ -252,13 +255,16 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { }, dropdownType: 'neutral' as DropdownBorder, filterOptions: getNumOptions( - numSlotsAvailable + module.count + module.model === 'magneticBlockV1' + ? numSlotsAvailable + + MAGNETIC_BLOCKS_ADJUSTMENT + + module.count + : numSlotsAvailable + module.count ), } return ( {t('pipette_type')} - + {PIPETTE_TYPES[robotType].map(type => { return type.value === '96' && (pipettesByMount.left.pipetteName != null || @@ -225,7 +225,6 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { {t('pipette_gen')} @@ -257,7 +256,7 @@ export function SelectPipettes(props: WizardTileProps): JSX.Element | null { {t('pipette_vol')} - + {PIPETTE_VOLUMES[robotType]?.map(volume => { if ( robotType === FLEX_ROBOT_TYPE && diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts index 02039fb312e..6ebef7c330d 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/utils.test.ts @@ -3,10 +3,7 @@ import { FLEX_ROBOT_TYPE, HEATERSHAKER_MODULE_TYPE, HEATERSHAKER_MODULE_V1, - MAGNETIC_BLOCK_TYPE, - MAGNETIC_BLOCK_V1, TEMPERATURE_MODULE_TYPE, - TEMPERATURE_MODULE_V1, TEMPERATURE_MODULE_V2, THERMOCYCLER_MODULE_TYPE, THERMOCYCLER_MODULE_V2, @@ -32,23 +29,11 @@ let MOCK_FORM_STATE = { } as WizardFormState describe('getNumSlotsAvailable', () => { - it('should return 0 for a gripper', () => { - const result = getNumSlotsAvailable(null, [], 'gripper') - expect(result).toBe(0) - }) - it('should return 1 for a non MoaM module', () => { - const result = getNumSlotsAvailable(null, [], TEMPERATURE_MODULE_V1) - expect(result).toBe(1) - }) - it('should return 2 for a thermocycler', () => { - const result = getNumSlotsAvailable(null, [], THERMOCYCLER_MODULE_V2) - expect(result).toBe(2) - }) - it('should return 8 when there are no modules or additional equipment for a heater-shaker', () => { - const result = getNumSlotsAvailable(null, [], HEATERSHAKER_MODULE_V1) + it('should return 8 when there are no modules or additional equipment', () => { + const result = getNumSlotsAvailable(null, []) expect(result).toBe(8) }) - it('should return 0 when there is a TC and 7 modules for a temperature module v2', () => { + it('should return 0 when there is a TC and 7 modules', () => { const mockModules = { 0: { model: HEATERSHAKER_MODULE_V1, @@ -87,10 +72,10 @@ describe('getNumSlotsAvailable', () => { slot: 'C3', }, } as any - const result = getNumSlotsAvailable(mockModules, [], TEMPERATURE_MODULE_V2) + const result = getNumSlotsAvailable(mockModules, []) expect(result).toBe(0) }) - it('should return 1 when there are 9 additional equipment and 1 is a waste chute on the staging area and one is a gripper for a heater-shaker', () => { + it('should return 1 when there are 9 additional equipment and 1 is a waste chute on the staging area and one is a gripper', () => { const mockAdditionalEquipment: AdditionalEquipment[] = [ 'trashBin', 'stagingArea', @@ -102,184 +87,20 @@ describe('getNumSlotsAvailable', () => { 'gripper', 'trashBin', ] - const result = getNumSlotsAvailable( - null, - mockAdditionalEquipment, - HEATERSHAKER_MODULE_V1 - ) - expect(result).toBe(1) - }) - it('should return 1 when there is a full deck but one staging area for waste chute', () => { - const mockModules = { - 0: { - model: HEATERSHAKER_MODULE_V1, - type: HEATERSHAKER_MODULE_TYPE, - slot: 'D1', - }, - 1: { - model: TEMPERATURE_MODULE_V2, - type: TEMPERATURE_MODULE_TYPE, - slot: 'D3', - }, - 2: { - model: TEMPERATURE_MODULE_V2, - type: TEMPERATURE_MODULE_TYPE, - slot: 'C1', - }, - 3: { - model: TEMPERATURE_MODULE_V2, - type: TEMPERATURE_MODULE_TYPE, - slot: 'B3', - }, - 4: { - model: THERMOCYCLER_MODULE_V2, - type: THERMOCYCLER_MODULE_TYPE, - slot: 'B1', - }, - } as any - const mockAdditionalEquipment: AdditionalEquipment[] = [ - 'trashBin', - 'stagingArea', - ] - const result = getNumSlotsAvailable( - mockModules, - mockAdditionalEquipment, - 'wasteChute' - ) - expect(result).toBe(1) - }) - it('should return 1 when there are 7 modules (with one magnetic block) and one trash for staging area', () => { - const mockModules = { - 0: { - model: HEATERSHAKER_MODULE_V1, - type: HEATERSHAKER_MODULE_TYPE, - slot: 'D1', - }, - 1: { - model: TEMPERATURE_MODULE_V2, - type: TEMPERATURE_MODULE_TYPE, - slot: 'D3', - }, - 2: { - model: TEMPERATURE_MODULE_V2, - type: TEMPERATURE_MODULE_TYPE, - slot: 'C1', - }, - 3: { - model: TEMPERATURE_MODULE_V2, - type: TEMPERATURE_MODULE_TYPE, - slot: 'B3', - }, - 4: { - model: THERMOCYCLER_MODULE_V2, - type: THERMOCYCLER_MODULE_TYPE, - slot: 'B1', - }, - 5: { - model: MAGNETIC_BLOCK_V1, - type: MAGNETIC_BLOCK_TYPE, - slot: 'C2', - }, - } as any - const mockAdditionalEquipment: AdditionalEquipment[] = ['trashBin'] - const result = getNumSlotsAvailable( - mockModules, - mockAdditionalEquipment, - 'stagingArea' - ) + const result = getNumSlotsAvailable(null, mockAdditionalEquipment) expect(result).toBe(1) }) - it('should return 1 when there are 8 modules with 2 magnetic blocks and one trash for staging area', () => { + it('should return 8 even when there is a magnetic block', () => { const mockModules = { 0: { - model: HEATERSHAKER_MODULE_V1, - type: HEATERSHAKER_MODULE_TYPE, - slot: 'D1', - }, - 1: { - model: TEMPERATURE_MODULE_V2, - type: TEMPERATURE_MODULE_TYPE, - slot: 'D3', - }, - 2: { - model: TEMPERATURE_MODULE_V2, - type: TEMPERATURE_MODULE_TYPE, - slot: 'C1', - }, - 3: { - model: TEMPERATURE_MODULE_V2, - type: TEMPERATURE_MODULE_TYPE, - slot: 'B3', - }, - 4: { - model: THERMOCYCLER_MODULE_V2, - type: THERMOCYCLER_MODULE_TYPE, - slot: 'B1', - }, - 5: { - model: MAGNETIC_BLOCK_V1, - type: MAGNETIC_BLOCK_TYPE, - slot: 'C2', - }, - 6: { - model: MAGNETIC_BLOCK_V1, - type: MAGNETIC_BLOCK_TYPE, - slot: 'D2', + model: 'magneticBlockV1', + type: 'magneticBlockType', + slot: 'B2', }, } as any - const mockAdditionalEquipment: AdditionalEquipment[] = ['trashBin'] - const result = getNumSlotsAvailable( - mockModules, - mockAdditionalEquipment, - 'stagingArea' - ) - expect(result).toBe(1) - }) - it('should return 8 when there are 4 staging area for magnetic block', () => { - const mockAdditionalEquipment: AdditionalEquipment[] = [ - 'stagingArea', - 'stagingArea', - 'stagingArea', - 'stagingArea', - ] - const result = getNumSlotsAvailable( - [], - mockAdditionalEquipment, - MAGNETIC_BLOCK_V1 - ) + const result = getNumSlotsAvailable(mockModules, []) expect(result).toBe(8) }) - it('should return 4 when there are 4 modules, 4 staging area for magnetic block', () => { - const mockModules = { - 0: { - model: HEATERSHAKER_MODULE_V1, - type: HEATERSHAKER_MODULE_TYPE, - slot: 'D1', - }, - 1: { - model: TEMPERATURE_MODULE_V2, - type: TEMPERATURE_MODULE_TYPE, - slot: 'D3', - }, - 2: { - model: THERMOCYCLER_MODULE_V2, - type: THERMOCYCLER_MODULE_TYPE, - slot: 'B1', - }, - } as any - const mockAdditionalEquipment: AdditionalEquipment[] = [ - 'stagingArea', - 'stagingArea', - 'stagingArea', - 'stagingArea', - ] - const result = getNumSlotsAvailable( - mockModules, - mockAdditionalEquipment, - MAGNETIC_BLOCK_V1 - ) - expect(result).toBe(4) - }) }) describe('getTrashSlot', () => { diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx index 01feee56b61..6849fc68a78 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/index.tsx @@ -284,10 +284,7 @@ export function CreateNewProtocolWizard(): JSX.Element | null { if (stagingAreas.length > 0) { stagingAreas.forEach((_, index) => { return dispatch( - createDeckFixture( - 'stagingArea', - STAGING_AREA_CUTOUTS.reverse()[index] - ) + createDeckFixture('stagingArea', STAGING_AREA_CUTOUTS[index]) ) }) } diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx index 90e06a8368e..e0a84c1340c 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/utils.tsx @@ -1,38 +1,26 @@ import { - ABSORBANCE_READER_V1, - getLabwareDefURI, - getLabwareDisplayName, - getPipetteSpecsV2, - HEATERSHAKER_MODULE_V1, MAGNETIC_BLOCK_TYPE, - MAGNETIC_BLOCK_V1, - MAGNETIC_MODULE_V1, - MAGNETIC_MODULE_V2, STAGING_AREA_CUTOUTS, - TEMPERATURE_MODULE_V1, - TEMPERATURE_MODULE_V2, THERMOCYCLER_MODULE_TYPE, - THERMOCYCLER_MODULE_V1, - THERMOCYCLER_MODULE_V2, + getLabwareDefURI, + getLabwareDisplayName, + getPipetteSpecsV2, WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' import wasteChuteImage from '../../assets/images/waste_chute.png' import trashBinImage from '../../assets/images/flex_trash_bin.png' import stagingAreaImage from '../../assets/images/staging_area.png' - import type { CutoutId, LabwareDefByDefURI, LabwareDefinition2, - ModuleModel, PipetteName, } from '@opentrons/shared-data' import type { DropdownOption } from '@opentrons/components' import type { AdditionalEquipment, WizardFormState } from './types' -const TOTAL_OUTER_SLOTS = 8 +const TOTAL_MODULE_SLOTS = 8 const MIDDLE_SLOT_NUM = 4 -const MAX_MAGNETIC_BLOCK_SLOTS = 12 export const getNumOptions = (length: number): DropdownOption[] => { return Array.from({ length }, (_, i) => ({ @@ -43,12 +31,9 @@ export const getNumOptions = (length: number): DropdownOption[] => { export const getNumSlotsAvailable = ( modules: WizardFormState['modules'], - additionalEquipment: WizardFormState['additionalEquipment'], - type: ModuleModel | AdditionalEquipment + additionalEquipment: WizardFormState['additionalEquipment'] ): number => { - const additionalEquipmentLength = additionalEquipment.filter( - ae => ae !== 'gripper' - ).length + const additionalEquipmentLength = additionalEquipment.length const hasTC = Object.values(modules || {}).some( module => module.type === THERMOCYCLER_MODULE_TYPE ) @@ -60,69 +45,32 @@ export const getNumSlotsAvailable = ( module => module.type === MAGNETIC_BLOCK_TYPE ) let filteredModuleLength = modules != null ? Object.keys(modules).length : 0 + if (hasTC) { + filteredModuleLength = filteredModuleLength + 1 + } if (magneticBlocks.length > 0) { // once blocks exceed 4, then we dont' want to subtract the amount available // because block can go into the center slots where all other modules/trashes can not const numBlocks = - magneticBlocks.length >= 4 ? MIDDLE_SLOT_NUM : magneticBlocks.length - filteredModuleLength = - filteredModuleLength - (type !== 'magneticBlockV1' ? numBlocks : 0) - } - if (hasTC) { - filteredModuleLength = filteredModuleLength + 1 + magneticBlocks.length > 4 ? MIDDLE_SLOT_NUM : magneticBlocks.length + filteredModuleLength = filteredModuleLength - numBlocks } + + const hasGripper = additionalEquipment.some(equipment => + equipment.includes('gripper') + ) + let filteredAdditionalEquipmentLength = additionalEquipmentLength - if (numStagingAreas >= 1 && hasWasteChute && type !== 'stagingArea') { + if (hasGripper) { filteredAdditionalEquipmentLength = filteredAdditionalEquipmentLength - 1 } - switch (type) { - case 'gripper': { - return 0 - } - // TODO: wire up absorbance reader - case ABSORBANCE_READER_V1: { - return 1 - } - // these modules don't support MoaM - case THERMOCYCLER_MODULE_V1: - case TEMPERATURE_MODULE_V1: - case MAGNETIC_MODULE_V1: - case MAGNETIC_MODULE_V2: { - return 1 - } - - case THERMOCYCLER_MODULE_V2: { - if (filteredModuleLength + filteredAdditionalEquipmentLength > 7) { - return 0 - } else { - return 2 - } - } - case 'trashBin': - case 'stagingArea': - case HEATERSHAKER_MODULE_V1: - case TEMPERATURE_MODULE_V2: { - return ( - TOTAL_OUTER_SLOTS - - (filteredModuleLength + filteredAdditionalEquipmentLength) - ) - } - case 'wasteChute': { - const adjustmentForStagingArea = numStagingAreas >= 1 ? 1 : 0 - return ( - TOTAL_OUTER_SLOTS - - (filteredModuleLength + - filteredAdditionalEquipmentLength - - adjustmentForStagingArea) - ) - } - case MAGNETIC_BLOCK_V1: { - return ( - MAX_MAGNETIC_BLOCK_SLOTS - - (filteredModuleLength + filteredAdditionalEquipmentLength) - ) - } + if (numStagingAreas === MIDDLE_SLOT_NUM && hasWasteChute) { + filteredAdditionalEquipmentLength = filteredAdditionalEquipmentLength - 1 } + return ( + TOTAL_MODULE_SLOTS - + (filteredModuleLength + filteredAdditionalEquipmentLength) + ) } interface EquipmentProps { diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx index 2e45768cf4d..b06cbfd535a 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupContainer.tsx @@ -67,7 +67,6 @@ const OT2_STANDARD_DECK_VIEW_LAYER_BLOCK_LIST: string[] = [ 'fixedTrash', ] export const lightFill = COLORS.grey35 -export const darkFill = COLORS.grey60 export function DeckSetupContainer(props: DeckSetupTabType): JSX.Element { const { tab } = props @@ -180,7 +179,6 @@ export function DeckSetupContainer(props: DeckSetupTabType): JSX.Element { height={zoomIn.slot != null ? '75vh' : '70vh'} flexDirection={DIRECTION_COLUMN} padding={SPACING.spacing40} - maxHeight="39.375rem" // this is to block deck view from enlarging > @@ -231,7 +228,6 @@ export function DeckSetupContainer(props: DeckSetupTabType): JSX.Element { key={fixture.id} cutoutId={fixture.location as StagingAreaLocation} deckDefinition={deckDef} - slotClipColor={darkFill} fixtureBaseColor={lightFill} /> ) @@ -288,7 +284,6 @@ export function DeckSetupContainer(props: DeckSetupTabType): JSX.Element { fixture.location as typeof WASTE_CHUTE_CUTOUT } deckDefinition={deckDef} - slotClipColor={darkFill} fixtureBaseColor={lightFill} /> ) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx index ed6150223c0..0b23fa3315e 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupDetails.tsx @@ -1,5 +1,5 @@ +import * as React from 'react' import values from 'lodash/values' -import { Fragment, useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { Module } from '@opentrons/components' @@ -28,9 +28,7 @@ import { DeckItemHover } from './DeckItemHover' import { SlotOverflowMenu } from './SlotOverflowMenu' import { HoveredItems } from './HoveredItems' import { SelectedHoveredItems } from './SelectedHoveredItems' -import { getAdjacentLabware } from './utils' -import type { ComponentProps, Dispatch, SetStateAction } from 'react' import type { ModuleTemporalProperties } from '@opentrons/step-generation' import type { AddressableArea, @@ -57,7 +55,7 @@ interface DeckSetupDetailsProps extends DeckSetupTabType { hoveredFixture: Fixture | null hoveredLabware: string | null hoveredModule: ModuleModel | null - setHover: Dispatch> + setHover: React.Dispatch> showGen1MultichannelCollisionWarnings: boolean stagingAreaCutoutIds: CutoutId[] selectedZoomInSlot?: DeckSlotId @@ -85,7 +83,9 @@ export function DeckSetupDetails(props: DeckSetupDetailsProps): JSX.Element { ) const selectedSlotInfo = useSelector(selectors.getZoomedInSlotInfo) const { selectedSlot } = selectedSlotInfo - const [menuListId, setShowMenuListForId] = useState(null) + const [menuListId, setShowMenuListForId] = React.useState( + null + ) const dispatch = useDispatch() const { @@ -100,7 +100,7 @@ export function DeckSetupDetails(props: DeckSetupDetailsProps): JSX.Element { deckDef, }) // initiate the slot's info - useEffect(() => { + React.useEffect(() => { dispatch( editSlotInfo({ createdNestedLabwareForSlot, @@ -132,15 +132,6 @@ export function DeckSetupDetails(props: DeckSetupDetailsProps): JSX.Element { ? getSlotsWithCollisions(deckDef, allModules) : [] - const adjacentLabware = - preSelectedFixture != null && selectedSlot.cutout != null - ? getAdjacentLabware( - preSelectedFixture, - selectedSlot.cutout, - activeDeckSetup.labware - ) - : null - return ( <> {/* all modules */} @@ -155,7 +146,7 @@ export function DeckSetupDetails(props: DeckSetupDetailsProps): JSX.Element { const moduleDef = getModuleDef2(moduleOnDeck.model) const getModuleInnerProps = ( moduleState: ModuleTemporalProperties['moduleState'] - ): ComponentProps['innerProps'] => { + ): React.ComponentProps['innerProps'] => { if (moduleState.type === THERMOCYCLER_MODULE_TYPE) { let lidMotorState = 'unknown' if (tab === 'startingDeck' || moduleState.lidOpen) { @@ -195,7 +186,7 @@ export function DeckSetupDetails(props: DeckSetupDetailsProps): JSX.Element { zDimension: labwareLoadedOnModule?.def.dimensions.zDimension ?? 0, } return moduleOnDeck.slot !== selectedSlot.slot ? ( - + ) : null} - + ) : null })} @@ -285,7 +276,7 @@ export function DeckSetupDetails(props: DeckSetupDetailsProps): JSX.Element { }) .map(addressableArea => { return ( - + - + ) })} {/* all labware on deck NOT those in modules */} @@ -308,10 +299,10 @@ export function DeckSetupDetails(props: DeckSetupDetailsProps): JSX.Element { if ( labware.slot === 'offDeck' || allModules.some(m => m.id === labware.slot) || - allLabware.some(lab => lab.id === labware.slot) || - labware.id === adjacentLabware?.id + allLabware.some(lab => lab.id === labware.slot) ) return null + const slotPosition = getPositionFromSlotId(labware.slot, deckDef) const slotBoundingBox = getAddressableAreaFromSlotId( labware.slot, @@ -322,7 +313,7 @@ export function DeckSetupDetails(props: DeckSetupDetailsProps): JSX.Element { return null } return labware.slot !== selectedSlot.slot ? ( - + - + ) : null })} @@ -385,7 +376,7 @@ export function DeckSetupDetails(props: DeckSetupDetailsProps): JSX.Element { ? slotForOnTheDeck : allModules.find(module => module.id === slotForOnTheDeck)?.slot return ( - + - + ) })} diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx index e73ab455dc7..6c000ad0428 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx @@ -20,7 +20,6 @@ import { MAGNETIC_MODULE_TYPE, MAGNETIC_MODULE_V1, MAGNETIC_MODULE_V2, - MODULE_MODELS, OT2_ROBOT_TYPE, } from '@opentrons/shared-data' @@ -47,7 +46,6 @@ import { selectors } from '../../../labware-ingred/selectors' import { useKitchen } from '../../../organisms/Kitchen/hooks' import { getDismissedHints } from '../../../tutorial/selectors' import { createContainerAboveModule } from '../../../step-forms/actions/thunks' -import { ConfirmDeleteStagingAreaModal } from '../../../organisms' import { FIXTURES, MOAM_MODELS } from './constants' import { getSlotInformation } from '../utils' import { getModuleModelsBySlot, getDeckErrors } from './utils' @@ -73,9 +71,6 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { const { makeSnackbar } = useKitchen() const selectedSlotInfo = useSelector(selectors.getZoomedInSlotInfo) const robotType = useSelector(getRobotType) - const [showDeleteLabwareModal, setShowDeleteLabwareModal] = useState< - ModuleModel | 'clear' | null - >(null) const isDismissedModuleHint = useSelector(getDismissedHints).includes( 'change_magnet_module_model' ) @@ -159,7 +154,6 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { createdModuleForSlot, createdLabwareForSlot, createFixtureForSlots, - matchingLabwareFor4thColumn, } = getSlotInformation({ deckSetup, slot }) let fixtures: Fixture[] = [] @@ -224,10 +218,6 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { if (createdNestedLabwareForSlot != null) { dispatch(deleteContainer({ labwareId: createdNestedLabwareForSlot.id })) } - // clear labware on staging area 4th column slot - if (matchingLabwareFor4thColumn != null) { - dispatch(deleteContainer({ labwareId: matchingLabwareFor4thColumn.id })) - } } handleResetToolbox() setSelectedHardware(null) @@ -288,26 +278,6 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { } return ( <> - {showDeleteLabwareModal != null ? ( - { - setShowDeleteLabwareModal(null) - }} - onConfirm={() => { - if (showDeleteLabwareModal === 'clear') { - handleClear() - handleResetToolbox() - } else if (MODULE_MODELS.includes(showDeleteLabwareModal)) { - setSelectedHardware(showDeleteLabwareModal) - dispatch(selectFixture({ fixture: null })) - dispatch(selectModule({ moduleModel: showDeleteLabwareModal })) - dispatch(selectLabware({ labwareDefUri: null })) - dispatch(selectNestedLabware({ nestedLabwareDefUri: null })) - } - setShowDeleteLabwareModal(null) - }} - /> - ) : null} {changeModuleWarning} } onCloseClick={() => { - if (matchingLabwareFor4thColumn != null) { - setShowDeleteLabwareModal('clear') - } else { - handleClear() - handleResetToolbox() - } + handleClear() + handleResetToolbox() }} onConfirmClick={() => { handleConfirm() @@ -441,12 +407,6 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { !isDismissedModuleHint ) { displayModuleWarning(true) - } else if ( - selectedFixture === 'stagingArea' || - (selectedFixture === 'wasteChuteAndStagingArea' && - matchingLabwareFor4thColumn != null) - ) { - setShowDeleteLabwareModal(model) } else { setSelectedHardware(model) dispatch(selectFixture({ fixture: null })) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/FixtureRender.tsx b/protocol-designer/src/pages/Designer/DeckSetup/FixtureRender.tsx index cf4d1129486..92803de701f 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/FixtureRender.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/FixtureRender.tsx @@ -1,5 +1,4 @@ import { Fragment } from 'react' -import { useSelector } from 'react-redux' import { COLORS, FlexTrash, @@ -8,11 +7,7 @@ import { WasteChuteFixture, WasteChuteStagingAreaFixture, } from '@opentrons/components' -import { getPositionFromSlotId } from '@opentrons/shared-data' -import { getInitialDeckSetup } from '../../../step-forms/selectors' -import { LabwareOnDeck as LabwareOnDeckComponent } from '../../../components/DeckSetup/LabwareOnDeck' -import { lightFill, darkFill } from './DeckSetupContainer' -import { getAdjacentLabware } from './utils' +import { lightFill } from './DeckSetupContainer' import type { TrashCutoutId, StagingAreaLocation } from '@opentrons/components' import type { CutoutId, @@ -30,34 +25,16 @@ interface FixtureRenderProps { } export const FixtureRender = (props: FixtureRenderProps): JSX.Element => { const { fixture, cutout, deckDef, robotType } = props - const deckSetup = useSelector(getInitialDeckSetup) - const { labware } = deckSetup - const adjacentLabware = getAdjacentLabware(fixture, cutout, labware) - - const renderLabwareOnDeck = (): JSX.Element | null => { - if (!adjacentLabware) return null - const slotPosition = getPositionFromSlotId(adjacentLabware.slot, deckDef) - return ( - - ) - } switch (fixture) { case 'stagingArea': { return ( - - - {renderLabwareOnDeck()} - + ) } case 'trashBin': { @@ -90,14 +67,12 @@ export const FixtureRender = (props: FixtureRenderProps): JSX.Element => { } case 'wasteChuteAndStagingArea': { return ( - - - {renderLabwareOnDeck()} - + ) } } diff --git a/protocol-designer/src/pages/Designer/DeckSetup/MagnetModuleChangeContent.tsx b/protocol-designer/src/pages/Designer/DeckSetup/MagnetModuleChangeContent.tsx index 3bf4e7de3a4..0a5e9c18471 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/MagnetModuleChangeContent.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/MagnetModuleChangeContent.tsx @@ -2,10 +2,10 @@ import { useTranslation } from 'react-i18next' import { DIRECTION_COLUMN, Flex, + Link, SPACING, StyledText, } from '@opentrons/components' -import { KnowledgeLink } from '../../../organisms' export function MagnetModuleChangeContent(): JSX.Element { const { t } = useTranslation('starting_deck_state') @@ -26,7 +26,13 @@ export function MagnetModuleChangeContent(): JSX.Element { - {t('read_more_gen1_gen2')} {t('here')} + {t('read_more_gen1_gen2')}{' '} + + {t('here')} + diff --git a/protocol-designer/src/pages/Designer/DeckSetup/SelectedHoveredItems.tsx b/protocol-designer/src/pages/Designer/DeckSetup/SelectedHoveredItems.tsx index 2f3d0b4323d..258f5fe07d6 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/SelectedHoveredItems.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/SelectedHoveredItems.tsx @@ -1,4 +1,5 @@ import { useSelector } from 'react-redux' +import { FixtureRender } from './FixtureRender' import { LabwareRender, Module } from '@opentrons/components' import { getModuleDef2, @@ -7,11 +8,8 @@ import { import { selectors } from '../../../labware-ingred/selectors' import { getOnlyLatestDefs } from '../../../labware-defs' import { getCustomLabwareDefsByURI } from '../../../labware-defs/selectors' -import { getInitialDeckSetup } from '../../../step-forms/selectors' -import { LabwareOnDeck } from '../../../components/DeckSetup/LabwareOnDeck' import { ModuleLabel } from './ModuleLabel' import { LabwareLabel } from '../LabwareLabel' -import { FixtureRender } from './FixtureRender' import type { CoordinateTuple, DeckDefinition, @@ -50,44 +48,20 @@ export const SelectedHoveredItems = ( } = selectedSlotInfo const customLabwareDefs = useSelector(getCustomLabwareDefsByURI) const defs = getOnlyLatestDefs() - const deckSetup = useSelector(getInitialDeckSetup) - const { labware, modules } = deckSetup - const matchingSelectedLabwareOnDeck = Object.values(labware).find(labware => { - const moduleUnderLabware = Object.values(modules).find( - mod => mod.id === labware.slot - ) - const matchingSlot = - moduleUnderLabware != null ? moduleUnderLabware.slot : labware.slot - return ( - matchingSlot === selectedSlot.slot && - labware.labwareDefURI === selectedLabwareDefUri - ) - }) - const matchingSelectedNestedLabwareOnDeck = Object.values(labware).find( - lw => { - const adapterUnderLabware = Object.values(labware).find( - lab => lab.id === lw.slot - ) - if (adapterUnderLabware == null) { - return - } - const moduleUnderLabware = Object.values(modules).find( - mod => mod.id === adapterUnderLabware.slot - ) - const matchingSlot = - moduleUnderLabware != null - ? moduleUnderLabware.slot - : adapterUnderLabware.slot - return ( - lw.labwareDefURI === selectedNestedLabwareDefUri && - matchingSlot === selectedSlot.slot - ) - } - ) + const hoveredLabwareDef = hoveredLabware != null ? defs[hoveredLabware] ?? customLabwareDefs[hoveredLabware] ?? null : null + const selectedLabwareDef = + selectedLabwareDefUri != null + ? defs[selectedLabwareDefUri] ?? customLabwareDefs[selectedLabwareDefUri] + : null + const selectedNestedLabwareDef = + selectedNestedLabwareDefUri != null + ? defs[selectedNestedLabwareDefUri] ?? + customLabwareDefs[selectedNestedLabwareDefUri] + : null const orientation = slotPosition != null @@ -109,9 +83,9 @@ export const SelectedHoveredItems = ( } labwareInfos.push(selectedLabwareLabel) } - if (matchingSelectedNestedLabwareOnDeck != null && hoveredLabware == null) { + if (selectedNestedLabwareDef != null && hoveredLabware == null) { const selectedNestedLabwareLabel = { - text: matchingSelectedNestedLabwareOnDeck.def.metadata.displayName, + text: selectedNestedLabwareDef.metadata.displayName, isSelected: true, isLast: hoveredLabware == null, } @@ -158,23 +132,19 @@ export const SelectedHoveredItems = ( orientation={orientation} > <> - {matchingSelectedLabwareOnDeck != null && + {selectedLabwareDef != null && selectedModuleModel != null && hoveredLabware == null ? ( - + + + ) : null} - {matchingSelectedNestedLabwareOnDeck != null && + {selectedNestedLabwareDef != null && selectedModuleModel != null && hoveredLabware == null ? ( - + + + ) : null} {hoveredLabwareDef != null && selectedModuleModel != null ? ( @@ -195,47 +165,41 @@ export const SelectedHoveredItems = ( ) : null} ) : null} - {matchingSelectedLabwareOnDeck != null && + {selectedLabwareDef != null && slotPosition != null && selectedModuleModel == null && hoveredLabware == null ? ( <> - + + + {selectedNestedLabwareDefUri == null ? ( ) : null} ) : null} - {matchingSelectedNestedLabwareOnDeck != null && + {selectedNestedLabwareDef != null && slotPosition != null && selectedModuleModel == null && hoveredLabware == null ? ( <> - - {matchingSelectedLabwareOnDeck != null ? ( + + + + {selectedLabwareDef != null ? ( ) => void + setShowMenuList: (value: React.SetStateAction) => void addEquipment: (slotId: string) => void menuListSlotPosition?: CoordinateTuple } @@ -81,14 +71,14 @@ export function SlotOverflowMenu( const { t } = useTranslation('starting_deck_state') const navigate = useNavigate() const dispatch = useDispatch>() - const [showDeleteLabwareModal, setShowDeleteLabwareModal] = useState( + const [showNickNameModal, setShowNickNameModal] = React.useState( false ) - const [showNickNameModal, setShowNickNameModal] = useState(false) const overflowWrapperRef = useOnClickOutside({ onClickOutside: () => { - if (showNickNameModal || showDeleteLabwareModal) return - setShowMenuList(false) + if (!showNickNameModal) { + setShowMenuList(false) + } }, }) const deckSetup = useSelector(getDeckSetupForActiveItem) @@ -121,20 +111,6 @@ export function SlotOverflowMenu( const fixturesOnSlot = Object.values(additionalEquipmentOnDeck).filter( ae => ae.location?.split('cutout')[1] === location ) - const stagingAreaCutout = fixturesOnSlot.find( - fixture => fixture.name === 'stagingArea' - )?.location - - let matchingLabware: LabwareOnDeck | null = null - if (stagingAreaCutout != null) { - const stagingAreaAddressableAreaName = getStagingAreaAddressableAreas([ - stagingAreaCutout, - ] as CutoutId[]) - matchingLabware = - Object.values(deckSetupLabware).find( - lw => lw.slot === stagingAreaAddressableAreaName[0] - ) ?? null - } const hasNoItems = moduleOnSlot == null && labwareOnSlot == null && fixturesOnSlot.length === 0 @@ -156,12 +132,7 @@ export function SlotOverflowMenu( if (nestedLabwareOnSlot != null) { dispatch(deleteContainer({ labwareId: nestedLabwareOnSlot.id })) } - // clear labware on staging area 4th column slot - if (matchingLabware != null) { - dispatch(deleteContainer({ labwareId: matchingLabware.id })) - } } - const showDuplicateBtn = (labwareOnSlot != null && !isLabwareAnAdapter && @@ -208,19 +179,6 @@ export function SlotOverflowMenu( }} /> ) : null} - {showDeleteLabwareModal ? ( - { - setShowDeleteLabwareModal(false) - setShowMenuList(false) - }} - onConfirm={() => { - handleClear() - setShowDeleteLabwareModal(false) - setShowMenuList(false) - }} - /> - ) : null} { + onClick={(e: React.MouseEvent) => { e.preventDefault() e.stopPropagation() }} @@ -248,7 +206,7 @@ export function SlotOverflowMenu( {showEditAndLiquidsBtns ? ( <> { + onClick={(e: React.MouseEvent) => { setShowNickNameModal(true) e.preventDefault() e.stopPropagation() @@ -296,15 +254,9 @@ export function SlotOverflowMenu( ) : null} { - if (matchingLabware != null) { - setShowDeleteLabwareModal(true) - e.preventDefault() - e.stopPropagation() - } else { - handleClear() - setShowMenuList(false) - } + onClick={() => { + handleClear() + setShowMenuList(false) }} > diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SelectedHoveredItems.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SelectedHoveredItems.test.tsx index 8915ebce03c..7b57073468f 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SelectedHoveredItems.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/SelectedHoveredItems.test.tsx @@ -6,28 +6,23 @@ import { renderWithProviders } from '../../../../__testing-utils__' import { FLEX_ROBOT_TYPE, HEATERSHAKER_MODULE_V1, - fixture24Tuberack, getDeckDefFromRobotType, } from '@opentrons/shared-data' -import { Module } from '@opentrons/components' +import { LabwareRender, Module } from '@opentrons/components' import { selectors } from '../../../../labware-ingred/selectors' -import { getInitialDeckSetup } from '../../../../step-forms/selectors' import { getCustomLabwareDefsByURI } from '../../../../labware-defs/selectors' -import { LabwareOnDeck } from '../../../../components/DeckSetup/LabwareOnDeck' import { FixtureRender } from '../FixtureRender' import { SelectedHoveredItems } from '../SelectedHoveredItems' import type * as OpentronsComponents from '@opentrons/components' -import type { LabwareDefinition2 } from '@opentrons/shared-data' -vi.mock('../../../../step-forms/selectors') vi.mock('../FixtureRender') vi.mock('../../../../labware-ingred/selectors') vi.mock('../../../../labware-defs/selectors') -vi.mock('../../../../components/DeckSetup/LabwareOnDeck') vi.mock('@opentrons/components', async importOriginal => { const actual = await importOriginal() return { ...actual, + LabwareRender: vi.fn(), Module: vi.fn(), } }) @@ -48,20 +43,6 @@ describe('SelectedHoveredItems', () => { hoveredFixture: null, slotPosition: [0, 0, 0], } - vi.mocked(getInitialDeckSetup).mockReturnValue({ - modules: {}, - additionalEquipmentOnDeck: {}, - pipettes: {}, - labware: { - labware: { - id: 'mockId', - def: fixture24Tuberack as LabwareDefinition2, - labwareDefURI: 'fixture/fixture_universal_flat_bottom_adapter/1', - slot: 'D3', - }, - }, - }) - vi.mocked(LabwareOnDeck).mockReturnValue(
mock LabwareOnDeck
) vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ selectedLabwareDefUri: null, selectedNestedLabwareDefUri: null, @@ -71,6 +52,7 @@ describe('SelectedHoveredItems', () => { }) vi.mocked(getCustomLabwareDefsByURI).mockReturnValue({}) vi.mocked(FixtureRender).mockReturnValue(
mock FixtureRender
) + vi.mocked(LabwareRender).mockReturnValue(
mock LabwareRender
) vi.mocked(Module).mockReturnValue(
mock Module
) }) it('renders a selected fixture by itself', () => { @@ -88,9 +70,9 @@ describe('SelectedHoveredItems', () => { }) render(props) screen.getByText('mock FixtureRender') - screen.getByText('mock LabwareOnDeck') + screen.getByText('mock LabwareRender') expect(screen.queryByText('mock Module')).not.toBeInTheDocument() - screen.getByText('Opentrons screwcap 2mL tuberack') + screen.getByText('Fixture Opentrons Universal Flat Heater-Shaker Adapter') }) it('renders a selected module', () => { vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ @@ -120,25 +102,6 @@ describe('SelectedHoveredItems', () => { screen.getByText('Fixture Opentrons Universal Flat Heater-Shaker Adapter') }) it('renders selected fixture and both labware and nested labware', () => { - vi.mocked(getInitialDeckSetup).mockReturnValue({ - modules: {}, - additionalEquipmentOnDeck: {}, - pipettes: {}, - labware: { - labware: { - id: 'mockId', - def: fixture24Tuberack as LabwareDefinition2, - labwareDefURI: 'fixture/fixture_universal_flat_bottom_adapter/1', - slot: 'D3', - }, - labware2: { - id: 'mockId2', - def: fixture24Tuberack as LabwareDefinition2, - labwareDefURI: 'fixture/fixture_universal_flat_bottom_adapter/1', - slot: 'mockId', - }, - }, - }) vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ selectedLabwareDefUri: 'fixture/fixture_universal_flat_bottom_adapter/1', selectedNestedLabwareDefUri: @@ -149,10 +112,12 @@ describe('SelectedHoveredItems', () => { }) render(props) screen.getByText('mock FixtureRender') - expect(screen.getAllByText('mock LabwareOnDeck')).toHaveLength(2) - expect(screen.getAllByText('Opentrons screwcap 2mL tuberack')).toHaveLength( - 2 - ) + expect(screen.getAllByText('mock LabwareRender')).toHaveLength(2) + expect( + screen.getAllByText( + 'Fixture Opentrons Universal Flat Heater-Shaker Adapter' + ) + ).toHaveLength(2) }) it('renders nothing when there is a hovered module but selected fixture', () => { props.hoveredModule = HEATERSHAKER_MODULE_V1 diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/utils.test.ts b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/utils.test.ts index 50325ad7197..d990339c58b 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/utils.test.ts +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/utils.test.ts @@ -44,7 +44,7 @@ describe('getModuleModelsBySlot', () => { ]) }) it('renders all flex modules for B1', () => { - expect(getModuleModelsBySlot(true, FLEX_ROBOT_TYPE, 'B1')).toEqual( + expect(getModuleModelsBySlot(false, FLEX_ROBOT_TYPE, 'B1')).toEqual( FLEX_MODULE_MODELS ) }) @@ -52,7 +52,7 @@ describe('getModuleModelsBySlot', () => { const noTC = FLEX_MODULE_MODELS.filter( model => model !== THERMOCYCLER_MODULE_V2 ) - expect(getModuleModelsBySlot(true, FLEX_ROBOT_TYPE, 'C1')).toEqual(noTC) + expect(getModuleModelsBySlot(false, FLEX_ROBOT_TYPE, 'C1')).toEqual(noTC) }) }) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/utils.ts b/protocol-designer/src/pages/Designer/DeckSetup/utils.ts index b231da91072..8109b8ca50e 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/utils.ts +++ b/protocol-designer/src/pages/Designer/DeckSetup/utils.ts @@ -1,6 +1,5 @@ import some from 'lodash/some' import { - ABSORBANCE_READER_V1, FLEX_ROBOT_TYPE, FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS, HEATERSHAKER_MODULE_TYPE, @@ -13,7 +12,6 @@ import { } from '@opentrons/shared-data' import { getOnlyLatestDefs } from '../../../labware-defs' -import { getStagingAreaAddressableAreas } from '../../../utils' import { FLEX_MODULE_MODELS, OT2_MODULE_MODELS, @@ -30,12 +28,7 @@ import type { ModuleModel, RobotType, } from '@opentrons/shared-data' -import type { - AllTemporalPropertiesForTimelineFrame, - InitialDeckSetup, - LabwareOnDeck, -} from '../../../step-forms' -import type { Fixture } from './constants' +import type { InitialDeckSetup } from '../../../step-forms' const OT2_TC_SLOTS = ['7', '8', '10', '11'] const FLEX_TC_SLOTS = ['A1', 'B1'] @@ -62,21 +55,20 @@ export function getModuleModelsBySlot( ): ModuleModel[] { const FLEX_MIDDLE_SLOTS = ['B2', 'C2', 'A2', 'D2'] const OT2_MIDDLE_SLOTS = ['2', '5', '8', '11'] - const FILTERED_MODULES = enableAbsorbanceReader - ? FLEX_MODULE_MODELS - : FLEX_MODULE_MODELS.filter(model => model !== ABSORBANCE_READER_V1) - let moduleModels: ModuleModel[] = FILTERED_MODULES + let moduleModels: ModuleModel[] = enableAbsorbanceReader + ? FLEX_MODULE_MODELS.filter(model => model !== 'absorbanceReaderV1') + : FLEX_MODULE_MODELS switch (robotType) { case FLEX_ROBOT_TYPE: { if (slot !== 'B1' && !FLEX_MIDDLE_SLOTS.includes(slot)) { - moduleModels = FILTERED_MODULES.filter( + moduleModels = FLEX_MODULE_MODELS.filter( model => model !== THERMOCYCLER_MODULE_V2 ) } if (FLEX_MIDDLE_SLOTS.includes(slot)) { - moduleModels = FILTERED_MODULES.filter( + moduleModels = FLEX_MODULE_MODELS.filter( model => model === MAGNETIC_BLOCK_V1 ) } @@ -261,22 +253,3 @@ export function animateZoom(props: AnimateZoomProps): void { } requestAnimationFrame(animate) } - -export const getAdjacentLabware = ( - fixture: Fixture, - cutout: CutoutId, - labware: AllTemporalPropertiesForTimelineFrame['labware'] -): LabwareOnDeck | null => { - let adjacentLabware: LabwareOnDeck | null = null - if (fixture === 'stagingArea' || fixture === 'wasteChuteAndStagingArea') { - const stagingAreaAddressableAreaName = getStagingAreaAddressableAreas([ - cutout, - ]) - - adjacentLabware = - Object.values(labware).find( - lw => lw.slot === stagingAreaAddressableAreaName[0] - ) ?? null - } - return adjacentLabware -} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/FlowRateField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/FlowRateField.tsx index 2e264548d28..e853a76ddaf 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/FlowRateField.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/FlowRateField.tsx @@ -59,8 +59,7 @@ export function FlowRateField(props: FlowRateFieldProps): JSX.Element { let errorMessage: string | null = null if ( (!isPristine && passThruProps.value !== undefined && flowRateNum === 0) || - outOfBounds || - (isPristine && flowRateNum === 0) + outOfBounds ) { errorMessage = i18n.format( t('step_edit_form.field.flow_rate.error_out_of_bounds', { @@ -72,10 +71,10 @@ export function FlowRateField(props: FlowRateFieldProps): JSX.Element { } useEffect(() => { - if (isPristine && passThruProps.value == null) { + if (isPristine && errorMessage != null) { passThruProps.updateValue(defaultFlowRate) } - }, [isPristine, passThruProps]) + }, []) return ( ) } diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/LabwareField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/LabwareField.tsx index daa290efc18..f0c6df3544d 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/LabwareField.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/LabwareField.tsx @@ -22,7 +22,7 @@ export function LabwareField(props: FieldProps): JSX.Element { {...props} name={name} options={allOptions} - title={t(`${name}`)} + title={t(`select_${name}`)} /> ) } diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PathField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PathField.tsx index dd2854344b2..77eb10b82ce 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PathField.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PathField.tsx @@ -6,7 +6,6 @@ import { Flex, RadioButton, SPACING, - TOOLTIP_TOP_START, Tooltip, useHoverTooltip, } from '@opentrons/components' @@ -63,23 +62,19 @@ interface PathButtonProps { function PathButton(props: PathButtonProps): JSX.Element { const { disabled, onClick, id, path, selected, subtitle } = props - const [targetProps, tooltipProps] = useHoverTooltip({ - placement: TOOLTIP_TOP_START, - }) + const [targetProps, tooltipProps] = useHoverTooltip() const { t } = useTranslation(['form', 'protocol_steps']) // TODO: update the tooltip and images const tooltip = ( - - - {t(`step_edit_form.field.path.title.${path}`)} - - {subtitle} - + + {t(`step_edit_form.field.path.title.${path}`)} + + {subtitle} ) return ( - + {tooltip} { {...props} options={pipetteOptions} value={value ? String(value) : null} - title={t('pipette')} + title={t('select_pipette')} /> ) } diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/TiprackField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/TiprackField.tsx index df9d5ad4fd0..9f9cca5157e 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/TiprackField.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/TiprackField.tsx @@ -1,14 +1,6 @@ import { useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' -import { - COLORS, - DIRECTION_COLUMN, - Flex, - ListItem, - SPACING, - StyledText, -} from '@opentrons/components' import { getPipetteEntities } from '../../../../../step-forms/selectors' import { getTiprackOptions } from '../../../../../ui/labware/selectors' import { DropdownStepFormField } from '../../../../../molecules' @@ -37,34 +29,12 @@ export function TiprackField(props: TiprackFieldProps): JSX.Element { }, [defaultTiprackUris, value, updateValue]) const hasMissingTiprack = defaultTiprackUris.length > tiprackOptions.length return ( - <> - {tiprackOptions.length > 1 ? ( - - ) : ( - - - {t('tiprack')} - - - - - {tiprackOptions[0].name} - - - - - )} - + ) } diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/VolumeField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/VolumeField.tsx index 5f0658ef949..2059a81b389 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/VolumeField.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/VolumeField.tsx @@ -8,7 +8,7 @@ export function VolumeField(props: FieldProps): JSX.Element { return ( diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/WellSelectionField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/WellSelectionField.tsx index ddd54b923f6..57125f7b8a1 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/WellSelectionField.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/WellSelectionField.tsx @@ -1,10 +1,7 @@ import { createPortal } from 'react-dom' import { useDispatch, useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' - import { - ALIGN_CENTER, - BORDERS, COLORS, DIRECTION_COLUMN, Flex, @@ -16,7 +13,6 @@ import { useHoverTooltip, } from '@opentrons/components' import { COLUMN } from '@opentrons/shared-data' - import { actions as stepsActions, getSelectedStepId, @@ -26,7 +22,6 @@ import { selectors as stepFormSelectors } from '../../../../../step-forms' import { SelectWellsModal } from '../../../../../organisms' import { getMainPagePortalEl } from '../../../../../components/portals/MainPageModalPortal' import { getNozzleType } from '../utils' - import type { FieldProps } from '../types' export type WellSelectionFieldProps = FieldProps & { @@ -99,12 +94,8 @@ export const WellSelectionField = ( const [targetProps, tooltipProps] = useHoverTooltip() return ( <> - - + + {i18n.format(label, 'capitalize')} @@ -123,12 +114,10 @@ export const WellSelectionField = ( disabled={disabled ?? labwareId != null} readOnly name={name} - value={primaryWellCount ?? errorToShow} + error={errorToShow} + value={primaryWellCount} onClick={handleOpen} hasBackgroundError={hasFormError} - size="medium" - borderRadius={BORDERS.borderRadius8} - padding={SPACING.spacing12} /> {createPortal( diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx index 2cc845fbe17..a14be039d93 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepFormToolbox.tsx @@ -1,4 +1,4 @@ -import { useRef, useState } from 'react' +import { useState } from 'react' import get from 'lodash/get' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' @@ -10,11 +10,11 @@ import { Icon, POSITION_RELATIVE, PrimaryButton, - SecondaryButton, SPACING, + SecondaryButton, StyledText, - Toolbox, TYPOGRAPHY, + Toolbox, } from '@opentrons/components' import { stepIconsByType } from '../../../../form-types' import { FormAlerts } from '../../../../organisms' @@ -40,20 +40,11 @@ import { getVisibleFormErrors, getVisibleFormWarnings, capitalizeFirstLetter, - getIsErrorOnCurrentPage, } from './utils' import type { StepFieldName } from '../../../../steplist/fieldLevel' import type { FormData, StepType } from '../../../../form-types' -import type { - FieldPropsByName, - FocusHandlers, - LiquidHandlingTab, - StepFormProps, -} from './types' -import { - getDynamicFieldFormErrorsForUnsavedForm, - getFormLevelErrorsForUnsavedForm, -} from '../../../../step-forms/selectors' +import type { FieldPropsByName, FocusHandlers, StepFormProps } from './types' +import { getFormLevelErrorsForUnsavedForm } from '../../../../step-forms/selectors' type StepFormMap = { [K in StepType]?: React.ComponentType | null @@ -99,8 +90,6 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { 'protocol_steps', ]) const { makeSnackbar } = useKitchen() - const toolsComponentRef = useRef(null) - const formWarningsForSelectedStep = useSelector( getFormWarningsForSelectedStep ) @@ -110,20 +99,18 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { const formLevelErrorsForUnsavedForm = useSelector( getFormLevelErrorsForUnsavedForm ) - const dynamicFormLevelErrorsForUnsavedForm = useSelector( - getDynamicFieldFormErrorsForUnsavedForm - ).map(error => ({ - title: error.title, - body: error.body, - dependentFields: error.dependentProfileFields, - })) const timeline = useSelector(getRobotStateTimeline) - const [toolboxStep, setToolboxStep] = useState(0) + const [toolboxStep, setToolboxStep] = useState( + // progress to step 2 if thermocycler form is populated + formData.thermocyclerFormType === 'thermocyclerProfile' || + formData.thermocyclerFormType === 'thermocyclerState' + ? 1 + : 0 + ) const [ showFormErrorsAndWarnings, setShowFormErrorsAndWarnings, ] = useState(false) - const [tab, setTab] = useState('aspirate') const visibleFormWarnings = getVisibleFormWarnings({ focusedField, dirtyFields: dirtyFields ?? [], @@ -132,12 +119,7 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { const visibleFormErrors = getVisibleFormErrors({ focusedField, dirtyFields: dirtyFields ?? [], - errors: [ - ...formLevelErrorsForUnsavedForm, - ...dynamicFormLevelErrorsForUnsavedForm, - ], - page: toolboxStep, - showErrors: showFormErrorsAndWarnings, + errors: formLevelErrorsForUnsavedForm, }) const [isRename, setIsRename] = useState(false) const icon = stepIconsByType[formData.stepType] @@ -147,13 +129,6 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { formData.stepType ) - const isAspirateError = formLevelErrorsForUnsavedForm.some( - error => error.tab === 'aspirate' && error.page === toolboxStep - ) - const isDispenseError = formLevelErrorsForUnsavedForm.some( - error => error.tab === 'dispense' && error.page === toolboxStep - ) - if (!ToolsComponent) { // early-exit if step form doesn't exist, this is a good check for when new steps // are added @@ -172,19 +147,6 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { visibleFormWarnings.length + timelineWarningsForSelectedStep.length const numErrors = timeline.errors?.length ?? 0 - const isErrorOnCurrentPage = getIsErrorOnCurrentPage({ - errors: formLevelErrorsForUnsavedForm, - page: toolboxStep, - }) - const handleScrollToTop = (): void => { - if (toolsComponentRef.current) { - toolsComponentRef.current.scrollIntoView({ - behavior: 'smooth', - block: 'start', - }) - } - } - const handleSaveClick = (): void => { if (canSave) { handleSave() @@ -197,31 +159,10 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { 'capitalize' ), t, - }) + }) as string ) } else { setShowFormErrorsAndWarnings(true) - if (tab === 'aspirate' && isDispenseError && !isAspirateError) { - setTab('dispense') - } - if (tab === 'dispense' && isAspirateError && !isDispenseError) { - setTab('aspirate') - } - handleScrollToTop() - } - } - - const handleContinue = (): void => { - if (isMultiStepToolbox && toolboxStep === 0) { - if (!isErrorOnCurrentPage) { - setToolboxStep(1) - setShowFormErrorsAndWarnings(false) - } else { - setShowFormErrorsAndWarnings(true) - handleScrollToTop() - } - } else { - handleSaveClick() } } @@ -240,7 +181,7 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { subHeader={ isMultiStepToolbox ? ( - {t('shared:part', { current: toolboxStep + 1, max: 2 })} + {t('shared:step', { current: toolboxStep + 1, max: 2 })} ) : null } @@ -267,13 +208,21 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { width="100%" onClick={() => { setToolboxStep(0) - setShowFormErrorsAndWarnings(false) }} > {i18n.format(t('shared:back'), 'capitalize')} ) : null} - + { + setToolboxStep(1) + } + : handleSaveClick + } + width="100%" + > {isMultiStepToolbox && toolboxStep === 0 ? i18n.format(t('shared:continue'), 'capitalize') : t('shared:save')} @@ -292,28 +241,23 @@ export function StepFormToolbox(props: StepFormToolboxProps): JSX.Element { } > -
- - -
+ + ) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/CommentTools/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/CommentTools/index.tsx index a6956cd342d..72688f43146 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/CommentTools/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/CommentTools/index.tsx @@ -1,50 +1,3 @@ -import { useTranslation } from 'react-i18next' -import styled from 'styled-components' -import { - BORDERS, - COLORS, - DIRECTION_COLUMN, - Flex, - SPACING, - StyledText, - TYPOGRAPHY, -} from '@opentrons/components' -import type { ChangeEvent } from 'react' -import type { StepFormProps } from '../../types' - -export function CommentTools(props: StepFormProps): JSX.Element { - const { t, i18n } = useTranslation('form') - const { propsForFields } = props - - return ( - - - {i18n.format(t('step_edit_form.field.comment.label'), 'capitalize')} - - ) => { - propsForFields.message.updateValue(e.currentTarget.value) - }} - /> - - ) +export function CommentTools(): JSX.Element { + return
TODO: wire this up
} - -// TODO: use TextArea component when we make it -const StyledTextArea = styled.textarea` - width: 100%; - height: 7rem; - box-sizing: border-box; - border: 1px solid ${COLORS.grey50}; - border-radius: ${BORDERS.borderRadius4}; - padding: ${SPACING.spacing8}; - font-size: ${TYPOGRAPHY.fontSizeH4}; - line-height: ${TYPOGRAPHY.lineHeight16}; - font-weight: ${TYPOGRAPHY.fontWeightRegular}; - resize: none; -` diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/HeaterShakerTools/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/HeaterShakerTools/index.tsx index 2d16704fdb3..6a0315bb3bf 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/HeaterShakerTools/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/HeaterShakerTools/index.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'react' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' import { @@ -5,6 +6,7 @@ import { COLORS, DIRECTION_COLUMN, Flex, + ListItem, SPACING, StyledText, } from '@opentrons/components' @@ -18,19 +20,50 @@ import { getFormErrorsMappedToField, getFormLevelError } from '../../utils' import type { StepFormProps } from '../../types' export function HeaterShakerTools(props: StepFormProps): JSX.Element { - const { propsForFields, formData, visibleFormErrors } = props + const { + propsForFields, + formData, + showFormErrors = false, + focusedField = null, + visibleFormErrors, + } = props const { t } = useTranslation(['application', 'form', 'protocol_steps']) const moduleLabwareOptions = useSelector(getHeaterShakerLabwareOptions) + useEffect(() => { + if (moduleLabwareOptions.length === 1) { + propsForFields.moduleId.updateValue(moduleLabwareOptions[0].value) + } + }, []) + const mappedErrorsToField = getFormErrorsMappedToField(visibleFormErrors) return ( - + {moduleLabwareOptions.length > 1 ? ( + + ) : ( + + + {t('protocol_steps:module')} + + + + + {moduleLabwareOptions[0].name} + + + + + )} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MagnetTools/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MagnetTools/index.tsx index 5d0d54cd5ea..2468923d9c2 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MagnetTools/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MagnetTools/index.tsx @@ -32,12 +32,11 @@ import { } from '../../../../../../step-forms/selectors' import { getModulesOnDeckByType } from '../../../../../../ui/modules/utils' import { LINE_CLAMP_TEXT_STYLE } from '../../../../../../atoms' -import { getFormErrorsMappedToField, getFormLevelError } from '../../utils' import type { StepFormProps } from '../../types' export function MagnetTools(props: StepFormProps): JSX.Element { - const { propsForFields, formData, visibleFormErrors } = props + const { propsForFields, formData } = props const { t } = useTranslation(['application', 'form', 'protocol_steps']) const moduleLabwareOptions = useSelector(getMagneticLabwareOptions) const moduleEntities = useSelector(getModuleEntities) @@ -70,9 +69,6 @@ export function MagnetTools(props: StepFormProps): JSX.Element { }) : '' const engageHeightCaption = `${engageHeightMinMax} ${engageHeightDefault}` - - const mappedErrorsToField = getFormErrorsMappedToField(visibleFormErrors) - return ( diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MixTools/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MixTools/index.tsx index aae66a5f762..a319afc572a 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MixTools/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MixTools/index.tsx @@ -1,5 +1,6 @@ import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' +import { useState } from 'react' import { DIRECTION_COLUMN, Divider, @@ -36,24 +37,16 @@ import { } from '../../PipetteFields' import { getBlowoutLocationOptionsForForm, - getFormErrorsMappedToField, - getFormLevelError, getLabwareFieldForPositioningField, } from '../../utils' import type { StepFormProps } from '../../types' export function MixTools(props: StepFormProps): JSX.Element { - const { - propsForFields, - formData, - toolboxStep, - visibleFormErrors, - tab, - setTab, - } = props + const { propsForFields, formData, toolboxStep, visibleFormErrors } = props const pipettes = useSelector(getPipetteEntities) const enableReturnTip = useSelector(getEnableReturnTip) const labwares = useSelector(getLabwareEntities) + const [tab, setTab] = useState<'aspirate' | 'dispense'>('aspirate') const { t, i18n } = useTranslation(['application', 'form']) const aspirateTab = { text: i18n.format(t('aspirate'), 'capitalize'), @@ -70,7 +63,6 @@ export function MixTools(props: StepFormProps): JSX.Element { setTab('dispense') }, } - const is96Channel = propsForFields.pipette.value != null && pipettes[String(propsForFields.pipette.value)].name === 'p1000_96' @@ -79,8 +71,6 @@ export function MixTools(props: StepFormProps): JSX.Element { const userSelectedDropTipLocation = labwares[String(propsForFields.dropTip_location.value)] != null - const mappedErrorsToField = getFormErrorsMappedToField(visibleFormErrors) - return toolboxStep === 0 ? ( @@ -91,10 +81,7 @@ export function MixTools(props: StepFormProps): JSX.Element { pipetteId={propsForFields.pipette.value} /> - + - error.dependentFields.includes('wells') + error.dependentFields.includes('labware') ) ?? false } - errorToShow={getFormLevelError('wells', mappedErrorsToField)} /> - + ) : null} @@ -260,10 +238,6 @@ export function MixTools(props: StepFormProps): JSX.Element { options={getBlowoutLocationOptionsForForm({ stepType: formData.stepType, })} - errorToShow={getFormLevelError( - 'blowout_location', - mappedErrorsToField - )} /> ) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLabwareTools/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLabwareTools/index.tsx index 8355ed6fa4d..fa71de6a2b4 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLabwareTools/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLabwareTools/index.tsx @@ -8,14 +8,13 @@ import { getAdditionalEquipment, getCurrentFormCanBeSaved, } from '../../../../../../step-forms/selectors' -import { getFormErrorsMappedToField, getFormLevelError } from '../../utils' import { MoveLabwareField } from './MoveLabwareField' import { LabwareLocationField } from './LabwareLocationField' import type { StepFormProps } from '../../types' export function MoveLabwareTools(props: StepFormProps): JSX.Element { - const { propsForFields, visibleFormErrors } = props + const { propsForFields } = props const { t, i18n } = useTranslation(['application', 'form', 'tooltip']) const robotType = useSelector(getRobotType) const canSave = useSelector(getCurrentFormCanBeSaved) @@ -24,40 +23,32 @@ export function MoveLabwareTools(props: StepFormProps): JSX.Element { equipment => equipment?.name === 'gripper' ) - const mappedErrorsToField = getFormErrorsMappedToField(visibleFormErrors) - return ( {robotType === FLEX_ROBOT_TYPE ? ( - <> - - - + ) : null} - + + ) } diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx index 19661741298..95291484386 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MoveLiquidTools/index.tsx @@ -1,5 +1,6 @@ import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' +import { useState } from 'react' import { DIRECTION_COLUMN, Divider, @@ -39,8 +40,6 @@ import { } from '../../PipetteFields' import { getBlowoutLocationOptionsForForm, - getFormErrorsMappedToField, - getFormLevelError, getLabwareFieldForPositioningField, } from '../../utils' import type { StepFieldName } from '../../../../../../form-types' @@ -51,17 +50,10 @@ const makeAddFieldNamePrefix = (prefix: string) => ( ): StepFieldName => `${prefix}_${fieldName}` export function MoveLiquidTools(props: StepFormProps): JSX.Element { - const { - toolboxStep, - propsForFields, - formData, - visibleFormErrors, - setShowFormErrorsAndWarnings, - tab, - setTab, - } = props + const { toolboxStep, propsForFields, formData, visibleFormErrors } = props const { t, i18n } = useTranslation(['protocol_steps', 'form']) const { path } = formData + const [tab, setTab] = useState<'aspirate' | 'dispense'>('aspirate') const additionalEquipmentEntities = useSelector( getAdditionalEquipmentEntities ) @@ -101,7 +93,6 @@ export function MoveLiquidTools(props: StepFormProps): JSX.Element { isActive: tab === 'aspirate', onClick: () => { setTab('aspirate') - setShowFormErrorsAndWarnings?.(false) }, } const dispenseTab = { @@ -110,14 +101,11 @@ export function MoveLiquidTools(props: StepFormProps): JSX.Element { isActive: tab === 'dispense', onClick: () => { setTab('dispense') - setShowFormErrorsAndWarnings?.(false) }, } const hideWellOrderField = tab === 'dispense' && (isWasteChuteSelected || isTrashBinSelected) - const mappedErrorsToField = getFormErrorsMappedToField(visibleFormErrors) - return toolboxStep === 0 ? ( @@ -129,15 +117,9 @@ export function MoveLiquidTools(props: StepFormProps): JSX.Element { pipetteId={propsForFields.pipette.value} /> - + - + - + {isDisposalLocation ? null : ( )} @@ -313,10 +290,6 @@ export function MoveLiquidTools(props: StepFormProps): JSX.Element { title={t('protocol_steps:mix_volume')} {...propsForFields[`${tab}_mix_volume`]} units={t('application:units.microliter')} - errorToShow={getFormLevelError( - `${tab}_mix_volume`, - mappedErrorsToField - )} /> ) : null} @@ -355,10 +324,6 @@ export function MoveLiquidTools(props: StepFormProps): JSX.Element { title={t('protocol_steps:delay_duration')} {...propsForFields[`${tab}_delay_seconds`]} units={t('application:units.seconds')} - errorToShow={getFormLevelError( - `${tab}_delay_seconds`, - mappedErrorsToField - )} /> ) : null} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/PauseTools/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/PauseTools/index.tsx index 07d80908d0b..98175175218 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/PauseTools/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/PauseTools/index.tsx @@ -37,6 +37,8 @@ export function PauseTools(props: StepFormProps): JSX.Element { const { propsForFields, visibleFormErrors, + focusedField, + showFormErrors, setShowFormErrorsAndWarnings, } = props @@ -82,10 +84,6 @@ export function PauseTools(props: StepFormProps): JSX.Element { const mappedErrorsToField = getFormErrorsMappedToField(visibleFormErrors) - const formLevelErrorsWithoutField = visibleFormErrors.filter( - error => error.dependentFields.length === 0 - ) - return ( <> @@ -135,133 +133,105 @@ export function PauseTools(props: StepFormProps): JSX.Element { largeDesktopBorderRadius disabled={!pauseUntilModuleEnabled} /> - {formLevelErrorsWithoutField.map(error => ( - - {error.title} - - ))} - {pauseAction != null ? ( - <> - {' '} - + + + {pauseAction === PAUSE_UNTIL_TIME ? ( - {pauseAction === PAUSE_UNTIL_TIME ? ( - - - - - - ) : null} - {pauseAction === PAUSE_UNTIL_TEMP ? ( - <> - - - {i18n.format( - t( - 'form:step_edit_form.field.moduleActionLabware.label' - ), - 'capitalize' - )} - - { - propsForFields.moduleId.updateValue(value) - }} - currentOption={ - moduleOptions.find( - option => - option.value === propsForFields.moduleId.value - ) ?? { name: '', value: '' } - } - dropdownType="neutral" - width="100%" - error={getFormLevelError( - 'moduleId', - mappedErrorsToField - )} - /> - - - - - - ) : null} - - - - {i18n.format( - t('form:step_edit_form.field.pauseMessage.label'), - 'capitalize' - )} - - ) => { - propsForFields.pauseMessage.updateValue( - e.currentTarget.value - ) - }} - height="7rem" - /> + + - - ) : null} + ) : null} + {pauseAction === PAUSE_UNTIL_TEMP ? ( + <> + + + {i18n.format( + t('form:step_edit_form.field.moduleActionLabware.label'), + 'capitalize' + )} + + { + propsForFields.moduleId.updateValue(value) + }} + currentOption={ + moduleOptions.find( + option => option.value === propsForFields.moduleId.value + ) ?? { name: '', value: '' } + } + dropdownType="neutral" + width="100%" + /> + + + + + + ) : null} + + + + {i18n.format( + t('form:step_edit_form.field.pauseMessage.label'), + 'capitalize' + )} + + ) => { + propsForFields.pauseMessage.updateValue(e.currentTarget.value) + }} + height="7rem" + /> +
diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/TemperatureTools/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/TemperatureTools/index.tsx index c5d03a44afa..a0e0ca76e27 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/TemperatureTools/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/TemperatureTools/index.tsx @@ -1,3 +1,4 @@ +import * as React from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' import { @@ -5,50 +6,112 @@ import { COLORS, DIRECTION_COLUMN, Flex, + ListItem, + RadioButton, SPACING, + StyledText, } from '@opentrons/components' -import { getTemperatureLabwareOptions } from '../../../../../../ui/modules/selectors' +import { + getTemperatureLabwareOptions, + getTemperatureModuleIds, +} from '../../../../../../ui/modules/selectors' import { DropdownStepFormField, - ToggleExpandStepFormField, + InputStepFormField, } from '../../../../../../molecules' -import { getFormErrorsMappedToField, getFormLevelError } from '../../utils' - import type { StepFormProps } from '../../types' export function TemperatureTools(props: StepFormProps): JSX.Element { - const { propsForFields, formData, visibleFormErrors } = props + const { propsForFields, formData } = props const { t } = useTranslation(['application', 'form', 'protocol_steps']) const moduleLabwareOptions = useSelector(getTemperatureLabwareOptions) + const temperatureModuleIds = useSelector(getTemperatureModuleIds) + const { setTemperature, moduleId } = formData - const mappedErrorsToField = getFormErrorsMappedToField(visibleFormErrors) + React.useEffect(() => { + if (moduleLabwareOptions.length === 1) { + propsForFields.moduleId.updateValue(moduleLabwareOptions[0].value) + } + }, []) return ( - - - - 1 ? ( + - + ) : ( + + + {t('protocol_steps:module')} + + + + + {moduleLabwareOptions[0].name} + + + + + )} + + {temperatureModuleIds != null + ? temperatureModuleIds.map(id => + id === moduleId ? ( + + + ) => { + propsForFields.setTemperature.updateValue( + e.currentTarget.value + ) + }} + buttonLabel={t( + 'form:step_edit_form.field.setTemperature.options.true' + )} + buttonValue="true" + isSelected={propsForFields.setTemperature.value === 'true'} + /> + + {setTemperature === 'true' && ( + + )} + + ) => { + propsForFields.setTemperature.updateValue( + e.currentTarget.value + ) + }} + buttonLabel={t( + 'form:step_edit_form.field.setTemperature.options.false' + )} + buttonValue="false" + isSelected={propsForFields.setTemperature.value === 'false'} + /> + + + ) : null + ) + : null} ) } diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/ProfileSettings.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/ProfileSettings.tsx index dc343ce693e..59d94469fd1 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/ProfileSettings.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/ProfileSettings.tsx @@ -18,7 +18,12 @@ interface ProfileSettingsProps { focusedField?: string | null } export function ProfileSettings(props: ProfileSettingsProps): JSX.Element { - const { propsForFields, visibleFormErrors } = props + const { + propsForFields, + showFormErrors, + visibleFormErrors, + focusedField, + } = props const mappedErrorsToField = getFormErrorsMappedToField(visibleFormErrors) @@ -38,7 +43,12 @@ export function ProfileSettings(props: ProfileSettingsProps): JSX.Element { units={t('units.microliter')} padding="0" showTooltip={false} - formLevelError={getFormLevelError('profileVolume', mappedErrorsToField)} + formLevelError={getFormLevelError( + showFormErrors, + 'profileVolume', + mappedErrorsToField, + focusedField + )} />
diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/ThermocyclerState.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/ThermocyclerState.tsx index 024c22c0e62..0b00c140f65 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/ThermocyclerState.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/ThermocyclerTools/ThermocyclerState.tsx @@ -33,6 +33,8 @@ export function ThermocyclerState(props: ThermocyclerStateProps): JSX.Element { formData, isHold = false, visibleFormErrors, + showFormErrors = true, + focusedField, } = props const { i18n, t } = useTranslation(['application', 'form']) @@ -81,7 +83,12 @@ export function ThermocyclerState(props: ThermocyclerStateProps): JSX.Element { isSelected={formData[blockFieldActive] === true} onLabel={t('form:step_edit_form.field.heaterShaker.shaker.toggleOn')} offLabel={t('form:step_edit_form.field.heaterShaker.shaker.toggleOff')} - formLevelError={getFormLevelError(blockTempField, mappedErrorsToField)} + formLevelError={getFormLevelError( + showFormErrors, + blockTempField, + mappedErrorsToField, + focusedField + )} /> ( - formData.thermocyclerFormType as ThermocyclerContentType + (formData.thermocyclerFormType as ThermocyclerContentType) ?? + 'thermocyclerState' ) if (toolboxStep === 0) { diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/__tests__/MagnetTools.test.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/__tests__/MagnetTools.test.tsx index 15b4adcd78b..368bfb1d1ee 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/__tests__/MagnetTools.test.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/__tests__/MagnetTools.test.tsx @@ -70,8 +70,6 @@ describe('MagnetTools', () => { }, }, showFormErrors: false, - tab: 'aspirate', - setTab: vi.fn(), } vi.mocked(getMagneticLabwareOptions).mockReturnValue([ { name: 'mock labware in mock module in slot abc', value: 'mockValue' }, diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/__tests__/TemperatureTools.test.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/__tests__/TemperatureTools.test.tsx index 904377f66f4..498e6b2e1db 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/__tests__/TemperatureTools.test.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/__tests__/TemperatureTools.test.tsx @@ -72,8 +72,6 @@ describe('TemperatureTools', () => { }, }, showFormErrors: false, - tab: 'aspirate', - setTab: vi.fn(), } vi.mocked(getTemperatureModuleIds).mockReturnValue(['mockId']) @@ -87,7 +85,9 @@ describe('TemperatureTools', () => { it('renders a temperature module form with 1 module', () => { render(props) - screen.getByText('Module state') + screen.getByText('Module') screen.getByText('mock module') + screen.getByText('Deactivate module') + screen.getByText('Change to temperature') }) }) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/__tests__/utils.test.ts b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/__tests__/utils.test.ts index 1f79fe88440..6007eae6d0c 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/__tests__/utils.test.ts +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/__tests__/utils.test.ts @@ -122,16 +122,26 @@ describe('getFormErrorsMappedToField', () => { }) describe('getFormLevelError', () => { - it('shows form-level error at field when showAtField is true', () => { - const result = getFormLevelError('field1', MAPPED_ERRORS) + it('shows form-level error at field when field is not focused and showAtField is true', () => { + const result = getFormLevelError(true, 'field1', MAPPED_ERRORS) expect(result).toEqual('form level error title') }) - it('shows no form-level error at field when showAtField is false', () => { - const result = getFormLevelError('field1', { - ...MAPPED_ERRORS, - field1: { ...MAPPED_ERRORS.field1, showAtField: false }, - }) + it('shows no form-level error at field when field is focused and showAtField is true', () => { + const result = getFormLevelError(true, 'field1', MAPPED_ERRORS, 'field1') + expect(result).toBeNull() + }) + + it('shows no form-level error at field when field is not focused and showAtField is false', () => { + const result = getFormLevelError( + true, + 'field1', + { + ...MAPPED_ERRORS, + field1: { ...MAPPED_ERRORS.field1, showAtField: false }, + }, + 'field2' + ) expect(result).toBeNull() }) }) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/types.ts b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/types.ts index c2d50cfbcb5..f0bd6970e73 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/types.ts +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/types.ts @@ -20,7 +20,6 @@ export interface FieldProps { export type FieldPropsByName = Record // Shared props across all step forms -export type LiquidHandlingTab = 'aspirate' | 'dispense' export interface StepFormProps { formData: FormData focusHandlers: FocusHandlers @@ -30,6 +29,4 @@ export interface StepFormProps { showFormErrors: boolean focusedField?: string | null setShowFormErrorsAndWarnings?: React.Dispatch> - tab: LiquidHandlingTab - setTab: React.Dispatch> } diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/utils.ts b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/utils.ts index 3821d0ba49d..563308f1238 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/utils.ts +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/utils.ts @@ -104,33 +104,20 @@ export const getDirtyFields = ( // exclude form "metadata" (not really fields) return without(dirtyFields, 'stepType', 'id') } - -export const getIsErrorOnCurrentPage = (args: { - errors: StepFormErrors - page: number -}): boolean => { - const { errors, page = 0 } = args - return errors.some(error => error.page == null || error.page === page) -} - export const getVisibleFormErrors = (args: { focusedField?: string | null dirtyFields: string[] errors: StepFormErrors - showErrors?: boolean - page: number }): StepFormErrors => { - const { focusedField, errors, page = 0, showErrors } = args - + const { focusedField, dirtyFields, errors } = args return errors.filter(error => { const dependentFieldsAreNotFocused = !error.dependentFields.includes( // @ts-expect-error(sa, 2021-6-22): focusedField might be undefined focusedField ) - - const isPageImplicated = error.page != null ? page === error.page : true - - return isPageImplicated && dependentFieldsAreNotFocused && showErrors + const dependentFieldsAreDirty = + difference(error.dependentFields, dirtyFields).length === 0 + return dependentFieldsAreNotFocused && dependentFieldsAreDirty }) } export const getVisibleFormWarnings = (args: { @@ -366,10 +353,14 @@ export const getFormErrorsMappedToField = ( } export const getFormLevelError = ( + showFormErrors: boolean, fieldName: string, - mappedErrorsToField: ErrorMappedToField + mappedErrorsToField: ErrorMappedToField, + focusedField?: string | null ): string | null => { - return mappedErrorsToField[fieldName] && + return showFormErrors && + focusedField !== fieldName && + mappedErrorsToField[fieldName] && mappedErrorsToField[fieldName].showAtField ? mappedErrorsToField[fieldName].title : null diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ConnectedStepInfo.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ConnectedStepInfo.tsx index 778159b6d31..1e533b2bfd3 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ConnectedStepInfo.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/ConnectedStepInfo.tsx @@ -46,11 +46,10 @@ import type { DeleteModalType } from '../../../../components/modals/ConfirmDelet export interface ConnectedStepInfoProps { stepId: StepIdType stepNumber: number - dragHovered?: boolean } export function ConnectedStepInfo(props: ConnectedStepInfoProps): JSX.Element { - const { stepId, stepNumber, dragHovered = false } = props + const { stepId, stepNumber } = props const { t } = useTranslation('application') const dispatch = useDispatch>() const stepIds = useSelector(getOrderedStepIds) @@ -216,7 +215,6 @@ export function ConnectedStepInfo(props: ConnectedStepInfoProps): JSX.Element { title={`${stepNumber}. ${ step.stepName || t(`stepType.${step.stepType}`) }`} - dragHovered={dragHovered} /> ) diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/DraggableSteps.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/DraggableSteps.tsx index d48ada0665b..bf92c681796 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/DraggableSteps.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/DraggableSteps.tsx @@ -14,7 +14,7 @@ import { selectors as stepFormSelectors } from '../../../../step-forms' import { stepIconsByType } from '../../../../form-types' import { StepContainer } from './StepContainer' import { ConnectedStepInfo } from './ConnectedStepInfo' -import type { DragLayerMonitor, DropTargetMonitor } from 'react-dnd' +import type { DragLayerMonitor, DropTargetOptions } from 'react-dnd' import type { StepIdType } from '../../../../form-types' import type { ConnectedStepItemProps } from '../../../../containers/ConnectedStepItem' @@ -44,7 +44,7 @@ function DragDropStep(props: DragDropStepProps): JSX.Element { [orderedStepIds] ) - const [{ handlerId, hovered }, drop] = useDrop( + const [{ handlerId }, drop] = useDrop( () => ({ accept: DND_TYPES.STEP_ITEM, canDrop: () => { @@ -57,9 +57,8 @@ function DragDropStep(props: DragDropStepProps): JSX.Element { moveStep(draggedId, overIndex) } }, - collect: (monitor: DropTargetMonitor) => ({ + collect: (monitor: DropTargetOptions) => ({ handlerId: monitor.getHandlerId(), - hovered: monitor.isOver(), }), }), [orderedStepIds] @@ -72,11 +71,7 @@ function DragDropStep(props: DragDropStepProps): JSX.Element { style={{ opacity: isDragging ? 0.3 : 1 }} data-handler-id={handlerId} > - + ) } diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx index 01abd0eb74e..d6f5b532e79 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/Timeline/StepContainer.tsx @@ -1,15 +1,14 @@ +import * as React from 'react' import { createPortal } from 'react-dom' -import { useEffect, useState, useRef } from 'react' import { useDispatch, useSelector } from 'react-redux' import { ALIGN_CENTER, BORDERS, Box, + Btn, COLORS, CURSOR_DEFAULT, CURSOR_POINTER, - DIRECTION_COLUMN, - Divider, Flex, Icon, JUSTIFY_SPACE_BETWEEN, @@ -56,7 +55,6 @@ export interface StepContainerProps { hovered?: boolean hasError?: boolean isStepAfterError?: boolean - dragHovered?: boolean } export function StepContainer(props: StepContainerProps): JSX.Element { @@ -73,11 +71,10 @@ export function StepContainer(props: StepContainerProps): JSX.Element { title, hasError = false, isStepAfterError = false, - dragHovered = false, } = props - const [top, setTop] = useState(0) - const menuRootRef = useRef(null) - const [stepOverflowMenu, setStepOverflowMenu] = useState(false) + const [top, setTop] = React.useState(0) + const menuRootRef = React.useRef(null) + const [stepOverflowMenu, setStepOverflowMenu] = React.useState(false) const isStartingOrEndingState = title === STARTING_DECK_STATE || title === FINAL_DECK_STATE const dispatch = useDispatch>() @@ -124,7 +121,7 @@ export function StepContainer(props: StepContainerProps): JSX.Element { setTop(top) } - useEffect(() => { + React.useEffect(() => { global.addEventListener('click', handleClick) return () => { global.removeEventListener('click', handleClick) @@ -191,17 +188,14 @@ export function StepContainer(props: StepContainerProps): JSX.Element { onCancelClick={cancelMultiDelete} /> )} - - ) : null} - - {dragHovered ? ( - - ) : null} -
+ + {stepOverflowMenu && stepId != null ? createPortal( { render(props) screen.getByText('Final deck state') }) - it('renders the divider if hover targets that step', () => { - render({ ...props, dragHovered: true }) - screen.getByTestId('divider') - }) }) diff --git a/protocol-designer/src/pages/Designer/__tests__/utils.test.ts b/protocol-designer/src/pages/Designer/__tests__/utils.test.ts index 6be91d71b2a..b1faa3f0d67 100644 --- a/protocol-designer/src/pages/Designer/__tests__/utils.test.ts +++ b/protocol-designer/src/pages/Designer/__tests__/utils.test.ts @@ -117,7 +117,6 @@ describe('getSlotInformation', () => { expect( getSlotInformation({ deckSetup: mockOt2DeckSetup, slot: '1' }) ).toEqual({ - matchingLabwareFor4thColumn: null, createdModuleForSlot: mockHS, createdLabwareForSlot: mockLabOnDeck1, createdNestedLabwareForSlot: mockLabOnDeck2, @@ -129,7 +128,6 @@ describe('getSlotInformation', () => { expect( getSlotInformation({ deckSetup: mockOt2DeckSetup, slot: '2' }) ).toEqual({ - matchingLabwareFor4thColumn: null, createdLabwareForSlot: mockLabOnDeck3, createFixtureForSlots: [], slotPosition: null, @@ -144,17 +142,12 @@ describe('getSlotInformation', () => { } expect( getSlotInformation({ deckSetup: mockDeckSetup, slot: 'A1' }) - ).toEqual({ - matchingLabwareFor4thColumn: null, - slotPosition: null, - createFixtureForSlots: [], - }) + ).toEqual({ slotPosition: null, createFixtureForSlots: [] }) }) it('renders a trashbin for a Flex on slot A3', () => { expect( getSlotInformation({ deckSetup: mockFlex2DeckSetup, slot: 'A3' }) ).toEqual({ - matchingLabwareFor4thColumn: null, slotPosition: null, createFixtureForSlots: [mockTrash], preSelectedFixture: 'trashBin', @@ -164,7 +157,6 @@ describe('getSlotInformation', () => { expect( getSlotInformation({ deckSetup: mockFlex2DeckSetup, slot: 'D1' }) ).toEqual({ - matchingLabwareFor4thColumn: null, slotPosition: null, createdModuleForSlot: mockHSFlex, createdLabwareForSlot: mockLabOnDeck1, @@ -176,7 +168,6 @@ describe('getSlotInformation', () => { expect( getSlotInformation({ deckSetup: mockFlex2DeckSetup, slot: 'D3' }) ).toEqual({ - matchingLabwareFor4thColumn: mockLabOnStagingArea, slotPosition: null, createFixtureForSlots: [mockWasteChute, mockStagingArea], preSelectedFixture: 'wasteChuteAndStagingArea', @@ -186,7 +177,6 @@ describe('getSlotInformation', () => { expect( getSlotInformation({ deckSetup: mockFlex2DeckSetup, slot: 'D4' }) ).toEqual({ - matchingLabwareFor4thColumn: null, slotPosition: null, createdLabwareForSlot: mockLabOnStagingArea, createFixtureForSlots: [mockWasteChute, mockStagingArea], diff --git a/protocol-designer/src/pages/Designer/utils.ts b/protocol-designer/src/pages/Designer/utils.ts index 3110f9d519d..c940e12c8d5 100644 --- a/protocol-designer/src/pages/Designer/utils.ts +++ b/protocol-designer/src/pages/Designer/utils.ts @@ -1,14 +1,9 @@ import { getPositionFromSlotId } from '@opentrons/shared-data' -import { getStagingAreaAddressableAreas } from '../../utils' import type { AdditionalEquipmentName, DeckSlot, } from '@opentrons/step-generation' -import type { - CoordinateTuple, - CutoutId, - DeckDefinition, -} from '@opentrons/shared-data' +import type { CoordinateTuple, DeckDefinition } from '@opentrons/shared-data' import type { AllTemporalPropertiesForTimelineFrame, LabwareOnDeck, @@ -23,7 +18,6 @@ interface AdditionalEquipment { } interface SlotInformation { - matchingLabwareFor4thColumn: LabwareOnDeck | null slotPosition: CoordinateTuple | null createdModuleForSlot?: ModuleOnDeck createdLabwareForSlot?: LabwareOnDeck @@ -72,24 +66,6 @@ export const getSlotInformation = ( } ) - const fixturesOnSlot = Object.values(additionalEquipmentOnDeck).filter( - ae => ae.location?.split('cutout')[1] === slot - ) - const stagingAreaCutout = fixturesOnSlot.find( - fixture => fixture.name === 'stagingArea' - )?.location - - let matchingLabware: LabwareOnDeck | null = null - if (stagingAreaCutout != null) { - const stagingAreaAddressableAreaName = getStagingAreaAddressableAreas([ - stagingAreaCutout, - ] as CutoutId[]) - matchingLabware = - Object.values(deckSetupLabware).find( - lw => lw.slot === stagingAreaAddressableAreaName[0] - ) ?? null - } - const preSelectedFixture = createFixtureForSlots != null && createFixtureForSlots.length === 2 ? ('wasteChuteAndStagingArea' as Fixture) @@ -102,6 +78,5 @@ export const getSlotInformation = ( createFixtureForSlots, preSelectedFixture, slotPosition: slotPosition, - matchingLabwareFor4thColumn: matchingLabware, } } diff --git a/protocol-designer/src/pages/ProtocolOverview/DeckThumbnail.tsx b/protocol-designer/src/pages/ProtocolOverview/DeckThumbnail.tsx index 250feb06df2..6420d45557a 100644 --- a/protocol-designer/src/pages/ProtocolOverview/DeckThumbnail.tsx +++ b/protocol-designer/src/pages/ProtocolOverview/DeckThumbnail.tsx @@ -48,7 +48,6 @@ const OT2_STANDARD_DECK_VIEW_LAYER_BLOCK_LIST: string[] = [ ] const lightFill = COLORS.grey35 -const darkFill = COLORS.grey60 interface DeckThumbnailProps { hoverSlot: DeckSlotId | null @@ -141,7 +140,6 @@ export function DeckThumbnail(props: DeckThumbnailProps): JSX.Element { deckDefinition={deckDef} showExpansion={cutoutId === 'cutoutA1'} fixtureBaseColor={lightFill} - slotClipColor={darkFill} /> ) : null })} @@ -151,7 +149,6 @@ export function DeckThumbnail(props: DeckThumbnailProps): JSX.Element { cutoutId={fixture.location as StagingAreaLocation} deckDefinition={deckDef} fixtureBaseColor={lightFill} - slotClipColor={darkFill} /> ))} {trash != null @@ -188,7 +185,6 @@ export function DeckThumbnail(props: DeckThumbnailProps): JSX.Element { cutoutId={fixture.location as typeof WASTE_CHUTE_CUTOUT} deckDefinition={deckDef} fixtureBaseColor={lightFill} - slotClipColor={darkFill} /> ))} diff --git a/protocol-designer/src/pages/ProtocolOverview/InstrumentsInfo.tsx b/protocol-designer/src/pages/ProtocolOverview/InstrumentsInfo.tsx index 63ce567a805..b6ca5356d96 100644 --- a/protocol-designer/src/pages/ProtocolOverview/InstrumentsInfo.tsx +++ b/protocol-designer/src/pages/ProtocolOverview/InstrumentsInfo.tsx @@ -40,22 +40,17 @@ export function InstrumentsInfo({ equipment => equipment?.name === 'gripper' ) - const has96Channel = leftPipette?.name === 'p1000_96' && rightPipette == null - - const pipetteInfo = (pipette?: PipetteOnDeck): JSX.Element => { + const pipetteInfo = (pipette?: PipetteOnDeck): JSX.Element | string => { const pipetteName = pipette != null ? getPipetteSpecsV2(pipette.name as PipetteName)?.displayName : t('na') - const tipsInfo = - pipette?.tiprackLabwareDef != null - ? pipette.tiprackLabwareDef.map(labware => labware.metadata.displayName) - : t('na') + const tipsInfo = pipette?.tiprackLabwareDef + ? pipette.tiprackLabwareDef.map(labware => labware.metadata.displayName) + : t('na') if (pipetteName === t('na') || tipsInfo === t('na')) { - return ( - {t('na')} - ) + return t('na') } return ( @@ -101,6 +96,7 @@ export function InstrumentsInfo({ type="large" description={ + {' '} - {has96Channel ? t('left_right_mount') : t('left_mount')} + {t('left_pip')} } - content={pipetteInfo(leftPipette)} + content={ + + {pipetteInfo(leftPipette)} + + } + /> +
+ + + + {t('right_pip')} + +
+ } + content={ + + {pipetteInfo(rightPipette)} + + } /> - {!has96Channel ? ( - - - - {t('right_mount')} - -
- } - content={pipetteInfo(rightPipette)} - /> - - ) : null} {robotType === FLEX_ROBOT_TYPE ? ( { screen.getByText('Instruments') screen.getByText('Robot type') screen.getAllByText('Opentrons Flex') - screen.getByText('Left Mount') - screen.getByText('Right Mount') - screen.getByText('Extension Mount') + screen.getByText('Left pipette') + screen.getByText('Right pipette') + screen.getByText('Extension mount') expect(screen.getAllByText('N/A').length).toBe(3) }) @@ -122,13 +103,4 @@ describe('InstrumentsInfo', () => { fireEvent.click(screen.getByText('Edit')) expect(mockSetShowEditInstrumentsModal).toHaveBeenCalled() }) - - it('should render left + right mount when 96 channels is selected', () => { - props = { - ...props, - pipettesOnDeck: mock96Pipette, - } - render(props) - screen.getByText('Left + Right Mount') - }) }) diff --git a/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolMetadata.test.tsx b/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolMetadata.test.tsx index 8df6988b1c4..405bd946279 100644 --- a/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolMetadata.test.tsx +++ b/protocol-designer/src/pages/ProtocolOverview/__tests__/ProtocolMetadata.test.tsx @@ -45,7 +45,7 @@ describe('ProtocolMetadata', () => { screen.getByText('Protocol Metadata') screen.getByText('Edit') screen.getByText('Required app version') - screen.getByText('8.2.0 or higher') + screen.getByText('8.0.0 or higher') }) it('should render protocol metadata', () => { diff --git a/protocol-designer/src/pages/ProtocolOverview/index.tsx b/protocol-designer/src/pages/ProtocolOverview/index.tsx index 1bea85833a2..f622ef6e08f 100644 --- a/protocol-designer/src/pages/ProtocolOverview/index.tsx +++ b/protocol-designer/src/pages/ProtocolOverview/index.tsx @@ -117,8 +117,8 @@ export function ProtocolOverview(): JSX.Element { useEffect(() => { if (formValues?.created == null) { - console.log( - 'formValues was possibly refreshed while on the overview page, redirecting to landing page' + console.warn( + 'formValues was refreshed while on the overview page, redirecting to landing page' ) navigate('/') } diff --git a/protocol-designer/src/step-forms/selectors/index.ts b/protocol-designer/src/step-forms/selectors/index.ts index 75c47774fca..70b4ff44ce2 100644 --- a/protocol-designer/src/step-forms/selectors/index.ts +++ b/protocol-designer/src/step-forms/selectors/index.ts @@ -651,20 +651,13 @@ export const getHydratedUnsavedForm: Selector< export const getDynamicFieldFormErrorsForUnsavedForm: Selector< BaseState, ProfileFormError[] -> = createSelector( - getHydratedUnsavedForm, - getInvariantContext, - (hydratedForm, invariantContext) => { - if (!hydratedForm) return [] +> = createSelector(getHydratedUnsavedForm, hydratedForm => { + if (!hydratedForm) return [] - const errors = [ - ..._dynamicFieldFormErrors(hydratedForm), - ..._dynamicMoveLabwareFieldFormErrors(hydratedForm, invariantContext), - ] + const errors = _dynamicFieldFormErrors(hydratedForm) - return errors - } -) + return errors +}) export const getFormLevelErrorsForUnsavedForm: Selector< BaseState, StepFormErrors diff --git a/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts b/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts index 9a78b49d0ed..c308f594d66 100644 --- a/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts +++ b/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts @@ -124,7 +124,10 @@ describe('createPresavedStepForm', () => { stepType: 'pause', moduleId: hasTempModule ? 'someTemperatureModuleId' : null, pauseAction: null, + pauseHour: null, pauseMessage: '', + pauseMinute: null, + pauseSecond: null, pauseTime: null, pauseTemperature: null, stepDetails: '', @@ -392,7 +395,7 @@ describe('createPresavedStepForm', () => { stepDetails: '', stepName: 'thermocycler', stepType: 'thermocycler', - thermocyclerFormType: 'thermocyclerState', + thermocyclerFormType: null, }) }) }) diff --git a/protocol-designer/src/steplist/fieldLevel/index.ts b/protocol-designer/src/steplist/fieldLevel/index.ts index 4d240fdd649..5440cda40e7 100644 --- a/protocol-designer/src/steplist/fieldLevel/index.ts +++ b/protocol-designer/src/steplist/fieldLevel/index.ts @@ -307,6 +307,9 @@ const stepFieldHelperMap: Record = { maskValue: composeMaskers(maskToFloat, trimDecimals(1)), castValue: Number, }, + setTemperature: { + getErrors: composeErrors(requiredField), + }, targetTemperature: { getErrors: composeErrors( minFieldValue(MIN_TEMP_MODULE_TEMP), diff --git a/protocol-designer/src/steplist/formLevel/errors.ts b/protocol-designer/src/steplist/formLevel/errors.ts index add7662903a..95a2fdd2a3d 100644 --- a/protocol-designer/src/steplist/formLevel/errors.ts +++ b/protocol-designer/src/steplist/formLevel/errors.ts @@ -55,8 +55,6 @@ export interface FormError { dependentFields: StepFieldName[] showAtField?: boolean showAtForm?: boolean - page?: number - tab?: 'aspirate' | 'dispense' } const INCOMPATIBLE_ASPIRATE_LABWARE: FormError = { title: 'Selected aspirate labware is incompatible with pipette', @@ -102,10 +100,8 @@ const MAGNET_ACTION_TYPE_REQUIRED: FormError = { dependentFields: ['magnetAction'], } const ENGAGE_HEIGHT_REQUIRED: FormError = { - title: 'Engage height required', + title: 'Engage height is required', dependentFields: ['magnetAction', 'engageHeight'], - showAtForm: false, - showAtField: true, } const ENGAGE_HEIGHT_MIN_EXCEEDED: FormError = { title: 'Specified distance is below module minimum', @@ -121,52 +117,46 @@ const MODULE_ID_REQUIRED: FormError = { dependentFields: ['moduleId'], } const TARGET_TEMPERATURE_REQUIRED: FormError = { - title: 'Temperature required', + title: 'Temperature is required', dependentFields: ['setTemperature', 'targetTemperature'], showAtForm: false, showAtField: true, } const PROFILE_VOLUME_REQUIRED: FormError = { - title: 'Well volume required', + title: 'Volume is required', dependentFields: ['thermocyclerFormType', 'profileVolume'], showAtForm: false, showAtField: true, - page: 1, } const PROFILE_LID_TEMPERATURE_REQUIRED: FormError = { - title: 'Temperature required', + title: 'Temperature is required', dependentFields: ['thermocyclerFormType', 'profileTargetLidTemp'], showAtForm: false, showAtField: true, - page: 1, } const LID_TEMPERATURE_REQUIRED: FormError = { - title: 'Temperature required', + title: 'Temperature is required', dependentFields: ['lidIsActive', 'lidTargetTemp'], showAtForm: false, showAtField: true, - page: 1, } const BLOCK_TEMPERATURE_REQUIRED: FormError = { - title: 'Temperature required', + title: 'Temperature is required', dependentFields: ['blockIsActive', 'blockTargetTemp'], showAtForm: false, showAtField: true, - page: 1, } const BLOCK_TEMPERATURE_HOLD_REQUIRED: FormError = { - title: 'Temperature required', + title: 'Temperature is required', dependentFields: ['blockIsActiveHold', 'blockTargetTempHold'], showAtForm: false, showAtField: true, - page: 1, } const LID_TEMPERATURE_HOLD_REQUIRED: FormError = { - title: 'Temperature required', + title: 'Temperature is required', dependentFields: ['lidIsActiveHold', 'lidTargetTempHold'], showAtForm: false, showAtField: true, - page: 1, } const SHAKE_SPEED_REQUIRED: FormError = { title: 'Speed required', @@ -180,18 +170,6 @@ const SHAKE_TIME_REQUIRED: FormError = { showAtForm: false, showAtField: true, } -const PAUSE_ACTION_REQUIRED: FormError = { - title: 'Pause type required', - dependentFields: [], - showAtForm: false, - showAtField: true, -} -const PAUSE_MODULE_REQUIRED: FormError = { - title: 'Select a module', - dependentFields: ['moduleId', 'pauseAction'], - showAtForm: false, - showAtField: true, -} const PAUSE_TEMP_REQUIRED: FormError = { title: 'Pause temperature required', dependentFields: ['pauseTemperature', 'pauseAction'], @@ -214,7 +192,7 @@ const HS_TEMPERATURE_REQUIRED: FormError = { showAtField: true, } const LABWARE_TO_MOVE_REQUIRED: FormError = { - title: 'Labware required', + title: 'Labware to move required', dependentFields: ['labware'], showAtForm: false, showAtField: true, @@ -225,134 +203,6 @@ const NEW_LABWARE_LOCATION_REQUIRED: FormError = { showAtForm: false, showAtField: true, } -const ASPIRATE_WELLS_REQUIRED: FormError = { - title: 'Choose wells', - dependentFields: ['aspirate_wells'], - showAtForm: false, - showAtField: true, - page: 0, -} -const DISPENSE_WELLS_REQUIRED: FormError = { - title: 'Choose wells', - dependentFields: ['dispense_wells'], - showAtForm: false, - showAtField: true, - page: 0, -} -const MIX_WELLS_REQUIRED: FormError = { - title: 'Choose wells', - dependentFields: ['wells'], - showAtForm: false, - showAtField: true, - page: 0, -} -const VOLUME_REQUIRED: FormError = { - title: 'Volume required', - dependentFields: ['volume'], - showAtForm: false, - showAtField: true, - page: 0, -} -const TIMES_REQUIRED: FormError = { - title: 'Repetitions required', - dependentFields: ['times'], - showAtForm: false, - showAtField: true, - page: 0, -} -const ASPIRATE_LABWARE_REQUIRED: FormError = { - title: 'Labware required', - dependentFields: ['aspirate_labware'], - showAtForm: false, - showAtField: true, - page: 0, -} -const DISPENSE_LABWARE_REQUIRED: FormError = { - title: 'Labware required', - dependentFields: ['dispense_labware'], - showAtForm: false, - showAtField: true, - page: 0, -} -const MIX_LABWARE_REQUIRED: FormError = { - title: 'Labware required', - dependentFields: ['labware'], - showAtForm: false, - showAtField: true, - page: 0, -} -const ASPIRATE_MIX_TIMES_REQUIRED: FormError = { - title: 'Repititions required', - dependentFields: ['aspirate_mix_times'], - showAtForm: false, - showAtField: true, - page: 1, - tab: 'aspirate', -} -const ASPIRATE_MIX_VOLUME_REQUIRED: FormError = { - title: 'Volume required', - dependentFields: ['aspirate_mix_checkbox', 'aspirate_mix_volume'], - showAtForm: false, - showAtField: true, - page: 1, - tab: 'aspirate', -} -const ASPIRATE_DELAY_DURATION_REQUIRED: FormError = { - title: 'Duration required', - dependentFields: ['aspirate_delay_checkbox', 'aspirate_delay_seconds'], - showAtForm: false, - showAtField: true, - page: 1, - tab: 'aspirate', -} -const ASPIRATE_AIRGAP_VOLUME_REQUIRED: FormError = { - title: 'Volume required', - dependentFields: ['aspirate_airGap_checkbox', 'aspirate_airGap_volume'], - showAtForm: false, - showAtField: true, - page: 1, - tab: 'aspirate', -} -const DISPENSE_MIX_TIMES_REQUIRED: FormError = { - title: 'Repititions required', - dependentFields: ['dispense_mix_checkbox', 'dispense_mix_times'], - showAtForm: false, - showAtField: true, - page: 1, - tab: 'dispense', -} -const DISPENSE_MIX_VOLUME_REQUIRED: FormError = { - title: 'Volume required', - dependentFields: ['dispense_mix_checkbox', 'dispense_mix_volume'], - showAtForm: false, - showAtField: true, - page: 1, - tab: 'dispense', -} -const DISPENSE_DELAY_DURATION_REQUIRED: FormError = { - title: 'Duration required', - dependentFields: ['dispense_delay_checkbox', 'dispense_delay_seconds'], - showAtForm: false, - showAtField: true, - page: 1, - tab: 'dispense', -} -const DISPENSE_AIRGAP_VOLUME_REQUIRED: FormError = { - title: 'Volume required', - dependentFields: ['dispense_airGap_checkbox', 'dispense_airGap_volume'], - showAtForm: false, - showAtField: true, - page: 1, - tab: 'dispense', -} -const BLOWOUT_LOCATION_REQUIRED: FormError = { - title: 'Volume required', - dependentFields: ['blowout_checkbox', 'blowout_location'], - showAtForm: false, - showAtField: true, - page: 1, - tab: 'dispense', -} export interface HydratedFormData { [key: string]: any @@ -415,7 +265,13 @@ export const pauseForTimeOrUntilTold = ( const { pauseAction, moduleId, pauseTemperature } = fields if (pauseAction === PAUSE_UNTIL_TIME) { - const { hours, minutes, seconds } = getTimeFromForm(fields, 'pauseTime') + const { hours, minutes, seconds } = getTimeFromForm( + fields, + 'pauseTime', + 'pauseSeconds', + 'pauseMinutes', + 'pauseSeconds' + ) // user selected pause for amount of time const totalSeconds = hours * 3600 + minutes * 60 + seconds return totalSeconds <= 0 ? TIME_PARAM_REQUIRED : null @@ -507,7 +363,7 @@ export const targetTemperatureRequired = ( fields: HydratedFormData ): FormError | null => { const { setTemperature, targetTemperature } = fields - return setTemperature && !targetTemperature + return setTemperature === 'true' && !targetTemperature ? TARGET_TEMPERATURE_REQUIRED : null } @@ -579,12 +435,6 @@ export const temperatureRequired = ( ? HS_TEMPERATURE_REQUIRED : null } -export const pauseActionRequired = ( - fields: HydratedFormData -): FormError | null => { - const { pauseAction } = fields - return pauseAction == null ? PAUSE_ACTION_REQUIRED : null -} export const pauseTimeRequired = ( fields: HydratedFormData ): FormError | null => { @@ -593,14 +443,6 @@ export const pauseTimeRequired = ( ? PAUSE_TIME_REQUIRED : null } -export const pauseModuleRequired = ( - fields: HydratedFormData -): FormError | null => { - const { moduleId, pauseAction } = fields - return pauseAction === PAUSE_UNTIL_TEMP && moduleId == null - ? PAUSE_MODULE_REQUIRED - : null -} export const pauseTemperatureRequired = ( fields: HydratedFormData ): FormError | null => { @@ -619,10 +461,8 @@ export const newLabwareLocationRequired = ( fields: HydratedFormData ): FormError | null => { const { newLocation } = fields - return newLocation == null || - Object.values(newLocation as Object).every(val => val == null) - ? NEW_LABWARE_LOCATION_REQUIRED - : null + console.log(fields) + return newLocation == null ? NEW_LABWARE_LOCATION_REQUIRED : null } export const engageHeightRangeExceeded = ( fields: HydratedFormData @@ -651,131 +491,6 @@ export const engageHeightRangeExceeded = ( return null } -export const aspirateWellsRequired = ( - fields: HydratedFormData -): FormError | null => { - const { aspirate_wells } = fields - return aspirate_wells == null || aspirate_wells.length === 0 - ? ASPIRATE_WELLS_REQUIRED - : null -} -export const dispenseWellsRequired = ( - fields: HydratedFormData -): FormError | null => { - const { dispense_wells, dispense_labware } = fields - return (dispense_wells == null || dispense_wells.length === 0) && - !( - dispense_labware != null && - (dispense_labware.name === 'wasteChute' || - dispense_labware.name === 'trashBin') - ) - ? DISPENSE_WELLS_REQUIRED - : null -} -export const mixWellsRequired = ( - fields: HydratedFormData -): FormError | null => { - const { wells } = fields - return wells == null || wells.length === 0 ? MIX_WELLS_REQUIRED : null -} -export const volumeRequired = (fields: HydratedFormData): FormError | null => { - const { volume } = fields - return !volume ? VOLUME_REQUIRED : null -} -export const timesRequired = (fields: HydratedFormData): FormError | null => { - const { times } = fields - return !times ? TIMES_REQUIRED : null -} -export const aspirateLabwareRequired = ( - fields: HydratedFormData -): FormError | null => { - const { aspirate_labware } = fields - return aspirate_labware == null ? ASPIRATE_LABWARE_REQUIRED : null -} -export const dispenseLabwareRequired = ( - fields: HydratedFormData -): FormError | null => { - const { dispense_labware } = fields - return dispense_labware == null ? DISPENSE_LABWARE_REQUIRED : null -} -export const mixLabwareRequired = ( - fields: HydratedFormData -): FormError | null => { - const { labware } = fields - return labware == null ? MIX_LABWARE_REQUIRED : null -} -export const aspirateMixTimesRequired = ( - fields: HydratedFormData -): FormError | null => { - const { aspirate_mix_checkbox, aspirate_mix_times } = fields - return aspirate_mix_checkbox && !aspirate_mix_times - ? ASPIRATE_MIX_TIMES_REQUIRED - : null -} -export const aspirateMixVolumeRequired = ( - fields: HydratedFormData -): FormError | null => { - const { aspirate_mix_checkbox, aspirate_mix_volume } = fields - return aspirate_mix_checkbox && !aspirate_mix_volume - ? ASPIRATE_MIX_VOLUME_REQUIRED - : null -} -export const aspirateDelayDurationRequired = ( - fields: HydratedFormData -): FormError | null => { - const { aspirate_delay_seconds, aspirate_delay_checkbox } = fields - return aspirate_delay_checkbox && !aspirate_delay_seconds - ? ASPIRATE_DELAY_DURATION_REQUIRED - : null -} -export const aspirateAirGapVolumeRequired = ( - fields: HydratedFormData -): FormError | null => { - const { aspirate_airGap_checkbox, aspirate_airGap_volume } = fields - return aspirate_airGap_checkbox && !aspirate_airGap_volume - ? ASPIRATE_AIRGAP_VOLUME_REQUIRED - : null -} -export const dispenseMixTimesRequired = ( - fields: HydratedFormData -): FormError | null => { - const { dispense_mix_checkbox, dispense_mix_times } = fields - return dispense_mix_checkbox && !dispense_mix_times - ? DISPENSE_MIX_TIMES_REQUIRED - : null -} -export const dispenseMixVolumeRequired = ( - fields: HydratedFormData -): FormError | null => { - const { dispense_mix_checkbox, dispense_mix_volume } = fields - return dispense_mix_checkbox && !dispense_mix_volume - ? DISPENSE_MIX_VOLUME_REQUIRED - : null -} -export const dispenseDelayDurationRequired = ( - fields: HydratedFormData -): FormError | null => { - const { dispense_delay_seconds, dispense_delay_checkbox } = fields - return dispense_delay_checkbox && !dispense_delay_seconds - ? DISPENSE_DELAY_DURATION_REQUIRED - : null -} -export const dispenseAirGapVolumeRequired = ( - fields: HydratedFormData -): FormError | null => { - const { dispense_airGap_checkbox, dispense_airGap_volume } = fields - return dispense_airGap_checkbox && !dispense_airGap_volume - ? DISPENSE_AIRGAP_VOLUME_REQUIRED - : null -} -export const blowoutLocationRequired = ( - fields: HydratedFormData -): FormError | null => { - const { blowout_checkbox, blowout_location } = fields - return blowout_checkbox && !blowout_location - ? BLOWOUT_LOCATION_REQUIRED - : null -} /******************* ** Helpers ** diff --git a/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts b/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts index 3fa3f71be12..40b35b3ccad 100644 --- a/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts +++ b/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts @@ -120,7 +120,11 @@ export function getDefaultsForStepType( return { moduleId: null, pauseAction: null, + // TODO: (nd: 10/23/2024) remove individual time unit fields + pauseHour: null, pauseMessage: '', + pauseMinute: null, + pauseSecond: null, pauseTemperature: null, pauseTime: null, } @@ -148,6 +152,9 @@ export function getDefaultsForStepType( case 'heaterShaker': return { heaterShakerSetTimer: null, + // TODO: (nd: 10/23/2024) remove individual time unit fields + heaterShakerTimerMinutes: null, + heaterShakerTimerSeconds: null, heaterShakerTimer: null, latchOpen: false, moduleId: null, @@ -173,7 +180,7 @@ export function getDefaultsForStepType( profileItemsById: {}, profileTargetLidTemp: null, profileVolume: null, - thermocyclerFormType: 'thermocyclerState', + thermocyclerFormType: null, } default: diff --git a/protocol-designer/src/steplist/formLevel/index.ts b/protocol-designer/src/steplist/formLevel/index.ts index 937d8edee64..dd7b9fea36c 100644 --- a/protocol-designer/src/steplist/formLevel/index.ts +++ b/protocol-designer/src/steplist/formLevel/index.ts @@ -21,27 +21,6 @@ import { shakeTimeRequired, pauseTimeRequired, pauseTemperatureRequired, - newLabwareLocationRequired, - labwareToMoveRequired, - pauseModuleRequired, - aspirateLabwareRequired, - dispenseLabwareRequired, - aspirateMixVolumeRequired, - aspirateMixTimesRequired, - aspirateDelayDurationRequired, - aspirateAirGapVolumeRequired, - dispenseMixTimesRequired, - dispenseDelayDurationRequired, - dispenseAirGapVolumeRequired, - dispenseMixVolumeRequired, - blowoutLocationRequired, - aspirateWellsRequired, - dispenseWellsRequired, - mixWellsRequired, - mixLabwareRequired, - volumeRequired, - timesRequired, - pauseActionRequired, } from './errors' import { @@ -84,52 +63,20 @@ const stepFormHelperMap: Partial> = { ), }, mix: { - getErrors: composeErrors( - incompatibleLabware, - volumeTooHigh, - mixWellsRequired, - mixLabwareRequired, - volumeRequired, - timesRequired, - aspirateDelayDurationRequired, - dispenseDelayDurationRequired, - blowoutLocationRequired - ), + getErrors: composeErrors(incompatibleLabware, volumeTooHigh), getWarnings: composeWarnings( belowPipetteMinimumVolume, mixTipPositionInTube ), }, pause: { - getErrors: composeErrors( - pauseActionRequired, - pauseTimeRequired, - pauseTemperatureRequired, - pauseModuleRequired - ), - }, - moveLabware: { - getErrors: composeErrors(labwareToMoveRequired, newLabwareLocationRequired), + getErrors: composeErrors(pauseTimeRequired, pauseTemperatureRequired), }, moveLiquid: { getErrors: composeErrors( incompatibleAspirateLabware, incompatibleDispenseLabware, - wellRatioMoveLiquid, - volumeRequired, - aspirateLabwareRequired, - dispenseLabwareRequired, - aspirateMixTimesRequired, - aspirateMixVolumeRequired, - aspirateDelayDurationRequired, - aspirateAirGapVolumeRequired, - dispenseMixTimesRequired, - dispenseMixVolumeRequired, - dispenseDelayDurationRequired, - dispenseAirGapVolumeRequired, - blowoutLocationRequired, - aspirateWellsRequired, - dispenseWellsRequired + wellRatioMoveLiquid ), getWarnings: composeWarnings( belowPipetteMinimumVolume, diff --git a/protocol-designer/src/steplist/formLevel/moveLabwareFormErrors.ts b/protocol-designer/src/steplist/formLevel/moveLabwareFormErrors.ts index 3362a0a2bff..b9ee871772d 100644 --- a/protocol-designer/src/steplist/formLevel/moveLabwareFormErrors.ts +++ b/protocol-designer/src/steplist/formLevel/moveLabwareFormErrors.ts @@ -32,7 +32,7 @@ const getMoveLabwareError = ( invariantContext.moduleEntities[newLocation.moduleId].type const modAllowList = COMPATIBLE_LABWARE_ALLOWLIST_BY_MODULE_TYPE[moduleType] errorString = !modAllowList.includes(loadName) - ? 'Labware incompatible with this module' + ? 'labware incompatible with this module' : null } else if ('labwareId' in newLocation) { const adapterValueDefUri = @@ -41,7 +41,7 @@ const getMoveLabwareError = ( const adapterAllowList = COMPATIBLE_LABWARE_ALLOWLIST_FOR_ADAPTER[adapterValueDefUri] errorString = !adapterAllowList?.includes(selectedLabwareDefUri) - ? 'Labware incompatible with this adapter' + ? 'labware incompatible with this adapter' : null } return errorString @@ -68,7 +68,7 @@ export const getMoveLabwareFormErrors = ( ? ([ { title: errorString, - dependentProfileFields: ['newLocation'], + dependentProfileFields: [], }, ] as ProfileFormError[]) : [] diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/heaterShakerFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/heaterShakerFormToArgs.ts index b232389708f..eda1e073fb2 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/heaterShakerFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/heaterShakerFormToArgs.ts @@ -23,7 +23,12 @@ export const heaterShakerFormToArgs = ( setShake ? !Number.isNaN(targetSpeed) : true, 'heaterShakerFormToArgs expected targeShake to be a number when setShake is true' ) - const { minutes, seconds } = getTimeFromForm(formData, 'heaterShakerTimer') + const { minutes, seconds } = getTimeFromForm( + formData, + 'heaterShakerTimer', + 'heaterShakerTimerSeconds', + 'heaterShakerTimerMinutes' + ) const isNullTime = minutes === 0 && seconds === 0 diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/pauseFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/pauseFormToArgs.ts index 1621e8d0aa2..88de52bacec 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/pauseFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/pauseFormToArgs.ts @@ -13,7 +13,13 @@ import type { export const pauseFormToArgs = ( formData: FormData ): PauseArgs | WaitForTemperatureArgs | null => { - const { hours, minutes, seconds } = getTimeFromForm(formData, 'pauseTime') + const { hours, minutes, seconds } = getTimeFromForm( + formData, + 'pauseTime', + 'pauseSecond', + 'pauseMinute', + 'pauseHour' + ) const totalSeconds = (hours ?? 0) * 3600 + minutes * 60 + seconds const temperature = parseFloat(formData.pauseTemperature as string) const message = formData.pauseMessage ?? '' diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/heaterShakerFormToArgs.test.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/heaterShakerFormToArgs.test.ts index 4329e6ca4f6..65a0b1f27ad 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/heaterShakerFormToArgs.test.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/heaterShakerFormToArgs.test.ts @@ -15,7 +15,8 @@ describe('heaterShakerFormToArgs', () => { latchOpen: false, targetHeaterShakerTemperature: '40', targetSpeed: '400', - heaterShakerTimer: '1:10', + heaterShakerTimerMinutes: '1', + heaterShakerTimerSeconds: '10', } const expected = { @@ -41,7 +42,8 @@ describe('heaterShakerFormToArgs', () => { latchOpen: false, targetHeaterShakerTemperature: '40', targetSpeed: null, - heaterShakerTimer: null, + heaterShakerTimerMinutes: null, + heaterShakerTimerSeconds: null, } const expected = { diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/pauseFormToArgs.test.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/pauseFormToArgs.test.ts index ab466604a82..83a3b51d555 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/pauseFormToArgs.test.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/test/pauseFormToArgs.test.ts @@ -16,7 +16,6 @@ describe('pauseFormToArgs', () => { pauseTemperature: '20', pauseMessage: 'pause message', moduleId: 'some_id', - pauseTime: null, } const expected = { commandCreatorFnName: 'waitForTemperature', @@ -56,7 +55,10 @@ describe('pauseFormToArgs', () => { pauseAction: PAUSE_UNTIL_TIME, description: 'some description', pauseMessage: 'some message', - pauseTime: '1:20:5', + pauseHour: 1, + pauseMinute: 20, + pauseSecond: 5, + pauseTime: null, } const expected = { commandCreatorFnName: 'delay', diff --git a/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts b/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts index a7d87f09c0d..ba0897607ac 100644 --- a/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts +++ b/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts @@ -115,6 +115,9 @@ describe('getDefaultsForStepType', () => { it('should get the correct defaults', () => { expect(getDefaultsForStepType('pause')).toEqual({ pauseAction: null, + pauseHour: null, + pauseMinute: null, + pauseSecond: null, pauseTime: null, pauseMessage: '', moduleId: null, @@ -159,6 +162,8 @@ describe('getDefaultsForStepType', () => { targetSpeed: null, latchOpen: false, heaterShakerSetTimer: null, + heaterShakerTimerMinutes: null, + heaterShakerTimerSeconds: null, heaterShakerTimer: null, }) }) @@ -166,7 +171,7 @@ describe('getDefaultsForStepType', () => { describe('thermocycler step', () => { it('should get the correct defaults', () => { expect(getDefaultsForStepType('thermocycler')).toEqual({ - thermocyclerFormType: 'thermocyclerState', + thermocyclerFormType: null, moduleId: null, blockIsActive: false, blockTargetTemp: null, diff --git a/protocol-designer/src/steplist/formLevel/warnings.tsx b/protocol-designer/src/steplist/formLevel/warnings.tsx index d40bf9b20af..ef9e5a62603 100644 --- a/protocol-designer/src/steplist/formLevel/warnings.tsx +++ b/protocol-designer/src/steplist/formLevel/warnings.tsx @@ -1,4 +1,5 @@ import { getWellTotalVolume } from '@opentrons/shared-data' +import { KnowledgeBaseLink } from '../../components/KnowledgeBaseLink' import type { FormError } from './errors' import type { LabwareDefinition2 } from '@opentrons/shared-data' @@ -28,8 +29,13 @@ const belowMinAirGapVolumeWarning = (min: number): FormWarning => ({ const belowPipetteMinVolumeWarning = (min: number): FormWarning => ({ type: 'BELOW_PIPETTE_MINIMUM_VOLUME', title: `Disposal volume is below recommended minimum (${min} uL)`, - body: - 'For accuracy in multi-dispense Transfers we recommend you use a disposal volume of at least the pipette`s minimum.', + body: ( + <> + { + 'For accuracy in multi-dispense Transfers we recommend you use a disposal volume of at least the pipette`s minimum. Read more ' + } + + ), dependentFields: ['pipette', 'volume'], }) @@ -42,8 +48,14 @@ const overMaxWellVolumeWarning = (): FormWarning => ({ const belowMinDisposalVolumeWarning = (min: number): FormWarning => ({ type: 'BELOW_MIN_DISPOSAL_VOLUME', title: `Disposal volume is below recommended minimum (${min} uL)`, - body: - 'For accuracy in multi-dispense Transfers we recommend you use a disposal volume of at least the pipette`s minimum.', + body: ( + <> + { + 'For accuracy in multi-dispense Transfers we recommend you use a disposal volume of at least the pipette`s minimum. Read more ' + } + {'here'}. + + ), dependentFields: ['disposalVolume_volume', 'pipette'], }) diff --git a/protocol-designer/src/steplist/utils/getTimeFromForm.ts b/protocol-designer/src/steplist/utils/getTimeFromForm.ts index 48771039045..ff35bdaeb09 100644 --- a/protocol-designer/src/steplist/utils/getTimeFromForm.ts +++ b/protocol-designer/src/steplist/utils/getTimeFromForm.ts @@ -11,15 +11,28 @@ interface TimeData { export const getTimeFromForm = ( formData: FormData | HydratedFormData, - timeField: string + timeField: string, + secondsField: string, + minutesField: string, + hoursField?: string ): TimeData => { - if (formData[timeField] == null) { - return { hours: 0, minutes: 0, seconds: 0 } - } - const timeSplit = formData[timeField].split(TIME_DELIMITER) - const [hoursFromForm, minutesFromForm, secondsFromForm] = - timeSplit.length === 3 ? timeSplit : [0, ...timeSplit] + let hoursFromForm + let minutesFromForm + let secondsFromForm + // importing results in stringified "null" value + if (formData[timeField] != null && formData[timeField] !== 'null') { + const timeSplit = formData[timeField].split(TIME_DELIMITER) + ;[hoursFromForm, minutesFromForm, secondsFromForm] = + timeSplit.length === 3 ? timeSplit : [0, ...timeSplit] + } else { + // TODO (nd 09/23/2024): remove individual time units after redesign FF is removed + ;[hoursFromForm, minutesFromForm, secondsFromForm] = [ + hoursField != null ? formData[hoursField] : null, + formData[minutesField], + formData[secondsField], + ] + } const hours = isNaN(parseFloat(hoursFromForm as string)) ? 0 : parseFloat(hoursFromForm as string) diff --git a/robot-server/robot_server/health/router.py b/robot-server/robot_server/health/router.py index a4ca84bd2c1..86a78255cd7 100644 --- a/robot-server/robot_server/health/router.py +++ b/robot-server/robot_server/health/router.py @@ -30,7 +30,6 @@ ] FLEX_LOG_PATHS = [ "/logs/serial.log", - "/logs/can_bus.log", "/logs/api.log", "/logs/server.log", "/logs/update_server.log", diff --git a/robot-server/robot_server/service/legacy/models/logs.py b/robot-server/robot_server/service/legacy/models/logs.py index 36ce947e2e3..5a501fdd2ff 100644 --- a/robot-server/robot_server/service/legacy/models/logs.py +++ b/robot-server/robot_server/service/legacy/models/logs.py @@ -6,7 +6,6 @@ class LogIdentifier(str, Enum): api = "api.log" serial = "serial.log" - can = "can_bus.log" server = "server.log" api_server = "combined_api_server.log" update_server = "update_server.log" diff --git a/robot-server/robot_server/service/legacy/routers/logs.py b/robot-server/robot_server/service/legacy/routers/logs.py index b3f57f0d281..69b92d5263c 100644 --- a/robot-server/robot_server/service/legacy/routers/logs.py +++ b/robot-server/robot_server/service/legacy/routers/logs.py @@ -14,7 +14,6 @@ LogIdentifier.api_server: "opentrons-robot-server", LogIdentifier.update_server: "opentrons-update-server", LogIdentifier.touchscreen: "opentrons-robot-app", - LogIdentifier.can: "opentrons-api-serial-can", } diff --git a/robot-server/tests/integration/fixtures.py b/robot-server/tests/integration/fixtures.py index 58362d59652..910eb9256dd 100644 --- a/robot-server/tests/integration/fixtures.py +++ b/robot-server/tests/integration/fixtures.py @@ -50,7 +50,6 @@ def check_ot3_health_response(response: Response) -> None: "board_revision": "UNKNOWN", "logs": [ "/logs/serial.log", - "/logs/can_bus.log", "/logs/api.log", "/logs/server.log", "/logs/update_server.log", diff --git a/shared-data/command/schemas/10.json b/shared-data/command/schemas/10.json index 61521073fac..93bc2387d63 100644 --- a/shared-data/command/schemas/10.json +++ b/shared-data/command/schemas/10.json @@ -4,7 +4,6 @@ "discriminator": { "propertyName": "commandType", "mapping": { - "airGapInPlace": "#/definitions/AirGapInPlaceCreate", "aspirate": "#/definitions/AspirateCreate", "aspirateInPlace": "#/definitions/AspirateInPlaceCreate", "comment": "#/definitions/CommentCreate", @@ -82,9 +81,6 @@ } }, "oneOf": [ - { - "$ref": "#/definitions/AirGapInPlaceCreate" - }, { "$ref": "#/definitions/AspirateCreate" }, @@ -306,67 +302,6 @@ } ], "definitions": { - "AirGapInPlaceParams": { - "title": "AirGapInPlaceParams", - "description": "Payload required to air gap in place.", - "type": "object", - "properties": { - "flowRate": { - "title": "Flowrate", - "description": "Speed in \u00b5L/s configured for the pipette", - "exclusiveMinimum": 0, - "type": "number" - }, - "volume": { - "title": "Volume", - "description": "The amount of liquid to aspirate, in \u00b5L. Must not be greater than the remaining available amount, which depends on the pipette (see `loadPipette`), its configuration (see `configureForVolume`), the tip (see `pickUpTip`), and the amount you've aspirated so far. There is some tolerance for floating point rounding errors.", - "minimum": 0, - "type": "number" - }, - "pipetteId": { - "title": "Pipetteid", - "description": "Identifier of pipette to use for liquid handling.", - "type": "string" - } - }, - "required": ["flowRate", "volume", "pipetteId"] - }, - "CommandIntent": { - "title": "CommandIntent", - "description": "Run intent for a given command.\n\nProps:\n PROTOCOL: the command is part of the protocol run itself.\n SETUP: the command is part of the setup phase of a run.", - "enum": ["protocol", "setup", "fixit"], - "type": "string" - }, - "AirGapInPlaceCreate": { - "title": "AirGapInPlaceCreate", - "description": "AirGapInPlace command request model.", - "type": "object", - "properties": { - "commandType": { - "title": "Commandtype", - "default": "airGapInPlace", - "enum": ["airGapInPlace"], - "type": "string" - }, - "params": { - "$ref": "#/definitions/AirGapInPlaceParams" - }, - "intent": { - "description": "The reason the command was added. If not specified or `protocol`, the command will be treated as part of the protocol run itself, and added to the end of the existing command queue.\n\nIf `setup`, the command will be treated as part of run setup. A setup command may only be enqueued if the run has not started.\n\nUse setup commands for activities like pre-run calibration checks and module setup, like pre-heating.", - "allOf": [ - { - "$ref": "#/definitions/CommandIntent" - } - ] - }, - "key": { - "title": "Key", - "description": "A key value, unique in this run, that can be used to track the same logical command across multiple runs of the same protocol. If a value is not provided, one will be generated.", - "type": "string" - } - }, - "required": ["params"] - }, "WellOrigin": { "title": "WellOrigin", "description": "Origin of WellLocation offset.\n\nProps:\n TOP: the top-center of the well\n BOTTOM: the bottom-center of the well\n CENTER: the middle-center of the well\n MENISCUS: the meniscus-center of the well", @@ -471,6 +406,12 @@ }, "required": ["labwareId", "wellName", "flowRate", "volume", "pipetteId"] }, + "CommandIntent": { + "title": "CommandIntent", + "description": "Run intent for a given command.\n\nProps:\n PROTOCOL: the command is part of the protocol run itself.\n SETUP: the command is part of the setup phase of a run.", + "enum": ["protocol", "setup", "fixit"], + "type": "string" + }, "AspirateCreate": { "title": "AspirateCreate", "description": "Create aspirate command request model.", @@ -1621,16 +1562,8 @@ "properties": { "liquidId": { "title": "Liquidid", - "description": "Unique identifier of the liquid to load. If this is the sentinel value EMPTY, all values of volumeByWell must be 0.", - "anyOf": [ - { - "type": "string" - }, - { - "enum": ["EMPTY"], - "type": "string" - } - ] + "description": "Unique identifier of the liquid to load.", + "type": "string" }, "labwareId": { "title": "Labwareid", @@ -1639,7 +1572,7 @@ }, "volumeByWell": { "title": "Volumebywell", - "description": "Volume of liquid, in \u00b5L, loaded into each well by name, in this labware. If the liquid id is the sentinel value EMPTY, all volumes must be 0.", + "description": "Volume of liquid, in \u00b5L, loaded into each well by name, in this labware.", "type": "object", "additionalProperties": { "type": "number" diff --git a/shared-data/command/types/pipetting.ts b/shared-data/command/types/pipetting.ts index d609af1854b..1ce5ad601cb 100644 --- a/shared-data/command/types/pipetting.ts +++ b/shared-data/command/types/pipetting.ts @@ -20,7 +20,6 @@ export type PipettingRunTimeCommand = | VerifyTipPresenceRunTimeCommand | LiquidProbeRunTimeCommand | TryLiquidProbeRunTimeCommand - | AirGapInPlaceRunTimeCommand export type PipettingCreateCommand = | AspirateCreateCommand @@ -40,7 +39,6 @@ export type PipettingCreateCommand = | VerifyTipPresenceCreateCommand | LiquidProbeCreateCommand | TryLiquidProbeCreateCommand - | AirGapInPlaceCreateCommand export interface ConfigureForVolumeCreateCommand extends CommonCommandCreateInfo { @@ -57,22 +55,6 @@ export interface ConfigureForVolumeRunTimeCommand ConfigureForVolumeCreateCommand { result?: BasicLiquidHandlingResult } - -export type AirGapInPlaceParams = FlowRateParams & - PipetteIdentityParams & - VolumeParams - -export interface AirGapInPlaceCreateCommand extends CommonCommandCreateInfo { - commandType: 'airGapInPlace' - params: AirGapInPlaceParams -} - -export interface AirGapInPlaceRunTimeCommand - extends CommonCommandRunTimeInfo, - AirGapInPlaceCreateCommand { - result?: BasicLiquidHandlingResult -} - export interface AspirateCreateCommand extends CommonCommandCreateInfo { commandType: 'aspirate' params: AspDispAirgapParams diff --git a/shared-data/labware/definitions/3/agilent_1_reservoir_290ml/2.json b/shared-data/labware/definitions/3/agilent_1_reservoir_290ml/2.json index 4a06fc94f97..984af88d3ed 100644 --- a/shared-data/labware/definitions/3/agilent_1_reservoir_290ml/2.json +++ b/shared-data/labware/definitions/3/agilent_1_reservoir_290ml/2.json @@ -54,27 +54,7 @@ }, "innerLabwareGeometry": { "cuboidalWell": { - "sections": [ - { - "shape": "cuboidal", - "topXDimension": 107.25, - "topYDimension": 8, - "bottomXDimension": 101.25, - "bottomYDimension": 1.66, - "topHeight": 2, - "bottomHeight": 8, - "yCount": 8 - }, - { - "shape": "cuboidal", - "topXDimension": 107.5, - "topYDimension": 71.25, - "bottomXDimension": 107.25, - "bottomYDimension": 71.0, - "topHeight": 39.22, - "bottomHeight": 2 - } - ] + "sections": [] } } } diff --git a/shared-data/labware/definitions/3/armadillo_96_wellplate_200ul_pcr_full_skirt/3.json b/shared-data/labware/definitions/3/armadillo_96_wellplate_200ul_pcr_full_skirt/3.json index 71c6f214707..0d682186706 100644 --- a/shared-data/labware/definitions/3/armadillo_96_wellplate_200ul_pcr_full_skirt/3.json +++ b/shared-data/labware/definitions/3/armadillo_96_wellplate_200ul_pcr_full_skirt/3.json @@ -1140,28 +1140,7 @@ ], "innerLabwareGeometry": { "conicalWell": { - "sections": [ - { - "shape": "spherical", - "radiusOfCurvature": 1.25, - "topHeight": 0.8, - "bottomHeight": 0.0 - }, - { - "shape": "conical", - "topDiameter": 5.5, - "bottomDiameter": 2.33, - "topHeight": 11.35, - "bottomHeight": 0.8 - }, - { - "shape": "conical", - "bottomDiameter": 5.5, - "topDiameter": 5.5, - "topHeight": 14.95, - "bottomHeight": 11.35 - } - ] + "sections": [] } } } diff --git a/shared-data/labware/definitions/3/nest_1_reservoir_195ml/3.json b/shared-data/labware/definitions/3/nest_1_reservoir_195ml/3.json index 842b916fb8c..5930984eab5 100644 --- a/shared-data/labware/definitions/3/nest_1_reservoir_195ml/3.json +++ b/shared-data/labware/definitions/3/nest_1_reservoir_195ml/3.json @@ -56,28 +56,7 @@ }, "innerLabwareGeometry": { "cuboidalWell": { - "sections": [ - { - "shape": "cuboidal", - "topXDimension": 9, - "topYDimension": 9, - "bottomXDimension": 1.93, - "bottomYDimension": 1.93, - "topHeight": 2, - "bottomHeight": 0, - "xCount": 12, - "yCount": 8 - }, - { - "shape": "cuboidal", - "topXDimension": 71.3, - "topYDimension": 70.6, - "bottomXDimension": 107.3, - "bottomYDimension": 106.8, - "topHeight": 26.85, - "bottomHeight": 2 - } - ] + "sections": [] } } } diff --git a/shared-data/labware/definitions/3/opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical/2.json b/shared-data/labware/definitions/3/opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical/2.json index 7827cbb5916..c8841946eab 100644 --- a/shared-data/labware/definitions/3/opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical/2.json +++ b/shared-data/labware/definitions/3/opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical/2.json @@ -34,7 +34,7 @@ "x": 13.88, "y": 67.75, "z": 6.85, - "geometryDefinitionId": "15mlconicalWell" + "geometryDefinitionId": "conicalWell" }, "B1": { "totalLiquidVolume": 15000, @@ -44,7 +44,7 @@ "x": 13.88, "y": 42.75, "z": 6.85, - "geometryDefinitionId": "15mlconicalWell" + "geometryDefinitionId": "conicalWell" }, "C1": { "totalLiquidVolume": 15000, @@ -54,7 +54,7 @@ "x": 13.88, "y": 17.75, "z": 6.85, - "geometryDefinitionId": "15mlconicalWell" + "geometryDefinitionId": "conicalWell" }, "A2": { "totalLiquidVolume": 15000, @@ -64,7 +64,7 @@ "x": 38.88, "y": 67.75, "z": 6.85, - "geometryDefinitionId": "15mlconicalWell" + "geometryDefinitionId": "conicalWell" }, "B2": { "totalLiquidVolume": 15000, @@ -74,7 +74,7 @@ "x": 38.88, "y": 42.75, "z": 6.85, - "geometryDefinitionId": "15mlconicalWell" + "geometryDefinitionId": "conicalWell" }, "C2": { "totalLiquidVolume": 15000, @@ -84,7 +84,7 @@ "x": 38.88, "y": 17.75, "z": 6.85, - "geometryDefinitionId": "15mlconicalWell" + "geometryDefinitionId": "conicalWell" }, "A3": { "totalLiquidVolume": 50000, @@ -94,7 +94,7 @@ "x": 71.38, "y": 60.25, "z": 7.3, - "geometryDefinitionId": "50mlconicalWell" + "geometryDefinitionId": "b" }, "B3": { "totalLiquidVolume": 50000, @@ -104,7 +104,7 @@ "x": 71.38, "y": 25.25, "z": 7.3, - "geometryDefinitionId": "50mlconicalWell" + "geometryDefinitionId": "b" }, "A4": { "totalLiquidVolume": 50000, @@ -114,7 +114,7 @@ "x": 106.38, "y": 60.25, "z": 7.3, - "geometryDefinitionId": "50mlconicalWell" + "geometryDefinitionId": "b" }, "B4": { "totalLiquidVolume": 50000, @@ -124,7 +124,7 @@ "x": 106.38, "y": 25.25, "z": 7.3, - "geometryDefinitionId": "50mlconicalWell" + "geometryDefinitionId": "b" } }, "brand": { @@ -172,7 +172,7 @@ "z": 0 }, "innerLabwareGeometry": { - "15mlconicalWell": { + "conicalWell": { "sections": [ { "shape": "spherical", @@ -203,7 +203,7 @@ } ] }, - "50mlconicalWell": { + "b": { "sections": [ { "shape": "conical", diff --git a/shared-data/labware/definitions/3/opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic/2.json b/shared-data/labware/definitions/3/opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic/2.json index 4c7683521d7..4a57b660efe 100644 --- a/shared-data/labware/definitions/3/opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic/2.json +++ b/shared-data/labware/definitions/3/opentrons_10_tuberack_falcon_4x50ml_6x15ml_conical_acrylic/2.json @@ -8,7 +8,7 @@ "x": 19, "y": 74, "z": 5.78, - "geometryDefinitionId": "15mlconicalWell" + "geometryDefinitionId": "conicalWell" }, "B1": { "totalLiquidVolume": 15000, @@ -18,7 +18,7 @@ "x": 19, "y": 42.5, "z": 5.78, - "geometryDefinitionId": "15mlconicalWell" + "geometryDefinitionId": "conicalWell" }, "C1": { "totalLiquidVolume": 15000, @@ -28,7 +28,7 @@ "x": 19, "y": 11, "z": 5.78, - "geometryDefinitionId": "15mlconicalWell" + "geometryDefinitionId": "conicalWell" }, "A2": { "totalLiquidVolume": 15000, @@ -38,7 +38,7 @@ "x": 42, "y": 74, "z": 5.78, - "geometryDefinitionId": "15mlconicalWell" + "geometryDefinitionId": "conicalWell" }, "B2": { "totalLiquidVolume": 15000, @@ -48,7 +48,7 @@ "x": 42, "y": 42.5, "z": 5.78, - "geometryDefinitionId": "15mlconicalWell" + "geometryDefinitionId": "conicalWell" }, "C2": { "totalLiquidVolume": 15000, @@ -58,7 +58,7 @@ "x": 42, "y": 11, "z": 5.78, - "geometryDefinitionId": "15mlconicalWell" + "geometryDefinitionId": "conicalWell" }, "A3": { "totalLiquidVolume": 50000, @@ -68,7 +68,7 @@ "x": 70.2, "y": 62.2, "z": 5.95, - "geometryDefinitionId": "50mlconicalWell" + "geometryDefinitionId": "conicalWell" }, "B3": { "totalLiquidVolume": 50000, @@ -78,7 +78,7 @@ "x": 70.2, "y": 16.8, "z": 5.95, - "geometryDefinitionId": "50mlconicalWell" + "geometryDefinitionId": "conicalWell" }, "A4": { "totalLiquidVolume": 50000, @@ -88,7 +88,7 @@ "x": 106.1, "y": 62.2, "z": 5.95, - "geometryDefinitionId": "50mlconicalWell" + "geometryDefinitionId": "conicalWell" }, "B4": { "totalLiquidVolume": 50000, @@ -98,7 +98,7 @@ "x": 106.1, "y": 16.8, "z": 5.95, - "geometryDefinitionId": "50mlconicalWell" + "geometryDefinitionId": "conicalWell" } }, "brand": { @@ -170,61 +170,8 @@ "z": 0 }, "innerLabwareGeometry": { - "15mlconicalWell": { - "sections": [ - { - "shape": "spherical", - "radiusOfCurvature": 2.9, - "topHeight": 0.8, - "bottomHeight": 0.0 - }, - { - "shape": "conical", - "bottomDiameter": 4, - "topDiameter": 13.5, - "topHeight": 20.7, - "bottomHeight": 0.8 - }, - { - "shape": "conical", - "bottomDiameter": 13.5, - "topDiameter": 14.5, - "topHeight": 108.6, - "bottomHeight": 20.7 - }, - { - "shape": "conical", - "bottomDiameter": 14.5, - "topDiameter": 14.7, - "topHeight": 118.2, - "bottomHeight": 108.6 - } - ] - }, - "50mlconicalWell": { - "sections": [ - { - "shape": "conical", - "bottomDiameter": 6.15, - "topDiameter": 26.18, - "topHeight": 14.3, - "bottomHeight": 0.0 - }, - { - "shape": "conical", - "bottomDiameter": 26.18, - "topDiameter": 27.52, - "topHeight": 100.65, - "bottomHeight": 14.3 - }, - { - "shape": "conical", - "bottomDiameter": 27.52, - "topDiameter": 27.81, - "topHeight": 112.85, - "bottomHeight": 100.65 - } - ] + "conicalWell": { + "sections": [] } } } diff --git a/shared-data/labware/definitions/3/opentrons_96_pcr_adapter_armadillo_wellplate_200ul/2.json b/shared-data/labware/definitions/3/opentrons_96_pcr_adapter_armadillo_wellplate_200ul/2.json index 3ac68ba32be..86c28b5cbf9 100644 --- a/shared-data/labware/definitions/3/opentrons_96_pcr_adapter_armadillo_wellplate_200ul/2.json +++ b/shared-data/labware/definitions/3/opentrons_96_pcr_adapter_armadillo_wellplate_200ul/2.json @@ -1122,28 +1122,7 @@ ], "innerLabwareGeometry": { "conicalWell": { - "sections": [ - { - "shape": "spherical", - "radiusOfCurvature": 1.25, - "topHeight": 0.8, - "bottomHeight": 0.0 - }, - { - "shape": "conical", - "topDiameter": 5.5, - "bottomDiameter": 2.33, - "topHeight": 11.35, - "bottomHeight": 0.8 - }, - { - "shape": "conical", - "bottomDiameter": 5.5, - "topDiameter": 5.5, - "topHeight": 14.95, - "bottomHeight": 11.35 - } - ] + "sections": [] } } } diff --git a/shared-data/labware/definitions/3/usascientific_12_reservoir_22ml/2.json b/shared-data/labware/definitions/3/usascientific_12_reservoir_22ml/2.json index d17d27c041a..6a8375138d5 100644 --- a/shared-data/labware/definitions/3/usascientific_12_reservoir_22ml/2.json +++ b/shared-data/labware/definitions/3/usascientific_12_reservoir_22ml/2.json @@ -203,27 +203,7 @@ }, "innerLabwareGeometry": { "cuboidalWell": { - "sections": [ - { - "shape": "squaredcone", - "bottomCrossSection": "circular", - "circleDiameter": 2.5, - "rectangleXDimension": 7.98, - "rectangleYDimension": 70.98, - "topHeight": 4.05, - "bottomHeight": 0.0, - "yCount": 8 - }, - { - "shape": "cuboidal", - "topXDimension": 8.34, - "topYDimension": 71.85, - "bottomXDimension": 7.98, - "bottomYDimension": 70.98, - "topHeight": 41.75, - "bottomHeight": 4.05 - } - ] + "sections": [] } } } diff --git a/shared-data/labware/schemas/3.json b/shared-data/labware/schemas/3.json index eef1252c419..e38c070919a 100644 --- a/shared-data/labware/schemas/3.json +++ b/shared-data/labware/schemas/3.json @@ -83,12 +83,6 @@ }, "bottomHeight": { "type": "number" - }, - "xCount": { - "type": "integer" - }, - "yCount": { - "type": "integer" } } }, @@ -118,12 +112,6 @@ }, "bottomHeight": { "type": "number" - }, - "xCount": { - "type": "integer" - }, - "yCount": { - "type": "integer" } } }, @@ -161,12 +149,6 @@ }, "bottomHeight": { "type": "number" - }, - "xCount": { - "type": "integer" - }, - "yCount": { - "type": "integer" } } }, @@ -205,12 +187,6 @@ }, "bottomHeight": { "type": "number" - }, - "xCount": { - "type": "integer" - }, - "yCount": { - "type": "integer" } } }, @@ -249,12 +225,6 @@ }, "bottomHeight": { "type": "number" - }, - "xCount": { - "type": "integer" - }, - "yCount": { - "type": "integer" } } }, diff --git a/shared-data/python/opentrons_shared_data/labware/labware_definition.py b/shared-data/python/opentrons_shared_data/labware/labware_definition.py index 3363c874c55..d82a76d55c4 100644 --- a/shared-data/python/opentrons_shared_data/labware/labware_definition.py +++ b/shared-data/python/opentrons_shared_data/labware/labware_definition.py @@ -256,21 +256,6 @@ class SphericalSegment(BaseModel): ..., description="Height of the bottom of the segment, must be 0.0", ) - xCount: _StrictNonNegativeInt = Field( - default=1, - description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells", - ) - yCount: _StrictNonNegativeInt = Field( - default=1, - description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells", - ) - - @cached_property - def count(self) -> int: - return self.xCount * self.yCount - - class Config: - keep_untouched = (cached_property,) class ConicalFrustum(BaseModel): @@ -291,21 +276,6 @@ class ConicalFrustum(BaseModel): ..., description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well", ) - xCount: _StrictNonNegativeInt = Field( - default=1, - description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells", - ) - yCount: _StrictNonNegativeInt = Field( - default=1, - description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells", - ) - - @cached_property - def count(self) -> int: - return self.xCount * self.yCount - - class Config: - keep_untouched = (cached_property,) class CuboidalFrustum(BaseModel): @@ -335,21 +305,6 @@ class CuboidalFrustum(BaseModel): ..., description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well", ) - xCount: _StrictNonNegativeInt = Field( - default=1, - description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells", - ) - yCount: _StrictNonNegativeInt = Field( - default=1, - description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells", - ) - - @cached_property - def count(self) -> int: - return self.xCount * self.yCount - - class Config: - keep_untouched = (cached_property,) # A squared cone is the intersection of a cube and a cone that both @@ -399,14 +354,6 @@ class SquaredConeSegment(BaseModel): ..., description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well", ) - xCount: _StrictNonNegativeInt = Field( - default=1, - description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells", - ) - yCount: _StrictNonNegativeInt = Field( - default=1, - description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells", - ) @staticmethod def _area_trap_points( @@ -486,10 +433,6 @@ def height_to_volume_table(self) -> Dict[float, float]: def volume_to_height_table(self) -> Dict[float, float]: return dict((v, k) for k, v in self.height_to_volume_table.items()) - @cached_property - def count(self) -> int: - return self.xCount * self.yCount - class Config: keep_untouched = (cached_property,) @@ -603,21 +546,6 @@ class RoundedCuboidSegment(BaseModel): ..., description="The height at the bottom of a bounded subsection of a well, relative to the bottom of the well", ) - xCount: _StrictNonNegativeInt = Field( - default=1, - description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells", - ) - yCount: _StrictNonNegativeInt = Field( - default=1, - description="Number of instances of this shape in the stackup, used for wells that have multiple sub-wells", - ) - - @cached_property - def count(self) -> int: - return self.xCount * self.yCount - - class Config: - keep_untouched = (cached_property,) class Metadata1(BaseModel): diff --git a/step-generation/src/commandCreators/atomic/dispense.ts b/step-generation/src/commandCreators/atomic/dispense.ts index c009ce34403..c06e0035f7b 100644 --- a/step-generation/src/commandCreators/atomic/dispense.ts +++ b/step-generation/src/commandCreators/atomic/dispense.ts @@ -221,7 +221,7 @@ export const dispense: CommandCreator = ( }, flowRate, // pushOut will always be undefined in step-generation for now - // since there is no easy way to allow users to for it in PD + // since there is no easy way to allow users to select a volume for it in PD }, ...(isAirGap && { meta: { isAirGap } }), }, diff --git a/step-generation/src/commandCreators/compound/transfer.ts b/step-generation/src/commandCreators/compound/transfer.ts index e892dc94b3d..b6d8a508ec2 100644 --- a/step-generation/src/commandCreators/compound/transfer.ts +++ b/step-generation/src/commandCreators/compound/transfer.ts @@ -263,7 +263,7 @@ export const transfer: CommandCreator = ( ? [ curryCommandCreator(configureForVolume, { pipetteId: args.pipette, - volume: chunksPerSubTransfer, + volume: args.volume, }), ] : []